updates on the design and added the riders route page

This commit is contained in:
2026-06-01 15:16:45 +05:30
parent 73b82877b8
commit e662154916
6 changed files with 562 additions and 135 deletions

View File

@@ -115,18 +115,18 @@ const hasValidPickup = (o) => Number.isFinite(toNum(pickupLat(o))) && Number.isF
// FRACTIONAL hours (e.g. 12.5 = 12:30). Half-hour boundaries are supported.
// Three named batches, bucketed by assigntime per spec:
// • Morning Batch: before 8 AM (00:00 → 08:00)
// • Afternoon Batch: 9 AM → 12 PM (09:00 → 12:00)
// • Afternoon Batch: 9 AM → 12:30 PM (09:00 → 12:30)
// • Evening Batch: 4 PM → 7 PM (16:00 → 19:00)
// Gaps (89 AM, 12 PM4 PM, 7 PM+) intentionally fall outside every batch.
// Gaps (89 AM, 12:30 PM4 PM, 7 PM+) intentionally fall outside every batch.
const BATCHES_DEFAULT_RAW = [
{ id: 'morning', name: 'Morning Batch', startHour: 0, endHour: 8 },
{ id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12 },
{ id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12.5 },
{ id: 'evening', name: 'Evening Batch', startHour: 16, endHour: 19 }
];
// v7: three-named-batch layout (Morning / Afternoon / Evening).
// Bumping the key drops cached 5-slot layouts from v6 and earlier.
const SLOTS_STORAGE_KEY = 'dispatch.slots.v7';
// v8: afternoon batch extended to 12:30 PM. Bumping from v7 wipes the
// cached layouts that still hold the old endHour: 12 value.
const SLOTS_STORAGE_KEY = 'dispatch.slots.v8';
// Every prior storage key. Wiped once on mount so stale layouts
// from earlier code versions can't reappear on the next page load.
@@ -136,7 +136,8 @@ const LEGACY_SLOTS_STORAGE_KEYS = [
'dispatch.slots.v3',
'dispatch.slots.v4',
'dispatch.slots.v5',
'dispatch.slots.v6'
'dispatch.slots.v6',
'dispatch.slots.v7'
];
// Build a label like "Slot 1 · 8 AM" (or "Slot 2 · 12:30 PM") from a
@@ -611,7 +612,7 @@ const Ico = ({ children }) => (
// to the X-Batch-Window header value the backend expects.
const ANALYSIS_BATCH_WINDOWS = [
{ key: 'morning', label: 'Morning', timeRange: '12:00 AM 8:00 AM', sub: 'Early shift orders', color: '#f59e0b', bg: '#fffbeb', border: '#fde68a' },
{ key: 'afternoon', label: 'Noon', timeRange: '9:00 AM 12:00 PM', sub: 'Lunch rush window', color: '#10b981', bg: '#ecfdf5', border: '#a7f3d0' },
{ key: 'afternoon', label: 'Noon', timeRange: '9:00 AM 12:30 PM', sub: 'Lunch rush window', color: '#10b981', bg: '#ecfdf5', border: '#a7f3d0' },
{ key: 'evening', label: 'Evening', timeRange: '4:00 PM 7:00 PM', sub: 'Dinner & end-of-day', color: '#6366f1', bg: '#eef2ff', border: '#c7d2fe' }
];
@@ -1041,6 +1042,12 @@ const Dispatch = ({
const preCompareCollapsedRef = useRef(false);
const prevCompareOpenRef = useRef(false);
// Compare-data-panel collapse — mirrors the left sidebar peek tab on the
// opposite edge so the operator can hide the right rail and let the map
// claim the full width during compare. Resets to expanded each time
// Compare opens fresh.
const [compareDataCollapsed, setCompareDataCollapsed] = useState(false);
// Compare UI — step focus on the unified compare map.
// focusedCompareStep: null = "overall" (whole day); 1..N = drill into
// that single delivery. The unified map zooms to that step's bounds
@@ -1725,6 +1732,9 @@ const Dispatch = ({
if (compareOpen && !prevCompareOpenRef.current) {
preCompareCollapsedRef.current = sidebarCollapsed;
setSidebarCollapsed(true);
// Fresh compare-open always reveals the data panel — last-collapsed state
// shouldn't carry across compare sessions.
setCompareDataCollapsed(false);
} else if (!compareOpen && prevCompareOpenRef.current) {
setSidebarCollapsed(preCompareCollapsedRef.current);
}
@@ -2726,6 +2736,24 @@ const Dispatch = ({
)}
</div>
)}
<div id="dispatch-top-tabs" className="dtt-inline">
<button
type="button"
className={`dtt-tab ${topView === 'live' ? 'active' : ''}`}
onClick={() => setTopView('live')}
>
<span className="dtt-icon"><MdMap /></span>
Live
</button>
<button
type="button"
className={`dtt-tab ${topView === 'analysis' ? 'active' : ''}`}
onClick={() => setTopView('analysis')}
>
<span className="dtt-icon"><MdInsights /></span>
Analysis
</button>
</div>
</div>
{/* Header right-cluster: profit/loss chip, total-orders pill, date picker.
@@ -2970,27 +2998,6 @@ const Dispatch = ({
</div>
)}
{!embedded && (
<div id="dispatch-top-tabs">
<button
type="button"
className={`dtt-tab ${topView === 'live' ? 'active' : ''}`}
onClick={() => setTopView('live')}
>
<span className="dtt-icon"><MdMap /></span>
Live
</button>
<button
type="button"
className={`dtt-tab ${topView === 'analysis' ? 'active' : ''}`}
onClick={() => setTopView('analysis')}
>
<span className="dtt-icon"><MdInsights /></span>
Analysis
</button>
</div>
)}
{(embedded || topView === 'live') && (<>
<div id="strat-row">
<button className={`sbt ${viewMode === 'kitchens' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: By Location'); setViewMode('kitchens'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdPlace /></span> By Location</button>
@@ -3439,7 +3446,7 @@ const Dispatch = ({
</div>
</div>
) : (
<div id="body" className={`${sidebarCollapsed ? 'sidebar-collapsed' : ''} ${compareOpen ? 'compare-mode' : ''}`.trim()}>
<div id="body" className={`${sidebarCollapsed ? 'sidebar-collapsed' : ''} ${compareOpen ? 'compare-mode' : ''} ${compareOpen && compareDataCollapsed ? 'compare-data-collapsed' : ''}`.trim()}>
<button
type="button"
className={`sidebar-toggle-tab${sidebarCollapsed ? ' is-collapsed' : ''}`}
@@ -3449,6 +3456,17 @@ const Dispatch = ({
>
{sidebarCollapsed ? <MdChevronRight /> : <MdChevronLeft />}
</button>
{compareOpen && focusedRider && (
<button
type="button"
className={`compare-data-toggle-tab${compareDataCollapsed ? ' is-collapsed' : ''}`}
onClick={() => setCompareDataCollapsed((v) => !v)}
title={compareDataCollapsed ? 'Show details panel' : 'Hide details panel'}
aria-label={compareDataCollapsed ? 'Show details panel' : 'Hide details panel'}
>
{compareDataCollapsed ? <MdChevronLeft /> : <MdChevronRight />}
</button>
)}
<div id="sidebar">
{/* Sidebar header — replaces the top-bar meta line. Hidden when a specific
rider is focused, since the focused-rider view already shows that rider's
@@ -4859,12 +4877,18 @@ const Dispatch = ({
title={
`Planned Step ${plannedStepNum}` +
(d.deliverycustomer ? ` · ${d.deliverycustomer}` : '') +
(d.expectedTs ? ` · ${d.expectedTs.format('hh:mm A')}` : '') +
(d.anomaly ? ' · deviation flagged' : '')
}
>
<span className="compare-step-circle">
{isLoading ? <span className="compare-step-spin" /> : plannedStepNum}
</span>
{d.expectedTs && (
<span className="compare-step-tick">
{d.expectedTs.format('HH:mm')}
</span>
)}
</button>
</React.Fragment>
);
@@ -5176,6 +5200,8 @@ const Dispatch = ({
const riders = Array.isArray(raw.rider_timelines) ? raw.rider_timelines : [];
const subs = Array.isArray(raw.substitution_opportunities) ? raw.substitution_opportunities : [];
const rec = raw.top_recommendation;
const hasRecRider = !!(rec && (rec.idle_rider_name || rec.idle_rider_id));
const hasRec = !!(rec && rec.action && rec.action !== 'none' && hasRecRider);
const win = raw.window || {};
const fleetMetrics = [
@@ -5240,7 +5266,7 @@ const Dispatch = ({
</div>
</div>
{rec && (
{hasRec ? (
<div className="da-section">
<div className="da-section-label">Top Recommendation</div>
<div className="da-rec">
@@ -5289,6 +5315,16 @@ const Dispatch = ({
)}
</div>
</div>
) : (
<div className="da-section">
<div className="da-section-label">Top Recommendation</div>
<div className="da-rec da-rec-empty">
<div className="da-rec-action">
<MdInsights />
<span>Fleet is balanced, no reassignment needed right now.</span>
</div>
</div>
</div>
)}
{riders.length > 0 && (