@burdenoff/microfe-movethewheels 2026.929.4 → 2026.930.2

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 (49) 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/hooks/useCustomers.js +52 -17
  5. package/dist/hooks/useFleet.js +126 -44
  6. package/dist/hooks/useInventory.js +43 -7
  7. package/dist/hooks/useMultiModalShipments.js +30 -26
  8. package/dist/hooks/useRefetchOnLeaveEdit.js +10 -0
  9. package/dist/hooks/useRoutes.js +55 -15
  10. package/dist/movethewheels/components/CustomerPicker.d.ts +5 -1
  11. package/dist/movethewheels/components/EntityCombobox.d.ts +6 -3
  12. package/dist/movethewheels/components/OrderItemRow.d.ts +7 -1
  13. package/dist/movethewheels/hooks/useCustomers.d.ts +3 -0
  14. package/dist/movethewheels/hooks/useFleet.d.ts +6 -0
  15. package/dist/movethewheels/hooks/useInventory.d.ts +2 -0
  16. package/dist/movethewheels/hooks/useMultiModalShipments.d.ts +1 -0
  17. package/dist/movethewheels/hooks/useRefetchOnLeaveEdit.d.ts +7 -0
  18. package/dist/movethewheels/hooks/useRoutes.d.ts +3 -0
  19. package/dist/movethewheels/utils/orderValidation.d.ts +11 -0
  20. package/dist/pages/CreateOrderPage.js +349 -297
  21. package/dist/pages/DriverDetailsPage.js +258 -251
  22. package/dist/pages/DriversPage.js +3 -1
  23. package/dist/pages/MultiModalShipmentPage.js +139 -125
  24. package/dist/pages/OrderDetailsPage.js +132 -140
  25. package/dist/pages/ProductDetailsPage.js +22 -19
  26. package/dist/pages/RouteDetailsPage.js +284 -281
  27. package/dist/pages/TrackingPage.js +2 -1
  28. package/dist/pages/VehicleDetailsPage.js +28 -25
  29. package/dist/utils/orderValidation.js +18 -0
  30. package/package.json +1 -1
  31. package/src/movethewheels/components/CustomerPicker.tsx +12 -0
  32. package/src/movethewheels/components/EntityCombobox.tsx +30 -11
  33. package/src/movethewheels/components/OrderItemRow.tsx +44 -0
  34. package/src/movethewheels/hooks/useCustomers.ts +38 -0
  35. package/src/movethewheels/hooks/useFleet.ts +125 -5
  36. package/src/movethewheels/hooks/useInventory.ts +42 -0
  37. package/src/movethewheels/hooks/useMultiModalShipments.ts +9 -7
  38. package/src/movethewheels/hooks/useRefetchOnLeaveEdit.ts +18 -0
  39. package/src/movethewheels/hooks/useRoutes.ts +51 -0
  40. package/src/movethewheels/pages/CreateOrderPage.tsx +66 -2
  41. package/src/movethewheels/pages/DriverDetailsPage.tsx +14 -1
  42. package/src/movethewheels/pages/DriversPage.tsx +7 -1
  43. package/src/movethewheels/pages/MultiModalShipmentPage.tsx +15 -1
  44. package/src/movethewheels/pages/OrderDetailsPage.tsx +25 -25
  45. package/src/movethewheels/pages/ProductDetailsPage.tsx +2 -0
  46. package/src/movethewheels/pages/RouteDetailsPage.tsx +10 -4
  47. package/src/movethewheels/pages/TrackingPage.tsx +7 -2
  48. package/src/movethewheels/pages/VehicleDetailsPage.tsx +2 -0
  49. package/src/movethewheels/utils/orderValidation.ts +53 -0
@@ -928,8 +928,11 @@ interface UseVehiclesResult {
928
928
  /** True when the last fetch returned a full page (rawCount === limit). */
929
929
  hasMore: boolean;
930
930
  isLoading: boolean;
931
+ /** True while an additional server page is appended. */
932
+ isLoadingMore: boolean;
931
933
  error: Error | null;
932
934
  refetch: () => void;
935
+ loadMore: () => Promise<void>;
933
936
  updateVehicleStatus: (id: string, status: VehicleStatus) => Promise<Vehicle>;
934
937
  assignDriver: (vehicleId: string, driverId: string) => Promise<Vehicle>;
935
938
  deleteVehicle: (id: string) => Promise<void>;
@@ -942,13 +945,16 @@ export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult
942
945
  const [totalVehicles, setTotalVehicles] = useState(0);
943
946
  const [hasMore, setHasMore] = useState(false);
944
947
  const [isLoading, setIsLoading] = useState(false);
948
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
945
949
  const [error, setError] = useState<Error | null>(null);
946
950
  // Stable dependency key for the `ids` array — a caller passing a fresh array
947
951
  // literal each render must not defeat the callback's memoization.
948
952
  const idsKey = ids ? [...ids].sort().join(',') : '';
953
+ const idsFilterMode = ids === undefined ? 'none' : 'exact';
954
+ const exactIds = useMemo(() => (idsKey ? idsKey.split(',') : []), [idsKey]);
949
955
 
950
956
  const load = useCallback(async () => {
951
- if (ids && ids.length === 0) {
957
+ if (idsFilterMode === 'exact' && exactIds.length === 0) {
952
958
  // Nothing to look up — skip the request and clear stale results.
953
959
  setVehicles([]);
954
960
  setTotalVehicles(0);
@@ -967,11 +973,14 @@ export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult
967
973
  status: status ? toApiVehicleStatus(status) : undefined,
968
974
  type: vehicleType?.toUpperCase(),
969
975
  driverId: driverId || undefined,
970
- ids: ids && ids.length > 0 ? [...ids] : undefined,
976
+ ids: exactIds.length > 0 ? exactIds : undefined,
977
+ },
978
+ pagination: {
979
+ limit: idsFilterMode === 'exact' ? exactIds.length : limit,
980
+ offset: idsFilterMode === 'exact' ? 0 : offset,
971
981
  },
972
- pagination: { limit: ids && ids.length > 0 ? ids.length : limit, offset: ids ? 0 : offset },
973
982
  });
974
- setHasMore(offset + data.vehicles.length < data.vehiclesCount);
983
+ setHasMore(idsFilterMode === 'none' && offset + data.vehicles.length < data.vehiclesCount);
975
984
  setTotalVehicles(data.vehiclesCount);
976
985
  setVehicles(data.vehicles.map(mapVehicle));
977
986
  } catch (fetchError) {
@@ -979,7 +988,65 @@ export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult
979
988
  } finally {
980
989
  setIsLoading(false);
981
990
  }
982
- }, [graphQLOptions, limit, offset, search, status, vehicleType, driverId, idsKey]);
991
+ }, [
992
+ graphQLOptions,
993
+ limit,
994
+ offset,
995
+ search,
996
+ status,
997
+ vehicleType,
998
+ driverId,
999
+ idsFilterMode,
1000
+ exactIds,
1001
+ ]);
1002
+
1003
+ const loadMore = useCallback(async () => {
1004
+ if (idsFilterMode === 'exact' || !hasMore || isLoading || isLoadingMore) return;
1005
+ const nextOffset = offset + vehicles.length;
1006
+ setIsLoadingMore(true);
1007
+ setError(null);
1008
+ try {
1009
+ const data = await executeGraphQL<{ vehicles: ApiVehicle[]; vehiclesCount: number }>(
1010
+ graphQLOptions,
1011
+ LIST_VEHICLES_QUERY,
1012
+ {
1013
+ filter: {
1014
+ search: search || undefined,
1015
+ status: status ? toApiVehicleStatus(status) : undefined,
1016
+ type: vehicleType?.toUpperCase(),
1017
+ driverId: driverId || undefined,
1018
+ },
1019
+ pagination: { limit, offset: nextOffset },
1020
+ }
1021
+ );
1022
+ const mapped = data.vehicles.map(mapVehicle);
1023
+ setTotalVehicles(data.vehiclesCount);
1024
+ setHasMore(nextOffset + mapped.length < data.vehiclesCount);
1025
+ setVehicles((previous) => {
1026
+ const existingIds = new Set(previous.map((vehicle) => vehicle.id));
1027
+ return [...previous, ...mapped.filter((vehicle) => !existingIds.has(vehicle.id))];
1028
+ });
1029
+ } catch (fetchError) {
1030
+ setError(
1031
+ fetchError instanceof Error ? fetchError : new Error('Failed to load more vehicles')
1032
+ );
1033
+ } finally {
1034
+ setIsLoadingMore(false);
1035
+ }
1036
+ }, [
1037
+ driverId,
1038
+ graphQLOptions,
1039
+ hasMore,
1040
+ idsFilterMode,
1041
+ isLoading,
1042
+ isLoadingMore,
1043
+ limit,
1044
+ offset,
1045
+ search,
1046
+ status,
1047
+ vehicleType,
1048
+ vehicles.length,
1049
+ ]);
983
1050
 
984
1051
  useEffect(() => {
985
1052
  void load();
@@ -1044,8 +1111,10 @@ export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult
1044
1111
  totalVehicles,
1045
1112
  hasMore,
1046
1113
  isLoading,
1114
+ isLoadingMore,
1047
1115
  error,
1048
1116
  refetch,
1117
+ loadMore,
1049
1118
  updateVehicleStatus,
1050
1119
  assignDriver,
1051
1120
  deleteVehicle,
@@ -1099,8 +1168,11 @@ interface UseDriversResult {
1099
1168
  /** True when the last fetch returned a full page (rawCount === limit). */
1100
1169
  hasMore: boolean;
1101
1170
  isLoading: boolean;
1171
+ /** True while an additional server page is appended. */
1172
+ isLoadingMore: boolean;
1102
1173
  error: Error | null;
1103
1174
  refetch: () => void;
1175
+ loadMore: () => Promise<void>;
1104
1176
  updateDriverStatus: (id: string, status: DriverStatus) => Promise<Driver>;
1105
1177
  assignVehicle: (driverId: string, vehicleId: string | null) => Promise<Driver>;
1106
1178
  }
@@ -1121,6 +1193,7 @@ export function useDrivers(options: UseDriversOptions = {}): UseDriversResult {
1121
1193
  const [totalDrivers, setTotalDrivers] = useState(0);
1122
1194
  const [hasMore, setHasMore] = useState(false);
1123
1195
  const [isLoading, setIsLoading] = useState(false);
1196
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
1124
1197
  const [error, setError] = useState<Error | null>(null);
1125
1198
 
1126
1199
  const load = useCallback(async () => {
@@ -1149,6 +1222,51 @@ export function useDrivers(options: UseDriversOptions = {}): UseDriversResult {
1149
1222
  }
1150
1223
  }, [graphQLOptions, limit, offset, status, search, licenseClass, vehicleId]);
1151
1224
 
1225
+ const loadMore = useCallback(async () => {
1226
+ if (!hasMore || isLoading || isLoadingMore) return;
1227
+ const nextOffset = offset + drivers.length;
1228
+ setIsLoadingMore(true);
1229
+ setError(null);
1230
+ try {
1231
+ const data = await executeGraphQL<{ drivers: ApiDriver[]; driversCount: number }>(
1232
+ graphQLOptions,
1233
+ LIST_DRIVERS_QUERY,
1234
+ {
1235
+ filter: {
1236
+ status: status ? toApiDriverStatus(status) : undefined,
1237
+ search: search || undefined,
1238
+ licenseClass: licenseClass || undefined,
1239
+ vehicleId: vehicleId || undefined,
1240
+ },
1241
+ pagination: { limit, offset: nextOffset },
1242
+ }
1243
+ );
1244
+ const mapped = data.drivers.map(mapDriver);
1245
+ setTotalDrivers(data.driversCount);
1246
+ setHasMore(nextOffset + mapped.length < data.driversCount);
1247
+ setDrivers((previous) => {
1248
+ const existingIds = new Set(previous.map((driver) => driver.id));
1249
+ return [...previous, ...mapped.filter((driver) => !existingIds.has(driver.id))];
1250
+ });
1251
+ } catch (fetchError) {
1252
+ setError(fetchError instanceof Error ? fetchError : new Error('Failed to load more drivers'));
1253
+ } finally {
1254
+ setIsLoadingMore(false);
1255
+ }
1256
+ }, [
1257
+ drivers.length,
1258
+ graphQLOptions,
1259
+ hasMore,
1260
+ isLoading,
1261
+ isLoadingMore,
1262
+ limit,
1263
+ licenseClass,
1264
+ offset,
1265
+ search,
1266
+ status,
1267
+ vehicleId,
1268
+ ]);
1269
+
1152
1270
  useEffect(() => {
1153
1271
  void load();
1154
1272
  }, [load]);
@@ -1196,8 +1314,10 @@ export function useDrivers(options: UseDriversOptions = {}): UseDriversResult {
1196
1314
  totalDrivers,
1197
1315
  hasMore,
1198
1316
  isLoading,
1317
+ isLoadingMore,
1199
1318
  error,
1200
1319
  refetch,
1320
+ loadMore,
1201
1321
  updateDriverStatus,
1202
1322
  assignVehicle,
1203
1323
  };
@@ -849,6 +849,7 @@ export function useProducts(options: UseProductsOptions = {}) {
849
849
  const [totalProducts, setTotalProducts] = useState(0);
850
850
  const [hasMore, setHasMore] = useState(false);
851
851
  const [isLoading, setIsLoading] = useState(false);
852
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
852
853
  const [error, setError] = useState<Error | null>(null);
853
854
 
854
855
  const load = useCallback(async () => {
@@ -872,6 +873,45 @@ export function useProducts(options: UseProductsOptions = {}) {
872
873
  }
873
874
  }, [graphQLOptions, limit, offset, search, category]);
874
875
 
876
+ const loadMore = useCallback(async () => {
877
+ if (!hasMore || isLoading || isLoadingMore) return;
878
+ const nextOffset = offset + products.length;
879
+ setIsLoadingMore(true);
880
+ setError(null);
881
+ try {
882
+ const data = await executeGraphQL<{
883
+ cargoItems: ApiProduct[];
884
+ cargoItemsCount: number;
885
+ }>(graphQLOptions, LIST_PRODUCTS_QUERY, {
886
+ filter: { search: search || undefined, category: category || undefined },
887
+ pagination: { limit, offset: nextOffset },
888
+ });
889
+ const mapped = data.cargoItems.map(mapProduct);
890
+ setTotalProducts(data.cargoItemsCount);
891
+ setHasMore(nextOffset + mapped.length < data.cargoItemsCount);
892
+ setProducts((previous) => {
893
+ const existingIds = new Set(previous.map((product) => product.id));
894
+ return [...previous, ...mapped.filter((product) => !existingIds.has(product.id))];
895
+ });
896
+ } catch (fetchError) {
897
+ setError(
898
+ fetchError instanceof Error ? fetchError : new Error('Failed to load more products')
899
+ );
900
+ } finally {
901
+ setIsLoadingMore(false);
902
+ }
903
+ }, [
904
+ category,
905
+ graphQLOptions,
906
+ hasMore,
907
+ isLoading,
908
+ isLoadingMore,
909
+ limit,
910
+ offset,
911
+ products.length,
912
+ search,
913
+ ]);
914
+
875
915
  useEffect(() => {
876
916
  void load();
877
917
  }, [load]);
@@ -881,8 +921,10 @@ export function useProducts(options: UseProductsOptions = {}) {
881
921
  totalProducts,
882
922
  hasMore,
883
923
  isLoading,
924
+ isLoadingMore,
884
925
  error,
885
926
  refetch: () => void load(),
927
+ loadMore,
886
928
  };
887
929
  }
888
930
 
@@ -140,11 +140,11 @@ const SHIPMENT_FIELDS = `
140
140
  `;
141
141
 
142
142
  const MULTI_MODAL_SHIPMENTS_QUERY = `
143
- query ListMultiModalShipments($pagination: PaginationInput) {
144
- shipments(filter: { isMultiModal: true }, pagination: $pagination) {
143
+ query ListMultiModalShipments($search: String, $pagination: PaginationInput) {
144
+ shipments(filter: { isMultiModal: true, search: $search }, pagination: $pagination) {
145
145
  ${SHIPMENT_FIELDS}
146
146
  }
147
- shipmentsCount(filter: { isMultiModal: true })
147
+ shipmentsCount(filter: { isMultiModal: true, search: $search })
148
148
  }
149
149
  `;
150
150
 
@@ -265,8 +265,10 @@ function useGraphqlOptions() {
265
265
  );
266
266
  }
267
267
 
268
- export function useMultiModalShipments(options: { limit?: number; offset?: number } = {}) {
269
- const { limit = 10, offset = 0 } = options;
268
+ export function useMultiModalShipments(
269
+ options: { limit?: number; offset?: number; search?: string } = {}
270
+ ) {
271
+ const { limit = 10, offset = 0, search = '' } = options;
270
272
  const graphQLOptions = useGraphqlOptions();
271
273
  const [shipments, setShipments] = useState<MultiModalShipment[]>([]);
272
274
  const [totalShipments, setTotalShipments] = useState(0);
@@ -281,7 +283,7 @@ export function useMultiModalShipments(options: { limit?: number; offset?: numbe
281
283
  const data = await executeGraphQL<{ shipments: ApiShipment[]; shipmentsCount: number }>(
282
284
  graphQLOptions,
283
285
  MULTI_MODAL_SHIPMENTS_QUERY,
284
- { pagination: { limit, offset } }
286
+ { search: search.trim() || undefined, pagination: { limit, offset } }
285
287
  );
286
288
  setShipments(data.shipments.map(mapShipment));
287
289
  setTotalShipments(data.shipmentsCount);
@@ -293,7 +295,7 @@ export function useMultiModalShipments(options: { limit?: number; offset?: numbe
293
295
  } finally {
294
296
  setIsLoading(false);
295
297
  }
296
- }, [graphQLOptions, limit, offset]);
298
+ }, [graphQLOptions, limit, offset, search]);
297
299
 
298
300
  useEffect(() => {
299
301
  void load();
@@ -0,0 +1,18 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ /**
4
+ * Detail pages render their `/edit` form from the same route component, so the
5
+ * component (and its record hook) stays mounted across edit → detail. Without a
6
+ * refetch the detail view keeps showing the pre-edit record. Call `refetch`
7
+ * exactly when the edit route is left, never on the initial mount.
8
+ */
9
+ export function useRefetchOnLeaveEdit(isEditRoute: boolean, refetch: () => void): void {
10
+ const wasEditing = useRef(isEditRoute);
11
+
12
+ useEffect(() => {
13
+ if (wasEditing.current && !isEditRoute) {
14
+ refetch();
15
+ }
16
+ wasEditing.current = isEditRoute;
17
+ }, [isEditRoute, refetch]);
18
+ }
@@ -18,8 +18,11 @@ interface UseRoutesResult {
18
18
  /** True when the last fetch has another server page. */
19
19
  hasMore: boolean;
20
20
  isLoading: boolean;
21
+ /** True while an additional server page is appended. */
22
+ isLoadingMore: boolean;
21
23
  error: Error | null;
22
24
  refetch: () => void;
25
+ loadMore: () => Promise<void>;
23
26
  createRoute: (route: Partial<Route>) => Promise<Route>;
24
27
  updateRoute: (
25
28
  id: string,
@@ -275,6 +278,7 @@ export function useRoutes(options: UseRoutesOptions = {}): UseRoutesResult {
275
278
  const [totalRoutes, setTotalRoutes] = useState(0);
276
279
  const [hasMore, setHasMore] = useState(false);
277
280
  const [isLoading, setIsLoading] = useState(false);
281
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
278
282
  const [error, setError] = useState<Error | null>(null);
279
283
 
280
284
  const load = useCallback(async () => {
@@ -304,6 +308,51 @@ export function useRoutes(options: UseRoutesOptions = {}): UseRoutesResult {
304
308
  }
305
309
  }, [driverId, graphQLOptions, search, status, vehicleId, limit, offset]);
306
310
 
311
+ const loadMore = useCallback(async () => {
312
+ if (!hasMore || isLoading || isLoadingMore) return;
313
+ const nextOffset = offset + routes.length;
314
+ setIsLoadingMore(true);
315
+ setError(null);
316
+ try {
317
+ const data = await executeGraphQL<{ routes: ApiRoute[]; routesCount: number }>(
318
+ graphQLOptions,
319
+ LIST_ROUTES_QUERY,
320
+ {
321
+ filter: {
322
+ driverId,
323
+ vehicleId,
324
+ status: status ? toApiRouteStatus(status) : undefined,
325
+ search: search || undefined,
326
+ },
327
+ pagination: { limit, offset: nextOffset },
328
+ }
329
+ );
330
+ const mapped = data.routes.map(mapRoute);
331
+ setTotalRoutes(data.routesCount);
332
+ setHasMore(nextOffset + mapped.length < data.routesCount);
333
+ setRoutes((previous) => {
334
+ const existingIds = new Set(previous.map((route) => route.id));
335
+ return [...previous, ...mapped.filter((route) => !existingIds.has(route.id))];
336
+ });
337
+ } catch (fetchError) {
338
+ setError(fetchError instanceof Error ? fetchError : new Error('Failed to load more routes'));
339
+ } finally {
340
+ setIsLoadingMore(false);
341
+ }
342
+ }, [
343
+ driverId,
344
+ graphQLOptions,
345
+ hasMore,
346
+ isLoading,
347
+ isLoadingMore,
348
+ limit,
349
+ offset,
350
+ routes.length,
351
+ search,
352
+ status,
353
+ vehicleId,
354
+ ]);
355
+
307
356
  useEffect(() => {
308
357
  void load();
309
358
  }, [load]);
@@ -436,8 +485,10 @@ export function useRoutes(options: UseRoutesOptions = {}): UseRoutesResult {
436
485
  totalRoutes,
437
486
  hasMore,
438
487
  isLoading,
488
+ isLoadingMore,
439
489
  error,
440
490
  refetch,
491
+ loadMore,
441
492
  createRoute,
442
493
  updateRoute,
443
494
  deleteRoute,
@@ -48,6 +48,7 @@ import { CustomerPicker } from '../components/CustomerPicker';
48
48
  import { EntityCombobox } from '../components/EntityCombobox';
49
49
  import { OrderItemRow } from '../components/OrderItemRow';
50
50
  import type { Customer } from '../types';
51
+ import { validateOrderItems, type OrderItemFieldErrors } from '../utils/orderValidation';
51
52
 
52
53
  /**
53
54
  * `<input type="datetime-local">` needs a `YYYY-MM-DDTHH:mm` string in LOCAL
@@ -77,8 +78,12 @@ export default function CreateOrderPage() {
77
78
  customers,
78
79
  createCustomer,
79
80
  isLoading: customersLoading,
81
+ isLoadingMore: customersLoadingMore,
80
82
  error: customersError,
81
83
  refetch: refetchCustomers,
84
+ loadMore: loadMoreCustomers,
85
+ hasMore: customersHaveMore,
86
+ totalCustomers,
82
87
  } = useCustomers({
83
88
  limit: 20,
84
89
  filters: { search: customerSearch || undefined },
@@ -91,13 +96,22 @@ export default function CreateOrderPage() {
91
96
  error: routesError,
92
97
  hasMore: routesHaveMore,
93
98
  totalRoutes,
99
+ loadMore: loadMoreRoutes,
100
+ isLoadingMore: routesLoadingMore,
94
101
  } = useRoutes({
95
102
  status: 'planned',
96
103
  search: routeSearch || undefined,
97
104
  limit: 20,
98
105
  });
99
106
  const [productSearch, setProductSearch] = useState('');
100
- const { products, isLoading: productsLoading } = useProducts({
107
+ const {
108
+ products,
109
+ isLoading: productsLoading,
110
+ isLoadingMore: productsLoadingMore,
111
+ hasMore: productsHaveMore,
112
+ totalProducts,
113
+ loadMore: loadMoreProducts,
114
+ } = useProducts({
101
115
  search: productSearch || undefined,
102
116
  limit: 20,
103
117
  });
@@ -124,6 +138,7 @@ export default function CreateOrderPage() {
124
138
  country: order?.deliveryAddress.country || 'USA',
125
139
  },
126
140
  items: order?.items || [],
141
+ trackingNumber: order?.trackingNumber || '',
127
142
  scheduledDelivery: toDateTimeLocalValue(order?.scheduledDelivery),
128
143
  specialInstructions: order?.specialInstructions || '',
129
144
  });
@@ -137,6 +152,7 @@ export default function CreateOrderPage() {
137
152
 
138
153
  const [isSubmitting, setIsSubmitting] = useState(false);
139
154
  const [formError, setFormError] = useState<string | null>(null);
155
+ const [itemErrors, setItemErrors] = useState<Record<string, OrderItemFieldErrors>>({});
140
156
 
141
157
  useEffect(() => {
142
158
  if (!linkedCustomer) {
@@ -176,6 +192,7 @@ export default function CreateOrderPage() {
176
192
  country: order.deliveryAddress.country || 'USA',
177
193
  },
178
194
  items: order.items || [],
195
+ trackingNumber: order.trackingNumber || '',
179
196
  scheduledDelivery: toDateTimeLocalValue(order.scheduledDelivery),
180
197
  specialInstructions: order.specialInstructions || '',
181
198
  });
@@ -210,6 +227,11 @@ export default function CreateOrderPage() {
210
227
  if (formData.items.length === 0) {
211
228
  return 'Add at least one item to the order.';
212
229
  }
230
+ const nextItemErrors = validateOrderItems(formData.items);
231
+ setItemErrors(nextItemErrors);
232
+ if (Object.keys(nextItemErrors).length > 0) {
233
+ return 'Fix the highlighted shipment item fields before continuing.';
234
+ }
213
235
  if (formData.scheduledDelivery && Number.isNaN(Date.parse(formData.scheduledDelivery))) {
214
236
  return 'Enter a valid scheduled delivery date and time.';
215
237
  }
@@ -251,6 +273,7 @@ export default function CreateOrderPage() {
251
273
  deliveryAddress: formData.deliveryAddress,
252
274
  items: formData.items,
253
275
  // `null` clears an existing slot on edit; the service field is nullable.
276
+ trackingNumber: formData.trackingNumber.trim() || undefined,
254
277
  scheduledDelivery: formData.scheduledDelivery ? new Date(formData.scheduledDelivery) : null,
255
278
  routeId: formData.routeId || undefined,
256
279
  specialInstructions: formData.specialInstructions,
@@ -346,6 +369,14 @@ export default function CreateOrderPage() {
346
369
  };
347
370
 
348
371
  const removeItem = (index: number) => {
372
+ const itemId = formData.items[index]?.id;
373
+ if (itemId) {
374
+ setItemErrors((previous) => {
375
+ const next = { ...previous };
376
+ delete next[itemId];
377
+ return next;
378
+ });
379
+ }
349
380
  setFormData((prev) => ({
350
381
  ...prev,
351
382
  items: prev.items.filter((_, i) => i !== index),
@@ -353,6 +384,14 @@ export default function CreateOrderPage() {
353
384
  };
354
385
 
355
386
  const updateItem = (index: number, field: string, value: unknown) => {
387
+ const itemId = formData.items[index]?.id;
388
+ if (itemId && itemErrors[itemId]) {
389
+ setItemErrors((previous) => {
390
+ const next = { ...previous };
391
+ delete next[itemId];
392
+ return next;
393
+ });
394
+ }
356
395
  setFormData((prev) => ({
357
396
  ...prev,
358
397
  items: prev.items.map((item, i) => (i === index ? { ...item, [field]: value } : item)),
@@ -489,6 +528,10 @@ export default function CreateOrderPage() {
489
528
  onOpenCustomers={goToCustomerCreate}
490
529
  selectedCustomer={selectedCustomer}
491
530
  onSearchQueryChange={setCustomerSearch}
531
+ hasMore={customersHaveMore}
532
+ totalCount={totalCustomers}
533
+ onLoadMore={loadMoreCustomers}
534
+ isLoadingMore={customersLoadingMore}
492
535
  />
493
536
  <div className="space-y-2">
494
537
  <Label htmlFor="priority">Priority</Label>
@@ -555,6 +598,8 @@ export default function CreateOrderPage() {
555
598
  onSearchChange={setRouteSearch}
556
599
  hasMore={routesHaveMore}
557
600
  totalCount={totalRoutes}
601
+ onLoadMore={loadMoreRoutes}
602
+ loadingMore={routesLoadingMore}
558
603
  />
559
604
  <p className="mt-2 text-xs text-text-secondary" role="status">
560
605
  {routesLoading
@@ -782,6 +827,11 @@ export default function CreateOrderPage() {
782
827
  index={index}
783
828
  products={products}
784
829
  productsLoading={productsLoading}
830
+ productsLoadingMore={productsLoadingMore}
831
+ productsHasMore={productsHaveMore}
832
+ productsTotalCount={totalProducts}
833
+ onLoadMoreProducts={loadMoreProducts}
834
+ errors={itemErrors[item.id]}
785
835
  onUpdate={updateItem}
786
836
  onRemove={removeItem}
787
837
  />
@@ -803,6 +853,20 @@ export default function CreateOrderPage() {
803
853
  description="Optional delivery timing and instructions for the carrier."
804
854
  >
805
855
  <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
856
+ <div className="space-y-2">
857
+ <Label htmlFor="trackingNumber">Tracking / reference number</Label>
858
+ <Input
859
+ id="trackingNumber"
860
+ value={formData.trackingNumber}
861
+ onChange={(e: ChangeEvent<HTMLInputElement>) =>
862
+ updateField('trackingNumber', e.target.value)
863
+ }
864
+ placeholder="Optional carrier reference"
865
+ />
866
+ <p className="text-xs text-text-secondary">
867
+ Use this when a carrier or customer already supplied a tracking reference.
868
+ </p>
869
+ </div>
806
870
  <div className="space-y-2">
807
871
  <Label htmlFor="scheduledDelivery">Scheduled delivery</Label>
808
872
  <Input
@@ -817,7 +881,7 @@ export default function CreateOrderPage() {
817
881
  Leave blank to dispatch as soon as a driver is available.
818
882
  </p>
819
883
  </div>
820
- <div className="space-y-2 lg:col-span-2">
884
+ <div className="space-y-2 lg:col-span-1">
821
885
  <Label htmlFor="specialInstructions">Special instructions</Label>
822
886
  <Textarea
823
887
  id="specialInstructions"
@@ -57,6 +57,7 @@ import {
57
57
  type UpdateDriverInput,
58
58
  } from '../hooks/useFleet';
59
59
  import { useDebouncedValue } from '../hooks/useDebouncedValue';
60
+ import { useRefetchOnLeaveEdit } from '../hooks/useRefetchOnLeaveEdit';
60
61
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
61
62
  import { formatCurrency, formatDate } from '../utils/formatters';
62
63
  import { joinPath } from '../utils/navigation';
@@ -132,7 +133,14 @@ function NewDriverForm({ editId }: { editId?: string }) {
132
133
 
133
134
  const [vehicleSearch, setVehicleSearch] = useState('');
134
135
  const debouncedVehicleSearch = useDebouncedValue(vehicleSearch);
135
- const { vehicles, isLoading: vehiclesLoading } = useVehicles({
136
+ const {
137
+ vehicles,
138
+ isLoading: vehiclesLoading,
139
+ isLoadingMore: vehiclesLoadingMore,
140
+ hasMore: vehiclesHaveMore,
141
+ totalVehicles,
142
+ loadMore: loadMoreVehicles,
143
+ } = useVehicles({
136
144
  limit: 20,
137
145
  search: debouncedVehicleSearch || undefined,
138
146
  });
@@ -434,6 +442,10 @@ function NewDriverForm({ editId }: { editId?: string }) {
434
442
  )}
435
443
  selectedOption={selectedVehicle}
436
444
  selectedLoading={selectedVehicleLoading}
445
+ hasMore={vehiclesHaveMore}
446
+ totalCount={totalVehicles}
447
+ onLoadMore={loadMoreVehicles}
448
+ loadingMore={vehiclesLoadingMore}
437
449
  placeholder="Unassigned"
438
450
  emptyText="No vehicles match that search."
439
451
  />
@@ -620,6 +632,7 @@ export default function DriverDetailsPage() {
620
632
  const isEditRoute = useLocation().pathname.endsWith('/edit');
621
633
  const { basePath, navigate } = useMoveTheWheels();
622
634
  const { driver, isLoading, error, refetch } = useDriver(driverId);
635
+ useRefetchOnLeaveEdit(isEditRoute, refetch);
623
636
  const { vehicle: assignedVehicle } = useVehicle(driver?.assignedVehicle);
624
637
 
625
638
  const goBack = () => navigate?.(joinPath(basePath, 'fleet/drivers'));
@@ -299,7 +299,13 @@ export default function DriversPage() {
299
299
  ))}
300
300
  </SelectContent>
301
301
  </Select>
302
- <Select value={licenseClassFilter} onValueChange={setLicenseClassFilter}>
302
+ <Select
303
+ value={licenseClassFilter}
304
+ onValueChange={(value) => {
305
+ setLicenseClassFilter(value);
306
+ setPage(0);
307
+ }}
308
+ >
303
309
  <SelectTrigger className="w-[180px]">
304
310
  <SelectValue placeholder="License class" />
305
311
  </SelectTrigger>
@@ -81,6 +81,8 @@ function formatMoney(
81
81
  export default function MultiModalShipmentPage() {
82
82
  const [pageIndex, setPageIndex] = useState(0);
83
83
  const pageSize = 10;
84
+ const [shipmentNumber, setShipmentNumber] = useState('');
85
+ const [shipmentSearch, setShipmentSearch] = useState('');
84
86
  const {
85
87
  shipments,
86
88
  totalShipments,
@@ -92,9 +94,9 @@ export default function MultiModalShipmentPage() {
92
94
  } = useMultiModalShipments({
93
95
  limit: pageSize,
94
96
  offset: pageIndex * pageSize,
97
+ search: shipmentSearch,
95
98
  });
96
99
 
97
- const [shipmentNumber, setShipmentNumber] = useState('');
98
100
  const [originAddress, setOriginAddress] = useState('');
99
101
  const [destinationAddress, setDestinationAddress] = useState('');
100
102
  const [legs, setLegs] = useState<ShipmentLegDraft[]>([emptyLeg()]);
@@ -529,6 +531,18 @@ export default function MultiModalShipmentPage() {
529
531
  </PageSection>
530
532
 
531
533
  <PageSection title="Planned Multi-Modal Shipments">
534
+ <div className="mb-4 max-w-xl space-y-2">
535
+ <Label htmlFor="planned-shipment-search">Search planned shipments</Label>
536
+ <Input
537
+ id="planned-shipment-search"
538
+ value={shipmentSearch}
539
+ onChange={(event) => {
540
+ setShipmentSearch(event.target.value);
541
+ setPageIndex(0);
542
+ }}
543
+ placeholder="Search by shipment or tracking number"
544
+ />
545
+ </div>
532
546
  {isLoading ? (
533
547
  <LoadingState message="Loading multi-modal shipments..." />
534
548
  ) : error ? (