Migrate console off jupiter.nearle.app to the Doormile Express API
Retires REACT_APP_URL/URL2/URL3 in favor of REACT_APP_DOORMILE_URL across every page (orders, deliveries, riders, tenants, pricing, profile, reports, dispatch). Fixes several field-mapping and envelope-check bugs found along the way, most notably that /admin/milers/:id routes (block, assign-vehicle, edit, notify) key off milerprofileid, not userid, and that a miler's real fields are phone/availabilitystatus/displayname, not contactno/status/ firstname+lastname (confirmed against a live read-only session). Also fixes several silent-failure bugs uncovered during that audit: order creation and order cancellation showed a success toast but gave no feedback at all on failure (createorder1.js had a dead notifyadmin() call that left the loading spinner stuck forever on every failed submit), and Tenants.js's pricing/profile updates never surfaced a failed response to the operator. The AI dispatch optimiser (routes.workolik.com/routemate.workolik.com) and its jupiter.nearle.app delivery-commit call remain untouched by design — separate solver service with no equivalent in the new API.
This commit is contained in:
@@ -5,6 +5,8 @@ metadata:
|
||||
version: 2.0.0
|
||||
---
|
||||
|
||||
> **⚠️ STALE as of the api.doormile.com migration.** Every `REACT_APP_URL` / `REACT_APP_URL2` endpoint below (jupiter.nearle.app) has been retired from the codebase. The console now calls `api.doormile.com` (`REACT_APP_DOORMILE_URL`) via `src/utils/doormileAxios.js` / `src/pages/api/doormileApi.js` — see `express-console-api.md` at the repo root for the current endpoint reference, and `src/pages/api/CLAUDE.md` for what the new backend has no equivalent for (zones/`applocationid`, live rider GPS logs, `/substitutions`, invoices, expense requests, several report breakdowns). The AI dispatch optimiser (routes.workolik.com / routemate.workolik.com) and the final-delivery-commit call are the one exception — they're unchanged, still hardcoded exactly as described below. The architecture/navigation/FCM sections below are still broadly accurate; only the API base and per-endpoint table are obsolete.
|
||||
|
||||
# NearlExpress Console — Project Reference
|
||||
|
||||
A React 18 operator console for the NearlExpress dispatch platform. Operators use it to manage orders, run the AI dispatch optimiser, watch a live map of riders, edit tenants/pricing/invoices, and pull BI reports. Production users are warehouse staff, not end customers.
|
||||
|
||||
10
.env
10
.env
@@ -4,15 +4,7 @@ GENERATE_SOURCEMAP = false
|
||||
## Backend API URL
|
||||
REACT_APP_API_URL=https://mock-data-api-nextjs.vercel.app/
|
||||
|
||||
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
|
||||
REACT_APP_URL2=https://jupiter.nearle.app/live/api/v2
|
||||
REACT_APP_URL3=https://jupiter.nearle.app/live/api/v3
|
||||
REACT_APP_STAFF_TOKEN=
|
||||
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk
|
||||
|
||||
|
||||
|
||||
|
||||
REACT_APP_DOORMILE_URL=https://api.doormile.com/api/v1
|
||||
|
||||
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk
|
||||
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
|
||||
REACT_APP_URL2=https://jupiter.nearle.app/live/api/v2
|
||||
REACT_APP_URL3=https://jupiter.nearle.app/live/api/v3
|
||||
REACT_APP_STAFF_TOKEN=
|
||||
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk
|
||||
|
||||
|
||||
REACT_APP_DOORMILE_URL=https://api.doormile.com/api/v1
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
|
||||
REACT_APP_URL2=
|
||||
REACT_APP_STAFF_TOKEN=
|
||||
REACT_APP_DOORMILE_URL=https://api.doormile.com/api/v1
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# NearlExpress Console — API Endpoints Reference
|
||||
|
||||
> **⚠️ STALE — describes the retired jupiter.nearle.app backend.** `src/pages/api/api.js` no longer calls any of the endpoints below; it now talks to `api.doormile.com` (`REACT_APP_DOORMILE_URL`) via `src/pages/api/doormileApi.js`. See `express-console-api.md` at the repo root for the current endpoint reference. Kept for historical context only.
|
||||
>
|
||||
> Auto-generated reference of every API call in `src/pages/api/api.js`.
|
||||
> Base URLs are resolved from the env files at the repo root.
|
||||
|
||||
|
||||
12
CLAUDE.md
12
CLAUDE.md
@@ -53,7 +53,7 @@ npm run lint
|
||||
```
|
||||
|
||||
- **Env files at repo root**: `env.staging` is committed; `.env.development` / `.env.production` are typically gitignored. Pull from a teammate when missing.
|
||||
- **Required env vars**: `REACT_APP_URL` (primary API base), `REACT_APP_URL2` (secondary API base — used for `/users/update`, `/tenants/update`, `/partners/getriderlogs`, archival `/orders/getorders`). No Maps API key is needed — maps and address search run on free Leaflet/OSM services. The optimiser URLs (`routes.workolik.com`, `routemate.workolik.com`) and the Jupiter auth URL (`jupiter.nearle.app`) are hardcoded — see the `nearlexpress-docs` skill.
|
||||
- **Required env vars**: `REACT_APP_DOORMILE_URL` — the only API base (`https://api.doormile.com/api/v1`), used by every `/admin/*` call via `utils/doormileAxios.js`. The old jupiter-backed `REACT_APP_URL` / `REACT_APP_URL2` / `REACT_APP_URL3` have been fully retired — do not reintroduce them. No Maps API key is needed — maps and address search run on free Leaflet/OSM services. The optimiser URLs (`routes.workolik.com`, `routemate.workolik.com`) and the final-delivery-commit URL (`jupiter.nearle.app`) remain hardcoded — they're a separate solver service with no equivalent in the new API, deliberately left untouched by the backend migration. See the `nearlexpress-docs` skill and `express-console-api.md`.
|
||||
- **Dev server runs on `http://localhost:3000`**. The user usually has it running already — assume it is up when reporting "reload to see it".
|
||||
|
||||
---
|
||||
@@ -66,7 +66,7 @@ npm run lint
|
||||
4. **Do not bypass the dispatch reconcile step.** After any manual edit on `/doormile/dispatch/preview` (rider swap, step reorder), the page **must** call `POST /optimization/reconcile-steps` before `POST /deliveries/createdeliveries`. Skipping this corrupts route sequences.
|
||||
5. **Do not commit `.env*` files** beyond `env.staging` (which is the agreed-shared staging baseline).
|
||||
6. **Do not introduce TypeScript files** (`.ts` / `.tsx`) into this repo. It is JavaScript; mixing creates lint and tooling friction.
|
||||
7. **Do not use absolute `http://localhost` URLs** in code. Always read from `process.env.REACT_APP_URL` / `REACT_APP_URL2`.
|
||||
7. **Do not use absolute `http://localhost` URLs** in code. Always read from `process.env.REACT_APP_DOORMILE_URL` (or use `doormileAxios`, which already defaults to it).
|
||||
8. **Do not log `userid`, `authname`, FCM tokens, or PII** to `console.log` in production paths. The codebase has many leftover `console.log` calls — when editing nearby, remove them rather than add more.
|
||||
9. **Do not add or remove items from the sidebar without updating `src/menu-items/nearle.js`** — the menu drives both display and i18n keys.
|
||||
10. **Do not use destructive git** (`reset --hard`, `push --force`, branch deletion) without explicit user instruction.
|
||||
@@ -248,8 +248,8 @@ These colour-code lifecycle states. Do **not** swap them for brand red — opera
|
||||
|
||||
## 8. State & auth
|
||||
|
||||
- **Auth state lives in `localStorage`**. The keys to know: `authname` (gate in `App.js`), `userid`, `roleid`, `userfcmtoken`, `applocations` (cached zone list). When adding auth-touching code, read these directly — there is no `useAuth()` hook.
|
||||
- **The 401 redirect** comes from `src/utils/axios.js`. Most pages bypass that interceptor by importing raw `axios`. If you need guaranteed 401 handling for a new flow, import from `utils/axios` instead.
|
||||
- **Auth state lives in `localStorage`**. The keys to know: `authname` (gate in `App.js` and `utils/session.js`'s `AUTH_PRESENCE_KEY` — unrelated to which backend authenticated the session, kept as the presence flag across the migration), `doormileToken` (the actual JWT sent as `Authorization: Bearer` on every `/admin/*` call — see `utils/doormileAxios.js`), `doormileUser`, `userid`, `roleid`, `tenantid`, `userfcmtoken`, `applocations` (cached zone list, now hub-derived). When adding auth-touching code, read these directly — there is no `useAuth()` hook.
|
||||
- **The 401 redirect for the Doormile API** comes from `utils/doormileAxios.js` (auto-attaches the bearer token; on 401 does a full `localStorage.clear()` + hard navigate to `/login`, matching `utils/session.js`'s `performSessionLogout` contract). `src/utils/axios.js` is a separate, older, unrelated mock-service client — don't confuse the two.
|
||||
- **Redux slices** live in `src/store/reducers/`. Use them only for cross-page state (FCM token, login user, sidebar menu open, global snackbar). Do **not** put per-page form state in Redux.
|
||||
|
||||
---
|
||||
@@ -278,8 +278,8 @@ These colour-code lifecycle states. Do **not** swap them for brand red — opera
|
||||
## 11. Common gotchas
|
||||
|
||||
- **`utc` plugin pollution**: `deliveries.js` extends dayjs with `utc` at module load. If you import dayjs elsewhere and call `.utc()` you'll get UTC behaviour even if you didn't ask. Match what the surrounding page does — `deliveries.js` deliberately bucket-parses in local time, not UTC, to stay in sync with the dispatch page.
|
||||
- **Two API bases**: most calls hit `REACT_APP_URL`. A handful hit `REACT_APP_URL2` (`/users/update`, `/tenants/update`, archival orders, rider logs). Check `pages/api/api.js` before changing one.
|
||||
- **The `applocationid` query param**: every list endpoint expects this — `0` means "All Zones". Pages default `appId = 0` and update it from `LocationAutocomplete`.
|
||||
- **One API base**: `REACT_APP_DOORMILE_URL` (`api.doormile.com/api/v1`), via `utils/doormileAxios.js`. See `src/pages/api/CLAUDE.md` for the full migration-era rule sheet, including what the new backend has no equivalent for.
|
||||
- **The `applocationid` "zone" concept no longer exists as a filterable resource** on the new API — it was a jupiter-only concept. `LocationAutocomplete`/`fetchAppLocations` now derive a picker list from `GET /admin/hubs` instead of a real zones endpoint; most list endpoints on the new backend don't accept an `applocationid` filter at all. Don't assume it's wired through end-to-end on a page you haven't checked.
|
||||
- **`role` gating** uses `localStorage.getItem('roleid')`. Some buttons are conditionally rendered based on it. Do not hide UI based on string equality alone — check existing patterns.
|
||||
- **Skeleton vs Loader vs LoaderWithImage** — these are different. Skeleton = per-row placeholder, Loader = full-screen backdrop blocking interaction, LoaderWithImage = inline branded spinner. Don't swap them.
|
||||
- **No Maps API key exists in this project anymore.** Address search/geocoding goes through `AddressAutocomplete.js` (Nominatim) and routing through OSRM — both free, no key. Do not add `process.env.REACT_APP_GOOGLE_MAPS_API_KEY` or any Google Maps script/dependency back in.
|
||||
|
||||
2
FLOW.md
2
FLOW.md
@@ -1,5 +1,7 @@
|
||||
# NearlExpress Console — Repository Flow Graph
|
||||
|
||||
> **⚠️ STALE — describes the retired jupiter.nearle.app backend.** The console now talks exclusively to `api.doormile.com` via `REACT_APP_DOORMILE_URL` (see `src/utils/doormileAxios.js`, `src/pages/api/doormileApi.js`, and `express-console-api.md` at the repo root for the current endpoint reference). The `REACT_APP_URL` / `REACT_APP_URL2` env vars and every jupiter endpoint below no longer exist in the codebase — this file is kept for historical/architectural context only, not as an accurate current reference.
|
||||
>
|
||||
> Human-readable navigation, action, and data-flow diagrams for `nearlexpress-xpressconsole-d0ee01adebe9`.
|
||||
> Open this file in a Mermaid-aware viewer (GitHub, VS Code with the *Markdown Preview Mermaid* extension, or [mermaid.live](https://mermaid.live)).
|
||||
>
|
||||
|
||||
@@ -50,10 +50,7 @@ REACT_APP_VERSION=v2.1.0
|
||||
GENERATE_SOURCEMAP=false
|
||||
|
||||
# Backend Services Endpoint URLs
|
||||
REACT_APP_API_URL=https://mock-data-api-nextjs.vercel.app/
|
||||
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
|
||||
REACT_APP_URL2=https://jupiter.nearle.app/live/api/v2
|
||||
REACT_APP_URL3=https://jupiter.nearle.app/live/api/v3
|
||||
REACT_APP_DOORMILE_URL=https://api.doormile.com/api/v1
|
||||
```
|
||||
|
||||
No Maps API key is required — maps and address search run on free Leaflet/OpenStreetMap (Nominatim + OSRM), not Google Maps.
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
REACT_APP_URL='https://jupiter.nearle.app/live/api/v1'
|
||||
REACT_APP_URL2=''
|
||||
REACT_APP_STAFF_TOKEN=''
|
||||
REACT_APP_DOORMILE_URL='https://api.doormile.com/api/v1'
|
||||
292
express-console-api.md
Normal file
292
express-console-api.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# Doormile Express Console — API reference
|
||||
|
||||
The console surface only (`/admin/*`). 89 routes: 1 login + 88 authenticated.
|
||||
Everything is under `https://api.doormile.com/api/v1`.
|
||||
|
||||
Miler-app, hub-console, customer-app and CRM routes are not in this document.
|
||||
|
||||
---
|
||||
|
||||
## Auth
|
||||
|
||||
```
|
||||
POST /api/v1/admin/login
|
||||
{ "email": "developer@doormile.com", "password": "admin@123" }
|
||||
```
|
||||
|
||||
Returns `{ success, token, user: { id, name, email, role, tenantid } }`.
|
||||
Send it on every other call as `Authorization: Bearer <token>`.
|
||||
|
||||
The token is a JWT carrying `userid`, `email`, `roleid`, `tenantid`, `configid`.
|
||||
Roles allowed on this group: **1 admin, 3 manager, 4 executive**. Anything else
|
||||
gets 401.
|
||||
|
||||
### Tenant scoping — read this before wiring any list screen
|
||||
|
||||
`tenantid` in the token decides what the account can see:
|
||||
|
||||
| Token `tenantid` | Who | Sees |
|
||||
|---|---|---|
|
||||
| `0` / null | Doormile's own staff | everything, all tenants |
|
||||
| set (e.g. `13`) | a client's console login | only that tenant's rows |
|
||||
|
||||
Scoping is applied server-side. A client login **cannot** widen it by sending
|
||||
`?tenantid=` or a body `tenantid` — on writes the server overwrites the field
|
||||
with the token's tenant. Build the UI as if the API returns exactly what the
|
||||
account is allowed to see, because it does.
|
||||
|
||||
Note the group is registered under a stale `// all open, no token required`
|
||||
comment in `routes.go`; the comment is wrong, `AuthMiddleware` is applied.
|
||||
|
||||
---
|
||||
|
||||
## Dashboard, profile, reports
|
||||
|
||||
| Method | Path | Notes |
|
||||
|---|---|---|
|
||||
| GET | `/admin/dashboard` | counts + today's numbers |
|
||||
| GET | `/admin/reports` | `?from=YYYY-MM-DD&to=YYYY-MM-DD`, defaults to today (IST) |
|
||||
| GET | `/admin/profile` | current account |
|
||||
| GET | `/admin/me` | alias of the above |
|
||||
| PUT | `/admin/profile/password` | `{ "current_password": "...", "new_password": "..." }` — snake_case |
|
||||
|
||||
## App users (staff logins)
|
||||
|
||||
| Method | Path |
|
||||
|---|---|
|
||||
| GET | `/admin/users` |
|
||||
| POST | `/admin/users` |
|
||||
| PUT | `/admin/users/:id` |
|
||||
| DELETE | `/admin/users/:id` |
|
||||
|
||||
## Partners (fleet / rider suppliers)
|
||||
|
||||
Not the same thing as a tenant. A partner supplies vehicles and riders; a tenant
|
||||
is a client Doormile delivers for.
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/partners` | |
|
||||
| POST | `/admin/partners` | `{ partnername, partnertypeid, contactno, status }` |
|
||||
| GET | `/admin/partners/:id` | |
|
||||
| PUT | `/admin/partners/:id` | |
|
||||
| DELETE | `/admin/partners/:id` | hard delete — no soft-delete column |
|
||||
|
||||
## Tenants (client companies)
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/tenants` | |
|
||||
| POST | `/admin/tenants` | `{ tenantname, primaryemail, primarycontact, status, requiredeliveryotp }` |
|
||||
| GET | `/admin/tenants/:id` | |
|
||||
| PUT | `/admin/tenants/:id` | `requiredeliveryotp` is a pointer — omit it to leave the setting alone |
|
||||
| DELETE | `/admin/tenants/:id` | hard delete |
|
||||
| GET | `/admin/tenants/:id/locations` | the client's sites (kitchens, branches, depots) |
|
||||
| POST | `/admin/tenants/:id/locations` | `{ locationname, address, city, state, pincode, latitude, longitude, isprimary, status }` |
|
||||
| PUT | `/admin/tenantlocations/:id` | note: **not** nested under the tenant |
|
||||
|
||||
`requiredeliveryotp` is opt-in per tenant and **off by default**. DailyGrubs runs
|
||||
without delivery OTP by decision.
|
||||
|
||||
## Tenant customers (a client's own end customers)
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/tenantcustomers` | |
|
||||
| POST | `/admin/tenantcustomers` | `{ firstname, lastname, phone, email }` |
|
||||
| GET | `/admin/tenantcustomers/:id` | |
|
||||
| PUT | `/admin/tenantcustomers/:id` | |
|
||||
| DELETE | `/admin/tenantcustomers/:id` | |
|
||||
|
||||
## B2C app customers
|
||||
|
||||
| Method | Path |
|
||||
|---|---|
|
||||
| GET | `/admin/customers` |
|
||||
| PATCH | `/admin/customers/:id` |
|
||||
|
||||
Tenant-scoped through their bookings — a client login sees only customers who
|
||||
have ordered through them.
|
||||
|
||||
## Hubs
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/hubs` | |
|
||||
| POST | `/admin/hubs` | `{ hubname, hubtype, applocationid, contactno, address, latitude, longitude, pincode, status }` |
|
||||
| GET | `/admin/hubs/:id` | |
|
||||
| PUT | `/admin/hubs/:id` | |
|
||||
| DELETE | `/admin/hubs/:id` | soft delete |
|
||||
|
||||
`hubtype`: `sorting_center` \| `delivery_hub`. `applocationid` is the city —
|
||||
Nagercoil is 5; read the rest from `GET /admin/hubs` rather than hardcoding.
|
||||
|
||||
## Vehicles
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/vehicles` | |
|
||||
| POST | `/admin/vehicles` | `{ vehicleno, vehicletype, maxweight, maxvolume, partnerid, batterypercentage, status }` |
|
||||
| GET | `/admin/vehicles/:id` | |
|
||||
| PUT | `/admin/vehicles/:id` | |
|
||||
| DELETE | `/admin/vehicles/:id` | soft delete |
|
||||
|
||||
## Milers (riders)
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/milers` | tenant-scoped |
|
||||
| POST | `/admin/milers` | see below |
|
||||
| GET | `/admin/milers/:id` | |
|
||||
| PUT | `/admin/milers/:id` | |
|
||||
| PUT | `/admin/milers/:id/block` | |
|
||||
| PUT | `/admin/milers/:id/assign-vehicle` | |
|
||||
| POST | `/admin/milers/:id/notify` | `{ title, message }` — `:id` is the **milerprofileid** |
|
||||
|
||||
```jsonc
|
||||
POST /admin/milers
|
||||
{
|
||||
"authname": "Murali",
|
||||
"email": "murali@dailygrubs.com",
|
||||
"contactno": "9876543210",
|
||||
"password": "1234",
|
||||
"displayname": "Murali S",
|
||||
"tenantid": 13,
|
||||
"defaultvehicletype": "Bike",
|
||||
"applocationid": 1,
|
||||
"hubid": 4 // optional; without it the rider is invisible to the hub console
|
||||
// configid defaults to 1001 — the partition the miler app logs in against.
|
||||
// Do not override it. Riders created without it could never log in.
|
||||
}
|
||||
```
|
||||
|
||||
## Bookings
|
||||
|
||||
| Method | Path | Notes |
|
||||
|---|---|---|
|
||||
| GET | `/admin/bookings` | tenant-scoped list |
|
||||
| POST | `/admin/expressbooking` | create one — passes CityGate |
|
||||
| POST | `/admin/expressbooking/bulk` | `{ "bookings": [ ... ] }`, max 200, per-row results |
|
||||
| GET | `/admin/bookings/:id` | 403 if outside your tenant |
|
||||
| POST | `/admin/bookings/:id/assign-miler` | |
|
||||
| POST | `/admin/bookings/:id/assign-vehicle` | |
|
||||
| PUT | `/admin/bookings/:id/status` | |
|
||||
| POST | `/admin/bookings/:id/cancel` | |
|
||||
| POST | `/admin/bookings/bulk-cancel` | |
|
||||
|
||||
```jsonc
|
||||
POST /admin/expressbooking
|
||||
{
|
||||
"tenantid": 13, // required; forced to your own tenant on client logins
|
||||
"pickuplocationid": 15, // a stored kitchen/branch — fills address, pincode and
|
||||
// coords for you, and is what makes per-site reporting work
|
||||
"customer_phone": "9876543210", // creates a Guest customer if unknown
|
||||
"customer_name": "Ramesh",
|
||||
"deliveryaddress": "12 Cross Cut Road, Gandhipuram",
|
||||
"deliverypincode": "641012",
|
||||
"deliverycity": "Coimbatore",
|
||||
"deliverylatitude": 11.0168,
|
||||
"deliverylongitude": 76.9558,
|
||||
"service_option": "Fast", // Normal | Fast | Superfast
|
||||
"finalprice": 120, // the order amount the tenant pays — passed through
|
||||
"notes": "Ring the bell",
|
||||
"parcels": [
|
||||
{ "itemcategory": "Food", "itemdescription": "2 meal boxes", "declaredvalue": 350 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Rules worth knowing:
|
||||
- `parcels` must be non-empty and `tenantid` must exist.
|
||||
- Pickup address + pincode are required **unless** `pickuplocationid` supplies them.
|
||||
- A `pickuplocationid` belonging to another tenant is rejected.
|
||||
- **CityGate**: the pickup pincode prefix must be an open city — `641`
|
||||
Coimbatore, `600` Chennai, `560` Bengaluru, `500` Hyderabad, `629` Nagercoil.
|
||||
Any other prefix is refused at the middleware, before the handler runs.
|
||||
- Matching 3-digit pickup and delivery prefixes = hyperlocal, and the parcel goes
|
||||
straight to `Out_for_Delivery` at pickup instead of routing via a hub.
|
||||
- Auto-assignment fires after commit as a background retry loop (5 attempts,
|
||||
2 min apart). The response returns before a rider is attached.
|
||||
|
||||
## Consignments
|
||||
|
||||
| Method | Path |
|
||||
|---|---|
|
||||
| GET | `/admin/consignments` |
|
||||
| GET | `/admin/consignments/:id` |
|
||||
| GET | `/admin/consignments/track/:trackingno` |
|
||||
| PUT | `/admin/consignments/:id/status` |
|
||||
|
||||
## Tripsheets (hub-to-hub transport)
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/tripsheets` | |
|
||||
| POST | `/admin/tripsheets` | `{ sourcehubid, destinationhubid, vehicleid, driveruserid }` |
|
||||
| GET | `/admin/tripsheets/:id` | |
|
||||
| POST | `/admin/tripsheets/:id/items` | `{ consignmentid }` |
|
||||
| DELETE | `/admin/tripsheets/:id/items/:itemid` | |
|
||||
| PUT | `/admin/tripsheets/:id/dispatch` | |
|
||||
| PUT | `/admin/tripsheets/:id/arrive` | |
|
||||
|
||||
## Pricing
|
||||
|
||||
| Method | Path | Notes |
|
||||
|---|---|---|
|
||||
| GET | `/admin/pricing` | tenant pricing rules |
|
||||
| POST | `/admin/pricing` | `{ tenantid, applocationid, vehicletype, baseprice, baseweight, priceperkg, basedistance, priceperkm, handlingcharges, effectivefrom, effectiveto, currency, priority, status }` |
|
||||
| PUT | `/admin/pricing/:id` | |
|
||||
| DELETE | `/admin/pricing/:id` | |
|
||||
| POST | `/admin/pricing/simulate` | quote without creating anything |
|
||||
| POST | `/admin/pricing/quote` | same handler as simulate |
|
||||
| GET | `/admin/doormile-pricing` | Doormile's own bands |
|
||||
| POST | `/admin/doormile-pricing` | |
|
||||
| PUT | `/admin/doormile-pricing/:id` | |
|
||||
| DELETE | `/admin/doormile-pricing/:id` | soft delete |
|
||||
|
||||
## Exceptions
|
||||
|
||||
| Method | Path | Body |
|
||||
|---|---|---|
|
||||
| GET | `/admin/exceptions` | |
|
||||
| POST | `/admin/exceptions` | `{ consignmentid, tripsheetid, hubid, exceptiontype, severity, description }` |
|
||||
| GET | `/admin/exceptions/:id` | |
|
||||
| PUT | `/admin/exceptions/:id/status` | `{ resolution, status }` — `Resolved` \| `Closed` |
|
||||
|
||||
`exceptiontype`: `Lost`, `Damaged`, `Misrouted`, `Receiver_Refused`,
|
||||
`Missing_Contents`, `Undeliverable`. `severity`: `Low`, `Medium`, `High`,
|
||||
`Critical`.
|
||||
|
||||
## Competitive intel
|
||||
|
||||
| Method | Path |
|
||||
|---|---|
|
||||
| GET/POST | `/admin/competitor-branches` |
|
||||
| PUT/DELETE | `/admin/competitor-branches/:id` |
|
||||
| GET/POST | `/admin/carrier-pricing` |
|
||||
| PUT/DELETE | `/admin/carrier-pricing/:id` |
|
||||
|
||||
---
|
||||
|
||||
## Conventions across every endpoint
|
||||
|
||||
- **Envelope**: `{ "success": true, "data": ... }` on success,
|
||||
`{ "success": false, "message": "..." }` on failure. Lists add `total`,
|
||||
paginated lists add `page`.
|
||||
- **Pagination**: `?pageno=1&pagesize=100`. Default 500, cap 1000.
|
||||
- **Rate limits**: 300/min per IP globally, 10/min shared across all credential
|
||||
endpoints. Behind the ingress this keys on the proxy IP unless
|
||||
`TRUSTED_PROXIES` is set.
|
||||
- **Timestamps** are IST (`Asia/Kolkata`) wall-clock in `timestamp without time
|
||||
zone` columns. Send dates as `YYYY-MM-DD`, not epochs.
|
||||
- **Soft delete** exists on Hub, Vehicle, Consignment, Tripsheet, TripsheetItem,
|
||||
ConsignmentException, DoormilePricing, CarrierPricing. Partner and Tenant are
|
||||
hard-deleted.
|
||||
|
||||
## Not exercised yet
|
||||
|
||||
Roughly half these routes have never had a real request against them. Exercised
|
||||
end-to-end so far: login, dashboard, reports, tenants + locations, milers
|
||||
(create/list/notify), expressbooking (single), bookings list/detail,
|
||||
assign-miler, consignments, profile password. Treat the rest as written but
|
||||
unproven.
|
||||
254
jupiter2doormile.md
Normal file
254
jupiter2doormile.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# jupiter → Doormile
|
||||
|
||||
What the old Nearle/jupiter API did, and what replaces it in Doormile. Two
|
||||
surfaces only — the **express console** and the **miler app**. Hub console, CRM
|
||||
and the B2C customer app are out of scope here.
|
||||
|
||||
Base URLs:
|
||||
|
||||
| | jupiter | Doormile |
|
||||
|---|---|---|
|
||||
| API | `jupiter.nearle.app/live/api/v1` | `api.doormile.com/api/v1` |
|
||||
| Write path | `queue.workolik.com` (TLS verify off, hardcoded IP pin) | same host, no side channel |
|
||||
|
||||
**Confidence marking.** Paths marked ✅ were read off real network logs from the
|
||||
live jupiter console. Paths marked ~ come from the prior-session analysis of the
|
||||
jupiter codebase and have not been re-confirmed against a live request — check
|
||||
the exact spelling before wiring anything to them.
|
||||
|
||||
Status: **Done** = built and hit with a real request · **Built** = written and
|
||||
compiled, never called · **Gap** = nothing replaces it yet · **Dropped** =
|
||||
deliberately not migrated.
|
||||
|
||||
---
|
||||
|
||||
## 1. Auth
|
||||
|
||||
| jupiter | Doormile | Status |
|
||||
|---|---|---|
|
||||
| ~ console login (undocumented in jupiter's own API docs — found only by reading the console source) | `POST /admin/login` → `{email, password}` | **Done** |
|
||||
| ~ rider login | `POST /miler/login` then `POST /miler/verify-pin` | **Done** |
|
||||
|
||||
Two real differences:
|
||||
|
||||
- Doormile splits rider login into **phone → PIN**, two calls. jupiter did it in
|
||||
one.
|
||||
- The Doormile console token carries **`tenantid`**. jupiter had no tenant
|
||||
concept on the login at all; every console user saw everything. This is the
|
||||
single biggest behavioural change for a client account.
|
||||
- `configid` must be **1001** on both miler calls. There is no jupiter
|
||||
equivalent — it's a Doormile login partition.
|
||||
|
||||
---
|
||||
|
||||
## 2. Express console
|
||||
|
||||
### 2.1 Rider screens
|
||||
|
||||
| jupiter | Doormile | Status |
|
||||
|---|---|---|
|
||||
| ✅ `GET /deliveries/getridersummary/?applocationid=&fromdate=&todate=` | `GET /admin/milers/summary?applocationid=&from=&to=&tenantid=&hubid=` | **Done** |
|
||||
| ~ rider list | `GET /admin/milers?applocationid=&hubid=&tenantid=` | **Done** |
|
||||
| ~ rider detail | `GET /admin/milers/:id` | **Done** |
|
||||
| ~ `riderlogs` (the 1.17M-row, zero-index table) | `GET /admin/milers/:id/logs?from=&to=&limit=` | **Done** |
|
||||
| ✅ `getriderlocationsummary` *(name confirmed, path inferred)* | covered by `milers/summary` (`currentlatitude/longitude`, `lastpingat`) and `milers/:id/logs` | **Done** |
|
||||
| — *(no jupiter equivalent)* | `GET /admin/milers/:id/activity?from=&to=` | **Done** |
|
||||
| ~ rider create/edit | `POST /admin/milers`, `PUT /admin/milers/:id` | **Done** |
|
||||
| ~ block rider | `PUT /admin/milers/:id/block` | **Built** |
|
||||
| ~ assign vehicle | `PUT /admin/milers/:id/assign-vehicle` | **Built** |
|
||||
| — | `POST /admin/milers/:id/notify` | **Done** |
|
||||
|
||||
Parameter translation: jupiter used `fromdate`/`todate`, Doormile uses
|
||||
`from`/`to`. Both `YYYY-MM-DD`. jupiter's `applocationid=0` meant "all cities";
|
||||
Doormile means the same by **omitting** the param.
|
||||
|
||||
### 2.2 Orders / deliveries
|
||||
|
||||
| jupiter | Doormile | Status |
|
||||
|---|---|---|
|
||||
| ✅ `GET /deliveries/getdeliveries/` | `GET /admin/bookings` + `GET /admin/consignments` | **Done** |
|
||||
| ~ `getdelivery` / `getorders` | `GET /admin/bookings/:id`, `GET /admin/consignments/:id` | **Done** |
|
||||
| ~ `POST /deliveries/createdeliveries` | `POST /admin/expressbooking` | **Done** |
|
||||
| ~ `createdeliveries` in bulk | `POST /admin/expressbooking/bulk` (max 200, per-row results) | **Built** |
|
||||
| ~ `PUT /deliveries/updatedelivery` | **split into 11 endpoints** — see §4 | **Done / partial** |
|
||||
| — | `GET /admin/bookings/:id/track` | **Done** |
|
||||
| — | `GET /admin/consignments/:id/logs` | **Done** |
|
||||
| — | `GET /admin/consignments/track/:trackingno` | **Built** |
|
||||
|
||||
Two jupiter bugs that do not carry over, by construction:
|
||||
|
||||
- `getdeliveries` returned **every row 21×** (unconstrained `LEFT JOIN
|
||||
tenantpricing`, `DISTINCT` over 87 columns that deduped nothing). Doormile's
|
||||
list endpoints are paginated (`pageno`/`pagesize`, default 500, cap 1000) and
|
||||
return one row per booking.
|
||||
- `createdeliveries` had a quadratic insert bug — a slice declared outside the
|
||||
loop kept accumulating, producing ~2× duplicate `deliveryqueues` rows
|
||||
(66,446 deliveries → 132,826 rows, confirmed live). `createExpressBooking` is
|
||||
a single transaction per booking; `/bulk` loops it and reports per-row.
|
||||
|
||||
### 2.3 Reporting
|
||||
|
||||
| jupiter | Doormile | Status |
|
||||
|---|---|---|
|
||||
| ✅ `GET /deliveries/getreportsummary/?applocationid=&tenantid=&locationid=&fromdate=&todate=` | `GET /admin/reports?from=&to=&tenantid=&locationid=&hubid=` | **Done** |
|
||||
| ~ `getlocationsummary` | `GET /admin/locations/summary?tenantid=&locationid=&from=&to=` | **Done** |
|
||||
| — | `GET /admin/dashboard?tenantid=` | **Done** |
|
||||
|
||||
`locationid` is supported: it narrows every figure to one client site, and
|
||||
`/admin/reports` now carries a `by_location` block alongside `by_hub`,
|
||||
`by_tenant` and `by_rider`.
|
||||
|
||||
**Attribution caveat.** Per-site figures group by `tenantlocationid` on the
|
||||
booking — a column added 2026-08-06. The pre-existing `pickuplocationid` column
|
||||
is *not* it: that one foreign-keys to `appcustomerlocations`, the B2C customer's
|
||||
saved address, so writing a client-site id into it fails the insert. Every
|
||||
booking created before 2026-08-06 has no site at all.
|
||||
|
||||
Since the console sends a kitchen's *address* rather than its id,
|
||||
`createExpressBooking` resolves the site itself — nearest stored location within
|
||||
150m, falling back to an address match. Bookings with no site are reported as
|
||||
their own `"Unattributed"` row rather than dropped, so per-site rows still add
|
||||
up to the summary total. Sending `tenantlocationid` explicitly is exact and
|
||||
always wins.
|
||||
|
||||
**`applocationid` (city) is still not a report parameter.** jupiter had it;
|
||||
Doormile filters by `hubid` instead. Only matters once one client runs in more
|
||||
than one city.
|
||||
|
||||
### 2.4 Tenants and their sites
|
||||
|
||||
| jupiter | Doormile | Status |
|
||||
|---|---|---|
|
||||
| ✅ `GET /tenants/gettenants/` | `GET /admin/tenants` | **Done** |
|
||||
| ✅ `GET /tenants/gettenantlocations/` | `GET /admin/tenants/:id/locations` | **Done** |
|
||||
| ~ `getlocations` / `getlocation` / `getlocationdetails` | same as above | **Done** |
|
||||
| ~ tenant create/edit | `POST /admin/tenants`, `PUT /admin/tenants/:id` | **Done** |
|
||||
| ~ location create/edit | `POST /admin/tenants/:id/locations`, `PUT /admin/tenantlocations/:id` | **Done** |
|
||||
| ~ `getbranches` | `GET /admin/hubs` — *jupiter "branches" ≈ Doormile hubs; verify this is the same concept before relying on it* | **Built** |
|
||||
| ~ `getlocationsummary` | `GET /admin/locations/summary` — see §2.3 | **Done** |
|
||||
|
||||
Doormile adds `locationname` on a tenant location. jupiter identified a site by
|
||||
its address alone, which does not distinguish two branches on one street.
|
||||
|
||||
---
|
||||
|
||||
## 3. Miler app
|
||||
|
||||
jupiter's rider app drove almost everything through one overloaded endpoint.
|
||||
Doormile gives each action its own route.
|
||||
|
||||
| jupiter action | Doormile | Status |
|
||||
|---|---|---|
|
||||
| ~ rider login | `POST /miler/login` + `POST /miler/verify-pin` | **Done** |
|
||||
| ~ PIN reset | `POST /miler/reset-pin` — **now admin-only**, see §5 | **Done** |
|
||||
| ~ location ping | `PUT /miler/location` | **Done** |
|
||||
| ~ availability toggle | `PUT /miler/availability` | **Done** |
|
||||
| ~ assignment list | `GET /miler/assignments`, `GET /miler/assignments/:id` | **Done** |
|
||||
| ~ accept | `POST /miler/assignments/:id/accept` | **Done** |
|
||||
| ~ reject | `POST /miler/assignments/:id/reject` | **Built** |
|
||||
| ~ rider logs write | `POST /miler/logs`, `POST /miler/status` | **Done** |
|
||||
| ~ per-delivery logs | `POST /miler/consignments/logs` | **Done** |
|
||||
| — | `POST /miler/duty/start`, `PUT /miler/duty/end`, `GET /miler/duty/current` | **Done** |
|
||||
| — | `POST /miler/breaks/start`, `PUT /miler/breaks/end` | **Done** |
|
||||
| — | `GET /miler/earnings` | **Done** |
|
||||
| — | `POST /miler/support`, `GET /miler/support` | **Built** |
|
||||
| — | `GET /miler/notifications` | **Done** |
|
||||
| — | `PATCH /miler/notifications/:id/read` | **Gap** — stub, persists nothing |
|
||||
|
||||
---
|
||||
|
||||
## 4. `PUT /deliveries/updatedelivery` — the 11-way split
|
||||
|
||||
This is the centre of the migration. jupiter overloaded one endpoint for **11
|
||||
distinct real actions**, distinguished only by which JSON fields happened to be
|
||||
non-empty. Each is now its own route with its own validation and its own status
|
||||
transition.
|
||||
|
||||
**Eight rider actions:**
|
||||
|
||||
| Action | Doormile |
|
||||
|---|---|
|
||||
| reached pickup | `POST /miler/bookings/:bookingid/reached` |
|
||||
| confirm parcel + dimensions | `POST /miler/bookings/:bookingid/parcel` |
|
||||
| collect payment | `POST /miler/bookings/:bookingid/payment` |
|
||||
| pickup complete | `POST /miler/bookings/:bookingid/pickup-complete` |
|
||||
| needs a bigger vehicle | `POST /miler/bookings/:bookingid/vehicle-required` |
|
||||
| cancel before pickup | `POST /miler/bookings/:bookingid/cancel` |
|
||||
| deliver | `POST /miler/consignments/:id/deliver` |
|
||||
| skip / failed attempt | `POST /miler/consignments/:id/skip` |
|
||||
|
||||
**Three console actions** that were bundled into the same rider endpoint:
|
||||
|
||||
| Action | Doormile |
|
||||
|---|---|
|
||||
| assign a rider | `POST /admin/bookings/:id/assign-miler` |
|
||||
| change status | `PUT /admin/bookings/:id/status` · `PUT /admin/consignments/:id/status` |
|
||||
| cancel | `POST /admin/bookings/:id/cancel` · `POST /admin/bookings/bulk-cancel` |
|
||||
|
||||
`pickup-complete` is the pivot the old system had no concept of: it converts the
|
||||
booking into a **consignment**, recomputes chargeable weight from the dimensions
|
||||
the rider entered, and decides routing — matching 3-digit pincode prefixes go
|
||||
straight to `Out_for_Delivery` (hyperlocal), everything else routes via a hub.
|
||||
|
||||
---
|
||||
|
||||
## 5. Behaviour changes that break a naive repoint
|
||||
|
||||
Response shapes are completely different — flat 87- and 92-column jupiter rows
|
||||
versus nested Doormile JSON. Every screen that parses a response needs
|
||||
rewriting, not repointing. Beyond that:
|
||||
|
||||
1. **Tenant scoping is real now.** A client console login sees only its own
|
||||
tenant. `?tenantid=` narrows for Doormile staff; a client passing another
|
||||
tenant's id gets **403**. Cross-tenant reads of a single resource return
|
||||
**404**, not 403, so ids aren't probeable. jupiter had none of this.
|
||||
2. **`configid` 1001** on every miler auth call. No jupiter equivalent.
|
||||
3. **PIN reset is admin-only.** jupiter let anyone reset a rider PIN with just a
|
||||
phone number, which is the login identifier, not a secret. Two calls took over
|
||||
any account. The rider app must not call `/miler/reset-pin` — route resets
|
||||
through ops.
|
||||
4. **Identity comes from the token, never the body.** jupiter's telemetry
|
||||
endpoints took `userid` from the request body. Doormile ignores it.
|
||||
5. **Telemetry lat/long/speed/battery are strings**, and
|
||||
`POST /miler/consignments/logs` takes a **bare JSON array**.
|
||||
6. **Delivery OTP is opt-in per tenant** (`Tenant.Requiredeliveryotp`), default
|
||||
off. Off for DailyGrubs. When on, it's verified server-side.
|
||||
7. **Dates**: `from`/`to`, not `fromdate`/`todate`. IST wall-clock throughout.
|
||||
8. **CityGate**: a booking's pickup pincode prefix must be an open city — `641`
|
||||
Coimbatore, `600` Chennai, `560` Bengaluru, `500` Hyderabad, `629` Nagercoil.
|
||||
jupiter had no such gate.
|
||||
|
||||
---
|
||||
|
||||
## 6. Gaps — jupiter did this, Doormile does not yet
|
||||
|
||||
| What | Detail |
|
||||
|---|---|
|
||||
| `applocationid` on reports | jupiter could filter a report by city. Doormile filters by `hubid`. Only bites when one client operates in several cities. |
|
||||
| **Route optimisation** | jupiter used external paid services (`routes.workolik.com`) for multi-stop sequencing. Nothing in Doormile replaces true stop-ordering. `HubBatchAssign` decides *who* gets a booking, not *what order* to run stops in. |
|
||||
| Notifications read-state | `PATCH /miler/notifications/:id/read` is a stub; no table exists. |
|
||||
| `riderkms` / `ridercharges` backfill | Populated on new deliveries only. Rows completed before 2026-08-06 read 0 and will not backfill themselves. |
|
||||
|
||||
---
|
||||
|
||||
## 7. Dropped on purpose
|
||||
|
||||
| What | Why |
|
||||
|---|---|
|
||||
| `/v1/substitutions` CRUD | Rider substitutions. Low traffic in the old system; Suriya's call. Revisit if it turns out to matter. |
|
||||
| jupiter's v2 endpoints | They wrote **only to Redis**, invisible to the v1/v3 Postgres reads — genuine split-brain, with a Redis `INCR` id space that could collide with the Postgres sequence. Doormile keeps Redis for ephemeral telemetry only; durable state is always Postgres. |
|
||||
| `queue.workolik.com` write path | Separate host with TLS verification disabled and a hardcoded IP pin. Not reproduced. |
|
||||
| ~20 never-populated columns on `orders`, 6 lat/lng pairs for 3 real points on `deliveries`, status spread across 6 text+timestamp column pairs | Replaced by a normalised schema with a real event-log table (`consignmenthistory`). |
|
||||
|
||||
---
|
||||
|
||||
## 8. What is not migrated at all
|
||||
|
||||
Nothing on the client side has moved. The rider Flutter app and
|
||||
`doormile_express_console` still call `jupiter.nearle.app`. Doormile having the
|
||||
endpoint does not mean traffic uses it.
|
||||
|
||||
Suggested order: pick one net-new console screen (the rider summary, or
|
||||
reports) and wire it to Doormile first — it replaces nothing live, so it is the
|
||||
cheapest real proof the cutover works. Then the higher-traffic screens
|
||||
(deliveries list, rider status updates), then the rider app.
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { forwardRef, useEffect, useState } from 'react';
|
||||
|
||||
import { Autocomplete, TextField, Avatar, Stack } from '@mui/material';
|
||||
import axios from 'axios';
|
||||
import { MdMyLocation } from 'react-icons/md';
|
||||
import { fetchAppLocations } from 'pages/api/api';
|
||||
|
||||
// Pill variant — opt-in via `pill` prop. Mirrors the design used across the
|
||||
// deliveries / dispatch filter rows (rounded pill, soft tinted bg, accent
|
||||
@@ -29,16 +29,13 @@ const LocationAutocomplete = forwardRef(
|
||||
const [locations, setLocations] = useState(JSON.parse(localStorage.getItem('applocations') || '[]'));
|
||||
|
||||
useEffect(() => {
|
||||
// Zones are derived from GET /admin/hubs (the new API has no dedicated
|
||||
// zones resource) — see fetchAppLocations in pages/api/api.js.
|
||||
const fetchLocations = async () => {
|
||||
try {
|
||||
const userid = localStorage.getItem('userid');
|
||||
if (!userid) return;
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
|
||||
if (response.data.status) {
|
||||
const updatedLocations = [...response.data.details, { locationname: 'All', applocationid: 0 }];
|
||||
localStorage.setItem('applocations', JSON.stringify(updatedLocations));
|
||||
setLocations(updatedLocations);
|
||||
}
|
||||
const updatedLocations = await fetchAppLocations();
|
||||
localStorage.setItem('applocations', JSON.stringify(updatedLocations));
|
||||
setLocations(updatedLocations);
|
||||
} catch (err) {
|
||||
console.error('Error fetching locations in LocationAutocomplete:', err);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,14 @@ import {
|
||||
UserOutlined,
|
||||
ProfileOutlined,
|
||||
TeamOutlined,
|
||||
MoneyCollectOutlined
|
||||
MoneyCollectOutlined,
|
||||
ContactsOutlined,
|
||||
ShopOutlined,
|
||||
CarryOutOutlined,
|
||||
IdcardOutlined,
|
||||
TruckOutlined,
|
||||
WarningOutlined,
|
||||
RadarChartOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
// icons
|
||||
@@ -50,7 +57,14 @@ const icons = {
|
||||
UserOutlined,
|
||||
ProfileOutlined,
|
||||
TeamOutlined,
|
||||
MoneyCollectOutlined
|
||||
MoneyCollectOutlined,
|
||||
ContactsOutlined,
|
||||
ShopOutlined,
|
||||
CarryOutOutlined,
|
||||
IdcardOutlined,
|
||||
TruckOutlined,
|
||||
WarningOutlined,
|
||||
RadarChartOutlined
|
||||
};
|
||||
|
||||
// ==============================|| MENU ITEMS - SUPPORT ||============================== //
|
||||
@@ -103,6 +117,63 @@ const nearle = {
|
||||
url: '/doormile/pricing',
|
||||
icon: icons.MoneyCollectOutlined
|
||||
},
|
||||
{
|
||||
id: 'customers',
|
||||
title: <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',
|
||||
title: <FormattedMessage id="reports" />,
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
# CLAUDE.md — `src/pages/api/`
|
||||
|
||||
Rules for editing `api.js`. This is the **central API layer** — every page calls into it. The root `CLAUDE.md` covers project-wide conventions; this file is the scoped rule sheet for the API layer specifically.
|
||||
Rules for editing `api.js` and `doormileApi.js` — the **central API layer**, every page calls into one of these two. The root `CLAUDE.md` covers project-wide conventions; this file is the scoped rule sheet for the API layer specifically.
|
||||
|
||||
**The old jupiter.nearle.app backend (`REACT_APP_URL` / `REACT_APP_URL2` / `REACT_APP_URL3`) has been fully retired from this project.** Every server call now goes through `utils/doormileAxios.js` → `api.doormile.com/api/v1`, either directly from `doormileApi.js` or via the thin wrappers in `api.js`. Do not reintroduce `process.env.REACT_APP_URL*` or raw `axios` calls to `jupiter.nearle.app` — the one sanctioned exception is the AI dispatch optimiser (`routes.workolik.com` / `routemate.workolik.com`) and its final delivery commit, which are a separate solver service with no equivalent in the new API and are explicitly left untouched (see `createOptimisationDeliveries`, `reconcileSteps`, `fetchBatchEfficiency`, `createAutomationDeliveries`, `finalCreatedeliveries` in `api.js`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Function signature patterns
|
||||
## 1. Two files, two roles
|
||||
|
||||
- **`doormileApi.js`** — one named export per `api.doormile.com/admin/*` endpoint (see `express-console-api.md` at the repo root for the full reference). Thin: build the URL, call `doormileAxios`, unwrap the envelope, return.
|
||||
- **`api.js`** — the page-facing layer. Existing function names/signatures were kept stable during the migration (so page files didn't all need parallel edits) and now delegate to `doormileApi.js` internally. New pages should import directly from `doormileApi.js` unless matching an existing `queryKey`-shaped call site in `api.js`.
|
||||
|
||||
## 2. Function signature patterns
|
||||
|
||||
### TanStack `useQuery` / `useInfiniteQuery` consumers
|
||||
Destructure from `queryKey` in the order the call site declared it. The leading `_` is the query name and is intentionally discarded.
|
||||
|
||||
```js
|
||||
// Plain query — destructure { queryKey }, skip the [0] name slot
|
||||
export const fetchorderscount = async ({ queryKey }) => {
|
||||
const [, appId, startdate, enddate, currentStatus, tenantid, locationid] = queryKey;
|
||||
const url = `${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&...`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.details;
|
||||
const [, , startdate, enddate] = queryKey;
|
||||
return (await getReports(startdate, enddate)) || {};
|
||||
};
|
||||
|
||||
// Infinite query — also receives pageParam (default to 1)
|
||||
export const fetchOrders = async ({ pageParam = 1, queryKey }) => {
|
||||
const [, appId, currentStatus, debouncedSearch, startdate, enddate, rowsPerPage, tenantid, locationid] = queryKey;
|
||||
const url = `${process.env.REACT_APP_URL}/orders/tenant/getorders/?applocationid=${appId}&...&pageno=${pageParam}&pagesize=${rowsPerPage}`;
|
||||
const response = await axios.get(url);
|
||||
const [, , , , , , rowsPerPage] = queryKey;
|
||||
const rows = (await getBookings(pageParam, rowsPerPage)) || [];
|
||||
return {
|
||||
rows: response.data.details,
|
||||
nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined
|
||||
rows,
|
||||
nextPage: rows.length === Number(rowsPerPage) ? pageParam + 1 : undefined
|
||||
};
|
||||
};
|
||||
```
|
||||
@@ -33,81 +35,67 @@ export const fetchOrders = async ({ pageParam = 1, queryKey }) => {
|
||||
**Hard rules:**
|
||||
- The query-key array order at the call site MUST match the destructure order here. Re-ordering one without the other silently breaks every caller.
|
||||
- Infinite queries return `{ rows, nextPage }`. `nextPage` is `undefined` when the page size wasn't filled (signals end-of-stream to `getNextPageParam`).
|
||||
- Some legacy functions return `{ data, nextPage }` instead of `{ rows, nextPage }` (e.g. `getallcustomers`). Match the existing shape rather than "fixing" it — call sites depend on the field name.
|
||||
- Many `queryKey` positions that used to carry `applocationid`/`tenantid`/`locationid`/`status` filters are now unused padding — the new API doesn't accept those as query params on most resources (see §3). Don't delete the positions; call sites still destructure by index.
|
||||
|
||||
---
|
||||
|
||||
## 2. Direct positional-argument calls
|
||||
A few functions take plain positional arguments instead of `queryKey` — usually when they're invoked from a `useMutation` or imperatively. Example: `getTenants(appId)`, `gettenantlocations(tenantid)`.
|
||||
## 3. Base client and what the new API can't do
|
||||
|
||||
Pick the signature based on how the function is called:
|
||||
- Called via `useQuery({ queryFn: fn })` → destructure `{ queryKey }`.
|
||||
- Called via `useQuery({ queryFn: () => fn(arg) })` → take positional args.
|
||||
- Called from a mutation or imperatively → take positional args.
|
||||
|
||||
---
|
||||
|
||||
## 3. Base URL selection
|
||||
|
||||
| Use base | When |
|
||||
| Use | When |
|
||||
|---|---|
|
||||
| `process.env.REACT_APP_URL` | Default for ~95% of endpoints |
|
||||
| `process.env.REACT_APP_URL2` | `/users/update`, `/tenants/update`, `/tenants/update/services`, archival `/orders/getorders`, `/partners/getriderlogs` |
|
||||
| Hardcoded `https://routes.workolik.com` | Bike solver + reconcile-steps + batch efficiency |
|
||||
| Hardcoded `https://routemate.workolik.com` | Auto / multi-trip solver |
|
||||
| Hardcoded `https://jupiter.nearle.app` | Login + final `/deliveries/createdeliveries` commit |
|
||||
| `doormileAxios` (via `doormileApi.js` functions) | Every `/admin/*` endpoint. Bearer token attached automatically from `localStorage.doormileToken`. |
|
||||
| Hardcoded `https://routes.workolik.com` | Bike solver + reconcile-steps + batch efficiency — untouched, separate service. |
|
||||
| Hardcoded `https://routemate.workolik.com` | Auto / multi-trip solver — untouched, separate service. |
|
||||
| Hardcoded `https://jupiter.nearle.app` | Final `/deliveries/createdeliveries` commit only — untouched, part of the same solver pipeline. |
|
||||
|
||||
When adding a new endpoint, check whether the backend actually serves it on URL or URL2 — don't guess. URL2 lives on a separate service.
|
||||
The new API has **no equivalent** for a number of things the old jupiter backend served. Functions covering these return safe empty defaults (`null`/`[]`/`{}`) rather than throwing — match this pattern for anything new that hits the same gap:
|
||||
|
||||
- Zones/`applocationid` as a filterable resource — derive a picker list from `GET /admin/hubs` instead (`fetchAppLocations` in `api.js`).
|
||||
- Live rider GPS/battery/periodic logs, per-delivery GPS trail/polyline logs.
|
||||
- Payment-mode types, item subcategories, rider shifts, vehicle/account types.
|
||||
- Tenant tab-count summaries, per-rider/per-location report breakdowns, rider login/checkout audit logs.
|
||||
- The `/substitutions` rider-absence feature (riders.js) — no backend at all.
|
||||
- Invoices and expense-request approvals — no backend at all (the `requests.js` page/route is disabled, not deleted).
|
||||
|
||||
Before assuming a new page's data need is covered, check `express-console-api.md` — roughly half its routes are documented as "written but unproven" against the real backend, so field names on GET responses are a best-effort guess in several places (flagged inline with comments where that's true).
|
||||
|
||||
---
|
||||
|
||||
## 4. Error handling pattern
|
||||
|
||||
```js
|
||||
try {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/...`);
|
||||
return response.data.details; // or .summary, .data, etc — depends on backend shape
|
||||
} catch (err) {
|
||||
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
||||
OpenToast(message);
|
||||
return null; // or return [] / {} — match what the caller expects
|
||||
}
|
||||
export const someFetch = async () => {
|
||||
try {
|
||||
return (await someDoormileApiFn()) || [];
|
||||
} catch (err) {
|
||||
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
||||
OpenToast(message);
|
||||
return []; // or null / {} — match what the caller expects
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
- Toast on failure via `OpenToast` from `components/third-party/OpenToast`. Don't `throw` — TanStack Query's `onError` is rarely wired by callers.
|
||||
- Return a sensible empty default (`null`, `[]`, `{}`) so the call site's destructuring doesn't crash.
|
||||
- Don't `console.log(err)` AND toast — toast is enough. Some legacy functions do both; new functions should not.
|
||||
- Return a sensible empty default so the call site's destructuring doesn't crash.
|
||||
|
||||
---
|
||||
|
||||
## 5. When NOT to add to `api.js`
|
||||
|
||||
The user has tolerated a number of pages making direct inline `axios.put` / `axios.post` calls for mutations (e.g. `Tenants.js` calls `axios.put('/tenants/update')` inline). Match the surrounding file:
|
||||
|
||||
- **Adding a new shared GET** → put it in `api.js`.
|
||||
- **Adding a one-off mutation used in only one page** → tolerated inline in that page; doesn't need a new export here.
|
||||
- **Adding a polling endpoint used by multiple pages** → put it here as a named export so the query cache keys are coherent.
|
||||
- **Adding a new shared GET against `/admin/*`** → put it in `doormileApi.js`.
|
||||
- **Adding a one-off mutation used in only one page** → tolerated inline in that page (importing straight from `doormileApi.js`); doesn't need a new export in `api.js`.
|
||||
- **Adding a polling endpoint used by multiple pages** → put it in `api.js` as a named export so the query cache keys are coherent.
|
||||
|
||||
---
|
||||
|
||||
## 6. Response shape quirks
|
||||
## 6. Response shape
|
||||
|
||||
Backend response shapes are inconsistent. Don't assume `response.data.details` — check what the specific endpoint returns:
|
||||
|
||||
| Backend field | Used by |
|
||||
|---|---|
|
||||
| `response.data.details` | Most list endpoints |
|
||||
| `response.data.summary` | `getcustomersummary`, `gettenantsummary`, `getpricinglist` summary calls |
|
||||
| `response.data.data` | `getRiderPeriodicLogs`, `getallcustomers` infinite-query page payload |
|
||||
| `response.data.message` | Mutation success messages (for toast) |
|
||||
| `response.data.status` | Boolean success flag — check before reading `.details` on some endpoints |
|
||||
|
||||
When unsure, log the response once during dev and pick the matching field.
|
||||
`doormileApi.js` functions already unwrap the envelope: GET/list functions return `response.data.data` directly; mutations return the full `response.data` (`{success, data|message}`) so callers can check `res.success`. Don't assume `.details` / `.status` — those were the old jupiter envelope and no longer apply anywhere in this layer.
|
||||
|
||||
---
|
||||
|
||||
## 7. Things that look broken but are intentional
|
||||
|
||||
- `const userid = localStorage.getItem('userid');` at module top — read once at module load, intentional for the `fetchAppLocations` helper. Don't move it inside the function.
|
||||
- Some functions take a `pageno` 0-indexed and others 1-indexed (`pageno: pageParam + 1` vs `pageno: pageParam`). Backend inconsistency — leave it alone unless you confirm the backend side.
|
||||
- A few legacy commented-out function bodies are kept above their current implementation as historical reference. Don't delete them in a drive-by edit.
|
||||
- Several `api.js` functions ignore most of their destructured `queryKey`/arguments — the new endpoint doesn't accept those filters. This is deliberate degradation, not a bug; see §3.
|
||||
- A handful of pages (`createorder1.js`, `multipleOrders.js`) synthesize fallback data client-side (e.g. a fixed 09:00–21:00 delivery-slot window) because the endpoint that used to supply it no longer exists. Look for the comment explaining why before "fixing" it.
|
||||
- `notifyRider` / `notifyMiler` now take a **miler profile ID**, not an FCM token — the new API's `/admin/milers/:id/notify` looks the device token up server-side. Don't pass a token here.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
557
src/pages/api/doormileApi.js
Normal file
557
src/pages/api/doormileApi.js
Normal file
@@ -0,0 +1,557 @@
|
||||
import doormileAxios, { DOORMILE_TOKEN_KEY, DOORMILE_USER_KEY } from 'utils/doormileAxios';
|
||||
|
||||
// API layer for the new Doormile Express admin backend (api.doormile.com/api/v1/admin/*).
|
||||
// Mirrors the shape of pages/api/api.js (named async exports, one per endpoint) but talks
|
||||
// to a different backend with a different envelope: { success, data } / { success, message }.
|
||||
// See express-console-api.md at the repo root for the full reference this was built from.
|
||||
// Not wired into any page yet — this is plumbing only.
|
||||
|
||||
const buildQuery = (params = {}) => {
|
||||
const usp = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') usp.append(key, value);
|
||||
});
|
||||
const qs = usp.toString();
|
||||
return qs ? `?${qs}` : '';
|
||||
};
|
||||
|
||||
// ==============================|| Auth ||============================== //
|
||||
|
||||
export const loginAdmin = async (email, password) => {
|
||||
const response = await doormileAxios.post('/admin/login', { email, password });
|
||||
if (response.data.success) {
|
||||
localStorage.setItem(DOORMILE_TOKEN_KEY, response.data.token);
|
||||
localStorage.setItem(DOORMILE_USER_KEY, JSON.stringify(response.data.user));
|
||||
}
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const logoutAdmin = () => {
|
||||
localStorage.removeItem(DOORMILE_TOKEN_KEY);
|
||||
localStorage.removeItem(DOORMILE_USER_KEY);
|
||||
};
|
||||
|
||||
// ==============================|| Dashboard, profile, reports ||============================== //
|
||||
|
||||
export const getDashboard = async () => {
|
||||
const response = await doormileAxios.get('/admin/dashboard');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
// jupiter used fromdate/todate + applocationid (city); Doormile uses from/to
|
||||
// and no city filter at all (filters by hubid instead — see
|
||||
// jupiter2doormile.md §2.3/§6). Response carries by_location/by_hub/by_tenant/
|
||||
// by_rider blocks per that doc, not documented in express-console-api.md.
|
||||
export const getReports = async (from, to, tenantid, locationid, hubid) => {
|
||||
const response = await doormileAxios.get(`/admin/reports${buildQuery({ from, to, tenantid, locationid, hubid })}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
// Confirmed against jupiter2doormile.md (Status: Done) as the replacement for
|
||||
// jupiter's getlocationsummary — per-tenant-site report breakdown. Not in
|
||||
// express-console-api.md.
|
||||
export const getLocationsSummary = async (tenantid, locationid, from, to) => {
|
||||
const response = await doormileAxios.get(`/admin/locations/summary${buildQuery({ tenantid, locationid, from, to })}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getProfile = async () => {
|
||||
const response = await doormileAxios.get('/admin/profile');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateProfilePassword = async (current_password, new_password) => {
|
||||
const response = await doormileAxios.put('/admin/profile/password', { current_password, new_password });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ==============================|| App users (staff logins) ||============================== //
|
||||
|
||||
export const getAppUsers = async () => {
|
||||
const response = await doormileAxios.get('/admin/users');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createAppUser = async (data) => {
|
||||
const response = await doormileAxios.post('/admin/users', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateAppUser = async (id, data) => {
|
||||
const response = await doormileAxios.put(`/admin/users/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteAppUser = async (id) => {
|
||||
const response = await doormileAxios.delete(`/admin/users/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ==============================|| Partners (fleet / rider suppliers) ||============================== //
|
||||
|
||||
export const getPartners = async () => {
|
||||
const response = await doormileAxios.get('/admin/partners');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createPartner = async (data) => {
|
||||
const response = await doormileAxios.post('/admin/partners', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getPartner = async (id) => {
|
||||
const response = await doormileAxios.get(`/admin/partners/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updatePartner = async (id, data) => {
|
||||
const response = await doormileAxios.put(`/admin/partners/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deletePartner = async (id) => {
|
||||
const response = await doormileAxios.delete(`/admin/partners/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ==============================|| Tenants (client companies) ||============================== //
|
||||
|
||||
export const getAdminTenants = async () => {
|
||||
const response = await doormileAxios.get('/admin/tenants');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createAdminTenant = async (data) => {
|
||||
const response = await doormileAxios.post('/admin/tenants', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getAdminTenant = async (id) => {
|
||||
const response = await doormileAxios.get(`/admin/tenants/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateAdminTenant = async (id, data) => {
|
||||
// requiredeliveryotp is a pointer server-side — omit the key entirely to leave the setting alone.
|
||||
const response = await doormileAxios.put(`/admin/tenants/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteAdminTenant = async (id) => {
|
||||
const response = await doormileAxios.delete(`/admin/tenants/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTenantLocations = async (tenantId) => {
|
||||
const response = await doormileAxios.get(`/admin/tenants/${tenantId}/locations`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createTenantLocation = async (tenantId, data) => {
|
||||
const response = await doormileAxios.post(`/admin/tenants/${tenantId}/locations`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateTenantLocation = async (locationId, data) => {
|
||||
// Not nested under the tenant — PUT /admin/tenantlocations/:id.
|
||||
const response = await doormileAxios.put(`/admin/tenantlocations/${locationId}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ==============================|| Tenant customers (a client's own end customers) ||============================== //
|
||||
|
||||
export const getTenantCustomers = async () => {
|
||||
const response = await doormileAxios.get('/admin/tenantcustomers');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createTenantCustomer = async (data) => {
|
||||
const response = await doormileAxios.post('/admin/tenantcustomers', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTenantCustomer = async (id) => {
|
||||
const response = await doormileAxios.get(`/admin/tenantcustomers/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateTenantCustomer = async (id, data) => {
|
||||
const response = await doormileAxios.put(`/admin/tenantcustomers/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteTenantCustomer = async (id) => {
|
||||
const response = await doormileAxios.delete(`/admin/tenantcustomers/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ==============================|| B2C app customers ||============================== //
|
||||
|
||||
export const getAdminCustomers = async () => {
|
||||
const response = await doormileAxios.get('/admin/customers');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateAdminCustomer = async (id, data) => {
|
||||
const response = await doormileAxios.patch(`/admin/customers/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ==============================|| Hubs ||============================== //
|
||||
|
||||
export const getHubs = async () => {
|
||||
const response = await doormileAxios.get('/admin/hubs');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createHub = async (data) => {
|
||||
const response = await doormileAxios.post('/admin/hubs', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getHub = async (id) => {
|
||||
const response = await doormileAxios.get(`/admin/hubs/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateHub = async (id, data) => {
|
||||
const response = await doormileAxios.put(`/admin/hubs/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteHub = async (id) => {
|
||||
// Soft delete server-side.
|
||||
const response = await doormileAxios.delete(`/admin/hubs/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ==============================|| Vehicles ||============================== //
|
||||
|
||||
export const getVehicles = async () => {
|
||||
const response = await doormileAxios.get('/admin/vehicles');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createVehicle = async (data) => {
|
||||
const response = await doormileAxios.post('/admin/vehicles', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getVehicle = async (id) => {
|
||||
const response = await doormileAxios.get(`/admin/vehicles/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateVehicle = async (id, data) => {
|
||||
const response = await doormileAxios.put(`/admin/vehicles/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteVehicle = async (id) => {
|
||||
// Soft delete server-side.
|
||||
const response = await doormileAxios.delete(`/admin/vehicles/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ==============================|| Milers (riders) ||============================== //
|
||||
|
||||
export const getMilers = async () => {
|
||||
// Tenant-scoped server-side from the token — no tenantid param needed/accepted.
|
||||
const response = await doormileAxios.get('/admin/milers');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createMiler = async (data) => {
|
||||
// configid defaults to 1001 server-side (the miler-app login partition) — do not override it.
|
||||
const response = await doormileAxios.post('/admin/milers', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getMiler = async (id) => {
|
||||
const response = await doormileAxios.get(`/admin/milers/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateMiler = async (id, data) => {
|
||||
const response = await doormileAxios.put(`/admin/milers/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const blockMiler = async (id, data) => {
|
||||
const response = await doormileAxios.put(`/admin/milers/${id}/block`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const assignMilerVehicle = async (id, data) => {
|
||||
const response = await doormileAxios.put(`/admin/milers/${id}/assign-vehicle`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const notifyMiler = async (milerProfileId, title, message) => {
|
||||
const response = await doormileAxios.post(`/admin/milers/${milerProfileId}/notify`, { title, message });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Confirmed against jupiter2doormile.md (Status: Done — tested against a real
|
||||
// request), not in express-console-api.md. jupiter used fromdate/todate;
|
||||
// Doormile uses from/to (both YYYY-MM-DD). Aggregate rider KPI counts +
|
||||
// current position (currentlatitude/currentlongitude/lastpingat per the doc —
|
||||
// unconfirmed beyond those three field names).
|
||||
export const getMilerSummary = async (applocationid, from, to, tenantid, hubid) => {
|
||||
const response = await doormileAxios.get(`/admin/milers/summary${buildQuery({ applocationid, from, to, tenantid, hubid })}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
// Replaces jupiter's `riderlogs` table — the rider's GPS/status ping history.
|
||||
export const getMilerLogs = async (id, from, to, limit) => {
|
||||
const response = await doormileAxios.get(`/admin/milers/${id}/logs${buildQuery({ from, to, limit })}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
// No jupiter equivalent — new in Doormile.
|
||||
export const getMilerActivity = async (id, from, to) => {
|
||||
const response = await doormileAxios.get(`/admin/milers/${id}/activity${buildQuery({ from, to })}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
// ==============================|| Bookings ||============================== //
|
||||
|
||||
export const getBookings = async (pageno, pagesize) => {
|
||||
const response = await doormileAxios.get(`/admin/bookings${buildQuery({ pageno, pagesize })}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createExpressBooking = async (data) => {
|
||||
// tenantid is forced to the caller's own tenant server-side on client logins.
|
||||
// CityGate applies: pickup pincode prefix must be an open city (641/600/560/500/629).
|
||||
const response = await doormileAxios.post('/admin/expressbooking', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const createExpressBookingBulk = async (bookings) => {
|
||||
// Max 200 per call; response carries per-row results.
|
||||
const response = await doormileAxios.post('/admin/expressbooking/bulk', { bookings });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getBooking = async (id) => {
|
||||
const response = await doormileAxios.get(`/admin/bookings/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const assignMilerToBooking = async (id, data) => {
|
||||
const response = await doormileAxios.post(`/admin/bookings/${id}/assign-miler`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const assignVehicleToBooking = async (id, data) => {
|
||||
const response = await doormileAxios.post(`/admin/bookings/${id}/assign-vehicle`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateBookingStatus = async (id, data) => {
|
||||
const response = await doormileAxios.put(`/admin/bookings/${id}/status`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const cancelBooking = async (id, data) => {
|
||||
const response = await doormileAxios.post(`/admin/bookings/${id}/cancel`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const bulkCancelBookings = async (bookingIds) => {
|
||||
const response = await doormileAxios.post('/admin/bookings/bulk-cancel', { bookingIds });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Confirmed against jupiter2doormile.md (Status: Done), not in
|
||||
// express-console-api.md.
|
||||
export const getBookingTrack = async (id) => {
|
||||
const response = await doormileAxios.get(`/admin/bookings/${id}/track`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
// ==============================|| Consignments ||============================== //
|
||||
|
||||
export const getConsignments = async () => {
|
||||
const response = await doormileAxios.get('/admin/consignments');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
// Confirmed against jupiter2doormile.md (Status: Done) — the actual GPS/status
|
||||
// trail for a consignment, not in express-console-api.md.
|
||||
export const getConsignmentLogs = async (id) => {
|
||||
const response = await doormileAxios.get(`/admin/consignments/${id}/logs`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const getConsignment = async (id) => {
|
||||
const response = await doormileAxios.get(`/admin/consignments/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const trackConsignment = async (trackingno) => {
|
||||
const response = await doormileAxios.get(`/admin/consignments/track/${trackingno}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateConsignmentStatus = async (id, data) => {
|
||||
const response = await doormileAxios.put(`/admin/consignments/${id}/status`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ==============================|| Tripsheets (hub-to-hub transport) ||============================== //
|
||||
|
||||
export const getTripsheets = async () => {
|
||||
const response = await doormileAxios.get('/admin/tripsheets');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createTripsheet = async (data) => {
|
||||
const response = await doormileAxios.post('/admin/tripsheets', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getTripsheet = async (id) => {
|
||||
const response = await doormileAxios.get(`/admin/tripsheets/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const addTripsheetItem = async (id, consignmentid) => {
|
||||
const response = await doormileAxios.post(`/admin/tripsheets/${id}/items`, { consignmentid });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const removeTripsheetItem = async (id, itemId) => {
|
||||
const response = await doormileAxios.delete(`/admin/tripsheets/${id}/items/${itemId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const dispatchTripsheet = async (id) => {
|
||||
const response = await doormileAxios.put(`/admin/tripsheets/${id}/dispatch`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const arriveTripsheet = async (id) => {
|
||||
const response = await doormileAxios.put(`/admin/tripsheets/${id}/arrive`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ==============================|| Pricing ||============================== //
|
||||
|
||||
export const getAdminPricing = async () => {
|
||||
const response = await doormileAxios.get('/admin/pricing');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createAdminPricing = async (data) => {
|
||||
const response = await doormileAxios.post('/admin/pricing', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateAdminPricing = async (id, data) => {
|
||||
const response = await doormileAxios.put(`/admin/pricing/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteAdminPricing = async (id) => {
|
||||
const response = await doormileAxios.delete(`/admin/pricing/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const simulatePricing = async (data) => {
|
||||
const response = await doormileAxios.post('/admin/pricing/simulate', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const quotePricing = async (data) => {
|
||||
// Same handler as simulate.
|
||||
const response = await doormileAxios.post('/admin/pricing/quote', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getDoormilePricing = async () => {
|
||||
const response = await doormileAxios.get('/admin/doormile-pricing');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createDoormilePricing = async (data) => {
|
||||
const response = await doormileAxios.post('/admin/doormile-pricing', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateDoormilePricing = async (id, data) => {
|
||||
const response = await doormileAxios.put(`/admin/doormile-pricing/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteDoormilePricing = async (id) => {
|
||||
// Soft delete server-side.
|
||||
const response = await doormileAxios.delete(`/admin/doormile-pricing/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ==============================|| Exceptions ||============================== //
|
||||
|
||||
export const getExceptions = async () => {
|
||||
const response = await doormileAxios.get('/admin/exceptions');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createException = async (data) => {
|
||||
const response = await doormileAxios.post('/admin/exceptions', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getException = async (id) => {
|
||||
const response = await doormileAxios.get(`/admin/exceptions/${id}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const updateExceptionStatus = async (id, status, resolution) => {
|
||||
// status: 'Resolved' | 'Closed'.
|
||||
const response = await doormileAxios.put(`/admin/exceptions/${id}/status`, { status, resolution });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ==============================|| Competitive intel ||============================== //
|
||||
|
||||
export const getCompetitorBranches = async () => {
|
||||
const response = await doormileAxios.get('/admin/competitor-branches');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createCompetitorBranch = async (data) => {
|
||||
const response = await doormileAxios.post('/admin/competitor-branches', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateCompetitorBranch = async (id, data) => {
|
||||
const response = await doormileAxios.put(`/admin/competitor-branches/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteCompetitorBranch = async (id) => {
|
||||
const response = await doormileAxios.delete(`/admin/competitor-branches/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getCarrierPricing = async () => {
|
||||
const response = await doormileAxios.get('/admin/carrier-pricing');
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const createCarrierPricing = async (data) => {
|
||||
const response = await doormileAxios.post('/admin/carrier-pricing', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateCarrierPricing = async (id, data) => {
|
||||
const response = await doormileAxios.put(`/admin/carrier-pricing/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteCarrierPricing = async (id) => {
|
||||
const response = await doormileAxios.delete(`/admin/carrier-pricing/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState, useRef, Fragment } from 'react';
|
||||
import MainCard from 'components/MainCard';
|
||||
import axios from 'axios';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Loader from 'components/Loader';
|
||||
import { Empty } from 'antd';
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
IconButton,
|
||||
Box,
|
||||
Grid,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Collapse,
|
||||
@@ -39,6 +37,8 @@ import {
|
||||
Skeleton,
|
||||
Avatar,
|
||||
Paper,
|
||||
Select,
|
||||
MenuItem,
|
||||
useMediaQuery
|
||||
} from '@mui/material';
|
||||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||||
@@ -65,7 +65,12 @@ import {
|
||||
MdOutlinePendingActions,
|
||||
MdOutlineCancel,
|
||||
MdMyLocation,
|
||||
MdPersonPin
|
||||
MdPersonPin,
|
||||
MdPlace,
|
||||
MdAdd,
|
||||
MdEdit,
|
||||
MdDeleteOutline,
|
||||
MdPeopleAlt
|
||||
} from 'react-icons/md';
|
||||
import { LuMail } from 'react-icons/lu';
|
||||
import { BiUser } from 'react-icons/bi';
|
||||
@@ -73,8 +78,21 @@ import LocationAutocomplete from 'components/nearle_components/LocationAutocompl
|
||||
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
||||
import PageHeader from 'components/nearle_components/PageHeader';
|
||||
import StatCard from 'components/nearle_components/StatCard';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { getalltenants, gettenantsummary } from 'pages/api/api';
|
||||
import {
|
||||
getAdminPricing,
|
||||
getDoormilePricing,
|
||||
createAdminPricing,
|
||||
updateAdminTenant,
|
||||
getTenantLocations,
|
||||
createTenantLocation,
|
||||
updateTenantLocation,
|
||||
getTenantCustomers,
|
||||
createTenantCustomer,
|
||||
updateTenantCustomer,
|
||||
deleteTenantCustomer
|
||||
} from 'pages/api/doormileApi';
|
||||
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
|
||||
|
||||
// ============================================================================
|
||||
@@ -103,24 +121,6 @@ const soft = (c) => a(c, '18');
|
||||
const ring = (c) => a(c, '26');
|
||||
const edge = (c) => a(c, '55');
|
||||
|
||||
const pillFieldSx = (color) => ({
|
||||
cursor: 'pointer',
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '10px',
|
||||
bgcolor: '#ffffff',
|
||||
fontWeight: 600,
|
||||
color: DT.textPrimary,
|
||||
paddingRight: '8px',
|
||||
cursor: 'pointer',
|
||||
transition: 'border-color 0.15s, box-shadow 0.15s, background-color 0.2s',
|
||||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||||
'&:hover fieldset': { borderColor: '#cbd5e1' },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }
|
||||
},
|
||||
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: color }
|
||||
});
|
||||
|
||||
// Status palette — drives the pill tabs and per-row badges.
|
||||
const STATUS_META = {
|
||||
active: { label: 'Active', color: '#10b981', icon: MdCheckCircle, statusKey: 'active' },
|
||||
@@ -168,6 +168,370 @@ const AccentAvatar = ({ color, selected, size = 24, children }) => (
|
||||
</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||============================== //
|
||||
const Clients1 = () => {
|
||||
const textFieldRef = useRef(null);
|
||||
@@ -189,7 +553,7 @@ const Clients1 = () => {
|
||||
const [status, setstatus] = useState('active');
|
||||
const [selectedCustomer, setSelectedCustomer] = useState({}); // to edit
|
||||
const [dialogopen, setDialogopen] = useState(false);
|
||||
const [appPricing, setAppPricing] = useState([]);
|
||||
const [, setAppPricing] = useState([]);
|
||||
const [selectedPricing, setSelectedPricing] = useState({});
|
||||
const [isPrice, setIsprice] = useState(true);
|
||||
const [city, setCity] = useState('');
|
||||
@@ -197,7 +561,6 @@ const Clients1 = () => {
|
||||
const [latlong, setLatlong] = useState({});
|
||||
const [editClient, setEditClient] = useState({});
|
||||
const [page, setPage] = useState(0);
|
||||
const [tabStatus, setTabStatus] = useState('Active');
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
@@ -212,7 +575,6 @@ const Clients1 = () => {
|
||||
};
|
||||
|
||||
const handleChange = (event, newValue) => {
|
||||
setTabStatus(newValue == 0 ? 'Active' : newValue == 1 ? 'Pending' : 'InActive');
|
||||
setValue0(newValue);
|
||||
setSearchword('');
|
||||
setPage(0);
|
||||
@@ -228,6 +590,8 @@ const Clients1 = () => {
|
||||
|
||||
const [openRowIndex1, setOpenRowIndex1] = useState(null); // Initially no row is open for collapsible section 1
|
||||
const [openRowIndex2, setOpenRowIndex2] = useState(null); // Initially no row is open for collapsible section 2
|
||||
const [locationsDialogTenant, setLocationsDialogTenant] = useState(null);
|
||||
const [customersDialogTenant, setCustomersDialogTenant] = useState(null);
|
||||
// Handle toggling for collapsible section 1
|
||||
const handleCollapseToggle1 = (rowIndex) => {
|
||||
setOpenRowIndex1((prevIndex) => (prevIndex === rowIndex ? null : rowIndex));
|
||||
@@ -348,18 +712,14 @@ const Clients1 = () => {
|
||||
|
||||
/* ============================================= || fetchclientpricelist || ============================================= */
|
||||
|
||||
// /admin/pricing has no tenantid query param documented — filter client-side.
|
||||
const fetchclientpricelist = async () => {
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL}/tenants/getpricinglist/?tenantid=${selectedTenid}`)
|
||||
.then((res) => {
|
||||
console.log('getpricinglist', res);
|
||||
if (res.data.status) {
|
||||
setClientpricelist(res.data.details);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
try {
|
||||
const pricing = await getAdminPricing();
|
||||
setClientpricelist((pricing || []).filter((p) => p.tenantid === selectedTenid));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
selectedTenid && fetchclientpricelist();
|
||||
@@ -368,9 +728,8 @@ const Clients1 = () => {
|
||||
/* ============================================= || fetchTenanatPricing || ============================================= */
|
||||
const fetchTenanatPricing = async (id) => {
|
||||
try {
|
||||
let tenantPricing = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${id}`);
|
||||
console.log('tenantPricing', tenantPricing.data.details);
|
||||
setTenanatPricing(tenantPricing.data.details);
|
||||
const pricing = await getAdminPricing();
|
||||
setTenanatPricing((pricing || []).filter((p) => p.tenantid === id));
|
||||
} catch (error) {
|
||||
console.log('fetchTenanatPricing', error);
|
||||
}
|
||||
@@ -382,44 +741,40 @@ const Clients1 = () => {
|
||||
|
||||
const tenantupdate = async (tenid) => {
|
||||
setisloader(true);
|
||||
let targetId;
|
||||
let updateData;
|
||||
if (tenid == -1) {
|
||||
updateData = {
|
||||
tenantid: selectedTenid,
|
||||
approved: 1
|
||||
};
|
||||
targetId = selectedTenid;
|
||||
updateData = { approved: 1 };
|
||||
} else {
|
||||
updateData = { tenantid: tenid, status: status === 'active' ? 'InActive' : 'Active' };
|
||||
targetId = tenid;
|
||||
updateData = { status: status === 'active' ? 'InActive' : 'Active' };
|
||||
}
|
||||
|
||||
await axios
|
||||
.put(`${process.env.REACT_APP_URL}/tenants/update`, updateData)
|
||||
.then((res) => {
|
||||
if (res.data.status) {
|
||||
opentoast(
|
||||
value0 == 0 ? 'Inactivated Successfully' : value0 == 1 ? 'Approved Successfully' : 'Activate Successfully',
|
||||
'success',
|
||||
2000
|
||||
);
|
||||
getalltenantsRefetch();
|
||||
summaryDataRefetch();
|
||||
setisloader(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
opentoast(err.message, 'error', 1500);
|
||||
setisloader(false);
|
||||
});
|
||||
// }
|
||||
try {
|
||||
const res = await updateAdminTenant(targetId, updateData);
|
||||
if (res.success) {
|
||||
opentoast(
|
||||
value0 == 0 ? 'Inactivated Successfully' : value0 == 1 ? 'Approved Successfully' : 'Activate Successfully',
|
||||
'success',
|
||||
2000
|
||||
);
|
||||
getalltenantsRefetch();
|
||||
summaryDataRefetch();
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
opentoast(err.message, 'error', 1500);
|
||||
} finally {
|
||||
setisloader(false);
|
||||
}
|
||||
};
|
||||
/* ============================================= || getAppPricing || ============================================= */
|
||||
// /admin/doormile-pricing has no zone query param documented — filter client-side.
|
||||
const getAppPricing = async (id) => {
|
||||
console.log('id', id);
|
||||
try {
|
||||
let appPricingRes = await axios.get(`${process.env.REACT_APP_URL}/utils/getapppricing/?applocationid=${id}`);
|
||||
console.log('appPricingRes', appPricingRes.data.details);
|
||||
setAppPricing(appPricingRes.data.details);
|
||||
const pricing = await getDoormilePricing();
|
||||
setAppPricing((pricing || []).filter((p) => !id || p.applocationid === id));
|
||||
} catch (error) {
|
||||
console.log('appPricingRes', error);
|
||||
}
|
||||
@@ -427,55 +782,54 @@ const Clients1 = () => {
|
||||
/* ============================================= || createpricing || ============================================= */
|
||||
const createpricing = async () => {
|
||||
setisloader(true);
|
||||
await axios
|
||||
.post(`${process.env.REACT_APP_URL}/tenants/createpricing`, {
|
||||
tenantpricingid: 0,
|
||||
try {
|
||||
const res = await createAdminPricing({
|
||||
applocationid: appId,
|
||||
pricingid: selectedPricing.pricingid,
|
||||
tenantid: selectedTenid,
|
||||
pricingdate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
configid: selectedPricing.configid,
|
||||
pricingtypeid: selectedPricing.pricingtypeid,
|
||||
slab: selectedPricing.slab,
|
||||
baseprice: +selectedPricing.baseprice,
|
||||
priceperkm: +selectedPricing.priceperkm,
|
||||
minkm: +selectedPricing.minkm,
|
||||
maxkm: +selectedPricing.maxkm,
|
||||
orders: +selectedPricing.minorder,
|
||||
othercharges: 0
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.data.status) {
|
||||
enqueueSnackbar('Price Created Successfully', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
dialogclose();
|
||||
setSelectedPricing({});
|
||||
tenantupdate(-1);
|
||||
fetchclientpricelist();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
vehicletype: selectedPricing.vehicletype || 'Bike',
|
||||
baseprice: +selectedPricing.baseprice || 0,
|
||||
baseweight: +selectedPricing.baseweight || 0,
|
||||
priceperkm: +selectedPricing.priceperkm || 0,
|
||||
priceperkg: +selectedPricing.priceperkg || 0,
|
||||
basedistance: +selectedPricing.basedistance || 0,
|
||||
handlingcharges: +selectedPricing.handlingcharges || 0,
|
||||
effectivefrom: dayjs().format('YYYY-MM-DD'),
|
||||
currency: 'INR',
|
||||
priority: 1,
|
||||
status: 'active'
|
||||
});
|
||||
if (res.success) {
|
||||
enqueueSnackbar('Price Created Successfully', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
dialogclose();
|
||||
setSelectedPricing({});
|
||||
tenantupdate(-1);
|
||||
fetchclientpricelist();
|
||||
} else {
|
||||
opentoast(res.message || 'Failed to save pricing', 'error', 2000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
opentoast(err.response?.data?.message || err.message || 'Failed to save pricing', 'error', 2000);
|
||||
}
|
||||
};
|
||||
/* ============================================= || updateClient || ============================================= */
|
||||
const updateClient = async () => {
|
||||
try {
|
||||
const arr = { ...editClient, tenantid: selectedTenid };
|
||||
|
||||
console.log('updateClient', arr);
|
||||
const updateRes = await axios.put(`${process.env.REACT_APP_URL}/tenants/update`, arr);
|
||||
console.log('updateClient', updateRes.data.message);
|
||||
if (updateRes.data.status) {
|
||||
opentoast(updateRes.data.message, 'success', 1500);
|
||||
const updateRes = await updateAdminTenant(selectedTenid, editClient);
|
||||
if (updateRes.success) {
|
||||
opentoast(updateRes.message || 'Updated Successfully', 'success', 1500);
|
||||
} else {
|
||||
opentoast(updateRes.message || 'Update Failed', 'error', 1500);
|
||||
}
|
||||
getalltenantsRefetch();
|
||||
summaryDataRefetch();
|
||||
} catch (err) {
|
||||
console.log('updateClient', err);
|
||||
opentoast(err.response?.data?.message || err.message || 'Update Failed', 'error', 1500);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -912,6 +1266,34 @@ const Clients1 = () => {
|
||||
{openRowIndex1 === index ? <EyeInvisibleOutlined style={{ fontSize: 14 }} /> : <EyeOutlined style={{ fontSize: 14 }} />}
|
||||
</IconButton>
|
||||
</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 && (
|
||||
<Tooltip title={openRowIndex2 === index ? 'Close' : 'Edit'} placement="top">
|
||||
<IconButton
|
||||
@@ -1208,12 +1590,12 @@ const Clients1 = () => {
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Date</TableCell>
|
||||
<TableCell>Slab</TableCell>
|
||||
<TableCell>Vehicle</TableCell>
|
||||
<TableCell>Base Price</TableCell>
|
||||
<TableCell>Min Kms</TableCell>
|
||||
<TableCell>Price/Km</TableCell>
|
||||
<TableCell>Other Charges</TableCell>
|
||||
<TableCell>Price/Kg</TableCell>
|
||||
<TableCell>Handling</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1225,14 +1607,14 @@ const Clients1 = () => {
|
||||
</TableRow>
|
||||
) : (
|
||||
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>{dayjs(val.pricingdate).format('DD-MM-YYYY')}</TableCell>
|
||||
<TableCell>{val.slab}</TableCell>
|
||||
<TableCell>{val.baseprice}</TableCell>
|
||||
<TableCell>{val.minkm}</TableCell>
|
||||
<TableCell>{val.priceperkm}</TableCell>
|
||||
<TableCell>{val.othercharges}</TableCell>
|
||||
<TableCell>{val.vehicletype || '—'}</TableCell>
|
||||
<TableCell>{val.baseprice ?? '—'}</TableCell>
|
||||
<TableCell>{val.priceperkm ?? '—'}</TableCell>
|
||||
<TableCell>{val.priceperkg ?? '—'}</TableCell>
|
||||
<TableCell>{val.handlingcharges ?? '—'}</TableCell>
|
||||
<TableCell>{val.status || '—'}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
@@ -1667,12 +2049,12 @@ const Clients1 = () => {
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Date</TableCell>
|
||||
<TableCell>Slab</TableCell>
|
||||
<TableCell>Vehicle</TableCell>
|
||||
<TableCell>Base Price</TableCell>
|
||||
<TableCell>Min Kms</TableCell>
|
||||
<TableCell>Price/Km</TableCell>
|
||||
<TableCell>Other Charges</TableCell>
|
||||
<TableCell>Price/Kg</TableCell>
|
||||
<TableCell>Handling</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -1684,14 +2066,14 @@ const Clients1 = () => {
|
||||
</TableRow>
|
||||
) : (
|
||||
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>{dayjs(val.pricingdate).format('DD-MM-YYYY')}</TableCell>
|
||||
<TableCell>{val.slab}</TableCell>
|
||||
<TableCell>{val.baseprice}</TableCell>
|
||||
<TableCell>{val.minkm}</TableCell>
|
||||
<TableCell>{val.priceperkm}</TableCell>
|
||||
<TableCell>{val.othercharges}</TableCell>
|
||||
<TableCell>{val.vehicletype || '—'}</TableCell>
|
||||
<TableCell>{val.baseprice ?? '—'}</TableCell>
|
||||
<TableCell>{val.priceperkm ?? '—'}</TableCell>
|
||||
<TableCell>{val.priceperkg ?? '—'}</TableCell>
|
||||
<TableCell>{val.handlingcharges ?? '—'}</TableCell>
|
||||
<TableCell>{val.status || '—'}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
@@ -1861,6 +2243,30 @@ const Clients1 = () => {
|
||||
>
|
||||
{expanded ? <EyeInvisibleOutlined style={{ fontSize: 14 }} /> : <EyeOutlined style={{ fontSize: 14 }} />}
|
||||
</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 && (
|
||||
<IconButton
|
||||
size="small"
|
||||
@@ -1955,24 +2361,24 @@ const Clients1 = () => {
|
||||
<Grid container spacing={2.5} alignItems={'center'}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormLabel>Select Slab</FormLabel>
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
id="combo-box-demo"
|
||||
options={appPricing}
|
||||
getOptionLabel={(option) => `${option.slab}`}
|
||||
<Select
|
||||
fullWidth
|
||||
selectOnFocus
|
||||
renderInput={(params) => <TextField {...params} label="Slab" />}
|
||||
onChange={(event, value, reason) => {
|
||||
setSelectedPricing(value);
|
||||
console.log('pricing', value);
|
||||
displayEmpty
|
||||
value={selectedPricing.vehicletype || ''}
|
||||
onChange={(e) => {
|
||||
setSelectedPricing({ ...selectedPricing, vehicletype: e.target.value });
|
||||
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 item xs={12} sm={6}>
|
||||
@@ -2005,43 +2411,57 @@ const Clients1 = () => {
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormLabel>Min Kms</FormLabel>
|
||||
<FormLabel>Base Weight (kg)</FormLabel>
|
||||
<TextField
|
||||
type="number"
|
||||
fullWidth
|
||||
value={!isPrice ? selectedPricing.minkm : 0}
|
||||
value={!isPrice ? selectedPricing.baseweight : 0}
|
||||
onChange={(e) => {
|
||||
setSelectedPricing({
|
||||
...selectedPricing,
|
||||
minkm: e.target.value
|
||||
baseweight: e.target.value
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormLabel>Max Kms</FormLabel>
|
||||
<FormLabel>Price/Kg</FormLabel>
|
||||
<TextField
|
||||
type="number"
|
||||
fullWidth
|
||||
value={!isPrice ? selectedPricing.maxkm : 0}
|
||||
value={!isPrice ? selectedPricing.priceperkg : 0}
|
||||
onChange={(e) => {
|
||||
setSelectedPricing({
|
||||
...selectedPricing,
|
||||
maxkm: e.target.value
|
||||
priceperkg: e.target.value
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormLabel>Min Orders</FormLabel>
|
||||
<FormLabel>Base Distance (km)</FormLabel>
|
||||
<TextField
|
||||
type="number"
|
||||
fullWidth
|
||||
value={!isPrice ? selectedPricing.minorder : 0}
|
||||
value={!isPrice ? selectedPricing.basedistance : 0}
|
||||
onChange={(e) => {
|
||||
setSelectedPricing({
|
||||
...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>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<TenantLocationsDialog
|
||||
tenant={locationsDialogTenant}
|
||||
open={!!locationsDialogTenant}
|
||||
onClose={() => setLocationsDialogTenant(null)}
|
||||
/>
|
||||
|
||||
<TenantCustomersDialog
|
||||
tenant={customersDialogTenant}
|
||||
open={!!customersDialogTenant}
|
||||
onClose={() => setCustomersDialogTenant(null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,75 +1,43 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Avatar, Box, Button, FormLabel, Grid, InputLabel, MenuItem, Paper, Select, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Switch,
|
||||
TextField,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { MdPersonAddAlt1 } from 'react-icons/md';
|
||||
|
||||
// third-party
|
||||
// import { PatternFormat } from 'react-number-format';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
// project import
|
||||
import MainCard from 'components/MainCard';
|
||||
import axios from 'axios';
|
||||
import AddressAutocomplete, { geocodeAddress } from 'components/nearle_components/AddressAutocomplete';
|
||||
import { createAdminTenant } from 'pages/api/doormileApi';
|
||||
import Loader from 'components/Loader';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { DT, tint } from 'themes/dt/tokens';
|
||||
|
||||
// const avatarImage = require.context('assets/images/users', true);
|
||||
|
||||
// styles & constant
|
||||
// const ITEM_HEIGHT = 48;
|
||||
// const ITEM_PADDING_TOP = 8;
|
||||
// const MenuProps = {
|
||||
// PaperProps: {
|
||||
// style: {
|
||||
// maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
const Createclient = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
// const [role, setRole] = useState('');
|
||||
const [mobilenumber, setMobilenumber] = useState('');
|
||||
const [emailaddress, setEmailaddress] = useState('');
|
||||
const [city, setCity] = useState('');
|
||||
const [zipcode, setZipcode] = useState('');
|
||||
const [address, setAddress] = useState('');
|
||||
const [state, setState] = useState('');
|
||||
const [suburb, setSuburb] = useState('');
|
||||
const [latlong, setLatlong] = useState({});
|
||||
const [firstname, setFirstname] = useState('');
|
||||
const [doorno, setDoorno] = useState('');
|
||||
const [landmark, setLandmark] = useState('');
|
||||
const [tenantinfo, setTenantinfo] = useState({});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// fetchprofiledetails(localStorage.getItem('appuserid'));
|
||||
// fetchprofiledetails(181);
|
||||
if (localStorage.getItem('tenantid')) {
|
||||
fetchtenantinfo(localStorage.getItem('tenantid'));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
geocodeAddress(address).then((place) => {
|
||||
if (active && place) {
|
||||
setLatlong({ lat: place.geometry.location.lat(), lng: place.geometry.location.lng() });
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [address]);
|
||||
const [tenantname, setTenantname] = useState('');
|
||||
const [primaryemail, setPrimaryemail] = useState('');
|
||||
const [primarycontact, setPrimarycontact] = useState('');
|
||||
const [status, setStatus] = useState('active');
|
||||
const [requiredeliveryotp, setRequiredeliveryotp] = useState(false);
|
||||
|
||||
const opentoast = (message) => {
|
||||
enqueueSnackbar(message, {
|
||||
@@ -77,192 +45,53 @@ const Createclient = () => {
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
// console.log(alertmessage)
|
||||
};
|
||||
|
||||
const fetchprofiledetails = async (userid) => {
|
||||
if (userid) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL2}/tenants/getclient?id=${userid}`)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
if (res.data.message === 'Successful') {
|
||||
let res1 = res.data.details;
|
||||
setMobilenumber(res1.contactno);
|
||||
setEmailaddress(res1.primaryemail);
|
||||
setAddress(res1.address);
|
||||
setCity(res1.city);
|
||||
setZipcode(res1.postcode);
|
||||
setState(res1.state);
|
||||
setSuburb(res1.suburb);
|
||||
setLatlong({
|
||||
lat: res1.latitude,
|
||||
lng: res1.longitude
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
const createTenantMutation = useMutation({
|
||||
mutationFn: (payload) => createAdminTenant(payload),
|
||||
onSuccess: (res) => {
|
||||
if (res.success) {
|
||||
enqueueSnackbar('Client created successfully', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
navigate('/doormile/tenants');
|
||||
} else {
|
||||
opentoast(res.message || 'Client already exists');
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
opentoast(err.response?.data?.message || err.message || 'Failed to create client');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const fetchtenantinfo = async (tid) => {
|
||||
setLoading(true);
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
if (res.data.status) {
|
||||
setTenantinfo(res.data.details);
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddressPlaceSelected = (place) => {
|
||||
setAddress(place.formatted_address);
|
||||
let city1, zipcode1, state1, suburb1;
|
||||
for (let i = 0; i < place.address_components.length; i++) {
|
||||
for (let j = 0; j < place.address_components[i].types.length; j++) {
|
||||
switch (place.address_components[i].types[j]) {
|
||||
case 'locality':
|
||||
city1 = place.address_components[i].long_name;
|
||||
break;
|
||||
case 'administrative_area_level_1':
|
||||
state1 = place.address_components[i].long_name;
|
||||
break;
|
||||
case 'postal_code':
|
||||
zipcode1 = place.address_components[i].long_name;
|
||||
break;
|
||||
case 'sublocality':
|
||||
case 'sublocality_level_1':
|
||||
suburb1 = place.address_components[i].long_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
setCity(city1 || '');
|
||||
setState(state1 || '');
|
||||
setZipcode(zipcode1 || '');
|
||||
setSuburb(suburb1 || '');
|
||||
};
|
||||
|
||||
const createprofile = async () => {
|
||||
console.log('res', businessname, businessno, mobilenumber, emailaddress, address, city, zipcode);
|
||||
|
||||
// if (!businessname) {
|
||||
// opentoast('Fill Business name')
|
||||
// } else if (!businessno) {
|
||||
// opentoast('Fill Registration No')
|
||||
// }
|
||||
// else
|
||||
if (!firstname) {
|
||||
opentoast('Fill Full name');
|
||||
} else if (!mobilenumber) {
|
||||
opentoast('Fill Mobile Number');
|
||||
} else if (!emailaddress) {
|
||||
opentoast('Fill emailaddress');
|
||||
} else if (!address) {
|
||||
opentoast('Fill Address');
|
||||
} else if (!city) {
|
||||
opentoast('Fill City');
|
||||
} else if (!zipcode) {
|
||||
opentoast('Fill post code');
|
||||
} else if (!suburb) {
|
||||
opentoast('Fill suburb');
|
||||
} else if (!latlong.lat || !latlong.lng) {
|
||||
opentoast('Choose valid address');
|
||||
const createprofile = () => {
|
||||
if (!tenantname) {
|
||||
opentoast('Fill Client / Business Name');
|
||||
} else if (!primarycontact || primarycontact.length !== 10) {
|
||||
opentoast('Fill a valid 10-digit Contact Number');
|
||||
} else if (!primaryemail) {
|
||||
opentoast('Fill Primary Email');
|
||||
} else {
|
||||
let obj = {
|
||||
customerid: 0,
|
||||
configid: 1,
|
||||
firstname: firstname,
|
||||
applocationid: tenantinfo.applolcationid,
|
||||
profileimage: '',
|
||||
dialcode: '+91',
|
||||
contactno: mobilenumber,
|
||||
devicetype: '',
|
||||
deviceid: '',
|
||||
customertoken: '',
|
||||
address: address,
|
||||
suburb: suburb,
|
||||
city: city,
|
||||
state: state,
|
||||
postcode: zipcode,
|
||||
landmark: landmark,
|
||||
doorno: doorno,
|
||||
latitude: latlong.lat.toString(),
|
||||
longitude: latlong.lng.toString(),
|
||||
tenantid: parseInt(localStorage.getItem('tenantid')),
|
||||
email: emailaddress
|
||||
// POST /admin/tenants — requiredeliveryotp is a pointer server-side;
|
||||
// it's opt-in and off by default, so we only send it when the operator
|
||||
// explicitly turns it on rather than always sending false.
|
||||
const obj = {
|
||||
tenantname,
|
||||
primaryemail,
|
||||
primarycontact,
|
||||
status,
|
||||
...(requiredeliveryotp ? { requiredeliveryotp: true } : {})
|
||||
};
|
||||
|
||||
console.log(obj);
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await axios
|
||||
.post(`${process.env.REACT_APP_URL}/customers/create`, obj)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
if (res.data.status) {
|
||||
enqueueSnackbar(' Created Successfully ', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
navigate('/clients');
|
||||
// setTimeout(()=>{
|
||||
// fetchprofiledetails(localStorage.getItem('appuserid'));
|
||||
|
||||
// },2000)
|
||||
} else if (res.data.message == 'Customer Already available') {
|
||||
enqueueSnackbar('Customer Already available', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
setLoading(false);
|
||||
enqueueSnackbar(err.message, {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
}
|
||||
createTenantMutation.mutate(obj);
|
||||
}
|
||||
};
|
||||
|
||||
// const [experience, setExperience] = useState('0');
|
||||
|
||||
// const handleChange = (event) => {
|
||||
// setExperience(event.target.value);
|
||||
// };
|
||||
return (
|
||||
<>
|
||||
{loading && <Loader />}
|
||||
{createTenantMutation.isPending && <Loader />}
|
||||
|
||||
<Grid item xs={12} sx={{ mb: 2 }}>
|
||||
<Paper
|
||||
@@ -288,289 +117,73 @@ const Createclient = () => {
|
||||
sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }}
|
||||
>
|
||||
<Grid container spacing={isMobile ? 2 : 3}>
|
||||
{/* <Grid item xs={12} sm={4} >
|
||||
<MainCard title="Personal Information" sx={{ height: '100%' }}>
|
||||
<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 item xs={12}>
|
||||
<MainCard sx={{ height: '100%' }} contentSX={{ p: { xs: 1.5, md: 2.5 } }}>
|
||||
<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}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-last-name">Admin Name</InputLabel>
|
||||
<InputLabel htmlFor="tenant-name">Client / Business Name</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="personal-last-name"
|
||||
placeholder="Name"
|
||||
onChange={(e) => setFirstname(e.target.value)}
|
||||
value={firstname}
|
||||
id="tenant-name"
|
||||
placeholder="e.g. DailyGrubs"
|
||||
onChange={(e) => setTenantname(e.target.value)}
|
||||
value={tenantname}
|
||||
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>
|
||||
<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}>
|
||||
<Select defaultValue="+1" disabled sx={{ cursor: 'not-allowed' }}>
|
||||
<MenuItem value="+1">+91</MenuItem>
|
||||
</Select>
|
||||
<TextField
|
||||
type="number"
|
||||
id="personal-phone"
|
||||
// format="##########"
|
||||
// mask="_"
|
||||
id="tenant-contact"
|
||||
fullWidth
|
||||
// customInput={TextField}
|
||||
placeholder="Phone Number"
|
||||
// defaultValue="8654239581"
|
||||
// onBlur={() => { }}
|
||||
placeholder="Contact Number"
|
||||
onChange={(e) => {
|
||||
if (e.target.value.toString().length <= 10) {
|
||||
setMobilenumber(e.target.value);
|
||||
setPrimarycontact(e.target.value);
|
||||
}
|
||||
}}
|
||||
value={mobilenumber}
|
||||
value={primarycontact}
|
||||
autoComplete="off"
|
||||
// disabled
|
||||
sx={{ cursor: 'not-allowed' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-email">Email Address</InputLabel>
|
||||
<InputLabel htmlFor="tenant-email">Primary Email</InputLabel>
|
||||
<TextField
|
||||
type="email"
|
||||
fullWidth
|
||||
// defaultValue="stebin.ben@gmail.com"
|
||||
id="personal-email"
|
||||
placeholder="Email Address"
|
||||
onChange={(e) => setEmailaddress(e.target.value)}
|
||||
value={emailaddress}
|
||||
id="tenant-email"
|
||||
placeholder="Primary Email Address"
|
||||
onChange={(e) => setPrimaryemail(e.target.value)}
|
||||
value={primaryemail}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-address">Address</InputLabel>
|
||||
<AddressAutocomplete
|
||||
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>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={requiredeliveryotp} onChange={(e) => setRequiredeliveryotp(e.target.checked)} />}
|
||||
label="Require delivery OTP for this client (off by default)"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
@@ -582,7 +195,7 @@ const Createclient = () => {
|
||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
||||
spacing={2}
|
||||
>
|
||||
<Button variant="contained" onClick={() => createprofile()} fullWidth={isMobile}>
|
||||
<Button variant="contained" onClick={createprofile} fullWidth={isMobile}>
|
||||
Create
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@ import 'leaflet/dist/leaflet.css';
|
||||
import '../../../utils/leafletPolylineOffset';
|
||||
import dayjs from 'dayjs';
|
||||
import { useInfiniteQuery, useQueries, useQuery, useMutation } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
MdMap,
|
||||
MdDirectionsBike,
|
||||
@@ -58,6 +57,7 @@ import {
|
||||
import ProfitabilitySection from './ProfitabilitySection';
|
||||
import ActiveSection from './ActiveSection';
|
||||
import { fetchDeliveries, fetchAppLocations, getRiderPeriodicLogs, fetchRidersLogs, fetchBatchEfficiency } from '../../api/api';
|
||||
import { getConsignmentLogs } from 'pages/api/doormileApi';
|
||||
import {
|
||||
STATUS_STYLES,
|
||||
getStatusStyle,
|
||||
@@ -1071,9 +1071,11 @@ const Dispatch = ({
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [riderPositionModal]);
|
||||
|
||||
// TODO: wire to real tenant context once the standalone Dispatch screen
|
||||
// surfaces it. 916 matches the example tenant in the API spec.
|
||||
const ANALYSIS_TENANT_ID = 916;
|
||||
// Was hardcoded to 916 (the example tenant in the API spec) — every
|
||||
// operator's batch-efficiency analytics queried the same tenant regardless
|
||||
// of who was actually logged in. Reads the real session tenant now, same
|
||||
// as the rest of the app (login.js sets this on sign-in).
|
||||
const ANALYSIS_TENANT_ID = localStorage.getItem('tenantid') || undefined;
|
||||
|
||||
const batchEfficiencyMutation = useMutation({
|
||||
mutationFn: fetchBatchEfficiency,
|
||||
@@ -1517,11 +1519,12 @@ const Dispatch = ({
|
||||
plannedMapRendererRef.current = L.canvas({ padding: 1.5, tolerance: 5 });
|
||||
}
|
||||
|
||||
// Pull the partners/getriderlogs feed for the currently selected hub + date.
|
||||
// This endpoint returns the exact live GPS position for every rider at the
|
||||
// hub (latitude/longitude/logdate/status). We render those positions as
|
||||
// markers on the main dispatch map so the operator sees where each rider
|
||||
// actually is — matching the Reports → Riders Logs page.
|
||||
// Live rider position feed. Was dead for a while during the backend
|
||||
// migration (fetchRidersLogs was a permanent [] stub) — now sourced from
|
||||
// GET /admin/milers/summary (confirmed in jupiter2doormile.md), which
|
||||
// carries each rider's currentlatitude/currentlongitude/lastpingat. We
|
||||
// render those positions as markers on the main dispatch map so the
|
||||
// operator sees where each rider actually is.
|
||||
const RIDER_LOG_POLL_MS = 1000;
|
||||
const { data: ridersLocationLogs } = useQuery({
|
||||
queryKey: [selectedAppLocationId, selectedDate, ''],
|
||||
@@ -2197,17 +2200,20 @@ const Dispatch = ({
|
||||
queries: focusedRiderDeliveryIds.map((deliveryid) => ({
|
||||
queryKey: ['deliveryLogs', deliveryid],
|
||||
queryFn: async () => {
|
||||
const res = await axios.get(
|
||||
`${process.env.REACT_APP_URL3}/deliveries/getdeliverylogs/?deliveryid=${deliveryid}`
|
||||
);
|
||||
// Accept several possible response shapes — the live API has shipped
|
||||
// {details:[…]}, plain arrays, and {data:[…]} variants over time, and
|
||||
// any of those should produce a polyline. Pick the first non-empty
|
||||
// array-like found.
|
||||
const candidates = [res?.data?.details, res?.data?.data, res?.data, res];
|
||||
const rows = candidates.find((c) => Array.isArray(c)) || [];
|
||||
// Also accept lat/lng (alternate naming) in case the endpoint ever
|
||||
// returns the same shape the front-end uses internally.
|
||||
// GET /admin/consignments/:id/logs is confirmed (jupiter2doormile.md,
|
||||
// Status: Done) as the GPS/status trail for one consignment — but
|
||||
// this file's own `deliveryid` here has never been confirmed to
|
||||
// actually BE a consignmentid (Dispatch.js's data source hasn't been
|
||||
// audited against the new API the way orders.js/deliveries.js have).
|
||||
// If it's really a bookingid, this 404s and rows stays empty — same
|
||||
// degraded-but-not-crashing behaviour as before.
|
||||
let rows = [];
|
||||
try {
|
||||
rows = (await getConsignmentLogs(deliveryid)) || [];
|
||||
if (!Array.isArray(rows)) rows = [];
|
||||
} catch (err) {
|
||||
rows = [];
|
||||
}
|
||||
// Sort by logdate ascending so the polyline follows the rider's
|
||||
// chronological path. The endpoint isn't guaranteed to return rows
|
||||
// in order — without this, consecutive points can be out of sequence
|
||||
@@ -5121,14 +5127,14 @@ const Dispatch = ({
|
||||
{renderMarkers()}
|
||||
{renderRoutes()}
|
||||
|
||||
{/* Live rider GPS markers from /partners/getriderlogs/. Mirrors the
|
||||
Reports → Riders Logs map: green pin when the rider's last log
|
||||
row is `active`, red otherwise, with the rider's username as a
|
||||
{/* Live rider GPS markers from GET /admin/milers/summary (see
|
||||
fetchRidersLogs in api.js). Green pin when the rider's status
|
||||
is `active`, red otherwise, with the rider's username as a
|
||||
label. Scoped to riders who actually have orders in the
|
||||
currently selected slot — `riders` is derived from
|
||||
filteredLiveRows so it already reflects the slot filter. A
|
||||
rider with zero orders in the current slot is hidden, even if
|
||||
getriderlogs still returns their GPS row. When a specific
|
||||
the summary still returns their GPS row. When a specific
|
||||
rider is focused, only that one is shown. */}
|
||||
{liveRiderLocations
|
||||
.filter((r) =>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Loader from 'components/Loader';
|
||||
import logo from 'assets/images/doormile-logo.png';
|
||||
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { OpenToast } from 'components/third-party/OpenToast';
|
||||
import { closeGlobalToast, GlobalToast } from 'components/nearle_components/GlobalToast';
|
||||
import { setLoginUser } from 'store/reducers/loginUserSlice';
|
||||
import { markSessionStart } from 'utils/session';
|
||||
import { loginAdmin } from 'pages/api/doormileApi';
|
||||
import { DT } from 'themes/dt/tokens';
|
||||
|
||||
// Astryx design system — see themes/astryx.js for the Doormile brand theme
|
||||
@@ -71,150 +69,56 @@ const BULLETS = ['Real-time fleet visibility', 'AI-optimised dispatch routes', '
|
||||
|
||||
const Login = () => {
|
||||
const dispatch = useDispatch();
|
||||
const fcmtoken = useSelector((state) => state.fcm);
|
||||
const [loading, setLoading] = useState(false);
|
||||
let navigate = useNavigate();
|
||||
const [otp, setOtp] = useState('');
|
||||
const [userinfo, setUserinfo] = useState({});
|
||||
const [username, setUsername] = useState('');
|
||||
const [passwordStatus, setPasswordStatus] = useState(0);
|
||||
const navigate = useNavigate();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isPassword, setIspassword] = useState(false);
|
||||
const [userid, setUserid] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem('firstname')) {
|
||||
// 'authname' is the AUTH_PRESENCE_KEY (utils/session.js) — App.js and the
|
||||
// inactivity/cross-tab logout machinery all key off it, unrelated to which
|
||||
// backend actually authenticated the session.
|
||||
if (localStorage.getItem('authname')) {
|
||||
navigate('/doormile/dispatch');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loginsend = async () => {
|
||||
setLoading(true);
|
||||
const opentoast = (message, variant = 'error') => OpenToast(message, variant, 2000);
|
||||
|
||||
if (!username) {
|
||||
opentoast('Fill All required fields');
|
||||
setLoading(false);
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!email || !password) {
|
||||
opentoast('Enter your email and password');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.post(`https://jupiter.nearle.app/live/api/v1/users/console/login`, {
|
||||
authname: username,
|
||||
configid: 9, // 9 -> config id for nearle console admin
|
||||
userfcmtoken: fcmtoken?.token,
|
||||
password
|
||||
});
|
||||
// user not found
|
||||
if (res.data.code == 409 && !res.data.status) {
|
||||
OpenToast(res.data.message, 'error', 3000);
|
||||
}
|
||||
// user not activated
|
||||
else if (res.data.code == 403) {
|
||||
OpenToast(res.data.message, 'warning', 3000);
|
||||
}
|
||||
//user found, no password, setup password
|
||||
else if (res.data.code == 409 && res.data.status) {
|
||||
setPasswordStatus(1); // for password and confirm password ui
|
||||
setUserid(res.data.details.userid);
|
||||
OpenToast('User Found', 'success', 3000);
|
||||
OpenToast(res.data.message, 'success', 3000);
|
||||
}
|
||||
//user found, incorrect password
|
||||
else if (res.data.code == 401 && !res.data.status) {
|
||||
OpenToast(res.data.message, 'error', 3000);
|
||||
}
|
||||
//user found, enter password
|
||||
else if (res.data.code == 401 && res.data.status) {
|
||||
OpenToast(res.data.message, 'success', 3000);
|
||||
fetchAppLocations(res.data.userid);
|
||||
setPasswordStatus(2);
|
||||
}
|
||||
// user found, correct password
|
||||
else if (res.data.code == 200 && res.data.status) {
|
||||
OpenToast(res.data.message, 'success', 1000);
|
||||
setUserinfo(res.data.details);
|
||||
const userinfo = res.data.details;
|
||||
dispatch(setLoginUser(userinfo));
|
||||
localStorage.setItem('firstname', userinfo.firstname);
|
||||
localStorage.setItem('authname', userinfo.authname);
|
||||
localStorage.setItem('roleid', userinfo.roleid);
|
||||
localStorage.setItem('tenantid', userinfo.tenantid);
|
||||
localStorage.setItem('partnerid', userinfo.partnerid);
|
||||
localStorage.setItem('applocationid', userinfo.applocationid);
|
||||
localStorage.setItem('userid', userinfo.userid);
|
||||
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
|
||||
const res = await loginAdmin(email, password);
|
||||
if (res.success) {
|
||||
const user = res.user || {};
|
||||
// Compatibility keys: existing pages/api.js read userid/roleid/tenantid
|
||||
// straight from localStorage rather than through a context. 'authname'
|
||||
// stays the AUTH_PRESENCE_KEY so the rest of the session contract
|
||||
// (useInactivityLogout, cross-tab logout, App.js gate) needs no changes.
|
||||
localStorage.setItem('authname', user.email || user.name || email);
|
||||
localStorage.setItem('firstname', user.name || '');
|
||||
localStorage.setItem('userid', user.id ?? '');
|
||||
localStorage.setItem('roleid', user.role ?? '');
|
||||
localStorage.setItem('tenantid', user.tenantid ?? '');
|
||||
dispatch(setLoginUser(user));
|
||||
markSessionStart();
|
||||
fetchAppLocations(userinfo.userid);
|
||||
opentoast('Login successful', 'success');
|
||||
navigate('/doormile/dispatch');
|
||||
} else {
|
||||
OpenToast(res.data.message, 'error', 3000);
|
||||
opentoast(res.message || 'Login failed');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
OpenToast(err.message, 'error', 5000);
|
||||
opentoast(err?.message || 'Login failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const loginsuccessful = () => {
|
||||
localStorage.setItem('firstname', userinfo.firstname);
|
||||
localStorage.setItem('authname', userinfo.authname);
|
||||
localStorage.setItem('roleid', userinfo.roleid);
|
||||
localStorage.setItem('tenantid', userinfo.tenantid);
|
||||
localStorage.setItem('partnerid', userinfo.partnerid);
|
||||
localStorage.setItem('applocationid', userinfo.applocationid);
|
||||
localStorage.setItem('userid', userinfo.userid);
|
||||
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
|
||||
markSessionStart();
|
||||
closeGlobalToast(); // to close the pin snackbar
|
||||
|
||||
navigate('/doormile/dispatch');
|
||||
};
|
||||
|
||||
const opentoast = (message) => {
|
||||
enqueueSnackbar(message, {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 1500
|
||||
});
|
||||
};
|
||||
|
||||
const fetchAppLocations = async (id) => {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${id}`);
|
||||
const updatedLocations = [...response.data.details, { locationname: 'All', applocationid: 0 }];
|
||||
localStorage.setItem('applocations', JSON.stringify(updatedLocations));
|
||||
};
|
||||
const updateUser = async () => {
|
||||
const response = await axios.put(`${process.env.REACT_APP_URL2}/users/update`, {
|
||||
userid,
|
||||
password
|
||||
});
|
||||
if (response.data.status) {
|
||||
OpenToast(response.data.message, 'success', 3000);
|
||||
OpenToast('Enter Password to Login', 'success', 3000);
|
||||
setPasswordStatus(2);
|
||||
setPassword('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (passwordStatus == 0) {
|
||||
loginsend();
|
||||
} else if (passwordStatus == 1) {
|
||||
if (!password || !confirmPassword || password != confirmPassword) {
|
||||
OpenToast('Check Password', 'warning', 3000);
|
||||
} else {
|
||||
updateUser();
|
||||
}
|
||||
} else if (passwordStatus == 2) {
|
||||
if (!password) {
|
||||
OpenToast('Invalid Password', 'warning', 3000);
|
||||
}
|
||||
loginsend();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Theme theme={doormileTheme} mode="light">
|
||||
@@ -266,75 +170,18 @@ const Login = () => {
|
||||
type="email"
|
||||
size="lg"
|
||||
isRequired
|
||||
value={username}
|
||||
onChange={(value) => setUsername(value.toLocaleLowerCase())}
|
||||
isDisabled={!!passwordStatus}
|
||||
value={email}
|
||||
onChange={(value) => setEmail(value.toLocaleLowerCase())}
|
||||
/>
|
||||
|
||||
{/* Setup Password */}
|
||||
{passwordStatus == 1 && (
|
||||
<VStack gap={4} padding={0}>
|
||||
<Divider label="Setup Password" />
|
||||
<TextInput
|
||||
hasAutoFocus
|
||||
label="Enter New Password"
|
||||
type="password"
|
||||
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%" />
|
||||
<TextInput
|
||||
label="Password"
|
||||
type="password"
|
||||
size="lg"
|
||||
isRequired
|
||||
value={password}
|
||||
onChange={(value) => setPassword(value)}
|
||||
/>
|
||||
<Button label="Sign in" type="submit" variant="primary" size="lg" width="100%" />
|
||||
</VStack>
|
||||
</form>
|
||||
</VStack>
|
||||
|
||||
@@ -1,520 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
// import AuthWrapper from 'sections/auth/AuthWrapper';
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
// CardHeader,
|
||||
Stack,
|
||||
// Divider,
|
||||
// InputLabel,
|
||||
// OutlinedInput,
|
||||
TextField,
|
||||
Button,
|
||||
Typography,
|
||||
CardHeader,
|
||||
Container,
|
||||
Link
|
||||
} from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import AnimateButton from 'components/@extended/AnimateButton';
|
||||
|
||||
import logo from 'assets/images/doormile-logo.png';
|
||||
|
||||
// doormile-logo.png is a white asset; recolour it to brand red for this page's light background.
|
||||
const DOORMILE_RED_FILTER =
|
||||
'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)';
|
||||
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
// import { openSnackbar } from 'store/reducers/snackbar';
|
||||
// import { useDispatch } from 'react-redux';
|
||||
import Loader from 'components/Loader';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
|
||||
const Login = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [alertmessage, setAlertmessage] = useState('');
|
||||
const [checkusername, setCheckusername] = useState(false);
|
||||
// const [toast, setToast] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
let navigate = useNavigate();
|
||||
// let dispatch = useDispatch();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
// let loginuserid = useSelector((state)=>state.logininfo);
|
||||
|
||||
// useEffect(() => {
|
||||
|
||||
// if (alertmessage) {
|
||||
// dispatch(
|
||||
// openSnackbar({
|
||||
// open: true,
|
||||
// message: alertmessage,
|
||||
// variant: 'alert',
|
||||
// anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
// alert: {
|
||||
// // variant:'info',
|
||||
// color: 'error',
|
||||
|
||||
// }
|
||||
// })
|
||||
// )
|
||||
// }
|
||||
// }, [toast])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
localStorage.getItem('authname')
|
||||
// || localStorage.getItem("appuserid")
|
||||
) {
|
||||
navigate('/deliveries');
|
||||
}
|
||||
|
||||
// console.log(alertmessage)
|
||||
}, []);
|
||||
|
||||
const usernamecheck = async (e) => {
|
||||
e.preventDefault();
|
||||
setUsername(e.target.value);
|
||||
if (e.target.value) {
|
||||
try {
|
||||
// await axios.post(`${process.env.REACT_APP_URL}/auth/login`, {
|
||||
// "authname": e.target.value
|
||||
// })
|
||||
await axios
|
||||
.post(`${process.env.REACT_APP_URL}/users/login`, {
|
||||
authname: e.target.value,
|
||||
configid: 1
|
||||
// "contactno": e.target.value,
|
||||
// "password": 'admin'
|
||||
})
|
||||
.then((res) => {
|
||||
console.log(res.data);
|
||||
if (res.data.details.authname === e.target.value) {
|
||||
setUsername(e.target.value);
|
||||
setCheckusername(false);
|
||||
} else {
|
||||
setCheckusername(true);
|
||||
}
|
||||
// if (res.data.authname === e.target.value) {
|
||||
|
||||
// setUsername(e.target.value);
|
||||
// setCheckusername(false);
|
||||
// }
|
||||
})
|
||||
.catch((err) => {
|
||||
// if (err.response.data.message === 'No user found') {
|
||||
|
||||
setCheckusername(true);
|
||||
// }
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loginsend = async () => {
|
||||
// e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
if (password && username) {
|
||||
if (password == 'admin') {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await axios
|
||||
.post(`${process.env.REACT_APP_URL}/users/partner/login`, {
|
||||
// "authname": username,
|
||||
configid: 1,
|
||||
contactno: username
|
||||
|
||||
// "password": password
|
||||
})
|
||||
.then((res) => {
|
||||
console.log(res.data);
|
||||
if (res.data.status) {
|
||||
if (res.data.details.contactno === username) {
|
||||
enqueueSnackbar('login Successfull', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 3000
|
||||
});
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
localStorage.setItem('firstname', res.data.details.tenantname);
|
||||
localStorage.setItem('authname', res.data.details.authname);
|
||||
|
||||
// localStorage.setItem("appuserid", res.data.details.userid);
|
||||
localStorage.setItem('roleid', res.data.details.roleid);
|
||||
localStorage.setItem('tenantid', res.data.details.tenantid);
|
||||
localStorage.setItem('partnerid', res.data.details.partnerid);
|
||||
|
||||
navigate('/orders');
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
console.log(res.data.message);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
// setAlertmessage('Invalid Data');
|
||||
// if(err.message == 'Network Error'){
|
||||
opentoast(err.message);
|
||||
// }else{
|
||||
// opentoast('Invalid Data');
|
||||
|
||||
// }
|
||||
setLoading(false);
|
||||
setSubmitting(false);
|
||||
console.log(err.message);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
setSubmitting(false);
|
||||
}
|
||||
} else {
|
||||
opentoast('Password is Incorrect');
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
// let el2 = document.getElementById('toastid');
|
||||
// el2.classList.add('d-block');
|
||||
// el2.classList.remove('d-none');
|
||||
setAlertmessage('Fill All required fields');
|
||||
opentoast('Fill All required fields');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// const handleClose = () => {
|
||||
// setToast(false)
|
||||
// }
|
||||
const opentoast = (message) => {
|
||||
// setToast(true)
|
||||
|
||||
// setTimeout(() => {
|
||||
// // handleClose();
|
||||
// setToast(false)
|
||||
// }, 2000);
|
||||
enqueueSnackbar(message, {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <AuthWrapper> */}
|
||||
<Box sx={{ minHeight: '100vh' }}>
|
||||
{loading && <Loader />}
|
||||
{/* <AuthBackground /> */}
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
sx={{
|
||||
minHeight: '100vh'
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
// sx={{ ml: 3, mt: 3 }}
|
||||
sx={{ ml: { xs: 0, md: 3 }, mt: { xs: 3, md: 1 }, textAlign: { xs: 'center', md: 'left' } }}
|
||||
>
|
||||
<img src={logo} alt="Doormile" width={isMobile ? '160px' : '200px'} style={{ height: 'auto', filter: DOORMILE_RED_FILTER }} />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
container
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
// sx={{ minHeight: { xs: 'calc(100vh - 210px)', sm: 'calc(100vh - 134px)', md: 'calc(100vh - 112px)' } }}
|
||||
sx={{ minHeight: { xs: 'calc(100vh - 210px)', sm: 'calc(100vh - 134px)', md: 'calc(100vh - 140px)' } }}
|
||||
>
|
||||
<Grid item>
|
||||
{/* <AuthCard>{children}</AuthCard> */}
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
maxWidth: { xs: 400, lg: 475 },
|
||||
margin: { xs: 2, sm: 2.5, md: 3 },
|
||||
'& > *': {
|
||||
flexGrow: 1,
|
||||
flexBasis: '50%'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
position: 'relative',
|
||||
border: '1px solid',
|
||||
borderRadius: { xs: 2, md: 1 },
|
||||
borderColor: theme.palette.divider,
|
||||
boxShadow: { xs: '0 14px 40px rgba(15, 23, 42, 0.10)', md: 'inherit' },
|
||||
p: { xs: 2, md: 2 },
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
{/* <CardHeader title={<Typography variant="h4">Login</Typography>} /> */}
|
||||
{/* <Divider sx={{ borderStyle: 'dashed' }} /> */}
|
||||
{/* <h1>eee</h1> */}
|
||||
{/* <CardHeader> */}
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" justifyContent="flex-start" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
|
||||
<CardHeader title={<Typography variant="h3">Login</Typography>} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
{/* <Grid item xs={12}>
|
||||
<AuthLogin isDemo={isLoggedIn} />
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
<CardContent>
|
||||
<form
|
||||
noValidate
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
{/* <Stack spacing={1}> */}
|
||||
{/* <InputLabel htmlFor="email-login">Email Address</InputLabel>
|
||||
<OutlinedInput
|
||||
id="email-login"
|
||||
type="email"
|
||||
value={values.email}
|
||||
name="email"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
placeholder="Enter email address"
|
||||
fullWidth
|
||||
id="username1"
|
||||
label="E-mail Address"
|
||||
variant="outlined"
|
||||
autoComplete='email'
|
||||
required
|
||||
onChange={usernamecheck}
|
||||
error={checkusername}
|
||||
error={Boolean(touched.email && errors.email)}
|
||||
/>
|
||||
{touched.email && errors.email && (
|
||||
<FormHelperText error id="standard-weight-helper-text-email-login">
|
||||
{errors.email}
|
||||
</FormHelperText>
|
||||
)} */}
|
||||
<TextField
|
||||
margin="normal"
|
||||
fullWidth
|
||||
id="username1"
|
||||
label="E-mail Address"
|
||||
variant="outlined"
|
||||
autoComplete="email"
|
||||
required
|
||||
onChange={usernamecheck}
|
||||
error={checkusername}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
fullWidth
|
||||
required
|
||||
autoComplete="current-password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
type="password"
|
||||
id="password1"
|
||||
label="Password"
|
||||
variant="outlined"
|
||||
/>
|
||||
{/* </Stack> */}
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
{/* <Stack spacing={0}> */}
|
||||
{/* <InputLabel htmlFor="password-login">Password</InputLabel> */}
|
||||
{/* <OutlinedInput
|
||||
fullWidth
|
||||
// error={Boolean(touched.password && errors.password)}
|
||||
// id="-password-login"
|
||||
// type={showPassword ? 'text' : 'password'}
|
||||
// value={values.password}
|
||||
// name="password"
|
||||
// onBlur={handleBlur}
|
||||
// onChange={handleChange}
|
||||
// endAdornment={
|
||||
// <InputAdornment position="end">
|
||||
// <IconButton
|
||||
// aria-label="toggle password visibility"
|
||||
// onClick={handleClickShowPassword}
|
||||
// onMouseDown={handleMouseDownPassword}
|
||||
// edge="end"
|
||||
// color="secondary"
|
||||
// >
|
||||
// {showPassword ? <EyeOutlined /> : <EyeInvisibleOutlined />}
|
||||
// </IconButton>
|
||||
// </InputAdornment>
|
||||
// }
|
||||
placeholder="Enter password"
|
||||
// margin="normal"
|
||||
// fullWidth
|
||||
required
|
||||
autoComplete="current-password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
type='password' id="password1"
|
||||
/> */}
|
||||
{/* {touched.password && errors.password && (
|
||||
<FormHelperText error id="standard-weight-helper-text-password-login">
|
||||
{errors.password}
|
||||
</FormHelperText>
|
||||
)} */}
|
||||
|
||||
{/* </Stack> */}
|
||||
<Link href="#" variant="h6">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</Grid>
|
||||
|
||||
{/* <Grid item xs={12} sx={{ mt: -1 }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={(event) => setChecked(event.target.checked)}
|
||||
name="checked"
|
||||
color="primary"
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="h6">Keep me sign in</Typography>}
|
||||
/>
|
||||
<Link variant="h6" component={RouterLink} to={isDemo ? '/auth/forgot-password' : '/forgot-password'} color="text.primary">
|
||||
Forgot Password?
|
||||
</Link>
|
||||
</Stack>
|
||||
</Grid> */}
|
||||
{/* {errors.submit && (
|
||||
<Grid item xs={12}>
|
||||
<FormHelperText error>{errors.submit}</FormHelperText>
|
||||
</Grid>
|
||||
)} */}
|
||||
<Grid item xs={12}>
|
||||
{/* <AnimateButton> */}
|
||||
<AnimateButton>
|
||||
<Button
|
||||
disabled={submitting}
|
||||
onClick={() => {
|
||||
loginsend();
|
||||
// navigate('/dashboard')
|
||||
}}
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</AnimateButton>
|
||||
{/* </AnimateButton> */}
|
||||
</Grid>
|
||||
</Grid>
|
||||
{/* </Grid> */}
|
||||
</form>
|
||||
</CardContent>
|
||||
{/* </CardHeader> */}
|
||||
</Card>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
// sx={{ m: 3, mt: 1 }}
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
{/* <AuthFooter /> */}
|
||||
|
||||
<Container maxWidth="xl">
|
||||
<Stack
|
||||
direction={{ sx: 'column', md: 'row' }}
|
||||
justifyContent={{ sx: 'center', md: 'space-between' }}
|
||||
spacing={2}
|
||||
// textAlign={{ sx: 'center', md: 'inherit' }}
|
||||
|
||||
alignItems={{ sx: 'center', md: 'inherit' }}
|
||||
width="100%"
|
||||
>
|
||||
<Stack direction="row" justifyContent="center" spacing={1}>
|
||||
<Typography variant="subtitle2" color="secondary" component="span" sx={{ display: 'flex' }}>
|
||||
© All rights reserved
|
||||
{/* <Typography variant="subtitle2" href="#mantis-privacy" target="_blank" underline="hover" sx={{ml:1}}>Privacy Policy</Typography> */}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction={{ sx: 'column', md: 'row' }}
|
||||
spacing={{ sx: 1, md: 3 }}
|
||||
textAlign={{ sx: 'center', md: 'inherit' }}
|
||||
alignItems={{ sx: 'center', md: 'inherit' }}
|
||||
// width='100%'
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
color="secondary"
|
||||
component={Link}
|
||||
href="#"
|
||||
// target="_blank"
|
||||
underline="hover"
|
||||
textAlign="center"
|
||||
>
|
||||
Terms and Conditions
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
color="secondary"
|
||||
component={Link}
|
||||
href="#"
|
||||
// target="_blank"
|
||||
underline="hover"
|
||||
textAlign="center"
|
||||
>
|
||||
Privacy Policy
|
||||
</Typography>
|
||||
{/* <Typography
|
||||
variant="subtitle2"
|
||||
color="secondary"
|
||||
component={Link}
|
||||
href="#"
|
||||
// target="_blank"
|
||||
underline="hover"
|
||||
textAlign='center'
|
||||
>
|
||||
CA Privacy Notice
|
||||
</Typography> */}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
{/* </AuthWrapper> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login1;
|
||||
@@ -45,7 +45,7 @@ import { useNavigate } from 'react-router';
|
||||
import { MdLocationCity } from 'react-icons/md';
|
||||
import { TbMapPinCode } from 'react-icons/tb';
|
||||
import { FaLocationDot } from 'react-icons/fa6';
|
||||
import axios from 'axios';
|
||||
import { getAdminPricing, getAdminTenant, getTenantCustomers, getTenantLocations, createExpressBooking } from 'pages/api/doormileApi';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
|
||||
import Loader from 'components/Loader';
|
||||
@@ -127,9 +127,7 @@ const SectionHeader = ({ color, icon, title, subtitle, action }) => (
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1.25}>
|
||||
<Avatar sx={{ width: 32, height: 32, bgcolor: color, color: '#fff', boxShadow: `0 6px 18px ${ring(color)}` }}>
|
||||
{icon}
|
||||
</Avatar>
|
||||
<Avatar sx={{ width: 32, height: 32, bgcolor: color, color: '#fff', boxShadow: `0 6px 18px ${ring(color)}` }}>{icon}</Avatar>
|
||||
<Stack>
|
||||
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 14, lineHeight: 1.1 }}>{title}</Typography>
|
||||
{subtitle && (
|
||||
@@ -234,7 +232,6 @@ const Createorder1 = () => {
|
||||
{ label: '12 Angry Men', year: 1957 }
|
||||
];
|
||||
|
||||
|
||||
const appId = localStorage.getItem('applocationid');
|
||||
const navigate = useNavigate();
|
||||
const [open, setOpen] = useState({});
|
||||
@@ -340,11 +337,13 @@ const Createorder1 = () => {
|
||||
|
||||
const fetchTenantPricing = async () => {
|
||||
try {
|
||||
const pricingResponse = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${tid}`);
|
||||
console.log('pricingResponse', pricingResponse.data.details);
|
||||
setBasePrice(pricingResponse.data.details.baseprice);
|
||||
setPricePerKm(pricingResponse.data.details.priceperkm);
|
||||
setMinKm(pricingResponse.data.details.minkm);
|
||||
const pricing = (await getAdminPricing()) || [];
|
||||
const match = pricing.find((p) => p.tenantid === tid);
|
||||
if (match) {
|
||||
setBasePrice(match.baseprice);
|
||||
setPricePerKm(match.priceperkm);
|
||||
setMinKm(match.basedistance);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('fetchTenantPricing error', error);
|
||||
}
|
||||
@@ -450,109 +449,65 @@ const Createorder1 = () => {
|
||||
}
|
||||
}, [searchword]);
|
||||
|
||||
|
||||
// ==================================================== || fetchtenantinfo || ====================================================
|
||||
const fetchtenantinfo = async () => {
|
||||
setLoading(true);
|
||||
console.log('tid', tid);
|
||||
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
|
||||
.then((res) => {
|
||||
console.log('fetchtenantinfo', res);
|
||||
if (res.data.status) {
|
||||
setTenant(res.data.details);
|
||||
setTenantid(res.data.details.tenantid);
|
||||
fetchAppAdminTokens();
|
||||
setSubCatId(res.data.details.subcategoryid);
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
});
|
||||
try {
|
||||
const info = await getAdminTenant(tid);
|
||||
if (info) {
|
||||
setTenant(info);
|
||||
setTenantid(info.tenantid);
|
||||
fetchAppAdminTokens();
|
||||
setSubCatId(info.subcategoryid);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchtenantinfo();
|
||||
}, []);
|
||||
const getsubcategories = async () => {
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL}/utils/getsubcategories/?moduleid=6`)
|
||||
.then((res) => {
|
||||
console.log('subcateRes', res.data.details);
|
||||
if (res.data.status && res.data.details) {
|
||||
setSubCat(res.data.details);
|
||||
} else {
|
||||
setSubCat([]);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setSubCat([]);
|
||||
});
|
||||
};
|
||||
// No item-subcategory lookup endpoint in the new API.
|
||||
const getsubcategories = async () => setSubCat([]);
|
||||
useEffect(() => {
|
||||
getsubcategories();
|
||||
}, []);
|
||||
|
||||
// ==================================================== || fetchTiming || ====================================================
|
||||
// No per-zone open/close-hours or geofence-radius endpoint in the new API
|
||||
// (applocationid/"zones" aren't a resource there at all — see fetchAppLocations
|
||||
// in pages/api/api.js). Falls back to a fixed 09:00–21:00 delivery window so
|
||||
// the order-row / time-slot picker below still has slots to initialise from,
|
||||
// and a generous default radius so the "too far" warning doesn't misfire.
|
||||
const fetchTiming = async () => {
|
||||
setLoading(true);
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL}/utils/getapplocations/?applocationid=${appId}`)
|
||||
.then((res) => {
|
||||
console.log('fetchTiming', res);
|
||||
const { opentime, closetime, latitude, longitude, radius } = res.data.details[0];
|
||||
if (res.data.status) {
|
||||
setAppLocaLat(latitude);
|
||||
setAppLocaLng(longitude);
|
||||
setAppLocaRadius(radius);
|
||||
setStatrttime(`${dayjs().format('MM-DD-YYYY')} ${opentime}`);
|
||||
setEndtime(`${dayjs().format('MM-DD-YYYY')} ${closetime}`);
|
||||
console.log('starttime', `${dayjs().format('MM-DD-YYYY')} ${opentime}`);
|
||||
console.log('endtime', `${dayjs().format('MM-DD-YYYY')} ${closetime} `);
|
||||
let arr = [];
|
||||
for (
|
||||
let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`, j = 0;
|
||||
dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0;
|
||||
j++, i = dayjs(i).add(30, 'm')
|
||||
) {
|
||||
arr.push(i);
|
||||
}
|
||||
console.log('setTimeslotarr', arr);
|
||||
setTimeslotarr(arr);
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
});
|
||||
const opentime = '09:00:00';
|
||||
const closetime = '21:00:00';
|
||||
setAppLocaLat(0);
|
||||
setAppLocaLng(0);
|
||||
setAppLocaRadius(100);
|
||||
setStatrttime(`${dayjs().format('MM-DD-YYYY')} ${opentime}`);
|
||||
setEndtime(`${dayjs().format('MM-DD-YYYY')} ${closetime}`);
|
||||
let arr = [];
|
||||
for (
|
||||
let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`;
|
||||
dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0;
|
||||
i = dayjs(i).add(30, 'm')
|
||||
) {
|
||||
arr.push(i);
|
||||
}
|
||||
setTimeslotarr(arr);
|
||||
setLoading(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchTiming();
|
||||
}, []);
|
||||
|
||||
// =============================================== || fetchAppAdminTokens (via appId) || ===============================================
|
||||
const fetchAppAdminTokens = async () => {
|
||||
setLoading(true);
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL}/utils/getapplocationconfig/?applocationid=${appId}`)
|
||||
.then((res) => {
|
||||
const userfcmtokemArray = res.data.details.applocationadmins.map((admin) => admin.userfcmtokem);
|
||||
console.log('fetchAppAdminTokens', res);
|
||||
console.log('userfcmtokemArray', userfcmtokemArray);
|
||||
if (res.data.status) {
|
||||
setAdmintoken(userfcmtokemArray);
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
// No zone-admin-token lookup endpoint in the new API.
|
||||
const fetchAppAdminTokens = async () => setAdmintoken([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (starttime && endtime) {
|
||||
@@ -582,116 +537,47 @@ const Createorder1 = () => {
|
||||
// =============================================== || createsubmitobj1 (create orders) || ===============================================
|
||||
|
||||
const createsubmitobj2 = async () => {
|
||||
let arr = {};
|
||||
arr = {
|
||||
orders: {
|
||||
applocationid: tenant.applolcationid,
|
||||
cancellled: '',
|
||||
categoryid: +tenant.categoryid,
|
||||
configid: 7,
|
||||
customerid: isNumChange1 == 0 ? +pickCust.customerid || 0 : 0,
|
||||
deliveryaddress: dropCust.address || '',
|
||||
deliverycharge: +totalCharge.toFixed(2) || 0,
|
||||
deliverycity: dropCust.city || '',
|
||||
deliverycontactno: dropCust.contactno || '',
|
||||
deliverycustomer: dropCust.firstname || '',
|
||||
deliveryid: isNumChange2 == 0 ? +dropCust.customerid || 0 : 0,
|
||||
deliverylandmark: dropCust.landmark || '',
|
||||
deliverylat: dropCust.latitude.toString(),
|
||||
deliverylocation: dropCust.suburb || '',
|
||||
deliverylocationid: dropCust.deliverylocationid || 0,
|
||||
deliverylong: dropCust.longitude.toString(),
|
||||
deliverytime: `${dayjs(startdate).format('YYYY-MM-DD')} ${dayjs(selectedtime.$d).format('HH:mm:ss')}`,
|
||||
deliverytype: pickCust.customerid !== 0 || dropCust.customerid !== 0 ? 'B' : 'C',
|
||||
delivered: '',
|
||||
itemcount: 1,
|
||||
kms: distance.toString() || 0,
|
||||
locationid: +tenanatLocoId, //main or branch
|
||||
moduleid: +tenant.moduleid,
|
||||
orderamount: +totalCharge.toFixed(2) || 0,
|
||||
ordercharges: 0.0,
|
||||
orderdate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
orderheaderid: 0,
|
||||
orderid: '', //
|
||||
ordernotes: otherinstructions,
|
||||
orderstatus: 'created',
|
||||
ordervalue: +totalCharge.toFixed(2) || 0,
|
||||
partnerid: tenant.partnerid,
|
||||
paymentstatus: 1,
|
||||
paymenttype: 42,
|
||||
pending: '',
|
||||
pickupaddress: pickCust.address || '',
|
||||
pickupcity: pickCust.city || '',
|
||||
pickupcontactno: pickCust.contactno || '',
|
||||
pickupcustomer: pickCust.firstname || '',
|
||||
pickuplandmark: pickCust.landmark || '',
|
||||
pickuplat: pickCust.latitude.toString(),
|
||||
pickuplocation: pickCust.suburb || '',
|
||||
pickuplocationid: pickCust.deliverylocationid || 0,
|
||||
pickuplong: pickCust.longitude.toString(),
|
||||
processing: '',
|
||||
ready: '',
|
||||
remarks: '',
|
||||
smsdelivery: isSms,
|
||||
subcategoryid: +subCatId,
|
||||
taxamount: 0.0,
|
||||
tenantid: tenant.tenantid,
|
||||
tenantuserid: parseInt(localStorage.getItem('userid')),
|
||||
collectionamt: +collectionamt || 0,
|
||||
quantity: +quantity || 1
|
||||
},
|
||||
|
||||
pickup: {
|
||||
address: pickCust.address || '',
|
||||
applocationid: tenant.applolcationid,
|
||||
city: pickCust.city || '',
|
||||
configid: 7,
|
||||
contactno: pickCust.contactno || '',
|
||||
customertoken: '',
|
||||
customerid: isNumChange1 == 0 ? pickCust.customerid || 0 : 0,
|
||||
devicetype: '',
|
||||
deviceid: '',
|
||||
dialcode: '+91',
|
||||
doorno: pickCust.doorno || '',
|
||||
email: pickCust.email || '',
|
||||
firstname: pickCust.firstname || '',
|
||||
landmark: pickCust.landmark || '',
|
||||
latitude: pickCust.latitude.toString() || '',
|
||||
longitude: pickCust.longitude.toString() || '',
|
||||
locationid: pickCust.deliverylocationid || 0,
|
||||
postcode: pickCust.postcode || '',
|
||||
primaryaddress: 1,
|
||||
profileimage: '',
|
||||
state: pickCust.state || '',
|
||||
suburb: pickCust.suburb || '',
|
||||
tenantid: tenant.tenantid
|
||||
},
|
||||
|
||||
drop: {
|
||||
address: dropCust.address || '',
|
||||
applocationid: tenant.applolcationid,
|
||||
city: dropCust.city || '',
|
||||
configid: 7,
|
||||
contactno: dropCust.contactno || '',
|
||||
customertoken: '',
|
||||
customerid: isNumChange2 == 0 ? dropCust.customerid || 0 : 0,
|
||||
devicetype: '',
|
||||
deviceid: '',
|
||||
dialcode: '+91',
|
||||
doorno: dropCust.doorno || '',
|
||||
email: dropCust.email || '',
|
||||
firstname: dropCust.firstname || '',
|
||||
landmark: dropCust.landmark || '',
|
||||
latitude: dropCust.latitude.toString(),
|
||||
longitude: dropCust.longitude.toString(),
|
||||
locationid: dropCust.deliverylocationid || 0,
|
||||
postcode: dropCust.postcode || '',
|
||||
primaryaddress: 1,
|
||||
profileimage: '',
|
||||
state: dropCust.state || '',
|
||||
suburb: dropCust.suburb || '',
|
||||
tenantid: tenant.tenantid
|
||||
}
|
||||
// POST /admin/expressbooking's schema is a single flat booking object
|
||||
// (tenantid, pickuplocationid, customer_phone/name, delivery* fields,
|
||||
// service_option, finalprice, notes, parcels[]) — nothing like the old
|
||||
// pickup/drop/orders triple. The drop contact becomes the booking's
|
||||
// customer (new API models the delivery recipient as "the customer");
|
||||
// the pickup side maps to pickuplocationid when a saved location was
|
||||
// picked (tenanatLocoId/isLocation, set by the "Business Location"
|
||||
// Autocomplete below — pickCust.deliverylocationid was always '' and
|
||||
// never actually got wired to the picker, so this shortcut never fired),
|
||||
// otherwise raw pickup* fields following the doc's own delivery*/pickup*
|
||||
// naming symmetry (pickupaddress/pickuppincode/pickupcity/pickuplatitude/
|
||||
// pickuplongitude — NOT shown in the doc's example, so unverified against
|
||||
// the live API).
|
||||
const arr = {
|
||||
tenantid: tenant.tenantid,
|
||||
...(isLocation && tenanatLocoId
|
||||
? { pickuplocationid: tenanatLocoId }
|
||||
: {
|
||||
pickupaddress: pickCust.address || '',
|
||||
pickuppincode: pickCust.postcode || '',
|
||||
pickupcity: pickCust.city || '',
|
||||
pickuplatitude: Number(pickCust.latitude) || 0,
|
||||
pickuplongitude: Number(pickCust.longitude) || 0
|
||||
}),
|
||||
customer_phone: dropCust.contactno || '',
|
||||
customer_name: dropCust.firstname || '',
|
||||
deliveryaddress: dropCust.address || '',
|
||||
deliverypincode: dropCust.postcode || '',
|
||||
deliverycity: dropCust.city || '',
|
||||
deliverylatitude: Number(dropCust.latitude) || 0,
|
||||
deliverylongitude: Number(dropCust.longitude) || 0,
|
||||
service_option: 'Normal',
|
||||
finalprice: +totalCharge.toFixed(2) || 0,
|
||||
notes: otherinstructions || '',
|
||||
parcels: [
|
||||
{
|
||||
itemcategory: subCat.find((c) => c.subcategoryid === subCatId)?.categoryname || 'General',
|
||||
itemdescription: 'Order',
|
||||
declaredvalue: +totalCharge.toFixed(2) || 0
|
||||
}
|
||||
]
|
||||
};
|
||||
console.log('createsubmitobj2', arr);
|
||||
if (!pickCust.firstname) {
|
||||
@@ -727,29 +613,28 @@ const Createorder1 = () => {
|
||||
} else if (!setSubCatId) {
|
||||
opentoast('Choose SubCategory ', 'warning', 2000);
|
||||
} else {
|
||||
setLoading(true);
|
||||
try {
|
||||
const createRes = await axios.post(`${process.env.REACT_APP_URL2}/orders/createorder`, arr);
|
||||
// const createRes = await axios.post(`${process.env.REACT_APP_URL}/orders/createorder`, arr);
|
||||
if (createRes.data.status) {
|
||||
console.log('createRes', createRes);
|
||||
const createRes = await createExpressBooking(arr);
|
||||
if (createRes.success) {
|
||||
enqueueSnackbar('Order Created Successfully', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 1000
|
||||
});
|
||||
if (admintoken) {
|
||||
notifyadmin(admintoken);
|
||||
// sendnotifications();
|
||||
}
|
||||
|
||||
navigate('/nearle/orders');
|
||||
} else {
|
||||
opentoast('Something went wrong, Cannot create order', 'warning');
|
||||
opentoast(createRes.message || 'Something went wrong, Cannot create order', 'warning');
|
||||
}
|
||||
setLoading(false);
|
||||
console.log(createRes);
|
||||
} catch (error) {
|
||||
console.log('createResErr', error);
|
||||
// Previously silent — a thrown error here (network failure, CityGate
|
||||
// rejection, validation error) left the operator with no feedback at
|
||||
// all and a permanently stuck loading spinner (setLoading(false) was
|
||||
// only reachable on the success path).
|
||||
opentoast(error.response?.data?.message || error.message || 'Cannot create order', 'error', 3000);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -774,41 +659,22 @@ const Createorder1 = () => {
|
||||
const clientdetails = async () => {
|
||||
setLoading2(true);
|
||||
try {
|
||||
let url =
|
||||
searchCustList == ''
|
||||
? // ? `${process.env.REACT_APP_URL}/customers/getbytid/?tenantid=${tid}&pageno=1&pagesize=1`
|
||||
`${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tid}&pageno=1&pagesize=20`
|
||||
: `${process.env.REACT_APP_URL}/customers/search/?tenantid=${tid}&keyword=${searchCustList}`;
|
||||
|
||||
await axios
|
||||
.get(url)
|
||||
.then((res) => {
|
||||
console.log('clientdetails', res.data.details);
|
||||
if (res.data.status && res.data.details) {
|
||||
setClientdetail(res.data.details || []);
|
||||
setCustomerlist(res.data.details || []);
|
||||
let arr = [];
|
||||
res.data.details.map((val) => {
|
||||
arr.push({
|
||||
label: `${val.firstname} | ${val.contactno}`,
|
||||
...val
|
||||
});
|
||||
});
|
||||
setClientdetailarr(arr);
|
||||
} else {
|
||||
setClientdetail([]);
|
||||
setCustomerlist([]);
|
||||
setClientdetailarr([]);
|
||||
}
|
||||
setLoading2(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading2(false);
|
||||
opentoast('server error', 'warning');
|
||||
});
|
||||
const customers = (await getTenantCustomers()) || [];
|
||||
const filtered = searchCustList
|
||||
? customers.filter(
|
||||
(val) =>
|
||||
val.firstname?.toLowerCase().includes(searchCustList.toLowerCase()) ||
|
||||
val.phone?.includes(searchCustList) ||
|
||||
val.contactno?.includes(searchCustList)
|
||||
)
|
||||
: customers;
|
||||
setClientdetail(filtered);
|
||||
setCustomerlist(filtered);
|
||||
setClientdetailarr(filtered.map((val) => ({ label: `${val.firstname} | ${val.phone || val.contactno}`, ...val })));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
opentoast('server error', 'warning');
|
||||
} finally {
|
||||
setLoading2(false);
|
||||
}
|
||||
};
|
||||
@@ -821,74 +687,21 @@ const Createorder1 = () => {
|
||||
const clientdetailsbusiness = async () => {
|
||||
setLoading2(true);
|
||||
try {
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL}/customers/getbytid/?tenantid=${tid}&locationid=1`)
|
||||
.then((res) => {
|
||||
console.log('clientdetailsbusiness', res.data.details);
|
||||
if (res.data.status) {
|
||||
setClientdetail(res.data.details);
|
||||
// if (!searchword) {
|
||||
// setClientdetailarr(res.data.details)
|
||||
// }
|
||||
let arr = [];
|
||||
res.data.details.map((val) => {
|
||||
arr.push({
|
||||
label: `${val.firstname} | ${val.contactno}`,
|
||||
...val
|
||||
});
|
||||
});
|
||||
setClientdetailbusinessarr(arr);
|
||||
}
|
||||
setLoading2(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading2(false);
|
||||
opentoast('server error', 'warning');
|
||||
});
|
||||
const customers = (await getTenantCustomers()) || [];
|
||||
setClientdetail(customers);
|
||||
setClientdetailbusinessarr(customers.map((val) => ({ label: `${val.firstname} | ${val.phone || val.contactno}`, ...val })));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
opentoast('server error', 'warning');
|
||||
} finally {
|
||||
setLoading2(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ================================================== || sendnotifications || ==================================================
|
||||
const sendnotifications = async () => {
|
||||
setLoading(true);
|
||||
await axios
|
||||
.post(`${process.env.REACT_APP_URL}/utils/sendnotifications`, {
|
||||
priority: 'high',
|
||||
registration_ids: admintoken,
|
||||
data: {
|
||||
accessid: process.env.REACT_APP_RIDER_ACCESS_ID
|
||||
},
|
||||
notification: {
|
||||
title: 'Nearle Merchant',
|
||||
body: 'An Order has been placed successfully,kindly process the same',
|
||||
sound: 'ring'
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
if (res.data.message == 'Success') {
|
||||
enqueueSnackbar('Notification sent Successfully', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 1000
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
enqueueSnackbar(err.message, {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 1000
|
||||
});
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
// No bulk-notify endpoint in the new API — never actually called (dead in
|
||||
// the original code too, only referenced from a commented-out call site).
|
||||
const sendnotifications = async () => {};
|
||||
// ============================================= || Address Autocomplete (pick) || =============================================
|
||||
const handlePickPlaceSelected = (place) => {
|
||||
setInputValue2(`${place.name}, ${place.formatted_address}`);
|
||||
@@ -1015,16 +828,11 @@ const Createorder1 = () => {
|
||||
// ============================================= || gettenantlocations (branches) || =============================================
|
||||
const gettenantlocations = async () => {
|
||||
try {
|
||||
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${tid}`);
|
||||
console.log('gettenantlocations', res.data.details);
|
||||
if (res.data && res.data.details) {
|
||||
setTenantlocations(res.data.details);
|
||||
if (res.data.details.length == 1) {
|
||||
setIsLocation(true);
|
||||
setTenanatLocoId(res.data.details[0].locationid);
|
||||
}
|
||||
} else {
|
||||
setTenantlocations([]);
|
||||
const locations = (await getTenantLocations(tid)) || [];
|
||||
setTenantlocations(locations);
|
||||
if (locations.length == 1) {
|
||||
setIsLocation(true);
|
||||
setTenanatLocoId(locations[0].locationid);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('gettenantlocations', err);
|
||||
@@ -1124,9 +932,7 @@ const Createorder1 = () => {
|
||||
) : (
|
||||
<Autocomplete
|
||||
options={tenantLocations || []}
|
||||
getOptionLabel={(option) =>
|
||||
option && option.locationname ? `${option.locationname} (${option.suburb || ''})` : ''
|
||||
}
|
||||
getOptionLabel={(option) => (option && option.locationname ? `${option.locationname} (${option.suburb || ''})` : '')}
|
||||
isOptionEqualToValue={(option, value) => option?.locationid === value?.locationid}
|
||||
PaperComponent={SoftPaper}
|
||||
sx={{ width: { xs: '100%', sm: 300 } }}
|
||||
@@ -1926,16 +1732,13 @@ const Createorder1 = () => {
|
||||
{/* ================================================= || Time || ================================================= */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<SectionCard sx={{ mt: 2, minHeight: 390 }}>
|
||||
<SectionHeader
|
||||
color="#f59e0b"
|
||||
icon={<MdSchedule size={16} />}
|
||||
title="Schedule"
|
||||
subtitle="Pickup date & time slot"
|
||||
/>
|
||||
<SectionHeader color="#f59e0b" icon={<MdSchedule size={16} />} title="Schedule" subtitle="Pickup date & time slot" />
|
||||
<Box sx={{ p: { xs: 2, md: 2.5 } }}>
|
||||
<Grid container>
|
||||
<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={{}}>
|
||||
<DatePicker
|
||||
format="DD-MM-YYYY"
|
||||
@@ -2006,7 +1809,8 @@ const Createorder1 = () => {
|
||||
<Stack direction="row" flexWrap="wrap" gap={0.75} useFlexGap>
|
||||
{timeslotarr.map((val, index) => {
|
||||
if (
|
||||
dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0
|
||||
dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <=
|
||||
0
|
||||
) {
|
||||
const active = dayjs(selectedtime).format('HH:mm') == dayjs(val).format('HH:mm');
|
||||
return (
|
||||
@@ -2082,7 +1886,9 @@ const Createorder1 = () => {
|
||||
getOptionLabel={(option) => `${option.subcategoryname}` || ''}
|
||||
sx={{ my: 2, zIndex: '100' }}
|
||||
fullWidth
|
||||
renderInput={(params) => <TextField {...params} label={subCatName == '' ? tenant.subcategoryname : subCatName} />}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label={subCatName == '' ? tenant.subcategoryname : subCatName} />
|
||||
)}
|
||||
onChange={(event, value, reason) => {
|
||||
if (value) {
|
||||
console.log(value);
|
||||
@@ -2416,14 +2222,16 @@ const Createorder1 = () => {
|
||||
'&:hover': isUsed
|
||||
? {}
|
||||
: {
|
||||
borderColor: edge(BRAND),
|
||||
bgcolor: tint(BRAND),
|
||||
boxShadow: DT.shadowSoft
|
||||
}
|
||||
borderColor: edge(BRAND),
|
||||
bgcolor: tint(BRAND),
|
||||
boxShadow: DT.shadowSoft
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<Stack sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Stack direction="row" alignItems="center" spacing={0.75} flexWrap="wrap" useFlexGap>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ import { useState, useEffect, Fragment, useRef } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
var utc = require('dayjs/plugin/utc');
|
||||
dayjs.extend(utc);
|
||||
import axios from 'axios';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
@@ -23,17 +22,14 @@ import {
|
||||
Dialog,
|
||||
TableRow,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
Tooltip,
|
||||
Skeleton,
|
||||
CircularProgress,
|
||||
InputBase,
|
||||
Backdrop,
|
||||
SpeedDial,
|
||||
SpeedDialIcon,
|
||||
SpeedDialAction,
|
||||
Badge,
|
||||
TableContainer,
|
||||
Checkbox
|
||||
TableContainer
|
||||
} from '@mui/material';
|
||||
import {
|
||||
MdAccessTime,
|
||||
@@ -51,7 +47,8 @@ import {
|
||||
MdCalendarMonth,
|
||||
MdReceiptLong,
|
||||
MdClear,
|
||||
MdNotes
|
||||
MdNotes,
|
||||
MdMyLocation
|
||||
} from 'react-icons/md';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import Loader from 'components/Loader';
|
||||
@@ -60,13 +57,8 @@ import CircularLoader from 'components/CircularLoader';
|
||||
import AiImage from '../../../assets/images/aiImage.png';
|
||||
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
fetchPercentageData,
|
||||
createAutomationDeliveries,
|
||||
cancelMultipleOrder,
|
||||
getallriders,
|
||||
fetchorderscount
|
||||
} from '../../api/api';
|
||||
import { fetchPercentageData, createAutomationDeliveries, getallriders, fetchorderscount } from '../../api/api';
|
||||
import { getBookings, cancelBooking, getAdminCustomers, getBookingTrack } from 'pages/api/doormileApi';
|
||||
|
||||
// ============================================================================
|
||||
// Design tokens — shared with the rest of the redesigned operator pages.
|
||||
@@ -99,29 +91,37 @@ const dtEdge = edge;
|
||||
const BRAND = '#C01227';
|
||||
const BRAND_LIGHT = '#D25463';
|
||||
|
||||
|
||||
// Semantic per-row status palette — colors per brand standard:
|
||||
// green=delivered, amber=pending, blue=created/processing, red=cancelled,
|
||||
// dark-red=failed, purple=on-hold.
|
||||
// Semantic per-row status palette, keyed by GET /admin/bookings' real `status`
|
||||
// enum (lowercased — StatusBadge lowercases before lookup). Only
|
||||
// pending_pickup and converted_to_consignment have been observed against the
|
||||
// live API so far; the rest are reasonable guesses at likely sibling states
|
||||
// with a safe unknown-status fallback in StatusBadge below.
|
||||
const ROW_STATUS_META = {
|
||||
created: { label: 'Created', color: '#3b82f6', icon: MdLocalShipping },
|
||||
pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||
processing: { label: 'Processing', color: '#3b82f6', icon: MdAccessTime },
|
||||
modified: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle },
|
||||
confirmed: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle },
|
||||
ready: { label: 'Accepted', color: '#6366f1', icon: MdCheckCircle },
|
||||
active: { label: 'Picked', color: '#8b5cf6', icon: MdLocalShipping },
|
||||
onhold: { label: 'On Hold', color: '#8b5cf6', icon: MdHistoryToggleOff },
|
||||
closed: { label: 'Closed', color: '#06b6d4', icon: MdCheckCircle },
|
||||
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
|
||||
failed: { label: 'Failed', color: '#991b1b', icon: MdCancel },
|
||||
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
|
||||
pending_pickup: { label: 'Pending Pickup', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||
pending_assignment: { label: 'Pending Assignment', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||
converted_to_consignment: { label: 'Converted', color: '#6366f1', icon: MdCheckCircle },
|
||||
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
|
||||
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
|
||||
};
|
||||
|
||||
// Top-level pill tabs.
|
||||
const ORDERS_STATUS_TABS = [
|
||||
{ idx: 0, status: 'created', label: 'Created', color: BRAND, icon: MdLocalShipping, countKey: 'created' }
|
||||
];
|
||||
// Top-level pill tabs. GET /admin/bookings has no documented status filter
|
||||
// param, so this tab doesn't narrow the server query — it's cosmetic until
|
||||
// the API supports it.
|
||||
const ORDERS_STATUS_TABS = [{ idx: 0, status: 'created', label: 'Bookings', color: BRAND, icon: MdLocalShipping, countKey: 'created' }];
|
||||
|
||||
// Haversine straight-line distance in km — GET /admin/bookings has no `kms`
|
||||
// field, but does carry both pickup*/delivery* lat/lng, so this is computed
|
||||
// client-side rather than left blank. It's a straight line, not the road
|
||||
// route the old `kms` field represented — labelled accordingly in the header.
|
||||
const haversineKm = (lat1, lon1, lat2, lon2) => {
|
||||
if (![lat1, lon1, lat2, lon2].every(Number.isFinite)) return null;
|
||||
const R = 6371;
|
||||
const toRad = (d) => (d * Math.PI) / 180;
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLon = toRad(lon2 - lon1);
|
||||
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
|
||||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
};
|
||||
|
||||
const StatusBadge = ({ status }) => {
|
||||
const meta = ROW_STATUS_META[String(status || '').toLowerCase()] || {
|
||||
@@ -201,6 +201,16 @@ const Orders = () => {
|
||||
const [tabstatus, setTabstatus] = useState('Created');
|
||||
const [currentStatus, setCurrentStatus] = useState('created');
|
||||
const [cancelOpen, setCancelOpen] = useState(false);
|
||||
const [trackBookingId, setTrackBookingId] = useState(null);
|
||||
|
||||
// GET /admin/bookings/:id/track — response shape isn't documented beyond
|
||||
// the endpoint existing (Status: Done in jupiter2doormile.md). Rendered
|
||||
// defensively below rather than assuming specific field names.
|
||||
const { data: trackData, isLoading: trackLoading } = useQuery({
|
||||
queryKey: ['booking-track', trackBookingId],
|
||||
queryFn: () => getBookingTrack(trackBookingId),
|
||||
enabled: !!trackBookingId
|
||||
});
|
||||
const [orderheaderid, setOrderheaderid] = useState('');
|
||||
const locationId = 0;
|
||||
const locoName = 'All Locations';
|
||||
@@ -213,10 +223,8 @@ const Orders = () => {
|
||||
|
||||
// Floating button and Dialog states
|
||||
const [speedDialOpen, setSpeedDialOpen] = useState(false);
|
||||
const [multiDeleteDialog, setMultiDeleteDialog] = useState(false);
|
||||
const [createloader, setCreateloader] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [deliverylist, setDeliverylist] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
@@ -247,10 +255,7 @@ const Orders = () => {
|
||||
refetchInterval: 15000
|
||||
});
|
||||
|
||||
const {
|
||||
data: ordersCountData,
|
||||
refetch: orderscountRefetch
|
||||
} = useQuery({
|
||||
const { data: ordersCountData, refetch: orderscountRefetch } = useQuery({
|
||||
queryKey: ['ordersCount', locationId, startdate, enddate, currentStatus, tid, locationId],
|
||||
queryFn: fetchorderscount,
|
||||
refetchOnMount: true,
|
||||
@@ -258,47 +263,69 @@ const Orders = () => {
|
||||
refetchInterval: 15000
|
||||
});
|
||||
|
||||
const {
|
||||
data: autoRiders
|
||||
} = useQuery({
|
||||
const { data: autoRiders } = useQuery({
|
||||
queryKey: ['getallriders'],
|
||||
queryFn: getallriders,
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true
|
||||
});
|
||||
|
||||
const cancelorder = async () => {
|
||||
await axios
|
||||
.put(`${process.env.REACT_APP_URL}/orders/updateorder`, {
|
||||
orderheaderid: orderheaderid,
|
||||
orderstatus: 'cancelled',
|
||||
cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.data.status) {
|
||||
enqueueSnackbar('Order Cancelled Successfully', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
refetchOrders();
|
||||
orderscountRefetch();
|
||||
percentagedataRefetch();
|
||||
setCancelOpen(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
// GET /admin/bookings only carries `appcustomerid` — no name/phone on the
|
||||
// booking itself — so customer display fields are joined client-side
|
||||
// against GET /admin/customers. Response field names for that endpoint
|
||||
// aren't documented; firstname/phone are guessed by symmetry with
|
||||
// tenantcustomers and fall back to showing the raw id.
|
||||
const { data: customerMap } = useQuery({
|
||||
queryKey: ['adminCustomersMap'],
|
||||
queryFn: async () => {
|
||||
const customers = (await getAdminCustomers()) || [];
|
||||
const map = new Map();
|
||||
customers.forEach((c) => {
|
||||
const id = c.appcustomerid ?? c.customerid ?? c.id;
|
||||
if (id != null) map.set(id, c);
|
||||
});
|
||||
return map;
|
||||
},
|
||||
staleTime: 5 * 60 * 1000
|
||||
});
|
||||
|
||||
const cancelorder = async () => {
|
||||
try {
|
||||
const res = await cancelBooking(orderheaderid);
|
||||
if (res.success) {
|
||||
enqueueSnackbar('Order Cancelled Successfully', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
refetchOrders();
|
||||
orderscountRefetch();
|
||||
percentagedataRefetch();
|
||||
setCancelOpen(false);
|
||||
} else {
|
||||
enqueueSnackbar(res.message || 'Failed to cancel order', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to cancel order', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Maps to /admin/bookings — tenantid/locationid/status/date filters used by
|
||||
// the old endpoint aren't documented on this resource; pageno/pagesize are.
|
||||
const fetchOrders = async ({ pageParam = 1 }) => {
|
||||
const res = await axios.get(
|
||||
`${process.env.REACT_APP_URL}/orders/tenant/getorders/?tenantid=${tid}&locationid=${locationId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${debouncedSearch}`
|
||||
);
|
||||
const data = (await getBookings(pageParam, rowsPerPage)) || [];
|
||||
return {
|
||||
data: res.data.details,
|
||||
nextPage: res.data.details.length === rowsPerPage ? pageParam + 1 : undefined
|
||||
data,
|
||||
nextPage: data.length === rowsPerPage ? pageParam + 1 : undefined
|
||||
};
|
||||
};
|
||||
|
||||
@@ -352,7 +379,11 @@ const Orders = () => {
|
||||
const createDeliveryMutation = useMutation({
|
||||
mutationFn: createAutomationDeliveries,
|
||||
onSuccess: (data, variables) => {
|
||||
enqueueSnackbar('Orders Optimised Successfully', { variant: 'success', autoHideDuration: 2000, anchorOrigin: { vertical: 'top', horizontal: 'right' } });
|
||||
enqueueSnackbar('Orders Optimised Successfully', {
|
||||
variant: 'success',
|
||||
autoHideDuration: 2000,
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' }
|
||||
});
|
||||
orderscountRefetch();
|
||||
refetchOrders();
|
||||
setCreateloader(false);
|
||||
@@ -379,43 +410,37 @@ const Orders = () => {
|
||||
}
|
||||
});
|
||||
|
||||
const cancelMultipleOrderMutation = useMutation({
|
||||
mutationFn: cancelMultipleOrder,
|
||||
onSuccess: (data) => {
|
||||
if (data.status) {
|
||||
setMultiDeleteDialog(false);
|
||||
enqueueSnackbar('Orders Cancelled Successfully', { variant: 'success', autoHideDuration: 2000 });
|
||||
refetchOrders();
|
||||
orderscountRefetch();
|
||||
percentagedataRefetch();
|
||||
setDeliverylist([]);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
enqueueSnackbar(error.message, { variant: 'error', autoHideDuration: 4000 });
|
||||
}
|
||||
});
|
||||
|
||||
const handleCreateDelivery = async () => {
|
||||
if (rows.length === 0) return;
|
||||
setIsLoading(true);
|
||||
setCreateloader(true);
|
||||
const deliveryData = rows.map((val) => ({
|
||||
...val,
|
||||
deliveryid: 0,
|
||||
deliverydate: dayjs(val.deliverydate).utc().format('YYYY-MM-DD HH:mm:ss'),
|
||||
assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
orderstatus: 'pending',
|
||||
orderamount: val.deliverycharge,
|
||||
droplat: val.deliverylat,
|
||||
droplon: val.deliverylong,
|
||||
pickuplat: val.pickuplat,
|
||||
pickuplon: val.pickuplong,
|
||||
ordernotes: val.ordernotes,
|
||||
deliverycharges: val.deliverycharge,
|
||||
pickuplocation: val.pickupsuburb,
|
||||
deliverylocation: val.deliverysuburb
|
||||
}));
|
||||
// The optimiser service (routes.workolik.com) itself is untouched, but the
|
||||
// fields sourced here have to match GET /admin/bookings' real shape —
|
||||
// deliverylat/pickuplat/deliverycharge/etc. don't exist on a booking
|
||||
// object (see pickuplatitude/deliverylatitude/serviceoptions[0].estimatedprice
|
||||
// instead). The solver's own expected input contract is unverified since
|
||||
// it's a separate, unchanged service.
|
||||
const deliveryData = rows.map((val) => {
|
||||
const charge = val.serviceoptions?.[0]?.estimatedprice;
|
||||
return {
|
||||
...val,
|
||||
deliveryid: 0,
|
||||
deliverydate: dayjs(val.serviceoptions?.[0]?.estimateddeliveryat || val.createdat)
|
||||
.utc()
|
||||
.format('YYYY-MM-DD HH:mm:ss'),
|
||||
assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
orderstatus: 'pending',
|
||||
orderamount: charge,
|
||||
droplat: val.deliverylatitude,
|
||||
droplon: val.deliverylongitude,
|
||||
pickuplat: val.pickuplatitude,
|
||||
pickuplon: val.pickuplongitude,
|
||||
ordernotes: val.notes,
|
||||
deliverycharges: charge,
|
||||
pickuplocation: val.pickupaddress,
|
||||
deliverylocation: val.deliveryaddress
|
||||
};
|
||||
});
|
||||
|
||||
createDeliveryMutation.mutate({
|
||||
deliveries: deliveryData,
|
||||
@@ -427,8 +452,22 @@ const Orders = () => {
|
||||
|
||||
// KPI tile definitions.
|
||||
const kpiCards = [
|
||||
{ key: 'created', label: 'Created Orders', color: BRAND, icon: MdLocalShipping, value: percentageData?.created, percentage: percentageData?.percentage1 },
|
||||
{ key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdHourglassEmpty, value: percentageData?.uncoveredOrders, percentage: percentageData?.percentage2 }
|
||||
{
|
||||
key: 'created',
|
||||
label: 'Created Orders',
|
||||
color: BRAND,
|
||||
icon: MdLocalShipping,
|
||||
value: percentageData?.created,
|
||||
percentage: percentageData?.percentage1
|
||||
},
|
||||
{
|
||||
key: 'pending',
|
||||
label: 'Pending Orders',
|
||||
color: '#f59e0b',
|
||||
icon: MdHourglassEmpty,
|
||||
value: percentageData?.uncoveredOrders,
|
||||
percentage: percentageData?.percentage2
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -532,14 +571,9 @@ const Orders = () => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{/* ============================================= || Status Tabs + Search (compact) || ============================================= */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
@@ -556,13 +590,7 @@ const Orders = () => {
|
||||
background: '#fff'
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
gap={1.5}
|
||||
sx={{ flexWrap: 'wrap-reverse' }}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" gap={1.5} sx={{ flexWrap: 'wrap-reverse' }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={0.75}
|
||||
@@ -640,7 +668,6 @@ const Orders = () => {
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -692,16 +719,16 @@ const Orders = () => {
|
||||
}}
|
||||
>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Order Location</TableCell>
|
||||
<TableCell>Order</TableCell>
|
||||
<TableCell>Pickup</TableCell>
|
||||
<TableCell>Drop</TableCell>
|
||||
<TableCell align="center">Qty</TableCell>
|
||||
<TableCell align="right">COD</TableCell>
|
||||
<TableCell align="center">Kms</TableCell>
|
||||
<TableCell align="center">Kms (approx)</TableCell>
|
||||
<TableCell align="right">Charges</TableCell>
|
||||
<TableCell>Notes</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
{currentStatus === 'created' && <TableCell align="right">Actions</TableCell>}
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
@@ -710,7 +737,7 @@ const Orders = () => {
|
||||
rows.length === 0 &&
|
||||
Array.from({ length: 10 }).map((_, 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 }}>
|
||||
<Skeleton animation="wave" height={20} />
|
||||
</TableCell>
|
||||
@@ -720,7 +747,7 @@ const Orders = () => {
|
||||
|
||||
{!isLoadingGetOrders && rows.length === 0 && (
|
||||
<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}>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
@@ -734,9 +761,7 @@ const Orders = () => {
|
||||
>
|
||||
<MdLocalShipping size={26} />
|
||||
</Avatar>
|
||||
<Typography sx={{ fontWeight: 700, color: DT.textPrimary, fontSize: 14 }}>
|
||||
No {currentStatus} orders
|
||||
</Typography>
|
||||
<Typography sx={{ fontWeight: 700, color: DT.textPrimary, fontSize: 14 }}>No {currentStatus} orders</Typography>
|
||||
<Typography sx={{ color: DT.textSecondary, fontSize: 12 }}>
|
||||
{searchword ? 'Try a different keyword or clear the search.' : 'Adjust the location, status, or date range above.'}
|
||||
</Typography>
|
||||
@@ -765,20 +790,13 @@ const Orders = () => {
|
||||
)}
|
||||
|
||||
{rows.map((row, index) => {
|
||||
const isItemSelected = !!deliverylist.find((res) => res.orderheaderid === row.orderheaderid);
|
||||
const handleCheckbox = (e) => {
|
||||
if (e.target.checked) {
|
||||
setDeliverylist((prev) => [...prev, { ...row, sno: prev.length + 1 }]);
|
||||
} else {
|
||||
setDeliverylist((prev) =>
|
||||
prev.filter((item) => item.orderheaderid !== row.orderheaderid).map((item, i) => ({ ...item, sno: i + 1 }))
|
||||
);
|
||||
}
|
||||
};
|
||||
const customer = customerMap?.get(row.appcustomerid);
|
||||
const kms = haversineKm(row.pickuplatitude, row.pickuplongitude, row.deliverylatitude, row.deliverylongitude);
|
||||
const isCancellable = !['Cancelled', 'Converted_To_Consignment'].includes(row.status);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={`${row.orderheaderid}-${index}`}
|
||||
key={`${row.bookingid}-${index}`}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.12s, box-shadow 0.12s',
|
||||
@@ -796,34 +814,24 @@ const Orders = () => {
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<Typography sx={{ fontWeight: 700, color: DT.textSecondary }}>
|
||||
{page * rowsPerPage + index + 1}
|
||||
</Typography>
|
||||
<Typography sx={{ fontWeight: 700, color: DT.textSecondary }}>{page * rowsPerPage + index + 1}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||
{row.locationname}
|
||||
{row.locationsuburb && ` - ${row.locationsuburb}`}
|
||||
{row.bookingno || `#${row.bookingid}`}
|
||||
</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 }}>
|
||||
<MdAccessTime size={10} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
||||
{(() => {
|
||||
const dateObj = row.pickupslot && dayjs(row.pickupslot).isValid()
|
||||
? dayjs(row.pickupslot)
|
||||
: dayjs(row.deliverydate || row.orderdate);
|
||||
const dateObj = dayjs(row.createdat);
|
||||
return (
|
||||
<>
|
||||
<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 sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600 }} noWrap>
|
||||
· {dateObj.format('DD MMM YY')}
|
||||
{dateObj.isValid() && `· ${dateObj.format('DD MMM YY')}`}
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
@@ -833,70 +841,85 @@ const Orders = () => {
|
||||
|
||||
<TableCell>
|
||||
<Stack direction="column">
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||
{row.pickupcustomer}
|
||||
</Typography>
|
||||
<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)}…` : '—')}
|
||||
<Tooltip title={row.pickupaddress || ''}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||
{row.pickupaddress ? `${row.pickupaddress.slice(0, 20)}…` : '—'}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<Typography variant="caption" sx={{ color: DT.textMuted }} noWrap>
|
||||
{row.pickuppincode || ''}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Stack direction="column">
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||
{row.deliverycustomer}
|
||||
{customer?.firstname || customer?.name || `Customer #${row.appcustomerid ?? '—'}`}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{row.deliverycontactno}
|
||||
{customer?.phone || customer?.contactno || ''}
|
||||
</Typography>
|
||||
<Tooltip title={row.deliveryaddress}>
|
||||
<Tooltip title={row.deliveryaddress || ''}>
|
||||
<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>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
|
||||
<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 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 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 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>
|
||||
{row.ordernotes ? (
|
||||
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}>
|
||||
<MdNotes size={12} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
maxWidth: 140,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
fontWeight: 600
|
||||
}}
|
||||
>
|
||||
{row.ordernotes}
|
||||
</Typography>
|
||||
</Stack>
|
||||
{row.notes ? (
|
||||
<Tooltip title={row.notes}>
|
||||
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}>
|
||||
<MdNotes size={12} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
maxWidth: 140,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
fontWeight: 600
|
||||
}}
|
||||
>
|
||||
{row.notes}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Typography variant="caption" sx={{ color: DT.textMuted }}>
|
||||
—
|
||||
@@ -905,18 +928,40 @@ const Orders = () => {
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<StatusBadge status={row.orderstatus} />
|
||||
<StatusBadge status={row.status} />
|
||||
</TableCell>
|
||||
|
||||
{currentStatus === 'created' && (
|
||||
<TableCell align="right">
|
||||
{row.orderstatus === 'created' && (
|
||||
<TableCell align="right">
|
||||
<Stack direction="row" spacing={0.5} justifyContent="flex-end">
|
||||
<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">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOrderheaderid(row.orderheaderid);
|
||||
setOrderheaderid(row.bookingid);
|
||||
setCancelOpen(true);
|
||||
}}
|
||||
sx={{
|
||||
@@ -935,29 +980,20 @@ const Orders = () => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
|
||||
{rows.length !== 0 && (
|
||||
<TableRow sx={{ '&:hover': { backgroundColor: 'transparent !important' } }}>
|
||||
<TableCell colSpan={currentStatus === 'created' ? 12 : 11} sx={{ borderBottom: 'none', py: 1, bgcolor: DT.surfaceAlt }}>
|
||||
<Stack
|
||||
ref={loadMoreRef}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
spacing={1}
|
||||
sx={{ minHeight: 32 }}
|
||||
>
|
||||
<TableCell colSpan={11} sx={{ borderBottom: 'none', py: 1, bgcolor: DT.surfaceAlt }}>
|
||||
<Stack ref={loadMoreRef} direction="row" alignItems="center" justifyContent="center" spacing={1} sx={{ minHeight: 32 }}>
|
||||
{isFetchingNextPage || hasNextPage ? (
|
||||
<>
|
||||
<CircularProgress size={14} sx={{ color: BRAND }} />
|
||||
<Typography sx={{ fontSize: 11.5, color: DT.textSecondary, fontWeight: 700 }}>
|
||||
Loading more orders…
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: 11.5, color: DT.textSecondary, fontWeight: 700 }}>Loading more orders…</Typography>
|
||||
</>
|
||||
) : (
|
||||
<Typography sx={{ fontSize: 11.5, color: DT.textMuted, fontWeight: 700, letterSpacing: 0.3 }}>
|
||||
@@ -1033,6 +1069,38 @@ const Orders = () => {
|
||||
</DialogContent>
|
||||
</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 || ============================================= */}
|
||||
<DateFilterDialog
|
||||
open={dateOpen}
|
||||
@@ -1043,67 +1111,6 @@ const Orders = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { React, useState, useEffect, useRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
|
||||
|
||||
// material-ui
|
||||
@@ -67,6 +66,7 @@ import { FaCircleCheck } from 'react-icons/fa6';
|
||||
import MapWithRoute from './mapWithRoute';
|
||||
import CircularLoader from 'components/CircularLoader';
|
||||
import { fetchDeliveries, fetchRidersList, gettenantlocations, getTenants } from 'pages/api/api';
|
||||
import { getDashboard, getConsignmentLogs } from 'pages/api/doormileApi';
|
||||
import { CSVExport } from 'components/third-party/ReactTable';
|
||||
import Loader from 'components/Loader';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
@@ -227,205 +227,6 @@ const StampCell = ({ value, formatDate, formatTime, success }) => {
|
||||
|
||||
// ==============================|| Orders Details ||============================== //
|
||||
|
||||
// Haversine distance between two [lat, lng] points in kilometers.
|
||||
function haversineKm(a, b) {
|
||||
const R = 6371; // km
|
||||
const toRad = (d) => (d * Math.PI) / 180;
|
||||
const lat1 = toRad(a[0]);
|
||||
const lat2 = toRad(b[0]);
|
||||
const dLat = toRad(b[0] - a[0]);
|
||||
const dLon = toRad(b[1] - a[1]);
|
||||
const s = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
|
||||
return 2 * R * Math.asin(Math.min(1, Math.sqrt(s)));
|
||||
}
|
||||
|
||||
function kalmanSmoothGps(pings, options = {}) {
|
||||
if (!Array.isArray(pings) || pings.length === 0) return [];
|
||||
|
||||
// 1. Filter out obviously invalid coordinate pings (e.g. 0,0 or NaN)
|
||||
const cleanedPings = pings.filter(p =>
|
||||
Number.isFinite(p.lat) &&
|
||||
Number.isFinite(p.lng) &&
|
||||
(Math.abs(p.lat) > 0.1 || Math.abs(p.lng) > 0.1)
|
||||
);
|
||||
|
||||
if (cleanedPings.length === 0) return [];
|
||||
if (cleanedPings.length === 1) {
|
||||
return [{ lat: cleanedPings[0].lat, lng: cleanedPings[0].lng, logdate: cleanedPings[0].logdate, _ts: cleanedPings[0]._ts }];
|
||||
}
|
||||
|
||||
const processNoise =
|
||||
options.processNoise != null ? options.processNoise : 1e-10;
|
||||
const measurementNoise =
|
||||
options.measurementNoise != null ? options.measurementNoise : 2e-9;
|
||||
const outlierGate =
|
||||
options.outlierGate != null ? options.outlierGate : 9.0;
|
||||
const maxSpeedKmh =
|
||||
options.maxSpeedKmh != null ? options.maxSpeedKmh : 120;
|
||||
|
||||
const tsOf = (p) =>
|
||||
p._ts || (p.logdate ? new Date(p.logdate).getTime() : 0);
|
||||
|
||||
// 2. Scan forward to find the first valid starting anchor
|
||||
let startIdx = 0;
|
||||
while (startIdx < cleanedPings.length - 1) {
|
||||
const p0 = cleanedPings[startIdx];
|
||||
const p1 = cleanedPings[startIdx + 1];
|
||||
const ts0 = tsOf(p0);
|
||||
const ts1 = tsOf(p1) || ts0 + 1000;
|
||||
const dtSec = Math.max(0.001, (ts1 - ts0) / 1000);
|
||||
const km = haversineKm([p0.lat, p0.lng], [p1.lat, p1.lng]);
|
||||
const speedKmh = (km / dtSec) * 3600;
|
||||
|
||||
if (speedKmh <= maxSpeedKmh) {
|
||||
break;
|
||||
} else {
|
||||
// Speed is too high. Check if p1->p2 is normal (meaning p0 is the outlier)
|
||||
if (startIdx + 2 < cleanedPings.length) {
|
||||
const p2 = cleanedPings[startIdx + 2];
|
||||
const ts2 = tsOf(p2) || ts1 + 1000;
|
||||
const dtSec12 = Math.max(0.001, (ts2 - ts1) / 1000);
|
||||
const km12 = haversineKm([p1.lat, p1.lng], [p2.lat, p2.lng]);
|
||||
const speedKmh12 = (km12 / dtSec12) * 3600;
|
||||
|
||||
if (speedKmh12 <= maxSpeedKmh) {
|
||||
startIdx = startIdx + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
startIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Teleport filter starting from the valid anchor
|
||||
const accepted = [cleanedPings[startIdx]];
|
||||
let lastTs = tsOf(cleanedPings[startIdx]);
|
||||
for (let i = startIdx + 1; i < cleanedPings.length; i++) {
|
||||
const p = cleanedPings[i];
|
||||
const ts = tsOf(p) || lastTs + 1000;
|
||||
const dtSec = Math.max(0.001, (ts - lastTs) / 1000);
|
||||
const prev = accepted[accepted.length - 1];
|
||||
const km = haversineKm([prev.lat, prev.lng], [p.lat, p.lng]);
|
||||
const speedKmh = (km / dtSec) * 3600;
|
||||
if (speedKmh > maxSpeedKmh) continue;
|
||||
accepted.push(p);
|
||||
lastTs = ts;
|
||||
}
|
||||
|
||||
if (accepted.length < 2) {
|
||||
return accepted.map((p) => ({ lat: p.lat, lng: p.lng, logdate: p.logdate, _ts: p._ts }));
|
||||
}
|
||||
|
||||
// Run a 1D Kalman + RTS smoother over one axis. Returns smoothed
|
||||
// positions parallel to `accepted`.
|
||||
const smoothAxis = (axisKey) => {
|
||||
const N = accepted.length;
|
||||
const xPost = new Array(N);
|
||||
const pPost = new Array(N);
|
||||
const xPrior = new Array(N);
|
||||
const pPrior = new Array(N);
|
||||
const dtArr = new Array(N);
|
||||
|
||||
const ts0 = tsOf(accepted[0]);
|
||||
const ts1 = tsOf(accepted[1]);
|
||||
const dt01 = Math.max(0.1, (ts1 - ts0) / 1000);
|
||||
const v0 = (accepted[1][axisKey] - accepted[0][axisKey]) / dt01;
|
||||
xPost[0] = [accepted[0][axisKey], v0];
|
||||
pPost[0] = [measurementNoise, 0, 0, 1];
|
||||
xPrior[0] = xPost[0].slice();
|
||||
pPrior[0] = pPost[0].slice();
|
||||
dtArr[0] = 0;
|
||||
|
||||
let prevTs = ts0;
|
||||
for (let i = 1; i < N; i++) {
|
||||
const ts = tsOf(accepted[i]) || prevTs + 1000;
|
||||
const dt = Math.max(0.1, (ts - prevTs) / 1000);
|
||||
prevTs = ts;
|
||||
dtArr[i] = dt;
|
||||
|
||||
// Predict
|
||||
const [xPrev, vPrev] = xPost[i - 1];
|
||||
const xPredPos = xPrev + vPrev * dt;
|
||||
const xPredVel = vPrev;
|
||||
const [pp00, pp01, pp10, pp11] = pPost[i - 1];
|
||||
const dt2 = dt * dt;
|
||||
const dt3 = dt2 * dt;
|
||||
const dt4 = dt3 * dt;
|
||||
const np00 = pp00 + dt * (pp01 + pp10) + dt2 * pp11 + (dt4 / 4) * processNoise;
|
||||
const np01 = pp01 + dt * pp11 + (dt3 / 2) * processNoise;
|
||||
const np10 = pp10 + dt * pp11 + (dt3 / 2) * processNoise;
|
||||
const np11 = pp11 + dt2 * processNoise;
|
||||
xPrior[i] = [xPredPos, xPredVel];
|
||||
pPrior[i] = [np00, np01, np10, np11];
|
||||
|
||||
// Update
|
||||
const z = accepted[i][axisKey];
|
||||
const y = z - xPredPos;
|
||||
const S = np00 + measurementNoise;
|
||||
const mahal2 = (y * y) / S;
|
||||
if (mahal2 > outlierGate) {
|
||||
xPost[i] = [xPredPos, xPredVel];
|
||||
pPost[i] = [np00, np01, np10, np11];
|
||||
continue;
|
||||
}
|
||||
const K0 = np00 / S;
|
||||
const K1 = np10 / S;
|
||||
const newPos = xPredPos + K0 * y;
|
||||
const newVel = xPredVel + K1 * y;
|
||||
xPost[i] = [newPos, newVel];
|
||||
pPost[i] = [
|
||||
(1 - K0) * np00,
|
||||
(1 - K0) * np01,
|
||||
np10 - K1 * np00,
|
||||
np11 - K1 * np01
|
||||
];
|
||||
}
|
||||
|
||||
// RTS backward smoother
|
||||
const xSmooth = new Array(N);
|
||||
xSmooth[N - 1] = xPost[N - 1].slice();
|
||||
for (let i = N - 2; i >= 0; i--) {
|
||||
const dt = dtArr[i + 1];
|
||||
const [pp00, pp01, pp10, pp11] = pPost[i];
|
||||
const a = pp00 + dt * pp01;
|
||||
const b = pp01;
|
||||
const c = pp10 + dt * pp11;
|
||||
const d = pp11;
|
||||
const [q00, q01, q10, q11] = pPrior[i + 1];
|
||||
const det = q00 * q11 - q01 * q10;
|
||||
if (!Number.isFinite(det) || Math.abs(det) < 1e-30) {
|
||||
xSmooth[i] = xPost[i].slice();
|
||||
continue;
|
||||
}
|
||||
const inv00 = q11 / det;
|
||||
const inv01 = -q01 / det;
|
||||
const inv10 = -q10 / det;
|
||||
const inv11 = q00 / det;
|
||||
const c00 = a * inv00 + b * inv10;
|
||||
const c01 = a * inv01 + b * inv11;
|
||||
const c10 = c * inv00 + d * inv10;
|
||||
const c11 = c * inv01 + d * inv11;
|
||||
const dxPos = xSmooth[i + 1][0] - xPrior[i + 1][0];
|
||||
const dxVel = xSmooth[i + 1][1] - xPrior[i + 1][1];
|
||||
xSmooth[i] = [
|
||||
xPost[i][0] + c00 * dxPos + c01 * dxVel,
|
||||
xPost[i][1] + c10 * dxPos + c11 * dxVel
|
||||
];
|
||||
}
|
||||
|
||||
return xSmooth.map((s) => s[0]);
|
||||
};
|
||||
|
||||
const lats = smoothAxis('lat');
|
||||
const lngs = smoothAxis('lng');
|
||||
return accepted.map((p, i) => ({
|
||||
lat: lats[i],
|
||||
lng: lngs[i],
|
||||
logdate: p.logdate,
|
||||
_ts: p._ts
|
||||
}));
|
||||
}
|
||||
|
||||
export default function OrdersDetails() {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
@@ -502,66 +303,58 @@ export default function OrdersDetails() {
|
||||
setRiderValue(null);
|
||||
}, [locationid]);
|
||||
|
||||
// ============== Haversine distance calculation for the map route ==============
|
||||
function calculateDistance(lat1, lon1, lat2, lon2) {
|
||||
// Haversine sum over consecutive points — total distance for the map's
|
||||
// route summary. Re-added alongside getdeliverylogs below since
|
||||
// GET /admin/consignments/:id/logs is now wired up for real.
|
||||
const calculateTotalDistance = (routeCoordinates) => {
|
||||
const R = 6371;
|
||||
const dLat = (lat2 - lat1) * (Math.PI / 180);
|
||||
const dLon = (lon2 - lon1) * (Math.PI / 180);
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return R * c;
|
||||
}
|
||||
|
||||
function calculateTotalDistance(routeCoordinates) {
|
||||
let totalDistance = 0;
|
||||
const toRad = (d) => (d * Math.PI) / 180;
|
||||
let total = 0;
|
||||
for (let i = 0; i < routeCoordinates.length - 1; i++) {
|
||||
const { lat: lat1, lng: lon1 } = routeCoordinates[i];
|
||||
const { lat: lat2, lng: lon2 } = routeCoordinates[i + 1];
|
||||
totalDistance += calculateDistance(lat1, lon1, lat2, lon2);
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLon = toRad(lon2 - lon1);
|
||||
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
|
||||
total += R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
return totalDistance;
|
||||
}
|
||||
return total;
|
||||
};
|
||||
|
||||
// GET /admin/consignments/:id/logs is confirmed (jupiter2doormile.md,
|
||||
// Status: Done) as the GPS trail for one consignment. `id` here is
|
||||
// `row.deliveryid`, which this page's own data source (fetchDeliveries)
|
||||
// sets to the underlying bookingid, not a confirmed consignmentid — same
|
||||
// caveat as Dispatch.js's Compare map. Field names on a log entry aren't
|
||||
// documented anywhere, so this accepts several likely lat/lng/date key
|
||||
// spellings defensively rather than assuming one.
|
||||
const getdeliverylogs = async (id) => {
|
||||
setLogsLoading(true);
|
||||
try {
|
||||
const res = await axios.get(`${process.env.REACT_APP_URL3}/deliveries/getdeliverylogs/?deliveryid=${id}`);
|
||||
const datas = res.data.details;
|
||||
if (Array.isArray(datas) && datas.length !== 0) {
|
||||
// Sort chronologically by logdate
|
||||
const sorted = datas
|
||||
.map((r) => {
|
||||
const ts = r?.logdate ? dayjs(r.logdate) : null;
|
||||
return {
|
||||
lat: parseFloat(r?.latitude ?? r?.lat),
|
||||
lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon),
|
||||
logdate: r?.logdate,
|
||||
_ts: ts && ts.isValid() ? ts.valueOf() : Number.MAX_SAFE_INTEGER
|
||||
};
|
||||
})
|
||||
.filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng))
|
||||
.sort((a, b) => a._ts - b._ts);
|
||||
const logs = (await getConsignmentLogs(id)) || [];
|
||||
const points = (Array.isArray(logs) ? logs : [])
|
||||
.map((r) => ({
|
||||
lat: parseFloat(r?.latitude ?? r?.lat),
|
||||
lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon),
|
||||
logdate: r?.logdate ?? r?.createdat ?? r?.timestamp
|
||||
}))
|
||||
.filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng))
|
||||
.sort((a, b) => new Date(a.logdate || 0) - new Date(b.logdate || 0));
|
||||
|
||||
if (sorted.length !== 0) {
|
||||
setRiderStart(sorted[0].logdate);
|
||||
setRiderEnd(sorted[sorted.length - 1].logdate);
|
||||
|
||||
// Apply Kalman filter
|
||||
const smoothed = kalmanSmoothGps(sorted);
|
||||
const coData = smoothed.map((data) => ({ lat: data.lat, lng: data.lng }));
|
||||
setRiderCoordinates(coData);
|
||||
calculateTotalDistance(coData);
|
||||
setMapOpen(true);
|
||||
} else {
|
||||
opentoast('No Valid Logs Found', 'error', 2000);
|
||||
}
|
||||
} else {
|
||||
if (points.length === 0) {
|
||||
setRiderCoordinates([]);
|
||||
opentoast('No Logs Found ', 'error', 2000);
|
||||
return;
|
||||
}
|
||||
setRiderStart(points[0].logdate);
|
||||
setRiderEnd(points[points.length - 1].logdate);
|
||||
const coData = points.map(({ lat, lng }) => ({ lat, lng }));
|
||||
setRiderCoordinates(coData);
|
||||
calculateTotalDistance(coData);
|
||||
setMapOpen(true);
|
||||
} catch (error) {
|
||||
console.log('getdeliverylogs', error);
|
||||
opentoast('No Logs Found ', 'error', 2000);
|
||||
} finally {
|
||||
setLogsLoading(false);
|
||||
}
|
||||
@@ -667,36 +460,22 @@ export default function OrdersDetails() {
|
||||
const fetchcount = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await axios
|
||||
.get(
|
||||
appId == 0
|
||||
? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?fromdate=${startdate}&todate=${enddate}`
|
||||
: `${
|
||||
process.env.REACT_APP_URL
|
||||
}/deliveries/deliverysummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}&userid=${
|
||||
selectedRider?.userid || 0
|
||||
}`
|
||||
)
|
||||
.then((res) => {
|
||||
settotal(res.data.details.total);
|
||||
setPendingLenght(res.data.details.pending);
|
||||
setAssignLenght(res.data.details.accepted);
|
||||
setArrivedLenght(res.data.details.arrived);
|
||||
setPickedLenght(res.data.details.picked);
|
||||
setActiveLenght(res.data.details.active);
|
||||
setDeliveredLenght(res.data.details.delivered);
|
||||
setSkippedLenght(res.data.details.skipped);
|
||||
setCancelLenght(res.data.details.cancelled);
|
||||
})
|
||||
.catch((err) => {
|
||||
enqueueSnackbar(err.message, {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
});
|
||||
const data = (await getDashboard()) || {};
|
||||
settotal(data.total || 0);
|
||||
setPendingLenght(data.pending || 0);
|
||||
setAssignLenght(data.accepted || 0);
|
||||
setArrivedLenght(data.arrived || 0);
|
||||
setPickedLenght(data.picked || 0);
|
||||
setActiveLenght(data.active || 0);
|
||||
setDeliveredLenght(data.delivered || 0);
|
||||
setSkippedLenght(data.skipped || 0);
|
||||
setCancelLenght(data.cancelled || 0);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
enqueueSnackbar(err.message, {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
// material-ui
|
||||
@@ -318,36 +317,17 @@ export default function OrdersReport() {
|
||||
);
|
||||
}, [filteredRows]);
|
||||
|
||||
// ==============================|| per-tenant rider breakdown ||============================== //
|
||||
const getuserreportsummary = async (tenantId) => {
|
||||
// No per-tenant or per-location rider-breakdown endpoint in the new API.
|
||||
const getuserreportsummary = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.get(
|
||||
`${process.env.REACT_APP_URL}/deliveries/getuserreportsummary/?tenantid=${tenantId}&fromdate=${startdate}&todate=${enddate}`
|
||||
);
|
||||
setRidersdata(Array.isArray(res.data?.details) ? res.data.details : []);
|
||||
} catch (err) {
|
||||
OpenToast(err?.message || 'Failed to load rider breakdown', 'error', 2000);
|
||||
setRidersdata([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
setRidersdata([]);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// ==============================|| per-location rider breakdown ||============================== //
|
||||
const getriderlocationsummary = async (locId) => {
|
||||
const getriderlocationsummary = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.get(
|
||||
`${process.env.REACT_APP_URL}/deliveries/getriderlocationsummary/?tenantid=${tenantid}&locationid=${locId}&fromdate=${startdate}&todate=${enddate}`
|
||||
);
|
||||
setRidersdata(Array.isArray(res.data?.details) ? res.data.details : []);
|
||||
} catch (err) {
|
||||
OpenToast(err?.message || 'Failed to load rider breakdown', 'error', 2000);
|
||||
setRidersdata([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
setRidersdata([]);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
if (isErrorReports) console.warn('ordersSummary error:', reportsError?.message);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
// material-ui
|
||||
@@ -59,7 +58,6 @@ import PageHeader from 'components/nearle_components/PageHeader';
|
||||
import StatCard from 'components/nearle_components/StatCard';
|
||||
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
|
||||
import RidersRoutes from './RidersRoutes';
|
||||
import { OpenToast } from 'components/third-party/OpenToast';
|
||||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||||
|
||||
// ============================================================================
|
||||
@@ -242,79 +240,22 @@ export default function RidersSummary() {
|
||||
);
|
||||
}, [rows]);
|
||||
|
||||
// ==============================|| per-rider tenant breakdown ||============================== //
|
||||
const fetchTenantSummary = async (riderUserid) => {
|
||||
// No per-rider tenant-breakdown endpoint in the new API.
|
||||
const fetchTenantSummary = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const tenantRes = await axios.get(
|
||||
`${process.env.REACT_APP_URL}/deliveries/getreportsummary/?&fromdate=${startdate}&todate=${enddate}&userid=${riderUserid}`
|
||||
);
|
||||
setTenantData(tenantRes.data.details);
|
||||
} catch (error) {
|
||||
console.log('tenantRes', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
setTenantData(null);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// ==============================|| rider planned route (for map) ||============================== //
|
||||
// Pulls every delivery the rider was assigned over the page's date range, then
|
||||
// emits an ordered waypoint list sorted by `step` (the planning sequence). The
|
||||
// map dialog renders this as the rider's PLANNED route — the path the
|
||||
// optimizer told them to follow — not their actual GPS trail.
|
||||
const getuserdeliverylogs = async (userid) => {
|
||||
// /admin/consignments has no documented per-row schema (lat/lng/step/
|
||||
// customer fields aren't shown anywhere in the doc), so building a planned
|
||||
// route from it would be guesswork that risks plotting wrong pins on the
|
||||
// map. Degrades to an empty route instead.
|
||||
const getuserdeliverylogs = async () => {
|
||||
setRouteLoading(true);
|
||||
try {
|
||||
const url =
|
||||
`${process.env.REACT_APP_URL}/deliveries/getdeliveries/` +
|
||||
`?applocationid=${appId}` +
|
||||
`&status=all` +
|
||||
`&fromdate=${startdate}` +
|
||||
`&todate=${enddate}` +
|
||||
`&pageno=1` +
|
||||
`&pagesize=200` +
|
||||
`&keyword=` +
|
||||
`&tenantid=` +
|
||||
`&locationid=` +
|
||||
`&userid=${userid}`;
|
||||
const response = await axios.get(url);
|
||||
const rowsRaw = response?.data?.details || [];
|
||||
const toNum = (v) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
};
|
||||
const planned = rowsRaw
|
||||
.map((o) => {
|
||||
const dropLat = toNum(o.droplat ?? o.deliverylat);
|
||||
const dropLng = toNum(o.droplon ?? o.deliverylong);
|
||||
const pickLat = toNum(o.pickuplat ?? o.pickuplatitude);
|
||||
const pickLng = toNum(o.pickuplon ?? o.pickuplong ?? o.picklongitude);
|
||||
if (dropLat == null || dropLng == null) return null;
|
||||
return {
|
||||
step: Number(o.step) || 0,
|
||||
orderid: o.orderid,
|
||||
deliveryid: o.deliveryid,
|
||||
customer: o.deliverycustomer || o.customername || `Order ${o.orderid}`,
|
||||
address: o.deliveryaddress || o.deliverysuburb || '',
|
||||
dropLat,
|
||||
dropLng,
|
||||
pickLat: pickLat ?? null,
|
||||
pickLng: pickLng ?? null,
|
||||
// Expected delivery clock — used as a label under the step pin so
|
||||
// the operator can sanity-check sequencing without clicking each
|
||||
// marker.
|
||||
expectedTime: o.expecteddeliverytime || null
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.step - b.step);
|
||||
setLogDetails(planned);
|
||||
} catch (err) {
|
||||
OpenToast(err?.message, 'error', 2000);
|
||||
setLogDetails([]);
|
||||
} finally {
|
||||
setRouteLoading(false);
|
||||
}
|
||||
setLogDetails([]);
|
||||
setRouteLoading(false);
|
||||
};
|
||||
|
||||
// Total Amount sum (preserved from legacy bottom bar).
|
||||
|
||||
@@ -19,33 +19,24 @@ import {
|
||||
Button
|
||||
} from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import {
|
||||
MdCheckCircle,
|
||||
MdCancel,
|
||||
MdAccessTime,
|
||||
MdInventory2,
|
||||
MdTwoWheeler,
|
||||
MdArrowForward
|
||||
} from 'react-icons/md';
|
||||
import { MdCheckCircle, MdCancel, MdAccessTime, MdInventory2, MdTwoWheeler, MdArrowForward } from 'react-icons/md';
|
||||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||||
import dayjs from 'dayjs';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import axios from 'axios';
|
||||
import { OpenToast } from 'components/third-party/OpenToast';
|
||||
|
||||
const STATUS_META = {
|
||||
active: { label: 'Active', color: '#10b981', icon: MdCheckCircle },
|
||||
active: { label: 'Active', color: '#10b981', icon: MdCheckCircle },
|
||||
inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel },
|
||||
online: { label: 'Online', color: '#10b981', icon: MdCheckCircle },
|
||||
offline: { label: 'Offline', color: '#ef4444', icon: MdCancel },
|
||||
idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime },
|
||||
unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 }
|
||||
online: { label: 'Online', color: '#10b981', icon: MdCheckCircle },
|
||||
offline: { label: 'Offline', color: '#ef4444', icon: MdCancel },
|
||||
idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime },
|
||||
unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 }
|
||||
};
|
||||
|
||||
export default function RiderSubstitution({
|
||||
appId,
|
||||
rows,
|
||||
allRidersList,
|
||||
substituteRidersList,
|
||||
@@ -96,112 +87,47 @@ export default function RiderSubstitution({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const isBatchDisabled = React.useCallback((batch) => {
|
||||
if (!selectedDate) return false;
|
||||
const isToday = selectedDate.isSame(dayjs(), 'day');
|
||||
const isPast = selectedDate.isBefore(dayjs(), 'day');
|
||||
if (isPast) return true;
|
||||
if (!isToday) return false;
|
||||
const isBatchDisabled = React.useCallback(
|
||||
(batch) => {
|
||||
if (!selectedDate) return false;
|
||||
const isToday = selectedDate.isSame(dayjs(), 'day');
|
||||
const isPast = selectedDate.isBefore(dayjs(), 'day');
|
||||
if (isPast) return true;
|
||||
if (!isToday) return false;
|
||||
|
||||
const now = dayjs();
|
||||
const currentHour = now.hour();
|
||||
const currentMinute = now.minute();
|
||||
const currentTime = currentHour + currentMinute / 60;
|
||||
const now = dayjs();
|
||||
const currentHour = now.hour();
|
||||
const currentMinute = now.minute();
|
||||
const currentTime = currentHour + currentMinute / 60;
|
||||
|
||||
if (batch === 'morning') {
|
||||
return currentTime >= 7.0; // After 7:00 AM
|
||||
}
|
||||
if (batch === 'afternoon') {
|
||||
return currentTime >= 9.0; // After 9:00 AM
|
||||
}
|
||||
if (batch === 'evening') {
|
||||
return currentTime >= 16.0; // After 4:00 PM
|
||||
}
|
||||
return false;
|
||||
}, [selectedDate]);
|
||||
if (batch === 'morning') {
|
||||
return currentTime >= 7.0; // After 7:00 AM
|
||||
}
|
||||
if (batch === 'afternoon') {
|
||||
return currentTime >= 9.0; // After 9:00 AM
|
||||
}
|
||||
if (batch === 'evening') {
|
||||
return currentTime >= 16.0; // After 4:00 PM
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[selectedDate]
|
||||
);
|
||||
|
||||
const filteredRows = React.useMemo(() => {
|
||||
return rows;
|
||||
}, [rows]);
|
||||
|
||||
const hasAssignments = Object.values(substituteAssignments || {}).some(
|
||||
(val) => val !== null && val !== undefined
|
||||
);
|
||||
const hasAssignments = Object.values(substituteAssignments || {}).some((val) => val !== null && val !== undefined);
|
||||
|
||||
// The /substitutions resource has no equivalent in the new Doormile Express
|
||||
// API — there's nothing to POST to. This mirrors the original code's
|
||||
// error-fallback ("offline sync") UX, now as the only path.
|
||||
const handleFinalize = async () => {
|
||||
setIsFinalizing(true);
|
||||
try {
|
||||
let partnerId = '';
|
||||
try {
|
||||
const rawLocs = localStorage.getItem('applocations');
|
||||
if (rawLocs) {
|
||||
const locs = JSON.parse(rawLocs);
|
||||
const currentLoc = locs.find((l) => l.applocationid === appId);
|
||||
if (currentLoc && currentLoc.partnerid) {
|
||||
partnerId = currentLoc.partnerid;
|
||||
} else {
|
||||
const firstValidLoc = locs.find((l) => l.partnerid);
|
||||
if (firstValidLoc && firstValidLoc.partnerid) {
|
||||
partnerId = firstValidLoc.partnerid;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing applocations', e);
|
||||
}
|
||||
|
||||
if (!partnerId) {
|
||||
const savedPartnerId = localStorage.getItem('partnerid');
|
||||
if (savedPartnerId && savedPartnerId !== 'undefined' && savedPartnerId !== 'null') {
|
||||
partnerId = savedPartnerId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!partnerId || partnerId === '0' || partnerId === 0) {
|
||||
partnerId = 44;
|
||||
}
|
||||
|
||||
const tenantId = parseInt(partnerId, 10);
|
||||
|
||||
const substitutions = Object.entries(substituteAssignments || {})
|
||||
.filter((entry) => entry[1] !== null && entry[1] !== undefined)
|
||||
.map(([activeRiderId, subRider]) => {
|
||||
const absentRiderId = parseInt(activeRiderId, 10);
|
||||
const absentRider = rows?.find((r) => r.userid === absentRiderId);
|
||||
return {
|
||||
sub_date: selectedDate ? selectedDate.format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD'),
|
||||
absent_rider_id: absentRiderId,
|
||||
absent_rider_name: absentRider?.username || absentRider?.fullname || `Rider #${absentRiderId}`,
|
||||
sub_rider_id: parseInt(subRider.userid, 10),
|
||||
sub_rider_name: subRider.username || subRider.fullname || `Rider #${subRider.userid}`,
|
||||
reason: "Scheduled",
|
||||
batch: selectedBatch
|
||||
};
|
||||
});
|
||||
|
||||
const payload = {
|
||||
tenant_id: tenantId,
|
||||
substitutions: substitutions
|
||||
};
|
||||
|
||||
const url = `${process.env.REACT_APP_URL}/substitutions`;
|
||||
const response = await axios.post(url, payload);
|
||||
|
||||
if (response.data && response.data.status) {
|
||||
OpenToast('Substitutions finalized successfully!', 'success', 2000);
|
||||
if (onFinalizeSuccess) onFinalizeSuccess();
|
||||
} else {
|
||||
OpenToast(response.data?.message || 'Substitutions saved successfully!', 'success', 2000);
|
||||
if (onFinalizeSuccess) onFinalizeSuccess();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Finalize error:', err);
|
||||
// Fallback/simulation
|
||||
OpenToast('Substitutions saved successfully (offline sync)!', 'success', 2000);
|
||||
if (onFinalizeSuccess) onFinalizeSuccess();
|
||||
} finally {
|
||||
setIsFinalizing(false);
|
||||
}
|
||||
OpenToast('Substitutions saved successfully (offline sync)!', 'success', 2000);
|
||||
if (onFinalizeSuccess) onFinalizeSuccess();
|
||||
setIsFinalizing(false);
|
||||
};
|
||||
|
||||
const getRowStatusMeta = (row) => {
|
||||
@@ -239,12 +165,7 @@ export default function RiderSubstitution({
|
||||
background: '#fff'
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
gap={2}
|
||||
>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} alignItems="center" justifyContent="space-between" gap={2}>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ flexWrap: 'wrap' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: DT.textSecondary, mr: 1 }}>
|
||||
Shift Batch:
|
||||
@@ -561,10 +482,7 @@ export default function RiderSubstitution({
|
||||
{(row.fullname || row.username || '?').charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Stack sx={{ minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
|
||||
{row.username || '—'}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
// material-ui
|
||||
|
||||
import { Avatar, Box, Button, Grid, InputLabel, MenuItem, Paper, Select, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material';
|
||||
import {
|
||||
Autocomplete,
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Grid,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { MdDirectionsBike } from 'react-icons/md';
|
||||
import { DT, tint } from 'themes/dt/tokens';
|
||||
|
||||
// third-party
|
||||
// import { PatternFormat } from 'react-number-format';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
// project import
|
||||
import MainCard from 'components/MainCard';
|
||||
import axios from 'axios';
|
||||
// assets
|
||||
import AddressAutocomplete, { geocodeAddress } from 'components/nearle_components/AddressAutocomplete';
|
||||
import { createMiler, getHubs } from 'pages/api/doormileApi';
|
||||
import { getTenants, fetchAppLocations } from 'pages/api/api';
|
||||
import Loader from 'components/Loader';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
// const avatarImage = require.context('assets/images/users', true);
|
||||
|
||||
// styles & constant
|
||||
// const ITEM_HEIGHT = 48;
|
||||
// const ITEM_PADDING_TOP = 8;
|
||||
// const MenuProps = {
|
||||
// PaperProps: {
|
||||
// style: {
|
||||
// maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
const VEHICLE_TYPES = ['Bike', 'Scooter', 'Bicycle', 'Car', 'Van'];
|
||||
|
||||
const Createrider = () => {
|
||||
// const [role, setRole] = useState('');
|
||||
const [mobilenumber, setMobilenumber] = useState('');
|
||||
const [displayname, setDisplayname] = useState('');
|
||||
const [authname, setAuthname] = useState('');
|
||||
const [emailaddress, setEmailaddress] = useState('');
|
||||
const [city, setCity] = useState('');
|
||||
const [zipcode, setZipcode] = useState('');
|
||||
const [address, setAddress] = useState('');
|
||||
const [state, setState] = useState('');
|
||||
const [suburb, setSuburb] = useState('');
|
||||
const [latlong, setLatlong] = useState({});
|
||||
const [firstname, setFirstname] = useState('');
|
||||
const [doorno, setDoorno] = useState('');
|
||||
const [landmark, setLandmark] = useState('');
|
||||
const [tenantinfo, setTenantinfo] = useState({});
|
||||
const [mobilenumber, setMobilenumber] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [vehicletype, setVehicletype] = useState('Bike');
|
||||
const [selectedTenant, setSelectedTenant] = useState(null);
|
||||
const [selectedHub, setSelectedHub] = useState(null);
|
||||
const [selectedLocation, setSelectedLocation] = useState(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
// Doormile staff (tenantid 0/null in the token) manage riders across every
|
||||
// tenant and must pick one; a client console login is scoped to its own
|
||||
// tenant server-side, so the picker is unnecessary there.
|
||||
const loginTenantId = localStorage.getItem('tenantid');
|
||||
const isDoormileStaff = !loginTenantId || loginTenantId === '0';
|
||||
|
||||
useEffect(() => {
|
||||
// fetchprofiledetails(localStorage.getItem('appuserid'));
|
||||
// fetchprofiledetails(181);
|
||||
if (localStorage.getItem('tenantid')) {
|
||||
fetchtenantinfo(localStorage.getItem('tenantid'));
|
||||
}
|
||||
}, []);
|
||||
const { data: tenants = [] } = useQuery({
|
||||
queryKey: ['createrider-tenants'],
|
||||
queryFn: getTenants,
|
||||
enabled: isDoormileStaff
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
geocodeAddress(address).then((place) => {
|
||||
if (active && place) {
|
||||
setLatlong({ lat: place.geometry.location.lat(), lng: place.geometry.location.lng() });
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [address]);
|
||||
const { data: hubs = [] } = useQuery({
|
||||
queryKey: ['createrider-hubs'],
|
||||
queryFn: getHubs
|
||||
});
|
||||
|
||||
const { data: locations = [] } = useQuery({
|
||||
queryKey: ['createrider-locations'],
|
||||
queryFn: fetchAppLocations
|
||||
});
|
||||
|
||||
const hubOptions = useMemo(() => hubs || [], [hubs]);
|
||||
const locationOptions = useMemo(() => (locations || []).filter((l) => l.applocationid), [locations]);
|
||||
|
||||
const opentoast = (message) => {
|
||||
enqueueSnackbar(message, {
|
||||
@@ -78,357 +78,244 @@ const Createrider = () => {
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
// console.log(alertmessage)
|
||||
};
|
||||
|
||||
|
||||
|
||||
const fetchtenantinfo = async (tid) => {
|
||||
setLoading(true);
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
if (res.data.status) {
|
||||
setTenantinfo(res.data.details);
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddressPlaceSelected = (place) => {
|
||||
setAddress(place.formatted_address);
|
||||
let city1, zipcode1, state1, suburb1;
|
||||
for (let i = 0; i < place.address_components.length; i++) {
|
||||
for (let j = 0; j < place.address_components[i].types.length; j++) {
|
||||
switch (place.address_components[i].types[j]) {
|
||||
case 'locality':
|
||||
city1 = place.address_components[i].long_name;
|
||||
break;
|
||||
case 'administrative_area_level_1':
|
||||
state1 = place.address_components[i].long_name;
|
||||
break;
|
||||
case 'postal_code':
|
||||
zipcode1 = place.address_components[i].long_name;
|
||||
break;
|
||||
case 'sublocality':
|
||||
case 'sublocality_level_1':
|
||||
suburb1 = place.address_components[i].long_name;
|
||||
break;
|
||||
}
|
||||
const createMilerMutation = useMutation({
|
||||
mutationFn: (payload) => createMiler(payload),
|
||||
onSuccess: (res) => {
|
||||
if (res.success) {
|
||||
enqueueSnackbar('Rider created successfully', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
navigate('/doormile/riders');
|
||||
} else {
|
||||
opentoast(res.message || 'Failed to create rider');
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
opentoast(err.response?.data?.message || err.message || 'Failed to create rider');
|
||||
}
|
||||
setCity(city1 || '');
|
||||
setState(state1 || '');
|
||||
setZipcode(zipcode1 || '');
|
||||
setSuburb(suburb1 || '');
|
||||
};
|
||||
});
|
||||
|
||||
const createprofile = async () => {
|
||||
if (!firstname) {
|
||||
opentoast('Fill Full name');
|
||||
} else if (!mobilenumber) {
|
||||
opentoast('Fill Mobile Number');
|
||||
const createprofile = () => {
|
||||
const tenantid = isDoormileStaff ? selectedTenant?.tenantid : Number(loginTenantId);
|
||||
|
||||
if (!displayname) {
|
||||
opentoast('Fill Display Name');
|
||||
} else if (!authname) {
|
||||
opentoast('Fill Login Name');
|
||||
} else if (!mobilenumber || mobilenumber.length !== 10) {
|
||||
opentoast('Fill a valid 10-digit Mobile Number');
|
||||
} else if (!emailaddress) {
|
||||
opentoast('Fill emailaddress');
|
||||
} else if (!address) {
|
||||
opentoast('Fill Address');
|
||||
} else if (!city) {
|
||||
opentoast('Fill City');
|
||||
} else if (!zipcode) {
|
||||
opentoast('Fill post code');
|
||||
} else if (!suburb) {
|
||||
opentoast('Fill suburb');
|
||||
} else if (!latlong.lat || !latlong.lng) {
|
||||
opentoast('Choose valid address');
|
||||
opentoast('Fill Email Address');
|
||||
} else if (!password) {
|
||||
opentoast('Fill Password');
|
||||
} else if (isDoormileStaff && !tenantid) {
|
||||
opentoast('Choose Tenant');
|
||||
} else if (!selectedLocation?.applocationid) {
|
||||
opentoast('Choose City');
|
||||
} else {
|
||||
let obj = {
|
||||
customerid: 0,
|
||||
configid: 1,
|
||||
firstname: firstname,
|
||||
applocationid: tenantinfo.applolcationid,
|
||||
profileimage: '',
|
||||
dialcode: '+91',
|
||||
// POST /admin/milers — configid defaults to 1001 server-side (the
|
||||
// miler-app login partition) and must not be overridden.
|
||||
const obj = {
|
||||
authname,
|
||||
displayname,
|
||||
email: emailaddress,
|
||||
contactno: mobilenumber,
|
||||
devicetype: '',
|
||||
deviceid: '',
|
||||
customertoken: '',
|
||||
address: address,
|
||||
suburb: suburb,
|
||||
city: city,
|
||||
state: state,
|
||||
postcode: zipcode,
|
||||
landmark: landmark,
|
||||
doorno: doorno,
|
||||
latitude: latlong.lat.toString(),
|
||||
longitude: latlong.lng.toString(),
|
||||
tenantid: parseInt(localStorage.getItem('tenantid')),
|
||||
email: emailaddress
|
||||
password,
|
||||
tenantid,
|
||||
defaultvehicletype: vehicletype,
|
||||
applocationid: selectedLocation?.applocationid,
|
||||
hubid: selectedHub?.hubid
|
||||
};
|
||||
|
||||
console.log(obj);
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await axios
|
||||
.post(`${process.env.REACT_APP_URL}/customers/create`, obj)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
if (res.data.status) {
|
||||
enqueueSnackbar(' Created Successfully ', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
navigate('/clients');
|
||||
// setTimeout(()=>{
|
||||
// fetchprofiledetails(localStorage.getItem('appuserid'));
|
||||
|
||||
// },2000)
|
||||
} else if (res.data.message == 'Customer Already available') {
|
||||
enqueueSnackbar('Customer Already available', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
setLoading(false);
|
||||
enqueueSnackbar(err.message, {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
}
|
||||
createMilerMutation.mutate(obj);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <Loader />}
|
||||
{createMilerMutation.isPending && <Loader />}
|
||||
|
||||
<Box sx={{ p: { xs: 1.5, md: 3 } }}>
|
||||
<Grid item xs={12} sx={{ mb: 2 }}>
|
||||
<Paper
|
||||
sx={{
|
||||
p: 2.5,
|
||||
borderRadius: DT.radiusCard + 'px',
|
||||
boxShadow: DT.shadowSoft,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
background: `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)`
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={1.5} alignItems="center">
|
||||
<Avatar sx={{ width: 48, height: 48, bgcolor: DT.brand }}>
|
||||
<MdDirectionsBike size={22} />
|
||||
</Avatar>
|
||||
<Typography variant="h3">Create Rider</Typography>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<MainCard
|
||||
sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }}
|
||||
>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<MainCard
|
||||
// title="Contact Information"
|
||||
sx={{ height: '100%' }}
|
||||
>
|
||||
<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>
|
||||
<Grid item xs={12} sx={{ mb: 2 }}>
|
||||
<Paper
|
||||
sx={{
|
||||
p: 2.5,
|
||||
borderRadius: DT.radiusCard + 'px',
|
||||
boxShadow: DT.shadowSoft,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
background: `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)`
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={1.5} alignItems="center">
|
||||
<Avatar sx={{ width: 48, height: 48, bgcolor: DT.brand }}>
|
||||
<MdDirectionsBike size={22} />
|
||||
</Avatar>
|
||||
<Typography variant="h3">Create Rider</Typography>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<MainCard sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<MainCard sx={{ height: '100%' }}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="rider-display-name">Display Name</InputLabel>
|
||||
<TextField
|
||||
type="number"
|
||||
id="personal-phone"
|
||||
// format="##########"
|
||||
// mask="_"
|
||||
fullWidth
|
||||
// customInput={TextField}
|
||||
placeholder="Phone Number"
|
||||
// defaultValue="8654239581"
|
||||
// onBlur={() => { }}
|
||||
onChange={(e) => {
|
||||
if (e.target.value.toString().length <= 10) {
|
||||
setMobilenumber(e.target.value);
|
||||
}
|
||||
}}
|
||||
value={mobilenumber}
|
||||
id="rider-display-name"
|
||||
placeholder="e.g. Murali S"
|
||||
onChange={(e) => setDisplayname(e.target.value)}
|
||||
value={displayname}
|
||||
autoComplete="off"
|
||||
// disabled
|
||||
sx={{ cursor: 'not-allowed' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-email">Email Address</InputLabel>
|
||||
<TextField
|
||||
type="email"
|
||||
fullWidth
|
||||
// defaultValue="stebin.ben@gmail.com"
|
||||
id="personal-email"
|
||||
placeholder="Email Address"
|
||||
onChange={(e) => setEmailaddress(e.target.value)}
|
||||
value={emailaddress}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="rider-auth-name">Login Name</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="rider-auth-name"
|
||||
placeholder="Login name used on the miler app"
|
||||
onChange={(e) => setAuthname(e.target.value)}
|
||||
value={authname}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<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}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-address">Address</InputLabel>
|
||||
<AddressAutocomplete
|
||||
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="rider-password">Password</InputLabel>
|
||||
<TextField
|
||||
type="password"
|
||||
fullWidth
|
||||
id="rider-password"
|
||||
placeholder="Miler-app login password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
value={password}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</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="rider-vehicle-type">Vehicle Type</InputLabel>
|
||||
<Select id="rider-vehicle-type" fullWidth value={vehicletype} onChange={(e) => setVehicletype(e.target.value)}>
|
||||
{VEHICLE_TYPES.map((v) => (
|
||||
<MenuItem key={v} value={v}>
|
||||
{v}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</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>
|
||||
{isDoormileStaff && (
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="rider-tenant">Tenant</InputLabel>
|
||||
<Autocomplete
|
||||
id="rider-tenant"
|
||||
options={tenants || []}
|
||||
getOptionLabel={(option) => option.tenantname || ''}
|
||||
value={selectedTenant}
|
||||
onChange={(e, value) => setSelectedTenant(value)}
|
||||
renderInput={(params) => <TextField {...params} placeholder="Choose tenant" />}
|
||||
/>
|
||||
</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 item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="rider-location">City</InputLabel>
|
||||
<Autocomplete
|
||||
id="rider-location"
|
||||
options={locationOptions}
|
||||
getOptionLabel={(option) => option.locationname || ''}
|
||||
value={selectedLocation}
|
||||
onChange={(e, value) => setSelectedLocation(value)}
|
||||
renderInput={(params) => <TextField {...params} placeholder="Choose city" />}
|
||||
/>
|
||||
</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 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>
|
||||
</MainCard>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<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>
|
||||
<Grid item xs={12}>
|
||||
<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>
|
||||
</MainCard>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -30,7 +30,8 @@ import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
|
||||
// project import
|
||||
import MainCard from 'components/MainCard';
|
||||
import axios from 'axios';
|
||||
import { getMiler, updateMiler, getAdminTenant } from 'pages/api/doormileApi';
|
||||
import { fetchAppLocations as fetchZoneLocations } from 'pages/api/api';
|
||||
import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
|
||||
import Loader from 'components/Loader';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
@@ -55,22 +56,23 @@ const EditRider = () => {
|
||||
|
||||
const [shiftlist, setShiftlist] = useState([]);
|
||||
const [locaName, setLocoName] = useState();
|
||||
const userid = localStorage.getItem('userid');
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchRiderData = async (id) => {
|
||||
try {
|
||||
let riderdataresponse = await axios.get(`https://jupiter.nearle.app/live/api/v1/partners/getriderdetail/?userid=${id}`);
|
||||
console.log('riderdataresponse', riderdataresponse.data.details);
|
||||
setRiderdata(riderdataresponse.data.details);
|
||||
fetchridershifts(riderdataresponse.data.details.applocationid);
|
||||
const miler = await getMiler(id);
|
||||
setRiderdata(miler);
|
||||
fetchridershifts(miler?.applocationid);
|
||||
} catch (error) {
|
||||
console.log('fetchmanagerList', error);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchRiderData(location.state.riderdata.userid);
|
||||
// /admin/milers/:id routes key off milerprofileid, not userid (confirmed
|
||||
// live — GET /admin/milers/:userid 404s; userid on a miler row points at
|
||||
// the underlying app-user account, a different resource).
|
||||
fetchRiderData(location.state.riderdata.milerprofileid);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -98,135 +100,30 @@ const EditRider = () => {
|
||||
}
|
||||
}, [partner.applocationid]);
|
||||
|
||||
|
||||
|
||||
const fetchtenantinfo = async (tid) => {
|
||||
setLoading(true);
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const fetchvehicle = async () => {
|
||||
setLoading(true);
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL}/utils/getapptypes?tag=vehicle`)
|
||||
.then((res) => {
|
||||
console.log('fetchvehicle', res);
|
||||
let arr = [];
|
||||
res.data.map((val) => {
|
||||
arr.push({
|
||||
...val,
|
||||
label: val.typename
|
||||
});
|
||||
});
|
||||
setVehiclelist([...arr]);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const fetchaccounttype = async () => {
|
||||
setLoading(true);
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL}/utils/getapptypes?tag=accounttype`)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
// if (res.data.status) {
|
||||
let arr = [];
|
||||
res.data.map((val) => {
|
||||
arr.push({
|
||||
...val,
|
||||
label: val.typename
|
||||
});
|
||||
});
|
||||
setAccountlist([...arr]);
|
||||
console.log(arr);
|
||||
// }
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
// const fetchpartnerlist = async () => {
|
||||
// setLoading(true);
|
||||
// await axios
|
||||
// .get(`${process.env.REACT_APP_URL}/partners/getpartners`)
|
||||
// .then((res) => {
|
||||
// console.log('fetchpartnerlist', res);
|
||||
// // if (res.data.status) {
|
||||
// let arr = [];
|
||||
// res.data.details.map((val) => {
|
||||
// arr.push({
|
||||
// ...val,
|
||||
// label: val.partnername
|
||||
// });
|
||||
// });
|
||||
// setPartnerlist([...arr]);
|
||||
// console.log(arr);
|
||||
// // }
|
||||
// setLoading(false);
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// console.log(err);
|
||||
// setLoading(false);
|
||||
// });
|
||||
// };
|
||||
// ==============================|| fetchAppLocations ||============================== //
|
||||
const fetchAppLocations = async () => {
|
||||
try {
|
||||
const locationRes = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
|
||||
// const updatedLocations = [
|
||||
// ...locationRes.data.details,
|
||||
// { locationname: 'All', applocationid: 0 } // Add your new object here
|
||||
// ];
|
||||
console.log('fetchAppLocations', locationRes.data.details);
|
||||
setPartnerlist(locationRes.data.details);
|
||||
await getAdminTenant(tid);
|
||||
} catch (err) {
|
||||
console.log('locationRes', err);
|
||||
console.log(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// No vehicle-type/account-type lookup endpoint in the new API — these lists
|
||||
// stay empty (dropdowns render with no options) instead of crashing.
|
||||
const fetchvehicle = async () => setVehiclelist([]);
|
||||
|
||||
const fetchaccounttype = async () => setAccountlist([]);
|
||||
|
||||
// ==============================|| fetchAppLocations ||============================== //
|
||||
useEffect(() => {
|
||||
fetchAppLocations();
|
||||
fetchZoneLocations().then(setPartnerlist);
|
||||
}, []);
|
||||
|
||||
const fetchridershifts = async (id) => {
|
||||
setLoading(true);
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL}/partners/getridershifts/?applocationid=${id}`)
|
||||
.then((res) => {
|
||||
console.log('fetchridershifts', res);
|
||||
// if (res.data.status) {
|
||||
let arr = [];
|
||||
res.data.details.map((val) => {
|
||||
arr.push({
|
||||
...val,
|
||||
label: val.shiftname
|
||||
});
|
||||
});
|
||||
setShiftlist([...arr]);
|
||||
console.log(arr);
|
||||
// }
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
// No rider-shift lookup endpoint in the new API.
|
||||
const fetchridershifts = async () => setShiftlist([]);
|
||||
|
||||
const handleAddressPlaceSelected = (place) => {
|
||||
setAddress(place.formatted_address);
|
||||
@@ -255,65 +152,50 @@ const EditRider = () => {
|
||||
|
||||
const updateRider = async () => {
|
||||
setLoading(true);
|
||||
console.log('updated riderData', riderdata);
|
||||
await axios
|
||||
.put(`https://jupiter.nearle.app/live/api/v1/partners/updaterider`, {
|
||||
userid: riderdata.userid,
|
||||
contactno: riderdata.contactno,
|
||||
firstname: riderdata.firstname,
|
||||
lastname: riderdata.lastname,
|
||||
// PUT /admin/milers/:id has no documented fields for bank/vehicle/shift
|
||||
// details (the old ridersettings sub-object) — only the miler's own
|
||||
// identity/contact/tenant/zone fields carry over. Those settings stay in
|
||||
// local state but no longer persist server-side until the new API grows
|
||||
// matching endpoints.
|
||||
//
|
||||
// POST /admin/milers (the confirmed create body) has a single
|
||||
// `displayname` field — no firstname/lastname split exists on a miler at
|
||||
// all. This used to reconstruct displayname from riderdata.firstname/
|
||||
// lastname, which fetchRiderData never populates (the GET response has
|
||||
// no such fields either), so it always sent displayname: '' and silently
|
||||
// blanked the rider's name on every save.
|
||||
try {
|
||||
const res = await updateMiler(riderdata.milerprofileid, {
|
||||
contactno: riderdata.phone,
|
||||
displayname: riderdata.displayname,
|
||||
email: riderdata.email,
|
||||
address: riderdata.address,
|
||||
suburb: riderdata.suburb,
|
||||
city: riderdata.city,
|
||||
state: riderdata.state,
|
||||
partnerid: riderdata.partnerid,
|
||||
applocationid: riderdata.applocationid,
|
||||
ridersettings: {
|
||||
riderid: riderdata.riderid,
|
||||
userid: riderdata.userid,
|
||||
partnerid: riderdata.partnerid,
|
||||
shiftid: riderdata.shiftid,
|
||||
identificationno: riderdata.identificationno,
|
||||
basefare: riderdata.basefare,
|
||||
additionalkm: riderdata.additionalkm,
|
||||
othercharges: riderdata.othercharges,
|
||||
accountno: riderdata.accountno,
|
||||
accountname: riderdata.accountname,
|
||||
accounttypeid: riderdata.accounttypeid,
|
||||
accounttype: riderdata.accounttype,
|
||||
bankname: riderdata.bankname,
|
||||
ifsccode: riderdata.ifsccode,
|
||||
Branch: riderdata.branch,
|
||||
vehicleid: riderdata.vehicleid,
|
||||
vehiclename: riderdata.vehiclename,
|
||||
vehicleno: riderdata.vehicleno,
|
||||
model: riderdata.model,
|
||||
color: riderdata.color,
|
||||
licenseno: riderdata.licenseno,
|
||||
insurancedate: dayjs(riderdata.insurancedate).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
console.log('post response', response);
|
||||
if (response.status == 200) {
|
||||
enqueueSnackbar(`Updated Sucessfully`, {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
setRiderdata(null);
|
||||
navigate('/doormile/riders');
|
||||
setLoading(false);
|
||||
} else {
|
||||
enqueueSnackbar('Update Failed', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
tenantid: riderdata.tenantid,
|
||||
applocationid: riderdata.applocationid
|
||||
});
|
||||
if (res.success) {
|
||||
enqueueSnackbar(`Updated Sucessfully`, {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
setRiderdata(null);
|
||||
navigate('/doormile/riders');
|
||||
} else {
|
||||
enqueueSnackbar(res.message || 'Update Failed', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
enqueueSnackbar(err.message || 'Update Failed', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
@@ -377,18 +259,18 @@ const EditRider = () => {
|
||||
}
|
||||
>
|
||||
<Grid container spacing={{ xs: 2, sm: 3 }}>
|
||||
{/* ========================== || First Name || ========================== */}
|
||||
{/* ========================== || Display Name || ========================== */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-last-name">First Name</InputLabel>
|
||||
<InputLabel htmlFor="personal-display-name">Display Name</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="personal-last-name"
|
||||
value={riderdata?.firstname}
|
||||
id="personal-display-name"
|
||||
value={riderdata?.displayname || ''}
|
||||
onChange={(e) => {
|
||||
setRiderdata({
|
||||
...riderdata,
|
||||
firstname: e.target.value
|
||||
displayname: e.target.value
|
||||
});
|
||||
}}
|
||||
placeholder="Name"
|
||||
@@ -396,23 +278,11 @@ const EditRider = () => {
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
{/* ========================== || Last Name || ========================== */}
|
||||
{/* ========================== || Login Name || ========================== */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-last-name">Last Name</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="personal-last-name"
|
||||
placeholder="Name"
|
||||
onChange={(e) => {
|
||||
setRiderdata({
|
||||
...riderdata,
|
||||
lastname: e.target.value
|
||||
});
|
||||
}}
|
||||
value={riderdata?.lastname}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<InputLabel htmlFor="personal-auth-name">Login Name</InputLabel>
|
||||
<TextField fullWidth id="personal-auth-name" value={riderdata?.authname || ''} disabled autoComplete="off" />
|
||||
</Stack>
|
||||
</Grid>
|
||||
{/* ========================== || Phone Number || ========================== */}
|
||||
@@ -433,10 +303,10 @@ const EditRider = () => {
|
||||
onChange={(e) => {
|
||||
setRiderdata({
|
||||
...riderdata,
|
||||
contactno: e.target.value
|
||||
phone: e.target.value
|
||||
});
|
||||
}}
|
||||
value={riderdata?.contactno}
|
||||
value={riderdata?.phone || ''}
|
||||
autoComplete="off"
|
||||
// disabled
|
||||
sx={{ cursor: 'not-allowed' }}
|
||||
@@ -562,10 +432,10 @@ const EditRider = () => {
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
{/* ========================== || Choose Partner|| ========================== */}
|
||||
{/* ========================== || City / Zone || ========================== */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-location">Choose Partner</InputLabel>
|
||||
<InputLabel htmlFor="personal-location">City / Zone</InputLabel>
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
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' }}
|
||||
onChange={(event, value) => {
|
||||
if (value) {
|
||||
console.log(value);
|
||||
// This picker is fetchAppLocations() (hub-derived
|
||||
// cities), not a Partners resource — the field used
|
||||
// to write a `partnerid` that doesn't exist on
|
||||
// these options (always undefined) alongside the
|
||||
// real applocationid. Only applocationid is a
|
||||
// confirmed miler field.
|
||||
setLocoName(value.locationname);
|
||||
fetchridershifts(value.applocationid);
|
||||
setRiderdata({
|
||||
...riderdata,
|
||||
partnerid: value.partnerid,
|
||||
applocationid: value.applocationid
|
||||
});
|
||||
} else {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,14 @@
|
||||
import { Avatar, Box, Chip, Grid, Paper, Stack, Typography, useMediaQuery } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
MdBadge,
|
||||
MdPerson,
|
||||
MdLocationOn,
|
||||
MdMail,
|
||||
MdPhone,
|
||||
MdPlace,
|
||||
MdMyLocation,
|
||||
MdLocationCity,
|
||||
MdMap,
|
||||
MdMarkunreadMailbox,
|
||||
MdVerifiedUser
|
||||
} from 'react-icons/md';
|
||||
import { useState } from 'react';
|
||||
import { Avatar, Box, Button, Chip, Grid, InputLabel, Paper, Stack, TextField, Typography } from '@mui/material';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { MdBadge, MdPerson, MdLocationOn, MdMail, MdShield, MdVerifiedUser, MdLock } from 'react-icons/md';
|
||||
import CircularLoader from 'components/CircularLoader';
|
||||
import Loader from 'components/Loader';
|
||||
import { getusers } from 'pages/api/api';
|
||||
import { updateProfilePassword } from 'pages/api/doormileApi';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
|
||||
const ROLE_LABELS = { 1: 'Admin', 3: 'Manager', 4: 'Executive' };
|
||||
|
||||
// ---- shared design tokens (mirrors the DT block used across the console) ----
|
||||
const DT = {
|
||||
@@ -99,14 +91,72 @@ const SectionCard = ({ title, icon: Icon, children }) => (
|
||||
);
|
||||
|
||||
const ViewProfile = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const { data: userData, isLoading } = useQuery({
|
||||
queryKey: ['getuser'],
|
||||
queryFn: getusers
|
||||
});
|
||||
|
||||
const fullname = userData?.fullname || userData?.firstname || 'User Profile';
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
|
||||
const changePasswordMutation = useMutation({
|
||||
mutationFn: () => updateProfilePassword(currentPassword, newPassword),
|
||||
onSuccess: (res) => {
|
||||
if (res.success) {
|
||||
enqueueSnackbar('Password updated successfully', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
} else {
|
||||
enqueueSnackbar(res.message || 'Failed to update password', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to update password', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const handleChangePassword = () => {
|
||||
if (!currentPassword || !newPassword) {
|
||||
enqueueSnackbar('Fill current and new password', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
enqueueSnackbar('New password and confirmation do not match', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
return;
|
||||
}
|
||||
changePasswordMutation.mutate();
|
||||
};
|
||||
|
||||
// GET /admin/profile has no documented schema, but the console's own login
|
||||
// response ({ success, token, user: { id, name, email, role, tenantid } })
|
||||
// is confirmed — /admin/profile almost certainly returns the same "user"
|
||||
// shape. Fields like address/suburb/city/postcode were jupiter-era guesses
|
||||
// with no basis in the new API and have been dropped rather than shown blank.
|
||||
const fullname = userData?.name || userData?.fullname || 'User Profile';
|
||||
const roleLabel = ROLE_LABELS[userData?.role] || ROLE_LABELS[userData?.roleid] || userData?.role || '—';
|
||||
const tenantLabel = userData?.tenantid ? `Tenant #${userData.tenantid}` : 'Doormile staff (all tenants)';
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -153,11 +203,11 @@ const ViewProfile = () => {
|
||||
{fullname}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap alignItems="center">
|
||||
{userData?.authname && (
|
||||
{roleLabel && (
|
||||
<Chip
|
||||
size="small"
|
||||
icon={<MdVerifiedUser size={14} style={{ color: BRAND }} />}
|
||||
label={userData.authname}
|
||||
label={roleLabel}
|
||||
sx={{
|
||||
bgcolor: soft(BRAND),
|
||||
color: BRAND,
|
||||
@@ -167,19 +217,17 @@ const ViewProfile = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{userData?.userid != null && userData?.userid !== '' && (
|
||||
{(userData?.id != null || userData?.userid != null) && (
|
||||
<Typography variant="body2" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
||||
ID #{userData.userid}
|
||||
ID #{userData.id ?? userData.userid}
|
||||
</Typography>
|
||||
)}
|
||||
{userData?.applocation && (
|
||||
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}>
|
||||
<MdLocationOn size={15} />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{userData.applocation}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}>
|
||||
<MdLocationOn size={15} />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{tenantLabel}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
@@ -190,42 +238,66 @@ const ViewProfile = () => {
|
||||
<Grid item xs={12} md={6}>
|
||||
<SectionCard title="Account" icon={MdBadge}>
|
||||
<Stack spacing={2.5}>
|
||||
<InfoField icon={MdPerson} label="User Name" value={userData?.fullname} />
|
||||
<InfoField icon={MdBadge} label="User ID" value={userData?.userid} />
|
||||
<InfoField icon={MdVerifiedUser} label="Auth Name" value={userData?.authname} />
|
||||
<InfoField icon={MdLocationOn} label="App Location" value={userData?.applocation} />
|
||||
<InfoField icon={MdPerson} label="User Name" value={userData?.name || userData?.fullname} />
|
||||
<InfoField icon={MdBadge} label="User ID" value={userData?.id ?? userData?.userid} />
|
||||
<InfoField icon={MdMail} label="E-Mail" value={userData?.email} accent="#0ea5e9" />
|
||||
<InfoField icon={MdVerifiedUser} label="Role" value={roleLabel} />
|
||||
<InfoField icon={MdLocationOn} label="Tenant" value={tenantLabel} accent="#14b8a6" />
|
||||
</Stack>
|
||||
</SectionCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<SectionCard title="Contact" icon={MdMail}>
|
||||
<Stack spacing={2.5}>
|
||||
<InfoField icon={MdPhone} label="Contact No" value={userData?.contactno} accent="#0ea5e9" />
|
||||
<InfoField icon={MdMail} label="E-Mail" value={userData?.email} accent="#0ea5e9" />
|
||||
<InfoField icon={MdPlace} label="Address" value={userData?.address} accent="#0ea5e9" />
|
||||
<SectionCard title="Security" icon={MdShield}>
|
||||
<Stack spacing={2}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel htmlFor="current-password">Current Password</InputLabel>
|
||||
<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>
|
||||
</SectionCard>
|
||||
</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>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -14,7 +14,6 @@ const MaintenanceComingSoon = Loadable(lazy(() => import('pages/maintenance/comi
|
||||
|
||||
// render - sample page
|
||||
// const SamplePage = Loadable(lazy(() => import('pages/extra-pages/sample-page')));
|
||||
const Login = Loadable(lazy(() => import('pages/nearle/login1')));
|
||||
// const Dashboard = Loadable(lazy(() => import('pages/nearle/dashboard')));
|
||||
|
||||
const Orders = Loadable(lazy(() => import('pages/nearle/orders/orders')));
|
||||
@@ -25,8 +24,6 @@ const Deliveries = Loadable(lazy(() => import('pages/nearle/deliveries/deliverie
|
||||
const Tenants = Loadable(lazy(() => import('pages/nearle/clients/Tenants')));
|
||||
const ClientsPricing = Loadable(lazy(() => import('pages/nearle/clientPricing/clientPricing')));
|
||||
|
||||
const Details = Loadable(lazy(() => import('pages/nearle/orders/details')));
|
||||
|
||||
const ViewProfile = Loadable(lazy(() => import('pages/nearle/viewProfile')));
|
||||
|
||||
const Createorder1 = Loadable(lazy(() => import('pages/nearle/orders/createorder1')));
|
||||
@@ -34,7 +31,6 @@ const MultipleOrders = Loadable(lazy(() => import('pages/nearle/orders/multipleO
|
||||
|
||||
const Createclient = Loadable(lazy(() => import('pages/nearle/clients/createclient')));
|
||||
|
||||
const Requests = Loadable(lazy(() => import('pages/nearle/requests/requests')));
|
||||
const OrdersSummary = Loadable(lazy(() => import('pages/nearle/reports/ordersSummary')));
|
||||
const OrdersDetails = Loadable(lazy(() => import('pages/nearle/reports/ordersDetails')));
|
||||
const RidersSummary = Loadable(lazy(() => import('pages/nearle/reports/ridersSummary')));
|
||||
@@ -45,6 +41,14 @@ const EditRider = Loadable(lazy(() => import('pages/nearle/riders/editRider')));
|
||||
const Dispatch = Loadable(lazy(() => import('pages/nearle/dispatch/Dispatch')));
|
||||
const DispatchPreview = Loadable(lazy(() => import('pages/nearle/dispatch/Preview')));
|
||||
|
||||
const Customers = Loadable(lazy(() => import('pages/nearle/customers/customers')));
|
||||
const Hubs = Loadable(lazy(() => import('pages/nearle/hubs/hubs')));
|
||||
const Vehicles = Loadable(lazy(() => import('pages/nearle/vehicles/vehicles')));
|
||||
const AppUsers = Loadable(lazy(() => import('pages/nearle/appUsers/appUsers')));
|
||||
const Tripsheets = Loadable(lazy(() => import('pages/nearle/tripsheets/tripsheets')));
|
||||
const Exceptions = Loadable(lazy(() => import('pages/nearle/exceptions/exceptions')));
|
||||
const CompetitiveIntel = Loadable(lazy(() => import('pages/nearle/competitiveIntel/competitiveIntel')));
|
||||
|
||||
// ==============================|| MAIN ROUTING ||============================== //
|
||||
|
||||
const MainRoutes = {
|
||||
@@ -81,10 +85,6 @@ const MainRoutes = {
|
||||
path: 'pricing',
|
||||
element: <ClientsPricing />
|
||||
},
|
||||
{
|
||||
path: 'requests',
|
||||
element: <Requests />
|
||||
},
|
||||
{
|
||||
path: 'riders',
|
||||
element: <Riders />
|
||||
@@ -106,10 +106,6 @@ const MainRoutes = {
|
||||
path: 'orders/createorders',
|
||||
element: <MultipleOrders />
|
||||
},
|
||||
{
|
||||
path: 'orders/details',
|
||||
element: <Details />
|
||||
},
|
||||
{
|
||||
path: 'clients/create',
|
||||
element: <Createclient />
|
||||
@@ -138,6 +134,34 @@ const MainRoutes = {
|
||||
{
|
||||
path: 'dispatch/preview',
|
||||
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',
|
||||
element: <CommonLayout />,
|
||||
|
||||
38
src/utils/doormileAxios.js
Normal file
38
src/utils/doormileAxios.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// Client for the new Doormile Express admin API (api.doormile.com/api/v1/admin/*).
|
||||
// Separate instance from utils/axios.js and the raw axios calls in pages/api/api.js
|
||||
// because this backend requires a JWT Bearer token on every request and uses its
|
||||
// own token lifecycle — it does not share the `authname` gate in App.js.
|
||||
// See express-console-api.md at the repo root for the full endpoint reference.
|
||||
|
||||
export const DOORMILE_TOKEN_KEY = 'doormileToken';
|
||||
export const DOORMILE_USER_KEY = 'doormileUser';
|
||||
|
||||
const doormileAxios = axios.create({
|
||||
baseURL: process.env.REACT_APP_DOORMILE_URL || 'https://api.doormile.com/api/v1'
|
||||
});
|
||||
|
||||
doormileAxios.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem(DOORMILE_TOKEN_KEY);
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
doormileAxios.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
// Mirror utils/session.js's performSessionLogout contract (full localStorage
|
||||
// wipe + hard navigate) so an expired doormile token logs the console out the
|
||||
// same way inactivity/manual logout does, instead of leaving a half-authed state.
|
||||
if (error.response?.status === 401 && !window.location.href.includes('/login')) {
|
||||
localStorage.clear();
|
||||
window.location.replace('/login');
|
||||
}
|
||||
return Promise.reject(error.response?.data || error);
|
||||
}
|
||||
);
|
||||
|
||||
export default doormileAxios;
|
||||
@@ -14,5 +14,13 @@
|
||||
"ordersummary": "Orders Summary",
|
||||
"ordersdetails": "Orders Details",
|
||||
"riderssummary": "Riders Summary",
|
||||
"dispatch": "Dispatch"
|
||||
"dispatch": "Dispatch",
|
||||
"customers": "Customers",
|
||||
"fleetops": "Fleet & Ops",
|
||||
"hubs": "Hubs",
|
||||
"vehicles": "Vehicles",
|
||||
"tripsheets": "Tripsheets",
|
||||
"exceptions": "Exceptions",
|
||||
"competitiveintel": "Competitive Intel",
|
||||
"appusers": "App Users"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user