@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.
- package/dist/MoveTheWheelsRoot.js +141 -133
- package/dist/MoveTheWheelsRoutes.js +29 -25
- package/dist/components/EpodCaptureForm.js +256 -0
- package/dist/generated/wspace-types.d.ts +9012 -132
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useCarrierIntegrations.js +141 -0
- package/dist/hooks/useDriverDay.js +153 -0
- package/dist/hooks/useOrders.js +1 -1
- package/dist/index.js +27 -26
- package/dist/movethewheels/components/EpodCaptureForm.d.ts +12 -0
- package/dist/movethewheels/hooks/index.d.ts +1 -0
- package/dist/movethewheels/hooks/useCarrierIntegrations.d.ts +39 -0
- package/dist/movethewheels/hooks/useDriverDay.d.ts +53 -0
- package/dist/movethewheels/pages/IntegrationsPage.d.ts +6 -0
- package/dist/movethewheels/utils/carrierIntegrations.d.ts +88 -0
- package/dist/movethewheels/utils/driverNavigation.d.ts +31 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/calendar-days.js +50 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/copy.js +16 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/pause.js +19 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/play.js +8 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/wifi-off.js +34 -0
- package/dist/pages/DeliveryCapturePage.js +450 -321
- package/dist/pages/IntegrationsPage.js +616 -0
- package/dist/pages/SettingsPage.js +168 -150
- package/dist/utils/carrierIntegrations.js +105 -0
- package/dist/utils/driverNavigation.js +51 -0
- package/dist/utils/formatters.js +18 -9
- package/package.json +4 -2
- package/src/generated/wspace-types.ts +10266 -142
- package/src/movethewheels/MoveTheWheelsRoot.tsx +8 -0
- package/src/movethewheels/MoveTheWheelsRoutes.tsx +4 -0
- package/src/movethewheels/components/EpodCaptureForm.tsx +343 -0
- package/src/movethewheels/hooks/index.ts +9 -0
- package/src/movethewheels/hooks/useCarrierIntegrations.ts +221 -0
- package/src/movethewheels/hooks/useDriverDay.ts +289 -0
- package/src/movethewheels/pages/DeliveryCapturePage.tsx +478 -392
- package/src/movethewheels/pages/IntegrationsPage.tsx +752 -0
- package/src/movethewheels/pages/SettingsPage.tsx +19 -3
- package/src/movethewheels/utils/carrierIntegrations.ts +252 -0
- 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
|
+
}
|