@burdenoff/microfe-movethewheels 2026.921.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.
Files changed (32) hide show
  1. package/dist/components/CustomerPicker.js +94 -101
  2. package/dist/components/EntityCombobox.js +105 -0
  3. package/dist/components/ui.js +2 -2
  4. package/dist/generated/wspace-types.d.ts +1676 -3
  5. package/dist/hooks/index.js +1 -1
  6. package/dist/hooks/useFleet.js +57 -51
  7. package/dist/hooks/useOrders.js +89 -84
  8. package/dist/hooks/useRoutes.js +1 -1
  9. package/dist/index.js +27 -27
  10. package/dist/movethewheels/components/CustomerPicker.d.ts +3 -2
  11. package/dist/movethewheels/components/EntityCombobox.d.ts +40 -0
  12. package/dist/movethewheels/components/ui.d.ts +1 -1
  13. package/dist/movethewheels/hooks/useFleet.d.ts +4 -0
  14. package/dist/movethewheels/hooks/useOrders.d.ts +15 -0
  15. package/dist/movethewheels/hooks/useRoutes.d.ts +16 -0
  16. package/dist/movethewheels/types/index.d.ts +5 -0
  17. package/dist/node_modules/lucide-react/dist/esm/icons/chevrons-up-down.js +11 -0
  18. package/dist/pages/CreateOrderPage.js +196 -134
  19. package/dist/pages/OrderDetailsPage.js +328 -263
  20. package/dist/pages/RouteDetailsPage.js +277 -266
  21. package/package.json +1 -1
  22. package/src/generated/wspace-types.ts +1948 -3
  23. package/src/movethewheels/components/CustomerPicker.tsx +26 -36
  24. package/src/movethewheels/components/EntityCombobox.tsx +237 -0
  25. package/src/movethewheels/components/ui.tsx +9 -0
  26. package/src/movethewheels/hooks/useFleet.ts +19 -6
  27. package/src/movethewheels/hooks/useOrders.ts +35 -0
  28. package/src/movethewheels/hooks/useRoutes.ts +1 -1
  29. package/src/movethewheels/pages/CreateOrderPage.tsx +108 -5
  30. package/src/movethewheels/pages/OrderDetailsPage.tsx +130 -60
  31. package/src/movethewheels/pages/RouteDetailsPage.tsx +57 -47
  32. package/src/movethewheels/types/index.ts +5 -0
@@ -16,6 +16,7 @@ import {
16
16
  SelectTrigger,
17
17
  SelectValue,
18
18
  } from './ui';
19
+ import { EntityCombobox } from './EntityCombobox';
19
20
  import type { Customer } from '../types';
20
21
  import { FormError } from './FormError';
21
22
 
@@ -30,7 +31,8 @@ interface CustomerPickerProps {
30
31
  onRetry: () => void;
31
32
  onOpenCustomers?: () => void;
32
33
  selectedCustomer?: Customer | null;
33
- searchQuery?: string;
34
+ /** Called with the (already debounced) query text as the user types in the
35
+ * combobox's search box, so the caller can re-fetch `customers` server-side. */
34
36
  onSearchQueryChange?: (query: string) => void;
35
37
  disabled?: boolean;
36
38
  }
@@ -51,13 +53,6 @@ const EMPTY_FORM: CustomerFormState = {
51
53
  company: '',
52
54
  };
53
55
 
54
- function mergeCustomers(customers: Customer[], selectedCustomer?: Customer | null): Customer[] {
55
- if (!selectedCustomer || customers.some((customer) => customer.id === selectedCustomer.id)) {
56
- return customers;
57
- }
58
- return [selectedCustomer, ...customers];
59
- }
60
-
61
56
  export function CustomerPicker({
62
57
  customers,
63
58
  value,
@@ -69,7 +64,6 @@ export function CustomerPicker({
69
64
  onRetry,
70
65
  onOpenCustomers,
71
66
  selectedCustomer,
72
- searchQuery = '',
73
67
  onSearchQueryChange,
74
68
  disabled = false,
75
69
  }: CustomerPickerProps) {
@@ -77,7 +71,6 @@ export function CustomerPicker({
77
71
  const [form, setForm] = useState<CustomerFormState>(EMPTY_FORM);
78
72
  const [isCreating, setIsCreating] = useState(false);
79
73
  const [createError, setCreateError] = useState<string | null>(null);
80
- const options = mergeCustomers(customers, selectedCustomer);
81
74
 
82
75
  const openCreateDialog = () => {
83
76
  setCreateError(null);
@@ -139,32 +132,29 @@ export function CustomerPicker({
139
132
  </Button>
140
133
  </div>
141
134
  <div className="flex flex-col gap-2 sm:flex-row sm:items-start">
142
- {onSearchQueryChange && (
143
- <Input
144
- value={searchQuery}
145
- onChange={(event) => onSearchQueryChange(event.target.value)}
146
- placeholder="Search customers"
147
- aria-label="Search customers"
148
- disabled={disabled}
149
- className="sm:max-w-56"
150
- />
151
- )}
152
- <Select
135
+ <EntityCombobox
136
+ id="customer"
153
137
  value={value}
154
- onValueChange={onValueChange}
155
- disabled={disabled || isLoading || Boolean(error)}
156
- >
157
- <SelectTrigger id="customer" className="flex-1">
158
- <SelectValue placeholder={isLoading ? 'Loading customers...' : 'Select customer'} />
159
- </SelectTrigger>
160
- <SelectContent>
161
- {options.map((customer) => (
162
- <SelectItem key={customer.id} value={customer.id}>
163
- {customer.name} {customer.email ? `(${customer.email})` : ''}
164
- </SelectItem>
165
- ))}
166
- </SelectContent>
167
- </Select>
138
+ onChange={onValueChange}
139
+ options={customers}
140
+ loading={isLoading}
141
+ getId={(customer) => customer.id}
142
+ getLabel={(customer) => `${customer.name}${customer.email ? ` (${customer.email})` : ''}`}
143
+ renderOption={(customer) => (
144
+ <>
145
+ {customer.name} {customer.email ? `(${customer.email})` : ''}
146
+ </>
147
+ )}
148
+ selectedOption={selectedCustomer}
149
+ required
150
+ placeholder={isLoading ? 'Loading customers...' : 'Select customer'}
151
+ searchPlaceholder="Search customers"
152
+ emptyText="No customers found."
153
+ disabled={disabled || Boolean(error)}
154
+ error={error ? 'Customers could not be loaded' : undefined}
155
+ onSearchChange={onSearchQueryChange}
156
+ className="flex-1"
157
+ />
168
158
  {error && (
169
159
  <Button type="button" variant="outline" size="sm" onClick={onRetry}>
170
160
  <RefreshCw size={14} className="mr-1" />
@@ -183,7 +173,7 @@ export function CustomerPicker({
183
173
  </Button>
184
174
  )}
185
175
  </div>
186
- ) : !isLoading && options.length === 0 ? (
176
+ ) : !isLoading && customers.length === 0 ? (
187
177
  <div className="rounded-lg border border-border-seam bg-bg-sunken/40 p-3">
188
178
  <div className="flex items-start gap-2">
189
179
  <UserPlus size={16} className="mt-0.5 shrink-0 text-text-secondary" />
@@ -0,0 +1,237 @@
1
+ /**
2
+ * EntityCombobox — searchable single-select for backend-record lists (routes,
3
+ * customers, vehicles, drivers, …).
4
+ *
5
+ * A button trigger shows the current selection's label (or a placeholder) and
6
+ * opens a Popover with a cmdk Command. Built entirely on existing fe-libs `ui`
7
+ * primitives (Command + Popover) — no new dependency, following the pattern
8
+ * already proven by `microfe-product`'s `ProductCombobox`.
9
+ *
10
+ * Every list this feeds (`useRoutes`, `useCustomers`, `useVehicles`,
11
+ * `useDrivers`) is capped to a small server page (`limit`, default 20) so the
12
+ * option list is never "fetch everything" — a workspace with thousands of
13
+ * records would otherwise either hang the request or silently truncate. Two
14
+ * search modes cover both call shapes in this app:
15
+ *
16
+ * - **Internal search** (pass `onSearchChange`): the combobox renders its own
17
+ * debounced `CommandInput` and calls back with the debounced query so the
18
+ * caller's hook re-fetches server-side. Used where each field owns its own
19
+ * search (saved route, customer).
20
+ * - **External search** (omit `onSearchChange`): no input is rendered inside
21
+ * the popover; the caller is expected to already be filtering `options` via
22
+ * a search control elsewhere on the page. Used where one shared search box
23
+ * drives two comboboxes at once (vehicle + driver assignment), so a second,
24
+ * redundant input isn't stacked on top of it.
25
+ *
26
+ * In both modes, the current selection's label is resolved independently via
27
+ * `selectedOption`/`selectedLoading` (typically a single-record lookup hook
28
+ * like `useRoute(value)`), so it keeps rendering correctly even once it falls
29
+ * outside the current search results or page.
30
+ */
31
+ import { useEffect, useState } from 'react';
32
+ import { Check, ChevronsUpDown } from 'lucide-react';
33
+ import {
34
+ Command,
35
+ CommandEmpty,
36
+ CommandInput,
37
+ CommandItem,
38
+ CommandList,
39
+ Popover,
40
+ PopoverContent,
41
+ PopoverTrigger,
42
+ } from './ui';
43
+ import { cn } from '../utils/cn';
44
+ import { useDebouncedValue } from '../hooks/useDebouncedValue';
45
+
46
+ interface EntityComboboxProps<T> {
47
+ /** Selected record id (controlled). Empty string = nothing selected. */
48
+ value: string;
49
+ /** Called with the selected record id ('' when cleared). */
50
+ onChange: (id: string) => void;
51
+ /** The current (already fetched/filtered) page of selectable records. */
52
+ options: T[];
53
+ /** True while `options` is (re)loading. */
54
+ loading?: boolean;
55
+ getId: (option: T) => string;
56
+ /** Rich content for a list row. */
57
+ renderOption: (option: T) => React.ReactNode;
58
+ /** Plain-text label for the trigger button (must be a string, not JSX). */
59
+ getLabel: (option: T) => string;
60
+ /** Independently-loaded current selection, so its label still renders even
61
+ * when it isn't present in `options` (filtered out or paginated away). */
62
+ selectedOption?: T | null;
63
+ selectedLoading?: boolean;
64
+ /** Renders a clearable "None" row when true (hidden when `required`). */
65
+ required?: boolean;
66
+ placeholder?: string;
67
+ searchPlaceholder?: string;
68
+ emptyText?: string;
69
+ noneLabel?: string;
70
+ disabled?: boolean;
71
+ id?: string;
72
+ /** Toggles the invalid border + aria-invalid. */
73
+ error?: string;
74
+ /** Extra classes merged onto the trigger button. */
75
+ className?: string;
76
+ /** Server-side search callback (debounced). Omit to hide the built-in search
77
+ * input when a caller already provides one elsewhere for the same list. */
78
+ onSearchChange?: (value: string) => void;
79
+ /** True when the source hook's page doesn't hold every matching record —
80
+ * paired with `totalCount` to render a "keep typing to narrow" hint. */
81
+ hasMore?: boolean;
82
+ totalCount?: number;
83
+ }
84
+
85
+ export function EntityCombobox<T>({
86
+ value,
87
+ onChange,
88
+ options,
89
+ loading = false,
90
+ getId,
91
+ renderOption,
92
+ getLabel,
93
+ selectedOption = null,
94
+ selectedLoading = false,
95
+ required = false,
96
+ placeholder = 'Select…',
97
+ searchPlaceholder = 'Search…',
98
+ emptyText = 'No results found.',
99
+ noneLabel = 'None',
100
+ disabled = false,
101
+ id,
102
+ error,
103
+ className,
104
+ onSearchChange,
105
+ hasMore = false,
106
+ totalCount,
107
+ }: EntityComboboxProps<T>) {
108
+ const [open, setOpen] = useState(false);
109
+ const [search, setSearch] = useState('');
110
+ const debouncedSearch = useDebouncedValue(search.trim(), 200);
111
+
112
+ useEffect(() => {
113
+ if (onSearchChange) {
114
+ onSearchChange(debouncedSearch);
115
+ }
116
+ // Only internal-search mode reacts to keystrokes; external-search callers
117
+ // manage their own query state and don't pass `onSearchChange`.
118
+ // eslint-disable-next-line react-hooks/exhaustive-deps
119
+ }, [debouncedSearch]);
120
+
121
+ const triggerLabel = selectedOption
122
+ ? getLabel(selectedOption)
123
+ : value
124
+ ? selectedLoading
125
+ ? 'Loading…'
126
+ : value
127
+ : placeholder;
128
+
129
+ const triggerClassName = cn(
130
+ 'w-full flex items-center justify-between gap-2 px-3 py-2',
131
+ 'bg-surface',
132
+ 'border border-default',
133
+ 'rounded-lg',
134
+ 'text-left text-sm text-text-primary',
135
+ 'focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]',
136
+ 'disabled:bg-bg-sunken disabled:cursor-not-allowed',
137
+ 'transition-all duration-200',
138
+ error && 'border-[var(--color-status-error)]',
139
+ !selectedOption && 'text-text-tertiary',
140
+ className
141
+ );
142
+
143
+ const showMoreHint = hasMore && typeof totalCount === 'number' && totalCount > options.length;
144
+
145
+ return (
146
+ <Popover
147
+ open={open}
148
+ onOpenChange={(next: boolean) => {
149
+ setOpen(next);
150
+ // Reset the query on close so reopening starts fresh, not pre-filtered.
151
+ if (!next) setSearch('');
152
+ }}
153
+ >
154
+ <PopoverTrigger asChild>
155
+ <button
156
+ type="button"
157
+ id={id}
158
+ role="combobox"
159
+ aria-expanded={open}
160
+ aria-required={required}
161
+ aria-invalid={error ? true : undefined}
162
+ disabled={disabled}
163
+ className={triggerClassName}
164
+ >
165
+ <span className="truncate">{triggerLabel}</span>
166
+ <ChevronsUpDown size={16} className="shrink-0 text-text-tertiary" aria-hidden="true" />
167
+ </button>
168
+ </PopoverTrigger>
169
+ <PopoverContent
170
+ align="start"
171
+ className="w-[var(--radix-popover-trigger-width)] p-0 border border-default bg-surface"
172
+ >
173
+ {/* Search is server-side (or driven externally) → disable cmdk's
174
+ client filter so we render exactly what was fetched. */}
175
+ <Command shouldFilter={false} className="bg-surface text-text-primary">
176
+ {onSearchChange && (
177
+ <CommandInput
178
+ value={search}
179
+ onValueChange={setSearch}
180
+ placeholder={searchPlaceholder}
181
+ className="text-text-primary placeholder:text-text-tertiary"
182
+ />
183
+ )}
184
+ <CommandList>
185
+ <CommandEmpty className="text-text-tertiary">
186
+ {loading ? 'Searching…' : emptyText}
187
+ </CommandEmpty>
188
+ {!required && (
189
+ <CommandItem
190
+ value="__none__"
191
+ onSelect={() => {
192
+ onChange('');
193
+ setOpen(false);
194
+ }}
195
+ className="text-text-tertiary"
196
+ >
197
+ <Check
198
+ size={16}
199
+ className={cn('shrink-0', value === '' ? 'opacity-100' : 'opacity-0')}
200
+ aria-hidden="true"
201
+ />
202
+ <span className="truncate italic">{noneLabel}</span>
203
+ </CommandItem>
204
+ )}
205
+ {options.map((option) => {
206
+ const optionId = getId(option);
207
+ return (
208
+ <CommandItem
209
+ key={optionId}
210
+ value={optionId}
211
+ onSelect={() => {
212
+ onChange(optionId);
213
+ setOpen(false);
214
+ }}
215
+ className="text-text-primary"
216
+ >
217
+ <Check
218
+ size={16}
219
+ className={cn('shrink-0', optionId === value ? 'opacity-100' : 'opacity-0')}
220
+ aria-hidden="true"
221
+ />
222
+ <span className="truncate">{renderOption(option)}</span>
223
+ </CommandItem>
224
+ );
225
+ })}
226
+ {showMoreHint && (
227
+ <div className="px-2 py-1.5 text-xs text-text-tertiary border-t border-border-seam">
228
+ Showing {options.length} of {totalCount}
229
+ {onSearchChange ? ' — keep typing to narrow results.' : '.'}
230
+ </div>
231
+ )}
232
+ </CommandList>
233
+ </Command>
234
+ </PopoverContent>
235
+ </Popover>
236
+ );
237
+ }
@@ -21,6 +21,15 @@ export {
21
21
  SelectItem,
22
22
  SelectTrigger,
23
23
  SelectValue,
24
+ Command,
25
+ CommandEmpty,
26
+ CommandGroup,
27
+ CommandInput,
28
+ CommandItem,
29
+ CommandList,
30
+ Popover,
31
+ PopoverContent,
32
+ PopoverTrigger,
24
33
  Tabs,
25
34
  TabsList,
26
35
  TabsTrigger,
@@ -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
  }),
@@ -695,6 +702,8 @@ interface UseVehiclesOptions extends UseListOptions {
695
702
  search?: string;
696
703
  status?: VehicleStatus;
697
704
  vehicleType?: Vehicle['type'];
705
+ /** Narrows to vehicles linked to this driver server-side (VehicleFilterInput.driverId). */
706
+ driverId?: string;
698
707
  }
699
708
 
700
709
  interface UseVehiclesResult {
@@ -710,7 +719,7 @@ interface UseVehiclesResult {
710
719
  }
711
720
 
712
721
  export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult {
713
- const { limit = 20, offset = 0, search, status, vehicleType } = options;
722
+ const { limit = 20, offset = 0, search, status, vehicleType, driverId } = options;
714
723
  const graphQLOptions = useGraphqlOptions();
715
724
  const [vehicles, setVehicles] = useState<Vehicle[]>([]);
716
725
  const [totalVehicles, setTotalVehicles] = useState(0);
@@ -730,6 +739,7 @@ export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult
730
739
  search: search || undefined,
731
740
  status: status ? toApiVehicleStatus(status) : undefined,
732
741
  type: vehicleType?.toUpperCase(),
742
+ driverId: driverId || undefined,
733
743
  },
734
744
  pagination: { limit, offset },
735
745
  });
@@ -741,7 +751,7 @@ export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult
741
751
  } finally {
742
752
  setIsLoading(false);
743
753
  }
744
- }, [graphQLOptions, limit, offset, search, status, vehicleType]);
754
+ }, [graphQLOptions, limit, offset, search, status, vehicleType, driverId]);
745
755
 
746
756
  useEffect(() => {
747
757
  void load();
@@ -859,10 +869,12 @@ interface UseDriversOptions extends UseListOptions {
859
869
  status?: DriverStatus;
860
870
  search?: string;
861
871
  licenseClass?: string;
872
+ /** Narrows to drivers linked to this vehicle server-side (DriverFilterInput.vehicleId). */
873
+ vehicleId?: string;
862
874
  }
863
875
 
864
876
  export function useDrivers(options: UseDriversOptions = {}): UseDriversResult {
865
- const { limit = 20, offset = 0, status, search, licenseClass } = options;
877
+ const { limit = 20, offset = 0, status, search, licenseClass, vehicleId } = options;
866
878
  const graphQLOptions = useGraphqlOptions();
867
879
  const [drivers, setDrivers] = useState<Driver[]>([]);
868
880
  const [totalDrivers, setTotalDrivers] = useState(0);
@@ -882,6 +894,7 @@ export function useDrivers(options: UseDriversOptions = {}): UseDriversResult {
882
894
  status: status ? toApiDriverStatus(status) : undefined,
883
895
  search: search || undefined,
884
896
  licenseClass: licenseClass || undefined,
897
+ vehicleId: vehicleId || undefined,
885
898
  },
886
899
  pagination: { limit, offset },
887
900
  });
@@ -893,7 +906,7 @@ export function useDrivers(options: UseDriversOptions = {}): UseDriversResult {
893
906
  } finally {
894
907
  setIsLoading(false);
895
908
  }
896
- }, [graphQLOptions, limit, offset, status, search, licenseClass]);
909
+ }, [graphQLOptions, limit, offset, status, search, licenseClass, vehicleId]);
897
910
 
898
911
  useEffect(() => {
899
912
  void load();
@@ -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,