/** * @license * SPDX-License-Identifier: Apache-2.0 */ /** * Shared constants and pure helpers for dispatch operations. * Reusable across DispatchView and child components. */ // ── Status Palette ──────────────────────────────────────────────────────────── export const STATUS_STYLES = { created: { label: 'Created', bg: '#3b82f6', fg: '#fff' }, pending: { label: 'Pending', bg: '#f59e0b', fg: '#fff' }, accepted: { label: 'Accepted', bg: '#8b5cf6', fg: '#fff' }, arrived: { label: 'Arrived', bg: '#ea580c', fg: '#fff' }, picked: { label: 'Picked', bg: '#0ea5e9', fg: '#fff' }, active: { label: 'Active', bg: '#0ea5e9', fg: '#fff' }, delivered: { label: 'Delivered', bg: '#22c55e', fg: '#fff' }, skipped: { label: 'Skipped', bg: '#94a3b8', fg: '#fff' }, cancelled: { label: 'Cancelled', bg: '#ef4444', fg: '#fff' }, }; export interface StatusStyle { label: string; bg: string; fg: string; } export function getStatusStyle(status: string | unknown): StatusStyle { const key = String(status || '').toLowerCase() as keyof typeof STATUS_STYLES; return STATUS_STYLES[key] || { label: String(status || 'Unknown'), bg: '#64748b', fg: '#fff', }; } // ── Order Status Sets ──────────────────────────────────────────────────────── export const FINAL_STATUSES = new Set(['delivered']); export const SKIPPED_STATUSES = new Set(['cancelled', 'skipped']); // ── Step-wise Color Palette (for >10 stops) ────────────────────────────────── export const STEP_PALETTE = [ '#2563eb', // blue-600 '#dc2626', // red-600 '#16a34a', // green-600 '#ea580c', // orange-600 '#9333ea', // purple-600 '#0891b2', // cyan-600 '#ca8a04', // yellow-600 '#db2777', // pink-600 '#0f766e', // teal-700 '#7c3aed', // violet-600 '#65a30d', // lime-600 '#0284c7', // sky-600 '#b91c1c', // red-700 '#15803d', // green-700 '#a16207', // yellow-700 '#86198f', // fuchsia-800 ]; export function stepColor(index: number): string { return STEP_PALETTE[((index % STEP_PALETTE.length) + STEP_PALETTE.length) % STEP_PALETTE.length]; } // ── Rider/Zone Color Palette ────────────────────────────────────────────────── const RIDER_COLORS = [ '#3b82f6', '#a855f7', '#10b981', '#f59e0b', '#ef4444', '#6366f1', '#14b8a6', '#ec4899', '#f97316', '#06b6d4', ]; export function colorFor(key: string): string { let hash = 0; for (let i = 0; i < key.length; i++) { hash = key.charCodeAt(i) + ((hash << 5) - hash); } return RIDER_COLORS[Math.abs(hash) % RIDER_COLORS.length]; } // ── Delivery Status Checkers ────────────────────────────────────────────────── export function isActiveDelivery(order: Record): boolean { const status = String(order?.orderstatus || '').toLowerCase(); return !FINAL_STATUSES.has(status) && !SKIPPED_STATUSES.has(status); } export function getActiveOrder(orders: Record[]): Record | null { if (!Array.isArray(orders) || !orders.length) return null; const sorted = [...orders].sort((a, b) => { const tA = Number(a.trip_number) || 1; const tB = Number(b.trip_number) || 1; if (tA !== tB) return tA - tB; return (Number(a.step) || 0) - (Number(b.step) || 0); }); return sorted.find(isActiveDelivery) || null; } // ── Time Batch Helpers ──────────────────────────────────────────────────────── export interface TimeBatch { id: string; name?: string; label: string; range: string; startHour: number; endHour: number; } const BATCHES_DEFAULT_RAW = [ { id: 'morning', name: 'Morning Batch', startHour: 0, endHour: 8 }, { id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12.5 }, { id: 'evening', name: 'Evening Batch', startHour: 16, endHour: 19 }, ]; export function formatHourLabel(h: number): string { const wholeHour = Math.floor(h); const minutes = Math.round((h - wholeHour) * 60); const hr = ((wholeHour + 11) % 12) + 1; const ampm = wholeHour >= 12 && wholeHour < 24 ? 'PM' : 'AM'; if (minutes === 0) return `${hr} ${ampm}`; const mm = String(minutes).padStart(2, '0'); return `${hr}:${mm} ${ampm}`; } export function formatSlotLabel(idx: number, startHour: number): string { return `Slot ${idx + 1} · ${formatHourLabel(startHour)}`; } export function formatSlotRange(startHour: number, endHour: number): string { if (endHour >= 24) return `After ${formatHourLabel(startHour)}`; return `${formatHourLabel(startHour)}–${formatHourLabel(endHour)}`; } export function getDefaultBatches(): TimeBatch[] { return BATCHES_DEFAULT_RAW.map((s, i) => ({ ...s, label: s.name || formatSlotLabel(i, s.startHour), range: formatSlotRange(s.startHour, s.endHour), })); } export function getBatchForHour(h: number, batches: TimeBatch[]): string | null { for (const b of batches) { if (h >= b.startHour && h < b.endHour) return b.id; } return null; } // ── Ordinal Numbers ─────────────────────────────────────────────────────────── export function ordinal(n: number | null | undefined): string { if (n == null) return ''; const s = ['th', 'st', 'nd', 'rd']; const v = n % 100; return n + (s[(v - 20) % 10] || s[v] || s[0]); } // ── Distance Calculations ───────────────────────────────────────────────────── export function haversineKm( a: [number, number], b: [number, number], ): number { const R = 6371; // Earth radius in km const toRad = (d: number) => (d * Math.PI) / 180; const lat1 = toRad(a[0]); const lat2 = toRad(b[0]); const dLat = toRad(b[0] - a[0]); const dLon = toRad(b[1] - a[1]); const s = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2; return 2 * R * Math.asin(Math.min(1, Math.sqrt(s))); } export function polylineLengthKm(points: Array<[number, number]>): number { if (!Array.isArray(points) || points.length < 2) return 0; let total = 0; for (let i = 1; i < points.length; i++) { total += haversineKm(points[i - 1], points[i]); } return total; } // ── Scooter Icon SVG ────────────────────────────────────────────────────────── export const SCOOTER_SVG_PATH = 'M19,17A2,2 0 0,1 17,19A2,2 0 0,1 15,17A2,2 0 0,1 17,15A2,2 0 0,1 19,17M7,17A2,2 0 0,1 5,19A2,2 0 0,1 3,17A2,2 0 0,1 5,15A2,2 0 0,1 7,17M21.43,11.33L19.43,6.33C19.23,5.84 18.75,5.5 18.21,5.5H15V3H11V9H15.6L16.96,12.44C15.82,12.87 15,13.97 15,15.25V16H13.68C13.23,14.82 12.1,14 10.75,14C9.4,14 8.27,14.82 7.82,16H6.18C5.73,14.82 4.6,14 3.25,14C1.9,14 0.77,14.82 0.32,16H0V18H2V17C2,15.9 2.9,15 4,15C5.1,15 6,15.9 6,17H8C8,15.9 8.9,15 10,15C11.1,15 12,15.9 12,17H14C14,15.9 14.9,15 16,15C16.59,15 17.11,15.26 17.47,15.68L18.66,12.7L21.84,13.33L21.43,11.33Z'; // ── Time Window Helpers ─────────────────────────────────────────────────────── export function extractTimeOnly(raw: unknown): string { const m = String(raw || '').match(/(\d{1,2}):(\d{2})/); return m ? `${m[1]}:${m[2]}` : ''; } export interface Row { [key: string]: unknown; }