feat: implement route planning dashboard including KPIs, user workload tracking, and route assignment management features
This commit is contained in:
@@ -5,19 +5,26 @@ import {
|
||||
RouteNoAssignedResponse
|
||||
} from '../models/route.model';
|
||||
|
||||
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'];
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class RouteState {
|
||||
|
||||
private loading = new BehaviorSubject<boolean>(false);
|
||||
loading$ = this.loading.asObservable();
|
||||
|
||||
private assignedRoutes = new BehaviorSubject<RouteAssignedResponse[]>([]);
|
||||
private assignedRoutes = new BehaviorSubject<RouteAssignedResponse[]>(this.loadAssigned());
|
||||
assignedRoutes$ = this.assignedRoutes.asObservable();
|
||||
|
||||
private noAssignedRoutes = new BehaviorSubject<RouteNoAssignedResponse[]>([]);
|
||||
private noAssignedRoutes = new BehaviorSubject<RouteNoAssignedResponse[]>(this.loadNoAssigned());
|
||||
noAssignedRoutes$ = this.noAssignedRoutes.asObservable();
|
||||
|
||||
private selectedRoute = new BehaviorSubject<RouteAssignedResponse | null>(null);
|
||||
private selectedRoute = new BehaviorSubject<RouteAssignedResponse | null>(this.loadSelected());
|
||||
selectedRoute$ = this.selectedRoute.asObservable();
|
||||
|
||||
resetLoading() {
|
||||
@@ -30,13 +37,208 @@ export class RouteState {
|
||||
|
||||
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,
|
||||
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 {
|
||||
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,
|
||||
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
|
||||
}
|
||||
};
|
||||
this.setDraftAssignedRoutes([...this.assignedRoutes.getValue(), newRoute]);
|
||||
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);
|
||||
}
|
||||
|
||||
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 user = 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);
|
||||
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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user