@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.
Files changed (126) hide show
  1. package/dist/{AIAssistantPage-C3cPsWt-.js → AIAssistantPage-3YOnHO9Y.js} +3 -3
  2. package/dist/{AnalyticsPage-Dz6g2MvK.js → AnalyticsPage-CMpI1070.js} +11 -11
  3. package/dist/CreateOrderPage-DRAgGo9i.js +490 -0
  4. package/dist/CustomerDetailsPage-BCeMmOFe.js +615 -0
  5. package/dist/{CustomersPage-2Y05LVyh.js → CustomersPage-CaNBMXmM.js} +64 -53
  6. package/dist/{DashboardPage-BVjQi0EB.js → DashboardPage-DIKlXde3.js} +16 -16
  7. package/dist/DataTable-XvUcTjwi.js +340 -0
  8. package/dist/DocumentsCard-U2lSe-cn.js +169 -0
  9. package/dist/{DriverDetailsPage-CN3WQ0B9.js → DriverDetailsPage-BQrYd_Lp.js} +178 -195
  10. package/dist/{DriversPage-B0Zpx8OQ.js → DriversPage-PYmdFiby.js} +65 -54
  11. package/dist/FinancePage-Cwh9Bw4M.js +136 -0
  12. package/dist/{FleetPage-DqkFi3s3.js → FleetPage-3ZHsB-TN.js} +23 -22
  13. package/dist/{FormError-BlOjnXOq.js → FormError-cuLV2jVD.js} +1 -1
  14. package/dist/{ImportExportPage-DKJRCezl.js → ImportExportPage-C6f7DqhM.js} +50 -70
  15. package/dist/{InventoryPage-DwfgCUQq.js → InventoryPage-DpiqVu1v.js} +9 -9
  16. package/dist/{LiveTrackingPage-DCX-K-LR.js → LiveTrackingPage-FAzzlVnX.js} +9 -9
  17. package/dist/MarketplacePage-Bh_2ns4v.js +426 -0
  18. package/dist/Notice-CSoDYKw9.js +46 -0
  19. package/dist/OrderDetailsPage-zCj0lW0j.js +584 -0
  20. package/dist/{OrdersListPage-BjDlCKJd.js → OrdersListPage-CAMsuUck.js} +166 -89
  21. package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-wsxAQkA0.js} +12 -12
  22. package/dist/{ProductsPage-C1G9h7Xd.js → ProductsPage-DC5qDLfz.js} +60 -49
  23. package/dist/ReportsPage-BwUJ37eO.js +356 -0
  24. package/dist/RouteDetailsPage-DyMJE9AS.js +583 -0
  25. package/dist/{RoutesPage-DMfoicM1.js → RoutesPage-Dtc669yc.js} +57 -46
  26. package/dist/{SettingsPage-Bn-v0mbQ.js → SettingsPage-CpH9xhQg.js} +5 -27
  27. package/dist/{TrackingPage-W-jtdM3Y.js → TrackingPage-DaNEFG_h.js} +10 -10
  28. package/dist/{VehicleDetailsPage-Bceo5oz3.js → VehicleDetailsPage-DldYNsYV.js} +125 -142
  29. package/dist/{VehiclesPage-VBtOep91.js → VehiclesPage-BFw13iMt.js} +61 -50
  30. package/dist/{WarehouseDetailsPage-L_6sWigM.js → WarehouseDetailsPage-CMG7or6G.js} +17 -17
  31. package/dist/{WarehousesPage-DCbXXXWE.js → WarehousesPage-Wkytnt86.js} +58 -47
  32. package/dist/arrow-right-Q3XQpTPR.js +11 -0
  33. package/dist/arrow-up-xNF1zJUG.js +17 -0
  34. package/dist/chevron-left-B2_r0Elx.js +8 -0
  35. package/dist/{wrench-CANXWxF-.js → fuel-B6_lnKDs.js} +2 -5
  36. package/dist/index.js +163 -174
  37. package/dist/upload-CHZofOqb.js +21 -0
  38. package/dist/{useAnalytics-DS8n0xgg.js → useAnalytics-D7HW2XRb.js} +52 -2
  39. package/dist/{useCustomers-BmTNGxiI.js → useCustomers-BPW4__PR.js} +29 -26
  40. package/dist/{useFleet-DKqXZ16m.js → useFleet-ChBkxAd6.js} +81 -69
  41. package/dist/{useInventory-DkyiVY_A.js → useInventory-CAAVMRp6.js} +83 -38
  42. package/dist/{useOrders-Cfk1GLiq.js → useOrders-B465S2nt.js} +58 -47
  43. package/dist/{useReports-CTqZr-AB.js → useReports-jQ3GpNOm.js} +40 -34
  44. package/dist/{useRoutes-BQWO607N.js → useRoutes-hS3d5-xr.js} +49 -41
  45. package/dist/{useTracking-B-va1kO3.js → useTracking-RjGz9PZG.js} +2 -2
  46. package/dist/user-check-BEKRCi7v.js +20 -0
  47. package/dist/wrench-q_rgJfaP.js +8 -0
  48. package/dist/x-tMeDMcCw.js +11 -0
  49. package/package.json +1 -1
  50. package/src/movethewheels/components/DataTable.tsx +252 -84
  51. package/src/movethewheels/components/DocumentsCard.tsx +102 -0
  52. package/src/movethewheels/components/Notice.tsx +61 -0
  53. package/src/movethewheels/components/index.ts +7 -0
  54. package/src/movethewheels/hooks/index.ts +9 -0
  55. package/src/movethewheels/hooks/useAnalytics.ts +95 -0
  56. package/src/movethewheels/hooks/useCustomers.ts +11 -4
  57. package/src/movethewheels/hooks/useEntityDocuments.ts +132 -0
  58. package/src/movethewheels/hooks/useFleet.ts +26 -7
  59. package/src/movethewheels/hooks/useInventory.ts +70 -7
  60. package/src/movethewheels/hooks/useOrders.ts +32 -2
  61. package/src/movethewheels/hooks/useReports.ts +14 -4
  62. package/src/movethewheels/hooks/useRoutes.ts +20 -3
  63. package/src/movethewheels/pages/AnalyticsPage.tsx +2 -2
  64. package/src/movethewheels/pages/CreateOrderPage.tsx +44 -16
  65. package/src/movethewheels/pages/CustomerDetailsPage.tsx +239 -33
  66. package/src/movethewheels/pages/CustomersPage.tsx +27 -11
  67. package/src/movethewheels/pages/DriverDetailsPage.tsx +16 -30
  68. package/src/movethewheels/pages/DriversPage.tsx +26 -9
  69. package/src/movethewheels/pages/FinancePage.tsx +67 -84
  70. package/src/movethewheels/pages/InventoryPage.tsx +1 -1
  71. package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
  72. package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
  73. package/src/movethewheels/pages/OrdersListPage.tsx +77 -14
  74. package/src/movethewheels/pages/ProductsPage.tsx +27 -7
  75. package/src/movethewheels/pages/ReportsPage.tsx +65 -4
  76. package/src/movethewheels/pages/RouteDetailsPage.tsx +74 -13
  77. package/src/movethewheels/pages/RoutesPage.tsx +22 -6
  78. package/src/movethewheels/pages/SettingsPage.tsx +3 -19
  79. package/src/movethewheels/pages/VehicleDetailsPage.tsx +3 -33
  80. package/src/movethewheels/pages/VehiclesPage.tsx +26 -5
  81. package/src/movethewheels/pages/WarehousesPage.tsx +22 -17
  82. package/src/movethewheels/types/index.ts +8 -0
  83. package/src/movethewheels/utils/download.ts +62 -0
  84. package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
  85. package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
  86. package/dist/DataTable-DmciIKKM.js +0 -249
  87. package/dist/FinancePage-DEDykSOP.js +0 -158
  88. package/dist/MarketplacePage-CIQ97_zp.js +0 -247
  89. package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
  90. package/dist/ReportsPage-Zp53q3_8.js +0 -284
  91. package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
  92. /package/dist/{MetricCard-BY0QrMD4.js → MetricCard-CVBVpu7o.js} +0 -0
  93. /package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-C87mDpDP.js} +0 -0
  94. /package/dist/{box-D9Pal4HM.js → box-Eu05PXDh.js} +0 -0
  95. /package/dist/{chart-column-BFIJysDR.js → chart-column-ouzgxIv_.js} +0 -0
  96. /package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-eV4LjaSi.js} +0 -0
  97. /package/dist/{circle-alert-iY24lSlp.js → circle-alert-Bzh2DBSF.js} +0 -0
  98. /package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-CGMSpQmD.js} +0 -0
  99. /package/dist/{clock-DJZtdl8u.js → clock-Bl3ptexL.js} +0 -0
  100. /package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-CAVU95SO.js} +0 -0
  101. /package/dist/{download-DRXxoiQR.js → download-C5NbvVDc.js} +0 -0
  102. /package/dist/{file-text-DWWY6_f5.js → file-text-D-1uvLZG.js} +0 -0
  103. /package/dist/{filter-DloLC2eE.js → filter-DUfUg6BO.js} +0 -0
  104. /package/dist/{formatters-CF2S2l1m.js → formatters-BQr-aR5W.js} +0 -0
  105. /package/dist/{map-CkJaULb2.js → map-C-M-FQLR.js} +0 -0
  106. /package/dist/{map-pin-BGiCvyCs.js → map-pin-Dznl5l40.js} +0 -0
  107. /package/dist/{navigation-D36prnAn.js → navigation-BJSJl-VG.js} +0 -0
  108. /package/dist/{package-BChUzpJS.js → package-CcRttV2N.js} +0 -0
  109. /package/dist/{phone-Ac9s3mo_.js → phone-DVSZHt_j.js} +0 -0
  110. /package/dist/{plus-Z76q-FZm.js → plus-C_XZDwBz.js} +0 -0
  111. /package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-CfgN67l3.js} +0 -0
  112. /package/dist/{save-CojywNhG.js → save-DJE8Fpdv.js} +0 -0
  113. /package/dist/{search-CDGefqb3.js → search-D_VyGcZT.js} +0 -0
  114. /package/dist/{square-pen-CopE9GmB.js → square-pen-D-6JYrVQ.js} +0 -0
  115. /package/dist/{star-nHlLjDKR.js → star-BpRlUZuU.js} +0 -0
  116. /package/dist/{store-BR9Voi5E.js → store-B-8HMuyZ.js} +0 -0
  117. /package/dist/{trash-2-BKhar6Hs.js → trash-2-8vmv-XV_.js} +0 -0
  118. /package/dist/{trending-up-Dohgd4t_.js → trending-up-Dz6PqPRj.js} +0 -0
  119. /package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-D7Y3J_4z.js} +0 -0
  120. /package/dist/{truck-C0v_hbLR.js → truck-D-y2IDzv.js} +0 -0
  121. /package/dist/{useAssistant-D4v3oxD3.js → useAssistant-DVW4QgQb.js} +0 -0
  122. /package/dist/{useImportExport-DHr-SWSw.js → useImportExport-DrPDHEDK.js} +0 -0
  123. /package/dist/{useSettings-CkDH_-gM.js → useSettings-BB8X2RLh.js} +0 -0
  124. /package/dist/{user-jyFU3lXk.js → user-DzfTn4dE.js} +0 -0
  125. /package/dist/{users-DZ-ilS6B.js → users-CCrq-w0C.js} +0 -0
  126. /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 structural fields so the load callback's inferred
155
- // `filters` dependency (React Compiler) matches a stable reference and the
156
- // effect does not re-run on every parent render.
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
- let mapped = data.customers.map(mapCustomer);
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 for React Compiler memoization.
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: 200, offset: 0 },
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: 200, offset: 0 },
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 for React Compiler memoization.
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
- export function useProducts() {
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: 500, offset: 0 },
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: 500, offset: 0 },
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
- setOrders(data.movethewheelsOrders.map(mapOrder));
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: 100, offset: 0 } }
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
  }