import React, { useMemo, useState } from 'react'; import { ArrowRight, ListPlus, PackageCheck, PlaneLanding, PlaneTakeoff, Plus, Truck } from 'lucide-react'; import { Button, DataTable, EmptyState, Field, Grid, IconButton, Input, KpiCard, Modal, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Stack, StatusBadge, } from '@/components/ds'; import { ListToolbar } from '@/components/doormile/ListToolbar'; import { useAddTripsheetItem, useArriveTripsheet, useCreateTripsheet, useDispatchTripsheet, useHubs, useMilers, useTripsheets, useVehicles, } from '@/lib/doormileHooks'; import { matchesQuery, orDash, percentOf, useDebouncedValue } from '@/lib/doormileFormat'; /** * Tripsheets — hub-to-hub transport. * * A tripsheet is a vehicle running between two hubs with consignments loaded * onto it, and it moves through three states: created, dispatched, arrived. * Consignments are added by id rather than picked from a list because the * console has no reliable consignment-search endpoint — the id comes off the * label being scanned at the dock. */ const EMPTY_FORM = { sourcehubid: '', destinationhubid: '', vehicleid: '', driveruserid: '' }; const NO_DRIVER = 'none'; export default function Tripsheets() { const { data: tripsheets = [], isLoading, isFetching } = useTripsheets(); const { data: hubs = [] } = useHubs(); const { data: vehicles = [] } = useVehicles(); const { data: milers = [] } = useMilers(); const [search, setSearch] = useState(''); const debouncedSearch = useDebouncedValue(search); const [activeFilter, setActiveFilter] = useState('all'); const [form, setForm] = useState(EMPTY_FORM); const [dialogOpen, setDialogOpen] = useState(false); const [errors, setErrors] = useState({}); const [itemTrip, setItemTrip] = useState(null); const [itemConsignmentId, setItemConsignmentId] = useState(''); const createTripsheet = useCreateTripsheet(); const addItem = useAddTripsheetItem(); const dispatchTrip = useDispatchTripsheet(); const arriveTrip = useArriveTripsheet(); const hubMap = useMemo(() => new Map((hubs || []).map((hub) => [hub.hubid, hub])), [hubs]); const hubName = (id) => hubMap.get(id)?.hubname || (id ? `Hub #${id}` : '—'); const stats = useMemo(() => { const total = tripsheets.length; const status = (row) => String(row.status || '').toLowerCase(); return { total, created: tripsheets.filter((row) => status(row).includes('creat')).length, dispatched: tripsheets.filter((row) => status(row).includes('dispatch')).length, arrived: tripsheets.filter((row) => status(row).includes('arriv')).length, }; }, [tripsheets]); const rows = useMemo(() => { const status = (row) => String(row.status || '').toLowerCase(); let list = tripsheets; if (activeFilter === 'created') list = list.filter((row) => status(row).includes('creat')); else if (activeFilter === 'dispatched') list = list.filter((row) => status(row).includes('dispatch')); else if (activeFilter === 'arrived') list = list.filter((row) => status(row).includes('arriv')); return list.filter((row) => matchesQuery( row, ['tripsheetid', 'status', (r) => hubName(r.sourcehubid), (r) => hubName(r.destinationhubid)], debouncedSearch ) ); }, [tripsheets, activeFilter, debouncedSearch, hubMap]); const toggleFilter = (key) => setActiveFilter((current) => (current === key ? 'all' : key)); const set = (key) => (value) => setForm((current) => ({ ...current, [key]: value })); const openCreate = () => { setForm(EMPTY_FORM); setErrors({}); setDialogOpen(true); }; const create = () => { const nextErrors = {}; if (!form.sourcehubid) nextErrors.sourcehubid = 'Choose the source hub'; if (!form.destinationhubid) nextErrors.destinationhubid = 'Choose the destination hub'; if (form.sourcehubid && form.sourcehubid === form.destinationhubid) { nextErrors.destinationhubid = 'A trip has to end somewhere other than where it started'; } if (!form.vehicleid) nextErrors.vehicleid = 'Choose a vehicle'; setErrors(nextErrors); if (Object.keys(nextErrors).length) return; createTripsheet.mutate( { sourcehubid: Number(form.sourcehubid), destinationhubid: Number(form.destinationhubid), vehicleid: Number(form.vehicleid), driveruserid: form.driveruserid ? Number(form.driveruserid) : undefined, }, { onSuccess: (res) => res?.success !== false && setDialogOpen(false) } ); }; const submitItem = () => { if (!itemConsignmentId.trim()) return; addItem.mutate( { id: itemTrip.tripsheetid, consignmentid: itemConsignmentId.trim() }, { onSuccess: (res) => res?.success !== false && setItemConsignmentId('') } ); }; const columns = useMemo( () => [ { key: 'tripsheetid', header: 'Tripsheet', sortable: true, cell: (row) => (

#{orDash(row.tripsheetid)}

{row.vehicleid ? `Vehicle #${row.vehicleid}` : 'No vehicle'}

), }, { key: 'route', header: 'Route', sortValue: (row) => hubName(row.sourcehubid), cell: (row) => (
{hubName(row.sourcehubid)}
), }, { key: 'status', header: 'Status', sortable: true, cell: (row) => , }, { key: 'actions', header: 'Actions', align: 'right', cell: (row) => (
{ setItemTrip(row); setItemConsignmentId(''); }} /> dispatchTrip.mutate(row.tripsheetid)} /> arriveTrip.mutate(row.tripsheetid)} />
), }, ], [hubMap, dispatchTrip, arriveTrip] ); return ( setActiveFilter('all')} /> toggleFilter('created')} /> toggleFilter('dispatched')} /> toggleFilter('arrived')} /> New tripsheet } /> row.tripsheetid} loading={isLoading} refreshing={isFetching && !isLoading} pageSize={10} defaultSort={{ key: 'tripsheetid', direction: 'desc' }} isFiltered={Boolean(debouncedSearch) || activeFilter !== 'all'} onClearFilters={() => { setSearch(''); setActiveFilter('all'); }} emptyState={ debouncedSearch || activeFilter !== 'all' ? undefined : ( ) } /> } >
!open && setItemTrip(null)} title="Load a consignment" description={`Tripsheet #${itemTrip?.tripsheetid ?? ''} — ${hubName(itemTrip?.sourcehubid)} → ${hubName(itemTrip?.destinationhubid)}`} icon={ListPlus} busy={addItem.isPending} footer={ <> } > setItemConsignmentId(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); submitItem(); } }} placeholder="40" />
); }