feat: implement route assignment management and sequencing module with interactive data visualization and editing capabilities
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user