@burdenoff/microfe-movethewheels 2026.1014.1 → 2026.1016.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.
@@ -0,0 +1,326 @@
1
+ import { useRef, useState } from 'react';
2
+ import { PermissionProvider, usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';
3
+ import {
4
+ ApprovePickReviewDocument,
5
+ RejectPickReviewDocument,
6
+ RevokePickReviewDocument,
7
+ PickReviewHistoryDocument,
8
+ type PickReviewHistoryQuery,
9
+ type PendingPickTaskFragment,
10
+ type RecordMtwPickReviewInput,
11
+ type MtwPickReviewAction,
12
+ } from '../../../generated/pending-pick-operations';
13
+ import { confirmPickReviewAcknowledgement } from '../../utils/pickReviewAcknowledgement';
14
+ import { useMoveTheWheels } from '../../providers/MoveTheWheelsProvider';
15
+ import { useConditionApi, useConditionQuery } from '../../hooks/useContainerCondition';
16
+ import { ContainerEvidencePicker, type SelectedEvidence } from '../ContainerEvidencePicker';
17
+ import { ReuseField, ReusePager, ReuseState, reuseCard } from '../ReuseControls';
18
+ import { Button } from '../ui';
19
+ export function PickReviewPanel({ task }: { task: PendingPickTaskFragment }) {
20
+ const { workspaceId, currentUser, apiGatewayUrl } = useMoveTheWheels();
21
+ return (
22
+ <PermissionProvider scopeId={workspaceId ?? null} gateway="workspace">
23
+ <MoveReview
24
+ key={JSON.stringify([
25
+ workspaceId,
26
+ currentUser?.id,
27
+ apiGatewayUrl,
28
+ task.id,
29
+ task.intentHash,
30
+ ])}
31
+ task={task}
32
+ />
33
+ </PermissionProvider>
34
+ );
35
+ }
36
+ function MoveReview({ task }: { task: PendingPickTaskFragment }) {
37
+ const permissions = usePermissions(),
38
+ { currentUser, workspaceId } = useMoveTheWheels(),
39
+ request = useConditionApi('SINGLE_ATTEMPT');
40
+ const storageKey = JSON.stringify([
41
+ 'mtw-review-intent',
42
+ workspaceId,
43
+ currentUser?.id,
44
+ task.id,
45
+ task.intentHash,
46
+ ]);
47
+ const [attemptKey, setAttemptKey] = useState<string | null>(() => {
48
+ try {
49
+ const value = sessionStorage.getItem(storageKey);
50
+ return value === null ||
51
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value)
52
+ ? value
53
+ : 'UNAVAILABLE';
54
+ } catch {
55
+ return 'UNAVAILABLE';
56
+ }
57
+ });
58
+ const uncertain = attemptKey !== null;
59
+ const [offset, setOffset] = useState(0),
60
+ [revision, setRevision] = useState(0),
61
+ [evidence, setEvidence] = useState<SelectedEvidence[]>([]),
62
+ [busy, setBusy] = useState(false),
63
+ [message, setMessage] = useState<string | null>(null);
64
+ const running = useRef(false);
65
+ const history = useConditionQuery<PickReviewHistoryQuery>(
66
+ PickReviewHistoryDocument,
67
+ permissions.hasPermission('mtw-move-review:list')
68
+ ? { taskId: task.id, limit: 10, offset }
69
+ : null,
70
+ revision
71
+ );
72
+ const page = history.data?.mtwPickReviewPage,
73
+ head = page?.latestDecision;
74
+ const independent =
75
+ currentUser?.id && currentUser.id !== task.createdBy && currentUser.id !== task.executorActorId;
76
+ const decision = async (form: HTMLFormElement, kind: 'APPROVE' | 'REJECT' | 'REVOKE') => {
77
+ if (
78
+ running.current ||
79
+ uncertain ||
80
+ !page ||
81
+ !independent ||
82
+ !permissions.hasPermission(`mtw-move-review:${kind.toLowerCase()}`)
83
+ )
84
+ return;
85
+ const data = new FormData(form),
86
+ field = (name: string) => String(data.get(name) ?? '').trim();
87
+ let input: RecordMtwPickReviewInput;
88
+ try {
89
+ if (
90
+ kind === 'APPROVE' &&
91
+ (!/(Z|[+-]\d{2}:\d{2})$/.test(field('validFrom')) ||
92
+ !/(Z|[+-]\d{2}:\d{2})$/.test(field('validUntil')))
93
+ )
94
+ throw new Error('Use explicit ISO timezone offsets for both validity bounds.');
95
+ const validFrom = kind === 'APPROVE' ? new Date(field('validFrom')).toISOString() : null;
96
+ const validUntil = kind === 'APPROVE' ? new Date(field('validUntil')).toISOString() : null;
97
+ const quantity = kind === 'APPROVE' ? Number(field('quantity')) : null;
98
+ const allowed = [
99
+ 'RESERVE',
100
+ 'STAGE',
101
+ 'RELEASE_UNPICKED',
102
+ 'RELEASE_STAGED_CUSTODY',
103
+ 'SHORT_PICK',
104
+ ] as const;
105
+ const actions: MtwPickReviewAction[] =
106
+ kind === 'APPROVE' ? allowed.filter((action) => data.has(action)) : [];
107
+ if (
108
+ !field('note') ||
109
+ (kind === 'APPROVE' &&
110
+ (!actions.length ||
111
+ !Number.isInteger(quantity) ||
112
+ quantity === null ||
113
+ quantity < 1 ||
114
+ quantity > task.requestedQuantity ||
115
+ !evidence.length ||
116
+ !field('handlingReference') ||
117
+ !field('handlingRevision') ||
118
+ !field('sopReference') ||
119
+ !field('sopRevision')))
120
+ )
121
+ throw new Error('Review all required fields and select exact evidence.');
122
+ input = {
123
+ taskId: task.id,
124
+ intentHash: task.intentHash,
125
+ subjectActorId: task.executorActorId,
126
+ expectedPreviousDecisionId: head?.id ?? null,
127
+ expectedVersion: head?.version ?? 0,
128
+ actions,
129
+ quantityCeiling: quantity,
130
+ validFrom,
131
+ validUntil,
132
+ handlingPolicy:
133
+ kind === 'APPROVE'
134
+ ? { reference: field('handlingReference'), revision: field('handlingRevision') }
135
+ : null,
136
+ sop:
137
+ kind === 'APPROVE'
138
+ ? { reference: field('sopReference'), revision: field('sopRevision') }
139
+ : null,
140
+ evidence:
141
+ kind === 'APPROVE'
142
+ ? evidence.map(({ fileId, versionId }) => ({ fileId, versionId }))
143
+ : [],
144
+ note: field('note'),
145
+ idempotencyKey: crypto.randomUUID(),
146
+ };
147
+ } catch (failure) {
148
+ setMessage(failure instanceof Error ? failure.message : 'Check the review fields.');
149
+ return;
150
+ }
151
+ // Store the nonsecret request key before dispatch; refresh must never authorize resubmission.
152
+ try {
153
+ sessionStorage.setItem(storageKey, input.idempotencyKey);
154
+ setAttemptKey(input.idempotencyKey);
155
+ } catch {
156
+ setAttemptKey('UNAVAILABLE');
157
+ setMessage('The intended request key could not be preserved. No decision was submitted.');
158
+ return;
159
+ }
160
+ running.current = true;
161
+ setBusy(true);
162
+ setMessage(null);
163
+ try {
164
+ const document =
165
+ kind === 'APPROVE'
166
+ ? ApprovePickReviewDocument
167
+ : kind === 'REJECT'
168
+ ? RejectPickReviewDocument
169
+ : RevokePickReviewDocument;
170
+ const result = await request<unknown>(document, { input });
171
+ confirmPickReviewAcknowledgement(result, input, kind, currentUser?.id ?? '');
172
+ sessionStorage.removeItem(storageKey);
173
+ setAttemptKey(null);
174
+ setRevision((value) => value + 1);
175
+ setMessage('Review recorded. Stock is unchanged.');
176
+ } catch {
177
+ setMessage(
178
+ 'The outcome is not confirmed. Preserve the intended request key and match it against review history; do not resend this decision.'
179
+ );
180
+ } finally {
181
+ running.current = false;
182
+ setBusy(false);
183
+ }
184
+ };
185
+ return (
186
+ <section className={reuseCard} aria-label="Independent move review">
187
+ <h4 className="font-semibold">Independent move review</h4>
188
+ <p>
189
+ Review only. No stock reservation, movement, qualification or physical safety certification.
190
+ Current stock and permissions must be checked again before any future execution.
191
+ </p>
192
+ <p className="break-all">
193
+ Exact intent SHA-256: {task.intentHash}; fixed executor: {task.executorActorId}
194
+ </p>
195
+ {permissions.isLoading && <p role="status">Checking review permissions…</p>}
196
+ {permissions.error && <p role="alert">Review permissions are unavailable.</p>}
197
+ {!permissions.isLoading && !permissions.hasPermission('mtw-move-review:list') && (
198
+ <p>You do not have permission to list move reviews.</p>
199
+ )}
200
+ <ReuseState loading={history.loading} error={history.error} />
201
+ {page && (
202
+ <>
203
+ <p>
204
+ Latest review: {page.reviewState}, evaluated {page.evaluatedAt}. Older approvals never
205
+ override a later rejection or revocation.
206
+ </p>
207
+ {page.items.map((item) => (
208
+ <article key={item.id} className="min-w-0 break-words rounded border p-3">
209
+ <p>
210
+ Version {item.version}: {item.kind} · {item.note}
211
+ </p>
212
+ <p className="break-all">
213
+ Reviewer {item.reviewerActorId}; receipt {item.id}; request key{' '}
214
+ {item.idempotencyKey}; intent {item.intentHash}
215
+ </p>
216
+ <p>
217
+ {item.actions.join(', ') || 'No actions granted'}; ceiling{' '}
218
+ {item.quantityCeiling ?? 'none'}; validity {item.validFrom ?? 'none'} to{' '}
219
+ {item.validUntil ?? 'none'} (exclusive).
220
+ </p>
221
+ <p>
222
+ Handling source{' '}
223
+ {item.handlingPolicy
224
+ ? `${item.handlingPolicy.reference} · revision ${item.handlingPolicy.revision}`
225
+ : 'none'}
226
+ ; SOP {item.sop ? `${item.sop.reference} · revision ${item.sop.revision}` : 'none'}.
227
+ </p>
228
+ {item.evidence.map((file) => (
229
+ <p key={file.versionId} className="break-all">
230
+ Evidence {file.fileId}, version {file.versionId}, SHA-256 {file.sha256Hash};
231
+ checked {file.checkedAt}.
232
+ </p>
233
+ ))}
234
+ </article>
235
+ ))}
236
+ <ReusePager
237
+ page={page}
238
+ offset={offset}
239
+ onChange={setOffset}
240
+ busy={busy || history.loading}
241
+ />
242
+ </>
243
+ )}
244
+ {!independent && <p>The creator and fixed executor cannot review this task.</p>}
245
+ {independent && page && (
246
+ <form onSubmit={(event) => event.preventDefault()}>
247
+ <fieldset disabled={busy || uncertain} className="space-y-3">
248
+ <legend>Record a decision on the exact intent</legend>
249
+ <ReuseField label="Decision note" name="note" maxLength={1000} />
250
+ {permissions.hasPermission('mtw-move-review:approve') && (
251
+ <details>
252
+ <summary>Approval scope and sourced handling review</summary>
253
+ <ReuseField
254
+ label={`Quantity ceiling (1–${task.requestedQuantity})`}
255
+ name="quantity"
256
+ type="number"
257
+ />
258
+ <ReuseField label="Valid from (ISO with timezone)" name="validFrom" />
259
+ <ReuseField label="Valid until (exclusive, ISO with timezone)" name="validUntil" />
260
+ {(
261
+ [
262
+ 'RESERVE',
263
+ 'STAGE',
264
+ 'RELEASE_UNPICKED',
265
+ 'RELEASE_STAGED_CUSTODY',
266
+ 'SHORT_PICK',
267
+ ] as const
268
+ ).map((action) => (
269
+ <label key={action} className="block">
270
+ <input type="checkbox" name={action} /> {action}
271
+ </label>
272
+ ))}
273
+ <ReuseField label="Handling policy reference" name="handlingReference" />
274
+ <ReuseField label="Handling policy revision" name="handlingRevision" />
275
+ <ReuseField label="SOP reference" name="sopReference" />
276
+ <ReuseField label="SOP revision" name="sopRevision" />
277
+ <ContainerEvidencePicker
278
+ value={evidence}
279
+ onChange={setEvidence}
280
+ description="Select readable exact Files versions supporting this handling/SOP review. References alone do not certify physical facts."
281
+ />
282
+ </details>
283
+ )}
284
+ <div className="flex flex-wrap gap-2">
285
+ {(['APPROVE', 'REJECT', 'REVOKE'] as const)
286
+ .filter((kind) =>
287
+ permissions.hasPermission(`mtw-move-review:${kind.toLowerCase()}`)
288
+ )
289
+ .map((kind) => (
290
+ <Button
291
+ type="button"
292
+ key={kind}
293
+ disabled={busy || uncertain || (kind === 'REVOKE' && !head)}
294
+ onClick={(event) => {
295
+ const form = event.currentTarget.form;
296
+ if (form) void decision(form, kind);
297
+ }}
298
+ >
299
+ {kind.toLowerCase()}
300
+ </Button>
301
+ ))}
302
+ </div>
303
+ </fieldset>
304
+ </form>
305
+ )}
306
+ <Button
307
+ type="button"
308
+ variant="outline"
309
+ disabled={busy}
310
+ onClick={() => setRevision((value) => value + 1)}
311
+ >
312
+ Refresh review history
313
+ </Button>
314
+ {uncertain && !busy && (
315
+ <p role="alert">
316
+ Submission remains stopped. Intended request key: {attemptKey}. This tab preserves it
317
+ across refresh for this actor, workspace and exact task. Match it to the read-only
318
+ history; an absent page result does not prove no commit. Do not resend or clear browser
319
+ storage to retry. If the key is unavailable, obtain read-only reconciliation before any
320
+ new decision.
321
+ </p>
322
+ )}
323
+ {message && <p role="status">{message}</p>}
324
+ </section>
325
+ );
326
+ }
@@ -0,0 +1,51 @@
1
+ import type {
2
+ PickReviewReceiptFragment,
3
+ RecordMtwPickReviewInput,
4
+ } from '../../generated/pending-pick-operations';
5
+
6
+ function record(value: unknown): value is Record<string, unknown> {
7
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
8
+ }
9
+
10
+ /** A truthy GraphQL data object is not an acknowledgment of the intended write. */
11
+ export function confirmPickReviewAcknowledgement(
12
+ result: unknown,
13
+ input: RecordMtwPickReviewInput,
14
+ kind: PickReviewReceiptFragment['kind'],
15
+ reviewerActorId: string
16
+ ): void {
17
+ const field =
18
+ kind === 'APPROVE'
19
+ ? 'approveMtwPickTask'
20
+ : kind === 'REJECT'
21
+ ? 'rejectMtwPickTask'
22
+ : 'revokeMtwPickTaskReview';
23
+ const receipt = record(result) ? result[field] : null;
24
+ if (
25
+ !record(receipt) ||
26
+ typeof receipt.id !== 'string' ||
27
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(receipt.id) ||
28
+ receipt.kind !== kind ||
29
+ receipt.taskId !== input.taskId ||
30
+ receipt.intentHash !== input.intentHash ||
31
+ receipt.subjectActorId !== input.subjectActorId ||
32
+ receipt.idempotencyKey !== input.idempotencyKey ||
33
+ !reviewerActorId ||
34
+ receipt.reviewerActorId !== reviewerActorId ||
35
+ receipt.version !== input.expectedVersion + 1 ||
36
+ receipt.previousDecisionId !== (input.expectedPreviousDecisionId ?? null) ||
37
+ receipt.classification !== 'REVIEW_ONLY_NO_STOCK_EFFECT' ||
38
+ receipt.note !== input.note ||
39
+ receipt.quantityCeiling !== (input.quantityCeiling ?? null) ||
40
+ receipt.validFrom !== (input.validFrom ?? null) ||
41
+ receipt.validUntil !== (input.validUntil ?? null) ||
42
+ !Array.isArray(receipt.actions) ||
43
+ receipt.actions.length !== input.actions.length ||
44
+ new Set(receipt.actions).size !== receipt.actions.length ||
45
+ !receipt.actions.every(
46
+ (action: unknown) =>
47
+ typeof action === 'string' && input.actions.some((expected) => expected === action)
48
+ )
49
+ )
50
+ throw new Error('The intended move-review receipt is not confirmed.');
51
+ }
@@ -0,0 +1,14 @@
1
+ fragment PickReviewReceipt on MtwPickReview {
2
+ id taskId version previousDecisionId intentHash subjectActorId kind actions quantityCeiling validFrom validUntil
3
+ handlingPolicy { reference revision } sop { reference revision }
4
+ evidence { fileId versionId sha256Hash checkedAt } note idempotencyKey reviewerActorId recordedAt classification
5
+ }
6
+ query PickReviewHistory($taskId: ID!, $limit: Int!, $offset: Int!) {
7
+ mtwPickReviewPage(taskId:$taskId,limit:$limit,offset:$offset) {
8
+ items { ...PickReviewReceipt } total limit offset hasMore nextOffset
9
+ latestDecision { ...PickReviewReceipt } reviewState evaluatedAt
10
+ }
11
+ }
12
+ mutation ApprovePickReview($input:RecordMtwPickReviewInput!) { approveMtwPickTask(input:$input) { ...PickReviewReceipt } }
13
+ mutation RejectPickReview($input:RecordMtwPickReviewInput!) { rejectMtwPickTask(input:$input) { ...PickReviewReceipt } }
14
+ mutation RevokePickReview($input:RecordMtwPickReviewInput!) { revokeMtwPickTaskReview(input:$input) { ...PickReviewReceipt } }
@@ -0,0 +1,58 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { EntityCombobox } from '../movethewheels/components/EntityCombobox';
4
+
5
+ const choices = [
6
+ { id: 'fictional-alpha', label: 'Fictional Alpha' },
7
+ { id: 'fictional-beta', label: 'Fictional Beta' },
8
+ ];
9
+
10
+ function PairedPickers() {
11
+ const [external, setExternal] = useState('');
12
+ const [internal, setInternal] = useState('');
13
+ const [search, setSearch] = useState('');
14
+ return (
15
+ <div className="w-80 space-y-6 p-4">
16
+ <p>Synthetic component examples; selections remain local to this story.</p>
17
+ <section>
18
+ <label htmlFor="fictional-external-picker">Fictional external picker</label>
19
+ <EntityCombobox
20
+ id="fictional-external-picker"
21
+ value={external}
22
+ onChange={setExternal}
23
+ options={choices}
24
+ selectedOption={choices.find((item) => item.id === external)}
25
+ getId={(item) => item.id}
26
+ getLabel={(item) => item.label}
27
+ renderOption={(item) => item.label}
28
+ />
29
+ <output aria-label="External selection">{external || 'None'}</output>
30
+ </section>
31
+ <section>
32
+ <label htmlFor="fictional-internal-picker">Fictional internal picker</label>
33
+ <EntityCombobox
34
+ id="fictional-internal-picker"
35
+ value={internal}
36
+ onChange={setInternal}
37
+ options={choices.filter((item) =>
38
+ item.label.toLowerCase().includes(search.toLowerCase())
39
+ )}
40
+ onSearchChange={setSearch}
41
+ selectedOption={choices.find((item) => item.id === internal)}
42
+ getId={(item) => item.id}
43
+ getLabel={(item) => item.label}
44
+ renderOption={(item) => item.label}
45
+ />
46
+ <output aria-label="Internal selection">{internal || 'None'}</output>
47
+ </section>
48
+ </div>
49
+ );
50
+ }
51
+
52
+ const meta: Meta<typeof PairedPickers> = {
53
+ title: 'MoveTheWheels/EntityCombobox',
54
+ component: PairedPickers,
55
+ };
56
+ export default meta;
57
+ type Story = StoryObj<typeof meta>;
58
+ export const TwoInstances: Story = {};