Migrate console off jupiter.nearle.app to the Doormile Express API
Retires REACT_APP_URL/URL2/URL3 in favor of REACT_APP_DOORMILE_URL across every page (orders, deliveries, riders, tenants, pricing, profile, reports, dispatch). Fixes several field-mapping and envelope-check bugs found along the way, most notably that /admin/milers/:id routes (block, assign-vehicle, edit, notify) key off milerprofileid, not userid, and that a miler's real fields are phone/availabilitystatus/displayname, not contactno/status/ firstname+lastname (confirmed against a live read-only session). Also fixes several silent-failure bugs uncovered during that audit: order creation and order cancellation showed a success toast but gave no feedback at all on failure (createorder1.js had a dead notifyadmin() call that left the loading spinner stuck forever on every failed submit), and Tenants.js's pricing/profile updates never surfaced a failed response to the operator. The AI dispatch optimiser (routes.workolik.com/routemate.workolik.com) and its jupiter.nearle.app delivery-commit call remain untouched by design — separate solver service with no equivalent in the new API.
This commit is contained in:
@@ -5,6 +5,8 @@ metadata:
|
|||||||
version: 2.0.0
|
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
|
# 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.
|
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.
|
||||||
|
|||||||
10
.env
10
.env
@@ -4,15 +4,7 @@ GENERATE_SOURCEMAP = false
|
|||||||
## Backend API URL
|
## Backend API URL
|
||||||
REACT_APP_API_URL=https://mock-data-api-nextjs.vercel.app/
|
REACT_APP_API_URL=https://mock-data-api-nextjs.vercel.app/
|
||||||
|
|
||||||
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
|
REACT_APP_DOORMILE_URL=https://api.doormile.com/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_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk
|
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1 @@
|
|||||||
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
|
REACT_APP_DOORMILE_URL=https://api.doormile.com/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
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
|
REACT_APP_DOORMILE_URL=https://api.doormile.com/api/v1
|
||||||
REACT_APP_URL2=
|
|
||||||
REACT_APP_STAFF_TOKEN=
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# NearlExpress Console — API Endpoints Reference
|
# 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`.
|
> 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.
|
> Base URLs are resolved from the env files at the repo root.
|
||||||
|
|
||||||
|
|||||||
12
CLAUDE.md
12
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.
|
- **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".
|
- **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.
|
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).
|
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.
|
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.
|
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.
|
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.
|
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
|
## 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.
|
- **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** 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.
|
- **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.
|
- **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
|
## 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.
|
- **`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.
|
- **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` query param**: every list endpoint expects this — `0` means "All Zones". Pages default `appId = 0` and update it from `LocationAutocomplete`.
|
- **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.
|
- **`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.
|
- **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.
|
- **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.
|
||||||
|
|||||||
2
FLOW.md
2
FLOW.md
@@ -1,5 +1,7 @@
|
|||||||
# NearlExpress Console — Repository Flow Graph
|
# 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`.
|
> 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)).
|
> Open this file in a Mermaid-aware viewer (GitHub, VS Code with the *Markdown Preview Mermaid* extension, or [mermaid.live](https://mermaid.live)).
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -50,10 +50,7 @@ REACT_APP_VERSION=v2.1.0
|
|||||||
GENERATE_SOURCEMAP=false
|
GENERATE_SOURCEMAP=false
|
||||||
|
|
||||||
# Backend Services Endpoint URLs
|
# Backend Services Endpoint URLs
|
||||||
REACT_APP_API_URL=https://mock-data-api-nextjs.vercel.app/
|
REACT_APP_DOORMILE_URL=https://api.doormile.com/api/v1
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
No Maps API key is required — maps and address search run on free Leaflet/OpenStreetMap (Nominatim + OSRM), not Google Maps.
|
No Maps API key is required — maps and address search run on free Leaflet/OpenStreetMap (Nominatim + OSRM), not Google Maps.
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
REACT_APP_URL='https://jupiter.nearle.app/live/api/v1'
|
REACT_APP_DOORMILE_URL='https://api.doormile.com/api/v1'
|
||||||
REACT_APP_URL2=''
|
|
||||||
REACT_APP_STAFF_TOKEN=''
|
|
||||||
292
express-console-api.md
Normal file
292
express-console-api.md
Normal file
@@ -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 <token>`.
|
||||||
|
|
||||||
|
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.
|
||||||
254
jupiter2doormile.md
Normal file
254
jupiter2doormile.md
Normal file
@@ -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.
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { forwardRef, useEffect, useState } from 'react';
|
import React, { forwardRef, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Autocomplete, TextField, Avatar, Stack } from '@mui/material';
|
import { Autocomplete, TextField, Avatar, Stack } from '@mui/material';
|
||||||
import axios from 'axios';
|
|
||||||
import { MdMyLocation } from 'react-icons/md';
|
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
|
// Pill variant — opt-in via `pill` prop. Mirrors the design used across the
|
||||||
// deliveries / dispatch filter rows (rounded pill, soft tinted bg, accent
|
// 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') || '[]'));
|
const [locations, setLocations] = useState(JSON.parse(localStorage.getItem('applocations') || '[]'));
|
||||||
|
|
||||||
useEffect(() => {
|
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 () => {
|
const fetchLocations = async () => {
|
||||||
try {
|
try {
|
||||||
const userid = localStorage.getItem('userid');
|
const updatedLocations = await fetchAppLocations();
|
||||||
if (!userid) return;
|
localStorage.setItem('applocations', JSON.stringify(updatedLocations));
|
||||||
const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
|
setLocations(updatedLocations);
|
||||||
if (response.data.status) {
|
|
||||||
const updatedLocations = [...response.data.details, { locationname: 'All', applocationid: 0 }];
|
|
||||||
localStorage.setItem('applocations', JSON.stringify(updatedLocations));
|
|
||||||
setLocations(updatedLocations);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching locations in LocationAutocomplete:', err);
|
console.error('Error fetching locations in LocationAutocomplete:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,14 @@ import {
|
|||||||
UserOutlined,
|
UserOutlined,
|
||||||
ProfileOutlined,
|
ProfileOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
MoneyCollectOutlined
|
MoneyCollectOutlined,
|
||||||
|
ContactsOutlined,
|
||||||
|
ShopOutlined,
|
||||||
|
CarryOutOutlined,
|
||||||
|
IdcardOutlined,
|
||||||
|
TruckOutlined,
|
||||||
|
WarningOutlined,
|
||||||
|
RadarChartOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
@@ -50,7 +57,14 @@ const icons = {
|
|||||||
UserOutlined,
|
UserOutlined,
|
||||||
ProfileOutlined,
|
ProfileOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
MoneyCollectOutlined
|
MoneyCollectOutlined,
|
||||||
|
ContactsOutlined,
|
||||||
|
ShopOutlined,
|
||||||
|
CarryOutOutlined,
|
||||||
|
IdcardOutlined,
|
||||||
|
TruckOutlined,
|
||||||
|
WarningOutlined,
|
||||||
|
RadarChartOutlined
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| MENU ITEMS - SUPPORT ||============================== //
|
// ==============================|| MENU ITEMS - SUPPORT ||============================== //
|
||||||
@@ -103,6 +117,63 @@ const nearle = {
|
|||||||
url: '/doormile/pricing',
|
url: '/doormile/pricing',
|
||||||
icon: icons.MoneyCollectOutlined
|
icon: icons.MoneyCollectOutlined
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'customers',
|
||||||
|
title: <FormattedMessage id="customers" />,
|
||||||
|
type: 'item',
|
||||||
|
url: '/doormile/customers',
|
||||||
|
icon: icons.ContactsOutlined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fleetops',
|
||||||
|
title: <FormattedMessage id="fleetops" />,
|
||||||
|
type: 'collapse',
|
||||||
|
icon: icons.TruckOutlined,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'hubs',
|
||||||
|
title: <FormattedMessage id="hubs" />,
|
||||||
|
type: 'item',
|
||||||
|
url: '/doormile/hubs',
|
||||||
|
icon: icons.ShopOutlined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vehicles',
|
||||||
|
title: <FormattedMessage id="vehicles" />,
|
||||||
|
type: 'item',
|
||||||
|
url: '/doormile/vehicles',
|
||||||
|
icon: icons.CarOutlined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tripsheets',
|
||||||
|
title: <FormattedMessage id="tripsheets" />,
|
||||||
|
type: 'item',
|
||||||
|
url: '/doormile/tripsheets',
|
||||||
|
icon: icons.CarryOutOutlined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'exceptions',
|
||||||
|
title: <FormattedMessage id="exceptions" />,
|
||||||
|
type: 'item',
|
||||||
|
url: '/doormile/exceptions',
|
||||||
|
icon: icons.WarningOutlined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'competitiveintel',
|
||||||
|
title: <FormattedMessage id="competitiveintel" />,
|
||||||
|
type: 'item',
|
||||||
|
url: '/doormile/competitive-intel',
|
||||||
|
icon: icons.RadarChartOutlined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'appusers',
|
||||||
|
title: <FormattedMessage id="appusers" />,
|
||||||
|
type: 'item',
|
||||||
|
url: '/doormile/app-users',
|
||||||
|
icon: icons.IdcardOutlined
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'reports',
|
id: 'reports',
|
||||||
title: <FormattedMessage id="reports" />,
|
title: <FormattedMessage id="reports" />,
|
||||||
|
|||||||
@@ -1,31 +1,33 @@
|
|||||||
# CLAUDE.md — `src/pages/api/`
|
# 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
|
### 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.
|
Destructure from `queryKey` in the order the call site declared it. The leading `_` is the query name and is intentionally discarded.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// Plain query — destructure { queryKey }, skip the [0] name slot
|
|
||||||
export const fetchorderscount = async ({ queryKey }) => {
|
export const fetchorderscount = async ({ queryKey }) => {
|
||||||
const [, appId, startdate, enddate, currentStatus, tenantid, locationid] = queryKey;
|
const [, , startdate, enddate] = queryKey;
|
||||||
const url = `${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&...`;
|
return (await getReports(startdate, enddate)) || {};
|
||||||
const response = await axios.get(url);
|
|
||||||
return response.data.details;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Infinite query — also receives pageParam (default to 1)
|
|
||||||
export const fetchOrders = async ({ pageParam = 1, queryKey }) => {
|
export const fetchOrders = async ({ pageParam = 1, queryKey }) => {
|
||||||
const [, appId, currentStatus, debouncedSearch, startdate, enddate, rowsPerPage, tenantid, locationid] = queryKey;
|
const [, , , , , , rowsPerPage] = queryKey;
|
||||||
const url = `${process.env.REACT_APP_URL}/orders/tenant/getorders/?applocationid=${appId}&...&pageno=${pageParam}&pagesize=${rowsPerPage}`;
|
const rows = (await getBookings(pageParam, rowsPerPage)) || [];
|
||||||
const response = await axios.get(url);
|
|
||||||
return {
|
return {
|
||||||
rows: response.data.details,
|
rows,
|
||||||
nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined
|
nextPage: rows.length === Number(rowsPerPage) ? pageParam + 1 : undefined
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
@@ -33,81 +35,67 @@ export const fetchOrders = async ({ pageParam = 1, queryKey }) => {
|
|||||||
**Hard rules:**
|
**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.
|
- 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`).
|
- 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
|
## 3. Base client and what the new API can't do
|
||||||
A few functions take plain positional arguments instead of `queryKey` — usually when they're invoked from a `useMutation` or imperatively. Example: `getTenants(appId)`, `gettenantlocations(tenantid)`.
|
|
||||||
|
|
||||||
Pick the signature based on how the function is called:
|
| Use | When |
|
||||||
- Called via `useQuery({ queryFn: fn })` → destructure `{ queryKey }`.
|
|
||||||
- Called via `useQuery({ queryFn: () => fn(arg) })` → take positional args.
|
|
||||||
- Called from a mutation or imperatively → take positional args.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Base URL selection
|
|
||||||
|
|
||||||
| Use base | When |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| `process.env.REACT_APP_URL` | Default for ~95% of endpoints |
|
| `doormileAxios` (via `doormileApi.js` functions) | Every `/admin/*` endpoint. Bearer token attached automatically from `localStorage.doormileToken`. |
|
||||||
| `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 — untouched, separate service. |
|
||||||
| Hardcoded `https://routes.workolik.com` | Bike solver + reconcile-steps + batch efficiency |
|
| Hardcoded `https://routemate.workolik.com` | Auto / multi-trip solver — untouched, separate service. |
|
||||||
| Hardcoded `https://routemate.workolik.com` | Auto / multi-trip solver |
|
| Hardcoded `https://jupiter.nearle.app` | Final `/deliveries/createdeliveries` commit only — untouched, part of the same solver pipeline. |
|
||||||
| Hardcoded `https://jupiter.nearle.app` | Login + final `/deliveries/createdeliveries` commit |
|
|
||||||
|
|
||||||
When adding a new endpoint, check whether the backend actually serves it on URL or URL2 — don't guess. URL2 lives on a separate service.
|
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
|
## 4. Error handling pattern
|
||||||
|
|
||||||
```js
|
```js
|
||||||
try {
|
export const someFetch = async () => {
|
||||||
const response = await axios.get(`${process.env.REACT_APP_URL}/...`);
|
try {
|
||||||
return response.data.details; // or .summary, .data, etc — depends on backend shape
|
return (await someDoormileApiFn()) || [];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
||||||
OpenToast(message);
|
OpenToast(message);
|
||||||
return null; // or return [] / {} — match what the caller expects
|
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.
|
- 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.
|
- Return a sensible empty default so the call site's destructuring doesn't crash.
|
||||||
- Don't `console.log(err)` AND toast — toast is enough. Some legacy functions do both; new functions should not.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. When NOT to add to `api.js`
|
## 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 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 new shared GET** → put it 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.
|
||||||
- **Adding a one-off mutation used in only one page** → tolerated inline in that page; doesn't need a new export here.
|
|
||||||
- **Adding a polling endpoint used by multiple pages** → put it here as a named export so the query cache keys are coherent.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Response shape quirks
|
## 6. Response shape
|
||||||
|
|
||||||
Backend response shapes are inconsistent. Don't assume `response.data.details` — check what the specific endpoint returns:
|
`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.
|
||||||
|
|
||||||
| Backend field | Used by |
|
|
||||||
|---|---|
|
|
||||||
| `response.data.details` | Most list endpoints |
|
|
||||||
| `response.data.summary` | `getcustomersummary`, `gettenantsummary`, `getpricinglist` summary calls |
|
|
||||||
| `response.data.data` | `getRiderPeriodicLogs`, `getallcustomers` infinite-query page payload |
|
|
||||||
| `response.data.message` | Mutation success messages (for toast) |
|
|
||||||
| `response.data.status` | Boolean success flag — check before reading `.details` on some endpoints |
|
|
||||||
|
|
||||||
When unsure, log the response once during dev and pick the matching field.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Things that look broken but are intentional
|
## 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.
|
- 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.
|
||||||
- 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 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.
|
||||||
- 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.
|
- `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.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
557
src/pages/api/doormileApi.js
Normal file
557
src/pages/api/doormileApi.js
Normal file
@@ -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;
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
|||||||
import React, { useEffect, useState, useRef, Fragment } from 'react';
|
import React, { useEffect, useState, useRef, Fragment } from 'react';
|
||||||
import MainCard from 'components/MainCard';
|
import MainCard from 'components/MainCard';
|
||||||
import axios from 'axios';
|
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
import { Empty } from 'antd';
|
import { Empty } from 'antd';
|
||||||
@@ -19,7 +18,6 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Box,
|
Box,
|
||||||
Grid,
|
Grid,
|
||||||
Autocomplete,
|
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Collapse,
|
Collapse,
|
||||||
@@ -39,6 +37,8 @@ import {
|
|||||||
Skeleton,
|
Skeleton,
|
||||||
Avatar,
|
Avatar,
|
||||||
Paper,
|
Paper,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
useMediaQuery
|
useMediaQuery
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||||||
@@ -65,7 +65,12 @@ import {
|
|||||||
MdOutlinePendingActions,
|
MdOutlinePendingActions,
|
||||||
MdOutlineCancel,
|
MdOutlineCancel,
|
||||||
MdMyLocation,
|
MdMyLocation,
|
||||||
MdPersonPin
|
MdPersonPin,
|
||||||
|
MdPlace,
|
||||||
|
MdAdd,
|
||||||
|
MdEdit,
|
||||||
|
MdDeleteOutline,
|
||||||
|
MdPeopleAlt
|
||||||
} from 'react-icons/md';
|
} from 'react-icons/md';
|
||||||
import { LuMail } from 'react-icons/lu';
|
import { LuMail } from 'react-icons/lu';
|
||||||
import { BiUser } from 'react-icons/bi';
|
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 DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
||||||
import PageHeader from 'components/nearle_components/PageHeader';
|
import PageHeader from 'components/nearle_components/PageHeader';
|
||||||
import StatCard from 'components/nearle_components/StatCard';
|
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 { 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';
|
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -103,24 +121,6 @@ const soft = (c) => a(c, '18');
|
|||||||
const ring = (c) => a(c, '26');
|
const ring = (c) => a(c, '26');
|
||||||
const edge = (c) => a(c, '55');
|
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.
|
// Status palette — drives the pill tabs and per-row badges.
|
||||||
const STATUS_META = {
|
const STATUS_META = {
|
||||||
active: { label: 'Active', color: '#10b981', icon: MdCheckCircle, statusKey: 'active' },
|
active: { label: 'Active', color: '#10b981', icon: MdCheckCircle, statusKey: 'active' },
|
||||||
@@ -168,6 +168,370 @@ const AccentAvatar = ({ color, selected, size = 24, children }) => (
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ==============================|| 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 (
|
||||||
|
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||||
|
<DialogTitle sx={{ fontWeight: 700 }}>
|
||||||
|
Locations · {tenant?.tenantname}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
{isLoading ? (
|
||||||
|
<Typography variant="body2" sx={{ color: DT.textSecondary, py: 2 }}>
|
||||||
|
Loading locations…
|
||||||
|
</Typography>
|
||||||
|
) : locations.length === 0 && !formOpen ? (
|
||||||
|
<Empty description="No locations added yet" />
|
||||||
|
) : (
|
||||||
|
<Stack spacing={1.5}>
|
||||||
|
{locations.map((loc) => (
|
||||||
|
<Paper key={loc.locationid} variant="outlined" sx={{ p: 1.5, borderRadius: 2 }}>
|
||||||
|
<Stack direction="row" justifyContent="space-between" alignItems="flex-start">
|
||||||
|
<Box>
|
||||||
|
<Typography sx={{ fontWeight: 700 }}>
|
||||||
|
{loc.locationname} {loc.isprimary && '· Primary'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||||
|
{[loc.address, loc.city, loc.state, loc.pincode].filter(Boolean).join(', ')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<IconButton size="small" onClick={() => openEdit(loc)}>
|
||||||
|
<MdEdit size={14} />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formOpen && (
|
||||||
|
<Grid container spacing={1.5} sx={{ mt: 1.5, pt: 1.5, borderTop: `1px solid ${DT.divider}` }}>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Location Name"
|
||||||
|
value={form.locationname}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, locationname: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField fullWidth label="Pincode" value={form.pincode} onChange={(e) => setForm((f) => ({ ...f, pincode: e.target.value }))} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField fullWidth label="Address" value={form.address} onChange={(e) => setForm((f) => ({ ...f, address: e.target.value }))} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField fullWidth label="City" value={form.city} onChange={(e) => setForm((f) => ({ ...f, city: e.target.value }))} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField fullWidth label="State" value={form.state} onChange={(e) => setForm((f) => ({ ...f, state: e.target.value }))} />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||||
|
{formOpen ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setFormOpen(false);
|
||||||
|
setForm(emptyLocationForm);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={handleSave}>
|
||||||
|
{form.locationid ? 'Save Changes' : 'Add Location'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button onClick={onClose}>Close</Button>
|
||||||
|
<Button variant="contained" startIcon={<MdAdd size={16} />} onClick={() => setFormOpen(true)}>
|
||||||
|
Add Location
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==============================|| 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 (
|
||||||
|
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||||
|
<DialogTitle sx={{ fontWeight: 700 }}>Customers · {tenant?.tenantname}</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
{isLoading ? (
|
||||||
|
<Typography variant="body2" sx={{ color: DT.textSecondary, py: 2 }}>
|
||||||
|
Loading customers…
|
||||||
|
</Typography>
|
||||||
|
) : customers.length === 0 && !formOpen ? (
|
||||||
|
<Empty description="No customers yet" />
|
||||||
|
) : (
|
||||||
|
<Stack spacing={1.5}>
|
||||||
|
{customers.map((c) => (
|
||||||
|
<Paper key={tenantCustId(c)} variant="outlined" sx={{ p: 1.5, borderRadius: 2 }}>
|
||||||
|
<Stack direction="row" justifyContent="space-between" alignItems="flex-start">
|
||||||
|
<Box>
|
||||||
|
<Typography sx={{ fontWeight: 700 }}>
|
||||||
|
{c.firstname} {c.lastname}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||||
|
{[c.phone, c.email].filter(Boolean).join(' · ')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Stack direction="row" spacing={0.5}>
|
||||||
|
<IconButton size="small" onClick={() => openEdit(c)}>
|
||||||
|
<MdEdit size={14} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton size="small" onClick={() => handleDelete(c)} sx={{ color: '#ef4444' }}>
|
||||||
|
<MdDeleteOutline size={14} />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formOpen && (
|
||||||
|
<Grid container spacing={1.5} sx={{ mt: 1.5, pt: 1.5, borderTop: `1px solid ${DT.divider}` }}>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField fullWidth label="First Name" value={form.firstname} onChange={(e) => setForm((f) => ({ ...f, firstname: e.target.value }))} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField fullWidth label="Last Name" value={form.lastname} onChange={(e) => setForm((f) => ({ ...f, lastname: e.target.value }))} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField fullWidth label="Phone" value={form.phone} onChange={(e) => setForm((f) => ({ ...f, phone: e.target.value }))} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField fullWidth label="Email" value={form.email} onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))} />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||||
|
{formOpen ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setFormOpen(false);
|
||||||
|
setForm(emptyCustomerForm);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={handleSave}>
|
||||||
|
{form.id ? 'Save Changes' : 'Add Customer'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button onClick={onClose}>Close</Button>
|
||||||
|
<Button variant="contained" startIcon={<MdAdd size={16} />} onClick={() => setFormOpen(true)}>
|
||||||
|
Add Customer
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// ==============================|| Starts here||============================== //
|
// ==============================|| Starts here||============================== //
|
||||||
const Clients1 = () => {
|
const Clients1 = () => {
|
||||||
const textFieldRef = useRef(null);
|
const textFieldRef = useRef(null);
|
||||||
@@ -189,7 +553,7 @@ const Clients1 = () => {
|
|||||||
const [status, setstatus] = useState('active');
|
const [status, setstatus] = useState('active');
|
||||||
const [selectedCustomer, setSelectedCustomer] = useState({}); // to edit
|
const [selectedCustomer, setSelectedCustomer] = useState({}); // to edit
|
||||||
const [dialogopen, setDialogopen] = useState(false);
|
const [dialogopen, setDialogopen] = useState(false);
|
||||||
const [appPricing, setAppPricing] = useState([]);
|
const [, setAppPricing] = useState([]);
|
||||||
const [selectedPricing, setSelectedPricing] = useState({});
|
const [selectedPricing, setSelectedPricing] = useState({});
|
||||||
const [isPrice, setIsprice] = useState(true);
|
const [isPrice, setIsprice] = useState(true);
|
||||||
const [city, setCity] = useState('');
|
const [city, setCity] = useState('');
|
||||||
@@ -197,7 +561,6 @@ const Clients1 = () => {
|
|||||||
const [latlong, setLatlong] = useState({});
|
const [latlong, setLatlong] = useState({});
|
||||||
const [editClient, setEditClient] = useState({});
|
const [editClient, setEditClient] = useState({});
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const [tabStatus, setTabStatus] = useState('Active');
|
|
||||||
|
|
||||||
const handleChangePage = (event, newPage) => {
|
const handleChangePage = (event, newPage) => {
|
||||||
setPage(newPage);
|
setPage(newPage);
|
||||||
@@ -212,7 +575,6 @@ const Clients1 = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (event, newValue) => {
|
const handleChange = (event, newValue) => {
|
||||||
setTabStatus(newValue == 0 ? 'Active' : newValue == 1 ? 'Pending' : 'InActive');
|
|
||||||
setValue0(newValue);
|
setValue0(newValue);
|
||||||
setSearchword('');
|
setSearchword('');
|
||||||
setPage(0);
|
setPage(0);
|
||||||
@@ -228,6 +590,8 @@ const Clients1 = () => {
|
|||||||
|
|
||||||
const [openRowIndex1, setOpenRowIndex1] = useState(null); // Initially no row is open for collapsible section 1
|
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 [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
|
// Handle toggling for collapsible section 1
|
||||||
const handleCollapseToggle1 = (rowIndex) => {
|
const handleCollapseToggle1 = (rowIndex) => {
|
||||||
setOpenRowIndex1((prevIndex) => (prevIndex === rowIndex ? null : rowIndex));
|
setOpenRowIndex1((prevIndex) => (prevIndex === rowIndex ? null : rowIndex));
|
||||||
@@ -348,18 +712,14 @@ const Clients1 = () => {
|
|||||||
|
|
||||||
/* ============================================= || fetchclientpricelist || ============================================= */
|
/* ============================================= || fetchclientpricelist || ============================================= */
|
||||||
|
|
||||||
|
// /admin/pricing has no tenantid query param documented — filter client-side.
|
||||||
const fetchclientpricelist = async () => {
|
const fetchclientpricelist = async () => {
|
||||||
await axios
|
try {
|
||||||
.get(`${process.env.REACT_APP_URL}/tenants/getpricinglist/?tenantid=${selectedTenid}`)
|
const pricing = await getAdminPricing();
|
||||||
.then((res) => {
|
setClientpricelist((pricing || []).filter((p) => p.tenantid === selectedTenid));
|
||||||
console.log('getpricinglist', res);
|
} catch (err) {
|
||||||
if (res.data.status) {
|
console.log(err);
|
||||||
setClientpricelist(res.data.details);
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectedTenid && fetchclientpricelist();
|
selectedTenid && fetchclientpricelist();
|
||||||
@@ -368,9 +728,8 @@ const Clients1 = () => {
|
|||||||
/* ============================================= || fetchTenanatPricing || ============================================= */
|
/* ============================================= || fetchTenanatPricing || ============================================= */
|
||||||
const fetchTenanatPricing = async (id) => {
|
const fetchTenanatPricing = async (id) => {
|
||||||
try {
|
try {
|
||||||
let tenantPricing = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${id}`);
|
const pricing = await getAdminPricing();
|
||||||
console.log('tenantPricing', tenantPricing.data.details);
|
setTenanatPricing((pricing || []).filter((p) => p.tenantid === id));
|
||||||
setTenanatPricing(tenantPricing.data.details);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('fetchTenanatPricing', error);
|
console.log('fetchTenanatPricing', error);
|
||||||
}
|
}
|
||||||
@@ -382,44 +741,40 @@ const Clients1 = () => {
|
|||||||
|
|
||||||
const tenantupdate = async (tenid) => {
|
const tenantupdate = async (tenid) => {
|
||||||
setisloader(true);
|
setisloader(true);
|
||||||
|
let targetId;
|
||||||
let updateData;
|
let updateData;
|
||||||
if (tenid == -1) {
|
if (tenid == -1) {
|
||||||
updateData = {
|
targetId = selectedTenid;
|
||||||
tenantid: selectedTenid,
|
updateData = { approved: 1 };
|
||||||
approved: 1
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
updateData = { tenantid: tenid, status: status === 'active' ? 'InActive' : 'Active' };
|
targetId = tenid;
|
||||||
|
updateData = { status: status === 'active' ? 'InActive' : 'Active' };
|
||||||
}
|
}
|
||||||
|
|
||||||
await axios
|
try {
|
||||||
.put(`${process.env.REACT_APP_URL}/tenants/update`, updateData)
|
const res = await updateAdminTenant(targetId, updateData);
|
||||||
.then((res) => {
|
if (res.success) {
|
||||||
if (res.data.status) {
|
opentoast(
|
||||||
opentoast(
|
value0 == 0 ? 'Inactivated Successfully' : value0 == 1 ? 'Approved Successfully' : 'Activate Successfully',
|
||||||
value0 == 0 ? 'Inactivated Successfully' : value0 == 1 ? 'Approved Successfully' : 'Activate Successfully',
|
'success',
|
||||||
'success',
|
2000
|
||||||
2000
|
);
|
||||||
);
|
getalltenantsRefetch();
|
||||||
getalltenantsRefetch();
|
summaryDataRefetch();
|
||||||
summaryDataRefetch();
|
}
|
||||||
setisloader(false);
|
} catch (err) {
|
||||||
}
|
console.log(err);
|
||||||
})
|
opentoast(err.message, 'error', 1500);
|
||||||
.catch((err) => {
|
} finally {
|
||||||
console.log(err);
|
setisloader(false);
|
||||||
opentoast(err.message, 'error', 1500);
|
}
|
||||||
setisloader(false);
|
|
||||||
});
|
|
||||||
// }
|
|
||||||
};
|
};
|
||||||
/* ============================================= || getAppPricing || ============================================= */
|
/* ============================================= || getAppPricing || ============================================= */
|
||||||
|
// /admin/doormile-pricing has no zone query param documented — filter client-side.
|
||||||
const getAppPricing = async (id) => {
|
const getAppPricing = async (id) => {
|
||||||
console.log('id', id);
|
|
||||||
try {
|
try {
|
||||||
let appPricingRes = await axios.get(`${process.env.REACT_APP_URL}/utils/getapppricing/?applocationid=${id}`);
|
const pricing = await getDoormilePricing();
|
||||||
console.log('appPricingRes', appPricingRes.data.details);
|
setAppPricing((pricing || []).filter((p) => !id || p.applocationid === id));
|
||||||
setAppPricing(appPricingRes.data.details);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('appPricingRes', error);
|
console.log('appPricingRes', error);
|
||||||
}
|
}
|
||||||
@@ -427,55 +782,54 @@ const Clients1 = () => {
|
|||||||
/* ============================================= || createpricing || ============================================= */
|
/* ============================================= || createpricing || ============================================= */
|
||||||
const createpricing = async () => {
|
const createpricing = async () => {
|
||||||
setisloader(true);
|
setisloader(true);
|
||||||
await axios
|
try {
|
||||||
.post(`${process.env.REACT_APP_URL}/tenants/createpricing`, {
|
const res = await createAdminPricing({
|
||||||
tenantpricingid: 0,
|
|
||||||
applocationid: appId,
|
applocationid: appId,
|
||||||
pricingid: selectedPricing.pricingid,
|
|
||||||
tenantid: selectedTenid,
|
tenantid: selectedTenid,
|
||||||
pricingdate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
vehicletype: selectedPricing.vehicletype || 'Bike',
|
||||||
configid: selectedPricing.configid,
|
baseprice: +selectedPricing.baseprice || 0,
|
||||||
pricingtypeid: selectedPricing.pricingtypeid,
|
baseweight: +selectedPricing.baseweight || 0,
|
||||||
slab: selectedPricing.slab,
|
priceperkm: +selectedPricing.priceperkm || 0,
|
||||||
baseprice: +selectedPricing.baseprice,
|
priceperkg: +selectedPricing.priceperkg || 0,
|
||||||
priceperkm: +selectedPricing.priceperkm,
|
basedistance: +selectedPricing.basedistance || 0,
|
||||||
minkm: +selectedPricing.minkm,
|
handlingcharges: +selectedPricing.handlingcharges || 0,
|
||||||
maxkm: +selectedPricing.maxkm,
|
effectivefrom: dayjs().format('YYYY-MM-DD'),
|
||||||
orders: +selectedPricing.minorder,
|
currency: 'INR',
|
||||||
othercharges: 0
|
priority: 1,
|
||||||
})
|
status: 'active'
|
||||||
.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);
|
|
||||||
});
|
});
|
||||||
|
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 || ============================================= */
|
/* ============================================= || updateClient || ============================================= */
|
||||||
const updateClient = async () => {
|
const updateClient = async () => {
|
||||||
try {
|
try {
|
||||||
const arr = { ...editClient, tenantid: selectedTenid };
|
const updateRes = await updateAdminTenant(selectedTenid, editClient);
|
||||||
|
if (updateRes.success) {
|
||||||
console.log('updateClient', arr);
|
opentoast(updateRes.message || 'Updated Successfully', 'success', 1500);
|
||||||
const updateRes = await axios.put(`${process.env.REACT_APP_URL}/tenants/update`, arr);
|
} else {
|
||||||
console.log('updateClient', updateRes.data.message);
|
opentoast(updateRes.message || 'Update Failed', 'error', 1500);
|
||||||
if (updateRes.data.status) {
|
|
||||||
opentoast(updateRes.data.message, 'success', 1500);
|
|
||||||
}
|
}
|
||||||
getalltenantsRefetch();
|
getalltenantsRefetch();
|
||||||
summaryDataRefetch();
|
summaryDataRefetch();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('updateClient', err);
|
console.log('updateClient', err);
|
||||||
|
opentoast(err.response?.data?.message || err.message || 'Update Failed', 'error', 1500);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -912,6 +1266,34 @@ const Clients1 = () => {
|
|||||||
{openRowIndex1 === index ? <EyeInvisibleOutlined style={{ fontSize: 14 }} /> : <EyeOutlined style={{ fontSize: 14 }} />}
|
{openRowIndex1 === index ? <EyeInvisibleOutlined style={{ fontSize: 14 }} /> : <EyeOutlined style={{ fontSize: 14 }} />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip title="Locations" placement="top">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
bgcolor: soft('#0ea5e9'),
|
||||||
|
color: '#0ea5e9',
|
||||||
|
border: `1px solid ${edge('#0ea5e9')}`,
|
||||||
|
'&:hover': { bgcolor: '#0ea5e9', color: '#fff' }
|
||||||
|
}}
|
||||||
|
onClick={() => setLocationsDialogTenant(row)}
|
||||||
|
>
|
||||||
|
<MdPlace size={14} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Customers" placement="top">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
bgcolor: soft('#14b8a6'),
|
||||||
|
color: '#14b8a6',
|
||||||
|
border: `1px solid ${edge('#14b8a6')}`,
|
||||||
|
'&:hover': { bgcolor: '#14b8a6', color: '#fff' }
|
||||||
|
}}
|
||||||
|
onClick={() => setCustomersDialogTenant(row)}
|
||||||
|
>
|
||||||
|
<MdPeopleAlt size={14} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
{value0 !== 1 && (
|
{value0 !== 1 && (
|
||||||
<Tooltip title={openRowIndex2 === index ? 'Close' : 'Edit'} placement="top">
|
<Tooltip title={openRowIndex2 === index ? 'Close' : 'Edit'} placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -1208,12 +1590,12 @@ const Clients1 = () => {
|
|||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>#</TableCell>
|
<TableCell>#</TableCell>
|
||||||
<TableCell>Date</TableCell>
|
<TableCell>Vehicle</TableCell>
|
||||||
<TableCell>Slab</TableCell>
|
|
||||||
<TableCell>Base Price</TableCell>
|
<TableCell>Base Price</TableCell>
|
||||||
<TableCell>Min Kms</TableCell>
|
|
||||||
<TableCell>Price/Km</TableCell>
|
<TableCell>Price/Km</TableCell>
|
||||||
<TableCell>Other Charges</TableCell>
|
<TableCell>Price/Kg</TableCell>
|
||||||
|
<TableCell>Handling</TableCell>
|
||||||
|
<TableCell>Status</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -1225,14 +1607,14 @@ const Clients1 = () => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
clientpricelist.map((val, i) => (
|
clientpricelist.map((val, i) => (
|
||||||
<TableRow key={val.id || i} sx={{ bgcolor: 'white' }}>
|
<TableRow key={val.pricingid || i} sx={{ bgcolor: 'white' }}>
|
||||||
<TableCell>{i + 1}</TableCell>
|
<TableCell>{i + 1}</TableCell>
|
||||||
<TableCell>{dayjs(val.pricingdate).format('DD-MM-YYYY')}</TableCell>
|
<TableCell>{val.vehicletype || '—'}</TableCell>
|
||||||
<TableCell>{val.slab}</TableCell>
|
<TableCell>{val.baseprice ?? '—'}</TableCell>
|
||||||
<TableCell>{val.baseprice}</TableCell>
|
<TableCell>{val.priceperkm ?? '—'}</TableCell>
|
||||||
<TableCell>{val.minkm}</TableCell>
|
<TableCell>{val.priceperkg ?? '—'}</TableCell>
|
||||||
<TableCell>{val.priceperkm}</TableCell>
|
<TableCell>{val.handlingcharges ?? '—'}</TableCell>
|
||||||
<TableCell>{val.othercharges}</TableCell>
|
<TableCell>{val.status || '—'}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -1667,12 +2049,12 @@ const Clients1 = () => {
|
|||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>#</TableCell>
|
<TableCell>#</TableCell>
|
||||||
<TableCell>Date</TableCell>
|
<TableCell>Vehicle</TableCell>
|
||||||
<TableCell>Slab</TableCell>
|
|
||||||
<TableCell>Base Price</TableCell>
|
<TableCell>Base Price</TableCell>
|
||||||
<TableCell>Min Kms</TableCell>
|
|
||||||
<TableCell>Price/Km</TableCell>
|
<TableCell>Price/Km</TableCell>
|
||||||
<TableCell>Other Charges</TableCell>
|
<TableCell>Price/Kg</TableCell>
|
||||||
|
<TableCell>Handling</TableCell>
|
||||||
|
<TableCell>Status</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -1684,14 +2066,14 @@ const Clients1 = () => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
clientpricelist.map((val, i) => (
|
clientpricelist.map((val, i) => (
|
||||||
<TableRow key={val.id || i} sx={{ bgcolor: 'white' }}>
|
<TableRow key={val.pricingid || i} sx={{ bgcolor: 'white' }}>
|
||||||
<TableCell>{i + 1}</TableCell>
|
<TableCell>{i + 1}</TableCell>
|
||||||
<TableCell>{dayjs(val.pricingdate).format('DD-MM-YYYY')}</TableCell>
|
<TableCell>{val.vehicletype || '—'}</TableCell>
|
||||||
<TableCell>{val.slab}</TableCell>
|
<TableCell>{val.baseprice ?? '—'}</TableCell>
|
||||||
<TableCell>{val.baseprice}</TableCell>
|
<TableCell>{val.priceperkm ?? '—'}</TableCell>
|
||||||
<TableCell>{val.minkm}</TableCell>
|
<TableCell>{val.priceperkg ?? '—'}</TableCell>
|
||||||
<TableCell>{val.priceperkm}</TableCell>
|
<TableCell>{val.handlingcharges ?? '—'}</TableCell>
|
||||||
<TableCell>{val.othercharges}</TableCell>
|
<TableCell>{val.status || '—'}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -1861,6 +2243,30 @@ const Clients1 = () => {
|
|||||||
>
|
>
|
||||||
{expanded ? <EyeInvisibleOutlined style={{ fontSize: 14 }} /> : <EyeOutlined style={{ fontSize: 14 }} />}
|
{expanded ? <EyeInvisibleOutlined style={{ fontSize: 14 }} /> : <EyeOutlined style={{ fontSize: 14 }} />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
bgcolor: soft('#0ea5e9'),
|
||||||
|
color: '#0ea5e9',
|
||||||
|
border: `1px solid ${edge('#0ea5e9')}`,
|
||||||
|
'&:hover': { bgcolor: '#0ea5e9', color: '#fff' }
|
||||||
|
}}
|
||||||
|
onClick={() => setLocationsDialogTenant(row)}
|
||||||
|
>
|
||||||
|
<MdPlace size={14} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
bgcolor: soft('#14b8a6'),
|
||||||
|
color: '#14b8a6',
|
||||||
|
border: `1px solid ${edge('#14b8a6')}`,
|
||||||
|
'&:hover': { bgcolor: '#14b8a6', color: '#fff' }
|
||||||
|
}}
|
||||||
|
onClick={() => setCustomersDialogTenant(row)}
|
||||||
|
>
|
||||||
|
<MdPeopleAlt size={14} />
|
||||||
|
</IconButton>
|
||||||
{value0 !== 1 && (
|
{value0 !== 1 && (
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
@@ -1955,24 +2361,24 @@ const Clients1 = () => {
|
|||||||
<Grid container spacing={2.5} alignItems={'center'}>
|
<Grid container spacing={2.5} alignItems={'center'}>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<FormLabel>Select Slab</FormLabel>
|
<FormLabel>Select Slab</FormLabel>
|
||||||
<Autocomplete
|
<Select
|
||||||
disablePortal
|
|
||||||
id="combo-box-demo"
|
|
||||||
options={appPricing}
|
|
||||||
getOptionLabel={(option) => `${option.slab}`}
|
|
||||||
fullWidth
|
fullWidth
|
||||||
selectOnFocus
|
displayEmpty
|
||||||
renderInput={(params) => <TextField {...params} label="Slab" />}
|
value={selectedPricing.vehicletype || ''}
|
||||||
onChange={(event, value, reason) => {
|
onChange={(e) => {
|
||||||
setSelectedPricing(value);
|
setSelectedPricing({ ...selectedPricing, vehicletype: e.target.value });
|
||||||
console.log('pricing', value);
|
|
||||||
setIsprice(false);
|
setIsprice(false);
|
||||||
|
|
||||||
if (reason === 'clear') {
|
|
||||||
setIsprice(true);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<MenuItem value="" disabled>
|
||||||
|
Choose vehicle type
|
||||||
|
</MenuItem>
|
||||||
|
{['Bike', 'Scooter', 'Bicycle', 'Car', 'Van'].map((v) => (
|
||||||
|
<MenuItem key={v} value={v}>
|
||||||
|
{v}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
@@ -2005,43 +2411,57 @@ const Clients1 = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<FormLabel>Min Kms</FormLabel>
|
<FormLabel>Base Weight (kg)</FormLabel>
|
||||||
<TextField
|
<TextField
|
||||||
type="number"
|
type="number"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={!isPrice ? selectedPricing.minkm : 0}
|
value={!isPrice ? selectedPricing.baseweight : 0}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSelectedPricing({
|
setSelectedPricing({
|
||||||
...selectedPricing,
|
...selectedPricing,
|
||||||
minkm: e.target.value
|
baseweight: e.target.value
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<FormLabel>Max Kms</FormLabel>
|
<FormLabel>Price/Kg</FormLabel>
|
||||||
<TextField
|
<TextField
|
||||||
type="number"
|
type="number"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={!isPrice ? selectedPricing.maxkm : 0}
|
value={!isPrice ? selectedPricing.priceperkg : 0}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSelectedPricing({
|
setSelectedPricing({
|
||||||
...selectedPricing,
|
...selectedPricing,
|
||||||
maxkm: e.target.value
|
priceperkg: e.target.value
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<FormLabel>Min Orders</FormLabel>
|
<FormLabel>Base Distance (km)</FormLabel>
|
||||||
<TextField
|
<TextField
|
||||||
type="number"
|
type="number"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={!isPrice ? selectedPricing.minorder : 0}
|
value={!isPrice ? selectedPricing.basedistance : 0}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSelectedPricing({
|
setSelectedPricing({
|
||||||
...selectedPricing,
|
...selectedPricing,
|
||||||
minorder: e.target.value
|
basedistance: e.target.value
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<FormLabel>Handling Charges</FormLabel>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
fullWidth
|
||||||
|
value={!isPrice ? selectedPricing.handlingcharges : 0}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedPricing({
|
||||||
|
...selectedPricing,
|
||||||
|
handlingcharges: e.target.value
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -2093,6 +2513,18 @@ const Clients1 = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<TenantLocationsDialog
|
||||||
|
tenant={locationsDialogTenant}
|
||||||
|
open={!!locationsDialogTenant}
|
||||||
|
onClose={() => setLocationsDialogTenant(null)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TenantCustomersDialog
|
||||||
|
tenant={customersDialogTenant}
|
||||||
|
open={!!customersDialogTenant}
|
||||||
|
onClose={() => setCustomersDialogTenant(null)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,75 +1,43 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
// material-ui
|
// material-ui
|
||||||
import { useTheme } from '@mui/material/styles';
|
import {
|
||||||
import { Avatar, Box, Button, FormLabel, Grid, InputLabel, MenuItem, Paper, Select, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
|
Avatar,
|
||||||
|
Button,
|
||||||
|
FormControlLabel,
|
||||||
|
Grid,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Switch,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme
|
||||||
|
} from '@mui/material';
|
||||||
import { MdPersonAddAlt1 } from 'react-icons/md';
|
import { MdPersonAddAlt1 } from 'react-icons/md';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
// third-party
|
|
||||||
// import { PatternFormat } from 'react-number-format';
|
|
||||||
|
|
||||||
// project import
|
// project import
|
||||||
import MainCard from 'components/MainCard';
|
import MainCard from 'components/MainCard';
|
||||||
import axios from 'axios';
|
import { createAdminTenant } from 'pages/api/doormileApi';
|
||||||
import AddressAutocomplete, { geocodeAddress } from 'components/nearle_components/AddressAutocomplete';
|
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { DT, tint } from 'themes/dt/tokens';
|
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 Createclient = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
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 navigate = useNavigate();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [tenantname, setTenantname] = useState('');
|
||||||
|
const [primaryemail, setPrimaryemail] = useState('');
|
||||||
useEffect(() => {
|
const [primarycontact, setPrimarycontact] = useState('');
|
||||||
// fetchprofiledetails(localStorage.getItem('appuserid'));
|
const [status, setStatus] = useState('active');
|
||||||
// fetchprofiledetails(181);
|
const [requiredeliveryotp, setRequiredeliveryotp] = useState(false);
|
||||||
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 opentoast = (message) => {
|
const opentoast = (message) => {
|
||||||
enqueueSnackbar(message, {
|
enqueueSnackbar(message, {
|
||||||
@@ -77,192 +45,53 @@ const Createclient = () => {
|
|||||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||||
autoHideDuration: 2000
|
autoHideDuration: 2000
|
||||||
});
|
});
|
||||||
// console.log(alertmessage)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchprofiledetails = async (userid) => {
|
const createTenantMutation = useMutation({
|
||||||
if (userid) {
|
mutationFn: (payload) => createAdminTenant(payload),
|
||||||
setLoading(true);
|
onSuccess: (res) => {
|
||||||
try {
|
if (res.success) {
|
||||||
await axios
|
enqueueSnackbar('Client created successfully', {
|
||||||
.get(`${process.env.REACT_APP_URL2}/tenants/getclient?id=${userid}`)
|
variant: 'success',
|
||||||
.then((res) => {
|
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||||
console.log(res);
|
autoHideDuration: 2000
|
||||||
if (res.data.message === 'Successful') {
|
});
|
||||||
let res1 = res.data.details;
|
navigate('/doormile/tenants');
|
||||||
setMobilenumber(res1.contactno);
|
} else {
|
||||||
setEmailaddress(res1.primaryemail);
|
opentoast(res.message || 'Client already exists');
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
opentoast(err.response?.data?.message || err.message || 'Failed to create client');
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const fetchtenantinfo = async (tid) => {
|
const createprofile = () => {
|
||||||
setLoading(true);
|
if (!tenantname) {
|
||||||
await axios
|
opentoast('Fill Client / Business Name');
|
||||||
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
|
} else if (!primarycontact || primarycontact.length !== 10) {
|
||||||
.then((res) => {
|
opentoast('Fill a valid 10-digit Contact Number');
|
||||||
console.log(res);
|
} else if (!primaryemail) {
|
||||||
if (res.data.status) {
|
opentoast('Fill Primary Email');
|
||||||
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');
|
|
||||||
} else {
|
} else {
|
||||||
let obj = {
|
// POST /admin/tenants — requiredeliveryotp is a pointer server-side;
|
||||||
customerid: 0,
|
// it's opt-in and off by default, so we only send it when the operator
|
||||||
configid: 1,
|
// explicitly turns it on rather than always sending false.
|
||||||
firstname: firstname,
|
const obj = {
|
||||||
applocationid: tenantinfo.applolcationid,
|
tenantname,
|
||||||
profileimage: '',
|
primaryemail,
|
||||||
dialcode: '+91',
|
primarycontact,
|
||||||
contactno: mobilenumber,
|
status,
|
||||||
devicetype: '',
|
...(requiredeliveryotp ? { requiredeliveryotp: true } : {})
|
||||||
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
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(obj);
|
createTenantMutation.mutate(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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// const [experience, setExperience] = useState('0');
|
|
||||||
|
|
||||||
// const handleChange = (event) => {
|
|
||||||
// setExperience(event.target.value);
|
|
||||||
// };
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{loading && <Loader />}
|
{createTenantMutation.isPending && <Loader />}
|
||||||
|
|
||||||
<Grid item xs={12} sx={{ mb: 2 }}>
|
<Grid item xs={12} sx={{ mb: 2 }}>
|
||||||
<Paper
|
<Paper
|
||||||
@@ -288,289 +117,73 @@ const Createclient = () => {
|
|||||||
sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }}
|
sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }}
|
||||||
>
|
>
|
||||||
<Grid container spacing={isMobile ? 2 : 3}>
|
<Grid container spacing={isMobile ? 2 : 3}>
|
||||||
{/* <Grid item xs={12} sm={4} >
|
<Grid item xs={12}>
|
||||||
<MainCard title="Personal Information" sx={{ height: '100%' }}>
|
<MainCard sx={{ height: '100%' }} contentSX={{ p: { xs: 1.5, md: 2.5 } }}>
|
||||||
<Grid container spacing={3}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Stack spacing={2.5} alignItems="center" sx={{ m: 3 }}>
|
|
||||||
<FormLabel
|
|
||||||
htmlFor="change-avtar"
|
|
||||||
sx={{
|
|
||||||
position: 'relative',
|
|
||||||
borderRadius: '50%',
|
|
||||||
overflow: 'hidden',
|
|
||||||
'&:hover .MuiBox-root': { opacity: 1 },
|
|
||||||
cursor: 'pointer'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar alt="Avatar 1"
|
|
||||||
src={avatar}
|
|
||||||
sx={{ width: 76, height: 76 }} />
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
backgroundColor: theme.palette.mode === ThemeMode.DARK ? 'rgba(255, 255, 255, .75)' : 'rgba(0,0,0,.65)',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
opacity: 0,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack spacing={0.5} alignItems="center">
|
|
||||||
<CameraOutlined style={{ color: theme.palette.secondary.lighter, fontSize: '1.5rem' }} />
|
|
||||||
<Typography sx={{ color: 'secondary.lighter' }} variant="caption">
|
|
||||||
Upload
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
</FormLabel>
|
|
||||||
<TextField
|
|
||||||
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
id="change-avtar"
|
|
||||||
placeholder="Outlined"
|
|
||||||
variant="outlined"
|
|
||||||
sx={{ display: 'none' }}
|
|
||||||
onChange={(e) => setSelectedImage(e.target.files?.[0])}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
|
|
||||||
<Grid item xs={12}
|
|
||||||
>
|
|
||||||
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}
|
|
||||||
>
|
|
||||||
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}
|
|
||||||
>
|
|
||||||
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12}
|
|
||||||
|
|
||||||
>
|
|
||||||
<Stack spacing={1.25}>
|
|
||||||
<InputLabel htmlFor="personal-role-name">Role</InputLabel>
|
|
||||||
<TextField fullWidth
|
|
||||||
|
|
||||||
id="personal-role-name" placeholder="Role Name" autoFocus
|
|
||||||
onChange={(e) => setRole(e.target.value)}
|
|
||||||
value={role}
|
|
||||||
autoComplete='off'
|
|
||||||
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
</Grid>
|
|
||||||
</MainCard>
|
|
||||||
</Grid> */}
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs={12}
|
|
||||||
// sm={8}
|
|
||||||
>
|
|
||||||
<MainCard
|
|
||||||
// title="Contact Information"
|
|
||||||
sx={{ height: '100%' }}
|
|
||||||
contentSX={{ p: { xs: 1.5, md: 2.5 } }}
|
|
||||||
>
|
|
||||||
<Grid container spacing={isMobile ? 2 : 3}>
|
<Grid container spacing={isMobile ? 2 : 3}>
|
||||||
{/* <Grid item xs={12} sm={6}>
|
|
||||||
<Stack spacing={1.25}>
|
|
||||||
<InputLabel htmlFor="personal-first-name">Business Name</InputLabel>
|
|
||||||
<TextField fullWidth
|
|
||||||
id="personal-first-name" placeholder="Business Name" autoFocus
|
|
||||||
onChange={(e) => setBusinessname(e.target.value)}
|
|
||||||
value={businessname}
|
|
||||||
autoComplete='off'
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Stack spacing={1.25}>
|
|
||||||
<InputLabel htmlFor="personal-last-name">Registration No</InputLabel>
|
|
||||||
<TextField fullWidth
|
|
||||||
id="personal-last-name" placeholder="Registration No"
|
|
||||||
onChange={(e) => setBusinessno(e.target.value)}
|
|
||||||
value={businessno}
|
|
||||||
autoComplete='off'
|
|
||||||
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Grid> */}
|
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Stack spacing={1.25}>
|
<Stack spacing={1.25}>
|
||||||
<InputLabel htmlFor="personal-last-name">Admin Name</InputLabel>
|
<InputLabel htmlFor="tenant-name">Client / Business Name</InputLabel>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
id="personal-last-name"
|
id="tenant-name"
|
||||||
placeholder="Name"
|
placeholder="e.g. DailyGrubs"
|
||||||
onChange={(e) => setFirstname(e.target.value)}
|
onChange={(e) => setTenantname(e.target.value)}
|
||||||
value={firstname}
|
value={tenantname}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}></Grid>
|
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Stack spacing={1.25}>
|
<Stack spacing={1.25}>
|
||||||
<InputLabel htmlFor="personal-phone">Phone Number</InputLabel>
|
<InputLabel htmlFor="tenant-status">Status</InputLabel>
|
||||||
|
<Select id="tenant-status" fullWidth value={status} onChange={(e) => setStatus(e.target.value)}>
|
||||||
|
<MenuItem value="active">Active</MenuItem>
|
||||||
|
<MenuItem value="inactive">Inactive</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Stack spacing={1.25}>
|
||||||
|
<InputLabel htmlFor="tenant-contact">Primary Contact Number</InputLabel>
|
||||||
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
|
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
|
||||||
<Select defaultValue="+1" disabled sx={{ cursor: 'not-allowed' }}>
|
<Select defaultValue="+1" disabled sx={{ cursor: 'not-allowed' }}>
|
||||||
<MenuItem value="+1">+91</MenuItem>
|
<MenuItem value="+1">+91</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
<TextField
|
<TextField
|
||||||
type="number"
|
type="number"
|
||||||
id="personal-phone"
|
id="tenant-contact"
|
||||||
// format="##########"
|
|
||||||
// mask="_"
|
|
||||||
fullWidth
|
fullWidth
|
||||||
// customInput={TextField}
|
placeholder="Contact Number"
|
||||||
placeholder="Phone Number"
|
|
||||||
// defaultValue="8654239581"
|
|
||||||
// onBlur={() => { }}
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.target.value.toString().length <= 10) {
|
if (e.target.value.toString().length <= 10) {
|
||||||
setMobilenumber(e.target.value);
|
setPrimarycontact(e.target.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
value={mobilenumber}
|
value={primarycontact}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
// disabled
|
|
||||||
sx={{ cursor: 'not-allowed' }}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Stack spacing={1.25}>
|
<Stack spacing={1.25}>
|
||||||
<InputLabel htmlFor="personal-email">Email Address</InputLabel>
|
<InputLabel htmlFor="tenant-email">Primary Email</InputLabel>
|
||||||
<TextField
|
<TextField
|
||||||
type="email"
|
type="email"
|
||||||
fullWidth
|
fullWidth
|
||||||
// defaultValue="stebin.ben@gmail.com"
|
id="tenant-email"
|
||||||
id="personal-email"
|
placeholder="Primary Email Address"
|
||||||
placeholder="Email Address"
|
onChange={(e) => setPrimaryemail(e.target.value)}
|
||||||
onChange={(e) => setEmailaddress(e.target.value)}
|
value={primaryemail}
|
||||||
value={emailaddress}
|
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Stack spacing={1.25}>
|
<FormControlLabel
|
||||||
<InputLabel htmlFor="personal-address">Address</InputLabel>
|
control={<Switch checked={requiredeliveryotp} onChange={(e) => setRequiredeliveryotp(e.target.checked)} />}
|
||||||
<AddressAutocomplete
|
label="Require delivery OTP for this client (off by default)"
|
||||||
id="personal-address"
|
/>
|
||||||
fullWidth
|
|
||||||
placeholder="Address"
|
|
||||||
value={address}
|
|
||||||
onChange={setAddress}
|
|
||||||
onPlaceSelected={handleAddressPlaceSelected}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Stack spacing={1.25}>
|
|
||||||
<InputLabel htmlFor="personal-location">Suburb</InputLabel>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
// defaultValue="New York"
|
|
||||||
id="personal-location"
|
|
||||||
placeholder="Location"
|
|
||||||
onChange={(e) => setSuburb(e.target.value)}
|
|
||||||
value={suburb}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Stack spacing={1.25}>
|
|
||||||
<InputLabel htmlFor="personal-zipcode">City</InputLabel>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
// defaultValue="956754"
|
|
||||||
// type='number'
|
|
||||||
id="personal-zipcode"
|
|
||||||
placeholder="City"
|
|
||||||
onChange={(e) => setCity(e.target.value)}
|
|
||||||
value={city}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Stack spacing={1.25}>
|
|
||||||
<InputLabel htmlFor="personal-location">State</InputLabel>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
// defaultValue="New York"
|
|
||||||
id="personal-location"
|
|
||||||
placeholder="State"
|
|
||||||
onChange={(e) => setState(e.target.value)}
|
|
||||||
value={state}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Stack spacing={1.25}>
|
|
||||||
<InputLabel htmlFor="personal-zipcode">Post Code</InputLabel>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
// defaultValue="956754"
|
|
||||||
type="number"
|
|
||||||
id="personal-zipcode"
|
|
||||||
placeholder="Zipcode"
|
|
||||||
onChange={(e) => setZipcode(e.target.value)}
|
|
||||||
value={zipcode}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Stack spacing={1.25}>
|
|
||||||
<InputLabel htmlFor="personal-location">Door No</InputLabel>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
// defaultValue="New York"
|
|
||||||
id="personal-location"
|
|
||||||
placeholder="Door No"
|
|
||||||
onChange={(e) => setDoorno(e.target.value)}
|
|
||||||
value={doorno}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Stack spacing={1.25}>
|
|
||||||
<InputLabel htmlFor="personal-email">Landmark</InputLabel>
|
|
||||||
<TextField
|
|
||||||
type="email"
|
|
||||||
fullWidth
|
|
||||||
// defaultValue="stebin.ben@gmail.com"
|
|
||||||
id="personal-email"
|
|
||||||
placeholder="Landmark"
|
|
||||||
onChange={(e) => setLandmark(e.target.value)}
|
|
||||||
value={landmark}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</MainCard>
|
</MainCard>
|
||||||
@@ -582,7 +195,7 @@ const Createclient = () => {
|
|||||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
alignItems={{ xs: 'stretch', sm: 'center' }}
|
||||||
spacing={2}
|
spacing={2}
|
||||||
>
|
>
|
||||||
<Button variant="contained" onClick={() => createprofile()} fullWidth={isMobile}>
|
<Button variant="contained" onClick={createprofile} fullWidth={isMobile}>
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@ import 'leaflet/dist/leaflet.css';
|
|||||||
import '../../../utils/leafletPolylineOffset';
|
import '../../../utils/leafletPolylineOffset';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useInfiniteQuery, useQueries, useQuery, useMutation } from '@tanstack/react-query';
|
import { useInfiniteQuery, useQueries, useQuery, useMutation } from '@tanstack/react-query';
|
||||||
import axios from 'axios';
|
|
||||||
import {
|
import {
|
||||||
MdMap,
|
MdMap,
|
||||||
MdDirectionsBike,
|
MdDirectionsBike,
|
||||||
@@ -58,6 +57,7 @@ import {
|
|||||||
import ProfitabilitySection from './ProfitabilitySection';
|
import ProfitabilitySection from './ProfitabilitySection';
|
||||||
import ActiveSection from './ActiveSection';
|
import ActiveSection from './ActiveSection';
|
||||||
import { fetchDeliveries, fetchAppLocations, getRiderPeriodicLogs, fetchRidersLogs, fetchBatchEfficiency } from '../../api/api';
|
import { fetchDeliveries, fetchAppLocations, getRiderPeriodicLogs, fetchRidersLogs, fetchBatchEfficiency } from '../../api/api';
|
||||||
|
import { getConsignmentLogs } from 'pages/api/doormileApi';
|
||||||
import {
|
import {
|
||||||
STATUS_STYLES,
|
STATUS_STYLES,
|
||||||
getStatusStyle,
|
getStatusStyle,
|
||||||
@@ -1071,9 +1071,11 @@ const Dispatch = ({
|
|||||||
return () => window.removeEventListener('keydown', onKey);
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
}, [riderPositionModal]);
|
}, [riderPositionModal]);
|
||||||
|
|
||||||
// TODO: wire to real tenant context once the standalone Dispatch screen
|
// Was hardcoded to 916 (the example tenant in the API spec) — every
|
||||||
// surfaces it. 916 matches the example tenant in the API spec.
|
// operator's batch-efficiency analytics queried the same tenant regardless
|
||||||
const ANALYSIS_TENANT_ID = 916;
|
// 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({
|
const batchEfficiencyMutation = useMutation({
|
||||||
mutationFn: fetchBatchEfficiency,
|
mutationFn: fetchBatchEfficiency,
|
||||||
@@ -1517,11 +1519,12 @@ const Dispatch = ({
|
|||||||
plannedMapRendererRef.current = L.canvas({ padding: 1.5, tolerance: 5 });
|
plannedMapRendererRef.current = L.canvas({ padding: 1.5, tolerance: 5 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pull the partners/getriderlogs feed for the currently selected hub + date.
|
// Live rider position feed. Was dead for a while during the backend
|
||||||
// This endpoint returns the exact live GPS position for every rider at the
|
// migration (fetchRidersLogs was a permanent [] stub) — now sourced from
|
||||||
// hub (latitude/longitude/logdate/status). We render those positions as
|
// GET /admin/milers/summary (confirmed in jupiter2doormile.md), which
|
||||||
// markers on the main dispatch map so the operator sees where each rider
|
// carries each rider's currentlatitude/currentlongitude/lastpingat. We
|
||||||
// actually is — matching the Reports → Riders Logs page.
|
// 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 RIDER_LOG_POLL_MS = 1000;
|
||||||
const { data: ridersLocationLogs } = useQuery({
|
const { data: ridersLocationLogs } = useQuery({
|
||||||
queryKey: [selectedAppLocationId, selectedDate, ''],
|
queryKey: [selectedAppLocationId, selectedDate, ''],
|
||||||
@@ -2197,17 +2200,20 @@ const Dispatch = ({
|
|||||||
queries: focusedRiderDeliveryIds.map((deliveryid) => ({
|
queries: focusedRiderDeliveryIds.map((deliveryid) => ({
|
||||||
queryKey: ['deliveryLogs', deliveryid],
|
queryKey: ['deliveryLogs', deliveryid],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await axios.get(
|
// GET /admin/consignments/:id/logs is confirmed (jupiter2doormile.md,
|
||||||
`${process.env.REACT_APP_URL3}/deliveries/getdeliverylogs/?deliveryid=${deliveryid}`
|
// Status: Done) as the GPS/status trail for one consignment — but
|
||||||
);
|
// this file's own `deliveryid` here has never been confirmed to
|
||||||
// Accept several possible response shapes — the live API has shipped
|
// actually BE a consignmentid (Dispatch.js's data source hasn't been
|
||||||
// {details:[…]}, plain arrays, and {data:[…]} variants over time, and
|
// audited against the new API the way orders.js/deliveries.js have).
|
||||||
// any of those should produce a polyline. Pick the first non-empty
|
// If it's really a bookingid, this 404s and rows stays empty — same
|
||||||
// array-like found.
|
// degraded-but-not-crashing behaviour as before.
|
||||||
const candidates = [res?.data?.details, res?.data?.data, res?.data, res];
|
let rows = [];
|
||||||
const rows = candidates.find((c) => Array.isArray(c)) || [];
|
try {
|
||||||
// Also accept lat/lng (alternate naming) in case the endpoint ever
|
rows = (await getConsignmentLogs(deliveryid)) || [];
|
||||||
// returns the same shape the front-end uses internally.
|
if (!Array.isArray(rows)) rows = [];
|
||||||
|
} catch (err) {
|
||||||
|
rows = [];
|
||||||
|
}
|
||||||
// Sort by logdate ascending so the polyline follows the rider's
|
// Sort by logdate ascending so the polyline follows the rider's
|
||||||
// chronological path. The endpoint isn't guaranteed to return rows
|
// chronological path. The endpoint isn't guaranteed to return rows
|
||||||
// in order — without this, consecutive points can be out of sequence
|
// in order — without this, consecutive points can be out of sequence
|
||||||
@@ -5121,14 +5127,14 @@ const Dispatch = ({
|
|||||||
{renderMarkers()}
|
{renderMarkers()}
|
||||||
{renderRoutes()}
|
{renderRoutes()}
|
||||||
|
|
||||||
{/* Live rider GPS markers from /partners/getriderlogs/. Mirrors the
|
{/* Live rider GPS markers from GET /admin/milers/summary (see
|
||||||
Reports → Riders Logs map: green pin when the rider's last log
|
fetchRidersLogs in api.js). Green pin when the rider's status
|
||||||
row is `active`, red otherwise, with the rider's username as a
|
is `active`, red otherwise, with the rider's username as a
|
||||||
label. Scoped to riders who actually have orders in the
|
label. Scoped to riders who actually have orders in the
|
||||||
currently selected slot — `riders` is derived from
|
currently selected slot — `riders` is derived from
|
||||||
filteredLiveRows so it already reflects the slot filter. A
|
filteredLiveRows so it already reflects the slot filter. A
|
||||||
rider with zero orders in the current slot is hidden, even if
|
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. */}
|
rider is focused, only that one is shown. */}
|
||||||
{liveRiderLocations
|
{liveRiderLocations
|
||||||
.filter((r) =>
|
.filter((r) =>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
import logo from 'assets/images/doormile-logo.png';
|
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 { OpenToast } from 'components/third-party/OpenToast';
|
||||||
import { closeGlobalToast, GlobalToast } from 'components/nearle_components/GlobalToast';
|
|
||||||
import { setLoginUser } from 'store/reducers/loginUserSlice';
|
import { setLoginUser } from 'store/reducers/loginUserSlice';
|
||||||
import { markSessionStart } from 'utils/session';
|
import { markSessionStart } from 'utils/session';
|
||||||
|
import { loginAdmin } from 'pages/api/doormileApi';
|
||||||
import { DT } from 'themes/dt/tokens';
|
import { DT } from 'themes/dt/tokens';
|
||||||
|
|
||||||
// Astryx design system — see themes/astryx.js for the Doormile brand theme
|
// 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 Login = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const fcmtoken = useSelector((state) => state.fcm);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
let navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [otp, setOtp] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [userinfo, setUserinfo] = useState({});
|
|
||||||
const [username, setUsername] = useState('');
|
|
||||||
const [passwordStatus, setPasswordStatus] = useState(0);
|
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
|
||||||
const [isPassword, setIspassword] = useState(false);
|
|
||||||
const [userid, setUserid] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
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');
|
navigate('/doormile/dispatch');
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loginsend = async () => {
|
const opentoast = (message, variant = 'error') => OpenToast(message, variant, 2000);
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
if (!username) {
|
const handleSubmit = async (e) => {
|
||||||
opentoast('Fill All required fields');
|
e.preventDefault();
|
||||||
setLoading(false);
|
if (!email || !password) {
|
||||||
|
opentoast('Enter your email and password');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await axios.post(`https://jupiter.nearle.app/live/api/v1/users/console/login`, {
|
const res = await loginAdmin(email, password);
|
||||||
authname: username,
|
if (res.success) {
|
||||||
configid: 9, // 9 -> config id for nearle console admin
|
const user = res.user || {};
|
||||||
userfcmtoken: fcmtoken?.token,
|
// Compatibility keys: existing pages/api.js read userid/roleid/tenantid
|
||||||
password
|
// straight from localStorage rather than through a context. 'authname'
|
||||||
});
|
// stays the AUTH_PRESENCE_KEY so the rest of the session contract
|
||||||
// user not found
|
// (useInactivityLogout, cross-tab logout, App.js gate) needs no changes.
|
||||||
if (res.data.code == 409 && !res.data.status) {
|
localStorage.setItem('authname', user.email || user.name || email);
|
||||||
OpenToast(res.data.message, 'error', 3000);
|
localStorage.setItem('firstname', user.name || '');
|
||||||
}
|
localStorage.setItem('userid', user.id ?? '');
|
||||||
// user not activated
|
localStorage.setItem('roleid', user.role ?? '');
|
||||||
else if (res.data.code == 403) {
|
localStorage.setItem('tenantid', user.tenantid ?? '');
|
||||||
OpenToast(res.data.message, 'warning', 3000);
|
dispatch(setLoginUser(user));
|
||||||
}
|
|
||||||
//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);
|
|
||||||
markSessionStart();
|
markSessionStart();
|
||||||
fetchAppLocations(userinfo.userid);
|
opentoast('Login successful', 'success');
|
||||||
navigate('/doormile/dispatch');
|
navigate('/doormile/dispatch');
|
||||||
} else {
|
} else {
|
||||||
OpenToast(res.data.message, 'error', 3000);
|
opentoast(res.message || 'Login failed');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
opentoast(err?.message || 'Login failed');
|
||||||
OpenToast(err.message, 'error', 5000);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
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 (
|
return (
|
||||||
<Theme theme={doormileTheme} mode="light">
|
<Theme theme={doormileTheme} mode="light">
|
||||||
@@ -266,75 +170,18 @@ const Login = () => {
|
|||||||
type="email"
|
type="email"
|
||||||
size="lg"
|
size="lg"
|
||||||
isRequired
|
isRequired
|
||||||
value={username}
|
value={email}
|
||||||
onChange={(value) => setUsername(value.toLocaleLowerCase())}
|
onChange={(value) => setEmail(value.toLocaleLowerCase())}
|
||||||
isDisabled={!!passwordStatus}
|
|
||||||
/>
|
/>
|
||||||
|
<TextInput
|
||||||
{/* Setup Password */}
|
label="Password"
|
||||||
{passwordStatus == 1 && (
|
type="password"
|
||||||
<VStack gap={4} padding={0}>
|
size="lg"
|
||||||
<Divider label="Setup Password" />
|
isRequired
|
||||||
<TextInput
|
value={password}
|
||||||
hasAutoFocus
|
onChange={(value) => setPassword(value)}
|
||||||
label="Enter New Password"
|
/>
|
||||||
type="password"
|
<Button label="Sign in" type="submit" variant="primary" size="lg" width="100%" />
|
||||||
size="lg"
|
|
||||||
isRequired
|
|
||||||
value={password}
|
|
||||||
onChange={(value) => setPassword(value)}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label="Re-Enter Password"
|
|
||||||
type="password"
|
|
||||||
size="lg"
|
|
||||||
isRequired
|
|
||||||
value={confirmPassword}
|
|
||||||
onChange={(value) => setConfirmPassword(value)}
|
|
||||||
status={
|
|
||||||
confirmPassword !== '' && password !== confirmPassword
|
|
||||||
? { type: 'error', message: 'Passwords do not match' }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</VStack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Enter Password */}
|
|
||||||
{passwordStatus == 2 && (
|
|
||||||
<VStack gap={4} padding={0}>
|
|
||||||
<Divider label="Enter Password" />
|
|
||||||
<TextInput
|
|
||||||
hasAutoFocus
|
|
||||||
label="Enter Password"
|
|
||||||
type="password"
|
|
||||||
size="lg"
|
|
||||||
isRequired
|
|
||||||
value={password}
|
|
||||||
onChange={(value) => setPassword(value)}
|
|
||||||
/>
|
|
||||||
</VStack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* OTP / retry */}
|
|
||||||
{isPassword && (
|
|
||||||
<VStack gap={1.5} padding={0}>
|
|
||||||
<HStack justify="between">
|
|
||||||
<Text type="label">Enter Password</Text>
|
|
||||||
<Link
|
|
||||||
onClick={() => {
|
|
||||||
setOtp('');
|
|
||||||
loginsend();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Retry
|
|
||||||
</Link>
|
|
||||||
</HStack>
|
|
||||||
<TextInput label="Password" type="password" value={password} onChange={(value) => setPassword(value)} isLabelHidden />
|
|
||||||
</VStack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button label="Continue" type="submit" variant="primary" size="lg" width="100%" />
|
|
||||||
</VStack>
|
</VStack>
|
||||||
</form>
|
</form>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
@@ -1,520 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
// import AuthWrapper from 'sections/auth/AuthWrapper';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Grid,
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
// CardHeader,
|
|
||||||
Stack,
|
|
||||||
// Divider,
|
|
||||||
// InputLabel,
|
|
||||||
// OutlinedInput,
|
|
||||||
TextField,
|
|
||||||
Button,
|
|
||||||
Typography,
|
|
||||||
CardHeader,
|
|
||||||
Container,
|
|
||||||
Link
|
|
||||||
} from '@mui/material';
|
|
||||||
import { useTheme } from '@mui/material/styles';
|
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
|
||||||
import AnimateButton from 'components/@extended/AnimateButton';
|
|
||||||
|
|
||||||
import logo from 'assets/images/doormile-logo.png';
|
|
||||||
|
|
||||||
// doormile-logo.png is a white asset; recolour it to brand red for this page's light background.
|
|
||||||
const DOORMILE_RED_FILTER =
|
|
||||||
'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)';
|
|
||||||
|
|
||||||
import axios from 'axios';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
// import { openSnackbar } from 'store/reducers/snackbar';
|
|
||||||
// import { useDispatch } from 'react-redux';
|
|
||||||
import Loader from 'components/Loader';
|
|
||||||
import { enqueueSnackbar } from 'notistack';
|
|
||||||
|
|
||||||
const Login = () => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
|
||||||
const [username, setUsername] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [alertmessage, setAlertmessage] = useState('');
|
|
||||||
const [checkusername, setCheckusername] = useState(false);
|
|
||||||
// const [toast, setToast] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
let navigate = useNavigate();
|
|
||||||
// let dispatch = useDispatch();
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
// let loginuserid = useSelector((state)=>state.logininfo);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
|
|
||||||
// if (alertmessage) {
|
|
||||||
// dispatch(
|
|
||||||
// openSnackbar({
|
|
||||||
// open: true,
|
|
||||||
// message: alertmessage,
|
|
||||||
// variant: 'alert',
|
|
||||||
// anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
|
||||||
// alert: {
|
|
||||||
// // variant:'info',
|
|
||||||
// color: 'error',
|
|
||||||
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }, [toast])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
localStorage.getItem('authname')
|
|
||||||
// || localStorage.getItem("appuserid")
|
|
||||||
) {
|
|
||||||
navigate('/deliveries');
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(alertmessage)
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const usernamecheck = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setUsername(e.target.value);
|
|
||||||
if (e.target.value) {
|
|
||||||
try {
|
|
||||||
// await axios.post(`${process.env.REACT_APP_URL}/auth/login`, {
|
|
||||||
// "authname": e.target.value
|
|
||||||
// })
|
|
||||||
await axios
|
|
||||||
.post(`${process.env.REACT_APP_URL}/users/login`, {
|
|
||||||
authname: e.target.value,
|
|
||||||
configid: 1
|
|
||||||
// "contactno": e.target.value,
|
|
||||||
// "password": 'admin'
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
console.log(res.data);
|
|
||||||
if (res.data.details.authname === e.target.value) {
|
|
||||||
setUsername(e.target.value);
|
|
||||||
setCheckusername(false);
|
|
||||||
} else {
|
|
||||||
setCheckusername(true);
|
|
||||||
}
|
|
||||||
// if (res.data.authname === e.target.value) {
|
|
||||||
|
|
||||||
// setUsername(e.target.value);
|
|
||||||
// setCheckusername(false);
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
// if (err.response.data.message === 'No user found') {
|
|
||||||
|
|
||||||
setCheckusername(true);
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loginsend = async () => {
|
|
||||||
// e.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
if (password && username) {
|
|
||||||
if (password == 'admin') {
|
|
||||||
setSubmitting(true);
|
|
||||||
try {
|
|
||||||
await axios
|
|
||||||
.post(`${process.env.REACT_APP_URL}/users/partner/login`, {
|
|
||||||
// "authname": username,
|
|
||||||
configid: 1,
|
|
||||||
contactno: username
|
|
||||||
|
|
||||||
// "password": password
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
console.log(res.data);
|
|
||||||
if (res.data.status) {
|
|
||||||
if (res.data.details.contactno === username) {
|
|
||||||
enqueueSnackbar('login Successfull', {
|
|
||||||
variant: 'success',
|
|
||||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
|
||||||
autoHideDuration: 3000
|
|
||||||
});
|
|
||||||
setUsername('');
|
|
||||||
setPassword('');
|
|
||||||
localStorage.setItem('firstname', res.data.details.tenantname);
|
|
||||||
localStorage.setItem('authname', res.data.details.authname);
|
|
||||||
|
|
||||||
// localStorage.setItem("appuserid", res.data.details.userid);
|
|
||||||
localStorage.setItem('roleid', res.data.details.roleid);
|
|
||||||
localStorage.setItem('tenantid', res.data.details.tenantid);
|
|
||||||
localStorage.setItem('partnerid', res.data.details.partnerid);
|
|
||||||
|
|
||||||
navigate('/orders');
|
|
||||||
setSubmitting(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(res.data.message);
|
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
|
|
||||||
// setAlertmessage('Invalid Data');
|
|
||||||
// if(err.message == 'Network Error'){
|
|
||||||
opentoast(err.message);
|
|
||||||
// }else{
|
|
||||||
// opentoast('Invalid Data');
|
|
||||||
|
|
||||||
// }
|
|
||||||
setLoading(false);
|
|
||||||
setSubmitting(false);
|
|
||||||
console.log(err.message);
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
setLoading(false);
|
|
||||||
setSubmitting(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
opentoast('Password is Incorrect');
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// let el2 = document.getElementById('toastid');
|
|
||||||
// el2.classList.add('d-block');
|
|
||||||
// el2.classList.remove('d-none');
|
|
||||||
setAlertmessage('Fill All required fields');
|
|
||||||
opentoast('Fill All required fields');
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// const handleClose = () => {
|
|
||||||
// setToast(false)
|
|
||||||
// }
|
|
||||||
const opentoast = (message) => {
|
|
||||||
// setToast(true)
|
|
||||||
|
|
||||||
// setTimeout(() => {
|
|
||||||
// // handleClose();
|
|
||||||
// setToast(false)
|
|
||||||
// }, 2000);
|
|
||||||
enqueueSnackbar(message, {
|
|
||||||
variant: 'error',
|
|
||||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
|
||||||
autoHideDuration: 2000
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* <AuthWrapper> */}
|
|
||||||
<Box sx={{ minHeight: '100vh' }}>
|
|
||||||
{loading && <Loader />}
|
|
||||||
{/* <AuthBackground /> */}
|
|
||||||
<Grid
|
|
||||||
container
|
|
||||||
direction="column"
|
|
||||||
justifyContent="flex-start"
|
|
||||||
sx={{
|
|
||||||
minHeight: '100vh'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs={12}
|
|
||||||
// sx={{ ml: 3, mt: 3 }}
|
|
||||||
sx={{ ml: { xs: 0, md: 3 }, mt: { xs: 3, md: 1 }, textAlign: { xs: 'center', md: 'left' } }}
|
|
||||||
>
|
|
||||||
<img src={logo} alt="Doormile" width={isMobile ? '160px' : '200px'} style={{ height: 'auto', filter: DOORMILE_RED_FILTER }} />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs={12}
|
|
||||||
container
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
// sx={{ minHeight: { xs: 'calc(100vh - 210px)', sm: 'calc(100vh - 134px)', md: 'calc(100vh - 112px)' } }}
|
|
||||||
sx={{ minHeight: { xs: 'calc(100vh - 210px)', sm: 'calc(100vh - 134px)', md: 'calc(100vh - 140px)' } }}
|
|
||||||
>
|
|
||||||
<Grid item>
|
|
||||||
{/* <AuthCard>{children}</AuthCard> */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: { xs: '100%', sm: 'auto' },
|
|
||||||
maxWidth: { xs: 400, lg: 475 },
|
|
||||||
margin: { xs: 2, sm: 2.5, md: 3 },
|
|
||||||
'& > *': {
|
|
||||||
flexGrow: 1,
|
|
||||||
flexBasis: '50%'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
sx={{
|
|
||||||
position: 'relative',
|
|
||||||
border: '1px solid',
|
|
||||||
borderRadius: { xs: 2, md: 1 },
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
boxShadow: { xs: '0 14px 40px rgba(15, 23, 42, 0.10)', md: 'inherit' },
|
|
||||||
p: { xs: 2, md: 2 },
|
|
||||||
width: '100%'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* <CardHeader title={<Typography variant="h4">Login</Typography>} /> */}
|
|
||||||
{/* <Divider sx={{ borderStyle: 'dashed' }} /> */}
|
|
||||||
{/* <h1>eee</h1> */}
|
|
||||||
{/* <CardHeader> */}
|
|
||||||
|
|
||||||
<Grid container spacing={3}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Stack direction="row" justifyContent="flex-start" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
|
|
||||||
<CardHeader title={<Typography variant="h3">Login</Typography>} />
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* <Grid item xs={12}>
|
|
||||||
<AuthLogin isDemo={isLoggedIn} />
|
|
||||||
</Grid> */}
|
|
||||||
</Grid>
|
|
||||||
<CardContent>
|
|
||||||
<form
|
|
||||||
noValidate
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
{/* <Stack spacing={1}> */}
|
|
||||||
{/* <InputLabel htmlFor="email-login">Email Address</InputLabel>
|
|
||||||
<OutlinedInput
|
|
||||||
id="email-login"
|
|
||||||
type="email"
|
|
||||||
value={values.email}
|
|
||||||
name="email"
|
|
||||||
onBlur={handleBlur}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="Enter email address"
|
|
||||||
fullWidth
|
|
||||||
id="username1"
|
|
||||||
label="E-mail Address"
|
|
||||||
variant="outlined"
|
|
||||||
autoComplete='email'
|
|
||||||
required
|
|
||||||
onChange={usernamecheck}
|
|
||||||
error={checkusername}
|
|
||||||
error={Boolean(touched.email && errors.email)}
|
|
||||||
/>
|
|
||||||
{touched.email && errors.email && (
|
|
||||||
<FormHelperText error id="standard-weight-helper-text-email-login">
|
|
||||||
{errors.email}
|
|
||||||
</FormHelperText>
|
|
||||||
)} */}
|
|
||||||
<TextField
|
|
||||||
margin="normal"
|
|
||||||
fullWidth
|
|
||||||
id="username1"
|
|
||||||
label="E-mail Address"
|
|
||||||
variant="outlined"
|
|
||||||
autoComplete="email"
|
|
||||||
required
|
|
||||||
onChange={usernamecheck}
|
|
||||||
error={checkusername}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
margin="normal"
|
|
||||||
fullWidth
|
|
||||||
required
|
|
||||||
autoComplete="current-password"
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
type="password"
|
|
||||||
id="password1"
|
|
||||||
label="Password"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
{/* </Stack> */}
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
{/* <Stack spacing={0}> */}
|
|
||||||
{/* <InputLabel htmlFor="password-login">Password</InputLabel> */}
|
|
||||||
{/* <OutlinedInput
|
|
||||||
fullWidth
|
|
||||||
// error={Boolean(touched.password && errors.password)}
|
|
||||||
// id="-password-login"
|
|
||||||
// type={showPassword ? 'text' : 'password'}
|
|
||||||
// value={values.password}
|
|
||||||
// name="password"
|
|
||||||
// onBlur={handleBlur}
|
|
||||||
// onChange={handleChange}
|
|
||||||
// endAdornment={
|
|
||||||
// <InputAdornment position="end">
|
|
||||||
// <IconButton
|
|
||||||
// aria-label="toggle password visibility"
|
|
||||||
// onClick={handleClickShowPassword}
|
|
||||||
// onMouseDown={handleMouseDownPassword}
|
|
||||||
// edge="end"
|
|
||||||
// color="secondary"
|
|
||||||
// >
|
|
||||||
// {showPassword ? <EyeOutlined /> : <EyeInvisibleOutlined />}
|
|
||||||
// </IconButton>
|
|
||||||
// </InputAdornment>
|
|
||||||
// }
|
|
||||||
placeholder="Enter password"
|
|
||||||
// margin="normal"
|
|
||||||
// fullWidth
|
|
||||||
required
|
|
||||||
autoComplete="current-password"
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
type='password' id="password1"
|
|
||||||
/> */}
|
|
||||||
{/* {touched.password && errors.password && (
|
|
||||||
<FormHelperText error id="standard-weight-helper-text-password-login">
|
|
||||||
{errors.password}
|
|
||||||
</FormHelperText>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{/* </Stack> */}
|
|
||||||
<Link href="#" variant="h6">
|
|
||||||
Forgot password?
|
|
||||||
</Link>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* <Grid item xs={12} sx={{ mt: -1 }}>
|
|
||||||
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={checked}
|
|
||||||
onChange={(event) => setChecked(event.target.checked)}
|
|
||||||
name="checked"
|
|
||||||
color="primary"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={<Typography variant="h6">Keep me sign in</Typography>}
|
|
||||||
/>
|
|
||||||
<Link variant="h6" component={RouterLink} to={isDemo ? '/auth/forgot-password' : '/forgot-password'} color="text.primary">
|
|
||||||
Forgot Password?
|
|
||||||
</Link>
|
|
||||||
</Stack>
|
|
||||||
</Grid> */}
|
|
||||||
{/* {errors.submit && (
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<FormHelperText error>{errors.submit}</FormHelperText>
|
|
||||||
</Grid>
|
|
||||||
)} */}
|
|
||||||
<Grid item xs={12}>
|
|
||||||
{/* <AnimateButton> */}
|
|
||||||
<AnimateButton>
|
|
||||||
<Button
|
|
||||||
disabled={submitting}
|
|
||||||
onClick={() => {
|
|
||||||
loginsend();
|
|
||||||
// navigate('/dashboard')
|
|
||||||
}}
|
|
||||||
fullWidth
|
|
||||||
size="large"
|
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</AnimateButton>
|
|
||||||
{/* </AnimateButton> */}
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
{/* </Grid> */}
|
|
||||||
</form>
|
|
||||||
</CardContent>
|
|
||||||
{/* </CardHeader> */}
|
|
||||||
</Card>
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs={12}
|
|
||||||
// sx={{ m: 3, mt: 1 }}
|
|
||||||
sx={{ mb: 1 }}
|
|
||||||
>
|
|
||||||
{/* <AuthFooter /> */}
|
|
||||||
|
|
||||||
<Container maxWidth="xl">
|
|
||||||
<Stack
|
|
||||||
direction={{ sx: 'column', md: 'row' }}
|
|
||||||
justifyContent={{ sx: 'center', md: 'space-between' }}
|
|
||||||
spacing={2}
|
|
||||||
// textAlign={{ sx: 'center', md: 'inherit' }}
|
|
||||||
|
|
||||||
alignItems={{ sx: 'center', md: 'inherit' }}
|
|
||||||
width="100%"
|
|
||||||
>
|
|
||||||
<Stack direction="row" justifyContent="center" spacing={1}>
|
|
||||||
<Typography variant="subtitle2" color="secondary" component="span" sx={{ display: 'flex' }}>
|
|
||||||
© All rights reserved
|
|
||||||
{/* <Typography variant="subtitle2" href="#mantis-privacy" target="_blank" underline="hover" sx={{ml:1}}>Privacy Policy</Typography> */}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack
|
|
||||||
direction={{ sx: 'column', md: 'row' }}
|
|
||||||
spacing={{ sx: 1, md: 3 }}
|
|
||||||
textAlign={{ sx: 'center', md: 'inherit' }}
|
|
||||||
alignItems={{ sx: 'center', md: 'inherit' }}
|
|
||||||
// width='100%'
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="subtitle2"
|
|
||||||
color="secondary"
|
|
||||||
component={Link}
|
|
||||||
href="#"
|
|
||||||
// target="_blank"
|
|
||||||
underline="hover"
|
|
||||||
textAlign="center"
|
|
||||||
>
|
|
||||||
Terms and Conditions
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="subtitle2"
|
|
||||||
color="secondary"
|
|
||||||
component={Link}
|
|
||||||
href="#"
|
|
||||||
// target="_blank"
|
|
||||||
underline="hover"
|
|
||||||
textAlign="center"
|
|
||||||
>
|
|
||||||
Privacy Policy
|
|
||||||
</Typography>
|
|
||||||
{/* <Typography
|
|
||||||
variant="subtitle2"
|
|
||||||
color="secondary"
|
|
||||||
component={Link}
|
|
||||||
href="#"
|
|
||||||
// target="_blank"
|
|
||||||
underline="hover"
|
|
||||||
textAlign='center'
|
|
||||||
>
|
|
||||||
CA Privacy Notice
|
|
||||||
</Typography> */}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
{/* </AuthWrapper> */}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Login1;
|
|
||||||
@@ -45,7 +45,7 @@ import { useNavigate } from 'react-router';
|
|||||||
import { MdLocationCity } from 'react-icons/md';
|
import { MdLocationCity } from 'react-icons/md';
|
||||||
import { TbMapPinCode } from 'react-icons/tb';
|
import { TbMapPinCode } from 'react-icons/tb';
|
||||||
import { FaLocationDot } from 'react-icons/fa6';
|
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 { useTheme } from '@mui/material/styles';
|
||||||
import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
|
import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
@@ -127,9 +127,7 @@ const SectionHeader = ({ color, icon, title, subtitle, action }) => (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack direction="row" alignItems="center" spacing={1.25}>
|
<Stack direction="row" alignItems="center" spacing={1.25}>
|
||||||
<Avatar sx={{ width: 32, height: 32, bgcolor: color, color: '#fff', boxShadow: `0 6px 18px ${ring(color)}` }}>
|
<Avatar sx={{ width: 32, height: 32, bgcolor: color, color: '#fff', boxShadow: `0 6px 18px ${ring(color)}` }}>{icon}</Avatar>
|
||||||
{icon}
|
|
||||||
</Avatar>
|
|
||||||
<Stack>
|
<Stack>
|
||||||
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 14, lineHeight: 1.1 }}>{title}</Typography>
|
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 14, lineHeight: 1.1 }}>{title}</Typography>
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
@@ -234,7 +232,6 @@ const Createorder1 = () => {
|
|||||||
{ label: '12 Angry Men', year: 1957 }
|
{ label: '12 Angry Men', year: 1957 }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
const appId = localStorage.getItem('applocationid');
|
const appId = localStorage.getItem('applocationid');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [open, setOpen] = useState({});
|
const [open, setOpen] = useState({});
|
||||||
@@ -340,11 +337,13 @@ const Createorder1 = () => {
|
|||||||
|
|
||||||
const fetchTenantPricing = async () => {
|
const fetchTenantPricing = async () => {
|
||||||
try {
|
try {
|
||||||
const pricingResponse = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${tid}`);
|
const pricing = (await getAdminPricing()) || [];
|
||||||
console.log('pricingResponse', pricingResponse.data.details);
|
const match = pricing.find((p) => p.tenantid === tid);
|
||||||
setBasePrice(pricingResponse.data.details.baseprice);
|
if (match) {
|
||||||
setPricePerKm(pricingResponse.data.details.priceperkm);
|
setBasePrice(match.baseprice);
|
||||||
setMinKm(pricingResponse.data.details.minkm);
|
setPricePerKm(match.priceperkm);
|
||||||
|
setMinKm(match.basedistance);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('fetchTenantPricing error', error);
|
console.log('fetchTenantPricing error', error);
|
||||||
}
|
}
|
||||||
@@ -450,109 +449,65 @@ const Createorder1 = () => {
|
|||||||
}
|
}
|
||||||
}, [searchword]);
|
}, [searchword]);
|
||||||
|
|
||||||
|
|
||||||
// ==================================================== || fetchtenantinfo || ====================================================
|
// ==================================================== || fetchtenantinfo || ====================================================
|
||||||
const fetchtenantinfo = async () => {
|
const fetchtenantinfo = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
console.log('tid', tid);
|
try {
|
||||||
|
const info = await getAdminTenant(tid);
|
||||||
await axios
|
if (info) {
|
||||||
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
|
setTenant(info);
|
||||||
.then((res) => {
|
setTenantid(info.tenantid);
|
||||||
console.log('fetchtenantinfo', res);
|
fetchAppAdminTokens();
|
||||||
if (res.data.status) {
|
setSubCatId(info.subcategoryid);
|
||||||
setTenant(res.data.details);
|
}
|
||||||
setTenantid(res.data.details.tenantid);
|
} catch (err) {
|
||||||
fetchAppAdminTokens();
|
console.log(err);
|
||||||
setSubCatId(res.data.details.subcategoryid);
|
} finally {
|
||||||
}
|
setLoading(false);
|
||||||
setLoading(false);
|
}
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchtenantinfo();
|
fetchtenantinfo();
|
||||||
}, []);
|
}, []);
|
||||||
const getsubcategories = async () => {
|
// No item-subcategory lookup endpoint in the new API.
|
||||||
await axios
|
const getsubcategories = async () => setSubCat([]);
|
||||||
.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([]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getsubcategories();
|
getsubcategories();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ==================================================== || fetchTiming || ====================================================
|
// ==================================================== || 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 () => {
|
const fetchTiming = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await axios
|
const opentime = '09:00:00';
|
||||||
.get(`${process.env.REACT_APP_URL}/utils/getapplocations/?applocationid=${appId}`)
|
const closetime = '21:00:00';
|
||||||
.then((res) => {
|
setAppLocaLat(0);
|
||||||
console.log('fetchTiming', res);
|
setAppLocaLng(0);
|
||||||
const { opentime, closetime, latitude, longitude, radius } = res.data.details[0];
|
setAppLocaRadius(100);
|
||||||
if (res.data.status) {
|
setStatrttime(`${dayjs().format('MM-DD-YYYY')} ${opentime}`);
|
||||||
setAppLocaLat(latitude);
|
setEndtime(`${dayjs().format('MM-DD-YYYY')} ${closetime}`);
|
||||||
setAppLocaLng(longitude);
|
let arr = [];
|
||||||
setAppLocaRadius(radius);
|
for (
|
||||||
setStatrttime(`${dayjs().format('MM-DD-YYYY')} ${opentime}`);
|
let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`;
|
||||||
setEndtime(`${dayjs().format('MM-DD-YYYY')} ${closetime}`);
|
dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0;
|
||||||
console.log('starttime', `${dayjs().format('MM-DD-YYYY')} ${opentime}`);
|
i = dayjs(i).add(30, 'm')
|
||||||
console.log('endtime', `${dayjs().format('MM-DD-YYYY')} ${closetime} `);
|
) {
|
||||||
let arr = [];
|
arr.push(i);
|
||||||
for (
|
}
|
||||||
let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`, j = 0;
|
setTimeslotarr(arr);
|
||||||
dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0;
|
setLoading(false);
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
fetchTiming();
|
fetchTiming();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// =============================================== || fetchAppAdminTokens (via appId) || ===============================================
|
// =============================================== || fetchAppAdminTokens (via appId) || ===============================================
|
||||||
const fetchAppAdminTokens = async () => {
|
// No zone-admin-token lookup endpoint in the new API.
|
||||||
setLoading(true);
|
const fetchAppAdminTokens = async () => setAdmintoken([]);
|
||||||
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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (starttime && endtime) {
|
if (starttime && endtime) {
|
||||||
@@ -582,116 +537,47 @@ const Createorder1 = () => {
|
|||||||
// =============================================== || createsubmitobj1 (create orders) || ===============================================
|
// =============================================== || createsubmitobj1 (create orders) || ===============================================
|
||||||
|
|
||||||
const createsubmitobj2 = async () => {
|
const createsubmitobj2 = async () => {
|
||||||
let arr = {};
|
// POST /admin/expressbooking's schema is a single flat booking object
|
||||||
arr = {
|
// (tenantid, pickuplocationid, customer_phone/name, delivery* fields,
|
||||||
orders: {
|
// service_option, finalprice, notes, parcels[]) — nothing like the old
|
||||||
applocationid: tenant.applolcationid,
|
// pickup/drop/orders triple. The drop contact becomes the booking's
|
||||||
cancellled: '',
|
// customer (new API models the delivery recipient as "the customer");
|
||||||
categoryid: +tenant.categoryid,
|
// the pickup side maps to pickuplocationid when a saved location was
|
||||||
configid: 7,
|
// picked (tenanatLocoId/isLocation, set by the "Business Location"
|
||||||
customerid: isNumChange1 == 0 ? +pickCust.customerid || 0 : 0,
|
// Autocomplete below — pickCust.deliverylocationid was always '' and
|
||||||
deliveryaddress: dropCust.address || '',
|
// never actually got wired to the picker, so this shortcut never fired),
|
||||||
deliverycharge: +totalCharge.toFixed(2) || 0,
|
// otherwise raw pickup* fields following the doc's own delivery*/pickup*
|
||||||
deliverycity: dropCust.city || '',
|
// naming symmetry (pickupaddress/pickuppincode/pickupcity/pickuplatitude/
|
||||||
deliverycontactno: dropCust.contactno || '',
|
// pickuplongitude — NOT shown in the doc's example, so unverified against
|
||||||
deliverycustomer: dropCust.firstname || '',
|
// the live API).
|
||||||
deliveryid: isNumChange2 == 0 ? +dropCust.customerid || 0 : 0,
|
const arr = {
|
||||||
deliverylandmark: dropCust.landmark || '',
|
tenantid: tenant.tenantid,
|
||||||
deliverylat: dropCust.latitude.toString(),
|
...(isLocation && tenanatLocoId
|
||||||
deliverylocation: dropCust.suburb || '',
|
? { pickuplocationid: tenanatLocoId }
|
||||||
deliverylocationid: dropCust.deliverylocationid || 0,
|
: {
|
||||||
deliverylong: dropCust.longitude.toString(),
|
pickupaddress: pickCust.address || '',
|
||||||
deliverytime: `${dayjs(startdate).format('YYYY-MM-DD')} ${dayjs(selectedtime.$d).format('HH:mm:ss')}`,
|
pickuppincode: pickCust.postcode || '',
|
||||||
deliverytype: pickCust.customerid !== 0 || dropCust.customerid !== 0 ? 'B' : 'C',
|
pickupcity: pickCust.city || '',
|
||||||
delivered: '',
|
pickuplatitude: Number(pickCust.latitude) || 0,
|
||||||
itemcount: 1,
|
pickuplongitude: Number(pickCust.longitude) || 0
|
||||||
kms: distance.toString() || 0,
|
}),
|
||||||
locationid: +tenanatLocoId, //main or branch
|
customer_phone: dropCust.contactno || '',
|
||||||
moduleid: +tenant.moduleid,
|
customer_name: dropCust.firstname || '',
|
||||||
orderamount: +totalCharge.toFixed(2) || 0,
|
deliveryaddress: dropCust.address || '',
|
||||||
ordercharges: 0.0,
|
deliverypincode: dropCust.postcode || '',
|
||||||
orderdate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
deliverycity: dropCust.city || '',
|
||||||
orderheaderid: 0,
|
deliverylatitude: Number(dropCust.latitude) || 0,
|
||||||
orderid: '', //
|
deliverylongitude: Number(dropCust.longitude) || 0,
|
||||||
ordernotes: otherinstructions,
|
service_option: 'Normal',
|
||||||
orderstatus: 'created',
|
finalprice: +totalCharge.toFixed(2) || 0,
|
||||||
ordervalue: +totalCharge.toFixed(2) || 0,
|
notes: otherinstructions || '',
|
||||||
partnerid: tenant.partnerid,
|
parcels: [
|
||||||
paymentstatus: 1,
|
{
|
||||||
paymenttype: 42,
|
itemcategory: subCat.find((c) => c.subcategoryid === subCatId)?.categoryname || 'General',
|
||||||
pending: '',
|
itemdescription: 'Order',
|
||||||
pickupaddress: pickCust.address || '',
|
declaredvalue: +totalCharge.toFixed(2) || 0
|
||||||
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
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
console.log('createsubmitobj2', arr);
|
console.log('createsubmitobj2', arr);
|
||||||
if (!pickCust.firstname) {
|
if (!pickCust.firstname) {
|
||||||
@@ -727,29 +613,28 @@ const Createorder1 = () => {
|
|||||||
} else if (!setSubCatId) {
|
} else if (!setSubCatId) {
|
||||||
opentoast('Choose SubCategory ', 'warning', 2000);
|
opentoast('Choose SubCategory ', 'warning', 2000);
|
||||||
} else {
|
} else {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const createRes = await axios.post(`${process.env.REACT_APP_URL2}/orders/createorder`, arr);
|
const createRes = await createExpressBooking(arr);
|
||||||
// const createRes = await axios.post(`${process.env.REACT_APP_URL}/orders/createorder`, arr);
|
if (createRes.success) {
|
||||||
if (createRes.data.status) {
|
|
||||||
console.log('createRes', createRes);
|
|
||||||
enqueueSnackbar('Order Created Successfully', {
|
enqueueSnackbar('Order Created Successfully', {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||||
autoHideDuration: 1000
|
autoHideDuration: 1000
|
||||||
});
|
});
|
||||||
if (admintoken) {
|
|
||||||
notifyadmin(admintoken);
|
|
||||||
// sendnotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate('/nearle/orders');
|
navigate('/nearle/orders');
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.log('createResErr', 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 () => {
|
const clientdetails = async () => {
|
||||||
setLoading2(true);
|
setLoading2(true);
|
||||||
try {
|
try {
|
||||||
let url =
|
const customers = (await getTenantCustomers()) || [];
|
||||||
searchCustList == ''
|
const filtered = searchCustList
|
||||||
? // ? `${process.env.REACT_APP_URL}/customers/getbytid/?tenantid=${tid}&pageno=1&pagesize=1`
|
? customers.filter(
|
||||||
`${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tid}&pageno=1&pagesize=20`
|
(val) =>
|
||||||
: `${process.env.REACT_APP_URL}/customers/search/?tenantid=${tid}&keyword=${searchCustList}`;
|
val.firstname?.toLowerCase().includes(searchCustList.toLowerCase()) ||
|
||||||
|
val.phone?.includes(searchCustList) ||
|
||||||
await axios
|
val.contactno?.includes(searchCustList)
|
||||||
.get(url)
|
)
|
||||||
.then((res) => {
|
: customers;
|
||||||
console.log('clientdetails', res.data.details);
|
setClientdetail(filtered);
|
||||||
if (res.data.status && res.data.details) {
|
setCustomerlist(filtered);
|
||||||
setClientdetail(res.data.details || []);
|
setClientdetailarr(filtered.map((val) => ({ label: `${val.firstname} | ${val.phone || val.contactno}`, ...val })));
|
||||||
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');
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
opentoast('server error', 'warning');
|
||||||
|
} finally {
|
||||||
setLoading2(false);
|
setLoading2(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -821,74 +687,21 @@ const Createorder1 = () => {
|
|||||||
const clientdetailsbusiness = async () => {
|
const clientdetailsbusiness = async () => {
|
||||||
setLoading2(true);
|
setLoading2(true);
|
||||||
try {
|
try {
|
||||||
await axios
|
const customers = (await getTenantCustomers()) || [];
|
||||||
.get(`${process.env.REACT_APP_URL}/customers/getbytid/?tenantid=${tid}&locationid=1`)
|
setClientdetail(customers);
|
||||||
.then((res) => {
|
setClientdetailbusinessarr(customers.map((val) => ({ label: `${val.firstname} | ${val.phone || val.contactno}`, ...val })));
|
||||||
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');
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
opentoast('server error', 'warning');
|
||||||
|
} finally {
|
||||||
setLoading2(false);
|
setLoading2(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ================================================== || sendnotifications || ==================================================
|
// ================================================== || sendnotifications || ==================================================
|
||||||
const sendnotifications = async () => {
|
// No bulk-notify endpoint in the new API — never actually called (dead in
|
||||||
setLoading(true);
|
// the original code too, only referenced from a commented-out call site).
|
||||||
await axios
|
const sendnotifications = async () => {};
|
||||||
.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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
// ============================================= || Address Autocomplete (pick) || =============================================
|
// ============================================= || Address Autocomplete (pick) || =============================================
|
||||||
const handlePickPlaceSelected = (place) => {
|
const handlePickPlaceSelected = (place) => {
|
||||||
setInputValue2(`${place.name}, ${place.formatted_address}`);
|
setInputValue2(`${place.name}, ${place.formatted_address}`);
|
||||||
@@ -1015,16 +828,11 @@ const Createorder1 = () => {
|
|||||||
// ============================================= || gettenantlocations (branches) || =============================================
|
// ============================================= || gettenantlocations (branches) || =============================================
|
||||||
const gettenantlocations = async () => {
|
const gettenantlocations = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${tid}`);
|
const locations = (await getTenantLocations(tid)) || [];
|
||||||
console.log('gettenantlocations', res.data.details);
|
setTenantlocations(locations);
|
||||||
if (res.data && res.data.details) {
|
if (locations.length == 1) {
|
||||||
setTenantlocations(res.data.details);
|
setIsLocation(true);
|
||||||
if (res.data.details.length == 1) {
|
setTenanatLocoId(locations[0].locationid);
|
||||||
setIsLocation(true);
|
|
||||||
setTenanatLocoId(res.data.details[0].locationid);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setTenantlocations([]);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('gettenantlocations', err);
|
console.log('gettenantlocations', err);
|
||||||
@@ -1124,9 +932,7 @@ const Createorder1 = () => {
|
|||||||
) : (
|
) : (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={tenantLocations || []}
|
options={tenantLocations || []}
|
||||||
getOptionLabel={(option) =>
|
getOptionLabel={(option) => (option && option.locationname ? `${option.locationname} (${option.suburb || ''})` : '')}
|
||||||
option && option.locationname ? `${option.locationname} (${option.suburb || ''})` : ''
|
|
||||||
}
|
|
||||||
isOptionEqualToValue={(option, value) => option?.locationid === value?.locationid}
|
isOptionEqualToValue={(option, value) => option?.locationid === value?.locationid}
|
||||||
PaperComponent={SoftPaper}
|
PaperComponent={SoftPaper}
|
||||||
sx={{ width: { xs: '100%', sm: 300 } }}
|
sx={{ width: { xs: '100%', sm: 300 } }}
|
||||||
@@ -1926,16 +1732,13 @@ const Createorder1 = () => {
|
|||||||
{/* ================================================= || Time || ================================================= */}
|
{/* ================================================= || Time || ================================================= */}
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<SectionCard sx={{ mt: 2, minHeight: 390 }}>
|
<SectionCard sx={{ mt: 2, minHeight: 390 }}>
|
||||||
<SectionHeader
|
<SectionHeader color="#f59e0b" icon={<MdSchedule size={16} />} title="Schedule" subtitle="Pickup date & time slot" />
|
||||||
color="#f59e0b"
|
|
||||||
icon={<MdSchedule size={16} />}
|
|
||||||
title="Schedule"
|
|
||||||
subtitle="Pickup date & time slot"
|
|
||||||
/>
|
|
||||||
<Box sx={{ p: { xs: 2, md: 2.5 } }}>
|
<Box sx={{ p: { xs: 2, md: 2.5 } }}>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textSecondary, mb: 1 }}>Date</Typography>
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textSecondary, mb: 1 }}>
|
||||||
|
Date
|
||||||
|
</Typography>
|
||||||
<LocalizationProvider dateAdapter={AdapterDayjs} sx={{}}>
|
<LocalizationProvider dateAdapter={AdapterDayjs} sx={{}}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
format="DD-MM-YYYY"
|
format="DD-MM-YYYY"
|
||||||
@@ -2006,7 +1809,8 @@ const Createorder1 = () => {
|
|||||||
<Stack direction="row" flexWrap="wrap" gap={0.75} useFlexGap>
|
<Stack direction="row" flexWrap="wrap" gap={0.75} useFlexGap>
|
||||||
{timeslotarr.map((val, index) => {
|
{timeslotarr.map((val, index) => {
|
||||||
if (
|
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');
|
const active = dayjs(selectedtime).format('HH:mm') == dayjs(val).format('HH:mm');
|
||||||
return (
|
return (
|
||||||
@@ -2082,7 +1886,9 @@ const Createorder1 = () => {
|
|||||||
getOptionLabel={(option) => `${option.subcategoryname}` || ''}
|
getOptionLabel={(option) => `${option.subcategoryname}` || ''}
|
||||||
sx={{ my: 2, zIndex: '100' }}
|
sx={{ my: 2, zIndex: '100' }}
|
||||||
fullWidth
|
fullWidth
|
||||||
renderInput={(params) => <TextField {...params} label={subCatName == '' ? tenant.subcategoryname : subCatName} />}
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} label={subCatName == '' ? tenant.subcategoryname : subCatName} />
|
||||||
|
)}
|
||||||
onChange={(event, value, reason) => {
|
onChange={(event, value, reason) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
console.log(value);
|
console.log(value);
|
||||||
@@ -2416,14 +2222,16 @@ const Createorder1 = () => {
|
|||||||
'&:hover': isUsed
|
'&:hover': isUsed
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
borderColor: edge(BRAND),
|
borderColor: edge(BRAND),
|
||||||
bgcolor: tint(BRAND),
|
bgcolor: tint(BRAND),
|
||||||
boxShadow: DT.shadowSoft
|
boxShadow: DT.shadowSoft
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar sx={{ width: 36, height: 36, bgcolor: soft(BRAND), color: BRAND, fontWeight: 800 }}>
|
<Avatar sx={{ width: 36, height: 36, bgcolor: soft(BRAND), color: BRAND, fontWeight: 800 }}>
|
||||||
{String(address.firstname || '?').charAt(0).toUpperCase()}
|
{String(address.firstname || '?')
|
||||||
|
.charAt(0)
|
||||||
|
.toUpperCase()}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Stack sx={{ flex: 1, minWidth: 0 }}>
|
<Stack sx={{ flex: 1, minWidth: 0 }}>
|
||||||
<Stack direction="row" alignItems="center" spacing={0.75} flexWrap="wrap" useFlexGap>
|
<Stack direction="row" alignItems="center" spacing={0.75} flexWrap="wrap" useFlexGap>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ import { useState, useEffect, Fragment, useRef } from 'react';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
var utc = require('dayjs/plugin/utc');
|
var utc = require('dayjs/plugin/utc');
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -23,17 +22,14 @@ import {
|
|||||||
Dialog,
|
Dialog,
|
||||||
TableRow,
|
TableRow,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogActions,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
InputBase,
|
InputBase,
|
||||||
Backdrop,
|
Backdrop,
|
||||||
SpeedDial,
|
TableContainer
|
||||||
SpeedDialIcon,
|
|
||||||
SpeedDialAction,
|
|
||||||
Badge,
|
|
||||||
TableContainer,
|
|
||||||
Checkbox
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
MdAccessTime,
|
MdAccessTime,
|
||||||
@@ -51,7 +47,8 @@ import {
|
|||||||
MdCalendarMonth,
|
MdCalendarMonth,
|
||||||
MdReceiptLong,
|
MdReceiptLong,
|
||||||
MdClear,
|
MdClear,
|
||||||
MdNotes
|
MdNotes,
|
||||||
|
MdMyLocation
|
||||||
} from 'react-icons/md';
|
} from 'react-icons/md';
|
||||||
import { DeleteOutlined } from '@ant-design/icons';
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
@@ -60,13 +57,8 @@ import CircularLoader from 'components/CircularLoader';
|
|||||||
import AiImage from '../../../assets/images/aiImage.png';
|
import AiImage from '../../../assets/images/aiImage.png';
|
||||||
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
|
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import {
|
import { fetchPercentageData, createAutomationDeliveries, getallriders, fetchorderscount } from '../../api/api';
|
||||||
fetchPercentageData,
|
import { getBookings, cancelBooking, getAdminCustomers, getBookingTrack } from 'pages/api/doormileApi';
|
||||||
createAutomationDeliveries,
|
|
||||||
cancelMultipleOrder,
|
|
||||||
getallriders,
|
|
||||||
fetchorderscount
|
|
||||||
} from '../../api/api';
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Design tokens — shared with the rest of the redesigned operator pages.
|
// Design tokens — shared with the rest of the redesigned operator pages.
|
||||||
@@ -99,29 +91,37 @@ const dtEdge = edge;
|
|||||||
const BRAND = '#C01227';
|
const BRAND = '#C01227';
|
||||||
const BRAND_LIGHT = '#D25463';
|
const BRAND_LIGHT = '#D25463';
|
||||||
|
|
||||||
|
// Semantic per-row status palette, keyed by GET /admin/bookings' real `status`
|
||||||
// Semantic per-row status palette — colors per brand standard:
|
// enum (lowercased — StatusBadge lowercases before lookup). Only
|
||||||
// green=delivered, amber=pending, blue=created/processing, red=cancelled,
|
// pending_pickup and converted_to_consignment have been observed against the
|
||||||
// dark-red=failed, purple=on-hold.
|
// 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 = {
|
const ROW_STATUS_META = {
|
||||||
created: { label: 'Created', color: '#3b82f6', icon: MdLocalShipping },
|
pending_pickup: { label: 'Pending Pickup', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||||
pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
pending_assignment: { label: 'Pending Assignment', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||||
processing: { label: 'Processing', color: '#3b82f6', icon: MdAccessTime },
|
converted_to_consignment: { label: 'Converted', color: '#6366f1', icon: MdCheckCircle },
|
||||||
modified: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle },
|
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
|
||||||
confirmed: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle },
|
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
|
||||||
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 }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Top-level pill tabs.
|
// Top-level pill tabs. GET /admin/bookings has no documented status filter
|
||||||
const ORDERS_STATUS_TABS = [
|
// param, so this tab doesn't narrow the server query — it's cosmetic until
|
||||||
{ idx: 0, status: 'created', label: 'Created', color: BRAND, icon: MdLocalShipping, countKey: 'created' }
|
// 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 StatusBadge = ({ status }) => {
|
||||||
const meta = ROW_STATUS_META[String(status || '').toLowerCase()] || {
|
const meta = ROW_STATUS_META[String(status || '').toLowerCase()] || {
|
||||||
@@ -201,6 +201,16 @@ const Orders = () => {
|
|||||||
const [tabstatus, setTabstatus] = useState('Created');
|
const [tabstatus, setTabstatus] = useState('Created');
|
||||||
const [currentStatus, setCurrentStatus] = useState('created');
|
const [currentStatus, setCurrentStatus] = useState('created');
|
||||||
const [cancelOpen, setCancelOpen] = useState(false);
|
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 [orderheaderid, setOrderheaderid] = useState('');
|
||||||
const locationId = 0;
|
const locationId = 0;
|
||||||
const locoName = 'All Locations';
|
const locoName = 'All Locations';
|
||||||
@@ -213,10 +223,8 @@ const Orders = () => {
|
|||||||
|
|
||||||
// Floating button and Dialog states
|
// Floating button and Dialog states
|
||||||
const [speedDialOpen, setSpeedDialOpen] = useState(false);
|
const [speedDialOpen, setSpeedDialOpen] = useState(false);
|
||||||
const [multiDeleteDialog, setMultiDeleteDialog] = useState(false);
|
|
||||||
const [createloader, setCreateloader] = useState(false);
|
const [createloader, setCreateloader] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [deliverylist, setDeliverylist] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = setTimeout(() => {
|
const handler = setTimeout(() => {
|
||||||
@@ -247,10 +255,7 @@ const Orders = () => {
|
|||||||
refetchInterval: 15000
|
refetchInterval: 15000
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const { data: ordersCountData, refetch: orderscountRefetch } = useQuery({
|
||||||
data: ordersCountData,
|
|
||||||
refetch: orderscountRefetch
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ['ordersCount', locationId, startdate, enddate, currentStatus, tid, locationId],
|
queryKey: ['ordersCount', locationId, startdate, enddate, currentStatus, tid, locationId],
|
||||||
queryFn: fetchorderscount,
|
queryFn: fetchorderscount,
|
||||||
refetchOnMount: true,
|
refetchOnMount: true,
|
||||||
@@ -258,47 +263,69 @@ const Orders = () => {
|
|||||||
refetchInterval: 15000
|
refetchInterval: 15000
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const { data: autoRiders } = useQuery({
|
||||||
data: autoRiders
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ['getallriders'],
|
queryKey: ['getallriders'],
|
||||||
queryFn: getallriders,
|
queryFn: getallriders,
|
||||||
refetchOnMount: true,
|
refetchOnMount: true,
|
||||||
refetchOnWindowFocus: true
|
refetchOnWindowFocus: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const cancelorder = async () => {
|
// GET /admin/bookings only carries `appcustomerid` — no name/phone on the
|
||||||
await axios
|
// booking itself — so customer display fields are joined client-side
|
||||||
.put(`${process.env.REACT_APP_URL}/orders/updateorder`, {
|
// against GET /admin/customers. Response field names for that endpoint
|
||||||
orderheaderid: orderheaderid,
|
// aren't documented; firstname/phone are guessed by symmetry with
|
||||||
orderstatus: 'cancelled',
|
// tenantcustomers and fall back to showing the raw id.
|
||||||
cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
const { data: customerMap } = useQuery({
|
||||||
})
|
queryKey: ['adminCustomersMap'],
|
||||||
.then((res) => {
|
queryFn: async () => {
|
||||||
if (res.data.status) {
|
const customers = (await getAdminCustomers()) || [];
|
||||||
enqueueSnackbar('Order Cancelled Successfully', {
|
const map = new Map();
|
||||||
variant: 'success',
|
customers.forEach((c) => {
|
||||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
const id = c.appcustomerid ?? c.customerid ?? c.id;
|
||||||
autoHideDuration: 2000
|
if (id != null) map.set(id, c);
|
||||||
});
|
|
||||||
refetchOrders();
|
|
||||||
orderscountRefetch();
|
|
||||||
percentagedataRefetch();
|
|
||||||
setCancelOpen(false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
});
|
});
|
||||||
|
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 fetchOrders = async ({ pageParam = 1 }) => {
|
||||||
const res = await axios.get(
|
const data = (await getBookings(pageParam, rowsPerPage)) || [];
|
||||||
`${process.env.REACT_APP_URL}/orders/tenant/getorders/?tenantid=${tid}&locationid=${locationId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${debouncedSearch}`
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
data: res.data.details,
|
data,
|
||||||
nextPage: res.data.details.length === rowsPerPage ? pageParam + 1 : undefined
|
nextPage: data.length === rowsPerPage ? pageParam + 1 : undefined
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -352,7 +379,11 @@ const Orders = () => {
|
|||||||
const createDeliveryMutation = useMutation({
|
const createDeliveryMutation = useMutation({
|
||||||
mutationFn: createAutomationDeliveries,
|
mutationFn: createAutomationDeliveries,
|
||||||
onSuccess: (data, variables) => {
|
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();
|
orderscountRefetch();
|
||||||
refetchOrders();
|
refetchOrders();
|
||||||
setCreateloader(false);
|
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 () => {
|
const handleCreateDelivery = async () => {
|
||||||
if (rows.length === 0) return;
|
if (rows.length === 0) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setCreateloader(true);
|
setCreateloader(true);
|
||||||
const deliveryData = rows.map((val) => ({
|
// The optimiser service (routes.workolik.com) itself is untouched, but the
|
||||||
...val,
|
// fields sourced here have to match GET /admin/bookings' real shape —
|
||||||
deliveryid: 0,
|
// deliverylat/pickuplat/deliverycharge/etc. don't exist on a booking
|
||||||
deliverydate: dayjs(val.deliverydate).utc().format('YYYY-MM-DD HH:mm:ss'),
|
// object (see pickuplatitude/deliverylatitude/serviceoptions[0].estimatedprice
|
||||||
assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
// instead). The solver's own expected input contract is unverified since
|
||||||
orderstatus: 'pending',
|
// it's a separate, unchanged service.
|
||||||
orderamount: val.deliverycharge,
|
const deliveryData = rows.map((val) => {
|
||||||
droplat: val.deliverylat,
|
const charge = val.serviceoptions?.[0]?.estimatedprice;
|
||||||
droplon: val.deliverylong,
|
return {
|
||||||
pickuplat: val.pickuplat,
|
...val,
|
||||||
pickuplon: val.pickuplong,
|
deliveryid: 0,
|
||||||
ordernotes: val.ordernotes,
|
deliverydate: dayjs(val.serviceoptions?.[0]?.estimateddeliveryat || val.createdat)
|
||||||
deliverycharges: val.deliverycharge,
|
.utc()
|
||||||
pickuplocation: val.pickupsuburb,
|
.format('YYYY-MM-DD HH:mm:ss'),
|
||||||
deliverylocation: val.deliverysuburb
|
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({
|
createDeliveryMutation.mutate({
|
||||||
deliveries: deliveryData,
|
deliveries: deliveryData,
|
||||||
@@ -427,8 +452,22 @@ const Orders = () => {
|
|||||||
|
|
||||||
// KPI tile definitions.
|
// KPI tile definitions.
|
||||||
const kpiCards = [
|
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 (
|
return (
|
||||||
@@ -532,14 +571,9 @@ const Orders = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* ============================================= || Status Tabs + Search (compact) || ============================================= */}
|
{/* ============================================= || Status Tabs + Search (compact) || ============================================= */}
|
||||||
<Paper
|
<Paper
|
||||||
elevation={0}
|
elevation={0}
|
||||||
@@ -556,13 +590,7 @@ const Orders = () => {
|
|||||||
background: '#fff'
|
background: '#fff'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack direction="row" alignItems="center" justifyContent="space-between" gap={1.5} sx={{ flexWrap: 'wrap-reverse' }}>
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="space-between"
|
|
||||||
gap={1.5}
|
|
||||||
sx={{ flexWrap: 'wrap-reverse' }}
|
|
||||||
>
|
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
spacing={0.75}
|
spacing={0.75}
|
||||||
@@ -640,7 +668,6 @@ const Orders = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
@@ -692,16 +719,16 @@ const Orders = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TableCell>#</TableCell>
|
<TableCell>#</TableCell>
|
||||||
<TableCell>Order Location</TableCell>
|
<TableCell>Order</TableCell>
|
||||||
<TableCell>Pickup</TableCell>
|
<TableCell>Pickup</TableCell>
|
||||||
<TableCell>Drop</TableCell>
|
<TableCell>Drop</TableCell>
|
||||||
<TableCell align="center">Qty</TableCell>
|
<TableCell align="center">Qty</TableCell>
|
||||||
<TableCell align="right">COD</TableCell>
|
<TableCell align="right">COD</TableCell>
|
||||||
<TableCell align="center">Kms</TableCell>
|
<TableCell align="center">Kms (approx)</TableCell>
|
||||||
<TableCell align="right">Charges</TableCell>
|
<TableCell align="right">Charges</TableCell>
|
||||||
<TableCell>Notes</TableCell>
|
<TableCell>Notes</TableCell>
|
||||||
<TableCell>Status</TableCell>
|
<TableCell>Status</TableCell>
|
||||||
{currentStatus === 'created' && <TableCell align="right">Actions</TableCell>}
|
<TableCell align="right">Actions</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|
||||||
@@ -710,7 +737,7 @@ const Orders = () => {
|
|||||||
rows.length === 0 &&
|
rows.length === 0 &&
|
||||||
Array.from({ length: 10 }).map((_, idx) => (
|
Array.from({ length: 10 }).map((_, idx) => (
|
||||||
<TableRow key={`sk-${idx}`}>
|
<TableRow key={`sk-${idx}`}>
|
||||||
{Array.from({ length: currentStatus === 'created' ? 12 : 11 }).map((__, ci) => (
|
{Array.from({ length: 11 }).map((__, ci) => (
|
||||||
<TableCell key={ci} sx={{ borderBottom: `1px solid ${DT.divider}`, py: 0.625, px: 1 }}>
|
<TableCell key={ci} sx={{ borderBottom: `1px solid ${DT.divider}`, py: 0.625, px: 1 }}>
|
||||||
<Skeleton animation="wave" height={20} />
|
<Skeleton animation="wave" height={20} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -720,7 +747,7 @@ const Orders = () => {
|
|||||||
|
|
||||||
{!isLoadingGetOrders && rows.length === 0 && (
|
{!isLoadingGetOrders && rows.length === 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={currentStatus === 'created' ? 12 : 11} sx={{ py: 7, borderBottom: 'none' }}>
|
<TableCell colSpan={11} sx={{ py: 7, borderBottom: 'none' }}>
|
||||||
<Stack alignItems="center" spacing={1.25}>
|
<Stack alignItems="center" spacing={1.25}>
|
||||||
<Avatar
|
<Avatar
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
@@ -734,9 +761,7 @@ const Orders = () => {
|
|||||||
>
|
>
|
||||||
<MdLocalShipping size={26} />
|
<MdLocalShipping size={26} />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Typography sx={{ fontWeight: 700, color: DT.textPrimary, fontSize: 14 }}>
|
<Typography sx={{ fontWeight: 700, color: DT.textPrimary, fontSize: 14 }}>No {currentStatus} orders</Typography>
|
||||||
No {currentStatus} orders
|
|
||||||
</Typography>
|
|
||||||
<Typography sx={{ color: DT.textSecondary, fontSize: 12 }}>
|
<Typography sx={{ color: DT.textSecondary, fontSize: 12 }}>
|
||||||
{searchword ? 'Try a different keyword or clear the search.' : 'Adjust the location, status, or date range above.'}
|
{searchword ? 'Try a different keyword or clear the search.' : 'Adjust the location, status, or date range above.'}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -765,20 +790,13 @@ const Orders = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{rows.map((row, index) => {
|
{rows.map((row, index) => {
|
||||||
const isItemSelected = !!deliverylist.find((res) => res.orderheaderid === row.orderheaderid);
|
const customer = customerMap?.get(row.appcustomerid);
|
||||||
const handleCheckbox = (e) => {
|
const kms = haversineKm(row.pickuplatitude, row.pickuplongitude, row.deliverylatitude, row.deliverylongitude);
|
||||||
if (e.target.checked) {
|
const isCancellable = !['Cancelled', 'Converted_To_Consignment'].includes(row.status);
|
||||||
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 }))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={`${row.orderheaderid}-${index}`}
|
key={`${row.bookingid}-${index}`}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'background-color 0.12s, box-shadow 0.12s',
|
transition: 'background-color 0.12s, box-shadow 0.12s',
|
||||||
@@ -796,34 +814,24 @@ const Orders = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Typography sx={{ fontWeight: 700, color: DT.textSecondary }}>
|
<Typography sx={{ fontWeight: 700, color: DT.textSecondary }}>{page * rowsPerPage + index + 1}</Typography>
|
||||||
{page * rowsPerPage + index + 1}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||||
{row.locationname}
|
{row.bookingno || `#${row.bookingid}`}
|
||||||
{row.locationsuburb && ` - ${row.locationsuburb}`}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Tooltip title="Order Id">
|
|
||||||
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
|
||||||
{row.orderid}
|
|
||||||
</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ mt: 0.125 }}>
|
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ mt: 0.125 }}>
|
||||||
<MdAccessTime size={10} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
<MdAccessTime size={10} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
||||||
{(() => {
|
{(() => {
|
||||||
const dateObj = row.pickupslot && dayjs(row.pickupslot).isValid()
|
const dateObj = dayjs(row.createdat);
|
||||||
? dayjs(row.pickupslot)
|
|
||||||
: dayjs(row.deliverydate || row.orderdate);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typography sx={{ fontSize: 10.5, color: DT.textSecondary, fontWeight: 700 }} noWrap>
|
<Typography sx={{ fontSize: 10.5, color: DT.textSecondary, fontWeight: 700 }} noWrap>
|
||||||
{dateObj.format('hh:mm A')}
|
{dateObj.isValid() ? dateObj.format('hh:mm A') : '—'}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600 }} noWrap>
|
<Typography sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600 }} noWrap>
|
||||||
· {dateObj.format('DD MMM YY')}
|
{dateObj.isValid() && `· ${dateObj.format('DD MMM YY')}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -833,70 +841,85 @@ const Orders = () => {
|
|||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Stack direction="column">
|
<Stack direction="column">
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
<Tooltip title={row.pickupaddress || ''}>
|
||||||
{row.pickupcustomer}
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||||
</Typography>
|
{row.pickupaddress ? `${row.pickupaddress.slice(0, 20)}…` : '—'}
|
||||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
|
||||||
{row.pickupcontactno}
|
|
||||||
</Typography>
|
|
||||||
<Tooltip title={row.pickupaddress}>
|
|
||||||
<Typography variant="caption" sx={{ color: DT.textMuted }} noWrap>
|
|
||||||
{row.pickupsuburb || (row.pickupaddress ? `${row.pickupaddress.slice(0, 20)}…` : '—')}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Typography variant="caption" sx={{ color: DT.textMuted }} noWrap>
|
||||||
|
{row.pickuppincode || ''}
|
||||||
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Stack direction="column">
|
<Stack direction="column">
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||||
{row.deliverycustomer}
|
{customer?.firstname || customer?.name || `Customer #${row.appcustomerid ?? '—'}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||||
{row.deliverycontactno}
|
{customer?.phone || customer?.contactno || ''}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Tooltip title={row.deliveryaddress}>
|
<Tooltip title={row.deliveryaddress || ''}>
|
||||||
<Typography variant="caption" sx={{ color: DT.textMuted }} noWrap>
|
<Typography variant="caption" sx={{ color: DT.textMuted }} noWrap>
|
||||||
{row.deliverysuburb ||
|
{row.deliveryaddress?.length > 20 ? `${row.deliveryaddress.slice(0, 20)}…` : row.deliveryaddress || '—'}
|
||||||
(row.deliveryaddress?.length > 20 ? `${row.deliveryaddress.slice(0, 20)}…` : row.deliveryaddress || '—')}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
<MetricCell value={row.quantity} color="#0ea5e9" icon={<MdInventory2 size={11} />} />
|
<MetricCell value={row.parcels?.length || 0} color="#0ea5e9" icon={<MdInventory2 size={11} />} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<MetricCell value={row.collectionamt} color="#10b981" icon={<MdCurrencyRupee size={11} />} isMoney />
|
<Tooltip title="Not available on the Doormile Express API yet">
|
||||||
|
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 700 }}>
|
||||||
|
—
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
<MetricCell value={row.kms} color="#f59e0b" icon={<MdStraighten size={11} />} />
|
{kms != null ? (
|
||||||
|
<Tooltip title="Straight-line distance — pickup/delivery road distance isn't returned by the API">
|
||||||
|
<span>
|
||||||
|
<MetricCell value={kms.toFixed(1)} color="#f59e0b" icon={<MdStraighten size={11} />} />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<MetricCell value={null} color="#f59e0b" icon={<MdStraighten size={11} />} />
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<MetricCell value={row.deliverycharge} color={BRAND} icon={<MdCurrencyRupee size={11} />} isMoney />
|
<MetricCell
|
||||||
|
value={row.serviceoptions?.[0]?.estimatedprice}
|
||||||
|
color={BRAND}
|
||||||
|
icon={<MdCurrencyRupee size={11} />}
|
||||||
|
isMoney
|
||||||
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{row.ordernotes ? (
|
{row.notes ? (
|
||||||
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}>
|
<Tooltip title={row.notes}>
|
||||||
<MdNotes size={12} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}>
|
||||||
<Typography
|
<MdNotes size={12} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
||||||
variant="caption"
|
<Typography
|
||||||
sx={{
|
variant="caption"
|
||||||
maxWidth: 140,
|
sx={{
|
||||||
whiteSpace: 'nowrap',
|
maxWidth: 140,
|
||||||
overflow: 'hidden',
|
whiteSpace: 'nowrap',
|
||||||
textOverflow: 'ellipsis',
|
overflow: 'hidden',
|
||||||
fontWeight: 600
|
textOverflow: 'ellipsis',
|
||||||
}}
|
fontWeight: 600
|
||||||
>
|
}}
|
||||||
{row.ordernotes}
|
>
|
||||||
</Typography>
|
{row.notes}
|
||||||
</Stack>
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<Typography variant="caption" sx={{ color: DT.textMuted }}>
|
<Typography variant="caption" sx={{ color: DT.textMuted }}>
|
||||||
—
|
—
|
||||||
@@ -905,18 +928,40 @@ const Orders = () => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<StatusBadge status={row.orderstatus} />
|
<StatusBadge status={row.status} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{currentStatus === 'created' && (
|
<TableCell align="right">
|
||||||
<TableCell align="right">
|
<Stack direction="row" spacing={0.5} justifyContent="flex-end">
|
||||||
{row.orderstatus === 'created' && (
|
<Tooltip title="Track">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdMyLocation size={14} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
{currentStatus === 'created' && isCancellable && (
|
||||||
<Tooltip title="Cancel Order">
|
<Tooltip title="Cancel Order">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setOrderheaderid(row.orderheaderid);
|
setOrderheaderid(row.bookingid);
|
||||||
setCancelOpen(true);
|
setCancelOpen(true);
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
@@ -935,29 +980,20 @@ const Orders = () => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</Stack>
|
||||||
)}
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{rows.length !== 0 && (
|
{rows.length !== 0 && (
|
||||||
<TableRow sx={{ '&:hover': { backgroundColor: 'transparent !important' } }}>
|
<TableRow sx={{ '&:hover': { backgroundColor: 'transparent !important' } }}>
|
||||||
<TableCell colSpan={currentStatus === 'created' ? 12 : 11} sx={{ borderBottom: 'none', py: 1, bgcolor: DT.surfaceAlt }}>
|
<TableCell colSpan={11} sx={{ borderBottom: 'none', py: 1, bgcolor: DT.surfaceAlt }}>
|
||||||
<Stack
|
<Stack ref={loadMoreRef} direction="row" alignItems="center" justifyContent="center" spacing={1} sx={{ minHeight: 32 }}>
|
||||||
ref={loadMoreRef}
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
spacing={1}
|
|
||||||
sx={{ minHeight: 32 }}
|
|
||||||
>
|
|
||||||
{isFetchingNextPage || hasNextPage ? (
|
{isFetchingNextPage || hasNextPage ? (
|
||||||
<>
|
<>
|
||||||
<CircularProgress size={14} sx={{ color: BRAND }} />
|
<CircularProgress size={14} sx={{ color: BRAND }} />
|
||||||
<Typography sx={{ fontSize: 11.5, color: DT.textSecondary, fontWeight: 700 }}>
|
<Typography sx={{ fontSize: 11.5, color: DT.textSecondary, fontWeight: 700 }}>Loading more orders…</Typography>
|
||||||
Loading more orders…
|
|
||||||
</Typography>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Typography sx={{ fontSize: 11.5, color: DT.textMuted, fontWeight: 700, letterSpacing: 0.3 }}>
|
<Typography sx={{ fontSize: 11.5, color: DT.textMuted, fontWeight: 700, letterSpacing: 0.3 }}>
|
||||||
@@ -1033,6 +1069,38 @@ const Orders = () => {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* ============================================= || Track Order || ============================================= */}
|
||||||
|
<Dialog open={!!trackBookingId} onClose={() => setTrackBookingId(null)} maxWidth="xs" fullWidth PaperProps={{ sx: { borderRadius: 3 } }}>
|
||||||
|
<DialogTitle sx={{ fontWeight: 700 }}>Track Order</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
{trackLoading ? (
|
||||||
|
<Stack alignItems="center" sx={{ py: 3 }}>
|
||||||
|
<CircularProgress size={28} />
|
||||||
|
</Stack>
|
||||||
|
) : trackData ? (
|
||||||
|
<Stack spacing={1.5}>
|
||||||
|
{Object.entries(trackData).map(([key, value]) => (
|
||||||
|
<Stack key={key} direction="row" justifyContent="space-between" spacing={2}>
|
||||||
|
<Typography variant="caption" sx={{ color: DT.textMuted, textTransform: 'uppercase', fontWeight: 700 }}>
|
||||||
|
{key}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'right', wordBreak: 'break-word' }}>
|
||||||
|
{typeof value === 'object' ? JSON.stringify(value) : String(value ?? '—')}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2" sx={{ color: DT.textSecondary, textAlign: 'center', py: 3 }}>
|
||||||
|
No tracking data available for this order yet.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setTrackBookingId(null)}>Close</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* ============================================= || Date Filter || ============================================= */}
|
{/* ============================================= || Date Filter || ============================================= */}
|
||||||
<DateFilterDialog
|
<DateFilterDialog
|
||||||
open={dateOpen}
|
open={dateOpen}
|
||||||
@@ -1043,67 +1111,6 @@ const Orders = () => {
|
|||||||
setDatestatus(label);
|
setDatestatus(label);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* ============================================= || Cancel Multiple Orders Dialog || ============================================= */}
|
|
||||||
<Dialog open={multiDeleteDialog} onClose={() => setMultiDeleteDialog(false)} maxWidth="xs" PaperProps={{ sx: { borderRadius: 3 } }}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
p: 2.5,
|
|
||||||
background: `linear-gradient(135deg, ${tint('#ef4444')} 0%, ${tint('#f59e0b')} 100%)`,
|
|
||||||
borderBottom: `1px solid ${DT.borderSubtle}`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
|
||||||
<Avatar sx={{ bgcolor: '#ef4444', color: '#fff', width: 40, height: 40 }}>
|
|
||||||
<MdDeleteOutline size={20} />
|
|
||||||
</Avatar>
|
|
||||||
<Typography variant="h5" sx={{ fontWeight: 800, color: DT.textPrimary }}>
|
|
||||||
Cancel Selected Orders
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
<DialogContent sx={{ pt: 3 }}>
|
|
||||||
<Stack alignItems="center" spacing={3}>
|
|
||||||
<Typography variant="body1" align="center" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
|
||||||
Are you sure you want to cancel the {deliverylist.length} selected orders? This action cannot be undone.
|
|
||||||
</Typography>
|
|
||||||
<Stack direction="row" spacing={1.5} sx={{ width: 1 }}>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
onClick={() => setMultiDeleteDialog(false)}
|
|
||||||
variant="outlined"
|
|
||||||
sx={{
|
|
||||||
borderRadius: 999,
|
|
||||||
py: 1,
|
|
||||||
borderColor: DT.borderSubtle,
|
|
||||||
color: DT.textSecondary,
|
|
||||||
fontWeight: 700,
|
|
||||||
'&:hover': { borderColor: DT.textSecondary, bgcolor: DT.surfaceAlt }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
No
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
variant="contained"
|
|
||||||
onClick={() => cancelMultipleOrderMutation.mutate(deliverylist)}
|
|
||||||
autoFocus
|
|
||||||
sx={{
|
|
||||||
borderRadius: 999,
|
|
||||||
py: 1,
|
|
||||||
bgcolor: '#ef4444',
|
|
||||||
fontWeight: 700,
|
|
||||||
boxShadow: `0 6px 18px ${ring('#ef4444')}`,
|
|
||||||
'&:hover': { bgcolor: '#dc2626' }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Yes, Cancel
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { React, useState, useEffect, useRef } from 'react';
|
import { React, useState, useEffect, useRef } from 'react';
|
||||||
import axios from 'axios';
|
|
||||||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
|
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
// material-ui
|
// material-ui
|
||||||
@@ -67,6 +66,7 @@ import { FaCircleCheck } from 'react-icons/fa6';
|
|||||||
import MapWithRoute from './mapWithRoute';
|
import MapWithRoute from './mapWithRoute';
|
||||||
import CircularLoader from 'components/CircularLoader';
|
import CircularLoader from 'components/CircularLoader';
|
||||||
import { fetchDeliveries, fetchRidersList, gettenantlocations, getTenants } from 'pages/api/api';
|
import { fetchDeliveries, fetchRidersList, gettenantlocations, getTenants } from 'pages/api/api';
|
||||||
|
import { getDashboard, getConsignmentLogs } from 'pages/api/doormileApi';
|
||||||
import { CSVExport } from 'components/third-party/ReactTable';
|
import { CSVExport } from 'components/third-party/ReactTable';
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
@@ -227,205 +227,6 @@ const StampCell = ({ value, formatDate, formatTime, success }) => {
|
|||||||
|
|
||||||
// ==============================|| Orders Details ||============================== //
|
// ==============================|| 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() {
|
export default function OrdersDetails() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
@@ -502,66 +303,58 @@ export default function OrdersDetails() {
|
|||||||
setRiderValue(null);
|
setRiderValue(null);
|
||||||
}, [locationid]);
|
}, [locationid]);
|
||||||
|
|
||||||
// ============== Haversine distance calculation for the map route ==============
|
// Haversine sum over consecutive points — total distance for the map's
|
||||||
function calculateDistance(lat1, lon1, lat2, lon2) {
|
// 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 R = 6371;
|
||||||
const dLat = (lat2 - lat1) * (Math.PI / 180);
|
const toRad = (d) => (d * Math.PI) / 180;
|
||||||
const dLon = (lon2 - lon1) * (Math.PI / 180);
|
let total = 0;
|
||||||
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;
|
|
||||||
for (let i = 0; i < routeCoordinates.length - 1; i++) {
|
for (let i = 0; i < routeCoordinates.length - 1; i++) {
|
||||||
const { lat: lat1, lng: lon1 } = routeCoordinates[i];
|
const { lat: lat1, lng: lon1 } = routeCoordinates[i];
|
||||||
const { lat: lat2, lng: lon2 } = routeCoordinates[i + 1];
|
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) => {
|
const getdeliverylogs = async (id) => {
|
||||||
setLogsLoading(true);
|
setLogsLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(`${process.env.REACT_APP_URL3}/deliveries/getdeliverylogs/?deliveryid=${id}`);
|
const logs = (await getConsignmentLogs(id)) || [];
|
||||||
const datas = res.data.details;
|
const points = (Array.isArray(logs) ? logs : [])
|
||||||
if (Array.isArray(datas) && datas.length !== 0) {
|
.map((r) => ({
|
||||||
// Sort chronologically by logdate
|
lat: parseFloat(r?.latitude ?? r?.lat),
|
||||||
const sorted = datas
|
lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon),
|
||||||
.map((r) => {
|
logdate: r?.logdate ?? r?.createdat ?? r?.timestamp
|
||||||
const ts = r?.logdate ? dayjs(r.logdate) : null;
|
}))
|
||||||
return {
|
.filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng))
|
||||||
lat: parseFloat(r?.latitude ?? r?.lat),
|
.sort((a, b) => new Date(a.logdate || 0) - new Date(b.logdate || 0));
|
||||||
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);
|
|
||||||
|
|
||||||
if (sorted.length !== 0) {
|
if (points.length === 0) {
|
||||||
setRiderStart(sorted[0].logdate);
|
setRiderCoordinates([]);
|
||||||
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 {
|
|
||||||
opentoast('No Logs Found ', 'error', 2000);
|
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) {
|
} catch (error) {
|
||||||
console.log('getdeliverylogs', error);
|
console.log('getdeliverylogs', error);
|
||||||
|
opentoast('No Logs Found ', 'error', 2000);
|
||||||
} finally {
|
} finally {
|
||||||
setLogsLoading(false);
|
setLogsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -667,36 +460,22 @@ export default function OrdersDetails() {
|
|||||||
const fetchcount = async () => {
|
const fetchcount = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await axios
|
const data = (await getDashboard()) || {};
|
||||||
.get(
|
settotal(data.total || 0);
|
||||||
appId == 0
|
setPendingLenght(data.pending || 0);
|
||||||
? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?fromdate=${startdate}&todate=${enddate}`
|
setAssignLenght(data.accepted || 0);
|
||||||
: `${
|
setArrivedLenght(data.arrived || 0);
|
||||||
process.env.REACT_APP_URL
|
setPickedLenght(data.picked || 0);
|
||||||
}/deliveries/deliverysummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}&userid=${
|
setActiveLenght(data.active || 0);
|
||||||
selectedRider?.userid || 0
|
setDeliveredLenght(data.delivered || 0);
|
||||||
}`
|
setSkippedLenght(data.skipped || 0);
|
||||||
)
|
setCancelLenght(data.cancelled || 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
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
enqueueSnackbar(err.message, {
|
||||||
|
variant: 'error',
|
||||||
|
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||||
|
autoHideDuration: 2000
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||||
import axios from 'axios';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
// material-ui
|
// material-ui
|
||||||
@@ -318,36 +317,17 @@ export default function OrdersReport() {
|
|||||||
);
|
);
|
||||||
}, [filteredRows]);
|
}, [filteredRows]);
|
||||||
|
|
||||||
// ==============================|| per-tenant rider breakdown ||============================== //
|
// No per-tenant or per-location rider-breakdown endpoint in the new API.
|
||||||
const getuserreportsummary = async (tenantId) => {
|
const getuserreportsummary = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
setRidersdata([]);
|
||||||
const res = await axios.get(
|
setLoading(false);
|
||||||
`${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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| per-location rider breakdown ||============================== //
|
const getriderlocationsummary = async () => {
|
||||||
const getriderlocationsummary = async (locId) => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
setRidersdata([]);
|
||||||
const res = await axios.get(
|
setLoading(false);
|
||||||
`${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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isErrorReports) console.warn('ordersSummary error:', reportsError?.message);
|
if (isErrorReports) console.warn('ordersSummary error:', reportsError?.message);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import axios from 'axios';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
// material-ui
|
// material-ui
|
||||||
@@ -59,7 +58,6 @@ import PageHeader from 'components/nearle_components/PageHeader';
|
|||||||
import StatCard from 'components/nearle_components/StatCard';
|
import StatCard from 'components/nearle_components/StatCard';
|
||||||
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
|
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
|
||||||
import RidersRoutes from './RidersRoutes';
|
import RidersRoutes from './RidersRoutes';
|
||||||
import { OpenToast } from 'components/third-party/OpenToast';
|
|
||||||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -242,79 +240,22 @@ export default function RidersSummary() {
|
|||||||
);
|
);
|
||||||
}, [rows]);
|
}, [rows]);
|
||||||
|
|
||||||
// ==============================|| per-rider tenant breakdown ||============================== //
|
// No per-rider tenant-breakdown endpoint in the new API.
|
||||||
const fetchTenantSummary = async (riderUserid) => {
|
const fetchTenantSummary = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
setTenantData(null);
|
||||||
const tenantRes = await axios.get(
|
setLoading(false);
|
||||||
`${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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| rider planned route (for map) ||============================== //
|
// ==============================|| rider planned route (for map) ||============================== //
|
||||||
// Pulls every delivery the rider was assigned over the page's date range, then
|
// /admin/consignments has no documented per-row schema (lat/lng/step/
|
||||||
// emits an ordered waypoint list sorted by `step` (the planning sequence). The
|
// customer fields aren't shown anywhere in the doc), so building a planned
|
||||||
// map dialog renders this as the rider's PLANNED route — the path the
|
// route from it would be guesswork that risks plotting wrong pins on the
|
||||||
// optimizer told them to follow — not their actual GPS trail.
|
// map. Degrades to an empty route instead.
|
||||||
const getuserdeliverylogs = async (userid) => {
|
const getuserdeliverylogs = async () => {
|
||||||
setRouteLoading(true);
|
setRouteLoading(true);
|
||||||
try {
|
setLogDetails([]);
|
||||||
const url =
|
setRouteLoading(false);
|
||||||
`${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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Total Amount sum (preserved from legacy bottom bar).
|
// Total Amount sum (preserved from legacy bottom bar).
|
||||||
|
|||||||
@@ -19,33 +19,24 @@ import {
|
|||||||
Button
|
Button
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import {
|
import { MdCheckCircle, MdCancel, MdAccessTime, MdInventory2, MdTwoWheeler, MdArrowForward } from 'react-icons/md';
|
||||||
MdCheckCircle,
|
|
||||||
MdCancel,
|
|
||||||
MdAccessTime,
|
|
||||||
MdInventory2,
|
|
||||||
MdTwoWheeler,
|
|
||||||
MdArrowForward
|
|
||||||
} from 'react-icons/md';
|
|
||||||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||||
import axios from 'axios';
|
|
||||||
import { OpenToast } from 'components/third-party/OpenToast';
|
import { OpenToast } from 'components/third-party/OpenToast';
|
||||||
|
|
||||||
const STATUS_META = {
|
const STATUS_META = {
|
||||||
active: { label: 'Active', color: '#10b981', icon: MdCheckCircle },
|
active: { label: 'Active', color: '#10b981', icon: MdCheckCircle },
|
||||||
inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel },
|
inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel },
|
||||||
online: { label: 'Online', color: '#10b981', icon: MdCheckCircle },
|
online: { label: 'Online', color: '#10b981', icon: MdCheckCircle },
|
||||||
offline: { label: 'Offline', color: '#ef4444', icon: MdCancel },
|
offline: { label: 'Offline', color: '#ef4444', icon: MdCancel },
|
||||||
idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime },
|
idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime },
|
||||||
unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 }
|
unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 }
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RiderSubstitution({
|
export default function RiderSubstitution({
|
||||||
appId,
|
|
||||||
rows,
|
rows,
|
||||||
allRidersList,
|
allRidersList,
|
||||||
substituteRidersList,
|
substituteRidersList,
|
||||||
@@ -96,112 +87,47 @@ export default function RiderSubstitution({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isBatchDisabled = React.useCallback((batch) => {
|
const isBatchDisabled = React.useCallback(
|
||||||
if (!selectedDate) return false;
|
(batch) => {
|
||||||
const isToday = selectedDate.isSame(dayjs(), 'day');
|
if (!selectedDate) return false;
|
||||||
const isPast = selectedDate.isBefore(dayjs(), 'day');
|
const isToday = selectedDate.isSame(dayjs(), 'day');
|
||||||
if (isPast) return true;
|
const isPast = selectedDate.isBefore(dayjs(), 'day');
|
||||||
if (!isToday) return false;
|
if (isPast) return true;
|
||||||
|
if (!isToday) return false;
|
||||||
|
|
||||||
const now = dayjs();
|
const now = dayjs();
|
||||||
const currentHour = now.hour();
|
const currentHour = now.hour();
|
||||||
const currentMinute = now.minute();
|
const currentMinute = now.minute();
|
||||||
const currentTime = currentHour + currentMinute / 60;
|
const currentTime = currentHour + currentMinute / 60;
|
||||||
|
|
||||||
if (batch === 'morning') {
|
if (batch === 'morning') {
|
||||||
return currentTime >= 7.0; // After 7:00 AM
|
return currentTime >= 7.0; // After 7:00 AM
|
||||||
}
|
}
|
||||||
if (batch === 'afternoon') {
|
if (batch === 'afternoon') {
|
||||||
return currentTime >= 9.0; // After 9:00 AM
|
return currentTime >= 9.0; // After 9:00 AM
|
||||||
}
|
}
|
||||||
if (batch === 'evening') {
|
if (batch === 'evening') {
|
||||||
return currentTime >= 16.0; // After 4:00 PM
|
return currentTime >= 16.0; // After 4:00 PM
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}, [selectedDate]);
|
},
|
||||||
|
[selectedDate]
|
||||||
|
);
|
||||||
|
|
||||||
const filteredRows = React.useMemo(() => {
|
const filteredRows = React.useMemo(() => {
|
||||||
return rows;
|
return rows;
|
||||||
}, [rows]);
|
}, [rows]);
|
||||||
|
|
||||||
const hasAssignments = Object.values(substituteAssignments || {}).some(
|
const hasAssignments = Object.values(substituteAssignments || {}).some((val) => val !== null && val !== undefined);
|
||||||
(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 () => {
|
const handleFinalize = async () => {
|
||||||
setIsFinalizing(true);
|
setIsFinalizing(true);
|
||||||
try {
|
OpenToast('Substitutions saved successfully (offline sync)!', 'success', 2000);
|
||||||
let partnerId = '';
|
if (onFinalizeSuccess) onFinalizeSuccess();
|
||||||
try {
|
setIsFinalizing(false);
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRowStatusMeta = (row) => {
|
const getRowStatusMeta = (row) => {
|
||||||
@@ -239,12 +165,7 @@ export default function RiderSubstitution({
|
|||||||
background: '#fff'
|
background: '#fff'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack direction={{ xs: 'column', sm: 'row' }} alignItems="center" justifyContent="space-between" gap={2}>
|
||||||
direction={{ xs: 'column', sm: 'row' }}
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="space-between"
|
|
||||||
gap={2}
|
|
||||||
>
|
|
||||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ flexWrap: 'wrap' }}>
|
<Stack direction="row" alignItems="center" spacing={1} sx={{ flexWrap: 'wrap' }}>
|
||||||
<Typography variant="body2" sx={{ fontWeight: 700, color: DT.textSecondary, mr: 1 }}>
|
<Typography variant="body2" sx={{ fontWeight: 700, color: DT.textSecondary, mr: 1 }}>
|
||||||
Shift Batch:
|
Shift Batch:
|
||||||
@@ -561,10 +482,7 @@ export default function RiderSubstitution({
|
|||||||
{(row.fullname || row.username || '?').charAt(0).toUpperCase()}
|
{(row.fullname || row.username || '?').charAt(0).toUpperCase()}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Stack sx={{ minWidth: 0 }}>
|
<Stack sx={{ minWidth: 0 }}>
|
||||||
<Typography
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
|
||||||
variant="subtitle2"
|
|
||||||
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
|
|
||||||
>
|
|
||||||
{row.username || '—'}
|
{row.username || '—'}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||||
|
|||||||
@@ -1,76 +1,76 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
// material-ui
|
// 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 { MdDirectionsBike } from 'react-icons/md';
|
||||||
import { DT, tint } from 'themes/dt/tokens';
|
import { DT, tint } from 'themes/dt/tokens';
|
||||||
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
// third-party
|
|
||||||
// import { PatternFormat } from 'react-number-format';
|
|
||||||
|
|
||||||
// project import
|
// project import
|
||||||
import MainCard from 'components/MainCard';
|
import MainCard from 'components/MainCard';
|
||||||
import axios from 'axios';
|
import { createMiler, getHubs } from 'pages/api/doormileApi';
|
||||||
// assets
|
import { getTenants, fetchAppLocations } from 'pages/api/api';
|
||||||
import AddressAutocomplete, { geocodeAddress } from 'components/nearle_components/AddressAutocomplete';
|
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
// const avatarImage = require.context('assets/images/users', true);
|
const VEHICLE_TYPES = ['Bike', 'Scooter', 'Bicycle', 'Car', 'Van'];
|
||||||
|
|
||||||
// styles & constant
|
|
||||||
// const ITEM_HEIGHT = 48;
|
|
||||||
// const ITEM_PADDING_TOP = 8;
|
|
||||||
// const MenuProps = {
|
|
||||||
// PaperProps: {
|
|
||||||
// style: {
|
|
||||||
// maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
const Createrider = () => {
|
const Createrider = () => {
|
||||||
// const [role, setRole] = useState('');
|
const [displayname, setDisplayname] = useState('');
|
||||||
const [mobilenumber, setMobilenumber] = useState('');
|
const [authname, setAuthname] = useState('');
|
||||||
const [emailaddress, setEmailaddress] = useState('');
|
const [emailaddress, setEmailaddress] = useState('');
|
||||||
const [city, setCity] = useState('');
|
const [mobilenumber, setMobilenumber] = useState('');
|
||||||
const [zipcode, setZipcode] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [address, setAddress] = useState('');
|
const [vehicletype, setVehicletype] = useState('Bike');
|
||||||
const [state, setState] = useState('');
|
const [selectedTenant, setSelectedTenant] = useState(null);
|
||||||
const [suburb, setSuburb] = useState('');
|
const [selectedHub, setSelectedHub] = useState(null);
|
||||||
const [latlong, setLatlong] = useState({});
|
const [selectedLocation, setSelectedLocation] = useState(null);
|
||||||
const [firstname, setFirstname] = useState('');
|
|
||||||
const [doorno, setDoorno] = useState('');
|
|
||||||
const [landmark, setLandmark] = useState('');
|
|
||||||
const [tenantinfo, setTenantinfo] = useState({});
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
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(() => {
|
const { data: tenants = [] } = useQuery({
|
||||||
// fetchprofiledetails(localStorage.getItem('appuserid'));
|
queryKey: ['createrider-tenants'],
|
||||||
// fetchprofiledetails(181);
|
queryFn: getTenants,
|
||||||
if (localStorage.getItem('tenantid')) {
|
enabled: isDoormileStaff
|
||||||
fetchtenantinfo(localStorage.getItem('tenantid'));
|
});
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: hubs = [] } = useQuery({
|
||||||
let active = true;
|
queryKey: ['createrider-hubs'],
|
||||||
geocodeAddress(address).then((place) => {
|
queryFn: getHubs
|
||||||
if (active && place) {
|
});
|
||||||
setLatlong({ lat: place.geometry.location.lat(), lng: place.geometry.location.lng() });
|
|
||||||
}
|
const { data: locations = [] } = useQuery({
|
||||||
});
|
queryKey: ['createrider-locations'],
|
||||||
return () => {
|
queryFn: fetchAppLocations
|
||||||
active = false;
|
});
|
||||||
};
|
|
||||||
}, [address]);
|
const hubOptions = useMemo(() => hubs || [], [hubs]);
|
||||||
|
const locationOptions = useMemo(() => (locations || []).filter((l) => l.applocationid), [locations]);
|
||||||
|
|
||||||
const opentoast = (message) => {
|
const opentoast = (message) => {
|
||||||
enqueueSnackbar(message, {
|
enqueueSnackbar(message, {
|
||||||
@@ -78,357 +78,244 @@ const Createrider = () => {
|
|||||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||||
autoHideDuration: 2000
|
autoHideDuration: 2000
|
||||||
});
|
});
|
||||||
// console.log(alertmessage)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createMilerMutation = useMutation({
|
||||||
|
mutationFn: (payload) => createMiler(payload),
|
||||||
const fetchtenantinfo = async (tid) => {
|
onSuccess: (res) => {
|
||||||
setLoading(true);
|
if (res.success) {
|
||||||
await axios
|
enqueueSnackbar('Rider created successfully', {
|
||||||
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
|
variant: 'success',
|
||||||
.then((res) => {
|
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||||
console.log(res);
|
autoHideDuration: 2000
|
||||||
if (res.data.status) {
|
});
|
||||||
setTenantinfo(res.data.details);
|
navigate('/doormile/riders');
|
||||||
}
|
} else {
|
||||||
setLoading(false);
|
opentoast(res.message || 'Failed to create rider');
|
||||||
})
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
opentoast(err.response?.data?.message || err.message || 'Failed to create rider');
|
||||||
}
|
}
|
||||||
setCity(city1 || '');
|
});
|
||||||
setState(state1 || '');
|
|
||||||
setZipcode(zipcode1 || '');
|
|
||||||
setSuburb(suburb1 || '');
|
|
||||||
};
|
|
||||||
|
|
||||||
const createprofile = async () => {
|
const createprofile = () => {
|
||||||
if (!firstname) {
|
const tenantid = isDoormileStaff ? selectedTenant?.tenantid : Number(loginTenantId);
|
||||||
opentoast('Fill Full name');
|
|
||||||
} else if (!mobilenumber) {
|
if (!displayname) {
|
||||||
opentoast('Fill Mobile Number');
|
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) {
|
} else if (!emailaddress) {
|
||||||
opentoast('Fill emailaddress');
|
opentoast('Fill Email Address');
|
||||||
} else if (!address) {
|
} else if (!password) {
|
||||||
opentoast('Fill Address');
|
opentoast('Fill Password');
|
||||||
} else if (!city) {
|
} else if (isDoormileStaff && !tenantid) {
|
||||||
opentoast('Fill City');
|
opentoast('Choose Tenant');
|
||||||
} else if (!zipcode) {
|
} else if (!selectedLocation?.applocationid) {
|
||||||
opentoast('Fill post code');
|
opentoast('Choose City');
|
||||||
} else if (!suburb) {
|
|
||||||
opentoast('Fill suburb');
|
|
||||||
} else if (!latlong.lat || !latlong.lng) {
|
|
||||||
opentoast('Choose valid address');
|
|
||||||
} else {
|
} else {
|
||||||
let obj = {
|
// POST /admin/milers — configid defaults to 1001 server-side (the
|
||||||
customerid: 0,
|
// miler-app login partition) and must not be overridden.
|
||||||
configid: 1,
|
const obj = {
|
||||||
firstname: firstname,
|
authname,
|
||||||
applocationid: tenantinfo.applolcationid,
|
displayname,
|
||||||
profileimage: '',
|
email: emailaddress,
|
||||||
dialcode: '+91',
|
|
||||||
contactno: mobilenumber,
|
contactno: mobilenumber,
|
||||||
devicetype: '',
|
password,
|
||||||
deviceid: '',
|
tenantid,
|
||||||
customertoken: '',
|
defaultvehicletype: vehicletype,
|
||||||
address: address,
|
applocationid: selectedLocation?.applocationid,
|
||||||
suburb: suburb,
|
hubid: selectedHub?.hubid
|
||||||
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
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(obj);
|
createMilerMutation.mutate(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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{loading && <Loader />}
|
{createMilerMutation.isPending && <Loader />}
|
||||||
|
|
||||||
<Box sx={{ p: { xs: 1.5, md: 3 } }}>
|
<Box sx={{ p: { xs: 1.5, md: 3 } }}>
|
||||||
<Grid item xs={12} sx={{ mb: 2 }}>
|
<Grid item xs={12} sx={{ mb: 2 }}>
|
||||||
<Paper
|
<Paper
|
||||||
sx={{
|
sx={{
|
||||||
p: 2.5,
|
p: 2.5,
|
||||||
borderRadius: DT.radiusCard + 'px',
|
borderRadius: DT.radiusCard + 'px',
|
||||||
boxShadow: DT.shadowSoft,
|
boxShadow: DT.shadowSoft,
|
||||||
border: '1px solid',
|
border: '1px solid',
|
||||||
borderColor: DT.borderSubtle,
|
borderColor: DT.borderSubtle,
|
||||||
background: `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)`
|
background: `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack direction="row" spacing={1.5} alignItems="center">
|
<Stack direction="row" spacing={1.5} alignItems="center">
|
||||||
<Avatar sx={{ width: 48, height: 48, bgcolor: DT.brand }}>
|
<Avatar sx={{ width: 48, height: 48, bgcolor: DT.brand }}>
|
||||||
<MdDirectionsBike size={22} />
|
<MdDirectionsBike size={22} />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Typography variant="h3">Create Rider</Typography>
|
<Typography variant="h3">Create Rider</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
<MainCard
|
<MainCard sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }}>
|
||||||
sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }}
|
<Grid container spacing={3}>
|
||||||
>
|
<Grid item xs={12}>
|
||||||
<Grid container spacing={3}>
|
<MainCard sx={{ height: '100%' }}>
|
||||||
<Grid item xs={12}>
|
<Grid container spacing={3}>
|
||||||
<MainCard
|
<Grid item xs={12} sm={6}>
|
||||||
// title="Contact Information"
|
<Stack spacing={1.25}>
|
||||||
sx={{ height: '100%' }}
|
<InputLabel htmlFor="rider-display-name">Display Name</InputLabel>
|
||||||
>
|
|
||||||
<Grid container spacing={3}>
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Stack spacing={1.25}>
|
|
||||||
<InputLabel htmlFor="personal-last-name">Admin Name</InputLabel>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
id="personal-last-name"
|
|
||||||
placeholder="Name"
|
|
||||||
onChange={(e) => setFirstname(e.target.value)}
|
|
||||||
value={firstname}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} sm={6}></Grid>
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Stack spacing={1.25}>
|
|
||||||
<InputLabel htmlFor="personal-phone">Phone Number</InputLabel>
|
|
||||||
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
|
|
||||||
<Select defaultValue="+1" disabled sx={{ cursor: 'not-allowed' }}>
|
|
||||||
<MenuItem value="+1">+91</MenuItem>
|
|
||||||
</Select>
|
|
||||||
<TextField
|
<TextField
|
||||||
type="number"
|
|
||||||
id="personal-phone"
|
|
||||||
// format="##########"
|
|
||||||
// mask="_"
|
|
||||||
fullWidth
|
fullWidth
|
||||||
// customInput={TextField}
|
id="rider-display-name"
|
||||||
placeholder="Phone Number"
|
placeholder="e.g. Murali S"
|
||||||
// defaultValue="8654239581"
|
onChange={(e) => setDisplayname(e.target.value)}
|
||||||
// onBlur={() => { }}
|
value={displayname}
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.value.toString().length <= 10) {
|
|
||||||
setMobilenumber(e.target.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
value={mobilenumber}
|
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
// disabled
|
|
||||||
sx={{ cursor: 'not-allowed' }}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Grid>
|
||||||
</Grid>
|
<Grid item xs={12} sm={6}>
|
||||||
<Grid item xs={12} sm={6}>
|
<Stack spacing={1.25}>
|
||||||
<Stack spacing={1.25}>
|
<InputLabel htmlFor="rider-auth-name">Login Name</InputLabel>
|
||||||
<InputLabel htmlFor="personal-email">Email Address</InputLabel>
|
<TextField
|
||||||
<TextField
|
fullWidth
|
||||||
type="email"
|
id="rider-auth-name"
|
||||||
fullWidth
|
placeholder="Login name used on the miler app"
|
||||||
// defaultValue="stebin.ben@gmail.com"
|
onChange={(e) => setAuthname(e.target.value)}
|
||||||
id="personal-email"
|
value={authname}
|
||||||
placeholder="Email Address"
|
autoComplete="off"
|
||||||
onChange={(e) => setEmailaddress(e.target.value)}
|
/>
|
||||||
value={emailaddress}
|
</Stack>
|
||||||
autoComplete="off"
|
</Grid>
|
||||||
/>
|
<Grid item xs={12} sm={6}>
|
||||||
</Stack>
|
<Stack spacing={1.25}>
|
||||||
</Grid>
|
<InputLabel htmlFor="rider-phone">Phone Number</InputLabel>
|
||||||
|
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
|
||||||
|
<Select defaultValue="+1" disabled sx={{ cursor: 'not-allowed' }}>
|
||||||
|
<MenuItem value="+1">+91</MenuItem>
|
||||||
|
</Select>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
id="rider-phone"
|
||||||
|
fullWidth
|
||||||
|
placeholder="Phone Number"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.value.toString().length <= 10) {
|
||||||
|
setMobilenumber(e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={mobilenumber}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Stack spacing={1.25}>
|
||||||
|
<InputLabel htmlFor="rider-email">Email Address</InputLabel>
|
||||||
|
<TextField
|
||||||
|
type="email"
|
||||||
|
fullWidth
|
||||||
|
id="rider-email"
|
||||||
|
placeholder="Email Address"
|
||||||
|
onChange={(e) => setEmailaddress(e.target.value)}
|
||||||
|
value={emailaddress}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Stack spacing={1.25}>
|
<Stack spacing={1.25}>
|
||||||
<InputLabel htmlFor="personal-address">Address</InputLabel>
|
<InputLabel htmlFor="rider-password">Password</InputLabel>
|
||||||
<AddressAutocomplete
|
<TextField
|
||||||
id="personal-address"
|
type="password"
|
||||||
fullWidth
|
fullWidth
|
||||||
placeholder="Address"
|
id="rider-password"
|
||||||
value={address}
|
placeholder="Miler-app login password"
|
||||||
onChange={setAddress}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
onPlaceSelected={handleAddressPlaceSelected}
|
value={password}
|
||||||
/>
|
autoComplete="off"
|
||||||
</Stack>
|
/>
|
||||||
</Grid>
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Stack spacing={1.25}>
|
<Stack spacing={1.25}>
|
||||||
<InputLabel htmlFor="personal-location">Suburb</InputLabel>
|
<InputLabel htmlFor="rider-vehicle-type">Vehicle Type</InputLabel>
|
||||||
<TextField
|
<Select id="rider-vehicle-type" fullWidth value={vehicletype} onChange={(e) => setVehicletype(e.target.value)}>
|
||||||
fullWidth
|
{VEHICLE_TYPES.map((v) => (
|
||||||
// defaultValue="New York"
|
<MenuItem key={v} value={v}>
|
||||||
id="personal-location"
|
{v}
|
||||||
placeholder="Location"
|
</MenuItem>
|
||||||
onChange={(e) => setSuburb(e.target.value)}
|
))}
|
||||||
value={suburb}
|
</Select>
|
||||||
autoComplete="off"
|
</Stack>
|
||||||
/>
|
</Grid>
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Stack spacing={1.25}>
|
|
||||||
<InputLabel htmlFor="personal-zipcode">City</InputLabel>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
// defaultValue="956754"
|
|
||||||
// type='number'
|
|
||||||
id="personal-zipcode"
|
|
||||||
placeholder="City"
|
|
||||||
onChange={(e) => setCity(e.target.value)}
|
|
||||||
value={city}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12} sm={6}>
|
{isDoormileStaff && (
|
||||||
<Stack spacing={1.25}>
|
<Grid item xs={12} sm={6}>
|
||||||
<InputLabel htmlFor="personal-location">State</InputLabel>
|
<Stack spacing={1.25}>
|
||||||
<TextField
|
<InputLabel htmlFor="rider-tenant">Tenant</InputLabel>
|
||||||
fullWidth
|
<Autocomplete
|
||||||
// defaultValue="New York"
|
id="rider-tenant"
|
||||||
id="personal-location"
|
options={tenants || []}
|
||||||
placeholder="State"
|
getOptionLabel={(option) => option.tenantname || ''}
|
||||||
onChange={(e) => setState(e.target.value)}
|
value={selectedTenant}
|
||||||
value={state}
|
onChange={(e, value) => setSelectedTenant(value)}
|
||||||
autoComplete="off"
|
renderInput={(params) => <TextField {...params} placeholder="Choose tenant" />}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
)}
|
||||||
<Stack spacing={1.25}>
|
|
||||||
<InputLabel htmlFor="personal-zipcode">Post Code</InputLabel>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
// defaultValue="956754"
|
|
||||||
type="number"
|
|
||||||
id="personal-zipcode"
|
|
||||||
placeholder="Zipcode"
|
|
||||||
onChange={(e) => setZipcode(e.target.value)}
|
|
||||||
value={zipcode}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Stack spacing={1.25}>
|
<Stack spacing={1.25}>
|
||||||
<InputLabel htmlFor="personal-location">Door No</InputLabel>
|
<InputLabel htmlFor="rider-location">City</InputLabel>
|
||||||
<TextField
|
<Autocomplete
|
||||||
fullWidth
|
id="rider-location"
|
||||||
// defaultValue="New York"
|
options={locationOptions}
|
||||||
id="personal-location"
|
getOptionLabel={(option) => option.locationname || ''}
|
||||||
placeholder="Door No"
|
value={selectedLocation}
|
||||||
onChange={(e) => setDoorno(e.target.value)}
|
onChange={(e, value) => setSelectedLocation(value)}
|
||||||
value={doorno}
|
renderInput={(params) => <TextField {...params} placeholder="Choose city" />}
|
||||||
autoComplete="off"
|
/>
|
||||||
/>
|
</Stack>
|
||||||
</Stack>
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Stack spacing={1.25}>
|
||||||
|
<InputLabel htmlFor="rider-hub">Hub (optional)</InputLabel>
|
||||||
|
<Autocomplete
|
||||||
|
id="rider-hub"
|
||||||
|
options={hubOptions}
|
||||||
|
getOptionLabel={(option) => option.hubname || ''}
|
||||||
|
value={selectedHub}
|
||||||
|
onChange={(e, value) => setSelectedHub(value)}
|
||||||
|
renderInput={(params) => <TextField {...params} placeholder="Choose hub (visibility on hub console)" />}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
</MainCard>
|
||||||
<Stack spacing={1.25}>
|
</Grid>
|
||||||
<InputLabel htmlFor="personal-email">Landmark</InputLabel>
|
<Grid item xs={12}>
|
||||||
<TextField
|
<Stack
|
||||||
type="email"
|
direction={{ xs: 'column', sm: 'row' }}
|
||||||
fullWidth
|
justifyContent="flex-end"
|
||||||
// defaultValue="stebin.ben@gmail.com"
|
alignItems={{ xs: 'stretch', sm: 'center' }}
|
||||||
id="personal-email"
|
spacing={2}
|
||||||
placeholder="Landmark"
|
>
|
||||||
onChange={(e) => setLandmark(e.target.value)}
|
<Button variant="contained" onClick={createprofile} fullWidth={isMobile}>
|
||||||
value={landmark}
|
Create
|
||||||
autoComplete="off"
|
</Button>
|
||||||
/>
|
</Stack>
|
||||||
</Stack>
|
</Grid>
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</MainCard>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
</MainCard>
|
||||||
<Stack
|
|
||||||
direction={{ xs: 'column', sm: 'row' }}
|
|
||||||
justifyContent="flex-end"
|
|
||||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
|
||||||
spacing={2}
|
|
||||||
>
|
|
||||||
<Button variant="contained" onClick={() => createprofile()} fullWidth={isMobile}>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</MainCard>
|
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
|||||||
|
|
||||||
// project import
|
// project import
|
||||||
import MainCard from 'components/MainCard';
|
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 AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
@@ -55,22 +56,23 @@ const EditRider = () => {
|
|||||||
|
|
||||||
const [shiftlist, setShiftlist] = useState([]);
|
const [shiftlist, setShiftlist] = useState([]);
|
||||||
const [locaName, setLocoName] = useState();
|
const [locaName, setLocoName] = useState();
|
||||||
const userid = localStorage.getItem('userid');
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const fetchRiderData = async (id) => {
|
const fetchRiderData = async (id) => {
|
||||||
try {
|
try {
|
||||||
let riderdataresponse = await axios.get(`https://jupiter.nearle.app/live/api/v1/partners/getriderdetail/?userid=${id}`);
|
const miler = await getMiler(id);
|
||||||
console.log('riderdataresponse', riderdataresponse.data.details);
|
setRiderdata(miler);
|
||||||
setRiderdata(riderdataresponse.data.details);
|
fetchridershifts(miler?.applocationid);
|
||||||
fetchridershifts(riderdataresponse.data.details.applocationid);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('fetchmanagerList', error);
|
console.log('fetchmanagerList', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
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(() => {
|
useEffect(() => {
|
||||||
@@ -98,135 +100,30 @@ const EditRider = () => {
|
|||||||
}
|
}
|
||||||
}, [partner.applocationid]);
|
}, [partner.applocationid]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const fetchtenantinfo = async (tid) => {
|
const fetchtenantinfo = async (tid) => {
|
||||||
setLoading(true);
|
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 {
|
try {
|
||||||
const locationRes = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
|
await getAdminTenant(tid);
|
||||||
// const updatedLocations = [
|
|
||||||
// ...locationRes.data.details,
|
|
||||||
// { locationname: 'All', applocationid: 0 } // Add your new object here
|
|
||||||
// ];
|
|
||||||
console.log('fetchAppLocations', locationRes.data.details);
|
|
||||||
setPartnerlist(locationRes.data.details);
|
|
||||||
} catch (err) {
|
} 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(() => {
|
useEffect(() => {
|
||||||
fetchAppLocations();
|
fetchZoneLocations().then(setPartnerlist);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchridershifts = async (id) => {
|
// No rider-shift lookup endpoint in the new API.
|
||||||
setLoading(true);
|
const fetchridershifts = async () => setShiftlist([]);
|
||||||
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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddressPlaceSelected = (place) => {
|
const handleAddressPlaceSelected = (place) => {
|
||||||
setAddress(place.formatted_address);
|
setAddress(place.formatted_address);
|
||||||
@@ -255,65 +152,50 @@ const EditRider = () => {
|
|||||||
|
|
||||||
const updateRider = async () => {
|
const updateRider = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
console.log('updated riderData', riderdata);
|
// PUT /admin/milers/:id has no documented fields for bank/vehicle/shift
|
||||||
await axios
|
// details (the old ridersettings sub-object) — only the miler's own
|
||||||
.put(`https://jupiter.nearle.app/live/api/v1/partners/updaterider`, {
|
// identity/contact/tenant/zone fields carry over. Those settings stay in
|
||||||
userid: riderdata.userid,
|
// local state but no longer persist server-side until the new API grows
|
||||||
contactno: riderdata.contactno,
|
// matching endpoints.
|
||||||
firstname: riderdata.firstname,
|
//
|
||||||
lastname: riderdata.lastname,
|
// 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,
|
email: riderdata.email,
|
||||||
address: riderdata.address,
|
tenantid: riderdata.tenantid,
|
||||||
suburb: riderdata.suburb,
|
applocationid: riderdata.applocationid
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -377,18 +259,18 @@ const EditRider = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Grid container spacing={{ xs: 2, sm: 3 }}>
|
<Grid container spacing={{ xs: 2, sm: 3 }}>
|
||||||
{/* ========================== || First Name || ========================== */}
|
{/* ========================== || Display Name || ========================== */}
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Stack spacing={1.25}>
|
<Stack spacing={1.25}>
|
||||||
<InputLabel htmlFor="personal-last-name">First Name</InputLabel>
|
<InputLabel htmlFor="personal-display-name">Display Name</InputLabel>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
id="personal-last-name"
|
id="personal-display-name"
|
||||||
value={riderdata?.firstname}
|
value={riderdata?.displayname || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setRiderdata({
|
setRiderdata({
|
||||||
...riderdata,
|
...riderdata,
|
||||||
firstname: e.target.value
|
displayname: e.target.value
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
@@ -396,23 +278,11 @@ const EditRider = () => {
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
{/* ========================== || Last Name || ========================== */}
|
{/* ========================== || Login Name || ========================== */}
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Stack spacing={1.25}>
|
<Stack spacing={1.25}>
|
||||||
<InputLabel htmlFor="personal-last-name">Last Name</InputLabel>
|
<InputLabel htmlFor="personal-auth-name">Login Name</InputLabel>
|
||||||
<TextField
|
<TextField fullWidth id="personal-auth-name" value={riderdata?.authname || ''} disabled autoComplete="off" />
|
||||||
fullWidth
|
|
||||||
id="personal-last-name"
|
|
||||||
placeholder="Name"
|
|
||||||
onChange={(e) => {
|
|
||||||
setRiderdata({
|
|
||||||
...riderdata,
|
|
||||||
lastname: e.target.value
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
value={riderdata?.lastname}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
{/* ========================== || Phone Number || ========================== */}
|
{/* ========================== || Phone Number || ========================== */}
|
||||||
@@ -433,10 +303,10 @@ const EditRider = () => {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setRiderdata({
|
setRiderdata({
|
||||||
...riderdata,
|
...riderdata,
|
||||||
contactno: e.target.value
|
phone: e.target.value
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
value={riderdata?.contactno}
|
value={riderdata?.phone || ''}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
// disabled
|
// disabled
|
||||||
sx={{ cursor: 'not-allowed' }}
|
sx={{ cursor: 'not-allowed' }}
|
||||||
@@ -562,10 +432,10 @@ const EditRider = () => {
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
{/* ========================== || Choose Partner|| ========================== */}
|
{/* ========================== || City / Zone || ========================== */}
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Stack spacing={1.25}>
|
<Stack spacing={1.25}>
|
||||||
<InputLabel htmlFor="personal-location">Choose Partner</InputLabel>
|
<InputLabel htmlFor="personal-location">City / Zone</InputLabel>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
disablePortal
|
disablePortal
|
||||||
id="combo-box-demo"
|
id="combo-box-demo"
|
||||||
@@ -574,12 +444,16 @@ const EditRider = () => {
|
|||||||
sx={{ width: { xs: '100%', sm: 300 }, height: '30px', ml: { xs: 0, sm: 3 }, zIndex: '100' }}
|
sx={{ width: { xs: '100%', sm: 300 }, height: '30px', ml: { xs: 0, sm: 3 }, zIndex: '100' }}
|
||||||
onChange={(event, value) => {
|
onChange={(event, value) => {
|
||||||
if (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);
|
setLocoName(value.locationname);
|
||||||
fetchridershifts(value.applocationid);
|
fetchridershifts(value.applocationid);
|
||||||
setRiderdata({
|
setRiderdata({
|
||||||
...riderdata,
|
...riderdata,
|
||||||
partnerid: value.partnerid,
|
|
||||||
applocationid: value.applocationid
|
applocationid: value.applocationid
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,14 @@
|
|||||||
import { Avatar, Box, Chip, Grid, Paper, Stack, Typography, useMediaQuery } from '@mui/material';
|
import { useState } from 'react';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { Avatar, Box, Button, Chip, Grid, InputLabel, Paper, Stack, TextField, Typography } from '@mui/material';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import {
|
import { MdBadge, MdPerson, MdLocationOn, MdMail, MdShield, MdVerifiedUser, MdLock } from 'react-icons/md';
|
||||||
MdBadge,
|
|
||||||
MdPerson,
|
|
||||||
MdLocationOn,
|
|
||||||
MdMail,
|
|
||||||
MdPhone,
|
|
||||||
MdPlace,
|
|
||||||
MdMyLocation,
|
|
||||||
MdLocationCity,
|
|
||||||
MdMap,
|
|
||||||
MdMarkunreadMailbox,
|
|
||||||
MdVerifiedUser
|
|
||||||
} from 'react-icons/md';
|
|
||||||
import CircularLoader from 'components/CircularLoader';
|
import CircularLoader from 'components/CircularLoader';
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
import { getusers } from 'pages/api/api';
|
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) ----
|
// ---- shared design tokens (mirrors the DT block used across the console) ----
|
||||||
const DT = {
|
const DT = {
|
||||||
@@ -99,14 +91,72 @@ const SectionCard = ({ title, icon: Icon, children }) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const ViewProfile = () => {
|
const ViewProfile = () => {
|
||||||
const theme = useTheme();
|
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
|
||||||
const { data: userData, isLoading } = useQuery({
|
const { data: userData, isLoading } = useQuery({
|
||||||
queryKey: ['getuser'],
|
queryKey: ['getuser'],
|
||||||
queryFn: getusers
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -153,11 +203,11 @@ const ViewProfile = () => {
|
|||||||
{fullname}
|
{fullname}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap alignItems="center">
|
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap alignItems="center">
|
||||||
{userData?.authname && (
|
{roleLabel && (
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
icon={<MdVerifiedUser size={14} style={{ color: BRAND }} />}
|
icon={<MdVerifiedUser size={14} style={{ color: BRAND }} />}
|
||||||
label={userData.authname}
|
label={roleLabel}
|
||||||
sx={{
|
sx={{
|
||||||
bgcolor: soft(BRAND),
|
bgcolor: soft(BRAND),
|
||||||
color: BRAND,
|
color: BRAND,
|
||||||
@@ -167,19 +217,17 @@ const ViewProfile = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{userData?.userid != null && userData?.userid !== '' && (
|
{(userData?.id != null || userData?.userid != null) && (
|
||||||
<Typography variant="body2" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
<Typography variant="body2" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
||||||
ID #{userData.userid}
|
ID #{userData.id ?? userData.userid}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{userData?.applocation && (
|
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}>
|
||||||
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}>
|
<MdLocationOn size={15} />
|
||||||
<MdLocationOn size={15} />
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
{tenantLabel}
|
||||||
{userData.applocation}
|
</Typography>
|
||||||
</Typography>
|
</Stack>
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -190,42 +238,66 @@ const ViewProfile = () => {
|
|||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<SectionCard title="Account" icon={MdBadge}>
|
<SectionCard title="Account" icon={MdBadge}>
|
||||||
<Stack spacing={2.5}>
|
<Stack spacing={2.5}>
|
||||||
<InfoField icon={MdPerson} label="User Name" value={userData?.fullname} />
|
<InfoField icon={MdPerson} label="User Name" value={userData?.name || userData?.fullname} />
|
||||||
<InfoField icon={MdBadge} label="User ID" value={userData?.userid} />
|
<InfoField icon={MdBadge} label="User ID" value={userData?.id ?? userData?.userid} />
|
||||||
<InfoField icon={MdVerifiedUser} label="Auth Name" value={userData?.authname} />
|
<InfoField icon={MdMail} label="E-Mail" value={userData?.email} accent="#0ea5e9" />
|
||||||
<InfoField icon={MdLocationOn} label="App Location" value={userData?.applocation} />
|
<InfoField icon={MdVerifiedUser} label="Role" value={roleLabel} />
|
||||||
|
<InfoField icon={MdLocationOn} label="Tenant" value={tenantLabel} accent="#14b8a6" />
|
||||||
</Stack>
|
</Stack>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<SectionCard title="Contact" icon={MdMail}>
|
<SectionCard title="Security" icon={MdShield}>
|
||||||
<Stack spacing={2.5}>
|
<Stack spacing={2}>
|
||||||
<InfoField icon={MdPhone} label="Contact No" value={userData?.contactno} accent="#0ea5e9" />
|
<Stack spacing={1}>
|
||||||
<InfoField icon={MdMail} label="E-Mail" value={userData?.email} accent="#0ea5e9" />
|
<InputLabel htmlFor="current-password">Current Password</InputLabel>
|
||||||
<InfoField icon={MdPlace} label="Address" value={userData?.address} accent="#0ea5e9" />
|
<TextField
|
||||||
|
id="current-password"
|
||||||
|
type="password"
|
||||||
|
fullWidth
|
||||||
|
placeholder="Current password"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<InputLabel htmlFor="new-password">New Password</InputLabel>
|
||||||
|
<TextField
|
||||||
|
id="new-password"
|
||||||
|
type="password"
|
||||||
|
fullWidth
|
||||||
|
placeholder="New password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<InputLabel htmlFor="confirm-password">Confirm New Password</InputLabel>
|
||||||
|
<TextField
|
||||||
|
id="confirm-password"
|
||||||
|
type="password"
|
||||||
|
fullWidth
|
||||||
|
placeholder="Confirm new password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<MdLock size={16} />}
|
||||||
|
onClick={handleChangePassword}
|
||||||
|
disabled={changePasswordMutation.isPending}
|
||||||
|
sx={{ alignSelf: 'flex-start' }}
|
||||||
|
>
|
||||||
|
Update Password
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<SectionCard title="Location" icon={MdMap}>
|
|
||||||
<Grid container spacing={isMobile ? 2.5 : 3}>
|
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
|
||||||
<InfoField icon={MdMyLocation} label="Suburb" value={userData?.suburb} accent="#14b8a6" />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
|
||||||
<InfoField icon={MdLocationCity} label="City" value={userData?.city} accent="#14b8a6" />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
|
||||||
<InfoField icon={MdMap} label="State" value={userData?.state} accent="#14b8a6" />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} sm={6} md={3}>
|
|
||||||
<InfoField icon={MdMarkunreadMailbox} label="Postcode" value={userData?.postcode} accent="#14b8a6" />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</SectionCard>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ const MaintenanceComingSoon = Loadable(lazy(() => import('pages/maintenance/comi
|
|||||||
|
|
||||||
// render - sample page
|
// render - sample page
|
||||||
// const SamplePage = Loadable(lazy(() => import('pages/extra-pages/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 Dashboard = Loadable(lazy(() => import('pages/nearle/dashboard')));
|
||||||
|
|
||||||
const Orders = Loadable(lazy(() => import('pages/nearle/orders/orders')));
|
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 Tenants = Loadable(lazy(() => import('pages/nearle/clients/Tenants')));
|
||||||
const ClientsPricing = Loadable(lazy(() => import('pages/nearle/clientPricing/clientPricing')));
|
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 ViewProfile = Loadable(lazy(() => import('pages/nearle/viewProfile')));
|
||||||
|
|
||||||
const Createorder1 = Loadable(lazy(() => import('pages/nearle/orders/createorder1')));
|
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 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 OrdersSummary = Loadable(lazy(() => import('pages/nearle/reports/ordersSummary')));
|
||||||
const OrdersDetails = Loadable(lazy(() => import('pages/nearle/reports/ordersDetails')));
|
const OrdersDetails = Loadable(lazy(() => import('pages/nearle/reports/ordersDetails')));
|
||||||
const RidersSummary = Loadable(lazy(() => import('pages/nearle/reports/ridersSummary')));
|
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 Dispatch = Loadable(lazy(() => import('pages/nearle/dispatch/Dispatch')));
|
||||||
const DispatchPreview = Loadable(lazy(() => import('pages/nearle/dispatch/Preview')));
|
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 ||============================== //
|
// ==============================|| MAIN ROUTING ||============================== //
|
||||||
|
|
||||||
const MainRoutes = {
|
const MainRoutes = {
|
||||||
@@ -81,10 +85,6 @@ const MainRoutes = {
|
|||||||
path: 'pricing',
|
path: 'pricing',
|
||||||
element: <ClientsPricing />
|
element: <ClientsPricing />
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'requests',
|
|
||||||
element: <Requests />
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'riders',
|
path: 'riders',
|
||||||
element: <Riders />
|
element: <Riders />
|
||||||
@@ -106,10 +106,6 @@ const MainRoutes = {
|
|||||||
path: 'orders/createorders',
|
path: 'orders/createorders',
|
||||||
element: <MultipleOrders />
|
element: <MultipleOrders />
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'orders/details',
|
|
||||||
element: <Details />
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'clients/create',
|
path: 'clients/create',
|
||||||
element: <Createclient />
|
element: <Createclient />
|
||||||
@@ -138,6 +134,34 @@ const MainRoutes = {
|
|||||||
{
|
{
|
||||||
path: 'dispatch/preview',
|
path: 'dispatch/preview',
|
||||||
element: <DispatchPreview />
|
element: <DispatchPreview />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'customers',
|
||||||
|
element: <Customers />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'hubs',
|
||||||
|
element: <Hubs />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'vehicles',
|
||||||
|
element: <Vehicles />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'app-users',
|
||||||
|
element: <AppUsers />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'tripsheets',
|
||||||
|
element: <Tripsheets />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'exceptions',
|
||||||
|
element: <Exceptions />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'competitive-intel',
|
||||||
|
element: <CompetitiveIntel />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -154,10 +178,6 @@ const MainRoutes = {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
path: '/login',
|
|
||||||
element: <Login />
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/maintenance',
|
path: '/maintenance',
|
||||||
element: <CommonLayout />,
|
element: <CommonLayout />,
|
||||||
|
|||||||
38
src/utils/doormileAxios.js
Normal file
38
src/utils/doormileAxios.js
Normal file
@@ -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;
|
||||||
@@ -14,5 +14,13 @@
|
|||||||
"ordersummary": "Orders Summary",
|
"ordersummary": "Orders Summary",
|
||||||
"ordersdetails": "Orders Details",
|
"ordersdetails": "Orders Details",
|
||||||
"riderssummary": "Riders Summary",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user