feat: implement route planning dashboard including KPIs, user workload tracking, and route assignment management features

This commit is contained in:
2026-08-29 11:55:03 -04:00
parent a0ea3fea48
commit cd5c57c5ee
41 changed files with 3811 additions and 151 deletions
@@ -9,6 +9,8 @@ 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,
@@ -30,23 +32,37 @@ export class AssignedRouteComponent {
assignedRoutes: RouteAssignedResponse[] = [];
isLoading = false;
cols: any[] = [];
editingRoute: RouteAssignedResponse | null = null;
editUserId = '';
editDay = '';
hasLocalChanges = false;
reoptLoading = false;
reoptError: string | null = null;
constructor(
private routesState: RouteState,
private cdr: ChangeDetectorRef,
private router: Router
private router: Router,
private rutasService: RutasService,
private planningInput: PlanningInputService
) { }
private subs: import('rxjs').Subscription[] = [];
ngOnInit() {
this.routesState.resetLoading();
this.routesState.loading$.subscribe(value => {
this.isLoading = value;
this.cdr.detectChanges();
});
this.routesState.assignedRoutes$.subscribe(data => {
this.assignedRoutes = [...data];
this.cdr.detectChanges();
});
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' },
@@ -60,14 +76,101 @@ export class AssignedRouteComponent {
];
}
onShowMap(row: RouteAssignedResponse) {
onShowMap(row: RouteAssignedResponse) {
this.routesState.sendRoute(row);
this.router.navigate(['/home/assigned-route']);
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<string, string>();
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;
}
cancelEdit(): void {
this.editingRoute = null;
}
saveEdit(): void {
if (!this.editingRoute || !this.editUserId || !this.editDay) 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',
@@ -119,4 +222,4 @@ export class AssignedRouteComponent {
saveAs(blob, 'rutas_planificadas.csv');
}
}
}