@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.
- package/dist/components/CustomerPicker.js +52 -48
- package/dist/components/EntityCombobox.js +25 -23
- package/dist/components/OrderItemRow.js +123 -78
- package/dist/generated/wspace-types.d.ts +1547 -64
- package/dist/hooks/useCustomers.js +55 -26
- package/dist/hooks/useFleet.js +236 -157
- package/dist/hooks/useInventory.js +214 -153
- package/dist/hooks/useMultiModalShipments.js +30 -26
- package/dist/hooks/useRoutes.js +64 -24
- package/dist/hooks/useTracking.js +14 -16
- package/dist/movethewheels/components/CustomerPicker.d.ts +5 -1
- package/dist/movethewheels/components/EntityCombobox.d.ts +6 -3
- package/dist/movethewheels/components/OrderItemRow.d.ts +7 -1
- package/dist/movethewheels/hooks/useCustomers.d.ts +3 -0
- package/dist/movethewheels/hooks/useFleet.d.ts +15 -2
- package/dist/movethewheels/hooks/useInventory.d.ts +15 -12
- package/dist/movethewheels/hooks/useMultiModalShipments.d.ts +1 -0
- package/dist/movethewheels/hooks/useRoutes.d.ts +7 -1
- package/dist/movethewheels/types/index.d.ts +33 -23
- package/dist/movethewheels/utils/formatters.d.ts +1 -1
- package/dist/movethewheels/utils/orderValidation.d.ts +11 -0
- package/dist/pages/CreateOrderPage.js +349 -297
- package/dist/pages/CustomerDetailsPage.js +6 -6
- package/dist/pages/CustomersPage.js +2 -2
- package/dist/pages/DashboardPage.js +1 -1
- package/dist/pages/DriverDetailsPage.js +340 -332
- package/dist/pages/DriversPage.js +7 -5
- package/dist/pages/FleetPage.js +1 -1
- package/dist/pages/InventoryPage.js +11 -12
- package/dist/pages/LiveTrackingPage.js +4 -4
- package/dist/pages/MultiModalShipmentPage.js +139 -125
- package/dist/pages/OrderDetailsPage.js +132 -140
- package/dist/pages/ProductDetailsPage.js +408 -249
- package/dist/pages/ProductsPage.js +5 -5
- package/dist/pages/RouteDetailsPage.js +208 -210
- package/dist/pages/RoutesPage.js +2 -2
- package/dist/pages/TrackingPage.js +16 -9
- package/dist/pages/WarehouseDetailsPage.js +166 -163
- package/dist/pages/WarehousesPage.js +29 -21
- package/dist/utils/orderValidation.js +18 -0
- package/package.json +1 -1
- package/src/generated/wspace-types.ts +1762 -64
- package/src/movethewheels/components/CustomerPicker.tsx +12 -0
- package/src/movethewheels/components/EntityCombobox.tsx +30 -11
- package/src/movethewheels/components/OrderItemRow.tsx +44 -0
- package/src/movethewheels/hooks/useCustomers.ts +45 -9
- package/src/movethewheels/hooks/useFleet.ts +214 -36
- package/src/movethewheels/hooks/useInventory.ts +185 -49
- package/src/movethewheels/hooks/useMultiModalShipments.ts +9 -7
- package/src/movethewheels/hooks/useRoutes.ts +87 -16
- package/src/movethewheels/hooks/useTracking.ts +23 -15
- package/src/movethewheels/pages/CreateOrderPage.tsx +66 -2
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +14 -8
- package/src/movethewheels/pages/CustomersPage.tsx +5 -2
- package/src/movethewheels/pages/DashboardPage.tsx +3 -1
- package/src/movethewheels/pages/DriverDetailsPage.tsx +25 -4
- package/src/movethewheels/pages/DriversPage.tsx +8 -2
- package/src/movethewheels/pages/FleetPage.tsx +1 -1
- package/src/movethewheels/pages/InventoryPage.tsx +20 -16
- package/src/movethewheels/pages/LiveTrackingPage.tsx +6 -4
- package/src/movethewheels/pages/MultiModalShipmentPage.tsx +15 -1
- package/src/movethewheels/pages/OrderDetailsPage.tsx +25 -25
- package/src/movethewheels/pages/ProductDetailsPage.tsx +228 -49
- package/src/movethewheels/pages/ProductsPage.tsx +13 -5
- package/src/movethewheels/pages/RouteDetailsPage.tsx +24 -12
- package/src/movethewheels/pages/RoutesPage.tsx +4 -2
- package/src/movethewheels/pages/TrackingPage.tsx +32 -12
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +42 -29
- package/src/movethewheels/pages/WarehousesPage.tsx +23 -15
- package/src/movethewheels/types/index.ts +36 -24
- package/src/movethewheels/utils/formatters.ts +1 -1
- 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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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 ??
|
|
416
|
+
description: product.description ?? undefined,
|
|
417
|
+
category: product.category ?? undefined,
|
|
368
418
|
brand: product.brand ?? undefined,
|
|
369
|
-
dimensions:
|
|
370
|
-
|
|
371
|
-
|
|
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 =
|
|
391
|
-
|
|
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
|
-
|
|
408
|
-
|
|
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:
|
|
424
|
-
phone:
|
|
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
|
|
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
|
-
|
|
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
|
-
[
|
|
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(
|
|
269
|
-
|
|
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: (
|
|
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:
|
|
209
|
-
|
|
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)) :
|
|
227
|
+
scheduledTime: item.scheduledTime ? new Date(String(item.scheduledTime)) : undefined,
|
|
213
228
|
actualTime: item.actualTime ? new Date(String(item.actualTime)) : undefined,
|
|
214
|
-
duration:
|
|
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
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
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 (
|
|
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
|
|
348
|
-
|
|
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
|
|
215
|
-
longitude
|
|
216
|
-
address:
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
);
|