initialisation on the doormile express console with astryx
This commit is contained in:
@@ -15,11 +15,11 @@ This skill is the **knowledge** Claude needs about the system (what each page do
|
||||
|
||||
## 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.
|
||||
- **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.
|
||||
- **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.
|
||||
- **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).
|
||||
@@ -70,16 +70,16 @@ flowchart TD
|
||||
Sidebar --> Invoice
|
||||
Sidebar --> ReportsHub
|
||||
|
||||
Dispatch[/"/nearle/dispatch<br/>Live Map · Riders · Batches"/]:::core
|
||||
Orders[/"/nearle/orders<br/>Orders Dashboard"/]:::core
|
||||
Deliveries[/"/nearle/deliveries<br/>Dispatched Deliveries"/]:::core
|
||||
Tenants[/"/nearle/tenants<br/>Client/Tenant Management"/]:::core
|
||||
Pricing[/"/nearle/pricing<br/>Pricing Matrix (master-detail)"/]:::core
|
||||
Customers[/"/nearle/customers<br/>Customer Directory"/]:::core
|
||||
Riders[/"/nearle/riders<br/>Rider Pool"/]:::core
|
||||
Invoice[/"/nearle/invoice<br/>Billing"/]:::core
|
||||
Requests[/"/nearle/requests<br/>Expense Approvals"/]:::core
|
||||
ReportsHub[/"/nearle/reports/*<br/>BI Suite"/]:::core
|
||||
Dispatch[/"/doormile/dispatch<br/>Live Map · Riders · Batches"/]:::core
|
||||
Orders[/"/doormile/orders<br/>Orders Dashboard"/]:::core
|
||||
Deliveries[/"/doormile/deliveries<br/>Dispatched Deliveries"/]:::core
|
||||
Tenants[/"/doormile/tenants<br/>Client/Tenant Management"/]:::core
|
||||
Pricing[/"/doormile/pricing<br/>Pricing Matrix (master-detail)"/]:::core
|
||||
Customers[/"/doormile/customers<br/>Customer Directory"/]:::core
|
||||
Riders[/"/doormile/riders<br/>Rider Pool"/]:::core
|
||||
Invoice[/"/doormile/invoice<br/>Billing"/]:::core
|
||||
Requests[/"/doormile/requests<br/>Expense Approvals"/]:::core
|
||||
ReportsHub[/"/doormile/reports/*<br/>BI Suite"/]:::core
|
||||
|
||||
OrdersCreate[/"/orders/create"/]:::sub
|
||||
OrdersMulti[/"/orders/createorders"/]:::sub
|
||||
@@ -164,12 +164,12 @@ flowchart TD
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant U as Operator
|
||||
participant O as /nearle/orders
|
||||
participant O as /doormile/orders
|
||||
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 J as jupiter.nearle.app
|
||||
participant D as /nearle/deliveries
|
||||
participant D as /doormile/deliveries
|
||||
participant F as FCM (rider device)
|
||||
|
||||
U->>O: Select pending orders (checkbox)
|
||||
@@ -327,29 +327,29 @@ sequenceDiagram
|
||||
| Route | File | Lazy import name |
|
||||
| --- | --- | --- |
|
||||
| `/login` | `pages/nearle/login1.js` | `Login` |
|
||||
| `/nearle/dispatch` | `pages/nearle/dispatch/Dispatch.js` | `Dispatch` |
|
||||
| `/nearle/dispatch/preview` | `pages/nearle/dispatch/Preview.js` | `DispatchPreview` |
|
||||
| `/nearle/orders` | `pages/nearle/orders/orders.js` | `Orders` |
|
||||
| `/nearle/orders/preview` | `pages/nearle/orders/OrdersPreview.js` | `OrdersPreview` |
|
||||
| `/nearle/orders/create` | `pages/nearle/orders/createorder1.js` | `Createorder1` |
|
||||
| `/nearle/orders/createorders` | `pages/nearle/orders/multipleOrders.js` | `MultipleOrders` |
|
||||
| `/nearle/orders/details` | `pages/nearle/orders/details.js` | `Details` |
|
||||
| `/nearle/deliveries` | `pages/nearle/deliveries/deliveries.js` | `Deliveries` |
|
||||
| `/nearle/tenants` | `pages/nearle/clients/Tenants.js` | `Tenants` |
|
||||
| `/nearle/clients/create` | `pages/nearle/clients/createclient.js` | `Createclient` |
|
||||
| `/nearle/pricing` | `pages/nearle/clientPricing/clientPricing.js` | `ClientsPricing` |
|
||||
| `/nearle/customers` | `pages/nearle/customers/customers.js` | `Customers` |
|
||||
| `/nearle/customer/create` | `pages/nearle/clients/createCustomer.js` | `CreateCustomer` |
|
||||
| `/nearle/riders` | `pages/nearle/riders/riders.js` | `Riders` |
|
||||
| `/nearle/riders/create` | `pages/nearle/riders/createrider.js` | `Createrider` |
|
||||
| `/nearle/riders/edit` | `pages/nearle/riders/editRider.js` | `EditRider` |
|
||||
| `/nearle/invoice` | `pages/nearle/invoice/invoice.js` | `Invoice` |
|
||||
| `/nearle/invoice/preview` | `pages/nearle/invoice/invoicePreview.js` | `InvoicePreview` |
|
||||
| `/nearle/requests` | `pages/nearle/requests/requests.js` | `Requests` |
|
||||
| `/nearle/reports/orderssummary` | `pages/nearle/reports/ordersSummary.js` | `OrdersSummary` |
|
||||
| `/nearle/reports/ordersdetails` | `pages/nearle/reports/ordersDetails.js` | `OrdersDetails` |
|
||||
| `/nearle/reports/riderssummary` | `pages/nearle/reports/ridersSummary.js` | `RidersSummary` |
|
||||
| `/nearle/reports/riderslogs` | `pages/nearle/reports/ridersLogs.js` | `RidersLogs` |
|
||||
| `/doormile/dispatch` | `pages/nearle/dispatch/Dispatch.js` | `Dispatch` |
|
||||
| `/doormile/dispatch/preview` | `pages/nearle/dispatch/Preview.js` | `DispatchPreview` |
|
||||
| `/doormile/orders` | `pages/nearle/orders/orders.js` | `Orders` |
|
||||
| `/doormile/orders/preview` | `pages/nearle/orders/OrdersPreview.js` | `OrdersPreview` |
|
||||
| `/doormile/orders/create` | `pages/nearle/orders/createorder1.js` | `Createorder1` |
|
||||
| `/doormile/orders/createorders` | `pages/nearle/orders/multipleOrders.js` | `MultipleOrders` |
|
||||
| `/doormile/orders/details` | `pages/nearle/orders/details.js` | `Details` |
|
||||
| `/doormile/deliveries` | `pages/nearle/deliveries/deliveries.js` | `Deliveries` |
|
||||
| `/doormile/tenants` | `pages/nearle/clients/Tenants.js` | `Tenants` |
|
||||
| `/doormile/clients/create` | `pages/nearle/clients/createclient.js` | `Createclient` |
|
||||
| `/doormile/pricing` | `pages/nearle/clientPricing/clientPricing.js` | `ClientsPricing` |
|
||||
| `/doormile/customers` | `pages/nearle/customers/customers.js` | `Customers` |
|
||||
| `/doormile/customer/create` | `pages/nearle/clients/createCustomer.js` | `CreateCustomer` |
|
||||
| `/doormile/riders` | `pages/nearle/riders/riders.js` | `Riders` |
|
||||
| `/doormile/riders/create` | `pages/nearle/riders/createrider.js` | `Createrider` |
|
||||
| `/doormile/riders/edit` | `pages/nearle/riders/editRider.js` | `EditRider` |
|
||||
| `/doormile/invoice` | `pages/nearle/invoice/invoice.js` | `Invoice` |
|
||||
| `/doormile/invoice/preview` | `pages/nearle/invoice/invoicePreview.js` | `InvoicePreview` |
|
||||
| `/doormile/requests` | `pages/nearle/requests/requests.js` | `Requests` |
|
||||
| `/doormile/reports/orderssummary` | `pages/nearle/reports/ordersSummary.js` | `OrdersSummary` |
|
||||
| `/doormile/reports/ordersdetails` | `pages/nearle/reports/ordersDetails.js` | `OrdersDetails` |
|
||||
| `/doormile/reports/riderssummary` | `pages/nearle/reports/ridersSummary.js` | `RidersSummary` |
|
||||
| `/doormile/reports/riderslogs` | `pages/nearle/reports/ridersLogs.js` | `RidersLogs` |
|
||||
| `/viewprofile` | `pages/nearle/viewProfile.js` | `ViewProfile` |
|
||||
| `/maintenance/{404,500,under-construction,coming-soon}` | `pages/maintenance/*` | — |
|
||||
|
||||
|
||||
95
API_ENDPOINTS.md
Normal file
95
API_ENDPOINTS.md
Normal 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` |
|
||||
73
CLAUDE.md
73
CLAUDE.md
@@ -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.
|
||||
> **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
|
||||
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
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`).
|
||||
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).
|
||||
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`.
|
||||
@@ -82,7 +82,7 @@ src/
|
||||
├── config.js # Theme constants (DRAWER_WIDTH=260, fontFamily, mode, presetColor, ThemeMode, MenuOrientation, ThemeDirection)
|
||||
├── routes/
|
||||
│ ├── 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
|
||||
├── layout/
|
||||
│ ├── MainLayout/ # Sidebar + header frame (used for all logged-in pages)
|
||||
@@ -117,7 +117,7 @@ src/
|
||||
|
||||
## 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
|
||||
const DT = {
|
||||
@@ -174,43 +174,43 @@ const AccentAvatar = ({ color, selected, size = 24, children }) => (
|
||||
|
||||
### Page anatomy (every operator page should follow this order)
|
||||
|
||||
> **Use brand purple `#662582` (with light variant `#9255AB`) for every brand surface below.** Don't use indigo `#6366f1` — that's reserved for the "Accepted" status badge only.
|
||||
> **Use brand red `#C01227` (with light variant `#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}`).
|
||||
2. **KPI tiles row** — `Grid` of 3–4 `Paper` cards, each with a 3px top stripe gradient, uppercase eyebrow label, large bold number, and a soft-tinted avatar holding an icon. The "primary" tile uses brand purple `#662582`; other tiles use semantic status colours.
|
||||
1. **Gradient header `<Paper>`** — `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#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 3–4 `Paper` cards, each with a 3px top stripe gradient, uppercase eyebrow label, large bold number, and a soft-tinted avatar holding an icon. The "primary" tile uses brand red `#C01227`; other tiles use semantic status colours.
|
||||
3. **Filter bar `<Paper>`** (optional) — pill-style `Autocomplete`s using `pillFieldSx(color)` + `SoftPaper`, each with an `AccentAvatar` start-adornment.
|
||||
4. **Pill status tabs + pill search** — tabs as clickable `<Box>` pills (active = filled accent + glow ring, inactive = `tint` bg + `edge` border), each with an avatar icon and a count badge. Tab accents use the **semantic status colour** for each tab (pending → amber, delivered → emerald, etc.). Search via `DebounceSearchBar` styled with brand purple `#662582` (tint bg, edge border, focus ring).
|
||||
5. **Table `<Paper>`** with `<TableContainer>` + sticky `<TableHead>` — uppercase muted headers on `DT.surfaceAlt`, rows with `borderBottom: 1px solid ${DT.divider}` and `:hover` row tint. Scrollbar thumb uses brand purple `edge('#662582')`.
|
||||
4. **Pill status tabs + pill search** — tabs as clickable `<Box>` pills (active = filled accent + glow ring, inactive = `tint` bg + `edge` border), each with an avatar icon and a count badge. Tab accents use the **semantic status colour** for each tab (pending → amber, delivered → emerald, etc.). Search via `DebounceSearchBar` styled with brand red `#C01227` (tint bg, edge border, focus ring).
|
||||
5. **Table `<Paper>`** with `<TableContainer>` + sticky `<TableHead>` — uppercase muted headers on `DT.surfaceAlt`, rows with `borderBottom: 1px solid ${DT.divider}` and `:hover` row tint. Scrollbar thumb uses brand red `edge('#C01227')`.
|
||||
6. **Status badges in cells** — `Stack` with `AccentAvatar` + label inside a soft pill (tint bg, edge border). Status colours come from a per-page `STATUS_META` map keyed by lowercase status string — these are **semantic**, not brand.
|
||||
7. **Edit / action icon buttons** — soft-pill `IconButton` using brand purple `#662582` (NOT `#8b5cf6` — that overlaps with the "Picked" status badge and confuses operators).
|
||||
7. **Edit / action icon buttons** — soft-pill `IconButton` using brand red `#C01227` (NOT `#8b5cf6` — that overlaps with the "Picked" status badge and confuses operators).
|
||||
8. **Empty state** — centered 64px avatar (soft grey), bold "No X to show" line, and a muted helper sentence. Do not use antd `<Empty />` for new code.
|
||||
|
||||
### Universal brand colour
|
||||
|
||||
**`#662582` — NearlExpress brand purple.** This is the canonical primary colour for this app, defined in `src/themes/theme/default.js` as `primary.main`. It drives the sidebar, the logo, and is what every new surface (page headers, KPI primary tile, search bars, edit-action buttons, dialog/popup headers, scrollbars) must use as the brand accent.
|
||||
**`#C01227` — 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`):
|
||||
|
||||
| Token | Hex | Use |
|
||||
|---|---|---|
|
||||
| `primary.lighter` | `#E8D9EF` | Very subtle wash bg |
|
||||
| `primary.light` / `primary.400` | `#9255AB` | Gradient pair with main |
|
||||
| `primary.main` | `#662582` | Brand primary — default for all brand surfaces |
|
||||
| `primary.dark` | `#4D1C61` | Hover / pressed states |
|
||||
| `primary.darker` | `#260E30` | Deep contrast text on light bg |
|
||||
| `primary.lighter` | `#F5DBDE` | Very subtle wash bg |
|
||||
| `primary.light` / `primary.400` | `#D25463` | Gradient pair with main |
|
||||
| `primary.main` | `#C01227` | Brand primary — default for all brand surfaces |
|
||||
| `primary.dark` | `#910E1D` | Hover / pressed states |
|
||||
| `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)
|
||||
|
||||
These colour-code lifecycle states. Do **not** swap them for brand purple — operators rely on the colour to identify status at a glance.
|
||||
These colour-code lifecycle states. Do **not** swap them for brand red — operators rely on the colour to identify status at a glance.
|
||||
|
||||
| Meaning | Colour |
|
||||
|--------------------------|-----------|
|
||||
| Pending / waiting | `#f59e0b` (amber) |
|
||||
| Accepted / assigned | `#6366f1` (indigo — semantically distinct from brand purple) |
|
||||
| Accepted / assigned | `#6366f1` (indigo — semantically distinct from brand red) |
|
||||
| Arrived | `#06b6d4` (cyan) |
|
||||
| Picked up | `#8b5cf6` (light purple — distinct from brand) |
|
||||
| Active / in-transit | `#14b8a6` (teal) |
|
||||
@@ -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 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
26
FLOW.md
@@ -56,16 +56,16 @@ flowchart TD
|
||||
Sidebar --> ViewProfile
|
||||
|
||||
%% ============================ Top-level pages ============================
|
||||
Dispatch[/"/nearle/dispatch<br/>Live Map · Riders · Batches"/]:::core
|
||||
Orders[/"/nearle/orders<br/>Orders Dashboard"/]:::core
|
||||
Deliveries[/"/nearle/deliveries<br/>Dispatched Deliveries"/]:::core
|
||||
Tenants[/"/nearle/tenants<br/>Client/Tenant Management"/]:::core
|
||||
Pricing[/"/nearle/pricing<br/>Pricing Matrix (master-detail)"/]:::core
|
||||
Customers[/"/nearle/customers<br/>Customer Directory"/]:::core
|
||||
Riders[/"/nearle/riders<br/>Rider Pool"/]:::core
|
||||
Invoice[/"/nearle/invoice<br/>Billing"/]:::core
|
||||
Requests[/"/nearle/requests<br/>Expense Approvals"/]:::core
|
||||
ReportsHub[/"/nearle/reports/*<br/>BI Suite"/]:::core
|
||||
Dispatch[/"/doormile/dispatch<br/>Live Map · Riders · Batches"/]:::core
|
||||
Orders[/"/doormile/orders<br/>Orders Dashboard"/]:::core
|
||||
Deliveries[/"/doormile/deliveries<br/>Dispatched Deliveries"/]:::core
|
||||
Tenants[/"/doormile/tenants<br/>Client/Tenant Management"/]:::core
|
||||
Pricing[/"/doormile/pricing<br/>Pricing Matrix (master-detail)"/]:::core
|
||||
Customers[/"/doormile/customers<br/>Customer Directory"/]:::core
|
||||
Riders[/"/doormile/riders<br/>Rider Pool"/]:::core
|
||||
Invoice[/"/doormile/invoice<br/>Billing"/]:::core
|
||||
Requests[/"/doormile/requests<br/>Expense Approvals"/]:::core
|
||||
ReportsHub[/"/doormile/reports/*<br/>BI Suite"/]:::core
|
||||
ViewProfile[/"/viewprofile<br/>Profile Manager"/]:::sub
|
||||
|
||||
%% ============================ Sub-routes ============================
|
||||
@@ -185,12 +185,12 @@ flowchart TD
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant U as Operator
|
||||
participant O as /nearle/orders
|
||||
participant O as /doormile/orders
|
||||
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 J as jupiter.nearle.app
|
||||
participant D as /nearle/deliveries
|
||||
participant D as /doormile/deliveries
|
||||
participant F as FCM (rider device)
|
||||
|
||||
U->>O: Select pending orders (checkbox)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
const webpack = require('webpack');
|
||||
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) {
|
||||
// @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 = {
|
||||
process: require.resolve('process/browser'),
|
||||
// zlib: require.resolve('browserify-zlib'),
|
||||
|
||||
979
package-lock.json
generated
979
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@@ -5,18 +5,21 @@
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^7.0.0",
|
||||
"@ant-design/icons": "^5.0.1",
|
||||
"@astryxdesign/core": "^0.1.9",
|
||||
"@astryxdesign/theme-neutral": "^0.1.9",
|
||||
"@emotion/cache": "^11.10.7",
|
||||
"@emotion/react": "^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/lab": "^5.0.0-alpha.127",
|
||||
"@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",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@reduxjs/toolkit": "^2.12.0",
|
||||
"@stylexjs/stylex": "^0.19.0",
|
||||
"@svgr/webpack": "^7.0.0",
|
||||
"@tanstack/react-query": "^5.17.9",
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"antd": "^5.11.5",
|
||||
"autosuggest-highlight": "^3.3.4",
|
||||
"axios": "^1.3.5",
|
||||
@@ -28,37 +31,35 @@
|
||||
"env-cmd": "^10.1.0",
|
||||
"firebase": "^10.14.1",
|
||||
"formik": "^2.2.9",
|
||||
"framer-motion": "^10.12.4",
|
||||
"framer-motion": "^12.42.2",
|
||||
"geolib": "^3.3.4",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"leaflet": "^1.9.4",
|
||||
"lodash": "^4.17.21",
|
||||
"mui-daterange-picker": "^1.0.5",
|
||||
"notistack": "^3.0.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"process": "^0.11.10",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react": "^19.2.8",
|
||||
"react-csv": "^2.2.2",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dnd": "^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-google-autocomplete": "^2.7.3",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-intl": "^6.4.1",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-intl": "^7.1.14",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-loading-icons": "^1.1.0",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^6.10.0",
|
||||
"react-router-dom": "^6.10.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-timer-hook": "^3.0.5",
|
||||
"react-to-print": "^2.15.0",
|
||||
"react18-input-otp": "^1.1.3",
|
||||
"redux": "^4.2.1",
|
||||
"react-to-print": "^3.3.0",
|
||||
"redux": "^5.0.1",
|
||||
"simplebar": "^6.2.5",
|
||||
"simplebar-react": "^3.2.4",
|
||||
"stream-browserify": "^3.0.0",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 69 KiB |
@@ -5,7 +5,7 @@
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Nearle Neighbourhood commerce" />
|
||||
<meta name="description" content="Doormile Neighbourhood commerce" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
@@ -28,7 +28,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Nearle Console</title>
|
||||
<title>Doormile Express Console</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
|
||||
BIN
public/logo192.png
Normal file
BIN
public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
BIN
src/assets/images/doormile-logo.png
Normal file
BIN
src/assets/images/doormile-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/images/doormile-mark.png
Normal file
BIN
src/assets/images/doormile-mark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
@@ -1,6 +1,6 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import nelogo from '../assets/images/logo-sm.png';
|
||||
import nelogo from '../assets/images/doormile-mark.png';
|
||||
|
||||
// Styled Loader Wrapper
|
||||
const LoaderWrapper = styled('div')(() => ({
|
||||
@@ -39,7 +39,7 @@ const CircularLoader = () => (
|
||||
|
||||
{/* Logo Positioned at the Center */}
|
||||
<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>
|
||||
</LoaderWrapper>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
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 { addDays, addWeeks, startOfWeek, endOfWeek, startOfMonth, endOfMonth, addMonths } from 'date-fns';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
|
||||
@@ -26,13 +26,13 @@ The `DT` design tokens (palette, alpha helpers, `pillFieldSx`, `SoftPaper`, `Acc
|
||||
|
||||
**Hard rules when editing components here:**
|
||||
|
||||
- **Universal brand colour is `#662582`** (NearlExpress purple — from `themes/theme/default.js` `primary.main`). Every brand surface (page header, dialog header, KPI primary tile, search bars, edit-action buttons, scrollbars) uses this. Gradient pair: `#662582 → #9255AB`.
|
||||
- **Semantic status palette is distinct** from the brand. Use these for lifecycle indicators only: sky `#0ea5e9`, emerald `#10b981`, amber `#f59e0b`, red `#ef4444`, status-purple `#8b5cf6` (lighter than brand), cyan `#06b6d4`, teal `#14b8a6`, orange `#f97316`, muted `#94a3b8`, indigo `#6366f1` (Accepted status). Don't replace these with brand purple — operators colour-code on them.
|
||||
- **Universal brand colour is `#C01227`** (Doormile 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`, cyan `#06b6d4`, teal `#14b8a6`, orange `#f97316`, muted `#94a3b8`, indigo `#6366f1` (Accepted status). Don't replace these with brand red — operators colour-code on them.
|
||||
- Don't introduce a colour from `theme.palette` for new surfaces — those are Mantis defaults and don't match the DT system. Use the hex values above directly.
|
||||
- Border radii: `12` (inner), `16` (card), `999` (pill). No other values.
|
||||
- Shadows: `DT.shadowSoft` / `DT.shadowMd` / `DT.shadowPop`. No raw `box-shadow` strings.
|
||||
|
||||
> Some existing redesigned pages (`deliveries.js`, `Tenants.js`, `clientPricing.js`) still use `#6366f1` as the brand accent — this is a legacy from the first design pass. `customers.js` and the `createorder1` Saved-Address dialog are already on `#662582`. When you next edit one of the legacy pages, migrate it to brand purple in the same PR.
|
||||
> Some existing redesigned pages (`deliveries.js`) 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
90
src/components/nearle_components/DateRangePicker.js
Normal file
90
src/components/nearle_components/DateRangePicker.js
Normal 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;
|
||||
@@ -1,7 +1,7 @@
|
||||
// LoaderWithImage.jsx
|
||||
import React from 'react';
|
||||
import { Box, CircularProgress } from '@mui/material';
|
||||
import nelogo from '../../assets/images/logo-sm.png';
|
||||
import nelogo from '../../assets/images/doormile-mark.png';
|
||||
|
||||
export default function LoaderWithImage({ size = 70, imgSize = 40, alt = 'loader' }) {
|
||||
return (
|
||||
@@ -25,7 +25,8 @@ export default function LoaderWithImage({ size = 70, imgSize = 40, alt = 'loader
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '50%',
|
||||
objectFit: 'contain'
|
||||
objectFit: 'cover',
|
||||
transform: 'scale(1.4)'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -42,7 +42,7 @@ MobileCardList.propTypes = {
|
||||
|
||||
// Card shell — coloured accent rail on the left, a header slot (status badge /
|
||||
// title / action buttons), then any field grid / collapse content as children.
|
||||
export const MobileCard = ({ accent = '#662582', header, footer, selected = false, onClick, children, sx }) => (
|
||||
export const MobileCard = ({ accent = '#C01227', header, footer, selected = false, onClick, children, sx }) => (
|
||||
<Paper
|
||||
elevation={0}
|
||||
onClick={onClick}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Avatar, Box, Card, CardContent, Skeleton, Stack, Typography } from '@mu
|
||||
// `color` is a hex accent (defaults to brand purple); `caption` is the small
|
||||
// muted line under the value (e.g. "96% of total").
|
||||
|
||||
export default function StatCard({ title, value, icon, color = '#662582', caption, loading = false }) {
|
||||
export default function StatCard({ title, value, icon, color = '#C01227', caption, loading = false }) {
|
||||
return (
|
||||
<Card sx={{ height: '100%' }}>
|
||||
<CardContent sx={{ p: { xs: 1.75, md: 2 }, '&:last-child': { pb: { xs: 1.75, md: 2 } } }}>
|
||||
|
||||
@@ -73,7 +73,7 @@ export const initFirebaseNotificationListener = () => {
|
||||
const registration = await navigator.serviceWorker.getRegistration();
|
||||
|
||||
if (registration) {
|
||||
registration.showNotification(notification.title || 'Nearle', {
|
||||
registration.showNotification(notification.title || 'Doormile', {
|
||||
body: notification.body,
|
||||
icon: notification.image || '/favicon.ico',
|
||||
data,
|
||||
|
||||
19
src/index.js
19
src/index.js
@@ -1,6 +1,6 @@
|
||||
import logger from './utils/logger';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { BrowserRouter, Link } from 'react-router-dom';
|
||||
|
||||
// third-party
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
@@ -8,6 +8,13 @@ import { Provider as ReduxProvider } from 'react-redux';
|
||||
// scroll bar
|
||||
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
|
||||
import 'assets/third-party/apex-chart.css';
|
||||
import 'assets/third-party/react-table.css';
|
||||
@@ -39,7 +46,7 @@ if (process.env.NODE_ENV !== 'development') {
|
||||
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'));
|
||||
|
||||
@@ -50,7 +57,13 @@ root.render(
|
||||
<ReduxProvider store={store}>
|
||||
{/* <ConfigProvider> */}{' '}
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
{/* 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 />
|
||||
</LinkProvider>
|
||||
</BrowserRouter>
|
||||
{/* </ConfigProvider> */}
|
||||
</ReduxProvider>
|
||||
|
||||
176
src/layout/MainLayout/AppSideNav.js
Normal file
176
src/layout/MainLayout/AppSideNav.js
Normal 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;
|
||||
199
src/layout/MainLayout/AppTopNav.js
Normal file
199
src/layout/MainLayout/AppTopNav.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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's{' '}
|
||||
<Typography component="span" variant="subtitle1">
|
||||
Cristina danny'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;
|
||||
@@ -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;
|
||||
@@ -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's{' '}
|
||||
<Typography component="span" variant="subtitle1">
|
||||
Cristina danny'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
|
||||
<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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,72 +1,58 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useState } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useMediaQuery, Box, Container, Toolbar } from '@mui/material';
|
||||
// Astryx design system — see themes/astryx.js and CLAUDE.md's Astryx CLI
|
||||
// section. This replaces the old MUI Drawer/Header/HorizontalBar shell;
|
||||
// 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 Drawer from './Drawer';
|
||||
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';
|
||||
import AppTopNav from './AppTopNav';
|
||||
import AppSideNav from './AppSideNav';
|
||||
|
||||
// ==============================|| MAIN LAYOUT ||============================== //
|
||||
|
||||
const MainLayout = () => {
|
||||
const theme = useTheme();
|
||||
const matchDownXL = useMediaQuery(theme.breakpoints.down('xl'));
|
||||
const downLG = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
|
||||
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]);
|
||||
// Lifted here (rather than left as SideNav's own uncontrolled state) so the
|
||||
// TopNav logo can track the sidebar's collapse state. Defaults to collapsed
|
||||
// on every load/reload (not persisted).
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(true);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', width: '100%' }}>
|
||||
<Header />
|
||||
{!isHorizontal ? <Drawer /> : <HorizontalBar />}
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
width: isHorizontal ? '100%' : drawerOpen ? 'calc(100% - 260px)' : { xs: '100%', lg: 'calc(100% - 60px)' },
|
||||
flexGrow: 1,
|
||||
p: { xs: 2, sm: 3 }
|
||||
}}
|
||||
<Theme theme={doormileTheme} mode="light">
|
||||
{/* height="auto" (not doormile_crm's "fill"): "fill" scrolls the
|
||||
content area in its own container, and CLAUDE.md's canonical
|
||||
useInfiniteQuery pattern observes a sentinel against the window's
|
||||
viewport (IntersectionObserver root=null), which only fires
|
||||
correctly with normal document-level scroll. */}
|
||||
<AppShell
|
||||
variant="section"
|
||||
height="auto"
|
||||
contentPadding={0}
|
||||
topNav={<AppTopNav isSidebarCollapsed={isSidebarCollapsed} />}
|
||||
sideNav={<AppSideNav isCollapsed={isSidebarCollapsed} onCollapsedChange={setIsSidebarCollapsed} />}
|
||||
mobileNav={{ breakpoint: 'lg' }}
|
||||
>
|
||||
<Toolbar sx={{ mt: isHorizontal ? 8 : 'inherit' }} />
|
||||
<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} /> */}
|
||||
<div className="main-content-area" style={{ minHeight: '100%', boxSizing: 'border-box' }}>
|
||||
<Outlet />
|
||||
{/* <Footer /> */}
|
||||
</Container>
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
<style>{`
|
||||
.main-content-area {
|
||||
padding: 24px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.main-content-area {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</AppShell>
|
||||
</Theme>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
// third-party
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlined';
|
||||
import ReceiptOutlinedIcon from '@mui/icons-material/ReceiptOutlined';
|
||||
import MopedOutlinedIcon from '@mui/icons-material/MopedOutlined';
|
||||
import NearMeOutlinedIcon from '@mui/icons-material/NearMeOutlined';
|
||||
import { TbListDetails } from 'react-icons/tb';
|
||||
|
||||
// assets
|
||||
import {
|
||||
@@ -19,14 +14,15 @@ import {
|
||||
StopOutlined,
|
||||
DashboardOutlined,
|
||||
ClockCircleOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
TeamOutlined,
|
||||
MailOutlined,
|
||||
ImportOutlined,
|
||||
BarChartOutlined,
|
||||
MoneyCollectOutlined,
|
||||
FileDoneOutlined
|
||||
FileDoneOutlined,
|
||||
PartitionOutlined,
|
||||
CarOutlined,
|
||||
UserOutlined,
|
||||
ProfileOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
// icons
|
||||
@@ -42,24 +38,22 @@ const icons = {
|
||||
DeploymentUnitOutlined,
|
||||
DashboardOutlined,
|
||||
ClockCircleOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
TeamOutlined,
|
||||
MailOutlined,
|
||||
ImportOutlined,
|
||||
BarChartOutlined,
|
||||
ReceiptOutlinedIcon,
|
||||
NearMeOutlinedIcon,
|
||||
DirectionsBikeOutlinedIcon,
|
||||
MopedOutlinedIcon,
|
||||
FileDoneOutlined
|
||||
FileDoneOutlined,
|
||||
PartitionOutlined,
|
||||
CarOutlined,
|
||||
UserOutlined,
|
||||
ProfileOutlined
|
||||
};
|
||||
|
||||
// ==============================|| MENU ITEMS - SUPPORT ||============================== //
|
||||
|
||||
const nearle = {
|
||||
id: 'nearle_Pages',
|
||||
title: <FormattedMessage id="Nearle" />,
|
||||
title: <FormattedMessage id="Doormile" />,
|
||||
icon: icons.FileDoneOutlined,
|
||||
type: 'group',
|
||||
children: [
|
||||
@@ -67,51 +61,29 @@ const nearle = {
|
||||
id: 'dispatch',
|
||||
title: <FormattedMessage id="dispatch" />,
|
||||
type: 'item',
|
||||
url: '/nearle/dispatch',
|
||||
icon: icons.DirectionsBikeOutlinedIcon
|
||||
url: '/doormile/dispatch',
|
||||
icon: icons.PartitionOutlined
|
||||
},
|
||||
{
|
||||
id: 'orders',
|
||||
title: <FormattedMessage id="orders" />,
|
||||
type: 'item',
|
||||
url: '/nearle/orders',
|
||||
url: '/doormile/orders',
|
||||
icon: icons.DashboardOutlined
|
||||
},
|
||||
|
||||
{
|
||||
id: 'deliveries',
|
||||
title: <FormattedMessage id="deliveries" />,
|
||||
type: 'item',
|
||||
url: '/nearle/deliveries',
|
||||
icon: MopedOutlinedIcon
|
||||
},
|
||||
{
|
||||
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
|
||||
url: '/doormile/deliveries',
|
||||
icon: icons.CarOutlined
|
||||
},
|
||||
{
|
||||
id: 'riders',
|
||||
title: <FormattedMessage id="riders" />,
|
||||
type: 'item',
|
||||
url: '/nearle/riders',
|
||||
icon: DirectionsBikeOutlinedIcon
|
||||
url: '/doormile/riders',
|
||||
icon: icons.UserOutlined
|
||||
},
|
||||
{
|
||||
id: 'reports',
|
||||
@@ -123,48 +95,24 @@ const nearle = {
|
||||
id: 'reports',
|
||||
title: <FormattedMessage id="ordersummary" />,
|
||||
type: 'item',
|
||||
url: '/nearle/reports/orderssummary',
|
||||
icon: TbListDetails
|
||||
url: '/doormile/reports/orderssummary',
|
||||
icon: icons.ProfileOutlined
|
||||
},
|
||||
{
|
||||
id: 'ordersdetails',
|
||||
title: <FormattedMessage id="ordersdetails" />,
|
||||
type: 'item',
|
||||
url: '/nearle/reports/ordersdetails',
|
||||
url: '/doormile/reports/ordersdetails',
|
||||
icon: icons.DashboardOutlined
|
||||
// target: true
|
||||
},
|
||||
{
|
||||
id: 'riderssummary',
|
||||
title: <FormattedMessage id="riderssummary" />,
|
||||
type: 'item',
|
||||
url: '/nearle/reports/riderssummary',
|
||||
icon: DirectionsBikeOutlinedIcon
|
||||
// target: true
|
||||
},
|
||||
{
|
||||
id: 'riderslogs',
|
||||
title: <FormattedMessage id="riderslogs" />,
|
||||
type: 'item',
|
||||
url: '/nearle/reports/riderslogs',
|
||||
icon: DirectionsBikeOutlinedIcon
|
||||
// target: true
|
||||
},
|
||||
{
|
||||
id: 'profitability',
|
||||
title: <FormattedMessage id="profitability" />,
|
||||
type: 'item',
|
||||
url: '/nearle/reports/profitability',
|
||||
icon: icons.BarChartOutlined
|
||||
url: '/doormile/reports/riderssummary',
|
||||
icon: icons.UserOutlined
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'invoice',
|
||||
title: <FormattedMessage id="invoice" />,
|
||||
type: 'item',
|
||||
url: '/nearle/invoice',
|
||||
icon: icons.ReceiptOutlinedIcon
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -233,7 +233,7 @@ export const notifyRider = async (riderToken) => {
|
||||
const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, {
|
||||
token: riderToken,
|
||||
notification: {
|
||||
title: 'NearleXpress',
|
||||
title: 'DoormileXpress',
|
||||
body: 'Orders have been placed for delivery. Kindly accept and process deliveries',
|
||||
sound: 'ring',
|
||||
image: ''
|
||||
@@ -271,12 +271,9 @@ export const cancelMultipleOrder = async (orderlist) => {
|
||||
// ==============================|| fetchDeliveries (deliveries) ||============================== //
|
||||
|
||||
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;
|
||||
const url =
|
||||
appId === 0
|
||||
? `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?appuserid=${userid}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`
|
||||
: `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?applocationid=${appId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
|
||||
const 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}`;
|
||||
const response = await axios.get(url);
|
||||
|
||||
return {
|
||||
@@ -311,10 +308,7 @@ export const fetchPercentageAPI = async (appId) => {
|
||||
// ==============================|| fetchCountAPI (deliveries) ||============================== //
|
||||
|
||||
export const fetchCountAPI = async (appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid) => {
|
||||
const url =
|
||||
appId == 0
|
||||
? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?appuserid=${userid}&fromdate=${startdate}&todate=${enddate}`
|
||||
: `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
|
||||
const url = `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
|
||||
const response = await axios.get(url);
|
||||
const data = response.data.details;
|
||||
return {
|
||||
@@ -369,103 +363,6 @@ export const updateDeliveryAPI = async (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) ||============================== //
|
||||
export const fetchAllRiders = async ({ pageParam = 1, queryKey }) => {
|
||||
try {
|
||||
@@ -554,10 +451,7 @@ export const fetchorderdetails = async ({ queryKey }) => {
|
||||
const [appId, startdate, enddate, page, rowsPerPage] = queryKey;
|
||||
|
||||
const response = await axios.get(
|
||||
appId == 0
|
||||
? `${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}`
|
||||
`${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) => ({
|
||||
...item,
|
||||
@@ -621,21 +515,6 @@ export const fetchLocations = async () => {
|
||||
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)||============================== //
|
||||
|
||||
export const fetchRidersLogs = async ({ queryKey }) => {
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
@@ -2,7 +2,8 @@ import { useEffect, useState } from 'react';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Box, Button, FormLabel, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
|
||||
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
|
||||
// import { PatternFormat } from 'react-number-format';
|
||||
@@ -15,6 +16,7 @@ import Loader from 'components/Loader';
|
||||
import Geocode from 'react-geocode';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { DT, tint } from 'themes/dt/tokens';
|
||||
// import { setLocationType } from 'react-geocode';
|
||||
|
||||
// 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({
|
||||
apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY,
|
||||
onPlaceSelected: (place) => {
|
||||
@@ -296,11 +292,28 @@ const Createclient = () => {
|
||||
{loading && <Loader />}
|
||||
|
||||
<Grid item xs={12} sx={{ mb: 2 }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h3">Create Client</Typography>
|
||||
</Stack>
|
||||
<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>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</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 item xs={12} sm={4} >
|
||||
<MainCard title="Personal Information" sx={{ height: '100%' }}>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -78,7 +78,7 @@ import {
|
||||
startOfMonth,
|
||||
startOfWeek
|
||||
} from 'date-fns';
|
||||
import { DateRangePicker } from 'mui-daterange-picker';
|
||||
import { DateRangePicker } from 'components/nearle_components/DateRangePicker';
|
||||
import * as React from 'react';
|
||||
import Loader from 'components/Loader';
|
||||
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';
|
||||
|
||||
// ============================================================================
|
||||
// Design tokens — extracted from the polished "Batch" dropdown so every
|
||||
// surface on this page (filters, KPIs, tabs, status badges, dialogs) shares
|
||||
// the same visual language. All helpers take a color and emit MUI sx values.
|
||||
// Design tokens — shared across every DT-styled operator page so filters,
|
||||
// KPIs, tabs, status badges, and dialogs stay visually consistent.
|
||||
// See src/themes/dt/tokens.js for the canonical source.
|
||||
// ============================================================================
|
||||
const DT = {
|
||||
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 }
|
||||
});
|
||||
import { DT, a, tint, soft, ring, edge, pillFieldSx } from 'themes/dt/tokens';
|
||||
|
||||
// Status palette — drives tab pills, row status badges, dialogs.
|
||||
const STATUS_META = {
|
||||
@@ -413,7 +362,7 @@ const Deliveries = () => {
|
||||
const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, {
|
||||
token: selectedRow.userfcmtoken,
|
||||
notification: {
|
||||
title: 'NearleXpress',
|
||||
title: 'DoormileXpress',
|
||||
body: `${selectedRow.orderid} have been Cancelled`,
|
||||
sound: 'ring',
|
||||
image: ''
|
||||
@@ -912,7 +861,7 @@ const Deliveries = () => {
|
||||
setLocoName={setLocoName}
|
||||
setPage={setPage}
|
||||
pill
|
||||
accentColor="#662582"
|
||||
accentColor="#C01227"
|
||||
icon={<MdMyLocation size={14} />}
|
||||
placeholder="Select Zone"
|
||||
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 }}>
|
||||
<Avatar sx={{ width: 28, height: 28, bgcolor: soft('#662582'), color: '#662582' }}>
|
||||
<Avatar sx={{ width: 28, height: 28, bgcolor: soft('#C01227'), color: '#C01227' }}>
|
||||
<MdTune size={16} />
|
||||
</Avatar>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.6, textTransform: 'uppercase', color: DT.textSecondary }}>
|
||||
@@ -1125,8 +1074,8 @@ const Deliveries = () => {
|
||||
transition: 'border-color 0.15s, box-shadow 0.15s',
|
||||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||||
'&:hover fieldset': { borderColor: '#cbd5e1' },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` },
|
||||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
|
||||
},
|
||||
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: '#94a3b8' }
|
||||
}}
|
||||
@@ -1344,15 +1293,15 @@ const Deliveries = () => {
|
||||
flexShrink: 0,
|
||||
cursor: 'pointer',
|
||||
borderRadius: DT.radiusField + 'px',
|
||||
border: `1px solid ${active ? meta.color : DT.borderSubtle}`,
|
||||
bgcolor: active ? meta.color : DT.surface,
|
||||
border: `1px solid ${active ? '#C01227' : DT.borderSubtle}`,
|
||||
bgcolor: active ? '#C01227' : 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 : DT.borderHover,
|
||||
bgcolor: active ? meta.color : DT.surfaceAlt
|
||||
borderColor: active ? '#C01227' : DT.borderHover,
|
||||
bgcolor: active ? '#C01227' : DT.surfaceAlt
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -1442,10 +1391,10 @@ const Deliveries = () => {
|
||||
overflowX: 'auto',
|
||||
'&::-webkit-scrollbar': { width: '10px', height: '10px', cursor: 'pointer' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: edge('#662582'),
|
||||
backgroundColor: edge('#C01227'),
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
'&:hover': { backgroundColor: '#662582' }
|
||||
'&:hover': { backgroundColor: '#C01227' }
|
||||
},
|
||||
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
|
||||
}}
|
||||
@@ -1562,7 +1511,7 @@ const Deliveries = () => {
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => handleMenuOpen(e, row)}
|
||||
sx={{ borderRadius: 999, bgcolor: tint('#662582'), color: '#662582', border: `1px solid ${edge('#662582')}`, '&:hover': { bgcolor: soft('#662582') } }}
|
||||
sx={{ borderRadius: 999, bgcolor: tint('#C01227'), color: '#C01227', border: `1px solid ${edge('#C01227')}`, '&:hover': { bgcolor: soft('#C01227') } }}
|
||||
>
|
||||
<EditOutlined />
|
||||
</IconButton>
|
||||
@@ -1650,7 +1599,7 @@ const Deliveries = () => {
|
||||
</MobileField>
|
||||
<MobileField label="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>
|
||||
)}
|
||||
@@ -1664,8 +1613,8 @@ const Deliveries = () => {
|
||||
{isOpen && (
|
||||
<Box sx={{ mt: 1.5, p: 1.25, borderRadius: 2, bgcolor: DT.surfaceAlt, border: `1px solid ${DT.divider}` }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
|
||||
<AccentAvatar color="#662582" size={22}><MdInventory2 size={12} /></AccentAvatar>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#662582' }}>
|
||||
<AccentAvatar color="#C01227" size={22}><MdInventory2 size={12} /></AccentAvatar>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
|
||||
Product Details
|
||||
</Typography>
|
||||
</Stack>
|
||||
@@ -2065,9 +2014,9 @@ const Deliveries = () => {
|
||||
height: 24,
|
||||
px: 0.875,
|
||||
borderRadius: 999,
|
||||
bgcolor: tint('#662582'),
|
||||
border: `1px solid ${edge('#662582')}`,
|
||||
color: '#662582',
|
||||
bgcolor: tint('#C01227'),
|
||||
border: `1px solid ${edge('#C01227')}`,
|
||||
color: '#C01227',
|
||||
fontWeight: 800,
|
||||
fontSize: 11
|
||||
}}
|
||||
@@ -2133,10 +2082,10 @@ const Deliveries = () => {
|
||||
onClick={(e) => handleMenuOpen(e, row)}
|
||||
sx={{
|
||||
borderRadius: 999,
|
||||
bgcolor: tint('#662582'),
|
||||
color: '#662582',
|
||||
border: `1px solid ${edge('#662582')}`,
|
||||
'&:hover': { bgcolor: soft('#662582') }
|
||||
bgcolor: tint('#C01227'),
|
||||
color: '#C01227',
|
||||
border: `1px solid ${edge('#C01227')}`,
|
||||
'&:hover': { bgcolor: soft('#C01227') }
|
||||
}}
|
||||
>
|
||||
<EditOutlined />
|
||||
@@ -2168,9 +2117,9 @@ const Deliveries = () => {
|
||||
background: '#fff'
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ px: 2, py: 1.25, borderBottom: `1px solid ${DT.divider}`, bgcolor: tint('#662582') }}>
|
||||
<AccentAvatar color="#662582"><MdInventory2 size={14} /></AccentAvatar>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#662582' }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ px: 2, py: 1.25, borderBottom: `1px solid ${DT.divider}`, bgcolor: tint('#C01227') }}>
|
||||
<AccentAvatar color="#C01227"><MdInventory2 size={14} /></AccentAvatar>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
|
||||
Product Details
|
||||
</Typography>
|
||||
</Stack>
|
||||
@@ -2778,14 +2727,14 @@ const Deliveries = () => {
|
||||
borderRadius: 2,
|
||||
bgcolor: '#fff',
|
||||
'& fieldset': { borderColor: DT.borderSubtle },
|
||||
'&:hover fieldset': { borderColor: '#662582' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 2 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` }
|
||||
'&:hover fieldset': { borderColor: '#C01227' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
|
||||
}
|
||||
}}
|
||||
>
|
||||
{['pending','accepted','started','arrived','delivered','cancelled'].map((s) => {
|
||||
const m = STATUS_META[s] || { label: s, color: '#662582', icon: MdHistoryToggleOff };
|
||||
const m = STATUS_META[s] || { label: s, color: '#C01227', icon: MdHistoryToggleOff };
|
||||
const Ic = m.icon;
|
||||
return (
|
||||
<MenuItem key={s} value={s} sx={{ gap: 1 }}>
|
||||
@@ -2812,9 +2761,9 @@ const Deliveries = () => {
|
||||
borderRadius: 2,
|
||||
bgcolor: '#fff',
|
||||
'& fieldset': { borderColor: DT.borderSubtle },
|
||||
'&:hover fieldset': { borderColor: '#662582' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 2 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` }
|
||||
'&:hover fieldset': { borderColor: '#C01227' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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)
|
||||
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
- 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
@@ -80,7 +80,7 @@ import logger from '../../../utils/logger';
|
||||
// emerald for the actual GPS trail (signals "live / real" data). Per-step
|
||||
// distinction in Combined view is carried by the numbered drop pins, which
|
||||
// keep STEP_PALETTE so the timeline link to a specific delivery survives.
|
||||
const COMBINED_PLANNED_COLOR = '#662582';
|
||||
const COMBINED_PLANNED_COLOR = '#C01227';
|
||||
const COMBINED_ACTUAL_COLOR = '#10b981';
|
||||
|
||||
const toNum = (v) => {
|
||||
@@ -5729,7 +5729,7 @@ const Dispatch = ({
|
||||
style={{
|
||||
boxShadow: 'var(--shadow-lg)',
|
||||
background: compareOpen
|
||||
? 'linear-gradient(135deg, #662582, #9255AB)'
|
||||
? 'linear-gradient(135deg, #C01227, #D25463)'
|
||||
: '#fff',
|
||||
marginLeft: 8,
|
||||
color: compareOpen ? '#fff' : undefined
|
||||
@@ -6328,7 +6328,7 @@ const Dispatch = ({
|
||||
<div className="da-section">
|
||||
<div className="da-hero-row">
|
||||
<div className="da-hero-card">
|
||||
<div className="da-hero-icon" style={{ background: '#6625821f', color: '#662582' }}>
|
||||
<div className="da-hero-icon" style={{ background: '#C012271f', color: '#C01227' }}>
|
||||
<MdOutlineInventory2 />
|
||||
</div>
|
||||
<div className="da-hero-value">{analysisFormatNum(fleet.total_orders)}</div>
|
||||
@@ -6809,7 +6809,7 @@ const Dispatch = ({
|
||||
<div className="da-pos-modal-title-wrap">
|
||||
<div
|
||||
className="da-pos-modal-avatar"
|
||||
style={{ background: `${riderPositionModal.color || '#662582'}22`, color: riderPositionModal.color || '#662582' }}
|
||||
style={{ background: `${riderPositionModal.color || '#C01227'}22`, color: riderPositionModal.color || '#C01227' }}
|
||||
>
|
||||
<MdTwoWheeler />
|
||||
</div>
|
||||
|
||||
@@ -297,7 +297,7 @@ const Preview = () => {
|
||||
// so a later reload / back-forward also bounces instead of re-using it.
|
||||
useEffect(() => {
|
||||
if (!stateData.dispatchPreviewData) {
|
||||
navigate('/nearle/orders', { replace: true });
|
||||
navigate('/doormile/orders', { replace: true });
|
||||
return;
|
||||
}
|
||||
if (typeof window !== 'undefined' && window.history?.state) {
|
||||
@@ -414,7 +414,7 @@ const Preview = () => {
|
||||
OpenToast('Delivery Created Successfully', 'success', 2000);
|
||||
setIsLoading(false);
|
||||
if (rider?.userfcmtoken) notifyRiderMutation.mutate(rider.userfcmtoken);
|
||||
navigate('/nearle/deliveries');
|
||||
navigate('/doormile/deliveries');
|
||||
},
|
||||
onError: (error) => {
|
||||
OpenToast(error.message, 'error', 4000);
|
||||
@@ -566,7 +566,7 @@ const Preview = () => {
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Tooltip title="Back to orders" placement="top">
|
||||
<IconButton
|
||||
onClick={() => navigate('/nearle/orders')}
|
||||
onClick={() => navigate('/doormile/orders')}
|
||||
sx={{ bgcolor: 'action.hover', '&:hover': { bgcolor: 'action.selected' } }}
|
||||
>
|
||||
<HiOutlineArrowLeft size={20} />
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,45 +1,90 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { enqueueSnackbar, closeSnackbar } from 'notistack';
|
||||
import AnimateButton from 'components/@extended/AnimateButton';
|
||||
import OtpInput from 'react18-input-otp';
|
||||
|
||||
import { Box, Card, CardContent, Stack, TextField, Button, Typography, Link, FormLabel, IconButton, InputAdornment } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Loader from 'components/Loader';
|
||||
import logo from 'assets/images/logo-nearle1.png';
|
||||
import expressImage from 'assets/images/express.png';
|
||||
import logo from 'assets/images/doormile-logo.png';
|
||||
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { OpenToast } from 'components/third-party/OpenToast';
|
||||
import { closeGlobalToast, GlobalToast } from 'components/nearle_components/GlobalToast';
|
||||
import Visibility from '@mui/icons-material/Visibility';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
import { setLoginUser } from 'store/reducers/loginUserSlice';
|
||||
import { markSessionStart } from 'utils/session';
|
||||
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 dispatch = useDispatch();
|
||||
const fcmtoken = useSelector((state) => state.fcm);
|
||||
const permission = useSelector((state) => state.fcm.permission);
|
||||
const theme = useTheme();
|
||||
const [loading, setLoading] = useState(false);
|
||||
let navigate = useNavigate();
|
||||
const [otp, setOtp] = useState('');
|
||||
const [currentotp, setCurrentotp] = useState('');
|
||||
const [userinfo, setUserinfo] = useState({});
|
||||
const [username, setUsername] = useState('');
|
||||
const [passwordStatus, setPasswordStatus] = useState(0);
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isPassword, setIspassword] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [userid, setUserid] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem('firstname')) {
|
||||
navigate('/nearle/dispatch');
|
||||
navigate('/doormile/dispatch');
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -100,7 +145,7 @@ const Login = () => {
|
||||
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
|
||||
markSessionStart();
|
||||
fetchAppLocations(userinfo.userid);
|
||||
navigate('/nearle/dispatch');
|
||||
navigate('/doormile/dispatch');
|
||||
} else {
|
||||
OpenToast(res.data.message, 'error', 3000);
|
||||
}
|
||||
@@ -123,7 +168,7 @@ const Login = () => {
|
||||
markSessionStart();
|
||||
closeGlobalToast(); // to close the pin snackbar
|
||||
|
||||
navigate('/nearle/dispatch');
|
||||
navigate('/doormile/dispatch');
|
||||
};
|
||||
|
||||
const opentoast = (message) => {
|
||||
@@ -152,320 +197,157 @@ const Login = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (passwordStatus == 0) {
|
||||
loginsend();
|
||||
} else if (passwordStatus == 1) {
|
||||
if (!password || !confirmPassword || password != confirmPassword) {
|
||||
OpenToast('Check Password', 'warning', 3000);
|
||||
} else {
|
||||
updateUser();
|
||||
}
|
||||
} else if (passwordStatus == 2) {
|
||||
if (!password) {
|
||||
OpenToast('Invalid Password', 'warning', 3000);
|
||||
}
|
||||
loginsend();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', display: 'flex', bgcolor: '#f8fafc' }}>
|
||||
{loading && <Loader />}
|
||||
<Theme theme={doormileTheme} mode="light">
|
||||
<AppShell contentPadding={0}>
|
||||
{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}>
|
||||
<img src={logo} alt="Doormile" style={logoLockupStyle} />
|
||||
|
||||
{/* ---- 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
|
||||
}}
|
||||
/>
|
||||
<VStack gap={2} maxWidth={430}>
|
||||
<Heading level={1} color="inherit">
|
||||
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>
|
||||
|
||||
{/* 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%)'
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
{/* ---- 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>
|
||||
|
||||
<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();
|
||||
if (passwordStatus == 0) {
|
||||
loginsend();
|
||||
} else if (passwordStatus == 1) {
|
||||
if (!password || !confirmPassword || password != confirmPassword) {
|
||||
OpenToast('Check Password', 'warning', 3000);
|
||||
} else {
|
||||
updateUser();
|
||||
}
|
||||
} else if (passwordStatus == 2) {
|
||||
if (!password) {
|
||||
OpenToast('Invalid Password', 'warning', 3000);
|
||||
}
|
||||
loginsend();
|
||||
}
|
||||
// if (currentotp) {
|
||||
// if (currentotp == otp) {
|
||||
// loginsuccessful();
|
||||
// fetchAppLocations();
|
||||
// } else {
|
||||
// opentoast('Invalid pin');
|
||||
// }
|
||||
// }
|
||||
}}
|
||||
>
|
||||
<Stack spacing={3}>
|
||||
{/* Email */}
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="E-mail Address"
|
||||
variant="outlined"
|
||||
autoComplete="email"
|
||||
required
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value.toLocaleLowerCase())}
|
||||
InputProps={{ readOnly: passwordStatus }}
|
||||
/>
|
||||
{/* Setup Password */}
|
||||
{passwordStatus == 1 && (
|
||||
<Stack display={'flex'} flexDirection={'column'} spacing={3}>
|
||||
<Typography variant="h4" textAlign="start" mb={3}>
|
||||
Setup Password
|
||||
</Typography>
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="Enter New Password"
|
||||
variant="outlined"
|
||||
required
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
<form noValidate onSubmit={handleSubmit}>
|
||||
<VStack gap={3} padding={0}>
|
||||
<TextInput
|
||||
hasAutoFocus
|
||||
label="E-mail Address"
|
||||
type="email"
|
||||
isRequired
|
||||
value={username}
|
||||
onChange={(value) => setUsername(value.toLocaleLowerCase())}
|
||||
isDisabled={!!passwordStatus}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
error={confirmPassword !== '' && password !== confirmPassword}
|
||||
fullWidth
|
||||
label="Re-Enter Password"
|
||||
variant="outlined"
|
||||
required
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowConfirmPassword((prev) => !prev)} edge="end">
|
||||
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{/* Enter Password */}
|
||||
{passwordStatus == 2 && (
|
||||
<Stack display={'flex'} flexDirection={'column'} spacing={3}>
|
||||
<Typography variant="h4" textAlign="start" mb={3}>
|
||||
Enter Password
|
||||
</Typography>
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="Enter Password"
|
||||
variant="outlined"
|
||||
required
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{/* Setup Password */}
|
||||
{passwordStatus == 1 && (
|
||||
<VStack gap={3} padding={0}>
|
||||
<Heading level={4}>Setup Password</Heading>
|
||||
<TextInput
|
||||
hasAutoFocus
|
||||
label="Enter New Password"
|
||||
type="password"
|
||||
isRequired
|
||||
value={password}
|
||||
onChange={(value) => setPassword(value)}
|
||||
/>
|
||||
<TextInput
|
||||
label="Re-Enter Password"
|
||||
type="password"
|
||||
isRequired
|
||||
value={confirmPassword}
|
||||
onChange={(value) => setConfirmPassword(value)}
|
||||
status={
|
||||
confirmPassword !== '' && password !== confirmPassword
|
||||
? { type: 'error', message: 'Passwords do not match' }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* OTP */}
|
||||
{isPassword && (
|
||||
<Stack spacing={1.5}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<FormLabel>Enter Password</FormLabel>
|
||||
<Link
|
||||
variant="body2"
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setOtp('');
|
||||
loginsend();
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Link>
|
||||
</Stack>
|
||||
{/* Enter Password */}
|
||||
{passwordStatus == 2 && (
|
||||
<VStack gap={3} padding={0}>
|
||||
<Heading level={4}>Enter Password</Heading>
|
||||
<TextInput
|
||||
hasAutoFocus
|
||||
label="Enter Password"
|
||||
type="password"
|
||||
isRequired
|
||||
value={password}
|
||||
onChange={(value) => setPassword(value)}
|
||||
/>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* <OtpInput
|
||||
shouldAutoFocus
|
||||
value={otp}
|
||||
onChange={(otp) => setOtp(otp)}
|
||||
numInputs={4}
|
||||
containerStyle={{ justifyContent: 'space-between' }}
|
||||
inputStyle={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${borderColor}`,
|
||||
fontSize: 18
|
||||
}}
|
||||
focusStyle={{
|
||||
outline: 'none',
|
||||
border: `1px solid ${theme.palette.primary.main}`,
|
||||
boxShadow: theme.customShadows.primary
|
||||
}}
|
||||
/> */}
|
||||
<TextField type="passowrd" value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||
</Stack>
|
||||
)}
|
||||
{/* Submit */}
|
||||
<AnimateButton>
|
||||
<Button fullWidth size="large" type="submit" variant="contained" color="primary">
|
||||
Continue
|
||||
</Button>
|
||||
</AnimateButton>
|
||||
</Stack>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* OTP / retry */}
|
||||
{isPassword && (
|
||||
<VStack gap={1.5} padding={0}>
|
||||
<HStack justify="between">
|
||||
<Text type="label">Enter Password</Text>
|
||||
<Link
|
||||
onClick={() => {
|
||||
setOtp('');
|
||||
loginsend();
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Link>
|
||||
</HStack>
|
||||
<TextInput label="Password" type="password" value={password} onChange={(value) => setPassword(value)} isLabelHidden />
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* footer */}
|
||||
<Stack direction="row" justifyContent="center" alignItems="center" flexWrap="wrap" useFlexGap spacing={2} sx={{ mt: 3 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component={Link}
|
||||
href="https://nearle.in"
|
||||
target="_blank"
|
||||
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
|
||||
>
|
||||
© All rights reserved
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component={Link}
|
||||
href="https://nearle.in/terms"
|
||||
target="_blank"
|
||||
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
|
||||
>
|
||||
Terms and Conditions
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component={Link}
|
||||
href="https://nearle.in/privacy"
|
||||
target="_blank"
|
||||
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
|
||||
>
|
||||
Privacy Policy
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Button label="Continue" type="submit" variant="primary" size="lg" width="100%" />
|
||||
</VStack>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
{/* footer */}
|
||||
<HStack justify="center" wrap="wrap" gap={2}>
|
||||
<Link href="https://nearle.in" target="_blank" isExternalLink>
|
||||
© All rights reserved
|
||||
</Link>
|
||||
<Link href="https://nearle.in/terms" target="_blank" isExternalLink>
|
||||
Terms and Conditions
|
||||
</Link>
|
||||
<Link href="https://nearle.in/privacy" target="_blank" isExternalLink>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Center>
|
||||
</HStack>
|
||||
</AppShell>
|
||||
</Theme>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,11 @@ import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import AnimateButton from 'components/@extended/AnimateButton';
|
||||
|
||||
import logo from 'assets/images/logo-nearle1.png';
|
||||
import logo from 'assets/images/doormile-logo.png';
|
||||
|
||||
// doormile-logo.png is a white asset; recolour it to brand red for this page's light background.
|
||||
const DOORMILE_RED_FILTER =
|
||||
'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)';
|
||||
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -228,7 +232,7 @@ const Login = () => {
|
||||
// sx={{ ml: 3, mt: 3 }}
|
||||
sx={{ ml: { xs: 0, md: 3 }, mt: { xs: 3, md: 1 }, textAlign: { xs: 'center', md: 'left' } }}
|
||||
>
|
||||
<img src={logo} alt="legendary" width={isMobile ? '160px' : '200px'} />
|
||||
<img src={logo} alt="Doormile" width={isMobile ? '160px' : '200px'} style={{ height: 'auto', filter: DOORMILE_RED_FILTER }} />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Grid
|
||||
|
||||
@@ -33,8 +33,8 @@ useMutation({
|
||||
|
||||
After the solver returns:
|
||||
1. Solver response → stored in the orders page state.
|
||||
2. Operator navigates to `OrdersPreview.js` (`/nearle/orders/preview`) for a first look.
|
||||
3. From there → `/nearle/dispatch/preview` (`Preview.js` in the dispatch folder) for drag-and-drop adjustment.
|
||||
2. Operator navigates to `OrdersPreview.js` (`/doormile/orders/preview`) for a first look.
|
||||
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.
|
||||
|
||||
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`).
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import {
|
||||
Autocomplete,
|
||||
Button,
|
||||
@@ -25,14 +26,14 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import dayjs from 'dayjs';
|
||||
import MainCard from 'components/MainCard';
|
||||
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 { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import Loader from 'components/Loader';
|
||||
import CircularLoader from 'components/CircularLoader';
|
||||
import { Empty } from 'antd';
|
||||
import HoverSocialCard from 'components/cards/statistics/HoverSocialCard';
|
||||
import { DashboardFilled, OpenAIFilled } from '@ant-design/icons';
|
||||
import { DashboardFilled } from '@ant-design/icons';
|
||||
import { MdDirectionsBike } from 'react-icons/md';
|
||||
import { FaMapLocationDot } from 'react-icons/fa6';
|
||||
import { HiOutlineArrowLeft } from 'react-icons/hi';
|
||||
@@ -234,9 +235,7 @@ const OrdersPreview = () => {
|
||||
|
||||
const {
|
||||
data: paymentModes = [],
|
||||
isLoading: paymentModesLoading,
|
||||
isError: paymentModesError,
|
||||
error: paymentModesErrorMessage
|
||||
isLoading: paymentModesLoading
|
||||
} = useQuery({
|
||||
queryKey: ['paymentmodes'],
|
||||
queryFn: fetchPaymentType
|
||||
@@ -246,10 +245,7 @@ const OrdersPreview = () => {
|
||||
|
||||
const {
|
||||
data: ridersList = [],
|
||||
isLoading: ridersListLoading,
|
||||
isError: ridersListError,
|
||||
error: ridersListErrorMessage,
|
||||
refetch: ridersListRefetch
|
||||
isLoading: ridersListLoading
|
||||
} = useQuery({
|
||||
queryKey: ['ridersList', appId], // Unique key for caching & re-fetching
|
||||
queryFn: fetchRidersList,
|
||||
@@ -282,13 +278,13 @@ const OrdersPreview = () => {
|
||||
onSuccess: (data, variables) => {
|
||||
console.log('data', data);
|
||||
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') {
|
||||
OpenToast('Delivery Created Successfully', 'success', 2000);
|
||||
}
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
navigate('/nearle/deliveries');
|
||||
navigate('/nearle/orders');
|
||||
}, 2000);
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -581,11 +577,6 @@ const OrdersPreview = () => {
|
||||
<TableCell>
|
||||
<Typography> {index + 1}</Typography>
|
||||
</TableCell>
|
||||
{/* {aiMode == 1 && (
|
||||
<TableCell>
|
||||
<Chip color="primary" label={val.zone_name} />
|
||||
</TableCell>
|
||||
)} */}
|
||||
<TableCell>
|
||||
<Tooltip title={val.tenantaddress}>
|
||||
<Typography variant="body1" noWrap>
|
||||
@@ -656,12 +647,6 @@ const OrdersPreview = () => {
|
||||
</Stack>
|
||||
</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">
|
||||
<Chip
|
||||
size="small"
|
||||
@@ -774,7 +759,7 @@ const OrdersPreview = () => {
|
||||
disabled={aiMode === 0 && (!rider || !payment)}
|
||||
onClick={handleManualCreateDelivery}
|
||||
>
|
||||
Assign Orders
|
||||
Finalise
|
||||
</Button>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { TableRow, TableCell, Skeleton, Stack } from '@mui/material';
|
||||
|
||||
export const OrdersTableSkeleton = ({ rowsPerPage = 5, col = 1 }) => {
|
||||
|
||||
79
src/pages/nearle/orders/RidersPinPointOSM.js
Normal file
79
src/pages/nearle/orders/RidersPinPointOSM.js
Normal 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
157
src/pages/nearle/orders/map.js
Normal file
157
src/pages/nearle/orders/map.js
Normal 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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
1610
src/pages/nearle/orders/miltiUploadBackup.js
Normal file
1610
src/pages/nearle/orders/miltiUploadBackup.js
Normal file
File diff suppressed because it is too large
Load Diff
846
src/pages/nearle/orders/multiOrderBackup.js
Normal file
846
src/pages/nearle/orders/multiOrderBackup.js
Normal 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
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React from 'react';
|
||||
import Loader from 'components/Loader';
|
||||
import { useEffect, useState, useRef, Fragment } from 'react';
|
||||
@@ -506,7 +507,7 @@ const MultipleOrders = () => {
|
||||
accessid: process.env.REACT_APP_RIDER_ACCESS_ID
|
||||
},
|
||||
notification: {
|
||||
title: 'Nearle Merchant',
|
||||
title: 'Doormile Merchant',
|
||||
body: 'An Order has been placed successfully,kindly process the same',
|
||||
sound: 'ring'
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState, useRef, Fragment } from 'react';
|
||||
import {
|
||||
@@ -786,7 +787,7 @@ const Createorder1 = () => {
|
||||
// notifyadmin(admintoken);
|
||||
sendnotifications();
|
||||
}
|
||||
navigate('/nearle/orders');
|
||||
navigate('/doormile/orders');
|
||||
} else {
|
||||
opentoast('Error in creating orders', 'warning');
|
||||
}
|
||||
@@ -837,7 +838,7 @@ const Createorder1 = () => {
|
||||
accessid: process.env.REACT_APP_RIDER_ACCESS_ID
|
||||
},
|
||||
notification: {
|
||||
title: 'Nearle Merchant',
|
||||
title: 'Doormile Merchant',
|
||||
body: 'An Order has been placed successfully,kindly process the same',
|
||||
sound: 'ring'
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import {
|
||||
Autocomplete,
|
||||
Button,
|
||||
@@ -200,7 +201,7 @@ const OptimisedOrderPreview = () => {
|
||||
onSettled: () => {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
navigate('/nearle/deliveries');
|
||||
navigate('/doormile/deliveries');
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
@@ -279,7 +280,7 @@ const OptimisedOrderPreview = () => {
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Tooltip title="Back to orders" placement="top">
|
||||
<IconButton
|
||||
onClick={() => navigate('/nearle/orders')}
|
||||
onClick={() => navigate('/doormile/orders')}
|
||||
sx={{
|
||||
bgcolor: 'action.hover',
|
||||
'&:hover': { bgcolor: 'action.selected' }
|
||||
@@ -419,7 +420,7 @@ const OptimisedOrderPreview = () => {
|
||||
<MobileCardList>
|
||||
{orders.map((val, i) => {
|
||||
const typeAccent =
|
||||
val.ordertype === 'Economy' ? '#10b981' : val.ordertype === 'Risky' ? '#ef4444' : '#662582';
|
||||
val.ordertype === 'Economy' ? '#10b981' : val.ordertype === 'Risky' ? '#ef4444' : '#C01227';
|
||||
return (
|
||||
<MobileCard key={i} accent={typeAccent}>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
|
||||
@@ -719,7 +720,7 @@ const OptimisedOrderPreview = () => {
|
||||
color="secondary"
|
||||
startIcon={<ArrowBackIcon />}
|
||||
onClick={() => {
|
||||
navigate('/nearle/orders');
|
||||
navigate('/doormile/orders');
|
||||
}}
|
||||
>
|
||||
Back
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
54
src/pages/nearle/orders/ridersPinPoint.js
Normal file
54
src/pages/nearle/orders/ridersPinPoint.js
Normal 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>
|
||||
);
|
||||
}
|
||||
2560
src/pages/nearle/orders/rough.js
Normal file
2560
src/pages/nearle/orders/rough.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { LoadScriptNext, GoogleMap } from '@react-google-maps/api';
|
||||
import { DT } from 'themes/dt/tokens';
|
||||
|
||||
const containerStyle = {
|
||||
width: '100%',
|
||||
@@ -46,7 +47,7 @@ const MapWithRouteGoogle = ({ coordinates, additionalProps, setMapOpen }) => {
|
||||
const route = new window.google.maps.Polyline({
|
||||
path: numericCoordinates,
|
||||
geodesic: false,
|
||||
strokeColor: '#1A73E8',
|
||||
strokeColor: DT.brand,
|
||||
strokeOpacity: 1.0,
|
||||
strokeWeight: 4
|
||||
});
|
||||
@@ -69,11 +70,13 @@ const MapWithRouteGoogle = ({ coordinates, additionalProps, setMapOpen }) => {
|
||||
right: 10,
|
||||
zIndex: 999,
|
||||
padding: '6px 12px',
|
||||
background: '#1A73E8',
|
||||
background: DT.brand,
|
||||
color: 'white',
|
||||
borderRadius: 6,
|
||||
borderRadius: DT.radiusInner,
|
||||
cursor: 'pointer',
|
||||
border: 'none'
|
||||
border: 'none',
|
||||
fontWeight: 600,
|
||||
boxShadow: DT.shadowMd
|
||||
}}
|
||||
>
|
||||
Close
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -107,7 +107,7 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
|
||||
// brand purple to match the planned-route polyline below.
|
||||
const stepIcon = (n, isFocused) => {
|
||||
const size = isFocused ? 38 : 32;
|
||||
const color = isFocused ? '#4D1C61' : '#662582';
|
||||
const color = isFocused ? '#910E1D' : '#C01227';
|
||||
const svg = encodeURIComponent(
|
||||
`<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"/>` +
|
||||
@@ -126,7 +126,7 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
|
||||
px: 2,
|
||||
py: 1.25,
|
||||
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',
|
||||
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. */}
|
||||
<Polyline
|
||||
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. */}
|
||||
<Polyline
|
||||
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
|
||||
path={dropPath}
|
||||
options={{
|
||||
strokeColor: '#662582',
|
||||
strokeColor: '#C01227',
|
||||
strokeOpacity: 0,
|
||||
strokeWeight: 0,
|
||||
icons: [
|
||||
@@ -229,7 +229,7 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
|
||||
icon: {
|
||||
path: 'M 0,-1 0,1',
|
||||
strokeOpacity: 0.6,
|
||||
strokeColor: '#662582',
|
||||
strokeColor: '#C01227',
|
||||
scale: 3
|
||||
},
|
||||
offset: '0',
|
||||
|
||||
@@ -5,8 +5,8 @@ import 'leaflet/dist/leaflet.css';
|
||||
import dayjs from 'dayjs';
|
||||
import { Chip, Stack, Typography, Box } from '@mui/material';
|
||||
import { CloseCircleOutlined } from '@ant-design/icons';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import CircularLoader from 'components/CircularLoader';
|
||||
import { DT } from 'themes/dt/tokens';
|
||||
|
||||
var utc = require('dayjs/plugin/utc');
|
||||
dayjs.extend(utc);
|
||||
@@ -33,7 +33,6 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => {
|
||||
console.log('additionalProps', additionalProps);
|
||||
|
||||
const mapRef = useRef(null);
|
||||
const theme = useTheme();
|
||||
const [routePoints, setRoutePoints] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -145,13 +144,13 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => {
|
||||
top: 12,
|
||||
right: 12,
|
||||
zIndex: 2000,
|
||||
bgcolor: theme.palette.error.main,
|
||||
bgcolor: DT.brand,
|
||||
color: '#fff',
|
||||
fontWeight: 600,
|
||||
borderRadius: '12px',
|
||||
borderRadius: DT.radiusInner + 'px',
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
boxShadow: theme.shadows[4],
|
||||
boxShadow: DT.shadowMd,
|
||||
cursor: 'pointer',
|
||||
'& .MuiChip-icon': { color: '#fff' }
|
||||
}}
|
||||
@@ -183,7 +182,7 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => {
|
||||
width: '100%',
|
||||
bgcolor: 'rgba(255,255,255,0.96)',
|
||||
p: 2,
|
||||
boxShadow: theme.shadows[3],
|
||||
boxShadow: DT.shadowPop,
|
||||
zIndex: 1500
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -114,8 +114,8 @@ const soft = (c) => a(c, '18');
|
||||
const ring = (c) => a(c, '26');
|
||||
const edge = (c) => a(c, '55');
|
||||
|
||||
const BRAND = '#662582';
|
||||
const BRAND_LIGHT = '#9255AB';
|
||||
const BRAND = '#C01227';
|
||||
const BRAND_LIGHT = '#D25463';
|
||||
|
||||
const SoftPaper = (props) => (
|
||||
<Paper
|
||||
@@ -153,7 +153,7 @@ const pillFieldSx = (color) => ({
|
||||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||||
'&:hover fieldset': { borderColor: '#cbd5e1' },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }
|
||||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1104,15 +1104,15 @@ export default function OrdersDetails() {
|
||||
flexShrink: 0,
|
||||
cursor: 'pointer',
|
||||
borderRadius: 999,
|
||||
border: `1px solid ${active ? meta.color : DT.borderSubtle}`,
|
||||
bgcolor: active ? meta.color : DT.surface,
|
||||
border: `1px solid ${active ? '#C01227' : DT.borderSubtle}`,
|
||||
bgcolor: active ? '#C01227' : 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
|
||||
borderColor: active ? '#C01227' : '#cbd5e1',
|
||||
bgcolor: active ? '#C01227' : DT.surfaceAlt
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -1172,7 +1172,7 @@ export default function OrdersDetails() {
|
||||
bgcolor: '#ffffff',
|
||||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||||
'&: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)}` }
|
||||
}}
|
||||
/>
|
||||
@@ -1817,10 +1817,21 @@ export default function OrdersDetails() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
<Divider />
|
||||
<Divider sx={{ minWidth: { xs: '100%', md: 1600 } }} />
|
||||
{rows?.length !== 0 && (
|
||||
<Stack justifyContent="center" alignItems="center" sx={{ width: '100%', py: 2 }}>
|
||||
<Stack ref={loadMoreRef} style={{ textAlign: 'center', width: '100%' }}>
|
||||
<Box sx={{ minWidth: { xs: '100%', md: 1600 }, py: 2 }}>
|
||||
<Stack
|
||||
ref={loadMoreRef}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
left: 0,
|
||||
width: '100%',
|
||||
maxWidth: '100vw',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{isFetchingNextPage || hasNextPage ? (
|
||||
<LoaderWithImage />
|
||||
) : (
|
||||
@@ -1829,7 +1840,7 @@ export default function OrdersDetails() {
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
|
||||
@@ -87,8 +87,8 @@ const soft = (c) => a(c, '18');
|
||||
const ring = (c) => a(c, '26');
|
||||
const edge = (c) => a(c, '55');
|
||||
|
||||
const BRAND = '#662582';
|
||||
const BRAND_LIGHT = '#9255AB';
|
||||
const BRAND = '#C01227';
|
||||
const BRAND_LIGHT = '#D25463';
|
||||
|
||||
const SoftPaper = (props) => (
|
||||
<Paper
|
||||
@@ -190,7 +190,7 @@ const pillFieldSx = (color) => ({
|
||||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||||
'&:hover fieldset': { borderColor: '#cbd5e1' },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }
|
||||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -588,7 +588,7 @@ export default function OrdersReport() {
|
||||
bgcolor: '#ffffff',
|
||||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||||
'&: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)}` }
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -86,8 +86,8 @@ const soft = (c) => a(c, '18');
|
||||
const ring = (c) => a(c, '26');
|
||||
const edge = (c) => a(c, '55');
|
||||
|
||||
const BRAND = '#662582';
|
||||
const BRAND_LIGHT = '#9255AB';
|
||||
const BRAND = '#C01227';
|
||||
const BRAND_LIGHT = '#D25463';
|
||||
|
||||
const SoftPaper = (props) => (
|
||||
<Paper
|
||||
@@ -265,17 +265,9 @@ export default function RidersSummary() {
|
||||
const getuserdeliverylogs = async (userid) => {
|
||||
setRouteLoading(true);
|
||||
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 =
|
||||
`${process.env.REACT_APP_URL}/deliveries/getdeliveries/` +
|
||||
`?${scopeParam}` +
|
||||
`?applocationid=${appId}` +
|
||||
`&status=all` +
|
||||
`&fromdate=${startdate}` +
|
||||
`&todate=${enddate}` +
|
||||
@@ -456,7 +448,7 @@ export default function RidersSummary() {
|
||||
bgcolor: '#ffffff',
|
||||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||||
'&: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)}` }
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -31,9 +31,12 @@ import {
|
||||
FormLabel,
|
||||
DialogActions,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
useTheme,
|
||||
Paper
|
||||
} from '@mui/material';
|
||||
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 { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
@@ -202,7 +205,7 @@ const Requests = () => {
|
||||
const [currenttenantid] = useState('');
|
||||
const [latlong, setLatlong] = useState({});
|
||||
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
|
||||
// const [alertmessage, setAlertmessage] = useState('');
|
||||
const [alertmessage, setAlertmessage] = useState('');
|
||||
// const [toast, setToast] = useState(false);
|
||||
const [rolesarr, setRolesarr] = useState([]);
|
||||
const [roleslist] = useState([]);
|
||||
@@ -215,6 +218,12 @@ const Requests = () => {
|
||||
const [refno, setRefno] = useState('');
|
||||
const [requestor, setRequestor] = 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(() => {
|
||||
setRolesarr([
|
||||
@@ -902,7 +911,7 @@ const Requests = () => {
|
||||
<MobileCardList scroll>
|
||||
{loading &&
|
||||
[0, 1, 2, 3, 4].map((item) => (
|
||||
<MobileCard key={item} accent="#662582">
|
||||
<MobileCard key={item} accent="#C01227">
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Skeleton variant="circular" width={32} height={32} />
|
||||
<Stack sx={{ flex: 1 }}>
|
||||
@@ -923,13 +932,13 @@ const Requests = () => {
|
||||
return (
|
||||
<MobileCard
|
||||
key={row.sno}
|
||||
accent="#662582"
|
||||
accent="#C01227"
|
||||
selected={isItemSelected}
|
||||
onClick={(event) => handleClick(event, row.sno)}
|
||||
header={
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
|
||||
<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() : '#'}
|
||||
</Avatar>
|
||||
<Stack sx={{ minWidth: 0 }}>
|
||||
@@ -942,7 +951,7 @@ const Requests = () => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
{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>
|
||||
}
|
||||
@@ -1813,8 +1822,6 @@ const Requests = () => {
|
||||
const [searchword, setSearchword] = useState('');
|
||||
const [dialogopen, setDialogopen] = useState(false);
|
||||
|
||||
// const [expandopen, setExpandopen] = React.useState('');
|
||||
|
||||
// const setinitial = (val)=>{
|
||||
// if(val){
|
||||
|
||||
@@ -1938,26 +1945,52 @@ const Requests = () => {
|
||||
xs={12}
|
||||
// sx={{ mb: -2.25 }}
|
||||
>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
justifyContent="space-between"
|
||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
||||
spacing={{ xs: 1.5, sm: 0 }}
|
||||
<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%)`
|
||||
}}
|
||||
>
|
||||
<Typography variant="h3">Payment Requests</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth={isMobile}
|
||||
onClick={() => {
|
||||
// setDialogopen(true)
|
||||
}}
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
justifyContent="space-between"
|
||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
||||
spacing={{ xs: 1.5, sm: 0 }}
|
||||
>
|
||||
Create Request
|
||||
</Button>
|
||||
</Stack>
|
||||
<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>
|
||||
</Stack>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth={isMobile}
|
||||
onClick={() => {
|
||||
setDialogopen(true);
|
||||
}}
|
||||
>
|
||||
Create Request
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<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
|
||||
sx={{
|
||||
p: 1,
|
||||
|
||||
@@ -269,7 +269,7 @@ export default function RiderSubstitution({
|
||||
bgcolor: BRAND,
|
||||
color: '#fff',
|
||||
'&:hover': {
|
||||
bgcolor: '#4D1C61'
|
||||
bgcolor: '#910E1D'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -657,7 +657,7 @@ export default function RiderSubstitution({
|
||||
fontWeight: 700,
|
||||
textTransform: 'none',
|
||||
'&:hover': {
|
||||
bgcolor: '#4D1C61'
|
||||
bgcolor: '#910E1D'
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -2,7 +2,9 @@ import { useEffect, useState } from 'react';
|
||||
|
||||
// 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
|
||||
// import { PatternFormat } from 'react-number-format';
|
||||
@@ -113,12 +115,6 @@ const Createrider = () => {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedImage) {
|
||||
setAvatar(URL.createObjectURL(selectedImage));
|
||||
}
|
||||
}, [selectedImage]);
|
||||
|
||||
const { ref: materialRef } = usePlacesWidget({
|
||||
apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY,
|
||||
onPlaceSelected: (place) => {
|
||||
@@ -160,14 +156,6 @@ const Createrider = () => {
|
||||
});
|
||||
|
||||
const createprofile = async () => {
|
||||
console.log('res', businessname, businessno, mobilenumber, emailaddress, address, city, zipcode);
|
||||
|
||||
// if (!businessname) {
|
||||
// opentoast('Fill Business name')
|
||||
// } else if (!businessno) {
|
||||
// opentoast('Fill Registration No')
|
||||
// }
|
||||
// else
|
||||
if (!firstname) {
|
||||
opentoast('Fill Full name');
|
||||
} else if (!mobilenumber) {
|
||||
@@ -260,16 +248,27 @@ const Createrider = () => {
|
||||
|
||||
<Box sx={{ p: { xs: 1.5, md: 3 } }}>
|
||||
<Grid item xs={12} sx={{ mb: 2 }}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
justifyContent="space-between"
|
||||
alignItems={{ xs: 'flex-start', sm: 'center' }}
|
||||
spacing={1}
|
||||
<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%)`
|
||||
}}
|
||||
>
|
||||
<Typography variant="h3">Create Rider</Typography>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={1.5} alignItems="center">
|
||||
<Avatar sx={{ width: 48, height: 48, bgcolor: DT.brand }}>
|
||||
<MdDirectionsBike size={22} />
|
||||
</Avatar>
|
||||
<Typography variant="h3">Create Rider</Typography>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<MainCard>
|
||||
<MainCard
|
||||
sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }}
|
||||
>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<MainCard
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Grid,
|
||||
@@ -17,6 +18,8 @@ import {
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { MdDirectionsBike } from 'react-icons/md';
|
||||
import { DT, tint } from 'themes/dt/tokens';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
@@ -106,9 +109,6 @@ const EditRider = () => {
|
||||
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
if (res.data.status) {
|
||||
setTenantinfo(res.data.details);
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -231,12 +231,6 @@ const EditRider = () => {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedImage) {
|
||||
setAvatar(URL.createObjectURL(selectedImage));
|
||||
}
|
||||
}, [selectedImage]);
|
||||
|
||||
const { ref: materialRef } = usePlacesWidget({
|
||||
apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY,
|
||||
onPlaceSelected: (place) => {
|
||||
@@ -265,7 +259,6 @@ const EditRider = () => {
|
||||
setCity(city1 || '');
|
||||
|
||||
setState(state1 || '');
|
||||
setZipcode(zipcode1 || '');
|
||||
setSuburb(suburb1 || '');
|
||||
|
||||
// setAddress(place.formatted_address)
|
||||
@@ -328,7 +321,7 @@ const EditRider = () => {
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
setRiderdata(null);
|
||||
navigate('/nearle/riders');
|
||||
navigate('/doormile/riders');
|
||||
setLoading(false);
|
||||
} else {
|
||||
enqueueSnackbar('Update Failed', {
|
||||
@@ -349,22 +342,33 @@ const EditRider = () => {
|
||||
</>
|
||||
)}
|
||||
<MainCard
|
||||
sx={{ borderRadius: DT.radiusCard + 'px', boxShadow: DT.shadowSoft, borderColor: DT.borderSubtle }}
|
||||
title={
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
justifyContent="space-between"
|
||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
||||
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
|
||||
startIcon={<ArrowBackIcon />}
|
||||
variant="outlined"
|
||||
fullWidth={isMobile}
|
||||
onClick={() => {
|
||||
setRiderdata(null);
|
||||
navigate('/nearle/riders');
|
||||
navigate('/doormile/riders');
|
||||
}}
|
||||
>
|
||||
Back to Riders
|
||||
@@ -664,16 +668,6 @@ const EditRider = () => {
|
||||
starttime: val.starttime,
|
||||
endtime: val.endtime
|
||||
});
|
||||
|
||||
setBasefare(val.basefare);
|
||||
setAdditionalkms(val.additionalkm);
|
||||
setOthercharges(val.additionalcharges);
|
||||
setShift(val);
|
||||
} else {
|
||||
setBasefare('');
|
||||
setAdditionalkms('');
|
||||
setOthercharges('');
|
||||
setShift({});
|
||||
}
|
||||
}}
|
||||
freeSolo
|
||||
@@ -694,7 +688,6 @@ const EditRider = () => {
|
||||
...riderdata,
|
||||
basefare: e.target.value
|
||||
});
|
||||
setBasefare(e.target.value);
|
||||
}}
|
||||
value={riderdata?.basefare}
|
||||
autoComplete="off"
|
||||
@@ -717,7 +710,6 @@ const EditRider = () => {
|
||||
...riderdata,
|
||||
additionalkm: e.target.value
|
||||
});
|
||||
setAdditionalkms(e.target.value);
|
||||
}}
|
||||
value={riderdata?.additionalkm}
|
||||
autoComplete="off"
|
||||
@@ -740,7 +732,6 @@ const EditRider = () => {
|
||||
...riderdata,
|
||||
additionalcharges: e.target.value
|
||||
});
|
||||
setOthercharges(e.target.value);
|
||||
}}
|
||||
value={riderdata?.additionalcharges}
|
||||
autoComplete="off"
|
||||
@@ -777,7 +768,6 @@ const EditRider = () => {
|
||||
...riderdata,
|
||||
accountno: e.target.value
|
||||
});
|
||||
setAccountno(e.target.value);
|
||||
}}
|
||||
autoComplete="off"
|
||||
/>
|
||||
@@ -798,7 +788,6 @@ const EditRider = () => {
|
||||
...riderdata,
|
||||
accountname: e.target.value
|
||||
});
|
||||
setAccountname(e.target.value);
|
||||
}}
|
||||
autoComplete="off"
|
||||
/>
|
||||
@@ -823,11 +812,7 @@ const EditRider = () => {
|
||||
...riderdata,
|
||||
accounttype: val.label
|
||||
});
|
||||
setAccount(val);
|
||||
setAccountType(val.label);
|
||||
// fetchroles(val.tenantid);
|
||||
} else {
|
||||
setAccount({});
|
||||
}
|
||||
}}
|
||||
freeSolo
|
||||
@@ -849,7 +834,6 @@ const EditRider = () => {
|
||||
...riderdata,
|
||||
bankname: e.target.value
|
||||
});
|
||||
setBankname(e.target.value);
|
||||
}}
|
||||
autoComplete="off"
|
||||
/>
|
||||
@@ -870,7 +854,6 @@ const EditRider = () => {
|
||||
...riderdata,
|
||||
ifsccode: e.target.value
|
||||
});
|
||||
setIfsc(e.target.value);
|
||||
}}
|
||||
autoComplete="off"
|
||||
/>
|
||||
@@ -891,7 +874,6 @@ const EditRider = () => {
|
||||
...riderdata,
|
||||
branch: e.target.value
|
||||
});
|
||||
setBranch(e.target.value);
|
||||
}}
|
||||
autoComplete="off"
|
||||
/>
|
||||
@@ -924,7 +906,6 @@ const EditRider = () => {
|
||||
onChange={(e, val) => {
|
||||
if (val) {
|
||||
console.log('vehi', val);
|
||||
setVehicle(val);
|
||||
setRiderdata({
|
||||
...riderdata,
|
||||
vehiclename: val.label,
|
||||
@@ -932,8 +913,6 @@ const EditRider = () => {
|
||||
});
|
||||
|
||||
// fetchroles(val.tenantid);
|
||||
} else {
|
||||
setVehicle({});
|
||||
}
|
||||
}}
|
||||
freeSolo
|
||||
@@ -974,7 +953,6 @@ const EditRider = () => {
|
||||
...riderdata,
|
||||
model: e.target.value
|
||||
});
|
||||
setModelyear(e.target.value);
|
||||
}}
|
||||
value={riderdata?.model}
|
||||
autoComplete="off"
|
||||
@@ -995,7 +973,6 @@ const EditRider = () => {
|
||||
...riderdata,
|
||||
color: e.target.value
|
||||
});
|
||||
setVehiclecolor(e.target.value);
|
||||
}}
|
||||
value={riderdata?.color}
|
||||
autoComplete="off"
|
||||
@@ -1054,7 +1031,6 @@ const EditRider = () => {
|
||||
label="Date"
|
||||
value={dayjs(riderdata?.insurancedate)}
|
||||
onChange={(e) => {
|
||||
setExpirydate(dayjs(e.$d).format('YYYY-MM-DD 00:00:00'));
|
||||
setRiderdata({
|
||||
...riderdata,
|
||||
insurancedate: dayjs(e.$d).format('YYYY-MM-DD 00:00:00')
|
||||
@@ -1078,16 +1054,16 @@ const EditRider = () => {
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
backgroundColor: 'secondary.lighter',
|
||||
backgroundColor: DT.surfaceAlt,
|
||||
p: 2,
|
||||
zIndex: 10,
|
||||
border: ' 1px solid ',
|
||||
borderColor: '#E6EBF1',
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
borderTop: 'none'
|
||||
}}
|
||||
>
|
||||
<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
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -77,7 +77,7 @@ import RiderSubstitution from './RiderSubstitution';
|
||||
// ============================================================================
|
||||
// Design tokens — shared with the deliveries / tenants / customers pages so
|
||||
// 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.
|
||||
// ============================================================================
|
||||
const DT = {
|
||||
@@ -100,7 +100,7 @@ const soft = (c) => a(c, '18');
|
||||
const ring = (c) => a(c, '26');
|
||||
const edge = (c) => a(c, '55');
|
||||
|
||||
const BRAND = '#662582';
|
||||
const BRAND = '#C01227';
|
||||
|
||||
const SoftPaper = (props) => (
|
||||
<Paper
|
||||
@@ -144,10 +144,10 @@ const STATUS_META = {
|
||||
// Pill-tab definitions for the rider listing tabs. Keeps brand purple for the
|
||||
// "ALL" view and emerald for "Active" so the colour matches the count's meaning.
|
||||
const TAB_META = [
|
||||
{ key: 0, label: 'All Riders', color: BRAND, icon: MdGroups, countKey: 'total' },
|
||||
{ key: 1, label: 'Active', color: '#10b981', icon: MdCheckCircle, countKey: 'active' },
|
||||
{ key: 2, label: 'Substitutes', color: '#8b5cf6', icon: MdTwoWheeler, countKey: 'substitute' },
|
||||
{ key: 3, label: 'Substitution History', color: '#f59e0b', icon: MdAccessTime, countKey: 'history' }
|
||||
{ key: 0, label: 'All Riders', color: BRAND, icon: MdGroups, countKey: 'total' },
|
||||
{ key: 1, label: 'Active', color: BRAND, icon: MdCheckCircle, countKey: 'active' },
|
||||
{ key: 2, label: 'Substitutes', color: BRAND, icon: MdTwoWheeler, countKey: 'substitute' },
|
||||
{ key: 3, label: 'Substitution History', color: BRAND, icon: MdAccessTime, countKey: 'history' }
|
||||
];
|
||||
|
||||
const KPI_META = (summary) => [
|
||||
@@ -763,7 +763,7 @@ const Riders = () => {
|
||||
bgcolor: '#ffffff',
|
||||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||||
'&: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)}` }
|
||||
}}
|
||||
/>
|
||||
@@ -813,7 +813,7 @@ const Riders = () => {
|
||||
bgcolor: BRAND,
|
||||
color: '#fff',
|
||||
'&:hover': {
|
||||
bgcolor: '#4D1C61'
|
||||
bgcolor: '#910E1D'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1244,7 +1244,7 @@ const Riders = () => {
|
||||
'&:hover': { bgcolor: BRAND, color: '#fff' }
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate('/nearle/riders/edit', { state: { riderdata: row } });
|
||||
navigate('/doormile/riders/edit', { state: { riderdata: row } });
|
||||
}}
|
||||
>
|
||||
<MdEdit size={14} />
|
||||
@@ -1696,7 +1696,7 @@ const Riders = () => {
|
||||
'&:hover': { bgcolor: BRAND, color: '#fff' }
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate('/nearle/riders/edit', { state: { riderdata: row } });
|
||||
navigate('/doormile/riders/edit', { state: { riderdata: row } });
|
||||
}}
|
||||
>
|
||||
<MdEdit size={14} />
|
||||
@@ -1914,7 +1914,7 @@ const Riders = () => {
|
||||
fontWeight: 600,
|
||||
bgcolor: BRAND,
|
||||
color: '#fff',
|
||||
'&:hover': { bgcolor: '#4D1C61' }
|
||||
'&:hover': { bgcolor: '#910E1D' }
|
||||
}}
|
||||
>
|
||||
Save Changes
|
||||
|
||||
@@ -31,8 +31,8 @@ const DT = {
|
||||
surface: '#ffffff',
|
||||
surfaceAlt: '#f8fafc'
|
||||
};
|
||||
const BRAND = '#662582';
|
||||
const BRAND_LIGHT = '#9255AB';
|
||||
const BRAND = '#C01227';
|
||||
const BRAND_LIGHT = '#D25463';
|
||||
const tint = (c) => `${c}08`;
|
||||
const soft = (c) => `${c}18`;
|
||||
|
||||
@@ -143,7 +143,7 @@ const ViewProfile = () => {
|
||||
color: '#fff',
|
||||
fontSize: { xs: 24, md: 28 },
|
||||
fontWeight: 700,
|
||||
boxShadow: '0 10px 24px rgba(102, 37, 130, 0.35)'
|
||||
boxShadow: '0 10px 24px rgba(192, 18, 39, 0.35)'
|
||||
}}
|
||||
>
|
||||
{initialsOf(fullname)}
|
||||
|
||||
@@ -11,7 +11,6 @@ import Loadable from 'components/Loadable';
|
||||
// const AuthForgotPassword = Loadable(lazy(() => import('pages/auth/forgot-password')));
|
||||
// const AuthCheckMail = Loadable(lazy(() => import('pages/auth/check-mail')));
|
||||
// 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')));
|
||||
|
||||
// ==============================|| AUTH ROUTING ||============================== //
|
||||
@@ -59,10 +58,6 @@ const LoginRoutes = {
|
||||
// {
|
||||
// path: 'reset-password',
|
||||
// element: <AuthResetPassword />
|
||||
// },
|
||||
// {
|
||||
// path: 'code-verification',
|
||||
// element: <AuthCodeVerification />
|
||||
// }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,17 +17,10 @@ const MaintenanceComingSoon = Loadable(lazy(() => import('pages/maintenance/comi
|
||||
const Login = Loadable(lazy(() => import('pages/nearle/login1')));
|
||||
// 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 OrdersPreview = Loadable(lazy(() => import('pages/nearle/orders/OrdersPreview')));
|
||||
|
||||
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')));
|
||||
|
||||
@@ -37,14 +30,11 @@ const Createorder1 = Loadable(lazy(() => import('pages/nearle/orders/createorder
|
||||
const MultipleOrders = Loadable(lazy(() => import('pages/nearle/orders/multipleOrders')));
|
||||
|
||||
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 OrdersSummary = Loadable(lazy(() => import('pages/nearle/reports/ordersSummary')));
|
||||
const OrdersDetails = Loadable(lazy(() => import('pages/nearle/reports/ordersDetails')));
|
||||
const RidersSummary = Loadable(lazy(() => import('pages/nearle/reports/ridersSummary')));
|
||||
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 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 DispatchPreview = Loadable(lazy(() => import('pages/nearle/dispatch/Preview')));
|
||||
|
||||
|
||||
// ==============================|| MAIN ROUTING ||============================== //
|
||||
|
||||
const MainRoutes = {
|
||||
@@ -67,7 +56,7 @@ const MainRoutes = {
|
||||
),
|
||||
children: [
|
||||
{
|
||||
path: 'nearle',
|
||||
path: 'doormile',
|
||||
children: [
|
||||
{
|
||||
path: 'orders',
|
||||
@@ -81,36 +70,6 @@ const MainRoutes = {
|
||||
path: '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',
|
||||
element: <Requests />
|
||||
@@ -144,10 +103,6 @@ const MainRoutes = {
|
||||
path: 'clients/create',
|
||||
element: <Createclient />
|
||||
},
|
||||
{
|
||||
path: 'customer/create',
|
||||
element: <CreateCustomer />
|
||||
},
|
||||
{
|
||||
path: 'reports',
|
||||
children: [
|
||||
@@ -162,14 +117,6 @@ const MainRoutes = {
|
||||
{
|
||||
path: 'riderssummary',
|
||||
element: <RidersSummary />
|
||||
},
|
||||
{
|
||||
path: 'riderslogs',
|
||||
element: <RidersLogs />
|
||||
},
|
||||
{
|
||||
path: 'profitability',
|
||||
element: <Profitability />
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -221,9 +168,8 @@ const MainRoutes = {
|
||||
element: <MaintenanceComingSoon />
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
export default MainRoutes;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user