api integration on the dispatch page
This commit is contained in:
@@ -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` }}>
|
||||
|
||||
Reference in New Issue
Block a user