initialisation on the doormile express console with astryx

This commit is contained in:
2026-07-29 18:16:25 +05:30
parent d5bde7d1ac
commit 7d672139b5
108 changed files with 12536 additions and 21351 deletions

View File

@@ -15,11 +15,11 @@ This skill is the **knowledge** Claude needs about the system (what each page do
## 1. Architecture at a glance ## 1. Architecture at a glance
- **Brand colour: `#662582` (NearlExpress purple)** — defined in `src/themes/theme/default.js` as `primary.main`. Used by the sidebar, page headers, dialog/popup headers, KPI primary tiles, search bars, edit-action buttons. Gradient pair `#662582 → #9255AB`. Status badges use a separate semantic palette (amber/indigo/cyan/teal/emerald/red/orange) — see root `CLAUDE.md` §6. - **Brand colour: `#C01227` (NearlExpress red)** — defined in `src/themes/theme/default.js` as `primary.main`. Used by the sidebar, page headers, dialog/popup headers, KPI primary tiles, search bars, edit-action buttons. Gradient pair `#C01227 → #D25463`. Status badges use a separate semantic palette (amber/indigo/cyan/teal/emerald/red/orange) — see root `CLAUDE.md` §6.
- **Stack:** React 18.2 + react-app-rewired (CRA), MUI 5, TanStack Query 5, Redux Toolkit, react-router 6, axios, dayjs, leaflet, firebase 10 (FCM), notistack, formik+yup, react-dnd. - **Stack:** React 18.2 + react-app-rewired (CRA), MUI 5, TanStack Query 5, Redux Toolkit, react-router 6, axios, dayjs, leaflet, firebase 10 (FCM), notistack, formik+yup, react-dnd.
- **Entry:** `src/index.js` → providers (Redux store, QueryClient, Router, ThemeCustomization, Notistack) → `src/App.js`. - **Entry:** `src/index.js` → providers (Redux store, QueryClient, Router, ThemeCustomization, Notistack) → `src/App.js`.
- **Auth gate:** `App.js` checks `localStorage.getItem('authname')` — empty redirects to `/login`. Login posts to `jupiter.nearle.app/users/console/login` with `configid: 9` and the device's FCM token. - **Auth gate:** `App.js` checks `localStorage.getItem('authname')` — empty redirects to `/login`. Login posts to `jupiter.nearle.app/users/console/login` with `configid: 9` and the device's FCM token.
- **Routing:** `src/routes/MainRoutes.js` declares all `/nearle/*` routes, lazy-loaded via `components/Loadable`. Sidebar items are declared in `src/menu-items/nearle.js`. - **Routing:** `src/routes/MainRoutes.js` declares all `/doormile/*` routes, lazy-loaded via `components/Loadable`. Sidebar items are declared in `src/menu-items/nearle.js`.
- **Data layer:** every server call lives in `src/pages/api/api.js`. Pages call those exports via `useQuery` / `useInfiniteQuery` / `useMutation`. Query keys MUST include every filter parameter so caching invalidates correctly. - **Data layer:** every server call lives in `src/pages/api/api.js`. Pages call those exports via `useQuery` / `useInfiniteQuery` / `useMutation`. Query keys MUST include every filter parameter so caching invalidates correctly.
- **State:** Redux Toolkit slices (`fcmSlice`, `loginUserSlice`, `menu`, `snackbar`, `toastSlice`, `auth`) for cross-page state; per-page UI state stays in `useState`. - **State:** Redux Toolkit slices (`fcmSlice`, `loginUserSlice`, `menu`, `snackbar`, `toastSlice`, `auth`) for cross-page state; per-page UI state stays in `useState`.
- **Two API bases:** `process.env.REACT_APP_URL` (primary) and `process.env.REACT_APP_URL2` (used for `/users/update`, `/tenants/update`, archival `/orders/getorders`, rider logs). - **Two API bases:** `process.env.REACT_APP_URL` (primary) and `process.env.REACT_APP_URL2` (used for `/users/update`, `/tenants/update`, archival `/orders/getorders`, rider logs).
@@ -70,16 +70,16 @@ flowchart TD
Sidebar --> Invoice Sidebar --> Invoice
Sidebar --> ReportsHub Sidebar --> ReportsHub
Dispatch[/"/nearle/dispatch<br/>Live Map · Riders · Batches"/]:::core Dispatch[/"/doormile/dispatch<br/>Live Map · Riders · Batches"/]:::core
Orders[/"/nearle/orders<br/>Orders Dashboard"/]:::core Orders[/"/doormile/orders<br/>Orders Dashboard"/]:::core
Deliveries[/"/nearle/deliveries<br/>Dispatched Deliveries"/]:::core Deliveries[/"/doormile/deliveries<br/>Dispatched Deliveries"/]:::core
Tenants[/"/nearle/tenants<br/>Client/Tenant Management"/]:::core Tenants[/"/doormile/tenants<br/>Client/Tenant Management"/]:::core
Pricing[/"/nearle/pricing<br/>Pricing Matrix (master-detail)"/]:::core Pricing[/"/doormile/pricing<br/>Pricing Matrix (master-detail)"/]:::core
Customers[/"/nearle/customers<br/>Customer Directory"/]:::core Customers[/"/doormile/customers<br/>Customer Directory"/]:::core
Riders[/"/nearle/riders<br/>Rider Pool"/]:::core Riders[/"/doormile/riders<br/>Rider Pool"/]:::core
Invoice[/"/nearle/invoice<br/>Billing"/]:::core Invoice[/"/doormile/invoice<br/>Billing"/]:::core
Requests[/"/nearle/requests<br/>Expense Approvals"/]:::core Requests[/"/doormile/requests<br/>Expense Approvals"/]:::core
ReportsHub[/"/nearle/reports/*<br/>BI Suite"/]:::core ReportsHub[/"/doormile/reports/*<br/>BI Suite"/]:::core
OrdersCreate[/"/orders/create"/]:::sub OrdersCreate[/"/orders/create"/]:::sub
OrdersMulti[/"/orders/createorders"/]:::sub OrdersMulti[/"/orders/createorders"/]:::sub
@@ -164,12 +164,12 @@ flowchart TD
sequenceDiagram sequenceDiagram
autonumber autonumber
participant U as Operator participant U as Operator
participant O as /nearle/orders participant O as /doormile/orders
participant S as Solver (routes/routemate.workolik.com) participant S as Solver (routes/routemate.workolik.com)
participant P as /nearle/dispatch/preview participant P as /doormile/dispatch/preview
participant R as reconcile-steps participant R as reconcile-steps
participant J as jupiter.nearle.app participant J as jupiter.nearle.app
participant D as /nearle/deliveries participant D as /doormile/deliveries
participant F as FCM (rider device) participant F as FCM (rider device)
U->>O: Select pending orders (checkbox) U->>O: Select pending orders (checkbox)
@@ -327,29 +327,29 @@ sequenceDiagram
| Route | File | Lazy import name | | Route | File | Lazy import name |
| --- | --- | --- | | --- | --- | --- |
| `/login` | `pages/nearle/login1.js` | `Login` | | `/login` | `pages/nearle/login1.js` | `Login` |
| `/nearle/dispatch` | `pages/nearle/dispatch/Dispatch.js` | `Dispatch` | | `/doormile/dispatch` | `pages/nearle/dispatch/Dispatch.js` | `Dispatch` |
| `/nearle/dispatch/preview` | `pages/nearle/dispatch/Preview.js` | `DispatchPreview` | | `/doormile/dispatch/preview` | `pages/nearle/dispatch/Preview.js` | `DispatchPreview` |
| `/nearle/orders` | `pages/nearle/orders/orders.js` | `Orders` | | `/doormile/orders` | `pages/nearle/orders/orders.js` | `Orders` |
| `/nearle/orders/preview` | `pages/nearle/orders/OrdersPreview.js` | `OrdersPreview` | | `/doormile/orders/preview` | `pages/nearle/orders/OrdersPreview.js` | `OrdersPreview` |
| `/nearle/orders/create` | `pages/nearle/orders/createorder1.js` | `Createorder1` | | `/doormile/orders/create` | `pages/nearle/orders/createorder1.js` | `Createorder1` |
| `/nearle/orders/createorders` | `pages/nearle/orders/multipleOrders.js` | `MultipleOrders` | | `/doormile/orders/createorders` | `pages/nearle/orders/multipleOrders.js` | `MultipleOrders` |
| `/nearle/orders/details` | `pages/nearle/orders/details.js` | `Details` | | `/doormile/orders/details` | `pages/nearle/orders/details.js` | `Details` |
| `/nearle/deliveries` | `pages/nearle/deliveries/deliveries.js` | `Deliveries` | | `/doormile/deliveries` | `pages/nearle/deliveries/deliveries.js` | `Deliveries` |
| `/nearle/tenants` | `pages/nearle/clients/Tenants.js` | `Tenants` | | `/doormile/tenants` | `pages/nearle/clients/Tenants.js` | `Tenants` |
| `/nearle/clients/create` | `pages/nearle/clients/createclient.js` | `Createclient` | | `/doormile/clients/create` | `pages/nearle/clients/createclient.js` | `Createclient` |
| `/nearle/pricing` | `pages/nearle/clientPricing/clientPricing.js` | `ClientsPricing` | | `/doormile/pricing` | `pages/nearle/clientPricing/clientPricing.js` | `ClientsPricing` |
| `/nearle/customers` | `pages/nearle/customers/customers.js` | `Customers` | | `/doormile/customers` | `pages/nearle/customers/customers.js` | `Customers` |
| `/nearle/customer/create` | `pages/nearle/clients/createCustomer.js` | `CreateCustomer` | | `/doormile/customer/create` | `pages/nearle/clients/createCustomer.js` | `CreateCustomer` |
| `/nearle/riders` | `pages/nearle/riders/riders.js` | `Riders` | | `/doormile/riders` | `pages/nearle/riders/riders.js` | `Riders` |
| `/nearle/riders/create` | `pages/nearle/riders/createrider.js` | `Createrider` | | `/doormile/riders/create` | `pages/nearle/riders/createrider.js` | `Createrider` |
| `/nearle/riders/edit` | `pages/nearle/riders/editRider.js` | `EditRider` | | `/doormile/riders/edit` | `pages/nearle/riders/editRider.js` | `EditRider` |
| `/nearle/invoice` | `pages/nearle/invoice/invoice.js` | `Invoice` | | `/doormile/invoice` | `pages/nearle/invoice/invoice.js` | `Invoice` |
| `/nearle/invoice/preview` | `pages/nearle/invoice/invoicePreview.js` | `InvoicePreview` | | `/doormile/invoice/preview` | `pages/nearle/invoice/invoicePreview.js` | `InvoicePreview` |
| `/nearle/requests` | `pages/nearle/requests/requests.js` | `Requests` | | `/doormile/requests` | `pages/nearle/requests/requests.js` | `Requests` |
| `/nearle/reports/orderssummary` | `pages/nearle/reports/ordersSummary.js` | `OrdersSummary` | | `/doormile/reports/orderssummary` | `pages/nearle/reports/ordersSummary.js` | `OrdersSummary` |
| `/nearle/reports/ordersdetails` | `pages/nearle/reports/ordersDetails.js` | `OrdersDetails` | | `/doormile/reports/ordersdetails` | `pages/nearle/reports/ordersDetails.js` | `OrdersDetails` |
| `/nearle/reports/riderssummary` | `pages/nearle/reports/ridersSummary.js` | `RidersSummary` | | `/doormile/reports/riderssummary` | `pages/nearle/reports/ridersSummary.js` | `RidersSummary` |
| `/nearle/reports/riderslogs` | `pages/nearle/reports/ridersLogs.js` | `RidersLogs` | | `/doormile/reports/riderslogs` | `pages/nearle/reports/ridersLogs.js` | `RidersLogs` |
| `/viewprofile` | `pages/nearle/viewProfile.js` | `ViewProfile` | | `/viewprofile` | `pages/nearle/viewProfile.js` | `ViewProfile` |
| `/maintenance/{404,500,under-construction,coming-soon}` | `pages/maintenance/*` | — | | `/maintenance/{404,500,under-construction,coming-soon}` | `pages/maintenance/*` | — |

95
API_ENDPOINTS.md Normal file
View File

@@ -0,0 +1,95 @@
# NearlExpress Console — API Endpoints Reference
> Auto-generated reference of every API call in `src/pages/api/api.js`.
> Base URLs are resolved from the env files at the repo root.
## Base URLs
| Env variable | Resolved value |
|---|---|
| `REACT_APP_URL` | `https://jupiter.nearle.app/live/api/v1` |
| `REACT_APP_URL2` | `https://jupiter.nearle.app/live/api/v2` |
| Bike solver (hardcoded) | `https://routes.workolik.com` |
| Auto solver (hardcoded) | `https://routemate.workolik.com` |
| Commit service (hardcoded) | `https://jupiter.nearle.app` |
> **Note:** In `env.staging`, `REACT_APP_URL2` is empty — URL2 endpoints resolve to relative paths in that build.
---
## Base `REACT_APP_URL` → `https://jupiter.nearle.app/live/api/v1`
| # | Function | Method | Full endpoint |
|---|---|---|---|
| 1 | `getRiderPeriodicLogs` | GET | `https://jupiter.nearle.app/live/api/v1/utils/getriderperiodiclogs?userid={userid}` |
| 2 | `fetchAppLocations` | GET | `https://jupiter.nearle.app/live/api/v1/partners/getlocations/?userid={userid}` |
| 3 | `fetchPercentageData` | GET | `https://jupiter.nearle.app/live/api/v1/orders/getordersummary/?applocationid={appId}&tenantid={tenantid}&locationid={locationid}&fromdate={startdate}&todate={enddate}` |
| 4 | `getTenants` | GET | `https://jupiter.nearle.app/live/api/v1/tenants/gettenants/?applocationid={appId}&status=active` |
| 5 | `gettenantlocations` | GET | `https://jupiter.nearle.app/live/api/v1/tenants/gettenantlocations/?tenantid={appId}` |
| 6 | `fetchorderscount` | GET | `https://jupiter.nearle.app/live/api/v1/orders/getordersummary/?applocationid={appId}&tenantid={tenantid}&locationid={locationid}&fromdate={startdate}&todate={enddate}&status={currentStatus}` |
| 7 | `fetchOrders` | GET | `https://jupiter.nearle.app/live/api/v1/orders/tenant/getorders/?applocationid={appId}&tenantid={tenantid}&locationid={locationid}&status={currentStatus}&fromdate={startdate}&todate={enddate}&keyword={search}&pageno={pageParam}&pagesize={rowsPerPage}` |
| 8 | `fetchPaymentType` | GET | `https://jupiter.nearle.app/live/api/v1/utils/getapptypes/?tag=paymentmode` |
| 9 | `fetchRidersList` | GET | `https://jupiter.nearle.app/live/api/v1/partners/getriders/?applocationid={appId}` |
| 10 | `notifyRider` | POST | `https://jupiter.nearle.app/live/api/v1/utils/notifyuser` |
| 11 | `cancelOrder` | PUT | `https://jupiter.nearle.app/live/api/v1/orders/updateorder` |
| 12 | `cancelMultipleOrder` | PUT | `https://jupiter.nearle.app/live/api/v1/orders/updatemultipleorders` |
| 13 | `fetchDeliveries` (all zones) | GET | `https://jupiter.nearle.app/live/api/v1/deliveries/getdeliveries/?appuserid={userid}&status={status}&fromdate={startdate}&todate={enddate}&pageno={pageParam}&pagesize={rowsPerPage}&keyword={search}&tenantid={tenantid}&locationid={locationid}&userid={riderid}` |
| 14 | `fetchDeliveries` (by zone) | GET | `https://jupiter.nearle.app/live/api/v1/deliveries/getdeliveries/?applocationid={appId}&status={status}&fromdate={startdate}&todate={enddate}&pageno={pageParam}&pagesize={rowsPerPage}&keyword={search}&tenantid={tenantid}&locationid={locationid}&userid={riderid}` |
| 15 | `fetchPercentageAPI` | GET | `https://jupiter.nearle.app/live/api/v1/deliveries/deliverysummary/?applocationid={appId}` |
| 16 | `fetchCountAPI` (all zones) | GET | `https://jupiter.nearle.app/live/api/v1/deliveries/deliverysummary/?appuserid={userid}&fromdate={startdate}&todate={enddate}` |
| 17 | `fetchCountAPI` (by zone) | GET | `https://jupiter.nearle.app/live/api/v1/deliveries/deliverysummary/?applocationid={appId}&fromdate={startdate}&todate={enddate}&tenantid={tenantid}&locationid={locationid}&userid={riderid}` |
| 18 | `cancelDeliveryAPI` | PUT | `https://jupiter.nearle.app/live/api/v1/deliveries/updatedelivery` |
| 19 | `getorderdetails` | GET | `https://jupiter.nearle.app/live/api/v1/orders/getorderdetails?orderheaderid={orderHeaderid}` |
| 20 | `changeRiderAPI` | PUT | `https://jupiter.nearle.app/live/api/v1/deliveries/updatedelivery` |
| 21 | `updateDeliveryAPI` | PUT | `https://jupiter.nearle.app/live/api/v1/deliveries/updatedelivery` |
| 22 | `fetchAllRiders` | GET | `https://jupiter.nearle.app/live/api/v1/partners/getallriders/?applocationid={appId}&pageno={pageParam}&pagesize=20&keyword={search}&status={''|Active}` |
| 23 | `getallridersummary` | GET | `https://jupiter.nearle.app/live/api/v1/partners/getallridersummary/?applocationid={appId}&status={''|Active}` |
| 24 | `fetchRiders` | GET | `https://jupiter.nearle.app/live/api/v1/partners/getriders/?applocationid={appId}&pageno={pageParam}&pagesize=20&keyword={search}` |
| 25 | `getriderstatus` | GET | `https://jupiter.nearle.app/live/api/v1/utils/getriderstatus` |
| 26 | `getreportsummary` | GET | `https://jupiter.nearle.app/live/api/v1/deliveries/getreportsummary/?applocationid={appId}&tenantid={tenantid}&locationid={locationid}&fromdate={startdate}&todate={enddate}` |
| 27 | `getreportlocationsummary` | GET | `https://jupiter.nearle.app/live/api/v1/deliveries/getreportlocationsummary/?applocationid={appId}&tenantid={tenantid}&locationid={locationid}&fromdate={startdate}&todate={enddate}` |
| 28 | `getriderbydelivery` | GET | `https://jupiter.nearle.app/live/api/v1/deliveries/getriderbydelivery/?applocationid={appId}&tenantid={tenantid}&locationid={locationid}&fromdate={startdate}&todate={enddate}` |
| 29 | `fetchCount` (all zones) | GET | `https://jupiter.nearle.app/live/api/v1/deliveries/deliverysummary/?fromdate={startdate}&todate={enddate}` |
| 30 | `fetchCount` (by zone) | GET | `https://jupiter.nearle.app/live/api/v1/deliveries/deliverysummary/?applocationid={appId}&fromdate={startdate}&todate={enddate}` |
| 31 | `fetchRidersSummary` | GET | `https://jupiter.nearle.app/live/api/v1/deliveries/getridersummary/?applocationid={appId}&fromdate={startdate}&todate={enddate}` |
| 32 | `fetchLocations` | GET | `https://jupiter.nearle.app/live/api/v1/partners/getpartners` |
| 33 | `fetchOrders1` | GET | `https://jupiter.nearle.app/live/api/v1/orders/tenant/getorders/?tenantid={tenantid}&locationid={locationid}&status={status}&fromdate={startdate}&todate={enddate}&pageno={pageParam}&pagesize={rowsPerPage}&keyword={search}` |
| 34 | `getusers` | GET | `https://jupiter.nearle.app/live/api/v1/users/getusers/?configid=9&userid={userid}` |
| 35 | `getallriders` | GET | `https://jupiter.nearle.app/live/api/v1/partners/getallriders?partnerid=64` |
---
## Base `REACT_APP_URL2` → `https://jupiter.nearle.app/live/api/v2`
| # | Function | Method | Full endpoint |
|---|---|---|---|
| 36 | `fetchorderdetails` (all zones) | GET | `https://jupiter.nearle.app/live/api/v2/orders/getorders/?appuserid={userid}&fromdate={startdate}&todate={enddate}&pageno={page+1}&pagesize={rowsPerPage}` |
| 37 | `fetchorderdetails` (by zone) | GET | `https://jupiter.nearle.app/live/api/v2/orders/getorders/?fromdate={startdate}&todate={enddate}&applocationid={appId}&pageno={page}&pagesize={rowsPerPage}` |
| 38 | `fetchRidersLogs` | GET | `https://jupiter.nearle.app/live/api/v2/partners/getriderlogs/?applocationid={appId}&fromdate={startdate}&todate={startdate}&keyword={search}` |
---
## Hardcoded — Bike solver `https://routes.workolik.com`
| # | Function | Method | Full endpoint |
|---|---|---|---|
| 39 | `createOptimisationDeliveries` | POST | `https://routes.workolik.com/api/v1/optimization/createdeliveries` |
| 40 | `reconcileSteps` | POST | `https://routes.workolik.com/api/v1/optimization/reconcile-steps` |
| 41 | `fetchBatchEfficiency` | POST | `https://routes.workolik.com/api/v1/batch/efficiency` |
| 42 | `createAutomationDeliveries` (bike mode) | POST | `https://routes.workolik.com/api/v1/optimization/riderassign?hypertuning_params={params}` |
---
## Hardcoded — Auto solver `https://routemate.workolik.com`
| # | Function | Method | Full endpoint |
|---|---|---|---|
| 43 | `createAutomationDeliveries` (auto mode) | POST | `https://routemate.workolik.com/api/v1/optimization/riderassign?strategy=multi_trip` |
---
## Hardcoded — Commit service `https://jupiter.nearle.app`
| # | Function | Method | Full endpoint |
|---|---|---|---|
| 44 | `finalCreatedeliveries` | POST | `https://jupiter.nearle.app/live/api/v1/deliveries/createdeliveries` |

View File

@@ -1,4 +1,4 @@
# CLAUDE.md — NearlExpress Console (xpressconsole) # CLAUDE.md — Doormile Express Console (xpressconsole)
> Project-level rules and conventions for Claude Code when working in this repo. > Project-level rules and conventions for Claude Code when working in this repo.
> **Read this in full before editing.** When in doubt about a pattern, copy from `src/pages/nearle/deliveries/deliveries.js` — it is the canonical reference for both the design system and the data layer. > **Read this in full before editing.** When in doubt about a pattern, copy from `src/pages/nearle/deliveries/deliveries.js` — it is the canonical reference for both the design system and the data layer.
@@ -7,7 +7,7 @@
## 1. What this is ## 1. What this is
A React 18 operator console for the NearlExpress dispatch platform. Operators use it to manage orders, run the AI dispatch optimiser, watch a live map of riders, edit tenants/pricing/invoices, and pull BI reports. Production users are warehouse staff, not end customers. A React 18 operator console for the Doormile Express dispatch platform. Operators use it to manage orders, run the AI dispatch optimiser, watch a live map of riders, edit tenants/pricing/invoices, and pull BI reports. Production users are warehouse staff, not end customers.
For the per-page API map and architectural flow chart, see the project skill **`nearlexpress-docs`** (`.claude/skills/nearlexpress-docs/SKILL.md`). Do not duplicate that content here. For the per-page API map and architectural flow chart, see the project skill **`nearlexpress-docs`** (`.claude/skills/nearlexpress-docs/SKILL.md`). Do not duplicate that content here.
@@ -63,7 +63,7 @@ npm run lint
1. **Do not introduce Next.js / SSR patterns.** No `getServerSideProps`, no `app/` directory, no `next/*` imports. This is CRA. 1. **Do not introduce Next.js / SSR patterns.** No `getServerSideProps`, no `app/` directory, no `next/*` imports. This is CRA.
2. **Do not rewrite shared design tokens.** Every new page that needs the polished UI must reuse the `DT` token block (see §6). Do not invent a parallel palette. 2. **Do not rewrite shared design tokens.** Every new page that needs the polished UI must reuse the `DT` token block (see §6). Do not invent a parallel palette.
3. **Do not change `package.json` dependency versions** unless explicitly asked. The build is sensitive to webpack/svgr/react-scripts versions (see `resolutions` in `package.json`). 3. **Do not change `package.json` dependency versions** unless explicitly asked. The build is sensitive to webpack/svgr/react-scripts versions (see `resolutions` in `package.json`).
4. **Do not bypass the dispatch reconcile step.** After any manual edit on `/nearle/dispatch/preview` (rider swap, step reorder), the page **must** call `POST /optimization/reconcile-steps` before `POST /deliveries/createdeliveries`. Skipping this corrupts route sequences. 4. **Do not bypass the dispatch reconcile step.** After any manual edit on `/doormile/dispatch/preview` (rider swap, step reorder), the page **must** call `POST /optimization/reconcile-steps` before `POST /deliveries/createdeliveries`. Skipping this corrupts route sequences.
5. **Do not commit `.env*` files** beyond `env.staging` (which is the agreed-shared staging baseline). 5. **Do not commit `.env*` files** beyond `env.staging` (which is the agreed-shared staging baseline).
6. **Do not introduce TypeScript files** (`.ts` / `.tsx`) into this repo. It is JavaScript; mixing creates lint and tooling friction. 6. **Do not introduce TypeScript files** (`.ts` / `.tsx`) into this repo. It is JavaScript; mixing creates lint and tooling friction.
7. **Do not use absolute `http://localhost` URLs** in code. Always read from `process.env.REACT_APP_URL` / `REACT_APP_URL2`. 7. **Do not use absolute `http://localhost` URLs** in code. Always read from `process.env.REACT_APP_URL` / `REACT_APP_URL2`.
@@ -82,7 +82,7 @@ src/
├── config.js # Theme constants (DRAWER_WIDTH=260, fontFamily, mode, presetColor, ThemeMode, MenuOrientation, ThemeDirection) ├── config.js # Theme constants (DRAWER_WIDTH=260, fontFamily, mode, presetColor, ThemeMode, MenuOrientation, ThemeDirection)
├── routes/ ├── routes/
│ ├── index.js # Combines MainRoutes + LoginRoutes │ ├── index.js # Combines MainRoutes + LoginRoutes
│ ├── MainRoutes.js # All /nearle/* routes, lazy-loaded via Loadable(lazy(...)) │ ├── MainRoutes.js # All /doormile/* routes, lazy-loaded via Loadable(lazy(...))
│ └── LoginRoutes.js │ └── LoginRoutes.js
├── layout/ ├── layout/
│ ├── MainLayout/ # Sidebar + header frame (used for all logged-in pages) │ ├── MainLayout/ # Sidebar + header frame (used for all logged-in pages)
@@ -117,7 +117,7 @@ src/
## 6. Design system (the `DT` token block) ## 6. Design system (the `DT` token block)
The polished pages (`deliveries.js`, `clients/Tenants.js`, `clientPricing/clientPricing.js`) share a token block at the top of the file. Every new operator page must paste and reuse this block — do not invent fresh colours, spacings, or radius numbers. The polished pages (`deliveries.js`) share a token block at the top of the file. Every new operator page must paste and reuse this block — do not invent fresh colours, spacings, or radius numbers.
```js ```js
const DT = { const DT = {
@@ -174,43 +174,43 @@ const AccentAvatar = ({ color, selected, size = 24, children }) => (
### Page anatomy (every operator page should follow this order) ### 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 `#D25463`) 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}`). 1. **Gradient header `<Paper>`**`linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 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 purple `#662582`; other tiles use semantic status colours. 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. 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). 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 purple `edge('#662582')`. 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. 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. 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 ### 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` — Doormile Express 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`): Variants (also from `theme/default.js`):
| Token | Hex | Use | | Token | Hex | Use |
|---|---|---| |---|---|---|
| `primary.lighter` | `#E8D9EF` | Very subtle wash bg | | `primary.lighter` | `#F5DBDE` | Very subtle wash bg |
| `primary.light` / `primary.400` | `#9255AB` | Gradient pair with main | | `primary.light` / `primary.400` | `#D25463` | Gradient pair with main |
| `primary.main` | `#662582` | Brand primary — default for all brand surfaces | | `primary.main` | `#C01227` | Brand primary — default for all brand surfaces |
| `primary.dark` | `#4D1C61` | Hover / pressed states | | `primary.dark` | `#910E1D` | Hover / pressed states |
| `primary.darker` | `#260E30` | Deep contrast text on light bg | | `primary.darker` | `#48070F` | 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('#D25463')} 100%)` (subtle wash) or `linear-gradient(135deg, #C01227 0%, #D25463 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` currently uses `#6366f1` (indigo) as its brand accent — a holdover from the first design pass before brand red was canonicalised. It is scheduled to migrate to `#C01227`. The `createorder1` Saved-Address dialog has already been migrated.
### Status palette (semantic — distinct from brand) ### 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 | | Meaning | Colour |
|--------------------------|-----------| |--------------------------|-----------|
| Pending / waiting | `#f59e0b` (amber) | | 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) | | Arrived | `#06b6d4` (cyan) |
| Picked up | `#8b5cf6` (light purple — distinct from brand) | | Picked up | `#8b5cf6` (light purple — distinct from brand) |
| Active / in-transit | `#14b8a6` (teal) | | Active / in-transit | `#14b8a6` (teal) |
@@ -300,3 +300,34 @@ These colour-code lifecycle states. Do **not** swap them for brand purple — op
- **For the per-page API map and the architectural flow chart** (which endpoint each page calls, what the optimisation pipeline does, FCM flow), invoke the project skill **`nearlexpress-docs`** (`.claude/skills/nearlexpress-docs/SKILL.md`) rather than restating the content here. - **For the per-page API map and the architectural flow chart** (which endpoint each page calls, what the optimisation pipeline does, FCM flow), invoke the project skill **`nearlexpress-docs`** (`.claude/skills/nearlexpress-docs/SKILL.md`) rather than restating the content here.
- **For shared design patterns** between pages, the source of truth is the `DT` token block and helpers near the top of `src/pages/nearle/deliveries/deliveries.js` (search for `const DT = {`). Copy from there, don't redesign. - **For shared design patterns** between pages, the source of truth is the `DT` token block and helpers near the top of `src/pages/nearle/deliveries/deliveries.js` (search for `const DT = {`). Copy from there, don't redesign.
<!-- ASTRYX:START -->
Astryx v0.1.9 · 153 components
CLI: run every command as `yarn dlx @astryxdesign/cli <cmd>` (shown below as `astryx ...`).
SETUP (once, in your app entry e.g. main.tsx) — without these, components render unstyled:
import "@astryxdesign/core/reset.css";
import "@astryxdesign/core/astryx.css";
WORKFLOW — discover, don't guess. Before writing UI:
1. `astryx build "<idea>"` — START HERE: returns a kit (closest [page] + [block]s + [component]s). No args = full playbook.
2. `astryx template <name> [--skeleton]` — scaffold the [page]/[block]s it named, or study their layout. Templates are reference code.
3. `astryx component <Name>` — props + examples for every component you use.
RULES:
- No <div> — components do all layout/spacing. Full page → AppShell; sidebar nav → SideNav.
- Frame first: pick the shell (AppShell / Layout+LayoutPanel) and budget regions in px BEFORE writing content (`astryx docs layout`).
- Dense data = rows (Table, List/Item) edge-to-edge — never Card-wrapped list items. Card = dashboard widgets, galleries, settings groups only.
- Status → StatusDot/Token; Badge only for counts and enumerated states, never decoration.
- Custom styling: component props first; else style/className with tokens — var(--color-*|--spacing-*|--radius-*). No raw hex/px. (No StyleX/Tailwind compiler here — don't use xstyle/utility classes.)
- Tokens for every value (`astryx docs tokens`). Brand/accent via `astryx theme` — never override --color-* in :root.
- SELF-CHECK before you finish: re-read the file and replace any raw <div>/<span> layout, imported .css/@apply, or hardcoded value (#hex, 16px) with the component or a token (var(--color-*|--spacing-*|…)). If unsure a component/prop exists, run `astryx component <Name>` / `astryx search "<thing>"`; don't hand-roll CSS.
MORE CLI:
search "<query>" find any component / hook / doc / template / block
component --list 153 components by category
template --list page + block recipes
docs <topic> color, elevation, icons, illustrations, internationalization, layout, migration, motion, principles, shape, spacing, styling, theme, tokens, typography
swizzle <Name> eject component source for deep customization
upgrade --apply run after any @astryxdesign/core bump
<!-- ASTRYX:END -->

26
FLOW.md
View File

@@ -56,16 +56,16 @@ flowchart TD
Sidebar --> ViewProfile Sidebar --> ViewProfile
%% ============================ Top-level pages ============================ %% ============================ Top-level pages ============================
Dispatch[/"/nearle/dispatch<br/>Live Map · Riders · Batches"/]:::core Dispatch[/"/doormile/dispatch<br/>Live Map · Riders · Batches"/]:::core
Orders[/"/nearle/orders<br/>Orders Dashboard"/]:::core Orders[/"/doormile/orders<br/>Orders Dashboard"/]:::core
Deliveries[/"/nearle/deliveries<br/>Dispatched Deliveries"/]:::core Deliveries[/"/doormile/deliveries<br/>Dispatched Deliveries"/]:::core
Tenants[/"/nearle/tenants<br/>Client/Tenant Management"/]:::core Tenants[/"/doormile/tenants<br/>Client/Tenant Management"/]:::core
Pricing[/"/nearle/pricing<br/>Pricing Matrix (master-detail)"/]:::core Pricing[/"/doormile/pricing<br/>Pricing Matrix (master-detail)"/]:::core
Customers[/"/nearle/customers<br/>Customer Directory"/]:::core Customers[/"/doormile/customers<br/>Customer Directory"/]:::core
Riders[/"/nearle/riders<br/>Rider Pool"/]:::core Riders[/"/doormile/riders<br/>Rider Pool"/]:::core
Invoice[/"/nearle/invoice<br/>Billing"/]:::core Invoice[/"/doormile/invoice<br/>Billing"/]:::core
Requests[/"/nearle/requests<br/>Expense Approvals"/]:::core Requests[/"/doormile/requests<br/>Expense Approvals"/]:::core
ReportsHub[/"/nearle/reports/*<br/>BI Suite"/]:::core ReportsHub[/"/doormile/reports/*<br/>BI Suite"/]:::core
ViewProfile[/"/viewprofile<br/>Profile Manager"/]:::sub ViewProfile[/"/viewprofile<br/>Profile Manager"/]:::sub
%% ============================ Sub-routes ============================ %% ============================ Sub-routes ============================
@@ -185,12 +185,12 @@ flowchart TD
sequenceDiagram sequenceDiagram
autonumber autonumber
participant U as Operator participant U as Operator
participant O as /nearle/orders participant O as /doormile/orders
participant S as Solver (routes/routemate.workolik.com) participant S as Solver (routes/routemate.workolik.com)
participant P as /nearle/dispatch/preview participant P as /doormile/dispatch/preview
participant R as reconcile-steps participant R as reconcile-steps
participant J as jupiter.nearle.app participant J as jupiter.nearle.app
participant D as /nearle/deliveries participant D as /doormile/deliveries
participant F as FCM (rider device) participant F as FCM (rider device)
U->>O: Select pending orders (checkbox) U->>O: Select pending orders (checkbox)

View File

@@ -1,4 +1,4 @@
# NearleXpress - Operator Dispatch Console & Deliveries Portal # Doormile Express - 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. 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,7 +1,29 @@
const webpack = require('webpack'); const webpack = require('webpack');
const WorkBoxPlugin = require('workbox-webpack-plugin'); const WorkBoxPlugin = require('workbox-webpack-plugin');
// NOTE on Astryx + StyleX custom styling (xstyle / stylex.create()):
// @stylexjs/babel-plugin alone only transforms the JS call sites — it does
// NOT inject the generated CSS. That requires a matching bundler plugin
// (@stylexjs/webpack-plugin), which is stuck at 0.11.1 and pins its own
// internal @stylexjs/babel-plugin@0.11.1 — two majors behind the 0.19.x
// core that this Astryx version requires, so the two would hash class
// names differently and produce the exact same "styled but invisible"
// bug either way. Until StyleX's webpack tooling catches up (or we write
// a custom metadata-collecting loader), custom Astryx styling is limited
// to component props; anything a prop can't express uses a plain native
// element with an inline `style` (StyleX-free, so it isn't affected).
// See themes/astryx.js and pages/nearle/login.js.
module.exports = function override(config) { module.exports = function override(config) {
// @astryxdesign/core ships as ESM with relative imports missing extensions
// (e.g. '../Tooltip/Tooltip'), which webpack 5 rejects under strict ESM
// resolution. Relax it for this package only rather than globally.
config.module.rules.push({
test: /\.m?js$/,
include: /node_modules[\\/]@astryxdesign/,
resolve: { fullySpecified: false }
});
config.resolve.fallback = { config.resolve.fallback = {
process: require.resolve('process/browser'), process: require.resolve('process/browser'),
// zlib: require.resolve('browserify-zlib'), // zlib: require.resolve('browserify-zlib'),

977
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,18 +5,21 @@
"dependencies": { "dependencies": {
"@ant-design/colors": "^7.0.0", "@ant-design/colors": "^7.0.0",
"@ant-design/icons": "^5.0.1", "@ant-design/icons": "^5.0.1",
"@astryxdesign/core": "^0.1.9",
"@astryxdesign/theme-neutral": "^0.1.9",
"@emotion/cache": "^11.10.7", "@emotion/cache": "^11.10.7",
"@emotion/react": "^11.10.6", "@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6", "@emotion/styled": "^11.10.6",
"@mui/base": "^5.0.0-alpha.126", "@mui/base": "^5.0.0-beta.70",
"@mui/icons-material": "^5.14.19", "@mui/icons-material": "^5.14.19",
"@mui/lab": "^5.0.0-alpha.127", "@mui/lab": "^5.0.0-alpha.127",
"@mui/material": "^5.12.1", "@mui/material": "^5.12.1",
"@mui/x-date-pickers": "^6.18.2", "@mui/x-date-pickers": "^7.29.4",
"@react-google-maps/api": "^2.20.7", "@react-google-maps/api": "^2.20.7",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^2.12.0",
"@stylexjs/stylex": "^0.19.0",
"@svgr/webpack": "^7.0.0", "@svgr/webpack": "^7.0.0",
"@tanstack/react-query": "^5.17.9", "@tanstack/react-query": "^5.101.4",
"antd": "^5.11.5", "antd": "^5.11.5",
"autosuggest-highlight": "^3.3.4", "autosuggest-highlight": "^3.3.4",
"axios": "^1.3.5", "axios": "^1.3.5",
@@ -28,37 +31,35 @@
"env-cmd": "^10.1.0", "env-cmd": "^10.1.0",
"firebase": "^10.14.1", "firebase": "^10.14.1",
"formik": "^2.2.9", "formik": "^2.2.9",
"framer-motion": "^10.12.4", "framer-motion": "^12.42.2",
"geolib": "^3.3.4", "geolib": "^3.3.4",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mui-daterange-picker": "^1.0.5",
"notistack": "^3.0.1", "notistack": "^3.0.1",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"process": "^0.11.10", "process": "^0.11.10",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.2.0", "react": "^19.2.8",
"react-csv": "^2.2.2", "react-csv": "^2.2.2",
"react-device-detect": "^2.2.3", "react-device-detect": "^2.2.3",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0", "react-dom": "^19.2.8",
"react-geocode": "^0.2.3", "react-geocode": "^0.2.3",
"react-google-autocomplete": "^2.7.3", "react-google-autocomplete": "^2.7.3",
"react-icons": "^4.12.0", "react-icons": "^4.12.0",
"react-intl": "^6.4.1", "react-intl": "^7.1.14",
"react-leaflet": "^4.2.1", "react-leaflet": "^5.0.0",
"react-loading-icons": "^1.1.0", "react-loading-icons": "^1.1.0",
"react-redux": "^8.0.5", "react-redux": "^9.2.0",
"react-router": "^6.10.0", "react-router": "^6.10.0",
"react-router-dom": "^6.10.0", "react-router-dom": "^6.10.0",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-timer-hook": "^3.0.5", "react-timer-hook": "^3.0.5",
"react-to-print": "^2.15.0", "react-to-print": "^3.3.0",
"react18-input-otp": "^1.1.3", "redux": "^5.0.1",
"redux": "^4.2.1",
"simplebar": "^6.2.5", "simplebar": "^6.2.5",
"simplebar-react": "^3.2.4", "simplebar-react": "^3.2.4",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -5,7 +5,7 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.png" /> <link rel="icon" href="%PUBLIC_URL%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta name="description" content="Nearle Neighbourhood commerce" /> <meta name="description" content="Doormile Neighbourhood commerce" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link <link
rel="stylesheet" rel="stylesheet"
@@ -28,7 +28,7 @@
work correctly both with client-side routing and a non-root public URL. 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`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>Nearle Console</title> <title>Doormile Express Console</title>
<link rel="preconnect" href="https://fonts.gstatic.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" />
<link <link

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -1,6 +1,6 @@
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import CircularProgress from '@mui/material/CircularProgress'; 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 // Styled Loader Wrapper
const LoaderWrapper = styled('div')(() => ({ const LoaderWrapper = styled('div')(() => ({
@@ -39,7 +39,7 @@ const CircularLoader = () => (
{/* Logo Positioned at the Center */} {/* Logo Positioned at the Center */}
<LogoWrapper> <LogoWrapper>
<img src={nelogo} alt="Logo" style={{ width: '100%', height: '100%' }} /> <img src={nelogo} alt="Doormile" style={{ width: '100%', height: '100%', objectFit: 'cover', transform: 'scale(1.4)' }} />
</LogoWrapper> </LogoWrapper>
</LoaderWrapper> </LoaderWrapper>
); );

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Dialog, DialogTitle, DialogContent, Button, Stack, Typography } from '@mui/material'; import { Dialog, DialogTitle, DialogContent, Button, Stack, Typography } from '@mui/material';
import { DateRangePicker } from 'mui-daterange-picker'; import { DateRangePicker } from 'components/nearle_components/DateRangePicker';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { addDays, addWeeks, startOfWeek, endOfWeek, startOfMonth, endOfMonth, addMonths } from 'date-fns'; import { addDays, addWeeks, startOfWeek, endOfWeek, startOfMonth, endOfMonth, addMonths } from 'date-fns';

View File

@@ -1,6 +1,6 @@
# CLAUDE.md — `src/components/nearle_components/` # CLAUDE.md — `src/components/nearle_components/`
Rules for editing the shared NearlExpress UI primitives. Rules for editing the shared Doormile Express UI primitives.
These components are imported across every page. Changes here have **fan-out impact** — measure twice, cut once. These components are imported across every page. Changes here have **fan-out impact** — measure twice, cut once.
@@ -26,13 +26,13 @@ The `DT` design tokens (palette, alpha helpers, `pillFieldSx`, `SoftPaper`, `Acc
**Hard rules when editing components here:** **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`. - **Universal brand colour is `#C01227`** (Doormile Express 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 → #D25463`.
- **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. - **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`, 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. - 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. - Border radii: `12` (inner), `16` (card), `999` (pill). No other values.
- Shadows: `DT.shadowSoft` / `DT.shadowMd` / `DT.shadowPop`. No raw `box-shadow` strings. - 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`) 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

@@ -0,0 +1,90 @@
import { useState } from 'react';
import { Box, Button, Divider, Stack, Typography } from '@mui/material';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { DT, soft, edge } from 'themes/dt/tokens';
// Drop-in replacement for the unmaintained `mui-daterange-picker` package
// (pinned to React 17, never updated — see the React 19 upgrade plan).
// Matches its public shape: `onChange({ startDate, endDate, label })` fires
// on every selection (preset or custom), `definedRanges` renders the quick
// picks. `open`/`toggle` are accepted for API compatibility but unused —
// visibility is already gated by the parent Dialog.
export const DateRangePicker = ({ onChange, definedRanges = [] }) => {
const [customStart, setCustomStart] = useState(null);
const [customEnd, setCustomEnd] = useState(null);
const pickPreset = (range) => {
onChange({ startDate: range.startDate, endDate: range.endDate, label: range.label });
};
const pickCustom = (nextStart, nextEnd) => {
if (nextStart && nextEnd) {
onChange({ startDate: nextStart.toDate(), endDate: nextEnd.toDate(), label: undefined });
}
};
return (
<Stack spacing={2.5} sx={{ width: '100%', pb: 1 }}>
<Stack direction="row" flexWrap="wrap" gap={1}>
{definedRanges.map((range) => (
<Button
key={range.label}
onClick={() => pickPreset(range)}
sx={{
borderRadius: DT.radiusPill + 'px',
px: 2,
py: 0.75,
fontWeight: 700,
fontSize: 13,
textTransform: 'none',
color: DT.textPrimary,
bgcolor: soft('#f59e0b'),
border: '1px solid',
borderColor: edge('#f59e0b'),
'&:hover': { bgcolor: soft('#f59e0b'), borderColor: '#f59e0b' }
}}
>
{range.label}
</Button>
))}
</Stack>
<Divider sx={{ borderColor: DT.divider }} />
<Box>
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 700, textTransform: 'uppercase' }}>
Custom range
</Typography>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} sx={{ mt: 1 }}>
<DatePicker
label="Start date"
value={customStart}
onChange={(v) => {
setCustomStart(v);
pickCustom(v, customEnd);
}}
format="DD-MM-YYYY"
slotProps={{ textField: { size: 'small', fullWidth: true } }}
/>
<DatePicker
label="End date"
value={customEnd}
minDate={customStart || undefined}
onChange={(v) => {
setCustomEnd(v);
pickCustom(customStart, v);
}}
format="DD-MM-YYYY"
slotProps={{ textField: { size: 'small', fullWidth: true } }}
/>
</Stack>
</LocalizationProvider>
</Box>
</Stack>
);
};
export default DateRangePicker;

View File

@@ -1,7 +1,7 @@
// LoaderWithImage.jsx // LoaderWithImage.jsx
import React from 'react'; import React from 'react';
import { Box, CircularProgress } from '@mui/material'; 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' }) { export default function LoaderWithImage({ size = 70, imgSize = 40, alt = 'loader' }) {
return ( return (
@@ -25,7 +25,8 @@ export default function LoaderWithImage({ size = 70, imgSize = 40, alt = 'loader
width: '100%', width: '100%',
height: '100%', height: '100%',
borderRadius: '50%', borderRadius: '50%',
objectFit: 'contain' objectFit: 'cover',
transform: 'scale(1.4)'
}} }}
/> />
</Box> </Box>

View File

@@ -42,7 +42,7 @@ MobileCardList.propTypes = {
// Card shell — coloured accent rail on the left, a header slot (status badge / // Card shell — coloured accent rail on the left, a header slot (status badge /
// title / action buttons), then any field grid / collapse content as children. // 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 <Paper
elevation={0} elevation={0}
onClick={onClick} 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 // `color` is a hex accent (defaults to brand purple); `caption` is the small
// muted line under the value (e.g. "96% of total"). // 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 ( return (
<Card sx={{ height: '100%' }}> <Card sx={{ height: '100%' }}>
<CardContent sx={{ p: { xs: 1.75, md: 2 }, '&:last-child': { pb: { xs: 1.75, md: 2 } } }}> <CardContent sx={{ p: { xs: 1.75, md: 2 }, '&:last-child': { pb: { xs: 1.75, md: 2 } } }}>

View File

@@ -73,7 +73,7 @@ export const initFirebaseNotificationListener = () => {
const registration = await navigator.serviceWorker.getRegistration(); const registration = await navigator.serviceWorker.getRegistration();
if (registration) { if (registration) {
registration.showNotification(notification.title || 'Nearle', { registration.showNotification(notification.title || 'Doormile', {
body: notification.body, body: notification.body,
icon: notification.image || '/favicon.ico', icon: notification.image || '/favicon.ico',
data, data,

View File

@@ -1,6 +1,6 @@
import logger from './utils/logger'; import logger from './utils/logger';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter, Link } from 'react-router-dom';
// third-party // third-party
import { Provider as ReduxProvider } from 'react-redux'; import { Provider as ReduxProvider } from 'react-redux';
@@ -8,6 +8,13 @@ import { Provider as ReduxProvider } from 'react-redux';
// scroll bar // scroll bar
import 'simplebar/dist/simplebar.css'; import 'simplebar/dist/simplebar.css';
// Astryx design system — zero-specificity :where() reset + component base
// styles, safe to load alongside MUI/Emotion (see themes/astryx.js for the
// Doormile brand theme applied per-page during migration).
import '@astryxdesign/core/reset.css';
import '@astryxdesign/core/astryx.css';
import { LinkProvider } from '@astryxdesign/core/Link';
// apex-chart // apex-chart
import 'assets/third-party/apex-chart.css'; import 'assets/third-party/apex-chart.css';
import 'assets/third-party/react-table.css'; import 'assets/third-party/react-table.css';
@@ -39,7 +46,7 @@ if (process.env.NODE_ENV !== 'development') {
console.warn = () => {}; // Optionally disable console.warn console.warn = () => {}; // Optionally disable console.warn
} }
logger.info('NearlExpress console application starting...'); logger.info('Doormile Express console application starting...');
// const root = ReactDOM.createRoot(document.getElementById('root')); // const root = ReactDOM.createRoot(document.getElementById('root'));
@@ -50,7 +57,13 @@ root.render(
<ReduxProvider store={store}> <ReduxProvider store={store}>
{/* <ConfigProvider> */}{' '} {/* <ConfigProvider> */}{' '}
<BrowserRouter> <BrowserRouter>
{/* Makes every Astryx nav/link component (SideNavItem, TopNavHeading,
DropdownMenu, ...) route through React Router's Link instead of a
hard <a> reload — see doormile_crm/src/main.jsx for the sibling
app's identical setup. */}
<LinkProvider component={Link}>
<App /> <App />
</LinkProvider>
</BrowserRouter> </BrowserRouter>
{/* </ConfigProvider> */} {/* </ConfigProvider> */}
</ReduxProvider> </ReduxProvider>

View File

@@ -0,0 +1,176 @@
import PropTypes from 'prop-types';
import { useLocation } from 'react-router-dom';
import { useIntl } from 'react-intl';
import { SideNav, SideNavSection, SideNavItem } from '@astryxdesign/core/SideNav';
import { Icon } from '@astryxdesign/core/Icon';
import nearle from 'menu-items/nearle';
import { DT } from 'themes/dt/tokens';
// Thin wrapper around Astryx's SideNav, driven by menu-items/nearle.js.
// Pattern (controlled collapse from the parent, section grouping, the CSS
// override block for row spacing/selected-state styling) mirrors
// doormile_crm/src/layout/MainLayout/Sidebar.jsx — that sibling app's
// Astryx sidebar is the validated reference; only the accent colour changes
// (brand red DT.brand instead of their navy) and items come from the
// existing nearle.js menu config instead of a flat navItems array.
// react-router routing works via the app-root LinkProvider (see
// src/index.js) — no per-item Link bridging needed.
const AppSideNav = ({ isCollapsed, onCollapsedChange }) => {
const { pathname } = useLocation();
const intl = useIntl();
const renderMenuItem = (item) => {
const label = intl.formatMessage({ id: item.id });
// Wrap explicitly in Astryx's own <Icon> rather than passing the bare
// component and letting SideNavItem's renderIconSlot auto-detect it:
// that detection only matches plain functions/forwardRef objects, not
// React.memo-wrapped ones (which several @ant-design/icons v5 icons
// are) — those fell through and got returned as a raw, uninstantiated
// element, crashing with "Objects are not valid as a React child".
// Wrapping here always renders through Icon's own component-mode path,
// which handles any component type and gives every icon the same
// normalised size/colour regardless of source library.
const icon = item.icon ? <Icon icon={item.icon} /> : undefined;
if (item.type === 'collapse') {
const isChildSelected = item.children.some((child) => pathname.startsWith(child.url));
return (
<SideNavItem key={item.id} label={label} icon={icon} collapsible={{ defaultIsCollapsed: !isChildSelected }} isSelected={isChildSelected}>
{item.children.map(renderMenuItem)}
</SideNavItem>
);
}
return <SideNavItem key={item.id} label={label} icon={icon} href={item.url} isSelected={pathname.startsWith(item.url)} />;
};
return (
<>
<SideNav
className="doormile-side-nav"
collapsible={{ isCollapsed, onCollapsedChange, buttonLabel: 'Collapse navigation' }}
style={{
backgroundColor: DT.surface,
borderRight: `1px solid ${DT.borderSubtle}`,
boxShadow: '1px 0 3px rgba(15, 23, 42, 0.03)',
paddingBlock: '12px',
paddingInline: '8px',
boxSizing: 'border-box',
'--spacing-12': '72px',
// Pages with tall/independently-scrolling content (e.g. deliveries'
// internal TableContainer scroll region) still let the document
// scroll past the header, so pin the nav explicitly rather than
// relying only on AppShell's own auto-mode sticky wrapper.
position: 'sticky',
top: 'var(--appshell-header-height, 0px)',
height: 'calc(100dvh - var(--appshell-header-height, 0px))',
overflowY: 'auto'
}}
>
<SideNavSection title="Doormile">{nearle.children.map(renderMenuItem)}</SideNavSection>
</SideNav>
<style>{`
.doormile-side-nav .astryx-side-nav-section > div:last-child {
gap: 6px !important;
}
.doormile-side-nav .astryx-side-nav-item[aria-label] {
width: 40px;
height: 40px;
margin-inline: auto;
padding: 0 !important;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.15s ease;
}
.doormile-side-nav .astryx-side-nav-item[aria-label] > * {
margin: 0 !important;
}
.doormile-side-nav .astryx-side-nav-item[aria-label] .astryx-icon {
width: 18px !important;
height: 18px !important;
margin: 0 !important;
display: flex;
align-items: center;
justify-content: center;
}
.doormile-side-nav .astryx-side-nav-item[aria-label]:hover {
background-color: ${DT.brand}14 !important;
}
.doormile-side-nav .astryx-side-nav-item[aria-label]:focus-visible {
outline: 2px solid ${DT.brand}66;
outline-offset: 2px;
}
.doormile-side-nav .astryx-side-nav-item[aria-label][data-selected='selected'] {
background-color: ${DT.brand}1f !important;
}
.doormile-side-nav .astryx-side-nav-item[aria-label][data-selected='selected'] .astryx-icon {
color: ${DT.brand} !important;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label]) {
position: relative;
margin-inline: 2px;
height: auto;
padding-block: 12px !important;
transition: background-color 0.15s ease, transform 0.15s ease;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label])::before {
content: '';
position: absolute;
left: -2px;
top: 12px;
bottom: 12px;
width: 3px;
border-radius: 3px;
background-color: ${DT.brand};
opacity: 0;
transition: opacity 0.15s ease;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label]):hover {
background-color: ${DT.brand}0d !important;
transform: translateX(2px);
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label]):focus-visible {
outline: 2px solid ${DT.brand}66;
outline-offset: 2px;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label])[data-selected='selected'] {
background-color: ${DT.brand}14 !important;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label])[data-selected='selected']::before {
opacity: 1;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label])[data-selected='selected'] .astryx-icon {
color: ${DT.brand} !important;
}
.doormile-side-nav > div:last-child {
padding-block: 8px !important;
}
.doormile-side-nav button[aria-label*="sidebar"],
.doormile-side-nav button[aria-label*="navigation"] {
border-radius: 50% !important;
transition: background-color 0.15s ease !important;
}
.doormile-side-nav button[aria-label*="sidebar"]:hover,
.doormile-side-nav button[aria-label*="navigation"]:hover {
background-color: ${DT.brand}14 !important;
}
`}</style>
</>
);
};
AppSideNav.propTypes = {
isCollapsed: PropTypes.bool.isRequired,
onCollapsedChange: PropTypes.func.isRequired
};
export default AppSideNav;

View File

@@ -0,0 +1,199 @@
import PropTypes from 'prop-types';
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { useQueryClient } from '@tanstack/react-query';
import { TopNav } from '@astryxdesign/core/TopNav';
import { TopNavHeading } from '@astryxdesign/core/TopNav';
import { DropdownMenu } from '@astryxdesign/core/DropdownMenu';
import { Popover } from '@astryxdesign/core/Popover';
import { IconButton } from '@astryxdesign/core/IconButton';
import { Avatar } from '@astryxdesign/core/Avatar';
import { HStack } from '@astryxdesign/core/HStack';
import { VStack } from '@astryxdesign/core/VStack';
import { Text } from '@astryxdesign/core/Text';
import { Heading } from '@astryxdesign/core/Heading';
import { Divider } from '@astryxdesign/core/Divider';
import { TbBoxMultiple1 } from 'react-icons/tb';
import { GrMultiple } from 'react-icons/gr';
import { BellOutlined, WindowsOutlined, EditOutlined, CommentOutlined, LogoutOutlined, GiftOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons';
import avatar1 from 'assets/images/users/avatar-1.png';
import logo from 'assets/images/doormile-logo.png';
import navbarMark from 'assets/images/doormile-mark.png';
import { clearFcmToken } from 'store/reducers/fcmSlice';
import { logoutUser } from 'store/reducers/loginUserSlice';
import { performSessionLogout } from 'utils/session';
import { DT } from 'themes/dt/tokens';
// doormile-logo.png is a white asset; recolour to brand red for this
// light TopNav surface (same trick login.js uses for its white-card logo).
const logoStyle = {
height: 24,
width: 'auto',
filter: 'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)'
};
// Collapsed rail: doormile-mark.png (red-on-white disc) needs a white
// backing + scale(1.4) crop to fill its circle cleanly — same composition
// the old DrawerHeader used, just sized for the nav-bar row instead of the
// full-height sidebar rail.
const markWrapperStyle = {
width: 32,
height: 32,
borderRadius: '50%',
background: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
flexShrink: 0
};
const markStyle = {
width: '100%',
height: '100%',
objectFit: 'cover',
transform: 'scale(1.4)',
display: 'block'
};
const NotificationPanel = () => (
<VStack gap={0} padding={0} width={320}>
<HStack justify="between" vAlign="center" padding={2}>
<Heading level={5}>Notifications</Heading>
</HStack>
<Divider />
<VStack gap={0} padding={0}>
{[
{ icon: <GiftOutlined />, text: "It's Cristina danny's birthday today.", time: '2 min ago' },
{ icon: <MessageOutlined />, text: 'Aida Burg commented your post.', time: '5 August' },
{ icon: <SettingOutlined />, text: 'Your profile is 60% complete.', time: '7 hours ago' }
].map((n) => (
<HStack key={n.text} gap={1.5} vAlign="center" padding={2}>
<Avatar name={n.text} size="sm" />
<VStack gap={0} padding={0}>
<Text type="body">{n.text}</Text>
<Text type="supporting">{n.time}</Text>
</VStack>
</HStack>
))}
</VStack>
<Divider />
<HStack justify="center" padding={1.5}>
<Text type="label" color="accent">
View all
</Text>
</HStack>
</VStack>
);
// ==============================|| MAIN LAYOUT - TOP NAV ||============================== //
const AppTopNav = ({ isSidebarCollapsed }) => {
const navigate = useNavigate();
const dispatch = useDispatch();
const queryClient = useQueryClient();
const topNavRef = useRef(null);
const [headerHeight, setHeaderHeight] = useState(null);
const firstname = localStorage.getItem('firstname') || '';
const handleLogout = () => {
performSessionLogout({ queryClient, dispatch, clearFcmToken, logoutUser });
};
// AppShell (height="auto") pins the sticky side nav below the header using
// a --appshell-header-height CSS var it measures itself via ResizeObserver
// (AppShell.tsx). On some routes that measurement lags/settles wrong, so
// the side nav's sticky offset is off and its bottom-anchored collapse
// button visibly jumps on scroll. Re-measure the header independently here
// and force the same variable with !important on the shell root — an
// author-stylesheet !important rule beats AppShell's own non-important
// inline style.setProperty() call on that same element, so this always
// wins regardless of what the internal measurement produced.
useEffect(() => {
const el = topNavRef.current;
if (!el) return undefined;
const updateHeight = () => setHeaderHeight(el.getBoundingClientRect().height);
updateHeight();
const observer = new ResizeObserver(updateHeight);
observer.observe(el);
return () => observer.disconnect();
}, []);
return (
<>
<TopNav
ref={topNavRef}
label="Doormile Express"
style={{
backgroundColor: DT.surface,
borderBottom: `1px solid ${DT.borderSubtle}`,
boxShadow: DT.shadowMd
}}
heading={
<TopNavHeading
logo={
isSidebarCollapsed ? (
<HStack gap={1} vAlign="center" padding={0}>
<span style={markWrapperStyle}>
<img src={navbarMark} alt="" style={markStyle} />
</span>
<img src={logo} alt="Doormile Express" style={logoStyle} />
</HStack>
) : (
<img src={logo} alt="Doormile Express" style={logoStyle} />
)
}
headingHref="/doormile/dispatch"
/>
}
endContent={
<HStack gap={1} vAlign="center" padding={0}>
<DropdownMenu
hasChevron={false}
button={{ label: 'Quick create', icon: <WindowsOutlined />, isIconOnly: true, variant: 'ghost' }}
items={[
{ label: 'Create Order', icon: <TbBoxMultiple1 />, onClick: () => navigate('/doormile/orders/create') },
{ label: 'Create Multiple Order', icon: <GrMultiple />, onClick: () => navigate('/doormile/orders/createorders') }
]}
/>
<Popover placement="below" alignment="end" content={<NotificationPanel />} label="Notifications">
<IconButton label="Notifications" tooltip="Notifications" variant="ghost" icon={<BellOutlined />} />
</Popover>
<DropdownMenu
hasChevron={false}
button={{ label: firstname || 'Profile', icon: <Avatar src={avatar1} name={firstname} size="xsm" tooltip={false} />, variant: 'ghost' }}
items={[
{ label: 'View Profile', icon: <EditOutlined />, onClick: () => navigate('/viewprofile') },
{ label: 'Support Ticket', icon: <CommentOutlined /> },
{ type: 'divider' },
{ label: 'Logout', icon: <LogoutOutlined />, onClick: handleLogout }
]}
/>
</HStack>
}
/>
{headerHeight != null && (
<style>{`
.astryx-app-shell {
--appshell-header-height: ${headerHeight}px !important;
}
`}</style>
)}
</>
);
};
AppTopNav.propTypes = {
isSidebarCollapsed: PropTypes.bool
};
export default AppTopNav;

View File

@@ -1,32 +0,0 @@
// material-ui
import { Button, Link, CardMedia, Stack, Typography } from '@mui/material';
// project import
import MainCard from 'components/MainCard';
// assets
import avatar from 'assets/images/users/avatar-group.png';
import AnimateButton from 'components/@extended/AnimateButton';
// ==============================|| DRAWER CONTENT - NAVIGATION CARD ||============================== //
const NavCard = () => (
<MainCard sx={{ bgcolor: 'grey.50', m: 3 }}>
<Stack alignItems="center" spacing={2.5}>
<CardMedia component="img" image={avatar} />
<Stack alignItems="center">
<Typography variant="h5">Help?</Typography>
<Typography variant="h6" color="secondary">
Get to resolve query
</Typography>
</Stack>
<AnimateButton>
<Button variant="shadow" size="small" component={Link} href="https://codedthemes.support-hub.io/" target="_blank">
Support
</Button>
</AnimateButton>
</Stack>
</MainCard>
);
export default NavCard;

View File

@@ -1,503 +0,0 @@
import PropTypes from 'prop-types';
import React, { useEffect, useState, useMemo } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
// material-ui
import { styled, useTheme } from '@mui/material/styles';
import {
Box,
Collapse,
ClickAwayListener,
List,
ListItemButton,
ListItemIcon,
ListItemText,
Paper,
Popper,
Typography,
useMediaQuery
} from '@mui/material';
// project import
import NavItem from './NavItem';
import Dot from 'components/@extended/Dot';
import SimpleBar from 'components/third-party/SimpleBar';
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';
// assets
import { BorderOutlined, DownOutlined, UpOutlined, RightOutlined } from '@ant-design/icons';
// mini-menu - wrapper
const PopperStyled = styled(Popper)(({ theme }) => ({
overflow: 'visible',
zIndex: 1202,
minWidth: 180,
'&:before': {
content: '""',
display: 'block',
position: 'absolute',
top: 38,
left: -5,
width: 10,
height: 10,
backgroundColor: theme.palette.background.paper,
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 120,
borderLeft: `1px solid ${theme.palette.grey.A800}`,
borderBottom: `1px solid ${theme.palette.grey.A800}`
}
}));
// ==============================|| NAVIGATION - LIST COLLAPSE ||============================== //
const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, setSelectedLevel, selectedLevel }) => {
const theme = useTheme();
const downLG = useMediaQuery(theme.breakpoints.down('lg'));
const menuState = useSelector((state) => state.menu);
const { drawerOpen } = menuState;
const { menuOrientation } = useConfig();
const navigation = useNavigate();
const [open, setOpen] = useState(false);
const [selected, setSelected] = useState(null);
const [anchorEl, setAnchorEl] = useState(null);
const handleClick = (event) => {
setAnchorEl(null);
setSelectedLevel(level);
if (drawerOpen) {
setOpen(!open);
setSelected(!selected ? menu.id : null);
setSelectedItems(!selected ? menu.id : '');
if (menu.url) navigation(`${menu.url}`);
} else {
setAnchorEl(event?.currentTarget);
}
};
const handlerIconLink = () => {
if (!drawerOpen) {
if (menu.url) navigation(`${menu.url}`);
setSelected(menu.id);
}
};
const handleHover = (event) => {
setAnchorEl(event?.currentTarget);
if (!drawerOpen) {
setSelected(menu.id);
}
};
const miniMenuOpened = Boolean(anchorEl);
const handleClose = () => {
setOpen(false);
if (!miniMenuOpened) {
if (!menu.url) {
setSelected(null);
}
}
setAnchorEl(null);
};
useMemo(() => {
if (selected === selectedItems) {
if (level === 1) {
setOpen(true);
}
} else {
if (level === selectedLevel) {
setOpen(false);
if (!miniMenuOpened && !drawerOpen && !selected) {
setSelected(null);
}
if (drawerOpen) {
setSelected(null);
}
}
}
}, [selectedItems, level, selected, miniMenuOpened, drawerOpen, selectedLevel]);
const { pathname } = useLocation();
useEffect(() => {
if (pathname === menu.url) {
setSelected(menu.id);
}
// eslint-disable-next-line
}, [pathname]);
const checkOpenForParent = (child, id) => {
child.forEach((item) => {
if (item.url === pathname) {
setOpen(true);
setSelected(id);
}
});
};
useEffect(() => {
setOpen(false);
if (!miniMenuOpened) {
setSelected(null);
}
if (miniMenuOpened) setAnchorEl(null);
if (menu.children) {
menu.children.forEach((item) => {
if (item.children?.length) {
checkOpenForParent(item.children, menu.id);
}
if (pathname && pathname.includes('product-details')) {
if (item.url && item.url.includes('product-details')) {
setSelected(menu.id);
setOpen(true);
}
}
if (item.url === pathname) {
setSelected(menu.id);
setOpen(true);
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname, menu.children]);
useEffect(() => {
if (menu.url === pathname) {
dispatch(activeItem({ openItem: [menu.id] }));
setSelected(menu.id);
setAnchorEl(null);
setOpen(true);
}
}, [pathname, menu]);
const navCollapse = menu.children?.map((item) => {
switch (item.type) {
case 'collapse':
return (
<NavCollapse
key={item.id}
setSelectedItems={setSelectedItems}
setSelectedLevel={setSelectedLevel}
selectedLevel={selectedLevel}
selectedItems={selectedItems}
menu={item}
level={level + 1}
parentId={parentId}
/>
);
case 'item':
return <NavItem key={item.id} item={item} level={level + 1} />;
default:
return (
<Typography key={item.id} variant="h6" color="error" align="center">
Fix - Collapse or Item
</Typography>
);
}
});
const isSelected = selected === menu.id;
const borderIcon = level === 1 ? <BorderOutlined style={{ fontSize: '1rem' }} /> : false;
const Icon = menu.icon;
const menuIcon = menu.icon ? <Icon style={{ fontSize: drawerOpen ? '1rem' : '1.25rem', color: 'white' }} /> : borderIcon;
// const textColor = theme.palette.mode === ThemeMode.DARK ? 'grey.400' : 'text.primary';
// const iconSelectedColor = theme.palette.mode === ThemeMode.DARK && drawerOpen ? theme.palette.text.primary : theme.palette.primary.main;
const popperId = miniMenuOpened ? `collapse-pop-${menu.id}` : undefined;
const FlexBox = { display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' };
const textColor = 'white';
const iconSelectedColor = 'white';
// const isSelected = true;
return (
<>
{menuOrientation === MenuOrientation.VERTICAL || downLG ? (
<>
<ListItemButton
disableRipple
selected={selected === menu.id}
{...(!drawerOpen && { onMouseEnter: handleClick, onMouseLeave: handleClose })}
onClick={handleClick}
sx={{
pl: drawerOpen ? `${level * 28}px` : 1.5,
py: !drawerOpen && level === 1 ? 1.25 : 1,
...(drawerOpen && {
'&:hover': {
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter'
bgcolor: '#7b1fa2'
},
'&.Mui-selected': {
bgcolor: 'transparent',
color: iconSelectedColor,
'&:hover': { color: iconSelectedColor, bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'transparent' }
}
}),
...(!drawerOpen && {
'&:hover': {
bgcolor: 'transparent'
// bgcolor:'#7b1fa2'
},
'&.Mui-selected': {
'&:hover': {
bgcolor: 'transparent'
},
bgcolor: 'transparent'
}
})
}}
>
{menuIcon && (
<ListItemIcon
onClick={handlerIconLink}
sx={{
minWidth: 28,
// color: selected === menu.id ? 'primary.main' : textColor,
// color: selected === menu.id ? textColor : textColor,
// bgcolor:'white',
// color:'white',
...(!drawerOpen && {
borderRadius: 1.5,
width: 36,
height: 36,
alignItems: 'center',
justifyContent: 'center',
'&:hover': {
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'secondary.light' : 'secondary.lighter'
bgcolor: '#7b1fa2',
color: 'white'
}
}),
...(!drawerOpen &&
selected === menu.id && {
bgcolor: 'primary.light',
color: 'primary.main',
'&:hover': {
bgcolor: '#7b1fa2',
color: 'primary.main'
}
})
}}
>
{menuIcon}
</ListItemIcon>
)}
{(drawerOpen || (!drawerOpen && level !== 1)) && (
<ListItemText
primary={
<Typography
variant="h6"
// color={selected === menu.id ? 'primary' : textColor}
// color={'white'}
color={selected === menu.id ? textColor : textColor}
>
{menu.title}
</Typography>
}
secondary={
menu.caption && (
<Typography variant="caption" color="secondary">
{menu.caption}
</Typography>
)
}
/>
)}
{(drawerOpen || (!drawerOpen && level !== 1)) &&
(miniMenuOpened || open ? (
<UpOutlined
style={{
fontSize: '0.625rem',
marginLeft: 1,
// color: theme.palette.primary.main
color: 'white'
}}
/>
) : (
<DownOutlined
style={{
fontSize: '0.625rem',
marginLeft: 1,
color: 'white'
}}
/>
))}
{!drawerOpen && (
<PopperStyled
open={miniMenuOpened}
anchorEl={anchorEl}
placement="right-start"
style={{
zIndex: 2001
}}
popperOptions={{
modifiers: [
{
name: 'offset',
options: {
offset: [-12, 1]
}
}
]
}}
>
{({ TransitionProps }) => (
<Transitions in={miniMenuOpened} {...TransitionProps}>
<Paper
sx={{
overflow: 'hidden',
mt: 1.5,
boxShadow: theme.customShadows.z1,
backgroundImage: 'none',
border: `2px solid ${theme.palette.primary.main}`,
width: 'auto'
}}
>
<ClickAwayListener onClickAway={handleClose}>
<SimpleBar
sx={{
overflowX: 'hidden',
overflowY: 'auto',
maxHeight: 'calc(100vh - 170px)'
}}
>
{navCollapse}
</SimpleBar>
</ClickAwayListener>
</Paper>
</Transitions>
)}
</PopperStyled>
)}
</ListItemButton>
{drawerOpen && (
<Collapse in={open} timeout="auto" unmountOnExit>
<List sx={{ p: 0 }}>{navCollapse}</List>
</Collapse>
)}
</>
) : (
<>
<ListItemButton
id={`boundary-${popperId}`}
disableRipple
selected={isSelected}
onMouseEnter={handleHover}
onMouseLeave={handleClose}
onClick={handleHover}
aria-describedby={popperId}
sx={{
'&.Mui-selected': {
bgcolor: 'transparent'
}
}}
>
<Box onClick={handlerIconLink} sx={FlexBox}>
{menuIcon && (
<ListItemIcon
sx={{
my: 'auto',
minWidth: !menu.icon ? 18 : 36
// color: theme.palette.secondary.dark
// color:'white'
}}
>
{menuIcon}
</ListItemIcon>
)}
{!menuIcon && level !== 1 && (
<ListItemIcon
sx={{ my: 'auto', minWidth: !menu.icon ? 18 : 36, bgcolor: 'transparent', '&:hover': { bgcolor: 'transparent' } }}
>
<Dot size={4} color={isSelected ? 'primary' : 'secondary'} />
</ListItemIcon>
)}
<ListItemText
primary={
<Typography
variant="body1"
// color="inherit"
// color="white"
sx={{ my: 'auto' }}
>
{menu.title}
</Typography>
}
/>
{miniMenuOpened ? <RightOutlined /> : <DownOutlined />}
</Box>
{anchorEl && (
<PopperStyled
id={popperId}
open={miniMenuOpened}
anchorEl={anchorEl}
placement="right-start"
style={{
zIndex: 2001
}}
modifiers={[
{
name: 'offset',
options: {
offset: [-10, 0]
}
}
]}
>
{({ TransitionProps }) => (
<Transitions in={miniMenuOpened} {...TransitionProps}>
<Paper
sx={{
overflow: 'hidden',
mt: 1.5,
py: 0.5,
boxShadow: theme.shadows[8],
backgroundImage: 'none'
}}
>
<ClickAwayListener onClickAway={handleClose}>
<SimpleBar
sx={{
overflowX: 'hidden',
overflowY: 'auto',
maxHeight: 'calc(100vh - 170px)'
}}
>
{navCollapse}
</SimpleBar>
</ClickAwayListener>
</Paper>
</Transitions>
)}
</PopperStyled>
)}
</ListItemButton>
</>
)}
</>
);
};
NavCollapse.propTypes = {
menu: PropTypes.object,
level: PropTypes.number,
parentId: PropTypes.string,
setSelectedItems: PropTypes.func,
selectedItems: PropTypes.string,
setSelectedLevel: PropTypes.func,
selectedLevel: PropTypes.number
};
export default NavCollapse;

View File

@@ -1,343 +0,0 @@
import PropTypes from 'prop-types';
import { Fragment, useEffect, useState } from 'react';
import { useLocation } from 'react-router';
// material-ui
import { styled, useTheme } from '@mui/material/styles';
import {
Box,
ClickAwayListener,
List,
ListItemButton,
ListItemIcon,
ListItemText,
Paper,
Popper,
Typography,
useMediaQuery
} from '@mui/material';
// third-party
import { FormattedMessage } from 'react-intl';
// project import
import NavItem from './NavItem';
import NavCollapse from './NavCollapse';
import SimpleBar from 'components/third-party/SimpleBar';
import Transitions from 'components/@extended/Transitions';
import { MenuOrientation } from 'config';
import useConfig from 'hooks/useConfig';
import { dispatch, useSelector } from 'store';
import { activeID } from 'store/reducers/menu';
// assets
import { DownOutlined, RightOutlined } from '@ant-design/icons';
// ==============================|| NAVIGATION - LIST GROUP ||============================== //
const PopperStyled = styled(Popper)(({ theme }) => ({
overflow: 'visible',
zIndex: 1202,
minWidth: 180,
'&:before': {
content: '""',
display: 'block',
position: 'absolute',
top: 5,
left: 32,
width: 12,
height: 12,
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 120,
borderWidth: '6px',
borderStyle: 'solid',
borderColor: `${theme.palette.background.paper} transparent transparent ${theme.palette.background.paper}`
}
}));
const NavGroup = ({ item, lastItem, remItems, lastItemId, setSelectedItems, selectedItems, setSelectedLevel, selectedLevel }) => {
const theme = useTheme();
const { pathname } = useLocation();
const { menuOrientation } = useConfig();
const menu = useSelector((state) => state.menu);
const { drawerOpen, selectedID } = menu;
const downLG = useMediaQuery(theme.breakpoints.down('lg'));
const [anchorEl, setAnchorEl] = useState(null);
const [currentItem, setCurrentItem] = useState(item);
const openMini = Boolean(anchorEl);
useEffect(() => {
if (lastItem) {
if (item.id === lastItemId) {
const localItem = { ...item };
const elements = remItems.map((ele) => ele.elements);
localItem.children = elements.flat(1);
setCurrentItem(localItem);
} else {
setCurrentItem(item);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item, lastItem, downLG]);
const checkOpenForParent = (child, id) => {
child.forEach((ele) => {
if (ele.children?.length) {
checkOpenForParent(ele.children, currentItem.id);
}
if (ele.url === pathname) {
dispatch(activeID(id));
}
});
};
const checkSelectedOnload = (data) => {
const childrens = data.children ? data.children : [];
childrens.forEach((itemCheck) => {
if (itemCheck.children?.length) {
checkOpenForParent(itemCheck.children, currentItem.id);
}
if (itemCheck.url === pathname) {
dispatch(activeID(currentItem.id));
}
});
};
useEffect(() => {
checkSelectedOnload(currentItem);
if (openMini) setAnchorEl(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname, currentItem]);
const handleClick = (event) => {
if (!openMini) {
setAnchorEl(event?.currentTarget);
}
};
const handleClose = () => {
setAnchorEl(null);
};
const Icon = currentItem?.icon;
const itemIcon = currentItem?.icon ? (
<Icon
style={{
fontSize: 20,
stroke: '1.5',
color: selectedID === currentItem.id ? theme.palette.primary.main : theme.palette.secondary.dark
}}
/>
) : null;
const navCollapse = item.children?.map((menuItem) => {
switch (menuItem.type) {
case 'collapse':
return (
<NavCollapse
key={menuItem.id}
menu={menuItem}
setSelectedItems={setSelectedItems}
setSelectedLevel={setSelectedLevel}
selectedLevel={selectedLevel}
selectedItems={selectedItems}
level={1}
parentId={currentItem.id}
/>
);
case 'item':
return <NavItem key={menuItem.id} item={menuItem} level={1} />;
default:
}
});
const moreItems = remItems.map((itemRem, i) => (
<Fragment key={i}>
{itemRem.title && (
<Typography variant="caption" sx={{ pl: 2 }}>
{itemRem.title}
</Typography>
)}
{itemRem?.elements?.map((menu) => {
switch (menu.type) {
case 'collapse':
return (
<NavCollapse
key={menu.id}
menu={menu}
level={1}
parentId={currentItem.id}
setSelectedItems={setSelectedItems}
setSelectedLevel={setSelectedLevel}
selectedLevel={selectedLevel}
selectedItems={selectedItems}
/>
);
case 'item':
return <NavItem key={menu.id} item={menu} level={1} />;
default:
return (
<Typography key={menu.id} variant="h6" color="error" align="center">
Menu Items Error
</Typography>
);
}
})}
</Fragment>
));
// menu list collapse & items
const items = currentItem.children?.map((menu) => {
switch (menu.type) {
case 'collapse':
return (
<NavCollapse
key={menu.id}
menu={menu}
level={1}
parentId={currentItem.id}
setSelectedItems={setSelectedItems}
setSelectedLevel={setSelectedLevel}
selectedLevel={selectedLevel}
selectedItems={selectedItems}
/>
);
case 'item':
return <NavItem key={menu.id} item={menu} level={1} />;
default:
return (
<Typography key={menu.id} variant="h6" color="error" align="center">
Menu Items Error
</Typography>
);
}
});
const popperId = openMini ? `group-pop-${item.id}` : undefined;
return (
<>
{menuOrientation === MenuOrientation.VERTICAL || downLG ? (
<List
subheader={
item.title &&
drawerOpen && (
<Box sx={{ pl: 3, mb: 1.5 }}>
<Typography
variant="subtitle2"
sx={{ color: '#fff' }}
>
{item.title}
</Typography>
{item.caption && (
<Typography variant="caption" color="secondary">
{item.caption}
</Typography>
)}
</Box>
)
}
sx={{ mt: drawerOpen && item.title ? 1.5 : 0, py: 0, zIndex: 0 }}
>
{navCollapse}
</List>
) : (
<List>
<ListItemButton
selected={selectedID === currentItem.id}
sx={{
p: 1,
my: 0.5,
mr: 1,
display: 'flex',
alignItems: 'center',
backgroundColor: 'inherit',
'&.Mui-selected': {
bgcolor: 'transparent'
}
}}
onMouseEnter={handleClick}
onClick={handleClick}
onMouseLeave={handleClose}
aria-describedby={popperId}
>
{itemIcon && (
<ListItemIcon sx={{ minWidth: 28 }}>
{currentItem.id === lastItemId ? <DownOutlined style={{ fontSize: 20, stroke: '1.5' }} /> : itemIcon}
</ListItemIcon>
)}
<ListItemText
sx={{ mr: 1 }}
primary={
<Typography
variant="body1"
color={selectedID === currentItem.id ? theme.palette.primary.main : theme.palette.secondary.dark}
>
{currentItem.id === lastItemId ? <FormattedMessage id="More Items" /> : currentItem.title}
</Typography>
}
/>
{openMini ? (
<DownOutlined style={{ fontSize: 16, stroke: '1.5' }} />
) : (
<RightOutlined style={{ fontSize: 16, stroke: '1.5' }} />
)}
{anchorEl && (
<PopperStyled
id={popperId}
open={openMini}
anchorEl={anchorEl}
placement="bottom-start"
style={{
zIndex: 2001
}}
>
{({ TransitionProps }) => (
<Transitions in={openMini} {...TransitionProps}>
<Paper
sx={{
mt: 0.5,
py: 1.25,
boxShadow: theme.shadows[8],
backgroundImage: 'none'
}}
>
<ClickAwayListener onClickAway={handleClose}>
<SimpleBar
sx={{
overflowX: 'hidden',
overflowY: 'auto',
maxHeight: 'calc(100vh - 170px)'
}}
>
{currentItem.id !== lastItemId ? items : moreItems}
</SimpleBar>
</ClickAwayListener>
</Paper>
</Transitions>
)}
</PopperStyled>
)}
</ListItemButton>
</List>
)}
</>
);
};
NavGroup.propTypes = {
item: PropTypes.object,
lastItem: PropTypes.number,
remItems: PropTypes.array,
lastItemId: PropTypes.string,
setSelectedItems: PropTypes.func,
selectedItems: PropTypes.string,
setSelectedLevel: PropTypes.func,
selectedLevel: PropTypes.number
};
export default NavGroup;

View File

@@ -1,294 +0,0 @@
import PropTypes from 'prop-types';
import { forwardRef, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Tooltip, Typography, useMediaQuery } from '@mui/material';
// project import
import Dot from 'components/@extended/Dot';
import { MenuOrientation, ThemeMode } from 'config';
import useConfig from 'hooks/useConfig';
// import { dispatch, useSelector } from 'store';
import { activeItem, openDrawer, setSelectedMenu } from 'store/reducers/menu';
// ==============================|| NAVIGATION - LIST ITEM ||============================== //
const NavItem = ({ item, level }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { menuOrientation } = useConfig();
const { drawerOpen, openItem } = useSelector((state) => state.menu);
const downLG = useMediaQuery(theme.breakpoints.down('lg'));
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 Icon = item.icon;
const isSelected = openItem.findIndex((id) => id === item.id) > -1;
const itemIcon = item.icon ? (
<Icon style={{ fontSize: drawerOpen ? '1rem' : '1.25rem', color: isSelected ? '#662582' : '#fff' }} />
) : (
false
);
// const { pathname } = useLocation();
const pathname = document.location.pathname;
// active menu item on page load
useEffect(() => {
if (pathname && pathname.includes('product-details')) {
if (item.url && item.url.includes('product-details')) {
dispatch(activeItem({ openItem: [item.id] }));
}
}
if (pathname && pathname.includes('kanban')) {
if (item.url && item.url.includes('kanban')) {
dispatch(activeItem({ openItem: [item.id] }));
}
}
if (pathname.includes(item.url)) {
dispatch(activeItem({ openItem: [item.id] }));
}
// eslint-disable-next-line
}, [pathname]);
useEffect(() => {
dispatch(setSelectedMenu(pathname));
}, [pathname]);
const textColor = theme.palette.mode === ThemeMode.DARK ? 'grey.400' : '#fff';
const iconSelectedColor = theme.palette.mode === ThemeMode.DARK && drawerOpen ? 'text.primary' : 'primary.main';
return (
<>
{menuOrientation === MenuOrientation.VERTICAL || downLG ? (
<Tooltip
title={!drawerOpen && level === 1 ? item.title : ''}
placement="right"
arrow
disableInteractive
componentsProps={{
tooltip: { sx: { bgcolor: '#0f172a', fontSize: 12, fontWeight: 600, px: 1.25, py: 0.75, borderRadius: 1.5 } },
arrow: { sx: { color: '#0f172a' } }
}}
>
<ListItemButton
{...listItemProps}
disabled={item.disabled}
selected={isSelected}
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,
'&: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'
}
}),
...(!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>}
/>
)}
</ListItemButton>
</Tooltip>
) : (
<ListItemButton
{...listItemProps}
disabled={item.disabled}
selected={isSelected}
sx={{
zIndex: 1201,
...(drawerOpen && {
'&:hover': {
bgcolor: 'transparent'
},
'&.Mui-selected': {
bgcolor: 'transparent',
color: iconSelectedColor,
'&:hover': {
color: iconSelectedColor,
bgcolor: 'transparent'
}
}
}),
...(!drawerOpen && {
'&:hover': {
bgcolor: 'transparent'
},
'&.Mui-selected': {
'&:hover': {
bgcolor: 'transparent'
},
bgcolor: 'transparent'
}
})
}}
>
{itemIcon && (
<ListItemIcon
sx={{
minWidth: 36,
...(!drawerOpen && {
borderRadius: 1.5,
width: 36,
height: 36,
alignItems: 'center',
justifyContent: 'flex-start',
'&:hover': {
bgcolor: 'transparent'
}
}),
...(!drawerOpen &&
isSelected && {
bgcolor: 'transparent',
'&:hover': {
bgcolor: 'transparent'
}
})
}}
>
{itemIcon}
</ListItemIcon>
)}
{!itemIcon && (
<ListItemIcon
sx={{
color: isSelected ? 'primary.main' : 'secondary.main',
...(!drawerOpen && {
borderRadius: 1.5,
alignItems: 'center',
justifyContent: 'flex-start',
'&:hover': {
bgcolor: 'transparent'
}
}),
...(!drawerOpen &&
isSelected && {
bgcolor: 'transparent',
'&:hover': {
bgcolor: 'transparent'
}
})
}}
>
<Dot size={4} color={isSelected ? 'primary' : 'secondary'} />
</ListItemIcon>
)}
<ListItemText
primary={
<Typography variant="h6" color="inherit">
{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>
)}
</>
);
};
NavItem.propTypes = {
item: PropTypes.object,
level: PropTypes.number
};
export default NavItem;

View File

@@ -1,111 +0,0 @@
import { useEffect, useLayoutEffect, useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Box, Typography, useMediaQuery } from '@mui/material';
import { Menu } from 'menu-items/dashboard';
import { useSelector } from 'store';
import useConfig from 'hooks/useConfig';
import { HORIZONTAL_MAX_ITEM, MenuOrientation } from 'config';
// project import
import NavGroup from './NavGroup';
import menuItem from 'menu-items';
// ==============================|| DRAWER CONTENT - NAVIGATION ||============================== //
const Navigation = () => {
const theme = useTheme();
const downLG = useMediaQuery(theme.breakpoints.down('lg'));
const { menuOrientation } = useConfig();
const { drawerOpen } = useSelector((state) => state.menu);
const [selectedItems, setSelectedItems] = useState('');
const [selectedLevel, setSelectedLevel] = useState(0);
const [menuItems, setMenuItems] = useState({ items: [] });
useEffect(() => {
handlerMenuItem();
// eslint-disable-next-line
}, []);
let getMenu = Menu();
const handlerMenuItem = () => {
const isFound = menuItem.items.some((element) => {
if (element.id === 'group-dashboard') {
return true;
}
return false;
});
if (getMenu?.id !== undefined && !isFound) {
menuItem.items.splice(0, 0, getMenu);
setMenuItems(menuItem);
}
};
useLayoutEffect(() => {
setMenuItems(menuItem);
// eslint-disable-next-line
}, [menuItem]);
const isHorizontal = menuOrientation === MenuOrientation.HORIZONTAL && !downLG;
const lastItem = isHorizontal ? HORIZONTAL_MAX_ITEM : null;
let lastItemIndex = menuItems.items.length - 1;
let remItems = [];
let lastItemId;
// first it checks menu item is more than giving HORIZONTAL_MAX_ITEM after that get lastItemid by giving horizontal max
// item and it sets horizontal menu by giving horizontal max item lastly slice menuItem from array and set into remItems
if (lastItem && lastItem < menuItems.items.length) {
lastItemId = menuItems.items[lastItem - 1].id;
lastItemIndex = lastItem - 1;
remItems = menuItems.items.slice(lastItem - 1, menuItems.items.length).map((item) => ({
title: item.title,
elements: item.children,
icon: item.icon
}));
}
const navGroups = menuItems.items.slice(0, lastItemIndex + 1).map((item) => {
switch (item.type) {
case 'group':
return (
<NavGroup
key={item.id}
setSelectedItems={setSelectedItems}
setSelectedLevel={setSelectedLevel}
selectedLevel={selectedLevel}
selectedItems={selectedItems}
lastItem={lastItem}
remItems={remItems}
lastItemId={lastItemId}
item={item}
/>
);
default:
return (
<Typography key={item.id} variant="h6" color="error" align="center">
Fix - Navigation Group
</Typography>
);
}
});
return (
<Box
sx={{
pt: drawerOpen ? (isHorizontal ? 0 : 2) : 0,
'& > ul:first-of-type': { mt: 0 },
display: isHorizontal ? { xs: 'block', lg: 'flex' } : 'block'
}}
>
{navGroups}
</Box>
);
};
export default Navigation;

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

@@ -1,37 +0,0 @@
// material-ui
import // useMediaQuery,
// useTheme
'@mui/material';
// project import
// import NavCard from './NavCard';
import Navigation from './Navigation';
// import { useSelector } from 'store';
import SimpleBar from 'components/third-party/SimpleBar';
// ==============================|| DRAWER CONTENT ||============================== //
const DrawerContent = () => {
// const theme = useTheme();
// const matchDownMD = useMediaQuery(theme.breakpoints.down('lg'));
// const menu = useSelector((state) => state.menu);
// const { drawerOpen } = menu;
return (
<SimpleBar
sx={{
'& .simplebar-content': {
display: 'flex',
flexDirection: 'column'
}
}}
>
<Navigation />
{/* no need navCrd just hided */}
{/* {drawerOpen && !matchDownMD && <NavCard />} */}
</SimpleBar>
);
};
export default DrawerContent;

View File

@@ -1,22 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { styled } from '@mui/material/styles';
import { Box } from '@mui/material';
// ==============================|| DRAWER HEADER - STYLED ||============================== //
const DrawerHeaderStyled = styled(Box, { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
...theme.mixins.toolbar,
display: 'flex',
alignItems: 'center',
justifyContent: open ? 'flex-start' : 'center',
paddingLeft: theme.spacing(open ? 3 : 0)
}));
DrawerHeaderStyled.propTypes = {
theme: PropTypes.object,
open: PropTypes.bool
};
export default DrawerHeaderStyled;

View File

@@ -1,58 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { useTheme } from '@mui/material/styles';
import { useMediaQuery } from '@mui/material';
// project import
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'
// ==============================|| DRAWER HEADER ||============================== //
const DrawerHeader = ({ open }) => {
const theme = useTheme();
const downLG = useMediaQuery(theme.breakpoints.down('lg'));
const { menuOrientation } = useConfig();
const isHorizontal = menuOrientation === MenuOrientation.HORIZONTAL && !downLG;
return (
<DrawerHeaderStyled
theme={theme}
open={open}
sx={{
minHeight: isHorizontal ? 'unset' : '60px',
width: isHorizontal ? { xs: '100%', lg: '424px' } : 'inherit',
paddingTop: isHorizontal ? { xs: '10px', lg: '0' } : '8px',
paddingBottom: isHorizontal ? { xs: '18px', lg: '0' } : '8px',
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'/>
}
{(!open) &&
<img src={logo1}
width='40px'
alt='logo'/>
}
</DrawerHeaderStyled>
);
};
DrawerHeader.propTypes = {
open: PropTypes.bool
};
export default DrawerHeader;

View File

@@ -1,62 +0,0 @@
import React from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
import { AppBar, Box, Container, useScrollTrigger } from '@mui/material';
// project imports
import Navigation from './DrawerContent/Navigation';
import useConfig from 'hooks/useConfig';
// ==============================|| HORIZONTAL MENU LIST ||============================== //
function ElevationScroll({ children, window }) {
const theme = useTheme();
// Note that you normally won't need to set the window ref as useScrollTrigger
// will default to window.
// This is only being set here because the demo is in an iframe.
const trigger = useScrollTrigger({
disableHysteresis: true,
threshold: 0,
target: window
});
theme.shadows[4] = theme.customShadows.z1;
return React.cloneElement(children, {
elevation: trigger ? 4 : 0
});
}
// ==============================|| HORIZONTAL MENU LIST ||============================== //
const CustomAppBar = () => {
const theme = useTheme();
const { container } = useConfig();
return (
<ElevationScroll>
<AppBar
sx={{
top: 60,
bgcolor: theme.palette.background.paper,
width: '100%',
height: 62,
justifyContent: 'center',
borderTop: `1px solid ${theme.palette.divider}`,
borderBottom: `1px solid ${theme.palette.divider}`,
zIndex: 1098,
color: theme.palette.grey[500]
}}
>
<Container maxWidth={container ? 'xl' : false}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Navigation />
</Box>
</Container>
</AppBar>
</ElevationScroll>
);
};
export default CustomAppBar;

View File

@@ -1,51 +0,0 @@
// material-ui
import { styled } from '@mui/material/styles';
import Drawer from '@mui/material/Drawer';
// project import
import { DRAWER_WIDTH, ThemeMode } from 'config';
const openedMixin = (theme) => ({
width: DRAWER_WIDTH,
// borderRight: `1px solid ${theme.palette.divider}`,
borderRight: 'none',
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen
}),
overflowX: 'hidden',
boxShadow: theme.palette.mode === ThemeMode.DARK ? theme.customShadows.z1 : 'none',
backgroundColor:'#662582',
});
const closedMixin = (theme) => ({
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
// overflowX: 'hidden',
width: theme.spacing(7.5),
borderRight: 'none',
boxShadow: theme.customShadows.z1,
backgroundColor:'#662582',
});
// ==============================|| DRAWER - MINI STYLED ||============================== //
const MiniDrawerStyled = styled(Drawer, { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
width: DRAWER_WIDTH,
flexShrink: 0,
whiteSpace: 'nowrap',
boxSizing: 'border-box',
...(open && {
...openedMixin(theme),
'& .MuiDrawer-paper': openedMixin(theme)
}),
...(!open && {
...closedMixin(theme),
'& .MuiDrawer-paper': closedMixin(theme)
})
}));
export default MiniDrawerStyled;

View File

@@ -1,74 +0,0 @@
import PropTypes from 'prop-types';
import { useMemo } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Box, Drawer, useMediaQuery } from '@mui/material';
// project import
import DrawerHeader from './DrawerHeader';
import DrawerContent from './DrawerContent';
import MiniDrawerStyled from './MiniDrawerStyled';
import { DRAWER_WIDTH } from 'config';
import { dispatch, useSelector } from 'store';
import { openDrawer } from 'store/reducers/menu';
// ==============================|| MAIN LAYOUT - DRAWER ||============================== //
const MainDrawer = ({ window }) => {
const theme = useTheme();
const matchDownMD = useMediaQuery(theme.breakpoints.down('lg'));
const menu = useSelector((state) => state.menu);
const { drawerOpen } = menu;
// responsive drawer container
const container = window !== undefined ? () => window().document.body : undefined;
// header content
const drawerContent = useMemo(() => <DrawerContent />, []);
const drawerHeader = useMemo(() => <DrawerHeader open={drawerOpen} />, [drawerOpen]);
return (
<Box component="nav" sx={{ flexShrink: { md: 0 }, zIndex: 1200 }} aria-label="mailbox folders">
{!matchDownMD ? (
<MiniDrawerStyled
variant="permanent"
open={drawerOpen}
>
{drawerHeader}
{drawerContent}
</MiniDrawerStyled>
) : (
<Drawer
container={container}
variant="temporary"
open={drawerOpen}
onClose={() => dispatch(openDrawer(!drawerOpen))}
ModalProps={{ keepMounted: true }}
sx={{
display: { xs: 'block', lg: 'none' },
'& .MuiDrawer-paper': {
boxSizing: 'border-box',
width: DRAWER_WIDTH,
borderRight: `1px solid ${theme.palette.divider}`,
backgroundImage: 'none',
boxShadow: 'inherit',
bgcolor:'#662582'
}
}}
>
{drawerHeader}
{drawerContent}
</Drawer>
)}
</Box>
);
};
MainDrawer.propTypes = {
window: PropTypes.object
};
export default MainDrawer;

View File

@@ -1,35 +0,0 @@
import PropTypes from 'prop-types';
// material-ui
import { styled } from '@mui/material/styles';
import AppBar from '@mui/material/AppBar';
// project import
import { DRAWER_WIDTH } from 'config';
// ==============================|| HEADER - APP BAR STYLED ||============================== //
const AppBarStyled = styled(AppBar, { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
...(!open && {
width: `calc(100% - ${theme.spacing(7.5)})`
}),
...(open && {
marginLeft: DRAWER_WIDTH,
width: `calc(100% - ${DRAWER_WIDTH}px)`,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen
})
})
}));
AppBarStyled.propTypes = {
open: PropTypes.bool
};
export default AppBarStyled;

View File

@@ -1,298 +0,0 @@
import { useRef, useState } from 'react';
import { Link } from 'react-router-dom';
// material-ui
import { useTheme } from '@mui/material/styles';
import {
Button,
Box,
CardMedia,
ClickAwayListener,
Grid,
List,
ListItemButton,
ListItemIcon,
ListItemText,
ListSubheader,
Paper,
Popper,
Stack,
Typography
} from '@mui/material';
// project import
import MainCard from 'components/MainCard';
import Dot from 'components/@extended/Dot';
import IconButton from 'components/@extended/IconButton';
import Transitions from 'components/@extended/Transitions';
import { DRAWER_WIDTH, ThemeMode } from 'config';
// assets
import { ArrowRightOutlined, WindowsOutlined } from '@ant-design/icons';
import backgroundVector from 'assets/images/mega-menu/back.svg';
import imageChart from 'assets/images/mega-menu/chart.svg';
import AnimateButton from 'components/@extended/AnimateButton';
// ==============================|| HEADER CONTENT - MEGA MENU SECTION ||============================== //
const MegaMenuSection = () => {
const theme = useTheme();
const anchorRef = useRef(null);
const [open, setOpen] = useState(false);
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(false);
};
const iconBackColorOpen = theme.palette.mode === ThemeMode.DARK ? 'grey.200' : 'grey.300';
const iconBackColor = theme.palette.mode === ThemeMode.DARK ? 'background.default' : 'grey.100';
return (
<Box sx={{ flexShrink: 0, ml: 0.75 }}>
<IconButton
color="secondary"
variant="light"
sx={{ color: 'text.primary', bgcolor: open ? iconBackColorOpen : iconBackColor }}
aria-label="open profile"
ref={anchorRef}
aria-controls={open ? 'profile-grow' : undefined}
aria-haspopup="true"
// onClick={handleToggle}
>
<WindowsOutlined />
</IconButton>
<Popper
placement="bottom"
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
popperOptions={{
modifiers: [
{
name: 'offset',
options: {
offset: [-180, 9]
}
}
]
}}
>
{({ TransitionProps }) => (
<Transitions type="grow" position="top" in={open} {...TransitionProps}>
<Paper
sx={{
boxShadow: theme.customShadows.z1,
minWidth: 750,
width: {
md: `calc(100vw - 100px)`,
lg: `calc(100vw - ${DRAWER_WIDTH + 100}px)`,
xl: `calc(100vw - ${DRAWER_WIDTH + 140}px)`
},
maxWidth: 1024
}}
>
<ClickAwayListener onClickAway={handleClose}>
<MainCard elevation={0} border={false} content={false}>
<Grid container>
<Grid
item
md={4}
sx={{
background: `url(${backgroundVector}), linear-gradient(183.77deg, ${theme.palette.primary.main} 11.46%, ${theme.palette.primary[700]} 100.33%)`
}}
>
<Box sx={{ p: 4.5, pb: 3 }}>
<Stack sx={{ color: 'background.paper' }}>
<Typography variant="h2" sx={{ fontSize: '1.875rem', mb: 1 }}>
Explore Components
</Typography>
<Typography variant="h6">
Try our pre made component pages to check how it feels and suits as per your need.
</Typography>
<Stack direction="row" justifyContent="space-between" alignItems="flex-end" sx={{ mt: -1 }}>
<AnimateButton>
<Button
variant="contained"
color="secondary"
sx={{
bgcolor: 'background.paper',
color: 'text.primary',
'&:hover': { bgcolor: 'background.paper', color: 'text.primary' }
}}
endIcon={<ArrowRightOutlined />}
component={Link}
to="/components-overview/buttons"
target="_blank"
>
View All
</Button>
</AnimateButton>
<CardMedia component="img" src={imageChart} alt="Chart" sx={{ mr: -2.5, mb: -2.5, width: 124 }} />
</Stack>
</Stack>
</Box>
</Grid>
<Grid item md={8}>
<Box
sx={{
p: 4,
'& .MuiList-root': {
pb: 0
},
'& .MuiListSubheader-root': {
p: 0,
pb: 1.5
},
'& .MuiListItemButton-root': {
p: 0.5,
'&:hover': {
background: 'transparent',
'& .MuiTypography-root': {
color: 'primary.main'
}
}
}
}}
>
<Grid container spacing={6}>
<Grid item xs={4}>
<List
component="nav"
aria-labelledby="nested-list-user"
subheader={
<ListSubheader id="nested-list-user">
<Typography variant="subtitle1" color="textPrimary">
Authentication
</Typography>
</ListSubheader>
}
>
<ListItemButton disableRipple component={Link} target="_blank" to="/auth/login">
<ListItemIcon>
<Dot size={7} color="secondary" variant="outlined" />
</ListItemIcon>
<ListItemText primary="Login" />
</ListItemButton>
<ListItemButton disableRipple component={Link} target="_blank" to="/auth/register">
<ListItemIcon>
<Dot size={7} color="secondary" variant="outlined" />
</ListItemIcon>
<ListItemText primary="Register" />
</ListItemButton>
<ListItemButton disableRipple component={Link} target="_blank" to="/auth/reset-password">
<ListItemIcon>
<Dot size={7} color="secondary" variant="outlined" />
</ListItemIcon>
<ListItemText primary="Reset Password" />
</ListItemButton>
<ListItemButton disableRipple component={Link} target="_blank" to="/auth/forgot-password">
<ListItemIcon>
<Dot size={7} color="secondary" variant="outlined" />
</ListItemIcon>
<ListItemText primary="Forgot Password" />
</ListItemButton>
<ListItemButton disableRipple component={Link} target="_blank" to="/auth/code-verification">
<ListItemIcon>
<Dot size={7} color="secondary" variant="outlined" />
</ListItemIcon>
<ListItemText primary="Verification Code" />
</ListItemButton>
</List>
</Grid>
<Grid item xs={4}>
<List
component="nav"
aria-labelledby="nested-list-user"
subheader={
<ListSubheader id="nested-list-user">
<Typography variant="subtitle1" color="textPrimary">
Other Pages
</Typography>
</ListSubheader>
}
>
<ListItemButton disableRipple component={Link} target="_blank" to="/">
<ListItemIcon>
<Dot size={7} color="secondary" variant="outlined" />
</ListItemIcon>
<ListItemText primary="About us" />
</ListItemButton>
<ListItemButton disableRipple component={Link} target="_blank" to="/contact-us">
<ListItemIcon>
<Dot size={7} color="secondary" variant="outlined" />
</ListItemIcon>
<ListItemText primary="Contact us" />
</ListItemButton>
<ListItemButton disableRipple component={Link} to="/pricing">
<ListItemIcon>
<Dot size={7} color="secondary" variant="outlined" />
</ListItemIcon>
<ListItemText primary="Pricing" />
</ListItemButton>
<ListItemButton disableRipple component={Link} to="/apps/profiles/user/payment">
<ListItemIcon>
<Dot size={7} color="secondary" variant="outlined" />
</ListItemIcon>
<ListItemText primary="Payment" />
</ListItemButton>
<ListItemButton disableRipple component={Link} target="_blank" to="/maintenance/under-construction">
<ListItemIcon>
<Dot size={7} color="secondary" variant="outlined" />
</ListItemIcon>
<ListItemText primary="Construction" />
</ListItemButton>
<ListItemButton disableRipple component={Link} target="_blank" to="/maintenance/coming-soon">
<ListItemIcon>
<Dot size={7} color="secondary" variant="outlined" />
</ListItemIcon>
<ListItemText primary="Coming Soon" />
</ListItemButton>
</List>
</Grid>
<Grid item xs={4}>
<List
component="nav"
aria-labelledby="nested-list-user"
subheader={
<ListSubheader id="nested-list-user">
<Typography variant="subtitle1" color="textPrimary">
SAAS Pages
</Typography>
</ListSubheader>
}
>
<ListItemButton disableRipple component={Link} target="_blank" to="/maintenance/404">
<ListItemIcon>
<Dot size={7} color="secondary" variant="outlined" />
</ListItemIcon>
<ListItemText primary="404 Error" />
</ListItemButton>
<ListItemButton disableRipple component={Link} target="_blank" to="/">
<ListItemIcon>
<Dot size={7} color="secondary" variant="outlined" />
</ListItemIcon>
<ListItemText primary="Landing" />
</ListItemButton>
</List>
</Grid>
</Grid>
</Box>
</Grid>
</Grid>
</MainCard>
</ClickAwayListener>
</Paper>
</Transitions>
)}
</Popper>
</Box>
);
};
export default MegaMenuSection;

View File

@@ -1,252 +0,0 @@
import { useRef, useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
import {
Avatar,
Box,
ClickAwayListener,
Divider,
List,
ListItemButton,
ListItemAvatar,
ListItemText,
ListItemSecondaryAction,
Paper,
Popper,
Typography,
useMediaQuery
} from '@mui/material';
// project import
import MainCard from 'components/MainCard';
import IconButton from 'components/@extended/IconButton';
import Transitions from 'components/@extended/Transitions';
import { ThemeMode } from 'config';
// assets
import avatar2 from 'assets/images/users/avatar-2.png';
import avatar3 from 'assets/images/users/avatar-3.png';
import avatar4 from 'assets/images/users/avatar-4.png';
import avatar5 from 'assets/images/users/avatar-5.png';
import { MailOutlined, CloseOutlined } from '@ant-design/icons';
// sx styles
const avatarSX = {
width: 48,
height: 48
};
const actionSX = {
mt: '6px',
ml: 1,
top: 'auto',
right: 'auto',
alignSelf: 'flex-start',
transform: 'none'
};
// ==============================|| HEADER CONTENT - MESSAGES ||============================== //
const Message = () => {
const theme = useTheme();
const matchesXs = useMediaQuery(theme.breakpoints.down('md'));
const anchorRef = useRef(null);
const [open, setOpen] = useState(false);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(false);
};
const iconBackColorOpen = theme.palette.mode === ThemeMode.DARK ? 'grey.200' : 'grey.300';
const iconBackColor = theme.palette.mode === ThemeMode.DARK ? 'background.default' : 'grey.100';
return (
<Box sx={{ flexShrink: 0, ml: 0.75 }}>
<IconButton
color="secondary"
variant="light"
sx={{ color: 'text.primary', bgcolor: open ? iconBackColorOpen : iconBackColor }}
aria-label="open profile"
ref={anchorRef}
aria-controls={open ? 'profile-grow' : undefined}
aria-haspopup="true"
onClick={handleToggle}
>
<MailOutlined />
</IconButton>
<Popper
placement={matchesXs ? 'bottom' : 'bottom-end'}
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
sx={{
maxHeight: 'calc(100vh - 250px)',
overflow: 'auto'
}}
popperOptions={{
modifiers: [
{
name: 'offset',
options: {
offset: [matchesXs ? -60 : 0, 9]
}
}
]
}}
>
{({ TransitionProps }) => (
<Transitions type="grow" position={matchesXs ? 'top' : 'top-right'} in={open} {...TransitionProps}>
<Paper
sx={{
boxShadow: theme.customShadows.z1,
width: '100%',
minWidth: 285,
maxWidth: 420,
[theme.breakpoints.down('md')]: {
maxWidth: 285
}
}}
>
<ClickAwayListener onClickAway={handleClose}>
<MainCard
title="Message"
elevation={0}
border={false}
content={false}
secondary={
<IconButton size="small" onClick={handleToggle}>
<CloseOutlined />
</IconButton>
}
>
<List
component="nav"
sx={{
p: 0,
'& .MuiListItemButton-root': {
py: 1.5,
'& .MuiAvatar-root': avatarSX,
'& .MuiListItemSecondaryAction-root': { ...actionSX, position: 'relative' }
}
}}
>
<ListItemButton>
<ListItemAvatar>
<Avatar alt="profile user" src={avatar2} />
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="h6">
It&apos;s{' '}
<Typography component="span" variant="subtitle1">
Cristina danny&apos;s
</Typography>{' '}
birthday today.
</Typography>
}
secondary="2 min ago"
/>
<ListItemSecondaryAction>
<Typography variant="caption" noWrap>
3:00 AM
</Typography>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton>
<ListItemAvatar>
<Avatar alt="profile user" src={avatar3} />
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="h6">
<Typography component="span" variant="subtitle1">
Aida Burg
</Typography>{' '}
commented your post.
</Typography>
}
secondary="5 August"
/>
<ListItemSecondaryAction>
<Typography variant="caption" noWrap>
6:00 PM
</Typography>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton>
<ListItemAvatar>
<Avatar alt="profile user" src={avatar4} />
</ListItemAvatar>
<ListItemText
primary={
<Typography component="span" variant="subtitle1">
There was a failure to your setup.
</Typography>
}
secondary="7 hours ago"
/>
<ListItemSecondaryAction>
<Typography variant="caption" noWrap>
2:45 PM
</Typography>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton>
<ListItemAvatar>
<Avatar alt="profile user" src={avatar5} />
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="h6">
<Typography component="span" variant="subtitle1">
Cristina Danny
</Typography>{' '}
invited to join{' '}
<Typography component="span" variant="subtitle1">
Meeting.
</Typography>
</Typography>
}
secondary="Daily scrum meeting time"
/>
<ListItemSecondaryAction>
<Typography variant="caption" noWrap>
9:10 PM
</Typography>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton sx={{ textAlign: 'center' }}>
<ListItemText
primary={
<Typography variant="h6" color="primary">
View All
</Typography>
}
/>
</ListItemButton>
</List>
</MainCard>
</ClickAwayListener>
</Paper>
</Transitions>
)}
</Popper>
</Box>
);
};
export default Message;

View File

@@ -1,104 +0,0 @@
import { useEffect, useRef, useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
import { AppBar, Box, ClickAwayListener, Paper, Popper, Toolbar } from '@mui/material';
// project import
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';
// ==============================|| HEADER CONTENT - MOBILE ||============================== //
const MobileSection = () => {
const theme = useTheme();
const [open, setOpen] = useState(false);
const anchorRef = useRef(null);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(false);
};
const prevOpen = useRef(open);
useEffect(() => {
if (prevOpen.current === true && open === false) {
anchorRef.current.focus();
}
prevOpen.current = open;
}, [open]);
return (
<>
<Box sx={{ flexShrink: 0, ml: 0.75 }}>
<IconButton
// sx={{ color: 'text.primary', bgcolor: open ? iconBackColorOpen : iconBackColor }}
sx={{ color: '#fff', bgcolor: 'transparent', ml: { xs: 0, lg: -2 },
fontSize:'25px',
':hover':{
color: '#fff', bgcolor: 'transparent'
} }}
aria-label="open more menu"
ref={anchorRef}
aria-controls={open ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={handleToggle}
color="secondary"
variant="light"
>
<MoreOutlined />
</IconButton>
</Box>
<Popper
placement="bottom-end"
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
style={{ width: '100%' }}
popperOptions={{
modifiers: [
{
name: 'offset',
options: {
offset: [0, 9]
}
}
]
}}
>
{({ TransitionProps }) => (
<Transitions type="fade" in={open} {...TransitionProps}>
<Paper sx={{ boxShadow: theme.customShadows.z1 }}>
<ClickAwayListener onClickAway={handleClose}>
<AppBar color="inherit">
<Toolbar>
{/* <Search /> */}
<Profile />
</Toolbar>
</AppBar>
</ClickAwayListener>
</Paper>
</Transitions>
)}
</Popper>
</>
);
};
export default MobileSection;

View File

@@ -1,303 +0,0 @@
import { useRef, useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
import {
Avatar,
Badge,
Box,
ClickAwayListener,
Divider,
List,
ListItemButton,
ListItemAvatar,
ListItemText,
ListItemSecondaryAction,
Paper,
Popper,
Tooltip,
Typography,
useMediaQuery
} from '@mui/material';
// project 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';
// sx styles
const avatarSX = {
width: 36,
height: 36,
fontSize: '1rem'
};
const actionSX = {
mt: '6px',
ml: 1,
top: 'auto',
right: 'auto',
alignSelf: 'flex-start',
transform: 'none'
};
// ==============================|| HEADER CONTENT - NOTIFICATION ||============================== //
const Notification = () => {
const theme = useTheme();
const matchesXs = useMediaQuery(theme.breakpoints.down('md'));
const anchorRef = useRef(null);
const [read, setRead] = useState(0);
const [open, setOpen] = useState(false);
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(false);
};
return (
<Box sx={{ flexShrink: 0, ml: 0.75 }}>
<Tooltip title='Notifications'>
<IconButton
// color="secondary"
// variant="light"
// sx={{ color: 'text.primary', bgcolor: open ? iconBackColorOpen : iconBackColor }}
sx={{ color: '#fff',
fontSize:'20px',
// bgcolor: open ? iconBackColorOpen : iconBackColor
':hover':{
bgcolor:'transparent',
color: '#fff',
},
// bgcolor:'transparent'
}}
aria-label="open profile"
ref={anchorRef}
aria-controls={open ? 'profile-grow' : undefined}
aria-haspopup="true"
// onClick={handleToggle}
>
<Badge badgeContent={read}
// color="primary"
sx={{
"& .MuiBadge-badge": {
color: "#662582",
backgroundColor: "white"
}
}}
>
<BellOutlined />
</Badge>
</IconButton>
</Tooltip>
<Popper
placement={matchesXs ? 'bottom' : 'bottom-end'}
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
popperOptions={{
modifiers: [
{
name: 'offset',
options: {
offset: [matchesXs ? -5 : 0, 9]
}
}
]
}}
>
{({ TransitionProps }) => (
<Transitions type="grow" position={matchesXs ? 'top' : 'top-right'} sx={{ overflow: 'hidden' }} in={open} {...TransitionProps}>
<Paper
sx={{
boxShadow: theme.customShadows.z1,
width: '100%',
minWidth: 285,
maxWidth: 420,
[theme.breakpoints.down('md')]: {
maxWidth: 285
}
}}
>
<ClickAwayListener onClickAway={handleClose}>
<MainCard
title="Notification"
elevation={0}
border={false}
content={false}
secondary={
<>
{read > 0 && (
<Tooltip title="Mark as all read">
<IconButton color="success" size="small" onClick={() => setRead(0)}>
<CheckCircleOutlined style={{ fontSize: '1.15rem' }} />
</IconButton>
</Tooltip>
)}
</>
}
>
<List
component="nav"
sx={{
p: 0,
'& .MuiListItemButton-root': {
py: 0.5,
'&.Mui-selected': { bgcolor: 'grey.50', color: 'text.primary' },
'& .MuiAvatar-root': avatarSX,
'& .MuiListItemSecondaryAction-root': { ...actionSX, position: 'relative' }
}
}}
>
<ListItemButton selected={read > 0}>
<ListItemAvatar>
<Avatar
sx={{
color: 'success.main',
bgcolor: 'success.lighter'
}}
>
<GiftOutlined />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="h6">
It&apos;s{' '}
<Typography component="span" variant="subtitle1">
Cristina danny&apos;s
</Typography>{' '}
birthday today.
</Typography>
}
secondary="2 min ago"
/>
<ListItemSecondaryAction>
<Typography variant="caption" noWrap>
3:00 AM
</Typography>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton selected={read > 0}>
<ListItemAvatar>
<Avatar
sx={{
color: 'primary.main',
bgcolor: 'primary.lighter'
}}
>
<MessageOutlined />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="h6">
<Typography component="span" variant="subtitle1">
Aida Burg
</Typography>{' '}
commented your post.
</Typography>
}
secondary="5 August"
/>
<ListItemSecondaryAction>
<Typography variant="caption" noWrap>
6:00 PM
</Typography>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton>
<ListItemAvatar>
<Avatar
sx={{
color: 'error.main',
bgcolor: 'error.lighter'
}}
>
<SettingOutlined />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="h6">
Your Profile is Complete &nbsp;
<Typography component="span" variant="subtitle1">
60%
</Typography>{' '}
</Typography>
}
secondary="7 hours ago"
/>
<ListItemSecondaryAction>
<Typography variant="caption" noWrap>
2:45 PM
</Typography>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton>
<ListItemAvatar>
<Avatar
sx={{
color: 'primary.main',
bgcolor: 'primary.lighter'
}}
>
C
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="h6">
<Typography component="span" variant="subtitle1">
Cristina Danny
</Typography>{' '}
invited to join{' '}
<Typography component="span" variant="subtitle1">
Meeting.
</Typography>
</Typography>
}
secondary="Daily scrum meeting time"
/>
<ListItemSecondaryAction>
<Typography variant="caption" noWrap>
9:10 PM
</Typography>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton sx={{ textAlign: 'center', py: `${12}px !important` }}>
<ListItemText
primary={
<Typography variant="h6" color="primary">
View All
</Typography>
}
/>
</ListItemButton>
</List>
</MainCard>
</ClickAwayListener>
</Paper>
</Transitions>
)}
</Popper>
</Box>
);
};
export default Notification;

View File

@@ -1,61 +0,0 @@
import PropTypes from 'prop-types';
import { useState } from 'react';
// material-ui
import { List, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
// assets
import { EditOutlined, LogoutOutlined, CommentOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router';
// ==============================|| HEADER PROFILE - PROFILE TAB ||============================== //
const ProfileTab = ({ handleLogout }) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const navigate = useNavigate();
const handleListItemClick = (event, index) => {
setSelectedIndex(index);
};
return (
<List component="nav" sx={{ p: 0, '& .MuiListItemIcon-root': { minWidth: 32 } }}>
<ListItemButton
selected={selectedIndex === 0}
onClick={(event) => {
handleListItemClick(event, 0);
navigate('/viewprofile');
}}
>
<ListItemIcon>
<EditOutlined />
</ListItemIcon>
<ListItemText primary="View Profile" />
</ListItemButton>
<ListItemButton selected={selectedIndex === 2} onClick={(event) => handleListItemClick(event, 1)}>
<ListItemIcon>
<CommentOutlined />
</ListItemIcon>
<ListItemText primary="Support Ticket" />
</ListItemButton>
{/* <ListItemButton selected={selectedIndex === 4} onClick={(event) => handleListItemClick(event, 4)}>
<ListItemIcon>
<WalletOutlined />
</ListItemIcon>
<ListItemText primary="Billing" />
</ListItemButton> */}
<ListItemButton selected={selectedIndex === 3} onClick={handleLogout}>
<ListItemIcon>
<LogoutOutlined />
</ListItemIcon>
<ListItemText primary="Logout" />
</ListItemButton>
</List>
);
};
ProfileTab.propTypes = {
handleLogout: PropTypes.func
};
export default ProfileTab;

View File

@@ -1,53 +0,0 @@
import { useState } from 'react';
// material-ui
import { List, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
// assets
import { CommentOutlined, LockOutlined, QuestionCircleOutlined, UserOutlined, UnorderedListOutlined } from '@ant-design/icons';
// ==============================|| HEADER PROFILE - SETTING TAB ||============================== //
const SettingTab = () => {
const [selectedIndex, setSelectedIndex] = useState(0);
const handleListItemClick = (event, index) => {
setSelectedIndex(index);
};
return (
<List component="nav" sx={{ p: 0, '& .MuiListItemIcon-root': { minWidth: 32 } }}>
<ListItemButton selected={selectedIndex === 0} onClick={(event) => handleListItemClick(event, 0)}>
<ListItemIcon>
<QuestionCircleOutlined />
</ListItemIcon>
<ListItemText primary="Support" />
</ListItemButton>
<ListItemButton selected={selectedIndex === 1} onClick={(event) => handleListItemClick(event, 1)}>
<ListItemIcon>
<UserOutlined />
</ListItemIcon>
<ListItemText primary="Account Settings" />
</ListItemButton>
<ListItemButton selected={selectedIndex === 2} onClick={(event) => handleListItemClick(event, 2)}>
<ListItemIcon>
<LockOutlined />
</ListItemIcon>
<ListItemText primary="Privacy Center" />
</ListItemButton>
<ListItemButton selected={selectedIndex === 3} onClick={(event) => handleListItemClick(event, 3)}>
<ListItemIcon>
<CommentOutlined />
</ListItemIcon>
<ListItemText primary="Feedback" />
</ListItemButton>
<ListItemButton selected={selectedIndex === 4} onClick={(event) => handleListItemClick(event, 4)}>
<ListItemIcon>
<UnorderedListOutlined />
</ListItemIcon>
<ListItemText primary="History" />
</ListItemButton>
</List>
);
};
export default SettingTab;

View File

@@ -1,211 +0,0 @@
import PropTypes from 'prop-types';
import { useRef, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Box, ButtonBase, CardContent, ClickAwayListener, Grid, Paper, Popper, Stack, Tab, Tabs, Tooltip, Typography } from '@mui/material';
// project import
import ProfileTab from './ProfileTab';
import SettingTab from './SettingTab';
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
import avatar1 from 'assets/images/users/avatar-1.png';
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 }) {
return (
<div role="tabpanel" hidden={value !== index} id={`profile-tabpanel-${index}`} aria-labelledby={`profile-tab-${index}`} {...other}>
{value === index && children}
</div>
);
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.any.isRequired,
value: PropTypes.any.isRequired
};
function a11yProps(index) {
return {
id: `profile-tab-${index}`,
'aria-controls': `profile-tabpanel-${index}`
};
}
// ==============================|| HEADER CONTENT - PROFILE ||============================== //
const Profile = () => {
const theme = useTheme();
const dispatch = useDispatch();
const queryClient = useQueryClient();
const handleLogout = () => {
performSessionLogout({ queryClient, dispatch, clearFcmToken, logoutUser });
};
const anchorRef = useRef(null);
const [open, setOpen] = useState(false);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(false);
};
const [value, setValue] = useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<Box sx={{ flexShrink: 0, ml: 0.75 }}>
<Tooltip title="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}
aria-controls={open ? 'profile-grow' : undefined}
aria-haspopup="true"
onClick={handleToggle}
>
<Stack direction="row" spacing={2} alignItems="center" sx={{ p: 0.5 }}>
<Avatar alt="profile user" src={avatar1} size="xs" />
<Typography variant="subtitle1">{/* {user?.name} */}</Typography>
</Stack>
</ButtonBase>
</Tooltip>
<Popper
placement="bottom-end"
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
popperOptions={{
modifiers: [
{
name: 'offset',
options: {
offset: [0, 9]
}
}
]
}}
>
{({ TransitionProps }) => (
<Transitions type="grow" position="top-right" in={open} {...TransitionProps}>
<Paper
sx={{
boxShadow: theme.customShadows.z1,
width: 290,
minWidth: 240,
maxWidth: 290,
[theme.breakpoints.down('md')]: {
maxWidth: 250
}
}}
>
<ClickAwayListener onClickAway={handleClose}>
<MainCard elevation={0} border={false} content={false}>
<CardContent sx={{ px: 2.5, pt: 3 }}>
<Grid container justifyContent="space-between" alignItems="center">
<Grid item>
<Stack direction="row" spacing={1.25} alignItems="center">
<Avatar alt="profile user" src={avatar1} sx={{ width: 32, height: 32 }} />
<Stack>
<Typography variant="h6">
{/* {user?.name} */}
{localStorage.getItem('firstname') || ''}
</Typography>
<Typography variant="body2" color="textSecondary">
{/* UI/UX Designer */}
Partner
</Typography>
</Stack>
</Stack>
</Grid>
<Grid item>
<Tooltip title="Logout">
<IconButton size="large" sx={{ color: 'text.primary' }} onClick={handleLogout}>
<LogoutOutlined />
</IconButton>
</Tooltip>
</Grid>
</Grid>
</CardContent>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs variant="fullWidth" value={value} onChange={handleChange} aria-label="profile tabs">
<Tab
sx={{
display: 'flex',
flexDirection: 'row',
// justifyContent: 'center',
justifyContent: 'flex-start',
alignItems: 'center',
textTransform: 'capitalize'
}}
icon={<UserOutlined style={{ marginBottom: 0, marginRight: '10px' }} />}
label="Profile"
{...a11yProps(0)}
/>
{/* <Tab
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
textTransform: 'capitalize'
}}
icon={<SettingOutlined style={{ marginBottom: 0, marginRight: '10px' }} />}
label="Setting"
{...a11yProps(1)}
/> */}
</Tabs>
</Box>
<TabPanel value={value} index={0} dir={theme.direction}>
<ProfileTab handleLogout={handleLogout} />
</TabPanel>
<TabPanel value={value} index={1} dir={theme.direction}>
<SettingTab />
</TabPanel>
</MainCard>
</ClickAwayListener>
</Paper>
</Transitions>
)}
</Popper>
</Box>
);
};
export default Profile;

View File

@@ -1,30 +0,0 @@
// material-ui
import { Box, FormControl, InputAdornment, OutlinedInput } from '@mui/material';
// assets
import { SearchOutlined } from '@ant-design/icons';
// ==============================|| HEADER CONTENT - SEARCH ||============================== //
const Search = () => (
<Box sx={{ width: '100%', ml: { xs: 0, md: 1 } }}>
<FormControl sx={{ width: { xs: '100%', md: 224 } }}>
<OutlinedInput
size="small"
id="header-search"
startAdornment={
<InputAdornment position="start" sx={{ mr: -0.5 }}>
<SearchOutlined />
</InputAdornment>
}
aria-describedby="header-search-text"
inputProps={{
'aria-label': 'weight'
}}
placeholder="Ctrl + K"
/>
</FormControl>
</Box>
);
export default Search;

View File

@@ -1,292 +0,0 @@
import { useMemo, useState } from 'react';
// material-ui
import {
Box,
useMediaQuery,
Stack,
Tooltip,
IconButton,
Popper,
ClickAwayListener,
List,
ListItemButton,
ListItemText,
Grid,
ListItemIcon,
Typography
} from '@mui/material';
import { TbBoxMultiple1 } from 'react-icons/tb';
import { GrMultiple } from 'react-icons/gr';
import { TbUserEdit } from 'react-icons/tb';
import Transitions from 'components/@extended/Transitions';
// project import
import Profile from './Profile';
import Notification from './Notification';
import { useNavigate } from 'react-router';
import {
WindowsOutlined
} from '@ant-design/icons';
import { useTheme } from '@mui/material/styles';
// ==============================|| HEADER - CONTENT ||============================== //
const HeaderContent = () => {
const matchesXs = useMediaQuery((theme) => theme.breakpoints.down('md'));
const navigate = useNavigate();
const [open, setOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const theme = useTheme();
const handleToggle = (e) => {
setOpen(!open);
setAnchorEl(e.currentTarget);
};
const handleClickAway = () => {
setOpen(false);
};
return (
<>
{/* {!matchesXs && <Search />} */}
<Stack width="100%" direction="row" justifyContent="space-between" spacing={2} alignItems="center">
{/* {!matchesXs && megaMenu} */}
<Typography variant="h5" sx={{ ml: 2, color: '#fff' }} noWrap>
{localStorage.getItem('firstname') || ''}
</Typography>
{matchesXs && <Box sx={{ ml: 1 }} />}
<Stack direction={'row'} spacing={2}>
<Box sx={{ flexShrink: 0, ml: 0.75 }}>
<Tooltip title="Quick Menu" placement="left-start">
<IconButton
// color="secondary"
// variant="light"
// sx={{
// color: 'text.primary',
// bgcolor: open ? iconBackColorOpen : iconBackColor
// }}
sx={{
color: '#fff',
fontSize: '20px',
// bgcolor: open ? iconBackColorOpen : iconBackColor
bgcolor: 'transparent'
// border:'1px solid #fff'
}}
aria-label="open profile"
// ref={anchorRef}
// aria-controls={open ? 'profile-grow' : undefined}
aria-haspopup="true"
onClick={handleToggle}
>
<WindowsOutlined />
</IconButton>
</Tooltip>
<Popper
open={open}
placement="bottom"
anchorEl={anchorEl}
role={undefined}
// transition
disablePortal
popperOptions={{
modifiers: [
{
name: 'offset',
options: {
offset: 0
}
}
]
}}
sx={{
// backgroundColor:'white',
// border:1,
p: 0,
zIndex: 5000,
boxShadow: theme.customShadows.z1
}}
>
<Transitions type="grow" position="top" sx={{ overflow: 'hidden' }} in={open}>
{/* <Box sx={{
backgroundColor: 'white',
border: '1px solid #e0e0e0 !important',
borderRadius: 1,
}}> */}
<Box
sx={
{
// boxShadow: theme.customShadows.z1,
}
}
>
<ClickAwayListener onClickAway={handleClickAway}>
{/* <List disablePadding> */}
<List
component="nav"
sx={{
mt: 1.5,
p: 0,
width: '100%',
minWidth: 200,
maxWidth: 290,
bgcolor: theme.palette.background.paper,
borderRadius: 0.5,
[theme.breakpoints.down('md')]: {
maxWidth: 250
}
}}
>
<ListItemButton
selected={location.pathname === '/nearle/orders/create'}
onClick={() => {
// console.log(const location = useLocation();)
navigate('/nearle/orders/create');
handleClickAway();
}}
>
<ListItemText
primary={
<Grid container>
<ListItemIcon sx={{ mr: 1, fontSize: '20px' }}>
<TbBoxMultiple1 />
</ListItemIcon>
<Typography color="textPrimary">Create Order</Typography>
</Grid>
}
/>
</ListItemButton>
<ListItemButton
selected={location.pathname === '/nearle/orders/createorders'}
onClick={() => {
// console.log(const location = useLocation();)
navigate('/nearle/orders/createorders');
handleClickAway();
}}
>
<ListItemText
primary={
<Grid container>
<ListItemIcon sx={{ mr: 1, fontSize: '20px' }}>
<GrMultiple />
</ListItemIcon>
<Typography color="textPrimary">Create Multiple Order</Typography>
</Grid>
}
/>
</ListItemButton>
<ListItemButton
selected={location.pathname === '/nearle/customer/create'}
onClick={() => {
navigate('/nearle/customer/create');
handleClickAway();
}}
>
<ListItemText
primary={
<Grid container>
<ListItemIcon sx={{ mr: 1, fontSize: '20px' }}>
<TbUserEdit />
</ListItemIcon>
<Typography color="textPrimary">Create Customer</Typography>
</Grid>
}
/>
</ListItemButton>
{/* <ListItemButton
selected={location.pathname === '/clients/create'}
onClick={() => {
navigate('/clients/create')
handleClickAway()
}} >
<ListItemText
primary={
<Grid container>
<ListItemIcon sx={{ mr: 1, fontSize: '20px' }}>
<CustomerServiceOutlined />
</ListItemIcon>
<Typography color="textPrimary">Create Client</Typography>
</Grid>
}
/>
</ListItemButton> */}
{/* <ListItemButton
selected={location.pathname === '/riders/create'}
onClick={() => {
navigate('/riders/create')
handleClickAway()
}} >
<ListItemText
primary={
<Grid container>
<ListItemIcon sx={{ mr: 1, fontSize: '20px' }}>
<SportsMotorsportsOutlinedIcon />
</ListItemIcon>
<Typography color="textPrimary">Create Rider</Typography>
</Grid>
}
/>
</ListItemButton> */}
{/* <ListItem disablePadding>
<ListItemButton sx={{ p: 2 }} onClick={() => {
navigate('/create_order')
handleClickAway()
}}>
<ListItemIcon sx={{ mr: 1, fontSize: '20px' }}>
<MailOutlined />
</ListItemIcon>
<ListItemText primary="Create Order" />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton sx={{ p: 2 }} onClick={() => {
navigate('/create_client')
handleClickAway()
}}>
<ListItemIcon sx={{ mr: 1, fontSize: '20px' }}>
<CustomerServiceOutlined />
</ListItemIcon>
<ListItemText primary="Create Client" />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton sx={{ p: 2 }} onClick={() => {
navigate('/create_staff')
handleClickAway()
}}>
<ListItemIcon sx={{ mr: 1, fontSize: '20px' }}>
<UserOutlined />
</ListItemIcon>
<ListItemText primary="Create Staff" />
</ListItemButton>
</ListItem> */}
</List>
</ClickAwayListener>
{/* </Box> */}
</Box>
</Transitions>
</Popper>
</Box>
<Notification />
{/* <Message /> */}
{/* {!matchesXs && <Profile />}
{matchesXs && <MobileSection />} */}
<Tooltip title="Notifications">
<Profile />
</Tooltip>
</Stack>
</Stack>
</>
);
};
export default HeaderContent;

View File

@@ -1,86 +0,0 @@
import { useMemo } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
import { AppBar, Toolbar, useMediaQuery } from '@mui/material';
// project import
import AppBarStyled from './AppBarStyled';
import HeaderContent from './HeaderContent';
import IconButton from 'components/@extended/IconButton';
import { MenuOrientation, ThemeMode } from 'config';
import useConfig from 'hooks/useConfig';
import { dispatch, useSelector } from 'store';
import { openDrawer } from 'store/reducers/menu';
// assets
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
// ==============================|| MAIN LAYOUT - HEADER ||============================== //
const Header = () => {
const theme = useTheme();
const downLG = useMediaQuery(theme.breakpoints.down('lg'));
const { menuOrientation } = useConfig();
const menu = useSelector((state) => state.menu);
const { drawerOpen } = menu;
const isHorizontal = menuOrientation === MenuOrientation.HORIZONTAL && !downLG;
// header content
const headerContent = useMemo(() => <HeaderContent />, []);
// common header
const mainHeader = (
<Toolbar>
{!isHorizontal ? (
<IconButton
aria-label="open drawer"
onClick={() => dispatch(openDrawer(!drawerOpen))}
edge="start"
// color="secondary"
// variant="light"
// sx={{ color: 'text.primary', bgcolor: drawerOpen ? iconBackColorOpen : iconBackColor, ml: { xs: 0, lg: -2 } }}
sx={{ color: '#fff', bgcolor: 'transparent', ml: { xs: 0, lg: -2 },
fontSize:'25px',
':hover':{
color: '#fff', bgcolor: 'transparent'
} }}
>
{!drawerOpen ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</IconButton>
) : null}
{headerContent}
</Toolbar>
);
// app-bar params
const appBar = {
position: 'fixed',
color: 'inherit',
elevation: 0,
sx: {
borderBottom: `1px solid ${theme.palette.divider}`,
zIndex: 1200,
width: isHorizontal ? '100%' : drawerOpen ? 'calc(100% - 260px)' : { xs: '100%', lg: 'calc(100% - 60px)' },
// boxShadow: theme.customShadows.z1
bgcolor:'#662582'
}
};
return (
<>
{!downLG ? (
<AppBarStyled open={drawerOpen} {...appBar}>
{mainHeader}
</AppBarStyled>
) : (
<AppBar {...appBar}>{mainHeader}</AppBar>
)}
</>
);
};
export default Header;

View File

@@ -1,72 +1,58 @@
import { useEffect } from 'react'; import { useState } from 'react';
import { useSelector } from 'react-redux';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
// material-ui // Astryx design system — see themes/astryx.js and CLAUDE.md's Astryx CLI
import { useTheme } from '@mui/material/styles'; // section. This replaces the old MUI Drawer/Header/HorizontalBar shell;
import { useMediaQuery, Box, Container, Toolbar } from '@mui/material'; // horizontal menu orientation (config.menuOrientation) was dropped since it
// was never exposed as a user-facing toggle in this app — see AppSideNav.js.
// AppShell config (variant/height/contentPadding/mobileNav breakpoint) and
// the lifted collapse state mirror doormile_crm/src/layout/MainLayout/index.jsx
// — that sibling Doormile app's validated Astryx shell.
import { AppShell } from '@astryxdesign/core/AppShell';
import { Theme } from '@astryxdesign/core/theme';
import { doormileTheme } from 'themes/astryx';
// project import import AppTopNav from './AppTopNav';
import Drawer from './Drawer'; import AppSideNav from './AppSideNav';
import Header from './Header';
import HorizontalBar from './Drawer/HorizontalBar';
import { MenuOrientation } from 'config';
import useConfig from 'hooks/useConfig';
import { dispatch } from 'store';
import { openDrawer } from 'store/reducers/menu';
// ==============================|| MAIN LAYOUT ||============================== // // ==============================|| MAIN LAYOUT ||============================== //
const MainLayout = () => { const MainLayout = () => {
const theme = useTheme(); // Lifted here (rather than left as SideNav's own uncontrolled state) so the
const matchDownXL = useMediaQuery(theme.breakpoints.down('xl')); // TopNav logo can track the sidebar's collapse state. Defaults to collapsed
const downLG = useMediaQuery(theme.breakpoints.down('lg')); // on every load/reload (not persisted).
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(true);
const { drawerOpen } = useSelector((state) => state.menu);
const { container, miniDrawer, menuOrientation } = useConfig();
const isHorizontal = menuOrientation === MenuOrientation.HORIZONTAL && !downLG;
// set media wise responsive drawer
useEffect(() => {
if (!miniDrawer) {
// dispatch(openDrawer(!matchDownXL));
dispatch(openDrawer(false));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [matchDownXL]);
return ( return (
<Box sx={{ display: 'flex', width: '100%' }}> <Theme theme={doormileTheme} mode="light">
<Header /> {/* height="auto" (not doormile_crm's "fill"): "fill" scrolls the
{!isHorizontal ? <Drawer /> : <HorizontalBar />} content area in its own container, and CLAUDE.md's canonical
<Box useInfiniteQuery pattern observes a sentinel against the window's
component="main" viewport (IntersectionObserver root=null), which only fires
sx={{ correctly with normal document-level scroll. */}
width: isHorizontal ? '100%' : drawerOpen ? 'calc(100% - 260px)' : { xs: '100%', lg: 'calc(100% - 60px)' }, <AppShell
flexGrow: 1, variant="section"
p: { xs: 2, sm: 3 } height="auto"
}} contentPadding={0}
topNav={<AppTopNav isSidebarCollapsed={isSidebarCollapsed} />}
sideNav={<AppSideNav isCollapsed={isSidebarCollapsed} onCollapsedChange={setIsSidebarCollapsed} />}
mobileNav={{ breakpoint: 'lg' }}
> >
<Toolbar sx={{ mt: isHorizontal ? 8 : 'inherit' }} /> <div className="main-content-area" style={{ minHeight: '100%', boxSizing: 'border-box' }}>
<Container
// maxWidth={container ? 'xl' : false}
maxWidth
sx={{
...(container && { px: { xs: 0, sm: 2 } }),
position: 'relative',
minHeight: 'calc(100vh - 110px)',
display: 'flex',
flexDirection: 'column'
}}
>
{/* <Breadcrumbs navigation={navigation} title titleBottom card={false} divider={false} /> */}
<Outlet /> <Outlet />
{/* <Footer /> */} </div>
</Container> <style>{`
</Box> .main-content-area {
</Box> padding: 24px;
}
@media (max-width: 480px) {
.main-content-area {
padding: 16px;
}
}
`}</style>
</AppShell>
</Theme>
); );
}; };

View File

@@ -1,10 +1,5 @@
// third-party // third-party
import { FormattedMessage } from 'react-intl'; 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 // assets
import { import {
@@ -19,14 +14,15 @@ import {
StopOutlined, StopOutlined,
DashboardOutlined, DashboardOutlined,
ClockCircleOutlined, ClockCircleOutlined,
UserOutlined,
SettingOutlined, SettingOutlined,
TeamOutlined,
MailOutlined, MailOutlined,
ImportOutlined, ImportOutlined,
BarChartOutlined, BarChartOutlined,
MoneyCollectOutlined, FileDoneOutlined,
FileDoneOutlined PartitionOutlined,
CarOutlined,
UserOutlined,
ProfileOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
// icons // icons
@@ -42,24 +38,22 @@ const icons = {
DeploymentUnitOutlined, DeploymentUnitOutlined,
DashboardOutlined, DashboardOutlined,
ClockCircleOutlined, ClockCircleOutlined,
UserOutlined,
SettingOutlined, SettingOutlined,
TeamOutlined,
MailOutlined, MailOutlined,
ImportOutlined, ImportOutlined,
BarChartOutlined, BarChartOutlined,
ReceiptOutlinedIcon, FileDoneOutlined,
NearMeOutlinedIcon, PartitionOutlined,
DirectionsBikeOutlinedIcon, CarOutlined,
MopedOutlinedIcon, UserOutlined,
FileDoneOutlined ProfileOutlined
}; };
// ==============================|| MENU ITEMS - SUPPORT ||============================== // // ==============================|| MENU ITEMS - SUPPORT ||============================== //
const nearle = { const nearle = {
id: 'nearle_Pages', id: 'nearle_Pages',
title: <FormattedMessage id="Nearle" />, title: <FormattedMessage id="Doormile" />,
icon: icons.FileDoneOutlined, icon: icons.FileDoneOutlined,
type: 'group', type: 'group',
children: [ children: [
@@ -67,51 +61,29 @@ const nearle = {
id: 'dispatch', id: 'dispatch',
title: <FormattedMessage id="dispatch" />, title: <FormattedMessage id="dispatch" />,
type: 'item', type: 'item',
url: '/nearle/dispatch', url: '/doormile/dispatch',
icon: icons.DirectionsBikeOutlinedIcon icon: icons.PartitionOutlined
}, },
{ {
id: 'orders', id: 'orders',
title: <FormattedMessage id="orders" />, title: <FormattedMessage id="orders" />,
type: 'item', type: 'item',
url: '/nearle/orders', url: '/doormile/orders',
icon: icons.DashboardOutlined icon: icons.DashboardOutlined
}, },
{ {
id: 'deliveries', id: 'deliveries',
title: <FormattedMessage id="deliveries" />, title: <FormattedMessage id="deliveries" />,
type: 'item', type: 'item',
url: '/nearle/deliveries', url: '/doormile/deliveries',
icon: MopedOutlinedIcon icon: icons.CarOutlined
},
{
id: 'tenants',
title: <FormattedMessage id="tenants" />,
type: 'item',
url: '/nearle/tenants',
icon: icons.UserOutlined
},
{
id: 'pricing',
title: <FormattedMessage id="pricing" />,
type: 'item',
url: '/nearle/pricing',
icon: MoneyCollectOutlined
},
{
id: 'customers',
title: <FormattedMessage id="customers" />,
type: 'item',
url: '/nearle/customers',
icon: icons.TeamOutlined
}, },
{ {
id: 'riders', id: 'riders',
title: <FormattedMessage id="riders" />, title: <FormattedMessage id="riders" />,
type: 'item', type: 'item',
url: '/nearle/riders', url: '/doormile/riders',
icon: DirectionsBikeOutlinedIcon icon: icons.UserOutlined
}, },
{ {
id: 'reports', id: 'reports',
@@ -123,48 +95,24 @@ const nearle = {
id: 'reports', id: 'reports',
title: <FormattedMessage id="ordersummary" />, title: <FormattedMessage id="ordersummary" />,
type: 'item', type: 'item',
url: '/nearle/reports/orderssummary', url: '/doormile/reports/orderssummary',
icon: TbListDetails icon: icons.ProfileOutlined
}, },
{ {
id: 'ordersdetails', id: 'ordersdetails',
title: <FormattedMessage id="ordersdetails" />, title: <FormattedMessage id="ordersdetails" />,
type: 'item', type: 'item',
url: '/nearle/reports/ordersdetails', url: '/doormile/reports/ordersdetails',
icon: icons.DashboardOutlined icon: icons.DashboardOutlined
// target: true
}, },
{ {
id: 'riderssummary', id: 'riderssummary',
title: <FormattedMessage id="riderssummary" />, title: <FormattedMessage id="riderssummary" />,
type: 'item', type: 'item',
url: '/nearle/reports/riderssummary', url: '/doormile/reports/riderssummary',
icon: DirectionsBikeOutlinedIcon icon: icons.UserOutlined
// 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

@@ -233,7 +233,7 @@ export const notifyRider = async (riderToken) => {
const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, { const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, {
token: riderToken, token: riderToken,
notification: { notification: {
title: 'NearleXpress', title: 'DoormileXpress',
body: 'Orders have been placed for delivery. Kindly accept and process deliveries', body: 'Orders have been placed for delivery. Kindly accept and process deliveries',
sound: 'ring', sound: 'ring',
image: '' image: ''
@@ -271,12 +271,9 @@ export const cancelMultipleOrder = async (orderlist) => {
// ==============================|| fetchDeliveries (deliveries) ||============================== // // ==============================|| fetchDeliveries (deliveries) ||============================== //
export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => { export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
let [, appId, userid, currentStatus, startdate, enddate, rowsPerPage, searchword, tenantid, locationid, riderid] = queryKey; let [, appId, , currentStatus, startdate, enddate, rowsPerPage, searchword, tenantid, locationid, riderid] = queryKey;
currentStatus = currentStatus == 'All' ? 'all' : currentStatus; currentStatus = currentStatus == 'All' ? 'all' : currentStatus;
const url = const url = `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?applocationid=${appId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
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(url);
return { return {
@@ -311,10 +308,7 @@ export const fetchPercentageAPI = async (appId) => {
// ==============================|| fetchCountAPI (deliveries) ||============================== // // ==============================|| fetchCountAPI (deliveries) ||============================== //
export const fetchCountAPI = async (appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid) => { export const fetchCountAPI = async (appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid) => {
const url = const url = `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
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 response = await axios.get(url);
const data = response.data.details; const data = response.data.details;
return { return {
@@ -369,103 +363,6 @@ export const updateDeliveryAPI = async (orderData) => {
return axios.put(`${process.env.REACT_APP_URL}/deliveries/updatedelivery`, orderData); return axios.put(`${process.env.REACT_APP_URL}/deliveries/updatedelivery`, orderData);
}; };
// ==============================|| getalltenants (tenants) ||============================== //
export const getalltenants = async ({ queryKey }) => {
const [, appId, 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
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
}
};
// ==============================|| gettenantsummary (tenants) ||============================== //
export const gettenantsummary = async ({ queryKey }) => {
const [, appId] = queryKey;
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantsummary/?moduleid=6&applocationid=${appId}`);
return response.data.summary; // return only data, keep it clean
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
}
};
// ==============================|| getpricinglist (tenants) ||============================== //
export const getpricinglist = async ({ queryKey }) => {
const [, appId] = queryKey;
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
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
}
};
// ==============================|| 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 [];
}
};
// ==============================|| getcustomersummary (customers) ||============================== //
export const getcustomersummary = async ({ queryKey }) => {
const [, appId] = queryKey;
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/customers/getcustomersummary?applocationid=${appId}`);
return response.data.summary;
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
return null; // return null for failure
}
};
// ==============================|| getallcustomers (customers) ||============================== //
export const getallcustomers = async ({ pageParam = 1, queryKey }) => {
const [, appId, debouncedSearch, rowsPerPage] = queryKey;
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/customers/getallcustomers/`, {
params: {
applocationid: appId,
keyword: debouncedSearch,
pageno: pageParam,
pagesize: rowsPerPage
}
});
return {
data: response.data.details || [],
nextPage: response.data.details?.length === rowsPerPage ? pageParam + 1 : undefined
};
} catch (err) {
const message = err.response?.data?.message || err.message || 'Something went wrong';
OpenToast(message);
throw err; // IMPORTANT for React Query
}
};
// ==============================|| fetchAllRiders (riders) ||============================== // // ==============================|| fetchAllRiders (riders) ||============================== //
export const fetchAllRiders = async ({ pageParam = 1, queryKey }) => { export const fetchAllRiders = async ({ pageParam = 1, queryKey }) => {
try { try {
@@ -554,10 +451,7 @@ export const fetchorderdetails = async ({ queryKey }) => {
const [appId, startdate, enddate, page, rowsPerPage] = queryKey; const [appId, startdate, enddate, page, rowsPerPage] = queryKey;
const response = await axios.get( const response = await axios.get(
appId == 0 `${process.env.REACT_APP_URL2}/orders/getorders/?fromdate=${startdate}&todate=${enddate}&applocationid=${appId}&pageno=${page}&pagesize=${rowsPerPage}`
? `${process.env.REACT_APP_URL2}/orders/getorders/?appuserid=${userid}&fromdate=${startdate}&todate=${enddate}&pageno=${page + 1
}&pagesize=${rowsPerPage}`
: `${process.env.REACT_APP_URL2}/orders/getorders/?fromdate=${startdate}&todate=${enddate}&applocationid=${appId}&pageno=${page}&pagesize=${rowsPerPage}`
); );
const detailsWithSNo = response.data.details.map((item, index) => ({ const detailsWithSNo = response.data.details.map((item, index) => ({
...item, ...item,
@@ -621,21 +515,6 @@ export const fetchLocations = async () => {
return 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)||============================== // // ==============================|| fetchRidersLogs (RiderLogs)||============================== //
export const fetchRidersLogs = async ({ queryKey }) => { export const fetchRidersLogs = async ({ queryKey }) => {

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,602 +0,0 @@
import React, { useMemo, useRef, useState } from 'react';
import {
Avatar,
Box,
Grid,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
useMediaQuery,
useTheme
} from '@mui/material';
import {
MdLocalOffer,
MdMyLocation,
MdAttachMoney,
MdGroups,
MdPlace,
MdSpeed,
MdPriceCheck,
MdStraighten,
MdReceiptLong,
MdOutlineLocalOffer,
MdOutlineGroups,
MdOutlineAttachMoney,
MdOutlinePlace
} from 'react-icons/md';
import { useQuery } from '@tanstack/react-query';
import Loader from 'components/Loader';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import { getallpricing } from 'pages/api/api';
// ============================================================================
// Design tokens — shared with the deliveries / tenants / customers pages so every
// surface (header, KPI tiles, table, badges) speaks the same visual language.
// Keep this block in sync with customers.js / deliveries.js.
// ============================================================================
const DT = {
radiusPill: 999,
radiusCard: 14,
radiusField: 10,
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',
borderHover: '#cbd5e1',
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc',
brand: '#662582'
};
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 SoftPaper = (props) => (
<Paper
{...props}
sx={{
mt: 0.75,
borderRadius: 2,
boxShadow: DT.shadowPop,
border: '1px solid',
borderColor: 'divider',
overflow: 'hidden'
}}
/>
);
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 formatRupees = (value) =>
new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 2
}).format(Number(value) || 0);
const formatDecimal = (value) =>
new Intl.NumberFormat('en-IN', { minimumFractionDigits: 2 }).format(Number(value) || 0);
// Numeric table value — plain, strong, right-readable text. The old version
// wrapped every cell in a coloured bordered pill, which made the table read
// like a rainbow; corporate data tables keep figures as quiet typography and
// let the column header carry the meaning.
const MetricPill = ({ label }) => (
<Typography
component="span"
sx={{ fontSize: 13.5, fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums' }}
>
{label}
</Typography>
);
// Subtle neutral category chip (zone / slab) — one quiet style, muted icon.
const CategoryChip = ({ icon, label }) => (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 8,
bgcolor: DT.surfaceAlt,
border: `1px solid ${DT.borderSubtle}`,
color: DT.textPrimary,
fontSize: 12,
fontWeight: 600,
whiteSpace: 'nowrap'
}}
>
<Box component="span" sx={{ display: 'inline-flex', color: DT.textMuted }}>
{icon}
</Box>
{label}
</Box>
);
// ==============================|| Pricing page ||============================== //
const ClientsPricing = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const containerRef = useRef();
const [appId, setAppId] = useState(0);
const [locaName, setLocoName] = useState('All');
const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const {
data: pricing = [],
isLoading
} = useQuery({
queryKey: ['getallpricing', appId],
queryFn: getallpricing,
keepPreviousData: true
});
const rows = useMemo(() => {
if (!debouncedSearch) return pricing;
const q = debouncedSearch.toLowerCase().trim();
return pricing.filter((row) =>
[row.applocation, row.appname, row.slab, String(row.pricingid)]
.filter(Boolean)
.some((field) => String(field).toLowerCase().includes(q))
);
}, [pricing, debouncedSearch]);
const stats = useMemo(() => {
const total = pricing.length;
const tenants = new Set(pricing.map((r) => r.appname).filter(Boolean)).size;
const avgBase = total
? pricing.reduce((sum, r) => sum + (Number(r.baseprice) || 0), 0) / total
: 0;
return { total, tenants, avgBase };
}, [pricing]);
const KPI_META = [
{ key: 'total', label: 'Total Pricing Slabs', color: BRAND, icon: MdOutlineLocalOffer, value: stats.total },
{ key: 'tenants', label: 'Tenants Priced', color: '#0ea5e9', icon: MdOutlineGroups, value: stats.tenants },
{ key: 'avg', label: 'Avg Base Price', color: '#f59e0b', icon: MdOutlineAttachMoney, value: formatRupees(stats.avgBase) },
{ key: 'zone', label: 'Active Zone', color: '#10b981', icon: MdOutlinePlace, value: locaName || 'All Zones' }
];
return (
<>
{isLoading && <Loader />}
{/* ============================================= || Header || ============================================= */}
<PageHeader
title="Pricing"
subtitle={`Live · ${locaName || 'All Zones'}`}
live
action={
<LocationAutocomplete
locaName={locaName}
setAppId={setAppId}
setLocoName={setLocoName}
pill
accentColor={BRAND}
icon={<MdMyLocation size={14} />}
placeholder="Select Zone"
paperComponent={SoftPaper}
sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
/>
}
/>
{/* ============================================= || KPI Cards || ============================================= */}
<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.key} xs={6} sm={6} md={3}>
<StatCard title={item.label} value={item.value} icon={<Icon size={20} />} color={item.color} />
</Grid>
);
})}
</Grid>
{/* ============================================= || Search Header || ============================================= */}
<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={{ xs: 'column', sm: 'row' }}
alignItems={{ xs: 'stretch', sm: 'center' }}
justifyContent="space-between"
spacing={1.25}
>
<Stack direction="row" alignItems="center" spacing={1.25}>
<AccentAvatar color={BRAND} size={32}>
<MdLocalOffer size={18} />
</AccentAvatar>
<Stack>
<Typography
variant="caption"
sx={{ fontWeight: 800, color: DT.textSecondary, letterSpacing: 0.6, textTransform: 'uppercase' }}
>
Pricing Catalog
</Typography>
<Typography variant="body2" sx={{ color: DT.textPrimary, fontWeight: 700 }}>
{pricing.length} total · {rows.length} shown
</Typography>
</Stack>
</Stack>
<Box sx={{ width: { xs: '100%', sm: 280, lg: 340 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
<DebounceSearchBar
value={searchword}
onChange={setSearchword}
onDebouncedChange={setDebouncedSearch}
placeholder={`Search pricing (ctrl+k)`}
sx={{
m: 0,
width: '100%',
borderRadius: DT.radiusField + 'px',
bgcolor: DT.surface,
'& fieldset': { borderColor: DT.borderSubtle, borderWidth: 1 },
'&:hover fieldset': { borderColor: DT.borderHover },
'&.Mui-focused fieldset': { borderColor: BRAND, 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 ? (
rows.length === 0 && !isLoading ? (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6, px: 2 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdLocalOffer size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No pricing to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center' }}>
{searchword ? 'Try a different keyword.' : 'Pick a zone above to load the catalog.'}
</Typography>
</Stack>
) : (
<MobileCardList scroll>
{rows.map((row, index) => (
<MobileCard
key={row.pricingid || `${row.appname}-${index}`}
accent={BRAND}
header={
<Stack direction="row" alignItems="center" 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 sx={{ minWidth: 0 }}>
<Typography
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary }}
noWrap
>
{row.appname || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.pricingid}
</Typography>
</Stack>
</Stack>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted, flexShrink: 0 }}>
{String(index + 1).padStart(2, '0')}
</Typography>
</Stack>
}
>
<Stack direction="row" spacing={0.75} sx={{ mt: 1, flexWrap: 'wrap', gap: 0.75 }}>
{row.applocation ? (
<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
}}
>
<MdPlace size={12} /> {row.applocation}
</Box>
) : null}
<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
}}
>
<MdSpeed size={12} /> {row.slab || '—'}
</Box>
</Stack>
<MobileFieldGrid>
<MobileField label="Base Price">
<MetricPill
color={BRAND}
icon={<MdPriceCheck size={12} />}
label={formatRupees(row.baseprice)}
/>
</MobileField>
<MobileField label="Price / KM">
<MetricPill
color="#10b981"
icon={<MdAttachMoney size={12} />}
label={formatRupees(row.priceperkm)}
/>
</MobileField>
<MobileField label="Min KM">
<MetricPill
color="#f59e0b"
icon={<MdStraighten size={12} />}
label={`${formatDecimal(row.minkm)} km`}
/>
</MobileField>
<MobileField label="Max KM">
<MetricPill
color="#ef4444"
icon={<MdStraighten size={12} />}
label={`${formatDecimal(row.maxkm)} km`}
/>
</MobileField>
<MobileField label="Min Orders">
<Stack direction="row" alignItems="center" spacing={0.5}>
<MdReceiptLong size={14} color={DT.textMuted} />
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.minorder ?? '—'}
</Typography>
</Stack>
</MobileField>
</MobileFieldGrid>
</MobileCard>
))}
</MobileCardList>
)
) : (
<TableContainer
ref={containerRef}
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: 860, md: 1080 } }}>
<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>Tenant</TableCell>
<TableCell>Zone</TableCell>
<TableCell>Slab</TableCell>
<TableCell align="center">Base Price</TableCell>
<TableCell align="center">Min KM</TableCell>
<TableCell align="center">Price / KM</TableCell>
<TableCell align="center">Max KM</TableCell>
<TableCell align="center">Min Orders</TableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading && <OrdersTableSkeleton col={5} />}
{rows.length === 0 && !isLoading ? (
<TableRow>
<TableCell colSpan={9} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdLocalOffer size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No pricing to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{searchword ? 'Try a different keyword.' : 'Pick a zone above to load the catalog.'}
</Typography>
</Stack>
</TableCell>
</TableRow>
) : (
rows.map((row, index) => (
<TableRow
key={row.pricingid || `${row.appname}-${index}`}
sx={{
transition: 'background-color 0.15s',
'& td': {
borderBottom: `1px solid ${DT.divider}`,
py: { xs: 1, md: 1.5 },
px: { xs: 1, md: 2 }
},
'&:hover': { backgroundColor: DT.surfaceAlt }
}}
>
<TableCell>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(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>
<Typography
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
>
{row.appname || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.pricingid}
</Typography>
</Stack>
</Stack>
</TableCell>
<TableCell>
{row.applocation ? (
<CategoryChip icon={<MdPlace size={12} />} label={row.applocation} />
) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}></Typography>
)}
</TableCell>
<TableCell>
<CategoryChip icon={<MdSpeed size={12} />} label={row.slab || '—'} />
</TableCell>
<TableCell align="center">
<MetricPill
color={BRAND}
icon={<MdPriceCheck size={12} />}
label={formatRupees(row.baseprice)}
width={110}
/>
</TableCell>
<TableCell align="center">
<MetricPill
color="#f59e0b"
icon={<MdStraighten size={12} />}
label={`${formatDecimal(row.minkm)} km`}
width={90}
/>
</TableCell>
<TableCell align="center">
<MetricPill
color="#10b981"
icon={<MdAttachMoney size={12} />}
label={formatRupees(row.priceperkm)}
width={110}
/>
</TableCell>
<TableCell align="center">
<MetricPill
color="#ef4444"
icon={<MdStraighten size={12} />}
label={`${formatDecimal(row.maxkm)} km`}
width={90}
/>
</TableCell>
<TableCell align="center">
<Stack direction="row" alignItems="center" justifyContent="center" spacing={0.5}>
<MdReceiptLong size={14} color={DT.textMuted} />
<Typography
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
>
{row.minorder ?? '—'}
</Typography>
</Stack>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
)}
</Paper>
</>
);
};
export default ClientsPricing;

File diff suppressed because it is too large Load Diff

View File

@@ -1,545 +0,0 @@
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';
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);
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;

View File

@@ -2,7 +2,8 @@ import { useEffect, useState } from 'react';
// material-ui // material-ui
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { Box, Button, FormLabel, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery } from '@mui/material'; import { Avatar, Box, Button, FormLabel, Grid, InputLabel, MenuItem, Paper, Select, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
import { MdPersonAddAlt1 } from 'react-icons/md';
// third-party // third-party
// import { PatternFormat } from 'react-number-format'; // import { PatternFormat } from 'react-number-format';
@@ -15,6 +16,7 @@ import Loader from 'components/Loader';
import Geocode from 'react-geocode'; import Geocode from 'react-geocode';
import { enqueueSnackbar } from 'notistack'; import { enqueueSnackbar } from 'notistack';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { DT, tint } from 'themes/dt/tokens';
// import { setLocationType } from 'react-geocode'; // import { setLocationType } from 'react-geocode';
// const avatarImage = require.context('assets/images/users', true); // const avatarImage = require.context('assets/images/users', true);
@@ -145,12 +147,6 @@ const Createclient = () => {
}); });
}; };
useEffect(() => {
if (selectedImage) {
setAvatar(URL.createObjectURL(selectedImage));
}
}, [selectedImage]);
const { ref: materialRef } = usePlacesWidget({ const { ref: materialRef } = usePlacesWidget({
apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY, apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY,
onPlaceSelected: (place) => { onPlaceSelected: (place) => {
@@ -296,11 +292,28 @@ const Createclient = () => {
{loading && <Loader />} {loading && <Loader />}
<Grid item xs={12} sx={{ mb: 2 }}> <Grid item xs={12} sx={{ mb: 2 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center"> <Paper
sx={{
p: 2.5,
borderRadius: DT.radiusCard + 'px',
boxShadow: DT.shadowSoft,
border: '1px solid',
borderColor: DT.borderSubtle,
background: `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)`
}}
>
<Stack direction="row" spacing={1.5} alignItems="center">
<Avatar sx={{ width: 48, height: 48, bgcolor: DT.brand }}>
<MdPersonAddAlt1 size={22} />
</Avatar>
<Typography variant="h3">Create Client</Typography> <Typography variant="h3">Create Client</Typography>
</Stack> </Stack>
</Paper>
</Grid> </Grid>
<MainCard contentSX={{ p: { xs: 1.5, md: 3 } }}> <MainCard
contentSX={{ p: { xs: 1.5, md: 3 } }}
sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }}
>
<Grid container spacing={isMobile ? 2 : 3}> <Grid container spacing={isMobile ? 2 : 3}>
{/* <Grid item xs={12} sm={4} > {/* <Grid item xs={12} sm={4} >
<MainCard title="Personal Information" sx={{ height: '100%' }}> <MainCard title="Personal Information" sx={{ height: '100%' }}>

File diff suppressed because it is too large Load Diff

View File

@@ -78,7 +78,7 @@ import {
startOfMonth, startOfMonth,
startOfWeek startOfWeek
} from 'date-fns'; } from 'date-fns';
import { DateRangePicker } from 'mui-daterange-picker'; import { DateRangePicker } from 'components/nearle_components/DateRangePicker';
import * as React from 'react'; import * as React from 'react';
import Loader from 'components/Loader'; import Loader from 'components/Loader';
import { KeyboardArrowDownOutlined, KeyboardArrowUpOutlined } from '@mui/icons-material'; import { KeyboardArrowDownOutlined, KeyboardArrowUpOutlined } from '@mui/icons-material';
@@ -105,62 +105,11 @@ import StatCard from 'components/nearle_components/StatCard';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
// ============================================================================ // ============================================================================
// Design tokens — extracted from the polished "Batch" dropdown so every // Design tokens — shared across every DT-styled operator page so filters,
// surface on this page (filters, KPIs, tabs, status badges, dialogs) shares // KPIs, tabs, status badges, and dialogs stay visually consistent.
// the same visual language. All helpers take a color and emit MUI sx values. // See src/themes/dt/tokens.js for the canonical source.
// ============================================================================ // ============================================================================
const DT = { import { DT, a, tint, soft, ring, edge, pillFieldSx } from 'themes/dt/tokens';
radiusPill: 999,
radiusCard: 14,
radiusInner: 10,
radiusField: 10,
// Restrained, low-contrast elevation — corporate (Linear/Stripe), not flashy.
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',
borderHover: '#cbd5e1',
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc',
brand: '#662582'
};
// Quick alpha helpers (hex + percentage suffix). Mirrors the batch-dropdown
// pattern (`${color}08`, `${color}18`, `${color}55`, `${color}26`).
const a = (c, suffix) => `${c}${suffix}`;
const tint = (c) => a(c, '08'); // very subtle surface tint
const soft = (c) => a(c, '18'); // soft chip / avatar bg
const ring = (c) => a(c, '26'); // focus ring color
const edge = (c) => a(c, '55'); // resting border
// Pill input sx — used by every filter Autocomplete/TextField on the page.
// Accepts the accent color and returns sx for the outer TextField. Width is
// driven by parent flex/grid so this helper stays width-agnostic.
// Neutral, corporate filter field: white surface, hairline border, brand focus
// ring. Colour is no longer used to tint the whole control (that produced the
// "rainbow" filter bar) — accent now lives only in the small start-adornment
// icon, which aids scanning without flooding the surface.
const pillFieldSx = () => ({
cursor: 'pointer',
'& .MuiOutlinedInput-root': {
borderRadius: DT.radiusField + 'px',
bgcolor: DT.surface,
fontWeight: 600,
color: DT.textPrimary,
paddingRight: '8px',
cursor: 'pointer',
transition: 'border-color 0.15s, box-shadow 0.15s',
'& fieldset': { borderColor: DT.borderSubtle, borderWidth: 1 },
'&:hover fieldset': { borderColor: DT.borderHover },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(DT.brand)}` },
'&.Mui-focused fieldset': { borderColor: DT.brand, borderWidth: 1.5 }
},
'& .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.
const STATUS_META = { const STATUS_META = {
@@ -413,7 +362,7 @@ const Deliveries = () => {
const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, { const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, {
token: selectedRow.userfcmtoken, token: selectedRow.userfcmtoken,
notification: { notification: {
title: 'NearleXpress', title: 'DoormileXpress',
body: `${selectedRow.orderid} have been Cancelled`, body: `${selectedRow.orderid} have been Cancelled`,
sound: 'ring', sound: 'ring',
image: '' image: ''
@@ -912,7 +861,7 @@ const Deliveries = () => {
setLocoName={setLocoName} setLocoName={setLocoName}
setPage={setPage} setPage={setPage}
pill pill
accentColor="#662582" accentColor="#C01227"
icon={<MdMyLocation size={14} />} icon={<MdMyLocation size={14} />}
placeholder="Select Zone" placeholder="Select Zone"
paperComponent={SoftPaper} paperComponent={SoftPaper}
@@ -962,7 +911,7 @@ const Deliveries = () => {
}} }}
> >
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: { xs: 1, md: 1.5 }, color: DT.textSecondary }}> <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} /> <MdTune size={16} />
</Avatar> </Avatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.6, textTransform: 'uppercase', color: DT.textSecondary }}> <Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.6, textTransform: 'uppercase', color: DT.textSecondary }}>
@@ -1125,8 +1074,8 @@ const Deliveries = () => {
transition: 'border-color 0.15s, box-shadow 0.15s', transition: 'border-color 0.15s, box-shadow 0.15s',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' }, '&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` }, '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 } '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
}, },
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: '#94a3b8' } '& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: '#94a3b8' }
}} }}
@@ -1344,15 +1293,15 @@ const Deliveries = () => {
flexShrink: 0, flexShrink: 0,
cursor: 'pointer', cursor: 'pointer',
borderRadius: DT.radiusField + 'px', borderRadius: DT.radiusField + 'px',
border: `1px solid ${active ? meta.color : DT.borderSubtle}`, border: `1px solid ${active ? '#C01227' : DT.borderSubtle}`,
bgcolor: active ? meta.color : DT.surface, bgcolor: active ? '#C01227' : DT.surface,
color: active ? '#fff' : DT.textSecondary, color: active ? '#fff' : DT.textSecondary,
fontWeight: 600, fontWeight: 600,
boxShadow: 'none', boxShadow: 'none',
transition: 'background-color 0.15s, border-color 0.15s, color 0.15s', transition: 'background-color 0.15s, border-color 0.15s, color 0.15s',
'&:hover': { '&:hover': {
borderColor: active ? meta.color : DT.borderHover, borderColor: active ? '#C01227' : DT.borderHover,
bgcolor: active ? meta.color : DT.surfaceAlt bgcolor: active ? '#C01227' : DT.surfaceAlt
} }
}} }}
> >
@@ -1442,10 +1391,10 @@ const Deliveries = () => {
overflowX: 'auto', overflowX: 'auto',
'&::-webkit-scrollbar': { width: '10px', height: '10px', cursor: 'pointer' }, '&::-webkit-scrollbar': { width: '10px', height: '10px', cursor: 'pointer' },
'&::-webkit-scrollbar-thumb': { '&::-webkit-scrollbar-thumb': {
backgroundColor: edge('#662582'), backgroundColor: edge('#C01227'),
borderRadius: '8px', borderRadius: '8px',
cursor: 'pointer', cursor: 'pointer',
'&:hover': { backgroundColor: '#662582' } '&:hover': { backgroundColor: '#C01227' }
}, },
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt } '&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}} }}
@@ -1562,7 +1511,7 @@ const Deliveries = () => {
<IconButton <IconButton
size="small" size="small"
onClick={(e) => handleMenuOpen(e, row)} 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 /> <EditOutlined />
</IconButton> </IconButton>
@@ -1650,7 +1599,7 @@ const Deliveries = () => {
</MobileField> </MobileField>
<MobileField label="Step"> <MobileField label="Step">
{row.step ? ( {row.step ? (
<Box sx={{ ...chipSx('#662582'), minWidth: 30, fontWeight: 800 }}>{row.step}</Box> <Box sx={{ ...chipSx('#C01227'), minWidth: 30, fontWeight: 800 }}>{row.step}</Box>
) : ( ) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}></Typography> <Typography variant="caption" sx={{ color: DT.textMuted }}></Typography>
)} )}
@@ -1664,8 +1613,8 @@ const Deliveries = () => {
{isOpen && ( {isOpen && (
<Box sx={{ mt: 1.5, p: 1.25, borderRadius: 2, bgcolor: DT.surfaceAlt, border: `1px solid ${DT.divider}` }}> <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 }}> <Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
<AccentAvatar color="#662582" size={22}><MdInventory2 size={12} /></AccentAvatar> <AccentAvatar color="#C01227" size={22}><MdInventory2 size={12} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#662582' }}> <Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
Product Details Product Details
</Typography> </Typography>
</Stack> </Stack>
@@ -2065,9 +2014,9 @@ const Deliveries = () => {
height: 24, height: 24,
px: 0.875, px: 0.875,
borderRadius: 999, borderRadius: 999,
bgcolor: tint('#662582'), bgcolor: tint('#C01227'),
border: `1px solid ${edge('#662582')}`, border: `1px solid ${edge('#C01227')}`,
color: '#662582', color: '#C01227',
fontWeight: 800, fontWeight: 800,
fontSize: 11 fontSize: 11
}} }}
@@ -2133,10 +2082,10 @@ const Deliveries = () => {
onClick={(e) => handleMenuOpen(e, row)} onClick={(e) => handleMenuOpen(e, row)}
sx={{ sx={{
borderRadius: 999, borderRadius: 999,
bgcolor: tint('#662582'), bgcolor: tint('#C01227'),
color: '#662582', color: '#C01227',
border: `1px solid ${edge('#662582')}`, border: `1px solid ${edge('#C01227')}`,
'&:hover': { bgcolor: soft('#662582') } '&:hover': { bgcolor: soft('#C01227') }
}} }}
> >
<EditOutlined /> <EditOutlined />
@@ -2168,9 +2117,9 @@ const Deliveries = () => {
background: '#fff' background: '#fff'
}} }}
> >
<Stack direction="row" alignItems="center" spacing={1} sx={{ px: 2, py: 1.25, borderBottom: `1px solid ${DT.divider}`, bgcolor: tint('#662582') }}> <Stack direction="row" alignItems="center" spacing={1} sx={{ px: 2, py: 1.25, borderBottom: `1px solid ${DT.divider}`, bgcolor: tint('#C01227') }}>
<AccentAvatar color="#662582"><MdInventory2 size={14} /></AccentAvatar> <AccentAvatar color="#C01227"><MdInventory2 size={14} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#662582' }}> <Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
Product Details Product Details
</Typography> </Typography>
</Stack> </Stack>
@@ -2778,14 +2727,14 @@ const Deliveries = () => {
borderRadius: 2, borderRadius: 2,
bgcolor: '#fff', bgcolor: '#fff',
'& fieldset': { borderColor: DT.borderSubtle }, '& fieldset': { borderColor: DT.borderSubtle },
'&:hover fieldset': { borderColor: '#662582' }, '&:hover fieldset': { borderColor: '#C01227' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 2 }, '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` } '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
} }
}} }}
> >
{['pending','accepted','started','arrived','delivered','cancelled'].map((s) => { {['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; const Ic = m.icon;
return ( return (
<MenuItem key={s} value={s} sx={{ gap: 1 }}> <MenuItem key={s} value={s} sx={{ gap: 1 }}>
@@ -2812,9 +2761,9 @@ const Deliveries = () => {
borderRadius: 2, borderRadius: 2,
bgcolor: '#fff', bgcolor: '#fff',
'& fieldset': { borderColor: DT.borderSubtle }, '& fieldset': { borderColor: DT.borderSubtle },
'&:hover fieldset': { borderColor: '#662582' }, '&:hover fieldset': { borderColor: '#C01227' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 2 }, '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` } '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
} }
}} }}
/> />

View File

@@ -43,7 +43,7 @@ Dispatch.js uses `react-leaflet` for declarative tile/marker rendering, BUT a lo
## 3. The reconcile rule (re-stated because it's load-bearing) ## 3. The reconcile rule (re-stated because it's load-bearing)
After **any** manual edit on `/nearle/dispatch/preview` (drag-and-drop step reorder, swap rider, change delivery sequence), the page **must** call `POST routes.workolik.com/optimization/reconcile-steps` before `POST jupiter.nearle.app/deliveries/createdeliveries`. After **any** manual edit on `/doormile/dispatch/preview` (drag-and-drop step reorder, swap rider, change delivery sequence), the page **must** call `POST routes.workolik.com/optimization/reconcile-steps` before `POST jupiter.nearle.app/deliveries/createdeliveries`.
Skipping reconcile corrupts route sequences in the database. This is the single biggest production bug to avoid in this area. Skipping reconcile corrupts route sequences in the database. This is the single biggest production bug to avoid in this area.
@@ -74,7 +74,7 @@ The dispatch page renders 100+ markers and polylines on every render. Watch for
- Drag-and-drop uses `react-dnd` with `react-dnd-html5-backend`. Don't swap libraries. - Drag-and-drop uses `react-dnd` with `react-dnd-html5-backend`. Don't swap libraries.
- After every drop, debounce a call to `reconcileSteps` from `api.js`. Don't call it synchronously on every drag tick — the optimiser will rate-limit you. - After every drop, debounce a call to `reconcileSteps` from `api.js`. Don't call it synchronously on every drag tick — the optimiser will rate-limit you.
- The "Assign" button calls `finalCreatedeliveries` → triggers `notifyRider` for each rider in the payload → redirects to `/nearle/deliveries`. Don't reorder these three steps. - The "Assign" button calls `finalCreatedeliveries` → triggers `notifyRider` for each rider in the payload → redirects to `/doormile/deliveries`. Don't reorder these three steps.
--- ---

File diff suppressed because it is too large Load Diff

View File

@@ -80,7 +80,7 @@ import logger from '../../../utils/logger';
// emerald for the actual GPS trail (signals "live / real" data). Per-step // emerald for the actual GPS trail (signals "live / real" data). Per-step
// distinction in Combined view is carried by the numbered drop pins, which // distinction in Combined view is carried by the numbered drop pins, which
// keep STEP_PALETTE so the timeline link to a specific delivery survives. // 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 COMBINED_ACTUAL_COLOR = '#10b981';
const toNum = (v) => { const toNum = (v) => {
@@ -5729,7 +5729,7 @@ const Dispatch = ({
style={{ style={{
boxShadow: 'var(--shadow-lg)', boxShadow: 'var(--shadow-lg)',
background: compareOpen background: compareOpen
? 'linear-gradient(135deg, #662582, #9255AB)' ? 'linear-gradient(135deg, #C01227, #D25463)'
: '#fff', : '#fff',
marginLeft: 8, marginLeft: 8,
color: compareOpen ? '#fff' : undefined color: compareOpen ? '#fff' : undefined
@@ -6328,7 +6328,7 @@ const Dispatch = ({
<div className="da-section"> <div className="da-section">
<div className="da-hero-row"> <div className="da-hero-row">
<div className="da-hero-card"> <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 /> <MdOutlineInventory2 />
</div> </div>
<div className="da-hero-value">{analysisFormatNum(fleet.total_orders)}</div> <div className="da-hero-value">{analysisFormatNum(fleet.total_orders)}</div>
@@ -6809,7 +6809,7 @@ const Dispatch = ({
<div className="da-pos-modal-title-wrap"> <div className="da-pos-modal-title-wrap">
<div <div
className="da-pos-modal-avatar" 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 /> <MdTwoWheeler />
</div> </div>

View File

@@ -297,7 +297,7 @@ const Preview = () => {
// so a later reload / back-forward also bounces instead of re-using it. // so a later reload / back-forward also bounces instead of re-using it.
useEffect(() => { useEffect(() => {
if (!stateData.dispatchPreviewData) { if (!stateData.dispatchPreviewData) {
navigate('/nearle/orders', { replace: true }); navigate('/doormile/orders', { replace: true });
return; return;
} }
if (typeof window !== 'undefined' && window.history?.state) { if (typeof window !== 'undefined' && window.history?.state) {
@@ -414,7 +414,7 @@ const Preview = () => {
OpenToast('Delivery Created Successfully', 'success', 2000); OpenToast('Delivery Created Successfully', 'success', 2000);
setIsLoading(false); setIsLoading(false);
if (rider?.userfcmtoken) notifyRiderMutation.mutate(rider.userfcmtoken); if (rider?.userfcmtoken) notifyRiderMutation.mutate(rider.userfcmtoken);
navigate('/nearle/deliveries'); navigate('/doormile/deliveries');
}, },
onError: (error) => { onError: (error) => {
OpenToast(error.message, 'error', 4000); OpenToast(error.message, 'error', 4000);
@@ -566,7 +566,7 @@ const Preview = () => {
<Stack direction="row" alignItems="center" spacing={1}> <Stack direction="row" alignItems="center" spacing={1}>
<Tooltip title="Back to orders" placement="top"> <Tooltip title="Back to orders" placement="top">
<IconButton <IconButton
onClick={() => navigate('/nearle/orders')} onClick={() => navigate('/doormile/orders')}
sx={{ bgcolor: 'action.hover', '&:hover': { bgcolor: 'action.selected' } }} sx={{ bgcolor: 'action.hover', '&:hover': { bgcolor: 'action.selected' } }}
> >
<HiOutlineArrowLeft size={20} /> <HiOutlineArrowLeft size={20} />

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,45 +1,90 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { enqueueSnackbar, closeSnackbar } from 'notistack'; import { enqueueSnackbar } 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 { useTheme } from '@mui/material/styles';
import axios from 'axios'; import axios from 'axios';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import Loader from 'components/Loader'; import Loader from 'components/Loader';
import logo from 'assets/images/logo-nearle1.png'; import logo from 'assets/images/doormile-logo.png';
import expressImage from 'assets/images/express.png';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { OpenToast } from 'components/third-party/OpenToast'; import { OpenToast } from 'components/third-party/OpenToast';
import { closeGlobalToast, GlobalToast } from 'components/nearle_components/GlobalToast'; import { 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 { setLoginUser } from 'store/reducers/loginUserSlice';
import { markSessionStart } from 'utils/session'; import { markSessionStart } from 'utils/session';
import { DT } from 'themes/dt/tokens';
// Astryx design system — see themes/astryx.js for the Doormile brand theme
// and CLAUDE.md's <!-- ASTRYX:START --> block for the CLI workflow.
// NOTE: custom CSS (xstyle/stylex.create()) isn't wired up yet — see the
// comment in config-overrides.js. Everything below uses Astryx component
// props only; the brand gradient panel and the two logo images are plain
// native elements with inline `style` for that reason.
import { AppShell } from '@astryxdesign/core/AppShell';
import { Theme } from '@astryxdesign/core/theme';
import { HStack } from '@astryxdesign/core/HStack';
import { VStack } from '@astryxdesign/core/VStack';
import { Center } from '@astryxdesign/core/Center';
import { Card } from '@astryxdesign/core/Card';
import { Heading } from '@astryxdesign/core/Heading';
import { Text } from '@astryxdesign/core/Text';
import { TextInput } from '@astryxdesign/core/TextInput';
import { Button } from '@astryxdesign/core/Button';
import { Link } from '@astryxdesign/core/Link';
import { doormileTheme } from 'themes/astryx';
const brandPanelStyle = {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
position: 'relative',
overflow: 'hidden',
width: '46%',
height: '100vh',
color: '#fff',
padding: 48,
background: `linear-gradient(150deg, ${DT.brand} 0%, #D25463 100%)`
};
const logoLockupStyle = { position: 'absolute', top: 48, left: 48, maxHeight: 60 };
const bulletDotStyle = {
width: 22,
height: 22,
borderRadius: '50%',
backgroundColor: 'rgba(255, 255, 255, 0.18)',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 13,
fontWeight: 700,
flexShrink: 0
};
// doormile-logo.png is a white asset; recolour to brand red for this
// white-background card (the brand-panel logo stays white as-is).
const formLogoStyle = {
maxHeight: 48,
filter: 'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)'
};
const BULLETS = ['Real-time fleet visibility', 'AI-optimised dispatch routes', 'Tenant, pricing & invoice control'];
const Login = () => { const Login = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const fcmtoken = useSelector((state) => state.fcm); const fcmtoken = useSelector((state) => state.fcm);
const permission = useSelector((state) => state.fcm.permission);
const theme = useTheme();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
let navigate = useNavigate(); let navigate = useNavigate();
const [otp, setOtp] = useState(''); const [otp, setOtp] = useState('');
const [currentotp, setCurrentotp] = useState('');
const [userinfo, setUserinfo] = useState({}); const [userinfo, setUserinfo] = useState({});
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [passwordStatus, setPasswordStatus] = useState(0); const [passwordStatus, setPasswordStatus] = useState(0);
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const [isPassword, setIspassword] = useState(false); const [isPassword, setIspassword] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [userid, setUserid] = useState(0); const [userid, setUserid] = useState(0);
useEffect(() => { useEffect(() => {
if (localStorage.getItem('firstname')) { if (localStorage.getItem('firstname')) {
navigate('/nearle/dispatch'); navigate('/doormile/dispatch');
} }
}, []); }, []);
@@ -100,7 +145,7 @@ const Login = () => {
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken); localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
markSessionStart(); markSessionStart();
fetchAppLocations(userinfo.userid); fetchAppLocations(userinfo.userid);
navigate('/nearle/dispatch'); navigate('/doormile/dispatch');
} else { } else {
OpenToast(res.data.message, 'error', 3000); OpenToast(res.data.message, 'error', 3000);
} }
@@ -123,7 +168,7 @@ const Login = () => {
markSessionStart(); markSessionStart();
closeGlobalToast(); // to close the pin snackbar closeGlobalToast(); // to close the pin snackbar
navigate('/nearle/dispatch'); navigate('/doormile/dispatch');
}; };
const opentoast = (message) => { const opentoast = (message) => {
@@ -152,124 +197,7 @@ const Login = () => {
} }
}; };
return ( const handleSubmit = (e) => {
<Box sx={{ minHeight: '100vh', display: 'flex', bgcolor: '#f8fafc' }}>
{loading && <Loader />}
{/* ---- Left brand panel (hidden on small screens) ---- */}
<Box
sx={{
display: { xs: 'none', md: 'flex' },
position: 'relative',
overflow: 'hidden',
flexBasis: '46%',
flexDirection: 'column',
justifyContent: 'center',
color: '#fff',
p: 6,
background: 'linear-gradient(150deg, #4D1C61 0%, #662582 52%, #9255AB 100%)'
}}
>
{/* Logo at the top-left corner */}
<img
src={expressImage}
alt="Operate your dispatch"
style={{
position: 'absolute',
top: 48,
left: 48,
maxHeight: 60
}}
/>
{/* decorative light glows */}
<Box
sx={{
position: 'absolute',
top: -120,
right: -80,
width: 360,
height: 360,
borderRadius: '50%',
background: 'radial-gradient(circle, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0) 70%)'
}}
/>
<Box
sx={{
position: 'absolute',
bottom: -150,
left: -110,
width: 440,
height: 440,
borderRadius: '50%',
background: 'radial-gradient(circle, rgba(255,255,255,0.12) 0%, rgba(255,255,255,0) 70%)'
}}
/>
<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: 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 NearlExpress operator console.
</Typography>
<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 ---- */}
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', p: { xs: 2.5, sm: 4 } }}>
<Box sx={{ width: '100%', maxWidth: 420 }}>
<Card
sx={{
width: '100%',
borderRadius: 3,
border: `1px solid ${theme.palette.divider}`,
boxShadow: '0 14px 40px rgba(15, 23, 42, 0.10)',
p: { xs: 2.5, sm: 4 }
}}
>
{/* Logo */}
<Stack alignItems="center" mb={2.5}>
<img src={logo} alt="loginpagelogo" style={{ maxHeight: 48 }} />
</Stack>
{/* Title */}
<Typography variant="h3" textAlign="center" sx={{ fontWeight: 700, mb: 0.5 }}>
Welcome back
</Typography>
<Typography variant="body2" textAlign="center" sx={{ color: '#64748b', mb: 3 }}>
Sign in to the NearlExpress console
</Typography>
<CardContent sx={{ p: 0 }}>
<form
noValidate
onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
if (passwordStatus == 0) { if (passwordStatus == 0) {
loginsend(); loginsend();
@@ -285,112 +213,107 @@ const Login = () => {
} }
loginsend(); loginsend();
} }
// if (currentotp) { };
// if (currentotp == otp) {
// loginsuccessful(); return (
// fetchAppLocations(); <Theme theme={doormileTheme} mode="light">
// } else { <AppShell contentPadding={0}>
// opentoast('Invalid pin'); {loading && <Loader />}
// } <HStack gap={0} height="100vh" wrap="nowrap">
// } {/* ---- Left brand panel (plain element: see the note at the top of
}} this file about custom styling not going through Astryx yet) ---- */}
> <div style={brandPanelStyle}>
<Stack spacing={3}> <img src={logo} alt="Doormile" style={logoLockupStyle} />
{/* Email */}
<TextField <VStack gap={2} maxWidth={430}>
autoFocus <Heading level={1} color="inherit">
fullWidth Operate your dispatch, end to end.
</Heading>
<Text type="large" color="inherit">
Orders, AI route optimisation, live rider tracking and billing all in the Doormile Express operator console.
</Text>
<VStack gap={1.5} padding={0}>
{BULLETS.map((t) => (
<HStack key={t} gap={1.25} vAlign="center">
<span style={bulletDotStyle}></span>
<Text color="inherit">{t}</Text>
</HStack>
))}
</VStack>
</VStack>
</div>
{/* ---- Right form panel ---- */}
<Center axis="both" width="54%" height="100vh">
<VStack width="100%" maxWidth={420} gap={3} padding={3}>
<Card padding={4} elevation="low">
<VStack gap={0.5} hAlign="center" padding={0}>
<img src={logo} alt="Doormile" style={formLogoStyle} />
<Heading level={2}>Welcome back</Heading>
<Text type="supporting">Sign in to the Doormile Express console</Text>
</VStack>
<form noValidate onSubmit={handleSubmit}>
<VStack gap={3} padding={0}>
<TextInput
hasAutoFocus
label="E-mail Address" label="E-mail Address"
variant="outlined" type="email"
autoComplete="email" isRequired
required
value={username} value={username}
onChange={(e) => setUsername(e.target.value.toLocaleLowerCase())} onChange={(value) => setUsername(value.toLocaleLowerCase())}
InputProps={{ readOnly: passwordStatus }} isDisabled={!!passwordStatus}
/> />
{/* Setup Password */} {/* Setup Password */}
{passwordStatus == 1 && ( {passwordStatus == 1 && (
<Stack display={'flex'} flexDirection={'column'} spacing={3}> <VStack gap={3} padding={0}>
<Typography variant="h4" textAlign="start" mb={3}> <Heading level={4}>Setup Password</Heading>
Setup Password <TextInput
</Typography> hasAutoFocus
<TextField
autoFocus
fullWidth
label="Enter New Password" label="Enter New Password"
variant="outlined" type="password"
required isRequired
type={showPassword ? 'text' : 'password'}
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(value) => setPassword(value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/> />
<TextInput
<TextField
error={confirmPassword !== '' && password !== confirmPassword}
fullWidth
label="Re-Enter Password" label="Re-Enter Password"
variant="outlined" type="password"
required isRequired
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword} value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(value) => setConfirmPassword(value)}
InputProps={{ status={
endAdornment: ( confirmPassword !== '' && password !== confirmPassword
<InputAdornment position="end"> ? { type: 'error', message: 'Passwords do not match' }
<IconButton onClick={() => setShowConfirmPassword((prev) => !prev)} edge="end"> : undefined
{showConfirmPassword ? <VisibilityOff /> : <Visibility />} }
</IconButton>
</InputAdornment>
)
}}
/> />
</Stack> </VStack>
)} )}
{/* Enter Password */} {/* Enter Password */}
{passwordStatus == 2 && ( {passwordStatus == 2 && (
<Stack display={'flex'} flexDirection={'column'} spacing={3}> <VStack gap={3} padding={0}>
<Typography variant="h4" textAlign="start" mb={3}> <Heading level={4}>Enter Password</Heading>
Enter Password <TextInput
</Typography> hasAutoFocus
<TextField
autoFocus
fullWidth
label="Enter Password" label="Enter Password"
variant="outlined" type="password"
required isRequired
type={showPassword ? 'text' : 'password'}
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(value) => setPassword(value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/> />
</Stack> </VStack>
)} )}
{/* OTP */} {/* OTP / retry */}
{isPassword && ( {isPassword && (
<Stack spacing={1.5}> <VStack gap={1.5} padding={0}>
<Stack direction="row" justifyContent="space-between"> <HStack justify="between">
<FormLabel>Enter Password</FormLabel> <Text type="label">Enter Password</Text>
<Link <Link
variant="body2"
sx={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
setOtp(''); setOtp('');
loginsend(); loginsend();
@@ -398,74 +321,33 @@ const Login = () => {
> >
Retry Retry
</Link> </Link>
</Stack> </HStack>
<TextInput label="Password" type="password" value={password} onChange={(value) => setPassword(value)} isLabelHidden />
{/* <OtpInput </VStack>
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>
)} )}
{/* Submit */}
<AnimateButton> <Button label="Continue" type="submit" variant="primary" size="lg" width="100%" />
<Button fullWidth size="large" type="submit" variant="contained" color="primary"> </VStack>
Continue
</Button>
</AnimateButton>
</Stack>
</form> </form>
</CardContent>
</Card> </Card>
{/* footer */} {/* footer */}
<Stack direction="row" justifyContent="center" alignItems="center" flexWrap="wrap" useFlexGap spacing={2} sx={{ mt: 3 }}> <HStack justify="center" wrap="wrap" gap={2}>
<Typography <Link href="https://nearle.in" target="_blank" isExternalLink>
variant="caption"
component={Link}
href="https://nearle.in"
target="_blank"
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
>
&copy; All rights reserved &copy; All rights reserved
</Typography> </Link>
<Typography <Link href="https://nearle.in/terms" target="_blank" isExternalLink>
variant="caption"
component={Link}
href="https://nearle.in/terms"
target="_blank"
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
>
Terms and Conditions Terms and Conditions
</Typography> </Link>
<Typography <Link href="https://nearle.in/privacy" target="_blank" isExternalLink>
variant="caption"
component={Link}
href="https://nearle.in/privacy"
target="_blank"
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
>
Privacy Policy Privacy Policy
</Typography> </Link>
</Stack> </HStack>
</Box> </VStack>
</Box> </Center>
</Box> </HStack>
</AppShell>
</Theme>
); );
}; };

View File

@@ -22,7 +22,11 @@ import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery'; import useMediaQuery from '@mui/material/useMediaQuery';
import AnimateButton from 'components/@extended/AnimateButton'; 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; recolour it to brand red for this page's light background.
const DOORMILE_RED_FILTER =
'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)';
import axios from 'axios'; import axios from 'axios';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@@ -228,7 +232,7 @@ const Login = () => {
// sx={{ ml: 3, mt: 3 }} // sx={{ ml: 3, mt: 3 }}
sx={{ ml: { xs: 0, md: 3 }, mt: { xs: 3, md: 1 }, textAlign: { xs: 'center', md: 'left' } }} 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={{ height: 'auto', filter: DOORMILE_RED_FILTER }} />
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<Grid <Grid

View File

@@ -33,8 +33,8 @@ useMutation({
After the solver returns: After the solver returns:
1. Solver response → stored in the orders page state. 1. Solver response → stored in the orders page state.
2. Operator navigates to `OrdersPreview.js` (`/nearle/orders/preview`) for a first look. 2. Operator navigates to `OrdersPreview.js` (`/doormile/orders/preview`) for a first look.
3. From there → `/nearle/dispatch/preview` (`Preview.js` in the dispatch folder) for drag-and-drop adjustment. 3. From there → `/doormile/dispatch/preview` (`Preview.js` in the dispatch folder) for drag-and-drop adjustment.
4. `Preview.js` is the one that calls `finalCreatedeliveries` to commit. 4. `Preview.js` is the one that calls `finalCreatedeliveries` to commit.
Don't try to commit from `orders.js` or `OrdersPreview.js` — they are read-only / staging steps. The reconcile-then-commit dance only happens on the dispatch preview page (see `src/pages/nearle/dispatch/CLAUDE.md`). Don't try to commit from `orders.js` or `OrdersPreview.js` — they are read-only / staging steps. The reconcile-then-commit dance only happens on the dispatch preview page (see `src/pages/nearle/dispatch/CLAUDE.md`).

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import { import {
Autocomplete, Autocomplete,
Button, Button,
@@ -25,14 +26,14 @@ import { useLocation, useNavigate } from 'react-router-dom';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import MainCard from 'components/MainCard'; import MainCard from 'components/MainCard';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { fetchPaymentType, fetchRidersList, finalCreatedeliveries, notifyRider } from 'pages/api/api'; import { fetchPaymentType, fetchRidersList, finalCreatedeliveries, notifyRider } from '../../api/api';
import { OpenToast } from 'components/third-party/OpenToast'; import { OpenToast } from 'components/third-party/OpenToast';
import { useMutation, useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import Loader from 'components/Loader'; import Loader from 'components/Loader';
import CircularLoader from 'components/CircularLoader'; import CircularLoader from 'components/CircularLoader';
import { Empty } from 'antd'; import { Empty } from 'antd';
import HoverSocialCard from 'components/cards/statistics/HoverSocialCard'; 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 { MdDirectionsBike } from 'react-icons/md';
import { FaMapLocationDot } from 'react-icons/fa6'; import { FaMapLocationDot } from 'react-icons/fa6';
import { HiOutlineArrowLeft } from 'react-icons/hi'; import { HiOutlineArrowLeft } from 'react-icons/hi';
@@ -234,9 +235,7 @@ const OrdersPreview = () => {
const { const {
data: paymentModes = [], data: paymentModes = [],
isLoading: paymentModesLoading, isLoading: paymentModesLoading
isError: paymentModesError,
error: paymentModesErrorMessage
} = useQuery({ } = useQuery({
queryKey: ['paymentmodes'], queryKey: ['paymentmodes'],
queryFn: fetchPaymentType queryFn: fetchPaymentType
@@ -246,10 +245,7 @@ const OrdersPreview = () => {
const { const {
data: ridersList = [], data: ridersList = [],
isLoading: ridersListLoading, isLoading: ridersListLoading
isError: ridersListError,
error: ridersListErrorMessage,
refetch: ridersListRefetch
} = useQuery({ } = useQuery({
queryKey: ['ridersList', appId], // Unique key for caching & re-fetching queryKey: ['ridersList', appId], // Unique key for caching & re-fetching
queryFn: fetchRidersList, queryFn: fetchRidersList,
@@ -282,13 +278,13 @@ const OrdersPreview = () => {
onSuccess: (data, variables) => { onSuccess: (data, variables) => {
console.log('data', data); console.log('data', data);
console.log('varialbles', variables); console.log('varialbles', variables);
notifyRiderMutation.mutate(rider.userfcmtoken || riderToken); // Call notifyRider after success notifyRiderMutation.mutate(rider?.userfcmtoken || riderToken); // Call notifyRider after success
if (data.status == 'accepted') { if (data.status == 'accepted') {
OpenToast('Delivery Created Successfully', 'success', 2000); OpenToast('Delivery Created Successfully', 'success', 2000);
} }
setTimeout(() => { setTimeout(() => {
setIsLoading(false); setIsLoading(false);
navigate('/nearle/deliveries'); navigate('/nearle/orders');
}, 2000); }, 2000);
}, },
onError: (error) => { onError: (error) => {
@@ -581,11 +577,6 @@ const OrdersPreview = () => {
<TableCell> <TableCell>
<Typography> {index + 1}</Typography> <Typography> {index + 1}</Typography>
</TableCell> </TableCell>
{/* {aiMode == 1 && (
<TableCell>
<Chip color="primary" label={val.zone_name} />
</TableCell>
)} */}
<TableCell> <TableCell>
<Tooltip title={val.tenantaddress}> <Tooltip title={val.tenantaddress}>
<Typography variant="body1" noWrap> <Typography variant="body1" noWrap>
@@ -656,12 +647,6 @@ const OrdersPreview = () => {
</Stack> </Stack>
</TableCell> </TableCell>
<TableCell align="left">{val.ordernotes}</TableCell> <TableCell align="left">{val.ordernotes}</TableCell>
{/* {aiMode == 1 && (
<TableCell align="left">
<Typography sx={{ whiteSpace: 'nowrap' }}>{val.username}</Typography>
<Typography>ID : {val.userid}</Typography>
</TableCell>
)} */}
<TableCell align="center"> <TableCell align="center">
<Chip <Chip
size="small" size="small"
@@ -774,7 +759,7 @@ const OrdersPreview = () => {
disabled={aiMode === 0 && (!rider || !payment)} disabled={aiMode === 0 && (!rider || !payment)}
onClick={handleManualCreateDelivery} onClick={handleManualCreateDelivery}
> >
Assign Orders Finalise
</Button> </Button>
</Stack> </Stack>
</MainCard> </MainCard>

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import { TableRow, TableCell, Skeleton, Stack } from '@mui/material'; import { TableRow, TableCell, Skeleton, Stack } from '@mui/material';
export const OrdersTableSkeleton = ({ rowsPerPage = 5, col = 1 }) => { export const OrdersTableSkeleton = ({ rowsPerPage = 5, col = 1 }) => {

View File

@@ -0,0 +1,79 @@
/* eslint-disable no-unused-vars */
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
// distance function (same)
function distance(lat1, lng1, lat2, lng2) {
const R = 6371;
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLng = (lng2 - lng1) * (Math.PI / 180);
const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.sin(dLng / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
const center = [11.015181, 76.953682];
const riders = [
{ id: 1, lat: 11.04362, lng: 76.924667 },
{ id: 2, lat: 11.00988, lng: 76.949966 },
{ id: 3, lat: 11.020983, lng: 76.966331 }
];
export default function RidersPinPointOSM() {
const sortedRiders = riders
.map((r) => ({
...r,
distance: distance(center[0], center[1], r.lat, r.lng)
}))
.sort((a, b) => a.distance - b.distance);
// purple center marker
const centerIcon = L.icon({
iconUrl: 'https://maps.google.com/mapfiles/ms/icons/purple-dot.png',
iconSize: [32, 32]
});
// basic numbered marker icon
const createMarkerIcon = (number) =>
L.divIcon({
className: 'custom-marker',
html: `
<div style="
background:#007bff;
color:white;
width:28px;
height:28px;
border-radius:50%;
display:flex;
align-items:center;
justify-content:center;
font-weight:bold;
border:2px solid white;
">
${number}
</div>
`,
iconSize: [30, 30],
iconAnchor: [15, 15]
});
return (
<MapContainer center={center} zoom={14} style={{ height: '300px', width: '100%' }}>
{/* OSM tiles */}
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{/* Purple center marker */}
<Marker position={center} icon={centerIcon} />
{/* Sorted rider markers */}
{sortedRiders.map((r, index) => (
<Marker key={r.id} position={[r.lat, r.lng]} icon={createMarkerIcon(index + 1)}>
<Popup>Rider {index + 1}</Popup>
</Marker>
))}
</MapContainer>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,157 @@
/* eslint-disable no-unused-vars */
import * as React from 'react';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import LocationOnIcon from '@mui/icons-material/LocationOn';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import parse from 'autosuggest-highlight/parse';
import { debounce } from '@mui/material/utils';
// This key was created specifically for the demo in mui.com.
// You need to create a new one for your application.
const GOOGLE_MAPS_API_KEY ='AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8';
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 };
export default function GoogleMaps() {
const [value, setValue] = React.useState(null);
const [inputValue, setInputValue] = React.useState('');
const [options, setOptions] = React.useState([]);
const loaded = React.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 = React.useMemo(
() =>
debounce((request, callback) => {
autocompleteService.current.getPlacePredictions(request, callback);
}, 400),
[],
);
React.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]);
return (
<Autocomplete
id="google-map-demo"
// sx={{ width: 300 }}
fullWidth
getOptionLabel={(option) =>
typeof option === 'string' ? option : option.description
}
filterOptions={(x) => x}
options={options}
autoComplete
includeInputInList
filterSelectedOptions
value={value}
noOptionsText="No locations"
onChange={(event, newValue) => {
setOptions(newValue ? [newValue, ...options] : options);
setValue(newValue);
}}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
renderInput={(params) => (
<TextField {...params}
// label="Add a location"
placeholder='Address'
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>
);
}}
/>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,846 @@
/* eslint-disable no-unused-vars */
import React from 'react';
import Loader from 'components/Loader';
import { useEffect, useState, Fragment } from 'react';
import { useTheme } from '@mui/material/styles';
import MainCard from 'components/MainCard';
import axios from 'axios';
import ClearIcon from '@mui/icons-material/Clear';
import { SearchOutlined, CloseOutlined } from '@ant-design/icons';
import { Empty } from 'antd';
import MyLocationIcon from '@mui/icons-material/MyLocation';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs from 'dayjs';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
import { enqueueSnackbar } from 'notistack';
import { useNavigate } from 'react-router';
import { calculateDrivingDistance, calculateTotalCharge } from '../../../utils/distance';
import { GoogleMap, LoadScript, Marker } from '@react-google-maps/api';
import {
FormControl,
InputAdornment,
Grid,
Typography,
Stack,
Button,
TextField,
Autocomplete,
Divider,
Dialog,
DialogTitle,
DialogContent,
Checkbox,
DialogActions,
CircularProgress,
IconButton,
OutlinedInput,
FormGroup,
FormControlLabel,
Table,
TableContainer,
TableCell,
TableBody,
TableRow,
Paper,
TableHead,
Box
} from '@mui/material';
import CircularLoader from 'components/nearle_components/CircularLoader';
// import RidersPinPointOSM from './RidersPinPointOSM';
import RidersPinPoint from './ridersPinPoint';
const MultipleOrders = () => {
const navigate = useNavigate();
const theme = useTheme();
const [loading, setLoading] = useState(false);
const [btnLoading, setBtnLoading] = useState(false);
const [appId, setAppId] = useState(0);
const [tenantLocations, setTenantlocations] = useState([]);
const userid = localStorage.getItem('userid');
const tenId = localStorage.getItem('tenantid');
const [tid, setTid] = useState(0);
const [isLocation, setIsLocation] = useState(false);
const [basePrice, setBasePrice] = useState(0);
const [pricePerKm, setPricePerKm] = useState(0);
const [minKm, setMinKm] = useState(0);
const [pickCust, setPickCust] = useState(null);
const [dropCust, setDropCust] = useState([]);
const [isCustomerOpen, setIsCustomerOpen] = useState(false);
const [searchCustList, setSearchCustList] = useState('');
const [customerlist, setCustomerlist] = useState([]);
const [startdate, setStartdate] = useState(dayjs().format('MM-DD-YYYY'));
const [timeslotarr, setTimeslotarr] = useState([]);
const [starttime, setStatrttime] = useState();
const [endtime, setEndtime] = useState();
const [alertmessage, setAlertmessage] = useState('');
const [otherinstructions, setOtherinstructions] = useState('');
const [admintoken, setAdmintoken] = useState();
const [totaldist, settotaldist] = useState(0);
const [totalAmt, settotalAmt] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [showMap, setShowMap] = useState(false);
useEffect(() => {
dropCust && console.log('dropCust', dropCust);
}, [dropCust]);
// =============================================== || opentoast || ===============================================
const opentoast = (message, variant, time) => {
enqueueSnackbar(message, {
variant: variant,
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: time ? time : 1500
});
console.log(alertmessage);
};
// ==============================|| fetchAppLocations ||============================== //
const fetchAppLocations = async () => {
try {
const locationRes = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
console.log('fetchAppLocations', locationRes.data.details);
} catch (err) {
console.log('locationRes', err);
}
};
useEffect(() => {
fetchAppLocations();
}, []);
// ============================================= || fetchTenantPricing || =============================================
const fetchTenantPricing = async (id) => {
try {
const pricingResponse = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${tenId}`);
console.log('pricingResponse', pricingResponse.data.details);
setBasePrice(pricingResponse.data.details.baseprice);
setPricePerKm(pricingResponse.data.details.priceperkm);
setMinKm(pricingResponse.data.details.minkm);
} catch (error) {
console.log('fetchTenantPricing error', error);
}
};
useEffect(() => {
fetchTenantPricing();
}, []);
// ============================================= || 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) {
setIsLocation(true);
setTenantlocations(res.data.details);
setPickCust(res.data.details[0]);
} else {
setTenantlocations(res.data.details);
}
} catch (err) {
console.log('gettenantlocations', err);
}
};
useEffect(() => {
gettenantlocations(tenId);
}, []);
// ========================================================= || clientdetails || =========================================================
const clientdetails = async () => {
try {
let url =
searchCustList == ''
? `${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tenId}&pageno=1&pagesize=10`
: `${process.env.REACT_APP_URL}/customers/search/?tenantid=${tenId}&keyword=${searchCustList}`;
await axios
.get(url)
.then((res) => {
if (res.data.status) {
console.log('clientdetails', res.data.details);
setCustomerlist(res.data.details);
let arr = [];
res.data.details.map((val) => {
arr.push({
label: `${val.firstname} | ${val.contactno}`,
...val
});
});
}
})
.catch((err) => {
console.log(err);
opentoast('server error', 'warning');
});
} catch (err) {
console.log(err);
}
};
useEffect(() => {
if (tenId) {
clientdetails();
}
}, [searchCustList.length > 3, searchCustList == '', tenId]);
// ========================================================= || calculateTotal(dist , charge) || =========================================================
const calculateTotal = () => {
let a1 = 0;
let a2 = 0;
dropCust?.map((customer) => {
a1 += customer.distance;
a2 += customer.totalcharge;
});
settotaldist(a1);
settotalAmt(a2);
};
useEffect(() => {
dropCust && calculateTotal();
}, [dropCust]);
// ========================================================= || handleCheckboxChange || =========================================================
const handleCheckboxChange = async (event, customer) => {
setIsLoading(true);
console.log('event', event.target.checked);
console.log('customer', customer);
if (event.target.checked) {
// If the checkbox is checked, calculate the distance and add the customer
try {
const obj = await calculateDistance(customer);
console.log('return of calculateDistance', obj);
const { roundedDistance, totalcharge } = obj;
// Create a new customer object with the distance property
const updatedCustomer = {
...customer,
distance: roundedDistance,
totalcharge: totalcharge
};
// Add the updated customer object to dropCust
setDropCust((prevDropCust) => [...prevDropCust, updatedCustomer]);
// Log the rounded distance
console.log(`Rounded Distance: ${roundedDistance} km`);
} catch (error) {
console.error('Failed to calculate distance:', error);
}
setIsLoading(false);
} else {
// If the checkbox is unchecked, remove the customer from dropCust
setDropCust((prevDropCust) => {
return prevDropCust.filter((cust) => cust.customerid !== customer.customerid);
});
setIsLoading(false);
}
};
// ========================================================= || calculateDistance || =========================================================
const calculateDistance = async (customer) => {
console.log('Distance calculation starts');
try {
const roundedDistance = await calculateDrivingDistance(pickCust, customer);
const totalcharge = calculateTotalCharge(roundedDistance, basePrice, pricePerKm, minKm);
return { roundedDistance, totalcharge };
} catch (error) {
console.error('Error calculating distance:', error);
throw error;
}
};
// ==================================================== || fetchTiming || ====================================================
const fetchTiming = async () => {
setLoading(true);
await axios
.get(`${process.env.REACT_APP_URL}/utils/getapplocations/?applocationid=${appId}`)
.then((res) => {
console.log('fetchTiming', res);
const { opentime, closetime, latitude, longitude, radius } = res.data.details[0];
if (res.data.status) {
setStatrttime(`${dayjs().format('MM-DD-YYYY')} ${opentime}`);
setEndtime(`${dayjs().format('MM-DD-YYYY')} ${closetime}`);
console.log('starttime', `${dayjs().format('MM-DD-YYYY')} ${opentime}`);
console.log('endtime', `${dayjs().format('MM-DD-YYYY')} ${closetime} `);
let arr = [];
for (
let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`, j = 0;
dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0;
j++, i = dayjs(i).add(30, 'm')
) {
arr.push(i);
}
console.log('setTimeslotarr', arr);
setTimeslotarr(arr);
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
};
useEffect(() => {
if (appId) {
fetchTiming();
}
}, [starttime, endtime, appId]);
const fetchAppAdminTokens = async () => {
setLoading(true);
await axios
.get(`${process.env.REACT_APP_URL}/utils/getapplocationconfig/?applocationid=${appId}`)
.then((res) => {
const userfcmtokemArray = res.data.details.applocationadmins.map((admin) => admin.userfcmtokem); // fcm => firebase cloud messaging
console.log('fetchAppAdminTokens', res);
console.log('userfcmtokemArray', userfcmtokemArray);
if (res.data.status) {
setAdmintoken(userfcmtokemArray);
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
};
useEffect(() => {
if (starttime && endtime) {
fetchAppAdminTokens();
}
}, [starttime, endtime]);
useEffect(() => {
console.log('pickCust', pickCust);
}, [pickCust]);
// ==================================================== || fetchtenantinfo || ====================================================
const fetchtenantinfo = async () => {
setLoading(true);
console.log('tid', tid);
await axios
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
.then((res) => {
console.log('fetchtenantinfo', res);
if (res.data.status) {
fetchAppAdminTokens();
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
};
useEffect(() => {
if (tid) {
fetchtenantinfo();
}
}, [tid]);
// ================================================== || sendnotifications || ==================================================
const sendnotifications = async () => {
setLoading(true);
await axios
.post(`${process.env.REACT_APP_URL}/utils/sendnotifications`, {
priority: 'high',
registration_ids: admintoken,
data: {
accessid: process.env.REACT_APP_RIDER_ACCESS_ID
},
notification: {
title: 'Nearle Merchant',
body: 'An Order has been placed successfully,kindly process the same',
sound: 'ring'
}
})
.then((res) => {
console.log(res);
if (res.data.message == 'Success') {
enqueueSnackbar('Notification sent Successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 1000
});
}
setLoading(false);
})
.catch((err) => {
console.log(err);
enqueueSnackbar(err.message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 1000
});
setLoading(false);
});
};
// =============================================== || creategrouporders || ===============================================
const creategrouporders = async () => {
const arr = dropCust?.map((customer) => ({
applocationid: pickCust.applocationid,
cancellled: '',
// categoryid: +tenant.categoryid,
configid: 9,
customerid: customer.customerid,
deliveryaddress: customer.address || '',
deliverycharge: +customer.totalcharge || 0,
deliverycity: customer.city || '',
deliverycontactno: customer.contactno || '',
deliverycustomer: customer.firstname || '',
deliveryid: +customer.customerid,
deliverylandmark: customer.landmark || '',
deliverylat: customer.latitude,
deliverylocation: customer.suburb || '',
deliverylocationid: customer.deliverylocationid || 0,
deliverylong: customer.longitude,
// deliverytime: `${dayjs(startdate).format('YYYY-MM-DD HH:mm:ss')} `,
deliverytime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
deliverytype: 'B',
delivered: '',
itemcount: 1,
kms: customer.distance.toString() || 0,
locationid: +pickCust.locationid,
moduleid: +pickCust.moduleid,
orderamount: +customer.totalcharge || 0,
ordercharges: 0.0,
orderdate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
orderheaderid: 0,
orderid: '', //
ordernotes: otherinstructions,
orderstatus: 'created',
ordervalue: +customer.totalcharge || 0,
partnerid: pickCust.partnerid,
partneruserid: +userid,
paymentstatus: 1,
paymenttype: 42,
pending: '',
pickupaddress: pickCust.address || '',
pickupcity: pickCust.locationcity || '',
pickupcontactno: pickCust.contactno || '',
pickupcustomer: pickCust.locationname || '',
pickuplandmark: pickCust.landmark || '',
pickuplat: pickCust.latitude,
pickuplocation: pickCust.suburb || '',
pickuplocationid: pickCust.locationid || 0,
pickuplong: pickCust.longitude,
processing: '',
ready: '',
remarks: '',
taxamount: 0.0,
tenantid: pickCust.tenantid,
tenantuserid: 0
}));
console.log('arr', arr);
if (!tenId) {
opentoast('Choose Client ', 'warning');
} else {
setLoading(true);
await axios
.post(`${process.env.REACT_APP_URL}/orders/createorders`, arr)
.then((res) => {
if (res.data.status) {
enqueueSnackbar('Order Created Successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 1000
});
if (admintoken) {
// notifyadmin(admintoken);
sendnotifications();
}
navigate('/nearle/orders');
} else {
opentoast(res.data.message, 'warning');
}
setLoading(false);
console.log(res);
})
.catch((err) => {
console.log(err);
// opentoast(err.data.message, 'warning');
setLoading(false);
});
}
console.log(arr);
};
return (
<>
{loading && <Loader />}
{/* <RidersPinPointOSM /> */}
<Grid container sx={{ mb: 2 }}>
<Grid item xs={12} sm={3} md={6}>
<Stack>
<Typography variant="h3" whiteSpace="nowrap">
Multiple Orders
</Typography>
</Stack>
</Grid>
<Grid item xs={12} sm={9} md={6}>
<Stack
sx={{}}
width={'100%'}
direction="row"
alignItems="center"
spacing={2}
justifyContent={'flex-end'}
flexWrap={{ xs: 'wrap', custom550: 'nowrap' }}
gap={2}
>
{/* Business Location */}
<Stack sx={{ width: '100%' }}>
{tenantLocations?.length === 1 ? (
<TextField
label="Business Location"
fullWidth
focused
value={tenantLocations[0]?.locationname}
InputProps={{
style: { color: theme.palette.primary.main },
startAdornment: (
<InputAdornment position="start">
<MyLocationIcon color="primary" />
</InputAdornment>
)
}}
/>
) : (
<Autocomplete
fullWidth
options={tenantLocations || []}
getOptionLabel={(option) => `${option.locationname} (${option.suburb})`}
onChange={(event, value, reason) => {
if (value) {
setTid(value.tenantid);
setIsLocation(true);
setPickCust(value);
}
if (reason === 'clear') setIsLocation(false);
}}
renderInput={(params) => <TextField {...params} label="Select Business Location" color="primary" fullWidth />}
/>
)}
</Stack>
{/* Date Picker */}
<Stack sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
format="DD-MM-YYYY"
disablePast
value={dayjs(startdate)}
sx={{ width: 150 }}
onChange={(e) => {
let diff = dayjs().diff(dayjs(dayjs(e).format('YYYY-MM-DD')), 'd');
if (diff <= 0) {
setStartdate(e);
let arr = [];
timeslotarr.forEach((val) => {
if (dayjs().diff(dayjs(`${dayjs(e).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0) {
arr.push(val);
}
});
if (arr[0]) {
setOrderarr([
{
sno: 1,
address: '',
customerid: '',
deliverytime: dayjs(arr[0]),
deliverylocationid: '',
clientname: '',
contactno: '',
latitude: '',
longitude: ''
}
]);
} else {
setOrderarr([]);
}
} else {
opentoast('choose Upcoming Date', 'warning');
setStartdate(NaN);
}
}}
/>
</LocalizationProvider>
</Stack>
</Stack>
</Grid>
</Grid>
{/* ===================================================== || Pickup || ===================================================== */}
{pickCust && (
<TableContainer component={Paper} sx={{ mb: 2 }}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Pickup Location</TableCell>
<TableCell>Address</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>{pickCust?.locationname}</TableCell>
<TableCell>{pickCust?.address}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
)}
{/* ===================================================== || Drop || ===================================================== */}
<MainCard
sx={{ height: '100%' }}
title={`Drop (${dropCust?.length || 0})`}
secondary={
<Button
variant="outlined"
size="small"
sx={{
'&:hover': {
bgcolor: theme.palette.primary.main,
color: 'white'
}
}}
onClick={() => {
if (!isLocation) {
opentoast('Select Business Location', 'warning');
} else {
setIsCustomerOpen(true);
setSearchCustList('');
}
}}
>
Select Customers
</Button>
}
>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>S.No</TableCell>
<TableCell>Customer</TableCell>
<TableCell>Address</TableCell>
<TableCell>Kms</TableCell>
<TableCell align="right">Charge</TableCell>
<TableCell>Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{!dropCust && (
<TableRow>
<TableCell colSpan={6}>
<Empty description={' Drop Customers Not Selected'} />
</TableCell>
</TableRow>
)}
{dropCust?.map((customer, index) => (
<TableRow key={index}>
<TableCell>{index + 1}</TableCell>
<TableCell>{customer.firstname}</TableCell>
<TableCell>{customer.address}</TableCell>
<TableCell>{customer.distance}</TableCell>
<TableCell align="right">{`${customer.totalcharge}.00`}</TableCell>
<TableCell align="center">
{
<CloseOutlined
style={{ cursor: 'pointer', color: 'red' }}
onClick={(event) => handleCheckboxChange(event, customer)}
/>
}
</TableCell>
</TableRow>
))}
{dropCust?.length != 0 && (
<TableRow>
<TableCell>
<Typography variant="h5">Total</Typography>
</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell>
<Typography variant="h5">{`${totaldist} `}</Typography>
</TableCell>
<TableCell align="right">
<Typography variant="h5"> {`${totalAmt}.00`}</Typography>
</TableCell>
<TableCell></TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</MainCard>
{/* ================================================= || Riders Map || ================================================= */}
{/* {showMap && dropCust.length >= 1 && <RidersPinPoint pickCust={pickCust} dropCust={dropCust} />} */}
{/* ================================================= || Notes || ================================================= */}
{dropCust && (
<MainCard sx={{ mt: 2 }} title={'Notes'}>
<Grid container>
<Grid item xs={12}>
<TextField
focused
id="outlined-multiline-static"
sx={{ width: '100%', height: '100%', mb: 2 }}
multiline
rows={1}
placeholder="Notes"
value={otherinstructions}
onChange={(e) => setOtherinstructions(e.target.value)}
/>
</Grid>
<Stack direction="row" justifyContent={'end'} sx={{ mt: 2, width: '100%' }}>
<Button
disabled={dropCust?.length == 0}
size="medium"
variant="outlined"
onClick={() => {
setLoading(true);
setBtnLoading(true);
creategrouporders();
setTimeout(() => {
setLoading(false);
setBtnLoading(false);
}, 2000);
}}
sx={{
'&:hover': {
transform: 'scale(1.05)',
transition: 'transform 0.3s ease'
}
}}
>
{btnLoading ? <CircularProgress color="primary" size={20} thickness={10} /> : 'Create'}
</Button>
</Stack>
</Grid>
</MainCard>
)}
{/* ============================================= || saved address Dialog || ============================================= */}
<Dialog
open={isCustomerOpen}
onClose={() => {
setIsCustomerOpen(false);
}}
fullWidth
sx={{ minWidth: 'lg' }}
>
{isLoading && <CircularLoader />}
<DialogTitle sx={{ bgcolor: theme.palette.primary.main, color: 'white' }}>
<Stack>
<Typography variant="h4"> {`Select Drop Customers (${dropCust?.length || 0})`}</Typography>
<FormControl
sx={{
width: '100%',
mt: 1
}}
>
<Stack spacing={2} sx={{ py: 0.2 }}>
<OutlinedInput
fullWidth
id="input-search-header"
placeholder="Search"
value={searchCustList}
onChange={(e) => setSearchCustList(e.target.value)}
sx={{
'& .MuiOutlinedInput-input': {
p: '10.5px 0px 12px'
},
bgcolor: 'white'
}}
startAdornment={
<InputAdornment position="start">
<SearchOutlined style={{ fontSize: 'small' }} />
</InputAdornment>
}
endAdornment={
<IconButton
sx={{ visibility: searchCustList ? 'visible' : 'hidden' }}
onClick={() => {
setSearchCustList('');
}}
>
<ClearIcon />
</IconButton>
}
autoComplete="off"
/>
</Stack>
</FormControl>
</Stack>
</DialogTitle>
<Divider />
<DialogContent sx={{ p: 2.5 }}>
{customerlist.length == 0 ? (
<Stack spacing={2} direction={'row'} alignItems={'center'} justifyContent={'center'} sx={{ minHeight: 600, maxHeight: 600 }}>
<Empty />
</Stack>
) : (
<Stack spacing={2} sx={{ minHeight: 600, maxHeight: 600 }}>
{customerlist &&
customerlist.map((customer, index) => (
<FormGroup key={index}>
<FormControlLabel
control={
<Checkbox
checked={dropCust?.some((cust) => cust.customerid === customer.customerid)} // Set the checked state of the checkbox based on whether the customer is in `dropCust`
onChange={(event) => handleCheckboxChange(event, customer)}
/>
}
label={
<div style={{ width: '100%' }}>
<Typography variant="subtitle1" sx={{ textAlign: 'left' }}>
{`${customer.firstname} (${customer.contactno})`}
</Typography>
<Typography variant="body2" color="secondary" sx={{ textAlign: 'left' }}>
{customer.address}
</Typography>
</div>
}
/>
</FormGroup>
))}
</Stack>
)}
</DialogContent>
<Divider />
<DialogActions sx={{ p: 2.5 }}>
<Button
color={dropCust?.length !== 0 ? 'primary' : 'error'}
variant="outlined"
sx={{
'&:hover': {
bgcolor: dropCust?.length !== 0 ? theme.palette.primary.main : theme.palette.error.main,
color: 'white'
}
}}
onClick={() => {
setIsCustomerOpen(false);
{
dropCust?.length !== 0 && setShowMap(true);
}
}}
>
{dropCust?.length !== 0 ? 'Continue' : 'Close'}
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default MultipleOrders;

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import React from 'react'; import React from 'react';
import Loader from 'components/Loader'; import Loader from 'components/Loader';
import { useEffect, useState, useRef, Fragment } from 'react'; import { useEffect, useState, useRef, Fragment } from 'react';
@@ -506,7 +507,7 @@ const MultipleOrders = () => {
accessid: process.env.REACT_APP_RIDER_ACCESS_ID accessid: process.env.REACT_APP_RIDER_ACCESS_ID
}, },
notification: { notification: {
title: 'Nearle Merchant', title: 'Doormile Merchant',
body: 'An Order has been placed successfully,kindly process the same', body: 'An Order has been placed successfully,kindly process the same',
sound: 'ring' sound: 'ring'
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import * as React from 'react'; import * as React from 'react';
import { useEffect, useState, useRef, Fragment } from 'react'; import { useEffect, useState, useRef, Fragment } from 'react';
import { import {
@@ -786,7 +787,7 @@ const Createorder1 = () => {
// notifyadmin(admintoken); // notifyadmin(admintoken);
sendnotifications(); sendnotifications();
} }
navigate('/nearle/orders'); navigate('/doormile/orders');
} else { } else {
opentoast('Error in creating orders', 'warning'); opentoast('Error in creating orders', 'warning');
} }
@@ -837,7 +838,7 @@ const Createorder1 = () => {
accessid: process.env.REACT_APP_RIDER_ACCESS_ID accessid: process.env.REACT_APP_RIDER_ACCESS_ID
}, },
notification: { notification: {
title: 'Nearle Merchant', title: 'Doormile Merchant',
body: 'An Order has been placed successfully,kindly process the same', body: 'An Order has been placed successfully,kindly process the same',
sound: 'ring' sound: 'ring'
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import { import {
Autocomplete, Autocomplete,
Button, Button,
@@ -200,7 +201,7 @@ const OptimisedOrderPreview = () => {
onSettled: () => { onSettled: () => {
setTimeout(() => { setTimeout(() => {
setIsLoading(false); setIsLoading(false);
navigate('/nearle/deliveries'); navigate('/doormile/deliveries');
}, 2000); }, 2000);
} }
}); });
@@ -279,7 +280,7 @@ const OptimisedOrderPreview = () => {
<Stack direction="row" alignItems="center" spacing={1}> <Stack direction="row" alignItems="center" spacing={1}>
<Tooltip title="Back to orders" placement="top"> <Tooltip title="Back to orders" placement="top">
<IconButton <IconButton
onClick={() => navigate('/nearle/orders')} onClick={() => navigate('/doormile/orders')}
sx={{ sx={{
bgcolor: 'action.hover', bgcolor: 'action.hover',
'&:hover': { bgcolor: 'action.selected' } '&:hover': { bgcolor: 'action.selected' }
@@ -419,7 +420,7 @@ const OptimisedOrderPreview = () => {
<MobileCardList> <MobileCardList>
{orders.map((val, i) => { {orders.map((val, i) => {
const typeAccent = const typeAccent =
val.ordertype === 'Economy' ? '#10b981' : val.ordertype === 'Risky' ? '#ef4444' : '#662582'; val.ordertype === 'Economy' ? '#10b981' : val.ordertype === 'Risky' ? '#ef4444' : '#C01227';
return ( return (
<MobileCard key={i} accent={typeAccent}> <MobileCard key={i} accent={typeAccent}>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}> <Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
@@ -719,7 +720,7 @@ const OptimisedOrderPreview = () => {
color="secondary" color="secondary"
startIcon={<ArrowBackIcon />} startIcon={<ArrowBackIcon />}
onClick={() => { onClick={() => {
navigate('/nearle/orders'); navigate('/doormile/orders');
}} }}
> >
Back Back

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
/* eslint-disable no-unused-vars */
import { LoadScriptNext, GoogleMap, Marker } from '@react-google-maps/api';
// distance function
function distance(lat1, lng1, lat2, lng2) {
const R = 6371;
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLng = (lng2 - lng1) * (Math.PI / 180);
const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.sin(dLng / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
const containerStyle = {
width: '100%',
height: '300px'
};
export default function RidersPinPoint({ pickCust, dropCust }) {
// Ensure valid lat/lng
const center = pickCust?.latitude && pickCust?.longitude ? { lat: Number(pickCust.latitude), lng: Number(pickCust.longitude) } : null;
// If center missing, don't render map
if (!center) return null;
const sortedRiders = dropCust
?.map((r) => ({
...r,
distance: distance(center.lat, center.lng, Number(r.latitude), Number(r.longitude))
}))
.sort((a, b) => a.distance - b.distance);
return (
<LoadScriptNext googleMapsApiKey={process.env.REACT_APP_GOOGLE_MAPS_API_KEY}>
<GoogleMap mapContainerStyle={containerStyle} zoom={11} center={center}>
<Marker position={center} icon={{ url: 'http://maps.google.com/mapfiles/ms/icons/purple-dot.png' }} />
{sortedRiders?.map((r, index) => (
<Marker
key={index}
position={{ lat: Number(r.latitude), lng: Number(r.longitude) }}
label={{
text: (index + 1).toString(),
color: 'white',
fontSize: '14px',
fontWeight: 'bold'
}}
/>
))}
</GoogleMap>
</LoadScriptNext>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { LoadScriptNext, GoogleMap } from '@react-google-maps/api'; import { LoadScriptNext, GoogleMap } from '@react-google-maps/api';
import { DT } from 'themes/dt/tokens';
const containerStyle = { const containerStyle = {
width: '100%', width: '100%',
@@ -46,7 +47,7 @@ const MapWithRouteGoogle = ({ coordinates, additionalProps, setMapOpen }) => {
const route = new window.google.maps.Polyline({ const route = new window.google.maps.Polyline({
path: numericCoordinates, path: numericCoordinates,
geodesic: false, geodesic: false,
strokeColor: '#1A73E8', strokeColor: DT.brand,
strokeOpacity: 1.0, strokeOpacity: 1.0,
strokeWeight: 4 strokeWeight: 4
}); });
@@ -69,11 +70,13 @@ const MapWithRouteGoogle = ({ coordinates, additionalProps, setMapOpen }) => {
right: 10, right: 10,
zIndex: 999, zIndex: 999,
padding: '6px 12px', padding: '6px 12px',
background: '#1A73E8', background: DT.brand,
color: 'white', color: 'white',
borderRadius: 6, borderRadius: DT.radiusInner,
cursor: 'pointer', cursor: 'pointer',
border: 'none' border: 'none',
fontWeight: 600,
boxShadow: DT.shadowMd
}} }}
> >
Close Close

View File

@@ -1,76 +0,0 @@
import { Button } from '@mui/material';
import { LoadScriptNext, GoogleMap, Marker, OverlayView } from '@react-google-maps/api';
const containerStyle = {
width: '100%',
height: 'calc(100vh - 150px)'
};
export default function RiderLocationMap({ riderLocations }) {
console.log('riderLocations', riderLocations);
const center = {
lat: Number(riderLocations?.[0]?.latitude || 11.0056),
lng: Number(riderLocations?.[0]?.longitude || 76.9661)
};
const GreenIcon = {
url: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
scaledSize: new window.google.maps.Size(25, 41),
anchor: new window.google.maps.Point(12, 41)
};
const RedIcon = {
url: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
scaledSize: new window.google.maps.Size(25, 41),
anchor: new window.google.maps.Point(12, 41)
};
return (
<LoadScriptNext googleMapsApiKey={process.env.REACT_APP_GOOGLE_MAPS_API_KEY}>
<GoogleMap mapContainerStyle={containerStyle} zoom={12} center={center}>
{riderLocations &&
riderLocations?.map((r, index) => {
const lat = Number(r.latitude);
const lng = Number(r.longitude);
return (
<div key={index}>
{/* Marker */}
<Marker
position={{ lat, lng }}
icon={r.status == 'active' ? GreenIcon : RedIcon}
label={{
fontSize: '14px',
fontWeight: 'bold'
}}
/>
<OverlayView position={{ lat, lng }} mapPaneName={OverlayView.OVERLAY_LAYER}>
<div
style={{
background: 'none',
color: 'green',
padding: '2px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 600,
whiteSpace: 'nowrap',
transform: 'translate(-50%, -140%)',
pointerEvents: 'none',
ml: 20
}}
>
<Button variant="contained" color="primary" size="small">
{` ${r.username} `}
{/* <br /> */}
{/* {`${r.contactno || '##### ##### '} `} */}
<br />
{`(${r.orderid || ''}) `}
</Button>
</div>
</OverlayView>
</div>
);
})}
</GoogleMap>
</LoadScriptNext>
);
}

View File

@@ -107,7 +107,7 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
// brand purple to match the planned-route polyline below. // brand purple to match the planned-route polyline below.
const stepIcon = (n, isFocused) => { const stepIcon = (n, isFocused) => {
const size = isFocused ? 38 : 32; const size = isFocused ? 38 : 32;
const color = isFocused ? '#4D1C61' : '#662582'; const color = isFocused ? '#910E1D' : '#C01227';
const svg = encodeURIComponent( const svg = encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="${size}" height="${size}">` + `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="${size}" height="${size}">` +
`<circle cx="16" cy="16" r="14" fill="${color}" stroke="white" stroke-width="3"/>` + `<circle cx="16" cy="16" r="14" fill="${color}" stroke="white" stroke-width="3"/>` +
@@ -126,7 +126,7 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
px: 2, px: 2,
py: 1.25, py: 1.25,
borderBottom: '1px solid rgba(15, 23, 42, 0.08)', borderBottom: '1px solid rgba(15, 23, 42, 0.08)',
background: 'linear-gradient(135deg, #662582 0%, #9255AB 100%)', background: 'linear-gradient(135deg, #C01227 0%, #D25463 100%)',
color: '#fff', color: '#fff',
flexShrink: 0 flexShrink: 0
}} }}
@@ -207,12 +207,12 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
{/* Translucent backdrop so the route stays legible on busy tiles. */} {/* Translucent backdrop so the route stays legible on busy tiles. */}
<Polyline <Polyline
path={routePath} path={routePath}
options={{ strokeColor: '#662582', strokeOpacity: 0.25, strokeWeight: 8 }} options={{ strokeColor: '#C01227', strokeOpacity: 0.25, strokeWeight: 8 }}
/> />
{/* Road-following planned route from the Directions API. */} {/* Road-following planned route from the Directions API. */}
<Polyline <Polyline
path={routePath} path={routePath}
options={{ strokeColor: '#662582', strokeOpacity: 0.95, strokeWeight: 4 }} options={{ strokeColor: '#C01227', strokeOpacity: 0.95, strokeWeight: 4 }}
/> />
</> </>
) : ( ) : (
@@ -221,7 +221,7 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
<Polyline <Polyline
path={dropPath} path={dropPath}
options={{ options={{
strokeColor: '#662582', strokeColor: '#C01227',
strokeOpacity: 0, strokeOpacity: 0,
strokeWeight: 0, strokeWeight: 0,
icons: [ icons: [
@@ -229,7 +229,7 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
icon: { icon: {
path: 'M 0,-1 0,1', path: 'M 0,-1 0,1',
strokeOpacity: 0.6, strokeOpacity: 0.6,
strokeColor: '#662582', strokeColor: '#C01227',
scale: 3 scale: 3
}, },
offset: '0', offset: '0',

View File

@@ -5,8 +5,8 @@ import 'leaflet/dist/leaflet.css';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Chip, Stack, Typography, Box } from '@mui/material'; import { Chip, Stack, Typography, Box } from '@mui/material';
import { CloseCircleOutlined } from '@ant-design/icons'; import { CloseCircleOutlined } from '@ant-design/icons';
import { useTheme } from '@mui/material/styles';
import CircularLoader from 'components/CircularLoader'; import CircularLoader from 'components/CircularLoader';
import { DT } from 'themes/dt/tokens';
var utc = require('dayjs/plugin/utc'); var utc = require('dayjs/plugin/utc');
dayjs.extend(utc); dayjs.extend(utc);
@@ -33,7 +33,6 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => {
console.log('additionalProps', additionalProps); console.log('additionalProps', additionalProps);
const mapRef = useRef(null); const mapRef = useRef(null);
const theme = useTheme();
const [routePoints, setRoutePoints] = useState([]); const [routePoints, setRoutePoints] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -145,13 +144,13 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => {
top: 12, top: 12,
right: 12, right: 12,
zIndex: 2000, zIndex: 2000,
bgcolor: theme.palette.error.main, bgcolor: DT.brand,
color: '#fff', color: '#fff',
fontWeight: 600, fontWeight: 600,
borderRadius: '12px', borderRadius: DT.radiusInner + 'px',
px: 1.5, px: 1.5,
py: 0.5, py: 0.5,
boxShadow: theme.shadows[4], boxShadow: DT.shadowMd,
cursor: 'pointer', cursor: 'pointer',
'& .MuiChip-icon': { color: '#fff' } '& .MuiChip-icon': { color: '#fff' }
}} }}
@@ -183,7 +182,7 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => {
width: '100%', width: '100%',
bgcolor: 'rgba(255,255,255,0.96)', bgcolor: 'rgba(255,255,255,0.96)',
p: 2, p: 2,
boxShadow: theme.shadows[3], boxShadow: DT.shadowPop,
zIndex: 1500 zIndex: 1500
}} }}
> >

View File

@@ -114,8 +114,8 @@ const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26'); const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55'); const edge = (c) => a(c, '55');
const BRAND = '#662582'; const BRAND = '#C01227';
const BRAND_LIGHT = '#9255AB'; const BRAND_LIGHT = '#D25463';
const SoftPaper = (props) => ( const SoftPaper = (props) => (
<Paper <Paper
@@ -153,7 +153,7 @@ const pillFieldSx = (color) => ({
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' }, '&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` }, '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 } '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
} }
}); });
@@ -1104,15 +1104,15 @@ export default function OrdersDetails() {
flexShrink: 0, flexShrink: 0,
cursor: 'pointer', cursor: 'pointer',
borderRadius: 999, borderRadius: 999,
border: `1px solid ${active ? meta.color : DT.borderSubtle}`, border: `1px solid ${active ? '#C01227' : DT.borderSubtle}`,
bgcolor: active ? meta.color : DT.surface, bgcolor: active ? '#C01227' : DT.surface,
color: active ? '#fff' : DT.textSecondary, color: active ? '#fff' : DT.textSecondary,
fontWeight: 600, fontWeight: 600,
boxShadow: 'none', boxShadow: 'none',
transition: 'background-color 0.15s, border-color 0.15s, color 0.15s', transition: 'background-color 0.15s, border-color 0.15s, color 0.15s',
'&:hover': { '&:hover': {
borderColor: active ? meta.color : '#cbd5e1', borderColor: active ? '#C01227' : '#cbd5e1',
bgcolor: active ? meta.color : DT.surfaceAlt bgcolor: active ? '#C01227' : DT.surfaceAlt
} }
}} }}
> >
@@ -1172,7 +1172,7 @@ export default function OrdersDetails() {
bgcolor: '#ffffff', bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' }, '&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }, '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` } '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}} }}
/> />
@@ -1817,10 +1817,21 @@ export default function OrdersDetails() {
</TableBody> </TableBody>
</Table> </Table>
)} )}
<Divider /> <Divider sx={{ minWidth: { xs: '100%', md: 1600 } }} />
{rows?.length !== 0 && ( {rows?.length !== 0 && (
<Stack justifyContent="center" alignItems="center" sx={{ width: '100%', py: 2 }}> <Box sx={{ minWidth: { xs: '100%', md: 1600 }, py: 2 }}>
<Stack ref={loadMoreRef} style={{ textAlign: 'center', width: '100%' }}> <Stack
ref={loadMoreRef}
alignItems="center"
justifyContent="center"
sx={{
position: 'sticky',
left: 0,
width: '100%',
maxWidth: '100vw',
textAlign: 'center'
}}
>
{isFetchingNextPage || hasNextPage ? ( {isFetchingNextPage || hasNextPage ? (
<LoaderWithImage /> <LoaderWithImage />
) : ( ) : (
@@ -1829,7 +1840,7 @@ export default function OrdersDetails() {
</Typography> </Typography>
)} )}
</Stack> </Stack>
</Stack> </Box>
)} )}
</TableContainer> </TableContainer>
</Paper> </Paper>

View File

@@ -87,8 +87,8 @@ const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26'); const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55'); const edge = (c) => a(c, '55');
const BRAND = '#662582'; const BRAND = '#C01227';
const BRAND_LIGHT = '#9255AB'; const BRAND_LIGHT = '#D25463';
const SoftPaper = (props) => ( const SoftPaper = (props) => (
<Paper <Paper
@@ -190,7 +190,7 @@ const pillFieldSx = (color) => ({
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' }, '&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` }, '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 } '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
} }
}); });
@@ -588,7 +588,7 @@ export default function OrdersReport() {
bgcolor: '#ffffff', bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' }, '&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }, '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` } '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}} }}
/> />

View File

@@ -1,790 +0,0 @@
import React, { useState, useEffect, useMemo } from 'react';
import PropTypes from 'prop-types';
import { useInfiniteQuery } from '@tanstack/react-query';
import {
Avatar,
Box,
Chip,
Grid,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Tooltip,
Typography,
useMediaQuery,
useTheme
} from '@mui/material';
import {
MdMyLocation,
MdCalendarMonth,
MdPerson,
MdOutlineCurrencyRupee,
MdStraighten,
MdPayments,
MdRoute,
MdTrendingUp,
MdTrendingDown
} from 'react-icons/md';
import dayjs from 'dayjs';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
import { fetchDeliveries } from 'pages/api/api';
import Loader from 'components/Loader';
import DateFilterDialog from 'components/DateFilterDialog';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
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 aColor = (c, suffix) => `${c}${suffix}`;
const soft = (c) => aColor(c, '18');
const tint = (c) => aColor(c, '08');
const edge = (c) => aColor(c, '55');
const ring = (c) => aColor(c, '26');
const BRAND = '#662582';
const SoftPaper = (props) => (
<Paper
{...props}
sx={{
mt: 0.75,
borderRadius: 2,
boxShadow: DT.shadowPop,
border: '1px solid',
borderColor: 'divider',
overflow: 'hidden'
}}
/>
);
SoftPaper.propTypes = {
children: PropTypes.node
};
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>
);
AccentAvatar.propTypes = {
color: PropTypes.string.isRequired,
selected: PropTypes.bool,
size: PropTypes.number,
children: PropTypes.node
};
const MetricPill = ({ color, icon, label, tooltip, minWidth = 80 }) => (
<Tooltip title={tooltip || ''} placement="top">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: '#ffffff',
border: `1px solid ${edge(color)}`,
color,
fontSize: 11,
fontWeight: 800,
minWidth,
justifyContent: 'center',
whiteSpace: 'nowrap'
}}
>
{icon}
{label}
</Box>
</Tooltip>
);
MetricPill.propTypes = {
color: PropTypes.string.isRequired,
icon: PropTypes.node,
label: PropTypes.string.isRequired,
tooltip: PropTypes.string,
minWidth: PropTypes.number
};
const BATCHES = [
{ id: 'morning', name: 'Morning Batch', startHour: 0, endHour: 8 },
{ id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12.5 },
{ id: 'evening', name: 'Evening Batch', startHour: 16, endHour: 19 }
];
const getBatchForHour = (h, batches = BATCHES) => {
for (const b of batches) {
if (h >= b.startHour && h < b.endHour) return b.id;
}
return null;
};
const getRowBatch = (r, batches = BATCHES) => {
const t = r?.assigntime;
if (!t) return null;
const str = String(t).trim();
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return null;
const d = dayjs(t);
if (!d.isValid()) return null;
return getBatchForHour(d.hour() + d.minute() / 60, batches);
};
function formatNumberToRupees(value) {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 2
}).format(Number(value) || 0);
}
export default function ProfitabilityReport() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
const [locaName, setLocoName] = useState('All');
const [open, setOpen] = useState(false);
const [datestatus, setDatestatus] = useState('Today');
const [appId, setAppId] = useState(0);
const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const liveUserid = typeof window !== 'undefined' ? localStorage.getItem('userid') || 0 : 0;
// Load slots configuration from localStorage to match Dispatch page edits
const customBatches = useMemo(() => {
if (typeof window === 'undefined') return BATCHES;
try {
const raw = window.localStorage.getItem('dispatch.slots.v9');
if (!raw) return BATCHES;
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed) || parsed.length !== BATCHES.length) return BATCHES;
return parsed.map((s, i) => {
const id = s.id || `slot-${i + 1}`;
const startHour = Number(s.startHour) || 0;
const endHour = Number(s.endHour) || 24;
return {
id,
name: s.name || BATCHES.find((b) => b.id === id)?.name || `Slot ${i + 1}`,
startHour,
endHour
};
});
} catch (e) {
return BATCHES;
}
}, []);
// Fetch all deliveries for the selected date range and zone
const {
data: deliveriesData,
isLoading: isLoadingDeliveries,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['fetchdeliveries', appId, liveUserid, 'all', startdate, enddate, 2000, '', 0, 0, 0],
queryFn: fetchDeliveries,
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
refetchOnWindowFocus: false
});
// Auto-page through all results
useEffect(() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
// Flatten and deduplicate deliveries by orderid
const liveRows = useMemo(() => {
const all = (deliveriesData?.pages || []).flatMap((p) => p.rows || []);
const seen = new Set();
const out = [];
for (const r of all) {
const key = r.orderid != null ? String(r.orderid) : null;
if (key && seen.has(key)) continue;
if (key) seen.add(key);
out.push(r);
}
return out;
}, [deliveriesData]);
// Group deliveries by rider
const ridersList = useMemo(() => {
const riderMap = {};
liveRows.forEach((r) => {
const key = String(r.userid || r.rider_id || '');
if (!key || key === 'unassigned' || key === '0') return;
if (!riderMap[key]) {
riderMap[key] = {
id: key,
riderName: r.ridername || r.rider_name || r.username || `Rider ${key}`,
orders: []
};
}
if (!riderMap[key].orders.some((existing) => existing.orderid === r.orderid)) {
riderMap[key].orders.push(r);
}
});
return Object.values(riderMap)
.map((r) => ({
...r,
orders: [...r.orders].sort((a, b) => {
const tA = a.trip_number || 1;
const tB = b.trip_number || 1;
if (tA !== tB) return tA - tB;
return (a.step || 0) - (b.step || 0);
})
}))
.sort((a, b) => b.orders.length - a.orders.length);
}, [liveRows]);
// Calculate profitability metrics for all riders
const stats = useMemo(() => {
let activeRiders = 0;
let profitableRiders = 0;
let lossRiders = 0;
let totalKms = 0;
let totalPlannedKms = 0;
let totalActualKms = 0;
const list = ridersList
.map((r) => {
let rRevenue = 0;
let rKms = 0;
let rPlannedKms = 0;
let rActualKms = 0;
const slotsByDate = {};
let ordersInSlots = 0;
r.orders.forEach((o) => {
const status = String(o.orderstatus || '').toLowerCase();
if (status === 'cancelled' || status === 'skipped') return;
const slot = getRowBatch(o, customBatches);
if (!slot) return;
const oKms = parseFloat(o.riderkms || 0);
rKms += oKms;
rPlannedKms += parseFloat(o.kms || 0);
rActualKms += parseFloat(o.actualkms || 0);
rRevenue += oKms <= 8 ? 30 : 30 + (oKms - 8) * 6;
const dateStr = o.assigntime
? dayjs(o.assigntime).format('YYYY-MM-DD')
: o.deliverydate
? dayjs(o.deliverydate).format('YYYY-MM-DD')
: null;
if (!dateStr) return;
if (!slotsByDate[dateStr]) {
slotsByDate[dateStr] = new Set();
}
slotsByDate[dateStr].add(slot);
ordersInSlots++;
});
if (ordersInSlots === 0) {
return null;
}
// Sum unique slots per day, capping at 3 slots max per day
let slotCount = 0;
Object.values(slotsByDate).forEach((set) => {
slotCount += Math.min(set.size, 3);
});
const rVarCost = rKms * 2.5;
const rFixedCost = slotCount * (500 / 3);
const rTotalCost = rVarCost + rFixedCost;
const rNet = rRevenue - rTotalCost;
const rMargin = rRevenue > 0 ? (rNet / rRevenue) * 100 : 0;
if (rNet >= 0) {
profitableRiders++;
} else {
lossRiders++;
}
totalKms += rKms;
totalPlannedKms += rPlannedKms;
totalActualKms += rActualKms;
activeRiders++;
return {
...r,
orderCount: ordersInSlots,
kms: rKms,
plannedKms: rPlannedKms,
actualKms: rActualKms,
revenue: rRevenue,
varCost: rVarCost,
fixedCost: rFixedCost,
totalCost: rTotalCost,
net: rNet,
margin: rMargin
};
})
.filter(Boolean);
return {
activeRiders,
profitableRiders,
lossRiders,
totalKms,
totalPlannedKms,
totalActualKms,
enrichedRiders: list
};
}, [ridersList, customBatches]);
// Filter riders by search query
const filteredRiders = useMemo(() => {
if (!stats?.enrichedRiders || !Array.isArray(stats.enrichedRiders)) return [];
const baseList = stats.enrichedRiders.filter(Boolean);
if (!debouncedSearch) return baseList;
const q = debouncedSearch.toLowerCase().trim();
return baseList.filter(
(r) => r && [r.riderName, String(r.id)].filter(Boolean).some((field) => String(field).toLowerCase().includes(q))
);
}, [stats?.enrichedRiders, debouncedSearch]);
const KPI_META = [
{
key: 'riders',
label: 'Riders Active',
color: BRAND,
icon: MdPerson,
value: stats?.activeRiders ?? 0
},
{
key: 'planned-kms',
label: 'Planned KMs',
color: '#0ea5e9',
icon: MdRoute,
value: `${(stats?.totalPlannedKms ?? 0).toFixed(1)} km`
},
{
key: 'actual-kms',
label: 'Actual KMs',
color: '#f59e0b',
icon: MdMyLocation,
value: `${(stats?.totalActualKms ?? 0).toFixed(1)} km`
},
{
key: 'rider-kms',
label: 'Rider KMs',
color: BRAND,
icon: MdStraighten,
value: `${(stats?.totalKms ?? 0).toFixed(1)} km`
},
{
key: 'total-distance',
label: 'Trip KMs',
color: '#10b981',
icon: MdStraighten,
value: `${(stats?.totalKms ?? 0).toFixed(1)} km`
}
];
return (
<>
{(isLoadingDeliveries || isFetchingNextPage) && <Loader />}
{/* Page Header */}
<PageHeader
title="Profitability Report"
subtitle={`Live · ${locaName || 'All Zones'} · ${datestatus}`}
live
action={
<LocationAutocomplete
locaName={locaName}
setAppId={setAppId}
setLocoName={setLocoName}
pill
accentColor={BRAND}
icon={<MdMyLocation size={14} />}
placeholder="Select Zone"
paperComponent={SoftPaper}
sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
/>
}
/>
{/* KPI Cards Grid */}
<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.key} xs={6} sm={4} md={2.4}>
<StatCard
title={item.label}
value={item.value ?? 0}
icon={<Icon size={20} />}
color={item.color}
loading={isLoadingDeliveries}
/>
</Grid>
);
})}
</Grid>
{/* Filter Bar (date + 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={{ xs: 'column', sm: 'row' }}
alignItems={{ xs: 'stretch', sm: 'center' }}
justifyContent="space-between"
spacing={1.25}
>
<Stack direction="row" alignItems="center" spacing={1.25} flexWrap="wrap">
<AccentAvatar color={BRAND} size={32}>
<MdPerson size={18} />
</AccentAvatar>
<Stack>
<Typography
variant="caption"
sx={{ fontWeight: 800, color: DT.textSecondary, letterSpacing: 0.6, textTransform: 'uppercase' }}
>
Profitability Overview · {datestatus}
</Typography>
<Typography variant="body2" sx={{ color: DT.textPrimary, fontWeight: 700 }}>
{filteredRiders.length} riders · {stats.profitableRiders} profitable · {stats.lossRiders} at loss
</Typography>
</Stack>
<Tooltip title="Date Filter" placement="top">
<Box
onClick={() => setOpen(true)}
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.75,
px: 1.25,
py: 0.75,
borderRadius: 999,
cursor: 'pointer',
bgcolor: tint('#f59e0b'),
border: `1.5px solid ${edge('#f59e0b')}`,
color: '#f59e0b',
fontWeight: 800,
fontSize: 12,
ml: 1,
transition: 'all 0.18s',
'&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` }
}}
>
<MdCalendarMonth size={14} />
{dayjs(startdate).format('DD/MM/YY')} {dayjs(enddate).format('DD/MM/YY')}
</Box>
</Tooltip>
</Stack>
<Box sx={{ width: { xs: '100%', sm: 280, lg: 340 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
<DebounceSearchBar
value={searchword}
onChange={setSearchword}
onDebouncedChange={setDebouncedSearch}
placeholder="Search riders"
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 & Mobile List Container */}
<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 ? (
<MobileCardList scroll>
{!filteredRiders || filteredRiders.length === 0 ? (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdPerson size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No riders to show
</Typography>
</Stack>
) : (
filteredRiders.map((row, index) => {
if (!row) return null;
const isProfit = (row.net ?? 0) >= 0;
return (
<MobileCard
key={row.id || index}
accent={isProfit ? '#10b981' : '#ef4444'}
header={
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color={isProfit ? '#10b981' : '#ef4444'} size={36}>
<MdPerson size={18} />
</AccentAvatar>
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.riderName}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.id}
</Typography>
</Stack>
</Stack>
}
>
<MobileFieldGrid columns={2}>
<MobileField label="Orders" value={row.orderCount} />
<MobileField label="Rider KMs" value={`${Math.round(row.kms)} km`} />
<MobileField label="Revenue" value={formatNumberToRupees(row.revenue)} />
<MobileField label="Fixed Cost" value={formatNumberToRupees(row.fixedCost)} />
<MobileField label="Variable Cost" value={formatNumberToRupees(row.varCost)} />
<MobileField label="Total Cost" value={formatNumberToRupees(row.totalCost)} />
<MobileField label="Net Profit" value={`${isProfit ? '+' : ''}${formatNumberToRupees(row.net)}`} full />
<MobileField label="Margin" value={`${Math.abs(row.margin).toFixed(0)}%`} full />
</MobileFieldGrid>
</MobileCard>
);
})
)}
</MobileCardList>
) : (
<TableContainer
sx={{
maxHeight: 'calc(100vh - 280px)',
'&::-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: 1000 }}>
<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>#</TableCell>
<TableCell>Rider</TableCell>
<TableCell align="center">Orders</TableCell>
<TableCell align="center">Rider KMs</TableCell>
<TableCell align="center">Revenue</TableCell>
<TableCell align="center">Fixed Cost</TableCell>
<TableCell align="center">Variable Cost</TableCell>
<TableCell align="center">Total Cost</TableCell>
<TableCell align="center">Net Profit</TableCell>
<TableCell align="center">Margin</TableCell>
</TableRow>
</TableHead>
<TableBody>
{!filteredRiders || filteredRiders.length === 0 ? (
<TableRow>
<TableCell colSpan={10} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdPerson size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No riders to show
</Typography>
</Stack>
</TableCell>
</TableRow>
) : (
filteredRiders.map((row, index) => {
if (!row) return null;
const isProfit = (row.net ?? 0) >= 0;
return (
<TableRow
key={row.id || index}
sx={{
transition: 'background-color 0.15s',
'& td': {
borderBottom: `1px solid ${DT.divider}`,
py: 1.5,
px: 2
},
'&:hover': { backgroundColor: DT.surfaceAlt }
}}
>
<TableCell>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(index + 1).padStart(2, '0')}
</Typography>
</TableCell>
<TableCell>
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color={BRAND} size={36}>
<MdPerson size={18} />
</AccentAvatar>
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{row.riderName}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.id}
</Typography>
</Stack>
</Stack>
</TableCell>
<TableCell align="center">
<Typography variant="body2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.orderCount}
</Typography>
</TableCell>
<TableCell align="center">
<MetricPill color="#10b981" icon={<MdStraighten size={11} />} label={`${Math.round(row.kms)} km`} tooltip="KMS" />
</TableCell>
<TableCell align="center">
<MetricPill
color={BRAND}
icon={<MdOutlineCurrencyRupee size={11} />}
label={formatNumberToRupees(row.revenue).replace('₹', '').trim()}
tooltip="Revenue"
/>
</TableCell>
<TableCell align="center">
<MetricPill
color="#6366f1"
icon={<MdPayments size={11} />}
label={formatNumberToRupees(row.fixedCost).replace('₹', '').trim()}
tooltip="Fixed Cost"
/>
</TableCell>
<TableCell align="center">
<MetricPill
color="#f59e0b"
icon={<MdRoute size={11} />}
label={formatNumberToRupees(row.varCost).replace('₹', '').trim()}
tooltip="Variable Cost"
/>
</TableCell>
<TableCell align="center">
<MetricPill
color="#94a3b8"
icon={<MdPayments size={11} />}
label={formatNumberToRupees(row.totalCost).replace('₹', '').trim()}
tooltip="Total Cost"
/>
</TableCell>
<TableCell align="center">
<MetricPill
color={isProfit ? '#10b981' : '#ef4444'}
icon={isProfit ? <MdTrendingUp size={11} /> : <MdTrendingDown size={11} />}
label={`${isProfit ? '+' : ''}${formatNumberToRupees(row.net).replace('₹', '').trim()}`}
tooltip="Net Profit"
/>
</TableCell>
<TableCell align="center">
<Chip
label={`${Math.abs(row.margin).toFixed(0)}%`}
color={isProfit ? 'success' : 'error'}
size="small"
sx={{ fontWeight: 700, minWidth: 60 }}
/>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
)}
</Paper>
{/* Date Filter Dialog */}
<DateFilterDialog
open={open}
onClose={() => setOpen(false)}
onSelect={(range) => {
setStartdate(range.startDate);
setEnddate(range.endDate);
setDatestatus(range.label);
}}
/>
</>
);
}

View File

@@ -1,355 +0,0 @@
import React, { useState, useEffect, Fragment } from 'react';
import {
Box,
Drawer,
IconButton,
Toolbar,
Typography,
AppBar,
useMediaQuery,
Divider,
List,
ListItem,
ListItemText,
useTheme,
ListItemAvatar,
Stack,
Button,
Checkbox,
Skeleton
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import SearchBar from 'components/nearle_components/SearchBar';
import { useQuery } from '@tanstack/react-query';
import { fetchRidersLogs } from 'pages/api/api';
import RiderLocationMap from './RiderLocationMap';
import MainCard from 'components/MainCard';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import dayjs from 'dayjs';
import error500 from 'assets/images/maintenance/Error500.png';
const drawerWidth = 350;
const RidersLogs = () => {
const theme = useTheme();
const isDesktop = useMediaQuery('(min-width:900px)');
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [open, setOpen] = useState(false);
const [selectedRiders, setSelectedRiders] = useState([]);
const [riderSearch, setRiderSearch] = useState('');
const appId = 1;
const {
data: riders,
isLoading: ridersIsLoading,
isFetching: riderIsFetching,
refetch: riderLogsRefetch,
error: riderLogsError
} = useQuery({
queryKey: [appId, dayjs().format('YYYY-MM-DD'), riderSearch],
queryFn: fetchRidersLogs,
refetchInterval: 5 * 60 * 1000
});
useEffect(() => {
// const sortedRiders = riders?.sort((a, b) => a.firstname.localeCompare(b.firstname));
setSelectedRiders(riders);
}, [riders]);
useEffect(() => {
console.log('selectedRiders', selectedRiders);
}, [selectedRiders]);
useEffect(() => {
setOpen(isDesktop);
}, [isDesktop]);
return (
<MainCard content={false}>
<Box sx={{ display: 'flex', width: '100%', height: '100%', position: 'relative' }}>
{/* Drawer */}
<Drawer
variant={isDesktop ? 'persistent' : 'temporary'}
open={open}
onClose={() => !isDesktop && setOpen(false)}
ModalProps={{ keepMounted: true }}
sx={{
'& .MuiDrawer-paper': {
width: isMobile ? '100vw' : drawerWidth,
maxWidth: isMobile ? '100vw' : drawerWidth,
position: 'absolute',
left: 0,
top: 0,
height: '100%',
overflowY: 'auto',
transition: 'transform 0.35s ease-in-out',
zIndex: 13
}
}}
>
{/* Search */}
<Box sx={{ position: 'sticky', top: 0, zIndex: 1 }}>
<SearchBar
value={riderSearch}
placeholder="Search Rider"
onChange={(e) => setRiderSearch(e.target.value)}
sx={{
height: 60,
bgcolor: 'white',
'& .MuiOutlinedInput-notchedOutline': {
borderBottom: '1px solid',
borderColor: theme.palette.secondary.light
}
}}
/>
<List>
<ListItem sx={{ cursor: 'pointer', '&:hover': { bgcolor: theme.palette.secondary.lighter }, bgcolor: 'white', mt: -1 }}>
<ListItemAvatar>
<Checkbox
checked={riders?.length == selectedRiders?.length}
onChange={(e) => {
if (e.target.checked) {
setSelectedRiders(riders);
}
}}
/>
</ListItemAvatar>
<ListItemText primary="All" />
</ListItem>
<Divider />
</List>
</Box>
{/* Rider List */}
<List>
{/* Individuals */}
{ridersIsLoading || riderIsFetching
? Array.from({ length: 10 }).map((_, index) => (
<Fragment key={index}>
<ListItem sx={{ py: 1.5, px: 2 }}>
<ListItemAvatar>
<Skeleton variant="circular" width={24} height={24} />
</ListItemAvatar>
<ListItemText
primary={<Skeleton variant="text" width="60%" height={22} />}
secondary={<Skeleton variant="text" width="40%" height={18} />}
/>
<Stack spacing={0.5} textAlign="right">
<Skeleton variant="text" width={50} height={18} />
<Skeleton variant="text" width={80} height={16} />
</Stack>
</ListItem>
<Divider />
</Fragment>
))
: !isMobile &&
riders?.map((row) => {
return (
<Fragment key={row.userid}>
<ListItem
sx={{
cursor: 'pointer',
py: 1,
px: 2,
borderRadius: 1,
'&:hover': { bgcolor: theme.palette.secondary.lighter }
}}
secondaryAction={
<Stack textAlign="right" spacing={0.5}>
<Typography variant="body2" noWrap sx={{ color: row.status == 'active' ? 'success.main' : 'error.main' }}>
{row.userid}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap>
{dayjs(row.logdate).format('DD/MM/YYYY hh:mm A')}
</Typography>
</Stack>
}
>
<ListItemAvatar>
<Checkbox
sx={{
color: row.status == 'active' ? 'green' : 'red',
'&.Mui-checked': {
color: row.status == 'active' ? 'green' : 'red'
}
}}
checked={
// INDIVIDUAL CHECKED CONDITION
selectedRiders?.length === 1 && selectedRiders[0]?.userid === row?.userid
}
onChange={(e) => {
if (e.target.checked) {
// SELECT ONE RIDER
setSelectedRiders([row]);
} else {
// UNCHECK -> SELECT ALL
setSelectedRiders(riders);
}
}}
/>
</ListItemAvatar>
<ListItemText
primary={
<Typography noWrap>
{row.username?.slice(0, 25) || ''}
{row.username?.length > 25 && '...'}
{/* {row.status === 'active' && <TaskAltIcon fontSize="small" color="success" sx={{ ml: 1 }} />} */}
</Typography>
}
secondary={
<Typography variant="caption" color="text.secondary" noWrap>
{row.contactno || '##########'}
</Typography>
}
/>
</ListItem>
<Divider />
</Fragment>
);
})}
</List>
{/* Mobile: rider rows rendered as app-style cards (same selection behaviour) */}
{isMobile && !ridersIsLoading && !riderIsFetching && (
<MobileCardList>
{riders?.map((row) => {
const isActive = row.status == 'active';
const isSelected = selectedRiders?.length === 1 && selectedRiders[0]?.userid === row?.userid;
return (
<MobileCard
key={row.userid}
accent={isActive ? '#10b981' : '#ef4444'}
selected={isSelected}
header={
<Stack direction="row" alignItems="flex-start" spacing={1}>
<Checkbox
sx={{
p: 0.5,
color: isActive ? 'green' : 'red',
'&.Mui-checked': { color: isActive ? 'green' : 'red' }
}}
checked={isSelected}
onChange={(e) => {
if (e.target.checked) {
setSelectedRiders([row]);
} else {
setSelectedRiders(riders);
}
}}
/>
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
<Typography noWrap sx={{ fontWeight: 600 }}>
{row.username?.slice(0, 25) || ''}
{row.username?.length > 25 && '...'}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap>
{row.contactno || '##########'}
</Typography>
</Box>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="User ID">
<Typography sx={{ fontSize: 13, fontWeight: 600, color: isActive ? 'success.main' : 'error.main' }} noWrap>
{row.userid}
</Typography>
</MobileField>
<MobileField label="Status" value={isActive ? 'Active' : 'Inactive'} />
<MobileField label="Last Log" value={dayjs(row.logdate).format('DD/MM/YYYY hh:mm A')} full />
</MobileFieldGrid>
</MobileCard>
);
})}
</MobileCardList>
)}
</Drawer>
{/* AppBar */}
<AppBar
elevation={0}
position="absolute"
sx={{
top: 0,
left: open && isDesktop ? `${drawerWidth}px` : 0,
width: open && isDesktop ? `calc(100% - ${drawerWidth}px)` : '100%',
transition: 'left 0.3s ease, width 0.3s ease',
backgroundColor: 'white',
borderBottom: '1px solid',
borderColor: theme.palette.secondary.light
}}
>
<Toolbar>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: '100%' }}>
<Stack direction="row" alignItems="center">
<IconButton color="primary" onClick={() => setOpen(!open)}>
<MenuIcon />
</IconButton>
<Typography variant="h5" color="primary" sx={{ ml: 2 }}>
Riders Locations
</Typography>
</Stack>
<Button
variant="outlined"
color="primary"
onClick={() => {
riderLogsRefetch();
}}
>
Refresh
</Button>
</Stack>
</Toolbar>
</AppBar>
{/* Map */}
<Box
sx={{
flexGrow: 1,
overflow: 'auto',
pt: '64px',
pl: open && isDesktop ? `${drawerWidth}px` : 0,
transition: 'padding-left 0.3s ease',
minHeight: '80vh'
}}
>
{(ridersIsLoading || riderIsFetching) && (
<Box position="relative" width="100%" height="80vh" display="grid" placeItems="center">
{/* <CircularLoader /> */}
<Skeleton
variant="rectangular"
width="100%"
height="100%"
animation="wave"
sx={{
position: 'absolute',
top: 0,
left: 0,
borderRadius: 1,
zIndex: 1
}}
/>
</Box>
)}
{selectedRiders?.length > 0 && <RiderLocationMap riderLocations={selectedRiders} />}
{riderLogsError && (
<Box sx={{ width: '100% ', height: '100%' }}>
<img src={error500} alt="mantis" style={{ height: '100%', width: '100%' }} />
</Box>
)}
</Box>
</Box>
</MainCard>
);
};
export default RidersLogs;

View File

@@ -86,8 +86,8 @@ const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26'); const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55'); const edge = (c) => a(c, '55');
const BRAND = '#662582'; const BRAND = '#C01227';
const BRAND_LIGHT = '#9255AB'; const BRAND_LIGHT = '#D25463';
const SoftPaper = (props) => ( const SoftPaper = (props) => (
<Paper <Paper
@@ -265,17 +265,9 @@ export default function RidersSummary() {
const getuserdeliverylogs = async (userid) => { const getuserdeliverylogs = async (userid) => {
setRouteLoading(true); setRouteLoading(true);
try { try {
// /deliveries/getdeliveries treats applocationid=0 differently from a
// real location id — when appId===0 ("All") the backend expects the
// logged-in operator's userid via appuserid instead. Mirrors the
// branching in api.js#fetchDeliveries.
const loggedInUserId = typeof window !== 'undefined' ? localStorage.getItem('userid') || 0 : 0;
const scopeParam = appId === 0
? `appuserid=${loggedInUserId}`
: `applocationid=${appId}`;
const url = const url =
`${process.env.REACT_APP_URL}/deliveries/getdeliveries/` + `${process.env.REACT_APP_URL}/deliveries/getdeliveries/` +
`?${scopeParam}` + `?applocationid=${appId}` +
`&status=all` + `&status=all` +
`&fromdate=${startdate}` + `&fromdate=${startdate}` +
`&todate=${enddate}` + `&todate=${enddate}` +
@@ -456,7 +448,7 @@ export default function RidersSummary() {
bgcolor: '#ffffff', bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' }, '&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }, '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` } '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}} }}
/> />

View File

@@ -31,9 +31,12 @@ import {
FormLabel, FormLabel,
DialogActions, DialogActions,
useMediaQuery, useMediaQuery,
useTheme useTheme,
Paper
} from '@mui/material'; } from '@mui/material';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import { MdPayments } from 'react-icons/md';
import { DT, tint } from 'themes/dt/tokens';
import { Autocomplete as Autocomplete1 } from '@mui/material'; import { Autocomplete as Autocomplete1 } from '@mui/material';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'; import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
@@ -202,7 +205,7 @@ const Requests = () => {
const [currenttenantid] = useState(''); const [currenttenantid] = useState('');
const [latlong, setLatlong] = useState({}); const [latlong, setLatlong] = useState({});
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY); Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
// const [alertmessage, setAlertmessage] = useState(''); const [alertmessage, setAlertmessage] = useState('');
// const [toast, setToast] = useState(false); // const [toast, setToast] = useState(false);
const [rolesarr, setRolesarr] = useState([]); const [rolesarr, setRolesarr] = useState([]);
const [roleslist] = useState([]); const [roleslist] = useState([]);
@@ -215,6 +218,12 @@ const Requests = () => {
const [refno, setRefno] = useState(''); const [refno, setRefno] = useState('');
const [requestor, setRequestor] = useState(''); const [requestor, setRequestor] = useState('');
const [bankname, setBankname] = useState(''); const [bankname, setBankname] = useState('');
const [amount, setAmount] = useState('');
const [accountno, setAccountno] = useState('');
const [ifsc, setIfsc] = useState('');
const [reason, setReason] = useState('');
const [expandopen, setExpandopen] = useState('');
const [editexpandopen, setEditexpandopen] = useState('');
useEffect(() => { useEffect(() => {
setRolesarr([ setRolesarr([
@@ -902,7 +911,7 @@ const Requests = () => {
<MobileCardList scroll> <MobileCardList scroll>
{loading && {loading &&
[0, 1, 2, 3, 4].map((item) => ( [0, 1, 2, 3, 4].map((item) => (
<MobileCard key={item} accent="#662582"> <MobileCard key={item} accent="#C01227">
<Stack direction="row" alignItems="center" spacing={1}> <Stack direction="row" alignItems="center" spacing={1}>
<Skeleton variant="circular" width={32} height={32} /> <Skeleton variant="circular" width={32} height={32} />
<Stack sx={{ flex: 1 }}> <Stack sx={{ flex: 1 }}>
@@ -923,13 +932,13 @@ const Requests = () => {
return ( return (
<MobileCard <MobileCard
key={row.sno} key={row.sno}
accent="#662582" accent="#C01227"
selected={isItemSelected} selected={isItemSelected}
onClick={(event) => handleClick(event, row.sno)} onClick={(event) => handleClick(event, row.sno)}
header={ header={
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}> <Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}> <Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<Avatar sx={{ width: 32, height: 32, bgcolor: '#66258218', color: '#662582', fontSize: 13 }}> <Avatar sx={{ width: 32, height: 32, bgcolor: '#C0122718', color: '#C01227', fontSize: 13 }}>
{row.requestor ? String(row.requestor).charAt(0).toUpperCase() : '#'} {row.requestor ? String(row.requestor).charAt(0).toUpperCase() : '#'}
</Avatar> </Avatar>
<Stack sx={{ minWidth: 0 }}> <Stack sx={{ minWidth: 0 }}>
@@ -942,7 +951,7 @@ const Requests = () => {
</Stack> </Stack>
</Stack> </Stack>
{row.amount != null && ( {row.amount != null && (
<Chip label={row.amount} size="small" sx={{ bgcolor: '#66258218', color: '#662582', fontWeight: 700 }} /> <Chip label={row.amount} size="small" sx={{ bgcolor: '#C0122718', color: '#C01227', fontWeight: 700 }} />
)} )}
</Stack> </Stack>
} }
@@ -1813,8 +1822,6 @@ const Requests = () => {
const [searchword, setSearchword] = useState(''); const [searchword, setSearchword] = useState('');
const [dialogopen, setDialogopen] = useState(false); const [dialogopen, setDialogopen] = useState(false);
// const [expandopen, setExpandopen] = React.useState('');
// const setinitial = (val)=>{ // const setinitial = (val)=>{
// if(val){ // if(val){
@@ -1937,6 +1944,16 @@ const Requests = () => {
item item
xs={12} xs={12}
// sx={{ mb: -2.25 }} // sx={{ mb: -2.25 }}
>
<Paper
sx={{
p: 2.5,
borderRadius: DT.radiusCard + 'px',
boxShadow: DT.shadowSoft,
border: '1px solid',
borderColor: DT.borderSubtle,
background: `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)`
}}
> >
<Stack <Stack
direction={{ xs: 'column', sm: 'row' }} direction={{ xs: 'column', sm: 'row' }}
@@ -1944,20 +1961,36 @@ const Requests = () => {
alignItems={{ xs: 'stretch', sm: 'center' }} alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={{ xs: 1.5, sm: 0 }} spacing={{ xs: 1.5, sm: 0 }}
> >
<Stack direction="row" spacing={1.5} alignItems="center">
<Avatar sx={{ width: 48, height: 48, bgcolor: DT.brand }}>
<MdPayments size={22} />
</Avatar>
<Typography variant="h3">Payment Requests</Typography> <Typography variant="h3">Payment Requests</Typography>
</Stack>
<Button <Button
variant="contained" variant="contained"
fullWidth={isMobile} fullWidth={isMobile}
onClick={() => { onClick={() => {
// setDialogopen(true) setDialogopen(true);
}} }}
> >
Create Request Create Request
</Button> </Button>
</Stack> </Stack>
</Paper>
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<Box sx={{ overflow: 'auto', border: 1, borderColor: 'grey.200', borderRadius: 2, backgroundColor: '#fff', minHeight: 400 }}> <Box
sx={{
overflow: 'auto',
border: '1px solid',
borderColor: DT.borderSubtle,
borderRadius: DT.radiusCard + 'px',
boxShadow: DT.shadowSoft,
backgroundColor: DT.surface,
minHeight: 400
}}
>
{/* <Box {/* <Box
sx={{ sx={{
p: 1, p: 1,

View File

@@ -269,7 +269,7 @@ export default function RiderSubstitution({
bgcolor: BRAND, bgcolor: BRAND,
color: '#fff', color: '#fff',
'&:hover': { '&:hover': {
bgcolor: '#4D1C61' bgcolor: '#910E1D'
} }
} }
} }
@@ -657,7 +657,7 @@ export default function RiderSubstitution({
fontWeight: 700, fontWeight: 700,
textTransform: 'none', textTransform: 'none',
'&:hover': { '&:hover': {
bgcolor: '#4D1C61' bgcolor: '#910E1D'
} }
}} }}
> >

View File

@@ -2,7 +2,9 @@ import { useEffect, useState } from 'react';
// material-ui // material-ui
import { Box, Button, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material'; import { Avatar, Box, Button, Grid, InputLabel, MenuItem, Paper, Select, Stack, TextField, Typography, useMediaQuery, useTheme } from '@mui/material';
import { MdDirectionsBike } from 'react-icons/md';
import { DT, tint } from 'themes/dt/tokens';
// third-party // third-party
// import { PatternFormat } from 'react-number-format'; // import { PatternFormat } from 'react-number-format';
@@ -113,12 +115,6 @@ const Createrider = () => {
}); });
}; };
useEffect(() => {
if (selectedImage) {
setAvatar(URL.createObjectURL(selectedImage));
}
}, [selectedImage]);
const { ref: materialRef } = usePlacesWidget({ const { ref: materialRef } = usePlacesWidget({
apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY, apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY,
onPlaceSelected: (place) => { onPlaceSelected: (place) => {
@@ -160,14 +156,6 @@ const Createrider = () => {
}); });
const createprofile = async () => { 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) { if (!firstname) {
opentoast('Fill Full name'); opentoast('Fill Full name');
} else if (!mobilenumber) { } else if (!mobilenumber) {
@@ -260,16 +248,27 @@ const Createrider = () => {
<Box sx={{ p: { xs: 1.5, md: 3 } }}> <Box sx={{ p: { xs: 1.5, md: 3 } }}>
<Grid item xs={12} sx={{ mb: 2 }}> <Grid item xs={12} sx={{ mb: 2 }}>
<Stack <Paper
direction={{ xs: 'column', sm: 'row' }} sx={{
justifyContent="space-between" p: 2.5,
alignItems={{ xs: 'flex-start', sm: 'center' }} borderRadius: DT.radiusCard + 'px',
spacing={1} boxShadow: DT.shadowSoft,
border: '1px solid',
borderColor: DT.borderSubtle,
background: `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)`
}}
> >
<Stack direction="row" spacing={1.5} alignItems="center">
<Avatar sx={{ width: 48, height: 48, bgcolor: DT.brand }}>
<MdDirectionsBike size={22} />
</Avatar>
<Typography variant="h3">Create Rider</Typography> <Typography variant="h3">Create Rider</Typography>
</Stack> </Stack>
</Paper>
</Grid> </Grid>
<MainCard> <MainCard
sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }}
>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12}> <Grid item xs={12}>
<MainCard <MainCard

View File

@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { import {
Avatar,
Box, Box,
Button, Button,
Grid, Grid,
@@ -17,6 +18,8 @@ import {
useMediaQuery, useMediaQuery,
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import { MdDirectionsBike } from 'react-icons/md';
import { DT, tint } from 'themes/dt/tokens';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
@@ -106,9 +109,6 @@ const EditRider = () => {
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`) .get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
.then((res) => { .then((res) => {
console.log(res); console.log(res);
if (res.data.status) {
setTenantinfo(res.data.details);
}
setLoading(false); setLoading(false);
}) })
.catch((err) => { .catch((err) => {
@@ -231,12 +231,6 @@ const EditRider = () => {
}); });
}; };
useEffect(() => {
if (selectedImage) {
setAvatar(URL.createObjectURL(selectedImage));
}
}, [selectedImage]);
const { ref: materialRef } = usePlacesWidget({ const { ref: materialRef } = usePlacesWidget({
apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY, apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY,
onPlaceSelected: (place) => { onPlaceSelected: (place) => {
@@ -265,7 +259,6 @@ const EditRider = () => {
setCity(city1 || ''); setCity(city1 || '');
setState(state1 || ''); setState(state1 || '');
setZipcode(zipcode1 || '');
setSuburb(suburb1 || ''); setSuburb(suburb1 || '');
// setAddress(place.formatted_address) // setAddress(place.formatted_address)
@@ -328,7 +321,7 @@ const EditRider = () => {
autoHideDuration: 2000 autoHideDuration: 2000
}); });
setRiderdata(null); setRiderdata(null);
navigate('/nearle/riders'); navigate('/doormile/riders');
setLoading(false); setLoading(false);
} else { } else {
enqueueSnackbar('Update Failed', { enqueueSnackbar('Update Failed', {
@@ -349,22 +342,33 @@ const EditRider = () => {
</> </>
)} )}
<MainCard <MainCard
sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }}
title={ title={
<Stack <Stack
direction={{ xs: 'column', sm: 'row' }} direction={{ xs: 'column', sm: 'row' }}
justifyContent="space-between" justifyContent="space-between"
alignItems={{ xs: 'stretch', sm: 'center' }} alignItems={{ xs: 'stretch', sm: 'center' }}
spacing={{ xs: 1.5, sm: 0 }} spacing={{ xs: 1.5, sm: 0 }}
sx={{ backgroundColor: 'secondary.lighter', width: '100%', height: '100%', p: 2 }} sx={{
width: '100%',
height: '100%',
p: 2,
background: `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D25463')} 100%)`
}}
> >
<Typography variant="h3">Edit Rider </Typography> <Stack direction="row" spacing={1.5} alignItems="center">
<Avatar sx={{ width: 40, height: 40, bgcolor: DT.brand }}>
<MdDirectionsBike size={20} />
</Avatar>
<Typography variant="h3">Edit Rider</Typography>
</Stack>
<Button <Button
startIcon={<ArrowBackIcon />} startIcon={<ArrowBackIcon />}
variant="outlined" variant="outlined"
fullWidth={isMobile} fullWidth={isMobile}
onClick={() => { onClick={() => {
setRiderdata(null); setRiderdata(null);
navigate('/nearle/riders'); navigate('/doormile/riders');
}} }}
> >
Back to Riders Back to Riders
@@ -664,16 +668,6 @@ const EditRider = () => {
starttime: val.starttime, starttime: val.starttime,
endtime: val.endtime endtime: val.endtime
}); });
setBasefare(val.basefare);
setAdditionalkms(val.additionalkm);
setOthercharges(val.additionalcharges);
setShift(val);
} else {
setBasefare('');
setAdditionalkms('');
setOthercharges('');
setShift({});
} }
}} }}
freeSolo freeSolo
@@ -694,7 +688,6 @@ const EditRider = () => {
...riderdata, ...riderdata,
basefare: e.target.value basefare: e.target.value
}); });
setBasefare(e.target.value);
}} }}
value={riderdata?.basefare} value={riderdata?.basefare}
autoComplete="off" autoComplete="off"
@@ -717,7 +710,6 @@ const EditRider = () => {
...riderdata, ...riderdata,
additionalkm: e.target.value additionalkm: e.target.value
}); });
setAdditionalkms(e.target.value);
}} }}
value={riderdata?.additionalkm} value={riderdata?.additionalkm}
autoComplete="off" autoComplete="off"
@@ -740,7 +732,6 @@ const EditRider = () => {
...riderdata, ...riderdata,
additionalcharges: e.target.value additionalcharges: e.target.value
}); });
setOthercharges(e.target.value);
}} }}
value={riderdata?.additionalcharges} value={riderdata?.additionalcharges}
autoComplete="off" autoComplete="off"
@@ -777,7 +768,6 @@ const EditRider = () => {
...riderdata, ...riderdata,
accountno: e.target.value accountno: e.target.value
}); });
setAccountno(e.target.value);
}} }}
autoComplete="off" autoComplete="off"
/> />
@@ -798,7 +788,6 @@ const EditRider = () => {
...riderdata, ...riderdata,
accountname: e.target.value accountname: e.target.value
}); });
setAccountname(e.target.value);
}} }}
autoComplete="off" autoComplete="off"
/> />
@@ -823,11 +812,7 @@ const EditRider = () => {
...riderdata, ...riderdata,
accounttype: val.label accounttype: val.label
}); });
setAccount(val);
setAccountType(val.label);
// fetchroles(val.tenantid); // fetchroles(val.tenantid);
} else {
setAccount({});
} }
}} }}
freeSolo freeSolo
@@ -849,7 +834,6 @@ const EditRider = () => {
...riderdata, ...riderdata,
bankname: e.target.value bankname: e.target.value
}); });
setBankname(e.target.value);
}} }}
autoComplete="off" autoComplete="off"
/> />
@@ -870,7 +854,6 @@ const EditRider = () => {
...riderdata, ...riderdata,
ifsccode: e.target.value ifsccode: e.target.value
}); });
setIfsc(e.target.value);
}} }}
autoComplete="off" autoComplete="off"
/> />
@@ -891,7 +874,6 @@ const EditRider = () => {
...riderdata, ...riderdata,
branch: e.target.value branch: e.target.value
}); });
setBranch(e.target.value);
}} }}
autoComplete="off" autoComplete="off"
/> />
@@ -924,7 +906,6 @@ const EditRider = () => {
onChange={(e, val) => { onChange={(e, val) => {
if (val) { if (val) {
console.log('vehi', val); console.log('vehi', val);
setVehicle(val);
setRiderdata({ setRiderdata({
...riderdata, ...riderdata,
vehiclename: val.label, vehiclename: val.label,
@@ -932,8 +913,6 @@ const EditRider = () => {
}); });
// fetchroles(val.tenantid); // fetchroles(val.tenantid);
} else {
setVehicle({});
} }
}} }}
freeSolo freeSolo
@@ -974,7 +953,6 @@ const EditRider = () => {
...riderdata, ...riderdata,
model: e.target.value model: e.target.value
}); });
setModelyear(e.target.value);
}} }}
value={riderdata?.model} value={riderdata?.model}
autoComplete="off" autoComplete="off"
@@ -995,7 +973,6 @@ const EditRider = () => {
...riderdata, ...riderdata,
color: e.target.value color: e.target.value
}); });
setVehiclecolor(e.target.value);
}} }}
value={riderdata?.color} value={riderdata?.color}
autoComplete="off" autoComplete="off"
@@ -1054,7 +1031,6 @@ const EditRider = () => {
label="Date" label="Date"
value={dayjs(riderdata?.insurancedate)} value={dayjs(riderdata?.insurancedate)}
onChange={(e) => { onChange={(e) => {
setExpirydate(dayjs(e.$d).format('YYYY-MM-DD 00:00:00'));
setRiderdata({ setRiderdata({
...riderdata, ...riderdata,
insurancedate: dayjs(e.$d).format('YYYY-MM-DD 00:00:00') insurancedate: dayjs(e.$d).format('YYYY-MM-DD 00:00:00')
@@ -1078,16 +1054,16 @@ const EditRider = () => {
sx={{ sx={{
position: 'sticky', position: 'sticky',
bottom: 0, bottom: 0,
backgroundColor: 'secondary.lighter', backgroundColor: DT.surfaceAlt,
p: 2, p: 2,
zIndex: 10, zIndex: 10,
border: ' 1px solid ', border: '1px solid',
borderColor: '#E6EBF1', borderColor: DT.borderSubtle,
borderTop: 'none' borderTop: 'none'
}} }}
> >
<Stack direction={{ xs: 'column-reverse', sm: 'row' }} justifyContent="flex-end" spacing={2}> <Stack direction={{ xs: 'column-reverse', sm: 'row' }} justifyContent="flex-end" spacing={2}>
<Button startIcon={<ArrowBackIcon />} variant="outlined" fullWidth={isMobile} onClick={() => navigate('/nearle/riders')}> <Button startIcon={<ArrowBackIcon />} variant="outlined" fullWidth={isMobile} onClick={() => navigate('/doormile/riders')}>
Back to Riders Back to Riders
</Button> </Button>
<Button <Button

View File

@@ -77,7 +77,7 @@ import RiderSubstitution from './RiderSubstitution';
// ============================================================================ // ============================================================================
// Design tokens — shared with the deliveries / tenants / customers pages so // Design tokens — shared with the deliveries / tenants / customers pages so
// every surface (header, KPI tiles, table, badges, dialog) speaks the same // every surface (header, KPI tiles, table, badges, dialog) speaks the same
// visual language. Brand purple `#662582` is the canonical primary; status // visual language. Brand purple `#C01227` is the canonical primary; status
// colours are semantic and distinct from the brand. // colours are semantic and distinct from the brand.
// ============================================================================ // ============================================================================
const DT = { const DT = {
@@ -100,7 +100,7 @@ const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26'); const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55'); const edge = (c) => a(c, '55');
const BRAND = '#662582'; const BRAND = '#C01227';
const SoftPaper = (props) => ( const SoftPaper = (props) => (
<Paper <Paper
@@ -145,9 +145,9 @@ const STATUS_META = {
// "ALL" view and emerald for "Active" so the colour matches the count's meaning. // "ALL" view and emerald for "Active" so the colour matches the count's meaning.
const TAB_META = [ const TAB_META = [
{ key: 0, label: 'All Riders', color: BRAND, icon: MdGroups, countKey: 'total' }, { key: 0, label: 'All Riders', color: BRAND, icon: MdGroups, countKey: 'total' },
{ key: 1, label: 'Active', color: '#10b981', icon: MdCheckCircle, countKey: 'active' }, { key: 1, label: 'Active', color: BRAND, icon: MdCheckCircle, countKey: 'active' },
{ key: 2, label: 'Substitutes', color: '#8b5cf6', icon: MdTwoWheeler, countKey: 'substitute' }, { key: 2, label: 'Substitutes', color: BRAND, icon: MdTwoWheeler, countKey: 'substitute' },
{ key: 3, label: 'Substitution History', color: '#f59e0b', icon: MdAccessTime, countKey: 'history' } { key: 3, label: 'Substitution History', color: BRAND, icon: MdAccessTime, countKey: 'history' }
]; ];
const KPI_META = (summary) => [ const KPI_META = (summary) => [
@@ -763,7 +763,7 @@ const Riders = () => {
bgcolor: '#ffffff', bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' }, '&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }, '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` } '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}} }}
/> />
@@ -813,7 +813,7 @@ const Riders = () => {
bgcolor: BRAND, bgcolor: BRAND,
color: '#fff', color: '#fff',
'&:hover': { '&:hover': {
bgcolor: '#4D1C61' bgcolor: '#910E1D'
} }
} }
} }
@@ -1244,7 +1244,7 @@ const Riders = () => {
'&:hover': { bgcolor: BRAND, color: '#fff' } '&:hover': { bgcolor: BRAND, color: '#fff' }
}} }}
onClick={() => { onClick={() => {
navigate('/nearle/riders/edit', { state: { riderdata: row } }); navigate('/doormile/riders/edit', { state: { riderdata: row } });
}} }}
> >
<MdEdit size={14} /> <MdEdit size={14} />
@@ -1696,7 +1696,7 @@ const Riders = () => {
'&:hover': { bgcolor: BRAND, color: '#fff' } '&:hover': { bgcolor: BRAND, color: '#fff' }
}} }}
onClick={() => { onClick={() => {
navigate('/nearle/riders/edit', { state: { riderdata: row } }); navigate('/doormile/riders/edit', { state: { riderdata: row } });
}} }}
> >
<MdEdit size={14} /> <MdEdit size={14} />
@@ -1914,7 +1914,7 @@ const Riders = () => {
fontWeight: 600, fontWeight: 600,
bgcolor: BRAND, bgcolor: BRAND,
color: '#fff', color: '#fff',
'&:hover': { bgcolor: '#4D1C61' } '&:hover': { bgcolor: '#910E1D' }
}} }}
> >
Save Changes Save Changes

View File

@@ -31,8 +31,8 @@ const DT = {
surface: '#ffffff', surface: '#ffffff',
surfaceAlt: '#f8fafc' surfaceAlt: '#f8fafc'
}; };
const BRAND = '#662582'; const BRAND = '#C01227';
const BRAND_LIGHT = '#9255AB'; const BRAND_LIGHT = '#D25463';
const tint = (c) => `${c}08`; const tint = (c) => `${c}08`;
const soft = (c) => `${c}18`; const soft = (c) => `${c}18`;
@@ -143,7 +143,7 @@ const ViewProfile = () => {
color: '#fff', color: '#fff',
fontSize: { xs: 24, md: 28 }, fontSize: { xs: 24, md: 28 },
fontWeight: 700, fontWeight: 700,
boxShadow: '0 10px 24px rgba(102, 37, 130, 0.35)' boxShadow: '0 10px 24px rgba(192, 18, 39, 0.35)'
}} }}
> >
{initialsOf(fullname)} {initialsOf(fullname)}

View File

@@ -11,7 +11,6 @@ import Loadable from 'components/Loadable';
// const AuthForgotPassword = Loadable(lazy(() => import('pages/auth/forgot-password'))); // const AuthForgotPassword = Loadable(lazy(() => import('pages/auth/forgot-password')));
// const AuthCheckMail = Loadable(lazy(() => import('pages/auth/check-mail'))); // const AuthCheckMail = Loadable(lazy(() => import('pages/auth/check-mail')));
// const AuthResetPassword = Loadable(lazy(() => import('pages/auth/reset-password'))); // const AuthResetPassword = Loadable(lazy(() => import('pages/auth/reset-password')));
// const AuthCodeVerification = Loadable(lazy(() => import('pages/auth/code-verification')));
const Login = Loadable(lazy(() => import('pages/nearle/login'))); const Login = Loadable(lazy(() => import('pages/nearle/login')));
// ==============================|| AUTH ROUTING ||============================== // // ==============================|| AUTH ROUTING ||============================== //
@@ -59,10 +58,6 @@ const LoginRoutes = {
// { // {
// path: 'reset-password', // path: 'reset-password',
// element: <AuthResetPassword /> // element: <AuthResetPassword />
// },
// {
// path: 'code-verification',
// element: <AuthCodeVerification />
// } // }
] ]
} }

View File

@@ -17,17 +17,10 @@ const MaintenanceComingSoon = Loadable(lazy(() => import('pages/maintenance/comi
const Login = Loadable(lazy(() => import('pages/nearle/login1'))); const Login = Loadable(lazy(() => import('pages/nearle/login1')));
// const Dashboard = Loadable(lazy(() => import('pages/nearle/dashboard'))); // const Dashboard = Loadable(lazy(() => import('pages/nearle/dashboard')));
const Tenants = Loadable(lazy(() => import('pages/nearle/clients/Tenants')));
const ClientsPricing = Loadable(lazy(() => import('pages/nearle/clientPricing/clientPricing')));
const Orders = Loadable(lazy(() => import('pages/nearle/orders/orders'))); const Orders = Loadable(lazy(() => import('pages/nearle/orders/orders')));
const OrdersPreview = Loadable(lazy(() => import('pages/nearle/orders/OrdersPreview'))); const OrdersPreview = Loadable(lazy(() => import('pages/nearle/orders/OrdersPreview')));
const Deliveries = Loadable(lazy(() => import('pages/nearle/deliveries/deliveries'))); const Deliveries = Loadable(lazy(() => import('pages/nearle/deliveries/deliveries')));
const Customers = Loadable(lazy(() => import('pages/nearle/customers/customers')));
const Invoice = Loadable(lazy(() => import('pages/nearle/invoice/invoice')));
const InvoicePreview = Loadable(lazy(() => import('../pages/nearle/invoice/invoicePreview')));
const Details = Loadable(lazy(() => import('pages/nearle/orders/details'))); const Details = Loadable(lazy(() => import('pages/nearle/orders/details')));
@@ -37,14 +30,11 @@ const Createorder1 = Loadable(lazy(() => import('pages/nearle/orders/createorder
const MultipleOrders = Loadable(lazy(() => import('pages/nearle/orders/multipleOrders'))); const MultipleOrders = Loadable(lazy(() => import('pages/nearle/orders/multipleOrders')));
const Createclient = Loadable(lazy(() => import('pages/nearle/clients/createclient'))); const Createclient = Loadable(lazy(() => import('pages/nearle/clients/createclient')));
const CreateCustomer = Loadable(lazy(() => import('pages/nearle/clients/createCustomer')));
const Requests = Loadable(lazy(() => import('pages/nearle/requests/requests'))); const Requests = Loadable(lazy(() => import('pages/nearle/requests/requests')));
const OrdersSummary = Loadable(lazy(() => import('pages/nearle/reports/ordersSummary'))); const OrdersSummary = Loadable(lazy(() => import('pages/nearle/reports/ordersSummary')));
const OrdersDetails = Loadable(lazy(() => import('pages/nearle/reports/ordersDetails'))); const OrdersDetails = Loadable(lazy(() => import('pages/nearle/reports/ordersDetails')));
const RidersSummary = Loadable(lazy(() => import('pages/nearle/reports/ridersSummary'))); const RidersSummary = Loadable(lazy(() => import('pages/nearle/reports/ridersSummary')));
const RidersLogs = Loadable(lazy(() => import('pages/nearle/reports/ridersLogs')));
const Profitability = Loadable(lazy(() => import('pages/nearle/reports/profitability')));
const Riders = Loadable(lazy(() => import('pages/nearle/riders/riders'))); const Riders = Loadable(lazy(() => import('pages/nearle/riders/riders')));
const Createrider = Loadable(lazy(() => import('pages/nearle/riders/createrider'))); const Createrider = Loadable(lazy(() => import('pages/nearle/riders/createrider')));
@@ -52,7 +42,6 @@ const EditRider = Loadable(lazy(() => import('pages/nearle/riders/editRider')));
const Dispatch = Loadable(lazy(() => import('pages/nearle/dispatch/Dispatch'))); const Dispatch = Loadable(lazy(() => import('pages/nearle/dispatch/Dispatch')));
const DispatchPreview = Loadable(lazy(() => import('pages/nearle/dispatch/Preview'))); const DispatchPreview = Loadable(lazy(() => import('pages/nearle/dispatch/Preview')));
// ==============================|| MAIN ROUTING ||============================== // // ==============================|| MAIN ROUTING ||============================== //
const MainRoutes = { const MainRoutes = {
@@ -67,7 +56,7 @@ const MainRoutes = {
), ),
children: [ children: [
{ {
path: 'nearle', path: 'doormile',
children: [ children: [
{ {
path: 'orders', path: 'orders',
@@ -81,36 +70,6 @@ const MainRoutes = {
path: 'deliveries', path: 'deliveries',
element: <Deliveries /> element: <Deliveries />
}, },
{
path: 'tenants',
element: <Tenants />
},
{
path: 'pricing',
element: <ClientsPricing />
},
{
path: 'customers',
element: <Customers />
},
{
path: 'invoice',
children: [
{
index: true,
element: <Invoice />
},
{
path: 'preview',
element: <InvoicePreview />
}
]
},
{
path: 'invoice/preview',
element: <InvoicePreview />
},
{ {
path: 'requests', path: 'requests',
element: <Requests /> element: <Requests />
@@ -144,10 +103,6 @@ const MainRoutes = {
path: 'clients/create', path: 'clients/create',
element: <Createclient /> element: <Createclient />
}, },
{
path: 'customer/create',
element: <CreateCustomer />
},
{ {
path: 'reports', path: 'reports',
children: [ children: [
@@ -162,14 +117,6 @@ const MainRoutes = {
{ {
path: 'riderssummary', path: 'riderssummary',
element: <RidersSummary /> element: <RidersSummary />
},
{
path: 'riderslogs',
element: <RidersLogs />
},
{
path: 'profitability',
element: <Profitability />
} }
] ]
}, },
@@ -221,9 +168,8 @@ const MainRoutes = {
element: <MaintenanceComingSoon /> element: <MaintenanceComingSoon />
} }
] ]
}, }
] ]
}; };
export default MainRoutes; export default MainRoutes;

View File

@@ -1,66 +0,0 @@
import { useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Button, Grid, Stack, Typography } from '@mui/material';
// third-party
import OtpInput from 'react18-input-otp';
// project import
import AnimateButton from 'components/@extended/AnimateButton';
import { ThemeMode } from 'config';
// ============================|| STATIC - CODE VERIFICATION ||============================ //
const AuthCodeVerification = () => {
const theme = useTheme();
const [otp, setOtp] = useState();
const borderColor = theme.palette.mode === ThemeMode.DARK ? theme.palette.grey[200] : theme.palette.grey[300];
return (
<Grid container spacing={3}>
<Grid item xs={12}>
<OtpInput
value={otp}
onChange={(otp) => setOtp(otp)}
numInputs={4}
containerStyle={{ justifyContent: 'space-between' }}
inputStyle={{
width: '100%',
margin: '8px',
padding: '10px',
border: `1px solid ${borderColor}`,
borderRadius: 4,
':hover': {
borderColor: theme.palette.primary.main
}
}}
focusStyle={{
outline: 'none',
boxShadow: theme.customShadows.primary,
border: `1px solid ${theme.palette.primary.main}`
}}
/>
</Grid>
<Grid item xs={12}>
<AnimateButton>
<Button disableElevation fullWidth size="large" type="submit" variant="contained">
Continue
</Button>
</AnimateButton>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="baseline">
<Typography>Did not receive the email? Check your spam filter, or</Typography>
<Typography variant="body1" sx={{ minWidth: 85, ml: 2, textDecoration: 'none', cursor: 'pointer' }} color="primary">
Resend code
</Typography>
</Stack>
</Grid>
</Grid>
);
};
export default AuthCodeVerification;

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