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(false); loading$ = this.loading.asObservable(); private assignedRoutes = new BehaviorSubject(this.loadAssigned()); assignedRoutes$ = this.assignedRoutes.asObservable(); private noAssignedRoutes = new BehaviorSubject(this.loadNoAssigned()); noAssignedRoutes$ = this.noAssignedRoutes.asObservable(); private selectedRoute = new BehaviorSubject(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): 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(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(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(); 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(); 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; } }