@burdenoff/microfe-movethewheels 2026.703.2 → 2026.703.4
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/{AIAssistantPage-C3cPsWt-.js → AIAssistantPage-3YOnHO9Y.js} +3 -3
- package/dist/{AnalyticsPage-Dz6g2MvK.js → AnalyticsPage-CMpI1070.js} +11 -11
- package/dist/CreateOrderPage-DRAgGo9i.js +490 -0
- package/dist/CustomerDetailsPage-BCeMmOFe.js +615 -0
- package/dist/{CustomersPage-2Y05LVyh.js → CustomersPage-CaNBMXmM.js} +64 -53
- package/dist/{DashboardPage-BVjQi0EB.js → DashboardPage-DIKlXde3.js} +16 -16
- package/dist/DataTable-XvUcTjwi.js +340 -0
- package/dist/DocumentsCard-U2lSe-cn.js +169 -0
- package/dist/{DriverDetailsPage-CN3WQ0B9.js → DriverDetailsPage-BQrYd_Lp.js} +178 -195
- package/dist/{DriversPage-B0Zpx8OQ.js → DriversPage-PYmdFiby.js} +65 -54
- package/dist/FinancePage-Cwh9Bw4M.js +136 -0
- package/dist/{FleetPage-DqkFi3s3.js → FleetPage-3ZHsB-TN.js} +23 -22
- package/dist/{FormError-BlOjnXOq.js → FormError-cuLV2jVD.js} +1 -1
- package/dist/{ImportExportPage-DKJRCezl.js → ImportExportPage-C6f7DqhM.js} +50 -70
- package/dist/{InventoryPage-DwfgCUQq.js → InventoryPage-DpiqVu1v.js} +9 -9
- package/dist/{LiveTrackingPage-DCX-K-LR.js → LiveTrackingPage-FAzzlVnX.js} +9 -9
- package/dist/MarketplacePage-Bh_2ns4v.js +426 -0
- package/dist/Notice-CSoDYKw9.js +46 -0
- package/dist/OrderDetailsPage-zCj0lW0j.js +584 -0
- package/dist/{OrdersListPage-BjDlCKJd.js → OrdersListPage-CAMsuUck.js} +166 -89
- package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-wsxAQkA0.js} +12 -12
- package/dist/{ProductsPage-C1G9h7Xd.js → ProductsPage-DC5qDLfz.js} +60 -49
- package/dist/ReportsPage-BwUJ37eO.js +356 -0
- package/dist/RouteDetailsPage-DyMJE9AS.js +583 -0
- package/dist/{RoutesPage-DMfoicM1.js → RoutesPage-Dtc669yc.js} +57 -46
- package/dist/{SettingsPage-Bn-v0mbQ.js → SettingsPage-CpH9xhQg.js} +5 -27
- package/dist/{TrackingPage-W-jtdM3Y.js → TrackingPage-DaNEFG_h.js} +10 -10
- package/dist/{VehicleDetailsPage-Bceo5oz3.js → VehicleDetailsPage-DldYNsYV.js} +125 -142
- package/dist/{VehiclesPage-VBtOep91.js → VehiclesPage-BFw13iMt.js} +61 -50
- package/dist/{WarehouseDetailsPage-L_6sWigM.js → WarehouseDetailsPage-CMG7or6G.js} +17 -17
- package/dist/{WarehousesPage-DCbXXXWE.js → WarehousesPage-Wkytnt86.js} +58 -47
- package/dist/arrow-right-Q3XQpTPR.js +11 -0
- package/dist/arrow-up-xNF1zJUG.js +17 -0
- package/dist/chevron-left-B2_r0Elx.js +8 -0
- package/dist/{wrench-CANXWxF-.js → fuel-B6_lnKDs.js} +2 -5
- package/dist/index.js +163 -174
- package/dist/upload-CHZofOqb.js +21 -0
- package/dist/{useAnalytics-DS8n0xgg.js → useAnalytics-D7HW2XRb.js} +52 -2
- package/dist/{useCustomers-BmTNGxiI.js → useCustomers-BPW4__PR.js} +29 -26
- package/dist/{useFleet-DKqXZ16m.js → useFleet-ChBkxAd6.js} +81 -69
- package/dist/{useInventory-DkyiVY_A.js → useInventory-CAAVMRp6.js} +83 -38
- package/dist/{useOrders-Cfk1GLiq.js → useOrders-B465S2nt.js} +58 -47
- package/dist/{useReports-CTqZr-AB.js → useReports-jQ3GpNOm.js} +40 -34
- package/dist/{useRoutes-BQWO607N.js → useRoutes-hS3d5-xr.js} +49 -41
- package/dist/{useTracking-B-va1kO3.js → useTracking-RjGz9PZG.js} +2 -2
- package/dist/user-check-BEKRCi7v.js +20 -0
- package/dist/wrench-q_rgJfaP.js +8 -0
- package/dist/x-tMeDMcCw.js +11 -0
- package/package.json +1 -1
- package/src/movethewheels/components/DataTable.tsx +252 -84
- package/src/movethewheels/components/DocumentsCard.tsx +102 -0
- package/src/movethewheels/components/Notice.tsx +61 -0
- package/src/movethewheels/components/index.ts +7 -0
- package/src/movethewheels/hooks/index.ts +9 -0
- package/src/movethewheels/hooks/useAnalytics.ts +95 -0
- package/src/movethewheels/hooks/useCustomers.ts +11 -4
- package/src/movethewheels/hooks/useEntityDocuments.ts +132 -0
- package/src/movethewheels/hooks/useFleet.ts +26 -7
- package/src/movethewheels/hooks/useInventory.ts +70 -7
- package/src/movethewheels/hooks/useOrders.ts +32 -2
- package/src/movethewheels/hooks/useReports.ts +14 -4
- package/src/movethewheels/hooks/useRoutes.ts +20 -3
- package/src/movethewheels/pages/AnalyticsPage.tsx +2 -2
- package/src/movethewheels/pages/CreateOrderPage.tsx +44 -16
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +239 -33
- package/src/movethewheels/pages/CustomersPage.tsx +27 -11
- package/src/movethewheels/pages/DriverDetailsPage.tsx +16 -30
- package/src/movethewheels/pages/DriversPage.tsx +26 -9
- package/src/movethewheels/pages/FinancePage.tsx +67 -84
- package/src/movethewheels/pages/InventoryPage.tsx +1 -1
- package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
- package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
- package/src/movethewheels/pages/OrdersListPage.tsx +77 -14
- package/src/movethewheels/pages/ProductsPage.tsx +27 -7
- package/src/movethewheels/pages/ReportsPage.tsx +65 -4
- package/src/movethewheels/pages/RouteDetailsPage.tsx +74 -13
- package/src/movethewheels/pages/RoutesPage.tsx +22 -6
- package/src/movethewheels/pages/SettingsPage.tsx +3 -19
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +3 -33
- package/src/movethewheels/pages/VehiclesPage.tsx +26 -5
- package/src/movethewheels/pages/WarehousesPage.tsx +22 -17
- package/src/movethewheels/types/index.ts +8 -0
- package/src/movethewheels/utils/download.ts +62 -0
- package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
- package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
- package/dist/DataTable-DmciIKKM.js +0 -249
- package/dist/FinancePage-DEDykSOP.js +0 -158
- package/dist/MarketplacePage-CIQ97_zp.js +0 -247
- package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
- package/dist/ReportsPage-Zp53q3_8.js +0 -284
- package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
- /package/dist/{MetricCard-BY0QrMD4.js → MetricCard-CVBVpu7o.js} +0 -0
- /package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-C87mDpDP.js} +0 -0
- /package/dist/{box-D9Pal4HM.js → box-Eu05PXDh.js} +0 -0
- /package/dist/{chart-column-BFIJysDR.js → chart-column-ouzgxIv_.js} +0 -0
- /package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-eV4LjaSi.js} +0 -0
- /package/dist/{circle-alert-iY24lSlp.js → circle-alert-Bzh2DBSF.js} +0 -0
- /package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-CGMSpQmD.js} +0 -0
- /package/dist/{clock-DJZtdl8u.js → clock-Bl3ptexL.js} +0 -0
- /package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-CAVU95SO.js} +0 -0
- /package/dist/{download-DRXxoiQR.js → download-C5NbvVDc.js} +0 -0
- /package/dist/{file-text-DWWY6_f5.js → file-text-D-1uvLZG.js} +0 -0
- /package/dist/{filter-DloLC2eE.js → filter-DUfUg6BO.js} +0 -0
- /package/dist/{formatters-CF2S2l1m.js → formatters-BQr-aR5W.js} +0 -0
- /package/dist/{map-CkJaULb2.js → map-C-M-FQLR.js} +0 -0
- /package/dist/{map-pin-BGiCvyCs.js → map-pin-Dznl5l40.js} +0 -0
- /package/dist/{navigation-D36prnAn.js → navigation-BJSJl-VG.js} +0 -0
- /package/dist/{package-BChUzpJS.js → package-CcRttV2N.js} +0 -0
- /package/dist/{phone-Ac9s3mo_.js → phone-DVSZHt_j.js} +0 -0
- /package/dist/{plus-Z76q-FZm.js → plus-C_XZDwBz.js} +0 -0
- /package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-CfgN67l3.js} +0 -0
- /package/dist/{save-CojywNhG.js → save-DJE8Fpdv.js} +0 -0
- /package/dist/{search-CDGefqb3.js → search-D_VyGcZT.js} +0 -0
- /package/dist/{square-pen-CopE9GmB.js → square-pen-D-6JYrVQ.js} +0 -0
- /package/dist/{star-nHlLjDKR.js → star-BpRlUZuU.js} +0 -0
- /package/dist/{store-BR9Voi5E.js → store-B-8HMuyZ.js} +0 -0
- /package/dist/{trash-2-BKhar6Hs.js → trash-2-8vmv-XV_.js} +0 -0
- /package/dist/{trending-up-Dohgd4t_.js → trending-up-Dz6PqPRj.js} +0 -0
- /package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-D7Y3J_4z.js} +0 -0
- /package/dist/{truck-C0v_hbLR.js → truck-D-y2IDzv.js} +0 -0
- /package/dist/{useAssistant-D4v3oxD3.js → useAssistant-DVW4QgQb.js} +0 -0
- /package/dist/{useImportExport-DHr-SWSw.js → useImportExport-DrPDHEDK.js} +0 -0
- /package/dist/{useSettings-CkDH_-gM.js → useSettings-BB8X2RLh.js} +0 -0
- /package/dist/{user-jyFU3lXk.js → user-DzfTn4dE.js} +0 -0
- /package/dist/{users-DZ-ilS6B.js → users-CCrq-w0C.js} +0 -0
- /package/dist/{warehouse-6m5kXMLu.js → warehouse-CaqfmMKh.js} +0 -0
|
@@ -469,6 +469,101 @@ export function useFleetAnalytics() {
|
|
|
469
469
|
return { fleetData, isLoading, error };
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
+
const FINANCE_METRICS_QUERY = `
|
|
473
|
+
query GetMovethewheelsFinanceMetrics {
|
|
474
|
+
financeMetrics {
|
|
475
|
+
revenue {
|
|
476
|
+
totalRevenue
|
|
477
|
+
deliveredRevenue
|
|
478
|
+
averageOrderValue
|
|
479
|
+
}
|
|
480
|
+
fuelCost
|
|
481
|
+
openInvoices
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
`;
|
|
485
|
+
|
|
486
|
+
export interface FinanceMetrics {
|
|
487
|
+
totalRevenue: number;
|
|
488
|
+
deliveredRevenue: number;
|
|
489
|
+
averageOrderValue: number;
|
|
490
|
+
fuelCost: number;
|
|
491
|
+
openInvoices: number;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Real finance figures straight from Query.financeMetrics — no simulated splits.
|
|
496
|
+
* `deliveredRevenue` is money already collected on delivered orders; `openInvoices`
|
|
497
|
+
* is the count of orders still owing. Both come from the service, never inferred.
|
|
498
|
+
*/
|
|
499
|
+
export function useFinanceMetrics() {
|
|
500
|
+
const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
|
|
501
|
+
useMoveTheWheels();
|
|
502
|
+
const [metrics, setMetrics] = useState<FinanceMetrics>({
|
|
503
|
+
totalRevenue: 0,
|
|
504
|
+
deliveredRevenue: 0,
|
|
505
|
+
averageOrderValue: 0,
|
|
506
|
+
fuelCost: 0,
|
|
507
|
+
openInvoices: 0,
|
|
508
|
+
});
|
|
509
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
510
|
+
const [error, setError] = useState<Error | null>(null);
|
|
511
|
+
|
|
512
|
+
const graphQLOptions = useMemo(
|
|
513
|
+
() => ({
|
|
514
|
+
apiGatewayUrl,
|
|
515
|
+
authToken,
|
|
516
|
+
workspaceToken,
|
|
517
|
+
workspaceId,
|
|
518
|
+
tenantId,
|
|
519
|
+
actorId: currentUser?.id,
|
|
520
|
+
}),
|
|
521
|
+
[apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
const load = useCallback(async () => {
|
|
525
|
+
setIsLoading(true);
|
|
526
|
+
setError(null);
|
|
527
|
+
try {
|
|
528
|
+
const data = await executeGraphQL<{
|
|
529
|
+
financeMetrics: {
|
|
530
|
+
revenue: {
|
|
531
|
+
totalRevenue: number;
|
|
532
|
+
deliveredRevenue: number;
|
|
533
|
+
averageOrderValue: number;
|
|
534
|
+
};
|
|
535
|
+
fuelCost: number;
|
|
536
|
+
openInvoices: number;
|
|
537
|
+
};
|
|
538
|
+
}>(graphQLOptions, FINANCE_METRICS_QUERY);
|
|
539
|
+
|
|
540
|
+
setMetrics({
|
|
541
|
+
totalRevenue: data.financeMetrics.revenue.totalRevenue,
|
|
542
|
+
deliveredRevenue: data.financeMetrics.revenue.deliveredRevenue,
|
|
543
|
+
averageOrderValue: data.financeMetrics.revenue.averageOrderValue,
|
|
544
|
+
fuelCost: data.financeMetrics.fuelCost,
|
|
545
|
+
openInvoices: data.financeMetrics.openInvoices,
|
|
546
|
+
});
|
|
547
|
+
} catch (fetchError) {
|
|
548
|
+
setError(
|
|
549
|
+
fetchError instanceof Error ? fetchError : new Error('Failed to load finance metrics')
|
|
550
|
+
);
|
|
551
|
+
} finally {
|
|
552
|
+
setIsLoading(false);
|
|
553
|
+
}
|
|
554
|
+
}, [graphQLOptions]);
|
|
555
|
+
|
|
556
|
+
useEffect(() => {
|
|
557
|
+
void load();
|
|
558
|
+
}, [load]);
|
|
559
|
+
|
|
560
|
+
const refetch = useCallback(() => {
|
|
561
|
+
void load();
|
|
562
|
+
}, [load]);
|
|
563
|
+
|
|
564
|
+
return { metrics, isLoading, error, refetch };
|
|
565
|
+
}
|
|
566
|
+
|
|
472
567
|
export function usePerformanceAnalytics() {
|
|
473
568
|
const { analytics, isLoading, error } = useAnalytics();
|
|
474
569
|
|
|
@@ -12,6 +12,8 @@ interface UseCustomersOptions {
|
|
|
12
12
|
interface UseCustomersResult {
|
|
13
13
|
customers: Customer[];
|
|
14
14
|
totalCustomers: number;
|
|
15
|
+
/** True when the last fetch returned a full page (rawCount === limit). */
|
|
16
|
+
hasMore: boolean;
|
|
15
17
|
isLoading: boolean;
|
|
16
18
|
error: Error | null;
|
|
17
19
|
refetch: () => void;
|
|
@@ -151,9 +153,10 @@ function mapCustomer(customer: ApiCustomer): Customer {
|
|
|
151
153
|
|
|
152
154
|
export function useCustomers(options: UseCustomersOptions = {}): UseCustomersResult {
|
|
153
155
|
const { filters: rawFilters, limit = 20, offset = 0 } = options;
|
|
154
|
-
// Stabilize filters by its
|
|
155
|
-
// `filters` dependency
|
|
156
|
-
//
|
|
156
|
+
// Stabilize the filters reference by its primitive fields so React Compiler's
|
|
157
|
+
// inferred `filters` dependency stays stable and loadCustomers is not
|
|
158
|
+
// recreated (and refetched) on every render when the caller passes an inline
|
|
159
|
+
// filters object.
|
|
157
160
|
const filters = useMemo(
|
|
158
161
|
() => rawFilters,
|
|
159
162
|
[rawFilters?.isActive, rawFilters?.search, rawFilters?.type]
|
|
@@ -162,6 +165,7 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
|
|
|
162
165
|
useMoveTheWheels();
|
|
163
166
|
|
|
164
167
|
const [customers, setCustomers] = useState<Customer[]>([]);
|
|
168
|
+
const [hasMore, setHasMore] = useState(false);
|
|
165
169
|
const [isLoading, setIsLoading] = useState(false);
|
|
166
170
|
const [error, setError] = useState<Error | null>(null);
|
|
167
171
|
|
|
@@ -191,7 +195,9 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
|
|
|
191
195
|
pagination: { limit, offset },
|
|
192
196
|
});
|
|
193
197
|
|
|
194
|
-
|
|
198
|
+
const raw = data.customers;
|
|
199
|
+
setHasMore(raw.length === limit);
|
|
200
|
+
let mapped = raw.map(mapCustomer);
|
|
195
201
|
if (filters?.isActive !== undefined) {
|
|
196
202
|
mapped = mapped.filter((item) => item.isActive === filters.isActive);
|
|
197
203
|
}
|
|
@@ -275,6 +281,7 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
|
|
|
275
281
|
return {
|
|
276
282
|
customers,
|
|
277
283
|
totalCustomers: customers.length,
|
|
284
|
+
hasMore,
|
|
278
285
|
isLoading,
|
|
279
286
|
error,
|
|
280
287
|
refetch,
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useEntityDocuments — real document listing via wspace-files-svc.
|
|
3
|
+
*
|
|
4
|
+
* MoveTheWheels does NOT own a Document model. Documents attached to a vehicle,
|
|
5
|
+
* driver or order live in the platform Files module (wspace-files-svc), which
|
|
6
|
+
* federates into the same workspace gateway. We tag each uploaded file with two
|
|
7
|
+
* marqueue entity tags — `mtwEntityType` and `mtwEntityId` — and list them back
|
|
8
|
+
* here through the federated `files(filter:{ tags:[...] })` query.
|
|
9
|
+
*
|
|
10
|
+
* There is no upload UI here (out of scope): uploading/managing happens in the
|
|
11
|
+
* Files module, deep-linked from DocumentsCard. This hook is read-only.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
15
|
+
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
16
|
+
import { executeGraphQL } from '../utils/graphqlClient';
|
|
17
|
+
|
|
18
|
+
export type EntityDocumentType = 'vehicle' | 'driver' | 'order';
|
|
19
|
+
|
|
20
|
+
export interface EntityDocument {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
/** Byte size (File.size is BigInt → string over JSON). */
|
|
24
|
+
size: number;
|
|
25
|
+
mimeType: string;
|
|
26
|
+
createdAt: Date;
|
|
27
|
+
/** Presigned download/open link, when the files service exposes one. */
|
|
28
|
+
downloadUrl?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ApiFile {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
size: string | number | null;
|
|
35
|
+
mimeType: string;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
downloadUrl?: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface UseEntityDocumentsResult {
|
|
41
|
+
documents: EntityDocument[];
|
|
42
|
+
isLoading: boolean;
|
|
43
|
+
error: Error | null;
|
|
44
|
+
refetch: () => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const LIST_ENTITY_FILES_QUERY = `
|
|
48
|
+
query ListEntityDocuments($filter: FileFilterInput, $pagination: PaginationInput) {
|
|
49
|
+
files(filter: $filter, pagination: $pagination) {
|
|
50
|
+
id
|
|
51
|
+
name
|
|
52
|
+
size
|
|
53
|
+
mimeType
|
|
54
|
+
createdAt
|
|
55
|
+
downloadUrl
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
function mapFile(file: ApiFile): EntityDocument {
|
|
61
|
+
return {
|
|
62
|
+
id: file.id,
|
|
63
|
+
name: file.name,
|
|
64
|
+
size: file.size != null ? Number(file.size) : 0,
|
|
65
|
+
mimeType: file.mimeType,
|
|
66
|
+
createdAt: new Date(file.createdAt),
|
|
67
|
+
downloadUrl: file.downloadUrl ?? undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function useEntityDocuments(
|
|
72
|
+
entityType: EntityDocumentType,
|
|
73
|
+
entityId: string | undefined
|
|
74
|
+
): UseEntityDocumentsResult {
|
|
75
|
+
const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
|
|
76
|
+
useMoveTheWheels();
|
|
77
|
+
const [documents, setDocuments] = useState<EntityDocument[]>([]);
|
|
78
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
79
|
+
const [error, setError] = useState<Error | null>(null);
|
|
80
|
+
|
|
81
|
+
const graphQLOptions = useMemo(
|
|
82
|
+
() => ({
|
|
83
|
+
apiGatewayUrl,
|
|
84
|
+
authToken,
|
|
85
|
+
workspaceToken,
|
|
86
|
+
workspaceId,
|
|
87
|
+
tenantId,
|
|
88
|
+
actorId: currentUser?.id,
|
|
89
|
+
}),
|
|
90
|
+
[apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const load = useCallback(async () => {
|
|
94
|
+
if (!entityId) {
|
|
95
|
+
setDocuments([]);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
setIsLoading(true);
|
|
99
|
+
setError(null);
|
|
100
|
+
try {
|
|
101
|
+
const data = await executeGraphQL<{ files: ApiFile[] }>(
|
|
102
|
+
graphQLOptions,
|
|
103
|
+
LIST_ENTITY_FILES_QUERY,
|
|
104
|
+
{
|
|
105
|
+
filter: {
|
|
106
|
+
tags: [
|
|
107
|
+
{ key: 'mtwEntityType', value: entityType },
|
|
108
|
+
{ key: 'mtwEntityId', value: entityId },
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
pagination: { limit: 50, offset: 0 },
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
setDocuments((data.files ?? []).map(mapFile));
|
|
115
|
+
} catch (fetchError) {
|
|
116
|
+
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load documents'));
|
|
117
|
+
setDocuments([]);
|
|
118
|
+
} finally {
|
|
119
|
+
setIsLoading(false);
|
|
120
|
+
}
|
|
121
|
+
}, [entityId, entityType, graphQLOptions]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
void load();
|
|
125
|
+
}, [load]);
|
|
126
|
+
|
|
127
|
+
const refetch = useCallback(() => {
|
|
128
|
+
void load();
|
|
129
|
+
}, [load]);
|
|
130
|
+
|
|
131
|
+
return { documents, isLoading, error, refetch };
|
|
132
|
+
}
|
|
@@ -428,7 +428,9 @@ function useGraphqlOptions() {
|
|
|
428
428
|
|
|
429
429
|
export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
|
|
430
430
|
const { filters: rawFilters } = options;
|
|
431
|
-
// Stabilize filters reference
|
|
431
|
+
// Stabilize the filters reference by its primitive fields so React Compiler's
|
|
432
|
+
// inferred `filters` dependency stays stable and `load` is not recreated (and
|
|
433
|
+
// refetched) every render when the caller passes an inline filters object.
|
|
432
434
|
const filters = useMemo(
|
|
433
435
|
() => rawFilters,
|
|
434
436
|
[rawFilters?.driverId, rawFilters?.status, rawFilters?.vehicleType]
|
|
@@ -534,9 +536,16 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
|
|
|
534
536
|
};
|
|
535
537
|
}
|
|
536
538
|
|
|
539
|
+
interface UseListOptions {
|
|
540
|
+
limit?: number;
|
|
541
|
+
offset?: number;
|
|
542
|
+
}
|
|
543
|
+
|
|
537
544
|
interface UseVehiclesResult {
|
|
538
545
|
vehicles: Vehicle[];
|
|
539
546
|
totalVehicles: number;
|
|
547
|
+
/** True when the last fetch returned a full page (rawCount === limit). */
|
|
548
|
+
hasMore: boolean;
|
|
540
549
|
isLoading: boolean;
|
|
541
550
|
error: Error | null;
|
|
542
551
|
refetch: () => void;
|
|
@@ -544,9 +553,11 @@ interface UseVehiclesResult {
|
|
|
544
553
|
assignDriver: (vehicleId: string, driverId: string) => Promise<Vehicle>;
|
|
545
554
|
}
|
|
546
555
|
|
|
547
|
-
export function useVehicles(): UseVehiclesResult {
|
|
556
|
+
export function useVehicles(options: UseListOptions = {}): UseVehiclesResult {
|
|
557
|
+
const { limit = 200, offset = 0 } = options;
|
|
548
558
|
const graphQLOptions = useGraphqlOptions();
|
|
549
559
|
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
|
560
|
+
const [hasMore, setHasMore] = useState(false);
|
|
550
561
|
const [isLoading, setIsLoading] = useState(false);
|
|
551
562
|
const [error, setError] = useState<Error | null>(null);
|
|
552
563
|
|
|
@@ -558,16 +569,17 @@ export function useVehicles(): UseVehiclesResult {
|
|
|
558
569
|
graphQLOptions,
|
|
559
570
|
LIST_VEHICLES_QUERY,
|
|
560
571
|
{
|
|
561
|
-
pagination: { limit
|
|
572
|
+
pagination: { limit, offset },
|
|
562
573
|
}
|
|
563
574
|
);
|
|
575
|
+
setHasMore(data.vehicles.length === limit);
|
|
564
576
|
setVehicles(data.vehicles.map(mapVehicle));
|
|
565
577
|
} catch (fetchError) {
|
|
566
578
|
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load vehicles'));
|
|
567
579
|
} finally {
|
|
568
580
|
setIsLoading(false);
|
|
569
581
|
}
|
|
570
|
-
}, [graphQLOptions]);
|
|
582
|
+
}, [graphQLOptions, limit, offset]);
|
|
571
583
|
|
|
572
584
|
useEffect(() => {
|
|
573
585
|
void load();
|
|
@@ -618,6 +630,7 @@ export function useVehicles(): UseVehiclesResult {
|
|
|
618
630
|
return {
|
|
619
631
|
vehicles,
|
|
620
632
|
totalVehicles: vehicles.length,
|
|
633
|
+
hasMore,
|
|
621
634
|
isLoading,
|
|
622
635
|
error,
|
|
623
636
|
refetch,
|
|
@@ -670,6 +683,8 @@ export function useVehicle(vehicleId: string | undefined) {
|
|
|
670
683
|
interface UseDriversResult {
|
|
671
684
|
drivers: Driver[];
|
|
672
685
|
totalDrivers: number;
|
|
686
|
+
/** True when the last fetch returned a full page (rawCount === limit). */
|
|
687
|
+
hasMore: boolean;
|
|
673
688
|
isLoading: boolean;
|
|
674
689
|
error: Error | null;
|
|
675
690
|
refetch: () => void;
|
|
@@ -677,9 +692,11 @@ interface UseDriversResult {
|
|
|
677
692
|
assignVehicle: (driverId: string, vehicleId: string) => Promise<Driver>;
|
|
678
693
|
}
|
|
679
694
|
|
|
680
|
-
export function useDrivers(): UseDriversResult {
|
|
695
|
+
export function useDrivers(options: UseListOptions = {}): UseDriversResult {
|
|
696
|
+
const { limit = 200, offset = 0 } = options;
|
|
681
697
|
const graphQLOptions = useGraphqlOptions();
|
|
682
698
|
const [drivers, setDrivers] = useState<Driver[]>([]);
|
|
699
|
+
const [hasMore, setHasMore] = useState(false);
|
|
683
700
|
const [isLoading, setIsLoading] = useState(false);
|
|
684
701
|
const [error, setError] = useState<Error | null>(null);
|
|
685
702
|
|
|
@@ -691,16 +708,17 @@ export function useDrivers(): UseDriversResult {
|
|
|
691
708
|
graphQLOptions,
|
|
692
709
|
LIST_DRIVERS_QUERY,
|
|
693
710
|
{
|
|
694
|
-
pagination: { limit
|
|
711
|
+
pagination: { limit, offset },
|
|
695
712
|
}
|
|
696
713
|
);
|
|
714
|
+
setHasMore(data.drivers.length === limit);
|
|
697
715
|
setDrivers(data.drivers.map(mapDriver));
|
|
698
716
|
} catch (fetchError) {
|
|
699
717
|
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load drivers'));
|
|
700
718
|
} finally {
|
|
701
719
|
setIsLoading(false);
|
|
702
720
|
}
|
|
703
|
-
}, [graphQLOptions]);
|
|
721
|
+
}, [graphQLOptions, limit, offset]);
|
|
704
722
|
|
|
705
723
|
useEffect(() => {
|
|
706
724
|
void load();
|
|
@@ -747,6 +765,7 @@ export function useDrivers(): UseDriversResult {
|
|
|
747
765
|
return {
|
|
748
766
|
drivers,
|
|
749
767
|
totalDrivers: drivers.length,
|
|
768
|
+
hasMore,
|
|
750
769
|
isLoading,
|
|
751
770
|
error,
|
|
752
771
|
refetch,
|
|
@@ -356,7 +356,9 @@ function useGraphqlOptions() {
|
|
|
356
356
|
|
|
357
357
|
export function useInventory(options: UseInventoryOptions = {}): UseInventoryResult {
|
|
358
358
|
const { filters: rawFilters } = options;
|
|
359
|
-
// Stabilize filters reference
|
|
359
|
+
// Stabilize the filters reference by its primitive fields so React Compiler's
|
|
360
|
+
// inferred `filters` dependency stays stable and `load` is not recreated (and
|
|
361
|
+
// refetched) every render when the caller passes an inline filters object.
|
|
360
362
|
const filters = useMemo(
|
|
361
363
|
() => rawFilters,
|
|
362
364
|
[rawFilters?.lowStock, rawFilters?.productId, rawFilters?.warehouseId]
|
|
@@ -532,9 +534,16 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
|
|
|
532
534
|
};
|
|
533
535
|
}
|
|
534
536
|
|
|
535
|
-
|
|
537
|
+
interface UseListOptions {
|
|
538
|
+
limit?: number;
|
|
539
|
+
offset?: number;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function useProducts(options: UseListOptions = {}) {
|
|
543
|
+
const { limit = 500, offset = 0 } = options;
|
|
536
544
|
const graphQLOptions = useGraphqlOptions();
|
|
537
545
|
const [products, setProducts] = useState<Product[]>([]);
|
|
546
|
+
const [hasMore, setHasMore] = useState(false);
|
|
538
547
|
const [isLoading, setIsLoading] = useState(false);
|
|
539
548
|
const [error, setError] = useState<Error | null>(null);
|
|
540
549
|
|
|
@@ -546,16 +555,17 @@ export function useProducts() {
|
|
|
546
555
|
graphQLOptions,
|
|
547
556
|
LIST_PRODUCTS_QUERY,
|
|
548
557
|
{
|
|
549
|
-
pagination: { limit
|
|
558
|
+
pagination: { limit, offset },
|
|
550
559
|
}
|
|
551
560
|
);
|
|
561
|
+
setHasMore(data.products.length === limit);
|
|
552
562
|
setProducts(data.products.map(mapProduct));
|
|
553
563
|
} catch (fetchError) {
|
|
554
564
|
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load products'));
|
|
555
565
|
} finally {
|
|
556
566
|
setIsLoading(false);
|
|
557
567
|
}
|
|
558
|
-
}, [graphQLOptions]);
|
|
568
|
+
}, [graphQLOptions, limit, offset]);
|
|
559
569
|
|
|
560
570
|
useEffect(() => {
|
|
561
571
|
void load();
|
|
@@ -564,12 +574,33 @@ export function useProducts() {
|
|
|
564
574
|
return {
|
|
565
575
|
products,
|
|
566
576
|
totalProducts: products.length,
|
|
577
|
+
hasMore,
|
|
567
578
|
isLoading,
|
|
568
579
|
error,
|
|
569
580
|
refetch: () => void load(),
|
|
570
581
|
};
|
|
571
582
|
}
|
|
572
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Bounded aggregate for the Products page "at a glance" tiles. Fetches up to
|
|
586
|
+
* `sampleSize` products (independent of the paginated list) and computes the
|
|
587
|
+
* special-handling / temperature-controlled counts so the tiles never reflect
|
|
588
|
+
* a single truncated page. There is no product *Count field on the frozen
|
|
589
|
+
* supergraph, so this bounded sample is the aggregate source.
|
|
590
|
+
*/
|
|
591
|
+
export function useProductStats(sampleSize = 500) {
|
|
592
|
+
const { products, isLoading, error } = useProducts({ limit: sampleSize, offset: 0 });
|
|
593
|
+
|
|
594
|
+
const stats = useMemo(() => {
|
|
595
|
+
const total = products.length;
|
|
596
|
+
const specialHandling = products.filter((item) => item.fragile || item.hazardous).length;
|
|
597
|
+
const temperatureControlled = products.filter((item) => item.temperatureControlled).length;
|
|
598
|
+
return { total, specialHandling, temperatureControlled };
|
|
599
|
+
}, [products]);
|
|
600
|
+
|
|
601
|
+
return { stats, isLoading, error };
|
|
602
|
+
}
|
|
603
|
+
|
|
573
604
|
export function useProduct(productId: string | undefined) {
|
|
574
605
|
const graphQLOptions = useGraphqlOptions();
|
|
575
606
|
const [product, setProduct] = useState<Product | null>(null);
|
|
@@ -606,9 +637,11 @@ export function useProduct(productId: string | undefined) {
|
|
|
606
637
|
return { product, isLoading, error, refetch: () => void load() };
|
|
607
638
|
}
|
|
608
639
|
|
|
609
|
-
export function useWarehouses() {
|
|
640
|
+
export function useWarehouses(options: UseListOptions = {}) {
|
|
641
|
+
const { limit = 500, offset = 0 } = options;
|
|
610
642
|
const graphQLOptions = useGraphqlOptions();
|
|
611
643
|
const [warehouses, setWarehouses] = useState<Warehouse[]>([]);
|
|
644
|
+
const [hasMore, setHasMore] = useState(false);
|
|
612
645
|
const [isLoading, setIsLoading] = useState(false);
|
|
613
646
|
const [error, setError] = useState<Error | null>(null);
|
|
614
647
|
|
|
@@ -620,16 +653,17 @@ export function useWarehouses() {
|
|
|
620
653
|
graphQLOptions,
|
|
621
654
|
LIST_WAREHOUSES_QUERY,
|
|
622
655
|
{
|
|
623
|
-
pagination: { limit
|
|
656
|
+
pagination: { limit, offset },
|
|
624
657
|
}
|
|
625
658
|
);
|
|
659
|
+
setHasMore(data.warehouses.length === limit);
|
|
626
660
|
setWarehouses(data.warehouses.map(mapWarehouse));
|
|
627
661
|
} catch (fetchError) {
|
|
628
662
|
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load warehouses'));
|
|
629
663
|
} finally {
|
|
630
664
|
setIsLoading(false);
|
|
631
665
|
}
|
|
632
|
-
}, [graphQLOptions]);
|
|
666
|
+
}, [graphQLOptions, limit, offset]);
|
|
633
667
|
|
|
634
668
|
useEffect(() => {
|
|
635
669
|
void load();
|
|
@@ -638,12 +672,41 @@ export function useWarehouses() {
|
|
|
638
672
|
return {
|
|
639
673
|
warehouses,
|
|
640
674
|
totalWarehouses: warehouses.length,
|
|
675
|
+
hasMore,
|
|
641
676
|
isLoading,
|
|
642
677
|
error,
|
|
643
678
|
refetch: () => void load(),
|
|
644
679
|
};
|
|
645
680
|
}
|
|
646
681
|
|
|
682
|
+
/**
|
|
683
|
+
* Bounded aggregate for the Warehouses page "at a glance" tiles — fetches up to
|
|
684
|
+
* `sampleSize` warehouses (independent of the paginated list) so the active /
|
|
685
|
+
* avg-utilization tiles never reflect a single truncated page.
|
|
686
|
+
*/
|
|
687
|
+
export function useWarehouseStats(sampleSize = 500) {
|
|
688
|
+
const { warehouses, isLoading, error } = useWarehouses({ limit: sampleSize, offset: 0 });
|
|
689
|
+
|
|
690
|
+
const stats = useMemo(() => {
|
|
691
|
+
const total = warehouses.length;
|
|
692
|
+
const active = warehouses.filter((item) => item.isActive).length;
|
|
693
|
+
const avgUtilization =
|
|
694
|
+
total > 0
|
|
695
|
+
? warehouses.reduce(
|
|
696
|
+
(sum, item) =>
|
|
697
|
+
sum +
|
|
698
|
+
(item.capacity.totalSpace > 0
|
|
699
|
+
? item.capacity.usedSpace / item.capacity.totalSpace
|
|
700
|
+
: 0),
|
|
701
|
+
0
|
|
702
|
+
) / total
|
|
703
|
+
: 0;
|
|
704
|
+
return { total, active, avgUtilization };
|
|
705
|
+
}, [warehouses]);
|
|
706
|
+
|
|
707
|
+
return { stats, isLoading, error };
|
|
708
|
+
}
|
|
709
|
+
|
|
647
710
|
export function useWarehouse(warehouseId: string | undefined) {
|
|
648
711
|
const graphQLOptions = useGraphqlOptions();
|
|
649
712
|
const [warehouse, setWarehouse] = useState<Warehouse | null>(null);
|
|
@@ -12,6 +12,12 @@ interface UseOrdersOptions {
|
|
|
12
12
|
interface UseOrdersResult {
|
|
13
13
|
orders: Order[];
|
|
14
14
|
totalOrders: number;
|
|
15
|
+
/**
|
|
16
|
+
* True when the most recent fetch returned a full page (rawCount === limit),
|
|
17
|
+
* i.e. there may be another page. Used for offset pagers where no grand
|
|
18
|
+
* total is available.
|
|
19
|
+
*/
|
|
20
|
+
hasMore: boolean;
|
|
15
21
|
isLoading: boolean;
|
|
16
22
|
error: Error | null;
|
|
17
23
|
refetch: () => void;
|
|
@@ -402,11 +408,28 @@ function mapOrder(order: ApiOrder): Order {
|
|
|
402
408
|
}
|
|
403
409
|
|
|
404
410
|
export function useOrders(options: UseOrdersOptions = {}): UseOrdersResult {
|
|
405
|
-
const { filters, limit = 20, offset = 0 } = options;
|
|
411
|
+
const { filters: rawFilters, limit = 20, offset = 0 } = options;
|
|
412
|
+
// React Compiler memoizes loadOrders on the inferred `filters` dependency, so
|
|
413
|
+
// an inline filters object from the caller would recreate the loader every
|
|
414
|
+
// render and refetch forever. Stabilize the reference by its primitive fields:
|
|
415
|
+
// the memoized `filters` only changes identity when a filter value actually
|
|
416
|
+
// changes, so stable-value renders no longer recreate loadOrders.
|
|
417
|
+
const filters = useMemo(
|
|
418
|
+
() => rawFilters,
|
|
419
|
+
[
|
|
420
|
+
rawFilters?.status,
|
|
421
|
+
rawFilters?.priority,
|
|
422
|
+
rawFilters?.orderType,
|
|
423
|
+
rawFilters?.customerId,
|
|
424
|
+
rawFilters?.driverId,
|
|
425
|
+
rawFilters?.unassigned,
|
|
426
|
+
]
|
|
427
|
+
);
|
|
406
428
|
const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
|
|
407
429
|
useMoveTheWheels();
|
|
408
430
|
|
|
409
431
|
const [orders, setOrders] = useState<Order[]>([]);
|
|
432
|
+
const [hasMore, setHasMore] = useState(false);
|
|
410
433
|
const [isLoading, setIsLoading] = useState(false);
|
|
411
434
|
const [error, setError] = useState<Error | null>(null);
|
|
412
435
|
|
|
@@ -441,7 +464,13 @@ export function useOrders(options: UseOrdersOptions = {}): UseOrdersResult {
|
|
|
441
464
|
pagination: { limit, offset },
|
|
442
465
|
});
|
|
443
466
|
|
|
444
|
-
|
|
467
|
+
const raw = data.movethewheelsOrders;
|
|
468
|
+
setHasMore(raw.length === limit);
|
|
469
|
+
let mapped = raw.map(mapOrder);
|
|
470
|
+
if (filters?.unassigned) {
|
|
471
|
+
mapped = mapped.filter((item) => !item.driverId);
|
|
472
|
+
}
|
|
473
|
+
setOrders(mapped);
|
|
445
474
|
} catch (fetchError) {
|
|
446
475
|
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load orders'));
|
|
447
476
|
} finally {
|
|
@@ -554,6 +583,7 @@ export function useOrders(options: UseOrdersOptions = {}): UseOrdersResult {
|
|
|
554
583
|
return {
|
|
555
584
|
orders,
|
|
556
585
|
totalOrders: orders.length,
|
|
586
|
+
hasMore,
|
|
557
587
|
isLoading,
|
|
558
588
|
error,
|
|
559
589
|
refetch,
|
|
@@ -68,8 +68,15 @@ export interface CreateReportInput {
|
|
|
68
68
|
status?: string;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
interface UseReportsOptions {
|
|
72
|
+
limit?: number;
|
|
73
|
+
offset?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
71
76
|
interface UseReportsResult {
|
|
72
77
|
reports: MovethewheelsReport[];
|
|
78
|
+
/** True when the last fetch returned a full page (rawCount === limit). */
|
|
79
|
+
hasMore: boolean;
|
|
73
80
|
isLoading: boolean;
|
|
74
81
|
error: Error | null;
|
|
75
82
|
refetch: () => void;
|
|
@@ -80,10 +87,12 @@ interface UseReportsResult {
|
|
|
80
87
|
) => Promise<GeneratedReport>;
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
export function useReports(): UseReportsResult {
|
|
90
|
+
export function useReports(options: UseReportsOptions = {}): UseReportsResult {
|
|
91
|
+
const { limit = 100, offset = 0 } = options;
|
|
84
92
|
const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
|
|
85
93
|
useMoveTheWheels();
|
|
86
94
|
const [reports, setReports] = useState<MovethewheelsReport[]>([]);
|
|
95
|
+
const [hasMore, setHasMore] = useState(false);
|
|
87
96
|
const [isLoading, setIsLoading] = useState(false);
|
|
88
97
|
const [error, setError] = useState<Error | null>(null);
|
|
89
98
|
|
|
@@ -106,15 +115,16 @@ export function useReports(): UseReportsResult {
|
|
|
106
115
|
const data = await executeGraphQL<{ movethewheelsReports: MovethewheelsReport[] }>(
|
|
107
116
|
graphQLOptions,
|
|
108
117
|
LIST_REPORTS_QUERY,
|
|
109
|
-
{ pagination: { limit
|
|
118
|
+
{ pagination: { limit, offset } }
|
|
110
119
|
);
|
|
120
|
+
setHasMore(data.movethewheelsReports.length === limit);
|
|
111
121
|
setReports(data.movethewheelsReports);
|
|
112
122
|
} catch (fetchError) {
|
|
113
123
|
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load reports'));
|
|
114
124
|
} finally {
|
|
115
125
|
setIsLoading(false);
|
|
116
126
|
}
|
|
117
|
-
}, [graphQLOptions]);
|
|
127
|
+
}, [graphQLOptions, limit, offset]);
|
|
118
128
|
|
|
119
129
|
useEffect(() => {
|
|
120
130
|
void load();
|
|
@@ -154,5 +164,5 @@ export function useReports(): UseReportsResult {
|
|
|
154
164
|
[graphQLOptions]
|
|
155
165
|
);
|
|
156
166
|
|
|
157
|
-
return { reports, isLoading, error, refetch, createReport, generateReport };
|
|
167
|
+
return { reports, hasMore, isLoading, error, refetch, createReport, generateReport };
|
|
158
168
|
}
|