api integration on the dispatch page

This commit is contained in:
2026-06-12 15:24:05 +05:30
parent 5378f2df1f
commit 0519e3b19c
7 changed files with 750 additions and 33 deletions

View File

@@ -4,19 +4,17 @@
*/
/**
* Dispatch page — a faithful port of the operations console's dispatch cockpit
* (nearle_console/dispatch). It reuses that page's actual stylesheet verbatim
* (`./DispatchView.css`, copied from Dispatch.css) and reproduces the same DOM /
* class structure: the `#hdr` bar, `#strat-row` view tabs, the 400px `#sidebar`
* (RIDER DISPATCH header + KPI tiles + rider/zone cards + per-trip order cards),
* and the `#map-wrap` centrepiece.
* Dispatch cockpit — integrates live deliveries, rider assignments, and route
* visualization. Reuses the dispatch page stylesheet from the operations console
* and reproduces its DOM structure: header, view-mode tabs, sidebar (KPI + group
* cards), and map centrepiece with Leaflet.
*
* The source map is a Leaflet canvas of planned-vs-actual rider routes (OSRM
* road-snapping, Kalman-smoothed GPS) plus AI rider-assignment posting to
* external optimisation services. Those need a mapping stack + dispatch backends
* this tenant doesn't expose, so the `#map-wrap` plots the real planned stop
* order and marks the live-GPS / compare / AI-assign layers as awaiting backend —
* no fabricated telemetry. Everything else is driven by the live Fiesta feed.
* Features:
* • Group deliveries by rider, zone, location, or status
* • Focus on a specific group to see its trip blocks and detailed order cards
* • Map-based route visualization with planned stops (actual GPS awaiting backend)
* • Real-time KPI cards (orders, riders, completion %)
* • Date navigation for historical dispatch view
*/
import React, { useMemo, useState } from 'react';
@@ -41,12 +39,23 @@ import {
List,
Play,
} from 'lucide-react';
import { useFiestaDeliveries, useFiestaRiders } from '../services/fiestaQueries';
import {
useFiestaDeliveries,
useFiestaRiders,
useFiestaRiderPeriodicLogs,
} from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, num as fnum, str as fstr, ymd, type Row } from '../services/fiestaApi';
import {
colorFor,
getStatusStyle,
STATUS_STYLES,
extractTimeOnly,
} from '../services/dispatchShared';
import DispatchMap, { type MapPoint } from './DispatchMap';
import RiderTelemetryPanel from './RiderTelemetryPanel';
import './DispatchView.css';
// ── Status colours (match the console palette) ───────────────────────────────────
// Legacy direct utilities (will be migrated to dispatchShared)
const STATUS_HEX: Record<string, string> = {
pending: '#f59e0b',
accepted: '#6366f1',
@@ -57,19 +66,12 @@ const STATUS_HEX: Record<string, string> = {
delivered: '#22c55e',
cancelled: '#ef4444',
};
function statusStyle(s: string): React.CSSProperties {
const hex = STATUS_HEX[s.toLowerCase()] || '#64748b';
return { background: `${hex}1f`, color: hex };
}
// Stable rider/zone colour.
const COLORS = ['#3b82f6', '#a855f7', '#10b981', '#f59e0b', '#ef4444', '#6366f1', '#14b8a6', '#ec4899', '#f97316', '#06b6d4'];
function colorFor(key: string): string {
let hash = 0;
for (let i = 0; i < key.length; i++) hash = key.charCodeAt(i) + ((hash << 5) - hash);
return COLORS[Math.abs(hash) % COLORS.length];
}
/** Drop coordinates from a delivery row (several field spellings), or null. */
function dropLatLon(r: Row): [number, number] | null {
const lat = fnum(r.droplat) || fnum(r.deliverylat) || fnum(r.deliverylatitude);
@@ -120,14 +122,24 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID }
const [date, setDate] = useState<string>(ymd(today));
const [viewMode, setViewMode] = useState<ViewMode>('riders');
const [focusedId, setFocusedId] = useState<string | null>(null);
const [focusedRiderId, setFocusedRiderId] = useState<number | null>(null);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [tripSort, setTripSort] = useState<'planned' | 'time'>('planned');
const [animateNonce, setAnimateNonce] = useState(0);
const [animating, setAnimating] = useState(false);
// Core dispatch data
const deliveriesQ = useFiestaDeliveries({ tenantid: tenantId, fromdate: date, todate: date, locationid });
const ridersQ = useFiestaRiders({ tenantid: tenantId });
// Rider periodic logs (GPS snapshots) for the focused rider
const riderLogsQ = useFiestaRiderPeriodicLogs({
userid: focusedRiderId ?? undefined,
fromdate: date,
todate: date,
tenantid: tenantId,
});
// Live deliveries only — no sample/demo fallback. When the feed is empty the
// cockpit shows a genuine empty state rather than fabricated riders/stops.
const allRows = deliveriesQ.data ?? [];
@@ -410,6 +422,8 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID }
setTripSort={setTripSort}
onBack={() => setFocusedId(null)}
fmtTime={fmtTime}
riderLogs={riderLogsQ.data}
riderLogsLoading={riderLogsQ.isLoading}
/>
) : groups.length === 0 ? (
<div className="ph">No deliveries for this day</div>
@@ -421,8 +435,19 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID }
{groups.map((g) => (
<React.Fragment key={g.id}>
{viewMode === 'riders'
? <RiderCard g={g} onClick={() => setFocusedId(g.id)} />
: <ZoneCard g={g} onClick={() => setFocusedId(g.id)} />}
? <RiderCard
g={g}
onClick={() => {
setFocusedId(g.id);
// Extract rider ID from first order in group for GPS logs
const rid = fnum(g.orders[0]?.userid);
if (rid) setFocusedRiderId(rid);
}}
/>
: <ZoneCard g={g} onClick={() => {
setFocusedId(g.id);
setFocusedRiderId(null);
}} />}
</React.Fragment>
))}
</>
@@ -575,6 +600,8 @@ function FocusedDetail({
setTripSort,
onBack,
fmtTime,
riderLogs,
riderLogsLoading,
}: {
focused: Group;
tripBlocks: Array<{ label: string; color: string; orders: Row[] }>;
@@ -583,6 +610,8 @@ function FocusedDetail({
setTripSort: (v: 'planned' | 'time') => void;
onBack: () => void;
fmtTime: (raw: unknown) => string;
riderLogs?: Row[];
riderLogsLoading?: boolean;
}) {
return (
<>
@@ -590,6 +619,14 @@ function FocusedDetail({
<span className="sbt-icon"><ChevronLeft size={15} /></span> Back to list
</button>
{riderLogs && riderLogs.length > 0 && (
<RiderTelemetryPanel
logs={riderLogs}
riderName={focused.name}
isLoading={riderLogsLoading}
/>
)}
{tripBlocks.map((blk, bi) => (
<div className="trip-block" key={bi}>
<div className="trip-header" style={{ background: `${blk.color}12`, borderColor: `${blk.color}40` }}>