@burdenoff/microfe-movethewheels 2026.1012.2 → 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/useShipmentPermissions.js +28 -0
- package/dist/movethewheels/hooks/useContainerLifecycle.d.ts +1 -1
- package/dist/movethewheels/hooks/useShipmentPermissions.d.ts +9 -0
- package/dist/pages/ContainersPage.js +184 -168
- package/dist/pages/FreeTimePage.js +205 -191
- package/package.json +4 -3
- package/src/movethewheels/hooks/useContainerLifecycle.ts +10 -6
- package/src/movethewheels/hooks/useShipmentPermissions.ts +38 -0
- package/src/movethewheels/pages/ContainersPage.tsx +198 -159
- 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
|
|
@@ -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
|
+
}
|
|
@@ -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' && (
|