@burdenoff/microfe-movethewheels 2026.923.1 → 2026.924.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,11 +36,6 @@ import {
36
36
  Separator,
37
37
  Input,
38
38
  Label,
39
- Select,
40
- SelectContent,
41
- SelectItem,
42
- SelectTrigger,
43
- SelectValue,
44
39
  Table,
45
40
  TableBody,
46
41
  TableCell,
@@ -48,9 +43,10 @@ import {
48
43
  TableHeader,
49
44
  TableRow,
50
45
  } from '../components/ui';
46
+ import { EntityCombobox } from '../components/EntityCombobox';
51
47
  import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
52
48
  import { useOrder, useOrders } from '../hooks/useOrders';
53
- import { useFleet } from '../hooks/useFleet';
49
+ import { useVehicles, useVehicle, useDrivers, useDriver } from '../hooks/useFleet';
54
50
  import { useDebouncedValue } from '../hooks/useDebouncedValue';
55
51
  import { FormError } from '../components/FormError';
56
52
  import { Notice, useNotice } from '../components/Notice';
@@ -105,20 +101,25 @@ export default function OrderDetailsPage() {
105
101
  const [advancing, setAdvancing] = useState(false);
106
102
  const [actionError, setActionError] = useState<string | null>(null);
107
103
  const debouncedFleetSearch = useDebouncedValue(fleetSearch);
108
- const { vehicles, isLoading: vehiclesLoading } = useFleet({
104
+ const { vehicles, isLoading: vehiclesLoading } = useVehicles({
109
105
  limit: 20,
110
- filters: {
111
- search: debouncedFleetSearch || undefined,
112
- driverId: selectedDriver || undefined,
113
- },
106
+ search: debouncedFleetSearch || undefined,
107
+ driverId: selectedDriver || undefined,
114
108
  });
115
- const { drivers, isLoading: driversLoading } = useFleet({
109
+ const { drivers, isLoading: driversLoading } = useDrivers({
116
110
  limit: 20,
117
- filters: {
118
- search: debouncedFleetSearch || undefined,
119
- vehicleId: selectedVehicle || undefined,
120
- },
111
+ search: debouncedFleetSearch || undefined,
112
+ vehicleId: selectedVehicle || undefined,
121
113
  });
114
+ // Resolved independently of the (searched, 20-row) lists above so the
115
+ // trigger still shows the right label once a pick scrolls out of the
116
+ // current search results.
117
+ const { vehicle: selectedVehicleRecord, isLoading: selectedVehicleLoading } = useVehicle(
118
+ selectedVehicle || undefined
119
+ );
120
+ const { driver: selectedDriverRecord, isLoading: selectedDriverLoading } = useDriver(
121
+ selectedDriver || undefined
122
+ );
122
123
  const { message: notice, notify } = useNotice();
123
124
  const availableVehicles = vehicles.filter(
124
125
  (vehicle) => vehicle.status === 'available' || vehicle.id === order?.vehicleId
@@ -564,54 +565,54 @@ export default function OrderDetailsPage() {
564
565
  </div>
565
566
  <div className="space-y-1.5">
566
567
  <Label htmlFor="assign-vehicle">Vehicle</Label>
567
- <Select value={selectedVehicle} onValueChange={setSelectedVehicle}>
568
- <SelectTrigger id="assign-vehicle">
569
- <SelectValue
570
- placeholder={
571
- order.vehicleId ? `Current: ${order.vehicleId}` : 'Select a vehicle'
572
- }
573
- />
574
- </SelectTrigger>
575
- <SelectContent>
576
- {availableVehicles.length === 0 ? (
577
- <SelectItem value="none" disabled>
578
- No available vehicles
579
- </SelectItem>
580
- ) : (
581
- availableVehicles.map((vehicle) => (
582
- <SelectItem key={vehicle.id} value={vehicle.id}>
583
- {vehicle.vehicleNumber} ({vehicle.type})
584
- </SelectItem>
585
- ))
586
- )}
587
- </SelectContent>
588
- </Select>
568
+ <EntityCombobox
569
+ id="assign-vehicle"
570
+ value={selectedVehicle}
571
+ onChange={setSelectedVehicle}
572
+ options={availableVehicles}
573
+ loading={vehiclesLoading}
574
+ getId={(vehicle) => vehicle.id}
575
+ getLabel={(vehicle) => `${vehicle.vehicleNumber} (${vehicle.type})`}
576
+ renderOption={(vehicle) => (
577
+ <>
578
+ {vehicle.vehicleNumber} ({vehicle.type})
579
+ </>
580
+ )}
581
+ selectedOption={selectedVehicleRecord}
582
+ selectedLoading={selectedVehicleLoading}
583
+ required
584
+ placeholder={
585
+ order.vehicleId ? `Current: ${order.vehicleId}` : 'Select a vehicle'
586
+ }
587
+ emptyText="No available vehicles."
588
+ // Search is driven by the shared "Search fleet" box above —
589
+ // no second input inside the popover.
590
+ />
589
591
  </div>
590
592
 
591
593
  <div className="space-y-1.5">
592
594
  <Label htmlFor="assign-driver">Driver</Label>
593
- <Select value={selectedDriver} onValueChange={setSelectedDriver}>
594
- <SelectTrigger id="assign-driver">
595
- <SelectValue
596
- placeholder={
597
- order.driverId ? `Current: ${order.driverId}` : 'Select a driver'
598
- }
599
- />
600
- </SelectTrigger>
601
- <SelectContent>
602
- {availableDrivers.length === 0 ? (
603
- <SelectItem value="none" disabled>
604
- No available drivers
605
- </SelectItem>
606
- ) : (
607
- availableDrivers.map((driver) => (
608
- <SelectItem key={driver.id} value={driver.id}>
609
- {driver.fullName} ({driver.employeeId})
610
- </SelectItem>
611
- ))
612
- )}
613
- </SelectContent>
614
- </Select>
595
+ <EntityCombobox
596
+ id="assign-driver"
597
+ value={selectedDriver}
598
+ onChange={setSelectedDriver}
599
+ options={availableDrivers}
600
+ loading={driversLoading}
601
+ getId={(driver) => driver.id}
602
+ getLabel={(driver) => `${driver.fullName} (${driver.employeeId})`}
603
+ renderOption={(driver) => (
604
+ <>
605
+ {driver.fullName} ({driver.employeeId})
606
+ </>
607
+ )}
608
+ selectedOption={selectedDriverRecord}
609
+ selectedLoading={selectedDriverLoading}
610
+ required
611
+ placeholder={
612
+ order.driverId ? `Current: ${order.driverId}` : 'Select a driver'
613
+ }
614
+ emptyText="No available drivers."
615
+ />
615
616
  </div>
616
617
 
617
618
  <Button
@@ -39,8 +39,9 @@ import {
39
39
  import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
40
40
  import { RouteMap, type RouteStop as MapRouteStop } from '../components/maps';
41
41
  import { firstRealLatLng } from '../utils/mapMarkers';
42
+ import { EntityCombobox } from '../components/EntityCombobox';
42
43
  import { useRoute, useRoutes } from '../hooks/useRoutes';
43
- import { useFleet, useVehicle, useDriver } from '../hooks/useFleet';
44
+ import { useVehicles, useVehicle, useDrivers, useDriver } from '../hooks/useFleet';
44
45
  import { useAssignmentSuggestions } from '../hooks/useFleetIntelligence';
45
46
  import { AssignmentSuggestions } from '../components/AssignmentSuggestions';
46
47
  import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';
@@ -51,15 +52,7 @@ import { joinPath } from '../utils/navigation';
51
52
  import { ROUTE_STATUS_COLORS } from '../constants';
52
53
  import { FormError } from '../components/FormError';
53
54
  import { Notice, useNotice } from '../components/Notice';
54
- import type { Route as RouteType, RouteStop, Vehicle, Driver } from '../types';
55
-
56
- /** Keeps a currently selected fleet item visible even when it falls outside the current search page. */
57
- function mergeSelected<T extends { id: string }>(items: T[], selected: T | null | undefined): T[] {
58
- if (!selected || items.some((item) => item.id === selected.id)) {
59
- return items;
60
- }
61
- return [selected, ...items];
62
- }
55
+ import type { Route as RouteType, RouteStop } from '../types';
63
56
 
64
57
  const ROUTE_STATUS_OPTIONS: { value: RouteType['status']; label: string }[] = [
65
58
  { value: 'planned', label: 'Planned' },
@@ -88,27 +81,25 @@ function NewRouteForm({ editId }: { editId?: string }) {
88
81
  driverId: '',
89
82
  stops: [{ address: '', type: 'delivery' }],
90
83
  });
91
- const { vehicles } = useFleet({
84
+ const { vehicles, isLoading: vehiclesLoading } = useVehicles({
92
85
  limit: 20,
93
- filters: {
94
- search: debouncedFleetSearch || undefined,
95
- driverId: form.driverId || undefined,
96
- },
86
+ search: debouncedFleetSearch || undefined,
87
+ driverId: form.driverId || undefined,
97
88
  });
98
- const { drivers } = useFleet({
89
+ const { drivers, isLoading: driversLoading } = useDrivers({
99
90
  limit: 20,
100
- filters: {
101
- search: debouncedFleetSearch || undefined,
102
- vehicleId: form.vehicleId || undefined,
103
- },
91
+ search: debouncedFleetSearch || undefined,
92
+ vehicleId: form.vehicleId || undefined,
104
93
  });
105
94
  // The selected vehicle/driver may fall outside the current search page (e.g. when
106
95
  // editing a route or after the assignee no longer matches the search term); fetch
107
- // them directly so the Select can still render the current selection.
108
- const { vehicle: selectedVehicle } = useVehicle(form.vehicleId || undefined);
109
- const { driver: selectedDriver } = useDriver(form.driverId || undefined);
110
- const vehicleOptions: Vehicle[] = mergeSelected(vehicles, selectedVehicle);
111
- const driverOptions: Driver[] = mergeSelected(drivers, selectedDriver);
96
+ // them directly so the combobox can still render the current selection's label.
97
+ const { vehicle: selectedVehicle, isLoading: selectedVehicleLoading } = useVehicle(
98
+ form.vehicleId || undefined
99
+ );
100
+ const { driver: selectedDriver, isLoading: selectedDriverLoading } = useDriver(
101
+ form.driverId || undefined
102
+ );
112
103
  const [submitting, setSubmitting] = useState(false);
113
104
  const [error, setError] = useState<string | null>(null);
114
105
 
@@ -296,39 +287,48 @@ function NewRouteForm({ editId }: { editId?: string }) {
296
287
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
297
288
  <div className="space-y-2">
298
289
  <Label htmlFor="vehicle">Vehicle (optional)</Label>
299
- <Select
290
+ <EntityCombobox
291
+ id="vehicle"
300
292
  value={form.vehicleId}
301
- onValueChange={(value) => setForm((prev) => ({ ...prev, vehicleId: value }))}
302
- >
303
- <SelectTrigger id="vehicle">
304
- <SelectValue placeholder="Assign later" />
305
- </SelectTrigger>
306
- <SelectContent>
307
- {vehicleOptions.map((vehicle) => (
308
- <SelectItem key={vehicle.id} value={vehicle.id}>
309
- {vehicle.vehicleNumber} ({vehicle.type})
310
- </SelectItem>
311
- ))}
312
- </SelectContent>
313
- </Select>
293
+ onChange={(value) => setForm((prev) => ({ ...prev, vehicleId: value }))}
294
+ options={vehicles}
295
+ loading={vehiclesLoading}
296
+ getId={(vehicle) => vehicle.id}
297
+ getLabel={(vehicle) => `${vehicle.vehicleNumber} (${vehicle.type})`}
298
+ renderOption={(vehicle) => (
299
+ <>
300
+ {vehicle.vehicleNumber} ({vehicle.type})
301
+ </>
302
+ )}
303
+ selectedOption={selectedVehicle}
304
+ selectedLoading={selectedVehicleLoading}
305
+ required
306
+ placeholder="Assign later"
307
+ emptyText="No available vehicles."
308
+ // Search is driven by the shared "Search fleet" box above.
309
+ />
314
310
  </div>
315
311
  <div className="space-y-2">
316
312
  <Label htmlFor="driver">Driver (optional)</Label>
317
- <Select
313
+ <EntityCombobox
314
+ id="driver"
318
315
  value={form.driverId}
319
- onValueChange={(value) => setForm((prev) => ({ ...prev, driverId: value }))}
320
- >
321
- <SelectTrigger id="driver">
322
- <SelectValue placeholder="Assign later" />
323
- </SelectTrigger>
324
- <SelectContent>
325
- {driverOptions.map((driver) => (
326
- <SelectItem key={driver.id} value={driver.id}>
327
- {driver.fullName} ({driver.employeeId})
328
- </SelectItem>
329
- ))}
330
- </SelectContent>
331
- </Select>
316
+ onChange={(value) => setForm((prev) => ({ ...prev, driverId: value }))}
317
+ options={drivers}
318
+ loading={driversLoading}
319
+ getId={(driver) => driver.id}
320
+ getLabel={(driver) => `${driver.fullName} (${driver.employeeId})`}
321
+ renderOption={(driver) => (
322
+ <>
323
+ {driver.fullName} ({driver.employeeId})
324
+ </>
325
+ )}
326
+ selectedOption={selectedDriver}
327
+ selectedLoading={selectedDriverLoading}
328
+ required
329
+ placeholder="Assign later"
330
+ emptyText="No available drivers."
331
+ />
332
332
  </div>
333
333
  </div>
334
334
  </div>