Files
Doormilexpress_console/src/pages/nearle/riders/riders.js
dharaneesh-r 59f31c8adf Migrate console off jupiter.nearle.app to the Doormile Express API
Retires REACT_APP_URL/URL2/URL3 in favor of REACT_APP_DOORMILE_URL across
every page (orders, deliveries, riders, tenants, pricing, profile, reports,
dispatch). Fixes several field-mapping and envelope-check bugs found along
the way, most notably that /admin/milers/:id routes (block, assign-vehicle,
edit, notify) key off milerprofileid, not userid, and that a miler's real
fields are phone/availabilitystatus/displayname, not contactno/status/
firstname+lastname (confirmed against a live read-only session).

Also fixes several silent-failure bugs uncovered during that audit: order
creation and order cancellation showed a success toast but gave no feedback
at all on failure (createorder1.js had a dead notifyadmin() call that left
the loading spinner stuck forever on every failed submit), and Tenants.js's
pricing/profile updates never surfaced a failed response to the operator.

The AI dispatch optimiser (routes.workolik.com/routemate.workolik.com) and
its jupiter.nearle.app delivery-commit call remain untouched by design —
separate solver service with no equivalent in the new API.
2026-08-06 19:26:23 +05:30

1860 lines
83 KiB
JavaScript

import * as React from 'react';
import { useState, useEffect, useRef, Fragment } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Avatar,
Paper,
Stack,
Typography,
Table,
TableCell,
TableBody,
TableHead,
IconButton,
TableRow,
Tooltip,
TableContainer,
Backdrop,
Collapse,
Grid,
Box,
Skeleton,
useMediaQuery,
ToggleButtonGroup,
ToggleButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Autocomplete,
TextField
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
var utc = require('dayjs/plugin/utc');
import dayjs from 'dayjs';
dayjs.extend(utc);
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import {
MdMyLocation,
MdCheckCircle,
MdCancel,
MdGroups,
MdOutlineGroups,
MdOutlineCheckCircle,
MdOutlineCancel,
MdEdit,
MdKeyboardArrowDown,
MdKeyboardArrowUp,
MdLocationOn,
MdBatteryStd,
MdPowerSettingsNew,
MdSpeed,
MdGpsFixed,
MdAccessTime,
MdInventory2,
MdTwoWheeler,
MdArrowForward,
MdDelete,
MdBlock,
MdDirectionsCar
} from 'react-icons/md';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import CircularLoader from 'components/CircularLoader';
import { fetchAllRiders, getallridersummary } from 'pages/api/api';
import { getMilers, blockMiler, assignMilerVehicle, getVehicles } from 'pages/api/doormileApi';
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import { OpenToast } from 'components/third-party/OpenToast';
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 `#C01227` is the canonical primary; status
// colours are semantic and distinct from the brand.
// ============================================================================
const DT = {
radiusPill: 999,
radiusCard: 14,
shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)',
shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)',
shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)',
textPrimary: '#0f172a',
textSecondary: '#64748b',
textMuted: '#94a3b8',
borderSubtle: '#e2e8f0',
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc'
};
const a = (c, suffix) => `${c}${suffix}`;
const tint = (c) => a(c, '08');
const soft = (c) => a(c, '18');
const ring = (c) => a(c, '26');
const edge = (c) => a(c, '55');
const BRAND = '#C01227';
const SoftPaper = (props) => (
<Paper
{...props}
sx={{
mt: 0.75,
borderRadius: 2,
boxShadow: DT.shadowPop,
border: '1px solid',
borderColor: 'divider',
overflow: 'hidden'
}}
/>
);
const AccentAvatar = ({ color, selected, size = 24, children }) => (
<Avatar
sx={{
width: size,
height: size,
bgcolor: selected ? color : soft(color),
color: selected ? '#fff' : color,
transition: 'background-color 0.15s, color 0.15s'
}}
>
{children}
</Avatar>
);
// Status palette — semantic only (do NOT swap for brand purple). Used by the
// per-row status badges and the lifecycle tabs (ALL, Active).
// GET /admin/milers returns `availabilitystatus` (confirmed live: Available,
// Assigned, On_Pickup, Offline) — not the "active"/"inactive" this used to
// assume (there is no `status` field on a miler at all).
const STATUS_META = {
active: { label: 'Active', color: '#10b981', icon: MdCheckCircle },
inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel },
online: { label: 'Online', color: '#10b981', icon: MdCheckCircle },
offline: { label: 'Offline', color: '#ef4444', icon: MdCancel },
available: { label: 'Available', color: '#10b981', icon: MdCheckCircle },
assigned: { label: 'Assigned', color: '#6366f1', icon: MdTwoWheeler },
on_pickup: { label: 'On Pickup', color: '#14b8a6', icon: MdTwoWheeler },
idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime },
unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 }
};
// Pill-tab definitions for the rider listing tabs. Keeps brand purple for the
// "ALL" view and emerald for "Active" so the colour matches the count's meaning.
const TAB_META = [
{ key: 0, label: 'All Riders', color: BRAND, icon: MdGroups, countKey: 'total' },
{ key: 1, label: 'Active', color: BRAND, icon: MdCheckCircle, countKey: 'active' },
{ key: 2, label: 'Substitutes', color: BRAND, icon: MdTwoWheeler, countKey: 'substitute' },
{ key: 3, label: 'Substitution History', color: BRAND, icon: MdAccessTime, countKey: 'history' }
];
const KPI_META = (summary) => [
{ key: 'total', label: 'Total Riders', color: BRAND, icon: MdOutlineGroups, value: summary?.total ?? 0 },
{ key: 'active', label: 'Active Riders', color: '#10b981', icon: MdOutlineCheckCircle, value: summary?.active ?? 0 },
{ key: 'inactive', label: 'Inactive Riders', color: '#ef4444', icon: MdOutlineCancel, value: summary?.inactive ?? 0 }
];
const Riders = () => {
const queryClient = useQueryClient();
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const loadMoreRef = useRef();
const containerRef = useRef();
const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const [locaName, setLocoName] = useState('All');
const [appId, setAppId] = useState(0);
const [tabvalue, setTabvalue] = useState(0);
const roleid = localStorage.getItem('roleid');
const totalCols = roleid == 1 ? 11 : 10;
const [logsRow, setLogsRow] = useState(null);
const [riderLogsdata] = useState(null);
const [historyDate, setHistoryDate] = useState(dayjs());
const [historyStatus, setHistoryStatus] = useState('all');
const [selectedDate, setSelectedDate] = useState(dayjs());
const [editingSub, setEditingSub] = useState(null);
const [newSubRider, setNewSubRider] = useState(null);
const getHistoryStatusColor = (status) => {
const s = (status || '').toLowerCase();
if (s === 'cancelled' || s === 'deleted' || s === 'inactive') return '#ef4444'; // Red
if (s === 'scheduled') return '#0ea5e9'; // Blue
if (s === 'active' || s === 'completed') return '#10b981'; // Green
return '#64748b'; // Gray
};
// The rider-substitution feature (/substitutions/*) has no equivalent in the
// new Doormile Express API at all — no such resource is documented. These
// handlers now no-op with an explanatory toast instead of calling jupiter.
const handleDeleteSubstitution = async (row) => {
if (window.confirm(`Are you sure you want to delete the substitution for ${row.absent_rider_name}?`)) {
OpenToast('Substitutions are not available on the Doormile Express API yet.', 'warning', 2500);
}
};
const handleUpdateSubstitutionSubmit = async () => {
if (!newSubRider) {
OpenToast('Please select a substitute rider', 'error', 2000);
return;
}
OpenToast('Substitutions are not available on the Doormile Express API yet.', 'warning', 2500);
setEditingSub(null);
setNewSubRider(null);
};
const [substituteAssignments, setSubstituteAssignments] = useState(() => {
try {
const saved = localStorage.getItem('rider_substitutions');
return saved ? JSON.parse(saved) : {};
} catch {
return {};
}
});
const handleAssignSubstitute = (activeRiderId, substituteRider) => {
const updated = {
...substituteAssignments,
[activeRiderId]: substituteRider
};
setSubstituteAssignments(updated);
localStorage.setItem('rider_substitutions', JSON.stringify(updated));
if (substituteRider) {
OpenToast(`Assigned ${substituteRider.username || substituteRider.fullname || 'Rider'} as substitute`, 'success', 2000);
} else {
OpenToast(`Removed substitute assignment`, 'info', 2000);
}
};
const handleFinalizeSuccess = () => {
setSubstituteAssignments({});
localStorage.removeItem('rider_substitutions');
queryClient.invalidateQueries({ queryKey: ['substitutionsHistory'] });
queryClient.invalidateQueries({ queryKey: ['subRidersLogs'] });
queryClient.invalidateQueries({ queryKey: ['activeSubstitutionsToday'] });
};
const { data: substituteRidersList } = useQuery({
queryKey: ['allRidersForSub', appId],
queryFn: async () => {
try {
const allRiders = (await getMilers()) || [];
const allowedIds = [1121, 772, 1116];
return allRiders.filter((rider) => allowedIds.includes(parseInt(rider.userid ?? rider.milerid)));
} catch (err) {
console.error(err);
return [];
}
}
});
// No rider login/checkout audit-log endpoint in the new API.
const { data: subRidersLogsData, isLoading: subRidersLogsLoading } = useQuery({
queryKey: ['subRidersLogs', appId, debouncedSearch, selectedDate.format('YYYY-MM-DD')],
queryFn: async () => []
});
// The /substitutions resource has no equivalent in the new API.
const { data: subHistoryData, isLoading: subHistoryLoading } = useQuery({
queryKey: ['substitutionsHistory', appId, historyDate, historyStatus],
queryFn: async () => []
});
const handleChangetab = (i) => {
setTabvalue(i);
setLogsRow(null);
};
// ==============================|| getallridersummary||============================== //
const { data: allRidersSummary, isLoading: riderSummarysLoading } = useQuery({
queryKey: ['allriders', appId, tabvalue],
queryFn: getallridersummary
});
// ==============================|| getRiderLogs (riders)||============================== //
// No live GPS/battery/periodic-log endpoint in the new API — the expanded
// row still opens, showing the panel's built-in "—" placeholders.
const getRiderLogs = async () => {
OpenToast('Live rider logs are not available on the Doormile Express API yet.', 'warning', 2500);
};
// ==============================|| fetchAllRiders||============================== //
// GET /admin/milers isn't paginated (fetchAllRiders always returns the full
// list in one shot) — getNextPageParam must not request a "next page" or it
// re-fetches and appends the identical full list again, duplicating every
// rider in `rows`.
const {
data: allRidersData,
isLoading: allRidersLoading,
isFetchingNextPage,
fetchNextPage,
hasNextPage
} = useInfiniteQuery({
queryKey: ['allriders', appId, debouncedSearch, tabvalue],
queryFn: fetchAllRiders,
getNextPageParam: () => undefined
});
const rows = (allRidersData?.pages.flatMap((page) => page.details || []) || []).filter(Boolean);
// GET /admin/milers accepts no search/status params — debouncedSearch and
// tabvalue only trigger a refetch (via the queryKey) that returns the exact
// same full list every time. Search and the "Active" tab were both silently
// no-ops until filtered client-side here.
const filteredRows = rows.filter((row) => {
// "Active" here means currently online/working (Available/Assigned/
// On_Pickup) — the only real "not working" state is Offline.
if (tabvalue === 1 && String(row.availabilitystatus || '').toLowerCase() === 'offline') return false;
if (!debouncedSearch) return true;
const q = debouncedSearch.toLowerCase();
return [row.displayname, row.phone].some((v) => String(v || '').toLowerCase().includes(q));
});
useEffect(() => {
if (!hasNextPage) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
fetchNextPage();
}
},
{
root: document.querySelector('.MuiTableContainer-root'),
rootMargin: '0px',
threshold: 1.0
}
);
if (loadMoreRef.current) observer.observe(loadMoreRef.current);
return () => {
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current);
};
}, [hasNextPage, fetchNextPage]);
const handleScroll = (event) => {
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
if (scrollTop + clientHeight >= scrollHeight - 50) {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}
};
// Per-row status meta — falls back to "unknown" if the rider's state key
// isn't in the palette (e.g. brand-new status string from the backend).
// Previously joined against a separate rider-status endpoint on every tab
// except "All Riders" — that endpoint has no equivalent in the new API and
// always returned null, so every rider on every other tab showed "Unknown"
// regardless of their real state. Now reads the miler's own
// `availabilitystatus` field directly on every tab (confirmed live — there
// is no `status` field on a miler at all).
const getRowStatusMeta = (row) => {
const key = (row?.availabilitystatus || '').toLowerCase();
return STATUS_META[key] || STATUS_META.unknown;
};
// PUT /admin/milers/:id/block — body shape isn't documented (Status: Built,
// never exercised against a real request), so no fields are sent beyond the
// id in the URL. Blocks whichever rider is passed after a confirm prompt.
const blockRiderMutation = useMutation({
// /admin/milers/:id routes key off milerprofileid, not userid (confirmed
// live — GET /admin/milers/:userid 404s; the miler's own userid field
// points at the underlying app-user account, a different resource).
mutationFn: (row) => blockMiler(row.milerprofileid),
onSuccess: (res) => {
if (res.success) {
OpenToast('Rider blocked', 'success', 2000);
queryClient.invalidateQueries({ queryKey: ['allriders'] });
} else {
OpenToast(res.message || 'Failed to block rider', 'error', 2000);
}
},
onError: (err) => {
OpenToast(err.response?.data?.message || err.message || 'Failed to block rider', 'error', 2000);
}
});
const handleBlockRider = (row) => {
if (window.confirm(`Block ${row.displayname || row.authname || 'this rider'}? They will not be able to log in to the miler app.`)) {
blockRiderMutation.mutate(row);
}
};
// PUT /admin/milers/:id/assign-vehicle — body shape isn't documented
// (Status: Built, never exercised), so this assumes `vehicleid` following
// the codebase's own <resource>id convention (GET /admin/vehicles/:id).
const [assignVehicleRider, setAssignVehicleRider] = useState(null);
const [selectedVehicle, setSelectedVehicle] = useState(null);
const { data: vehicles = [] } = useQuery({
queryKey: ['riders-vehicles'],
queryFn: getVehicles,
enabled: !!assignVehicleRider
});
const assignVehicleMutation = useMutation({
mutationFn: () => assignMilerVehicle(assignVehicleRider.milerprofileid, { vehicleid: selectedVehicle?.vehicleid }),
onSuccess: (res) => {
if (res.success) {
OpenToast('Vehicle assigned', 'success', 2000);
queryClient.invalidateQueries({ queryKey: ['allriders'] });
setAssignVehicleRider(null);
setSelectedVehicle(null);
} else {
OpenToast(res.message || 'Failed to assign vehicle', 'error', 2000);
}
},
onError: (err) => {
OpenToast(err.response?.data?.message || err.message || 'Failed to assign vehicle', 'error', 2000);
}
});
return (
<>
<Backdrop
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
open={
allRidersLoading ||
riderSummarysLoading ||
(tabvalue === 2 && subRidersLogsLoading) ||
(tabvalue === 3 && subHistoryLoading)
}
>
<CircularLoader color="inherit" />
</Backdrop>
{/* ============================================= || Header | ============================================= */}
<PageHeader
title="Riders"
subtitle={`Live · ${locaName || 'All Zones'}`}
live
action={
<LocationAutocomplete
locaName={locaName}
setAppId={setAppId}
setLocoName={setLocoName}
pill
accentColor={BRAND}
icon={<MdMyLocation size={14} />}
placeholder="Select Zone"
paperComponent={SoftPaper}
sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
/>
}
/>
{/* ============================================= || KPI Cards | ============================================= */}
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
{KPI_META(allRidersSummary).map((item) => {
const Icon = item.icon;
return (
<Grid item key={item.key} xs={12} sm={4}>
<StatCard
title={item.label}
value={item.value ?? 0}
icon={<Icon size={20} />}
color={item.color}
loading={riderSummarysLoading}
/>
</Grid>
);
})}
</Grid>
{/* ============================================= || Status Tabs + Search || ============================================= */}
<Paper
elevation={0}
sx={{
mt: { xs: 1.5, md: 2 },
p: { xs: 1, md: 1.5 },
borderTopLeftRadius: DT.radiusCard / 8,
borderTopRightRadius: DT.radiusCard / 8,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
border: '1px solid',
borderColor: DT.borderSubtle,
borderBottom: 0,
background: '#fff'
}}
>
<Stack direction="row" alignItems="center" justifyContent="space-between" gap={1.5} sx={{ flexWrap: 'wrap-reverse' }}>
<Stack
direction="row"
spacing={0.75}
sx={{
flex: 1,
overflowX: 'auto',
py: 0.5,
px: 0.25,
'&::-webkit-scrollbar': { height: 6 },
'&::-webkit-scrollbar-thumb': { backgroundColor: DT.borderSubtle, borderRadius: 4 }
}}
>
{TAB_META.map((t) => {
const Icon = t.icon;
const active = tabvalue === t.key;
const count =
t.key === 2
? subRidersLogsData?.length || 0
: t.key === 3
? subHistoryData?.length || 0
: allRidersSummary?.[t.countKey] ?? 0;
const countLoading = t.key === 2 ? subRidersLogsLoading : t.key === 3 ? subHistoryLoading : riderSummarysLoading;
return (
<Box
key={t.key}
onClick={() => handleChangetab(t.key)}
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: { xs: 0.625, md: 0.875 },
pl: 0.5,
pr: { xs: 1, md: 1.25 },
py: 0.5,
flexShrink: 0,
cursor: 'pointer',
borderRadius: '10px',
border: `1px solid ${active ? t.color : DT.borderSubtle}`,
bgcolor: active ? t.color : DT.surface,
color: active ? '#fff' : DT.textSecondary,
fontWeight: 600,
boxShadow: 'none',
transition: 'background-color 0.15s, border-color 0.15s, color 0.15s',
'&:hover': {
borderColor: active ? t.color : '#cbd5e1',
bgcolor: active ? t.color : '#f8fafc'
}
}}
>
<Avatar
sx={{
width: { xs: 22, md: 26 },
height: { xs: 22, md: 26 },
bgcolor: active ? 'rgba(255,255,255,0.22)' : soft(t.color),
color: active ? '#fff' : t.color
}}
>
<Icon size={13} />
</Avatar>
<Typography variant="caption" sx={{ fontWeight: 600, fontSize: { xs: 11.5, md: 13 }, lineHeight: 1 }}>
{t.label}
</Typography>
<Box
sx={{
minWidth: { xs: 22, md: 26 },
height: { xs: 18, md: 22 },
px: 0.625,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 999,
fontSize: { xs: 10, md: 11 },
fontWeight: 700,
bgcolor: active ? 'rgba(255,255,255,0.22)' : '#f8fafc',
color: active ? '#fff' : '#64748b',
border: 'none'
}}
>
{countLoading ? <Skeleton variant="text" width={14} height={10} /> : count}
</Box>
</Box>
);
})}
</Stack>
<Box sx={{ width: { xs: '100%', sm: 240, lg: 280 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
<DebounceSearchBar
value={searchword}
onChange={setSearchword}
onDebouncedChange={setDebouncedSearch}
placeholder="Search riders (ctrl+k)"
sx={{
m: 0,
width: '100%',
borderRadius: 999,
bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}}
/>
</Box>
</Stack>
</Paper>
{tabvalue === 3 && (
<Paper
elevation={0}
sx={{
mb: 2,
p: 2,
borderRadius: `${DT.radiusCard / 8}px`,
border: '1px solid',
borderColor: DT.borderSubtle,
background: '#fff'
}}
>
<Stack direction={{ xs: 'column', sm: 'row' }} alignItems="center" justifyContent="space-between" gap={2}>
<Stack direction="row" alignItems="center" spacing={1} sx={{ flexWrap: 'wrap' }}>
<Typography variant="body2" sx={{ fontWeight: 700, color: DT.textSecondary, mr: 1 }}>
Status:
</Typography>
<ToggleButtonGroup
value={historyStatus}
exclusive
onChange={(e, val) => val && setHistoryStatus(val)}
aria-label="status selection"
size="small"
sx={{
'& .MuiToggleButton-root': {
borderRadius: '8px',
mx: 0.5,
border: `1px solid ${DT.borderSubtle}`,
color: DT.textSecondary,
fontWeight: 600,
textTransform: 'capitalize',
px: 2,
py: 0.5,
'&.Mui-selected': {
bgcolor: BRAND,
color: '#fff',
'&:hover': {
bgcolor: '#910E1D'
}
}
}
}}
>
<ToggleButton value="all">All</ToggleButton>
<ToggleButton value="scheduled">Scheduled</ToggleButton>
</ToggleButtonGroup>
</Stack>
<Stack direction="row" spacing={1.5} alignItems="center">
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label="Select Date"
value={historyDate}
onChange={(newValue) => newValue && setHistoryDate(newValue)}
slotProps={{
textField: {
size: 'small',
sx: {
width: 180,
'& .MuiOutlinedInput-root': {
borderRadius: '20px',
'& fieldset': { borderColor: DT.borderSubtle },
'&:hover fieldset': { borderColor: BRAND },
'&.Mui-focused fieldset': { borderColor: BRAND }
}
}
}
}}
/>
</LocalizationProvider>
</Stack>
</Stack>
</Paper>
)}
{tabvalue === 3 ? (
<Paper
elevation={0}
sx={{
borderRadius: `${DT.radiusCard / 8}px`,
border: '1px solid',
borderColor: DT.borderSubtle,
overflow: 'hidden',
background: '#fff'
}}
>
{subHistoryData?.length === 0 ? (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdAccessTime size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No substitution history to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center', px: 2 }}>
Historical substitution logs will be listed here.
</Typography>
</Stack>
) : isMobile ? (
<MobileCardList sx={{ p: 1.25 }}>
{subHistoryData?.map((row, index) => (
<MobileCard
key={row.id ?? index}
accent="#f59e0b"
header={
<Stack spacing={1.25}>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{dayjs(row.sub_date).format('DD MMM YYYY')}
</Typography>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
px: 1,
py: 0.25,
borderRadius: 999,
bgcolor: tint(getHistoryStatusColor(row.status)),
border: `1px solid ${edge(getHistoryStatusColor(row.status))}`,
color: getHistoryStatusColor(row.status),
fontWeight: 800,
fontSize: 10
}}
>
{row.status || 'scheduled'}
</Box>
</Stack>
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Stack spacing={0.5} sx={{ width: '45%' }}>
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 700 }}>
Absent Rider
</Typography>
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 13 }} noWrap>
{row.absent_rider_name}
</Typography>
<Typography variant="caption" sx={{ color: DT.textMuted }}>
#{row.absent_rider_id}
</Typography>
</Stack>
<MdArrowForward size={16} style={{ color: DT.textSecondary }} />
<Stack spacing={0.5} sx={{ width: '45%', alignItems: 'flex-end', textAlign: 'right' }}>
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 700 }}>
Substitute
</Typography>
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 13 }} noWrap>
{row.sub_rider_name}
</Typography>
<Typography variant="caption" sx={{ color: DT.textMuted }}>
#{row.sub_rider_id}
</Typography>
</Stack>
</Stack>
{row.reason && (
<Box sx={{ pt: 0.5, borderTop: `1px solid ${DT.divider}` }}>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
Reason: <strong>{row.reason}</strong>
</Typography>
</Box>
)}
</Stack>
}
/>
))}
</MobileCardList>
) : (
<TableContainer
sx={{
maxHeight: 'calc(100vh - 240px)',
'&::-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: 800 }}>
<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>Date</TableCell>
<TableCell>Absent Rider</TableCell>
<TableCell align="center"></TableCell>
<TableCell>Substitute Rider</TableCell>
<TableCell>Reason</TableCell>
<TableCell align="center">Status</TableCell>
<TableCell align="right">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{subHistoryData?.map((row, index) => (
<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="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{dayjs(row.sub_date).format('DD MMM YYYY')}
</Typography>
</TableCell>
<TableCell>
<Stack direction="row" alignItems="center" spacing={1}>
<Avatar sx={{ width: 32, height: 32, bgcolor: soft(BRAND), color: BRAND, fontWeight: 700, fontSize: 14 }}>
{(row.absent_rider_name || '?').charAt(0).toUpperCase()}
</Avatar>
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.absent_rider_name}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
#{row.absent_rider_id}
</Typography>
</Stack>
</Stack>
</TableCell>
<TableCell align="center">
<MdArrowForward size={18} style={{ color: DT.textSecondary }} />
</TableCell>
<TableCell>
<Stack direction="row" alignItems="center" spacing={1}>
<Avatar sx={{ width: 32, height: 32, bgcolor: soft('#10b981'), color: '#10b981', fontWeight: 700, fontSize: 14 }}>
{(row.sub_rider_name || '?').charAt(0).toUpperCase()}
</Avatar>
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.sub_rider_name}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
#{row.sub_rider_id}
</Typography>
</Stack>
</Stack>
</TableCell>
<TableCell>
<Typography variant="body2" sx={{ color: DT.textPrimary, fontWeight: 500 }}>
{row.reason || '—'}
</Typography>
</TableCell>
<TableCell align="center">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
px: 1.5,
py: 0.5,
borderRadius: 999,
bgcolor: tint(getHistoryStatusColor(row.status)),
border: `1px solid ${edge(getHistoryStatusColor(row.status))}`,
color: getHistoryStatusColor(row.status),
fontWeight: 800,
fontSize: 11,
textTransform: 'capitalize'
}}
>
{row.status || 'scheduled'}
</Box>
</TableCell>
<TableCell align="right">
{(row.status || '').toLowerCase() !== 'cancelled' && (
<Stack direction="row" spacing={0.75} justifyContent="flex-end">
<Tooltip title="Update substitution" placement="top">
<IconButton
size="small"
sx={{
bgcolor: soft(BRAND),
color: BRAND,
border: `1px solid ${edge(BRAND)}`,
'&:hover': { bgcolor: BRAND, color: '#fff' }
}}
onClick={() => {
setEditingSub(row);
setNewSubRider(null);
}}
>
<MdEdit size={14} />
</IconButton>
</Tooltip>
<Tooltip title="Delete substitution" placement="top">
<IconButton
size="small"
sx={{
bgcolor: soft('#ef4444'),
color: '#ef4444',
border: `1px solid ${edge('#ef4444')}`,
'&:hover': { bgcolor: '#ef4444', color: '#fff' }
}}
onClick={() => {
handleDeleteSubstitution(row);
}}
>
<MdDelete size={14} />
</IconButton>
</Tooltip>
</Stack>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Paper>
) : tabvalue === 2 ? (
<RiderSubstitution
appId={appId}
rows={subRidersLogsData || []}
allRidersList={rows}
substituteRidersList={substituteRidersList}
substituteAssignments={substituteAssignments}
handleAssignSubstitute={handleAssignSubstitute}
onFinalizeSuccess={handleFinalizeSuccess}
selectedDate={selectedDate}
setSelectedDate={setSelectedDate}
DT={DT}
BRAND={BRAND}
edge={edge}
tint={tint}
soft={soft}
roleid={roleid}
/>
) : (
<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'
}}
>
<TableContainer
ref={containerRef}
onScroll={handleScroll}
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 }
}}
>
{isMobile ? (
/* ===================== MOBILE: card list ===================== */
<MobileCardList sx={{ p: 1.25 }}>
{allRidersLoading && (
<Stack alignItems="center" sx={{ py: 6 }}>
<LoaderWithImage />
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
Loading riders
</Typography>
</Stack>
)}
{filteredRows?.length === 0 && !allRidersLoading && (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdTwoWheeler size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No riders to show
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center', px: 2 }}>
{searchword ? 'Try a different keyword.' : `No ${tabvalue === 0 ? '' : 'active '}riders for this zone.`}
</Typography>
</Stack>
)}
{filteredRows?.length !== 0 &&
filteredRows?.map((row, index) => {
const statusMeta = getRowStatusMeta(row);
const StatusIcon = statusMeta.icon;
const expanded = logsRow === row.userid;
return (
<MobileCard
key={row.userid ?? index}
accent={statusMeta.color}
selected={expanded}
header={
<Stack spacing={1.25}>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ minWidth: 0 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(index + 1).padStart(2, '0')}
</Typography>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: '#ffffff',
border: `1px solid ${edge(BRAND)}`,
color: BRAND,
fontWeight: 800,
fontSize: 11
}}
>
#{row?.userid}
</Box>
<Stack
direction="row"
alignItems="center"
spacing={0.5}
sx={{
display: 'inline-flex',
pl: 0.5,
pr: 1,
py: 0.25,
borderRadius: 999,
bgcolor: tint(statusMeta.color),
border: `1px solid ${edge(statusMeta.color)}`,
color: statusMeta.color
}}
>
<AccentAvatar color={statusMeta.color} size={18}>
<StatusIcon size={11} />
</AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, fontSize: 10.5, lineHeight: 1 }}>
{statusMeta.label}
</Typography>
</Stack>
</Stack>
{roleid == 1 && (
<Stack direction="row" spacing={0.75} sx={{ flexShrink: 0 }}>
<IconButton
size="small"
sx={{
borderRadius: 999,
bgcolor: soft(BRAND),
color: BRAND,
border: `1px solid ${edge(BRAND)}`,
'&:hover': { bgcolor: BRAND, color: '#fff' }
}}
onClick={() => {
navigate('/doormile/riders/edit', { state: { riderdata: row } });
}}
>
<MdEdit size={14} />
</IconButton>
<IconButton
size="small"
sx={{
borderRadius: 999,
bgcolor: soft('#ef4444'),
color: '#ef4444',
border: `1px solid ${edge('#ef4444')}`,
'&:hover': { bgcolor: '#ef4444', color: '#fff' }
}}
onClick={() => handleBlockRider(row)}
>
<MdBlock size={14} />
</IconButton>
<IconButton
size="small"
sx={{
borderRadius: 999,
bgcolor: soft('#0ea5e9'),
color: '#0ea5e9',
border: `1px solid ${edge('#0ea5e9')}`,
'&:hover': { bgcolor: '#0ea5e9', color: '#fff' }
}}
onClick={() => setAssignVehicleRider(row)}
>
<MdDirectionsCar size={14} />
</IconButton>
{tabvalue != 0 && (
<IconButton
size="small"
sx={{
borderRadius: 999,
bgcolor: expanded ? '#0ea5e9' : soft('#0ea5e9'),
color: expanded ? '#fff' : '#0ea5e9',
border: `1px solid ${edge('#0ea5e9')}`,
'&:hover': { bgcolor: '#0ea5e9', color: '#fff' }
}}
onClick={() => {
if (row.userid == logsRow) {
setLogsRow(null);
} else {
setLogsRow(row.userid);
getRiderLogs(row.userid);
}
}}
>
{expanded ? <MdKeyboardArrowUp size={14} /> : <MdKeyboardArrowDown size={14} />}
</IconButton>
)}
</Stack>
)}
</Stack>
<Stack direction="row" alignItems="center" spacing={1}>
<Avatar
sx={{
width: 36,
height: 36,
bgcolor: soft(BRAND),
color: BRAND,
fontWeight: 800,
fontSize: 16,
border: `1px solid ${edge(BRAND)}`,
flexShrink: 0
}}
>
{(row.displayname || row.authname || '?').charAt(0).toUpperCase()}
</Avatar>
<Box sx={{ minWidth: 0 }}>
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 15 }} noWrap>
{row.displayname || row.authname || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.phone || '—'}
</Typography>
</Box>
</Stack>
</Stack>
}
>
<MobileFieldGrid>
<MobileField label="Address" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textSecondary }} noWrap>
{row.suburb || (row.address ? row.address.slice(0, 20) + '…' : '—')}
</Typography>
<Typography variant="caption" sx={{ color: DT.textMuted }} noWrap>
{row.city || ''}
</Typography>
</MobileField>
<MobileField label="Vehicle">
<Stack direction="row" alignItems="center" spacing={0.75}>
<AccentAvatar color="#0ea5e9" size={22}>
<MdTwoWheeler size={12} />
</AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.defaultvehicletype || '—'}
</Typography>
</Stack>
</MobileField>
<MobileField label="Shift" value={`#${row.shiftid ?? '—'}`} />
<MobileField label="Time">
<Stack spacing={0.5}>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
px: 0.875,
py: 0.25,
borderRadius: 999,
bgcolor: tint('#10b981'),
border: `1px solid ${edge('#10b981')}`,
color: '#10b981',
fontSize: 10.5,
fontWeight: 800
}}
>
{row.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.starttime}`).format('hh:mm A') : '—'}
</Box>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
px: 0.875,
py: 0.25,
borderRadius: 999,
bgcolor: tint('#ef4444'),
border: `1px solid ${edge('#ef4444')}`,
color: '#ef4444',
fontSize: 10.5,
fontWeight: 800
}}
>
{row.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.endtime}`).format('hh:mm A') : '—'}
</Box>
</Stack>
</MobileField>
<MobileField label="Fare" value={row.basefare ?? '—'} />
<MobileField label="Fuel" value={row.fuelcharge ?? '—'} />
</MobileFieldGrid>
{expanded && tabvalue !== 0 && (
<Box
sx={{
mt: 1.5,
p: 1.25,
borderRadius: 2,
bgcolor: '#ffffff',
border: `1px solid ${edge(BRAND)}`
}}
>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1.25 }}>
<AccentAvatar color={BRAND} size={26}>
<MdGpsFixed size={14} />
</AccentAvatar>
<Typography
sx={{
fontWeight: 800,
color: DT.textSecondary,
letterSpacing: 0.6,
textTransform: 'uppercase',
fontSize: 11
}}
>
Live telemetry {row.displayname || row.authname || `Rider #${row.userid}`}
</Typography>
</Stack>
<Grid container spacing={1.25}>
<LogChip
color="#ef4444"
icon={MdLocationOn}
label="Location"
value={riderLogsdata?.latitude ? `${riderLogsdata.latitude}, ${riderLogsdata.longitude}` : '—'}
/>
<LogChip
color="#10b981"
icon={MdBatteryStd}
label="Battery"
value={riderLogsdata?.battery ? `${riderLogsdata.battery}%` : 'N/A'}
/>
<LogChip
color={riderLogsdata?.is_charging ? '#10b981' : '#94a3b8'}
icon={MdPowerSettingsNew}
label="Charging"
value={riderLogsdata?.is_charging ? 'Charging' : 'Not Charging'}
/>
<LogChip
color="#0ea5e9"
icon={MdSpeed}
label="Speed"
value={riderLogsdata?.speed !== undefined ? `${riderLogsdata.speed} km/h` : '—'}
/>
<LogChip
color="#8b5cf6"
icon={MdGpsFixed}
label="Accuracy"
value={riderLogsdata?.accuracy !== undefined ? `${riderLogsdata.accuracy} m` : '—'}
/>
<LogChip color="#f59e0b" icon={MdAccessTime} label="Log time" value={riderLogsdata?.logdate || '—'} />
<LogChip color={BRAND} icon={MdInventory2} label="Active order" value={riderLogsdata?.orderid || 'N/A'} />
<LogChip
color={riderLogsdata?.status === 'idle' ? '#f59e0b' : '#10b981'}
icon={MdCheckCircle}
label="Status"
value={riderLogsdata?.status || 'unknown'}
/>
</Grid>
</Box>
)}
</MobileCard>
);
})}
{filteredRows?.length !== 0 && (
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
{isFetchingNextPage || hasNextPage ? (
<LoaderWithImage />
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
No more riders
</Typography>
)}
</div>
)}
</MobileCardList>
) : (
<Table stickyHeader sx={{ minWidth: { xs: 1100, md: 1300 } }}>
<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>ID</TableCell>
<TableCell>Rider</TableCell>
<TableCell>Address</TableCell>
<TableCell>Vehicle</TableCell>
<TableCell>Shift</TableCell>
<TableCell align="center">Time</TableCell>
<TableCell>Fare</TableCell>
<TableCell>Fuel</TableCell>
<TableCell align="center">Status</TableCell>
{roleid == 1 && <TableCell align="right">Action</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{allRidersLoading && <OrdersTableSkeleton col={6} />}
{filteredRows?.length === 0 && !allRidersLoading && (
<TableRow>
<TableCell colSpan={totalCols} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdTwoWheeler 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.' : `No ${tabvalue === 0 ? '' : 'active '}riders for this zone.`}
</Typography>
</Stack>
</TableCell>
</TableRow>
)}
{filteredRows?.length !== 0 &&
filteredRows?.map((row, index) => {
const statusMeta = getRowStatusMeta(row);
const StatusIcon = statusMeta.icon;
const expanded = logsRow === row.userid;
return (
<Fragment key={row.userid ?? index}>
<TableRow
sx={{
cursor: 'pointer',
transition: 'background-color 0.15s',
'& td': {
borderBottom: `1px solid ${DT.divider}`,
py: { xs: 1, md: 1.5 },
px: { xs: 1, md: 2 }
},
'&:hover': { backgroundColor: DT.surfaceAlt },
...(expanded && { backgroundColor: tint(BRAND) })
}}
>
<TableCell>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(index + 1).padStart(2, '0')}
</Typography>
</TableCell>
<TableCell>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: '#ffffff',
border: `1px solid ${edge(BRAND)}`,
color: BRAND,
fontWeight: 800,
fontSize: 11,
minWidth: 56
}}
>
#{row?.userid}
</Box>
</TableCell>
<TableCell>
<Stack direction="row" alignItems="center" spacing={1}>
<Avatar
sx={{
width: 36,
height: 36,
bgcolor: soft(BRAND),
color: BRAND,
fontWeight: 800,
fontSize: 16,
border: `1px solid ${edge(BRAND)}`
}}
>
{(row.displayname || row.authname || '?').charAt(0).toUpperCase()}
</Avatar>
<Stack sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{row.displayname || row.authname || '—'}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.phone || '—'}
</Typography>
</Stack>
</Stack>
</TableCell>
<TableCell sx={{ maxWidth: 220 }}>
<Tooltip title={row.address || ''} placement="top">
<Stack>
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 600 }} noWrap>
{row.suburb || (row.address ? row.address.slice(0, 20) + '…' : '—')}
</Typography>
<Typography variant="caption" sx={{ color: DT.textMuted }} noWrap>
{row.city || ''}
</Typography>
</Stack>
</Tooltip>
</TableCell>
<TableCell>
<Stack direction="row" alignItems="center" spacing={0.75}>
<AccentAvatar color="#0ea5e9" size={22}>
<MdTwoWheeler size={12} />
</AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.defaultvehicletype || '—'}
</Typography>
</Stack>
</TableCell>
<TableCell>
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
#{row.shiftid ?? '—'}
</Typography>
</TableCell>
<TableCell align="center">
<Stack spacing={0.5} alignItems="center">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.375,
px: 0.875,
py: 0.25,
borderRadius: 999,
bgcolor: tint('#10b981'),
border: `1px solid ${edge('#10b981')}`,
color: '#10b981',
fontSize: 10.5,
fontWeight: 800,
minWidth: 88,
justifyContent: 'center'
}}
>
{row.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.starttime}`).format('hh:mm A') : '—'}
</Box>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.375,
px: 0.875,
py: 0.25,
borderRadius: 999,
bgcolor: tint('#ef4444'),
border: `1px solid ${edge('#ef4444')}`,
color: '#ef4444',
fontSize: 10.5,
fontWeight: 800,
minWidth: 88,
justifyContent: 'center'
}}
>
{row.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.endtime}`).format('hh:mm A') : '—'}
</Box>
</Stack>
</TableCell>
<TableCell>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.basefare ?? '—'}
</Typography>
</TableCell>
<TableCell>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.fuelcharge ?? '—'}
</Typography>
</TableCell>
<TableCell align="center">
<Stack
direction="row"
alignItems="center"
spacing={0.5}
sx={{
display: 'inline-flex',
pl: 0.5,
pr: 1,
py: 0.25,
borderRadius: 999,
bgcolor: tint(statusMeta.color),
border: `1px solid ${edge(statusMeta.color)}`,
color: statusMeta.color
}}
>
<AccentAvatar color={statusMeta.color} size={20}>
<StatusIcon size={12} />
</AccentAvatar>
<Typography variant="caption" sx={{ fontWeight: 800, fontSize: 11, lineHeight: 1 }}>
{statusMeta.label}
</Typography>
</Stack>
</TableCell>
{roleid == 1 && (
<TableCell align="right">
<Stack direction="row" spacing={0.75} justifyContent="flex-end">
<Tooltip title="Edit rider" placement="top">
<IconButton
size="small"
sx={{
bgcolor: soft(BRAND),
color: BRAND,
border: `1px solid ${edge(BRAND)}`,
'&:hover': { bgcolor: BRAND, color: '#fff' }
}}
onClick={() => {
navigate('/doormile/riders/edit', { state: { riderdata: row } });
}}
>
<MdEdit size={14} />
</IconButton>
</Tooltip>
<Tooltip title="Block rider" placement="top">
<IconButton
size="small"
sx={{
bgcolor: soft('#ef4444'),
color: '#ef4444',
border: `1px solid ${edge('#ef4444')}`,
'&:hover': { bgcolor: '#ef4444', color: '#fff' }
}}
onClick={() => handleBlockRider(row)}
>
<MdBlock size={14} />
</IconButton>
</Tooltip>
<Tooltip title="Assign vehicle" placement="top">
<IconButton
size="small"
sx={{
bgcolor: soft('#0ea5e9'),
color: '#0ea5e9',
border: `1px solid ${edge('#0ea5e9')}`,
'&:hover': { bgcolor: '#0ea5e9', color: '#fff' }
}}
onClick={() => setAssignVehicleRider(row)}
>
<MdDirectionsCar size={14} />
</IconButton>
</Tooltip>
{tabvalue != 0 && (
<Tooltip title={expanded ? 'Hide logs' : 'View live logs'} placement="top">
<IconButton
size="small"
sx={{
bgcolor: expanded ? '#0ea5e9' : soft('#0ea5e9'),
color: expanded ? '#fff' : '#0ea5e9',
border: `1px solid ${edge('#0ea5e9')}`,
'&:hover': { bgcolor: '#0ea5e9', color: '#fff' }
}}
onClick={() => {
if (row.userid == logsRow) {
setLogsRow(null);
} else {
setLogsRow(row.userid);
getRiderLogs(row.userid);
}
}}
>
{expanded ? <MdKeyboardArrowUp size={14} /> : <MdKeyboardArrowDown size={14} />}
</IconButton>
</Tooltip>
)}
</Stack>
</TableCell>
)}
</TableRow>
{/* ============ Collapsible row — live rider logs ============ */}
{expanded && tabvalue !== 0 && (
<TableRow>
<TableCell colSpan={totalCols} sx={{ p: 0, borderBottom: `1px solid ${DT.divider}` }}>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<Box
sx={{
p: { xs: 1.5, md: 2 },
bgcolor: '#ffffff',
borderTop: `1px solid ${edge(BRAND)}`
}}
>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1.25 }}>
<AccentAvatar color={BRAND} size={26}>
<MdGpsFixed size={14} />
</AccentAvatar>
<Typography
sx={{
fontWeight: 800,
color: DT.textSecondary,
letterSpacing: 0.6,
textTransform: 'uppercase',
fontSize: 11
}}
>
Live telemetry {row.displayname || row.authname || `Rider #${row.userid}`}
</Typography>
</Stack>
<Grid container spacing={1.25}>
<LogChip
color="#ef4444"
icon={MdLocationOn}
label="Location"
value={riderLogsdata?.latitude ? `${riderLogsdata.latitude}, ${riderLogsdata.longitude}` : '—'}
/>
<LogChip
color="#10b981"
icon={MdBatteryStd}
label="Battery"
value={riderLogsdata?.battery ? `${riderLogsdata.battery}%` : 'N/A'}
/>
<LogChip
color={riderLogsdata?.is_charging ? '#10b981' : '#94a3b8'}
icon={MdPowerSettingsNew}
label="Charging"
value={riderLogsdata?.is_charging ? 'Charging' : 'Not Charging'}
/>
<LogChip
color="#0ea5e9"
icon={MdSpeed}
label="Speed"
value={riderLogsdata?.speed !== undefined ? `${riderLogsdata.speed} km/h` : '—'}
/>
<LogChip
color="#8b5cf6"
icon={MdGpsFixed}
label="Accuracy"
value={riderLogsdata?.accuracy !== undefined ? `${riderLogsdata.accuracy} m` : '—'}
/>
<LogChip color="#f59e0b" icon={MdAccessTime} label="Log time" value={riderLogsdata?.logdate || '—'} />
<LogChip
color={BRAND}
icon={MdInventory2}
label="Active order"
value={riderLogsdata?.orderid || 'N/A'}
/>
<LogChip
color={riderLogsdata?.status === 'idle' ? '#f59e0b' : '#10b981'}
icon={MdCheckCircle}
label="Status"
value={riderLogsdata?.status || 'unknown'}
/>
</Grid>
</Box>
</Collapse>
</TableCell>
</TableRow>
)}
</Fragment>
);
})}
{filteredRows?.length !== 0 && (
<TableRow>
<TableCell colSpan={totalCols} sx={{ borderBottom: 'none' }}>
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
{isFetchingNextPage || hasNextPage ? (
<LoaderWithImage />
) : (
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
No more riders
</Typography>
)}
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</TableContainer>
</Paper>
)}
<Dialog
open={Boolean(editingSub)}
onClose={() => setEditingSub(null)}
maxWidth="xs"
fullWidth
PaperProps={{
sx: {
borderRadius: `${DT.radiusCard / 8}px`,
p: 1
}
}}
>
<DialogTitle sx={{ fontWeight: 700, pb: 1 }}>Update Substitution</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 1 }}>
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
Choose a new substitute rider for <strong>{editingSub?.absent_rider_name}</strong>.
</Typography>
<Autocomplete
size="small"
options={substituteRidersList || []}
isOptionEqualToValue={(option, value) => option?.userid === value?.userid}
getOptionLabel={(option) => `${option.username || option.fullname || ''} (#${option.userid})`}
value={newSubRider}
onChange={(event, newValue) => setNewSubRider(newValue)}
renderInput={(params) => (
<TextField
{...params}
placeholder="Select Substitute Rider"
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: '8px',
bgcolor: '#f8fafc',
'& fieldset': { borderColor: DT.borderSubtle }
}
}}
/>
)}
/>
</Stack>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button
variant="outlined"
onClick={() => setEditingSub(null)}
sx={{
borderRadius: '8px',
textTransform: 'none',
fontWeight: 600,
borderColor: DT.borderSubtle,
color: DT.textSecondary
}}
>
Cancel
</Button>
<Button
variant="contained"
onClick={handleUpdateSubstitutionSubmit}
disabled={!newSubRider}
sx={{
borderRadius: '8px',
textTransform: 'none',
fontWeight: 600,
bgcolor: BRAND,
color: '#fff',
'&:hover': { bgcolor: '#910E1D' }
}}
>
Save Changes
</Button>
</DialogActions>
</Dialog>
<Dialog
open={!!assignVehicleRider}
onClose={() => {
setAssignVehicleRider(null);
setSelectedVehicle(null);
}}
maxWidth="xs"
fullWidth
>
<DialogTitle sx={{ fontWeight: 700 }}>
Assign Vehicle · {assignVehicleRider?.displayname || assignVehicleRider?.authname}
</DialogTitle>
<DialogContent>
<Autocomplete
sx={{ mt: 1 }}
options={vehicles || []}
getOptionLabel={(option) => `${option.vehicleno || ''}${option.vehicletype ? ` (${option.vehicletype})` : ''}`}
value={selectedVehicle}
onChange={(e, value) => setSelectedVehicle(value)}
renderInput={(params) => <TextField {...params} placeholder="Choose vehicle" autoFocus />}
/>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button
onClick={() => {
setAssignVehicleRider(null);
setSelectedVehicle(null);
}}
>
Cancel
</Button>
<Button variant="contained" disabled={!selectedVehicle} onClick={() => assignVehicleMutation.mutate()}>
Assign
</Button>
</DialogActions>
</Dialog>
</>
);
};
// Inline stat chip used in the rider-logs collapse row. Mirrors the StatChip
// pattern from the pricing page so the telemetry block reads at a glance.
const LogChip = ({ color, icon: Icon, label, value }) => (
<Grid item xs={6} sm={4} md={3}>
<Stack
direction="row"
alignItems="center"
spacing={1}
sx={{
px: 1.125,
py: 0.75,
borderRadius: 1.75,
bgcolor: '#fff',
border: `1px solid ${edge(color)}`,
minWidth: 0
}}
>
<AccentAvatar color={color} size={28}>
<Icon size={14} />
</AccentAvatar>
<Stack sx={{ minWidth: 0 }}>
<Typography
sx={{
color: DT.textMuted,
fontWeight: 700,
lineHeight: 1,
textTransform: 'uppercase',
letterSpacing: 0.4,
fontSize: 9.5
}}
>
{label}
</Typography>
<Typography
sx={{
color: DT.textPrimary,
fontWeight: 800,
fontSize: 13,
lineHeight: 1.2,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
mt: 0.25
}}
>
{value}
</Typography>
</Stack>
</Stack>
</Grid>
);
export default Riders;