feat: implement route assignment management and sequencing module with interactive data visualization and editing capabilities
This commit is contained in:
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { RouteState } from '../../../core/route-state/route';
|
||||
import { PlanningExportService } from '../../../shared/services/planning-export.service';
|
||||
import { summarizeDailyRoutes } from '../../../shared/utils/route-sequencing.utils';
|
||||
import { RouteAssignedResponse } from '../../../core/models/route.model';
|
||||
|
||||
interface KpiSummary {
|
||||
@@ -28,12 +29,15 @@ interface ReasonSummary {
|
||||
}
|
||||
|
||||
interface UserLoad {
|
||||
id: string;
|
||||
name: string;
|
||||
routes: number;
|
||||
distance: number;
|
||||
workHours: number;
|
||||
capacityHours: number;
|
||||
utilization: number;
|
||||
status: 'ok' | 'warning' | 'over-capacity';
|
||||
alertMessage: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -71,6 +75,10 @@ export class PlanningKpisComponent implements OnInit, OnDestroy {
|
||||
return Math.min(Math.max(value, 0), 100);
|
||||
}
|
||||
|
||||
utilizationClass(load: UserLoad): string {
|
||||
return load.status;
|
||||
}
|
||||
|
||||
exportPlanning(): void {
|
||||
this.exportService.export();
|
||||
}
|
||||
@@ -79,9 +87,10 @@ export class PlanningKpisComponent implements OnInit, OnDestroy {
|
||||
const assigned = this.routeState.getAssignedSnapshot();
|
||||
const unassigned = this.routeState.getNoAssignedSnapshot();
|
||||
const totalPdvs = assigned.length + unassigned.length;
|
||||
const totalDistance = assigned.reduce((sum, route) => sum + this.numberValue(route.json.distancia), 0);
|
||||
const totalTravelHours = assigned.reduce((sum, route) => sum + this.numberValue(route.json.horas_desplazamiento), 0);
|
||||
const totalWorkHours = assigned.reduce((sum, route) => sum + this.parseHours(route.json.horas_trabajo), 0);
|
||||
const dailyRoutes = summarizeDailyRoutes(assigned);
|
||||
const totalDistance = dailyRoutes.reduce((sum, day) => sum + day.distanceKm, 0);
|
||||
const totalTravelHours = dailyRoutes.reduce((sum, day) => sum + day.travelHours, 0);
|
||||
const totalWorkHours = dailyRoutes.reduce((sum, day) => sum + day.workHours, 0);
|
||||
const users = new Set(assigned.map(route => String(route.json.usuario_id))).size;
|
||||
const days = new Set(assigned.map(route => route.json.dia)).size;
|
||||
const loads = new Map<string, UserLoad>();
|
||||
@@ -89,25 +98,34 @@ export class PlanningKpisComponent implements OnInit, OnDestroy {
|
||||
for (const route of assigned) {
|
||||
const key = String(route.json.usuario_id);
|
||||
const current = loads.get(key) ?? {
|
||||
id: key,
|
||||
name: route.json.nombre_usuario,
|
||||
routes: 0,
|
||||
distance: 0,
|
||||
workHours: 0,
|
||||
capacityHours: this.numberValue(route.json.hora_laboral_semanal_usuario),
|
||||
utilization: 0
|
||||
utilization: 0,
|
||||
status: 'ok',
|
||||
alertMessage: ''
|
||||
};
|
||||
current.routes++;
|
||||
current.distance += this.numberValue(route.json.distancia);
|
||||
current.workHours += this.parseHours(route.json.horas_trabajo);
|
||||
// Sequential distance and work are aggregated per user below.
|
||||
loads.set(key, current);
|
||||
}
|
||||
|
||||
this.userLoads = Array.from(loads.values())
|
||||
.map(load => ({
|
||||
...load,
|
||||
utilization: load.capacityHours ? (load.workHours / load.capacityHours) * 100 : 0
|
||||
}))
|
||||
.sort((a, b) => b.routes - a.routes || b.workHours - a.workHours);
|
||||
this.userLoads = Array.from(loads.values()).map(load => {
|
||||
const userDays = dailyRoutes.filter(day => day.userId === load.id);
|
||||
load.distance = userDays.reduce((sum, day) => sum + day.distanceKm, 0);
|
||||
load.workHours = userDays.reduce((sum, day) => sum + day.workHours, 0);
|
||||
load.utilization = load.capacityHours ? (load.workHours / load.capacityHours) * 100 : 0;
|
||||
load.status = load.utilization > 100 ? 'over-capacity' : load.utilization > 90 ? 'warning' : 'ok';
|
||||
load.alertMessage = load.status === 'over-capacity'
|
||||
? `Alerta: ${load.name} supera el 100% de su capacidad por carga (${load.workHours.toFixed(1)} h de ${load.capacityHours.toFixed(1)} h). Reasigna PDVs o reduce horas.`
|
||||
: load.status === 'warning'
|
||||
? `${load.name} está sobre el 90% de su capacidad por carga (${load.workHours.toFixed(1)} h de ${load.capacityHours.toFixed(1)} h).`
|
||||
: `${load.name} está dentro de capacidad por carga (${load.workHours.toFixed(1)} h de ${load.capacityHours.toFixed(1)} h).`;
|
||||
return load;
|
||||
}).sort((a, b) => b.routes - a.routes || b.workHours - a.workHours);
|
||||
const routeCounts = this.userLoads.map(load => load.routes);
|
||||
const assignmentBalance = routeCounts.length ? Math.max(...routeCounts) - Math.min(...routeCounts) : 0;
|
||||
const averageUtilization = this.userLoads.length
|
||||
@@ -147,11 +165,6 @@ export class PlanningKpisComponent implements OnInit, OnDestroy {
|
||||
return Number.isFinite(result) ? result : 0;
|
||||
}
|
||||
|
||||
private parseHours(value: number | string | null | undefined): number {
|
||||
const result = Number.parseFloat(String(value ?? ''));
|
||||
return Number.isFinite(result) ? result : 0;
|
||||
}
|
||||
|
||||
private emptySummary(): KpiSummary {
|
||||
return {
|
||||
totalPdvs: 0,
|
||||
|
||||
Reference in New Issue
Block a user