Files
rutero/route-planner/src/app/core/route-state/route.ts
T

341 lines
14 KiB
TypeScript

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
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';
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', 'Sábado', 'Sabado'];
export interface UnassignedAssignment {
userId: string;
day: string;
distance: number;
duration: number;
}
@Injectable({ providedIn: 'root' })
export class RouteState {
private loading = new BehaviorSubject<boolean>(false);
loading$ = this.loading.asObservable();
private assignedRoutes = new BehaviorSubject<RouteAssignedResponse[]>(this.loadAssigned());
assignedRoutes$ = this.assignedRoutes.asObservable();
private noAssignedRoutes = new BehaviorSubject<RouteNoAssignedResponse[]>(this.loadNoAssigned());
noAssignedRoutes$ = this.noAssignedRoutes.asObservable();
private selectedRoute = new BehaviorSubject<RouteAssignedResponse | null>(this.loadSelected());
selectedRoute$ = this.selectedRoute.asObservable();
resetLoading() {
this.loading.next(false);
}
setLoading(value: boolean) {
this.loading.next(value);
}
setAssignedRoutes(data: RouteAssignedResponse[]) {
this.assignedRoutes.next(data);
this.persist(STORAGE_ASSIGNED, data);
this.persist(STORAGE_ASSIGNED_ORIGINAL, data);
}
updateAssignedRoute(
route: RouteAssignedResponse,
userId: string,
day: string,
metrics?: { distance: number; duration: number }
): void {
const profile = this.assignedRoutes.getValue().find(item => String(item.json.usuario_id) === String(userId));
if (!profile) return;
const updated = this.assignedRoutes.getValue().map(item => {
if (item !== route) return item;
return {
json: {
...item.json,
usuario_id: profile.json.usuario_id,
nombre_usuario: profile.json.nombre_usuario,
latitud_usuario: profile.json.latitud_usuario,
longitud_usuario: profile.json.longitud_usuario,
pais_usuario: profile.json.pais_usuario,
ciudad_usuario: profile.json.ciudad_usuario,
estado_usuario: profile.json.estado_usuario,
provincia_usuario: profile.json.provincia_usuario,
region_usuario: profile.json.region_usuario,
zona_usuario: profile.json.zona_usuario,
hora_laboral_semanal_usuario: profile.json.hora_laboral_semanal_usuario,
dia: day,
distancia: metrics?.distance ?? item.json.distancia,
horas_desplazamiento: metrics?.duration ?? item.json.horas_desplazamiento
}
};
});
this.setDraftAssignedRoutes(updated);
}
addAssignedFromUnassigned(route: RouteNoAssignedResponse, userId: string, day: string, distance: number, duration: number): void {
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),
latitud_usuario: profile.json.latitud_usuario,
longitud_usuario: profile.json.longitud_usuario,
latitud_pdv: route.latitud,
longitud_pdv: route.longitud,
nombre_usuario: profile.json.nombre_usuario,
nombre_pdv: route.pdv_nombre,
usuario_id: profile.json.usuario_id,
pdv_id: route.pdv_id,
pais: route.pais,
ciudad: route.ciudad,
estado: route.estado,
provincia: route.provincia,
region: route.region,
zona: route.zona,
metro: route.metro,
pais_usuario: profile.json.pais_usuario,
ciudad_usuario: profile.json.ciudad_usuario,
estado_usuario: profile.json.estado_usuario,
provincia_usuario: profile.json.provincia_usuario,
region_usuario: profile.json.region_usuario,
zona_usuario: profile.json.zona_usuario
} } 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));
}
updateNoAssignedRoute(route: RouteNoAssignedResponse, changes: Partial<RouteNoAssignedResponse>): void {
const updated = this.noAssignedRoutes.getValue().map(item => item.pdv_id === route.pdv_id ? { ...item, ...changes } : item);
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);
}
reoptimizeAssignedRoutes(): void {
const current = this.assignedRoutes.getValue();
const profiles = Array.from(new Map(current.map(route => [String(route.json.usuario_id), route])).values());
if (profiles.length < 2) return;
const load = new Map<string, number>(profiles.map(profile => [String(profile.json.usuario_id), 0]));
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()
.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) + routeHours);
return {
json: {
...route.json,
usuario_id: user.json.usuario_id,
nombre_usuario: user.json.nombre_usuario,
latitud_usuario: user.json.latitud_usuario,
longitud_usuario: user.json.longitud_usuario,
pais_usuario: user.json.pais_usuario,
ciudad_usuario: user.json.ciudad_usuario,
estado_usuario: user.json.estado_usuario,
provincia_usuario: user.json.provincia_usuario,
region_usuario: user.json.region_usuario,
zona_usuario: user.json.zona_usuario,
hora_laboral_semanal_usuario: user.json.hora_laboral_semanal_usuario
}
};
});
this.setDraftAssignedRoutes(optimized);
}
setNoAssignedRoutes(data: RouteNoAssignedResponse[]) {
this.noAssignedRoutes.next(data);
this.persist(STORAGE_NO_ASSIGNED, data);
this.persist(STORAGE_NO_ASSIGNED_ORIGINAL, data);
}
sendRoute(route: RouteAssignedResponse) {
this.selectedRoute.next(route);
this.persist(STORAGE_SELECTED, route);
}
getAssignedSnapshot(): RouteAssignedResponse[] {
return this.assignedRoutes.getValue();
}
getNoAssignedSnapshot(): RouteNoAssignedResponse[] {
return this.noAssignedRoutes.getValue();
}
findRoute(pdvId: string | number, usuarioId?: string): RouteAssignedResponse | null {
const pid = String(pdvId);
const routes = this.getAssignedSnapshot();
const found = routes.find(r => {
const jp = r.json;
if (String(jp.pdv_id) !== pid) return false;
if (usuarioId && String(jp.usuario_id) !== String(usuarioId)) return false;
return true;
});
return found ?? null;
}
getUsuariosUnicos(): { id: string; nombre: string }[] {
const map = new Map<string, string>();
for (const r of this.getAssignedSnapshot()) {
const id = String(r.json.usuario_id);
if (!map.has(id)) map.set(id, r.json.nombre_usuario);
}
return Array.from(map.entries()).map(([id, nombre]) => ({ id, nombre }));
}
getDiasUnicos(): string[] {
const set = new Set<string>();
for (const r of this.getAssignedSnapshot()) set.add(r.json.dia);
return Array.from(set).sort((a, b) => {
const indexA = DAY_ORDER.indexOf(a);
const indexB = DAY_ORDER.indexOf(b);
return (indexA < 0 ? DAY_ORDER.length : indexA) - (indexB < 0 ? DAY_ORDER.length : indexB);
});
}
clearAll() {
this.assignedRoutes.next([]);
this.noAssignedRoutes.next([]);
this.selectedRoute.next(null);
try {
sessionStorage.removeItem(STORAGE_ASSIGNED);
sessionStorage.removeItem(STORAGE_ASSIGNED_ORIGINAL);
sessionStorage.removeItem(STORAGE_NO_ASSIGNED);
sessionStorage.removeItem(STORAGE_NO_ASSIGNED_ORIGINAL);
sessionStorage.removeItem(STORAGE_SELECTED);
} catch { /* ignore */ }
}
private persist(key: string, data: unknown) {
try {
sessionStorage.setItem(key, JSON.stringify(data));
} catch { /* ignore storage errors */ }
}
private setDraftAssignedRoutes(data: RouteAssignedResponse[]): void {
this.assignedRoutes.next(data);
this.persist(STORAGE_ASSIGNED, data);
}
private setDraftNoAssignedRoutes(data: RouteNoAssignedResponse[]): void {
this.noAssignedRoutes.next(data);
this.persist(STORAGE_NO_ASSIGNED, data);
}
private loadAssigned(): RouteAssignedResponse[] {
try {
const raw = sessionStorage.getItem(STORAGE_ASSIGNED);
if (raw) return JSON.parse(raw) as RouteAssignedResponse[];
} catch { /* ignore */ }
return [];
}
private loadOriginalAssigned(): RouteAssignedResponse[] {
try {
const raw = sessionStorage.getItem(STORAGE_ASSIGNED_ORIGINAL);
if (raw) return JSON.parse(raw) as RouteAssignedResponse[];
} catch { /* ignore */ }
return this.loadAssigned();
}
private loadNoAssigned(): RouteNoAssignedResponse[] {
try {
const raw = sessionStorage.getItem(STORAGE_NO_ASSIGNED);
if (raw) return JSON.parse(raw) as RouteNoAssignedResponse[];
} catch { /* ignore */ }
return [];
}
private loadSelected(): RouteAssignedResponse | null {
try {
const raw = sessionStorage.getItem(STORAGE_SELECTED);
if (raw) return JSON.parse(raw) as RouteAssignedResponse;
} catch { /* ignore */ }
return null;
}
}