@burdenoff/microfe-movethewheels 2026.921.1 → 2026.923.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.
@@ -533,7 +533,13 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
533
533
  // refetched) every render when the caller passes an inline filters object.
534
534
  const filters = useMemo(
535
535
  () => rawFilters,
536
- [rawFilters?.driverId, rawFilters?.search, rawFilters?.status, rawFilters?.vehicleType]
536
+ [
537
+ rawFilters?.driverId,
538
+ rawFilters?.search,
539
+ rawFilters?.status,
540
+ rawFilters?.vehicleId,
541
+ rawFilters?.vehicleType,
542
+ ]
537
543
  );
538
544
  const graphQLOptions = useGraphqlOptions();
539
545
  const [vehicles, setVehicles] = useState<Vehicle[]>([]);
@@ -551,13 +557,14 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
551
557
  search: filters?.search,
552
558
  type: filters?.vehicleType?.toUpperCase(),
553
559
  status: filters?.status ? toApiVehicleStatus(filters.status) : undefined,
560
+ driverId: filters?.driverId,
554
561
  },
555
562
  pagination: { limit, offset: 0 },
556
563
  }),
557
564
  executeGraphQL<{ drivers: ApiDriver[] }>(graphQLOptions, LIST_DRIVERS_QUERY, {
558
565
  filter: {
559
566
  search: filters?.search,
560
- vehicleId: filters?.driverId,
567
+ vehicleId: filters?.vehicleId,
561
568
  },
562
569
  pagination: { limit, offset: 0 },
563
570
  }),
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
3
3
  import type { Order, OrderFilter, OrderStatus } from '../types';
4
+ import { mapRoute } from './useRoutes';
4
5
  import { executeGraphQL } from '../utils/graphqlClient';
5
6
 
6
7
  interface UseOrdersOptions {
@@ -53,6 +54,21 @@ export interface ApiOrder {
53
54
  scheduledDelivery?: string | null;
54
55
  vehicleId?: string | null;
55
56
  driverId?: string | null;
57
+ routeId?: string | null;
58
+ route?: {
59
+ id: string;
60
+ name: string;
61
+ status: string;
62
+ vehicleId?: string | null;
63
+ driverId?: string | null;
64
+ waypoints?: unknown;
65
+ estimatedDistance?: number | null;
66
+ estimatedDuration?: number | null;
67
+ fuelCost?: number | null;
68
+ actualDistance?: number | null;
69
+ actualDuration?: number | null;
70
+ createdAt: string;
71
+ } | null;
56
72
  createdAt: string;
57
73
  updatedAt: string;
58
74
  customer?: {
@@ -85,6 +101,21 @@ const DELIVERY_JOB_FIELDS = `
85
101
  scheduledDelivery
86
102
  vehicleId
87
103
  driverId
104
+ routeId
105
+ route {
106
+ id
107
+ name
108
+ status
109
+ vehicleId
110
+ driverId
111
+ waypoints
112
+ estimatedDistance
113
+ estimatedDuration
114
+ fuelCost
115
+ actualDistance
116
+ actualDuration
117
+ createdAt
118
+ }
88
119
  createdAt
89
120
  updatedAt
90
121
  customer {
@@ -302,6 +333,8 @@ export function mapOrder(order: ApiOrder): Order {
302
333
  scheduledDelivery: order.scheduledDelivery ? new Date(order.scheduledDelivery) : undefined,
303
334
  vehicleId: order.vehicleId ?? undefined,
304
335
  driverId: order.driverId ?? undefined,
336
+ routeId: order.routeId ?? undefined,
337
+ route: order.route ? mapRoute(order.route) : undefined,
305
338
  createdAt: new Date(order.createdAt),
306
339
  updatedAt: new Date(order.updatedAt),
307
340
  };
@@ -409,6 +442,7 @@ export function useOrders(options: UseOrdersOptions = {}): UseOrdersResult {
409
442
  scheduledDelivery: orderData.scheduledDelivery?.toISOString(),
410
443
  vehicleId: orderData.vehicleId,
411
444
  driverId: orderData.driverId,
445
+ routeId: orderData.routeId,
412
446
  },
413
447
  });
414
448
 
@@ -440,6 +474,7 @@ export function useOrders(options: UseOrdersOptions = {}): UseOrdersResult {
440
474
  updates.scheduledDelivery === null ? null : updates.scheduledDelivery?.toISOString(),
441
475
  vehicleId: updates.vehicleId,
442
476
  driverId: updates.driverId,
477
+ routeId: updates.routeId,
443
478
  },
444
479
  });
445
480
 
@@ -218,7 +218,7 @@ function mapWaypoints(waypoints: unknown): RouteStop[] {
218
218
  });
219
219
  }
220
220
 
221
- function mapRoute(route: ApiRoute): Route {
221
+ export function mapRoute(route: ApiRoute): Route {
222
222
  return {
223
223
  id: route.id,
224
224
  name: route.name,
@@ -6,7 +6,17 @@
6
6
 
7
7
  import { useEffect, useState } from 'react';
8
8
  import { useParams, useSearchParams } from 'react-router';
9
- import { Package, ArrowLeft, Save, Plus, Trash2, Users, MapPin, CalendarClock } from 'lucide-react';
9
+ import {
10
+ Package,
11
+ ArrowLeft,
12
+ Save,
13
+ Plus,
14
+ Trash2,
15
+ Users,
16
+ MapPin,
17
+ CalendarClock,
18
+ Route as RouteIcon,
19
+ } from 'lucide-react';
10
20
  import { PageLayout, EmptyState } from '../components/PageLayout';
11
21
  import { FormError } from '../components/FormError';
12
22
  import {
@@ -28,6 +38,7 @@ import {
28
38
  import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
29
39
  import { useOrder, useOrders } from '../hooks/useOrders';
30
40
  import { useCustomer, useCustomers } from '../hooks/useCustomers';
41
+ import { useRoutes } from '../hooks/useRoutes';
31
42
  import { useDebouncedValue } from '../hooks/useDebouncedValue';
32
43
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
33
44
  import { joinPath, buildCustomerCreatePath } from '../utils/navigation';
@@ -70,6 +81,14 @@ export default function CreateOrderPage() {
70
81
  filters: { search: debouncedCustomerSearch || undefined },
71
82
  });
72
83
  const { customer: linkedCustomer } = useCustomer(prefilledCustomerId ?? order?.customerId);
84
+ const {
85
+ routes,
86
+ isLoading: routesLoading,
87
+ error: routesError,
88
+ } = useRoutes({
89
+ status: 'planned',
90
+ limit: 20,
91
+ });
73
92
 
74
93
  const isEditing = !!orderId && !!order;
75
94
 
@@ -77,6 +96,7 @@ export default function CreateOrderPage() {
77
96
  const [formData, setFormData] = useState({
78
97
  customerId: prefilledCustomerId || order?.customerId || '',
79
98
  priority: order?.priority || 'normal',
99
+ routeId: order?.routeId || '',
80
100
  pickupAddress: {
81
101
  street: order?.pickupAddress.street || '',
82
102
  city: order?.pickupAddress.city || '',
@@ -121,6 +141,7 @@ export default function CreateOrderPage() {
121
141
  setFormData({
122
142
  customerId: order.customerId || '',
123
143
  priority: order.priority || 'normal',
144
+ routeId: order.routeId || '',
124
145
  pickupAddress: {
125
146
  street: order.pickupAddress.street || '',
126
147
  city: order.pickupAddress.city || '',
@@ -212,6 +233,7 @@ export default function CreateOrderPage() {
212
233
  items: formData.items,
213
234
  // `null` clears an existing slot on edit; the service field is nullable.
214
235
  scheduledDelivery: formData.scheduledDelivery ? new Date(formData.scheduledDelivery) : null,
236
+ routeId: formData.routeId || undefined,
215
237
  specialInstructions: formData.specialInstructions,
216
238
  };
217
239
 
@@ -252,6 +274,35 @@ export default function CreateOrderPage() {
252
274
  }));
253
275
  };
254
276
 
277
+ const selectSavedRoute = (value: string) => {
278
+ if (value === 'none') {
279
+ updateField('routeId', '');
280
+ return;
281
+ }
282
+ const route = routes.find((candidate) => candidate.id === value);
283
+ if (!route) return;
284
+
285
+ const pickup = route.stops.find((stop) => stop.type === 'pickup')?.address;
286
+ const delivery = [...route.stops].reverse().find((stop) => stop.type === 'delivery')?.address;
287
+ const mergeAddress = (current: typeof formData.pickupAddress, saved: typeof pickup) =>
288
+ saved
289
+ ? {
290
+ street: current.street.trim() ? current.street : saved.street,
291
+ city: current.city.trim() ? current.city : saved.city,
292
+ state: current.state.trim() ? current.state : saved.state,
293
+ postalCode: current.postalCode.trim() ? current.postalCode : saved.postalCode,
294
+ country: current.country.trim() ? current.country : saved.country,
295
+ }
296
+ : current;
297
+
298
+ setFormData((previous) => ({
299
+ ...previous,
300
+ routeId: route.id,
301
+ pickupAddress: mergeAddress(previous.pickupAddress, pickup),
302
+ deliveryAddress: mergeAddress(previous.deliveryAddress, delivery),
303
+ }));
304
+ };
305
+
255
306
  const addItem = () => {
256
307
  setFormData((prev) => ({
257
308
  ...prev,
@@ -404,6 +455,38 @@ export default function CreateOrderPage() {
404
455
  </div>
405
456
  </EmphasisPanel>
406
457
 
458
+ {/* Saved route — optional shortcut; manual addresses remain authoritative. */}
459
+ <Card>
460
+ <CardHeader>
461
+ <CardTitle className="flex items-center gap-2">
462
+ <RouteIcon size={18} />
463
+ Saved Route (Optional)
464
+ </CardTitle>
465
+ </CardHeader>
466
+ <CardContent className="space-y-2">
467
+ <Select value={formData.routeId || 'none'} onValueChange={selectSavedRoute}>
468
+ <SelectTrigger id="saved-route">
469
+ <SelectValue placeholder="Choose a saved route" />
470
+ </SelectTrigger>
471
+ <SelectContent>
472
+ <SelectItem value="none">No saved route</SelectItem>
473
+ {routes.map((route) => (
474
+ <SelectItem key={route.id} value={route.id}>
475
+ {route.name} · {route.stops.length} stops
476
+ </SelectItem>
477
+ ))}
478
+ </SelectContent>
479
+ </Select>
480
+ <p className="text-xs text-text-secondary">
481
+ {routesLoading
482
+ ? 'Loading planned routes…'
483
+ : routesError
484
+ ? 'Saved routes are unavailable; enter pickup and delivery addresses manually.'
485
+ : 'A saved route fills any blank address fields. You can always edit both addresses below.'}
486
+ </p>
487
+ </CardContent>
488
+ </Card>
489
+
407
490
  {/* Addresses */}
408
491
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
409
492
  {/* Pickup Address */}
@@ -4,7 +4,7 @@
4
4
  * Displays detailed information about a specific order.
5
5
  */
6
6
 
7
- import { useState } from 'react';
7
+ import { useEffect, useState } from 'react';
8
8
  import { useParams } from 'react-router';
9
9
  import {
10
10
  Package,
@@ -99,19 +99,27 @@ export default function OrderDetailsPage() {
99
99
  const { order, isLoading, error, refetch } = useOrder(orderId);
100
100
  const { updateOrder, updateOrderStatus } = useOrders();
101
101
  const [fleetSearch, setFleetSearch] = useState('');
102
- const debouncedFleetSearch = useDebouncedValue(fleetSearch);
103
- const { vehicles, drivers } = useFleet({
104
- limit: 20,
105
- filters: { search: debouncedFleetSearch || undefined },
106
- });
107
- const { message: notice, notify } = useNotice();
108
-
109
102
  const [selectedVehicle, setSelectedVehicle] = useState('');
110
103
  const [selectedDriver, setSelectedDriver] = useState('');
111
104
  const [assigning, setAssigning] = useState(false);
112
105
  const [advancing, setAdvancing] = useState(false);
113
106
  const [actionError, setActionError] = useState<string | null>(null);
114
-
107
+ const debouncedFleetSearch = useDebouncedValue(fleetSearch);
108
+ const { vehicles, isLoading: vehiclesLoading } = useFleet({
109
+ limit: 20,
110
+ filters: {
111
+ search: debouncedFleetSearch || undefined,
112
+ driverId: selectedDriver || undefined,
113
+ },
114
+ });
115
+ const { drivers, isLoading: driversLoading } = useFleet({
116
+ limit: 20,
117
+ filters: {
118
+ search: debouncedFleetSearch || undefined,
119
+ vehicleId: selectedVehicle || undefined,
120
+ },
121
+ });
122
+ const { message: notice, notify } = useNotice();
115
123
  const availableVehicles = vehicles.filter(
116
124
  (vehicle) => vehicle.status === 'available' || vehicle.id === order?.vehicleId
117
125
  );
@@ -119,6 +127,26 @@ export default function OrderDetailsPage() {
119
127
  (driver) => driver.status === 'available' || driver.id === order?.driverId
120
128
  );
121
129
 
130
+ useEffect(() => {
131
+ if (
132
+ !driversLoading &&
133
+ selectedDriver &&
134
+ !drivers.some((driver) => driver.id === selectedDriver)
135
+ ) {
136
+ setSelectedDriver('');
137
+ }
138
+ }, [drivers, driversLoading, selectedDriver]);
139
+
140
+ useEffect(() => {
141
+ if (
142
+ !vehiclesLoading &&
143
+ selectedVehicle &&
144
+ !vehicles.some((vehicle) => vehicle.id === selectedVehicle)
145
+ ) {
146
+ setSelectedVehicle('');
147
+ }
148
+ }, [selectedVehicle, vehicles, vehiclesLoading]);
149
+
122
150
  const handleAssign = async () => {
123
151
  if (!orderId || (!selectedVehicle && !selectedDriver)) {
124
152
  setActionError('Pick a vehicle and/or a driver to assign.');
@@ -194,7 +222,7 @@ export default function OrderDetailsPage() {
194
222
  <ArrowLeft size={16} />
195
223
  Back
196
224
  </Button>
197
- <Button variant="outline">
225
+ <Button variant="outline" onClick={() => window.print()}>
198
226
  <Printer size={16} />
199
227
  Print
200
228
  </Button>
@@ -340,6 +368,47 @@ export default function OrderDetailsPage() {
340
368
  </CardContent>
341
369
  </Card>
342
370
 
371
+ {/* Saved route */}
372
+ <Card>
373
+ <CardHeader>
374
+ <CardTitle className="flex items-center gap-2">
375
+ <Truck size={18} />
376
+ Saved Route
377
+ </CardTitle>
378
+ </CardHeader>
379
+ <CardContent>
380
+ {order.route ? (
381
+ <div className="space-y-3">
382
+ <div className="flex items-center justify-between gap-3">
383
+ <div>
384
+ <p className="font-medium">{order.route.name}</p>
385
+ <p className="text-sm text-text-secondary">
386
+ {order.route.stops.length} stops ·{' '}
387
+ {order.route.status.replace('_', ' ').toLowerCase()}
388
+ </p>
389
+ </div>
390
+ <Badge variant="outline">Reusable route</Badge>
391
+ </div>
392
+ <div className="space-y-2">
393
+ {order.route.stops.map((stop, index) => (
394
+ <div key={stop.id} className="flex items-start gap-2 text-sm">
395
+ <span className="text-text-secondary">{index + 1}.</span>
396
+ <span>
397
+ <span className="font-medium capitalize">{stop.type}</span> ·{' '}
398
+ {formatAddress(stop.address)}
399
+ </span>
400
+ </div>
401
+ ))}
402
+ </div>
403
+ </div>
404
+ ) : (
405
+ <p className="text-sm text-text-secondary">
406
+ No saved route was selected for this order.
407
+ </p>
408
+ )}
409
+ </CardContent>
410
+ </Card>
411
+
343
412
  {/* Order Items */}
344
413
  <Card>
345
414
  <CardHeader>
@@ -74,10 +74,6 @@ function NewRouteForm({ editId }: { editId?: string }) {
74
74
  const { route } = useRoute(editId);
75
75
  const [fleetSearch, setFleetSearch] = useState('');
76
76
  const debouncedFleetSearch = useDebouncedValue(fleetSearch);
77
- const { vehicles, drivers } = useFleet({
78
- limit: 20,
79
- filters: { search: debouncedFleetSearch || undefined },
80
- });
81
77
  const isEditing = !!editId;
82
78
  const [form, setForm] = useState<{
83
79
  name: string;
@@ -92,6 +88,20 @@ function NewRouteForm({ editId }: { editId?: string }) {
92
88
  driverId: '',
93
89
  stops: [{ address: '', type: 'delivery' }],
94
90
  });
91
+ const { vehicles } = useFleet({
92
+ limit: 20,
93
+ filters: {
94
+ search: debouncedFleetSearch || undefined,
95
+ driverId: form.driverId || undefined,
96
+ },
97
+ });
98
+ const { drivers } = useFleet({
99
+ limit: 20,
100
+ filters: {
101
+ search: debouncedFleetSearch || undefined,
102
+ vehicleId: form.vehicleId || undefined,
103
+ },
104
+ });
95
105
  // The selected vehicle/driver may fall outside the current search page (e.g. when
96
106
  // editing a route or after the assignee no longer matches the search term); fetch
97
107
  // them directly so the Select can still render the current selection.
@@ -280,6 +280,8 @@ export interface Order {
280
280
  actualDelivery?: Date;
281
281
  driverId?: string;
282
282
  vehicleId?: string;
283
+ routeId?: string;
284
+ route?: Route;
283
285
  trackingNumber: string;
284
286
  specialInstructions?: string;
285
287
  pricing: OrderPricing;
@@ -757,7 +759,10 @@ export interface OrderFilter {
757
759
  export interface FleetFilter {
758
760
  vehicleType?: VehicleType;
759
761
  status?: VehicleStatus;
762
+ /** Filter vehicles to those linked to this driver. */
760
763
  driverId?: string;
764
+ /** Filter drivers to those linked to this vehicle. */
765
+ vehicleId?: string;
761
766
  search?: string;
762
767
  }
763
768