import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { Employee, EmployeeInput } from '../../core/models/employee.model'; import { EmployeeService } from '../../core/services/employee.service'; import { AuthService } from '../../core/services/auth.service'; import { WORK_SCHEDULES, workSchedule } from '../../core/models/work-schedule.model'; import { normalizeRow, cleanNum } from '../../shared/utils/header-normalization.utils'; import * as XLSX from 'xlsx'; type EmployeeForm = Omit & { activo: boolean }; @Component({ standalone: true, selector: 'app-employees', imports: [CommonModule, FormsModule], templateUrl: './employees.component.html', styleUrls: ['./employees.component.css'] }) export class EmployeesComponent implements OnDestroy, OnInit { employees: Employee[] = []; form = this.emptyForm(); editingId: string | null = null; loading = true; saving = false; error = ''; importMessage = ''; importing = false; openRestDaysFor: string | null = null; readonly schedules = WORK_SCHEDULES; private destroyed = false; private loadSequence = 0; constructor( private employeesService: EmployeeService, private auth: AuthService, private router: Router, private cdr: ChangeDetectorRef ) {} ngOnInit(): void { void this.load(); } ngOnDestroy(): void { this.destroyed = true; this.loadSequence++; } get activeEmployees(): number { return this.employees.filter(employee => employee.activo).length; } get selectedScheduleHours(): number { return workSchedule(this.form.jornada_codigo).weeklyHours; } async load(): Promise { const sequence = ++this.loadSequence; this.loading = true; this.error = ''; try { const employees = await this.employeesService.list(); if (this.destroyed || sequence !== this.loadSequence) return; this.employees = employees; this.cdr.detectChanges(); } catch (error: any) { if (!this.destroyed && sequence === this.loadSequence) { this.error = error?.message ?? 'No se pudieron cargar los empleados.'; } } finally { this.loading = false; if (!this.destroyed && sequence === this.loadSequence) this.cdr.detectChanges(); } } edit(employee: Employee): void { this.editingId = employee.id; } cancelEdit(): void { this.editingId = null; this.error = ''; void this.load(); } async saveInline(employee: Employee): Promise { const rest = Number(employee.horas_descanso_por_dia); const visits = Number(employee.visitas_maximas_por_dia); if (!Number.isInteger(rest) || rest < 0 || rest > 3 || !Number.isInteger(visits) || visits < 1) { this.error = 'Descanso/día debe ser un entero entre 0 y 3, y Visitas/día debe ser un entero mayor que 0.'; return; } this.saving = true; this.error = ''; try { await this.employeesService.update(employee.id, { ...employee, dias_por_semana: workSchedule(employee.jornada_codigo).days.length, horas_descanso_por_dia: rest, visitas_maximas_por_dia: visits, dias_descanso: (employee.dias_descanso ?? []).filter(day => workSchedule(employee.jornada_codigo).days.includes(day)) }); this.editingId = null; await this.load(); } catch (error: any) { this.error = error?.message ?? 'No se pudo actualizar el empleado.'; } finally { this.saving = false; } } onInlineScheduleChange(employee: Employee): void { employee.dias_descanso = (employee.dias_descanso ?? []).filter(day => workSchedule(employee.jornada_codigo).days.includes(day)); employee.dias_por_semana = workSchedule(employee.jornada_codigo).days.length; } async save(): Promise { if (!this.form.employee_code.trim() || !this.form.nombre.trim()) { this.error = 'El ID y el nombre son obligatorios.'; return; } if (this.form.horas_descanso_por_dia >= this.form.horas_por_dia) { this.error = 'Las horas de descanso deben ser menores que las horas de trabajo diarias.'; return; } this.saving = true; this.error = ''; const employee: EmployeeInput = { ...this.form, dias_por_semana: workSchedule(this.form.jornada_codigo).days.length, employee_code: this.form.employee_code.trim(), nombre: this.form.nombre.trim(), usuario: this.nullable(this.form.usuario), email: this.nullable(this.form.email), perfil: this.nullable(this.form.perfil), pais: this.nullable(this.form.pais), regional: this.nullable(this.form.regional) }; try { if (this.editingId) await this.employeesService.update(this.editingId, employee); else await this.employeesService.create(employee); this.cancelEdit(); await this.load(); } catch (error: any) { this.error = error?.message ?? 'No se pudo guardar el empleado.'; } finally { this.saving = false; } } async toggleActive(employee: Employee): Promise { try { await this.employeesService.update(employee.id, { ...employee, activo: !employee.activo }); await this.load(); } catch (error: any) { this.error = error?.message ?? 'No se pudo actualizar el estado.'; } } scheduleLabel(employee: Employee): string { return workSchedule(employee.jornada_codigo).label; } availableRestDays(employee: Employee): string[] { return workSchedule(employee.jornada_codigo).days; } toggleRestDayEditor(employee: Employee): void { this.openRestDaysFor = this.openRestDaysFor === employee.id ? null : employee.id; } async toggleRestDay(employee: Employee, day: string, checked: boolean): Promise { const days = new Set(employee.dias_descanso ?? []); checked ? days.add(day) : days.delete(day); employee.dias_descanso = Array.from(days); } async importFile(event: Event): Promise { const file = (event.target as HTMLInputElement).files?.[0]; if (!file) return; this.importing = true; this.importMessage = ''; this.error = ''; try { const workbook = XLSX.read(await file.arrayBuffer(), { type: 'array' }); const sheet = workbook.Sheets[workbook.SheetNames[0]]; const rows = XLSX.utils.sheet_to_json(sheet, { defval: null, raw: true }).map(row => normalizeRow(row as Record)); const employees = rows.map((row, index) => this.fromExcelRow(row, index + 2)); const invalid = employees.filter((employee): employee is null => employee === null).length; const valid = employees.filter((employee): employee is EmployeeInput => employee !== null); if (!valid.length) throw new Error('El archivo no contiene empleados válidos.'); await this.employeesService.upsert(valid); this.importMessage = `${valid.length} empleados guardados${invalid ? `; ${invalid} filas omitidas por datos inválidos` : ''}.`; await this.load(); } catch (error: any) { this.error = error?.message ?? 'No se pudo importar el archivo.'; } finally { this.importing = false; (event.target as HTMLInputElement).value = ''; } } async remove(employee: Employee): Promise { if (!window.confirm(`¿Eliminar a ${employee.nombre}? Esta acción no se puede deshacer.`)) return; try { await this.employeesService.remove(employee.id); await this.load(); } catch (error: any) { this.error = error?.message ?? 'No se pudo eliminar el empleado.'; } } async signOut(): Promise { this.destroyed = true; this.loadSequence++; this.employees = []; this.loading = true; await this.auth.signOut(); await this.router.navigate(['/login']); } async goHome(): Promise { await this.router.navigate(['/home']); } private nullable(value: string | null): string | null { return value?.trim() || null; } private emptyForm(): EmployeeForm { return { employee_code: '', nombre: '', usuario: '', email: '', perfil: 'Promotor', pais: 'Republica Dominicana', regional: '', latitud: 18.4861, longitud: -69.9312, horas_por_dia: 8, dias_por_semana: 5, jornada_codigo: 'LUN_VIE_40', horas_descanso_por_dia: 1, dias_descanso: [], visitas_maximas_por_dia: 3, activo: true }; } private fromExcelRow(row: Record, excelRow: number): EmployeeInput | null { const code = String(row['id'] ?? '').trim(); const nombre = String(row['nombre_del_empleado'] ?? '').trim(); const latitude = cleanNum(row['latitud']); const longitude = cleanNum(row['longitud']); const dailyHours = cleanNum(row['horas_por_dia_usuario']); const weeklyHours = cleanNum(row['horas_por_semana_usuario']); if (!code || !nombre || !Number.isFinite(latitude) || !Number.isFinite(longitude) || !Number.isFinite(dailyHours)) return null; const jornadaTexto = String(row['jornada'] ?? '').trim(); const jornadaCodigo = this.schedules.some(schedule => schedule.code === jornadaTexto) ? jornadaTexto : weeklyHours === 44 ? 'LUN_SAB_44' : 'LUN_VIE_40'; const restDays = String(row['dias_descanso'] ?? '').split(',').map(day => day.trim()).filter(day => workSchedule(jornadaCodigo).days.includes(day)); const active = !['no', 'n', '0', 'false'].includes(String(row['usuario_activo'] ?? 'si').trim().toLowerCase()); return { employee_code: code, nombre, usuario: this.nullable(String(row['usuario'] ?? '')), email: this.nullable(String(row['email'] ?? '')), perfil: this.nullable(String(row['perfil_de_acceso'] ?? '')), pais: this.nullable(String(row['pais'] ?? '')), regional: this.nullable(String(row['regionales'] ?? '')), latitud: latitude, longitud: longitude, horas_por_dia: dailyHours, dias_por_semana: workSchedule(jornadaCodigo).days.length, jornada_codigo: jornadaCodigo, horas_descanso_por_dia: Math.min(3, Math.max(0, Math.round(cleanNum(row['horas_descanso_por_dia']) || 1))), dias_descanso: restDays, visitas_maximas_por_dia: Math.max(1, cleanNum(row['visitas_maximas_por_dia']) || 3), activo: active }; } }