diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..b80aacf
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,44 @@
+{
+ "$schema": "https://json.schemastore.org/claude-code-settings.json",
+ "permissions": {
+ "allow": [
+ "Bash(npm install)",
+ "Bash(npm install:*)",
+ "Bash(npm run lint)",
+ "Bash(npm run lint:*)",
+ "Bash(npm run build)",
+ "Bash(npm run build:*)",
+ "Bash(npm start)",
+ "Bash(npm run start:*)",
+ "Bash(yarn)",
+ "Bash(yarn install)",
+ "Bash(yarn lint)",
+ "Bash(yarn build)",
+ "Bash(yarn start)",
+ "Bash(npx eslint:*)",
+ "Bash(git status:*)",
+ "Bash(git diff:*)",
+ "Bash(git log:*)",
+ "Bash(git show:*)",
+ "Bash(git branch:*)",
+ "Bash(git ls-files:*)",
+ "Bash(git rev-parse:*)",
+ "Bash(ls:*)",
+ "Bash(pwd)",
+ "Bash(wc:*)",
+ "Bash(node -e:*)",
+ "Bash(node --version)"
+ ],
+ "deny": [
+ "Bash(rm -rf /:*)",
+ "Bash(git push --force:*)",
+ "Bash(git reset --hard:*)",
+ "Bash(git commit --amend:*)",
+ "Bash(git checkout -- .:*)",
+ "Bash(git clean -f:*)",
+ "Bash(git branch -D:*)",
+ "Bash(npm publish:*)",
+ "Bash(yarn publish:*)"
+ ]
+ }
+}
diff --git a/.claude/skills/nearlexpress-docs/SKILL.md b/.claude/skills/nearlexpress-docs/SKILL.md
new file mode 100644
index 0000000..d1abb00
--- /dev/null
+++ b/.claude/skills/nearlexpress-docs/SKILL.md
@@ -0,0 +1,409 @@
+---
+name: nearlexpress-docs
+description: "Architecture, navigation flow, per-page API map, optimisation pipeline, and FCM rules for the NearlExpress Console (nearlexpress-xpressconsole-d0ee01adebe9). Load when working anywhere in this repo or when the user asks about its pages (orders, deliveries, dispatch, tenants, pricing, customers, riders, invoices, reports, requests), the AI dispatch solvers (routes.workolik.com / routemate.workolik.com), the optimisation → preview → reconcile → assign → notify pipeline, FCM/Firebase notifications, TanStack Query data flow, or which API endpoint a given page calls."
+metadata:
+ version: 2.0.0
+---
+
+# NearlExpress Console — Project Reference
+
+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.
+
+This skill is the **knowledge** Claude needs about the system (what each page does, which API each calls, how the pieces wire together). The **rules** Claude must follow when editing live in the root `CLAUDE.md` — don't restate them here.
+
+---
+
+## 1. Architecture at a glance
+
+- **Brand colour: `#662582` (NearlExpress purple)** — defined in `src/themes/theme/default.js` as `primary.main`. Used by the sidebar, page headers, dialog/popup headers, KPI primary tiles, search bars, edit-action buttons. Gradient pair `#662582 → #9255AB`. Status badges use a separate semantic palette (amber/indigo/cyan/teal/emerald/red/orange) — see root `CLAUDE.md` §6.
+- **Stack:** React 18.2 + react-app-rewired (CRA), MUI 5, TanStack Query 5, Redux Toolkit, react-router 6, axios, dayjs, leaflet, firebase 10 (FCM), notistack, formik+yup, react-dnd.
+- **Entry:** `src/index.js` → providers (Redux store, QueryClient, Router, ThemeCustomization, Notistack) → `src/App.js`.
+- **Auth gate:** `App.js` checks `localStorage.getItem('authname')` — empty redirects to `/login`. Login posts to `jupiter.nearle.app/users/console/login` with `configid: 9` and the device's FCM token.
+- **Routing:** `src/routes/MainRoutes.js` declares all `/nearle/*` routes, lazy-loaded via `components/Loadable`. Sidebar items are declared in `src/menu-items/nearle.js`.
+- **Data layer:** every server call lives in `src/pages/api/api.js`. Pages call those exports via `useQuery` / `useInfiniteQuery` / `useMutation`. Query keys MUST include every filter parameter so caching invalidates correctly.
+- **State:** Redux Toolkit slices (`fcmSlice`, `loginUserSlice`, `menu`, `snackbar`, `toastSlice`, `auth`) for cross-page state; per-page UI state stays in `useState`.
+- **Two API bases:** `process.env.REACT_APP_URL` (primary) and `process.env.REACT_APP_URL2` (used for `/users/update`, `/tenants/update`, archival `/orders/getorders`, rider logs).
+- **External services (hardcoded):** `routes.workolik.com` (Bike/Manual solver), `routemate.workolik.com` (Auto/multi-trip solver), `jupiter.nearle.app` (login + final delivery commit).
+- **localStorage keys:** `authname`, `userid`, `roleid`, `userfcmtoken`, `applocations` (cached zone list).
+- **Notifications:** FCM init in `App.js` via `firebase_notification/notification.js` (`generateToken`, `initFirebaseNotificationListener`). Service worker at `public/firebase-messaging-sw.js`.
+
+---
+
+## 2. End-to-end console flow
+
+```mermaid
+flowchart TD
+ classDef entry fill:#f9f6ff,stroke:#a78bfa,stroke-width:2px,color:#2e1065;
+ classDef auth fill:#fdf2f8,stroke:#ec4899,stroke-width:2px,color:#831843;
+ classDef nav fill:#eef2ff,stroke:#6366f1,stroke-width:2px,color:#312e81;
+ classDef core fill:#e0f2fe,stroke:#0ea5e9,stroke-width:2px,color:#0c4a6e;
+ classDef sub fill:#ecfeff,stroke:#06b6d4,stroke-width:1.5px,color:#155e75;
+ classDef action fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#78350f;
+ classDef ext fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d;
+ classDef report fill:#f5f5f4,stroke:#78716c,stroke-width:1.5px,color:#292524;
+ classDef state fill:#fdf4ff,stroke:#8b5cf6,stroke-width:1.5px,color:#581c87;
+
+ Boot([Browser load
src/index.js]):::entry
+ Providers["Providers wiring
Redux · QueryClient · Router · Theme · Notistack"]:::entry
+ App["src/App.js
localStorage('authname') gate"]:::auth
+ FCMInit["generateToken()
initFirebaseNotificationListener()"]:::auth
+ SW["public/firebase-messaging-sw.js"]:::auth
+ Login[/"/login
pages/nearle/login1.js"/]:::auth
+
+ Boot --> Providers --> App
+ App -- no authname --> Login
+ Login -- POST jupiter.nearle.app/users/console/login --> AuthOK{Auth OK?}
+ AuthOK -- yes (save authname/userid/roleid/userfcmtoken) --> Layout
+ AuthOK -- no --> Login
+ App --> FCMInit --> SW
+
+ Layout["layout/MainLayout
Sidebar + Header + Outlet"]:::nav
+ Sidebar["menu-items/nearle.js"]:::nav
+ Layout --- Sidebar
+ Sidebar --> Dispatch
+ Sidebar --> Orders
+ Sidebar --> Deliveries
+ Sidebar --> Tenants
+ Sidebar --> Pricing
+ Sidebar --> Customers
+ Sidebar --> Riders
+ Sidebar --> Invoice
+ Sidebar --> ReportsHub
+
+ Dispatch[/"/nearle/dispatch
Live Map · Riders · Batches"/]:::core
+ Orders[/"/nearle/orders
Orders Dashboard"/]:::core
+ Deliveries[/"/nearle/deliveries
Dispatched Deliveries"/]:::core
+ Tenants[/"/nearle/tenants
Client/Tenant Management"/]:::core
+ Pricing[/"/nearle/pricing
Pricing Matrix (master-detail)"/]:::core
+ Customers[/"/nearle/customers
Customer Directory"/]:::core
+ Riders[/"/nearle/riders
Rider Pool"/]:::core
+ Invoice[/"/nearle/invoice
Billing"/]:::core
+ Requests[/"/nearle/requests
Expense Approvals"/]:::core
+ ReportsHub[/"/nearle/reports/*
BI Suite"/]:::core
+
+ OrdersCreate[/"/orders/create"/]:::sub
+ OrdersMulti[/"/orders/createorders"/]:::sub
+ OrdersDetails[/"/orders/details"/]:::sub
+ OrdersPreview[/"/orders/preview"/]:::sub
+ DispatchPreview[/"/dispatch/preview"/]:::sub
+ RidersCreate[/"/riders/create"/]:::sub
+ RidersEdit[/"/riders/edit"/]:::sub
+ ClientsCreate[/"/clients/create"/]:::sub
+ CustomerCreate[/"/customer/create"/]:::sub
+ InvoicePreview[/"/invoice/preview"/]:::sub
+ ReportsOS[/"/reports/orderssummary"/]:::report
+ ReportsOD[/"/reports/ordersdetails"/]:::report
+ ReportsRS[/"/reports/riderssummary"/]:::report
+ ReportsRL[/"/reports/riderslogs"/]:::report
+
+ Orders --> OrdersCreate
+ Orders --> OrdersMulti
+ Orders --> OrdersDetails
+ Riders --> RidersCreate
+ Riders --> RidersEdit
+ Tenants --> ClientsCreate
+ Customers --> CustomerCreate
+ Invoice --> InvoicePreview
+ ReportsHub --> ReportsOS
+ ReportsHub --> ReportsOD
+ ReportsHub --> ReportsRS
+ ReportsHub --> ReportsRL
+
+ %% Optimisation pipeline
+ SelectOrders["Select N pending orders"]:::action
+ ChooseSolver{"Choose Dispatch Mode"}:::action
+ SolverBike["Mode 1 · Bike
POST routes.workolik.com
/optimization/riderassign"]:::ext
+ SolverAuto["Mode 2 · Auto
POST routemate.workolik.com
/optimization/riderassign"]:::ext
+ SolverManual["Mode 0 · Manual
POST routes.workolik.com
/optimization/createdeliveries"]:::ext
+ Reconcile["POST routes.workolik.com
/optimization/reconcile-steps"]:::ext
+ FinalAssign["POST jupiter.nearle.app
/deliveries/createdeliveries"]:::ext
+ NotifyRider["POST /utils/notifyuser (FCM)"]:::ext
+
+ Orders --> SelectOrders --> ChooseSolver
+ ChooseSolver -- Bike --> SolverBike
+ ChooseSolver -- Auto --> SolverAuto
+ ChooseSolver -- Manual --> SolverManual
+ SolverBike --> OrdersPreview
+ SolverAuto --> OrdersPreview
+ SolverManual --> OrdersPreview
+ OrdersPreview --> DispatchPreview
+ DispatchPreview -. manual edit .-> Reconcile
+ Reconcile --> DispatchPreview
+ DispatchPreview -- Assign --> FinalAssign
+ FinalAssign --> NotifyRider
+ FinalAssign -- redirect --> Deliveries
+
+ %% Deliveries actions
+ DChangeRider["Change Rider"]:::action
+ DCancel["Cancel Delivery"]:::action
+ DUpdate["Update Amount / Notes"]:::action
+ Deliveries --> DChangeRider --> NotifyRider
+ Deliveries --> DCancel --> NotifyRider
+ Deliveries --> DUpdate
+
+ %% Cross-cutting
+ subgraph CROSS [Cross-cutting infrastructure]
+ direction LR
+ Store["Redux: fcm · login · menu · snackbar · toast · auth"]:::state
+ QC["TanStack Query (cache + infinite scroll)"]:::state
+ Env["REACT_APP_URL · REACT_APP_URL2 · REACT_APP_GOOGLE_MAPS_API_KEY"]:::state
+ LS["localStorage: authname · userid · roleid · userfcmtoken · applocations"]:::state
+ end
+
+ Layout -. reads .-> Store
+ Layout -. reads .-> LS
+ Orders & Deliveries & Dispatch & Tenants & Pricing & Customers & Riders & Invoice -. fetches via .-> QC
+ QC -. base URLs from .-> Env
+```
+
+---
+
+## 3. Optimisation pipeline (sequence)
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant U as Operator
+ participant O as /nearle/orders
+ participant S as Solver (routes/routemate.workolik.com)
+ participant P as /nearle/dispatch/preview
+ participant R as reconcile-steps
+ participant J as jupiter.nearle.app
+ participant D as /nearle/deliveries
+ participant F as FCM (rider device)
+
+ U->>O: Select pending orders (checkbox)
+ U->>O: Pick dispatch mode (Bike / Auto / Manual)
+ O->>S: POST riderassign or createdeliveries
+ S-->>O: Optimised route & assignments
+ O->>P: Redirect to preview
+ U->>P: Drag steps / swap rider (optional)
+ P->>R: POST reconcile-steps (after each manual edit)
+ R-->>P: Updated, validated sequence
+ U->>P: Confirm "Assign"
+ P->>J: POST /deliveries/createdeliveries
+ J-->>P: 200 OK
+ P->>F: POST /utils/notifyuser (per rider)
+ P->>D: Redirect to deliveries dashboard
+```
+
+**Hard rule:** never call `POST /deliveries/createdeliveries` after manual edits without calling `POST /optimization/reconcile-steps` first. Skipping reconcile corrupts route sequences.
+
+---
+
+## 4. Page-by-page API map
+
+> Every named function below lives in `src/pages/api/api.js`. URLs use `REACT_APP_URL` unless noted otherwise (`REACT_APP_URL2`).
+
+### 4.1 Authentication
+- **Page:** `pages/nearle/login1.js`
+
+| Endpoint | Method | Function | Purpose |
+| --- | --- | --- | --- |
+| `https://jupiter.nearle.app/live/api/v1/users/console/login` | `POST` | *direct axios* | Console staff auth. Requires `authname`, `password`, `configid: 9`, `userfcmtoken`. |
+| `/partners/getlocations/?userid={id}` | `GET` | `fetchAppLocations` | Operational locations assigned to the operator. |
+| `REACT_APP_URL2/users/update` | `PUT` | `updateUser` | First-time password setup / change password. |
+
+### 4.2 Orders
+- **Page:** `pages/nearle/orders/orders.js`
+
+| Endpoint | Method | Function | Purpose |
+| --- | --- | --- | --- |
+| `/orders/getordersummary/?applocationid={appId}` | `GET` | `fetchPercentageData` / `fetchorderscount` | Status statistics (total, pending, delivered, cancelled). |
+| `/orders/tenant/getorders/` | `GET` | `fetchOrders` | Infinite paginated order list. |
+| `/utils/getapptypes/?tag=paymentmode` | `GET` | `fetchPaymentType` | Payment-mode dropdown options. |
+| `/partners/getriders/?applocationid={appId}` | `GET` | `fetchRidersList` | Active riders for manual assignment. |
+| `/tenants/gettenants/?applocationid={appId}&status=active` | `GET` | `getTenants` | Active tenants for filter. |
+| `/tenants/gettenantlocations/?tenantid={appId}` | `GET` | `gettenantlocations` | Store branches for a tenant. |
+| `/partners/getallriders?partnerid=64` | `GET` | `getallriders` | Full driver pool for bulk dispatch. |
+| `/orders/updateorder` | `PUT` | `cancelOrder` | Cancel a single order. |
+| `/orders/updatemultipleorders` | `PUT` | `cancelMultipleOrder` | Bulk cancel. |
+
+### 4.3 AI dispatch / optimisation
+- **Pages:** `orders/orders.js`, `dispatch/Preview.js`
+
+| Endpoint | Method | Function | Mode |
+| --- | --- | --- | --- |
+| `https://routes.workolik.com/api/v1/optimization/createdeliveries` | `POST` | `createOptimisationDeliveries` | Mode 0 · Manual |
+| `https://routes.workolik.com/api/v1/optimization/riderassign?hypertuning_params={params}` | `POST` | `createAutomationDeliveries` | Mode 1 · Bike hyper-tuning |
+| `https://routemate.workolik.com/api/v1/optimization/riderassign?strategy=multi_trip` | `POST` | `createAutomationDeliveries` | Mode 2 · Auto multi-trip |
+| `https://routes.workolik.com/api/v1/optimization/reconcile-steps` | `POST` | `reconcileSteps` | Validate sequence after manual edits |
+| `https://jupiter.nearle.app/live/api/v1/deliveries/createdeliveries` | `POST` | `finalCreatedeliveries` | Commit assignments |
+
+### 4.4 Deliveries
+- **Page:** `pages/nearle/deliveries/deliveries.js`
+
+| Endpoint | Method | Function | Purpose |
+| --- | --- | --- | --- |
+| `/deliveries/getdeliveries/` | `GET` | `fetchDeliveries` | Streams delivery records (infinite query). Client-side wave filter by `assigntime`. |
+| `/deliveries/deliverysummary/` | `GET` | `fetchCountAPI` / `fetchPercentageAPI` | Lifecycle chip counts (pending/accepted/arrived/picked/active/skipped/delivered/cancelled). |
+| `/orders/getorderdetails?orderheaderid={id}` | `GET` | `getorderdetails` | Items detail for invoice preview. |
+| `/deliveries/updatedelivery` | `PUT` | `cancelDeliveryAPI` | Cancel with feedback. |
+| `/deliveries/updatedelivery` | `PUT` | `changeRiderAPI` | In-transit rider reassignment (also resets `assigntime`). |
+| `/deliveries/updatedelivery` | `PUT` | `updateDeliveryAPI` | Amount / notes update. |
+| `/utils/notifyuser` | `POST` | `notifyRider` | FCM push to rider's device. Required after every rider-affecting mutation. |
+
+### 4.5 Tenants
+- **Page:** `pages/nearle/clients/Tenants.js`
+
+| Endpoint | Method | Function | Purpose |
+| --- | --- | --- | --- |
+| `/tenants/getalltenants/?status={status}&applocationid={appId}&keyword={kw}&pageno={p}&pagesize={n}&moduleid=6` | `GET` | `getalltenants` | Onboarded tenants. |
+| `/tenants/gettenantsummary/?moduleid=6&applocationid={appId}` | `GET` | `gettenantsummary` | Tab counts. |
+| `/tenants/getpricinglist/?moduleid=6&applocationid={appId}` | `GET` | `getpricinglist` | Subscription / billing contracts. |
+| `/tenants/gettenantpricing/?tenantid={id}` | `GET` | *direct axios* | Tenant base pricing config. |
+| `REACT_APP_URL2/tenants/update` | `PUT` | *direct axios* | Profile / coordinates / approval. |
+| `REACT_APP_URL2/tenants/update/services` | `PUT` | *direct axios* | Customised tenant service rates. |
+
+### 4.6 Pricing
+- **Page:** `pages/nearle/clientPricing/clientPricing.js`
+
+| Endpoint | Method | Function | Purpose |
+| --- | --- | --- | --- |
+| `/utils/getallpricing/?applocationid={appId}` | `GET` | *direct axios* | Base pricing models for the zone. |
+
+### 4.7 Customers
+- **Page:** `pages/nearle/customers/customers.js`
+
+| Endpoint | Method | Function | Purpose |
+| --- | --- | --- | --- |
+| `/customers/getcustomersummary?applocationid={appId}` | `GET` | `getcustomersummary` | Customer count summary. |
+| `/customers/getallcustomers/` | `GET` | `getallcustomers` | Searchable directory (infinite query). |
+| `/customers/update` | `PUT` | *direct axios* | Update customer profile + address. |
+
+### 4.8 Riders
+- **Pages:** `riders/riders.js`, `createrider.js`, `editRider.js`
+
+| Endpoint | Method | Function | Purpose |
+| --- | --- | --- | --- |
+| `/partners/getallriders/?applocationid={appId}&pageno={p}&pagesize=20&keyword={kw}&status={status}` | `GET` | `fetchAllRiders` | Filtered driver list. |
+| `/partners/getallridersummary/?applocationid={appId}&status={status}` | `GET` | `getallridersummary` | Crew breakdowns. |
+| `/partners/getriders/?applocationid={appId}&pageno={p}&pagesize=20&keyword={kw}` | `GET` | `fetchRiders` | Active/online riders. |
+| `/utils/getriderstatus` | `GET` | `getriderstatus` | Check-in history. |
+| `/partners/createrider` | `POST` | *direct axios* | Onboard rider. |
+| `/partners/update/rider` | `PUT` | *direct axios* | Edit rider vehicle/license/bank/status. |
+
+### 4.9 Invoices
+- **Pages:** `invoice/invoice.js`, `invoicePreview.js`
+
+| Endpoint | Method | Function | Purpose |
+| --- | --- | --- | --- |
+| `/invoice/getinvoiceinsight` | `GET` | `fetchinvoiceinsight` | Global accounting summary. |
+| `/invoice/getallinvoice/?billstatus={status}` | `GET` | `fetchdeliverylist` | Invoice list (Paid vs Unpaid tabs). |
+
+### 4.10 Dispatch (live tracker)
+- **Page:** `pages/nearle/dispatch/Dispatch.js`
+
+| Endpoint | Method | Function | Purpose |
+| --- | --- | --- | --- |
+| `/utils/getriderperiodiclogs?userid={id}` | `GET` | `getRiderPeriodicLogs` | Rider GPS/battery/connectivity/current order — polled for live map. |
+| `https://routes.workolik.com/api/v1/batch/efficiency` | `POST` | `fetchBatchEfficiency` | Per-batch solver efficiency rating. |
+
+### 4.11 Reports / BI
+- **Pages:** `reports/ordersSummary.js`, `ordersDetails.js`, `ridersSummary.js`, `ridersLogs.js`
+
+| Endpoint | Method | Function | Purpose |
+| --- | --- | --- | --- |
+| `/deliveries/getreportsummary/?applocationid&tenantid&locationid&fromdate&todate` | `GET` | `getreportsummary` | Aggregated fulfilment. |
+| `/deliveries/getreportlocationsummary/?...` | `GET` | `getreportlocationsummary` | Per-branch breakdown. |
+| `REACT_APP_URL2/orders/getorders/?fromdate&todate&applocationid&pageno&pagesize` | `GET` | `fetchorderdetails` | Archival historical orders. |
+| `/deliveries/getriderbydelivery/?applocationid&tenantid&locationid&fromdate&todate` | `GET` | `getriderbydelivery` | Per-rider delivery trace. |
+| `/deliveries/deliverysummary/?applocationid&fromdate&todate` | `GET` | `fetchCount` | Volume metrics over range. |
+| `/deliveries/getridersummary/?applocationid&fromdate&todate` | `GET` | `fetchRidersSummary` | Per-rider performance stats. |
+| `/partners/getpartners` | `GET` | `fetchLocations` | Merchant locations for routing. |
+| `REACT_APP_URL2/partners/getriderlogs/?applocationid&fromdate&todate&keyword` | `GET` | `fetchRidersLogs` | Driver login / checkout audit. |
+
+### 4.12 Requests (expense reimbursements)
+- **Page:** `pages/nearle/requests/requests.js`
+
+| Endpoint | Method | Function | Purpose |
+| --- | --- | --- | --- |
+| `/payments/requests/getpaymentrequest/?partnerid={tid}&status={0 or 1}` | `GET` | `clientdetailspending` / `clientdetailsapproved` | Status 0 = Pending, 1 = Approved. |
+
+---
+
+## 5. Route → file map (full)
+
+| Route | File | Lazy import name |
+| --- | --- | --- |
+| `/login` | `pages/nearle/login1.js` | `Login` |
+| `/nearle/dispatch` | `pages/nearle/dispatch/Dispatch.js` | `Dispatch` |
+| `/nearle/dispatch/preview` | `pages/nearle/dispatch/Preview.js` | `DispatchPreview` |
+| `/nearle/orders` | `pages/nearle/orders/orders.js` | `Orders` |
+| `/nearle/orders/preview` | `pages/nearle/orders/OrdersPreview.js` | `OrdersPreview` |
+| `/nearle/orders/create` | `pages/nearle/orders/createorder1.js` | `Createorder1` |
+| `/nearle/orders/createorders` | `pages/nearle/orders/multipleOrders.js` | `MultipleOrders` |
+| `/nearle/orders/details` | `pages/nearle/orders/details.js` | `Details` |
+| `/nearle/deliveries` | `pages/nearle/deliveries/deliveries.js` | `Deliveries` |
+| `/nearle/tenants` | `pages/nearle/clients/Tenants.js` | `Tenants` |
+| `/nearle/clients/create` | `pages/nearle/clients/createclient.js` | `Createclient` |
+| `/nearle/pricing` | `pages/nearle/clientPricing/clientPricing.js` | `ClientsPricing` |
+| `/nearle/customers` | `pages/nearle/customers/customers.js` | `Customers` |
+| `/nearle/customer/create` | `pages/nearle/clients/createCustomer.js` | `CreateCustomer` |
+| `/nearle/riders` | `pages/nearle/riders/riders.js` | `Riders` |
+| `/nearle/riders/create` | `pages/nearle/riders/createrider.js` | `Createrider` |
+| `/nearle/riders/edit` | `pages/nearle/riders/editRider.js` | `EditRider` |
+| `/nearle/invoice` | `pages/nearle/invoice/invoice.js` | `Invoice` |
+| `/nearle/invoice/preview` | `pages/nearle/invoice/invoicePreview.js` | `InvoicePreview` |
+| `/nearle/requests` | `pages/nearle/requests/requests.js` | `Requests` |
+| `/nearle/reports/orderssummary` | `pages/nearle/reports/ordersSummary.js` | `OrdersSummary` |
+| `/nearle/reports/ordersdetails` | `pages/nearle/reports/ordersDetails.js` | `OrdersDetails` |
+| `/nearle/reports/riderssummary` | `pages/nearle/reports/ridersSummary.js` | `RidersSummary` |
+| `/nearle/reports/riderslogs` | `pages/nearle/reports/ridersLogs.js` | `RidersLogs` |
+| `/viewprofile` | `pages/nearle/viewProfile.js` | `ViewProfile` |
+| `/maintenance/{404,500,under-construction,coming-soon}` | `pages/maintenance/*` | — |
+
+---
+
+## 6. Per-page state-flow primer
+
+```mermaid
+flowchart LR
+ subgraph PAGE [Any operator page]
+ UI[React component] --> QH["useQuery / useInfiniteQuery / useMutation"]
+ UI --> US["useState (local UI state)"]
+ end
+ QH -- "queryKey = [name, appId, filters...]" --> Cache["TanStack Query cache"]
+ QH -- queryFn --> ApiLayer["pages/api/api.js"]
+ ApiLayer --> AxRaw["axios (raw)"]
+ ApiLayer -. some calls .-> AxInt["utils/axios.js (401 interceptor)"]
+ AxRaw --> EnvURL["process.env.REACT_APP_URL"]
+ AxRaw --> EnvURL2["process.env.REACT_APP_URL2"]
+ AxRaw --> ExtOpt["routes / routemate / jupiter (hardcoded)"]
+ UI -. dispatches .-> RTK["Redux store"]
+ UI -. emits toast .-> Notistack
+ UI -. reads .-> LS["localStorage"]
+```
+
+**Conventions for new pages:**
+- Query keys must include every filter parameter so caching invalidates correctly.
+- Use `useInfiniteQuery` for paginated rows; auto-drain with an `IntersectionObserver` on a sentinel `
` at the bottom of the `TableContainer`. The canonical pattern is in `deliveries.js` (search for `useInfiniteQuery({` and the adjacent `IntersectionObserver` block).
+- Mutations: after success, call `.refetch()` on every related query. Project does not yet use `queryClient.invalidateQueries`.
+- Errors → `OpenToast(message, 'error', 2000)` from `components/third-party/OpenToast`.
+
+---
+
+## 7. FCM rules
+
+- Initialised once in `App.js` (`generateToken` + `initFirebaseNotificationListener`).
+- After any mutation affecting a rider (assign, cancel, change rider), call `POST /utils/notifyuser` with the target rider's `userfcmtoken`.
+- The service worker (`public/firebase-messaging-sw.js`) handles background tokens and desktop notifications. Subtle bugs there cause silent delivery failures — do not edit without a clear reason.
+
+---
+
+## 8. Glossary
+
+- **Zone / `applocationid`** — operator's warehouse / operational hub. `0` means "All Zones". Every list endpoint takes this.
+- **Tenant** — the business / brand whose orders flow through the platform.
+- **Location** — a tenant's branch within a zone.
+- **Rider / Partner** — the delivery driver.
+- **Batch / Wave** — a time-of-day slot (Morning 0–8, Afternoon 9–12, Evening 16–19). Bucketed by `assigntime` on the deliveries page in **local** time to match dispatch's bucketing.
+- **Slab** — a pricing tier (base + per-km + min-km + max-km + min-orders).
+- **Solver hypertuning** — the optimiser's strategy (Balanced, Fuel Saver, Aggressive, Strict Zones).
+
+---
+
+## 9. Cross-references
+
+- Project rules and conventions Claude must follow are in the root `CLAUDE.md`. Don't restate them here.
+- Page-level design system tokens (the `DT` block, `pillFieldSx`, `SoftPaper`, `AccentAvatar`) are documented in `CLAUDE.md` §6 — the implementation source-of-truth is `src/pages/nearle/deliveries/deliveries.js` lines 104–297.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..021bcad
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,302 @@
+# 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.
diff --git a/FLOW.md b/FLOW.md
new file mode 100644
index 0000000..b52ba66
--- /dev/null
+++ b/FLOW.md
@@ -0,0 +1,255 @@
+# NearlExpress Console — Repository Flow Graph
+
+> Human-readable navigation, action, and data-flow diagrams for `nearlexpress-xpressconsole-d0ee01adebe9`.
+> Open this file in a Mermaid-aware viewer (GitHub, VS Code with the *Markdown Preview Mermaid* extension, or [mermaid.live](https://mermaid.live)).
+>
+> The same diagrams (plus the per-endpoint table) are also available to Claude via the project-level skill at `.claude/skills/nearlexpress-docs/SKILL.md`. **This file is the human-facing copy** — kept at the root for quick GitHub browsing.
+
+---
+
+## 1. End-to-end console flow
+
+```mermaid
+flowchart TD
+ %% ============================ Styling ============================
+ classDef entry fill:#f9f6ff,stroke:#a78bfa,stroke-width:2px,color:#2e1065;
+ classDef auth fill:#fdf2f8,stroke:#ec4899,stroke-width:2px,color:#831843;
+ classDef nav fill:#eef2ff,stroke:#6366f1,stroke-width:2px,color:#312e81;
+ classDef core fill:#e0f2fe,stroke:#0ea5e9,stroke-width:2px,color:#0c4a6e;
+ classDef sub fill:#ecfeff,stroke:#06b6d4,stroke-width:1.5px,color:#155e75;
+ classDef action fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#78350f;
+ classDef solver fill:#fffbeb,stroke:#d97706,stroke-width:2px,color:#7c2d12;
+ classDef api fill:#f0fdf4,stroke:#10b981,stroke-width:1.5px,color:#064e3b;
+ classDef state fill:#fdf4ff,stroke:#8b5cf6,stroke-width:1.5px,color:#581c87;
+ classDef ext fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d;
+ classDef report fill:#f5f5f4,stroke:#78716c,stroke-width:1.5px,color:#292524;
+
+ %% ============================ Entry & Auth ============================
+ Boot([Browser load
src/index.js]):::entry
+ Providers["Providers wiring
Redux store · QueryClient · Router · ThemeCustomization · Notistack"]:::entry
+ App["src/App.js
localStorage('authname') gate"]:::auth
+ FCMInit["generateToken()
initFirebaseNotificationListener()"]:::auth
+ SW["public/firebase-messaging-sw.js
FCM Service Worker"]:::auth
+ Login[/"/login
pages/nearle/login1.js"/]:::auth
+
+ Boot --> Providers --> App
+ App -- no authname --> Login
+ Login -- POST /users/console/login
(jupiter.nearle.app) --> AuthOK{Auth OK?}
+ AuthOK -- yes, save authname/userid/roleid/userfcmtoken --> Layout
+ AuthOK -- no --> Login
+ App --> FCMInit --> SW
+
+ %% ============================ Main Layout & Sidebar ============================
+ Layout["layout/MainLayout
Sidebar + Header + Outlet"]:::nav
+ Sidebar["menu-items/nearle.js
(sidebar definition)"]:::nav
+
+ Layout --- Sidebar
+ Sidebar --> Dispatch
+ Sidebar --> Orders
+ Sidebar --> Deliveries
+ Sidebar --> Tenants
+ Sidebar --> Pricing
+ Sidebar --> Customers
+ Sidebar --> Riders
+ Sidebar --> Invoice
+ Sidebar --> ReportsHub
+ Sidebar --> ViewProfile
+
+ %% ============================ Top-level pages ============================
+ Dispatch[/"/nearle/dispatch
Live Map · Riders · Batches"/]:::core
+ Orders[/"/nearle/orders
Orders Dashboard"/]:::core
+ Deliveries[/"/nearle/deliveries
Dispatched Deliveries"/]:::core
+ Tenants[/"/nearle/tenants
Client/Tenant Management"/]:::core
+ Pricing[/"/nearle/pricing
Pricing Matrix (master-detail)"/]:::core
+ Customers[/"/nearle/customers
Customer Directory"/]:::core
+ Riders[/"/nearle/riders
Rider Pool"/]:::core
+ Invoice[/"/nearle/invoice
Billing"/]:::core
+ Requests[/"/nearle/requests
Expense Approvals"/]:::core
+ ReportsHub[/"/nearle/reports/*
BI Suite"/]:::core
+ ViewProfile[/"/viewprofile
Profile Manager"/]:::sub
+
+ %% ============================ Sub-routes ============================
+ OrdersCreate[/"/orders/create
Single Order Create"/]:::sub
+ OrdersMulti[/"/orders/createorders
Bulk Order Create"/]:::sub
+ OrdersDetails[/"/orders/details
Order Detail View"/]:::sub
+ OrdersPreview[/"/orders/preview
Optimised Preview"/]:::sub
+ DispatchPreview[/"/dispatch/preview
Dispatch Review & Adjust"/]:::sub
+ RidersCreate[/"/riders/create
Onboard Rider"/]:::sub
+ RidersEdit[/"/riders/edit
Edit Rider"/]:::sub
+ ClientsCreate[/"/clients/create
Onboard Tenant"/]:::sub
+ CustomerCreate[/"/customer/create
Onboard Customer"/]:::sub
+ InvoicePreview[/"/invoice/preview
Invoice Preview"/]:::sub
+ ReportsOS[/"/reports/orderssummary"/]:::report
+ ReportsOD[/"/reports/ordersdetails"/]:::report
+ ReportsRS[/"/reports/riderssummary"/]:::report
+ ReportsRL[/"/reports/riderslogs"/]:::report
+
+ Orders --> OrdersCreate
+ Orders --> OrdersMulti
+ Orders --> OrdersDetails
+ Riders --> RidersCreate
+ Riders --> RidersEdit
+ Tenants --> ClientsCreate
+ Customers --> CustomerCreate
+ Invoice --> InvoicePreview
+ ReportsHub --> ReportsOS
+ ReportsHub --> ReportsOD
+ ReportsHub --> ReportsRS
+ ReportsHub --> ReportsRL
+
+ %% ============================ Optimisation Pipeline ============================
+ SelectOrders["Select N pending orders
(checkbox column)"]:::action
+ ChooseSolver{"Choose Dispatch Mode"}:::solver
+ SolverBike["Mode 1 · Bike
POST routes.workolik.com
/optimization/riderassign"]:::ext
+ SolverAuto["Mode 2 · Auto
POST routemate.workolik.com
/optimization/riderassign"]:::ext
+ SolverManual["Mode 0 · Manual
POST routes.workolik.com
/optimization/createdeliveries"]:::ext
+ Reconcile["POST routes.workolik.com
/optimization/reconcile-steps"]:::ext
+ FinalAssign["POST jupiter.nearle.app
/deliveries/createdeliveries"]:::ext
+ NotifyRider["POST /utils/notifyuser
(FCM push to rider)"]:::ext
+
+ Orders --> SelectOrders --> ChooseSolver
+ ChooseSolver -- Bike --> SolverBike
+ ChooseSolver -- Auto --> SolverAuto
+ ChooseSolver -- Manual --> SolverManual
+ SolverBike --> OrdersPreview
+ SolverAuto --> OrdersPreview
+ SolverManual --> OrdersPreview
+ OrdersPreview --> DispatchPreview
+ DispatchPreview -. drag-and-drop / change rider .-> Reconcile
+ Reconcile --> DispatchPreview
+ DispatchPreview -- Assign --> FinalAssign
+ FinalAssign --> NotifyRider
+ FinalAssign -- redirect --> Deliveries
+
+ %% ============================ Deliveries actions ============================
+ DChangeRider["Change Rider"]:::action
+ DCancel["Cancel Delivery"]:::action
+ DUpdate["Update Amount / Notes"]:::action
+
+ Deliveries --> DChangeRider --> NotifyRider
+ Deliveries --> DCancel --> NotifyRider
+ Deliveries --> DUpdate
+ Deliveries -. row expand .-> OrderItems["Order items detail
GET /orders/getorderdetails"]:::api
+
+ %% ============================ Dispatch (live) ============================
+ DispatchMap["Live Leaflet map
Riders · Routes · Batches"]:::action
+ RiderLogs["GET /utils/getriderperiodiclogs
(polled)"]:::api
+ BatchEff["POST routes.workolik.com
/batch/efficiency"]:::ext
+
+ Dispatch --> DispatchMap
+ Dispatch --> RiderLogs
+ Dispatch --> BatchEff
+ Dispatch -. drilldown .-> DispatchPreview
+
+ %% ============================ Tenants / Pricing ============================
+ TenantTabs["Tabs: Active · Pending · Inactive"]:::action
+ TenantView["Row → View (collapse)"]:::action
+ TenantEdit["Row → Edit (collapse)"]:::action
+ TenantApprove["Approve Pending
+ Set Pricing Dialog"]:::action
+
+ Tenants --> TenantTabs
+ Tenants --> TenantView
+ Tenants --> TenantEdit
+ Tenants --> TenantApprove
+
+ PricingMaster["Master: Location List"]:::action
+ PricingDetail["Detail: Slabs (StatChips)"]:::action
+ Pricing --> PricingMaster --> PricingDetail
+
+ %% ============================ Cross-cutting concerns ============================
+ subgraph CROSS [Cross-cutting infrastructure]
+ direction LR
+ Store["Redux Toolkit store
fcmSlice · loginUserSlice
menu · snackbar · toastSlice · auth"]:::state
+ QC["@tanstack/react-query
QueryClient (cache + infinite scroll)"]:::state
+ AxiosBase["axios (raw) +
utils/axios.js (401 → /login)"]:::state
+ Env["env vars: REACT_APP_URL ·
REACT_APP_URL2 · REACT_APP_GOOGLE_MAPS_API_KEY"]:::state
+ LS["localStorage:
authname · userid · roleid ·
userfcmtoken · applocations"]:::state
+ Toasts["Notistack · OpenToast wrapper"]:::state
+ end
+
+ Layout -. reads .-> Store
+ Layout -. reads .-> LS
+ Layout -. uses .-> QC
+ Orders & Deliveries & Dispatch & Tenants & Pricing & Customers & Riders & Invoice & ReportsHub -. fetches via .-> QC
+ QC -. HTTP .-> AxiosBase
+ AxiosBase -. base URLs from .-> Env
+ DChangeRider & DCancel & DUpdate -. updates --> QC
+ Toasts -. consumed by .-> Sidebar
+```
+
+---
+
+## 2. Optimisation pipeline (sequence)
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant U as Operator
+ participant O as /nearle/orders
+ participant S as Solver (routes/routemate.workolik.com)
+ participant P as /nearle/dispatch/preview
+ participant R as reconcile-steps
+ participant J as jupiter.nearle.app
+ participant D as /nearle/deliveries
+ participant F as FCM (rider device)
+
+ U->>O: Select pending orders (checkbox)
+ U->>O: Pick dispatch mode (Bike / Auto / Manual)
+ O->>S: POST riderassign or createdeliveries
+ S-->>O: Optimised route & assignments
+ O->>P: Redirect to preview with payload
+ U->>P: Drag steps / swap rider (optional)
+ P->>R: POST reconcile-steps (after each manual edit)
+ R-->>P: Updated, validated sequence
+ U->>P: Confirm "Assign"
+ P->>J: POST /deliveries/createdeliveries
+ J-->>P: 200 OK, deliveries persisted
+ P->>F: POST /utils/notifyuser (per rider)
+ F-->>R: (out-of-band ack)
+ P->>D: Redirect to deliveries dashboard
+```
+
+---
+
+## 3. State & data flow (per page)
+
+```mermaid
+flowchart LR
+ subgraph PAGE [Any operator page]
+ UI[React component] --> QH["useQuery / useInfiniteQuery / useMutation"]
+ UI --> US["useState (local UI state)"]
+ end
+
+ QH -- "queryKey = [name, appId, filters...]" --> Cache["TanStack Query cache"]
+ QH -- queryFn --> ApiLayer["pages/api/api.js
(named async fns)"]
+ ApiLayer --> AxRaw["axios (raw)"]
+ ApiLayer -. some calls .-> AxInt["utils/axios.js
(401 interceptor)"]
+ AxRaw --> EnvURL["process.env.REACT_APP_URL"]
+ AxRaw --> EnvURL2["process.env.REACT_APP_URL2"]
+ AxRaw --> ExtOpt["routes / routemate / jupiter (hardcoded)"]
+
+ UI -. dispatches .-> RTK["Redux store (fcm · login · menu · snackbar · toast · auth)"]
+ UI -. emits toast .-> Notistack
+ UI -. reads .-> LS["localStorage"]
+
+ Notistack -. renders .-> Header[Top-right toasts]
+ RTK -. read by .-> Layout[MainLayout / Sidebar]
+```
+
+---
+
+## 4. Reading the graph
+
+- **Solid arrow** = direct call / navigation.
+- **Dotted arrow** = read-only reference, polling, or async side-effect (FCM, cache, toast).
+- **Cluster colours**:
+ - Purple = entry/auth
+ - Indigo = navigation frame
+ - Cyan/Sky = top-level pages
+ - Soft cyan = sub-routes
+ - Amber = user-initiated actions
+ - Red = external service (workolik, jupiter, Firebase)
+ - Emerald = console-internal API/data
+ - Violet = cross-cutting infra
+
+For the full per-endpoint table with payloads and use cases (Authentication, Orders, Deliveries, Tenants, Pricing, Customers, Riders, Invoices, Dispatch, Reports, Requests), see `.claude/skills/nearlexpress-docs/SKILL.md`.
diff --git a/src/components/nearle_components/CLAUDE.md b/src/components/nearle_components/CLAUDE.md
new file mode 100644
index 0000000..fc347f8
--- /dev/null
+++ b/src/components/nearle_components/CLAUDE.md
@@ -0,0 +1,112 @@
+# CLAUDE.md — `src/components/nearle_components/`
+
+Rules for editing the shared NearlExpress UI primitives.
+
+These components are imported across every page. Changes here have **fan-out impact** — measure twice, cut once.
+
+---
+
+## 1. What lives here
+
+| Component | Use | Status |
+|---|---|---|
+| `LocationAutocomplete.js` | Zone picker on every operator page | ✅ Canonical — has `pill` variant matching DT system |
+| `DebounceSearchBar.js` | 500ms-debounced search with ⌘/Ctrl+K focus | ✅ Canonical |
+| `LoaderWithImage.js` | Inline branded spinner for "loading more" rows | ✅ Canonical |
+| `GlobalToast.js` | Global toast wrapper | ✅ Used at root |
+| `SearchBar.js` | Non-debounced search input | Legacy — prefer `DebounceSearchBar` |
+| `TableLoader.js` | Inline table loading state | Legacy — prefer `OrdersTableSkeleton` |
+| `TitleCard.js` | Old page header | ⛔ **Legacy** — do not use on new pages. The replacement is the gradient `` header pattern documented in root `CLAUDE.md` §6, implemented in `deliveries.js` (search for the comment `Header` near the first `` in the JSX `return`). |
+
+---
+
+## 2. Design system token discipline
+
+The `DT` design tokens (palette, alpha helpers, `pillFieldSx`, `SoftPaper`, `AccentAvatar`) are documented in the root `CLAUDE.md` §6 and the source-of-truth implementation is the token block near the top of `src/pages/nearle/deliveries/deliveries.js` (search for `const DT = {`).
+
+**Hard rules when editing components here:**
+
+- **Universal brand colour is `#662582`** (NearlExpress purple — from `themes/theme/default.js` `primary.main`). Every brand surface (page header, dialog header, KPI primary tile, search bars, edit-action buttons, scrollbars) uses this. Gradient pair: `#662582 → #9255AB`.
+- **Semantic status palette is distinct** from the brand. Use these for lifecycle indicators only: sky `#0ea5e9`, emerald `#10b981`, amber `#f59e0b`, red `#ef4444`, status-purple `#8b5cf6` (lighter than brand), cyan `#06b6d4`, teal `#14b8a6`, orange `#f97316`, muted `#94a3b8`, indigo `#6366f1` (Accepted status). Don't replace these with brand purple — operators colour-code on them.
+- Don't introduce a colour from `theme.palette` for new surfaces — those are Mantis defaults and don't match the DT system. Use the hex values above directly.
+- Border radii: `12` (inner), `16` (card), `999` (pill). No other values.
+- Shadows: `DT.shadowSoft` / `DT.shadowMd` / `DT.shadowPop`. No raw `box-shadow` strings.
+
+> Some existing redesigned pages (`deliveries.js`, `Tenants.js`, `clientPricing.js`) still use `#6366f1` as the brand accent — this is a legacy from the first design pass. `customers.js` and the `createorder1` Saved-Address dialog are already on `#662582`. When you next edit one of the legacy pages, migrate it to brand purple in the same PR.
+
+---
+
+## 3. `LocationAutocomplete` — the `pill` prop is opt-in
+
+The component supports two visual modes, controlled by the `pill` prop:
+
+```jsx
+// Default (legacy, "Select Zones" label, outlined TextField) — used by old pages
+
+
+// Pill variant — used by all redesigned pages (deliveries, tenants, pricing, customers)
+}
+ placeholder="Select Zone"
+ paperComponent={SoftPaper}
+ setAppId={...}
+ setLocoName={...}
+/>
+```
+
+- **For any new page:** use `pill`. Always.
+- **For existing legacy pages (orders, invoice, riders, etc.):** keep the default until that page is redesigned. Don't change them piecemeal.
+- The `accentColor` defaults to `#6366f1` — only override when the page's accent is different (rare).
+
+---
+
+## 4. `DebounceSearchBar` — debounce + ⌘/Ctrl+K shortcut
+
+```jsx
+
+```
+
+- **Don't lower the debounce below 500ms.** TanStack Query's cache keys include `debouncedSearch`, so each keystroke would cause a new server hit if the debounce drops.
+- ⌘/Ctrl+K to focus and `Esc` to blur are wired globally inside this component. Don't add competing keyboard shortcuts at the page level using the same keys.
+- The pill aesthetic (rounded `999`, tinted bg, accent border) is applied via the `sx` prop at the call site — see the ``). Don't add a component that's a single instance of a styled `Paper`.
+
+---
+
+## 6. When to inline instead
+
+The `DT` token block at the top of each page is repeated intentionally. Don't extract it into a shared module here. Reasons:
+
+- It's a 30-line block of constants — repetition is fine.
+- Pages occasionally tweak a token for their own use (e.g. picking different KPI colours). A shared module would lock everyone into the same defaults.
+- Importing from a shared file creates a cross-page coupling that's painful when one page wants to evolve its visual language.
+
+Same logic for `SoftPaper`, `AccentAvatar`, `pillFieldSx` — these are 5–20 line helpers that live inside each page file. Don't extract.
+
+---
+
+## 7. Backwards compatibility
+
+If you change a component's public prop signature:
+- Old call sites still using the old prop name will break silently (React doesn't warn on unknown props).
+- Either: keep the old prop name working (alias both), OR grep `src/pages/` for all consumers and update them in the same PR.
+
+The component's `forwardRef` shape is part of the contract — `Tenants.js` uses `tenantRef` / `locationRef` to imperatively focus inputs. Don't break ref-forwarding.
diff --git a/src/components/nearle_components/LocationAutocomplete.js b/src/components/nearle_components/LocationAutocomplete.js
index 0468768..dc8eaca 100644
--- a/src/components/nearle_components/LocationAutocomplete.js
+++ b/src/components/nearle_components/LocationAutocomplete.js
@@ -1,51 +1,135 @@
import React, { forwardRef, useEffect, useState } from 'react';
-import { Autocomplete, TextField } from '@mui/material';
+
+import { Autocomplete, TextField, Avatar, Stack } from '@mui/material';
import axios from 'axios';
+import { MdMyLocation } from 'react-icons/md';
-const LocationAutocomplete = forwardRef(({ setAppId, setLocoName, setPage, sx, textfeildSx }, ref) => {
- const [locations, setLocations] = useState(JSON.parse(localStorage.getItem('applocations') || '[]'));
+// Pill variant — opt-in via `pill` prop. Mirrors the design used across the
+// deliveries / dispatch filter rows (rounded pill, soft tinted bg, accent
+// border + focus ring, optional leading avatar icon). The default
+// (`pill={false}` or omitted) renders the original outlined TextField with
+// label "Select Zones" so existing callers are not affected.
+const LocationAutocomplete = forwardRef(
+ (
+ {
+ setAppId,
+ setLocoName,
+ setPage,
+ sx,
+ textfeildSx,
+ textfieldSx,
+ pill = false,
+ accentColor = '#6366f1',
+ icon,
+ placeholder = 'Select Zone',
+ paperComponent
+ },
+ ref
+ ) => {
+ const [locations, setLocations] = useState(JSON.parse(localStorage.getItem('applocations') || '[]'));
- useEffect(() => {
- const fetchLocations = async () => {
- try {
- const userid = localStorage.getItem('userid');
- if (!userid) return;
- const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
- if (response.data.status) {
- const updatedLocations = [...response.data.details, { locationname: 'All', applocationid: 0 }];
- localStorage.setItem('applocations', JSON.stringify(updatedLocations));
- setLocations(updatedLocations);
+ useEffect(() => {
+ const fetchLocations = async () => {
+ try {
+ const userid = localStorage.getItem('userid');
+ if (!userid) return;
+ const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
+ if (response.data.status) {
+ const updatedLocations = [...response.data.details, { locationname: 'All', applocationid: 0 }];
+ localStorage.setItem('applocations', JSON.stringify(updatedLocations));
+ setLocations(updatedLocations);
+ }
+ } catch (err) {
+ console.error('Error fetching locations in LocationAutocomplete:', err);
}
- } catch (err) {
- console.error('Error fetching locations in LocationAutocomplete:', err);
+ };
+
+ if (locations.length === 0) {
+ fetchLocations();
}
- };
+ }, [locations.length]);
- if (locations.length === 0) {
- fetchLocations();
- }
- }, [locations.length]);
+ // Helpers (only used by pill variant) — match the deliveries page's
+ // token shorthand so the same opacity ramp is applied here.
+ const a = (suffix) => `${accentColor}${suffix}`;
+ const tint = a('08');
+ const ring = a('26');
+ const edge = a('55');
+ const soft = a('18');
- return (
- option?.locationname ?? ''}
- sx={{ ...sx }}
- onChange={(event, value, reason) => {
- if (reason === 'clear') {
- setAppId?.(0);
- setLocoName?.('');
- setPage?.(0);
- } else if (value) {
- setAppId?.(value.applocationid);
- setLocoName?.(value.locationname);
- setPage?.(0);
+ const pillSx = pill
+ ? {
+ cursor: 'pointer',
+ '& .MuiOutlinedInput-root': {
+ borderRadius: '999px',
+ bgcolor: tint,
+ fontWeight: 600,
+ color: '#0f172a',
+ paddingRight: '8px',
+ cursor: 'pointer',
+ transition: 'border-color 0.15s, box-shadow 0.15s, background-color 0.2s',
+ '& fieldset': { borderColor: edge, borderWidth: 1.5 },
+ '&:hover fieldset': { borderColor: accentColor },
+ '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring}` },
+ '&.Mui-focused fieldset': { borderColor: accentColor, borderWidth: 2 }
+ },
+ '& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: accentColor }
}
- }}
- renderInput={(params) => }
- />
- );
-});
+ : {};
+
+ const Adornment = pill && (
+
+
+ {icon || }
+
+
+ );
+
+ return (
+ option?.locationname ?? ''}
+ PaperComponent={paperComponent}
+ sx={{ ...sx }}
+ onChange={(event, value, reason) => {
+ if (reason === 'clear') {
+ setAppId?.(0);
+ setLocoName?.('');
+ setPage?.(0);
+ } else if (value) {
+ setAppId?.(value.applocationid);
+ setLocoName?.(value.locationname);
+ setPage?.(0);
+ }
+ }}
+ renderInput={(params) =>
+ pill ? (
+
+ ) : (
+
+ )
+ }
+ />
+ );
+ }
+);
export default LocationAutocomplete;
diff --git a/src/pages/api/CLAUDE.md b/src/pages/api/CLAUDE.md
new file mode 100644
index 0000000..3a835db
--- /dev/null
+++ b/src/pages/api/CLAUDE.md
@@ -0,0 +1,113 @@
+# CLAUDE.md — `src/pages/api/`
+
+Rules for editing `api.js`. This is the **central API layer** — every page calls into it. The root `CLAUDE.md` covers project-wide conventions; this file is the scoped rule sheet for the API layer specifically.
+
+---
+
+## 1. Function signature patterns
+
+### TanStack `useQuery` / `useInfiniteQuery` consumers
+Destructure from `queryKey` in the order the call site declared it. The leading `_` is the query name and is intentionally discarded.
+
+```js
+// Plain query — destructure { queryKey }, skip the [0] name slot
+export const fetchorderscount = async ({ queryKey }) => {
+ const [, appId, startdate, enddate, currentStatus, tenantid, locationid] = queryKey;
+ const url = `${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&...`;
+ const response = await axios.get(url);
+ return response.data.details;
+};
+
+// Infinite query — also receives pageParam (default to 1)
+export const fetchOrders = async ({ pageParam = 1, queryKey }) => {
+ const [, appId, currentStatus, debouncedSearch, startdate, enddate, rowsPerPage, tenantid, locationid] = queryKey;
+ const url = `${process.env.REACT_APP_URL}/orders/tenant/getorders/?applocationid=${appId}&...&pageno=${pageParam}&pagesize=${rowsPerPage}`;
+ const response = await axios.get(url);
+ return {
+ rows: response.data.details,
+ nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined
+ };
+};
+```
+
+**Hard rules:**
+- The query-key array order at the call site MUST match the destructure order here. Re-ordering one without the other silently breaks every caller.
+- Infinite queries return `{ rows, nextPage }`. `nextPage` is `undefined` when the page size wasn't filled (signals end-of-stream to `getNextPageParam`).
+- Some legacy functions return `{ data, nextPage }` instead of `{ rows, nextPage }` (e.g. `getallcustomers`). Match the existing shape rather than "fixing" it — call sites depend on the field name.
+
+---
+
+## 2. Direct positional-argument calls
+A few functions take plain positional arguments instead of `queryKey` — usually when they're invoked from a `useMutation` or imperatively. Example: `getTenants(appId)`, `gettenantlocations(tenantid)`.
+
+Pick the signature based on how the function is called:
+- Called via `useQuery({ queryFn: fn })` → destructure `{ queryKey }`.
+- Called via `useQuery({ queryFn: () => fn(arg) })` → take positional args.
+- Called from a mutation or imperatively → take positional args.
+
+---
+
+## 3. Base URL selection
+
+| Use base | When |
+|---|---|
+| `process.env.REACT_APP_URL` | Default for ~95% of endpoints |
+| `process.env.REACT_APP_URL2` | `/users/update`, `/tenants/update`, `/tenants/update/services`, archival `/orders/getorders`, `/partners/getriderlogs` |
+| Hardcoded `https://routes.workolik.com` | Bike solver + reconcile-steps + batch efficiency |
+| Hardcoded `https://routemate.workolik.com` | Auto / multi-trip solver |
+| Hardcoded `https://jupiter.nearle.app` | Login + final `/deliveries/createdeliveries` commit |
+
+When adding a new endpoint, check whether the backend actually serves it on URL or URL2 — don't guess. URL2 lives on a separate service.
+
+---
+
+## 4. Error handling pattern
+
+```js
+try {
+ const response = await axios.get(`${process.env.REACT_APP_URL}/...`);
+ return response.data.details; // or .summary, .data, etc — depends on backend shape
+} catch (err) {
+ const message = err.response?.data?.message || err.message || 'Something went wrong';
+ OpenToast(message);
+ return null; // or return [] / {} — match what the caller expects
+}
+```
+
+- Toast on failure via `OpenToast` from `components/third-party/OpenToast`. Don't `throw` — TanStack Query's `onError` is rarely wired by callers.
+- Return a sensible empty default (`null`, `[]`, `{}`) so the call site's destructuring doesn't crash.
+- Don't `console.log(err)` AND toast — toast is enough. Some legacy functions do both; new functions should not.
+
+---
+
+## 5. When NOT to add to `api.js`
+
+The user has tolerated a number of pages making direct inline `axios.put` / `axios.post` calls for mutations (e.g. `Tenants.js` calls `axios.put('/tenants/update')` inline). Match the surrounding file:
+
+- **Adding a new shared GET** → put it in `api.js`.
+- **Adding a one-off mutation used in only one page** → tolerated inline in that page; doesn't need a new export here.
+- **Adding a polling endpoint used by multiple pages** → put it here as a named export so the query cache keys are coherent.
+
+---
+
+## 6. Response shape quirks
+
+Backend response shapes are inconsistent. Don't assume `response.data.details` — check what the specific endpoint returns:
+
+| Backend field | Used by |
+|---|---|
+| `response.data.details` | Most list endpoints |
+| `response.data.summary` | `getcustomersummary`, `gettenantsummary`, `getpricinglist` summary calls |
+| `response.data.data` | `getRiderPeriodicLogs`, `getallcustomers` infinite-query page payload |
+| `response.data.message` | Mutation success messages (for toast) |
+| `response.data.status` | Boolean success flag — check before reading `.details` on some endpoints |
+
+When unsure, log the response once during dev and pick the matching field.
+
+---
+
+## 7. Things that look broken but are intentional
+
+- `const userid = localStorage.getItem('userid');` at module top — read once at module load, intentional for the `fetchAppLocations` helper. Don't move it inside the function.
+- Some functions take a `pageno` 0-indexed and others 1-indexed (`pageno: pageParam + 1` vs `pageno: pageParam`). Backend inconsistency — leave it alone unless you confirm the backend side.
+- A few legacy commented-out function bodies are kept above their current implementation as historical reference. Don't delete them in a drive-by edit.
diff --git a/src/pages/api/api.js b/src/pages/api/api.js
index 988720f..8361e6a 100644
--- a/src/pages/api/api.js
+++ b/src/pages/api/api.js
@@ -406,6 +406,20 @@ export const getpricinglist = async ({ queryKey }) => {
return null; // return null for failure
}
};
+// ==============================|| getallpricing (clientPricing) ||============================== //
+
+export const getallpricing = async ({ queryKey }) => {
+ const [, appId] = queryKey;
+ try {
+ const response = await axios.get(`${process.env.REACT_APP_URL}/utils/getallpricing/?applocationid=${appId}`);
+ return response.data.details || [];
+ } catch (err) {
+ const message = err.response?.data?.message || err.message || 'Something went wrong';
+ OpenToast(message);
+ return [];
+ }
+};
+
// ==============================|| getcustomersummary (customers) ||============================== //
export const getcustomersummary = async ({ queryKey }) => {
diff --git a/src/pages/nearle/clientPricing/clientPricing.js b/src/pages/nearle/clientPricing/clientPricing.js
index 1df45fb..1307f93 100644
--- a/src/pages/nearle/clientPricing/clientPricing.js
+++ b/src/pages/nearle/clientPricing/clientPricing.js
@@ -1,130 +1,603 @@
-import React, { useEffect, useState } from 'react';
-import axios from 'axios';
-import { Stack, TableContainer, Table, TableHead, TableRow, TableCell, TableBody, Chip } from '@mui/material';
-import MainCard from 'components/MainCard';
+import React, { useMemo, useRef, useState } from 'react';
+import {
+ Avatar,
+ Box,
+ Grid,
+ Paper,
+ Stack,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Typography
+} from '@mui/material';
+import {
+ MdLocalOffer,
+ MdMyLocation,
+ MdAttachMoney,
+ MdGroups,
+ MdPlace,
+ MdSpeed,
+ MdPriceCheck,
+ MdStraighten,
+ MdReceiptLong
+} from 'react-icons/md';
+import { useQuery } from '@tanstack/react-query';
+
import Loader from 'components/Loader';
-import TitleCard from 'components/nearle_components/TitleCard';
+import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
-import { Empty } from 'antd';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
+import { getallpricing } from 'pages/api/api';
+
+// ============================================================================
+// Design tokens — shared with the deliveries / tenants / customers pages so every
+// surface (header, KPI tiles, table, badges) speaks the same visual language.
+// Keep this block in sync with customers.js / deliveries.js.
+// ============================================================================
+const DT = {
+ radiusPill: 999,
+ radiusCard: 16,
+ 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'
+};
+const a = (c, suffix) => `${c}${suffix}`;
+const tint = (c) => a(c, '08');
+const soft = (c) => a(c, '18');
+const ring = (c) => a(c, '26');
+const edge = (c) => a(c, '55');
+
+const BRAND = '#662582';
+const BRAND_LIGHT = '#9255AB';
+
+const SoftPaper = (props) => (
+
+);
+
+const AccentAvatar = ({ color, selected, size = 24, children }) => (
+
+ {children}
+
+);
+
+const formatRupees = (value) =>
+ new Intl.NumberFormat('en-IN', {
+ style: 'currency',
+ currency: 'INR',
+ minimumFractionDigits: 2
+ }).format(Number(value) || 0);
+
+const formatDecimal = (value) =>
+ new Intl.NumberFormat('en-IN', { minimumFractionDigits: 2 }).format(Number(value) || 0);
+
+// Soft pill for metric cells in the table.
+const MetricPill = ({ color, icon, label, width }) => (
+
+ {icon}
+ {label}
+
+);
+
+// ==============================|| Pricing page ||============================== //
-// ==============================|| Starts here ||============================== //
const ClientsPricing = () => {
+ const containerRef = useRef();
const [appId, setAppId] = useState(0);
- const [locaName, setLocoName] = useState('');
- const [pricing, setpricing] = useState([]);
- const [isLoader, setIsloader] = useState([]);
+ const [locaName, setLocoName] = useState('All');
+ const [searchword, setSearchword] = useState('');
+ const [debouncedSearch, setDebouncedSearch] = useState('');
- // ==============================|| formatNumberToRupees ||============================== //
- function formatNumberToRupees(value) {
- return new Intl.NumberFormat('en-IN', {
- style: 'currency',
- currency: 'INR',
- minimumFractionDigits: 2
- }).format(value);
- }
- function dotzerozero(value) {
- return new Intl.NumberFormat('en-IN', {
- minimumFractionDigits: 2
- }).format(value);
- }
+ const {
+ data: pricing = [],
+ isLoading
+ } = useQuery({
+ queryKey: ['getallpricing', appId],
+ queryFn: getallpricing,
+ keepPreviousData: true
+ });
- // ==============================|| getAllPricing ||============================== //
- const getAllPricing = async () => {
- setIsloader(true);
- try {
- const pricingres = await axios.get(`${process.env.REACT_APP_URL}/utils/getallpricing/?applocationid=${appId}`);
- console.log('pricingres', pricingres.data.details);
- setpricing(pricingres.data.details);
- setIsloader(false);
- } catch (err) {
- console.log('pricingres', err);
- }
- };
- useEffect(() => {
- getAllPricing();
- }, [appId]);
+ const rows = useMemo(() => {
+ if (!debouncedSearch) return pricing;
+ const q = debouncedSearch.toLowerCase().trim();
+ return pricing.filter((row) =>
+ [row.applocation, row.appname, row.slab, String(row.pricingid)]
+ .filter(Boolean)
+ .some((field) => String(field).toLowerCase().includes(q))
+ );
+ }, [pricing, debouncedSearch]);
+
+ const stats = useMemo(() => {
+ const total = pricing.length;
+ const tenants = new Set(pricing.map((r) => r.appname).filter(Boolean)).size;
+ const avgBase = total
+ ? pricing.reduce((sum, r) => sum + (Number(r.baseprice) || 0), 0) / total
+ : 0;
+ return { total, tenants, avgBase };
+ }, [pricing]);
+
+ const KPI_META = [
+ { key: 'total', label: 'Total Pricing Slabs', color: BRAND, icon: MdLocalOffer, value: stats.total },
+ { key: 'tenants', label: 'Tenants Priced', color: '#0ea5e9', icon: MdGroups, value: stats.tenants },
+ { key: 'avg', label: 'Avg Base Price', color: '#f59e0b', icon: MdAttachMoney, value: formatRupees(stats.avgBase) },
+ { key: 'zone', label: 'Active Zone', color: '#10b981', icon: MdPlace, value: locaName || 'All Zones' }
+ ];
return (
<>
- {isLoader && }
- {/* ============================================= || Title Card || ============================================= */}
-
-
-
+ {isLoading && }
+
+ {/* ============================================= || Header || ============================================= */}
+
+
+
+
+
+
+
+
+ Pricing
+
+
+
+
+ Live · {locaName || 'All Zones'}
+
+
+
+
}
+ placeholder="Select Zone"
+ paperComponent={SoftPaper}
+ sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
/>
-
- {/* ============================================= || table || ============================================= */}
-
-
-
-
-
- S.No
- Location
- Pricing Id
- Name
+
+
+ {/* ============================================= || KPI Cards || ============================================= */}
+
+ {KPI_META.map((item) => {
+ const Icon = item.icon;
+ return (
+
+
+
+
+
+
+ {item.label}
+
+
+ {item.value}
+
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ {/* ============================================= || Search Header || ============================================= */}
+
+
+
+
+
+
+
+
+ Pricing Catalog
+
+
+ {pricing.length} total · {rows.length} shown
+
+
+
+
+
+
+
+
+
+ {/* ============================================= || Table || ============================================= */}
+
+
+
+
+
+ #
+ Tenant
+ Zone
Slab
Base Price
- MinKm
- Price/Km
- MaxKm
- Min Orders
+ Min KM
+ Price / KM
+ Max KM
+ Min Orders
+
- {isLoader && }
- {pricing?.length === 0 && !isLoader ? (
+ {isLoading && }
+ {rows.length === 0 && !isLoading ? (
-
-
+
+
+
+
+
+
+ No pricing to show
+
+
+ {searchword ? 'Try a different keyword.' : 'Pick a zone above to load the catalog.'}
+
+
) : (
- pricing?.map((data, index) => (
-
- {index + 1}
-
- {data.applocation}
-
-
-
+ rows.map((row, index) => (
+
+
+
+ {String(index + 1).padStart(2, '0')}
+
- {data.appname}
+
+
+
+
+
+
+
+ {row.appname || '—'}
+
+
+ ID #{row.pricingid}
+
+
+
+
- {data.slab}
+
+ {row.applocation ? (
+
+ {row.applocation}
+
+ ) : (
+ —
+ )}
+
-
-
+
+
+ {row.slab || '—'}
+
-
+ }
+ label={formatRupees(row.baseprice)}
+ width={110}
+ />
-
+ }
+ label={`${formatDecimal(row.minkm)} km`}
+ width={90}
+ />
-
+ }
+ label={formatRupees(row.priceperkm)}
+ width={110}
+ />
- {data.minorder}
+
+ }
+ label={`${formatDecimal(row.maxkm)} km`}
+ width={90}
+ />
+
+
+
+
+
+
+ {row.minorder ?? '—'}
+
+
+
))
)}
-
+
>
);
};
diff --git a/src/pages/nearle/clients/Tenants.js b/src/pages/nearle/clients/Tenants.js
index 03fa425..1fd8c5a 100644
--- a/src/pages/nearle/clients/Tenants.js
+++ b/src/pages/nearle/clients/Tenants.js
@@ -1,5 +1,4 @@
-import React, { useEffect, useState, useRef, useMemo } from 'react';
-import TitleCard from 'components/nearle_components/TitleCard';
+import React, { useEffect, useState, useRef, useMemo, Fragment } from 'react';
import MainCard from 'components/MainCard';
import axios from 'axios';
import { useTheme } from '@mui/material/styles';
@@ -40,26 +39,32 @@ import {
DialogTitle,
FormLabel,
TablePagination,
- Chip,
- Skeleton
+ Skeleton,
+ Avatar,
+ Paper
} from '@mui/material';
import {
EyeOutlined,
EyeInvisibleOutlined,
EditOutlined,
IssuesCloseOutlined,
- StopOutlined
+ StopOutlined,
+ CloseOutlined
} from '@ant-design/icons';
import { PopupTransition } from 'components/@extended/Transitions';
-import { FaRegCheckCircle } from 'react-icons/fa';
-import { TbBrandDatabricks } from 'react-icons/tb';
-import { GiMoneyStack } from 'react-icons/gi';
-import { FiUser } from 'react-icons/fi';
-import { FaRegAddressCard } from 'react-icons/fa';
-import { GiModernCity } from 'react-icons/gi';
-import { MdNumbers } from 'react-icons/md';
-import { TbWorldLongitude } from 'react-icons/tb';
-import { FiPhoneCall } from 'react-icons/fi';
+import { FaRegCheckCircle, FaRegAddressCard } from 'react-icons/fa';
+import { TbBrandDatabricks, TbWorldLongitude, TbWorldLatitude } from 'react-icons/tb';
+import { GiMoneyStack, GiModernCity } from 'react-icons/gi';
+import { FiUser, FiPhoneCall } from 'react-icons/fi';
+import {
+ MdNumbers,
+ MdGroups,
+ MdCheckCircle,
+ MdHourglassEmpty,
+ MdCancel,
+ MdMyLocation,
+ MdPersonPin
+} from 'react-icons/md';
import { LuMail } from 'react-icons/lu';
import { BiUser } from 'react-icons/bi';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
@@ -68,6 +73,97 @@ import { useQuery } from '@tanstack/react-query';
import { getalltenants, gettenantsummary } from 'pages/api/api';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
+// ============================================================================
+// Design tokens — shared with the deliveries page so every surface (header,
+// filters, KPI tiles, tabs, badges, dialogs) speaks the same visual language.
+// ============================================================================
+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'
+};
+
+const a = (c, suffix) => `${c}${suffix}`;
+const tint = (c) => a(c, '08');
+const soft = (c) => a(c, '18');
+const ring = (c) => a(c, '26');
+const edge = (c) => a(c, '55');
+
+const pillFieldSx = (color) => ({
+ cursor: 'pointer',
+ '& .MuiOutlinedInput-root': {
+ borderRadius: DT.radiusPill + 'px',
+ bgcolor: tint(color),
+ fontWeight: 600,
+ color: DT.textPrimary,
+ paddingRight: '8px',
+ cursor: 'pointer',
+ transition: 'border-color 0.15s, box-shadow 0.15s, background-color 0.2s',
+ '& 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 }
+ },
+ '& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: color }
+});
+
+// Status palette — drives the pill tabs and per-row badges.
+const STATUS_META = {
+ active: { label: 'Active', color: '#10b981', icon: MdCheckCircle, statusKey: 'active' },
+ pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty, statusKey: 'pending' },
+ inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel, statusKey: 'inactive' }
+};
+
+const STATUS_TABS = [
+ { status: 'active', countKey: 'active', tabStatus: 'Active' },
+ { status: 'pending', countKey: 'pending', tabStatus: 'Pending' },
+ { status: 'inactive', countKey: 'inactive', tabStatus: 'InActive' }
+];
+
+const KPI_META = [
+ { key: 'active', label: 'Active Tenants', color: '#10b981', icon: MdCheckCircle, countKey: 'active' },
+ { key: 'pending', label: 'Pending Approval', color: '#f59e0b', icon: MdHourglassEmpty, countKey: 'pending' },
+ { key: 'inactive', label: 'Inactive Tenants', color: '#ef4444', icon: MdCancel, countKey: 'inactive' }
+];
+
+const SoftPaper = (props) => (
+
+);
+
+const AccentAvatar = ({ color, selected, size = 24, children }) => (
+
+ {children}
+
+);
+
// ==============================|| Starts here||============================== //
const Clients1 = () => {
const textFieldRef = useRef(null);
@@ -474,114 +570,356 @@ const Clients1 = () => {
}
};
+ const activeTabMeta = STATUS_META[STATUS_TABS[value0]?.status] || STATUS_META.active;
+
return (
-
+ <>
{(getalltenantsIsLoading || summaryDataIsLoading || isloader) &&
}
- {/* ========================================================== || Titlecard || ========================================================== */}
-
-
-
- {/* ========================================================== || MainCard (searchword) || ========================================================== */}
-
+
+
+
+
+
+
+
+ Tenants
+
+
+
+
+ Live · {locaName || 'All Zones'}
+
+
+
+
+ }
+ placeholder="Select Zone"
+ paperComponent={SoftPaper}
+ sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
+ />
+
+
+
+ {/* ============================================= || KPI Cards | ============================================= */}
+
+ {KPI_META.map((item) => {
+ const Icon = item.icon;
+ const value = summaryData?.[item.countKey] ?? 0;
+ return (
+
+ {
+ const idx = STATUS_TABS.findIndex((t) => t.countKey === item.countKey);
+ if (idx >= 0) handleChange(null, idx);
+ }}
+ >
+
+
+
+
+ {item.label}
+
+ {summaryDataIsLoading ? (
+
+ ) : (
+
+ {value}
+
+ )}
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ {/* ============================================= || Status Tabs + Search || ============================================= */}
+
+
- {/* ============================================= || Tabs || ================================================= */}
-
-
- {
- setstatus('active');
- }}
- icon={
- : summaryData?.active || 0}
- color="primary"
- variant="light"
- sx={{ minWidth: 32, justifyContent: 'center' }}
- size="small"
- />
- }
- iconPosition="end"
- />
- {
- setstatus('pending');
- }}
- icon={
- : summaryData?.pending || 0}
- color="primary"
- variant="light"
- sx={{ minWidth: 32, justifyContent: 'center' }}
- size="small"
- />
- }
- iconPosition="end"
- />
- {
- setstatus('inactive');
- }}
- icon={
- : summaryData?.inactive || 0}
- color="primary"
- variant="light"
- sx={{ minWidth: 32, justifyContent: 'center' }}
- size="small"
- />
- }
- iconPosition="end"
- />
-
-
- {/* ============================================= || SearchOutlined || ================================================= */}
+ {STATUS_TABS.map((t, idx) => {
+ const meta = STATUS_META[t.status];
+ const Icon = meta.icon;
+ const active = value0 === idx;
+ const count = summaryData?.[t.countKey] ?? 0;
+ return (
+ {
+ setstatus(t.status);
+ handleChange(null, idx);
+ }}
+ sx={{
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: { xs: 0.625, md: 0.875 },
+ pl: 0.5,
+ pr: { xs: 1, md: 1.25 },
+ py: 0.5,
+ flexShrink: 0,
+ cursor: 'pointer',
+ borderRadius: 999,
+ border: `1.5px solid ${active ? meta.color : edge(meta.color)}`,
+ bgcolor: active ? meta.color : tint(meta.color),
+ color: active ? '#fff' : meta.color,
+ fontWeight: 700,
+ boxShadow: active ? `0 6px 18px ${ring(meta.color)}` : 'none',
+ transition: 'all 0.18s',
+ '&:hover': {
+ borderColor: meta.color,
+ boxShadow: active ? `0 6px 18px ${ring(meta.color)}` : `0 0 0 3px ${ring(meta.color)}`
+ }
+ }}
+ >
+
+
+
+
+ {meta.label}
+
+
+ {summaryDataIsLoading ? : count}
+
+
+ );
+ })}
+
+ {/* Search */}
+
-
- }
+
+
+
+
+ {/* ============================================= || Table || ============================================= */}
+
- {/* ============================================= || TableContainer| ============================================= */}
-
-
+
+
-
- S.No
- Client
- Contact
- Address
-
- Actions
-
+
+ #
+ Status
+ Client
+ Contact
+ Address
+ Actions
@@ -589,111 +927,220 @@ const Clients1 = () => {
{tenantList?.length == 0 && !isloader ? (
-
-
-
+
+
+
+
+
+
+ No tenants to show
+
+
+ {`No ${activeTabMeta.label.toLowerCase()} tenants for this filter.`}
+
) : (
- tenantList?.map((row, index) => (
- <>
+ tenantList?.map((row, index) => {
+ const rowStatusKey = (value0 === 0 ? 'active' : value0 === 1 ? 'pending' : 'inactive');
+ const rowStatusMeta = STATUS_META[rowStatusKey];
+ const RowStatusIcon = rowStatusMeta.icon;
+ return (
+
{/* ============================================ || tablerow 1 || ============================================ */}
-
- {index + 1 + page * rowsPerPage}
+
-
-
- {row.tenantname}
+
+ {String(index + 1 + page * rowsPerPage).padStart(2, '0')}
+
+
+
+
+
+
+
+
+ {rowStatusMeta.label}
- Id : {row.tenantid}
- {row.primarycontact}
- {row.primaryemail}
+
+
+
+
+
+
+ {row.tenantname}
+
+
+ ID #{row.tenantid}
+
+
+
- {row.address}
-
-
-
+
+
+ {row.primarycontact || '—'}
+
+
+ {row.primaryemail || '—'}
+
+
+
+
+
+
-
- {value0 == 0 ? (
- {
- setSelectedCustomer(row);
- setSelectedtenid(row.tenantid);
- setAppId(row.applocationid);
- setClientstatus(row.approved);
- setTimeout(() => {
- tenantupdate(row.tenantid);
- }, 100);
- }}
- />
- ) : value0 == 1 ? (
- {
- setSelectedCustomer(row);
- setDialogopen(true);
- setSelectedtenid(row.tenantid);
- setAppId(row.applocationid);
- getAppPricing(row.applolcationid);
- }}
- />
- ) : (
- {
- setSelectedCustomer(row);
- setSelectedtenid(row.tenantid);
- setAppId(row.applocationid);
- setTimeout(() => {
- tenantupdate(row.tenantid);
- }, 100);
- }}
- />
- )}
+ {row.address || '—'}
+
+
+
+
+
+ {value0 == 0 && (
+
+ {
+ setSelectedCustomer(row);
+ setSelectedtenid(row.tenantid);
+ setAppId(row.applocationid);
+ setTimeout(() => {
+ tenantupdate(row.tenantid);
+ }, 100);
+ }}
+ >
+
+
-
- {
- setSelectedCustomer(row);
- handleCollapseToggle1(index); // makes 1st collpase open
- setOpenRowIndex2(-1); //makes 2nd collapse close , if open
- setSelectedtenid(row.tenantid);
- // setAppId(row.applocationid);
- }}
- sx={{
- color: openRowIndex1 === index ? theme.palette.error.main : theme.palette.primary.main
- }}
- >
- {openRowIndex1 === index ? : }
-
- {value0 !== 1 && (
+ )}
+ {value0 == 1 && (
+
+ {
+ setSelectedCustomer(row);
+ setDialogopen(true);
+ setSelectedtenid(row.tenantid);
+ setAppId(row.applocationid);
+ getAppPricing(row.applolcationid);
+ }}
+ >
+
+
+
+ )}
+ {value0 == 2 && (
+
+ {
+ setSelectedCustomer(row);
+ setSelectedtenid(row.tenantid);
+ setAppId(row.applocationid);
+ setTimeout(() => {
+ tenantupdate(row.tenantid);
+ }, 100);
+ }}
+ >
+
+
+
+ )}
+
{
setSelectedCustomer(row);
- handleCollapseToggle2(index); // makse 2nd collpase open
- setOpenRowIndex1(-1); // makse 1st collpse close if open
+ handleCollapseToggle1(index);
+ setOpenRowIndex2(-1);
setSelectedtenid(row.tenantid);
- setAppId(row.applocationid);
- }}
- sx={{
- color: openRowIndex2 === index ? theme.palette.error.main : theme.palette.primary.main
}}
>
-
- {openRowIndex2 === index ? : }
-
+ {openRowIndex1 === index ? : }
+
+ {value0 !== 1 && (
+
+ {
+ setSelectedCustomer(row);
+ handleCollapseToggle2(index);
+ setOpenRowIndex1(-1);
+ setSelectedtenid(row.tenantid);
+ setAppId(row.applocationid);
+ }}
+ >
+ {openRowIndex2 === index ? : }
+
+
)}
@@ -1532,21 +1979,20 @@ const Clients1 = () => {
)}
- >
- ))
+
+ );
+ })
)}
-
{/* ============================================= || Pagination| ============================================= */}
{!searchword && tenantList?.length > 0 && (
<>
-
+
{
/>
>
)}
-
+
{/* // ==============================||( Client Pricing ) dialog (dialogopen) ||============================== // */}
-
+ >
);
};
diff --git a/src/pages/nearle/customers/customers.js b/src/pages/nearle/customers/customers.js
index 631b954..54bb707 100644
--- a/src/pages/nearle/customers/customers.js
+++ b/src/pages/nearle/customers/customers.js
@@ -1,15 +1,11 @@
import { React, useState, useEffect, useRef, useMemo } from 'react';
-import { Empty } from 'antd';
import axios from 'axios';
import { FaRegEdit } from 'react-icons/fa';
-import { RiEdit2Fill } from 'react-icons/ri';
-import { useTheme } from '@mui/material/styles';
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
// material-ui
import {
Box,
- Divider,
Table,
TableBody,
TableCell,
@@ -27,14 +23,26 @@ import {
DialogContent,
Button,
TextField,
- Autocomplete
+ Autocomplete,
+ Avatar,
+ Paper
} from '@mui/material';
+import {
+ MdPeopleAlt,
+ MdMyLocation,
+ MdPersonPin,
+ MdPhone,
+ MdLocationOn,
+ MdEdit,
+ MdGroups,
+ MdHowToReg,
+ MdPlace
+} from 'react-icons/md';
import Geocode from 'react-geocode';
import LocationOnIcon from '@mui/icons-material/LocationOn';
import parse from 'autosuggest-highlight/parse';
import { debounce } from '@mui/material/utils';
// project imports
-import MainCard from 'components/MainCard';
import Loader from 'components/Loader';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
@@ -43,6 +51,59 @@ import { getallcustomers, getcustomersummary } from 'pages/api/api';
import { OpenToast } from 'components/third-party/OpenToast';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
+// ============================================================================
+// Design tokens — shared with the deliveries / tenants / pricing pages so every
+// surface (header, KPI tiles, table, badges, dialog) speaks the same visual
+// language. Keep this block in sync with deliveries.js:109–162.
+// ============================================================================
+const DT = {
+ radiusPill: 999,
+ radiusCard: 16,
+ 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'
+};
+const a = (c, suffix) => `${c}${suffix}`;
+const tint = (c) => a(c, '08');
+const soft = (c) => a(c, '18');
+const ring = (c) => a(c, '26');
+const edge = (c) => a(c, '55');
+
+const SoftPaper = (props) => (
+
+);
+
+const AccentAvatar = ({ color, selected, size = 24, children }) => (
+
+ {children}
+
+);
+
// ==============================|| google address ||============================== //
const GOOGLE_MAPS_API_KEY = process.env.REACT_APP_GOOGLE_MAPS_API_KEY;
@@ -67,7 +128,6 @@ export default function Customers() {
const loadMoreRef = useRef();
const [rowsPerPage] = useState(50);
const [page] = useState(0);
- const theme = useTheme();
const [appId, setAppId] = useState(0);
const [locaName, setLocoName] = useState('All');
const [selectedCustomer, setSelectedCustomer] = useState({}); // to edit
@@ -326,131 +386,433 @@ export default function Customers() {
}
}
};
+ const KPI_META = [
+ { key: 'total', label: 'Total Customers', color: '#662582', icon: MdGroups, value: pageCount?.Total ?? 0 },
+ { key: 'loaded', label: 'Loaded in View', color: '#0ea5e9', icon: MdHowToReg, value: rows.length },
+ { key: 'zone', label: 'Active Zone', color: '#10b981', icon: MdPlace, value: locaName || 'All Zones' }
+ ];
+
return (
<>
- {(customerSummaryIsLoading || customersIsLoading) && (
- <>
- {/* */}
-
- >
- )}
- {/* */}
-
-
- {/* Left: Title */}
- Customers
- {/* Right: Controls */}
-
-
+ {(customerSummaryIsLoading || customersIsLoading) && }
-
+
+
+
+
+
+
+
+ Customers
+
+
+
+
+ Live · {locaName || 'All Zones'}
+
- }
+ }
+ placeholder="Select Zone"
+ paperComponent={SoftPaper}
+ sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
+ />
+
+
+
+ {/* ============================================= || KPI Cards | ============================================= */}
+
+ {KPI_META.map((item) => {
+ const Icon = item.icon;
+ return (
+
+
+
+
+
+
+ {item.label}
+
+
+ {item.value}
+
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ {/* ============================================= || Search header | ============================================= */}
+
-
-
+
+
+
+
+
+
+
+ Directory
+
+
+ {pageCount?.Total ?? 0} total · {rows.length} loaded
+
+
+
+
+
+
+
+
+
+ {/* ============================================= || Table || ============================================= */}
+
+
+
-
- #
- Name
- Contact
- Address
- Location
- Action
+
+ #
+ Customer
+ Contact
+ Address
+ Location
+ Action
{customersIsLoading && }
- {rows?.length == 0 && !customersIsLoading ? (
+ {rows?.length === 0 && !customersIsLoading ? (
-
-
-
+
+
+
+
+
+
+ No customers to show
+
+
+ {searchword ? 'Try a different keyword.' : 'Pick a zone above to load the directory.'}
+
) : (
- rows?.map((row, index) => {
- return (
-
- {index + 1 + page * rowsPerPage}
-
-
- {row.firstname}
-
-
- Id : {row.customerid}
-
-
-
-
- {row.contactno}
-
-
- {row.email}
-
-
- {row.address}
- {row.suburb}
-
-
- {
- console.log('row', row);
- setSelectedCustomer(row);
- setTimeout(() => {
- setOpen(true);
- }, 0);
- }}
+ rows?.map((row, index) => (
+
+
+
+ {String(index + 1 + page * rowsPerPage).padStart(2, '0')}
+
+
+
+
+
+
+
+
+
-
-
-
-
-
- );
- })
+ {row.firstname || '—'}
+
+
+ ID #{row.customerid}
+
+
+
+
+
+
+
+
+
+ {row.contactno || '—'}
+
+
+ {row.email && (
+
+ {row.email}
+
+ )}
+
+
+
+
+
+ {row.address || '—'}
+
+
+
+
+ {row.suburb ? (
+
+ {row.suburb}
+
+ ) : (
+ —
+ )}
+
+
+
+ {
+ setSelectedCustomer(row);
+ setTimeout(() => setOpen(true), 0);
+ }}
+ sx={{
+ bgcolor: soft('#8b5cf6'),
+ color: '#8b5cf6',
+ border: `1px solid ${edge('#8b5cf6')}`,
+ '&:hover': { bgcolor: '#8b5cf6', color: '#fff' }
+ }}
+ >
+
+
+
+
+
+ ))
)}
- {rows?.length != 0 && (
+ {rows?.length !== 0 && (
-
+
- {isFetchingNextPage ? : hasNextPage ? : 'No More Orders'}
+ {isFetchingNextPage || hasNextPage ? (
+
+ ) : (
+
+ No more customers
+
+ )}
@@ -458,8 +820,7 @@ export default function Customers() {
-
-
+
{/* ======================================== || Edit Dialog || ======================================== */}