new changes in api base url

This commit is contained in:
2026-07-09 15:49:45 +05:30
parent 7cd0ae85ef
commit 4ef9cd9085
29 changed files with 2714 additions and 1191 deletions

View File

@@ -17,11 +17,14 @@
* • Date navigation for historical dispatch view
*/
import React, { useMemo, useState } from 'react';
import React, { useMemo, useState, useEffect } from 'react';
import {
Map as MapIcon,
MapPin,
Bike,
Store,
Users,
Phone,
ShoppingBag,
Truck,
Package,
@@ -43,6 +46,8 @@ import {
useFiestaDeliveries,
useFiestaRiders,
useFiestaRiderPeriodicLogs,
useFiestaTenantLocations,
useFiestaTenantCustomers,
} from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, num as fnum, str as fstr, ymd, type Row } from '../services/fiestaApi';
import {
@@ -53,6 +58,7 @@ import {
} from '../services/dispatchShared';
import DispatchMap, { type MapPoint } from './DispatchMap';
import RiderTelemetryPanel from './RiderTelemetryPanel';
import CustomerDetailPanel from './CustomerDetailPanel';
import './DispatchView.css';
// Legacy direct utilities (will be migrated to dispatchShared)
@@ -87,10 +93,10 @@ function pickupLatLon(r: Row): [number, number] | null {
}
// ── View modes (match #strat-row tabs) ───────────────────────────────────────────
type ViewMode = 'kitchens' | 'zones' | 'riders';
type ViewMode = 'stores' | 'zones' | 'customers' | 'riders' | 'kitchens';
const VIEW_TABS: Array<{ id: ViewMode; label: string; icon: typeof MapIcon }> = [
{ id: 'kitchens', label: 'By Location', icon: MapPin },
{ id: 'riders', label: 'By Rider', icon: Bike },
{ id: 'stores', label: 'By Store', icon: Store },
{ id: 'customers', label: 'By Customer', icon: Users },
];
interface Group {
@@ -104,23 +110,27 @@ interface Group {
riders: Set<string>;
suburbs: Map<string, number>;
statusCounts: Record<string, number>;
raw?: any;
}
interface DispatchViewProps {
locationid?: number;
tenantId?: number;
headerTabs?: React.ReactNode;
date: string;
viewMode: 'stores' | 'zones' | 'customers' | 'riders' | 'kitchens';
}
const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID, headerTabs }: DispatchViewProps) {
const today = new Date();
const [date, setDate] = useState<string>(ymd(today));
const [viewMode, setViewMode] = useState<ViewMode>('riders');
export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID, date, viewMode }: DispatchViewProps) {
const [focusedId, setFocusedId] = useState<string | null>(null);
const [focusedRiderId, setFocusedRiderId] = useState<number | null>(null);
useEffect(() => {
setFocusedId(null);
}, [viewMode]);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [tripSort, setTripSort] = useState<'planned' | 'time'>('planned');
const [animateNonce, setAnimateNonce] = useState(0);
@@ -128,6 +138,8 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
// Core dispatch data
const deliveriesQ = useFiestaDeliveries({ tenantid: tenantId, fromdate: date, todate: date, locationid });
const locationsQ = useFiestaTenantLocations(tenantId);
const customersQ = useFiestaTenantCustomers({ tenantid: tenantId, locationid: locationid || 0 });
const ridersQ = useFiestaRiders({ tenantid: tenantId });
// Rider periodic logs (GPS snapshots) for the focused rider
@@ -152,15 +164,38 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
// ── Grouping ────────────────────────────────────────────────────────────────
const groups = useMemo<Group[]>(() => {
const map = new Map<string, Group>();
if (viewMode === 'stores' && locationsQ.data) {
for (const loc of locationsQ.data) {
const id = String(fnum(loc.locationid)).toLowerCase();
const name = fstr(loc.locationname) || `Store ${id}`;
map.set(id, { id, name, color: colorFor(id), orders: [], delivered: 0, totalKm: 0, profit: 0, riders: new Set(), suburbs: new Map(), statusCounts: {} });
}
}
if (viewMode === 'customers' && customersQ.data) {
for (const cust of customersQ.data) {
const id = String(fnum(cust.customerid) || fstr(cust.contactno)).toLowerCase();
const name = fstr(cust.customername) || fstr(cust.name) || `Customer ${id}`;
map.set(id, { id, name, color: colorFor(id), orders: [], delivered: 0, totalKm: 0, profit: 0, riders: new Set(), suburbs: new Map(), statusCounts: {}, raw: cust });
}
}
const titleCase = (s: string) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);
const keyOf = (r: Row): { id: string; name: string } => {
if (viewMode === 'riders') {
const id = fstr(r.userid) || fstr(r.ridername) || 'unassigned';
return { id, name: fstr(r.ridername) || fstr(r.username) || (id === 'unassigned' ? 'Unassigned' : `Rider ${id}`) };
}
if (viewMode === 'kitchens') {
const name = fstr(r.pickupcustomer) || fstr(r.pickuplocation) || 'Pickup';
return { id: name.toLowerCase(), name };
if (viewMode === 'stores') {
const locId = fstr(r.locationid) || fstr(r.pickuplocationid) || 'unknown';
const name = fstr(r.pickupcustomer) || fstr(r.pickuplocation) || `Store ${locId}`;
return { id: locId.toLowerCase(), name };
}
if (viewMode === 'customers') {
const custId = fstr(r.customerid) || fstr(r.contactno) || fstr(r.deliverycustomerphone) || 'unknown';
const name = fstr(r.deliverycustomer) || fstr(r.customername) || `Customer ${custId}`;
return { id: custId.toLowerCase(), name };
}
const name = fstr(r.deliverysuburb) || fstr(r.zone_name) || 'Unzoned';
return { id: name.toLowerCase(), name };
@@ -183,8 +218,8 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
const sub = fstr(r.deliverysuburb);
if (sub) g.suburbs.set(sub, (g.suburbs.get(sub) ?? 0) + 1);
}
return Array.from(map.values()).sort((a, b) => b.orders.length - a.orders.length);
}, [rows, viewMode]);
return Array.from(map.values()).sort((a, b) => b.orders.length - a.orders.length || a.name.localeCompare(b.name));
}, [rows, viewMode, locationsQ.data, customersQ.data]);
const focused = groups.find((g) => g.id === focusedId) ?? null;
const groupedByRider = viewMode !== 'riders';
@@ -257,21 +292,10 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
// KPI scope.
const totalOrders = rows.length;
const totalDelivered = rows.filter((r) => fstr(r.orderstatus).toLowerCase() === 'delivered').length;
const activeRiders = new Set(rows.map((r) => fstr(r.userid) || fstr(r.ridername)).filter(Boolean)).size;
const fleetSize = (ridersQ.data ?? []).length;
// Date chip helpers.
const isToday = date === ymd(today);
const dateObj = new Date(`${date}T00:00:00`);
const prettyDate = `${WEEKDAYS[dateObj.getDay()]}, ${dateObj.getDate()} ${MONTHS[dateObj.getMonth()]}`;
const shiftDate = (delta: number) => {
const d = new Date(`${date}T00:00:00`);
d.setDate(d.getDate() + delta);
if (d > today) return;
setDate(ymd(d));
setFocusedId(null);
};
const fmtTime = (raw: unknown): string => {
const m = fstr(raw).match(/(\d{1,2}):(\d{2})/);
return m ? `${m[1]}:${m[2]}` : '';
@@ -280,90 +304,6 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
return (
<div style={{ height: '100%', minHeight: 0 }}>
<div className="dispatch-container embedded">
{/* ── Header ── */}
<div id="hdr">
<div className="logo flex items-center gap-5">
<div className="flex items-center gap-3">
<div className="logo-badge shadow-sm">D</div>
<div className="logo-name tracking-tight">Dispatch</div>
<div className="logo-city-wrap ml-1">
<span className="logo-city" style={{ cursor: 'default' }}>
<MapPin size={13} className="text-purple-500" />
<span className="logo-city-text font-semibold text-slate-700">Coimbatore</span>
</span>
</div>
</div>
</div>
<div className="hdr-stats">
{deliveriesQ.isLoading ? (
<span className="live-status">
<span className="live-dot" /> Syncing
</span>
) : deliveriesQ.isError ? (
<span className="live-status live-status-error">
<span className="live-dot error" /> Offline
</span>
) : totalOrders === 0 ? (
<span className="live-status" title="No deliveries dispatched for this day">
<span className="live-dot" style={{ background: '#94a3b8' }} /> No deliveries today
</span>
) : (
<span className="live-status live-status-ready">
<span className="live-dot ready" /> Live · {totalOrders} orders
</span>
)}
<div className={`date-chip${isToday ? ' is-today' : ''}`}>
<button className="date-chip-nav" onClick={() => shiftDate(-1)} title="Previous day">
<ChevronLeft size={16} />
</button>
<div className="date-chip-main" style={{ position: 'relative' }}>
<span className="date-chip-icon"><Calendar size={14} /></span>
<span className="date-chip-text">
<span className="date-chip-label">
Date {isToday && <span className="date-chip-today-pill">Today</span>}
</span>
<span className="date-chip-value">{prettyDate}</span>
</span>
<input
type="date"
value={date}
max={ymd(today)}
onChange={(e) => { setDate(e.target.value); setFocusedId(null); }}
style={{ position: 'absolute', inset: 0, opacity: 0, cursor: 'pointer', width: '100%', height: '100%' }}
aria-label="Pick date"
/>
</div>
<button className="date-chip-nav" onClick={() => shiftDate(1)} disabled={isToday} title="Next day">
<ChevronRight size={16} />
</button>
</div>
</div>
</div>
{/* ── View-mode tabs ── */}
<div id="strat-row">
{VIEW_TABS.map((t) => {
const Icon = t.icon;
return (
<button
key={t.id}
className={`sbt ${viewMode === t.id ? 'active' : ''}`}
onClick={() => { setViewMode(t.id); setFocusedId(null); }}
>
<span className="sbt-icon"><Icon size={15} /></span>
{t.label}
</button>
);
})}
{headerTabs && (
<div style={{ marginLeft: 8 }}>
{headerTabs}
</div>
)}
</div>
{/* ── Body ── */}
<div id="body" className={sidebarCollapsed ? 'sidebar-collapsed' : ''}>
<button
@@ -371,7 +311,7 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
onClick={() => setSidebarCollapsed((c) => !c)}
title={sidebarCollapsed ? 'Show panel' : 'Hide panel'}
>
{sidebarCollapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
{sidebarCollapsed ? <ChevronRight size={26} /> : <ChevronLeft size={18} />}
</button>
{/* Sidebar */}
@@ -380,7 +320,7 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
<div className="sb-header-top">
<div className="sb-header-title">
<span className="sb-title-bar" aria-hidden="true" />
<span className="sb-title-text">RIDER DISPATCH</span>
<span className="sb-title-text">CONSOLE</span>
</div>
<span className="sb-header-scope">
<span className="sb-scope-dot" />
@@ -395,11 +335,17 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
<div className="sb-tile-label">Orders</div>
</div>
</div>
<div className="sb-tile sb-tile-riders">
<span className="sb-tile-icon"><Bike size={16} /></span>
<div className="sb-tile sb-tile-deliveries">
<span className="sb-tile-icon">
<Truck size={16} />
</span>
<div className="sb-tile-body">
<div className="sb-tile-value">{activeRiders}{fleetSize ? `/${fleetSize}` : ''}</div>
<div className="sb-tile-label">Riders</div>
<div className="sb-tile-value">
{`${totalDelivered}/${totalOrders}`}
</div>
<div className="sb-tile-label">
Deliveries
</div>
</div>
</div>
</div>
@@ -408,7 +354,7 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
<div id="riders-panel">
{deliveriesQ.isLoading ? (
<div className="ph">Loading dispatch feed</div>
) : focused ? (
) : focused && viewMode !== 'customers' ? (
<FocusedDetail
focused={focused}
tripBlocks={tripBlocks}
@@ -421,78 +367,109 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
riderLogsLoading={riderLogsQ.isLoading}
/>
) : groups.length === 0 ? (
<div className="ph">No deliveries for this day</div>
<div className="ph">{viewMode === 'customers' ? 'No customers found' : 'No deliveries for this day'}</div>
) : (
<>
<div className="ph">
{viewMode === 'riders' ? 'Riders' : viewMode === 'kitchens' ? 'Pickup points' : 'Zones'} ({groups.length})
{viewMode === 'riders' ? 'Riders' : viewMode === 'customers' ? 'Customers' : viewMode === 'stores' ? 'Stores' : 'Zones'} ({groups.length})
</div>
{groups.map((g) => (
<React.Fragment key={g.id}>
{viewMode === 'riders'
? <RiderCard
{groups.map((g) => {
const isSelected = focusedId === g.id;
return (
<React.Fragment key={g.id}>
{viewMode === 'customers' ? (
<CustomerCard g={g} onClick={() => setFocusedId(g.id)} isSelected={isSelected} />
) : viewMode === 'riders' ? (
<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={() => {
) : (
<ZoneCard g={g} onClick={() => {
setFocusedId(g.id);
setFocusedRiderId(null);
}} />}
</React.Fragment>
))}
}} />
)}
</React.Fragment>
);
})}
</>
)}
</div>
</div>
{/* Map area */}
<div id="map-wrap">
{/* Live Leaflet route map */}
<DispatchMap
points={mapPoints}
route={Boolean(focused)}
routeColor={focused?.color || '#662582'}
start={routeStart}
resizeKey={`${sidebarCollapsed}|${viewMode}|${focusedId}`}
animateNonce={animateNonce}
/>
{/* Contextual note overlaid on the map */}
{mapPoints.length === 0 ? (
<div className="dmp-overlay-note">
<MapIcon size={13} /> No drop coordinates in {focused ? 'this route' : 'these deliveries'} yet.
{/* Main Content Area */}
{viewMode === 'customers' ? (
<div id="customer-panel-wrap" style={{ flex: 1, position: 'relative', zIndex: 50, backgroundColor: '#f8fafc', overflow: 'hidden', minWidth: 0 }}>
<style>{`
#customer-panel-wrap * {
margin: revert;
padding: revert;
box-sizing: border-box;
}
`}</style>
<div className="relative h-full w-full flex flex-col">
{focused?.raw ? (
<CustomerDetailPanel customer={focused.raw} onClose={() => setFocusedId(null)} />
) : (
<div className="flex-1 flex flex-col items-center justify-center text-center h-full w-full">
<div className="w-14 h-14 rounded-2xl bg-purple-50 border border-purple-100 text-[#662582] flex items-center justify-center mb-md">
<Users size={24} />
</div>
<p className="font-bold text-sm text-slate-900">Select a customer</p>
<p className="text-xs text-slate-500 mt-1">Choose a customer from the list to view their billing history.</p>
</div>
)}
</div>
) : !focused ? (
<div className="dmp-overlay-note">
<MapIcon size={13} /> Select a {viewMode === 'kitchens' ? 'pickup point' : viewMode === 'zones' ? 'zone' : viewMode === 'riders' ? 'rider' : 'group'} to draw its route.
</div>
) : null}
{/* bottom-right overlay controls (gated) */}
<div id="ov-br">
<button
className={`sbt ${animating ? 'active' : ''}`}
disabled={!focused || mapPoints.length < 2}
onClick={() => {
if (!focused || mapPoints.length < 2) return;
setAnimating(true);
setAnimateNonce((n) => n + 1);
window.setTimeout(() => setAnimating(false), 2300);
}}
title={focused ? 'Replay the route draw' : 'Select a rider to animate its route'}
>
<span className="sbt-icon"><Play size={14} /></span> {animating ? 'Animating…' : 'Animate Routes'}
</button>
<button className="sbt" disabled title="Planned-vs-actual compare needs rider GPS telemetry (awaiting backend)">
<span className="sbt-icon"><ArrowLeftRight size={14} /></span> Compare
</button>
</div>
</div>
) : (
<div id="map-wrap">
{/* Live Leaflet route map */}
<DispatchMap
points={mapPoints}
route={Boolean(focused)}
routeColor={focused?.color || '#662582'}
start={routeStart}
resizeKey={`${sidebarCollapsed}|${viewMode}|${focusedId}`}
animateNonce={animateNonce}
/>
{/* Contextual note overlaid on the map */}
{mapPoints.length === 0 ? (
<div className="dmp-overlay-note">
<MapIcon size={13} /> No drop coordinates in {focused ? 'this route' : 'these deliveries'} yet.
</div>
) : !focused ? (
<div className="dmp-overlay-note">
<MapIcon size={13} /> Select a {viewMode === 'kitchens' ? 'pickup point' : viewMode === 'zones' ? 'zone' : viewMode === 'riders' ? 'rider' : 'group'} to draw its route.
</div>
) : null}
{/* bottom-right overlay controls (gated) */}
<div id="ov-br">
<button
className={`sbt ${animating ? 'active' : ''}`}
disabled={!focused || mapPoints.length < 2}
onClick={() => {
if (!focused || mapPoints.length < 2) return;
setAnimating(true);
setAnimateNonce((n) => n + 1);
window.setTimeout(() => setAnimating(false), 2300);
}}
title={focused ? 'Replay the route draw' : 'Select a rider to animate its route'}
>
<span className="sbt-icon"><Play size={14} /></span> {animating ? 'Animating…' : 'Animate Routes'}
</button>
<button className="sbt" disabled title="Planned-vs-actual compare needs rider GPS telemetry (awaiting backend)">
<span className="sbt-icon"><ArrowLeftRight size={14} /></span> Compare
</button>
</div>
</div>
)}
</div>
</div>
</div>
@@ -699,3 +676,42 @@ function FocusedDetail({
</>
);
}
// ── Customer card ───────────────────────────────────────────────────────────────────
function CustomerCard({ g, onClick, isSelected }: { g: Group; onClick: () => void; isSelected?: boolean }) {
const customer = g.raw;
const name = customer ? fstr(customer.customername) || fstr(customer.name) : g.name;
const phone = customer ? fstr(customer.contactno) || fstr(customer.phone) : '';
return (
<div
className={`rcard transition-all duration-300 cursor-pointer overflow-hidden relative group ${
isSelected
? 'border-purple-300 shadow-md bg-purple-50/50'
: 'bg-white border-slate-200 hover:border-purple-200 hover:shadow-sm'
}`}
onClick={onClick}
>
{isSelected && (
<div className="absolute left-0 top-0 bottom-0 w-1 bg-[#662582] rounded-l-md"></div>
)}
<div className="flex items-center gap-4 p-3.5 pl-4 border-none">
<div className={`w-11 h-11 rounded-xl flex items-center justify-center shrink-0 transition-colors ${
isSelected ? 'bg-[#662582] text-white shadow-md' : 'bg-slate-100 text-slate-500 group-hover:bg-purple-100 group-hover:text-[#662582]'
}`}>
<Users size={20} strokeWidth={isSelected ? 2 : 1.5} />
</div>
<div className="min-w-0 flex-1">
<div className={`font-bold text-sm truncate tracking-tight ${isSelected ? 'text-[#0f172a]' : 'text-slate-800'}`}>
{name || 'Unknown Customer'}
</div>
{phone && (
<div className={`text-xs truncate font-medium mt-0.5 flex items-center gap-1 ${isSelected ? 'text-[#662582]' : 'text-slate-500'}`}>
<Phone size={10} /> {phone}
</div>
)}
</div>
</div>
</div>
);
}