/** * @license * SPDX-License-Identifier: Apache-2.0 */ /** * Deliveries page — replicated from the operations console (nearle_console/ * deliveries), rebuilt against the shared console UI kit (`./consoleUi`) so it * matches the source design (gradient header, KPI cards, batch + status pill * tabs, STATUS_META colours, metric-pill table). The board loads the day's * deliveries once and filters client-side by delivery wave, lifecycle status, and * keyword. Rider write-actions (reassign/cancel/notify) need the dispatch + FCM * backends this tenant doesn't expose, so they surface an "awaiting backend" note. */ import React, { useMemo, useState } from 'react'; import { Truck, Clock, CheckCircle2, XCircle, Calendar, Sun, Sunset, Moon, Layers, UserCheck, MapPin, Phone, Package, Loader2, X, Bike, } from 'lucide-react'; import { useFiestaDeliverySummary, useFiestaDeliveries, useFiestaRiders, useFiestaOrderDetails } from '../services/fiestaQueries'; import { FIESTA_TENANT_ID, num as fnum, str as fstr, ymd, type Row } from '../services/fiestaApi'; import { shortTime } from '../services/fiestaMappers'; import AwaitingApi from './AwaitingApi'; import { GradientHeader, LiveStatus, KpiStrip, Pill, StatusChip, MetricPill, SearchPill, FilterBar, TH_STYLE, DELIVERY_STATUS, statusColor, BRAND, BRAND_LIGHT, TEXT, TEXT_2, TEXT_3, BORDER, DIVIDER, SURFACE_ALT, tint, soft, edge, } from './consoleUi'; interface DeliveriesViewProps { searchQuery?: string; locationid?: number; } type DeliveryStatus = 'pending' | 'accepted' | 'arrived' | 'picked' | 'active' | 'skipped' | 'delivered' | 'cancelled'; const STATUS_TABS: Array<{ key: DeliveryStatus; label: string }> = [ { key: 'pending', label: 'Pending' }, { key: 'accepted', label: 'Accepted' }, { key: 'arrived', label: 'Arrived' }, { key: 'picked', label: 'Picked' }, { key: 'active', label: 'Active' }, { key: 'skipped', label: 'Skipped' }, { key: 'delivered', label: 'Delivered' }, { key: 'cancelled', label: 'Cancelled' }, ]; // Batch waves — canonical half-open hour ranges (match Dispatch). type BatchId = 'all' | 'morning' | 'afternoon' | 'evening'; const BATCHES: Array<{ id: BatchId; label: string; range: string; color: string; icon: typeof Sun }> = [ { id: 'all', label: 'All', range: 'Full day', color: '#7c3aed', icon: Layers }, { id: 'morning', label: 'Morning', range: '12 AM – 8 AM', color: '#0ea5e9', icon: Sun }, { id: 'afternoon', label: 'Afternoon', range: '9 AM – 12:30 PM', color: '#f59e0b', icon: Sunset }, { id: 'evening', label: 'Evening', range: '4 PM – 7 PM', color: '#6366f1', icon: Moon }, ]; function rowHourFrac(r: Row): number | null { const m = (fstr(r.assigntime) || fstr(r.deliverytime) || fstr(r.deliverydate)).match(/[ T](\d{1,2}):(\d{2})/); return m ? Number(m[1]) + Number(m[2]) / 60 : null; } function inBatch(r: Row, b: BatchId): boolean { if (b === 'all') return true; const h = rowHourFrac(r); if (h == null) return false; if (b === 'morning') return h >= 0 && h < 8; if (b === 'afternoon') return h >= 9 && h < 12.5; return h >= 16 && h < 19; } function initialBatch(): BatchId { const h = new Date().getHours(); if (h >= 0 && h < 8) return 'morning'; if (h >= 9 && h < 12.5) return 'afternoon'; if (h >= 16 && h < 19) return 'evening'; return 'all'; } export default function DeliveriesView({ searchQuery = '', locationid }: DeliveriesViewProps) { const today = new Date(); const [fromdate, setFromdate] = useState(ymd(today)); const [todate, setTodate] = useState(ymd(today)); const dayOffset = (n: number) => { const d = new Date(); d.setDate(d.getDate() - n); return ymd(d); }; const presets = [ { key: 'today', label: 'Today', from: ymd(today), to: ymd(today) }, { key: 'yesterday', label: 'Yesterday', from: dayOffset(1), to: dayOffset(1) }, { key: '7d', label: 'Last 7 Days', from: dayOffset(6), to: ymd(today) }, ]; const activePreset = presets.find((p) => p.from === fromdate && p.to === todate)?.key ?? 'custom'; const [batch, setBatch] = useState(initialBatch()); const [status, setStatus] = useState('pending'); const [localSearch, setLocalSearch] = useState(''); const [detailRow, setDetailRow] = useState(null); const summaryQ = useFiestaDeliverySummary({ tenantid: FIESTA_TENANT_ID, fromdate, todate }); const deliveriesQ = useFiestaDeliveries({ tenantid: FIESTA_TENANT_ID, fromdate, todate }); const ridersQ = useFiestaRiders({ tenantid: FIESTA_TENANT_ID }); const allRows = deliveriesQ.data ?? []; const summary = summaryQ.data; const batchRows = useMemo(() => allRows.filter((r) => (!locationid || fnum(r.locationid) === locationid) && inBatch(r, batch)), [allRows, batch, locationid]); const statusCounts = useMemo(() => { const acc: Record = {}; for (const r of batchRows) { const s = fstr(r.orderstatus).toLowerCase(); acc[s] = (acc[s] ?? 0) + 1; } return acc; }, [batchRows]); const rows = useMemo(() => { const term = (localSearch || searchQuery).toLowerCase(); return batchRows.filter((r) => { if (fstr(r.orderstatus).toLowerCase() !== status) return false; if (!term) return true; return [r.orderid, r.deliverycustomer, r.deliveryaddress, r.deliverysuburb, r.pickupcustomer, r.ridername, r.username].some((f) => fstr(f).toLowerCase().includes(term)); }); }, [batchRows, status, localSearch, searchQuery]); const activeFleet = (ridersQ.data ?? []).filter((r) => fstr(r.starttime)).length; const total = summary?.total ?? 0; const pct = (n: number) => (total > 0 ? `${Math.round((n / total) * 100)}% of total` : 'In range'); const kpis = [ { label: 'Total Deliveries', value: total.toLocaleString('en-IN'), color: '#6366f1', icon: , badge: undefined }, { label: 'Pending', value: (summary?.pending ?? 0).toLocaleString('en-IN'), color: '#f59e0b', icon: , badge: pct(summary?.pending ?? 0) }, { label: 'Delivered', value: (summary?.delivered ?? 0).toLocaleString('en-IN'), color: '#10b981', icon: , badge: pct(summary?.delivered ?? 0) }, { label: 'Cancelled', value: (summary?.cancelled ?? 0).toLocaleString('en-IN'), color: '#ef4444', icon: , badge: pct(summary?.cancelled ?? 0) }, ]; return (
: deliveriesQ.isError ? : } right={ Coimbatore } />
{/* Date + waves */}
View {presets.map((p) => ( { setFromdate(p.from); setTodate(p.to); }}>{p.label} ))}
setFromdate(e.target.value)} className="rounded-full outline-none font-semibold" style={{ padding: '6px 12px', border: `1.5px solid ${edge('#f59e0b')}`, background: tint('#f59e0b'), color: '#b45309' }} /> setTodate(e.target.value)} className="rounded-full outline-none font-semibold" style={{ padding: '6px 12px', border: `1.5px solid ${edge('#f59e0b')}`, background: tint('#f59e0b'), color: '#b45309' }} />
Wave {BATCHES.map((b) => { const Icon = b.icon; const count = allRows.filter((r) => (!locationid || fnum(r.locationid) === locationid) && inBatch(r, b.id)).length; return ( setBatch(b.id)} title={b.range} count={count}> {b.label} ); })}
{/* Status tabs + search */}
{STATUS_TABS.map((t) => { const color = statusColor(DELIVERY_STATUS, t.key); return ( setStatus(t.key)} count={statusCounts[t.key] ?? 0}>{t.label} ); })}
{/* Table */}
{['#', 'Status', 'Order', 'Drop', 'Rider', 'ETA', 'KMs', 'Amount', ''].map((h, i) => ())} {deliveriesQ.isLoading ? ( ) : rows.length === 0 ? ( ) : ( rows.map((r, i) => { const st = fstr(r.orderstatus).toLowerCase(); const rider = fstr(r.ridername) || fstr(r.username); const kms = fnum(r.kms); const actualKms = fnum(r.cumulativekms); const charge = fnum(r.deliverycharges); const amt = fnum(r.deliveryamt); return ( (e.currentTarget.style.background = SURFACE_ALT)} onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}> ); }) )}
{h}
Loading deliveries…
No {status} deliveries in this wave. Try another status, wave, or date.
{i + 1}

{fstr(r.orderid) || `DLV-${fstr(r.deliveryid)}`}

{shortTime(r.assigntime || r.deliverydate)}

{fstr(r.deliverycustomer) || '—'}

{fstr(r.deliverysuburb) || fstr(r.deliveryaddress)}

{rider ? ( {rider} ) : Unassigned} {shortTime(r.expecteddeliverytime) || '—'}
{kms ? kms.toFixed(1) : '—'} {actualKms > 0 && {actualKms.toFixed(1)}}
{charge > 0 && ₹{charge.toLocaleString('en-IN')}} {amt > 0 && ₹{amt.toLocaleString('en-IN')}} {charge === 0 && amt === 0 && }
{rows.length} {status} · {BATCHES.find((b) => b.id === batch)?.label} wave
{detailRow && setDetailRow(null)} />}
); } // ── Delivery details modal ────────────────────────────────────────────────────── function DeliveryDetailModal({ row, onClose }: { row: Row; onClose: () => void }) { const orderheaderid = row.orderheaderid ?? row.orderid; const detailsQ = useFiestaOrderDetails(orderheaderid as number | string); const lines = (detailsQ.data ?? []).map((d) => { const quantity = fnum(d.quantity) || fnum(d.qty) || fnum(d.orderqty); const price = fnum(d.price) || fnum(d.unitprice) || fnum(d.retailprice); return { name: fstr(d.productname) || fstr(d.itemname) || 'Item', quantity, price, lineTotal: fnum(d.amount) || fnum(d.productsumprice) || price * quantity }; }); const st = fstr(row.orderstatus).toLowerCase(); const rider = fstr(row.ridername) || fstr(row.username); const steps = [ { label: 'Assigned', field: 'assigntime' }, { label: 'Accepted', field: 'acceptedtime' }, { label: 'Arrived', field: 'arrivaltime' }, { label: 'Picked', field: 'pickuptime' }, { label: 'Delivered', field: 'deliverytime' }, ]; return (
{ if (e.target === e.currentTarget) onClose(); }}>

{fstr(row.orderid) || `Delivery ${fstr(row.deliveryid)}`}

{rider || 'Unassigned'}
{fstr(row.deliverycustomer) || 'Customer'}
{fstr(row.deliverycontactno) &&
{fstr(row.deliverycontactno)}
}
{fstr(row.deliveryaddress) || fstr(row.deliverysuburb) || 'Address unavailable'}
Delivery Timeline
{steps.map((s) => { const ts = fstr(row[s.field]); const done = Boolean(ts); return (
{s.label} {done ? shortTime(ts) : '—'}
); })}
Items
{detailsQ.isLoading &&
Loading items…
} {!detailsQ.isLoading && lines.length === 0 &&
No line items returned.
} {lines.map((item, idx) => (

{item.name}

Qty: {item.quantity} × ₹{item.price}

₹{item.lineTotal.toLocaleString('en-IN')}
))}
); }