feat:
- conexion supabase - autenticacion solo correos glm - pantalla gestor empleados
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user