Files
Doormilexpress_console/CLAUDE.md

24 KiB
Raw Blame History

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 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.
  • 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_URL (primary API base), REACT_APP_URL2 (secondary API base — used for /users/update, /tenants/update, /partners/getriderlogs, archival /orders/getorders). 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 Jupiter auth URL (jupiter.nearle.app) are hardcoded — see the nearlexpress-docs skill.
  • 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_URL / REACT_APP_URL2.
  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 (the DT token block)

The polished pages (deliveries.js) share a token block at the top of the file. Every new operator page must paste and reuse this block — do not invent fresh colours, spacings, or radius numbers.

const DT = {
  radiusPill: 999,
  radiusCard: 16,
  radiusInner: 12,
  shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
  shadowMd:   '0 8px 24px rgba(15, 23, 42, 0.08)',
  shadowPop:  '0 18px 50px rgba(15, 23, 42, 0.18)',
  textPrimary:   '#0f172a',
  textSecondary: '#64748b',
  textMuted:     '#94a3b8',
  borderSubtle:  '#e2e8f0',
  divider:       '#f1f5f9',
  surface:       '#ffffff',
  surfaceAlt:    '#f8fafc'
};

// Alpha-suffix helpers — append hex transparency to any accent colour.
const a    = (c, suffix) => `${c}${suffix}`;
const tint = (c) => a(c, '08'); // very subtle surface tint
const soft = (c) => a(c, '18'); // soft chip / avatar bg
const ring = (c) => a(c, '26'); // focus ring
const edge = (c) => a(c, '55'); // resting border

// Pill-style filter inputs.
const pillFieldSx = (color) => ({
  '& .MuiOutlinedInput-root': {
    borderRadius: DT.radiusPill + 'px',
    bgcolor: tint(color),
    fontWeight: 600,
    '& fieldset':                { borderColor: edge(color), borderWidth: 1.5 },
    '&:hover fieldset':          { borderColor: color },
    '&.Mui-focused':             { boxShadow: `0 0 0 3px ${ring(color)}` },
    '&.Mui-focused fieldset':    { borderColor: color, borderWidth: 2 }
  }
});

// Soft Paper used by all Autocomplete popups (matches the deliveries batch dropdown).
const SoftPaper = (props) => (
  <Paper {...props} sx={{ mt: 0.75, borderRadius: 2, boxShadow: DT.shadowPop, border: '1px solid', borderColor: 'divider', overflow: 'hidden' }} />
);

// Colored avatar — flips between filled and soft based on `selected`.
const AccentAvatar = ({ color, selected, size = 24, children }) => (
  <Avatar sx={{
    width: size, height: size,
    bgcolor: selected ? color : soft(color),
    color: selected ? '#fff' : color,
    transition: 'background-color 0.15s, color 0.15s'
  }}>{children}</Avatar>
);

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.

  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}).
  2. KPI tiles rowGrid of 34 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 Autocompletes using pillFieldSx(color) + SoftPaper, each with an AccentAvatar start-adornment.
  4. Pill status tabs + pill search — tabs as clickable <Box> pills (active = filled accent + glow ring, inactive = tint bg + edge border), each with an avatar icon and a count badge. Tab accents use the semantic status colour for each tab (pending → amber, delivered → emerald, etc.). 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 cellsStack 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

#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).

Migration note: deliveries.js currently uses #6366f1 (indigo) as its brand accent — a holdover from the first design pass before brand red was canonicalised. It is scheduled to migrate to #C01227. The createorder1 Saved-Address dialog has already been migrated.

Status palette (semantic — distinct from brand)

These colour-code lifecycle states. Do not swap them for brand red — operators rely on the colour to identify status at a glance.

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 / 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).
  • 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.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), userid, roleid, userfcmtoken, applocations (cached zone list). When adding auth-touching code, read these directly — there is no useAuth() hook.
  • The 401 redirect comes from src/utils/axios.js. Most pages bypass that interceptor by importing raw axios. If you need guaranteed 401 handling for a new flow, import from utils/axios instead.
  • 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.
  • Two API bases: most calls hit REACT_APP_URL. A handful hit REACT_APP_URL2 (/users/update, /tenants/update, archival orders, rider logs). Check pages/api/api.js before changing one.
  • The applocationid query param: every list endpoint expects this — 0 means "All Zones". Pages default appId = 0 and update it from LocationAutocomplete.
  • 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 source of truth is the DT token block and helpers near the top of src/pages/nearle/deliveries/deliveries.js (search for const DT = {). Copy from there, don't redesign.

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