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:
2026-08-06 19:26:23 +05:30
parent 0a36212ac8
commit 59f31c8adf
39 changed files with 7006 additions and 9170 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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=

View File

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

View File

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

View File

@@ -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)).
> >

View File

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

View File

@@ -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
View 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
View 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.

View File

@@ -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);
} }

View File

@@ -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" />,

View File

@@ -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:0021: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

View 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

View File

@@ -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)}
/>
</> </>
); );
}; };

View File

@@ -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

View File

@@ -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) =>

View File

@@ -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>

View File

@@ -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' }}>
&copy; 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;

View File

@@ -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:0021: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

View File

@@ -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>
); );
}; };

View File

@@ -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);
} }

View File

@@ -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);

View File

@@ -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).

View File

@@ -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 }}>

View File

@@ -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>
</> </>
); );

View File

@@ -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

View File

@@ -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&nbsp;#{userData.userid} ID&nbsp;#{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>
</> </>

View File

@@ -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 />,

View 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;

View File

@@ -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"
} }