@burdenoff/microfe-movethewheels 2026.920.3 → 2026.923.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/components/CarbonFootprintPanel.js +1 -1
  2. package/dist/components/CustomerPicker.js +76 -65
  3. package/dist/components/DataTable.js +140 -139
  4. package/dist/components/DocumentsCard.js +14 -8
  5. package/dist/generated/wspace-types.d.ts +2719 -8
  6. package/dist/hooks/index.js +1 -1
  7. package/dist/hooks/useAnalytics.js +21 -1
  8. package/dist/hooks/useCustomers.js +94 -78
  9. package/dist/hooks/useDebouncedValue.js +11 -0
  10. package/dist/hooks/useEntityDocuments.js +28 -18
  11. package/dist/hooks/useEpod.js +24 -17
  12. package/dist/hooks/useFleet.js +148 -125
  13. package/dist/hooks/useInventory.js +255 -186
  14. package/dist/hooks/useMultiModalShipments.js +35 -27
  15. package/dist/hooks/useOrders.js +139 -134
  16. package/dist/hooks/useRoutes.js +97 -75
  17. package/dist/hooks/useTracking.js +100 -88
  18. package/dist/index.js +6 -6
  19. package/dist/movethewheels/components/CustomerPicker.d.ts +3 -1
  20. package/dist/movethewheels/components/DataTable.d.ts +5 -1
  21. package/dist/movethewheels/hooks/useCustomers.d.ts +0 -1
  22. package/dist/movethewheels/hooks/useDebouncedValue.d.ts +1 -0
  23. package/dist/movethewheels/hooks/useEntityDocuments.d.ts +2 -0
  24. package/dist/movethewheels/hooks/useEpod.d.ts +6 -1
  25. package/dist/movethewheels/hooks/useFleet.d.ts +49 -5
  26. package/dist/movethewheels/hooks/useInventory.d.ts +24 -17
  27. package/dist/movethewheels/hooks/useMultiModalShipments.d.ts +6 -1
  28. package/dist/movethewheels/hooks/useOrders.d.ts +48 -5
  29. package/dist/movethewheels/hooks/useRoutes.d.ts +18 -1
  30. package/dist/movethewheels/hooks/useTracking.d.ts +12 -11
  31. package/dist/movethewheels/types/index.d.ts +12 -5
  32. package/dist/pages/CreateOrderPage.js +191 -129
  33. package/dist/pages/CustomersPage.js +61 -54
  34. package/dist/pages/DeliveryCapturePage.js +131 -109
  35. package/dist/pages/DriversPage.js +100 -92
  36. package/dist/pages/InventoryPage.js +239 -236
  37. package/dist/pages/MultiModalShipmentPage.js +163 -130
  38. package/dist/pages/OrderDetailsPage.js +190 -106
  39. package/dist/pages/OrdersListPage.js +66 -60
  40. package/dist/pages/ProductDetailsPage.js +241 -200
  41. package/dist/pages/ProductsPage.js +59 -52
  42. package/dist/pages/RouteDetailsPage.js +324 -293
  43. package/dist/pages/RoutesPage.js +55 -48
  44. package/dist/pages/VehiclesPage.js +63 -56
  45. package/dist/pages/WarehousesPage.js +60 -53
  46. package/package.json +1 -1
  47. package/src/generated/wspace-types.ts +3136 -8
  48. package/src/movethewheels/components/CarbonFootprintPanel.tsx +0 -1
  49. package/src/movethewheels/components/CustomerPicker.tsx +14 -0
  50. package/src/movethewheels/components/DataTable.tsx +34 -18
  51. package/src/movethewheels/components/DocumentsCard.tsx +6 -1
  52. package/src/movethewheels/components/maps/leaflet-setup.ts +1 -8
  53. package/src/movethewheels/hooks/useAnalytics.ts +27 -0
  54. package/src/movethewheels/hooks/useCustomers.ts +64 -24
  55. package/src/movethewheels/hooks/useDebouncedValue.ts +12 -0
  56. package/src/movethewheels/hooks/useEntityDocuments.ts +43 -25
  57. package/src/movethewheels/hooks/useEpod.ts +14 -7
  58. package/src/movethewheels/hooks/useFleet.ts +66 -38
  59. package/src/movethewheels/hooks/useFleetIntelligence.ts +1 -4
  60. package/src/movethewheels/hooks/useInventory.ts +211 -90
  61. package/src/movethewheels/hooks/useMultiModalShipments.ts +15 -11
  62. package/src/movethewheels/hooks/useOrders.ts +48 -14
  63. package/src/movethewheels/hooks/useRoutes.ts +81 -24
  64. package/src/movethewheels/hooks/useTracking.ts +130 -98
  65. package/src/movethewheels/pages/CreateOrderPage.tsx +93 -2
  66. package/src/movethewheels/pages/CustomersPage.tsx +11 -1
  67. package/src/movethewheels/pages/DeliveryCapturePage.tsx +31 -1
  68. package/src/movethewheels/pages/DriversPage.tsx +24 -12
  69. package/src/movethewheels/pages/InventoryPage.tsx +57 -32
  70. package/src/movethewheels/pages/MultiModalShipmentPage.tsx +52 -3
  71. package/src/movethewheels/pages/OrderDetailsPage.tsx +91 -6
  72. package/src/movethewheels/pages/OrdersListPage.tsx +20 -3
  73. package/src/movethewheels/pages/ProductDetailsPage.tsx +52 -3
  74. package/src/movethewheels/pages/ProductsPage.tsx +11 -1
  75. package/src/movethewheels/pages/RouteDetailsPage.tsx +45 -5
  76. package/src/movethewheels/pages/RoutesPage.tsx +11 -1
  77. package/src/movethewheels/pages/VehiclesPage.tsx +19 -2
  78. package/src/movethewheels/pages/WarehousesPage.tsx +11 -1
  79. package/src/movethewheels/types/index.ts +12 -5
@@ -5,6 +5,9 @@ import { executeGraphQL } from '../utils/graphqlClient';
5
5
 
6
6
  interface UseInventoryOptions {
7
7
  filters?: InventoryFilter;
8
+ search?: string;
9
+ limit?: number;
10
+ offset?: number;
8
11
  }
9
12
 
10
13
  export interface CreateProductInput {
@@ -32,9 +35,11 @@ export interface CreateWarehouseInput {
32
35
 
33
36
  interface UseInventoryResult {
34
37
  inventory: Inventory[];
38
+ lowStockItems: Inventory[];
35
39
  products: Product[];
36
40
  warehouses: Warehouse[];
37
41
  totalItems: number;
42
+ hasMore: boolean;
38
43
  isLoading: boolean;
39
44
  error: Error | null;
40
45
  refetch: () => void;
@@ -68,6 +73,8 @@ interface ApiInventoryItem {
68
73
  reorderPoint?: number | null;
69
74
  createdAt: string;
70
75
  updatedAt: string;
76
+ cargoItem?: { id: string; name: string; sku: string } | null;
77
+ warehouse?: { id: string; name: string; code: string } | null;
71
78
  }
72
79
 
73
80
  interface ApiProduct {
@@ -111,7 +118,18 @@ const LIST_INVENTORY_QUERY = `
111
118
  reorderPoint
112
119
  createdAt
113
120
  updatedAt
121
+ cargoItem {
122
+ id
123
+ name
124
+ sku
125
+ }
126
+ warehouse {
127
+ id
128
+ name
129
+ code
130
+ }
114
131
  }
132
+ inventoryItemsCount(filter: $filter)
115
133
  }
116
134
  `;
117
135
 
@@ -133,6 +151,7 @@ const LIST_PRODUCTS_QUERY = `
133
151
  createdAt
134
152
  updatedAt
135
153
  }
154
+ cargoItemsCount(filter: $filter)
136
155
  }
137
156
  `;
138
157
 
@@ -148,6 +167,7 @@ const LIST_WAREHOUSES_QUERY = `
148
167
  usedCapacity
149
168
  createdAt
150
169
  }
170
+ warehousesCount(filter: $filter)
151
171
  }
152
172
  `;
153
173
 
@@ -386,7 +406,10 @@ function mapInventory(item: ApiInventoryItem): Inventory {
386
406
  return {
387
407
  id: item.id,
388
408
  productId: item.cargoItemId,
409
+ productName: item.cargoItem?.name,
410
+ productSku: item.cargoItem?.sku,
389
411
  warehouseId: item.warehouseId,
412
+ warehouseName: item.warehouse?.name,
390
413
  zoneId: '',
391
414
  quantity: item.onHand,
392
415
  reservedQuantity: item.allocated,
@@ -423,18 +446,20 @@ function useGraphqlOptions() {
423
446
  }
424
447
 
425
448
  export function useInventory(options: UseInventoryOptions = {}): UseInventoryResult {
426
- const { filters: rawFilters } = options;
449
+ const { filters: rawFilters, search, limit = 10, offset = 0 } = options;
427
450
  // Stabilize the filters reference by its primitive fields so React Compiler's
428
- // inferred `filters` dependency stays stable and `load` is not recreated (and
429
- // refetched) every render when the caller passes an inline filters object.
451
+ // inferred `filters` dependency stays stable (callers commonly pass inline objects).
430
452
  const filters = useMemo(
431
453
  () => rawFilters,
432
- [rawFilters?.lowStock, rawFilters?.productId, rawFilters?.warehouseId]
454
+ [rawFilters?.lowStock, rawFilters?.productId, rawFilters?.search, rawFilters?.warehouseId]
433
455
  );
434
456
  const graphQLOptions = useGraphqlOptions();
435
457
  const [inventory, setInventory] = useState<Inventory[]>([]);
458
+ const [lowStockItems, setLowStockItems] = useState<Inventory[]>([]);
436
459
  const [products, setProducts] = useState<Product[]>([]);
437
460
  const [warehouses, setWarehouses] = useState<Warehouse[]>([]);
461
+ const [totalItems, setTotalItems] = useState(0);
462
+ const [hasMore, setHasMore] = useState(false);
438
463
  const [isLoading, setIsLoading] = useState(false);
439
464
  const [error, setError] = useState<Error | null>(null);
440
465
 
@@ -442,7 +467,21 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
442
467
  setIsLoading(true);
443
468
  setError(null);
444
469
  try {
445
- const [inventoryData, productData, warehouseData] = await Promise.all([
470
+ const inventoryFilter = {
471
+ warehouseId: filters?.warehouseId,
472
+ cargoItemId: filters?.productId,
473
+ search: search || filters?.search || undefined,
474
+ lowStock: filters?.lowStock || undefined,
475
+ };
476
+ const [inventoryData, lowStockData, productData, warehouseData] = await Promise.all([
477
+ executeGraphQL<{ inventoryItems: ApiInventoryItem[]; inventoryItemsCount: number }>(
478
+ graphQLOptions,
479
+ LIST_INVENTORY_QUERY,
480
+ {
481
+ filter: inventoryFilter,
482
+ pagination: { limit, offset },
483
+ }
484
+ ),
446
485
  executeGraphQL<{ inventoryItems: ApiInventoryItem[] }>(
447
486
  graphQLOptions,
448
487
  LIST_INVENTORY_QUERY,
@@ -450,24 +489,23 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
450
489
  filter: {
451
490
  warehouseId: filters?.warehouseId,
452
491
  cargoItemId: filters?.productId,
492
+ lowStock: true,
453
493
  },
454
- pagination: { limit: 500, offset: 0 },
494
+ pagination: { limit: 5, offset: 0 },
455
495
  }
456
496
  ),
457
497
  executeGraphQL<{ cargoItems: ApiProduct[] }>(graphQLOptions, LIST_PRODUCTS_QUERY, {
458
- pagination: { limit: 500, offset: 0 },
498
+ pagination: { limit: 5, offset: 0 },
459
499
  }),
460
500
  executeGraphQL<{ warehouses: ApiWarehouse[] }>(graphQLOptions, LIST_WAREHOUSES_QUERY, {
461
- pagination: { limit: 500, offset: 0 },
501
+ pagination: { limit: 5, offset: 0 },
462
502
  }),
463
503
  ]);
464
504
 
465
- let nextInventory = inventoryData.inventoryItems.map(mapInventory);
466
- if (filters?.lowStock) {
467
- nextInventory = nextInventory.filter((item) => item.availableQuantity <= item.reorderLevel);
468
- }
469
-
470
- setInventory(nextInventory);
505
+ setInventory(inventoryData.inventoryItems.map(mapInventory));
506
+ setLowStockItems(lowStockData.inventoryItems.map(mapInventory));
507
+ setTotalItems(inventoryData.inventoryItemsCount);
508
+ setHasMore(offset + inventoryData.inventoryItems.length < inventoryData.inventoryItemsCount);
471
509
  setProducts(productData.cargoItems.map(mapProduct));
472
510
  setWarehouses(warehouseData.warehouses.map(mapWarehouse));
473
511
  } catch (fetchError) {
@@ -477,7 +515,7 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
477
515
  } finally {
478
516
  setIsLoading(false);
479
517
  }
480
- }, [filters, graphQLOptions]);
518
+ }, [filters, graphQLOptions, limit, offset, search]);
481
519
 
482
520
  useEffect(() => {
483
521
  void load();
@@ -635,9 +673,11 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
635
673
 
636
674
  return {
637
675
  inventory,
676
+ lowStockItems,
638
677
  products,
639
678
  warehouses,
640
- totalItems: inventory.length,
679
+ totalItems,
680
+ hasMore,
641
681
  isLoading,
642
682
  error,
643
683
  refetch,
@@ -655,10 +695,20 @@ interface UseListOptions {
655
695
  offset?: number;
656
696
  }
657
697
 
658
- export function useProducts(options: UseListOptions = {}) {
659
- const { limit = 500, offset = 0 } = options;
698
+ interface UseProductsOptions extends UseListOptions {
699
+ search?: string;
700
+ category?: string;
701
+ }
702
+
703
+ interface UseWarehousesOptions extends UseListOptions {
704
+ search?: string;
705
+ }
706
+
707
+ export function useProducts(options: UseProductsOptions = {}) {
708
+ const { limit = 20, offset = 0, search, category } = options;
660
709
  const graphQLOptions = useGraphqlOptions();
661
710
  const [products, setProducts] = useState<Product[]>([]);
711
+ const [totalProducts, setTotalProducts] = useState(0);
662
712
  const [hasMore, setHasMore] = useState(false);
663
713
  const [isLoading, setIsLoading] = useState(false);
664
714
  const [error, setError] = useState<Error | null>(null);
@@ -667,21 +717,22 @@ export function useProducts(options: UseListOptions = {}) {
667
717
  setIsLoading(true);
668
718
  setError(null);
669
719
  try {
670
- const data = await executeGraphQL<{ cargoItems: ApiProduct[] }>(
671
- graphQLOptions,
672
- LIST_PRODUCTS_QUERY,
673
- {
674
- pagination: { limit, offset },
675
- }
676
- );
677
- setHasMore(data.cargoItems.length === limit);
720
+ const data = await executeGraphQL<{
721
+ cargoItems: ApiProduct[];
722
+ cargoItemsCount: number;
723
+ }>(graphQLOptions, LIST_PRODUCTS_QUERY, {
724
+ filter: { search: search || undefined, category: category || undefined },
725
+ pagination: { limit, offset },
726
+ });
727
+ setHasMore(offset + data.cargoItems.length < data.cargoItemsCount);
728
+ setTotalProducts(data.cargoItemsCount);
678
729
  setProducts(data.cargoItems.map(mapProduct));
679
730
  } catch (fetchError) {
680
731
  setError(fetchError instanceof Error ? fetchError : new Error('Failed to load products'));
681
732
  } finally {
682
733
  setIsLoading(false);
683
734
  }
684
- }, [graphQLOptions, limit, offset]);
735
+ }, [graphQLOptions, limit, offset, search, category]);
685
736
 
686
737
  useEffect(() => {
687
738
  void load();
@@ -689,7 +740,7 @@ export function useProducts(options: UseListOptions = {}) {
689
740
 
690
741
  return {
691
742
  products,
692
- totalProducts: products.length,
743
+ totalProducts,
693
744
  hasMore,
694
745
  isLoading,
695
746
  error,
@@ -697,22 +748,43 @@ export function useProducts(options: UseListOptions = {}) {
697
748
  };
698
749
  }
699
750
 
700
- /**
701
- * Bounded aggregate for the Products page "at a glance" tiles. Fetches up to
702
- * `sampleSize` products (independent of the paginated list) and computes the
703
- * special-handling / temperature-controlled counts so the tiles never reflect
704
- * a single truncated page. There is no product *Count field on the frozen
705
- * supergraph, so this bounded sample is the aggregate source.
706
- */
707
- export function useProductStats(sampleSize = 500) {
708
- const { products, isLoading, error } = useProducts({ limit: sampleSize, offset: 0 });
751
+ const PRODUCT_STATS_QUERY = `
752
+ query CargoItemStats {
753
+ cargoItemStats {
754
+ total
755
+ specialHandling
756
+ temperatureControlled
757
+ }
758
+ }
759
+ `;
760
+
761
+ export function useProductStats() {
762
+ const graphQLOptions = useGraphqlOptions();
763
+ const [stats, setStats] = useState({ total: 0, specialHandling: 0, temperatureControlled: 0 });
764
+ const [isLoading, setIsLoading] = useState(false);
765
+ const [error, setError] = useState<Error | null>(null);
766
+
767
+ useEffect(() => {
768
+ const load = async () => {
769
+ setIsLoading(true);
770
+ setError(null);
771
+ try {
772
+ const data = await executeGraphQL<{ cargoItemStats: typeof stats }>(
773
+ graphQLOptions,
774
+ PRODUCT_STATS_QUERY
775
+ );
776
+ setStats(data.cargoItemStats);
777
+ } catch (fetchError) {
778
+ setError(
779
+ fetchError instanceof Error ? fetchError : new Error('Failed to load product stats')
780
+ );
781
+ } finally {
782
+ setIsLoading(false);
783
+ }
784
+ };
709
785
 
710
- const stats = useMemo(() => {
711
- const total = products.length;
712
- const specialHandling = products.filter((item) => item.fragile || item.hazardous).length;
713
- const temperatureControlled = products.filter((item) => item.temperatureControlled).length;
714
- return { total, specialHandling, temperatureControlled };
715
- }, [products]);
786
+ void load();
787
+ }, [graphQLOptions]);
716
788
 
717
789
  return { stats, isLoading, error };
718
790
  }
@@ -753,10 +825,11 @@ export function useProduct(productId: string | undefined) {
753
825
  return { product, isLoading, error, refetch: () => void load() };
754
826
  }
755
827
 
756
- export function useWarehouses(options: UseListOptions = {}) {
757
- const { limit = 500, offset = 0 } = options;
828
+ export function useWarehouses(options: UseWarehousesOptions = {}) {
829
+ const { limit = 20, offset = 0, search } = options;
758
830
  const graphQLOptions = useGraphqlOptions();
759
831
  const [warehouses, setWarehouses] = useState<Warehouse[]>([]);
832
+ const [totalWarehouses, setTotalWarehouses] = useState(0);
760
833
  const [hasMore, setHasMore] = useState(false);
761
834
  const [isLoading, setIsLoading] = useState(false);
762
835
  const [error, setError] = useState<Error | null>(null);
@@ -765,21 +838,22 @@ export function useWarehouses(options: UseListOptions = {}) {
765
838
  setIsLoading(true);
766
839
  setError(null);
767
840
  try {
768
- const data = await executeGraphQL<{ warehouses: ApiWarehouse[] }>(
769
- graphQLOptions,
770
- LIST_WAREHOUSES_QUERY,
771
- {
772
- pagination: { limit, offset },
773
- }
774
- );
775
- setHasMore(data.warehouses.length === limit);
841
+ const data = await executeGraphQL<{
842
+ warehouses: ApiWarehouse[];
843
+ warehousesCount: number;
844
+ }>(graphQLOptions, LIST_WAREHOUSES_QUERY, {
845
+ filter: { search: search || undefined },
846
+ pagination: { limit, offset },
847
+ });
848
+ setTotalWarehouses(data.warehousesCount);
849
+ setHasMore(offset + data.warehouses.length < data.warehousesCount);
776
850
  setWarehouses(data.warehouses.map(mapWarehouse));
777
851
  } catch (fetchError) {
778
852
  setError(fetchError instanceof Error ? fetchError : new Error('Failed to load warehouses'));
779
853
  } finally {
780
854
  setIsLoading(false);
781
855
  }
782
- }, [graphQLOptions, limit, offset]);
856
+ }, [graphQLOptions, limit, offset, search]);
783
857
 
784
858
  useEffect(() => {
785
859
  void load();
@@ -787,7 +861,7 @@ export function useWarehouses(options: UseListOptions = {}) {
787
861
 
788
862
  return {
789
863
  warehouses,
790
- totalWarehouses: warehouses.length,
864
+ totalWarehouses,
791
865
  hasMore,
792
866
  isLoading,
793
867
  error,
@@ -795,30 +869,43 @@ export function useWarehouses(options: UseListOptions = {}) {
795
869
  };
796
870
  }
797
871
 
798
- /**
799
- * Bounded aggregate for the Warehouses page "at a glance" tiles — fetches up to
800
- * `sampleSize` warehouses (independent of the paginated list) so the active /
801
- * avg-utilization tiles never reflect a single truncated page.
802
- */
803
- export function useWarehouseStats(sampleSize = 500) {
804
- const { warehouses, isLoading, error } = useWarehouses({ limit: sampleSize, offset: 0 });
805
-
806
- const stats = useMemo(() => {
807
- const total = warehouses.length;
808
- const active = warehouses.filter((item) => item.isActive).length;
809
- const avgUtilization =
810
- total > 0
811
- ? warehouses.reduce(
812
- (sum, item) =>
813
- sum +
814
- (item.capacity.totalSpace > 0
815
- ? item.capacity.usedSpace / item.capacity.totalSpace
816
- : 0),
817
- 0
818
- ) / total
819
- : 0;
820
- return { total, active, avgUtilization };
821
- }, [warehouses]);
872
+ const WAREHOUSE_STATS_QUERY = `
873
+ query WarehouseStats {
874
+ warehouseStats {
875
+ total
876
+ active
877
+ avgUtilization
878
+ }
879
+ }
880
+ `;
881
+
882
+ export function useWarehouseStats() {
883
+ const graphQLOptions = useGraphqlOptions();
884
+ const [stats, setStats] = useState({ total: 0, active: 0, avgUtilization: 0 });
885
+ const [isLoading, setIsLoading] = useState(false);
886
+ const [error, setError] = useState<Error | null>(null);
887
+
888
+ useEffect(() => {
889
+ const load = async () => {
890
+ setIsLoading(true);
891
+ setError(null);
892
+ try {
893
+ const data = await executeGraphQL<{ warehouseStats: typeof stats }>(
894
+ graphQLOptions,
895
+ WAREHOUSE_STATS_QUERY
896
+ );
897
+ setStats(data.warehouseStats);
898
+ } catch (fetchError) {
899
+ setError(
900
+ fetchError instanceof Error ? fetchError : new Error('Failed to load warehouse stats')
901
+ );
902
+ } finally {
903
+ setIsLoading(false);
904
+ }
905
+ };
906
+
907
+ void load();
908
+ }, [graphQLOptions]);
822
909
 
823
910
  return { stats, isLoading, error };
824
911
  }
@@ -941,8 +1028,8 @@ export function useInventoryStats() {
941
1028
  * the row instead of being joined against a separately-paged warehouse list.
942
1029
  */
943
1030
  const PRODUCT_STOCK_LEVELS_QUERY = `
944
- query GetCargoItemStockLevels($cargoItemId: ID!) {
945
- inventoryItems(filter: { cargoItemId: $cargoItemId }, pagination: { limit: 200, offset: 0 }) {
1031
+ query GetCargoItemStockLevels($cargoItemId: ID!, $pagination: PaginationInput) {
1032
+ inventoryItems(filter: { cargoItemId: $cargoItemId }, pagination: $pagination) {
946
1033
  id
947
1034
  onHand
948
1035
  available
@@ -955,6 +1042,10 @@ const PRODUCT_STOCK_LEVELS_QUERY = `
955
1042
  code
956
1043
  }
957
1044
  }
1045
+ inventoryItemsCount(filter: { cargoItemId: $cargoItemId })
1046
+ inventoryStats(filter: { cargoItemId: $cargoItemId }) {
1047
+ totalAvailable
1048
+ }
958
1049
  }
959
1050
  `;
960
1051
 
@@ -980,28 +1071,48 @@ interface ApiProductStockLevel {
980
1071
  warehouse?: { id: string; name: string; code: string } | null;
981
1072
  }
982
1073
 
1074
+ interface ApiProductStockSummary {
1075
+ totalAvailable: number;
1076
+ }
1077
+
1078
+ interface ProductStockLevelOptions {
1079
+ limit?: number;
1080
+ offset?: number;
1081
+ }
1082
+
983
1083
  /** Where a catalog item is stocked, and how much of it is free to promise. */
984
- export function useProductStockLevels(productId: string | undefined) {
1084
+ export function useProductStockLevels(
1085
+ productId: string | undefined,
1086
+ options: ProductStockLevelOptions = {}
1087
+ ) {
1088
+ const { limit = 20, offset = 0 } = options;
985
1089
  const graphQLOptions = useGraphqlOptions();
986
1090
  const [levels, setLevels] = useState<ProductStockLevel[]>([]);
1091
+ const [totalLevels, setTotalLevels] = useState(0);
1092
+ const [totalAvailable, setTotalAvailable] = useState(0);
987
1093
  const [isLoading, setIsLoading] = useState(false);
988
1094
  const [error, setError] = useState<Error | null>(null);
989
1095
 
990
1096
  const load = useCallback(async () => {
991
1097
  if (!productId) {
992
1098
  setLevels([]);
1099
+ setTotalLevels(0);
1100
+ setTotalAvailable(0);
993
1101
  return;
994
1102
  }
995
1103
  setIsLoading(true);
996
1104
  setError(null);
997
1105
  try {
998
- const data = await executeGraphQL<{ inventoryItems: ApiProductStockLevel[] }>(
999
- graphQLOptions,
1000
- PRODUCT_STOCK_LEVELS_QUERY,
1001
- { cargoItemId: productId }
1002
- );
1106
+ const data = await executeGraphQL<{
1107
+ inventoryItems: ApiProductStockLevel[];
1108
+ inventoryItemsCount: number;
1109
+ inventoryStats: ApiProductStockSummary;
1110
+ }>(graphQLOptions, PRODUCT_STOCK_LEVELS_QUERY, {
1111
+ cargoItemId: productId,
1112
+ pagination: { limit, offset },
1113
+ });
1003
1114
  setLevels(
1004
- data.inventoryItems.map((item) => ({
1115
+ (data.inventoryItems ?? []).map((item) => ({
1005
1116
  inventoryItemId: item.id,
1006
1117
  warehouseId: item.warehouse?.id ?? '',
1007
1118
  warehouseName: item.warehouse?.name ?? 'Unknown warehouse',
@@ -1013,12 +1124,14 @@ export function useProductStockLevels(productId: string | undefined) {
1013
1124
  reorderPoint: item.reorderPoint ?? undefined,
1014
1125
  }))
1015
1126
  );
1127
+ setTotalLevels(data.inventoryItemsCount ?? 0);
1128
+ setTotalAvailable(data.inventoryStats?.totalAvailable ?? 0);
1016
1129
  } catch (fetchError) {
1017
1130
  setError(fetchError instanceof Error ? fetchError : new Error('Failed to load stock levels'));
1018
1131
  } finally {
1019
1132
  setIsLoading(false);
1020
1133
  }
1021
- }, [graphQLOptions, productId]);
1134
+ }, [graphQLOptions, limit, offset, productId]);
1022
1135
 
1023
1136
  useEffect(() => {
1024
1137
  void load();
@@ -1028,7 +1141,15 @@ export function useProductStockLevels(productId: string | undefined) {
1028
1141
  void load();
1029
1142
  }, [load]);
1030
1143
 
1031
- return { levels, isLoading, error, refetch };
1144
+ return {
1145
+ levels,
1146
+ totalLevels,
1147
+ totalAvailable,
1148
+ hasMore: offset + levels.length < totalLevels,
1149
+ isLoading,
1150
+ error,
1151
+ refetch,
1152
+ };
1032
1153
  }
1033
1154
 
1034
1155
  export type InventoryTransactionType = 'RECEIPT' | 'ISSUE' | 'TRANSFER' | 'ADJUSTMENT' | 'RETURN';
@@ -12,12 +12,7 @@ import { executeGraphQL } from '../utils/graphqlClient';
12
12
 
13
13
  export type TransportMode = 'ROAD' | 'AIR' | 'OCEAN' | 'RAIL';
14
14
  export type ShipmentLegStatus =
15
- | 'PLANNED'
16
- | 'BOOKED'
17
- | 'IN_TRANSIT'
18
- | 'ARRIVED'
19
- | 'DELIVERED'
20
- | 'CANCELLED';
15
+ 'PLANNED' | 'BOOKED' | 'IN_TRANSIT' | 'ARRIVED' | 'DELIVERED' | 'CANCELLED';
21
16
  export type OceanContainerType = 'LCL' | 'FCL_20FT' | 'FCL_40FT' | 'FCL_40FT_HC';
22
17
 
23
18
  export interface FreightRateBreakdownLine {
@@ -139,7 +134,7 @@ const SHIPMENT_FIELDS = `
139
134
  origin
140
135
  destination
141
136
  createdAt
142
- legs {
137
+ legs(pagination: { limit: 20, offset: 0 }) {
143
138
  ${LEG_FIELDS}
144
139
  }
145
140
  `;
@@ -149,6 +144,7 @@ const MULTI_MODAL_SHIPMENTS_QUERY = `
149
144
  shipments(filter: { isMultiModal: true }, pagination: $pagination) {
150
145
  ${SHIPMENT_FIELDS}
151
146
  }
147
+ shipmentsCount(filter: { isMultiModal: true })
152
148
  }
153
149
  `;
154
150
 
@@ -269,9 +265,12 @@ function useGraphqlOptions() {
269
265
  );
270
266
  }
271
267
 
272
- export function useMultiModalShipments() {
268
+ export function useMultiModalShipments(options: { limit?: number; offset?: number } = {}) {
269
+ const { limit = 10, offset = 0 } = options;
273
270
  const graphQLOptions = useGraphqlOptions();
274
271
  const [shipments, setShipments] = useState<MultiModalShipment[]>([]);
272
+ const [totalShipments, setTotalShipments] = useState(0);
273
+ const [hasMore, setHasMore] = useState(false);
275
274
  const [isLoading, setIsLoading] = useState(false);
276
275
  const [error, setError] = useState<Error | null>(null);
277
276
 
@@ -279,12 +278,14 @@ export function useMultiModalShipments() {
279
278
  setIsLoading(true);
280
279
  setError(null);
281
280
  try {
282
- const data = await executeGraphQL<{ shipments: ApiShipment[] }>(
281
+ const data = await executeGraphQL<{ shipments: ApiShipment[]; shipmentsCount: number }>(
283
282
  graphQLOptions,
284
283
  MULTI_MODAL_SHIPMENTS_QUERY,
285
- { pagination: { limit: 100, offset: 0 } }
284
+ { pagination: { limit, offset } }
286
285
  );
287
286
  setShipments(data.shipments.map(mapShipment));
287
+ setTotalShipments(data.shipmentsCount);
288
+ setHasMore(offset + data.shipments.length < data.shipmentsCount);
288
289
  } catch (fetchError) {
289
290
  setError(
290
291
  fetchError instanceof Error ? fetchError : new Error('Failed to load multi-modal shipments')
@@ -292,7 +293,7 @@ export function useMultiModalShipments() {
292
293
  } finally {
293
294
  setIsLoading(false);
294
295
  }
295
- }, [graphQLOptions]);
296
+ }, [graphQLOptions, limit, offset]);
296
297
 
297
298
  useEffect(() => {
298
299
  void load();
@@ -333,6 +334,7 @@ export function useMultiModalShipments() {
333
334
  );
334
335
  const created = mapShipment(data.planMultiModalShipment);
335
336
  setShipments((previous) => [created, ...previous]);
337
+ setTotalShipments((previous) => previous + 1);
336
338
  return created;
337
339
  },
338
340
  [graphQLOptions]
@@ -340,6 +342,8 @@ export function useMultiModalShipments() {
340
342
 
341
343
  return {
342
344
  shipments,
345
+ totalShipments,
346
+ hasMore,
343
347
  isLoading,
344
348
  error,
345
349
  refetch: () => void load(),