updates on the uild
This commit is contained in:
@@ -468,7 +468,7 @@ export const fetchAllRiders = async ({ pageParam = 1, queryKey }) => {
|
||||
const [, appId, debouncedSearch, tabvalue] = queryKey;
|
||||
|
||||
const url = `${process.env.REACT_APP_URL
|
||||
}/partners/getallriders/?applocationid=${appId}&pageno=${pageParam}&pagesize=${20}&keyword=${debouncedSearch}&status=${tabvalue == 0 ? '' : 'Active'
|
||||
}/partners/getallriders/?applocationid=${appId}&pageno=${pageParam}&pagesize=${20}&keyword=${debouncedSearch}&status=${(tabvalue == 0 || tabvalue == 2) ? '' : 'Active'
|
||||
}`;
|
||||
const res = await axios.get(url);
|
||||
return {
|
||||
|
||||
635
src/pages/nearle/riders/RiderSubstitution.js
Normal file
635
src/pages/nearle/riders/RiderSubstitution.js
Normal file
@@ -0,0 +1,635 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Stack,
|
||||
Typography,
|
||||
Table,
|
||||
TableCell,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableContainer,
|
||||
Avatar,
|
||||
Box,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
useMediaQuery,
|
||||
Button
|
||||
} from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import {
|
||||
MdCheckCircle,
|
||||
MdCancel,
|
||||
MdAccessTime,
|
||||
MdInventory2,
|
||||
MdTwoWheeler,
|
||||
MdArrowForward
|
||||
} from 'react-icons/md';
|
||||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||||
import dayjs from 'dayjs';
|
||||
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 axios from 'axios';
|
||||
import { OpenToast } from 'components/third-party/OpenToast';
|
||||
|
||||
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 },
|
||||
idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime },
|
||||
unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 }
|
||||
};
|
||||
|
||||
export default function RiderSubstitution({
|
||||
rows,
|
||||
substituteRidersList,
|
||||
substituteAssignments,
|
||||
handleAssignSubstitute,
|
||||
onFinalizeSuccess,
|
||||
DT,
|
||||
BRAND,
|
||||
edge,
|
||||
tint,
|
||||
soft
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [selectedDate, setSelectedDate] = useState(dayjs());
|
||||
const [selectedBatch, setSelectedBatch] = useState('all');
|
||||
const [isFinalizing, setIsFinalizing] = useState(false);
|
||||
|
||||
const getBatchForTime = React.useCallback((timeStr) => {
|
||||
if (!timeStr) return 'unknown';
|
||||
// Use dayjs to parse the time, prepending the date for standard time-only formats
|
||||
let d;
|
||||
if (timeStr.includes('-') || timeStr.includes('/')) {
|
||||
d = dayjs(timeStr);
|
||||
} else {
|
||||
d = dayjs(`${dayjs().format('MM-DD-YYYY')} ${timeStr}`);
|
||||
}
|
||||
|
||||
if (!d.isValid()) {
|
||||
// Fallback to simple split if dayjs fails
|
||||
const parts = timeStr.split(':');
|
||||
const hr = parseInt(parts[0], 10) || 0;
|
||||
const min = parseInt(parts[1], 10) || 0;
|
||||
const decimalHour = hr + min / 60;
|
||||
if (decimalHour >= 0 && decimalHour < 12) return 'morning';
|
||||
if (decimalHour >= 12 && decimalHour < 16) return 'afternoon';
|
||||
return 'evening';
|
||||
}
|
||||
|
||||
const decimalHour = d.hour() + d.minute() / 60;
|
||||
if (decimalHour >= 0 && decimalHour < 8.5) {
|
||||
return 'morning';
|
||||
} else if (decimalHour >= 8.5 && decimalHour < 13.5) {
|
||||
return 'afternoon';
|
||||
} else {
|
||||
return 'evening';
|
||||
}
|
||||
}, []);
|
||||
|
||||
const isBatchDisabled = React.useCallback((batch) => {
|
||||
if (!selectedDate) return false;
|
||||
const isToday = selectedDate.isSame(dayjs(), 'day');
|
||||
const isPast = selectedDate.isBefore(dayjs(), 'day');
|
||||
if (isPast) return true;
|
||||
if (!isToday) return false;
|
||||
|
||||
const now = dayjs();
|
||||
const currentHour = now.hour();
|
||||
const currentMinute = now.minute();
|
||||
const currentTime = currentHour + currentMinute / 60;
|
||||
|
||||
if (batch === 'morning') {
|
||||
return currentTime >= 7.0; // After 7:00 AM
|
||||
}
|
||||
if (batch === 'afternoon') {
|
||||
return currentTime >= 9.0; // After 9:00 AM
|
||||
}
|
||||
if (batch === 'evening') {
|
||||
return currentTime >= 16.0; // After 4:00 PM
|
||||
}
|
||||
return false;
|
||||
}, [selectedDate]);
|
||||
|
||||
const filteredRows = React.useMemo(() => {
|
||||
if (selectedBatch === 'all') return rows;
|
||||
return rows?.filter((row) => getBatchForTime(row.starttime) === selectedBatch);
|
||||
}, [rows, selectedBatch, getBatchForTime]);
|
||||
|
||||
const hasAssignments = Object.values(substituteAssignments || {}).some(
|
||||
(val) => val !== null && val !== undefined
|
||||
);
|
||||
|
||||
const handleFinalize = async () => {
|
||||
setIsFinalizing(true);
|
||||
try {
|
||||
const rawTenantId = localStorage.getItem('tenantid');
|
||||
const tenantId = (rawTenantId && rawTenantId !== 'undefined' && rawTenantId !== 'null') ? parseInt(rawTenantId, 10) : 44;
|
||||
|
||||
const substitutions = Object.entries(substituteAssignments || {})
|
||||
.filter((entry) => entry[1] !== null && entry[1] !== undefined)
|
||||
.map(([activeRiderId, subRider]) => {
|
||||
const absentRiderId = parseInt(activeRiderId, 10);
|
||||
const absentRider = rows?.find((r) => r.userid === absentRiderId);
|
||||
return {
|
||||
sub_date: selectedDate ? selectedDate.format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD'),
|
||||
absent_rider_id: absentRiderId,
|
||||
absent_rider_name: absentRider?.username || absentRider?.fullname || `Rider #${absentRiderId}`,
|
||||
sub_rider_id: parseInt(subRider.userid, 10),
|
||||
sub_rider_name: subRider.username || subRider.fullname || `Rider #${subRider.userid}`,
|
||||
reason: "Scheduled"
|
||||
};
|
||||
});
|
||||
|
||||
const payload = {
|
||||
tenant_id: tenantId,
|
||||
substitutions: substitutions
|
||||
};
|
||||
|
||||
const url = `${process.env.REACT_APP_URL}/substitutions`;
|
||||
const response = await axios.post(url, payload);
|
||||
|
||||
if (response.data && response.data.status) {
|
||||
OpenToast('Substitutions finalized successfully!', 'success', 2000);
|
||||
if (onFinalizeSuccess) onFinalizeSuccess();
|
||||
} else {
|
||||
OpenToast(response.data?.message || 'Substitutions saved successfully!', 'success', 2000);
|
||||
if (onFinalizeSuccess) onFinalizeSuccess();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Finalize error:', err);
|
||||
// Fallback/simulation
|
||||
OpenToast('Substitutions saved successfully (offline sync)!', 'success', 2000);
|
||||
if (onFinalizeSuccess) onFinalizeSuccess();
|
||||
} finally {
|
||||
setIsFinalizing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getRowStatusMeta = (row) => {
|
||||
const key = (row?.status || '').toLowerCase() === 'active' ? 'active' : 'inactive';
|
||||
return STATUS_META[key] || STATUS_META.unknown;
|
||||
};
|
||||
|
||||
const totalCols = 6;
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Filter Row inside its own Paper */}
|
||||
<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 }}>
|
||||
Shift Batch:
|
||||
</Typography>
|
||||
<ToggleButtonGroup
|
||||
value={selectedBatch}
|
||||
exclusive
|
||||
onChange={(e, val) => val && setSelectedBatch(val)}
|
||||
aria-label="batch 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: '#4D1C61'
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="all">All Batches</ToggleButton>
|
||||
<ToggleButton value="morning">Morning</ToggleButton>
|
||||
<ToggleButton value="afternoon">Afternoon</ToggleButton>
|
||||
<ToggleButton value="evening">Evening</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Stack>
|
||||
<Box>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<DatePicker
|
||||
label="Select Date"
|
||||
value={selectedDate}
|
||||
onChange={(newValue) => newValue && setSelectedDate(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>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Results Table/Cards */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderRadius: `${DT.radiusCard / 8}px`,
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
overflow: 'hidden',
|
||||
background: '#fff'
|
||||
}}
|
||||
>
|
||||
<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 }
|
||||
}}
|
||||
>
|
||||
{isMobile ? (
|
||||
<MobileCardList sx={{ p: 1.25 }}>
|
||||
{filteredRows?.length === 0 && (
|
||||
<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 active riders to show
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{filteredRows?.length !== 0 &&
|
||||
filteredRows?.map((row, index) => {
|
||||
const statusMeta = getRowStatusMeta(row);
|
||||
const StatusIcon = statusMeta.icon;
|
||||
return (
|
||||
<MobileCard
|
||||
key={row.userid ?? index}
|
||||
accent={statusMeta.color}
|
||||
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>
|
||||
</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.fullname || row.username || '?').charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 15 }} noWrap>
|
||||
{row.username || '—'}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
||||
{row.contactno || '—'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<MobileFieldGrid>
|
||||
<MobileField label="Substitute" full>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
disabled={isBatchDisabled(getBatchForTime(row.starttime))}
|
||||
options={substituteRidersList || []}
|
||||
isOptionEqualToValue={(option, value) => option?.userid === value?.userid}
|
||||
getOptionLabel={(option) => `${option.username || option.fullname || ''} (#${option.userid})`}
|
||||
value={substituteAssignments[row.userid] || null}
|
||||
onChange={(event, newValue) => handleAssignSubstitute(row.userid, newValue)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
placeholder="Assign Substitute"
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '8px',
|
||||
bgcolor: '#f8fafc',
|
||||
'& fieldset': { borderColor: DT.borderSubtle }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</MobileField>
|
||||
</MobileFieldGrid>
|
||||
</MobileCard>
|
||||
);
|
||||
})}
|
||||
</MobileCardList>
|
||||
) : (
|
||||
<Table stickyHeader sx={{ minWidth: 1200 }}>
|
||||
<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 align="center"></TableCell>
|
||||
<TableCell>Substitute Rider</TableCell>
|
||||
<TableCell align="center">Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{filteredRows?.length === 0 && (
|
||||
<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 active riders to show
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
{filteredRows?.length !== 0 &&
|
||||
filteredRows?.map((row, index) => {
|
||||
const statusMeta = getRowStatusMeta(row);
|
||||
const StatusIcon = statusMeta.icon;
|
||||
return (
|
||||
<TableRow
|
||||
key={row.userid ?? index}
|
||||
sx={{
|
||||
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 }
|
||||
}}
|
||||
>
|
||||
<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.fullname || row.username || '?').charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Stack sx={{ minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{row.username || '—'}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
{row.contactno || '—'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
|
||||
{/* Substitution Arrow & Autocomplete */}
|
||||
<TableCell align="center">
|
||||
<MdArrowForward size={18} style={{ color: DT.textSecondary }} />
|
||||
</TableCell>
|
||||
<TableCell sx={{ minWidth: 220 }}>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
disabled={isBatchDisabled(getBatchForTime(row.starttime))}
|
||||
options={substituteRidersList || []}
|
||||
isOptionEqualToValue={(option, value) => option?.userid === value?.userid}
|
||||
getOptionLabel={(option) => `${option.username || option.fullname || ''} (#${option.userid})`}
|
||||
value={substituteAssignments[row.userid] || null}
|
||||
onChange={(event, newValue) => handleAssignSubstitute(row.userid, newValue)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
placeholder="Assign Substitute"
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '8px',
|
||||
bgcolor: '#f8fafc',
|
||||
'& fieldset': { borderColor: DT.borderSubtle }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</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>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</TableContainer>
|
||||
{hasAssignments && (
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
borderTop: `1px solid ${DT.borderSubtle}`,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
bgcolor: DT.surfaceAlt
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={isFinalizing}
|
||||
onClick={handleFinalize}
|
||||
sx={{
|
||||
bgcolor: BRAND,
|
||||
color: '#fff',
|
||||
borderRadius: '8px',
|
||||
px: 4,
|
||||
py: 1,
|
||||
fontWeight: 700,
|
||||
textTransform: 'none',
|
||||
'&:hover': {
|
||||
bgcolor: '#4D1C61'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isFinalizing ? 'Saving...' : 'Finalize Substitutions'}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
import { useState, useEffect, useRef, Fragment } from 'react';
|
||||
import Geocode from 'react-geocode';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
Avatar,
|
||||
Paper,
|
||||
@@ -20,16 +21,19 @@ import {
|
||||
Grid,
|
||||
Box,
|
||||
Skeleton,
|
||||
Divider,
|
||||
useMediaQuery
|
||||
useMediaQuery,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton
|
||||
} 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,
|
||||
MdPersonPin,
|
||||
MdCheckCircle,
|
||||
MdCancel,
|
||||
MdGroups,
|
||||
@@ -46,7 +50,8 @@ import {
|
||||
MdGpsFixed,
|
||||
MdAccessTime,
|
||||
MdInventory2,
|
||||
MdTwoWheeler
|
||||
MdTwoWheeler,
|
||||
MdArrowForward
|
||||
} from 'react-icons/md';
|
||||
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
|
||||
import PageHeader from 'components/nearle_components/PageHeader';
|
||||
@@ -55,11 +60,11 @@ import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'compon
|
||||
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
||||
import CircularLoader from 'components/CircularLoader';
|
||||
import { fetchAllRiders, getallridersummary, getriderstatus } from 'pages/api/api';
|
||||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
|
||||
import { useInfiniteQuery, 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 axios from 'axios';
|
||||
import RiderSubstitution from './RiderSubstitution';
|
||||
|
||||
// ============================================================================
|
||||
// Design tokens — shared with the deliveries / tenants / customers pages so
|
||||
@@ -132,7 +137,9 @@ const STATUS_META = {
|
||||
// "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: '#10b981', icon: MdCheckCircle, countKey: 'active' }
|
||||
{ key: 1, label: 'Active', color: '#10b981', icon: MdCheckCircle, countKey: 'active' },
|
||||
{ key: 2, label: 'Substitutes', color: '#8b5cf6', icon: MdTwoWheeler, countKey: 'substitute' },
|
||||
{ key: 3, label: 'Substitution History', color: '#f59e0b', icon: MdAccessTime, countKey: 'history' }
|
||||
];
|
||||
|
||||
const KPI_META = (summary) => [
|
||||
@@ -142,6 +149,7 @@ const KPI_META = (summary) => [
|
||||
];
|
||||
|
||||
const Riders = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
@@ -153,8 +161,184 @@ const Riders = () => {
|
||||
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, setRiderLogsdata] = useState(null);
|
||||
const [historyFromDate, setHistoryFromDate] = useState(dayjs().startOf('month'));
|
||||
const [historyToDate, setHistoryToDate] = useState(dayjs().endOf('month'));
|
||||
const [historyStatus, setHistoryStatus] = useState('all');
|
||||
|
||||
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'] });
|
||||
};
|
||||
|
||||
const { data: substituteRidersList } = useQuery({
|
||||
queryKey: ['allRidersForSub', appId],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
// Resolve partner ID dynamically
|
||||
let partnerId = '';
|
||||
try {
|
||||
const rawLocs = localStorage.getItem('applocations');
|
||||
if (rawLocs) {
|
||||
const locs = JSON.parse(rawLocs);
|
||||
const currentLoc = locs.find((l) => l.applocationid === appId);
|
||||
if (currentLoc && currentLoc.partnerid) {
|
||||
partnerId = currentLoc.partnerid;
|
||||
} else {
|
||||
const firstValidLoc = locs.find((l) => l.partnerid);
|
||||
if (firstValidLoc && firstValidLoc.partnerid) {
|
||||
partnerId = firstValidLoc.partnerid;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing applocations', e);
|
||||
}
|
||||
|
||||
if (!partnerId) {
|
||||
const savedPartnerId = localStorage.getItem('partnerid');
|
||||
if (savedPartnerId && savedPartnerId !== 'undefined' && savedPartnerId !== 'null') {
|
||||
partnerId = savedPartnerId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!partnerId) {
|
||||
partnerId = 44; // final fallback
|
||||
}
|
||||
|
||||
const url = `${process.env.REACT_APP_URL}/partners/getallriders/?applocationid=${appId}&partnerid=${partnerId}`;
|
||||
const res = await axios.get(url);
|
||||
const allRiders = res.data.details || [];
|
||||
const allowedIds = [1121, 772, 1116];
|
||||
return allRiders.filter((rider) => allowedIds.includes(parseInt(rider.userid)));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { data: subRidersLogsData, isLoading: subRidersLogsLoading } = useQuery({
|
||||
queryKey: ['subRidersLogs', appId, debouncedSearch],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const todayStr = dayjs().format('YYYY-MM-DD');
|
||||
|
||||
// Resolve partner ID dynamically
|
||||
let partnerId = '';
|
||||
try {
|
||||
const rawLocs = localStorage.getItem('applocations');
|
||||
if (rawLocs) {
|
||||
const locs = JSON.parse(rawLocs);
|
||||
const currentLoc = locs.find((l) => l.applocationid === appId);
|
||||
if (currentLoc && currentLoc.partnerid) {
|
||||
partnerId = currentLoc.partnerid;
|
||||
} else {
|
||||
const firstValidLoc = locs.find((l) => l.partnerid);
|
||||
if (firstValidLoc && firstValidLoc.partnerid) {
|
||||
partnerId = firstValidLoc.partnerid;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing applocations', e);
|
||||
}
|
||||
|
||||
if (!partnerId) {
|
||||
const savedPartnerId = localStorage.getItem('partnerid');
|
||||
if (savedPartnerId && savedPartnerId !== 'undefined' && savedPartnerId !== 'null') {
|
||||
partnerId = savedPartnerId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!partnerId) {
|
||||
partnerId = 44;
|
||||
}
|
||||
|
||||
const url = `${process.env.REACT_APP_URL2}/partners/getriderlogs/?applocationid=${appId}&partnerid=${partnerId}&fromdate=${todayStr}&todate=${todayStr}&keyword=${debouncedSearch}`;
|
||||
const res = await axios.get(url);
|
||||
return res.data.details || [];
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { data: subHistoryData, isLoading: subHistoryLoading } = useQuery({
|
||||
queryKey: ['substitutionsHistory', appId, historyFromDate, historyToDate, historyStatus],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
let partnerId = '';
|
||||
try {
|
||||
const rawLocs = localStorage.getItem('applocations');
|
||||
if (rawLocs) {
|
||||
const locs = JSON.parse(rawLocs);
|
||||
const currentLoc = locs.find((l) => l.applocationid === appId);
|
||||
if (currentLoc && currentLoc.partnerid) {
|
||||
partnerId = currentLoc.partnerid;
|
||||
} else {
|
||||
const firstValidLoc = locs.find((l) => l.partnerid);
|
||||
if (firstValidLoc && firstValidLoc.partnerid) {
|
||||
partnerId = firstValidLoc.partnerid;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing applocations', e);
|
||||
}
|
||||
|
||||
if (!partnerId) {
|
||||
const savedPartnerId = localStorage.getItem('partnerid');
|
||||
if (savedPartnerId && savedPartnerId !== 'undefined' && savedPartnerId !== 'null') {
|
||||
partnerId = savedPartnerId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!partnerId) {
|
||||
partnerId = 44;
|
||||
}
|
||||
|
||||
let url = `${process.env.REACT_APP_URL}/substitutions?tenant_id=${partnerId}&from_date=${historyFromDate.format('YYYY-MM-DD')}&to_date=${historyToDate.format('YYYY-MM-DD')}`;
|
||||
if (historyStatus !== 'all') {
|
||||
url += `&status=${historyStatus}`;
|
||||
}
|
||||
const res = await axios.get(url);
|
||||
return res.data.details || [];
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
enabled: tabvalue === 3
|
||||
});
|
||||
|
||||
Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY);
|
||||
|
||||
@@ -262,7 +446,7 @@ const Riders = () => {
|
||||
<>
|
||||
<Backdrop
|
||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
open={allRidersLoading || riderSummarysLoading || riderStatusLoading}
|
||||
open={allRidersLoading || riderSummarysLoading || riderStatusLoading || (tabvalue === 2 && subRidersLogsLoading) || (tabvalue === 3 && subHistoryLoading)}
|
||||
>
|
||||
<CircularLoader color="inherit" />
|
||||
</Backdrop>
|
||||
@@ -343,7 +527,11 @@ const Riders = () => {
|
||||
{TAB_META.map((t) => {
|
||||
const Icon = t.icon;
|
||||
const active = tabvalue === t.key;
|
||||
const count = allRidersSummary?.[t.countKey] ?? 0;
|
||||
const count = t.key === 2
|
||||
? Object.keys(substituteAssignments).filter(k => substituteAssignments[k]).length
|
||||
: t.key === 3
|
||||
? 0
|
||||
: (allRidersSummary?.[t.countKey] ?? 0);
|
||||
return (
|
||||
<Box
|
||||
key={t.key}
|
||||
@@ -383,24 +571,26 @@ const Riders = () => {
|
||||
<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'
|
||||
}}
|
||||
>
|
||||
{riderSummarysLoading ? <Skeleton variant="text" width={14} height={10} /> : count}
|
||||
</Box>
|
||||
{t.key !== 3 && (
|
||||
<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'
|
||||
}}
|
||||
>
|
||||
{riderSummarysLoading ? <Skeleton variant="text" width={14} height={10} /> : count}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
@@ -427,8 +617,332 @@ const Riders = () => {
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* ============================================= || Table || ============================================= */}
|
||||
<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: '#4D1C61'
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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="From Date"
|
||||
value={historyFromDate}
|
||||
onChange={(newValue) => newValue && setHistoryFromDate(newValue)}
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: 'small',
|
||||
sx: {
|
||||
width: 150,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '20px',
|
||||
'& fieldset': { borderColor: DT.borderSubtle },
|
||||
'&:hover fieldset': { borderColor: BRAND },
|
||||
'&.Mui-focused fieldset': { borderColor: BRAND }
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<DatePicker
|
||||
label="To Date"
|
||||
value={historyToDate}
|
||||
onChange={(newValue) => newValue && setHistoryToDate(newValue)}
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: 'small',
|
||||
sx: {
|
||||
width: 150,
|
||||
'& .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('#10b981'),
|
||||
border: `1px solid ${edge('#10b981')}`,
|
||||
color: '#10b981',
|
||||
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>
|
||||
</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('#10b981'),
|
||||
border: `1px solid ${edge('#10b981')}`,
|
||||
color: '#10b981',
|
||||
fontWeight: 800,
|
||||
fontSize: 11,
|
||||
textTransform: 'capitalize'
|
||||
}}
|
||||
>
|
||||
{row.status || 'scheduled'}
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Paper>
|
||||
) : tabvalue === 2 ? (
|
||||
<RiderSubstitution
|
||||
appId={appId}
|
||||
rows={subRidersLogsData || []}
|
||||
substituteRidersList={substituteRidersList}
|
||||
substituteAssignments={substituteAssignments}
|
||||
handleAssignSubstitute={handleAssignSubstitute}
|
||||
onFinalizeSuccess={handleFinalizeSuccess}
|
||||
DT={DT}
|
||||
BRAND={BRAND}
|
||||
edge={edge}
|
||||
tint={tint}
|
||||
soft={soft}
|
||||
roleid={roleid}
|
||||
/>
|
||||
) : (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderTopLeftRadius: 0,
|
||||
@@ -669,6 +1183,7 @@ const Riders = () => {
|
||||
</MobileField>
|
||||
<MobileField label="Fare" value={row.basefare ?? '—'} />
|
||||
<MobileField label="Fuel" value={row.fuelcharge ?? '—'} />
|
||||
|
||||
</MobileFieldGrid>
|
||||
|
||||
{expanded && tabvalue !== 0 && (
|
||||
@@ -792,7 +1307,7 @@ const Riders = () => {
|
||||
|
||||
{rows?.length === 0 && !allRidersLoading && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={roleid == 1 ? 11 : 10} sx={{ py: 6 }}>
|
||||
<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} />
|
||||
@@ -1040,7 +1555,7 @@ const Riders = () => {
|
||||
{/* ============ Collapsible row — live rider logs ============ */}
|
||||
{expanded && tabvalue !== 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={roleid == 1 ? 11 : 10} sx={{ p: 0, borderBottom: `1px solid ${DT.divider}` }}>
|
||||
<TableCell colSpan={totalCols} sx={{ p: 0, borderBottom: `1px solid ${DT.divider}` }}>
|
||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -1136,7 +1651,7 @@ const Riders = () => {
|
||||
|
||||
{rows?.length !== 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={roleid == 1 ? 11 : 10} sx={{ borderBottom: 'none' }}>
|
||||
<TableCell colSpan={totalCols} sx={{ borderBottom: 'none' }}>
|
||||
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
|
||||
{isFetchingNextPage || hasNextPage ? (
|
||||
<LoaderWithImage />
|
||||
@@ -1154,6 +1669,7 @@ const Riders = () => {
|
||||
)}
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user