import { RouteAssignedResponse } from '../../core/models/route.model'; export interface SequencedRoute { route: RouteAssignedResponse; origin: [number, number]; destination: [number, number]; order: number; totalStops: number; previousPdvName: string | null; } export interface DailyRouteSummary { key: string; userId: string; userName: string; day: string; routes: SequencedRoute[]; distanceKm: number; travelHours: number; workHours: number; capacityHours: number; totalHours: number; exceedsCapacity: boolean; } export function sequenceRoutes(routes: RouteAssignedResponse[]): SequencedRoute[] { const groups = new Map(); for (const route of routes) { const key = `${route.json.usuario_id}\u0000${route.json.dia}`; const group = groups.get(key) ?? []; group.push(route); groups.set(key, group); } const sequenced: SequencedRoute[] = []; for (const group of groups.values()) { const first = group[0].json; let current: [number, number] = [Number(first.longitud_usuario), Number(first.latitud_usuario)]; let previousPdvName: string | null = null; const hasManualOrder = group.every(route => Number.isFinite(route.json.visit_order)); const remaining = hasManualOrder ? [...group].sort((a, b) => Number(a.json.visit_order) - Number(b.json.visit_order)) : [...group]; while (remaining.length) { let nearestIndex = 0; if (!hasManualOrder) { let nearestDistance = Number.POSITIVE_INFINITY; for (let index = 0; index < remaining.length; index++) { const candidate = remaining[index].json; const distance = haversineKm(current, [Number(candidate.longitud_pdv), Number(candidate.latitud_pdv)]); if (distance < nearestDistance) { nearestDistance = distance; nearestIndex = index; } } } const [route] = remaining.splice(nearestIndex, 1); const destination: [number, number] = [Number(route.json.longitud_pdv), Number(route.json.latitud_pdv)]; sequenced.push({ route, origin: current, destination, order: group.length - remaining.length, totalStops: group.length, previousPdvName }); current = destination; previousPdvName = route.json.nombre_pdv; } } return sequenced; } export function summarizeDailyRoutes(routes: RouteAssignedResponse[]): DailyRouteSummary[] { const sequences = sequenceRoutes(routes); const grouped = new Map(); for (const segment of sequences) { const key = `${segment.route.json.usuario_id}\u0000${segment.route.json.dia}`; const group = grouped.get(key) ?? []; group.push(segment); grouped.set(key, group); } return Array.from(grouped, ([key, segments]) => { const first = segments[0].route.json; const distanceKm = segments.reduce((sum, segment) => sum + haversineKm(segment.origin, segment.destination), 0); const travelHours = segments.reduce((sum, segment) => sum + Number(segment.route.json.horas_desplazamiento || 0), 0); const workHours = segments.reduce((sum, segment) => sum + Number(segment.route.json.horas_trabajo || segment.route.json.hora_minima_semanal_pdv || 0), 0); const capacityHours = Number(first.hora_laboral_semanal_usuario || 0) / 5; const totalHours = workHours + travelHours; return { key, userId: String(first.usuario_id), userName: first.nombre_usuario, day: first.dia, routes: segments, distanceKm, travelHours, workHours, capacityHours, totalHours, exceedsCapacity: totalHours > capacityHours }; }); } export function haversineKm(origin: [number, number], destination: [number, number]): number { const toRadians = (value: number) => value * Math.PI / 180; const [lon1, lat1] = origin; const [lon2, lat2] = destination; const dLat = toRadians(lat2 - lat1); const dLon = toRadians(lon2 - lon1); const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * Math.sin(dLon / 2) ** 2; return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); }