@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.
- package/dist/components/CustomerPicker.js +94 -101
- package/dist/components/EntityCombobox.js +105 -0
- package/dist/components/ui.js +2 -2
- package/dist/hooks/useFleet.js +53 -49
- package/dist/index.js +22 -22
- package/dist/movethewheels/components/CustomerPicker.d.ts +3 -2
- package/dist/movethewheels/components/EntityCombobox.d.ts +40 -0
- package/dist/movethewheels/components/ui.d.ts +1 -1
- package/dist/movethewheels/hooks/useFleet.d.ts +4 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/chevrons-up-down.js +11 -0
- package/dist/pages/CreateOrderPage.js +175 -169
- package/dist/pages/OrderDetailsPage.js +291 -294
- package/dist/pages/RouteDetailsPage.js +298 -296
- package/package.json +1 -1
- package/src/movethewheels/components/CustomerPicker.tsx +26 -36
- package/src/movethewheels/components/EntityCombobox.tsx +237 -0
- package/src/movethewheels/components/ui.tsx +9 -0
- package/src/movethewheels/hooks/useFleet.ts +10 -4
- package/src/movethewheels/pages/CreateOrderPage.tsx +39 -19
- package/src/movethewheels/pages/OrderDetailsPage.tsx +61 -60
- package/src/movethewheels/pages/RouteDetailsPage.tsx +53 -53
|
@@ -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 {
|
|
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 } =
|
|
104
|
+
const { vehicles, isLoading: vehiclesLoading } = useVehicles({
|
|
109
105
|
limit: 20,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
driverId: selectedDriver || undefined,
|
|
113
|
-
},
|
|
106
|
+
search: debouncedFleetSearch || undefined,
|
|
107
|
+
driverId: selectedDriver || undefined,
|
|
114
108
|
});
|
|
115
|
-
const { drivers, isLoading: driversLoading } =
|
|
109
|
+
const { drivers, isLoading: driversLoading } = useDrivers({
|
|
116
110
|
limit: 20,
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
<
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
<
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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 {
|
|
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
|
|
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 } =
|
|
84
|
+
const { vehicles, isLoading: vehiclesLoading } = useVehicles({
|
|
92
85
|
limit: 20,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
driverId: form.driverId || undefined,
|
|
96
|
-
},
|
|
86
|
+
search: debouncedFleetSearch || undefined,
|
|
87
|
+
driverId: form.driverId || undefined,
|
|
97
88
|
});
|
|
98
|
-
const { drivers } =
|
|
89
|
+
const { drivers, isLoading: driversLoading } = useDrivers({
|
|
99
90
|
limit: 20,
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
108
|
-
const { vehicle: selectedVehicle } = useVehicle(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
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
|
-
<
|
|
290
|
+
<EntityCombobox
|
|
291
|
+
id="vehicle"
|
|
300
292
|
value={form.vehicleId}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
<
|
|
313
|
+
<EntityCombobox
|
|
314
|
+
id="driver"
|
|
318
315
|
value={form.driverId}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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>
|