updates on the ui design and desing updates

This commit is contained in:
2026-08-14 12:09:53 +05:30
parent 8cd8e34b11
commit d3dd687a8d
17 changed files with 7808 additions and 7620 deletions

View File

@@ -19,55 +19,49 @@ These components are imported across every page. Changes here have **fan-out imp
|---|---|---|
| `PageHeader.js` | Page title + live subtitle + action slot | ✅ **Astryx** — `Heading`/`Text`/`HStack`/`StatusDot` |
| `StatCard.js` | KPI / metric tile | ✅ **Astryx** — `Card`/`Text`/`Center`/`Skeleton` |
| `StatusBadge.js` | Per-row lifecycle badge | ✅ **Astryx** — `Badge` driven by `themes/dt/status.js` |
| `StatusTabs.js` | Status filter strip with counts | ✅ **Astryx** — `TabList`/`Tab`/`Badge` |
| `DebounceSearchBar.js` | 500ms-debounced search with ⌘/Ctrl+K focus | ✅ **Astryx** — `TextInput`; **no `sx` prop** (see §4) |
| `LocationAutocomplete.js` | Zone picker on every operator page | MUI — has `pill` variant matching DT system |
| `LoaderWithImage.js` | Inline branded spinner for "loading more" rows | MUI |
| `TableLoader.js` | Inline table loading rows | ✅ **Astryx** — use this, not `OrdersTableSkeleton` |
| `LocationAutocomplete.js` | Zone picker on every operator page | ✅ **Astryx** |
| `AddressAutocomplete.js` | Nominatim address search | ✅ **Astryx** — `TextInput` + hand-rolled list (`Typeahead` has no freeSolo) |
| `LoaderWithImage.js` | Inline branded spinner for "loading more" rows | ✅ **Astryx** |
| `GlobalToast.js` | Global toast wrapper | ✅ Used at root |
| `SearchBar.js` | Non-debounced search input | Legacy — prefer `DebounceSearchBar` |
| `TableLoader.js` | Inline table loading state | Legacy — prefer `OrdersTableSkeleton` |
| `TitleCard.js` | Old page header | ⛔ **Legacy** — do not use on new pages. The replacement is `PageHeader.js`. |
Every component in this folder is on Astryx. `SearchBar.js` and `TitleCard.js` were deleted during the migration — if you find a reference to either, it's stale: use `DebounceSearchBar` and `PageHeader`.
---
## 2. Design system token discipline
## 2. Design system discipline
The `DT` design tokens (palette, alpha helpers, `pillFieldSx`, `SoftPaper`, `AccentAvatar`) are documented in the root `CLAUDE.md` §6 and the source-of-truth implementation is the token block near the top of `src/pages/nearle/deliveries/deliveries.js` (search for `const DT = {`).
The design system is documented in the root `CLAUDE.md` §6. Read it before editing anything here.
**Hard rules when editing components here:**
**Hard rules when editing components in this folder:**
- **Universal brand colour is `#C01227`** (Doormile Express red — from `themes/theme/default.js` `primary.main`). Every brand surface (page header, dialog header, KPI primary tile, search bars, edit-action buttons, scrollbars) uses this. Gradient pair: `#C01227 → #D25463`.
- **Semantic status palette is distinct** from the brand. Use these for lifecycle indicators only: sky `#0ea5e9`, emerald `#10b981`, amber `#f59e0b`, red `#ef4444`, status-purple `#8b5cf6`, cyan `#06b6d4`, teal `#14b8a6`, orange `#f97316`, muted `#94a3b8`, indigo `#6366f1` (Accepted status). Don't replace these with brand red — operators colour-code on them.
- Don't introduce a colour from `theme.palette` for new surfaces — those are Mantis defaults and don't match the DT system. Use the hex values above directly.
- Border radii: `12` (inner), `16` (card), `999` (pill). No other values.
- Shadows: `DT.shadowSoft` / `DT.shadowMd` / `DT.shadowPop`. No raw `box-shadow` strings.
> Some existing redesigned pages (`deliveries.js`) still use `#6366f1` as the brand accent — this is a legacy from the first design pass. `customers.js` and the `createorder1` Saved-Address dialog are already on `#C01227`. When you next edit one of the legacy pages, migrate it to brand red in the same PR.
- **The brand is black** (`DT.brand`, pinned as `--color-accent` in `themes/astryx.js`). Colour carries *meaning only* — chrome stays neutral. Read the accent as `var(--color-accent)`, never a literal `#000000`.
- **Never render a status by picking a colour by hand.** Use `<StatusBadge status={…}/>`, or `getStatusMeta()` from `themes/dt/status.js` if you need the parts. It resolves backend enums, aliases, and casing, and degrades to a neutral badge for unknown states.
- **A raw hex is acceptable in exactly one situation**: a caller-supplied status accent that the token system can't express — `StatCard`'s `color` prop and `AccentAvatar`. Everything else uses `var(--color-*)`, `var(--spacing-*)`, `var(--radius-*)`.
- **No `<div>`/`<span>` for layout.** `HStack`, `VStack`, `Grid`, `Center`, `Stack`.
- **No `sx` prop** — it doesn't exist in Astryx. Restyle through `themes/astryx.js` so every call site moves together.
- **Check the real API before using a component**: `npx -y @astryxdesign/cli component <Name>`. (`yarn dlx` from the generated block does not work — `yarn` is not on PATH.)
---
## 3. `LocationAutocomplete` — the `pill` prop is opt-in
The component supports two visual modes, controlled by the `pill` prop:
## 3. `LocationAutocomplete` — the zone picker
```jsx
// Default (legacy, "Select Zones" label, outlined TextField) — used by old pages
<LocationAutocomplete setAppId={...} setLocoName={...} />
// Pill variant — used by all redesigned pages (deliveries, tenants, pricing, customers)
<LocationAutocomplete
pill
accentColor="#6366f1"
icon={<MdMyLocation size={14} />}
locaName={locaName}
setAppId={setAppId}
setLocoName={setLocoName}
setPage={setPage} // optional — resets pagination on zone change
placeholder="Select Zone"
paperComponent={SoftPaper}
setAppId={...}
setLocoName={...}
/>
```
- **For any new page:** use `pill`. Always.
- **For existing legacy pages (orders, invoice, riders, etc.):** keep the default until that page is redesigned. Don't change them piecemeal.
- The `accentColor` defaults to `#6366f1` — only override when the page's accent is different (rare).
- The old `pill` / `accentColor` / `icon` / `paperComponent` props are **gone**. There is one visual mode now: appearance comes from the Astryx theme, so the picker looks identical on every page. If you find a call site passing them, it's stale.
- The list is derived from `GET /admin/hubs`, not a real zones endpoint — the `applocationid` "zone" concept was jupiter-only and most list endpoints on the new backend don't accept it as a filter. Don't assume selecting a zone actually filters a page you haven't checked (root `CLAUDE.md` §11).
---
@@ -93,10 +87,10 @@ The component supports two visual modes, controlled by the `pill` prop:
Only when:
1. Used by **two or more pages** in production code.
2. Has its own internal state or shortcuts (otherwise just use an `AccentAvatar` + `Box` inline).
2. Has its own internal state or shortcuts (otherwise compose Astryx primitives inline).
3. The component encapsulates a non-trivial pattern that's been duplicated more than twice.
Don't add a wrapper that just renames an MUI primitive (e.g. `<NearleButton>`). Don't add a component that's a single instance of a styled `Paper`.
Don't add a wrapper that just renames an Astryx primitive (e.g. `<NearleButton>`) — restyle it in the theme instead. Don't add a component that's a single instance of a styled `Card`.
---
@@ -109,10 +103,19 @@ Don't add a wrapper that just renames an MUI primitive (e.g. `<NearleButton>`).
> to `14` and the restrained set. That drift is what made pages stop looking
> alike.
`DT`, the alpha helpers (`a`/`tint`/`soft`/`ring`/`edge`), `pillFieldSx`, and
the table chrome (`tableScrollSx`/`tableHeadSx`/`tableRowSx`) live in
`src/themes/dt/tokens.js`. `SoftPaper` and `AccentAvatar` live in
`src/themes/dt/primitives.js`. **Import them. Never re-declare them in a page.**
The same failure repeated with status: five pages each grew a private
`STATUS_META`, and they disagreed on what to call the same order — that is why
`themes/dt/status.js` now exists.
`DT`, the raw `STATUS` hexes, and the alpha helpers (`a`/`tint`/`soft`/`ring`/`edge`)
live in `src/themes/dt/tokens.js`. Lifecycle rendering lives in
`src/themes/dt/status.js`. `TableScroll` and `AccentAvatar` live in
`src/themes/dt/primitives.js`. The theme itself lives in `src/themes/astryx.js`.
**Import them. Never re-declare them in a page.**
The `pillFieldSx` / `table*Sx` exports in `tokens.js` are MUI-only and marked
⛔ LEGACY — they exist solely for the pages awaiting conversion and are deleted
with the MUI dependency. Don't add a call site.
If a page genuinely needs a different value, it should be a new named export in
the shared module, not a local override — that way the divergence is visible in

View File

@@ -0,0 +1,38 @@
import PropTypes from 'prop-types';
import { Badge } from '@astryxdesign/core/Badge';
import { getStatusMeta } from 'themes/dt/status';
// ==============================|| STATUS BADGE ||============================== //
// The per-row lifecycle indicator for every operator table.
//
// This replaces the hand-rolled "soft pill" that each page used to build out of
// an <AccentAvatar> plus a tinted <Box> — six near-identical copies that drifted
// on padding, radius, and icon size. Astryx's Badge already is that pill, so the
// only thing worth sharing is the status → variant lookup, which now lives in
// themes/dt/status.js.
//
// <StatusBadge status={row.orderstatus} />
//
// `status` accepts a canonical key (`pending`), a raw backend enum
// (`miler_assigned`), or any casing — getStatusMeta resolves all three and
// falls back to a neutral badge showing the raw string, so an unmapped status
// is visible rather than silently blank.
//
// Pass `label` only to override the registry's wording for a page-specific
// nuance; the default keeps every page calling the same state the same thing,
// which is the whole point of the registry.
export default function StatusBadge({ status, label, showIcon = true }) {
const meta = getStatusMeta(status);
const Icon = meta.icon;
return <Badge variant={meta.badge} label={label ?? meta.label} icon={showIcon ? <Icon size={14} /> : undefined} />;
}
StatusBadge.propTypes = {
status: PropTypes.string,
label: PropTypes.node,
showIcon: PropTypes.bool
};

View File

@@ -0,0 +1,71 @@
import PropTypes from 'prop-types';
import { TabList, Tab } from '@astryxdesign/core/TabList';
import { Badge } from '@astryxdesign/core/Badge';
import { getStatusMeta } from 'themes/dt/status';
// ==============================|| STATUS TABS ||============================== //
// The top-level status filter strip on every list page.
//
// Under MUI these were hand-rolled clickable <Box> pills, because MUI's <Tabs>
// couldn't carry a count chip or a per-tab accent. Astryx's Tab has an
// `endContent` slot and a `selectedIcon`, so the pills are now real tabs —
// which also means arrow-key navigation and correct `role="tab"` semantics
// come for free instead of being missing (the old pills were plain divs with
// onClick, which is half of Dispatch.js's outstanding a11y lint errors).
//
// <StatusTabs
// value={currentStatus}
// onChange={setCurrentStatus}
// tabs={[
// { status: 'pending', count: batchCounts.uncoveredLength },
// { status: 'delivered', count: batchCounts.coveredLength }
// ]}
// />
//
// Each tab's `status` resolves through the shared registry for its label and
// icon, so a page only supplies the ORDER of the tabs and where each count
// comes from. Pass `label` on an entry to override the registry wording.
//
// Counts render as a neutral Badge — informational, not urgent. Astryx's Badge
// guidance reserves the loud `error` variant for counts that demand action, so
// a page that wants that (an exceptions queue, say) passes `isUrgent` on the
// entry rather than every count screaming by default.
export default function StatusTabs({ value, onChange, tabs, size = 'md', hasDivider = true }) {
return (
<TabList value={value} onChange={onChange} size={size} hasDivider={hasDivider}>
{tabs.map((tab) => {
const meta = getStatusMeta(tab.status);
const Icon = meta.icon;
const hasCount = tab.count !== undefined && tab.count !== null;
return (
<Tab
key={tab.status}
value={tab.status}
label={tab.label ?? meta.label}
icon={<Icon size={15} />}
endContent={hasCount ? <Badge variant={tab.isUrgent ? 'error' : 'neutral'} label={tab.count} /> : undefined}
/>
);
})}
</TabList>
);
}
StatusTabs.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func,
tabs: PropTypes.arrayOf(
PropTypes.shape({
status: PropTypes.string.isRequired,
label: PropTypes.node,
count: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
isUrgent: PropTypes.bool
})
).isRequired,
size: PropTypes.oneOf(['sm', 'md', 'lg']),
hasDivider: PropTypes.bool
};

View File

@@ -611,6 +611,20 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
deliverytype: customer ? 'B' : 'C',
orderdate: b.createdat,
deliverydate: b.serviceoptions?.[0]?.estimateddeliveryat || b.updatedat,
// ⚠ NOT a real assignment time. The Doormile bookings feed has no
// assignment timestamp (the true one lives on `bookingassignments`,
// reachable only per-booking via GET /admin/bookings/:id/track), so this
// is the booking's last-modified column. It moves every time ANYTHING
// touches the row — status change, parcel scan, payment, pickup-complete.
//
// **Never bucket or group by this field.** Dispatch.js and deliveries.js
// used to bucket their Morning/Afternoon/Evening batches on it, which
// meant an order re-stamped during the evening silently jumped out of the
// batch it was actually assigned to and into whichever window contained
// the current clock time — the same orders appearing under Afternoon and
// then Evening on the same day. Both now bucket on
// `expecteddeliverytime`, which is stable. It remains fine to DISPLAY
// this as a "last updated" stamp, which is all the reports use it for.
assigntime: b.updatedat,
orderstatus: mapBookingStatusToDeliveryStatus(b.status),
droplat: b.deliverylatitude,

File diff suppressed because it is too large Load Diff

View File

@@ -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.
---

View File

@@ -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 {

View File

@@ -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))

View File

@@ -22,6 +22,7 @@ import TableLoader from 'components/nearle_components/TableLoader';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import StatusBadge from 'components/nearle_components/StatusBadge';
import { getHubs, createHub, updateHub, deleteHub } from 'pages/api/doormileApi';
import { DT, STATUS } from 'themes/dt/tokens';
import { TableScroll } from 'themes/dt/primitives';
@@ -224,7 +225,7 @@ const Hubs = () => {
<TableCell>{row.applocationid ?? '—'}</TableCell>
<TableCell>{row.address || '—'}</TableCell>
<TableCell>{row.pincode || '—'}</TableCell>
<TableCell>{row.status || '—'}</TableCell>
<TableCell>{row.status ? <StatusBadge status={row.status} /> : '—'}</TableCell>
<TableCell>
<HStack gap={1} justify="end">
<IconButton

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -704,6 +704,7 @@ const Orders = () => {
<Paper
elevation={0}
sx={{
mt: { xs: 1.5, md: 2 },
mb: { xs: 1, md: 1.25 },
px: { xs: 1.5, sm: 2 },
py: { xs: 1, sm: 1.25 },

View File

@@ -22,9 +22,7 @@ import { useTheme } from '@mui/material/styles';
import { MdCheckCircle, MdCancel, MdAccessTime, MdInventory2, MdTwoWheeler, MdArrowForward } from 'react-icons/md';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import dayjs from 'dayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { DateInput } from '@astryxdesign/core/DateInput';
import { OpenToast } from 'components/third-party/OpenToast';
const STATUS_META = {
@@ -221,27 +219,13 @@ export default function RiderSubstitution({
</ToggleButtonGroup>
</Stack>
<Box>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label="Select Date"
value={selectedDate}
onChange={(newValue) => newValue && setSelectedDate(newValue)}
slotProps={{
textField: {
size: 'small',
sx: {
width: 180,
'& .MuiOutlinedInput-root': {
borderRadius: '20px',
'& fieldset': { borderColor: DT.borderSubtle },
'&:hover fieldset': { borderColor: BRAND },
'&.Mui-focused fieldset': { borderColor: BRAND }
}
}
}
}}
/>
</LocalizationProvider>
<DateInput
label="Select Date"
isLabelHidden
size="sm"
value={selectedDate ? dayjs(selectedDate).format('YYYY-MM-DD') : undefined}
onChange={(v) => v && setSelectedDate(dayjs(v))}
/>
</Box>
</Stack>
</Paper>

View File

@@ -34,9 +34,7 @@ import { useTheme } from '@mui/material/styles';
var utc = require('dayjs/plugin/utc');
import dayjs from 'dayjs';
dayjs.extend(utc);
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { DateInput } from '@astryxdesign/core/DateInput';
import {
MdCheckCircle,
MdCancel,
@@ -420,8 +418,8 @@ const Riders = () => {
<LocationAutocomplete
locaName={locaName}
setAppId={setAppId}
setLocoName={setLocoName}
setLocoName={setLocoName}
placeholder="Select Zone"
/>
}
@@ -553,7 +551,7 @@ const Riders = () => {
<DebounceSearchBar
value={searchword}
onChange={setSearchword}
<Box sx={{ width: { xs: '100%', sm: 240, lg: 280 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
onDebouncedChange={setDebouncedSearch}
placeholder="Search riders (ctrl+k)"
/>
</Box>
@@ -607,27 +605,13 @@ const Riders = () => {
<ToggleButton value="scheduled">Scheduled</ToggleButton>
</ToggleButtonGroup>
</Stack>
>
<ToggleButton value="all">All</ToggleButton>
<ToggleButton value="scheduled">Scheduled</ToggleButton>
</ToggleButtonGroup>
</Stack>
<Stack direction="row" spacing={1.5} alignItems="center">
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label="Select Date"
value={historyDate}
onChange={(newValue) => newValue && setHistoryDate(newValue)}
slotProps={{
textField: {
size: 'small',
sx: {
width: 180,
'& .MuiOutlinedInput-root': {
borderRadius: '20px',
'& fieldset': { borderColor: DT.borderSubtle },
'&:hover fieldset': { borderColor: BRAND },
'&.Mui-focused fieldset': { borderColor: BRAND }
<Stack direction="row" spacing={1.5} alignItems="center">
<DateInput
label="Select Date"
isLabelHidden
size="sm"
value={historyDate ? dayjs(historyDate).format('YYYY-MM-DD') : undefined}
onChange={(v) => v && setHistoryDate(dayjs(v))}
/>
</Stack>
</Stack>

95
src/themes/dt/status.js Normal file
View File

@@ -0,0 +1,95 @@
// ============================================================================
// Canonical lifecycle-status registry.
//
// Five pages (deliveries, orders, ordersDetails, Tenants, and the dispatch
// panels) each carried their own private `STATUS_META` map. They agreed on the
// hexes but disagreed on everything else — orders.js keys off the raw
// `GET /admin/bookings` enum (`pending_pickup`, `miler_assigned`, …) while
// deliveries.js keys off api.js's generic `pending`/`accepted`/`delivered`
// mapping, so the same order rendered under two different labels depending on
// which page you were looking at.
//
// This module is the one place that knows what a status LOOKS like. It does
// not know what a status MEANS to a given page — the pending/accepted bucket
// rules stay in each page's query layer, because those genuinely differ (see
// the long comment above orders.js's STATUS_TABS).
//
// Each entry carries four renderings of the same state so a page never has to
// pick a colour by hand:
// color — hex, for the surfaces that take a raw accent (StatCard,
// AccentAvatar, chart series). Matches CLAUDE.md's status palette.
// badge — Astryx <Badge variant>. Non-semantic tinted variants on purpose:
// per Astryx's Badge guidance the solid semantic variants
// (success/warning/error) are for states that demand attention, and
// a table where every row shouts is a table where nothing does.
// `cancelled` is the one state that keeps a loud variant.
// dot — Astryx <StatusDot variant>, which only has five values, so
// several lifecycle states collapse onto `accent` here.
// icon — react-icons component (not an element) so callers size it.
// ============================================================================
import {
MdHourglassEmpty,
MdPersonPin,
MdLocationOn,
MdInventory2,
MdRoute,
MdSkipNext,
MdCheckCircle,
MdCancel,
MdList,
MdHelpOutline
} from 'react-icons/md';
export const STATUS_META = {
all: { label: 'All', color: '#000000', badge: 'neutral', dot: 'neutral', icon: MdList },
pending: { label: 'Pending', color: '#f59e0b', badge: 'yellow', dot: 'warning', icon: MdHourglassEmpty },
accepted: { label: 'Accepted', color: '#6366f1', badge: 'blue', dot: 'accent', icon: MdPersonPin },
arrived: { label: 'Arrived', color: '#06b6d4', badge: 'cyan', dot: 'accent', icon: MdLocationOn },
picked: { label: 'Picked', color: '#8b5cf6', badge: 'purple', dot: 'accent', icon: MdInventory2 },
active: { label: 'Active', color: '#14b8a6', badge: 'teal', dot: 'success', icon: MdRoute },
skipped: { label: 'Skipped', color: '#f97316', badge: 'orange', dot: 'warning', icon: MdSkipNext },
delivered: { label: 'Delivered', color: '#10b981', badge: 'green', dot: 'success', icon: MdCheckCircle },
cancelled: { label: 'Cancelled', color: '#ef4444', badge: 'error', dot: 'error', icon: MdCancel },
inactive: { label: 'Inactive', color: '#ef4444', badge: 'red', dot: 'error', icon: MdCancel }
};
// Raw backend enums that render as one of the states above. Kept separate from
// STATUS_META so the canonical list stays readable and so a page can still ask
// "is this a known alias?" rather than silently falling back.
//
// The booking enums come from `GET /admin/bookings` (confirmed live — see
// orders.js). `converted_to_consignment` is deliberately `accepted` and not
// `picked`: it fires when the rider marks pickup COMPLETE, but the operator
// workflow on the Orders page treats everything before hand-off as assigned.
export const STATUS_ALIASES = {
pending_pickup: 'pending',
pending_assignment: 'pending',
miler_assigned: 'accepted',
pickup_scheduled: 'accepted',
converted_to_consignment: 'accepted',
in_transit: 'active',
intransit: 'active',
completed: 'delivered',
cancel: 'cancelled',
canceled: 'cancelled'
};
// Fallback for a status the backend invents that nobody has mapped yet. Renders
// as a neutral badge with the raw string as its label, so an unknown state is
// visible and debuggable rather than blank.
const unknownStatus = (status) => ({
label: String(status || 'Unknown'),
color: '#94a3b8',
badge: 'neutral',
dot: 'neutral',
icon: MdHelpOutline
});
// Resolve any status string — canonical key, backend alias, or arbitrary
// casing — to its visual meta. Always returns an object; never throws.
export function getStatusMeta(status) {
if (!status) return unknownStatus(status);
const raw = String(status).trim();
const key = raw.toLowerCase();
return STATUS_META[key] || STATUS_META[STATUS_ALIASES[raw]] || STATUS_META[STATUS_ALIASES[key]] || unknownStatus(raw);
}

View File

@@ -30,6 +30,12 @@ export const DT = {
// Semantic status palette — lifecycle colours, deliberately distinct from the
// brand (see root CLAUDE.md §6). Pages have historically inlined these hexes;
// new code should reference STATUS so there is one place to change them.
//
// This map is the RAW HEX layer, for surfaces that take an arbitrary accent
// (StatCard, AccentAvatar, chart series). To RENDER a status — a row badge or
// a filter tab — use `themes/dt/status.js` instead: it resolves backend enums
// and aliases, and carries the matching Astryx Badge/StatusDot variants and
// icon alongside the hex. Reach for STATUS only when you need the bare colour.
// This also replaces the `theme.palette.error/success/...` lookups that used
// to pull the same colours out of the MUI theme.
// ---------------------------------------------------------------------------
@@ -57,6 +63,26 @@ export const soft = (c) => a(c, '18'); // soft chip / avatar bg
export const ring = (c) => a(c, '26'); // focus ring color
export const edge = (c) => a(c, '55'); // resting border
// ---------------------------------------------------------------------------
// ⛔ LEGACY — MUI-ONLY. Everything from here to the end of this file emits MUI
// `sx` objects and cannot be used by an Astryx page.
//
// They stay only because the pages still awaiting conversion import them
// (`pillFieldSx` ×3 files, `tableScrollSx` ×4, `tableHeadSx` ×3,
// `tableRowSx` ×3). They are deleted along with the MUI dependency once the
// last page converts — do NOT add a new call site.
//
// Astryx equivalents:
// pillFieldSx → nothing. Field chrome comes from the theme
// (`themes/astryx.js` → components['text-input']), so every
// input matches without a per-call-site helper.
// tableScrollSx → <TableScroll> from themes/dt/primitives (also supplies
// the sticky header MUI's TableContainer used to provide).
// tableHeadSx → <Table density dividers hasHover> — header casing and
// row rules are the component's own chrome.
// tableRowSx → same; `hasHover` covers the hover tint.
// ---------------------------------------------------------------------------
// Pill input sx — used by every filter Autocomplete/TextField on a page.
// Neutral, corporate filter field: white surface, hairline border, brand
// focus ring. Width is driven by parent flex/grid so this helper stays