initialisation on the doormile express console with astryx
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { LoadScriptNext, GoogleMap } from '@react-google-maps/api';
|
||||
import { DT } from 'themes/dt/tokens';
|
||||
|
||||
const containerStyle = {
|
||||
width: '100%',
|
||||
@@ -46,7 +47,7 @@ const MapWithRouteGoogle = ({ coordinates, additionalProps, setMapOpen }) => {
|
||||
const route = new window.google.maps.Polyline({
|
||||
path: numericCoordinates,
|
||||
geodesic: false,
|
||||
strokeColor: '#1A73E8',
|
||||
strokeColor: DT.brand,
|
||||
strokeOpacity: 1.0,
|
||||
strokeWeight: 4
|
||||
});
|
||||
@@ -69,11 +70,13 @@ const MapWithRouteGoogle = ({ coordinates, additionalProps, setMapOpen }) => {
|
||||
right: 10,
|
||||
zIndex: 999,
|
||||
padding: '6px 12px',
|
||||
background: '#1A73E8',
|
||||
background: DT.brand,
|
||||
color: 'white',
|
||||
borderRadius: 6,
|
||||
borderRadius: DT.radiusInner,
|
||||
cursor: 'pointer',
|
||||
border: 'none'
|
||||
border: 'none',
|
||||
fontWeight: 600,
|
||||
boxShadow: DT.shadowMd
|
||||
}}
|
||||
>
|
||||
Close
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { Button } from '@mui/material';
|
||||
import { LoadScriptNext, GoogleMap, Marker, OverlayView } from '@react-google-maps/api';
|
||||
|
||||
const containerStyle = {
|
||||
width: '100%',
|
||||
height: 'calc(100vh - 150px)'
|
||||
};
|
||||
|
||||
export default function RiderLocationMap({ riderLocations }) {
|
||||
console.log('riderLocations', riderLocations);
|
||||
|
||||
const center = {
|
||||
lat: Number(riderLocations?.[0]?.latitude || 11.0056),
|
||||
lng: Number(riderLocations?.[0]?.longitude || 76.9661)
|
||||
};
|
||||
const GreenIcon = {
|
||||
url: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
|
||||
scaledSize: new window.google.maps.Size(25, 41),
|
||||
anchor: new window.google.maps.Point(12, 41)
|
||||
};
|
||||
|
||||
const RedIcon = {
|
||||
url: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
|
||||
scaledSize: new window.google.maps.Size(25, 41),
|
||||
anchor: new window.google.maps.Point(12, 41)
|
||||
};
|
||||
|
||||
return (
|
||||
<LoadScriptNext googleMapsApiKey={process.env.REACT_APP_GOOGLE_MAPS_API_KEY}>
|
||||
<GoogleMap mapContainerStyle={containerStyle} zoom={12} center={center}>
|
||||
{riderLocations &&
|
||||
riderLocations?.map((r, index) => {
|
||||
const lat = Number(r.latitude);
|
||||
const lng = Number(r.longitude);
|
||||
return (
|
||||
<div key={index}>
|
||||
{/* Marker */}
|
||||
<Marker
|
||||
position={{ lat, lng }}
|
||||
icon={r.status == 'active' ? GreenIcon : RedIcon}
|
||||
label={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
/>
|
||||
<OverlayView position={{ lat, lng }} mapPaneName={OverlayView.OVERLAY_LAYER}>
|
||||
<div
|
||||
style={{
|
||||
background: 'none',
|
||||
color: 'green',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
whiteSpace: 'nowrap',
|
||||
transform: 'translate(-50%, -140%)',
|
||||
pointerEvents: 'none',
|
||||
ml: 20
|
||||
}}
|
||||
>
|
||||
<Button variant="contained" color="primary" size="small">
|
||||
{` ${r.username} `}
|
||||
{/* <br /> */}
|
||||
{/* {`${r.contactno || '##### ##### '} `} */}
|
||||
<br />
|
||||
{`(${r.orderid || ''}) `}
|
||||
</Button>
|
||||
</div>
|
||||
</OverlayView>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</GoogleMap>
|
||||
</LoadScriptNext>
|
||||
);
|
||||
}
|
||||
@@ -107,7 +107,7 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
|
||||
// brand purple to match the planned-route polyline below.
|
||||
const stepIcon = (n, isFocused) => {
|
||||
const size = isFocused ? 38 : 32;
|
||||
const color = isFocused ? '#4D1C61' : '#662582';
|
||||
const color = isFocused ? '#910E1D' : '#C01227';
|
||||
const svg = encodeURIComponent(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="${size}" height="${size}">` +
|
||||
`<circle cx="16" cy="16" r="14" fill="${color}" stroke="white" stroke-width="3"/>` +
|
||||
@@ -126,7 +126,7 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
|
||||
px: 2,
|
||||
py: 1.25,
|
||||
borderBottom: '1px solid rgba(15, 23, 42, 0.08)',
|
||||
background: 'linear-gradient(135deg, #662582 0%, #9255AB 100%)',
|
||||
background: 'linear-gradient(135deg, #C01227 0%, #D25463 100%)',
|
||||
color: '#fff',
|
||||
flexShrink: 0
|
||||
}}
|
||||
@@ -207,12 +207,12 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
|
||||
{/* Translucent backdrop so the route stays legible on busy tiles. */}
|
||||
<Polyline
|
||||
path={routePath}
|
||||
options={{ strokeColor: '#662582', strokeOpacity: 0.25, strokeWeight: 8 }}
|
||||
options={{ strokeColor: '#C01227', strokeOpacity: 0.25, strokeWeight: 8 }}
|
||||
/>
|
||||
{/* Road-following planned route from the Directions API. */}
|
||||
<Polyline
|
||||
path={routePath}
|
||||
options={{ strokeColor: '#662582', strokeOpacity: 0.95, strokeWeight: 4 }}
|
||||
options={{ strokeColor: '#C01227', strokeOpacity: 0.95, strokeWeight: 4 }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
@@ -221,7 +221,7 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
|
||||
<Polyline
|
||||
path={dropPath}
|
||||
options={{
|
||||
strokeColor: '#662582',
|
||||
strokeColor: '#C01227',
|
||||
strokeOpacity: 0,
|
||||
strokeWeight: 0,
|
||||
icons: [
|
||||
@@ -229,7 +229,7 @@ export default function RidersRoutes({ details, loading, riderName, dateRange, o
|
||||
icon: {
|
||||
path: 'M 0,-1 0,1',
|
||||
strokeOpacity: 0.6,
|
||||
strokeColor: '#662582',
|
||||
strokeColor: '#C01227',
|
||||
scale: 3
|
||||
},
|
||||
offset: '0',
|
||||
|
||||
@@ -5,8 +5,8 @@ import 'leaflet/dist/leaflet.css';
|
||||
import dayjs from 'dayjs';
|
||||
import { Chip, Stack, Typography, Box } from '@mui/material';
|
||||
import { CloseCircleOutlined } from '@ant-design/icons';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import CircularLoader from 'components/CircularLoader';
|
||||
import { DT } from 'themes/dt/tokens';
|
||||
|
||||
var utc = require('dayjs/plugin/utc');
|
||||
dayjs.extend(utc);
|
||||
@@ -33,7 +33,6 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => {
|
||||
console.log('additionalProps', additionalProps);
|
||||
|
||||
const mapRef = useRef(null);
|
||||
const theme = useTheme();
|
||||
const [routePoints, setRoutePoints] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -145,13 +144,13 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => {
|
||||
top: 12,
|
||||
right: 12,
|
||||
zIndex: 2000,
|
||||
bgcolor: theme.palette.error.main,
|
||||
bgcolor: DT.brand,
|
||||
color: '#fff',
|
||||
fontWeight: 600,
|
||||
borderRadius: '12px',
|
||||
borderRadius: DT.radiusInner + 'px',
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
boxShadow: theme.shadows[4],
|
||||
boxShadow: DT.shadowMd,
|
||||
cursor: 'pointer',
|
||||
'& .MuiChip-icon': { color: '#fff' }
|
||||
}}
|
||||
@@ -183,7 +182,7 @@ const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => {
|
||||
width: '100%',
|
||||
bgcolor: 'rgba(255,255,255,0.96)',
|
||||
p: 2,
|
||||
boxShadow: theme.shadows[3],
|
||||
boxShadow: DT.shadowPop,
|
||||
zIndex: 1500
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -114,8 +114,8 @@ const soft = (c) => a(c, '18');
|
||||
const ring = (c) => a(c, '26');
|
||||
const edge = (c) => a(c, '55');
|
||||
|
||||
const BRAND = '#662582';
|
||||
const BRAND_LIGHT = '#9255AB';
|
||||
const BRAND = '#C01227';
|
||||
const BRAND_LIGHT = '#D25463';
|
||||
|
||||
const SoftPaper = (props) => (
|
||||
<Paper
|
||||
@@ -153,7 +153,7 @@ const pillFieldSx = (color) => ({
|
||||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||||
'&:hover fieldset': { borderColor: '#cbd5e1' },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }
|
||||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1104,15 +1104,15 @@ export default function OrdersDetails() {
|
||||
flexShrink: 0,
|
||||
cursor: 'pointer',
|
||||
borderRadius: 999,
|
||||
border: `1px solid ${active ? meta.color : DT.borderSubtle}`,
|
||||
bgcolor: active ? meta.color : DT.surface,
|
||||
border: `1px solid ${active ? '#C01227' : DT.borderSubtle}`,
|
||||
bgcolor: active ? '#C01227' : DT.surface,
|
||||
color: active ? '#fff' : DT.textSecondary,
|
||||
fontWeight: 600,
|
||||
boxShadow: 'none',
|
||||
transition: 'background-color 0.15s, border-color 0.15s, color 0.15s',
|
||||
'&:hover': {
|
||||
borderColor: active ? meta.color : '#cbd5e1',
|
||||
bgcolor: active ? meta.color : DT.surfaceAlt
|
||||
borderColor: active ? '#C01227' : '#cbd5e1',
|
||||
bgcolor: active ? '#C01227' : DT.surfaceAlt
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -1172,7 +1172,7 @@ export default function OrdersDetails() {
|
||||
bgcolor: '#ffffff',
|
||||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||||
'&:hover fieldset': { borderColor: '#cbd5e1' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
|
||||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
|
||||
}}
|
||||
/>
|
||||
@@ -1817,10 +1817,21 @@ export default function OrdersDetails() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
<Divider />
|
||||
<Divider sx={{ minWidth: { xs: '100%', md: 1600 } }} />
|
||||
{rows?.length !== 0 && (
|
||||
<Stack justifyContent="center" alignItems="center" sx={{ width: '100%', py: 2 }}>
|
||||
<Stack ref={loadMoreRef} style={{ textAlign: 'center', width: '100%' }}>
|
||||
<Box sx={{ minWidth: { xs: '100%', md: 1600 }, py: 2 }}>
|
||||
<Stack
|
||||
ref={loadMoreRef}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
left: 0,
|
||||
width: '100%',
|
||||
maxWidth: '100vw',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{isFetchingNextPage || hasNextPage ? (
|
||||
<LoaderWithImage />
|
||||
) : (
|
||||
@@ -1829,7 +1840,7 @@ export default function OrdersDetails() {
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
|
||||
@@ -87,8 +87,8 @@ const soft = (c) => a(c, '18');
|
||||
const ring = (c) => a(c, '26');
|
||||
const edge = (c) => a(c, '55');
|
||||
|
||||
const BRAND = '#662582';
|
||||
const BRAND_LIGHT = '#9255AB';
|
||||
const BRAND = '#C01227';
|
||||
const BRAND_LIGHT = '#D25463';
|
||||
|
||||
const SoftPaper = (props) => (
|
||||
<Paper
|
||||
@@ -190,7 +190,7 @@ const pillFieldSx = (color) => ({
|
||||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||||
'&:hover fieldset': { borderColor: '#cbd5e1' },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 }
|
||||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -588,7 +588,7 @@ export default function OrdersReport() {
|
||||
bgcolor: '#ffffff',
|
||||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||||
'&:hover fieldset': { borderColor: '#cbd5e1' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
|
||||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,790 +0,0 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Chip,
|
||||
Grid,
|
||||
Paper,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import {
|
||||
MdMyLocation,
|
||||
MdCalendarMonth,
|
||||
MdPerson,
|
||||
MdOutlineCurrencyRupee,
|
||||
MdStraighten,
|
||||
MdPayments,
|
||||
MdRoute,
|
||||
MdTrendingUp,
|
||||
MdTrendingDown
|
||||
} from 'react-icons/md';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
var utc = require('dayjs/plugin/utc');
|
||||
dayjs.extend(utc);
|
||||
|
||||
import { fetchDeliveries } from 'pages/api/api';
|
||||
import Loader from 'components/Loader';
|
||||
import DateFilterDialog from 'components/DateFilterDialog';
|
||||
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
|
||||
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
||||
import PageHeader from 'components/nearle_components/PageHeader';
|
||||
import StatCard from 'components/nearle_components/StatCard';
|
||||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||||
|
||||
const DT = {
|
||||
radiusPill: 999,
|
||||
radiusCard: 14,
|
||||
shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)',
|
||||
shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)',
|
||||
shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)',
|
||||
textPrimary: '#0f172a',
|
||||
textSecondary: '#64748b',
|
||||
textMuted: '#94a3b8',
|
||||
borderSubtle: '#e2e8f0',
|
||||
divider: '#f1f5f9',
|
||||
surface: '#ffffff',
|
||||
surfaceAlt: '#f8fafc'
|
||||
};
|
||||
|
||||
const aColor = (c, suffix) => `${c}${suffix}`;
|
||||
const soft = (c) => aColor(c, '18');
|
||||
const tint = (c) => aColor(c, '08');
|
||||
const edge = (c) => aColor(c, '55');
|
||||
const ring = (c) => aColor(c, '26');
|
||||
|
||||
const BRAND = '#662582';
|
||||
|
||||
const SoftPaper = (props) => (
|
||||
<Paper
|
||||
{...props}
|
||||
sx={{
|
||||
mt: 0.75,
|
||||
borderRadius: 2,
|
||||
boxShadow: DT.shadowPop,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
SoftPaper.propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
const AccentAvatar = ({ color, selected, size = 24, children }) => (
|
||||
<Avatar
|
||||
sx={{
|
||||
width: size,
|
||||
height: size,
|
||||
bgcolor: selected ? color : soft(color),
|
||||
color: selected ? '#fff' : color,
|
||||
transition: 'background-color 0.15s, color 0.15s'
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Avatar>
|
||||
);
|
||||
|
||||
AccentAvatar.propTypes = {
|
||||
color: PropTypes.string.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
size: PropTypes.number,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
const MetricPill = ({ color, icon, label, tooltip, minWidth = 80 }) => (
|
||||
<Tooltip title={tooltip || ''} placement="top">
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
px: 1,
|
||||
py: 0.375,
|
||||
borderRadius: 999,
|
||||
bgcolor: '#ffffff',
|
||||
border: `1px solid ${edge(color)}`,
|
||||
color,
|
||||
fontSize: 11,
|
||||
fontWeight: 800,
|
||||
minWidth,
|
||||
justifyContent: 'center',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
MetricPill.propTypes = {
|
||||
color: PropTypes.string.isRequired,
|
||||
icon: PropTypes.node,
|
||||
label: PropTypes.string.isRequired,
|
||||
tooltip: PropTypes.string,
|
||||
minWidth: PropTypes.number
|
||||
};
|
||||
|
||||
const BATCHES = [
|
||||
{ id: 'morning', name: 'Morning Batch', startHour: 0, endHour: 8 },
|
||||
{ id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12.5 },
|
||||
{ id: 'evening', name: 'Evening Batch', startHour: 16, endHour: 19 }
|
||||
];
|
||||
|
||||
const getBatchForHour = (h, batches = BATCHES) => {
|
||||
for (const b of batches) {
|
||||
if (h >= b.startHour && h < b.endHour) return b.id;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getRowBatch = (r, batches = BATCHES) => {
|
||||
const t = r?.assigntime;
|
||||
if (!t) return null;
|
||||
const str = String(t).trim();
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return null;
|
||||
const d = dayjs(t);
|
||||
if (!d.isValid()) return null;
|
||||
return getBatchForHour(d.hour() + d.minute() / 60, batches);
|
||||
};
|
||||
|
||||
function formatNumberToRupees(value) {
|
||||
return new Intl.NumberFormat('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'INR',
|
||||
minimumFractionDigits: 2
|
||||
}).format(Number(value) || 0);
|
||||
}
|
||||
|
||||
export default function ProfitabilityReport() {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
const [locaName, setLocoName] = useState('All');
|
||||
const [open, setOpen] = useState(false);
|
||||
const [datestatus, setDatestatus] = useState('Today');
|
||||
const [appId, setAppId] = useState(0);
|
||||
|
||||
const [searchword, setSearchword] = useState('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||
|
||||
const liveUserid = typeof window !== 'undefined' ? localStorage.getItem('userid') || 0 : 0;
|
||||
|
||||
// Load slots configuration from localStorage to match Dispatch page edits
|
||||
const customBatches = useMemo(() => {
|
||||
if (typeof window === 'undefined') return BATCHES;
|
||||
try {
|
||||
const raw = window.localStorage.getItem('dispatch.slots.v9');
|
||||
if (!raw) return BATCHES;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed) || parsed.length !== BATCHES.length) return BATCHES;
|
||||
return parsed.map((s, i) => {
|
||||
const id = s.id || `slot-${i + 1}`;
|
||||
const startHour = Number(s.startHour) || 0;
|
||||
const endHour = Number(s.endHour) || 24;
|
||||
return {
|
||||
id,
|
||||
name: s.name || BATCHES.find((b) => b.id === id)?.name || `Slot ${i + 1}`,
|
||||
startHour,
|
||||
endHour
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
return BATCHES;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Fetch all deliveries for the selected date range and zone
|
||||
const {
|
||||
data: deliveriesData,
|
||||
isLoading: isLoadingDeliveries,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage
|
||||
} = useInfiniteQuery({
|
||||
queryKey: ['fetchdeliveries', appId, liveUserid, 'all', startdate, enddate, 2000, '', 0, 0, 0],
|
||||
queryFn: fetchDeliveries,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
// Auto-page through all results
|
||||
useEffect(() => {
|
||||
if (hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||
|
||||
// Flatten and deduplicate deliveries by orderid
|
||||
const liveRows = useMemo(() => {
|
||||
const all = (deliveriesData?.pages || []).flatMap((p) => p.rows || []);
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
for (const r of all) {
|
||||
const key = r.orderid != null ? String(r.orderid) : null;
|
||||
if (key && seen.has(key)) continue;
|
||||
if (key) seen.add(key);
|
||||
out.push(r);
|
||||
}
|
||||
return out;
|
||||
}, [deliveriesData]);
|
||||
|
||||
// Group deliveries by rider
|
||||
const ridersList = useMemo(() => {
|
||||
const riderMap = {};
|
||||
liveRows.forEach((r) => {
|
||||
const key = String(r.userid || r.rider_id || '');
|
||||
if (!key || key === 'unassigned' || key === '0') return;
|
||||
if (!riderMap[key]) {
|
||||
riderMap[key] = {
|
||||
id: key,
|
||||
riderName: r.ridername || r.rider_name || r.username || `Rider ${key}`,
|
||||
orders: []
|
||||
};
|
||||
}
|
||||
if (!riderMap[key].orders.some((existing) => existing.orderid === r.orderid)) {
|
||||
riderMap[key].orders.push(r);
|
||||
}
|
||||
});
|
||||
|
||||
return Object.values(riderMap)
|
||||
.map((r) => ({
|
||||
...r,
|
||||
orders: [...r.orders].sort((a, b) => {
|
||||
const tA = a.trip_number || 1;
|
||||
const tB = b.trip_number || 1;
|
||||
if (tA !== tB) return tA - tB;
|
||||
return (a.step || 0) - (b.step || 0);
|
||||
})
|
||||
}))
|
||||
.sort((a, b) => b.orders.length - a.orders.length);
|
||||
}, [liveRows]);
|
||||
|
||||
// Calculate profitability metrics for all riders
|
||||
const stats = useMemo(() => {
|
||||
let activeRiders = 0;
|
||||
let profitableRiders = 0;
|
||||
let lossRiders = 0;
|
||||
let totalKms = 0;
|
||||
let totalPlannedKms = 0;
|
||||
let totalActualKms = 0;
|
||||
|
||||
const list = ridersList
|
||||
.map((r) => {
|
||||
let rRevenue = 0;
|
||||
let rKms = 0;
|
||||
let rPlannedKms = 0;
|
||||
let rActualKms = 0;
|
||||
const slotsByDate = {};
|
||||
let ordersInSlots = 0;
|
||||
|
||||
r.orders.forEach((o) => {
|
||||
const status = String(o.orderstatus || '').toLowerCase();
|
||||
if (status === 'cancelled' || status === 'skipped') return;
|
||||
|
||||
const slot = getRowBatch(o, customBatches);
|
||||
if (!slot) return;
|
||||
|
||||
const oKms = parseFloat(o.riderkms || 0);
|
||||
rKms += oKms;
|
||||
rPlannedKms += parseFloat(o.kms || 0);
|
||||
rActualKms += parseFloat(o.actualkms || 0);
|
||||
rRevenue += oKms <= 8 ? 30 : 30 + (oKms - 8) * 6;
|
||||
|
||||
const dateStr = o.assigntime
|
||||
? dayjs(o.assigntime).format('YYYY-MM-DD')
|
||||
: o.deliverydate
|
||||
? dayjs(o.deliverydate).format('YYYY-MM-DD')
|
||||
: null;
|
||||
if (!dateStr) return;
|
||||
|
||||
if (!slotsByDate[dateStr]) {
|
||||
slotsByDate[dateStr] = new Set();
|
||||
}
|
||||
slotsByDate[dateStr].add(slot);
|
||||
ordersInSlots++;
|
||||
});
|
||||
|
||||
if (ordersInSlots === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sum unique slots per day, capping at 3 slots max per day
|
||||
let slotCount = 0;
|
||||
Object.values(slotsByDate).forEach((set) => {
|
||||
slotCount += Math.min(set.size, 3);
|
||||
});
|
||||
|
||||
const rVarCost = rKms * 2.5;
|
||||
const rFixedCost = slotCount * (500 / 3);
|
||||
const rTotalCost = rVarCost + rFixedCost;
|
||||
const rNet = rRevenue - rTotalCost;
|
||||
const rMargin = rRevenue > 0 ? (rNet / rRevenue) * 100 : 0;
|
||||
|
||||
if (rNet >= 0) {
|
||||
profitableRiders++;
|
||||
} else {
|
||||
lossRiders++;
|
||||
}
|
||||
|
||||
totalKms += rKms;
|
||||
totalPlannedKms += rPlannedKms;
|
||||
totalActualKms += rActualKms;
|
||||
activeRiders++;
|
||||
|
||||
return {
|
||||
...r,
|
||||
orderCount: ordersInSlots,
|
||||
kms: rKms,
|
||||
plannedKms: rPlannedKms,
|
||||
actualKms: rActualKms,
|
||||
revenue: rRevenue,
|
||||
varCost: rVarCost,
|
||||
fixedCost: rFixedCost,
|
||||
totalCost: rTotalCost,
|
||||
net: rNet,
|
||||
margin: rMargin
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
activeRiders,
|
||||
profitableRiders,
|
||||
lossRiders,
|
||||
totalKms,
|
||||
totalPlannedKms,
|
||||
totalActualKms,
|
||||
enrichedRiders: list
|
||||
};
|
||||
}, [ridersList, customBatches]);
|
||||
|
||||
// Filter riders by search query
|
||||
const filteredRiders = useMemo(() => {
|
||||
if (!stats?.enrichedRiders || !Array.isArray(stats.enrichedRiders)) return [];
|
||||
const baseList = stats.enrichedRiders.filter(Boolean);
|
||||
if (!debouncedSearch) return baseList;
|
||||
const q = debouncedSearch.toLowerCase().trim();
|
||||
return baseList.filter(
|
||||
(r) => r && [r.riderName, String(r.id)].filter(Boolean).some((field) => String(field).toLowerCase().includes(q))
|
||||
);
|
||||
}, [stats?.enrichedRiders, debouncedSearch]);
|
||||
|
||||
const KPI_META = [
|
||||
{
|
||||
key: 'riders',
|
||||
label: 'Riders Active',
|
||||
color: BRAND,
|
||||
icon: MdPerson,
|
||||
value: stats?.activeRiders ?? 0
|
||||
},
|
||||
{
|
||||
key: 'planned-kms',
|
||||
label: 'Planned KMs',
|
||||
color: '#0ea5e9',
|
||||
icon: MdRoute,
|
||||
value: `${(stats?.totalPlannedKms ?? 0).toFixed(1)} km`
|
||||
},
|
||||
{
|
||||
key: 'actual-kms',
|
||||
label: 'Actual KMs',
|
||||
color: '#f59e0b',
|
||||
icon: MdMyLocation,
|
||||
value: `${(stats?.totalActualKms ?? 0).toFixed(1)} km`
|
||||
},
|
||||
{
|
||||
key: 'rider-kms',
|
||||
label: 'Rider KMs',
|
||||
color: BRAND,
|
||||
icon: MdStraighten,
|
||||
value: `${(stats?.totalKms ?? 0).toFixed(1)} km`
|
||||
},
|
||||
{
|
||||
key: 'total-distance',
|
||||
label: 'Trip KMs',
|
||||
color: '#10b981',
|
||||
icon: MdStraighten,
|
||||
value: `${(stats?.totalKms ?? 0).toFixed(1)} km`
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isLoadingDeliveries || isFetchingNextPage) && <Loader />}
|
||||
|
||||
{/* Page Header */}
|
||||
<PageHeader
|
||||
title="Profitability Report"
|
||||
subtitle={`Live · ${locaName || 'All Zones'} · ${datestatus}`}
|
||||
live
|
||||
action={
|
||||
<LocationAutocomplete
|
||||
locaName={locaName}
|
||||
setAppId={setAppId}
|
||||
setLocoName={setLocoName}
|
||||
pill
|
||||
accentColor={BRAND}
|
||||
icon={<MdMyLocation size={14} />}
|
||||
placeholder="Select Zone"
|
||||
paperComponent={SoftPaper}
|
||||
sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* KPI Cards Grid */}
|
||||
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
|
||||
{KPI_META.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Grid item key={item.key} xs={6} sm={4} md={2.4}>
|
||||
<StatCard
|
||||
title={item.label}
|
||||
value={item.value ?? 0}
|
||||
icon={<Icon size={20} />}
|
||||
color={item.color}
|
||||
loading={isLoadingDeliveries}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
{/* Filter Bar (date + search) */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
mt: { xs: 1.5, md: 2 },
|
||||
p: { xs: 1, md: 1.5 },
|
||||
borderTopLeftRadius: DT.radiusCard / 8,
|
||||
borderTopRightRadius: DT.radiusCard / 8,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
borderBottom: 0,
|
||||
background: '#fff'
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
||||
justifyContent="space-between"
|
||||
spacing={1.25}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1.25} flexWrap="wrap">
|
||||
<AccentAvatar color={BRAND} size={32}>
|
||||
<MdPerson size={18} />
|
||||
</AccentAvatar>
|
||||
<Stack>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ fontWeight: 800, color: DT.textSecondary, letterSpacing: 0.6, textTransform: 'uppercase' }}
|
||||
>
|
||||
Profitability Overview · {datestatus}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: DT.textPrimary, fontWeight: 700 }}>
|
||||
{filteredRiders.length} riders · {stats.profitableRiders} profitable · {stats.lossRiders} at loss
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Tooltip title="Date Filter" placement="top">
|
||||
<Box
|
||||
onClick={() => setOpen(true)}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.75,
|
||||
px: 1.25,
|
||||
py: 0.75,
|
||||
borderRadius: 999,
|
||||
cursor: 'pointer',
|
||||
bgcolor: tint('#f59e0b'),
|
||||
border: `1.5px solid ${edge('#f59e0b')}`,
|
||||
color: '#f59e0b',
|
||||
fontWeight: 800,
|
||||
fontSize: 12,
|
||||
ml: 1,
|
||||
transition: 'all 0.18s',
|
||||
'&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` }
|
||||
}}
|
||||
>
|
||||
<MdCalendarMonth size={14} />
|
||||
{dayjs(startdate).format('DD/MM/YY')} – {dayjs(enddate).format('DD/MM/YY')}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
<Box sx={{ width: { xs: '100%', sm: 280, lg: 340 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
|
||||
<DebounceSearchBar
|
||||
value={searchword}
|
||||
onChange={setSearchword}
|
||||
onDebouncedChange={setDebouncedSearch}
|
||||
placeholder="Search riders"
|
||||
sx={{
|
||||
m: 0,
|
||||
width: '100%',
|
||||
borderRadius: 999,
|
||||
bgcolor: '#ffffff',
|
||||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||||
'&:hover fieldset': { borderColor: '#cbd5e1' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Table & Mobile List Container */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderTopLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomLeftRadius: DT.radiusCard / 8,
|
||||
borderBottomRightRadius: DT.radiusCard / 8,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
overflow: 'hidden',
|
||||
background: '#fff'
|
||||
}}
|
||||
>
|
||||
{isMobile ? (
|
||||
<MobileCardList scroll>
|
||||
{!filteredRiders || filteredRiders.length === 0 ? (
|
||||
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
|
||||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||||
<MdPerson size={28} />
|
||||
</Avatar>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||||
No riders to show
|
||||
</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
filteredRiders.map((row, index) => {
|
||||
if (!row) return null;
|
||||
const isProfit = (row.net ?? 0) >= 0;
|
||||
return (
|
||||
<MobileCard
|
||||
key={row.id || index}
|
||||
accent={isProfit ? '#10b981' : '#ef4444'}
|
||||
header={
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<AccentAvatar color={isProfit ? '#10b981' : '#ef4444'} size={36}>
|
||||
<MdPerson size={18} />
|
||||
</AccentAvatar>
|
||||
<Stack>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||||
{row.riderName}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
ID #{row.id}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<MobileFieldGrid columns={2}>
|
||||
<MobileField label="Orders" value={row.orderCount} />
|
||||
<MobileField label="Rider KMs" value={`${Math.round(row.kms)} km`} />
|
||||
<MobileField label="Revenue" value={formatNumberToRupees(row.revenue)} />
|
||||
<MobileField label="Fixed Cost" value={formatNumberToRupees(row.fixedCost)} />
|
||||
<MobileField label="Variable Cost" value={formatNumberToRupees(row.varCost)} />
|
||||
<MobileField label="Total Cost" value={formatNumberToRupees(row.totalCost)} />
|
||||
<MobileField label="Net Profit" value={`${isProfit ? '+' : ''}${formatNumberToRupees(row.net)}`} full />
|
||||
<MobileField label="Margin" value={`${Math.abs(row.margin).toFixed(0)}%`} full />
|
||||
</MobileFieldGrid>
|
||||
</MobileCard>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</MobileCardList>
|
||||
) : (
|
||||
<TableContainer
|
||||
sx={{
|
||||
maxHeight: 'calc(100vh - 280px)',
|
||||
'&::-webkit-scrollbar': { width: 10, height: 10 },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: edge(BRAND),
|
||||
borderRadius: 8,
|
||||
'&:hover': { backgroundColor: BRAND }
|
||||
},
|
||||
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
|
||||
}}
|
||||
>
|
||||
<Table stickyHeader sx={{ minWidth: 1000 }}>
|
||||
<TableHead>
|
||||
<TableRow
|
||||
sx={{
|
||||
'& th': {
|
||||
backgroundColor: DT.surfaceAlt,
|
||||
color: DT.textSecondary,
|
||||
fontSize: 11,
|
||||
fontWeight: 800,
|
||||
letterSpacing: 0.6,
|
||||
textTransform: 'uppercase',
|
||||
whiteSpace: 'nowrap',
|
||||
borderBottom: `1px solid ${DT.borderSubtle}`,
|
||||
py: 1.25,
|
||||
px: 2
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Rider</TableCell>
|
||||
<TableCell align="center">Orders</TableCell>
|
||||
<TableCell align="center">Rider KMs</TableCell>
|
||||
<TableCell align="center">Revenue</TableCell>
|
||||
<TableCell align="center">Fixed Cost</TableCell>
|
||||
<TableCell align="center">Variable Cost</TableCell>
|
||||
<TableCell align="center">Total Cost</TableCell>
|
||||
<TableCell align="center">Net Profit</TableCell>
|
||||
<TableCell align="center">Margin</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{!filteredRiders || filteredRiders.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={10} sx={{ py: 6 }}>
|
||||
<Stack alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||||
<MdPerson size={28} />
|
||||
</Avatar>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||||
No riders to show
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRiders.map((row, index) => {
|
||||
if (!row) return null;
|
||||
const isProfit = (row.net ?? 0) >= 0;
|
||||
return (
|
||||
<TableRow
|
||||
key={row.id || index}
|
||||
sx={{
|
||||
transition: 'background-color 0.15s',
|
||||
'& td': {
|
||||
borderBottom: `1px solid ${DT.divider}`,
|
||||
py: 1.5,
|
||||
px: 2
|
||||
},
|
||||
'&:hover': { backgroundColor: DT.surfaceAlt }
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<AccentAvatar color={BRAND} size={36}>
|
||||
<MdPerson size={18} />
|
||||
</AccentAvatar>
|
||||
<Stack>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
|
||||
{row.riderName}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
ID #{row.id}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||||
{row.orderCount}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<MetricPill color="#10b981" icon={<MdStraighten size={11} />} label={`${Math.round(row.kms)} km`} tooltip="KMS" />
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<MetricPill
|
||||
color={BRAND}
|
||||
icon={<MdOutlineCurrencyRupee size={11} />}
|
||||
label={formatNumberToRupees(row.revenue).replace('₹', '').trim()}
|
||||
tooltip="Revenue"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<MetricPill
|
||||
color="#6366f1"
|
||||
icon={<MdPayments size={11} />}
|
||||
label={formatNumberToRupees(row.fixedCost).replace('₹', '').trim()}
|
||||
tooltip="Fixed Cost"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<MetricPill
|
||||
color="#f59e0b"
|
||||
icon={<MdRoute size={11} />}
|
||||
label={formatNumberToRupees(row.varCost).replace('₹', '').trim()}
|
||||
tooltip="Variable Cost"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<MetricPill
|
||||
color="#94a3b8"
|
||||
icon={<MdPayments size={11} />}
|
||||
label={formatNumberToRupees(row.totalCost).replace('₹', '').trim()}
|
||||
tooltip="Total Cost"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<MetricPill
|
||||
color={isProfit ? '#10b981' : '#ef4444'}
|
||||
icon={isProfit ? <MdTrendingUp size={11} /> : <MdTrendingDown size={11} />}
|
||||
label={`${isProfit ? '+' : ''}${formatNumberToRupees(row.net).replace('₹', '').trim()}`}
|
||||
tooltip="Net Profit"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Chip
|
||||
label={`${Math.abs(row.margin).toFixed(0)}%`}
|
||||
color={isProfit ? 'success' : 'error'}
|
||||
size="small"
|
||||
sx={{ fontWeight: 700, minWidth: 60 }}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Date Filter Dialog */}
|
||||
<DateFilterDialog
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onSelect={(range) => {
|
||||
setStartdate(range.startDate);
|
||||
setEnddate(range.endDate);
|
||||
setDatestatus(range.label);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,355 +0,0 @@
|
||||
import React, { useState, useEffect, Fragment } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Drawer,
|
||||
IconButton,
|
||||
Toolbar,
|
||||
Typography,
|
||||
AppBar,
|
||||
useMediaQuery,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
useTheme,
|
||||
ListItemAvatar,
|
||||
Stack,
|
||||
Button,
|
||||
Checkbox,
|
||||
Skeleton
|
||||
} from '@mui/material';
|
||||
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import SearchBar from 'components/nearle_components/SearchBar';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchRidersLogs } from 'pages/api/api';
|
||||
import RiderLocationMap from './RiderLocationMap';
|
||||
import MainCard from 'components/MainCard';
|
||||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import error500 from 'assets/images/maintenance/Error500.png';
|
||||
|
||||
const drawerWidth = 350;
|
||||
|
||||
const RidersLogs = () => {
|
||||
const theme = useTheme();
|
||||
const isDesktop = useMediaQuery('(min-width:900px)');
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedRiders, setSelectedRiders] = useState([]);
|
||||
const [riderSearch, setRiderSearch] = useState('');
|
||||
const appId = 1;
|
||||
const {
|
||||
data: riders,
|
||||
isLoading: ridersIsLoading,
|
||||
isFetching: riderIsFetching,
|
||||
refetch: riderLogsRefetch,
|
||||
error: riderLogsError
|
||||
} = useQuery({
|
||||
queryKey: [appId, dayjs().format('YYYY-MM-DD'), riderSearch],
|
||||
queryFn: fetchRidersLogs,
|
||||
refetchInterval: 5 * 60 * 1000
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// const sortedRiders = riders?.sort((a, b) => a.firstname.localeCompare(b.firstname));
|
||||
setSelectedRiders(riders);
|
||||
}, [riders]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('selectedRiders', selectedRiders);
|
||||
}, [selectedRiders]);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(isDesktop);
|
||||
}, [isDesktop]);
|
||||
|
||||
return (
|
||||
<MainCard content={false}>
|
||||
<Box sx={{ display: 'flex', width: '100%', height: '100%', position: 'relative' }}>
|
||||
{/* Drawer */}
|
||||
<Drawer
|
||||
variant={isDesktop ? 'persistent' : 'temporary'}
|
||||
open={open}
|
||||
onClose={() => !isDesktop && setOpen(false)}
|
||||
ModalProps={{ keepMounted: true }}
|
||||
sx={{
|
||||
'& .MuiDrawer-paper': {
|
||||
width: isMobile ? '100vw' : drawerWidth,
|
||||
maxWidth: isMobile ? '100vw' : drawerWidth,
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
transition: 'transform 0.35s ease-in-out',
|
||||
zIndex: 13
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Search */}
|
||||
<Box sx={{ position: 'sticky', top: 0, zIndex: 1 }}>
|
||||
<SearchBar
|
||||
value={riderSearch}
|
||||
placeholder="Search Rider"
|
||||
onChange={(e) => setRiderSearch(e.target.value)}
|
||||
sx={{
|
||||
height: 60,
|
||||
bgcolor: 'white',
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderBottom: '1px solid',
|
||||
borderColor: theme.palette.secondary.light
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<List>
|
||||
<ListItem sx={{ cursor: 'pointer', '&:hover': { bgcolor: theme.palette.secondary.lighter }, bgcolor: 'white', mt: -1 }}>
|
||||
<ListItemAvatar>
|
||||
<Checkbox
|
||||
checked={riders?.length == selectedRiders?.length}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedRiders(riders);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="All" />
|
||||
</ListItem>
|
||||
<Divider />
|
||||
</List>
|
||||
</Box>
|
||||
{/* Rider List */}
|
||||
<List>
|
||||
{/* Individuals */}
|
||||
{ridersIsLoading || riderIsFetching
|
||||
? Array.from({ length: 10 }).map((_, index) => (
|
||||
<Fragment key={index}>
|
||||
<ListItem sx={{ py: 1.5, px: 2 }}>
|
||||
<ListItemAvatar>
|
||||
<Skeleton variant="circular" width={24} height={24} />
|
||||
</ListItemAvatar>
|
||||
|
||||
<ListItemText
|
||||
primary={<Skeleton variant="text" width="60%" height={22} />}
|
||||
secondary={<Skeleton variant="text" width="40%" height={18} />}
|
||||
/>
|
||||
|
||||
<Stack spacing={0.5} textAlign="right">
|
||||
<Skeleton variant="text" width={50} height={18} />
|
||||
<Skeleton variant="text" width={80} height={16} />
|
||||
</Stack>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
</Fragment>
|
||||
))
|
||||
: !isMobile &&
|
||||
riders?.map((row) => {
|
||||
return (
|
||||
<Fragment key={row.userid}>
|
||||
<ListItem
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
py: 1,
|
||||
px: 2,
|
||||
borderRadius: 1,
|
||||
'&:hover': { bgcolor: theme.palette.secondary.lighter }
|
||||
}}
|
||||
secondaryAction={
|
||||
<Stack textAlign="right" spacing={0.5}>
|
||||
<Typography variant="body2" noWrap sx={{ color: row.status == 'active' ? 'success.main' : 'error.main' }}>
|
||||
{row.userid}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" noWrap>
|
||||
{dayjs(row.logdate).format('DD/MM/YYYY hh:mm A')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Checkbox
|
||||
sx={{
|
||||
color: row.status == 'active' ? 'green' : 'red',
|
||||
'&.Mui-checked': {
|
||||
color: row.status == 'active' ? 'green' : 'red'
|
||||
}
|
||||
}}
|
||||
checked={
|
||||
// INDIVIDUAL CHECKED CONDITION
|
||||
selectedRiders?.length === 1 && selectedRiders[0]?.userid === row?.userid
|
||||
}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
// SELECT ONE RIDER
|
||||
setSelectedRiders([row]);
|
||||
} else {
|
||||
// UNCHECK -> SELECT ALL
|
||||
setSelectedRiders(riders);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography noWrap>
|
||||
{row.username?.slice(0, 25) || ''}
|
||||
{row.username?.length > 25 && '...'}
|
||||
|
||||
{/* {row.status === 'active' && <TaskAltIcon fontSize="small" color="success" sx={{ ml: 1 }} />} */}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Typography variant="caption" color="text.secondary" noWrap>
|
||||
{row.contactno || '##########'}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
|
||||
{/* Mobile: rider rows rendered as app-style cards (same selection behaviour) */}
|
||||
{isMobile && !ridersIsLoading && !riderIsFetching && (
|
||||
<MobileCardList>
|
||||
{riders?.map((row) => {
|
||||
const isActive = row.status == 'active';
|
||||
const isSelected = selectedRiders?.length === 1 && selectedRiders[0]?.userid === row?.userid;
|
||||
return (
|
||||
<MobileCard
|
||||
key={row.userid}
|
||||
accent={isActive ? '#10b981' : '#ef4444'}
|
||||
selected={isSelected}
|
||||
header={
|
||||
<Stack direction="row" alignItems="flex-start" spacing={1}>
|
||||
<Checkbox
|
||||
sx={{
|
||||
p: 0.5,
|
||||
color: isActive ? 'green' : 'red',
|
||||
'&.Mui-checked': { color: isActive ? 'green' : 'red' }
|
||||
}}
|
||||
checked={isSelected}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedRiders([row]);
|
||||
} else {
|
||||
setSelectedRiders(riders);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
|
||||
<Typography noWrap sx={{ fontWeight: 600 }}>
|
||||
{row.username?.slice(0, 25) || ''}
|
||||
{row.username?.length > 25 && '...'}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" noWrap>
|
||||
{row.contactno || '##########'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<MobileFieldGrid>
|
||||
<MobileField label="User ID">
|
||||
<Typography sx={{ fontSize: 13, fontWeight: 600, color: isActive ? 'success.main' : 'error.main' }} noWrap>
|
||||
{row.userid}
|
||||
</Typography>
|
||||
</MobileField>
|
||||
<MobileField label="Status" value={isActive ? 'Active' : 'Inactive'} />
|
||||
<MobileField label="Last Log" value={dayjs(row.logdate).format('DD/MM/YYYY hh:mm A')} full />
|
||||
</MobileFieldGrid>
|
||||
</MobileCard>
|
||||
);
|
||||
})}
|
||||
</MobileCardList>
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
{/* AppBar */}
|
||||
<AppBar
|
||||
elevation={0}
|
||||
position="absolute"
|
||||
sx={{
|
||||
top: 0,
|
||||
left: open && isDesktop ? `${drawerWidth}px` : 0,
|
||||
width: open && isDesktop ? `calc(100% - ${drawerWidth}px)` : '100%',
|
||||
transition: 'left 0.3s ease, width 0.3s ease',
|
||||
backgroundColor: 'white',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: theme.palette.secondary.light
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: '100%' }}>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<IconButton color="primary" onClick={() => setOpen(!open)}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
||||
<Typography variant="h5" color="primary" sx={{ ml: 2 }}>
|
||||
Riders Locations
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
riderLogsRefetch();
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Stack>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{/* Map */}
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
overflow: 'auto',
|
||||
pt: '64px',
|
||||
pl: open && isDesktop ? `${drawerWidth}px` : 0,
|
||||
transition: 'padding-left 0.3s ease',
|
||||
minHeight: '80vh'
|
||||
}}
|
||||
>
|
||||
{(ridersIsLoading || riderIsFetching) && (
|
||||
<Box position="relative" width="100%" height="80vh" display="grid" placeItems="center">
|
||||
{/* <CircularLoader /> */}
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
width="100%"
|
||||
height="100%"
|
||||
animation="wave"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
borderRadius: 1,
|
||||
zIndex: 1
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{selectedRiders?.length > 0 && <RiderLocationMap riderLocations={selectedRiders} />}
|
||||
{riderLogsError && (
|
||||
<Box sx={{ width: '100% ', height: '100%' }}>
|
||||
<img src={error500} alt="mantis" style={{ height: '100%', width: '100%' }} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</MainCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default RidersLogs;
|
||||
@@ -86,8 +86,8 @@ const soft = (c) => a(c, '18');
|
||||
const ring = (c) => a(c, '26');
|
||||
const edge = (c) => a(c, '55');
|
||||
|
||||
const BRAND = '#662582';
|
||||
const BRAND_LIGHT = '#9255AB';
|
||||
const BRAND = '#C01227';
|
||||
const BRAND_LIGHT = '#D25463';
|
||||
|
||||
const SoftPaper = (props) => (
|
||||
<Paper
|
||||
@@ -265,17 +265,9 @@ export default function RidersSummary() {
|
||||
const getuserdeliverylogs = async (userid) => {
|
||||
setRouteLoading(true);
|
||||
try {
|
||||
// /deliveries/getdeliveries treats applocationid=0 differently from a
|
||||
// real location id — when appId===0 ("All") the backend expects the
|
||||
// logged-in operator's userid via appuserid instead. Mirrors the
|
||||
// branching in api.js#fetchDeliveries.
|
||||
const loggedInUserId = typeof window !== 'undefined' ? localStorage.getItem('userid') || 0 : 0;
|
||||
const scopeParam = appId === 0
|
||||
? `appuserid=${loggedInUserId}`
|
||||
: `applocationid=${appId}`;
|
||||
const url =
|
||||
`${process.env.REACT_APP_URL}/deliveries/getdeliveries/` +
|
||||
`?${scopeParam}` +
|
||||
`?applocationid=${appId}` +
|
||||
`&status=all` +
|
||||
`&fromdate=${startdate}` +
|
||||
`&todate=${enddate}` +
|
||||
@@ -456,7 +448,7 @@ export default function RidersSummary() {
|
||||
bgcolor: '#ffffff',
|
||||
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
|
||||
'&:hover fieldset': { borderColor: '#cbd5e1' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
|
||||
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user