diff --git a/.claude/skills/nearlexpress-docs/SKILL.md b/.claude/skills/nearlexpress-docs/SKILL.md index 44b3298..5a90cdd 100644 --- a/.claude/skills/nearlexpress-docs/SKILL.md +++ b/.claude/skills/nearlexpress-docs/SKILL.md @@ -5,6 +5,8 @@ metadata: version: 2.0.0 --- +> **⚠️ STALE as of the api.doormile.com migration.** Every `REACT_APP_URL` / `REACT_APP_URL2` endpoint below (jupiter.nearle.app) has been retired from the codebase. The console now calls `api.doormile.com` (`REACT_APP_DOORMILE_URL`) via `src/utils/doormileAxios.js` / `src/pages/api/doormileApi.js` — see `express-console-api.md` at the repo root for the current endpoint reference, and `src/pages/api/CLAUDE.md` for what the new backend has no equivalent for (zones/`applocationid`, live rider GPS logs, `/substitutions`, invoices, expense requests, several report breakdowns). The AI dispatch optimiser (routes.workolik.com / routemate.workolik.com) and the final-delivery-commit call are the one exception — they're unchanged, still hardcoded exactly as described below. The architecture/navigation/FCM sections below are still broadly accurate; only the API base and per-endpoint table are obsolete. + # 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. diff --git a/.env b/.env index a04f0dc..f820a10 100644 --- a/.env +++ b/.env @@ -4,15 +4,7 @@ GENERATE_SOURCEMAP = false ## Backend API URL REACT_APP_API_URL=https://mock-data-api-nextjs.vercel.app/ -REACT_APP_URL=https://jupiter.nearle.app/live/api/v1 -REACT_APP_URL2=https://jupiter.nearle.app/live/api/v2 -REACT_APP_URL3=https://jupiter.nearle.app/live/api/v3 -REACT_APP_STAFF_TOKEN= -REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk - - - - +REACT_APP_DOORMILE_URL=https://api.doormile.com/api/v1 REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk diff --git a/.env.development b/.env.development index dd90009..ac6c9ca 100644 --- a/.env.development +++ b/.env.development @@ -1,7 +1 @@ -REACT_APP_URL=https://jupiter.nearle.app/live/api/v1 -REACT_APP_URL2=https://jupiter.nearle.app/live/api/v2 -REACT_APP_URL3=https://jupiter.nearle.app/live/api/v3 -REACT_APP_STAFF_TOKEN= -REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk - - +REACT_APP_DOORMILE_URL=https://api.doormile.com/api/v1 diff --git a/.env.staging b/.env.staging index b0c47ec..ac6c9ca 100644 --- a/.env.staging +++ b/.env.staging @@ -1,3 +1 @@ -REACT_APP_URL=https://jupiter.nearle.app/live/api/v1 -REACT_APP_URL2= -REACT_APP_STAFF_TOKEN= \ No newline at end of file +REACT_APP_DOORMILE_URL=https://api.doormile.com/api/v1 diff --git a/API_ENDPOINTS.md b/API_ENDPOINTS.md index 98e9198..ac53167 100644 --- a/API_ENDPOINTS.md +++ b/API_ENDPOINTS.md @@ -1,5 +1,7 @@ # NearlExpress Console — API Endpoints Reference +> **⚠️ STALE — describes the retired jupiter.nearle.app backend.** `src/pages/api/api.js` no longer calls any of the endpoints below; it now talks to `api.doormile.com` (`REACT_APP_DOORMILE_URL`) via `src/pages/api/doormileApi.js`. See `express-console-api.md` at the repo root for the current endpoint reference. Kept for historical context only. +> > Auto-generated reference of every API call in `src/pages/api/api.js`. > Base URLs are resolved from the env files at the repo root. diff --git a/CLAUDE.md b/CLAUDE.md index 1ac1295..bb44abb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,7 +53,7 @@ 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`). No Maps API key is needed — maps and address search run on free Leaflet/OSM services. The optimiser URLs (`routes.workolik.com`, `routemate.workolik.com`) and the Jupiter auth URL (`jupiter.nearle.app`) are hardcoded — see the `nearlexpress-docs` skill. +- **Required env vars**: `REACT_APP_DOORMILE_URL` — the only API base (`https://api.doormile.com/api/v1`), used by every `/admin/*` call via `utils/doormileAxios.js`. The old jupiter-backed `REACT_APP_URL` / `REACT_APP_URL2` / `REACT_APP_URL3` have been fully retired — do not reintroduce them. No Maps API key is needed — maps and address search run on free Leaflet/OSM services. The optimiser URLs (`routes.workolik.com`, `routemate.workolik.com`) and the final-delivery-commit URL (`jupiter.nearle.app`) remain hardcoded — they're a separate solver service with no equivalent in the new API, deliberately left untouched by the backend migration. See the `nearlexpress-docs` skill and `express-console-api.md`. - **Dev server runs on `http://localhost:3000`**. The user usually has it running already — assume it is up when reporting "reload to see it". --- @@ -66,7 +66,7 @@ npm run lint 4. **Do not bypass the dispatch reconcile step.** After any manual edit on `/doormile/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`. +7. **Do not use absolute `http://localhost` URLs** in code. Always read from `process.env.REACT_APP_DOORMILE_URL` (or use `doormileAxios`, which already defaults to it). 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. @@ -248,8 +248,8 @@ These colour-code lifecycle states. Do **not** swap them for brand red — opera ## 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. +- **Auth state lives in `localStorage`**. The keys to know: `authname` (gate in `App.js` and `utils/session.js`'s `AUTH_PRESENCE_KEY` — unrelated to which backend authenticated the session, kept as the presence flag across the migration), `doormileToken` (the actual JWT sent as `Authorization: Bearer` on every `/admin/*` call — see `utils/doormileAxios.js`), `doormileUser`, `userid`, `roleid`, `tenantid`, `userfcmtoken`, `applocations` (cached zone list, now hub-derived). When adding auth-touching code, read these directly — there is no `useAuth()` hook. +- **The 401 redirect for the Doormile API** comes from `utils/doormileAxios.js` (auto-attaches the bearer token; on 401 does a full `localStorage.clear()` + hard navigate to `/login`, matching `utils/session.js`'s `performSessionLogout` contract). `src/utils/axios.js` is a separate, older, unrelated mock-service client — don't confuse the two. - **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. --- @@ -278,8 +278,8 @@ These colour-code lifecycle states. Do **not** swap them for brand red — opera ## 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`. +- **One API base**: `REACT_APP_DOORMILE_URL` (`api.doormile.com/api/v1`), via `utils/doormileAxios.js`. See `src/pages/api/CLAUDE.md` for the full migration-era rule sheet, including what the new backend has no equivalent for. +- **The `applocationid` "zone" concept no longer exists as a filterable resource** on the new API — it was a jupiter-only concept. `LocationAutocomplete`/`fetchAppLocations` now derive a picker list from `GET /admin/hubs` instead of a real zones endpoint; most list endpoints on the new backend don't accept an `applocationid` filter at all. Don't assume it's wired through end-to-end on a page you haven't checked. - **`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. - **No Maps API key exists in this project anymore.** Address search/geocoding goes through `AddressAutocomplete.js` (Nominatim) and routing through OSRM — both free, no key. Do not add `process.env.REACT_APP_GOOGLE_MAPS_API_KEY` or any Google Maps script/dependency back in. diff --git a/FLOW.md b/FLOW.md index d78fd4e..f100025 100644 --- a/FLOW.md +++ b/FLOW.md @@ -1,5 +1,7 @@ # NearlExpress Console — Repository Flow Graph +> **⚠️ STALE — describes the retired jupiter.nearle.app backend.** The console now talks exclusively to `api.doormile.com` via `REACT_APP_DOORMILE_URL` (see `src/utils/doormileAxios.js`, `src/pages/api/doormileApi.js`, and `express-console-api.md` at the repo root for the current endpoint reference). The `REACT_APP_URL` / `REACT_APP_URL2` env vars and every jupiter endpoint below no longer exist in the codebase — this file is kept for historical/architectural context only, not as an accurate current reference. +> > 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)). > diff --git a/README.md b/README.md index 595eaf5..9f5f15d 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,7 @@ REACT_APP_VERSION=v2.1.0 GENERATE_SOURCEMAP=false # Backend Services Endpoint URLs -REACT_APP_API_URL=https://mock-data-api-nextjs.vercel.app/ -REACT_APP_URL=https://jupiter.nearle.app/live/api/v1 -REACT_APP_URL2=https://jupiter.nearle.app/live/api/v2 -REACT_APP_URL3=https://jupiter.nearle.app/live/api/v3 +REACT_APP_DOORMILE_URL=https://api.doormile.com/api/v1 ``` No Maps API key is required — maps and address search run on free Leaflet/OpenStreetMap (Nominatim + OSRM), not Google Maps. diff --git a/env.staging b/env.staging index 9afabf2..21a920d 100644 --- a/env.staging +++ b/env.staging @@ -1,3 +1 @@ -REACT_APP_URL='https://jupiter.nearle.app/live/api/v1' -REACT_APP_URL2='' -REACT_APP_STAFF_TOKEN='' \ No newline at end of file +REACT_APP_DOORMILE_URL='https://api.doormile.com/api/v1' \ No newline at end of file diff --git a/express-console-api.md b/express-console-api.md new file mode 100644 index 0000000..37dbe95 --- /dev/null +++ b/express-console-api.md @@ -0,0 +1,292 @@ +# Doormile Express Console — API reference + +The console surface only (`/admin/*`). 89 routes: 1 login + 88 authenticated. +Everything is under `https://api.doormile.com/api/v1`. + +Miler-app, hub-console, customer-app and CRM routes are not in this document. + +--- + +## Auth + +``` +POST /api/v1/admin/login +{ "email": "developer@doormile.com", "password": "admin@123" } +``` + +Returns `{ success, token, user: { id, name, email, role, tenantid } }`. +Send it on every other call as `Authorization: Bearer `. + +The token is a JWT carrying `userid`, `email`, `roleid`, `tenantid`, `configid`. +Roles allowed on this group: **1 admin, 3 manager, 4 executive**. Anything else +gets 401. + +### Tenant scoping — read this before wiring any list screen + +`tenantid` in the token decides what the account can see: + +| Token `tenantid` | Who | Sees | +|---|---|---| +| `0` / null | Doormile's own staff | everything, all tenants | +| set (e.g. `13`) | a client's console login | only that tenant's rows | + +Scoping is applied server-side. A client login **cannot** widen it by sending +`?tenantid=` or a body `tenantid` — on writes the server overwrites the field +with the token's tenant. Build the UI as if the API returns exactly what the +account is allowed to see, because it does. + +Note the group is registered under a stale `// all open, no token required` +comment in `routes.go`; the comment is wrong, `AuthMiddleware` is applied. + +--- + +## Dashboard, profile, reports + +| Method | Path | Notes | +|---|---|---| +| GET | `/admin/dashboard` | counts + today's numbers | +| GET | `/admin/reports` | `?from=YYYY-MM-DD&to=YYYY-MM-DD`, defaults to today (IST) | +| GET | `/admin/profile` | current account | +| GET | `/admin/me` | alias of the above | +| PUT | `/admin/profile/password` | `{ "current_password": "...", "new_password": "..." }` — snake_case | + +## App users (staff logins) + +| Method | Path | +|---|---| +| GET | `/admin/users` | +| POST | `/admin/users` | +| PUT | `/admin/users/:id` | +| DELETE | `/admin/users/:id` | + +## Partners (fleet / rider suppliers) + +Not the same thing as a tenant. A partner supplies vehicles and riders; a tenant +is a client Doormile delivers for. + +| Method | Path | Body | +|---|---|---| +| GET | `/admin/partners` | | +| POST | `/admin/partners` | `{ partnername, partnertypeid, contactno, status }` | +| GET | `/admin/partners/:id` | | +| PUT | `/admin/partners/:id` | | +| DELETE | `/admin/partners/:id` | hard delete — no soft-delete column | + +## Tenants (client companies) + +| Method | Path | Body | +|---|---|---| +| GET | `/admin/tenants` | | +| POST | `/admin/tenants` | `{ tenantname, primaryemail, primarycontact, status, requiredeliveryotp }` | +| GET | `/admin/tenants/:id` | | +| PUT | `/admin/tenants/:id` | `requiredeliveryotp` is a pointer — omit it to leave the setting alone | +| DELETE | `/admin/tenants/:id` | hard delete | +| GET | `/admin/tenants/:id/locations` | the client's sites (kitchens, branches, depots) | +| POST | `/admin/tenants/:id/locations` | `{ locationname, address, city, state, pincode, latitude, longitude, isprimary, status }` | +| PUT | `/admin/tenantlocations/:id` | note: **not** nested under the tenant | + +`requiredeliveryotp` is opt-in per tenant and **off by default**. DailyGrubs runs +without delivery OTP by decision. + +## Tenant customers (a client's own end customers) + +| Method | Path | Body | +|---|---|---| +| GET | `/admin/tenantcustomers` | | +| POST | `/admin/tenantcustomers` | `{ firstname, lastname, phone, email }` | +| GET | `/admin/tenantcustomers/:id` | | +| PUT | `/admin/tenantcustomers/:id` | | +| DELETE | `/admin/tenantcustomers/:id` | | + +## B2C app customers + +| Method | Path | +|---|---| +| GET | `/admin/customers` | +| PATCH | `/admin/customers/:id` | + +Tenant-scoped through their bookings — a client login sees only customers who +have ordered through them. + +## Hubs + +| Method | Path | Body | +|---|---|---| +| GET | `/admin/hubs` | | +| POST | `/admin/hubs` | `{ hubname, hubtype, applocationid, contactno, address, latitude, longitude, pincode, status }` | +| GET | `/admin/hubs/:id` | | +| PUT | `/admin/hubs/:id` | | +| DELETE | `/admin/hubs/:id` | soft delete | + +`hubtype`: `sorting_center` \| `delivery_hub`. `applocationid` is the city — +Nagercoil is 5; read the rest from `GET /admin/hubs` rather than hardcoding. + +## Vehicles + +| Method | Path | Body | +|---|---|---| +| GET | `/admin/vehicles` | | +| POST | `/admin/vehicles` | `{ vehicleno, vehicletype, maxweight, maxvolume, partnerid, batterypercentage, status }` | +| GET | `/admin/vehicles/:id` | | +| PUT | `/admin/vehicles/:id` | | +| DELETE | `/admin/vehicles/:id` | soft delete | + +## Milers (riders) + +| Method | Path | Body | +|---|---|---| +| GET | `/admin/milers` | tenant-scoped | +| POST | `/admin/milers` | see below | +| GET | `/admin/milers/:id` | | +| PUT | `/admin/milers/:id` | | +| PUT | `/admin/milers/:id/block` | | +| PUT | `/admin/milers/:id/assign-vehicle` | | +| POST | `/admin/milers/:id/notify` | `{ title, message }` — `:id` is the **milerprofileid** | + +```jsonc +POST /admin/milers +{ + "authname": "Murali", + "email": "murali@dailygrubs.com", + "contactno": "9876543210", + "password": "1234", + "displayname": "Murali S", + "tenantid": 13, + "defaultvehicletype": "Bike", + "applocationid": 1, + "hubid": 4 // optional; without it the rider is invisible to the hub console + // configid defaults to 1001 — the partition the miler app logs in against. + // Do not override it. Riders created without it could never log in. +} +``` + +## Bookings + +| Method | Path | Notes | +|---|---|---| +| GET | `/admin/bookings` | tenant-scoped list | +| POST | `/admin/expressbooking` | create one — passes CityGate | +| POST | `/admin/expressbooking/bulk` | `{ "bookings": [ ... ] }`, max 200, per-row results | +| GET | `/admin/bookings/:id` | 403 if outside your tenant | +| POST | `/admin/bookings/:id/assign-miler` | | +| POST | `/admin/bookings/:id/assign-vehicle` | | +| PUT | `/admin/bookings/:id/status` | | +| POST | `/admin/bookings/:id/cancel` | | +| POST | `/admin/bookings/bulk-cancel` | | + +```jsonc +POST /admin/expressbooking +{ + "tenantid": 13, // required; forced to your own tenant on client logins + "pickuplocationid": 15, // a stored kitchen/branch — fills address, pincode and + // coords for you, and is what makes per-site reporting work + "customer_phone": "9876543210", // creates a Guest customer if unknown + "customer_name": "Ramesh", + "deliveryaddress": "12 Cross Cut Road, Gandhipuram", + "deliverypincode": "641012", + "deliverycity": "Coimbatore", + "deliverylatitude": 11.0168, + "deliverylongitude": 76.9558, + "service_option": "Fast", // Normal | Fast | Superfast + "finalprice": 120, // the order amount the tenant pays — passed through + "notes": "Ring the bell", + "parcels": [ + { "itemcategory": "Food", "itemdescription": "2 meal boxes", "declaredvalue": 350 } + ] +} +``` + +Rules worth knowing: +- `parcels` must be non-empty and `tenantid` must exist. +- Pickup address + pincode are required **unless** `pickuplocationid` supplies them. +- A `pickuplocationid` belonging to another tenant is rejected. +- **CityGate**: the pickup pincode prefix must be an open city — `641` + Coimbatore, `600` Chennai, `560` Bengaluru, `500` Hyderabad, `629` Nagercoil. + Any other prefix is refused at the middleware, before the handler runs. +- Matching 3-digit pickup and delivery prefixes = hyperlocal, and the parcel goes + straight to `Out_for_Delivery` at pickup instead of routing via a hub. +- Auto-assignment fires after commit as a background retry loop (5 attempts, + 2 min apart). The response returns before a rider is attached. + +## Consignments + +| Method | Path | +|---|---| +| GET | `/admin/consignments` | +| GET | `/admin/consignments/:id` | +| GET | `/admin/consignments/track/:trackingno` | +| PUT | `/admin/consignments/:id/status` | + +## Tripsheets (hub-to-hub transport) + +| Method | Path | Body | +|---|---|---| +| GET | `/admin/tripsheets` | | +| POST | `/admin/tripsheets` | `{ sourcehubid, destinationhubid, vehicleid, driveruserid }` | +| GET | `/admin/tripsheets/:id` | | +| POST | `/admin/tripsheets/:id/items` | `{ consignmentid }` | +| DELETE | `/admin/tripsheets/:id/items/:itemid` | | +| PUT | `/admin/tripsheets/:id/dispatch` | | +| PUT | `/admin/tripsheets/:id/arrive` | | + +## Pricing + +| Method | Path | Notes | +|---|---|---| +| GET | `/admin/pricing` | tenant pricing rules | +| POST | `/admin/pricing` | `{ tenantid, applocationid, vehicletype, baseprice, baseweight, priceperkg, basedistance, priceperkm, handlingcharges, effectivefrom, effectiveto, currency, priority, status }` | +| PUT | `/admin/pricing/:id` | | +| DELETE | `/admin/pricing/:id` | | +| POST | `/admin/pricing/simulate` | quote without creating anything | +| POST | `/admin/pricing/quote` | same handler as simulate | +| GET | `/admin/doormile-pricing` | Doormile's own bands | +| POST | `/admin/doormile-pricing` | | +| PUT | `/admin/doormile-pricing/:id` | | +| DELETE | `/admin/doormile-pricing/:id` | soft delete | + +## Exceptions + +| Method | Path | Body | +|---|---|---| +| GET | `/admin/exceptions` | | +| POST | `/admin/exceptions` | `{ consignmentid, tripsheetid, hubid, exceptiontype, severity, description }` | +| GET | `/admin/exceptions/:id` | | +| PUT | `/admin/exceptions/:id/status` | `{ resolution, status }` — `Resolved` \| `Closed` | + +`exceptiontype`: `Lost`, `Damaged`, `Misrouted`, `Receiver_Refused`, +`Missing_Contents`, `Undeliverable`. `severity`: `Low`, `Medium`, `High`, +`Critical`. + +## Competitive intel + +| Method | Path | +|---|---| +| GET/POST | `/admin/competitor-branches` | +| PUT/DELETE | `/admin/competitor-branches/:id` | +| GET/POST | `/admin/carrier-pricing` | +| PUT/DELETE | `/admin/carrier-pricing/:id` | + +--- + +## Conventions across every endpoint + +- **Envelope**: `{ "success": true, "data": ... }` on success, + `{ "success": false, "message": "..." }` on failure. Lists add `total`, + paginated lists add `page`. +- **Pagination**: `?pageno=1&pagesize=100`. Default 500, cap 1000. +- **Rate limits**: 300/min per IP globally, 10/min shared across all credential + endpoints. Behind the ingress this keys on the proxy IP unless + `TRUSTED_PROXIES` is set. +- **Timestamps** are IST (`Asia/Kolkata`) wall-clock in `timestamp without time + zone` columns. Send dates as `YYYY-MM-DD`, not epochs. +- **Soft delete** exists on Hub, Vehicle, Consignment, Tripsheet, TripsheetItem, + ConsignmentException, DoormilePricing, CarrierPricing. Partner and Tenant are + hard-deleted. + +## Not exercised yet + +Roughly half these routes have never had a real request against them. Exercised +end-to-end so far: login, dashboard, reports, tenants + locations, milers +(create/list/notify), expressbooking (single), bookings list/detail, +assign-miler, consignments, profile password. Treat the rest as written but +unproven. diff --git a/jupiter2doormile.md b/jupiter2doormile.md new file mode 100644 index 0000000..3e82f5c --- /dev/null +++ b/jupiter2doormile.md @@ -0,0 +1,254 @@ +# jupiter → Doormile + +What the old Nearle/jupiter API did, and what replaces it in Doormile. Two +surfaces only — the **express console** and the **miler app**. Hub console, CRM +and the B2C customer app are out of scope here. + +Base URLs: + +| | jupiter | Doormile | +|---|---|---| +| API | `jupiter.nearle.app/live/api/v1` | `api.doormile.com/api/v1` | +| Write path | `queue.workolik.com` (TLS verify off, hardcoded IP pin) | same host, no side channel | + +**Confidence marking.** Paths marked ✅ were read off real network logs from the +live jupiter console. Paths marked ~ come from the prior-session analysis of the +jupiter codebase and have not been re-confirmed against a live request — check +the exact spelling before wiring anything to them. + +Status: **Done** = built and hit with a real request · **Built** = written and +compiled, never called · **Gap** = nothing replaces it yet · **Dropped** = +deliberately not migrated. + +--- + +## 1. Auth + +| jupiter | Doormile | Status | +|---|---|---| +| ~ console login (undocumented in jupiter's own API docs — found only by reading the console source) | `POST /admin/login` → `{email, password}` | **Done** | +| ~ rider login | `POST /miler/login` then `POST /miler/verify-pin` | **Done** | + +Two real differences: + +- Doormile splits rider login into **phone → PIN**, two calls. jupiter did it in + one. +- The Doormile console token carries **`tenantid`**. jupiter had no tenant + concept on the login at all; every console user saw everything. This is the + single biggest behavioural change for a client account. +- `configid` must be **1001** on both miler calls. There is no jupiter + equivalent — it's a Doormile login partition. + +--- + +## 2. Express console + +### 2.1 Rider screens + +| jupiter | Doormile | Status | +|---|---|---| +| ✅ `GET /deliveries/getridersummary/?applocationid=&fromdate=&todate=` | `GET /admin/milers/summary?applocationid=&from=&to=&tenantid=&hubid=` | **Done** | +| ~ rider list | `GET /admin/milers?applocationid=&hubid=&tenantid=` | **Done** | +| ~ rider detail | `GET /admin/milers/:id` | **Done** | +| ~ `riderlogs` (the 1.17M-row, zero-index table) | `GET /admin/milers/:id/logs?from=&to=&limit=` | **Done** | +| ✅ `getriderlocationsummary` *(name confirmed, path inferred)* | covered by `milers/summary` (`currentlatitude/longitude`, `lastpingat`) and `milers/:id/logs` | **Done** | +| — *(no jupiter equivalent)* | `GET /admin/milers/:id/activity?from=&to=` | **Done** | +| ~ rider create/edit | `POST /admin/milers`, `PUT /admin/milers/:id` | **Done** | +| ~ block rider | `PUT /admin/milers/:id/block` | **Built** | +| ~ assign vehicle | `PUT /admin/milers/:id/assign-vehicle` | **Built** | +| — | `POST /admin/milers/:id/notify` | **Done** | + +Parameter translation: jupiter used `fromdate`/`todate`, Doormile uses +`from`/`to`. Both `YYYY-MM-DD`. jupiter's `applocationid=0` meant "all cities"; +Doormile means the same by **omitting** the param. + +### 2.2 Orders / deliveries + +| jupiter | Doormile | Status | +|---|---|---| +| ✅ `GET /deliveries/getdeliveries/` | `GET /admin/bookings` + `GET /admin/consignments` | **Done** | +| ~ `getdelivery` / `getorders` | `GET /admin/bookings/:id`, `GET /admin/consignments/:id` | **Done** | +| ~ `POST /deliveries/createdeliveries` | `POST /admin/expressbooking` | **Done** | +| ~ `createdeliveries` in bulk | `POST /admin/expressbooking/bulk` (max 200, per-row results) | **Built** | +| ~ `PUT /deliveries/updatedelivery` | **split into 11 endpoints** — see §4 | **Done / partial** | +| — | `GET /admin/bookings/:id/track` | **Done** | +| — | `GET /admin/consignments/:id/logs` | **Done** | +| — | `GET /admin/consignments/track/:trackingno` | **Built** | + +Two jupiter bugs that do not carry over, by construction: + +- `getdeliveries` returned **every row 21×** (unconstrained `LEFT JOIN + tenantpricing`, `DISTINCT` over 87 columns that deduped nothing). Doormile's + list endpoints are paginated (`pageno`/`pagesize`, default 500, cap 1000) and + return one row per booking. +- `createdeliveries` had a quadratic insert bug — a slice declared outside the + loop kept accumulating, producing ~2× duplicate `deliveryqueues` rows + (66,446 deliveries → 132,826 rows, confirmed live). `createExpressBooking` is + a single transaction per booking; `/bulk` loops it and reports per-row. + +### 2.3 Reporting + +| jupiter | Doormile | Status | +|---|---|---| +| ✅ `GET /deliveries/getreportsummary/?applocationid=&tenantid=&locationid=&fromdate=&todate=` | `GET /admin/reports?from=&to=&tenantid=&locationid=&hubid=` | **Done** | +| ~ `getlocationsummary` | `GET /admin/locations/summary?tenantid=&locationid=&from=&to=` | **Done** | +| — | `GET /admin/dashboard?tenantid=` | **Done** | + +`locationid` is supported: it narrows every figure to one client site, and +`/admin/reports` now carries a `by_location` block alongside `by_hub`, +`by_tenant` and `by_rider`. + +**Attribution caveat.** Per-site figures group by `tenantlocationid` on the +booking — a column added 2026-08-06. The pre-existing `pickuplocationid` column +is *not* it: that one foreign-keys to `appcustomerlocations`, the B2C customer's +saved address, so writing a client-site id into it fails the insert. Every +booking created before 2026-08-06 has no site at all. + +Since the console sends a kitchen's *address* rather than its id, +`createExpressBooking` resolves the site itself — nearest stored location within +150m, falling back to an address match. Bookings with no site are reported as +their own `"Unattributed"` row rather than dropped, so per-site rows still add +up to the summary total. Sending `tenantlocationid` explicitly is exact and +always wins. + +**`applocationid` (city) is still not a report parameter.** jupiter had it; +Doormile filters by `hubid` instead. Only matters once one client runs in more +than one city. + +### 2.4 Tenants and their sites + +| jupiter | Doormile | Status | +|---|---|---| +| ✅ `GET /tenants/gettenants/` | `GET /admin/tenants` | **Done** | +| ✅ `GET /tenants/gettenantlocations/` | `GET /admin/tenants/:id/locations` | **Done** | +| ~ `getlocations` / `getlocation` / `getlocationdetails` | same as above | **Done** | +| ~ tenant create/edit | `POST /admin/tenants`, `PUT /admin/tenants/:id` | **Done** | +| ~ location create/edit | `POST /admin/tenants/:id/locations`, `PUT /admin/tenantlocations/:id` | **Done** | +| ~ `getbranches` | `GET /admin/hubs` — *jupiter "branches" ≈ Doormile hubs; verify this is the same concept before relying on it* | **Built** | +| ~ `getlocationsummary` | `GET /admin/locations/summary` — see §2.3 | **Done** | + +Doormile adds `locationname` on a tenant location. jupiter identified a site by +its address alone, which does not distinguish two branches on one street. + +--- + +## 3. Miler app + +jupiter's rider app drove almost everything through one overloaded endpoint. +Doormile gives each action its own route. + +| jupiter action | Doormile | Status | +|---|---|---| +| ~ rider login | `POST /miler/login` + `POST /miler/verify-pin` | **Done** | +| ~ PIN reset | `POST /miler/reset-pin` — **now admin-only**, see §5 | **Done** | +| ~ location ping | `PUT /miler/location` | **Done** | +| ~ availability toggle | `PUT /miler/availability` | **Done** | +| ~ assignment list | `GET /miler/assignments`, `GET /miler/assignments/:id` | **Done** | +| ~ accept | `POST /miler/assignments/:id/accept` | **Done** | +| ~ reject | `POST /miler/assignments/:id/reject` | **Built** | +| ~ rider logs write | `POST /miler/logs`, `POST /miler/status` | **Done** | +| ~ per-delivery logs | `POST /miler/consignments/logs` | **Done** | +| — | `POST /miler/duty/start`, `PUT /miler/duty/end`, `GET /miler/duty/current` | **Done** | +| — | `POST /miler/breaks/start`, `PUT /miler/breaks/end` | **Done** | +| — | `GET /miler/earnings` | **Done** | +| — | `POST /miler/support`, `GET /miler/support` | **Built** | +| — | `GET /miler/notifications` | **Done** | +| — | `PATCH /miler/notifications/:id/read` | **Gap** — stub, persists nothing | + +--- + +## 4. `PUT /deliveries/updatedelivery` — the 11-way split + +This is the centre of the migration. jupiter overloaded one endpoint for **11 +distinct real actions**, distinguished only by which JSON fields happened to be +non-empty. Each is now its own route with its own validation and its own status +transition. + +**Eight rider actions:** + +| Action | Doormile | +|---|---| +| reached pickup | `POST /miler/bookings/:bookingid/reached` | +| confirm parcel + dimensions | `POST /miler/bookings/:bookingid/parcel` | +| collect payment | `POST /miler/bookings/:bookingid/payment` | +| pickup complete | `POST /miler/bookings/:bookingid/pickup-complete` | +| needs a bigger vehicle | `POST /miler/bookings/:bookingid/vehicle-required` | +| cancel before pickup | `POST /miler/bookings/:bookingid/cancel` | +| deliver | `POST /miler/consignments/:id/deliver` | +| skip / failed attempt | `POST /miler/consignments/:id/skip` | + +**Three console actions** that were bundled into the same rider endpoint: + +| Action | Doormile | +|---|---| +| assign a rider | `POST /admin/bookings/:id/assign-miler` | +| change status | `PUT /admin/bookings/:id/status` · `PUT /admin/consignments/:id/status` | +| cancel | `POST /admin/bookings/:id/cancel` · `POST /admin/bookings/bulk-cancel` | + +`pickup-complete` is the pivot the old system had no concept of: it converts the +booking into a **consignment**, recomputes chargeable weight from the dimensions +the rider entered, and decides routing — matching 3-digit pincode prefixes go +straight to `Out_for_Delivery` (hyperlocal), everything else routes via a hub. + +--- + +## 5. Behaviour changes that break a naive repoint + +Response shapes are completely different — flat 87- and 92-column jupiter rows +versus nested Doormile JSON. Every screen that parses a response needs +rewriting, not repointing. Beyond that: + +1. **Tenant scoping is real now.** A client console login sees only its own + tenant. `?tenantid=` narrows for Doormile staff; a client passing another + tenant's id gets **403**. Cross-tenant reads of a single resource return + **404**, not 403, so ids aren't probeable. jupiter had none of this. +2. **`configid` 1001** on every miler auth call. No jupiter equivalent. +3. **PIN reset is admin-only.** jupiter let anyone reset a rider PIN with just a + phone number, which is the login identifier, not a secret. Two calls took over + any account. The rider app must not call `/miler/reset-pin` — route resets + through ops. +4. **Identity comes from the token, never the body.** jupiter's telemetry + endpoints took `userid` from the request body. Doormile ignores it. +5. **Telemetry lat/long/speed/battery are strings**, and + `POST /miler/consignments/logs` takes a **bare JSON array**. +6. **Delivery OTP is opt-in per tenant** (`Tenant.Requiredeliveryotp`), default + off. Off for DailyGrubs. When on, it's verified server-side. +7. **Dates**: `from`/`to`, not `fromdate`/`todate`. IST wall-clock throughout. +8. **CityGate**: a booking's pickup pincode prefix must be an open city — `641` + Coimbatore, `600` Chennai, `560` Bengaluru, `500` Hyderabad, `629` Nagercoil. + jupiter had no such gate. + +--- + +## 6. Gaps — jupiter did this, Doormile does not yet + +| What | Detail | +|---|---| +| `applocationid` on reports | jupiter could filter a report by city. Doormile filters by `hubid`. Only bites when one client operates in several cities. | +| **Route optimisation** | jupiter used external paid services (`routes.workolik.com`) for multi-stop sequencing. Nothing in Doormile replaces true stop-ordering. `HubBatchAssign` decides *who* gets a booking, not *what order* to run stops in. | +| Notifications read-state | `PATCH /miler/notifications/:id/read` is a stub; no table exists. | +| `riderkms` / `ridercharges` backfill | Populated on new deliveries only. Rows completed before 2026-08-06 read 0 and will not backfill themselves. | + +--- + +## 7. Dropped on purpose + +| What | Why | +|---|---| +| `/v1/substitutions` CRUD | Rider substitutions. Low traffic in the old system; Suriya's call. Revisit if it turns out to matter. | +| jupiter's v2 endpoints | They wrote **only to Redis**, invisible to the v1/v3 Postgres reads — genuine split-brain, with a Redis `INCR` id space that could collide with the Postgres sequence. Doormile keeps Redis for ephemeral telemetry only; durable state is always Postgres. | +| `queue.workolik.com` write path | Separate host with TLS verification disabled and a hardcoded IP pin. Not reproduced. | +| ~20 never-populated columns on `orders`, 6 lat/lng pairs for 3 real points on `deliveries`, status spread across 6 text+timestamp column pairs | Replaced by a normalised schema with a real event-log table (`consignmenthistory`). | + +--- + +## 8. What is not migrated at all + +Nothing on the client side has moved. The rider Flutter app and +`doormile_express_console` still call `jupiter.nearle.app`. Doormile having the +endpoint does not mean traffic uses it. + +Suggested order: pick one net-new console screen (the rider summary, or +reports) and wire it to Doormile first — it replaces nothing live, so it is the +cheapest real proof the cutover works. Then the higher-traffic screens +(deliveries list, rider status updates), then the rider app. diff --git a/src/components/nearle_components/LocationAutocomplete.js b/src/components/nearle_components/LocationAutocomplete.js index b03d3f8..e892d43 100644 --- a/src/components/nearle_components/LocationAutocomplete.js +++ b/src/components/nearle_components/LocationAutocomplete.js @@ -1,8 +1,8 @@ import React, { forwardRef, useEffect, useState } from 'react'; import { Autocomplete, TextField, Avatar, Stack } from '@mui/material'; -import axios from 'axios'; import { MdMyLocation } from 'react-icons/md'; +import { fetchAppLocations } from 'pages/api/api'; // Pill variant — opt-in via `pill` prop. Mirrors the design used across the // deliveries / dispatch filter rows (rounded pill, soft tinted bg, accent @@ -29,16 +29,13 @@ const LocationAutocomplete = forwardRef( const [locations, setLocations] = useState(JSON.parse(localStorage.getItem('applocations') || '[]')); useEffect(() => { + // Zones are derived from GET /admin/hubs (the new API has no dedicated + // zones resource) — see fetchAppLocations in pages/api/api.js. 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); - } + const updatedLocations = await fetchAppLocations(); + localStorage.setItem('applocations', JSON.stringify(updatedLocations)); + setLocations(updatedLocations); } catch (err) { console.error('Error fetching locations in LocationAutocomplete:', err); } diff --git a/src/menu-items/nearle.js b/src/menu-items/nearle.js index a072e5f..2d82fcc 100644 --- a/src/menu-items/nearle.js +++ b/src/menu-items/nearle.js @@ -24,7 +24,14 @@ import { UserOutlined, ProfileOutlined, TeamOutlined, - MoneyCollectOutlined + MoneyCollectOutlined, + ContactsOutlined, + ShopOutlined, + CarryOutOutlined, + IdcardOutlined, + TruckOutlined, + WarningOutlined, + RadarChartOutlined } from '@ant-design/icons'; // icons @@ -50,7 +57,14 @@ const icons = { UserOutlined, ProfileOutlined, TeamOutlined, - MoneyCollectOutlined + MoneyCollectOutlined, + ContactsOutlined, + ShopOutlined, + CarryOutOutlined, + IdcardOutlined, + TruckOutlined, + WarningOutlined, + RadarChartOutlined }; // ==============================|| MENU ITEMS - SUPPORT ||============================== // @@ -103,6 +117,63 @@ const nearle = { url: '/doormile/pricing', icon: icons.MoneyCollectOutlined }, + { + id: 'customers', + title: , + type: 'item', + url: '/doormile/customers', + icon: icons.ContactsOutlined + }, + { + id: 'fleetops', + title: , + type: 'collapse', + icon: icons.TruckOutlined, + children: [ + { + id: 'hubs', + title: , + type: 'item', + url: '/doormile/hubs', + icon: icons.ShopOutlined + }, + { + id: 'vehicles', + title: , + type: 'item', + url: '/doormile/vehicles', + icon: icons.CarOutlined + }, + { + id: 'tripsheets', + title: , + type: 'item', + url: '/doormile/tripsheets', + icon: icons.CarryOutOutlined + }, + { + id: 'exceptions', + title: , + type: 'item', + url: '/doormile/exceptions', + icon: icons.WarningOutlined + }, + { + id: 'competitiveintel', + title: , + type: 'item', + url: '/doormile/competitive-intel', + icon: icons.RadarChartOutlined + }, + { + id: 'appusers', + title: , + type: 'item', + url: '/doormile/app-users', + icon: icons.IdcardOutlined + } + ] + }, { id: 'reports', title: , diff --git a/src/pages/api/CLAUDE.md b/src/pages/api/CLAUDE.md index 3a835db..fdd78bb 100644 --- a/src/pages/api/CLAUDE.md +++ b/src/pages/api/CLAUDE.md @@ -1,31 +1,33 @@ # 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. +Rules for editing `api.js` and `doormileApi.js` — the **central API layer**, every page calls into one of these two. The root `CLAUDE.md` covers project-wide conventions; this file is the scoped rule sheet for the API layer specifically. + +**The old jupiter.nearle.app backend (`REACT_APP_URL` / `REACT_APP_URL2` / `REACT_APP_URL3`) has been fully retired from this project.** Every server call now goes through `utils/doormileAxios.js` → `api.doormile.com/api/v1`, either directly from `doormileApi.js` or via the thin wrappers in `api.js`. Do not reintroduce `process.env.REACT_APP_URL*` or raw `axios` calls to `jupiter.nearle.app` — the one sanctioned exception is the AI dispatch optimiser (`routes.workolik.com` / `routemate.workolik.com`) and its final delivery commit, which are a separate solver service with no equivalent in the new API and are explicitly left untouched (see `createOptimisationDeliveries`, `reconcileSteps`, `fetchBatchEfficiency`, `createAutomationDeliveries`, `finalCreatedeliveries` in `api.js`). --- -## 1. Function signature patterns +## 1. Two files, two roles + +- **`doormileApi.js`** — one named export per `api.doormile.com/admin/*` endpoint (see `express-console-api.md` at the repo root for the full reference). Thin: build the URL, call `doormileAxios`, unwrap the envelope, return. +- **`api.js`** — the page-facing layer. Existing function names/signatures were kept stable during the migration (so page files didn't all need parallel edits) and now delegate to `doormileApi.js` internally. New pages should import directly from `doormileApi.js` unless matching an existing `queryKey`-shaped call site in `api.js`. + +## 2. 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; + const [, , startdate, enddate] = queryKey; + return (await getReports(startdate, enddate)) || {}; }; -// 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); + const [, , , , , , rowsPerPage] = queryKey; + const rows = (await getBookings(pageParam, rowsPerPage)) || []; return { - rows: response.data.details, - nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined + rows, + nextPage: rows.length === Number(rowsPerPage) ? pageParam + 1 : undefined }; }; ``` @@ -33,81 +35,67 @@ export const fetchOrders = async ({ pageParam = 1, queryKey }) => { **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. +- Many `queryKey` positions that used to carry `applocationid`/`tenantid`/`locationid`/`status` filters are now unused padding — the new API doesn't accept those as query params on most resources (see §3). Don't delete the positions; call sites still destructure by index. --- -## 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)`. +## 3. Base client and what the new API can't do -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 | +| Use | 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 | +| `doormileAxios` (via `doormileApi.js` functions) | Every `/admin/*` endpoint. Bearer token attached automatically from `localStorage.doormileToken`. | +| Hardcoded `https://routes.workolik.com` | Bike solver + reconcile-steps + batch efficiency — untouched, separate service. | +| Hardcoded `https://routemate.workolik.com` | Auto / multi-trip solver — untouched, separate service. | +| Hardcoded `https://jupiter.nearle.app` | Final `/deliveries/createdeliveries` commit only — untouched, part of the same solver pipeline. | -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. +The new API has **no equivalent** for a number of things the old jupiter backend served. Functions covering these return safe empty defaults (`null`/`[]`/`{}`) rather than throwing — match this pattern for anything new that hits the same gap: + +- Zones/`applocationid` as a filterable resource — derive a picker list from `GET /admin/hubs` instead (`fetchAppLocations` in `api.js`). +- Live rider GPS/battery/periodic logs, per-delivery GPS trail/polyline logs. +- Payment-mode types, item subcategories, rider shifts, vehicle/account types. +- Tenant tab-count summaries, per-rider/per-location report breakdowns, rider login/checkout audit logs. +- The `/substitutions` rider-absence feature (riders.js) — no backend at all. +- Invoices and expense-request approvals — no backend at all (the `requests.js` page/route is disabled, not deleted). + +Before assuming a new page's data need is covered, check `express-console-api.md` — roughly half its routes are documented as "written but unproven" against the real backend, so field names on GET responses are a best-effort guess in several places (flagged inline with comments where that's true). --- ## 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 -} +export const someFetch = async () => { + try { + return (await someDoormileApiFn()) || []; + } catch (err) { + const message = err.response?.data?.message || err.message || 'Something went wrong'; + OpenToast(message); + return []; // or null / {} — 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. +- Return a sensible empty default so the call site's destructuring doesn't crash. --- ## 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. +- **Adding a new shared GET against `/admin/*`** → put it in `doormileApi.js`. +- **Adding a one-off mutation used in only one page** → tolerated inline in that page (importing straight from `doormileApi.js`); doesn't need a new export in `api.js`. +- **Adding a polling endpoint used by multiple pages** → put it in `api.js` as a named export so the query cache keys are coherent. --- -## 6. Response shape quirks +## 6. Response shape -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. +`doormileApi.js` functions already unwrap the envelope: GET/list functions return `response.data.data` directly; mutations return the full `response.data` (`{success, data|message}`) so callers can check `res.success`. Don't assume `.details` / `.status` — those were the old jupiter envelope and no longer apply anywhere in this layer. --- ## 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. +- Several `api.js` functions ignore most of their destructured `queryKey`/arguments — the new endpoint doesn't accept those filters. This is deliberate degradation, not a bug; see §3. +- A handful of pages (`createorder1.js`, `multipleOrders.js`) synthesize fallback data client-side (e.g. a fixed 09:00–21:00 delivery-slot window) because the endpoint that used to supply it no longer exists. Look for the comment explaining why before "fixing" it. +- `notifyRider` / `notifyMiler` now take a **miler profile ID**, not an FCM token — the new API's `/admin/milers/:id/notify` looks the device token up server-side. Don't pass a token here. diff --git a/src/pages/api/api.js b/src/pages/api/api.js index cafedbf..07a7fb1 100644 --- a/src/pages/api/api.js +++ b/src/pages/api/api.js @@ -1,185 +1,226 @@ import axios from 'axios'; import { OpenToast } from 'components/third-party/OpenToast'; -import dayjs from 'dayjs'; -const userid = localStorage.getItem('userid'); +import { + getMilers, + getMiler, + getMilerSummary, + getMilerLogs, + notifyMiler, + getAdminTenants, + getAdminCustomers, + getTenantLocations, + getHubs, + getBookings, + getBooking, + assignMilerToBooking, + cancelBooking, + bulkCancelBookings, + updateConsignmentStatus, + getAdminPricing, + getPartners, + getReports, + getLocationsSummary, + getDashboard, + getProfile +} from 'pages/api/doormileApi'; -// ==============================|| getRiderPeriodicLogs ||============================== // -// Returns the rider's latest periodic log entry — battery, GPS, status, current -// order. Used by the Rider Info modal on the Dispatch page. -export const getRiderPeriodicLogs = async (userid) => { - const url = `${process.env.REACT_APP_URL}/utils/getriderperiodiclogs${userid ? `?userid=${userid}` : ''}`; - const response = await axios.get(url); - if (response.data && response.data.status) return response.data.data; - return null; +// This file talks exclusively to api.doormile.com (see utils/doormileAxios.js / +// pages/api/doormileApi.js) — the old jupiter.nearle.app backend (REACT_APP_URL / +// REACT_APP_URL2) has been fully retired from this project. See +// express-console-api.md at the repo root for the endpoint reference. +// +// Several resources the old backend exposed (payment-mode types, tenant tab +// counts, rider check-in history, per-location/per-rider report breakdowns, +// rider login/checkout audit logs, live rider GPS/battery logs) have no +// equivalent in the new API. Those functions below return safe empty defaults +// (rather than throwing) so pages render an empty state instead of crashing — +// each is commented with why. + +// The Deliveries page's STATUS_META/STATUS_TABS still use the old jupiter +// lifecycle keys (pending/accepted/arrived/picked/active/skipped/delivered/ +// cancelled) for its tabs, chip counts, and row badges. The new booking +// status enum uses different strings entirely (Pending_Pickup, +// Converted_To_Consignment, Out_for_Delivery, ...) — only Pending_Pickup, +// Converted_To_Consignment (from a real sample) and Out_for_Delivery (named +// in express-console-api.md's CityGate note) are confirmed; the rest of this +// mapping is a best-effort guess. Unmapped statuses pass through lowercased, +// which the page's own fallback renders as an "unknown" badge rather than +// crashing. There's no confirmed equivalent for arrived/picked/skipped at +// all, so those tabs will show a 0 count until the real enum is confirmed. +const BOOKING_STATUS_TO_DELIVERY_STATUS = { + pending_pickup: 'pending', + converted_to_consignment: 'accepted', + out_for_delivery: 'active', + delivered: 'delivered', + cancelled: 'cancelled' +}; +const mapBookingStatusToDeliveryStatus = (status) => { + const key = String(status || '').toLowerCase(); + return BOOKING_STATUS_TO_DELIVERY_STATUS[key] || key; }; -// ==============================|| fetchAppLocations||============================== // +// Haversine straight-line distance in km — used wherever a resource carries +// lat/lng but no road-distance field (bookings, and by extension deliveries). +const haversineKm = (lat1, lon1, lat2, lon2) => { + if (![lat1, lon1, lat2, lon2].every(Number.isFinite)) return undefined; + const R = 6371; + const toRad = (d) => (d * Math.PI) / 180; + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lon2 - lon1); + const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +}; +// ==============================|| getRiderPeriodicLogs ||============================== // +// Confirmed by jupiter2doormile.md as the direct replacement for jupiter's +// riderlogs table (Status: Done): GET /admin/milers/:id/logs. Field names on +// a log entry aren't documented anywhere (no example JSON in either +// reference doc) — this returns the most recent entry as-is and lets the +// page's own optional-chaining render whatever fields actually come back. +// Falls back to the miler record itself (which may carry a last-known +// position) if the logs call fails or returns nothing. +// Despite the parameter name (kept for caller compatibility), this must be +// a milerprofileid — /admin/milers/:id/* routes 404 on a plain userid +// (confirmed live). +export const getRiderPeriodicLogs = async (userid) => { + if (!userid) return null; + try { + const logs = await getMilerLogs(userid, undefined, undefined, 1); + const latest = Array.isArray(logs) ? logs[0] : logs; + if (latest) return latest; + } catch (err) { + console.log('getRiderPeriodicLogs', err.message); + } + try { + return (await getMiler(userid)) || null; + } catch (err) { + return null; + } +}; + +// ==============================|| fetchAppLocations (zone/location picker) ||============================== // +// The new API has no "zones" resource — applocationid only exists as a field +// on Hubs. Derive a zone picker list from the distinct cities in GET /admin/hubs. export const fetchAppLocations = async () => { - const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`); - const updatedLocations = [ - ...response.data.details, - { locationname: 'All', applocationid: 0 } // Add your new object here - ]; - - return updatedLocations; + try { + const hubs = await getHubs(); + const seen = new Map(); + (hubs || []).forEach((hub) => { + if (hub.applocationid != null && !seen.has(hub.applocationid)) { + seen.set(hub.applocationid, { applocationid: hub.applocationid, locationname: hub.city || hub.hubname }); + } + }); + return [...seen.values(), { locationname: 'All', applocationid: 0 }]; + } catch (err) { + OpenToast(err.message, 'error', 2000); + return [{ locationname: 'All', applocationid: 0 }]; + } }; // ==============================|| fetchPercentageData (orders) ||============================== // - +// Best-effort against /admin/reports — field names are our assumption since +// the doc only promises "counts + today's numbers", not an exact schema. export const fetchPercentageData = async ({ queryKey }) => { - const [, appId, startdate, enddate, tenantid, locationid] = queryKey; - const response = await axios.get( - `${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}` - ); - const details = response.data.details; - + const [, , startdate, enddate] = queryKey; + const details = (await getReports(startdate, enddate)) || {}; + const total = details.total || 0; return { - created: details.created.toString(), - uncoveredOrders: details.pending.toString(), - coveredOrders: details.delivered.toString(), - cancelled: details.cancelled.toString(), - percentage1: (Math.round((details.created / details.total) * 100) || 0).toString(), - percentage2: (Math.round((details.pending / details.total) * 100) || 0).toString(), - percentage3: (Math.round((details.delivered / details.total) * 100) || 0).toString(), - percentage4: (Math.round((details.cancelled / details.total) * 100) || 0).toString() + created: String(details.created || 0), + uncoveredOrders: String(details.pending || 0), + coveredOrders: String(details.delivered || 0), + cancelled: String(details.cancelled || 0), + percentage1: String(Math.round(((details.created || 0) / total) * 100) || 0), + percentage2: String(Math.round(((details.pending || 0) / total) * 100) || 0), + percentage3: String(Math.round(((details.delivered || 0) / total) * 100) || 0), + percentage4: String(Math.round(((details.cancelled || 0) / total) * 100) || 0) }; }; // ===================================================== || getTenants || ===================================================== -export const getTenants = async (appId) => { - const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenants/?applocationid=${appId}&status=active`); - if (response.data.status) { - let arr = []; - response.data.details.map((val) => { - arr.push({ - ...val, - label: `${val.tenantname}` - }); - }); - return arr; - } +export const getTenants = async () => { + const tenants = await getAdminTenants(); + return (tenants || []).map((val) => ({ ...val, label: val.tenantname })); }; + // ============================================= || gettenantlocations (branches) || ============================================= -export const gettenantlocations = async (appId) => { +export const gettenantlocations = async (tenantId) => { try { - const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${appId}`); - return res.data.details; + return (await getTenantLocations(tenantId)) || []; } catch (err) { console.log('gettenantlocations', err); + return []; } }; + // ==============================|| fetchorderscount (orders) ||============================== // export const fetchorderscount = async ({ queryKey }) => { - const [, appId, startdate, enddate, currentStatus, tenantid, locationid] = queryKey; - - const url = `${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}&status=${currentStatus}`; - - const response = await axios.get(url); - - return response.data.details; + const [, , startdate, enddate] = queryKey; + return (await getReports(startdate, enddate)) || {}; }; // ==============================|| fetchOrders (orders) ||============================== // - -// export const fetchOrders = async ({ queryKey }) => { -// const [, appId, currentStatus, searchword, startdate, enddate, page, rowsPerPage, tenantid, locationid] = queryKey; -// const url = `${ -// process.env.REACT_APP_URL -// }/orders/getorders/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${ -// page + 1 -// }&pagesize=${rowsPerPage}&keyword=${searchword}`; -// const response = await axios.get(url); -// return response.data.details.map((val, i) => ({ ...val, sno: i + 1 })); -// }; +// Maps to /admin/bookings. The old endpoint's applocationid/tenantid/locationid +// filters and free-text `status` are not documented on this resource — pageno/ +// pagesize are (see "Conventions" in express-console-api.md). 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}&tenantid=${tenantid}&locationid=${locationid}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&keyword=${debouncedSearch}&pageno=${pageParam}&pagesize=${rowsPerPage}`; - - const response = await axios.get(url); - + const [, , , , , , rowsPerPage] = queryKey; + const rows = (await getBookings(pageParam, rowsPerPage)) || []; return { - rows: response.data.details, - nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined + rows, + nextPage: rows.length === Number(rowsPerPage) ? pageParam + 1 : undefined }; }; // ==============================|| fetchPaymentType (orders) ||============================== // - -export const fetchPaymentType = async () => { - const { data } = await axios.get(`${process.env.REACT_APP_URL}/utils/getapptypes/?tag=paymentmode`); - return data.details.map((val) => ({ - ...val, - label: val.typename - })); -}; +// No payment-mode-types endpoint in the new API. +export const fetchPaymentType = async () => []; // ==============================|| fetchRidersList (orders) ||============================== // -export const fetchRidersList = async ({ queryKey }) => { +export const fetchRidersList = async () => { try { - const [, appId] = queryKey; // Extract appId from queryKey - const { data } = await axios.get(`${process.env.REACT_APP_URL}/partners/getriders/?applocationid=${appId}`); - console.log('data', data); - const response = data?.details - ? data?.details.map((val) => ({ - ...val, - label: `${val.firstname} ${val.lastname} | ${val.contactno}` - })) - : []; - return response; + const milers = await getMilers(); + return (milers || []).map((val) => ({ + ...val, + label: `${val.displayname || val.authname || ''} | ${val.contactno || ''}` + })); } catch (err) { OpenToast(err.message, 'error', 2000); - throw err; // 🔥 REQUIRED + throw err; } }; // ==============================|| createOptimisationDeliveries (orders) Arrange the order ||============================== // - +// AI dispatch optimiser — a separate solver service, left untouched by this +// migration (no equivalent exists in the new admin API). export const createOptimisationDeliveries = async (deliveryData) => { - // optimse the orders const response = await axios.post(`https://routes.workolik.com/api/v1/optimization/createdeliveries`, deliveryData.deliveries); return response.data; }; // ==============================|| reconcileSteps (Preview - validate rider/order step assignments) ||============================== // export const reconcileSteps = async ({ riders }) => { - const response = await axios.post( - `https://routes.workolik.com/api/v1/optimization/reconcile-steps`, - { riders } - ); + const response = await axios.post(`https://routes.workolik.com/api/v1/optimization/reconcile-steps`, { riders }); return response.data; }; // ==============================|| fetchBatchEfficiency (Dispatch - Analysis view) ||============================== // -// Calls POST /api/v1/batch/efficiency with a JSON body { batch, tenant_id }. -// `batch` is one of: 'morning' | 'afternoon' | 'evening'. export const fetchBatchEfficiency = async ({ batch, tenantId }) => { const response = await axios.post( `https://routes.workolik.com/api/v1/batch/efficiency`, - { - batch, - tenant_id: tenantId - }, - { - headers: { 'Content-Type': 'application/json' }, - // Let success:false envelopes flow through so the UI can surface the - // server's own error message (instead of axios throwing on 4xx/5xx). - validateStatus: () => true - } + { batch, tenant_id: tenantId }, + { headers: { 'Content-Type': 'application/json' }, validateStatus: () => true } ); return response.data; }; // ==============================|| finalCreatedeliveries (orders) ||============================== // - +// Final delivery commit — also part of the separate solver/optimiser pipeline, +// left untouched (still posts to jupiter.nearle.app directly, not via +// REACT_APP_URL, so this migration doesn't need to change it). export const finalCreatedeliveries = async (deliveryData) => { - // Go backend types Deliveries.userid (and rider_id) as int. Coerce at the - // boundary so any upstream string — including ones that only surface in the - // deployed build's data flow — can't cause a 500 unmarshal error. const toInt = (v) => { const n = Number(v); return Number.isFinite(n) ? n : v; @@ -189,229 +230,273 @@ export const finalCreatedeliveries = async (deliveryData) => { userid: toInt(d.userid), rider_id: toInt(d.rider_id) })); - console.log('deliveryData', deliveries); const response = await axios.post(`https://jupiter.nearle.app/live/api/v1/deliveries/createdeliveries`, deliveries); return response.data; }; // ==============================|| createAutomationDeliveries (orders) Auto rider Assign ||============================== // - +// Also part of the optimiser pipeline (routes.workolik.com / routemate.workolik.com) — untouched. export const createAutomationDeliveries = async (variables) => { - console.log('variables', variables); - const absentRiders = Array.isArray(variables.absent_riders) ? variables.absent_riders : []; - - // Bike mode (routes.workolik) historically accepted just the deliveries - // array as the body. To carry the operator's "Absent Riders" picks - // through to the AI assignment, we now wrap the body as - // { deliveries: [...], absent_riders: [...] } - // for that endpoint. Auto mode (routemate) already uses a structured - // body via `variables.data`, so we just merge absent_riders into it. const url = variables.selectedMode.value == 1 ? `https://routes.workolik.com/api/v1/optimization/riderassign?hypertuning_params=${variables.hypertuning_params}` : `https://routemate.workolik.com/api/v1/optimization/riderassign?strategy=multi_trip`; - const body = variables.selectedMode.value == 1 ? { deliveries: variables.deliveries, absent_riders: absentRiders } : { ...(variables.data || {}), absent_riders: absentRiders }; - - console.log('createAutomationDeliveries body', body); - const response = await axios.post(url, body); return response.data; }; // ==============================|| notifyRider (orders / deliveries) ||============================== // - -export const notifyRider = async (riderToken) => { - if (!riderToken) { - throw new Error('Invalid rider token'); +// New API notifies by milerprofileid + title/message (server looks up the +// device token itself) instead of the caller passing an FCM token directly. +export const notifyRider = async ( + milerProfileId, + title = 'DoormileXpress', + message = 'Orders have been placed for delivery. Kindly accept and process deliveries.' +) => { + if (!milerProfileId) { + throw new Error('Invalid miler profile id'); } - console.log('notify rider called'); - console.log('riderToken', riderToken); - const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, { - token: riderToken, - notification: { - title: 'DoormileXpress', - body: 'Orders have been placed for delivery. Kindly accept and process deliveries', - sound: 'ring', - image: '' - } - }); - return response.data; + return notifyMiler(milerProfileId, title, message); }; // ==============================|| cancelOrder (orders) ||============================== // -export const cancelOrder = async (orderheaderid) => { - const response = await axios.put(`${process.env.REACT_APP_URL}/orders/updateorder`, { - orderheaderid: orderheaderid, - orderstatus: 'cancelled', - cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss') - }); +export const cancelOrder = async (bookingId) => cancelBooking(bookingId); - return response.data; -}; // ==============================|| cancelMultipleOrder (orders) ||============================== // export const cancelMultipleOrder = async (orderlist) => { - console.log('data', orderlist); - - const data = orderlist?.map((e) => ({ - orderheaderid: e.orderheaderid, - orderstatus: 'cancelled', - cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss') - })); - - // Send request if needed - const response = await axios.put(`${process.env.REACT_APP_URL}/orders/updatemultipleorders`, data); - return response.data; + const bookingIds = (orderlist || []).map((e) => e.bookingid ?? e.id); + return bulkCancelBookings(bookingIds); }; -// ==============================|| fetchDeliveries (deliveries) ||============================== // +// ==============================|| fetchDeliveries (deliveries) ||============================== // +// GET /admin/consignments has zero documented response schema (see +// express-console-api.md — it's listed with no example body at all, unlike +// bookings). Rather than bind the deliveries table to an unknown shape, this +// builds the list from /admin/bookings (fully confirmed schema — see +// getBookings/fetchOrders in this same file) filtered to bookings that have +// actually been dispatched (assignedmileruserid or consignmentid set) — the +// Deliveries page is conceptually "orders that have moved past just being +// created", same underlying resource as the Orders page. +// +// Real consignment-only data (route step order, transit minutes, cumulative +// km, live delivery-leg status distinct from the booking's own status) has no +// confirmed field names and is NOT fabricated here — those cells render a +// "—" placeholder in the UI instead of invented numbers. If GET +// /admin/consignments' real shape is confirmed later, this is the function to +// extend with a join by booking.consignmentid. +// +// Each row is normalised onto the *old* jupiter field names the Deliveries +// page already reads (orderheaderid, deliverycustomer, ridername, etc.) so +// deliveries.js itself doesn't need a full rewrite — same compatibility-shim +// approach as fetchAllRiders' `{details}` wrapper. export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => { - let [, appId, , currentStatus, startdate, enddate, rowsPerPage, searchword, tenantid, locationid, riderid] = queryKey; - currentStatus = currentStatus == 'All' ? 'all' : currentStatus; - const url = `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?applocationid=${appId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`; - const response = await axios.get(url); + const [, , , , , , rowsPerPage] = queryKey; + const [bookings, customers, milers, tenants] = await Promise.all([ + getBookings(pageParam, rowsPerPage), + getAdminCustomers().catch(() => []), + getMilers().catch(() => []), + getAdminTenants().catch(() => []) + ]); + const customerMap = new Map((customers || []).map((c) => [c.appcustomerid ?? c.customerid ?? c.id, c])); + const milerMap = new Map((milers || []).map((m) => [m.userid ?? m.milerid, m])); + const tenantMap = new Map((tenants || []).map((t) => [t.tenantid, t])); + + const dispatched = (bookings || []).filter((b) => b.assignedmileruserid || b.consignmentid); + + const rows = dispatched.map((b) => { + const customer = customerMap.get(b.appcustomerid); + const miler = milerMap.get(b.assignedmileruserid); + const tenant = tenantMap.get(b.tenantid); + const charge = b.serviceoptions?.[0]?.estimatedprice; + return { + orderheaderid: b.bookingid, + deliveryid: b.bookingid, + orderid: b.bookingno || `#${b.bookingid}`, + consignmentid: b.consignmentid, + tenantname: tenant?.tenantname || '', + tenantsuburb: '', + applocation: '', + tenantadress: tenant?.primaryemail || '', + locationname: tenant?.tenantname || '', + locationsuburb: '', + pickupcustomer: '', + pickupcontactno: '', + Pickupaddress: b.pickupaddress || '', + pickupaddress: b.pickupaddress || '', + pickuplocation: b.pickupaddress || '', + pickupsuburb: '', + deliverycustomer: customer?.firstname || customer?.name || (b.appcustomerid ? `Customer #${b.appcustomerid}` : ''), + deliverycontactno: customer?.phone || customer?.contactno || '', + deliveryaddress: b.deliveryaddress || '', + deliverylocation: b.deliveryaddress || '', + deliverysuburb: '', + ridername: miler?.displayname || (b.assignedmileruserid ? `Rider #${b.assignedmileruserid}` : ''), + userid: b.assignedmileruserid, + // GET /admin/milers/:id/notify (and block/assign-vehicle) key off + // milerprofileid, not the userid stored on the booking — confirmed + // live (a booking's assignedmileruserid matches a miler's `userid` + // field, which 404s against /admin/milers/:id; milerprofileid is the + // real primary key of that resource). + milerprofileid: miler?.milerprofileid, + ridercontact: miler?.phone || '', + expecteddeliverytime: b.serviceoptions?.[0]?.estimateddeliveryat, + // No route-plan data source (step order/transit time/cumulative km were + // computed by the old jupiter backend from the dispatch optimiser's + // output, not stored on a booking/consignment) — left undefined so the + // UI's own "—" fallbacks render instead of a fabricated number. + transitminutes: undefined, + cumulativekms: undefined, + step: undefined, + // No road-distance field on a booking — approximated as a straight + // line between pickup and delivery coordinates. + kms: haversineKm(b.pickuplatitude, b.pickuplongitude, b.deliverylatitude, b.deliverylongitude), + pickuplatitude: b.pickuplatitude, + pickuplongitude: b.pickuplongitude, + deliverylatitude: b.deliverylatitude, + deliverylongitude: b.deliverylongitude, + deliverycharges: charge, + deliveryamt: charge, + deliveryamount: charge, + Quantity: b.parcels?.length || 0, + quantity: b.parcels?.length || 0, + collectionamt: undefined, + notes: b.notes || '', + deliverytype: customer ? 'B' : 'C', + orderdate: b.createdat, + deliverydate: b.serviceoptions?.[0]?.estimateddeliveryat || b.updatedat, + assigntime: b.updatedat, + orderstatus: mapBookingStatusToDeliveryStatus(b.status), + droplat: b.deliverylatitude, + droplon: b.deliverylongitude + }; + }); return { - rows: response.data.details, - nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined + rows, + nextPage: rows.length === Number(rowsPerPage) ? pageParam + 1 : undefined }; }; +// Shared by fetchPercentageAPI/fetchCountAPI so the chip counts are derived +// from the exact same source (and status mapping) as fetchDeliveries' rows — +// counting against the separately-guessed /admin/dashboard shape produced +// numbers that didn't match what the table actually showed. +const getDeliveryStatusCounts = async () => { + const bookings = (await getBookings(1, 1000)) || []; + const dispatched = bookings.filter((b) => b.assignedmileruserid || b.consignmentid); + const counts = { total: dispatched.length }; + dispatched.forEach((b) => { + const status = mapBookingStatusToDeliveryStatus(b.status); + counts[status] = (counts[status] || 0) + 1; + }); + return counts; +}; + // ==============================|| fetchPercentageAPI (deliveries) ||============================== // -export const fetchPercentageAPI = async (appId) => { - const url = `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}`; - const response = await axios.get(url); - - const data = response.data.details; - +export const fetchPercentageAPI = async () => { + const data = await getDeliveryStatusCounts(); return { - coveredOrders: data.delivered.toString(), - cancelledOrders: data.cancelled.toString(), - uncoveredOrders: data.pending.toString(), - assignedOrders: data.accepted.toString(), - createdOrders: data.created.toString(), - closedOrders: data.delivered.toString(), - pickedOrders: data.picked.toString(), - percentage1: (Math.round((data.pending / data.total) * 100) || 0).toString(), - percentage2: (Math.round((data.accepted / data.total) * 100) || 0).toString(), - percentage3: (Math.round((data.picked / data.total) * 100) || 0).toString(), - percentage4: (Math.round((data.delivered / data.total) * 100) || 0).toString() + coveredOrders: String(data.delivered || 0), + cancelledOrders: String(data.cancelled || 0), + uncoveredOrders: String(data.pending || 0), + assignedOrders: String(data.accepted || 0), + createdOrders: String(data.total || 0), + closedOrders: String(data.delivered || 0), + pickedOrders: String(data.picked || 0), + percentage1: String(Math.round(((data.pending || 0) / (data.total || 1)) * 100) || 0), + percentage2: String(Math.round(((data.accepted || 0) / (data.total || 1)) * 100) || 0), + percentage3: String(Math.round(((data.picked || 0) / (data.total || 1)) * 100) || 0), + percentage4: String(Math.round(((data.delivered || 0) / (data.total || 1)) * 100) || 0) }; }; // ==============================|| fetchCountAPI (deliveries) ||============================== // -export const fetchCountAPI = async (appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid) => { - const url = `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`; - const response = await axios.get(url); - const data = response.data.details; +export const fetchCountAPI = async () => { + const data = await getDeliveryStatusCounts(); return { - total: data.total, - uncoveredLength: data.pending, - assignedLength: data.accepted, - arrivedLength: data.arrived, - pickedLength: data.picked, - activeLength: data.active, - coveredLength: data.delivered, - cancelLength: data.cancelled, - skippedLength: data.skipped + total: data.total || 0, + uncoveredLength: data.pending || 0, + assignedLength: data.accepted || 0, + arrivedLength: data.arrived || 0, + pickedLength: data.picked || 0, + activeLength: data.active || 0, + coveredLength: data.delivered || 0, + cancelLength: data.cancelled || 0, + skippedLength: data.skipped || 0 }; }; // ==============================|| cancelDeliveryAPI (deliveries) ||============================== // -export const cancelDeliveryAPI = async (selectedRow, cancelFeed) => { - const payload = { - deliveryid: selectedRow.deliveryid, - orderheaderid: selectedRow.orderheaderid, - orderstatus: 'cancelled', - canceltime: dayjs().format('YYYY-MM-DD HH:mm:ss'), - feedback: cancelFeed - }; - const response = await axios.put(`${process.env.REACT_APP_URL}/deliveries/updatedelivery`, payload); - return response.data; -}; +// jupiter2doormile.md §4 confirms cancel is its own booking-level action — +// POST /admin/bookings/:id/cancel — not a consignment status PUT. cancelFeed +// isn't part of the documented cancel body; sent as a best-effort extra field +// rather than silently dropped, since it's unconfirmed whether the server +// persists it. +export const cancelDeliveryAPI = async (selectedRow, cancelFeed) => + cancelBooking(selectedRow.orderheaderid ?? selectedRow.deliveryid, { reason: cancelFeed }); + // ==============================|| getorderdetails (deliveries) ||============================== // -export const getorderdetails = async (orderHeaderid) => { - const response = await axios.get(`${process.env.REACT_APP_URL}/orders/getorderdetails?orderheaderid=${orderHeaderid}`); - return response.data; -}; +export const getorderdetails = async (orderHeaderid) => getBooking(orderHeaderid); // ==============================|| changeRiderAPI (deliveries) ||============================== // +// POST /admin/bookings/:id/assign-miler has no documented body — `milerid` as +// the JSON key is a guess (unverified, this is a write endpoint we didn't +// test live), but the VALUE must be the miler's milerprofileid regardless of +// key name: selectedRider comes straight from getMilers(), whose own .userid +// field is a different resource (confirmed live via GET /admin/milers/:id). +export const changeRiderAPI = async (selectedRider, selectedRow) => + assignMilerToBooking(selectedRow.orderheaderid ?? selectedRow.deliveryid, { milerid: selectedRider.milerprofileid }); -export const changeRiderAPI = async (selectedRider, selectedRow) => { - console.log('selectedRider', selectedRider); - console.log('selectedRow', selectedRow); - - return axios.put(`${process.env.REACT_APP_URL}/deliveries/updatedelivery`, { - userid: selectedRider.userid, - deliveryid: selectedRow.deliveryid, - orderheaderid: selectedRow.orderheaderid, - orderstatus: 'pending', - assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss') - }); -}; // ==============================|| updateDeliveryAPI (deliveries) ||============================== // - -export const updateDeliveryAPI = async (orderData) => { - return axios.put(`${process.env.REACT_APP_URL}/deliveries/updatedelivery`, orderData); -}; +// No amount/notes field exists on PUT /admin/consignments/:id/status — closest +// available write is a status update. Free-text amount/notes edits have no home +// in the new API yet. +export const updateDeliveryAPI = async (orderData) => updateConsignmentStatus(orderData.deliveryid ?? orderData.consignmentid, orderData); // ==============================|| getalltenants (tenants) ||============================== // -export const getalltenants = async ({ queryKey }) => { - const [, appId, debouncedSearch, status, page, rowsPerPage] = queryKey; +export const getalltenants = async () => { try { - let url = `${process.env.REACT_APP_URL - }/tenants/getalltenants/?status=${status}&applocationid=${appId}&keyword=${debouncedSearch}&pageno=${page + 1 - }&pagesize=${rowsPerPage}&moduleid=6`; - const response = await axios.get(url); - return response.data.details; // return only data, keep it clean + return (await getAdminTenants()) || []; } catch (err) { const message = err.response?.data?.message || err.message || 'Something went wrong'; OpenToast(message); - return null; // return null for failure + return null; } }; // ==============================|| gettenantsummary (tenants) ||============================== // +// No tenant-tab-counts endpoint in the new API. +export const gettenantsummary = async () => null; -export const gettenantsummary = async ({ queryKey }) => { - const [, appId] = queryKey; - try { - const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantsummary/?moduleid=6&applocationid=${appId}`); - return response.data.summary; // return only data, keep it clean - } catch (err) { - const message = err.response?.data?.message || err.message || 'Something went wrong'; - OpenToast(message); - return null; // return null for failure - } -}; // ==============================|| getpricinglist (tenants) ||============================== // -export const getpricinglist = async ({ queryKey }) => { - const [, appId] = queryKey; +export const getpricinglist = async () => { try { - const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/getpricinglist/?moduleid=6&applocationid=${appId}`); - return response.data.summary; // return only data, keep it clean + return (await getAdminPricing()) || []; } catch (err) { const message = err.response?.data?.message || err.message || 'Something went wrong'; OpenToast(message); - return null; // return null for failure + return null; } }; // ==============================|| getallpricing (clientPricing) ||============================== // - -export const getallpricing = async ({ queryKey }) => { - const [, appId] = queryKey; +// This is the client-facing "Pricing" page (tenant pricing rules, GET +// /admin/pricing) — not GET /admin/doormile-pricing, which is Doormile's own +// internal banding with no tenant concept. Previously pointed at +// getDoormilePricing(), so the page's "Tenant"/"Tenants Priced" columns never +// had real data behind them. +export const getallpricing = async () => { try { - const response = await axios.get(`${process.env.REACT_APP_URL}/utils/getallpricing/?applocationid=${appId}`); - return response.data.details || []; + return (await getAdminPricing()) || []; } catch (err) { const message = err.response?.data?.message || err.message || 'Something went wrong'; OpenToast(message); @@ -420,51 +505,36 @@ export const getallpricing = async ({ queryKey }) => { }; // ==============================|| fetchAllRiders (riders) ||============================== // -export const fetchAllRiders = async ({ pageParam = 1, queryKey }) => { +export const fetchAllRiders = async () => { try { - const [, appId, debouncedSearch, tabvalue] = queryKey; - - const url = `${process.env.REACT_APP_URL - }/partners/getallriders/?applocationid=${appId}&pageno=${pageParam}&pagesize=${20}&keyword=${debouncedSearch}&status=${(tabvalue == 0 || tabvalue == 2) ? '' : 'Active' - }`; - const res = await axios.get(url); - return { - details: res.data.details, - nextPage: res.data.details.length === 20 ? pageParam + 1 : undefined - }; + const milers = await getMilers(); + return { details: milers || [], nextPage: undefined }; } catch (err) { console.log('fetchAllRiders err', err.message); - return []; + return { details: [], nextPage: undefined }; } }; // ==============================|| getallridersummary (riders) ||============================== // -export const getallridersummary = async ({ queryKey }) => { +export const getallridersummary = async () => { try { - const [, appId, tabvalue] = queryKey; - const response = await axios.get( - `${process.env.REACT_APP_URL}/partners/getallridersummary/?applocationid=${appId}&status=${tabvalue == 0 ? '' : 'Active'}` - ); - return response.data.details; + const milers = (await getMilers()) || []; + // A miler has no `status` field — availabilitystatus is the real one + // (confirmed live: Available/Assigned/On_Pickup/Offline). "Active" here + // means not Offline, matching riders.js's own tab filter. + const active = milers.filter((m) => String(m.availabilitystatus || '').toLowerCase() !== 'offline').length; + return { total: milers.length, active, inactive: milers.length - active }; } catch (err) { console.log('getallridersummary err', err.message); - return []; + return { total: 0, active: 0, inactive: 0 }; } }; // ==============================|| fetchRiders (riders), active riders ||============================== // -export const fetchRiders = async ({ pageParam = 1, queryKey }) => { +export const fetchRiders = async () => { try { - const [, appId, debouncedSearch] = queryKey; - - const url = `${process.env.REACT_APP_URL - }/partners/getriders/?applocationid=${appId}&pageno=${pageParam}&pagesize=${20}&keyword=${debouncedSearch}`; - - const res = await axios.get(url); - return { - details: res.data.details, - nextPage: res.data.details.length === 20 ? pageParam + 1 : undefined - }; + const milers = await getMilers(); + return { details: milers || [], nextPage: undefined }; } catch (err) { console.log('fetchRiders err', err.message); return []; @@ -472,149 +542,121 @@ export const fetchRiders = async ({ pageParam = 1, queryKey }) => { }; // ==============================|| getriderstatus (riders)||============================== // -export const getriderstatus = async () => { - const response = await axios.get(`${process.env.REACT_APP_URL}/utils/getriderstatus`); - return response.data.data; -}; +// No check-in-history endpoint in the new API. +export const getriderstatus = async () => null; // ==============================|| getreportsummary (orders summary)||============================== // export const getreportsummary = async ({ queryKey }) => { - console.log('queryKey for getreportsummary', queryKey); - const [appId, tenantid, locationid, startdate, enddate] = queryKey; - const response = await axios.get( - `${process.env.REACT_APP_URL}/deliveries/getreportsummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}` - ); - console.log('getreportsummary', response.data.details); - - return response.data.details; + const [, tenantid, locationid, startdate, enddate] = queryKey; + return (await getReports(startdate, enddate, tenantid, locationid)) || {}; }; // ==============================|| getreportlocationsummary (orders summary)||============================== // +// Confirmed against jupiter2doormile.md (Status: Done) as the replacement for +// jupiter's getlocationsummary — GET /admin/locations/summary. export const getreportlocationsummary = async ({ queryKey }) => { - console.log('queryKey for getreportlocationsummary', queryKey); - const [appId, tenantid, locationid, startdate, enddate] = queryKey; - const response = await axios.get( - `${process.env.REACT_APP_URL}/deliveries/getreportlocationsummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}` - ); - console.log('getreportlocationsummary', response.data.details); - - return response.data.details; + const [, tenantid, locationid, startdate, enddate] = queryKey; + return (await getLocationsSummary(tenantid, locationid, startdate, enddate)) || []; }; + // ==============================|| fetchorderdetails (orders detail)||============================== // export const fetchorderdetails = async ({ queryKey }) => { - console.log('queryKey of fetchorderdetails', queryKey); - - const [appId, startdate, enddate, page, rowsPerPage] = queryKey; - - const response = await axios.get( - `${process.env.REACT_APP_URL2}/orders/getorders/?fromdate=${startdate}&todate=${enddate}&applocationid=${appId}&pageno=${page}&pagesize=${rowsPerPage}` - ); - const detailsWithSNo = response.data.details.map((item, index) => ({ - ...item, - sno: index + 1 - })); - console.log('fetchorderdetails', detailsWithSNo); - return detailsWithSNo; + const [, , , page, rowsPerPage] = queryKey; + const rows = (await getBookings(page, rowsPerPage)) || []; + return rows.map((item, index) => ({ ...item, sno: index + 1 })); }; // ==============================|| getriderbydelivery (orders detail)||============================== // - -export const getriderbydelivery = async (startdate, enddate, appId = 0, tenantid = 0, locationid = 0) => { - // const [, startdate, enddate] = queryKey; - - try { - const response = await axios.get( - `${process.env.REACT_APP_URL}/deliveries/getriderbydelivery/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}` - ); - return response.data.details || []; - } catch (err) { - console.log('getriderbydelivery', err.message); - return []; - } -}; +// No per-rider-delivery-trace endpoint in the new API. +export const getriderbydelivery = async () => []; // ==============================|| fetchCount (orders detail)||============================== // -export const fetchCount = async ({ queryKey }) => { - console.log('queryKey of fetchCount', queryKey); - const [appId, startdate, enddate] = queryKey; - let url = - appId == 0 - ? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?fromdate=${startdate}&todate=${enddate}` - : `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}`; - - const response = await axios.get(url); - return response.data.details; -}; +export const fetchCount = async () => (await getDashboard()) || {}; // ==============================|| fetchRidersSummary (riders summary)||============================== // - +// No per-rider performance summary endpoint in the new API. +// Confirmed against jupiter2doormile.md (Status: Done) as the replacement +// for jupiter's getridersummary — GET /admin/milers/summary. Per-rider row +// field names aren't documented anywhere; this passes the response through +// as-is and lets the page's own rendering degrade gracefully on unknown keys. export const fetchRidersSummary = async ({ queryKey }) => { - console.log('queryKey for fetchRidersSummary', queryKey); const [, appId, startdate, enddate] = queryKey; - const response = await axios.get( - `${process.env.REACT_APP_URL}/deliveries/getridersummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}` - ); - console.log('fetchRidersSummary', response.data.details); - - return response.data.details; + const data = await getMilerSummary(appId || undefined, startdate, enddate); + return Array.isArray(data) ? data : data ? [data] : []; }; // ==============================|| fetchLocations (orders summary))||============================== // export const fetchLocations = async () => { - const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getpartners`); - const updatedLocations = [ - ...response.data.details, - { partnername: 'All', partnerid: -1 } // Add your new object here - ]; - console.log('fetchLocations', updatedLocations); - return updatedLocations; + const partners = (await getPartners()) || []; + return [...partners, { partnername: 'All', partnerid: -1 }]; }; // ==============================|| fetchRidersLogs (RiderLogs)||============================== // - -export const fetchRidersLogs = async ({ queryKey }) => { - const [appId, startdate, riderSearch = ''] = queryKey; - const riderLogsResponse = await axios.get( - `${process.env.REACT_APP_URL2}/partners/getriderlogs/?applocationid=${appId}&fromdate=${startdate || ''}&todate=${startdate}&keyword=${riderSearch || '' - } ` - ); - console.log('fetchRidersLogs', riderLogsResponse.data.details); - return riderLogsResponse.data.details; +// No rider login/checkout audit-log endpoint in the new API. +// Was a permanent [] stub ("no rider login/checkout audit-log endpoint") — +// that was true for the jupiter audit-log concept this was originally named +// after, but jupiter2doormile.md confirms (Status: Done) that +// GET /admin/milers/summary carries live position (currentlatitude/ +// currentlongitude/lastpingat per that doc) for every rider at a zone. This +// feeds Dispatch.js's live map pins and "All Active Routes" view, which were +// both silently dead with the old stub. +// +// GET /admin/milers/summary confirmed live: { milerprofileid, userid, +// displayname, phone, availabilitystatus, defaultvehicletype, hubid, +// hubname, rating, onduty, dutystartedat, currentlatitude, currentlongitude, +// lastlocationupdatedat, lastpingat, assigned, accepted, rejected, +// completed, cancelled, delivered, riderkms, ridercharges } — no `status`, +// `contactno`, or current-order-id field exists at all. `id` is exposed as +// an alias of milerprofileid, which is what every /admin/milers/:id/* route +// (notify, block, assign-vehicle, logs) actually keys off — not userid. +export const fetchRidersLogs = async ({ queryKey } = {}) => { + const [appId] = queryKey || []; + try { + const data = await getMilerSummary(appId || undefined); + const riders = Array.isArray(data) ? data : data?.riders || data?.milers || []; + return riders.map((r) => ({ + id: r.milerprofileid, + milerprofileid: r.milerprofileid, + userid: r.userid, + username: r.displayname, + status: r.availabilitystatus, + contactno: r.phone, + orderid: undefined, + logdate: r.lastpingat ?? r.lastlocationupdatedat, + latitude: r.currentlatitude, + longitude: r.currentlongitude + })); + } catch (err) { + console.log('fetchRidersLogs', err.message); + return []; + } }; // ==============================|| getorders (Locations)||============================== // -// fetchOrders.js export const fetchOrders1 = async ({ pageParam = 1, queryKey }) => { - const [, tenantid, locationid, status, startdate, enddate, searchword, rowsPerPage] = queryKey; - - const res = await axios.get( - `${process.env.REACT_APP_URL}/orders/tenant/getorders/?tenantid=${tenantid}&locationid=${locationid}&status=${status}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}` - ); - - return { - details: res.data.details, - nextPage: res.data.details.length === rowsPerPage ? pageParam + 1 : undefined - }; + const [, , , , , , , rowsPerPage] = queryKey; + const rows = (await getBookings(pageParam, rowsPerPage)) || []; + return { details: rows, nextPage: rows.length === rowsPerPage ? pageParam + 1 : undefined }; }; // ==============================|| getusers (viewProfile)||============================== // export const getusers = async () => { try { - const res = await axios.get(`${process.env.REACT_APP_URL}/users/getusers/?configid=9&userid=${userid}`); - return res.data.details; + return (await getProfile()) || {}; } catch (err) { console.log('getusers', err.message); + return {}; } }; // ==============================|| getallriders (order)||============================== // export const getallriders = async () => { try { - const res = await axios.get(`${process.env.REACT_APP_URL}/partners/getallriders?partnerid=64`); - return res.data.details; + return (await getMilers()) || []; } catch (err) { console.log('getallriders', err.message); + return []; } }; diff --git a/src/pages/api/doormileApi.js b/src/pages/api/doormileApi.js new file mode 100644 index 0000000..0aefc7a --- /dev/null +++ b/src/pages/api/doormileApi.js @@ -0,0 +1,557 @@ +import doormileAxios, { DOORMILE_TOKEN_KEY, DOORMILE_USER_KEY } from 'utils/doormileAxios'; + +// API layer for the new Doormile Express admin backend (api.doormile.com/api/v1/admin/*). +// Mirrors the shape of pages/api/api.js (named async exports, one per endpoint) but talks +// to a different backend with a different envelope: { success, data } / { success, message }. +// See express-console-api.md at the repo root for the full reference this was built from. +// Not wired into any page yet — this is plumbing only. + +const buildQuery = (params = {}) => { + const usp = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') usp.append(key, value); + }); + const qs = usp.toString(); + return qs ? `?${qs}` : ''; +}; + +// ==============================|| Auth ||============================== // + +export const loginAdmin = async (email, password) => { + const response = await doormileAxios.post('/admin/login', { email, password }); + if (response.data.success) { + localStorage.setItem(DOORMILE_TOKEN_KEY, response.data.token); + localStorage.setItem(DOORMILE_USER_KEY, JSON.stringify(response.data.user)); + } + return response.data; +}; + +export const logoutAdmin = () => { + localStorage.removeItem(DOORMILE_TOKEN_KEY); + localStorage.removeItem(DOORMILE_USER_KEY); +}; + +// ==============================|| Dashboard, profile, reports ||============================== // + +export const getDashboard = async () => { + const response = await doormileAxios.get('/admin/dashboard'); + return response.data.data; +}; + +// jupiter used fromdate/todate + applocationid (city); Doormile uses from/to +// and no city filter at all (filters by hubid instead — see +// jupiter2doormile.md §2.3/§6). Response carries by_location/by_hub/by_tenant/ +// by_rider blocks per that doc, not documented in express-console-api.md. +export const getReports = async (from, to, tenantid, locationid, hubid) => { + const response = await doormileAxios.get(`/admin/reports${buildQuery({ from, to, tenantid, locationid, hubid })}`); + return response.data.data; +}; + +// Confirmed against jupiter2doormile.md (Status: Done) as the replacement for +// jupiter's getlocationsummary — per-tenant-site report breakdown. Not in +// express-console-api.md. +export const getLocationsSummary = async (tenantid, locationid, from, to) => { + const response = await doormileAxios.get(`/admin/locations/summary${buildQuery({ tenantid, locationid, from, to })}`); + return response.data.data; +}; + +export const getProfile = async () => { + const response = await doormileAxios.get('/admin/profile'); + return response.data.data; +}; + +export const updateProfilePassword = async (current_password, new_password) => { + const response = await doormileAxios.put('/admin/profile/password', { current_password, new_password }); + return response.data; +}; + +// ==============================|| App users (staff logins) ||============================== // + +export const getAppUsers = async () => { + const response = await doormileAxios.get('/admin/users'); + return response.data.data; +}; + +export const createAppUser = async (data) => { + const response = await doormileAxios.post('/admin/users', data); + return response.data; +}; + +export const updateAppUser = async (id, data) => { + const response = await doormileAxios.put(`/admin/users/${id}`, data); + return response.data; +}; + +export const deleteAppUser = async (id) => { + const response = await doormileAxios.delete(`/admin/users/${id}`); + return response.data; +}; + +// ==============================|| Partners (fleet / rider suppliers) ||============================== // + +export const getPartners = async () => { + const response = await doormileAxios.get('/admin/partners'); + return response.data.data; +}; + +export const createPartner = async (data) => { + const response = await doormileAxios.post('/admin/partners', data); + return response.data; +}; + +export const getPartner = async (id) => { + const response = await doormileAxios.get(`/admin/partners/${id}`); + return response.data.data; +}; + +export const updatePartner = async (id, data) => { + const response = await doormileAxios.put(`/admin/partners/${id}`, data); + return response.data; +}; + +export const deletePartner = async (id) => { + const response = await doormileAxios.delete(`/admin/partners/${id}`); + return response.data; +}; + +// ==============================|| Tenants (client companies) ||============================== // + +export const getAdminTenants = async () => { + const response = await doormileAxios.get('/admin/tenants'); + return response.data.data; +}; + +export const createAdminTenant = async (data) => { + const response = await doormileAxios.post('/admin/tenants', data); + return response.data; +}; + +export const getAdminTenant = async (id) => { + const response = await doormileAxios.get(`/admin/tenants/${id}`); + return response.data.data; +}; + +export const updateAdminTenant = async (id, data) => { + // requiredeliveryotp is a pointer server-side — omit the key entirely to leave the setting alone. + const response = await doormileAxios.put(`/admin/tenants/${id}`, data); + return response.data; +}; + +export const deleteAdminTenant = async (id) => { + const response = await doormileAxios.delete(`/admin/tenants/${id}`); + return response.data; +}; + +export const getTenantLocations = async (tenantId) => { + const response = await doormileAxios.get(`/admin/tenants/${tenantId}/locations`); + return response.data.data; +}; + +export const createTenantLocation = async (tenantId, data) => { + const response = await doormileAxios.post(`/admin/tenants/${tenantId}/locations`, data); + return response.data; +}; + +export const updateTenantLocation = async (locationId, data) => { + // Not nested under the tenant — PUT /admin/tenantlocations/:id. + const response = await doormileAxios.put(`/admin/tenantlocations/${locationId}`, data); + return response.data; +}; + +// ==============================|| Tenant customers (a client's own end customers) ||============================== // + +export const getTenantCustomers = async () => { + const response = await doormileAxios.get('/admin/tenantcustomers'); + return response.data.data; +}; + +export const createTenantCustomer = async (data) => { + const response = await doormileAxios.post('/admin/tenantcustomers', data); + return response.data; +}; + +export const getTenantCustomer = async (id) => { + const response = await doormileAxios.get(`/admin/tenantcustomers/${id}`); + return response.data.data; +}; + +export const updateTenantCustomer = async (id, data) => { + const response = await doormileAxios.put(`/admin/tenantcustomers/${id}`, data); + return response.data; +}; + +export const deleteTenantCustomer = async (id) => { + const response = await doormileAxios.delete(`/admin/tenantcustomers/${id}`); + return response.data; +}; + +// ==============================|| B2C app customers ||============================== // + +export const getAdminCustomers = async () => { + const response = await doormileAxios.get('/admin/customers'); + return response.data.data; +}; + +export const updateAdminCustomer = async (id, data) => { + const response = await doormileAxios.patch(`/admin/customers/${id}`, data); + return response.data; +}; + +// ==============================|| Hubs ||============================== // + +export const getHubs = async () => { + const response = await doormileAxios.get('/admin/hubs'); + return response.data.data; +}; + +export const createHub = async (data) => { + const response = await doormileAxios.post('/admin/hubs', data); + return response.data; +}; + +export const getHub = async (id) => { + const response = await doormileAxios.get(`/admin/hubs/${id}`); + return response.data.data; +}; + +export const updateHub = async (id, data) => { + const response = await doormileAxios.put(`/admin/hubs/${id}`, data); + return response.data; +}; + +export const deleteHub = async (id) => { + // Soft delete server-side. + const response = await doormileAxios.delete(`/admin/hubs/${id}`); + return response.data; +}; + +// ==============================|| Vehicles ||============================== // + +export const getVehicles = async () => { + const response = await doormileAxios.get('/admin/vehicles'); + return response.data.data; +}; + +export const createVehicle = async (data) => { + const response = await doormileAxios.post('/admin/vehicles', data); + return response.data; +}; + +export const getVehicle = async (id) => { + const response = await doormileAxios.get(`/admin/vehicles/${id}`); + return response.data.data; +}; + +export const updateVehicle = async (id, data) => { + const response = await doormileAxios.put(`/admin/vehicles/${id}`, data); + return response.data; +}; + +export const deleteVehicle = async (id) => { + // Soft delete server-side. + const response = await doormileAxios.delete(`/admin/vehicles/${id}`); + return response.data; +}; + +// ==============================|| Milers (riders) ||============================== // + +export const getMilers = async () => { + // Tenant-scoped server-side from the token — no tenantid param needed/accepted. + const response = await doormileAxios.get('/admin/milers'); + return response.data.data; +}; + +export const createMiler = async (data) => { + // configid defaults to 1001 server-side (the miler-app login partition) — do not override it. + const response = await doormileAxios.post('/admin/milers', data); + return response.data; +}; + +export const getMiler = async (id) => { + const response = await doormileAxios.get(`/admin/milers/${id}`); + return response.data.data; +}; + +export const updateMiler = async (id, data) => { + const response = await doormileAxios.put(`/admin/milers/${id}`, data); + return response.data; +}; + +export const blockMiler = async (id, data) => { + const response = await doormileAxios.put(`/admin/milers/${id}/block`, data); + return response.data; +}; + +export const assignMilerVehicle = async (id, data) => { + const response = await doormileAxios.put(`/admin/milers/${id}/assign-vehicle`, data); + return response.data; +}; + +export const notifyMiler = async (milerProfileId, title, message) => { + const response = await doormileAxios.post(`/admin/milers/${milerProfileId}/notify`, { title, message }); + return response.data; +}; + +// Confirmed against jupiter2doormile.md (Status: Done — tested against a real +// request), not in express-console-api.md. jupiter used fromdate/todate; +// Doormile uses from/to (both YYYY-MM-DD). Aggregate rider KPI counts + +// current position (currentlatitude/currentlongitude/lastpingat per the doc — +// unconfirmed beyond those three field names). +export const getMilerSummary = async (applocationid, from, to, tenantid, hubid) => { + const response = await doormileAxios.get(`/admin/milers/summary${buildQuery({ applocationid, from, to, tenantid, hubid })}`); + return response.data.data; +}; + +// Replaces jupiter's `riderlogs` table — the rider's GPS/status ping history. +export const getMilerLogs = async (id, from, to, limit) => { + const response = await doormileAxios.get(`/admin/milers/${id}/logs${buildQuery({ from, to, limit })}`); + return response.data.data; +}; + +// No jupiter equivalent — new in Doormile. +export const getMilerActivity = async (id, from, to) => { + const response = await doormileAxios.get(`/admin/milers/${id}/activity${buildQuery({ from, to })}`); + return response.data.data; +}; + +// ==============================|| Bookings ||============================== // + +export const getBookings = async (pageno, pagesize) => { + const response = await doormileAxios.get(`/admin/bookings${buildQuery({ pageno, pagesize })}`); + return response.data.data; +}; + +export const createExpressBooking = async (data) => { + // tenantid is forced to the caller's own tenant server-side on client logins. + // CityGate applies: pickup pincode prefix must be an open city (641/600/560/500/629). + const response = await doormileAxios.post('/admin/expressbooking', data); + return response.data; +}; + +export const createExpressBookingBulk = async (bookings) => { + // Max 200 per call; response carries per-row results. + const response = await doormileAxios.post('/admin/expressbooking/bulk', { bookings }); + return response.data; +}; + +export const getBooking = async (id) => { + const response = await doormileAxios.get(`/admin/bookings/${id}`); + return response.data.data; +}; + +export const assignMilerToBooking = async (id, data) => { + const response = await doormileAxios.post(`/admin/bookings/${id}/assign-miler`, data); + return response.data; +}; + +export const assignVehicleToBooking = async (id, data) => { + const response = await doormileAxios.post(`/admin/bookings/${id}/assign-vehicle`, data); + return response.data; +}; + +export const updateBookingStatus = async (id, data) => { + const response = await doormileAxios.put(`/admin/bookings/${id}/status`, data); + return response.data; +}; + +export const cancelBooking = async (id, data) => { + const response = await doormileAxios.post(`/admin/bookings/${id}/cancel`, data); + return response.data; +}; + +export const bulkCancelBookings = async (bookingIds) => { + const response = await doormileAxios.post('/admin/bookings/bulk-cancel', { bookingIds }); + return response.data; +}; + +// Confirmed against jupiter2doormile.md (Status: Done), not in +// express-console-api.md. +export const getBookingTrack = async (id) => { + const response = await doormileAxios.get(`/admin/bookings/${id}/track`); + return response.data.data; +}; + +// ==============================|| Consignments ||============================== // + +export const getConsignments = async () => { + const response = await doormileAxios.get('/admin/consignments'); + return response.data.data; +}; + +// Confirmed against jupiter2doormile.md (Status: Done) — the actual GPS/status +// trail for a consignment, not in express-console-api.md. +export const getConsignmentLogs = async (id) => { + const response = await doormileAxios.get(`/admin/consignments/${id}/logs`); + return response.data.data; +}; + +export const getConsignment = async (id) => { + const response = await doormileAxios.get(`/admin/consignments/${id}`); + return response.data.data; +}; + +export const trackConsignment = async (trackingno) => { + const response = await doormileAxios.get(`/admin/consignments/track/${trackingno}`); + return response.data.data; +}; + +export const updateConsignmentStatus = async (id, data) => { + const response = await doormileAxios.put(`/admin/consignments/${id}/status`, data); + return response.data; +}; + +// ==============================|| Tripsheets (hub-to-hub transport) ||============================== // + +export const getTripsheets = async () => { + const response = await doormileAxios.get('/admin/tripsheets'); + return response.data.data; +}; + +export const createTripsheet = async (data) => { + const response = await doormileAxios.post('/admin/tripsheets', data); + return response.data; +}; + +export const getTripsheet = async (id) => { + const response = await doormileAxios.get(`/admin/tripsheets/${id}`); + return response.data.data; +}; + +export const addTripsheetItem = async (id, consignmentid) => { + const response = await doormileAxios.post(`/admin/tripsheets/${id}/items`, { consignmentid }); + return response.data; +}; + +export const removeTripsheetItem = async (id, itemId) => { + const response = await doormileAxios.delete(`/admin/tripsheets/${id}/items/${itemId}`); + return response.data; +}; + +export const dispatchTripsheet = async (id) => { + const response = await doormileAxios.put(`/admin/tripsheets/${id}/dispatch`); + return response.data; +}; + +export const arriveTripsheet = async (id) => { + const response = await doormileAxios.put(`/admin/tripsheets/${id}/arrive`); + return response.data; +}; + +// ==============================|| Pricing ||============================== // + +export const getAdminPricing = async () => { + const response = await doormileAxios.get('/admin/pricing'); + return response.data.data; +}; + +export const createAdminPricing = async (data) => { + const response = await doormileAxios.post('/admin/pricing', data); + return response.data; +}; + +export const updateAdminPricing = async (id, data) => { + const response = await doormileAxios.put(`/admin/pricing/${id}`, data); + return response.data; +}; + +export const deleteAdminPricing = async (id) => { + const response = await doormileAxios.delete(`/admin/pricing/${id}`); + return response.data; +}; + +export const simulatePricing = async (data) => { + const response = await doormileAxios.post('/admin/pricing/simulate', data); + return response.data; +}; + +export const quotePricing = async (data) => { + // Same handler as simulate. + const response = await doormileAxios.post('/admin/pricing/quote', data); + return response.data; +}; + +export const getDoormilePricing = async () => { + const response = await doormileAxios.get('/admin/doormile-pricing'); + return response.data.data; +}; + +export const createDoormilePricing = async (data) => { + const response = await doormileAxios.post('/admin/doormile-pricing', data); + return response.data; +}; + +export const updateDoormilePricing = async (id, data) => { + const response = await doormileAxios.put(`/admin/doormile-pricing/${id}`, data); + return response.data; +}; + +export const deleteDoormilePricing = async (id) => { + // Soft delete server-side. + const response = await doormileAxios.delete(`/admin/doormile-pricing/${id}`); + return response.data; +}; + +// ==============================|| Exceptions ||============================== // + +export const getExceptions = async () => { + const response = await doormileAxios.get('/admin/exceptions'); + return response.data.data; +}; + +export const createException = async (data) => { + const response = await doormileAxios.post('/admin/exceptions', data); + return response.data; +}; + +export const getException = async (id) => { + const response = await doormileAxios.get(`/admin/exceptions/${id}`); + return response.data.data; +}; + +export const updateExceptionStatus = async (id, status, resolution) => { + // status: 'Resolved' | 'Closed'. + const response = await doormileAxios.put(`/admin/exceptions/${id}/status`, { status, resolution }); + return response.data; +}; + +// ==============================|| Competitive intel ||============================== // + +export const getCompetitorBranches = async () => { + const response = await doormileAxios.get('/admin/competitor-branches'); + return response.data.data; +}; + +export const createCompetitorBranch = async (data) => { + const response = await doormileAxios.post('/admin/competitor-branches', data); + return response.data; +}; + +export const updateCompetitorBranch = async (id, data) => { + const response = await doormileAxios.put(`/admin/competitor-branches/${id}`, data); + return response.data; +}; + +export const deleteCompetitorBranch = async (id) => { + const response = await doormileAxios.delete(`/admin/competitor-branches/${id}`); + return response.data; +}; + +export const getCarrierPricing = async () => { + const response = await doormileAxios.get('/admin/carrier-pricing'); + return response.data.data; +}; + +export const createCarrierPricing = async (data) => { + const response = await doormileAxios.post('/admin/carrier-pricing', data); + return response.data; +}; + +export const updateCarrierPricing = async (id, data) => { + const response = await doormileAxios.put(`/admin/carrier-pricing/${id}`, data); + return response.data; +}; + +export const deleteCarrierPricing = async (id) => { + const response = await doormileAxios.delete(`/admin/carrier-pricing/${id}`); + return response.data; +}; diff --git a/src/pages/nearle/clientPricing/clientPricing.js b/src/pages/nearle/clientPricing/clientPricing.js index 46b75f3..5f0b9db 100644 --- a/src/pages/nearle/clientPricing/clientPricing.js +++ b/src/pages/nearle/clientPricing/clientPricing.js @@ -1,9 +1,19 @@ import React, { useMemo, useRef, useState } from 'react'; import { + Autocomplete, Avatar, Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, Grid, + IconButton, + InputLabel, + MenuItem, Paper, + Select, Stack, Table, TableBody, @@ -11,6 +21,8 @@ import { TableContainer, TableHead, TableRow, + TextField, + Tooltip, Typography, useMediaQuery, useTheme @@ -18,19 +30,21 @@ import { import { MdLocalOffer, MdMyLocation, - MdAttachMoney, MdGroups, MdPlace, MdSpeed, MdPriceCheck, - MdStraighten, MdReceiptLong, MdOutlineLocalOffer, MdOutlineGroups, MdOutlineAttachMoney, - MdOutlinePlace + MdOutlinePlace, + MdAdd, + MdEdit, + MdDeleteOutline, + MdCalculate } from 'react-icons/md'; -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import Loader from 'components/Loader'; import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; @@ -39,7 +53,9 @@ import PageHeader from 'components/nearle_components/PageHeader'; import StatCard from 'components/nearle_components/StatCard'; import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; -import { getallpricing } from 'pages/api/api'; +import { getallpricing, getTenants, fetchAppLocations } from 'pages/api/api'; +import { createAdminPricing, updateAdminPricing, deleteAdminPricing, simulatePricing } from 'pages/api/doormileApi'; +import { enqueueSnackbar } from 'notistack'; // ============================================================================ // Design tokens — shared with the deliveries / tenants / customers pages so every @@ -70,6 +86,7 @@ const ring = (c) => a(c, '26'); const edge = (c) => a(c, '55'); const BRAND = '#662582'; +const VEHICLE_TYPES = ['Bike', 'Scooter', 'Bicycle', 'Car', 'Van']; const SoftPaper = (props) => ( minimumFractionDigits: 2 }).format(Number(value) || 0); -const formatDecimal = (value) => - new Intl.NumberFormat('en-IN', { minimumFractionDigits: 2 }).format(Number(value) || 0); - -// Numeric table value — plain, strong, right-readable text. The old version -// wrapped every cell in a coloured bordered pill, which made the table read -// like a rainbow; corporate data tables keep figures as quiet typography and -// let the column header carry the meaning. const MetricPill = ({ label }) => ( ( ); -// Subtle neutral category chip (zone / slab) — one quiet style, muted icon. const CategoryChip = ({ icon, label }) => ( ( ); +const emptyForm = { + pricingid: null, + tenant: null, + location: null, + vehicletype: 'Bike', + baseprice: '', + baseweight: '', + priceperkg: '', + basedistance: '', + priceperkm: '', + handlingcharges: '', + priority: 1, + status: 'active' +}; + // ==============================|| Pricing page ||============================== // const ClientsPricing = () => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); const containerRef = useRef(); + const queryClient = useQueryClient(); const [appId, setAppId] = useState(0); const [locaName, setLocoName] = useState('All'); const [searchword, setSearchword] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); + const [dialogOpen, setDialogOpen] = useState(false); + const [form, setForm] = useState(emptyForm); + const [quoteOpen, setQuoteOpen] = useState(false); + const [quoteForm, setQuoteForm] = useState({ tenant: null, location: null, vehicletype: 'Bike', weight: '', distance: '' }); - const { - data: pricing = [], - isLoading - } = useQuery({ + const { data: pricing = [], isLoading } = useQuery({ queryKey: ['getallpricing', appId], queryFn: getallpricing, keepPreviousData: true }); + const { data: tenants = [] } = useQuery({ queryKey: ['pricing-tenants'], queryFn: getTenants }); + const { data: locations = [] } = useQuery({ queryKey: ['pricing-locations'], queryFn: fetchAppLocations }); + + const tenantMap = useMemo(() => new Map((tenants || []).map((t) => [t.tenantid, t])), [tenants]); + const locationMap = useMemo( + () => new Map((locations || []).filter((l) => l.applocationid).map((l) => [l.applocationid, l])), + [locations] + ); + const locationOptions = useMemo(() => (locations || []).filter((l) => l.applocationid), [locations]); + + const tenantName = (row) => row.tenantname || tenantMap.get(row.tenantid)?.tenantname || (row.tenantid ? `Tenant #${row.tenantid}` : '—'); + const zoneName = (row) => + row.applocation || locationMap.get(row.applocationid)?.locationname || (row.applocationid ? `Zone #${row.applocationid}` : '—'); + 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)] + [tenantName(row), row.vehicletype, zoneName(row), String(row.pricingid)] .filter(Boolean) .some((field) => String(field).toLowerCase().includes(q)) ); - }, [pricing, debouncedSearch]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pricing, debouncedSearch, tenantMap, locationMap]); 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 }; + const tenantsPriced = new Set(pricing.map((r) => r.tenantid).filter(Boolean)).size; + const avgBase = total ? pricing.reduce((sum, r) => sum + (Number(r.baseprice) || 0), 0) / total : 0; + const activeCount = pricing.filter((r) => String(r.status).toLowerCase() === 'active').length; + return { total, tenantsPriced, avgBase, activeCount }; }, [pricing]); const KPI_META = [ - { key: 'total', label: 'Total Pricing Slabs', color: BRAND, icon: MdOutlineLocalOffer, value: stats.total }, - { key: 'tenants', label: 'Tenants Priced', color: '#0ea5e9', icon: MdOutlineGroups, value: stats.tenants }, + { key: 'total', label: 'Total Pricing Rules', color: BRAND, icon: MdOutlineLocalOffer, value: stats.total }, + { key: 'tenants', label: 'Tenants Priced', color: '#0ea5e9', icon: MdOutlineGroups, value: stats.tenantsPriced }, { key: 'avg', label: 'Avg Base Price', color: '#f59e0b', icon: MdOutlineAttachMoney, value: formatRupees(stats.avgBase) }, - { key: 'zone', label: 'Active Zone', color: '#10b981', icon: MdOutlinePlace, value: locaName || 'All Zones' } + { key: 'active', label: 'Active Rules', color: '#10b981', icon: MdOutlinePlace, value: stats.activeCount } ]; + const opentoast = (message, variant = 'error') => { + enqueueSnackbar(message, { variant, anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); + }; + + const savePricingMutation = useMutation({ + mutationFn: (payload) => (payload.pricingid ? updateAdminPricing(payload.pricingid, payload.data) : createAdminPricing(payload.data)), + onSuccess: (res) => { + if (res.success) { + opentoast(form.pricingid ? 'Pricing rule updated' : 'Pricing rule created', 'success'); + queryClient.invalidateQueries({ queryKey: ['getallpricing'] }); + setDialogOpen(false); + setForm(emptyForm); + } else { + opentoast(res.message || 'Failed to save pricing rule'); + } + }, + onError: (err) => opentoast(err.response?.data?.message || err.message || 'Failed to save pricing rule') + }); + + const deletePricingMutation = useMutation({ + mutationFn: (id) => deleteAdminPricing(id), + onSuccess: (res) => { + if (res.success) { + opentoast('Pricing rule deleted', 'success'); + queryClient.invalidateQueries({ queryKey: ['getallpricing'] }); + } else { + opentoast(res.message || 'Failed to delete pricing rule'); + } + }, + onError: (err) => opentoast(err.response?.data?.message || err.message || 'Failed to delete pricing rule') + }); + + // POST /admin/pricing/simulate — request/response shape isn't documented + // beyond the endpoint existing ("same handler as quote"). Rendered + // defensively below rather than assuming a specific result field name. + const quoteMutation = useMutation({ + mutationFn: (data) => simulatePricing(data), + onError: (err) => opentoast(err.response?.data?.message || err.message || 'Failed to get quote') + }); + + const handleQuote = () => { + if (!quoteForm.tenant?.tenantid || !quoteForm.distance) { + opentoast('Choose Tenant and fill Distance'); + return; + } + quoteMutation.mutate({ + tenantid: quoteForm.tenant.tenantid, + applocationid: quoteForm.location?.applocationid, + vehicletype: quoteForm.vehicletype, + weight: Number(quoteForm.weight) || 0, + distance: Number(quoteForm.distance) || 0 + }); + }; + + const openCreateDialog = () => { + setForm(emptyForm); + setDialogOpen(true); + }; + + const openEditDialog = (row) => { + setForm({ + pricingid: row.pricingid, + tenant: tenantMap.get(row.tenantid) || (row.tenantid ? { tenantid: row.tenantid, tenantname: tenantName(row) } : null), + location: locationMap.get(row.applocationid) || null, + vehicletype: row.vehicletype || 'Bike', + baseprice: row.baseprice ?? '', + baseweight: row.baseweight ?? '', + priceperkg: row.priceperkg ?? '', + basedistance: row.basedistance ?? '', + priceperkm: row.priceperkm ?? '', + handlingcharges: row.handlingcharges ?? '', + priority: row.priority ?? 1, + status: row.status || 'active' + }); + setDialogOpen(true); + }; + + const handleDelete = (row) => { + if (window.confirm(`Delete this pricing rule for ${tenantName(row)}?`)) { + deletePricingMutation.mutate(row.pricingid); + } + }; + + const handleSave = () => { + if (!form.tenant?.tenantid) { + opentoast('Choose Tenant'); + return; + } + if (!form.baseprice || !form.priceperkm) { + opentoast('Fill Base Price and Price / KM'); + return; + } + // POST/PUT /admin/pricing body per express-console-api.md. + const data = { + tenantid: form.tenant.tenantid, + applocationid: form.location?.applocationid, + vehicletype: form.vehicletype, + baseprice: Number(form.baseprice), + baseweight: Number(form.baseweight) || 0, + priceperkg: Number(form.priceperkg) || 0, + basedistance: Number(form.basedistance) || 0, + priceperkm: Number(form.priceperkm), + handlingcharges: Number(form.handlingcharges) || 0, + currency: 'INR', + priority: Number(form.priority) || 1, + status: form.status + }; + savePricingMutation.mutate({ pricingid: form.pricingid, data }); + }; + return ( <> - {isLoading && } + {(isLoading || savePricingMutation.isPending || deletePricingMutation.isPending) && } {/* ============================================= || Header || ============================================= */} { subtitle={`Live · ${locaName || 'All Zones'}`} live action={ - } - placeholder="Select Zone" - paperComponent={SoftPaper} - sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }} - /> + + } + placeholder="Select Zone" + paperComponent={SoftPaper} + sx={{ width: { xs: '100%', sm: 220 }, zIndex: 100 }} + /> + + + } /> @@ -312,14 +480,14 @@ const ClientsPricing = () => { No pricing to show - {searchword ? 'Try a different keyword.' : 'Pick a zone above to load the catalog.'} + {searchword ? 'Try a different keyword.' : 'Create a pricing rule to get started.'} ) : ( {rows.map((row, index) => ( @@ -328,44 +496,43 @@ const ClientsPricing = () => { - - {row.appname || '—'} + + {tenantName(row)} ID #{row.pricingid} - - {String(index + 1).padStart(2, '0')} - + + openEditDialog(row)} sx={{ color: BRAND }}> + + + handleDelete(row)} sx={{ color: '#ef4444' }}> + + + } > - {row.applocation ? ( - - {row.applocation} - - ) : null} + + {zoneName(row)} + { fontWeight: 800 }} > - {row.slab || '—'} + {row.vehicletype || '—'} - } - label={formatRupees(row.baseprice)} - /> + - } - label={formatRupees(row.priceperkm)} - /> + - - } - label={`${formatDecimal(row.minkm)} km`} - /> + + - - } - label={`${formatDecimal(row.maxkm)} km`} - /> + + - + - {row.minorder ?? '—'} + {row.status || '—'} @@ -428,173 +579,372 @@ const ClientsPricing = () => { ) ) : ( - - - - - # - Tenant - Zone - Slab - Base Price - Min KM - Price / KM - Max KM - Min Orders - - - - - {isLoading && } - {rows.length === 0 && !isLoading ? ( - - - - - - - - No pricing to show - - - {searchword ? 'Try a different keyword.' : 'Pick a zone above to load the catalog.'} - - - + +
+ + + # + Tenant + Zone + Vehicle + Base Price + Price / KM + Price / KG + Handling + Status + Actions - ) : ( - rows.map((row, index) => ( - - - - {String(index + 1).padStart(2, '0')} - - + - - - - - - - - {row.appname || '—'} - - - ID #{row.pricingid} - - - - - - - {row.applocation ? ( - } label={row.applocation} /> - ) : ( - - )} - - - - } label={row.slab || '—'} /> - - - - } - label={formatRupees(row.baseprice)} - width={110} - /> - - - - } - label={`${formatDecimal(row.minkm)} km`} - width={90} - /> - - - - } - label={formatRupees(row.priceperkm)} - width={110} - /> - - - - } - label={`${formatDecimal(row.maxkm)} km`} - width={90} - /> - - - - - - - {row.minorder ?? '—'} + + {isLoading && } + {rows.length === 0 && !isLoading ? ( + + + + + + + + No pricing to show + + + {searchword ? 'Try a different keyword.' : 'Create a pricing rule to get started.'} - )) - )} - -
-
+ ) : ( + rows.map((row, index) => ( + + + + {String(index + 1).padStart(2, '0')} + + + + + + + + + + + {tenantName(row)} + + + ID #{row.pricingid} + + + + + + + } label={zoneName(row)} /> + + + + } label={row.vehicletype || '—'} /> + + + + + + + + + + + + + + + + + + + + } label={row.status || '—'} /> + + + + + + openEditDialog(row)} sx={{ color: BRAND }}> + + + + + handleDelete(row)} sx={{ color: '#ef4444' }}> + + + + + + + )) + )} + + + )}
+ + {/* ============================================= || Create / Edit dialog || ============================================= */} + setDialogOpen(false)} maxWidth="sm" fullWidth> + {form.pricingid ? 'Edit Pricing Rule' : 'New Pricing Rule'} + + + + + Tenant + option.tenantname || ''} + value={form.tenant} + onChange={(e, value) => setForm((f) => ({ ...f, tenant: value }))} + renderInput={(params) => } + /> + + + + + Zone (optional) + option.locationname || ''} + value={form.location} + onChange={(e, value) => setForm((f) => ({ ...f, location: value }))} + renderInput={(params) => } + /> + + + + + Vehicle Type + + + + + + Status + + + + + + Base Price + setForm((f) => ({ ...f, baseprice: e.target.value }))} /> + + + + + Base Weight (kg) + setForm((f) => ({ ...f, baseweight: e.target.value }))} /> + + + + + Price / KG + setForm((f) => ({ ...f, priceperkg: e.target.value }))} /> + + + + + Base Distance (km) + setForm((f) => ({ ...f, basedistance: e.target.value }))} + /> + + + + + Price / KM + setForm((f) => ({ ...f, priceperkm: e.target.value }))} /> + + + + + Handling Charges + setForm((f) => ({ ...f, handlingcharges: e.target.value }))} + /> + + + + + Priority + setForm((f) => ({ ...f, priority: e.target.value }))} /> + + + + + + + + + + + {/* ============================================= || Quote price dialog || ============================================= */} + setQuoteOpen(false)} maxWidth="xs" fullWidth> + Quote Price + + + + + Tenant + option.tenantname || ''} + value={quoteForm.tenant} + onChange={(e, value) => setQuoteForm((f) => ({ ...f, tenant: value }))} + renderInput={(params) => } + /> + + + + + Zone (optional) + option.locationname || ''} + value={quoteForm.location} + onChange={(e, value) => setQuoteForm((f) => ({ ...f, location: value }))} + renderInput={(params) => } + /> + + + + + Vehicle Type + + + + + + Weight (kg) + setQuoteForm((f) => ({ ...f, weight: e.target.value }))} + /> + + + + + Distance (km) + setQuoteForm((f) => ({ ...f, distance: e.target.value }))} + /> + + + + {quoteMutation.isPending && ( + + + Calculating… + + + )} + + {quoteMutation.data && ( + + + {quoteMutation.data.success === false ? ( + + {quoteMutation.data.message || 'No quote available'} + + ) : ( + + {Object.entries(quoteMutation.data.data || quoteMutation.data).map(([key, value]) => ( + + + {key} + + + {typeof value === 'object' ? JSON.stringify(value) : String(value ?? '—')} + + + ))} + + )} + + + )} + + + + + + + ); }; diff --git a/src/pages/nearle/clients/Tenants.js b/src/pages/nearle/clients/Tenants.js index e953eac..2663a8d 100644 --- a/src/pages/nearle/clients/Tenants.js +++ b/src/pages/nearle/clients/Tenants.js @@ -1,6 +1,5 @@ import React, { useEffect, useState, useRef, Fragment } from 'react'; import MainCard from 'components/MainCard'; -import axios from 'axios'; import { useTheme } from '@mui/material/styles'; import Loader from 'components/Loader'; import { Empty } from 'antd'; @@ -19,7 +18,6 @@ import { IconButton, Box, Grid, - Autocomplete, TextField, Tooltip, Collapse, @@ -39,6 +37,8 @@ import { Skeleton, Avatar, Paper, + Select, + MenuItem, useMediaQuery } from '@mui/material'; import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; @@ -65,7 +65,12 @@ import { MdOutlinePendingActions, MdOutlineCancel, MdMyLocation, - MdPersonPin + MdPersonPin, + MdPlace, + MdAdd, + MdEdit, + MdDeleteOutline, + MdPeopleAlt } from 'react-icons/md'; import { LuMail } from 'react-icons/lu'; import { BiUser } from 'react-icons/bi'; @@ -73,8 +78,21 @@ import LocationAutocomplete from 'components/nearle_components/LocationAutocompl import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; import PageHeader from 'components/nearle_components/PageHeader'; import StatCard from 'components/nearle_components/StatCard'; -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { getalltenants, gettenantsummary } from 'pages/api/api'; +import { + getAdminPricing, + getDoormilePricing, + createAdminPricing, + updateAdminTenant, + getTenantLocations, + createTenantLocation, + updateTenantLocation, + getTenantCustomers, + createTenantCustomer, + updateTenantCustomer, + deleteTenantCustomer +} from 'pages/api/doormileApi'; import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; // ============================================================================ @@ -103,24 +121,6 @@ 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: '10px', - bgcolor: '#ffffff', - fontWeight: 600, - color: DT.textPrimary, - paddingRight: '8px', - cursor: 'pointer', - transition: 'border-color 0.15s, box-shadow 0.15s, background-color 0.2s', - '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, - '&:hover fieldset': { borderColor: '#cbd5e1' }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` }, - '&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 } - }, - '& .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' }, @@ -168,6 +168,370 @@ const AccentAvatar = ({ color, selected, size = 24, children }) => ( ); +// ==============================|| Tenant locations (branches/sites) dialog ||============================== // +// Self-contained — deliberately does not touch the existing openRowIndex1/2 + +// Tabs collapse machinery above, which is fragile legacy state. GET/POST/PUT +// /admin/tenants/:id/locations + /admin/tenantlocations/:id per +// jupiter2doormile.md §2.4 — no location UI existed anywhere before this. +const emptyLocationForm = { + locationid: null, + locationname: '', + address: '', + city: '', + state: '', + pincode: '', + isprimary: false, + status: 'active' +}; + +const TenantLocationsDialog = ({ tenant, open, onClose }) => { + const queryClient = useQueryClient(); + const [form, setForm] = useState(emptyLocationForm); + const [formOpen, setFormOpen] = useState(false); + + const { data: locations = [], isLoading } = useQuery({ + queryKey: ['tenant-locations', tenant?.tenantid], + queryFn: () => getTenantLocations(tenant.tenantid), + enabled: !!tenant?.tenantid && open + }); + + const saveMutation = useMutation({ + mutationFn: (payload) => + payload.locationid ? updateTenantLocation(payload.locationid, payload.data) : createTenantLocation(tenant.tenantid, payload.data), + onSuccess: (res) => { + if (res.success) { + enqueueSnackbar(form.locationid ? 'Location updated' : 'Location added', { + variant: 'success', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + queryClient.invalidateQueries({ queryKey: ['tenant-locations', tenant?.tenantid] }); + setForm(emptyLocationForm); + setFormOpen(false); + } else { + enqueueSnackbar(res.message || 'Failed to save location', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + } + }, + onError: (err) => { + enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to save location', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + } + }); + + const handleSave = () => { + if (!form.locationname || !form.address) { + enqueueSnackbar('Fill Location Name and Address', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + return; + } + const { locationid, ...data } = form; + saveMutation.mutate({ locationid, data }); + }; + + const openEdit = (loc) => { + setForm({ + locationid: loc.locationid, + locationname: loc.locationname || '', + address: loc.address || '', + city: loc.city || '', + state: loc.state || '', + pincode: loc.pincode || '', + isprimary: !!loc.isprimary, + status: loc.status || 'active' + }); + setFormOpen(true); + }; + + return ( + + + Locations · {tenant?.tenantname} + + + {isLoading ? ( + + Loading locations… + + ) : locations.length === 0 && !formOpen ? ( + + ) : ( + + {locations.map((loc) => ( + + + + + {loc.locationname} {loc.isprimary && '· Primary'} + + + {[loc.address, loc.city, loc.state, loc.pincode].filter(Boolean).join(', ')} + + + openEdit(loc)}> + + + + + ))} + + )} + + {formOpen && ( + + + setForm((f) => ({ ...f, locationname: e.target.value }))} + /> + + + setForm((f) => ({ ...f, pincode: e.target.value }))} /> + + + setForm((f) => ({ ...f, address: e.target.value }))} /> + + + setForm((f) => ({ ...f, city: e.target.value }))} /> + + + setForm((f) => ({ ...f, state: e.target.value }))} /> + + + )} + + + {formOpen ? ( + <> + + + + ) : ( + <> + + + + )} + + + ); +}; + +// ==============================|| Tenant customers dialog ||============================== // +// GET /admin/tenantcustomers has no documented tenantid filter (unlike +// locations, which are nested under /admin/tenants/:id/locations) — filtered +// client-side here the same way fetchclientpricelist already filters +// /admin/pricing client-side, since neither endpoint accepts the param. +// createclient.js used to (incorrectly) create tenant customers instead of +// tenants; now that it correctly creates tenants, this dialog is the only +// place left that can create one. +const emptyCustomerForm = { id: null, firstname: '', lastname: '', phone: '', email: '' }; + +// GET /admin/tenantcustomers doesn't document its id field name either — same +// defensive fallback as customers.js's custId() rather than assuming `id`. +const tenantCustId = (row) => row.id ?? row.tenantcustomerid ?? row.customerid; + +const TenantCustomersDialog = ({ tenant, open, onClose }) => { + const queryClient = useQueryClient(); + const [form, setForm] = useState(emptyCustomerForm); + const [formOpen, setFormOpen] = useState(false); + + const { data: allCustomers = [], isLoading } = useQuery({ + queryKey: ['tenant-customers-all'], + queryFn: getTenantCustomers, + enabled: open + }); + + const customers = (allCustomers || []).filter((c) => !tenant?.tenantid || c.tenantid === tenant.tenantid); + + const saveMutation = useMutation({ + mutationFn: (payload) => (payload.id ? updateTenantCustomer(payload.id, payload.data) : createTenantCustomer(payload.data)), + onSuccess: (res) => { + if (res.success) { + enqueueSnackbar(form.id ? 'Customer updated' : 'Customer added', { + variant: 'success', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + queryClient.invalidateQueries({ queryKey: ['tenant-customers-all'] }); + setForm(emptyCustomerForm); + setFormOpen(false); + } else { + enqueueSnackbar(res.message || 'Failed to save customer', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + } + }, + onError: (err) => { + enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to save customer', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + } + }); + + const deleteMutation = useMutation({ + mutationFn: (id) => deleteTenantCustomer(id), + onSuccess: (res) => { + if (res.success) { + enqueueSnackbar('Customer deleted', { + variant: 'success', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + queryClient.invalidateQueries({ queryKey: ['tenant-customers-all'] }); + } else { + enqueueSnackbar(res.message || 'Failed to delete customer', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + } + }, + onError: (err) => { + enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to delete customer', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + } + }); + + const handleSave = () => { + if (!form.firstname || !form.phone) { + enqueueSnackbar('Fill Name and Phone', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + return; + } + const { id, ...data } = form; + saveMutation.mutate({ id, data }); + }; + + const openEdit = (row) => { + setForm({ + id: tenantCustId(row), + firstname: row.firstname || '', + lastname: row.lastname || '', + phone: row.phone || '', + email: row.email || '' + }); + setFormOpen(true); + }; + + const handleDelete = (row) => { + if (window.confirm(`Delete customer "${row.firstname}"?`)) deleteMutation.mutate(tenantCustId(row)); + }; + + return ( + + Customers · {tenant?.tenantname} + + {isLoading ? ( + + Loading customers… + + ) : customers.length === 0 && !formOpen ? ( + + ) : ( + + {customers.map((c) => ( + + + + + {c.firstname} {c.lastname} + + + {[c.phone, c.email].filter(Boolean).join(' · ')} + + + + openEdit(c)}> + + + handleDelete(c)} sx={{ color: '#ef4444' }}> + + + + + + ))} + + )} + + {formOpen && ( + + + setForm((f) => ({ ...f, firstname: e.target.value }))} /> + + + setForm((f) => ({ ...f, lastname: e.target.value }))} /> + + + setForm((f) => ({ ...f, phone: e.target.value }))} /> + + + setForm((f) => ({ ...f, email: e.target.value }))} /> + + + )} + + + {formOpen ? ( + <> + + + + ) : ( + <> + + + + )} + + + ); +}; + // ==============================|| Starts here||============================== // const Clients1 = () => { const textFieldRef = useRef(null); @@ -189,7 +553,7 @@ const Clients1 = () => { const [status, setstatus] = useState('active'); const [selectedCustomer, setSelectedCustomer] = useState({}); // to edit const [dialogopen, setDialogopen] = useState(false); - const [appPricing, setAppPricing] = useState([]); + const [, setAppPricing] = useState([]); const [selectedPricing, setSelectedPricing] = useState({}); const [isPrice, setIsprice] = useState(true); const [city, setCity] = useState(''); @@ -197,7 +561,6 @@ const Clients1 = () => { const [latlong, setLatlong] = useState({}); const [editClient, setEditClient] = useState({}); const [page, setPage] = useState(0); - const [tabStatus, setTabStatus] = useState('Active'); const handleChangePage = (event, newPage) => { setPage(newPage); @@ -212,7 +575,6 @@ const Clients1 = () => { }; const handleChange = (event, newValue) => { - setTabStatus(newValue == 0 ? 'Active' : newValue == 1 ? 'Pending' : 'InActive'); setValue0(newValue); setSearchword(''); setPage(0); @@ -228,6 +590,8 @@ const Clients1 = () => { const [openRowIndex1, setOpenRowIndex1] = useState(null); // Initially no row is open for collapsible section 1 const [openRowIndex2, setOpenRowIndex2] = useState(null); // Initially no row is open for collapsible section 2 + const [locationsDialogTenant, setLocationsDialogTenant] = useState(null); + const [customersDialogTenant, setCustomersDialogTenant] = useState(null); // Handle toggling for collapsible section 1 const handleCollapseToggle1 = (rowIndex) => { setOpenRowIndex1((prevIndex) => (prevIndex === rowIndex ? null : rowIndex)); @@ -348,18 +712,14 @@ const Clients1 = () => { /* ============================================= || fetchclientpricelist || ============================================= */ + // /admin/pricing has no tenantid query param documented — filter client-side. const fetchclientpricelist = async () => { - await axios - .get(`${process.env.REACT_APP_URL}/tenants/getpricinglist/?tenantid=${selectedTenid}`) - .then((res) => { - console.log('getpricinglist', res); - if (res.data.status) { - setClientpricelist(res.data.details); - } - }) - .catch((err) => { - console.log(err); - }); + try { + const pricing = await getAdminPricing(); + setClientpricelist((pricing || []).filter((p) => p.tenantid === selectedTenid)); + } catch (err) { + console.log(err); + } }; useEffect(() => { selectedTenid && fetchclientpricelist(); @@ -368,9 +728,8 @@ const Clients1 = () => { /* ============================================= || fetchTenanatPricing || ============================================= */ const fetchTenanatPricing = async (id) => { try { - let tenantPricing = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${id}`); - console.log('tenantPricing', tenantPricing.data.details); - setTenanatPricing(tenantPricing.data.details); + const pricing = await getAdminPricing(); + setTenanatPricing((pricing || []).filter((p) => p.tenantid === id)); } catch (error) { console.log('fetchTenanatPricing', error); } @@ -382,44 +741,40 @@ const Clients1 = () => { const tenantupdate = async (tenid) => { setisloader(true); + let targetId; let updateData; if (tenid == -1) { - updateData = { - tenantid: selectedTenid, - approved: 1 - }; + targetId = selectedTenid; + updateData = { approved: 1 }; } else { - updateData = { tenantid: tenid, status: status === 'active' ? 'InActive' : 'Active' }; + targetId = tenid; + updateData = { status: status === 'active' ? 'InActive' : 'Active' }; } - await axios - .put(`${process.env.REACT_APP_URL}/tenants/update`, updateData) - .then((res) => { - if (res.data.status) { - opentoast( - value0 == 0 ? 'Inactivated Successfully' : value0 == 1 ? 'Approved Successfully' : 'Activate Successfully', - 'success', - 2000 - ); - getalltenantsRefetch(); - summaryDataRefetch(); - setisloader(false); - } - }) - .catch((err) => { - console.log(err); - opentoast(err.message, 'error', 1500); - setisloader(false); - }); - // } + try { + const res = await updateAdminTenant(targetId, updateData); + if (res.success) { + opentoast( + value0 == 0 ? 'Inactivated Successfully' : value0 == 1 ? 'Approved Successfully' : 'Activate Successfully', + 'success', + 2000 + ); + getalltenantsRefetch(); + summaryDataRefetch(); + } + } catch (err) { + console.log(err); + opentoast(err.message, 'error', 1500); + } finally { + setisloader(false); + } }; /* ============================================= || getAppPricing || ============================================= */ + // /admin/doormile-pricing has no zone query param documented — filter client-side. const getAppPricing = async (id) => { - console.log('id', id); try { - let appPricingRes = await axios.get(`${process.env.REACT_APP_URL}/utils/getapppricing/?applocationid=${id}`); - console.log('appPricingRes', appPricingRes.data.details); - setAppPricing(appPricingRes.data.details); + const pricing = await getDoormilePricing(); + setAppPricing((pricing || []).filter((p) => !id || p.applocationid === id)); } catch (error) { console.log('appPricingRes', error); } @@ -427,55 +782,54 @@ const Clients1 = () => { /* ============================================= || createpricing || ============================================= */ const createpricing = async () => { setisloader(true); - await axios - .post(`${process.env.REACT_APP_URL}/tenants/createpricing`, { - tenantpricingid: 0, + try { + const res = await createAdminPricing({ applocationid: appId, - pricingid: selectedPricing.pricingid, tenantid: selectedTenid, - pricingdate: dayjs().format('YYYY-MM-DD HH:mm:ss'), - configid: selectedPricing.configid, - pricingtypeid: selectedPricing.pricingtypeid, - slab: selectedPricing.slab, - baseprice: +selectedPricing.baseprice, - priceperkm: +selectedPricing.priceperkm, - minkm: +selectedPricing.minkm, - maxkm: +selectedPricing.maxkm, - orders: +selectedPricing.minorder, - othercharges: 0 - }) - .then((res) => { - if (res.data.status) { - enqueueSnackbar('Price Created Successfully', { - variant: 'success', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - dialogclose(); - setSelectedPricing({}); - tenantupdate(-1); - fetchclientpricelist(); - } - }) - .catch((err) => { - console.log(err); + vehicletype: selectedPricing.vehicletype || 'Bike', + baseprice: +selectedPricing.baseprice || 0, + baseweight: +selectedPricing.baseweight || 0, + priceperkm: +selectedPricing.priceperkm || 0, + priceperkg: +selectedPricing.priceperkg || 0, + basedistance: +selectedPricing.basedistance || 0, + handlingcharges: +selectedPricing.handlingcharges || 0, + effectivefrom: dayjs().format('YYYY-MM-DD'), + currency: 'INR', + priority: 1, + status: 'active' }); + if (res.success) { + enqueueSnackbar('Price Created Successfully', { + variant: 'success', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + dialogclose(); + setSelectedPricing({}); + tenantupdate(-1); + fetchclientpricelist(); + } else { + opentoast(res.message || 'Failed to save pricing', 'error', 2000); + } + } catch (err) { + console.log(err); + opentoast(err.response?.data?.message || err.message || 'Failed to save pricing', 'error', 2000); + } }; /* ============================================= || updateClient || ============================================= */ const updateClient = async () => { try { - const arr = { ...editClient, tenantid: selectedTenid }; - - console.log('updateClient', arr); - const updateRes = await axios.put(`${process.env.REACT_APP_URL}/tenants/update`, arr); - console.log('updateClient', updateRes.data.message); - if (updateRes.data.status) { - opentoast(updateRes.data.message, 'success', 1500); + const updateRes = await updateAdminTenant(selectedTenid, editClient); + if (updateRes.success) { + opentoast(updateRes.message || 'Updated Successfully', 'success', 1500); + } else { + opentoast(updateRes.message || 'Update Failed', 'error', 1500); } getalltenantsRefetch(); summaryDataRefetch(); } catch (err) { console.log('updateClient', err); + opentoast(err.response?.data?.message || err.message || 'Update Failed', 'error', 1500); } }; @@ -912,6 +1266,34 @@ const Clients1 = () => { {openRowIndex1 === index ? : } + + setLocationsDialogTenant(row)} + > + + + + + setCustomersDialogTenant(row)} + > + + + {value0 !== 1 && ( { # - Date - Slab + Vehicle Base Price - Min Kms Price/Km - Other Charges + Price/Kg + Handling + Status @@ -1225,14 +1607,14 @@ const Clients1 = () => { ) : ( clientpricelist.map((val, i) => ( - + {i + 1} - {dayjs(val.pricingdate).format('DD-MM-YYYY')} - {val.slab} - {val.baseprice} - {val.minkm} - {val.priceperkm} - {val.othercharges} + {val.vehicletype || '—'} + {val.baseprice ?? '—'} + {val.priceperkm ?? '—'} + {val.priceperkg ?? '—'} + {val.handlingcharges ?? '—'} + {val.status || '—'} )) )} @@ -1667,12 +2049,12 @@ const Clients1 = () => { # - Date - Slab + Vehicle Base Price - Min Kms Price/Km - Other Charges + Price/Kg + Handling + Status @@ -1684,14 +2066,14 @@ const Clients1 = () => { ) : ( clientpricelist.map((val, i) => ( - + {i + 1} - {dayjs(val.pricingdate).format('DD-MM-YYYY')} - {val.slab} - {val.baseprice} - {val.minkm} - {val.priceperkm} - {val.othercharges} + {val.vehicletype || '—'} + {val.baseprice ?? '—'} + {val.priceperkm ?? '—'} + {val.priceperkg ?? '—'} + {val.handlingcharges ?? '—'} + {val.status || '—'} )) )} @@ -1861,6 +2243,30 @@ const Clients1 = () => { > {expanded ? : } + setLocationsDialogTenant(row)} + > + + + setCustomersDialogTenant(row)} + > + + {value0 !== 1 && ( { Select Slab - `${option.slab}`} + @@ -2005,43 +2411,57 @@ const Clients1 = () => { - Min Kms + Base Weight (kg) { setSelectedPricing({ ...selectedPricing, - minkm: e.target.value + baseweight: e.target.value }); }} /> - Max Kms + Price/Kg { setSelectedPricing({ ...selectedPricing, - maxkm: e.target.value + priceperkg: e.target.value }); }} /> - Min Orders + Base Distance (km) { setSelectedPricing({ ...selectedPricing, - minorder: e.target.value + basedistance: e.target.value + }); + }} + /> + + + Handling Charges + { + setSelectedPricing({ + ...selectedPricing, + handlingcharges: e.target.value }); }} /> @@ -2093,6 +2513,18 @@ const Clients1 = () => { + + setLocationsDialogTenant(null)} + /> + + setCustomersDialogTenant(null)} + /> ); }; diff --git a/src/pages/nearle/clients/createclient.js b/src/pages/nearle/clients/createclient.js index f15ae7a..87ae28a 100644 --- a/src/pages/nearle/clients/createclient.js +++ b/src/pages/nearle/clients/createclient.js @@ -1,75 +1,43 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; // material-ui -import { useTheme } from '@mui/material/styles'; -import { Avatar, Box, Button, FormLabel, Grid, InputLabel, MenuItem, Paper, Select, Stack, TextField, Typography, useMediaQuery } from '@mui/material'; +import { + Avatar, + Button, + FormControlLabel, + Grid, + InputLabel, + MenuItem, + Paper, + Select, + Stack, + Switch, + TextField, + Typography, + useMediaQuery, + useTheme +} from '@mui/material'; import { MdPersonAddAlt1 } from 'react-icons/md'; - -// third-party -// import { PatternFormat } from 'react-number-format'; +import { useMutation } from '@tanstack/react-query'; // project import import MainCard from 'components/MainCard'; -import axios from 'axios'; -import AddressAutocomplete, { geocodeAddress } from 'components/nearle_components/AddressAutocomplete'; +import { createAdminTenant } from 'pages/api/doormileApi'; import Loader from 'components/Loader'; import { enqueueSnackbar } from 'notistack'; import { useNavigate } from 'react-router'; import { DT, tint } from 'themes/dt/tokens'; -// const avatarImage = require.context('assets/images/users', true); - -// styles & constant -// const ITEM_HEIGHT = 48; -// const ITEM_PADDING_TOP = 8; -// const MenuProps = { -// PaperProps: { -// style: { -// maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP -// } -// } -// }; - const Createclient = () => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); - // const [role, setRole] = useState(''); - const [mobilenumber, setMobilenumber] = useState(''); - const [emailaddress, setEmailaddress] = useState(''); - const [city, setCity] = useState(''); - const [zipcode, setZipcode] = useState(''); - const [address, setAddress] = useState(''); - const [state, setState] = useState(''); - const [suburb, setSuburb] = useState(''); - const [latlong, setLatlong] = useState({}); - const [firstname, setFirstname] = useState(''); - const [doorno, setDoorno] = useState(''); - const [landmark, setLandmark] = useState(''); - const [tenantinfo, setTenantinfo] = useState({}); - const navigate = useNavigate(); - const [loading, setLoading] = useState(false); - - useEffect(() => { - // fetchprofiledetails(localStorage.getItem('appuserid')); - // fetchprofiledetails(181); - if (localStorage.getItem('tenantid')) { - fetchtenantinfo(localStorage.getItem('tenantid')); - } - }, []); - - useEffect(() => { - let active = true; - geocodeAddress(address).then((place) => { - if (active && place) { - setLatlong({ lat: place.geometry.location.lat(), lng: place.geometry.location.lng() }); - } - }); - return () => { - active = false; - }; - }, [address]); + const [tenantname, setTenantname] = useState(''); + const [primaryemail, setPrimaryemail] = useState(''); + const [primarycontact, setPrimarycontact] = useState(''); + const [status, setStatus] = useState('active'); + const [requiredeliveryotp, setRequiredeliveryotp] = useState(false); const opentoast = (message) => { enqueueSnackbar(message, { @@ -77,192 +45,53 @@ const Createclient = () => { anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); - // console.log(alertmessage) }; - const fetchprofiledetails = async (userid) => { - if (userid) { - setLoading(true); - try { - await axios - .get(`${process.env.REACT_APP_URL2}/tenants/getclient?id=${userid}`) - .then((res) => { - console.log(res); - if (res.data.message === 'Successful') { - let res1 = res.data.details; - setMobilenumber(res1.contactno); - setEmailaddress(res1.primaryemail); - setAddress(res1.address); - setCity(res1.city); - setZipcode(res1.postcode); - setState(res1.state); - setSuburb(res1.suburb); - setLatlong({ - lat: res1.latitude, - lng: res1.longitude - }); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - } catch (err) { - console.log(err); - setLoading(false); + const createTenantMutation = useMutation({ + mutationFn: (payload) => createAdminTenant(payload), + onSuccess: (res) => { + if (res.success) { + enqueueSnackbar('Client created successfully', { + variant: 'success', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + navigate('/doormile/tenants'); + } else { + opentoast(res.message || 'Client already exists'); } + }, + onError: (err) => { + opentoast(err.response?.data?.message || err.message || 'Failed to create client'); } - }; + }); - const fetchtenantinfo = async (tid) => { - setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`) - .then((res) => { - console.log(res); - if (res.data.status) { - setTenantinfo(res.data.details); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; - - const handleAddressPlaceSelected = (place) => { - setAddress(place.formatted_address); - let city1, zipcode1, state1, suburb1; - for (let i = 0; i < place.address_components.length; i++) { - for (let j = 0; j < place.address_components[i].types.length; j++) { - switch (place.address_components[i].types[j]) { - case 'locality': - city1 = place.address_components[i].long_name; - break; - case 'administrative_area_level_1': - state1 = place.address_components[i].long_name; - break; - case 'postal_code': - zipcode1 = place.address_components[i].long_name; - break; - case 'sublocality': - case 'sublocality_level_1': - suburb1 = place.address_components[i].long_name; - break; - } - } - } - setCity(city1 || ''); - setState(state1 || ''); - setZipcode(zipcode1 || ''); - setSuburb(suburb1 || ''); - }; - - const createprofile = async () => { - console.log('res', businessname, businessno, mobilenumber, emailaddress, address, city, zipcode); - - // if (!businessname) { - // opentoast('Fill Business name') - // } else if (!businessno) { - // opentoast('Fill Registration No') - // } - // else - if (!firstname) { - opentoast('Fill Full name'); - } else if (!mobilenumber) { - opentoast('Fill Mobile Number'); - } else if (!emailaddress) { - opentoast('Fill emailaddress'); - } else if (!address) { - opentoast('Fill Address'); - } else if (!city) { - opentoast('Fill City'); - } else if (!zipcode) { - opentoast('Fill post code'); - } else if (!suburb) { - opentoast('Fill suburb'); - } else if (!latlong.lat || !latlong.lng) { - opentoast('Choose valid address'); + const createprofile = () => { + if (!tenantname) { + opentoast('Fill Client / Business Name'); + } else if (!primarycontact || primarycontact.length !== 10) { + opentoast('Fill a valid 10-digit Contact Number'); + } else if (!primaryemail) { + opentoast('Fill Primary Email'); } else { - let obj = { - customerid: 0, - configid: 1, - firstname: firstname, - applocationid: tenantinfo.applolcationid, - profileimage: '', - dialcode: '+91', - contactno: mobilenumber, - devicetype: '', - deviceid: '', - customertoken: '', - address: address, - suburb: suburb, - city: city, - state: state, - postcode: zipcode, - landmark: landmark, - doorno: doorno, - latitude: latlong.lat.toString(), - longitude: latlong.lng.toString(), - tenantid: parseInt(localStorage.getItem('tenantid')), - email: emailaddress + // POST /admin/tenants — requiredeliveryotp is a pointer server-side; + // it's opt-in and off by default, so we only send it when the operator + // explicitly turns it on rather than always sending false. + const obj = { + tenantname, + primaryemail, + primarycontact, + status, + ...(requiredeliveryotp ? { requiredeliveryotp: true } : {}) }; - console.log(obj); - - setLoading(true); - try { - await axios - .post(`${process.env.REACT_APP_URL}/customers/create`, obj) - .then((res) => { - console.log(res); - if (res.data.status) { - enqueueSnackbar(' Created Successfully ', { - variant: 'success', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - navigate('/clients'); - // setTimeout(()=>{ - // fetchprofiledetails(localStorage.getItem('appuserid')); - - // },2000) - } else if (res.data.message == 'Customer Already available') { - enqueueSnackbar('Customer Already available', { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - - setLoading(false); - enqueueSnackbar(err.message, { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - }); - } catch (err) { - console.log(err); - setLoading(false); - } + createTenantMutation.mutate(obj); } }; - // const [experience, setExperience] = useState('0'); - - // const handleChange = (event) => { - // setExperience(event.target.value); - // }; return ( <> - {loading && } + {createTenantMutation.isPending && } { sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }} > - {/* - - - - - - - - - - - Upload - - - - - setSelectedImage(e.target.files?.[0])} - /> - - - - - - - - - - - - - - - - - - Role - setRole(e.target.value)} - value={role} - autoComplete='off' - - /> - - - - - - */} - - + + - {/* - - Business Name - setBusinessname(e.target.value)} - value={businessname} - autoComplete='off' - /> - - - - - Registration No - setBusinessno(e.target.value)} - value={businessno} - autoComplete='off' - - /> - - */} - Admin Name + Client / Business Name setFirstname(e.target.value)} - value={firstname} + id="tenant-name" + placeholder="e.g. DailyGrubs" + onChange={(e) => setTenantname(e.target.value)} + value={tenantname} autoComplete="off" /> - - Phone Number + Status + + + + + + Primary Contact Number { }} + placeholder="Contact Number" onChange={(e) => { if (e.target.value.toString().length <= 10) { - setMobilenumber(e.target.value); + setPrimarycontact(e.target.value); } }} - value={mobilenumber} + value={primarycontact} autoComplete="off" - // disabled - sx={{ cursor: 'not-allowed' }} /> - Email Address + Primary Email setEmailaddress(e.target.value)} - value={emailaddress} + id="tenant-email" + placeholder="Primary Email Address" + onChange={(e) => setPrimaryemail(e.target.value)} + value={primaryemail} autoComplete="off" /> - - - Address - - - - - - - Suburb - setSuburb(e.target.value)} - value={suburb} - autoComplete="off" - /> - - - - - City - setCity(e.target.value)} - value={city} - autoComplete="off" - /> - - - - - - State - setState(e.target.value)} - value={state} - autoComplete="off" - /> - - - - - Post Code - setZipcode(e.target.value)} - value={zipcode} - autoComplete="off" - /> - - - - - - Door No - setDoorno(e.target.value)} - value={doorno} - autoComplete="off" - /> - - - - - Landmark - setLandmark(e.target.value)} - value={landmark} - autoComplete="off" - /> - + setRequiredeliveryotp(e.target.checked)} />} + label="Require delivery OTP for this client (off by default)" + /> @@ -582,7 +195,7 @@ const Createclient = () => { alignItems={{ xs: 'stretch', sm: 'center' }} spacing={2} > - diff --git a/src/pages/nearle/deliveries/deliveries.js b/src/pages/nearle/deliveries/deliveries.js index c576bbd..41ace71 100644 --- a/src/pages/nearle/deliveries/deliveries.js +++ b/src/pages/nearle/deliveries/deliveries.js @@ -5,7 +5,7 @@ import { useState, useEffect, Fragment, useRef, useMemo } from 'react'; import dayjs from 'dayjs'; var utc = require('dayjs/plugin/utc'); dayjs.extend(utc); -import axios from 'axios'; +import { notifyMiler } from 'pages/api/doormileApi'; import { useTheme } from '@mui/material/styles'; import { MdOutlineDateRange, @@ -69,15 +69,7 @@ import { } from '@mui/material'; import { PopupTransition } from 'components/@extended/Transitions'; -import { - addDays, - addMonths, - addWeeks, - endOfMonth, - endOfWeek, - startOfMonth, - startOfWeek -} from 'date-fns'; +import { addDays, addMonths, addWeeks, endOfMonth, endOfWeek, startOfMonth, startOfWeek } from 'date-fns'; import { DateRangePicker } from 'components/nearle_components/DateRangePicker'; import * as React from 'react'; import Loader from 'components/Loader'; @@ -113,34 +105,34 @@ import { DT, a, tint, soft, ring, edge, pillFieldSx } from 'themes/dt/tokens'; // Status palette — drives tab pills, row status badges, dialogs. const STATUS_META = { - pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty }, - accepted: { label: 'Accepted', color: '#6366f1', icon: MdPersonPin }, - arrived: { label: 'Arrived', color: '#06b6d4', icon: MdLocationOn }, - picked: { label: 'Picked', color: '#8b5cf6', icon: MdInventory2 }, - active: { label: 'Active', color: '#14b8a6', icon: MdRoute }, - skipped: { label: 'Skipped', color: '#f97316', icon: MdSkipNext }, - delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle }, - cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel } + pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty }, + accepted: { label: 'Accepted', color: '#6366f1', icon: MdPersonPin }, + arrived: { label: 'Arrived', color: '#06b6d4', icon: MdLocationOn }, + picked: { label: 'Picked', color: '#8b5cf6', icon: MdInventory2 }, + active: { label: 'Active', color: '#14b8a6', icon: MdRoute }, + skipped: { label: 'Skipped', color: '#f97316', icon: MdSkipNext }, + delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle }, + cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel } }; // Ordered status list driving the tabs row (left → right). Each entry binds // the visual meta above to the `currentStatus` key the queries use AND to the // `batchCounts` key (so the chip count for the tab is one lookup). const STATUS_TABS = [ - { status: 'pending', countKey: 'uncoveredLength' }, - { status: 'accepted', countKey: 'assignedLength' }, - { status: 'arrived', countKey: 'arrivedLength' }, - { status: 'picked', countKey: 'pickedLength' }, - { status: 'active', countKey: 'activeLength' }, - { status: 'skipped', countKey: 'skippedLength' }, - { status: 'delivered', countKey: 'coveredLength' }, - { status: 'cancelled', countKey: 'cancelLength' } + { status: 'pending', countKey: 'uncoveredLength' }, + { status: 'accepted', countKey: 'assignedLength' }, + { status: 'arrived', countKey: 'arrivedLength' }, + { status: 'picked', countKey: 'pickedLength' }, + { status: 'active', countKey: 'activeLength' }, + { status: 'skipped', countKey: 'skippedLength' }, + { status: 'delivered', countKey: 'coveredLength' }, + { status: 'cancelled', countKey: 'cancelLength' } ]; // KPI palette + icons — mirrors the four cards across the top of the page. const KPI_META = [ - { key: 'created', label: 'Created Orders', color: '#0ea5e9', icon: MdOutlineLocalShipping }, - { key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdOutlinePendingActions }, + { key: 'created', label: 'Created Orders', color: '#0ea5e9', icon: MdOutlineLocalShipping }, + { key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdOutlinePendingActions }, { key: 'delivered', label: 'Delivered Orders', color: '#10b981', icon: MdOutlineCheckCircle }, { key: 'cancelled', label: 'Cancelled Orders', color: '#ef4444', icon: MdOutlineCancel } ]; @@ -152,7 +144,15 @@ const KPI_META = [ const BATCH_OPTIONS = [ { id: 'all', label: 'All Batches', range: 'Across the day', color: '#7c3aed', iconKey: 'all' }, { id: 'morning', label: 'Morning Batch', range: '12 AM to 8 AM', color: '#0ea5e9', iconKey: 'morning', startHour: 0, endHour: 8 }, - { id: 'afternoon', label: 'Afternoon Batch', range: '9 AM to 12:30 PM', color: '#f59e0b', iconKey: 'afternoon', startHour: 9, endHour: 12.5 }, + { + id: 'afternoon', + label: 'Afternoon Batch', + range: '9 AM to 12:30 PM', + color: '#f59e0b', + iconKey: 'afternoon', + startHour: 9, + endHour: 12.5 + }, { id: 'evening', label: 'Evening Batch', range: '4 PM to 7 PM', color: '#6366f1', iconKey: 'evening', startHour: 16, endHour: 19 } ]; @@ -359,19 +359,9 @@ const Deliveries = () => { const cancelridernotification = async () => { console.log('cancelridernotification', selectedRow); try { - const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, { - token: selectedRow.userfcmtoken, - notification: { - title: 'DoormileXpress', - body: `${selectedRow.orderid} have been Cancelled`, - sound: 'ring', - image: '' - }, - data: { - type: 'cancel' - } - }); - return response.data; + // /admin/milers/:id/notify keys off milerprofileid, not the userid + // stored on the booking (confirmed live) — see fetchDeliveries in api.js. + return await notifyMiler(selectedRow.milerprofileid, 'DoormileXpress', `${selectedRow.orderid} have been Cancelled`); } catch (err) { opentoast(err.message, 'error', 2000); console.log(err); @@ -428,14 +418,20 @@ const Deliveries = () => { onSuccess: (res, { selectedRider, selectedRow }) => { setLoading1(false); setChangeDialogOpen(false); - if (res.data.message === 'Success') { - logger.info(`Rider changed successfully for order ID ${selectedRow?.orderid}. New Rider: ${selectedRider?.firstname} ${selectedRider?.lastname}`); + if (res.success) { + logger.info( + `Rider changed successfully for order ID ${selectedRow?.orderid}. New Rider: ${selectedRider?.displayname || selectedRider?.authname}` + ); opentoast('Rider Changed Successfully', 'success'); } fetchCountRefetch(); // Refresh count data fetchDeliveriesRefetch(); // Refresh deliveries countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips - notifyRiderMutation.mutate(selectedRider.userfcmtoken); + // notifyMiler wants a miler profile id, not an FCM token — the new API + // looks the device token up server-side. selectedRider comes straight + // from getMilers(), which has milerprofileid directly (confirmed live; + // .userid there is the underlying app-user id, a different resource). + notifyRiderMutation.mutate(selectedRider.milerprofileid); }, onError: (err, { selectedRider, selectedRow }) => { logger.error(`Failed to change rider for order ID ${selectedRow?.orderid}:`, err); @@ -564,27 +560,12 @@ const Deliveries = () => { isLoading: countSourceIsLoading, refetch: countSourceRefetch } = useInfiniteQuery({ - queryKey: [ - 'fetchdeliveries-batchcounts', - appId, - userid, - 'all', - startdate, - enddate, - 200, - '', - tenantid, - locationid, - riderid - ], + queryKey: ['fetchdeliveries-batchcounts', appId, userid, 'all', startdate, enddate, 200, '', tenantid, locationid, riderid], queryFn: fetchDeliveries, getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined }); - const countSourceRows = useMemo( - () => (countSourceData?.pages || []).flatMap((p) => p.rows || []), - [countSourceData] - ); + const countSourceRows = useMemo(() => (countSourceData?.pages || []).flatMap((p) => p.rows || []), [countSourceData]); useEffect(() => { if (countHasNext && !countIsFetchingNext) countFetchNext(); @@ -606,7 +587,9 @@ const Deliveries = () => { // fields. Server-side search isn't needed since every row is loaded. const filteredRows = useMemo(() => { const wantStatus = String(currentStatus || '').toLowerCase(); - const q = String(debouncedSearch || '').trim().toLowerCase(); + const q = String(debouncedSearch || '') + .trim() + .toLowerCase(); return countSourceRows.filter((r) => { if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return false; const s = String(r.orderstatus || '').toLowerCase(); @@ -785,7 +768,7 @@ const Deliveries = () => { mutationFn: (orderData) => updateDeliveryAPI(orderData), onSuccess: (res) => { console.log(res); - if (res.data.status) { + if (res.success) { opentoast('Updated Successfully', 'success'); setDeliveryamount(''); setNotes(''); @@ -804,14 +787,14 @@ const Deliveries = () => { const errorMessage = fetchDeliveriesIsError ? `Error fetching deliveries: ${fetchDeliveriesError?.message}` : fetchCountIsError - ? `Error fetching count summary: ${fetchCountError?.message}` - : ridersListIsError - ? `Error fetching riders: ${ridersListError?.message}` - : fetchtenantsIsError - ? `Error tenant list: ${fetchtenantsError?.message}` - : fetchlocationsIsError - ? `Error location list: ${fetchlocationsError?.message}` - : null; + ? `Error fetching count summary: ${fetchCountError?.message}` + : ridersListIsError + ? `Error fetching riders: ${ridersListError?.message}` + : fetchtenantsIsError + ? `Error tenant list: ${fetchtenantsError?.message}` + : fetchlocationsIsError + ? `Error location list: ${fetchlocationsError?.message}` + : null; if (errorMessage) { console.log('errorMessage', errorMessage); @@ -821,29 +804,19 @@ const Deliveries = () => { return ( <> - {(fetchCountIsLoading || - countSourceIsLoading || - fetchtenantsIsLoading || - fetchlocationsIsLoading || - riderListIsLoading) && ( - <> - - {/* */} - - )} + {(fetchCountIsLoading || countSourceIsLoading || fetchtenantsIsLoading || fetchlocationsIsLoading || riderListIsLoading) && ( + <> + + {/* */} + + )} { theme.zIndex.drawer + 1 }} - open={ - fetchCountIsLoading || - fetchDeliveriesIsLoading || - fetchtenantsIsLoading || - fetchlocationsIsLoading || - riderListIsLoading - } + open={fetchCountIsLoading || fetchDeliveriesIsLoading || fetchtenantsIsLoading || fetchlocationsIsLoading || riderListIsLoading} > {/* */} @@ -874,9 +847,21 @@ const Deliveries = () => { {[ { ...KPI_META[0], value: batchTotals.all, percentage: null }, - { ...KPI_META[1], value: countData?.uncoveredLength, percentage: countData?.total ? Math.round((countData.uncoveredLength / countData.total) * 100) : 0 }, - { ...KPI_META[2], value: countData?.coveredLength, percentage: countData?.total ? Math.round((countData.coveredLength / countData.total) * 100) : 0 }, - { ...KPI_META[3], value: countData?.cancelLength, percentage: countData?.total ? Math.round((countData.cancelLength / countData.total) * 100) : 0 } + { + ...KPI_META[1], + value: countData?.uncoveredLength, + percentage: countData?.total ? Math.round((countData.uncoveredLength / countData.total) * 100) : 0 + }, + { + ...KPI_META[2], + value: countData?.coveredLength, + percentage: countData?.total ? Math.round((countData.coveredLength / countData.total) * 100) : 0 + }, + { + ...KPI_META[3], + value: countData?.cancelLength, + percentage: countData?.total ? Math.round((countData.cancelLength / countData.total) * 100) : 0 + } ].map((item) => { const Icon = item.icon; const pct = typeof item.percentage === 'number' ? item.percentage : Number(item.percentage); @@ -918,327 +903,317 @@ const Deliveries = () => { Filters - - - {/* Batch dropdown — replaces the legacy "Deliveries-{datestatus}" and + + + {/* Batch dropdown — replaces the legacy "Deliveries-{datestatus}" and "Orders-All" pills. Filters the loaded rows by the slot the operator picks; defaults to 'all'. Mirrors the dispatch page's batch model. UI mirrors the polished Absent Riders dropdown from the orders page — custom Paper, per-batch color/icon, selected indicator. */} - {(() => { - const activeBatch = BATCH_OPTIONS.find((b) => b.id === selectedBatch) || BATCH_OPTIONS[0]; - const ActiveIcon = BATCH_ICONS[activeBatch.iconKey] || MdAccessTime; - return ( - opts} - options={BATCH_OPTIONS} - value={activeBatch} - onChange={(e, val) => val && setSelectedBatch(val.id)} - getOptionLabel={(option) => option.label} - isOptionEqualToValue={(option, value) => option.id === value.id} - PaperComponent={SoftPaper} - ListboxProps={{ sx: { py: 0, maxHeight: 360 } }} - renderOption={(props, option, { selected }) => { - const Icon = BATCH_ICONS[option.iconKey] || MdAccessTime; - const total = batchTotals[option.id] ?? 0; - return ( -
  • - { + const activeBatch = BATCH_OPTIONS.find((b) => b.id === selectedBatch) || BATCH_OPTIONS[0]; + const ActiveIcon = BATCH_ICONS[activeBatch.iconKey] || MdAccessTime; + return ( + opts} + options={BATCH_OPTIONS} + value={activeBatch} + onChange={(e, val) => val && setSelectedBatch(val.id)} + getOptionLabel={(option) => option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + PaperComponent={SoftPaper} + ListboxProps={{ sx: { py: 0, maxHeight: 360 } }} + renderOption={(props, option, { selected }) => { + const Icon = BATCH_ICONS[option.iconKey] || MdAccessTime; + const total = batchTotals[option.id] ?? 0; + return ( +
  • - - - - - {option.label} - - - {option.range} - - - {/* Per-batch total — same number you see next to each + + + + + {option.label} + + + {option.range} + + + {/* Per-batch total — same number you see next to each batch on the dispatch page. Lets the operator eyeball-compare without summing the per-status chip counts on the tabs. */} - 0 ? `${option.color}18` : '#f1f5f9', - color: total > 0 ? option.color : '#94a3b8', - border: `1px solid ${total > 0 ? option.color + '55' : '#e2e8f0'}`, - '& .MuiChip-label': { px: 0.75 } - }} - /> - {selected && ( - 0 ? `${option.color}18` : '#f1f5f9', + color: total > 0 ? option.color : '#94a3b8', + border: `1px solid ${total > 0 ? option.color + '55' : '#e2e8f0'}`, + '& .MuiChip-label': { px: 0.75 } }} - > - - - )} -
  • - ); - }} - renderInput={(params) => ( - + /> + {selected && ( - + -
    - ) - }} - sx={{ - minWidth: { xs: 180, sm: 220, md: 240 }, - cursor: 'pointer', - '& .MuiOutlinedInput-root': { - borderRadius: '10px', - bgcolor: '#ffffff', - fontWeight: 600, - color: '#0f172a', - paddingRight: '8px', + )} + + ); + }} + renderInput={(params) => ( + + + + +
    + ) + }} + sx={{ + minWidth: { xs: 180, sm: 220, md: 240 }, cursor: 'pointer', - transition: 'border-color 0.15s, box-shadow 0.15s', - '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, - '&:hover fieldset': { borderColor: '#cbd5e1' }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }, - '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 } - }, - '& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: '#94a3b8' } - }} - /> - )} - /> - ); - })()} + '& .MuiOutlinedInput-root': { + borderRadius: '10px', + bgcolor: '#ffffff', + fontWeight: 600, + color: '#0f172a', + paddingRight: '8px', + cursor: 'pointer', + transition: 'border-color 0.15s, box-shadow 0.15s', + '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, + '&:hover fieldset': { borderColor: '#cbd5e1' }, + '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }, + '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 } + }, + '& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: '#94a3b8' } + }} + /> + )} + /> + ); + })()} - {/* Date range pill — opens the date picker. Stays visible only when + {/* Date range pill — opens the date picker. Stays visible only when a date range is set. Uses the same pill aesthetic as Batch. */} - {startdate && enddate && ( - setOpen(true)} - sx={{ - pl: 0.5, - pr: 1.25, - py: 0.5, - borderRadius: '10px', - cursor: 'pointer', - bgcolor: '#ffffff', - border: '1px solid #e2e8f0', - transition: 'border-color 0.15s', - '&:hover': { borderColor: '#cbd5e1' } - }} - > - - - - - {dayjs(startdate).format('DD MMM')} – {dayjs(enddate).format('DD MMM')} - - - )} + {startdate && enddate && ( + setOpen(true)} + sx={{ + pl: 0.5, + pr: 1.25, + py: 0.5, + borderRadius: '10px', + cursor: 'pointer', + bgcolor: '#ffffff', + border: '1px solid #e2e8f0', + transition: 'border-color 0.15s', + '&:hover': { borderColor: '#cbd5e1' } + }} + > + + + + + {dayjs(startdate).format('DD MMM')} – {dayjs(enddate).format('DD MMM')} + + + )} +
    + {/* Tenant */} + { + if (!appId) { + event.preventDefault(); + OpenToast('Please select your zone first!', 'warning', 3000); + setTimeout(() => locationRef.current?.focus(), 0); + } + }} + onChange={(e, val, reason) => { + if (reason === 'clear') { + setTenantid(0); + setTenantValue(null); + setLocationid(0); + setLocationValue(null); + } else { + setTenantid(val?.tenantid || 0); + setTenantValue(val); + setLocationid(0); + setLocationValue(null); + } + }} + renderInput={(params) => ( + + + + +
    + ) + }} + sx={pillFieldSx('#0ea5e9')} + /> + )} + /> + {/* Location */} + `${option.locationname} (${option.suburb})` || ''} + value={locationValue} + PaperComponent={SoftPaper} + sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }} + onOpen={(event) => { + if (!appId && !tenantid) { + event.preventDefault(); + OpenToast('Please select your Zone and Tenant first!', 'warning', 3000); + setTimeout(() => locationRef.current?.focus(), 0); + } else if (!tenantid) { + event.preventDefault(); + OpenToast('Please select your Tenant first!', 'warning', 3000); + setTimeout(() => tenantRef.current?.focus(), 0); + } + }} + onChange={(e, val, reason) => { + if (reason === 'clear') { + setLocationid(0); + setLocationValue(null); + } else { + setLocationid(val.locationid || 0); + setLocationValue(val); + } + }} + renderInput={(params) => ( + + + + + + ) + }} + sx={pillFieldSx('#14b8a6')} + /> + )} + /> + {/* Rider */} + `${option.displayname || option.authname || ''} (${option.contactno || ''})`} + sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }} + onChange={(e, value, reason) => { + if (reason === 'clear') setRiderid(0); + else setRiderid(value.userid); + }} + onOpen={(event) => { + if (!appId) { + event.preventDefault(); + OpenToast('Please select your zone first!', 'warning', 3000); + setTimeout(() => locationRef.current?.focus(), 0); + } + }} + renderInput={(params) => ( + + + + + + ) + }} + sx={pillFieldSx('#8b5cf6')} + /> + )} + /> - - {/* Tenant */} - { - if (!appId) { - event.preventDefault(); - OpenToast('Please select your zone first!', 'warning', 3000); - setTimeout(() => locationRef.current?.focus(), 0); - } - }} - onChange={(e, val, reason) => { - if (reason === 'clear') { - setTenantid(0); - setTenantValue(null); - setLocationid(0); - setLocationValue(null); - } else { - setTenantid(val?.tenantid || 0); - setTenantValue(val); - setLocationid(0); - setLocationValue(null); - } - }} - renderInput={(params) => ( - - - - ) - }} - sx={pillFieldSx('#0ea5e9')} - /> - )} - /> - {/* Location */} - `${option.locationname} (${option.suburb})` || ''} - value={locationValue} - PaperComponent={SoftPaper} - sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }} - onOpen={(event) => { - if (!appId && !tenantid) { - event.preventDefault(); - OpenToast('Please select your Zone and Tenant first!', 'warning', 3000); - setTimeout(() => locationRef.current?.focus(), 0); - } else if (!tenantid) { - event.preventDefault(); - OpenToast('Please select your Tenant first!', 'warning', 3000); - setTimeout(() => tenantRef.current?.focus(), 0); - } - }} - onChange={(e, val, reason) => { - if (reason === 'clear') { - setLocationid(0); - setLocationValue(null); - } else { - setLocationid(val.locationid || 0); - setLocationValue(val); - } - }} - renderInput={(params) => ( - - - - ) - }} - sx={pillFieldSx('#14b8a6')} - /> - )} - /> - {/* Rider */} - `${option.firstname} ${option.lastname} (${option.contactno})`} - sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }} - onChange={(e, value, reason) => { - if (reason === 'clear') setRiderid(0); - else setRiderid(value.userid); - }} - onOpen={(event) => { - if (!appId) { - event.preventDefault(); - OpenToast('Please select your zone first!', 'warning', 3000); - setTimeout(() => locationRef.current?.focus(), 0); - } - }} - renderInput={(params) => ( - - - - ) - }} - sx={pillFieldSx('#8b5cf6')} - /> - )} - /> -
    {/* ============================================= || Status Tabs + Search || ============================================= */} { background: '#fff' }} > - + { const showSelect = tabstatus == 'Created'; const totalCols = 15 + (showAction ? 1 : 0) + (showSelect ? 1 : 0); return isMobile ? ( - /* ===================== MOBILE: card list ===================== */ - - {filteredRows.length === 0 && !loading1 && !countSourceLoading && ( - - - - - - No deliveries to show - - - {selectedBatch === 'all' - ? `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders for this filter.` - : `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders in ${BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch'}.`} - - - )} - {filteredRows.length === 0 && countSourceLoading && ( - - - - Loading deliveries… - - - )} - {filteredRows.map((row, index) => { - const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || { - label: row.orderstatus || '—', - color: '#94a3b8', - icon: MdHistoryToggleOff - }; - const RowStatusIcon = rowStatusMeta.icon; - const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); - const isOpen = productCollapse?.orderid === row?.orderid; - const chipSx = (c) => ({ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: c, fontWeight: 700, fontSize: 12, whiteSpace: 'nowrap' }); - return ( - - - - {showSelect && ( - { - if (e.target.checked) { - let arr = deliverylist; - arr.push({ ...row, sno: deliverylist.length + 1 }); - setDeliverylist([...arr]); - } else { - let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); - if (res) { - let arr = deliverylist; - arr.splice(res.sno - 1, 1); - arr.map((val, i) => { - val.sno = i + 1; - }); - setDeliverylist([...arr]); - } - } - }} - checked={!!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)} - /> - )} - - {String(page * rowsPerPage + index + 1).padStart(2, '0')} - - - - - - - {rowStatusMeta.label} - - - - - {row.deliverytype == 'C' && ( - { - if (productCollapse?.orderid === row.orderid) { - setProductCollapse(null); - setOrderHeaderId(null); - } else { - setProductCollapse(row); - setOrderHeaderId(row.orderheaderid); - } - }} - sx={{ borderRadius: 999, bgcolor: tint('#06b6d4'), color: '#06b6d4', border: `1px solid ${edge('#06b6d4')}`, '&:hover': { bgcolor: soft('#06b6d4') } }} - > - {isOpen ? : } - - )} - {showAction && ( - handleMenuOpen(e, row)} - sx={{ borderRadius: 999, bgcolor: tint('#C01227'), color: '#C01227', border: `1px solid ${edge('#C01227')}`, '&:hover': { bgcolor: soft('#C01227') } }} - > - - - )} - - - - - {row.tenantname} - - - {[row.tenantsuburb, row.applocation].filter(Boolean).join(' · ') || '—'} - - - - } - > - - - - {`${row.locationname}-(${row.locationsuburb})`} - - - {row.orderid} · {row.deliveryid} - - - - - {row.pickupcustomer || '—'} - - - {row.pickupcontactno} - - - - - {row.deliverycustomer || '—'} - - - {row.deliverycontactno} - - - - {row.ridername ? ( - - - - - - - {row.ridername} - - - ID #{row.userid} · {row.ridercontact || '—'} - - - - ) : ( - - Unassigned - - )} - - - - {row.transitminutes || 0}m - - - - {row.kms || 0} km - {row.cumulativekms || 0} km - - - - - ₹ {row.deliverycharges?.toFixed(2) ?? '0.00'} - ₹ {row.deliveryamt?.toFixed(2) ?? '0.00'} - - - - - - {row.collectionamt ? `₹ ${row.collectionamt.toFixed(2)}` : '—'} - - - - {row.step ? ( - {row.step} - ) : ( - - )} - - {row.notes && ( - - {row.notes} - - )} - - {isOpen && ( - - - - - Product Details - - - - {orderdetails?.details?.map((product, idx2) => ( - - - - - {product?.productname || 'Unnamed'} - - - Qty {product?.orderqty || 0} · ₹ {product?.price || 0} - - - - ₹ {(product?.productsumprice + product?.taxamount).toFixed(2) || 0} - - - ))} - - Total Amount - - ₹ {orderdetails?.pricedetails?.orderamount?.toFixed(2)} - - - - - )} - - ); - })} - {countSourceRows?.length != 0 && ( -
    - {countIsFetchingNext || countHasNext ? ( - - ) : ( - - · End of list · - + /* ===================== MOBILE: card list ===================== */ + + {filteredRows.length === 0 && !loading1 && !countSourceLoading && ( + + + + + + No deliveries to show + + + {selectedBatch === 'all' + ? `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders for this filter.` + : `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders in ${ + BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch' + }.`} + + )} -
    - )} - - ) : ( - - - - {showSelect && ( - - 0 && deliverylist.length != filteredRows.length} - onChange={(e) => { - if (e.target.checked) { - setDeliverylist([...filteredRows]); - } else { - setDeliverylist([]); - } - }} - checked={deliverylist.length > 0 && deliverylist.length == filteredRows.length} - /> - - )} - # - Status - Tenant - Order / Location - Pickup - Drop - Rider - ETA - Transit - Kms - Amount - Notes - Step - Qty - COD - {showAction && Action} - - - {(loading1 || countSourceIsLoading) && } - - {filteredRows.length == 0 && !loading1 && !countSourceLoading && ( - - - - - - - - No deliveries to show - - - {selectedBatch === 'all' - ? `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders for this filter.` - : `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders in ${BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch'}.`} - - - - - )} - {filteredRows.length == 0 && countSourceLoading && ( - - + {filteredRows.length === 0 && countSourceLoading && ( + - + Loading deliveries… - - - )} - {filteredRows.map((row, index) => { - const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || { - label: row.orderstatus || '—', - color: '#94a3b8', - icon: MdHistoryToggleOff - }; - const RowStatusIcon = rowStatusMeta.icon; - const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); - return ( - - - {tabstatus == 'Created' && ( - - { - if (e.target.checked) { - let arr = deliverylist; - arr.push({ ...row, sno: deliverylist.length + 1 }); - setDeliverylist([...arr]); - } else { - let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); - if (res) { - let arr = deliverylist; - let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); - arr.splice(res.sno - 1, 1); - arr.map((val, i) => { - val.sno = i + 1; - }); - setDeliverylist([...arr]); - } - } - console.log('list', deliverylist); - }} - checked={deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)} - /> - - )} - - - {String(page * rowsPerPage + index + 1).padStart(2, '0')} - - - {/* Status badge */} - - - - - - - {rowStatusMeta.label} - - - - {/* Tenants */} - - - - - {row.tenantname} - - - {row.tenantsuburb} - - - {row.applocation} - - - - - {/* order details */} - - - - {`${row.locationname}-(${row.locationsuburb})`} - - - - - - - - {row.orderid} - - - - - {dayjs(row.orderdate).utc().format('DD/MM/YYYY')} - - - {dayjs(row.orderdate).utc().format('hh:mm A')} - - - - - - - - - {row.deliveryid} - - - - - {dayjs(row.deliverydate).utc().format('DD/MM/YYYY')} - - - - {dayjs(row.deliverydate).utc().format('hh:mm A')} - - - - - - {/* pickup */} - - - - {row.pickupcustomer} - - {row.pickupcontactno} - - - {row.pickuplocation || (row.Pickupaddress ? row.Pickupaddress.slice(0, 14) + '…' : '—')} - - - - - {/* drop */} - - - - {row.deliverycustomer} - - {row.deliverycontactno} - - - {row.deliverylocation || (row.deliveryaddress ? row.deliveryaddress.slice(0, 14) + '…' : '—')} - - - - - {/* rider */} - - {row.ridername ? ( - - - - - - - {row.ridername} - - - ID #{row.userid} · {row.ridercontact || '—'} + + )} + {filteredRows.map((row, index) => { + const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || { + label: row.orderstatus || '—', + color: '#94a3b8', + icon: MdHistoryToggleOff + }; + const RowStatusIcon = rowStatusMeta.icon; + const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); + const isOpen = productCollapse?.orderid === row?.orderid; + const chipSx = (c) => ({ + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + color: c, + fontWeight: 700, + fontSize: 12, + whiteSpace: 'nowrap' + }); + return ( + + + + {showSelect && ( + { + if (e.target.checked) { + let arr = deliverylist; + arr.push({ ...row, sno: deliverylist.length + 1 }); + setDeliverylist([...arr]); + } else { + let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); + if (res) { + let arr = deliverylist; + arr.splice(res.sno - 1, 1); + arr.map((val, i) => { + val.sno = i + 1; + }); + setDeliverylist([...arr]); + } + } + }} + checked={!!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)} + /> + )} + + {String(page * rowsPerPage + index + 1).padStart(2, '0')} + + + + + + {rowStatusMeta.label} + + - - ) : ( - - Unassigned - - )} - - {/* Estimated Delivery Time */} - - - {row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : '—'} - - - {/* Transit Minutes */} - - - {row.transitminutes || 0}m - - - {/* kms */} - - - - - {row.kms || 0} km - - - - - {row.cumulativekms || 0} km - - - - - {/* amount */} - - - - - ₹ {row.deliverycharges?.toFixed(2) ?? '0.00'} - - - - - ₹ {row.deliveryamt?.toFixed(2) ?? '0.00'} - - - - - {/* notes */} - - {row.notes ? ( - - - {row.notes} - - - ) : ( - - )} - - {/* step */} - - {row.step ? ( - - {row.step} - - ) : ( - - )} - - {/* qty */} - - - {row.Quantity || '—'} - - - {/* COD */} - - - {row.collectionamt ? `₹ ${row.collectionamt.toFixed(2)}` : '—'} - - - {/* Action */} - {tabstatus !== 'Cancelled' && tabstatus !== 'Delivered' && ( - - - {row.deliverytype == 'C' && ( - + + {row.deliverytype == 'C' && ( { @@ -2071,147 +1498,852 @@ const Deliveries = () => { '&:hover': { bgcolor: soft('#06b6d4') } }} > - {productCollapse?.orderid === row.orderid ? : } + {isOpen ? : } - - )} - - - handleMenuOpen(e, row)} - sx={{ - borderRadius: 999, - bgcolor: tint('#C01227'), - color: '#C01227', - border: `1px solid ${edge('#C01227')}`, - '&:hover': { bgcolor: soft('#C01227') } - }} - > - - - + )} + {showAction && ( + handleMenuOpen(e, row)} + sx={{ + borderRadius: 999, + bgcolor: tint('#C01227'), + color: '#C01227', + border: `1px solid ${edge('#C01227')}`, + '&:hover': { bgcolor: soft('#C01227') } + }} + > + + + )} + - - )} - - {productCollapse?.orderid === row?.orderid && ( - - - - - + + {row.tenantname} + + + {[row.tenantsuburb, row.applocation].filter(Boolean).join(' · ') || '—'} + + + + } + > + + + + {`${row.locationname}-(${row.locationsuburb})`} + + + {row.orderid} · {row.deliveryid} + + + + + {row.pickupcustomer || '—'} + + + {row.pickupcontactno} + + + + + {row.deliverycustomer || '—'} + + + {row.deliverycontactno} + + + + {row.ridername ? ( + + + + + + + {row.ridername} + + + ID #{row.userid} · {row.ridercontact || '—'} + + + + ) : ( + + Unassigned + + )} + + + + {row.transitminutes || 0}m + + + + {row.kms || 0} km + {row.cumulativekms || 0} km + + + + + ₹ {row.deliverycharges?.toFixed(2) ?? '0.00'} + ₹ {row.deliveryamt?.toFixed(2) ?? '0.00'} + + + + + + {row.collectionamt ? `₹ ${row.collectionamt.toFixed(2)}` : '—'} + + + + {row.step ? ( + {row.step} + ) : ( + + — + + )} + + {row.notes && ( + + + {row.notes} + + + )} + + {isOpen && ( + + + + + + + Product Details + + + + {/* GET /admin/bookings has no product image/qty/tax breakdown — this + renders the real "parcels" array (itemcategory/itemdescription/ + declaredvalue) instead of the old product-catalog shape. */} + {orderdetails?.parcels?.map((parcel, idx2) => ( + - - - - Product Details + + + + + + {parcel?.itemdescription || parcel?.itemcategory || 'Item'} + + + {parcel?.itemcategory || ''} {parcel?.weight ? `· ${parcel.weight}kg` : ''} -
    - - + ₹ {(parcel?.declaredvalue || 0).toFixed(2)} + + + ))} + + + Delivery Charge + + + ₹ {(orderdetails?.serviceoptions?.[0]?.estimatedprice || 0).toFixed(2)} + + + + + )} + + ); + })} + {countSourceRows?.length != 0 && ( +
    + {countIsFetchingNext || countHasNext ? ( + + ) : ( + + · End of list · + + )} +
    + )} + + ) : ( +
    + + + {showSelect && ( + + 0 && deliverylist.length != filteredRows.length} + onChange={(e) => { + if (e.target.checked) { + setDeliverylist([...filteredRows]); + } else { + setDeliverylist([]); + } + }} + checked={deliverylist.length > 0 && deliverylist.length == filteredRows.length} + /> + + )} + # + Status + Tenant + Order / Location + Pickup + Drop + Rider + ETA + Transit + Kms + Amount + Notes + Step + Qty + COD + {showAction && Action} + + + {(loading1 || countSourceIsLoading) && } + + {filteredRows.length == 0 && !loading1 && !countSourceLoading && ( + + + + + + + + No deliveries to show + + + {selectedBatch === 'all' + ? `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders for this filter.` + : `No ${(STATUS_META[currentStatus]?.label || tabstatus).toLowerCase()} orders in ${ + BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch' + }.`} + + + + + )} + {filteredRows.length == 0 && countSourceLoading && ( + + + + + Loading deliveries… + + + + )} + {filteredRows.map((row, index) => { + const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || { + label: row.orderstatus || '—', + color: '#94a3b8', + icon: MdHistoryToggleOff + }; + const RowStatusIcon = rowStatusMeta.icon; + const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); + return ( + + + {tabstatus == 'Created' && ( + + { + if (e.target.checked) { + let arr = deliverylist; + arr.push({ ...row, sno: deliverylist.length + 1 }); + setDeliverylist([...arr]); + } else { + let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); + if (res) { + let arr = deliverylist; + let res = deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid); + arr.splice(res.sno - 1, 1); + arr.map((val, i) => { + val.sno = i + 1; + }); + setDeliverylist([...arr]); + } + } + console.log('list', deliverylist); + }} + checked={deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)} + /> + + )} + + + {String(page * rowsPerPage + index + 1).padStart(2, '0')} + + + {/* Status badge */} + + + + + + + {rowStatusMeta.label} + + + + {/* Tenants */} + + + + + {row.tenantname} + + + {row.tenantsuburb} + + + {row.applocation} + + + + + {/* order details */} + + + + {`${row.locationname}-(${row.locationsuburb})`} + + + + + + + + {row.orderid} + + + + + {dayjs(row.orderdate).utc().format('DD/MM/YYYY')} + + + {dayjs(row.orderdate).utc().format('hh:mm A')} + + + + - + + + + {row.deliveryid} + + + + + {dayjs(row.deliverydate).utc().format('DD/MM/YYYY')} + + + + {dayjs(row.deliverydate).utc().format('hh:mm A')} + + + + + + {/* pickup */} + + + + {row.pickupcustomer} + + + {row.pickupcontactno} + + + + {row.pickuplocation || (row.Pickupaddress ? row.Pickupaddress.slice(0, 14) + '…' : '—')} + + + + + {/* drop */} + + + + {row.deliverycustomer} + + + {row.deliverycontactno} + + + + {row.deliverylocation || (row.deliveryaddress ? row.deliveryaddress.slice(0, 14) + '…' : '—')} + + + + + {/* rider */} + + {row.ridername ? ( + + + + + + + {row.ridername} + + + ID #{row.userid} · {row.ridercontact || '—'} + + + + ) : ( + + Unassigned + + )} + + {/* Estimated Delivery Time */} + + + {row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : '—'} + + + {/* Transit Minutes */} + + + {row.transitminutes || 0}m + + + {/* kms */} + + + + + {row.kms || 0} km + + + + + {row.cumulativekms || 0} km + + + + + {/* amount */} + + + + + ₹ {row.deliverycharges?.toFixed(2) ?? '0.00'} + + + + + ₹ {row.deliveryamt?.toFixed(2) ?? '0.00'} + + + + + {/* notes */} + + {row.notes ? ( + + + {row.notes} + + + ) : ( + + — + + )} + + {/* step */} + + {row.step ? ( + + {row.step} + + ) : ( + + — + + )} + + {/* qty */} + + + {row.Quantity || '—'} + + + {/* COD */} + + + {row.collectionamt ? `₹ ${row.collectionamt.toFixed(2)}` : '—'} + + + {/* Action */} + {tabstatus !== 'Cancelled' && tabstatus !== 'Delivered' && ( + + + {row.deliverytype == 'C' && ( + + { + if (productCollapse?.orderid === row.orderid) { + setProductCollapse(null); + setOrderHeaderId(null); + } else { + setProductCollapse(row); + setOrderHeaderId(row.orderheaderid); + } + }} sx={{ - '& th': { - fontSize: 11, - fontWeight: 800, - letterSpacing: 0.5, - textTransform: 'uppercase', - color: DT.textSecondary, - borderBottom: `1px solid ${DT.divider}` - }, - '&:hover': { backgroundColor: 'transparent !important' } + borderRadius: 999, + bgcolor: tint('#06b6d4'), + color: '#06b6d4', + border: `1px solid ${edge('#06b6d4')}`, + '&:hover': { bgcolor: soft('#06b6d4') } }} > - # - Product - Description - Qty - Cost - Price - Tax - Amount - - - - {orderdetails?.details?.map((product, idx2) => ( - + ) : ( + + )} + + + )} + + + handleMenuOpen(e, row)} + sx={{ + borderRadius: 999, + bgcolor: tint('#C01227'), + color: '#C01227', + border: `1px solid ${edge('#C01227')}`, + '&:hover': { bgcolor: soft('#C01227') } + }} + > + + + + + + )} + + {productCollapse?.orderid === row?.orderid && ( + + + + + + + + + + - {String(idx2 + 1).padStart(2, '0')} - - - - - {product?.productname || 'Unnamed'} - - - - {product?.productdescription || '-'} - {product?.orderqty || 0} - ₹ {product?.price || 0} - ₹ {(product?.productsumprice ?? 0).toFixed(2)} - ₹ {(product?.taxamount ?? 0).toFixed(2)} - - ₹ {(product?.productsumprice + product?.taxamount).toFixed(2) || 0} - - - ))} - - - Total Amount - - - ₹ {orderdetails?.pricedetails?.orderamount?.toFixed(2)} - - - -
    -
    - - - - - )} - - ); - })} - {countSourceRows?.length != 0 && ( - - -
    - {countIsFetchingNext ? : countHasNext ? : ( - - · End of list · - - )} -
    -
    -
    - )} - - + Product Details + + + + + + # + Item + Category + Weight + Declared Value + + + + {/* GET /admin/bookings has no product image/qty/tax breakdown — + this renders the real "parcels" array instead of the old + product-catalog shape. */} + {orderdetails?.parcels?.map((parcel, idx2) => ( + + + {String(idx2 + 1).padStart(2, '0')} + + + + + + + + {parcel?.itemdescription || 'Item'} + + + + {parcel?.itemcategory || '-'} + + {parcel?.weight ? `${parcel.weight}kg` : '-'} + + + ₹ {(parcel?.declaredvalue || 0).toFixed(2)} + + + ))} + + + Delivery Charge + + + ₹ {(orderdetails?.serviceoptions?.[0]?.estimatedprice || 0).toFixed(2)} + + + +
    + + + + + + )} + + ); + })} + {countSourceRows?.length != 0 && ( + + +
    + {countIsFetchingNext ? ( + + ) : countHasNext ? ( + + ) : ( + + · End of list · + + )} +
    +
    +
    + )} + + ); })()} @@ -2250,11 +2382,13 @@ const Deliveries = () => { {selectedRow?.orderstatus !== 'delivered' && ( { - notifyRiderMutation.mutate(selectedRow.userfcmtoken); + notifyRiderMutation.mutate(selectedRow.milerprofileid); handleMenuClose(); }} > - + + + Notify Rider )} @@ -2270,7 +2404,9 @@ const Deliveries = () => { handleMenuClose(); }} > - + + + Change Rider )} @@ -2282,14 +2418,16 @@ const Deliveries = () => { setDeliverylat(selectedRow.droplat); setDeliverylong(selectedRow.droplon); setNotes(selectedRow.notes); - setDeliveryamount(selectedRow.deliveryamount); + setDeliveryamount(selectedRow.deliveryamt ?? selectedRow.deliveryamount); setUpdateStatus(selectedRow.orderstatus || 'delivered'); setCurrentorder(selectedRow); setDialogopen(true); handleMenuClose(); }} > - + + + Update Status )} @@ -2301,7 +2439,9 @@ const Deliveries = () => { handleMenuClose(); }} > - + + + Cancel Delivery )} @@ -2438,7 +2578,7 @@ const Deliveries = () => { fullWidth options={ridersList} PaperComponent={SoftPaper} - getOptionLabel={(option) => `${option.firstname} ${option.lastname} (${option.contactno})`} + getOptionLabel={(option) => `${option.displayname || option.authname || ''} (${option.contactno || ''})`} onChange={(e, value) => { setSelectedRider(value); logger.debug('Rider selected in dropdown:', value ? `${value.firstname} ${value.lastname}` : 'None'); @@ -2451,7 +2591,9 @@ const Deliveries = () => { ...params.InputProps, startAdornment: ( - + + + ) }} @@ -2482,7 +2624,9 @@ const Deliveries = () => { variant="contained" onClick={() => { setLoading1(true); - logger.info(`Initiating rider assignment change for order ID ${selectedRow?.orderid} to rider: ${selectedRider?.firstname} ${selectedRider?.lastname}`); + logger.info( + `Initiating rider assignment change for order ID ${selectedRow?.orderid} to rider: ${selectedRider?.displayname || selectedRider?.authname}` + ); changeRiderMutation.mutate({ selectedRider, selectedRow }); }} sx={{ @@ -2667,7 +2811,10 @@ const Deliveries = () => { { label: 'Delivery Longitude', value: deliverylong, set: (v) => setDeliverylong(v), color: '#06b6d4' } ].map((f) => ( - + {f.label} { ))} - + Amount { /> - + Status { } }} > - {['pending','accepted','started','arrived','delivered','cancelled'].map((s) => { + {['pending', 'accepted', 'started', 'arrived', 'delivered', 'cancelled'].map((s) => { const m = STATUS_META[s] || { label: s, color: '#C01227', icon: MdHistoryToggleOff }; const Ic = m.icon; return ( - + + + {m.label} ); @@ -2746,7 +2901,10 @@ const Deliveries = () => { - + Notes window.removeEventListener('keydown', onKey); }, [riderPositionModal]); - // TODO: wire to real tenant context once the standalone Dispatch screen - // surfaces it. 916 matches the example tenant in the API spec. - const ANALYSIS_TENANT_ID = 916; + // Was hardcoded to 916 (the example tenant in the API spec) — every + // operator's batch-efficiency analytics queried the same tenant regardless + // of who was actually logged in. Reads the real session tenant now, same + // as the rest of the app (login.js sets this on sign-in). + const ANALYSIS_TENANT_ID = localStorage.getItem('tenantid') || undefined; const batchEfficiencyMutation = useMutation({ mutationFn: fetchBatchEfficiency, @@ -1517,11 +1519,12 @@ const Dispatch = ({ plannedMapRendererRef.current = L.canvas({ padding: 1.5, tolerance: 5 }); } - // Pull the partners/getriderlogs feed for the currently selected hub + date. - // This endpoint returns the exact live GPS position for every rider at the - // hub (latitude/longitude/logdate/status). We render those positions as - // markers on the main dispatch map so the operator sees where each rider - // actually is — matching the Reports → Riders Logs page. + // Live rider position feed. Was dead for a while during the backend + // migration (fetchRidersLogs was a permanent [] stub) — now sourced from + // GET /admin/milers/summary (confirmed in jupiter2doormile.md), which + // carries each rider's currentlatitude/currentlongitude/lastpingat. We + // render those positions as markers on the main dispatch map so the + // operator sees where each rider actually is. const RIDER_LOG_POLL_MS = 1000; const { data: ridersLocationLogs } = useQuery({ queryKey: [selectedAppLocationId, selectedDate, ''], @@ -2197,17 +2200,20 @@ const Dispatch = ({ queries: focusedRiderDeliveryIds.map((deliveryid) => ({ queryKey: ['deliveryLogs', deliveryid], queryFn: async () => { - const res = await axios.get( - `${process.env.REACT_APP_URL3}/deliveries/getdeliverylogs/?deliveryid=${deliveryid}` - ); - // Accept several possible response shapes — the live API has shipped - // {details:[…]}, plain arrays, and {data:[…]} variants over time, and - // any of those should produce a polyline. Pick the first non-empty - // array-like found. - const candidates = [res?.data?.details, res?.data?.data, res?.data, res]; - const rows = candidates.find((c) => Array.isArray(c)) || []; - // Also accept lat/lng (alternate naming) in case the endpoint ever - // returns the same shape the front-end uses internally. + // GET /admin/consignments/:id/logs is confirmed (jupiter2doormile.md, + // Status: Done) as the GPS/status trail for one consignment — but + // this file's own `deliveryid` here has never been confirmed to + // actually BE a consignmentid (Dispatch.js's data source hasn't been + // audited against the new API the way orders.js/deliveries.js have). + // If it's really a bookingid, this 404s and rows stays empty — same + // degraded-but-not-crashing behaviour as before. + let rows = []; + try { + rows = (await getConsignmentLogs(deliveryid)) || []; + if (!Array.isArray(rows)) rows = []; + } catch (err) { + rows = []; + } // Sort by logdate ascending so the polyline follows the rider's // chronological path. The endpoint isn't guaranteed to return rows // in order — without this, consecutive points can be out of sequence @@ -5121,14 +5127,14 @@ const Dispatch = ({ {renderMarkers()} {renderRoutes()} - {/* Live rider GPS markers from /partners/getriderlogs/. Mirrors the - Reports → Riders Logs map: green pin when the rider's last log - row is `active`, red otherwise, with the rider's username as a + {/* Live rider GPS markers from GET /admin/milers/summary (see + fetchRidersLogs in api.js). Green pin when the rider's status + is `active`, red otherwise, with the rider's username as a label. Scoped to riders who actually have orders in the currently selected slot — `riders` is derived from filteredLiveRows so it already reflects the slot filter. A rider with zero orders in the current slot is hidden, even if - getriderlogs still returns their GPS row. When a specific + the summary still returns their GPS row. When a specific rider is focused, only that one is shown. */} {liveRiderLocations .filter((r) => diff --git a/src/pages/nearle/login.js b/src/pages/nearle/login.js index 50a0f88..080687b 100644 --- a/src/pages/nearle/login.js +++ b/src/pages/nearle/login.js @@ -1,15 +1,13 @@ import { useState, useEffect } from 'react'; -import { enqueueSnackbar } from 'notistack'; -import axios from 'axios'; import { useNavigate } from 'react-router-dom'; import Loader from 'components/Loader'; import logo from 'assets/images/doormile-logo.png'; -import { useSelector, useDispatch } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { OpenToast } from 'components/third-party/OpenToast'; -import { closeGlobalToast, GlobalToast } from 'components/nearle_components/GlobalToast'; import { setLoginUser } from 'store/reducers/loginUserSlice'; import { markSessionStart } from 'utils/session'; +import { loginAdmin } from 'pages/api/doormileApi'; import { DT } from 'themes/dt/tokens'; // Astryx design system — see themes/astryx.js for the Doormile brand theme @@ -71,150 +69,56 @@ const BULLETS = ['Real-time fleet visibility', 'AI-optimised dispatch routes', ' const Login = () => { const dispatch = useDispatch(); - const fcmtoken = useSelector((state) => state.fcm); const [loading, setLoading] = useState(false); - let navigate = useNavigate(); - const [otp, setOtp] = useState(''); - const [userinfo, setUserinfo] = useState({}); - const [username, setUsername] = useState(''); - const [passwordStatus, setPasswordStatus] = useState(0); + const navigate = useNavigate(); + const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [isPassword, setIspassword] = useState(false); - const [userid, setUserid] = useState(0); useEffect(() => { - if (localStorage.getItem('firstname')) { + // 'authname' is the AUTH_PRESENCE_KEY (utils/session.js) — App.js and the + // inactivity/cross-tab logout machinery all key off it, unrelated to which + // backend actually authenticated the session. + if (localStorage.getItem('authname')) { navigate('/doormile/dispatch'); } }, []); - const loginsend = async () => { - setLoading(true); + const opentoast = (message, variant = 'error') => OpenToast(message, variant, 2000); - if (!username) { - opentoast('Fill All required fields'); - setLoading(false); + const handleSubmit = async (e) => { + e.preventDefault(); + if (!email || !password) { + opentoast('Enter your email and password'); return; } + setLoading(true); try { - const res = await axios.post(`https://jupiter.nearle.app/live/api/v1/users/console/login`, { - authname: username, - configid: 9, // 9 -> config id for nearle console admin - userfcmtoken: fcmtoken?.token, - password - }); - // user not found - if (res.data.code == 409 && !res.data.status) { - OpenToast(res.data.message, 'error', 3000); - } - // user not activated - else if (res.data.code == 403) { - OpenToast(res.data.message, 'warning', 3000); - } - //user found, no password, setup password - else if (res.data.code == 409 && res.data.status) { - setPasswordStatus(1); // for password and confirm password ui - setUserid(res.data.details.userid); - OpenToast('User Found', 'success', 3000); - OpenToast(res.data.message, 'success', 3000); - } - //user found, incorrect password - else if (res.data.code == 401 && !res.data.status) { - OpenToast(res.data.message, 'error', 3000); - } - //user found, enter password - else if (res.data.code == 401 && res.data.status) { - OpenToast(res.data.message, 'success', 3000); - fetchAppLocations(res.data.userid); - setPasswordStatus(2); - } - // user found, correct password - else if (res.data.code == 200 && res.data.status) { - OpenToast(res.data.message, 'success', 1000); - setUserinfo(res.data.details); - const userinfo = res.data.details; - dispatch(setLoginUser(userinfo)); - localStorage.setItem('firstname', userinfo.firstname); - localStorage.setItem('authname', userinfo.authname); - localStorage.setItem('roleid', userinfo.roleid); - localStorage.setItem('tenantid', userinfo.tenantid); - localStorage.setItem('partnerid', userinfo.partnerid); - localStorage.setItem('applocationid', userinfo.applocationid); - localStorage.setItem('userid', userinfo.userid); - localStorage.setItem('userfcmtoken', userinfo.userfcmtoken); + const res = await loginAdmin(email, password); + if (res.success) { + const user = res.user || {}; + // Compatibility keys: existing pages/api.js read userid/roleid/tenantid + // straight from localStorage rather than through a context. 'authname' + // stays the AUTH_PRESENCE_KEY so the rest of the session contract + // (useInactivityLogout, cross-tab logout, App.js gate) needs no changes. + localStorage.setItem('authname', user.email || user.name || email); + localStorage.setItem('firstname', user.name || ''); + localStorage.setItem('userid', user.id ?? ''); + localStorage.setItem('roleid', user.role ?? ''); + localStorage.setItem('tenantid', user.tenantid ?? ''); + dispatch(setLoginUser(user)); markSessionStart(); - fetchAppLocations(userinfo.userid); + opentoast('Login successful', 'success'); navigate('/doormile/dispatch'); } else { - OpenToast(res.data.message, 'error', 3000); + opentoast(res.message || 'Login failed'); } } catch (err) { - console.error(err); - OpenToast(err.message, 'error', 5000); + opentoast(err?.message || 'Login failed'); } finally { setLoading(false); } }; - const loginsuccessful = () => { - localStorage.setItem('firstname', userinfo.firstname); - localStorage.setItem('authname', userinfo.authname); - localStorage.setItem('roleid', userinfo.roleid); - localStorage.setItem('tenantid', userinfo.tenantid); - localStorage.setItem('partnerid', userinfo.partnerid); - localStorage.setItem('applocationid', userinfo.applocationid); - localStorage.setItem('userid', userinfo.userid); - localStorage.setItem('userfcmtoken', userinfo.userfcmtoken); - markSessionStart(); - closeGlobalToast(); // to close the pin snackbar - - navigate('/doormile/dispatch'); - }; - - const opentoast = (message) => { - enqueueSnackbar(message, { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 1500 - }); - }; - - const fetchAppLocations = async (id) => { - const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${id}`); - const updatedLocations = [...response.data.details, { locationname: 'All', applocationid: 0 }]; - localStorage.setItem('applocations', JSON.stringify(updatedLocations)); - }; - const updateUser = async () => { - const response = await axios.put(`${process.env.REACT_APP_URL2}/users/update`, { - userid, - password - }); - if (response.data.status) { - OpenToast(response.data.message, 'success', 3000); - OpenToast('Enter Password to Login', 'success', 3000); - setPasswordStatus(2); - setPassword(''); - } - }; - - const handleSubmit = (e) => { - e.preventDefault(); - if (passwordStatus == 0) { - loginsend(); - } else if (passwordStatus == 1) { - if (!password || !confirmPassword || password != confirmPassword) { - OpenToast('Check Password', 'warning', 3000); - } else { - updateUser(); - } - } else if (passwordStatus == 2) { - if (!password) { - OpenToast('Invalid Password', 'warning', 3000); - } - loginsend(); - } - }; return ( @@ -266,75 +170,18 @@ const Login = () => { type="email" size="lg" isRequired - value={username} - onChange={(value) => setUsername(value.toLocaleLowerCase())} - isDisabled={!!passwordStatus} + value={email} + onChange={(value) => setEmail(value.toLocaleLowerCase())} /> - - {/* Setup Password */} - {passwordStatus == 1 && ( - - - setPassword(value)} - /> - setConfirmPassword(value)} - status={ - confirmPassword !== '' && password !== confirmPassword - ? { type: 'error', message: 'Passwords do not match' } - : undefined - } - /> - - )} - - {/* Enter Password */} - {passwordStatus == 2 && ( - - - setPassword(value)} - /> - - )} - - {/* OTP / retry */} - {isPassword && ( - - - Enter Password - { - setOtp(''); - loginsend(); - }} - > - Retry - - - setPassword(value)} isLabelHidden /> - - )} - - - - {/* */} - -
    - {/*
    */} - - - {/* */} - - - - - - - {/* */} - - - - - - © All rights reserved - {/* Privacy Policy */} - - - - - - Terms and Conditions - - - Privacy Policy - - {/* - CA Privacy Notice - */} - - - - - - - {/* */} - - ); -}; - -export default Login1; diff --git a/src/pages/nearle/orders/createorder1.js b/src/pages/nearle/orders/createorder1.js index ec02531..bf39c89 100644 --- a/src/pages/nearle/orders/createorder1.js +++ b/src/pages/nearle/orders/createorder1.js @@ -45,7 +45,7 @@ import { useNavigate } from 'react-router'; import { MdLocationCity } from 'react-icons/md'; import { TbMapPinCode } from 'react-icons/tb'; import { FaLocationDot } from 'react-icons/fa6'; -import axios from 'axios'; +import { getAdminPricing, getAdminTenant, getTenantCustomers, getTenantLocations, createExpressBooking } from 'pages/api/doormileApi'; import { useTheme } from '@mui/material/styles'; import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete'; import Loader from 'components/Loader'; @@ -127,9 +127,7 @@ const SectionHeader = ({ color, icon, title, subtitle, action }) => ( }} > - - {icon} - + {icon} {title} {subtitle && ( @@ -234,7 +232,6 @@ const Createorder1 = () => { { label: '12 Angry Men', year: 1957 } ]; - const appId = localStorage.getItem('applocationid'); const navigate = useNavigate(); const [open, setOpen] = useState({}); @@ -340,11 +337,13 @@ const Createorder1 = () => { const fetchTenantPricing = async () => { try { - const pricingResponse = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${tid}`); - console.log('pricingResponse', pricingResponse.data.details); - setBasePrice(pricingResponse.data.details.baseprice); - setPricePerKm(pricingResponse.data.details.priceperkm); - setMinKm(pricingResponse.data.details.minkm); + const pricing = (await getAdminPricing()) || []; + const match = pricing.find((p) => p.tenantid === tid); + if (match) { + setBasePrice(match.baseprice); + setPricePerKm(match.priceperkm); + setMinKm(match.basedistance); + } } catch (error) { console.log('fetchTenantPricing error', error); } @@ -450,109 +449,65 @@ const Createorder1 = () => { } }, [searchword]); - // ==================================================== || fetchtenantinfo || ==================================================== const fetchtenantinfo = async () => { setLoading(true); - console.log('tid', tid); - - await axios - .get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`) - .then((res) => { - console.log('fetchtenantinfo', res); - if (res.data.status) { - setTenant(res.data.details); - setTenantid(res.data.details.tenantid); - fetchAppAdminTokens(); - setSubCatId(res.data.details.subcategoryid); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); + try { + const info = await getAdminTenant(tid); + if (info) { + setTenant(info); + setTenantid(info.tenantid); + fetchAppAdminTokens(); + setSubCatId(info.subcategoryid); + } + } catch (err) { + console.log(err); + } finally { + setLoading(false); + } }; useEffect(() => { fetchtenantinfo(); }, []); - const getsubcategories = async () => { - await axios - .get(`${process.env.REACT_APP_URL}/utils/getsubcategories/?moduleid=6`) - .then((res) => { - console.log('subcateRes', res.data.details); - if (res.data.status && res.data.details) { - setSubCat(res.data.details); - } else { - setSubCat([]); - } - }) - .catch((err) => { - console.log(err); - setSubCat([]); - }); - }; + // No item-subcategory lookup endpoint in the new API. + const getsubcategories = async () => setSubCat([]); useEffect(() => { getsubcategories(); }, []); // ==================================================== || fetchTiming || ==================================================== + // No per-zone open/close-hours or geofence-radius endpoint in the new API + // (applocationid/"zones" aren't a resource there at all — see fetchAppLocations + // in pages/api/api.js). Falls back to a fixed 09:00–21:00 delivery window so + // the order-row / time-slot picker below still has slots to initialise from, + // and a generous default radius so the "too far" warning doesn't misfire. const fetchTiming = async () => { setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL}/utils/getapplocations/?applocationid=${appId}`) - .then((res) => { - console.log('fetchTiming', res); - const { opentime, closetime, latitude, longitude, radius } = res.data.details[0]; - if (res.data.status) { - setAppLocaLat(latitude); - setAppLocaLng(longitude); - setAppLocaRadius(radius); - setStatrttime(`${dayjs().format('MM-DD-YYYY')} ${opentime}`); - setEndtime(`${dayjs().format('MM-DD-YYYY')} ${closetime}`); - console.log('starttime', `${dayjs().format('MM-DD-YYYY')} ${opentime}`); - console.log('endtime', `${dayjs().format('MM-DD-YYYY')} ${closetime} `); - let arr = []; - for ( - let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`, j = 0; - dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0; - j++, i = dayjs(i).add(30, 'm') - ) { - arr.push(i); - } - console.log('setTimeslotarr', arr); - setTimeslotarr(arr); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); + const opentime = '09:00:00'; + const closetime = '21:00:00'; + setAppLocaLat(0); + setAppLocaLng(0); + setAppLocaRadius(100); + setStatrttime(`${dayjs().format('MM-DD-YYYY')} ${opentime}`); + setEndtime(`${dayjs().format('MM-DD-YYYY')} ${closetime}`); + let arr = []; + for ( + let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`; + dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0; + i = dayjs(i).add(30, 'm') + ) { + arr.push(i); + } + setTimeslotarr(arr); + setLoading(false); }; useEffect(() => { fetchTiming(); }, []); // =============================================== || fetchAppAdminTokens (via appId) || =============================================== - const fetchAppAdminTokens = async () => { - setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL}/utils/getapplocationconfig/?applocationid=${appId}`) - .then((res) => { - const userfcmtokemArray = res.data.details.applocationadmins.map((admin) => admin.userfcmtokem); - console.log('fetchAppAdminTokens', res); - console.log('userfcmtokemArray', userfcmtokemArray); - if (res.data.status) { - setAdmintoken(userfcmtokemArray); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; + // No zone-admin-token lookup endpoint in the new API. + const fetchAppAdminTokens = async () => setAdmintoken([]); useEffect(() => { if (starttime && endtime) { @@ -582,116 +537,47 @@ const Createorder1 = () => { // =============================================== || createsubmitobj1 (create orders) || =============================================== const createsubmitobj2 = async () => { - let arr = {}; - arr = { - orders: { - applocationid: tenant.applolcationid, - cancellled: '', - categoryid: +tenant.categoryid, - configid: 7, - customerid: isNumChange1 == 0 ? +pickCust.customerid || 0 : 0, - deliveryaddress: dropCust.address || '', - deliverycharge: +totalCharge.toFixed(2) || 0, - deliverycity: dropCust.city || '', - deliverycontactno: dropCust.contactno || '', - deliverycustomer: dropCust.firstname || '', - deliveryid: isNumChange2 == 0 ? +dropCust.customerid || 0 : 0, - deliverylandmark: dropCust.landmark || '', - deliverylat: dropCust.latitude.toString(), - deliverylocation: dropCust.suburb || '', - deliverylocationid: dropCust.deliverylocationid || 0, - deliverylong: dropCust.longitude.toString(), - deliverytime: `${dayjs(startdate).format('YYYY-MM-DD')} ${dayjs(selectedtime.$d).format('HH:mm:ss')}`, - deliverytype: pickCust.customerid !== 0 || dropCust.customerid !== 0 ? 'B' : 'C', - delivered: '', - itemcount: 1, - kms: distance.toString() || 0, - locationid: +tenanatLocoId, //main or branch - moduleid: +tenant.moduleid, - orderamount: +totalCharge.toFixed(2) || 0, - ordercharges: 0.0, - orderdate: dayjs().format('YYYY-MM-DD HH:mm:ss'), - orderheaderid: 0, - orderid: '', // - ordernotes: otherinstructions, - orderstatus: 'created', - ordervalue: +totalCharge.toFixed(2) || 0, - partnerid: tenant.partnerid, - paymentstatus: 1, - paymenttype: 42, - pending: '', - pickupaddress: pickCust.address || '', - pickupcity: pickCust.city || '', - pickupcontactno: pickCust.contactno || '', - pickupcustomer: pickCust.firstname || '', - pickuplandmark: pickCust.landmark || '', - pickuplat: pickCust.latitude.toString(), - pickuplocation: pickCust.suburb || '', - pickuplocationid: pickCust.deliverylocationid || 0, - pickuplong: pickCust.longitude.toString(), - processing: '', - ready: '', - remarks: '', - smsdelivery: isSms, - subcategoryid: +subCatId, - taxamount: 0.0, - tenantid: tenant.tenantid, - tenantuserid: parseInt(localStorage.getItem('userid')), - collectionamt: +collectionamt || 0, - quantity: +quantity || 1 - }, - - pickup: { - address: pickCust.address || '', - applocationid: tenant.applolcationid, - city: pickCust.city || '', - configid: 7, - contactno: pickCust.contactno || '', - customertoken: '', - customerid: isNumChange1 == 0 ? pickCust.customerid || 0 : 0, - devicetype: '', - deviceid: '', - dialcode: '+91', - doorno: pickCust.doorno || '', - email: pickCust.email || '', - firstname: pickCust.firstname || '', - landmark: pickCust.landmark || '', - latitude: pickCust.latitude.toString() || '', - longitude: pickCust.longitude.toString() || '', - locationid: pickCust.deliverylocationid || 0, - postcode: pickCust.postcode || '', - primaryaddress: 1, - profileimage: '', - state: pickCust.state || '', - suburb: pickCust.suburb || '', - tenantid: tenant.tenantid - }, - - drop: { - address: dropCust.address || '', - applocationid: tenant.applolcationid, - city: dropCust.city || '', - configid: 7, - contactno: dropCust.contactno || '', - customertoken: '', - customerid: isNumChange2 == 0 ? dropCust.customerid || 0 : 0, - devicetype: '', - deviceid: '', - dialcode: '+91', - doorno: dropCust.doorno || '', - email: dropCust.email || '', - firstname: dropCust.firstname || '', - landmark: dropCust.landmark || '', - latitude: dropCust.latitude.toString(), - longitude: dropCust.longitude.toString(), - locationid: dropCust.deliverylocationid || 0, - postcode: dropCust.postcode || '', - primaryaddress: 1, - profileimage: '', - state: dropCust.state || '', - suburb: dropCust.suburb || '', - tenantid: tenant.tenantid - } + // POST /admin/expressbooking's schema is a single flat booking object + // (tenantid, pickuplocationid, customer_phone/name, delivery* fields, + // service_option, finalprice, notes, parcels[]) — nothing like the old + // pickup/drop/orders triple. The drop contact becomes the booking's + // customer (new API models the delivery recipient as "the customer"); + // the pickup side maps to pickuplocationid when a saved location was + // picked (tenanatLocoId/isLocation, set by the "Business Location" + // Autocomplete below — pickCust.deliverylocationid was always '' and + // never actually got wired to the picker, so this shortcut never fired), + // otherwise raw pickup* fields following the doc's own delivery*/pickup* + // naming symmetry (pickupaddress/pickuppincode/pickupcity/pickuplatitude/ + // pickuplongitude — NOT shown in the doc's example, so unverified against + // the live API). + const arr = { + tenantid: tenant.tenantid, + ...(isLocation && tenanatLocoId + ? { pickuplocationid: tenanatLocoId } + : { + pickupaddress: pickCust.address || '', + pickuppincode: pickCust.postcode || '', + pickupcity: pickCust.city || '', + pickuplatitude: Number(pickCust.latitude) || 0, + pickuplongitude: Number(pickCust.longitude) || 0 + }), + customer_phone: dropCust.contactno || '', + customer_name: dropCust.firstname || '', + deliveryaddress: dropCust.address || '', + deliverypincode: dropCust.postcode || '', + deliverycity: dropCust.city || '', + deliverylatitude: Number(dropCust.latitude) || 0, + deliverylongitude: Number(dropCust.longitude) || 0, + service_option: 'Normal', + finalprice: +totalCharge.toFixed(2) || 0, + notes: otherinstructions || '', + parcels: [ + { + itemcategory: subCat.find((c) => c.subcategoryid === subCatId)?.categoryname || 'General', + itemdescription: 'Order', + declaredvalue: +totalCharge.toFixed(2) || 0 + } + ] }; console.log('createsubmitobj2', arr); if (!pickCust.firstname) { @@ -727,29 +613,28 @@ const Createorder1 = () => { } else if (!setSubCatId) { opentoast('Choose SubCategory ', 'warning', 2000); } else { + setLoading(true); try { - const createRes = await axios.post(`${process.env.REACT_APP_URL2}/orders/createorder`, arr); - // const createRes = await axios.post(`${process.env.REACT_APP_URL}/orders/createorder`, arr); - if (createRes.data.status) { - console.log('createRes', createRes); + const createRes = await createExpressBooking(arr); + if (createRes.success) { enqueueSnackbar('Order Created Successfully', { variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 1000 }); - if (admintoken) { - notifyadmin(admintoken); - // sendnotifications(); - } - navigate('/nearle/orders'); } else { - opentoast('Something went wrong, Cannot create order', 'warning'); + opentoast(createRes.message || 'Something went wrong, Cannot create order', 'warning'); } - setLoading(false); - console.log(createRes); } catch (error) { console.log('createResErr', error); + // Previously silent — a thrown error here (network failure, CityGate + // rejection, validation error) left the operator with no feedback at + // all and a permanently stuck loading spinner (setLoading(false) was + // only reachable on the success path). + opentoast(error.response?.data?.message || error.message || 'Cannot create order', 'error', 3000); + } finally { + setLoading(false); } } }; @@ -774,41 +659,22 @@ const Createorder1 = () => { const clientdetails = async () => { setLoading2(true); try { - let url = - searchCustList == '' - ? // ? `${process.env.REACT_APP_URL}/customers/getbytid/?tenantid=${tid}&pageno=1&pagesize=1` - `${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tid}&pageno=1&pagesize=20` - : `${process.env.REACT_APP_URL}/customers/search/?tenantid=${tid}&keyword=${searchCustList}`; - - await axios - .get(url) - .then((res) => { - console.log('clientdetails', res.data.details); - if (res.data.status && res.data.details) { - setClientdetail(res.data.details || []); - setCustomerlist(res.data.details || []); - let arr = []; - res.data.details.map((val) => { - arr.push({ - label: `${val.firstname} | ${val.contactno}`, - ...val - }); - }); - setClientdetailarr(arr); - } else { - setClientdetail([]); - setCustomerlist([]); - setClientdetailarr([]); - } - setLoading2(false); - }) - .catch((err) => { - console.log(err); - setLoading2(false); - opentoast('server error', 'warning'); - }); + const customers = (await getTenantCustomers()) || []; + const filtered = searchCustList + ? customers.filter( + (val) => + val.firstname?.toLowerCase().includes(searchCustList.toLowerCase()) || + val.phone?.includes(searchCustList) || + val.contactno?.includes(searchCustList) + ) + : customers; + setClientdetail(filtered); + setCustomerlist(filtered); + setClientdetailarr(filtered.map((val) => ({ label: `${val.firstname} | ${val.phone || val.contactno}`, ...val }))); } catch (err) { console.log(err); + opentoast('server error', 'warning'); + } finally { setLoading2(false); } }; @@ -821,74 +687,21 @@ const Createorder1 = () => { const clientdetailsbusiness = async () => { setLoading2(true); try { - await axios - .get(`${process.env.REACT_APP_URL}/customers/getbytid/?tenantid=${tid}&locationid=1`) - .then((res) => { - console.log('clientdetailsbusiness', res.data.details); - if (res.data.status) { - setClientdetail(res.data.details); - // if (!searchword) { - // setClientdetailarr(res.data.details) - // } - let arr = []; - res.data.details.map((val) => { - arr.push({ - label: `${val.firstname} | ${val.contactno}`, - ...val - }); - }); - setClientdetailbusinessarr(arr); - } - setLoading2(false); - }) - .catch((err) => { - console.log(err); - setLoading2(false); - opentoast('server error', 'warning'); - }); + const customers = (await getTenantCustomers()) || []; + setClientdetail(customers); + setClientdetailbusinessarr(customers.map((val) => ({ label: `${val.firstname} | ${val.phone || val.contactno}`, ...val }))); } catch (err) { console.log(err); + opentoast('server error', 'warning'); + } finally { setLoading2(false); } }; // ================================================== || sendnotifications || ================================================== - const sendnotifications = async () => { - setLoading(true); - await axios - .post(`${process.env.REACT_APP_URL}/utils/sendnotifications`, { - priority: 'high', - registration_ids: admintoken, - data: { - accessid: process.env.REACT_APP_RIDER_ACCESS_ID - }, - notification: { - title: 'Nearle Merchant', - body: 'An Order has been placed successfully,kindly process the same', - sound: 'ring' - } - }) - .then((res) => { - console.log(res); - if (res.data.message == 'Success') { - enqueueSnackbar('Notification sent Successfully', { - variant: 'success', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 1000 - }); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - enqueueSnackbar(err.message, { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 1000 - }); - setLoading(false); - }); - }; + // No bulk-notify endpoint in the new API — never actually called (dead in + // the original code too, only referenced from a commented-out call site). + const sendnotifications = async () => {}; // ============================================= || Address Autocomplete (pick) || ============================================= const handlePickPlaceSelected = (place) => { setInputValue2(`${place.name}, ${place.formatted_address}`); @@ -1015,16 +828,11 @@ const Createorder1 = () => { // ============================================= || gettenantlocations (branches) || ============================================= const gettenantlocations = async () => { try { - const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${tid}`); - console.log('gettenantlocations', res.data.details); - if (res.data && res.data.details) { - setTenantlocations(res.data.details); - if (res.data.details.length == 1) { - setIsLocation(true); - setTenanatLocoId(res.data.details[0].locationid); - } - } else { - setTenantlocations([]); + const locations = (await getTenantLocations(tid)) || []; + setTenantlocations(locations); + if (locations.length == 1) { + setIsLocation(true); + setTenanatLocoId(locations[0].locationid); } } catch (err) { console.log('gettenantlocations', err); @@ -1124,9 +932,7 @@ const Createorder1 = () => { ) : ( - option && option.locationname ? `${option.locationname} (${option.suburb || ''})` : '' - } + getOptionLabel={(option) => (option && option.locationname ? `${option.locationname} (${option.suburb || ''})` : '')} isOptionEqualToValue={(option, value) => option?.locationid === value?.locationid} PaperComponent={SoftPaper} sx={{ width: { xs: '100%', sm: 300 } }} @@ -1926,16 +1732,13 @@ const Createorder1 = () => { {/* ================================================= || Time || ================================================= */} - } - title="Schedule" - subtitle="Pickup date & time slot" - /> + } title="Schedule" subtitle="Pickup date & time slot" /> - Date + + Date + { {timeslotarr.map((val, index) => { if ( - dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0 + dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= + 0 ) { const active = dayjs(selectedtime).format('HH:mm') == dayjs(val).format('HH:mm'); return ( @@ -2082,7 +1886,9 @@ const Createorder1 = () => { getOptionLabel={(option) => `${option.subcategoryname}` || ''} sx={{ my: 2, zIndex: '100' }} fullWidth - renderInput={(params) => } + renderInput={(params) => ( + + )} onChange={(event, value, reason) => { if (value) { console.log(value); @@ -2416,14 +2222,16 @@ const Createorder1 = () => { '&:hover': isUsed ? {} : { - borderColor: edge(BRAND), - bgcolor: tint(BRAND), - boxShadow: DT.shadowSoft - } + borderColor: edge(BRAND), + bgcolor: tint(BRAND), + boxShadow: DT.shadowSoft + } }} > - {String(address.firstname || '?').charAt(0).toUpperCase()} + {String(address.firstname || '?') + .charAt(0) + .toUpperCase()} diff --git a/src/pages/nearle/orders/miltiUploadBackup.js b/src/pages/nearle/orders/miltiUploadBackup.js deleted file mode 100644 index 490794f..0000000 --- a/src/pages/nearle/orders/miltiUploadBackup.js +++ /dev/null @@ -1,1610 +0,0 @@ -/* eslint-disable no-unused-vars */ -import React from 'react'; -import Loader from 'components/Loader'; -import { useEffect, useState, Fragment, useRef } from 'react'; -import { useTheme } from '@mui/material/styles'; -import MainCard from 'components/MainCard'; -import axios from 'axios'; -import ClearIcon from '@mui/icons-material/Clear'; -import { SearchOutlined, CloseOutlined, ExclamationCircleOutlined, FileAddOutlined } from '@ant-design/icons'; -import { Empty } from 'antd'; -import MyLocationIcon from '@mui/icons-material/MyLocation'; -import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import dayjs from 'dayjs'; -var utc = require('dayjs/plugin/utc'); -dayjs.extend(utc); -import { enqueueSnackbar } from 'notistack'; -import { useNavigate } from 'react-router'; -import Papa from 'papaparse'; -import { calculateDrivingDistance, calculateTotalCharge } from '../../../utils/distance'; -import * as XLSX from 'xlsx'; - -import { - FormControl, - InputAdornment, - Grid, - Typography, - Stack, - Box, - Button, - TextField, - Autocomplete, - Chip, - Divider, - Dialog, - DialogTitle, - DialogContent, - Checkbox, - DialogActions, - CircularProgress, - IconButton, - OutlinedInput, - FormGroup, - FormControlLabel, - Table, - TableContainer, - TableCell, - TableBody, - TableRow, - Paper, - TableHead, - FormLabel, - RadioGroup, - Radio, - Backdrop, - List, - ListItem, - ListItemText -} from '@mui/material'; -import CircularLoader from 'components/nearle_components/CircularLoader'; - -const MultipleOrders = () => { - const navigate = useNavigate(); - const theme = useTheme(); - const locationRef = useRef(null); - const tenantRef = useRef(null); - const userid = localStorage.getItem('userid'); - const [locations, setLocations] = useState([]); - const [tenantlist, setTenantlist] = useState([]); - const [loading, setLoading] = useState(false); - const [btnLoading, setBtnLoading] = useState(false); - const [appId, setAppId] = useState(0); - const [tenantLocations, setTenantlocations] = useState([]); - // const [tenantid, setTenantid] = useState(0); - const tenantid = localStorage.getItem('tenantid') || 0; - const [locationid, setLocationid] = useState(0); - const [basePrice, setBasePrice] = useState(0); - const [pricePerKm, setPricePerKm] = useState(0); - const [minKm, setMinKm] = useState(0); - const [pickCust, setPickCust] = useState(null); - const [dropCust, setDropCust] = useState([]); - const [isCustomerOpen, setIsCustomerOpen] = useState(false); - const [searchCustList, setSearchCustList] = useState(''); - const [customerlist, setCustomerlist] = useState([]); - const [startdate, setStartdate] = useState(dayjs().format('MM-DD-YYYY')); - const [timeslotarr, setTimeslotarr] = useState([]); - const [starttime, setStatrttime] = useState(); - const [endtime, setEndtime] = useState(); - const [selectedtime, setSelectedtime] = useState(''); - const [alertmessage, setAlertmessage] = useState(''); - const [otherinstructions, setOtherinstructions] = useState(''); - const [admintoken, setAdmintoken] = useState(); - const [totaldist, settotaldist] = useState(0); - const [totalAmt, settotalAmt] = useState(0); - const [totalQty, settotalQty] = useState(0); - const [totalCash, settotalCash] = useState(0); - const [users, setUsers] = useState([]); - const [uploadType, setUploadType] = useState(null); - const [tenantValue, setTenantValue] = useState(null); - const [locationValue, setLocationValue] = useState(null); - const [pickupSlotsList, setPickupSlotsList] = useState(null); - const [pickupSlot, setPickupSlot] = useState(null); - - useEffect(() => { - if (timeslotarr[0]) { - let arr = []; - timeslotarr.map((val) => { - if (dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0) { - arr.push(val); - } - }); - } - }, [timeslotarr]); - - // =============================================== || opentoast || =============================================== - const opentoast = (message, variant, time) => { - enqueueSnackbar(message, { - variant: variant, - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: time ? time : 1500 - }); - console.log(alertmessage); - }; - - // 🔹 Smart toast wrapper — prevents duplicate toasts for same message within 3 seconds - let toastCache = {}; - const OpenToast = (message, type = 'info', timeout = 10000) => { - const key = `${type}-${message}`; - if (toastCache[key]) return; // skip duplicates - opentoast(message, type, timeout); // your existing toast/snackbar - toastCache[key] = true; - setTimeout(() => delete toastCache[key], 3000); // reset after delay - }; - - // ==============================|| fetchAppLocations ||============================== // - - const fetchAppLocations = async () => { - setLoading(true); - - try { - const locationRes = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`); - console.log('fetchAppLocations', locationRes.data.details); - setLocations(locationRes.data.details); - } catch (err) { - console.log('locationRes', err); - OpenToast(err.message, 'error', 5000); - } finally { - setLoading(false); - } - }; - useEffect(() => { - fetchAppLocations(); - }, []); - - // ===================================================== || fetchtenantinfolist || ===================================================== - - const fetchtenantinfolist = async () => { - setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL}/tenants/gettenants/?applocationid=${appId}&status=active`) - - .then((res) => { - console.log(res); - if (res.data.status) { - let arr = []; - res.data.details.map((val) => { - arr.push({ - ...val, - label: `${val.tenantname}` - }); - }); - setTenantlist(arr); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; - useEffect(() => { - appId && fetchtenantinfolist(); - }, [appId]); - // ============================================= || fetchTenantPricing || ============================================= - - const fetchTenantPricing = async (id) => { - try { - const pricingResponse = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${id}`); - console.log('pricingResponse', pricingResponse.data.details); - setBasePrice(pricingResponse.data.details.baseprice); - setPricePerKm(pricingResponse.data.details.priceperkm); - setMinKm(pricingResponse.data.details.minkm); - } catch (error) { - console.log('fetchTenantPricing error', error); - } - }; - // ============================================= || gettenantlocations (branches) || ============================================= - const gettenantlocations = async (id) => { - try { - const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`); - console.log('gettenantlocations', res.data.details); - if (res.data.details.length == 1) { - setTenantlocations(res.data.details); - setPickCust(res.data.details[0]); - setLocationid(res.data.details[0].locationid); - setLocationValue(res.data.details[0].locationid); - setPickupSlotsList(res.data.details[0].slots); - } else { - setTenantlocations(res.data.details); - } - } catch (err) { - console.log('gettenantlocations', err); - } - }; - useEffect(() => { - gettenantlocations(tenantid); - }, [tenantid]); - // ========================================================= || clientdetails || ========================================================= - const clientdetails = async () => { - try { - let url = - searchCustList == '' - ? `${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tenantid}&pageno=1&pagesize=30` - : `${process.env.REACT_APP_URL}/customers/search/?tenantid=${tenantid}&keyword=${searchCustList}`; - await axios - .get(url) - .then((res) => { - if (res.data.status) { - console.log('clientdetails', res.data.details); - - setCustomerlist(res.data.details); - let arr = []; - res.data.details.map((val) => { - arr.push({ - label: `${val.firstname} | ${val.contactno}`, - ...val - }); - }); - } - }) - .catch((err) => { - console.log(err); - opentoast('server error', 'warning'); - }); - } catch (err) { - console.log(err); - } - }; - useEffect(() => { - if (tenantid) { - clientdetails(); - } - }, [searchCustList.length > 3, searchCustList == '', tenantid]); - - // ========================================================= || calculateTotal(dist , charge) || ========================================================= - const calculateTotal = () => { - let a1 = 0; - let a2 = 0; - let a3 = 0; - let a4 = 0; - dropCust.map((customer) => { - a1 += customer.distance; - a2 += customer.totalcharge; - a3 += customer.quantity; - a4 += customer.collectionamt; - }); - settotaldist(a1); - settotalAmt(a2); - settotalQty(a3); - settotalCash(a4); - }; - useEffect(() => { - calculateTotal(); - }, [dropCust]); - - // ========================================================= || handleCheckboxChange || ========================================================= - const handleCheckboxChange = async (event, customer) => { - setLoading(true); - if (event.target.checked) { - // If the checkbox is checked, calculate the distance and add the customer - try { - const obj = await calculateDistance(customer); - const { roundedDistance, totalcharge } = obj; - // Create a new customer object with the distance property - const updatedCustomer = { - ...customer, - distance: roundedDistance, - totalcharge: totalcharge - }; - - // Add the updated customer object to dropCust - setDropCust((prevDropCust) => [...prevDropCust, updatedCustomer]); - - // Log the rounded distance - // console.log(`Rounded Distance: ${roundedDistance} km`); - } catch (error) { - console.error('Failed to calculate distance:', error); - } finally { - setLoading(false); - } - } else { - // If the checkbox is unchecked, remove the customer from dropCust - setDropCust((prevDropCust) => { - return prevDropCust.filter((cust) => cust.customerid !== customer.customerid); - }); - setLoading(false); - } - }; - // ========================================================= || handleCheckboxChange1 || ========================================================= - // const handleCheckboxChange1 = async (customer) => { - // console.log('customer', customer); - // setLoading(true); - // try { - // const obj = await calculateDistance(customer); - // const { roundedDistance, totalcharge } = obj; - // // Create a new customer object with the distance property - // const updatedCustomer = { - // ...customer, - // distance: roundedDistance, - // totalcharge: totalcharge - // }; - - // // Add the updated customer object to dropCust - // setDropCust((prevDropCust) => [...prevDropCust, updatedCustomer]); - - // // Log the rounded distance - // console.log(`Rounded Distance: ${roundedDistance} km`); - // setLoading(false); - // } catch (error) { - // console.error('Failed to calculate distance:', error); - // } - // }; - const handleCheckboxChange1 = async (customer) => { - console.log('customer', customer); - - setLoading(true); - - try { - setDropCust((prevDropCust) => { - const isAlreadySelected = prevDropCust.some((c) => c.firstname === customer.firstname); - - // 🔴 REMOVE if already exists - if (isAlreadySelected) { - return prevDropCust.filter((c) => c.firstname !== customer.firstname); - } - - // 🟢 ADD if not exists (calculate distance) - return prevDropCust; - }); - - // Only calculate distance if customer is not already added - const alreadyExists = dropCust.some((c) => c.firstname === customer.firstname); - - if (!alreadyExists) { - const obj = await calculateDistance(customer); - const { roundedDistance, totalcharge } = obj; - - const updatedCustomer = { - ...customer, - distance: roundedDistance, - totalcharge - }; - - setDropCust((prevDropCust) => [...prevDropCust, updatedCustomer]); - - console.log(`Rounded Distance: ${roundedDistance} km`); - } - } catch (error) { - console.error('Failed to calculate distance:', error); - } finally { - setLoading(false); - } - }; - - // ========================================================= || calculateDistance || ========================================================= - - // 🔹 Main distance calculation function - const calculateDistance = async (customer) => { - try { - // --- Input validation --- - if (!customer || typeof customer !== 'object') { - throw new Error('Invalid customer data: expected an object.'); - } - - if (!pickCust || typeof pickCust !== 'object') { - throw new Error('Origin (pickCust) data missing or invalid.'); - } - - // --- Compute distance --- - const roundedDistance = await calculateDrivingDistance(pickCust, customer); - - // --- Calculate total charge --- - const totalcharge = calculateTotalCharge(roundedDistance, basePrice, pricePerKm, minKm); - return { roundedDistance, totalcharge }; - } catch (error) { - // --- Categorized smart error handling --- - console.log('on calculateDistance', error.message); - if (error.message.includes('Invalid coordinates') || error.message.includes('Invalid coordinate')) { - console.log('📍 Invalid coordinate format:', error.message); - OpenToast('Invalid coordinate format. Check location data.', 'warning', 3000); - } else if (error.message.includes('Origin') || error.message.includes('customer')) { - console.log('❌ Missing or invalid input data:', error.message); - OpenToast('Missing or invalid input data for distance calculation.', 'warning', 3000); - } else { - console.log('💥 Unexpected error calculating distance:', error); - OpenToast('Unexpected error during distance calculation.', 'error', 3000); - } - - throw error; // keeps your current flow intact - } - }; - - // ==================================================== || fetchTiming || ==================================================== - const fetchTiming = async () => { - setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL}/utils/getapplocations/?applocationid=${appId}`) - .then((res) => { - console.log('fetchTiming', res); - const { opentime, closetime } = res.data.details[0]; - if (res.data.status) { - setStatrttime(`${dayjs().format('MM-DD-YYYY')} ${opentime}`); - setEndtime(`${dayjs().format('MM-DD-YYYY')} ${closetime}`); - console.log('starttime', `${dayjs().format('MM-DD-YYYY')} ${opentime}`); - console.log('endtime', `${dayjs().format('MM-DD-YYYY')} ${closetime} `); - let arr = []; - for ( - let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`, j = 0; - dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0; - j++, i = dayjs(i).add(30, 'm') - ) { - arr.push(i); - } - console.log('setTimeslotarr', arr); - setTimeslotarr(arr); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; - useEffect(() => { - if (appId) { - fetchTiming(); - } - }, [appId]); - - const fetchAppAdminTokens = async () => { - setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL}/utils/getapplocationconfig/?applocationid=${appId}`) - .then((res) => { - const userfcmtokemArray = res.data.details.applocationadmins.map((admin) => admin.userfcmtokem); // fcm => firebase cloud messaging - console.log('fetchAppAdminTokens', res); - console.log('userfcmtokemArray', userfcmtokemArray); - if (res.data.status) { - setAdmintoken(userfcmtokemArray); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; - - useEffect(() => { - if (appId) { - fetchAppAdminTokens(); - } - }, [appId]); - - useEffect(() => { - console.log('pickCust', pickCust); - }, [pickCust]); - useEffect(() => { - console.log('dropCust', dropCust); - }, [dropCust]); - // // ==================================================== || fetchtenantinfo || ==================================================== - // const fetchtenantinfo = async () => { - // setLoading(true); - // console.log('tenantid', tenantid); - - // await axios - // .get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tenantid}`) - // .then((res) => { - // console.log('fetchtenantinfo', res); - // if (res.data.status) { - // setTenantid(res.data.details.tenantid); - // } - // setLoading(false); - // }) - // .catch((err) => { - // console.log(err); - // setLoading(false); - // }); - // }; - // useEffect(() => { - // if (tenantid) { - // fetchtenantinfo(); - // } - // }, [tenantid]); - // ================================================== || sendnotifications || ================================================== - const sendnotifications = async () => { - setLoading(true); - await axios - .post(`${process.env.REACT_APP_URL}/utils/sendnotifications`, { - priority: 'high', - registration_ids: admintoken, - data: { - accessid: process.env.REACT_APP_RIDER_ACCESS_ID - }, - notification: { - title: 'Nearle Merchant', - body: 'An Order has been placed successfully,kindly process the same', - sound: 'ring' - } - }) - .then((res) => { - console.log(res); - if (res.data.message == 'Success') { - enqueueSnackbar('Notification sent Successfully', { - variant: 'success', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 1000 - }); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - enqueueSnackbar(err.message, { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 1000 - }); - setLoading(false); - }); - }; - - const cleanReceiverName = (name) => { - if (typeof name !== 'string') return name; - return name.replace(/^[\d.\s]+/, '').trim(); - }; - - const handleFileUpload = (event) => { - console.log('Normal upload started...'); - try { - const file = event.target.files?.[0]; - if (!file) { - opentoast('No file selected.', 'warning'); - return; - } - - const fileName = file.name.toLowerCase(); - const isCSV = fileName.endsWith('.csv'); - const isExcel = fileName.endsWith('.xls') || fileName.endsWith('.xlsx'); - - // Invalid file - if (!isCSV && !isExcel) { - opentoast('Invalid file type. Please upload a CSV or Excel file.', 'warning'); - return; - } - - // ---------------------- CSV ---------------------- - if (isCSV) { - Papa.parse(file, { - header: true, - dynamicTyping: true, - skipEmptyLines: true, - complete: (results) => { - const data = results.data || []; - if (data.length === 0) { - opentoast('CSV file is empty or invalid.', 'warning'); - setUsers([]); - return; - } - const cleanedData = data.map((row) => ({ - ...row, - firstname: cleanReceiverName(row.firstname) - })); - console.log('✅ Parsed CSV Data:', cleanedData); - setUsers(cleanedData); - opentoast('CSV file uploaded successfully, ✅', 'success', 3000); - opentoast(' Press Continue to add delivery customers', 'warning', 5000); - }, - error: (error) => { - console.error('❌ CSV Parse Error:', error.message); - opentoast(`CSV parsing failed: ${error.message}`, 'warning'); - } - }); - } - - // ---------------------- EXCEL (.xls / .xlsx) ---------------------- - if (isExcel) { - const reader = new FileReader(); - - reader.onload = (e) => { - try { - const data = e.target.result; - - // Use correct mode for binary Excel formats - const workbook = XLSX.read(data, { - type: 'binary', - cellDates: true, - cellNF: false, - cellText: false - }); - const firstSheet = workbook.SheetNames[0]; - const worksheet = workbook.Sheets[firstSheet]; - const jsonData = XLSX.utils.sheet_to_json(worksheet, { defval: '' }); - - if (!jsonData || jsonData.length === 0) { - opentoast('Excel file is empty or invalid.', 'warning'); - setUsers([]); - return; - } - - const cleanedData = jsonData.map((row) => ({ - ...row, - firstname: cleanReceiverName(row.firstname) - })); - - console.log('✅ Parsed Excel Data:', cleanedData); - setUsers(cleanedData); - opentoast('Excel file uploaded successfully ✅, press continue', 'success', 3000); - } catch (err) { - console.error('❌ Excel Parse Error:', err); - opentoast(`Error reading Excel: ${err.message}`, 'warning'); - } - }; - - // ✅ Key fix: use readAsBinaryString for both .xls & .xlsx - reader.readAsBinaryString(file); - } - } catch (err) { - console.error('Unexpected error during file upload:', err.message); - opentoast(`Unexpected error: ${err.message}`, 'warning'); - } - }; - - // your header mapping - const headerMap = { - 'pickupdate(yyyy-mmm-dd)': 'date', - 'sendername*': 'locationname', - 'senderphone*': 'locationcontact', - 'senderaddress*': 'locationaddress', - 'receivername*': 'firstname', - 'receiverphone': 'contactno', - 'receiveralternatephone*': 'altcontactno', - receiverfulladdress: 'address', - receiverlatitude: 'latitude', - receiverlongitude: 'longitude', - 'itemdescription*': 'description', - Quantity: 'quantity', - ' Collect Cash': 'collectionamt' - }; - - // helper to normalize headers - const normalizeHeader = (header) => header?.toString().trim().toLowerCase().replace(/\s+/g, ''); - - const handleFileDirectUpload = (event) => { - try { - const file = event.target.files?.[0]; - if (!file) { - opentoast('No file selected.', 'warning'); - return; - } - - const fileName = file.name.toLowerCase(); - const isCSV = fileName.endsWith('.csv'); - const isExcel = fileName.endsWith('.xls') || fileName.endsWith('.xlsx'); - - if (!isCSV && !isExcel) { - opentoast('Invalid file type. Please upload a CSV or Excel file.', 'warning'); - return; - } - - const processData = (data, headers) => { - console.log('data', data); - const normalizedMap = {}; - for (const key in headerMap) { - normalizedMap[normalizeHeader(key)] = headerMap[key]; - } - console.log('normalizedMap', normalizedMap); - - const mappedData = data.map((row) => { - const newRow = {}; - for (const key in row) { - const cleanKey = normalizeHeader(key); - const newKey = normalizedMap[cleanKey] || cleanKey; - let value = row[key]; - if (newKey === 'firstname') value = cleanReceiverName(value); - - newRow[newKey] = value; - } - - return newRow; - }); - - const missingCols = Object.keys(headerMap) - .filter((clientCol) => clientCol.endsWith('*')) - .filter((clientCol) => !headers.includes(normalizeHeader(clientCol))); - - if (missingCols.length > 0) { - isExcel && opentoast(`Missing columns: ${missingCols.join(', ')}`, 'warning'); - } - - console.log('✅ Final Processed Data:', mappedData); - setUsers(mappedData); - opentoast('File uploaded and successfully ', 'success', 3000); - opentoast('Press Continue', 'warning', 3000); - }; - - // ============ CSV handler ============ - if (isCSV) { - Papa.parse(file, { - header: true, - dynamicTyping: true, - skipEmptyLines: true, - complete: (results) => { - if (!results.data?.length) { - opentoast('CSV file is empty or has no valid rows.', 'warning'); - setUsers([]); - return; - } - const headers = results.meta.fields.map(normalizeHeader); - processData(results.data, headers); - }, - error: (error) => { - console.error('❌ CSV Parsing Error:', error); - opentoast(`CSV parsing failed: ${error.message}`, 'warning'); - } - }); - } - - // ============ Excel handler ============ - if (isExcel) { - const reader = new FileReader(); - reader.onload = (e) => { - try { - const data = e.target.result; - // Try reading as binary first - let workbook; - try { - workbook = XLSX.read(data, { type: 'binary' }); - } catch { - // fallback for modern XLSX files - const arrayBuffer = new Uint8Array(data); - workbook = XLSX.read(arrayBuffer, { type: 'array' }); - } - - const firstSheet = workbook.SheetNames[0]; - const worksheet = workbook.Sheets[firstSheet]; - const jsonData = XLSX.utils.sheet_to_json(worksheet, { defval: '' }); - - if (!jsonData?.length) { - opentoast('Excel file is empty or invalid.', 'warning'); - setUsers([]); - return; - } - - const headers = Object.keys(jsonData[0]).map(normalizeHeader); - processData(jsonData, headers); - } catch (err) { - console.error('❌ Error processing Excel:', err); - opentoast(`Error reading Excel: ${err.message}`, 'warning'); - } - }; - - // Important: use readAsBinaryString for Excel - reader.readAsBinaryString(file); - } - } catch (err) { - console.error('Unexpected error during file upload:', err); - opentoast(`Unexpected error: ${err.message}`, 'warning'); - } - }; - - // =============================================== || createorders || =============================================== - const createorders = async () => { - // ===================== Build Payload ===================== - const arr = dropCust.map((customer) => ({ - applocationid: pickCust.applocationid, - configid: 9, - partnerid: pickCust.partnerid, - partneruserid: +userid, - paymentstatus: 1, - paymenttype: 42, - pickupaddress: pickCust.address || '', - pickupcity: pickCust.city || '', - pickupcontactno: pickCust.contactno || '', - pickupcustomer: pickCust.locationname || '', - pickuplandmark: pickCust.landmark || '', - pickuplat: pickCust.latitude, - pickuplocation: pickCust.suburb || '', - pickuplocationid: pickCust.locationid || 0, - pickuplong: pickCust.longitude, - tenantid: pickCust.tenantid, - - customerid: +customer?.customerid, - deliveryaddress: customer.address || '', - deliverycharge: +customer.totalcharge || 0, - deliverycity: customer.city || '', - deliverycontactno: customer.contactno?.toString() || '', - deliverycustomer: customer.firstname || '', - deliveryid: +customer.customerid, - deliverylandmark: customer.landmark || '', - deliverylat: customer.latitude?.toString() || '', - deliverylocation: customer.suburb || '', - deliverylocationid: customer.deliverylocationid || 0, - deliverylong: customer.longitude?.toString() || '', - deliverytime: `${dayjs(startdate).format('YYYY-MM-DD')} ${dayjs(selectedtime.$d).format('HH:mm:ss')}`, - deliverytype: 'B', - - itemcount: 1, - quantity: customer.quantity, - collectionamt: customer.collectionamt, - kms: customer.distance?.toString() || '0', - locationid: +pickCust.locationid, - moduleid: +pickCust.moduleid, - - orderamount: +customer.totalcharge || 0, - ordercharges: 0.0, - orderdate: dayjs().format('YYYY-MM-DD HH:mm:ss'), - ordernotes: otherinstructions, - orderstatus: 'created', - ordervalue: +customer.totalcharge || 0, - pickupSlot - })); - - console.log('arr', arr); - - // ===================== Validation ===================== - if (!tenantid) { - opentoast('Choose Client', 'warning'); - return; - } - setLoading(true); - try { - const res = await axios.post(`${process.env.REACT_APP_URL}/orders/createorders`, arr); - if (res.data.status) { - opentoast('Order Created Successfully', 'success', 2000); - if (admintoken) { - sendnotifications(); - } - navigate('/nearle/orders'); - setLoading(false); - } else { - console.log(res.data); - console.error('Create order failed (API response):', res.data); - opentoast(res?.data?.message || 'Order creation failed. Please try again.', 'warning', 3000); - } - } catch (err) { - opentoast(err.message, 'error', 2000); - console.log('create orders', err.message); - console.error('Create order error:', { - message: err.message, - response: err.response, - request: err.request, - stack: err.stack - }); - - // Exact but short error for user - let toastMessage = 'Something went wrong. Please try again.'; - - if (err.response) { - // Server responded with error - toastMessage = err.response.data?.message || `Server error (${err.response.status})`; - } else if (err.request) { - // No response received - toastMessage = 'Network error. Check your internet connection.'; - } - opentoast(toastMessage, 'error'); - setLoading(false); - } finally { - setLoading(false); - setBtnLoading(false); - } - }; - - const [fileName, setFileName] = useState(''); - const removeFileExtension = (fileName) => { - return fileName.replace(/\.[^/.]+$/, ''); - }; - - const onFileChange = (event) => { - const file = event.target.files[0]; - if (!file) return; - const cleanedName = removeFileExtension(file.name); - setFileName((prev) => (prev ? `${prev}, ${cleanedName}` : cleanedName)); - if (tenantid === 916) { - handleFileDirectUpload(event); - } else { - handleFileDirectUpload(event); - // handleFileUpload(event); - } - }; - - const handleQuantityChange = (customerid, value) => { - setDropCust((prev) => prev.map((cust) => (cust.customerid === customerid ? { ...cust, quantity: Number(value) || 0 } : cust))); - }; - const handleCollectionAmtChange = (customerid, value) => { - setDropCust((prev) => prev.map((cust) => (cust.customerid === customerid ? { ...cust, collectionamt: Number(value) || 0 } : cust))); - }; - - return ( - <> - {loading && ( - <> - - {/* */} - - )} - { - theme.zIndex.drawer + 1 - }} - open={btnLoading} // when loader = true, backdrop covers the page - > - - - } - - - {/* ===================================================== ||Business Location || ===================================================== */} - - - Create Multiple Order - - - {tenantLocations?.length === 1 ? ( - - - - ) - }} - /> - ) : ( - `${option.locationname} (${option.suburb})`} - value={locationValue} - onOpen={(event) => { - if (!appId && !tenantid) { - event.preventDefault(); - - OpenToast('Please select Location and Tenant first!', 'warning', 3000); - - setTimeout(() => { - locationRef.current?.focus(); - }, 0); - } else if (!tenantid) { - event.preventDefault(); - - OpenToast('Please select Tenant first!', 'warning', 3000); - - setTimeout(() => { - tenantRef.current?.focus(); - }, 0); - } - }} - onChange={(event, value, reason) => { - if (reason === 'clear') { - setLocationid(0); - setLocationValue(null); - setPickCust(null); - } else { - setLocationid(value?.locationid || 0); - setLocationValue(value); - setPickCust(value); - setPickupSlotsList(value?.slots); - } - }} - renderInput={(params) => } - /> - )} - - - - - {/* ===================================================== || Pickup || ===================================================== */} - - - {locationid !== 0 && ( - - - - - - )} - {/* ================================================= || Time || ================================================= */} - - - - - - { - setStartdate(e); - let dateres11 = dayjs().diff(dayjs(`${dayjs(e).format('YYYY-MM-DD')}`), 'd'); - console.log('dateres11'); - console.log(dateres11); - setSelectedtime(''); - if (dateres11 <= 0) { - console.log('startdate', e); - setStartdate(e); - - let arr = []; - timeslotarr.map((val) => { - if (dayjs().diff(dayjs(`${dayjs(e).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0) { - arr.push(val); - } - }); - } else { - setAlertmessage('choose Upcoming Date'); - opentoast('choose Upcoming Date', 'warning'); - setStartdate(NaN); - } - }} - value={dayjs(startdate)} - sx={{ width: 'auto', mt: 0 }} - disablePast - /> - - - {/* {timeslotarr.length > 0 && ( - - - - - - - Time - - - - - - { - setStartdate(e); - let dateres11 = dayjs().diff(dayjs(`${dayjs(e).format('YYYY-MM-DD')}`), 'd'); - console.log('dateres11'); - console.log(dateres11); - setSelectedtime(''); - if (dateres11 <= 0) { - console.log('startdate', e); - setStartdate(e); - - let arr = []; - timeslotarr.map((val) => { - if ( - dayjs().diff(dayjs(`${dayjs(e).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0 - ) { - arr.push(val); - } - }); - } else { - setAlertmessage('choose Upcoming Date'); - opentoast('choose Upcoming Date', 'warning'); - setStartdate(NaN); - } - }} - value={dayjs(startdate)} - sx={{ width: 'auto', mt: 0 }} - disablePast - /> - - - - - - - {timeslotarr.map((val, index) => { - if ( - dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0 - ) { - return ( - - - { - console.log('selectedtime', val); - setSelectedtime(val); - }} - // onClick={() => { - // if (distance > appLocaRadius) { - // setOpen(true); - // } else if (showDistance) { - // console.log('selectedtime', val); - // setSelectedtime(val); - // } else { - // opentoast('Out of city limit', 'error'); - // } - // }} - /> - - - ); - } - })} - - - - - - )} */} - - - { - if (reason === 'clear') { - setSelectedtime(null); - setPickupSlot(null); - } else { - // Convert to AM/PM and merge with date - const formattedTime = dayjs(newValue.time, 'HH:mm').format('hh:mm A'); - setSelectedtime(formattedTime); - const finalDateTime = dayjs(`${startdate} ${formattedTime}`, 'MM-DD-YYYY hh:mm A').format('YYYY-MM-DD hh:mm A'); - setPickupSlot(finalDateTime); - } - }} - getOptionLabel={(option) => `${option.name} (${dayjs(option.time, 'HH:mm').format('hh:mm A')})`} - renderInput={(params) => } - /> - - - - - - {/* ===================================================== || Drop || ===================================================== */} - - - - - Drop ({dropCust?.length || 0}) - - - {/* ================= Upload CSV ================= */} - {uploadType === 0 && ( - <> - {fileName && ( - - - - {fileName} - - - )} - - - - - - )} - - {/* ================= Continue ================= */} - {users.length >= 1 && uploadType === 0 && ( - - )} - - {/* ================= Select Customers ================= */} - {uploadType === 1 && ( - - )} - - {/* ================= Upload Type ================= */} - - - Upload Type - { - if (!locationid) { - OpenToast('Please select Business Location!', 'warning', 3000); - return; - } - setUploadType(Number(e.target.value)); - setDropCust([]); - setUsers([]); - setFileName(''); - }} - > - } label="Excel / CSV" /> - } label="Selection" /> - - - - - - } - > - - - {dropCust?.length > 0 ? ( - <> - - - S.No - Customer - Address - Quantity - - - Cash Collect - - Kms - - Charge - Action - - - - - {dropCust?.map((customer, index) => ( - - {index + 1} - {customer.firstname} - {customer.address} - - {uploadType == 0 ? ( - {customer.quantity} - ) : ( - handleQuantityChange(customer.customerid, e.target.value)} - inputProps={{ min: 0 }} - /> - )} - - - {uploadType == 0 ? ( - ₹{Number(customer.collectionamt || 0).toFixed(2)} - ) : ( - { - if (e.target.value <= 0) { - handleCollectionAmtChange(customer.customerid, 0); - } else { - handleCollectionAmtChange(customer.customerid, e.target.value); - } - }} - inputProps={{ min: 0 }} - InputProps={{ - startAdornment: - }} - /> - )} - - - {customer.distance} - {`₹${customer?.totalcharge?.toFixed(2)}`} - - { - <> - handleCheckboxChange(event, customer)} - onClick={() => handleCheckboxChange1(customer)} - /> - - } - - - ))} - {dropCust?.length != 0 && ( - - Total - - - - {`${totalQty} `} - - - - {`${totalCash?.toFixed(2)} `} - - - {`${totaldist} `} - - - {`₹${totalAmt?.toFixed(2)}`} - - - - - )} - - - ) : ( - - {/* Header */} - - {' '} - - Important Instructions - - - - {/* Ordered List */} - - - Choose either Upload Type to upload CSV/Excel files, or - Selection Type to select from saved customers. - - - - Uploaded CSV or Excel files must follow the required format and contain the correct column names. - - - - Multiple files can be uploaded, but only one file at a time. - - - - Invalid or incorrectly formatted files will not be processed. - - - - )} -
    -
    - -
    - - - {/* ================================================= || Notes || ================================================= */} - - - - setOtherinstructions(e.target.value)} - /> - - - - - - - -
    - {/* ============================================= || saved address Dialog || ============================================= */} - { - setIsCustomerOpen(false); - }} - fullWidth - sx={{ minWidth: 'lg' }} - > - - - {`Select Drop Customers (${dropCust.length || 0})`} - - - setSearchCustList(e.target.value)} - sx={{ - '& .MuiOutlinedInput-input': { - p: '10.5px 0px 12px' - }, - bgcolor: 'white' - }} - startAdornment={ - - - - } - endAdornment={ - { - setSearchCustList(''); - }} - > - - - } - autoComplete="off" - /> - - - - - - - {customerlist?.length == 0 ? ( - - - - ) : ( - - {customerlist && - customerlist?.map((customer, index) => ( - - cust.customerid === customer.customerid)} // Set the checked state of the checkbox based on whether the customer is in `dropCust` - onChange={(event) => handleCheckboxChange(event, customer)} - /> - } - label={ -
    - - {`${customer.firstname} (${customer.contactno})`} - - - - {customer.address} - -
    - } - /> -
    - ))} -
    - )} -
    - - - - -
    - - ); -}; - -export default MultipleOrders; diff --git a/src/pages/nearle/orders/multipleOrders.js b/src/pages/nearle/orders/multipleOrders.js index a1f99f3..b045285 100644 --- a/src/pages/nearle/orders/multipleOrders.js +++ b/src/pages/nearle/orders/multipleOrders.js @@ -1,6 +1,7 @@ /* eslint-disable no-unused-vars */ import React, { useEffect, useState, useRef } from 'react'; -import axios from 'axios'; +import { getAdminPricing, getTenantLocations, getTenantCustomers, createExpressBookingBulk } from 'pages/api/doormileApi'; +import { fetchAppLocations as fetchZoneLocations } from 'pages/api/api'; import Papa from 'papaparse'; import * as XLSX from 'xlsx'; import dayjs from 'dayjs'; @@ -156,8 +157,8 @@ const MultipleOrders = () => { const fetchAppLocations = async () => { setLoading(true); try { - const res = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`); - setLocations(res.data.details || []); + const locations = await fetchZoneLocations(); + setLocations(locations || []); } catch (err) { OpenToast(err.message, 'error', 5000); } finally { @@ -165,28 +166,33 @@ const MultipleOrders = () => { } }; - useEffect(() => { fetchAppLocations(); }, []); + useEffect(() => { + fetchAppLocations(); + }, []); // ============================== fetchTenantPricing ============================== const fetchTenantPricing = async (id) => { try { - const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${id}`); - const d = res.data.details || {}; - setBasePrice(d.baseprice || 0); - setPricePerKm(d.priceperkm || 0); - setMinKm(d.minkm || 0); + const pricing = (await getAdminPricing()) || []; + const match = pricing.find((p) => p.tenantid === id); + if (match) { + setBasePrice(match.baseprice || 0); + setPricePerKm(match.priceperkm || 0); + setMinKm(match.basedistance || 0); + } } catch (err) { console.log('fetchTenantPricing', err); } }; - useEffect(() => { if (tenantid) fetchTenantPricing(tenantid); }, []); + useEffect(() => { + if (tenantid) fetchTenantPricing(tenantid); + }, []); // ============================== gettenantlocations ============================== const gettenantlocations = async (id) => { try { - const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`); - const details = res.data.details || []; + const details = (await getTenantLocations(id)) || []; if (details.length === 1) { setTenantlocations(details); setPickCust(details[0]); @@ -201,16 +207,23 @@ const MultipleOrders = () => { } }; - useEffect(() => { if (tenantid) gettenantlocations(tenantid); }, []); + useEffect(() => { + if (tenantid) gettenantlocations(tenantid); + }, []); // ============================== clientdetails ============================== const clientdetails = async () => { try { - const url = searchCustList === '' - ? `${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tenantid}&pageno=1&pagesize=30` - : `${process.env.REACT_APP_URL}/customers/search/?tenantid=${tenantid}&keyword=${searchCustList}`; - const res = await axios.get(url); - if (res.data.status) setCustomerlist(res.data.details || []); + const customers = (await getTenantCustomers()) || []; + const filtered = searchCustList + ? customers.filter( + (val) => + val.firstname?.toLowerCase().includes(searchCustList.toLowerCase()) || + val.phone?.includes(searchCustList) || + val.contactno?.includes(searchCustList) + ) + : customers; + setCustomerlist(filtered); } catch (err) { opentoast('server error', 'warning'); } @@ -226,7 +239,10 @@ const MultipleOrders = () => { // ============================== totals ============================== useEffect(() => { - let a1 = 0, a2 = 0, a3 = 0, a4 = 0; + let a1 = 0, + a2 = 0, + a3 = 0, + a4 = 0; dropCust.forEach((c) => { a1 += Number(c.distance) || 0; a2 += Number(c.totalcharge) || 0; @@ -298,35 +314,15 @@ const MultipleOrders = () => { } }; - // ============================== fetchAppAdminTokens ============================== - const fetchAppAdminTokens = async () => { - try { - const res = await axios.get(`${process.env.REACT_APP_URL}/utils/getapplocationconfig/?applocationid=${appId}`); - if (res.data.status) { - const tokens = res.data.details.applocationadmins.map((a) => a.userfcmtokem); - setAdmintoken(tokens); - } - } catch (err) { - console.log(err); - } - }; + // No zone-admin-token or bulk-notify endpoint in the new API. + const fetchAppAdminTokens = async () => setAdmintoken([]); - useEffect(() => { if (appId) fetchAppAdminTokens(); }, [appId]); + useEffect(() => { + if (appId) fetchAppAdminTokens(); + }, [appId]); // ============================== sendnotifications ============================== - const sendnotifications = async () => { - try { - const res = await axios.post(`${process.env.REACT_APP_URL}/utils/sendnotifications`, { - priority: 'high', - registration_ids: admintoken, - data: { accessid: process.env.REACT_APP_RIDER_ACCESS_ID }, - notification: { title: 'Nearle Merchant', body: 'An Order has been placed successfully, kindly process the same', sound: 'ring' } - }); - if (res.data.message === 'Success') opentoast('Notification sent Successfully', 'success', 1000); - } catch (err) { - opentoast(err.message, 'error', 1000); - } - }; + const sendnotifications = async () => {}; // ============================== CSV / XLSX upload ============================== const cleanReceiverName = (name) => (typeof name === 'string' ? name.replace(/^[\d.\s]+/, '').trim() : name); @@ -351,11 +347,17 @@ const MultipleOrders = () => { const handleFileDirectUpload = (event) => { try { const file = event.target.files?.[0]; - if (!file) { opentoast('No file selected.', 'warning'); return; } + if (!file) { + opentoast('No file selected.', 'warning'); + return; + } const fileNameLower = file.name.toLowerCase(); const isCSV = fileNameLower.endsWith('.csv'); const isExcel = fileNameLower.endsWith('.xls') || fileNameLower.endsWith('.xlsx'); - if (!isCSV && !isExcel) { opentoast('Invalid file type. Please upload a CSV or Excel file.', 'warning'); return; } + if (!isCSV && !isExcel) { + opentoast('Invalid file type. Please upload a CSV or Excel file.', 'warning'); + return; + } const processData = (data, headers) => { const normalizedMap = {}; @@ -383,9 +385,15 @@ const MultipleOrders = () => { if (isCSV) { Papa.parse(file, { - header: true, dynamicTyping: true, skipEmptyLines: true, + header: true, + dynamicTyping: true, + skipEmptyLines: true, complete: (results) => { - if (!results.data?.length) { opentoast('CSV file is empty or has no valid rows.', 'warning'); setUsers([]); return; } + if (!results.data?.length) { + opentoast('CSV file is empty or has no valid rows.', 'warning'); + setUsers([]); + return; + } processData(results.data, results.meta.fields.map(normalizeHeader)); }, error: (error) => opentoast(`CSV parsing failed: ${error.message}`, 'warning') @@ -397,7 +405,11 @@ const MultipleOrders = () => { try { const workbook = XLSX.read(e.target.result, { type: 'binary' }); const jsonData = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], { defval: '' }); - if (!jsonData?.length) { opentoast('Excel file is empty or invalid.', 'warning'); setUsers([]); return; } + if (!jsonData?.length) { + opentoast('Excel file is empty or invalid.', 'warning'); + setUsers([]); + return; + } processData(jsonData, Object.keys(jsonData[0]).map(normalizeHeader)); } catch (err) { opentoast(`Error reading Excel: ${err.message}`, 'warning'); @@ -442,71 +454,76 @@ const MultipleOrders = () => { // ============================== createorders ============================== const createorders = async () => { - if (!tenantid) { opentoast('Client not found. Please re-login.', 'warning'); return; } - if (!pickCust) { opentoast('Pickup location required', 'warning'); return; } - if (!pickupSlot) { opentoast('Select a pickup slot', 'warning'); return; } - if (!dropCust.length) { opentoast('Add at least one drop customer', 'warning'); return; } + if (!tenantid) { + opentoast('Client not found. Please re-login.', 'warning'); + return; + } + if (!pickCust) { + opentoast('Pickup location required', 'warning'); + return; + } + if (!pickupSlot) { + opentoast('Select a pickup slot', 'warning'); + return; + } + if (!dropCust.length) { + opentoast('Add at least one drop customer', 'warning'); + return; + } const deliverytime = buildDeliveryTime(); - const arr = dropCust.map((customer) => ({ - applocationid: pickCust.applocationid, - configid: 9, - partnerid: pickCust.partnerid, - partneruserid: +userid, - paymentstatus: 1, - paymenttype: 42, - pickupaddress: pickCust.address || '', - pickupcity: pickCust.city || '', - pickupcontactno: pickCust.contactno || '', - pickupcustomer: pickCust.locationname || '', - pickuplandmark: pickCust.landmark || '', - pickuplat: pickCust.latitude, - pickuplocation: pickCust.suburb || '', - pickuplocationid: pickCust.locationid || 0, - pickuplong: pickCust.longitude, - tenantid: pickCust.tenantid, - customerid: +customer?.customerid, + // POST /admin/expressbooking/bulk takes { bookings: [...] } where each + // entry is a flat booking object (tenantid, pickuplocationid, customer_*, + // delivery*, service_option, finalprice, notes, parcels[]) — not the old + // per-row orders/pickup/drop shape. pickup* fields follow the doc's own + // delivery*/pickup* naming symmetry (unverified — not shown in the doc's + // example, only pickuplocationid is confirmed). + // tenantid comes from the logged-in session (see the top-level `tenantid` + // const above), not from pickCust — pickCust is a tenant *location* + // object (GET /admin/tenants/:id/locations) and was never confirmed to + // carry a tenantid back-reference, so `pickCust.tenantid` was silently + // undefined on every submit. + const bookings = dropCust.map((customer) => ({ + tenantid: Number(tenantid), + ...(pickCust.locationid + ? { pickuplocationid: pickCust.locationid } + : { + pickupaddress: pickCust.address || '', + pickuppincode: pickCust.postcode || '', + pickupcity: pickCust.city || '', + pickuplatitude: Number(pickCust.latitude) || 0, + pickuplongitude: Number(pickCust.longitude) || 0 + }), + customer_phone: customer.contactno?.toString() || '', + customer_name: customer.firstname || '', deliveryaddress: customer.address || '', - deliverycharge: +customer.totalcharge || 0, + deliverypincode: customer.postcode || '', deliverycity: customer.city || '', - deliverycontactno: customer.contactno?.toString() || '', - deliverycustomer: customer.firstname || '', - deliveryid: +customer.customerid, - deliverylandmark: customer.landmark || '', - deliverylat: customer.latitude?.toString() || '', - deliverylocation: customer.suburb || '', - deliverylocationid: customer.deliverylocationid || 0, - deliverylong: customer.longitude?.toString() || '', + deliverylatitude: Number(customer.latitude) || 0, + deliverylongitude: Number(customer.longitude) || 0, deliverytime, - deliverytype: 'B', - itemcount: 1, - quantity: customer.quantity, - collectionamt: customer.collectionamt, - kms: customer.distance?.toString() || '0', - locationid: +pickCust.locationid, - moduleid: +pickCust.moduleid, - orderamount: +customer.totalcharge || 0, - ordercharges: 0.0, - orderdate: dayjs().format('YYYY-MM-DD HH:mm:ss'), - ordernotes: otherinstructions, - orderstatus: 'created', - ordervalue: +customer.totalcharge || 0, - pickupSlot + service_option: 'Normal', + finalprice: +customer.totalcharge || 0, + notes: otherinstructions || '', + parcels: [{ itemcategory: 'General', itemdescription: 'Order', declaredvalue: +customer.totalcharge || 0 }] })); setLoading(true); setBtnLoading(true); try { - const res = await axios.post(`${process.env.REACT_APP_URL}/orders/createorders`, arr); - if (res.data.status) { + const res = await createExpressBookingBulk(bookings); + if (res.success) { opentoast('Orders Created Successfully', 'success', 2000); - if (admintoken) sendnotifications(); navigate('/nearle/orders'); } else { - opentoast(res?.data?.message || 'Order creation failed. Please try again.', 'warning', 3000); + opentoast(res?.message || 'Order creation failed. Please try again.', 'warning', 3000); } } catch (err) { - const msg = err.response ? err.response.data?.message || `Server error (${err.response.status})` : err.request ? 'Network error. Check your internet connection.' : 'Something went wrong.'; + const msg = err.response + ? err.response.data?.message || `Server error (${err.response.status})` + : err.request + ? 'Network error. Check your internet connection.' + : 'Something went wrong.'; opentoast(msg, 'error', 3000); } finally { setLoading(false); @@ -548,7 +565,9 @@ const MultipleOrders = () => { size="small" label={`${dropCust.length} drop${dropCust.length === 1 ? '' : 's'}`} sx={{ - height: 20, fontSize: 10.5, fontWeight: 700, + height: 20, + fontSize: 10.5, + fontWeight: 700, bgcolor: dropCust.length ? 'rgba(24,144,255,0.10)' : '#f1f5f9', color: dropCust.length ? '#1890ff' : '#94a3b8', border: `1px solid ${dropCust.length ? 'rgba(24,144,255,0.25)' : '#e2e8f0'}` @@ -562,13 +581,16 @@ const MultipleOrders = () => { {/* 50 / 50 workspace */} - {/* LEFT — input fields */} { size="small" fullWidth options={tenantLocations || []} - getOptionLabel={(option) => option ? `${option.locationname} (${option.suburb || option.locationsuburb || ''})` : ''} + getOptionLabel={(option) => + option ? `${option.locationname} (${option.suburb || option.locationsuburb || ''})` : '' + } value={locationValue} onChange={(event, value, reason) => { if (reason === 'clear' || !value) { @@ -633,7 +657,12 @@ const MultipleOrders = () => { /> )} sx={{ - '& .MuiOutlinedInput-root': { borderRadius: '10px', height: '36px', alignItems: 'center', padding: '0 9px !important' }, + '& .MuiOutlinedInput-root': { + borderRadius: '10px', + height: '36px', + alignItems: 'center', + padding: '0 9px !important' + }, '& .MuiAutocomplete-input': { padding: '0 !important' } }} /> @@ -642,10 +671,14 @@ const MultipleOrders = () => { { - if (!e || !dayjs(e).isValid()) { setStartdate(dayjs().format('MM-DD-YYYY')); return; } + if (!e || !dayjs(e).isValid()) { + setStartdate(dayjs().format('MM-DD-YYYY')); + return; + } const diffDays = dayjs().diff(dayjs(dayjs(e).format('YYYY-MM-DD')), 'd'); if (diffDays <= 0) { setStartdate(dayjs(e).format('MM-DD-YYYY')); @@ -659,7 +692,9 @@ const MultipleOrders = () => { disablePast slotProps={{ textField: { - size: 'small', fullWidth: true, InputLabelProps: { shrink: true }, + size: 'small', + fullWidth: true, + InputLabelProps: { shrink: true }, InputProps: { startAdornment: ( @@ -675,7 +710,9 @@ const MultipleOrders = () => { { if (!locationid) { @@ -686,21 +723,36 @@ const MultipleOrders = () => { }} onClose={() => setSlotDropdownOpen(false)} sx={{ - '& .MuiOutlinedInput-root': { borderRadius: '10px', height: '36px', alignItems: 'center', padding: '0 9px !important' }, + '& .MuiOutlinedInput-root': { + borderRadius: '10px', + height: '36px', + alignItems: 'center', + padding: '0 9px !important' + }, '& .MuiAutocomplete-input': { padding: '0 !important' } }} onChange={(e, newValue, reason) => { - if (reason === 'clear' || !newValue) { setSelectedtime(null); setPickupSlot(null); return; } - if (!newValue.time) { OpenToast('This slot has no time configured.', 'warning', 3000); return; } + if (reason === 'clear' || !newValue) { + setSelectedtime(null); + setPickupSlot(null); + return; + } + if (!newValue.time) { + OpenToast('This slot has no time configured.', 'warning', 3000); + return; + } const formattedTime = dayjs(newValue.time, 'HH:mm').format('hh:mm A'); setSelectedtime(formattedTime); const finalDateTime = dayjs(`${startdate} ${formattedTime}`, 'MM-DD-YYYY hh:mm A').format('YYYY-MM-DD hh:mm A'); setPickupSlot(finalDateTime); }} - getOptionLabel={(option) => option ? `${option.name} (${dayjs(option.time, 'HH:mm').format('hh:mm A')})` : ''} + getOptionLabel={(option) => (option ? `${option.name} (${dayjs(option.time, 'HH:mm').format('hh:mm A')})` : '')} renderInput={(params) => ( { {pickCust ? ( - + @@ -733,9 +808,7 @@ const MultipleOrders = () => { ) : ( - - Pickup auto-fills once a Business Location is selected. - + Pickup auto-fills once a Business Location is selected. )} @@ -754,12 +827,20 @@ const MultipleOrders = () => { Special Dispatch Notes setOtherinstructions(e.target.value)} sx={{ - '& .MuiOutlinedInput-root': { borderRadius: '10px', height: '36px', alignItems: 'center', fontSize: '12px', background: '#ffffff' }, + '& .MuiOutlinedInput-root': { + borderRadius: '10px', + height: '36px', + alignItems: 'center', + fontSize: '12px', + background: '#ffffff' + }, '& .MuiOutlinedInput-input': { padding: '0 14px !important', fontSize: '12px' } }} /> @@ -770,27 +851,100 @@ const MultipleOrders = () => { {/* Card: Summary + Submit */} - Bulk Summary - Live totals + + Bulk Summary + + + Live totals + {(() => { const metric = ({ icon: Icon, label, value, accent, active }) => ( - - + + - {label} - {value} + + {label} + + + {value} + ); return ( - {metric({ icon: FaRoute, label: 'Distance', value: totaldist ? `${totaldist} km` : '—', accent: '#1890ff', active: !!totaldist })} - {metric({ icon: FaBoxes, label: 'Quantity', value: totalQty || 0, accent: '#16a34a', active: !!totalQty })} - {metric({ icon: FaMoneyBillWave, label: 'Cash Collect', value: `₹${Number(totalCash).toFixed(2)}`, accent: '#d97706', active: !!totalCash })} - {metric({ icon: FaTruck, label: 'Deliveries', value: dropCust.length, accent: '#65387a', active: dropCust.length > 0 })} + + {metric({ + icon: FaRoute, + label: 'Distance', + value: totaldist ? `${totaldist} km` : '—', + accent: '#1890ff', + active: !!totaldist + })} + + + {metric({ icon: FaBoxes, label: 'Quantity', value: totalQty || 0, accent: '#16a34a', active: !!totalQty })} + + + {metric({ + icon: FaMoneyBillWave, + label: 'Cash Collect', + value: `₹${Number(totalCash).toFixed(2)}`, + accent: '#d97706', + active: !!totalCash + })} + + + {metric({ + icon: FaTruck, + label: 'Deliveries', + value: dropCust.length, + accent: '#65387a', + active: dropCust.length > 0 + })} + ); })()} @@ -801,22 +955,26 @@ const MultipleOrders = () => {
    Total Charge
    -
    ₹{Number(totalAmt).toFixed(2)}
    +
    + ₹{Number(totalAmt).toFixed(2)} +
    )}
    @@ -824,7 +982,10 @@ const MultipleOrders = () => { {/* RIGHT — file / drop preview */} - + {/* Preview header */} @@ -834,58 +995,161 @@ const MultipleOrders = () => { {previewMode === 'drops' && ( - - )} - {previewMode === 'preview' && ( - } - sx={{ height: 22, fontWeight: 700, fontSize: 11, bgcolor: 'rgba(245,158,11,0.12)', color: '#b45309', border: '1px solid rgba(245,158,11,0.30)', '& .MuiChip-icon': { color: '#b45309' } }} + )} - {previewMode !== 'empty' && (() => { - const handleHeaderPick = (val) => { - if (!prereqOk) { OpenToast('Please select a Business Location first.', 'warning', 3000); return; } - setUploadType(val); - if (val === 0) document.getElementById('upload-file')?.click(); - else if (val === 1) { setIsCustomerOpen(true); setSearchCustList(''); } - }; - return ( - - - - - ); - })()} + {previewMode === 'preview' && ( + } + sx={{ + height: 22, + fontWeight: 700, + fontSize: 11, + bgcolor: 'rgba(245,158,11,0.12)', + color: '#b45309', + border: '1px solid rgba(245,158,11,0.30)', + '& .MuiChip-icon': { color: '#b45309' } + }} + /> + )} + {previewMode !== 'empty' && + (() => { + const handleHeaderPick = (val) => { + if (!prereqOk) { + OpenToast('Please select a Business Location first.', 'warning', 3000); + return; + } + setUploadType(val); + if (val === 0) document.getElementById('upload-file')?.click(); + else if (val === 1) { + setIsCustomerOpen(true); + setSearchCustList(''); + } + }; + return ( + + + + + ); + })()} {fileName && ( - {fileName} + + {fileName} + )} {previewMode === 'preview' && users.length >= 1 && ( - + - Process & Calculate Distances - Click continue to import spreadsheet rows and calculate drop charges. + + Process & Calculate Distances + + + Click continue to import spreadsheet rows and calculate drop charges. + - @@ -893,7 +1157,6 @@ const MultipleOrders = () => { {/* Scrollable preview body */} - {/* Drops — table */} {previewMode === 'drops' && ( @@ -903,28 +1166,52 @@ const MultipleOrders = () => { # Customer Address - Qty - Cash + + Qty + + + Cash + Km - Charge - + + Charge + + + {' '} + {dropCust.map((customer, index) => ( {index + 1} - {customer.firstname} + + {customer.firstname} + - + {customer.address} - {uploadType === 0 ? customer.quantity : ( - handleQuantityChange(customer.customerid, e.target.value)} inputProps={{ min: 0 }} sx={{ width: 64, '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 30 } }} @@ -932,9 +1219,18 @@ const MultipleOrders = () => { )} - {uploadType === 0 ? `₹${Number(customer.collectionamt || 0).toFixed(2)}` : ( - { const v = Number(e.target.value); handleCollectionAmtChange(customer.customerid, v > 0 ? v : 0); }} + {uploadType === 0 ? ( + `₹${Number(customer.collectionamt || 0).toFixed(2)}` + ) : ( + { + const v = Number(e.target.value); + handleCollectionAmtChange(customer.customerid, v > 0 ? v : 0); + }} inputProps={{ min: 0 }} InputProps={{ startAdornment: }} sx={{ width: 90, '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 30 } }} @@ -942,7 +1238,9 @@ const MultipleOrders = () => { )} {customer.distance} - ₹{Number(customer?.totalcharge || 0).toFixed(2)} + + ₹{Number(customer?.totalcharge || 0).toFixed(2)} + handleCheckboxChange1(customer)} sx={{ color: '#ef4444', p: 0.5 }}> @@ -955,10 +1253,16 @@ const MultipleOrders = () => { Total - {totalQty} - ₹{Number(totalCash).toFixed(2)} + + {totalQty} + + + ₹{Number(totalCash).toFixed(2)} + {totaldist} - ₹{Number(totalAmt).toFixed(2)} + + ₹{Number(totalAmt).toFixed(2)} + @@ -976,25 +1280,44 @@ const MultipleOrders = () => { Name Contact Address - Qty - Cash + + Qty + + + Cash + {users.map((u, i) => ( {i + 1} - {u.firstname || '—'} + + {u.firstname || '—'} + {u.contactno || '—'} - + {u.address || '—'} - {u.quantity ?? '—'} - {u.collectionamt != null ? `₹${Number(u.collectionamt).toFixed(2)}` : '—'} + + {u.quantity ?? '—'} + + + {u.collectionamt != null ? `₹${Number(u.collectionamt).toFixed(2)}` : '—'} + ))} @@ -1003,59 +1326,145 @@ const MultipleOrders = () => { )} {/* Empty state */} - {previewMode === 'empty' && (() => { - const handleEmptyPick = (val) => { - if (!prereqOk) { OpenToast('Please select a Business Location first.', 'warning', 3000); return; } - setUploadType(val); - setDropCust([]); setUsers([]); setFileName(''); - if (val === 0) document.getElementById('upload-file')?.click(); - else if (val === 1) { setIsCustomerOpen(true); setSearchCustList(''); } - }; - const tile = ({ value, icon: Icon, title, sub, accent }) => ( - handleEmptyPick(value)} - onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleEmptyPick(value); } }} - sx={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', textAlign: 'left', gap: 1.25, px: 1.5, py: 1.25, borderRadius: '10px', border: '1.5px solid #eef2f6', bgcolor: '#fff', opacity: prereqOk ? 1 : 0.6, cursor: prereqOk ? 'pointer' : 'not-allowed', transition: 'all 0.18s ease', '&:hover': prereqOk ? { borderColor: accent, boxShadow: `0 4px 12px -4px ${accent}40`, transform: 'translateY(-1px)' } : undefined }} - > - - + {previewMode === 'empty' && + (() => { + const handleEmptyPick = (val) => { + if (!prereqOk) { + OpenToast('Please select a Business Location first.', 'warning', 3000); + return; + } + setUploadType(val); + setDropCust([]); + setUsers([]); + setFileName(''); + if (val === 0) document.getElementById('upload-file')?.click(); + else if (val === 1) { + setIsCustomerOpen(true); + setSearchCustList(''); + } + }; + const tile = ({ value, icon: Icon, title, sub, accent }) => ( + handleEmptyPick(value)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleEmptyPick(value); + } + }} + sx={{ + flex: 1, + minWidth: 0, + display: 'flex', + alignItems: 'center', + textAlign: 'left', + gap: 1.25, + px: 1.5, + py: 1.25, + borderRadius: '10px', + border: '1.5px solid #eef2f6', + bgcolor: '#fff', + opacity: prereqOk ? 1 : 0.6, + cursor: prereqOk ? 'pointer' : 'not-allowed', + transition: 'all 0.18s ease', + '&:hover': prereqOk + ? { borderColor: accent, boxShadow: `0 4px 12px -4px ${accent}40`, transform: 'translateY(-1px)' } + : undefined + }} + > + + + + + {title} + + {sub} + + - - {title} - {sub} - - - ); - return ( - - - - - Choose a Drop Source to begin - - Select a source below to import or pick your delivery customers. - - - {tile({ value: 0, icon: MdOutlineCloudUpload, title: 'Excel / CSV', sub: 'Bulk upload a sheet', accent: '#1890ff' })} - {tile({ value: 1, icon: FaUsers, title: 'Selection', sub: 'Pick saved customers', accent: '#65387a' })} + ); + return ( + + + + + Choose a Drop Source to begin + + Select a source below to import or pick your delivery customers. + + + {tile({ + value: 0, + icon: MdOutlineCloudUpload, + title: 'Excel / CSV', + sub: 'Bulk upload a sheet', + accent: '#1890ff' + })} + {tile({ value: 1, icon: FaUsers, title: 'Selection', sub: 'Pick saved customers', accent: '#65387a' })} + - - ); - })()} + ); + })()}
    - + {/* Saved customers dialog */} setIsCustomerOpen(false)} - fullWidth fullScreen={isMobile} + open={isCustomerOpen} + onClose={() => setIsCustomerOpen(false)} + fullWidth + fullScreen={isMobile} sx={{ '& .MuiDialog-paper': { borderRadius: { xs: 0, sm: '16px' }, overflow: 'hidden' } }} > @@ -1065,11 +1474,23 @@ const MultipleOrders = () => { setSearchCustList(e.target.value)} + autoFocus + fullWidth + id="input-search-header" + placeholder="Search by name, contact, or address…" + value={searchCustList} + onChange={(e) => setSearchCustList(e.target.value)} sx={{ bgcolor: 'white', borderRadius: '10px', '& .MuiOutlinedInput-input': { p: '10px 14px' } }} - startAdornment={} - endAdornment={ setSearchCustList('')}>} + startAdornment={ + + + + } + endAdornment={ + setSearchCustList('')}> + + + } autoComplete="off" /> @@ -1108,7 +1529,8 @@ const MultipleOrders = () => { - - - - )} - {users.length >= 1 && ( - - )} - -
    - } - > - - - - - S.No - Customer - Address - Kms - Charge - Action - - - - {dropCust.map((customer, index) => ( - - {index + 1} - {customer.firstname} - {/* {customer.ReceiverName} */} - {customer.address} - {customer.distance} - {`₹${customer.totalcharge}.00`} - - { - <> - handleCheckboxChange(event, customer)} - /> - - } - - - ))} - {dropCust.length != 0 && ( - - Total - - - - {`${totaldist} `} - - - - {`₹${totalAmt}.00`} - - - - )} - -
    -
    - - - - - {/* ================================================= || Notes || ================================================= */} - - - - setOtherinstructions(e.target.value)} - /> - - - - - - - - - {/* ============================================= || saved address Dialog || ============================================= */} - { - setIsCustomerOpen(false); - }} - fullWidth - sx={{ minWidth: 'lg' }} - > - - - {`Select Drop Customers (${dropCust.length || 0})`} - - - setSearchCustList(e.target.value)} - sx={{ - '& .MuiOutlinedInput-input': { - p: '10.5px 0px 12px' - }, - bgcolor: 'white' - }} - startAdornment={ - - - - } - endAdornment={ - { - setSearchCustList(''); - }} - > - - - } - autoComplete="off" - /> - - - - - - - {customerlist.length == 0 ? ( - - - - ) : ( - - {customerlist && - customerlist.map((customer, index) => ( - - cust.customerid === customer.customerid)} // Set the checked state of the checkbox based on whether the customer is in `dropCust` - onChange={(event) => handleCheckboxChange(event, customer)} - /> - } - label={ -
    - - {`${customer.firstname} (${customer.contactno})`} - - - - {customer.address} - -
    - } - /> -
    - ))} -
    - )} -
    - - - - -
    - - ); -}; - -export default MultipleOrders; diff --git a/src/pages/nearle/orders/orders.js b/src/pages/nearle/orders/orders.js index 7be13bf..52d48e2 100644 --- a/src/pages/nearle/orders/orders.js +++ b/src/pages/nearle/orders/orders.js @@ -5,7 +5,6 @@ import { useState, useEffect, Fragment, useRef } from 'react'; import dayjs from 'dayjs'; var utc = require('dayjs/plugin/utc'); dayjs.extend(utc); -import axios from 'axios'; import { Avatar, @@ -23,17 +22,14 @@ import { Dialog, TableRow, DialogContent, + DialogTitle, + DialogActions, Tooltip, Skeleton, CircularProgress, InputBase, Backdrop, - SpeedDial, - SpeedDialIcon, - SpeedDialAction, - Badge, - TableContainer, - Checkbox + TableContainer } from '@mui/material'; import { MdAccessTime, @@ -51,7 +47,8 @@ import { MdCalendarMonth, MdReceiptLong, MdClear, - MdNotes + MdNotes, + MdMyLocation } from 'react-icons/md'; import { DeleteOutlined } from '@ant-design/icons'; import Loader from 'components/Loader'; @@ -60,13 +57,8 @@ import CircularLoader from 'components/CircularLoader'; import AiImage from '../../../assets/images/aiImage.png'; import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query'; import { useNavigate, useLocation } from 'react-router-dom'; -import { - fetchPercentageData, - createAutomationDeliveries, - cancelMultipleOrder, - getallriders, - fetchorderscount -} from '../../api/api'; +import { fetchPercentageData, createAutomationDeliveries, getallriders, fetchorderscount } from '../../api/api'; +import { getBookings, cancelBooking, getAdminCustomers, getBookingTrack } from 'pages/api/doormileApi'; // ============================================================================ // Design tokens — shared with the rest of the redesigned operator pages. @@ -99,29 +91,37 @@ const dtEdge = edge; const BRAND = '#C01227'; const BRAND_LIGHT = '#D25463'; - -// Semantic per-row status palette — colors per brand standard: -// green=delivered, amber=pending, blue=created/processing, red=cancelled, -// dark-red=failed, purple=on-hold. +// Semantic per-row status palette, keyed by GET /admin/bookings' real `status` +// enum (lowercased — StatusBadge lowercases before lookup). Only +// pending_pickup and converted_to_consignment have been observed against the +// live API so far; the rest are reasonable guesses at likely sibling states +// with a safe unknown-status fallback in StatusBadge below. const ROW_STATUS_META = { - created: { label: 'Created', color: '#3b82f6', icon: MdLocalShipping }, - pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty }, - processing: { label: 'Processing', color: '#3b82f6', icon: MdAccessTime }, - modified: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle }, - confirmed: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle }, - ready: { label: 'Accepted', color: '#6366f1', icon: MdCheckCircle }, - active: { label: 'Picked', color: '#8b5cf6', icon: MdLocalShipping }, - onhold: { label: 'On Hold', color: '#8b5cf6', icon: MdHistoryToggleOff }, - closed: { label: 'Closed', color: '#06b6d4', icon: MdCheckCircle }, - delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle }, - failed: { label: 'Failed', color: '#991b1b', icon: MdCancel }, - cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel } + pending_pickup: { label: 'Pending Pickup', color: '#f59e0b', icon: MdHourglassEmpty }, + pending_assignment: { label: 'Pending Assignment', color: '#f59e0b', icon: MdHourglassEmpty }, + converted_to_consignment: { label: 'Converted', color: '#6366f1', icon: MdCheckCircle }, + delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle }, + cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel } }; -// Top-level pill tabs. -const ORDERS_STATUS_TABS = [ - { idx: 0, status: 'created', label: 'Created', color: BRAND, icon: MdLocalShipping, countKey: 'created' } -]; +// Top-level pill tabs. GET /admin/bookings has no documented status filter +// param, so this tab doesn't narrow the server query — it's cosmetic until +// the API supports it. +const ORDERS_STATUS_TABS = [{ idx: 0, status: 'created', label: 'Bookings', color: BRAND, icon: MdLocalShipping, countKey: 'created' }]; + +// Haversine straight-line distance in km — GET /admin/bookings has no `kms` +// field, but does carry both pickup*/delivery* lat/lng, so this is computed +// client-side rather than left blank. It's a straight line, not the road +// route the old `kms` field represented — labelled accordingly in the header. +const haversineKm = (lat1, lon1, lat2, lon2) => { + if (![lat1, lon1, lat2, lon2].every(Number.isFinite)) return null; + const R = 6371; + const toRad = (d) => (d * Math.PI) / 180; + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lon2 - lon1); + const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +}; const StatusBadge = ({ status }) => { const meta = ROW_STATUS_META[String(status || '').toLowerCase()] || { @@ -201,6 +201,16 @@ const Orders = () => { const [tabstatus, setTabstatus] = useState('Created'); const [currentStatus, setCurrentStatus] = useState('created'); const [cancelOpen, setCancelOpen] = useState(false); + const [trackBookingId, setTrackBookingId] = useState(null); + + // GET /admin/bookings/:id/track — response shape isn't documented beyond + // the endpoint existing (Status: Done in jupiter2doormile.md). Rendered + // defensively below rather than assuming specific field names. + const { data: trackData, isLoading: trackLoading } = useQuery({ + queryKey: ['booking-track', trackBookingId], + queryFn: () => getBookingTrack(trackBookingId), + enabled: !!trackBookingId + }); const [orderheaderid, setOrderheaderid] = useState(''); const locationId = 0; const locoName = 'All Locations'; @@ -213,10 +223,8 @@ const Orders = () => { // Floating button and Dialog states const [speedDialOpen, setSpeedDialOpen] = useState(false); - const [multiDeleteDialog, setMultiDeleteDialog] = useState(false); const [createloader, setCreateloader] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [deliverylist, setDeliverylist] = useState([]); useEffect(() => { const handler = setTimeout(() => { @@ -247,10 +255,7 @@ const Orders = () => { refetchInterval: 15000 }); - const { - data: ordersCountData, - refetch: orderscountRefetch - } = useQuery({ + const { data: ordersCountData, refetch: orderscountRefetch } = useQuery({ queryKey: ['ordersCount', locationId, startdate, enddate, currentStatus, tid, locationId], queryFn: fetchorderscount, refetchOnMount: true, @@ -258,47 +263,69 @@ const Orders = () => { refetchInterval: 15000 }); - const { - data: autoRiders - } = useQuery({ + const { data: autoRiders } = useQuery({ queryKey: ['getallriders'], queryFn: getallriders, refetchOnMount: true, refetchOnWindowFocus: true }); - const cancelorder = async () => { - await axios - .put(`${process.env.REACT_APP_URL}/orders/updateorder`, { - orderheaderid: orderheaderid, - orderstatus: 'cancelled', - cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss') - }) - .then((res) => { - if (res.data.status) { - enqueueSnackbar('Order Cancelled Successfully', { - variant: 'success', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - refetchOrders(); - orderscountRefetch(); - percentagedataRefetch(); - setCancelOpen(false); - } - }) - .catch((err) => { - console.log(err); + // GET /admin/bookings only carries `appcustomerid` — no name/phone on the + // booking itself — so customer display fields are joined client-side + // against GET /admin/customers. Response field names for that endpoint + // aren't documented; firstname/phone are guessed by symmetry with + // tenantcustomers and fall back to showing the raw id. + const { data: customerMap } = useQuery({ + queryKey: ['adminCustomersMap'], + queryFn: async () => { + const customers = (await getAdminCustomers()) || []; + const map = new Map(); + customers.forEach((c) => { + const id = c.appcustomerid ?? c.customerid ?? c.id; + if (id != null) map.set(id, c); }); + return map; + }, + staleTime: 5 * 60 * 1000 + }); + + const cancelorder = async () => { + try { + const res = await cancelBooking(orderheaderid); + if (res.success) { + enqueueSnackbar('Order Cancelled Successfully', { + variant: 'success', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + refetchOrders(); + orderscountRefetch(); + percentagedataRefetch(); + setCancelOpen(false); + } else { + enqueueSnackbar(res.message || 'Failed to cancel order', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + } + } catch (err) { + console.log(err); + enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to cancel order', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + } }; + // Maps to /admin/bookings — tenantid/locationid/status/date filters used by + // the old endpoint aren't documented on this resource; pageno/pagesize are. const fetchOrders = async ({ pageParam = 1 }) => { - const res = await axios.get( - `${process.env.REACT_APP_URL}/orders/tenant/getorders/?tenantid=${tid}&locationid=${locationId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${debouncedSearch}` - ); + const data = (await getBookings(pageParam, rowsPerPage)) || []; return { - data: res.data.details, - nextPage: res.data.details.length === rowsPerPage ? pageParam + 1 : undefined + data, + nextPage: data.length === rowsPerPage ? pageParam + 1 : undefined }; }; @@ -352,7 +379,11 @@ const Orders = () => { const createDeliveryMutation = useMutation({ mutationFn: createAutomationDeliveries, onSuccess: (data, variables) => { - enqueueSnackbar('Orders Optimised Successfully', { variant: 'success', autoHideDuration: 2000, anchorOrigin: { vertical: 'top', horizontal: 'right' } }); + enqueueSnackbar('Orders Optimised Successfully', { + variant: 'success', + autoHideDuration: 2000, + anchorOrigin: { vertical: 'top', horizontal: 'right' } + }); orderscountRefetch(); refetchOrders(); setCreateloader(false); @@ -379,43 +410,37 @@ const Orders = () => { } }); - const cancelMultipleOrderMutation = useMutation({ - mutationFn: cancelMultipleOrder, - onSuccess: (data) => { - if (data.status) { - setMultiDeleteDialog(false); - enqueueSnackbar('Orders Cancelled Successfully', { variant: 'success', autoHideDuration: 2000 }); - refetchOrders(); - orderscountRefetch(); - percentagedataRefetch(); - setDeliverylist([]); - } - }, - onError: (error) => { - enqueueSnackbar(error.message, { variant: 'error', autoHideDuration: 4000 }); - } - }); - const handleCreateDelivery = async () => { if (rows.length === 0) return; setIsLoading(true); setCreateloader(true); - const deliveryData = rows.map((val) => ({ - ...val, - deliveryid: 0, - deliverydate: dayjs(val.deliverydate).utc().format('YYYY-MM-DD HH:mm:ss'), - assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss'), - orderstatus: 'pending', - orderamount: val.deliverycharge, - droplat: val.deliverylat, - droplon: val.deliverylong, - pickuplat: val.pickuplat, - pickuplon: val.pickuplong, - ordernotes: val.ordernotes, - deliverycharges: val.deliverycharge, - pickuplocation: val.pickupsuburb, - deliverylocation: val.deliverysuburb - })); + // The optimiser service (routes.workolik.com) itself is untouched, but the + // fields sourced here have to match GET /admin/bookings' real shape — + // deliverylat/pickuplat/deliverycharge/etc. don't exist on a booking + // object (see pickuplatitude/deliverylatitude/serviceoptions[0].estimatedprice + // instead). The solver's own expected input contract is unverified since + // it's a separate, unchanged service. + const deliveryData = rows.map((val) => { + const charge = val.serviceoptions?.[0]?.estimatedprice; + return { + ...val, + deliveryid: 0, + deliverydate: dayjs(val.serviceoptions?.[0]?.estimateddeliveryat || val.createdat) + .utc() + .format('YYYY-MM-DD HH:mm:ss'), + assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss'), + orderstatus: 'pending', + orderamount: charge, + droplat: val.deliverylatitude, + droplon: val.deliverylongitude, + pickuplat: val.pickuplatitude, + pickuplon: val.pickuplongitude, + ordernotes: val.notes, + deliverycharges: charge, + pickuplocation: val.pickupaddress, + deliverylocation: val.deliveryaddress + }; + }); createDeliveryMutation.mutate({ deliveries: deliveryData, @@ -427,8 +452,22 @@ const Orders = () => { // KPI tile definitions. const kpiCards = [ - { key: 'created', label: 'Created Orders', color: BRAND, icon: MdLocalShipping, value: percentageData?.created, percentage: percentageData?.percentage1 }, - { key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdHourglassEmpty, value: percentageData?.uncoveredOrders, percentage: percentageData?.percentage2 } + { + key: 'created', + label: 'Created Orders', + color: BRAND, + icon: MdLocalShipping, + value: percentageData?.created, + percentage: percentageData?.percentage1 + }, + { + key: 'pending', + label: 'Pending Orders', + color: '#f59e0b', + icon: MdHourglassEmpty, + value: percentageData?.uncoveredOrders, + percentage: percentageData?.percentage2 + } ]; return ( @@ -532,14 +571,9 @@ const Orders = () => {
    - - - - - {/* ============================================= || Status Tabs + Search (compact) || ============================================= */} { background: '#fff' }} > - + { ); })} - @@ -692,16 +719,16 @@ const Orders = () => { }} > # - Order Location + Order Pickup Drop Qty COD - Kms + Kms (approx) Charges Notes Status - {currentStatus === 'created' && Actions} + Actions @@ -710,7 +737,7 @@ const Orders = () => { rows.length === 0 && Array.from({ length: 10 }).map((_, idx) => ( - {Array.from({ length: currentStatus === 'created' ? 12 : 11 }).map((__, ci) => ( + {Array.from({ length: 11 }).map((__, ci) => ( @@ -720,7 +747,7 @@ const Orders = () => { {!isLoadingGetOrders && rows.length === 0 && ( - + { > - - No {currentStatus} orders - + No {currentStatus} orders {searchword ? 'Try a different keyword or clear the search.' : 'Adjust the location, status, or date range above.'} @@ -765,20 +790,13 @@ const Orders = () => { )} {rows.map((row, index) => { - const isItemSelected = !!deliverylist.find((res) => res.orderheaderid === row.orderheaderid); - const handleCheckbox = (e) => { - if (e.target.checked) { - setDeliverylist((prev) => [...prev, { ...row, sno: prev.length + 1 }]); - } else { - setDeliverylist((prev) => - prev.filter((item) => item.orderheaderid !== row.orderheaderid).map((item, i) => ({ ...item, sno: i + 1 })) - ); - } - }; + const customer = customerMap?.get(row.appcustomerid); + const kms = haversineKm(row.pickuplatitude, row.pickuplongitude, row.deliverylatitude, row.deliverylongitude); + const isCancellable = !['Cancelled', 'Converted_To_Consignment'].includes(row.status); return ( { }} > - - {page * rowsPerPage + index + 1} - + {page * rowsPerPage + index + 1} - {row.locationname} - {row.locationsuburb && ` - ${row.locationsuburb}`} + {row.bookingno || `#${row.bookingid}`} - - - {row.orderid} - - {(() => { - const dateObj = row.pickupslot && dayjs(row.pickupslot).isValid() - ? dayjs(row.pickupslot) - : dayjs(row.deliverydate || row.orderdate); + const dateObj = dayjs(row.createdat); return ( <> - {dateObj.format('hh:mm A')} + {dateObj.isValid() ? dateObj.format('hh:mm A') : '—'} - · {dateObj.format('DD MMM YY')} + {dateObj.isValid() && `· ${dateObj.format('DD MMM YY')}`} ); @@ -833,70 +841,85 @@ const Orders = () => { - - {row.pickupcustomer} - - - {row.pickupcontactno} - - - - {row.pickupsuburb || (row.pickupaddress ? `${row.pickupaddress.slice(0, 20)}…` : '—')} + + + {row.pickupaddress ? `${row.pickupaddress.slice(0, 20)}…` : '—'} + + {row.pickuppincode || ''} + - {row.deliverycustomer} + {customer?.firstname || customer?.name || `Customer #${row.appcustomerid ?? '—'}`} - {row.deliverycontactno} + {customer?.phone || customer?.contactno || ''} - + - {row.deliverysuburb || - (row.deliveryaddress?.length > 20 ? `${row.deliveryaddress.slice(0, 20)}…` : row.deliveryaddress || '—')} + {row.deliveryaddress?.length > 20 ? `${row.deliveryaddress.slice(0, 20)}…` : row.deliveryaddress || '—'} - } /> + } /> - } isMoney /> + + + — + + - } /> + {kms != null ? ( + + + } /> + + + ) : ( + } /> + )} - } isMoney /> + } + isMoney + /> - {row.ordernotes ? ( - - - - {row.ordernotes} - - + {row.notes ? ( + + + + + {row.notes} + + + ) : ( — @@ -905,18 +928,40 @@ const Orders = () => { - + - {currentStatus === 'created' && ( - - {row.orderstatus === 'created' && ( + + + + { + e.stopPropagation(); + setTrackBookingId(row.bookingid); + }} + sx={{ + bgcolor: tint('#0ea5e9'), + border: `1px solid ${edge('#0ea5e9')}`, + color: '#0ea5e9', + borderRadius: 999, + p: 0.75, + '&:hover': { + bgcolor: soft('#0ea5e9'), + borderColor: '#0ea5e9' + } + }} + > + + + + {currentStatus === 'created' && isCancellable && ( { e.stopPropagation(); - setOrderheaderid(row.orderheaderid); + setOrderheaderid(row.bookingid); setCancelOpen(true); }} sx={{ @@ -935,29 +980,20 @@ const Orders = () => { )} - - )} + + ); })} {rows.length !== 0 && ( - - + + {isFetchingNextPage || hasNextPage ? ( <> - - Loading more orders… - + Loading more orders… ) : ( @@ -1033,6 +1069,38 @@ const Orders = () => { + {/* ============================================= || Track Order || ============================================= */} + setTrackBookingId(null)} maxWidth="xs" fullWidth PaperProps={{ sx: { borderRadius: 3 } }}> + Track Order + + {trackLoading ? ( + + + + ) : trackData ? ( + + {Object.entries(trackData).map(([key, value]) => ( + + + {key} + + + {typeof value === 'object' ? JSON.stringify(value) : String(value ?? '—')} + + + ))} + + ) : ( + + No tracking data available for this order yet. + + )} + + + + + + {/* ============================================= || Date Filter || ============================================= */} { setDatestatus(label); }} /> - - {/* ============================================= || Cancel Multiple Orders Dialog || ============================================= */} - setMultiDeleteDialog(false)} maxWidth="xs" PaperProps={{ sx: { borderRadius: 3 } }}> - - - - - - - Cancel Selected Orders - - - - - - - Are you sure you want to cancel the {deliverylist.length} selected orders? This action cannot be undone. - - - - - - - - - ); }; diff --git a/src/pages/nearle/reports/ordersDetails.js b/src/pages/nearle/reports/ordersDetails.js index e595f68..39ae86c 100644 --- a/src/pages/nearle/reports/ordersDetails.js +++ b/src/pages/nearle/reports/ordersDetails.js @@ -1,5 +1,4 @@ import { React, useState, useEffect, useRef } from 'react'; -import axios from 'axios'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; // material-ui @@ -67,6 +66,7 @@ import { FaCircleCheck } from 'react-icons/fa6'; import MapWithRoute from './mapWithRoute'; import CircularLoader from 'components/CircularLoader'; import { fetchDeliveries, fetchRidersList, gettenantlocations, getTenants } from 'pages/api/api'; +import { getDashboard, getConsignmentLogs } from 'pages/api/doormileApi'; import { CSVExport } from 'components/third-party/ReactTable'; import Loader from 'components/Loader'; import { enqueueSnackbar } from 'notistack'; @@ -227,205 +227,6 @@ const StampCell = ({ value, formatDate, formatTime, success }) => { // ==============================|| Orders Details ||============================== // -// Haversine distance between two [lat, lng] points in kilometers. -function haversineKm(a, b) { - const R = 6371; // km - const toRad = (d) => (d * Math.PI) / 180; - const lat1 = toRad(a[0]); - const lat2 = toRad(b[0]); - const dLat = toRad(b[0] - a[0]); - const dLon = toRad(b[1] - a[1]); - const s = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2; - return 2 * R * Math.asin(Math.min(1, Math.sqrt(s))); -} - -function kalmanSmoothGps(pings, options = {}) { - if (!Array.isArray(pings) || pings.length === 0) return []; - - // 1. Filter out obviously invalid coordinate pings (e.g. 0,0 or NaN) - const cleanedPings = pings.filter(p => - Number.isFinite(p.lat) && - Number.isFinite(p.lng) && - (Math.abs(p.lat) > 0.1 || Math.abs(p.lng) > 0.1) - ); - - if (cleanedPings.length === 0) return []; - if (cleanedPings.length === 1) { - return [{ lat: cleanedPings[0].lat, lng: cleanedPings[0].lng, logdate: cleanedPings[0].logdate, _ts: cleanedPings[0]._ts }]; - } - - const processNoise = - options.processNoise != null ? options.processNoise : 1e-10; - const measurementNoise = - options.measurementNoise != null ? options.measurementNoise : 2e-9; - const outlierGate = - options.outlierGate != null ? options.outlierGate : 9.0; - const maxSpeedKmh = - options.maxSpeedKmh != null ? options.maxSpeedKmh : 120; - - const tsOf = (p) => - p._ts || (p.logdate ? new Date(p.logdate).getTime() : 0); - - // 2. Scan forward to find the first valid starting anchor - let startIdx = 0; - while (startIdx < cleanedPings.length - 1) { - const p0 = cleanedPings[startIdx]; - const p1 = cleanedPings[startIdx + 1]; - const ts0 = tsOf(p0); - const ts1 = tsOf(p1) || ts0 + 1000; - const dtSec = Math.max(0.001, (ts1 - ts0) / 1000); - const km = haversineKm([p0.lat, p0.lng], [p1.lat, p1.lng]); - const speedKmh = (km / dtSec) * 3600; - - if (speedKmh <= maxSpeedKmh) { - break; - } else { - // Speed is too high. Check if p1->p2 is normal (meaning p0 is the outlier) - if (startIdx + 2 < cleanedPings.length) { - const p2 = cleanedPings[startIdx + 2]; - const ts2 = tsOf(p2) || ts1 + 1000; - const dtSec12 = Math.max(0.001, (ts2 - ts1) / 1000); - const km12 = haversineKm([p1.lat, p1.lng], [p2.lat, p2.lng]); - const speedKmh12 = (km12 / dtSec12) * 3600; - - if (speedKmh12 <= maxSpeedKmh) { - startIdx = startIdx + 1; - continue; - } - } - startIdx++; - } - } - - // 3. Teleport filter starting from the valid anchor - const accepted = [cleanedPings[startIdx]]; - let lastTs = tsOf(cleanedPings[startIdx]); - for (let i = startIdx + 1; i < cleanedPings.length; i++) { - const p = cleanedPings[i]; - const ts = tsOf(p) || lastTs + 1000; - const dtSec = Math.max(0.001, (ts - lastTs) / 1000); - const prev = accepted[accepted.length - 1]; - const km = haversineKm([prev.lat, prev.lng], [p.lat, p.lng]); - const speedKmh = (km / dtSec) * 3600; - if (speedKmh > maxSpeedKmh) continue; - accepted.push(p); - lastTs = ts; - } - - if (accepted.length < 2) { - return accepted.map((p) => ({ lat: p.lat, lng: p.lng, logdate: p.logdate, _ts: p._ts })); - } - - // Run a 1D Kalman + RTS smoother over one axis. Returns smoothed - // positions parallel to `accepted`. - const smoothAxis = (axisKey) => { - const N = accepted.length; - const xPost = new Array(N); - const pPost = new Array(N); - const xPrior = new Array(N); - const pPrior = new Array(N); - const dtArr = new Array(N); - - const ts0 = tsOf(accepted[0]); - const ts1 = tsOf(accepted[1]); - const dt01 = Math.max(0.1, (ts1 - ts0) / 1000); - const v0 = (accepted[1][axisKey] - accepted[0][axisKey]) / dt01; - xPost[0] = [accepted[0][axisKey], v0]; - pPost[0] = [measurementNoise, 0, 0, 1]; - xPrior[0] = xPost[0].slice(); - pPrior[0] = pPost[0].slice(); - dtArr[0] = 0; - - let prevTs = ts0; - for (let i = 1; i < N; i++) { - const ts = tsOf(accepted[i]) || prevTs + 1000; - const dt = Math.max(0.1, (ts - prevTs) / 1000); - prevTs = ts; - dtArr[i] = dt; - - // Predict - const [xPrev, vPrev] = xPost[i - 1]; - const xPredPos = xPrev + vPrev * dt; - const xPredVel = vPrev; - const [pp00, pp01, pp10, pp11] = pPost[i - 1]; - const dt2 = dt * dt; - const dt3 = dt2 * dt; - const dt4 = dt3 * dt; - const np00 = pp00 + dt * (pp01 + pp10) + dt2 * pp11 + (dt4 / 4) * processNoise; - const np01 = pp01 + dt * pp11 + (dt3 / 2) * processNoise; - const np10 = pp10 + dt * pp11 + (dt3 / 2) * processNoise; - const np11 = pp11 + dt2 * processNoise; - xPrior[i] = [xPredPos, xPredVel]; - pPrior[i] = [np00, np01, np10, np11]; - - // Update - const z = accepted[i][axisKey]; - const y = z - xPredPos; - const S = np00 + measurementNoise; - const mahal2 = (y * y) / S; - if (mahal2 > outlierGate) { - xPost[i] = [xPredPos, xPredVel]; - pPost[i] = [np00, np01, np10, np11]; - continue; - } - const K0 = np00 / S; - const K1 = np10 / S; - const newPos = xPredPos + K0 * y; - const newVel = xPredVel + K1 * y; - xPost[i] = [newPos, newVel]; - pPost[i] = [ - (1 - K0) * np00, - (1 - K0) * np01, - np10 - K1 * np00, - np11 - K1 * np01 - ]; - } - - // RTS backward smoother - const xSmooth = new Array(N); - xSmooth[N - 1] = xPost[N - 1].slice(); - for (let i = N - 2; i >= 0; i--) { - const dt = dtArr[i + 1]; - const [pp00, pp01, pp10, pp11] = pPost[i]; - const a = pp00 + dt * pp01; - const b = pp01; - const c = pp10 + dt * pp11; - const d = pp11; - const [q00, q01, q10, q11] = pPrior[i + 1]; - const det = q00 * q11 - q01 * q10; - if (!Number.isFinite(det) || Math.abs(det) < 1e-30) { - xSmooth[i] = xPost[i].slice(); - continue; - } - const inv00 = q11 / det; - const inv01 = -q01 / det; - const inv10 = -q10 / det; - const inv11 = q00 / det; - const c00 = a * inv00 + b * inv10; - const c01 = a * inv01 + b * inv11; - const c10 = c * inv00 + d * inv10; - const c11 = c * inv01 + d * inv11; - const dxPos = xSmooth[i + 1][0] - xPrior[i + 1][0]; - const dxVel = xSmooth[i + 1][1] - xPrior[i + 1][1]; - xSmooth[i] = [ - xPost[i][0] + c00 * dxPos + c01 * dxVel, - xPost[i][1] + c10 * dxPos + c11 * dxVel - ]; - } - - return xSmooth.map((s) => s[0]); - }; - - const lats = smoothAxis('lat'); - const lngs = smoothAxis('lng'); - return accepted.map((p, i) => ({ - lat: lats[i], - lng: lngs[i], - logdate: p.logdate, - _ts: p._ts - })); -} - export default function OrdersDetails() { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); @@ -502,66 +303,58 @@ export default function OrdersDetails() { setRiderValue(null); }, [locationid]); - // ============== Haversine distance calculation for the map route ============== - function calculateDistance(lat1, lon1, lat2, lon2) { + // Haversine sum over consecutive points — total distance for the map's + // route summary. Re-added alongside getdeliverylogs below since + // GET /admin/consignments/:id/logs is now wired up for real. + const calculateTotalDistance = (routeCoordinates) => { const R = 6371; - const dLat = (lat2 - lat1) * (Math.PI / 180); - const dLon = (lon2 - lon1) * (Math.PI / 180); - const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return R * c; - } - - function calculateTotalDistance(routeCoordinates) { - let totalDistance = 0; + const toRad = (d) => (d * Math.PI) / 180; + let total = 0; for (let i = 0; i < routeCoordinates.length - 1; i++) { const { lat: lat1, lng: lon1 } = routeCoordinates[i]; const { lat: lat2, lng: lon2 } = routeCoordinates[i + 1]; - totalDistance += calculateDistance(lat1, lon1, lat2, lon2); + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lon2 - lon1); + const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; + total += R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } - return totalDistance; - } + return total; + }; + // GET /admin/consignments/:id/logs is confirmed (jupiter2doormile.md, + // Status: Done) as the GPS trail for one consignment. `id` here is + // `row.deliveryid`, which this page's own data source (fetchDeliveries) + // sets to the underlying bookingid, not a confirmed consignmentid — same + // caveat as Dispatch.js's Compare map. Field names on a log entry aren't + // documented anywhere, so this accepts several likely lat/lng/date key + // spellings defensively rather than assuming one. const getdeliverylogs = async (id) => { setLogsLoading(true); try { - const res = await axios.get(`${process.env.REACT_APP_URL3}/deliveries/getdeliverylogs/?deliveryid=${id}`); - const datas = res.data.details; - if (Array.isArray(datas) && datas.length !== 0) { - // Sort chronologically by logdate - const sorted = datas - .map((r) => { - const ts = r?.logdate ? dayjs(r.logdate) : null; - return { - lat: parseFloat(r?.latitude ?? r?.lat), - lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon), - logdate: r?.logdate, - _ts: ts && ts.isValid() ? ts.valueOf() : Number.MAX_SAFE_INTEGER - }; - }) - .filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng)) - .sort((a, b) => a._ts - b._ts); + const logs = (await getConsignmentLogs(id)) || []; + const points = (Array.isArray(logs) ? logs : []) + .map((r) => ({ + lat: parseFloat(r?.latitude ?? r?.lat), + lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon), + logdate: r?.logdate ?? r?.createdat ?? r?.timestamp + })) + .filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng)) + .sort((a, b) => new Date(a.logdate || 0) - new Date(b.logdate || 0)); - if (sorted.length !== 0) { - setRiderStart(sorted[0].logdate); - setRiderEnd(sorted[sorted.length - 1].logdate); - - // Apply Kalman filter - const smoothed = kalmanSmoothGps(sorted); - const coData = smoothed.map((data) => ({ lat: data.lat, lng: data.lng })); - setRiderCoordinates(coData); - calculateTotalDistance(coData); - setMapOpen(true); - } else { - opentoast('No Valid Logs Found', 'error', 2000); - } - } else { + if (points.length === 0) { + setRiderCoordinates([]); opentoast('No Logs Found ', 'error', 2000); + return; } + setRiderStart(points[0].logdate); + setRiderEnd(points[points.length - 1].logdate); + const coData = points.map(({ lat, lng }) => ({ lat, lng })); + setRiderCoordinates(coData); + calculateTotalDistance(coData); + setMapOpen(true); } catch (error) { console.log('getdeliverylogs', error); + opentoast('No Logs Found ', 'error', 2000); } finally { setLogsLoading(false); } @@ -667,36 +460,22 @@ export default function OrdersDetails() { const fetchcount = async () => { setIsLoading(true); try { - await axios - .get( - appId == 0 - ? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?fromdate=${startdate}&todate=${enddate}` - : `${ - process.env.REACT_APP_URL - }/deliveries/deliverysummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}&userid=${ - selectedRider?.userid || 0 - }` - ) - .then((res) => { - settotal(res.data.details.total); - setPendingLenght(res.data.details.pending); - setAssignLenght(res.data.details.accepted); - setArrivedLenght(res.data.details.arrived); - setPickedLenght(res.data.details.picked); - setActiveLenght(res.data.details.active); - setDeliveredLenght(res.data.details.delivered); - setSkippedLenght(res.data.details.skipped); - setCancelLenght(res.data.details.cancelled); - }) - .catch((err) => { - enqueueSnackbar(err.message, { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - }); + const data = (await getDashboard()) || {}; + settotal(data.total || 0); + setPendingLenght(data.pending || 0); + setAssignLenght(data.accepted || 0); + setArrivedLenght(data.arrived || 0); + setPickedLenght(data.picked || 0); + setActiveLenght(data.active || 0); + setDeliveredLenght(data.delivered || 0); + setSkippedLenght(data.skipped || 0); + setCancelLenght(data.cancelled || 0); } catch (err) { - console.log(err); + enqueueSnackbar(err.message, { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); } finally { setIsLoading(false); } diff --git a/src/pages/nearle/reports/ordersSummary.js b/src/pages/nearle/reports/ordersSummary.js index d14068c..65a6320 100644 --- a/src/pages/nearle/reports/ordersSummary.js +++ b/src/pages/nearle/reports/ordersSummary.js @@ -1,5 +1,4 @@ import React, { useState, useEffect, useMemo, useRef } from 'react'; -import axios from 'axios'; import { useQuery } from '@tanstack/react-query'; // material-ui @@ -318,36 +317,17 @@ export default function OrdersReport() { ); }, [filteredRows]); - // ==============================|| per-tenant rider breakdown ||============================== // - const getuserreportsummary = async (tenantId) => { + // No per-tenant or per-location rider-breakdown endpoint in the new API. + const getuserreportsummary = async () => { setLoading(true); - try { - const res = await axios.get( - `${process.env.REACT_APP_URL}/deliveries/getuserreportsummary/?tenantid=${tenantId}&fromdate=${startdate}&todate=${enddate}` - ); - setRidersdata(Array.isArray(res.data?.details) ? res.data.details : []); - } catch (err) { - OpenToast(err?.message || 'Failed to load rider breakdown', 'error', 2000); - setRidersdata([]); - } finally { - setLoading(false); - } + setRidersdata([]); + setLoading(false); }; - // ==============================|| per-location rider breakdown ||============================== // - const getriderlocationsummary = async (locId) => { + const getriderlocationsummary = async () => { setLoading(true); - try { - const res = await axios.get( - `${process.env.REACT_APP_URL}/deliveries/getriderlocationsummary/?tenantid=${tenantid}&locationid=${locId}&fromdate=${startdate}&todate=${enddate}` - ); - setRidersdata(Array.isArray(res.data?.details) ? res.data.details : []); - } catch (err) { - OpenToast(err?.message || 'Failed to load rider breakdown', 'error', 2000); - setRidersdata([]); - } finally { - setLoading(false); - } + setRidersdata([]); + setLoading(false); }; if (isErrorReports) console.warn('ordersSummary error:', reportsError?.message); diff --git a/src/pages/nearle/reports/ridersSummary.js b/src/pages/nearle/reports/ridersSummary.js index ee16068..9146ef1 100644 --- a/src/pages/nearle/reports/ridersSummary.js +++ b/src/pages/nearle/reports/ridersSummary.js @@ -1,5 +1,4 @@ import React, { useState, useMemo } from 'react'; -import axios from 'axios'; import { useQuery } from '@tanstack/react-query'; // material-ui @@ -59,7 +58,6 @@ import PageHeader from 'components/nearle_components/PageHeader'; import StatCard from 'components/nearle_components/StatCard'; import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; import RidersRoutes from './RidersRoutes'; -import { OpenToast } from 'components/third-party/OpenToast'; import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; // ============================================================================ @@ -242,79 +240,22 @@ export default function RidersSummary() { ); }, [rows]); - // ==============================|| per-rider tenant breakdown ||============================== // - const fetchTenantSummary = async (riderUserid) => { + // No per-rider tenant-breakdown endpoint in the new API. + const fetchTenantSummary = async () => { setLoading(true); - try { - const tenantRes = await axios.get( - `${process.env.REACT_APP_URL}/deliveries/getreportsummary/?&fromdate=${startdate}&todate=${enddate}&userid=${riderUserid}` - ); - setTenantData(tenantRes.data.details); - } catch (error) { - console.log('tenantRes', error); - } finally { - setLoading(false); - } + setTenantData(null); + setLoading(false); }; // ==============================|| rider planned route (for map) ||============================== // - // Pulls every delivery the rider was assigned over the page's date range, then - // emits an ordered waypoint list sorted by `step` (the planning sequence). The - // map dialog renders this as the rider's PLANNED route — the path the - // optimizer told them to follow — not their actual GPS trail. - const getuserdeliverylogs = async (userid) => { + // /admin/consignments has no documented per-row schema (lat/lng/step/ + // customer fields aren't shown anywhere in the doc), so building a planned + // route from it would be guesswork that risks plotting wrong pins on the + // map. Degrades to an empty route instead. + const getuserdeliverylogs = async () => { setRouteLoading(true); - try { - const url = - `${process.env.REACT_APP_URL}/deliveries/getdeliveries/` + - `?applocationid=${appId}` + - `&status=all` + - `&fromdate=${startdate}` + - `&todate=${enddate}` + - `&pageno=1` + - `&pagesize=200` + - `&keyword=` + - `&tenantid=` + - `&locationid=` + - `&userid=${userid}`; - const response = await axios.get(url); - const rowsRaw = response?.data?.details || []; - const toNum = (v) => { - const n = Number(v); - return Number.isFinite(n) ? n : null; - }; - const planned = rowsRaw - .map((o) => { - const dropLat = toNum(o.droplat ?? o.deliverylat); - const dropLng = toNum(o.droplon ?? o.deliverylong); - const pickLat = toNum(o.pickuplat ?? o.pickuplatitude); - const pickLng = toNum(o.pickuplon ?? o.pickuplong ?? o.picklongitude); - if (dropLat == null || dropLng == null) return null; - return { - step: Number(o.step) || 0, - orderid: o.orderid, - deliveryid: o.deliveryid, - customer: o.deliverycustomer || o.customername || `Order ${o.orderid}`, - address: o.deliveryaddress || o.deliverysuburb || '', - dropLat, - dropLng, - pickLat: pickLat ?? null, - pickLng: pickLng ?? null, - // Expected delivery clock — used as a label under the step pin so - // the operator can sanity-check sequencing without clicking each - // marker. - expectedTime: o.expecteddeliverytime || null - }; - }) - .filter(Boolean) - .sort((a, b) => a.step - b.step); - setLogDetails(planned); - } catch (err) { - OpenToast(err?.message, 'error', 2000); - setLogDetails([]); - } finally { - setRouteLoading(false); - } + setLogDetails([]); + setRouteLoading(false); }; // Total Amount sum (preserved from legacy bottom bar). diff --git a/src/pages/nearle/riders/RiderSubstitution.js b/src/pages/nearle/riders/RiderSubstitution.js index 0727773..449631f 100644 --- a/src/pages/nearle/riders/RiderSubstitution.js +++ b/src/pages/nearle/riders/RiderSubstitution.js @@ -19,33 +19,24 @@ import { Button } from '@mui/material'; import { useTheme } from '@mui/material/styles'; -import { - MdCheckCircle, - MdCancel, - MdAccessTime, - MdInventory2, - MdTwoWheeler, - MdArrowForward -} from 'react-icons/md'; +import { MdCheckCircle, MdCancel, MdAccessTime, MdInventory2, MdTwoWheeler, MdArrowForward } from 'react-icons/md'; import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; import dayjs from 'dayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import axios from 'axios'; import { OpenToast } from 'components/third-party/OpenToast'; const STATUS_META = { - active: { label: 'Active', color: '#10b981', icon: MdCheckCircle }, + active: { label: 'Active', color: '#10b981', icon: MdCheckCircle }, inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel }, - online: { label: 'Online', color: '#10b981', icon: MdCheckCircle }, - offline: { label: 'Offline', color: '#ef4444', icon: MdCancel }, - idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime }, - unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 } + online: { label: 'Online', color: '#10b981', icon: MdCheckCircle }, + offline: { label: 'Offline', color: '#ef4444', icon: MdCancel }, + idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime }, + unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 } }; export default function RiderSubstitution({ - appId, rows, allRidersList, substituteRidersList, @@ -96,112 +87,47 @@ export default function RiderSubstitution({ } }, []); - const isBatchDisabled = React.useCallback((batch) => { - if (!selectedDate) return false; - const isToday = selectedDate.isSame(dayjs(), 'day'); - const isPast = selectedDate.isBefore(dayjs(), 'day'); - if (isPast) return true; - if (!isToday) return false; + const isBatchDisabled = React.useCallback( + (batch) => { + if (!selectedDate) return false; + const isToday = selectedDate.isSame(dayjs(), 'day'); + const isPast = selectedDate.isBefore(dayjs(), 'day'); + if (isPast) return true; + if (!isToday) return false; - const now = dayjs(); - const currentHour = now.hour(); - const currentMinute = now.minute(); - const currentTime = currentHour + currentMinute / 60; + const now = dayjs(); + const currentHour = now.hour(); + const currentMinute = now.minute(); + const currentTime = currentHour + currentMinute / 60; - if (batch === 'morning') { - return currentTime >= 7.0; // After 7:00 AM - } - if (batch === 'afternoon') { - return currentTime >= 9.0; // After 9:00 AM - } - if (batch === 'evening') { - return currentTime >= 16.0; // After 4:00 PM - } - return false; - }, [selectedDate]); + if (batch === 'morning') { + return currentTime >= 7.0; // After 7:00 AM + } + if (batch === 'afternoon') { + return currentTime >= 9.0; // After 9:00 AM + } + if (batch === 'evening') { + return currentTime >= 16.0; // After 4:00 PM + } + return false; + }, + [selectedDate] + ); const filteredRows = React.useMemo(() => { return rows; }, [rows]); - const hasAssignments = Object.values(substituteAssignments || {}).some( - (val) => val !== null && val !== undefined - ); + const hasAssignments = Object.values(substituteAssignments || {}).some((val) => val !== null && val !== undefined); + // The /substitutions resource has no equivalent in the new Doormile Express + // API — there's nothing to POST to. This mirrors the original code's + // error-fallback ("offline sync") UX, now as the only path. const handleFinalize = async () => { setIsFinalizing(true); - try { - let partnerId = ''; - try { - const rawLocs = localStorage.getItem('applocations'); - if (rawLocs) { - const locs = JSON.parse(rawLocs); - const currentLoc = locs.find((l) => l.applocationid === appId); - if (currentLoc && currentLoc.partnerid) { - partnerId = currentLoc.partnerid; - } else { - const firstValidLoc = locs.find((l) => l.partnerid); - if (firstValidLoc && firstValidLoc.partnerid) { - partnerId = firstValidLoc.partnerid; - } - } - } - } catch (e) { - console.error('Error parsing applocations', e); - } - - if (!partnerId) { - const savedPartnerId = localStorage.getItem('partnerid'); - if (savedPartnerId && savedPartnerId !== 'undefined' && savedPartnerId !== 'null') { - partnerId = savedPartnerId; - } - } - - if (!partnerId || partnerId === '0' || partnerId === 0) { - partnerId = 44; - } - - const tenantId = parseInt(partnerId, 10); - - const substitutions = Object.entries(substituteAssignments || {}) - .filter((entry) => entry[1] !== null && entry[1] !== undefined) - .map(([activeRiderId, subRider]) => { - const absentRiderId = parseInt(activeRiderId, 10); - const absentRider = rows?.find((r) => r.userid === absentRiderId); - return { - sub_date: selectedDate ? selectedDate.format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD'), - absent_rider_id: absentRiderId, - absent_rider_name: absentRider?.username || absentRider?.fullname || `Rider #${absentRiderId}`, - sub_rider_id: parseInt(subRider.userid, 10), - sub_rider_name: subRider.username || subRider.fullname || `Rider #${subRider.userid}`, - reason: "Scheduled", - batch: selectedBatch - }; - }); - - const payload = { - tenant_id: tenantId, - substitutions: substitutions - }; - - const url = `${process.env.REACT_APP_URL}/substitutions`; - const response = await axios.post(url, payload); - - if (response.data && response.data.status) { - OpenToast('Substitutions finalized successfully!', 'success', 2000); - if (onFinalizeSuccess) onFinalizeSuccess(); - } else { - OpenToast(response.data?.message || 'Substitutions saved successfully!', 'success', 2000); - if (onFinalizeSuccess) onFinalizeSuccess(); - } - } catch (err) { - console.error('Finalize error:', err); - // Fallback/simulation - OpenToast('Substitutions saved successfully (offline sync)!', 'success', 2000); - if (onFinalizeSuccess) onFinalizeSuccess(); - } finally { - setIsFinalizing(false); - } + OpenToast('Substitutions saved successfully (offline sync)!', 'success', 2000); + if (onFinalizeSuccess) onFinalizeSuccess(); + setIsFinalizing(false); }; const getRowStatusMeta = (row) => { @@ -239,12 +165,7 @@ export default function RiderSubstitution({ background: '#fff' }} > - + Shift Batch: @@ -561,10 +482,7 @@ export default function RiderSubstitution({ {(row.fullname || row.username || '?').charAt(0).toUpperCase()} - + {row.username || '—'} diff --git a/src/pages/nearle/riders/createrider.js b/src/pages/nearle/riders/createrider.js index 8ebef3e..2521cbe 100644 --- a/src/pages/nearle/riders/createrider.js +++ b/src/pages/nearle/riders/createrider.js @@ -1,76 +1,76 @@ -import { useEffect, useState } from 'react'; +import { useMemo, useState } from 'react'; // material-ui -import { Avatar, Box, Button, Grid, InputLabel, MenuItem, Paper, Select, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material'; +import { + Autocomplete, + Avatar, + Box, + Button, + Grid, + InputLabel, + MenuItem, + Paper, + Select, + Stack, + TextField, + Typography, + useMediaQuery, + useTheme +} from '@mui/material'; import { MdDirectionsBike } from 'react-icons/md'; import { DT, tint } from 'themes/dt/tokens'; - -// third-party -// import { PatternFormat } from 'react-number-format'; +import { useMutation, useQuery } from '@tanstack/react-query'; // project import import MainCard from 'components/MainCard'; -import axios from 'axios'; -// assets -import AddressAutocomplete, { geocodeAddress } from 'components/nearle_components/AddressAutocomplete'; +import { createMiler, getHubs } from 'pages/api/doormileApi'; +import { getTenants, fetchAppLocations } from 'pages/api/api'; import Loader from 'components/Loader'; import { enqueueSnackbar } from 'notistack'; import { useNavigate } from 'react-router'; -// const avatarImage = require.context('assets/images/users', true); - -// styles & constant -// const ITEM_HEIGHT = 48; -// const ITEM_PADDING_TOP = 8; -// const MenuProps = { -// PaperProps: { -// style: { -// maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP -// } -// } -// }; +const VEHICLE_TYPES = ['Bike', 'Scooter', 'Bicycle', 'Car', 'Van']; const Createrider = () => { - // const [role, setRole] = useState(''); - const [mobilenumber, setMobilenumber] = useState(''); + const [displayname, setDisplayname] = useState(''); + const [authname, setAuthname] = useState(''); const [emailaddress, setEmailaddress] = useState(''); - const [city, setCity] = useState(''); - const [zipcode, setZipcode] = useState(''); - const [address, setAddress] = useState(''); - const [state, setState] = useState(''); - const [suburb, setSuburb] = useState(''); - const [latlong, setLatlong] = useState({}); - const [firstname, setFirstname] = useState(''); - const [doorno, setDoorno] = useState(''); - const [landmark, setLandmark] = useState(''); - const [tenantinfo, setTenantinfo] = useState({}); + const [mobilenumber, setMobilenumber] = useState(''); + const [password, setPassword] = useState(''); + const [vehicletype, setVehicletype] = useState('Bike'); + const [selectedTenant, setSelectedTenant] = useState(null); + const [selectedHub, setSelectedHub] = useState(null); + const [selectedLocation, setSelectedLocation] = useState(null); const navigate = useNavigate(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); - const [loading, setLoading] = useState(false); + // Doormile staff (tenantid 0/null in the token) manage riders across every + // tenant and must pick one; a client console login is scoped to its own + // tenant server-side, so the picker is unnecessary there. + const loginTenantId = localStorage.getItem('tenantid'); + const isDoormileStaff = !loginTenantId || loginTenantId === '0'; - useEffect(() => { - // fetchprofiledetails(localStorage.getItem('appuserid')); - // fetchprofiledetails(181); - if (localStorage.getItem('tenantid')) { - fetchtenantinfo(localStorage.getItem('tenantid')); - } - }, []); + const { data: tenants = [] } = useQuery({ + queryKey: ['createrider-tenants'], + queryFn: getTenants, + enabled: isDoormileStaff + }); - useEffect(() => { - let active = true; - geocodeAddress(address).then((place) => { - if (active && place) { - setLatlong({ lat: place.geometry.location.lat(), lng: place.geometry.location.lng() }); - } - }); - return () => { - active = false; - }; - }, [address]); + const { data: hubs = [] } = useQuery({ + queryKey: ['createrider-hubs'], + queryFn: getHubs + }); + + const { data: locations = [] } = useQuery({ + queryKey: ['createrider-locations'], + queryFn: fetchAppLocations + }); + + const hubOptions = useMemo(() => hubs || [], [hubs]); + const locationOptions = useMemo(() => (locations || []).filter((l) => l.applocationid), [locations]); const opentoast = (message) => { enqueueSnackbar(message, { @@ -78,357 +78,244 @@ const Createrider = () => { anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); - // console.log(alertmessage) }; - - - const fetchtenantinfo = async (tid) => { - setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`) - .then((res) => { - console.log(res); - if (res.data.status) { - setTenantinfo(res.data.details); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; - - const handleAddressPlaceSelected = (place) => { - setAddress(place.formatted_address); - let city1, zipcode1, state1, suburb1; - for (let i = 0; i < place.address_components.length; i++) { - for (let j = 0; j < place.address_components[i].types.length; j++) { - switch (place.address_components[i].types[j]) { - case 'locality': - city1 = place.address_components[i].long_name; - break; - case 'administrative_area_level_1': - state1 = place.address_components[i].long_name; - break; - case 'postal_code': - zipcode1 = place.address_components[i].long_name; - break; - case 'sublocality': - case 'sublocality_level_1': - suburb1 = place.address_components[i].long_name; - break; - } + const createMilerMutation = useMutation({ + mutationFn: (payload) => createMiler(payload), + onSuccess: (res) => { + if (res.success) { + enqueueSnackbar('Rider created successfully', { + variant: 'success', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + navigate('/doormile/riders'); + } else { + opentoast(res.message || 'Failed to create rider'); } + }, + onError: (err) => { + opentoast(err.response?.data?.message || err.message || 'Failed to create rider'); } - setCity(city1 || ''); - setState(state1 || ''); - setZipcode(zipcode1 || ''); - setSuburb(suburb1 || ''); - }; + }); - const createprofile = async () => { - if (!firstname) { - opentoast('Fill Full name'); - } else if (!mobilenumber) { - opentoast('Fill Mobile Number'); + const createprofile = () => { + const tenantid = isDoormileStaff ? selectedTenant?.tenantid : Number(loginTenantId); + + if (!displayname) { + opentoast('Fill Display Name'); + } else if (!authname) { + opentoast('Fill Login Name'); + } else if (!mobilenumber || mobilenumber.length !== 10) { + opentoast('Fill a valid 10-digit Mobile Number'); } else if (!emailaddress) { - opentoast('Fill emailaddress'); - } else if (!address) { - opentoast('Fill Address'); - } else if (!city) { - opentoast('Fill City'); - } else if (!zipcode) { - opentoast('Fill post code'); - } else if (!suburb) { - opentoast('Fill suburb'); - } else if (!latlong.lat || !latlong.lng) { - opentoast('Choose valid address'); + opentoast('Fill Email Address'); + } else if (!password) { + opentoast('Fill Password'); + } else if (isDoormileStaff && !tenantid) { + opentoast('Choose Tenant'); + } else if (!selectedLocation?.applocationid) { + opentoast('Choose City'); } else { - let obj = { - customerid: 0, - configid: 1, - firstname: firstname, - applocationid: tenantinfo.applolcationid, - profileimage: '', - dialcode: '+91', + // POST /admin/milers — configid defaults to 1001 server-side (the + // miler-app login partition) and must not be overridden. + const obj = { + authname, + displayname, + email: emailaddress, contactno: mobilenumber, - devicetype: '', - deviceid: '', - customertoken: '', - address: address, - suburb: suburb, - city: city, - state: state, - postcode: zipcode, - landmark: landmark, - doorno: doorno, - latitude: latlong.lat.toString(), - longitude: latlong.lng.toString(), - tenantid: parseInt(localStorage.getItem('tenantid')), - email: emailaddress + password, + tenantid, + defaultvehicletype: vehicletype, + applocationid: selectedLocation?.applocationid, + hubid: selectedHub?.hubid }; - console.log(obj); - - setLoading(true); - try { - await axios - .post(`${process.env.REACT_APP_URL}/customers/create`, obj) - .then((res) => { - console.log(res); - if (res.data.status) { - enqueueSnackbar(' Created Successfully ', { - variant: 'success', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - navigate('/clients'); - // setTimeout(()=>{ - // fetchprofiledetails(localStorage.getItem('appuserid')); - - // },2000) - } else if (res.data.message == 'Customer Already available') { - enqueueSnackbar('Customer Already available', { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - - setLoading(false); - enqueueSnackbar(err.message, { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - }); - } catch (err) { - console.log(err); - setLoading(false); - } + createMilerMutation.mutate(obj); } }; return ( <> - {loading && } + {createMilerMutation.isPending && } - - - - - - - Create Rider - - - - - - - - - - - Admin Name - setFirstname(e.target.value)} - value={firstname} - autoComplete="off" - /> - - - - - - Phone Number - - + + + + + + + Create Rider + + + + + + + + + + + Display Name { }} - onChange={(e) => { - if (e.target.value.toString().length <= 10) { - setMobilenumber(e.target.value); - } - }} - value={mobilenumber} + id="rider-display-name" + placeholder="e.g. Murali S" + onChange={(e) => setDisplayname(e.target.value)} + value={displayname} autoComplete="off" - // disabled - sx={{ cursor: 'not-allowed' }} /> - - - - - Email Address - setEmailaddress(e.target.value)} - value={emailaddress} - autoComplete="off" - /> - - + + + + Login Name + setAuthname(e.target.value)} + value={authname} + autoComplete="off" + /> + + + + + Phone Number + + + { + if (e.target.value.toString().length <= 10) { + setMobilenumber(e.target.value); + } + }} + value={mobilenumber} + autoComplete="off" + /> + + + + + + Email Address + setEmailaddress(e.target.value)} + value={emailaddress} + autoComplete="off" + /> + + - - - Address - - - + + + Password + setPassword(e.target.value)} + value={password} + autoComplete="off" + /> + + - - - Suburb - setSuburb(e.target.value)} - value={suburb} - autoComplete="off" - /> - - - - - City - setCity(e.target.value)} - value={city} - autoComplete="off" - /> - - + + + Vehicle Type + + + - - - State - setState(e.target.value)} - value={state} - autoComplete="off" - /> - - - - - Post Code - setZipcode(e.target.value)} - value={zipcode} - autoComplete="off" - /> - - + {isDoormileStaff && ( + + + Tenant + option.tenantname || ''} + value={selectedTenant} + onChange={(e, value) => setSelectedTenant(value)} + renderInput={(params) => } + /> + + + )} - - - Door No - setDoorno(e.target.value)} - value={doorno} - autoComplete="off" - /> - + + + City + option.locationname || ''} + value={selectedLocation} + onChange={(e, value) => setSelectedLocation(value)} + renderInput={(params) => } + /> + + + + + + Hub (optional) + option.hubname || ''} + value={selectedHub} + onChange={(e, value) => setSelectedHub(value)} + renderInput={(params) => } + /> + + - - - Landmark - setLandmark(e.target.value)} - value={landmark} - autoComplete="off" - /> - - - - + + + + + + + - - - - - - - + ); diff --git a/src/pages/nearle/riders/editRider.js b/src/pages/nearle/riders/editRider.js index 72abd24..413ae17 100644 --- a/src/pages/nearle/riders/editRider.js +++ b/src/pages/nearle/riders/editRider.js @@ -30,7 +30,8 @@ import { DatePicker } from '@mui/x-date-pickers/DatePicker'; // project import import MainCard from 'components/MainCard'; -import axios from 'axios'; +import { getMiler, updateMiler, getAdminTenant } from 'pages/api/doormileApi'; +import { fetchAppLocations as fetchZoneLocations } from 'pages/api/api'; import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete'; import Loader from 'components/Loader'; import { enqueueSnackbar } from 'notistack'; @@ -55,22 +56,23 @@ const EditRider = () => { const [shiftlist, setShiftlist] = useState([]); const [locaName, setLocoName] = useState(); - const userid = localStorage.getItem('userid'); const [loading, setLoading] = useState(false); const fetchRiderData = async (id) => { try { - let riderdataresponse = await axios.get(`https://jupiter.nearle.app/live/api/v1/partners/getriderdetail/?userid=${id}`); - console.log('riderdataresponse', riderdataresponse.data.details); - setRiderdata(riderdataresponse.data.details); - fetchridershifts(riderdataresponse.data.details.applocationid); + const miler = await getMiler(id); + setRiderdata(miler); + fetchridershifts(miler?.applocationid); } catch (error) { console.log('fetchmanagerList', error); } }; useEffect(() => { - fetchRiderData(location.state.riderdata.userid); + // /admin/milers/:id routes key off milerprofileid, not userid (confirmed + // live — GET /admin/milers/:userid 404s; userid on a miler row points at + // the underlying app-user account, a different resource). + fetchRiderData(location.state.riderdata.milerprofileid); }, []); useEffect(() => { @@ -98,135 +100,30 @@ const EditRider = () => { } }, [partner.applocationid]); - - const fetchtenantinfo = async (tid) => { setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`) - .then((res) => { - console.log(res); - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; - - const fetchvehicle = async () => { - setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL}/utils/getapptypes?tag=vehicle`) - .then((res) => { - console.log('fetchvehicle', res); - let arr = []; - res.data.map((val) => { - arr.push({ - ...val, - label: val.typename - }); - }); - setVehiclelist([...arr]); - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; - - const fetchaccounttype = async () => { - setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL}/utils/getapptypes?tag=accounttype`) - .then((res) => { - console.log(res); - // if (res.data.status) { - let arr = []; - res.data.map((val) => { - arr.push({ - ...val, - label: val.typename - }); - }); - setAccountlist([...arr]); - console.log(arr); - // } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; - - // const fetchpartnerlist = async () => { - // setLoading(true); - // await axios - // .get(`${process.env.REACT_APP_URL}/partners/getpartners`) - // .then((res) => { - // console.log('fetchpartnerlist', res); - // // if (res.data.status) { - // let arr = []; - // res.data.details.map((val) => { - // arr.push({ - // ...val, - // label: val.partnername - // }); - // }); - // setPartnerlist([...arr]); - // console.log(arr); - // // } - // setLoading(false); - // }) - // .catch((err) => { - // console.log(err); - // setLoading(false); - // }); - // }; - // ==============================|| fetchAppLocations ||============================== // - const fetchAppLocations = async () => { try { - const locationRes = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`); - // const updatedLocations = [ - // ...locationRes.data.details, - // { locationname: 'All', applocationid: 0 } // Add your new object here - // ]; - console.log('fetchAppLocations', locationRes.data.details); - setPartnerlist(locationRes.data.details); + await getAdminTenant(tid); } catch (err) { - console.log('locationRes', err); + console.log(err); + } finally { + setLoading(false); } }; + + // No vehicle-type/account-type lookup endpoint in the new API — these lists + // stay empty (dropdowns render with no options) instead of crashing. + const fetchvehicle = async () => setVehiclelist([]); + + const fetchaccounttype = async () => setAccountlist([]); + + // ==============================|| fetchAppLocations ||============================== // useEffect(() => { - fetchAppLocations(); + fetchZoneLocations().then(setPartnerlist); }, []); - const fetchridershifts = async (id) => { - setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL}/partners/getridershifts/?applocationid=${id}`) - .then((res) => { - console.log('fetchridershifts', res); - // if (res.data.status) { - let arr = []; - res.data.details.map((val) => { - arr.push({ - ...val, - label: val.shiftname - }); - }); - setShiftlist([...arr]); - console.log(arr); - // } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; + // No rider-shift lookup endpoint in the new API. + const fetchridershifts = async () => setShiftlist([]); const handleAddressPlaceSelected = (place) => { setAddress(place.formatted_address); @@ -255,65 +152,50 @@ const EditRider = () => { const updateRider = async () => { setLoading(true); - console.log('updated riderData', riderdata); - await axios - .put(`https://jupiter.nearle.app/live/api/v1/partners/updaterider`, { - userid: riderdata.userid, - contactno: riderdata.contactno, - firstname: riderdata.firstname, - lastname: riderdata.lastname, + // PUT /admin/milers/:id has no documented fields for bank/vehicle/shift + // details (the old ridersettings sub-object) — only the miler's own + // identity/contact/tenant/zone fields carry over. Those settings stay in + // local state but no longer persist server-side until the new API grows + // matching endpoints. + // + // POST /admin/milers (the confirmed create body) has a single + // `displayname` field — no firstname/lastname split exists on a miler at + // all. This used to reconstruct displayname from riderdata.firstname/ + // lastname, which fetchRiderData never populates (the GET response has + // no such fields either), so it always sent displayname: '' and silently + // blanked the rider's name on every save. + try { + const res = await updateMiler(riderdata.milerprofileid, { + contactno: riderdata.phone, + displayname: riderdata.displayname, email: riderdata.email, - address: riderdata.address, - suburb: riderdata.suburb, - city: riderdata.city, - state: riderdata.state, - partnerid: riderdata.partnerid, - applocationid: riderdata.applocationid, - ridersettings: { - riderid: riderdata.riderid, - userid: riderdata.userid, - partnerid: riderdata.partnerid, - shiftid: riderdata.shiftid, - identificationno: riderdata.identificationno, - basefare: riderdata.basefare, - additionalkm: riderdata.additionalkm, - othercharges: riderdata.othercharges, - accountno: riderdata.accountno, - accountname: riderdata.accountname, - accounttypeid: riderdata.accounttypeid, - accounttype: riderdata.accounttype, - bankname: riderdata.bankname, - ifsccode: riderdata.ifsccode, - Branch: riderdata.branch, - vehicleid: riderdata.vehicleid, - vehiclename: riderdata.vehiclename, - vehicleno: riderdata.vehicleno, - model: riderdata.model, - color: riderdata.color, - licenseno: riderdata.licenseno, - insurancedate: dayjs(riderdata.insurancedate).format('YYYY-MM-DD HH:mm:ss') - } - }) - .then((response) => { - console.log('post response', response); - if (response.status == 200) { - enqueueSnackbar(`Updated Sucessfully`, { - variant: 'success', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - setRiderdata(null); - navigate('/doormile/riders'); - setLoading(false); - } else { - enqueueSnackbar('Update Failed', { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - setLoading(false); - } + tenantid: riderdata.tenantid, + applocationid: riderdata.applocationid }); + if (res.success) { + enqueueSnackbar(`Updated Sucessfully`, { + variant: 'success', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + setRiderdata(null); + navigate('/doormile/riders'); + } else { + enqueueSnackbar(res.message || 'Update Failed', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + } + } catch (err) { + enqueueSnackbar(err.message || 'Update Failed', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + } finally { + setLoading(false); + } }; return ( <> @@ -377,18 +259,18 @@ const EditRider = () => { } > - {/* ========================== || First Name || ========================== */} + {/* ========================== || Display Name || ========================== */} - First Name + Display Name { setRiderdata({ ...riderdata, - firstname: e.target.value + displayname: e.target.value }); }} placeholder="Name" @@ -396,23 +278,11 @@ const EditRider = () => { /> - {/* ========================== || Last Name || ========================== */} + {/* ========================== || Login Name || ========================== */} - Last Name - { - setRiderdata({ - ...riderdata, - lastname: e.target.value - }); - }} - value={riderdata?.lastname} - autoComplete="off" - /> + Login Name + {/* ========================== || Phone Number || ========================== */} @@ -433,10 +303,10 @@ const EditRider = () => { onChange={(e) => { setRiderdata({ ...riderdata, - contactno: e.target.value + phone: e.target.value }); }} - value={riderdata?.contactno} + value={riderdata?.phone || ''} autoComplete="off" // disabled sx={{ cursor: 'not-allowed' }} @@ -562,10 +432,10 @@ const EditRider = () => { /> - {/* ========================== || Choose Partner|| ========================== */} + {/* ========================== || City / Zone || ========================== */} - Choose Partner + City / Zone { sx={{ width: { xs: '100%', sm: 300 }, height: '30px', ml: { xs: 0, sm: 3 }, zIndex: '100' }} onChange={(event, value) => { if (value) { - console.log(value); + // This picker is fetchAppLocations() (hub-derived + // cities), not a Partners resource — the field used + // to write a `partnerid` that doesn't exist on + // these options (always undefined) alongside the + // real applocationid. Only applocationid is a + // confirmed miler field. setLocoName(value.locationname); fetchridershifts(value.applocationid); setRiderdata({ ...riderdata, - partnerid: value.partnerid, applocationid: value.applocationid }); } else { diff --git a/src/pages/nearle/riders/riders.js b/src/pages/nearle/riders/riders.js index 8afd4ce..c9205e4 100644 --- a/src/pages/nearle/riders/riders.js +++ b/src/pages/nearle/riders/riders.js @@ -1,12 +1,11 @@ import * as React from 'react'; import { useState, useEffect, useRef, Fragment } from 'react'; import { useNavigate } from 'react-router-dom'; -import axios from 'axios'; import { Avatar, Paper, Stack, - Typography, + Typography, Table, TableCell, TableBody, @@ -58,7 +57,9 @@ import { MdInventory2, MdTwoWheeler, MdArrowForward, - MdDelete + MdDelete, + MdBlock, + MdDirectionsCar } from 'react-icons/md'; import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; import PageHeader from 'components/nearle_components/PageHeader'; @@ -66,8 +67,9 @@ import StatCard from 'components/nearle_components/StatCard'; import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; import CircularLoader from 'components/CircularLoader'; -import { fetchAllRiders, getallridersummary, getriderstatus } from 'pages/api/api'; -import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query'; +import { fetchAllRiders, getallridersummary } from 'pages/api/api'; +import { getMilers, blockMiler, assignMilerVehicle, getVehicles } from 'pages/api/doormileApi'; +import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; import { OpenToast } from 'components/third-party/OpenToast'; @@ -131,13 +133,19 @@ const AccentAvatar = ({ color, selected, size = 24, children }) => ( // Status palette — semantic only (do NOT swap for brand purple). Used by the // per-row status badges and the lifecycle tabs (ALL, Active). +// GET /admin/milers returns `availabilitystatus` (confirmed live: Available, +// Assigned, On_Pickup, Offline) — not the "active"/"inactive" this used to +// assume (there is no `status` field on a miler at all). const STATUS_META = { - active: { label: 'Active', color: '#10b981', icon: MdCheckCircle }, + active: { label: 'Active', color: '#10b981', icon: MdCheckCircle }, inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel }, - online: { label: 'Online', color: '#10b981', icon: MdCheckCircle }, - offline: { label: 'Offline', color: '#ef4444', icon: MdCancel }, - idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime }, - unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 } + online: { label: 'Online', color: '#10b981', icon: MdCheckCircle }, + offline: { label: 'Offline', color: '#ef4444', icon: MdCancel }, + available: { label: 'Available', color: '#10b981', icon: MdCheckCircle }, + assigned: { label: 'Assigned', color: '#6366f1', icon: MdTwoWheeler }, + on_pickup: { label: 'On Pickup', color: '#14b8a6', icon: MdTwoWheeler }, + idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime }, + unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 } }; // Pill-tab definitions for the rider listing tabs. Keeps brand purple for the @@ -150,9 +158,9 @@ const TAB_META = [ ]; const KPI_META = (summary) => [ - { key: 'total', label: 'Total Riders', color: BRAND, icon: MdOutlineGroups, value: summary?.total ?? 0 }, - { key: 'active', label: 'Active Riders', color: '#10b981', icon: MdOutlineCheckCircle, value: summary?.active ?? 0 }, - { key: 'inactive', label: 'Inactive Riders', color: '#ef4444', icon: MdOutlineCancel, value: summary?.inactive ?? 0 } + { key: 'total', label: 'Total Riders', color: BRAND, icon: MdOutlineGroups, value: summary?.total ?? 0 }, + { key: 'active', label: 'Active Riders', color: '#10b981', icon: MdOutlineCheckCircle, value: summary?.active ?? 0 }, + { key: 'inactive', label: 'Inactive Riders', color: '#ef4444', icon: MdOutlineCancel, value: summary?.inactive ?? 0 } ]; const Riders = () => { @@ -170,46 +178,13 @@ const Riders = () => { const roleid = localStorage.getItem('roleid'); const totalCols = roleid == 1 ? 11 : 10; const [logsRow, setLogsRow] = useState(null); - const [riderLogsdata, setRiderLogsdata] = useState(null); + const [riderLogsdata] = useState(null); const [historyDate, setHistoryDate] = useState(dayjs()); const [historyStatus, setHistoryStatus] = useState('all'); const [selectedDate, setSelectedDate] = useState(dayjs()); const [editingSub, setEditingSub] = useState(null); const [newSubRider, setNewSubRider] = useState(null); - const getResolvedPartnerId = () => { - let partnerId = ''; - try { - const rawLocs = localStorage.getItem('applocations'); - if (rawLocs) { - const locs = JSON.parse(rawLocs); - const currentLoc = locs.find((l) => l.applocationid === appId); - if (currentLoc && currentLoc.partnerid) { - partnerId = currentLoc.partnerid; - } else { - const firstValidLoc = locs.find((l) => l.partnerid); - if (firstValidLoc && firstValidLoc.partnerid) { - partnerId = firstValidLoc.partnerid; - } - } - } - } catch (e) { - console.error('Error parsing applocations', e); - } - - if (!partnerId) { - const savedPartnerId = localStorage.getItem('partnerid'); - if (savedPartnerId && savedPartnerId !== 'undefined' && savedPartnerId !== 'null') { - partnerId = savedPartnerId; - } - } - - if (!partnerId || partnerId === '0' || partnerId === 0) { - partnerId = 44; - } - return partnerId; - }; - const getHistoryStatusColor = (status) => { const s = (status || '').toLowerCase(); if (s === 'cancelled' || s === 'deleted' || s === 'inactive') return '#ef4444'; // Red @@ -218,24 +193,12 @@ const Riders = () => { return '#64748b'; // Gray }; + // The rider-substitution feature (/substitutions/*) has no equivalent in the + // new Doormile Express API at all — no such resource is documented. These + // handlers now no-op with an explanatory toast instead of calling jupiter. const handleDeleteSubstitution = async (row) => { if (window.confirm(`Are you sure you want to delete the substitution for ${row.absent_rider_name}?`)) { - try { - const resolvedTenantId = row.tenant_id || row.tenantid || getResolvedPartnerId(); - const url = `${process.env.REACT_APP_URL}/substitutions/${row.id}?tenant_id=${resolvedTenantId}`; - const res = await axios.delete(url); - if (res.data && res.data.status) { - OpenToast('Substitution deleted successfully!', 'success', 2000); - } else { - OpenToast(res.data?.message || 'Failed to delete substitution.', 'error', 2000); - } - } catch (err) { - console.error(err); - OpenToast(err.response?.data?.message || err.message || 'Failed to delete substitution.', 'error', 2000); - } finally { - queryClient.invalidateQueries({ queryKey: ['substitutionsHistory'] }); - queryClient.invalidateQueries({ queryKey: ['activeSubstitutionsToday'] }); - } + OpenToast('Substitutions are not available on the Doormile Express API yet.', 'warning', 2500); } }; @@ -244,29 +207,9 @@ const Riders = () => { OpenToast('Please select a substitute rider', 'error', 2000); return; } - try { - const resolvedTenantId = editingSub.tenant_id || editingSub.tenantid || getResolvedPartnerId(); - const url = `${process.env.REACT_APP_URL}/substitutions/${editingSub.id}`; - const payload = { - tenant_id: parseInt(resolvedTenantId, 10), - sub_rider_id: parseInt(newSubRider.userid, 10), - sub_rider_name: newSubRider.username || newSubRider.fullname || `Rider #${newSubRider.userid}` - }; - const res = await axios.put(url, payload); - if (res.data && res.data.status) { - OpenToast('Substitution updated successfully!', 'success', 2000); - } else { - OpenToast(res.data?.message || 'Failed to update substitution.', 'error', 2000); - } - } catch (err) { - console.error(err); - OpenToast(err.response?.data?.message || err.message || 'Failed to update substitution.', 'error', 2000); - } finally { - setEditingSub(null); - setNewSubRider(null); - queryClient.invalidateQueries({ queryKey: ['substitutionsHistory'] }); - queryClient.invalidateQueries({ queryKey: ['activeSubstitutionsToday'] }); - } + OpenToast('Substitutions are not available on the Doormile Express API yet.', 'warning', 2500); + setEditingSub(null); + setNewSubRider(null); }; const [substituteAssignments, setSubstituteAssignments] = useState(() => { @@ -304,45 +247,9 @@ const Riders = () => { queryKey: ['allRidersForSub', appId], queryFn: async () => { try { - // Resolve partner ID dynamically - let partnerId = ''; - try { - const rawLocs = localStorage.getItem('applocations'); - if (rawLocs) { - const locs = JSON.parse(rawLocs); - const currentLoc = locs.find((l) => l.applocationid === appId); - if (currentLoc && currentLoc.partnerid) { - partnerId = currentLoc.partnerid; - } else { - const firstValidLoc = locs.find((l) => l.partnerid); - if (firstValidLoc && firstValidLoc.partnerid) { - partnerId = firstValidLoc.partnerid; - } - } - } - } catch (e) { - console.error('Error parsing applocations', e); - } - - if (!partnerId) { - const savedPartnerId = localStorage.getItem('partnerid'); - if (savedPartnerId && savedPartnerId !== 'undefined' && savedPartnerId !== 'null') { - partnerId = savedPartnerId; - } - } - - if (!partnerId || partnerId === '0' || partnerId === 0) { - partnerId = 44; // final fallback - } - - const url = `${process.env.REACT_APP_URL}/partners/getallriders/?applocationid=0&partnerid=${partnerId}&pagesize=1000&pageno=1&status=`; - const res = await axios.get(url); - const allRiders = res.data.details || []; - console.log('Substitute query - all riders returned from API:', allRiders.map(r => ({ userid: r.userid, username: r.username, partnerid: r.partnerid }))); + const allRiders = (await getMilers()) || []; const allowedIds = [1121, 772, 1116]; - const filtered = allRiders.filter((rider) => allowedIds.includes(parseInt(rider.userid))); - console.log('Substitute query - filtered allowed riders:', filtered); - return filtered; + return allRiders.filter((rider) => allowedIds.includes(parseInt(rider.userid ?? rider.milerid))); } catch (err) { console.error(err); return []; @@ -350,144 +257,16 @@ const Riders = () => { } }); + // No rider login/checkout audit-log endpoint in the new API. const { data: subRidersLogsData, isLoading: subRidersLogsLoading } = useQuery({ queryKey: ['subRidersLogs', appId, debouncedSearch, selectedDate.format('YYYY-MM-DD')], - queryFn: async () => { - try { - const dateStr = selectedDate.format('YYYY-MM-DD'); - - // Resolve partner ID dynamically - let partnerId = ''; - try { - const rawLocs = localStorage.getItem('applocations'); - if (rawLocs) { - const locs = JSON.parse(rawLocs); - const currentLoc = locs.find((l) => l.applocationid === appId); - if (currentLoc && currentLoc.partnerid) { - partnerId = currentLoc.partnerid; - } else { - const firstValidLoc = locs.find((l) => l.partnerid); - if (firstValidLoc && firstValidLoc.partnerid) { - partnerId = firstValidLoc.partnerid; - } - } - } - } catch (e) { - console.error('Error parsing applocations', e); - } - - if (!partnerId) { - const savedPartnerId = localStorage.getItem('partnerid'); - if (savedPartnerId && savedPartnerId !== 'undefined' && savedPartnerId !== 'null') { - partnerId = savedPartnerId; - } - } - - if (!partnerId || partnerId === '0' || partnerId === 0) { - partnerId = 44; - } - - const url = `${process.env.REACT_APP_URL2}/partners/getriderlogs/?applocationid=${appId}&partnerid=${partnerId}&fromdate=${dateStr}&todate=${dateStr}&keyword=${debouncedSearch}`; - const res = await axios.get(url); - return res.data.details || []; - } catch (err) { - console.error(err); - return []; - } - } - }); - - const { data: activeSubsToday, isLoading: activeSubsTodayLoading } = useQuery({ - queryKey: ['activeSubstitutionsToday', appId, selectedDate.format('YYYY-MM-DD')], - queryFn: async () => { - try { - let partnerId = ''; - try { - const rawLocs = localStorage.getItem('applocations'); - if (rawLocs) { - const locs = JSON.parse(rawLocs); - const currentLoc = locs.find((l) => l.applocationid === appId); - if (currentLoc && currentLoc.partnerid) { - partnerId = currentLoc.partnerid; - } else { - const firstValidLoc = locs.find((l) => l.partnerid); - if (firstValidLoc && firstValidLoc.partnerid) { - partnerId = firstValidLoc.partnerid; - } - } - } - } catch (e) { - console.error('Error parsing applocations', e); - } - - if (!partnerId) { - const savedPartnerId = localStorage.getItem('partnerid'); - if (savedPartnerId && savedPartnerId !== 'undefined' && savedPartnerId !== 'null') { - partnerId = savedPartnerId; - } - } - - if (!partnerId || partnerId === '0' || partnerId === 0) { - partnerId = 44; - } - - const dateStr = selectedDate.format('YYYY-MM-DD'); - const url = `${process.env.REACT_APP_URL}/substitutions?tenant_id=${partnerId}&from_date=${dateStr}&to_date=${dateStr}`; - const res = await axios.get(url); - return res.data.details || []; - } catch (err) { - console.error(err); - return []; - } - } + queryFn: async () => [] }); + // The /substitutions resource has no equivalent in the new API. const { data: subHistoryData, isLoading: subHistoryLoading } = useQuery({ queryKey: ['substitutionsHistory', appId, historyDate, historyStatus], - queryFn: async () => { - try { - let partnerId = ''; - try { - const rawLocs = localStorage.getItem('applocations'); - if (rawLocs) { - const locs = JSON.parse(rawLocs); - const currentLoc = locs.find((l) => l.applocationid === appId); - if (currentLoc && currentLoc.partnerid) { - partnerId = currentLoc.partnerid; - } else { - const firstValidLoc = locs.find((l) => l.partnerid); - if (firstValidLoc && firstValidLoc.partnerid) { - partnerId = firstValidLoc.partnerid; - } - } - } - } catch (e) { - console.error('Error parsing applocations', e); - } - - if (!partnerId) { - const savedPartnerId = localStorage.getItem('partnerid'); - if (savedPartnerId && savedPartnerId !== 'undefined' && savedPartnerId !== 'null') { - partnerId = savedPartnerId; - } - } - - if (!partnerId || partnerId === '0' || partnerId === 0) { - partnerId = 44; - } - - const dateStr = historyDate.format('YYYY-MM-DD'); - let url = `${process.env.REACT_APP_URL}/substitutions?tenant_id=${partnerId}&from_date=${dateStr}&to_date=${dateStr}`; - if (historyStatus !== 'all') { - url += `&status=${historyStatus}`; - } - const res = await axios.get(url); - return res.data.details || []; - } catch (err) { - console.error(err); - return []; - } - } + queryFn: async () => [] }); const handleChangetab = (i) => { @@ -502,32 +281,17 @@ const Riders = () => { }); // ==============================|| getRiderLogs (riders)||============================== // - const getRiderLogs = async (userid) => { - try { - const res = await axios.get(`${process.env.REACT_APP_URL}/utils/getriderperiodiclogs?userid=${userid}`); - if (res.data.data.length == 0) { - setLogsRow(null); - OpenToast(res.data.message, 'error', 2000); - } else { - setRiderLogsdata(res.data.data); - } - } catch (err) { - OpenToast(err.message, 'error', 2000); - } + // No live GPS/battery/periodic-log endpoint in the new API — the expanded + // row still opens, showing the panel's built-in "—" placeholders. + const getRiderLogs = async () => { + OpenToast('Live rider logs are not available on the Doormile Express API yet.', 'warning', 2500); }; - // ==============================|| getriderstatus||============================== // - const { - data: ridersStatus, - isLoading: riderStatusLoading, - isError: riderstatusIsError, - error: riderStatusError - } = useQuery({ - queryKey: ['ridersStatus'], - queryFn: getriderstatus - }); - // ==============================|| fetchAllRiders||============================== // + // GET /admin/milers isn't paginated (fetchAllRiders always returns the full + // list in one shot) — getNextPageParam must not request a "next page" or it + // re-fetches and appends the identical full list again, duplicating every + // rider in `rows`. const { data: allRidersData, isLoading: allRidersLoading, @@ -537,11 +301,24 @@ const Riders = () => { } = useInfiniteQuery({ queryKey: ['allriders', appId, debouncedSearch, tabvalue], queryFn: fetchAllRiders, - getNextPageParam: (lastPage, pages) => (lastPage.details?.length ? pages.length + 1 : undefined) + getNextPageParam: () => undefined }); const rows = (allRidersData?.pages.flatMap((page) => page.details || []) || []).filter(Boolean); + // GET /admin/milers accepts no search/status params — debouncedSearch and + // tabvalue only trigger a refetch (via the queryKey) that returns the exact + // same full list every time. Search and the "Active" tab were both silently + // no-ops until filtered client-side here. + const filteredRows = rows.filter((row) => { + // "Active" here means currently online/working (Available/Assigned/ + // On_Pickup) — the only real "not working" state is Offline. + if (tabvalue === 1 && String(row.availabilitystatus || '').toLowerCase() === 'offline') return false; + if (!debouncedSearch) return true; + const q = debouncedSearch.toLowerCase(); + return [row.displayname, row.phone].some((v) => String(v || '').toLowerCase().includes(q)); + }); + useEffect(() => { if (!hasNextPage) return; const observer = new IntersectionObserver( @@ -571,30 +348,85 @@ const Riders = () => { } }; - const errMessage = riderstatusIsError ? riderStatusError : null; - useEffect(() => { - if (errMessage) { - OpenToast(errMessage, 'error', 2000); - } - }, [errMessage]); - // Per-row status meta — falls back to "unknown" if the rider's state key // isn't in the palette (e.g. brand-new status string from the backend). + // Previously joined against a separate rider-status endpoint on every tab + // except "All Riders" — that endpoint has no equivalent in the new API and + // always returned null, so every rider on every other tab showed "Unknown" + // regardless of their real state. Now reads the miler's own + // `availabilitystatus` field directly on every tab (confirmed live — there + // is no `status` field on a miler at all). const getRowStatusMeta = (row) => { - if (tabvalue == 0) { - const key = (row?.status || '').toLowerCase() === 'active' ? 'active' : 'inactive'; - return STATUS_META[key] || STATUS_META.unknown; - } - const state = ridersStatus?.find((s) => s.userid === row?.userid); - const key = (state?.status || 'unknown').toLowerCase(); + const key = (row?.availabilitystatus || '').toLowerCase(); return STATUS_META[key] || STATUS_META.unknown; }; + // PUT /admin/milers/:id/block — body shape isn't documented (Status: Built, + // never exercised against a real request), so no fields are sent beyond the + // id in the URL. Blocks whichever rider is passed after a confirm prompt. + const blockRiderMutation = useMutation({ + // /admin/milers/:id routes key off milerprofileid, not userid (confirmed + // live — GET /admin/milers/:userid 404s; the miler's own userid field + // points at the underlying app-user account, a different resource). + mutationFn: (row) => blockMiler(row.milerprofileid), + onSuccess: (res) => { + if (res.success) { + OpenToast('Rider blocked', 'success', 2000); + queryClient.invalidateQueries({ queryKey: ['allriders'] }); + } else { + OpenToast(res.message || 'Failed to block rider', 'error', 2000); + } + }, + onError: (err) => { + OpenToast(err.response?.data?.message || err.message || 'Failed to block rider', 'error', 2000); + } + }); + + const handleBlockRider = (row) => { + if (window.confirm(`Block ${row.displayname || row.authname || 'this rider'}? They will not be able to log in to the miler app.`)) { + blockRiderMutation.mutate(row); + } + }; + + // PUT /admin/milers/:id/assign-vehicle — body shape isn't documented + // (Status: Built, never exercised), so this assumes `vehicleid` following + // the codebase's own id convention (GET /admin/vehicles/:id). + const [assignVehicleRider, setAssignVehicleRider] = useState(null); + const [selectedVehicle, setSelectedVehicle] = useState(null); + + const { data: vehicles = [] } = useQuery({ + queryKey: ['riders-vehicles'], + queryFn: getVehicles, + enabled: !!assignVehicleRider + }); + + const assignVehicleMutation = useMutation({ + mutationFn: () => assignMilerVehicle(assignVehicleRider.milerprofileid, { vehicleid: selectedVehicle?.vehicleid }), + onSuccess: (res) => { + if (res.success) { + OpenToast('Vehicle assigned', 'success', 2000); + queryClient.invalidateQueries({ queryKey: ['allriders'] }); + setAssignVehicleRider(null); + setSelectedVehicle(null); + } else { + OpenToast(res.message || 'Failed to assign vehicle', 'error', 2000); + } + }, + onError: (err) => { + OpenToast(err.response?.data?.message || err.message || 'Failed to assign vehicle', 'error', 2000); + } + }); + return ( <> theme.zIndex.drawer + 1 }} - open={allRidersLoading || riderSummarysLoading || riderStatusLoading || (tabvalue === 2 && subRidersLogsLoading) || (tabvalue === 3 && subHistoryLoading)} + open={ + allRidersLoading || + riderSummarysLoading || + (tabvalue === 2 && subRidersLogsLoading) || + (tabvalue === 3 && subHistoryLoading) + } > @@ -653,13 +485,7 @@ const Riders = () => { background: '#fff' }} > - + { {TAB_META.map((t) => { const Icon = t.icon; const active = tabvalue === t.key; - const count = t.key === 2 - ? (subRidersLogsData?.length || 0) - : t.key === 3 - ? (subHistoryData?.length || 0) - : (allRidersSummary?.[t.countKey] ?? 0); - const countLoading = t.key === 2 - ? subRidersLogsLoading - : t.key === 3 - ? subHistoryLoading - : riderSummarysLoading; + const count = + t.key === 2 + ? subRidersLogsData?.length || 0 + : t.key === 3 + ? subHistoryData?.length || 0 + : allRidersSummary?.[t.countKey] ?? 0; + const countLoading = t.key === 2 ? subRidersLogsLoading : t.key === 3 ? subHistoryLoading : riderSummarysLoading; return ( { - {t.label} + {t.label} { background: '#fff' }} > - + Status: @@ -1118,77 +936,435 @@ const Riders = () => { /> ) : ( - - {isMobile ? ( - /* ===================== MOBILE: card list ===================== */ - - {allRidersLoading && ( - - - - Loading riders… - - - )} + + {isMobile ? ( + /* ===================== MOBILE: card list ===================== */ + + {allRidersLoading && ( + + + + Loading riders… + + + )} - {rows?.length === 0 && !allRidersLoading && ( - - - - - - No riders to show - - - {searchword - ? 'Try a different keyword.' - : `No ${tabvalue === 0 ? '' : 'active '}riders for this zone.`} - - - )} + {filteredRows?.length === 0 && !allRidersLoading && ( + + + + + + No riders to show + + + {searchword ? 'Try a different keyword.' : `No ${tabvalue === 0 ? '' : 'active '}riders for this zone.`} + + + )} - {rows?.length !== 0 && - rows?.map((row, index) => { - const statusMeta = getRowStatusMeta(row); - const StatusIcon = statusMeta.icon; - const expanded = logsRow === row.userid; - return ( - - - + {filteredRows?.length !== 0 && + filteredRows?.map((row, index) => { + const statusMeta = getRowStatusMeta(row); + const StatusIcon = statusMeta.icon; + const expanded = logsRow === row.userid; + return ( + + + + + {String(index + 1).padStart(2, '0')} + + + #{row?.userid} + + + + + + + {statusMeta.label} + + + + {roleid == 1 && ( + + { + navigate('/doormile/riders/edit', { state: { riderdata: row } }); + }} + > + + + handleBlockRider(row)} + > + + + setAssignVehicleRider(row)} + > + + + {tabvalue != 0 && ( + { + if (row.userid == logsRow) { + setLogsRow(null); + } else { + setLogsRow(row.userid); + getRiderLogs(row.userid); + } + }} + > + {expanded ? : } + + )} + + )} + + + + {(row.displayname || row.authname || '?').charAt(0).toUpperCase()} + + + + {row.displayname || row.authname || '—'} + + + {row.phone || '—'} + + + + + } + > + + + + {row.suburb || (row.address ? row.address.slice(0, 20) + '…' : '—')} + + + {row.city || ''} + + + + + + + + + {row.defaultvehicletype || '—'} + + + + + + + + {row.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.starttime}`).format('hh:mm A') : '—'} + + + {row.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.endtime}`).format('hh:mm A') : '—'} + + + + + + + + {expanded && tabvalue !== 0 && ( + + + + + + + Live telemetry — {row.displayname || row.authname || `Rider #${row.userid}`} + + + + + + + + + + + + + + )} + + ); + })} + + {filteredRows?.length !== 0 && ( +
    + {isFetchingNextPage || hasNextPage ? ( + + ) : ( + + No more riders + + )} +
    + )} +
    + ) : ( + + + + # + ID + Rider + Address + Vehicle + Shift + Time + Fare + Fuel + Status + {roleid == 1 && Action} + + + + {allRidersLoading && } + + {filteredRows?.length === 0 && !allRidersLoading && ( + + + + + + + + No riders to show + + + {searchword ? 'Try a different keyword.' : `No ${tabvalue === 0 ? '' : 'active '}riders for this zone.`} + + + + + )} + + {filteredRows?.length !== 0 && + filteredRows?.map((row, index) => { + const statusMeta = getRowStatusMeta(row); + const StatusIcon = statusMeta.icon; + const expanded = logsRow === row.userid; + return ( + + + {String(index + 1).padStart(2, '0')} + + { border: `1px solid ${edge(BRAND)}`, color: BRAND, fontWeight: 800, - fontSize: 11 + fontSize: 11, + minWidth: 56 }} > #{row?.userid} + + + + + {(row.displayname || row.authname || '?').charAt(0).toUpperCase()} + + + + {row.displayname || row.authname || '—'} + + + {row.phone || '—'} + + + + + + + + + {row.suburb || (row.address ? row.address.slice(0, 20) + '…' : '—')} + + + {row.city || ''} + + + + + + + + + + + {row.defaultvehicletype || '—'} + + + + + + #{row.shiftid ?? '—'} + + + + + + {row.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.starttime}`).format('hh:mm A') : '—'} + + + {row.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.endtime}`).format('hh:mm A') : '—'} + + + + + + {row.basefare ?? '—'} + + + + + {row.fuelcharge ?? '—'} + + + { color: statusMeta.color }} > - - + + - + {statusMeta.label} - + {roleid == 1 && ( - - { - navigate('/doormile/riders/edit', { state: { riderdata: row } }); - }} - > - - - {tabvalue != 0 && ( - { - if (row.userid == logsRow) { - setLogsRow(null); - } else { - setLogsRow(row.userid); - getRiderLogs(row.userid); - } - }} - > - {expanded ? : } - - )} - - )} - - - - {(row.fullname || row.username || '?').charAt(0).toUpperCase()} - - - - {row.username || '—'} - - - {row.contactno || '—'} - - - - - } - > - - - - {row.suburb || (row.address ? row.address.slice(0, 20) + '…' : '—')} - - - {row.city || ''} - - - - - - - - - {row.vehicleno || '—'} - - - - - - - - {row.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.starttime}`).format('hh:mm A') : '—'} - - - {row.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.endtime}`).format('hh:mm A') : '—'} - - - - - - - - - {expanded && tabvalue !== 0 && ( - - - - - - - Live telemetry — {row.username || `Rider #${row.userid}`} - - - - - - - - - - - - - - )} - - ); - })} - - {rows?.length !== 0 && ( -
    - {isFetchingNextPage || hasNextPage ? ( - - ) : ( - - No more riders - - )} -
    - )} - - ) : ( -
    - - - # - ID - Rider - Address - Vehicle - Shift - Time - Fare - Fuel - Status - {roleid == 1 && Action} - - - - {allRidersLoading && } - - {rows?.length === 0 && !allRidersLoading && ( - - - - - - - - No riders to show - - - {searchword - ? 'Try a different keyword.' - : `No ${tabvalue === 0 ? '' : 'active '}riders for this zone.`} - - - - - )} - - {rows?.length !== 0 && - rows?.map((row, index) => { - const statusMeta = getRowStatusMeta(row); - const StatusIcon = statusMeta.icon; - const expanded = logsRow === row.userid; - return ( - - - - - {String(index + 1).padStart(2, '0')} - - - - - #{row?.userid} - - - - - - {(row.fullname || row.username || '?').charAt(0).toUpperCase()} - - - - {row.username || '—'} - - - {row.contactno || '—'} - - - - - - - - - {row.suburb || (row.address ? row.address.slice(0, 20) + '…' : '—')} - - - {row.city || ''} - - - - - - - - - - - {row.vehicleno || '—'} - - - - - - #{row.shiftid ?? '—'} - - - - - - {row.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.starttime}`).format('hh:mm A') : '—'} - - - {row.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.endtime}`).format('hh:mm A') : '—'} - - - - - - {row.basefare ?? '—'} - - - - - {row.fuelcharge ?? '—'} - - - - - - - - - {statusMeta.label} - - - - {roleid == 1 && ( - - - - { - navigate('/doormile/riders/edit', { state: { riderdata: row } }); - }} - > - - - - {tabvalue != 0 && ( - - { - if (row.userid == logsRow) { - setLogsRow(null); - } else { - setLogsRow(row.userid); - getRiderLogs(row.userid); - } - }} - > - {expanded ? : } - - - )} - - - )} - - - {/* ============ Collapsible row — live rider logs ============ */} - {expanded && tabvalue !== 0 && ( - - - - - - - - - - Live telemetry — {row.username || `Rider #${row.userid}`} - + + + + { + navigate('/doormile/riders/edit', { state: { riderdata: row } }); + }} + > + + + + + handleBlockRider(row)} + > + + + + + setAssignVehicleRider(row)} + > + + + + {tabvalue != 0 && ( + + { + if (row.userid == logsRow) { + setLogsRow(null); + } else { + setLogsRow(row.userid); + getRiderLogs(row.userid); + } + }} + > + {expanded ? : } + + + )} - - - - - - - - - - - - - - - )} - - ); - })} + + )} + - {rows?.length !== 0 && ( - - -
    - {isFetchingNextPage || hasNextPage ? ( - - ) : ( - - No more riders - - )} -
    -
    -
    - )} -
    -
    - )} -
    -
    + {/* ============ Collapsible row — live rider logs ============ */} + {expanded && tabvalue !== 0 && ( + + + + + + + + + + Live telemetry — {row.displayname || row.authname || `Rider #${row.userid}`} + + + + + + + + + + + + + + + + + )} + + ); + })} + + {filteredRows?.length !== 0 && ( + + +
    + {isFetchingNextPage || hasNextPage ? ( + + ) : ( + + No more riders + + )} +
    +
    +
    + )} + + + )} + + )} { + + { + setAssignVehicleRider(null); + setSelectedVehicle(null); + }} + maxWidth="xs" + fullWidth + > + + Assign Vehicle · {assignVehicleRider?.displayname || assignVehicleRider?.authname} + + + `${option.vehicleno || ''}${option.vehicletype ? ` (${option.vehicletype})` : ''}`} + value={selectedVehicle} + onChange={(e, value) => setSelectedVehicle(value)} + renderInput={(params) => } + /> + + + + + + ); }; diff --git a/src/pages/nearle/viewProfile.js b/src/pages/nearle/viewProfile.js index 3059250..8920761 100644 --- a/src/pages/nearle/viewProfile.js +++ b/src/pages/nearle/viewProfile.js @@ -1,22 +1,14 @@ -import { Avatar, Box, Chip, Grid, Paper, Stack, Typography, useMediaQuery } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { useQuery } from '@tanstack/react-query'; -import { - MdBadge, - MdPerson, - MdLocationOn, - MdMail, - MdPhone, - MdPlace, - MdMyLocation, - MdLocationCity, - MdMap, - MdMarkunreadMailbox, - MdVerifiedUser -} from 'react-icons/md'; +import { useState } from 'react'; +import { Avatar, Box, Button, Chip, Grid, InputLabel, Paper, Stack, TextField, Typography } from '@mui/material'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { MdBadge, MdPerson, MdLocationOn, MdMail, MdShield, MdVerifiedUser, MdLock } from 'react-icons/md'; import CircularLoader from 'components/CircularLoader'; import Loader from 'components/Loader'; import { getusers } from 'pages/api/api'; +import { updateProfilePassword } from 'pages/api/doormileApi'; +import { enqueueSnackbar } from 'notistack'; + +const ROLE_LABELS = { 1: 'Admin', 3: 'Manager', 4: 'Executive' }; // ---- shared design tokens (mirrors the DT block used across the console) ---- const DT = { @@ -99,14 +91,72 @@ const SectionCard = ({ title, icon: Icon, children }) => ( ); const ViewProfile = () => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); const { data: userData, isLoading } = useQuery({ queryKey: ['getuser'], queryFn: getusers }); - const fullname = userData?.fullname || userData?.firstname || 'User Profile'; + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + + const changePasswordMutation = useMutation({ + mutationFn: () => updateProfilePassword(currentPassword, newPassword), + onSuccess: (res) => { + if (res.success) { + enqueueSnackbar('Password updated successfully', { + variant: 'success', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + } else { + enqueueSnackbar(res.message || 'Failed to update password', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + } + }, + onError: (err) => { + enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to update password', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + } + }); + + const handleChangePassword = () => { + if (!currentPassword || !newPassword) { + enqueueSnackbar('Fill current and new password', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + return; + } + if (newPassword !== confirmPassword) { + enqueueSnackbar('New password and confirmation do not match', { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + return; + } + changePasswordMutation.mutate(); + }; + + // GET /admin/profile has no documented schema, but the console's own login + // response ({ success, token, user: { id, name, email, role, tenantid } }) + // is confirmed — /admin/profile almost certainly returns the same "user" + // shape. Fields like address/suburb/city/postcode were jupiter-era guesses + // with no basis in the new API and have been dropped rather than shown blank. + const fullname = userData?.name || userData?.fullname || 'User Profile'; + const roleLabel = ROLE_LABELS[userData?.role] || ROLE_LABELS[userData?.roleid] || userData?.role || '—'; + const tenantLabel = userData?.tenantid ? `Tenant #${userData.tenantid}` : 'Doormile staff (all tenants)'; return ( <> @@ -153,11 +203,11 @@ const ViewProfile = () => { {fullname}
    - {userData?.authname && ( + {roleLabel && ( } - label={userData.authname} + label={roleLabel} sx={{ bgcolor: soft(BRAND), color: BRAND, @@ -167,19 +217,17 @@ const ViewProfile = () => { }} /> )} - {userData?.userid != null && userData?.userid !== '' && ( + {(userData?.id != null || userData?.userid != null) && ( - ID #{userData.userid} + ID #{userData.id ?? userData.userid} )} - {userData?.applocation && ( - - - - {userData.applocation} - - - )} + + + + {tenantLabel} + +
    @@ -190,42 +238,66 @@ const ViewProfile = () => { - - - - + + + + + - - - - - + + + + Current Password + setCurrentPassword(e.target.value)} + autoComplete="off" + /> + + + New Password + setNewPassword(e.target.value)} + autoComplete="off" + /> + + + Confirm New Password + setConfirmPassword(e.target.value)} + autoComplete="off" + /> + + - - - - - - - - - - - - - - - - - - -
    diff --git a/src/routes/MainRoutes.js b/src/routes/MainRoutes.js index f811e8a..c4b309f 100644 --- a/src/routes/MainRoutes.js +++ b/src/routes/MainRoutes.js @@ -14,7 +14,6 @@ const MaintenanceComingSoon = Loadable(lazy(() => import('pages/maintenance/comi // render - sample page // const SamplePage = Loadable(lazy(() => import('pages/extra-pages/sample-page'))); -const Login = Loadable(lazy(() => import('pages/nearle/login1'))); // const Dashboard = Loadable(lazy(() => import('pages/nearle/dashboard'))); const Orders = Loadable(lazy(() => import('pages/nearle/orders/orders'))); @@ -25,8 +24,6 @@ const Deliveries = Loadable(lazy(() => import('pages/nearle/deliveries/deliverie const Tenants = Loadable(lazy(() => import('pages/nearle/clients/Tenants'))); const ClientsPricing = Loadable(lazy(() => import('pages/nearle/clientPricing/clientPricing'))); -const Details = Loadable(lazy(() => import('pages/nearle/orders/details'))); - const ViewProfile = Loadable(lazy(() => import('pages/nearle/viewProfile'))); const Createorder1 = Loadable(lazy(() => import('pages/nearle/orders/createorder1'))); @@ -34,7 +31,6 @@ const MultipleOrders = Loadable(lazy(() => import('pages/nearle/orders/multipleO const Createclient = Loadable(lazy(() => import('pages/nearle/clients/createclient'))); -const Requests = Loadable(lazy(() => import('pages/nearle/requests/requests'))); const OrdersSummary = Loadable(lazy(() => import('pages/nearle/reports/ordersSummary'))); const OrdersDetails = Loadable(lazy(() => import('pages/nearle/reports/ordersDetails'))); const RidersSummary = Loadable(lazy(() => import('pages/nearle/reports/ridersSummary'))); @@ -45,6 +41,14 @@ const EditRider = Loadable(lazy(() => import('pages/nearle/riders/editRider'))); const Dispatch = Loadable(lazy(() => import('pages/nearle/dispatch/Dispatch'))); const DispatchPreview = Loadable(lazy(() => import('pages/nearle/dispatch/Preview'))); +const Customers = Loadable(lazy(() => import('pages/nearle/customers/customers'))); +const Hubs = Loadable(lazy(() => import('pages/nearle/hubs/hubs'))); +const Vehicles = Loadable(lazy(() => import('pages/nearle/vehicles/vehicles'))); +const AppUsers = Loadable(lazy(() => import('pages/nearle/appUsers/appUsers'))); +const Tripsheets = Loadable(lazy(() => import('pages/nearle/tripsheets/tripsheets'))); +const Exceptions = Loadable(lazy(() => import('pages/nearle/exceptions/exceptions'))); +const CompetitiveIntel = Loadable(lazy(() => import('pages/nearle/competitiveIntel/competitiveIntel'))); + // ==============================|| MAIN ROUTING ||============================== // const MainRoutes = { @@ -81,10 +85,6 @@ const MainRoutes = { path: 'pricing', element: }, - { - path: 'requests', - element: - }, { path: 'riders', element: @@ -106,10 +106,6 @@ const MainRoutes = { path: 'orders/createorders', element: }, - { - path: 'orders/details', - element:
    - }, { path: 'clients/create', element: @@ -138,6 +134,34 @@ const MainRoutes = { { path: 'dispatch/preview', element: + }, + { + path: 'customers', + element: + }, + { + path: 'hubs', + element: + }, + { + path: 'vehicles', + element: + }, + { + path: 'app-users', + element: + }, + { + path: 'tripsheets', + element: + }, + { + path: 'exceptions', + element: + }, + { + path: 'competitive-intel', + element: } ] }, @@ -154,10 +178,6 @@ const MainRoutes = { ] }, - { - path: '/login', - element: - }, { path: '/maintenance', element: , diff --git a/src/utils/doormileAxios.js b/src/utils/doormileAxios.js new file mode 100644 index 0000000..907d3a3 --- /dev/null +++ b/src/utils/doormileAxios.js @@ -0,0 +1,38 @@ +import axios from 'axios'; + +// Client for the new Doormile Express admin API (api.doormile.com/api/v1/admin/*). +// Separate instance from utils/axios.js and the raw axios calls in pages/api/api.js +// because this backend requires a JWT Bearer token on every request and uses its +// own token lifecycle — it does not share the `authname` gate in App.js. +// See express-console-api.md at the repo root for the full endpoint reference. + +export const DOORMILE_TOKEN_KEY = 'doormileToken'; +export const DOORMILE_USER_KEY = 'doormileUser'; + +const doormileAxios = axios.create({ + baseURL: process.env.REACT_APP_DOORMILE_URL || 'https://api.doormile.com/api/v1' +}); + +doormileAxios.interceptors.request.use((config) => { + const token = localStorage.getItem(DOORMILE_TOKEN_KEY); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +doormileAxios.interceptors.response.use( + (response) => response, + (error) => { + // Mirror utils/session.js's performSessionLogout contract (full localStorage + // wipe + hard navigate) so an expired doormile token logs the console out the + // same way inactivity/manual logout does, instead of leaving a half-authed state. + if (error.response?.status === 401 && !window.location.href.includes('/login')) { + localStorage.clear(); + window.location.replace('/login'); + } + return Promise.reject(error.response?.data || error); + } +); + +export default doormileAxios; diff --git a/src/utils/locales/en.json b/src/utils/locales/en.json index d936ffe..67a4805 100644 --- a/src/utils/locales/en.json +++ b/src/utils/locales/en.json @@ -14,5 +14,13 @@ "ordersummary": "Orders Summary", "ordersdetails": "Orders Details", "riderssummary": "Riders Summary", - "dispatch": "Dispatch" + "dispatch": "Dispatch", + "customers": "Customers", + "fleetops": "Fleet & Ops", + "hubs": "Hubs", + "vehicles": "Vehicles", + "tripsheets": "Tripsheets", + "exceptions": "Exceptions", + "competitiveintel": "Competitive Intel", + "appusers": "App Users" }