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 { 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; 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 = ''; } }