Files
Doormilexpress_console/CLAUDE.md
dharaneesh-r 59f31c8adf Migrate console off jupiter.nearle.app to the Doormile Express API
Retires REACT_APP_URL/URL2/URL3 in favor of REACT_APP_DOORMILE_URL across
every page (orders, deliveries, riders, tenants, pricing, profile, reports,
dispatch). Fixes several field-mapping and envelope-check bugs found along
the way, most notably that /admin/milers/:id routes (block, assign-vehicle,
edit, notify) key off milerprofileid, not userid, and that a miler's real
fields are phone/availabilitystatus/displayname, not contactno/status/
firstname+lastname (confirmed against a live read-only session).

Also fixes several silent-failure bugs uncovered during that audit: order
creation and order cancellation showed a success toast but gave no feedback
at all on failure (createorder1.js had a dead notifyadmin() call that left
the loading spinner stuck forever on every failed submit), and Tenants.js's
pricing/profile updates never surfaced a failed response to the operator.

The AI dispatch optimiser (routes.workolik.com/routemate.workolik.com) and
its jupiter.nearle.app delivery-commit call remain untouched by design —
separate solver service with no equivalent in the new API.
2026-08-06 19:26:23 +05:30

25 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_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 (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 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 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