1978 lines
82 KiB
JavaScript
1978 lines
82 KiB
JavaScript
import * as React from 'react';
|
|
import { useState, useEffect, useRef, Fragment } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import axios from 'axios';
|
|
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
|
|
} 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, getriderstatus } from 'pages/api/api';
|
|
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 RiderSubstitution from './RiderSubstitution';
|
|
|
|
// ============================================================================
|
|
// Design tokens — shared with the deliveries / tenants / customers pages so
|
|
// every surface (header, KPI tiles, table, badges, dialog) speaks the same
|
|
// visual language. Brand purple `#662582` is the canonical primary; status
|
|
// 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 = '#662582';
|
|
|
|
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).
|
|
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 }
|
|
};
|
|
|
|
// 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: '#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) => [
|
|
{ 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, setRiderLogsdata] = 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 getResolvedPartnerId = () => {
|
|
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 === '0' || partnerId === 0) {
|
|
partnerId = 44;
|
|
}
|
|
return partnerId;
|
|
};
|
|
|
|
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
|
|
};
|
|
|
|
const handleDeleteSubstitution = async (row) => {
|
|
if (window.confirm(`Are you sure you want to delete the substitution for ${row.absent_rider_name}?`)) {
|
|
try {
|
|
const resolvedTenantId = row.tenant_id || row.tenantid || getResolvedPartnerId();
|
|
const url = `${process.env.REACT_APP_URL}/substitutions/${row.id}?tenant_id=${resolvedTenantId}`;
|
|
const res = await axios.delete(url);
|
|
if (res.data && res.data.status) {
|
|
OpenToast('Substitution deleted successfully!', 'success', 2000);
|
|
} else {
|
|
OpenToast(res.data?.message || 'Failed to delete substitution.', 'error', 2000);
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
OpenToast(err.response?.data?.message || err.message || 'Failed to delete substitution.', 'error', 2000);
|
|
} finally {
|
|
queryClient.invalidateQueries({ queryKey: ['substitutionsHistory'] });
|
|
queryClient.invalidateQueries({ queryKey: ['activeSubstitutionsToday'] });
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleUpdateSubstitutionSubmit = async () => {
|
|
if (!newSubRider) {
|
|
OpenToast('Please select a substitute rider', 'error', 2000);
|
|
return;
|
|
}
|
|
try {
|
|
const resolvedTenantId = editingSub.tenant_id || editingSub.tenantid || getResolvedPartnerId();
|
|
const url = `${process.env.REACT_APP_URL}/substitutions/${editingSub.id}`;
|
|
const payload = {
|
|
tenant_id: parseInt(resolvedTenantId, 10),
|
|
sub_rider_id: parseInt(newSubRider.userid, 10),
|
|
sub_rider_name: newSubRider.username || newSubRider.fullname || `Rider #${newSubRider.userid}`
|
|
};
|
|
const res = await axios.put(url, payload);
|
|
if (res.data && res.data.status) {
|
|
OpenToast('Substitution updated successfully!', 'success', 2000);
|
|
} else {
|
|
OpenToast(res.data?.message || 'Failed to update substitution.', 'error', 2000);
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
OpenToast(err.response?.data?.message || err.message || 'Failed to update substitution.', 'error', 2000);
|
|
} finally {
|
|
setEditingSub(null);
|
|
setNewSubRider(null);
|
|
queryClient.invalidateQueries({ queryKey: ['substitutionsHistory'] });
|
|
queryClient.invalidateQueries({ queryKey: ['activeSubstitutionsToday'] });
|
|
}
|
|
};
|
|
|
|
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 {
|
|
// 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 === '0' || partnerId === 0) {
|
|
partnerId = 44; // final fallback
|
|
}
|
|
|
|
const url = `${process.env.REACT_APP_URL}/partners/getallriders/?applocationid=0&partnerid=${partnerId}&pagesize=1000&pageno=1&status=`;
|
|
const res = await axios.get(url);
|
|
const allRiders = res.data.details || [];
|
|
console.log('Substitute query - all riders returned from API:', allRiders.map(r => ({ userid: r.userid, username: r.username, partnerid: r.partnerid })));
|
|
const allowedIds = [1121, 772, 1116];
|
|
const filtered = allRiders.filter((rider) => allowedIds.includes(parseInt(rider.userid)));
|
|
console.log('Substitute query - filtered allowed riders:', filtered);
|
|
return filtered;
|
|
} catch (err) {
|
|
console.error(err);
|
|
return [];
|
|
}
|
|
}
|
|
});
|
|
|
|
const { data: subRidersLogsData, isLoading: subRidersLogsLoading } = useQuery({
|
|
queryKey: ['subRidersLogs', appId, debouncedSearch, selectedDate.format('YYYY-MM-DD')],
|
|
queryFn: async () => {
|
|
try {
|
|
const dateStr = selectedDate.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 === '0' || partnerId === 0) {
|
|
partnerId = 44;
|
|
}
|
|
|
|
const url = `${process.env.REACT_APP_URL2}/partners/getriderlogs/?applocationid=${appId}&partnerid=${partnerId}&fromdate=${dateStr}&todate=${dateStr}&keyword=${debouncedSearch}`;
|
|
const res = await axios.get(url);
|
|
return res.data.details || [];
|
|
} catch (err) {
|
|
console.error(err);
|
|
return [];
|
|
}
|
|
}
|
|
});
|
|
|
|
const { data: activeSubsToday, isLoading: activeSubsTodayLoading } = useQuery({
|
|
queryKey: ['activeSubstitutionsToday', appId, selectedDate.format('YYYY-MM-DD')],
|
|
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 === '0' || partnerId === 0) {
|
|
partnerId = 44;
|
|
}
|
|
|
|
const dateStr = selectedDate.format('YYYY-MM-DD');
|
|
const url = `${process.env.REACT_APP_URL}/substitutions?tenant_id=${partnerId}&from_date=${dateStr}&to_date=${dateStr}`;
|
|
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, historyDate, 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 === '0' || partnerId === 0) {
|
|
partnerId = 44;
|
|
}
|
|
|
|
const dateStr = historyDate.format('YYYY-MM-DD');
|
|
let url = `${process.env.REACT_APP_URL}/substitutions?tenant_id=${partnerId}&from_date=${dateStr}&to_date=${dateStr}`;
|
|
if (historyStatus !== 'all') {
|
|
url += `&status=${historyStatus}`;
|
|
}
|
|
const res = await axios.get(url);
|
|
return res.data.details || [];
|
|
} catch (err) {
|
|
console.error(err);
|
|
return [];
|
|
}
|
|
}
|
|
});
|
|
|
|
const handleChangetab = (i) => {
|
|
setTabvalue(i);
|
|
setLogsRow(null);
|
|
};
|
|
|
|
// ==============================|| getallridersummary||============================== //
|
|
const { data: allRidersSummary, isLoading: riderSummarysLoading } = useQuery({
|
|
queryKey: ['allriders', appId, tabvalue],
|
|
queryFn: getallridersummary
|
|
});
|
|
|
|
// ==============================|| getRiderLogs (riders)||============================== //
|
|
const getRiderLogs = async (userid) => {
|
|
try {
|
|
const res = await axios.get(`${process.env.REACT_APP_URL}/utils/getriderperiodiclogs?userid=${userid}`);
|
|
if (res.data.data.length == 0) {
|
|
setLogsRow(null);
|
|
OpenToast(res.data.message, 'error', 2000);
|
|
} else {
|
|
setRiderLogsdata(res.data.data);
|
|
}
|
|
} catch (err) {
|
|
OpenToast(err.message, 'error', 2000);
|
|
}
|
|
};
|
|
|
|
// ==============================|| getriderstatus||============================== //
|
|
const {
|
|
data: ridersStatus,
|
|
isLoading: riderStatusLoading,
|
|
isError: riderstatusIsError,
|
|
error: riderStatusError
|
|
} = useQuery({
|
|
queryKey: ['ridersStatus'],
|
|
queryFn: getriderstatus
|
|
});
|
|
|
|
// ==============================|| fetchAllRiders||============================== //
|
|
const {
|
|
data: allRidersData,
|
|
isLoading: allRidersLoading,
|
|
isFetchingNextPage,
|
|
fetchNextPage,
|
|
hasNextPage
|
|
} = useInfiniteQuery({
|
|
queryKey: ['allriders', appId, debouncedSearch, tabvalue],
|
|
queryFn: fetchAllRiders,
|
|
getNextPageParam: (lastPage, pages) => (lastPage.details?.length ? pages.length + 1 : undefined)
|
|
});
|
|
|
|
const rows = (allRidersData?.pages.flatMap((page) => page.details || []) || []).filter(Boolean);
|
|
|
|
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();
|
|
}
|
|
}
|
|
};
|
|
|
|
const errMessage = riderstatusIsError ? riderStatusError : null;
|
|
useEffect(() => {
|
|
if (errMessage) {
|
|
OpenToast(errMessage, 'error', 2000);
|
|
}
|
|
}, [errMessage]);
|
|
|
|
// 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).
|
|
const getRowStatusMeta = (row) => {
|
|
if (tabvalue == 0) {
|
|
const key = (row?.status || '').toLowerCase() === 'active' ? 'active' : 'inactive';
|
|
return STATUS_META[key] || STATUS_META.unknown;
|
|
}
|
|
const state = ridersStatus?.find((s) => s.userid === row?.userid);
|
|
const key = (state?.status || 'unknown').toLowerCase();
|
|
return STATUS_META[key] || STATUS_META.unknown;
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Backdrop
|
|
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
|
open={allRidersLoading || riderSummarysLoading || riderStatusLoading || (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: '#662582', 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: '#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="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>
|
|
)}
|
|
|
|
{rows?.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>
|
|
)}
|
|
|
|
{rows?.length !== 0 &&
|
|
rows?.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('/nearle/riders/edit', { state: { riderdata: row } });
|
|
}}
|
|
>
|
|
<MdEdit 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.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="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.vehicleno || '—'}
|
|
</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.username || `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>
|
|
);
|
|
})}
|
|
|
|
{rows?.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} />}
|
|
|
|
{rows?.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>
|
|
)}
|
|
|
|
{rows?.length !== 0 &&
|
|
rows?.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.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>
|
|
<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.vehicleno || '—'}
|
|
</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('/nearle/riders/edit', { state: { riderdata: row } });
|
|
}}
|
|
>
|
|
<MdEdit 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.username || `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>
|
|
);
|
|
})}
|
|
|
|
{rows?.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: '#4D1C61' }
|
|
}}
|
|
>
|
|
Save Changes
|
|
</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;
|