Files
Doormilexpress_console/CLAUDE.md

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 19.2 + react-app-rewired 2.2 (CRA, not Next.js — no pages/ file-system routing; routes live in src/routes/MainRoutes.js).
  • 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.
  • 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.
  • State: @reduxjs/toolkit 1.9 for cross-page state (FCM token, login user, menu, snackbar, toast). Page-local UI state stays in useState.
  • Routing: react-router-dom 6.10, lazy-loaded via components/Loadable.
  • Forms: formik 2.2 + yup 1.1 where present; new simple forms can use plain useState.
  • Dates: dayjs 1.11 with utc plugin (already extended at the top of deliveries.js). Use dayjs(...).utc() for backend timestamps and bare dayjs(...) for local-time bucketing — see the batch-bucketing comment in deliveries.js for the rationale.
  • Maps: leaflet + react-leaflet only. 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) — see src/firebase_notification/. Toasts via notistack 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.staging is committed; .env.development / .env.production are 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 via utils/doormileAxios.js. The old jupiter-backed REACT_APP_URL / REACT_APP_URL2 / REACT_APP_URL3 have 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 the nearlexpress-docs skill and express-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)

  1. Do not introduce Next.js / SSR patterns. No getServerSideProps, no app/ directory, no next/* imports. This is CRA.
  2. Do not rewrite shared design tokens. Every new page that needs the polished UI must reuse the DT token block (see §6). Do not invent a parallel palette.
  3. Do not change package.json dependency versions unless explicitly asked. The build is sensitive to webpack/svgr/react-scripts versions (see resolutions in package.json).
  4. Do not bypass the dispatch reconcile step. After any manual edit on /doormile/dispatch/preview (rider swap, step reorder), the page must call POST /optimization/reconcile-steps before POST /deliveries/createdeliveries. Skipping this corrupts route sequences.
  5. Do not commit .env* files beyond env.staging (which is the agreed-shared staging baseline).
  6. Do not introduce TypeScript files (.ts / .tsx) into this repo. It is JavaScript; mixing creates lint and tooling friction.
  7. Do not use absolute http://localhost URLs in code. Always read from process.env.REACT_APP_DOORMILE_URL (or use doormileAxios, which already defaults to it).
  8. Do not log userid, authname, FCM tokens, or PII to console.log in production paths. The codebase has many leftover console.log calls — when editing nearby, remove them rather than add more.
  9. Do not add or remove items from the sidebar without updating src/menu-items/nearle.js — the menu drives both display and i18n keys.
  10. 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 to jsconfig.json ("baseUrl": "src"). Prefer import x from 'pages/api/api' over deep relative paths. Look at the surrounding file's import style and match it.

6. Design system (Astryx + the DT tokens)

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:

npx -y @astryxdesign/cli component <Name>

(The root README's yarn dlx form does not work here — yarn is not on PATH.)

6.1 Where the design system lives

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, …

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.

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

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.

Anything genuinely brand-toned should read var(--color-accent) / DT.brand, never a literal #000000.

6.3 Status palette (semantic — the only place colour is allowed to shout)

Never render a status by picking a colour by hand. Resolve it:

import StatusBadge from 'components/nearle_components/StatusBadge';

<StatusBadge status={row.orderstatus} />   // handles casing, backend enums, unknowns

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.

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

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.

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.

6.4 Page anatomy

Follow this order. pages/nearle/hubs/hubs.js is the reference implementation — copy its structure.

  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.

7. Data layer rules

  • Every server call belongs in src/pages/api/api.js as an exported async function. Page files should never construct URLs inline for GETs. 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 useInfiniteQuery for paginated rows. getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined. Auto-drain pages with an IntersectionObserver on a sentinel <div ref={loadMoreRef} /> placed at the bottom of the <TableContainer>. The canonical pattern lives in deliveries.js (search for useInfiniteQuery({ and the adjacent IntersectionObserver setup).
  • Mutations go through useMutation with onSuccess / onError. After a successful mutation, call .refetch() on every related query — the codebase does not yet use queryClient.invalidateQueries. Match the surrounding file.
  • Errors → OpenToast(message, 'error', 2000) from components/third-party/OpenToast. Toasts always anchor top-right. Duration is 2000ms for normal, 3000ms for "must be acknowledged".
  • Skeleton loading: use OrdersTableSkeleton for tables (props: rowsPerPage default 5, col default 1 — col is the count of variable middle columns; total rendered columns = col + 4 since checkbox, serial, notes, status, and actions are always present). Use Loader for full-page modal backdrop and LoaderWithImage for inline "loading deliveries…" states.

8. State & auth

  • Auth state lives in localStorage. The keys to know: authname (gate in App.js and utils/session.js's AUTH_PRESENCE_KEY — unrelated to which backend authenticated the session, kept as the presence flag across the migration), doormileToken (the actual JWT sent as Authorization: Bearer on every /admin/* call — see utils/doormileAxios.js), doormileUser, userid, roleid, tenantid, userfcmtoken, applocations (cached zone list, now hub-derived). When adding auth-touching code, read these directly — there is no useAuth() hook.
  • The 401 redirect for the Doormile API comes from utils/doormileAxios.js (auto-attaches the bearer token; on 401 does a full localStorage.clear() + hard navigate to /login, matching utils/session.js's performSessionLogout contract). src/utils/axios.js is 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.js via generateToken() + initFirebaseNotificationListener() from firebase_notification/notification.js.
  • After any mutation that affects a rider (assign, cancel, change rider), call POST /utils/notifyuser with the target rider's userfcmtoken. Don't forget to also call notifyRider mutation 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

  1. Create the page file under src/pages/nearle/<feature>/<name>.js.
  2. Add the lazy import at the top of src/routes/MainRoutes.js:
    const MyPage = Loadable(lazy(() => import('pages/nearle/<feature>/<name>')));
    
  3. Register the route inside the nearle children array.
  4. Add a sidebar entry in src/menu-items/nearle.js (id, <FormattedMessage id="..." /> title, url, icon).
  5. Add the i18n key in the relevant src/utils/locales/*.json file (or wherever the project keeps them; most existing items use the English string as the id).

11. Common gotchas

  • utc plugin pollution: deliveries.js extends dayjs with utc at 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.js deliberately 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), via utils/doormileAxios.js. See src/pages/api/CLAUDE.md for 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/fetchAppLocations now derive a picker list from GET /admin/hubs instead of a real zones endpoint; most list endpoints on the new backend don't accept an applocationid filter at all. Don't assume it's wired through end-to-end on a page you haven't checked.
  • role gating uses localStorage.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 add process.env.REACT_APP_GOOGLE_MAPS_API_KEY or any Google Maps script/dependency back in.
  • Tenants.js has 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.lock both present — match the user's last commit's lockfile choice before suggesting npm install vs yarn install.
  • Verify after edits with a quick JSX parse check (the user has acorn available in node_modules) — do not assume the build will pass just because Edit succeeded.
  • Mark // removed / // unused comments 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 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 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:

  1. astryx build "<idea>" — START HERE: returns a kit (closest [page] + [block]s + [component]s). No args = full playbook.
  2. astryx template <name> [--skeleton] — scaffold the [page]/[block]s it named, or study their layout. Templates are reference code.
  3. 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 via astryx 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