Compare commits
24 Commits
e21a1e66b5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 49e86a4428 | |||
| 18d35a0ad2 | |||
| 709a06274a | |||
| 1a71732396 | |||
| b8f1cd5f1d | |||
| 283c24942d | |||
| 6143644039 | |||
| c22daee1de | |||
| a6e62a70f0 | |||
| ab59421861 | |||
| c52350df0f | |||
| ed7640ad1e | |||
| 8224f45974 | |||
| d90609f5a0 | |||
| e468861709 | |||
| d716bed6a5 | |||
| bb0296777b | |||
| 67afa55c5b | |||
| 17faae1f6e | |||
| 96db14331b | |||
| e385e2fbfd | |||
| b4cf2be556 | |||
| 39e97c3041 | |||
| f50d5a0c56 |
7
.env
@@ -6,9 +6,10 @@ REACT_APP_API_URL=https://mock-data-api-nextjs.vercel.app/
|
||||
|
||||
## Google Map Key
|
||||
|
||||
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
|
||||
REACT_APP_URL2=https://jupiter.nearle.app/live/api/v2
|
||||
REACT_APP_URL3=https://jupiter.nearle.app/live/api/v3
|
||||
REACT_APP_URL=https://api.doormile.com/api/v1
|
||||
REACT_APP_URL2=https://api.doormile.com/api/v1
|
||||
REACT_APP_URL3=https://api.doormile.com/api/v1
|
||||
REACT_APP_INTERNAL_KEY=doormile-internal-2024
|
||||
REACT_APP_STAFF_TOKEN=
|
||||
REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8
|
||||
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
|
||||
REACT_APP_URL2=https://jupiter.nearle.app/live/api/v2
|
||||
REACT_APP_URL3=https://jupiter.nearle.app/live/api/v3
|
||||
REACT_APP_URL=https://api.doormile.com/api/v1
|
||||
REACT_APP_URL2=https://api.doormile.com/api/v1
|
||||
REACT_APP_URL3=https://api.doormile.com/api/v1
|
||||
REACT_APP_INTERNAL_KEY=doormile-internal-2024
|
||||
REACT_APP_STAFF_TOKEN=
|
||||
REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8
|
||||
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk
|
||||
|
||||
GENERATE_SOURCEMAP=false
|
||||
DISABLE_ESLINT_PLUGIN=true
|
||||
|
||||
|
||||
|
||||
|
||||
10
.env.staging
@@ -1,3 +1,7 @@
|
||||
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
|
||||
REACT_APP_URL2=
|
||||
REACT_APP_STAFF_TOKEN=
|
||||
REACT_APP_URL=https://api.doormile.com/api/v1
|
||||
REACT_APP_URL2=https://api.doormile.com/api/v1
|
||||
REACT_APP_INTERNAL_KEY=doormile-internal-2024
|
||||
REACT_APP_STAFF_TOKEN=
|
||||
|
||||
GENERATE_SOURCEMAP=false
|
||||
DISABLE_ESLINT_PLUGIN=true
|
||||
8
.gitignore
vendored
@@ -104,3 +104,11 @@ dist
|
||||
|
||||
# wincompare file
|
||||
*.bak
|
||||
|
||||
# Env files — only env.staging (no leading dot) is the agreed-shared
|
||||
# baseline and gets committed. Everything else holds live API keys.
|
||||
.env
|
||||
.env.development
|
||||
.env.staging
|
||||
.env.production
|
||||
.env.local
|
||||
|
||||
32
CLAUDE.md
@@ -174,43 +174,43 @@ const AccentAvatar = ({ color, selected, size = 24, children }) => (
|
||||
|
||||
### Page anatomy (every operator page should follow this order)
|
||||
|
||||
> **Use brand purple `#662582` (with light variant `#9255AB`) for every brand surface below.** Don't use indigo `#6366f1` — that's reserved for the "Accepted" status badge only.
|
||||
> **Use brand red `#C01227` (with light variant `#D35968`) for every brand surface below.** Don't use indigo `#6366f1` — that's reserved for the "Accepted" status badge only.
|
||||
|
||||
1. **Gradient header `<Paper>`** — `linear-gradient(135deg, ${tint('#662582')} 0%, ${tint('#9255AB')} 100%)`, 48px filled `#662582` avatar with a page icon, `Typography variant="h3"` title, "Live · {zone}" sub-line with an 8px green pulsing dot, and a pill `LocationAutocomplete` on the right (`pill accentColor="#662582" paperComponent={SoftPaper}`).
|
||||
2. **KPI tiles row** — `Grid` of 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('#D35968')} 100%)`, 48px filled `#C01227` avatar with a page icon, `Typography variant="h3"` title, "Live · {zone}" sub-line with an 8px green pulsing dot, and a pill `LocationAutocomplete` on the right (`pill accentColor="#C01227" paperComponent={SoftPaper}`).
|
||||
2. **KPI tiles row** — `Grid` of 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` — NearlExpress brand red.** This is the canonical primary colour for this app, defined in `src/themes/theme/default.js` as `primary.main`. It drives the sidebar, the logo, and is what every new surface (page headers, KPI primary tile, search bars, edit-action buttons, dialog/popup headers, scrollbars) must use as the brand accent.
|
||||
|
||||
Variants (also from `theme/default.js`):
|
||||
|
||||
| Token | Hex | Use |
|
||||
|---|---|---|
|
||||
| `primary.lighter` | `#E8D9EF` | Very subtle wash bg |
|
||||
| `primary.light` / `primary.400` | `#9255AB` | Gradient pair with main |
|
||||
| `primary.main` | `#662582` | Brand primary — default for all brand surfaces |
|
||||
| `primary.dark` | `#4D1C61` | Hover / pressed states |
|
||||
| `primary.darker` | `#260E30` | Deep contrast text on light bg |
|
||||
| `primary.lighter` | `#F6DBDF` | Very subtle wash bg |
|
||||
| `primary.light` / `primary.400` | `#D35968` | Gradient pair with main |
|
||||
| `primary.main` | `#C01227` | Brand primary — default for all brand surfaces |
|
||||
| `primary.dark` | `#900E1D` | Hover / pressed states |
|
||||
| `primary.darker` | `#47070E` | Deep contrast text on light bg |
|
||||
|
||||
**Page header / dialog header gradient:** `linear-gradient(135deg, ${tint('#662582')} 0%, ${tint('#9255AB')} 100%)` (subtle wash) or `linear-gradient(135deg, #662582 0%, #9255AB 100%)` (solid, for dialog titles).
|
||||
**Page header / dialog header gradient:** `linear-gradient(135deg, ${tint('#C01227')} 0%, ${tint('#D35968')} 100%)` (subtle wash) or `linear-gradient(135deg, #C01227 0%, #D35968 100%)` (solid, for dialog titles).
|
||||
|
||||
> **Migration note:** `deliveries.js`, `Tenants.js`, and `clientPricing.js` currently use `#6366f1` (indigo) as their brand accent — a holdover from the first design pass before brand purple was canonicalised. They are scheduled to migrate to `#662582`. `customers.js` and the createorder1 Saved-Address dialog have already been migrated.
|
||||
> **Migration note:** `deliveries.js`, `Tenants.js`, and `clientPricing.js` currently use `#6366f1` (indigo) as their brand accent — a holdover from the first design pass before brand red was canonicalised. They are scheduled to migrate to `#C01227`. `customers.js` and the createorder1 Saved-Address dialog have already been migrated.
|
||||
|
||||
### Status palette (semantic — distinct from brand)
|
||||
|
||||
These colour-code lifecycle states. Do **not** swap them for brand purple — operators rely on the colour to identify status at a glance.
|
||||
These colour-code lifecycle states. Do **not** swap them for brand red — operators rely on the colour to identify status at a glance.
|
||||
|
||||
| Meaning | Colour |
|
||||
|--------------------------|-----------|
|
||||
| Pending / waiting | `#f59e0b` (amber) |
|
||||
| Accepted / assigned | `#6366f1` (indigo — semantically distinct from brand purple) |
|
||||
| Accepted / assigned | `#6366f1` (indigo — semantically distinct from brand red) |
|
||||
| Arrived | `#06b6d4` (cyan) |
|
||||
| Picked up | `#8b5cf6` (light purple — distinct from brand) |
|
||||
| Active / in-transit | `#14b8a6` (teal) |
|
||||
|
||||
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
# Move to Nginx's public folder
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
# 1. CRUCIAL: Remove Nginx's default "Welcome" page files completely
|
||||
RUN rm -rf ./*
|
||||
|
||||
# 2. Copy your compiled static assets into the root folder.
|
||||
# NOTE: If your folder is named "dist" instead of "build", change "build/" to "dist/"
|
||||
COPY build/ .
|
||||
|
||||
# 3. Copy your custom Nginx configuration (which you already have in your log)
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,4 +1,4 @@
|
||||
# NearleXpress - Operator Dispatch Console & Deliveries Portal
|
||||
# Doormile - Operator Dispatch Console & Deliveries Portal
|
||||
|
||||
A high-fidelity, real-time dispatcher command center and order-delivery console built using React, React-Leaflet, TanStack Query, and Material UI.
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
REACT_APP_URL='https://jupiter.nearle.app/live/api/v1'
|
||||
REACT_APP_URL2=''
|
||||
REACT_APP_URL='https://api.doormile.com/api/v1'
|
||||
REACT_APP_URL2='https://api.doormile.com/api/v1'
|
||||
REACT_APP_INTERNAL_KEY='doormile-internal-2024'
|
||||
REACT_APP_STAFF_TOKEN=''
|
||||
18
nginx.conf
Normal file
@@ -0,0 +1,18 @@
|
||||
events {}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
# This line forces Nginx to pass routing back to React Router
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
package-lock.json
generated
@@ -21417,20 +21417,6 @@
|
||||
"is-typedarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ua-parser-js": {
|
||||
"version": "1.0.40",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz",
|
||||
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 69 KiB |
@@ -4,8 +4,8 @@
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Nearle Neighbourhood commerce" />
|
||||
<meta name="theme-color" content="#C01227" />
|
||||
<meta name="description" content="Doormile — corporate logistics & last-mile delivery admin console" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
@@ -28,7 +28,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Nearle Console</title>
|
||||
<title>Doormile Console</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
|
||||
BIN
public/logo192.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
53
src/App.js
@@ -2,7 +2,6 @@
|
||||
import Routes from 'routes';
|
||||
import ThemeCustomization from 'themes';
|
||||
import Locales from 'components/Locales';
|
||||
// import RTLLayout from 'components/RTLLayout';
|
||||
import ScrollTop from 'components/ScrollTop';
|
||||
import Snackbar from 'components/@extended/Snackbar';
|
||||
import Notistack from 'components/third-party/Notistack';
|
||||
@@ -10,12 +9,30 @@ import { useNavigate } from 'react-router';
|
||||
import { useEffect } from 'react';
|
||||
import { generateToken, initFirebaseNotificationListener } from 'firebase_notification/notification';
|
||||
import InternetStatus from 'components/updateNetworkStatus';
|
||||
|
||||
// auth-provider
|
||||
// import { JWTProvider as AuthProvider } from 'contexts/JWTContext';
|
||||
import useInactivityLogout from 'hooks/useInactivityLogout';
|
||||
|
||||
// ==============================|| APP - THEME, ROUTER, LOCAL ||============================== //
|
||||
|
||||
// Was previously declared inline inside App(), which made it a fresh
|
||||
// component reference on every App render — React would unmount/remount it
|
||||
// each time, re-firing its useEffect (and the FCM permission toast) on
|
||||
// every App re-render instead of once per app load.
|
||||
const AppContent = () => {
|
||||
useInactivityLogout();
|
||||
|
||||
useEffect(() => {
|
||||
generateToken();
|
||||
initFirebaseNotificationListener();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Routes />
|
||||
<Snackbar />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
@@ -24,39 +41,17 @@ const App = () => {
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
const AppContent = () => {
|
||||
useEffect(() => {
|
||||
generateToken();
|
||||
initFirebaseNotificationListener();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Routes />
|
||||
<Snackbar />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThemeCustomization>
|
||||
<InternetStatus />
|
||||
{/* <RTLLayout> */}
|
||||
<Locales>
|
||||
<ScrollTop>
|
||||
{/* <AuthProvider> */}
|
||||
<>
|
||||
<Notistack>
|
||||
{/* <Routes />
|
||||
<Snackbar /> */}
|
||||
<AppContent />
|
||||
</Notistack>
|
||||
</>
|
||||
{/* </AuthProvider> */}
|
||||
<Notistack>
|
||||
<AppContent />
|
||||
</Notistack>
|
||||
</ScrollTop>
|
||||
</Locales>
|
||||
{/* </RTLLayout> */}
|
||||
</ThemeCustomization>
|
||||
</>
|
||||
);
|
||||
|
||||
BIN
src/assets/images/doormile-logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/images/doormile-mark.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
@@ -1,32 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// material-ui
|
||||
import { Box, CircularProgress, Typography } from '@mui/material';
|
||||
|
||||
// ==============================|| PROGRESS - CIRCULAR LABEL ||============================== //
|
||||
|
||||
export default function CircularWithLabel({ value, ...others }) {
|
||||
return (
|
||||
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
||||
<CircularProgress variant="determinate" value={value} {...others} />
|
||||
<Box
|
||||
sx={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" component="div" color="text.secondary">{`${Math.round(value)}%`}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
CircularWithLabel.propTypes = {
|
||||
value: PropTypes.number
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// material-ui
|
||||
import { Box, CircularProgress, Typography, circularProgressClasses } from '@mui/material';
|
||||
|
||||
// ==============================|| PROGRESS - CIRCULAR PATH ||============================== //
|
||||
|
||||
export default function CircularWithPath({ value, size, variant, thickness, showLabel, pathColor, sx, ...others }) {
|
||||
return (
|
||||
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
||||
<CircularProgress
|
||||
variant="determinate"
|
||||
sx={{ color: pathColor ? pathColor : 'grey.200' }}
|
||||
size={size}
|
||||
thickness={thickness}
|
||||
{...others}
|
||||
value={100}
|
||||
/>
|
||||
{showLabel && (
|
||||
<Box
|
||||
sx={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" component="div" color="text.secondary">
|
||||
{value ? `${Math.round(value)}%` : '0%'}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<CircularProgress
|
||||
variant={variant}
|
||||
sx={{
|
||||
...sx,
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
[`& .${circularProgressClasses.circle}`]: {
|
||||
strokeLinecap: 'round'
|
||||
}
|
||||
}}
|
||||
size={size}
|
||||
thickness={thickness}
|
||||
value={value}
|
||||
{...others}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
CircularWithPath.propTypes = {
|
||||
value: PropTypes.number,
|
||||
size: PropTypes.number,
|
||||
variant: PropTypes.string,
|
||||
thickness: PropTypes.number,
|
||||
showLabel: PropTypes.bool,
|
||||
pathColor: PropTypes.string,
|
||||
sx: PropTypes.array,
|
||||
others: PropTypes.array
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// material-ui
|
||||
import { Box, LinearProgress } from '@mui/material';
|
||||
|
||||
// ==============================|| PROGRESS - LINEAR ICON ||============================== //
|
||||
|
||||
export default function LinearWithIcon({ icon, value, ...others }) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box sx={{ width: '100%', mr: 1 }}>
|
||||
<LinearProgress variant="determinate" value={value} {...others} />
|
||||
</Box>
|
||||
<Box sx={{ minWidth: 35 }}>{icon}</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
LinearWithIcon.propTypes = {
|
||||
icon: PropTypes.node,
|
||||
value: PropTypes.number
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// material-ui
|
||||
import { Box, LinearProgress, Typography } from '@mui/material';
|
||||
|
||||
// ==============================|| PROGRESS - LINEAR WITH LABEL ||============================== //
|
||||
|
||||
export default function LinearWithLabel({ value, ...others }) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box sx={{ width: '100%', mr: 1 }}>
|
||||
<LinearProgress variant="determinate" value={value} {...others} />
|
||||
</Box>
|
||||
<Box sx={{ minWidth: 35 }}>
|
||||
<Typography variant="body2" color="text.secondary">{`${Math.round(value)}%`}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
LinearWithLabel.propTypes = {
|
||||
value: PropTypes.number
|
||||
};
|
||||
@@ -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')(() => ({
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// material-ui
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import createCache from '@emotion/cache';
|
||||
|
||||
// third-party
|
||||
import rtlPlugin from 'stylis-plugin-rtl';
|
||||
|
||||
// project import
|
||||
import { ThemeDirection } from 'config';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
|
||||
// ==============================|| RTL LAYOUT ||============================== //
|
||||
|
||||
const RTLLayout = ({ children }) => {
|
||||
const { themeDirection } = useConfig();
|
||||
|
||||
useEffect(() => {
|
||||
document.dir = themeDirection;
|
||||
}, [themeDirection]);
|
||||
|
||||
const cacheRtl = createCache({
|
||||
key: themeDirection === ThemeDirection.RTL ? 'rtl' : 'css',
|
||||
prepend: true,
|
||||
stylisPlugins: themeDirection === ThemeDirection.RTL ? [rtlPlugin] : []
|
||||
});
|
||||
|
||||
return <CacheProvider value={cacheRtl}>{children}</CacheProvider>;
|
||||
};
|
||||
|
||||
RTLLayout.propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
export default RTLLayout;
|
||||
@@ -1,10 +0,0 @@
|
||||
// material-ui
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
const ScrollX = styled('div')({
|
||||
width: '100%',
|
||||
overflowX: 'auto',
|
||||
display: 'block'
|
||||
});
|
||||
|
||||
export default ScrollX;
|
||||
@@ -1,49 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// material-ui
|
||||
import { Box, Grid, Link, Stack, Typography } from '@mui/material';
|
||||
|
||||
// assets
|
||||
import { GlobalOutlined, NodeExpandOutlined } from '@ant-design/icons';
|
||||
|
||||
// ==============================|| COMPONENTS - BREADCRUMBS ||============================== //
|
||||
|
||||
const ComponentHeader = ({ title, caption, directory, link }) => (
|
||||
<Box sx={{ pl: 3 }}>
|
||||
<Stack spacing={1.25}>
|
||||
<Typography variant="h2">{title}</Typography>
|
||||
{caption && (
|
||||
<Typography variant="h6" color="textSecondary">
|
||||
{caption}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<Grid container spacing={0.75} sx={{ mt: 1.75 }}>
|
||||
{directory && (
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
<NodeExpandOutlined style={{ marginRight: 10 }} />
|
||||
{directory}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
{link && (
|
||||
<Grid item xs={12}>
|
||||
<Link variant="caption" color="primary" href={link} target="_blank">
|
||||
<GlobalOutlined style={{ marginRight: 10 }} />
|
||||
{link}
|
||||
</Link>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
|
||||
ComponentHeader.propTypes = {
|
||||
title: PropTypes.string,
|
||||
caption: PropTypes.string,
|
||||
directory: PropTypes.string,
|
||||
link: PropTypes.string
|
||||
};
|
||||
|
||||
export default ComponentHeader;
|
||||
@@ -1,52 +0,0 @@
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Fab, Badge } from '@mui/material';
|
||||
|
||||
// project import
|
||||
|
||||
// assets
|
||||
|
||||
// ==============================|| CART ITEMS - FLOATING BUTTON ||============================== //
|
||||
|
||||
const FloatingCart = ({ element, count = 0, onClick, sx }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
// const cart = useSelector((state) => state.cart);
|
||||
// const totalQuantity = sum(cart.checkout.products.map((item) => item.quantity));
|
||||
|
||||
return (
|
||||
<Fab
|
||||
// component={Link}
|
||||
// to="/apps/e-commerce/checkout"
|
||||
onClick={onClick} // ← important
|
||||
size="large"
|
||||
sx={{
|
||||
top: '75%',
|
||||
position: 'fixed',
|
||||
right: 0,
|
||||
zIndex: theme.zIndex.speedDial,
|
||||
boxShadow: theme.customShadows.primary,
|
||||
bgcolor: 'primary.lighter',
|
||||
color: 'primary.main',
|
||||
borderRadius: '25%',
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
'&:hover': {
|
||||
bgcolor: 'primary.100',
|
||||
boxShadow: theme.customShadows.primary
|
||||
},
|
||||
'&:focus-visible': {
|
||||
outline: `2px solid ${theme.palette.primary.dark}`,
|
||||
outlineOffset: 2
|
||||
},
|
||||
...sx
|
||||
}}
|
||||
>
|
||||
<Badge showZero badgeContent={count} color="error">
|
||||
{element}
|
||||
</Badge>
|
||||
</Fab>
|
||||
);
|
||||
};
|
||||
|
||||
export default FloatingCart;
|
||||
@@ -1,176 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Box, Button, CardContent, CardMedia, Chip, Divider, Grid, Rating, Stack, Typography } from '@mui/material';
|
||||
|
||||
// project import
|
||||
import MainCard from 'components/MainCard';
|
||||
import IconButton from 'components/@extended/IconButton';
|
||||
import SkeletonProductPlaceholder from 'components/cards/skeleton/ProductPlaceholder';
|
||||
import { useDispatch, useSelector } from 'store';
|
||||
import { addProduct } from 'store/reducers/cart';
|
||||
import { openSnackbar } from 'store/reducers/snackbar';
|
||||
|
||||
// assets
|
||||
import { HeartOutlined, HeartFilled } from '@ant-design/icons';
|
||||
|
||||
const prodImage = require.context('assets/images/e-commerce', true);
|
||||
|
||||
// ==============================|| PRODUCT CARD ||============================== //
|
||||
|
||||
const ProductCard = ({ id, color, name, brand, offer, isStock, image, description, offerPrice, salePrice, rating }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const prodProfile = image && prodImage(`./${image}`);
|
||||
const [productRating] = useState(rating);
|
||||
const [wishlisted, setWishlisted] = useState(false);
|
||||
const cart = useSelector((state) => state.cart);
|
||||
|
||||
const addCart = () => {
|
||||
dispatch(addProduct({ id, name, image, salePrice, offerPrice, color, size: 8, quantity: 1, description }, cart.checkout.products));
|
||||
dispatch(
|
||||
openSnackbar({
|
||||
open: true,
|
||||
message: 'Add To Cart Success',
|
||||
variant: 'alert',
|
||||
alert: {
|
||||
color: 'success'
|
||||
},
|
||||
close: false
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const addToFavourite = () => {
|
||||
setWishlisted(!wishlisted);
|
||||
dispatch(
|
||||
openSnackbar({
|
||||
open: true,
|
||||
message: 'Added to favourites',
|
||||
variant: 'alert',
|
||||
alert: {
|
||||
color: 'success'
|
||||
},
|
||||
close: false
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<SkeletonProductPlaceholder />
|
||||
) : (
|
||||
<MainCard
|
||||
content={false}
|
||||
boxShadow
|
||||
sx={{
|
||||
'&:hover': {
|
||||
transform: 'scale3d(1.02, 1.02, 1)',
|
||||
transition: 'all .4s ease-in-out'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: 250, m: 'auto' }}>
|
||||
<CardMedia
|
||||
sx={{ height: 250, textDecoration: 'none', opacity: isStock ? 1 : 0.25 }}
|
||||
image={prodProfile}
|
||||
component={Link}
|
||||
to={`/apps/e-commerce/product-details/${id}`}
|
||||
/>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
sx={{ width: '100%', position: 'absolute', top: 0, pt: 1.75, pl: 2, pr: 1 }}
|
||||
>
|
||||
{!isStock && <Chip variant="light" color="error" size="small" label="Sold out" />}
|
||||
{offer && <Chip label={offer} variant="combined" color="success" size="small" />}
|
||||
<IconButton color="secondary" sx={{ ml: 'auto', '&:hover': { background: 'transparent' } }} onClick={addToFavourite}>
|
||||
{wishlisted ? (
|
||||
<HeartFilled style={{ fontSize: '1.15rem', color: theme.palette.error.main }} />
|
||||
) : (
|
||||
<HeartOutlined style={{ fontSize: '1.15rem' }} />
|
||||
)}
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<CardContent sx={{ p: 2 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Stack>
|
||||
<Typography
|
||||
component={Link}
|
||||
to={`/apps/e-commerce/product-details/${id}`}
|
||||
color="textPrimary"
|
||||
variant="h5"
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'block',
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography variant="h6" color="textSecondary">
|
||||
{brand}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="flex-end" flexWrap="wrap" rowGap={1.75}>
|
||||
<Stack>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Typography variant="h5">${offerPrice}</Typography>
|
||||
{salePrice && (
|
||||
<Typography variant="h6" color="textSecondary" sx={{ textDecoration: 'line-through' }}>
|
||||
${salePrice}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack direction="row" alignItems="flex-start">
|
||||
<Rating precision={0.5} name="size-small" value={productRating} size="small" readOnly />
|
||||
<Typography variant="caption">({productRating?.toFixed(1)})</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Button variant="contained" onClick={addCart} disabled={!isStock}>
|
||||
{!isStock ? 'Sold Out' : 'Add to Cart'}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</MainCard>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ProductCard.propTypes = {
|
||||
id: PropTypes.number,
|
||||
color: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
brand: PropTypes.string,
|
||||
isStock: PropTypes.bool,
|
||||
image: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
offerPrice: PropTypes.number,
|
||||
salePrice: PropTypes.number,
|
||||
offer: PropTypes.string,
|
||||
rating: PropTypes.number
|
||||
};
|
||||
|
||||
export default ProductCard;
|
||||
@@ -1,52 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// material-ui
|
||||
import { Grid, Rating, Stack, Typography } from '@mui/material';
|
||||
|
||||
// project imports
|
||||
import Avatar from 'components/@extended/Avatar';
|
||||
|
||||
// assets
|
||||
import { StarFilled, StarOutlined } from '@ant-design/icons';
|
||||
|
||||
const avatarImage = require.context('assets/images/users', true);
|
||||
|
||||
// ==============================|| PRODUCT DETAILS - REVIEW ||============================== //
|
||||
|
||||
const ProductReview = ({ avatar, date, name, rating, review }) => (
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Avatar alt={name} src={avatar && avatarImage(`./${avatar}`)} />
|
||||
<Stack spacing={2}>
|
||||
<Stack>
|
||||
<Typography variant="subtitle1" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{date}
|
||||
</Typography>
|
||||
<Rating
|
||||
size="small"
|
||||
name="simple-controlled"
|
||||
value={rating < 4 ? rating + 1 : rating}
|
||||
icon={<StarFilled style={{ fontSize: 'inherit' }} />}
|
||||
emptyIcon={<StarOutlined style={{ fontSize: 'inherit' }} />}
|
||||
precision={0.1}
|
||||
readOnly
|
||||
/>
|
||||
</Stack>
|
||||
<Typography variant="body2">{review}</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
ProductReview.propTypes = {
|
||||
avatar: PropTypes.string,
|
||||
date: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
rating: PropTypes.number,
|
||||
review: PropTypes.string
|
||||
};
|
||||
|
||||
export default ProductReview;
|
||||
@@ -1,44 +0,0 @@
|
||||
// material-ui
|
||||
import { CardContent, Grid, Skeleton, Stack } from '@mui/material';
|
||||
|
||||
// project import
|
||||
import MainCard from 'components/MainCard';
|
||||
|
||||
// ===========================|| SKELETON - PRODUCT CARD ||=========================== //
|
||||
|
||||
const ProductPlaceholder = () => (
|
||||
<MainCard content={false} boxShadow>
|
||||
<Skeleton variant="rectangular" height={220} />
|
||||
<CardContent sx={{ p: 2 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Skeleton variant="rectangular" height={20} />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Skeleton variant="rectangular" height={45} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sx={{ pt: '8px !important' }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Skeleton variant="rectangular" height={20} width={90} />
|
||||
<Skeleton variant="rectangular" height={20} width={38} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<Skeleton variant="rectangular" height={20} width={40} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Skeleton variant="rectangular" height={17} width={20} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Skeleton variant="rectangular" height={32} width={47} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</MainCard>
|
||||
);
|
||||
|
||||
export default ProductPlaceholder;
|
||||
@@ -1,66 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// material-ui
|
||||
import { Box, Chip, Grid, Stack, Typography } from '@mui/material';
|
||||
|
||||
// project import
|
||||
import MainCard from 'components/MainCard';
|
||||
|
||||
// assets
|
||||
import { FallOutlined, RiseOutlined } from '@ant-design/icons';
|
||||
|
||||
// ==============================|| STATISTICS - ECOMMERCE CARD ||============================== //
|
||||
|
||||
const AnalyticEcommerce = ({ color = 'primary', title, count, percentage, isLoss, extra }) => (
|
||||
<MainCard contentSX={{ p: 2.25 }}>
|
||||
<Stack spacing={0.5}>
|
||||
<Typography variant="h6" color="textSecondary">
|
||||
{title}
|
||||
</Typography>
|
||||
<Grid container alignItems="center">
|
||||
<Grid item>
|
||||
<Typography variant="h4" color="inherit">
|
||||
{count}
|
||||
</Typography>
|
||||
</Grid>
|
||||
{percentage && (
|
||||
<Grid item>
|
||||
<Chip
|
||||
variant="combined"
|
||||
color={color}
|
||||
icon={
|
||||
<>
|
||||
{!isLoss && <RiseOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
|
||||
{isLoss && <FallOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
|
||||
</>
|
||||
}
|
||||
label={`${percentage}%`}
|
||||
sx={{ ml: 1.25, pl: 1 }}
|
||||
size="small"
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Stack>
|
||||
<Box sx={{ pt: 2.25 }}>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
You made an extra{' '}
|
||||
<Typography component="span" variant="caption" sx={{ color: `${color || 'primary'}.main` }}>
|
||||
{extra}
|
||||
</Typography>{' '}
|
||||
this year
|
||||
</Typography>
|
||||
</Box>
|
||||
</MainCard>
|
||||
);
|
||||
|
||||
AnalyticEcommerce.propTypes = {
|
||||
title: PropTypes.string,
|
||||
count: PropTypes.string,
|
||||
percentage: PropTypes.number,
|
||||
isLoss: PropTypes.bool,
|
||||
color: PropTypes.string,
|
||||
extra: PropTypes.string
|
||||
};
|
||||
|
||||
export default AnalyticEcommerce;
|
||||
@@ -1,56 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// material-ui
|
||||
import { Box, Chip, Stack, Typography } from '@mui/material';
|
||||
|
||||
// project import
|
||||
import MainCard from 'components/MainCard';
|
||||
|
||||
// assets
|
||||
import { RiseOutlined, FallOutlined } from '@ant-design/icons';
|
||||
|
||||
// ==============================|| STATISTICS - ECOMMERCE CARD ||============================== //
|
||||
|
||||
const AnalyticsDataCard = ({ color = 'primary', title, count, percentage, isLoss, children }) => (
|
||||
<MainCard content={false}>
|
||||
<Box sx={{ p: 2.25 }}>
|
||||
<Stack spacing={0.5}>
|
||||
<Typography variant="h6" color="textSecondary">
|
||||
{title}
|
||||
</Typography>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Typography variant="h4" color="inherit">
|
||||
{count}
|
||||
</Typography>
|
||||
{percentage && (
|
||||
<Chip
|
||||
variant="combined"
|
||||
color={color}
|
||||
icon={
|
||||
<>
|
||||
{!isLoss && <RiseOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
|
||||
{isLoss && <FallOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />}
|
||||
</>
|
||||
}
|
||||
label={`${percentage}%`}
|
||||
sx={{ ml: 1.25, pl: 1 }}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
{children}
|
||||
</MainCard>
|
||||
);
|
||||
|
||||
AnalyticsDataCard.propTypes = {
|
||||
title: PropTypes.string,
|
||||
count: PropTypes.string,
|
||||
percentage: PropTypes.number,
|
||||
isLoss: PropTypes.bool,
|
||||
color: PropTypes.string,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
export default AnalyticsDataCard;
|
||||
@@ -26,13 +26,13 @@ The `DT` design tokens (palette, alpha helpers, `pillFieldSx`, `SoftPaper`, `Acc
|
||||
|
||||
**Hard rules when editing components here:**
|
||||
|
||||
- **Universal brand colour is `#662582`** (NearlExpress purple — from `themes/theme/default.js` `primary.main`). Every brand surface (page header, dialog header, KPI primary tile, search bars, edit-action buttons, scrollbars) uses this. Gradient pair: `#662582 → #9255AB`.
|
||||
- **Semantic status palette is distinct** from the brand. Use these for lifecycle indicators only: sky `#0ea5e9`, emerald `#10b981`, amber `#f59e0b`, red `#ef4444`, status-purple `#8b5cf6` (lighter than brand), cyan `#06b6d4`, teal `#14b8a6`, orange `#f97316`, muted `#94a3b8`, indigo `#6366f1` (Accepted status). Don't replace these with brand purple — operators colour-code on them.
|
||||
- **Universal brand colour is `#C01227`** (Doormile red — from `themes/theme/default.js` `primary.main`). Every brand surface (page header, dialog header, KPI primary tile, search bars, edit-action buttons, scrollbars) uses this. Gradient pair: `#C01227 → #D35968`.
|
||||
- **Semantic status palette is distinct** from the brand. Use these for lifecycle indicators only: sky `#0ea5e9`, emerald `#10b981`, amber `#f59e0b`, red `#ef4444`, status-purple `#8b5cf6` (lighter than brand), cyan `#06b6d4`, teal `#14b8a6`, orange `#f97316`, muted `#94a3b8`, indigo `#6366f1` (Accepted status). Don't replace these with brand red — operators colour-code on them.
|
||||
- Don't introduce a colour from `theme.palette` for new surfaces — those are Mantis defaults and don't match the DT system. Use the hex values above directly.
|
||||
- Border radii: `12` (inner), `16` (card), `999` (pill). No other values.
|
||||
- Shadows: `DT.shadowSoft` / `DT.shadowMd` / `DT.shadowPop`. No raw `box-shadow` strings.
|
||||
|
||||
> Some existing redesigned pages (`deliveries.js`, `Tenants.js`, `clientPricing.js`) still use `#6366f1` as the brand accent — this is a legacy from the first design pass. `customers.js` and the `createorder1` Saved-Address dialog are already on `#662582`. When you next edit one of the legacy pages, migrate it to brand purple in the same PR.
|
||||
> Some existing redesigned pages (`deliveries.js`, `Tenants.js`, `clientPricing.js`) still use `#6366f1` as the brand accent — this is a legacy from the first design pass. `customers.js` and the `createorder1` Saved-Address dialog are already on `#C01227`. When you next edit one of the legacy pages, migrate it to brand red in the same PR.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 } } }}>
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Chip } from '@mui/material';
|
||||
|
||||
// ==============================|| STATUS CHIP (Doormile-style soft badge) ||============================== //
|
||||
// One consistent soft-filled chip for every lifecycle status across orders,
|
||||
// deliveries, riders, tenants, invoices. Colour encodes meaning; the chip stays
|
||||
// quiet (soft bg + readable dark text) so tables don't turn into a rainbow.
|
||||
|
||||
// Soft background + readable foreground per semantic tone.
|
||||
const TONE = {
|
||||
amber: { bg: '#FEF3C7', fg: '#92400E' },
|
||||
indigo: { bg: '#E0E7FF', fg: '#3730A3' },
|
||||
cyan: { bg: '#CFFAFE', fg: '#155E75' },
|
||||
violet: { bg: '#EDE9FE', fg: '#5B21B6' },
|
||||
teal: { bg: '#CCFBF1', fg: '#115E59' },
|
||||
emerald: { bg: '#D1FAE5', fg: '#065F46' },
|
||||
red: { bg: '#FEE2E2', fg: '#991B1B' },
|
||||
orange: { bg: '#FFEDD5', fg: '#9A3412' },
|
||||
sky: { bg: '#E0F2FE', fg: '#075985' },
|
||||
slate: { bg: '#F1F5F9', fg: '#475569' },
|
||||
brand: { bg: '#F1E6F7', fg: '#5B1F73' }
|
||||
};
|
||||
|
||||
// Status keyword -> tone + display label.
|
||||
const MAP = {
|
||||
pending: { tone: 'amber', label: 'Pending' },
|
||||
created: { tone: 'sky', label: 'Created' },
|
||||
assigned: { tone: 'indigo', label: 'Assigned' },
|
||||
accepted: { tone: 'indigo', label: 'Accepted' },
|
||||
arrived: { tone: 'cyan', label: 'Arrived' },
|
||||
picked: { tone: 'violet', label: 'Picked' },
|
||||
'picked-up':{ tone: 'violet', label: 'Picked Up' },
|
||||
started: { tone: 'cyan', label: 'Started' },
|
||||
active: { tone: 'teal', label: 'Active' },
|
||||
'in-transit': { tone: 'teal', label: 'In Transit' },
|
||||
delivered: { tone: 'emerald', label: 'Delivered' },
|
||||
completed: { tone: 'emerald', label: 'Completed' },
|
||||
skipped: { tone: 'orange', label: 'Skipped' },
|
||||
failed: { tone: 'red', label: 'Failed' },
|
||||
cancelled: { tone: 'red', label: 'Cancelled' },
|
||||
// riders / tenants
|
||||
online: { tone: 'emerald', label: 'Online' },
|
||||
offline: { tone: 'slate', label: 'Offline' },
|
||||
inactive: { tone: 'slate', label: 'Inactive' },
|
||||
idle: { tone: 'amber', label: 'Idle' },
|
||||
unknown: { tone: 'slate', label: 'Unknown' },
|
||||
// invoices
|
||||
paid: { tone: 'emerald', label: 'Paid' },
|
||||
unpaid: { tone: 'amber', label: 'Unpaid' },
|
||||
open: { tone: 'sky', label: 'Open' },
|
||||
overdue: { tone: 'red', label: 'Overdue' },
|
||||
prepaid: { tone: 'emerald', label: 'Prepaid' },
|
||||
cod: { tone: 'amber', label: 'COD' }
|
||||
};
|
||||
|
||||
export default function StatusChip({ status, label, size = 'small', sx }) {
|
||||
const key = String(status || '').toLowerCase().trim().replace(/\s+/g, '-');
|
||||
const cfg = MAP[key] || { tone: 'slate', label: status || '—' };
|
||||
const tone = TONE[cfg.tone] || TONE.slate;
|
||||
return (
|
||||
<Chip
|
||||
size={size}
|
||||
label={label || cfg.label}
|
||||
sx={{ bgcolor: tone.bg, color: tone.fg, border: 'none', fontWeight: 600, ...sx }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
StatusChip.propTypes = {
|
||||
status: PropTypes.string,
|
||||
label: PropTypes.node,
|
||||
size: PropTypes.string,
|
||||
sx: PropTypes.object
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Stack, Typography, Box } from '@mui/material';
|
||||
|
||||
const TitleCard = ({ sx, title, children, starticon }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
...sx
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" flexWrap={'wrap'} alignItems="center" justifyContent="space-between" gap={1}>
|
||||
<Stack>
|
||||
{starticon && starticon}
|
||||
<Typography variant="h3">{title}</Typography>
|
||||
</Stack>
|
||||
{children}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TitleCard;
|
||||
@@ -1,147 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { createContext, useEffect, useReducer } from 'react';
|
||||
|
||||
// third-party
|
||||
import { Chance } from 'chance';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
// reducer - state management
|
||||
import { LOGIN, LOGOUT } from 'store/reducers/actions';
|
||||
import authReducer from 'store/reducers/auth';
|
||||
|
||||
// project import
|
||||
import Loader from 'components/Loader';
|
||||
import axios from 'utils/axios';
|
||||
|
||||
const chance = new Chance();
|
||||
|
||||
// constant
|
||||
const initialState = {
|
||||
isLoggedIn: false,
|
||||
isInitialized: false,
|
||||
user: null
|
||||
|
||||
};
|
||||
|
||||
const verifyToken = (serviceToken) => {
|
||||
if (!serviceToken) {
|
||||
return false;
|
||||
}
|
||||
const decoded = jwtDecode(serviceToken);
|
||||
/**
|
||||
* Property 'exp' does not exist on type '<T = unknown>(token: string, options?: JwtDecodeOptions | undefined) => T'.
|
||||
*/
|
||||
return decoded.exp > Date.now() / 1000;
|
||||
};
|
||||
|
||||
const setSession = (serviceToken) => {
|
||||
if (serviceToken) {
|
||||
localStorage.setItem('serviceToken', serviceToken);
|
||||
axios.defaults.headers.common.Authorization = `Bearer ${serviceToken}`;
|
||||
} else {
|
||||
localStorage.removeItem('serviceToken');
|
||||
delete axios.defaults.headers.common.Authorization;
|
||||
}
|
||||
};
|
||||
|
||||
// ==============================|| JWT CONTEXT & PROVIDER ||============================== //
|
||||
|
||||
const JWTContext = createContext(null);
|
||||
|
||||
export const JWTProvider = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(authReducer, initialState);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
console.log(verifyToken)
|
||||
// try {
|
||||
// const serviceToken = window.localStorage.getItem('serviceToken');
|
||||
// if (serviceToken && verifyToken(serviceToken)) {
|
||||
// setSession(serviceToken);
|
||||
// const response = await axios.get('/api/account/me');
|
||||
// const { user } = response.data;
|
||||
// dispatch({
|
||||
// type: LOGIN,
|
||||
// payload: {
|
||||
// isLoggedIn: true,
|
||||
// user
|
||||
// }
|
||||
// });
|
||||
// } else {
|
||||
// dispatch({
|
||||
// type: LOGOUT
|
||||
// });
|
||||
// }
|
||||
// } catch (err) {
|
||||
// console.error(err);
|
||||
// dispatch({
|
||||
// type: LOGOUT
|
||||
// });
|
||||
// }
|
||||
};
|
||||
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const login = async (email, password) => {
|
||||
const response = await axios.post('/api/account/login', { email, password });
|
||||
const { serviceToken, user } = response.data;
|
||||
setSession(serviceToken);
|
||||
dispatch({
|
||||
type: LOGIN,
|
||||
payload: {
|
||||
isLoggedIn: true,
|
||||
user
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const register = async (email, password, firstName, lastName) => {
|
||||
// todo: this flow need to be recode as it not verified
|
||||
const id = chance.bb_pin();
|
||||
const response = await axios.post('/api/account/register', {
|
||||
id,
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName
|
||||
});
|
||||
let users = response.data;
|
||||
|
||||
if (window.localStorage.getItem('users') !== undefined && window.localStorage.getItem('users') !== null) {
|
||||
const localUsers = window.localStorage.getItem('users');
|
||||
users = [
|
||||
...JSON.parse(localUsers),
|
||||
{
|
||||
id,
|
||||
email,
|
||||
password,
|
||||
name: `${firstName} ${lastName}`
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
window.localStorage.setItem('users', JSON.stringify(users));
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setSession(null);
|
||||
dispatch({ type: LOGOUT });
|
||||
};
|
||||
|
||||
const resetPassword = async () => {};
|
||||
|
||||
const updateProfile = () => {};
|
||||
|
||||
if (state.isInitialized !== undefined && !state.isInitialized) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
return <JWTContext.Provider value={{ ...state, login, logout, register, resetPassword, updateProfile }}>{children}</JWTContext.Provider>;
|
||||
};
|
||||
|
||||
JWTProvider.propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
export default JWTContext;
|
||||
@@ -22,6 +22,12 @@ const opentoast = (message, color, vertical = 'bottom') => {
|
||||
});
|
||||
};
|
||||
|
||||
// Notification.requestPermission() re-resolves with the browser's current
|
||||
// (often already-decided) permission every time this runs. Without a guard,
|
||||
// any repeat call — e.g. from a remount — re-shows the same permanent,
|
||||
// non-auto-dismissing toast and they stack up. Only warn once per tab.
|
||||
let notificationWarningShown = false;
|
||||
|
||||
// ===================== Generate FCM Token =====================
|
||||
export const generateToken = async () => {
|
||||
try {
|
||||
@@ -29,7 +35,10 @@ export const generateToken = async () => {
|
||||
dispatch(setFcmPermission(permission));
|
||||
|
||||
if (permission !== 'granted') {
|
||||
opentoast('Enable notifications to receive OTP, alerts, and updates', 'error');
|
||||
if (!notificationWarningShown) {
|
||||
notificationWarningShown = true;
|
||||
opentoast('Enable notifications to receive OTP, alerts, and updates', 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
// auth provider
|
||||
import AuthContext from 'contexts/JWTContext';
|
||||
|
||||
// ==============================|| AUTH HOOKS ||============================== //
|
||||
|
||||
const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
|
||||
if (!context) throw new Error('context must be use inside provider');
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export default useAuth;
|
||||
96
src/hooks/useInactivityLogout.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { clearFcmToken } from 'store/reducers/fcmSlice';
|
||||
import { logoutUser } from 'store/reducers/loginUserSlice';
|
||||
import {
|
||||
ABSOLUTE_SESSION_TIMEOUT_MS,
|
||||
ACTIVITY_STORAGE_KEY,
|
||||
AUTH_PRESENCE_KEY,
|
||||
INACTIVITY_TIMEOUT_MS,
|
||||
SESSION_START_STORAGE_KEY,
|
||||
isSessionActive,
|
||||
markActivity,
|
||||
markSessionStart,
|
||||
performSessionLogout
|
||||
} from 'utils/session';
|
||||
|
||||
const ACTIVITY_EVENTS = ['mousemove', 'mousedown', 'keydown', 'scroll', 'touchstart', 'wheel'];
|
||||
const ACTIVITY_WRITE_THROTTLE_MS = 5000;
|
||||
const IDLE_CHECK_INTERVAL_MS = 15000;
|
||||
|
||||
// Two independent timers, both enforced from localStorage so every tab on the
|
||||
// origin agrees:
|
||||
// - INACTIVITY_TIMEOUT_MS: logs out after 15 minutes with no interaction, so
|
||||
// a laptop left unlocked doesn't leave the console open indefinitely.
|
||||
// - ABSOLUTE_SESSION_TIMEOUT_MS: a hard 30-minute cap since login, even if
|
||||
// the user has been continuously active, so localStorage (auth keys, FCM
|
||||
// token, cached zone list) never lingers on disk longer than that.
|
||||
// A logout in one tab (auth key cleared) is picked up by the others via the
|
||||
// 'storage' event.
|
||||
const useInactivityLogout = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const dispatch = useDispatch();
|
||||
const lastWriteRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const doLogout = () => performSessionLogout({ queryClient, dispatch, clearFcmToken, logoutUser });
|
||||
|
||||
const handleActivity = () => {
|
||||
const now = Date.now();
|
||||
if (now - lastWriteRef.current < ACTIVITY_WRITE_THROTTLE_MS) return;
|
||||
lastWriteRef.current = now;
|
||||
markActivity();
|
||||
};
|
||||
|
||||
const handleStorage = (event) => {
|
||||
if (event.key === AUTH_PRESENCE_KEY && !event.newValue) {
|
||||
doLogout();
|
||||
}
|
||||
};
|
||||
|
||||
// Guards against the browser restoring a cached (bfcache) copy of a
|
||||
// protected page via the back/forward button after logout happened.
|
||||
const handlePageShow = (event) => {
|
||||
if (event.persisted && !isSessionActive()) {
|
||||
window.location.replace('/login');
|
||||
}
|
||||
};
|
||||
|
||||
if (isSessionActive()) {
|
||||
if (!localStorage.getItem(ACTIVITY_STORAGE_KEY)) markActivity();
|
||||
// Sessions that were already open before this feature shipped won't have
|
||||
// a start time yet — give them a fresh 30-minute window instead of
|
||||
// treating them as already expired.
|
||||
if (!localStorage.getItem(SESSION_START_STORAGE_KEY)) markSessionStart();
|
||||
}
|
||||
|
||||
ACTIVITY_EVENTS.forEach((eventName) => window.addEventListener(eventName, handleActivity, { passive: true }));
|
||||
window.addEventListener('storage', handleStorage);
|
||||
window.addEventListener('pageshow', handlePageShow);
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
if (!isSessionActive()) return;
|
||||
|
||||
const lastActivity = Number(localStorage.getItem(ACTIVITY_STORAGE_KEY)) || Date.now();
|
||||
if (Date.now() - lastActivity >= INACTIVITY_TIMEOUT_MS) {
|
||||
doLogout();
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionStart = Number(localStorage.getItem(SESSION_START_STORAGE_KEY)) || Date.now();
|
||||
if (Date.now() - sessionStart >= ABSOLUTE_SESSION_TIMEOUT_MS) {
|
||||
doLogout();
|
||||
}
|
||||
}, IDLE_CHECK_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
ACTIVITY_EVENTS.forEach((eventName) => window.removeEventListener(eventName, handleActivity));
|
||||
window.removeEventListener('storage', handleStorage);
|
||||
window.removeEventListener('pageshow', handlePageShow);
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [queryClient, dispatch]);
|
||||
};
|
||||
|
||||
export default useInactivityLogout;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
// ==============================|| CARD - PAGINATION ||============================== //
|
||||
|
||||
export default function usePagination(data, itemsPerPage) {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const maxPage = Math.ceil(data.length / itemsPerPage);
|
||||
|
||||
function currentData() {
|
||||
const begin = (currentPage - 1) * itemsPerPage;
|
||||
const end = begin + itemsPerPage;
|
||||
return data.slice(begin, end);
|
||||
}
|
||||
|
||||
function next() {
|
||||
setCurrentPage((currentPage) => Math.min(currentPage + 1, maxPage));
|
||||
}
|
||||
|
||||
function prev() {
|
||||
setCurrentPage((currentPage) => Math.max(currentPage - 1, 1));
|
||||
}
|
||||
|
||||
function jump(page) {
|
||||
const pageNumber = Math.max(1, page);
|
||||
setCurrentPage(() => Math.min(pageNumber, maxPage));
|
||||
}
|
||||
|
||||
return { next, prev, jump, currentData, currentPage, maxPage };
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
// ==============================|| ELEMENT REFERENCE HOOKS ||============================== //
|
||||
|
||||
const useScriptRef = () => {
|
||||
const scripted = useRef(true);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
scripted.current = false;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return scripted;
|
||||
};
|
||||
|
||||
export default useScriptRef;
|
||||
@@ -1,4 +1,5 @@
|
||||
import logger from './utils/logger';
|
||||
import axios from 'axios';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
@@ -19,6 +20,11 @@ import { store } from 'store';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
const storedToken = localStorage.getItem('token');
|
||||
if (storedToken) {
|
||||
axios.defaults.headers.common.Authorization = `Bearer ${storedToken}`;
|
||||
}
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container);
|
||||
const queryClient = new QueryClient({
|
||||
|
||||
@@ -27,7 +27,7 @@ import Transitions from 'components/@extended/Transitions';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import { dispatch, useSelector } from 'store';
|
||||
import { activeItem } from 'store/reducers/menu';
|
||||
import { MenuOrientation, ThemeMode } from 'config';
|
||||
import { MenuOrientation } from 'config';
|
||||
|
||||
// assets
|
||||
import { BorderOutlined, DownOutlined, UpOutlined, RightOutlined } from '@ant-design/icons';
|
||||
@@ -234,13 +234,27 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
|
||||
|
||||
...(drawerOpen && {
|
||||
'&:hover': {
|
||||
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter'
|
||||
bgcolor: '#7b1fa2'
|
||||
bgcolor: '#fff',
|
||||
'& .MuiTypography-root': {
|
||||
color: '#C01227 !important'
|
||||
},
|
||||
'& svg, & .anticon': {
|
||||
color: '#C01227 !important'
|
||||
}
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
bgcolor: 'transparent',
|
||||
color: iconSelectedColor,
|
||||
'&:hover': { color: iconSelectedColor, bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'transparent' }
|
||||
'&:hover': {
|
||||
bgcolor: '#fff',
|
||||
color: '#C01227',
|
||||
'& .MuiTypography-root': {
|
||||
color: '#C01227 !important'
|
||||
},
|
||||
'& svg, & .anticon': {
|
||||
color: '#C01227 !important'
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
...(!drawerOpen && {
|
||||
@@ -273,20 +287,23 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
'&:hover': {
|
||||
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'secondary.light' : 'secondary.lighter'
|
||||
bgcolor: '#7b1fa2',
|
||||
color: 'white'
|
||||
bgcolor: '#fff',
|
||||
'& svg': {
|
||||
color: '#C01227 !important'
|
||||
}
|
||||
}
|
||||
}),
|
||||
...(!drawerOpen &&
|
||||
selected === menu.id && {
|
||||
bgcolor: 'primary.light',
|
||||
color: 'primary.main',
|
||||
'&:hover': {
|
||||
bgcolor: '#7b1fa2',
|
||||
color: 'primary.main'
|
||||
bgcolor: 'primary.light',
|
||||
color: 'primary.main',
|
||||
'&:hover': {
|
||||
bgcolor: '#fff',
|
||||
'& svg': {
|
||||
color: '#C01227 !important'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
{menuIcon}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { forwardRef, useEffect, useState } from 'react';
|
||||
import { forwardRef, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
@@ -41,7 +41,7 @@ const NavItem = ({ item, level }) => {
|
||||
const isSelected = openItem.findIndex((id) => id === item.id) > -1;
|
||||
|
||||
const itemIcon = item.icon ? (
|
||||
<Icon style={{ fontSize: drawerOpen ? '1rem' : '1.25rem', color: isSelected ? '#662582' : '#fff' }} />
|
||||
<Icon style={{ fontSize: drawerOpen ? '1rem' : '1.25rem', color: isSelected ? '#C01227' : '#fff' }} />
|
||||
) : (
|
||||
false
|
||||
);
|
||||
@@ -97,87 +97,96 @@ const NavItem = ({ item, level }) => {
|
||||
onClick={() => {
|
||||
// dispatch(setSelectedMenu(item));
|
||||
}}
|
||||
sx={{
|
||||
zIndex: 1201,
|
||||
pl: drawerOpen ? `${level * 28}px` : 1.5,
|
||||
py: !drawerOpen && level === 1 ? 1.25 : 1,
|
||||
...(drawerOpen && {
|
||||
'&:hover': {
|
||||
bgcolor: '#7b1fa2'
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter',
|
||||
borderRight: `2px solid ${theme.palette.primary.main}`,
|
||||
color: iconSelectedColor,
|
||||
sx={{
|
||||
zIndex: 1201,
|
||||
pl: drawerOpen ? `${level * 28}px` : 1.5,
|
||||
py: !drawerOpen && level === 1 ? 1.25 : 1,
|
||||
...(drawerOpen && {
|
||||
'&:hover': {
|
||||
color: iconSelectedColor,
|
||||
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter'
|
||||
}
|
||||
}
|
||||
}),
|
||||
...(!drawerOpen && {
|
||||
bgcolor: '#662582',
|
||||
'&:hover': {
|
||||
bgcolor: '#662582'
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
'&:hover': {
|
||||
bgcolor: 'transparent'
|
||||
},
|
||||
bgcolor: 'transparent'
|
||||
}
|
||||
})
|
||||
}}
|
||||
{...(downLG && {
|
||||
onClick: () => {
|
||||
dispatch(openDrawer(false));
|
||||
}
|
||||
})}
|
||||
>
|
||||
{itemIcon && (
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 28,
|
||||
...(!drawerOpen && {
|
||||
borderRadius: 1.5,
|
||||
width: 36,
|
||||
height: 36,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
'&:hover': {
|
||||
bgcolor: '#7b1fa2'
|
||||
bgcolor: '#fff',
|
||||
'& .MuiTypography-root': {
|
||||
color: '#C01227 !important'
|
||||
},
|
||||
'& svg, & .anticon': {
|
||||
color: '#C01227 !important'
|
||||
}
|
||||
}),
|
||||
...(!drawerOpen &&
|
||||
isSelected && {
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter',
|
||||
borderRight: `2px solid ${theme.palette.primary.main}`,
|
||||
color: iconSelectedColor,
|
||||
'&:hover': {
|
||||
color: iconSelectedColor,
|
||||
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter'
|
||||
}
|
||||
}
|
||||
}),
|
||||
...(!drawerOpen && {
|
||||
bgcolor: '#C01227',
|
||||
'&:hover': {
|
||||
bgcolor: '#C01227'
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
'&:hover': {
|
||||
bgcolor: 'transparent'
|
||||
},
|
||||
bgcolor: 'transparent'
|
||||
}
|
||||
})
|
||||
}}
|
||||
{...(downLG && {
|
||||
onClick: () => {
|
||||
dispatch(openDrawer(false));
|
||||
}
|
||||
})}
|
||||
>
|
||||
{itemIcon && (
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 28,
|
||||
...(!drawerOpen && {
|
||||
borderRadius: 1.5,
|
||||
width: 36,
|
||||
height: 36,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
'&:hover': {
|
||||
bgcolor: '#fff',
|
||||
'& svg, & .anticon': {
|
||||
color: '#C01227 !important'
|
||||
}
|
||||
}
|
||||
}),
|
||||
...(!drawerOpen &&
|
||||
isSelected && {
|
||||
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'primary.900' : 'primary.lighter',
|
||||
'&:hover': {
|
||||
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'primary.darker' : 'primary.lighter'
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
{itemIcon}
|
||||
</ListItemIcon>
|
||||
)}
|
||||
{(drawerOpen || (!drawerOpen && level !== 1)) && (
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="h6" sx={{ color: isSelected ? iconSelectedColor : textColor, whiteSpace: 'nowrap' }}>
|
||||
{item.title}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{(drawerOpen || (!drawerOpen && level !== 1)) && item.chip && (
|
||||
<Chip
|
||||
color={item.chip.color}
|
||||
variant={item.chip.variant}
|
||||
size={item.chip.size}
|
||||
label={item.chip.label}
|
||||
avatar={item.chip.avatar && <Avatar>{item.chip.avatar}</Avatar>}
|
||||
/>
|
||||
)}
|
||||
}}
|
||||
>
|
||||
{itemIcon}
|
||||
</ListItemIcon>
|
||||
)}
|
||||
{(drawerOpen || (!drawerOpen && level !== 1)) && (
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="h6" sx={{ color: isSelected ? iconSelectedColor : textColor, whiteSpace: 'nowrap' }}>
|
||||
{item.title}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{(drawerOpen || (!drawerOpen && level !== 1)) && item.chip && (
|
||||
<Chip
|
||||
color={item.chip.color}
|
||||
variant={item.chip.variant}
|
||||
size={item.chip.size}
|
||||
label={item.chip.label}
|
||||
avatar={item.chip.avatar && <Avatar>{item.chip.avatar}</Avatar>}
|
||||
/>
|
||||
)}
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
@@ -229,11 +238,11 @@ const NavItem = ({ item, level }) => {
|
||||
}),
|
||||
...(!drawerOpen &&
|
||||
isSelected && {
|
||||
bgcolor: 'transparent',
|
||||
'&:hover': {
|
||||
bgcolor: 'transparent'
|
||||
}
|
||||
})
|
||||
bgcolor: 'transparent',
|
||||
'&:hover': {
|
||||
bgcolor: 'transparent'
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
{itemIcon}
|
||||
@@ -254,11 +263,11 @@ const NavItem = ({ item, level }) => {
|
||||
}),
|
||||
...(!drawerOpen &&
|
||||
isSelected && {
|
||||
bgcolor: 'transparent',
|
||||
'&:hover': {
|
||||
bgcolor: 'transparent'
|
||||
}
|
||||
})
|
||||
bgcolor: 'transparent',
|
||||
'&:hover': {
|
||||
bgcolor: 'transparent'
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Dot size={4} color={isSelected ? 'primary' : 'secondary'} />
|
||||
|
||||
@@ -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;
|
||||
@@ -10,8 +10,9 @@ import DrawerHeaderStyled from './DrawerHeaderStyled';
|
||||
import { MenuOrientation } from 'config';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
|
||||
import logo from 'assets/images/logo-nearle9.png'
|
||||
import logo1 from 'assets/images/logo-sm1.png'
|
||||
import logo from 'assets/images/doormile-logo.png'
|
||||
import logo1 from 'assets/images/doormile-mark.png'
|
||||
|
||||
// ==============================|| DRAWER HEADER ||============================== //
|
||||
|
||||
const DrawerHeader = ({ open }) => {
|
||||
@@ -33,19 +34,15 @@ const DrawerHeader = ({ open }) => {
|
||||
paddingLeft: isHorizontal ? { xs: '24px', lg: '0' } : open ? '24px' : 0
|
||||
}}
|
||||
>
|
||||
{/* <Logo isIcon={!open} sx={{ width: open ? 'auto' : 35, height: 35 }} /> */}
|
||||
|
||||
{(open) &&
|
||||
<img src={logo}
|
||||
// width='160px'
|
||||
// height='45px'
|
||||
// width='170px'
|
||||
alt='logo'/>
|
||||
style={{ height: '29px', width: 'auto' }}
|
||||
alt='Doormile'/>
|
||||
}
|
||||
{(!open) &&
|
||||
<img src={logo1}
|
||||
width='40px'
|
||||
alt='logo'/>
|
||||
<img src={logo1}
|
||||
width='50px'
|
||||
alt='Doormile'/>
|
||||
}
|
||||
</DrawerHeaderStyled>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ const openedMixin = (theme) => ({
|
||||
}),
|
||||
overflowX: 'hidden',
|
||||
boxShadow: theme.palette.mode === ThemeMode.DARK ? theme.customShadows.z1 : 'none',
|
||||
backgroundColor:'#662582',
|
||||
backgroundColor:'#C01227',
|
||||
});
|
||||
|
||||
const closedMixin = (theme) => ({
|
||||
@@ -27,7 +27,7 @@ const closedMixin = (theme) => ({
|
||||
width: theme.spacing(7.5),
|
||||
borderRight: 'none',
|
||||
boxShadow: theme.customShadows.z1,
|
||||
backgroundColor:'#662582',
|
||||
backgroundColor:'#C01227',
|
||||
});
|
||||
|
||||
// ==============================|| DRAWER - MINI STYLED ||============================== //
|
||||
|
||||
@@ -55,7 +55,7 @@ const MainDrawer = ({ window }) => {
|
||||
borderRight: `1px solid ${theme.palette.divider}`,
|
||||
backgroundImage: 'none',
|
||||
boxShadow: 'inherit',
|
||||
bgcolor:'#662582'
|
||||
bgcolor:'#C01227'
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { AppBar, Box, ClickAwayListener, Paper, Popper, Toolbar } from '@mui/mat
|
||||
import Profile from './Profile';
|
||||
import IconButton from 'components/@extended/IconButton';
|
||||
import Transitions from 'components/@extended/Transitions';
|
||||
import { ThemeMode } from 'config';
|
||||
|
||||
// assets
|
||||
import { MoreOutlined } from '@ant-design/icons';
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
import MainCard from 'components/MainCard';
|
||||
import IconButton from 'components/@extended/IconButton';
|
||||
import Transitions from 'components/@extended/Transitions';
|
||||
import { ThemeMode } from 'config';
|
||||
|
||||
// assets
|
||||
import { BellOutlined, CheckCircleOutlined, GiftOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
@@ -90,7 +89,7 @@ const Notification = () => {
|
||||
// color="primary"
|
||||
sx={{
|
||||
"& .MuiBadge-badge": {
|
||||
color: "#662582",
|
||||
color: "#C01227",
|
||||
backgroundColor: "white"
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -7,14 +7,10 @@ import { List, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'
|
||||
// assets
|
||||
import { EditOutlined, LogoutOutlined, CommentOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { clearFcmToken } from 'store/reducers/fcmSlice';
|
||||
import { logoutUser } from 'store/reducers/loginUserSlice';
|
||||
|
||||
// ==============================|| HEADER PROFILE - PROFILE TAB ||============================== //
|
||||
|
||||
const ProfileTab = ({ handleLogout }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const navigate = useNavigate();
|
||||
const handleListItemClick = (event, index) => {
|
||||
@@ -48,18 +44,7 @@ const ProfileTab = ({ handleLogout }) => {
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Billing" />
|
||||
</ListItemButton> */}
|
||||
<ListItemButton
|
||||
selected={selectedIndex === 3}
|
||||
// onClick={handleLogout}
|
||||
onClick={() => {
|
||||
handleLogout();
|
||||
dispatch(clearFcmToken()); // ✅ dispatch the action
|
||||
dispatch(logoutUser()); // ✅ dispatch logout user as initial state
|
||||
}}
|
||||
// onClick={()=>{
|
||||
// navigate('/login')
|
||||
// }}
|
||||
>
|
||||
<ListItemButton selected={selectedIndex === 3} onClick={handleLogout}>
|
||||
<ListItemIcon>
|
||||
<LogoutOutlined />
|
||||
</ListItemIcon>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
@@ -13,8 +13,6 @@ import Avatar from 'components/@extended/Avatar';
|
||||
import MainCard from 'components/MainCard';
|
||||
import Transitions from 'components/@extended/Transitions';
|
||||
import IconButton from 'components/@extended/IconButton';
|
||||
|
||||
import { ThemeMode } from 'config';
|
||||
// import useAuth from 'hooks/useAuth';
|
||||
|
||||
// assets
|
||||
@@ -23,6 +21,7 @@ import { LogoutOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { clearFcmToken } from 'store/reducers/fcmSlice';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { logoutUser } from 'store/reducers/loginUserSlice';
|
||||
import { performSessionLogout } from 'utils/session';
|
||||
|
||||
// tab panel wrapper
|
||||
function TabPanel({ children, value, index, ...other }) {
|
||||
@@ -50,30 +49,11 @@ function a11yProps(index) {
|
||||
|
||||
const Profile = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// const { logout, user } = useAuth();
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
// await logout();
|
||||
|
||||
// navigate(`/login`, {
|
||||
// state: {
|
||||
// from: ''
|
||||
// }
|
||||
// });
|
||||
localStorage.removeItem('firstname');
|
||||
localStorage.removeItem('appuserid');
|
||||
localStorage.removeItem('authname');
|
||||
localStorage.removeItem('roleid');
|
||||
localStorage.removeItem('tenantid');
|
||||
localStorage.clear();
|
||||
|
||||
navigate('/login');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
const handleLogout = () => {
|
||||
performSessionLogout({ queryClient, dispatch, clearFcmToken, logoutUser });
|
||||
};
|
||||
|
||||
const anchorRef = useRef(null);
|
||||
@@ -101,13 +81,7 @@ const Profile = () => {
|
||||
<ButtonBase
|
||||
sx={{
|
||||
p: 0.25,
|
||||
// bgcolor: open ? iconBackColorOpen : 'transparent',
|
||||
borderRadius: 1
|
||||
// '&:hover': { bgcolor: theme.palette.mode === ThemeMode.DARK ? 'secondary.light' : 'secondary.lighter' },
|
||||
// '&:focus-visible': {
|
||||
// outline: `2px solid ${theme.palette.secondary.dark}`,
|
||||
// outlineOffset: 2
|
||||
// }
|
||||
}}
|
||||
aria-label="open profile"
|
||||
ref={anchorRef}
|
||||
@@ -173,16 +147,7 @@ const Profile = () => {
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Tooltip title="Logout">
|
||||
<IconButton
|
||||
size="large"
|
||||
sx={{ color: 'text.primary' }}
|
||||
// onClick={handleLogout}>
|
||||
onClick={() => {
|
||||
handleLogout();
|
||||
dispatch(clearFcmToken()); // ✅ dispatch the action dispatch(logoutUser()); // ✅ dispatch logout user as initial state dispatch(logoutUser()); // ✅ dispatch logout user as initial state
|
||||
dispatch(logoutUser()); // ✅ dispatch logout user as initial state
|
||||
}}
|
||||
>
|
||||
<IconButton size="large" sx={{ color: 'text.primary' }} onClick={handleLogout}>
|
||||
<LogoutOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
// material-ui
|
||||
import {
|
||||
|
||||
@@ -9,7 +9,7 @@ import AppBarStyled from './AppBarStyled';
|
||||
import HeaderContent from './HeaderContent';
|
||||
import IconButton from 'components/@extended/IconButton';
|
||||
|
||||
import { MenuOrientation, ThemeMode } from 'config';
|
||||
import { MenuOrientation } from 'config';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import { dispatch, useSelector } from 'store';
|
||||
import { openDrawer } from 'store/reducers/menu';
|
||||
@@ -66,7 +66,7 @@ const Header = () => {
|
||||
zIndex: 1200,
|
||||
width: isHorizontal ? '100%' : drawerOpen ? 'calc(100% - 260px)' : { xs: '100%', lg: 'calc(100% - 60px)' },
|
||||
// boxShadow: theme.customShadows.z1
|
||||
bgcolor:'#662582'
|
||||
bgcolor:'#C01227'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// third-party
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlined';
|
||||
import ReceiptOutlinedIcon from '@mui/icons-material/ReceiptOutlined';
|
||||
import MopedOutlinedIcon from '@mui/icons-material/MopedOutlined';
|
||||
import NearMeOutlinedIcon from '@mui/icons-material/NearMeOutlined';
|
||||
import { TbListDetails } from 'react-icons/tb';
|
||||
|
||||
// assets
|
||||
import {
|
||||
@@ -24,7 +22,6 @@ import {
|
||||
TeamOutlined,
|
||||
MailOutlined,
|
||||
ImportOutlined,
|
||||
BarChartOutlined,
|
||||
MoneyCollectOutlined,
|
||||
FileDoneOutlined
|
||||
} from '@ant-design/icons';
|
||||
@@ -47,8 +44,6 @@ const icons = {
|
||||
TeamOutlined,
|
||||
MailOutlined,
|
||||
ImportOutlined,
|
||||
BarChartOutlined,
|
||||
ReceiptOutlinedIcon,
|
||||
NearMeOutlinedIcon,
|
||||
DirectionsBikeOutlinedIcon,
|
||||
MopedOutlinedIcon,
|
||||
@@ -59,10 +54,17 @@ const icons = {
|
||||
|
||||
const nearle = {
|
||||
id: 'nearle_Pages',
|
||||
title: <FormattedMessage id="Nearle" />,
|
||||
title: <FormattedMessage id="Doormile" />,
|
||||
icon: icons.FileDoneOutlined,
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
id: 'dashboard',
|
||||
title: <FormattedMessage id="dashboard" />,
|
||||
type: 'item',
|
||||
url: '/nearle/dashboard',
|
||||
icon: icons.DashboardOutlined
|
||||
},
|
||||
{
|
||||
id: 'dispatch',
|
||||
title: <FormattedMessage id="dispatch" />,
|
||||
@@ -70,6 +72,13 @@ const nearle = {
|
||||
url: '/nearle/dispatch',
|
||||
icon: icons.DirectionsBikeOutlinedIcon
|
||||
},
|
||||
{
|
||||
id: 'hubs',
|
||||
title: <FormattedMessage id="hubs" />,
|
||||
type: 'item',
|
||||
url: '/nearle/hubs',
|
||||
icon: icons.DeploymentUnitOutlined
|
||||
},
|
||||
{
|
||||
id: 'orders',
|
||||
title: <FormattedMessage id="orders" />,
|
||||
@@ -112,59 +121,6 @@ const nearle = {
|
||||
type: 'item',
|
||||
url: '/nearle/riders',
|
||||
icon: DirectionsBikeOutlinedIcon
|
||||
},
|
||||
{
|
||||
id: 'reports',
|
||||
title: <FormattedMessage id="reports" />,
|
||||
type: 'collapse',
|
||||
icon: icons.BarChartOutlined,
|
||||
children: [
|
||||
{
|
||||
id: 'reports',
|
||||
title: <FormattedMessage id="ordersummary" />,
|
||||
type: 'item',
|
||||
url: '/nearle/reports/orderssummary',
|
||||
icon: TbListDetails
|
||||
},
|
||||
{
|
||||
id: 'ordersdetails',
|
||||
title: <FormattedMessage id="ordersdetails" />,
|
||||
type: 'item',
|
||||
url: '/nearle/reports/ordersdetails',
|
||||
icon: icons.DashboardOutlined
|
||||
// target: true
|
||||
},
|
||||
{
|
||||
id: 'riderssummary',
|
||||
title: <FormattedMessage id="riderssummary" />,
|
||||
type: 'item',
|
||||
url: '/nearle/reports/riderssummary',
|
||||
icon: DirectionsBikeOutlinedIcon
|
||||
// target: true
|
||||
},
|
||||
{
|
||||
id: 'riderslogs',
|
||||
title: <FormattedMessage id="riderslogs" />,
|
||||
type: 'item',
|
||||
url: '/nearle/reports/riderslogs',
|
||||
icon: DirectionsBikeOutlinedIcon
|
||||
// target: true
|
||||
},
|
||||
{
|
||||
id: 'profitability',
|
||||
title: <FormattedMessage id="profitability" />,
|
||||
type: 'item',
|
||||
url: '/nearle/reports/profitability',
|
||||
icon: icons.BarChartOutlined
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'invoice',
|
||||
title: <FormattedMessage id="invoice" />,
|
||||
type: 'item',
|
||||
url: '/nearle/invoice',
|
||||
icon: icons.ReceiptOutlinedIcon
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -4,82 +4,111 @@ import dayjs from 'dayjs';
|
||||
const userid = localStorage.getItem('userid');
|
||||
|
||||
// ==============================|| getRiderPeriodicLogs ||============================== //
|
||||
// Returns the rider's latest periodic log entry — battery, GPS, status, current
|
||||
// order. Used by the Rider Info modal on the Dispatch page.
|
||||
// Returns the miler's latest known position/status. Doormile has no periodic-log
|
||||
// stream yet (that lands with EMQX) — this reads the live snapshot off /admin/milers/:id
|
||||
// instead. battery/speed have no Doormile equivalent yet, so they're always null.
|
||||
export const getRiderPeriodicLogs = async (userid) => {
|
||||
const url = `${process.env.REACT_APP_URL}/utils/getriderperiodiclogs${userid ? `?userid=${userid}` : ''}`;
|
||||
const response = await axios.get(url);
|
||||
if (response.data && response.data.status) return response.data.data;
|
||||
return null;
|
||||
if (!userid) return null;
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/milers/${userid}`);
|
||||
const m = response.data?.data;
|
||||
if (!m) return null;
|
||||
return {
|
||||
userid: m.userid,
|
||||
lat: m.currentlat,
|
||||
lon: m.currentlon,
|
||||
status: m.availabilitystatus,
|
||||
battery: null,
|
||||
speed: null
|
||||
};
|
||||
};
|
||||
|
||||
// ==============================|| fetchAppLocations||============================== //
|
||||
|
||||
export const fetchAppLocations = async () => {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
|
||||
const updatedLocations = [
|
||||
...response.data.details,
|
||||
{ locationname: 'All', applocationid: 0 } // Add your new object here
|
||||
];
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/hubs`);
|
||||
const hubs = (response.data?.data || []).map((h) => ({
|
||||
...h,
|
||||
applocationid: h.hubid,
|
||||
locationname: h.hubname
|
||||
}));
|
||||
return [...hubs, { locationname: 'All', applocationid: 0 }];
|
||||
};
|
||||
|
||||
return updatedLocations;
|
||||
// ==============================|| fetchHubs / createHub / updateHub (hubs) ||============================== //
|
||||
|
||||
export const fetchHubs = async () => {
|
||||
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/hubs`);
|
||||
return res.data?.data || [];
|
||||
};
|
||||
export const createHub = async (body) => {
|
||||
const res = await axios.post(`${process.env.REACT_APP_URL}/admin/hubs`, body);
|
||||
return res.data;
|
||||
};
|
||||
export const updateHub = async (id, body) => {
|
||||
const res = await axios.patch(`${process.env.REACT_APP_URL}/admin/hubs/${id}`, body);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
// ==============================|| fetchPercentageData (orders) ||============================== //
|
||||
|
||||
export const fetchPercentageData = async ({ queryKey }) => {
|
||||
const [, appId, startdate, enddate, tenantid, locationid] = queryKey;
|
||||
const response = await axios.get(
|
||||
`${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}`
|
||||
const statuses = ['Pending_Pickup', 'Miler_Assigned', 'Delivered', 'Cancelled'];
|
||||
const [pending, assigned, delivered, cancelled] = await Promise.all(
|
||||
statuses.map((status) => axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status } }))
|
||||
);
|
||||
const details = response.data.details;
|
||||
const created = pending.data.total + assigned.data.total + delivered.data.total + cancelled.data.total;
|
||||
|
||||
return {
|
||||
created: details.created.toString(),
|
||||
uncoveredOrders: details.pending.toString(),
|
||||
coveredOrders: details.delivered.toString(),
|
||||
cancelled: details.cancelled.toString(),
|
||||
percentage1: (Math.round((details.created / details.total) * 100) || 0).toString(),
|
||||
percentage2: (Math.round((details.pending / details.total) * 100) || 0).toString(),
|
||||
percentage3: (Math.round((details.delivered / details.total) * 100) || 0).toString(),
|
||||
percentage4: (Math.round((details.cancelled / details.total) * 100) || 0).toString()
|
||||
created: created.toString(),
|
||||
uncoveredOrders: pending.data.total.toString(),
|
||||
coveredOrders: delivered.data.total.toString(),
|
||||
cancelled: cancelled.data.total.toString(),
|
||||
percentage1: (Math.round((created / created) * 100) || 0).toString(),
|
||||
percentage2: (Math.round((pending.data.total / created) * 100) || 0).toString(),
|
||||
percentage3: (Math.round((delivered.data.total / created) * 100) || 0).toString(),
|
||||
percentage4: (Math.round((cancelled.data.total / created) * 100) || 0).toString()
|
||||
};
|
||||
};
|
||||
|
||||
// ===================================================== || getTenants || =====================================================
|
||||
|
||||
export const getTenants = async (appId) => {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenants/?applocationid=${appId}&status=active`);
|
||||
if (response.data.status) {
|
||||
let arr = [];
|
||||
response.data.details.map((val) => {
|
||||
arr.push({
|
||||
...val,
|
||||
label: `${val.tenantname}`
|
||||
});
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
// appId (hub/zone) has no equivalent filter on Doormile's CRM clients endpoint —
|
||||
// kept as a parameter only so existing call sites (deliveries.js, orders.js,
|
||||
// reports/*) don't need to change their queryFn wiring.
|
||||
export const getTenants = async () => {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/crm/clients`);
|
||||
return (response.data?.data || []).map((val) => ({
|
||||
...val,
|
||||
tenantid: val.clientid,
|
||||
tenantname: val.clientname,
|
||||
label: val.clientname
|
||||
}));
|
||||
};
|
||||
// ============================================= || gettenantlocations (branches) || =============================================
|
||||
export const gettenantlocations = async (appId) => {
|
||||
try {
|
||||
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${appId}`);
|
||||
return res.data.details;
|
||||
} catch (err) {
|
||||
console.log('gettenantlocations', err);
|
||||
}
|
||||
// No Doormile equivalent to tenant branches — call sites use this for a
|
||||
// branch/location dropdown that doesn't apply to Doormile's client model.
|
||||
export const gettenantlocations = async () => {
|
||||
return [];
|
||||
};
|
||||
// ==============================|| fetchorderscount (orders) ||============================== //
|
||||
|
||||
export const fetchorderscount = async ({ queryKey }) => {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [, appId, startdate, enddate, currentStatus, tenantid, locationid] = queryKey;
|
||||
|
||||
const url = `${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}&status=${currentStatus}`;
|
||||
const [all, pending, delivered, cancelled] = await Promise.all([
|
||||
axios.get(`${process.env.REACT_APP_URL}/admin/bookings`),
|
||||
axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: 'Pending_Pickup' } }),
|
||||
axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: 'Delivered' } }),
|
||||
axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: 'Cancelled' } })
|
||||
]);
|
||||
|
||||
const response = await axios.get(url);
|
||||
|
||||
return response.data.details;
|
||||
return {
|
||||
created: all.data.total,
|
||||
pending: pending.data.total,
|
||||
delivered: delivered.data.total,
|
||||
cancelled: cancelled.data.total
|
||||
};
|
||||
};
|
||||
|
||||
// ==============================|| fetchOrders (orders) ||============================== //
|
||||
@@ -95,46 +124,93 @@ export const fetchorderscount = async ({ queryKey }) => {
|
||||
// return response.data.details.map((val, i) => ({ ...val, sno: i + 1 }));
|
||||
// };
|
||||
export const fetchOrders = async ({ pageParam = 1, queryKey }) => {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [, appId, currentStatus, debouncedSearch, startdate, enddate, rowsPerPage, tenantid, locationid] = queryKey;
|
||||
|
||||
const url = `${process.env.REACT_APP_URL}/orders/tenant/getorders/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&keyword=${debouncedSearch}&pageno=${pageParam}&pagesize=${rowsPerPage}`;
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, {
|
||||
params: {
|
||||
status: currentStatus === 'All' ? undefined : currentStatus,
|
||||
keyword: debouncedSearch,
|
||||
pageno: pageParam,
|
||||
pagesize: rowsPerPage
|
||||
}
|
||||
});
|
||||
|
||||
const response = await axios.get(url);
|
||||
// TEMPORARY: backend fix in progress — pageno/pagesize are currently
|
||||
// ignored server-side and it returns every matching record on every call.
|
||||
// Slice client-side so infinite scroll doesn't dump the whole dataset on
|
||||
// page 1. Safe to remove once the backend honours pagination.
|
||||
const all = response.data.data || [];
|
||||
const size = Number(rowsPerPage);
|
||||
const start = (pageParam - 1) * size;
|
||||
const rows = all.slice(start, start + size);
|
||||
|
||||
return {
|
||||
rows: response.data.details,
|
||||
nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined
|
||||
rows,
|
||||
nextPage: start + size < all.length ? pageParam + 1 : undefined
|
||||
};
|
||||
};
|
||||
|
||||
// ==============================|| fetchPaymentType (orders) ||============================== //
|
||||
|
||||
export const fetchPaymentType = async () => {
|
||||
const { data } = await axios.get(`${process.env.REACT_APP_URL}/utils/getapptypes/?tag=paymentmode`);
|
||||
return data.details.map((val) => ({
|
||||
...val,
|
||||
label: val.typename
|
||||
}));
|
||||
};
|
||||
// No Doormile payment-types endpoint exists yet — static defaults.
|
||||
export const fetchPaymentType = async () => [
|
||||
{ apptypeid: 1, typename: 'Cash', label: 'Cash' },
|
||||
{ apptypeid: 2, typename: 'Online', label: 'Online' },
|
||||
{ apptypeid: 3, typename: 'COD', label: 'COD' }
|
||||
];
|
||||
|
||||
// ==============================|| fetchRidersList (orders) ||============================== //
|
||||
|
||||
export const fetchRidersList = async ({ queryKey }) => {
|
||||
try {
|
||||
const [, appId] = queryKey; // Extract appId from queryKey
|
||||
const { data } = await axios.get(`${process.env.REACT_APP_URL}/partners/getriders/?applocationid=${appId}`);
|
||||
console.log('data', data);
|
||||
const response = data?.details
|
||||
? data?.details.map((val) => ({
|
||||
...val,
|
||||
label: `${val.firstname} ${val.lastname} | ${val.contactno}`
|
||||
}))
|
||||
: [];
|
||||
return response;
|
||||
} catch (err) {
|
||||
OpenToast(err.message, 'error', 2000);
|
||||
throw err; // 🔥 REQUIRED
|
||||
}
|
||||
export const fetchRidersList = async () => {
|
||||
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
|
||||
return (res.data?.data || []).map((m) => ({
|
||||
...m,
|
||||
userid: m.userid,
|
||||
label: `${m.displayname} | ${m.phone}`,
|
||||
firstname: m.displayname,
|
||||
lastname: '',
|
||||
contactno: m.phone
|
||||
}));
|
||||
};
|
||||
|
||||
// ==============================|| Doormile assignment (new) ||============================== //
|
||||
// NOTE: createOptimisationDeliveries / reconcileSteps / fetchBatchEfficiency /
|
||||
// finalCreatedeliveries / createAutomationDeliveries below still call the legacy
|
||||
// NearlExpress optimiser (routes.workolik.com / routemate.workolik.com /
|
||||
// jupiter.nearle.app) and are wired into Dispatch.js, Preview.js, OrdersPreview.js,
|
||||
// orders.js and deliveries.js. Per the "don't touch Dispatch/Preview internals —
|
||||
// Phase 3" instruction, they're left as-is rather than renamed/repointed, since
|
||||
// doing so would silently break those pages' running mutations without a
|
||||
// corresponding page rewrite. The Doormile-native replacements are added here
|
||||
// instead, so Phase 3 can cut individual call sites over one at a time.
|
||||
|
||||
// assignBooking — triggers Doormile's AI assignment engine for one booking via
|
||||
// the internal-key-gated endpoint. SECURITY: X-Internal-Key is a static secret
|
||||
// baked into this client bundle — anyone can extract it from the shipped JS and
|
||||
// call this endpoint directly. Prefer routing this through the already-
|
||||
// authenticated admin JWT (see autoAssignBooking below) once the backend
|
||||
// supports it; keep this only if /internal/bookings/:id/reassign truly must
|
||||
// stay key-gated rather than JWT-gated.
|
||||
export const assignBooking = async (bookingId) => {
|
||||
const response = await axios.post(
|
||||
`${process.env.REACT_APP_URL}/internal/bookings/${bookingId}/reassign`,
|
||||
{},
|
||||
{ headers: { 'X-Internal-Key': process.env.REACT_APP_INTERNAL_KEY } }
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// autoAssignBooking — same intent as assignBooking but authenticated with the
|
||||
// operator's own admin JWT instead of a shared static key.
|
||||
export const autoAssignBooking = async (bookingId) => {
|
||||
const response = await axios.post(`${process.env.REACT_APP_URL}/hub/bookings/${bookingId}/auto-assign`, {});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// fetchDashboardStats — hub-level stats, replaces the workolik batch-efficiency call.
|
||||
export const fetchDashboardStats = async () => {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/hub/dashboard`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ==============================|| createOptimisationDeliveries (orders) Arrange the order ||============================== //
|
||||
@@ -145,14 +221,9 @@ export const createOptimisationDeliveries = async (deliveryData) => {
|
||||
return response.data;
|
||||
};
|
||||
// ==============================|| reconcileSteps (Preview - validate rider/order step assignments) ||============================== //
|
||||
|
||||
export const reconcileSteps = async ({ riders }) => {
|
||||
const response = await axios.post(
|
||||
`https://routes.workolik.com/api/v1/optimization/reconcile-steps`,
|
||||
{ riders }
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
// No Doormile equivalent — made a no-op so CLAUDE.md's "always reconcile before
|
||||
// createdeliveries" hard constraint keeps holding trivially until Phase 3.
|
||||
export const reconcileSteps = async (data) => data;
|
||||
|
||||
// ==============================|| fetchBatchEfficiency (Dispatch - Analysis view) ||============================== //
|
||||
// Calls POST /api/v1/batch/efficiency with a JSON body { batch, tenant_id }.
|
||||
@@ -223,34 +294,19 @@ export const createAutomationDeliveries = async (variables) => {
|
||||
};
|
||||
|
||||
// ==============================|| notifyRider (orders / deliveries) ||============================== //
|
||||
// Doormile sends miler FCM notifications automatically from the Go backend on
|
||||
// status changes, so there's no equivalent client-triggered endpoint. No-op.
|
||||
export const notifyRider = async () => ({ success: true });
|
||||
|
||||
export const notifyRider = async (riderToken) => {
|
||||
if (!riderToken) {
|
||||
throw new Error('Invalid rider token');
|
||||
}
|
||||
console.log('notify rider called');
|
||||
console.log('riderToken', riderToken);
|
||||
const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, {
|
||||
token: riderToken,
|
||||
notification: {
|
||||
title: 'NearleXpress',
|
||||
body: 'Orders have been placed for delivery. Kindly accept and process deliveries',
|
||||
sound: 'ring',
|
||||
image: ''
|
||||
}
|
||||
});
|
||||
// ==============================|| cancelOrder (orders) ||============================== //
|
||||
export const cancelOrder = async (bookingid) => {
|
||||
const response = await axios.post(`${process.env.REACT_APP_URL}/admin/bookings/${bookingid}/cancel`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ==============================|| cancelOrder (orders) ||============================== //
|
||||
|
||||
export const cancelOrder = async (orderheaderid) => {
|
||||
const response = await axios.put(`${process.env.REACT_APP_URL}/orders/updateorder`, {
|
||||
orderheaderid: orderheaderid,
|
||||
orderstatus: 'cancelled',
|
||||
cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
});
|
||||
|
||||
// ==============================|| updateBookingStatus (bookings) ||============================== //
|
||||
export const updateBookingStatus = async (bookingid, status) => {
|
||||
const response = await axios.put(`${process.env.REACT_APP_URL}/admin/bookings/${bookingid}/status`, { status });
|
||||
return response.data;
|
||||
};
|
||||
// ==============================|| cancelMultipleOrder (orders) ||============================== //
|
||||
@@ -270,97 +326,105 @@ export const cancelMultipleOrder = async (orderlist) => {
|
||||
};
|
||||
// ==============================|| fetchDeliveries (deliveries) ||============================== //
|
||||
|
||||
// NOTE: NearlExpress "deliveries" tabs are keyed off statuses
|
||||
// (pending/accepted/arrived/picked/active/delivered/cancelled/skipped) that
|
||||
// don't exist in Doormile's booking status vocabulary (Pending_Pickup,
|
||||
// Miler_Assigned, Pickup_Scheduled, At_Customer, Picked_Up, At_Hub, Delivered,
|
||||
// Cancelled, Assignment_Failed). This passes currentStatus straight through —
|
||||
// deliveries.js's own tab definitions still need updating to Doormile statuses
|
||||
// (flagged, not guessed).
|
||||
export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let [, appId, userid, currentStatus, startdate, enddate, rowsPerPage, searchword, tenantid, locationid, riderid] = queryKey;
|
||||
currentStatus = currentStatus == 'All' ? 'all' : currentStatus;
|
||||
const url =
|
||||
appId === 0
|
||||
? `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?appuserid=${userid}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`
|
||||
: `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?applocationid=${appId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
|
||||
const response = await axios.get(url);
|
||||
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, {
|
||||
params: {
|
||||
status: currentStatus === 'all' || currentStatus === 'All' ? undefined : currentStatus,
|
||||
keyword: searchword,
|
||||
pageno: pageParam,
|
||||
pagesize: rowsPerPage
|
||||
}
|
||||
});
|
||||
|
||||
// TEMPORARY: same backend pagination bug as fetchOrders — slice client-side
|
||||
// until pageno/pagesize are honoured server-side.
|
||||
const all = response.data.data || [];
|
||||
const size = Number(rowsPerPage);
|
||||
const start = (pageParam - 1) * size;
|
||||
const rows = all.slice(start, start + size);
|
||||
|
||||
return {
|
||||
rows: response.data.details,
|
||||
nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined
|
||||
rows,
|
||||
nextPage: start + size < all.length ? pageParam + 1 : undefined
|
||||
};
|
||||
};
|
||||
|
||||
// ==============================|| fetchPercentageAPI (deliveries) ||============================== //
|
||||
|
||||
export const fetchPercentageAPI = async (appId) => {
|
||||
const url = `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}`;
|
||||
const response = await axios.get(url);
|
||||
|
||||
const data = response.data.details;
|
||||
// UNVERIFIED: status param + pagesize=1-for-count-only pattern hasn't been
|
||||
// confirmed against the real backend the way bookings/clients/milers were —
|
||||
// implemented per explicit instruction, flag if it 404s/403s like
|
||||
// /admin/customers did.
|
||||
export const fetchPercentageAPI = async () => {
|
||||
const statuses = ['Pending_Pickup', 'Miler_Assigned', 'Picked_Up', 'Delivered', 'Cancelled'];
|
||||
const results = await Promise.all(
|
||||
statuses.map((s) => axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: s, pagesize: 1 } }))
|
||||
);
|
||||
const [pending, assigned, picked, delivered, cancelled] = results.map((r) => r.data?.total || 0);
|
||||
const total = pending + assigned + picked + delivered + cancelled;
|
||||
|
||||
return {
|
||||
coveredOrders: data.delivered.toString(),
|
||||
cancelledOrders: data.cancelled.toString(),
|
||||
uncoveredOrders: data.pending.toString(),
|
||||
assignedOrders: data.accepted.toString(),
|
||||
createdOrders: data.created.toString(),
|
||||
closedOrders: data.delivered.toString(),
|
||||
pickedOrders: data.picked.toString(),
|
||||
percentage1: (Math.round((data.pending / data.total) * 100) || 0).toString(),
|
||||
percentage2: (Math.round((data.accepted / data.total) * 100) || 0).toString(),
|
||||
percentage3: (Math.round((data.picked / data.total) * 100) || 0).toString(),
|
||||
percentage4: (Math.round((data.delivered / data.total) * 100) || 0).toString()
|
||||
coveredOrders: delivered.toString(),
|
||||
cancelledOrders: cancelled.toString(),
|
||||
uncoveredOrders: pending.toString(),
|
||||
assignedOrders: assigned.toString(),
|
||||
createdOrders: total.toString(),
|
||||
closedOrders: delivered.toString(),
|
||||
pickedOrders: picked.toString(),
|
||||
total: total.toString(),
|
||||
percentage1: (Math.round((pending / total) * 100) || 0).toString(),
|
||||
percentage2: (Math.round((assigned / total) * 100) || 0).toString(),
|
||||
percentage3: (Math.round((picked / total) * 100) || 0).toString(),
|
||||
percentage4: (Math.round((delivered / total) * 100) || 0).toString()
|
||||
};
|
||||
};
|
||||
|
||||
// ==============================|| fetchCountAPI (deliveries) ||============================== //
|
||||
|
||||
export const fetchCountAPI = async (appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid) => {
|
||||
const url =
|
||||
appId == 0
|
||||
? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?appuserid=${userid}&fromdate=${startdate}&todate=${enddate}`
|
||||
: `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
|
||||
const response = await axios.get(url);
|
||||
const data = response.data.details;
|
||||
export const fetchCountAPI = async () => {
|
||||
const statuses = ['Pending_Pickup', 'Miler_Assigned', 'Pickup_Scheduled', 'At_Customer', 'Picked_Up', 'At_Hub', 'Delivered', 'Cancelled'];
|
||||
const results = await Promise.all(
|
||||
statuses.map((s) => axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: s, pagesize: 1 } }))
|
||||
);
|
||||
const [pending, assigned, scheduled, atcustomer, picked, athub, delivered, cancelled] = results.map((r) => r.data?.total || 0);
|
||||
return {
|
||||
total: data.total,
|
||||
uncoveredLength: data.pending,
|
||||
assignedLength: data.accepted,
|
||||
arrivedLength: data.arrived,
|
||||
pickedLength: data.picked,
|
||||
activeLength: data.active,
|
||||
coveredLength: data.delivered,
|
||||
cancelLength: data.cancelled,
|
||||
skippedLength: data.skipped
|
||||
total: pending + assigned + scheduled + atcustomer + picked + athub + delivered + cancelled,
|
||||
uncoveredLength: pending,
|
||||
assignedLength: assigned,
|
||||
arrivedLength: scheduled,
|
||||
pickedLength: picked,
|
||||
activeLength: atcustomer + athub,
|
||||
coveredLength: delivered,
|
||||
cancelLength: cancelled,
|
||||
skippedLength: 0
|
||||
};
|
||||
};
|
||||
|
||||
// ==============================|| cancelDeliveryAPI (deliveries) ||============================== //
|
||||
|
||||
export const cancelDeliveryAPI = async (selectedRow, cancelFeed) => {
|
||||
const payload = {
|
||||
deliveryid: selectedRow.deliveryid,
|
||||
orderheaderid: selectedRow.orderheaderid,
|
||||
orderstatus: 'cancelled',
|
||||
canceltime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
feedback: cancelFeed
|
||||
};
|
||||
const response = await axios.put(`${process.env.REACT_APP_URL}/deliveries/updatedelivery`, payload);
|
||||
export const cancelDeliveryAPI = async (selectedRow) => {
|
||||
const response = await axios.post(`${process.env.REACT_APP_URL}/admin/bookings/${selectedRow.bookingid}/cancel`);
|
||||
return response.data;
|
||||
};
|
||||
// ==============================|| getorderdetails (deliveries) ||============================== //
|
||||
export const getorderdetails = async (orderHeaderid) => {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/orders/getorderdetails?orderheaderid=${orderHeaderid}`);
|
||||
export const getorderdetails = async (bookingid) => {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings/${bookingid}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// ==============================|| changeRiderAPI (deliveries) ||============================== //
|
||||
|
||||
export const changeRiderAPI = async (selectedRider, selectedRow) => {
|
||||
console.log('selectedRider', selectedRider);
|
||||
console.log('selectedRow', selectedRow);
|
||||
|
||||
return axios.put(`${process.env.REACT_APP_URL}/deliveries/updatedelivery`, {
|
||||
userid: selectedRider.userid,
|
||||
deliveryid: selectedRow.deliveryid,
|
||||
orderheaderid: selectedRow.orderheaderid,
|
||||
orderstatus: 'pending',
|
||||
assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
return axios.post(`${process.env.REACT_APP_URL}/hub/bookings/${selectedRow.bookingid}/assign-miler`, {
|
||||
mileruserid: selectedRider.userid
|
||||
});
|
||||
};
|
||||
// ==============================|| updateDeliveryAPI (deliveries) ||============================== //
|
||||
@@ -370,152 +434,158 @@ export const updateDeliveryAPI = async (orderData) => {
|
||||
};
|
||||
|
||||
// ==============================|| getalltenants (tenants) ||============================== //
|
||||
|
||||
// Doormile's /crm/clients has no documented status/keyword/page filter params
|
||||
// (unlike the old /tenants/getalltenants), so this fetches the full client
|
||||
// list and filters + paginates client-side.
|
||||
export const getalltenants = async ({ queryKey }) => {
|
||||
const [, appId, debouncedSearch, status, page, rowsPerPage] = queryKey;
|
||||
const [, , debouncedSearch, status, page, rowsPerPage] = queryKey;
|
||||
try {
|
||||
let url = `${process.env.REACT_APP_URL
|
||||
}/tenants/getalltenants/?status=${status}&applocationid=${appId}&keyword=${debouncedSearch}&pageno=${page + 1
|
||||
}&pagesize=${rowsPerPage}&moduleid=6`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.details; // return only data, keep it clean
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/crm/clients`);
|
||||
let clients = (response.data?.data || []).map((c) => ({
|
||||
...c,
|
||||
tenantid: c.clientid,
|
||||
tenantname: c.clientname
|
||||
}));
|
||||
|
||||
if (status) {
|
||||
clients = clients.filter((c) => (c.status || '').toLowerCase() === status.toLowerCase());
|
||||
}
|
||||
if (debouncedSearch) {
|
||||
const kw = debouncedSearch.toLowerCase();
|
||||
clients = clients.filter((c) => (c.clientname || '').toLowerCase().includes(kw) || (c.email || '').toLowerCase().includes(kw));
|
||||
}
|
||||
|
||||
const start = page * rowsPerPage;
|
||||
return clients.slice(start, start + rowsPerPage);
|
||||
} catch (err) {
|
||||
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
||||
OpenToast(message);
|
||||
return null; // return null for failure
|
||||
return null;
|
||||
}
|
||||
};
|
||||
// ==============================|| gettenantsummary (tenants) ||============================== //
|
||||
|
||||
export const gettenantsummary = async ({ queryKey }) => {
|
||||
const [, appId] = queryKey;
|
||||
export const gettenantsummary = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantsummary/?moduleid=6&applocationid=${appId}`);
|
||||
return response.data.summary; // return only data, keep it clean
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/crm/clients`);
|
||||
const clients = response.data?.data || [];
|
||||
const countOf = (s) => clients.filter((c) => (c.status || '').toLowerCase() === s).length;
|
||||
return { active: countOf('active'), pending: countOf('pending'), inactive: countOf('inactive') };
|
||||
} catch (err) {
|
||||
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
||||
OpenToast(message);
|
||||
return null; // return null for failure
|
||||
return null;
|
||||
}
|
||||
};
|
||||
// ==============================|| getpricinglist (tenants) ||============================== //
|
||||
|
||||
export const getpricinglist = async ({ queryKey }) => {
|
||||
const [, appId] = queryKey;
|
||||
export const getpricinglist = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/getpricinglist/?moduleid=6&applocationid=${appId}`);
|
||||
return response.data.summary; // return only data, keep it clean
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/pricing`);
|
||||
return response.data?.data || [];
|
||||
} catch (err) {
|
||||
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
||||
OpenToast(message);
|
||||
return null; // return null for failure
|
||||
OpenToast(err.message, 'error', 2000);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
// ==============================|| getallpricing (clientPricing) ||============================== //
|
||||
|
||||
export const getallpricing = async ({ queryKey }) => {
|
||||
const [, appId] = queryKey;
|
||||
try {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/utils/getallpricing/?applocationid=${appId}`);
|
||||
return response.data.details || [];
|
||||
} catch (err) {
|
||||
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
||||
OpenToast(message);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
export const getallpricing = getpricinglist;
|
||||
|
||||
// ==============================|| getcustomersummary (customers) ||============================== //
|
||||
|
||||
export const getcustomersummary = async ({ queryKey }) => {
|
||||
const [, appId] = queryKey;
|
||||
export const getcustomersummary = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/customers/getcustomersummary?applocationid=${appId}`);
|
||||
return response.data.summary;
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/customers`);
|
||||
const customers = response.data?.data || [];
|
||||
const total = response.data?.total || customers.length;
|
||||
return { Total: total };
|
||||
} catch (err) {
|
||||
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
||||
OpenToast(message);
|
||||
return null; // return null for failure
|
||||
OpenToast(err.message, 'error', 2000);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// ==============================|| getallcustomers (customers) ||============================== //
|
||||
|
||||
// Backend rows key the customer id as `appcustomerid` — aliased to `userid`
|
||||
// here so any call site still expecting the old field name keeps working.
|
||||
export const getallcustomers = async ({ pageParam = 1, queryKey }) => {
|
||||
const [, appId, debouncedSearch, rowsPerPage] = queryKey;
|
||||
const [, , debouncedSearch, rowsPerPage] = queryKey;
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/customers/getallcustomers/`, {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/customers`, {
|
||||
params: {
|
||||
applocationid: appId,
|
||||
keyword: debouncedSearch,
|
||||
keyword: debouncedSearch || undefined,
|
||||
pageno: pageParam,
|
||||
pagesize: rowsPerPage
|
||||
pagesize: rowsPerPage || 20
|
||||
}
|
||||
});
|
||||
const customers = (response.data?.data || []).map((c) => ({
|
||||
...c,
|
||||
userid: c.appcustomerid
|
||||
}));
|
||||
|
||||
return {
|
||||
data: response.data.details || [],
|
||||
nextPage: response.data.details?.length === rowsPerPage ? pageParam + 1 : undefined
|
||||
data: customers,
|
||||
nextPage: customers.length === Number(rowsPerPage) ? pageParam + 1 : undefined
|
||||
};
|
||||
} catch (err) {
|
||||
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
||||
|
||||
OpenToast(message);
|
||||
OpenToast(err.message, 'error', 2000);
|
||||
throw err; // IMPORTANT for React Query
|
||||
}
|
||||
};
|
||||
|
||||
// ==============================|| fetchAllRiders (riders) ||============================== //
|
||||
export const fetchAllRiders = async ({ pageParam = 1, queryKey }) => {
|
||||
// Doormile's /admin/milers has no documented pagination or search/status
|
||||
// filter params, unlike the old /partners/getallriders. Fetches the full miler
|
||||
// list and filters client-side; single page (nextPage always undefined) until
|
||||
// the backend adds server-side paging.
|
||||
export const fetchAllRiders = async ({ queryKey }) => {
|
||||
try {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [, appId, debouncedSearch, tabvalue] = queryKey;
|
||||
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
|
||||
let milers = res.data?.data || [];
|
||||
|
||||
const url = `${process.env.REACT_APP_URL
|
||||
}/partners/getallriders/?applocationid=${appId}&pageno=${pageParam}&pagesize=${20}&keyword=${debouncedSearch}&status=${(tabvalue == 0 || tabvalue == 2) ? '' : 'Active'
|
||||
}`;
|
||||
const res = await axios.get(url);
|
||||
return {
|
||||
details: res.data.details,
|
||||
nextPage: res.data.details.length === 20 ? pageParam + 1 : undefined
|
||||
};
|
||||
if (debouncedSearch) {
|
||||
const kw = debouncedSearch.toLowerCase();
|
||||
milers = milers.filter(
|
||||
(m) => (m.displayname || '').toLowerCase().includes(kw) || (m.phone || '').includes(debouncedSearch)
|
||||
);
|
||||
}
|
||||
if (tabvalue != 0 && tabvalue != 2) {
|
||||
milers = milers.filter((m) => m.availabilitystatus === 'Available');
|
||||
}
|
||||
|
||||
return { details: milers, nextPage: undefined };
|
||||
} catch (err) {
|
||||
console.log('fetchAllRiders err', err.message);
|
||||
return [];
|
||||
return { details: [], nextPage: undefined };
|
||||
}
|
||||
};
|
||||
// ==============================|| getallridersummary (riders) ||============================== //
|
||||
export const getallridersummary = async ({ queryKey }) => {
|
||||
export const getallridersummary = async () => {
|
||||
try {
|
||||
const [, appId, tabvalue] = queryKey;
|
||||
const response = await axios.get(
|
||||
`${process.env.REACT_APP_URL}/partners/getallridersummary/?applocationid=${appId}&status=${tabvalue == 0 ? '' : 'Active'}`
|
||||
);
|
||||
return response.data.details;
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
|
||||
const milers = response.data?.data || [];
|
||||
const active = milers.filter((m) => m.availabilitystatus === 'Available').length;
|
||||
return { total: milers.length, active, inactive: milers.length - active };
|
||||
} catch (err) {
|
||||
console.log('getallridersummary err', err.message);
|
||||
return [];
|
||||
return { total: 0, active: 0, inactive: 0 };
|
||||
}
|
||||
};
|
||||
|
||||
// ==============================|| fetchRiders (riders), active riders ||============================== //
|
||||
// Not currently imported anywhere (riders.js uses fetchAllRiders) — updated for
|
||||
// consistency in case a future page picks it up.
|
||||
export const fetchRiders = async (appId) => {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
|
||||
return response.data?.data || [];
|
||||
};
|
||||
|
||||
export const fetchRiders = async ({ pageParam = 1, queryKey }) => {
|
||||
try {
|
||||
const [, appId, debouncedSearch] = queryKey;
|
||||
|
||||
const url = `${process.env.REACT_APP_URL
|
||||
}/partners/getriders/?applocationid=${appId}&pageno=${pageParam}&pagesize=${20}&keyword=${debouncedSearch}`;
|
||||
|
||||
const res = await axios.get(url);
|
||||
return {
|
||||
details: res.data.details,
|
||||
nextPage: res.data.details.length === 20 ? pageParam + 1 : undefined
|
||||
};
|
||||
} catch (err) {
|
||||
console.log('fetchRiders err', err.message);
|
||||
return [];
|
||||
}
|
||||
// ==============================|| fetchMilerDetail (riders) ||============================== //
|
||||
export const fetchMilerDetail = async (milerid) => {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/milers/${milerid}`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
// ==============================|| getriderstatus (riders)||============================== //
|
||||
@@ -524,29 +594,6 @@ export const getriderstatus = async () => {
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
// ==============================|| getreportsummary (orders summary)||============================== //
|
||||
export const getreportsummary = async ({ queryKey }) => {
|
||||
console.log('queryKey for getreportsummary', queryKey);
|
||||
const [appId, tenantid, locationid, startdate, enddate] = queryKey;
|
||||
const response = await axios.get(
|
||||
`${process.env.REACT_APP_URL}/deliveries/getreportsummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}`
|
||||
);
|
||||
console.log('getreportsummary', response.data.details);
|
||||
|
||||
return response.data.details;
|
||||
};
|
||||
|
||||
// ==============================|| getreportlocationsummary (orders summary)||============================== //
|
||||
export const getreportlocationsummary = async ({ queryKey }) => {
|
||||
console.log('queryKey for getreportlocationsummary', queryKey);
|
||||
const [appId, tenantid, locationid, startdate, enddate] = queryKey;
|
||||
const response = await axios.get(
|
||||
`${process.env.REACT_APP_URL}/deliveries/getreportlocationsummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}`
|
||||
);
|
||||
console.log('getreportlocationsummary', response.data.details);
|
||||
|
||||
return response.data.details;
|
||||
};
|
||||
// ==============================|| fetchorderdetails (orders detail)||============================== //
|
||||
export const fetchorderdetails = async ({ queryKey }) => {
|
||||
console.log('queryKey of fetchorderdetails', queryKey);
|
||||
@@ -567,119 +614,27 @@ export const fetchorderdetails = async ({ queryKey }) => {
|
||||
return detailsWithSNo;
|
||||
};
|
||||
|
||||
// ==============================|| getriderbydelivery (orders detail)||============================== //
|
||||
|
||||
export const getriderbydelivery = async (startdate, enddate, appId = 0, tenantid = 0, locationid = 0) => {
|
||||
// const [, startdate, enddate] = queryKey;
|
||||
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${process.env.REACT_APP_URL}/deliveries/getriderbydelivery/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}`
|
||||
);
|
||||
return response.data.details || [];
|
||||
} catch (err) {
|
||||
console.log('getriderbydelivery', err.message);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// ==============================|| fetchCount (orders detail)||============================== //
|
||||
|
||||
export const fetchCount = async ({ queryKey }) => {
|
||||
console.log('queryKey of fetchCount', queryKey);
|
||||
const [appId, startdate, enddate] = queryKey;
|
||||
let url =
|
||||
appId == 0
|
||||
? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?fromdate=${startdate}&todate=${enddate}`
|
||||
: `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}`;
|
||||
|
||||
const response = await axios.get(url);
|
||||
return response.data.details;
|
||||
};
|
||||
|
||||
// ==============================|| fetchRidersSummary (riders summary)||============================== //
|
||||
|
||||
export const fetchRidersSummary = async ({ queryKey }) => {
|
||||
console.log('queryKey for fetchRidersSummary', queryKey);
|
||||
const [, appId, startdate, enddate] = queryKey;
|
||||
const response = await axios.get(
|
||||
`${process.env.REACT_APP_URL}/deliveries/getridersummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}`
|
||||
);
|
||||
console.log('fetchRidersSummary', response.data.details);
|
||||
|
||||
return response.data.details;
|
||||
};
|
||||
|
||||
// ==============================|| fetchLocations (orders summary))||============================== //
|
||||
// Not needed as a separate concept in Doormile — returns the hub list instead.
|
||||
export const fetchLocations = async () => {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getpartners`);
|
||||
const updatedLocations = [
|
||||
...response.data.details,
|
||||
{ partnername: 'All', partnerid: -1 } // Add your new object here
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/hubs`);
|
||||
return [
|
||||
...(response.data?.data || []).map((h) => ({ ...h, partnername: h.hubname, partnerid: h.hubid })),
|
||||
{ partnername: 'All', partnerid: -1 }
|
||||
];
|
||||
console.log('fetchLocations', updatedLocations);
|
||||
return updatedLocations;
|
||||
};
|
||||
|
||||
// ==============================|| fetchinvoiceinsight (Invoice)||============================== //
|
||||
|
||||
export const fetchinvoiceinsight = async () => {
|
||||
const insightResponse = await axios.get(`${process.env.REACT_APP_URL}/invoice/getinvoiceinsight`);
|
||||
return insightResponse.data.details;
|
||||
};
|
||||
|
||||
// ==============================|| fetchdeliverylist (Invoice)||============================== //
|
||||
|
||||
export const fetchdeliverylist = async ({ queryKey }) => {
|
||||
const [billStatus] = queryKey;
|
||||
const deliveyResponse = await axios.get(`${process.env.REACT_APP_URL}/invoice/getallinvoice/?billstatus=${billStatus}`);
|
||||
console.log('fetchdeliverylist', deliveyResponse.data.details);
|
||||
return deliveyResponse.data.details;
|
||||
};
|
||||
// ==============================|| fetchRidersLogs (RiderLogs)||============================== //
|
||||
// No Doormile equivalent yet (real-time miler location stream lands with EMQX).
|
||||
export const fetchRidersLogs = async () => [];
|
||||
|
||||
export const fetchRidersLogs = async ({ queryKey }) => {
|
||||
const [appId, startdate, riderSearch = ''] = queryKey;
|
||||
const riderLogsResponse = await axios.get(
|
||||
`${process.env.REACT_APP_URL2}/partners/getriderlogs/?applocationid=${appId}&fromdate=${startdate || ''}&todate=${startdate}&keyword=${riderSearch || ''
|
||||
} `
|
||||
);
|
||||
console.log('fetchRidersLogs', riderLogsResponse.data.details);
|
||||
return riderLogsResponse.data.details;
|
||||
};
|
||||
|
||||
// ==============================|| getorders (Locations)||============================== //
|
||||
// fetchOrders.js
|
||||
|
||||
export const fetchOrders1 = async ({ pageParam = 1, queryKey }) => {
|
||||
const [, tenantid, locationid, status, startdate, enddate, searchword, rowsPerPage] = queryKey;
|
||||
|
||||
const res = await axios.get(
|
||||
`${process.env.REACT_APP_URL}/orders/tenant/getorders/?tenantid=${tenantid}&locationid=${locationid}&status=${status}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}`
|
||||
);
|
||||
|
||||
return {
|
||||
details: res.data.details,
|
||||
nextPage: res.data.details.length === rowsPerPage ? pageParam + 1 : undefined
|
||||
};
|
||||
};
|
||||
// ==============================|| getusers (viewProfile)||============================== //
|
||||
|
||||
export const getusers = async () => {
|
||||
try {
|
||||
const res = await axios.get(`${process.env.REACT_APP_URL}/users/getusers/?configid=9&userid=${userid}`);
|
||||
return res.data.details;
|
||||
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/users`);
|
||||
return res.data?.data || [];
|
||||
} catch (err) {
|
||||
console.log('getusers', err.message);
|
||||
}
|
||||
};
|
||||
// ==============================|| getallriders (order)||============================== //
|
||||
|
||||
export const getallriders = async () => {
|
||||
try {
|
||||
const res = await axios.get(`${process.env.REACT_APP_URL}/partners/getallriders?partnerid=64`);
|
||||
return res.data.details;
|
||||
} catch (err) {
|
||||
console.log('getallriders', err.message);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Box, Button, Grid, Divider, Typography, useMediaQuery } from '@mui/material';
|
||||
|
||||
// project import
|
||||
import useAuth from 'hooks/useAuth';
|
||||
import AnimateButton from 'components/@extended/AnimateButton';
|
||||
import AuthWrapper from 'sections/auth/AuthWrapper';
|
||||
|
||||
// ================================|| CHECK MAIL ||================================ //
|
||||
|
||||
const CheckMail = () => {
|
||||
const theme = useTheme();
|
||||
const matchDownSM = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
const { isLoggedIn } = useAuth();
|
||||
|
||||
return (
|
||||
<AuthWrapper>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Box sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
|
||||
<Typography variant="h3">Hi, Check Your Mail</Typography>
|
||||
<Typography color="secondary" sx={{ mb: 0.5, mt: 1.25 }}>
|
||||
We have sent a password recover instructions to your email.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<AnimateButton>
|
||||
<Button
|
||||
component={Link}
|
||||
to={isLoggedIn ? '/auth/login' : '/login'}
|
||||
disableElevation
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</AnimateButton>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Divider>
|
||||
<Typography variant={matchDownSM ? 'subtitle1' : 'h5'}>Sign up with</Typography>
|
||||
</Divider>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AuthWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckMail;
|
||||
@@ -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,41 +0,0 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// material-ui
|
||||
import { Grid, Stack, Typography } from '@mui/material';
|
||||
|
||||
// project import
|
||||
import useAuth from 'hooks/useAuth';
|
||||
import AuthWrapper from 'sections/auth/AuthWrapper';
|
||||
import AuthForgotPassword from 'sections/auth/auth-forms/AuthForgotPassword';
|
||||
|
||||
// ================================|| FORGOT PASSWORD ||================================ //
|
||||
|
||||
const ForgotPassword = () => {
|
||||
const { isLoggedIn } = useAuth();
|
||||
|
||||
return (
|
||||
<AuthWrapper>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
|
||||
<Typography variant="h3">Forgot Password</Typography>
|
||||
<Typography
|
||||
component={Link}
|
||||
to={isLoggedIn ? '/auth/login' : '/login'}
|
||||
variant="body1"
|
||||
sx={{ textDecoration: 'none' }}
|
||||
color="primary"
|
||||
>
|
||||
Back to Login
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<AuthForgotPassword />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AuthWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// material-ui
|
||||
import { Grid, Stack, Typography } from '@mui/material';
|
||||
|
||||
// project import
|
||||
import useAuth from 'hooks/useAuth';
|
||||
import AuthWrapper from 'sections/auth/AuthWrapper';
|
||||
import AuthLogin from 'sections/auth/auth-forms/AuthLogin';
|
||||
|
||||
// ================================|| LOGIN ||================================ //
|
||||
|
||||
const Login = () => {
|
||||
const { isLoggedIn } = useAuth();
|
||||
|
||||
return (
|
||||
<AuthWrapper>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
|
||||
<Typography variant="h3">Login</Typography>
|
||||
<Typography
|
||||
component={Link}
|
||||
to={isLoggedIn ? '/auth/register' : '/register'}
|
||||
variant="body1"
|
||||
sx={{ textDecoration: 'none' }}
|
||||
color="primary"
|
||||
>
|
||||
Don't have an account?
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<AuthLogin />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AuthWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// material-ui
|
||||
import { Grid, Stack, Typography } from '@mui/material';
|
||||
|
||||
// project import
|
||||
import useAuth from 'hooks/useAuth';
|
||||
import AuthWrapper from 'sections/auth/AuthWrapper';
|
||||
import FirebaseRegister from 'sections/auth/auth-forms/AuthRegister';
|
||||
|
||||
// ================================|| REGISTER ||================================ //
|
||||
|
||||
const Register = () => {
|
||||
const { isLoggedIn } = useAuth();
|
||||
|
||||
return (
|
||||
<AuthWrapper>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
|
||||
<Typography variant="h3">Sign up</Typography>
|
||||
<Typography
|
||||
component={Link}
|
||||
to={isLoggedIn ? '/auth/login' : '/login'}
|
||||
variant="body1"
|
||||
sx={{ textDecoration: 'none' }}
|
||||
color="primary"
|
||||
>
|
||||
Already have an account?
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FirebaseRegister />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AuthWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
@@ -1,26 +0,0 @@
|
||||
// material-ui
|
||||
import { Grid, Stack, Typography } from '@mui/material';
|
||||
|
||||
// project import
|
||||
import AuthWrapper from 'sections/auth/AuthWrapper';
|
||||
import AuthResetPassword from 'sections/auth/auth-forms/AuthResetPassword';
|
||||
|
||||
// ================================|| RESET PASSWORD ||================================ //
|
||||
|
||||
const ResetPassword = () => (
|
||||
<AuthWrapper>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Stack sx={{ mb: { xs: -0.5, sm: 0.5 } }} spacing={1}>
|
||||
<Typography variant="h3">Reset Password</Typography>
|
||||
<Typography color="secondary">Please choose your new password</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<AuthResetPassword />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AuthWrapper>
|
||||
);
|
||||
|
||||
export default ResetPassword;
|
||||
@@ -1,24 +0,0 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
|
||||
const CtrlK = () => {
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (event) => {
|
||||
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
textFieldRef.current.focus();
|
||||
}
|
||||
|
||||
if (event.key === 'Escape' && document.activeElement === textFieldRef.current) {
|
||||
textFieldRef.current.blur();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
}, [textFieldRef]);
|
||||
};
|
||||
|
||||
export default CtrlK;
|
||||
@@ -1,20 +0,0 @@
|
||||
// material-ui
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
// project import
|
||||
import MainCard from 'components/MainCard';
|
||||
|
||||
// ==============================|| SAMPLE PAGE ||============================== //
|
||||
|
||||
const SamplePage = () => (
|
||||
<MainCard title="Sample Card">
|
||||
<Typography variant="body2">
|
||||
Lorem ipsum dolor sit amen, consenter nipissing eli, sed do elusion tempos incident ut laborers et doolie magna alissa. Ut enif ad
|
||||
minim venice, quin nostrum exercitation illampu laborings nisi ut liquid ex ea commons construal. Duos aube grue dolor in reprehended
|
||||
in voltage veil esse colum doolie eu fujian bulla parian. Exceptive sin ocean cuspidate non president, sunk in culpa qui officiate
|
||||
descent molls anim id est labours.
|
||||
</Typography>
|
||||
</MainCard>
|
||||
);
|
||||
|
||||
export default SamplePage;
|
||||
@@ -5,7 +5,7 @@ import { useTheme } from '@mui/material/styles';
|
||||
import { useMediaQuery, Box, Button, Grid, Stack, TextField, Typography } from '@mui/material';
|
||||
|
||||
// third party
|
||||
import { useTimer } from 'react-timer-hook';
|
||||
const { useTimer } = require('react-timer-hook');
|
||||
|
||||
// assets
|
||||
import coming from 'assets/images/maintenance/coming-soon.png';
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Grid, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import CircularLoader from 'components/CircularLoader';
|
||||
import Loader from 'components/Loader';
|
||||
import MainCard from 'components/MainCard';
|
||||
import { getusers } from 'pages/api/api';
|
||||
|
||||
const ViewProfile = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const {
|
||||
data: userdata,
|
||||
isLoading,
|
||||
isError,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['getuser'],
|
||||
queryFn: getusers
|
||||
});
|
||||
return (
|
||||
<>
|
||||
{isLoading && (
|
||||
<>
|
||||
<Loader />
|
||||
<CircularLoader />
|
||||
</>
|
||||
)}
|
||||
<MainCard sx={{ p: { xs: 1.5, md: 3 } }}>
|
||||
<Grid container spacing={isMobile ? 2 : 3}>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
{userdata?.firstname}
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
{' '}
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
{' '}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewProfile;
|
||||
308
src/pages/nearle/bookings/BookingDetail.js
Normal file
@@ -0,0 +1,308 @@
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Avatar, Box, Button, Chip, Grid, Paper, Stack, Step, StepLabel, Stepper, Typography, useMediaQuery } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
MdArrowBack,
|
||||
MdLocalShipping,
|
||||
MdPerson,
|
||||
MdPhone,
|
||||
MdLocationOn,
|
||||
MdTwoWheeler,
|
||||
MdStar,
|
||||
MdOutlineSmartToy,
|
||||
MdOutlineAssignmentInd,
|
||||
MdOutlineCancel
|
||||
} from 'react-icons/md';
|
||||
import Loader from 'components/Loader';
|
||||
import { OpenToast } from 'components/third-party/OpenToast';
|
||||
import { getorderdetails, autoAssignBooking, cancelOrder } from 'pages/api/api';
|
||||
|
||||
const DT = {
|
||||
radiusCard: 16,
|
||||
textPrimary: '#0f172a',
|
||||
textSecondary: '#64748b',
|
||||
textMuted: '#94a3b8',
|
||||
borderSubtle: '#e2e8f0',
|
||||
divider: '#f1f5f9',
|
||||
surface: '#ffffff',
|
||||
surfaceAlt: '#f8fafc'
|
||||
};
|
||||
const a = (c, suffix) => `${c}${suffix}`;
|
||||
const tint = (c) => a(c, '08');
|
||||
const soft = (c) => a(c, '18');
|
||||
const edge = (c) => a(c, '55');
|
||||
const BRAND = '#C01227';
|
||||
|
||||
// Ordered status progression used for the stepper. Doormile's real status
|
||||
// vocabulary — Assignment_Failed and Cancelled are terminal/off-path states
|
||||
// shown separately rather than as stepper nodes.
|
||||
const STATUS_STEPS = ['Pending_Pickup', 'Miler_Assigned', 'Pickup_Scheduled', 'At_Customer', 'Picked_Up', 'At_Hub', 'Delivered'];
|
||||
const STEP_LABELS = {
|
||||
Pending_Pickup: 'Pending Pickup',
|
||||
Miler_Assigned: 'Miler Assigned',
|
||||
Pickup_Scheduled: 'Pickup Scheduled',
|
||||
At_Customer: 'At Customer',
|
||||
Picked_Up: 'Picked Up',
|
||||
At_Hub: 'At Hub',
|
||||
Delivered: 'Delivered'
|
||||
};
|
||||
|
||||
const InfoRow = ({ icon: Icon, label, value, color = BRAND }) => (
|
||||
<Stack direction="row" alignItems="flex-start" spacing={1.5}>
|
||||
<Avatar sx={{ width: 32, height: 32, bgcolor: soft(color), color }}>
|
||||
<Icon size={16} />
|
||||
</Avatar>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.4 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography sx={{ fontWeight: 600, color: DT.textPrimary, wordBreak: 'break-word' }}>{value || '—'}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const BookingDetail = () => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['bookingDetail', id],
|
||||
queryFn: () => getorderdetails(id),
|
||||
enabled: Boolean(id)
|
||||
});
|
||||
|
||||
// Confirmed shape (booking id 5): bookingid, bookingreference, status,
|
||||
// pickupaddress, deliveryaddress, assignedmileruserid, createdat,
|
||||
// bookingparcels (nested parcels), serviceoptions, payments.
|
||||
const booking = data?.data || data || {};
|
||||
|
||||
const reassignMutation = useMutation({
|
||||
mutationFn: () => autoAssignBooking(id),
|
||||
onSuccess: () => {
|
||||
OpenToast('Reassignment triggered', 'success', 2000);
|
||||
queryClient.invalidateQueries({ queryKey: ['bookingDetail', id] });
|
||||
},
|
||||
onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
|
||||
});
|
||||
|
||||
const cancelMutation = useMutation({
|
||||
mutationFn: () => cancelOrder(id),
|
||||
onSuccess: () => {
|
||||
OpenToast('Booking cancelled', 'success', 2000);
|
||||
queryClient.invalidateQueries({ queryKey: ['bookingDetail', id] });
|
||||
},
|
||||
onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
|
||||
});
|
||||
|
||||
if (isLoading) return <Loader />;
|
||||
|
||||
const status = booking.status || 'Pending_Pickup';
|
||||
const isTerminal = ['cancelled', 'assignment_failed'].includes(String(status).toLowerCase());
|
||||
const activeStepIdx = STATUS_STEPS.indexOf(status);
|
||||
const parcels = booking.bookingparcels || booking.parcels || [];
|
||||
const miler = booking.miler || booking.assignedmiler || null;
|
||||
const agentDecision = booking.agent_decision_id ? booking.agentdecision || booking.agent_decision : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" spacing={1.5} sx={{ mb: 2 }}>
|
||||
<Button startIcon={<MdArrowBack size={16} />} onClick={() => navigate('/nearle/orders')} sx={{ color: DT.textSecondary, textTransform: 'none', fontWeight: 700 }}>
|
||||
Back to Bookings
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: { xs: 2, md: 3 },
|
||||
borderRadius: `${DT.radiusCard}px`,
|
||||
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint('#D35968')} 100%)`,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
mb: 2
|
||||
}}
|
||||
>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" alignItems={{ xs: 'flex-start', sm: 'center' }} spacing={2}>
|
||||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ width: 48, height: 48, bgcolor: BRAND, color: '#fff' }}>
|
||||
<MdLocalShipping size={24} />
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h3">{booking.bookingreference || `Booking #${id}`}</Typography>
|
||||
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
|
||||
{booking.createdat ? new Date(booking.createdat).toLocaleString() : '—'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Chip
|
||||
label={STEP_LABELS[status] || status}
|
||||
sx={{ fontWeight: 800, bgcolor: isTerminal ? soft('#ef4444') : soft(BRAND), color: isTerminal ? '#ef4444' : BRAND, border: `1px solid ${edge(isTerminal ? '#ef4444' : BRAND)}` }}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{isError && (
|
||||
<Paper elevation={0} sx={{ p: 2, mb: 2, borderRadius: 2, border: `1px solid ${edge('#ef4444')}`, bgcolor: tint('#ef4444') }}>
|
||||
<Typography sx={{ color: '#ef4444', fontWeight: 600 }}>Could not load full booking details — showing whatever came back.</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Status timeline */}
|
||||
{!isTerminal && (
|
||||
<Paper elevation={0} sx={{ p: { xs: 2, md: 3 }, mb: 2, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff' }}>
|
||||
<Stepper activeStep={activeStepIdx} alternativeLabel={!isMobile} orientation={isMobile ? 'vertical' : 'horizontal'}>
|
||||
{STATUS_STEPS.map((s) => (
|
||||
<Step key={s}>
|
||||
<StepLabel
|
||||
sx={{
|
||||
'& .MuiStepIcon-root.Mui-active': { color: BRAND },
|
||||
'& .MuiStepIcon-root.Mui-completed': { color: BRAND }
|
||||
}}
|
||||
>
|
||||
{STEP_LABELS[s]}
|
||||
</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
|
||||
Customer & Pickup
|
||||
</Typography>
|
||||
<Stack spacing={2}>
|
||||
<InfoRow icon={MdPerson} label="Customer" value={booking.customername || booking.customer?.name} />
|
||||
<InfoRow icon={MdPhone} label="Phone" value={booking.customerphone || booking.customer?.phone} />
|
||||
<InfoRow icon={MdLocationOn} label="Pickup Address" value={booking.pickupaddress} color="#0ea5e9" />
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
|
||||
Delivery
|
||||
</Typography>
|
||||
<Stack spacing={2}>
|
||||
<InfoRow icon={MdLocationOn} label="Delivery Address" value={booking.deliveryaddress} color="#10b981" />
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
|
||||
Assigned Miler
|
||||
</Typography>
|
||||
{booking.assignedmileruserid || miler ? (
|
||||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ width: 44, height: 44, bgcolor: soft('#8b5cf6'), color: '#8b5cf6' }}>
|
||||
<MdTwoWheeler size={22} />
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||||
{miler?.displayname || `Miler #${booking.assignedmileruserid}`}
|
||||
</Typography>
|
||||
<Stack direction="row" alignItems="center" spacing={1.5} sx={{ mt: 0.25 }}>
|
||||
{miler?.phone && (
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{miler.phone}
|
||||
</Typography>
|
||||
)}
|
||||
{miler?.rating != null && (
|
||||
<Stack direction="row" alignItems="center" spacing={0.25}>
|
||||
<MdStar size={13} style={{ color: '#f59e0b' }} />
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{miler.rating}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: DT.textMuted }}>
|
||||
No miler assigned yet.
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
|
||||
Parcel Details
|
||||
</Typography>
|
||||
{parcels.length === 0 ? (
|
||||
<Typography variant="body2" sx={{ color: DT.textMuted }}>
|
||||
No parcel details available.
|
||||
</Typography>
|
||||
) : (
|
||||
<Stack spacing={1}>
|
||||
{parcels.map((p, i) => (
|
||||
<Stack key={i} direction="row" justifyContent="space-between" sx={{ p: 1, borderRadius: 1.5, bgcolor: DT.surfaceAlt }}>
|
||||
<Typography variant="body2">{p.description || p.name || `Parcel ${i + 1}`}</Typography>
|
||||
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
|
||||
{p.weight ? `${p.weight}kg` : ''}
|
||||
</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{agentDecision && (
|
||||
<Grid item xs={12}>
|
||||
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: `1px solid ${edge('#6366f1')}`, background: tint('#6366f1') }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
|
||||
<Avatar sx={{ width: 32, height: 32, bgcolor: soft('#6366f1'), color: '#6366f1' }}>
|
||||
<MdOutlineSmartToy size={16} />
|
||||
</Avatar>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800, color: '#6366f1' }}>
|
||||
AI Assignment Reasoning
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography variant="body2" sx={{ color: DT.textPrimary }}>
|
||||
{agentDecision.reasoning || agentDecision.reason || JSON.stringify(agentDecision)}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<Stack direction="row" spacing={1.5} justifyContent="flex-end" sx={{ mt: 2.5 }}>
|
||||
{!isTerminal && (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<MdOutlineAssignmentInd size={16} />}
|
||||
disabled={reassignMutation.isLoading}
|
||||
onClick={() => reassignMutation.mutate()}
|
||||
sx={{ bgcolor: '#6366f1', '&:hover': { bgcolor: '#4f46e5' } }}
|
||||
>
|
||||
Reassign Miler
|
||||
</Button>
|
||||
)}
|
||||
{!isTerminal && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
startIcon={<MdOutlineCancel size={16} />}
|
||||
disabled={cancelMutation.isLoading}
|
||||
onClick={() => cancelMutation.mutate()}
|
||||
>
|
||||
Cancel Booking
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookingDetail;
|
||||
@@ -61,7 +61,7 @@ const DT = {
|
||||
divider: '#f1f5f9',
|
||||
surface: '#ffffff',
|
||||
surfaceAlt: '#f8fafc',
|
||||
brand: '#662582'
|
||||
brand: '#C01227'
|
||||
};
|
||||
const a = (c, suffix) => `${c}${suffix}`;
|
||||
const tint = (c) => a(c, '08');
|
||||
@@ -69,7 +69,7 @@ const soft = (c) => a(c, '18');
|
||||
const ring = (c) => a(c, '26');
|
||||
const edge = (c) => a(c, '55');
|
||||
|
||||
const BRAND = '#662582';
|
||||
const BRAND = '#C01227';
|
||||
|
||||
const SoftPaper = (props) => (
|
||||
<Paper
|
||||
|
||||
@@ -1,545 +1,10 @@
|
||||
import { React, useEffect, useState, useRef } from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Button, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, IconButton, Autocomplete, useMediaQuery } from '@mui/material';
|
||||
import MainCard from 'components/MainCard';
|
||||
import axios from 'axios';
|
||||
import Loader from 'components/Loader';
|
||||
import Geocode from 'react-geocode';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { useNavigate } from 'react-router';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
|
||||
import { OpenToast } from 'components/third-party/OpenToast';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const CreateCustomer = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [appId, setAppId] = useState(0);
|
||||
const locationRef = useRef(null);
|
||||
const [mobilenumber, setMobilenumber] = useState('');
|
||||
const [emailaddress, setEmailaddress] = useState('');
|
||||
const [address, setAddress] = useState('');
|
||||
const [firstname, setFirstname] = useState('');
|
||||
const [doorno, setDoorno] = useState('');
|
||||
const [landmark, setLandmark] = useState('');
|
||||
const [inputValue2, setInputValue2] = useState('');
|
||||
const [appLocaLat, setAppLocaLat] = useState();
|
||||
const [appLocaLng, setAppLocaLng] = useState();
|
||||
const [appLocaRadius, setAppLocaRadius] = useState();
|
||||
const [locaName, setLocoName] = useState('Select Location');
|
||||
const [tenantlist, setTenantlist] = useState([]);
|
||||
const [tid, setTid] = useState(0);
|
||||
const [pickCust, setPickCust] = useState({});
|
||||
const [startPoint, setStartPoint] = useState({ latitude: 0, longitude: 0 });
|
||||
const [loading, setLoading] = useState(false);
|
||||
export default function CreateCustomer() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize Google Maps Autocomplete
|
||||
if (inputValue2) {
|
||||
const autocompleteInput = document.getElementById('addressAuto1');
|
||||
const autocomplete = new window.google.maps.places.Autocomplete(autocompleteInput, {
|
||||
// types: ['(cities)'], // You can adjust the types parameter based on your requirements
|
||||
strictBounds: true,
|
||||
bounds: new window.google.maps.Circle({
|
||||
// center: new window.google.maps.LatLng(11.0050707, 76.9509083),
|
||||
// radius: 100000
|
||||
center: new window.google.maps.LatLng(appLocaLat, appLocaLng),
|
||||
|
||||
radius: appLocaRadius * 1000
|
||||
}).getBounds()
|
||||
});
|
||||
// Event listener for autocomplete place changed
|
||||
autocomplete.addListener('place_changed', () => {
|
||||
const place = autocomplete.getPlace();
|
||||
setInputValue2(`${place.name}, ${place.formatted_address}`);
|
||||
console.log('new place', place); // Do something with the selected place
|
||||
console.log(' pick (new place) lat lng', { lat: place.geometry.location.lat(), lng: place.geometry.location.lng() }); // Do something with the selected place
|
||||
// to trigger getDistance
|
||||
setStartPoint({ latitude: place.geometry.location.lat(), longitude: place.geometry.location.lng() });
|
||||
setAddress(`${place.name} ${place.formatted_address}`);
|
||||
setPickCust({ ...pickCust, address: `${place.name} ${place.formatted_address}` });
|
||||
const address = {
|
||||
address: `${place.name} ${place.formatted_address}`,
|
||||
street_number: '',
|
||||
route: '',
|
||||
locality: '',
|
||||
sublocality_level_1: '',
|
||||
administrative_area_level_3: '',
|
||||
administrative_area_level_1: '',
|
||||
country: '',
|
||||
postal_code: ''
|
||||
};
|
||||
place.address_components.forEach((component) => {
|
||||
component.types.forEach((type) => {
|
||||
switch (type) {
|
||||
case 'street_number':
|
||||
address.street_number = component.long_name;
|
||||
break;
|
||||
case 'route':
|
||||
address.route = component.long_name;
|
||||
break;
|
||||
case 'locality':
|
||||
address.locality = component.long_name;
|
||||
break;
|
||||
case 'sublocality_level_1':
|
||||
address.sublocality_level_1 = component.long_name;
|
||||
break;
|
||||
case 'administrative_area_level_3':
|
||||
address.administrative_area_level_3 = component.long_name;
|
||||
break;
|
||||
case 'administrative_area_level_1':
|
||||
address.administrative_area_level_1 = component.long_name;
|
||||
break;
|
||||
case 'country':
|
||||
address.country = component.long_name;
|
||||
break;
|
||||
case 'postal_code':
|
||||
address.postal_code = component.long_name;
|
||||
break;
|
||||
// Add more cases as needed for other types
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Use address object as per your requirements
|
||||
setPickCust({
|
||||
...pickCust,
|
||||
address: address.address,
|
||||
doorno: `${address.street_number} ${address.route}`,
|
||||
suburb: address.administrative_area_level_3,
|
||||
city: address.locality,
|
||||
state: address.administrative_area_level_1,
|
||||
postcode: address.postal_code,
|
||||
latitude: place.geometry.location.lat(),
|
||||
longitude: place.geometry.location.lng()
|
||||
});
|
||||
console.log('Pick Address:', address);
|
||||
});
|
||||
}
|
||||
}, [inputValue2]);
|
||||
// ==================================================== || getapplocations || ====================================================
|
||||
const getapplocations = async () => {
|
||||
setLoading(true);
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL}/utils/getapplocations/?applocationid=${appId}`)
|
||||
.then((res) => {
|
||||
console.log('getapplocations', res);
|
||||
const { latitude, longitude, radius } = res.data.details[0];
|
||||
if (res.data.status) {
|
||||
setAppLocaLat(latitude);
|
||||
setAppLocaLng(longitude);
|
||||
setAppLocaRadius(radius);
|
||||
console.log('radius', radius);
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (appId) {
|
||||
getapplocations();
|
||||
}
|
||||
}, [appId]);
|
||||
|
||||
// ===================================================== || fetchtenantinfolist || =====================================================
|
||||
|
||||
const fetchtenantinfolist = async (id) => {
|
||||
setLoading(true);
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL}/tenants/gettenants/?applocationid=${id}&status=active`)
|
||||
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
if (res.data.status) {
|
||||
let arr = [];
|
||||
res.data.details.map((val) => {
|
||||
arr.push({
|
||||
...val,
|
||||
label: `${val.tenantname}`
|
||||
});
|
||||
});
|
||||
setTenantlist(arr);
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
appId && fetchtenantinfolist(appId);
|
||||
}, [appId]);
|
||||
// ============================================= || gettenantlocations (branches) || =============================================
|
||||
const gettenantlocations = async (id) => {
|
||||
try {
|
||||
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`);
|
||||
console.log('gettenantlocations', res.data.details);
|
||||
if (res.data.details.length == 1) {
|
||||
} else {
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('gettenantlocations', err);
|
||||
}
|
||||
};
|
||||
|
||||
const opentoast = (message) => {
|
||||
enqueueSnackbar(message, {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
};
|
||||
|
||||
const createprofile = async () => {
|
||||
let obj = {
|
||||
applocationid: +appId,
|
||||
tenantid: +tid,
|
||||
customerid: 0,
|
||||
configid: 1,
|
||||
firstname: firstname,
|
||||
dialcode: '+91',
|
||||
contactno: mobilenumber,
|
||||
email: emailaddress,
|
||||
doorno: doorno,
|
||||
address: pickCust.address,
|
||||
suburb: pickCust.suburb,
|
||||
city: pickCust.city,
|
||||
state: pickCust.state,
|
||||
postcode: pickCust.postcode,
|
||||
landmark: landmark,
|
||||
latitude: startPoint.latitude.toString(),
|
||||
longitude: startPoint.longitude.toString(),
|
||||
profileimage: '',
|
||||
devicetype: '',
|
||||
deviceid: '',
|
||||
customertoken: '',
|
||||
primaryaddress: 1
|
||||
};
|
||||
console.log(obj);
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await axios
|
||||
.post(`${process.env.REACT_APP_URL}/customers/create`, obj)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
if (res.data.status) {
|
||||
enqueueSnackbar(' Created Successfully ', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
navigate('/nearle/customers');
|
||||
} else if (res.data.message == 'Customer Already available') {
|
||||
enqueueSnackbar('Customer Already available', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
setLoading(false);
|
||||
enqueueSnackbar(err.message, {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <Loader />}
|
||||
<Grid item xs={12} sx={{ mb: 2 }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h3">Create Customer</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<MainCard sx={{ p: { xs: 1.5, md: 3 } }}>
|
||||
<Grid container spacing={{ xs: 2, md: 3 }}>
|
||||
<Grid item xs={12}>
|
||||
<Grid container spacing={{ xs: 2, md: 3 }}>
|
||||
{/* ===================================================== || Choose location || ===================================================== */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<LocationAutocomplete ref={locationRef} locaName={locaName} setAppId={setAppId} setLocoName={setLocoName} sx={{}} />
|
||||
</Grid>
|
||||
{/* ===================================================== || Choose client || ===================================================== */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
disabled={appId == 0}
|
||||
id="free-solo-demo"
|
||||
sx={{}}
|
||||
options={tenantlist || []}
|
||||
renderInput={(params) => <TextField {...params} label="Choose Client" focused />}
|
||||
onChange={(e, val, reason) => {
|
||||
if (val) {
|
||||
console.log('Client', val);
|
||||
gettenantlocations(val.tenantid);
|
||||
setTid(val.tenantid);
|
||||
} else {
|
||||
setClientinfo({});
|
||||
setTenantid('');
|
||||
}
|
||||
if (reason == 'clear') {
|
||||
}
|
||||
}}
|
||||
/>{' '}
|
||||
</Grid>
|
||||
|
||||
{/* ===================================================== || Name|| ===================================================== */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-last-name">Name</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="personal-last-name"
|
||||
placeholder="Name"
|
||||
onChange={(e) => setFirstname(e.target.value)}
|
||||
value={firstname}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
{/* ===================================================== || Phone Number || ===================================================== */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-phone">Phone Number</InputLabel>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
|
||||
<Select defaultValue="+1" disabled sx={{ cursor: 'not-allowed' }}>
|
||||
<MenuItem value="+1">+91</MenuItem>
|
||||
</Select>
|
||||
<TextField
|
||||
type="number"
|
||||
id="personal-phone"
|
||||
fullWidth
|
||||
placeholder="Phone Number"
|
||||
onChange={(e) => {
|
||||
if (e.target.value.toString().length <= 10) {
|
||||
setMobilenumber(e.target.value);
|
||||
}
|
||||
}}
|
||||
value={mobilenumber}
|
||||
autoComplete="off"
|
||||
// disabled
|
||||
sx={{ cursor: 'not-allowed' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Grid>
|
||||
{/* ===================================================== || Email|| ===================================================== */}
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-email">Email </InputLabel>
|
||||
<TextField
|
||||
type="email"
|
||||
fullWidth
|
||||
id="personal-email"
|
||||
placeholder="Email "
|
||||
onChange={(e) => setEmailaddress(e.target.value)}
|
||||
value={emailaddress}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
{/* ===================================================== || door no || ===================================================== */}
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-location">Door No</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
// defaultValue="New York"
|
||||
id="personal-location"
|
||||
placeholder="Door No"
|
||||
onChange={(e) => setDoorno(e.target.value)}
|
||||
value={doorno}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
{/* ===================================================== || Address || ===================================================== */}
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-email"> Address</InputLabel>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
id="addressAuto1"
|
||||
fullWidth
|
||||
value={inputValue2}
|
||||
onChange={(e) => {
|
||||
if (appId) {
|
||||
appId && setInputValue2(e.target.value);
|
||||
} else {
|
||||
OpenToast('Select Location First', 'warning', 3000);
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setInputValue2('');
|
||||
setPickCust({
|
||||
...pickCust,
|
||||
doorno: '',
|
||||
suburb: '',
|
||||
city: '',
|
||||
postcode: '',
|
||||
landmark: ''
|
||||
});
|
||||
setStartPoint({ latitude: 0, longitude: 0 });
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-location">Location</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
// defaultValue="New York"
|
||||
id="personal-location"
|
||||
placeholder="Location"
|
||||
onChange={(e) => setPickCust({ ...pickCust, suburb: e.target.value })}
|
||||
value={pickCust.suburb}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-zipcode">City</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="personal-zipcode"
|
||||
placeholder="City"
|
||||
onChange={(e) => setPickCust({ ...pickCust, city: e.target.value })}
|
||||
value={pickCust.city}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-location">State</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
// defaultValue="New York"
|
||||
id="personal-location"
|
||||
placeholder="State"
|
||||
onChange={(e) => setPickCust({ ...pickCust, state: e.target.value })}
|
||||
value={pickCust.state}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-zipcode">Post Code</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
// defaultValue="956754"
|
||||
type="number"
|
||||
id="personal-zipcode"
|
||||
placeholder="Zipcode"
|
||||
onChange={(e) => setPickCust({ ...pickCust, postcode: e.target.value })}
|
||||
value={pickCust.postcode}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-email">Landmark</InputLabel>
|
||||
<TextField
|
||||
type="email"
|
||||
fullWidth
|
||||
// defaultValue="stebin.ben@gmail.com"
|
||||
id="personal-email"
|
||||
placeholder="Landmark"
|
||||
onChange={(e) => setLandmark(e.target.value)}
|
||||
value={landmark}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
justifyContent="flex-end"
|
||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
||||
spacing={2}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth={isMobile}
|
||||
onClick={() => {
|
||||
if (appId === '') {
|
||||
opentoast('Select Applocation ');
|
||||
} else if (tid === '') {
|
||||
opentoast('Select Tenant');
|
||||
} else if (firstname === '') {
|
||||
opentoast('Enter Name');
|
||||
} else if (mobilenumber === '') {
|
||||
opentoast('Enter Mobile Number ');
|
||||
} else if (address === '') {
|
||||
opentoast('Enter Address ');
|
||||
} else if (pickCust.city === '') {
|
||||
opentoast('Enter City ');
|
||||
} else if (pickCust.state === '') {
|
||||
opentoast('Enter State ');
|
||||
} else if (pickCust.suburb === '') {
|
||||
opentoast('Enter location ');
|
||||
} else if (pickCust.postcode === '') {
|
||||
opentoast('Enter Post Code ');
|
||||
} else if (landmark === '') {
|
||||
opentoast('Enter Land Mark ');
|
||||
} else if (pickCust.latitude === '') {
|
||||
opentoast('Invalid latitude ');
|
||||
} else if (pickCust.longitude === '') {
|
||||
opentoast('Invaiid Longitude ');
|
||||
} else {
|
||||
createprofile();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateCustomer;
|
||||
navigate('/nearle/customers');
|
||||
}, [navigate]);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,88 +1,26 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Box, Button, FormLabel, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
|
||||
|
||||
// third-party
|
||||
// import { PatternFormat } from 'react-number-format';
|
||||
import { Button, Grid, InputLabel, MenuItem, Select, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
|
||||
|
||||
// project import
|
||||
import MainCard from 'components/MainCard';
|
||||
import axios from 'axios';
|
||||
import { usePlacesWidget } from 'react-google-autocomplete';
|
||||
import Loader from 'components/Loader';
|
||||
import Geocode from 'react-geocode';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { useNavigate } from 'react-router';
|
||||
// import { setLocationType } from 'react-geocode';
|
||||
|
||||
// const avatarImage = require.context('assets/images/users', true);
|
||||
|
||||
// styles & constant
|
||||
// const ITEM_HEIGHT = 48;
|
||||
// const ITEM_PADDING_TOP = 8;
|
||||
// const MenuProps = {
|
||||
// PaperProps: {
|
||||
// style: {
|
||||
// maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
const Createclient = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
// const [role, setRole] = useState('');
|
||||
const [mobilenumber, setMobilenumber] = useState('');
|
||||
const [emailaddress, setEmailaddress] = useState('');
|
||||
const [city, setCity] = useState('');
|
||||
const [zipcode, setZipcode] = useState('');
|
||||
const [address, setAddress] = useState('');
|
||||
const [state, setState] = useState('');
|
||||
const [suburb, setSuburb] = useState('');
|
||||
const [latlong, setLatlong] = useState({});
|
||||
const [firstname, setFirstname] = useState('');
|
||||
const [doorno, setDoorno] = useState('');
|
||||
const [landmark, setLandmark] = useState('');
|
||||
const [tenantinfo, setTenantinfo] = useState({});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
|
||||
// Geocode.setApiKey('AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8');
|
||||
|
||||
const [clientname, setClientname] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// fetchprofiledetails(localStorage.getItem('appuserid'));
|
||||
// fetchprofiledetails(181);
|
||||
if (localStorage.getItem('tenantid')) {
|
||||
fetchtenantinfo(localStorage.getItem('tenantid'));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
Geocode.fromAddress(address).then(
|
||||
(response) => {
|
||||
if (response.status == 'OK') {
|
||||
const { lat, lng } = response.results[0].geometry.location;
|
||||
setLatlong({
|
||||
lat,
|
||||
lng
|
||||
});
|
||||
console.log(response);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}, [address]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const opentoast = (message) => {
|
||||
enqueueSnackbar(message, {
|
||||
@@ -90,207 +28,45 @@ const Createclient = () => {
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
// console.log(alertmessage)
|
||||
};
|
||||
|
||||
const fetchprofiledetails = async (userid) => {
|
||||
if (userid) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL2}/tenants/getclient?id=${userid}`)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
if (res.data.message === 'Successful') {
|
||||
let res1 = res.data.details;
|
||||
setMobilenumber(res1.contactno);
|
||||
setEmailaddress(res1.primaryemail);
|
||||
setAddress(res1.address);
|
||||
setCity(res1.city);
|
||||
setZipcode(res1.postcode);
|
||||
setState(res1.state);
|
||||
setSuburb(res1.suburb);
|
||||
setLatlong({
|
||||
lat: res1.latitude,
|
||||
lng: res1.longitude
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchtenantinfo = async (tid) => {
|
||||
setLoading(true);
|
||||
await axios
|
||||
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
if (res.data.status) {
|
||||
setTenantinfo(res.data.details);
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedImage) {
|
||||
setAvatar(URL.createObjectURL(selectedImage));
|
||||
}
|
||||
}, [selectedImage]);
|
||||
|
||||
const { ref: materialRef } = usePlacesWidget({
|
||||
apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY,
|
||||
onPlaceSelected: (place) => {
|
||||
console.log(place);
|
||||
|
||||
setAddress(place.formatted_address);
|
||||
let city1, zipcode1, state1, suburb1;
|
||||
for (let i = 0; i < place.address_components.length; i++) {
|
||||
for (let j = 0; j < place.address_components[i].types.length; j++) {
|
||||
switch (place.address_components[i].types[j]) {
|
||||
case 'locality':
|
||||
city1 = place.address_components[i].long_name;
|
||||
break;
|
||||
case 'administrative_area_level_1':
|
||||
state1 = place.address_components[i].long_name;
|
||||
break;
|
||||
case 'postal_code':
|
||||
zipcode1 = place.address_components[i].long_name;
|
||||
break;
|
||||
case 'sublocality':
|
||||
suburb1 = place.address_components[i].long_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
setCity(city1 || '');
|
||||
setState(state1 || '');
|
||||
setZipcode(zipcode1 || '');
|
||||
setSuburb(suburb1 || '');
|
||||
|
||||
// setAddress(place.formatted_address)
|
||||
},
|
||||
// inputAutocompleteValue: "country",
|
||||
options: {
|
||||
// componentRestrictions: 'us',
|
||||
// types: ["establishment"]
|
||||
types: ['address' || 'geocode']
|
||||
}
|
||||
});
|
||||
|
||||
const createprofile = async () => {
|
||||
console.log('res', businessname, businessno, mobilenumber, emailaddress, address, city, zipcode);
|
||||
|
||||
// if (!businessname) {
|
||||
// opentoast('Fill Business name')
|
||||
// } else if (!businessno) {
|
||||
// opentoast('Fill Registration No')
|
||||
// }
|
||||
// else
|
||||
if (!firstname) {
|
||||
opentoast('Fill Full name');
|
||||
if (!clientname) {
|
||||
opentoast('Fill Client Name');
|
||||
} else if (!mobilenumber) {
|
||||
opentoast('Fill Mobile Number');
|
||||
} else if (!emailaddress) {
|
||||
opentoast('Fill emailaddress');
|
||||
} else if (!address) {
|
||||
opentoast('Fill Address');
|
||||
opentoast('Fill Email Address');
|
||||
} else if (!city) {
|
||||
opentoast('Fill City');
|
||||
} else if (!zipcode) {
|
||||
opentoast('Fill post code');
|
||||
} else if (!suburb) {
|
||||
opentoast('Fill suburb');
|
||||
} else if (!latlong.lat || !latlong.lng) {
|
||||
opentoast('Choose valid address');
|
||||
} else {
|
||||
let obj = {
|
||||
customerid: 0,
|
||||
configid: 1,
|
||||
firstname: firstname,
|
||||
applocationid: tenantinfo.applolcationid,
|
||||
profileimage: '',
|
||||
dialcode: '+91',
|
||||
contactno: mobilenumber,
|
||||
devicetype: '',
|
||||
deviceid: '',
|
||||
customertoken: '',
|
||||
address: address,
|
||||
suburb: suburb,
|
||||
city: city,
|
||||
state: state,
|
||||
postcode: zipcode,
|
||||
landmark: landmark,
|
||||
doorno: doorno,
|
||||
latitude: latlong.lat.toString(),
|
||||
longitude: latlong.lng.toString(),
|
||||
tenantid: parseInt(localStorage.getItem('tenantid')),
|
||||
email: emailaddress
|
||||
};
|
||||
|
||||
console.log(obj);
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await axios
|
||||
.post(`${process.env.REACT_APP_URL}/customers/create`, obj)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
if (res.data.status) {
|
||||
enqueueSnackbar(' Created Successfully ', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
navigate('/clients');
|
||||
// setTimeout(()=>{
|
||||
// fetchprofiledetails(localStorage.getItem('appuserid'));
|
||||
|
||||
// },2000)
|
||||
} else if (res.data.message == 'Customer Already available') {
|
||||
enqueueSnackbar('Customer Already available', {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
setLoading(false);
|
||||
enqueueSnackbar(err.message, {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
});
|
||||
await axios.post(`${process.env.REACT_APP_URL}/crm/clients`, {
|
||||
clientname,
|
||||
email: emailaddress,
|
||||
phone: mobilenumber,
|
||||
city,
|
||||
status: 'pending'
|
||||
});
|
||||
enqueueSnackbar('Created Successfully', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
navigate('/nearle/tenants');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
enqueueSnackbar(err.response?.data?.message || err.message, {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// const [experience, setExperience] = useState('0');
|
||||
|
||||
// const handleChange = (event) => {
|
||||
// setExperience(event.target.value);
|
||||
// };
|
||||
return (
|
||||
<>
|
||||
{loading && <Loader />}
|
||||
@@ -302,158 +78,34 @@ const Createclient = () => {
|
||||
</Grid>
|
||||
<MainCard contentSX={{ p: { xs: 1.5, md: 3 } }}>
|
||||
<Grid container spacing={isMobile ? 2 : 3}>
|
||||
{/* <Grid item xs={12} sm={4} >
|
||||
<MainCard title="Personal Information" sx={{ height: '100%' }}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Stack spacing={2.5} alignItems="center" sx={{ m: 3 }}>
|
||||
<FormLabel
|
||||
htmlFor="change-avtar"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
'&:hover .MuiBox-root': { opacity: 1 },
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<Avatar alt="Avatar 1"
|
||||
src={avatar}
|
||||
sx={{ width: 76, height: 76 }} />
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
backgroundColor: theme.palette.mode === ThemeMode.DARK ? 'rgba(255, 255, 255, .75)' : 'rgba(0,0,0,.65)',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
opacity: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Stack spacing={0.5} alignItems="center">
|
||||
<CameraOutlined style={{ color: theme.palette.secondary.lighter, fontSize: '1.5rem' }} />
|
||||
<Typography sx={{ color: 'secondary.lighter' }} variant="caption">
|
||||
Upload
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</FormLabel>
|
||||
<TextField
|
||||
|
||||
type="file"
|
||||
accept="image/*"
|
||||
id="change-avtar"
|
||||
placeholder="Outlined"
|
||||
variant="outlined"
|
||||
sx={{ display: 'none' }}
|
||||
onChange={(e) => setSelectedImage(e.target.files?.[0])}
|
||||
/>
|
||||
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
|
||||
<Grid item xs={12}
|
||||
>
|
||||
|
||||
</Grid>
|
||||
<Grid item xs={12}
|
||||
>
|
||||
|
||||
</Grid>
|
||||
<Grid item xs={12}
|
||||
>
|
||||
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}
|
||||
|
||||
>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-role-name">Role</InputLabel>
|
||||
<TextField fullWidth
|
||||
|
||||
id="personal-role-name" placeholder="Role Name" autoFocus
|
||||
onChange={(e) => setRole(e.target.value)}
|
||||
value={role}
|
||||
autoComplete='off'
|
||||
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</MainCard>
|
||||
</Grid> */}
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
// sm={8}
|
||||
>
|
||||
<MainCard
|
||||
// title="Contact Information"
|
||||
sx={{ height: '100%' }}
|
||||
contentSX={{ p: { xs: 1.5, md: 2.5 } }}
|
||||
>
|
||||
<Grid item xs={12}>
|
||||
<MainCard sx={{ height: '100%' }} contentSX={{ p: { xs: 1.5, md: 2.5 } }}>
|
||||
<Grid container spacing={isMobile ? 2 : 3}>
|
||||
{/* <Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-first-name">Business Name</InputLabel>
|
||||
<TextField fullWidth
|
||||
id="personal-first-name" placeholder="Business Name" autoFocus
|
||||
onChange={(e) => setBusinessname(e.target.value)}
|
||||
value={businessname}
|
||||
autoComplete='off'
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-last-name">Registration No</InputLabel>
|
||||
<TextField fullWidth
|
||||
id="personal-last-name" placeholder="Registration No"
|
||||
onChange={(e) => setBusinessno(e.target.value)}
|
||||
value={businessno}
|
||||
autoComplete='off'
|
||||
|
||||
/>
|
||||
</Stack>
|
||||
</Grid> */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-last-name">Admin Name</InputLabel>
|
||||
<InputLabel htmlFor="client-name">Client Name</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="personal-last-name"
|
||||
placeholder="Name"
|
||||
onChange={(e) => setFirstname(e.target.value)}
|
||||
value={firstname}
|
||||
id="client-name"
|
||||
placeholder="Client Name"
|
||||
onChange={(e) => setClientname(e.target.value)}
|
||||
value={clientname}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}></Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-phone">Phone Number</InputLabel>
|
||||
<InputLabel htmlFor="client-phone">Phone Number</InputLabel>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
|
||||
<Select defaultValue="+1" disabled sx={{ cursor: 'not-allowed' }}>
|
||||
<MenuItem value="+1">+91</MenuItem>
|
||||
</Select>
|
||||
<TextField
|
||||
type="number"
|
||||
id="personal-phone"
|
||||
// format="##########"
|
||||
// mask="_"
|
||||
id="client-phone"
|
||||
fullWidth
|
||||
// customInput={TextField}
|
||||
placeholder="Phone Number"
|
||||
// defaultValue="8654239581"
|
||||
// onBlur={() => { }}
|
||||
onChange={(e) => {
|
||||
if (e.target.value.toString().length <= 10) {
|
||||
setMobilenumber(e.target.value);
|
||||
@@ -461,20 +113,17 @@ const Createclient = () => {
|
||||
}}
|
||||
value={mobilenumber}
|
||||
autoComplete="off"
|
||||
// disabled
|
||||
sx={{ cursor: 'not-allowed' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-email">Email Address</InputLabel>
|
||||
<InputLabel htmlFor="client-email">Email Address</InputLabel>
|
||||
<TextField
|
||||
type="email"
|
||||
fullWidth
|
||||
// defaultValue="stebin.ben@gmail.com"
|
||||
id="personal-email"
|
||||
id="client-email"
|
||||
placeholder="Email Address"
|
||||
onChange={(e) => setEmailaddress(e.target.value)}
|
||||
value={emailaddress}
|
||||
@@ -482,44 +131,12 @@ const Createclient = () => {
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-address">Address</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
// defaultValue="Street 110-B Kalians Bag, Dewan, M.P. New York"
|
||||
id="personal-address"
|
||||
placeholder="Address"
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
inputRef={materialRef}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-location">Suburb</InputLabel>
|
||||
<InputLabel htmlFor="client-city">City</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
// defaultValue="New York"
|
||||
id="personal-location"
|
||||
placeholder="Location"
|
||||
onChange={(e) => setSuburb(e.target.value)}
|
||||
value={suburb}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-zipcode">City</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
// defaultValue="956754"
|
||||
// type='number'
|
||||
id="personal-zipcode"
|
||||
id="client-city"
|
||||
placeholder="City"
|
||||
onChange={(e) => setCity(e.target.value)}
|
||||
value={city}
|
||||
@@ -527,66 +144,6 @@ const Createclient = () => {
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-location">State</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
// defaultValue="New York"
|
||||
id="personal-location"
|
||||
placeholder="State"
|
||||
onChange={(e) => setState(e.target.value)}
|
||||
value={state}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-zipcode">Post Code</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
// defaultValue="956754"
|
||||
type="number"
|
||||
id="personal-zipcode"
|
||||
placeholder="Zipcode"
|
||||
onChange={(e) => setZipcode(e.target.value)}
|
||||
value={zipcode}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-location">Door No</InputLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
// defaultValue="New York"
|
||||
id="personal-location"
|
||||
placeholder="Door No"
|
||||
onChange={(e) => setDoorno(e.target.value)}
|
||||
value={doorno}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1.25}>
|
||||
<InputLabel htmlFor="personal-email">Landmark</InputLabel>
|
||||
<TextField
|
||||
type="email"
|
||||
fullWidth
|
||||
// defaultValue="stebin.ben@gmail.com"
|
||||
id="personal-email"
|
||||
placeholder="Landmark"
|
||||
onChange={(e) => setLandmark(e.target.value)}
|
||||
value={landmark}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { React, useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { React, useState, useEffect, useRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import { FaRegEdit } from 'react-icons/fa';
|
||||
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
DialogContent,
|
||||
Button,
|
||||
TextField,
|
||||
Autocomplete,
|
||||
Avatar,
|
||||
Paper,
|
||||
useMediaQuery,
|
||||
@@ -33,19 +32,12 @@ import {
|
||||
MdMyLocation,
|
||||
MdPersonPin,
|
||||
MdPhone,
|
||||
MdLocationOn,
|
||||
MdEdit,
|
||||
MdGroups,
|
||||
MdHowToReg,
|
||||
MdPlace,
|
||||
MdOutlineGroups,
|
||||
MdOutlineHowToReg,
|
||||
MdOutlinePlace
|
||||
} from 'react-icons/md';
|
||||
import Geocode from 'react-geocode';
|
||||
import LocationOnIcon from '@mui/icons-material/LocationOn';
|
||||
import parse from 'autosuggest-highlight/parse';
|
||||
import { debounce } from '@mui/material/utils';
|
||||
// project imports
|
||||
import Loader from 'components/Loader';
|
||||
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
||||
@@ -111,23 +103,6 @@ const AccentAvatar = ({ color, selected, size = 24, children }) => (
|
||||
</Avatar>
|
||||
);
|
||||
|
||||
// ==============================|| google address ||============================== //
|
||||
const GOOGLE_MAPS_API_KEY = process.env.REACT_APP_GOOGLE_MAPS_API_KEY;
|
||||
|
||||
function loadScript(src, position, id) {
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('async', '');
|
||||
script.setAttribute('id', id);
|
||||
script.src = src;
|
||||
position.appendChild(script);
|
||||
}
|
||||
|
||||
const autocompleteService = { current: null };
|
||||
|
||||
// ==============================|| MUI TABLE - ENHANCED ||============================== //
|
||||
|
||||
export default function Customers() {
|
||||
@@ -141,146 +116,9 @@ export default function Customers() {
|
||||
const [locaName, setLocoName] = useState('All');
|
||||
const [selectedCustomer, setSelectedCustomer] = useState({}); // to edit
|
||||
const [open, setOpen] = useState(false);
|
||||
const [address, setAddress] = useState('');
|
||||
const [latlong, setLatlong] = useState({});
|
||||
const [city, setCity] = useState('');
|
||||
const [postcode, setPostcode] = useState('');
|
||||
const [state, setState] = useState('');
|
||||
const [suburb, setSuburb] = useState('');
|
||||
const [searchword, setSearchword] = useState('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||
|
||||
// ==============================|| for google address ||============================== //
|
||||
const [value, setValue] = useState(null);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [options, setOptions] = useState([]);
|
||||
const loaded = useRef(false);
|
||||
if (typeof window !== 'undefined' && !loaded.current) {
|
||||
if (!document.querySelector('#google-maps')) {
|
||||
loadScript(
|
||||
`https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places`,
|
||||
document.querySelector('head'),
|
||||
'google-maps'
|
||||
);
|
||||
}
|
||||
|
||||
loaded.current = true;
|
||||
}
|
||||
const fetch = useMemo(
|
||||
() =>
|
||||
debounce((request, callback) => {
|
||||
autocompleteService.current.getPlacePredictions(request, callback);
|
||||
}, 400),
|
||||
[]
|
||||
);
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
if (!autocompleteService.current && window.google) {
|
||||
autocompleteService.current = new window.google.maps.places.AutocompleteService();
|
||||
}
|
||||
if (!autocompleteService.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (inputValue === '') {
|
||||
setOptions(value ? [value] : []);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
fetch({ input: inputValue }, (results) => {
|
||||
if (active) {
|
||||
let newOptions = [];
|
||||
|
||||
if (value) {
|
||||
newOptions = [value];
|
||||
}
|
||||
|
||||
if (results) {
|
||||
newOptions = [...newOptions, ...results];
|
||||
}
|
||||
|
||||
setOptions(newOptions);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [value, inputValue, fetch]);
|
||||
|
||||
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
|
||||
useEffect(() => {
|
||||
try {
|
||||
console.log('selected address =>', address);
|
||||
Geocode.fromAddress(address).then(
|
||||
(response) => {
|
||||
console.log('lat long response =>', response.results[0]);
|
||||
if (response.status == 'OK') {
|
||||
const { lat, lng } = response.results[0].geometry.location;
|
||||
setLatlong({
|
||||
lat,
|
||||
lng
|
||||
});
|
||||
// setSelectedCustomer({
|
||||
// ...selectedCustomer,
|
||||
// latitude: lat,
|
||||
// longitude: lng
|
||||
// });
|
||||
if (response.results[0].address_components) {
|
||||
let place = response.results[0];
|
||||
let city1, zipcode1, state1, suburb1;
|
||||
for (let i = 0; i < place.address_components.length; i++) {
|
||||
for (let j = 0; j < place.address_components[i].types.length; j++) {
|
||||
switch (place.address_components[i].types[j]) {
|
||||
case 'locality':
|
||||
city1 = place.address_components[i].long_name;
|
||||
break;
|
||||
case 'administrative_area_level_1':
|
||||
state1 = place.address_components[i].long_name;
|
||||
break;
|
||||
case 'postal_code':
|
||||
zipcode1 = place.address_components[i].long_name;
|
||||
break;
|
||||
case 'sublocality':
|
||||
suburb1 = place.address_components[i].long_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
setCity(city1 || '');
|
||||
setState(state1 || '');
|
||||
setPostcode(zipcode1 || '');
|
||||
setSuburb(suburb1 || '');
|
||||
setSelectedCustomer((prev) => ({
|
||||
...prev,
|
||||
city: city1 || '',
|
||||
state: state1 || '',
|
||||
postcode: zipcode1 || '',
|
||||
suburb: suburb1 || '',
|
||||
latitude: lat || '',
|
||||
longitude: lng || ''
|
||||
}));
|
||||
}
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}, [address]);
|
||||
|
||||
// useEffect(() => {
|
||||
// selectedCustomer &&
|
||||
// setLatlong({
|
||||
// lat: selectedCustomer.latitude,
|
||||
// lng: selectedCustomer.longitude
|
||||
// });
|
||||
// }, [selectedCustomer]);
|
||||
|
||||
// ==============================|| getallcustomers (customers) ||============================== //
|
||||
|
||||
const {
|
||||
@@ -336,67 +174,23 @@ export default function Customers() {
|
||||
useEffect(() => {
|
||||
console.log('pageCount', pageCount);
|
||||
}, [pageCount]);
|
||||
// ==============================|| updateCustomer (post)||============================== //
|
||||
// ==============================|| updateCustomer (PATCH) ||============================== //
|
||||
const updateCustomer = async () => {
|
||||
console.log('selectedCustomer', selectedCustomer);
|
||||
|
||||
if (!selectedCustomer.firstname) {
|
||||
OpenToast('Enter Door NO', 'warning', 1500);
|
||||
} else if (!selectedCustomer.contactno) {
|
||||
OpenToast('Enter Contact Number ', 'warning', 1500);
|
||||
} else if (!selectedCustomer.address) {
|
||||
OpenToast('Enter Valid Address', 'warning', 1500);
|
||||
} else if (!selectedCustomer.suburb) {
|
||||
OpenToast('Enter Suburb', 'warning', 1500);
|
||||
} else if (!selectedCustomer.city) {
|
||||
OpenToast('Enter City ', 'warning', 1500);
|
||||
} else if (!selectedCustomer.state) {
|
||||
OpenToast('Enter State', 'warning', 1500);
|
||||
} else if (!selectedCustomer.postcode) {
|
||||
OpenToast('Enter PostCode', 'warning', 1500);
|
||||
} else if (!selectedCustomer.landmark) {
|
||||
OpenToast('Enter Landmark', 'warning', 1500);
|
||||
} else if (!selectedCustomer.latitude) {
|
||||
OpenToast('Enter Latitude', 'warning', 1500);
|
||||
} else if (!selectedCustomer.longitude) {
|
||||
OpenToast('Enter Longitude', 'warning', 1500);
|
||||
} else {
|
||||
try {
|
||||
const postUpdateResponse = await axios.put(`${process.env.REACT_APP_URL}/customers/update`, {
|
||||
customerid: selectedCustomer.customerid,
|
||||
configid: 1,
|
||||
firstname: selectedCustomer.firstname,
|
||||
applocationid: selectedCustomer.applocationid,
|
||||
profileimage: '',
|
||||
dialcode: '+91',
|
||||
contactno: selectedCustomer.contactno,
|
||||
devicetype: '',
|
||||
deviceid: '',
|
||||
customertoken: '123',
|
||||
address: selectedCustomer.address,
|
||||
suburb: suburb,
|
||||
city: city,
|
||||
state: state,
|
||||
postcode: postcode,
|
||||
landmark: selectedCustomer.landmark,
|
||||
doorno: selectedCustomer.doorno,
|
||||
latitude: selectedCustomer.latitude.toString(),
|
||||
longitude: selectedCustomer.longitude.toString()
|
||||
});
|
||||
|
||||
console.log('postUpdateResponse', postUpdateResponse);
|
||||
if (postUpdateResponse.data.status) {
|
||||
OpenToast(postUpdateResponse.data.message, 'success', 1500);
|
||||
setOpen(false);
|
||||
getallcustomersRefetch();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('postUpdate error', error);
|
||||
}
|
||||
try {
|
||||
await axios.patch(`${process.env.REACT_APP_URL}/admin/customers/${selectedCustomer.appcustomerid}`, {
|
||||
name: selectedCustomer.name,
|
||||
phone: selectedCustomer.phone,
|
||||
email: selectedCustomer.email
|
||||
});
|
||||
OpenToast('Customer updated successfully', 'success', 2000);
|
||||
setOpen(false);
|
||||
getallcustomersRefetch();
|
||||
} catch (err) {
|
||||
OpenToast(err.response?.data?.message || 'Update failed', 'error', 2000);
|
||||
}
|
||||
};
|
||||
const KPI_META = [
|
||||
{ key: 'total', label: 'Total Customers', color: '#662582', icon: MdOutlineGroups, value: pageCount?.Total ?? 0 },
|
||||
{ key: 'total', label: 'Total Customers', color: '#C01227', icon: MdOutlineGroups, value: pageCount?.Total ?? 0 },
|
||||
{ key: 'loaded', label: 'Loaded in View', color: '#0ea5e9', icon: MdOutlineHowToReg, value: rows.length },
|
||||
{ key: 'zone', label: 'Active Zone', color: '#10b981', icon: MdOutlinePlace, value: locaName || 'All Zones' }
|
||||
];
|
||||
@@ -416,7 +210,7 @@ export default function Customers() {
|
||||
setAppId={setAppId}
|
||||
setLocoName={setLocoName}
|
||||
pill
|
||||
accentColor="#662582"
|
||||
accentColor="#C01227"
|
||||
icon={<MdMyLocation size={14} />}
|
||||
placeholder="Select Zone"
|
||||
paperComponent={SoftPaper}
|
||||
@@ -466,7 +260,7 @@ export default function Customers() {
|
||||
spacing={1.25}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1.25}>
|
||||
<AccentAvatar color="#662582" size={32}>
|
||||
<AccentAvatar color="#C01227" size={32}>
|
||||
<MdGroups size={18} />
|
||||
</AccentAvatar>
|
||||
<Stack>
|
||||
@@ -491,11 +285,11 @@ export default function Customers() {
|
||||
m: 0,
|
||||
width: '100%',
|
||||
borderRadius: 999,
|
||||
bgcolor: tint('#662582'),
|
||||
'& fieldset': { borderColor: edge('#662582'), borderWidth: 1.5 },
|
||||
'&:hover fieldset': { borderColor: '#662582' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 2 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` }
|
||||
bgcolor: tint('#C01227'),
|
||||
'& fieldset': { borderColor: edge('#C01227'), borderWidth: 1.5 },
|
||||
'&:hover fieldset': { borderColor: '#C01227' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@@ -534,20 +328,20 @@ export default function Customers() {
|
||||
) : (
|
||||
rows?.map((row, index) => (
|
||||
<MobileCard
|
||||
key={row.customerid || `${row.firstname}-${index}`}
|
||||
accent="#662582"
|
||||
key={row.appcustomerid || `${row.name}-${index}`}
|
||||
accent="#C01227"
|
||||
header={
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
|
||||
<AccentAvatar color="#662582" size={36}>
|
||||
<AccentAvatar color="#C01227" size={36}>
|
||||
<MdPersonPin size={18} />
|
||||
</AccentAvatar>
|
||||
<Stack sx={{ minWidth: 0 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||
{row.firstname || '—'}
|
||||
{row.name || '—'}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
ID #{row.customerid}
|
||||
ID #{row.appcustomerid}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -573,35 +367,10 @@ export default function Customers() {
|
||||
}
|
||||
>
|
||||
<MobileFieldGrid>
|
||||
<MobileField label="Contact" value={row.contactno || '—'} />
|
||||
<MobileField label="Location">
|
||||
{row.suburb ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
px: 1,
|
||||
py: 0.375,
|
||||
borderRadius: 999,
|
||||
bgcolor: tint('#10b981'),
|
||||
border: `1px solid ${edge('#10b981')}`,
|
||||
color: '#10b981',
|
||||
fontSize: 11,
|
||||
fontWeight: 800
|
||||
}}
|
||||
>
|
||||
<MdLocationOn size={12} /> {row.suburb}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textMuted }}>—</Typography>
|
||||
)}
|
||||
</MobileField>
|
||||
<MobileField label="Address" full>
|
||||
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }}>
|
||||
{row.address || '—'}
|
||||
</Typography>
|
||||
</MobileField>
|
||||
<MobileField label="Phone" value={row.phone || '—'} />
|
||||
<MobileField label="Email" value={row.email || '—'} />
|
||||
<MobileField label="Total Bookings" value={row.totalbookings ?? 0} />
|
||||
<MobileField label="Joined" value={row.createdat ? new Date(row.createdat).toLocaleDateString() : '—'} />
|
||||
</MobileFieldGrid>
|
||||
</MobileCard>
|
||||
))
|
||||
@@ -626,9 +395,9 @@ export default function Customers() {
|
||||
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
|
||||
'&::-webkit-scrollbar': { width: 10, height: 10 },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: edge('#662582'),
|
||||
backgroundColor: edge('#C01227'),
|
||||
borderRadius: 8,
|
||||
'&:hover': { backgroundColor: '#662582' }
|
||||
'&:hover': { backgroundColor: '#C01227' }
|
||||
},
|
||||
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
|
||||
}}
|
||||
@@ -652,10 +421,11 @@ export default function Customers() {
|
||||
}}
|
||||
>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Customer</TableCell>
|
||||
<TableCell>Contact</TableCell>
|
||||
<TableCell>Address</TableCell>
|
||||
<TableCell>Location</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Phone</TableCell>
|
||||
<TableCell>Email</TableCell>
|
||||
<TableCell>Total Bookings</TableCell>
|
||||
<TableCell>Joined</TableCell>
|
||||
<TableCell align="right">Action</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -664,7 +434,7 @@ export default function Customers() {
|
||||
{customersIsLoading && <OrdersTableSkeleton />}
|
||||
{rows?.length === 0 && !customersIsLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} sx={{ py: 6 }}>
|
||||
<TableCell colSpan={7} sx={{ py: 6 }}>
|
||||
<Stack alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||||
<MdGroups size={28} />
|
||||
@@ -681,7 +451,7 @@ export default function Customers() {
|
||||
) : (
|
||||
rows?.map((row, index) => (
|
||||
<TableRow
|
||||
key={row.customerid || `${row.firstname}-${index}`}
|
||||
key={row.appcustomerid || `${row.name}-${index}`}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.15s',
|
||||
@@ -700,7 +470,7 @@ export default function Customers() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<AccentAvatar color="#662582" size={36}>
|
||||
<AccentAvatar color="#C01227" size={36}>
|
||||
<MdPersonPin size={18} />
|
||||
</AccentAvatar>
|
||||
<Stack>
|
||||
@@ -708,70 +478,39 @@ export default function Customers() {
|
||||
variant="subtitle2"
|
||||
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{row.firstname || '—'}
|
||||
{row.name || '—'}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
ID #{row.customerid}
|
||||
ID #{row.appcustomerid}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stack>
|
||||
<Stack direction="row" alignItems="center" spacing={0.5}>
|
||||
<MdPhone size={12} color={DT.textMuted} />
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{row.contactno || '—'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
{row.email && (
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{row.email}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell sx={{ maxWidth: 280 }}>
|
||||
<Tooltip title={row.address || ''} placement="top">
|
||||
<Stack direction="row" alignItems="center" spacing={0.5}>
|
||||
<MdPhone size={12} color={DT.textMuted} />
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: DT.textSecondary,
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
variant="subtitle2"
|
||||
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{row.address || '—'}
|
||||
{row.phone || '—'}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{row.suburb ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
px: 1,
|
||||
py: 0.375,
|
||||
borderRadius: 999,
|
||||
bgcolor: tint('#10b981'),
|
||||
border: `1px solid ${edge('#10b981')}`,
|
||||
color: '#10b981',
|
||||
fontSize: 11,
|
||||
fontWeight: 800
|
||||
}}
|
||||
>
|
||||
<MdLocationOn size={12} /> {row.suburb}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="caption" sx={{ color: DT.textMuted }}>—</Typography>
|
||||
)}
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{row.email || '—'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||||
{row.totalbookings ?? 0}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{row.createdat ? new Date(row.createdat).toLocaleDateString() : '—'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip title="Edit customer" placement="top">
|
||||
@@ -797,7 +536,7 @@ export default function Customers() {
|
||||
)}
|
||||
{rows?.length !== 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} sx={{ borderBottom: 'none' }}>
|
||||
<TableCell colSpan={7} sx={{ borderBottom: 'none' }}>
|
||||
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
|
||||
{isFetchingNextPage || hasNextPage ? (
|
||||
<LoaderWithImage />
|
||||
@@ -829,7 +568,7 @@ export default function Customers() {
|
||||
<DialogTitle
|
||||
id="alert-dialog-title"
|
||||
sx={{
|
||||
background: `linear-gradient(135deg, #662582 0%, #9255AB 100%)`,
|
||||
background: `linear-gradient(135deg, #C01227 0%, #D35968 100%)`,
|
||||
color: '#fff',
|
||||
py: 2
|
||||
}}
|
||||
@@ -843,36 +582,36 @@ export default function Customers() {
|
||||
Customer
|
||||
</Typography>
|
||||
<Typography sx={{ fontWeight: 800, fontSize: { xs: '1.05rem', sm: '1.2rem' }, lineHeight: 1.2, mt: 0.25 }}>
|
||||
Edit {selectedCustomer?.firstname || 'Customer'}
|
||||
Edit {selectedCustomer?.name || 'Customer'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 2 }}>
|
||||
<Grid item xs={6}>
|
||||
<Typography sx={{ mb: 1 }}>Customer Name</Typography>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography sx={{ mb: 1 }}>Name</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
defaultValue={selectedCustomer?.firstname}
|
||||
value={selectedCustomer?.name || ''}
|
||||
onChange={(e) => {
|
||||
setSelectedCustomer({
|
||||
...selectedCustomer,
|
||||
firstname: e.target.value
|
||||
});
|
||||
setSelectedCustomer((prev) => ({
|
||||
...prev,
|
||||
name: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography sx={{ mb: 1 }}>Contact Number</Typography>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography sx={{ mb: 1 }}>Phone</Typography>
|
||||
<Stack direction={'row'} spacing={1}>
|
||||
<TextField readonly variant="outlined" value={'+91'} sx={{ width: 60 }} />
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
type="text"
|
||||
value={selectedCustomer?.contactno || ''}
|
||||
value={selectedCustomer?.phone || ''}
|
||||
inputProps={{
|
||||
maxLength: 10,
|
||||
inputMode: 'numeric', // mobile numeric keypad
|
||||
@@ -883,168 +622,40 @@ export default function Customers() {
|
||||
|
||||
setSelectedCustomer((prev) => ({
|
||||
...prev,
|
||||
contactno: value
|
||||
phone: value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography sx={{ mb: 1 }}>Address</Typography>
|
||||
<Autocomplete
|
||||
id="google-map-demo"
|
||||
sx={{}}
|
||||
fullWidth
|
||||
getOptionLabel={(option) => (typeof option === 'string' ? option : option?.description || '')}
|
||||
filterOptions={(x) => x}
|
||||
options={options}
|
||||
autoComplete
|
||||
includeInputInList
|
||||
filterSelectedOptions
|
||||
value={selectedCustomer?.address}
|
||||
noOptionsText="No locations"
|
||||
onChange={(event, newValue) => {
|
||||
setOptions(newValue ? [newValue, ...options] : options);
|
||||
setValue(newValue);
|
||||
console.log('newValue', newValue || '');
|
||||
setAddress(newValue?.description);
|
||||
setSelectedCustomer({
|
||||
...selectedCustomer,
|
||||
address: newValue?.description
|
||||
});
|
||||
}}
|
||||
onInputChange={(event, newInputValue) => {
|
||||
setInputValue(newInputValue);
|
||||
}}
|
||||
renderInput={(params) => <TextField {...params} fullWidth />}
|
||||
renderOption={(props, option) => {
|
||||
const matches = option.structured_formatting.main_text_matched_substrings || [];
|
||||
|
||||
const parts = parse(
|
||||
option.structured_formatting.main_text,
|
||||
matches.map((match) => [match.offset, match.offset + match.length])
|
||||
);
|
||||
|
||||
return (
|
||||
<li {...props}>
|
||||
<Grid container alignItems="center">
|
||||
<Grid item sx={{ display: 'flex', width: 44 }}>
|
||||
<LocationOnIcon sx={{ color: 'text.secondary' }} />
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
sx={{
|
||||
width: 'calc(100% - 44px)',
|
||||
wordWrap: 'break-word'
|
||||
}}
|
||||
>
|
||||
{parts?.map((part, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
component="span"
|
||||
sx={{
|
||||
fontWeight: part.highlight ? 'bold' : 'regular'
|
||||
}}
|
||||
>
|
||||
{part.text}
|
||||
</Box>
|
||||
))}
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{option?.structured_formatting.secondary_text}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography sx={{ mb: 1 }}>Location</Typography>
|
||||
<Typography sx={{ mb: 1 }}>Email</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={selectedCustomer?.suburb}
|
||||
type="email"
|
||||
value={selectedCustomer?.email || ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSelectedCustomer((prev) => ({
|
||||
...prev,
|
||||
suburb: value
|
||||
email: e.target.value
|
||||
}));
|
||||
// setSuburb(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography sx={{ mb: 1 }}>City</Typography>
|
||||
<Grid item xs={12} sm={6} sx={{ cursor: 'not-allowed' }}>
|
||||
<Typography sx={{ mb: 1 }}>Total Bookings</Typography>
|
||||
<TextField variant="outlined" fullWidth disabled value={selectedCustomer?.totalbookings ?? 0} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} sx={{ cursor: 'not-allowed' }}>
|
||||
<Typography sx={{ mb: 1 }}>Joined</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={selectedCustomer.city || city || ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSelectedCustomer((prev) => ({
|
||||
...prev,
|
||||
city: value
|
||||
}));
|
||||
// setCity(e.target.value);
|
||||
}}
|
||||
disabled
|
||||
value={selectedCustomer?.createdat ? new Date(selectedCustomer.createdat).toLocaleDateString() : '—'}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography sx={{ mb: 1 }}>State</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={selectedCustomer.state || state || ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSelectedCustomer((prev) => ({
|
||||
...prev,
|
||||
state: value
|
||||
}));
|
||||
// setState(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography sx={{ mb: 1 }}>Postcode</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={postcode == '' ? selectedCustomer.postcode : postcode}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSelectedCustomer((prev) => ({
|
||||
...prev,
|
||||
postcode: value
|
||||
}));
|
||||
// setPostcode(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography sx={{ mb: 1 }}>Landmark</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
defaultValue={selectedCustomer.landmark}
|
||||
onChange={(e) => {
|
||||
setSelectedCustomer({
|
||||
...selectedCustomer,
|
||||
landmark: e.target.value
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6} sx={{ cursor: 'not-allowed' }}>
|
||||
<Typography sx={{ mb: 1 }}>Latitude</Typography>
|
||||
<TextField variant="outlined" fullWidth value={latlong.lat} sx={{ cursor: 'not-allowed' }} />
|
||||
</Grid>
|
||||
<Grid item xs={6} sx={{ cursor: 'not-allowed' }}>
|
||||
<Typography sx={{ mb: 1 }}>Longitude</Typography>
|
||||
<TextField readonly variant="outlined" fullWidth value={latlong.lng} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ mr: 2, mb: 2 }}>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
const Dashboard = () => {
|
||||
return (
|
||||
<>
|
||||
<h1>Dashboard</h1>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
317
src/pages/nearle/dashboard/Dashboard.js
Normal file
@@ -0,0 +1,317 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Avatar, Box, Chip, Grid, Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography, Button } from '@mui/material';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
MdOutlineLocalShipping,
|
||||
MdOutlinePendingActions,
|
||||
MdOutlineCheckCircle,
|
||||
MdTwoWheeler,
|
||||
MdOutlineSmartToy,
|
||||
MdLocationCity,
|
||||
MdArrowForward,
|
||||
MdCircle
|
||||
} from 'react-icons/md';
|
||||
import PageHeader from 'components/nearle_components/PageHeader';
|
||||
import StatCard from 'components/nearle_components/StatCard';
|
||||
import { fetchHubs } from 'pages/api/api';
|
||||
|
||||
const DT = {
|
||||
radiusCard: 16,
|
||||
textPrimary: '#0f172a',
|
||||
textSecondary: '#64748b',
|
||||
textMuted: '#94a3b8',
|
||||
borderSubtle: '#e2e8f0',
|
||||
divider: '#f1f5f9',
|
||||
surface: '#ffffff',
|
||||
surfaceAlt: '#f8fafc'
|
||||
};
|
||||
const a = (c, suffix) => `${c}${suffix}`;
|
||||
const tint = (c) => a(c, '08');
|
||||
const soft = (c) => a(c, '18');
|
||||
const edge = (c) => a(c, '55');
|
||||
const BRAND = '#C01227';
|
||||
|
||||
// Static list — the actual multi-agent pipeline (decide-assignment etc.) has
|
||||
// no status endpoint yet, so this is presentational only, per spec.
|
||||
const AGENTS = [
|
||||
'Intake Agent',
|
||||
'Geocoding Agent',
|
||||
'Pricing Agent',
|
||||
'Miler Matching Agent',
|
||||
'Route Optimisation Agent',
|
||||
'Notification Agent',
|
||||
'Fraud Detection Agent',
|
||||
'Reconciliation Agent'
|
||||
];
|
||||
|
||||
const fetchBookingsByStatus = async (status, pagesize = 1) => {
|
||||
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status, pagesize } });
|
||||
return res.data;
|
||||
};
|
||||
|
||||
const Dashboard = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: activeStatusCounts, isLoading: activeLoading } = useQuery({
|
||||
queryKey: ['dashboardActiveCounts'],
|
||||
queryFn: async () => {
|
||||
const [assigned, scheduled, atCustomer] = await Promise.all([
|
||||
fetchBookingsByStatus('Miler_Assigned'),
|
||||
fetchBookingsByStatus('Pickup_Scheduled'),
|
||||
fetchBookingsByStatus('At_Customer')
|
||||
]);
|
||||
return (assigned.total || 0) + (scheduled.total || 0) + (atCustomer.total || 0);
|
||||
}
|
||||
});
|
||||
|
||||
const { data: pendingCount, isLoading: pendingLoading } = useQuery({
|
||||
queryKey: ['dashboardPendingCount'],
|
||||
queryFn: async () => (await fetchBookingsByStatus('Pending_Pickup')).total || 0
|
||||
});
|
||||
|
||||
const { data: deliveredTodayCount, isLoading: deliveredLoading } = useQuery({
|
||||
queryKey: ['dashboardDeliveredToday'],
|
||||
queryFn: async () => (await fetchBookingsByStatus('Delivered')).total || 0
|
||||
});
|
||||
|
||||
const { data: milers = [], isLoading: milersLoading } = useQuery({
|
||||
queryKey: ['dashboardMilers'],
|
||||
queryFn: async () => {
|
||||
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
|
||||
return res.data?.data || [];
|
||||
}
|
||||
});
|
||||
const availableMilers = milers.filter((m) => m.availabilitystatus === 'Available').length;
|
||||
|
||||
const { data: hubs = [] } = useQuery({ queryKey: ['fetchHubs'], queryFn: fetchHubs });
|
||||
|
||||
const { data: recentBookings = [], isLoading: recentLoading } = useQuery({
|
||||
queryKey: ['dashboardRecentBookings'],
|
||||
queryFn: async () => {
|
||||
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { pagesize: 10 } });
|
||||
return res.data?.data || [];
|
||||
}
|
||||
});
|
||||
|
||||
// No `city` field is confirmed on a booking (only pickupaddress/deliveryaddress
|
||||
// were ever specified) -- derives a best-effort city breakdown from hubs
|
||||
// instead, since hubs do have a confirmed `city` field. Flagged rather than
|
||||
// guessed at a booking-level city grouping that may not exist.
|
||||
const cityRows = Object.values(
|
||||
hubs.reduce((acc, h) => {
|
||||
const city = h.city || 'Unknown';
|
||||
if (!acc[city]) acc[city] = { city, milers: 0, hubs: 0 };
|
||||
acc[city].hubs += 1;
|
||||
acc[city].milers += milers.filter((m) => m.hubid === h.hubid).length;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const kpis = [
|
||||
{ key: 'active', label: 'Active Bookings', color: BRAND, icon: MdOutlineLocalShipping, value: activeStatusCounts ?? 0, loading: activeLoading },
|
||||
{ key: 'pending', label: 'Pending Assignment', color: '#f59e0b', icon: MdOutlinePendingActions, value: pendingCount ?? 0, loading: pendingLoading },
|
||||
{ key: 'delivered', label: 'Delivered Today', color: '#10b981', icon: MdOutlineCheckCircle, value: deliveredTodayCount ?? 0, loading: deliveredLoading },
|
||||
{ key: 'milers', label: 'Milers Available', color: '#0ea5e9', icon: MdTwoWheeler, value: availableMilers, loading: milersLoading }
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Dashboard" subtitle="Live · Operations overview" live />
|
||||
|
||||
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
|
||||
{kpis.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Grid item key={item.key} xs={6} sm={3}>
|
||||
<StatCard title={item.label} value={item.value} icon={<Icon size={20} />} color={item.color} loading={item.loading} />
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} md={7}>
|
||||
<Paper elevation={0} sx={{ borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', overflow: 'hidden' }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ p: 2, borderBottom: `1px solid ${DT.divider}` }}>
|
||||
<MdLocationCity size={18} color={BRAND} />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
|
||||
City Breakdown
|
||||
</Typography>
|
||||
</Stack>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow sx={{ '& th': { color: DT.textSecondary, fontWeight: 800, fontSize: 11, textTransform: 'uppercase' } }}>
|
||||
<TableCell>City</TableCell>
|
||||
<TableCell align="center">Hubs</TableCell>
|
||||
<TableCell align="center">Milers</TableCell>
|
||||
<TableCell align="center">Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{cityRows.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} sx={{ textAlign: 'center', py: 4, color: DT.textMuted }}>
|
||||
No hub data yet.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{cityRows.map((row) => (
|
||||
<TableRow key={row.city} sx={{ '& td': { borderBottom: `1px solid ${DT.divider}` } }}>
|
||||
<TableCell sx={{ fontWeight: 700 }}>{row.city}</TableCell>
|
||||
<TableCell align="center">{row.hubs}</TableCell>
|
||||
<TableCell align="center">{row.milers}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Chip
|
||||
size="small"
|
||||
label={row.milers > 0 ? 'Active' : 'Idle'}
|
||||
sx={{
|
||||
bgcolor: row.milers > 0 ? tint('#10b981') : tint('#94a3b8'),
|
||||
color: row.milers > 0 ? '#10b981' : '#94a3b8',
|
||||
border: `1px solid ${edge(row.milers > 0 ? '#10b981' : '#94a3b8')}`,
|
||||
fontWeight: 700
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
|
||||
<Paper elevation={0} sx={{ mt: 2.5, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', overflow: 'hidden' }}>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ p: 2, borderBottom: `1px solid ${DT.divider}` }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<MdOutlineLocalShipping size={18} color={BRAND} />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
|
||||
Recent Bookings
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Button size="small" endIcon={<MdArrowForward size={14} />} onClick={() => navigate('/nearle/orders')} sx={{ textTransform: 'none', fontWeight: 700 }}>
|
||||
View all
|
||||
</Button>
|
||||
</Stack>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow sx={{ '& th': { color: DT.textSecondary, fontWeight: 800, fontSize: 11, textTransform: 'uppercase' } }}>
|
||||
<TableCell>Booking</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Created</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{recentLoading && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} sx={{ textAlign: 'center', py: 4, color: DT.textMuted }}>
|
||||
Loading…
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{!recentLoading && recentBookings.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} sx={{ textAlign: 'center', py: 4, color: DT.textMuted }}>
|
||||
No bookings yet.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{recentBookings.map((b) => (
|
||||
<TableRow
|
||||
key={b.bookingid}
|
||||
hover
|
||||
sx={{ cursor: 'pointer', '& td': { borderBottom: `1px solid ${DT.divider}` } }}
|
||||
onClick={() => navigate(`/nearle/bookings/${b.bookingid}`)}
|
||||
>
|
||||
<TableCell sx={{ fontWeight: 700 }}>{b.bookingreference || `#${b.bookingid}`}</TableCell>
|
||||
<TableCell>
|
||||
<Chip size="small" label={b.status || '—'} sx={{ bgcolor: tint(BRAND), color: BRAND, border: `1px solid ${edge(BRAND)}`, fontWeight: 700 }} />
|
||||
</TableCell>
|
||||
<TableCell sx={{ color: DT.textSecondary, fontSize: 13 }}>
|
||||
{b.createdat ? dayjs(b.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={5}>
|
||||
<Paper elevation={0} sx={{ borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', overflow: 'hidden' }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ p: 2, borderBottom: `1px solid ${DT.divider}` }}>
|
||||
<MdOutlineSmartToy size={18} color="#6366f1" />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
|
||||
Agent System Status
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack spacing={0} sx={{ p: 1 }}>
|
||||
{AGENTS.map((agent) => (
|
||||
<Stack
|
||||
key={agent}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
sx={{ px: 1.5, py: 1.25, borderRadius: 2, '&:hover': { bgcolor: DT.surfaceAlt } }}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1.25}>
|
||||
<Avatar sx={{ width: 30, height: 30, bgcolor: soft('#6366f1'), color: '#6366f1' }}>
|
||||
<MdOutlineSmartToy size={15} />
|
||||
</Avatar>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, color: DT.textPrimary }}>
|
||||
{agent}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack direction="row" alignItems="center" spacing={0.5}>
|
||||
<MdCircle size={8} color="#10b981" />
|
||||
<Typography variant="caption" sx={{ color: '#10b981', fontWeight: 700 }}>
|
||||
Running
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Paper
|
||||
elevation={0}
|
||||
component="button"
|
||||
onClick={() => navigate('/nearle/hubs')}
|
||||
sx={{
|
||||
mt: 2.5,
|
||||
width: '100%',
|
||||
p: 2,
|
||||
borderRadius: `${DT.radiusCard}px`,
|
||||
border: `1px solid ${edge(BRAND)}`,
|
||||
background: tint(BRAND),
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
textAlign: 'left',
|
||||
font: 'inherit'
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ bgcolor: BRAND, color: '#fff' }}>
|
||||
<MdLocationCity size={18} />
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 800, color: DT.textPrimary }}>Manage Hubs</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{hubs.length} hub{hubs.length === 1 ? '' : 's'} configured
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<MdArrowForward size={18} color={BRAND} />
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
MdStorefront,
|
||||
MdLocationOn,
|
||||
MdDirectionsBike,
|
||||
MdLocalShipping,
|
||||
MdNotificationsActive,
|
||||
MdPersonPin,
|
||||
MdHistoryToggleOff,
|
||||
@@ -26,8 +25,6 @@ import {
|
||||
MdCancel,
|
||||
MdInventory2,
|
||||
MdHourglassEmpty,
|
||||
MdRoute,
|
||||
MdSkipNext,
|
||||
MdTune,
|
||||
MdMyLocation,
|
||||
MdOutlineLocalShipping,
|
||||
@@ -126,7 +123,7 @@ const DT = {
|
||||
divider: '#f1f5f9',
|
||||
surface: '#ffffff',
|
||||
surfaceAlt: '#f8fafc',
|
||||
brand: '#662582'
|
||||
brand: '#C01227'
|
||||
};
|
||||
|
||||
// Quick alpha helpers (hex + percentage suffix). Mirrors the batch-dropdown
|
||||
@@ -162,30 +159,34 @@ const pillFieldSx = () => ({
|
||||
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: DT.textMuted }
|
||||
});
|
||||
|
||||
// Status palette — drives tab pills, row status badges, dialogs.
|
||||
// Status palette — drives tab pills, row status badges, dialogs. Keys are
|
||||
// Doormile's real booking statuses, lowercased (row.status is compared
|
||||
// lowercased everywhere below so exact backend casing doesn't matter).
|
||||
const STATUS_META = {
|
||||
pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||
accepted: { label: 'Accepted', color: '#6366f1', icon: MdPersonPin },
|
||||
arrived: { label: 'Arrived', color: '#06b6d4', icon: MdLocationOn },
|
||||
picked: { label: 'Picked', color: '#8b5cf6', icon: MdInventory2 },
|
||||
active: { label: 'Active', color: '#14b8a6', icon: MdRoute },
|
||||
skipped: { label: 'Skipped', color: '#f97316', icon: MdSkipNext },
|
||||
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
|
||||
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
|
||||
all: { label: 'All', color: '#94a3b8', icon: MdAllInclusive },
|
||||
pending_pickup: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||
miler_assigned: { label: 'Assigned', color: '#6366f1', icon: MdPersonPin },
|
||||
pickup_scheduled: { label: 'Scheduled', color: '#06b6d4', icon: MdHistoryToggleOff },
|
||||
at_customer: { label: 'At Customer', color: '#8b5cf6', icon: MdLocationOn },
|
||||
picked_up: { label: 'Picked Up', color: '#14b8a6', icon: MdInventory2 },
|
||||
at_hub: { label: 'At Hub', color: '#0ea5e9', icon: MdStorefront },
|
||||
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
|
||||
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
|
||||
};
|
||||
|
||||
// Ordered status list driving the tabs row (left → right). Each entry binds
|
||||
// the visual meta above to the `currentStatus` key the queries use AND to the
|
||||
// `batchCounts` key (so the chip count for the tab is one lookup).
|
||||
// Ordered status list driving the tabs row (left → right). `status` is the
|
||||
// lowercased key into STATUS_META/batchCounts; `apiStatus` is the exact
|
||||
// casing Doormile's /admin/bookings status filter expects.
|
||||
const STATUS_TABS = [
|
||||
{ status: 'pending', countKey: 'uncoveredLength' },
|
||||
{ status: 'accepted', countKey: 'assignedLength' },
|
||||
{ status: 'arrived', countKey: 'arrivedLength' },
|
||||
{ status: 'picked', countKey: 'pickedLength' },
|
||||
{ status: 'active', countKey: 'activeLength' },
|
||||
{ status: 'skipped', countKey: 'skippedLength' },
|
||||
{ status: 'delivered', countKey: 'coveredLength' },
|
||||
{ status: 'cancelled', countKey: 'cancelLength' }
|
||||
{ status: 'all', apiStatus: 'all', countKey: 'all' },
|
||||
{ status: 'pending_pickup', apiStatus: 'Pending_Pickup', countKey: 'pending_pickup' },
|
||||
{ status: 'miler_assigned', apiStatus: 'Miler_Assigned', countKey: 'miler_assigned' },
|
||||
{ status: 'pickup_scheduled', apiStatus: 'Pickup_Scheduled', countKey: 'pickup_scheduled' },
|
||||
{ status: 'at_customer', apiStatus: 'At_Customer', countKey: 'at_customer' },
|
||||
{ status: 'picked_up', apiStatus: 'Picked_Up', countKey: 'picked_up' },
|
||||
{ status: 'at_hub', apiStatus: 'At_Hub', countKey: 'at_hub' },
|
||||
{ status: 'delivered', apiStatus: 'Delivered', countKey: 'delivered' },
|
||||
{ status: 'cancelled', apiStatus: 'Cancelled', countKey: 'cancelled' }
|
||||
];
|
||||
|
||||
// KPI palette + icons — mirrors the four cards across the top of the page.
|
||||
@@ -322,10 +323,9 @@ const Deliveries = () => {
|
||||
const [appId, setAppId] = useState(0);
|
||||
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
const [tabstatus, setTabstatus] = useState('Pending');
|
||||
const [tabstatus, setTabstatus] = useState('All');
|
||||
const [tabvalue, setTabvalue] = useState(0);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [datestatus, setDatestatus] = useState('Today');
|
||||
const [kms, setKms] = useState('');
|
||||
const [cumulativekms, setCumulativeKms] = useState();
|
||||
const [deliveryamount, setDeliveryamount] = useState();
|
||||
@@ -333,13 +333,12 @@ const Deliveries = () => {
|
||||
const [currentorder, setCurrentorder] = useState({});
|
||||
const [deliverylat, setDeliverylat] = useState('');
|
||||
const [deliverylong, setDeliverylong] = useState('');
|
||||
const [currentStatus, setCurrentStatus] = useState('pending');
|
||||
const [currentStatus, setCurrentStatus] = useState('all');
|
||||
const [updateStatus, setUpdateStatus] = useState('delivered');
|
||||
const locationRef = useRef(null);
|
||||
const tenantRef = useRef(null);
|
||||
const [page, setPage] = React.useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(50);
|
||||
const [totalCount, setTotalCount] = React.useState();
|
||||
const [productCollapse, setProductCollapse] = useState(null);
|
||||
const [orderHeaderid, setOrderHeaderId] = useState(null);
|
||||
const [searchword, setSearchword] = useState('');
|
||||
@@ -347,8 +346,6 @@ const Deliveries = () => {
|
||||
const [menuAnchorEl, setMenuAnchorEl] = React.useState(null);
|
||||
const [selectedRow, setSelectedRow] = useState(null);
|
||||
const [loading1, setLoading1] = useState(false);
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
const [open2, setOpen2] = useState('');
|
||||
const [cancelDeliveryOpen, setCancelDeliveryOpen] = useState(false);
|
||||
const [changeDialogOpen, setChangeDialogOpen] = useState(false);
|
||||
const [cancelFeed, setCancelFeed] = useState('');
|
||||
@@ -488,7 +485,7 @@ const Deliveries = () => {
|
||||
countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips
|
||||
notifyRiderMutation.mutate(selectedRider.userfcmtoken);
|
||||
},
|
||||
onError: (err, { selectedRider, selectedRow }) => {
|
||||
onError: (err, { selectedRow }) => {
|
||||
logger.error(`Failed to change rider for order ID ${selectedRow?.orderid}:`, err);
|
||||
opentoast(err.message, 'error');
|
||||
setLoading1(false);
|
||||
@@ -521,48 +518,9 @@ const Deliveries = () => {
|
||||
setPage(0);
|
||||
setTabvalue(i);
|
||||
setRowsPerPage(50);
|
||||
if (i === 0) {
|
||||
setTabstatus('Pending');
|
||||
setCurrentStatus('pending');
|
||||
setTotalCount(countData?.uncoveredLength);
|
||||
}
|
||||
if (i === 1) {
|
||||
setTabstatus('Assigned');
|
||||
setCurrentStatus('accepted');
|
||||
setTotalCount(countData?.assignedLength);
|
||||
}
|
||||
if (i === 2) {
|
||||
setTabstatus('Arrived');
|
||||
setCurrentStatus('arrived');
|
||||
setTotalCount(countData?.arrivedLength);
|
||||
}
|
||||
if (i === 3) {
|
||||
setTabstatus('Picked');
|
||||
setCurrentStatus('picked');
|
||||
setTotalCount(countData?.pickedLength);
|
||||
}
|
||||
if (i === 4) {
|
||||
setTabstatus('Active');
|
||||
setCurrentStatus('active');
|
||||
setTotalCount(countData?.activeLength);
|
||||
}
|
||||
if (i === 5) {
|
||||
setTabstatus('Skipped');
|
||||
setCurrentStatus('skipped');
|
||||
setTotalCount(countData?.skippedLength);
|
||||
}
|
||||
if (i === 6) {
|
||||
setTabstatus('Delivered');
|
||||
setCurrentStatus('delivered');
|
||||
setTotalCount(countData?.coveredLength);
|
||||
}
|
||||
|
||||
if (i === 7) {
|
||||
setTabstatus('Cancelled');
|
||||
setCurrentStatus('cancelled');
|
||||
setTotalCount(countData?.cancelLength);
|
||||
}
|
||||
console.log(i);
|
||||
const tab = STATUS_TABS[i];
|
||||
setTabstatus(STATUS_META[tab.status]?.label || tab.status);
|
||||
setCurrentStatus(tab.apiStatus);
|
||||
setSearchword('');
|
||||
};
|
||||
|
||||
@@ -660,21 +618,10 @@ const Deliveries = () => {
|
||||
const q = String(debouncedSearch || '').trim().toLowerCase();
|
||||
return countSourceRows.filter((r) => {
|
||||
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return false;
|
||||
const s = String(r.orderstatus || '').toLowerCase();
|
||||
if (wantStatus && s !== wantStatus) return false;
|
||||
const s = String(r.status || '').toLowerCase();
|
||||
if (wantStatus && wantStatus !== 'all' && s !== wantStatus) return false;
|
||||
if (q) {
|
||||
const hay = [
|
||||
r.deliverycustomer,
|
||||
r.deliveryaddress,
|
||||
r.deliverysuburb,
|
||||
r.pickupcustomer,
|
||||
r.pickupaddress,
|
||||
r.pickupsuburb,
|
||||
r.orderid,
|
||||
r.tenantname,
|
||||
r.ridername,
|
||||
r.username
|
||||
]
|
||||
const hay = [r.bookingreference, r.bookingid, r.pickupaddress, r.deliveryaddress, r.assignedmileruserid]
|
||||
.map((v) => String(v || '').toLowerCase())
|
||||
.join(' ');
|
||||
if (!hay.includes(q)) return false;
|
||||
@@ -687,49 +634,12 @@ const Deliveries = () => {
|
||||
// *Length keys returned by fetchCountAPI so the JSX swap-in is mechanical
|
||||
// (countData?.uncoveredLength → batchCounts.uncoveredLength).
|
||||
const batchCounts = useMemo(() => {
|
||||
const c = {
|
||||
uncoveredLength: 0,
|
||||
assignedLength: 0,
|
||||
arrivedLength: 0,
|
||||
pickedLength: 0,
|
||||
activeLength: 0,
|
||||
skippedLength: 0,
|
||||
coveredLength: 0,
|
||||
cancelLength: 0
|
||||
};
|
||||
const c = {};
|
||||
countSourceRows.forEach((r) => {
|
||||
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return;
|
||||
const s = String(r.orderstatus || '').toLowerCase();
|
||||
switch (s) {
|
||||
case 'pending':
|
||||
c.uncoveredLength += 1;
|
||||
break;
|
||||
case 'accepted':
|
||||
case 'assigned':
|
||||
c.assignedLength += 1;
|
||||
break;
|
||||
case 'arrived':
|
||||
c.arrivedLength += 1;
|
||||
break;
|
||||
case 'picked':
|
||||
c.pickedLength += 1;
|
||||
break;
|
||||
case 'active':
|
||||
c.activeLength += 1;
|
||||
break;
|
||||
case 'skipped':
|
||||
c.skippedLength += 1;
|
||||
break;
|
||||
case 'delivered':
|
||||
c.coveredLength += 1;
|
||||
break;
|
||||
case 'cancelled':
|
||||
case 'canceled':
|
||||
c.cancelLength += 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const s = String(r.status || '').toLowerCase();
|
||||
c[s] = (c[s] || 0) + 1;
|
||||
c.all = (c.all || 0) + 1;
|
||||
});
|
||||
return c;
|
||||
}, [countSourceRows, selectedBatch]);
|
||||
@@ -807,13 +717,6 @@ const Deliveries = () => {
|
||||
queryKey: ['fetchCountData', appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid, tabstatus],
|
||||
queryFn: () => fetchCountAPI(appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid)
|
||||
});
|
||||
useEffect(() => {
|
||||
console.log('countData', countData);
|
||||
if (tabvalue === 0 && countData) {
|
||||
setTotalCount(countData.uncoveredLength);
|
||||
}
|
||||
}, [countData]);
|
||||
|
||||
// ==============================|| fetchRidersList ||============================== //
|
||||
|
||||
const {
|
||||
@@ -912,7 +815,7 @@ const Deliveries = () => {
|
||||
setLocoName={setLocoName}
|
||||
setPage={setPage}
|
||||
pill
|
||||
accentColor="#662582"
|
||||
accentColor="#C01227"
|
||||
icon={<MdMyLocation size={14} />}
|
||||
placeholder="Select Zone"
|
||||
paperComponent={SoftPaper}
|
||||
@@ -962,7 +865,7 @@ const Deliveries = () => {
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: { xs: 1, md: 1.5 }, color: DT.textSecondary }}>
|
||||
<Avatar sx={{ width: 28, height: 28, bgcolor: soft('#662582'), color: '#662582' }}>
|
||||
<Avatar sx={{ width: 28, height: 28, bgcolor: soft('#C01227'), color: '#C01227' }}>
|
||||
<MdTune size={16} />
|
||||
</Avatar>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.6, textTransform: 'uppercase', color: DT.textSecondary }}>
|
||||
@@ -1125,8 +1028,8 @@ const Deliveries = () => {
|
||||
transition: 'border-color 0.15s, box-shadow 0.15s',
|
||||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||||
'&:hover fieldset': { borderColor: '#cbd5e1' },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` },
|
||||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
|
||||
},
|
||||
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: '#94a3b8' }
|
||||
}}
|
||||
@@ -1442,10 +1345,10 @@ const Deliveries = () => {
|
||||
overflowX: 'auto',
|
||||
'&::-webkit-scrollbar': { width: '10px', height: '10px', cursor: 'pointer' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: edge('#662582'),
|
||||
backgroundColor: edge('#C01227'),
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
'&:hover': { backgroundColor: '#662582' }
|
||||
'&:hover': { backgroundColor: '#C01227' }
|
||||
},
|
||||
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
|
||||
}}
|
||||
@@ -1453,7 +1356,7 @@ const Deliveries = () => {
|
||||
{(() => {
|
||||
const showAction = tabstatus !== 'Cancelled' && tabstatus !== 'Delivered';
|
||||
const showSelect = tabstatus == 'Created';
|
||||
const totalCols = 15 + (showAction ? 1 : 0) + (showSelect ? 1 : 0);
|
||||
const totalCols = 7 + (showAction ? 1 : 0) + (showSelect ? 1 : 0);
|
||||
return isMobile ? (
|
||||
/* ===================== MOBILE: card list ===================== */
|
||||
<MobileCardList sx={{ p: 1.25 }}>
|
||||
@@ -1481,15 +1384,14 @@ const Deliveries = () => {
|
||||
</Stack>
|
||||
)}
|
||||
{filteredRows.map((row, index) => {
|
||||
const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || {
|
||||
label: row.orderstatus || '—',
|
||||
const rowStatusMeta = STATUS_META[String(row.status || '').toLowerCase()] || {
|
||||
label: row.status || '—',
|
||||
color: '#94a3b8',
|
||||
icon: MdHistoryToggleOff
|
||||
};
|
||||
const RowStatusIcon = rowStatusMeta.icon;
|
||||
const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
|
||||
const isOpen = productCollapse?.orderid === row?.orderid;
|
||||
const chipSx = (c) => ({ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: c, fontWeight: 700, fontSize: 12, whiteSpace: 'nowrap' });
|
||||
return (
|
||||
<MobileCard
|
||||
key={row.orderheaderid ?? `${row.tenantname}-${index}`}
|
||||
@@ -1562,7 +1464,7 @@ const Deliveries = () => {
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => handleMenuOpen(e, row)}
|
||||
sx={{ borderRadius: 999, bgcolor: tint('#662582'), color: '#662582', border: `1px solid ${edge('#662582')}`, '&:hover': { bgcolor: soft('#662582') } }}
|
||||
sx={{ borderRadius: 999, bgcolor: tint('#C01227'), color: '#C01227', border: `1px solid ${edge('#C01227')}`, '&:hover': { bgcolor: soft('#C01227') } }}
|
||||
>
|
||||
<EditOutlined />
|
||||
</IconButton>
|
||||
@@ -1571,54 +1473,35 @@ const Deliveries = () => {
|
||||
</Stack>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 15 }} noWrap>
|
||||
{row.tenantname}
|
||||
{row.bookingreference || `#${row.bookingid}`}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
||||
{[row.tenantsuburb, row.applocation].filter(Boolean).join(' · ') || '—'}
|
||||
{row.createdat ? dayjs(row.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<MobileFieldGrid>
|
||||
<MobileField label="Order / Location" full>
|
||||
<MobileField label="Pickup" full>
|
||||
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
|
||||
{`${row.locationname}-(${row.locationsuburb})`}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{row.orderid} · {row.deliveryid}
|
||||
{row.pickupaddress || '—'}
|
||||
</Typography>
|
||||
</MobileField>
|
||||
<MobileField label="Pickup">
|
||||
<MobileField label="Delivery" full>
|
||||
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
|
||||
{row.pickupcustomer || '—'}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
||||
{row.pickupcontactno}
|
||||
{row.deliveryaddress || '—'}
|
||||
</Typography>
|
||||
</MobileField>
|
||||
<MobileField label="Drop">
|
||||
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
|
||||
{row.deliverycustomer || '—'}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
||||
{row.deliverycontactno}
|
||||
</Typography>
|
||||
</MobileField>
|
||||
<MobileField label="Rider" full>
|
||||
{row.ridername ? (
|
||||
<MobileField label="Miler" full>
|
||||
{row.assignedmileruserid ? (
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<AccentAvatar color="#8b5cf6" size={24}>
|
||||
<MdDirectionsBike size={13} />
|
||||
</AccentAvatar>
|
||||
<Stack sx={{ minWidth: 0 }}>
|
||||
<Typography sx={{ fontSize: 13, fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||
{row.ridername}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
||||
ID #{row.userid} · {row.ridercontact || '—'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography sx={{ fontSize: 13, fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||
#{row.assignedmileruserid}
|
||||
</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
|
||||
@@ -1626,46 +1509,12 @@ const Deliveries = () => {
|
||||
</Typography>
|
||||
)}
|
||||
</MobileField>
|
||||
<MobileField label="ETA" value={row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : '—'} />
|
||||
<MobileField label="Transit">
|
||||
<Box sx={chipSx('#06b6d4')}>{row.transitminutes || 0}m</Box>
|
||||
</MobileField>
|
||||
<MobileField label="Kms · plan / act">
|
||||
<Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap>
|
||||
<Box sx={chipSx('#ef4444')}>{row.kms || 0} km</Box>
|
||||
<Box sx={chipSx('#10b981')}>{row.cumulativekms || 0} km</Box>
|
||||
</Stack>
|
||||
</MobileField>
|
||||
<MobileField label="Amount · chg / amt">
|
||||
<Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap>
|
||||
<Box sx={chipSx('#ef4444')}>₹ {row.deliverycharges?.toFixed(2) ?? '0.00'}</Box>
|
||||
<Box sx={chipSx('#10b981')}>₹ {row.deliveryamt?.toFixed(2) ?? '0.00'}</Box>
|
||||
</Stack>
|
||||
</MobileField>
|
||||
<MobileField label="Qty" value={row.Quantity || '—'} />
|
||||
<MobileField label="COD">
|
||||
<Typography sx={{ fontSize: 13, fontWeight: 800, color: row.collectionamt ? '#ef4444' : DT.textMuted }}>
|
||||
{row.collectionamt ? `₹ ${row.collectionamt.toFixed(2)}` : '—'}
|
||||
</Typography>
|
||||
</MobileField>
|
||||
<MobileField label="Step">
|
||||
{row.step ? (
|
||||
<Box sx={{ ...chipSx('#662582'), minWidth: 30, fontWeight: 800 }}>{row.step}</Box>
|
||||
) : (
|
||||
<Typography variant="caption" sx={{ color: DT.textMuted }}>—</Typography>
|
||||
)}
|
||||
</MobileField>
|
||||
{row.notes && (
|
||||
<MobileField label="Notes" full>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>{row.notes}</Typography>
|
||||
</MobileField>
|
||||
)}
|
||||
</MobileFieldGrid>
|
||||
{isOpen && (
|
||||
<Box sx={{ mt: 1.5, p: 1.25, borderRadius: 2, bgcolor: DT.surfaceAlt, border: `1px solid ${DT.divider}` }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
|
||||
<AccentAvatar color="#662582" size={22}><MdInventory2 size={12} /></AccentAvatar>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#662582' }}>
|
||||
<AccentAvatar color="#C01227" size={22}><MdInventory2 size={12} /></AccentAvatar>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
|
||||
Product Details
|
||||
</Typography>
|
||||
</Stack>
|
||||
@@ -1746,19 +1595,11 @@ const Deliveries = () => {
|
||||
)}
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Tenant</TableCell>
|
||||
<TableCell>Order / Location</TableCell>
|
||||
<TableCell>Booking</TableCell>
|
||||
<TableCell>Pickup</TableCell>
|
||||
<TableCell>Drop</TableCell>
|
||||
<TableCell>Rider</TableCell>
|
||||
<TableCell>ETA</TableCell>
|
||||
<TableCell>Transit</TableCell>
|
||||
<TableCell>Kms</TableCell>
|
||||
<TableCell>Amount</TableCell>
|
||||
<TableCell>Notes</TableCell>
|
||||
<TableCell>Step</TableCell>
|
||||
<TableCell>Qty</TableCell>
|
||||
<TableCell>COD</TableCell>
|
||||
<TableCell>Delivery</TableCell>
|
||||
<TableCell>Miler</TableCell>
|
||||
<TableCell>Created</TableCell>
|
||||
{showAction && <TableCell align="right">Action</TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -1794,8 +1635,8 @@ const Deliveries = () => {
|
||||
</TableRow>
|
||||
)}
|
||||
{filteredRows.map((row, index) => {
|
||||
const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || {
|
||||
label: row.orderstatus || '—',
|
||||
const rowStatusMeta = STATUS_META[String(row.status || '').toLowerCase()] || {
|
||||
label: row.status || '—',
|
||||
color: '#94a3b8',
|
||||
icon: MdHistoryToggleOff
|
||||
};
|
||||
@@ -1876,108 +1717,38 @@ const Deliveries = () => {
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
{/* Tenants */}
|
||||
{/* Booking */}
|
||||
<TableCell>
|
||||
<Tooltip title={row.tenantadress}>
|
||||
<Stack>
|
||||
<Typography noWrap variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||||
{row.tenantname}
|
||||
</Typography>
|
||||
<Typography noWrap variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{row.tenantsuburb}
|
||||
</Typography>
|
||||
<Typography noWrap variant="caption" sx={{ color: DT.textMuted }}>
|
||||
{row.applocation}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||
{row.bookingreference || `#${row.bookingid}`}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
{/* Pickup */}
|
||||
<TableCell sx={{ maxWidth: 220 }}>
|
||||
<Tooltip title={row.pickupaddress || ''} placement="top">
|
||||
<Typography variant="body2" noWrap>
|
||||
{row.pickupaddress || '—'}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
{/* order details */}
|
||||
<TableCell align="left">
|
||||
<Tooltip title="Location Name-Suburb" placement="top">
|
||||
<Typography variant="subtitle1" noWrap>
|
||||
{`${row.locationname}-(${row.locationsuburb})`}
|
||||
{/* Delivery */}
|
||||
<TableCell sx={{ maxWidth: 220 }}>
|
||||
<Tooltip title={row.deliveryaddress || ''} placement="top">
|
||||
<Typography variant="body2" noWrap>
|
||||
{row.deliveryaddress || '—'}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
|
||||
<Stack display={'flex'} flexDirection={'row'} gap={3}>
|
||||
<Stack>
|
||||
<Tooltip title="Order Id" placement="top">
|
||||
<Typography variant="body2" noWrap>
|
||||
{row.orderid}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<Tooltip title="Ordered date" placement="top">
|
||||
<Typography noWrap sx={{ fontSize: '12px' }}>
|
||||
{dayjs(row.orderdate).utc().format('DD/MM/YYYY')}
|
||||
</Typography>
|
||||
<Typography noWrap sx={{ fontSize: '11px' }}>
|
||||
{dayjs(row.orderdate).utc().format('hh:mm A')}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
-
|
||||
<Stack>
|
||||
<Tooltip title="Delivery Id" placement="top">
|
||||
<Typography variant="body2" noWrap>
|
||||
{row.deliveryid}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delivery date" placement="top">
|
||||
<Typography noWrap sx={{ fontSize: '12px' }}>
|
||||
{dayjs(row.deliverydate).utc().format('DD/MM/YYYY')}
|
||||
</Typography>
|
||||
|
||||
<Typography noWrap sx={{ fontSize: '11px' }}>
|
||||
{dayjs(row.deliverydate).utc().format('hh:mm A')}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
{/* pickup */}
|
||||
<TableCell align="left">
|
||||
<Stack>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
|
||||
{row.pickupcustomer}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>{row.pickupcontactno}</Typography>
|
||||
<Tooltip title={row.Pickupaddress} sx={{ whiteSpace: 'nowrap' }}>
|
||||
<Typography variant="caption" sx={{ color: DT.textMuted }}>
|
||||
{row.pickuplocation || (row.Pickupaddress ? row.Pickupaddress.slice(0, 14) + '…' : '—')}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
{/* drop */}
|
||||
<TableCell align="left">
|
||||
<Stack>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
|
||||
{row.deliverycustomer}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>{row.deliverycontactno}</Typography>
|
||||
<Tooltip title={row.deliveryaddress}>
|
||||
<Typography variant="caption" sx={{ color: DT.textMuted, whiteSpace: 'nowrap' }}>
|
||||
{row.deliverylocation || (row.deliveryaddress ? row.deliveryaddress.slice(0, 14) + '…' : '—')}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
{/* rider */}
|
||||
{/* Miler */}
|
||||
<TableCell>
|
||||
{row.ridername ? (
|
||||
{row.assignedmileruserid ? (
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<AccentAvatar color="#8b5cf6" size={28}>
|
||||
<MdDirectionsBike size={14} />
|
||||
</AccentAvatar>
|
||||
<Stack>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||
{row.ridername}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
ID #{row.userid} · {row.ridercontact || '—'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||
#{row.assignedmileruserid}
|
||||
</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
|
||||
@@ -1985,116 +1756,10 @@ const Deliveries = () => {
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
{/* Estimated Delivery Time */}
|
||||
<TableCell align="left">
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
|
||||
{row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : '—'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
{/* Transit Minutes */}
|
||||
<TableCell align="left">
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
px: 1,
|
||||
py: 0.5,
|
||||
borderRadius: 999,
|
||||
bgcolor: tint('#06b6d4'),
|
||||
color: '#06b6d4',
|
||||
fontWeight: 700,
|
||||
fontSize: 12,
|
||||
border: `1px solid ${edge('#06b6d4')}`
|
||||
}}
|
||||
>
|
||||
{row.transitminutes || 0}m
|
||||
</Box>
|
||||
</TableCell>
|
||||
{/* kms */}
|
||||
{/* Created */}
|
||||
<TableCell>
|
||||
<Stack direction="column" spacing={0.5} alignItems="flex-start">
|
||||
<Tooltip title="Planned KMS" placement="top">
|
||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#ef4444'), color: '#ef4444', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#ef4444')}`, whiteSpace: 'nowrap', minWidth: 75 }}>
|
||||
{row.kms || 0} km
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Tooltip title="Actual KMS" placement="top">
|
||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#10b981'), color: '#10b981', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#10b981')}`, whiteSpace: 'nowrap', minWidth: 75 }}>
|
||||
{row.cumulativekms || 0} km
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
{/* amount */}
|
||||
<TableCell align="left">
|
||||
<Stack direction="column" spacing={0.5} alignItems="flex-start">
|
||||
<Tooltip title="Delivery Charge" placement="top">
|
||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#ef4444'), color: '#ef4444', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#ef4444')}`, whiteSpace: 'nowrap', minWidth: 85 }}>
|
||||
₹ {row.deliverycharges?.toFixed(2) ?? '0.00'}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delivery Amount" placement="top">
|
||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#10b981'), color: '#10b981', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#10b981')}`, whiteSpace: 'nowrap', minWidth: 85 }}>
|
||||
₹ {row.deliveryamt?.toFixed(2) ?? '0.00'}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
{/* notes */}
|
||||
<TableCell>
|
||||
{row.notes ? (
|
||||
<Tooltip title={row.notes}>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary, maxWidth: 160, display: 'inline-block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{row.notes}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Typography variant="caption" sx={{ color: DT.textMuted }}>—</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
{/* step */}
|
||||
<TableCell align="center">
|
||||
{row.step ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minWidth: 30,
|
||||
height: 24,
|
||||
px: 0.875,
|
||||
borderRadius: 999,
|
||||
bgcolor: tint('#662582'),
|
||||
border: `1px solid ${edge('#662582')}`,
|
||||
color: '#662582',
|
||||
fontWeight: 800,
|
||||
fontSize: 11
|
||||
}}
|
||||
>
|
||||
{row.step}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="caption" sx={{ color: DT.textMuted }}>—</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
{/* qty */}
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: row.Quantity ? DT.textPrimary : DT.textMuted, whiteSpace: 'nowrap' }}>
|
||||
{row.Quantity || '—'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
{/* COD */}
|
||||
<TableCell>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
color: row.collectionamt ? '#ef4444' : DT.textMuted,
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{row.collectionamt ? `₹ ${row.collectionamt.toFixed(2)}` : '—'}
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
||||
{row.createdat ? dayjs(row.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
{/* Action */}
|
||||
@@ -2133,10 +1798,10 @@ const Deliveries = () => {
|
||||
onClick={(e) => handleMenuOpen(e, row)}
|
||||
sx={{
|
||||
borderRadius: 999,
|
||||
bgcolor: tint('#662582'),
|
||||
color: '#662582',
|
||||
border: `1px solid ${edge('#662582')}`,
|
||||
'&:hover': { bgcolor: soft('#662582') }
|
||||
bgcolor: tint('#C01227'),
|
||||
color: '#C01227',
|
||||
border: `1px solid ${edge('#C01227')}`,
|
||||
'&:hover': { bgcolor: soft('#C01227') }
|
||||
}}
|
||||
>
|
||||
<EditOutlined />
|
||||
@@ -2168,9 +1833,9 @@ const Deliveries = () => {
|
||||
background: '#fff'
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ px: 2, py: 1.25, borderBottom: `1px solid ${DT.divider}`, bgcolor: tint('#662582') }}>
|
||||
<AccentAvatar color="#662582"><MdInventory2 size={14} /></AccentAvatar>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#662582' }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ px: 2, py: 1.25, borderBottom: `1px solid ${DT.divider}`, bgcolor: tint('#C01227') }}>
|
||||
<AccentAvatar color="#C01227"><MdInventory2 size={14} /></AccentAvatar>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
|
||||
Product Details
|
||||
</Typography>
|
||||
</Stack>
|
||||
@@ -2298,7 +1963,7 @@ const Deliveries = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedRow?.orderstatus !== 'delivered' && (
|
||||
{String(selectedRow?.status || '').toLowerCase() !== 'delivered' && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
notifyRiderMutation.mutate(selectedRow.userfcmtoken);
|
||||
@@ -2309,7 +1974,9 @@ const Deliveries = () => {
|
||||
Notify Rider
|
||||
</MenuItem>
|
||||
)}
|
||||
{['pending', 'accepted', 'arrived'].includes(selectedRow?.orderstatus) && (
|
||||
{['pending_pickup', 'miler_assigned', 'pickup_scheduled', 'at_customer'].includes(
|
||||
String(selectedRow?.status || '').toLowerCase()
|
||||
) && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (!appId) {
|
||||
@@ -2334,7 +2001,7 @@ const Deliveries = () => {
|
||||
setDeliverylong(selectedRow.droplon);
|
||||
setNotes(selectedRow.notes);
|
||||
setDeliveryamount(selectedRow.deliveryamount);
|
||||
setUpdateStatus(selectedRow.orderstatus || 'delivered');
|
||||
setUpdateStatus(selectedRow.status || 'delivered');
|
||||
setCurrentorder(selectedRow);
|
||||
setDialogopen(true);
|
||||
handleMenuClose();
|
||||
@@ -2344,7 +2011,7 @@ const Deliveries = () => {
|
||||
Update Status
|
||||
</MenuItem>
|
||||
)}
|
||||
{selectedRow?.orderstatus !== 'cancelled' && selectedRow?.orderstatus !== 'delivered' && (
|
||||
{!['cancelled', 'delivered'].includes(String(selectedRow?.status || '').toLowerCase()) && (
|
||||
<MenuItem
|
||||
sx={{ color: '#ef4444 !important' }}
|
||||
onClick={() => {
|
||||
@@ -2597,11 +2264,6 @@ const Deliveries = () => {
|
||||
} else {
|
||||
setStartdate(dayjs(range.startDate).format('YYYY-MM-DD'));
|
||||
setEnddate(dayjs(range.endDate).format('YYYY-MM-DD'));
|
||||
if (range.label) {
|
||||
setDatestatus(range.label);
|
||||
} else {
|
||||
setDatestatus('');
|
||||
}
|
||||
}
|
||||
console.log(range);
|
||||
}}
|
||||
@@ -2646,11 +2308,6 @@ const Deliveries = () => {
|
||||
startDate: startOfMonth(addMonths(new Date(), -1)),
|
||||
endDate: endOfMonth(addMonths(new Date(), -1))
|
||||
}
|
||||
// {
|
||||
// label: 'All',
|
||||
// startDate: new Date(),
|
||||
// endDate: addDays(new Date(), -1),
|
||||
// },
|
||||
]}
|
||||
/>
|
||||
</DialogContent>
|
||||
@@ -2778,14 +2435,14 @@ const Deliveries = () => {
|
||||
borderRadius: 2,
|
||||
bgcolor: '#fff',
|
||||
'& fieldset': { borderColor: DT.borderSubtle },
|
||||
'&:hover fieldset': { borderColor: '#662582' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 2 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` }
|
||||
'&:hover fieldset': { borderColor: '#C01227' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
|
||||
}
|
||||
}}
|
||||
>
|
||||
{['pending','accepted','started','arrived','delivered','cancelled'].map((s) => {
|
||||
const m = STATUS_META[s] || { label: s, color: '#662582', icon: MdHistoryToggleOff };
|
||||
const m = STATUS_META[s] || { label: s, color: '#C01227', icon: MdHistoryToggleOff };
|
||||
const Ic = m.icon;
|
||||
return (
|
||||
<MenuItem key={s} value={s} sx={{ gap: 1 }}>
|
||||
@@ -2812,9 +2469,9 @@ const Deliveries = () => {
|
||||
borderRadius: 2,
|
||||
bgcolor: '#fff',
|
||||
'& fieldset': { borderColor: DT.borderSubtle },
|
||||
'&:hover fieldset': { borderColor: '#662582' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 2 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#662582')}` }
|
||||
'&:hover fieldset': { borderColor: '#C01227' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 2 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring('#C01227')}` }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -96,7 +96,7 @@ const ActiveSection = ({
|
||||
);
|
||||
}
|
||||
|
||||
const renderActiveDeliveryCard = (o, i) => {
|
||||
const renderActiveDeliveryCard = (o) => {
|
||||
const rid = o.rider_id || o.userid;
|
||||
const rider = riders.find((r) => String(r.id) === String(rid));
|
||||
const color = getRiderColor(rid);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { MapContainer, TileLayer, Marker, Popup, Polyline, Tooltip, useMap, useMapEvents, ZoomControl } from 'react-leaflet';
|
||||
import { MapContainer, TileLayer, Marker, Popup, Polyline, Tooltip, useMap, ZoomControl } from 'react-leaflet';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
// Side-effect import: patches L.Polyline so pathOptions.offset (in screen px)
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
MdStraighten,
|
||||
MdLocationOn,
|
||||
MdMarkunreadMailbox,
|
||||
MdMoveToInbox,
|
||||
MdPlace,
|
||||
MdTwoWheeler,
|
||||
MdNotes,
|
||||
@@ -59,7 +58,6 @@ import ProfitabilitySection from './ProfitabilitySection';
|
||||
import ActiveSection from './ActiveSection';
|
||||
import { fetchDeliveries, fetchAppLocations, getRiderPeriodicLogs, fetchRidersLogs, fetchBatchEfficiency } from '../../api/api';
|
||||
import {
|
||||
STATUS_STYLES,
|
||||
getStatusStyle,
|
||||
FINAL_STATUSES,
|
||||
SKIPPED_STATUSES,
|
||||
@@ -80,7 +78,7 @@ import logger from '../../../utils/logger';
|
||||
// emerald for the actual GPS trail (signals "live / real" data). Per-step
|
||||
// distinction in Combined view is carried by the numbered drop pins, which
|
||||
// keep STEP_PALETTE so the timeline link to a specific delivery survives.
|
||||
const COMBINED_PLANNED_COLOR = '#662582';
|
||||
const COMBINED_PLANNED_COLOR = '#C01227';
|
||||
const COMBINED_ACTUAL_COLOR = '#10b981';
|
||||
|
||||
const toNum = (v) => {
|
||||
@@ -232,15 +230,6 @@ const getRowBatch = (r, fieldId = 'all', batches = BATCHES_DEFAULT) => {
|
||||
return getBatchForHour(d.hour() + d.minute() / 60, batches);
|
||||
};
|
||||
|
||||
// Sits inside the Compare MapContainer and unpins any pinned popup whenever
|
||||
// the operator clicks empty map space. Markers' click events do NOT bubble
|
||||
// to the map, so this only fires on background clicks (which is what we
|
||||
// want — clicking elsewhere should release the pin).
|
||||
function CompareMapClickUnpin({ onUnpin }) {
|
||||
useMapEvents({ click: () => onUnpin() });
|
||||
return null;
|
||||
}
|
||||
|
||||
// Captures the Leaflet map instance for the parent component via a ref. Kept
|
||||
// available even after the two-map Compare layout was unified into one map,
|
||||
// since future per-step imperative zoom logic still needs a handle on the
|
||||
@@ -983,12 +972,6 @@ const ANALYSIS_BATCH_WINDOWS = [
|
||||
|
||||
// Tolerant field-name lookup so the Analysis card still renders cleanly even
|
||||
// if the API response uses slightly different keys than expected.
|
||||
const analysisPick = (obj, keys) => {
|
||||
for (const k of keys) {
|
||||
if (obj && obj[k] != null && obj[k] !== '') return obj[k];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const analysisFormatNum = (v) => {
|
||||
if (v == null) return '—';
|
||||
if (typeof v === 'number') return v.toLocaleString('en-IN');
|
||||
@@ -996,14 +979,6 @@ const analysisFormatNum = (v) => {
|
||||
if (Number.isFinite(n)) return n.toLocaleString('en-IN');
|
||||
return String(v);
|
||||
};
|
||||
const analysisFormatKm = (v) => (v == null ? '—' : `${parseFloat(v).toFixed(1)} km`);
|
||||
const analysisFormatRupees = (v) => (v == null ? '—' : `₹${parseFloat(v).toFixed(0)}`);
|
||||
const analysisFormatPct = (v) => {
|
||||
if (v == null) return '—';
|
||||
const n = parseFloat(v);
|
||||
if (!Number.isFinite(n)) return '—';
|
||||
return `${n > 1 ? n.toFixed(1) : (n * 100).toFixed(1)}%`;
|
||||
};
|
||||
// Parse "HH:mm:ss" or "HH:mm" → seconds since midnight. Returns null when the
|
||||
// string is missing or malformed. Used to compute gantt percentages for the
|
||||
// rider timelines on the Analysis page — the API ships those fields as bare
|
||||
@@ -1104,7 +1079,7 @@ const Dispatch = ({
|
||||
// • no batch is selected,
|
||||
// • the previous request is still in flight (prevents queue
|
||||
// stacking on slow networks),
|
||||
// • the browser tab is hidden (saves API quota on routes.workolik).
|
||||
// • the browser tab is hidden (saves API quota on the Doormile AI layer).
|
||||
// Loading state is tracked through a ref so the interval doesn't
|
||||
// reset on every in-flight flip. ─────────────────────────────────
|
||||
const ANALYSIS_POLL_MS = 15000;
|
||||
@@ -1163,7 +1138,6 @@ const Dispatch = ({
|
||||
// Short-lived close timer for the general map order/marker popups.
|
||||
// Gives the cursor a ~200ms window to travel from the marker onto the popup
|
||||
// or vice versa without immediately triggering a close.
|
||||
const activePopupMarkerRef = useRef(null);
|
||||
const popupHoverTimerRef = useRef(null);
|
||||
// Order shown in the centered popup overlay. Rendered outside the leaflet
|
||||
// map (see `dispatch-popup-center` overlay near the bottom of the JSX) so
|
||||
@@ -2150,19 +2124,14 @@ const Dispatch = ({
|
||||
}
|
||||
// "All Active Routes": the header must reflect exactly what the list/map
|
||||
// shows — the in-progress orders and the riders working them — NOT the whole
|
||||
// day's totals. We count only active deliveries (and `visibleRiders`, which
|
||||
// is already gated to active-order riders) so the tiles can't disagree with
|
||||
// the list/map below.
|
||||
// day's totals. Source from `activeViewRiders`, the SAME sticky-cached list
|
||||
// that <ActiveSection> renders as cards (see `visibleRiders={activeViewRiders}`
|
||||
// below) — not `visibleRiders`, which additionally requires a rider to be
|
||||
// present in the independently-polled GPS-log feed with status 'active'/
|
||||
// 'pending'. That extra join rarely resolves, which is why this tile used to
|
||||
// show 0 riders even while real rider cards were on screen.
|
||||
if (isAllActiveView) {
|
||||
// One order per rider, via getActiveOrder — the same "rider's single
|
||||
// in-progress delivery" resolver used elsewhere on this page (see
|
||||
// dispatchShared.js). A multi-drop batch can carry `orderstatus:
|
||||
// 'active'` on more than one of a rider's stops at once (the current
|
||||
// leg plus a queued one), so filtering allViewOrders by isActiveDelivery
|
||||
// directly counted every such row and let Orders outnumber the distinct
|
||||
// riders backing them (e.g. 5 orders / 3 riders). Resolving exactly one
|
||||
// active order per activeViewRiders entry keeps the two tiles 1:1.
|
||||
const activeOrders = activeViewRiders.map((r) => getActiveOrder(r.orders)).filter(Boolean);
|
||||
const activeOrders = activeViewRiders.flatMap((r) => r.orders);
|
||||
return {
|
||||
orders: activeOrders.length,
|
||||
riders: activeViewRiders.length,
|
||||
@@ -3155,7 +3124,7 @@ const Dispatch = ({
|
||||
? new Map(riderActualTracks.map((t) => [String(t.deliveryid), t.sequenceStep]))
|
||||
: null;
|
||||
|
||||
return ordersToRender.map((o, idx) => {
|
||||
return ordersToRender.map((o) => {
|
||||
const rid = o.rider_id;
|
||||
const active = rid ? activeRiders.has(rid) : true;
|
||||
let color = getRiderColor(rid);
|
||||
@@ -3261,9 +3230,13 @@ const Dispatch = ({
|
||||
const routes = [];
|
||||
const zoneRiderIds = focusedZone ? new Set(focusedZone.riders.map((zr) => String(zr.rider_id))) : null;
|
||||
if (hidePlanned) return routes;
|
||||
// visibleRiders === riders in every view except "All Active Routes", where
|
||||
// it's pre-filtered to riders whose live GPS is currently active.
|
||||
visibleRiders.forEach(r => {
|
||||
// In "All Active Routes" draw for exactly the riders shown as cards
|
||||
// (activeViewRiders) — not `visibleRiders`, which additionally requires a
|
||||
// rider to be present in the independently-polled GPS-log feed with status
|
||||
// 'active'/'pending'. That join rarely resolves, which left the map with
|
||||
// no polyline for a rider whose active-order marker/card was clearly
|
||||
// visible and clickable. Every other view keeps using `riders` (unfiltered).
|
||||
(isAllActiveView ? activeViewRiders : riders).forEach(r => {
|
||||
const isActive = activeRiders.has(r.id);
|
||||
if (focusedRider && focusedRider.id !== r.id) return;
|
||||
if (focusedKitchen && !focusedKitchen.riders.has(r.id)) return;
|
||||
@@ -3478,13 +3451,6 @@ const Dispatch = ({
|
||||
return routes;
|
||||
};
|
||||
|
||||
const toggleRider = (rid) => {
|
||||
const newActive = new Set(activeRiders);
|
||||
if (newActive.has(rid)) newActive.delete(rid);
|
||||
else newActive.add(rid);
|
||||
setActiveRiders(newActive);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`dispatch-container${embedded ? ' embedded' : ''}${compareOpen ? ' compare-open' : ''}`}>
|
||||
{!embedded && (
|
||||
@@ -5374,15 +5340,6 @@ const Dispatch = ({
|
||||
(o) => o.deliveryid != null && String(o.deliveryid) === String(t.deliveryid)
|
||||
);
|
||||
|
||||
const statusStyle = getStatusStyle(t.orderstatus);
|
||||
const flagSvg = t.orderstatus
|
||||
? `<svg class="cmark-flag" viewBox="0 0 18 22" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="1.5" y1="0" x2="1.5" y2="22" stroke="#0f172a" stroke-width="1.6" stroke-linecap="round"/>
|
||||
<polygon points="2,1 17,1 13.5,5.5 17,10 2,10" fill="${statusStyle.bg}" stroke="#0f172a" stroke-width="0.6" stroke-linejoin="round"/>
|
||||
${isDelivered ? '<polyline points="5,5.5 7,7.5 11,3.5" fill="none" stroke="#fff" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>' : ''}
|
||||
</svg>`
|
||||
: '';
|
||||
|
||||
const dropClasses = ['compare-step-pin'];
|
||||
if (isFocusedStep) dropClasses.push('is-focused');
|
||||
if (isDelivered) dropClasses.push('is-delivered');
|
||||
@@ -5730,7 +5687,7 @@ const Dispatch = ({
|
||||
style={{
|
||||
boxShadow: 'var(--shadow-lg)',
|
||||
background: compareOpen
|
||||
? 'linear-gradient(135deg, #662582, #9255AB)'
|
||||
? 'linear-gradient(135deg, #C01227, #D35968)'
|
||||
: '#fff',
|
||||
marginLeft: 8,
|
||||
color: compareOpen ? '#fff' : undefined
|
||||
@@ -6329,7 +6286,7 @@ const Dispatch = ({
|
||||
<div className="da-section">
|
||||
<div className="da-hero-row">
|
||||
<div className="da-hero-card">
|
||||
<div className="da-hero-icon" style={{ background: '#6625821f', color: '#662582' }}>
|
||||
<div className="da-hero-icon" style={{ background: '#C012271f', color: '#C01227' }}>
|
||||
<MdOutlineInventory2 />
|
||||
</div>
|
||||
<div className="da-hero-value">{analysisFormatNum(fleet.total_orders)}</div>
|
||||
@@ -6810,7 +6767,7 @@ const Dispatch = ({
|
||||
<div className="da-pos-modal-title-wrap">
|
||||
<div
|
||||
className="da-pos-modal-avatar"
|
||||
style={{ background: `${riderPositionModal.color || '#662582'}22`, color: riderPositionModal.color || '#662582' }}
|
||||
style={{ background: `${riderPositionModal.color || '#C01227'}22`, color: riderPositionModal.color || '#C01227' }}
|
||||
>
|
||||
<MdTwoWheeler />
|
||||
</div>
|
||||
|
||||
533
src/pages/nearle/hubs/Hubs.js
Normal file
@@ -0,0 +1,533 @@
|
||||
import { useState, Fragment } from 'react';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Collapse,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { DeploymentUnitOutlined } from '@ant-design/icons';
|
||||
import { MdAdd, MdEdit, MdKeyboardArrowDown, MdKeyboardArrowUp, MdOutlineWarehouse, MdTwoWheeler, MdStar } from 'react-icons/md';
|
||||
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
|
||||
import { OpenToast } from 'components/third-party/OpenToast';
|
||||
import { fetchHubs, createHub, updateHub } from 'pages/api/api';
|
||||
|
||||
// ============================================================================
|
||||
// DT token block — CLAUDE.md §6, copied verbatim.
|
||||
// ============================================================================
|
||||
const DT = {
|
||||
radiusPill: 999,
|
||||
radiusCard: 16,
|
||||
radiusInner: 12,
|
||||
shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
|
||||
shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
|
||||
textPrimary: '#0f172a',
|
||||
textSecondary: '#64748b',
|
||||
textMuted: '#94a3b8',
|
||||
borderSubtle: '#e2e8f0',
|
||||
divider: '#f1f5f9',
|
||||
surface: '#ffffff',
|
||||
surfaceAlt: '#f8fafc'
|
||||
};
|
||||
|
||||
const a = (c, suffix) => `${c}${suffix}`;
|
||||
const tint = (c) => a(c, '08');
|
||||
const soft = (c) => a(c, '18');
|
||||
const edge = (c) => a(c, '55');
|
||||
|
||||
const BRAND = '#C01227';
|
||||
|
||||
// Confirmed from the backend test: hubs carry `applocationid` (1–4), not a
|
||||
// literal city string. Everything else here (contact/address/pincode as
|
||||
// editable fields) is inferred from the create-body shape, not confirmed
|
||||
// present on the GET response — flagged in the summary, not guessed silently.
|
||||
const CITY_MAP = {
|
||||
1: { name: 'Coimbatore', code: 'CBE', color: '#f59e0b' },
|
||||
2: { name: 'Hyderabad', code: 'HYD', color: '#06b6d4' },
|
||||
3: { name: 'Bangalore', code: 'BLR', color: '#8b5cf6' },
|
||||
4: { name: 'Chennai', code: 'CHN', color: '#10b981' }
|
||||
};
|
||||
const CITY_NAME_TO_ID = Object.fromEntries(Object.entries(CITY_MAP).map(([id, c]) => [c.name, Number(id)]));
|
||||
|
||||
const HUB_TYPES = [
|
||||
{ value: 'sorting_center', label: 'Sorting Center' },
|
||||
{ value: 'spoke', label: 'Spoke' },
|
||||
{ value: 'pickup_point', label: 'Pickup Point' }
|
||||
];
|
||||
|
||||
const AccentAvatar = ({ color, selected, size = 24, children }) => (
|
||||
<Avatar
|
||||
sx={{
|
||||
width: size,
|
||||
height: size,
|
||||
bgcolor: selected ? color : soft(color),
|
||||
color: selected ? '#fff' : color,
|
||||
transition: 'background-color 0.15s, color 0.15s'
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Avatar>
|
||||
);
|
||||
|
||||
const EMPTY_FORM = { hubname: '', hubtype: 'sorting_center', city: 'Coimbatore', capacity: '', contact: '', address: '', pincode: '' };
|
||||
|
||||
const fetchAllMilers = () =>
|
||||
axios.get(`${process.env.REACT_APP_URL}/admin/milers`).then((r) => r.data?.data || []);
|
||||
|
||||
const Hubs = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const [selectedCity, setSelectedCity] = useState('All');
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingHub, setEditingHub] = useState(null);
|
||||
const [form, setForm] = useState(EMPTY_FORM);
|
||||
const [expandedHubId, setExpandedHubId] = useState(null);
|
||||
|
||||
const { data: hubs = [], isLoading: hubsLoading } = useQuery({
|
||||
queryKey: ['fetchHubs'],
|
||||
queryFn: fetchHubs
|
||||
});
|
||||
|
||||
const { data: milers = [] } = useQuery({
|
||||
queryKey: ['fetchAllMilers'],
|
||||
queryFn: fetchAllMilers
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: createHub,
|
||||
onSuccess: () => {
|
||||
OpenToast('Hub created', 'success', 2000);
|
||||
setDialogOpen(false);
|
||||
setForm(EMPTY_FORM);
|
||||
queryClient.invalidateQueries({ queryKey: ['fetchHubs'] });
|
||||
},
|
||||
onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, body }) => updateHub(id, body),
|
||||
onSuccess: () => {
|
||||
OpenToast('Hub updated', 'success', 2000);
|
||||
setDialogOpen(false);
|
||||
setEditingHub(null);
|
||||
setForm(EMPTY_FORM);
|
||||
queryClient.invalidateQueries({ queryKey: ['fetchHubs'] });
|
||||
},
|
||||
onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
|
||||
});
|
||||
|
||||
const filteredHubs =
|
||||
selectedCity === 'All' ? hubs : hubs.filter((h) => CITY_MAP[h.applocationid]?.name === selectedCity);
|
||||
|
||||
const kpis = [
|
||||
{ key: 'total', label: 'Total Hubs', color: BRAND, value: hubs.length },
|
||||
{ key: 'sorting', label: 'Sorting Centers', color: '#3b82f6', value: hubs.filter((h) => h.hubtype === 'sorting_center').length },
|
||||
{ key: 'spokes', label: 'Spokes', color: '#14b8a6', value: hubs.filter((h) => h.hubtype === 'spoke').length },
|
||||
{ key: 'cities', label: 'Cities Covered', color: '#10b981', value: new Set(hubs.map((h) => h.applocationid).filter(Boolean)).size }
|
||||
];
|
||||
|
||||
const openCreate = () => {
|
||||
setEditingHub(null);
|
||||
setForm(EMPTY_FORM);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
const openEdit = (hub) => {
|
||||
setEditingHub(hub);
|
||||
setForm({
|
||||
hubname: hub.hubname || '',
|
||||
hubtype: hub.hubtype || 'sorting_center',
|
||||
city: CITY_MAP[hub.applocationid]?.name || 'Coimbatore',
|
||||
capacity: hub.capacity ?? '',
|
||||
contact: hub.contact || '',
|
||||
address: hub.address || '',
|
||||
pincode: hub.pincode || ''
|
||||
});
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!form.hubname) {
|
||||
OpenToast('Enter a hub name', 'warning', 2000);
|
||||
return;
|
||||
}
|
||||
const body = {
|
||||
hubname: form.hubname,
|
||||
hubtype: form.hubtype,
|
||||
applocationid: CITY_NAME_TO_ID[form.city],
|
||||
capacity: form.capacity === '' ? undefined : Number(form.capacity),
|
||||
contact: form.contact,
|
||||
address: form.address,
|
||||
pincode: form.pincode
|
||||
};
|
||||
if (editingHub) {
|
||||
updateMutation.mutate({ id: editingHub.hubid, body });
|
||||
} else {
|
||||
createMutation.mutate(body);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ============================================= || Header || ============================================= */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: { xs: 2, md: 3 },
|
||||
borderRadius: `${DT.radiusCard}px`,
|
||||
background: 'linear-gradient(135deg, #C012270A 0%, #D359680A 100%)',
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
mb: { xs: 1.5, md: 2 }
|
||||
}}
|
||||
>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" alignItems={{ xs: 'flex-start', sm: 'center' }} spacing={2}>
|
||||
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ width: 48, height: 48, bgcolor: BRAND, color: '#fff' }}>
|
||||
<DeploymentUnitOutlined style={{ fontSize: 24 }} />
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h3">Hubs</Typography>
|
||||
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ mt: 0.25 }}>
|
||||
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: '#10b981', animation: 'pulse 1.6s infinite' }} />
|
||||
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
|
||||
Live · {selectedCity}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||
{['All', ...Object.values(CITY_MAP).map((c) => c.name)].map((c) => (
|
||||
<Chip
|
||||
key={c}
|
||||
label={c}
|
||||
onClick={() => setSelectedCity(c)}
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
bgcolor: selectedCity === c ? BRAND : '#fff',
|
||||
color: selectedCity === c ? '#fff' : DT.textSecondary,
|
||||
border: `1px solid ${selectedCity === c ? BRAND : DT.borderSubtle}`,
|
||||
'&:hover': { bgcolor: selectedCity === c ? BRAND : DT.surfaceAlt }
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* ============================================= || KPI Cards || ============================================= */}
|
||||
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
|
||||
{kpis.map((item) => (
|
||||
<Grid item key={item.key} xs={6} sm={3}>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: `${DT.radiusInner}px`,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
borderTop: `3px solid ${item.color}`,
|
||||
background: '#fff',
|
||||
boxShadow: DT.shadowSoft
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, color: DT.textMuted, textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
||||
{item.label}
|
||||
</Typography>
|
||||
<Typography variant="h3" sx={{ mt: 0.5, color: DT.textPrimary }}>
|
||||
{hubsLoading ? '—' : item.value}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* ============================================= || New Hub || ============================================= */}
|
||||
<Stack direction="row" justifyContent="flex-end" sx={{ mb: 1.5 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<MdAdd size={16} />}
|
||||
onClick={openCreate}
|
||||
sx={{ borderRadius: DT.radiusPill, textTransform: 'none', fontWeight: 700, bgcolor: BRAND, boxShadow: 'none', '&:hover': { bgcolor: '#900E1D', boxShadow: 'none' } }}
|
||||
>
|
||||
New Hub
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{/* ============================================= || Table || ============================================= */}
|
||||
<Paper elevation={0} sx={{ borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, overflow: 'hidden', background: '#fff' }}>
|
||||
<TableContainer sx={{ maxHeight: 'calc(100vh - 190px)' }}>
|
||||
<Table stickyHeader sx={{ minWidth: 900 }}>
|
||||
<TableHead>
|
||||
<TableRow
|
||||
sx={{
|
||||
'& th': {
|
||||
backgroundColor: DT.surfaceAlt,
|
||||
color: DT.textSecondary,
|
||||
fontSize: 11,
|
||||
fontWeight: 800,
|
||||
letterSpacing: 0.6,
|
||||
textTransform: 'uppercase',
|
||||
whiteSpace: 'nowrap',
|
||||
borderBottom: `1px solid ${DT.borderSubtle}`,
|
||||
py: 1.25,
|
||||
px: 2
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TableCell>Hub Name</TableCell>
|
||||
<TableCell>City</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Capacity</TableCell>
|
||||
<TableCell align="center">Milers</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{hubsLoading && <OrdersTableSkeleton col={5} />}
|
||||
{!hubsLoading && filteredHubs.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} sx={{ py: 6 }}>
|
||||
<Stack alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||||
<MdOutlineWarehouse size={28} />
|
||||
</Avatar>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||||
No hubs to show
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{filteredHubs.map((hub) => {
|
||||
const hubMilers = milers.filter((m) => m.hubid === hub.hubid);
|
||||
const expanded = expandedHubId === hub.hubid;
|
||||
const city = CITY_MAP[hub.applocationid];
|
||||
const isActive = (hub.status || 'active').toLowerCase() === 'active';
|
||||
return (
|
||||
<Fragment key={hub.hubid}>
|
||||
<TableRow sx={{ '& td': { borderBottom: `1px solid ${DT.divider}`, py: 1.5, px: 2 }, '&:hover': { backgroundColor: DT.surfaceAlt } }}>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800, color: BRAND }}>
|
||||
{hub.hubname || '—'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{city ? (
|
||||
<Chip size="small" label={city.name} sx={{ bgcolor: tint(city.color), color: city.color, border: `1px solid ${edge(city.color)}`, fontWeight: 700 }} />
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: DT.textMuted }}>
|
||||
—
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" sx={{ color: DT.textSecondary, textTransform: 'capitalize' }}>
|
||||
{(hub.hubtype || '—').replace(/_/g, ' ')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">{hub.capacity ?? '—'}</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Tooltip title="View milers at this hub">
|
||||
<Chip
|
||||
size="small"
|
||||
icon={<MdTwoWheeler size={14} />}
|
||||
label={hubMilers.length}
|
||||
onClick={() => setExpandedHubId(expanded ? null : hub.hubid)}
|
||||
sx={{ bgcolor: tint(BRAND), color: BRAND, border: `1px solid ${edge(BRAND)}`, fontWeight: 700, cursor: 'pointer' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
size="small"
|
||||
label={isActive ? 'Active' : 'Inactive'}
|
||||
sx={{
|
||||
bgcolor: tint(isActive ? '#10b981' : '#ef4444'),
|
||||
color: isActive ? '#10b981' : '#ef4444',
|
||||
border: `1px solid ${edge(isActive ? '#10b981' : '#ef4444')}`,
|
||||
fontWeight: 700
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Stack direction="row" justifyContent="flex-end" spacing={0.75}>
|
||||
<Tooltip title="Edit hub">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => openEdit(hub)}
|
||||
sx={{ bgcolor: soft(BRAND), color: BRAND, border: `1px solid ${edge(BRAND)}`, '&:hover': { bgcolor: BRAND, color: '#fff' } }}
|
||||
>
|
||||
<MdEdit size={14} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton size="small" onClick={() => setExpandedHubId(expanded ? null : hub.hubid)}>
|
||||
{expanded ? <MdKeyboardArrowUp size={16} /> : <MdKeyboardArrowDown size={16} />}
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} sx={{ p: 0, border: 0 }}>
|
||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ p: 2, bgcolor: DT.surfaceAlt }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 800, color: DT.textSecondary, textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
||||
Milers at {hub.hubname}
|
||||
</Typography>
|
||||
{hubMilers.length === 0 ? (
|
||||
<Typography variant="body2" sx={{ color: DT.textMuted, mt: 1 }}>
|
||||
No milers assigned to this hub.
|
||||
</Typography>
|
||||
) : (
|
||||
<TableContainer sx={{ mt: 1, borderRadius: 2, border: `1px solid ${DT.borderSubtle}`, background: '#fff' }}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow sx={{ '& th': { color: DT.textSecondary, fontWeight: 800, fontSize: 10.5, textTransform: 'uppercase' } }}>
|
||||
<TableCell>Miler</TableCell>
|
||||
<TableCell>Phone</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="right">Rating</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{hubMilers.map((m) => {
|
||||
const availColor = m.availabilitystatus === 'Available' ? '#10b981' : m.availabilitystatus === 'On_Break' ? '#f59e0b' : '#94a3b8';
|
||||
return (
|
||||
<TableRow key={m.userid}>
|
||||
<TableCell>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<AccentAvatar color={BRAND} size={26}>
|
||||
<MdTwoWheeler size={13} />
|
||||
</AccentAvatar>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{m.displayname || `Miler #${m.userid}`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
|
||||
{m.phone || '—'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
size="small"
|
||||
label={m.availabilitystatus || '—'}
|
||||
sx={{ bgcolor: tint(availColor), color: availColor, border: `1px solid ${edge(availColor)}`, fontWeight: 700 }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Stack direction="row" alignItems="center" justifyContent="flex-end" spacing={0.5}>
|
||||
<MdStar size={13} style={{ color: '#f59e0b' }} />
|
||||
<Typography variant="body2">{m.rating ?? '—'}</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
|
||||
{/* ============================================= || Create / Edit dialog || ============================================= */}
|
||||
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth PaperProps={{ sx: { borderRadius: 3 } }}>
|
||||
<DialogTitle sx={{ background: 'linear-gradient(135deg, #C01227 0%, #D35968 100%)', color: '#fff' }}>
|
||||
{editingHub ? `Edit ${editingHub.hubname}` : 'Create New Hub'}
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ mt: 2 }}>
|
||||
<Grid container spacing={2.5} sx={{ mt: 0.5 }}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography sx={{ mb: 1 }}>Hub Name</Typography>
|
||||
<TextField fullWidth value={form.hubname} onChange={(e) => setForm({ ...form, hubname: e.target.value })} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography sx={{ mb: 1 }}>Hub Type</Typography>
|
||||
<Select fullWidth value={form.hubtype} onChange={(e) => setForm({ ...form, hubtype: e.target.value })}>
|
||||
{HUB_TYPES.map((t) => (
|
||||
<MenuItem key={t.value} value={t.value}>
|
||||
{t.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography sx={{ mb: 1 }}>City</Typography>
|
||||
<Select fullWidth value={form.city} onChange={(e) => setForm({ ...form, city: e.target.value })}>
|
||||
{Object.values(CITY_MAP).map((c) => (
|
||||
<MenuItem key={c.name} value={c.name}>
|
||||
{c.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography sx={{ mb: 1 }}>Capacity</Typography>
|
||||
<TextField fullWidth type="number" value={form.capacity} onChange={(e) => setForm({ ...form, capacity: e.target.value })} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography sx={{ mb: 1 }}>Contact</Typography>
|
||||
<TextField fullWidth value={form.contact} onChange={(e) => setForm({ ...form, contact: e.target.value })} />
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography sx={{ mb: 1 }}>Pincode</Typography>
|
||||
<TextField fullWidth inputProps={{ maxLength: 6 }} value={form.pincode} onChange={(e) => setForm({ ...form, pincode: e.target.value.replace(/\D/g, '') })} />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography sx={{ mb: 1 }}>Address</Typography>
|
||||
<TextField fullWidth multiline minRows={2} value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 2.5 }}>
|
||||
<Button onClick={() => setDialogOpen(false)} color="secondary" variant="outlined">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
variant="contained"
|
||||
disabled={createMutation.isLoading || updateMutation.isLoading}
|
||||
sx={{ bgcolor: BRAND, '&:hover': { bgcolor: '#900E1D' } }}
|
||||
>
|
||||
{editingHub ? 'Save Changes' : 'Create Hub'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hubs;
|
||||
@@ -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,151 +1,83 @@
|
||||
// This is the active login page — matched by routes/LoginRoutes.js, which is
|
||||
// registered before MainRoutes.js and wins the route match for both '/' and
|
||||
// '/login'. (login1.js is dead code — MainRoutes.js's own '/login' route is
|
||||
// unreachable because LoginRoutes.js's un-prefixed '/login' matches first.)
|
||||
import { useState, useEffect } from 'react';
|
||||
import { enqueueSnackbar, closeSnackbar } from 'notistack';
|
||||
import AnimateButton from 'components/@extended/AnimateButton';
|
||||
import OtpInput from 'react18-input-otp';
|
||||
|
||||
import { Box, Card, CardContent, Stack, TextField, Button, Typography, Link, FormLabel, IconButton, InputAdornment } from '@mui/material';
|
||||
import { Box, Card, CardContent, Stack, TextField, Button, Typography, Link, IconButton, InputAdornment } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Loader from 'components/Loader';
|
||||
import logo from 'assets/images/logo-nearle1.png';
|
||||
import doormileLogo from 'assets/images/doormile-logo.png';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { OpenToast } from 'components/third-party/OpenToast';
|
||||
import { closeGlobalToast, GlobalToast } from 'components/nearle_components/GlobalToast';
|
||||
import Visibility from '@mui/icons-material/Visibility';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
import { setLoginUser } from 'store/reducers/loginUserSlice';
|
||||
import { markSessionStart } from 'utils/session';
|
||||
|
||||
// doormile-logo.png is a white asset; this recolours it to brand red (#C01227) for light surfaces.
|
||||
const DOORMILE_RED_FILTER = 'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)';
|
||||
|
||||
const Login = () => {
|
||||
const dispatch = useDispatch();
|
||||
const fcmtoken = useSelector((state) => state.fcm);
|
||||
const permission = useSelector((state) => state.fcm.permission);
|
||||
const theme = useTheme();
|
||||
const [loading, setLoading] = useState(false);
|
||||
let navigate = useNavigate();
|
||||
const [otp, setOtp] = useState('');
|
||||
const [currentotp, setCurrentotp] = useState('');
|
||||
const [userinfo, setUserinfo] = useState({});
|
||||
const [username, setUsername] = useState('');
|
||||
const [passwordStatus, setPasswordStatus] = useState(0);
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isPassword, setIspassword] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [userid, setUserid] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem('firstname')) {
|
||||
if (localStorage.getItem('authname')) {
|
||||
navigate('/nearle/dispatch');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loginsend = async () => {
|
||||
setLoading(true);
|
||||
|
||||
if (!username) {
|
||||
if (!username || !password) {
|
||||
opentoast('Fill All required fields');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.post(`https://jupiter.nearle.app/live/api/v1/users/console/login`, {
|
||||
authname: username,
|
||||
configid: 9, // 9 -> config id for nearle console admin
|
||||
userfcmtoken: fcmtoken?.token,
|
||||
password
|
||||
const res = await axios.post(`${process.env.REACT_APP_URL}/admin/login`, {
|
||||
email: username,
|
||||
password,
|
||||
userfcmtoken: fcmtoken?.token
|
||||
});
|
||||
// user not found
|
||||
if (res.data.code == 409 && !res.data.status) {
|
||||
OpenToast(res.data.message, 'error', 3000);
|
||||
}
|
||||
// user not activated
|
||||
else if (res.data.code == 403) {
|
||||
OpenToast(res.data.message, 'warning', 3000);
|
||||
}
|
||||
//user found, no password, setup password
|
||||
else if (res.data.code == 409 && res.data.status) {
|
||||
setPasswordStatus(1); // for password and confirm password ui
|
||||
setUserid(res.data.details.userid);
|
||||
OpenToast('User Found', 'success', 3000);
|
||||
OpenToast(res.data.message, 'success', 3000);
|
||||
}
|
||||
//user found, incorrect password
|
||||
else if (res.data.code == 401 && !res.data.status) {
|
||||
OpenToast(res.data.message, 'error', 3000);
|
||||
}
|
||||
//user found, enter password
|
||||
else if (res.data.code == 401 && res.data.status) {
|
||||
OpenToast(res.data.message, 'success', 3000);
|
||||
fetchAppLocations(res.data.userid);
|
||||
setPasswordStatus(2);
|
||||
}
|
||||
// user found, correct password
|
||||
else if (res.data.code == 200 && res.data.status) {
|
||||
OpenToast(res.data.message, 'success', 1000);
|
||||
setUserinfo(res.data.details);
|
||||
const userinfo = res.data.details;
|
||||
dispatch(setLoginUser(userinfo));
|
||||
localStorage.setItem('firstname', userinfo.firstname);
|
||||
localStorage.setItem('authname', userinfo.authname);
|
||||
localStorage.setItem('roleid', userinfo.roleid);
|
||||
localStorage.setItem('tenantid', userinfo.tenantid);
|
||||
localStorage.setItem('partnerid', userinfo.partnerid);
|
||||
localStorage.setItem('applocationid', userinfo.applocationid);
|
||||
localStorage.setItem('userid', userinfo.userid);
|
||||
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
|
||||
fetchAppLocations(userinfo.userid);
|
||||
|
||||
if (res.data.success) {
|
||||
OpenToast('Login Successful', 'success', 1000);
|
||||
// The live backend's success response doesn't nest user fields under
|
||||
// `data` the way the original spec described — fall back to reading
|
||||
// them off the top-level response if `data` isn't there.
|
||||
const token = res.data.token;
|
||||
const data = res.data.data || res.data.user || res.data;
|
||||
dispatch(setLoginUser(data));
|
||||
localStorage.setItem('authname', data.email || data.firstname || username);
|
||||
localStorage.setItem('userid', data.userid ?? data.id ?? '');
|
||||
localStorage.setItem('roleid', data.roleid ?? data.role ?? '');
|
||||
localStorage.setItem('token', token);
|
||||
axios.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||
markSessionStart();
|
||||
navigate('/nearle/dispatch');
|
||||
} else {
|
||||
OpenToast(res.data.message, 'error', 3000);
|
||||
OpenToast(res.data.message || 'Invalid Data', 'error', 3000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
OpenToast(err.message, 'error', 5000);
|
||||
OpenToast(err.response?.data?.message || err.message, 'error', 5000);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const loginsuccessful = () => {
|
||||
localStorage.setItem('firstname', userinfo.firstname);
|
||||
localStorage.setItem('authname', userinfo.authname);
|
||||
localStorage.setItem('roleid', userinfo.roleid);
|
||||
localStorage.setItem('tenantid', userinfo.tenantid);
|
||||
localStorage.setItem('partnerid', userinfo.partnerid);
|
||||
localStorage.setItem('applocationid', userinfo.applocationid);
|
||||
localStorage.setItem('userid', userinfo.userid);
|
||||
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
|
||||
closeGlobalToast(); // to close the pin snackbar
|
||||
|
||||
navigate('/nearle/dispatch');
|
||||
};
|
||||
|
||||
const opentoast = (message) => {
|
||||
enqueueSnackbar(message, {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 1500
|
||||
});
|
||||
};
|
||||
|
||||
const fetchAppLocations = async (id) => {
|
||||
const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${id}`);
|
||||
const updatedLocations = [...response.data.details, { locationname: 'All', applocationid: 0 }];
|
||||
localStorage.setItem('applocations', JSON.stringify(updatedLocations));
|
||||
};
|
||||
const updateUser = async () => {
|
||||
const response = await axios.put(`${process.env.REACT_APP_URL2}/users/update`, {
|
||||
userid,
|
||||
password
|
||||
});
|
||||
if (response.data.status) {
|
||||
OpenToast(response.data.message, 'success', 3000);
|
||||
OpenToast('Enter Password to Login', 'success', 3000);
|
||||
setPasswordStatus(2);
|
||||
setPassword('');
|
||||
}
|
||||
OpenToast(message, 'error', 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -160,12 +92,24 @@ const Login = () => {
|
||||
overflow: 'hidden',
|
||||
flexBasis: '46%',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
p: 6,
|
||||
background: 'linear-gradient(150deg, #4D1C61 0%, #662582 52%, #9255AB 100%)'
|
||||
background: 'linear-gradient(150deg, #900E1D 0%, #C01227 52%, #D35968 100%)'
|
||||
}}
|
||||
>
|
||||
{/* Logo at the top-left corner */}
|
||||
<img
|
||||
src={doormileLogo}
|
||||
alt="Doormile"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 48,
|
||||
left: 48,
|
||||
maxHeight: 40
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* decorative light glows */}
|
||||
<Box
|
||||
sx={{
|
||||
@@ -190,54 +134,39 @@ const Login = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: '#fff',
|
||||
borderRadius: 2,
|
||||
px: 1.5,
|
||||
py: 0.75,
|
||||
display: 'inline-flex',
|
||||
boxShadow: '0 8px 20px rgba(0,0,0,0.18)'
|
||||
}}
|
||||
>
|
||||
<img src={logo} alt="NearlExpress" style={{ height: 30, display: 'block' }} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<Typography sx={{ fontSize: 34, fontWeight: 700, lineHeight: 1.18, letterSpacing: '-0.02em', mb: 2 }}>
|
||||
<Box sx={{ position: 'relative', maxWidth: 430 }}>
|
||||
<Typography sx={{ fontSize: 40, fontWeight: 700, lineHeight: 1.18, letterSpacing: '-0.02em', mb: 2 }}>
|
||||
Operate your dispatch,
|
||||
<br />
|
||||
end to end.
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: 16, color: 'rgba(255,255,255,0.82)', maxWidth: 430, lineHeight: 1.6 }}>
|
||||
Orders, AI route optimisation, live rider tracking and billing — all in the NearlExpress operator console.
|
||||
<Typography sx={{ fontSize: 17.5, color: 'rgba(255,255,255,0.85)', mb: 4, lineHeight: 1.6 }}>
|
||||
Orders, AI route optimisation, live rider tracking and billing — all in the Doormile operator console.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Stack spacing={1.25} sx={{ position: 'relative' }}>
|
||||
{['Real-time fleet visibility', 'AI-optimised dispatch routes', 'Tenant, pricing & invoice control'].map((t) => (
|
||||
<Stack key={t} direction="row" spacing={1.25} alignItems="center">
|
||||
<Box
|
||||
sx={{
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: '50%',
|
||||
bgcolor: 'rgba(255,255,255,0.18)',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 13,
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
✓
|
||||
</Box>
|
||||
<Typography sx={{ fontSize: 14.5, color: 'rgba(255,255,255,0.9)' }}>{t}</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack spacing={1.5} sx={{ display: 'inline-flex', textAlign: 'left' }}>
|
||||
{['Real-time fleet visibility', 'AI-optimised dispatch routes', 'Tenant, pricing & invoice control'].map((t) => (
|
||||
<Stack key={t} direction="row" spacing={1.25} alignItems="center">
|
||||
<Box
|
||||
sx={{
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: '50%',
|
||||
bgcolor: 'rgba(255,255,255,0.18)',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 13,
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
✓
|
||||
</Box>
|
||||
<Typography sx={{ fontSize: 16, color: 'rgba(255,255,255,0.9)' }}>{t}</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* ---- Right form panel ---- */}
|
||||
@@ -254,7 +183,7 @@ const Login = () => {
|
||||
>
|
||||
{/* Logo */}
|
||||
<Stack alignItems="center" mb={2.5}>
|
||||
<img src={logo} alt="loginpagelogo" style={{ maxHeight: 48 }} />
|
||||
<img src={doormileLogo} alt="Doormile" style={{ maxHeight: 40, filter: DOORMILE_RED_FILTER }} />
|
||||
</Stack>
|
||||
|
||||
{/* Title */}
|
||||
@@ -262,7 +191,7 @@ const Login = () => {
|
||||
Welcome back
|
||||
</Typography>
|
||||
<Typography variant="body2" textAlign="center" sx={{ color: '#64748b', mb: 3 }}>
|
||||
Sign in to the NearlExpress console
|
||||
Sign in to the Doormile console
|
||||
</Typography>
|
||||
|
||||
<CardContent sx={{ p: 0 }}>
|
||||
@@ -270,28 +199,7 @@ const Login = () => {
|
||||
noValidate
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (passwordStatus == 0) {
|
||||
loginsend();
|
||||
} else if (passwordStatus == 1) {
|
||||
if (!password || !confirmPassword || password != confirmPassword) {
|
||||
OpenToast('Check Password', 'warning', 3000);
|
||||
} else {
|
||||
updateUser();
|
||||
}
|
||||
} else if (passwordStatus == 2) {
|
||||
if (!password) {
|
||||
OpenToast('Invalid Password', 'warning', 3000);
|
||||
}
|
||||
loginsend();
|
||||
}
|
||||
// if (currentotp) {
|
||||
// if (currentotp == otp) {
|
||||
// loginsuccessful();
|
||||
// fetchAppLocations();
|
||||
// } else {
|
||||
// opentoast('Invalid pin');
|
||||
// }
|
||||
// }
|
||||
loginsend();
|
||||
}}
|
||||
>
|
||||
<Stack spacing={3}>
|
||||
@@ -305,122 +213,27 @@ const Login = () => {
|
||||
required
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value.toLocaleLowerCase())}
|
||||
InputProps={{ readOnly: passwordStatus }}
|
||||
/>
|
||||
{/* Setup Password */}
|
||||
{passwordStatus == 1 && (
|
||||
<Stack display={'flex'} flexDirection={'column'} spacing={3}>
|
||||
<Typography variant="h4" textAlign="start" mb={3}>
|
||||
Setup Password
|
||||
</Typography>
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="Enter New Password"
|
||||
variant="outlined"
|
||||
required
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
error={confirmPassword !== '' && password !== confirmPassword}
|
||||
fullWidth
|
||||
label="Re-Enter Password"
|
||||
variant="outlined"
|
||||
required
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowConfirmPassword((prev) => !prev)} edge="end">
|
||||
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{/* Enter Password */}
|
||||
{passwordStatus == 2 && (
|
||||
<Stack display={'flex'} flexDirection={'column'} spacing={3}>
|
||||
<Typography variant="h4" textAlign="start" mb={3}>
|
||||
Enter Password
|
||||
</Typography>
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="Enter Password"
|
||||
variant="outlined"
|
||||
required
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* OTP */}
|
||||
{isPassword && (
|
||||
<Stack spacing={1.5}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<FormLabel>Enter Password</FormLabel>
|
||||
<Link
|
||||
variant="body2"
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setOtp('');
|
||||
loginsend();
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Link>
|
||||
</Stack>
|
||||
|
||||
{/* <OtpInput
|
||||
shouldAutoFocus
|
||||
value={otp}
|
||||
onChange={(otp) => setOtp(otp)}
|
||||
numInputs={4}
|
||||
containerStyle={{ justifyContent: 'space-between' }}
|
||||
inputStyle={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${borderColor}`,
|
||||
fontSize: 18
|
||||
}}
|
||||
focusStyle={{
|
||||
outline: 'none',
|
||||
border: `1px solid ${theme.palette.primary.main}`,
|
||||
boxShadow: theme.customShadows.primary
|
||||
}}
|
||||
/> */}
|
||||
<TextField type="passowrd" value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||
</Stack>
|
||||
)}
|
||||
{/* Password */}
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Password"
|
||||
variant="outlined"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{/* Submit */}
|
||||
<AnimateButton>
|
||||
<Button fullWidth size="large" type="submit" variant="contained" color="primary">
|
||||
@@ -439,7 +252,7 @@ const Login = () => {
|
||||
component={Link}
|
||||
href="https://nearle.in"
|
||||
target="_blank"
|
||||
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
|
||||
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#C01227' } }}
|
||||
>
|
||||
© All rights reserved
|
||||
</Typography>
|
||||
@@ -448,7 +261,7 @@ const Login = () => {
|
||||
component={Link}
|
||||
href="https://nearle.in/terms"
|
||||
target="_blank"
|
||||
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
|
||||
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#C01227' } }}
|
||||
>
|
||||
Terms and Conditions
|
||||
</Typography>
|
||||
@@ -457,7 +270,7 @@ const Login = () => {
|
||||
component={Link}
|
||||
href="https://nearle.in/privacy"
|
||||
target="_blank"
|
||||
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
|
||||
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#C01227' } }}
|
||||
>
|
||||
Privacy Policy
|
||||
</Typography>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// UNUSED — routes/LoginRoutes.js is registered before MainRoutes.js and
|
||||
// defines its own un-prefixed '/login' route pointing at pages/nearle/login,
|
||||
// which wins the match. This file's '/login' route inside MainRoutes.js is
|
||||
// unreachable. Kept updated to Doormile endpoints anyway (harmless), but
|
||||
// pages/nearle/login.js is the one that actually renders.
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
// import AuthWrapper from 'sections/auth/AuthWrapper';
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
@@ -22,22 +25,25 @@ import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import AnimateButton from 'components/@extended/AnimateButton';
|
||||
|
||||
import logo from 'assets/images/logo-nearle1.png';
|
||||
import logo from 'assets/images/doormile-logo.png';
|
||||
|
||||
// doormile-logo.png is a white asset; this recolours it to brand red (#C01227) for light surfaces.
|
||||
const DOORMILE_RED_FILTER = 'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)';
|
||||
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
// import { openSnackbar } from 'store/reducers/snackbar';
|
||||
// import { useDispatch } from 'react-redux';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Loader from 'components/Loader';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
|
||||
const Login = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const fcmtoken = useSelector((state) => state.fcm);
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [alertmessage, setAlertmessage] = useState('');
|
||||
const [checkusername, setCheckusername] = useState(false);
|
||||
const [, setAlertmessage] = useState('');
|
||||
// const [toast, setToast] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
let navigate = useNavigate();
|
||||
@@ -75,119 +81,48 @@ const Login = () => {
|
||||
// console.log(alertmessage)
|
||||
}, []);
|
||||
|
||||
const usernamecheck = async (e) => {
|
||||
e.preventDefault();
|
||||
setUsername(e.target.value);
|
||||
if (e.target.value) {
|
||||
try {
|
||||
// await axios.post(`${process.env.REACT_APP_URL}/auth/login`, {
|
||||
// "authname": e.target.value
|
||||
// })
|
||||
await axios
|
||||
.post(`${process.env.REACT_APP_URL}/users/login`, {
|
||||
authname: e.target.value,
|
||||
configid: 1
|
||||
// "contactno": e.target.value,
|
||||
// "password": 'admin'
|
||||
})
|
||||
.then((res) => {
|
||||
console.log(res.data);
|
||||
if (res.data.details.authname === e.target.value) {
|
||||
setUsername(e.target.value);
|
||||
setCheckusername(false);
|
||||
} else {
|
||||
setCheckusername(true);
|
||||
}
|
||||
// if (res.data.authname === e.target.value) {
|
||||
|
||||
// setUsername(e.target.value);
|
||||
// setCheckusername(false);
|
||||
// }
|
||||
})
|
||||
.catch((err) => {
|
||||
// if (err.response.data.message === 'No user found') {
|
||||
|
||||
setCheckusername(true);
|
||||
// }
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loginsend = async () => {
|
||||
// e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
if (password && username) {
|
||||
if (password == 'admin') {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await axios
|
||||
.post(`${process.env.REACT_APP_URL}/users/partner/login`, {
|
||||
// "authname": username,
|
||||
configid: 1,
|
||||
contactno: username
|
||||
|
||||
// "password": password
|
||||
})
|
||||
.then((res) => {
|
||||
console.log(res.data);
|
||||
if (res.data.status) {
|
||||
if (res.data.details.contactno === username) {
|
||||
enqueueSnackbar('login Successfull', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 3000
|
||||
});
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
localStorage.setItem('firstname', res.data.details.tenantname);
|
||||
localStorage.setItem('authname', res.data.details.authname);
|
||||
|
||||
// localStorage.setItem("appuserid", res.data.details.userid);
|
||||
localStorage.setItem('roleid', res.data.details.roleid);
|
||||
localStorage.setItem('tenantid', res.data.details.tenantid);
|
||||
localStorage.setItem('partnerid', res.data.details.partnerid);
|
||||
|
||||
navigate('/orders');
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
console.log(res.data.message);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
// setAlertmessage('Invalid Data');
|
||||
// if(err.message == 'Network Error'){
|
||||
opentoast(err.message);
|
||||
// }else{
|
||||
// opentoast('Invalid Data');
|
||||
|
||||
// }
|
||||
setLoading(false);
|
||||
setSubmitting(false);
|
||||
console.log(err.message);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
setSubmitting(false);
|
||||
}
|
||||
} else {
|
||||
opentoast('Password is Incorrect');
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
// let el2 = document.getElementById('toastid');
|
||||
// el2.classList.add('d-block');
|
||||
// el2.classList.remove('d-none');
|
||||
if (!password || !username) {
|
||||
setAlertmessage('Fill All required fields');
|
||||
opentoast('Fill All required fields');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res = await axios.post(`${process.env.REACT_APP_URL}/admin/login`, {
|
||||
email: username,
|
||||
password,
|
||||
userfcmtoken: fcmtoken?.token
|
||||
});
|
||||
|
||||
if (res.data.success) {
|
||||
const { token, data } = res.data;
|
||||
enqueueSnackbar('Login Successful', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 3000
|
||||
});
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
localStorage.setItem('authname', data.email || data.firstname);
|
||||
localStorage.setItem('userid', data.userid);
|
||||
localStorage.setItem('roleid', data.roleid);
|
||||
localStorage.setItem('token', token);
|
||||
axios.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||
|
||||
navigate('/orders');
|
||||
} else {
|
||||
opentoast(res.data.message || 'Invalid Data');
|
||||
}
|
||||
} catch (err) {
|
||||
opentoast(err.response?.data?.message || err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -228,7 +163,7 @@ const Login = () => {
|
||||
// sx={{ ml: 3, mt: 3 }}
|
||||
sx={{ ml: { xs: 0, md: 3 }, mt: { xs: 3, md: 1 }, textAlign: { xs: 'center', md: 'left' } }}
|
||||
>
|
||||
<img src={logo} alt="legendary" width={isMobile ? '160px' : '200px'} />
|
||||
<img src={logo} alt="Doormile" width={isMobile ? '160px' : '200px'} style={{ filter: DOORMILE_RED_FILTER }} />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Grid
|
||||
@@ -275,10 +210,6 @@ const Login = () => {
|
||||
<CardHeader title={<Typography variant="h3">Login</Typography>} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
{/* <Grid item xs={12}>
|
||||
<AuthLogin isDemo={isLoggedIn} />
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
<CardContent>
|
||||
<form
|
||||
@@ -322,8 +253,8 @@ const Login = () => {
|
||||
variant="outlined"
|
||||
autoComplete="email"
|
||||
required
|
||||
onChange={usernamecheck}
|
||||
error={checkusername}
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
@@ -513,4 +444,4 @@ const Login = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Login1;
|
||||
export default Login;
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Backdrop,
|
||||
IconButton
|
||||
} from '@mui/material';
|
||||
import React, { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||||
@@ -32,7 +32,7 @@ import Loader from 'components/Loader';
|
||||
import CircularLoader from 'components/CircularLoader';
|
||||
import { Empty } from 'antd';
|
||||
import HoverSocialCard from 'components/cards/statistics/HoverSocialCard';
|
||||
import { DashboardFilled, OpenAIFilled } from '@ant-design/icons';
|
||||
import { DashboardFilled } from '@ant-design/icons';
|
||||
import { MdDirectionsBike } from 'react-icons/md';
|
||||
import { FaMapLocationDot } from 'react-icons/fa6';
|
||||
import { HiOutlineArrowLeft } from 'react-icons/hi';
|
||||
@@ -234,9 +234,7 @@ const OrdersPreview = () => {
|
||||
|
||||
const {
|
||||
data: paymentModes = [],
|
||||
isLoading: paymentModesLoading,
|
||||
isError: paymentModesError,
|
||||
error: paymentModesErrorMessage
|
||||
isLoading: paymentModesLoading
|
||||
} = useQuery({
|
||||
queryKey: ['paymentmodes'],
|
||||
queryFn: fetchPaymentType
|
||||
@@ -246,24 +244,13 @@ const OrdersPreview = () => {
|
||||
|
||||
const {
|
||||
data: ridersList = [],
|
||||
isLoading: ridersListLoading,
|
||||
isError: ridersListError,
|
||||
error: ridersListErrorMessage,
|
||||
refetch: ridersListRefetch
|
||||
isLoading: ridersListLoading
|
||||
} = useQuery({
|
||||
queryKey: ['ridersList', appId], // Unique key for caching & re-fetching
|
||||
queryFn: fetchRidersList,
|
||||
enabled: appId !== 0 // Ensures query runs only when appId is valid
|
||||
});
|
||||
|
||||
const getRiderName = async (userid) => {
|
||||
await ridersList.map((rider) => {
|
||||
if (rider.userid == userid) {
|
||||
return rider.firstname;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ======================================================= || notifyRiderMutation || =======================================================
|
||||
|
||||
const notifyRiderMutation = useMutation({
|
||||
|
||||
@@ -696,7 +696,7 @@
|
||||
|
||||
.weight-card-btn.active.weight-heavy {
|
||||
background: rgba(99, 102, 241, 0.04) !important;
|
||||
border-color: #662582 !important;
|
||||
border-color: #C01227 !important;
|
||||
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.15) !important;
|
||||
}
|
||||
|
||||
@@ -707,7 +707,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: #662582;
|
||||
background: #C01227;
|
||||
}
|
||||
|
||||
/* Premium Card Overrides */
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Button,
|
||||
TextField,
|
||||
Autocomplete,
|
||||
Chip,
|
||||
Divider,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
@@ -19,15 +18,15 @@ import {
|
||||
IconButton,
|
||||
Switch,
|
||||
OutlinedInput,
|
||||
FormGroup,
|
||||
FormControlLabel,
|
||||
Box,
|
||||
Card,
|
||||
useMediaQuery
|
||||
} from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { calculateDrivingDistance, calculateTotalCharge } from '../../../utils/distance';
|
||||
import { Empty } from 'antd';
|
||||
import { FaPhoneAlt, FaBox, FaBoxes, FaTruck, FaArrowRight, FaArrowLeft, FaCheck, FaRoute, FaMoneyBillWave, FaChartLine, FaReceipt, FaPaperPlane } from 'react-icons/fa';
|
||||
import { FaPhoneAlt, FaBox, FaBoxes, FaArrowRight, FaArrowLeft, FaCheck, FaRoute, FaMoneyBillWave, FaChartLine, FaReceipt, FaPaperPlane } from 'react-icons/fa';
|
||||
import { GiDoorHandle } from 'react-icons/gi';
|
||||
import { FaLandmarkDome } from 'react-icons/fa6';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
@@ -38,8 +37,6 @@ import axios from 'axios';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Geocode from 'react-geocode';
|
||||
import Loader from 'components/Loader';
|
||||
import * as geolib from 'geolib';
|
||||
import MainCard from 'components/MainCard';
|
||||
import { FaUser } from 'react-icons/fa6';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
@@ -49,7 +46,7 @@ import dayjs from 'dayjs';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
var utc = require('dayjs/plugin/utc');
|
||||
dayjs.extend(utc);
|
||||
import { SearchOutlined, CloseOutlined, CalendarOutlined, ClockCircleOutlined, FileTextOutlined, MessageOutlined } from '@ant-design/icons';
|
||||
import { SearchOutlined, CalendarOutlined, ClockCircleOutlined, FileTextOutlined, MessageOutlined } from '@ant-design/icons';
|
||||
import MyLocationIcon from '@mui/icons-material/MyLocation';
|
||||
import HighlightOffIcon from '@mui/icons-material/HighlightOff';
|
||||
import { OpenToast } from 'components/third-party/OpenToast';
|
||||
@@ -57,7 +54,6 @@ import { MapContainer, TileLayer, Marker, Polyline, useMap } from 'react-leaflet
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import './OrdersRedesign.css';
|
||||
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
||||
import AnimateButton from 'components/@extended/AnimateButton';
|
||||
|
||||
const pickupIcon = typeof window !== 'undefined' ? new L.Icon({
|
||||
@@ -158,9 +154,9 @@ const OrderMap = ({ startPoint, endPoint, appLocaLat, appLocaLng }) => {
|
||||
{hasPick && <Marker position={pickCoords} icon={pickupIcon} />}
|
||||
{hasDrop && <Marker position={dropCoords} icon={dropoffIcon} />}
|
||||
{routePoints.length > 0 ? (
|
||||
<Polyline positions={routePoints} color="#662582" weight={4} />
|
||||
<Polyline positions={routePoints} color="#C01227" weight={4} />
|
||||
) : (
|
||||
hasPick && hasDrop && <Polyline positions={[pickCoords, dropCoords]} color="#662582" weight={4} />
|
||||
hasPick && hasDrop && <Polyline positions={[pickCoords, dropCoords]} color="#C01227" weight={4} />
|
||||
)}
|
||||
<MapBoundsController startPoint={startPoint} endPoint={endPoint} />
|
||||
</MapContainer>
|
||||
@@ -191,14 +187,12 @@ const Createorder1 = () => {
|
||||
const tenantRef = useRef(null);
|
||||
const [inputValue1, setInputValue1] = React.useState('');
|
||||
const [inputValue2, setInputValue2] = React.useState('');
|
||||
const [tenanatLocoId, setTenanatLocoId] = useState(localStorage.getItem('locationid'));
|
||||
const [isLocation, setIsLocation] = useState(false);
|
||||
const textFieldRef1 = useRef(null);
|
||||
const textFieldRef1a = useRef(null);
|
||||
const textFieldRef2 = useRef(null);
|
||||
const [appId, setAppId] = useState(0);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [clientdetail, setClientdetail] = useState([]);
|
||||
const [, setClientdetail] = useState([]);
|
||||
const [startdate, setStartdate] = useState(dayjs().format('MM-DD-YYYY'));
|
||||
// const [starttime, setStatrttime] = useState(`${dayjs().format('MM-DD-YYYY')} 08:00:00`);
|
||||
const [starttime, setStatrttime] = useState();
|
||||
@@ -223,16 +217,15 @@ const Createorder1 = () => {
|
||||
const [minKm, setMinKm] = useState(0);
|
||||
const [totalCharge, setTotalCharge] = useState(0);
|
||||
const [subCat, setSubCat] = useState([]);
|
||||
const [subCatName, setSubCatName] = useState('Select ');
|
||||
const [, setSubCatName] = useState('Select ');
|
||||
const [subCatId, setSubCatId] = useState(0);
|
||||
const [weight, setWeight] = useState('');
|
||||
const [weight] = useState('');
|
||||
const [tenantid, setTenantid] = useState(0);
|
||||
const [locationid, setLocationid] = useState(0);
|
||||
const [selectedCatChip, setSelectedCatChip] = useState(null);
|
||||
const [isCustomerOpen, setIsCustomerOpen] = useState(false);
|
||||
const [searchCustList, setSearchCustList] = useState('');
|
||||
const [customerlist, setCustomerlist] = useState([]);
|
||||
const [defaultPickup, setDefaultPickup] = useState(null);
|
||||
const [, setDefaultPickup] = useState(null);
|
||||
const [pickCust, setPickCust] = useState(null);
|
||||
const [dropCust, setDropCust] = useState(null);
|
||||
const [pickordrop, setpickordrop] = useState(0); // 1 ->pick 2 -> drop
|
||||
@@ -329,20 +322,6 @@ const Createorder1 = () => {
|
||||
appId && fetchtenantinfolist();
|
||||
}, [appId]);
|
||||
|
||||
const handleChipClick = (chipLabel) => {
|
||||
setSelectedCatChip(chipLabel);
|
||||
};
|
||||
|
||||
const chipStyle = (chipLabel) => ({
|
||||
cursor: 'pointer',
|
||||
backgroundColor: selectedCatChip === chipLabel ? theme.palette.primary.main : 'default',
|
||||
color: selectedCatChip === chipLabel ? '#fff' : '',
|
||||
'&:hover': {
|
||||
backgroundColor: selectedCatChip === chipLabel ? theme.palette.primary.main : theme.palette.primary.light,
|
||||
color: '#fff'
|
||||
}
|
||||
});
|
||||
|
||||
const fetchTenantPricing = async (id) => {
|
||||
try {
|
||||
const pricingResponse = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${id}`);
|
||||
@@ -365,130 +344,25 @@ const Createorder1 = () => {
|
||||
}, [startPoint, endPoint]);
|
||||
|
||||
// google distance matrix logic
|
||||
/*
|
||||
const calculateDistance = async (pickup, drop) => {
|
||||
const service = new google.maps.DistanceMatrixService();
|
||||
const getDistanceMatrix = (origins, destinations, travelMode, unitSystem) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
service.getDistanceMatrix(
|
||||
{
|
||||
origins: [new google.maps.LatLng(origins.latitude, origins.longitude)],
|
||||
destinations: [new google.maps.LatLng(destinations.latitude, destinations.longitude)],
|
||||
travelMode: travelMode,
|
||||
unitSystem: unitSystem
|
||||
},
|
||||
(response, status) => {
|
||||
if (status === 'OK') {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(new Error(`Error calculating distance: ${status}`));
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
// Use await to wait for the promise to resolve
|
||||
const response = await getDistanceMatrix(pickup, drop, 'DRIVING', google.maps.UnitSystem.METRIC);
|
||||
|
||||
// Handle the response
|
||||
const results = response.rows[0].elements;
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const element = results[i];
|
||||
|
||||
// Extract the numerical value of the distance
|
||||
|
||||
const distance = element.distance.value;
|
||||
console.log('distance in m ', distance);
|
||||
const distanceInKm = (distance / 1000).toFixed(2);
|
||||
console.log('distance in km ', distanceInKm);
|
||||
const roundedDistance = Math.round(distanceInKm);
|
||||
console.log('roundedDistance', roundedDistance);
|
||||
setDistance(roundedDistance);
|
||||
if (roundedDistance < minKm) {
|
||||
setTotalCharge(basePrice);
|
||||
} else {
|
||||
console.log('minKm', minKm);
|
||||
console.log('pricePerKm', pricePerKm);
|
||||
console.log('basePrice', basePrice);
|
||||
const total = (roundedDistance - minKm) * pricePerKm + basePrice;
|
||||
console.log('total', total);
|
||||
setTotalCharge(total);
|
||||
}
|
||||
setShowDistance(true);
|
||||
if (roundedDistance > appLocaRadius) {
|
||||
setShowDistance(true);
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
// Extract the numerical value of the duration
|
||||
const durationMatch = element.duration.text.match(/([\d.]+)/);
|
||||
const duration = durationMatch ? parseInt(durationMatch[0]) : null;
|
||||
|
||||
// Display only the numerical values
|
||||
console.log(`Distance: ${roundedDistance}, Duration: ${duration}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error calculating distance:', error);
|
||||
}
|
||||
};*/
|
||||
// Haversine + 1.3
|
||||
const calculateDistance = async (pickup, drop) => {
|
||||
// Haversine formula
|
||||
const haversineDistance = (lat1, lon1, lat2, lon2) => {
|
||||
const toRad = (value) => (value * Math.PI) / 180;
|
||||
|
||||
const R = 6371; // Earth radius in KM
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLon = toRad(lon2 - lon1);
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return R * c; // distance in KM
|
||||
};
|
||||
|
||||
try {
|
||||
// 1. Get aerial distance
|
||||
const aerialDistance = haversineDistance(pickup.latitude, pickup.longitude, drop.latitude, drop.longitude);
|
||||
|
||||
// 2. Convert to road approximation (1.3x)
|
||||
const distanceInKm = (aerialDistance * 1.3).toFixed(2);
|
||||
|
||||
console.log('distance in km ', distanceInKm);
|
||||
|
||||
const roundedDistance = Math.round(distanceInKm);
|
||||
console.log('roundedDistance', roundedDistance);
|
||||
|
||||
// 🔽 SAME AS YOUR EXISTING LOGIC (UNCHANGED)
|
||||
const roundedDistance = await calculateDrivingDistance(pickup, drop);
|
||||
console.log('calculated distance in km:', roundedDistance);
|
||||
setDistance(roundedDistance);
|
||||
|
||||
if (roundedDistance < minKm) {
|
||||
setTotalCharge(basePrice);
|
||||
} else {
|
||||
console.log('minKm', minKm);
|
||||
console.log('pricePerKm', pricePerKm);
|
||||
console.log('basePrice', basePrice);
|
||||
|
||||
const total = (roundedDistance - minKm) * pricePerKm + basePrice;
|
||||
|
||||
console.log('total', total);
|
||||
setTotalCharge(total);
|
||||
}
|
||||
const total = calculateTotalCharge(roundedDistance, basePrice, pricePerKm, minKm);
|
||||
console.log('total charge:', total);
|
||||
setTotalCharge(total);
|
||||
|
||||
setShowDistance(true);
|
||||
|
||||
if (roundedDistance > appLocaRadius) {
|
||||
setShowDistance(true);
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
// ⏱️ Approximate duration (optional, since no API)
|
||||
// ⏱️ Approximate duration
|
||||
const avgSpeed = 40; // km/h (adjust if needed)
|
||||
const duration = Math.round((roundedDistance / avgSpeed) * 60); // minutes
|
||||
|
||||
console.log(`Distance: ${roundedDistance}, Duration: ${duration}`);
|
||||
} catch (error) {
|
||||
console.error('Error calculating distance:', error);
|
||||
@@ -577,9 +451,9 @@ const Createorder1 = () => {
|
||||
console.log('endtime', `${dayjs().format('MM-DD-YYYY')} ${closetime} `);
|
||||
let arr = [];
|
||||
for (
|
||||
let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`, j = 0;
|
||||
let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`;
|
||||
dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0;
|
||||
j++, i = dayjs(i).add(30, 'm')
|
||||
i = dayjs(i).add(30, 'm')
|
||||
) {
|
||||
arr.push(i);
|
||||
}
|
||||
@@ -963,7 +837,6 @@ const Createorder1 = () => {
|
||||
// radius: 100000 //km to m
|
||||
}).getBounds()
|
||||
});
|
||||
let arr = [];
|
||||
// Event listener for autocomplete place changed
|
||||
autocomplete.addListener('place_changed', () => {
|
||||
const place = autocomplete.getPlace();
|
||||
@@ -1492,7 +1365,7 @@ const Createorder1 = () => {
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchOutlined style={{ fontSize: 15, color: '#662582' }} />
|
||||
<SearchOutlined style={{ fontSize: 15, color: '#C01227' }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
@@ -1528,7 +1401,7 @@ const Createorder1 = () => {
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<FaLocationDot style={{ fontSize: 14, color: '#662582' }} />
|
||||
<FaLocationDot style={{ fontSize: 14, color: '#C01227' }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
@@ -2270,7 +2143,7 @@ const Createorder1 = () => {
|
||||
{/* Map Card */}
|
||||
<Card className="orders-card" sx={{ p: 1.5, display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography sx={{ fontWeight: 700, mb: 1.25, display: 'flex', alignItems: 'center', gap: 0.75, color: '#1e293b', fontSize: '14px', letterSpacing: '-0.01em' }}>
|
||||
<MyLocationIcon sx={{ color: '#662582', fontSize: 16 }} />
|
||||
<MyLocationIcon sx={{ color: '#C01227', fontSize: 16 }} />
|
||||
Live Route Preview
|
||||
</Typography>
|
||||
<div className="map-preview-wrapper">
|
||||
@@ -2449,7 +2322,7 @@ const Createorder1 = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{ background: 'linear-gradient(135deg, #662582 0%, #9255AB 100%)', color: 'white', py: 2.5 }}>
|
||||
<DialogTitle sx={{ background: 'linear-gradient(135deg, #C01227 0%, #D35968 100%)', color: 'white', py: 2.5 }}>
|
||||
<Stack spacing={1.5}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 600, color: 'white' }}>
|
||||
{`Select Saved Address (${pickordrop === 1 ? 'Pickup' : 'Drop'})`}
|
||||
|
||||
@@ -1,39 +1,15 @@
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
Fragment
|
||||
// useReducer
|
||||
} from 'react';
|
||||
import { useEffect, useState, Fragment } from 'react';
|
||||
import BorderColorIcon from '@mui/icons-material/BorderColor';
|
||||
import {
|
||||
// Navigate,
|
||||
// useSearchParams,
|
||||
useLocation,
|
||||
useNavigate
|
||||
} from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import {
|
||||
// UserOutlined,
|
||||
EnvironmentOutlined,
|
||||
EditTwoTone
|
||||
// DeleteTwoTone
|
||||
} from '@ant-design/icons';
|
||||
// import WomanIcon from '@mui/icons-material/Woman';
|
||||
// import { Link } from 'react-router-dom';
|
||||
// import SoupKitchenIcon from '@mui/icons-material/SoupKitchen';
|
||||
import { EnvironmentOutlined, EditTwoTone } from '@ant-design/icons';
|
||||
import DirectionsCarIcon from '@mui/icons-material/DirectionsCar';
|
||||
import { KeyboardArrowUp, KeyboardArrowDown } from '@mui/icons-material';
|
||||
|
||||
// import { PopupTransition } from 'components/@extended/Transitions';
|
||||
// import { useDispatch } from 'react-redux';
|
||||
// import { openSnackbar } from 'store/reducers/snackbar';
|
||||
// assets
|
||||
import { DeleteFilled, NotificationOutlined } from '@ant-design/icons';
|
||||
var utc = require('dayjs/plugin/utc');
|
||||
// import { groupBy } from "core-js/actual/array/group-by";
|
||||
// import "lodash.chunk";
|
||||
// var chunk = require('lodash.chunk');
|
||||
import {
|
||||
Grid,
|
||||
Typography,
|
||||
@@ -79,13 +55,8 @@ import { PopupTransition } from 'components/@extended/Transitions';
|
||||
import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined';
|
||||
import MainCard from 'components/MainCard';
|
||||
import Loader from 'components/Loader';
|
||||
// import AlertCustomerDelete from 'sections/apps/customer/AlertCustomerDelete';
|
||||
import dayjs from 'dayjs';
|
||||
dayjs.extend(utc);
|
||||
// import { Link as RouterLink } from 'react-router-dom';
|
||||
// import PlayCircleFilled from '@mui/icons-material/PlayCircleFilled';
|
||||
// import SmileFilled from '@mui/icons-material/Mood';
|
||||
// import HeartFilled from '@mui/icons-material/Favorite';
|
||||
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
@@ -181,23 +152,6 @@ const Details = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
// const fetchorderdetails = async () => {
|
||||
// setLoading(true);
|
||||
// await axios
|
||||
// .get(`${process.env.REACT_APP_URL}/orders/orderbyid/?orderheaderid=${orderheaderid}`)
|
||||
|
||||
// .then((res) => {
|
||||
// console.log(res);
|
||||
|
||||
// setLoading(false);
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// console.log(err);
|
||||
// setLoading(false);
|
||||
// });
|
||||
|
||||
// };
|
||||
|
||||
const fetchorderaddons = async () => {
|
||||
setLoading(true);
|
||||
await axios
|
||||
@@ -249,21 +203,6 @@ const Details = () => {
|
||||
console.log('res');
|
||||
console.log(res);
|
||||
setOrderarr(res.data.Details || []);
|
||||
// let result = res.data.Details.find((res1) => res1.orderheaderid == searchParams.get('id'))
|
||||
// orderheaderid
|
||||
// console.log(result)
|
||||
// setOrderaddons(result.orderaddons);
|
||||
// setVenuetype(result.venuetype)
|
||||
// setOtherinstructions(result.remarks)
|
||||
// console.log("res");
|
||||
// let result = _.chain(res.data.Details)
|
||||
|
||||
// .groupBy("shiftid")
|
||||
|
||||
// .map((value, key) => ({shiftid:key, locationaddress: value[0].locationaddress, roles: value }))
|
||||
// .value()
|
||||
|
||||
// setcategoryarr(result);
|
||||
console.log('categoryarr');
|
||||
setcategoryarr(res.data.Details);
|
||||
console.log(res.data.Details);
|
||||
@@ -305,15 +244,7 @@ const Details = () => {
|
||||
const cancelorder = async () => {
|
||||
await axios
|
||||
.put(`${process.env.REACT_APP_URL2}/orders/cancel`, {
|
||||
// "Orderheaderid": parseInt(orderheaderid),
|
||||
// "Tenantid": parseInt(tenantid),
|
||||
// "Orderstatus": "cancelled",
|
||||
// "Currentdatetime": dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
// "Cod": false,
|
||||
// "Remarks": "",
|
||||
orderheaderid: parseInt(orderheaderid),
|
||||
// "orderdetailid":78,
|
||||
// "shiftid":788,
|
||||
orderstatus: 'cancelled',
|
||||
cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
unserviceable: invoiceeligible ? 0 : 1
|
||||
@@ -443,20 +374,10 @@ const Details = () => {
|
||||
fetchorderaddons();
|
||||
fetchorderattires();
|
||||
fetchassignedcount();
|
||||
// fetchuserdetails();
|
||||
console.log(location.state || '');
|
||||
// setOrderid(location.state.orderid || '');
|
||||
// setEventlocation(location.state.eventlocation || '');
|
||||
// setEventlocation(address || []);
|
||||
|
||||
// setOrderdate(dayjs(location.state.orderdate.substring(0, 10)).format('MM/DD/YYYY') || '');
|
||||
// setDuedate(dayjs(location.state.orderdate.substring(0, 10)).format('MM/DD/YYYY') || '')
|
||||
// setEventname(location.state.eventname || '');
|
||||
// setClientname(location.state.tenantname || '')
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
// fetchorderdetails();
|
||||
console.log(orderheaderid, tenantid);
|
||||
}, [orderheaderid, tenantid, assignedpendingcount]);
|
||||
|
||||
@@ -522,12 +443,6 @@ const Details = () => {
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
if (res.data.message === 'Successfully created') {
|
||||
// if (orderheaderid && tenantid) {
|
||||
// fetchorderdetails();
|
||||
// fetchorderaddons();
|
||||
// fetchorderattires();
|
||||
// }
|
||||
|
||||
enqueueSnackbar('Roles assigned successfully', {
|
||||
variant: 'success',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
@@ -631,32 +546,6 @@ const Details = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// const updateorderstatus = async () => {
|
||||
|
||||
// await axios.put(`${process.env.REACT_APP_URL2}/orders/updateorderstatus`,{
|
||||
// "orderheaderid":orderheaderid,
|
||||
// "tenantid":tenantid,
|
||||
// "orderstatus":"processing",
|
||||
// "pending":"",
|
||||
// "processing":dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
// "completed":""
|
||||
// })
|
||||
// .then((res) => {
|
||||
// console.log(res)
|
||||
// fetchorderdetails();
|
||||
// fetchorderaddons();
|
||||
// fetchorderattires();
|
||||
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// console.log(err)
|
||||
// fetchorderdetails();
|
||||
// fetchorderaddons();
|
||||
// fetchorderattires();
|
||||
// })
|
||||
|
||||
// }
|
||||
|
||||
const fetchassignedcount = async () => {
|
||||
// console.log(obj1)
|
||||
await axios
|
||||
@@ -895,7 +784,7 @@ const Details = () => {
|
||||
{stafflist.map((val, i) => {
|
||||
const isSelected = staffarr.find((res) => res.userid == val.userid) ? true : false;
|
||||
return (
|
||||
<MobileCard key={i} accent="#662582" selected={isSelected}>
|
||||
<MobileCard key={i} accent="#C01227" selected={isSelected}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Avatar alt="" src={''} sx={{ width: 32, height: 32 }} />
|
||||
@@ -1549,14 +1438,14 @@ const Details = () => {
|
||||
{isMobile ? (
|
||||
<MobileCardList>
|
||||
{val5.orderdetails.length === 0 && (
|
||||
<MobileCard accent="#662582">
|
||||
<MobileCard accent="#C01227">
|
||||
<Skeleton animation="wave" />
|
||||
<Skeleton animation="wave" />
|
||||
<Skeleton animation="wave" />
|
||||
</MobileCard>
|
||||
)}
|
||||
{val5.orderdetails.map((row, i) => (
|
||||
<MobileCard key={i + 1} accent="#662582" sx={{ opacity: row.status == 0 ? '' : '0.7' }}>
|
||||
<MobileCard key={i + 1} accent="#C01227" sx={{ opacity: row.status == 0 ? '' : '0.7' }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
|
||||
<Stack direction="column">
|
||||
<Typography sx={{ fontSize: 10, fontWeight: 800, color: '#94a3b8' }}>#{i + 1}</Typography>
|
||||
|
||||