1010 lines
46 KiB
JavaScript
1010 lines
46 KiB
JavaScript
import React, { useState, useMemo } from 'react';
|
||
import { useQuery } from '@tanstack/react-query';
|
||
|
||
// material-ui
|
||
import {
|
||
Avatar,
|
||
Box,
|
||
Chip,
|
||
Collapse,
|
||
Dialog,
|
||
DialogContent,
|
||
Divider,
|
||
Grid,
|
||
IconButton,
|
||
Paper,
|
||
Stack,
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableContainer,
|
||
TableHead,
|
||
TableRow,
|
||
Tooltip,
|
||
Typography,
|
||
useMediaQuery,
|
||
useTheme
|
||
} from '@mui/material';
|
||
import {
|
||
MdDirectionsBike,
|
||
MdCalendarMonth,
|
||
MdLocalShipping,
|
||
MdCheckCircle,
|
||
MdHourglassEmpty,
|
||
MdCancel,
|
||
MdCurrencyRupee,
|
||
MdStraighten,
|
||
MdMap,
|
||
MdExpandMore,
|
||
MdExpandLess,
|
||
MdGroups,
|
||
MdPerson,
|
||
MdOutlineLocalShipping,
|
||
MdOutlineCheckCircle,
|
||
MdOutlineCurrencyRupee
|
||
} from 'react-icons/md';
|
||
|
||
import dayjs from 'dayjs';
|
||
var utc = require('dayjs/plugin/utc');
|
||
dayjs.extend(utc);
|
||
|
||
import { fetchRidersSummary } 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 { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
|
||
import RidersRoutes from './RidersRoutes';
|
||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||
import { DT, tint, soft, ring, edge } from 'themes/dt/tokens';
|
||
import { AccentAvatar } from 'themes/dt/primitives';
|
||
|
||
const BRAND = '#000000';
|
||
const BRAND_LIGHT = '#3A3A3A';
|
||
|
||
// Pill used in cells for numeric / km / amount metrics.
|
||
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>
|
||
);
|
||
|
||
// Coloured numeric cell — non-zero values get a soft red badge to draw the eye,
|
||
// preserving the legacy "red when present" affordance.
|
||
const CountCell = ({ value, color = '#ef4444', icon }) => {
|
||
const n = Number(value) || 0;
|
||
if (n === 0) {
|
||
return (
|
||
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 700 }}>
|
||
0
|
||
</Typography>
|
||
);
|
||
}
|
||
return (
|
||
<Box
|
||
sx={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: 0.5,
|
||
px: 0.875,
|
||
py: 0.25,
|
||
borderRadius: 999,
|
||
bgcolor: '#ffffff',
|
||
border: `1px solid ${edge(color)}`,
|
||
color,
|
||
fontSize: 12,
|
||
fontWeight: 800,
|
||
minWidth: 36,
|
||
justifyContent: 'center'
|
||
}}
|
||
>
|
||
{icon}
|
||
{n}
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
function formatNumberToRupees(value) {
|
||
return new Intl.NumberFormat('en-IN', {
|
||
style: 'currency',
|
||
currency: 'INR',
|
||
minimumFractionDigits: 2
|
||
}).format(Number(value) || 0);
|
||
}
|
||
|
||
// ==============================|| Riders Summary ||============================== //
|
||
|
||
export default function RidersSummary() {
|
||
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 [tenantData, setTenantData] = useState([]);
|
||
const [openRow, setOpenRow] = useState(null);
|
||
const [appId, setAppId] = useState(0);
|
||
const [loading, setLoading] = useState(false);
|
||
const [mapOpen, setMapOpen] = useState(false);
|
||
const [logDetails, setLogDetails] = useState(null);
|
||
const [selectedRider, setSelectedRider] = useState(null);
|
||
const [routeLoading, setRouteLoading] = useState(false);
|
||
const [searchword, setSearchword] = useState('');
|
||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||
|
||
// ==============================|| fetchRidersSummary ||============================== //
|
||
const { isLoading: isLoadingReports, data: rows } = useQuery({
|
||
queryKey: ['ridersummary', appId, startdate, enddate],
|
||
queryFn: fetchRidersSummary
|
||
});
|
||
|
||
// Client-side filter across rider name + id.
|
||
// GET /admin/milers/summary confirmed live: { milerprofileid, userid,
|
||
// displayname, phone, availabilitystatus, rating, assigned, accepted,
|
||
// rejected, completed, cancelled, delivered, riderkms, ridercharges, ... }
|
||
// — no firstname/lastname/charges/deliveryamt/totalorders/pending/kms/
|
||
// cumulativekms field exists at all, which is why this whole page showed
|
||
// "—" everywhere.
|
||
const filteredRows = useMemo(() => {
|
||
if (!rows) return [];
|
||
if (!debouncedSearch) return rows;
|
||
const q = debouncedSearch.toLowerCase().trim();
|
||
return rows.filter((r) => [r.displayname, String(r.userid)].filter(Boolean).some((field) => String(field).toLowerCase().includes(q)));
|
||
}, [rows, debouncedSearch]);
|
||
|
||
// Aggregate KPIs from the loaded summary.
|
||
const stats = useMemo(() => {
|
||
if (!rows || rows.length === 0) return { riders: 0, orders: 0, delivered: 0, amount: 0 };
|
||
return rows.reduce(
|
||
(acc, r) => {
|
||
acc.riders += 1;
|
||
acc.orders += Number(r.assigned) || 0;
|
||
acc.delivered += Number(r.delivered) || 0;
|
||
acc.amount += Number(r.ridercharges) || 0;
|
||
return acc;
|
||
},
|
||
{ riders: 0, orders: 0, delivered: 0, amount: 0 }
|
||
);
|
||
}, [rows]);
|
||
|
||
// No per-rider tenant-breakdown endpoint in the new API.
|
||
const fetchTenantSummary = async () => {
|
||
setLoading(true);
|
||
setTenantData(null);
|
||
setLoading(false);
|
||
};
|
||
|
||
// ==============================|| rider planned route (for map) ||============================== //
|
||
// /admin/consignments has no documented per-row schema (lat/lng/step/
|
||
// customer fields aren't shown anywhere in the doc), so building a planned
|
||
// route from it would be guesswork that risks plotting wrong pins on the
|
||
// map. Degrades to an empty route instead.
|
||
const getuserdeliverylogs = async () => {
|
||
setRouteLoading(true);
|
||
setLogDetails([]);
|
||
setRouteLoading(false);
|
||
};
|
||
|
||
// Total Amount sum (preserved from legacy bottom bar).
|
||
const total = useMemo(() => {
|
||
if (!rows) return 0;
|
||
return rows.reduce((sum, row) => sum + (Number(row.ridercharges) || 0), 0);
|
||
}, [rows]);
|
||
|
||
const KPI_META = [
|
||
{ key: 'riders', label: 'Active Riders', color: BRAND, icon: MdDirectionsBike, value: stats.riders },
|
||
{ key: 'orders', label: 'Total Orders', color: '#0ea5e9', icon: MdOutlineLocalShipping, value: stats.orders },
|
||
{ key: 'delivered', label: 'Delivered', color: '#10b981', icon: MdOutlineCheckCircle, value: stats.delivered },
|
||
{ key: 'amount', label: 'Total Amount', color: '#f59e0b', icon: MdOutlineCurrencyRupee, value: formatNumberToRupees(total) }
|
||
];
|
||
|
||
return (
|
||
<>
|
||
{(isLoadingReports || loading) && <Loader />}
|
||
|
||
{/* ============================================= || Header || ============================================= */}
|
||
<PageHeader
|
||
title="Riders Summary"
|
||
subtitle={`Live · ${locaName || 'All Zones'} · ${datestatus}`}
|
||
live
|
||
action={
|
||
<LocationAutocomplete
|
||
locaName={locaName}
|
||
setAppId={setAppId}
|
||
setLocoName={setLocoName}
|
||
|
||
|
||
|
||
placeholder="Select Zone"
|
||
|
||
|
||
/>
|
||
}
|
||
/>
|
||
|
||
{/* ============================================= || KPI Cards || ============================================= */}
|
||
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
|
||
{KPI_META.map((item) => {
|
||
const Icon = item.icon;
|
||
return (
|
||
<Grid item key={item.key} xs={6} sm={6} md={3}>
|
||
<StatCard
|
||
title={item.label}
|
||
value={item.value ?? 0}
|
||
icon={<Icon size={20} />}
|
||
color={item.color}
|
||
loading={isLoadingReports}
|
||
/>
|
||
</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}>
|
||
<MdDirectionsBike size={18} />
|
||
</AccentAvatar>
|
||
<Stack>
|
||
<Typography
|
||
variant="caption"
|
||
sx={{ fontWeight: 800, color: DT.textSecondary, letterSpacing: 0.6, textTransform: 'uppercase' }}
|
||
>
|
||
Orders · {datestatus}
|
||
</Typography>
|
||
<Typography variant="body2" sx={{ color: DT.textPrimary, fontWeight: 700 }}>
|
||
{filteredRows.length} riders · {stats.orders} orders
|
||
</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 (ctrl+k)"
|
||
|
||
/>
|
||
</Box>
|
||
</Stack>
|
||
</Paper>
|
||
|
||
{/* ============================================= || Table || ============================================= */}
|
||
<Paper
|
||
elevation={0}
|
||
sx={{
|
||
borderTopLeftRadius: 0,
|
||
borderTopRightRadius: 0,
|
||
borderBottomLeftRadius: DT.radiusCard / 8,
|
||
borderBottomRightRadius: DT.radiusCard / 8,
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
overflow: 'hidden',
|
||
background: '#fff'
|
||
}}
|
||
>
|
||
{isMobile ? (
|
||
<MobileCardList scroll>
|
||
{isLoadingReports && (
|
||
<Stack alignItems="center" sx={{ py: 4 }}>
|
||
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 700 }}>
|
||
Loading riders…
|
||
</Typography>
|
||
</Stack>
|
||
)}
|
||
{(!filteredRows || filteredRows.length === 0) && !isLoadingReports ? (
|
||
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
|
||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||
<MdDirectionsBike size={28} />
|
||
</Avatar>
|
||
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||
No riders to show
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||
{searchword ? 'Try a different keyword.' : 'Pick a zone or date range to load the summary.'}
|
||
</Typography>
|
||
</Stack>
|
||
) : (
|
||
filteredRows.map((row, index) => {
|
||
const isOpen = openRow === row.userid;
|
||
const amount = Number(row.ridercharges) || 0;
|
||
const riderName = row?.displayname || '—';
|
||
return (
|
||
<MobileCard
|
||
key={row.userid || index}
|
||
accent={BRAND}
|
||
selected={isOpen}
|
||
header={
|
||
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
|
||
<Stack direction="row" alignItems="center" spacing={1} sx={{ minWidth: 0 }}>
|
||
<AccentAvatar color={BRAND} size={36}>
|
||
<MdPerson size={18} />
|
||
</AccentAvatar>
|
||
<Stack sx={{ minWidth: 0 }}>
|
||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||
{riderName}
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||
#{String(index + 1).padStart(2, '0')} · ID #{row.userid}
|
||
</Typography>
|
||
</Stack>
|
||
</Stack>
|
||
<Stack direction="row" spacing={0.75} sx={{ flexShrink: 0 }}>
|
||
<Tooltip title="View planned route" placement="top">
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => {
|
||
setSelectedRider({
|
||
userid: row?.userid,
|
||
name: row?.displayname || `Rider ${row?.userid}`
|
||
});
|
||
setLogDetails(null);
|
||
setMapOpen(true);
|
||
getuserdeliverylogs(row?.userid);
|
||
}}
|
||
sx={{
|
||
bgcolor: soft('#0ea5e9'),
|
||
color: '#0ea5e9',
|
||
border: `1px solid ${edge('#0ea5e9')}`,
|
||
'&:hover': { bgcolor: '#0ea5e9', color: '#fff' }
|
||
}}
|
||
>
|
||
<MdMap size={14} />
|
||
</IconButton>
|
||
</Tooltip>
|
||
<Tooltip title={isOpen ? 'Collapse breakdown' : 'Expand breakdown'} placement="top">
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => {
|
||
const isOpening = !isOpen;
|
||
setOpenRow(isOpening ? row.userid : null);
|
||
if (isOpening) fetchTenantSummary(row.userid);
|
||
}}
|
||
sx={{
|
||
bgcolor: isOpen ? BRAND : soft(BRAND),
|
||
color: isOpen ? '#fff' : BRAND,
|
||
border: `1px solid ${edge(BRAND)}`,
|
||
'&:hover': { bgcolor: BRAND, color: '#fff' }
|
||
}}
|
||
>
|
||
{isOpen ? <MdExpandLess size={16} /> : <MdExpandMore size={16} />}
|
||
</IconButton>
|
||
</Tooltip>
|
||
</Stack>
|
||
</Stack>
|
||
}
|
||
>
|
||
<MobileFieldGrid columns={2}>
|
||
<MobileField label="Orders">
|
||
<CountCell value={row.assigned} color={BRAND} icon={<MdLocalShipping size={11} />} />
|
||
</MobileField>
|
||
<MobileField label="Delivered">
|
||
<CountCell value={row.delivered} color="#10b981" icon={<MdCheckCircle size={11} />} />
|
||
</MobileField>
|
||
<MobileField label="Accepted">
|
||
<CountCell value={row.accepted} color="#f59e0b" icon={<MdHourglassEmpty size={11} />} />
|
||
</MobileField>
|
||
<MobileField label="Cancelled">
|
||
<CountCell value={row.cancelled} color="#ef4444" icon={<MdCancel size={11} />} />
|
||
</MobileField>
|
||
<MobileField label="KMS">
|
||
<MetricPill
|
||
color="#ef4444"
|
||
icon={<MdStraighten size={11} />}
|
||
label={`${Number(row.riderkms || 0).toFixed(2)} km`}
|
||
tooltip="KMS"
|
||
/>
|
||
</MobileField>
|
||
<MobileField label="Amount" full>
|
||
<MetricPill
|
||
color={BRAND}
|
||
icon={<MdCurrencyRupee size={11} />}
|
||
label={formatNumberToRupees(amount).replace('₹', '').trim()}
|
||
tooltip="Total Amount"
|
||
minWidth={100}
|
||
/>
|
||
</MobileField>
|
||
</MobileFieldGrid>
|
||
|
||
{/* per-tenant breakdown */}
|
||
{isOpen && (
|
||
<Collapse in={isOpen} timeout="auto" unmountOnExit>
|
||
<Paper
|
||
elevation={0}
|
||
sx={{
|
||
mt: 1.25,
|
||
borderRadius: DT.radiusCard / 8,
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
overflow: 'hidden',
|
||
boxShadow: DT.shadowSoft
|
||
}}
|
||
>
|
||
<Stack
|
||
direction="row"
|
||
alignItems="center"
|
||
spacing={1}
|
||
sx={{
|
||
px: 1.5,
|
||
py: 1,
|
||
background: '#ffffff',
|
||
borderBottom: `1px solid ${DT.borderSubtle}`
|
||
}}
|
||
>
|
||
<AccentAvatar color={BRAND} size={24} selected>
|
||
<MdGroups size={12} />
|
||
</AccentAvatar>
|
||
<Typography
|
||
variant="caption"
|
||
sx={{ fontWeight: 800, color: DT.textPrimary, letterSpacing: 0.6, textTransform: 'uppercase' }}
|
||
>
|
||
Tenant Breakdown
|
||
</Typography>
|
||
</Stack>
|
||
<Stack divider={<Divider />}>
|
||
{loading && (
|
||
<Typography variant="caption" sx={{ color: DT.textMuted, textAlign: 'center', py: 2 }}>
|
||
Loading…
|
||
</Typography>
|
||
)}
|
||
{!loading && (!tenantData || tenantData.length === 0) ? (
|
||
<Typography variant="caption" sx={{ color: DT.textMuted, textAlign: 'center', py: 2 }}>
|
||
No tenant breakdown available.
|
||
</Typography>
|
||
) : (
|
||
tenantData?.map((sub, sidx) => (
|
||
<Box key={`${sub.tenantname}-${sidx}`} sx={{ p: 1.5 }}>
|
||
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ mb: 1 }}>
|
||
<AccentAvatar color="#0ea5e9" size={24}>
|
||
<MdGroups size={12} />
|
||
</AccentAvatar>
|
||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||
{sub.tenantname || '—'}
|
||
</Typography>
|
||
</Stack>
|
||
<MobileFieldGrid columns={2}>
|
||
<MobileField label="All">
|
||
<CountCell value={sub.totalorders} color={BRAND} icon={<MdLocalShipping size={10} />} />
|
||
</MobileField>
|
||
<MobileField label="Completed">
|
||
<CountCell value={sub.deliveriescompleted} color="#10b981" icon={<MdCheckCircle size={10} />} />
|
||
</MobileField>
|
||
<MobileField label="Pending">
|
||
<CountCell value={sub.deliveriespending} color="#f59e0b" icon={<MdHourglassEmpty size={10} />} />
|
||
</MobileField>
|
||
<MobileField label="Cancelled">
|
||
<CountCell value={sub.deliveriescancelled} color="#ef4444" icon={<MdCancel size={10} />} />
|
||
</MobileField>
|
||
<MobileField label="KMS">
|
||
<MetricPill
|
||
color="#ef4444"
|
||
icon={<MdStraighten size={10} />}
|
||
label={`${Number(sub.kms || 0).toFixed(2)} km`}
|
||
tooltip="KMS"
|
||
/>
|
||
</MobileField>
|
||
<MobileField label="Actual KMS">
|
||
<MetricPill
|
||
color="#10b981"
|
||
icon={<MdStraighten size={10} />}
|
||
label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`}
|
||
tooltip="Actual KMS"
|
||
/>
|
||
</MobileField>
|
||
<MobileField label="Amount" full>
|
||
<MetricPill
|
||
color={BRAND}
|
||
icon={<MdCurrencyRupee size={10} />}
|
||
label={formatNumberToRupees(
|
||
Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0)
|
||
)
|
||
.replace('₹', '')
|
||
.trim()}
|
||
tooltip="Total Amount"
|
||
minWidth={100}
|
||
/>
|
||
</MobileField>
|
||
</MobileFieldGrid>
|
||
</Box>
|
||
))
|
||
)}
|
||
</Stack>
|
||
</Paper>
|
||
</Collapse>
|
||
)}
|
||
</MobileCard>
|
||
);
|
||
})
|
||
)}
|
||
</MobileCardList>
|
||
) : (
|
||
<TableContainer
|
||
sx={{
|
||
maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' },
|
||
'&::-webkit-scrollbar': { width: 10, height: 10 },
|
||
'&::-webkit-scrollbar-thumb': {
|
||
backgroundColor: edge(BRAND),
|
||
borderRadius: 8,
|
||
'&:hover': { backgroundColor: BRAND }
|
||
},
|
||
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
|
||
}}
|
||
>
|
||
<Table stickyHeader sx={{ minWidth: { xs: 880, md: 1080 } }}>
|
||
<TableHead>
|
||
<TableRow
|
||
sx={{
|
||
'& th': {
|
||
backgroundColor: DT.surfaceAlt,
|
||
color: DT.textSecondary,
|
||
fontSize: { xs: 10, md: 11 },
|
||
fontWeight: 800,
|
||
letterSpacing: 0.6,
|
||
textTransform: 'uppercase',
|
||
whiteSpace: 'nowrap',
|
||
borderBottom: `1px solid ${DT.borderSubtle}`,
|
||
py: { xs: 1, md: 1.25 },
|
||
px: { xs: 1, md: 2 }
|
||
}
|
||
}}
|
||
>
|
||
<TableCell>#</TableCell>
|
||
<TableCell>Rider</TableCell>
|
||
<TableCell align="center">Orders</TableCell>
|
||
<TableCell align="center">Accepted</TableCell>
|
||
<TableCell align="center">Cancelled</TableCell>
|
||
<TableCell align="center">Delivered</TableCell>
|
||
<TableCell align="center">KMS</TableCell>
|
||
<TableCell align="center">Amount</TableCell>
|
||
<TableCell align="right">Action</TableCell>
|
||
</TableRow>
|
||
</TableHead>
|
||
|
||
<TableBody>
|
||
{isLoadingReports && <OrdersTableSkeleton col={5} />}
|
||
{(!filteredRows || filteredRows.length === 0) && !isLoadingReports ? (
|
||
<TableRow>
|
||
<TableCell colSpan={9} sx={{ py: 6 }}>
|
||
<Stack alignItems="center" spacing={1.5}>
|
||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||
<MdDirectionsBike size={28} />
|
||
</Avatar>
|
||
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||
No riders to show
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||
{searchword ? 'Try a different keyword.' : 'Pick a zone or date range to load the summary.'}
|
||
</Typography>
|
||
</Stack>
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
filteredRows.map((row, index) => {
|
||
const isOpen = openRow === row.userid;
|
||
const amount = Number(row.ridercharges) || 0;
|
||
return (
|
||
<React.Fragment key={row.userid || index}>
|
||
<TableRow
|
||
sx={{
|
||
transition: 'background-color 0.15s',
|
||
'& td': {
|
||
borderBottom: isOpen ? 'none' : `1px solid ${DT.divider}`,
|
||
py: { xs: 1, md: 1.5 },
|
||
px: { xs: 1, md: 2 }
|
||
},
|
||
'&:hover': { backgroundColor: isOpen ? '#fff' : DT.surfaceAlt },
|
||
bgcolor: isOpen ? tint(BRAND) : 'transparent'
|
||
}}
|
||
>
|
||
<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?.displayname || '—'}
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||
ID #{row.userid}
|
||
</Typography>
|
||
</Stack>
|
||
</Stack>
|
||
</TableCell>
|
||
|
||
<TableCell align="center">
|
||
<CountCell value={row.assigned} color={BRAND} icon={<MdLocalShipping size={11} />} />
|
||
</TableCell>
|
||
<TableCell align="center">
|
||
<CountCell value={row.accepted} color="#f59e0b" icon={<MdHourglassEmpty size={11} />} />
|
||
</TableCell>
|
||
<TableCell align="center">
|
||
<CountCell value={row.cancelled} color="#ef4444" icon={<MdCancel size={11} />} />
|
||
</TableCell>
|
||
<TableCell align="center">
|
||
<CountCell value={row.delivered} color="#10b981" icon={<MdCheckCircle size={11} />} />
|
||
</TableCell>
|
||
|
||
<TableCell align="center">
|
||
<MetricPill
|
||
color="#ef4444"
|
||
icon={<MdStraighten size={11} />}
|
||
label={`${Number(row.riderkms || 0).toFixed(2)} km`}
|
||
tooltip="KMS"
|
||
/>
|
||
</TableCell>
|
||
|
||
<TableCell align="center">
|
||
<MetricPill
|
||
color={BRAND}
|
||
icon={<MdCurrencyRupee size={11} />}
|
||
label={formatNumberToRupees(amount).replace('₹', '').trim()}
|
||
tooltip="Total Amount"
|
||
minWidth={100}
|
||
/>
|
||
</TableCell>
|
||
|
||
<TableCell align="right">
|
||
<Stack direction="row" spacing={0.75} justifyContent="flex-end">
|
||
<Tooltip title="View planned route" placement="top">
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => {
|
||
setSelectedRider({
|
||
userid: row?.userid,
|
||
name: row?.displayname || `Rider ${row?.userid}`
|
||
});
|
||
setLogDetails(null);
|
||
setMapOpen(true);
|
||
getuserdeliverylogs(row?.userid);
|
||
}}
|
||
sx={{
|
||
bgcolor: soft('#0ea5e9'),
|
||
color: '#0ea5e9',
|
||
border: `1px solid ${edge('#0ea5e9')}`,
|
||
'&:hover': { bgcolor: '#0ea5e9', color: '#fff' }
|
||
}}
|
||
>
|
||
<MdMap size={14} />
|
||
</IconButton>
|
||
</Tooltip>
|
||
<Tooltip title={isOpen ? 'Collapse breakdown' : 'Expand breakdown'} placement="top">
|
||
<IconButton
|
||
size="small"
|
||
onClick={() => {
|
||
const isOpening = !isOpen;
|
||
setOpenRow(isOpening ? row.userid : null);
|
||
if (isOpening) fetchTenantSummary(row.userid);
|
||
}}
|
||
sx={{
|
||
bgcolor: isOpen ? BRAND : soft(BRAND),
|
||
color: isOpen ? '#fff' : BRAND,
|
||
border: `1px solid ${edge(BRAND)}`,
|
||
'&:hover': { bgcolor: BRAND, color: '#fff' }
|
||
}}
|
||
>
|
||
{isOpen ? <MdExpandLess size={16} /> : <MdExpandMore size={16} />}
|
||
</IconButton>
|
||
</Tooltip>
|
||
</Stack>
|
||
</TableCell>
|
||
</TableRow>
|
||
|
||
{/* ============================================= || Collapsible per-tenant breakdown || ============================================= */}
|
||
{isOpen && (
|
||
<TableRow>
|
||
<TableCell
|
||
colSpan={9}
|
||
sx={{
|
||
p: 0,
|
||
borderBottom: `1px solid ${DT.divider}`,
|
||
bgcolor: tint(BRAND)
|
||
}}
|
||
>
|
||
<Collapse in={isOpen} timeout="auto" unmountOnExit>
|
||
<Box sx={{ p: { xs: 1, md: 2 } }}>
|
||
<Paper
|
||
elevation={0}
|
||
sx={{
|
||
borderRadius: DT.radiusCard / 8,
|
||
border: '1px solid',
|
||
borderColor: DT.borderSubtle,
|
||
overflow: 'hidden',
|
||
boxShadow: DT.shadowSoft
|
||
}}
|
||
>
|
||
<Stack
|
||
direction="row"
|
||
alignItems="center"
|
||
spacing={1}
|
||
sx={{
|
||
px: 2,
|
||
py: 1.25,
|
||
background: '#ffffff',
|
||
borderBottom: `1px solid ${DT.borderSubtle}`
|
||
}}
|
||
>
|
||
<AccentAvatar color={BRAND} size={28} selected>
|
||
<MdGroups size={14} />
|
||
</AccentAvatar>
|
||
<Typography
|
||
variant="caption"
|
||
sx={{ fontWeight: 800, color: DT.textPrimary, letterSpacing: 0.6, textTransform: 'uppercase' }}
|
||
>
|
||
Tenant Breakdown · {row?.displayname || ''}
|
||
</Typography>
|
||
</Stack>
|
||
|
||
<Table size="small">
|
||
<TableHead>
|
||
<TableRow
|
||
sx={{
|
||
'& th': {
|
||
backgroundColor: DT.surfaceAlt,
|
||
color: DT.textSecondary,
|
||
fontSize: 10,
|
||
fontWeight: 800,
|
||
letterSpacing: 0.6,
|
||
textTransform: 'uppercase',
|
||
whiteSpace: 'nowrap',
|
||
borderBottom: `1px solid ${DT.borderSubtle}`,
|
||
py: 1,
|
||
px: 1.5
|
||
}
|
||
}}
|
||
>
|
||
<TableCell>#</TableCell>
|
||
<TableCell>Client</TableCell>
|
||
<TableCell align="center">All</TableCell>
|
||
<TableCell align="center">Pending</TableCell>
|
||
<TableCell align="center">Completed</TableCell>
|
||
<TableCell align="center">Cancelled</TableCell>
|
||
<TableCell align="center">KMS</TableCell>
|
||
<TableCell align="center">Amount</TableCell>
|
||
</TableRow>
|
||
</TableHead>
|
||
<TableBody>
|
||
{loading && <OrdersTableSkeleton col={4} />}
|
||
{!loading && (!tenantData || tenantData.length === 0) ? (
|
||
<TableRow>
|
||
<TableCell colSpan={8} sx={{ py: 3 }}>
|
||
<Typography variant="caption" sx={{ color: DT.textMuted, textAlign: 'center', display: 'block' }}>
|
||
No tenant breakdown available.
|
||
</Typography>
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
tenantData?.map((sub, sidx) => (
|
||
<TableRow
|
||
key={`${sub.tenantname}-${sidx}`}
|
||
sx={{
|
||
'& td': {
|
||
borderBottom: `1px solid ${DT.divider}`,
|
||
py: 1,
|
||
px: 1.5
|
||
},
|
||
'&:hover': { backgroundColor: DT.surfaceAlt }
|
||
}}
|
||
>
|
||
<TableCell>
|
||
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
|
||
{String(sidx + 1).padStart(2, '0')}
|
||
</Typography>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Stack direction="row" alignItems="center" spacing={0.75}>
|
||
<AccentAvatar color="#0ea5e9" size={24}>
|
||
<MdGroups size={12} />
|
||
</AccentAvatar>
|
||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
|
||
{sub.tenantname || '—'}
|
||
</Typography>
|
||
</Stack>
|
||
</TableCell>
|
||
<TableCell align="center">
|
||
<CountCell value={sub.totalorders} color={BRAND} icon={<MdLocalShipping size={10} />} />
|
||
</TableCell>
|
||
<TableCell align="center">
|
||
<CountCell value={sub.deliveriespending} color="#f59e0b" icon={<MdHourglassEmpty size={10} />} />
|
||
</TableCell>
|
||
<TableCell align="center">
|
||
<CountCell value={sub.deliveriescompleted} color="#10b981" icon={<MdCheckCircle size={10} />} />
|
||
</TableCell>
|
||
<TableCell align="center">
|
||
<CountCell value={sub.deliveriescancelled} color="#ef4444" icon={<MdCancel size={10} />} />
|
||
</TableCell>
|
||
<TableCell align="center">
|
||
<Stack direction="column" spacing={0.5} alignItems="center">
|
||
<MetricPill
|
||
color="#ef4444"
|
||
icon={<MdStraighten size={10} />}
|
||
label={`${Number(sub.kms || 0).toFixed(2)} km`}
|
||
tooltip="KMS"
|
||
/>
|
||
<MetricPill
|
||
color="#10b981"
|
||
icon={<MdStraighten size={10} />}
|
||
label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`}
|
||
tooltip="Actual KMS"
|
||
/>
|
||
</Stack>
|
||
</TableCell>
|
||
<TableCell align="center">
|
||
<MetricPill
|
||
color={BRAND}
|
||
icon={<MdCurrencyRupee size={10} />}
|
||
label={formatNumberToRupees(
|
||
Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0)
|
||
)
|
||
.replace('₹', '')
|
||
.trim()}
|
||
tooltip="Total Amount"
|
||
minWidth={100}
|
||
/>
|
||
</TableCell>
|
||
</TableRow>
|
||
))
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</Paper>
|
||
</Box>
|
||
</Collapse>
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
</React.Fragment>
|
||
);
|
||
})
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</TableContainer>
|
||
)}
|
||
|
||
{/* ============================================= || Total Bar || ============================================= */}
|
||
{filteredRows.length > 0 && (
|
||
<>
|
||
<Divider />
|
||
<Stack
|
||
direction="row"
|
||
alignItems="center"
|
||
justifyContent="flex-end"
|
||
spacing={1.5}
|
||
sx={{
|
||
px: 2,
|
||
py: 1.5,
|
||
background: '#ffffff'
|
||
}}
|
||
>
|
||
<Typography
|
||
variant="caption"
|
||
sx={{ fontWeight: 800, color: DT.textSecondary, letterSpacing: 0.6, textTransform: 'uppercase' }}
|
||
>
|
||
Grand Total
|
||
</Typography>
|
||
<Chip
|
||
label={formatNumberToRupees(total)}
|
||
sx={{
|
||
fontWeight: 800,
|
||
fontSize: 13,
|
||
color: '#fff',
|
||
background: `linear-gradient(135deg, ${BRAND} 0%, ${BRAND_LIGHT} 100%)`,
|
||
boxShadow: `0 6px 16px ${ring(BRAND)}`
|
||
}}
|
||
/>
|
||
</Stack>
|
||
</>
|
||
)}
|
||
</Paper>
|
||
|
||
{/* ============================================= || Map Dialog || ============================================= */}
|
||
<Dialog
|
||
maxWidth="xl"
|
||
fullScreen
|
||
open={mapOpen}
|
||
onClose={() => {
|
||
setMapOpen(false);
|
||
setLogDetails(null);
|
||
setSelectedRider(null);
|
||
}}
|
||
>
|
||
<DialogContent sx={{ p: 0, height: '100vh' }}>
|
||
<RidersRoutes
|
||
details={logDetails}
|
||
loading={routeLoading}
|
||
riderName={selectedRider?.name}
|
||
dateRange={`${dayjs(startdate).format('DD/MM/YY')} – ${dayjs(enddate).format('DD/MM/YY')}`}
|
||
onClose={() => {
|
||
setMapOpen(false);
|
||
setLogDetails(null);
|
||
setSelectedRider(null);
|
||
}}
|
||
/>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* ============================================= || Date Filter Dialog || ============================================= */}
|
||
<DateFilterDialog
|
||
open={open}
|
||
onClose={() => setOpen(false)}
|
||
onSelect={(range) => {
|
||
setStartdate(range.startDate);
|
||
setEnddate(range.endDate);
|
||
setDatestatus(range.label);
|
||
}}
|
||
/>
|
||
</>
|
||
);
|
||
}
|