@burdenoff/microfe-movethewheels 2026.1012.1 → 2026.1013.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.
- package/dist/hooks/useContainerLifecycle.js +49 -42
- package/dist/hooks/useEpod.js +134 -70
- package/dist/hooks/useShipmentPermissions.js +28 -0
- package/dist/movethewheels/hooks/useContainerLifecycle.d.ts +1 -1
- package/dist/movethewheels/hooks/useEpod.d.ts +1 -0
- package/dist/movethewheels/hooks/useShipmentPermissions.d.ts +9 -0
- package/dist/pages/ContainersPage.js +184 -168
- package/dist/pages/DeliveryCapturePage.js +87 -85
- package/dist/pages/FreeTimePage.js +205 -191
- package/package.json +4 -3
- package/src/movethewheels/hooks/useContainerLifecycle.ts +10 -6
- package/src/movethewheels/hooks/useEpod.ts +109 -33
- package/src/movethewheels/hooks/useShipmentPermissions.ts +38 -0
- package/src/movethewheels/pages/ContainersPage.tsx +198 -159
- package/src/movethewheels/pages/DeliveryCapturePage.tsx +11 -8
- package/src/movethewheels/pages/FreeTimePage.tsx +55 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-movethewheels",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.1013.1",
|
|
4
4
|
"description": "Burdenoff MoveTheWheels Microfrontend - Logistics & Supply Chain Management Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
37
37
|
"type:check": "bash scripts/build-limits.sh tsc --noEmit",
|
|
38
38
|
"validate:schema-drift": "graphql-schema-drift",
|
|
39
|
-
"sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run validate:schema-drift && bun run lint:sanity && bun run format && bun run type:check && bun run verify:live-logistics-contract && bun run verify:empty-reuse-retry && bun run verify:empty-reuse-recovery && bun run verify:trip-utilization-input && bun run verify:trip-draft-context && bun run verify:stock-binding-input && bun run verify:stock-observation-input && bun run verify:pending-pick-input && bun run verify:freight-input && bun run verify:freight-recovery && bun run verify:procedure-input && bun run verify:consolidation && bun run verify:multimodal-single-attempt && bun run verify:workspace-predispatch && bun run verify:tracking-quality-outcomes && bun run verify:shipment-status-authority && bun run verify:braces-depth && bun run build && bun run build-storybook'",
|
|
39
|
+
"sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run validate:schema-drift && bun run lint:sanity && bun run format && bun run type:check && bun run verify:live-logistics-contract && bun run verify:empty-reuse-retry && bun run verify:empty-reuse-recovery && bun run verify:trip-utilization-input && bun run verify:trip-draft-context && bun run verify:stock-binding-input && bun run verify:stock-observation-input && bun run verify:pending-pick-input && bun run verify:freight-input && bun run verify:freight-recovery && bun run verify:procedure-input && bun run verify:consolidation && bun run verify:multimodal-single-attempt && bun run verify:workspace-predispatch && bun run verify:tracking-quality-outcomes && bun run verify:shipment-status-authority && bun run verify:free-time-permissions && bun run verify:braces-depth && bun run build && bun run build-storybook'",
|
|
40
40
|
"storybook": "storybook dev -p 6006",
|
|
41
41
|
"build-storybook": "storybook build",
|
|
42
42
|
"test:storybook": "test-storybook",
|
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
"verify:consolidation": "bun scripts/verify-consolidation-input.mjs && bun scripts/verify-consolidation-recovery-hook.mjs && bun scripts/verify-consolidation-mounted-hook.mjs",
|
|
65
65
|
"verify:tracking-quality-outcomes": "bun test scripts/verify-tracking-quality-outcomes.test.ts",
|
|
66
66
|
"verify:braces-depth": "bun test scripts/braces-depth-patch.test.ts",
|
|
67
|
-
"verify:shipment-status-authority": "bun scripts/verify-shipment-status-command.mjs"
|
|
67
|
+
"verify:shipment-status-authority": "bun scripts/verify-shipment-status-command.mjs",
|
|
68
|
+
"verify:free-time-permissions": "bun scripts/verify-free-time-permission-gates.mjs"
|
|
68
69
|
},
|
|
69
70
|
"dependencies": {
|
|
70
71
|
"@apollo/client": "^4.0.11",
|
|
@@ -64,7 +64,7 @@ function describeError(error: unknown): string {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** Source-labelled physical equipment; cargo shipment completion does not close a cycle. */
|
|
67
|
-
export function useContainerLifecycle() {
|
|
67
|
+
export function useContainerLifecycle(readEnabled = true) {
|
|
68
68
|
const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
|
|
69
69
|
useMoveTheWheels();
|
|
70
70
|
const options = useMemo(
|
|
@@ -103,6 +103,10 @@ export function useContainerLifecycle() {
|
|
|
103
103
|
setError(null);
|
|
104
104
|
setContainers([]);
|
|
105
105
|
setHasMore(false);
|
|
106
|
+
if (!readEnabled) {
|
|
107
|
+
setLoading(false);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
106
110
|
void executeGraphQL<ContainerRegistryQuery>(options, print(ContainerRegistryDocument), {
|
|
107
111
|
pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },
|
|
108
112
|
})
|
|
@@ -121,7 +125,7 @@ export function useContainerLifecycle() {
|
|
|
121
125
|
return () => {
|
|
122
126
|
cancelled = true;
|
|
123
127
|
};
|
|
124
|
-
}, [options, page, revision]);
|
|
128
|
+
}, [options, page, revision, readEnabled]);
|
|
125
129
|
|
|
126
130
|
// Clear selections whenever the security/workspace context changes.
|
|
127
131
|
useEffect(() => {
|
|
@@ -137,7 +141,7 @@ export function useContainerLifecycle() {
|
|
|
137
141
|
setCycles([]);
|
|
138
142
|
setHasMoreCycles(false);
|
|
139
143
|
setCycleError(null);
|
|
140
|
-
if (!containerId) {
|
|
144
|
+
if (!readEnabled || !containerId) {
|
|
141
145
|
setCyclesLoading(false);
|
|
142
146
|
return;
|
|
143
147
|
}
|
|
@@ -161,14 +165,14 @@ export function useContainerLifecycle() {
|
|
|
161
165
|
return () => {
|
|
162
166
|
cancelled = true;
|
|
163
167
|
};
|
|
164
|
-
}, [options, containerId, cyclePage, revision]);
|
|
168
|
+
}, [options, containerId, cyclePage, revision, readEnabled]);
|
|
165
169
|
|
|
166
170
|
useEffect(() => {
|
|
167
171
|
let cancelled = false;
|
|
168
172
|
setEvents([]);
|
|
169
173
|
setHasMoreEvents(false);
|
|
170
174
|
setEventError(null);
|
|
171
|
-
if (!cycleId) {
|
|
175
|
+
if (!readEnabled || !cycleId) {
|
|
172
176
|
setEventsLoading(false);
|
|
173
177
|
return;
|
|
174
178
|
}
|
|
@@ -192,7 +196,7 @@ export function useContainerLifecycle() {
|
|
|
192
196
|
return () => {
|
|
193
197
|
cancelled = true;
|
|
194
198
|
};
|
|
195
|
-
}, [options, cycleId, eventPage, revision]);
|
|
199
|
+
}, [options, cycleId, eventPage, revision, readEnabled]);
|
|
196
200
|
|
|
197
201
|
const refresh = useCallback(() => setRevision((value) => value + 1), []);
|
|
198
202
|
// Keep the same key after an uncertain response so a manual retry replays the
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Deferred (see BOFF-4399 follow-up notes): barcode/RFID capture and
|
|
12
12
|
* multi-language delivery instructions.
|
|
13
13
|
*/
|
|
14
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
14
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
15
15
|
import { parse } from 'graphql';
|
|
16
16
|
import { useShipmentStatusCommand } from './useShipmentStatusAuthority';
|
|
17
17
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
@@ -212,51 +212,127 @@ interface ApiPendingJob {
|
|
|
212
212
|
export function useMyPendingEpodDeliveryJobs(options: { limit?: number; offset?: number } = {}) {
|
|
213
213
|
const { limit = 20, offset = 0 } = options;
|
|
214
214
|
const graphQLOptions = useGraphQLOptions();
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
215
|
+
const scopeKey = JSON.stringify([
|
|
216
|
+
graphQLOptions.apiGatewayUrl,
|
|
217
|
+
graphQLOptions.workspaceId,
|
|
218
|
+
graphQLOptions.tenantId,
|
|
219
|
+
graphQLOptions.actorId,
|
|
220
|
+
]);
|
|
221
|
+
const readKey = JSON.stringify([scopeKey, limit, offset]);
|
|
222
|
+
// A validated capture ACK makes this stop ineligible for capture immediately.
|
|
223
|
+
// Keep that fact for this actor/scope while a pending-list refresh is delayed.
|
|
224
|
+
// This is an in-memory UI exclusion, never a fabricated server job update.
|
|
225
|
+
const acknowledgedJobs = useRef({ scopeKey, ids: new Set<string>() });
|
|
226
|
+
const requestId = useRef(0);
|
|
227
|
+
const [snapshot, setSnapshot] = useState<{
|
|
228
|
+
scopeKey: string;
|
|
229
|
+
readKey: string;
|
|
230
|
+
jobs: PendingEpodDeliveryJob[];
|
|
231
|
+
totalJobs: number;
|
|
232
|
+
hasMore: boolean;
|
|
233
|
+
isLoading: boolean;
|
|
234
|
+
error: Error | null;
|
|
235
|
+
}>({ scopeKey, readKey, jobs: [], totalJobs: 0, hasMore: false, isLoading: true, error: null });
|
|
220
236
|
|
|
221
237
|
const load = useCallback(async () => {
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
const request = ++requestId.current;
|
|
239
|
+
if (acknowledgedJobs.current.scopeKey !== scopeKey) {
|
|
240
|
+
acknowledgedJobs.current = { scopeKey, ids: new Set<string>() };
|
|
241
|
+
}
|
|
242
|
+
setSnapshot((previous) => ({
|
|
243
|
+
scopeKey,
|
|
244
|
+
readKey,
|
|
245
|
+
jobs: previous.readKey === readKey ? previous.jobs : [],
|
|
246
|
+
totalJobs: previous.readKey === readKey ? previous.totalJobs : 0,
|
|
247
|
+
hasMore: previous.readKey === readKey ? previous.hasMore : false,
|
|
248
|
+
isLoading: true,
|
|
249
|
+
error: null,
|
|
250
|
+
}));
|
|
224
251
|
try {
|
|
225
252
|
const data = await executeGraphQL<{
|
|
226
253
|
myPendingEpodDeliveryJobs: ApiPendingJob[];
|
|
227
254
|
myPendingEpodDeliveryJobsCount: number;
|
|
228
255
|
}>(graphQLOptions, MY_PENDING_EPOD_JOBS_QUERY, { pagination: { limit, offset } });
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
256
|
+
if (request !== requestId.current) return;
|
|
257
|
+
const rows = data.myPendingEpodDeliveryJobs;
|
|
258
|
+
const total = data.myPendingEpodDeliveryJobsCount ?? 0;
|
|
259
|
+
const excluded = rows.filter((job) => acknowledgedJobs.current.ids.has(job.id)).length;
|
|
260
|
+
setSnapshot({
|
|
261
|
+
scopeKey,
|
|
262
|
+
readKey,
|
|
263
|
+
totalJobs: Math.max(0, total - excluded),
|
|
264
|
+
// Pagination still follows the server page, including any excluded row.
|
|
265
|
+
hasMore: offset + rows.length < total,
|
|
266
|
+
jobs: rows
|
|
267
|
+
.filter((job) => !acknowledgedJobs.current.ids.has(job.id))
|
|
268
|
+
.map((job) => ({
|
|
269
|
+
id: job.id,
|
|
270
|
+
deliveryJobNumber: job.deliveryJobNumber,
|
|
271
|
+
status: job.status,
|
|
272
|
+
priority: job.priority,
|
|
273
|
+
deliveryAddress: job.deliveryAddress,
|
|
274
|
+
scheduledDelivery: job.scheduledDelivery ? new Date(job.scheduledDelivery) : undefined,
|
|
275
|
+
customerName: job.customer?.name ?? undefined,
|
|
276
|
+
customerPhone: job.customer?.phone ?? undefined,
|
|
277
|
+
})),
|
|
278
|
+
isLoading: false,
|
|
279
|
+
error: null,
|
|
280
|
+
});
|
|
245
281
|
} catch (fetchError) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
282
|
+
if (request !== requestId.current) return;
|
|
283
|
+
setSnapshot({
|
|
284
|
+
scopeKey,
|
|
285
|
+
readKey,
|
|
286
|
+
jobs: [],
|
|
287
|
+
totalJobs: 0,
|
|
288
|
+
hasMore: false,
|
|
289
|
+
isLoading: false,
|
|
290
|
+
error:
|
|
291
|
+
fetchError instanceof Error ? fetchError : new Error('Failed to load pending deliveries'),
|
|
292
|
+
});
|
|
252
293
|
}
|
|
253
|
-
}, [graphQLOptions, limit, offset]);
|
|
294
|
+
}, [graphQLOptions, limit, offset, scopeKey, readKey]);
|
|
295
|
+
|
|
296
|
+
const cancelPendingRead = useCallback(() => {
|
|
297
|
+
requestId.current++;
|
|
298
|
+
}, []);
|
|
254
299
|
|
|
255
300
|
useEffect(() => {
|
|
256
301
|
void load();
|
|
257
|
-
|
|
302
|
+
return cancelPendingRead;
|
|
303
|
+
}, [load, cancelPendingRead]);
|
|
304
|
+
|
|
305
|
+
const retireAcknowledgedJob = useCallback(
|
|
306
|
+
(deliveryJobId: string) => {
|
|
307
|
+
if (
|
|
308
|
+
acknowledgedJobs.current.scopeKey !== scopeKey ||
|
|
309
|
+
acknowledgedJobs.current.ids.has(deliveryJobId)
|
|
310
|
+
)
|
|
311
|
+
return;
|
|
312
|
+
acknowledgedJobs.current.ids.add(deliveryJobId);
|
|
313
|
+
setSnapshot((previous) => {
|
|
314
|
+
if (previous.scopeKey !== scopeKey) return previous;
|
|
315
|
+
const removed = previous.jobs.some((job) => job.id === deliveryJobId);
|
|
316
|
+
return {
|
|
317
|
+
...previous,
|
|
318
|
+
jobs: previous.jobs.filter((job) => job.id !== deliveryJobId),
|
|
319
|
+
totalJobs: Math.max(0, previous.totalJobs - (removed ? 1 : 0)),
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
[scopeKey]
|
|
324
|
+
);
|
|
258
325
|
|
|
259
|
-
|
|
326
|
+
const current = snapshot.readKey === readKey;
|
|
327
|
+
return {
|
|
328
|
+
jobs: current ? snapshot.jobs : [],
|
|
329
|
+
totalJobs: current ? snapshot.totalJobs : 0,
|
|
330
|
+
hasMore: current && snapshot.hasMore,
|
|
331
|
+
isLoading: !current || snapshot.isLoading,
|
|
332
|
+
error: current ? snapshot.error : null,
|
|
333
|
+
refetch: load,
|
|
334
|
+
retireAcknowledgedJob,
|
|
335
|
+
};
|
|
260
336
|
}
|
|
261
337
|
|
|
262
338
|
export interface CaptureEpodArgs {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
import { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';
|
|
3
|
+
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
4
|
+
|
|
5
|
+
/** Match ordinary effective permissions to this page's current actor and workspace. */
|
|
6
|
+
export function useShipmentPermissions() {
|
|
7
|
+
const permissions = usePermissions();
|
|
8
|
+
const { workspaceId, currentUser } = useMoveTheWheels();
|
|
9
|
+
const currentScope = Boolean(
|
|
10
|
+
workspaceId &&
|
|
11
|
+
currentUser?.id &&
|
|
12
|
+
permissions.scopeId === workspaceId &&
|
|
13
|
+
(permissions.isSuperAdmin ||
|
|
14
|
+
(permissions.permissions?.scopeId === workspaceId &&
|
|
15
|
+
permissions.permissions.actorId === currentUser.id))
|
|
16
|
+
);
|
|
17
|
+
const ready = currentScope && !permissions.isLoading && !permissions.error;
|
|
18
|
+
const canRead = ready && permissions.hasPermission('shipment:read');
|
|
19
|
+
const canCreate = canRead && permissions.hasPermission('shipment:create');
|
|
20
|
+
const canUpdate = canRead && permissions.hasPermission('shipment:update');
|
|
21
|
+
const current = useRef({ create: false, update: false });
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
current.current = { create: canCreate, update: canUpdate };
|
|
24
|
+
return () => {
|
|
25
|
+
// Retained callbacks from a revoked or unmounted scope cannot submit a command.
|
|
26
|
+
current.current = { create: false, update: false };
|
|
27
|
+
};
|
|
28
|
+
}, [canCreate, canUpdate]);
|
|
29
|
+
const allows = useCallback((action: 'create' | 'update') => current.current[action], []);
|
|
30
|
+
return {
|
|
31
|
+
canRead,
|
|
32
|
+
canCreate,
|
|
33
|
+
canUpdate,
|
|
34
|
+
allows,
|
|
35
|
+
loading: permissions.isLoading,
|
|
36
|
+
failed: Boolean(permissions.error),
|
|
37
|
+
};
|
|
38
|
+
}
|