26 KiB
CLAUDE.md — Doormile Express Console (xpressconsole)
Project-level rules and conventions for Claude Code when working in this repo. Read this in full before editing. When in doubt about a pattern, copy from
src/pages/nearle/deliveries/deliveries.js— it is the canonical reference for both the design system and the data layer.
1. What this is
A React 18 operator console for the Doormile Express dispatch platform. Operators use it to manage orders, run the AI dispatch optimiser, watch a live map of riders, edit tenants/pricing/invoices, and pull BI reports. Production users are warehouse staff, not end customers.
For the per-page API map and architectural flow chart, see the project skill nearlexpress-docs (.claude/skills/nearlexpress-docs/SKILL.md). Do not duplicate that content here.
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 insrc/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. - Icons:
@ant-design/icons(page actions:EditOutlined,EyeOutlined,CloseOutlined, etc.) andreact-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 introducefetch,swr, oruseEffect-driven fetches for new code. - HTTP:
axios 1.3. Most pages importaxiosdirectly (raw).src/utils/axios.jsexists with a 401 →/logininterceptor but is not widely adopted — match the surrounding file rather than introducing it. - State:
@reduxjs/toolkit 1.9for cross-page state (FCM token, login user, menu, snackbar, toast). Page-local UI state stays inuseState. - Routing:
react-router-dom 6.10, lazy-loaded viacomponents/Loadable. - Forms:
formik 2.2+yup 1.1where present; new simple forms can use plainuseState. - Dates:
dayjs 1.11withutcplugin (already extended at the top ofdeliveries.js). Usedayjs(...).utc()for backend timestamps and baredayjs(...)for local-time bucketing — see the batch-bucketing comment indeliveries.jsfor the rationale. - Maps:
leaflet+react-leafletonly. Geocoding/address search via free OSM Nominatim (src/components/nearle_components/AddressAutocomplete.js) and routing via OSRM — no API key required. Google Maps (@react-google-maps/api,react-geocode,react-google-autocomplete) has been fully removed; do not reintroduce it. - Notifications:
firebase 10.14(FCM) — seesrc/firebase_notification/. Toasts vianotistack 3. - Drag-and-drop:
react-dnd(used on the Dispatch Preview page).
3. Dev workflow
# Install
npm install # or `yarn`
# Run locally (uses .env)
npm start
# Run with a specific env file
npm run start:dev # env.development
npm run start:staging # env.staging
# Build
npm run build
npm run build:dev
npm run build:staging
# Lint (the only checked gate — there are no tests of consequence)
npm run lint
- Env files at repo root:
env.stagingis committed;.env.development/.env.productionare typically gitignored. Pull from a teammate when missing. - Required env vars:
REACT_APP_DOORMILE_URL— the only API base (https://api.doormile.com/api/v1), used by every/admin/*call viautils/doormileAxios.js. The old jupiter-backedREACT_APP_URL/REACT_APP_URL2/REACT_APP_URL3have been fully retired — do not reintroduce them. No Maps API key is needed — maps and address search run on free Leaflet/OSM services. The optimiser URLs (routes.workolik.com,routemate.workolik.com) and the final-delivery-commit URL (jupiter.nearle.app) remain hardcoded — they're a separate solver service with no equivalent in the new API, deliberately left untouched by the backend migration. See thenearlexpress-docsskill andexpress-console-api.md. - Dev server runs on
http://localhost:3000. The user usually has it running already — assume it is up when reporting "reload to see it".
4. Hard constraints (do NOT)
- Do not introduce Next.js / SSR patterns. No
getServerSideProps, noapp/directory, nonext/*imports. This is CRA. - Do not rewrite shared design tokens. Every new page that needs the polished UI must reuse the
DTtoken block (see §6). Do not invent a parallel palette. - Do not change
package.jsondependency versions unless explicitly asked. The build is sensitive to webpack/svgr/react-scripts versions (seeresolutionsinpackage.json). - Do not bypass the dispatch reconcile step. After any manual edit on
/doormile/dispatch/preview(rider swap, step reorder), the page must callPOST /optimization/reconcile-stepsbeforePOST /deliveries/createdeliveries. Skipping this corrupts route sequences. - Do not commit
.env*files beyondenv.staging(which is the agreed-shared staging baseline). - Do not introduce TypeScript files (
.ts/.tsx) into this repo. It is JavaScript; mixing creates lint and tooling friction. - Do not use absolute
http://localhostURLs in code. Always read fromprocess.env.REACT_APP_DOORMILE_URL(or usedoormileAxios, which already defaults to it). - Do not log
userid,authname, FCM tokens, or PII toconsole.login production paths. The codebase has many leftoverconsole.logcalls — when editing nearby, remove them rather than add more. - Do not add or remove items from the sidebar without updating
src/menu-items/nearle.js— the menu drives both display and i18n keys. - Do not use destructive git (
reset --hard,push --force, branch deletion) without explicit user instruction.
5. Architecture map
src/
├── App.js # Auth gate (localStorage('authname') → /login), mounts FCM listener, ThemeCustomization, Locales, Notistack, Snackbar
├── index.js # Provider wiring: Redux store, TanStack QueryClient, Router
├── config.js # Theme constants (DRAWER_WIDTH=260, fontFamily, mode, presetColor, ThemeMode, MenuOrientation, ThemeDirection)
├── routes/
│ ├── index.js # Combines MainRoutes + LoginRoutes
│ ├── MainRoutes.js # All /doormile/* routes, lazy-loaded via Loadable(lazy(...))
│ └── LoginRoutes.js
├── layout/
│ ├── MainLayout/ # Sidebar + header frame (used for all logged-in pages)
│ └── CommonLayout/ # Bare frame (login, maintenance pages)
├── menu-items/
│ └── nearle.js # Sidebar definition — id, title (FormattedMessage), url, icon. Must add new pages here.
├── store/
│ ├── index.js # configureStore + useDispatch/useSelector exports
│ └── reducers/ # fcmSlice, loginUserSlice, menu, snackbar, toastSlice, auth, actions
├── themes/ # MUI theme, palette, typography, shadows, overrides
├── pages/
│ ├── api/api.js # CENTRAL API layer — every query/mutation function lives here
│ └── nearle/ # All operator pages, grouped by feature folder
└── components/
├── Loadable.js # React.Suspense wrapper for lazy routes
├── Loader.js # Full-page backdrop spinner
├── nearle_components/
│ ├── DebounceSearchBar.js # 500ms-debounced search input with ⌘/Ctrl+K focus shortcut
│ ├── LocationAutocomplete.js # Zone picker (has `pill` variant — use it for new pages)
│ ├── LoaderWithImage.js
│ ├── GlobalToast.js
│ ├── SearchBar.js
│ ├── TableLoader.js
│ └── TitleCard.js # LEGACY — do not use for new pages (replaced by the gradient Paper header in §6)
└── third-party/
└── OpenToast.js # Wrapper around notistack — use this for toast emissions
- Absolute imports work from
src/thanks tojsconfig.json("baseUrl": "src"). Preferimport x from 'pages/api/api'over deep relative paths. Look at the surrounding file's import style and match it.
6. Design system (the DT token block)
The design system lives in two shared modules. Import from them — never redeclare.
import { DT, a, tint, soft, ring, edge, pillFieldSx, tableScrollSx, tableHeadSx, tableRowSx } from 'themes/dt/tokens';
import { SoftPaper, AccentAvatar } from 'themes/dt/primitives';
| Export | From | What it is |
|---|---|---|
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 carry their own copy and they drifted —orders.jssat onradiusCard: 16and the old heavy shadows while every other page had moved to14and the restrained set, which is exactly why pages stopped matching. They now all import. If a token needs to change, change it inthemes/dt/tokens.jsso every page moves together.
Page anatomy (every operator page should follow this order)
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.
- Gradient header
<Paper>—linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%), 48px filled#C01227avatar with a page icon,Typography variant="h3"title, "Live · {zone}" sub-line with an 8px green pulsing dot, and a pillLocationAutocompleteon the right (pill accentColor="#C01227" paperComponent={SoftPaper}). - KPI tiles row —
Gridof 3–4Papercards, 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. - Filter bar
<Paper>(optional) — pill-styleAutocompletes usingpillFieldSx(color)+SoftPaper, each with anAccentAvatarstart-adornment. - Pill status tabs + pill search — tabs as clickable
<Box>pills (inactive =tintbg +edgeborder, 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. Seedeliveries.js'sSTATUS_TABS.map(canonical) for the exactactive ? '#C01227' : ...ternary shape;orders.js,riders.js,Tenants.js, andreports/ordersDetails.jsall follow the same pattern. Search viaDebounceSearchBarstyled with brand red#C01227(tint bg, edge border, focus ring). - Table
<Paper>with<TableContainer>+ sticky<TableHead>— uppercase muted headers onDT.surfaceAlt, rows withborderBottom: 1px solid ${DT.divider}and:hoverrow tint. Scrollbar thumb uses brand rededge('#C01227'). - Status badges in cells —
StackwithAccentAvatar+ label inside a soft pill (tint bg, edge border). Status colours come from a per-pageSTATUS_METAmap keyed by lowercase status string — these are semantic, not brand. - Edit / action icon buttons — soft-pill
IconButtonusing brand red#C01227(NOT#8b5cf6— that overlaps with the "Picked" status badge and confuses operators). - 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
#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.
Variants (also from theme/default.js):
| Token | Hex | Use |
|---|---|---|
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).
Off-brand accents are gone.
deliveries.js's old indigo#6366f1brand accent,Tenants.js's purple#662582, andmultipleOrders.js's antd blue#1890ff/ purple#65387ahave all been retired — brand surfaces are#C01227and metric accents come from the status palette below. Indigo#6366f1now appears only as the "Accepted" status colour.
Status palette (semantic — distinct from brand)
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.
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 | Colour |
|---|---|
| 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
- 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. - Don't reach for
purple lighter/e1bee7or 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). - Don't change avatar sizes mid-page — pick from
{18, 20, 22, 24, 28, 32, 36, 40, 44, 48, 56, 64}and stay consistent.
7. Data layer rules
- Every server call belongs in
src/pages/api/api.jsas an exported async function. Page files should never construct URLs inline forGETs. Mutations are sometimes kept inline (e.g. tenant pricing update) — that's tolerated but discouraged. - Use TanStack Query for all reads. Query keys must include every filter that affects the response so the cache invalidates correctly. Example:
useQuery({ queryKey: ['fetchCountData', appId, userid, startdate, enddate, tenantid, locationid, riderid, tabstatus], queryFn: () => fetchCountAPI(appId, userid, ...) }); - Use
useInfiniteQueryfor paginated rows.getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined. Auto-drain pages with anIntersectionObserveron a sentinel<div ref={loadMoreRef} />placed at the bottom of the<TableContainer>. The canonical pattern lives indeliveries.js(search foruseInfiniteQuery({and the adjacentIntersectionObserversetup). - Mutations go through
useMutationwithonSuccess/onError. After a successful mutation, call.refetch()on every related query — the codebase does not yet usequeryClient.invalidateQueries. Match the surrounding file. - Errors →
OpenToast(message, 'error', 2000)fromcomponents/third-party/OpenToast. Toasts always anchor top-right. Duration is 2000ms for normal, 3000ms for "must be acknowledged". - Skeleton loading: use
OrdersTableSkeletonfor tables (props:rowsPerPagedefault 5,coldefault 1 —colis the count of variable middle columns; total rendered columns =col + 4since checkbox, serial, notes, status, and actions are always present). UseLoaderfor full-page modal backdrop andLoaderWithImagefor inline "loading deliveries…" states.
8. State & auth
- Auth state lives in
localStorage. The keys to know:authname(gate inApp.jsandutils/session.js'sAUTH_PRESENCE_KEY— unrelated to which backend authenticated the session, kept as the presence flag across the migration),doormileToken(the actual JWT sent asAuthorization: Beareron every/admin/*call — seeutils/doormileAxios.js),doormileUser,userid,roleid,tenantid,userfcmtoken,applocations(cached zone list, now hub-derived). When adding auth-touching code, read these directly — there is nouseAuth()hook. - The 401 redirect for the Doormile API comes from
utils/doormileAxios.js(auto-attaches the bearer token; on 401 does a fulllocalStorage.clear()+ hard navigate to/login, matchingutils/session.js'sperformSessionLogoutcontract).src/utils/axios.jsis a separate, older, unrelated mock-service client — don't confuse the two. - Redux slices live in
src/store/reducers/. Use them only for cross-page state (FCM token, login user, sidebar menu open, global snackbar). Do not put per-page form state in Redux.
9. FCM / notifications
- Initialised once in
App.jsviagenerateToken()+initFirebaseNotificationListener()fromfirebase_notification/notification.js. - After any mutation that affects a rider (assign, cancel, change rider), call
POST /utils/notifyuserwith the target rider'suserfcmtoken. Don't forget to also callnotifyRidermutation on success. - Service worker file:
public/firebase-messaging-sw.js— do not edit unless the user explicitly asks. Subtle bugs here cause silent delivery failures.
10. Routing & adding a new page
- Create the page file under
src/pages/nearle/<feature>/<name>.js. - Add the lazy import at the top of
src/routes/MainRoutes.js:const MyPage = Loadable(lazy(() => import('pages/nearle/<feature>/<name>'))); - Register the route inside the
nearlechildren array. - Add a sidebar entry in
src/menu-items/nearle.js(id,<FormattedMessage id="..." />title, url, icon). - Add the i18n key in the relevant
src/utils/locales/*.jsonfile (or wherever the project keeps them; most existing items use the English string as the id).
11. Common gotchas
utcplugin pollution:deliveries.jsextends dayjs withutcat module load. If you import dayjs elsewhere and call.utc()you'll get UTC behaviour even if you didn't ask. Match what the surrounding page does —deliveries.jsdeliberately bucket-parses in local time, not UTC, to stay in sync with the dispatch page.- One API base:
REACT_APP_DOORMILE_URL(api.doormile.com/api/v1), viautils/doormileAxios.js. Seesrc/pages/api/CLAUDE.mdfor the full migration-era rule sheet, including what the new backend has no equivalent for. - The
applocationid"zone" concept no longer exists as a filterable resource on the new API — it was a jupiter-only concept.LocationAutocomplete/fetchAppLocationsnow derive a picker list fromGET /admin/hubsinstead of a real zones endpoint; most list endpoints on the new backend don't accept anapplocationidfilter at all. Don't assume it's wired through end-to-end on a page you haven't checked. rolegating useslocalStorage.getItem('roleid'). Some buttons are conditionally rendered based on it. Do not hide UI based on string equality alone — check existing patterns.- Skeleton vs Loader vs LoaderWithImage — these are different. Skeleton = per-row placeholder, Loader = full-screen backdrop blocking interaction, LoaderWithImage = inline branded spinner. Don't swap them.
- No Maps API key exists in this project anymore. Address search/geocoding goes through
AddressAutocomplete.js(Nominatim) and routing through OSRM — both free, no key. Do not addprocess.env.REACT_APP_GOOGLE_MAPS_API_KEYor any Google Maps script/dependency back in. Tenants.jshas known dangling references (setClientstatus,setState,setSuburb,setTenanatPricing,<Collapse in={open}>) inherited from legacy code. They are tolerated. Do not "fix" them as a drive-by — they are out of scope and removing them risks breaking the row collapse contents.
12. Communication style for changes
- For UI changes, the user runs the dev server at
http://localhost:3000. After editing a page, end with one short sentence telling them which route to reload (e.g. "Reload http://localhost:3000/nearle/pricing to see it"). - Don't commit unless asked. The project has
package-lock.json+yarn.lockboth present — match the user's last commit's lockfile choice before suggestingnpm installvsyarn install. - Verify after edits with a quick JSX parse check (the user has
acornavailable innode_modules) — do not assume the build will pass just because Edit succeeded. - Mark
// removed/// unusedcomments as code smells — delete dead code instead of commenting it out, unless the user explicitly says "keep it commented for now".
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 shared design patterns between pages, the source of truth is
src/themes/dt/tokens.js(values,pillFieldSx, the threetable*Sxhelpers) andsrc/themes/dt/primitives.js(SoftPaper,AccentAvatar). Import from them, don't redesign and don't re-declare locally.src/pages/nearle/deliveries/deliveries.jsremains the reference for how those tokens are composed into a page (filter bar → tabs → table), but it is no longer where the values live.
Astryx v0.1.9 · 153 components
CLI: run every command as yarn dlx @astryxdesign/cli <cmd> (shown below as astryx ...).
SETUP (once, in your app entry e.g. main.tsx) — without these, components render unstyled: import "@astryxdesign/core/reset.css"; import "@astryxdesign/core/astryx.css";
WORKFLOW — discover, don't guess. Before writing UI:
astryx build "<idea>"— START HERE: returns a kit (closest [page] + [block]s + [component]s). No args = full playbook.astryx template <name> [--skeleton]— scaffold the [page]/[block]s it named, or study their layout. Templates are reference code.astryx component <Name>— props + examples for every component you use.
RULES:
- No — components do all layout/spacing. Full page → AppShell; sidebar nav → SideNav.
- Frame first: pick the shell (AppShell / Layout+LayoutPanel) and budget regions in px BEFORE writing content (
astryx docs layout). - Dense data = rows (Table, List/Item) edge-to-edge — never Card-wrapped list items. Card = dashboard widgets, galleries, settings groups only.
- Status → StatusDot/Token; Badge only for counts and enumerated states, never decoration.
- Custom styling: component props first; else style/className with tokens — var(--color-|--spacing-|--radius-*). No raw hex/px. (No StyleX/Tailwind compiler here — don't use xstyle/utility classes.)
- Tokens for every value (
astryx docs tokens). Brand/accent viaastryx theme— never override --color-* in :root. - SELF-CHECK before you finish: re-read the file and replace any raw / layout, imported .css/@apply, or hardcoded value (#hex, 16px) with the component or a token (var(--color-|--spacing-|…)). If unsure a component/prop exists, run
astryx component <Name>/astryx search "<thing>"; don't hand-roll CSS.
MORE CLI: search "" find any component / hook / doc / template / block component --list 153 components by category template --list page + block recipes docs color, elevation, icons, illustrations, internationalization, layout, migration, motion, principles, shape, spacing, styling, theme, tokens, typography swizzle eject component source for deep customization upgrade --apply run after any @astryxdesign/core bump