@burdenoff/microfe-movethewheels 2026.920.2 → 2026.921.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/components/CarbonFootprintPanel.js +1 -1
  2. package/dist/components/CustomerPicker.js +271 -0
  3. package/dist/components/DataTable.js +140 -139
  4. package/dist/components/DocumentsCard.js +14 -8
  5. package/dist/generated/wspace-types.d.ts +1043 -5
  6. package/dist/hooks/useAnalytics.js +21 -1
  7. package/dist/hooks/useCustomers.js +94 -78
  8. package/dist/hooks/useDebouncedValue.js +11 -0
  9. package/dist/hooks/useEntityDocuments.js +28 -18
  10. package/dist/hooks/useEpod.js +24 -17
  11. package/dist/hooks/useFleet.js +146 -125
  12. package/dist/hooks/useInventory.js +255 -186
  13. package/dist/hooks/useMultiModalShipments.js +35 -27
  14. package/dist/hooks/useOrders.js +73 -73
  15. package/dist/hooks/useRoutes.js +97 -75
  16. package/dist/hooks/useTracking.js +100 -88
  17. package/dist/movethewheels/components/CustomerPicker.d.ts +19 -0
  18. package/dist/movethewheels/components/DataTable.d.ts +5 -1
  19. package/dist/movethewheels/hooks/useCustomers.d.ts +0 -1
  20. package/dist/movethewheels/hooks/useDebouncedValue.d.ts +1 -0
  21. package/dist/movethewheels/hooks/useEntityDocuments.d.ts +2 -0
  22. package/dist/movethewheels/hooks/useEpod.d.ts +6 -1
  23. package/dist/movethewheels/hooks/useFleet.d.ts +49 -5
  24. package/dist/movethewheels/hooks/useInventory.d.ts +24 -17
  25. package/dist/movethewheels/hooks/useMultiModalShipments.d.ts +6 -1
  26. package/dist/movethewheels/hooks/useOrders.d.ts +33 -5
  27. package/dist/movethewheels/hooks/useRoutes.d.ts +2 -1
  28. package/dist/movethewheels/hooks/useTracking.d.ts +12 -11
  29. package/dist/movethewheels/types/index.d.ts +7 -5
  30. package/dist/movethewheels/utils/navigation.d.ts +14 -0
  31. package/dist/pages/CreateOrderPage.js +228 -217
  32. package/dist/pages/CustomerDetailsPage.js +220 -220
  33. package/dist/pages/CustomersPage.js +61 -54
  34. package/dist/pages/DeliveryCapturePage.js +131 -109
  35. package/dist/pages/DriversPage.js +100 -92
  36. package/dist/pages/InventoryPage.js +239 -236
  37. package/dist/pages/MultiModalShipmentPage.js +163 -130
  38. package/dist/pages/OrderDetailsPage.js +224 -208
  39. package/dist/pages/OrdersListPage.js +66 -60
  40. package/dist/pages/ProductDetailsPage.js +241 -200
  41. package/dist/pages/ProductsPage.js +59 -52
  42. package/dist/pages/RouteDetailsPage.js +343 -321
  43. package/dist/pages/RoutesPage.js +55 -48
  44. package/dist/pages/VehiclesPage.js +63 -56
  45. package/dist/pages/WarehousesPage.js +60 -53
  46. package/dist/utils/navigation.js +22 -1
  47. package/package.json +1 -1
  48. package/src/generated/wspace-types.ts +1188 -5
  49. package/src/movethewheels/components/CarbonFootprintPanel.tsx +0 -1
  50. package/src/movethewheels/components/CustomerPicker.tsx +321 -0
  51. package/src/movethewheels/components/DataTable.tsx +34 -18
  52. package/src/movethewheels/components/DocumentsCard.tsx +6 -1
  53. package/src/movethewheels/components/maps/leaflet-setup.ts +1 -8
  54. package/src/movethewheels/hooks/useAnalytics.ts +27 -0
  55. package/src/movethewheels/hooks/useCustomers.ts +64 -24
  56. package/src/movethewheels/hooks/useDebouncedValue.ts +12 -0
  57. package/src/movethewheels/hooks/useEntityDocuments.ts +43 -25
  58. package/src/movethewheels/hooks/useEpod.ts +14 -7
  59. package/src/movethewheels/hooks/useFleet.ts +58 -37
  60. package/src/movethewheels/hooks/useFleetIntelligence.ts +1 -4
  61. package/src/movethewheels/hooks/useInventory.ts +211 -90
  62. package/src/movethewheels/hooks/useMultiModalShipments.ts +15 -11
  63. package/src/movethewheels/hooks/useOrders.ts +13 -14
  64. package/src/movethewheels/hooks/useRoutes.ts +80 -23
  65. package/src/movethewheels/hooks/useTracking.ts +130 -98
  66. package/src/movethewheels/pages/CreateOrderPage.tsx +92 -31
  67. package/src/movethewheels/pages/CustomerDetailsPage.tsx +15 -6
  68. package/src/movethewheels/pages/CustomersPage.tsx +11 -1
  69. package/src/movethewheels/pages/DeliveryCapturePage.tsx +31 -1
  70. package/src/movethewheels/pages/DriversPage.tsx +24 -12
  71. package/src/movethewheels/pages/InventoryPage.tsx +57 -32
  72. package/src/movethewheels/pages/MultiModalShipmentPage.tsx +52 -3
  73. package/src/movethewheels/pages/OrderDetailsPage.tsx +17 -1
  74. package/src/movethewheels/pages/OrdersListPage.tsx +20 -3
  75. package/src/movethewheels/pages/ProductDetailsPage.tsx +52 -3
  76. package/src/movethewheels/pages/ProductsPage.tsx +11 -1
  77. package/src/movethewheels/pages/RouteDetailsPage.tsx +35 -5
  78. package/src/movethewheels/pages/RoutesPage.tsx +11 -1
  79. package/src/movethewheels/pages/VehiclesPage.tsx +19 -2
  80. package/src/movethewheels/pages/WarehousesPage.tsx +11 -1
  81. package/src/movethewheels/types/index.ts +7 -5
  82. package/src/movethewheels/utils/navigation.ts +32 -0
@@ -12,7 +12,6 @@ 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
15
  hasMore: boolean;
17
16
  isLoading: boolean;
18
17
  error: Error | null;
@@ -55,6 +54,7 @@ const LIST_CUSTOMERS_QUERY = `
55
54
  updatedAt
56
55
  deletedAt
57
56
  }
57
+ customersCount(filter: $filter)
58
58
  }
59
59
  `;
60
60
 
@@ -165,6 +165,7 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
165
165
  useMoveTheWheels();
166
166
 
167
167
  const [customers, setCustomers] = useState<Customer[]>([]);
168
+ const [totalCustomers, setTotalCustomers] = useState(0);
168
169
  const [hasMore, setHasMore] = useState(false);
169
170
  const [isLoading, setIsLoading] = useState(false);
170
171
  const [error, setError] = useState<Error | null>(null);
@@ -187,6 +188,7 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
187
188
  try {
188
189
  const data = await executeGraphQL<{
189
190
  customers: ApiCustomer[];
191
+ customersCount: number;
190
192
  }>(graphQLOptions, LIST_CUSTOMERS_QUERY, {
191
193
  filter: {
192
194
  type: filters?.type,
@@ -196,11 +198,9 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
196
198
  });
197
199
 
198
200
  const raw = data.customers;
199
- setHasMore(raw.length === limit);
200
- let mapped = raw.map(mapCustomer);
201
- if (filters?.isActive !== undefined) {
202
- mapped = mapped.filter((item) => item.isActive === filters.isActive);
203
- }
201
+ setTotalCustomers(data.customersCount);
202
+ setHasMore(offset + raw.length < data.customersCount);
203
+ const mapped = raw.map(mapCustomer);
204
204
  setCustomers(mapped);
205
205
  } catch (fetchError) {
206
206
  setError(fetchError instanceof Error ? fetchError : new Error('Failed to load customers'));
@@ -280,7 +280,7 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
280
280
 
281
281
  return {
282
282
  customers,
283
- totalCustomers: customers.length,
283
+ totalCustomers,
284
284
  hasMore,
285
285
  isLoading,
286
286
  error,
@@ -342,25 +342,65 @@ export function useCustomer(customerId: string | undefined) {
342
342
  return { customer, isLoading, error, refetch };
343
343
  }
344
344
 
345
+ const CUSTOMER_STATS_QUERY = `
346
+ query CustomerStats {
347
+ customerStats {
348
+ total
349
+ b2b
350
+ b2c
351
+ active
352
+ inactive
353
+ avgRating
354
+ }
355
+ }
356
+ `;
357
+
345
358
  export function useCustomerStats() {
346
- const { customers, isLoading, error } = useCustomers({ limit: 500, offset: 0 });
347
-
348
- const stats = useMemo(() => {
349
- const total = customers.length;
350
- const b2b = customers.filter((item) => item.type === 'b2b').length;
351
- const b2c = total - b2b;
352
- const active = customers.filter((item) => item.isActive).length;
353
- const totalRating = customers.reduce((sum, item) => sum + item.rating, 0);
354
-
355
- return {
356
- total,
357
- b2b,
358
- b2c,
359
- active,
360
- inactive: total - active,
361
- avgRating: total > 0 ? totalRating / total : 0,
359
+ const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
360
+ useMoveTheWheels();
361
+ const graphQLOptions = useMemo(
362
+ () => ({
363
+ apiGatewayUrl,
364
+ authToken,
365
+ workspaceToken,
366
+ workspaceId,
367
+ tenantId,
368
+ actorId: currentUser?.id,
369
+ }),
370
+ [apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
371
+ );
372
+ const [stats, setStats] = useState({
373
+ total: 0,
374
+ b2b: 0,
375
+ b2c: 0,
376
+ active: 0,
377
+ inactive: 0,
378
+ avgRating: 0,
379
+ });
380
+ const [isLoading, setIsLoading] = useState(false);
381
+ const [error, setError] = useState<Error | null>(null);
382
+
383
+ useEffect(() => {
384
+ const load = async () => {
385
+ setIsLoading(true);
386
+ setError(null);
387
+ try {
388
+ const data = await executeGraphQL<{ customerStats: typeof stats }>(
389
+ graphQLOptions,
390
+ CUSTOMER_STATS_QUERY
391
+ );
392
+ setStats(data.customerStats);
393
+ } catch (fetchError) {
394
+ setError(
395
+ fetchError instanceof Error ? fetchError : new Error('Failed to load customer stats')
396
+ );
397
+ } finally {
398
+ setIsLoading(false);
399
+ }
362
400
  };
363
- }, [customers]);
401
+
402
+ void load();
403
+ }, [graphQLOptions]);
364
404
 
365
405
  return { stats, isLoading, error };
366
406
  }
@@ -0,0 +1,12 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export function useDebouncedValue<T>(value: T, delay = 300): T {
4
+ const [debouncedValue, setDebouncedValue] = useState(value);
5
+
6
+ useEffect(() => {
7
+ const timer = window.setTimeout(() => setDebouncedValue(value), delay);
8
+ return () => window.clearTimeout(timer);
9
+ }, [value, delay]);
10
+
11
+ return debouncedValue;
12
+ }
@@ -55,9 +55,11 @@ interface ApiFile {
55
55
 
56
56
  interface UseEntityDocumentsResult {
57
57
  documents: EntityDocument[];
58
+ hasMore: boolean;
58
59
  isLoading: boolean;
59
60
  error: Error | null;
60
61
  refetch: () => void;
62
+ loadMore: () => void;
61
63
  }
62
64
 
63
65
  const LIST_ENTITY_FILES_QUERY = `
@@ -71,6 +73,7 @@ const LIST_ENTITY_FILES_QUERY = `
71
73
  createdAt
72
74
  downloadUrl
73
75
  }
76
+ hasMore
74
77
  }
75
78
  }
76
79
  `;
@@ -93,6 +96,8 @@ export function useEntityDocuments(
93
96
  const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
94
97
  useMoveTheWheels();
95
98
  const [documents, setDocuments] = useState<EntityDocument[]>([]);
99
+ const [hasMore, setHasMore] = useState(false);
100
+ const [offset, setOffset] = useState(0);
96
101
  const [isLoading, setIsLoading] = useState(false);
97
102
  const [error, setError] = useState<Error | null>(null);
98
103
 
@@ -108,43 +113,56 @@ export function useEntityDocuments(
108
113
  [apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
109
114
  );
110
115
 
111
- const load = useCallback(async () => {
112
- if (!entityId) {
113
- setDocuments([]);
114
- return;
115
- }
116
- setIsLoading(true);
117
- setError(null);
118
- try {
119
- const data = await executeGraphQL<{ getFiles: { files: ApiFile[] } }>(
120
- graphQLOptions,
121
- LIST_ENTITY_FILES_QUERY,
122
- {
116
+ const pageSize = 20;
117
+
118
+ const load = useCallback(
119
+ async (requestedOffset = 0, append = false) => {
120
+ if (!entityId) {
121
+ setDocuments([]);
122
+ setHasMore(false);
123
+ setOffset(0);
124
+ return;
125
+ }
126
+ setIsLoading(true);
127
+ setError(null);
128
+ try {
129
+ const data = await executeGraphQL<{
130
+ getFiles: { files: ApiFile[]; hasMore?: boolean | null };
131
+ }>(graphQLOptions, LIST_ENTITY_FILES_QUERY, {
123
132
  filter: {
124
133
  tags: [
125
134
  { key: 'mtwEntityType', label: entityType },
126
135
  { key: 'mtwEntityId', label: entityId },
127
136
  ],
128
137
  },
129
- pagination: { limit: 50, offset: 0 },
130
- }
131
- );
132
- setDocuments((data.getFiles?.files ?? []).map(mapFile));
133
- } catch (fetchError) {
134
- setError(fetchError instanceof Error ? fetchError : new Error('Failed to load documents'));
135
- setDocuments([]);
136
- } finally {
137
- setIsLoading(false);
138
- }
139
- }, [entityId, entityType, graphQLOptions]);
138
+ pagination: { limit: pageSize, offset: requestedOffset },
139
+ });
140
+ const nextDocuments = (data.getFiles?.files ?? []).map(mapFile);
141
+ setDocuments((previous) => (append ? [...previous, ...nextDocuments] : nextDocuments));
142
+ setHasMore(data.getFiles?.hasMore ?? nextDocuments.length === pageSize);
143
+ setOffset(requestedOffset);
144
+ } catch (fetchError) {
145
+ setError(fetchError instanceof Error ? fetchError : new Error('Failed to load documents'));
146
+ if (!append) setDocuments([]);
147
+ } finally {
148
+ setIsLoading(false);
149
+ }
150
+ },
151
+ [entityId, entityType, graphQLOptions]
152
+ );
140
153
 
141
154
  useEffect(() => {
142
155
  void load();
143
156
  }, [load]);
144
157
 
145
158
  const refetch = useCallback(() => {
146
- void load();
159
+ void load(0, false);
147
160
  }, [load]);
148
161
 
149
- return { documents, isLoading, error, refetch };
162
+ const loadMore = useCallback(() => {
163
+ if (!hasMore || isLoading) return;
164
+ void load(offset + pageSize, true);
165
+ }, [hasMore, isLoading, load, offset]);
166
+
167
+ return { documents, hasMore, isLoading, error, refetch, loadMore };
150
168
  }
@@ -70,6 +70,7 @@ const MY_PENDING_EPOD_JOBS_QUERY = `
70
70
  phone
71
71
  }
72
72
  }
73
+ myPendingEpodDeliveryJobsCount
73
74
  }
74
75
  `;
75
76
 
@@ -205,9 +206,12 @@ interface ApiPendingJob {
205
206
  }
206
207
 
207
208
  /** Type C self-list: the CALLING driver's own jobs still awaiting ePOD capture — feeds the capture flow's job picker. */
208
- export function useMyPendingEpodDeliveryJobs() {
209
+ export function useMyPendingEpodDeliveryJobs(options: { limit?: number; offset?: number } = {}) {
210
+ const { limit = 20, offset = 0 } = options;
209
211
  const graphQLOptions = useGraphQLOptions();
210
212
  const [jobs, setJobs] = useState<PendingEpodDeliveryJob[]>([]);
213
+ const [totalJobs, setTotalJobs] = useState(0);
214
+ const [hasMore, setHasMore] = useState(false);
211
215
  const [isLoading, setIsLoading] = useState(false);
212
216
  const [error, setError] = useState<Error | null>(null);
213
217
 
@@ -215,10 +219,13 @@ export function useMyPendingEpodDeliveryJobs() {
215
219
  setIsLoading(true);
216
220
  setError(null);
217
221
  try {
218
- const data = await executeGraphQL<{ myPendingEpodDeliveryJobs: ApiPendingJob[] }>(
219
- graphQLOptions,
220
- MY_PENDING_EPOD_JOBS_QUERY,
221
- { pagination: { limit: 50, offset: 0 } }
222
+ const data = await executeGraphQL<{
223
+ myPendingEpodDeliveryJobs: ApiPendingJob[];
224
+ myPendingEpodDeliveryJobsCount: number;
225
+ }>(graphQLOptions, MY_PENDING_EPOD_JOBS_QUERY, { pagination: { limit, offset } });
226
+ setTotalJobs(data.myPendingEpodDeliveryJobsCount ?? 0);
227
+ setHasMore(
228
+ offset + data.myPendingEpodDeliveryJobs.length < data.myPendingEpodDeliveryJobsCount
222
229
  );
223
230
  setJobs(
224
231
  data.myPendingEpodDeliveryJobs.map((job) => ({
@@ -240,13 +247,13 @@ export function useMyPendingEpodDeliveryJobs() {
240
247
  } finally {
241
248
  setIsLoading(false);
242
249
  }
243
- }, [graphQLOptions]);
250
+ }, [graphQLOptions, limit, offset]);
244
251
 
245
252
  useEffect(() => {
246
253
  void load();
247
254
  }, [load]);
248
255
 
249
- return { jobs, isLoading, error, refetch: load };
256
+ return { jobs, totalJobs, hasMore, isLoading, error, refetch: load };
250
257
  }
251
258
 
252
259
  export interface CaptureEpodArgs {
@@ -5,6 +5,7 @@ import { executeGraphQL } from '../utils/graphqlClient';
5
5
 
6
6
  interface UseFleetOptions {
7
7
  filters?: FleetFilter;
8
+ limit?: number;
8
9
  }
9
10
 
10
11
  export interface CreateVehicleInput {
@@ -39,7 +40,7 @@ interface UseFleetResult {
39
40
  updateDriver: (id: string, input: CreateDriverInput) => Promise<Driver>;
40
41
  }
41
42
 
42
- interface ApiVehicle {
43
+ export interface ApiVehicle {
43
44
  id: string;
44
45
  vehicleNumber: string;
45
46
  type: string;
@@ -60,7 +61,7 @@ interface ApiVehicle {
60
61
  updatedAt?: string;
61
62
  }
62
63
 
63
- interface ApiDriver {
64
+ export interface ApiDriver {
64
65
  id: string;
65
66
  userId?: string | null;
66
67
  firstName: string;
@@ -100,6 +101,7 @@ const LIST_VEHICLES_QUERY = `
100
101
  createdAt
101
102
  updatedAt
102
103
  }
104
+ vehiclesCount(filter: $filter)
103
105
  }
104
106
  `;
105
107
 
@@ -122,6 +124,7 @@ const LIST_DRIVERS_QUERY = `
122
124
  vehicleId
123
125
  createdAt
124
126
  }
127
+ driversCount(filter: $filter)
125
128
  }
126
129
  `;
127
130
 
@@ -408,7 +411,7 @@ function toApiDriverStatus(status: DriverStatus): string {
408
411
  }
409
412
  }
410
413
 
411
- function mapVehicle(vehicle: ApiVehicle): Vehicle {
414
+ export function mapVehicle(vehicle: ApiVehicle): Vehicle {
412
415
  return {
413
416
  id: vehicle.id,
414
417
  vehicleNumber: vehicle.vehicleNumber,
@@ -443,7 +446,7 @@ function mapVehicle(vehicle: ApiVehicle): Vehicle {
443
446
  };
444
447
  }
445
448
 
446
- function mapDriver(driver: ApiDriver): Driver {
449
+ export function mapDriver(driver: ApiDriver): Driver {
447
450
  return {
448
451
  id: driver.id,
449
452
  userId: driver.userId ?? driver.id,
@@ -524,13 +527,13 @@ function useGraphqlOptions() {
524
527
  }
525
528
 
526
529
  export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
527
- const { filters: rawFilters } = options;
530
+ const { filters: rawFilters, limit = 20 } = options;
528
531
  // Stabilize the filters reference by its primitive fields so React Compiler's
529
532
  // inferred `filters` dependency stays stable and `load` is not recreated (and
530
533
  // refetched) every render when the caller passes an inline filters object.
531
534
  const filters = useMemo(
532
535
  () => rawFilters,
533
- [rawFilters?.driverId, rawFilters?.status, rawFilters?.vehicleType]
536
+ [rawFilters?.driverId, rawFilters?.search, rawFilters?.status, rawFilters?.vehicleType]
534
537
  );
535
538
  const graphQLOptions = useGraphqlOptions();
536
539
  const [vehicles, setVehicles] = useState<Vehicle[]>([]);
@@ -545,16 +548,18 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
545
548
  const [vehicleData, driverData] = await Promise.all([
546
549
  executeGraphQL<{ vehicles: ApiVehicle[] }>(graphQLOptions, LIST_VEHICLES_QUERY, {
547
550
  filter: {
551
+ search: filters?.search,
548
552
  type: filters?.vehicleType?.toUpperCase(),
549
553
  status: filters?.status ? toApiVehicleStatus(filters.status) : undefined,
550
554
  },
551
- pagination: { limit: 200, offset: 0 },
555
+ pagination: { limit, offset: 0 },
552
556
  }),
553
557
  executeGraphQL<{ drivers: ApiDriver[] }>(graphQLOptions, LIST_DRIVERS_QUERY, {
554
558
  filter: {
559
+ search: filters?.search,
555
560
  vehicleId: filters?.driverId,
556
561
  },
557
- pagination: { limit: 200, offset: 0 },
562
+ pagination: { limit, offset: 0 },
558
563
  }),
559
564
  ]);
560
565
 
@@ -565,7 +570,7 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
565
570
  } finally {
566
571
  setIsLoading(false);
567
572
  }
568
- }, [filters, graphQLOptions]);
573
+ }, [filters, graphQLOptions, limit]);
569
574
 
570
575
  useEffect(() => {
571
576
  void load();
@@ -686,6 +691,12 @@ interface UseListOptions {
686
691
  offset?: number;
687
692
  }
688
693
 
694
+ interface UseVehiclesOptions extends UseListOptions {
695
+ search?: string;
696
+ status?: VehicleStatus;
697
+ vehicleType?: Vehicle['type'];
698
+ }
699
+
689
700
  interface UseVehiclesResult {
690
701
  vehicles: Vehicle[];
691
702
  totalVehicles: number;
@@ -698,10 +709,11 @@ interface UseVehiclesResult {
698
709
  assignDriver: (vehicleId: string, driverId: string) => Promise<Vehicle>;
699
710
  }
700
711
 
701
- export function useVehicles(options: UseListOptions = {}): UseVehiclesResult {
702
- const { limit = 200, offset = 0 } = options;
712
+ export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult {
713
+ const { limit = 20, offset = 0, search, status, vehicleType } = options;
703
714
  const graphQLOptions = useGraphqlOptions();
704
715
  const [vehicles, setVehicles] = useState<Vehicle[]>([]);
716
+ const [totalVehicles, setTotalVehicles] = useState(0);
705
717
  const [hasMore, setHasMore] = useState(false);
706
718
  const [isLoading, setIsLoading] = useState(false);
707
719
  const [error, setError] = useState<Error | null>(null);
@@ -710,21 +722,26 @@ export function useVehicles(options: UseListOptions = {}): UseVehiclesResult {
710
722
  setIsLoading(true);
711
723
  setError(null);
712
724
  try {
713
- const data = await executeGraphQL<{ vehicles: ApiVehicle[] }>(
714
- graphQLOptions,
715
- LIST_VEHICLES_QUERY,
716
- {
717
- pagination: { limit, offset },
718
- }
719
- );
720
- setHasMore(data.vehicles.length === limit);
725
+ const data = await executeGraphQL<{
726
+ vehicles: ApiVehicle[];
727
+ vehiclesCount: number;
728
+ }>(graphQLOptions, LIST_VEHICLES_QUERY, {
729
+ filter: {
730
+ search: search || undefined,
731
+ status: status ? toApiVehicleStatus(status) : undefined,
732
+ type: vehicleType?.toUpperCase(),
733
+ },
734
+ pagination: { limit, offset },
735
+ });
736
+ setHasMore(offset + data.vehicles.length < data.vehiclesCount);
737
+ setTotalVehicles(data.vehiclesCount);
721
738
  setVehicles(data.vehicles.map(mapVehicle));
722
739
  } catch (fetchError) {
723
740
  setError(fetchError instanceof Error ? fetchError : new Error('Failed to load vehicles'));
724
741
  } finally {
725
742
  setIsLoading(false);
726
743
  }
727
- }, [graphQLOptions, limit, offset]);
744
+ }, [graphQLOptions, limit, offset, search, status, vehicleType]);
728
745
 
729
746
  useEffect(() => {
730
747
  void load();
@@ -774,7 +791,7 @@ export function useVehicles(options: UseListOptions = {}): UseVehiclesResult {
774
791
 
775
792
  return {
776
793
  vehicles,
777
- totalVehicles: vehicles.length,
794
+ totalVehicles,
778
795
  hasMore,
779
796
  isLoading,
780
797
  error,
@@ -838,17 +855,17 @@ interface UseDriversResult {
838
855
  }
839
856
 
840
857
  interface UseDriversOptions extends UseListOptions {
841
- /**
842
- * Narrows the roster server-side via `DriverFilterInput.status` so the pager
843
- * still counts whole result sets rather than filtering one fetched page.
844
- */
858
+ /** Narrows the roster server-side via DriverFilterInput.status. */
845
859
  status?: DriverStatus;
860
+ search?: string;
861
+ licenseClass?: string;
846
862
  }
847
863
 
848
864
  export function useDrivers(options: UseDriversOptions = {}): UseDriversResult {
849
- const { limit = 200, offset = 0, status } = options;
865
+ const { limit = 20, offset = 0, status, search, licenseClass } = options;
850
866
  const graphQLOptions = useGraphqlOptions();
851
867
  const [drivers, setDrivers] = useState<Driver[]>([]);
868
+ const [totalDrivers, setTotalDrivers] = useState(0);
852
869
  const [hasMore, setHasMore] = useState(false);
853
870
  const [isLoading, setIsLoading] = useState(false);
854
871
  const [error, setError] = useState<Error | null>(null);
@@ -857,22 +874,26 @@ export function useDrivers(options: UseDriversOptions = {}): UseDriversResult {
857
874
  setIsLoading(true);
858
875
  setError(null);
859
876
  try {
860
- const data = await executeGraphQL<{ drivers: ApiDriver[] }>(
861
- graphQLOptions,
862
- LIST_DRIVERS_QUERY,
863
- {
864
- filter: status ? { status: toApiDriverStatus(status) } : undefined,
865
- pagination: { limit, offset },
866
- }
867
- );
868
- setHasMore(data.drivers.length === limit);
877
+ const data = await executeGraphQL<{
878
+ drivers: ApiDriver[];
879
+ driversCount: number;
880
+ }>(graphQLOptions, LIST_DRIVERS_QUERY, {
881
+ filter: {
882
+ status: status ? toApiDriverStatus(status) : undefined,
883
+ search: search || undefined,
884
+ licenseClass: licenseClass || undefined,
885
+ },
886
+ pagination: { limit, offset },
887
+ });
888
+ setHasMore(offset + data.drivers.length < data.driversCount);
889
+ setTotalDrivers(data.driversCount);
869
890
  setDrivers(data.drivers.map(mapDriver));
870
891
  } catch (fetchError) {
871
892
  setError(fetchError instanceof Error ? fetchError : new Error('Failed to load drivers'));
872
893
  } finally {
873
894
  setIsLoading(false);
874
895
  }
875
- }, [graphQLOptions, limit, offset, status]);
896
+ }, [graphQLOptions, limit, offset, status, search, licenseClass]);
876
897
 
877
898
  useEffect(() => {
878
899
  void load();
@@ -918,7 +939,7 @@ export function useDrivers(options: UseDriversOptions = {}): UseDriversResult {
918
939
 
919
940
  return {
920
941
  drivers,
921
- totalDrivers: drivers.length,
942
+ totalDrivers,
922
943
  hasMore,
923
944
  isLoading,
924
945
  error,
@@ -11,10 +11,7 @@ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
11
11
  import { executeGraphQL } from '../utils/graphqlClient';
12
12
 
13
13
  export type FleetAlertType =
14
- | 'LICENSE_EXPIRED'
15
- | 'LICENSE_EXPIRING'
16
- | 'MAINTENANCE_OVERDUE'
17
- | 'MAINTENANCE_DUE_SOON';
14
+ 'LICENSE_EXPIRED' | 'LICENSE_EXPIRING' | 'MAINTENANCE_OVERDUE' | 'MAINTENANCE_DUE_SOON';
18
15
  export type FleetAlertSeverity = 'CRITICAL' | 'WARNING';
19
16
  export type FleetAlertSubjectType = 'VEHICLE' | 'DRIVER';
20
17