# 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 `<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`, 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) => (
  <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 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 `<Paper>`** — `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 `<Paper>`** (optional) — pill-style `Autocomplete`s 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 purple `#662582` (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 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 `<Empty />` 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 `<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 `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 `<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`:
   ```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.
- **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`, `<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.
