Files
nearle_console/CLAUDE.md

303 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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 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.