Dispatch Assignment Board
blocklogisticsLast-mile route dispatching, delivery driver stop sequencing, and vehicle payload load balancing: live dispatch overview KPIs, zone filtering, AI route auto-sequencing, and driver route cards with vehicle payload load balancing, live progress tracking, interactive stops timeline, driver communications, and telemetry actions.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/dispatch-assignment-board.json$npx shadcn@latest add https://uipkge.dev/r/react/dispatch-assignment-board.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/dispatch-assignment-board.json$bunx shadcn@latest add https://uipkge.dev/r/react/dispatch-assignment-board.jsonNamed registry:
npx shadcn@latest add @uipkge-react/dispatch-assignment-boardInstalls to:components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
initialZone | string | — | optional |
initialFilter | 'all' | 'in_transit' | 'high_load' | 'near_complete' | — | optional |
className | string | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
RouteStopinterface RouteStop {
id: string
sequence: number
locationName: string
address: string
status: StopStatus
deliveredTime?: string
estimatedTime?: string
packagesCount: number
packageType: string
notes?: string
recipient?: string
}DriverRouteinterface DriverRoute {
id: string
routeCode: string
routeName: string
zone: string
driver: {
name: string
avatar?: string
initials: string
phone: string
status: 'on_route' | 'break' | 'loading'
radioChannel: string
}
vehicle: {
name: string
type: string
plateNumber: string
powerLevel: string
powerType: 'electric' | 'diesel'
}
completedStops: number
totalStops: number
currentPayloadLbs: number
maxPayloadLbs: number
shiftWindow: string
estimatedCompletion: string
telemetry: {
speed: string
heading: string
currentLocation: string
lastPing: string
signal: string
}
stops: RouteStop[]
}ActiveModalStateinterface ActiveModalState {
type: 'gps' | 'call' | 'reassign'
route: DriverRoute
targetRouteId?: string
}Files installed (4)
components/blocks/DispatchAssignmentBoard.tsx33.2 kB
'use client' import * as React from 'react' import { ArrowRightLeft, BatteryCharging, Clock, Download, Fuel, Gauge, MapPin, Navigation, PackageCheck, Phone, Search, ShieldCheck, CheckCircle2, Truck, X, Check, } from 'lucide-react' import { DispatchActionModal } from './DispatchActionModal' import { DRIVER_ROUTES_DATA } from './dispatch-data' import type { ActiveModalState, DriverRoute, RouteStop, StopStatus } from './dispatch-assignment-board-types' export type { ActiveModalState, DriverRoute, RouteStop, StopStatus } from './dispatch-assignment-board-types' import { cn } from '@/lib/utils' import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { Progress } from '@/components/ui/progress' import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Separator } from '@/components/ui/separator' export interface DispatchAssignmentBoardProps { initialZone?: string initialFilter?: 'all' | 'in_transit' | 'high_load' | 'near_complete' className?: string } export function DispatchAssignmentBoard({ initialZone = 'Metro Los Angeles - North Zone', initialFilter = 'all', className, }: DispatchAssignmentBoardProps) { const [selectedZone, setSelectedZone] = React.useState(initialZone) const [filterType, setFilterType] = React.useState<'all' | 'in_transit' | 'high_load' | 'near_complete'>( initialFilter, ) const [searchQuery, setSearchQuery] = React.useState('') const [isOptimizing, setIsOptimizing] = React.useState(false) const [notificationBanner, setNotificationBanner] = React.useState<{ title: string message: string type: 'success' | 'info' } | null>(null) const [activeModal, setActiveModal] = React.useState<{ type: 'gps' | 'call' | 'reassign' route: DriverRoute targetRouteId?: string } | null>(null) const filteredRoutes = React.useMemo(() => { return DRIVER_ROUTES_DATA.filter((route) => { // Zone filter if (selectedZone !== 'all' && route.zone !== selectedZone) { return false } // Category / Quick filter if (filterType === 'in_transit' && route.driver.status !== 'on_route') { return false } if (filterType === 'high_load' && route.currentPayloadLbs / route.maxPayloadLbs < 0.8) { return false } if (filterType === 'near_complete' && route.completedStops / route.totalStops < 0.75) { return false } // Search query if (searchQuery.trim()) { const q = searchQuery.toLowerCase() const matchRoute = route.routeCode.toLowerCase().includes(q) || route.routeName.toLowerCase().includes(q) const matchDriver = route.driver.name.toLowerCase().includes(q) const matchVehicle = route.vehicle.name.toLowerCase().includes(q) || route.vehicle.plateNumber.toLowerCase().includes(q) const matchStop = route.stops.some( (s) => s.locationName.toLowerCase().includes(q) || s.address.toLowerCase().includes(q), ) if (!matchRoute && !matchDriver && !matchVehicle && !matchStop) { return false } } return true }) }, [selectedZone, filterType, searchQuery]) const handleAutoOptimize = () => { setIsOptimizing(true) setNotificationBanner(null) setTimeout(() => { setIsOptimizing(false) setNotificationBanner({ title: 'AI Route Optimization Complete', message: 'Re-sequenced 4 active routes · Saved 18.4 road miles · Estimated 22 min overall transit reduction.', type: 'success', }) }, 750) } const handleExportManifests = () => { setNotificationBanner({ title: 'Dispatch Manifests Exported', message: 'Generated PDF loading manifests and CSV stop sequences for 4 routes (180 total stops).', type: 'info', }) } const openModal = (type: 'gps' | 'call' | 'reassign', route: DriverRoute) => { setActiveModal({ type, route, targetRouteId: DRIVER_ROUTES_DATA.find((r) => r.id !== route.id)?.id, }) } const closeModal = () => { setActiveModal(null) } const handleConfirmReassign = () => { if (activeModal) { const route = activeModal.route setNotificationBanner({ title: 'Stops Successfully Reassigned', message: `Pending stop from ${route.routeCode} transferred. Driver GPS routes and manifest synced.`, type: 'success', }) closeModal() } } return ( <div className={cn('text-foreground w-full space-y-6', className)} data-slot="dispatch-assignment-board"> {/* Header Section */} <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> <div className="space-y-1"> <div className="flex flex-wrap items-center gap-2"> <div className="bg-success/10 text-success flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-semibold"> <span className="relative flex h-2 w-2"> <span className="bg-success absolute inline-flex h-full w-full rounded-full opacity-75" /> <span className="bg-success relative inline-flex h-2 w-2 rounded-full" /> </span> <span>Live Dispatch Operations</span> </div> <div className="text-muted-foreground flex items-center gap-1 text-xs"> <Clock className="h-3.5 w-3.5" /> <span className="font-mono tabular-nums">Today · Aug 21, 2026</span> </div> </div> <h1 className="text-foreground text-xl font-bold tracking-tight sm:text-2xl"> Delivery Dispatch & Route Optimization </h1> <p className="text-muted-foreground text-xs sm:text-sm"> Last-mile route sequencing, real-time stop execution, and vehicle payload load balancing. </p> </div> {/* Header Controls */} <div className="flex flex-wrap items-center gap-2.5"> <div className="w-full sm:w-64"> <Select value={selectedZone} onValueChange={setSelectedZone}> <SelectTrigger className="h-9 w-full text-xs font-medium" aria-label="Select Dispatch Zone"> <SelectValue placeholder="Select Zone" /> </SelectTrigger> <SelectContent> <SelectGroup> <SelectLabel className="text-xs">Dispatch Zones</SelectLabel> <SelectItem value="all" className="text-xs"> All Dispatch Zones </SelectItem> <SelectItem value="Metro Los Angeles - North Zone" className="text-xs"> Metro Los Angeles - North Zone </SelectItem> <SelectItem value="Metro Los Angeles - South Zone" className="text-xs"> Metro Los Angeles - South Zone </SelectItem> <SelectItem value="San Fernando Valley Hub" className="text-xs"> San Fernando Valley Hub </SelectItem> <SelectItem value="Orange County Central" className="text-xs"> Orange County Central </SelectItem> </SelectGroup> </SelectContent> </Select> </div> <Button aria-label="Download attachment" variant="outline" size="sm" className="h-9 gap-1.5 text-xs font-medium" onClick={handleExportManifests} > <Download className="h-3.5 w-3.5" /> <span>Export Manifests</span> </Button> <Button size="sm" className="h-9 gap-1.5 text-xs font-medium shadow-xs" disabled={isOptimizing} onClick={handleAutoOptimize} > <Navigation className={cn('h-3.5 w-3.5', isOptimizing && 'text-primary-foreground animate-spin')} /> <span>{isOptimizing ? 'Optimizing Routes...' : 'Auto-Optimize Routes'}</span> </Button> </div> </div> {/* Notification Banner */} {notificationBanner && ( <div className={cn( 'animate-in fade-in-50 flex items-start justify-between rounded-lg border p-3.5 text-xs transition-colors duration-200', notificationBanner.type === 'success' ? 'border-success/30 bg-success/10 text-success' : 'border-primary/30 bg-primary/10 text-foreground', )} > <div className="flex items-start gap-2.5"> {notificationBanner.type === 'success' ? ( <CheckCircle2 className="text-success mt-0.5 h-4 w-4 shrink-0" /> ) : ( <PackageCheck className="text-primary mt-0.5 h-4 w-4 shrink-0" /> )} <div> <span className="font-semibold">{notificationBanner.title}:</span> <span className="text-muted-foreground dark:text-foreground/80 ml-1">{notificationBanner.message}</span> </div> </div> <button aria-label="Dismiss notification" type="button" className="text-muted-foreground hover:text-foreground transition-colors" onClick={() => setNotificationBanner(null)} > <X className="h-4 w-4" /> </button> </div> )} {/* Dispatch Overview KPI Cards (4 Cards) */} <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> {/* KPI 1: Active Routes */} <Card className="border-border shadow-xs"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-muted-foreground text-xs font-medium">Total Routes Active</CardTitle> <div className="bg-primary/10 text-primary flex h-8 w-8 items-center justify-center rounded-md"> <Truck className="h-4 w-4" /> </div> </CardHeader> <CardContent className="space-y-1.5"> <div className="flex items-baseline gap-2"> <span className="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums">8</span> <span className="text-muted-foreground text-xs font-medium">routes dispatched</span> </div> <div className="text-muted-foreground flex items-center gap-2 text-xs"> <Badge variant="outline" className="border-success/20 bg-success/10 text-success h-5 px-1.5 text-xs font-medium" > 6 In Transit </Badge> <span>2 loading at hub</span> </div> </CardContent> </Card> {/* KPI 2: Stops Remaining & Progress */} <Card className="border-border shadow-xs"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-muted-foreground text-xs font-medium">Total Stops Remaining</CardTitle> <div className="bg-info/10 text-info flex h-8 w-8 items-center justify-center rounded-md"> <PackageCheck className="h-4 w-4" /> </div> </CardHeader> <CardContent className="space-y-2"> <div className="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div className="flex items-baseline gap-1"> <span className="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums">142</span> <span className="text-muted-foreground font-mono text-xs tabular-nums">/ 180 stops</span> </div> <span className="text-info text-info font-mono text-xs font-semibold tabular-nums">78.8%</span> </div> <Progress value={78.8} className="h-1.5" /> <p className="text-muted-foreground text-xs"> <span className="text-foreground font-mono font-semibold tabular-nums">38</span> stops pending completion </p> </CardContent> </Card> {/* KPI 3: On-Time Rate */} <Card className="border-border shadow-xs"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-muted-foreground text-xs font-medium">On-Time Delivery Rate</CardTitle> <div className="bg-success/10 text-success flex h-8 w-8 items-center justify-center rounded-md"> <ShieldCheck className="h-4 w-4" /> </div> </CardHeader> <CardContent className="space-y-1.5"> <div className="flex items-baseline gap-2"> <span className="text-success text-success font-mono text-2xl font-bold tracking-tight tabular-nums"> 97.4% </span> <Badge variant="outline" className="border-success/20 bg-success/10 text-success h-5 px-1.5 text-xs font-medium" > +1.8% vs SLA </Badge> </div> <p className="text-muted-foreground text-xs"> Target: <span className="text-foreground font-mono font-medium tabular-nums">≥95.0%</span> · 0 SLA breaches </p> </CardContent> </Card> {/* KPI 4: Cargo Weight Dispatched */} <Card className="border-border shadow-xs"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-muted-foreground text-xs font-medium">Total Cargo Weight Dispatched</CardTitle> <div className="bg-warning/10 text-warning flex h-8 w-8 items-center justify-center rounded-md"> <Gauge className="h-4 w-4" /> </div> </CardHeader> <CardContent className="space-y-2"> <div className="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div className="flex items-baseline gap-1"> <span className="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums">14,280</span> <span className="text-muted-foreground text-xs">lbs</span> </div> <Badge variant="outline" className="border-warning/20 bg-warning/10 text-warning h-5 px-1.5 text-xs font-medium" > 82% capacity </Badge> </div> <div className="bg-muted relative h-1.5 w-full overflow-hidden rounded-full"> <div className="bg-warning h-full rounded-full transition-[width] duration-500" style={{ width: '82%' }} /> </div> <p className="text-muted-foreground text-xs"> Payload limit: <span className="text-foreground font-mono font-medium tabular-nums">17,400 lbs</span> · Well balanced </p> </CardContent> </Card> </div> {/* Filter & Search Controls Bar */} <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-wrap items-center gap-1.5"> <Button size="sm" variant={filterType === 'all' ? 'default' : 'outline'} className="h-8 text-xs font-medium" onClick={() => setFilterType('all')} > All Routes ({DRIVER_ROUTES_DATA.length}) </Button> <Button size="sm" variant={filterType === 'in_transit' ? 'default' : 'outline'} className="h-8 text-xs font-medium" onClick={() => setFilterType('in_transit')} > In Transit (4) </Button> <Button size="sm" variant={filterType === 'high_load' ? 'default' : 'outline'} className="h-8 text-xs font-medium" onClick={() => setFilterType('high_load')} > High Load (>80%) </Button> <Button size="sm" variant={filterType === 'near_complete' ? 'default' : 'outline'} className="h-8 text-xs font-medium" onClick={() => setFilterType('near_complete')} > Near Completion (>70%) </Button> </div> <div className="relative w-full sm:w-72"> <Search className="text-muted-foreground absolute top-2.5 left-2.5 h-3.5 w-3.5" /> <input value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} type="text" placeholder="Filter driver, route, or stop..." className="border-border bg-background text-foreground placeholder:text-muted-foreground focus-visible:ring-ring h-8 w-full rounded-md border pr-3 pl-8 text-xs focus-visible:ring-2 focus-visible:outline-hidden" /> {searchQuery && ( <button type="button" className="text-muted-foreground hover:text-foreground absolute top-2 right-2" aria-label="Clear search" onClick={() => setSearchQuery('')} > <X className="h-3.5 w-3.5" /> </button> )} </div> </div> {/* Empty State when filtered out */} {filteredRoutes.length === 0 ? ( <div className="border-border flex flex-col items-center justify-center rounded-xl border border-dashed py-12 text-center"> <Truck className="text-muted-foreground/50 mb-3 h-10 w-10" /> <h3 className="text-foreground text-sm font-semibold">No matching dispatch routes found</h3> <p className="text-muted-foreground mt-1 max-w-sm text-xs"> Try resetting your zone selector or search query to see active drivers and vehicle assignments. </p> <Button variant="outline" size="sm" className="mt-4 h-8 text-xs" onClick={() => { setSelectedZone('all') setSearchQuery('') setFilterType('all') }} > Reset Filters </Button> </div> ) : ( /* Driver Routes Grid / Multi-Card Board (4 Driver Route Cards) */ <div className="grid grid-cols-1 gap-6 xl:grid-cols-2"> {filteredRoutes.map((route) => ( <Card key={route.id} className="border-border flex flex-col shadow-xs transition-[color,background-color,border-color,box-shadow] hover:shadow-sm" > {/* Card Header: Route ID, Status, Driver and Vehicle snapshot */} <CardHeader className="space-y-3 pb-3"> <div className="flex flex-wrap items-start justify-between gap-2"> <div className="space-y-0.5"> <div className="flex items-center gap-2"> <span className="text-primary font-mono text-xs font-semibold">{route.routeCode}</span> <CardTitle className="text-foreground text-sm font-bold sm:text-base"> {route.routeName} </CardTitle> </div> <p className="text-muted-foreground text-xs">{route.zone}</p> </div> <div className="flex items-center gap-1.5"> <Badge variant="outline" className="border-success/20 bg-success/10 text-success h-5 px-2 text-xs font-medium" > In Transit · On Schedule </Badge> </div> </div> <Separator /> {/* Driver Profile & Vehicle Metadata */} <div className="bg-muted/40 flex flex-col gap-3 rounded-lg p-3 sm:flex-row sm:items-center sm:justify-between"> <div className="flex items-center gap-3"> <Avatar className="border-border h-9 w-9 border"> <AvatarFallback className="bg-primary/10 text-primary text-xs font-bold"> {route.driver.initials} </AvatarFallback> </Avatar> <div className="space-y-0.5"> <div className="flex items-center gap-1.5"> <span className="text-foreground text-xs font-semibold">{route.driver.name}</span> <span className="bg-success inline-flex h-1.5 w-1.5 rounded-full" title="Active" /> </div> <div className="text-muted-foreground flex items-center gap-2 text-xs"> <span className="font-mono tabular-nums">{route.driver.phone}</span> <span>·</span> <span>{route.driver.radioChannel}</span> </div> </div> </div> <div className="border-border flex items-center justify-between gap-x-2 border-t pt-2 sm:border-t-0 sm:pt-0 sm:text-right"> <div className="space-y-0.5"> <div className="flex items-center gap-1.5 sm:justify-end"> <Truck className="text-muted-foreground h-3.5 w-3.5" /> <span className="text-foreground text-xs font-medium">{route.vehicle.name}</span> </div> <div className="text-muted-foreground flex items-center gap-1.5 text-xs sm:justify-end"> <span className="font-mono text-xs">{route.vehicle.plateNumber}</span> <span>·</span> <span className="flex items-center gap-1 font-mono text-xs tabular-nums"> {route.vehicle.powerType === 'electric' ? ( <BatteryCharging className="text-success h-3 w-3" /> ) : ( <Fuel className="text-info h-3 w-3" /> )} {route.vehicle.powerLevel} </span> </div> </div> </div> </div> </CardHeader> <CardContent className="flex-1 space-y-4"> {/* Route Progress & Payload Load Balance Overview */} <div className="border-border bg-card grid grid-cols-1 gap-3 rounded-lg border p-3 sm:grid-cols-2"> {/* Stops Progress */} <div className="space-y-1.5"> <div className="flex items-center justify-between gap-x-2 text-xs"> <span className="text-muted-foreground">Stops Completion</span> <span className="text-foreground font-mono font-semibold tabular-nums"> {route.completedStops} / {route.totalStops}{' '} <span className="text-muted-foreground font-normal"> ({Math.round((route.completedStops / route.totalStops) * 100)}%) </span> </span> </div> <Progress value={(route.completedStops / route.totalStops) * 100} className="h-1.5" /> <div className="text-muted-foreground flex items-center justify-between gap-x-2 font-mono text-xs tabular-nums"> <span>Shift: {route.shiftWindow}</span> <span>ETA: {route.estimatedCompletion}</span> </div> </div> {/* Payload Weight Utilization */} <div className="space-y-1.5"> <div className="flex items-center justify-between gap-x-2 text-xs"> <span className="text-muted-foreground">Payload Load Balance</span> <span className="text-foreground font-mono font-semibold tabular-nums"> {route.currentPayloadLbs.toLocaleString()} / {route.maxPayloadLbs.toLocaleString()} lbs </span> </div> <div className="bg-muted relative h-1.5 w-full overflow-hidden rounded-full"> <div className={cn( 'h-full rounded-full transition-[width,background-color] duration-500', route.currentPayloadLbs / route.maxPayloadLbs > 0.85 ? 'bg-warning' : 'bg-primary', )} style={{ width: `${(route.currentPayloadLbs / route.maxPayloadLbs) * 100}%`, }} /> </div> <div className="text-muted-foreground flex items-center justify-between gap-x-2 font-mono text-xs tabular-nums"> <span>{Math.round((route.currentPayloadLbs / route.maxPayloadLbs) * 100)}% capacity</span> <span className={ route.currentPayloadLbs / route.maxPayloadLbs > 0.85 ? 'text-warning font-medium' : 'text-muted-foreground' } > {route.maxPayloadLbs - route.currentPayloadLbs} lbs available </span> </div> </div> </div> {/* Stops Timeline Section */} <div className="space-y-2"> <div className="flex items-center justify-between gap-x-2"> <h4 className="text-muted-foreground text-xs font-semibold tracking-wide uppercase"> Stop Sequence & Delivery Execution </h4> <span className="text-muted-foreground font-mono text-xs tabular-nums"> {route.stops.filter((s) => s.status === 'delivered').length} done ·{' '} {route.stops.filter((s) => s.status === 'in_progress').length} active ·{' '} {route.stops.filter((s) => s.status === 'pending').length} pending </span> </div> {/* Timeline List */} <div className="before:bg-border relative space-y-3 pl-2 before:absolute before:top-3 before:bottom-3 before:left-5 before:w-px"> {route.stops.map((stop) => ( <div key={stop.id} className={cn( 'hover:border-border hover:bg-muted/30 relative flex items-start gap-3 rounded-lg border border-transparent p-2 transition-colors', stop.status === 'in_progress' && 'border-primary/30 bg-primary/5 dark:bg-primary/10', )} > {/* Status Icon Node */} <div className="relative z-10 flex shrink-0 items-center justify-center"> {/* Delivered Check Icon */} {stop.status === 'delivered' && ( <div className="ring-background bg-success/15 text-success text-success flex h-6 w-6 items-center justify-center rounded-full ring-2"> <Check className="h-3.5 w-3.5 stroke-[3]" /> </div> )} {/* In Progress Pulsing Map Pin */} {stop.status === 'in_progress' && ( <div className="bg-primary text-primary-foreground ring-primary/20 relative flex h-6 w-6 items-center justify-center rounded-full shadow-xs ring-4"> <MapPin className="h-3.5 w-3.5 animate-bounce" /> </div> )} {/* Pending Clock / Circle Node */} {stop.status === 'pending' && ( <div className="border-border bg-muted text-muted-foreground ring-background flex h-6 w-6 items-center justify-center rounded-full border ring-2"> <Clock className="h-3 w-3" /> </div> )} </div> {/* Stop Details */} <div className="min-w-0 flex-1 space-y-0.5"> <div className="flex flex-wrap items-center justify-between gap-1"> <div className="flex items-center gap-1.5 truncate"> <span className="text-muted-foreground font-mono text-xs font-semibold"> #{stop.sequence} </span> <span className="text-foreground truncate text-xs font-semibold"> {stop.locationName} </span> </div> {/* Stop Status Timestamp / ETA */} <div className="flex items-center gap-1"> {stop.status === 'delivered' && ( <Badge variant="outline" className="border-success/20 bg-success/10 text-success text-success h-4.5 px-1.5 font-mono text-xs font-medium tabular-nums" > Delivered {stop.deliveredTime} </Badge> )} {stop.status === 'in_progress' && ( <Badge className="bg-primary text-primary-foreground h-4.5 px-1.5 font-mono text-xs font-medium tabular-nums"> In Progress · ETA {stop.estimatedTime} </Badge> )} {stop.status === 'pending' && ( <Badge variant="secondary" className="text-muted-foreground h-4.5 px-1.5 font-mono text-xs font-medium tabular-nums" > Pending </Badge> )} </div> </div> <p className="text-muted-foreground truncate text-xs">{stop.address}</p> <div className="text-muted-foreground flex flex-wrap items-center gap-2 pt-0.5 text-xs"> <span className="text-foreground font-mono font-medium tabular-nums"> {stop.packagesCount} {stop.packageType} </span> {stop.recipient && <span className="text-muted-foreground">· {stop.recipient}</span>} {stop.notes && <span className="text-warning font-medium">· {stop.notes}</span>} </div> </div> </div> ))} </div> </div> </CardContent> {/* Card Footer: Quick Action Buttons */} <CardFooter className="border-border border-t pt-3"> <div className="flex w-full flex-wrap items-center justify-between gap-2"> <div className="flex items-center gap-1.5"> <Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs font-medium" onClick={() => openModal('reassign', route)} > <ArrowRightLeft className="h-3.5 w-3.5" /> <span>Reassign Stops</span> </Button> <Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs font-medium" onClick={() => openModal('call', route)} > <Phone className="h-3.5 w-3.5" /> <span>Call Driver</span> </Button> </div> <Button variant="default" size="sm" className="h-8 gap-1.5 text-xs font-medium shadow-xs" onClick={() => openModal('gps', route)} > <Navigation className="h-3.5 w-3.5" /> <span>View Live GPS</span> </Button> </div> </CardFooter> </Card> ))} </div> )} {/* Action Modals (GPS telemetry, direct call, reassign stops) */} <DispatchActionModal activeModal={activeModal} allRoutes={DRIVER_ROUTES_DATA} onClose={closeModal} onConfirmReassign={handleConfirmReassign} onSelectTargetRoute={(targetId) => { if (activeModal) { setActiveModal({ ...activeModal, targetRouteId: targetId }) } }} onOpenModal={openModal} /> </div> ) }components/blocks/DispatchActionModal.tsx13.7 kB
'use client' import * as React from 'react' import { ArrowRightLeft, Check, MapPin, Navigation, Phone, PhoneCall, X } from 'lucide-react' import { cn } from '@/lib/utils' import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Separator } from '@/components/ui/separator' import type { ActiveModalState, DriverRoute } from './dispatch-assignment-board-types' interface DispatchActionModalProps { activeModal: ActiveModalState | null allRoutes: DriverRoute[] onClose: () => void onConfirmReassign: () => void onSelectTargetRoute: (targetId: string) => void onOpenModal: (type: 'gps' | 'call' | 'reassign', route: DriverRoute) => void } export function DispatchActionModal({ activeModal, allRoutes, onClose, onConfirmReassign, onSelectTargetRoute, onOpenModal, }: DispatchActionModalProps) { if (!activeModal) return null return ( <div className="animate-in fade-in fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4 backdrop-blur-xs duration-150" onClick={(e) => { if (e.target === e.currentTarget) onClose() }} > {/* GPS Telemetry Modal */} {activeModal.type === 'gps' && ( <div className="border-border bg-card w-full max-w-lg space-y-4 rounded-xl border p-6 shadow-xl"> <div className="flex items-center justify-between gap-x-2"> <div className="flex items-center gap-2"> <div className="bg-primary/10 text-primary flex h-8 w-8 items-center justify-center rounded-md"> <Navigation className="h-4 w-4" /> </div> <div> <h3 className="text-foreground text-sm font-bold">Live Telemetry & GPS Tracking</h3> <p className="text-muted-foreground text-xs"> {activeModal.route.routeCode} · {activeModal.route.driver.name} </p> </div> </div> <button aria-label="Close modal" type="button" className="text-muted-foreground hover:text-foreground rounded-md p-1" onClick={onClose} > <X className="h-4 w-4" /> </button> </div> <Separator /> <div className="grid grid-cols-2 gap-3 sm:grid-cols-4"> <div className="border-border bg-muted/30 rounded-lg border p-2.5 text-center"> <span className="text-muted-foreground text-xs">Current Speed</span> <p className="text-foreground font-mono text-sm font-bold">{activeModal.route.telemetry.speed}</p> </div> <div className="border-border bg-muted/30 rounded-lg border p-2.5 text-center"> <span className="text-muted-foreground text-xs">Heading</span> <p className="text-foreground font-mono text-sm font-bold">{activeModal.route.telemetry.heading}</p> </div> <div className="border-border bg-muted/30 rounded-lg border p-2.5 text-center"> <span className="text-muted-foreground text-xs">Power / Fuel</span> <p className="text-success font-mono text-sm font-bold">{activeModal.route.vehicle.powerLevel}</p> </div> <div className="border-border bg-muted/30 rounded-lg border p-2.5 text-center"> <span className="text-muted-foreground text-xs">GPS Ping</span> <p className="text-foreground font-mono text-sm font-bold">{activeModal.route.telemetry.lastPing}</p> </div> </div> <div className="border-border bg-muted/20 space-y-1.5 rounded-lg border p-3"> <div className="flex items-center justify-between gap-x-2 text-xs"> <span className="text-muted-foreground">Current Coordinates & Street</span> <span className="text-success font-mono font-medium">Signal: {activeModal.route.telemetry.signal}</span> </div> <div className="flex items-start gap-2"> <MapPin className="text-primary mt-0.5 h-4 w-4 shrink-0" /> <p className="text-foreground text-xs font-semibold">{activeModal.route.telemetry.currentLocation}</p> </div> </div> {/* Next Stop Snapshot */} <div className="border-primary/20 bg-primary/5 space-y-1 rounded-lg border p-3"> <span className="text-primary text-xs font-semibold tracking-wide uppercase">Next Stop In Sequence</span> <p className="text-foreground text-xs font-bold"> {activeModal.route.stops.find((s) => s.status === 'in_progress')?.locationName || 'Hub Return'} </p> <p className="text-muted-foreground text-xs"> {activeModal.route.stops.find((s) => s.status === 'in_progress')?.address} </p> </div> <div className="flex justify-end gap-2 pt-2"> <Button aria-label="Close modal" variant="outline" size="sm" className="h-8 text-xs" onClick={onClose}> Close </Button> <Button size="sm" className="h-8 text-xs font-medium" onClick={() => onOpenModal('call', activeModal.route)} > <Phone className="mr-1.5 h-3.5 w-3.5" /> Call Driver </Button> </div> </div> )} {/* Call Driver Modal */} {activeModal.type === 'call' && ( <div className="border-border bg-card w-full max-w-md space-y-4 rounded-xl border p-6 shadow-xl"> <div className="flex items-center justify-between gap-x-2"> <div className="flex items-center gap-2"> <div className="bg-success/10 text-success flex h-8 w-8 items-center justify-center rounded-md"> <PhoneCall className="h-4 w-4" /> </div> <div> <h3 className="text-foreground text-sm font-bold">Dispatch Driver Communications</h3> <p className="text-muted-foreground text-xs">{activeModal.route.routeCode} Direct Link</p> </div> </div> <button aria-label="Close modal" type="button" className="text-muted-foreground hover:text-foreground rounded-md p-1" onClick={onClose} > <X className="h-4 w-4" /> </button> </div> <Separator /> <div className="border-border bg-muted/40 flex items-center gap-3 rounded-lg border p-3"> <Avatar className="border-border h-11 w-11 border"> <AvatarFallback className="bg-primary/10 text-primary text-sm font-bold"> {activeModal.route.driver.initials} </AvatarFallback> </Avatar> <div className="space-y-0.5"> <h4 className="text-foreground text-sm font-semibold">{activeModal.route.driver.name}</h4> <p className="text-muted-foreground font-mono text-xs tabular-nums">{activeModal.route.driver.phone}</p> <p className="text-muted-foreground text-xs"> {activeModal.route.driver.radioChannel} · Hands-Free Active </p> </div> </div> <div className="space-y-2 text-xs"> <span className="text-muted-foreground font-medium">Quick Broadcast Dispatch Presets:</span> <div className="grid grid-cols-2 gap-2"> <button type="button" className="border-border bg-background hover:border-primary/40 hover:bg-accent rounded-md border p-2 text-left transition-colors" onClick={onConfirmReassign} > <span className="text-foreground block font-semibold">Traffic Alert</span> <span className="text-muted-foreground text-xs">Reroute around I-10 slowdown</span> </button> <button type="button" className="border-border bg-background hover:border-primary/40 hover:bg-accent rounded-md border p-2 text-left transition-colors" onClick={onConfirmReassign} > <span className="text-foreground block font-semibold">Priority Stop</span> <span className="text-muted-foreground text-xs">Bump hospital drop to next</span> </button> </div> </div> <div className="flex justify-end gap-2 pt-2"> <Button aria-label="Close modal" variant="outline" size="sm" className="h-8 text-xs" onClick={onClose}> Cancel </Button> <Button size="sm" className="bg-success hover:bg-success/90 h-8 text-xs font-medium text-white" onClick={onConfirmReassign} > <Phone className="mr-1.5 h-3.5 w-3.5" /> Start Direct Call </Button> </div> </div> )} {/* Reassign Stops Modal */} {activeModal.type === 'reassign' && ( <div className="border-border bg-card w-full max-w-lg space-y-4 rounded-xl border p-6 shadow-xl"> <div className="flex items-center justify-between gap-x-2"> <div className="flex items-center gap-2"> <div className="bg-warning/10 text-warning flex h-8 w-8 items-center justify-center rounded-md"> <ArrowRightLeft className="h-4 w-4" /> </div> <div> <h3 className="text-foreground text-sm font-bold">Reassign Pending Stops</h3> <p className="text-muted-foreground text-xs"> Transfer workload from {activeModal.route.routeCode} ({activeModal.route.driver.name}) </p> </div> </div> <button aria-label="Close modal" type="button" className="text-muted-foreground hover:text-foreground rounded-md p-1" onClick={onClose} > <X className="h-4 w-4" /> </button> </div> <Separator /> <div className="space-y-3 text-xs"> <div> <label className="text-foreground mb-1 block font-semibold">Pending Stop to Transfer:</label> <div className="border-border bg-muted/40 rounded-lg border p-3"> <div className="flex items-center justify-between gap-x-2"> <span className="text-foreground font-semibold"> {activeModal.route.stops.find((s) => s.status === 'pending')?.locationName || 'Stop #4 - Delivery Destination'} </span> <Badge variant="outline" className="text-xs"> {activeModal.route.stops.find((s) => s.status === 'pending')?.packagesCount || 2} packages </Badge> </div> <p className="text-muted-foreground mt-0.5 text-xs"> {activeModal.route.stops.find((s) => s.status === 'pending')?.address} </p> </div> </div> <div> <label className="text-foreground mb-1 block font-semibold">Target Available Route / Driver:</label> <div className="space-y-2"> {allRoutes .filter((r) => r.id !== activeModal?.route.id) .map((target) => ( <div key={target.id} className={cn( 'flex cursor-pointer items-center justify-between rounded-lg border p-2.5 transition-colors', activeModal.targetRouteId === target.id ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted/30', )} onClick={() => onSelectTargetRoute(target.id)} > <div className="space-y-0.5"> <div className="flex items-center gap-1.5"> <span className="text-foreground font-semibold"> {target.routeCode} · {target.routeName} </span> <span className="text-muted-foreground text-xs">({target.driver.name})</span> </div> <p className="text-muted-foreground text-xs"> Current Load: {target.currentPayloadLbs} lbs ( {Math.round((target.currentPayloadLbs / target.maxPayloadLbs) * 100)}%) ·{' '} {target.totalStops - target.completedStops} stops left </p> </div> <div className={cn( 'border-border flex h-5 w-5 items-center justify-center rounded-full border', activeModal.targetRouteId === target.id && 'border-primary bg-primary text-primary-foreground', )} > {activeModal.targetRouteId === target.id && <Check className="h-3 w-3 stroke-3" />} </div> </div> ))} </div> </div> </div> <div className="flex justify-end gap-2 pt-2"> <Button aria-label="Close modal" variant="outline" size="sm" className="h-8 text-xs" onClick={onClose}> Cancel </Button> <Button size="sm" className="h-8 text-xs font-medium" onClick={onConfirmReassign}> <Check className="mr-1.5 h-3.5 w-3.5" /> Confirm Stop Reassignment </Button> </div> </div> )} </div> ) }components/blocks/dispatch-assignment-board-types.ts1.1 kB
export type StopStatus = 'delivered' | 'in_progress' | 'pending' export interface RouteStop { id: string sequence: number locationName: string address: string status: StopStatus deliveredTime?: string estimatedTime?: string packagesCount: number packageType: string notes?: string recipient?: string } export interface DriverRoute { id: string routeCode: string routeName: string zone: string driver: { name: string avatar?: string initials: string phone: string status: 'on_route' | 'break' | 'loading' radioChannel: string } vehicle: { name: string type: string plateNumber: string powerLevel: string powerType: 'electric' | 'diesel' } completedStops: number totalStops: number currentPayloadLbs: number maxPayloadLbs: number shiftWindow: string estimatedCompletion: string telemetry: { speed: string heading: string currentLocation: string lastPing: string signal: string } stops: RouteStop[] } export interface ActiveModalState { type: 'gps' | 'call' | 'reassign' route: DriverRoute targetRouteId?: string }components/blocks/dispatch-data.ts8.4 kB
import type { DriverRoute } from './dispatch-assignment-board-types' export const DRIVER_ROUTES_DATA: DriverRoute[] = [ { id: 'route-01', routeCode: 'Route #01', routeName: 'Downtown Metro', zone: 'Metro Los Angeles - North Zone', driver: { name: 'Elena Rostova', initials: 'ER', phone: '+1 (213) 555-0182', status: 'on_route', radioChannel: 'Ch 4 · North Dispatch', }, vehicle: { name: 'Ford Transit #204', type: 'EV Cargo Van', plateNumber: 'CA-8KDX92', powerLevel: '74% Battery', powerType: 'electric', }, completedStops: 18, totalStops: 24, currentPayloadLbs: 1850, maxPayloadLbs: 2200, shiftWindow: '07:30 AM – 16:30 PM', estimatedCompletion: '15:45 PM', telemetry: { speed: '32 mph', heading: 'NW 315°', currentLocation: 'Grand Ave & 5th St, Los Angeles, CA 90071', lastPing: '12s ago', signal: '5G Active', }, stops: [ { id: 'r1-s1', sequence: 1, locationName: 'Acme Industrial Hub', address: '1240 S Santa Fe Ave', status: 'delivered', deliveredTime: '09:15 AM', packagesCount: 12, packageType: 'Cartons', recipient: 'Signed by J. Miller', }, { id: 'r1-s2', sequence: 2, locationName: 'Apex Tech Campus', address: '800 Wilshire Blvd, Ste 400', status: 'delivered', deliveredTime: '10:45 AM', packagesCount: 8, packageType: 'Parcels', recipient: 'Dock 4B Security', }, { id: 'r1-s3', sequence: 3, locationName: 'Metro Distribution Center', address: '1950 E 7th St', status: 'in_progress', estimatedTime: '14:15 PM', packagesCount: 15, packageType: 'Heavy Boxes', notes: 'Priority Rush · Forklift offload requested', }, { id: 'r1-s4', sequence: 4, locationName: 'Pasadena Hospital Supply', address: '650 S Raymond Ave', status: 'pending', packagesCount: 2, packageType: 'Medical Coolers', notes: 'Cold-Chain · Temperature Log Required', }, ], }, { id: 'route-02', routeCode: 'Route #02', routeName: 'Westside & Santa Monica', zone: 'Metro Los Angeles - North Zone', driver: { name: 'Marcus Vance', initials: 'MV', phone: '+1 (310) 555-0194', status: 'on_route', radioChannel: 'Ch 2 · West Dispatch', }, vehicle: { name: 'Mercedes Sprinter #118', type: 'High-Roof Van', plateNumber: 'CA-9TMR44', powerLevel: '68% Fuel', powerType: 'diesel', }, completedStops: 22, totalStops: 26, currentPayloadLbs: 2450, maxPayloadLbs: 3000, shiftWindow: '08:00 AM – 17:00 PM', estimatedCompletion: '16:15 PM', telemetry: { speed: '28 mph', heading: 'W 270°', currentLocation: 'Ocean Ave & Colorado Ave, Santa Monica, CA', lastPing: '8s ago', signal: '5G Active', }, stops: [ { id: 'r2-s1', sequence: 1, locationName: 'Pacific Design Center', address: '8687 Melrose Ave', status: 'delivered', deliveredTime: '09:40 AM', packagesCount: 6, packageType: 'Crates', recipient: 'Direct Receipt · Bay 2', }, { id: 'r2-s2', sequence: 2, locationName: 'Oceanview Medical Plaza', address: '1301 20th St, Floor 3', status: 'delivered', deliveredTime: '11:20 AM', packagesCount: 14, packageType: 'Packages', recipient: 'Receptionist Amy K.', }, { id: 'r2-s3', sequence: 3, locationName: 'Santa Monica Pier Logistics', address: '200 Santa Monica Pier', status: 'in_progress', estimatedTime: '14:40 PM', packagesCount: 4, packageType: 'Cartons', notes: 'Dock Bay 2 · Access Gate Code #4491', }, { id: 'r2-s4', sequence: 4, locationName: 'Silicon Beach Incubator', address: '12020 Ocean Park Blvd', status: 'pending', packagesCount: 5, packageType: 'Parcels', notes: 'Signature Required · 5th Floor Suite 500', }, ], }, { id: 'route-03', routeCode: 'Route #03', routeName: 'Pasadena & Glendale', zone: 'San Fernando Valley Hub', driver: { name: 'Sofia Chen', initials: 'SC', phone: '+1 (626) 555-0147', status: 'on_route', radioChannel: 'Ch 5 · Valley Dispatch', }, vehicle: { name: 'Ram ProMaster #309', type: 'Electric Courier', plateNumber: 'CA-7KPL55', powerLevel: '82% Battery', powerType: 'electric', }, completedStops: 14, totalStops: 20, currentPayloadLbs: 1620, maxPayloadLbs: 2100, shiftWindow: '08:15 AM – 16:45 PM', estimatedCompletion: '16:30 PM', telemetry: { speed: '38 mph', heading: 'E 090°', currentLocation: 'Colorado Blvd & Lake Ave, Pasadena, CA', lastPing: '18s ago', signal: '5G Active', }, stops: [ { id: 'r3-s1', sequence: 1, locationName: 'Glendale Aerospace Labs', address: '4300 San Fernando Rd', status: 'delivered', deliveredTime: '10:05 AM', packagesCount: 3, packageType: 'Heavy Units', recipient: 'Forklift Bay Delivery', }, { id: 'r3-s2', sequence: 2, locationName: 'Foothill Commercial Park', address: '2210 Foothill Blvd', status: 'delivered', deliveredTime: '11:50 AM', packagesCount: 9, packageType: 'Boxes', recipient: 'Suite 200 Mailroom', }, { id: 'r3-s3', sequence: 3, locationName: 'Huntington Research Institute', address: '1151 Oxford Rd', status: 'in_progress', estimatedTime: '14:25 PM', packagesCount: 6, packageType: 'Lab Samples', notes: 'Fragile · Bio-Sample Insulated Crate', }, { id: 'r3-s4', sequence: 4, locationName: 'Old Town Commerce Plaza', address: '300 E Colorado Blvd', status: 'pending', packagesCount: 4, packageType: 'Packages', notes: 'Front Desk Drop · Notify Concierge', }, ], }, { id: 'route-04', routeCode: 'Route #04', routeName: 'South Bay Logistics', zone: 'Metro Los Angeles - South Zone', driver: { name: 'Tariq Mansour', initials: 'TM', phone: '+1 (424) 555-0133', status: 'on_route', radioChannel: 'Ch 3 · South Dispatch', }, vehicle: { name: 'Freightliner Custom #412', type: 'Box Truck', plateNumber: 'CA-5WXZ91', powerLevel: '88% Fuel', powerType: 'diesel', }, completedStops: 19, totalStops: 22, currentPayloadLbs: 3100, maxPayloadLbs: 3800, shiftWindow: '07:00 AM – 16:00 PM', estimatedCompletion: '15:20 PM', telemetry: { speed: '45 mph', heading: 'S 180°', currentLocation: 'I-405 Southbound at Carson St, CA', lastPing: '5s ago', signal: '5G Active', }, stops: [ { id: 'r4-s1', sequence: 1, locationName: 'Port of LA Cargo Terminal', address: '425 S Palos Verdes St', status: 'delivered', deliveredTime: '08:30 AM', packagesCount: 18, packageType: 'Pallets', recipient: 'Customs Manifest #882', }, { id: 'r4-s2', sequence: 2, locationName: 'Torrance Industrial Park', address: '190th St & Western Ave', status: 'delivered', deliveredTime: '10:15 AM', packagesCount: 11, packageType: 'Crates', recipient: 'Security Gate 5', }, { id: 'r4-s3', sequence: 3, locationName: 'Long Beach Tech Hub', address: '100 W Ocean Blvd', status: 'in_progress', estimatedTime: '14:50 PM', packagesCount: 8, packageType: 'Hardware Cases', notes: 'Server Equipment · Handle with Care', }, { id: 'r4-s4', sequence: 4, locationName: 'Manhattan Beach Retail Depot', address: '3200 Sepulveda Blvd', status: 'pending', packagesCount: 6, packageType: 'Store Parcels', notes: 'Store Receiving · Rear Loading Dock', }, ], }, ]
Raw manifest:https://uipkge.dev/r/react/dispatch-assignment-board.json