@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.
@@ -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 <ContainerWorkbench key={`${workspaceId ?? ''}:${currentUser?.id ?? ''}`} />;
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
- <form onSubmit={register} className="space-y-3 border-t pt-4">
171
- <h3 className="font-semibold">Register container</h3>
172
- <Label htmlFor="container-number">ISO container number</Label>
173
- <Input
174
- id="container-number"
175
- value={number}
176
- onChange={(event) => setNumber(event.target.value)}
177
- required
178
- maxLength={20}
179
- placeholder="Four letters and seven digits"
180
- />
181
- <Label htmlFor="equipment-type">Physical equipment type</Label>
182
- <select
183
- id="equipment-type"
184
- className={selectClass}
185
- value={equipmentType}
186
- onChange={(event) =>
187
- setEquipmentType(
188
- EQUIPMENT_TYPES.find((value) => value === event.target.value) ?? 'OTHER'
189
- )
190
- }
191
- >
192
- {EQUIPMENT_TYPES.map((value) => (
193
- <option key={value} value={value}>
194
- {readable(value)}
195
- </option>
196
- ))}
197
- </select>
198
- <Label htmlFor="container-owner">Owner (optional)</Label>
199
- <Input
200
- id="container-owner"
201
- value={ownerName}
202
- onChange={(event) => setOwnerName(event.target.value)}
203
- maxLength={200}
204
- />
205
- <Label htmlFor="container-source">Information source</Label>
206
- <Input
207
- id="container-source"
208
- value={source}
209
- onChange={(event) => setSource(event.target.value)}
210
- required
211
- maxLength={120}
212
- placeholder="Equipment release document or carrier portal"
213
- />
214
- <Button type="submit" disabled={busy || !number.trim() || !source.trim()}>
215
- Register container
216
- </Button>
217
- </form>
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
- <form onSubmit={record} className="space-y-3 border-t pt-4">
349
- <h3 className="font-semibold">Record event</h3>
350
- <Label htmlFor="container-event">Event</Label>
351
- <select
352
- id="container-event"
353
- className={selectClass}
354
- value={eventType}
355
- onChange={(event) =>
356
- setEventType(
357
- CONTAINER_EVENT_TYPES.find((value) => value === event.target.value) ??
358
- 'POSITION_UPDATE'
359
- )
360
- }
361
- >
362
- {CONTAINER_EVENT_TYPES.map((value) => (
363
- <option key={value} value={value}>
364
- {readable(value)}
365
- </option>
366
- ))}
367
- </select>
368
- <Label htmlFor="event-kind">Event certainty</Label>
369
- <select
370
- id="event-kind"
371
- className={selectClass}
372
- value={kind}
373
- onChange={(event) =>
374
- setKind(
375
- event.target.value === 'PLANNED'
376
- ? 'PLANNED'
377
- : event.target.value === 'ESTIMATED'
378
- ? 'ESTIMATED'
379
- : 'ACTUAL'
380
- )
381
- }
382
- >
383
- <option value="ACTUAL">Actual</option>
384
- <option value="PLANNED">Planned</option>
385
- <option value="ESTIMATED">Estimated</option>
386
- </select>
387
- <Label htmlFor="event-time">Event time (your local timezone)</Label>
388
- <Input
389
- id="event-time"
390
- type="datetime-local"
391
- step={1}
392
- value={eventTime}
393
- onChange={(event) => setEventTime(event.target.value)}
394
- required
395
- />
396
- <Label htmlFor="event-source">Event source</Label>
397
- <Input
398
- id="event-source"
399
- value={eventSource}
400
- onChange={(event) => setEventSource(event.target.value)}
401
- required
402
- maxLength={120}
403
- />
404
- <Label htmlFor="source-reference">Source reference (optional)</Label>
405
- <Input
406
- id="source-reference"
407
- value={sourceReference}
408
- onChange={(event) => setSourceReference(event.target.value)}
409
- maxLength={200}
410
- />
411
- <Label htmlFor="event-location">Location (optional)</Label>
412
- <Input
413
- id="event-location"
414
- value={location}
415
- onChange={(event) => setLocation(event.target.value)}
416
- maxLength={200}
417
- />
418
- <Label htmlFor="event-evidence">
419
- Evidence note
420
- {eventType === 'RETURN_ACCEPTED' && kind === 'ACTUAL'
421
- ? ' (required for acceptance)'
422
- : ' (optional)'}
423
- </Label>
424
- <Input
425
- id="event-evidence"
426
- value={evidence}
427
- onChange={(event) => setEvidence(event.target.value)}
428
- maxLength={2000}
429
- required={eventType === 'RETURN_ACCEPTED' && kind === 'ACTUAL'}
430
- />
431
- <Button type="submit" disabled={busy || !eventSource.trim()}>
432
- Record event
433
- </Button>
434
- </form>
435
- <div className="space-y-3 border-t pt-4">
436
- <p className="text-sm text-text-secondary">
437
- Closure checks the complete recorded history for actual empty return and
438
- acceptance. A planned return or delivered shipment is insufficient.
439
- </p>
440
- <Button
441
- variant="outline"
442
- disabled={busy || data.cyclesLoading || data.eventsLoading}
443
- onClick={() =>
444
- void act(
445
- () => data.closeCycle(cycle),
446
- 'Cycle closed after accepted empty return.'
447
- )
448
- }
449
- >
450
- Close cycle
451
- </Button>
452
- </div>
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
- onCaptured();
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 } = useMyPendingEpodDeliveryJobs({
362
- limit: pageSize,
363
- offset: pageIndex * pageSize,
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
- <Workbench key={`${workspaceId}:${tenantId}:${currentUser?.id}:${apiGatewayUrl}`} />
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 Workbench() {
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>(FreeContainersDocument, {
135
- pagination: { limit: pageSize, offset: containerOffset },
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
- containerId ? { containerId, pagination: { limit: pageSize, offset: cycleOffset } } : null
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
- cycleId ? { cycleId, pagination: { limit: pageSize, offset: eventOffset } } : null,
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
- cycleId ? { cycleId, pagination: { limit: pageSize, offset: clockOffset } } : null,
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
- clockId ? { clockId, pagination: { limit: pageSize, offset: agreementOffset } } : null,
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
- clockId ? { clockId, pagination: { limit: pageSize, offset: extensionOffset } } : null,
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);