new chnages in ui
This commit is contained in:
@@ -111,32 +111,23 @@ const pickupLat = (o) => o.pickuplat || o.pickuplatitude || o.pickup_lat;
|
||||
const pickupLon = (o) => o.pickuplong || o.pickuplongitude || o.picklongitude || o.pickup_lon;
|
||||
const hasValidPickup = (o) => Number.isFinite(toNum(pickupLat(o))) && Number.isFinite(toNum(pickupLon(o)));
|
||||
|
||||
// Named delivery slots — operator's mental model of the day's waves.
|
||||
// Named delivery batches — operator's mental model of the day's waves.
|
||||
// Each entry covers a half-open range [startHour, endHour) measured in
|
||||
// FRACTIONAL hours (e.g. 12.5 = 12:30). Half-hour boundaries are supported
|
||||
// so slot 1 can end at 12:30 PM and slot 2 can start there.
|
||||
// Slot 5 ends at 24 so anything from 8 PM until midnight buckets there.
|
||||
// Default slot layout. Used as the seed for the editable slot config the
|
||||
// operator can tweak at runtime — see slotsConfig state + the slot-edit
|
||||
// popover below. Don't read BATCHES_DEFAULT directly at runtime; read
|
||||
// component state instead so user edits take effect.
|
||||
// Five named waves:
|
||||
// • Slot 1: morning rush (8 AM → 12:30 PM)
|
||||
// • Slot 2: lunch (12:20 PM → 3 PM)
|
||||
// • Slot 3: afternoon (3 PM → 7 PM)
|
||||
// • Slot 4: evening (7 PM → 8 PM)
|
||||
// • Slot 5: night (8 PM → midnight)
|
||||
// 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)
|
||||
// • Evening Batch: 4 PM → 7 PM (16:00 → 19:00)
|
||||
// Gaps (8–9 AM, 12 PM–4 PM, 7 PM+) intentionally fall outside every batch.
|
||||
const BATCHES_DEFAULT_RAW = [
|
||||
{ id: 'slot-1', startHour: 8, endHour: 12.5 },
|
||||
{ id: 'slot-2', startHour: 12 + 20 / 60, endHour: 15 },
|
||||
{ id: 'slot-3', startHour: 15, endHour: 19 },
|
||||
{ id: 'slot-4', startHour: 19, endHour: 20 },
|
||||
{ id: 'slot-5', startHour: 20, endHour: 24 }
|
||||
{ id: 'morning', name: 'Morning Batch', startHour: 0, endHour: 8 },
|
||||
{ id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12 },
|
||||
{ id: 'evening', name: 'Evening Batch', startHour: 16, endHour: 19 }
|
||||
];
|
||||
|
||||
// v6: the five-named-wave layout with validation checks for array lengths.
|
||||
// Bumping the key drops cached layouts from v5 and earlier in favour of the new defaults.
|
||||
const SLOTS_STORAGE_KEY = 'dispatch.slots.v6';
|
||||
// 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';
|
||||
|
||||
// Every prior storage key. Wiped once on mount so stale layouts
|
||||
// from earlier code versions can't reappear on the next page load.
|
||||
@@ -145,7 +136,8 @@ const LEGACY_SLOTS_STORAGE_KEYS = [
|
||||
'dispatch.slots.v2',
|
||||
'dispatch.slots.v3',
|
||||
'dispatch.slots.v4',
|
||||
'dispatch.slots.v5'
|
||||
'dispatch.slots.v5',
|
||||
'dispatch.slots.v6'
|
||||
];
|
||||
|
||||
// Build a label like "Slot 1 · 8 AM" (or "Slot 2 · 12:30 PM") from a
|
||||
@@ -178,9 +170,12 @@ const formatSlotRange = (startHour, endHour) => {
|
||||
// Doing it through the formatters (instead of hardcoding "Slot 2 · 12:30 PM"
|
||||
// etc.) guarantees user-edited slots and default slots render the same way
|
||||
// — no chance of drift between the two paths.
|
||||
// Prefer the explicit `name` (e.g. "Morning Batch") when provided; fall back
|
||||
// to the auto-generated "Slot N · 8 AM" label for any user-added slot that
|
||||
// doesn't carry a name.
|
||||
const BATCHES_DEFAULT = BATCHES_DEFAULT_RAW.map((s, i) => ({
|
||||
...s,
|
||||
label: formatSlotLabel(i, s.startHour),
|
||||
label: s.name || formatSlotLabel(i, s.startHour),
|
||||
range: formatSlotRange(s.startHour, s.endHour)
|
||||
}));
|
||||
|
||||
@@ -196,12 +191,14 @@ const getBatchForHour = (h, batches) => {
|
||||
// timestamp `getRowBatch` reads. "Delivery" defaults to actual deliverytime
|
||||
// with a fallback to expecteddeliverytime so undelivered orders still bucket.
|
||||
const TIME_FIELDS = [
|
||||
{ id: 'delivery', label: 'Delivery', keys: ['deliverytime', 'expecteddeliverytime'] },
|
||||
{ id: 'delivered', label: 'Delivered', keys: ['deliverytime'] },
|
||||
{ id: 'pending', label: 'Pending', keys: ['expecteddeliverytime'] },
|
||||
{ id: 'assigned', label: 'Assigned', keys: ['assigntime'] },
|
||||
{ id: 'accepted', label: 'Accepted', keys: ['acceptedtime'] },
|
||||
{ id: 'started', label: 'Started', keys: ['starttime'] },
|
||||
{ id: 'arrived', label: 'Arrived', keys: ['arrivaltime'] },
|
||||
{ id: 'pickup', label: 'Pickup', keys: ['pickuptime'] }
|
||||
{ id: 'pickup', label: 'Pickup', keys: ['pickuptime'] },
|
||||
{ id: 'all', label: 'All', keys: ['deliverytime', 'expecteddeliverytime', 'assigntime', 'acceptedtime', 'arrivaltime', 'pickuptime', 'starttime'] }
|
||||
];
|
||||
|
||||
const getTimeFieldValue = (r, fieldId) => {
|
||||
@@ -212,7 +209,7 @@ const getTimeFieldValue = (r, fieldId) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const getRowBatch = (r, fieldId = 'delivery', batches = BATCHES_DEFAULT) => {
|
||||
const getRowBatch = (r, fieldId = 'all', batches = BATCHES_DEFAULT) => {
|
||||
const t = getTimeFieldValue(r, fieldId);
|
||||
if (!t) return null;
|
||||
const str = String(t).trim();
|
||||
@@ -548,6 +545,22 @@ L.Icon.Default.mergeOptions({
|
||||
|
||||
const RIDER_COLORS = ['#0055FF', '#00D82C', '#FF6B00', '#9D00FF', '#FF00A8', '#00C2B2', '#FF9900', '#FF0000'];
|
||||
|
||||
// Deterministic rider-id → palette slot mapping. The main `riders` array
|
||||
// assigns colors by iteration order (see useMemo around line 1308), which
|
||||
// means the same rider can shuffle to a different color across live-data
|
||||
// refetches. The Rider Info list needs a fixed color per rider so the dot
|
||||
// next to "Rajan A" doesn't flip from blue to green on the next poll, so
|
||||
// it uses this hash-based lookup instead of `getRiderColor(id)`.
|
||||
const getStableRiderColor = (id) => {
|
||||
const s = String(id ?? '');
|
||||
if (!s) return RIDER_COLORS[0];
|
||||
let h = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
h = (h * 31 + s.charCodeAt(i)) >>> 0;
|
||||
}
|
||||
return RIDER_COLORS[h % RIDER_COLORS.length];
|
||||
};
|
||||
|
||||
// STATUS_STYLES, getStatusStyle, FINAL_STATUSES, SKIPPED_STATUSES,
|
||||
// STEP_PALETTE, stepColor — moved to ./dispatchShared.js so the
|
||||
// extracted CompareDataPanel component can import them without forcing
|
||||
@@ -715,6 +728,13 @@ const Dispatch = ({
|
||||
// or vice versa without immediately triggering a close.
|
||||
const activePopupMarkerRef = useRef(null);
|
||||
const popupHoverTimerRef = useRef(null);
|
||||
// Order shown in the centered popup overlay. Rendered outside the leaflet
|
||||
// map (see `dispatch-popup-center` overlay near the bottom of the JSX) so
|
||||
// `position: fixed` actually centers on the viewport. Drives behavior that
|
||||
// used to flow through leaflet's marker-attached <Popup>: hover opens it,
|
||||
// mouseout closes it after a ~200ms grace window (unless pinned), click
|
||||
// toggles a pinned state stored in pinnedPopupsRef.
|
||||
const [centerPopupOrder, setCenterPopupOrder] = useState(null);
|
||||
const isControlled = selectedRiderId !== undefined;
|
||||
const [clock, setClock] = useState('');
|
||||
|
||||
@@ -736,11 +756,11 @@ const Dispatch = ({
|
||||
const [locationMenuOpen, setLocationMenuOpen] = useState(false);
|
||||
const locationMenuRef = useRef(null);
|
||||
|
||||
// Which timestamp column drives slot bucketing. Default = delivery time
|
||||
// (operator's primary mental model — "did this order land in the X-Y wave?").
|
||||
// Switching to Assigned/Accepted/Arrived/Pickup/Started rebuckets every row
|
||||
// through `getRowBatch(_, selectedTimeField)`.
|
||||
const [selectedTimeField, setSelectedTimeField] = useState('delivery');
|
||||
// Which timestamp column drives slot bucketing. Default = assigntime so
|
||||
// orders bucket into Morning/Afternoon/Evening by when they were assigned,
|
||||
// per current spec. The status-wise time-field dropdown is hidden for now
|
||||
// (see commented-out block in JSX), so this stays fixed at 'assigned'.
|
||||
const [selectedTimeField, setSelectedTimeField] = useState('assigned');
|
||||
const [timeFieldMenuOpen, setTimeFieldMenuOpen] = useState(false);
|
||||
const timeFieldMenuRef = useRef(null);
|
||||
|
||||
@@ -755,18 +775,27 @@ const Dispatch = ({
|
||||
const raw = window.localStorage.getItem(SLOTS_STORAGE_KEY);
|
||||
if (!raw) return BATCHES_DEFAULT;
|
||||
const parsed = JSON.parse(raw);
|
||||
// If the parsed slots length does not match BATCHES_DEFAULT_RAW, the data is stale
|
||||
// (e.g. written from a 3-slot layout version during hot reload). Discard it and load default 5 slots.
|
||||
// If the parsed slots length does not match BATCHES_DEFAULT_RAW, the data
|
||||
// is stale (e.g. written from the older 5-slot layout). Discard it and
|
||||
// load the default 3-batch layout.
|
||||
if (!Array.isArray(parsed) || parsed.length !== BATCHES_DEFAULT_RAW.length) return BATCHES_DEFAULT;
|
||||
// Re-derive label + range from the saved hours so any UI tweaks to the
|
||||
// formatter (e.g. AM/PM style) flow through to old persisted slots.
|
||||
return parsed.map((s, i) => ({
|
||||
id: s.id || `slot-${i + 1}`,
|
||||
startHour: Number(s.startHour) || 0,
|
||||
endHour: Number(s.endHour) || 24,
|
||||
label: formatSlotLabel(i, Number(s.startHour) || 0),
|
||||
range: formatSlotRange(Number(s.startHour) || 0, Number(s.endHour) || 24)
|
||||
}));
|
||||
// formatter (e.g. AM/PM style) flow through to old persisted slots. If
|
||||
// the saved id matches a default batch, prefer that batch's friendly
|
||||
// name ("Morning Batch", etc.) over the generated "Slot N · time" label.
|
||||
return parsed.map((s, i) => {
|
||||
const id = s.id || `slot-${i + 1}`;
|
||||
const startHour = Number(s.startHour) || 0;
|
||||
const endHour = Number(s.endHour) || 24;
|
||||
const defaultMatch = BATCHES_DEFAULT.find((b) => b.id === id);
|
||||
return {
|
||||
id,
|
||||
startHour,
|
||||
endHour,
|
||||
label: defaultMatch?.name || formatSlotLabel(i, startHour),
|
||||
range: formatSlotRange(startHour, endHour)
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
return BATCHES_DEFAULT;
|
||||
}
|
||||
@@ -1956,16 +1985,20 @@ const Dispatch = ({
|
||||
}, [selectedBatch]);
|
||||
|
||||
// When the user clicks a step in the focused-rider sidebar (sets focusedStop),
|
||||
// also open that marker's popup so they see the order details without a second click.
|
||||
// Wait one frame so MapController has a chance to recenter first.
|
||||
// surface that order in the centered popup overlay so the details show up
|
||||
// without a second click. focusedStop is a slim { orderid, lat, lon }, so
|
||||
// hydrate the full order out of allOrders before passing it to the popup
|
||||
// (renderOrderPopupContent needs the rich timeline + status fields).
|
||||
// Wait ~350ms so MapController has a chance to recenter first (matches
|
||||
// the prior delay before openPopup was called).
|
||||
useEffect(() => {
|
||||
if (!focusedStop) return;
|
||||
const t = setTimeout(() => {
|
||||
const marker = orderMarkerRefs.current[String(focusedStop.orderid)];
|
||||
if (marker && typeof marker.openPopup === 'function') marker.openPopup();
|
||||
const fullOrder = allOrders?.find?.((o) => String(o.orderid) === String(focusedStop.orderid));
|
||||
if (fullOrder) setCenterPopupOrder(fullOrder);
|
||||
}, 350);
|
||||
return () => clearTimeout(t);
|
||||
}, [focusedStop]);
|
||||
}, [focusedStop, allOrders]);
|
||||
|
||||
const startAnimation = () => {
|
||||
if (isAnimating) {
|
||||
@@ -2152,68 +2185,62 @@ const Dispatch = ({
|
||||
const getRiderColor = (rid) => riders.find(r => r.id === rid)?.color || '#475569';
|
||||
|
||||
// Shared rider-card markup, used in the "By Rider" panel and inside the focused-zone detail.
|
||||
const renderRiderCard = (r, i) => (
|
||||
<div key={r.id} className="rcard" onClick={() => handleRiderFocus(r)} style={{ animationDelay: `${i * 0.05}s` }}>
|
||||
<div className="rcard-top">
|
||||
<div className="rcard-emo" style={{ background: `${r.color}18`, borderColor: `${r.color}50`, color: r.color }}><MdTwoWheeler /></div>
|
||||
<div className="rcard-info">
|
||||
<div className="rcard-name">{r.riderName}</div>
|
||||
<div className="rcard-zone">{r.orders[0]?.zone_name || locationName || 'Local'} · {new Set(r.orders.map(o => o.trip_number || 1)).size} trips</div>
|
||||
const renderRiderCard = (r, i) => {
|
||||
const total = r.orders.length;
|
||||
const delivered = r.orders.filter((o) =>
|
||||
FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase())
|
||||
).length;
|
||||
const isDone = total > 0 && delivered >= total;
|
||||
return (
|
||||
<div key={r.id} className="rcard" onClick={() => handleRiderFocus(r)} style={{ animationDelay: `${i * 0.05}s` }}>
|
||||
<div className="rcard-top">
|
||||
<div className="rcard-emo" style={{ background: `${r.color}18`, borderColor: `${r.color}50`, color: r.color }}><MdTwoWheeler /></div>
|
||||
<div className="rcard-info">
|
||||
<div className="rcard-name">{r.riderName}</div>
|
||||
<div className="rcard-zone">{r.orders[0]?.zone_name || locationName || 'Local'} · {new Set(r.orders.map(o => o.trip_number || 1)).size} trips</div>
|
||||
</div>
|
||||
<div
|
||||
className={`rcard-badge ${isDone ? 'is-done' : ''}`}
|
||||
style={isDone ? undefined : { background: `${r.color}18`, color: r.color }}
|
||||
title={`${delivered} delivered of ${total} total`}
|
||||
>
|
||||
{delivered}/{total}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bar-bg"><div className="bar-fg" style={{ width: `${Math.min(100, (total / 15) * 100)}%`, background: r.color }}></div></div>
|
||||
<div className="rcard-meta"><span><Ico><MdStraighten /></Ico>{r.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0).toFixed(1)} km</span><span><Ico><MdAccountBalanceWallet /></Ico>₹{r.orders.reduce((s, o) => s + parseFloat(o.profit || 0), 0).toFixed(0)}</span></div>
|
||||
<div className="step-ids">
|
||||
{r.orders.slice(0, 15).map(o => <span key={o.orderid} className="step-id">S{o.step}</span>)}
|
||||
</div>
|
||||
<div className="rcard-badge" style={{ background: `${r.color}18`, color: r.color }}>{r.orders.length}</div>
|
||||
</div>
|
||||
<div className="bar-bg"><div className="bar-fg" style={{ width: `${Math.min(100, (r.orders.length / 15) * 100)}%`, background: r.color }}></div></div>
|
||||
<div className="rcard-meta"><span><Ico><MdStraighten /></Ico>{r.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0).toFixed(1)} km</span><span><Ico><MdAccountBalanceWallet /></Ico>₹{r.orders.reduce((s, o) => s + parseFloat(o.profit || 0), 0).toFixed(0)}</span></div>
|
||||
<div className="step-ids">
|
||||
{r.orders.slice(0, 15).map(o => <span key={o.orderid} className="step-id">S{o.step}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
// Returns true when the order's centered popup should stay open even after
|
||||
// the cursor leaves the marker: either explicitly pinned via click, or the
|
||||
// matching compare-step is focused (so clicking a step in the right panel
|
||||
// keeps the card visible while the maps recenter).
|
||||
const isOrderPopupPinned = (o) => {
|
||||
if (!o) return false;
|
||||
if (pinnedPopupsRef.current.has(String(o.orderid))) return true;
|
||||
if (compareOpen && focusedRider && o.deliveryid != null) {
|
||||
const track = riderActualTracks.find((t) => String(t.deliveryid) === String(o.deliveryid));
|
||||
if (track && focusedCompareStep === track.sequenceStep) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Shared order-popup body used by both the planned-route number markers
|
||||
// and the Compare mode actual-track drop pins. Extracted so clicking a
|
||||
// pin on either layer surfaces the exact same Timeline + Details + KM
|
||||
// chips — operators learn the layout once and trust it everywhere.
|
||||
// Hover behavior (keep-open while cursor is over the card) is owned by
|
||||
// the centered overlay wrapper, not this function.
|
||||
const renderOrderPopupContent = (o) => {
|
||||
const statusStyle = getStatusStyle(o.orderstatus);
|
||||
|
||||
const isPinned = () => {
|
||||
if (pinnedPopupsRef.current.has(String(o.orderid))) return true;
|
||||
if (compareOpen && focusedRider && o.deliveryid != null) {
|
||||
const track = riderActualTracks.find((t) => String(t.deliveryid) === String(o.deliveryid));
|
||||
if (track && focusedCompareStep === track.sequenceStep) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (popupHoverTimerRef.current) {
|
||||
clearTimeout(popupHoverTimerRef.current);
|
||||
popupHoverTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (isPinned()) return;
|
||||
|
||||
if (popupHoverTimerRef.current) {
|
||||
clearTimeout(popupHoverTimerRef.current);
|
||||
}
|
||||
popupHoverTimerRef.current = setTimeout(() => {
|
||||
if (activePopupMarkerRef.current) {
|
||||
activePopupMarkerRef.current.closePopup();
|
||||
activePopupMarkerRef.current = null;
|
||||
}
|
||||
popupHoverTimerRef.current = null;
|
||||
}, 200);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
<div style={{ height: '100%', width: '100%' }}>
|
||||
<div className="pu-header">
|
||||
<div className="pu-header-top">
|
||||
<div className="pu-id">ORDER #{o.orderid}</div>
|
||||
@@ -2254,21 +2281,31 @@ const Dispatch = ({
|
||||
<div className="pu-section">
|
||||
<div className="pu-section-label">Details</div>
|
||||
<div className="pu-details-grid">
|
||||
{o.pickupcustomer && (
|
||||
{(o.pickupcustomer || o.locationname || o.pickuplocation) && (
|
||||
<div className="pu-detail">
|
||||
<div className="pu-detail-icon"><MdRestaurant /></div>
|
||||
<div className="pu-detail-body">
|
||||
<div className="pu-detail-label">Kitchen</div>
|
||||
<div className="pu-detail-value" title={o.pickupcustomer}>{o.pickupcustomer}</div>
|
||||
<div className="pu-detail-label">Pickup</div>
|
||||
<div
|
||||
className="pu-detail-value"
|
||||
title={o.pickupcustomer || o.locationname || o.pickuplocation}
|
||||
>
|
||||
{o.pickupcustomer || o.locationname || o.pickuplocation}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(o.locationname || o.pickuplocation) && (
|
||||
{(o.deliverysuburb || o.deliveryaddress) && (
|
||||
<div className="pu-detail">
|
||||
<div className="pu-detail-icon"><MdPlace /></div>
|
||||
<div className="pu-detail-body">
|
||||
<div className="pu-detail-label">Pickup</div>
|
||||
<div className="pu-detail-value" title={o.locationname || o.pickuplocation}>{o.locationname || o.pickuplocation}</div>
|
||||
<div className="pu-detail-label">Drop</div>
|
||||
<div
|
||||
className="pu-detail-value"
|
||||
title={o.deliveryaddress || o.deliverysuburb}
|
||||
>
|
||||
{o.deliverysuburb || extractArea(o.deliveryaddress)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -2403,46 +2440,37 @@ const Dispatch = ({
|
||||
else delete orderMarkerRefs.current[String(o.orderid)];
|
||||
}}
|
||||
eventHandlers={{
|
||||
mouseover: (e) => {
|
||||
const marker = e.target;
|
||||
mouseover: () => {
|
||||
if (popupHoverTimerRef.current) {
|
||||
clearTimeout(popupHoverTimerRef.current);
|
||||
popupHoverTimerRef.current = null;
|
||||
}
|
||||
activePopupMarkerRef.current = marker;
|
||||
marker.openPopup();
|
||||
setCenterPopupOrder(o);
|
||||
},
|
||||
mouseout: (e) => {
|
||||
const marker = e.target;
|
||||
mouseout: () => {
|
||||
if (pinnedPopupsRef.current.has(String(o.orderid))) return;
|
||||
|
||||
if (popupHoverTimerRef.current) {
|
||||
clearTimeout(popupHoverTimerRef.current);
|
||||
}
|
||||
popupHoverTimerRef.current = setTimeout(() => {
|
||||
marker.closePopup();
|
||||
if (activePopupMarkerRef.current === marker) {
|
||||
activePopupMarkerRef.current = null;
|
||||
}
|
||||
setCenterPopupOrder((cur) =>
|
||||
cur && String(cur.orderid) === String(o.orderid) ? null : cur
|
||||
);
|
||||
popupHoverTimerRef.current = null;
|
||||
}, 200);
|
||||
},
|
||||
click: (e) => {
|
||||
click: () => {
|
||||
const id = String(o.orderid);
|
||||
if (pinnedPopupsRef.current.has(id)) {
|
||||
pinnedPopupsRef.current.delete(id);
|
||||
e.target.closePopup();
|
||||
setCenterPopupOrder(null);
|
||||
} else {
|
||||
pinnedPopupsRef.current.add(id);
|
||||
e.target.openPopup();
|
||||
setCenterPopupOrder(o);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Popup maxWidth={520} minWidth={460} className="dispatch-popup" autoPan={true} autoPanPadding={[40, 40]}>
|
||||
{renderOrderPopupContent(o)}
|
||||
</Popup>
|
||||
</Marker>
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -2963,11 +2991,10 @@ const Dispatch = ({
|
||||
|
||||
{shouldFetchLive && viewMode !== 'rider-info' && (
|
||||
<div id="batch-row">
|
||||
<span className="batch-label">Slot</span>
|
||||
{/* Dropdown to pick which timestamp drives slot bucketing. Mirrors
|
||||
the hub-location dropdown's look so it reads as the same kind of
|
||||
filter control. The chosen field reruns batchCounts +
|
||||
filteredLiveRows via selectedTimeField. */}
|
||||
<span className="batch-label">Batch</span>
|
||||
{/* Status-wise (time-field) filter is hidden for now per spec —
|
||||
bucketing is locked to `assigntime`. Restore this block to bring
|
||||
back the Delivered/Pending/Assigned/... dropdown.
|
||||
<div className="time-field-wrap" ref={timeFieldMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -2978,7 +3005,7 @@ const Dispatch = ({
|
||||
title="Bucket slots by this timestamp"
|
||||
>
|
||||
<MdAccessTime />
|
||||
<span className="time-field-text">{TIME_FIELDS.find((f) => f.id === selectedTimeField)?.label || 'Delivery'}</span>
|
||||
<span className="time-field-text">{TIME_FIELDS.find((f) => f.id === selectedTimeField)?.label || 'Delivered'}</span>
|
||||
<MdExpandMore className="time-field-caret" />
|
||||
</button>
|
||||
{timeFieldMenuOpen && (
|
||||
@@ -3006,9 +3033,11 @@ const Dispatch = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Slot editor — lets the operator tweak start/end hours, add a new
|
||||
slot, remove an existing one, or reset to defaults. Persists via
|
||||
SLOTS_STORAGE_KEY in localStorage. */}
|
||||
*/}
|
||||
{/* Slot editor (Edit slots button + panel) is hidden for now per
|
||||
spec — the three batches (Morning / Afternoon / Evening) are
|
||||
fixed. Restore this block to bring back the operator-editable
|
||||
start/end hours, add-slot, and reset-to-defaults controls.
|
||||
<div className="slot-edit-wrap" ref={slotEditRef}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -3040,9 +3069,6 @@ const Dispatch = ({
|
||||
step={0.5}
|
||||
value={s.startHour}
|
||||
onChange={(e) => {
|
||||
// Half-hour-aware: parseFloat + snap to nearest 0.5
|
||||
// so 12.5 (12:30) is a valid value and odd inputs
|
||||
// like 12.7 round to 12.5.
|
||||
const raw = parseFloat(e.target.value);
|
||||
const snapped = Number.isFinite(raw) ? Math.round(raw * 2) / 2 : 0;
|
||||
const v = Math.max(0, Math.min(23.5, snapped));
|
||||
@@ -3127,6 +3153,7 @@ const Dispatch = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
*/}
|
||||
{/* Inner scroller — keeps the "Slot" label fixed while the chip list scrolls
|
||||
horizontally when it overflows. */}
|
||||
<div className="batch-scroll">
|
||||
@@ -3191,7 +3218,7 @@ const Dispatch = ({
|
||||
className={`ri-rider-item ${isActive ? 'active' : ''}`}
|
||||
onClick={() => setRiderInfoUserid(r.id)}
|
||||
>
|
||||
<span className="ri-rider-dot" style={{ background: getRiderColor(r.id) }} />
|
||||
<span className="ri-rider-dot" style={{ background: getStableRiderColor(r.id) }} />
|
||||
<span className="ri-rider-info-block">
|
||||
<span className="ri-rider-name">{r.riderName}</span>
|
||||
<span className="ri-rider-meta">#{r.id}</span>
|
||||
@@ -4371,32 +4398,29 @@ const Dispatch = ({
|
||||
eventHandlers={
|
||||
orderForTrack
|
||||
? {
|
||||
// Match the planned-route marker UX: hover opens
|
||||
// the rich order popup, leaving it pinned while
|
||||
// a step is focused (so click-to-focus keeps the
|
||||
// modal visible after the cursor moves away). The
|
||||
// Match the planned-route marker UX: hover surfaces
|
||||
// the rich order card in the centered overlay. The
|
||||
// ~200ms grace timer on mouseout lets the cursor
|
||||
// travel onto the popup itself without flicker.
|
||||
mouseover: (e) => {
|
||||
const marker = e.target;
|
||||
// travel onto the overlay without flicker. Pinning
|
||||
// is implicit while focusedCompareStep === this
|
||||
// step, so the card stays put while the user is
|
||||
// inspecting this delivery.
|
||||
mouseover: () => {
|
||||
if (popupHoverTimerRef.current) {
|
||||
clearTimeout(popupHoverTimerRef.current);
|
||||
popupHoverTimerRef.current = null;
|
||||
}
|
||||
activePopupMarkerRef.current = marker;
|
||||
marker.openPopup();
|
||||
setCenterPopupOrder(orderForTrack);
|
||||
},
|
||||
mouseout: (e) => {
|
||||
mouseout: () => {
|
||||
if (focusedCompareStep === t.sequenceStep) return;
|
||||
const marker = e.target;
|
||||
if (popupHoverTimerRef.current) {
|
||||
clearTimeout(popupHoverTimerRef.current);
|
||||
}
|
||||
popupHoverTimerRef.current = setTimeout(() => {
|
||||
marker.closePopup();
|
||||
if (activePopupMarkerRef.current === marker) {
|
||||
activePopupMarkerRef.current = null;
|
||||
}
|
||||
setCenterPopupOrder((cur) =>
|
||||
cur && String(cur.orderid) === String(orderForTrack.orderid) ? null : cur
|
||||
);
|
||||
popupHoverTimerRef.current = null;
|
||||
}, 200);
|
||||
},
|
||||
@@ -4452,17 +4476,6 @@ const Dispatch = ({
|
||||
})()}
|
||||
</Tooltip>
|
||||
)}
|
||||
{orderForTrack && (
|
||||
<Popup
|
||||
maxWidth={520}
|
||||
minWidth={460}
|
||||
className="dispatch-popup"
|
||||
autoPan={true}
|
||||
autoPanPadding={[40, 40]}
|
||||
>
|
||||
{renderOrderPopupContent(orderForTrack)}
|
||||
</Popup>
|
||||
)}
|
||||
</Marker>
|
||||
</React.Fragment>
|
||||
);
|
||||
@@ -4508,6 +4521,10 @@ const Dispatch = ({
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{/* Right-corner rider/kitchen legend hidden per spec — the same
|
||||
delivered/total count now renders inside the left sidebar's
|
||||
rider card badge (see renderRiderCard). Restore this block to
|
||||
bring back the floating top-right chip list.
|
||||
<div id="ov-tr">
|
||||
{viewMode === 'kitchens' ? (
|
||||
kitchens.slice(0, 10).map(k => {
|
||||
@@ -4541,6 +4558,7 @@ const Dispatch = ({
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
*/}
|
||||
|
||||
<div id="ov-br">
|
||||
<button className={`sbt ${isAnimating ? 'active' : ''}`} onClick={startAnimation} style={{ boxShadow: 'var(--shadow-lg)', background: isAnimating ? 'var(--accent)' : '#fff' }}>
|
||||
@@ -4852,6 +4870,48 @@ const Dispatch = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Centered order popup — sibling of the map (NOT inside leaflet's
|
||||
transformed panes) so position: fixed actually pins it to the
|
||||
viewport. Replaces the marker-attached leaflet Popup so the rich
|
||||
order card stays fully visible at the screen center on small
|
||||
laptop displays. */}
|
||||
{centerPopupOrder && (
|
||||
<div
|
||||
className="dispatch-popup-center"
|
||||
role="dialog"
|
||||
aria-label={`Order ${centerPopupOrder.orderid} details`}
|
||||
onMouseEnter={() => {
|
||||
if (popupHoverTimerRef.current) {
|
||||
clearTimeout(popupHoverTimerRef.current);
|
||||
popupHoverTimerRef.current = null;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (isOrderPopupPinned(centerPopupOrder)) return;
|
||||
if (popupHoverTimerRef.current) clearTimeout(popupHoverTimerRef.current);
|
||||
popupHoverTimerRef.current = setTimeout(() => {
|
||||
setCenterPopupOrder(null);
|
||||
popupHoverTimerRef.current = null;
|
||||
}, 200);
|
||||
}}
|
||||
>
|
||||
<div className="dispatch-popup-card dispatch-popup">
|
||||
<button
|
||||
type="button"
|
||||
className="dispatch-popup-center-close"
|
||||
aria-label="Close order details"
|
||||
onClick={() => {
|
||||
pinnedPopupsRef.current.delete(String(centerPopupOrder.orderid));
|
||||
setCenterPopupOrder(null);
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{renderOrderPopupContent(centerPopupOrder)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user