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
@@ -1,36 +1,49 @@
import { Component, ChangeDetectorRef } from '@angular/core';
import { Component, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TableModule } from 'primeng/table';
import { RouteNoAssignedResponse } from '../../../core/models/route.model';
import { RouteState } from '../../../core/route-state/route';
import { Subscription } from 'rxjs';
import { OrsService } from '../../../core/services/ors.service';
@Component({
standalone: true,
selector: 'app-no-assigned-route',
imports: [CommonModule, TableModule],
imports: [CommonModule, FormsModule, TableModule],
templateUrl: './no-assigned-route.component.html',
styleUrls: ['./no-assigned-route.component.css']
})
export class NoAssignedRouteComponent {
export class NoAssignedRouteComponent implements OnDestroy {
noAssignedRoutes: RouteNoAssignedResponse[] = [];
isLoading = false;
cols: any[] = [];
cols: { field: string; header: string }[] = [];
private subs: Subscription[] = [];
assignmentUser: Record<string, string> = {};
assignmentDay: Record<string, string> = {};
assigningId: number | null = null;
actionError: string | null = null;
constructor(
private routesState: RouteState,
private cdr: ChangeDetectorRef
private cdr: ChangeDetectorRef,
private orsService: OrsService
) {}
ngOnInit() {
this.routesState.loading$.subscribe(value => {
this.isLoading = value;
this.cdr.detectChanges();
});
this.routesState.noAssignedRoutes$.subscribe(data => {
this.noAssignedRoutes = [...data];
this.cdr.detectChanges();
});
this.subs.push(
this.routesState.loading$.subscribe(value => {
this.isLoading = value;
this.cdr.detectChanges();
})
);
this.subs.push(
this.routesState.noAssignedRoutes$.subscribe(data => {
this.noAssignedRoutes = [...data];
this.cdr.detectChanges();
})
);
this.cols = [
{ field: 'pdv_id', header: 'PDV ID' },
{ field: 'pdv_nombre', header: 'Nombre' },
@@ -40,4 +53,45 @@ export class NoAssignedRouteComponent {
{ field: 'motivo', header: 'Motivo' }
];
}
get userOptions(): { id: string; name: string }[] {
return this.routesState.getUsuariosUnicos().map(user => ({ id: user.id, name: user.nombre }));
}
get dayOptions(): string[] {
return this.routesState.getDiasUnicos();
}
async assignRoute(route: RouteNoAssignedResponse): Promise<void> {
const userId = this.assignmentUser[String(route.pdv_id)];
const day = this.assignmentDay[String(route.pdv_id)];
if (!userId || !day) {
this.actionError = 'Selecciona usuario y día antes de asignar.';
return;
}
const user = this.routesState.getAssignedSnapshot().find(item => String(item.json.usuario_id) === userId);
if (!user) return;
this.assigningId = route.pdv_id;
this.actionError = null;
try {
const origin = [Number(user.json.longitud_usuario), Number(user.json.latitud_usuario)];
const destination = [Number(route.longitud), Number(route.latitud)];
const result = await this.orsService.processPairs([{ pdv_id: route.pdv_id, origin, destination }], 1, true);
const geo = (result[0] as { geojson?: { features?: { properties?: { summary?: { distance: number; duration: number } } }[] } }).geojson;
const summary = geo?.features?.[0]?.properties?.summary;
if (!summary) throw new Error('ORS no devolvió una ruta vial');
this.routesState.addAssignedFromUnassigned(route, userId, day, summary.distance / 1000, summary.duration / 3600);
delete this.assignmentUser[String(route.pdv_id)];
delete this.assignmentDay[String(route.pdv_id)];
} catch {
this.actionError = `No se pudo calcular la ruta vial para ${route.pdv_nombre}.`;
} finally {
this.assigningId = null;
this.cdr.detectChanges();
}
}
ngOnDestroy() {
this.subs.forEach(s => s.unsubscribe());
}
}