1137 lines
50 KiB
JavaScript
1137 lines
50 KiB
JavaScript
import { useState, useEffect, useRef, Fragment } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
var utc = require('dayjs/plugin/utc');
|
|
import dayjs from 'dayjs';
|
|
dayjs.extend(utc);
|
|
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import {
|
|
MdCheckCircle,
|
|
MdCancel,
|
|
MdGroups,
|
|
MdOutlineGroups,
|
|
MdOutlineCheckCircle,
|
|
MdOutlineCancel,
|
|
MdEdit,
|
|
MdKeyboardArrowDown,
|
|
MdKeyboardArrowUp,
|
|
MdLocationOn,
|
|
MdBatteryStd,
|
|
MdPowerSettingsNew,
|
|
MdSpeed,
|
|
MdGpsFixed,
|
|
MdAccessTime,
|
|
MdInventory2,
|
|
MdTwoWheeler,
|
|
MdArrowForward,
|
|
MdDelete,
|
|
MdBlock,
|
|
MdDirectionsCar
|
|
} from 'react-icons/md';
|
|
|
|
import { Grid } from '@astryxdesign/core/Grid';
|
|
import { HStack } from '@astryxdesign/core/HStack';
|
|
import { VStack } from '@astryxdesign/core/VStack';
|
|
import { Card } from '@astryxdesign/core/Card';
|
|
import { Text } from '@astryxdesign/core/Text';
|
|
import { IconButton } from '@astryxdesign/core/IconButton';
|
|
import { Tooltip } from '@astryxdesign/core/Tooltip';
|
|
import { Badge } from '@astryxdesign/core/Badge';
|
|
import { Selector } from '@astryxdesign/core/Selector';
|
|
import { DateInput } from '@astryxdesign/core/DateInput';
|
|
import { TabList, Tab } from '@astryxdesign/core/TabList';
|
|
import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core/SegmentedControl';
|
|
import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog';
|
|
import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout';
|
|
import { Button } from '@astryxdesign/core/Button';
|
|
import { EmptyState } from '@astryxdesign/core/EmptyState';
|
|
import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } from '@astryxdesign/core/Table';
|
|
|
|
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
|
|
import PageHeader from 'components/nearle_components/PageHeader';
|
|
import StatCard from 'components/nearle_components/StatCard';
|
|
import TableLoader from 'components/nearle_components/TableLoader';
|
|
import { PageShell, KpiGrid, FilterToolbar, DataCard } from 'components/nearle_components/PageLayout';
|
|
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
|
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
|
import Loader from 'components/Loader';
|
|
import { fetchAllRiders, getallridersummary } from 'pages/api/api';
|
|
import { getMilers, blockMiler, assignMilerVehicle, getVehicles, getMilerLogs, getMiler } from 'pages/api/doormileApi';
|
|
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
|
|
import { OpenToast } from 'components/third-party/OpenToast';
|
|
import RiderSubstitution from './RiderSubstitution';
|
|
import { useIsNarrowViewport } from 'hooks/useIsNarrowViewport';
|
|
import { DT, tint, edge } from 'themes/dt/tokens';
|
|
import { TableScroll, AccentAvatar } from 'themes/dt/primitives';
|
|
|
|
const BRAND = DT.brand;
|
|
|
|
// Status palette — semantic only (do NOT swap for brand purple). Used by the
|
|
// per-row status badges and the lifecycle tabs (ALL, Active).
|
|
// GET /admin/milers returns `availabilitystatus` (confirmed live: Available,
|
|
// Assigned, On_Pickup, Offline) — not the "active"/"inactive" this used to
|
|
// assume (there is no `status` field on a miler at all).
|
|
const STATUS_META = {
|
|
active: { label: 'Active', color: '#10b981', icon: MdCheckCircle, badge: 'green' },
|
|
inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel, badge: 'red' },
|
|
online: { label: 'Online', color: '#10b981', icon: MdCheckCircle, badge: 'green' },
|
|
offline: { label: 'Offline', color: '#ef4444', icon: MdCancel, badge: 'red' },
|
|
available: { label: 'Available', color: '#10b981', icon: MdCheckCircle, badge: 'green' },
|
|
assigned: { label: 'Assigned', color: '#6366f1', icon: MdTwoWheeler, badge: 'blue' },
|
|
on_pickup: { label: 'On Pickup', color: '#14b8a6', icon: MdTwoWheeler, badge: 'teal' },
|
|
// Confirmed live: PUT /admin/milers/:id/block sets availabilitystatus to
|
|
// "Blocked" — a value this map didn't have, so a blocked rider fell
|
|
// through to "unknown" instead of clearly reading as blocked.
|
|
blocked: { label: 'Blocked', color: '#7f1d1d', icon: MdBlock, badge: 'red' },
|
|
idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime, badge: 'yellow' },
|
|
unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2, badge: 'neutral' }
|
|
};
|
|
|
|
// 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', icon: MdGroups, countKey: 'total' },
|
|
{ key: 1, label: 'Active', icon: MdCheckCircle, countKey: 'active' },
|
|
{ key: 2, label: 'Substitutes', icon: MdTwoWheeler, countKey: 'substitute' },
|
|
{ key: 3, label: 'Substitution History', icon: MdAccessTime, countKey: 'history' }
|
|
];
|
|
|
|
const HISTORY_STATUS_OPTIONS = [
|
|
{ value: 'all', label: 'All' },
|
|
{ value: 'scheduled', label: 'Scheduled' }
|
|
];
|
|
|
|
const KPI_META = (summary) => [
|
|
{ key: 'total', label: 'Total Riders', color: BRAND, icon: MdOutlineGroups, value: summary?.total ?? 0 },
|
|
{ key: 'available', label: 'Available', color: '#10b981', icon: MdOutlineCheckCircle, value: summary?.available ?? 0 },
|
|
{ key: 'onDelivery', label: 'On Delivery', color: '#14b8a6', icon: MdTwoWheeler, value: summary?.onDelivery ?? 0 },
|
|
{ key: 'inactive', label: 'Inactive Riders', color: '#ef4444', icon: MdOutlineCancel, value: summary?.inactive ?? 0 }
|
|
];
|
|
|
|
// 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 }) => (
|
|
<HStack
|
|
gap={1.5}
|
|
vAlign="center"
|
|
padding={2}
|
|
style={{ borderRadius: 12, background: '#fff', border: `1px solid ${edge(color)}`, minWidth: 0 }}
|
|
>
|
|
<AccentAvatar color={color} size={28}>
|
|
<Icon size={14} />
|
|
</AccentAvatar>
|
|
<VStack gap={0} padding={0} minWidth={0}>
|
|
<Text type="label" weight="bold" color="secondary" style={{ letterSpacing: 0.4, textTransform: 'uppercase', fontSize: 9.5 }}>
|
|
{label}
|
|
</Text>
|
|
<Text weight="bold" maxLines={1}>
|
|
{value}
|
|
</Text>
|
|
</VStack>
|
|
</HStack>
|
|
);
|
|
|
|
const TelemetryPanel = ({ row, riderLogsdata }) => (
|
|
<Card padding={3} elevation="none" style={{ background: '#ffffff', border: `1px solid ${edge(BRAND)}` }}>
|
|
<HStack gap={1.5} vAlign="center" style={{ marginBottom: 10 }}>
|
|
<AccentAvatar color={BRAND} size={26}>
|
|
<MdGpsFixed size={14} />
|
|
</AccentAvatar>
|
|
<Text type="label" weight="bold" color="secondary" style={{ letterSpacing: 0.6, textTransform: 'uppercase' }}>
|
|
Live telemetry — {row.displayname || row.authname || `Rider #${row.userid}`}
|
|
</Text>
|
|
</HStack>
|
|
<Grid columns={{ minWidth: 160, repeat: 'fit' }} gap={1.5}>
|
|
<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>
|
|
</Card>
|
|
);
|
|
|
|
const Riders = () => {
|
|
const queryClient = useQueryClient();
|
|
const navigate = useNavigate();
|
|
const isMobile = useIsNarrowViewport();
|
|
const loadMoreRef = 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);
|
|
|
|
// The rider-substitution feature (/substitutions/*) has no equivalent in the
|
|
// new Doormile Express API at all — no such resource is documented. These
|
|
// handlers now no-op with an explanatory toast instead of calling jupiter.
|
|
const handleDeleteSubstitution = async (row) => {
|
|
if (window.confirm(`Are you sure you want to delete the substitution for ${row.absent_rider_name}?`)) {
|
|
OpenToast('Substitutions are not available on the Doormile Express API yet.', 'warning', 2500);
|
|
}
|
|
};
|
|
|
|
const handleUpdateSubstitutionSubmit = async () => {
|
|
if (!newSubRider) {
|
|
OpenToast('Please select a substitute rider', 'error', 2000);
|
|
return;
|
|
}
|
|
OpenToast('Substitutions are not available on the Doormile Express API yet.', 'warning', 2500);
|
|
setEditingSub(null);
|
|
setNewSubRider(null);
|
|
};
|
|
|
|
const [substituteAssignments, setSubstituteAssignments] = useState(() => {
|
|
try {
|
|
const saved = localStorage.getItem('rider_substitutions');
|
|
return saved ? JSON.parse(saved) : {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
});
|
|
|
|
const handleAssignSubstitute = (activeRiderId, substituteRider) => {
|
|
const updated = {
|
|
...substituteAssignments,
|
|
[activeRiderId]: substituteRider
|
|
};
|
|
setSubstituteAssignments(updated);
|
|
localStorage.setItem('rider_substitutions', JSON.stringify(updated));
|
|
if (substituteRider) {
|
|
OpenToast(`Assigned ${substituteRider.username || substituteRider.fullname || 'Rider'} as substitute`, 'success', 2000);
|
|
} else {
|
|
OpenToast(`Removed substitute assignment`, 'info', 2000);
|
|
}
|
|
};
|
|
|
|
const handleFinalizeSuccess = () => {
|
|
setSubstituteAssignments({});
|
|
localStorage.removeItem('rider_substitutions');
|
|
queryClient.invalidateQueries({ queryKey: ['substitutionsHistory'] });
|
|
queryClient.invalidateQueries({ queryKey: ['subRidersLogs'] });
|
|
queryClient.invalidateQueries({ queryKey: ['activeSubstitutionsToday'] });
|
|
};
|
|
|
|
const { data: substituteRidersList } = useQuery({
|
|
queryKey: ['allRidersForSub', appId],
|
|
queryFn: async () => {
|
|
try {
|
|
const allRiders = (await getMilers()) || [];
|
|
const allowedIds = [1121, 772, 1116];
|
|
return allRiders.filter((rider) => allowedIds.includes(parseInt(rider.userid ?? rider.milerid)));
|
|
} catch (err) {
|
|
console.error(err);
|
|
return [];
|
|
}
|
|
}
|
|
});
|
|
|
|
// No rider login/checkout audit-log endpoint in the new API.
|
|
const { data: subRidersLogsData, isLoading: subRidersLogsLoading } = useQuery({
|
|
queryKey: ['subRidersLogs', appId, debouncedSearch, selectedDate.format('YYYY-MM-DD')],
|
|
queryFn: async () => []
|
|
});
|
|
|
|
// The /substitutions resource has no equivalent in the new API.
|
|
const { data: subHistoryData, isLoading: subHistoryLoading } = useQuery({
|
|
queryKey: ['substitutionsHistory', appId, historyDate, historyStatus],
|
|
queryFn: async () => []
|
|
});
|
|
|
|
const handleChangetab = (i) => {
|
|
setTabvalue(i);
|
|
setLogsRow(null);
|
|
};
|
|
|
|
// ==============================|| getallridersummary||============================== //
|
|
const { data: allRidersSummary, isLoading: riderSummarysLoading } = useQuery({
|
|
queryKey: ['allriders', appId, tabvalue],
|
|
queryFn: getallridersummary
|
|
});
|
|
|
|
// ==============================|| getRiderLogs (riders)||============================== //
|
|
// GET /admin/milers/:id/logs is confirmed live (Status: Done) — id is
|
|
// milerprofileid, not userid. A log entry's own field names are still
|
|
// unconfirmed (every rider in the tenant we tested against had an empty
|
|
// log history), so this falls back to the miler's own last-known position
|
|
// (currentlatitude/currentlongitude/lastlocationupdatedat/
|
|
// availabilitystatus — all confirmed fields) when there's no ping yet,
|
|
// rather than showing nothing.
|
|
const getRiderLogs = async (milerprofileid) => {
|
|
if (!milerprofileid) return;
|
|
setRiderLogsdata(null);
|
|
try {
|
|
const logs = await getMilerLogs(milerprofileid, undefined, undefined, 1);
|
|
const latest = Array.isArray(logs) ? logs[0] : null;
|
|
if (latest) {
|
|
setRiderLogsdata(latest);
|
|
} else {
|
|
const miler = await getMiler(milerprofileid);
|
|
setRiderLogsdata(
|
|
miler
|
|
? {
|
|
latitude: miler.currentlatitude,
|
|
longitude: miler.currentlongitude,
|
|
logdate: miler.lastlocationupdatedat || miler.lastpingat,
|
|
status: miler.availabilitystatus
|
|
}
|
|
: null
|
|
);
|
|
}
|
|
} catch (err) {
|
|
OpenToast('Could not load rider logs', 'error', 2000);
|
|
}
|
|
};
|
|
|
|
// ==============================|| fetchAllRiders||============================== //
|
|
// GET /admin/milers isn't paginated (fetchAllRiders always returns the full
|
|
// list in one shot) — getNextPageParam must not request a "next page" or it
|
|
// re-fetches and appends the identical full list again, duplicating every
|
|
// rider in `rows`.
|
|
const {
|
|
data: allRidersData,
|
|
isLoading: allRidersLoading,
|
|
isFetchingNextPage,
|
|
fetchNextPage,
|
|
hasNextPage
|
|
} = useInfiniteQuery({
|
|
queryKey: ['allriders', appId, debouncedSearch, tabvalue],
|
|
queryFn: fetchAllRiders,
|
|
getNextPageParam: () => undefined
|
|
});
|
|
|
|
const rows = (allRidersData?.pages.flatMap((page) => page.details || []) || []).filter(Boolean);
|
|
|
|
// GET /admin/milers accepts no search/status params — debouncedSearch and
|
|
// tabvalue only trigger a refetch (via the queryKey) that returns the exact
|
|
// same full list every time. Search and the "Active" tab were both silently
|
|
// no-ops until filtered client-side here.
|
|
const filteredRows = rows.filter((row) => {
|
|
// "Active" here means currently online/working (Available/Assigned/
|
|
// On_Pickup) — Offline and Blocked (confirmed live enum) both mean not
|
|
// working.
|
|
const status = String(row.availabilitystatus || '').toLowerCase();
|
|
if (tabvalue === 1 && (status === 'offline' || status === 'blocked')) return false;
|
|
if (!debouncedSearch) return true;
|
|
const q = debouncedSearch.toLowerCase();
|
|
return [row.displayname, row.phone].some((v) =>
|
|
String(v || '')
|
|
.toLowerCase()
|
|
.includes(q)
|
|
);
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!hasNextPage) return;
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
if (entries[0].isIntersecting) {
|
|
fetchNextPage();
|
|
}
|
|
},
|
|
{
|
|
root: document.querySelector('.dt-table-scroll'),
|
|
rootMargin: '0px',
|
|
threshold: 1.0
|
|
}
|
|
);
|
|
if (loadMoreRef.current) observer.observe(loadMoreRef.current);
|
|
return () => {
|
|
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current);
|
|
};
|
|
}, [hasNextPage, fetchNextPage]);
|
|
|
|
// Per-row status meta — falls back to "unknown" if the rider's state key
|
|
// isn't in the palette (e.g. brand-new status string from the backend).
|
|
// Previously joined against a separate rider-status endpoint on every tab
|
|
// except "All Riders" — that endpoint has no equivalent in the new API and
|
|
// always returned null, so every rider on every other tab showed "Unknown"
|
|
// regardless of their real state. Now reads the miler's own
|
|
// `availabilitystatus` field directly on every tab (confirmed live — there
|
|
// is no `status` field on a miler at all).
|
|
const getRowStatusMeta = (row) => {
|
|
const key = (row?.availabilitystatus || '').toLowerCase();
|
|
return STATUS_META[key] || STATUS_META.unknown;
|
|
};
|
|
|
|
// PUT /admin/milers/:id/block — body shape isn't documented (Status: Built,
|
|
// never exercised against a real request), so no fields are sent beyond the
|
|
// id in the URL. Blocks whichever rider is passed after a confirm prompt.
|
|
const blockRiderMutation = useMutation({
|
|
// /admin/milers/:id routes key off milerprofileid, not userid (confirmed
|
|
// live — GET /admin/milers/:userid 404s; the miler's own userid field
|
|
// points at the underlying app-user account, a different resource).
|
|
mutationFn: (row) => blockMiler(row.milerprofileid),
|
|
onSuccess: (res) => {
|
|
if (res.success) {
|
|
OpenToast('Rider blocked', 'success', 2000);
|
|
queryClient.invalidateQueries({ queryKey: ['allriders'] });
|
|
} else {
|
|
OpenToast(res.message || 'Failed to block rider', 'error', 2000);
|
|
}
|
|
},
|
|
onError: (err) => {
|
|
OpenToast(err.response?.data?.message || err.message || 'Failed to block rider', 'error', 2000);
|
|
}
|
|
});
|
|
|
|
const handleBlockRider = (row) => {
|
|
if (window.confirm(`Block ${row.displayname || row.authname || 'this rider'}? They will not be able to log in to the miler app.`)) {
|
|
blockRiderMutation.mutate(row);
|
|
}
|
|
};
|
|
|
|
// PUT /admin/milers/:id/assign-vehicle — body shape isn't documented
|
|
// (Status: Built, never exercised), so this assumes `vehicleid` following
|
|
// the codebase's own <resource>id convention (GET /admin/vehicles/:id).
|
|
const [assignVehicleRider, setAssignVehicleRider] = useState(null);
|
|
const [selectedVehicle, setSelectedVehicle] = useState(null);
|
|
|
|
const { data: vehicles = [] } = useQuery({
|
|
queryKey: ['riders-vehicles'],
|
|
queryFn: getVehicles,
|
|
enabled: !!assignVehicleRider
|
|
});
|
|
|
|
const assignVehicleMutation = useMutation({
|
|
mutationFn: () => assignMilerVehicle(assignVehicleRider.milerprofileid, { vehicleid: selectedVehicle?.vehicleid }),
|
|
onSuccess: (res) => {
|
|
if (res.success) {
|
|
OpenToast('Vehicle assigned', 'success', 2000);
|
|
queryClient.invalidateQueries({ queryKey: ['allriders'] });
|
|
setAssignVehicleRider(null);
|
|
setSelectedVehicle(null);
|
|
} else {
|
|
OpenToast(res.message || 'Failed to assign vehicle', 'error', 2000);
|
|
}
|
|
},
|
|
onError: (err) => {
|
|
OpenToast(err.response?.data?.message || err.message || 'Failed to assign vehicle', 'error', 2000);
|
|
}
|
|
});
|
|
|
|
const substituteOptions = (substituteRidersList || []).map((r) => ({
|
|
value: String(r.userid),
|
|
label: `${r.username || r.fullname || ''} (#${r.userid})`
|
|
}));
|
|
const vehicleOptions = (vehicles || []).map((v) => ({
|
|
value: String(v.vehicleid),
|
|
label: `${v.vehicleno || ''}${v.vehicletype ? ` (${v.vehicletype})` : ''}`
|
|
}));
|
|
|
|
const RowActions = ({ row, expanded }) => (
|
|
<HStack gap={0.75} justify="end">
|
|
<IconButton
|
|
label="Edit rider"
|
|
tooltip="Edit rider"
|
|
size="sm"
|
|
variant="ghost"
|
|
icon={<MdEdit size={14} />}
|
|
onClick={() => navigate('/doormile/riders/edit', { state: { riderdata: row } })}
|
|
/>
|
|
<IconButton
|
|
label="Block rider"
|
|
tooltip="Block rider"
|
|
size="sm"
|
|
variant="destructive"
|
|
icon={<MdBlock size={14} />}
|
|
onClick={() => handleBlockRider(row)}
|
|
/>
|
|
<IconButton
|
|
label="Assign vehicle"
|
|
tooltip="Assign vehicle"
|
|
size="sm"
|
|
variant="ghost"
|
|
icon={<MdDirectionsCar size={14} />}
|
|
onClick={() => setAssignVehicleRider(row)}
|
|
/>
|
|
{tabvalue != 0 && (
|
|
<IconButton
|
|
label={expanded ? 'Hide logs' : 'View live logs'}
|
|
tooltip={expanded ? 'Hide logs' : 'View live logs'}
|
|
size="sm"
|
|
variant={expanded ? 'primary' : 'ghost'}
|
|
icon={expanded ? <MdKeyboardArrowUp size={14} /> : <MdKeyboardArrowDown size={14} />}
|
|
onClick={() => {
|
|
if (row.milerprofileid == logsRow) {
|
|
setLogsRow(null);
|
|
} else {
|
|
setLogsRow(row.milerprofileid);
|
|
getRiderLogs(row.milerprofileid);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</HStack>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{(allRidersLoading || riderSummarysLoading || (tabvalue === 2 && subRidersLogsLoading) || (tabvalue === 3 && subHistoryLoading)) && (
|
|
<Loader />
|
|
)}
|
|
|
|
<PageShell>
|
|
<PageHeader
|
|
title="Riders"
|
|
subtitle={`Live · ${locaName || 'All Zones'}`}
|
|
live
|
|
action={<LocationAutocomplete locaName={locaName} setAppId={setAppId} setLocoName={setLocoName} placeholder="Select Zone" />}
|
|
/>
|
|
|
|
<KpiGrid>
|
|
{KPI_META(allRidersSummary).map((item) => {
|
|
const Icon = item.icon;
|
|
return (
|
|
<StatCard
|
|
key={item.key}
|
|
title={item.label}
|
|
value={item.value ?? 0}
|
|
icon={<Icon size={20} />}
|
|
color={item.color}
|
|
loading={riderSummarysLoading}
|
|
/>
|
|
);
|
|
})}
|
|
</KpiGrid>
|
|
|
|
<DataCard
|
|
toolbar={
|
|
<FilterToolbar max={tabvalue === 3 ? 3 : 1}>
|
|
<HStack justify="end">
|
|
<VStack maxWidth={260} width="100%" padding={0} gap={0}>
|
|
<DebounceSearchBar
|
|
value={searchword}
|
|
onChange={setSearchword}
|
|
onDebouncedChange={setDebouncedSearch}
|
|
placeholder="Search riders (ctrl+k)"
|
|
/>
|
|
</VStack>
|
|
</HStack>
|
|
{tabvalue === 3 && (
|
|
<HStack gap={2} vAlign="center">
|
|
<Text weight="bold" color="secondary">
|
|
Status:
|
|
</Text>
|
|
<SegmentedControl label="History status" value={historyStatus} onChange={setHistoryStatus} size="sm">
|
|
{HISTORY_STATUS_OPTIONS.map((o) => (
|
|
<SegmentedControlItem key={o.value} value={o.value} label={o.label} />
|
|
))}
|
|
</SegmentedControl>
|
|
</HStack>
|
|
)}
|
|
{tabvalue === 3 && (
|
|
<DateInput
|
|
label="Select Date"
|
|
isLabelHidden
|
|
size="sm"
|
|
value={historyDate ? dayjs(historyDate).format('YYYY-MM-DD') : undefined}
|
|
onChange={(v) => v && setHistoryDate(dayjs(v))}
|
|
/>
|
|
)}
|
|
</FilterToolbar>
|
|
}
|
|
tabs={
|
|
<TabList value={String(tabvalue)} onChange={(v) => handleChangetab(Number(v))} size="sm">
|
|
{TAB_META.map((t) => {
|
|
const Icon = t.icon;
|
|
const count =
|
|
t.key === 2
|
|
? subRidersLogsData?.length || 0
|
|
: t.key === 3
|
|
? subHistoryData?.length || 0
|
|
: allRidersSummary?.[t.countKey] ?? 0;
|
|
return (
|
|
<Tab
|
|
key={t.key}
|
|
value={String(t.key)}
|
|
label={t.label}
|
|
icon={<Icon size={15} />}
|
|
endContent={<Badge variant="neutral" label={count} />}
|
|
/>
|
|
);
|
|
})}
|
|
</TabList>
|
|
}
|
|
>
|
|
{tabvalue === 3 ? (
|
|
<>
|
|
{subHistoryData?.length === 0 ? (
|
|
<EmptyState
|
|
title="No substitution history to show"
|
|
description="Historical substitution logs will be listed here."
|
|
icon={<MdAccessTime size={28} />}
|
|
/>
|
|
) : isMobile ? (
|
|
<MobileCardList scroll>
|
|
{subHistoryData?.map((row, index) => (
|
|
<MobileCard
|
|
key={row.id ?? index}
|
|
accent="#f59e0b"
|
|
header={
|
|
<VStack gap={1.5} padding={0}>
|
|
<HStack justify="between" vAlign="center">
|
|
<Text type="supporting" weight="bold" color="secondary">
|
|
{dayjs(row.sub_date).format('DD MMM YYYY')}
|
|
</Text>
|
|
<Badge variant="neutral" label={row.status || 'scheduled'} />
|
|
</HStack>
|
|
<HStack justify="between">
|
|
<VStack gap={0.5} padding={0} style={{ width: '45%' }}>
|
|
<Text type="supporting" weight="bold" color="secondary">
|
|
Absent Rider
|
|
</Text>
|
|
<Text weight="bold" maxLines={1}>
|
|
{row.absent_rider_name}
|
|
</Text>
|
|
<Text type="supporting" color="secondary">
|
|
#{row.absent_rider_id}
|
|
</Text>
|
|
</VStack>
|
|
<MdArrowForward size={16} color={DT.textSecondary} />
|
|
<VStack gap={0.5} padding={0} style={{ width: '45%', alignItems: 'flex-end', textAlign: 'right' }}>
|
|
<Text type="supporting" weight="bold" color="secondary">
|
|
Substitute
|
|
</Text>
|
|
<Text weight="bold" maxLines={1}>
|
|
{row.sub_rider_name}
|
|
</Text>
|
|
<Text type="supporting" color="secondary">
|
|
#{row.sub_rider_id}
|
|
</Text>
|
|
</VStack>
|
|
</HStack>
|
|
{row.reason && (
|
|
<Text type="supporting" color="secondary" style={{ paddingTop: 4, borderTop: `1px solid ${DT.divider}` }}>
|
|
Reason: <strong>{row.reason}</strong>
|
|
</Text>
|
|
)}
|
|
</VStack>
|
|
}
|
|
/>
|
|
))}
|
|
</MobileCardList>
|
|
) : (
|
|
<TableScroll minWidth={800}>
|
|
<Table density="balanced" dividers="rows" hasHover>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHeaderCell>Date</TableHeaderCell>
|
|
<TableHeaderCell>Absent Rider</TableHeaderCell>
|
|
<TableHeaderCell style={{ textAlign: 'center' }}></TableHeaderCell>
|
|
<TableHeaderCell>Substitute Rider</TableHeaderCell>
|
|
<TableHeaderCell>Reason</TableHeaderCell>
|
|
<TableHeaderCell style={{ textAlign: 'center' }}>Status</TableHeaderCell>
|
|
<TableHeaderCell>Action</TableHeaderCell>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{subHistoryData?.map((row, index) => (
|
|
<TableRow key={row.id ?? index}>
|
|
<TableCell>
|
|
<Text weight="semibold">{dayjs(row.sub_date).format('DD MMM YYYY')}</Text>
|
|
</TableCell>
|
|
<TableCell>
|
|
<HStack gap={1.5} vAlign="center">
|
|
<AccentAvatar color={BRAND} selected size={32}>
|
|
<Text weight="bold">{(row.absent_rider_name || '?').charAt(0).toUpperCase()}</Text>
|
|
</AccentAvatar>
|
|
<VStack gap={0} padding={0}>
|
|
<Text weight="semibold">{row.absent_rider_name}</Text>
|
|
<Text type="supporting" color="secondary">
|
|
#{row.absent_rider_id}
|
|
</Text>
|
|
</VStack>
|
|
</HStack>
|
|
</TableCell>
|
|
<TableCell style={{ textAlign: 'center' }}>
|
|
<MdArrowForward size={18} color={DT.textSecondary} />
|
|
</TableCell>
|
|
<TableCell>
|
|
<HStack gap={1.5} vAlign="center">
|
|
<AccentAvatar color="#10b981" selected size={32}>
|
|
<Text weight="bold">{(row.sub_rider_name || '?').charAt(0).toUpperCase()}</Text>
|
|
</AccentAvatar>
|
|
<VStack gap={0} padding={0}>
|
|
<Text weight="semibold">{row.sub_rider_name}</Text>
|
|
<Text type="supporting" color="secondary">
|
|
#{row.sub_rider_id}
|
|
</Text>
|
|
</VStack>
|
|
</HStack>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Text>{row.reason || '—'}</Text>
|
|
</TableCell>
|
|
<TableCell style={{ textAlign: 'center' }}>
|
|
<Badge variant="neutral" label={row.status || 'scheduled'} />
|
|
</TableCell>
|
|
<TableCell>
|
|
{(row.status || '').toLowerCase() !== 'cancelled' && (
|
|
<HStack gap={0.75} justify="end">
|
|
<IconButton
|
|
label="Update substitution"
|
|
tooltip="Update substitution"
|
|
size="sm"
|
|
variant="ghost"
|
|
icon={<MdEdit size={14} />}
|
|
onClick={() => {
|
|
setEditingSub(row);
|
|
setNewSubRider(null);
|
|
}}
|
|
/>
|
|
<IconButton
|
|
label="Delete substitution"
|
|
tooltip="Delete substitution"
|
|
size="sm"
|
|
variant="destructive"
|
|
icon={<MdDelete size={14} />}
|
|
onClick={() => handleDeleteSubstitution(row)}
|
|
/>
|
|
</HStack>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</TableScroll>
|
|
)}
|
|
</>
|
|
) : tabvalue === 2 ? (
|
|
<RiderSubstitution
|
|
bare
|
|
appId={appId}
|
|
rows={subRidersLogsData || []}
|
|
allRidersList={rows}
|
|
substituteRidersList={substituteRidersList}
|
|
substituteAssignments={substituteAssignments}
|
|
handleAssignSubstitute={handleAssignSubstitute}
|
|
onFinalizeSuccess={handleFinalizeSuccess}
|
|
selectedDate={selectedDate}
|
|
setSelectedDate={setSelectedDate}
|
|
roleid={roleid}
|
|
/>
|
|
) : (
|
|
<>
|
|
{isMobile ? (
|
|
<MobileCardList scroll>
|
|
{allRidersLoading && (
|
|
<VStack vAlign="center" padding={4}>
|
|
<LoaderWithImage />
|
|
<Text type="supporting" color="secondary" style={{ marginTop: 8 }}>
|
|
Loading riders…
|
|
</Text>
|
|
</VStack>
|
|
)}
|
|
|
|
{filteredRows?.length === 0 && !allRidersLoading && (
|
|
<EmptyState
|
|
title="No riders to show"
|
|
description={searchword ? 'Try a different keyword.' : `No ${tabvalue === 0 ? '' : 'active '}riders for this zone.`}
|
|
icon={<MdTwoWheeler size={28} />}
|
|
/>
|
|
)}
|
|
|
|
{filteredRows?.length !== 0 &&
|
|
filteredRows?.map((row, index) => {
|
|
const statusMeta = getRowStatusMeta(row);
|
|
const expanded = logsRow === row.milerprofileid;
|
|
return (
|
|
<MobileCard
|
|
key={row.userid ?? index}
|
|
accent={statusMeta.color}
|
|
selected={expanded}
|
|
header={
|
|
<VStack gap={1.5} padding={0}>
|
|
<HStack justify="between" vAlign="center" gap={1}>
|
|
<HStack gap={1.5} vAlign="center" minWidth={0}>
|
|
<Text type="supporting" weight="bold" color="secondary">
|
|
{String(index + 1).padStart(2, '0')}
|
|
</Text>
|
|
<div
|
|
style={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
padding: '3px 8px',
|
|
borderRadius: 999,
|
|
background: '#ffffff',
|
|
border: `1px solid ${edge(BRAND)}`,
|
|
color: BRAND,
|
|
fontWeight: 800,
|
|
fontSize: 11
|
|
}}
|
|
>
|
|
#{row?.userid}
|
|
</div>
|
|
<Badge variant={statusMeta.badge} icon={<statusMeta.icon size={11} />} label={statusMeta.label} />
|
|
</HStack>
|
|
{roleid == 1 && <RowActions row={row} expanded={expanded} />}
|
|
</HStack>
|
|
<VStack gap={0} padding={0} minWidth={0}>
|
|
<Text weight="bold" maxLines={1}>
|
|
{row.displayname || row.authname || '—'}
|
|
</Text>
|
|
<Text type="supporting" color="secondary" maxLines={1}>
|
|
{row.phone || '—'}
|
|
</Text>
|
|
</VStack>
|
|
</VStack>
|
|
}
|
|
>
|
|
<MobileFieldGrid>
|
|
<MobileField label="Address" full>
|
|
<Text weight="semibold" maxLines={1}>
|
|
{row.suburb || (row.address ? row.address.slice(0, 20) + '…' : '—')}
|
|
</Text>
|
|
<Text type="supporting" color="secondary" maxLines={1}>
|
|
{row.city || ''}
|
|
</Text>
|
|
</MobileField>
|
|
<MobileField label="Vehicle">
|
|
<HStack gap={1} vAlign="center">
|
|
<AccentAvatar color="#0ea5e9" size={22}>
|
|
<MdTwoWheeler size={12} />
|
|
</AccentAvatar>
|
|
<Text type="supporting" weight="bold">
|
|
{row.defaultvehicletype || '—'}
|
|
</Text>
|
|
</HStack>
|
|
</MobileField>
|
|
<MobileField label="Shift" value={`#${row.shiftid ?? '—'}`} />
|
|
<MobileField label="Time">
|
|
<VStack gap={0.5} padding={0}>
|
|
<Badge
|
|
variant="green"
|
|
label={row.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.starttime}`).format('hh:mm A') : '—'}
|
|
/>
|
|
<Badge
|
|
variant="red"
|
|
label={row.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.endtime}`).format('hh:mm A') : '—'}
|
|
/>
|
|
</VStack>
|
|
</MobileField>
|
|
<MobileField label="Fare" value={row.basefare ?? '—'} />
|
|
<MobileField label="Fuel" value={row.fuelcharge ?? '—'} />
|
|
</MobileFieldGrid>
|
|
|
|
{expanded && tabvalue !== 0 && (
|
|
<div style={{ marginTop: 'var(--spacing-6)' }}>
|
|
<TelemetryPanel row={row} riderLogsdata={riderLogsdata} />
|
|
</div>
|
|
)}
|
|
</MobileCard>
|
|
);
|
|
})}
|
|
|
|
{filteredRows?.length !== 0 && (
|
|
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
|
|
{isFetchingNextPage || hasNextPage ? (
|
|
<LoaderWithImage />
|
|
) : (
|
|
<Text type="supporting" weight="bold" color="secondary">
|
|
No more riders
|
|
</Text>
|
|
)}
|
|
</div>
|
|
)}
|
|
</MobileCardList>
|
|
) : (
|
|
<TableScroll minWidth={1300}>
|
|
<Table density="balanced" dividers="rows" hasHover>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHeaderCell style={{ minWidth: 40, width: 40, maxWidth: 40 }}>#</TableHeaderCell>
|
|
<TableHeaderCell>ID</TableHeaderCell>
|
|
<TableHeaderCell>Rider</TableHeaderCell>
|
|
<TableHeaderCell>Address</TableHeaderCell>
|
|
<TableHeaderCell>Vehicle</TableHeaderCell>
|
|
<TableHeaderCell>Shift</TableHeaderCell>
|
|
<TableHeaderCell style={{ textAlign: 'center' }}>Time</TableHeaderCell>
|
|
<TableHeaderCell>Fare</TableHeaderCell>
|
|
<TableHeaderCell>Fuel</TableHeaderCell>
|
|
<TableHeaderCell style={{ textAlign: 'center' }}>Status</TableHeaderCell>
|
|
{roleid == 1 && <TableHeaderCell>Action</TableHeaderCell>}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{allRidersLoading && <TableLoader rows={6} columns={totalCols} />}
|
|
|
|
{filteredRows?.length === 0 && !allRidersLoading && (
|
|
<TableRow>
|
|
<TableCell colSpan={totalCols}>
|
|
<EmptyState
|
|
title="No riders to show"
|
|
description={
|
|
searchword ? 'Try a different keyword.' : `No ${tabvalue === 0 ? '' : 'active '}riders for this zone.`
|
|
}
|
|
icon={<MdTwoWheeler size={28} />}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
|
|
{filteredRows?.length !== 0 &&
|
|
filteredRows?.map((row, index) => {
|
|
const statusMeta = getRowStatusMeta(row);
|
|
const expanded = logsRow === row.milerprofileid;
|
|
return (
|
|
<Fragment key={row.userid ?? index}>
|
|
<TableRow style={{ backgroundColor: expanded ? tint(BRAND) : undefined }}>
|
|
<TableCell>
|
|
<Text type="supporting" weight="bold" color="secondary">
|
|
{String(index + 1).padStart(2, '0')}
|
|
</Text>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div
|
|
style={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
padding: '3px 8px',
|
|
borderRadius: 999,
|
|
background: '#ffffff',
|
|
border: `1px solid ${edge(BRAND)}`,
|
|
color: BRAND,
|
|
fontWeight: 800,
|
|
fontSize: 11,
|
|
minWidth: 56
|
|
}}
|
|
>
|
|
#{row?.userid}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<VStack gap={0} padding={0} minWidth={0}>
|
|
<Text weight="semibold" style={{ whiteSpace: 'nowrap' }}>
|
|
{row.displayname || row.authname || '—'}
|
|
</Text>
|
|
<Text type="supporting" color="secondary">
|
|
{row.phone || '—'}
|
|
</Text>
|
|
</VStack>
|
|
</TableCell>
|
|
<TableCell style={{ maxWidth: 220 }}>
|
|
<Tooltip content={row.address || ''}>
|
|
<VStack gap={0} padding={0}>
|
|
<Text type="supporting" weight="bold" color="secondary" maxLines={1}>
|
|
{row.suburb || (row.address ? row.address.slice(0, 20) + '…' : '—')}
|
|
</Text>
|
|
<Text type="supporting" color="secondary" maxLines={1}>
|
|
{row.city || ''}
|
|
</Text>
|
|
</VStack>
|
|
</Tooltip>
|
|
</TableCell>
|
|
<TableCell>
|
|
<HStack gap={1} vAlign="center">
|
|
<AccentAvatar color="#0ea5e9" size={22}>
|
|
<MdTwoWheeler size={12} />
|
|
</AccentAvatar>
|
|
<Text type="supporting" weight="bold">
|
|
{row.defaultvehicletype || '—'}
|
|
</Text>
|
|
</HStack>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Text type="supporting" weight="bold" color="secondary">
|
|
#{row.shiftid ?? '—'}
|
|
</Text>
|
|
</TableCell>
|
|
<TableCell style={{ textAlign: 'center' }}>
|
|
<VStack gap={0.5} padding={0} vAlign="center">
|
|
<Badge
|
|
variant="green"
|
|
label={
|
|
row.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.starttime}`).format('hh:mm A') : '—'
|
|
}
|
|
/>
|
|
<Badge
|
|
variant="red"
|
|
label={row.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.endtime}`).format('hh:mm A') : '—'}
|
|
/>
|
|
</VStack>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Text type="supporting" weight="bold">
|
|
{row.basefare ?? '—'}
|
|
</Text>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Text type="supporting" weight="bold">
|
|
{row.fuelcharge ?? '—'}
|
|
</Text>
|
|
</TableCell>
|
|
<TableCell style={{ textAlign: 'center' }}>
|
|
<Badge variant={statusMeta.badge} icon={<statusMeta.icon size={12} />} label={statusMeta.label} />
|
|
</TableCell>
|
|
{roleid == 1 && (
|
|
<TableCell>
|
|
<RowActions row={row} expanded={expanded} />
|
|
</TableCell>
|
|
)}
|
|
</TableRow>
|
|
|
|
{expanded && tabvalue !== 0 && (
|
|
<TableRow>
|
|
<TableCell colSpan={totalCols} style={{ padding: 12 }}>
|
|
<TelemetryPanel row={row} riderLogsdata={riderLogsdata} />
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</Fragment>
|
|
);
|
|
})}
|
|
|
|
{filteredRows?.length !== 0 && (
|
|
<TableRow>
|
|
<TableCell colSpan={totalCols}>
|
|
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
|
|
{isFetchingNextPage || hasNextPage ? (
|
|
<LoaderWithImage />
|
|
) : (
|
|
<Text type="supporting" weight="bold" color="secondary">
|
|
No more riders
|
|
</Text>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</TableScroll>
|
|
)}
|
|
</>
|
|
)}
|
|
</DataCard>
|
|
</PageShell>
|
|
|
|
<Dialog isOpen={Boolean(editingSub)} onOpenChange={(next) => !next && setEditingSub(null)} width={420} purpose="form">
|
|
<Layout
|
|
header={<DialogHeader title="Update Substitution" onOpenChange={(next) => !next && setEditingSub(null)} />}
|
|
content={
|
|
<LayoutContent padding={4}>
|
|
<VStack gap={3} padding={0}>
|
|
<Text color="secondary">
|
|
Choose a new substitute rider for <strong>{editingSub?.absent_rider_name}</strong>.
|
|
</Text>
|
|
<Selector
|
|
label="Substitute Rider"
|
|
isLabelHidden
|
|
options={substituteOptions}
|
|
value={newSubRider ? String(newSubRider.userid) : ''}
|
|
onChange={(v) => setNewSubRider((substituteRidersList || []).find((r) => String(r.userid) === v) || null)}
|
|
placeholder="Select Substitute Rider"
|
|
hasSearch
|
|
/>
|
|
</VStack>
|
|
</LayoutContent>
|
|
}
|
|
footer={
|
|
<LayoutFooter hasDivider>
|
|
<HStack gap={2} justify="end">
|
|
<Button label="Cancel" variant="secondary" onClick={() => setEditingSub(null)} />
|
|
<Button label="Save Changes" variant="primary" isDisabled={!newSubRider} onClick={handleUpdateSubstitutionSubmit} />
|
|
</HStack>
|
|
</LayoutFooter>
|
|
}
|
|
/>
|
|
</Dialog>
|
|
|
|
<Dialog
|
|
isOpen={!!assignVehicleRider}
|
|
onOpenChange={(next) => {
|
|
if (!next) {
|
|
setAssignVehicleRider(null);
|
|
setSelectedVehicle(null);
|
|
}
|
|
}}
|
|
width={420}
|
|
purpose="form"
|
|
>
|
|
<Layout
|
|
header={
|
|
<DialogHeader
|
|
title={`Assign Vehicle · ${assignVehicleRider?.displayname || assignVehicleRider?.authname || ''}`}
|
|
onOpenChange={(next) => {
|
|
if (!next) {
|
|
setAssignVehicleRider(null);
|
|
setSelectedVehicle(null);
|
|
}
|
|
}}
|
|
/>
|
|
}
|
|
content={
|
|
<LayoutContent padding={4}>
|
|
<Selector
|
|
label="Vehicle"
|
|
isLabelHidden
|
|
options={vehicleOptions}
|
|
value={selectedVehicle ? String(selectedVehicle.vehicleid) : ''}
|
|
onChange={(v) => setSelectedVehicle((vehicles || []).find((x) => String(x.vehicleid) === v) || null)}
|
|
placeholder="Choose vehicle"
|
|
hasSearch
|
|
/>
|
|
</LayoutContent>
|
|
}
|
|
footer={
|
|
<LayoutFooter hasDivider>
|
|
<HStack gap={2} justify="end">
|
|
<Button
|
|
label="Cancel"
|
|
variant="secondary"
|
|
onClick={() => {
|
|
setAssignVehicleRider(null);
|
|
setSelectedVehicle(null);
|
|
}}
|
|
/>
|
|
<Button label="Assign" variant="primary" isDisabled={!selectedVehicle} onClick={() => assignVehicleMutation.mutate()} />
|
|
</HStack>
|
|
</LayoutFooter>
|
|
}
|
|
/>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default Riders;
|