# CLAUDE.md — NearlExpress 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 NearlExpress 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 `` 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`, plus `@react-google-maps/api` for Google. Geocoding via `react-geocode`. Maps API key is `process.env.REACT_APP_GOOGLE_MAPS_API_KEY`. - **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 ```bash # 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`), `REACT_APP_GOOGLE_MAPS_API_KEY`. 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 `/nearle/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 /nearle/* 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`, `clients/Tenants.js`, `clientPricing/clientPricing.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. ```js 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) => ( ); // Colored avatar — flips between filled and soft based on `selected`. const AccentAvatar = ({ color, selected, size = 24, children }) => ( {children} ); ``` ### Page anatomy (every operator page should follow this order) > **Use brand purple `#662582` (with light variant `#9255AB`) for every brand surface below.** Don't use indigo `#6366f1` — that's reserved for the "Accepted" status badge only. 1. **Gradient header ``** — `linear-gradient(135deg, ${tint('#662582')} 0%, ${tint('#9255AB')} 100%)`, 48px filled `#662582` 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="#662582" paperComponent={SoftPaper}`). 2. **KPI tiles row** — `Grid` of 3–4 `Paper` cards, each with a 3px top stripe gradient, uppercase eyebrow label, large bold number, and a soft-tinted avatar holding an icon. The "primary" tile uses brand purple `#662582`; other tiles use semantic status colours. 3. **Filter bar ``** (optional) — pill-style `Autocomplete`s using `pillFieldSx(color)` + `SoftPaper`, each with an `AccentAvatar` start-adornment. 4. **Pill status tabs + pill search** — tabs as clickable `` 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 purple `#662582` (tint bg, edge border, focus ring). 5. **Table ``** with `` + sticky `` — uppercase muted headers on `DT.surfaceAlt`, rows with `borderBottom: 1px solid ${DT.divider}` and `:hover` row tint. Scrollbar thumb uses brand purple `edge('#662582')`. 6. **Status badges in cells** — `Stack` with `AccentAvatar` + label inside a soft pill (tint bg, edge border). Status colours come from a per-page `STATUS_META` map keyed by lowercase status string — these are **semantic**, not brand. 7. **Edit / action icon buttons** — soft-pill `IconButton` using brand purple `#662582` (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 `` for new code. ### Universal brand colour **`#662582` — NearlExpress brand purple.** 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` | `#E8D9EF` | Very subtle wash bg | | `primary.light` / `primary.400` | `#9255AB` | Gradient pair with main | | `primary.main` | `#662582` | Brand primary — default for all brand surfaces | | `primary.dark` | `#4D1C61` | Hover / pressed states | | `primary.darker` | `#260E30` | Deep contrast text on light bg | **Page header / dialog header gradient:** `linear-gradient(135deg, ${tint('#662582')} 0%, ${tint('#9255AB')} 100%)` (subtle wash) or `linear-gradient(135deg, #662582 0%, #9255AB 100%)` (solid, for dialog titles). > **Migration note:** `deliveries.js`, `Tenants.js`, and `clientPricing.js` currently use `#6366f1` (indigo) as their brand accent — a holdover from the first design pass before brand purple was canonicalised. They are scheduled to migrate to `#662582`. `customers.js` and the createorder1 Saved-Address dialog have already been migrated. ### Status palette (semantic — distinct from brand) These colour-code lifecycle states. Do **not** swap them for brand purple — 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 purple) | | 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 `` for status/filter switching — they were replaced by the pill `` pattern on every redesigned page. Pages that still use `` (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 `GET`s. 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: ```js 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 `
` placed at the bottom of the ``. 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//.js`. 2. Add the lazy import at the top of `src/routes/MainRoutes.js`: ```js const MyPage = Loadable(lazy(() => import('pages/nearle//'))); ``` 3. Register the route inside the `nearle` children array. 4. Add a sidebar entry in `src/menu-items/nearle.js` (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. - **Maps API key** is read from env on every render in some places — wrapping it in a `useMemo` is fine but unnecessary. Don't add new direct `process.env.REACT_APP_GOOGLE_MAPS_API_KEY` reads outside map-related files. - **`Tenants.js` has known dangling references** (`setClientstatus`, `setState`, `setSuburb`, `setTenanatPricing`, ``) 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.