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

149
CLAUDE.md
View File

@@ -15,8 +15,8 @@ For the per-page API map and architectural flow chart, see the project skill **`
## 2. Stack (pinned — do not upgrade without a deliberate audit) ## 2. Stack (pinned — do not upgrade without a deliberate audit)
- **React 18.2** + **react-app-rewired 2.2** (CRA, not Next.js — no `pages/` file-system routing; routes live in `src/routes/MainRoutes.js`). - **React 19.2** + **react-app-rewired 2.2** (CRA, not Next.js — no `pages/` file-system routing; routes live in `src/routes/MainRoutes.js`).
- **MUI 5.12** (`@mui/material`) is the primary UI library. Ant Design (`antd 5.11`) is used **only** for `<Empty />` placeholders in legacy pages — prefer MUI for new work. - **Astryx 0.1.9** (`@astryxdesign/core`) is the UI library — see §6. **MUI 5.12** (`@mui/material`) and Ant Design (`antd 5.11`) are **legacy**, present only on the pages not yet converted, and are being removed. Never write new UI against them.
- **Icons**: `@ant-design/icons` (page actions: `EditOutlined`, `EyeOutlined`, `CloseOutlined`, etc.) **and** `react-icons` (page identity: `Md*`, `Tb*`, `Fi*`, `Gi*`, `Lu*`). Both coexist; pick by what's already imported nearby. - **Icons**: `@ant-design/icons` (page actions: `EditOutlined`, `EyeOutlined`, `CloseOutlined`, etc.) **and** `react-icons` (page identity: `Md*`, `Tb*`, `Fi*`, `Gi*`, `Lu*`). Both coexist; pick by what's already imported nearby.
- **Data layer**: `@tanstack/react-query 5.17` (`useQuery`, `useInfiniteQuery`, `useMutation`). All server fetches go through this. Do **not** introduce `fetch`, `swr`, or `useEffect`-driven fetches for new code. - **Data layer**: `@tanstack/react-query 5.17` (`useQuery`, `useInfiniteQuery`, `useMutation`). All server fetches go through this. Do **not** introduce `fetch`, `swr`, or `useEffect`-driven fetches for new code.
- **HTTP**: `axios 1.3`. Most pages import `axios` directly (raw). `src/utils/axios.js` exists with a 401 → `/login` interceptor but is **not** widely adopted — match the surrounding file rather than introducing it. - **HTTP**: `axios 1.3`. Most pages import `axios` directly (raw). `src/utils/axios.js` exists with a 401 → `/login` interceptor but is **not** widely adopted — match the surrounding file rather than introducing it.
@@ -115,93 +115,102 @@ src/
--- ---
## 6. Design system (the `DT` token block) ## 6. Design system (Astryx + the `DT` tokens)
**The design system lives in two shared modules. Import from them — never redeclare.** The UI is **Astryx** (`@astryxdesign/core@0.1.9`). MUI is legacy and on its way out — see §6.6. Discover components with the CLI, don't guess:
```js ```bash
import { DT, a, tint, soft, ring, edge, pillFieldSx, tableScrollSx, tableHeadSx, tableRowSx } from 'themes/dt/tokens'; npx -y @astryxdesign/cli component <Name>
import { SoftPaper, AccentAvatar } from 'themes/dt/primitives';
``` ```
| Export | From | What it is | (The root README's `yarn dlx` form does not work here — `yarn` is not on PATH.)
|---|---|---|
| `DT` | `themes/dt/tokens` | Radii (`radiusPill/Card/Inner/Field`), the three shadows, text/border/surface colours, `DT.brand` (`#C01227`) |
| `a` `tint` `soft` `ring` `edge` | `themes/dt/tokens` | Alpha-suffix helpers — append hex transparency to any accent (`08` / `18` / `26` / `55`) |
| `pillFieldSx` | `themes/dt/tokens` | Filter `Autocomplete`/`TextField` styling. Takes no arguments — the field is neutral white with a brand focus ring on every page. Legacy call sites pass a colour; it's ignored. |
| `tableScrollSx` | `themes/dt/tokens` | Brand-red scrollbar for `<TableContainer>`. Spread it: `sx={{ maxHeight: ..., ...tableScrollSx }}` |
| `tableHeadSx` | `themes/dt/tokens` | Uppercase muted header row. Use as `<TableRow sx={tableHeadSx}>` |
| `tableRowSx` | `themes/dt/tokens` | Body row hairline divider + hover tint. Use as `<TableRow sx={tableRowSx}>` |
| `SoftPaper` | `themes/dt/primitives` | `PaperComponent` for every Autocomplete popup |
| `AccentAvatar` | `themes/dt/primitives` | Tinted icon avatar; fills solid when `selected` |
> **Do not paste a local `const DT = {…}` into a page.** Seventeen pages used to ### 6.1 Where the design system lives
> carry their own copy and they drifted — `orders.js` sat on `radiusCard: 16`
> and the old heavy shadows while every other page had moved to `14` and the
> restrained set, which is exactly why pages stopped matching. They now all
> import. If a token needs to change, change it in `themes/dt/tokens.js` so
> every page moves together.
### Page anatomy (every operator page should follow this order) | Module | What it owns |
|---|---|
| `themes/astryx.js` | **The theme.** `doormileTheme` — brand accent, card/input radii, component overrides. Mounted by `layout/MainLayout` and `login.js` via `<Theme theme={doormileTheme} mode="light">`. |
| `themes/dt/status.js` | **The status registry.** `STATUS_META`, `STATUS_ALIASES`, `getStatusMeta()` — every lifecycle state's label, hex, Astryx Badge variant, StatusDot variant, and icon. |
| `themes/dt/tokens.js` | `DT` (radii, shadows, surfaces, `DT.brand`), `STATUS` raw hexes, alpha helpers `a`/`tint`/`soft`/`ring`/`edge`. Its `*Sx` exports are **legacy MUI only**. |
| `themes/dt/primitives.js` | `TableScroll` (sticky-header + brand scrollbar wrapper), `AccentAvatar`, `softPopupStyle`. |
| `components/nearle_components/` | `PageHeader`, `StatCard`, `StatusBadge`, `StatusTabs`, `DebounceSearchBar`, `TableLoader`, … |
> **Use brand red `#C01227` (with light variant `#D25463`) for every brand surface below.** Don't use indigo `#6366f1` — that's reserved for the "Accepted" status badge only. **Never redeclare these locally.** Seventeen pages once carried private `DT` copies and drifted (`orders.js` sat on `radiusCard: 16` and the old heavy shadows while everything else had moved to `14`); five pages carried private `STATUS_META` copies that disagreed on labels for the same order. Change the shared module so every page moves together.
1. **Gradient header `<Paper>`** — `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)`, 48px filled `#C01227` avatar with a page icon, `Typography variant="h3"` title, "Live · {zone}" sub-line with an 8px green pulsing dot, and a pill `LocationAutocomplete` on the right (`pill accentColor="#C01227" paperComponent={SoftPaper}`). ### 6.2 Brand colour
2. **KPI tiles row** — `Grid` of 3–4 `Paper` cards, each with a 3px top stripe gradient, uppercase eyebrow label, large bold number, and a soft-tinted avatar holding an icon. The "primary" tile uses brand red `#C01227`; other tiles use semantic status colours.
3. **Filter bar `<Paper>`** (optional) — pill-style `Autocomplete`s using `pillFieldSx(color)` + `SoftPaper`, each with an `AccentAvatar` start-adornment.
4. **Pill status tabs + pill search** — tabs as clickable `<Box>` pills (inactive = `tint` bg + `edge` border, using each tab's **semantic status colour** — pending → amber, delivered → emerald, etc.). The **selected** tab always fills with **brand red `#C01227`** (border, bg, glow ring, hover), regardless of which status it represents — status colour is a resting-state accent only, not a selection colour. See `deliveries.js`'s `STATUS_TABS.map` (canonical) for the exact `active ? '#C01227' : ...` ternary shape; `orders.js`, `riders.js`, `Tenants.js`, and `reports/ordersDetails.js` all follow the same pattern. Search via `DebounceSearchBar` styled with brand red `#C01227` (tint bg, edge border, focus ring).
5. **Table `<Paper>`** with `<TableContainer>` + sticky `<TableHead>` — uppercase muted headers on `DT.surfaceAlt`, rows with `borderBottom: 1px solid ${DT.divider}` and `:hover` row tint. Scrollbar thumb uses brand red `edge('#C01227')`.
6. **Status badges in cells** — `Stack` with `AccentAvatar` + label inside a soft pill (tint bg, edge border). Status colours come from a per-page `STATUS_META` map keyed by lowercase status string — these are **semantic**, not brand.
7. **Edit / action icon buttons** — soft-pill `IconButton` using brand red `#C01227` (NOT `#8b5cf6` — that overlaps with the "Picked" status badge and confuses operators).
8. **Empty state** — centered 64px avatar (soft grey), bold "No X to show" line, and a muted helper sentence. Do not use antd `<Empty />` for new code.
### Universal brand colour **The brand is black (`#000000`)** — `DT.brand`, pinned into the theme as `--color-accent`. The palette is deliberately quiet: chrome is neutral, and colour carries *meaning only* (status). Do not introduce a second brand accent.
**`#C01227` — Doormile Express brand red.** This is the canonical primary colour for this app, defined in `src/themes/theme/default.js` as `primary.main`. It drives the sidebar, the logo, and is what every new surface (page headers, KPI primary tile, search bars, edit-action buttons, dialog/popup headers, scrollbars) must use as the brand accent. > Astryx's accent generator derives a ramp from the accent's hue and lightness. Black has neither, so feeding it `#000000` made the generator invent a hue and render every primary button magenta. That's why `themes/astryx.js` pins the accent tokens explicitly instead of using `color: { accent }`. Don't "simplify" it back.
Variants (also from `theme/default.js`): Anything genuinely brand-toned should read `var(--color-accent)` / `DT.brand`, never a literal `#000000`.
| Token | Hex | Use | ### 6.3 Status palette (semantic — the only place colour is allowed to shout)
|---|---|---|
| `primary.lighter` | `#F5DBDE` | Very subtle wash bg |
| `primary.light` / `primary.400` | `#D25463` | Gradient pair with main |
| `primary.main` | `#C01227` | Brand primary — default for all brand surfaces |
| `primary.dark` | `#910E1D` | Hover / pressed states |
| `primary.darker` | `#48070F` | Deep contrast text on light bg |
**Page header / dialog header gradient:** `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)` (subtle wash) or `linear-gradient(135deg, #C01227 0%, #D25463 100%)` (solid, for dialog titles). Never render a status by picking a colour by hand. Resolve it:
> **Off-brand accents are gone.** `deliveries.js`'s old indigo `#6366f1` brand ```jsx
> accent, `Tenants.js`'s purple `#662582`, and `multipleOrders.js`'s antd blue import StatusBadge from 'components/nearle_components/StatusBadge';
> `#1890ff` / purple `#65387a` have all been retired — brand surfaces are
> `#C01227` and metric accents come from the status palette below. Indigo
> `#6366f1` now appears only as the "Accepted" status colour.
### Status palette (semantic — distinct from brand) <StatusBadge status={row.orderstatus} /> // handles casing, backend enums, unknowns
```
These colour-code lifecycle states. Do **not** swap them for brand red on status **badges** (table-row status pills, `AccentAvatar` dropdown icons, KPI tiles) — operators rely on the colour to identify status at a glance in those read-only contexts. `getStatusMeta()` accepts a canonical key (`pending`), a raw backend enum (`miler_assigned`, `converted_to_consignment`), or any casing, and falls back to a neutral badge showing the raw string so an unmapped state is visible rather than blank.
**Exception — selected status tabs:** the one deliberate place brand red *does* replace the status colour is a **selected** pill tab (§4 above) — clicking "Pending"/"Active"/etc. fills that tab red, not amber/green, so the clicked state always reads as "this is the active filter" in the one consistent brand colour. The status colour still shows on every *unselected* tab (as a tint) and on the row badges underneath, so at-a-glance status identification in the table itself is unaffected. | Meaning | Hex | Astryx `Badge` | `StatusDot` |
|---|---|---|---|
| Pending / waiting | `#f59e0b` | `yellow` | `warning` |
| Accepted / assigned | `#6366f1` | `blue` | `accent` |
| Arrived | `#06b6d4` | `cyan` | `accent` |
| Picked up | `#8b5cf6` | `purple` | `accent` |
| Active / in-transit | `#14b8a6` | `teal` | `success` |
| Delivered / success | `#10b981` | `green` | `success` |
| Skipped | `#f97316` | `orange` | `warning` |
| Cancelled / error | `#ef4444` | `error` | `error` |
| Neutral / unknown | `#94a3b8` | `neutral` | `neutral` |
| Meaning | Colour | Badges use the **tinted** non-semantic variants on purpose. Astryx reserves the solid `success`/`warning`/`error` variants for states that demand attention, and a table where every row shouts is a table where nothing does — `cancelled` is the one lifecycle state that keeps a loud variant.
|--------------------------|-----------|
| Pending / waiting | `#f59e0b` (amber) |
| Accepted / assigned | `#6366f1` (indigo — semantically distinct from brand red) |
| Arrived | `#06b6d4` (cyan) |
| Picked up | `#8b5cf6` (light purple — distinct from brand) |
| Active / in-transit | `#14b8a6` (teal) |
| Delivered / success | `#10b981` (emerald) |
| Cancelled / error | `#ef4444` (red) |
| Skipped | `#f97316` (orange) |
| Neutral / muted | `#94a3b8` (slate) |
| Sky accent (tenant, info)| `#0ea5e9` |
### Don'ts for the design system The hexes in `STATUS` (tokens.js) are the raw layer, for surfaces taking an arbitrary accent (`StatCard color=`, `AccentAvatar`, chart series). Use `status.js` to *render*; use `STATUS` only when you need a bare colour.
- Don't use raw MUI `<Tabs>` for status/filter switching — they were replaced by the pill `<Box>` pattern on every redesigned page. Pages that still use `<Tabs>` (e.g. for inline collapse views) are tolerated but new top-level navigation should use pills. ### 6.4 Page anatomy
- Don't reach for `purple lighter` / `e1bee7` or other Mantis theme accents on new pages — those exist in legacy code only.
- Don't apply box-shadow to row hover; only tint the background (`DT.surfaceAlt`). Follow this order. **`pages/nearle/hubs/hubs.js` is the reference implementation** — copy its structure.
- Don't change avatar sizes mid-page — pick from `{18, 20, 22, 24, 28, 32, 36, 40, 44, 48, 56, 64}` and stay consistent.
1. **`<PageHeader>`** — title, `live` subtitle with pulsing `StatusDot`, and an `action` slot (primary `Button`, zone picker). Plain and un-boxed; the old gradient header Paper is gone.
2. **KPI row** — `<Grid columns={{ minWidth: 220 }} gap={3}>` of `<StatCard>`. `color` is a status hex, or `DT.brand` for the primary tile. No top-stripes, no rainbow.
3. **`<StatusTabs>`** — the status filter strip. Supply the tab *order* and where each count comes from; labels and icons resolve from the registry.
4. **Filter + search bar** — a `<Card padding={3}>` holding an `HStack justify="between"` with the row count on the left and `<DebounceSearchBar>` on the right.
5. **Table** — `<Card padding={0}>` → `<TableScroll minWidth={…}>` → `<Table density="balanced" dividers="rows" hasHover>`. `TableScroll` supplies the sticky header and scrollbar that MUI's `TableContainer` used to.
6. **Status cells** — `<StatusBadge>`. Never a hand-rolled tinted `Box`.
7. **Row actions** — `<IconButton size="sm" variant="ghost">`, `variant="destructive"` for delete. Always pass `label` **and** `tooltip`.
8. **Empty state** — `<EmptyState title description icon>` inside a full-width `TableCell colSpan={n}`. Never antd `<Empty/>`.
9. **Loading** — `<TableLoader>` for rows, `<Loader>` for a blocking full-page backdrop. These are not interchangeable.
### 6.5 Astryx rules and known gotchas
- **No `<div>` for layout.** `HStack` / `VStack` / `Grid` / `Center` / `Stack` do spacing and alignment. No raw `<span>` wrappers either.
- **No raw values.** Colours and spacing come from tokens (`var(--color-*)`, `var(--spacing-*)`, `var(--radius-*)`). An inline `style` is the escape hatch *only* for a caller-supplied status hex, which the token system genuinely can't express (see `StatCard`).
- **No `sx` prop** — it doesn't exist. Restyle through `themes/astryx.js` so every call site moves at once.
- **`Grid` takes `columns={{ minWidth: N }}`**, not `{ base, sm }` breakpoints.
- **`Table` children-mode cells have no `align` prop** — use `style`.
- **There is no `Tabs` and no `Stepper`** in 0.1.9. Use `TabList`/`Tab` (with `endContent` for counts, `TabMenu` for overflow) and `SegmentedControl`/`ProgressBar` for wizard steps.
- **`Spinner` maxes out at 18px** — large loaders are CSS rings.
- **`Selector` doesn't forward `ref`**; **`Typeahead` has no freeSolo**, which is why `AddressAutocomplete` is a `TextInput` plus a hand-rolled list.
- **Astryx has 153 components** — search before hand-rolling: `npx -y @astryxdesign/cli search "<thing>"`.
### 6.6 Migration status
Converted: the app shell, `login.js`, the maintenance pages, the entire shared-component layer, and the pages `hubs`, `vehicles`, `reports/{profitability, RidersRoutes, mapWithRoute}`.
Still MUI: 25 operator pages and 9 `src/themes/*` files. Four pages still import antd `<Empty/>`.
Rules while both stacks coexist:
- A page is converted **whole**, never half. Mixed-stack pages are how the palette drifts.
- `ThemeCustomization` must stay mounted in `App.js` until the final page converts; the MUI theme files and the `@mui/*` / `@emotion/*` / `antd` dependencies are removed only after that.
- `OrdersTableSkeleton` renders rows *into* a page's table, so it can only convert alongside its last consumer. Converted pages use `TableLoader` instead.
- Don't add a call site for anything marked ⛔ LEGACY in `tokens.js`.
--- ---
@@ -275,7 +284,7 @@ These colour-code lifecycle states. Do **not** swap them for brand red on status
## 13. Cross-references ## 13. Cross-references
- **For the per-page API map and the architectural flow chart** (which endpoint each page calls, what the optimisation pipeline does, FCM flow), invoke the project skill **`nearlexpress-docs`** (`.claude/skills/nearlexpress-docs/SKILL.md`) rather than restating the content here. - **For the per-page API map and the architectural flow chart** (which endpoint each page calls, what the optimisation pipeline does, FCM flow), invoke the project skill **`nearlexpress-docs`** (`.claude/skills/nearlexpress-docs/SKILL.md`) rather than restating the content here.
- **For shared design patterns** between pages, the source of truth is `src/themes/dt/tokens.js` (values, `pillFieldSx`, the three `table*Sx` helpers) and `src/themes/dt/primitives.js` (`SoftPaper`, `AccentAvatar`). Import from them, don't redesign and don't re-declare locally. `src/pages/nearle/deliveries/deliveries.js` remains the reference for how those tokens are *composed* into a page (filter bar → tabs → table), but it is no longer where the values live. - **For shared design patterns** between pages, the sources of truth are `src/themes/astryx.js` (the theme), `src/themes/dt/status.js` (lifecycle rendering), `src/themes/dt/tokens.js` (radii, shadows, raw hexes), and `src/themes/dt/primitives.js` (`TableScroll`, `AccentAvatar`). Import from them; don't redesign and don't re-declare locally. `src/pages/nearle/hubs/hubs.js` is the reference for how they *compose* into a page (header → KPIs → tabs → filter → table).
<!-- ASTRYX:START --> <!-- ASTRYX:START -->
Astryx v0.1.9 · 153 components Astryx v0.1.9 · 153 components

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` | | `PageHeader.js` | Page title + live subtitle + action slot | ✅ **Astryx** — `Heading`/`Text`/`HStack`/`StatusDot` |
| `StatCard.js` | KPI / metric tile | ✅ **Astryx** — `Card`/`Text`/`Center`/`Skeleton` | | `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) | | `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 | | `TableLoader.js` | Inline table loading rows | ✅ **Astryx** — use this, not `OrdersTableSkeleton` |
| `LoaderWithImage.js` | Inline branded spinner for "loading more" rows | MUI | | `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 | | `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` | 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`.
| `TitleCard.js` | Old page header | ⛔ **Legacy** — do not use on new pages. The replacement is `PageHeader.js`. |
--- ---
## 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`. - **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`.
- **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. - **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.
- 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. - **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-*)`.
- Border radii: `12` (inner), `16` (card), `999` (pill). No other values. - **No `<div>`/`<span>` for layout.** `HStack`, `VStack`, `Grid`, `Center`, `Stack`.
- Shadows: `DT.shadowSoft` / `DT.shadowMd` / `DT.shadowPop`. No raw `box-shadow` strings. - **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.)
> 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.
--- ---
## 3. `LocationAutocomplete` — the `pill` prop is opt-in ## 3. `LocationAutocomplete` — the zone picker
The component supports two visual modes, controlled by the `pill` prop:
```jsx ```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 <LocationAutocomplete
pill locaName={locaName}
accentColor="#6366f1" setAppId={setAppId}
icon={<MdMyLocation size={14} />} setLocoName={setLocoName}
setPage={setPage} // optional — resets pagination on zone change
placeholder="Select Zone" placeholder="Select Zone"
paperComponent={SoftPaper}
setAppId={...}
setLocoName={...}
/> />
``` ```
- **For any new page:** use `pill`. Always. - 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.
- **For existing legacy pages (orders, invoice, riders, etc.):** keep the default until that page is redesigned. Don't change them piecemeal. - 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).
- The `accentColor` defaults to `#6366f1` — only override when the page's accent is different (rare).
--- ---
@@ -93,10 +87,10 @@ The component supports two visual modes, controlled by the `pill` prop:
Only when: Only when:
1. Used by **two or more pages** in production code. 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. 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 > to `14` and the restrained set. That drift is what made pages stop looking
> alike. > alike.
`DT`, the alpha helpers (`a`/`tint`/`soft`/`ring`/`edge`), `pillFieldSx`, and The same failure repeated with status: five pages each grew a private
the table chrome (`tableScrollSx`/`tableHeadSx`/`tableRowSx`) live in `STATUS_META`, and they disagreed on what to call the same order — that is why
`src/themes/dt/tokens.js`. `SoftPaper` and `AccentAvatar` live in `themes/dt/status.js` now exists.
`src/themes/dt/primitives.js`. **Import them. Never re-declare them in a page.**
`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 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 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', deliverytype: customer ? 'B' : 'C',
orderdate: b.createdat, orderdate: b.createdat,
deliverydate: b.serviceoptions?.[0]?.estimateddeliveryat || b.updatedat, 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, assigntime: b.updatedat,
orderstatus: mapBookingStatusToDeliveryStatus(b.status), orderstatus: mapBookingStatusToDeliveryStatus(b.status),
droplat: b.deliverylatitude, 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. 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 ```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: 'morning', startHour: 0, endHour: 8 }, // 12 AM – 8 AM
{ id: 'afternoon', startHour: 9, endHour: 12.5 }, // 9 AM – 12:30 PM { 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. **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`) ### 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'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 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 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. - 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 { .dispatch-container .logo-badge {
width: 32px; width: 44px;
height: 32px; height: 44px;
border-radius: 8px;
background: linear-gradient(135deg, #3A3A3A, #2563eb);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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 { .dispatch-container .logo-name {
@@ -1902,21 +1903,6 @@
margin-bottom: 12px; 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 { .dispatch-container .rcard-info {
flex: 1; flex: 1;
} }
@@ -4945,6 +4931,12 @@
/* Markers - styled as clean flags natively in Dispatch.js */ /* 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 { .dispatch-container .kitchen-mark {
background: var(--kitchen); background: var(--kitchen);
border: 3px solid #fff; border: 3px solid #fff;
@@ -6814,10 +6806,8 @@
} }
.dispatch-container .logo-badge { .dispatch-container .logo-badge {
width: 28px; width: 38px;
height: 28px; height: 38px;
font-size: 13px;
border-radius: 6px;
} }
.dispatch-container .logo { .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 // 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). // they share the same road geometry (otherwise they'd stack and read as one).
import '../../../utils/leafletPolylineOffset'; import '../../../utils/leafletPolylineOffset';
import doormileMark from 'assets/images/doormile-mark.png';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useInfiniteQuery, useQueries, useQuery, useMutation } from '@tanstack/react-query'; import { useInfiniteQuery, useQueries, useQuery, useMutation } from '@tanstack/react-query';
import { 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. // Named delivery batches — operator's mental model of the day's waves.
// Each entry covers a half-open range [startHour, endHour) measured in // Each entry covers a half-open range [startHour, endHour) measured in
// FRACTIONAL hours (e.g. 12.5 = 12:30). Half-hour boundaries are supported. // 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) // • Morning Batch: before 8 AM (00:00 → 08:00)
// • Afternoon Batch: 9 AM → 12:30 PM (09:00 → 12:30) // • Afternoon Batch: 9 AM → 12:30 PM (09:00 → 12:30)
// • Evening Batch: 4 PM → 7 PM (16:00 → 19:00) // • 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. // with a fallback to expecteddeliverytime so undelivered orders still bucket.
const TIME_FIELDS = [ const TIME_FIELDS = [
{ id: 'delivered', label: 'Delivered', keys: ['deliverytime'] }, { 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: 'assigned', label: 'Assigned', keys: ['assigntime'] },
{ id: 'accepted', label: 'Accepted', keys: ['acceptedtime'] }, { id: 'accepted', label: 'Accepted', keys: ['acceptedtime'] },
{ id: 'started', label: 'Started', keys: ['starttime'] }, { id: 'started', label: 'Started', keys: ['starttime'] },
@@ -294,20 +300,6 @@ function MapAutoResize({ trigger }) {
return null; 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 // haversineKm/polylineLengthKm/kalmanSmoothGps moved to dispatchShared.js so
// deliveries.js's Update Status dialog can compute the same real, GPS-based // deliveries.js's Update Status dialog can compute the same real, GPS-based
// Actual KMs figure instead of leaving that field permanently blank. // Actual KMs figure instead of leaving that field permanently blank.
@@ -930,9 +922,6 @@ const Dispatch = ({
const [centerPopupOrder, setCenterPopupOrder] = useState(null); const [centerPopupOrder, setCenterPopupOrder] = useState(null);
const isControlled = selectedRiderId !== undefined; const isControlled = selectedRiderId !== undefined;
const [clock, setClock] = useState(''); 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 // 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 // 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 [locationMenuOpen, setLocationMenuOpen] = useState(false);
const locationMenuRef = useRef(null); const locationMenuRef = useRef(null);
// Which timestamp column drives slot bucketing. Default = assigntime so // Which timestamp column drives slot bucketing. Default = 'due'
// orders bucket into Morning/Afternoon/Evening by when they were assigned, // (expecteddeliverytime) so orders bucket into Morning/Afternoon/Evening by
// per current spec. The status-wise time-field dropdown is hidden for now // the delivery slot they were promised for. The status-wise time-field
// (see commented-out block in JSX), so this stays fixed at 'assigned'. // dropdown is hidden for now (see commented-out block in JSX), so this stays
const [selectedTimeField, setSelectedTimeField] = useState('assigned'); // 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 [timeFieldMenuOpen, setTimeFieldMenuOpen] = useState(false);
const timeFieldMenuRef = useRef(null); 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, // Kitchen badge is a FIXED 31px at every zoom level — deliberately not
// so scale the badge down as the operator zooms out past street level // zoom-responsive.
// (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 // It used to scale with zoom (46px at street level, shrinking as the operator
// badge beyond its original design size, and floored at 0.55 so it never // zoomed out) which made the marker read as a big amber blob over a
// shrinks past legibility. // city-wide view. Rather than tune the curve, the size is now constant: a
const kitchenIconZoomScale = Math.max(0.55, Math.min(1, 1 + (mapZoom - 12) * 0.12)); // 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 createKitchenIcon = (name, focused = false) => {
const base = focused ? 56 : 46; const size = focused ? KITCHEN_ICON_FOCUSED_SIZE : KITCHEN_ICON_SIZE;
const size = Math.round(base * kitchenIconZoomScale);
const anchor = Math.round(size / 2); const anchor = Math.round(size / 2);
const border = focused ? 3 : 2;
const glow = 14;
return L.divIcon({ return L.divIcon({
className: '', className: '',
iconSize: [size, size], iconSize: [size, size],
iconAnchor: [anchor, anchor], iconAnchor: [anchor, anchor],
popupAnchor: [0, -(anchor + 2)], 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 && ( {!embedded && (
<div id="hdr"> <div id="hdr">
<div className="logo"> <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> <div className="logo-name">Dispatch</div>
{appLocations && appLocations.length > 0 && ( {appLocations && appLocations.length > 0 && (
<div className="logo-city-wrap" ref={locationMenuRef}> <div className="logo-city-wrap" ref={locationMenuRef}>
@@ -3637,8 +3651,10 @@ const Dispatch = ({
<div id="batch-row"> <div id="batch-row">
<span className="batch-label">Batch</span> <span className="batch-label">Batch</span>
{/* Status-wise (time-field) filter is hidden for now per spec — {/* Status-wise (time-field) filter is hidden for now per spec —
bucketing is locked to `assigntime`. Restore this block to bring bucketing is locked to `due` (expecteddeliverytime). Restore this
back the Delivered/Pending/Assigned/... dropdown. 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}> <div className="time-field-wrap" ref={timeFieldMenuRef}>
<button <button
type="button" type="button"
@@ -4902,7 +4918,6 @@ const Dispatch = ({
/> />
)} )}
<MapAutoResize trigger={`${sidebarCollapsed}|${compareOpen}|${compareDataCollapsed}`} /> <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} /> <MapController focusedItem={compareFocusItem || ((focusedRider || focusedKitchen) && focusedStop) || focusedRider || focusedKitchen || focusedZone} viewMode={viewMode} orders={allViewOrders} kitchens={kitchens} locationKey={selectedAppLocationId} extraPoints={allViewLivePoints} />
{kitchens {kitchens
.filter(k => Number.isFinite(k.lat) && Number.isFinite(k.lon)) .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 DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import PageHeader from 'components/nearle_components/PageHeader'; import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard'; import StatCard from 'components/nearle_components/StatCard';
import StatusBadge from 'components/nearle_components/StatusBadge';
import { getHubs, createHub, updateHub, deleteHub } from 'pages/api/doormileApi'; import { getHubs, createHub, updateHub, deleteHub } from 'pages/api/doormileApi';
import { DT, STATUS } from 'themes/dt/tokens'; import { DT, STATUS } from 'themes/dt/tokens';
import { TableScroll } from 'themes/dt/primitives'; import { TableScroll } from 'themes/dt/primitives';
@@ -224,7 +225,7 @@ const Hubs = () => {
<TableCell>{row.applocationid ?? '—'}</TableCell> <TableCell>{row.applocationid ?? '—'}</TableCell>
<TableCell>{row.address || '—'}</TableCell> <TableCell>{row.address || '—'}</TableCell>
<TableCell>{row.pincode || '—'}</TableCell> <TableCell>{row.pincode || '—'}</TableCell>
<TableCell>{row.status || '—'}</TableCell> <TableCell>{row.status ? <StatusBadge status={row.status} /> : '—'}</TableCell>
<TableCell> <TableCell>
<HStack gap={1} justify="end"> <HStack gap={1} justify="end">
<IconButton <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 <Paper
elevation={0} elevation={0}
sx={{ sx={{
mt: { xs: 1.5, md: 2 },
mb: { xs: 1, md: 1.25 }, mb: { xs: 1, md: 1.25 },
px: { xs: 1.5, sm: 2 }, px: { xs: 1.5, sm: 2 },
py: { xs: 1, sm: 1.25 }, 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 { MdCheckCircle, MdCancel, MdAccessTime, MdInventory2, MdTwoWheeler, MdArrowForward } from 'react-icons/md';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { DateInput } from '@astryxdesign/core/DateInput';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { OpenToast } from 'components/third-party/OpenToast'; import { OpenToast } from 'components/third-party/OpenToast';
const STATUS_META = { const STATUS_META = {
@@ -221,27 +219,13 @@ export default function RiderSubstitution({
</ToggleButtonGroup> </ToggleButtonGroup>
</Stack> </Stack>
<Box> <Box>
<LocalizationProvider dateAdapter={AdapterDayjs}> <DateInput
<DatePicker label="Select Date"
label="Select Date" isLabelHidden
value={selectedDate} size="sm"
onChange={(newValue) => newValue && setSelectedDate(newValue)} value={selectedDate ? dayjs(selectedDate).format('YYYY-MM-DD') : undefined}
slotProps={{ onChange={(v) => v && setSelectedDate(dayjs(v))}
textField: { />
size: 'small',
sx: {
width: 180,
'& .MuiOutlinedInput-root': {
borderRadius: '20px',
'& fieldset': { borderColor: DT.borderSubtle },
'&:hover fieldset': { borderColor: BRAND },
'&.Mui-focused fieldset': { borderColor: BRAND }
}
}
}
}}
/>
</LocalizationProvider>
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -34,9 +34,7 @@ import { useTheme } from '@mui/material/styles';
var utc = require('dayjs/plugin/utc'); var utc = require('dayjs/plugin/utc');
import dayjs from 'dayjs'; import dayjs from 'dayjs';
dayjs.extend(utc); dayjs.extend(utc);
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { DateInput } from '@astryxdesign/core/DateInput';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { import {
MdCheckCircle, MdCheckCircle,
MdCancel, MdCancel,
@@ -420,8 +418,8 @@ const Riders = () => {
<LocationAutocomplete <LocationAutocomplete
locaName={locaName} locaName={locaName}
setAppId={setAppId} setAppId={setAppId}
setLocoName={setLocoName} setLocoName={setLocoName}
placeholder="Select Zone" placeholder="Select Zone"
/> />
} }
@@ -553,7 +551,7 @@ const Riders = () => {
<DebounceSearchBar <DebounceSearchBar
value={searchword} value={searchword}
onChange={setSearchword} 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)" placeholder="Search riders (ctrl+k)"
/> />
</Box> </Box>
@@ -607,27 +605,13 @@ const Riders = () => {
<ToggleButton value="scheduled">Scheduled</ToggleButton> <ToggleButton value="scheduled">Scheduled</ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
</Stack> </Stack>
> <Stack direction="row" spacing={1.5} alignItems="center">
<ToggleButton value="all">All</ToggleButton> <DateInput
<ToggleButton value="scheduled">Scheduled</ToggleButton> label="Select Date"
</ToggleButtonGroup> isLabelHidden
</Stack> size="sm"
<Stack direction="row" spacing={1.5} alignItems="center"> value={historyDate ? dayjs(historyDate).format('YYYY-MM-DD') : undefined}
<LocalizationProvider dateAdapter={AdapterDayjs}> onChange={(v) => v && setHistoryDate(dayjs(v))}
<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> </Stack>
</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 // Semantic status palette — lifecycle colours, deliberately distinct from the
// brand (see root CLAUDE.md §6). Pages have historically inlined these hexes; // 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. // 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 // This also replaces the `theme.palette.error/success/...` lookups that used
// to pull the same colours out of the MUI theme. // 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 ring = (c) => a(c, '26'); // focus ring color
export const edge = (c) => a(c, '55'); // resting border 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. // Pill input sx — used by every filter Autocomplete/TextField on a page.
// Neutral, corporate filter field: white surface, hairline border, brand // Neutral, corporate filter field: white surface, hairline border, brand
// focus ring. Width is driven by parent flex/grid so this helper stays // focus ring. Width is driven by parent flex/grid so this helper stays