import { Component, ChangeDetectorRef, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { RouteAssignedResponse } from '../../../core/models/route.model'; import { saveAs } from 'file-saver'; import { Table } from 'primeng/table'; import { TableModule } from 'primeng/table'; import { InputTextModule } from 'primeng/inputtext'; import { TooltipModule } from 'primeng/tooltip'; import { RouteState } from '../../../core/route-state/route'; import { RutasService } from '../../../core/services/router.service'; import { PlanningInputService } from '../../../core/services/planning-input.service'; @Component({ standalone: true, selector: 'app-assigned-route', imports: [ CommonModule, FormsModule, TableModule, InputTextModule, TooltipModule ], templateUrl: './assigned-route.component.html', styleUrl: './assigned-route.component.css' }) export class AssignedRouteComponent { @ViewChild('dt') table!: Table; rutasExpandido = false; tablaRutas = ''; assignedRoutes: RouteAssignedResponse[] = []; isLoading = false; cols: any[] = []; editingRoute: RouteAssignedResponse | null = null; editUserId = ''; editDay = ''; hasLocalChanges = false; editError: string | null = null; reoptLoading = false; reoptError: string | null = null; constructor( private routesState: RouteState, private cdr: ChangeDetectorRef, private router: Router, private rutasService: RutasService, private planningInput: PlanningInputService ) { } private subs: import('rxjs').Subscription[] = []; ngOnInit() { this.routesState.resetLoading(); this.subs.push( this.routesState.loading$.subscribe(value => { this.isLoading = value; this.cdr.detectChanges(); }) ); this.subs.push( this.routesState.assignedRoutes$.subscribe(data => { this.assignedRoutes = [...data]; this.cdr.detectChanges(); }) ); this.cols = [ { field: 'json.nombre_usuario', header: 'Usuario' }, { field: 'json.nombre_pdv', header: 'PDV' }, { field: 'json.hora_laboral_semanal_usuario', header: 'Sem. Usuario (hr)' }, { field: 'json.hora_minima_semanal_pdv', header: 'Horas Min. PDV' }, { field: 'json.horas_trabajo', header: 'Horas Trabajo' }, { field: 'json.dia', header: 'Día' }, { field: 'json.distancia', header: 'Distancia (km)' }, { field: 'json.horas_desplazamiento', header: 'Duración (min)' }, { field: '', header: '' } ]; } onShowMap(row: RouteAssignedResponse) { this.routesState.sendRoute(row); this.router.navigate(['/home/assigned-route'], { queryParams: { pdv_id: row.json.pdv_id, usuario_id: row.json.usuario_id, dia: row.json.dia } }); } goToOverviewMap() { // If table has filtered view, use first visible row's usuario as default; else first assigned const target = (this.table?.filteredValue?.[0] as RouteAssignedResponse | undefined) ?? this.assignedRoutes[0]; if (target) { this.router.navigate(['/home/mapa'], { queryParams: { usuario_id: target.json.usuario_id, dia: 'Todos' } }); } else { this.router.navigate(['/home/mapa']); } } goToUserMap(usuarioId: string) { this.router.navigate(['/home/mapa'], { queryParams: { usuario_id: usuarioId, dia: 'Todos' } }); } get userOptions(): { id: string; name: string }[] { const users = new Map(); for (const route of this.assignedRoutes) users.set(String(route.json.usuario_id), route.json.nombre_usuario); return Array.from(users, ([id, name]) => ({ id, name })); } get dayOptions(): string[] { return this.routesState.getDiasUnicos(); } startEdit(route: RouteAssignedResponse): void { this.editingRoute = route; this.editUserId = String(route.json.usuario_id); this.editDay = route.json.dia; this.editError = null; } cancelEdit(): void { this.editingRoute = null; } saveEdit(): void { if (!this.editingRoute || !this.editUserId || !this.editDay) return; const capacity = this.routesState.canAssignToDay( this.editUserId, this.editDay, Number(this.editingRoute.json.horas_trabajo || this.editingRoute.json.hora_minima_semanal_pdv || 0), this.editingRoute ); if (!capacity.allowed) { this.editError = `La asignación excede la capacidad diaria (${capacity.totalHours.toFixed(1)} h de ${capacity.capacityHours.toFixed(1)} h).`; return; } this.routesState.updateAssignedRoute(this.editingRoute, this.editUserId, this.editDay); this.hasLocalChanges = true; this.editingRoute = null; } get canReoptimize(): boolean { return this.assignedRoutes.length > 0 && !!this.planningInput.getFormData() && !this.reoptLoading; } reoptimizeLocally(): void { const data = this.planningInput.getFormData(); if (!data || !this.assignedRoutes.length || this.reoptLoading) return; this.reoptLoading = true; this.reoptError = null; this.routesState.setLoading(true); this.rutasService.reoptimizarRutas(data).subscribe({ next: response => { const items = response as unknown as unknown[]; const assigned = items.find(item => item && typeof item === 'object' && 'assigned' in item) as { assigned?: unknown } | undefined; const noAssigned = items.find(item => item && typeof item === 'object' && 'noAssigned' in item) as { noAssigned?: unknown } | undefined; if (assigned?.assigned) this.routesState.setAssignedRoutes(assigned.assigned as never); if (noAssigned?.noAssigned) this.routesState.setNoAssignedRoutes(noAssigned.noAssigned as never); this.hasLocalChanges = false; this.reoptLoading = false; this.routesState.setLoading(false); this.cdr.detectChanges(); }, error: err => { this.reoptError = err?.error?.error || err?.message || 'No se pudo reoptimizar la planificación'; this.reoptLoading = false; this.routesState.setLoading(false); this.cdr.detectChanges(); } }); this.editingRoute = null; } restoreOriginal(): void { this.routesState.restoreOriginalAssignedRoutes(); this.hasLocalChanges = false; this.editingRoute = null; } toggleRutas() { this.rutasExpandido = !this.rutasExpandido; } ngOnDestroy() { this.subs.forEach(s => s.unsubscribe()); } downloadCSV() { const headers = [ 'Usuario ID', 'Usuario', 'Long Usuario', 'Lat Usuario', 'PDV ID', 'PDV', 'Long PDV', 'Lat PDV', 'Horas Usuario', 'Horas PDV', 'Horas Trabajo', 'Día', 'Distancia', 'Duracion' ]; const data = this.table.filteredValue && this.table.filteredValue.length ? this.table.filteredValue : this.assignedRoutes; const rows = data.map(r => [ r.json.usuario_id, r.json.nombre_usuario, r.json.longitud_usuario, r.json.latitud_usuario, r.json.pdv_id, r.json.nombre_pdv, r.json.longitud_pdv, r.json.latitud_pdv, r.json.hora_laboral_semanal_usuario, r.json.hora_minima_semanal_pdv, r.json.horas_trabajo, r.json.dia, r.json.distancia, r.json.horas_desplazamiento ]); const csvContent = [headers, ...rows].map(r => r.join(',')).join('\n'); const bom = '\uFEFF'; const blob = new Blob([bom + csvContent], { type: 'text/csv;charset=utf-8;' }); saveAs(blob, 'rutas_planificadas.csv'); } }