import React, { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import dayjs from 'dayjs'; import { Ban, Bell, Bike, Car, CheckCircle2, CircleSlash, Pencil, Plus, Radio, Users, } from 'lucide-react'; import { Avatar, Button, ConfirmModal, DataTable, Drawer, EmptyState, Field, Grid, IconButton, Input, KpiCard, Modal, PageHeader, SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Stack, StatusBadge, Surface, Tabs, Textarea, ZoneSelector, } from '@/components/ds'; import { useAssignMilerVehicle, useBlockMiler, useMilerLogs, useMilers, useNotifyMiler, useRiderSummaryCounts, useVehicles, } from '@/lib/doormileHooks'; import { useZone } from '@/lib/ZoneContext'; import { formatDoormileTimestamp } from '@/lib/doormileTimestamp'; import { currency, matchesQuery, orDash, useDebouncedValue } from '@/lib/doormileFormat'; /** * Riders — the milers who actually carry the parcels. * * Two identifiers matter here and they are not interchangeable: `userid` is * what a booking's assignment records and what `assign-miler` wants, while * `milerprofileid` is the primary key of this resource and what block, notify, * assign-vehicle and logs all key on. Passing one where the other belongs 404s. * * The rider-substitution feature from the source console is deliberately absent: * it has no backend endpoint at all, so there is nothing here to build against. */ /** "Active" means neither offline nor blocked — the same rule the counts use. */ const isActive = (rider) => !['offline', 'blocked'].includes(String(rider?.availabilitystatus || '').toLowerCase()); const riderName = (rider) => rider?.displayname || rider?.authname || '—'; /** A bare `HH:mm:ss` from the shift record, rendered as a clock time. */ const shiftTime = (value) => value ? dayjs(`${dayjs().format('YYYY-MM-DD')} ${value}`).format('hh:mm A') : '—'; const TABS = [ { value: 'all', label: 'All riders' }, { value: 'active', label: 'Active' }, { value: 'blocked', label: 'Blocked' }, ]; export default function Riders() { const navigate = useNavigate(); const { data: riders = [], isLoading, isFetching } = useMilers(); const { data: summary } = useRiderSummaryCounts(); const { data: vehicles = [] } = useVehicles(); const [search, setSearch] = useState(''); const debouncedSearch = useDebouncedValue(search); const [tab, setTab] = useState('all'); const [blockRow, setBlockRow] = useState(null); const [vehicleRow, setVehicleRow] = useState(null); const [selectedVehicle, setSelectedVehicle] = useState(''); const [notifyRow, setNotifyRow] = useState(null); const [notice, setNotice] = useState({ title: 'DoormileXpress', message: '' }); const [logsRow, setLogsRow] = useState(null); const blockMiler = useBlockMiler(); const assignVehicle = useAssignMilerVehicle(); const notifyMiler = useNotifyMiler(); const { selectedZone, isAllZones, matchesZone } = useZone(); const zoneRiders = useMemo(() => { return (riders || []).filter(matchesZone); }, [riders, matchesZone]); const counts = useMemo(() => { const list = zoneRiders; return { all: list.length, active: list.filter(isActive).length, blocked: list.filter((rider) => String(rider.availabilitystatus || '').toLowerCase() === 'blocked').length, }; }, [zoneRiders]); const rows = useMemo(() => { let list = zoneRiders; if (tab === 'active') list = list.filter(isActive); else if (tab === 'blocked') { list = list.filter((rider) => String(rider.availabilitystatus || '').toLowerCase() === 'blocked'); } return list.filter((rider) => matchesQuery(rider, [riderName, 'phone', 'userid', 'city', 'suburb', 'defaultvehicletype'], debouncedSearch) ); }, [zoneRiders, tab, debouncedSearch]); const columns = useMemo( () => [ { key: 'rider', header: 'Miler', sortable: true, accessor: riderName, cell: (row) => (

{riderName(row)}

{orDash(row.phone)} · #{row.userid}

), }, { key: 'address', header: 'Base', sortValue: (row) => row.suburb || row.address || '', cell: (row) => (

{row.suburb || row.address || '—'}

{row.city ?

{row.city}

: null}
), hideBelow: 'lg', }, { key: 'defaultvehicletype', header: 'Vehicle', sortable: true, accessor: (row) => orDash(row.defaultvehicletype), hideBelow: 'md', }, { key: 'shift', header: 'Shift', sortValue: (row) => row.starttime || '', accessor: (row) => `${shiftTime(row.starttime)} – ${shiftTime(row.endtime)}`, hideBelow: 'lg', }, { key: 'basefare', header: 'Base fare', sortValue: (row) => Number(row.basefare) || 0, accessor: (row) => currency(row.basefare), align: 'right', hideBelow: 'lg', }, { key: 'availabilitystatus', header: 'Status', sortable: true, cell: (row) => , }, { key: 'actions', header: 'Actions', align: 'right', cell: (row) => (
navigate('/doormile/milers/edit', { state: { rider: row } })} /> { setVehicleRow(row); setSelectedVehicle(''); }} /> { setNotifyRow(row); setNotice({ title: 'DoormileXpress', message: '' }); }} /> setLogsRow(row)} /> setBlockRow(row)} />
), }, ], [navigate] ); return ( navigate('/doormile/milers/create')}> New miler } /> setTab('all')} /> String(r.availabilitystatus || '').toLowerCase() === 'on_delivery' || String(r.availabilitystatus || '').toLowerCase() === 'ondelivery' ).length } label="On delivery" tone="accent" index={2} loading={isLoading} /> setTab('blocked')} /> {/* Wraps, and the tab strip scrolls in its own track — see Orders.jsx for why two `shrink-0` halves scrolled the whole page on a phone. */}
({ ...entry, count: counts[entry.value] }))} value={tab} onChange={setTab} className="shrink-0" />

{rows.length === (counts[tab] ?? counts.all) ? `${counts[tab] ?? counts.all} milers` : `${rows.length} of ${counts[tab] ?? counts.all} milers`}

row.milerprofileid ?? row.userid} loading={isLoading} refreshing={isFetching && !isLoading} pageSize={10} defaultSort={{ key: 'rider', direction: 'asc' }} isFiltered={Boolean(debouncedSearch)} onClearFilters={() => setSearch('')} emptyState={ debouncedSearch ? undefined : ( navigate('/doormile/milers/create') }} /> ) } />
!open && setBlockRow(null)} title="Block this miler?" description={`${riderName(blockRow || {})} will stop receiving assignments until they are unblocked.`} confirmLabel="Block miler" busy={blockMiler.isPending} onConfirm={() => { blockMiler.mutate( { id: blockRow.milerprofileid, data: {} }, { onSettled: () => setBlockRow(null) } ); }} /> !open && setVehicleRow(null)} title="Assign a vehicle" description={riderName(vehicleRow || {})} icon={Car} busy={assignVehicle.isPending} footer={ <> } > !open && setNotifyRow(null)} title="Notify this miler" description={`${riderName(notifyRow || {})} — delivered to the miler app as a push notification.`} icon={Bell} busy={notifyMiler.isPending} footer={ <> } > setNotice({ ...notice, title: e.target.value })} />