@burdenoff/microfe-movethewheels 2026.1031.6 → 2026.1101.2

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 (40) hide show
  1. package/dist/MoveTheWheelsRoot.js +141 -133
  2. package/dist/MoveTheWheelsRoutes.js +29 -25
  3. package/dist/components/EpodCaptureForm.js +256 -0
  4. package/dist/generated/wspace-types.d.ts +9012 -132
  5. package/dist/hooks/index.js +1 -0
  6. package/dist/hooks/useCarrierIntegrations.js +141 -0
  7. package/dist/hooks/useDriverDay.js +153 -0
  8. package/dist/hooks/useOrders.js +1 -1
  9. package/dist/index.js +27 -26
  10. package/dist/movethewheels/components/EpodCaptureForm.d.ts +12 -0
  11. package/dist/movethewheels/hooks/index.d.ts +1 -0
  12. package/dist/movethewheels/hooks/useCarrierIntegrations.d.ts +39 -0
  13. package/dist/movethewheels/hooks/useDriverDay.d.ts +53 -0
  14. package/dist/movethewheels/pages/IntegrationsPage.d.ts +6 -0
  15. package/dist/movethewheels/utils/carrierIntegrations.d.ts +88 -0
  16. package/dist/movethewheels/utils/driverNavigation.d.ts +31 -0
  17. package/dist/node_modules/lucide-react/dist/esm/icons/calendar-days.js +50 -0
  18. package/dist/node_modules/lucide-react/dist/esm/icons/copy.js +16 -0
  19. package/dist/node_modules/lucide-react/dist/esm/icons/pause.js +19 -0
  20. package/dist/node_modules/lucide-react/dist/esm/icons/play.js +8 -0
  21. package/dist/node_modules/lucide-react/dist/esm/icons/wifi-off.js +34 -0
  22. package/dist/pages/DeliveryCapturePage.js +450 -321
  23. package/dist/pages/IntegrationsPage.js +616 -0
  24. package/dist/pages/SettingsPage.js +168 -150
  25. package/dist/utils/carrierIntegrations.js +105 -0
  26. package/dist/utils/driverNavigation.js +51 -0
  27. package/dist/utils/formatters.js +18 -9
  28. package/package.json +4 -2
  29. package/src/generated/wspace-types.ts +10266 -142
  30. package/src/movethewheels/MoveTheWheelsRoot.tsx +8 -0
  31. package/src/movethewheels/MoveTheWheelsRoutes.tsx +4 -0
  32. package/src/movethewheels/components/EpodCaptureForm.tsx +343 -0
  33. package/src/movethewheels/hooks/index.ts +9 -0
  34. package/src/movethewheels/hooks/useCarrierIntegrations.ts +221 -0
  35. package/src/movethewheels/hooks/useDriverDay.ts +289 -0
  36. package/src/movethewheels/pages/DeliveryCapturePage.tsx +478 -392
  37. package/src/movethewheels/pages/IntegrationsPage.tsx +752 -0
  38. package/src/movethewheels/pages/SettingsPage.tsx +19 -3
  39. package/src/movethewheels/utils/carrierIntegrations.ts +252 -0
  40. package/src/movethewheels/utils/driverNavigation.ts +103 -0
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Driver day view data (BOFF-3991): the calling driver's stops for one local
3
+ * calendar day, in driving sequence, from the `myDeliveryDay` Type C
4
+ * self-list in `wspace-movethewheels-svc` (src/modules/epod/schema/epod.gql).
5
+ * The driver is resolved from the authenticated actor server-side; this hook
6
+ * never sends a driver or user id.
7
+ */
8
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
9
+ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
10
+ import type { Order } from '../types';
11
+ import { executeGraphQL } from '../utils/graphqlClient';
12
+ import { deviceTimeZone, timezoneOffsetFor } from '../utils/driverNavigation';
13
+
14
+ const STOP_FIELDS = `
15
+ sequence
16
+ state
17
+ awaitingCapture
18
+ overdue
19
+ deliveredAt
20
+ routeId
21
+ routeName
22
+ deliveryJob {
23
+ id
24
+ deliveryJobNumber
25
+ status
26
+ priority
27
+ deliveryAddress
28
+ scheduledDelivery
29
+ specialInstructions
30
+ customer {
31
+ name
32
+ phone
33
+ }
34
+ }
35
+ `;
36
+
37
+ const MY_DELIVERY_DAY_QUERY = `
38
+ query MyDeliveryDay($date: String!, $timezoneOffsetMinutes: Int, $timeZone: String) {
39
+ myDeliveryDay(date: $date, timezoneOffsetMinutes: $timezoneOffsetMinutes, timeZone: $timeZone) {
40
+ date
41
+ driverId
42
+ driverName
43
+ totalStops
44
+ completedStops
45
+ remainingStops
46
+ stops {
47
+ ${STOP_FIELDS}
48
+ }
49
+ }
50
+ }
51
+ `;
52
+
53
+ export type DriverStopState = 'COMPLETED' | 'EN_ROUTE' | 'UPCOMING' | 'FAILED';
54
+
55
+ export interface DriverStop {
56
+ sequence: number;
57
+ state: DriverStopState;
58
+ awaitingCapture: boolean;
59
+ overdue: boolean;
60
+ deliveredAt?: Date;
61
+ routeId?: string;
62
+ routeName?: string;
63
+ jobId: string;
64
+ deliveryJobNumber: string;
65
+ status: string;
66
+ priority: string;
67
+ deliveryAddress: Order['deliveryAddress'] | null;
68
+ scheduledDelivery?: Date;
69
+ specialInstructions?: string;
70
+ customerName?: string;
71
+ customerPhone?: string;
72
+ }
73
+
74
+ export interface DriverDay {
75
+ date: string;
76
+ driverId: string | null;
77
+ driverName: string | null;
78
+ stops: DriverStop[];
79
+ totalStops: number;
80
+ completedStops: number;
81
+ remainingStops: number;
82
+ nextStop: DriverStop | null;
83
+ }
84
+
85
+ interface ApiStop {
86
+ sequence: number;
87
+ state: DriverStopState;
88
+ awaitingCapture: boolean;
89
+ overdue: boolean;
90
+ deliveredAt?: string | null;
91
+ routeId?: string | null;
92
+ routeName?: string | null;
93
+ deliveryJob: {
94
+ id: string;
95
+ deliveryJobNumber: string;
96
+ status: string;
97
+ priority: string;
98
+ deliveryAddress: Order['deliveryAddress'] | null;
99
+ scheduledDelivery?: string | null;
100
+ specialInstructions?: string | null;
101
+ customer?: { name?: string | null; phone?: string | null } | null;
102
+ };
103
+ }
104
+
105
+ interface ApiDriverDay {
106
+ date: string;
107
+ driverId: string | null;
108
+ driverName: string | null;
109
+ totalStops: number;
110
+ completedStops: number;
111
+ remainingStops: number;
112
+ stops: ApiStop[];
113
+ }
114
+
115
+ function mapStop(stop: ApiStop): DriverStop {
116
+ const job = stop.deliveryJob;
117
+ return {
118
+ sequence: stop.sequence,
119
+ state: stop.state,
120
+ awaitingCapture: stop.awaitingCapture,
121
+ overdue: stop.overdue,
122
+ deliveredAt: stop.deliveredAt ? new Date(stop.deliveredAt) : undefined,
123
+ routeId: stop.routeId ?? undefined,
124
+ routeName: stop.routeName ?? undefined,
125
+ jobId: job.id,
126
+ deliveryJobNumber: job.deliveryJobNumber,
127
+ status: job.status,
128
+ priority: job.priority,
129
+ deliveryAddress: job.deliveryAddress,
130
+ scheduledDelivery: job.scheduledDelivery ? new Date(job.scheduledDelivery) : undefined,
131
+ specialInstructions: job.specialInstructions ?? undefined,
132
+ customerName: job.customer?.name ?? undefined,
133
+ customerPhone: job.customer?.phone ?? undefined,
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Applies acknowledged captures on top of a server snapshot: a stop whose
139
+ * capture the server already acknowledged is shown completed even if a
140
+ * delayed read still reports it en route. This is an in-memory UI fact for
141
+ * the current actor/scope, never a fabricated server update.
142
+ */
143
+ export function applyAcknowledgedCaptures(
144
+ stops: DriverStop[],
145
+ acknowledged: ReadonlySet<string>
146
+ ): DriverDay['stops'] {
147
+ return stops.map((stop) =>
148
+ acknowledged.has(stop.jobId) && stop.state !== 'COMPLETED'
149
+ ? { ...stop, state: 'COMPLETED', awaitingCapture: false, status: 'DELIVERED' }
150
+ : stop
151
+ );
152
+ }
153
+
154
+ /** Derived counters and the next stop, recomputed after acknowledgement overrides. */
155
+ export function summariseStops(stops: DriverStop[]) {
156
+ const completedStops = stops.filter(
157
+ (stop) => stop.state === 'COMPLETED' || stop.state === 'FAILED'
158
+ ).length;
159
+ return {
160
+ totalStops: stops.length,
161
+ completedStops,
162
+ remainingStops: stops.length - completedStops,
163
+ nextStop:
164
+ stops.find((stop) => stop.awaitingCapture) ??
165
+ stops.find((stop) => stop.state === 'UPCOMING') ??
166
+ null,
167
+ };
168
+ }
169
+
170
+ export function useMyDeliveryDay(date: string) {
171
+ const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
172
+ useMoveTheWheels();
173
+ const graphQLOptions = useMemo(
174
+ () => ({
175
+ apiGatewayUrl,
176
+ authToken,
177
+ workspaceToken,
178
+ workspaceId,
179
+ tenantId,
180
+ actorId: currentUser?.id,
181
+ }),
182
+ [apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
183
+ );
184
+ const scopeKey = JSON.stringify([apiGatewayUrl, workspaceId, tenantId, currentUser?.id]);
185
+ const readKey = JSON.stringify([scopeKey, date]);
186
+ const [acknowledgedJobs, setAcknowledgedJobs] = useState<{
187
+ scopeKey: string;
188
+ ids: ReadonlySet<string>;
189
+ }>({ scopeKey, ids: new Set<string>() });
190
+ const requestId = useRef(0);
191
+ const [snapshot, setSnapshot] = useState<{
192
+ readKey: string;
193
+ day: ApiDriverDay | null;
194
+ isLoading: boolean;
195
+ error: Error | null;
196
+ }>({ readKey, day: null, isLoading: true, error: null });
197
+
198
+ const load = useCallback(async () => {
199
+ const request = ++requestId.current;
200
+ setSnapshot((previous) => ({
201
+ readKey,
202
+ day: previous.readKey === readKey ? previous.day : null,
203
+ isLoading: true,
204
+ error: null,
205
+ }));
206
+ try {
207
+ const data = await executeGraphQL<{ myDeliveryDay: ApiDriverDay }>(
208
+ graphQLOptions,
209
+ MY_DELIVERY_DAY_QUERY,
210
+ { date, timezoneOffsetMinutes: timezoneOffsetFor(date), timeZone: deviceTimeZone() }
211
+ );
212
+ if (request !== requestId.current) return;
213
+ setSnapshot({ readKey, day: data.myDeliveryDay, isLoading: false, error: null });
214
+ } catch (fetchError) {
215
+ if (request !== requestId.current) return;
216
+ setSnapshot((previous) => ({
217
+ readKey,
218
+ // Keep the last good list on screen (e.g. a dropped connection on the road).
219
+ day: previous.readKey === readKey ? previous.day : null,
220
+ isLoading: false,
221
+ error:
222
+ fetchError instanceof Error ? fetchError : new Error('Failed to load your deliveries'),
223
+ }));
224
+ }
225
+ }, [graphQLOptions, date, readKey]);
226
+
227
+ const cancelPendingRead = useCallback(() => {
228
+ requestId.current++;
229
+ }, []);
230
+
231
+ useEffect(() => {
232
+ void load();
233
+ return cancelPendingRead;
234
+ }, [load, cancelPendingRead]);
235
+
236
+ const markCaptured = useCallback(
237
+ (deliveryJobId: string) => {
238
+ setAcknowledgedJobs((previous) => {
239
+ // An actor/workspace switch starts a fresh acknowledgement set.
240
+ const ids = new Set(previous.scopeKey === scopeKey ? previous.ids : []);
241
+ ids.add(deliveryJobId);
242
+ return { scopeKey, ids };
243
+ });
244
+ },
245
+ [scopeKey]
246
+ );
247
+
248
+ const current = snapshot.readKey === readKey ? snapshot : null;
249
+ const currentDay = current?.day ?? null;
250
+ const day = useMemo<DriverDay | null>(() => {
251
+ if (!currentDay) return null;
252
+ const acknowledged =
253
+ acknowledgedJobs.scopeKey === scopeKey ? acknowledgedJobs.ids : new Set<string>();
254
+ const stops = applyAcknowledgedCaptures(currentDay.stops.map(mapStop), acknowledged);
255
+ return {
256
+ date: currentDay.date,
257
+ driverId: currentDay.driverId,
258
+ driverName: currentDay.driverName,
259
+ stops,
260
+ ...summariseStops(stops),
261
+ };
262
+ }, [currentDay, scopeKey, acknowledgedJobs]);
263
+
264
+ return {
265
+ day,
266
+ isLoading: !current || current.isLoading,
267
+ error: current?.error ?? null,
268
+ refetch: load,
269
+ markCaptured,
270
+ };
271
+ }
272
+
273
+ /** Tracks `navigator.onLine` so the driver sees when capture is unavailable. */
274
+ export function useOnlineStatus(): boolean {
275
+ const [online, setOnline] = useState(() =>
276
+ typeof navigator === 'undefined' ? true : navigator.onLine !== false
277
+ );
278
+ useEffect(() => {
279
+ if (typeof window === 'undefined') return;
280
+ const update = () => setOnline(navigator.onLine !== false);
281
+ window.addEventListener('online', update);
282
+ window.addEventListener('offline', update);
283
+ return () => {
284
+ window.removeEventListener('online', update);
285
+ window.removeEventListener('offline', update);
286
+ };
287
+ }, []);
288
+ return online;
289
+ }