updates on the ui design and desing updates
This commit is contained in:
@@ -10,7 +10,7 @@ Rules for editing `Dispatch.js`, `Preview.js`, `CompareDataPanel.js`, and `dispa
|
||||
Dispatch.js defines the canonical batch hour ranges. `deliveries.js` mirrors them — **the two pages must agree on which batch a given row belongs to**, otherwise the same delivery shows up in one batch on one page and a different batch on the other.
|
||||
|
||||
```js
|
||||
// BATCH_OPTIONS — half-open [startHour, endHour) in LOCAL time, not UTC
|
||||
// BATCHES_DEFAULT_RAW — half-open [startHour, endHour) in LOCAL time, not UTC
|
||||
[
|
||||
{ id: 'morning', startHour: 0, endHour: 8 }, // 12 AM – 8 AM
|
||||
{ id: 'afternoon', startHour: 9, endHour: 12.5 }, // 9 AM – 12:30 PM
|
||||
@@ -21,11 +21,19 @@ Dispatch.js defines the canonical batch hour ranges. `deliveries.js` mirrors the
|
||||
**Gaps are intentional** (8–9 AM, 12 PM–4 PM, after 7 PM). Rows that fall in a gap belong to no batch — *not* to the nearest one.
|
||||
|
||||
### Time-field selection (`selectedTimeField`)
|
||||
Default `'assigned'` → bucket key is `['assigntime']`. Other options use other timestamp fields (`pickedtime`, `deliverytime`). If you add a new time field option, make sure `deliveries.js` is updated too — they read each other's bucketing.
|
||||
Default `'due'` → bucket key is `['expecteddeliverytime']` (the booking's promised delivery slot, `serviceoptions[0].estimateddeliveryat`). `deliveries.js` hardcodes the same key in `BATCH_TIME_KEYS`. If you change one, change both — they read each other's bucketing.
|
||||
|
||||
### ⛔ Never bucket on `assigntime`
|
||||
It is **not** an assignment time. The Doormile bookings feed has no assignment timestamp, so `api.js` maps `assigntime` to the booking's `updatedat` — its last-modified column. Any status change, parcel scan, payment or pickup-complete re-stamps it.
|
||||
|
||||
Bucketing on it (which both pages did until this was found) means an order silently leaves the batch it belongs to and joins whichever window contains the current clock time, so the same orders appear under Afternoon and then Evening on the same day. The Dispatch page's 15-second poll makes the counts move on their own.
|
||||
|
||||
Displaying it as a "last updated" stamp is fine — that's all the reports use it for. The true assignment record lives on `bookingassignments`, reachable only per-booking via `GET /admin/bookings/:id/track` (`{ booking, assignments[], riders[] }`); its `assignments[]` has not been observed non-empty, so it is not a verified source yet. If the backend ever exposes an `assignedat` on the list feed, that becomes the correct bucket key.
|
||||
|
||||
### Don'ts
|
||||
- Don't bucket in UTC. Use `dayjs(t)` (local), not `dayjs(t).utc()`. The original deliveries page had a UTC bucketing bug that hid orders mid-day; the multi-line comment above `getRowBatchId` in `deliveries.js` (search for `getRowBatchId`) explains it. Don't reintroduce.
|
||||
- Don't bucket bare `YYYY-MM-DD` strings — they parse to midnight and mis-bucket into Morning. Skip them.
|
||||
- Don't bucket on a timestamp that the backend re-stamps. See above.
|
||||
- Don't add a 4th batch without updating both pages and confirming with the backend what the new boundary means for assignments.
|
||||
|
||||
---
|
||||
|
||||
@@ -75,16 +75,17 @@
|
||||
}
|
||||
|
||||
.dispatch-container .logo-badge {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #3A3A3A, #2563eb);
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 800;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dispatch-container .logo-badge-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.dispatch-container .logo-name {
|
||||
@@ -1902,21 +1903,6 @@
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.dispatch-container .kitchen-mark {
|
||||
background: #f59e0b;
|
||||
color: #fff;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 800;
|
||||
font-size: 14px;
|
||||
border: 3px solid #fff;
|
||||
box-shadow: 0 0 20px rgba(245, 158, 11, 0.6), 0 0 40px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.dispatch-container .rcard-info {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -4945,6 +4931,12 @@
|
||||
|
||||
/* Markers - styled as clean flags natively in Dispatch.js */
|
||||
|
||||
/* Colour + shape only. createKitchenIcon() in Dispatch.js pins
|
||||
width/height/font-size/border-width/box-shadow inline on every render, so
|
||||
the size values below are never what actually paints — the marker is a fixed
|
||||
31px (38px focused) at every zoom level. Change the size there, not here.
|
||||
(A second, conflicting .kitchen-mark rule used to sit ~3000 lines earlier in
|
||||
this file and was silently overridden by this one. It has been removed.) */
|
||||
.dispatch-container .kitchen-mark {
|
||||
background: var(--kitchen);
|
||||
border: 3px solid #fff;
|
||||
@@ -6814,10 +6806,8 @@
|
||||
}
|
||||
|
||||
.dispatch-container .logo-badge {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.dispatch-container .logo {
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'leaflet/dist/leaflet.css';
|
||||
// Compare → Combined mode to render planned + actual as parallel rails when
|
||||
// they share the same road geometry (otherwise they'd stack and read as one).
|
||||
import '../../../utils/leafletPolylineOffset';
|
||||
import doormileMark from 'assets/images/doormile-mark.png';
|
||||
import dayjs from 'dayjs';
|
||||
import { useInfiniteQuery, useQueries, useQuery, useMutation } from '@tanstack/react-query';
|
||||
import {
|
||||
@@ -137,7 +138,7 @@ const pickupName = (o) => o.pickupcustomer || o.kitchen_key || o.locationname ||
|
||||
// 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.
|
||||
// Three named batches, bucketed by assigntime per spec:
|
||||
// Three named batches, bucketed by expected delivery time (see TIME_FIELDS):
|
||||
// • Morning Batch: before 8 AM (00:00 → 08:00)
|
||||
// • Afternoon Batch: 9 AM → 12:30 PM (09:00 → 12:30)
|
||||
// • Evening Batch: 4 PM → 7 PM (16:00 → 19:00)
|
||||
@@ -217,7 +218,12 @@ const getBatchForHour = (h, batches) => {
|
||||
// with a fallback to expecteddeliverytime so undelivered orders still bucket.
|
||||
const TIME_FIELDS = [
|
||||
{ id: 'delivered', label: 'Delivered', keys: ['deliverytime'] },
|
||||
{ id: 'pending', label: 'Pending', keys: ['expecteddeliverytime'] },
|
||||
// The batch-bucketing default. `expecteddeliverytime` is the booking's
|
||||
// promised delivery slot (serviceoptions[0].estimateddeliveryat) — it is set
|
||||
// once and not re-stamped as the order progresses, which is exactly what a
|
||||
// "wave" needs. Do not point this at `assigntime`: that field is the
|
||||
// booking's last-modified column (see api.js) and drifts through the day.
|
||||
{ id: 'due', label: 'Due', keys: ['expecteddeliverytime'] },
|
||||
{ id: 'assigned', label: 'Assigned', keys: ['assigntime'] },
|
||||
{ id: 'accepted', label: 'Accepted', keys: ['acceptedtime'] },
|
||||
{ id: 'started', label: 'Started', keys: ['starttime'] },
|
||||
@@ -294,20 +300,6 @@ function MapAutoResize({ trigger }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Leaflet divIcons are pixel-fixed by design — they don't shrink as the map
|
||||
// zooms out, so a kitchen badge sized to look right at street level (zoom
|
||||
// ~13+) reads as oversized once the operator zooms out to see a whole city.
|
||||
// Reports current zoom up so the kitchen marker size can scale down with it.
|
||||
function MapZoomTracker({ onZoomChange }) {
|
||||
const map = useMap();
|
||||
useEffect(() => {
|
||||
onZoomChange(map.getZoom());
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [map]);
|
||||
useMapEvents({ zoomend: (e) => onZoomChange(e.target.getZoom()) });
|
||||
return null;
|
||||
}
|
||||
|
||||
// haversineKm/polylineLengthKm/kalmanSmoothGps moved to dispatchShared.js so
|
||||
// deliveries.js's Update Status dialog can compute the same real, GPS-based
|
||||
// Actual KMs figure instead of leaving that field permanently blank.
|
||||
@@ -930,9 +922,6 @@ const Dispatch = ({
|
||||
const [centerPopupOrder, setCenterPopupOrder] = useState(null);
|
||||
const isControlled = selectedRiderId !== undefined;
|
||||
const [clock, setClock] = useState('');
|
||||
// Current map zoom, mirrored from MapZoomTracker — drives the kitchen
|
||||
// marker's zoom-responsive size (see kitchenIconZoomScale below).
|
||||
const [mapZoom, setMapZoom] = useState(12);
|
||||
|
||||
// Fetch all hubs/locations the logged-in user has access to. The list is
|
||||
// rendered as a dropdown next to the Dispatch title so the operator can
|
||||
@@ -952,11 +941,20 @@ const Dispatch = ({
|
||||
const [locationMenuOpen, setLocationMenuOpen] = useState(false);
|
||||
const locationMenuRef = useRef(null);
|
||||
|
||||
// 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');
|
||||
// Which timestamp column drives slot bucketing. Default = 'due'
|
||||
// (expecteddeliverytime) so orders bucket into Morning/Afternoon/Evening by
|
||||
// the delivery slot they were promised for. The status-wise time-field
|
||||
// dropdown is hidden for now (see commented-out block in JSX), so this stays
|
||||
// fixed at 'due'.
|
||||
//
|
||||
// Was 'assigned' (assigntime). That looked right — the spec says "bucket by
|
||||
// assign time" — but on the Doormile backend `assigntime` is mapped to the
|
||||
// booking's last-modified column (api.js), so it moves whenever the row is
|
||||
// touched. Today's live orders therefore migrated into whichever batch
|
||||
// contained the current clock time, and the 15s poll made the counts shift
|
||||
// on their own. deliveries.js buckets on the same field for the same reason
|
||||
// — the two pages must agree (see this folder's CLAUDE.md §1).
|
||||
const [selectedTimeField, setSelectedTimeField] = useState('due');
|
||||
const [timeFieldMenuOpen, setTimeFieldMenuOpen] = useState(false);
|
||||
const timeFieldMenuRef = useRef(null);
|
||||
|
||||
@@ -2674,24 +2672,38 @@ const Dispatch = ({
|
||||
}
|
||||
};
|
||||
|
||||
// divIcons are pixel-fixed and don't shrink with the map's own zoom-out,
|
||||
// so scale the badge down as the operator zooms out past street level
|
||||
// (zoom 12 is this map's initial zoom — see the MapContainer below).
|
||||
// Clamped to 1 at the top so zooming in past the default never grows the
|
||||
// badge beyond its original design size, and floored at 0.55 so it never
|
||||
// shrinks past legibility.
|
||||
const kitchenIconZoomScale = Math.max(0.55, Math.min(1, 1 + (mapZoom - 12) * 0.12));
|
||||
// Kitchen badge is a FIXED 31px at every zoom level — deliberately not
|
||||
// zoom-responsive.
|
||||
//
|
||||
// It used to scale with zoom (46px at street level, shrinking as the operator
|
||||
// zoomed out) which made the marker read as a big amber blob over a
|
||||
// city-wide view. Rather than tune the curve, the size is now constant: a
|
||||
// pin that never changes size is easier to scan for, and 31px is the size
|
||||
// the scaled version happened to hit at city zoom, which is the level
|
||||
// operators actually work at.
|
||||
//
|
||||
// The white ring and amber glow are pinned to match. They're what made the
|
||||
// old marker look oversized — the stylesheet painted a 3px ring and a
|
||||
// 20/40px blur regardless of badge size, so a small dot sat inside an ~80px
|
||||
// halo. These inline values win over the .kitchen-mark rule in Dispatch.css,
|
||||
// which now only carries the unscaled design defaults.
|
||||
//
|
||||
// The focused badge stays proportionally larger (same 56:46 ratio as the
|
||||
// original design) so drilling into a kitchen still visibly marks it.
|
||||
const KITCHEN_ICON_SIZE = 31;
|
||||
const KITCHEN_ICON_FOCUSED_SIZE = 38;
|
||||
|
||||
const createKitchenIcon = (name, focused = false) => {
|
||||
const base = focused ? 56 : 46;
|
||||
const size = Math.round(base * kitchenIconZoomScale);
|
||||
const size = focused ? KITCHEN_ICON_FOCUSED_SIZE : KITCHEN_ICON_SIZE;
|
||||
const anchor = Math.round(size / 2);
|
||||
const border = focused ? 3 : 2;
|
||||
const glow = 14;
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
iconSize: [size, size],
|
||||
iconAnchor: [anchor, anchor],
|
||||
popupAnchor: [0, -(anchor + 2)],
|
||||
html: `<div class="kitchen-mark${focused ? ' is-focused' : ''}" style="width:${size}px;height:${size}px;font-size:${Math.max(11, Math.round(size * 0.38))}px">${(name || 'K').charAt(0).toUpperCase()}</div>`
|
||||
html: `<div class="kitchen-mark${focused ? ' is-focused' : ''}" style="width:${size}px;height:${size}px;font-size:${Math.round(size * 0.38)}px;border-width:${border}px;box-shadow:0 0 ${glow}px rgba(245,158,11,0.8), 0 0 ${glow * 2}px rgba(245,158,11,0.4)">${(name || 'K').charAt(0).toUpperCase()}</div>`
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3292,7 +3304,9 @@ const Dispatch = ({
|
||||
{!embedded && (
|
||||
<div id="hdr">
|
||||
<div className="logo">
|
||||
<div className="logo-badge">D</div>
|
||||
<div className="logo-badge">
|
||||
<img src={doormileMark} alt="Doormile" className="logo-badge-img" />
|
||||
</div>
|
||||
<div className="logo-name">Dispatch</div>
|
||||
{appLocations && appLocations.length > 0 && (
|
||||
<div className="logo-city-wrap" ref={locationMenuRef}>
|
||||
@@ -3637,8 +3651,10 @@ const Dispatch = ({
|
||||
<div id="batch-row">
|
||||
<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.
|
||||
bucketing is locked to `due` (expecteddeliverytime). Restore this
|
||||
block to bring back the Delivered/Due/Assigned/... dropdown. Note
|
||||
that picking "Assigned" there buckets on the booking's
|
||||
last-modified column, which drifts through the day (see api.js).
|
||||
<div className="time-field-wrap" ref={timeFieldMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -4902,7 +4918,6 @@ const Dispatch = ({
|
||||
/>
|
||||
)}
|
||||
<MapAutoResize trigger={`${sidebarCollapsed}|${compareOpen}|${compareDataCollapsed}`} />
|
||||
<MapZoomTracker onZoomChange={setMapZoom} />
|
||||
<MapController focusedItem={compareFocusItem || ((focusedRider || focusedKitchen) && focusedStop) || focusedRider || focusedKitchen || focusedZone} viewMode={viewMode} orders={allViewOrders} kitchens={kitchens} locationKey={selectedAppLocationId} extraPoints={allViewLivePoints} />
|
||||
{kitchens
|
||||
.filter(k => Number.isFinite(k.lat) && Number.isFinite(k.lon))
|
||||
|
||||
Reference in New Issue
Block a user