feat: implement route assignment management and sequencing module with interactive data visualization and editing capabilities

This commit is contained in:
2026-08-31 17:56:33 -04:00
parent cd5c57c5ee
commit 9f42175afc
29 changed files with 458 additions and 218 deletions
@@ -26,6 +26,7 @@ export interface RouteAssignedJson {
nombre_pdv: string;
usuario_id: string;
pdv_id: number;
visit_order?: number;
}
export interface RouteNoAssignedResponse {
pdv_id: number;
@@ -34,4 +35,4 @@ export interface RouteAssignedJson {
latitud: number,
longitud: number,
motivo: string;
}
}
+83 -12
View File
@@ -4,6 +4,7 @@ import {
RouteAssignedResponse,
RouteNoAssignedResponse
} from '../models/route.model';
import { summarizeDailyRoutes } from '../../shared/utils/route-sequencing.utils';
const STORAGE_ASSIGNED = 'rp_assigned_routes';
const STORAGE_ASSIGNED_ORIGINAL = 'rp_assigned_routes_original';
@@ -11,6 +12,12 @@ const STORAGE_NO_ASSIGNED = 'rp_no_assigned_routes';
const STORAGE_NO_ASSIGNED_ORIGINAL = 'rp_no_assigned_routes_original';
const STORAGE_SELECTED = 'rp_selected_route';
const DAY_ORDER = ['Lunes', 'Martes', 'Miércoles', 'Miercoles', 'Jueves', 'Viernes'];
export interface UnassignedAssignment {
userId: string;
day: string;
distance: number;
duration: number;
}
@Injectable({ providedIn: 'root' })
export class RouteState {
@@ -69,13 +76,18 @@ export class RouteState {
}
addAssignedFromUnassigned(route: RouteNoAssignedResponse, userId: string, day: string, distance: number, duration: number): void {
const profile = this.assignedRoutes.getValue().find(item => String(item.json.usuario_id) === String(userId));
if (!profile) return;
const newRoute: RouteAssignedResponse = {
json: {
dia: day,
distancia: distance,
horas_desplazamiento: duration,
this.addAssignedFromUnassignedMany(route, [{ userId, day, distance, duration }]);
}
addAssignedFromUnassignedMany(route: RouteNoAssignedResponse, assignments: UnassignedAssignment[]): void {
const routes = this.assignedRoutes.getValue();
const newRoutes = assignments.map(assignment => {
const profile = routes.find(item => String(item.json.usuario_id) === String(assignment.userId));
if (!profile) return null;
return { json: {
dia: assignment.day,
distancia: assignment.distance,
horas_desplazamiento: assignment.duration,
hora_laboral_semanal_usuario: profile.json.hora_laboral_semanal_usuario,
hora_minima_semanal_pdv: route.minimo_horas_semana,
horas_trabajo: String(route.minimo_horas_semana),
@@ -87,9 +99,10 @@ export class RouteState {
nombre_pdv: route.pdv_nombre,
usuario_id: profile.json.usuario_id,
pdv_id: route.pdv_id
}
};
this.setDraftAssignedRoutes([...this.assignedRoutes.getValue(), newRoute]);
} } as RouteAssignedResponse;
});
if (newRoutes.some(item => !item)) return;
this.setDraftAssignedRoutes([...routes, ...(newRoutes as RouteAssignedResponse[])]);
this.setDraftNoAssignedRoutes(this.noAssignedRoutes.getValue().filter(item => item.pdv_id !== route.pdv_id));
}
@@ -98,6 +111,57 @@ export class RouteState {
this.setDraftNoAssignedRoutes(updated);
}
canAssignToDay(
userId: string,
day: string,
requiredHours: number,
excluding?: RouteAssignedResponse,
additionalTravelHours = 0
): { allowed: boolean; totalHours: number; capacityHours: number } {
const userRoute = this.assignedRoutes.getValue().find(route => String(route.json.usuario_id) === String(userId));
if (!userRoute) return { allowed: false, totalHours: 0, capacityHours: 0 };
const weeklyCapacity = Number(userRoute.json.hora_laboral_semanal_usuario || 0);
const weeklyLoad = this.assignedRoutes.getValue()
.filter(route => String(route.json.usuario_id) === String(userId) && route !== excluding)
.reduce((sum, route) => sum + Number(route.json.horas_trabajo || route.json.hora_minima_semanal_pdv || 0), 0) + requiredHours;
if (weeklyLoad > weeklyCapacity) {
return { allowed: false, totalHours: weeklyLoad, capacityHours: weeklyCapacity };
}
const existing = this.assignedRoutes.getValue().filter(route =>
String(route.json.usuario_id) === String(userId) && route.json.dia === day && route !== excluding
);
const capacityHours = Number(userRoute.json.hora_laboral_semanal_usuario || 0) / 5;
const workHours = existing.reduce((sum, route) => sum + Number(route.json.horas_trabajo || route.json.hora_minima_semanal_pdv || 0), 0) + requiredHours;
const existingTravelHours = summarizeDailyRoutes(existing).reduce((sum, item) => sum + item.travelHours, 0);
const totalHours = workHours + existingTravelHours + additionalTravelHours;
return { allowed: totalHours <= capacityHours, totalHours, capacityHours };
}
moveRouteVisit(route: RouteAssignedResponse, direction: -1 | 1): void {
const group = this.assignedRoutes.getValue().filter(item =>
String(item.json.usuario_id) === String(route.json.usuario_id) && item.json.dia === route.json.dia
);
const sequence = summarizeDailyRoutes(group)[0]?.routes ?? [];
const index = sequence.findIndex(item => item.route === route);
const target = index + direction;
if (index < 0 || target < 0 || target >= sequence.length) return;
const ordered = [...sequence];
[ordered[index], ordered[target]] = [ordered[target], ordered[index]];
const orderByRoute = new Map<RouteAssignedResponse, number>(ordered.map((item, position) => [item.route, position + 1]));
this.setDraftAssignedRoutes(this.assignedRoutes.getValue().map(item => {
const order = orderByRoute.get(item);
return order ? { json: { ...item.json, visit_order: order } } : item;
}));
}
resetRouteVisitOrder(userId: string, day: string): void {
this.setDraftAssignedRoutes(this.assignedRoutes.getValue().map(route =>
String(route.json.usuario_id) === String(userId) && route.json.dia === day
? { json: { ...route.json, visit_order: undefined } }
: route
));
}
restoreOriginalAssignedRoutes(): void {
const original = this.loadOriginalAssigned();
this.setDraftAssignedRoutes(original);
@@ -112,11 +176,18 @@ export class RouteState {
const optimized = [...current]
.sort((a, b) => Number(b.json.hora_minima_semanal_pdv) - Number(a.json.hora_minima_semanal_pdv))
.map(route => {
const routeHours = Number(route.json.horas_trabajo || route.json.hora_minima_semanal_pdv || 0);
const user = profiles
.slice()
.sort((a, b) => (load.get(String(a.json.usuario_id)) ?? 0) - (load.get(String(b.json.usuario_id)) ?? 0))[0];
.filter(profile => {
const userId = String(profile.json.usuario_id);
const capacity = Number(profile.json.hora_laboral_semanal_usuario || 0);
return (load.get(userId) ?? 0) + routeHours <= capacity;
})
.sort((a, b) => (load.get(String(a.json.usuario_id)) ?? 0) - (load.get(String(b.json.usuario_id)) ?? 0))[0]
?? profiles.slice().sort((a, b) => (load.get(String(a.json.usuario_id)) ?? 0) - (load.get(String(b.json.usuario_id)) ?? 0))[0];
const userId = String(user.json.usuario_id);
load.set(userId, (load.get(userId) ?? 0) + 1);
load.set(userId, (load.get(userId) ?? 0) + routeHours);
return {
json: {
...route.json,
@@ -4,14 +4,10 @@ import { Injectable } from '@angular/core';
export class PlanningInputService {
private pdvFile: File | null = null;
private usuariosFile: File | null = null;
private radioKm = 50;
private velocidad = 28;
setFiles(pdvFile: File, usuariosFile: File, radioKm: number, velocidad: number): void {
setFiles(pdvFile: File, usuariosFile: File): void {
this.pdvFile = pdvFile;
this.usuariosFile = usuariosFile;
this.radioKm = radioKm;
this.velocidad = velocidad;
}
getFormData(): FormData | null {
@@ -19,8 +15,6 @@ export class PlanningInputService {
const data = new FormData();
data.append('pdv_file', this.pdvFile, this.pdvFile.name);
data.append('usuarios_file', this.usuariosFile, this.usuariosFile.name);
data.append('radio_km', String(this.radioKm));
data.append('velocidad', String(this.velocidad));
return data;
}