@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
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
3
- import type { Inventory, InventoryFilter, Product, Warehouse } from '../types';
3
+ import type { Inventory, InventoryFilter, Product, ProductDimensions, Warehouse } from '../types';
4
4
  import { executeGraphQL } from '../utils/graphqlClient';
5
5
 
6
6
  interface UseInventoryOptions {
@@ -13,9 +13,15 @@ interface UseInventoryOptions {
13
13
  export interface CreateProductInput {
14
14
  name: string;
15
15
  sku: string;
16
- category?: string;
17
- brand?: string;
18
- weight?: number;
16
+ description?: string | null;
17
+ category?: string | null;
18
+ brand?: string | null;
19
+ dimensions?: ProductDimensions | null;
20
+ weight?: number | null;
21
+ value?: number | null;
22
+ hazardous?: boolean;
23
+ fragile?: boolean;
24
+ temperatureControlled?: boolean;
19
25
  }
20
26
 
21
27
  export interface CreateWarehouseInput {
@@ -23,14 +29,8 @@ export interface CreateWarehouseInput {
23
29
  name: string;
24
30
  /** WarehouseType enum value, e.g. `DISTRIBUTION`, `FULFILLMENT`. */
25
31
  type: string;
26
- address: {
27
- street: string;
28
- city: string;
29
- state: string;
30
- postalCode: string;
31
- country: string;
32
- };
33
- /** Free-text on the backend, e.g. "ACTIVE", "INACTIVE". Defaults to ACTIVE. */
32
+ address: Record<string, string>;
33
+ /** Free-text on the backend, e.g. "ACTIVE", "INACTIVE". */
34
34
  status?: string;
35
35
  contactInfo?: { email?: string; phone?: string };
36
36
  totalCapacity?: number;
@@ -57,6 +57,7 @@ interface UseInventoryResult {
57
57
  createProduct: (input: CreateProductInput) => Promise<Product>;
58
58
  createWarehouse: (input: CreateWarehouseInput) => Promise<Warehouse>;
59
59
  updateProduct: (id: string, input: CreateProductInput) => Promise<Product>;
60
+ deleteProduct: (id: string) => Promise<void>;
60
61
  updateWarehouse: (id: string, input: CreateWarehouseInput) => Promise<Warehouse>;
61
62
  deleteWarehouse: (id: string) => Promise<void>;
62
63
  }
@@ -299,6 +300,12 @@ const UPDATE_PRODUCT_MUTATION = `
299
300
  }
300
301
  `;
301
302
 
303
+ const DELETE_PRODUCT_MUTATION = `
304
+ mutation DeleteCargoItem($id: ID!) {
305
+ deleteCargoItem(id: $id)
306
+ }
307
+ `;
308
+
302
309
  const CREATE_WAREHOUSE_MUTATION = `
303
310
  mutation CreateWarehouse($input: CreateWarehouseInput!) {
304
311
  createWarehouse(input: $input) {
@@ -357,24 +364,61 @@ const ADJUST_INVENTORY_MUTATION = `
357
364
  }
358
365
  }
359
366
  `;
367
+ const TRANSFER_INVENTORY_STOCK_MUTATION = `
368
+ mutation TransferInventoryStock($input: TransferInventoryStockInput!) {
369
+ transferInventoryStock(input: $input) {
370
+ source {
371
+ id
372
+ cargoItemId
373
+ warehouseId
374
+ onHand
375
+ available
376
+ allocated
377
+ damaged
378
+ reorderPoint
379
+ createdAt
380
+ updatedAt
381
+ }
382
+ destination {
383
+ id
384
+ cargoItemId
385
+ warehouseId
386
+ onHand
387
+ available
388
+ allocated
389
+ damaged
390
+ reorderPoint
391
+ createdAt
392
+ updatedAt
393
+ }
394
+ }
395
+ }
396
+ `;
397
+
398
+ function mapDimensions(
399
+ raw: Record<string, unknown> | null | undefined
400
+ ): ProductDimensions | undefined {
401
+ if (!raw) return undefined;
402
+ const dimensions: ProductDimensions = {};
403
+ for (const key of ['length', 'width', 'height'] as const) {
404
+ const value = raw[key];
405
+ if (typeof value === 'number' && Number.isFinite(value)) dimensions[key] = value;
406
+ }
407
+ if (typeof raw.unit === 'string' && raw.unit.trim()) dimensions.unit = raw.unit;
408
+ return Object.keys(dimensions).length > 0 ? dimensions : undefined;
409
+ }
360
410
 
361
411
  function mapProduct(product: ApiProduct): Product {
362
412
  return {
363
413
  id: product.id,
364
414
  sku: product.sku,
365
415
  name: product.name,
366
- description: product.description ?? '',
367
- category: product.category ?? 'general',
416
+ description: product.description ?? undefined,
417
+ category: product.category ?? undefined,
368
418
  brand: product.brand ?? undefined,
369
- dimensions: {
370
- length: 0,
371
- width: 0,
372
- height: 0,
373
- unit: 'cm',
374
- ...(product.dimensions ?? {}),
375
- } as Product['dimensions'],
376
- weight: product.weight ?? 0,
377
- value: product.value ?? 0,
419
+ dimensions: mapDimensions(product.dimensions),
420
+ weight: product.weight ?? undefined,
421
+ value: product.value ?? undefined,
378
422
  barcode: undefined,
379
423
  images: [],
380
424
  attributes: [],
@@ -386,9 +430,21 @@ function mapProduct(product: ApiProduct): Product {
386
430
  };
387
431
  }
388
432
 
433
+ function mapJsonStringRecord(
434
+ raw: Record<string, unknown> | null | undefined
435
+ ): Record<string, string> {
436
+ if (!raw) return {};
437
+ return Object.fromEntries(
438
+ Object.entries(raw).flatMap(([key, value]) =>
439
+ typeof value === 'string' && value.trim() ? [[key, value]] : []
440
+ )
441
+ );
442
+ }
443
+
389
444
  function mapWarehouse(warehouse: ApiWarehouse): Warehouse {
390
- const totalSpace = warehouse.totalCapacity ?? 0;
391
- const usedSpace = warehouse.usedCapacity ?? 0;
445
+ const totalSpace =
446
+ typeof warehouse.totalCapacity === 'number' ? warehouse.totalCapacity : undefined;
447
+ const usedSpace = typeof warehouse.usedCapacity === 'number' ? warehouse.usedCapacity : undefined;
392
448
  const operatingHoursRaw = warehouse.operatingHours ?? {};
393
449
  const operatingHours: Warehouse['operatingHours'] = {};
394
450
  for (const [day, hours] of Object.entries(operatingHoursRaw)) {
@@ -399,20 +455,17 @@ function mapWarehouse(warehouse: ApiWarehouse): Warehouse {
399
455
  }
400
456
  }
401
457
  const contactInfoRaw = warehouse.contactInfo ?? undefined;
458
+ const utilization =
459
+ totalSpace !== undefined && totalSpace > 0 && usedSpace !== undefined
460
+ ? (usedSpace / totalSpace) * 100
461
+ : undefined;
402
462
  return {
403
463
  id: warehouse.id,
404
464
  name: warehouse.name,
405
465
  code: warehouse.code,
406
- address: {
407
- street: '',
408
- city: '',
409
- state: '',
410
- postalCode: '',
411
- country: '',
412
- ...(warehouse.address ?? {}),
413
- },
414
- type: (warehouse.type?.toLowerCase() ?? 'distribution') as Warehouse['type'],
415
- status: warehouse.status ?? 'ACTIVE',
466
+ address: mapJsonStringRecord(warehouse.address),
467
+ type: warehouse.type.toLowerCase() as Warehouse['type'],
468
+ status: warehouse.status,
416
469
  capacity: {
417
470
  totalSpace,
418
471
  usedSpace,
@@ -420,11 +473,11 @@ function mapWarehouse(warehouse: ApiWarehouse): Warehouse {
420
473
  operatingHours,
421
474
  contactInfo: contactInfoRaw
422
475
  ? {
423
- email: (contactInfoRaw.email as string | undefined) ?? undefined,
424
- phone: (contactInfoRaw.phone as string | undefined) ?? undefined,
476
+ email: typeof contactInfoRaw.email === 'string' ? contactInfoRaw.email : undefined,
477
+ phone: typeof contactInfoRaw.phone === 'string' ? contactInfoRaw.phone : undefined,
425
478
  }
426
479
  : undefined,
427
- utilization: totalSpace > 0 ? (usedSpace / totalSpace) * 100 : 0,
480
+ utilization,
428
481
  createdAt: new Date(warehouse.createdAt),
429
482
  };
430
483
  }
@@ -585,23 +638,41 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
585
638
  productId: string,
586
639
  quantity: number
587
640
  ): Promise<void> => {
641
+ if (!Number.isInteger(quantity) || quantity <= 0) {
642
+ throw new Error('Transfer quantity must be a positive whole number.');
643
+ }
644
+ if (fromWarehouseId === toWarehouseId) {
645
+ throw new Error('Source and destination warehouses must be different.');
646
+ }
588
647
  const source = inventory.find(
589
648
  (item) => item.warehouseId === fromWarehouseId && item.productId === productId
590
649
  );
591
- if (!source) {
592
- throw new Error('Source inventory not found');
593
- }
594
-
595
- await updateStock(source.id, -Math.abs(quantity), 'transfer out');
596
-
650
+ if (!source) throw new Error('Source inventory not found');
597
651
  const destination = inventory.find(
598
652
  (item) => item.warehouseId === toWarehouseId && item.productId === productId
599
653
  );
600
- if (destination) {
601
- await updateStock(destination.id, Math.abs(quantity), 'transfer in');
602
- }
654
+ if (!destination) throw new Error('Destination inventory not found');
655
+
656
+ const data = await executeGraphQL<{
657
+ transferInventoryStock: { source: ApiInventoryItem; destination: ApiInventoryItem };
658
+ }>(graphQLOptions, TRANSFER_INVENTORY_STOCK_MUTATION, {
659
+ input: {
660
+ sourceInventoryItemId: source.id,
661
+ destinationInventoryItemId: destination.id,
662
+ quantity,
663
+ },
664
+ });
665
+ const updatedSource = mapInventory(data.transferInventoryStock.source);
666
+ const updatedDestination = mapInventory(data.transferInventoryStock.destination);
667
+ setInventory((previous) =>
668
+ previous.map((item) => {
669
+ if (item.id === updatedSource.id) return updatedSource;
670
+ if (item.id === updatedDestination.id) return updatedDestination;
671
+ return item;
672
+ })
673
+ );
603
674
  },
604
- [inventory, updateStock]
675
+ [graphQLOptions, inventory]
605
676
  );
606
677
 
607
678
  const createProduct = useCallback(
@@ -613,9 +684,15 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
613
684
  input: {
614
685
  name: input.name,
615
686
  sku: input.sku,
687
+ description: input.description,
616
688
  category: input.category,
617
689
  brand: input.brand,
690
+ dimensions: input.dimensions,
618
691
  weight: input.weight,
692
+ value: input.value,
693
+ hazardous: input.hazardous,
694
+ fragile: input.fragile,
695
+ temperatureControlled: input.temperatureControlled,
619
696
  },
620
697
  }
621
698
  );
@@ -661,9 +738,15 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
661
738
  input: {
662
739
  name: input.name,
663
740
  sku: input.sku,
741
+ description: input.description,
664
742
  category: input.category,
665
743
  brand: input.brand,
744
+ dimensions: input.dimensions,
666
745
  weight: input.weight,
746
+ value: input.value,
747
+ hazardous: input.hazardous,
748
+ fragile: input.fragile,
749
+ temperatureControlled: input.temperatureControlled,
667
750
  },
668
751
  }
669
752
  );
@@ -674,6 +757,16 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
674
757
  [graphQLOptions]
675
758
  );
676
759
 
760
+ const deleteProduct = useCallback(
761
+ async (id: string): Promise<void> => {
762
+ await executeGraphQL<{ deleteCargoItem: boolean }>(graphQLOptions, DELETE_PRODUCT_MUTATION, {
763
+ id,
764
+ });
765
+ setProducts((previous) => previous.filter((item) => item.id !== id));
766
+ },
767
+ [graphQLOptions]
768
+ );
769
+
677
770
  const updateWarehouse = useCallback(
678
771
  async (id: string, input: CreateWarehouseInput): Promise<Warehouse> => {
679
772
  const data = await executeGraphQL<{ updateWarehouse: ApiWarehouse }>(
@@ -725,8 +818,9 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
725
818
  updateStock,
726
819
  transferStock,
727
820
  createProduct,
728
- createWarehouse,
729
821
  updateProduct,
822
+ deleteProduct,
823
+ createWarehouse,
730
824
  updateWarehouse,
731
825
  deleteWarehouse,
732
826
  };
@@ -755,6 +849,7 @@ export function useProducts(options: UseProductsOptions = {}) {
755
849
  const [totalProducts, setTotalProducts] = useState(0);
756
850
  const [hasMore, setHasMore] = useState(false);
757
851
  const [isLoading, setIsLoading] = useState(false);
852
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
758
853
  const [error, setError] = useState<Error | null>(null);
759
854
 
760
855
  const load = useCallback(async () => {
@@ -778,6 +873,45 @@ export function useProducts(options: UseProductsOptions = {}) {
778
873
  }
779
874
  }, [graphQLOptions, limit, offset, search, category]);
780
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
+
781
915
  useEffect(() => {
782
916
  void load();
783
917
  }, [load]);
@@ -787,8 +921,10 @@ export function useProducts(options: UseProductsOptions = {}) {
787
921
  totalProducts,
788
922
  hasMore,
789
923
  isLoading,
924
+ isLoadingMore,
790
925
  error,
791
926
  refetch: () => void load(),
927
+ loadMore,
792
928
  };
793
929
  }
794
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();
@@ -18,10 +18,19 @@ 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
- updateRoute: (id: string, updates: Partial<Route>) => Promise<Route>;
27
+ updateRoute: (
28
+ id: string,
29
+ updates: Omit<Partial<Route>, 'vehicleId' | 'driverId'> & {
30
+ vehicleId?: string | null;
31
+ driverId?: string | null;
32
+ }
33
+ ) => Promise<Route>;
25
34
  deleteRoute: (id: string) => Promise<void>;
26
35
  optimizeRoute: (routeId: string) => Promise<Route>;
27
36
  updateStopStatus: (
@@ -205,13 +214,19 @@ function mapWaypoints(waypoints: unknown): RouteStop[] {
205
214
  state: String(addrObj.state ?? item.state ?? ''),
206
215
  postalCode: String(addrObj.postalCode ?? item.postalCode ?? ''),
207
216
  country: String(addrObj.country ?? item.country ?? ''),
208
- latitude: Number(addrObj.latitude ?? item.latitude ?? item.lat ?? 0),
209
- longitude: Number(addrObj.longitude ?? item.longitude ?? item.lng ?? 0),
217
+ latitude:
218
+ typeof (addrObj.latitude ?? item.latitude ?? item.lat) === 'number'
219
+ ? ((addrObj.latitude ?? item.latitude ?? item.lat) as number)
220
+ : undefined,
221
+ longitude:
222
+ typeof (addrObj.longitude ?? item.longitude ?? item.lng) === 'number'
223
+ ? ((addrObj.longitude ?? item.longitude ?? item.lng) as number)
224
+ : undefined,
210
225
  },
211
226
  type: item.type === 'pickup' ? 'pickup' : 'delivery',
212
- scheduledTime: item.scheduledTime ? new Date(String(item.scheduledTime)) : new Date(),
227
+ scheduledTime: item.scheduledTime ? new Date(String(item.scheduledTime)) : undefined,
213
228
  actualTime: item.actualTime ? new Date(String(item.actualTime)) : undefined,
214
- duration: Number(item.duration ?? 0),
229
+ duration: typeof item.duration === 'number' ? item.duration : undefined,
215
230
  status: (item.status as RouteStop['status']) ?? 'pending',
216
231
  notes: item.notes ? String(item.notes) : undefined,
217
232
  };
@@ -222,8 +237,8 @@ export function mapRoute(route: ApiRoute): Route {
222
237
  return {
223
238
  id: route.id,
224
239
  name: route.name,
225
- driverId: route.driverId ?? '',
226
- vehicleId: route.vehicleId ?? '',
240
+ driverId: route.driverId ?? undefined,
241
+ vehicleId: route.vehicleId ?? undefined,
227
242
  status: mapRouteStatus(route.status),
228
243
  stops: mapWaypoints(route.waypoints),
229
244
  // Prefer the real post-drive distance once it exists; the backend fetches
@@ -232,12 +247,10 @@ export function mapRoute(route: ApiRoute): Route {
232
247
  estimatedDuration: route.estimatedDuration ?? 0,
233
248
  fuelCost: route.fuelCost ?? undefined,
234
249
  actualDuration: route.actualDuration ?? undefined,
235
- // No `optimized` flag exists on the backend — `optimizeRoute` is the only
236
- // path that computes a real `estimatedDistance` (create/update never
237
- // collect one), so a positive value is a reasonably honest proxy. This
238
- // used to be hardcoded `false` and reset on every reload even for a
239
- // route that really had been optimized (BOFF-7909 follow-up).
240
- optimized: (route.estimatedDistance ?? 0) > 0,
250
+ // The route object has no optimization marker. `estimatedDistance` is a
251
+ // measurement, not proof that the optimize mutation ran, so leave this
252
+ // unknown rather than presenting an inferred Yes/No.
253
+ optimized: undefined,
241
254
  createdAt: new Date(route.createdAt),
242
255
  };
243
256
  }
@@ -265,6 +278,7 @@ export function useRoutes(options: UseRoutesOptions = {}): UseRoutesResult {
265
278
  const [totalRoutes, setTotalRoutes] = useState(0);
266
279
  const [hasMore, setHasMore] = useState(false);
267
280
  const [isLoading, setIsLoading] = useState(false);
281
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
268
282
  const [error, setError] = useState<Error | null>(null);
269
283
 
270
284
  const load = useCallback(async () => {
@@ -294,6 +308,51 @@ export function useRoutes(options: UseRoutesOptions = {}): UseRoutesResult {
294
308
  }
295
309
  }, [driverId, graphQLOptions, search, status, vehicleId, limit, offset]);
296
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
+
297
356
  useEffect(() => {
298
357
  void load();
299
358
  }, [load]);
@@ -334,7 +393,13 @@ export function useRoutes(options: UseRoutesOptions = {}): UseRoutesResult {
334
393
  );
335
394
 
336
395
  const updateRoute = useCallback(
337
- async (id: string, updates: Partial<Route>): Promise<Route> => {
396
+ async (
397
+ id: string,
398
+ updates: Omit<Partial<Route>, 'vehicleId' | 'driverId'> & {
399
+ vehicleId?: string | null;
400
+ driverId?: string | null;
401
+ }
402
+ ): Promise<Route> => {
338
403
  const data = await executeGraphQL<{ updateRoute: ApiRoute }>(
339
404
  graphQLOptions,
340
405
  UPDATE_ROUTE_MUTATION,
@@ -344,8 +409,12 @@ export function useRoutes(options: UseRoutesOptions = {}): UseRoutesResult {
344
409
  name: updates.name,
345
410
  status: toApiRouteStatus(updates.status),
346
411
  shipmentId: undefined,
347
- vehicleId: updates.vehicleId,
348
- driverId: updates.driverId,
412
+ vehicleId: Object.prototype.hasOwnProperty.call(updates, 'vehicleId')
413
+ ? (updates.vehicleId ?? null)
414
+ : undefined,
415
+ driverId: Object.prototype.hasOwnProperty.call(updates, 'driverId')
416
+ ? (updates.driverId ?? null)
417
+ : undefined,
349
418
  waypoints: updates.stops,
350
419
  // See createRoute — the edit form doesn't collect a distance
351
420
  // either, and `distance` must never masquerade as `actualDistance`.
@@ -416,8 +485,10 @@ export function useRoutes(options: UseRoutesOptions = {}): UseRoutesResult {
416
485
  totalRoutes,
417
486
  hasMore,
418
487
  isLoading,
488
+ isLoadingMore,
419
489
  error,
420
490
  refetch,
491
+ loadMore,
421
492
  createRoute,
422
493
  updateRoute,
423
494
  deleteRoute,
@@ -209,11 +209,23 @@ const TRACKING_ALERTS_QUERY = `
209
209
  }
210
210
  `;
211
211
 
212
- function mapTrackingEventToLocation(event: ApiTrackingEvent): Location {
212
+ function mapTrackingEventToLocation(event: ApiTrackingEvent): Location | null {
213
+ const raw = event.location;
214
+ if (!raw) return null;
215
+ const latitude = raw.latitude ?? raw.lat;
216
+ const longitude = raw.longitude ?? raw.lng;
217
+ if (
218
+ typeof latitude !== 'number' ||
219
+ !Number.isFinite(latitude) ||
220
+ typeof longitude !== 'number' ||
221
+ !Number.isFinite(longitude)
222
+ ) {
223
+ return null;
224
+ }
213
225
  return {
214
- latitude: Number((event.location?.latitude as number | undefined) ?? 0),
215
- longitude: Number((event.location?.longitude as number | undefined) ?? 0),
216
- address: (event.location?.address as string | undefined) ?? undefined,
226
+ latitude,
227
+ longitude,
228
+ address: typeof raw.address === 'string' ? raw.address : undefined,
217
229
  timestamp: new Date(event.timestamp),
218
230
  };
219
231
  }
@@ -225,9 +237,10 @@ function mapTrackingEventsToTracking(
225
237
  const latest = events[0];
226
238
  if (!latest) return null;
227
239
 
228
- const locationHistory = events.map(mapTrackingEventToLocation);
240
+ const locationHistory = events
241
+ .map(mapTrackingEventToLocation)
242
+ .filter((location): location is Location => location !== null);
229
243
  const currentLocation = locationHistory[0];
230
- if (!currentLocation) return null;
231
244
 
232
245
  return {
233
246
  id: latest.id,
@@ -307,11 +320,7 @@ export function useTracking(options: UseTrackingOptions = {}): UseTrackingResult
307
320
  id: shipment.id,
308
321
  entityId: entityId ?? shipment.deliveryJobId ?? shipment.id,
309
322
  entityType: entityType ?? 'order',
310
- currentLocation: {
311
- latitude: 0,
312
- longitude: 0,
313
- timestamp: new Date(),
314
- },
323
+ currentLocation: undefined,
315
324
  locationHistory: [],
316
325
  status: shipment.status.toLowerCase(),
317
326
  estimatedArrival: shipment.estimatedDelivery
@@ -319,7 +328,7 @@ export function useTracking(options: UseTrackingOptions = {}): UseTrackingResult
319
328
  : undefined,
320
329
  nextCheckpoint: shipment.shipmentNumber,
321
330
  alerts: [],
322
- lastUpdated: new Date(),
331
+ lastUpdated: undefined,
323
332
  }));
324
333
 
325
334
  setTracking(mapped);
@@ -518,7 +527,7 @@ export function useVehicleTracking(vehicleId: string | undefined) {
518
527
  currentLocation: vehicle.currentLocation,
519
528
  locationHistory: [vehicle.currentLocation],
520
529
  status: vehicle.status,
521
- lastUpdated: vehicle.currentLocation.timestamp ?? new Date(),
530
+ lastUpdated: vehicle.currentLocation.timestamp,
522
531
  alerts: [],
523
532
  };
524
533
  }, [vehicle]);
@@ -538,7 +547,7 @@ export function useDriverTracking(driverId: string | undefined) {
538
547
  currentLocation: driver.currentLocation,
539
548
  locationHistory: [driver.currentLocation],
540
549
  status: driver.status,
541
- lastUpdated: driver.currentLocation.timestamp ?? new Date(),
550
+ lastUpdated: driver.currentLocation.timestamp,
542
551
  alerts: [],
543
552
  };
544
553
  }, [driver]);
@@ -622,7 +631,6 @@ export function useTrackingAlerts() {
622
631
  type: 'delay',
623
632
  severity: 'medium',
624
633
  message: `${alert.shipmentNumber}: ${alert.message}`,
625
- timestamp: new Date(),
626
634
  acknowledged: false,
627
635
  }))
628
636
  );