Files
rutero/route-planner/src/app/private/employees/employees.component.ts
T
EidanThen c45182012c feat:
- conexion supabase
- autenticacion solo correos glm
- pantalla gestor empleados
2026-09-05 12:42:29 -04:00

273 lines
10 KiB
TypeScript

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<EmployeeInput, 'activo'> & { 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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<string, unknown>));
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<void> {
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<void> {
this.destroyed = true;
this.loadSequence++;
this.employees = [];
this.loading = true;
await this.auth.signOut();
await this.router.navigate(['/login']);
}
async goHome(): Promise<void> {
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<string, unknown>, 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
};
}
}