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

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