Compare commits

..

24 Commits

Author SHA1 Message Date
49e86a4428 feat: console fully wired to Doormile backend
- customers.js: edit dialog calls PATCH /admin/customers/:id (live)
- createclient.js: fixed ReferenceError, posts to /crm/clients
- createCustomer.js: redirects to customers list (no B2C create flow)

Console status:
 Login, Dashboard, Hubs, Bookings, Consignments
 Milers (CRUD), Clients (CRUD), Customers (view + edit)
 Cancel booking, Auto-assign, Status updates
Phase 3 pending: Dispatch/Live Operations

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 12:22:45 +05:30
18d35a0ad2 fix: customer edit dialog field names + safe stub
- pre-fill from confirmed fields (name/phone/email)
- submit stubbed to toast until PATCH /admin/customers/:id exists
- flagged in comment for Phase 3 backend work

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 12:10:32 +05:30
709a06274a feat: console Phase 2 final - live cancel and customer endpoints
- cancelOrder/cancelDeliveryAPI: real POST /admin/bookings/:id/cancel
- getallcustomers/getcustomersummary: real /admin/customers, aliasing
  appcustomerid -> userid for existing call sites
- BookingDetail: confirmed field names from live backend, cancel button
  now hits the live endpoint
- customers.js: table columns show Name/Phone/Email/Total Bookings/Joined
  using appcustomerid/name/phone/email/totalbookings/createdat

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 11:48:45 +05:30
1a71732396 feat: Doormile console Phase 2 complete
Part A: remaining api.js legacy functions replaced
  - fetchRidersList, cancelOrder/Delivery, changeRiderAPI
  - fetchPercentageAPI, fetchCountAPI → Doormile status counts
  - getpricinglist, fetchLocations, fetchRidersLogs, getusers
  - fetchPaymentType, getorderdetails

Part B: deliveries.js status tabs → Doormile statuses

Part C: 3 new pages
  - pages/nearle/hubs/Hubs.js (hub CRUD + miler counts)
  - pages/nearle/bookings/BookingDetail.js (booking detail + timeline)
  - pages/nearle/dashboard/Dashboard.js (live KPIs + city breakdown)

Part D: routes + menu updated
2026-07-08 19:12:28 +05:30
b8f1cd5f1d fix: point customers page at /admin/customers instead of legacy path
getallcustomers/getcustomersummary still called the old
/customers/getallcustomers and /customers/getcustomersummary paths,
which 403 on api.doormile.com (auth was accepted, route wasn't valid).
Repointed both at /admin/customers per the original conversion plan.

No Doormile customer response shape has been confirmed anywhere in this
work (unlike bookings/clients/milers, which had example shapes given
up front) -- field names in customers.js (firstname/contactno/address/
customerid/etc) are still the old NearlExpress shape and left as-is
rather than guessed. Won't crash, just shows blanks for any field that
doesn't exist on a real Doormile customer. Need the actual response
body to do a real field-mapping pass, same as the login.js episode.

updateCustomer (PUT /customers/update) is still unmapped -- flagged in
a comment rather than guessed at, consistent with cancelOrder.
2026-07-08 17:54:36 +05:30
283c24942d fix: stop the notification-permission toast from stacking
AppContent was declared inline inside App(), making it a new component
reference on every App render -- React would unmount/remount it each
time, re-running its useEffect (generateToken + FCM listener setup) and
re-showing the "Enable notifications" toast, which never auto-dismisses
and has no dedup, so they piled up. Moved AppContent to a stable
top-level component and added a one-per-tab guard on the toast itself
as a second line of defense.
2026-07-08 17:45:32 +05:30
6143644039 remove: invoice, reports (orders/riders summary+logs, profitability), legacy APIs
Dropped entirely rather than adapted to Doormile, per direction: none of
these are needed for the Doormile console right now.

Deleted:
  - src/pages/nearle/invoice/ (invoice.js, invoicePreview.js)
  - src/pages/nearle/reports/ (ordersSummary, ordersDetails, ridersSummary,
    ridersLogs, profitability, plus their only consumers: mapWithRoute.js,
    RiderLocationMap.js, RidersRoutes.js)

Removed the corresponding routes (MainRoutes.js) and sidebar entries
(menu-items/nearle.js: the whole "reports" collapse + "invoice" item),
and their locale keys from en.json.

Removed now-orphaned api.js functions that only those pages called:
getreportsummary, getreportlocationsummary, getriderbydelivery, fetchCount,
fetchRidersSummary, fetchinvoiceinsight, fetchdeliverylist, fetchOrders1,
getallriders. Verified each had zero remaining importers before removing.

Kept fetchorderdetails (still used by orders/details.js) and fetchRidersLogs
(still used by Dispatch.js) -- same name, different consumer than the
deleted reports pages.
2026-07-08 17:40:02 +05:30
c22daee1de feat: rebuild orders.js around Doormile bookings, drop AI-optimiser pipeline
orders.js was the entry point of the whole NearlExpress optimiser flow
(checkbox multi-select -> createOptimisationDeliveries/createAutomationDeliveries
-> navigate to dispatch/Preview.js -> finalCreatedeliveries), none of which
maps to Doormile (assignment happens server-side; there's no manual
route-sequencing/preview step per the original conversion plan).

Stripped: SpeedDial (AI/manual assign, bulk delete), checkbox multi-select,
the two full-screen assign/preview dialogs, tenant/location filters, date
range filter, transport-mode + hyper-tuning selectors, absent-riders picker,
product-line collapse, CSV export, and the embedded <Dispatch> render --
all NearlExpress-only concepts with no Doormile equivalent. This is what
was crashing (.toFixed() on undefined collectionamt/deliverycharge/etc,
fields that don't exist on a Doormile booking).

Rebuilt around the real booking shape (bookingid, bookingreference,
pickupaddress, deliveryaddress, status, createdat, assignedmileruserid)
with the 9 real Doormile statuses, and replaced the whole multi-step
assign flow with a single per-row "auto-assign" action calling
autoAssignBooking(bookingId).

Consequence: OrdersPreview.js and dispatch/Preview.js are no longer
reachable via navigation from this page (still routed directly by URL).
OrdersPreview.js was already unreachable before this change -- its only
navigate() call was commented out. dispatch/Preview.js's own file was not
rewritten; that's Dispatch.js's live-map ecosystem, a separate and much
larger undertaking.

Cancel booking still calls the old /orders/updateorder endpoint -- no
Doormile cancel-booking endpoint has been specified anywhere in this
conversion work, so it's left pointing at the unmapped legacy path
rather than guessed at.
2026-07-08 17:32:31 +05:30
a6e62a70f0 fix: tolerate flat (non-nested) success response shape from /admin/login
The live backend returns success:true + token but doesn't nest user
fields under a data object the way the spec described, causing
'Cannot read properties of undefined (reading email)'. Falls back to
reading fields off data.data, data.user, or the top-level response.
2026-07-08 17:20:25 +05:30
ab59421861 fix: point the actual active login page (login.js) at Doormile auth
LoginRoutes.js is registered before MainRoutes.js and defines its own
un-prefixed '/login' route pointing at pages/nearle/login — it wins the
route match, so login1.js (fixed in the earlier commit) was never being
rendered. login.js still hit jupiter.nearle.app/live/api/v1/users/console/login
with the old multi-step (email lookup -> setup/enter password) flow.

Collapsed to a single POST /admin/login with { email, password,
userfcmtoken }, matching Doormile's one-shot auth response. Visual
layout (branded two-panel screen, "Welcome back" copy) is unchanged.

Confirmed live: curl against api.doormile.com/api/v1/admin/login,
/admin/milers, /crm/clients, /admin/bookings all return the expected
{success,...} shapes and status codes for this code to handle.
2026-07-08 17:06:01 +05:30
c52350df0f feat: convert NearlExpress console to Doormile admin
Phase 1: env config + login auth (Doormile JWT)
Phase 2: full API layer rewrite in api.js
  - All endpoints now point at api.doormile.com/api/v1
  - Removed all jupiter.nearle.app references
  - Removed all routes.workolik.com references (Phase 3)
  - Miler CRUD: GET/POST/PATCH /admin/milers
  - Clients: GET/POST/PATCH /crm/clients
  - Bookings: GET /admin/bookings
  - Auto-assign: POST /hub/bookings/:id/auto-assign

Pages rewritten:
  - riders.js - Doormile miler fields, status mapping
  - createrider.js - fixed broken form, real miler creation
  - editRider.js - stripped bank/vehicle/insurance, hub mapping
  - Tenants.js - stripped pricing dialog, CRM client fields

Menu: renamed Orders/Deliveries/Tenants/Riders/Dispatch
  to Bookings/Consignments/Clients/Milers/Live Operations

Pending Phase 3: dispatch pages, reports, consignments
2026-07-08 15:47:01 +05:30
ed7640ad1e updates on the build regarding the dockerfile and nginx config file 2026-07-08 11:41:56 +05:30
8224f45974 updates and removed on the dead codes 2026-07-07 12:38:20 +05:30
d90609f5a0 updates on the readme file regarding the namechange 2026-07-07 11:19:13 +05:30
e468861709 initialization 2026-07-07 11:16:41 +05:30
d716bed6a5 updates on the build fix on the coming-soon page 2026-07-04 16:26:38 +05:30
bb0296777b updates on the build fix 2026-07-04 16:17:11 +05:30
67afa55c5b updates on the changes google.maps.DistanceMatrixService to orsm 2026-07-04 16:12:53 +05:30
17faae1f6e updates on the dispatch page faster improvements fix 2026-07-02 12:59:46 +05:30
96db14331b alignments on the image in the login page section left side 2026-07-02 12:18:24 +05:30
e385e2fbfd updates on the images and the build 2026-07-02 12:01:21 +05:30
b4cf2be556 updates on the login page and order page and fixed on the logo and the date validation 2026-07-02 11:11:48 +05:30
39e97c3041 updates on the login 2026-07-01 16:19:49 +05:30
f50d5a0c56 Revert "updates on the dispatch page"
This reverts commit e21a1e66b5.
2026-07-01 13:33:12 +05:30
150 changed files with 3824 additions and 29121 deletions

7
.env
View File

@@ -6,9 +6,10 @@ REACT_APP_API_URL=https://mock-data-api-nextjs.vercel.app/
## Google Map Key
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_URL=https://api.doormile.com/api/v1
REACT_APP_URL2=https://api.doormile.com/api/v1
REACT_APP_URL3=https://api.doormile.com/api/v1
REACT_APP_INTERNAL_KEY=doormile-internal-2024
REACT_APP_STAFF_TOKEN=
REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk

View File

@@ -1,8 +1,13 @@
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_URL=https://api.doormile.com/api/v1
REACT_APP_URL2=https://api.doormile.com/api/v1
REACT_APP_URL3=https://api.doormile.com/api/v1
REACT_APP_INTERNAL_KEY=doormile-internal-2024
REACT_APP_STAFF_TOKEN=
REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk
GENERATE_SOURCEMAP=false
DISABLE_ESLINT_PLUGIN=true

View File

@@ -1,3 +1,7 @@
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
REACT_APP_URL2=
REACT_APP_STAFF_TOKEN=
REACT_APP_URL=https://api.doormile.com/api/v1
REACT_APP_URL2=https://api.doormile.com/api/v1
REACT_APP_INTERNAL_KEY=doormile-internal-2024
REACT_APP_STAFF_TOKEN=
GENERATE_SOURCEMAP=false
DISABLE_ESLINT_PLUGIN=true

8
.gitignore vendored
View File

@@ -104,3 +104,11 @@ dist
# wincompare file
*.bak
# Env files — only env.staging (no leading dot) is the agreed-shared
# baseline and gets committed. Everything else holds live API keys.
.env
.env.development
.env.staging
.env.production
.env.local

View File

@@ -174,43 +174,43 @@ const AccentAvatar = ({ color, selected, size = 24, children }) => (
### Page anatomy (every operator page should follow this order)
> **Use brand purple `#662582` (with light variant `#9255AB`) for every brand surface below.** Don't use indigo `#6366f1` — that's reserved for the "Accepted" status badge only.
> **Use brand red `#C01227` (with light variant `#D35968`) for every brand surface below.** Don't use indigo `#6366f1` — that's reserved for the "Accepted" status badge only.
1. **Gradient header `<Paper>`**`linear-gradient(135deg, ${tint('#662582')} 0%, ${tint('#9255AB')} 100%)`, 48px filled `#662582` avatar with a page icon, `Typography variant="h3"` title, "Live · {zone}" sub-line with an 8px green pulsing dot, and a pill `LocationAutocomplete` on the right (`pill accentColor="#662582" paperComponent={SoftPaper}`).
2. **KPI tiles row**`Grid` of 34 `Paper` cards, each with a 3px top stripe gradient, uppercase eyebrow label, large bold number, and a soft-tinted avatar holding an icon. The "primary" tile uses brand purple `#662582`; other tiles use semantic status colours.
1. **Gradient header `<Paper>`**`linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D35968')} 100%)`, 48px filled `#C01227` avatar with a page icon, `Typography variant="h3"` title, "Live · {zone}" sub-line with an 8px green pulsing dot, and a pill `LocationAutocomplete` on the right (`pill accentColor="#C01227" paperComponent={SoftPaper}`).
2. **KPI tiles row**`Grid` of 34 `Paper` cards, each with a 3px top stripe gradient, uppercase eyebrow label, large bold number, and a soft-tinted avatar holding an icon. The "primary" tile uses brand red `#C01227`; other tiles use semantic status colours.
3. **Filter bar `<Paper>`** (optional) — pill-style `Autocomplete`s using `pillFieldSx(color)` + `SoftPaper`, each with an `AccentAvatar` start-adornment.
4. **Pill status tabs + pill search** — tabs as clickable `<Box>` pills (active = filled accent + glow ring, inactive = `tint` bg + `edge` border), each with an avatar icon and a count badge. Tab accents use the **semantic status colour** for each tab (pending → amber, delivered → emerald, etc.). Search via `DebounceSearchBar` styled with brand purple `#662582` (tint bg, edge border, focus ring).
5. **Table `<Paper>`** with `<TableContainer>` + sticky `<TableHead>` — uppercase muted headers on `DT.surfaceAlt`, rows with `borderBottom: 1px solid ${DT.divider}` and `:hover` row tint. Scrollbar thumb uses brand purple `edge('#662582')`.
4. **Pill status tabs + pill search** — tabs as clickable `<Box>` pills (active = filled accent + glow ring, inactive = `tint` bg + `edge` border), each with an avatar icon and a count badge. Tab accents use the **semantic status colour** for each tab (pending → amber, delivered → emerald, etc.). Search via `DebounceSearchBar` styled with brand red `#C01227` (tint bg, edge border, focus ring).
5. **Table `<Paper>`** with `<TableContainer>` + sticky `<TableHead>` — uppercase muted headers on `DT.surfaceAlt`, rows with `borderBottom: 1px solid ${DT.divider}` and `:hover` row tint. Scrollbar thumb uses brand red `edge('#C01227')`.
6. **Status badges in cells**`Stack` with `AccentAvatar` + label inside a soft pill (tint bg, edge border). Status colours come from a per-page `STATUS_META` map keyed by lowercase status string — these are **semantic**, not brand.
7. **Edit / action icon buttons** — soft-pill `IconButton` using brand purple `#662582` (NOT `#8b5cf6` — that overlaps with the "Picked" status badge and confuses operators).
7. **Edit / action icon buttons** — soft-pill `IconButton` using brand red `#C01227` (NOT `#8b5cf6` — that overlaps with the "Picked" status badge and confuses operators).
8. **Empty state** — centered 64px avatar (soft grey), bold "No X to show" line, and a muted helper sentence. Do not use antd `<Empty />` for new code.
### Universal brand colour
**`#662582` — NearlExpress brand purple.** This is the canonical primary colour for this app, defined in `src/themes/theme/default.js` as `primary.main`. It drives the sidebar, the logo, and is what every new surface (page headers, KPI primary tile, search bars, edit-action buttons, dialog/popup headers, scrollbars) must use as the brand accent.
**`#C01227` — NearlExpress brand red.** This is the canonical primary colour for this app, defined in `src/themes/theme/default.js` as `primary.main`. It drives the sidebar, the logo, and is what every new surface (page headers, KPI primary tile, search bars, edit-action buttons, dialog/popup headers, scrollbars) must use as the brand accent.
Variants (also from `theme/default.js`):
| Token | Hex | Use |
|---|---|---|
| `primary.lighter` | `#E8D9EF` | Very subtle wash bg |
| `primary.light` / `primary.400` | `#9255AB` | Gradient pair with main |
| `primary.main` | `#662582` | Brand primary — default for all brand surfaces |
| `primary.dark` | `#4D1C61` | Hover / pressed states |
| `primary.darker` | `#260E30` | Deep contrast text on light bg |
| `primary.lighter` | `#F6DBDF` | Very subtle wash bg |
| `primary.light` / `primary.400` | `#D35968` | Gradient pair with main |
| `primary.main` | `#C01227` | Brand primary — default for all brand surfaces |
| `primary.dark` | `#900E1D` | Hover / pressed states |
| `primary.darker` | `#47070E` | Deep contrast text on light bg |
**Page header / dialog header gradient:** `linear-gradient(135deg, ${tint('#662582')} 0%, ${tint('#9255AB')} 100%)` (subtle wash) or `linear-gradient(135deg, #662582 0%, #9255AB 100%)` (solid, for dialog titles).
**Page header / dialog header gradient:** `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D35968')} 100%)` (subtle wash) or `linear-gradient(135deg, #C01227 0%, #D35968 100%)` (solid, for dialog titles).
> **Migration note:** `deliveries.js`, `Tenants.js`, and `clientPricing.js` currently use `#6366f1` (indigo) as their brand accent — a holdover from the first design pass before brand purple was canonicalised. They are scheduled to migrate to `#662582`. `customers.js` and the createorder1 Saved-Address dialog have already been migrated.
> **Migration note:** `deliveries.js`, `Tenants.js`, and `clientPricing.js` currently use `#6366f1` (indigo) as their brand accent — a holdover from the first design pass before brand red was canonicalised. They are scheduled to migrate to `#C01227`. `customers.js` and the createorder1 Saved-Address dialog have already been migrated.
### Status palette (semantic — distinct from brand)
These colour-code lifecycle states. Do **not** swap them for brand purple — operators rely on the colour to identify status at a glance.
These colour-code lifecycle states. Do **not** swap them for brand red — operators rely on the colour to identify status at a glance.
| Meaning | Colour |
|--------------------------|-----------|
| Pending / waiting | `#f59e0b` (amber) |
| Accepted / assigned | `#6366f1` (indigo — semantically distinct from brand purple) |
| Accepted / assigned | `#6366f1` (indigo — semantically distinct from brand red) |
| Arrived | `#06b6d4` (cyan) |
| Picked up | `#8b5cf6` (light purple — distinct from brand) |
| Active / in-transit | `#14b8a6` (teal) |

18
Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM nginx:alpine
# Move to Nginx's public folder
WORKDIR /usr/share/nginx/html
# 1. CRUCIAL: Remove Nginx's default "Welcome" page files completely
RUN rm -rf ./*
# 2. Copy your compiled static assets into the root folder.
# NOTE: If your folder is named "dist" instead of "build", change "build/" to "dist/"
COPY build/ .
# 3. Copy your custom Nginx configuration (which you already have in your log)
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,4 +1,4 @@
# NearleXpress - Operator Dispatch Console & Deliveries Portal
# Doormile - Operator Dispatch Console & Deliveries Portal
A high-fidelity, real-time dispatcher command center and order-delivery console built using React, React-Leaflet, TanStack Query, and Material UI.

View File

@@ -1,3 +1,4 @@
REACT_APP_URL='https://jupiter.nearle.app/live/api/v1'
REACT_APP_URL2=''
REACT_APP_URL='https://api.doormile.com/api/v1'
REACT_APP_URL2='https://api.doormile.com/api/v1'
REACT_APP_INTERNAL_KEY='doormile-internal-2024'
REACT_APP_STAFF_TOKEN=''

18
nginx.conf Normal file
View File

@@ -0,0 +1,18 @@
events {}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
# This line forces Nginx to pass routing back to React Router
try_files $uri $uri/ /index.html;
}
}
}

14
package-lock.json generated
View File

@@ -21417,20 +21417,6 @@
"is-typedarray": "^1.0.0"
}
},
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/ua-parser-js": {
"version": "1.0.40",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -4,8 +4,8 @@
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Nearle Neighbourhood commerce" />
<meta name="theme-color" content="#C01227" />
<meta name="description" content="Doormile — corporate logistics & last-mile delivery admin console" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link
rel="stylesheet"
@@ -28,7 +28,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Nearle Console</title>
<title>Doormile Console</title>
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -2,7 +2,6 @@
import Routes from 'routes';
import ThemeCustomization from 'themes';
import Locales from 'components/Locales';
// import RTLLayout from 'components/RTLLayout';
import ScrollTop from 'components/ScrollTop';
import Snackbar from 'components/@extended/Snackbar';
import Notistack from 'components/third-party/Notistack';
@@ -10,12 +9,30 @@ import { useNavigate } from 'react-router';
import { useEffect } from 'react';
import { generateToken, initFirebaseNotificationListener } from 'firebase_notification/notification';
import InternetStatus from 'components/updateNetworkStatus';
// auth-provider
// import { JWTProvider as AuthProvider } from 'contexts/JWTContext';
import useInactivityLogout from 'hooks/useInactivityLogout';
// ==============================|| APP - THEME, ROUTER, LOCAL ||============================== //
// Was previously declared inline inside App(), which made it a fresh
// component reference on every App render — React would unmount/remount it
// each time, re-firing its useEffect (and the FCM permission toast) on
// every App re-render instead of once per app load.
const AppContent = () => {
useInactivityLogout();
useEffect(() => {
generateToken();
initFirebaseNotificationListener();
}, []);
return (
<>
<Routes />
<Snackbar />
</>
);
};
const App = () => {
const navigate = useNavigate();
useEffect(() => {
@@ -24,39 +41,17 @@ const App = () => {
}
}, [navigate]);
const AppContent = () => {
useEffect(() => {
generateToken();
initFirebaseNotificationListener();
}, []);
return (
<>
<Routes />
<Snackbar />
</>
);
};
return (
<>
<ThemeCustomization>
<InternetStatus />
{/* <RTLLayout> */}
<Locales>
<ScrollTop>
{/* <AuthProvider> */}
<>
<Notistack>
{/* <Routes />
<Snackbar /> */}
<AppContent />
</Notistack>
</>
{/* </AuthProvider> */}
<Notistack>
<AppContent />
</Notistack>
</ScrollTop>
</Locales>
{/* </RTLLayout> */}
</ThemeCustomization>
</>
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -1,32 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, CircularProgress, Typography } from '@mui/material';
// ==============================|| PROGRESS - CIRCULAR LABEL ||============================== //
export default function CircularWithLabel({ value, ...others }) {
return (
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<CircularProgress variant="determinate" value={value} {...others} />
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography variant="caption" component="div" color="text.secondary">{`${Math.round(value)}%`}</Typography>
</Box>
</Box>
);
}
CircularWithLabel.propTypes = {
value: PropTypes.number
};

View File

@@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, CircularProgress, Typography, circularProgressClasses } from '@mui/material';
// ==============================|| PROGRESS - CIRCULAR PATH ||============================== //
export default function CircularWithPath({ value, size, variant, thickness, showLabel, pathColor, sx, ...others }) {
return (
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<CircularProgress
variant="determinate"
sx={{ color: pathColor ? pathColor : 'grey.200' }}
size={size}
thickness={thickness}
{...others}
value={100}
/>
{showLabel && (
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography variant="caption" component="div" color="text.secondary">
{value ? `${Math.round(value)}%` : '0%'}
</Typography>
</Box>
)}
<CircularProgress
variant={variant}
sx={{
...sx,
position: 'absolute',
left: 0,
[`& .${circularProgressClasses.circle}`]: {
strokeLinecap: 'round'
}
}}
size={size}
thickness={thickness}
value={value}
{...others}
/>
</Box>
);
}
CircularWithPath.propTypes = {
value: PropTypes.number,
size: PropTypes.number,
variant: PropTypes.string,
thickness: PropTypes.number,
showLabel: PropTypes.bool,
pathColor: PropTypes.string,
sx: PropTypes.array,
others: PropTypes.array
};

View File

@@ -1,22 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, LinearProgress } from '@mui/material';
// ==============================|| PROGRESS - LINEAR ICON ||============================== //
export default function LinearWithIcon({ icon, value, ...others }) {
return (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ width: '100%', mr: 1 }}>
<LinearProgress variant="determinate" value={value} {...others} />
</Box>
<Box sx={{ minWidth: 35 }}>{icon}</Box>
</Box>
);
}
LinearWithIcon.propTypes = {
icon: PropTypes.node,
value: PropTypes.number
};

View File

@@ -1,23 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, LinearProgress, Typography } from '@mui/material';
// ==============================|| PROGRESS - LINEAR WITH LABEL ||============================== //
export default function LinearWithLabel({ value, ...others }) {
return (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ width: '100%', mr: 1 }}>
<LinearProgress variant="determinate" value={value} {...others} />
</Box>
<Box sx={{ minWidth: 35 }}>
<Typography variant="body2" color="text.secondary">{`${Math.round(value)}%`}</Typography>
</Box>
</Box>
);
}
LinearWithLabel.propTypes = {
value: PropTypes.number
};

View File

@@ -1,6 +1,6 @@
import { styled } from '@mui/material/styles';
import CircularProgress from '@mui/material/CircularProgress';
import nelogo from '../assets/images/logo-sm.png';
import nelogo from '../assets/images/doormile-mark.png';
// Styled Loader Wrapper
const LoaderWrapper = styled('div')(() => ({

View File

@@ -1,37 +0,0 @@
import PropTypes from 'prop-types';
import { useEffect } from 'react';
// material-ui
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
// third-party
import rtlPlugin from 'stylis-plugin-rtl';
// project import
import { ThemeDirection } from 'config';
import useConfig from 'hooks/useConfig';
// ==============================|| RTL LAYOUT ||============================== //
const RTLLayout = ({ children }) => {
const { themeDirection } = useConfig();
useEffect(() => {
document.dir = themeDirection;
}, [themeDirection]);
const cacheRtl = createCache({
key: themeDirection === ThemeDirection.RTL ? 'rtl' : 'css',
prepend: true,
stylisPlugins: themeDirection === ThemeDirection.RTL ? [rtlPlugin] : []
});
return <CacheProvider value={cacheRtl}>{children}</CacheProvider>;
};
RTLLayout.propTypes = {
children: PropTypes.node
};
export default RTLLayout;

View File

@@ -1,10 +0,0 @@
// material-ui
import { styled } from '@mui/material/styles';
const ScrollX = styled('div')({
width: '100%',
overflowX: 'auto',
display: 'block'
});
export default ScrollX;

View File

@@ -1,49 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, Grid, Link, Stack, Typography } from '@mui/material';
// assets
import { GlobalOutlined, NodeExpandOutlined } from '@ant-design/icons';
// ==============================|| COMPONENTS - BREADCRUMBS ||============================== //
const ComponentHeader = ({ title, caption, directory, link }) => (
<Box sx={{ pl: 3 }}>
<Stack spacing={1.25}>
<Typography variant="h2">{title}</Typography>
{caption && (
<Typography variant="h6" color="textSecondary">
{caption}
</Typography>
)}
</Stack>
<Grid container spacing={0.75} sx={{ mt: 1.75 }}>
{directory && (
<Grid item xs={12}>
<Typography variant="caption" color="textSecondary">
<NodeExpandOutlined style={{ marginRight: 10 }} />
{directory}
</Typography>
</Grid>
)}
{link && (
<Grid item xs={12}>
<Link variant="caption" color="primary" href={link} target="_blank">
<GlobalOutlined style={{ marginRight: 10 }} />
{link}
</Link>
</Grid>
)}
</Grid>
</Box>
);
ComponentHeader.propTypes = {
title: PropTypes.string,
caption: PropTypes.string,
directory: PropTypes.string,
link: PropTypes.string
};
export default ComponentHeader;

View File

@@ -1,52 +0,0 @@
// material-ui
import { useTheme } from '@mui/material/styles';
import { Fab, Badge } from '@mui/material';
// project import
// assets
// ==============================|| CART ITEMS - FLOATING BUTTON ||============================== //
const FloatingCart = ({ element, count = 0, onClick, sx }) => {
const theme = useTheme();
// const cart = useSelector((state) => state.cart);
// const totalQuantity = sum(cart.checkout.products.map((item) => item.quantity));
return (
<Fab
// component={Link}
// to="/apps/e-commerce/checkout"
onClick={onClick} // ← important
size="large"
sx={{
top: '75%',
position: 'fixed',
right: 0,
zIndex: theme.zIndex.speedDial,
boxShadow: theme.customShadows.primary,
bgcolor: 'primary.lighter',
color: 'primary.main',
borderRadius: '25%',
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
'&:hover': {
bgcolor: 'primary.100',
boxShadow: theme.customShadows.primary
},
'&:focus-visible': {
outline: `2px solid ${theme.palette.primary.dark}`,
outlineOffset: 2
},
...sx
}}
>
<Badge showZero badgeContent={count} color="error">
{element}
</Badge>
</Fab>
);
};
export default FloatingCart;

View File

@@ -1,176 +0,0 @@
import PropTypes from 'prop-types';
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Box, Button, CardContent, CardMedia, Chip, Divider, Grid, Rating, Stack, Typography } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
import IconButton from 'components/@extended/IconButton';
import SkeletonProductPlaceholder from 'components/cards/skeleton/ProductPlaceholder';
import { useDispatch, useSelector } from 'store';
import { addProduct } from 'store/reducers/cart';
import { openSnackbar } from 'store/reducers/snackbar';
// assets
import { HeartOutlined, HeartFilled } from '@ant-design/icons';
const prodImage = require.context('assets/images/e-commerce', true);
// ==============================|| PRODUCT CARD ||============================== //
const ProductCard = ({ id, color, name, brand, offer, isStock, image, description, offerPrice, salePrice, rating }) => {
const theme = useTheme();
const dispatch = useDispatch();
const prodProfile = image && prodImage(`./${image}`);
const [productRating] = useState(rating);
const [wishlisted, setWishlisted] = useState(false);
const cart = useSelector((state) => state.cart);
const addCart = () => {
dispatch(addProduct({ id, name, image, salePrice, offerPrice, color, size: 8, quantity: 1, description }, cart.checkout.products));
dispatch(
openSnackbar({
open: true,
message: 'Add To Cart Success',
variant: 'alert',
alert: {
color: 'success'
},
close: false
})
);
};
const addToFavourite = () => {
setWishlisted(!wishlisted);
dispatch(
openSnackbar({
open: true,
message: 'Added to favourites',
variant: 'alert',
alert: {
color: 'success'
},
close: false
})
);
};
const [isLoading, setLoading] = useState(true);
useEffect(() => {
setLoading(false);
}, []);
return (
<>
{isLoading ? (
<SkeletonProductPlaceholder />
) : (
<MainCard
content={false}
boxShadow
sx={{
'&:hover': {
transform: 'scale3d(1.02, 1.02, 1)',
transition: 'all .4s ease-in-out'
}
}}
>
<Box sx={{ width: 250, m: 'auto' }}>
<CardMedia
sx={{ height: 250, textDecoration: 'none', opacity: isStock ? 1 : 0.25 }}
image={prodProfile}
component={Link}
to={`/apps/e-commerce/product-details/${id}`}
/>
</Box>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
sx={{ width: '100%', position: 'absolute', top: 0, pt: 1.75, pl: 2, pr: 1 }}
>
{!isStock && <Chip variant="light" color="error" size="small" label="Sold out" />}
{offer && <Chip label={offer} variant="combined" color="success" size="small" />}
<IconButton color="secondary" sx={{ ml: 'auto', '&:hover': { background: 'transparent' } }} onClick={addToFavourite}>
{wishlisted ? (
<HeartFilled style={{ fontSize: '1.15rem', color: theme.palette.error.main }} />
) : (
<HeartOutlined style={{ fontSize: '1.15rem' }} />
)}
</IconButton>
</Stack>
<Divider />
<CardContent sx={{ p: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Stack>
<Typography
component={Link}
to={`/apps/e-commerce/product-details/${id}`}
color="textPrimary"
variant="h5"
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'block',
textDecoration: 'none'
}}
>
{name}
</Typography>
<Typography variant="h6" color="textSecondary">
{brand}
</Typography>
</Stack>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-end" flexWrap="wrap" rowGap={1.75}>
<Stack>
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="h5">${offerPrice}</Typography>
{salePrice && (
<Typography variant="h6" color="textSecondary" sx={{ textDecoration: 'line-through' }}>
${salePrice}
</Typography>
)}
</Stack>
<Stack direction="row" alignItems="flex-start">
<Rating precision={0.5} name="size-small" value={productRating} size="small" readOnly />
<Typography variant="caption">({productRating?.toFixed(1)})</Typography>
</Stack>
</Stack>
<Button variant="contained" onClick={addCart} disabled={!isStock}>
{!isStock ? 'Sold Out' : 'Add to Cart'}
</Button>
</Stack>
</Grid>
</Grid>
</CardContent>
</MainCard>
)}
</>
);
};
ProductCard.propTypes = {
id: PropTypes.number,
color: PropTypes.string,
name: PropTypes.string,
brand: PropTypes.string,
isStock: PropTypes.bool,
image: PropTypes.string,
description: PropTypes.string,
offerPrice: PropTypes.number,
salePrice: PropTypes.number,
offer: PropTypes.string,
rating: PropTypes.number
};
export default ProductCard;

View File

@@ -1,52 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Grid, Rating, Stack, Typography } from '@mui/material';
// project imports
import Avatar from 'components/@extended/Avatar';
// assets
import { StarFilled, StarOutlined } from '@ant-design/icons';
const avatarImage = require.context('assets/images/users', true);
// ==============================|| PRODUCT DETAILS - REVIEW ||============================== //
const ProductReview = ({ avatar, date, name, rating, review }) => (
<Grid item xs={12}>
<Stack direction="row" spacing={1}>
<Avatar alt={name} src={avatar && avatarImage(`./${avatar}`)} />
<Stack spacing={2}>
<Stack>
<Typography variant="subtitle1" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>
{name}
</Typography>
<Typography variant="caption" color="textSecondary">
{date}
</Typography>
<Rating
size="small"
name="simple-controlled"
value={rating < 4 ? rating + 1 : rating}
icon={<StarFilled style={{ fontSize: 'inherit' }} />}
emptyIcon={<StarOutlined style={{ fontSize: 'inherit' }} />}
precision={0.1}
readOnly
/>
</Stack>
<Typography variant="body2">{review}</Typography>
</Stack>
</Stack>
</Grid>
);
ProductReview.propTypes = {
avatar: PropTypes.string,
date: PropTypes.string,
name: PropTypes.string,
rating: PropTypes.number,
review: PropTypes.string
};
export default ProductReview;

View File

@@ -1,44 +0,0 @@
// material-ui
import { CardContent, Grid, Skeleton, Stack } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
// ===========================|| SKELETON - PRODUCT CARD ||=========================== //
const ProductPlaceholder = () => (
<MainCard content={false} boxShadow>
<Skeleton variant="rectangular" height={220} />
<CardContent sx={{ p: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Skeleton variant="rectangular" height={20} />
</Grid>
<Grid item xs={12}>
<Skeleton variant="rectangular" height={45} />
</Grid>
<Grid item xs={12} sx={{ pt: '8px !important' }}>
<Stack direction="row" alignItems="center" spacing={1}>
<Skeleton variant="rectangular" height={20} width={90} />
<Skeleton variant="rectangular" height={20} width={38} />
</Stack>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Grid container spacing={1}>
<Grid item>
<Skeleton variant="rectangular" height={20} width={40} />
</Grid>
<Grid item>
<Skeleton variant="rectangular" height={17} width={20} />
</Grid>
</Grid>
<Skeleton variant="rectangular" height={32} width={47} />
</Stack>
</Grid>
</Grid>
</CardContent>
</MainCard>
);
export default ProductPlaceholder;

View File

@@ -1,66 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, Chip, Grid, Stack, Typography } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
// assets
import { FallOutlined, RiseOutlined } from '@ant-design/icons';
// ==============================|| STATISTICS - ECOMMERCE CARD ||============================== //
const AnalyticEcommerce = ({ color = 'primary', title, count, percentage, isLoss, extra }) => (
<MainCard contentSX={{ p: 2.25 }}>
<Stack spacing={0.5}>
<Typography variant="h6" color="textSecondary">
{title}
</Typography>
<Grid container alignItems="center">
<Grid item>
<Typography variant="h4" color="inherit">
{count}
</Typography>
</Grid>
{percentage && (
<Grid item>
<Chip
variant="combined"
color={color}
icon={
<>
{!isLoss && <RiseOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
{isLoss && <FallOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
</>
}
label={`${percentage}%`}
sx={{ ml: 1.25, pl: 1 }}
size="small"
/>
</Grid>
)}
</Grid>
</Stack>
<Box sx={{ pt: 2.25 }}>
<Typography variant="caption" color="textSecondary">
You made an extra{' '}
<Typography component="span" variant="caption" sx={{ color: `${color || 'primary'}.main` }}>
{extra}
</Typography>{' '}
this year
</Typography>
</Box>
</MainCard>
);
AnalyticEcommerce.propTypes = {
title: PropTypes.string,
count: PropTypes.string,
percentage: PropTypes.number,
isLoss: PropTypes.bool,
color: PropTypes.string,
extra: PropTypes.string
};
export default AnalyticEcommerce;

View File

@@ -1,56 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { Box, Chip, Stack, Typography } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
// assets
import { RiseOutlined, FallOutlined } from '@ant-design/icons';
// ==============================|| STATISTICS - ECOMMERCE CARD ||============================== //
const AnalyticsDataCard = ({ color = 'primary', title, count, percentage, isLoss, children }) => (
<MainCard content={false}>
<Box sx={{ p: 2.25 }}>
<Stack spacing={0.5}>
<Typography variant="h6" color="textSecondary">
{title}
</Typography>
<Stack direction="row" alignItems="center">
<Typography variant="h4" color="inherit">
{count}
</Typography>
{percentage && (
<Chip
variant="combined"
color={color}
icon={
<>
{!isLoss && <RiseOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
{isLoss && <FallOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
</>
}
label={`${percentage}%`}
sx={{ ml: 1.25, pl: 1 }}
size="small"
/>
)}
</Stack>
</Stack>
</Box>
{children}
</MainCard>
);
AnalyticsDataCard.propTypes = {
title: PropTypes.string,
count: PropTypes.string,
percentage: PropTypes.number,
isLoss: PropTypes.bool,
color: PropTypes.string,
children: PropTypes.node
};
export default AnalyticsDataCard;

View File

@@ -26,13 +26,13 @@ The `DT` design tokens (palette, alpha helpers, `pillFieldSx`, `SoftPaper`, `Acc
**Hard rules when editing components here:**
- **Universal brand colour is `#662582`** (NearlExpress purple — from `themes/theme/default.js` `primary.main`). Every brand surface (page header, dialog header, KPI primary tile, search bars, edit-action buttons, scrollbars) uses this. Gradient pair: `#662582 → #9255AB`.
- **Semantic status palette is distinct** from the brand. Use these for lifecycle indicators only: sky `#0ea5e9`, emerald `#10b981`, amber `#f59e0b`, red `#ef4444`, status-purple `#8b5cf6` (lighter than brand), cyan `#06b6d4`, teal `#14b8a6`, orange `#f97316`, muted `#94a3b8`, indigo `#6366f1` (Accepted status). Don't replace these with brand purple — operators colour-code on them.
- **Universal brand colour is `#C01227`** (Doormile red — from `themes/theme/default.js` `primary.main`). Every brand surface (page header, dialog header, KPI primary tile, search bars, edit-action buttons, scrollbars) uses this. Gradient pair: `#C01227 → #D35968`.
- **Semantic status palette is distinct** from the brand. Use these for lifecycle indicators only: sky `#0ea5e9`, emerald `#10b981`, amber `#f59e0b`, red `#ef4444`, status-purple `#8b5cf6` (lighter than brand), cyan `#06b6d4`, teal `#14b8a6`, orange `#f97316`, muted `#94a3b8`, indigo `#6366f1` (Accepted status). Don't replace these with brand red — operators colour-code on them.
- Don't introduce a colour from `theme.palette` for new surfaces — those are Mantis defaults and don't match the DT system. Use the hex values above directly.
- Border radii: `12` (inner), `16` (card), `999` (pill). No other values.
- Shadows: `DT.shadowSoft` / `DT.shadowMd` / `DT.shadowPop`. No raw `box-shadow` strings.
> Some existing redesigned pages (`deliveries.js`, `Tenants.js`, `clientPricing.js`) still use `#6366f1` as the brand accent — this is a legacy from the first design pass. `customers.js` and the `createorder1` Saved-Address dialog are already on `#662582`. When you next edit one of the legacy pages, migrate it to brand purple in the same PR.
> Some existing redesigned pages (`deliveries.js`, `Tenants.js`, `clientPricing.js`) still use `#6366f1` as the brand accent — this is a legacy from the first design pass. `customers.js` and the `createorder1` Saved-Address dialog are already on `#C01227`. When you next edit one of the legacy pages, migrate it to brand red in the same PR.
---

View File

@@ -1,7 +1,7 @@
// LoaderWithImage.jsx
import React from 'react';
import { Box, CircularProgress } from '@mui/material';
import nelogo from '../../assets/images/logo-sm.png';
import nelogo from '../../assets/images/doormile-mark.png';
export default function LoaderWithImage({ size = 70, imgSize = 40, alt = 'loader' }) {
return (

View File

@@ -42,7 +42,7 @@ MobileCardList.propTypes = {
// Card shell — coloured accent rail on the left, a header slot (status badge /
// title / action buttons), then any field grid / collapse content as children.
export const MobileCard = ({ accent = '#662582', header, footer, selected = false, onClick, children, sx }) => (
export const MobileCard = ({ accent = '#C01227', header, footer, selected = false, onClick, children, sx }) => (
<Paper
elevation={0}
onClick={onClick}

View File

@@ -10,7 +10,7 @@ import { Avatar, Box, Card, CardContent, Skeleton, Stack, Typography } from '@mu
// `color` is a hex accent (defaults to brand purple); `caption` is the small
// muted line under the value (e.g. "96% of total").
export default function StatCard({ title, value, icon, color = '#662582', caption, loading = false }) {
export default function StatCard({ title, value, icon, color = '#C01227', caption, loading = false }) {
return (
<Card sx={{ height: '100%' }}>
<CardContent sx={{ p: { xs: 1.75, md: 2 }, '&:last-child': { pb: { xs: 1.75, md: 2 } } }}>

View File

@@ -1,74 +0,0 @@
import PropTypes from 'prop-types';
import { Chip } from '@mui/material';
// ==============================|| STATUS CHIP (Doormile-style soft badge) ||============================== //
// One consistent soft-filled chip for every lifecycle status across orders,
// deliveries, riders, tenants, invoices. Colour encodes meaning; the chip stays
// quiet (soft bg + readable dark text) so tables don't turn into a rainbow.
// Soft background + readable foreground per semantic tone.
const TONE = {
amber: { bg: '#FEF3C7', fg: '#92400E' },
indigo: { bg: '#E0E7FF', fg: '#3730A3' },
cyan: { bg: '#CFFAFE', fg: '#155E75' },
violet: { bg: '#EDE9FE', fg: '#5B21B6' },
teal: { bg: '#CCFBF1', fg: '#115E59' },
emerald: { bg: '#D1FAE5', fg: '#065F46' },
red: { bg: '#FEE2E2', fg: '#991B1B' },
orange: { bg: '#FFEDD5', fg: '#9A3412' },
sky: { bg: '#E0F2FE', fg: '#075985' },
slate: { bg: '#F1F5F9', fg: '#475569' },
brand: { bg: '#F1E6F7', fg: '#5B1F73' }
};
// Status keyword -> tone + display label.
const MAP = {
pending: { tone: 'amber', label: 'Pending' },
created: { tone: 'sky', label: 'Created' },
assigned: { tone: 'indigo', label: 'Assigned' },
accepted: { tone: 'indigo', label: 'Accepted' },
arrived: { tone: 'cyan', label: 'Arrived' },
picked: { tone: 'violet', label: 'Picked' },
'picked-up':{ tone: 'violet', label: 'Picked Up' },
started: { tone: 'cyan', label: 'Started' },
active: { tone: 'teal', label: 'Active' },
'in-transit': { tone: 'teal', label: 'In Transit' },
delivered: { tone: 'emerald', label: 'Delivered' },
completed: { tone: 'emerald', label: 'Completed' },
skipped: { tone: 'orange', label: 'Skipped' },
failed: { tone: 'red', label: 'Failed' },
cancelled: { tone: 'red', label: 'Cancelled' },
// riders / tenants
online: { tone: 'emerald', label: 'Online' },
offline: { tone: 'slate', label: 'Offline' },
inactive: { tone: 'slate', label: 'Inactive' },
idle: { tone: 'amber', label: 'Idle' },
unknown: { tone: 'slate', label: 'Unknown' },
// invoices
paid: { tone: 'emerald', label: 'Paid' },
unpaid: { tone: 'amber', label: 'Unpaid' },
open: { tone: 'sky', label: 'Open' },
overdue: { tone: 'red', label: 'Overdue' },
prepaid: { tone: 'emerald', label: 'Prepaid' },
cod: { tone: 'amber', label: 'COD' }
};
export default function StatusChip({ status, label, size = 'small', sx }) {
const key = String(status || '').toLowerCase().trim().replace(/\s+/g, '-');
const cfg = MAP[key] || { tone: 'slate', label: status || '—' };
const tone = TONE[cfg.tone] || TONE.slate;
return (
<Chip
size={size}
label={label || cfg.label}
sx={{ bgcolor: tone.bg, color: tone.fg, border: 'none', fontWeight: 600, ...sx }}
/>
);
}
StatusChip.propTypes = {
status: PropTypes.string,
label: PropTypes.node,
size: PropTypes.string,
sx: PropTypes.object
};

View File

@@ -1,22 +0,0 @@
import React from 'react';
import { Stack, Typography, Box } from '@mui/material';
const TitleCard = ({ sx, title, children, starticon }) => {
return (
<Box
sx={{
...sx
}}
>
<Stack direction="row" flexWrap={'wrap'} alignItems="center" justifyContent="space-between" gap={1}>
<Stack>
{starticon && starticon}
<Typography variant="h3">{title}</Typography>
</Stack>
{children}
</Stack>
</Box>
);
};
export default TitleCard;

View File

@@ -1,147 +0,0 @@
import PropTypes from 'prop-types';
import { createContext, useEffect, useReducer } from 'react';
// third-party
import { Chance } from 'chance';
import jwtDecode from 'jwt-decode';
// reducer - state management
import { LOGIN, LOGOUT } from 'store/reducers/actions';
import authReducer from 'store/reducers/auth';
// project import
import Loader from 'components/Loader';
import axios from 'utils/axios';
const chance = new Chance();
// constant
const initialState = {
isLoggedIn: false,
isInitialized: false,
user: null
};
const verifyToken = (serviceToken) => {
if (!serviceToken) {
return false;
}
const decoded = jwtDecode(serviceToken);
/**
* Property 'exp' does not exist on type '<T = unknown>(token: string, options?: JwtDecodeOptions | undefined) => T'.
*/
return decoded.exp > Date.now() / 1000;
};
const setSession = (serviceToken) => {
if (serviceToken) {
localStorage.setItem('serviceToken', serviceToken);
axios.defaults.headers.common.Authorization = `Bearer ${serviceToken}`;
} else {
localStorage.removeItem('serviceToken');
delete axios.defaults.headers.common.Authorization;
}
};
// ==============================|| JWT CONTEXT & PROVIDER ||============================== //
const JWTContext = createContext(null);
export const JWTProvider = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, initialState);
useEffect(() => {
const init = async () => {
console.log(verifyToken)
// try {
// const serviceToken = window.localStorage.getItem('serviceToken');
// if (serviceToken && verifyToken(serviceToken)) {
// setSession(serviceToken);
// const response = await axios.get('/api/account/me');
// const { user } = response.data;
// dispatch({
// type: LOGIN,
// payload: {
// isLoggedIn: true,
// user
// }
// });
// } else {
// dispatch({
// type: LOGOUT
// });
// }
// } catch (err) {
// console.error(err);
// dispatch({
// type: LOGOUT
// });
// }
};
init();
}, []);
const login = async (email, password) => {
const response = await axios.post('/api/account/login', { email, password });
const { serviceToken, user } = response.data;
setSession(serviceToken);
dispatch({
type: LOGIN,
payload: {
isLoggedIn: true,
user
}
});
};
const register = async (email, password, firstName, lastName) => {
// todo: this flow need to be recode as it not verified
const id = chance.bb_pin();
const response = await axios.post('/api/account/register', {
id,
email,
password,
firstName,
lastName
});
let users = response.data;
if (window.localStorage.getItem('users') !== undefined && window.localStorage.getItem('users') !== null) {
const localUsers = window.localStorage.getItem('users');
users = [
...JSON.parse(localUsers),
{
id,
email,
password,
name: `${firstName} ${lastName}`
}
];
}
window.localStorage.setItem('users', JSON.stringify(users));
};
const logout = () => {
setSession(null);
dispatch({ type: LOGOUT });
};
const resetPassword = async () => {};
const updateProfile = () => {};
if (state.isInitialized !== undefined && !state.isInitialized) {
return <Loader />;
}
return <JWTContext.Provider value={{ ...state, login, logout, register, resetPassword, updateProfile }}>{children}</JWTContext.Provider>;
};
JWTProvider.propTypes = {
children: PropTypes.node
};
export default JWTContext;

View File

@@ -22,6 +22,12 @@ const opentoast = (message, color, vertical = 'bottom') => {
});
};
// Notification.requestPermission() re-resolves with the browser's current
// (often already-decided) permission every time this runs. Without a guard,
// any repeat call — e.g. from a remount — re-shows the same permanent,
// non-auto-dismissing toast and they stack up. Only warn once per tab.
let notificationWarningShown = false;
// ===================== Generate FCM Token =====================
export const generateToken = async () => {
try {
@@ -29,7 +35,10 @@ export const generateToken = async () => {
dispatch(setFcmPermission(permission));
if (permission !== 'granted') {
opentoast('Enable notifications to receive OTP, alerts, and updates', 'error');
if (!notificationWarningShown) {
notificationWarningShown = true;
opentoast('Enable notifications to receive OTP, alerts, and updates', 'error');
}
return;
}

View File

@@ -1,16 +0,0 @@
import { useContext } from 'react';
// auth provider
import AuthContext from 'contexts/JWTContext';
// ==============================|| AUTH HOOKS ||============================== //
const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error('context must be use inside provider');
return context;
};
export default useAuth;

View File

@@ -0,0 +1,96 @@
import { useEffect, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useDispatch } from 'react-redux';
import { clearFcmToken } from 'store/reducers/fcmSlice';
import { logoutUser } from 'store/reducers/loginUserSlice';
import {
ABSOLUTE_SESSION_TIMEOUT_MS,
ACTIVITY_STORAGE_KEY,
AUTH_PRESENCE_KEY,
INACTIVITY_TIMEOUT_MS,
SESSION_START_STORAGE_KEY,
isSessionActive,
markActivity,
markSessionStart,
performSessionLogout
} from 'utils/session';
const ACTIVITY_EVENTS = ['mousemove', 'mousedown', 'keydown', 'scroll', 'touchstart', 'wheel'];
const ACTIVITY_WRITE_THROTTLE_MS = 5000;
const IDLE_CHECK_INTERVAL_MS = 15000;
// Two independent timers, both enforced from localStorage so every tab on the
// origin agrees:
// - INACTIVITY_TIMEOUT_MS: logs out after 15 minutes with no interaction, so
// a laptop left unlocked doesn't leave the console open indefinitely.
// - ABSOLUTE_SESSION_TIMEOUT_MS: a hard 30-minute cap since login, even if
// the user has been continuously active, so localStorage (auth keys, FCM
// token, cached zone list) never lingers on disk longer than that.
// A logout in one tab (auth key cleared) is picked up by the others via the
// 'storage' event.
const useInactivityLogout = () => {
const queryClient = useQueryClient();
const dispatch = useDispatch();
const lastWriteRef = useRef(0);
useEffect(() => {
const doLogout = () => performSessionLogout({ queryClient, dispatch, clearFcmToken, logoutUser });
const handleActivity = () => {
const now = Date.now();
if (now - lastWriteRef.current < ACTIVITY_WRITE_THROTTLE_MS) return;
lastWriteRef.current = now;
markActivity();
};
const handleStorage = (event) => {
if (event.key === AUTH_PRESENCE_KEY && !event.newValue) {
doLogout();
}
};
// Guards against the browser restoring a cached (bfcache) copy of a
// protected page via the back/forward button after logout happened.
const handlePageShow = (event) => {
if (event.persisted && !isSessionActive()) {
window.location.replace('/login');
}
};
if (isSessionActive()) {
if (!localStorage.getItem(ACTIVITY_STORAGE_KEY)) markActivity();
// Sessions that were already open before this feature shipped won't have
// a start time yet — give them a fresh 30-minute window instead of
// treating them as already expired.
if (!localStorage.getItem(SESSION_START_STORAGE_KEY)) markSessionStart();
}
ACTIVITY_EVENTS.forEach((eventName) => window.addEventListener(eventName, handleActivity, { passive: true }));
window.addEventListener('storage', handleStorage);
window.addEventListener('pageshow', handlePageShow);
const intervalId = setInterval(() => {
if (!isSessionActive()) return;
const lastActivity = Number(localStorage.getItem(ACTIVITY_STORAGE_KEY)) || Date.now();
if (Date.now() - lastActivity >= INACTIVITY_TIMEOUT_MS) {
doLogout();
return;
}
const sessionStart = Number(localStorage.getItem(SESSION_START_STORAGE_KEY)) || Date.now();
if (Date.now() - sessionStart >= ABSOLUTE_SESSION_TIMEOUT_MS) {
doLogout();
}
}, IDLE_CHECK_INTERVAL_MS);
return () => {
ACTIVITY_EVENTS.forEach((eventName) => window.removeEventListener(eventName, handleActivity));
window.removeEventListener('storage', handleStorage);
window.removeEventListener('pageshow', handlePageShow);
clearInterval(intervalId);
};
}, [queryClient, dispatch]);
};
export default useInactivityLogout;

View File

@@ -1,29 +0,0 @@
import { useState } from 'react';
// ==============================|| CARD - PAGINATION ||============================== //
export default function usePagination(data, itemsPerPage) {
const [currentPage, setCurrentPage] = useState(1);
const maxPage = Math.ceil(data.length / itemsPerPage);
function currentData() {
const begin = (currentPage - 1) * itemsPerPage;
const end = begin + itemsPerPage;
return data.slice(begin, end);
}
function next() {
setCurrentPage((currentPage) => Math.min(currentPage + 1, maxPage));
}
function prev() {
setCurrentPage((currentPage) => Math.max(currentPage - 1, 1));
}
function jump(page) {
const pageNumber = Math.max(1, page);
setCurrentPage(() => Math.min(pageNumber, maxPage));
}
return { next, prev, jump, currentData, currentPage, maxPage };
}

View File

@@ -1,18 +0,0 @@
import { useEffect, useRef } from 'react';
// ==============================|| ELEMENT REFERENCE HOOKS ||============================== //
const useScriptRef = () => {
const scripted = useRef(true);
useEffect(
() => () => {
scripted.current = false;
},
[]
);
return scripted;
};
export default useScriptRef;

View File

@@ -1,4 +1,5 @@
import logger from './utils/logger';
import axios from 'axios';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
@@ -19,6 +20,11 @@ import { store } from 'store';
import reportWebVitals from './reportWebVitals';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const storedToken = localStorage.getItem('token');
if (storedToken) {
axios.defaults.headers.common.Authorization = `Bearer ${storedToken}`;
}
const container = document.getElementById('root');
const root = createRoot(container);
const queryClient = new QueryClient({

View File

@@ -27,7 +27,7 @@ import Transitions from 'components/@extended/Transitions';
import useConfig from 'hooks/useConfig';
import { dispatch, useSelector } from 'store';
import { activeItem } from 'store/reducers/menu';
import { MenuOrientation, ThemeMode } from 'config';
import { MenuOrientation } from 'config';
// assets
import { BorderOutlined, DownOutlined, UpOutlined, RightOutlined } from '@ant-design/icons';
@@ -234,13 +234,27 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
...(drawerOpen && {
'&:hover': {
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter'
bgcolor: '#7b1fa2'
bgcolor: '#fff',
'& .MuiTypography-root': {
color: '#C01227 !important'
},
'& svg, & .anticon': {
color: '#C01227 !important'
}
},
'&.Mui-selected': {
bgcolor: 'transparent',
color: iconSelectedColor,
'&:hover': { color: iconSelectedColor, bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'transparent' }
'&:hover': {
bgcolor: '#fff',
color: '#C01227',
'& .MuiTypography-root': {
color: '#C01227 !important'
},
'& svg, & .anticon': {
color: '#C01227 !important'
}
}
}
}),
...(!drawerOpen && {
@@ -273,20 +287,23 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
alignItems: 'center',
justifyContent: 'center',
'&:hover': {
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'secondary.light' : 'secondary.lighter'
bgcolor: '#7b1fa2',
color: 'white'
bgcolor: '#fff',
'& svg': {
color: '#C01227 !important'
}
}
}),
...(!drawerOpen &&
selected === menu.id && {
bgcolor: 'primary.light',
color: 'primary.main',
'&:hover': {
bgcolor: '#7b1fa2',
color: 'primary.main'
bgcolor: 'primary.light',
color: 'primary.main',
'&:hover': {
bgcolor: '#fff',
'& svg': {
color: '#C01227 !important'
}
})
}
})
}}
>
{menuIcon}

View File

@@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
import { forwardRef, useEffect, useState } from 'react';
import { forwardRef, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
@@ -41,7 +41,7 @@ const NavItem = ({ item, level }) => {
const isSelected = openItem.findIndex((id) => id === item.id) > -1;
const itemIcon = item.icon ? (
<Icon style={{ fontSize: drawerOpen ? '1rem' : '1.25rem', color: isSelected ? '#662582' : '#fff' }} />
<Icon style={{ fontSize: drawerOpen ? '1rem' : '1.25rem', color: isSelected ? '#C01227' : '#fff' }} />
) : (
false
);
@@ -97,87 +97,96 @@ const NavItem = ({ item, level }) => {
onClick={() => {
// dispatch(setSelectedMenu(item));
}}
sx={{
zIndex: 1201,
pl: drawerOpen ? `${level * 28}px` : 1.5,
py: !drawerOpen && level === 1 ? 1.25 : 1,
...(drawerOpen && {
'&:hover': {
bgcolor: '#7b1fa2'
},
'&.Mui-selected': {
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter',
borderRight: `2px solid ${theme.palette.primary.main}`,
color: iconSelectedColor,
sx={{
zIndex: 1201,
pl: drawerOpen ? `${level * 28}px` : 1.5,
py: !drawerOpen && level === 1 ? 1.25 : 1,
...(drawerOpen && {
'&:hover': {
color: iconSelectedColor,
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter'
}
}
}),
...(!drawerOpen && {
bgcolor: '#662582',
'&:hover': {
bgcolor: '#662582'
},
'&.Mui-selected': {
'&:hover': {
bgcolor: 'transparent'
},
bgcolor: 'transparent'
}
})
}}
{...(downLG && {
onClick: () => {
dispatch(openDrawer(false));
}
})}
>
{itemIcon && (
<ListItemIcon
sx={{
minWidth: 28,
...(!drawerOpen && {
borderRadius: 1.5,
width: 36,
height: 36,
alignItems: 'center',
justifyContent: 'center',
'&:hover': {
bgcolor: '#7b1fa2'
bgcolor: '#fff',
'& .MuiTypography-root': {
color: '#C01227 !important'
},
'& svg, & .anticon': {
color: '#C01227 !important'
}
}),
...(!drawerOpen &&
isSelected && {
},
'&.Mui-selected': {
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter',
borderRight: `2px solid ${theme.palette.primary.main}`,
color: iconSelectedColor,
'&:hover': {
color: iconSelectedColor,
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter'
}
}
}),
...(!drawerOpen && {
bgcolor: '#C01227',
'&:hover': {
bgcolor: '#C01227'
},
'&.Mui-selected': {
'&:hover': {
bgcolor: 'transparent'
},
bgcolor: 'transparent'
}
})
}}
{...(downLG && {
onClick: () => {
dispatch(openDrawer(false));
}
})}
>
{itemIcon && (
<ListItemIcon
sx={{
minWidth: 28,
...(!drawerOpen && {
borderRadius: 1.5,
width: 36,
height: 36,
alignItems: 'center',
justifyContent: 'center',
'&:hover': {
bgcolor: '#fff',
'& svg, & .anticon': {
color: '#C01227 !important'
}
}
}),
...(!drawerOpen &&
isSelected && {
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'primary.900' : 'primary.lighter',
'&:hover': {
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'primary.darker' : 'primary.lighter'
}
})
}}
>
{itemIcon}
</ListItemIcon>
)}
{(drawerOpen || (!drawerOpen && level !== 1)) && (
<ListItemText
primary={
<Typography variant="h6" sx={{ color: isSelected ? iconSelectedColor : textColor, whiteSpace: 'nowrap' }}>
{item.title}
</Typography>
}
/>
)}
{(drawerOpen || (!drawerOpen && level !== 1)) && item.chip && (
<Chip
color={item.chip.color}
variant={item.chip.variant}
size={item.chip.size}
label={item.chip.label}
avatar={item.chip.avatar && <Avatar>{item.chip.avatar}</Avatar>}
/>
)}
}}
>
{itemIcon}
</ListItemIcon>
)}
{(drawerOpen || (!drawerOpen && level !== 1)) && (
<ListItemText
primary={
<Typography variant="h6" sx={{ color: isSelected ? iconSelectedColor : textColor, whiteSpace: 'nowrap' }}>
{item.title}
</Typography>
}
/>
)}
{(drawerOpen || (!drawerOpen && level !== 1)) && item.chip && (
<Chip
color={item.chip.color}
variant={item.chip.variant}
size={item.chip.size}
label={item.chip.label}
avatar={item.chip.avatar && <Avatar>{item.chip.avatar}</Avatar>}
/>
)}
</ListItemButton>
</Tooltip>
) : (
@@ -229,11 +238,11 @@ const NavItem = ({ item, level }) => {
}),
...(!drawerOpen &&
isSelected && {
bgcolor: 'transparent',
'&:hover': {
bgcolor: 'transparent'
}
})
bgcolor: 'transparent',
'&:hover': {
bgcolor: 'transparent'
}
})
}}
>
{itemIcon}
@@ -254,11 +263,11 @@ const NavItem = ({ item, level }) => {
}),
...(!drawerOpen &&
isSelected && {
bgcolor: 'transparent',
'&:hover': {
bgcolor: 'transparent'
}
})
bgcolor: 'transparent',
'&:hover': {
bgcolor: 'transparent'
}
})
}}
>
<Dot size={4} color={isSelected ? 'primary' : 'secondary'} />

View File

@@ -1,99 +0,0 @@
import PropTypes from 'prop-types';
import { forwardRef, useEffect } from 'react';
import { Link } from 'react-router-dom';
// material-ui
import { useTheme } from '@mui/material/styles';
import { useMediaQuery, Avatar, Chip, ListItemButton, ListItemText, Typography } from '@mui/material';
// project imports
import { ThemeMode } from 'config';
import { dispatch, useSelector } from 'store';
import { activeComponent, openComponentDrawer } from 'store/reducers/menu';
// ==============================|| NAVIGATION - LIST ITEM ||============================== //
const NavItem = ({ item }) => {
const theme = useTheme();
const matchesMD = useMediaQuery(theme.breakpoints.down('md'));
const menu = useSelector((state) => state.menu);
const { openComponent } = menu;
let itemTarget = '_self';
if (item.target) {
itemTarget = '_blank';
}
let listItemProps = { component: forwardRef((props, ref) => <Link {...props} to={item.url} target={itemTarget} ref={ref} />) };
if (item?.external) {
listItemProps = { component: 'a', href: item.url, target: itemTarget };
}
const itemHandler = (id) => {
dispatch(activeComponent({ openComponent: id }));
if (matchesMD) dispatch(openComponentDrawer({ componentDrawerOpen: false }));
};
// active menu item on page load
useEffect(() => {
const currentIndex = document.location.pathname
.toString()
.split('/')
.findIndex((id) => id === item.id);
if (currentIndex > -1) {
dispatch(activeComponent({ openComponent: item.id }));
}
// eslint-disable-next-line
}, []);
const textColor = theme.palette.mode === ThemeMode.DARK ? 'grey.400' : 'text.primary';
const iconSelectedColor = theme.palette.mode === ThemeMode.DARK ? 'text.primary' : 'primary.main';
return (
<ListItemButton
{...listItemProps}
disabled={item.disabled}
onClick={() => itemHandler(item.id)}
selected={openComponent === item.id}
sx={{
pl: 4,
py: 1,
mb: 0.5,
'&:hover': {
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter'
},
'&.Mui-selected': {
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter',
borderRight: `2px solid ${theme.palette.primary.main}`,
'&:hover': {
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter'
}
}
}}
>
<ListItemText
primary={
<Typography variant="h6" sx={{ color: openComponent === item.id ? iconSelectedColor : textColor }}>
{item.title}
</Typography>
}
/>
{item.chip && (
<Chip
color={item.chip.color}
variant={item.chip.variant}
size={item.chip.size}
label={item.chip.label}
avatar={item.chip.avatar && <Avatar>{item.chip.avatar}</Avatar>}
/>
)}
</ListItemButton>
);
};
NavItem.propTypes = {
item: PropTypes.object
};
export default NavItem;

View File

@@ -10,8 +10,9 @@ import DrawerHeaderStyled from './DrawerHeaderStyled';
import { MenuOrientation } from 'config';
import useConfig from 'hooks/useConfig';
import logo from 'assets/images/logo-nearle9.png'
import logo1 from 'assets/images/logo-sm1.png'
import logo from 'assets/images/doormile-logo.png'
import logo1 from 'assets/images/doormile-mark.png'
// ==============================|| DRAWER HEADER ||============================== //
const DrawerHeader = ({ open }) => {
@@ -33,19 +34,15 @@ const DrawerHeader = ({ open }) => {
paddingLeft: isHorizontal ? { xs: '24px', lg: '0' } : open ? '24px' : 0
}}
>
{/* <Logo isIcon={!open} sx={{ width: open ? 'auto' : 35, height: 35 }} /> */}
{(open) &&
<img src={logo}
// width='160px'
// height='45px'
// width='170px'
alt='logo'/>
style={{ height: '29px', width: 'auto' }}
alt='Doormile'/>
}
{(!open) &&
<img src={logo1}
width='40px'
alt='logo'/>
<img src={logo1}
width='50px'
alt='Doormile'/>
}
</DrawerHeaderStyled>
);

View File

@@ -15,7 +15,7 @@ const openedMixin = (theme) => ({
}),
overflowX: 'hidden',
boxShadow: theme.palette.mode === ThemeMode.DARK ? theme.customShadows.z1 : 'none',
backgroundColor:'#662582',
backgroundColor:'#C01227',
});
const closedMixin = (theme) => ({
@@ -27,7 +27,7 @@ const closedMixin = (theme) => ({
width: theme.spacing(7.5),
borderRight: 'none',
boxShadow: theme.customShadows.z1,
backgroundColor:'#662582',
backgroundColor:'#C01227',
});
// ==============================|| DRAWER - MINI STYLED ||============================== //

View File

@@ -55,7 +55,7 @@ const MainDrawer = ({ window }) => {
borderRight: `1px solid ${theme.palette.divider}`,
backgroundImage: 'none',
boxShadow: 'inherit',
bgcolor:'#662582'
bgcolor:'#C01227'
}
}}
>

View File

@@ -8,7 +8,6 @@ import { AppBar, Box, ClickAwayListener, Paper, Popper, Toolbar } from '@mui/mat
import Profile from './Profile';
import IconButton from 'components/@extended/IconButton';
import Transitions from 'components/@extended/Transitions';
import { ThemeMode } from 'config';
// assets
import { MoreOutlined } from '@ant-design/icons';

View File

@@ -24,7 +24,6 @@ import {
import MainCard from 'components/MainCard';
import IconButton from 'components/@extended/IconButton';
import Transitions from 'components/@extended/Transitions';
import { ThemeMode } from 'config';
// assets
import { BellOutlined, CheckCircleOutlined, GiftOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons';
@@ -90,7 +89,7 @@ const Notification = () => {
// color="primary"
sx={{
"& .MuiBadge-badge": {
color: "#662582",
color: "#C01227",
backgroundColor: "white"
}
}}

View File

@@ -7,14 +7,10 @@ import { List, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'
// assets
import { EditOutlined, LogoutOutlined, CommentOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router';
import { useDispatch } from 'react-redux';
import { clearFcmToken } from 'store/reducers/fcmSlice';
import { logoutUser } from 'store/reducers/loginUserSlice';
// ==============================|| HEADER PROFILE - PROFILE TAB ||============================== //
const ProfileTab = ({ handleLogout }) => {
const dispatch = useDispatch();
const [selectedIndex, setSelectedIndex] = useState(0);
const navigate = useNavigate();
const handleListItemClick = (event, index) => {
@@ -48,18 +44,7 @@ const ProfileTab = ({ handleLogout }) => {
</ListItemIcon>
<ListItemText primary="Billing" />
</ListItemButton> */}
<ListItemButton
selected={selectedIndex === 3}
// onClick={handleLogout}
onClick={() => {
handleLogout();
dispatch(clearFcmToken()); // ✅ dispatch the action
dispatch(logoutUser()); // ✅ dispatch logout user as initial state
}}
// onClick={()=>{
// navigate('/login')
// }}
>
<ListItemButton selected={selectedIndex === 3} onClick={handleLogout}>
<ListItemIcon>
<LogoutOutlined />
</ListItemIcon>

View File

@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import { useRef, useState } from 'react';
import { useNavigate } from 'react-router';
import { useQueryClient } from '@tanstack/react-query';
// material-ui
import { useTheme } from '@mui/material/styles';
@@ -13,8 +13,6 @@ import Avatar from 'components/@extended/Avatar';
import MainCard from 'components/MainCard';
import Transitions from 'components/@extended/Transitions';
import IconButton from 'components/@extended/IconButton';
import { ThemeMode } from 'config';
// import useAuth from 'hooks/useAuth';
// assets
@@ -23,6 +21,7 @@ import { LogoutOutlined, UserOutlined } from '@ant-design/icons';
import { clearFcmToken } from 'store/reducers/fcmSlice';
import { useDispatch } from 'react-redux';
import { logoutUser } from 'store/reducers/loginUserSlice';
import { performSessionLogout } from 'utils/session';
// tab panel wrapper
function TabPanel({ children, value, index, ...other }) {
@@ -50,30 +49,11 @@ function a11yProps(index) {
const Profile = () => {
const theme = useTheme();
const navigate = useNavigate();
const dispatch = useDispatch();
const queryClient = useQueryClient();
// const { logout, user } = useAuth();
const handleLogout = async () => {
try {
// await logout();
// navigate(`/login`, {
// state: {
// from: ''
// }
// });
localStorage.removeItem('firstname');
localStorage.removeItem('appuserid');
localStorage.removeItem('authname');
localStorage.removeItem('roleid');
localStorage.removeItem('tenantid');
localStorage.clear();
navigate('/login');
} catch (err) {
console.error(err);
}
const handleLogout = () => {
performSessionLogout({ queryClient, dispatch, clearFcmToken, logoutUser });
};
const anchorRef = useRef(null);
@@ -101,13 +81,7 @@ const Profile = () => {
<ButtonBase
sx={{
p: 0.25,
// bgcolor: open ? iconBackColorOpen : 'transparent',
borderRadius: 1
// '&:hover': { bgcolor: theme.palette.mode === ThemeMode.DARK ? 'secondary.light' : 'secondary.lighter' },
// '&:focus-visible': {
// outline: `2px solid ${theme.palette.secondary.dark}`,
// outlineOffset: 2
// }
}}
aria-label="open profile"
ref={anchorRef}
@@ -173,16 +147,7 @@ const Profile = () => {
</Grid>
<Grid item>
<Tooltip title="Logout">
<IconButton
size="large"
sx={{ color: 'text.primary' }}
// onClick={handleLogout}>
onClick={() => {
handleLogout();
dispatch(clearFcmToken()); // ✅ dispatch the action dispatch(logoutUser()); // ✅ dispatch logout user as initial state dispatch(logoutUser()); // ✅ dispatch logout user as initial state
dispatch(logoutUser()); // ✅ dispatch logout user as initial state
}}
>
<IconButton size="large" sx={{ color: 'text.primary' }} onClick={handleLogout}>
<LogoutOutlined />
</IconButton>
</Tooltip>

View File

@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useState } from 'react';
// material-ui
import {

View File

@@ -9,7 +9,7 @@ import AppBarStyled from './AppBarStyled';
import HeaderContent from './HeaderContent';
import IconButton from 'components/@extended/IconButton';
import { MenuOrientation, ThemeMode } from 'config';
import { MenuOrientation } from 'config';
import useConfig from 'hooks/useConfig';
import { dispatch, useSelector } from 'store';
import { openDrawer } from 'store/reducers/menu';
@@ -66,7 +66,7 @@ const Header = () => {
zIndex: 1200,
width: isHorizontal ? '100%' : drawerOpen ? 'calc(100% - 260px)' : { xs: '100%', lg: 'calc(100% - 60px)' },
// boxShadow: theme.customShadows.z1
bgcolor:'#662582'
bgcolor:'#C01227'
}
};

View File

@@ -1,10 +1,8 @@
// third-party
import { FormattedMessage } from 'react-intl';
import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlined';
import ReceiptOutlinedIcon from '@mui/icons-material/ReceiptOutlined';
import MopedOutlinedIcon from '@mui/icons-material/MopedOutlined';
import NearMeOutlinedIcon from '@mui/icons-material/NearMeOutlined';
import { TbListDetails } from 'react-icons/tb';
// assets
import {
@@ -24,7 +22,6 @@ import {
TeamOutlined,
MailOutlined,
ImportOutlined,
BarChartOutlined,
MoneyCollectOutlined,
FileDoneOutlined
} from '@ant-design/icons';
@@ -47,8 +44,6 @@ const icons = {
TeamOutlined,
MailOutlined,
ImportOutlined,
BarChartOutlined,
ReceiptOutlinedIcon,
NearMeOutlinedIcon,
DirectionsBikeOutlinedIcon,
MopedOutlinedIcon,
@@ -59,10 +54,17 @@ const icons = {
const nearle = {
id: 'nearle_Pages',
title: <FormattedMessage id="Nearle" />,
title: <FormattedMessage id="Doormile" />,
icon: icons.FileDoneOutlined,
type: 'group',
children: [
{
id: 'dashboard',
title: <FormattedMessage id="dashboard" />,
type: 'item',
url: '/nearle/dashboard',
icon: icons.DashboardOutlined
},
{
id: 'dispatch',
title: <FormattedMessage id="dispatch" />,
@@ -70,6 +72,13 @@ const nearle = {
url: '/nearle/dispatch',
icon: icons.DirectionsBikeOutlinedIcon
},
{
id: 'hubs',
title: <FormattedMessage id="hubs" />,
type: 'item',
url: '/nearle/hubs',
icon: icons.DeploymentUnitOutlined
},
{
id: 'orders',
title: <FormattedMessage id="orders" />,
@@ -112,59 +121,6 @@ const nearle = {
type: 'item',
url: '/nearle/riders',
icon: DirectionsBikeOutlinedIcon
},
{
id: 'reports',
title: <FormattedMessage id="reports" />,
type: 'collapse',
icon: icons.BarChartOutlined,
children: [
{
id: 'reports',
title: <FormattedMessage id="ordersummary" />,
type: 'item',
url: '/nearle/reports/orderssummary',
icon: TbListDetails
},
{
id: 'ordersdetails',
title: <FormattedMessage id="ordersdetails" />,
type: 'item',
url: '/nearle/reports/ordersdetails',
icon: icons.DashboardOutlined
// target: true
},
{
id: 'riderssummary',
title: <FormattedMessage id="riderssummary" />,
type: 'item',
url: '/nearle/reports/riderssummary',
icon: DirectionsBikeOutlinedIcon
// target: true
},
{
id: 'riderslogs',
title: <FormattedMessage id="riderslogs" />,
type: 'item',
url: '/nearle/reports/riderslogs',
icon: DirectionsBikeOutlinedIcon
// target: true
},
{
id: 'profitability',
title: <FormattedMessage id="profitability" />,
type: 'item',
url: '/nearle/reports/profitability',
icon: icons.BarChartOutlined
}
]
},
{
id: 'invoice',
title: <FormattedMessage id="invoice" />,
type: 'item',
url: '/nearle/invoice',
icon: icons.ReceiptOutlinedIcon
}
]
};

View File

@@ -4,82 +4,111 @@ import dayjs from 'dayjs';
const userid = localStorage.getItem('userid');
// ==============================|| getRiderPeriodicLogs ||============================== //
// Returns the rider's latest periodic log entry — battery, GPS, status, current
// order. Used by the Rider Info modal on the Dispatch page.
// Returns the miler's latest known position/status. Doormile has no periodic-log
// stream yet (that lands with EMQX) — this reads the live snapshot off /admin/milers/:id
// instead. battery/speed have no Doormile equivalent yet, so they're always null.
export const getRiderPeriodicLogs = async (userid) => {
const url = `${process.env.REACT_APP_URL}/utils/getriderperiodiclogs${userid ? `?userid=${userid}` : ''}`;
const response = await axios.get(url);
if (response.data && response.data.status) return response.data.data;
return null;
if (!userid) return null;
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/milers/${userid}`);
const m = response.data?.data;
if (!m) return null;
return {
userid: m.userid,
lat: m.currentlat,
lon: m.currentlon,
status: m.availabilitystatus,
battery: null,
speed: null
};
};
// ==============================|| fetchAppLocations||============================== //
export const fetchAppLocations = async () => {
const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
const updatedLocations = [
...response.data.details,
{ locationname: 'All', applocationid: 0 } // Add your new object here
];
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/hubs`);
const hubs = (response.data?.data || []).map((h) => ({
...h,
applocationid: h.hubid,
locationname: h.hubname
}));
return [...hubs, { locationname: 'All', applocationid: 0 }];
};
return updatedLocations;
// ==============================|| fetchHubs / createHub / updateHub (hubs) ||============================== //
export const fetchHubs = async () => {
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/hubs`);
return res.data?.data || [];
};
export const createHub = async (body) => {
const res = await axios.post(`${process.env.REACT_APP_URL}/admin/hubs`, body);
return res.data;
};
export const updateHub = async (id, body) => {
const res = await axios.patch(`${process.env.REACT_APP_URL}/admin/hubs/${id}`, body);
return res.data;
};
// ==============================|| fetchPercentageData (orders) ||============================== //
export const fetchPercentageData = async ({ queryKey }) => {
const [, appId, startdate, enddate, tenantid, locationid] = queryKey;
const response = await axios.get(
`${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}`
const statuses = ['Pending_Pickup', 'Miler_Assigned', 'Delivered', 'Cancelled'];
const [pending, assigned, delivered, cancelled] = await Promise.all(
statuses.map((status) => axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status } }))
);
const details = response.data.details;
const created = pending.data.total + assigned.data.total + delivered.data.total + cancelled.data.total;
return {
created: details.created.toString(),
uncoveredOrders: details.pending.toString(),
coveredOrders: details.delivered.toString(),
cancelled: details.cancelled.toString(),
percentage1: (Math.round((details.created / details.total) * 100) || 0).toString(),
percentage2: (Math.round((details.pending / details.total) * 100) || 0).toString(),
percentage3: (Math.round((details.delivered / details.total) * 100) || 0).toString(),
percentage4: (Math.round((details.cancelled / details.total) * 100) || 0).toString()
created: created.toString(),
uncoveredOrders: pending.data.total.toString(),
coveredOrders: delivered.data.total.toString(),
cancelled: cancelled.data.total.toString(),
percentage1: (Math.round((created / created) * 100) || 0).toString(),
percentage2: (Math.round((pending.data.total / created) * 100) || 0).toString(),
percentage3: (Math.round((delivered.data.total / created) * 100) || 0).toString(),
percentage4: (Math.round((cancelled.data.total / created) * 100) || 0).toString()
};
};
// ===================================================== || getTenants || =====================================================
export const getTenants = async (appId) => {
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenants/?applocationid=${appId}&status=active`);
if (response.data.status) {
let arr = [];
response.data.details.map((val) => {
arr.push({
...val,
label: `${val.tenantname}`
});
});
return arr;
}
// appId (hub/zone) has no equivalent filter on Doormile's CRM clients endpoint —
// kept as a parameter only so existing call sites (deliveries.js, orders.js,
// reports/*) don't need to change their queryFn wiring.
export const getTenants = async () => {
const response = await axios.get(`${process.env.REACT_APP_URL}/crm/clients`);
return (response.data?.data || []).map((val) => ({
...val,
tenantid: val.clientid,
tenantname: val.clientname,
label: val.clientname
}));
};
// ============================================= || gettenantlocations (branches) || =============================================
export const gettenantlocations = async (appId) => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${appId}`);
return res.data.details;
} catch (err) {
console.log('gettenantlocations', err);
}
// No Doormile equivalent to tenant branches — call sites use this for a
// branch/location dropdown that doesn't apply to Doormile's client model.
export const gettenantlocations = async () => {
return [];
};
// ==============================|| fetchorderscount (orders) ||============================== //
export const fetchorderscount = async ({ queryKey }) => {
// eslint-disable-next-line no-unused-vars
const [, appId, startdate, enddate, currentStatus, tenantid, locationid] = queryKey;
const url = `${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}&status=${currentStatus}`;
const [all, pending, delivered, cancelled] = await Promise.all([
axios.get(`${process.env.REACT_APP_URL}/admin/bookings`),
axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: 'Pending_Pickup' } }),
axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: 'Delivered' } }),
axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: 'Cancelled' } })
]);
const response = await axios.get(url);
return response.data.details;
return {
created: all.data.total,
pending: pending.data.total,
delivered: delivered.data.total,
cancelled: cancelled.data.total
};
};
// ==============================|| fetchOrders (orders) ||============================== //
@@ -95,46 +124,93 @@ export const fetchorderscount = async ({ queryKey }) => {
// return response.data.details.map((val, i) => ({ ...val, sno: i + 1 }));
// };
export const fetchOrders = async ({ pageParam = 1, queryKey }) => {
// eslint-disable-next-line no-unused-vars
const [, appId, currentStatus, debouncedSearch, startdate, enddate, rowsPerPage, tenantid, locationid] = queryKey;
const url = `${process.env.REACT_APP_URL}/orders/tenant/getorders/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&keyword=${debouncedSearch}&pageno=${pageParam}&pagesize=${rowsPerPage}`;
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, {
params: {
status: currentStatus === 'All' ? undefined : currentStatus,
keyword: debouncedSearch,
pageno: pageParam,
pagesize: rowsPerPage
}
});
const response = await axios.get(url);
// TEMPORARY: backend fix in progress — pageno/pagesize are currently
// ignored server-side and it returns every matching record on every call.
// Slice client-side so infinite scroll doesn't dump the whole dataset on
// page 1. Safe to remove once the backend honours pagination.
const all = response.data.data || [];
const size = Number(rowsPerPage);
const start = (pageParam - 1) * size;
const rows = all.slice(start, start + size);
return {
rows: response.data.details,
nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined
rows,
nextPage: start + size < all.length ? pageParam + 1 : undefined
};
};
// ==============================|| fetchPaymentType (orders) ||============================== //
export const fetchPaymentType = async () => {
const { data } = await axios.get(`${process.env.REACT_APP_URL}/utils/getapptypes/?tag=paymentmode`);
return data.details.map((val) => ({
...val,
label: val.typename
}));
};
// No Doormile payment-types endpoint exists yet — static defaults.
export const fetchPaymentType = async () => [
{ apptypeid: 1, typename: 'Cash', label: 'Cash' },
{ apptypeid: 2, typename: 'Online', label: 'Online' },
{ apptypeid: 3, typename: 'COD', label: 'COD' }
];
// ==============================|| fetchRidersList (orders) ||============================== //
export const fetchRidersList = async ({ queryKey }) => {
try {
const [, appId] = queryKey; // Extract appId from queryKey
const { data } = await axios.get(`${process.env.REACT_APP_URL}/partners/getriders/?applocationid=${appId}`);
console.log('data', data);
const response = data?.details
? data?.details.map((val) => ({
...val,
label: `${val.firstname} ${val.lastname} | ${val.contactno}`
}))
: [];
return response;
} catch (err) {
OpenToast(err.message, 'error', 2000);
throw err; // 🔥 REQUIRED
}
export const fetchRidersList = async () => {
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
return (res.data?.data || []).map((m) => ({
...m,
userid: m.userid,
label: `${m.displayname} | ${m.phone}`,
firstname: m.displayname,
lastname: '',
contactno: m.phone
}));
};
// ==============================|| Doormile assignment (new) ||============================== //
// NOTE: createOptimisationDeliveries / reconcileSteps / fetchBatchEfficiency /
// finalCreatedeliveries / createAutomationDeliveries below still call the legacy
// NearlExpress optimiser (routes.workolik.com / routemate.workolik.com /
// jupiter.nearle.app) and are wired into Dispatch.js, Preview.js, OrdersPreview.js,
// orders.js and deliveries.js. Per the "don't touch Dispatch/Preview internals —
// Phase 3" instruction, they're left as-is rather than renamed/repointed, since
// doing so would silently break those pages' running mutations without a
// corresponding page rewrite. The Doormile-native replacements are added here
// instead, so Phase 3 can cut individual call sites over one at a time.
// assignBooking — triggers Doormile's AI assignment engine for one booking via
// the internal-key-gated endpoint. SECURITY: X-Internal-Key is a static secret
// baked into this client bundle — anyone can extract it from the shipped JS and
// call this endpoint directly. Prefer routing this through the already-
// authenticated admin JWT (see autoAssignBooking below) once the backend
// supports it; keep this only if /internal/bookings/:id/reassign truly must
// stay key-gated rather than JWT-gated.
export const assignBooking = async (bookingId) => {
const response = await axios.post(
`${process.env.REACT_APP_URL}/internal/bookings/${bookingId}/reassign`,
{},
{ headers: { 'X-Internal-Key': process.env.REACT_APP_INTERNAL_KEY } }
);
return response.data;
};
// autoAssignBooking — same intent as assignBooking but authenticated with the
// operator's own admin JWT instead of a shared static key.
export const autoAssignBooking = async (bookingId) => {
const response = await axios.post(`${process.env.REACT_APP_URL}/hub/bookings/${bookingId}/auto-assign`, {});
return response.data;
};
// fetchDashboardStats — hub-level stats, replaces the workolik batch-efficiency call.
export const fetchDashboardStats = async () => {
const response = await axios.get(`${process.env.REACT_APP_URL}/hub/dashboard`);
return response.data;
};
// ==============================|| createOptimisationDeliveries (orders) Arrange the order ||============================== //
@@ -145,14 +221,9 @@ export const createOptimisationDeliveries = async (deliveryData) => {
return response.data;
};
// ==============================|| reconcileSteps (Preview - validate rider/order step assignments) ||============================== //
export const reconcileSteps = async ({ riders }) => {
const response = await axios.post(
`https://routes.workolik.com/api/v1/optimization/reconcile-steps`,
{ riders }
);
return response.data;
};
// No Doormile equivalent — made a no-op so CLAUDE.md's "always reconcile before
// createdeliveries" hard constraint keeps holding trivially until Phase 3.
export const reconcileSteps = async (data) => data;
// ==============================|| fetchBatchEfficiency (Dispatch - Analysis view) ||============================== //
// Calls POST /api/v1/batch/efficiency with a JSON body { batch, tenant_id }.
@@ -223,34 +294,19 @@ export const createAutomationDeliveries = async (variables) => {
};
// ==============================|| notifyRider (orders / deliveries) ||============================== //
// Doormile sends miler FCM notifications automatically from the Go backend on
// status changes, so there's no equivalent client-triggered endpoint. No-op.
export const notifyRider = async () => ({ success: true });
export const notifyRider = async (riderToken) => {
if (!riderToken) {
throw new Error('Invalid rider token');
}
console.log('notify rider called');
console.log('riderToken', riderToken);
const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, {
token: riderToken,
notification: {
title: 'NearleXpress',
body: 'Orders have been placed for delivery. Kindly accept and process deliveries',
sound: 'ring',
image: ''
}
});
// ==============================|| cancelOrder (orders) ||============================== //
export const cancelOrder = async (bookingid) => {
const response = await axios.post(`${process.env.REACT_APP_URL}/admin/bookings/${bookingid}/cancel`);
return response.data;
};
// ==============================|| cancelOrder (orders) ||============================== //
export const cancelOrder = async (orderheaderid) => {
const response = await axios.put(`${process.env.REACT_APP_URL}/orders/updateorder`, {
orderheaderid: orderheaderid,
orderstatus: 'cancelled',
cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss')
});
// ==============================|| updateBookingStatus (bookings) ||============================== //
export const updateBookingStatus = async (bookingid, status) => {
const response = await axios.put(`${process.env.REACT_APP_URL}/admin/bookings/${bookingid}/status`, { status });
return response.data;
};
// ==============================|| cancelMultipleOrder (orders) ||============================== //
@@ -270,97 +326,105 @@ export const cancelMultipleOrder = async (orderlist) => {
};
// ==============================|| fetchDeliveries (deliveries) ||============================== //
// NOTE: NearlExpress "deliveries" tabs are keyed off statuses
// (pending/accepted/arrived/picked/active/delivered/cancelled/skipped) that
// don't exist in Doormile's booking status vocabulary (Pending_Pickup,
// Miler_Assigned, Pickup_Scheduled, At_Customer, Picked_Up, At_Hub, Delivered,
// Cancelled, Assignment_Failed). This passes currentStatus straight through —
// deliveries.js's own tab definitions still need updating to Doormile statuses
// (flagged, not guessed).
export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
// eslint-disable-next-line no-unused-vars
let [, appId, userid, currentStatus, startdate, enddate, rowsPerPage, searchword, tenantid, locationid, riderid] = queryKey;
currentStatus = currentStatus == 'All' ? 'all' : currentStatus;
const url =
appId === 0
? `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?appuserid=${userid}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`
: `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?applocationid=${appId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
const response = await axios.get(url);
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, {
params: {
status: currentStatus === 'all' || currentStatus === 'All' ? undefined : currentStatus,
keyword: searchword,
pageno: pageParam,
pagesize: rowsPerPage
}
});
// TEMPORARY: same backend pagination bug as fetchOrders — slice client-side
// until pageno/pagesize are honoured server-side.
const all = response.data.data || [];
const size = Number(rowsPerPage);
const start = (pageParam - 1) * size;
const rows = all.slice(start, start + size);
return {
rows: response.data.details,
nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined
rows,
nextPage: start + size < all.length ? pageParam + 1 : undefined
};
};
// ==============================|| fetchPercentageAPI (deliveries) ||============================== //
export const fetchPercentageAPI = async (appId) => {
const url = `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}`;
const response = await axios.get(url);
const data = response.data.details;
// UNVERIFIED: status param + pagesize=1-for-count-only pattern hasn't been
// confirmed against the real backend the way bookings/clients/milers were —
// implemented per explicit instruction, flag if it 404s/403s like
// /admin/customers did.
export const fetchPercentageAPI = async () => {
const statuses = ['Pending_Pickup', 'Miler_Assigned', 'Picked_Up', 'Delivered', 'Cancelled'];
const results = await Promise.all(
statuses.map((s) => axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: s, pagesize: 1 } }))
);
const [pending, assigned, picked, delivered, cancelled] = results.map((r) => r.data?.total || 0);
const total = pending + assigned + picked + delivered + cancelled;
return {
coveredOrders: data.delivered.toString(),
cancelledOrders: data.cancelled.toString(),
uncoveredOrders: data.pending.toString(),
assignedOrders: data.accepted.toString(),
createdOrders: data.created.toString(),
closedOrders: data.delivered.toString(),
pickedOrders: data.picked.toString(),
percentage1: (Math.round((data.pending / data.total) * 100) || 0).toString(),
percentage2: (Math.round((data.accepted / data.total) * 100) || 0).toString(),
percentage3: (Math.round((data.picked / data.total) * 100) || 0).toString(),
percentage4: (Math.round((data.delivered / data.total) * 100) || 0).toString()
coveredOrders: delivered.toString(),
cancelledOrders: cancelled.toString(),
uncoveredOrders: pending.toString(),
assignedOrders: assigned.toString(),
createdOrders: total.toString(),
closedOrders: delivered.toString(),
pickedOrders: picked.toString(),
total: total.toString(),
percentage1: (Math.round((pending / total) * 100) || 0).toString(),
percentage2: (Math.round((assigned / total) * 100) || 0).toString(),
percentage3: (Math.round((picked / total) * 100) || 0).toString(),
percentage4: (Math.round((delivered / total) * 100) || 0).toString()
};
};
// ==============================|| fetchCountAPI (deliveries) ||============================== //
export const fetchCountAPI = async (appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid) => {
const url =
appId == 0
? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?appuserid=${userid}&fromdate=${startdate}&todate=${enddate}`
: `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
const response = await axios.get(url);
const data = response.data.details;
export const fetchCountAPI = async () => {
const statuses = ['Pending_Pickup', 'Miler_Assigned', 'Pickup_Scheduled', 'At_Customer', 'Picked_Up', 'At_Hub', 'Delivered', 'Cancelled'];
const results = await Promise.all(
statuses.map((s) => axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: s, pagesize: 1 } }))
);
const [pending, assigned, scheduled, atcustomer, picked, athub, delivered, cancelled] = results.map((r) => r.data?.total || 0);
return {
total: data.total,
uncoveredLength: data.pending,
assignedLength: data.accepted,
arrivedLength: data.arrived,
pickedLength: data.picked,
activeLength: data.active,
coveredLength: data.delivered,
cancelLength: data.cancelled,
skippedLength: data.skipped
total: pending + assigned + scheduled + atcustomer + picked + athub + delivered + cancelled,
uncoveredLength: pending,
assignedLength: assigned,
arrivedLength: scheduled,
pickedLength: picked,
activeLength: atcustomer + athub,
coveredLength: delivered,
cancelLength: cancelled,
skippedLength: 0
};
};
// ==============================|| cancelDeliveryAPI (deliveries) ||============================== //
export const cancelDeliveryAPI = async (selectedRow, cancelFeed) => {
const payload = {
deliveryid: selectedRow.deliveryid,
orderheaderid: selectedRow.orderheaderid,
orderstatus: 'cancelled',
canceltime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
feedback: cancelFeed
};
const response = await axios.put(`${process.env.REACT_APP_URL}/deliveries/updatedelivery`, payload);
export const cancelDeliveryAPI = async (selectedRow) => {
const response = await axios.post(`${process.env.REACT_APP_URL}/admin/bookings/${selectedRow.bookingid}/cancel`);
return response.data;
};
// ==============================|| getorderdetails (deliveries) ||============================== //
export const getorderdetails = async (orderHeaderid) => {
const response = await axios.get(`${process.env.REACT_APP_URL}/orders/getorderdetails?orderheaderid=${orderHeaderid}`);
export const getorderdetails = async (bookingid) => {
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings/${bookingid}`);
return response.data;
};
// ==============================|| changeRiderAPI (deliveries) ||============================== //
export const changeRiderAPI = async (selectedRider, selectedRow) => {
console.log('selectedRider', selectedRider);
console.log('selectedRow', selectedRow);
return axios.put(`${process.env.REACT_APP_URL}/deliveries/updatedelivery`, {
userid: selectedRider.userid,
deliveryid: selectedRow.deliveryid,
orderheaderid: selectedRow.orderheaderid,
orderstatus: 'pending',
assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss')
return axios.post(`${process.env.REACT_APP_URL}/hub/bookings/${selectedRow.bookingid}/assign-miler`, {
mileruserid: selectedRider.userid
});
};
// ==============================|| updateDeliveryAPI (deliveries) ||============================== //
@@ -370,152 +434,158 @@ export const updateDeliveryAPI = async (orderData) => {
};
// ==============================|| getalltenants (tenants) ||============================== //
// Doormile's /crm/clients has no documented status/keyword/page filter params
// (unlike the old /tenants/getalltenants), so this fetches the full client
// list and filters + paginates client-side.
export const getalltenants = async ({ queryKey }) => {
const [, appId, debouncedSearch, status, page, rowsPerPage] = queryKey;
const [, , debouncedSearch, status, page, rowsPerPage] = queryKey;
try {
let url = `${process.env.REACT_APP_URL
}/tenants/getalltenants/?status=${status}&applocationid=${appId}&keyword=${debouncedSearch}&pageno=${page + 1
}&pagesize=${rowsPerPage}&moduleid=6`;
const response = await axios.get(url);
return response.data.details; // return only data, keep it clean
const response = await axios.get(`${process.env.REACT_APP_URL}/crm/clients`);
let clients = (response.data?.data || []).map((c) => ({
...c,
tenantid: c.clientid,
tenantname: c.clientname
}));
if (status) {
clients = clients.filter((c) => (c.status || '').toLowerCase() === status.toLowerCase());
}
if (debouncedSearch) {
const kw = debouncedSearch.toLowerCase();
clients = clients.filter((c) => (c.clientname || '').toLowerCase().includes(kw) || (c.email || '').toLowerCase().includes(kw));
}
const start = page * rowsPerPage;
return clients.slice(start, start + rowsPerPage);
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
return null;
}
};
// ==============================|| gettenantsummary (tenants) ||============================== //
export const gettenantsummary = async ({ queryKey }) => {
const [, appId] = queryKey;
export const gettenantsummary = async () => {
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantsummary/?moduleid=6&applocationid=${appId}`);
return response.data.summary; // return only data, keep it clean
const response = await axios.get(`${process.env.REACT_APP_URL}/crm/clients`);
const clients = response.data?.data || [];
const countOf = (s) => clients.filter((c) => (c.status || '').toLowerCase() === s).length;
return { active: countOf('active'), pending: countOf('pending'), inactive: countOf('inactive') };
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
return null;
}
};
// ==============================|| getpricinglist (tenants) ||============================== //
export const getpricinglist = async ({ queryKey }) => {
const [, appId] = queryKey;
export const getpricinglist = async () => {
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/getpricinglist/?moduleid=6&applocationid=${appId}`);
return response.data.summary; // return only data, keep it clean
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/pricing`);
return response.data?.data || [];
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
OpenToast(err.message, 'error', 2000);
return [];
}
};
// ==============================|| getallpricing (clientPricing) ||============================== //
export const getallpricing = async ({ queryKey }) => {
const [, appId] = queryKey;
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/utils/getallpricing/?applocationid=${appId}`);
return response.data.details || [];
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return [];
}
};
export const getallpricing = getpricinglist;
// ==============================|| getcustomersummary (customers) ||============================== //
export const getcustomersummary = async ({ queryKey }) => {
const [, appId] = queryKey;
export const getcustomersummary = async () => {
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/customers/getcustomersummary?applocationid=${appId}`);
return response.data.summary;
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/customers`);
const customers = response.data?.data || [];
const total = response.data?.total || customers.length;
return { Total: total };
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
OpenToast(err.message, 'error', 2000);
return null;
}
};
// ==============================|| getallcustomers (customers) ||============================== //
// Backend rows key the customer id as `appcustomerid` — aliased to `userid`
// here so any call site still expecting the old field name keeps working.
export const getallcustomers = async ({ pageParam = 1, queryKey }) => {
const [, appId, debouncedSearch, rowsPerPage] = queryKey;
const [, , debouncedSearch, rowsPerPage] = queryKey;
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/customers/getallcustomers/`, {
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/customers`, {
params: {
applocationid: appId,
keyword: debouncedSearch,
keyword: debouncedSearch || undefined,
pageno: pageParam,
pagesize: rowsPerPage
pagesize: rowsPerPage || 20
}
});
const customers = (response.data?.data || []).map((c) => ({
...c,
userid: c.appcustomerid
}));
return {
data: response.data.details || [],
nextPage: response.data.details?.length === rowsPerPage ? pageParam + 1 : undefined
data: customers,
nextPage: customers.length === Number(rowsPerPage) ? pageParam + 1 : undefined
};
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
OpenToast(err.message, 'error', 2000);
throw err; // IMPORTANT for React Query
}
};
// ==============================|| fetchAllRiders (riders) ||============================== //
export const fetchAllRiders = async ({ pageParam = 1, queryKey }) => {
// Doormile's /admin/milers has no documented pagination or search/status
// filter params, unlike the old /partners/getallriders. Fetches the full miler
// list and filters client-side; single page (nextPage always undefined) until
// the backend adds server-side paging.
export const fetchAllRiders = async ({ queryKey }) => {
try {
// eslint-disable-next-line no-unused-vars
const [, appId, debouncedSearch, tabvalue] = queryKey;
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
let milers = res.data?.data || [];
const url = `${process.env.REACT_APP_URL
}/partners/getallriders/?applocationid=${appId}&pageno=${pageParam}&pagesize=${20}&keyword=${debouncedSearch}&status=${(tabvalue == 0 || tabvalue == 2) ? '' : 'Active'
}`;
const res = await axios.get(url);
return {
details: res.data.details,
nextPage: res.data.details.length === 20 ? pageParam + 1 : undefined
};
if (debouncedSearch) {
const kw = debouncedSearch.toLowerCase();
milers = milers.filter(
(m) => (m.displayname || '').toLowerCase().includes(kw) || (m.phone || '').includes(debouncedSearch)
);
}
if (tabvalue != 0 && tabvalue != 2) {
milers = milers.filter((m) => m.availabilitystatus === 'Available');
}
return { details: milers, nextPage: undefined };
} catch (err) {
console.log('fetchAllRiders err', err.message);
return [];
return { details: [], nextPage: undefined };
}
};
// ==============================|| getallridersummary (riders) ||============================== //
export const getallridersummary = async ({ queryKey }) => {
export const getallridersummary = async () => {
try {
const [, appId, tabvalue] = queryKey;
const response = await axios.get(
`${process.env.REACT_APP_URL}/partners/getallridersummary/?applocationid=${appId}&status=${tabvalue == 0 ? '' : 'Active'}`
);
return response.data.details;
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
const milers = response.data?.data || [];
const active = milers.filter((m) => m.availabilitystatus === 'Available').length;
return { total: milers.length, active, inactive: milers.length - active };
} catch (err) {
console.log('getallridersummary err', err.message);
return [];
return { total: 0, active: 0, inactive: 0 };
}
};
// ==============================|| fetchRiders (riders), active riders ||============================== //
// Not currently imported anywhere (riders.js uses fetchAllRiders) — updated for
// consistency in case a future page picks it up.
export const fetchRiders = async (appId) => {
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
return response.data?.data || [];
};
export const fetchRiders = async ({ pageParam = 1, queryKey }) => {
try {
const [, appId, debouncedSearch] = queryKey;
const url = `${process.env.REACT_APP_URL
}/partners/getriders/?applocationid=${appId}&pageno=${pageParam}&pagesize=${20}&keyword=${debouncedSearch}`;
const res = await axios.get(url);
return {
details: res.data.details,
nextPage: res.data.details.length === 20 ? pageParam + 1 : undefined
};
} catch (err) {
console.log('fetchRiders err', err.message);
return [];
}
// ==============================|| fetchMilerDetail (riders) ||============================== //
export const fetchMilerDetail = async (milerid) => {
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/milers/${milerid}`);
return response.data.data;
};
// ==============================|| getriderstatus (riders)||============================== //
@@ -524,29 +594,6 @@ export const getriderstatus = async () => {
return response.data.data;
};
// ==============================|| getreportsummary (orders summary)||============================== //
export const getreportsummary = async ({ queryKey }) => {
console.log('queryKey for getreportsummary', queryKey);
const [appId, tenantid, locationid, startdate, enddate] = queryKey;
const response = await axios.get(
`${process.env.REACT_APP_URL}/deliveries/getreportsummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}`
);
console.log('getreportsummary', response.data.details);
return response.data.details;
};
// ==============================|| getreportlocationsummary (orders summary)||============================== //
export const getreportlocationsummary = async ({ queryKey }) => {
console.log('queryKey for getreportlocationsummary', queryKey);
const [appId, tenantid, locationid, startdate, enddate] = queryKey;
const response = await axios.get(
`${process.env.REACT_APP_URL}/deliveries/getreportlocationsummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}`
);
console.log('getreportlocationsummary', response.data.details);
return response.data.details;
};
// ==============================|| fetchorderdetails (orders detail)||============================== //
export const fetchorderdetails = async ({ queryKey }) => {
console.log('queryKey of fetchorderdetails', queryKey);
@@ -567,119 +614,27 @@ export const fetchorderdetails = async ({ queryKey }) => {
return detailsWithSNo;
};
// ==============================|| getriderbydelivery (orders detail)||============================== //
export const getriderbydelivery = async (startdate, enddate, appId = 0, tenantid = 0, locationid = 0) => {
// const [, startdate, enddate] = queryKey;
try {
const response = await axios.get(
`${process.env.REACT_APP_URL}/deliveries/getriderbydelivery/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}`
);
return response.data.details || [];
} catch (err) {
console.log('getriderbydelivery', err.message);
return [];
}
};
// ==============================|| fetchCount (orders detail)||============================== //
export const fetchCount = async ({ queryKey }) => {
console.log('queryKey of fetchCount', queryKey);
const [appId, startdate, enddate] = queryKey;
let url =
appId == 0
? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?fromdate=${startdate}&todate=${enddate}`
: `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}`;
const response = await axios.get(url);
return response.data.details;
};
// ==============================|| fetchRidersSummary (riders summary)||============================== //
export const fetchRidersSummary = async ({ queryKey }) => {
console.log('queryKey for fetchRidersSummary', queryKey);
const [, appId, startdate, enddate] = queryKey;
const response = await axios.get(
`${process.env.REACT_APP_URL}/deliveries/getridersummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}`
);
console.log('fetchRidersSummary', response.data.details);
return response.data.details;
};
// ==============================|| fetchLocations (orders summary))||============================== //
// Not needed as a separate concept in Doormile — returns the hub list instead.
export const fetchLocations = async () => {
const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getpartners`);
const updatedLocations = [
...response.data.details,
{ partnername: 'All', partnerid: -1 } // Add your new object here
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/hubs`);
return [
...(response.data?.data || []).map((h) => ({ ...h, partnername: h.hubname, partnerid: h.hubid })),
{ partnername: 'All', partnerid: -1 }
];
console.log('fetchLocations', updatedLocations);
return updatedLocations;
};
// ==============================|| fetchinvoiceinsight (Invoice)||============================== //
export const fetchinvoiceinsight = async () => {
const insightResponse = await axios.get(`${process.env.REACT_APP_URL}/invoice/getinvoiceinsight`);
return insightResponse.data.details;
};
// ==============================|| fetchdeliverylist (Invoice)||============================== //
export const fetchdeliverylist = async ({ queryKey }) => {
const [billStatus] = queryKey;
const deliveyResponse = await axios.get(`${process.env.REACT_APP_URL}/invoice/getallinvoice/?billstatus=${billStatus}`);
console.log('fetchdeliverylist', deliveyResponse.data.details);
return deliveyResponse.data.details;
};
// ==============================|| fetchRidersLogs (RiderLogs)||============================== //
// No Doormile equivalent yet (real-time miler location stream lands with EMQX).
export const fetchRidersLogs = async () => [];
export const fetchRidersLogs = async ({ queryKey }) => {
const [appId, startdate, riderSearch = ''] = queryKey;
const riderLogsResponse = await axios.get(
`${process.env.REACT_APP_URL2}/partners/getriderlogs/?applocationid=${appId}&fromdate=${startdate || ''}&todate=${startdate}&keyword=${riderSearch || ''
} `
);
console.log('fetchRidersLogs', riderLogsResponse.data.details);
return riderLogsResponse.data.details;
};
// ==============================|| getorders (Locations)||============================== //
// fetchOrders.js
export const fetchOrders1 = async ({ pageParam = 1, queryKey }) => {
const [, tenantid, locationid, status, startdate, enddate, searchword, rowsPerPage] = queryKey;
const res = await axios.get(
`${process.env.REACT_APP_URL}/orders/tenant/getorders/?tenantid=${tenantid}&locationid=${locationid}&status=${status}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}`
);
return {
details: res.data.details,
nextPage: res.data.details.length === rowsPerPage ? pageParam + 1 : undefined
};
};
// ==============================|| getusers (viewProfile)||============================== //
export const getusers = async () => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/users/getusers/?configid=9&userid=${userid}`);
return res.data.details;
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/users`);
return res.data?.data || [];
} catch (err) {
console.log('getusers', err.message);
}
};
// ==============================|| getallriders (order)||============================== //
export const getallriders = async () => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/partners/getallriders?partnerid=64`);
return res.data.details;
} catch (err) {
console.log('getallriders', err.message);
return [];
}
};

View File

@@ -1,57 +0,0 @@
import { Link } from 'react-router-dom';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Box, Button, Grid, Divider, Typography, useMediaQuery } from '@mui/material';
// project import
import useAuth from 'hooks/useAuth';
import AnimateButton from 'components/@extended/AnimateButton';
import AuthWrapper from 'sections/auth/AuthWrapper';
// ================================|| CHECK MAIL ||================================ //
const CheckMail = () => {
const theme = useTheme();
const matchDownSM = useMediaQuery(theme.breakpoints.down('sm'));
const { isLoggedIn } = useAuth();
return (
<AuthWrapper>
<Grid container spacing={3}>
<Grid item xs={12}>
<Box sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
<Typography variant="h3">Hi, Check Your Mail</Typography>
<Typography color="secondary" sx={{ mb: 0.5, mt: 1.25 }}>
We have sent a password recover instructions to your email.
</Typography>
</Box>
</Grid>
<Grid item xs={12}>
<AnimateButton>
<Button
component={Link}
to={isLoggedIn ? '/auth/login' : '/login'}
disableElevation
fullWidth
size="large"
type="submit"
variant="contained"
color="primary"
>
Sign in
</Button>
</AnimateButton>
</Grid>
<Grid item xs={12}>
<Divider>
<Typography variant={matchDownSM ? 'subtitle1' : 'h5'}>Sign up with</Typography>
</Divider>
</Grid>
</Grid>
</AuthWrapper>
);
};
export default CheckMail;

View File

@@ -1,29 +0,0 @@
// material-ui
import { Grid, Stack, Typography } from '@mui/material';
// project import
import AuthWrapper from 'sections/auth/AuthWrapper';
import AuthCodeVerification from 'sections/auth/auth-forms/AuthCodeVerification';
// ================================|| CODE VERIFICATION ||================================ //
const CodeVerification = () => (
<AuthWrapper>
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack spacing={1}>
<Typography variant="h3">Enter Verification Code</Typography>
<Typography color="secondary">We send you on mail.</Typography>
</Stack>
</Grid>
<Grid item xs={12}>
<Typography>We`ve send you code on jone. ****@company.com</Typography>
</Grid>
<Grid item xs={12}>
<AuthCodeVerification />
</Grid>
</Grid>
</AuthWrapper>
);
export default CodeVerification;

View File

@@ -1,41 +0,0 @@
import { Link } from 'react-router-dom';
// material-ui
import { Grid, Stack, Typography } from '@mui/material';
// project import
import useAuth from 'hooks/useAuth';
import AuthWrapper from 'sections/auth/AuthWrapper';
import AuthForgotPassword from 'sections/auth/auth-forms/AuthForgotPassword';
// ================================|| FORGOT PASSWORD ||================================ //
const ForgotPassword = () => {
const { isLoggedIn } = useAuth();
return (
<AuthWrapper>
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
<Typography variant="h3">Forgot Password</Typography>
<Typography
component={Link}
to={isLoggedIn ? '/auth/login' : '/login'}
variant="body1"
sx={{ textDecoration: 'none' }}
color="primary"
>
Back to Login
</Typography>
</Stack>
</Grid>
<Grid item xs={12}>
<AuthForgotPassword />
</Grid>
</Grid>
</AuthWrapper>
);
};
export default ForgotPassword;

View File

@@ -1,41 +0,0 @@
import { Link } from 'react-router-dom';
// material-ui
import { Grid, Stack, Typography } from '@mui/material';
// project import
import useAuth from 'hooks/useAuth';
import AuthWrapper from 'sections/auth/AuthWrapper';
import AuthLogin from 'sections/auth/auth-forms/AuthLogin';
// ================================|| LOGIN ||================================ //
const Login = () => {
const { isLoggedIn } = useAuth();
return (
<AuthWrapper>
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
<Typography variant="h3">Login</Typography>
<Typography
component={Link}
to={isLoggedIn ? '/auth/register' : '/register'}
variant="body1"
sx={{ textDecoration: 'none' }}
color="primary"
>
Don&apos;t have an account?
</Typography>
</Stack>
</Grid>
<Grid item xs={12}>
<AuthLogin />
</Grid>
</Grid>
</AuthWrapper>
);
};
export default Login;

View File

@@ -1,41 +0,0 @@
import { Link } from 'react-router-dom';
// material-ui
import { Grid, Stack, Typography } from '@mui/material';
// project import
import useAuth from 'hooks/useAuth';
import AuthWrapper from 'sections/auth/AuthWrapper';
import FirebaseRegister from 'sections/auth/auth-forms/AuthRegister';
// ================================|| REGISTER ||================================ //
const Register = () => {
const { isLoggedIn } = useAuth();
return (
<AuthWrapper>
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
<Typography variant="h3">Sign up</Typography>
<Typography
component={Link}
to={isLoggedIn ? '/auth/login' : '/login'}
variant="body1"
sx={{ textDecoration: 'none' }}
color="primary"
>
Already have an account?
</Typography>
</Stack>
</Grid>
<Grid item xs={12}>
<FirebaseRegister />
</Grid>
</Grid>
</AuthWrapper>
);
};
export default Register;

View File

@@ -1,26 +0,0 @@
// material-ui
import { Grid, Stack, Typography } from '@mui/material';
// project import
import AuthWrapper from 'sections/auth/AuthWrapper';
import AuthResetPassword from 'sections/auth/auth-forms/AuthResetPassword';
// ================================|| RESET PASSWORD ||================================ //
const ResetPassword = () => (
<AuthWrapper>
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack sx={{ mb: { xs: -0.5, sm: 0.5 } }} spacing={1}>
<Typography variant="h3">Reset Password</Typography>
<Typography color="secondary">Please choose your new password</Typography>
</Stack>
</Grid>
<Grid item xs={12}>
<AuthResetPassword />
</Grid>
</Grid>
</AuthWrapper>
);
export default ResetPassword;

View File

@@ -1,24 +0,0 @@
import React, { useRef, useEffect } from 'react';
const CtrlK = () => {
useEffect(() => {
const handleKeyPress = (event) => {
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
textFieldRef.current.focus();
}
if (event.key === 'Escape' && document.activeElement === textFieldRef.current) {
textFieldRef.current.blur();
}
};
document.addEventListener('keydown', handleKeyPress);
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
}, [textFieldRef]);
};
export default CtrlK;

View File

View File

@@ -1,20 +0,0 @@
// material-ui
import { Typography } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
// ==============================|| SAMPLE PAGE ||============================== //
const SamplePage = () => (
<MainCard title="Sample Card">
<Typography variant="body2">
Lorem ipsum dolor sit amen, consenter nipissing eli, sed do elusion tempos incident ut laborers et doolie magna alissa. Ut enif ad
minim venice, quin nostrum exercitation illampu laborings nisi ut liquid ex ea commons construal. Duos aube grue dolor in reprehended
in voltage veil esse colum doolie eu fujian bulla parian. Exceptive sin ocean cuspidate non president, sunk in culpa qui officiate
descent molls anim id est labours.
</Typography>
</MainCard>
);
export default SamplePage;

View File

@@ -5,7 +5,7 @@ import { useTheme } from '@mui/material/styles';
import { useMediaQuery, Box, Button, Grid, Stack, TextField, Typography } from '@mui/material';
// third party
import { useTimer } from 'react-timer-hook';
const { useTimer } = require('react-timer-hook');
// assets
import coming from 'assets/images/maintenance/coming-soon.png';

View File

@@ -1,45 +0,0 @@
import { Grid, useMediaQuery, useTheme } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import CircularLoader from 'components/CircularLoader';
import Loader from 'components/Loader';
import MainCard from 'components/MainCard';
import { getusers } from 'pages/api/api';
const ViewProfile = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const {
data: userdata,
isLoading,
isError,
error
} = useQuery({
queryKey: ['getuser'],
queryFn: getusers
});
return (
<>
{isLoading && (
<>
<Loader />
<CircularLoader />
</>
)}
<MainCard sx={{ p: { xs: 1.5, md: 3 } }}>
<Grid container spacing={isMobile ? 2 : 3}>
<Grid item xs={12} sm={6} md={4}>
{userdata?.firstname}
</Grid>
<Grid item xs={12} sm={6} md={4}>
{' '}
</Grid>
<Grid item xs={12} sm={6} md={4}>
{' '}
</Grid>
</Grid>
</MainCard>
</>
);
};
export default ViewProfile;

View File

@@ -0,0 +1,308 @@
import { useParams, useNavigate } from 'react-router-dom';
import { Avatar, Box, Button, Chip, Grid, Paper, Stack, Step, StepLabel, Stepper, Typography, useMediaQuery } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
MdArrowBack,
MdLocalShipping,
MdPerson,
MdPhone,
MdLocationOn,
MdTwoWheeler,
MdStar,
MdOutlineSmartToy,
MdOutlineAssignmentInd,
MdOutlineCancel
} from 'react-icons/md';
import Loader from 'components/Loader';
import { OpenToast } from 'components/third-party/OpenToast';
import { getorderdetails, autoAssignBooking, cancelOrder } from 'pages/api/api';
const DT = {
radiusCard: 16,
textPrimary: '#0f172a',
textSecondary: '#64748b',
textMuted: '#94a3b8',
borderSubtle: '#e2e8f0',
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc'
};
const a = (c, suffix) => `${c}${suffix}`;
const tint = (c) => a(c, '08');
const soft = (c) => a(c, '18');
const edge = (c) => a(c, '55');
const BRAND = '#C01227';
// Ordered status progression used for the stepper. Doormile's real status
// vocabulary — Assignment_Failed and Cancelled are terminal/off-path states
// shown separately rather than as stepper nodes.
const STATUS_STEPS = ['Pending_Pickup', 'Miler_Assigned', 'Pickup_Scheduled', 'At_Customer', 'Picked_Up', 'At_Hub', 'Delivered'];
const STEP_LABELS = {
Pending_Pickup: 'Pending Pickup',
Miler_Assigned: 'Miler Assigned',
Pickup_Scheduled: 'Pickup Scheduled',
At_Customer: 'At Customer',
Picked_Up: 'Picked Up',
At_Hub: 'At Hub',
Delivered: 'Delivered'
};
const InfoRow = ({ icon: Icon, label, value, color = BRAND }) => (
<Stack direction="row" alignItems="flex-start" spacing={1.5}>
<Avatar sx={{ width: 32, height: 32, bgcolor: soft(color), color }}>
<Icon size={16} />
</Avatar>
<Box sx={{ minWidth: 0 }}>
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.4 }}>
{label}
</Typography>
<Typography sx={{ fontWeight: 600, color: DT.textPrimary, wordBreak: 'break-word' }}>{value || '—'}</Typography>
</Box>
</Stack>
);
const BookingDetail = () => {
const { id } = useParams();
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const queryClient = useQueryClient();
const { data, isLoading, isError } = useQuery({
queryKey: ['bookingDetail', id],
queryFn: () => getorderdetails(id),
enabled: Boolean(id)
});
// Confirmed shape (booking id 5): bookingid, bookingreference, status,
// pickupaddress, deliveryaddress, assignedmileruserid, createdat,
// bookingparcels (nested parcels), serviceoptions, payments.
const booking = data?.data || data || {};
const reassignMutation = useMutation({
mutationFn: () => autoAssignBooking(id),
onSuccess: () => {
OpenToast('Reassignment triggered', 'success', 2000);
queryClient.invalidateQueries({ queryKey: ['bookingDetail', id] });
},
onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
});
const cancelMutation = useMutation({
mutationFn: () => cancelOrder(id),
onSuccess: () => {
OpenToast('Booking cancelled', 'success', 2000);
queryClient.invalidateQueries({ queryKey: ['bookingDetail', id] });
},
onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
});
if (isLoading) return <Loader />;
const status = booking.status || 'Pending_Pickup';
const isTerminal = ['cancelled', 'assignment_failed'].includes(String(status).toLowerCase());
const activeStepIdx = STATUS_STEPS.indexOf(status);
const parcels = booking.bookingparcels || booking.parcels || [];
const miler = booking.miler || booking.assignedmiler || null;
const agentDecision = booking.agent_decision_id ? booking.agentdecision || booking.agent_decision : null;
return (
<>
<Stack direction="row" alignItems="center" spacing={1.5} sx={{ mb: 2 }}>
<Button startIcon={<MdArrowBack size={16} />} onClick={() => navigate('/nearle/orders')} sx={{ color: DT.textSecondary, textTransform: 'none', fontWeight: 700 }}>
Back to Bookings
</Button>
</Stack>
<Paper
elevation={0}
sx={{
p: { xs: 2, md: 3 },
borderRadius: `${DT.radiusCard}px`,
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint('#D35968')} 100%)`,
border: '1px solid',
borderColor: DT.borderSubtle,
mb: 2
}}
>
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" alignItems={{ xs: 'flex-start', sm: 'center' }} spacing={2}>
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 48, height: 48, bgcolor: BRAND, color: '#fff' }}>
<MdLocalShipping size={24} />
</Avatar>
<Box>
<Typography variant="h3">{booking.bookingreference || `Booking #${id}`}</Typography>
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
{booking.createdat ? new Date(booking.createdat).toLocaleString() : '—'}
</Typography>
</Box>
</Stack>
<Chip
label={STEP_LABELS[status] || status}
sx={{ fontWeight: 800, bgcolor: isTerminal ? soft('#ef4444') : soft(BRAND), color: isTerminal ? '#ef4444' : BRAND, border: `1px solid ${edge(isTerminal ? '#ef4444' : BRAND)}` }}
/>
</Stack>
</Paper>
{isError && (
<Paper elevation={0} sx={{ p: 2, mb: 2, borderRadius: 2, border: `1px solid ${edge('#ef4444')}`, bgcolor: tint('#ef4444') }}>
<Typography sx={{ color: '#ef4444', fontWeight: 600 }}>Could not load full booking details showing whatever came back.</Typography>
</Paper>
)}
{/* Status timeline */}
{!isTerminal && (
<Paper elevation={0} sx={{ p: { xs: 2, md: 3 }, mb: 2, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff' }}>
<Stepper activeStep={activeStepIdx} alternativeLabel={!isMobile} orientation={isMobile ? 'vertical' : 'horizontal'}>
{STATUS_STEPS.map((s) => (
<Step key={s}>
<StepLabel
sx={{
'& .MuiStepIcon-root.Mui-active': { color: BRAND },
'& .MuiStepIcon-root.Mui-completed': { color: BRAND }
}}
>
{STEP_LABELS[s]}
</StepLabel>
</Step>
))}
</Stepper>
</Paper>
)}
<Grid container spacing={2.5}>
<Grid item xs={12} md={6}>
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
Customer & Pickup
</Typography>
<Stack spacing={2}>
<InfoRow icon={MdPerson} label="Customer" value={booking.customername || booking.customer?.name} />
<InfoRow icon={MdPhone} label="Phone" value={booking.customerphone || booking.customer?.phone} />
<InfoRow icon={MdLocationOn} label="Pickup Address" value={booking.pickupaddress} color="#0ea5e9" />
</Stack>
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
Delivery
</Typography>
<Stack spacing={2}>
<InfoRow icon={MdLocationOn} label="Delivery Address" value={booking.deliveryaddress} color="#10b981" />
</Stack>
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
Assigned Miler
</Typography>
{booking.assignedmileruserid || miler ? (
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 44, height: 44, bgcolor: soft('#8b5cf6'), color: '#8b5cf6' }}>
<MdTwoWheeler size={22} />
</Avatar>
<Box>
<Typography sx={{ fontWeight: 700, color: DT.textPrimary }}>
{miler?.displayname || `Miler #${booking.assignedmileruserid}`}
</Typography>
<Stack direction="row" alignItems="center" spacing={1.5} sx={{ mt: 0.25 }}>
{miler?.phone && (
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{miler.phone}
</Typography>
)}
{miler?.rating != null && (
<Stack direction="row" alignItems="center" spacing={0.25}>
<MdStar size={13} style={{ color: '#f59e0b' }} />
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{miler.rating}
</Typography>
</Stack>
)}
</Stack>
</Box>
</Stack>
) : (
<Typography variant="body2" sx={{ color: DT.textMuted }}>
No miler assigned yet.
</Typography>
)}
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
Parcel Details
</Typography>
{parcels.length === 0 ? (
<Typography variant="body2" sx={{ color: DT.textMuted }}>
No parcel details available.
</Typography>
) : (
<Stack spacing={1}>
{parcels.map((p, i) => (
<Stack key={i} direction="row" justifyContent="space-between" sx={{ p: 1, borderRadius: 1.5, bgcolor: DT.surfaceAlt }}>
<Typography variant="body2">{p.description || p.name || `Parcel ${i + 1}`}</Typography>
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
{p.weight ? `${p.weight}kg` : ''}
</Typography>
</Stack>
))}
</Stack>
)}
</Paper>
</Grid>
{agentDecision && (
<Grid item xs={12}>
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: `1px solid ${edge('#6366f1')}`, background: tint('#6366f1') }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
<Avatar sx={{ width: 32, height: 32, bgcolor: soft('#6366f1'), color: '#6366f1' }}>
<MdOutlineSmartToy size={16} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 800, color: '#6366f1' }}>
AI Assignment Reasoning
</Typography>
</Stack>
<Typography variant="body2" sx={{ color: DT.textPrimary }}>
{agentDecision.reasoning || agentDecision.reason || JSON.stringify(agentDecision)}
</Typography>
</Paper>
</Grid>
)}
</Grid>
<Stack direction="row" spacing={1.5} justifyContent="flex-end" sx={{ mt: 2.5 }}>
{!isTerminal && (
<Button
variant="contained"
startIcon={<MdOutlineAssignmentInd size={16} />}
disabled={reassignMutation.isLoading}
onClick={() => reassignMutation.mutate()}
sx={{ bgcolor: '#6366f1', '&:hover': { bgcolor: '#4f46e5' } }}
>
Reassign Miler
</Button>
)}
{!isTerminal && (
<Button
variant="outlined"
color="error"
startIcon={<MdOutlineCancel size={16} />}
disabled={cancelMutation.isLoading}
onClick={() => cancelMutation.mutate()}
>
Cancel Booking
</Button>
)}
</Stack>
</>
);
};
export default BookingDetail;

View File

@@ -61,7 +61,7 @@ const DT = {
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc',
brand: '#662582'
brand: '#C01227'
};
const a = (c, suffix) => `${c}${suffix}`;
const tint = (c) => a(c, '08');
@@ -69,7 +69,7 @@ const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55');
const BRAND = '#662582';
const BRAND = '#C01227';
const SoftPaper = (props) => (
<Paper

File diff suppressed because it is too large Load Diff

View File

@@ -1,545 +1,10 @@
import { React, useEffect, useState, useRef } from 'react';
import { useTheme } from '@mui/material/styles';
import { Button, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, IconButton, Autocomplete, useMediaQuery } from '@mui/material';
import MainCard from 'components/MainCard';
import axios from 'axios';
import Loader from 'components/Loader';
import Geocode from 'react-geocode';
import { enqueueSnackbar } from 'notistack';
import { useNavigate } from 'react-router';
import CloseIcon from '@mui/icons-material/Close';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import { OpenToast } from 'components/third-party/OpenToast';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
const CreateCustomer = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [appId, setAppId] = useState(0);
const locationRef = useRef(null);
const [mobilenumber, setMobilenumber] = useState('');
const [emailaddress, setEmailaddress] = useState('');
const [address, setAddress] = useState('');
const [firstname, setFirstname] = useState('');
const [doorno, setDoorno] = useState('');
const [landmark, setLandmark] = useState('');
const [inputValue2, setInputValue2] = useState('');
const [appLocaLat, setAppLocaLat] = useState();
const [appLocaLng, setAppLocaLng] = useState();
const [appLocaRadius, setAppLocaRadius] = useState();
const [locaName, setLocoName] = useState('Select Location');
const [tenantlist, setTenantlist] = useState([]);
const [tid, setTid] = useState(0);
const [pickCust, setPickCust] = useState({});
const [startPoint, setStartPoint] = useState({ latitude: 0, longitude: 0 });
const [loading, setLoading] = useState(false);
export default function CreateCustomer() {
const navigate = useNavigate();
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
useEffect(() => {
// Initialize Google Maps Autocomplete
if (inputValue2) {
const autocompleteInput = document.getElementById('addressAuto1');
const autocomplete = new window.google.maps.places.Autocomplete(autocompleteInput, {
// types: ['(cities)'], // You can adjust the types parameter based on your requirements
strictBounds: true,
bounds: new window.google.maps.Circle({
// center: new window.google.maps.LatLng(11.0050707, 76.9509083),
// radius: 100000
center: new window.google.maps.LatLng(appLocaLat, appLocaLng),
radius: appLocaRadius * 1000
}).getBounds()
});
// Event listener for autocomplete place changed
autocomplete.addListener('place_changed', () => {
const place = autocomplete.getPlace();
setInputValue2(`${place.name}, ${place.formatted_address}`);
console.log('new place', place); // Do something with the selected place
console.log(' pick (new place) lat lng', { lat: place.geometry.location.lat(), lng: place.geometry.location.lng() }); // Do something with the selected place
// to trigger getDistance
setStartPoint({ latitude: place.geometry.location.lat(), longitude: place.geometry.location.lng() });
setAddress(`${place.name} ${place.formatted_address}`);
setPickCust({ ...pickCust, address: `${place.name} ${place.formatted_address}` });
const address = {
address: `${place.name} ${place.formatted_address}`,
street_number: '',
route: '',
locality: '',
sublocality_level_1: '',
administrative_area_level_3: '',
administrative_area_level_1: '',
country: '',
postal_code: ''
};
place.address_components.forEach((component) => {
component.types.forEach((type) => {
switch (type) {
case 'street_number':
address.street_number = component.long_name;
break;
case 'route':
address.route = component.long_name;
break;
case 'locality':
address.locality = component.long_name;
break;
case 'sublocality_level_1':
address.sublocality_level_1 = component.long_name;
break;
case 'administrative_area_level_3':
address.administrative_area_level_3 = component.long_name;
break;
case 'administrative_area_level_1':
address.administrative_area_level_1 = component.long_name;
break;
case 'country':
address.country = component.long_name;
break;
case 'postal_code':
address.postal_code = component.long_name;
break;
// Add more cases as needed for other types
}
});
});
// Use address object as per your requirements
setPickCust({
...pickCust,
address: address.address,
doorno: `${address.street_number} ${address.route}`,
suburb: address.administrative_area_level_3,
city: address.locality,
state: address.administrative_area_level_1,
postcode: address.postal_code,
latitude: place.geometry.location.lat(),
longitude: place.geometry.location.lng()
});
console.log('Pick Address:', address);
});
}
}, [inputValue2]);
// ==================================================== || getapplocations || ====================================================
const getapplocations = async () => {
setLoading(true);
await axios
.get(`${process.env.REACT_APP_URL}/utils/getapplocations/?applocationid=${appId}`)
.then((res) => {
console.log('getapplocations', res);
const { latitude, longitude, radius } = res.data.details[0];
if (res.data.status) {
setAppLocaLat(latitude);
setAppLocaLng(longitude);
setAppLocaRadius(radius);
console.log('radius', radius);
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
};
useEffect(() => {
if (appId) {
getapplocations();
}
}, [appId]);
// ===================================================== || fetchtenantinfolist || =====================================================
const fetchtenantinfolist = async (id) => {
setLoading(true);
await axios
.get(`${process.env.REACT_APP_URL}/tenants/gettenants/?applocationid=${id}&status=active`)
.then((res) => {
console.log(res);
if (res.data.status) {
let arr = [];
res.data.details.map((val) => {
arr.push({
...val,
label: `${val.tenantname}`
});
});
setTenantlist(arr);
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
};
useEffect(() => {
appId && fetchtenantinfolist(appId);
}, [appId]);
// ============================================= || gettenantlocations (branches) || =============================================
const gettenantlocations = async (id) => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`);
console.log('gettenantlocations', res.data.details);
if (res.data.details.length == 1) {
} else {
}
} catch (err) {
console.log('gettenantlocations', err);
}
};
const opentoast = (message) => {
enqueueSnackbar(message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
};
const createprofile = async () => {
let obj = {
applocationid: +appId,
tenantid: +tid,
customerid: 0,
configid: 1,
firstname: firstname,
dialcode: '+91',
contactno: mobilenumber,
email: emailaddress,
doorno: doorno,
address: pickCust.address,
suburb: pickCust.suburb,
city: pickCust.city,
state: pickCust.state,
postcode: pickCust.postcode,
landmark: landmark,
latitude: startPoint.latitude.toString(),
longitude: startPoint.longitude.toString(),
profileimage: '',
devicetype: '',
deviceid: '',
customertoken: '',
primaryaddress: 1
};
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('/nearle/customers');
} else if (res.data.message == 'Customer Already available') {
enqueueSnackbar('Customer Already available', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
enqueueSnackbar(err.message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
});
} catch (err) {
console.log(err);
setLoading(false);
}
};
return (
<>
{loading && <Loader />}
<Grid item xs={12} sx={{ mb: 2 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="h3">Create Customer</Typography>
</Stack>
</Grid>
<MainCard sx={{ p: { xs: 1.5, md: 3 } }}>
<Grid container spacing={{ xs: 2, md: 3 }}>
<Grid item xs={12}>
<Grid container spacing={{ xs: 2, md: 3 }}>
{/* ===================================================== || Choose location || ===================================================== */}
<Grid item xs={12} md={6}>
<LocationAutocomplete ref={locationRef} locaName={locaName} setAppId={setAppId} setLocoName={setLocoName} sx={{}} />
</Grid>
{/* ===================================================== || Choose client || ===================================================== */}
<Grid item xs={12} md={6}>
<Autocomplete
fullWidth
disabled={appId == 0}
id="free-solo-demo"
sx={{}}
options={tenantlist || []}
renderInput={(params) => <TextField {...params} label="Choose Client" focused />}
onChange={(e, val, reason) => {
if (val) {
console.log('Client', val);
gettenantlocations(val.tenantid);
setTid(val.tenantid);
} else {
setClientinfo({});
setTenantid('');
}
if (reason == 'clear') {
}
}}
/>{' '}
</Grid>
{/* ===================================================== || Name|| ===================================================== */}
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-last-name">Name</InputLabel>
<TextField
fullWidth
id="personal-last-name"
placeholder="Name"
onChange={(e) => setFirstname(e.target.value)}
value={firstname}
autoComplete="off"
/>
</Stack>
</Grid>
{/* ===================================================== || Phone Number || ===================================================== */}
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-phone">Phone Number</InputLabel>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Select defaultValue="+1" disabled sx={{ cursor: 'not-allowed' }}>
<MenuItem value="+1">+91</MenuItem>
</Select>
<TextField
type="number"
id="personal-phone"
fullWidth
placeholder="Phone Number"
onChange={(e) => {
if (e.target.value.toString().length <= 10) {
setMobilenumber(e.target.value);
}
}}
value={mobilenumber}
autoComplete="off"
// disabled
sx={{ cursor: 'not-allowed' }}
/>
</Stack>
</Stack>
</Grid>
{/* ===================================================== || Email|| ===================================================== */}
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-email">Email </InputLabel>
<TextField
type="email"
fullWidth
id="personal-email"
placeholder="Email "
onChange={(e) => setEmailaddress(e.target.value)}
value={emailaddress}
autoComplete="off"
/>
</Stack>
</Grid>
{/* ===================================================== || door no || ===================================================== */}
<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>
{/* ===================================================== || Address || ===================================================== */}
<Grid item xs={12}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-email"> Address</InputLabel>
<TextField
variant="outlined"
id="addressAuto1"
fullWidth
value={inputValue2}
onChange={(e) => {
if (appId) {
appId && setInputValue2(e.target.value);
} else {
OpenToast('Select Location First', 'warning', 3000);
}
}}
InputProps={{
endAdornment: (
<IconButton
onClick={() => {
setInputValue2('');
setPickCust({
...pickCust,
doorno: '',
suburb: '',
city: '',
postcode: '',
landmark: ''
});
setStartPoint({ latitude: 0, longitude: 0 });
}}
size="small"
>
<CloseIcon />
</IconButton>
)
}}
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-location">Location</InputLabel>
<TextField
fullWidth
// defaultValue="New York"
id="personal-location"
placeholder="Location"
onChange={(e) => setPickCust({ ...pickCust, suburb: e.target.value })}
value={pickCust.suburb}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-zipcode">City</InputLabel>
<TextField
fullWidth
id="personal-zipcode"
placeholder="City"
onChange={(e) => setPickCust({ ...pickCust, city: e.target.value })}
value={pickCust.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) => setPickCust({ ...pickCust, state: e.target.value })}
value={pickCust.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) => setPickCust({ ...pickCust, postcode: e.target.value })}
value={pickCust.postcode}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-email">Landmark</InputLabel>
<TextField
type="email"
fullWidth
// defaultValue="stebin.ben@gmail.com"
id="personal-email"
placeholder="Landmark"
onChange={(e) => setLandmark(e.target.value)}
value={landmark}
autoComplete="off"
/>
</Stack>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
justifyContent="flex-end"
alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={2}
>
<Button
variant="contained"
fullWidth={isMobile}
onClick={() => {
if (appId === '') {
opentoast('Select Applocation ');
} else if (tid === '') {
opentoast('Select Tenant');
} else if (firstname === '') {
opentoast('Enter Name');
} else if (mobilenumber === '') {
opentoast('Enter Mobile Number ');
} else if (address === '') {
opentoast('Enter Address ');
} else if (pickCust.city === '') {
opentoast('Enter City ');
} else if (pickCust.state === '') {
opentoast('Enter State ');
} else if (pickCust.suburb === '') {
opentoast('Enter location ');
} else if (pickCust.postcode === '') {
opentoast('Enter Post Code ');
} else if (landmark === '') {
opentoast('Enter Land Mark ');
} else if (pickCust.latitude === '') {
opentoast('Invalid latitude ');
} else if (pickCust.longitude === '') {
opentoast('Invaiid Longitude ');
} else {
createprofile();
}
}}
>
Create
</Button>
</Stack>
</Grid>
</Grid>
</MainCard>
</>
);
};
export default CreateCustomer;
navigate('/nearle/customers');
}, [navigate]);
return null;
}

View File

@@ -1,88 +1,26 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Box, Button, FormLabel, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
// third-party
// import { PatternFormat } from 'react-number-format';
import { Button, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
import axios from 'axios';
import { usePlacesWidget } from 'react-google-autocomplete';
import Loader from 'components/Loader';
import Geocode from 'react-geocode';
import { enqueueSnackbar } from 'notistack';
import { useNavigate } from 'react-router';
// import { setLocationType } from 'react-geocode';
// 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();
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
// Geocode.setApiKey('AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8');
const [clientname, setClientname] = useState('');
const [loading, setLoading] = useState(false);
useEffect(() => {
// fetchprofiledetails(localStorage.getItem('appuserid'));
// fetchprofiledetails(181);
if (localStorage.getItem('tenantid')) {
fetchtenantinfo(localStorage.getItem('tenantid'));
}
}, []);
useEffect(() => {
try {
Geocode.fromAddress(address).then(
(response) => {
if (response.status == 'OK') {
const { lat, lng } = response.results[0].geometry.location;
setLatlong({
lat,
lng
});
console.log(response);
}
},
(error) => {
console.log(error);
}
);
} catch (err) {
console.log(err);
}
}, [address]);
const navigate = useNavigate();
const opentoast = (message) => {
enqueueSnackbar(message, {
@@ -90,207 +28,45 @@ 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 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);
});
};
useEffect(() => {
if (selectedImage) {
setAvatar(URL.createObjectURL(selectedImage));
}
}, [selectedImage]);
const { ref: materialRef } = usePlacesWidget({
apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY,
onPlaceSelected: (place) => {
console.log(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':
suburb1 = place.address_components[i].long_name;
break;
}
}
}
setCity(city1 || '');
setState(state1 || '');
setZipcode(zipcode1 || '');
setSuburb(suburb1 || '');
// setAddress(place.formatted_address)
},
// inputAutocompleteValue: "country",
options: {
// componentRestrictions: 'us',
// types: ["establishment"]
types: ['address' || 'geocode']
}
});
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');
if (!clientname) {
opentoast('Fill Client Name');
} else if (!mobilenumber) {
opentoast('Fill Mobile Number');
} else if (!emailaddress) {
opentoast('Fill emailaddress');
} else if (!address) {
opentoast('Fill Address');
opentoast('Fill Email Address');
} else if (!city) {
opentoast('Fill City');
} else if (!zipcode) {
opentoast('Fill post code');
} else if (!suburb) {
opentoast('Fill suburb');
} else if (!latlong.lat || !latlong.lng) {
opentoast('Choose valid address');
} else {
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
};
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
});
});
await axios.post(`${process.env.REACT_APP_URL}/crm/clients`, {
clientname,
email: emailaddress,
phone: mobilenumber,
city,
status: 'pending'
});
enqueueSnackbar('Created Successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
navigate('/nearle/tenants');
} catch (err) {
console.log(err);
enqueueSnackbar(err.response?.data?.message || err.message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
} finally {
setLoading(false);
}
}
};
// const [experience, setExperience] = useState('0');
// const handleChange = (event) => {
// setExperience(event.target.value);
// };
return (
<>
{loading && <Loader />}
@@ -302,158 +78,34 @@ const Createclient = () => {
</Grid>
<MainCard contentSX={{ p: { xs: 1.5, md: 3 } }}>
<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="client-name">Client Name</InputLabel>
<TextField
fullWidth
id="personal-last-name"
placeholder="Name"
onChange={(e) => setFirstname(e.target.value)}
value={firstname}
id="client-name"
placeholder="Client Name"
onChange={(e) => setClientname(e.target.value)}
value={clientname}
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="client-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="personal-phone"
// format="##########"
// mask="_"
id="client-phone"
fullWidth
// customInput={TextField}
placeholder="Phone Number"
// defaultValue="8654239581"
// onBlur={() => { }}
onChange={(e) => {
if (e.target.value.toString().length <= 10) {
setMobilenumber(e.target.value);
@@ -461,20 +113,17 @@ const Createclient = () => {
}}
value={mobilenumber}
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="client-email">Email Address</InputLabel>
<TextField
type="email"
fullWidth
// defaultValue="stebin.ben@gmail.com"
id="personal-email"
id="client-email"
placeholder="Email Address"
onChange={(e) => setEmailaddress(e.target.value)}
value={emailaddress}
@@ -482,44 +131,12 @@ const Createclient = () => {
/>
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-address">Address</InputLabel>
<TextField
fullWidth
// defaultValue="Street 110-B Kalians Bag, Dewan, M.P. New York"
id="personal-address"
placeholder="Address"
value={address}
onChange={(e) => setAddress(e.target.value)}
inputRef={materialRef}
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-location">Suburb</InputLabel>
<InputLabel htmlFor="client-city">City</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"
id="client-city"
placeholder="City"
onChange={(e) => setCity(e.target.value)}
value={city}
@@ -527,66 +144,6 @@ const Createclient = () => {
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-location">State</InputLabel>
<TextField
fullWidth
// defaultValue="New York"
id="personal-location"
placeholder="State"
onChange={(e) => setState(e.target.value)}
value={state}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-zipcode">Post Code</InputLabel>
<TextField
fullWidth
// defaultValue="956754"
type="number"
id="personal-zipcode"
placeholder="Zipcode"
onChange={(e) => setZipcode(e.target.value)}
value={zipcode}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-location">Door No</InputLabel>
<TextField
fullWidth
// defaultValue="New York"
id="personal-location"
placeholder="Door No"
onChange={(e) => setDoorno(e.target.value)}
value={doorno}
autoComplete="off"
/>
</Stack>
</Grid>
<Grid item xs={12} sm={6}>
<Stack spacing={1.25}>
<InputLabel htmlFor="personal-email">Landmark</InputLabel>
<TextField
type="email"
fullWidth
// defaultValue="stebin.ben@gmail.com"
id="personal-email"
placeholder="Landmark"
onChange={(e) => setLandmark(e.target.value)}
value={landmark}
autoComplete="off"
/>
</Stack>
</Grid>
</Grid>
</MainCard>
</Grid>

View File

@@ -1,4 +1,4 @@
import { React, useState, useEffect, useRef, useMemo } from 'react';
import { React, useState, useEffect, useRef } from 'react';
import axios from 'axios';
import { FaRegEdit } from 'react-icons/fa';
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
@@ -23,7 +23,6 @@ import {
DialogContent,
Button,
TextField,
Autocomplete,
Avatar,
Paper,
useMediaQuery,
@@ -33,19 +32,12 @@ import {
MdMyLocation,
MdPersonPin,
MdPhone,
MdLocationOn,
MdEdit,
MdGroups,
MdHowToReg,
MdPlace,
MdOutlineGroups,
MdOutlineHowToReg,
MdOutlinePlace
} from 'react-icons/md';
import Geocode from 'react-geocode';
import LocationOnIcon from '@mui/icons-material/LocationOn';
import parse from 'autosuggest-highlight/parse';
import { debounce } from '@mui/material/utils';
// project imports
import Loader from 'components/Loader';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
@@ -111,23 +103,6 @@ const AccentAvatar = ({ color, selected, size = 24, children }) => (
</Avatar>
);
// ==============================|| google address ||============================== //
const GOOGLE_MAPS_API_KEY = process.env.REACT_APP_GOOGLE_MAPS_API_KEY;
function loadScript(src, position, id) {
if (!position) {
return;
}
const script = document.createElement('script');
script.setAttribute('async', '');
script.setAttribute('id', id);
script.src = src;
position.appendChild(script);
}
const autocompleteService = { current: null };
// ==============================|| MUI TABLE - ENHANCED ||============================== //
export default function Customers() {
@@ -141,146 +116,9 @@ export default function Customers() {
const [locaName, setLocoName] = useState('All');
const [selectedCustomer, setSelectedCustomer] = useState({}); // to edit
const [open, setOpen] = useState(false);
const [address, setAddress] = useState('');
const [latlong, setLatlong] = useState({});
const [city, setCity] = useState('');
const [postcode, setPostcode] = useState('');
const [state, setState] = useState('');
const [suburb, setSuburb] = useState('');
const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
// ==============================|| for google address ||============================== //
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = useState('');
const [options, setOptions] = useState([]);
const loaded = useRef(false);
if (typeof window !== 'undefined' && !loaded.current) {
if (!document.querySelector('#google-maps')) {
loadScript(
`https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places`,
document.querySelector('head'),
'google-maps'
);
}
loaded.current = true;
}
const fetch = useMemo(
() =>
debounce((request, callback) => {
autocompleteService.current.getPlacePredictions(request, callback);
}, 400),
[]
);
useEffect(() => {
let active = true;
if (!autocompleteService.current && window.google) {
autocompleteService.current = new window.google.maps.places.AutocompleteService();
}
if (!autocompleteService.current) {
return undefined;
}
if (inputValue === '') {
setOptions(value ? [value] : []);
return undefined;
}
fetch({ input: inputValue }, (results) => {
if (active) {
let newOptions = [];
if (value) {
newOptions = [value];
}
if (results) {
newOptions = [...newOptions, ...results];
}
setOptions(newOptions);
}
});
return () => {
active = false;
};
}, [value, inputValue, fetch]);
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
useEffect(() => {
try {
console.log('selected address =>', address);
Geocode.fromAddress(address).then(
(response) => {
console.log('lat long response =>', response.results[0]);
if (response.status == 'OK') {
const { lat, lng } = response.results[0].geometry.location;
setLatlong({
lat,
lng
});
// setSelectedCustomer({
// ...selectedCustomer,
// latitude: lat,
// longitude: lng
// });
if (response.results[0].address_components) {
let place = response.results[0];
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':
suburb1 = place.address_components[i].long_name;
break;
}
}
}
setCity(city1 || '');
setState(state1 || '');
setPostcode(zipcode1 || '');
setSuburb(suburb1 || '');
setSelectedCustomer((prev) => ({
...prev,
city: city1 || '',
state: state1 || '',
postcode: zipcode1 || '',
suburb: suburb1 || '',
latitude: lat || '',
longitude: lng || ''
}));
}
}
},
(error) => {
console.log(error);
}
);
} catch (err) {
console.log(err);
}
}, [address]);
// useEffect(() => {
// selectedCustomer &&
// setLatlong({
// lat: selectedCustomer.latitude,
// lng: selectedCustomer.longitude
// });
// }, [selectedCustomer]);
// ==============================|| getallcustomers (customers) ||============================== //
const {
@@ -336,67 +174,23 @@ export default function Customers() {
useEffect(() => {
console.log('pageCount', pageCount);
}, [pageCount]);
// ==============================|| updateCustomer (post)||============================== //
// ==============================|| updateCustomer (PATCH) ||============================== //
const updateCustomer = async () => {
console.log('selectedCustomer', selectedCustomer);
if (!selectedCustomer.firstname) {
OpenToast('Enter Door NO', 'warning', 1500);
} else if (!selectedCustomer.contactno) {
OpenToast('Enter Contact Number ', 'warning', 1500);
} else if (!selectedCustomer.address) {
OpenToast('Enter Valid Address', 'warning', 1500);
} else if (!selectedCustomer.suburb) {
OpenToast('Enter Suburb', 'warning', 1500);
} else if (!selectedCustomer.city) {
OpenToast('Enter City ', 'warning', 1500);
} else if (!selectedCustomer.state) {
OpenToast('Enter State', 'warning', 1500);
} else if (!selectedCustomer.postcode) {
OpenToast('Enter PostCode', 'warning', 1500);
} else if (!selectedCustomer.landmark) {
OpenToast('Enter Landmark', 'warning', 1500);
} else if (!selectedCustomer.latitude) {
OpenToast('Enter Latitude', 'warning', 1500);
} else if (!selectedCustomer.longitude) {
OpenToast('Enter Longitude', 'warning', 1500);
} else {
try {
const postUpdateResponse = await axios.put(`${process.env.REACT_APP_URL}/customers/update`, {
customerid: selectedCustomer.customerid,
configid: 1,
firstname: selectedCustomer.firstname,
applocationid: selectedCustomer.applocationid,
profileimage: '',
dialcode: '+91',
contactno: selectedCustomer.contactno,
devicetype: '',
deviceid: '',
customertoken: '123',
address: selectedCustomer.address,
suburb: suburb,
city: city,
state: state,
postcode: postcode,
landmark: selectedCustomer.landmark,
doorno: selectedCustomer.doorno,
latitude: selectedCustomer.latitude.toString(),
longitude: selectedCustomer.longitude.toString()
});
console.log('postUpdateResponse', postUpdateResponse);
if (postUpdateResponse.data.status) {
OpenToast(postUpdateResponse.data.message, 'success', 1500);
setOpen(false);
getallcustomersRefetch();
}
} catch (error) {
console.log('postUpdate error', error);
}
try {
await axios.patch(`${process.env.REACT_APP_URL}/admin/customers/${selectedCustomer.appcustomerid}`, {
name: selectedCustomer.name,
phone: selectedCustomer.phone,
email: selectedCustomer.email
});
OpenToast('Customer updated successfully', 'success', 2000);
setOpen(false);
getallcustomersRefetch();
} catch (err) {
OpenToast(err.response?.data?.message || 'Update failed', 'error', 2000);
}
};
const KPI_META = [
{ key: 'total', label: 'Total Customers', color: '#662582', icon: MdOutlineGroups, value: pageCount?.Total ?? 0 },
{ key: 'total', label: 'Total Customers', color: '#C01227', icon: MdOutlineGroups, value: pageCount?.Total ?? 0 },
{ key: 'loaded', label: 'Loaded in View', color: '#0ea5e9', icon: MdOutlineHowToReg, value: rows.length },
{ key: 'zone', label: 'Active Zone', color: '#10b981', icon: MdOutlinePlace, value: locaName || 'All Zones' }
];
@@ -416,7 +210,7 @@ export default function Customers() {
setAppId={setAppId}
setLocoName={setLocoName}
pill
accentColor="#662582"
accentColor="#C01227"
icon={<MdMyLocation size={14} />}
placeholder="Select Zone"
paperComponent={SoftPaper}
@@ -466,7 +260,7 @@ export default function Customers() {
spacing={1.25}
>
<Stack direction="row" alignItems="center" spacing={1.25}>
<AccentAvatar color="#662582" size={32}>
<AccentAvatar color="#C01227" size={32}>
<MdGroups size={18} />
</AccentAvatar>
<Stack>
@@ -491,11 +285,11 @@ export default function Customers() {
m: 0,
width: '100%',
borderRadius: 999,
bgcolor: tint('#662582'),
'& fieldset': { borderColor: edge('#662582'), borderWidth: 1.5 },
'&:hover fieldset': { borderColor: '#662582' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` }
bgcolor: tint('#C01227'),
'& fieldset': { borderColor: edge('#C01227'), borderWidth: 1.5 },
'&:hover fieldset': { borderColor: '#C01227' },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
}}
/>
</Box>
@@ -534,20 +328,20 @@ export default function Customers() {
) : (
rows?.map((row, index) => (
<MobileCard
key={row.customerid || `${row.firstname}-${index}`}
accent="#662582"
key={row.appcustomerid || `${row.name}-${index}`}
accent="#C01227"
header={
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<AccentAvatar color="#662582" size={36}>
<AccentAvatar color="#C01227" size={36}>
<MdPersonPin size={18} />
</AccentAvatar>
<Stack sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.firstname || '—'}
{row.name || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.customerid}
ID #{row.appcustomerid}
</Typography>
</Stack>
</Stack>
@@ -573,35 +367,10 @@ export default function Customers() {
}
>
<MobileFieldGrid>
<MobileField label="Contact" value={row.contactno || '—'} />
<MobileField label="Location">
{row.suburb ? (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint('#10b981'),
border: `1px solid ${edge('#10b981')}`,
color: '#10b981',
fontSize: 11,
fontWeight: 800
}}
>
<MdLocationOn size={12} /> {row.suburb}
</Box>
) : (
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textMuted }}></Typography>
)}
</MobileField>
<MobileField label="Address" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }}>
{row.address || '—'}
</Typography>
</MobileField>
<MobileField label="Phone" value={row.phone || '—'} />
<MobileField label="Email" value={row.email || '—'} />
<MobileField label="Total Bookings" value={row.totalbookings ?? 0} />
<MobileField label="Joined" value={row.createdat ? new Date(row.createdat).toLocaleDateString() : '—'} />
</MobileFieldGrid>
</MobileCard>
))
@@ -626,9 +395,9 @@ export default function Customers() {
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
'&::-webkit-scrollbar': { width: 10, height: 10 },
'&::-webkit-scrollbar-thumb': {
backgroundColor: edge('#662582'),
backgroundColor: edge('#C01227'),
borderRadius: 8,
'&:hover': { backgroundColor: '#662582' }
'&:hover': { backgroundColor: '#C01227' }
},
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
@@ -652,10 +421,11 @@ export default function Customers() {
}}
>
<TableCell>#</TableCell>
<TableCell>Customer</TableCell>
<TableCell>Contact</TableCell>
<TableCell>Address</TableCell>
<TableCell>Location</TableCell>
<TableCell>Name</TableCell>
<TableCell>Phone</TableCell>
<TableCell>Email</TableCell>
<TableCell>Total Bookings</TableCell>
<TableCell>Joined</TableCell>
<TableCell align="right">Action</TableCell>
</TableRow>
</TableHead>
@@ -664,7 +434,7 @@ export default function Customers() {
{customersIsLoading && <OrdersTableSkeleton />}
{rows?.length === 0 && !customersIsLoading ? (
<TableRow>
<TableCell colSpan={6} sx={{ py: 6 }}>
<TableCell colSpan={7} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdGroups size={28} />
@@ -681,7 +451,7 @@ export default function Customers() {
) : (
rows?.map((row, index) => (
<TableRow
key={row.customerid || `${row.firstname}-${index}`}
key={row.appcustomerid || `${row.name}-${index}`}
sx={{
cursor: 'pointer',
transition: 'background-color 0.15s',
@@ -700,7 +470,7 @@ export default function Customers() {
</TableCell>
<TableCell>
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color="#662582" size={36}>
<AccentAvatar color="#C01227" size={36}>
<MdPersonPin size={18} />
</AccentAvatar>
<Stack>
@@ -708,70 +478,39 @@ export default function Customers() {
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
>
{row.firstname || '—'}
{row.name || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.customerid}
ID #{row.appcustomerid}
</Typography>
</Stack>
</Stack>
</TableCell>
<TableCell>
<Stack>
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdPhone size={12} color={DT.textMuted} />
<Typography
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
>
{row.contactno || '—'}
</Typography>
</Stack>
{row.email && (
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.email}
</Typography>
)}
</Stack>
</TableCell>
<TableCell sx={{ maxWidth: 280 }}>
<Tooltip title={row.address || ''} placement="top">
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdPhone size={12} color={DT.textMuted} />
<Typography
variant="caption"
sx={{
color: DT.textSecondary,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden'
}}
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
>
{row.address || '—'}
{row.phone || '—'}
</Typography>
</Tooltip>
</Stack>
</TableCell>
<TableCell>
{row.suburb ? (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint('#10b981'),
border: `1px solid ${edge('#10b981')}`,
color: '#10b981',
fontSize: 11,
fontWeight: 800
}}
>
<MdLocationOn size={12} /> {row.suburb}
</Box>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}></Typography>
)}
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.email || '—'}
</Typography>
</TableCell>
<TableCell>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.totalbookings ?? 0}
</Typography>
</TableCell>
<TableCell>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.createdat ? new Date(row.createdat).toLocaleDateString() : '—'}
</Typography>
</TableCell>
<TableCell align="right">
<Tooltip title="Edit customer" placement="top">
@@ -797,7 +536,7 @@ export default function Customers() {
)}
{rows?.length !== 0 && (
<TableRow>
<TableCell colSpan={6} sx={{ borderBottom: 'none' }}>
<TableCell colSpan={7} sx={{ borderBottom: 'none' }}>
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
{isFetchingNextPage || hasNextPage ? (
<LoaderWithImage />
@@ -829,7 +568,7 @@ export default function Customers() {
<DialogTitle
id="alert-dialog-title"
sx={{
background: `linear-gradient(135deg, #662582 0%, #9255AB 100%)`,
background: `linear-gradient(135deg, #C01227 0%, #D35968 100%)`,
color: '#fff',
py: 2
}}
@@ -843,36 +582,36 @@ export default function Customers() {
Customer
</Typography>
<Typography sx={{ fontWeight: 800, fontSize: { xs: '1.05rem', sm: '1.2rem' }, lineHeight: 1.2, mt: 0.25 }}>
Edit {selectedCustomer?.firstname || 'Customer'}
Edit {selectedCustomer?.name || 'Customer'}
</Typography>
</Stack>
</Stack>
</DialogTitle>
<DialogContent>
<Grid container spacing={2} sx={{ mt: 2 }}>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>Customer Name</Typography>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>Name</Typography>
<TextField
variant="outlined"
fullWidth
defaultValue={selectedCustomer?.firstname}
value={selectedCustomer?.name || ''}
onChange={(e) => {
setSelectedCustomer({
...selectedCustomer,
firstname: e.target.value
});
setSelectedCustomer((prev) => ({
...prev,
name: e.target.value
}));
}}
/>
</Grid>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>Contact Number</Typography>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>Phone</Typography>
<Stack direction={'row'} spacing={1}>
<TextField readonly variant="outlined" value={'+91'} sx={{ width: 60 }} />
<TextField
variant="outlined"
fullWidth
type="text"
value={selectedCustomer?.contactno || ''}
value={selectedCustomer?.phone || ''}
inputProps={{
maxLength: 10,
inputMode: 'numeric', // mobile numeric keypad
@@ -883,168 +622,40 @@ export default function Customers() {
setSelectedCustomer((prev) => ({
...prev,
contactno: value
phone: value
}));
}}
/>
</Stack>
</Grid>
<Grid item xs={12}>
<Typography sx={{ mb: 1 }}>Address</Typography>
<Autocomplete
id="google-map-demo"
sx={{}}
fullWidth
getOptionLabel={(option) => (typeof option === 'string' ? option : option?.description || '')}
filterOptions={(x) => x}
options={options}
autoComplete
includeInputInList
filterSelectedOptions
value={selectedCustomer?.address}
noOptionsText="No locations"
onChange={(event, newValue) => {
setOptions(newValue ? [newValue, ...options] : options);
setValue(newValue);
console.log('newValue', newValue || '');
setAddress(newValue?.description);
setSelectedCustomer({
...selectedCustomer,
address: newValue?.description
});
}}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
renderInput={(params) => <TextField {...params} fullWidth />}
renderOption={(props, option) => {
const matches = option.structured_formatting.main_text_matched_substrings || [];
const parts = parse(
option.structured_formatting.main_text,
matches.map((match) => [match.offset, match.offset + match.length])
);
return (
<li {...props}>
<Grid container alignItems="center">
<Grid item sx={{ display: 'flex', width: 44 }}>
<LocationOnIcon sx={{ color: 'text.secondary' }} />
</Grid>
<Grid
item
sx={{
width: 'calc(100% - 44px)',
wordWrap: 'break-word'
}}
>
{parts?.map((part, index) => (
<Box
key={index}
component="span"
sx={{
fontWeight: part.highlight ? 'bold' : 'regular'
}}
>
{part.text}
</Box>
))}
<Typography variant="body2" color="text.secondary">
{option?.structured_formatting.secondary_text}
</Typography>
</Grid>
</Grid>
</li>
);
}}
/>
</Grid>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>Location</Typography>
<Typography sx={{ mb: 1 }}>Email</Typography>
<TextField
variant="outlined"
fullWidth
value={selectedCustomer?.suburb}
type="email"
value={selectedCustomer?.email || ''}
onChange={(e) => {
const value = e.target.value;
setSelectedCustomer((prev) => ({
...prev,
suburb: value
email: e.target.value
}));
// setSuburb(e.target.value);
}}
/>
</Grid>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>City</Typography>
<Grid item xs={12} sm={6} sx={{ cursor: 'not-allowed' }}>
<Typography sx={{ mb: 1 }}>Total Bookings</Typography>
<TextField variant="outlined" fullWidth disabled value={selectedCustomer?.totalbookings ?? 0} />
</Grid>
<Grid item xs={12} sm={6} sx={{ cursor: 'not-allowed' }}>
<Typography sx={{ mb: 1 }}>Joined</Typography>
<TextField
variant="outlined"
fullWidth
value={selectedCustomer.city || city || ''}
onChange={(e) => {
const value = e.target.value;
setSelectedCustomer((prev) => ({
...prev,
city: value
}));
// setCity(e.target.value);
}}
disabled
value={selectedCustomer?.createdat ? new Date(selectedCustomer.createdat).toLocaleDateString() : '—'}
/>
</Grid>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>State</Typography>
<TextField
variant="outlined"
fullWidth
value={selectedCustomer.state || state || ''}
onChange={(e) => {
const value = e.target.value;
setSelectedCustomer((prev) => ({
...prev,
state: value
}));
// setState(e.target.value);
}}
/>
</Grid>
<Grid item xs={6}>
<Typography sx={{ mb: 1 }}>Postcode</Typography>
<TextField
variant="outlined"
fullWidth
value={postcode == '' ? selectedCustomer.postcode : postcode}
onChange={(e) => {
const value = e.target.value;
setSelectedCustomer((prev) => ({
...prev,
postcode: value
}));
// setPostcode(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<Typography sx={{ mb: 1 }}>Landmark</Typography>
<TextField
variant="outlined"
fullWidth
defaultValue={selectedCustomer.landmark}
onChange={(e) => {
setSelectedCustomer({
...selectedCustomer,
landmark: e.target.value
});
}}
/>
</Grid>
<Grid item xs={6} sx={{ cursor: 'not-allowed' }}>
<Typography sx={{ mb: 1 }}>Latitude</Typography>
<TextField variant="outlined" fullWidth value={latlong.lat} sx={{ cursor: 'not-allowed' }} />
</Grid>
<Grid item xs={6} sx={{ cursor: 'not-allowed' }}>
<Typography sx={{ mb: 1 }}>Longitude</Typography>
<TextField readonly variant="outlined" fullWidth value={latlong.lng} />
</Grid>
</Grid>
</DialogContent>
<DialogActions sx={{ mr: 2, mb: 2 }}>

View File

@@ -1,9 +0,0 @@
const Dashboard = () => {
return (
<>
<h1>Dashboard</h1>
</>
);
};
export default Dashboard;

View File

@@ -0,0 +1,317 @@
import { useNavigate } from 'react-router-dom';
import { Avatar, Box, Chip, Grid, Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography, Button } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import dayjs from 'dayjs';
import {
MdOutlineLocalShipping,
MdOutlinePendingActions,
MdOutlineCheckCircle,
MdTwoWheeler,
MdOutlineSmartToy,
MdLocationCity,
MdArrowForward,
MdCircle
} from 'react-icons/md';
import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import { fetchHubs } from 'pages/api/api';
const DT = {
radiusCard: 16,
textPrimary: '#0f172a',
textSecondary: '#64748b',
textMuted: '#94a3b8',
borderSubtle: '#e2e8f0',
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc'
};
const a = (c, suffix) => `${c}${suffix}`;
const tint = (c) => a(c, '08');
const soft = (c) => a(c, '18');
const edge = (c) => a(c, '55');
const BRAND = '#C01227';
// Static list — the actual multi-agent pipeline (decide-assignment etc.) has
// no status endpoint yet, so this is presentational only, per spec.
const AGENTS = [
'Intake Agent',
'Geocoding Agent',
'Pricing Agent',
'Miler Matching Agent',
'Route Optimisation Agent',
'Notification Agent',
'Fraud Detection Agent',
'Reconciliation Agent'
];
const fetchBookingsByStatus = async (status, pagesize = 1) => {
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status, pagesize } });
return res.data;
};
const Dashboard = () => {
const navigate = useNavigate();
const { data: activeStatusCounts, isLoading: activeLoading } = useQuery({
queryKey: ['dashboardActiveCounts'],
queryFn: async () => {
const [assigned, scheduled, atCustomer] = await Promise.all([
fetchBookingsByStatus('Miler_Assigned'),
fetchBookingsByStatus('Pickup_Scheduled'),
fetchBookingsByStatus('At_Customer')
]);
return (assigned.total || 0) + (scheduled.total || 0) + (atCustomer.total || 0);
}
});
const { data: pendingCount, isLoading: pendingLoading } = useQuery({
queryKey: ['dashboardPendingCount'],
queryFn: async () => (await fetchBookingsByStatus('Pending_Pickup')).total || 0
});
const { data: deliveredTodayCount, isLoading: deliveredLoading } = useQuery({
queryKey: ['dashboardDeliveredToday'],
queryFn: async () => (await fetchBookingsByStatus('Delivered')).total || 0
});
const { data: milers = [], isLoading: milersLoading } = useQuery({
queryKey: ['dashboardMilers'],
queryFn: async () => {
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
return res.data?.data || [];
}
});
const availableMilers = milers.filter((m) => m.availabilitystatus === 'Available').length;
const { data: hubs = [] } = useQuery({ queryKey: ['fetchHubs'], queryFn: fetchHubs });
const { data: recentBookings = [], isLoading: recentLoading } = useQuery({
queryKey: ['dashboardRecentBookings'],
queryFn: async () => {
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { pagesize: 10 } });
return res.data?.data || [];
}
});
// No `city` field is confirmed on a booking (only pickupaddress/deliveryaddress
// were ever specified) -- derives a best-effort city breakdown from hubs
// instead, since hubs do have a confirmed `city` field. Flagged rather than
// guessed at a booking-level city grouping that may not exist.
const cityRows = Object.values(
hubs.reduce((acc, h) => {
const city = h.city || 'Unknown';
if (!acc[city]) acc[city] = { city, milers: 0, hubs: 0 };
acc[city].hubs += 1;
acc[city].milers += milers.filter((m) => m.hubid === h.hubid).length;
return acc;
}, {})
);
const kpis = [
{ key: 'active', label: 'Active Bookings', color: BRAND, icon: MdOutlineLocalShipping, value: activeStatusCounts ?? 0, loading: activeLoading },
{ key: 'pending', label: 'Pending Assignment', color: '#f59e0b', icon: MdOutlinePendingActions, value: pendingCount ?? 0, loading: pendingLoading },
{ key: 'delivered', label: 'Delivered Today', color: '#10b981', icon: MdOutlineCheckCircle, value: deliveredTodayCount ?? 0, loading: deliveredLoading },
{ key: 'milers', label: 'Milers Available', color: '#0ea5e9', icon: MdTwoWheeler, value: availableMilers, loading: milersLoading }
];
return (
<>
<PageHeader title="Dashboard" subtitle="Live · Operations overview" live />
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
{kpis.map((item) => {
const Icon = item.icon;
return (
<Grid item key={item.key} xs={6} sm={3}>
<StatCard title={item.label} value={item.value} icon={<Icon size={20} />} color={item.color} loading={item.loading} />
</Grid>
);
})}
</Grid>
<Grid container spacing={2.5}>
<Grid item xs={12} md={7}>
<Paper elevation={0} sx={{ borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', overflow: 'hidden' }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ p: 2, borderBottom: `1px solid ${DT.divider}` }}>
<MdLocationCity size={18} color={BRAND} />
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
City Breakdown
</Typography>
</Stack>
<TableContainer>
<Table>
<TableHead>
<TableRow sx={{ '& th': { color: DT.textSecondary, fontWeight: 800, fontSize: 11, textTransform: 'uppercase' } }}>
<TableCell>City</TableCell>
<TableCell align="center">Hubs</TableCell>
<TableCell align="center">Milers</TableCell>
<TableCell align="center">Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{cityRows.length === 0 && (
<TableRow>
<TableCell colSpan={4} sx={{ textAlign: 'center', py: 4, color: DT.textMuted }}>
No hub data yet.
</TableCell>
</TableRow>
)}
{cityRows.map((row) => (
<TableRow key={row.city} sx={{ '& td': { borderBottom: `1px solid ${DT.divider}` } }}>
<TableCell sx={{ fontWeight: 700 }}>{row.city}</TableCell>
<TableCell align="center">{row.hubs}</TableCell>
<TableCell align="center">{row.milers}</TableCell>
<TableCell align="center">
<Chip
size="small"
label={row.milers > 0 ? 'Active' : 'Idle'}
sx={{
bgcolor: row.milers > 0 ? tint('#10b981') : tint('#94a3b8'),
color: row.milers > 0 ? '#10b981' : '#94a3b8',
border: `1px solid ${edge(row.milers > 0 ? '#10b981' : '#94a3b8')}`,
fontWeight: 700
}}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Paper>
<Paper elevation={0} sx={{ mt: 2.5, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', overflow: 'hidden' }}>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ p: 2, borderBottom: `1px solid ${DT.divider}` }}>
<Stack direction="row" alignItems="center" spacing={1}>
<MdOutlineLocalShipping size={18} color={BRAND} />
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
Recent Bookings
</Typography>
</Stack>
<Button size="small" endIcon={<MdArrowForward size={14} />} onClick={() => navigate('/nearle/orders')} sx={{ textTransform: 'none', fontWeight: 700 }}>
View all
</Button>
</Stack>
<TableContainer>
<Table>
<TableHead>
<TableRow sx={{ '& th': { color: DT.textSecondary, fontWeight: 800, fontSize: 11, textTransform: 'uppercase' } }}>
<TableCell>Booking</TableCell>
<TableCell>Status</TableCell>
<TableCell>Created</TableCell>
</TableRow>
</TableHead>
<TableBody>
{recentLoading && (
<TableRow>
<TableCell colSpan={3} sx={{ textAlign: 'center', py: 4, color: DT.textMuted }}>
Loading
</TableCell>
</TableRow>
)}
{!recentLoading && recentBookings.length === 0 && (
<TableRow>
<TableCell colSpan={3} sx={{ textAlign: 'center', py: 4, color: DT.textMuted }}>
No bookings yet.
</TableCell>
</TableRow>
)}
{recentBookings.map((b) => (
<TableRow
key={b.bookingid}
hover
sx={{ cursor: 'pointer', '& td': { borderBottom: `1px solid ${DT.divider}` } }}
onClick={() => navigate(`/nearle/bookings/${b.bookingid}`)}
>
<TableCell sx={{ fontWeight: 700 }}>{b.bookingreference || `#${b.bookingid}`}</TableCell>
<TableCell>
<Chip size="small" label={b.status || '—'} sx={{ bgcolor: tint(BRAND), color: BRAND, border: `1px solid ${edge(BRAND)}`, fontWeight: 700 }} />
</TableCell>
<TableCell sx={{ color: DT.textSecondary, fontSize: 13 }}>
{b.createdat ? dayjs(b.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Paper>
</Grid>
<Grid item xs={12} md={5}>
<Paper elevation={0} sx={{ borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', overflow: 'hidden' }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ p: 2, borderBottom: `1px solid ${DT.divider}` }}>
<MdOutlineSmartToy size={18} color="#6366f1" />
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
Agent System Status
</Typography>
</Stack>
<Stack spacing={0} sx={{ p: 1 }}>
{AGENTS.map((agent) => (
<Stack
key={agent}
direction="row"
alignItems="center"
justifyContent="space-between"
sx={{ px: 1.5, py: 1.25, borderRadius: 2, '&:hover': { bgcolor: DT.surfaceAlt } }}
>
<Stack direction="row" alignItems="center" spacing={1.25}>
<Avatar sx={{ width: 30, height: 30, bgcolor: soft('#6366f1'), color: '#6366f1' }}>
<MdOutlineSmartToy size={15} />
</Avatar>
<Typography variant="body2" sx={{ fontWeight: 600, color: DT.textPrimary }}>
{agent}
</Typography>
</Stack>
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdCircle size={8} color="#10b981" />
<Typography variant="caption" sx={{ color: '#10b981', fontWeight: 700 }}>
Running
</Typography>
</Stack>
</Stack>
))}
</Stack>
</Paper>
<Paper
elevation={0}
component="button"
onClick={() => navigate('/nearle/hubs')}
sx={{
mt: 2.5,
width: '100%',
p: 2,
borderRadius: `${DT.radiusCard}px`,
border: `1px solid ${edge(BRAND)}`,
background: tint(BRAND),
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
textAlign: 'left',
font: 'inherit'
}}
>
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar sx={{ bgcolor: BRAND, color: '#fff' }}>
<MdLocationCity size={18} />
</Avatar>
<Box>
<Typography sx={{ fontWeight: 800, color: DT.textPrimary }}>Manage Hubs</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{hubs.length} hub{hubs.length === 1 ? '' : 's'} configured
</Typography>
</Box>
</Stack>
<MdArrowForward size={18} color={BRAND} />
</Paper>
</Grid>
</Grid>
</>
);
};
export default Dashboard;

View File

@@ -18,7 +18,6 @@ import {
MdStorefront,
MdLocationOn,
MdDirectionsBike,
MdLocalShipping,
MdNotificationsActive,
MdPersonPin,
MdHistoryToggleOff,
@@ -26,8 +25,6 @@ import {
MdCancel,
MdInventory2,
MdHourglassEmpty,
MdRoute,
MdSkipNext,
MdTune,
MdMyLocation,
MdOutlineLocalShipping,
@@ -126,7 +123,7 @@ const DT = {
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc',
brand: '#662582'
brand: '#C01227'
};
// Quick alpha helpers (hex + percentage suffix). Mirrors the batch-dropdown
@@ -162,30 +159,34 @@ const pillFieldSx = () => ({
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: DT.textMuted }
});
// Status palette — drives tab pills, row status badges, dialogs.
// Status palette — drives tab pills, row status badges, dialogs. Keys are
// Doormile's real booking statuses, lowercased (row.status is compared
// lowercased everywhere below so exact backend casing doesn't matter).
const STATUS_META = {
pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
accepted: { label: 'Accepted', color: '#6366f1', icon: MdPersonPin },
arrived: { label: 'Arrived', color: '#06b6d4', icon: MdLocationOn },
picked: { label: 'Picked', color: '#8b5cf6', icon: MdInventory2 },
active: { label: 'Active', color: '#14b8a6', icon: MdRoute },
skipped: { label: 'Skipped', color: '#f97316', icon: MdSkipNext },
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
all: { label: 'All', color: '#94a3b8', icon: MdAllInclusive },
pending_pickup: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
miler_assigned: { label: 'Assigned', color: '#6366f1', icon: MdPersonPin },
pickup_scheduled: { label: 'Scheduled', color: '#06b6d4', icon: MdHistoryToggleOff },
at_customer: { label: 'At Customer', color: '#8b5cf6', icon: MdLocationOn },
picked_up: { label: 'Picked Up', color: '#14b8a6', icon: MdInventory2 },
at_hub: { label: 'At Hub', color: '#0ea5e9', icon: MdStorefront },
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
};
// Ordered status list driving the tabs row (left → right). Each entry binds
// the visual meta above to the `currentStatus` key the queries use AND to the
// `batchCounts` key (so the chip count for the tab is one lookup).
// Ordered status list driving the tabs row (left → right). `status` is the
// lowercased key into STATUS_META/batchCounts; `apiStatus` is the exact
// casing Doormile's /admin/bookings status filter expects.
const STATUS_TABS = [
{ status: 'pending', countKey: 'uncoveredLength' },
{ status: 'accepted', countKey: 'assignedLength' },
{ status: 'arrived', countKey: 'arrivedLength' },
{ status: 'picked', countKey: 'pickedLength' },
{ status: 'active', countKey: 'activeLength' },
{ status: 'skipped', countKey: 'skippedLength' },
{ status: 'delivered', countKey: 'coveredLength' },
{ status: 'cancelled', countKey: 'cancelLength' }
{ status: 'all', apiStatus: 'all', countKey: 'all' },
{ status: 'pending_pickup', apiStatus: 'Pending_Pickup', countKey: 'pending_pickup' },
{ status: 'miler_assigned', apiStatus: 'Miler_Assigned', countKey: 'miler_assigned' },
{ status: 'pickup_scheduled', apiStatus: 'Pickup_Scheduled', countKey: 'pickup_scheduled' },
{ status: 'at_customer', apiStatus: 'At_Customer', countKey: 'at_customer' },
{ status: 'picked_up', apiStatus: 'Picked_Up', countKey: 'picked_up' },
{ status: 'at_hub', apiStatus: 'At_Hub', countKey: 'at_hub' },
{ status: 'delivered', apiStatus: 'Delivered', countKey: 'delivered' },
{ status: 'cancelled', apiStatus: 'Cancelled', countKey: 'cancelled' }
];
// KPI palette + icons — mirrors the four cards across the top of the page.
@@ -322,10 +323,9 @@ const Deliveries = () => {
const [appId, setAppId] = useState(0);
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
const [tabstatus, setTabstatus] = useState('Pending');
const [tabstatus, setTabstatus] = useState('All');
const [tabvalue, setTabvalue] = useState(0);
const [open, setOpen] = useState(false);
const [datestatus, setDatestatus] = useState('Today');
const [kms, setKms] = useState('');
const [cumulativekms, setCumulativeKms] = useState();
const [deliveryamount, setDeliveryamount] = useState();
@@ -333,13 +333,12 @@ const Deliveries = () => {
const [currentorder, setCurrentorder] = useState({});
const [deliverylat, setDeliverylat] = useState('');
const [deliverylong, setDeliverylong] = useState('');
const [currentStatus, setCurrentStatus] = useState('pending');
const [currentStatus, setCurrentStatus] = useState('all');
const [updateStatus, setUpdateStatus] = useState('delivered');
const locationRef = useRef(null);
const tenantRef = useRef(null);
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(50);
const [totalCount, setTotalCount] = React.useState();
const [productCollapse, setProductCollapse] = useState(null);
const [orderHeaderid, setOrderHeaderId] = useState(null);
const [searchword, setSearchword] = useState('');
@@ -347,8 +346,6 @@ const Deliveries = () => {
const [menuAnchorEl, setMenuAnchorEl] = React.useState(null);
const [selectedRow, setSelectedRow] = useState(null);
const [loading1, setLoading1] = useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);
const [open2, setOpen2] = useState('');
const [cancelDeliveryOpen, setCancelDeliveryOpen] = useState(false);
const [changeDialogOpen, setChangeDialogOpen] = useState(false);
const [cancelFeed, setCancelFeed] = useState('');
@@ -488,7 +485,7 @@ const Deliveries = () => {
countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips
notifyRiderMutation.mutate(selectedRider.userfcmtoken);
},
onError: (err, { selectedRider, selectedRow }) => {
onError: (err, { selectedRow }) => {
logger.error(`Failed to change rider for order ID ${selectedRow?.orderid}:`, err);
opentoast(err.message, 'error');
setLoading1(false);
@@ -521,48 +518,9 @@ const Deliveries = () => {
setPage(0);
setTabvalue(i);
setRowsPerPage(50);
if (i === 0) {
setTabstatus('Pending');
setCurrentStatus('pending');
setTotalCount(countData?.uncoveredLength);
}
if (i === 1) {
setTabstatus('Assigned');
setCurrentStatus('accepted');
setTotalCount(countData?.assignedLength);
}
if (i === 2) {
setTabstatus('Arrived');
setCurrentStatus('arrived');
setTotalCount(countData?.arrivedLength);
}
if (i === 3) {
setTabstatus('Picked');
setCurrentStatus('picked');
setTotalCount(countData?.pickedLength);
}
if (i === 4) {
setTabstatus('Active');
setCurrentStatus('active');
setTotalCount(countData?.activeLength);
}
if (i === 5) {
setTabstatus('Skipped');
setCurrentStatus('skipped');
setTotalCount(countData?.skippedLength);
}
if (i === 6) {
setTabstatus('Delivered');
setCurrentStatus('delivered');
setTotalCount(countData?.coveredLength);
}
if (i === 7) {
setTabstatus('Cancelled');
setCurrentStatus('cancelled');
setTotalCount(countData?.cancelLength);
}
console.log(i);
const tab = STATUS_TABS[i];
setTabstatus(STATUS_META[tab.status]?.label || tab.status);
setCurrentStatus(tab.apiStatus);
setSearchword('');
};
@@ -660,21 +618,10 @@ const Deliveries = () => {
const q = String(debouncedSearch || '').trim().toLowerCase();
return countSourceRows.filter((r) => {
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return false;
const s = String(r.orderstatus || '').toLowerCase();
if (wantStatus && s !== wantStatus) return false;
const s = String(r.status || '').toLowerCase();
if (wantStatus && wantStatus !== 'all' && s !== wantStatus) return false;
if (q) {
const hay = [
r.deliverycustomer,
r.deliveryaddress,
r.deliverysuburb,
r.pickupcustomer,
r.pickupaddress,
r.pickupsuburb,
r.orderid,
r.tenantname,
r.ridername,
r.username
]
const hay = [r.bookingreference, r.bookingid, r.pickupaddress, r.deliveryaddress, r.assignedmileruserid]
.map((v) => String(v || '').toLowerCase())
.join(' ');
if (!hay.includes(q)) return false;
@@ -687,49 +634,12 @@ const Deliveries = () => {
// *Length keys returned by fetchCountAPI so the JSX swap-in is mechanical
// (countData?.uncoveredLength → batchCounts.uncoveredLength).
const batchCounts = useMemo(() => {
const c = {
uncoveredLength: 0,
assignedLength: 0,
arrivedLength: 0,
pickedLength: 0,
activeLength: 0,
skippedLength: 0,
coveredLength: 0,
cancelLength: 0
};
const c = {};
countSourceRows.forEach((r) => {
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return;
const s = String(r.orderstatus || '').toLowerCase();
switch (s) {
case 'pending':
c.uncoveredLength += 1;
break;
case 'accepted':
case 'assigned':
c.assignedLength += 1;
break;
case 'arrived':
c.arrivedLength += 1;
break;
case 'picked':
c.pickedLength += 1;
break;
case 'active':
c.activeLength += 1;
break;
case 'skipped':
c.skippedLength += 1;
break;
case 'delivered':
c.coveredLength += 1;
break;
case 'cancelled':
case 'canceled':
c.cancelLength += 1;
break;
default:
break;
}
const s = String(r.status || '').toLowerCase();
c[s] = (c[s] || 0) + 1;
c.all = (c.all || 0) + 1;
});
return c;
}, [countSourceRows, selectedBatch]);
@@ -807,13 +717,6 @@ const Deliveries = () => {
queryKey: ['fetchCountData', appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid, tabstatus],
queryFn: () => fetchCountAPI(appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid)
});
useEffect(() => {
console.log('countData', countData);
if (tabvalue === 0 && countData) {
setTotalCount(countData.uncoveredLength);
}
}, [countData]);
// ==============================|| fetchRidersList ||============================== //
const {
@@ -912,7 +815,7 @@ const Deliveries = () => {
setLocoName={setLocoName}
setPage={setPage}
pill
accentColor="#662582"
accentColor="#C01227"
icon={<MdMyLocation size={14} />}
placeholder="Select Zone"
paperComponent={SoftPaper}
@@ -962,7 +865,7 @@ const Deliveries = () => {
}}
>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: { xs: 1, md: 1.5 }, color: DT.textSecondary }}>
<Avatar sx={{ width: 28, height: 28, bgcolor: soft('#662582'), color: '#662582' }}>
<Avatar sx={{ width: 28, height: 28, bgcolor: soft('#C01227'), color: '#C01227' }}>
<MdTune size={16} />
</Avatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.6, textTransform: 'uppercase', color: DT.textSecondary }}>
@@ -1125,8 +1028,8 @@ const Deliveries = () => {
transition: 'border-color 0.15s, box-shadow 0.15s',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
},
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: '#94a3b8' }
}}
@@ -1442,10 +1345,10 @@ const Deliveries = () => {
overflowX: 'auto',
'&::-webkit-scrollbar': { width: '10px', height: '10px', cursor: 'pointer' },
'&::-webkit-scrollbar-thumb': {
backgroundColor: edge('#662582'),
backgroundColor: edge('#C01227'),
borderRadius: '8px',
cursor: 'pointer',
'&:hover': { backgroundColor: '#662582' }
'&:hover': { backgroundColor: '#C01227' }
},
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
@@ -1453,7 +1356,7 @@ const Deliveries = () => {
{(() => {
const showAction = tabstatus !== 'Cancelled' && tabstatus !== 'Delivered';
const showSelect = tabstatus == 'Created';
const totalCols = 15 + (showAction ? 1 : 0) + (showSelect ? 1 : 0);
const totalCols = 7 + (showAction ? 1 : 0) + (showSelect ? 1 : 0);
return isMobile ? (
/* ===================== MOBILE: card list ===================== */
<MobileCardList sx={{ p: 1.25 }}>
@@ -1481,15 +1384,14 @@ const Deliveries = () => {
</Stack>
)}
{filteredRows.map((row, index) => {
const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || {
label: row.orderstatus || '—',
const rowStatusMeta = STATUS_META[String(row.status || '').toLowerCase()] || {
label: row.status || '—',
color: '#94a3b8',
icon: MdHistoryToggleOff
};
const RowStatusIcon = rowStatusMeta.icon;
const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
const isOpen = productCollapse?.orderid === row?.orderid;
const chipSx = (c) => ({ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: c, fontWeight: 700, fontSize: 12, whiteSpace: 'nowrap' });
return (
<MobileCard
key={row.orderheaderid ?? `${row.tenantname}-${index}`}
@@ -1562,7 +1464,7 @@ const Deliveries = () => {
<IconButton
size="small"
onClick={(e) => handleMenuOpen(e, row)}
sx={{ borderRadius: 999, bgcolor: tint('#662582'), color: '#662582', border: `1px solid ${edge('#662582')}`, '&:hover': { bgcolor: soft('#662582') } }}
sx={{ borderRadius: 999, bgcolor: tint('#C01227'), color: '#C01227', border: `1px solid ${edge('#C01227')}`, '&:hover': { bgcolor: soft('#C01227') } }}
>
<EditOutlined />
</IconButton>
@@ -1571,54 +1473,35 @@ const Deliveries = () => {
</Stack>
<Box sx={{ minWidth: 0 }}>
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 15 }} noWrap>
{row.tenantname}
{row.bookingreference || `#${row.bookingid}`}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{[row.tenantsuburb, row.applocation].filter(Boolean).join(' · ') || '—'}
{row.createdat ? dayjs(row.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
</Typography>
</Box>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Order / Location" full>
<MobileField label="Pickup" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
{`${row.locationname}-(${row.locationsuburb})`}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.orderid} · {row.deliveryid}
{row.pickupaddress || '—'}
</Typography>
</MobileField>
<MobileField label="Pickup">
<MobileField label="Delivery" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
{row.pickupcustomer || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.pickupcontactno}
{row.deliveryaddress || '—'}
</Typography>
</MobileField>
<MobileField label="Drop">
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
{row.deliverycustomer || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.deliverycontactno}
</Typography>
</MobileField>
<MobileField label="Rider" full>
{row.ridername ? (
<MobileField label="Miler" full>
{row.assignedmileruserid ? (
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color="#8b5cf6" size={24}>
<MdDirectionsBike size={13} />
</AccentAvatar>
<Stack sx={{ minWidth: 0 }}>
<Typography sx={{ fontSize: 13, fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.ridername}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
ID #{row.userid} · {row.ridercontact || '—'}
</Typography>
</Stack>
<Typography sx={{ fontSize: 13, fontWeight: 700, color: DT.textPrimary }} noWrap>
#{row.assignedmileruserid}
</Typography>
</Stack>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
@@ -1626,46 +1509,12 @@ const Deliveries = () => {
</Typography>
)}
</MobileField>
<MobileField label="ETA" value={row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : '—'} />
<MobileField label="Transit">
<Box sx={chipSx('#06b6d4')}>{row.transitminutes || 0}m</Box>
</MobileField>
<MobileField label="Kms · plan / act">
<Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap>
<Box sx={chipSx('#ef4444')}>{row.kms || 0} km</Box>
<Box sx={chipSx('#10b981')}>{row.cumulativekms || 0} km</Box>
</Stack>
</MobileField>
<MobileField label="Amount · chg / amt">
<Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap>
<Box sx={chipSx('#ef4444')}> {row.deliverycharges?.toFixed(2) ?? '0.00'}</Box>
<Box sx={chipSx('#10b981')}> {row.deliveryamt?.toFixed(2) ?? '0.00'}</Box>
</Stack>
</MobileField>
<MobileField label="Qty" value={row.Quantity || '—'} />
<MobileField label="COD">
<Typography sx={{ fontSize: 13, fontWeight: 800, color: row.collectionamt ? '#ef4444' : DT.textMuted }}>
{row.collectionamt ? `${row.collectionamt.toFixed(2)}` : '—'}
</Typography>
</MobileField>
<MobileField label="Step">
{row.step ? (
<Box sx={{ ...chipSx('#662582'), minWidth: 30, fontWeight: 800 }}>{row.step}</Box>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}></Typography>
)}
</MobileField>
{row.notes && (
<MobileField label="Notes" full>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>{row.notes}</Typography>
</MobileField>
)}
</MobileFieldGrid>
{isOpen && (
<Box sx={{ mt: 1.5, p: 1.25, borderRadius: 2, bgcolor: DT.surfaceAlt, border: `1px solid ${DT.divider}` }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
<AccentAvatar color="#662582" size={22}><MdInventory2 size={12} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#662582' }}>
<AccentAvatar color="#C01227" size={22}><MdInventory2 size={12} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
Product Details
</Typography>
</Stack>
@@ -1746,19 +1595,11 @@ const Deliveries = () => {
)}
<TableCell>#</TableCell>
<TableCell>Status</TableCell>
<TableCell>Tenant</TableCell>
<TableCell>Order / Location</TableCell>
<TableCell>Booking</TableCell>
<TableCell>Pickup</TableCell>
<TableCell>Drop</TableCell>
<TableCell>Rider</TableCell>
<TableCell>ETA</TableCell>
<TableCell>Transit</TableCell>
<TableCell>Kms</TableCell>
<TableCell>Amount</TableCell>
<TableCell>Notes</TableCell>
<TableCell>Step</TableCell>
<TableCell>Qty</TableCell>
<TableCell>COD</TableCell>
<TableCell>Delivery</TableCell>
<TableCell>Miler</TableCell>
<TableCell>Created</TableCell>
{showAction && <TableCell align="right">Action</TableCell>}
</TableRow>
</TableHead>
@@ -1794,8 +1635,8 @@ const Deliveries = () => {
</TableRow>
)}
{filteredRows.map((row, index) => {
const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || {
label: row.orderstatus || '—',
const rowStatusMeta = STATUS_META[String(row.status || '').toLowerCase()] || {
label: row.status || '—',
color: '#94a3b8',
icon: MdHistoryToggleOff
};
@@ -1876,108 +1717,38 @@ const Deliveries = () => {
</Typography>
</Stack>
</TableCell>
{/* Tenants */}
{/* Booking */}
<TableCell>
<Tooltip title={row.tenantadress}>
<Stack>
<Typography noWrap variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.tenantname}
</Typography>
<Typography noWrap variant="caption" sx={{ color: DT.textSecondary }}>
{row.tenantsuburb}
</Typography>
<Typography noWrap variant="caption" sx={{ color: DT.textMuted }}>
{row.applocation}
</Typography>
</Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.bookingreference || `#${row.bookingid}`}
</Typography>
</TableCell>
{/* Pickup */}
<TableCell sx={{ maxWidth: 220 }}>
<Tooltip title={row.pickupaddress || ''} placement="top">
<Typography variant="body2" noWrap>
{row.pickupaddress || '—'}
</Typography>
</Tooltip>
</TableCell>
{/* order details */}
<TableCell align="left">
<Tooltip title="Location Name-Suburb" placement="top">
<Typography variant="subtitle1" noWrap>
{`${row.locationname}-(${row.locationsuburb})`}
{/* Delivery */}
<TableCell sx={{ maxWidth: 220 }}>
<Tooltip title={row.deliveryaddress || ''} placement="top">
<Typography variant="body2" noWrap>
{row.deliveryaddress || '—'}
</Typography>
</Tooltip>
<Stack display={'flex'} flexDirection={'row'} gap={3}>
<Stack>
<Tooltip title="Order Id" placement="top">
<Typography variant="body2" noWrap>
{row.orderid}
</Typography>
</Tooltip>
<Tooltip title="Ordered date" placement="top">
<Typography noWrap sx={{ fontSize: '12px' }}>
{dayjs(row.orderdate).utc().format('DD/MM/YYYY')}
</Typography>
<Typography noWrap sx={{ fontSize: '11px' }}>
{dayjs(row.orderdate).utc().format('hh:mm A')}
</Typography>
</Tooltip>
</Stack>
-
<Stack>
<Tooltip title="Delivery Id" placement="top">
<Typography variant="body2" noWrap>
{row.deliveryid}
</Typography>
</Tooltip>
<Tooltip title="Delivery date" placement="top">
<Typography noWrap sx={{ fontSize: '12px' }}>
{dayjs(row.deliverydate).utc().format('DD/MM/YYYY')}
</Typography>
<Typography noWrap sx={{ fontSize: '11px' }}>
{dayjs(row.deliverydate).utc().format('hh:mm A')}
</Typography>
</Tooltip>
</Stack>
</Stack>
</TableCell>
{/* pickup */}
<TableCell align="left">
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{row.pickupcustomer}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>{row.pickupcontactno}</Typography>
<Tooltip title={row.Pickupaddress} sx={{ whiteSpace: 'nowrap' }}>
<Typography variant="caption" sx={{ color: DT.textMuted }}>
{row.pickuplocation || (row.Pickupaddress ? row.Pickupaddress.slice(0, 14) + '…' : '—')}
</Typography>
</Tooltip>
</Stack>
</TableCell>
{/* drop */}
<TableCell align="left">
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{row.deliverycustomer}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>{row.deliverycontactno}</Typography>
<Tooltip title={row.deliveryaddress}>
<Typography variant="caption" sx={{ color: DT.textMuted, whiteSpace: 'nowrap' }}>
{row.deliverylocation || (row.deliveryaddress ? row.deliveryaddress.slice(0, 14) + '…' : '—')}
</Typography>
</Tooltip>
</Stack>
</TableCell>
{/* rider */}
{/* Miler */}
<TableCell>
{row.ridername ? (
{row.assignedmileruserid ? (
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color="#8b5cf6" size={28}>
<MdDirectionsBike size={14} />
</AccentAvatar>
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.ridername}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.userid} · {row.ridercontact || '—'}
</Typography>
</Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
#{row.assignedmileruserid}
</Typography>
</Stack>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
@@ -1985,116 +1756,10 @@ const Deliveries = () => {
</Typography>
)}
</TableCell>
{/* Estimated Delivery Time */}
<TableCell align="left">
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : '—'}
</Typography>
</TableCell>
{/* Transit Minutes */}
<TableCell align="left">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.5,
borderRadius: 999,
bgcolor: tint('#06b6d4'),
color: '#06b6d4',
fontWeight: 700,
fontSize: 12,
border: `1px solid ${edge('#06b6d4')}`
}}
>
{row.transitminutes || 0}m
</Box>
</TableCell>
{/* kms */}
{/* Created */}
<TableCell>
<Stack direction="column" spacing={0.5} alignItems="flex-start">
<Tooltip title="Planned KMS" placement="top">
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#ef4444'), color: '#ef4444', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#ef4444')}`, whiteSpace: 'nowrap', minWidth: 75 }}>
{row.kms || 0} km
</Box>
</Tooltip>
<Tooltip title="Actual KMS" placement="top">
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#10b981'), color: '#10b981', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#10b981')}`, whiteSpace: 'nowrap', minWidth: 75 }}>
{row.cumulativekms || 0} km
</Box>
</Tooltip>
</Stack>
</TableCell>
{/* amount */}
<TableCell align="left">
<Stack direction="column" spacing={0.5} alignItems="flex-start">
<Tooltip title="Delivery Charge" placement="top">
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#ef4444'), color: '#ef4444', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#ef4444')}`, whiteSpace: 'nowrap', minWidth: 85 }}>
{row.deliverycharges?.toFixed(2) ?? '0.00'}
</Box>
</Tooltip>
<Tooltip title="Delivery Amount" placement="top">
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#10b981'), color: '#10b981', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#10b981')}`, whiteSpace: 'nowrap', minWidth: 85 }}>
{row.deliveryamt?.toFixed(2) ?? '0.00'}
</Box>
</Tooltip>
</Stack>
</TableCell>
{/* notes */}
<TableCell>
{row.notes ? (
<Tooltip title={row.notes}>
<Typography variant="caption" sx={{ color: DT.textSecondary, maxWidth: 160, display: 'inline-block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{row.notes}
</Typography>
</Tooltip>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}></Typography>
)}
</TableCell>
{/* step */}
<TableCell align="center">
{row.step ? (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: 30,
height: 24,
px: 0.875,
borderRadius: 999,
bgcolor: tint('#662582'),
border: `1px solid ${edge('#662582')}`,
color: '#662582',
fontWeight: 800,
fontSize: 11
}}
>
{row.step}
</Box>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}></Typography>
)}
</TableCell>
{/* qty */}
<TableCell>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: row.Quantity ? DT.textPrimary : DT.textMuted, whiteSpace: 'nowrap' }}>
{row.Quantity || '—'}
</Typography>
</TableCell>
{/* COD */}
<TableCell>
<Typography
variant="subtitle2"
sx={{
fontWeight: 800,
color: row.collectionamt ? '#ef4444' : DT.textMuted,
whiteSpace: 'nowrap'
}}
>
{row.collectionamt ? `${row.collectionamt.toFixed(2)}` : '—'}
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.createdat ? dayjs(row.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
</Typography>
</TableCell>
{/* Action */}
@@ -2133,10 +1798,10 @@ const Deliveries = () => {
onClick={(e) => handleMenuOpen(e, row)}
sx={{
borderRadius: 999,
bgcolor: tint('#662582'),
color: '#662582',
border: `1px solid ${edge('#662582')}`,
'&:hover': { bgcolor: soft('#662582') }
bgcolor: tint('#C01227'),
color: '#C01227',
border: `1px solid ${edge('#C01227')}`,
'&:hover': { bgcolor: soft('#C01227') }
}}
>
<EditOutlined />
@@ -2168,9 +1833,9 @@ const Deliveries = () => {
background: '#fff'
}}
>
<Stack direction="row" alignItems="center" spacing={1} sx={{ px: 2, py: 1.25, borderBottom: `1px solid ${DT.divider}`, bgcolor: tint('#662582') }}>
<AccentAvatar color="#662582"><MdInventory2 size={14} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#662582' }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ px: 2, py: 1.25, borderBottom: `1px solid ${DT.divider}`, bgcolor: tint('#C01227') }}>
<AccentAvatar color="#C01227"><MdInventory2 size={14} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
Product Details
</Typography>
</Stack>
@@ -2298,7 +1963,7 @@ const Deliveries = () => {
}
}}
>
{selectedRow?.orderstatus !== 'delivered' && (
{String(selectedRow?.status || '').toLowerCase() !== 'delivered' && (
<MenuItem
onClick={() => {
notifyRiderMutation.mutate(selectedRow.userfcmtoken);
@@ -2309,7 +1974,9 @@ const Deliveries = () => {
Notify Rider
</MenuItem>
)}
{['pending', 'accepted', 'arrived'].includes(selectedRow?.orderstatus) && (
{['pending_pickup', 'miler_assigned', 'pickup_scheduled', 'at_customer'].includes(
String(selectedRow?.status || '').toLowerCase()
) && (
<MenuItem
onClick={() => {
if (!appId) {
@@ -2334,7 +2001,7 @@ const Deliveries = () => {
setDeliverylong(selectedRow.droplon);
setNotes(selectedRow.notes);
setDeliveryamount(selectedRow.deliveryamount);
setUpdateStatus(selectedRow.orderstatus || 'delivered');
setUpdateStatus(selectedRow.status || 'delivered');
setCurrentorder(selectedRow);
setDialogopen(true);
handleMenuClose();
@@ -2344,7 +2011,7 @@ const Deliveries = () => {
Update Status
</MenuItem>
)}
{selectedRow?.orderstatus !== 'cancelled' && selectedRow?.orderstatus !== 'delivered' && (
{!['cancelled', 'delivered'].includes(String(selectedRow?.status || '').toLowerCase()) && (
<MenuItem
sx={{ color: '#ef4444 !important' }}
onClick={() => {
@@ -2597,11 +2264,6 @@ const Deliveries = () => {
} else {
setStartdate(dayjs(range.startDate).format('YYYY-MM-DD'));
setEnddate(dayjs(range.endDate).format('YYYY-MM-DD'));
if (range.label) {
setDatestatus(range.label);
} else {
setDatestatus('');
}
}
console.log(range);
}}
@@ -2646,11 +2308,6 @@ const Deliveries = () => {
startDate: startOfMonth(addMonths(new Date(), -1)),
endDate: endOfMonth(addMonths(new Date(), -1))
}
// {
// label: 'All',
// startDate: new Date(),
// endDate: addDays(new Date(), -1),
// },
]}
/>
</DialogContent>
@@ -2778,14 +2435,14 @@ const Deliveries = () => {
borderRadius: 2,
bgcolor: '#fff',
'& fieldset': { borderColor: DT.borderSubtle },
'&:hover fieldset': { borderColor: '#662582' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` }
'&:hover fieldset': { borderColor: '#C01227' },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
}
}}
>
{['pending','accepted','started','arrived','delivered','cancelled'].map((s) => {
const m = STATUS_META[s] || { label: s, color: '#662582', icon: MdHistoryToggleOff };
const m = STATUS_META[s] || { label: s, color: '#C01227', icon: MdHistoryToggleOff };
const Ic = m.icon;
return (
<MenuItem key={s} value={s} sx={{ gap: 1 }}>
@@ -2812,9 +2469,9 @@ const Deliveries = () => {
borderRadius: 2,
bgcolor: '#fff',
'& fieldset': { borderColor: DT.borderSubtle },
'&:hover fieldset': { borderColor: '#662582' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` }
'&:hover fieldset': { borderColor: '#C01227' },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
}
}}
/>

View File

@@ -96,7 +96,7 @@ const ActiveSection = ({
);
}
const renderActiveDeliveryCard = (o, i) => {
const renderActiveDeliveryCard = (o) => {
const rid = o.rider_id || o.userid;
const rider = riders.find((r) => String(r.id) === String(rid));
const color = getRiderColor(rid);

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { MapContainer, TileLayer, Marker, Popup, Polyline, Tooltip, useMap, useMapEvents, ZoomControl } from 'react-leaflet';
import { MapContainer, TileLayer, Marker, Popup, Polyline, Tooltip, useMap, ZoomControl } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
// Side-effect import: patches L.Polyline so pathOptions.offset (in screen px)
@@ -22,7 +22,6 @@ import {
MdStraighten,
MdLocationOn,
MdMarkunreadMailbox,
MdMoveToInbox,
MdPlace,
MdTwoWheeler,
MdNotes,
@@ -59,7 +58,6 @@ import ProfitabilitySection from './ProfitabilitySection';
import ActiveSection from './ActiveSection';
import { fetchDeliveries, fetchAppLocations, getRiderPeriodicLogs, fetchRidersLogs, fetchBatchEfficiency } from '../../api/api';
import {
STATUS_STYLES,
getStatusStyle,
FINAL_STATUSES,
SKIPPED_STATUSES,
@@ -80,7 +78,7 @@ import logger from '../../../utils/logger';
// emerald for the actual GPS trail (signals "live / real" data). Per-step
// distinction in Combined view is carried by the numbered drop pins, which
// keep STEP_PALETTE so the timeline link to a specific delivery survives.
const COMBINED_PLANNED_COLOR = '#662582';
const COMBINED_PLANNED_COLOR = '#C01227';
const COMBINED_ACTUAL_COLOR = '#10b981';
const toNum = (v) => {
@@ -232,15 +230,6 @@ const getRowBatch = (r, fieldId = 'all', batches = BATCHES_DEFAULT) => {
return getBatchForHour(d.hour() + d.minute() / 60, batches);
};
// Sits inside the Compare MapContainer and unpins any pinned popup whenever
// the operator clicks empty map space. Markers' click events do NOT bubble
// to the map, so this only fires on background clicks (which is what we
// want — clicking elsewhere should release the pin).
function CompareMapClickUnpin({ onUnpin }) {
useMapEvents({ click: () => onUnpin() });
return null;
}
// Captures the Leaflet map instance for the parent component via a ref. Kept
// available even after the two-map Compare layout was unified into one map,
// since future per-step imperative zoom logic still needs a handle on the
@@ -983,12 +972,6 @@ const ANALYSIS_BATCH_WINDOWS = [
// Tolerant field-name lookup so the Analysis card still renders cleanly even
// if the API response uses slightly different keys than expected.
const analysisPick = (obj, keys) => {
for (const k of keys) {
if (obj && obj[k] != null && obj[k] !== '') return obj[k];
}
return null;
};
const analysisFormatNum = (v) => {
if (v == null) return '—';
if (typeof v === 'number') return v.toLocaleString('en-IN');
@@ -996,14 +979,6 @@ const analysisFormatNum = (v) => {
if (Number.isFinite(n)) return n.toLocaleString('en-IN');
return String(v);
};
const analysisFormatKm = (v) => (v == null ? '—' : `${parseFloat(v).toFixed(1)} km`);
const analysisFormatRupees = (v) => (v == null ? '—' : `${parseFloat(v).toFixed(0)}`);
const analysisFormatPct = (v) => {
if (v == null) return '—';
const n = parseFloat(v);
if (!Number.isFinite(n)) return '—';
return `${n > 1 ? n.toFixed(1) : (n * 100).toFixed(1)}%`;
};
// Parse "HH:mm:ss" or "HH:mm" → seconds since midnight. Returns null when the
// string is missing or malformed. Used to compute gantt percentages for the
// rider timelines on the Analysis page — the API ships those fields as bare
@@ -1104,7 +1079,7 @@ const Dispatch = ({
// • no batch is selected,
// • the previous request is still in flight (prevents queue
// stacking on slow networks),
// • the browser tab is hidden (saves API quota on routes.workolik).
// • the browser tab is hidden (saves API quota on the Doormile AI layer).
// Loading state is tracked through a ref so the interval doesn't
// reset on every in-flight flip. ─────────────────────────────────
const ANALYSIS_POLL_MS = 15000;
@@ -1163,7 +1138,6 @@ const Dispatch = ({
// Short-lived close timer for the general map order/marker popups.
// Gives the cursor a ~200ms window to travel from the marker onto the popup
// or vice versa without immediately triggering a close.
const activePopupMarkerRef = useRef(null);
const popupHoverTimerRef = useRef(null);
// Order shown in the centered popup overlay. Rendered outside the leaflet
// map (see `dispatch-popup-center` overlay near the bottom of the JSX) so
@@ -2150,19 +2124,14 @@ const Dispatch = ({
}
// "All Active Routes": the header must reflect exactly what the list/map
// shows — the in-progress orders and the riders working them — NOT the whole
// day's totals. We count only active deliveries (and `visibleRiders`, which
// is already gated to active-order riders) so the tiles can't disagree with
// the list/map below.
// day's totals. Source from `activeViewRiders`, the SAME sticky-cached list
// that <ActiveSection> renders as cards (see `visibleRiders={activeViewRiders}`
// below) — not `visibleRiders`, which additionally requires a rider to be
// present in the independently-polled GPS-log feed with status 'active'/
// 'pending'. That extra join rarely resolves, which is why this tile used to
// show 0 riders even while real rider cards were on screen.
if (isAllActiveView) {
// One order per rider, via getActiveOrder — the same "rider's single
// in-progress delivery" resolver used elsewhere on this page (see
// dispatchShared.js). A multi-drop batch can carry `orderstatus:
// 'active'` on more than one of a rider's stops at once (the current
// leg plus a queued one), so filtering allViewOrders by isActiveDelivery
// directly counted every such row and let Orders outnumber the distinct
// riders backing them (e.g. 5 orders / 3 riders). Resolving exactly one
// active order per activeViewRiders entry keeps the two tiles 1:1.
const activeOrders = activeViewRiders.map((r) => getActiveOrder(r.orders)).filter(Boolean);
const activeOrders = activeViewRiders.flatMap((r) => r.orders);
return {
orders: activeOrders.length,
riders: activeViewRiders.length,
@@ -3155,7 +3124,7 @@ const Dispatch = ({
? new Map(riderActualTracks.map((t) => [String(t.deliveryid), t.sequenceStep]))
: null;
return ordersToRender.map((o, idx) => {
return ordersToRender.map((o) => {
const rid = o.rider_id;
const active = rid ? activeRiders.has(rid) : true;
let color = getRiderColor(rid);
@@ -3261,9 +3230,13 @@ const Dispatch = ({
const routes = [];
const zoneRiderIds = focusedZone ? new Set(focusedZone.riders.map((zr) => String(zr.rider_id))) : null;
if (hidePlanned) return routes;
// visibleRiders === riders in every view except "All Active Routes", where
// it's pre-filtered to riders whose live GPS is currently active.
visibleRiders.forEach(r => {
// In "All Active Routes" draw for exactly the riders shown as cards
// (activeViewRiders) — not `visibleRiders`, which additionally requires a
// rider to be present in the independently-polled GPS-log feed with status
// 'active'/'pending'. That join rarely resolves, which left the map with
// no polyline for a rider whose active-order marker/card was clearly
// visible and clickable. Every other view keeps using `riders` (unfiltered).
(isAllActiveView ? activeViewRiders : riders).forEach(r => {
const isActive = activeRiders.has(r.id);
if (focusedRider && focusedRider.id !== r.id) return;
if (focusedKitchen && !focusedKitchen.riders.has(r.id)) return;
@@ -3478,13 +3451,6 @@ const Dispatch = ({
return routes;
};
const toggleRider = (rid) => {
const newActive = new Set(activeRiders);
if (newActive.has(rid)) newActive.delete(rid);
else newActive.add(rid);
setActiveRiders(newActive);
};
return (
<div className={`dispatch-container${embedded ? ' embedded' : ''}${compareOpen ? ' compare-open' : ''}`}>
{!embedded && (
@@ -5374,15 +5340,6 @@ const Dispatch = ({
(o) => o.deliveryid != null && String(o.deliveryid) === String(t.deliveryid)
);
const statusStyle = getStatusStyle(t.orderstatus);
const flagSvg = t.orderstatus
? `<svg class="cmark-flag" viewBox="0 0 18 22" xmlns="http://www.w3.org/2000/svg">
<line x1="1.5" y1="0" x2="1.5" y2="22" stroke="#0f172a" stroke-width="1.6" stroke-linecap="round"/>
<polygon points="2,1 17,1 13.5,5.5 17,10 2,10" fill="${statusStyle.bg}" stroke="#0f172a" stroke-width="0.6" stroke-linejoin="round"/>
${isDelivered ? '<polyline points="5,5.5 7,7.5 11,3.5" fill="none" stroke="#fff" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>' : ''}
</svg>`
: '';
const dropClasses = ['compare-step-pin'];
if (isFocusedStep) dropClasses.push('is-focused');
if (isDelivered) dropClasses.push('is-delivered');
@@ -5730,7 +5687,7 @@ const Dispatch = ({
style={{
boxShadow: 'var(--shadow-lg)',
background: compareOpen
? 'linear-gradient(135deg, #662582, #9255AB)'
? 'linear-gradient(135deg, #C01227, #D35968)'
: '#fff',
marginLeft: 8,
color: compareOpen ? '#fff' : undefined
@@ -6329,7 +6286,7 @@ const Dispatch = ({
<div className="da-section">
<div className="da-hero-row">
<div className="da-hero-card">
<div className="da-hero-icon" style={{ background: '#6625821f', color: '#662582' }}>
<div className="da-hero-icon" style={{ background: '#C012271f', color: '#C01227' }}>
<MdOutlineInventory2 />
</div>
<div className="da-hero-value">{analysisFormatNum(fleet.total_orders)}</div>
@@ -6810,7 +6767,7 @@ const Dispatch = ({
<div className="da-pos-modal-title-wrap">
<div
className="da-pos-modal-avatar"
style={{ background: `${riderPositionModal.color || '#662582'}22`, color: riderPositionModal.color || '#662582' }}
style={{ background: `${riderPositionModal.color || '#C01227'}22`, color: riderPositionModal.color || '#C01227' }}
>
<MdTwoWheeler />
</div>

View File

@@ -0,0 +1,533 @@
import { useState, Fragment } from 'react';
import axios from 'axios';
import {
Avatar,
Box,
Button,
Chip,
Collapse,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Grid,
IconButton,
MenuItem,
Paper,
Select,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Tooltip,
Typography
} from '@mui/material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { DeploymentUnitOutlined } from '@ant-design/icons';
import { MdAdd, MdEdit, MdKeyboardArrowDown, MdKeyboardArrowUp, MdOutlineWarehouse, MdTwoWheeler, MdStar } from 'react-icons/md';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import { OpenToast } from 'components/third-party/OpenToast';
import { fetchHubs, createHub, updateHub } from 'pages/api/api';
// ============================================================================
// DT token block — CLAUDE.md §6, copied verbatim.
// ============================================================================
const DT = {
radiusPill: 999,
radiusCard: 16,
radiusInner: 12,
shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
textPrimary: '#0f172a',
textSecondary: '#64748b',
textMuted: '#94a3b8',
borderSubtle: '#e2e8f0',
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc'
};
const a = (c, suffix) => `${c}${suffix}`;
const tint = (c) => a(c, '08');
const soft = (c) => a(c, '18');
const edge = (c) => a(c, '55');
const BRAND = '#C01227';
// Confirmed from the backend test: hubs carry `applocationid` (14), not a
// literal city string. Everything else here (contact/address/pincode as
// editable fields) is inferred from the create-body shape, not confirmed
// present on the GET response — flagged in the summary, not guessed silently.
const CITY_MAP = {
1: { name: 'Coimbatore', code: 'CBE', color: '#f59e0b' },
2: { name: 'Hyderabad', code: 'HYD', color: '#06b6d4' },
3: { name: 'Bangalore', code: 'BLR', color: '#8b5cf6' },
4: { name: 'Chennai', code: 'CHN', color: '#10b981' }
};
const CITY_NAME_TO_ID = Object.fromEntries(Object.entries(CITY_MAP).map(([id, c]) => [c.name, Number(id)]));
const HUB_TYPES = [
{ value: 'sorting_center', label: 'Sorting Center' },
{ value: 'spoke', label: 'Spoke' },
{ value: 'pickup_point', label: 'Pickup Point' }
];
const AccentAvatar = ({ color, selected, size = 24, children }) => (
<Avatar
sx={{
width: size,
height: size,
bgcolor: selected ? color : soft(color),
color: selected ? '#fff' : color,
transition: 'background-color 0.15s, color 0.15s'
}}
>
{children}
</Avatar>
);
const EMPTY_FORM = { hubname: '', hubtype: 'sorting_center', city: 'Coimbatore', capacity: '', contact: '', address: '', pincode: '' };
const fetchAllMilers = () =>
axios.get(`${process.env.REACT_APP_URL}/admin/milers`).then((r) => r.data?.data || []);
const Hubs = () => {
const queryClient = useQueryClient();
const [selectedCity, setSelectedCity] = useState('All');
const [dialogOpen, setDialogOpen] = useState(false);
const [editingHub, setEditingHub] = useState(null);
const [form, setForm] = useState(EMPTY_FORM);
const [expandedHubId, setExpandedHubId] = useState(null);
const { data: hubs = [], isLoading: hubsLoading } = useQuery({
queryKey: ['fetchHubs'],
queryFn: fetchHubs
});
const { data: milers = [] } = useQuery({
queryKey: ['fetchAllMilers'],
queryFn: fetchAllMilers
});
const createMutation = useMutation({
mutationFn: createHub,
onSuccess: () => {
OpenToast('Hub created', 'success', 2000);
setDialogOpen(false);
setForm(EMPTY_FORM);
queryClient.invalidateQueries({ queryKey: ['fetchHubs'] });
},
onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
});
const updateMutation = useMutation({
mutationFn: ({ id, body }) => updateHub(id, body),
onSuccess: () => {
OpenToast('Hub updated', 'success', 2000);
setDialogOpen(false);
setEditingHub(null);
setForm(EMPTY_FORM);
queryClient.invalidateQueries({ queryKey: ['fetchHubs'] });
},
onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
});
const filteredHubs =
selectedCity === 'All' ? hubs : hubs.filter((h) => CITY_MAP[h.applocationid]?.name === selectedCity);
const kpis = [
{ key: 'total', label: 'Total Hubs', color: BRAND, value: hubs.length },
{ key: 'sorting', label: 'Sorting Centers', color: '#3b82f6', value: hubs.filter((h) => h.hubtype === 'sorting_center').length },
{ key: 'spokes', label: 'Spokes', color: '#14b8a6', value: hubs.filter((h) => h.hubtype === 'spoke').length },
{ key: 'cities', label: 'Cities Covered', color: '#10b981', value: new Set(hubs.map((h) => h.applocationid).filter(Boolean)).size }
];
const openCreate = () => {
setEditingHub(null);
setForm(EMPTY_FORM);
setDialogOpen(true);
};
const openEdit = (hub) => {
setEditingHub(hub);
setForm({
hubname: hub.hubname || '',
hubtype: hub.hubtype || 'sorting_center',
city: CITY_MAP[hub.applocationid]?.name || 'Coimbatore',
capacity: hub.capacity ?? '',
contact: hub.contact || '',
address: hub.address || '',
pincode: hub.pincode || ''
});
setDialogOpen(true);
};
const handleSubmit = () => {
if (!form.hubname) {
OpenToast('Enter a hub name', 'warning', 2000);
return;
}
const body = {
hubname: form.hubname,
hubtype: form.hubtype,
applocationid: CITY_NAME_TO_ID[form.city],
capacity: form.capacity === '' ? undefined : Number(form.capacity),
contact: form.contact,
address: form.address,
pincode: form.pincode
};
if (editingHub) {
updateMutation.mutate({ id: editingHub.hubid, body });
} else {
createMutation.mutate(body);
}
};
return (
<>
{/* ============================================= || Header || ============================================= */}
<Paper
elevation={0}
sx={{
p: { xs: 2, md: 3 },
borderRadius: `${DT.radiusCard}px`,
background: 'linear-gradient(135deg, #C012270A 0%, #D359680A 100%)',
border: '1px solid',
borderColor: DT.borderSubtle,
mb: { xs: 1.5, md: 2 }
}}
>
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" alignItems={{ xs: 'flex-start', sm: 'center' }} spacing={2}>
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 48, height: 48, bgcolor: BRAND, color: '#fff' }}>
<DeploymentUnitOutlined style={{ fontSize: 24 }} />
</Avatar>
<Box>
<Typography variant="h3">Hubs</Typography>
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ mt: 0.25 }}>
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: '#10b981', animation: 'pulse 1.6s infinite' }} />
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
Live · {selectedCity}
</Typography>
</Stack>
</Box>
</Stack>
<Stack direction="row" spacing={1} flexWrap="wrap">
{['All', ...Object.values(CITY_MAP).map((c) => c.name)].map((c) => (
<Chip
key={c}
label={c}
onClick={() => setSelectedCity(c)}
sx={{
fontWeight: 700,
bgcolor: selectedCity === c ? BRAND : '#fff',
color: selectedCity === c ? '#fff' : DT.textSecondary,
border: `1px solid ${selectedCity === c ? BRAND : DT.borderSubtle}`,
'&:hover': { bgcolor: selectedCity === c ? BRAND : DT.surfaceAlt }
}}
/>
))}
</Stack>
</Stack>
</Paper>
{/* ============================================= || KPI Cards || ============================================= */}
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
{kpis.map((item) => (
<Grid item key={item.key} xs={6} sm={3}>
<Paper
elevation={0}
sx={{
p: 2,
borderRadius: `${DT.radiusInner}px`,
border: '1px solid',
borderColor: DT.borderSubtle,
borderTop: `3px solid ${item.color}`,
background: '#fff',
boxShadow: DT.shadowSoft
}}
>
<Typography variant="caption" sx={{ fontWeight: 800, color: DT.textMuted, textTransform: 'uppercase', letterSpacing: 0.5 }}>
{item.label}
</Typography>
<Typography variant="h3" sx={{ mt: 0.5, color: DT.textPrimary }}>
{hubsLoading ? '—' : item.value}
</Typography>
</Paper>
</Grid>
))}
</Grid>
{/* ============================================= || New Hub || ============================================= */}
<Stack direction="row" justifyContent="flex-end" sx={{ mb: 1.5 }}>
<Button
variant="contained"
startIcon={<MdAdd size={16} />}
onClick={openCreate}
sx={{ borderRadius: DT.radiusPill, textTransform: 'none', fontWeight: 700, bgcolor: BRAND, boxShadow: 'none', '&:hover': { bgcolor: '#900E1D', boxShadow: 'none' } }}
>
New Hub
</Button>
</Stack>
{/* ============================================= || Table || ============================================= */}
<Paper elevation={0} sx={{ borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, overflow: 'hidden', background: '#fff' }}>
<TableContainer sx={{ maxHeight: 'calc(100vh - 190px)' }}>
<Table stickyHeader sx={{ minWidth: 900 }}>
<TableHead>
<TableRow
sx={{
'& th': {
backgroundColor: DT.surfaceAlt,
color: DT.textSecondary,
fontSize: 11,
fontWeight: 800,
letterSpacing: 0.6,
textTransform: 'uppercase',
whiteSpace: 'nowrap',
borderBottom: `1px solid ${DT.borderSubtle}`,
py: 1.25,
px: 2
}
}}
>
<TableCell>Hub Name</TableCell>
<TableCell>City</TableCell>
<TableCell>Type</TableCell>
<TableCell>Capacity</TableCell>
<TableCell align="center">Milers</TableCell>
<TableCell>Status</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{hubsLoading && <OrdersTableSkeleton col={5} />}
{!hubsLoading && filteredHubs.length === 0 && (
<TableRow>
<TableCell colSpan={7} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdOutlineWarehouse size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No hubs to show
</Typography>
</Stack>
</TableCell>
</TableRow>
)}
{filteredHubs.map((hub) => {
const hubMilers = milers.filter((m) => m.hubid === hub.hubid);
const expanded = expandedHubId === hub.hubid;
const city = CITY_MAP[hub.applocationid];
const isActive = (hub.status || 'active').toLowerCase() === 'active';
return (
<Fragment key={hub.hubid}>
<TableRow sx={{ '& td': { borderBottom: `1px solid ${DT.divider}`, py: 1.5, px: 2 }, '&:hover': { backgroundColor: DT.surfaceAlt } }}>
<TableCell>
<Typography variant="subtitle2" sx={{ fontWeight: 800, color: BRAND }}>
{hub.hubname || '—'}
</Typography>
</TableCell>
<TableCell>
{city ? (
<Chip size="small" label={city.name} sx={{ bgcolor: tint(city.color), color: city.color, border: `1px solid ${edge(city.color)}`, fontWeight: 700 }} />
) : (
<Typography variant="body2" sx={{ color: DT.textMuted }}>
</Typography>
)}
</TableCell>
<TableCell>
<Typography variant="body2" sx={{ color: DT.textSecondary, textTransform: 'capitalize' }}>
{(hub.hubtype || '—').replace(/_/g, ' ')}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2">{hub.capacity ?? '—'}</Typography>
</TableCell>
<TableCell align="center">
<Tooltip title="View milers at this hub">
<Chip
size="small"
icon={<MdTwoWheeler size={14} />}
label={hubMilers.length}
onClick={() => setExpandedHubId(expanded ? null : hub.hubid)}
sx={{ bgcolor: tint(BRAND), color: BRAND, border: `1px solid ${edge(BRAND)}`, fontWeight: 700, cursor: 'pointer' }}
/>
</Tooltip>
</TableCell>
<TableCell>
<Chip
size="small"
label={isActive ? 'Active' : 'Inactive'}
sx={{
bgcolor: tint(isActive ? '#10b981' : '#ef4444'),
color: isActive ? '#10b981' : '#ef4444',
border: `1px solid ${edge(isActive ? '#10b981' : '#ef4444')}`,
fontWeight: 700
}}
/>
</TableCell>
<TableCell align="right">
<Stack direction="row" justifyContent="flex-end" spacing={0.75}>
<Tooltip title="Edit hub">
<IconButton
size="small"
onClick={() => openEdit(hub)}
sx={{ bgcolor: soft(BRAND), color: BRAND, border: `1px solid ${edge(BRAND)}`, '&:hover': { bgcolor: BRAND, color: '#fff' } }}
>
<MdEdit size={14} />
</IconButton>
</Tooltip>
<IconButton size="small" onClick={() => setExpandedHubId(expanded ? null : hub.hubid)}>
{expanded ? <MdKeyboardArrowUp size={16} /> : <MdKeyboardArrowDown size={16} />}
</IconButton>
</Stack>
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={7} sx={{ p: 0, border: 0 }}>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<Box sx={{ p: 2, bgcolor: DT.surfaceAlt }}>
<Typography variant="caption" sx={{ fontWeight: 800, color: DT.textSecondary, textTransform: 'uppercase', letterSpacing: 0.5 }}>
Milers at {hub.hubname}
</Typography>
{hubMilers.length === 0 ? (
<Typography variant="body2" sx={{ color: DT.textMuted, mt: 1 }}>
No milers assigned to this hub.
</Typography>
) : (
<TableContainer sx={{ mt: 1, borderRadius: 2, border: `1px solid ${DT.borderSubtle}`, background: '#fff' }}>
<Table size="small">
<TableHead>
<TableRow sx={{ '& th': { color: DT.textSecondary, fontWeight: 800, fontSize: 10.5, textTransform: 'uppercase' } }}>
<TableCell>Miler</TableCell>
<TableCell>Phone</TableCell>
<TableCell>Status</TableCell>
<TableCell align="right">Rating</TableCell>
</TableRow>
</TableHead>
<TableBody>
{hubMilers.map((m) => {
const availColor = m.availabilitystatus === 'Available' ? '#10b981' : m.availabilitystatus === 'On_Break' ? '#f59e0b' : '#94a3b8';
return (
<TableRow key={m.userid}>
<TableCell>
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color={BRAND} size={26}>
<MdTwoWheeler size={13} />
</AccentAvatar>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{m.displayname || `Miler #${m.userid}`}
</Typography>
</Stack>
</TableCell>
<TableCell>
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
{m.phone || '—'}
</Typography>
</TableCell>
<TableCell>
<Chip
size="small"
label={m.availabilitystatus || '—'}
sx={{ bgcolor: tint(availColor), color: availColor, border: `1px solid ${edge(availColor)}`, fontWeight: 700 }}
/>
</TableCell>
<TableCell align="right">
<Stack direction="row" alignItems="center" justifyContent="flex-end" spacing={0.5}>
<MdStar size={13} style={{ color: '#f59e0b' }} />
<Typography variant="body2">{m.rating ?? '—'}</Typography>
</Stack>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
</Collapse>
</TableCell>
</TableRow>
</Fragment>
);
})}
</TableBody>
</Table>
</TableContainer>
</Paper>
{/* ============================================= || Create / Edit dialog || ============================================= */}
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth PaperProps={{ sx: { borderRadius: 3 } }}>
<DialogTitle sx={{ background: 'linear-gradient(135deg, #C01227 0%, #D35968 100%)', color: '#fff' }}>
{editingHub ? `Edit ${editingHub.hubname}` : 'Create New Hub'}
</DialogTitle>
<DialogContent sx={{ mt: 2 }}>
<Grid container spacing={2.5} sx={{ mt: 0.5 }}>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>Hub Name</Typography>
<TextField fullWidth value={form.hubname} onChange={(e) => setForm({ ...form, hubname: e.target.value })} />
</Grid>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>Hub Type</Typography>
<Select fullWidth value={form.hubtype} onChange={(e) => setForm({ ...form, hubtype: e.target.value })}>
{HUB_TYPES.map((t) => (
<MenuItem key={t.value} value={t.value}>
{t.label}
</MenuItem>
))}
</Select>
</Grid>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>City</Typography>
<Select fullWidth value={form.city} onChange={(e) => setForm({ ...form, city: e.target.value })}>
{Object.values(CITY_MAP).map((c) => (
<MenuItem key={c.name} value={c.name}>
{c.name}
</MenuItem>
))}
</Select>
</Grid>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>Capacity</Typography>
<TextField fullWidth type="number" value={form.capacity} onChange={(e) => setForm({ ...form, capacity: e.target.value })} />
</Grid>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>Contact</Typography>
<TextField fullWidth value={form.contact} onChange={(e) => setForm({ ...form, contact: e.target.value })} />
</Grid>
<Grid item xs={12} sm={6}>
<Typography sx={{ mb: 1 }}>Pincode</Typography>
<TextField fullWidth inputProps={{ maxLength: 6 }} value={form.pincode} onChange={(e) => setForm({ ...form, pincode: e.target.value.replace(/\D/g, '') })} />
</Grid>
<Grid item xs={12}>
<Typography sx={{ mb: 1 }}>Address</Typography>
<TextField fullWidth multiline minRows={2} value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} />
</Grid>
</Grid>
</DialogContent>
<DialogActions sx={{ p: 2.5 }}>
<Button onClick={() => setDialogOpen(false)} color="secondary" variant="outlined">
Cancel
</Button>
<Button
onClick={handleSubmit}
variant="contained"
disabled={createMutation.isLoading || updateMutation.isLoading}
sx={{ bgcolor: BRAND, '&:hover': { bgcolor: '#900E1D' } }}
>
{editingHub ? 'Save Changes' : 'Create Hub'}
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default Hubs;

View File

@@ -1,859 +0,0 @@
import React, { useState, useMemo } from 'react';
import { Outlet, useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import dayjs from 'dayjs';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
import {
Avatar,
Box,
Divider,
Grid,
IconButton,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TablePagination,
TableRow,
Tooltip,
Typography,
useMediaQuery
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import {
MdReceiptLong,
MdDashboard,
MdHourglassEmpty,
MdReportProblem,
MdCheckCircle,
MdGroups,
MdEventNote,
MdCurrencyRupee,
MdVisibility,
MdInventory2,
MdOutlinePendingActions,
MdOutlineCheckCircle
} from 'react-icons/md';
import { fetchinvoiceinsight, fetchdeliverylist } from 'pages/api/api';
import Loader from 'components/Loader';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
// ============================================================================
// Design tokens — shared with deliveries / tenants / customers / pricing /
// orders-details / riders-summary pages.
// ============================================================================
const DT = {
radiusPill: 999,
radiusCard: 14,
shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)',
shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)',
shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)',
textPrimary: '#0f172a',
textSecondary: '#64748b',
textMuted: '#94a3b8',
borderSubtle: '#e2e8f0',
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc'
};
const a = (c, suffix) => `${c}${suffix}`;
const tint = (c) => a(c, '08');
const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55');
const BRAND = '#662582';
const AccentAvatar = ({ color, selected, size = 24, children }) => (
<Avatar
sx={{
width: size,
height: size,
bgcolor: selected ? color : soft(color),
color: selected ? '#fff' : color,
transition: 'background-color 0.15s, color 0.15s'
}}
>
{children}
</Avatar>
);
// Bill status → tab visual meta (semantic colours; brand purple reserved for "All").
const STATUS_META = {
0: { key: 'all', label: 'All', color: BRAND, icon: MdDashboard, countKey: 'totalcount' },
1: { key: 'open', label: 'Open', color: '#ef4444', icon: MdHourglassEmpty, countKey: 'pendingcount' },
2: { key: 'overdue', label: 'Overdue', color: '#f59e0b', icon: MdReportProblem, countKey: 'overduecount' },
3: { key: 'paid', label: 'Paid', color: '#10b981', icon: MdCheckCircle, countKey: 'paidcount' }
};
const STATUS_TABS = [0, 1, 2, 3];
function formatNumberToRupees(value) {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 2
}).format(Number(value) || 0);
}
const Invoice = () => {
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [billStatus, setBillStatus] = useState(0);
const [isloader, setIsLoader] = useState(false);
const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const handleDebouncedSearch = React.useCallback((val) => {
setDebouncedSearch(val);
setPage(0);
}, []);
// ============================================= || fetchinvoiceinsight ||
const {
data: insightdata,
isLoading: isInsightLoading,
isError: isInsightError,
error: insightError
} = useQuery({
queryKey: ['invoiceInsight'],
queryFn: fetchinvoiceinsight,
refetchInterval: 300000
});
// ============================================= || fetchdeliverylist ||
// NOTE: queryKey shape MUST stay `[billStatus]` — `fetchdeliverylist`
// destructures `const [billStatus] = queryKey`.
const {
data: deliveryList,
isLoading: isDeliveryLoading,
isError: isDeliveryError,
error: deliveryError
} = useQuery({
queryKey: [billStatus],
queryFn: fetchdeliverylist,
refetchInterval: 300000
});
const isLoading = isInsightLoading || isDeliveryLoading;
const isError = isInsightError || isDeliveryError;
const errorMessage = insightError?.message || deliveryError?.message;
// Client-side filter across tenant name, contact person, invoice number.
const filteredList = useMemo(() => {
if (!deliveryList) return [];
if (!debouncedSearch) return deliveryList;
const q = debouncedSearch.toLowerCase().trim();
return deliveryList.filter((row) =>
[row.tenantname, row.contactperson, String(row.invoiceno)]
.filter(Boolean)
.some((field) => String(field).toLowerCase().includes(q))
);
}, [deliveryList, debouncedSearch]);
const activePage = useMemo(() => {
const maxPage = Math.max(0, Math.ceil(filteredList.length / rowsPerPage) - 1);
return Math.min(page, maxPage);
}, [filteredList.length, page, rowsPerPage]);
// Keep page state in sync when filters or data updates shrink the list below current page
React.useEffect(() => {
if (page !== activePage) {
setPage(activePage);
}
}, [page, activePage]);
const pagedList = useMemo(
() => filteredList.slice(activePage * rowsPerPage, activePage * rowsPerPage + rowsPerPage),
[filteredList, activePage, rowsPerPage]
);
const grandTotal = useMemo(
() => filteredList.reduce((sum, r) => sum + (Number(r.totalamount) || 0), 0),
[filteredList]
);
const pageTotal = useMemo(
() => pagedList.reduce((sum, r) => sum + (Number(r.totalamount) || 0), 0),
[pagedList]
);
const handleChangePage = (event, newPage) => setPage(newPage);
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(+event?.target?.value);
setPage(0);
};
if (isError) {
return errorMessage;
}
const KPI_META = [
{ idx: 0, label: 'All Invoices', color: BRAND, icon: MdDashboard, value: insightdata?.totalcount ?? 0 },
{ idx: 1, label: 'Open', color: '#ef4444', icon: MdOutlinePendingActions, value: insightdata?.pendingcount ?? 0 },
{ idx: 2, label: 'Overdue', color: '#f59e0b', icon: MdReportProblem, value: insightdata?.overduecount ?? 0 },
{ idx: 3, label: 'Paid', color: '#10b981', icon: MdOutlineCheckCircle, value: insightdata?.paidcount ?? 0 }
];
const activeMeta = STATUS_META[billStatus];
return (
<>
{(isloader || isLoading) && <Loader />}
{/* ============================================= || Header || ============================================= */}
<PageHeader
title="Invoices"
subtitle={`Live · Viewing ${activeMeta.label.toLowerCase()} invoices`}
live
action={
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 1,
px: 1.5,
py: 0.875,
borderRadius: 999,
bgcolor: '#ffffff',
border: `1.5px solid ${edge(BRAND)}`,
color: BRAND,
fontWeight: 800,
fontSize: 12
}}
>
<MdCurrencyRupee size={14} />
<Typography variant="caption" sx={{ fontWeight: 800, color: DT.textSecondary, letterSpacing: 0.4, textTransform: 'uppercase' }}>
Grand Total
</Typography>
<Typography sx={{ fontWeight: 800, color: BRAND, fontSize: 13 }}>
{formatNumberToRupees(grandTotal)}
</Typography>
</Box>
}
/>
{/* ============================================= || KPI Cards (clickable filter) || ============================================= */}
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
{KPI_META.map((item) => {
const Icon = item.icon;
return (
<Grid item key={item.idx} xs={6} sm={6} md={3}>
<Box
onClick={() => {
setBillStatus(item.idx);
setPage(0);
}}
sx={{ cursor: 'pointer', height: '100%' }}
>
<StatCard
title={item.label}
value={item.value}
icon={<Icon size={20} />}
color={item.color}
loading={isInsightLoading}
/>
</Box>
</Grid>
);
})}
</Grid>
{/* ============================================= || Status Tabs + Search || ============================================= */}
<Paper
elevation={0}
sx={{
mt: { xs: 1.5, md: 2 },
p: { xs: 1, md: 1.5 },
borderTopLeftRadius: DT.radiusCard / 8,
borderTopRightRadius: DT.radiusCard / 8,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
border: '1px solid',
borderColor: DT.borderSubtle,
borderBottom: 0,
background: '#fff'
}}
>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
gap={1.5}
sx={{ flexWrap: 'wrap-reverse' }}
>
<Stack
direction="row"
spacing={0.75}
sx={{
flex: 1,
overflowX: 'auto',
py: 0.5,
px: 0.25,
'&::-webkit-scrollbar': { height: 6 },
'&::-webkit-scrollbar-thumb': { backgroundColor: DT.borderSubtle, borderRadius: 4 }
}}
>
{STATUS_TABS.map((idx) => {
const meta = STATUS_META[idx];
const Icon = meta.icon;
const active = billStatus === idx;
const count = insightdata?.[meta.countKey] ?? 0;
return (
<Box
key={idx}
onClick={() => {
setBillStatus(idx);
setPage(0);
}}
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: { xs: 0.625, md: 0.875 },
pl: 0.5,
pr: { xs: 1, md: 1.25 },
py: 0.5,
flexShrink: 0,
cursor: 'pointer',
borderRadius: 999,
border: `1px solid ${active ? meta.color : DT.borderSubtle}`,
bgcolor: active ? meta.color : DT.surface,
color: active ? '#fff' : DT.textSecondary,
fontWeight: 600,
boxShadow: 'none',
transition: 'background-color 0.15s, border-color 0.15s, color 0.15s',
'&:hover': {
borderColor: active ? meta.color : '#cbd5e1',
bgcolor: active ? meta.color : DT.surfaceAlt
}
}}
>
<Avatar
sx={{
width: { xs: 20, md: 22 },
height: { xs: 20, md: 22 },
bgcolor: active ? 'rgba(255,255,255,0.22)' : soft(meta.color),
color: active ? '#fff' : meta.color
}}
>
<Icon size={12} />
</Avatar>
<Typography
variant="caption"
sx={{
fontWeight: 600,
fontSize: { xs: 11.5, md: 13 },
lineHeight: 1
}}
>
{meta.label}
</Typography>
<Box
sx={{
minWidth: { xs: 20, md: 24 },
height: { xs: 18, md: 20 },
px: 0.625,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 999,
fontSize: { xs: 10, md: 11 },
fontWeight: 700,
bgcolor: active ? 'rgba(255,255,255,0.22)' : DT.surfaceAlt,
color: active ? '#fff' : DT.textSecondary,
border: 'none'
}}
>
{count}
</Box>
</Box>
);
})}
</Stack>
<Box sx={{ width: { xs: '100%', sm: 240, lg: 280 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
<DebounceSearchBar
value={searchword}
onChange={setSearchword}
onDebouncedChange={handleDebouncedSearch}
placeholder="Search invoices (ctrl+k)"
sx={{
m: 0,
width: '100%',
borderRadius: 999,
bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}}
/>
</Box>
</Stack>
</Paper>
{/* ============================================= || Table || ============================================= */}
<Paper
elevation={0}
sx={{
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
borderBottomLeftRadius: DT.radiusCard / 8,
borderBottomRightRadius: DT.radiusCard / 8,
border: '1px solid',
borderColor: DT.borderSubtle,
overflow: 'hidden',
background: '#fff'
}}
>
{isMobile ? (
<>
{isDeliveryLoading ? (
<Box sx={{ p: 1.5 }}>
<OrdersTableSkeleton col={4} />
</Box>
) : pagedList.length === 0 ? (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6, px: 2 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdReceiptLong size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No invoices to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center' }}>
{searchword
? 'Try a different keyword.'
: `No ${activeMeta.label.toLowerCase()} invoices for this filter.`}
</Typography>
</Stack>
) : (
<MobileCardList>
{pagedList.map((item, index) => {
const overdue =
billStatus === 2 ||
(item.duedate && dayjs(item.duedate).isBefore(dayjs(), 'day') && billStatus !== 3);
return (
<MobileCard
key={item.invoiceno || index}
accent={BRAND}
header={
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<AccentAvatar color={BRAND} size={36}>
<MdGroups size={18} />
</AccentAvatar>
<Stack spacing={0.25} sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{item.tenantname || '—'}
</Typography>
{item.contactperson && (
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{item.contactperson}
</Typography>
)}
</Stack>
</Stack>
<Tooltip title="Preview invoice" placement="left">
<IconButton
size="small"
onClick={() => {
setIsLoader(true);
setTimeout(() => {
setIsLoader(false);
navigate('/nearle/invoice/preview', { state: item });
}, 500);
}}
sx={{
flexShrink: 0,
bgcolor: soft(BRAND),
color: BRAND,
border: `1px solid ${edge(BRAND)}`,
'&:hover': { bgcolor: BRAND, color: '#fff' }
}}
>
<MdVisibility size={16} />
</IconButton>
</Tooltip>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Invoice ID">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint('#0ea5e9'),
border: `1px solid ${edge('#0ea5e9')}`,
color: '#0ea5e9',
fontSize: 11,
fontWeight: 800
}}
>
<MdReceiptLong size={12} /> {item.invoiceno || '—'}
</Box>
</MobileField>
<MobileField label="Amount" align="right">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: '#ffffff',
border: `1px solid ${edge(BRAND)}`,
color: BRAND,
fontSize: 11,
fontWeight: 800,
justifyContent: 'center'
}}
>
<MdCurrencyRupee size={11} />
{formatNumberToRupees(item.totalamount).replace('₹', '').trim()}
</Box>
</MobileField>
<MobileField label="Invoice Date">
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdEventNote size={12} color={DT.textMuted} />
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{item.transactiondate ? dayjs(item.transactiondate).format('DD/MM/YYYY') : '—'}
</Typography>
</Stack>
</MobileField>
<MobileField label="Due Date">
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdEventNote size={12} color={overdue && billStatus !== 3 ? '#ef4444' : DT.textMuted} />
<Typography
variant="caption"
sx={{
fontWeight: 700,
color: overdue && billStatus !== 3 ? '#ef4444' : DT.textPrimary
}}
noWrap
>
{item.duedate ? dayjs(item.duedate).format('DD/MM/YYYY') : '—'}
</Typography>
</Stack>
</MobileField>
<MobileField label="Items">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 0.875,
py: 0.25,
borderRadius: 999,
bgcolor: tint('#14b8a6'),
border: `1px solid ${edge('#14b8a6')}`,
color: '#14b8a6',
fontSize: 11,
fontWeight: 800,
minWidth: 44,
justifyContent: 'center'
}}
>
<MdInventory2 size={11} /> {item.itemcount ?? 0}
</Box>
</MobileField>
</MobileFieldGrid>
</MobileCard>
);
})}
</MobileCardList>
)}
</>
) : (
<TableContainer
sx={{
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
'&::-webkit-scrollbar': { width: 10, height: 10 },
'&::-webkit-scrollbar-thumb': {
backgroundColor: edge(BRAND),
borderRadius: 8,
'&:hover': { backgroundColor: BRAND }
},
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
>
<Table stickyHeader sx={{ minWidth: { xs: 880, md: 1060 } }}>
<TableHead>
<TableRow
sx={{
'& th': {
backgroundColor: DT.surfaceAlt,
color: DT.textSecondary,
fontSize: { xs: 10, md: 11 },
fontWeight: 800,
letterSpacing: 0.6,
textTransform: 'uppercase',
whiteSpace: 'nowrap',
borderBottom: `1px solid ${DT.borderSubtle}`,
py: { xs: 1, md: 1.25 },
px: { xs: 1, md: 2 }
}
}}
>
<TableCell>#</TableCell>
<TableCell>Client</TableCell>
<TableCell>Invoice ID</TableCell>
<TableCell>Invoice Date</TableCell>
<TableCell>Due Date</TableCell>
<TableCell align="center">Items</TableCell>
<TableCell align="right">Amount</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{isDeliveryLoading && <OrdersTableSkeleton col={4} />}
{!isDeliveryLoading && pagedList.length === 0 ? (
<TableRow>
<TableCell colSpan={8} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdReceiptLong size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No invoices to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{searchword
? 'Try a different keyword.'
: `No ${activeMeta.label.toLowerCase()} invoices for this filter.`}
</Typography>
</Stack>
</TableCell>
</TableRow>
) : (
pagedList.map((item, index) => {
const overdue = billStatus === 2 || (item.duedate && dayjs(item.duedate).isBefore(dayjs(), 'day') && billStatus !== 3);
return (
<TableRow
key={item.invoiceno || index}
sx={{
transition: 'background-color 0.15s',
'& td': {
borderBottom: `1px solid ${DT.divider}`,
py: { xs: 1, md: 1.5 },
px: { xs: 1, md: 2 },
verticalAlign: 'top'
},
'&:hover': { backgroundColor: DT.surfaceAlt }
}}
>
<TableCell>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(activePage * rowsPerPage + index + 1).padStart(2, '0')}
</Typography>
</TableCell>
<TableCell>
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color={BRAND} size={36}>
<MdGroups size={18} />
</AccentAvatar>
<Stack spacing={0.25} sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{item.tenantname || '—'}
</Typography>
{item.contactperson && (
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{item.contactperson}
</Typography>
)}
</Stack>
</Stack>
</TableCell>
<TableCell>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: tint('#0ea5e9'),
border: `1px solid ${edge('#0ea5e9')}`,
color: '#0ea5e9',
fontSize: 11,
fontWeight: 800
}}
>
<MdReceiptLong size={12} /> {item.invoiceno || '—'}
</Box>
</TableCell>
<TableCell>
<Stack spacing={0.25}>
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdEventNote size={12} color={DT.textMuted} />
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{item.transactiondate ? dayjs(item.transactiondate).format('DD/MM/YYYY') : '—'}
</Typography>
</Stack>
<Typography variant="caption" sx={{ color: DT.textSecondary, pl: 2 }}>
{item.transactiondate ? dayjs(item.transactiondate).utc().format('hh:mm A') : ''}
</Typography>
</Stack>
</TableCell>
<TableCell>
<Stack spacing={0.25}>
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdEventNote
size={12}
color={overdue && billStatus !== 3 ? '#ef4444' : DT.textMuted}
/>
<Typography
variant="caption"
sx={{
fontWeight: 700,
color: overdue && billStatus !== 3 ? '#ef4444' : DT.textPrimary,
whiteSpace: 'nowrap'
}}
>
{item.duedate ? dayjs(item.duedate).format('DD/MM/YYYY') : '—'}
</Typography>
</Stack>
<Typography variant="caption" sx={{ color: DT.textSecondary, pl: 2 }}>
{item.duedate ? dayjs(item.duedate).utc().format('hh:mm A') : ''}
</Typography>
</Stack>
</TableCell>
<TableCell align="center">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 0.875,
py: 0.25,
borderRadius: 999,
bgcolor: tint('#14b8a6'),
border: `1px solid ${edge('#14b8a6')}`,
color: '#14b8a6',
fontSize: 11,
fontWeight: 800,
minWidth: 44,
justifyContent: 'center'
}}
>
<MdInventory2 size={11} /> {item.itemcount ?? 0}
</Box>
</TableCell>
<TableCell align="right">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: '#ffffff',
border: `1px solid ${edge(BRAND)}`,
color: BRAND,
fontSize: 11,
fontWeight: 800,
minWidth: 110,
justifyContent: 'center'
}}
>
<MdCurrencyRupee size={11} />
{formatNumberToRupees(item.totalamount).replace('₹', '').trim()}
</Box>
</TableCell>
<TableCell align="center">
<Tooltip title="Preview invoice" placement="left">
<IconButton
size="small"
onClick={() => {
setIsLoader(true);
setTimeout(() => {
setIsLoader(false);
navigate('/nearle/invoice/preview', { state: item });
}, 500);
}}
sx={{
bgcolor: soft(BRAND),
color: BRAND,
border: `1px solid ${edge(BRAND)}`,
'&:hover': { bgcolor: BRAND, color: '#fff' }
}}
>
<MdVisibility size={16} />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
)}
<Divider />
<Stack
direction={{ xs: 'column', sm: 'row' }}
alignItems={{ xs: 'flex-start', sm: 'center' }}
justifyContent="space-between"
sx={{
px: 2,
py: 1,
background: '#ffffff'
}}
>
<Typography variant="caption" sx={{ fontWeight: 800, color: DT.textSecondary, letterSpacing: 0.6, textTransform: 'uppercase' }}>
Page total · {formatNumberToRupees(pageTotal)}
</Typography>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 100]}
component="div"
count={filteredList.length}
rowsPerPage={rowsPerPage}
page={activePage}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
sx={{
'& .MuiTablePagination-toolbar': { minHeight: 40, px: 0 },
'& .MuiTablePagination-selectLabel, & .MuiTablePagination-displayedRows': {
fontWeight: 700,
color: DT.textSecondary
}
}}
/>
</Stack>
</Paper>
<Outlet />
</>
);
};
export default Invoice;

View File

@@ -1,486 +0,0 @@
import React, { useRef, useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
// import nearleLogo from '../../../assets/images/nearleLogo.png';
import logo_nearle1 from '../../../assets/images/logo-nearle1.png';
import axios from 'axios';
import dayjs from 'dayjs';
import Loader from 'components/Loader';
import { enqueueSnackbar } from 'notistack';
import { DownloadOutlined, PrinterFilled } from '@ant-design/icons';
import ReactToPrint, { useReactToPrint } from 'react-to-print';
import { SearchOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
// import jsPDF from 'jspdf';
import { useNavigate } from 'react-router-dom';
import { FaArrowLeft } from 'react-icons/fa6';
import { FaIndianRupeeSign } from 'react-icons/fa6';
// import autoTable from 'jspdf-autotable';
import {
Grid,
Button,
Divider,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TablePagination,
TableRow,
Tabs,
Tab,
Typography,
Box,
OutlinedInput,
InputAdornment,
IconButton,
TextField,
Tooltip,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Stack,
Chip
} from '@mui/material';
const InvoicePreview = () => {
const [selected, setselected] = useState({});
const location = useLocation();
const navigate = useNavigate();
console.log('previewSelect', location.state);
const componentRef = useRef(null);
const [tabletype, settabletype] = useState(true);
const [paydialog, setpaydialog] = useState(false);
const [refnumber, setRefnumber] = useState('');
const [remarks, setRemarks] = useState('');
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
useEffect(() => {
setselected(location.state);
}, []);
// ================================================= || formatNumberToRupees || =================================================
function formatNumberToRupees(value) {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 2
}).format(value);
}
// ================================================= || updatePayment || =================================================
const updatePayment = async () => {
try {
const updateResponse = await axios.put(`${process.env.REACT_APP_URL}/invoice/updatestatus`, {
salesid: selected.salesid,
referenceno: refnumber,
referencedate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
billstatus: 2,
paymentremarks: remarks
});
if (updateResponse.status) {
enqueueSnackbar(' Updated Successfully ', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 1000
});
}
console.log('updateResponse', updateResponse);
} catch (error) {
console.log('updateResponse', error);
}
};
return (
<>
<Stack
direction={{ xs: 'column', md: 'row' }}
justifyContent="Space-between"
alignItems={{ xs: 'stretch', md: 'center' }}
spacing={2}
sx={{ px: { xs: 1.5, md: 2.5 }, py: 1, bgcolor: '#eeeeee' }}
>
<Stack direction={'row'} alignItems={'center'} spacing={2}>
<Tooltip title="back">
<IconButton
color="primary"
onClick={() => {
navigate('/nearle/invoice');
}}
>
<FaArrowLeft size={'large'} />
</IconButton>
</Tooltip>
<Stack alignItems={'center'}>
<Typography variant="h3" color={'primary'}>
Invoice Details
</Typography>
<Chip
size="small"
color="warning"
variant="outlined"
sx={{ bgcolor: theme.palette.warning.lighter }}
label={`Invoice No :${'\u00a0\u00a0'}${selected.invoiceno}`}
/>
</Stack>
</Stack>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ width: { xs: '100%', md: 'auto' } }}>
<Button
variant="outlined"
color="primary"
fullWidth={isMobile}
sx={{
'&:hover': {
backgroundColor: 'primary.main',
color: 'primary.contrastText'
}
}}
onClick={() => {
setpaydialog(true);
}}
>
{' '}
<FaIndianRupeeSign />
Update Payment
</Button>
<ReactToPrint
trigger={() => (
<Button
size="small"
startIcon={<PrinterFilled />}
variant="outlined"
color="primary"
fullWidth={isMobile}
sx={{
'&:hover': {
backgroundColor: 'primary.main',
color: 'primary.contrastText'
}
}}
>
Print
</Button>
)}
content={() => componentRef.current}
/>
</Stack>
</Stack>
<Box sx={{ pb: 2.5, border: '1px solid #eee', overflowX: { xs: 'auto', md: 'visible' } }}>
{/* minWidth keeps the invoice at a legible fixed layout on phones —
the parent's overflowX:auto then lets it scroll horizontally
instead of squishing the header into vertical slivers. 720px sits
within the print page width, so printing is unaffected. */}
<div ref={componentRef} style={{ width: '100%', minWidth: 720 }}>
<Box id="print" sx={{ p: 2.5 }}>
<Box sx={{ pb: 2.5 }}>
<Stack
sx={{
flexDirection: 'row',
// bgcolor: theme.palette.primary.main,
border: '1px solid #eee',
px: 3
}}
justifyContent="space-between"
>
<Box sx={{ pt: 0.5 }}>
<Stack direction="row" spacing={2}>
<img src={logo_nearle1} style={{ width: '150px', height: '50px' }} />{' '}
</Stack>
{/* <Typography
sx={{ color: theme.palette.primary.main, py: 0.5 }}
>
{`Invoice No: ${"\u00a0\u00a0\u00a0"}${selected.invoiceno}`}
</Typography> */}
<Stack direction="row" justifyContent="space-between">
<Typography
sx={{
overflow: 'hidden',
color: theme.palette.primary.main
}}
variant="subtitle1"
>
Invoice No :
</Typography>
<Typography sx={{ color: theme.palette.primary.main }}>{`${'\u00a0\u00a0'}${selected.invoiceno}`}</Typography>
</Stack>
</Box>
<Box sx={{ pt: 2.5, pb: 1.75 }}>
<Stack direction="row" justifyContent="space-between">
<Typography sx={{ pl: 4, color: theme.palette.primary.main }} variant="subtitle1">
Date :{' '}
</Typography>
<Typography sx={{ color: theme.palette.primary.main }}>
{dayjs(selected.transactiondate).format('DD-MM-YYYY')}
</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between">
<Typography
sx={{
pr: 2,
overflow: 'hidden',
color: theme.palette.primary.main
}}
variant="subtitle1"
>
Due Date :
</Typography>
<Typography sx={{ color: theme.palette.primary.main }}>{dayjs(selected.dueDate).format('DD-MM-YYYY')}</Typography>
</Stack>
{/* <Stack direction="row" justifyContent="space-between">
<Typography
sx={{
pr: 2,
overflow: "hidden",
color: theme.palette.primary.main,
}}
variant="subtitle1"
>
Invoice No :
</Typography>
<Typography sx={{ color: theme.palette.primary.main }}>
{`${"\u00a0\u00a0\u00a0"}${selected.invoiceno}`}
</Typography>
</Stack> */}
</Box>
</Stack>
<Box sx={{ pt: 2.5 }}>
<Grid container spacing={2} justifyContent="space-between" direction="row">
<Grid item xs={12} sm={6}>
<Box
sx={{
border: 1,
minHeight: 240,
borderColor: 'grey.200',
borderRadius: 0.5,
p: 2.5
}}
>
<Grid container direction="row">
<Grid item md={8}>
<Stack spacing={2}>
<Typography variant="h5">From:</Typography>
<Stack sx={{ width: '100%' }}>
<Typography variant="subtitle1">Nearle Technology Privite Limited.</Typography>
<Typography color="secondary">
424, 4<sup>th</sup>floor,
</Typography>
<Typography color="secondary">Red rose towers,</Typography>
<Typography color="secondary">DB Road, RS Puram,</Typography>
<Typography color="secondary">641002.</Typography>
<Typography color="secondary">care@nearle.in</Typography>
<Typography color="secondary">9047968666</Typography>
</Stack>
</Stack>
</Grid>
</Grid>
</Box>
</Grid>
<Grid item xs={12} sm={6}>
<Box
sx={{
border: 1,
minHeight: 240,
borderColor: 'grey.200',
borderRadius: 0.5,
p: 2.5
}}
>
<Grid container direction="row">
<Grid item md={8}>
<Stack spacing={2}>
<Typography variant="h5">To:</Typography>
<Stack sx={{ width: '100%' }}>
<Typography variant="subtitle1">{selected.tenantname}</Typography>
<Typography color="secondary">{selected.address}</Typography>
<Typography color="secondary">{selected.suburb}</Typography>
<Typography color="secondary">{selected.city}</Typography>
<Typography color="secondary">{selected.state}</Typography>{' '}
</Stack>
</Stack>
</Grid>
</Grid>
</Box>
</Grid>
</Grid>
</Box>
</Box>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>S.No</TableCell>
<TableCell>Particulars</TableCell>
<TableCell>Unit</TableCell>
<TableCell>Quantity</TableCell>
<TableCell align="right">Rate</TableCell>
{/* {selected && selected.pricingtypeid === 73 && ( */}
<TableCell align="right">Other Charges</TableCell>
{/* )} */}
<TableCell align="right">Amount</TableCell>
</TableRow>
</TableHead>
{selected.tenantsalesdetails && (
<TableBody>
<TableRow>
<TableCell>1</TableCell>
<TableCell>
<Typography>
{`Invoice from ${dayjs(selected.tenantsalesdetails[0].fromdate).format('DD-MM-YYYY')} to ${dayjs(
selected.tenantsalesdetails[0].todate
).format('DD-MM-YYYY')}`}
</Typography>
</TableCell>
<TableCell>
<Typography>{selected.tenantsalesdetails[0].pricingtype}</Typography>
</TableCell>
<TableCell>
<Typography>{`${selected.tenantsalesdetails[0].quantity.toFixed(2)} km`}</Typography>
</TableCell>
<TableCell>
<Typography align="right">{`${selected.tenantsalesdetails[0].baserate.toFixed(2)}`}</Typography>
</TableCell>
{/* {selected.tenantsalesdetails[0].pricingtypeid == 73 && ( */}
<TableCell align="right">
<Typography>{`${selected.tenantsalesdetails[0].othercharges}.00`}</Typography>
</TableCell>
{/* )} */}
<TableCell align="right">
<Typography>{`${selected.tenantsalesdetails[0].amount}.00`}</Typography>
</TableCell>
</TableRow>
</TableBody>
)}
</Table>
</TableContainer>
<Divider />
<Box sx={{ p: 2.5 }}>
<Grid container direction="row" justifyContent="flex-end">
<Grid item md={4}>
<Stack spacing={2}>
<Stack direction="row" justifyContent="space-between">
<Typography color="secondary">Sub Total:</Typography>
<Typography variant="h6">{formatNumberToRupees(selected.salesamount)}</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between">
<Typography color="secondary">Discount:</Typography>
<Typography variant="h6" color={theme.palette.error.main}>
- {formatNumberToRupees(selected.discountamt)}
</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between">
<Typography color={theme.palette.grey[500]}>Tax:</Typography>
<Typography variant="h6" color={theme.palette.success.main}>
+ {formatNumberToRupees(selected.taxamount)}
</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between">
<Typography sx={{ pr: 2 }} variant="subtitle1">
Grand Total:
</Typography>
<Typography variant="h6">{formatNumberToRupees(Math.round(selected.totalamount))}</Typography>
</Stack>
</Stack>
</Grid>
</Grid>
</Box>
</Box>
<Divider />
<Box sx={{ p: 2.5 }}>
<Typography>Notes: {selected.remarks}</Typography>
</Box>
<Divider />
</div>
</Box>
{/* ================================================= || updatePayment Dialog || ================================================= */}
<Dialog
open={paydialog}
onClose={() => {
setpaydialog(false);
}}
maxWidth={'sm'}
fullWidth
>
<DialogTitle sx={{ bgcolor: theme.palette.primary.main }}>
<Stack direction={'row'} spacing={1}>
<Typography variant="h2" sx={{ color: 'white' }}>
</Typography>
<Typography variant="h3" sx={{ color: 'white' }}>
Update Payment
</Typography>
</Stack>
</DialogTitle>
<DialogContent dividers>
<Stack spacing={1} sx={{ mb: 2 }}>
<Typography>Reference No</Typography>
<TextField
type="number"
placeholder="Enter Reference Number"
sx={{ width: '100%' }}
onChange={(e) => {
setRefnumber(e.target.value);
}}
/>
</Stack>
<Stack spacing={2} sx={{ mb: 2 }}>
<Typography>Remarks</Typography>
<TextField
multiline
required
placeholder="Enter Remarks"
sx={{ width: '100%' }}
onChange={(e) => {
setRemarks(e.target.value);
}}
/>
</Stack>
</DialogContent>
<DialogActions>
<Button
variant="outlined"
sx={{
'&:hover': {
backgroundColor: 'primary.main',
color: 'primary.contrastText'
},
m: 2
}}
onClick={() => {
setpaydialog(false);
}}
>
Cancel
</Button>
<Button
variant="outlined"
disabled={refnumber == '' || remarks == ''}
sx={{
'&:hover': {
backgroundColor: 'primary.main',
color: 'primary.contrastText'
},
m: 2
}}
onClick={() => {
setpaydialog(false);
updatePayment();
navigate('/nearle/invoice');
}}
>
Update
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default InvoicePreview;

View File

@@ -1,151 +1,83 @@
// This is the active login page — matched by routes/LoginRoutes.js, which is
// registered before MainRoutes.js and wins the route match for both '/' and
// '/login'. (login1.js is dead code — MainRoutes.js's own '/login' route is
// unreachable because LoginRoutes.js's un-prefixed '/login' matches first.)
import { useState, useEffect } from 'react';
import { enqueueSnackbar, closeSnackbar } from 'notistack';
import AnimateButton from 'components/@extended/AnimateButton';
import OtpInput from 'react18-input-otp';
import { Box, Card, CardContent, Stack, TextField, Button, Typography, Link, FormLabel, IconButton, InputAdornment } from '@mui/material';
import { Box, Card, CardContent, Stack, TextField, Button, Typography, Link, IconButton, InputAdornment } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
import Loader from 'components/Loader';
import logo from 'assets/images/logo-nearle1.png';
import doormileLogo from 'assets/images/doormile-logo.png';
import { useSelector, useDispatch } from 'react-redux';
import { OpenToast } from 'components/third-party/OpenToast';
import { closeGlobalToast, GlobalToast } from 'components/nearle_components/GlobalToast';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import { setLoginUser } from 'store/reducers/loginUserSlice';
import { markSessionStart } from 'utils/session';
// doormile-logo.png is a white asset; this recolours it to brand red (#C01227) for light surfaces.
const DOORMILE_RED_FILTER = 'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)';
const Login = () => {
const dispatch = useDispatch();
const fcmtoken = useSelector((state) => state.fcm);
const permission = useSelector((state) => state.fcm.permission);
const theme = useTheme();
const [loading, setLoading] = useState(false);
let navigate = useNavigate();
const [otp, setOtp] = useState('');
const [currentotp, setCurrentotp] = useState('');
const [userinfo, setUserinfo] = useState({});
const [username, setUsername] = useState('');
const [passwordStatus, setPasswordStatus] = useState(0);
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isPassword, setIspassword] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [userid, setUserid] = useState(0);
useEffect(() => {
if (localStorage.getItem('firstname')) {
if (localStorage.getItem('authname')) {
navigate('/nearle/dispatch');
}
}, []);
const loginsend = async () => {
setLoading(true);
if (!username) {
if (!username || !password) {
opentoast('Fill All required fields');
setLoading(false);
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
const res = await axios.post(`${process.env.REACT_APP_URL}/admin/login`, {
email: username,
password,
userfcmtoken: fcmtoken?.token
});
// 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);
fetchAppLocations(userinfo.userid);
if (res.data.success) {
OpenToast('Login Successful', 'success', 1000);
// The live backend's success response doesn't nest user fields under
// `data` the way the original spec described — fall back to reading
// them off the top-level response if `data` isn't there.
const token = res.data.token;
const data = res.data.data || res.data.user || res.data;
dispatch(setLoginUser(data));
localStorage.setItem('authname', data.email || data.firstname || username);
localStorage.setItem('userid', data.userid ?? data.id ?? '');
localStorage.setItem('roleid', data.roleid ?? data.role ?? '');
localStorage.setItem('token', token);
axios.defaults.headers.common.Authorization = `Bearer ${token}`;
markSessionStart();
navigate('/nearle/dispatch');
} else {
OpenToast(res.data.message, 'error', 3000);
OpenToast(res.data.message || 'Invalid Data', 'error', 3000);
}
} catch (err) {
console.error(err);
OpenToast(err.message, 'error', 5000);
OpenToast(err.response?.data?.message || err.message, 'error', 5000);
} 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);
closeGlobalToast(); // to close the pin snackbar
navigate('/nearle/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('');
}
OpenToast(message, 'error', 1500);
};
return (
@@ -160,12 +92,24 @@ const Login = () => {
overflow: 'hidden',
flexBasis: '46%',
flexDirection: 'column',
justifyContent: 'space-between',
justifyContent: 'center',
color: '#fff',
p: 6,
background: 'linear-gradient(150deg, #4D1C61 0%, #662582 52%, #9255AB 100%)'
background: 'linear-gradient(150deg, #900E1D 0%, #C01227 52%, #D35968 100%)'
}}
>
{/* Logo at the top-left corner */}
<img
src={doormileLogo}
alt="Doormile"
style={{
position: 'absolute',
top: 48,
left: 48,
maxHeight: 40
}}
/>
{/* decorative light glows */}
<Box
sx={{
@@ -190,54 +134,39 @@ const Login = () => {
}}
/>
<Box sx={{ position: 'relative' }}>
<Box
sx={{
bgcolor: '#fff',
borderRadius: 2,
px: 1.5,
py: 0.75,
display: 'inline-flex',
boxShadow: '0 8px 20px rgba(0,0,0,0.18)'
}}
>
<img src={logo} alt="NearlExpress" style={{ height: 30, display: 'block' }} />
</Box>
</Box>
<Box sx={{ position: 'relative' }}>
<Typography sx={{ fontSize: 34, fontWeight: 700, lineHeight: 1.18, letterSpacing: '-0.02em', mb: 2 }}>
<Box sx={{ position: 'relative', maxWidth: 430 }}>
<Typography sx={{ fontSize: 40, fontWeight: 700, lineHeight: 1.18, letterSpacing: '-0.02em', mb: 2 }}>
Operate your dispatch,
<br />
end to end.
</Typography>
<Typography sx={{ fontSize: 16, color: 'rgba(255,255,255,0.82)', maxWidth: 430, lineHeight: 1.6 }}>
Orders, AI route optimisation, live rider tracking and billing all in the NearlExpress operator console.
<Typography sx={{ fontSize: 17.5, color: 'rgba(255,255,255,0.85)', mb: 4, lineHeight: 1.6 }}>
Orders, AI route optimisation, live rider tracking and billing all in the Doormile operator console.
</Typography>
</Box>
<Stack spacing={1.25} sx={{ position: 'relative' }}>
{['Real-time fleet visibility', 'AI-optimised dispatch routes', 'Tenant, pricing & invoice control'].map((t) => (
<Stack key={t} direction="row" spacing={1.25} alignItems="center">
<Box
sx={{
width: 22,
height: 22,
borderRadius: '50%',
bgcolor: 'rgba(255,255,255,0.18)',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 13,
fontWeight: 700
}}
>
</Box>
<Typography sx={{ fontSize: 14.5, color: 'rgba(255,255,255,0.9)' }}>{t}</Typography>
</Stack>
))}
</Stack>
<Stack spacing={1.5} sx={{ display: 'inline-flex', textAlign: 'left' }}>
{['Real-time fleet visibility', 'AI-optimised dispatch routes', 'Tenant, pricing & invoice control'].map((t) => (
<Stack key={t} direction="row" spacing={1.25} alignItems="center">
<Box
sx={{
width: 22,
height: 22,
borderRadius: '50%',
bgcolor: 'rgba(255,255,255,0.18)',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 13,
fontWeight: 700
}}
>
</Box>
<Typography sx={{ fontSize: 16, color: 'rgba(255,255,255,0.9)' }}>{t}</Typography>
</Stack>
))}
</Stack>
</Box>
</Box>
{/* ---- Right form panel ---- */}
@@ -254,7 +183,7 @@ const Login = () => {
>
{/* Logo */}
<Stack alignItems="center" mb={2.5}>
<img src={logo} alt="loginpagelogo" style={{ maxHeight: 48 }} />
<img src={doormileLogo} alt="Doormile" style={{ maxHeight: 40, filter: DOORMILE_RED_FILTER }} />
</Stack>
{/* Title */}
@@ -262,7 +191,7 @@ const Login = () => {
Welcome back
</Typography>
<Typography variant="body2" textAlign="center" sx={{ color: '#64748b', mb: 3 }}>
Sign in to the NearlExpress console
Sign in to the Doormile console
</Typography>
<CardContent sx={{ p: 0 }}>
@@ -270,28 +199,7 @@ const Login = () => {
noValidate
onSubmit={(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();
}
// if (currentotp) {
// if (currentotp == otp) {
// loginsuccessful();
// fetchAppLocations();
// } else {
// opentoast('Invalid pin');
// }
// }
loginsend();
}}
>
<Stack spacing={3}>
@@ -305,122 +213,27 @@ const Login = () => {
required
value={username}
onChange={(e) => setUsername(e.target.value.toLocaleLowerCase())}
InputProps={{ readOnly: passwordStatus }}
/>
{/* Setup Password */}
{passwordStatus == 1 && (
<Stack display={'flex'} flexDirection={'column'} spacing={3}>
<Typography variant="h4" textAlign="start" mb={3}>
Setup Password
</Typography>
<TextField
autoFocus
fullWidth
label="Enter New Password"
variant="outlined"
required
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/>
<TextField
error={confirmPassword !== '' && password !== confirmPassword}
fullWidth
label="Re-Enter Password"
variant="outlined"
required
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowConfirmPassword((prev) => !prev)} edge="end">
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/>
</Stack>
)}
{/* Enter Password */}
{passwordStatus == 2 && (
<Stack display={'flex'} flexDirection={'column'} spacing={3}>
<Typography variant="h4" textAlign="start" mb={3}>
Enter Password
</Typography>
<TextField
autoFocus
fullWidth
label="Enter Password"
variant="outlined"
required
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/>
</Stack>
)}
{/* OTP */}
{isPassword && (
<Stack spacing={1.5}>
<Stack direction="row" justifyContent="space-between">
<FormLabel>Enter Password</FormLabel>
<Link
variant="body2"
sx={{ cursor: 'pointer' }}
onClick={() => {
setOtp('');
loginsend();
}}
>
Retry
</Link>
</Stack>
{/* <OtpInput
shouldAutoFocus
value={otp}
onChange={(otp) => setOtp(otp)}
numInputs={4}
containerStyle={{ justifyContent: 'space-between' }}
inputStyle={{
width: 48,
height: 48,
borderRadius: 8,
border: `1px solid ${borderColor}`,
fontSize: 18
}}
focusStyle={{
outline: 'none',
border: `1px solid ${theme.palette.primary.main}`,
boxShadow: theme.customShadows.primary
}}
/> */}
<TextField type="passowrd" value={password} onChange={(e) => setPassword(e.target.value)} />
</Stack>
)}
{/* Password */}
<TextField
fullWidth
label="Password"
variant="outlined"
autoComplete="current-password"
required
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/>
{/* Submit */}
<AnimateButton>
<Button fullWidth size="large" type="submit" variant="contained" color="primary">
@@ -439,7 +252,7 @@ const Login = () => {
component={Link}
href="https://nearle.in"
target="_blank"
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#C01227' } }}
>
&copy; All rights reserved
</Typography>
@@ -448,7 +261,7 @@ const Login = () => {
component={Link}
href="https://nearle.in/terms"
target="_blank"
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#C01227' } }}
>
Terms and Conditions
</Typography>
@@ -457,7 +270,7 @@ const Login = () => {
component={Link}
href="https://nearle.in/privacy"
target="_blank"
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#C01227' } }}
>
Privacy Policy
</Typography>

View File

@@ -1,6 +1,9 @@
// UNUSED — routes/LoginRoutes.js is registered before MainRoutes.js and
// defines its own un-prefixed '/login' route pointing at pages/nearle/login,
// which wins the match. This file's '/login' route inside MainRoutes.js is
// unreachable. Kept updated to Doormile endpoints anyway (harmless), but
// pages/nearle/login.js is the one that actually renders.
import { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
// import AuthWrapper from 'sections/auth/AuthWrapper';
import {
Box,
Grid,
@@ -22,22 +25,25 @@ import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import AnimateButton from 'components/@extended/AnimateButton';
import logo from 'assets/images/logo-nearle1.png';
import logo from 'assets/images/doormile-logo.png';
// doormile-logo.png is a white asset; this recolours it to brand red (#C01227) for light surfaces.
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 { useSelector } 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 fcmtoken = useSelector((state) => state.fcm);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [alertmessage, setAlertmessage] = useState('');
const [checkusername, setCheckusername] = useState(false);
const [, setAlertmessage] = useState('');
// const [toast, setToast] = useState(false);
const [loading, setLoading] = useState(false);
let navigate = useNavigate();
@@ -75,119 +81,48 @@ const Login = () => {
// 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');
if (!password || !username) {
setAlertmessage('Fill All required fields');
opentoast('Fill All required fields');
setLoading(false);
return;
}
setSubmitting(true);
try {
const res = await axios.post(`${process.env.REACT_APP_URL}/admin/login`, {
email: username,
password,
userfcmtoken: fcmtoken?.token
});
if (res.data.success) {
const { token, data } = res.data;
enqueueSnackbar('Login Successful', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 3000
});
setUsername('');
setPassword('');
localStorage.setItem('authname', data.email || data.firstname);
localStorage.setItem('userid', data.userid);
localStorage.setItem('roleid', data.roleid);
localStorage.setItem('token', token);
axios.defaults.headers.common.Authorization = `Bearer ${token}`;
navigate('/orders');
} else {
opentoast(res.data.message || 'Invalid Data');
}
} catch (err) {
opentoast(err.response?.data?.message || err.message);
} finally {
setLoading(false);
setSubmitting(false);
}
};
@@ -228,7 +163,7 @@ const Login = () => {
// 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="legendary" width={isMobile ? '160px' : '200px'} />
<img src={logo} alt="Doormile" width={isMobile ? '160px' : '200px'} style={{ filter: DOORMILE_RED_FILTER }} />
</Grid>
<Grid item xs={12}>
<Grid
@@ -275,10 +210,6 @@ const Login = () => {
<CardHeader title={<Typography variant="h3">Login</Typography>} />
</Stack>
</Grid>
{/* <Grid item xs={12}>
<AuthLogin isDemo={isLoggedIn} />
</Grid> */}
</Grid>
<CardContent>
<form
@@ -322,8 +253,8 @@ const Login = () => {
variant="outlined"
autoComplete="email"
required
onChange={usernamecheck}
error={checkusername}
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<TextField
margin="normal"
@@ -513,4 +444,4 @@ const Login = () => {
);
};
export default Login1;
export default Login;

View File

@@ -17,7 +17,7 @@ import {
Backdrop,
IconButton
} from '@mui/material';
import React, { Fragment, useEffect, useMemo, useState } from 'react';
import React, { Fragment, useEffect, useState } from 'react';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
@@ -32,7 +32,7 @@ import Loader from 'components/Loader';
import CircularLoader from 'components/CircularLoader';
import { Empty } from 'antd';
import HoverSocialCard from 'components/cards/statistics/HoverSocialCard';
import { DashboardFilled, OpenAIFilled } from '@ant-design/icons';
import { DashboardFilled } from '@ant-design/icons';
import { MdDirectionsBike } from 'react-icons/md';
import { FaMapLocationDot } from 'react-icons/fa6';
import { HiOutlineArrowLeft } from 'react-icons/hi';
@@ -234,9 +234,7 @@ const OrdersPreview = () => {
const {
data: paymentModes = [],
isLoading: paymentModesLoading,
isError: paymentModesError,
error: paymentModesErrorMessage
isLoading: paymentModesLoading
} = useQuery({
queryKey: ['paymentmodes'],
queryFn: fetchPaymentType
@@ -246,24 +244,13 @@ const OrdersPreview = () => {
const {
data: ridersList = [],
isLoading: ridersListLoading,
isError: ridersListError,
error: ridersListErrorMessage,
refetch: ridersListRefetch
isLoading: ridersListLoading
} = useQuery({
queryKey: ['ridersList', appId], // Unique key for caching & re-fetching
queryFn: fetchRidersList,
enabled: appId !== 0 // Ensures query runs only when appId is valid
});
const getRiderName = async (userid) => {
await ridersList.map((rider) => {
if (rider.userid == userid) {
return rider.firstname;
}
});
};
// ======================================================= || notifyRiderMutation || =======================================================
const notifyRiderMutation = useMutation({

View File

@@ -696,7 +696,7 @@
.weight-card-btn.active.weight-heavy {
background: rgba(99, 102, 241, 0.04) !important;
border-color: #662582 !important;
border-color: #C01227 !important;
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.15) !important;
}
@@ -707,7 +707,7 @@
left: 0;
right: 0;
height: 3px;
background: #662582;
background: #C01227;
}
/* Premium Card Overrides */

View File

@@ -9,7 +9,6 @@ import {
Button,
TextField,
Autocomplete,
Chip,
Divider,
DialogTitle,
DialogContent,
@@ -19,15 +18,15 @@ import {
IconButton,
Switch,
OutlinedInput,
FormGroup,
FormControlLabel,
Box,
Card,
useMediaQuery
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { calculateDrivingDistance, calculateTotalCharge } from '../../../utils/distance';
import { Empty } from 'antd';
import { FaPhoneAlt, FaBox, FaBoxes, FaTruck, FaArrowRight, FaArrowLeft, FaCheck, FaRoute, FaMoneyBillWave, FaChartLine, FaReceipt, FaPaperPlane } from 'react-icons/fa';
import { FaPhoneAlt, FaBox, FaBoxes, FaArrowRight, FaArrowLeft, FaCheck, FaRoute, FaMoneyBillWave, FaChartLine, FaReceipt, FaPaperPlane } from 'react-icons/fa';
import { GiDoorHandle } from 'react-icons/gi';
import { FaLandmarkDome } from 'react-icons/fa6';
import ClearIcon from '@mui/icons-material/Clear';
@@ -38,8 +37,6 @@ import axios from 'axios';
import { useTheme } from '@mui/material/styles';
import Geocode from 'react-geocode';
import Loader from 'components/Loader';
import * as geolib from 'geolib';
import MainCard from 'components/MainCard';
import { FaUser } from 'react-icons/fa6';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
@@ -49,7 +46,7 @@ import dayjs from 'dayjs';
import { enqueueSnackbar } from 'notistack';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
import { SearchOutlined, CloseOutlined, CalendarOutlined, ClockCircleOutlined, FileTextOutlined, MessageOutlined } from '@ant-design/icons';
import { SearchOutlined, CalendarOutlined, ClockCircleOutlined, FileTextOutlined, MessageOutlined } from '@ant-design/icons';
import MyLocationIcon from '@mui/icons-material/MyLocation';
import HighlightOffIcon from '@mui/icons-material/HighlightOff';
import { OpenToast } from 'components/third-party/OpenToast';
@@ -57,7 +54,6 @@ import { MapContainer, TileLayer, Marker, Polyline, useMap } from 'react-leaflet
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import './OrdersRedesign.css';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import AnimateButton from 'components/@extended/AnimateButton';
const pickupIcon = typeof window !== 'undefined' ? new L.Icon({
@@ -158,9 +154,9 @@ const OrderMap = ({ startPoint, endPoint, appLocaLat, appLocaLng }) => {
{hasPick && <Marker position={pickCoords} icon={pickupIcon} />}
{hasDrop && <Marker position={dropCoords} icon={dropoffIcon} />}
{routePoints.length > 0 ? (
<Polyline positions={routePoints} color="#662582" weight={4} />
<Polyline positions={routePoints} color="#C01227" weight={4} />
) : (
hasPick && hasDrop && <Polyline positions={[pickCoords, dropCoords]} color="#662582" weight={4} />
hasPick && hasDrop && <Polyline positions={[pickCoords, dropCoords]} color="#C01227" weight={4} />
)}
<MapBoundsController startPoint={startPoint} endPoint={endPoint} />
</MapContainer>
@@ -191,14 +187,12 @@ const Createorder1 = () => {
const tenantRef = useRef(null);
const [inputValue1, setInputValue1] = React.useState('');
const [inputValue2, setInputValue2] = React.useState('');
const [tenanatLocoId, setTenanatLocoId] = useState(localStorage.getItem('locationid'));
const [isLocation, setIsLocation] = useState(false);
const textFieldRef1 = useRef(null);
const textFieldRef1a = useRef(null);
const textFieldRef2 = useRef(null);
const [appId, setAppId] = useState(0);
const [open, setOpen] = useState(false);
const [clientdetail, setClientdetail] = useState([]);
const [, setClientdetail] = useState([]);
const [startdate, setStartdate] = useState(dayjs().format('MM-DD-YYYY'));
// const [starttime, setStatrttime] = useState(`${dayjs().format('MM-DD-YYYY')} 08:00:00`);
const [starttime, setStatrttime] = useState();
@@ -223,16 +217,15 @@ const Createorder1 = () => {
const [minKm, setMinKm] = useState(0);
const [totalCharge, setTotalCharge] = useState(0);
const [subCat, setSubCat] = useState([]);
const [subCatName, setSubCatName] = useState('Select ');
const [, setSubCatName] = useState('Select ');
const [subCatId, setSubCatId] = useState(0);
const [weight, setWeight] = useState('');
const [weight] = useState('');
const [tenantid, setTenantid] = useState(0);
const [locationid, setLocationid] = useState(0);
const [selectedCatChip, setSelectedCatChip] = useState(null);
const [isCustomerOpen, setIsCustomerOpen] = useState(false);
const [searchCustList, setSearchCustList] = useState('');
const [customerlist, setCustomerlist] = useState([]);
const [defaultPickup, setDefaultPickup] = useState(null);
const [, setDefaultPickup] = useState(null);
const [pickCust, setPickCust] = useState(null);
const [dropCust, setDropCust] = useState(null);
const [pickordrop, setpickordrop] = useState(0); // 1 ->pick 2 -> drop
@@ -329,20 +322,6 @@ const Createorder1 = () => {
appId && fetchtenantinfolist();
}, [appId]);
const handleChipClick = (chipLabel) => {
setSelectedCatChip(chipLabel);
};
const chipStyle = (chipLabel) => ({
cursor: 'pointer',
backgroundColor: selectedCatChip === chipLabel ? theme.palette.primary.main : 'default',
color: selectedCatChip === chipLabel ? '#fff' : '',
'&:hover': {
backgroundColor: selectedCatChip === chipLabel ? theme.palette.primary.main : theme.palette.primary.light,
color: '#fff'
}
});
const fetchTenantPricing = async (id) => {
try {
const pricingResponse = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${id}`);
@@ -365,130 +344,25 @@ const Createorder1 = () => {
}, [startPoint, endPoint]);
// google distance matrix logic
/*
const calculateDistance = async (pickup, drop) => {
const service = new google.maps.DistanceMatrixService();
const getDistanceMatrix = (origins, destinations, travelMode, unitSystem) => {
return new Promise((resolve, reject) => {
service.getDistanceMatrix(
{
origins: [new google.maps.LatLng(origins.latitude, origins.longitude)],
destinations: [new google.maps.LatLng(destinations.latitude, destinations.longitude)],
travelMode: travelMode,
unitSystem: unitSystem
},
(response, status) => {
if (status === 'OK') {
resolve(response);
} else {
reject(new Error(`Error calculating distance: ${status}`));
}
}
);
});
};
try {
// Use await to wait for the promise to resolve
const response = await getDistanceMatrix(pickup, drop, 'DRIVING', google.maps.UnitSystem.METRIC);
// Handle the response
const results = response.rows[0].elements;
for (let i = 0; i < results.length; i++) {
const element = results[i];
// Extract the numerical value of the distance
const distance = element.distance.value;
console.log('distance in m ', distance);
const distanceInKm = (distance / 1000).toFixed(2);
console.log('distance in km ', distanceInKm);
const roundedDistance = Math.round(distanceInKm);
console.log('roundedDistance', roundedDistance);
setDistance(roundedDistance);
if (roundedDistance < minKm) {
setTotalCharge(basePrice);
} else {
console.log('minKm', minKm);
console.log('pricePerKm', pricePerKm);
console.log('basePrice', basePrice);
const total = (roundedDistance - minKm) * pricePerKm + basePrice;
console.log('total', total);
setTotalCharge(total);
}
setShowDistance(true);
if (roundedDistance > appLocaRadius) {
setShowDistance(true);
setOpen(true);
}
// Extract the numerical value of the duration
const durationMatch = element.duration.text.match(/([\d.]+)/);
const duration = durationMatch ? parseInt(durationMatch[0]) : null;
// Display only the numerical values
console.log(`Distance: ${roundedDistance}, Duration: ${duration}`);
}
} catch (error) {
console.error('Error calculating distance:', error);
}
};*/
// Haversine + 1.3
const calculateDistance = async (pickup, drop) => {
// Haversine formula
const haversineDistance = (lat1, lon1, lat2, lon2) => {
const toRad = (value) => (value * Math.PI) / 180;
const R = 6371; // Earth radius in KM
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // distance in KM
};
try {
// 1. Get aerial distance
const aerialDistance = haversineDistance(pickup.latitude, pickup.longitude, drop.latitude, drop.longitude);
// 2. Convert to road approximation (1.3x)
const distanceInKm = (aerialDistance * 1.3).toFixed(2);
console.log('distance in km ', distanceInKm);
const roundedDistance = Math.round(distanceInKm);
console.log('roundedDistance', roundedDistance);
// 🔽 SAME AS YOUR EXISTING LOGIC (UNCHANGED)
const roundedDistance = await calculateDrivingDistance(pickup, drop);
console.log('calculated distance in km:', roundedDistance);
setDistance(roundedDistance);
if (roundedDistance < minKm) {
setTotalCharge(basePrice);
} else {
console.log('minKm', minKm);
console.log('pricePerKm', pricePerKm);
console.log('basePrice', basePrice);
const total = (roundedDistance - minKm) * pricePerKm + basePrice;
console.log('total', total);
setTotalCharge(total);
}
const total = calculateTotalCharge(roundedDistance, basePrice, pricePerKm, minKm);
console.log('total charge:', total);
setTotalCharge(total);
setShowDistance(true);
if (roundedDistance > appLocaRadius) {
setShowDistance(true);
setOpen(true);
}
// ⏱️ Approximate duration (optional, since no API)
// ⏱️ Approximate duration
const avgSpeed = 40; // km/h (adjust if needed)
const duration = Math.round((roundedDistance / avgSpeed) * 60); // minutes
console.log(`Distance: ${roundedDistance}, Duration: ${duration}`);
} catch (error) {
console.error('Error calculating distance:', error);
@@ -577,9 +451,9 @@ const Createorder1 = () => {
console.log('endtime', `${dayjs().format('MM-DD-YYYY')} ${closetime} `);
let arr = [];
for (
let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`, j = 0;
let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`;
dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0;
j++, i = dayjs(i).add(30, 'm')
i = dayjs(i).add(30, 'm')
) {
arr.push(i);
}
@@ -963,7 +837,6 @@ const Createorder1 = () => {
// radius: 100000 //km to m
}).getBounds()
});
let arr = [];
// Event listener for autocomplete place changed
autocomplete.addListener('place_changed', () => {
const place = autocomplete.getPlace();
@@ -1492,7 +1365,7 @@ const Createorder1 = () => {
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchOutlined style={{ fontSize: 15, color: '#662582' }} />
<SearchOutlined style={{ fontSize: 15, color: '#C01227' }} />
</InputAdornment>
),
endAdornment: (
@@ -1528,7 +1401,7 @@ const Createorder1 = () => {
InputProps={{
startAdornment: (
<InputAdornment position="start">
<FaLocationDot style={{ fontSize: 14, color: '#662582' }} />
<FaLocationDot style={{ fontSize: 14, color: '#C01227' }} />
</InputAdornment>
),
endAdornment: (
@@ -2270,7 +2143,7 @@ const Createorder1 = () => {
{/* Map Card */}
<Card className="orders-card" sx={{ p: 1.5, display: 'flex', flexDirection: 'column' }}>
<Typography sx={{ fontWeight: 700, mb: 1.25, display: 'flex', alignItems: 'center', gap: 0.75, color: '#1e293b', fontSize: '14px', letterSpacing: '-0.01em' }}>
<MyLocationIcon sx={{ color: '#662582', fontSize: 16 }} />
<MyLocationIcon sx={{ color: '#C01227', fontSize: 16 }} />
Live Route Preview
</Typography>
<div className="map-preview-wrapper">
@@ -2449,7 +2322,7 @@ const Createorder1 = () => {
}
}}
>
<DialogTitle sx={{ background: 'linear-gradient(135deg, #662582 0%, #9255AB 100%)', color: 'white', py: 2.5 }}>
<DialogTitle sx={{ background: 'linear-gradient(135deg, #C01227 0%, #D35968 100%)', color: 'white', py: 2.5 }}>
<Stack spacing={1.5}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'white' }}>
{`Select Saved Address (${pickordrop === 1 ? 'Pickup' : 'Drop'})`}

View File

@@ -1,39 +1,15 @@
import {
useEffect,
useState,
Fragment
// useReducer
} from 'react';
import { useEffect, useState, Fragment } from 'react';
import BorderColorIcon from '@mui/icons-material/BorderColor';
import {
// Navigate,
// useSearchParams,
useLocation,
useNavigate
} from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import axios from 'axios';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import {
// UserOutlined,
EnvironmentOutlined,
EditTwoTone
// DeleteTwoTone
} from '@ant-design/icons';
// import WomanIcon from '@mui/icons-material/Woman';
// import { Link } from 'react-router-dom';
// import SoupKitchenIcon from '@mui/icons-material/SoupKitchen';
import { EnvironmentOutlined, EditTwoTone } from '@ant-design/icons';
import DirectionsCarIcon from '@mui/icons-material/DirectionsCar';
import { KeyboardArrowUp, KeyboardArrowDown } from '@mui/icons-material';
// import { PopupTransition } from 'components/@extended/Transitions';
// import { useDispatch } from 'react-redux';
// import { openSnackbar } from 'store/reducers/snackbar';
// assets
import { DeleteFilled, NotificationOutlined } from '@ant-design/icons';
var utc = require('dayjs/plugin/utc');
// import { groupBy } from "core-js/actual/array/group-by";
// import "lodash.chunk";
// var chunk = require('lodash.chunk');
import {
Grid,
Typography,
@@ -79,13 +55,8 @@ import { PopupTransition } from 'components/@extended/Transitions';
import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined';
import MainCard from 'components/MainCard';
import Loader from 'components/Loader';
// import AlertCustomerDelete from 'sections/apps/customer/AlertCustomerDelete';
import dayjs from 'dayjs';
dayjs.extend(utc);
// import { Link as RouterLink } from 'react-router-dom';
// import PlayCircleFilled from '@mui/icons-material/PlayCircleFilled';
// import SmileFilled from '@mui/icons-material/Mood';
// import HeartFilled from '@mui/icons-material/Favorite';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
@@ -181,23 +152,6 @@ const Details = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// const fetchorderdetails = async () => {
// setLoading(true);
// await axios
// .get(`${process.env.REACT_APP_URL}/orders/orderbyid/?orderheaderid=${orderheaderid}`)
// .then((res) => {
// console.log(res);
// setLoading(false);
// })
// .catch((err) => {
// console.log(err);
// setLoading(false);
// });
// };
const fetchorderaddons = async () => {
setLoading(true);
await axios
@@ -249,21 +203,6 @@ const Details = () => {
console.log('res');
console.log(res);
setOrderarr(res.data.Details || []);
// let result = res.data.Details.find((res1) => res1.orderheaderid == searchParams.get('id'))
// orderheaderid
// console.log(result)
// setOrderaddons(result.orderaddons);
// setVenuetype(result.venuetype)
// setOtherinstructions(result.remarks)
// console.log("res");
// let result = _.chain(res.data.Details)
// .groupBy("shiftid")
// .map((value, key) => ({shiftid:key, locationaddress: value[0].locationaddress, roles: value }))
// .value()
// setcategoryarr(result);
console.log('categoryarr');
setcategoryarr(res.data.Details);
console.log(res.data.Details);
@@ -305,15 +244,7 @@ const Details = () => {
const cancelorder = async () => {
await axios
.put(`${process.env.REACT_APP_URL2}/orders/cancel`, {
// "Orderheaderid": parseInt(orderheaderid),
// "Tenantid": parseInt(tenantid),
// "Orderstatus": "cancelled",
// "Currentdatetime": dayjs().format('YYYY-MM-DD HH:mm:ss'),
// "Cod": false,
// "Remarks": "",
orderheaderid: parseInt(orderheaderid),
// "orderdetailid":78,
// "shiftid":788,
orderstatus: 'cancelled',
cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss'),
unserviceable: invoiceeligible ? 0 : 1
@@ -443,20 +374,10 @@ const Details = () => {
fetchorderaddons();
fetchorderattires();
fetchassignedcount();
// fetchuserdetails();
console.log(location.state || '');
// setOrderid(location.state.orderid || '');
// setEventlocation(location.state.eventlocation || '');
// setEventlocation(address || []);
// setOrderdate(dayjs(location.state.orderdate.substring(0, 10)).format('MM/DD/YYYY') || '');
// setDuedate(dayjs(location.state.orderdate.substring(0, 10)).format('MM/DD/YYYY') || '')
// setEventname(location.state.eventname || '');
// setClientname(location.state.tenantname || '')
} else {
setLoading(false);
}
// fetchorderdetails();
console.log(orderheaderid, tenantid);
}, [orderheaderid, tenantid, assignedpendingcount]);
@@ -522,12 +443,6 @@ const Details = () => {
.then((res) => {
console.log(res);
if (res.data.message === 'Successfully created') {
// if (orderheaderid && tenantid) {
// fetchorderdetails();
// fetchorderaddons();
// fetchorderattires();
// }
enqueueSnackbar('Roles assigned successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
@@ -631,32 +546,6 @@ const Details = () => {
});
};
// const updateorderstatus = async () => {
// await axios.put(`${process.env.REACT_APP_URL2}/orders/updateorderstatus`,{
// "orderheaderid":orderheaderid,
// "tenantid":tenantid,
// "orderstatus":"processing",
// "pending":"",
// "processing":dayjs().format('YYYY-MM-DD HH:mm:ss'),
// "completed":""
// })
// .then((res) => {
// console.log(res)
// fetchorderdetails();
// fetchorderaddons();
// fetchorderattires();
// })
// .catch((err) => {
// console.log(err)
// fetchorderdetails();
// fetchorderaddons();
// fetchorderattires();
// })
// }
const fetchassignedcount = async () => {
// console.log(obj1)
await axios
@@ -895,7 +784,7 @@ const Details = () => {
{stafflist.map((val, i) => {
const isSelected = staffarr.find((res) => res.userid == val.userid) ? true : false;
return (
<MobileCard key={i} accent="#662582" selected={isSelected}>
<MobileCard key={i} accent="#C01227" selected={isSelected}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1}>
<Avatar alt="" src={''} sx={{ width: 32, height: 32 }} />
@@ -1549,14 +1438,14 @@ const Details = () => {
{isMobile ? (
<MobileCardList>
{val5.orderdetails.length === 0 && (
<MobileCard accent="#662582">
<MobileCard accent="#C01227">
<Skeleton animation="wave" />
<Skeleton animation="wave" />
<Skeleton animation="wave" />
</MobileCard>
)}
{val5.orderdetails.map((row, i) => (
<MobileCard key={i + 1} accent="#662582" sx={{ opacity: row.status == 0 ? '' : '0.7' }}>
<MobileCard key={i + 1} accent="#C01227" sx={{ opacity: row.status == 0 ? '' : '0.7' }}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
<Stack direction="column">
<Typography sx={{ fontSize: 10, fontWeight: 800, color: '#94a3b8' }}>#{i + 1}</Typography>

Some files were not shown because too many files have changed in this diff Show More