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

@@ -3786,7 +3786,7 @@
background: rgba(15, 23, 42, 0.02);
border: 1px solid rgba(15, 23, 42, 0.06);
border-radius: 12px;
padding: 10px 14px;
padding: 18px 18px;
gap: 16px;
box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.02);
}
@@ -3840,16 +3840,11 @@
position: relative;
}
/* Planned track overrides to align vertically centered since there are no ticks */
.dispatch-container .compare-timeline-track.is-planned .compare-step {
gap: 0;
padding: 0;
height: 32px;
}
/* Planned track now also carries a time tick under the circle, so it uses the
same column layout as the actual row (circle stacked above the tick). */
.dispatch-container .compare-timeline-track.is-planned .compare-step-spacer {
margin-bottom: 0;
align-self: center;
margin-bottom: 22px;
/* Centers spacer dynamically relative to the 32px circle (matches actual). */
}
/* Actual track overrides for the spacer alignment */
@@ -6848,7 +6843,7 @@
}
.dispatch-container .compare-timeline-container {
padding: 6px 10px;
padding: 12px 12px;
gap: 12px;
border-radius: 8px;
}
@@ -6894,14 +6889,10 @@
background: rgba(99, 102, 241, 0.5);
}
/* Planned track overrides to align vertically centered since there are no ticks */
.dispatch-container .compare-timeline-track.is-planned .compare-step {
height: 24px;
}
/* Planned track now also carries a time tick under the circle, so the spacer
aligns the same way as the actual row (mirrors the 24px circle center). */
.dispatch-container .compare-timeline-track.is-planned .compare-step-spacer {
margin-bottom: 0;
align-self: center;
margin-bottom: 14px;
}
/* Actual track overrides for the spacer alignment */
@@ -7098,7 +7089,7 @@
}
.dispatch-container .compare-timeline-container {
padding: 6px 10px;
padding: 12px 12px;
gap: 12px;
border-radius: 8px;
}
@@ -7124,14 +7115,10 @@
padding-bottom: 2px;
}
/* Planned track overrides to align vertically centered since there are no ticks */
.dispatch-container .compare-timeline-track.is-planned .compare-step {
height: 24px;
}
/* Planned track now also carries a time tick under the circle, so the spacer
aligns the same way as the actual row (mirrors the 24px circle center). */
.dispatch-container .compare-timeline-track.is-planned .compare-step-spacer {
margin-bottom: 0;
align-self: center;
margin-bottom: 14px;
}
/* Actual track overrides for the spacer alignment */
@@ -7312,6 +7299,8 @@
gap: 12px;
padding: 12px;
background: linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%);
transition: grid-template-columns 0.32s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.dispatch-container #body.compare-mode #sidebar,
@@ -7319,6 +7308,89 @@
display: none !important;
}
/* Collapsed-data-panel state — drop the right column entirely so the map
claims the full body width. The panel itself is masked via overflow on
the body grid; the peek tab below stays visible to re-open. */
.dispatch-container #body.compare-mode.compare-data-collapsed {
grid-template-columns: minmax(0, 1fr) 0;
gap: 0;
}
.dispatch-container #body.compare-mode.compare-data-collapsed .compare-data-panel {
opacity: 0;
pointer-events: none;
transform: translateX(20px);
}
.dispatch-container .compare-data-panel {
transition: opacity 0.24s ease, transform 0.32s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Peek tab for the right-side compare data panel — vertical pill mirroring
the left sidebar's toggle, but anchored to the panel's left edge. Tracks
the panel by sitting at right:0 when expanded (so it hugs the panel's
outside-left edge) and snaps flush to the viewport's right side when
collapsed. */
.dispatch-container .compare-data-toggle-tab {
position: absolute;
top: 50%;
/* Anchor flush against the panel's outside-left edge. Panel max width is
440px (see compare-mode grid-template-columns above) plus the 12px grid
gap; transform: translate(50%, …) re-centres the 22-wide pill on that
boundary so half of it sits on the panel side and half on the map side
— same visual treatment as the left sidebar's peek tab. */
right: calc(440px + 12px);
transform: translate(50%, -50%);
width: 22px;
height: 56px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
border: 1px solid var(--border, rgba(15, 23, 42, 0.12));
border-radius: 10px;
background: #fff;
color: var(--text, #0f172a);
font-size: 18px;
line-height: 1;
cursor: pointer;
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.12),
0 1px 3px rgba(15, 23, 42, 0.06);
z-index: 1200;
transition: right 0.32s cubic-bezier(0.4, 0, 0.2, 1),
background 0.18s ease,
color 0.18s ease,
transform 0.18s ease,
box-shadow 0.18s ease;
}
.dispatch-container .compare-data-toggle-tab:hover {
background: linear-gradient(135deg, #6366f1, #3b82f6);
color: #fff;
transform: translate(50%, -50%) scale(1.06);
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.35);
}
.dispatch-container .compare-data-toggle-tab:focus-visible {
outline: 2px solid var(--accent, #3b82f6);
outline-offset: 2px;
}
.dispatch-container .compare-data-toggle-tab.is-collapsed {
right: 0;
transform: translate(0, -50%);
border-radius: 10px 0 0 10px;
border-right: none;
}
.dispatch-container .compare-data-toggle-tab.is-collapsed:hover {
transform: translate(0, -50%) scale(1.06);
}
.dispatch-container .compare-data-toggle-tab svg {
display: block;
}
/* Header strip — sits above the unified map (row 1, col 1) and
carries the rider title, the step timeline + load progress, and
the layer legend. The Sync toggle was removed when the second
@@ -7973,6 +8045,12 @@
grid-row: 3;
max-height: 50vh;
}
/* Single-column layout stacks the panel BELOW the map, so the
side-anchored peek tab no longer makes geometric sense — hide it. */
.dispatch-container .compare-data-toggle-tab {
display: none;
}
}
/* Hide filter chrome when Compare takes over the screen — view-mode
@@ -9062,46 +9140,53 @@
}
/* ============================================================
Top-level Live / Analysis tabs (standalone Dispatch only)
Top-level Live / Analysis tabs (pinned inside header, left of profit)
============================================================ */
.dispatch-container #dispatch-top-tabs {
display: flex;
gap: 6px;
padding: 8px 12px 0 12px;
border-bottom: 1px solid var(--border);
background: var(--bg);
display: inline-flex;
align-items: center;
gap: 4px;
padding: 0;
background: transparent;
flex-shrink: 0;
}
.dispatch-container #dispatch-top-tabs.dtt-inline {
margin-right: 4px;
}
.dispatch-container .dtt-tab {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border: none;
background: transparent;
font-size: 13px;
font-weight: 600;
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--bg);
font-size: 12px;
font-weight: 700;
color: var(--text-muted);
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.15s, border-color 0.15s;
line-height: 1;
transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.dispatch-container .dtt-tab:hover {
color: var(--text);
background: var(--bg-sub);
}
.dispatch-container .dtt-tab.active {
color: var(--accent);
border-bottom-color: var(--accent);
color: #fff;
background: var(--accent);
border-color: var(--accent);
box-shadow: 0 2px 6px rgba(59, 130, 246, 0.25);
}
.dispatch-container .dtt-icon {
display: inline-flex;
align-items: center;
font-size: 15px;
font-size: 14px;
}
/* ============================================================
@@ -9477,6 +9562,19 @@
gap: 8px;
}
.dispatch-container .da-rec.da-rec-empty {
background: #f8fafc;
border: 1px dashed #e2e8f0;
color: #64748b;
}
.dispatch-container .da-rec.da-rec-empty .da-rec-action {
color: #64748b;
text-transform: none;
font-weight: 500;
letter-spacing: 0;
}
.dispatch-container .da-rec-head {
display: flex;
justify-content: space-between;

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 && (