@burdenoff/microfe-movethewheels 2026.822.1 → 2026.827.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.
- package/dist/generated/wspace-operations.d.ts +168 -912
- package/dist/generated/wspace-types.d.ts +78432 -35831
- package/dist/hooks/useFleet.js +142 -74
- package/dist/hooks/useInventory.js +163 -70
- package/dist/hooks/useOrders.js +2 -2
- package/dist/hooks/useTracking.js +5 -5
- package/dist/index.js +33 -33
- package/dist/movethewheels/hooks/index.d.ts +2 -2
- package/dist/movethewheels/hooks/useFleet.d.ts +32 -1
- package/dist/movethewheels/hooks/useInventory.d.ts +67 -12
- package/dist/movethewheels/hooks/useOrders.d.ts +25 -2
- package/dist/movethewheels/types/index.d.ts +2 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/arrow-down-right.js +11 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/arrow-left-right.js +22 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/arrow-up-right.js +11 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/calendar-clock.js +32 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/history.js +18 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/lock.js +16 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/user-round.js +13 -0
- package/dist/pages/CreateOrderPage.js +246 -195
- package/dist/pages/DeliveryCapturePage.js +91 -78
- package/dist/pages/DriversPage.js +202 -108
- package/dist/pages/InventoryPage.js +356 -131
- package/dist/pages/ProductDetailsPage.js +212 -156
- package/dist/pages/VehicleDetailsPage.js +257 -193
- package/dist/utils/formatters.js +4 -1
- package/package.json +1 -1
- package/src/generated/wspace-operations.ts +134 -929
- package/src/generated/wspace-types.ts +91730 -40059
- package/src/movethewheels/hooks/index.ts +9 -0
- package/src/movethewheels/hooks/useFleet.ts +133 -3
- package/src/movethewheels/hooks/useInventory.ts +311 -53
- package/src/movethewheels/hooks/useOrders.ts +31 -7
- package/src/movethewheels/hooks/useTracking.ts +14 -14
- package/src/movethewheels/pages/CreateOrderPage.tsx +67 -10
- package/src/movethewheels/pages/DeliveryCapturePage.tsx +8 -0
- package/src/movethewheels/pages/DriversPage.tsx +181 -31
- package/src/movethewheels/pages/InventoryPage.tsx +299 -19
- package/src/movethewheels/pages/ProductDetailsPage.tsx +68 -3
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +86 -27
- package/src/movethewheels/types/index.ts +2 -0
- package/src/operations/wspace/movethewheels/fragments/core.graphql +23 -53
- package/src/operations/wspace/movethewheels/fragments/core.withdrawn.graphql +59 -0
- package/src/operations/wspace/movethewheels/mutations/entities.graphql +5 -5
- package/src/operations/wspace/movethewheels/mutations/marketplace-reports.graphql +18 -78
- package/src/operations/wspace/movethewheels/mutations/marketplace-reports.withdrawn.graphql +93 -0
- package/src/operations/wspace/movethewheels/mutations/orders.graphql +7 -7
- package/src/operations/wspace/movethewheels/queries/dashboard.graphql +3 -3
- package/src/operations/wspace/movethewheels/queries/entities.graphql +3 -3
- package/src/operations/wspace/movethewheels/queries/logistics.graphql +3 -3
- package/src/operations/wspace/movethewheels/queries/marketplace-reports.withdrawn.graphql +58 -0
- package/src/operations/wspace/movethewheels/queries/orders.graphql +3 -3
- package/src/operations/wspace/movethewheels/queries/marketplace-reports.graphql +0 -40
|
@@ -51,9 +51,15 @@ interface UseInventoryResult {
|
|
|
51
51
|
updateWarehouse: (id: string, input: CreateWarehouseInput) => Promise<Warehouse>;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Wire shape of MtwInventoryItem (wspace-movethewheels-svc:
|
|
55
|
+
// src/modules/inventory/schema/inventory.gql). The catalog domain was renamed
|
|
56
|
+
// Product -> MtwCargoItem on 2026-07-07 (commit 7e99ec5): the root fields are
|
|
57
|
+
// `cargoItems`/`cargoItem`/`createCargoItem`/`updateCargoItem` and the inventory
|
|
58
|
+
// link column is `cargoItemId`. The MFE keeps its own `Product`/`Inventory.productId`
|
|
59
|
+
// domain names; the rename is absorbed in the mappers below.
|
|
54
60
|
interface ApiInventoryItem {
|
|
55
61
|
id: string;
|
|
56
|
-
|
|
62
|
+
cargoItemId: string;
|
|
57
63
|
warehouseId: string;
|
|
58
64
|
onHand: number;
|
|
59
65
|
available: number;
|
|
@@ -96,7 +102,7 @@ const LIST_INVENTORY_QUERY = `
|
|
|
96
102
|
query ListInventoryItems($filter: InventoryFilterInput, $pagination: PaginationInput) {
|
|
97
103
|
inventoryItems(filter: $filter, pagination: $pagination) {
|
|
98
104
|
id
|
|
99
|
-
|
|
105
|
+
cargoItemId
|
|
100
106
|
warehouseId
|
|
101
107
|
onHand
|
|
102
108
|
available
|
|
@@ -110,8 +116,8 @@ const LIST_INVENTORY_QUERY = `
|
|
|
110
116
|
`;
|
|
111
117
|
|
|
112
118
|
const LIST_PRODUCTS_QUERY = `
|
|
113
|
-
query
|
|
114
|
-
|
|
119
|
+
query ListCargoItems($filter: CargoItemFilterInput, $pagination: PaginationInput) {
|
|
120
|
+
cargoItems(filter: $filter, pagination: $pagination) {
|
|
115
121
|
id
|
|
116
122
|
sku
|
|
117
123
|
name
|
|
@@ -146,8 +152,8 @@ const LIST_WAREHOUSES_QUERY = `
|
|
|
146
152
|
`;
|
|
147
153
|
|
|
148
154
|
const GET_PRODUCT_QUERY = `
|
|
149
|
-
query
|
|
150
|
-
|
|
155
|
+
query GetCargoItem($id: ID!) {
|
|
156
|
+
cargoItem(id: $id) {
|
|
151
157
|
id
|
|
152
158
|
sku
|
|
153
159
|
name
|
|
@@ -190,12 +196,36 @@ const INVENTORY_STATS_QUERY = `
|
|
|
190
196
|
totalDamaged
|
|
191
197
|
lowStockCount
|
|
192
198
|
}
|
|
199
|
+
cargoItemsCount
|
|
200
|
+
warehousesCount
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Stock ledger for the workspace, newest first (the service orders
|
|
206
|
+
* `inventoryTransactions` by `createdAt: desc`). `InventoryFilterInput` accepts
|
|
207
|
+
* `cargoItemId` / `warehouseId`, so the same query backs a per-item ledger.
|
|
208
|
+
*/
|
|
209
|
+
const LIST_INVENTORY_TRANSACTIONS_QUERY = `
|
|
210
|
+
query ListInventoryTransactions($filter: InventoryFilterInput, $pagination: PaginationInput) {
|
|
211
|
+
inventoryTransactions(filter: $filter, pagination: $pagination) {
|
|
212
|
+
id
|
|
213
|
+
transactionNumber
|
|
214
|
+
type
|
|
215
|
+
quantity
|
|
216
|
+
cargoItemId
|
|
217
|
+
warehouseId
|
|
218
|
+
notes
|
|
219
|
+
referenceType
|
|
220
|
+
referenceId
|
|
221
|
+
createdAt
|
|
222
|
+
}
|
|
193
223
|
}
|
|
194
224
|
`;
|
|
195
225
|
|
|
196
226
|
const CREATE_PRODUCT_MUTATION = `
|
|
197
|
-
mutation
|
|
198
|
-
|
|
227
|
+
mutation CreateCargoItem($input: CreateCargoItemInput!) {
|
|
228
|
+
createCargoItem(input: $input) {
|
|
199
229
|
id
|
|
200
230
|
sku
|
|
201
231
|
name
|
|
@@ -215,8 +245,8 @@ const CREATE_PRODUCT_MUTATION = `
|
|
|
215
245
|
`;
|
|
216
246
|
|
|
217
247
|
const UPDATE_PRODUCT_MUTATION = `
|
|
218
|
-
mutation
|
|
219
|
-
|
|
248
|
+
mutation UpdateCargoItem($id: ID!, $input: UpdateCargoItemInput!) {
|
|
249
|
+
updateCargoItem(id: $id, input: $input) {
|
|
220
250
|
id
|
|
221
251
|
sku
|
|
222
252
|
name
|
|
@@ -269,7 +299,7 @@ const ADJUST_INVENTORY_MUTATION = `
|
|
|
269
299
|
mutation AdjustInventoryStock($input: AdjustInventoryStockInput!) {
|
|
270
300
|
adjustInventoryStock(input: $input) {
|
|
271
301
|
id
|
|
272
|
-
|
|
302
|
+
cargoItemId
|
|
273
303
|
warehouseId
|
|
274
304
|
onHand
|
|
275
305
|
available
|
|
@@ -355,7 +385,7 @@ function mapWarehouse(warehouse: ApiWarehouse): Warehouse {
|
|
|
355
385
|
function mapInventory(item: ApiInventoryItem): Inventory {
|
|
356
386
|
return {
|
|
357
387
|
id: item.id,
|
|
358
|
-
productId: item.
|
|
388
|
+
productId: item.cargoItemId,
|
|
359
389
|
warehouseId: item.warehouseId,
|
|
360
390
|
zoneId: '',
|
|
361
391
|
quantity: item.onHand,
|
|
@@ -419,12 +449,12 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
|
|
|
419
449
|
{
|
|
420
450
|
filter: {
|
|
421
451
|
warehouseId: filters?.warehouseId,
|
|
422
|
-
|
|
452
|
+
cargoItemId: filters?.productId,
|
|
423
453
|
},
|
|
424
454
|
pagination: { limit: 500, offset: 0 },
|
|
425
455
|
}
|
|
426
456
|
),
|
|
427
|
-
executeGraphQL<{
|
|
457
|
+
executeGraphQL<{ cargoItems: ApiProduct[] }>(graphQLOptions, LIST_PRODUCTS_QUERY, {
|
|
428
458
|
pagination: { limit: 500, offset: 0 },
|
|
429
459
|
}),
|
|
430
460
|
executeGraphQL<{ warehouses: ApiWarehouse[] }>(graphQLOptions, LIST_WAREHOUSES_QUERY, {
|
|
@@ -438,7 +468,7 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
|
|
|
438
468
|
}
|
|
439
469
|
|
|
440
470
|
setInventory(nextInventory);
|
|
441
|
-
setProducts(productData.
|
|
471
|
+
setProducts(productData.cargoItems.map(mapProduct));
|
|
442
472
|
setWarehouses(warehouseData.warehouses.map(mapWarehouse));
|
|
443
473
|
} catch (fetchError) {
|
|
444
474
|
setError(
|
|
@@ -515,7 +545,7 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
|
|
|
515
545
|
|
|
516
546
|
const createProduct = useCallback(
|
|
517
547
|
async (input: CreateProductInput): Promise<Product> => {
|
|
518
|
-
const data = await executeGraphQL<{
|
|
548
|
+
const data = await executeGraphQL<{ createCargoItem: ApiProduct }>(
|
|
519
549
|
graphQLOptions,
|
|
520
550
|
CREATE_PRODUCT_MUTATION,
|
|
521
551
|
{
|
|
@@ -528,7 +558,7 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
|
|
|
528
558
|
},
|
|
529
559
|
}
|
|
530
560
|
);
|
|
531
|
-
const created = mapProduct(data.
|
|
561
|
+
const created = mapProduct(data.createCargoItem);
|
|
532
562
|
setProducts((previous) => [created, ...previous]);
|
|
533
563
|
return created;
|
|
534
564
|
},
|
|
@@ -559,7 +589,7 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
|
|
|
559
589
|
|
|
560
590
|
const updateProduct = useCallback(
|
|
561
591
|
async (id: string, input: CreateProductInput): Promise<Product> => {
|
|
562
|
-
const data = await executeGraphQL<{
|
|
592
|
+
const data = await executeGraphQL<{ updateCargoItem: ApiProduct }>(
|
|
563
593
|
graphQLOptions,
|
|
564
594
|
UPDATE_PRODUCT_MUTATION,
|
|
565
595
|
{
|
|
@@ -573,7 +603,7 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
|
|
|
573
603
|
},
|
|
574
604
|
}
|
|
575
605
|
);
|
|
576
|
-
const updated = mapProduct(data.
|
|
606
|
+
const updated = mapProduct(data.updateCargoItem);
|
|
577
607
|
setProducts((previous) => previous.map((item) => (item.id === id ? updated : item)));
|
|
578
608
|
return updated;
|
|
579
609
|
},
|
|
@@ -637,15 +667,15 @@ export function useProducts(options: UseListOptions = {}) {
|
|
|
637
667
|
setIsLoading(true);
|
|
638
668
|
setError(null);
|
|
639
669
|
try {
|
|
640
|
-
const data = await executeGraphQL<{
|
|
670
|
+
const data = await executeGraphQL<{ cargoItems: ApiProduct[] }>(
|
|
641
671
|
graphQLOptions,
|
|
642
672
|
LIST_PRODUCTS_QUERY,
|
|
643
673
|
{
|
|
644
674
|
pagination: { limit, offset },
|
|
645
675
|
}
|
|
646
676
|
);
|
|
647
|
-
setHasMore(data.
|
|
648
|
-
setProducts(data.
|
|
677
|
+
setHasMore(data.cargoItems.length === limit);
|
|
678
|
+
setProducts(data.cargoItems.map(mapProduct));
|
|
649
679
|
} catch (fetchError) {
|
|
650
680
|
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load products'));
|
|
651
681
|
} finally {
|
|
@@ -701,14 +731,14 @@ export function useProduct(productId: string | undefined) {
|
|
|
701
731
|
setIsLoading(true);
|
|
702
732
|
setError(null);
|
|
703
733
|
try {
|
|
704
|
-
const data = await executeGraphQL<{
|
|
734
|
+
const data = await executeGraphQL<{ cargoItem: ApiProduct | null }>(
|
|
705
735
|
graphQLOptions,
|
|
706
736
|
GET_PRODUCT_QUERY,
|
|
707
737
|
{
|
|
708
738
|
id: productId,
|
|
709
739
|
}
|
|
710
740
|
);
|
|
711
|
-
setProduct(data.
|
|
741
|
+
setProduct(data.cargoItem ? mapProduct(data.cargoItem) : null);
|
|
712
742
|
} catch (fetchError) {
|
|
713
743
|
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load product'));
|
|
714
744
|
} finally {
|
|
@@ -827,15 +857,39 @@ export function useWarehouse(warehouseId: string | undefined) {
|
|
|
827
857
|
return { warehouse, isLoading, error, refetch: () => void load() };
|
|
828
858
|
}
|
|
829
859
|
|
|
860
|
+
export interface InventoryStats {
|
|
861
|
+
/** Units physically in the warehouses. */
|
|
862
|
+
totalOnHand: number;
|
|
863
|
+
/** Units free to promise on a new order. */
|
|
864
|
+
totalAvailable: number;
|
|
865
|
+
/** Units already committed to open orders. */
|
|
866
|
+
totalAllocated: number;
|
|
867
|
+
/** Units written off as damaged. */
|
|
868
|
+
totalDamaged: number;
|
|
869
|
+
/** Stock lines at or under their reorder point. */
|
|
870
|
+
lowStockCount: number;
|
|
871
|
+
/** Catalog size (`cargoItemsCount`), workspace-wide — not a page length. */
|
|
872
|
+
totalProducts: number;
|
|
873
|
+
/** Warehouse count (`warehousesCount`), workspace-wide. */
|
|
874
|
+
totalWarehouses: number;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Workspace-wide inventory aggregates, every figure served by the module:
|
|
879
|
+
* `inventoryStats` for the stock position, `cargoItemsCount`/`warehousesCount`
|
|
880
|
+
* for catalog and network size. There is no aggregate stock-value field on the
|
|
881
|
+
* subgraph, so no monetary total is reported here.
|
|
882
|
+
*/
|
|
830
883
|
export function useInventoryStats() {
|
|
831
884
|
const graphQLOptions = useGraphqlOptions();
|
|
832
|
-
const [stats, setStats] = useState({
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
885
|
+
const [stats, setStats] = useState<InventoryStats>({
|
|
886
|
+
totalOnHand: 0,
|
|
887
|
+
totalAvailable: 0,
|
|
888
|
+
totalAllocated: 0,
|
|
889
|
+
totalDamaged: 0,
|
|
890
|
+
lowStockCount: 0,
|
|
836
891
|
totalProducts: 0,
|
|
837
892
|
totalWarehouses: 0,
|
|
838
|
-
warehouseUtilization: [] as Array<{ id: string; name: string; utilization: number }>,
|
|
839
893
|
});
|
|
840
894
|
const [isLoading, setIsLoading] = useState(false);
|
|
841
895
|
const [error, setError] = useState<Error | null>(null);
|
|
@@ -845,32 +899,26 @@ export function useInventoryStats() {
|
|
|
845
899
|
setIsLoading(true);
|
|
846
900
|
setError(null);
|
|
847
901
|
try {
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
902
|
+
const data = await executeGraphQL<{
|
|
903
|
+
inventoryStats: {
|
|
904
|
+
totalOnHand: number;
|
|
905
|
+
totalAvailable: number;
|
|
906
|
+
totalAllocated: number;
|
|
907
|
+
totalDamaged: number;
|
|
908
|
+
lowStockCount: number;
|
|
909
|
+
};
|
|
910
|
+
cargoItemsCount: number;
|
|
911
|
+
warehousesCount: number;
|
|
912
|
+
}>(graphQLOptions, INVENTORY_STATS_QUERY);
|
|
859
913
|
|
|
860
914
|
setStats({
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
name: warehouse.name,
|
|
869
|
-
utilization:
|
|
870
|
-
(warehouse.totalCapacity ?? 0) > 0
|
|
871
|
-
? ((warehouse.usedCapacity ?? 0) / (warehouse.totalCapacity ?? 1)) * 100
|
|
872
|
-
: 0,
|
|
873
|
-
})),
|
|
915
|
+
totalOnHand: data.inventoryStats.totalOnHand,
|
|
916
|
+
totalAvailable: data.inventoryStats.totalAvailable,
|
|
917
|
+
totalAllocated: data.inventoryStats.totalAllocated,
|
|
918
|
+
totalDamaged: data.inventoryStats.totalDamaged,
|
|
919
|
+
lowStockCount: data.inventoryStats.lowStockCount,
|
|
920
|
+
totalProducts: data.cargoItemsCount,
|
|
921
|
+
totalWarehouses: data.warehousesCount,
|
|
874
922
|
});
|
|
875
923
|
} catch (fetchError) {
|
|
876
924
|
setError(
|
|
@@ -886,3 +934,213 @@ export function useInventoryStats() {
|
|
|
886
934
|
|
|
887
935
|
return { stats, isLoading, error };
|
|
888
936
|
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Per-warehouse stock for one catalog item. Uses the nested
|
|
940
|
+
* `MtwInventoryItem.warehouse` resolver so the warehouse name comes back with
|
|
941
|
+
* the row instead of being joined against a separately-paged warehouse list.
|
|
942
|
+
*/
|
|
943
|
+
const PRODUCT_STOCK_LEVELS_QUERY = `
|
|
944
|
+
query GetCargoItemStockLevels($cargoItemId: ID!) {
|
|
945
|
+
inventoryItems(filter: { cargoItemId: $cargoItemId }, pagination: { limit: 200, offset: 0 }) {
|
|
946
|
+
id
|
|
947
|
+
onHand
|
|
948
|
+
available
|
|
949
|
+
allocated
|
|
950
|
+
damaged
|
|
951
|
+
reorderPoint
|
|
952
|
+
warehouse {
|
|
953
|
+
id
|
|
954
|
+
name
|
|
955
|
+
code
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
`;
|
|
960
|
+
|
|
961
|
+
export interface ProductStockLevel {
|
|
962
|
+
inventoryItemId: string;
|
|
963
|
+
warehouseId: string;
|
|
964
|
+
warehouseName: string;
|
|
965
|
+
warehouseCode: string;
|
|
966
|
+
onHand: number;
|
|
967
|
+
available: number;
|
|
968
|
+
allocated: number;
|
|
969
|
+
damaged: number;
|
|
970
|
+
reorderPoint?: number;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
interface ApiProductStockLevel {
|
|
974
|
+
id: string;
|
|
975
|
+
onHand: number;
|
|
976
|
+
available: number;
|
|
977
|
+
allocated: number;
|
|
978
|
+
damaged: number;
|
|
979
|
+
reorderPoint?: number | null;
|
|
980
|
+
warehouse?: { id: string; name: string; code: string } | null;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/** Where a catalog item is stocked, and how much of it is free to promise. */
|
|
984
|
+
export function useProductStockLevels(productId: string | undefined) {
|
|
985
|
+
const graphQLOptions = useGraphqlOptions();
|
|
986
|
+
const [levels, setLevels] = useState<ProductStockLevel[]>([]);
|
|
987
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
988
|
+
const [error, setError] = useState<Error | null>(null);
|
|
989
|
+
|
|
990
|
+
const load = useCallback(async () => {
|
|
991
|
+
if (!productId) {
|
|
992
|
+
setLevels([]);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
setIsLoading(true);
|
|
996
|
+
setError(null);
|
|
997
|
+
try {
|
|
998
|
+
const data = await executeGraphQL<{ inventoryItems: ApiProductStockLevel[] }>(
|
|
999
|
+
graphQLOptions,
|
|
1000
|
+
PRODUCT_STOCK_LEVELS_QUERY,
|
|
1001
|
+
{ cargoItemId: productId }
|
|
1002
|
+
);
|
|
1003
|
+
setLevels(
|
|
1004
|
+
data.inventoryItems.map((item) => ({
|
|
1005
|
+
inventoryItemId: item.id,
|
|
1006
|
+
warehouseId: item.warehouse?.id ?? '',
|
|
1007
|
+
warehouseName: item.warehouse?.name ?? 'Unknown warehouse',
|
|
1008
|
+
warehouseCode: item.warehouse?.code ?? '',
|
|
1009
|
+
onHand: item.onHand,
|
|
1010
|
+
available: item.available,
|
|
1011
|
+
allocated: item.allocated,
|
|
1012
|
+
damaged: item.damaged,
|
|
1013
|
+
reorderPoint: item.reorderPoint ?? undefined,
|
|
1014
|
+
}))
|
|
1015
|
+
);
|
|
1016
|
+
} catch (fetchError) {
|
|
1017
|
+
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load stock levels'));
|
|
1018
|
+
} finally {
|
|
1019
|
+
setIsLoading(false);
|
|
1020
|
+
}
|
|
1021
|
+
}, [graphQLOptions, productId]);
|
|
1022
|
+
|
|
1023
|
+
useEffect(() => {
|
|
1024
|
+
void load();
|
|
1025
|
+
}, [load]);
|
|
1026
|
+
|
|
1027
|
+
const refetch = useCallback(() => {
|
|
1028
|
+
void load();
|
|
1029
|
+
}, [load]);
|
|
1030
|
+
|
|
1031
|
+
return { levels, isLoading, error, refetch };
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
export type InventoryTransactionType = 'RECEIPT' | 'ISSUE' | 'TRANSFER' | 'ADJUSTMENT' | 'RETURN';
|
|
1035
|
+
|
|
1036
|
+
export interface InventoryTransaction {
|
|
1037
|
+
id: string;
|
|
1038
|
+
transactionNumber: string;
|
|
1039
|
+
type: InventoryTransactionType;
|
|
1040
|
+
/** Signed by `type`: receipts/returns add stock, issues remove it. */
|
|
1041
|
+
quantity: number;
|
|
1042
|
+
productId: string;
|
|
1043
|
+
warehouseId: string;
|
|
1044
|
+
notes?: string;
|
|
1045
|
+
referenceType?: string;
|
|
1046
|
+
referenceId?: string;
|
|
1047
|
+
createdAt: Date;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
interface ApiInventoryTransaction {
|
|
1051
|
+
id: string;
|
|
1052
|
+
transactionNumber: string;
|
|
1053
|
+
type: string;
|
|
1054
|
+
quantity: number;
|
|
1055
|
+
cargoItemId: string;
|
|
1056
|
+
warehouseId: string;
|
|
1057
|
+
notes?: string | null;
|
|
1058
|
+
referenceType?: string | null;
|
|
1059
|
+
referenceId?: string | null;
|
|
1060
|
+
createdAt: string;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const TRANSACTION_TYPES: InventoryTransactionType[] = [
|
|
1064
|
+
'RECEIPT',
|
|
1065
|
+
'ISSUE',
|
|
1066
|
+
'TRANSFER',
|
|
1067
|
+
'ADJUSTMENT',
|
|
1068
|
+
'RETURN',
|
|
1069
|
+
];
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* The service returns `InventoryTransactionType` in upper snake case, but the
|
|
1073
|
+
* value is normalised here rather than trusted, so an unexpected casing renders
|
|
1074
|
+
* as ADJUSTMENT-neutral text instead of a broken label.
|
|
1075
|
+
*/
|
|
1076
|
+
function mapTransactionType(type: string): InventoryTransactionType {
|
|
1077
|
+
const normalized = type.toUpperCase();
|
|
1078
|
+
return (
|
|
1079
|
+
TRANSACTION_TYPES.find((candidate) => candidate === normalized) ??
|
|
1080
|
+
('ADJUSTMENT' as InventoryTransactionType)
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function mapTransaction(transaction: ApiInventoryTransaction): InventoryTransaction {
|
|
1085
|
+
return {
|
|
1086
|
+
id: transaction.id,
|
|
1087
|
+
transactionNumber: transaction.transactionNumber,
|
|
1088
|
+
type: mapTransactionType(transaction.type),
|
|
1089
|
+
quantity: transaction.quantity,
|
|
1090
|
+
productId: transaction.cargoItemId,
|
|
1091
|
+
warehouseId: transaction.warehouseId,
|
|
1092
|
+
notes: transaction.notes ?? undefined,
|
|
1093
|
+
referenceType: transaction.referenceType ?? undefined,
|
|
1094
|
+
referenceId: transaction.referenceId ?? undefined,
|
|
1095
|
+
createdAt: new Date(transaction.createdAt),
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
interface UseInventoryTransactionsOptions {
|
|
1100
|
+
limit?: number;
|
|
1101
|
+
productId?: string;
|
|
1102
|
+
warehouseId?: string;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/** Recent stock movements from `Query.inventoryTransactions` (newest first). */
|
|
1106
|
+
export function useInventoryTransactions(options: UseInventoryTransactionsOptions = {}) {
|
|
1107
|
+
const { limit = 10, productId, warehouseId } = options;
|
|
1108
|
+
const graphQLOptions = useGraphqlOptions();
|
|
1109
|
+
const [transactions, setTransactions] = useState<InventoryTransaction[]>([]);
|
|
1110
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
1111
|
+
const [error, setError] = useState<Error | null>(null);
|
|
1112
|
+
|
|
1113
|
+
const load = useCallback(async () => {
|
|
1114
|
+
setIsLoading(true);
|
|
1115
|
+
setError(null);
|
|
1116
|
+
try {
|
|
1117
|
+
const data = await executeGraphQL<{ inventoryTransactions: ApiInventoryTransaction[] }>(
|
|
1118
|
+
graphQLOptions,
|
|
1119
|
+
LIST_INVENTORY_TRANSACTIONS_QUERY,
|
|
1120
|
+
{
|
|
1121
|
+
filter: productId || warehouseId ? { cargoItemId: productId, warehouseId } : undefined,
|
|
1122
|
+
pagination: { limit, offset: 0 },
|
|
1123
|
+
}
|
|
1124
|
+
);
|
|
1125
|
+
setTransactions(data.inventoryTransactions.map(mapTransaction));
|
|
1126
|
+
} catch (fetchError) {
|
|
1127
|
+
setError(
|
|
1128
|
+
fetchError instanceof Error
|
|
1129
|
+
? fetchError
|
|
1130
|
+
: new Error('Failed to load inventory transactions')
|
|
1131
|
+
);
|
|
1132
|
+
} finally {
|
|
1133
|
+
setIsLoading(false);
|
|
1134
|
+
}
|
|
1135
|
+
}, [graphQLOptions, limit, productId, warehouseId]);
|
|
1136
|
+
|
|
1137
|
+
useEffect(() => {
|
|
1138
|
+
void load();
|
|
1139
|
+
}, [load]);
|
|
1140
|
+
|
|
1141
|
+
const refetch = useCallback(() => {
|
|
1142
|
+
void load();
|
|
1143
|
+
}, [load]);
|
|
1144
|
+
|
|
1145
|
+
return { transactions, isLoading, error, refetch };
|
|
1146
|
+
}
|
|
@@ -9,6 +9,17 @@ interface UseOrdersOptions {
|
|
|
9
9
|
offset?: number;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Write shape for create/update. Identical to `Partial<Order>` except that
|
|
14
|
+
* `scheduledDelivery` also accepts `null`: `UpdateDeliveryJobInput.scheduledDelivery`
|
|
15
|
+
* is nullable on the service, so an explicit `null` clears a previously booked
|
|
16
|
+
* slot, while `undefined` leaves it untouched. Without the distinction, clearing
|
|
17
|
+
* the field in the form would silently keep the old date.
|
|
18
|
+
*/
|
|
19
|
+
export type OrderWriteInput = Omit<Partial<Order>, 'scheduledDelivery'> & {
|
|
20
|
+
scheduledDelivery?: Date | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
12
23
|
interface UseOrdersResult {
|
|
13
24
|
orders: Order[];
|
|
14
25
|
totalOrders: number;
|
|
@@ -21,8 +32,8 @@ interface UseOrdersResult {
|
|
|
21
32
|
isLoading: boolean;
|
|
22
33
|
error: Error | null;
|
|
23
34
|
refetch: () => void;
|
|
24
|
-
createOrder: (order:
|
|
25
|
-
updateOrder: (id: string, updates:
|
|
35
|
+
createOrder: (order: OrderWriteInput) => Promise<Order>;
|
|
36
|
+
updateOrder: (id: string, updates: OrderWriteInput) => Promise<Order>;
|
|
26
37
|
deleteOrder: (id: string) => Promise<void>;
|
|
27
38
|
updateOrderStatus: (id: string, status: OrderStatus) => Promise<Order>;
|
|
28
39
|
}
|
|
@@ -160,7 +171,13 @@ const DELETE_ORDER_MUTATION = `
|
|
|
160
171
|
}
|
|
161
172
|
`;
|
|
162
173
|
|
|
163
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Normalises a service `DeliveryJobStatus` into the MFE's OrderStatus union.
|
|
176
|
+
* Exported so any surface rendering a raw job status badge normalises it the
|
|
177
|
+
* same way — the service casing is upper snake case and would miss the badge
|
|
178
|
+
* colour map otherwise.
|
|
179
|
+
*/
|
|
180
|
+
export function mapOrderStatus(status: string): OrderStatus {
|
|
164
181
|
switch (status) {
|
|
165
182
|
case 'PENDING':
|
|
166
183
|
return 'pending';
|
|
@@ -183,7 +200,12 @@ function mapOrderStatus(status: string): OrderStatus {
|
|
|
183
200
|
}
|
|
184
201
|
}
|
|
185
202
|
|
|
186
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Normalises a service `DeliveryPriority` (LOW/MEDIUM/HIGH/URGENT/CRITICAL)
|
|
205
|
+
* into the four-level union PriorityBadge understands. MEDIUM and CRITICAL have
|
|
206
|
+
* no direct badge entry, so rendering the raw value would break the colour map.
|
|
207
|
+
*/
|
|
208
|
+
export function mapOrderPriority(priority: string): 'low' | 'normal' | 'high' | 'urgent' {
|
|
187
209
|
switch (priority) {
|
|
188
210
|
case 'LOW':
|
|
189
211
|
return 'low';
|
|
@@ -366,7 +388,7 @@ export function useOrders(options: UseOrdersOptions = {}): UseOrdersResult {
|
|
|
366
388
|
}, [loadOrders]);
|
|
367
389
|
|
|
368
390
|
const createOrder = useCallback(
|
|
369
|
-
async (orderData:
|
|
391
|
+
async (orderData: OrderWriteInput): Promise<Order> => {
|
|
370
392
|
const deliveryJobNumber =
|
|
371
393
|
orderData.orderNumber ??
|
|
372
394
|
`ORD-${new Date().getFullYear()}-${Date.now().toString().slice(-6)}`;
|
|
@@ -399,7 +421,7 @@ export function useOrders(options: UseOrdersOptions = {}): UseOrdersResult {
|
|
|
399
421
|
);
|
|
400
422
|
|
|
401
423
|
const updateOrder = useCallback(
|
|
402
|
-
async (id: string, updates:
|
|
424
|
+
async (id: string, updates: OrderWriteInput): Promise<Order> => {
|
|
403
425
|
const data = await executeGraphQL<{
|
|
404
426
|
updateDeliveryJob: ApiOrder;
|
|
405
427
|
}>(graphQLOptions, UPDATE_ORDER_MUTATION, {
|
|
@@ -414,7 +436,9 @@ export function useOrders(options: UseOrdersOptions = {}): UseOrdersResult {
|
|
|
414
436
|
pricing: updates.pricing,
|
|
415
437
|
specialInstructions: updates.specialInstructions,
|
|
416
438
|
trackingNumber: updates.trackingNumber,
|
|
417
|
-
|
|
439
|
+
// `null` clears the booked slot; `undefined` leaves it untouched.
|
|
440
|
+
scheduledDelivery:
|
|
441
|
+
updates.scheduledDelivery === null ? null : updates.scheduledDelivery?.toISOString(),
|
|
418
442
|
vehicleId: updates.vehicleId,
|
|
419
443
|
driverId: updates.driverId,
|
|
420
444
|
},
|
|
@@ -30,7 +30,7 @@ interface ApiTrackingEvent {
|
|
|
30
30
|
interface ApiShipment {
|
|
31
31
|
id: string;
|
|
32
32
|
shipmentNumber: string;
|
|
33
|
-
|
|
33
|
+
deliveryJobId?: string | null;
|
|
34
34
|
status: string;
|
|
35
35
|
estimatedDelivery?: string | null;
|
|
36
36
|
trackingEvents?: ApiTrackingEvent[];
|
|
@@ -41,7 +41,7 @@ const SHIPMENTS_QUERY = `
|
|
|
41
41
|
shipments(filter: $filter, pagination: $pagination) {
|
|
42
42
|
id
|
|
43
43
|
shipmentNumber
|
|
44
|
-
|
|
44
|
+
deliveryJobId
|
|
45
45
|
status
|
|
46
46
|
estimatedDelivery
|
|
47
47
|
}
|
|
@@ -53,7 +53,7 @@ const SHIPMENT_QUERY = `
|
|
|
53
53
|
shipment(id: $id) {
|
|
54
54
|
id
|
|
55
55
|
shipmentNumber
|
|
56
|
-
|
|
56
|
+
deliveryJobId
|
|
57
57
|
status
|
|
58
58
|
estimatedDelivery
|
|
59
59
|
trackingEvents {
|
|
@@ -68,9 +68,9 @@ const SHIPMENT_QUERY = `
|
|
|
68
68
|
}
|
|
69
69
|
`;
|
|
70
70
|
|
|
71
|
-
const
|
|
72
|
-
query
|
|
73
|
-
|
|
71
|
+
const DELIVERY_JOB_TRACKING_QUERY = `
|
|
72
|
+
query GetDeliveryJobTracking($deliveryJobId: ID!) {
|
|
73
|
+
deliveryJobTracking(deliveryJobId: $deliveryJobId) {
|
|
74
74
|
id
|
|
75
75
|
shipmentId
|
|
76
76
|
eventType
|
|
@@ -86,7 +86,7 @@ const ACTIVE_DELIVERIES_QUERY = `
|
|
|
86
86
|
activeDeliveries {
|
|
87
87
|
id
|
|
88
88
|
shipmentNumber
|
|
89
|
-
|
|
89
|
+
deliveryJobId
|
|
90
90
|
status
|
|
91
91
|
estimatedDelivery
|
|
92
92
|
}
|
|
@@ -163,7 +163,7 @@ export function useTracking(options: UseTrackingOptions = {}): UseTrackingResult
|
|
|
163
163
|
|
|
164
164
|
const mapped = data.shipments.map((shipment) => ({
|
|
165
165
|
id: shipment.id,
|
|
166
|
-
entityId: shipment.
|
|
166
|
+
entityId: shipment.deliveryJobId ?? shipment.id,
|
|
167
167
|
entityType: 'order' as const,
|
|
168
168
|
currentLocation: {
|
|
169
169
|
latitude: 0,
|
|
@@ -224,14 +224,14 @@ export function useOrderTracking(orderId: string | undefined) {
|
|
|
224
224
|
setIsLoading(true);
|
|
225
225
|
setError(null);
|
|
226
226
|
try {
|
|
227
|
-
const data = await executeGraphQL<{
|
|
227
|
+
const data = await executeGraphQL<{ deliveryJobTracking: ApiTrackingEvent[] }>(
|
|
228
228
|
graphQLOptions,
|
|
229
|
-
|
|
229
|
+
DELIVERY_JOB_TRACKING_QUERY,
|
|
230
230
|
{
|
|
231
|
-
orderId,
|
|
231
|
+
deliveryJobId: orderId,
|
|
232
232
|
}
|
|
233
233
|
);
|
|
234
|
-
const latest = data.
|
|
234
|
+
const latest = data.deliveryJobTracking[data.deliveryJobTracking.length - 1];
|
|
235
235
|
setTracking(latest ? mapTrackingEventToTracking(latest, orderId) : null);
|
|
236
236
|
} catch (fetchError) {
|
|
237
237
|
setError(
|
|
@@ -327,7 +327,7 @@ export function useLiveTracking() {
|
|
|
327
327
|
|
|
328
328
|
const shipmentEvents = shipmentData.shipment?.trackingEvents ?? [];
|
|
329
329
|
const latestEvent = shipmentEvents[shipmentEvents.length - 1];
|
|
330
|
-
const order = orders.find((item) => item.id === shipment.
|
|
330
|
+
const order = orders.find((item) => item.id === shipment.deliveryJobId);
|
|
331
331
|
if (!order) {
|
|
332
332
|
return null;
|
|
333
333
|
}
|
|
@@ -346,7 +346,7 @@ export function useLiveTracking() {
|
|
|
346
346
|
driver,
|
|
347
347
|
vehicle,
|
|
348
348
|
tracking: latestEvent
|
|
349
|
-
? mapTrackingEventToTracking(latestEvent, shipment.
|
|
349
|
+
? mapTrackingEventToTracking(latestEvent, shipment.deliveryJobId ?? shipment.id)
|
|
350
350
|
: null,
|
|
351
351
|
estimatedArrival: shipment.estimatedDelivery
|
|
352
352
|
? new Date(shipment.estimatedDelivery)
|