@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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useState, type FormEvent } from 'react';
|
|
2
2
|
import { Boxes, RefreshCw } from 'lucide-react';
|
|
3
|
+
import { PermissionProvider } from '@burdenoff/fe-libs/shared/providers/shell';
|
|
4
|
+
import { useShipmentPermissions } from '../hooks/useShipmentPermissions';
|
|
3
5
|
import { PageLayout } from '../components/PageLayout';
|
|
4
6
|
import { Button, Input, Label } from '../components/ui';
|
|
5
7
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
@@ -23,12 +25,25 @@ const sectionClass = 'rounded-xl border bg-bg-surface p-5 space-y-4';
|
|
|
23
25
|
|
|
24
26
|
/** Remount on identity/workspace changes so previous workspace records never flash. */
|
|
25
27
|
export default function ContainersPage() {
|
|
26
|
-
const { workspaceId, currentUser } = useMoveTheWheels();
|
|
27
|
-
return
|
|
28
|
+
const { workspaceId, tenantId, currentUser, apiGatewayUrl } = useMoveTheWheels();
|
|
29
|
+
return (
|
|
30
|
+
<PermissionProvider
|
|
31
|
+
key={`${workspaceId}:${tenantId}:${currentUser?.id}:${apiGatewayUrl}`}
|
|
32
|
+
scopeId={workspaceId ?? null}
|
|
33
|
+
gateway="workspace"
|
|
34
|
+
>
|
|
35
|
+
<ContainerAccess />
|
|
36
|
+
</PermissionProvider>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ContainerAccess() {
|
|
41
|
+
const access = useShipmentPermissions();
|
|
42
|
+
return <ContainerWorkbench access={access} />;
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
function ContainerWorkbench() {
|
|
31
|
-
const data = useContainerLifecycle();
|
|
45
|
+
function ContainerWorkbench({ access }: { access: ReturnType<typeof useShipmentPermissions> }) {
|
|
46
|
+
const data = useContainerLifecycle(access.canRead);
|
|
32
47
|
const [number, setNumber] = useState('');
|
|
33
48
|
const [equipmentType, setEquipmentType] = useState<EquipmentType>('DRY_20');
|
|
34
49
|
const [ownerName, setOwnerName] = useState('');
|
|
@@ -63,6 +78,7 @@ function ContainerWorkbench() {
|
|
|
63
78
|
};
|
|
64
79
|
const register = (event: FormEvent) => {
|
|
65
80
|
event.preventDefault();
|
|
81
|
+
if (!access.allows('create')) return;
|
|
66
82
|
void act(async () => {
|
|
67
83
|
await data.register({
|
|
68
84
|
containerNumber: number.trim(),
|
|
@@ -75,7 +91,7 @@ function ContainerWorkbench() {
|
|
|
75
91
|
};
|
|
76
92
|
const record = (event: FormEvent) => {
|
|
77
93
|
event.preventDefault();
|
|
78
|
-
if (!cycle) return;
|
|
94
|
+
if (!cycle || !access.allows('update')) return;
|
|
79
95
|
void act(async () => {
|
|
80
96
|
const timestamp = new Date(eventTime);
|
|
81
97
|
if (!Number.isFinite(timestamp.getTime())) throw new Error('Enter a valid event time.');
|
|
@@ -94,6 +110,21 @@ function ContainerWorkbench() {
|
|
|
94
110
|
}, 'Event recorded with its source and receipt time.');
|
|
95
111
|
};
|
|
96
112
|
|
|
113
|
+
if (!access.canRead)
|
|
114
|
+
return (
|
|
115
|
+
<PageLayout
|
|
116
|
+
title="Containers"
|
|
117
|
+
subtitle="Physical equipment, usage cycles and sourced movement history"
|
|
118
|
+
>
|
|
119
|
+
<p role={access.failed ? 'alert' : 'status'}>
|
|
120
|
+
{access.loading
|
|
121
|
+
? 'Checking shipment permissions…'
|
|
122
|
+
: access.failed
|
|
123
|
+
? 'Shipment permissions are unavailable. Reload to retry.'
|
|
124
|
+
: 'Shipment read permission is required.'}
|
|
125
|
+
</p>
|
|
126
|
+
</PageLayout>
|
|
127
|
+
);
|
|
97
128
|
return (
|
|
98
129
|
<PageLayout
|
|
99
130
|
title="Containers"
|
|
@@ -167,54 +198,56 @@ function ContainerWorkbench() {
|
|
|
167
198
|
data.setPage(page);
|
|
168
199
|
}}
|
|
169
200
|
/>
|
|
170
|
-
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
{
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
201
|
+
{access.canCreate && (
|
|
202
|
+
<form onSubmit={register} className="space-y-3 border-t pt-4">
|
|
203
|
+
<h3 className="font-semibold">Register container</h3>
|
|
204
|
+
<Label htmlFor="container-number">ISO container number</Label>
|
|
205
|
+
<Input
|
|
206
|
+
id="container-number"
|
|
207
|
+
value={number}
|
|
208
|
+
onChange={(event) => setNumber(event.target.value)}
|
|
209
|
+
required
|
|
210
|
+
maxLength={20}
|
|
211
|
+
placeholder="Four letters and seven digits"
|
|
212
|
+
/>
|
|
213
|
+
<Label htmlFor="equipment-type">Physical equipment type</Label>
|
|
214
|
+
<select
|
|
215
|
+
id="equipment-type"
|
|
216
|
+
className={selectClass}
|
|
217
|
+
value={equipmentType}
|
|
218
|
+
onChange={(event) =>
|
|
219
|
+
setEquipmentType(
|
|
220
|
+
EQUIPMENT_TYPES.find((value) => value === event.target.value) ?? 'OTHER'
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
>
|
|
224
|
+
{EQUIPMENT_TYPES.map((value) => (
|
|
225
|
+
<option key={value} value={value}>
|
|
226
|
+
{readable(value)}
|
|
227
|
+
</option>
|
|
228
|
+
))}
|
|
229
|
+
</select>
|
|
230
|
+
<Label htmlFor="container-owner">Owner (optional)</Label>
|
|
231
|
+
<Input
|
|
232
|
+
id="container-owner"
|
|
233
|
+
value={ownerName}
|
|
234
|
+
onChange={(event) => setOwnerName(event.target.value)}
|
|
235
|
+
maxLength={200}
|
|
236
|
+
/>
|
|
237
|
+
<Label htmlFor="container-source">Information source</Label>
|
|
238
|
+
<Input
|
|
239
|
+
id="container-source"
|
|
240
|
+
value={source}
|
|
241
|
+
onChange={(event) => setSource(event.target.value)}
|
|
242
|
+
required
|
|
243
|
+
maxLength={120}
|
|
244
|
+
placeholder="Equipment release document or carrier portal"
|
|
245
|
+
/>
|
|
246
|
+
<Button type="submit" disabled={busy || !number.trim() || !source.trim()}>
|
|
247
|
+
Register container
|
|
248
|
+
</Button>
|
|
249
|
+
</form>
|
|
250
|
+
)}
|
|
218
251
|
</section>
|
|
219
252
|
<section className={sectionClass}>
|
|
220
253
|
<h2 className="text-lg font-semibold">Usage cycles</h2>
|
|
@@ -266,11 +299,12 @@ function ContainerWorkbench() {
|
|
|
266
299
|
}}
|
|
267
300
|
/>
|
|
268
301
|
)}
|
|
269
|
-
{data.containerId && (
|
|
302
|
+
{data.containerId && access.canCreate && (
|
|
270
303
|
<form
|
|
271
304
|
className="space-y-3 border-t pt-4"
|
|
272
305
|
onSubmit={(event) => {
|
|
273
306
|
event.preventDefault();
|
|
307
|
+
if (!access.allows('create')) return;
|
|
274
308
|
void act(async () => {
|
|
275
309
|
const timestamp = new Date(openingTime);
|
|
276
310
|
if (!Number.isFinite(timestamp.getTime())) {
|
|
@@ -345,111 +379,116 @@ function ContainerWorkbench() {
|
|
|
345
379
|
)}
|
|
346
380
|
{cycle?.status === 'OPEN' && (
|
|
347
381
|
<>
|
|
348
|
-
|
|
349
|
-
<
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
{
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
382
|
+
{access.canUpdate && (
|
|
383
|
+
<form onSubmit={record} className="space-y-3 border-t pt-4">
|
|
384
|
+
<h3 className="font-semibold">Record event</h3>
|
|
385
|
+
<Label htmlFor="container-event">Event</Label>
|
|
386
|
+
<select
|
|
387
|
+
id="container-event"
|
|
388
|
+
className={selectClass}
|
|
389
|
+
value={eventType}
|
|
390
|
+
onChange={(event) =>
|
|
391
|
+
setEventType(
|
|
392
|
+
CONTAINER_EVENT_TYPES.find((value) => value === event.target.value) ??
|
|
393
|
+
'POSITION_UPDATE'
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
>
|
|
397
|
+
{CONTAINER_EVENT_TYPES.map((value) => (
|
|
398
|
+
<option key={value} value={value}>
|
|
399
|
+
{readable(value)}
|
|
400
|
+
</option>
|
|
401
|
+
))}
|
|
402
|
+
</select>
|
|
403
|
+
<Label htmlFor="event-kind">Event certainty</Label>
|
|
404
|
+
<select
|
|
405
|
+
id="event-kind"
|
|
406
|
+
className={selectClass}
|
|
407
|
+
value={kind}
|
|
408
|
+
onChange={(event) =>
|
|
409
|
+
setKind(
|
|
410
|
+
event.target.value === 'PLANNED'
|
|
411
|
+
? 'PLANNED'
|
|
412
|
+
: event.target.value === 'ESTIMATED'
|
|
413
|
+
? 'ESTIMATED'
|
|
414
|
+
: 'ACTUAL'
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
>
|
|
418
|
+
<option value="ACTUAL">Actual</option>
|
|
419
|
+
<option value="PLANNED">Planned</option>
|
|
420
|
+
<option value="ESTIMATED">Estimated</option>
|
|
421
|
+
</select>
|
|
422
|
+
<Label htmlFor="event-time">Event time (your local timezone)</Label>
|
|
423
|
+
<Input
|
|
424
|
+
id="event-time"
|
|
425
|
+
type="datetime-local"
|
|
426
|
+
step={1}
|
|
427
|
+
value={eventTime}
|
|
428
|
+
onChange={(event) => setEventTime(event.target.value)}
|
|
429
|
+
required
|
|
430
|
+
/>
|
|
431
|
+
<Label htmlFor="event-source">Event source</Label>
|
|
432
|
+
<Input
|
|
433
|
+
id="event-source"
|
|
434
|
+
value={eventSource}
|
|
435
|
+
onChange={(event) => setEventSource(event.target.value)}
|
|
436
|
+
required
|
|
437
|
+
maxLength={120}
|
|
438
|
+
/>
|
|
439
|
+
<Label htmlFor="source-reference">Source reference (optional)</Label>
|
|
440
|
+
<Input
|
|
441
|
+
id="source-reference"
|
|
442
|
+
value={sourceReference}
|
|
443
|
+
onChange={(event) => setSourceReference(event.target.value)}
|
|
444
|
+
maxLength={200}
|
|
445
|
+
/>
|
|
446
|
+
<Label htmlFor="event-location">Location (optional)</Label>
|
|
447
|
+
<Input
|
|
448
|
+
id="event-location"
|
|
449
|
+
value={location}
|
|
450
|
+
onChange={(event) => setLocation(event.target.value)}
|
|
451
|
+
maxLength={200}
|
|
452
|
+
/>
|
|
453
|
+
<Label htmlFor="event-evidence">
|
|
454
|
+
Evidence note
|
|
455
|
+
{eventType === 'RETURN_ACCEPTED' && kind === 'ACTUAL'
|
|
456
|
+
? ' (required for acceptance)'
|
|
457
|
+
: ' (optional)'}
|
|
458
|
+
</Label>
|
|
459
|
+
<Input
|
|
460
|
+
id="event-evidence"
|
|
461
|
+
value={evidence}
|
|
462
|
+
onChange={(event) => setEvidence(event.target.value)}
|
|
463
|
+
maxLength={2000}
|
|
464
|
+
required={eventType === 'RETURN_ACCEPTED' && kind === 'ACTUAL'}
|
|
465
|
+
/>
|
|
466
|
+
<Button type="submit" disabled={busy || !eventSource.trim()}>
|
|
467
|
+
Record event
|
|
468
|
+
</Button>
|
|
469
|
+
</form>
|
|
470
|
+
)}
|
|
471
|
+
{access.canUpdate && (
|
|
472
|
+
<div className="space-y-3 border-t pt-4">
|
|
473
|
+
<p className="text-sm text-text-secondary">
|
|
474
|
+
Closure checks the complete recorded history for actual empty return and
|
|
475
|
+
acceptance. A planned return or delivered shipment is insufficient.
|
|
476
|
+
</p>
|
|
477
|
+
<Button
|
|
478
|
+
variant="outline"
|
|
479
|
+
disabled={busy || data.cyclesLoading || data.eventsLoading}
|
|
480
|
+
onClick={() =>
|
|
481
|
+
access.allows('update') &&
|
|
482
|
+
void act(
|
|
483
|
+
() => data.closeCycle(cycle),
|
|
484
|
+
'Cycle closed after accepted empty return.'
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
>
|
|
488
|
+
Close cycle
|
|
489
|
+
</Button>
|
|
490
|
+
</div>
|
|
491
|
+
)}
|
|
453
492
|
</>
|
|
454
493
|
)}
|
|
455
494
|
{cycle?.status === 'CLOSED' && (
|
|
@@ -78,7 +78,7 @@ function CaptureForm({
|
|
|
78
78
|
onCancel,
|
|
79
79
|
}: {
|
|
80
80
|
job: PendingEpodDeliveryJob;
|
|
81
|
-
onCaptured: () => void;
|
|
81
|
+
onCaptured: (deliveryJobId: string) => void;
|
|
82
82
|
onCancel: () => void;
|
|
83
83
|
}) {
|
|
84
84
|
const { captureEpod, isSubmitting, error: captureError, captureFeedback } = useCaptureEpod();
|
|
@@ -163,7 +163,7 @@ function CaptureForm({
|
|
|
163
163
|
const position = await getCurrentPosition();
|
|
164
164
|
if (!active.current) return;
|
|
165
165
|
|
|
166
|
-
await captureEpod({
|
|
166
|
+
const proof = await captureEpod({
|
|
167
167
|
expectedShipmentId: ownedStatus.data.myEpodShipmentStatus?.id ?? null,
|
|
168
168
|
expectedShipmentStatusRevision: ownedStatus.data.myEpodShipmentStatus?.statusRevision,
|
|
169
169
|
completionAttestation,
|
|
@@ -178,7 +178,8 @@ function CaptureForm({
|
|
|
178
178
|
notes: notes.trim() || undefined,
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
if (!active.current) return;
|
|
182
|
+
onCaptured(proof.deliveryJobId);
|
|
182
183
|
} catch {
|
|
183
184
|
setCompletionAttestation(false);
|
|
184
185
|
setRequiredStatusRefresh(statusRefresh + 1);
|
|
@@ -358,14 +359,16 @@ export default function DeliveryCapturePage() {
|
|
|
358
359
|
const { workspaceId, tenantId, currentUser, apiGatewayUrl } = useMoveTheWheels();
|
|
359
360
|
const [pageIndex, setPageIndex] = useState(0);
|
|
360
361
|
const pageSize = 20;
|
|
361
|
-
const { jobs, hasMore, isLoading, error, refetch } =
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
362
|
+
const { jobs, hasMore, isLoading, error, refetch, retireAcknowledgedJob } =
|
|
363
|
+
useMyPendingEpodDeliveryJobs({
|
|
364
|
+
limit: pageSize,
|
|
365
|
+
offset: pageIndex * pageSize,
|
|
366
|
+
});
|
|
365
367
|
const [selectedJob, setSelectedJob] = useState<PendingEpodDeliveryJob | null>(null);
|
|
366
368
|
const { message, notify } = useNotice();
|
|
367
369
|
|
|
368
|
-
const handleCaptured = () => {
|
|
370
|
+
const handleCaptured = (deliveryJobId: string) => {
|
|
371
|
+
retireAcknowledgedJob(deliveryJobId);
|
|
369
372
|
notify('Proof of delivery captured — delivery marked complete.');
|
|
370
373
|
setSelectedJob(null);
|
|
371
374
|
setPageIndex(0);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useState, type ReactNode } from 'react';
|
|
2
2
|
import { Clock } from 'lucide-react';
|
|
3
|
+
import { PermissionProvider } from '@burdenoff/fe-libs/shared/providers/shell';
|
|
4
|
+
import { useShipmentPermissions } from '../hooks/useShipmentPermissions';
|
|
3
5
|
|
|
4
6
|
import { PageLayout } from '../components/PageLayout';
|
|
5
7
|
import { Button, Input } from '../components/ui';
|
|
@@ -105,11 +107,22 @@ export default function FreeTimePage() {
|
|
|
105
107
|
subtitle="Separate sourced commercial obligations, actual events and recorded counterparty decisions."
|
|
106
108
|
icon={<Clock aria-hidden="true" />}
|
|
107
109
|
>
|
|
108
|
-
<
|
|
110
|
+
<PermissionProvider
|
|
111
|
+
key={`${workspaceId}:${tenantId}:${currentUser?.id}:${apiGatewayUrl}`}
|
|
112
|
+
scopeId={workspaceId ?? null}
|
|
113
|
+
gateway="workspace"
|
|
114
|
+
>
|
|
115
|
+
<FreeTimeAccess />
|
|
116
|
+
</PermissionProvider>
|
|
109
117
|
</PageLayout>
|
|
110
118
|
);
|
|
111
119
|
}
|
|
112
|
-
function
|
|
120
|
+
function FreeTimeAccess() {
|
|
121
|
+
const access = useShipmentPermissions();
|
|
122
|
+
return <Workbench access={access} />;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function Workbench({ access }: { access: ReturnType<typeof useShipmentPermissions> }) {
|
|
113
126
|
const [containerId, setContainerId] = useState('');
|
|
114
127
|
const [cycleId, setCycleId] = useState('');
|
|
115
128
|
const [clockId, setClockId] = useState('');
|
|
@@ -131,41 +144,56 @@ function Workbench() {
|
|
|
131
144
|
const [extensionId, setExtensionId] = useState('');
|
|
132
145
|
const [decisionKind, setDecisionKind] = useState<'ACCEPTED' | 'REJECTED' | 'EXPIRED'>('ACCEPTED');
|
|
133
146
|
const mutations = useConditionAction();
|
|
134
|
-
const containers = useConditionQuery<FreeContainersQuery>(
|
|
135
|
-
|
|
136
|
-
|
|
147
|
+
const containers = useConditionQuery<FreeContainersQuery>(
|
|
148
|
+
FreeContainersDocument,
|
|
149
|
+
access.canRead
|
|
150
|
+
? {
|
|
151
|
+
pagination: { limit: pageSize, offset: containerOffset },
|
|
152
|
+
}
|
|
153
|
+
: null
|
|
154
|
+
);
|
|
137
155
|
const cycles = useConditionQuery<FreeCyclesQuery>(
|
|
138
156
|
FreeCyclesDocument,
|
|
139
|
-
|
|
157
|
+
access.canRead && containerId
|
|
158
|
+
? { containerId, pagination: { limit: pageSize, offset: cycleOffset } }
|
|
159
|
+
: null
|
|
140
160
|
);
|
|
141
161
|
const events = useConditionQuery<FreeEventsQuery>(
|
|
142
162
|
FreeEventsDocument,
|
|
143
|
-
|
|
163
|
+
access.canRead && cycleId
|
|
164
|
+
? { cycleId, pagination: { limit: pageSize, offset: eventOffset } }
|
|
165
|
+
: null,
|
|
144
166
|
mutations.revision
|
|
145
167
|
);
|
|
146
168
|
const clocks = useConditionQuery<FreeClocksQuery>(
|
|
147
169
|
FreeClocksDocument,
|
|
148
|
-
|
|
170
|
+
access.canRead && cycleId
|
|
171
|
+
? { cycleId, pagination: { limit: pageSize, offset: clockOffset } }
|
|
172
|
+
: null,
|
|
149
173
|
mutations.revision
|
|
150
174
|
);
|
|
151
175
|
const agreements = useConditionQuery<FreeAgreementsQuery>(
|
|
152
176
|
FreeAgreementsDocument,
|
|
153
|
-
|
|
177
|
+
access.canRead && clockId
|
|
178
|
+
? { clockId, pagination: { limit: pageSize, offset: agreementOffset } }
|
|
179
|
+
: null,
|
|
154
180
|
mutations.revision
|
|
155
181
|
);
|
|
156
182
|
const currentAgreement = useConditionQuery<FreeAgreementsQuery>(
|
|
157
183
|
FreeAgreementsDocument,
|
|
158
|
-
clockId ? { clockId, pagination: { limit: 1, offset: 0 } } : null,
|
|
184
|
+
access.canRead && clockId ? { clockId, pagination: { limit: 1, offset: 0 } } : null,
|
|
159
185
|
mutations.revision
|
|
160
186
|
);
|
|
161
187
|
const extensions = useConditionQuery<FreeExtensionsQuery>(
|
|
162
188
|
FreeExtensionsDocument,
|
|
163
|
-
|
|
189
|
+
access.canRead && clockId
|
|
190
|
+
? { clockId, pagination: { limit: pageSize, offset: extensionOffset } }
|
|
191
|
+
: null,
|
|
164
192
|
mutations.revision
|
|
165
193
|
);
|
|
166
194
|
const exposure = useConditionQuery<FreeExposureQuery>(
|
|
167
195
|
FreeExposureDocument,
|
|
168
|
-
clockId ? { clockId, asOf: evaluation } : null,
|
|
196
|
+
access.canRead && clockId ? { clockId, asOf: evaluation } : null,
|
|
169
197
|
mutations.revision
|
|
170
198
|
);
|
|
171
199
|
const clock = clocks.data?.mtwFreeTimeClockPage.items.find((item) => item.id === clockId);
|
|
@@ -192,6 +220,16 @@ function Workbench() {
|
|
|
192
220
|
if (!evidence.length) throw new Error('Select exact decision/request evidence versions.');
|
|
193
221
|
return evidence.map(({ fileId, versionId }) => ({ fileId, versionId }));
|
|
194
222
|
};
|
|
223
|
+
if (!access.canRead)
|
|
224
|
+
return (
|
|
225
|
+
<p role={access.failed ? 'alert' : 'status'}>
|
|
226
|
+
{access.loading
|
|
227
|
+
? 'Checking shipment permissions…'
|
|
228
|
+
: access.failed
|
|
229
|
+
? 'Shipment permissions are unavailable. Reload to retry.'
|
|
230
|
+
: 'Shipment read permission is required.'}
|
|
231
|
+
</p>
|
|
232
|
+
);
|
|
195
233
|
return (
|
|
196
234
|
<div className="space-y-6">
|
|
197
235
|
<p className="rounded-lg border p-4 text-sm">
|
|
@@ -370,7 +408,7 @@ function Workbench() {
|
|
|
370
408
|
</RecordState>
|
|
371
409
|
</section>
|
|
372
410
|
)}
|
|
373
|
-
{cycleId && (
|
|
411
|
+
{cycleId && access.canUpdate && (
|
|
374
412
|
<section className={card} aria-label="Record contractual change">
|
|
375
413
|
<h2 className="text-lg font-semibold">Record contractual change</h2>
|
|
376
414
|
<label className="block">
|
|
@@ -456,6 +494,8 @@ function Workbench() {
|
|
|
456
494
|
(action === 'correct' && currentAgreement.loading)
|
|
457
495
|
}
|
|
458
496
|
onSubmit={async (agreement) => {
|
|
497
|
+
if (!access.allows('update'))
|
|
498
|
+
throw new Error('Shipment update permission is required.');
|
|
459
499
|
if (action === 'open') {
|
|
460
500
|
if (!obligation.trim())
|
|
461
501
|
throw new Error('Enter a distinct commercial obligation reference.');
|
|
@@ -485,6 +525,8 @@ function Workbench() {
|
|
|
485
525
|
event.preventDefault();
|
|
486
526
|
setValidation(null);
|
|
487
527
|
try {
|
|
528
|
+
if (!access.allows('update'))
|
|
529
|
+
throw new Error('Shipment update permission is required.');
|
|
488
530
|
if (!clock || !current)
|
|
489
531
|
throw new Error('Reload the current obligation and agreement.');
|
|
490
532
|
const values = new FormData(event.currentTarget);
|