Files
daily_merchant_web/src/services/dispatchShared.ts

196 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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<string, unknown>): boolean {
const status = String(order?.orderstatus || '').toLowerCase();
return !FINAL_STATUSES.has(status) && !SKIPPED_STATUSES.has(status);
}
export function getActiveOrder(orders: Record<string, unknown>[]): Record<string, unknown> | 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;
}