@burdenoff/microfe-movethewheels 2026.830.5 → 2026.920.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 (38) hide show
  1. package/dist/MoveTheWheelsRoutes.js +7 -3
  2. package/dist/components/AssignmentSuggestions.js +135 -0
  3. package/dist/components/FleetAlertsPanel.js +114 -0
  4. package/dist/components/ShippingQuoteResults.js +142 -0
  5. package/dist/components/VehicleServiceCard.js +176 -0
  6. package/dist/hooks/useFleet.js +4 -3
  7. package/dist/hooks/useFleetIntelligence.js +91 -0
  8. package/dist/hooks/useShippingCarriers.js +29 -0
  9. package/dist/movethewheels/components/AssignmentSuggestions.d.ts +17 -0
  10. package/dist/movethewheels/components/FleetAlertsPanel.d.ts +14 -0
  11. package/dist/movethewheels/components/ShippingQuoteResults.d.ts +8 -0
  12. package/dist/movethewheels/components/VehicleServiceCard.d.ts +10 -0
  13. package/dist/movethewheels/hooks/useFleetIntelligence.d.ts +64 -0
  14. package/dist/movethewheels/hooks/useShippingCarriers.d.ts +43 -0
  15. package/dist/movethewheels/pages/ShippingRatesPage.d.ts +9 -0
  16. package/dist/movethewheels/types/index.d.ts +2 -0
  17. package/dist/node_modules/lucide-react/dist/esm/icons/scale.js +26 -0
  18. package/dist/node_modules/lucide-react/dist/esm/icons/sparkles.js +26 -0
  19. package/dist/pages/FleetPage.js +156 -146
  20. package/dist/pages/RouteDetailsPage.js +202 -184
  21. package/dist/pages/ShippingRatesPage.js +177 -0
  22. package/dist/pages/VehicleDetailsPage.js +149 -131
  23. package/dist/utils/graphqlClient.js +14 -5
  24. package/package.json +1 -1
  25. package/src/movethewheels/MoveTheWheelsRoutes.tsx +2 -0
  26. package/src/movethewheels/components/AssignmentSuggestions.tsx +234 -0
  27. package/src/movethewheels/components/FleetAlertsPanel.tsx +148 -0
  28. package/src/movethewheels/components/ShippingQuoteResults.tsx +199 -0
  29. package/src/movethewheels/components/VehicleServiceCard.tsx +220 -0
  30. package/src/movethewheels/hooks/useFleet.ts +12 -2
  31. package/src/movethewheels/hooks/useFleetIntelligence.ts +226 -0
  32. package/src/movethewheels/hooks/useShippingCarriers.ts +110 -0
  33. package/src/movethewheels/pages/FleetPage.tsx +21 -0
  34. package/src/movethewheels/pages/RouteDetailsPage.tsx +33 -1
  35. package/src/movethewheels/pages/ShippingRatesPage.tsx +216 -0
  36. package/src/movethewheels/pages/VehicleDetailsPage.tsx +27 -0
  37. package/src/movethewheels/types/index.ts +2 -0
  38. package/src/movethewheels/utils/graphqlClient.ts +20 -1
@@ -0,0 +1,220 @@
1
+ /**
2
+ * VehicleServiceCard
3
+ *
4
+ * Shows a vehicle's service schedule (last service, next due date/mileage) and
5
+ * lets an operator log a completed service. Logging a service advances the
6
+ * odometer, sets the next due markers from the intervals given, and returns a
7
+ * vehicle that was in the workshop to service — so the "due" alert clears
8
+ * instead of lingering.
9
+ */
10
+
11
+ import { useState } from 'react';
12
+ import { Wrench } from 'lucide-react';
13
+
14
+ import { FormError } from './FormError';
15
+ import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label } from './ui';
16
+
17
+ import type { RecordVehicleServiceInput } from '../hooks/useFleetIntelligence';
18
+ import type { Vehicle } from '../types';
19
+
20
+ interface VehicleServiceCardProps {
21
+ vehicle: Vehicle;
22
+ onRecord: (input: RecordVehicleServiceInput) => Promise<void>;
23
+ t: (key: string, fallback: string) => string;
24
+ }
25
+
26
+ const DEFAULT_INTERVAL_DAYS = 90;
27
+ const DEFAULT_INTERVAL_KM = 10000;
28
+
29
+ const formatDay = (date: Date | undefined): string | null =>
30
+ date
31
+ ? date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
32
+ : null;
33
+
34
+ const toDateInput = (date: Date): string => date.toISOString().slice(0, 10);
35
+
36
+ /** Empty string -> undefined; otherwise a finite non-negative number, or NaN when invalid. */
37
+ const parseOptionalNumber = (value: string): number | undefined => {
38
+ if (value.trim() === '') return undefined;
39
+ const parsed = Number(value);
40
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : Number.NaN;
41
+ };
42
+
43
+ export function VehicleServiceCard({ vehicle, onRecord, t }: VehicleServiceCardProps) {
44
+ const [open, setOpen] = useState(false);
45
+ const [saving, setSaving] = useState(false);
46
+ const [error, setError] = useState<string | null>(null);
47
+ const [form, setForm] = useState({
48
+ serviceDate: toDateInput(new Date()),
49
+ mileage: vehicle.mileage !== undefined ? String(vehicle.mileage) : '',
50
+ intervalDays: String(DEFAULT_INTERVAL_DAYS),
51
+ intervalKm: String(DEFAULT_INTERVAL_KM),
52
+ });
53
+
54
+ const lastService = formatDay(vehicle.lastMaintenance);
55
+ const nextDate = formatDay(vehicle.nextMaintenance);
56
+ const nextMileage = vehicle.nextMaintenanceMileage;
57
+ const hasSchedule = nextDate !== null || nextMileage !== undefined;
58
+
59
+ const submit = async (event: React.FormEvent) => {
60
+ event.preventDefault();
61
+ const mileage = parseOptionalNumber(form.mileage);
62
+ const intervalDays = parseOptionalNumber(form.intervalDays);
63
+ const intervalKm = parseOptionalNumber(form.intervalKm);
64
+ if ([mileage, intervalDays, intervalKm].some((n) => n !== undefined && Number.isNaN(n))) {
65
+ setError(t('movethewheels.vehicles.service.invalid', 'Enter valid, non-negative numbers.'));
66
+ return;
67
+ }
68
+ if (intervalKm !== undefined && mileage === undefined && vehicle.mileage === undefined) {
69
+ setError(
70
+ t(
71
+ 'movethewheels.vehicles.service.needsMileage',
72
+ 'Enter the odometer reading to set a service-by-distance interval.'
73
+ )
74
+ );
75
+ return;
76
+ }
77
+
78
+ setSaving(true);
79
+ setError(null);
80
+ try {
81
+ await onRecord({
82
+ // Noon UTC keeps the calendar day stable in any timezone.
83
+ serviceDate: `${form.serviceDate}T12:00:00.000Z`,
84
+ ...(mileage !== undefined ? { mileage } : {}),
85
+ ...(intervalDays !== undefined ? { intervalDays: Math.round(intervalDays) } : {}),
86
+ ...(intervalKm !== undefined ? { intervalKm } : {}),
87
+ });
88
+ setOpen(false);
89
+ } catch (e) {
90
+ setError(e instanceof Error ? e.message : 'Could not record the service.');
91
+ } finally {
92
+ setSaving(false);
93
+ }
94
+ };
95
+
96
+ return (
97
+ <Card>
98
+ <CardHeader>
99
+ <CardTitle className="flex items-center gap-2">
100
+ <Wrench size={18} aria-hidden="true" />
101
+ {t('movethewheels.vehicles.service.title', 'Service schedule')}
102
+ </CardTitle>
103
+ </CardHeader>
104
+ <CardContent className="space-y-4">
105
+ <dl className="grid grid-cols-2 gap-4 text-sm">
106
+ <div>
107
+ <dt className="text-text-secondary">
108
+ {t('movethewheels.vehicles.service.last', 'Last service')}
109
+ </dt>
110
+ <dd className="font-medium">
111
+ {lastService ?? t('movethewheels.vehicles.service.none', 'Not recorded')}
112
+ </dd>
113
+ </div>
114
+ <div>
115
+ <dt className="text-text-secondary">
116
+ {t('movethewheels.vehicles.service.next', 'Next service due')}
117
+ </dt>
118
+ <dd className="font-medium">
119
+ {hasSchedule
120
+ ? [
121
+ nextDate,
122
+ nextMileage !== undefined ? `${nextMileage.toLocaleString()} km` : null,
123
+ ]
124
+ .filter(Boolean)
125
+ .join(' / ')
126
+ : t('movethewheels.vehicles.service.noSchedule', 'No schedule set')}
127
+ </dd>
128
+ </div>
129
+ </dl>
130
+
131
+ {!open ? (
132
+ <Button variant="outline" size="sm" onClick={() => setOpen(true)}>
133
+ {t('movethewheels.vehicles.service.record', 'Record service')}
134
+ </Button>
135
+ ) : (
136
+ <form onSubmit={submit} className="space-y-3">
137
+ <FormError message={error} />
138
+ <div className="grid grid-cols-2 gap-3">
139
+ <div className="space-y-1">
140
+ <Label htmlFor="service-date">
141
+ {t('movethewheels.vehicles.service.date', 'Service date')}
142
+ </Label>
143
+ <Input
144
+ id="service-date"
145
+ type="date"
146
+ required
147
+ value={form.serviceDate}
148
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
149
+ setForm((f) => ({ ...f, serviceDate: e.target.value }))
150
+ }
151
+ />
152
+ </div>
153
+ <div className="space-y-1">
154
+ <Label htmlFor="service-mileage">
155
+ {t('movethewheels.vehicles.service.mileage', 'Odometer (km)')}
156
+ </Label>
157
+ <Input
158
+ id="service-mileage"
159
+ type="number"
160
+ min={0}
161
+ value={form.mileage}
162
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
163
+ setForm((f) => ({ ...f, mileage: e.target.value }))
164
+ }
165
+ />
166
+ </div>
167
+ <div className="space-y-1">
168
+ <Label htmlFor="service-interval-days">
169
+ {t('movethewheels.vehicles.service.intervalDays', 'Next service in (days)')}
170
+ </Label>
171
+ <Input
172
+ id="service-interval-days"
173
+ type="number"
174
+ min={0}
175
+ value={form.intervalDays}
176
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
177
+ setForm((f) => ({ ...f, intervalDays: e.target.value }))
178
+ }
179
+ />
180
+ </div>
181
+ <div className="space-y-1">
182
+ <Label htmlFor="service-interval-km">
183
+ {t('movethewheels.vehicles.service.intervalKm', 'Next service in (km)')}
184
+ </Label>
185
+ <Input
186
+ id="service-interval-km"
187
+ type="number"
188
+ min={0}
189
+ value={form.intervalKm}
190
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
191
+ setForm((f) => ({ ...f, intervalKm: e.target.value }))
192
+ }
193
+ />
194
+ </div>
195
+ </div>
196
+ <div className="flex gap-2">
197
+ <Button type="submit" size="sm" disabled={saving}>
198
+ {saving
199
+ ? t('movethewheels.vehicles.service.saving', 'Saving…')
200
+ : t('movethewheels.vehicles.service.save', 'Save service')}
201
+ </Button>
202
+ <Button
203
+ type="button"
204
+ variant="ghost"
205
+ size="sm"
206
+ disabled={saving}
207
+ onClick={() => {
208
+ setOpen(false);
209
+ setError(null);
210
+ }}
211
+ >
212
+ {t('movethewheels.vehicles.service.cancel', 'Cancel')}
213
+ </Button>
214
+ </div>
215
+ </form>
216
+ )}
217
+ </CardContent>
218
+ </Card>
219
+ );
220
+ }
@@ -53,6 +53,9 @@ interface ApiVehicle {
53
53
  currentLocation?: Record<string, unknown> | null;
54
54
  fuelLevel?: number | null;
55
55
  mileage?: number | null;
56
+ lastServiceDate?: string | null;
57
+ nextServiceDueDate?: string | null;
58
+ nextServiceDueMileage?: number | null;
56
59
  createdAt: string;
57
60
  updatedAt?: string;
58
61
  }
@@ -91,6 +94,9 @@ const LIST_VEHICLES_QUERY = `
91
94
  currentLocation
92
95
  fuelLevel
93
96
  mileage
97
+ lastServiceDate
98
+ nextServiceDueDate
99
+ nextServiceDueMileage
94
100
  createdAt
95
101
  updatedAt
96
102
  }
@@ -135,6 +141,9 @@ const GET_VEHICLE_QUERY = `
135
141
  currentLocation
136
142
  fuelLevel
137
143
  mileage
144
+ lastServiceDate
145
+ nextServiceDueDate
146
+ nextServiceDueMileage
138
147
  createdAt
139
148
  updatedAt
140
149
  }
@@ -424,8 +433,9 @@ function mapVehicle(vehicle: ApiVehicle): Vehicle {
424
433
  timestamp: new Date(),
425
434
  }
426
435
  : undefined,
427
- lastMaintenance: undefined,
428
- nextMaintenance: undefined,
436
+ lastMaintenance: vehicle.lastServiceDate ? new Date(vehicle.lastServiceDate) : undefined,
437
+ nextMaintenance: vehicle.nextServiceDueDate ? new Date(vehicle.nextServiceDueDate) : undefined,
438
+ nextMaintenanceMileage: vehicle.nextServiceDueMileage ?? undefined,
429
439
  fuelLevel: vehicle.fuelLevel ?? undefined,
430
440
  mileage: vehicle.mileage ?? undefined,
431
441
  documents: [],
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Fleet intelligence hooks — compliance/maintenance alerts, dispatch
3
+ * suggestions for a route, and logging a completed vehicle service.
4
+ *
5
+ * All three sit on top of wspace-movethewheels-svc's fleet module; the ranking
6
+ * and alert rules live server-side so every surface (web, CLI, MCP) agrees.
7
+ */
8
+
9
+ import { useCallback, useEffect, useMemo, useState } from 'react';
10
+ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
11
+ import { executeGraphQL } from '../utils/graphqlClient';
12
+
13
+ export type FleetAlertType =
14
+ | 'LICENSE_EXPIRED'
15
+ | 'LICENSE_EXPIRING'
16
+ | 'MAINTENANCE_OVERDUE'
17
+ | 'MAINTENANCE_DUE_SOON';
18
+ export type FleetAlertSeverity = 'CRITICAL' | 'WARNING';
19
+ export type FleetAlertSubjectType = 'VEHICLE' | 'DRIVER';
20
+
21
+ export interface FleetAlert {
22
+ id: string;
23
+ type: FleetAlertType;
24
+ severity: FleetAlertSeverity;
25
+ subjectType: FleetAlertSubjectType;
26
+ subjectId: string;
27
+ subjectName: string;
28
+ message: string;
29
+ dueDate: string | null;
30
+ daysUntilDue: number | null;
31
+ kmUntilDue: number | null;
32
+ }
33
+
34
+ export interface VehicleSuggestion {
35
+ score: number;
36
+ reasons: string[];
37
+ vehicle: { id: string; vehicleNumber: string; make: string | null; model: string | null };
38
+ }
39
+
40
+ export interface DriverSuggestion {
41
+ score: number;
42
+ reasons: string[];
43
+ driver: { id: string; firstName: string; lastName: string; rating: number | null };
44
+ }
45
+
46
+ export interface AssignmentSuggestions {
47
+ routeId: string;
48
+ vehicles: VehicleSuggestion[];
49
+ drivers: DriverSuggestion[];
50
+ }
51
+
52
+ export interface RecordVehicleServiceInput {
53
+ /** ISO date-time. Defaults to now on the server. */
54
+ serviceDate?: string;
55
+ mileage?: number;
56
+ intervalDays?: number;
57
+ intervalKm?: number;
58
+ }
59
+
60
+ const FLEET_ALERTS_QUERY = `
61
+ query FleetAlerts {
62
+ fleetAlerts {
63
+ id
64
+ type
65
+ severity
66
+ subjectType
67
+ subjectId
68
+ subjectName
69
+ message
70
+ dueDate
71
+ daysUntilDue
72
+ kmUntilDue
73
+ }
74
+ }
75
+ `;
76
+
77
+ const ASSIGNMENT_SUGGESTIONS_QUERY = `
78
+ query AssignmentSuggestions($routeId: ID!, $limit: Int) {
79
+ assignmentSuggestions(routeId: $routeId, limit: $limit) {
80
+ routeId
81
+ vehicles {
82
+ score
83
+ reasons
84
+ vehicle {
85
+ id
86
+ vehicleNumber
87
+ make
88
+ model
89
+ }
90
+ }
91
+ drivers {
92
+ score
93
+ reasons
94
+ driver {
95
+ id
96
+ firstName
97
+ lastName
98
+ rating
99
+ }
100
+ }
101
+ }
102
+ }
103
+ `;
104
+
105
+ const RECORD_VEHICLE_SERVICE_MUTATION = `
106
+ mutation RecordVehicleService($id: ID!, $input: RecordVehicleServiceInput!) {
107
+ recordVehicleService(id: $id, input: $input) {
108
+ id
109
+ status
110
+ mileage
111
+ lastServiceDate
112
+ nextServiceDueDate
113
+ nextServiceDueMileage
114
+ }
115
+ }
116
+ `;
117
+
118
+ function useGraphqlOptions() {
119
+ const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
120
+ useMoveTheWheels();
121
+ return useMemo(
122
+ () => ({
123
+ apiGatewayUrl,
124
+ authToken,
125
+ workspaceToken,
126
+ workspaceId,
127
+ tenantId,
128
+ actorId: currentUser?.id,
129
+ }),
130
+ [apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
131
+ );
132
+ }
133
+
134
+ interface AsyncResource<T> {
135
+ data: T;
136
+ isLoading: boolean;
137
+ error: Error | null;
138
+ refetch: () => void;
139
+ }
140
+
141
+ export function useFleetAlerts(): AsyncResource<FleetAlert[]> {
142
+ const graphQLOptions = useGraphqlOptions();
143
+ const [data, setData] = useState<FleetAlert[]>([]);
144
+ const [isLoading, setIsLoading] = useState(true);
145
+ const [error, setError] = useState<Error | null>(null);
146
+ const [nonce, setNonce] = useState(0);
147
+
148
+ useEffect(() => {
149
+ let active = true;
150
+ setIsLoading(true);
151
+ setError(null);
152
+ executeGraphQL<{ fleetAlerts: FleetAlert[] }>(graphQLOptions, FLEET_ALERTS_QUERY)
153
+ .then((result) => {
154
+ if (active) setData(result.fleetAlerts);
155
+ })
156
+ .catch((e: unknown) => {
157
+ if (active) setError(e instanceof Error ? e : new Error('Failed to load fleet alerts'));
158
+ })
159
+ .finally(() => {
160
+ if (active) setIsLoading(false);
161
+ });
162
+ return () => {
163
+ active = false;
164
+ };
165
+ }, [graphQLOptions, nonce]);
166
+
167
+ const refetch = useCallback(() => setNonce((n) => n + 1), []);
168
+ return { data, isLoading, error, refetch };
169
+ }
170
+
171
+ const EMPTY_SUGGESTIONS: AssignmentSuggestions = { routeId: '', vehicles: [], drivers: [] };
172
+
173
+ export function useAssignmentSuggestions(
174
+ routeId: string | undefined,
175
+ limit = 3
176
+ ): AsyncResource<AssignmentSuggestions> {
177
+ const graphQLOptions = useGraphqlOptions();
178
+ const [data, setData] = useState<AssignmentSuggestions>(EMPTY_SUGGESTIONS);
179
+ const [isLoading, setIsLoading] = useState(Boolean(routeId));
180
+ const [error, setError] = useState<Error | null>(null);
181
+ const [nonce, setNonce] = useState(0);
182
+
183
+ useEffect(() => {
184
+ if (!routeId) {
185
+ setData(EMPTY_SUGGESTIONS);
186
+ setIsLoading(false);
187
+ return undefined;
188
+ }
189
+ let active = true;
190
+ setIsLoading(true);
191
+ setError(null);
192
+ executeGraphQL<{ assignmentSuggestions: AssignmentSuggestions }>(
193
+ graphQLOptions,
194
+ ASSIGNMENT_SUGGESTIONS_QUERY,
195
+ { routeId, limit }
196
+ )
197
+ .then((result) => {
198
+ if (active) setData(result.assignmentSuggestions);
199
+ })
200
+ .catch((e: unknown) => {
201
+ if (active) setError(e instanceof Error ? e : new Error('Failed to load suggestions'));
202
+ })
203
+ .finally(() => {
204
+ if (active) setIsLoading(false);
205
+ });
206
+ return () => {
207
+ active = false;
208
+ };
209
+ }, [graphQLOptions, routeId, limit, nonce]);
210
+
211
+ const refetch = useCallback(() => setNonce((n) => n + 1), []);
212
+ return { data, isLoading, error, refetch };
213
+ }
214
+
215
+ export function useRecordVehicleService() {
216
+ const graphQLOptions = useGraphqlOptions();
217
+ return useCallback(
218
+ async (vehicleId: string, input: RecordVehicleServiceInput): Promise<void> => {
219
+ await executeGraphQL(graphQLOptions, RECORD_VEHICLE_SERVICE_MUTATION, {
220
+ id: vehicleId,
221
+ input,
222
+ });
223
+ },
224
+ [graphQLOptions]
225
+ );
226
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Shipping rate comparison — every carrier's price for one pincode pair, on top
3
+ * of wspace-movethewheels-svc's carrier module. The carrier accounts belong to
4
+ * the platform, so there is nothing for a workspace to configure.
5
+ */
6
+
7
+ import { useCallback, useMemo } from 'react';
8
+ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
9
+ import { executeGraphQL } from '../utils/graphqlClient';
10
+
11
+ export type CarrierProvider = 'SHIPROCKET';
12
+ export type CarrierServiceMode = 'SURFACE' | 'AIR';
13
+
14
+ export interface ShippingQuoteInput {
15
+ pickupPostcode: string;
16
+ deliveryPostcode: string;
17
+ weightKg: number;
18
+ lengthCm?: number;
19
+ widthCm?: number;
20
+ heightCm?: number;
21
+ cod?: boolean;
22
+ }
23
+
24
+ export interface ShippingQuote {
25
+ provider: CarrierProvider;
26
+ courierId: string;
27
+ courierName: string;
28
+ price: number;
29
+ currency: string;
30
+ estimatedDeliveryDays: number | null;
31
+ estimatedDeliveryDate: string | null;
32
+ rating: number | null;
33
+ mode: CarrierServiceMode | null;
34
+ isRecommended: boolean;
35
+ isCheapest: boolean;
36
+ isFastest: boolean;
37
+ }
38
+
39
+ export interface CarrierOutcome {
40
+ provider: CarrierProvider;
41
+ ok: boolean;
42
+ quoteCount: number;
43
+ errorMessage: string | null;
44
+ }
45
+
46
+ export interface ShippingQuoteResult {
47
+ chargeableWeightKg: number;
48
+ quotes: ShippingQuote[];
49
+ providers: CarrierOutcome[];
50
+ }
51
+
52
+ const SHIPPING_QUOTES_QUERY = `
53
+ query ShippingQuotes($input: ShippingQuoteInput!) {
54
+ shippingQuotes(input: $input) {
55
+ chargeableWeightKg
56
+ quotes {
57
+ provider
58
+ courierId
59
+ courierName
60
+ price
61
+ currency
62
+ estimatedDeliveryDays
63
+ estimatedDeliveryDate
64
+ rating
65
+ mode
66
+ isRecommended
67
+ isCheapest
68
+ isFastest
69
+ }
70
+ providers {
71
+ provider
72
+ ok
73
+ quoteCount
74
+ errorMessage
75
+ }
76
+ }
77
+ }
78
+ `;
79
+
80
+ function useGraphqlOptions() {
81
+ const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
82
+ useMoveTheWheels();
83
+ return useMemo(
84
+ () => ({
85
+ apiGatewayUrl,
86
+ authToken,
87
+ workspaceToken,
88
+ workspaceId,
89
+ tenantId,
90
+ actorId: currentUser?.id,
91
+ }),
92
+ [apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
93
+ );
94
+ }
95
+
96
+ /** Imperative: a comparison is something the operator asks for, not page-load data. */
97
+ export function useShippingQuotes() {
98
+ const graphQLOptions = useGraphqlOptions();
99
+ return useCallback(
100
+ async (input: ShippingQuoteInput): Promise<ShippingQuoteResult> => {
101
+ const result = await executeGraphQL<{ shippingQuotes: ShippingQuoteResult }>(
102
+ graphQLOptions,
103
+ SHIPPING_QUOTES_QUERY,
104
+ { input }
105
+ );
106
+ return result.shippingQuotes;
107
+ },
108
+ [graphQLOptions]
109
+ );
110
+ }
@@ -20,6 +20,8 @@ import {
20
20
  } from '../components/ui';
21
21
  import { CTAOverflowMenu, EmphasisPanel, PagePurpose, NextSteps } from '@burdenoff/fe-libs/ui';
22
22
  import { useFleetStats, useVehicles, useDrivers } from '../hooks/useFleet';
23
+ import { useFleetAlerts, type FleetAlert } from '../hooks/useFleetIntelligence';
24
+ import { FleetAlertsPanel } from '../components/FleetAlertsPanel';
23
25
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
24
26
  import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';
25
27
  import { joinPath } from '../utils/navigation';
@@ -50,6 +52,13 @@ export default function FleetPage() {
50
52
  refetch: refetchDrivers,
51
53
  } = useDrivers();
52
54
 
55
+ const {
56
+ data: alerts,
57
+ isLoading: alertsLoading,
58
+ error: alertsError,
59
+ refetch: refetchAlerts,
60
+ } = useFleetAlerts();
61
+
53
62
  const isLoading = statsLoading || vehiclesLoading || driversLoading;
54
63
  const error = statsError ?? vehiclesError ?? driversError;
55
64
  const refetchAll = () => {
@@ -64,6 +73,9 @@ export default function FleetPage() {
64
73
  }
65
74
  };
66
75
 
76
+ const openAlert = (alert: FleetAlert) =>
77
+ goTo(`fleet/${alert.subjectType === 'VEHICLE' ? 'vehicles' : 'drivers'}/${alert.subjectId}`);
78
+
67
79
  const onRowKeyDown = (e: React.KeyboardEvent, action: () => void) => {
68
80
  if (e.key === 'Enter' || e.key === ' ') {
69
81
  e.preventDefault();
@@ -136,6 +148,15 @@ export default function FleetPage() {
136
148
  ]}
137
149
  />
138
150
 
151
+ <FleetAlertsPanel
152
+ alerts={alerts}
153
+ isLoading={alertsLoading}
154
+ error={alertsError}
155
+ onRetry={refetchAlerts}
156
+ onOpen={openAlert}
157
+ t={t}
158
+ />
159
+
139
160
  {/* Key Metrics */}
140
161
  {error && !isLoading ? (
141
162
  <Card className="border-status-error-border">