c45182012c
- conexion supabase - autenticacion solo correos glm - pantalla gestor empleados
276 lines
9.9 KiB
TypeScript
276 lines
9.9 KiB
TypeScript
import { Component, ChangeDetectorRef, OnInit } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { AssignedRouteComponent } from './assigned-route/assigned-route.component';
|
|
import { NoAssignedRouteComponent } from './no-assigned-route/no-assigned-route.component';
|
|
import { RutasService } from '../../core/services/router.service';
|
|
import { RouteState } from '../../core/route-state/route';
|
|
import { ApiResponse } from '../../core/models/route.model';
|
|
import { ExcelPreviewService, PreviewResult } from '../../shared/services/excel-preview.service';
|
|
import { Subscription } from 'rxjs';
|
|
import { PlanningKpisComponent } from './planning-kpis/planning-kpis.component';
|
|
import { PlanningInputService } from '../../core/services/planning-input.service';
|
|
import { RouterLink } from '@angular/router';
|
|
import { EmployeeService } from '../../core/services/employee.service';
|
|
|
|
@Component({
|
|
standalone: true,
|
|
selector: 'app-home',
|
|
imports: [CommonModule, FormsModule, RouterLink, AssignedRouteComponent, NoAssignedRouteComponent, PlanningKpisComponent],
|
|
templateUrl: './home.component.html',
|
|
styleUrls: ['./home.component.css']
|
|
})
|
|
export class HomeComponent implements OnInit {
|
|
pdvFile: File | null = null;
|
|
usuariosFile: File | null = null;
|
|
pdvPreview: PreviewResult | null = null;
|
|
usuariosPreview: PreviewResult | null = null;
|
|
pdvPreviewLoading = false;
|
|
usuariosPreviewLoading = false;
|
|
activeEmployees = 0;
|
|
employeesLoading = true;
|
|
uploadError: string | null = null;
|
|
isLoading = false;
|
|
private calcSub: Subscription | null = null;
|
|
|
|
constructor(
|
|
private rutasService: RutasService,
|
|
private routeState: RouteState,
|
|
private previewService: ExcelPreviewService,
|
|
private planningInput: PlanningInputService,
|
|
private employeesService: EmployeeService,
|
|
private cdr: ChangeDetectorRef
|
|
) {}
|
|
|
|
ngOnInit(): void {
|
|
void this.loadEmployees();
|
|
}
|
|
|
|
private async loadEmployees(): Promise<void> {
|
|
this.employeesLoading = true;
|
|
try {
|
|
this.activeEmployees = (await this.employeesService.list()).filter(employee => employee.activo).length;
|
|
} catch {
|
|
this.activeEmployees = 0;
|
|
} finally {
|
|
this.employeesLoading = false;
|
|
this.cdr.detectChanges();
|
|
}
|
|
}
|
|
|
|
async onPdvFileChange(event: Event) {
|
|
const input = event.target as HTMLInputElement;
|
|
if (input.files && input.files.length > 0) {
|
|
const file = input.files[0];
|
|
const basicError = this.validateFileBasic(file, 'PDV');
|
|
if (basicError) {
|
|
this.pdvFile = null;
|
|
this.pdvPreview = null;
|
|
this.uploadError = basicError;
|
|
return;
|
|
}
|
|
this.pdvFile = file;
|
|
if (this.usuariosFile) this.planningInput.setFiles(file, this.usuariosFile);
|
|
this.uploadError = null;
|
|
this.pdvPreviewLoading = true;
|
|
this.pdvPreview = null;
|
|
this.cdr.detectChanges();
|
|
try {
|
|
this.pdvPreview = await this.previewService.previewFile(file, 'PDV');
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : 'Error al leer el archivo PDV';
|
|
this.pdvPreview = {
|
|
fileName: file.name,
|
|
type: 'PDV',
|
|
valid: false,
|
|
totalRows: 0,
|
|
validRows: 0,
|
|
headers: [],
|
|
normalizedHeaders: [],
|
|
missingHeaders: [],
|
|
errors: [{ row: 1, field: 'file', message: msg }],
|
|
duplicateIds: [],
|
|
sampleValidRows: [],
|
|
};
|
|
} finally {
|
|
this.pdvPreviewLoading = false;
|
|
this.cdr.detectChanges();
|
|
}
|
|
}
|
|
}
|
|
|
|
async onUsuariosFileChange(event: Event) {
|
|
const input = event.target as HTMLInputElement;
|
|
if (input.files && input.files.length > 0) {
|
|
const file = input.files[0];
|
|
const basicError = this.validateFileBasic(file, 'Usuarios');
|
|
if (basicError) {
|
|
this.usuariosFile = null;
|
|
this.usuariosPreview = null;
|
|
this.uploadError = basicError;
|
|
return;
|
|
}
|
|
this.usuariosFile = file;
|
|
if (this.pdvFile) this.planningInput.setFiles(this.pdvFile, file);
|
|
this.uploadError = null;
|
|
this.usuariosPreviewLoading = true;
|
|
this.usuariosPreview = null;
|
|
this.cdr.detectChanges();
|
|
try {
|
|
this.usuariosPreview = await this.previewService.previewFile(file, 'Usuarios');
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : 'Error al leer el archivo Usuarios';
|
|
this.usuariosPreview = {
|
|
fileName: file.name,
|
|
type: 'Usuarios',
|
|
valid: false,
|
|
totalRows: 0,
|
|
validRows: 0,
|
|
headers: [],
|
|
normalizedHeaders: [],
|
|
missingHeaders: [],
|
|
errors: [{ row: 1, field: 'file', message: msg }],
|
|
duplicateIds: [],
|
|
sampleValidRows: [],
|
|
};
|
|
} finally {
|
|
this.usuariosPreviewLoading = false;
|
|
this.cdr.detectChanges();
|
|
}
|
|
}
|
|
}
|
|
|
|
private validateFileBasic(file: File, type: string): string | null {
|
|
const validExtensions = ['.xlsx', '.xls'];
|
|
const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
|
|
if (!validExtensions.includes(ext)) {
|
|
return `El archivo ${type} debe ser .xlsx o .xls`;
|
|
}
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
return `El archivo ${type} supera 10MB`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
get canSubmit(): boolean {
|
|
if (!this.pdvFile || !this.activeEmployees) return false;
|
|
if (this.pdvPreviewLoading || this.employeesLoading) return false;
|
|
if (this.isLoading) return false;
|
|
if (this.uploadError) return false;
|
|
// Require valid previews before enabling submit
|
|
if (this.pdvPreview && !this.pdvPreview.valid) return false;
|
|
if (!this.pdvPreview) return false;
|
|
return true;
|
|
}
|
|
|
|
get submitDisabledReason(): string | null {
|
|
if (!this.pdvFile) return 'Sube el archivo PDV';
|
|
if (this.employeesLoading) return 'Cargando empleados...';
|
|
if (!this.activeEmployees) return 'No hay empleados activos. Regístralos en Gestionar empleados.';
|
|
if (this.pdvPreviewLoading) return 'Validando archivo PDV...';
|
|
if (this.pdvPreview && !this.pdvPreview.valid) return 'Corrige los errores del archivo PDV';
|
|
return null;
|
|
}
|
|
|
|
async cargarRutas() {
|
|
if (!this.canSubmit) return;
|
|
|
|
this.isLoading = true;
|
|
this.uploadError = null;
|
|
this.routeState.setLoading(true);
|
|
let pdvForOptimizer: File;
|
|
try {
|
|
pdvForOptimizer = await this.previewService.preparePdvForOptimizer(this.pdvFile!);
|
|
} catch {
|
|
this.uploadError = 'No se pudo preparar el archivo PDV para el optimizador.';
|
|
this.routeState.setLoading(false);
|
|
this.isLoading = false;
|
|
this.cdr.detectChanges();
|
|
return;
|
|
}
|
|
let employeesFile: File;
|
|
try {
|
|
employeesFile = await this.employeesService.activeEmployeesFile();
|
|
} catch {
|
|
this.uploadError = 'No se pudieron obtener los empleados activos.';
|
|
this.routeState.setLoading(false);
|
|
this.isLoading = false;
|
|
return;
|
|
}
|
|
this.planningInput.setFiles(pdvForOptimizer, employeesFile);
|
|
|
|
const formData = new FormData();
|
|
formData.append('pdv_file', pdvForOptimizer, pdvForOptimizer.name);
|
|
formData.append('usuarios_file', employeesFile, employeesFile.name);
|
|
|
|
this.calcSub?.unsubscribe();
|
|
this.calcSub = this.rutasService.calcularRutas(formData).subscribe({
|
|
next: (res: ApiResponse) => {
|
|
// ApiResponse is [AssignedResponse, NoAssignedResponse] but backend may return object with error
|
|
const anyRes = res as unknown as Record<string, unknown>;
|
|
if (anyRes && 'error' in anyRes) {
|
|
this.uploadError = String((anyRes as { error: unknown }).error);
|
|
this.routeState.setLoading(false);
|
|
this.isLoading = false;
|
|
this.cdr.detectChanges();
|
|
return;
|
|
}
|
|
const arr = res as unknown as unknown[];
|
|
const assigned = (arr as ApiResponse).find(r => r && typeof r === 'object' && 'assigned' in r) as unknown as { assigned: unknown } | undefined;
|
|
const noAssigned = (arr as ApiResponse).find(r => r && typeof r === 'object' && 'noAssigned' in r) as unknown as { noAssigned: unknown } | undefined;
|
|
if (assigned) {
|
|
this.routeState.setAssignedRoutes(assigned.assigned as never);
|
|
}
|
|
if (noAssigned) {
|
|
this.routeState.setNoAssignedRoutes(noAssigned.noAssigned as never);
|
|
}
|
|
this.routeState.setLoading(false);
|
|
this.isLoading = false;
|
|
this.cdr.detectChanges();
|
|
},
|
|
error: (err) => {
|
|
this.uploadError = this.mapBackendError(err);
|
|
this.routeState.setLoading(false);
|
|
this.isLoading = false;
|
|
this.cdr.detectChanges();
|
|
}
|
|
});
|
|
}
|
|
|
|
cancelarCarga() {
|
|
this.calcSub?.unsubscribe();
|
|
this.calcSub = null;
|
|
this.isLoading = false;
|
|
this.routeState.setLoading(false);
|
|
this.uploadError = 'Cálculo cancelado';
|
|
}
|
|
|
|
private mapBackendError(err: unknown): string {
|
|
const anyErr = err as { error?: { error?: string; message?: string }; message?: string; status?: number; name?: string };
|
|
if (anyErr?.name === 'TimeoutError') return 'El cálculo tardó demasiado (timeout 30s). Intenta de nuevo con menos filas.';
|
|
if (anyErr?.error?.error) return anyErr.error.error;
|
|
if (anyErr?.error?.message) return anyErr.error.message;
|
|
if (anyErr?.message) return anyErr.message;
|
|
return 'Error al procesar los archivos';
|
|
}
|
|
|
|
clearFiles() {
|
|
this.calcSub?.unsubscribe();
|
|
this.planningInput.clear();
|
|
this.routeState.clearAll();
|
|
this.pdvFile = null;
|
|
this.usuariosFile = null;
|
|
this.pdvPreview = null;
|
|
this.usuariosPreview = null;
|
|
this.pdvPreviewLoading = false;
|
|
this.usuariosPreviewLoading = false;
|
|
this.uploadError = null;
|
|
this.isLoading = false;
|
|
this.routeState.setLoading(false);
|
|
const pdvInput = document.getElementById('pdvFile') as HTMLInputElement;
|
|
const usuariosInput = document.getElementById('usuariosFile') as HTMLInputElement;
|
|
if (pdvInput) pdvInput.value = '';
|
|
if (usuariosInput) usuariosInput.value = '';
|
|
}
|
|
}
|