@burdenoff/microfe-movethewheels 2026.929.3 → 2026.930.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 (72) hide show
  1. package/dist/components/CustomerPicker.js +52 -48
  2. package/dist/components/EntityCombobox.js +25 -23
  3. package/dist/components/OrderItemRow.js +123 -78
  4. package/dist/generated/wspace-types.d.ts +1547 -64
  5. package/dist/hooks/useCustomers.js +55 -26
  6. package/dist/hooks/useFleet.js +236 -157
  7. package/dist/hooks/useInventory.js +214 -153
  8. package/dist/hooks/useMultiModalShipments.js +30 -26
  9. package/dist/hooks/useRoutes.js +64 -24
  10. package/dist/hooks/useTracking.js +14 -16
  11. package/dist/movethewheels/components/CustomerPicker.d.ts +5 -1
  12. package/dist/movethewheels/components/EntityCombobox.d.ts +6 -3
  13. package/dist/movethewheels/components/OrderItemRow.d.ts +7 -1
  14. package/dist/movethewheels/hooks/useCustomers.d.ts +3 -0
  15. package/dist/movethewheels/hooks/useFleet.d.ts +15 -2
  16. package/dist/movethewheels/hooks/useInventory.d.ts +15 -12
  17. package/dist/movethewheels/hooks/useMultiModalShipments.d.ts +1 -0
  18. package/dist/movethewheels/hooks/useRoutes.d.ts +7 -1
  19. package/dist/movethewheels/types/index.d.ts +33 -23
  20. package/dist/movethewheels/utils/formatters.d.ts +1 -1
  21. package/dist/movethewheels/utils/orderValidation.d.ts +11 -0
  22. package/dist/pages/CreateOrderPage.js +349 -297
  23. package/dist/pages/CustomerDetailsPage.js +6 -6
  24. package/dist/pages/CustomersPage.js +2 -2
  25. package/dist/pages/DashboardPage.js +1 -1
  26. package/dist/pages/DriverDetailsPage.js +340 -332
  27. package/dist/pages/DriversPage.js +7 -5
  28. package/dist/pages/FleetPage.js +1 -1
  29. package/dist/pages/InventoryPage.js +11 -12
  30. package/dist/pages/LiveTrackingPage.js +4 -4
  31. package/dist/pages/MultiModalShipmentPage.js +139 -125
  32. package/dist/pages/OrderDetailsPage.js +132 -140
  33. package/dist/pages/ProductDetailsPage.js +408 -249
  34. package/dist/pages/ProductsPage.js +5 -5
  35. package/dist/pages/RouteDetailsPage.js +208 -210
  36. package/dist/pages/RoutesPage.js +2 -2
  37. package/dist/pages/TrackingPage.js +16 -9
  38. package/dist/pages/WarehouseDetailsPage.js +166 -163
  39. package/dist/pages/WarehousesPage.js +29 -21
  40. package/dist/utils/orderValidation.js +18 -0
  41. package/package.json +1 -1
  42. package/src/generated/wspace-types.ts +1762 -64
  43. package/src/movethewheels/components/CustomerPicker.tsx +12 -0
  44. package/src/movethewheels/components/EntityCombobox.tsx +30 -11
  45. package/src/movethewheels/components/OrderItemRow.tsx +44 -0
  46. package/src/movethewheels/hooks/useCustomers.ts +45 -9
  47. package/src/movethewheels/hooks/useFleet.ts +214 -36
  48. package/src/movethewheels/hooks/useInventory.ts +185 -49
  49. package/src/movethewheels/hooks/useMultiModalShipments.ts +9 -7
  50. package/src/movethewheels/hooks/useRoutes.ts +87 -16
  51. package/src/movethewheels/hooks/useTracking.ts +23 -15
  52. package/src/movethewheels/pages/CreateOrderPage.tsx +66 -2
  53. package/src/movethewheels/pages/CustomerDetailsPage.tsx +14 -8
  54. package/src/movethewheels/pages/CustomersPage.tsx +5 -2
  55. package/src/movethewheels/pages/DashboardPage.tsx +3 -1
  56. package/src/movethewheels/pages/DriverDetailsPage.tsx +25 -4
  57. package/src/movethewheels/pages/DriversPage.tsx +8 -2
  58. package/src/movethewheels/pages/FleetPage.tsx +1 -1
  59. package/src/movethewheels/pages/InventoryPage.tsx +20 -16
  60. package/src/movethewheels/pages/LiveTrackingPage.tsx +6 -4
  61. package/src/movethewheels/pages/MultiModalShipmentPage.tsx +15 -1
  62. package/src/movethewheels/pages/OrderDetailsPage.tsx +25 -25
  63. package/src/movethewheels/pages/ProductDetailsPage.tsx +228 -49
  64. package/src/movethewheels/pages/ProductsPage.tsx +13 -5
  65. package/src/movethewheels/pages/RouteDetailsPage.tsx +24 -12
  66. package/src/movethewheels/pages/RoutesPage.tsx +4 -2
  67. package/src/movethewheels/pages/TrackingPage.tsx +32 -12
  68. package/src/movethewheels/pages/WarehouseDetailsPage.tsx +42 -29
  69. package/src/movethewheels/pages/WarehousesPage.tsx +23 -15
  70. package/src/movethewheels/types/index.ts +36 -24
  71. package/src/movethewheels/utils/formatters.ts +1 -1
  72. package/src/movethewheels/utils/orderValidation.ts +53 -0
@@ -27,8 +27,12 @@ interface CustomerPickerProps {
27
27
  onCreated: (customer: Customer) => void;
28
28
  createCustomer: (customer: Partial<Customer>) => Promise<Customer>;
29
29
  isLoading: boolean;
30
+ isLoadingMore?: boolean;
30
31
  error: Error | null;
31
32
  onRetry: () => void;
33
+ onLoadMore?: () => void | Promise<void>;
34
+ hasMore?: boolean;
35
+ totalCount?: number;
32
36
  onOpenCustomers?: () => void;
33
37
  selectedCustomer?: Customer | null;
34
38
  /** Called with the (already debounced) query text as the user types in the
@@ -60,8 +64,12 @@ export function CustomerPicker({
60
64
  onCreated,
61
65
  createCustomer,
62
66
  isLoading,
67
+ isLoadingMore = false,
63
68
  error,
64
69
  onRetry,
70
+ onLoadMore,
71
+ hasMore = false,
72
+ totalCount,
65
73
  onOpenCustomers,
66
74
  selectedCustomer,
67
75
  onSearchQueryChange,
@@ -153,6 +161,10 @@ export function CustomerPicker({
153
161
  disabled={disabled || Boolean(error)}
154
162
  error={error ? 'Customers could not be loaded' : undefined}
155
163
  onSearchChange={onSearchQueryChange}
164
+ hasMore={hasMore}
165
+ totalCount={totalCount}
166
+ onLoadMore={onLoadMore}
167
+ loadingMore={isLoadingMore}
156
168
  className="flex-1"
157
169
  />
158
170
  {error && (
@@ -8,10 +8,10 @@
8
8
  * already proven by `microfe-product`'s `ProductCombobox`.
9
9
  *
10
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:
11
+ * `useDrivers`, and product pickers) is capped to a small server page (`limit`,
12
+ * default 20). When the source exposes `onLoadMore`, the accessible Load more
13
+ * control requests the next server page and appends it without losing the
14
+ * current selection.
15
15
  *
16
16
  * - **Internal search** (pass `onSearchChange`): the combobox renders its own
17
17
  * debounced `CommandInput` and calls back with the debounced query so the
@@ -76,10 +76,13 @@ interface EntityComboboxProps<T> {
76
76
  /** Server-side search callback (debounced). Omit to hide the built-in search
77
77
  * input when a caller already provides one elsewhere for the same list. */
78
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. */
79
+ /** True when the source hook has another server page available. */
81
80
  hasMore?: boolean;
82
81
  totalCount?: number;
82
+ /** Fetches and appends the next server page without replacing the current options. */
83
+ onLoadMore?: () => void | Promise<void>;
84
+ /** True while the next server page is being fetched. */
85
+ loadingMore?: boolean;
83
86
  }
84
87
 
85
88
  export function EntityCombobox<T>({
@@ -104,6 +107,8 @@ export function EntityCombobox<T>({
104
107
  onSearchChange,
105
108
  hasMore = false,
106
109
  totalCount,
110
+ onLoadMore,
111
+ loadingMore = false,
107
112
  }: EntityComboboxProps<T>) {
108
113
  const [open, setOpen] = useState(false);
109
114
  const [search, setSearch] = useState('');
@@ -140,7 +145,7 @@ export function EntityCombobox<T>({
140
145
  className
141
146
  );
142
147
 
143
- const showMoreHint = hasMore && typeof totalCount === 'number' && totalCount > options.length;
148
+ const canLoadMore = Boolean(onLoadMore && hasMore);
144
149
 
145
150
  return (
146
151
  <Popover
@@ -223,10 +228,24 @@ export function EntityCombobox<T>({
223
228
  </CommandItem>
224
229
  );
225
230
  })}
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.' : '.'}
231
+ {canLoadMore && (
232
+ <div className="border-t border-border-seam p-2">
233
+ <button
234
+ type="button"
235
+ className="w-full rounded-md px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/10 focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)] disabled:cursor-not-allowed disabled:opacity-60"
236
+ onClick={() => void onLoadMore?.()}
237
+ disabled={loading || loadingMore}
238
+ aria-busy={loadingMore}
239
+ aria-label={
240
+ loadingMore
241
+ ? 'Loading more options'
242
+ : `Load more options${typeof totalCount === 'number' ? ` (${options.length} of ${totalCount} shown)` : ''}`
243
+ }
244
+ >
245
+ {loadingMore
246
+ ? 'Loading more…'
247
+ : `Load more${typeof totalCount === 'number' ? ` (${options.length} of ${totalCount})` : ''}`}
248
+ </button>
230
249
  </div>
231
250
  )}
232
251
  </CommandList>
@@ -12,12 +12,18 @@ import { Button, Input, Label } from './ui';
12
12
  import { EntityCombobox } from './EntityCombobox';
13
13
  import { useProduct, useProductStockLevels } from '../hooks/useInventory';
14
14
  import type { Dimensions, OrderItem, Product } from '../types';
15
+ import type { OrderItemFieldErrors } from '../utils/orderValidation';
15
16
 
16
17
  interface OrderItemRowProps {
17
18
  item: OrderItem;
18
19
  index: number;
19
20
  products: Product[];
20
21
  productsLoading: boolean;
22
+ productsLoadingMore?: boolean;
23
+ productsHasMore?: boolean;
24
+ productsTotalCount?: number;
25
+ onLoadMoreProducts?: () => void | Promise<void>;
26
+ errors?: OrderItemFieldErrors;
21
27
  onUpdate: (index: number, field: string, value: unknown) => void;
22
28
  onRemove: (index: number) => void;
23
29
  }
@@ -37,6 +43,11 @@ export function OrderItemRow({
37
43
  index,
38
44
  products,
39
45
  productsLoading,
46
+ productsLoadingMore = false,
47
+ productsHasMore = false,
48
+ productsTotalCount,
49
+ onLoadMoreProducts,
50
+ errors = {},
40
51
  onUpdate,
41
52
  onRemove,
42
53
  }: OrderItemRowProps) {
@@ -80,6 +91,17 @@ export function OrderItemRow({
80
91
  });
81
92
  };
82
93
 
94
+ const fieldError = (field: keyof OrderItemFieldErrors) =>
95
+ errors[field] ? (
96
+ <p
97
+ id={`item-${field}-${item.id}-error`}
98
+ className="text-xs text-status-error-text"
99
+ role="alert"
100
+ >
101
+ {errors[field]}
102
+ </p>
103
+ ) : null;
104
+
83
105
  return (
84
106
  <div
85
107
  className="rounded-card border border-border-subtle bg-bg-surface p-4 shadow-elevation-1 sm:p-5"
@@ -131,6 +153,10 @@ export function OrderItemRow({
131
153
  placeholder="Pick from your product catalog"
132
154
  emptyText="No products found."
133
155
  noneLabel="Enter details manually"
156
+ hasMore={productsHasMore}
157
+ totalCount={productsTotalCount}
158
+ onLoadMore={onLoadMoreProducts}
159
+ loadingMore={productsLoadingMore}
134
160
  />
135
161
  {item.productId && !stockLoading && (
136
162
  <p className="mt-2 text-xs text-text-secondary" role="status">
@@ -149,7 +175,10 @@ export function OrderItemRow({
149
175
  onUpdate(index, 'name', e.target.value)
150
176
  }
151
177
  placeholder="Product name"
178
+ aria-invalid={Boolean(errors.name)}
179
+ aria-describedby={errors.name ? `item-name-${item.id}-error` : undefined}
152
180
  />
181
+ {fieldError('name')}
153
182
  </div>
154
183
  <div className="space-y-2 lg:col-span-3">
155
184
  <Label htmlFor={`item-sku-${item.id}`}>SKU</Label>
@@ -160,7 +189,10 @@ export function OrderItemRow({
160
189
  onUpdate(index, 'sku', e.target.value)
161
190
  }
162
191
  placeholder="SKU-001"
192
+ aria-invalid={Boolean(errors.sku)}
193
+ aria-describedby={errors.sku ? `item-sku-${item.id}-error` : undefined}
163
194
  />
195
+ {fieldError('sku')}
164
196
  </div>
165
197
  <div className="space-y-2 lg:col-span-2">
166
198
  <Label htmlFor={`item-quantity-${item.id}`}>Quantity</Label>
@@ -172,7 +204,10 @@ export function OrderItemRow({
172
204
  onChange={(e: ChangeEvent<HTMLInputElement>) =>
173
205
  onUpdate(index, 'quantity', parseInt(e.target.value, 10) || 1)
174
206
  }
207
+ aria-invalid={Boolean(errors.quantity)}
208
+ aria-describedby={errors.quantity ? `item-quantity-${item.id}-error` : undefined}
175
209
  />
210
+ {fieldError('quantity')}
176
211
  </div>
177
212
  <div className="space-y-2 lg:col-span-2">
178
213
  <Label htmlFor={`item-weight-${item.id}`}>Weight (kg)</Label>
@@ -185,7 +220,10 @@ export function OrderItemRow({
185
220
  onChange={(e: ChangeEvent<HTMLInputElement>) =>
186
221
  onUpdate(index, 'weight', parseFloat(e.target.value) || 0)
187
222
  }
223
+ aria-invalid={Boolean(errors.weight)}
224
+ aria-describedby={errors.weight ? `item-weight-${item.id}-error` : undefined}
188
225
  />
226
+ {fieldError('weight')}
189
227
  </div>
190
228
  <div className="space-y-2 lg:col-span-1">
191
229
  <Label htmlFor={`item-value-${item.id}`}>Value ($)</Label>
@@ -198,7 +236,10 @@ export function OrderItemRow({
198
236
  onChange={(e: ChangeEvent<HTMLInputElement>) =>
199
237
  onUpdate(index, 'value', parseFloat(e.target.value) || 0)
200
238
  }
239
+ aria-invalid={Boolean(errors.value)}
240
+ aria-describedby={errors.value ? `item-value-${item.id}-error` : undefined}
201
241
  />
242
+ {fieldError('value')}
202
243
  </div>
203
244
  </div>
204
245
 
@@ -221,7 +262,10 @@ export function OrderItemRow({
221
262
  onChange={(e: ChangeEvent<HTMLInputElement>) =>
222
263
  updateDimension(field, e.target.value)
223
264
  }
265
+ aria-invalid={Boolean(errors[field])}
266
+ aria-describedby={errors[field] ? `item-${field}-${item.id}-error` : undefined}
224
267
  />
268
+ {fieldError(field)}
225
269
  </div>
226
270
  ))}
227
271
  </div>
@@ -14,8 +14,11 @@ interface UseCustomersResult {
14
14
  totalCustomers: number;
15
15
  hasMore: boolean;
16
16
  isLoading: boolean;
17
+ /** True while an additional server page is appended. */
18
+ isLoadingMore: boolean;
17
19
  error: Error | null;
18
20
  refetch: () => void;
21
+ loadMore: () => Promise<void>;
19
22
  createCustomer: (customer: Partial<Customer>) => Promise<Customer>;
20
23
  updateCustomer: (id: string, updates: Partial<Customer>) => Promise<Customer>;
21
24
  deleteCustomer: (id: string) => Promise<void>;
@@ -125,6 +128,7 @@ const DELETE_CUSTOMER_MUTATION = `
125
128
  `;
126
129
 
127
130
  function mapCustomer(customer: ApiCustomer): Customer {
131
+ const address = customer.address ?? {};
128
132
  return {
129
133
  id: customer.id,
130
134
  name: customer.name,
@@ -132,17 +136,14 @@ function mapCustomer(customer: ApiCustomer): Customer {
132
136
  phone: customer.phone ?? '',
133
137
  type: customer.type.toLowerCase() === 'b2b' ? 'b2b' : 'b2c',
134
138
  company: customer.company ?? undefined,
135
- address: {
136
- street: '',
137
- city: '',
138
- state: '',
139
- postalCode: '',
140
- country: '',
141
- ...(customer.address ?? {}),
142
- },
139
+ address: Object.fromEntries(
140
+ Object.entries(address).filter(
141
+ ([, value]) => typeof value === 'string' && value.trim().length > 0
142
+ )
143
+ ),
143
144
  businessLicense: customer.businessLicense ?? undefined,
144
145
  taxId: customer.taxId ?? undefined,
145
- rating: customer.rating ?? 0,
146
+ rating: customer.rating ?? undefined,
146
147
  createdAt: new Date(customer.createdAt),
147
148
  updatedAt: new Date(customer.updatedAt),
148
149
  };
@@ -165,6 +166,7 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
165
166
  const [totalCustomers, setTotalCustomers] = useState(0);
166
167
  const [hasMore, setHasMore] = useState(false);
167
168
  const [isLoading, setIsLoading] = useState(false);
169
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
168
170
  const [error, setError] = useState<Error | null>(null);
169
171
 
170
172
  const graphQLOptions = useMemo(
@@ -206,6 +208,38 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
206
208
  }
207
209
  }, [filters, graphQLOptions, limit, offset]);
208
210
 
211
+ const loadMore = useCallback(async () => {
212
+ if (!hasMore || isLoading || isLoadingMore) return;
213
+ const nextOffset = offset + customers.length;
214
+ setIsLoadingMore(true);
215
+ setError(null);
216
+ try {
217
+ const data = await executeGraphQL<{
218
+ customers: ApiCustomer[];
219
+ customersCount: number;
220
+ }>(graphQLOptions, LIST_CUSTOMERS_QUERY, {
221
+ filter: {
222
+ type: filters?.type,
223
+ search: filters?.search,
224
+ },
225
+ pagination: { limit, offset: nextOffset },
226
+ });
227
+ const mapped = data.customers.map(mapCustomer);
228
+ setTotalCustomers(data.customersCount);
229
+ setHasMore(nextOffset + mapped.length < data.customersCount);
230
+ setCustomers((previous) => {
231
+ const existingIds = new Set(previous.map((customer) => customer.id));
232
+ return [...previous, ...mapped.filter((customer) => !existingIds.has(customer.id))];
233
+ });
234
+ } catch (fetchError) {
235
+ setError(
236
+ fetchError instanceof Error ? fetchError : new Error('Failed to load more customers')
237
+ );
238
+ } finally {
239
+ setIsLoadingMore(false);
240
+ }
241
+ }, [customers.length, filters, graphQLOptions, hasMore, isLoading, isLoadingMore, limit, offset]);
242
+
209
243
  useEffect(() => {
210
244
  void loadCustomers();
211
245
  }, [loadCustomers]);
@@ -280,8 +314,10 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
280
314
  totalCustomers,
281
315
  hasMore,
282
316
  isLoading,
317
+ isLoadingMore,
283
318
  error,
284
319
  refetch,
320
+ loadMore,
285
321
  createCustomer,
286
322
  updateCustomer,
287
323
  deleteCustomer,