c45182012c
- conexion supabase - autenticacion solo correos glm - pantalla gestor empleados
273 lines
10 KiB
TypeScript
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
|
|
};
|
|
}
|
|
}
|