Compare commits

...

10 Commits

84 changed files with 795 additions and 5675 deletions

View File

@@ -5,4 +5,8 @@ REACT_APP_STAFF_TOKEN=
REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk
GENERATE_SOURCEMAP=false
DISABLE_ESLINT_PLUGIN=true

View File

@@ -1,3 +1,6 @@
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
REACT_APP_URL2=
REACT_APP_STAFF_TOKEN=
REACT_APP_URL=https://jupiter.nearle.app/live/api/v1
REACT_APP_URL2=
REACT_APP_STAFF_TOKEN=
GENERATE_SOURCEMAP=false
DISABLE_ESLINT_PLUGIN=true

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

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

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -10,6 +10,7 @@ import { useNavigate } from 'react-router';
import { useEffect } from 'react';
import { generateToken, initFirebaseNotificationListener } from 'firebase_notification/notification';
import InternetStatus from 'components/updateNetworkStatus';
import useInactivityLogout from 'hooks/useInactivityLogout';
// auth-provider
// import { JWTProvider as AuthProvider } from 'contexts/JWTContext';
@@ -25,6 +26,8 @@ const App = () => {
}, [navigate]);
const AppContent = () => {
useInactivityLogout();
useEffect(() => {
generateToken();
initFirebaseNotificationListener();

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -234,13 +234,27 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
...(drawerOpen && {
'&:hover': {
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter'
bgcolor: '#7b1fa2'
bgcolor: '#fff',
'& .MuiTypography-root': {
color: '#C01227 !important'
},
'& svg, & .anticon': {
color: '#C01227 !important'
}
},
'&.Mui-selected': {
bgcolor: 'transparent',
color: iconSelectedColor,
'&:hover': { color: iconSelectedColor, bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'transparent' }
'&:hover': {
bgcolor: '#fff',
color: '#C01227',
'& .MuiTypography-root': {
color: '#C01227 !important'
},
'& svg, & .anticon': {
color: '#C01227 !important'
}
}
}
}),
...(!drawerOpen && {
@@ -273,20 +287,23 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
alignItems: 'center',
justifyContent: 'center',
'&:hover': {
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'secondary.light' : 'secondary.lighter'
bgcolor: '#7b1fa2',
color: 'white'
bgcolor: '#fff',
'& svg': {
color: '#C01227 !important'
}
}
}),
...(!drawerOpen &&
selected === menu.id && {
bgcolor: 'primary.light',
color: 'primary.main',
'&:hover': {
bgcolor: '#7b1fa2',
color: 'primary.main'
bgcolor: 'primary.light',
color: 'primary.main',
'&:hover': {
bgcolor: '#fff',
'& svg': {
color: '#C01227 !important'
}
})
}
})
}}
>
{menuIcon}

View File

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

View File

@@ -10,8 +10,12 @@ 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'
// doormile-logo.png is a white asset; this recolours it to brand red (#C01227) for the light sidebar background.
const DOORMILE_RED_FILTER = 'brightness(0) saturate(100%) invert(14%) sepia(85%) saturate(4649%) hue-rotate(345deg) brightness(97%) contrast(92%)';
// ==============================|| DRAWER HEADER ||============================== //
const DrawerHeader = ({ open }) => {
@@ -37,15 +41,13 @@ const DrawerHeader = ({ open }) => {
{(open) &&
<img src={logo}
// width='160px'
// height='45px'
// width='170px'
alt='logo'/>
style={{ height: '29px', width: 'auto' }}
alt='Doormile'/>
}
{(!open) &&
<img src={logo1}
width='40px'
alt='logo'/>
<img src={logo1}
width='50px'
alt='Doormile'/>
}
</DrawerHeaderStyled>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ const icons = {
const nearle = {
id: 'nearle_Pages',
title: <FormattedMessage id="Nearle" />,
title: <FormattedMessage id="Doormile" />,
icon: icons.FileDoneOutlined,
type: 'group',
children: [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -119,7 +119,7 @@ const pillFieldSx = (color) => ({
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
},
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: color }
});
@@ -596,7 +596,7 @@ const Clients1 = () => {
setLocoName={setLocoName}
setPage={setPage}
pill
accentColor="#662582"
accentColor="#C01227"
icon={<MdMyLocation size={14} />}
placeholder="Select Zone"
paperComponent={SoftPaper}
@@ -622,7 +622,7 @@ const Clients1 = () => {
title={item.label}
value={value ?? 0}
icon={<Icon size={20} />}
color={idx === 0 ? '#662582' : item.color}
color={idx === 0 ? '#C01227' : item.color}
loading={summaryDataIsLoading}
/>
</Box>
@@ -749,11 +749,11 @@ const Clients1 = () => {
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>
@@ -780,9 +780,9 @@ const Clients1 = () => {
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
'&::-webkit-scrollbar': { width: '10px', height: '10px' },
'&::-webkit-scrollbar-thumb': {
backgroundColor: edge('#662582'),
backgroundColor: edge('#C01227'),
borderRadius: '8px',
'&:hover': { backgroundColor: '#662582' }
'&:hover': { backgroundColor: '#C01227' }
},
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
@@ -883,7 +883,7 @@ const Clients1 = () => {
</TableCell>
<TableCell>
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color="#662582" size={32}>
<AccentAvatar color="#C01227" size={32}>
<MdPersonPin size={16} />
</AccentAvatar>
<Stack>
@@ -996,10 +996,10 @@ const Clients1 = () => {
<IconButton
size="small"
sx={{
bgcolor: openRowIndex1 === index ? '#662582' : soft('#662582'),
color: openRowIndex1 === index ? '#fff' : '#662582',
border: `1px solid ${edge('#662582')}`,
'&:hover': { bgcolor: '#662582', color: '#fff' }
bgcolor: openRowIndex1 === index ? '#C01227' : soft('#C01227'),
color: openRowIndex1 === index ? '#fff' : '#C01227',
border: `1px solid ${edge('#C01227')}`,
'&:hover': { bgcolor: '#C01227', color: '#fff' }
}}
onClick={() => {
setSelectedCustomer(row);
@@ -1913,7 +1913,7 @@ const Clients1 = () => {
header={
<Stack direction="row" alignItems="flex-start" 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 }}>
@@ -2012,10 +2012,10 @@ const Clients1 = () => {
<IconButton
size="small"
sx={{
bgcolor: expanded ? '#662582' : soft('#662582'),
color: expanded ? '#fff' : '#662582',
border: `1px solid ${edge('#662582')}`,
'&:hover': { bgcolor: '#662582', color: '#fff' }
bgcolor: expanded ? '#C01227' : soft('#C01227'),
color: expanded ? '#fff' : '#C01227',
border: `1px solid ${edge('#C01227')}`,
'&:hover': { bgcolor: '#C01227', color: '#fff' }
}}
onClick={() => {
setSelectedCustomer(row);
@@ -2108,7 +2108,7 @@ const Clients1 = () => {
>
<DialogTitle
sx={{
bgcolor: '#662582',
bgcolor: '#C01227',
color: '#fff',
fontSize: '20px'
}}

View File

@@ -396,7 +396,7 @@ export default function Customers() {
}
};
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 +416,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 +466,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 +491,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>
@@ -535,11 +535,11 @@ export default function Customers() {
rows?.map((row, index) => (
<MobileCard
key={row.customerid || `${row.firstname}-${index}`}
accent="#662582"
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 }}>
@@ -626,9 +626,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 }
}}
@@ -700,7 +700,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>
@@ -829,7 +829,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
}}

View File

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

View File

@@ -126,7 +126,7 @@ const DT = {
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc',
brand: '#662582'
brand: '#C01227'
};
// Quick alpha helpers (hex + percentage suffix). Mirrors the batch-dropdown
@@ -912,7 +912,7 @@ const Deliveries = () => {
setLocoName={setLocoName}
setPage={setPage}
pill
accentColor="#662582"
accentColor="#C01227"
icon={<MdMyLocation size={14} />}
placeholder="Select Zone"
paperComponent={SoftPaper}
@@ -962,7 +962,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 +1125,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 +1442,10 @@ const Deliveries = () => {
overflowX: 'auto',
'&::-webkit-scrollbar': { width: '10px', height: '10px', cursor: 'pointer' },
'&::-webkit-scrollbar-thumb': {
backgroundColor: edge('#662582'),
backgroundColor: edge('#C01227'),
borderRadius: '8px',
cursor: 'pointer',
'&:hover': { backgroundColor: '#662582' }
'&:hover': { backgroundColor: '#C01227' }
},
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
@@ -1562,7 +1562,7 @@ const Deliveries = () => {
<IconButton
size="small"
onClick={(e) => handleMenuOpen(e, row)}
sx={{ borderRadius: 999, bgcolor: tint('#662582'), color: '#662582', border: `1px solid ${edge('#662582')}`, '&:hover': { bgcolor: soft('#662582') } }}
sx={{ borderRadius: 999, bgcolor: tint('#C01227'), color: '#C01227', border: `1px solid ${edge('#C01227')}`, '&:hover': { bgcolor: soft('#C01227') } }}
>
<EditOutlined />
</IconButton>
@@ -1650,7 +1650,7 @@ const Deliveries = () => {
</MobileField>
<MobileField label="Step">
{row.step ? (
<Box sx={{ ...chipSx('#662582'), minWidth: 30, fontWeight: 800 }}>{row.step}</Box>
<Box sx={{ ...chipSx('#C01227'), minWidth: 30, fontWeight: 800 }}>{row.step}</Box>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}></Typography>
)}
@@ -1664,8 +1664,8 @@ const Deliveries = () => {
{isOpen && (
<Box sx={{ mt: 1.5, p: 1.25, borderRadius: 2, bgcolor: DT.surfaceAlt, border: `1px solid ${DT.divider}` }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
<AccentAvatar color="#662582" size={22}><MdInventory2 size={12} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#662582' }}>
<AccentAvatar color="#C01227" size={22}><MdInventory2 size={12} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
Product Details
</Typography>
</Stack>
@@ -2065,9 +2065,9 @@ const Deliveries = () => {
height: 24,
px: 0.875,
borderRadius: 999,
bgcolor: tint('#662582'),
border: `1px solid ${edge('#662582')}`,
color: '#662582',
bgcolor: tint('#C01227'),
border: `1px solid ${edge('#C01227')}`,
color: '#C01227',
fontWeight: 800,
fontSize: 11
}}
@@ -2133,10 +2133,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 +2168,9 @@ const Deliveries = () => {
background: '#fff'
}}
>
<Stack direction="row" alignItems="center" spacing={1} sx={{ px: 2, py: 1.25, borderBottom: `1px solid ${DT.divider}`, bgcolor: tint('#662582') }}>
<AccentAvatar color="#662582"><MdInventory2 size={14} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#662582' }}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ px: 2, py: 1.25, borderBottom: `1px solid ${DT.divider}`, bgcolor: tint('#C01227') }}>
<AccentAvatar color="#C01227"><MdInventory2 size={14} /></AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, letterSpacing: 0.5, textTransform: 'uppercase', color: '#C01227' }}>
Product Details
</Typography>
</Stack>
@@ -2778,14 +2778,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 +2812,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')}` }
}
}}
/>

File diff suppressed because it is too large Load Diff

View File

@@ -80,7 +80,7 @@ import logger from '../../../utils/logger';
// emerald for the actual GPS trail (signals "live / real" data). Per-step
// 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) => {
@@ -2150,19 +2150,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,
@@ -3261,9 +3256,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;
@@ -5730,7 +5729,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 +6328,7 @@ const Dispatch = ({
<div className="da-section">
<div className="da-hero-row">
<div className="da-hero-card">
<div className="da-hero-icon" style={{ background: '#6625821f', color: '#662582' }}>
<div className="da-hero-icon" style={{ background: '#C012271f', color: '#C01227' }}>
<MdOutlineInventory2 />
</div>
<div className="da-hero-value">{analysisFormatNum(fleet.total_orders)}</div>
@@ -6810,7 +6809,7 @@ const Dispatch = ({
<div className="da-pos-modal-title-wrap">
<div
className="da-pos-modal-avatar"
style={{ background: `${riderPositionModal.color || '#662582'}22`, color: riderPositionModal.color || '#662582' }}
style={{ background: `${riderPositionModal.color || '#C01227'}22`, color: riderPositionModal.color || '#C01227' }}
>
<MdTwoWheeler />
</div>

View File

@@ -72,7 +72,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 AccentAvatar = ({ color, selected, size = 24, children }) => (
<Avatar
@@ -396,7 +396,7 @@ const Invoice = () => {
bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}}
/>

View File

@@ -3,7 +3,10 @@ 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 logo_nearle1 from '../../../assets/images/doormile-logo.png';
// doormile-logo.png is a white asset; this recolours it to brand red (#C01227) for the light invoice background.
const DOORMILE_RED_FILTER = 'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)';
import axios from 'axios';
import dayjs from 'dayjs';
import Loader from 'components/Loader';
@@ -191,7 +194,7 @@ const InvoicePreview = () => {
>
<Box sx={{ pt: 0.5 }}>
<Stack direction="row" spacing={2}>
<img src={logo_nearle1} style={{ width: '150px', height: '50px' }} />{' '}
<img src={logo_nearle1} style={{ width: '150px', height: '20px', filter: DOORMILE_RED_FILTER }} />{' '}
</Stack>
{/* <Typography
sx={{ color: theme.palette.primary.main, py: 0.5 }}

View File

@@ -8,13 +8,17 @@ 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();
@@ -96,6 +100,7 @@ const Login = () => {
localStorage.setItem('applocationid', userinfo.applocationid);
localStorage.setItem('userid', userinfo.userid);
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
markSessionStart();
fetchAppLocations(userinfo.userid);
navigate('/nearle/dispatch');
} else {
@@ -117,6 +122,7 @@ const Login = () => {
localStorage.setItem('applocationid', userinfo.applocationid);
localStorage.setItem('userid', userinfo.userid);
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
markSessionStart();
closeGlobalToast(); // to close the pin snackbar
navigate('/nearle/dispatch');
@@ -160,12 +166,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 +208,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 +257,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 +265,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 }}>
@@ -439,7 +442,7 @@ const Login = () => {
component={Link}
href="https://nearle.in"
target="_blank"
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#C01227' } }}
>
&copy; All rights reserved
</Typography>
@@ -448,7 +451,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 +460,7 @@ const Login = () => {
component={Link}
href="https://nearle.in/privacy"
target="_blank"
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#C01227' } }}
>
Privacy Policy
</Typography>

View File

@@ -22,7 +22,10 @@ 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';
@@ -228,7 +231,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

View File

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

View File

@@ -26,6 +26,7 @@ import {
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 { GiDoorHandle } from 'react-icons/gi';
@@ -158,9 +159,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>
@@ -365,130 +366,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);
@@ -1492,7 +1388,7 @@ const Createorder1 = () => {
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchOutlined style={{ fontSize: 15, color: '#662582' }} />
<SearchOutlined style={{ fontSize: 15, color: '#C01227' }} />
</InputAdornment>
),
endAdornment: (
@@ -1528,7 +1424,7 @@ const Createorder1 = () => {
InputProps={{
startAdornment: (
<InputAdornment position="start">
<FaLocationDot style={{ fontSize: 14, color: '#662582' }} />
<FaLocationDot style={{ fontSize: 14, color: '#C01227' }} />
</InputAdornment>
),
endAdornment: (
@@ -2270,7 +2166,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 +2345,7 @@ const Createorder1 = () => {
}
}}
>
<DialogTitle sx={{ background: 'linear-gradient(135deg, #662582 0%, #9255AB 100%)', color: 'white', py: 2.5 }}>
<DialogTitle sx={{ background: 'linear-gradient(135deg, #C01227 0%, #D35968 100%)', color: 'white', py: 2.5 }}>
<Stack spacing={1.5}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'white' }}>
{`Select Saved Address (${pickordrop === 1 ? 'Pickup' : 'Drop'})`}

View File

@@ -895,7 +895,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 +1549,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>

View File

@@ -7,6 +7,7 @@ import { useNavigate } from 'react-router';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import { enqueueSnackbar } from 'notistack';
import { calculateDrivingDistance, calculateTotalCharge } from '../../../utils/distance';
import {
FormControl,
@@ -296,53 +297,17 @@ const MultipleOrders = () => {
settotalCash(a4);
}, [dropCust]);
// ============================== distance (Google) ==============================
// ============================== distance (OSRM/Haversine) ==============================
const calculateDistance = async (customer) => {
if (typeof window === 'undefined' || !window.google?.maps?.DistanceMatrixService) {
throw new Error('Google Maps not loaded');
}
const service = new window.google.maps.DistanceMatrixService();
const getDistanceMatrix = (origins, destinations) =>
new Promise((resolve, reject) => {
try {
if (!origins || !destinations) return reject(new Error('Origin or destination data missing.'));
service.getDistanceMatrix(
{
origins: [new window.google.maps.LatLng(origins.latitude, origins.longitude)],
destinations: [new window.google.maps.LatLng(destinations.latitude, destinations.longitude)],
travelMode: 'DRIVING',
unitSystem: window.google.maps.UnitSystem.METRIC
},
(response, status) => {
if (status === 'OK') resolve(response);
else reject(new Error(`Google API error: ${status}`));
}
);
} catch (err) {
reject(new Error(`Unexpected error inside DistanceMatrixService: ${err.message}`));
}
});
try {
if (!customer || typeof customer !== 'object') throw new Error('Invalid customer data.');
if (!pickCust || typeof pickCust !== 'object') throw new Error('Origin (pickCust) data missing or invalid.');
const response = await getDistanceMatrix(pickCust, customer);
const distVal = response?.rows?.[0]?.elements?.[0]?.distance?.value;
if (distVal == null) throw new Error('Malformed Distance Matrix response: missing distance value.');
const km = distVal / 1000;
const roundedDistance = Math.round(km);
const totalcharge =
roundedDistance < minKm ? basePrice : (roundedDistance - minKm) * pricePerKm + basePrice;
const roundedDistance = await calculateDrivingDistance(pickCust, customer);
const totalcharge = calculateTotalCharge(roundedDistance, basePrice, pricePerKm, minKm);
return { roundedDistance, totalcharge };
} catch (error) {
if (error.message.includes('Google API')) {
OpenToast('Invalid coordinates or Google API error.', 'error', 3000);
} else if (error.message.includes('Malformed Distance Matrix')) {
OpenToast('Google Distance Matrix returned invalid data.', 'error', 3000);
} else if (error.message.includes('Origin') || error.message.includes('customer')) {
if (error.message.includes('Origin') || error.message.includes('customer') || error.message.includes('coordinates')) {
OpenToast('Missing or invalid input data for distance calculation.', 'warning', 3000);
} else {
OpenToast('Unexpected error during distance calculation.', 'error', 3000);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,741 +0,0 @@
import {
Autocomplete,
Button,
Chip,
Divider,
Grid,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Tooltip,
Typography,
Backdrop,
IconButton,
Badge,
Accordion,
AccordionSummary,
AccordionActions,
AccordionDetails,
useMediaQuery
} from '@mui/material';
import React, { Fragment, useEffect, useMemo, useState } from 'react';
import { useTheme } from '@mui/material/styles';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import { useLocation, useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
import MainCard from 'components/MainCard';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { createAutomationDeliveries, fetchPaymentType, fetchRidersList, finalCreatedeliveries, notifyRider } from 'pages/api/api';
import { OpenToast } from 'components/third-party/OpenToast';
import { useMutation, useQuery } from '@tanstack/react-query';
import Loader from 'components/Loader';
import CircularLoader from 'components/CircularLoader';
import { Empty } from 'antd';
import HoverSocialCard from 'components/cards/statistics/HoverSocialCard';
import { DashboardFilled, DashboardOutlined, OpenAIFilled } from '@ant-design/icons';
import { MdDirectionsBike } from 'react-icons/md';
import { FaMapLocationDot } from 'react-icons/fa6';
import { HiOutlineArrowLeft } from 'react-icons/hi';
import { IoReload } from 'react-icons/io5';
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CheckBoxIcon from '@mui/icons-material/CheckBox';
import { GiProfit } from 'react-icons/gi';
import { ArrowRightAltOutlined } from '@mui/icons-material';
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
import WarningIcon from '@mui/icons-material/Warning';
import BoltIcon from '@mui/icons-material/Bolt';
import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee';
import SavingsIcon from '@mui/icons-material/Savings';
import EnergySavingsLeafIcon from '@mui/icons-material/EnergySavingsLeaf';
import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlined';
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
import { exportToExcel } from 'components/exportToExcel';
import CSVExport from 'components/third-party/ReactTable';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
const OptimisedOrderPreview = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const navigate = useNavigate();
const location = useLocation();
console.log('location.state', location.state);
const [rider, setRider] = useState(null);
const [payment, setPayment] = useState(null);
const [finaldeliveryList, setFinalDeliveryList] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [deliverylist, setDeliverylist] = useState(location.state?.deliverylist);
const [zoneData, setZoneData] = useState(location?.state?.zoneData);
const [metaData, setMetaData] = useState(location.state?.metaData);
const riderToken = location.state?.riderToken;
const appId = location.state?.appId;
const aiMode = location.state?.aiMode;
const reassignOrders = location.state?.reassignOrders;
const [csvExportData, setCsvExportData] = useState([]);
useEffect(() => {
console.log('called');
console.log('aiMode', aiMode);
console.log('riderToken', riderToken);
console.log('zoneData', zoneData);
console.log('metaData', metaData);
console.log('reassignOrders', reassignOrders);
}, [zoneData, metaData, deliverylist]);
useEffect(() => {
const filtered = finaldeliveryList.map((item) => ({
zone_name: item.zone_name,
ordernotes: item.ordernotes,
rider: item.rider,
step: item.step,
ordertype: item.ordertype,
orderamount: item.orderamount,
riderkms: item.riderkms,
cumulativekms: item.cumulativekms,
baseprice: item.baseprice,
minkm: item.minkm,
priceperkm: item.priceperkm,
kms: item.kms,
actualkms: item.actualkms,
rider_charge: item.rider_charge,
deliveryamt: item.deliveryamt,
deliverycharges: item.deliverycharges,
profit: item.profit
}));
setCsvExportData(filtered);
console.log('csvExportData', filtered);
}, [finaldeliveryList]);
useEffect(() => {
if (!deliverylist?.length) return;
const updateDeliveryAmtList = deliverylist.map((list) => {
const cumulativeKms = Number(list.cumulativekms || 0);
const minKm = Number(list.minkm || 0);
const basePrice = Number(list.baseprice || 0);
const pricePerKm = Number(list.priceperkm || 0);
if (cumulativeKms <= minKm) {
return {
...list,
deliveryamt: basePrice
};
}
return {
...list,
deliveryamt: (cumulativeKms - minKm) * pricePerKm + basePrice
};
});
setFinalDeliveryList(updateDeliveryAmtList);
console.log('finaldeliveryList', updateDeliveryAmtList);
}, [deliverylist]);
// ==============================|| fetchPaymentType ||============================== //
const {
data: paymentModes = [],
isLoading: paymentModesLoading,
isError: paymentModesError,
error: paymentModesErrorMessage
} = useQuery({
queryKey: ['paymentmodes'],
queryFn: fetchPaymentType
});
// ==============================|| fetchRidersList ||============================== //
const {
data: ridersList = [],
isLoading: ridersListLoading,
isError: ridersListError,
error: ridersListErrorMessage,
refetch: ridersListRefetch
} = 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({
mutationFn: notifyRider, // Using the separate function
onSuccess: () => {
OpenToast('Notification sent Successfully', 'success', 2000);
},
onError: (error) => {
OpenToast(error.message, 'error', 2000);
}
});
const createFinalDeliveryMutation = useMutation({
mutationFn: finalCreatedeliveries, // for optimised delivery create
onSuccess: (data, variables) => {
console.log('data', data);
console.log('varialbles', variables);
notifyRiderMutation.mutate(rider.userfcmtoken || riderToken); // Call notifyRider after success
if (data.status == 'accepted') {
OpenToast('Delivery Created Successfully', 'success', 2000);
}
},
onError: (error) => {
OpenToast(error.message, 'error', 4000);
},
onSettled: () => {
setTimeout(() => {
setIsLoading(false);
navigate('/nearle/deliveries');
}, 2000);
}
});
const handleFinalCreateDelivery = async () => {
// console.log('Final delivery list:', finaldeliveryList);
setIsLoading(true);
createFinalDeliveryMutation.mutate({
deliveries: finaldeliveryList
});
};
const createDeliveryMutation = useMutation({
// 0 -> manual rider assign 1 -> ai rider assign
mutationFn: createAutomationDeliveries,
onSuccess: (data, variables) => {
console.log('data', data);
console.log('varialbles', variables);
OpenToast('Orders Optimised Successfully', 'success', 2000);
// notifyRiderMutation.mutate(variables?.riderToken); // Call notifyRider after success
setZoneData(data?.zones);
setDeliverylist(data?.details);
setMetaData(data?.meta);
setIsLoading(false);
},
onError: (error) => {
OpenToast(error.message, 'error', 4000);
setIsLoading(false);
},
onSettled: () => {
setIsLoading(false);
}
});
const tuningTypes = [
{
tuneid: 1,
type: 'Rider'
},
{
tuneid: 2,
type: 'Pickup'
},
{
tuneid: 3,
type: 'Zone'
}
];
return (
<Fragment>
{
<Backdrop
sx={{
color: '#fff',
zIndex: (theme) => theme.zIndex.drawer + 1
}}
open={paymentModesLoading || ridersListLoading || isLoading} // when loader = true, backdrop covers the page
>
<CircularLoader color="inherit" />
</Backdrop>
}
{(paymentModesLoading || ridersListLoading || isLoading) && (
<>
<Loader />
<CircularLoader />
</>
)}
<Stack
direction={{ xs: 'column', md: 'row' }}
alignItems={{ xs: 'stretch', md: 'center' }}
justifyContent="space-between"
spacing={{ xs: 1.5, md: 0 }}
sx={{}}
>
<Stack direction="row" alignItems="center" spacing={1}>
<Tooltip title="Back to orders" placement="top">
<IconButton
onClick={() => navigate('/nearle/orders')}
sx={{
bgcolor: 'action.hover',
'&:hover': { bgcolor: 'action.selected' }
}}
>
<HiOutlineArrowLeft size={20} />
</IconButton>
</Tooltip>
<Typography variant="h3" fontWeight={600}>
Assign Orders
</Typography>
</Stack>
{/* <Button
variant="contained"
color="primary"
startIcon={<IoReload />}
onClick={() => {
setIsLoading(true);
createDeliveryMutation.mutate({ deliveries: reassignOrders });
}}
>
Tuning
</Button> */}
<Autocomplete
multiple
id="checkboxes-tags-demo"
options={tuningTypes}
disableCloseOnSelect
getOptionLabel={(option) => option.type}
renderOption={(props, option, { selected }) => {
const { id, ...optionProps } = props;
const SelectionIcon = selected ? CheckBoxIcon : CheckBoxOutlineBlankIcon;
return (
<li key={id} {...optionProps}>
<SelectionIcon fontSize="small" style={{ marginRight: 8, padding: 9, boxSizing: 'content-box' }} />
{option.type}
</li>
);
}}
sx={{ width: { xs: '100%', md: 400 } }}
renderInput={(params) => (
<TextField
{...params}
sx={{
'& .MuiInputBase-input': {
lineHeight: '1.5' // 👈 important
}
}}
label="Hyper Tuning"
// placeholder="Favorites"
/>
)}
/>
{/* <Button
variant="outlined"
size="small"
startIcon={<DownloadOutlinedIcon />}
onClick={() => {
exportToExcel(finaldeliveryList, 'users', 'UsersData');
}}
>
Export Report
</Button> */}
<CSVExport
data={csvExportData}
filename={`Orders_Detail_${dayjs().format('YYYY-MM-DD_HHmmss')}.csv`}
label=" CSV"
style={{ m: 1 }}
/>
</Stack>
<Stack sx={{ my: 2 }}>
<Grid container spacing={2}>
<Grid item xs={6} sm={6} md={3}>
<HoverSocialCard
secondary={metaData?.total_orders}
primary={'Orders'}
percentage={<DashboardFilled />}
color={theme.palette.success.main}
sx={{ cursor: 'pointer' }}
/>
</Grid>
<Grid item xs={6} sm={6} md={3}>
<HoverSocialCard
secondary={metaData?.utilized_riders}
primary={'Riders'}
percentage={<MdDirectionsBike />}
color={theme.palette.warning.main}
/>
</Grid>
<Grid item xs={6} sm={6} md={3}>
<HoverSocialCard
secondary={zoneData?.length}
primary={'Zones'}
percentage={<FaMapLocationDot />}
color={theme.palette.info.main}
/>
</Grid>
<Grid item xs={6} sm={6} md={3}>
<HoverSocialCard
secondary={metaData?.total_profit}
primary={'Profit'}
percentage={<GiProfit />}
color={theme.palette.error.main}
/>
</Grid>
</Grid>
</Stack>
<MainCard content={false}>
{zoneData?.map((zone, index) => {
const riders = zone?.riders.flatMap((rider) => rider);
const orders = zone?.riders?.flatMap((rider) => rider.orders);
return (
<Accordion key={index}>
<AccordionSummary>
<Stack direction="row" alignItems="center" spacing={2} sx={{ m: 2 }}>
<Typography variant="h5" fontWeight={1000}>
Zone {index + 1} :
</Typography>
<Chip label={zone.zone_name} color="primary" variant="light" size="small" sx={{ minWidth: 100 }} />
<Tooltip title={`Orders`} placement="top">
<Badge badgeContent={zone.total_orders} color="secondary">
<DashboardOutlined style={{ fontSize: 20 }} />
</Badge>
</Tooltip>
<Tooltip title={`Riders`} placement="top">
<Badge color="secondary" badgeContent={zone.active_riders_count}>
<DirectionsBikeOutlinedIcon style={{ fontSize: 20 }} />
</Badge>
</Tooltip>
</Stack>
</AccordionSummary>
<AccordionDetails sx={{ p: { xs: 0, md: 1 } }}>
{isMobile ? (
<MobileCardList>
{orders.map((val, i) => {
const typeAccent =
val.ordertype === 'Economy' ? '#10b981' : val.ordertype === 'Risky' ? '#ef4444' : '#662582';
return (
<MobileCard key={i} accent={typeAccent}>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<Chip size="small" label={`#${i + 1}`} variant="light" color="primary" />
<Typography variant="subtitle2" noWrap sx={{ minWidth: 0 }}>
{`${val.locationname}-(${val.locationsuburb})`}
</Typography>
</Stack>
<Chip
size="small"
label={val.ordertype}
icon={
val.ordertype === 'Economy' ? (
<EnergySavingsLeafIcon />
) : val.ordertype === 'Risky' ? (
<WarningIcon />
) : (
<BoltIcon />
)
}
color={val.ordertype === 'Economy' ? 'success' : val.ordertype === 'Risky' ? 'error' : 'primary'}
variant="light"
/>
</Stack>
<MobileFieldGrid>
<MobileField label="Order Id" value={val.orderid} />
<MobileField label="Rider">
<Typography sx={{ fontSize: 13, fontWeight: 600 }} noWrap>
{val.rider}
</Typography>
<Typography variant="caption">ID : {val.userid}</Typography>
</MobileField>
<MobileField label="Pickup">
<Stack spacing={0.25} sx={{ minWidth: 0 }}>
<Typography variant="caption" noWrap>
{val.pickupcustomer}
</Typography>
<Typography variant="caption" noWrap>
{val.pickupcontactno}
</Typography>
<Tooltip title={val.pickupaddress}>
<Typography variant="caption" noWrap sx={{ cursor: 'pointer' }}>
{val.pickupsuburb || val.pickupaddress.slice(0, 20)}
</Typography>
</Tooltip>
<Chip
size="small"
label={dayjs(val.pickupslot).format('DD/MM/YYYY hh:mm A')}
variant="light"
sx={{ color: '#0a803b', bgcolor: '#c3f3c7', alignSelf: 'flex-start' }}
/>
</Stack>
</MobileField>
<MobileField label="Delivery">
<Stack spacing={0.25} sx={{ minWidth: 0 }}>
<Typography variant="caption" noWrap>
{val.deliverycustomer}
</Typography>
<Typography variant="caption" noWrap>
{val.deliverycontactno}
</Typography>
<Tooltip title={val.deliveryaddress}>
<Typography variant="caption" noWrap sx={{ cursor: 'pointer' }}>
{val.deliverysuburb || val.deliveryaddress.slice(0, 20)}
</Typography>
</Tooltip>
<Chip
size="small"
label={dayjs(val.expecteddeliverytime).format('DD/MM/YYYY hh:mm A')}
variant="light"
sx={{ color: '#DD2C00', background: '#FBE9E7', alignSelf: 'flex-start' }}
/>
</Stack>
</MobileField>
{val.ordernotes ? <MobileField full label="Notes" value={val.ordernotes} /> : null}
<MobileField label="Profit">
<Chip
size="small"
label={`${parseFloat(val?.profit).toFixed(2)}`}
variant="light"
sx={{ color: '#009688', bgcolor: '#b2dfdb' }}
/>
</MobileField>
<MobileField label="Charges">
<Chip
size="small"
label={`${val.deliverycharge.toFixed(2)} `}
variant="light"
sx={{ color: '#0074e7', bgcolor: '#cce3fa' }}
/>
</MobileField>
<MobileField label="KMS">
<Chip size="small" label={`${val.kms} km`} color="error" variant="light" />
</MobileField>
<MobileField label="Cumulative KMS">
<Chip size="small" label={`${val.cumulativekms} km`} color="success" variant="light" />
</MobileField>
</MobileFieldGrid>
</MobileCard>
);
})}
</MobileCardList>
) : (
<TableContainer sx={{ p: 0, m: 0 }}>
<Table stickyHeader>
<TableHead>
<TableRow>
<TableCell>#</TableCell>
{/* <TableCell>Tenant</TableCell> */}
<TableCell>Order Location</TableCell>
<TableCell>Pickup</TableCell>
<TableCell>Delivery</TableCell>
<TableCell>Notes</TableCell>
<TableCell>Rider</TableCell>
<TableCell align="center">Type</TableCell>
<TableCell align="center">Profit</TableCell>
<TableCell align="center">Charges</TableCell>
<TableCell align="center">KMS</TableCell>
</TableRow>
</TableHead>
<TableBody>
{orders.map((val, i) => (
<Fragment key={i}>
<TableRow sx={{}}>
<TableCell>
<Typography> {i + 1}</Typography>
</TableCell>
{/* <TableCell>
<Tooltip title={val.tenantaddress}>
<Typography variant="body1" noWrap>
{val.tenantname}
</Typography>
<Typography noWrap sx={{ fontSize: '11px' }}>
{val.tenantsuburb}
<br />
</Typography>
<Typography noWrap variant="body2">
{val.applocation}
</Typography>
</Tooltip>
</TableCell> */}
<TableCell align="left">
<Tooltip title={val.locationaddress} placement="top">
<Typography variant="body1" noWrap>
{`${val.locationname}-(${val.locationsuburb})`}
</Typography>
</Tooltip>
<Tooltip title="Order Id">
<Typography variant="body2" noWrap>
{val.orderid}
</Typography>
</Tooltip>
<Stack display={'flex'} flexDirection={'row'} gap={3} sx={{ cursor: 'pointer' }}>
<Tooltip title="Pickup Time">
<Typography noWrap sx={{ fontSize: '12px' }}>
{dayjs(val.pickupslot).format('DD/MM/YYYY')}
</Typography>
<Chip
size="small"
label={dayjs(val.pickupslot).format('hh:mm A')}
variant="light"
sx={{ color: '#0a803b', bgcolor: '#c3f3c7' }}
/>
</Tooltip>
<ArrowRightAltOutlined />
<Tooltip title="Estimated Delivery time">
<Typography noWrap sx={{ fontSize: '12px' }}>
{dayjs(val.expecteddeliverytime).format('DD/MM/YYYY')}
</Typography>
<Chip
size="small"
label={dayjs(val.expecteddeliverytime).format('hh:mm A')}
variant="light"
sx={{ color: '#DD2C00', background: '#FBE9E7' }}
/>
</Tooltip>
</Stack>
</TableCell>
<TableCell align="left">
<Stack direction={'row'} spacing={1}>
<Stack direction="column">
<Typography variant="caption">{val.pickupcustomer}</Typography>
<Typography variant="caption">{val.pickupcontactno}</Typography>
<Tooltip title={val.pickupaddress}>
<Typography sx={{ cursor: 'pointer' }} variant="caption">
{val.pickupsuburb || val.pickupaddress.slice(0, 20)}
</Typography>
</Tooltip>
</Stack>
</Stack>
</TableCell>
<TableCell align="left">
<Stack direction={'row'} spacing={1}>
<Stack direction="column">
<Typography variant="caption">{val.deliverycustomer}</Typography>
<Typography variant="caption">{val.deliverycontactno}</Typography>
<Tooltip title={val.deliveryaddress}>
<Typography sx={{ cursor: 'pointer' }} variant="caption">
{val.deliverysuburb || val.deliveryaddress.slice(0, 20)}
</Typography>
</Tooltip>
</Stack>
</Stack>
</TableCell>
<TableCell align="left">
<Typography variant="caption" sx={{ whiteSpace: 'nowrap' }}>
{val.ordernotes}
</Typography>
</TableCell>
<TableCell align="left">
<Typography variant="caption" sx={{ whiteSpace: 'nowrap' }}>
{val.rider}
</Typography>
<br />
<Typography variant="caption">ID : {val.userid}</Typography>
</TableCell>
<TableCell align="center">
<Chip
size="small"
label={val.ordertype}
icon={
val.ordertype === 'Economy' ? (
<EnergySavingsLeafIcon />
) : val.ordertype === 'Risky' ? (
<WarningIcon />
) : (
<BoltIcon />
)
}
color={val.ordertype === 'Economy' ? 'success' : val.ordertype === 'Risky' ? 'error' : 'primary'}
variant="light"
/>
</TableCell>
<TableCell align="center">
<Chip
size="small"
label={`${parseFloat(val?.profit).toFixed(2)}`}
variant="light"
sx={{ color: '#009688', bgcolor: '#b2dfdb' }}
/>
</TableCell>
<TableCell align="center">
<Stack display={'flex'} flexDirection={'column'} gap={1} sx={{ cursor: 'pointer' }}>
<Tooltip title="Charges" placement="top">
<Chip
size="small"
label={`${val.deliverycharge.toFixed(2)} `}
variant="light"
sx={{ color: '#0074e7', bgcolor: '#cce3fa' }}
/>
</Tooltip>
{/* <Tooltip title="Amount" placement="left">
<Chip size="small" label={`₹ ${val.deliveryamt.toFixed(2)} `} color="success" variant="light" />
</Tooltip> */}
</Stack>
</TableCell>
<TableCell align="center">
<Stack display={'flex'} flexDirection={'column'} gap={1} sx={{ cursor: 'pointer' }}>
<Tooltip title="KMS" placement="top">
<Chip size="small" label={`${val.kms} km`} color="error" variant="light" />
</Tooltip>
<Tooltip title="Cumulative Kms" placement="right">
<Chip size="small" label={`${val.cumulativekms} km`} color="success" variant="light" />
</Tooltip>
</Stack>
</TableCell>
</TableRow>
</Fragment>
))}
</TableBody>
</Table>
</TableContainer>
)}
</AccordionDetails>
</Accordion>
);
})}
<Divider />
<Stack
display={'flex'}
flexDirection={{ xs: 'column', sm: 'row' }}
gap={2}
alignItems={{ xs: 'stretch', sm: 'center' }}
justifyContent={'end'}
sx={{ p: 2 }}
>
<Button
sx={{ width: { xs: '100%', sm: 'auto' } }}
variant="contained"
color="secondary"
startIcon={<ArrowBackIcon />}
onClick={() => {
navigate('/nearle/orders');
}}
>
Back
</Button>
<Button
sx={{ my: { xs: 0, sm: 2 }, width: { xs: '100%', sm: 'auto' } }}
variant="contained"
disabled={aiMode === 0 && (!rider || !payment)}
onClick={handleFinalCreateDelivery}
>
Assign Orders
</Button>
</Stack>
</MainCard>
</Fragment>
);
};
export default OptimisedOrderPreview;

View File

@@ -144,7 +144,7 @@ const dtSoft = (c) => dtA(c, '18');
const dtRing = (c) => dtA(c, '26');
const dtEdge = (c) => dtA(c, '55');
const BRAND = '#662582';
const BRAND = '#C01227';
const SoftPaper = (props) => (
<Paper
@@ -181,8 +181,8 @@ const pillFieldSx = () => ({
fontWeight: 600,
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${dtRing('#662582')}` },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }
'&.Mui-focused': { boxShadow: `0 0 0 3px ${dtRing('#C01227')}` },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
}
});
@@ -206,6 +206,16 @@ const ORDERS_STATUS_TABS = [
{ idx: 3, status: 'cancelled', label: 'Cancelled', color: '#ef4444', icon: MdCancel, countKey: 'cancelled' }
];
const formatOrderDate = (row) => {
const dateCandidates = [row.pickupslot, row.deliverydate, row.orderdate];
for (const candidate of dateCandidates) {
if (candidate && dayjs(candidate).isValid()) {
return `${dayjs(candidate).utc().format('DD/MM/YYYY')} ${dayjs(candidate).format('hh:mm A')}`;
}
}
return '—';
};
const Orders = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
@@ -957,7 +967,7 @@ const Orders = () => {
{/* ============================================= || KPI Cards || ============================================= */}
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
{[
{ key: 'created', label: 'Created Orders', color: '#662582', icon: MdOutlineReceiptLong, value: percentageData?.created, percentage: percentageData?.percentage1 },
{ key: 'created', label: 'Created Orders', color: '#C01227', icon: MdOutlineReceiptLong, value: percentageData?.created, percentage: percentageData?.percentage1 },
{ key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdOutlinePendingActions, value: percentageData?.uncoveredOrders, percentage: percentageData?.percentage2 },
{ key: 'delivered', label: 'Delivered Orders', color: '#10b981', icon: MdOutlineCheckCircle, value: percentageData?.coveredOrders, percentage: percentageData?.percentage3 },
{ key: 'cancelled', label: 'Cancelled Orders', color: '#ef4444', icon: MdOutlineCancel, value: percentageData?.cancelled, percentage: percentageData?.percentage4 }
@@ -1269,7 +1279,7 @@ const Orders = () => {
bgcolor: dtTint(BRAND),
'& fieldset': { borderColor: dtEdge(BRAND), borderWidth: 1.5 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${dtRing(BRAND)}` }
}}
/>
@@ -1469,7 +1479,7 @@ const Orders = () => {
{`${row.locationname}-(${row.locationsuburb})`}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.orderid} · {dayjs(row.pickupslot).utc().format('DD/MM/YYYY')} {dayjs(row.pickupslot).format('hh:mm A')}
{row.orderid} · {formatOrderDate(row)}
</Typography>
</MobileField>
<MobileField label="Pickup">
@@ -1691,7 +1701,7 @@ const Orders = () => {
</Tooltip>
<Tooltip title="Pickup Slot">
<Typography noWrap sx={{ fontSize: '12px' }}>
{dayjs(row.pickupslot).utc().format('DD/MM/YYYY')} {dayjs(row.pickupslot).format('hh:mm A')}
{formatOrderDate(row)}
</Typography>
</Tooltip>
</TableCell>

View File

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

View File

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

View File

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

View File

@@ -66,7 +66,7 @@ const tint = (c) => aColor(c, '08');
const edge = (c) => aColor(c, '55');
const ring = (c) => aColor(c, '26');
const BRAND = '#662582';
const BRAND = '#C01227';
const SoftPaper = (props) => (
<Paper
@@ -552,7 +552,7 @@ export default function ProfitabilityReport() {
bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}}
/>

View File

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

View File

@@ -902,7 +902,7 @@ const Requests = () => {
<MobileCardList scroll>
{loading &&
[0, 1, 2, 3, 4].map((item) => (
<MobileCard key={item} accent="#662582">
<MobileCard key={item} accent="#C01227">
<Stack direction="row" alignItems="center" spacing={1}>
<Skeleton variant="circular" width={32} height={32} />
<Stack sx={{ flex: 1 }}>
@@ -923,13 +923,13 @@ const Requests = () => {
return (
<MobileCard
key={row.sno}
accent="#662582"
accent="#C01227"
selected={isItemSelected}
onClick={(event) => handleClick(event, row.sno)}
header={
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
<Avatar sx={{ width: 32, height: 32, bgcolor: '#66258218', color: '#662582', fontSize: 13 }}>
<Avatar sx={{ width: 32, height: 32, bgcolor: '#C0122718', color: '#C01227', fontSize: 13 }}>
{row.requestor ? String(row.requestor).charAt(0).toUpperCase() : '#'}
</Avatar>
<Stack sx={{ minWidth: 0 }}>
@@ -942,7 +942,7 @@ const Requests = () => {
</Stack>
</Stack>
{row.amount != null && (
<Chip label={row.amount} size="small" sx={{ bgcolor: '#66258218', color: '#662582', fontWeight: 700 }} />
<Chip label={row.amount} size="small" sx={{ bgcolor: '#C0122718', color: '#C01227', fontWeight: 700 }} />
)}
</Stack>
}

View File

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

View File

@@ -77,7 +77,7 @@ import RiderSubstitution from './RiderSubstitution';
// ============================================================================
// Design tokens — shared with the deliveries / tenants / customers pages so
// every surface (header, KPI tiles, table, badges, dialog) speaks the same
// visual language. Brand purple `#662582` is the canonical primary; status
// visual language. Brand purple `#C01227` is the canonical primary; status
// colours are semantic and distinct from the brand.
// ============================================================================
const DT = {
@@ -100,7 +100,7 @@ const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55');
const BRAND = '#662582';
const BRAND = '#C01227';
const SoftPaper = (props) => (
<Paper
@@ -763,7 +763,7 @@ const Riders = () => {
bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}}
/>
@@ -813,7 +813,7 @@ const Riders = () => {
bgcolor: BRAND,
color: '#fff',
'&:hover': {
bgcolor: '#4D1C61'
bgcolor: '#900E1D'
}
}
}
@@ -1914,7 +1914,7 @@ const Riders = () => {
fontWeight: 600,
bgcolor: BRAND,
color: '#fff',
'&:hover': { bgcolor: '#4D1C61' }
'&:hover': { bgcolor: '#900E1D' }
}}
>
Save Changes

View File

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

View File

@@ -1,31 +0,0 @@
import React from 'react';
import { useTheme } from '@mui/material/styles';
import { CardActions, Grid, Stack, Typography } from '@mui/material';
const TitleCard = ({ title, secondary, sx }) => {
const theme = useTheme();
return (
<CardActions
sx={{
position: 'sticky',
top: '30px',
bgcolor: theme.palette.background.default,
zIndex: 10,
width: '100%',
...sx
}}
>
<Grid container>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center" gap={2} sx={{ flexWrap: 'wrap' }}>
<Typography variant="h3">{title}</Typography>
{secondary}
</Stack>
</Grid>
</Grid>
</CardActions>
);
};
export default TitleCard;

View File

@@ -121,7 +121,7 @@ export default function ThemeCustomization({ children }) {
// soft slate that tints toward brand purple on hover.
'*': {
scrollbarWidth: 'thin', // Firefox
scrollbarColor: 'rgba(102, 37, 130, 0.28) transparent'
scrollbarColor: 'rgba(192, 18, 39, 0.28) transparent'
},
'*::-webkit-scrollbar': {
width: '10px',
@@ -131,13 +131,13 @@ export default function ThemeCustomization({ children }) {
backgroundColor: 'transparent'
},
'*::-webkit-scrollbar-thumb': {
backgroundColor: 'rgba(102, 37, 130, 0.26)',
backgroundColor: 'rgba(192, 18, 39, 0.26)',
borderRadius: '8px',
border: '2px solid transparent',
backgroundClip: 'padding-box'
},
'*::-webkit-scrollbar-thumb:hover': {
backgroundColor: 'rgba(102, 37, 130, 0.45)'
backgroundColor: 'rgba(192, 18, 39, 0.45)'
},
'*::-webkit-scrollbar-corner': {
backgroundColor: 'transparent'

View File

@@ -23,10 +23,10 @@ export default function Autocomplete() {
borderRadius: 8,
margin: '1px 0',
'&[aria-selected="true"]': {
backgroundColor: 'rgba(102, 37, 130, 0.08)'
backgroundColor: 'rgba(192, 18, 39, 0.08)'
},
'&.Mui-focused': {
backgroundColor: 'rgba(102, 37, 130, 0.06)'
backgroundColor: 'rgba(192, 18, 39, 0.06)'
}
}
},

View File

@@ -40,16 +40,16 @@ const Default = (colors) => {
// contrastText
// },
primary: {
lighter: '#E8D9EF',
100: '#CBA7DA',
200: '#AE76C4',
light: '#9255AB',
400: '#9255AB',
main: '#662582',
dark: '#4D1C61',
700: '#3A1549',
darker: '#260E30',
900: '#17081D',
lighter: '#F6DBDF',
100: '#E9ACB3',
200: '#DD7F8A',
light: '#D35968',
400: '#D35968',
main: '#C01227',
dark: '#900E1D',
700: '#6D0A16',
darker: '#47070E',
900: '#2A0409',
contrastText: '#FFFFFF'
},
secondary: {

65
src/utils/distance.js Normal file
View File

@@ -0,0 +1,65 @@
/**
* Calculates distance (in km) between origin and destination.
* Uses OSRM as primary driving distance source, and falls back to Haversine with a 1.3 multiplier.
*
* @param {Object} origin - { latitude, longitude }
* @param {Object} destination - { latitude, longitude }
* @returns {Promise<number>} - Round distance in KM
*/
export const calculateDrivingDistance = async (origin, destination) => {
const lat1 = origin?.latitude;
const lon1 = origin?.longitude;
const lat2 = destination?.latitude;
const lon2 = destination?.longitude;
if (lat1 == null || lon1 == null || lat2 == null || lon2 == null) {
throw new Error("Invalid coordinates");
}
// 1. Try OSRM API (either self-hosted or public demo server for fallback)
const osrmBaseUrl = process.env.REACT_APP_OSRM_URL || "https://router.project-osrm.org";
try {
const url = `${osrmBaseUrl}/route/v1/driving/${lon1},${lat1};${lon2},${lat2}?overview=false`;
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
if (data.routes && data.routes.length > 0) {
const distanceInMeters = data.routes[0].distance;
const distanceInKm = distanceInMeters / 1000;
return Math.round(distanceInKm);
}
}
} catch (error) {
console.warn("OSRM API failed, falling back to Haversine math:", error);
}
// 2. Fallback: Haversine Formula with 1.3x multiplier
const toRad = (val) => (val * 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));
const aerialDistance = R * c;
return Math.round(aerialDistance * 1.3);
};
/**
* Calculates total charge based on distance and pricing tier
*
* @param {number} distanceKm
* @param {number} basePrice
* @param {number} pricePerKm
* @param {number} minKm
* @returns {number}
*/
export const calculateTotalCharge = (distanceKm, basePrice, pricePerKm, minKm) => {
if (distanceKm < minKm) {
return basePrice;
}
return (distanceKm - minKm) * pricePerKm + basePrice;
};

View File

@@ -18,5 +18,6 @@
"riderslogs": "Riders Logs",
"invoice": "Invoice",
"dispatch": "Dispatch",
"profitability": "Profitability"
"profitability": "Profitability",
"Doormile": "Doormile"
}

38
src/utils/session.js Normal file
View File

@@ -0,0 +1,38 @@
// Shared session/logout contract used by both the manual "Logout" buttons
// and the automatic inactivity logout (see hooks/useInactivityLogout.js).
// Keeping this in one place means every logout path clears the same things
// the same way, instead of each caller hand-rolling its own localStorage cleanup.
export const ACTIVITY_STORAGE_KEY = 'lastActivityTime';
export const SESSION_START_STORAGE_KEY = 'sessionStartTime';
export const AUTH_PRESENCE_KEY = 'authname';
export const INACTIVITY_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes, resets on user activity
export const ABSOLUTE_SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes, hard cap since login regardless of activity
export const markActivity = () => {
localStorage.setItem(ACTIVITY_STORAGE_KEY, String(Date.now()));
};
// Called once at login to start the absolute-lifetime clock for the session.
export const markSessionStart = () => {
localStorage.setItem(SESSION_START_STORAGE_KEY, String(Date.now()));
};
export const isSessionActive = () => Boolean(localStorage.getItem(AUTH_PRESENCE_KEY));
// Wipes local auth state + the in-memory query cache, then hard-navigates to
// /login. A real navigation (not react-router) is deliberate: it throws away
// the entire JS heap (Redux store, component state, any variable holding
// fetched data) so nothing sensitive survives in memory after logout, and it
// prevents the back button from resurrecting a stale authenticated page from
// the render tree.
export const performSessionLogout = ({ queryClient, dispatch, clearFcmToken, logoutUser } = {}) => {
try {
queryClient?.clear();
if (dispatch && clearFcmToken) dispatch(clearFcmToken());
if (dispatch && logoutUser) dispatch(logoutUser());
} finally {
localStorage.clear();
window.location.replace('/login');
}
};

View File

@@ -6572,11 +6572,6 @@ fs.realpath@^1.0.0:
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@^2.3.2, fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"