Files
nearle_console/CLAUDE.md

21 KiB
Raw Permalink Blame History

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

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

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 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 purple #662582; 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 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 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 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 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.
  • 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.