@burdenoff/microfe-movethewheels 2026.805.1 → 2026.827.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/components/CarbonFootprintPanel.js +108 -0
  2. package/dist/components/index.js +1 -0
  3. package/dist/generated/wspace-operations.d.ts +168 -912
  4. package/dist/generated/wspace-types.d.ts +78432 -35831
  5. package/dist/hooks/useAnalytics.js +45 -4
  6. package/dist/hooks/useFleet.js +142 -74
  7. package/dist/hooks/useInventory.js +163 -70
  8. package/dist/hooks/useOrders.js +2 -2
  9. package/dist/hooks/useTracking.js +5 -5
  10. package/dist/index.js +33 -32
  11. package/dist/movethewheels/components/CarbonFootprintPanel.d.ts +7 -0
  12. package/dist/movethewheels/components/index.d.ts +1 -0
  13. package/dist/movethewheels/hooks/index.d.ts +3 -3
  14. package/dist/movethewheels/hooks/useAnalytics.d.ts +22 -0
  15. package/dist/movethewheels/hooks/useFleet.d.ts +32 -1
  16. package/dist/movethewheels/hooks/useInventory.d.ts +67 -12
  17. package/dist/movethewheels/hooks/useOrders.d.ts +25 -2
  18. package/dist/movethewheels/types/index.d.ts +2 -0
  19. package/dist/node_modules/lucide-react/dist/esm/icons/arrow-down-right.js +11 -0
  20. package/dist/node_modules/lucide-react/dist/esm/icons/arrow-left-right.js +22 -0
  21. package/dist/node_modules/lucide-react/dist/esm/icons/arrow-up-right.js +11 -0
  22. package/dist/node_modules/lucide-react/dist/esm/icons/calendar-clock.js +32 -0
  23. package/dist/node_modules/lucide-react/dist/esm/icons/history.js +18 -0
  24. package/dist/node_modules/lucide-react/dist/esm/icons/leaf.js +11 -0
  25. package/dist/node_modules/lucide-react/dist/esm/icons/lock.js +16 -0
  26. package/dist/node_modules/lucide-react/dist/esm/icons/user-round.js +13 -0
  27. package/dist/pages/AnalyticsPage.js +81 -79
  28. package/dist/pages/CreateOrderPage.js +246 -195
  29. package/dist/pages/DeliveryCapturePage.js +91 -78
  30. package/dist/pages/DriversPage.js +202 -108
  31. package/dist/pages/InventoryPage.js +356 -131
  32. package/dist/pages/ProductDetailsPage.js +212 -156
  33. package/dist/pages/VehicleDetailsPage.js +257 -193
  34. package/dist/utils/formatters.js +4 -1
  35. package/package.json +1 -1
  36. package/src/generated/wspace-operations.ts +134 -929
  37. package/src/generated/wspace-types.ts +91730 -40059
  38. package/src/movethewheels/components/CarbonFootprintPanel.tsx +133 -0
  39. package/src/movethewheels/components/UpcomingDemandPanel.tsx +20 -11
  40. package/src/movethewheels/components/index.ts +3 -0
  41. package/src/movethewheels/hooks/index.ts +11 -0
  42. package/src/movethewheels/hooks/useAnalytics.ts +86 -0
  43. package/src/movethewheels/hooks/useDemandForecast.ts +10 -4
  44. package/src/movethewheels/hooks/useFleet.ts +133 -3
  45. package/src/movethewheels/hooks/useInventory.ts +311 -53
  46. package/src/movethewheels/hooks/useOrders.ts +31 -7
  47. package/src/movethewheels/hooks/useTracking.ts +14 -14
  48. package/src/movethewheels/pages/AnalyticsPage.tsx +5 -5
  49. package/src/movethewheels/pages/CreateOrderPage.tsx +67 -10
  50. package/src/movethewheels/pages/DeliveryCapturePage.tsx +8 -0
  51. package/src/movethewheels/pages/DriversPage.tsx +181 -31
  52. package/src/movethewheels/pages/InventoryPage.tsx +299 -19
  53. package/src/movethewheels/pages/ProductDetailsPage.tsx +68 -3
  54. package/src/movethewheels/pages/VehicleDetailsPage.tsx +86 -27
  55. package/src/movethewheels/types/index.ts +2 -0
  56. package/src/operations/wspace/movethewheels/fragments/core.graphql +23 -53
  57. package/src/operations/wspace/movethewheels/fragments/core.withdrawn.graphql +59 -0
  58. package/src/operations/wspace/movethewheels/mutations/entities.graphql +5 -5
  59. package/src/operations/wspace/movethewheels/mutations/marketplace-reports.graphql +18 -78
  60. package/src/operations/wspace/movethewheels/mutations/marketplace-reports.withdrawn.graphql +93 -0
  61. package/src/operations/wspace/movethewheels/mutations/orders.graphql +7 -7
  62. package/src/operations/wspace/movethewheels/queries/dashboard.graphql +3 -3
  63. package/src/operations/wspace/movethewheels/queries/entities.graphql +3 -3
  64. package/src/operations/wspace/movethewheels/queries/logistics.graphql +3 -3
  65. package/src/operations/wspace/movethewheels/queries/marketplace-reports.withdrawn.graphql +58 -0
  66. package/src/operations/wspace/movethewheels/queries/orders.graphql +3 -3
  67. package/src/operations/wspace/movethewheels/queries/marketplace-reports.graphql +0 -40
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Carbon footprint panel (BOFF-4391).
3
+ *
4
+ * Surfaces workspace-level CO2e totals and per-vehicle intensity. Data comes
5
+ * from Query.emissionsSummary via useEmissionsSummary.
6
+ */
7
+
8
+ import { Leaf, Truck } from 'lucide-react';
9
+ import { Card, CardContent, CardHeader, CardTitle } from './ui';
10
+ import { MetricCard, MetricGrid } from './MetricCard';
11
+ import { useEmissionsSummary } from '../hooks/useAnalytics';
12
+
13
+ function formatKg(value: number): string {
14
+ if (value >= 1000) {
15
+ return `${(value / 1000).toFixed(2)} t`;
16
+ }
17
+ return `${value.toFixed(1)} kg`;
18
+ }
19
+
20
+ function formatKm(value: number): string {
21
+ if (value >= 1000) {
22
+ return `${(value / 1000).toFixed(1)}k km`;
23
+ }
24
+ return `${value.toFixed(1)} km`;
25
+ }
26
+
27
+ export function CarbonFootprintPanel() {
28
+ const { summary, isLoading, error } = useEmissionsSummary();
29
+
30
+ if (error) {
31
+ return (
32
+ <Card className="border-status-error-border">
33
+ <CardContent className="p-6 text-center">
34
+ <p className="text-sm text-status-error-text">Couldn&apos;t load emissions data</p>
35
+ </CardContent>
36
+ </Card>
37
+ );
38
+ }
39
+
40
+ if (isLoading) {
41
+ return (
42
+ <Card>
43
+ <CardHeader>
44
+ <CardTitle className="flex items-center gap-2">
45
+ <Leaf size={18} className="text-status-success-text" />
46
+ Carbon footprint
47
+ </CardTitle>
48
+ </CardHeader>
49
+ <CardContent>
50
+ <MetricGrid columns={3}>
51
+ {Array.from({ length: 3 }).map((_, i) => (
52
+ <div key={i} className="h-24 rounded-xl bg-bg-sunken motion-safe:animate-pulse" />
53
+ ))}
54
+ </MetricGrid>
55
+ </CardContent>
56
+ </Card>
57
+ );
58
+ }
59
+
60
+ const hasData = summary.totalDistanceKm > 0;
61
+
62
+ return (
63
+ <Card>
64
+ <CardHeader>
65
+ <CardTitle className="flex items-center gap-2">
66
+ <Leaf size={18} className="text-status-success-text" />
67
+ Carbon footprint
68
+ </CardTitle>
69
+ </CardHeader>
70
+ <CardContent className="space-y-6">
71
+ <MetricGrid columns={3}>
72
+ <MetricCard
73
+ label="Total CO2e"
74
+ value={formatKg(summary.totalCo2eKg)}
75
+ icon={<Leaf size={20} />}
76
+ accent="success"
77
+ />
78
+ <MetricCard
79
+ label="Total distance"
80
+ value={formatKm(summary.totalDistanceKm)}
81
+ icon={<Truck size={20} />}
82
+ accent="blue"
83
+ />
84
+ <MetricCard
85
+ label="Avg intensity"
86
+ value={`${(summary.averageCo2eKgPerKm * 1000).toFixed(1)} g/km`}
87
+ icon={<Leaf size={20} />}
88
+ accent="warning"
89
+ />
90
+ </MetricGrid>
91
+
92
+ {hasData && summary.byVehicle.length > 0 && (
93
+ <div className="space-y-3">
94
+ <h4 className="text-sm font-medium text-text-secondary">By vehicle</h4>
95
+ <div className="space-y-2">
96
+ {summary.byVehicle
97
+ .filter((v) => v.totalDistanceKm > 0 || v.co2eKg > 0)
98
+ .slice(0, 5)
99
+ .map((vehicle) => (
100
+ <div
101
+ key={vehicle.vehicleId ?? 'unassigned'}
102
+ className="flex items-center justify-between rounded-lg bg-bg-sunken px-3 py-2"
103
+ >
104
+ <div className="flex items-center gap-2">
105
+ <Truck size={16} className="text-text-secondary" />
106
+ <span className="text-sm font-medium text-text-primary">
107
+ {vehicle.vehicleNumber ?? 'Unassigned'}
108
+ </span>
109
+ </div>
110
+ <div className="text-right">
111
+ <p className="text-sm font-medium text-text-primary">
112
+ {formatKg(vehicle.co2eKg)}
113
+ </p>
114
+ <p className="text-xs text-text-secondary">
115
+ {formatKm(vehicle.totalDistanceKm)}
116
+ </p>
117
+ </div>
118
+ </div>
119
+ ))}
120
+ </div>
121
+ </div>
122
+ )}
123
+
124
+ {!hasData && (
125
+ <p className="text-sm text-text-secondary">
126
+ No route distance recorded yet. Assign vehicles and distances to routes to see emissions
127
+ estimates.
128
+ </p>
129
+ )}
130
+ </CardContent>
131
+ </Card>
132
+ );
133
+ }
@@ -10,10 +10,7 @@ import { Calendar, TrendingUp, TrendingDown, AlertCircle } from 'lucide-react';
10
10
 
11
11
  import { Card, CardContent, CardHeader, CardTitle, Skeleton } from './ui';
12
12
 
13
- import type {
14
- ForecastPoint,
15
- ForecastVariance,
16
- } from '../hooks/useDemandForecast';
13
+ import type { ForecastPoint, ForecastVariance } from '../hooks/useDemandForecast';
17
14
 
18
15
  interface UpcomingDemandPanelProps {
19
16
  laneForecast: ForecastPoint[];
@@ -42,7 +39,9 @@ const topByRevenue = (rows: ForecastPoint[], weekStart: string): ForecastPoint[]
42
39
  };
43
40
 
44
41
  const totalVolume = (rows: ForecastPoint[], weekStart: string): number =>
45
- rows.filter((row) => row.periodStart === weekStart).reduce((sum, row) => sum + row.predictedVolume, 0);
42
+ rows
43
+ .filter((row) => row.periodStart === weekStart)
44
+ .reduce((sum, row) => sum + row.predictedVolume, 0);
46
45
 
47
46
  const formatConfidence = (confidence: number): string => {
48
47
  if (confidence <= 0) return 'Not enough history';
@@ -177,13 +176,18 @@ function ForecastList({ title, rows }: ForecastListProps) {
177
176
  <p className="text-sm font-medium mb-3">{title}</p>
178
177
  <ul className="space-y-2">
179
178
  {rows.map((row) => (
180
- <li key={`${row.scopeType}-${row.scopeKey}`} className="flex items-center justify-between text-sm">
179
+ <li
180
+ key={`${row.scopeType}-${row.scopeKey}`}
181
+ className="flex items-center justify-between text-sm"
182
+ >
181
183
  <span className="truncate font-medium">{row.scopeKey}</span>
182
184
  <span className="ml-2 shrink-0 text-text-secondary">
183
185
  {row.predictedVolume} jobs ·{' '}
184
- {new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(
185
- row.predictedRevenue
186
- )}
186
+ {new Intl.NumberFormat(undefined, {
187
+ style: 'currency',
188
+ currency: 'USD',
189
+ maximumFractionDigits: 0,
190
+ }).format(row.predictedRevenue)}
187
191
  </span>
188
192
  </li>
189
193
  ))}
@@ -207,9 +211,14 @@ function VarianceList({ rows }: VarianceListProps) {
207
211
  {rows.map((row) => {
208
212
  const isOvershoot = row.volumeVariance > 0;
209
213
  const Icon = isOvershoot ? TrendingUp : TrendingDown;
210
- const tone = isOvershoot ? 'text-emerald-600 dark:text-emerald-400' : 'text-rose-600 dark:text-rose-400';
214
+ const tone = isOvershoot
215
+ ? 'text-emerald-600 dark:text-emerald-400'
216
+ : 'text-rose-600 dark:text-rose-400';
211
217
  return (
212
- <li key={`${row.scopeType}-${row.scopeKey}-${row.periodStart}`} className="flex items-center justify-between text-sm">
218
+ <li
219
+ key={`${row.scopeType}-${row.scopeKey}-${row.periodStart}`}
220
+ className="flex items-center justify-between text-sm"
221
+ >
213
222
  <span className="flex items-center gap-2 truncate">
214
223
  <Icon size={14} className={tone} aria-hidden="true" />
215
224
  <span className="truncate font-medium">{row.scopeKey}</span>
@@ -74,3 +74,6 @@ export { Notice, useNotice } from './Notice';
74
74
 
75
75
  // Documents (reuse of wspace-files-svc)
76
76
  export { DocumentsCard } from './DocumentsCard';
77
+
78
+ // Sustainability / carbon tracking (BOFF-4391)
79
+ export { CarbonFootprintPanel } from './CarbonFootprintPanel';
@@ -12,9 +12,12 @@ export {
12
12
  useFleet,
13
13
  useVehicles,
14
14
  useVehicle,
15
+ useVehicleDrivers,
15
16
  useDrivers,
16
17
  useDriver,
18
+ useDriverStatusCounts,
17
19
  useFleetStats,
20
+ type DriverStatusCounts,
18
21
  } from './useFleet';
19
22
 
20
23
  // Inventory (Products & Warehouses)
@@ -22,9 +25,15 @@ export {
22
25
  useInventory,
23
26
  useProducts,
24
27
  useProduct,
28
+ useProductStockLevels,
25
29
  useWarehouses,
26
30
  useWarehouse,
27
31
  useInventoryStats,
32
+ useInventoryTransactions,
33
+ type InventoryStats,
34
+ type InventoryTransaction,
35
+ type InventoryTransactionType,
36
+ type ProductStockLevel,
28
37
  } from './useInventory';
29
38
 
30
39
  // Customers
@@ -43,6 +52,8 @@ export {
43
52
  usePerformanceAnalytics,
44
53
  useFinanceMetrics,
45
54
  type FinanceMetrics,
55
+ useEmissionsSummary,
56
+ type EmissionsSummary,
46
57
  } from './useAnalytics';
47
58
 
48
59
  // Tracking
@@ -493,6 +493,22 @@ const FINANCE_METRICS_QUERY = `
493
493
  }
494
494
  `;
495
495
 
496
+ const EMISSIONS_SUMMARY_QUERY = `
497
+ query GetMovethewheelsEmissionsSummary {
498
+ emissionsSummary {
499
+ totalCo2eKg
500
+ totalDistanceKm
501
+ averageCo2eKgPerKm
502
+ byVehicle {
503
+ vehicleId
504
+ vehicleNumber
505
+ totalDistanceKm
506
+ co2eKg
507
+ }
508
+ }
509
+ }
510
+ `;
511
+
496
512
  export interface FinanceMetrics {
497
513
  totalRevenue: number;
498
514
  deliveredRevenue: number;
@@ -593,3 +609,73 @@ export function usePerformanceAnalytics() {
593
609
 
594
610
  return { performanceData, isLoading, error };
595
611
  }
612
+
613
+ export interface EmissionsSummary {
614
+ totalCo2eKg: number;
615
+ totalDistanceKm: number;
616
+ averageCo2eKgPerKm: number;
617
+ byVehicle: Array<{
618
+ vehicleId: string | null;
619
+ vehicleNumber: string | null;
620
+ totalDistanceKm: number;
621
+ co2eKg: number;
622
+ }>;
623
+ }
624
+
625
+ /**
626
+ * Real emissions figures straight from Query.emissionsSummary (BOFF-4391).
627
+ * Returns totals, per-km intensity, and a per-vehicle breakdown sourced from
628
+ * cached route CO2e values and actual/estimated distances.
629
+ */
630
+ export function useEmissionsSummary() {
631
+ const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
632
+ useMoveTheWheels();
633
+ const [summary, setSummary] = useState<EmissionsSummary>({
634
+ totalCo2eKg: 0,
635
+ totalDistanceKm: 0,
636
+ averageCo2eKgPerKm: 0,
637
+ byVehicle: [],
638
+ });
639
+ const [isLoading, setIsLoading] = useState(false);
640
+ const [error, setError] = useState<Error | null>(null);
641
+
642
+ const graphQLOptions = useMemo(
643
+ () => ({
644
+ apiGatewayUrl,
645
+ authToken,
646
+ workspaceToken,
647
+ workspaceId,
648
+ tenantId,
649
+ actorId: currentUser?.id,
650
+ }),
651
+ [apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
652
+ );
653
+
654
+ const load = useCallback(async () => {
655
+ setIsLoading(true);
656
+ setError(null);
657
+ try {
658
+ const data = await executeGraphQL<{
659
+ emissionsSummary: EmissionsSummary;
660
+ }>(graphQLOptions, EMISSIONS_SUMMARY_QUERY);
661
+
662
+ setSummary(data.emissionsSummary);
663
+ } catch (fetchError) {
664
+ setError(
665
+ fetchError instanceof Error ? fetchError : new Error('Failed to load emissions summary')
666
+ );
667
+ } finally {
668
+ setIsLoading(false);
669
+ }
670
+ }, [graphQLOptions]);
671
+
672
+ useEffect(() => {
673
+ void load();
674
+ }, [load]);
675
+
676
+ const refetch = useCallback(() => {
677
+ void load();
678
+ }, [load]);
679
+
680
+ return { summary, isLoading, error, refetch };
681
+ }
@@ -134,9 +134,7 @@ export function useDemandForecast(weeks = 4, varianceLimit = 8): UseDemandForeca
134
134
  setCustomerVariance(data.customerVariance ?? []);
135
135
  } catch (fetchError) {
136
136
  setError(
137
- fetchError instanceof Error
138
- ? fetchError
139
- : new Error('Failed to load demand forecast')
137
+ fetchError instanceof Error ? fetchError : new Error('Failed to load demand forecast')
140
138
  );
141
139
  } finally {
142
140
  setIsLoading(false);
@@ -151,5 +149,13 @@ export function useDemandForecast(weeks = 4, varianceLimit = 8): UseDemandForeca
151
149
  void load();
152
150
  }, [load]);
153
151
 
154
- return { laneForecast, customerForecast, laneVariance, customerVariance, isLoading, error, refetch };
152
+ return {
153
+ laneForecast,
154
+ customerForecast,
155
+ laneVariance,
156
+ customerVariance,
157
+ isLoading,
158
+ error,
159
+ refetch,
160
+ };
155
161
  }
@@ -46,6 +46,7 @@ interface ApiVehicle {
46
46
  make?: string | null;
47
47
  model?: string | null;
48
48
  licensePlate?: string | null;
49
+ ownershipType?: string | null;
49
50
  year?: number | null;
50
51
  capacity?: Record<string, unknown> | null;
51
52
  status: string;
@@ -83,6 +84,7 @@ const LIST_VEHICLES_QUERY = `
83
84
  make
84
85
  model
85
86
  licensePlate
87
+ ownershipType
86
88
  year
87
89
  capacity
88
90
  status
@@ -126,6 +128,7 @@ const GET_VEHICLE_QUERY = `
126
128
  make
127
129
  model
128
130
  licensePlate
131
+ ownershipType
129
132
  year
130
133
  capacity
131
134
  status
@@ -169,6 +172,7 @@ const UPDATE_VEHICLE_STATUS_MUTATION = `
169
172
  make
170
173
  model
171
174
  licensePlate
175
+ ownershipType
172
176
  year
173
177
  capacity
174
178
  status
@@ -234,6 +238,7 @@ const CREATE_VEHICLE_MUTATION = `
234
238
  make
235
239
  model
236
240
  licensePlate
241
+ ownershipType
237
242
  year
238
243
  capacity
239
244
  status
@@ -277,6 +282,7 @@ const UPDATE_VEHICLE_MUTATION = `
277
282
  make
278
283
  model
279
284
  licensePlate
285
+ ownershipType
280
286
  year
281
287
  capacity
282
288
  status
@@ -311,6 +317,21 @@ const UPDATE_DRIVER_MUTATION = `
311
317
  }
312
318
  `;
313
319
 
320
+ /**
321
+ * `fleetStats` only breaks drivers down into available / on-delivery, so an
322
+ * "on break" or "off duty" tile derived from it is always wrong (on-break
323
+ * drivers get lumped into the off-duty remainder). `driversCount` takes the
324
+ * same DriverFilterInput as the list, so each roster tile can be counted
325
+ * exactly, workspace-wide, instead of inferred from a subtraction.
326
+ */
327
+ const DRIVER_STATUS_COUNTS_QUERY = `
328
+ query GetDriverStatusCounts {
329
+ onBreak: driversCount(filter: { status: ON_BREAK })
330
+ offDuty: driversCount(filter: { status: OFF_DUTY })
331
+ inactive: driversCount(filter: { status: INACTIVE })
332
+ }
333
+ `;
334
+
314
335
  const FLEET_STATS_QUERY = `
315
336
  query GetFleetStats {
316
337
  fleetStats {
@@ -386,6 +407,7 @@ function mapVehicle(vehicle: ApiVehicle): Vehicle {
386
407
  make: vehicle.make ?? '',
387
408
  model: vehicle.model ?? '',
388
409
  licensePlate: vehicle.licensePlate ?? '',
410
+ ownershipType: vehicle.ownershipType ?? undefined,
389
411
  year: vehicle.year ?? new Date().getFullYear(),
390
412
  capacity: {
391
413
  weight: 0,
@@ -805,8 +827,16 @@ interface UseDriversResult {
805
827
  assignVehicle: (driverId: string, vehicleId: string) => Promise<Driver>;
806
828
  }
807
829
 
808
- export function useDrivers(options: UseListOptions = {}): UseDriversResult {
809
- const { limit = 200, offset = 0 } = options;
830
+ interface UseDriversOptions extends UseListOptions {
831
+ /**
832
+ * Narrows the roster server-side via `DriverFilterInput.status` so the pager
833
+ * still counts whole result sets rather than filtering one fetched page.
834
+ */
835
+ status?: DriverStatus;
836
+ }
837
+
838
+ export function useDrivers(options: UseDriversOptions = {}): UseDriversResult {
839
+ const { limit = 200, offset = 0, status } = options;
810
840
  const graphQLOptions = useGraphqlOptions();
811
841
  const [drivers, setDrivers] = useState<Driver[]>([]);
812
842
  const [hasMore, setHasMore] = useState(false);
@@ -821,6 +851,7 @@ export function useDrivers(options: UseListOptions = {}): UseDriversResult {
821
851
  graphQLOptions,
822
852
  LIST_DRIVERS_QUERY,
823
853
  {
854
+ filter: status ? { status: toApiDriverStatus(status) } : undefined,
824
855
  pagination: { limit, offset },
825
856
  }
826
857
  );
@@ -831,7 +862,7 @@ export function useDrivers(options: UseListOptions = {}): UseDriversResult {
831
862
  } finally {
832
863
  setIsLoading(false);
833
864
  }
834
- }, [graphQLOptions, limit, offset]);
865
+ }, [graphQLOptions, limit, offset, status]);
835
866
 
836
867
  useEffect(() => {
837
868
  void load();
@@ -928,6 +959,105 @@ export function useDriver(driverId: string | undefined) {
928
959
  return { driver, isLoading, error, refetch };
929
960
  }
930
961
 
962
+ export interface DriverStatusCounts {
963
+ onBreak: number;
964
+ offDuty: number;
965
+ inactive: number;
966
+ }
967
+
968
+ /** Exact per-status driver counts for the roster tiles (see DRIVER_STATUS_COUNTS_QUERY). */
969
+ export function useDriverStatusCounts() {
970
+ const graphQLOptions = useGraphqlOptions();
971
+ const [counts, setCounts] = useState<DriverStatusCounts>({
972
+ onBreak: 0,
973
+ offDuty: 0,
974
+ inactive: 0,
975
+ });
976
+ const [isLoading, setIsLoading] = useState(false);
977
+ const [error, setError] = useState<Error | null>(null);
978
+
979
+ const load = useCallback(async () => {
980
+ setIsLoading(true);
981
+ setError(null);
982
+ try {
983
+ const data = await executeGraphQL<DriverStatusCounts>(
984
+ graphQLOptions,
985
+ DRIVER_STATUS_COUNTS_QUERY
986
+ );
987
+ setCounts({
988
+ onBreak: data.onBreak,
989
+ offDuty: data.offDuty,
990
+ inactive: data.inactive,
991
+ });
992
+ } catch (fetchError) {
993
+ setError(
994
+ fetchError instanceof Error ? fetchError : new Error('Failed to load driver status counts')
995
+ );
996
+ } finally {
997
+ setIsLoading(false);
998
+ }
999
+ }, [graphQLOptions]);
1000
+
1001
+ useEffect(() => {
1002
+ void load();
1003
+ }, [load]);
1004
+
1005
+ const refetch = useCallback(() => {
1006
+ void load();
1007
+ }, [load]);
1008
+
1009
+ return { counts, isLoading, error, refetch };
1010
+ }
1011
+
1012
+ /**
1013
+ * The driver(s) currently assigned to a vehicle, resolved server-side through
1014
+ * `DriverFilterInput.vehicleId` (the same link `assignDriverVehicle` writes).
1015
+ * A vehicle normally has one driver, but the filter returns a list, so the
1016
+ * caller gets every assignment rather than a silently-picked first row.
1017
+ */
1018
+ export function useVehicleDrivers(vehicleId: string | undefined) {
1019
+ const graphQLOptions = useGraphqlOptions();
1020
+ const [drivers, setDrivers] = useState<Driver[]>([]);
1021
+ const [isLoading, setIsLoading] = useState(false);
1022
+ const [error, setError] = useState<Error | null>(null);
1023
+
1024
+ const load = useCallback(async () => {
1025
+ if (!vehicleId) {
1026
+ setDrivers([]);
1027
+ return;
1028
+ }
1029
+ setIsLoading(true);
1030
+ setError(null);
1031
+ try {
1032
+ const data = await executeGraphQL<{ drivers: ApiDriver[] }>(
1033
+ graphQLOptions,
1034
+ LIST_DRIVERS_QUERY,
1035
+ {
1036
+ filter: { vehicleId },
1037
+ pagination: { limit: 25, offset: 0 },
1038
+ }
1039
+ );
1040
+ setDrivers(data.drivers.map(mapDriver));
1041
+ } catch (fetchError) {
1042
+ setError(
1043
+ fetchError instanceof Error ? fetchError : new Error('Failed to load assigned driver')
1044
+ );
1045
+ } finally {
1046
+ setIsLoading(false);
1047
+ }
1048
+ }, [graphQLOptions, vehicleId]);
1049
+
1050
+ useEffect(() => {
1051
+ void load();
1052
+ }, [load]);
1053
+
1054
+ const refetch = useCallback(() => {
1055
+ void load();
1056
+ }, [load]);
1057
+
1058
+ return { drivers, isLoading, error, refetch };
1059
+ }
1060
+
931
1061
  export function useFleetStats() {
932
1062
  const graphQLOptions = useGraphqlOptions();
933
1063
  const [stats, setStats] = useState({