@burdenoff/microfe-movethewheels 2026.1016.3 → 2026.1016.4

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.
Files changed (88) hide show
  1. package/README.md +10 -0
  2. package/dist/MoveTheWheelsRoot.js +5 -0
  3. package/dist/MoveTheWheelsRoutes.js +25 -21
  4. package/dist/components/ContainerEvidencePicker.js +55 -55
  5. package/dist/components/FleetAlertsPanel.js +2 -2
  6. package/dist/components/freight-invoice/InvoiceForm.js +366 -0
  7. package/dist/components/freight-invoice/InvoiceResults.js +240 -0
  8. package/dist/generated/consolidation-operations.d.ts +195 -265
  9. package/dist/generated/container-condition-operations.d.ts +124 -187
  10. package/dist/generated/container-operations.d.ts +59 -85
  11. package/dist/generated/empty-reuse-operations.d.ts +165 -271
  12. package/dist/generated/free-time-operations.d.ts +120 -184
  13. package/dist/generated/freight-cost-operations.d.ts +307 -476
  14. package/dist/generated/freight-invoice-operations.d.ts +1447 -0
  15. package/dist/generated/logistics-procedure-operations.d.ts +100 -156
  16. package/dist/generated/pending-pick-operations.d.ts +769 -1025
  17. package/dist/generated/shipment-status-authority-operations.d.ts +82 -103
  18. package/dist/generated/stock-binding-operations.d.ts +188 -260
  19. package/dist/generated/stock-observation-operations.d.ts +103 -138
  20. package/dist/generated/tracking-quality-operations.d.ts +27 -41
  21. package/dist/generated/trip-utilization-operations.d.ts +137 -185
  22. package/dist/generated/wspace-operations.d.ts +628 -2272
  23. package/dist/generated/wspace-types.d.ts +4 -23
  24. package/dist/hooks/useInvoiceComparisonRecovery.js +118 -0
  25. package/dist/movethewheels/components/ContainerEvidencePicker.d.ts +2 -1
  26. package/dist/movethewheels/components/freight-invoice/InvoiceForm.d.ts +19 -0
  27. package/dist/movethewheels/components/freight-invoice/InvoiceResults.d.ts +9 -0
  28. package/dist/movethewheels/hooks/useFleet.d.ts +1 -1
  29. package/dist/movethewheels/hooks/useFleetIntelligence.d.ts +1 -1
  30. package/dist/movethewheels/hooks/useInvoiceComparisonRecovery.d.ts +22 -0
  31. package/dist/movethewheels/hooks/useProcedureSelection.d.ts +1 -3
  32. package/dist/movethewheels/pages/FreightInvoicePage.d.ts +10 -0
  33. package/dist/movethewheels/utils/driverLicenseExpiry.d.ts +6 -0
  34. package/dist/movethewheels/utils/invoiceComparison.d.ts +32 -0
  35. package/dist/pages/DriverDetailsPage.js +306 -282
  36. package/dist/pages/FreightEstimatePage.js +1 -0
  37. package/dist/pages/FreightInvoicePage.js +346 -0
  38. package/dist/src/generated/consolidation-operations.js +3961 -503
  39. package/dist/src/generated/container-condition-operations.js +2968 -206
  40. package/dist/src/generated/container-operations.js +997 -99
  41. package/dist/src/generated/empty-reuse-operations.js +4811 -747
  42. package/dist/src/generated/free-time-operations.js +2824 -233
  43. package/dist/src/generated/freight-cost-operations.js +4852 -812
  44. package/dist/src/generated/freight-invoice-operations.js +4894 -0
  45. package/dist/src/generated/logistics-procedure-operations.js +2707 -360
  46. package/dist/src/generated/pending-pick-operations.js +8244 -1196
  47. package/dist/src/generated/shipment-status-authority-operations.js +1157 -84
  48. package/dist/src/generated/stock-binding-operations.js +2779 -173
  49. package/dist/src/generated/stock-observation-operations.js +1274 -91
  50. package/dist/src/generated/tracking-quality-operations.js +407 -42
  51. package/dist/src/generated/trip-utilization-operations.js +1765 -138
  52. package/dist/src/generated/wspace-operations.js +428 -706
  53. package/dist/utils/driverLicenseExpiry.js +14 -0
  54. package/dist/utils/invoiceComparison.js +272 -0
  55. package/package.json +29 -29
  56. package/src/generated/consolidation-operations.ts +60 -559
  57. package/src/generated/container-condition-operations.ts +63 -265
  58. package/src/generated/container-operations.ts +37 -132
  59. package/src/generated/empty-reuse-operations.ts +83 -824
  60. package/src/generated/free-time-operations.ts +55 -284
  61. package/src/generated/freight-cost-operations.ts +42 -848
  62. package/src/generated/freight-invoice-operations.ts +830 -0
  63. package/src/generated/logistics-procedure-operations.ts +60 -414
  64. package/src/generated/pending-pick-operations.ts +48 -1239
  65. package/src/generated/shipment-status-authority-operations.ts +33 -113
  66. package/src/generated/stock-binding-operations.ts +29 -198
  67. package/src/generated/stock-observation-operations.ts +24 -111
  68. package/src/generated/tracking-quality-operations.ts +19 -57
  69. package/src/generated/trip-utilization-operations.ts +32 -166
  70. package/src/generated/wspace-operations.ts +284 -2906
  71. package/src/generated/wspace-types.ts +2 -8
  72. package/src/movethewheels/MoveTheWheelsRoot.tsx +6 -0
  73. package/src/movethewheels/MoveTheWheelsRoutes.tsx +2 -0
  74. package/src/movethewheels/components/ContainerEvidencePicker.tsx +12 -4
  75. package/src/movethewheels/components/FleetAlertsPanel.tsx +4 -2
  76. package/src/movethewheels/components/freight-invoice/InvoiceForm.tsx +412 -0
  77. package/src/movethewheels/components/freight-invoice/InvoiceResults.tsx +162 -0
  78. package/src/movethewheels/hooks/useFleet.ts +1 -1
  79. package/src/movethewheels/hooks/useFleetIntelligence.ts +5 -1
  80. package/src/movethewheels/hooks/useInvoiceComparisonRecovery.ts +227 -0
  81. package/src/movethewheels/pages/DriverDetailsPage.tsx +35 -9
  82. package/src/movethewheels/pages/FreightEstimatePage.tsx +1 -0
  83. package/src/movethewheels/pages/FreightInvoicePage.tsx +477 -0
  84. package/src/movethewheels/utils/driverLicenseExpiry.ts +25 -0
  85. package/src/movethewheels/utils/invoiceComparison.ts +520 -0
  86. package/src/operations/freight-invoice/comparison.graphql +40 -0
  87. package/src/operations/wspace/freight-invoice-evidence.graphql +6 -0
  88. package/patches/braces@3.0.3.patch +0 -87
@@ -0,0 +1,477 @@
1
+ import { useState } from 'react';
2
+ import { useSearchParams } from 'react-router';
3
+ import { FileText } from 'lucide-react';
4
+ import { PermissionProvider } from '@burdenoff/fe-libs/shared/providers/shell';
5
+ import { PageLayout } from '../components/PageLayout';
6
+ import { Button } from '../components/ui';
7
+ import { PinnedSelect, ReusePager, ReuseState, reuseCard } from '../components/ReuseControls';
8
+ import { FreightText } from '../components/freight-cost/FreightFields';
9
+ import { InvoiceForm } from '../components/freight-invoice/InvoiceForm';
10
+ import { InvoiceHistoryRecord } from '../components/freight-invoice/InvoiceResults';
11
+ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
12
+ import { useShipmentPermissions } from '../hooks/useShipmentPermissions';
13
+ import { useConditionApi, useConditionQuery } from '../hooks/useContainerCondition';
14
+ import {
15
+ useInvoiceComparisonRecovery,
16
+ useInvoiceComparisonScope,
17
+ } from '../hooks/useInvoiceComparisonRecovery';
18
+ import {
19
+ InvoiceShipmentsDocument,
20
+ InvoiceEstimatePageDocument,
21
+ InvoiceEstimateVersionDocument,
22
+ InvoiceComparisonHistoryDocument,
23
+ InvoiceComparisonExactDocument,
24
+ InvoiceStructuralPreviewDocument,
25
+ type InvoiceShipmentsQuery,
26
+ type InvoiceEstimatePageQuery,
27
+ type InvoiceEstimateVersionQuery,
28
+ type InvoiceComparisonHistoryQuery,
29
+ type InvoiceComparisonExactQuery,
30
+ type InvoiceReceiptFragment,
31
+ type InvoiceStructuralPreviewQuery,
32
+ } from '../../generated/freight-invoice-operations';
33
+ import {
34
+ sameInvoiceScope,
35
+ type InvoiceIntent,
36
+ type InvoiceScope,
37
+ } from '../utils/invoiceComparison';
38
+
39
+ export default function FreightInvoicePage() {
40
+ const owner = useMoveTheWheels();
41
+ return (
42
+ <PageLayout
43
+ title="Freight invoice comparisons"
44
+ subtitle="Compare manually reported source facts with one immutable estimate version."
45
+ icon={<FileText aria-hidden="true" />}
46
+ >
47
+ <PermissionProvider
48
+ key={`${owner.workspaceId}:${owner.tenantId}:${owner.currentUser?.id}:${owner.apiGatewayUrl}`}
49
+ scopeId={owner.workspaceId ?? null}
50
+ gateway="workspace"
51
+ >
52
+ <InvoiceAccess />
53
+ </PermissionProvider>
54
+ </PageLayout>
55
+ );
56
+ }
57
+ function InvoiceAccess() {
58
+ const access = useShipmentPermissions(),
59
+ scope = useInvoiceComparisonScope(access.canRead),
60
+ actions = useInvoiceComparisonRecovery(scope, () => access.allows('update'));
61
+ return (
62
+ <>
63
+ {!access.canRead && (
64
+ <p role="status">
65
+ {access.loading
66
+ ? 'Checking shipment read permission…'
67
+ : access.failed
68
+ ? 'Shipment permissions are unavailable. Reload to inspect them.'
69
+ : 'Shipment read permission in the current workspace is required.'}
70
+ </p>
71
+ )}
72
+ <InvoiceWorkbench key={actions.scopeKey} access={access} scope={scope} actions={actions} />
73
+ </>
74
+ );
75
+ }
76
+ function InvoiceWorkbench({
77
+ access,
78
+ scope,
79
+ actions,
80
+ }: {
81
+ access: ReturnType<typeof useShipmentPermissions>;
82
+ scope: ReturnType<typeof useInvoiceComparisonScope>;
83
+ actions: ReturnType<typeof useInvoiceComparisonRecovery>;
84
+ }) {
85
+ const request = useConditionApi(),
86
+ [params, setParams] = useSearchParams();
87
+ const [shipment, setShipment] = useState(
88
+ () => params.get('invoiceShipment')?.slice(0, 255) ?? ''
89
+ ),
90
+ [shipmentOffset, setShipmentOffset] = useState(0),
91
+ [estimateOffset, setEstimateOffset] = useState(0);
92
+ const [estimateId, setEstimateId] = useState(''),
93
+ [started, setStarted] = useState(false),
94
+ [correction, setCorrection] = useState<InvoiceReceiptFragment | null>(null);
95
+ const [identity, setIdentity] = useState(
96
+ () => params.get('invoiceIdentity')?.slice(0, 255) ?? ''
97
+ ),
98
+ [identityEntry, setIdentityEntry] = useState(identity),
99
+ [historyOffset, setHistoryOffset] = useState(0),
100
+ [selectedId, setSelectedId] = useState('');
101
+ const [known, setKnown] = useState<InvoiceReceiptFragment | null>(null),
102
+ [error, setError] = useState<string | null>(null);
103
+ const enabled = !!scope && access.canRead,
104
+ paused = actions.busy || actions.uncertain;
105
+ const shipments = useConditionQuery<InvoiceShipmentsQuery>(
106
+ InvoiceShipmentsDocument,
107
+ enabled ? { pagination: { limit: 25, offset: shipmentOffset } } : null,
108
+ actions.revision
109
+ );
110
+ const estimates = useConditionQuery<InvoiceEstimatePageQuery>(
111
+ InvoiceEstimatePageDocument,
112
+ enabled && shipment
113
+ ? { shipmentId: shipment, pagination: { limit: 25, offset: estimateOffset } }
114
+ : null,
115
+ actions.revision
116
+ );
117
+ const exact = useConditionQuery<InvoiceEstimateVersionQuery>(
118
+ InvoiceEstimateVersionDocument,
119
+ enabled && shipment && estimateId ? { shipmentId: shipment, id: estimateId } : null,
120
+ actions.revision
121
+ );
122
+ const history = useConditionQuery<InvoiceComparisonHistoryQuery>(
123
+ InvoiceComparisonHistoryDocument,
124
+ enabled && shipment && identity
125
+ ? {
126
+ shipmentId: shipment,
127
+ invoiceIdentityId: identity,
128
+ pagination: { limit: 10, offset: historyOffset },
129
+ }
130
+ : null,
131
+ actions.revision
132
+ );
133
+ const comparison = useConditionQuery<InvoiceComparisonExactQuery>(
134
+ InvoiceComparisonExactDocument,
135
+ enabled && shipment && selectedId ? { shipmentId: shipment, id: selectedId } : null,
136
+ actions.revision
137
+ );
138
+ const record = comparison.data?.mtwFreightInvoiceComparison ?? known;
139
+ const updateHistory = (row: InvoiceReceiptFragment) => {
140
+ setShipment(row.shipmentId);
141
+ setKnown(row);
142
+ setSelectedId(row.id);
143
+ setIdentity(row.invoiceIdentityId);
144
+ setIdentityEntry(row.invoiceIdentityId);
145
+ setHistoryOffset(0);
146
+ setStarted(false);
147
+ setCorrection(null);
148
+ setParams(
149
+ (previous) => {
150
+ const next = new URLSearchParams(previous);
151
+ next.set('invoiceShipment', row.shipmentId);
152
+ next.set('invoiceIdentity', row.invoiceIdentityId);
153
+ return next;
154
+ },
155
+ { replace: true }
156
+ );
157
+ };
158
+ const options =
159
+ shipments.data?.shipments.map((s) => ({
160
+ id: s.id,
161
+ label: `${s.shipmentNumber} — ${s.primaryMode} — ${s.status}`,
162
+ })) ?? [];
163
+ if (shipment && !options.some((s) => s.id === shipment))
164
+ options.unshift({ id: shipment, label: 'Selected shipment outside this loaded page' });
165
+ const shipmentPage = shipments.data
166
+ ? {
167
+ total: shipments.data.shipmentsCount,
168
+ limit: 25,
169
+ offset: shipmentOffset,
170
+ hasMore: shipmentOffset + shipments.data.shipments.length < shipments.data.shipmentsCount,
171
+ nextOffset: shipmentOffset + 25,
172
+ }
173
+ : undefined;
174
+ const selected = exact.data?.mtwFreightEstimateVersion;
175
+ return (
176
+ <div className="min-w-0 space-y-6 break-words">
177
+ <p className="rounded border p-4 text-sm">
178
+ Record only what the supplier source reports. Unknown is not zero. This comparison
179
+ establishes no quote acceptance, liability, invoice approval or payment. Corrections append
180
+ a new immutable revision.
181
+ </p>
182
+ {actions.uncertain && (
183
+ <InvoiceRequestRecovery
184
+ pending={actions.pending}
185
+ scope={scope}
186
+ busy={actions.busy}
187
+ canReconcile={actions.canReconcile}
188
+ onRead={() =>
189
+ void actions.reconcile().then((row) => {
190
+ if (row) updateHistory(row);
191
+ })
192
+ }
193
+ />
194
+ )}
195
+ {actions.feedback && (
196
+ <p role={actions.feedback.error ? 'alert' : 'status'}>{actions.feedback.message}</p>
197
+ )}
198
+ {enabled && (
199
+ <>
200
+ <section className={reuseCard}>
201
+ <h2 className="text-lg font-semibold">Choose the exact recorded estimate</h2>
202
+ <PinnedSelect
203
+ label="Invoice shipment"
204
+ value={shipment}
205
+ disabled={paused || started}
206
+ options={options}
207
+ onChange={(id) => {
208
+ setShipment(id);
209
+ setEstimateId('');
210
+ setEstimateOffset(0);
211
+ setIdentity('');
212
+ setIdentityEntry('');
213
+ setSelectedId('');
214
+ setKnown(null);
215
+ setCorrection(null);
216
+ setStarted(false);
217
+ setParams(
218
+ (previous) => {
219
+ const next = new URLSearchParams(previous);
220
+ next.set('invoiceShipment', id);
221
+ next.delete('invoiceIdentity');
222
+ return next;
223
+ },
224
+ { replace: true }
225
+ );
226
+ }}
227
+ />
228
+ <ReuseState loading={shipments.loading} error={shipments.error} />
229
+ <ReusePager
230
+ page={shipmentPage}
231
+ busy={paused || started || shipments.loading}
232
+ offset={shipmentOffset}
233
+ onChange={setShipmentOffset}
234
+ />
235
+ {shipment && (
236
+ <>
237
+ <PinnedSelect
238
+ label="Exact estimate version"
239
+ value={estimateId}
240
+ disabled={paused || started}
241
+ options={
242
+ estimates.data?.mtwFreightEstimatePage.items.map((v) => ({
243
+ id: v.id,
244
+ label: `${v.estimateReference} / version ${v.version} / ${v.facts.kind} / ${v.facts.title}`,
245
+ })) ?? []
246
+ }
247
+ onChange={(id) => setEstimateId(id)}
248
+ />
249
+ <ReuseState loading={estimates.loading} error={estimates.error} />
250
+ <ReusePager
251
+ page={estimates.data?.mtwFreightEstimatePage}
252
+ busy={paused || started || estimates.loading}
253
+ offset={estimateOffset}
254
+ onChange={setEstimateOffset}
255
+ />
256
+ </>
257
+ )}
258
+ <ReuseState loading={exact.loading} error={exact.error} />
259
+ {selected && (
260
+ <p>
261
+ Pinned {selected.facts.kind}: {selected.estimateReference} version{' '}
262
+ {selected.version}. Source {selected.facts.sourceReference}/
263
+ {selected.facts.sourceRevision}; source validity {selected.validityAtRead}. This
264
+ does not establish acceptance.
265
+ </p>
266
+ )}
267
+ <div className="flex flex-wrap gap-3">
268
+ <Button
269
+ type="button"
270
+ disabled={
271
+ paused ||
272
+ !selected ||
273
+ selected.shipmentId !== shipment ||
274
+ exact.loading ||
275
+ !!exact.error
276
+ }
277
+ onClick={() => {
278
+ setStarted(true);
279
+ setError(null);
280
+ }}
281
+ >
282
+ {correction
283
+ ? `Start correction draft after revision ${correction.revision}`
284
+ : 'Start manual invoice draft'}
285
+ </Button>
286
+ {started && (
287
+ <Button
288
+ type="button"
289
+ variant="outline"
290
+ disabled={paused}
291
+ onClick={() => {
292
+ setStarted(false);
293
+ setCorrection(null);
294
+ }}
295
+ >
296
+ Discard unsent draft
297
+ </Button>
298
+ )}
299
+ <Button
300
+ type="button"
301
+ variant="outline"
302
+ disabled={actions.busy}
303
+ onClick={actions.refresh}
304
+ >
305
+ Refresh recorded data
306
+ </Button>
307
+ </div>
308
+ </section>
309
+ {started && selected && selected.shipmentId === shipment && (
310
+ <InvoiceForm
311
+ key={`${selected.id}:${correction?.id ?? 'first'}`}
312
+ estimate={selected}
313
+ previous={correction}
314
+ disabled={paused || exact.loading || !!exact.error}
315
+ canRecord={access.canUpdate}
316
+ onPreview={async (input) => {
317
+ const result = await request<InvoiceStructuralPreviewQuery>(
318
+ InvoiceStructuralPreviewDocument,
319
+ { input }
320
+ );
321
+ return result.previewMtwFreightInvoice;
322
+ }}
323
+ onSave={async (input, pin) => {
324
+ const result = await actions.run(input, pin);
325
+ if (result) updateHistory(result);
326
+ return result;
327
+ }}
328
+ />
329
+ )}
330
+ {error && <p role="alert">{error}</p>}
331
+ {shipment && (
332
+ <section className={reuseCard}>
333
+ <h2 className="text-lg font-semibold">Recorded invoice history</h2>
334
+ <p>
335
+ A confirmed save opens its invoice history here. Bookmark this page to return to it.
336
+ Complete invoice browsing is not available in this view. A missing page never proves
337
+ that an unresolved save had no effect.
338
+ </p>
339
+ <details>
340
+ <summary className="min-h-11 cursor-pointer">
341
+ Advanced: open a known receipt identity
342
+ </summary>
343
+ <FreightText
344
+ label="Known invoice identity ID"
345
+ value={identityEntry}
346
+ disabled={actions.busy}
347
+ onChange={setIdentityEntry}
348
+ />
349
+ <Button
350
+ type="button"
351
+ variant="outline"
352
+ disabled={actions.busy || !identityEntry.trim()}
353
+ onClick={() => {
354
+ setIdentity(identityEntry.trim());
355
+ setHistoryOffset(0);
356
+ setSelectedId('');
357
+ setKnown(null);
358
+ setParams(
359
+ (previous) => {
360
+ const next = new URLSearchParams(previous);
361
+ next.set('invoiceShipment', shipment);
362
+ next.set('invoiceIdentity', identityEntry.trim());
363
+ return next;
364
+ },
365
+ { replace: true }
366
+ );
367
+ }}
368
+ >
369
+ Read identity history
370
+ </Button>
371
+ </details>
372
+ {identity && (
373
+ <>
374
+ <ReuseState loading={history.loading} error={history.error} />
375
+ {history.data?.mtwFreightInvoiceComparisonPage.items.map((row) => (
376
+ <Button
377
+ key={row.id}
378
+ className="h-auto max-w-full whitespace-normal break-words text-left"
379
+ type="button"
380
+ variant="outline"
381
+ onClick={() => {
382
+ setSelectedId(row.id);
383
+ setKnown(null);
384
+ }}
385
+ >
386
+ {row.facts.supplierLabel ?? row.facts.supplierReference} / invoice{' '}
387
+ {row.facts.invoiceReference}, revision {row.revision} / {row.recordedAt}
388
+ </Button>
389
+ ))}
390
+ <ReusePager
391
+ page={history.data?.mtwFreightInvoiceComparisonPage}
392
+ busy={history.loading || actions.busy}
393
+ offset={historyOffset}
394
+ onChange={setHistoryOffset}
395
+ />
396
+ </>
397
+ )}
398
+ </section>
399
+ )}
400
+ <ReuseState loading={comparison.loading} error={comparison.error} />
401
+ {record && record.shipmentId === shipment && (
402
+ <>
403
+ <InvoiceHistoryRecord record={record} />
404
+ <Button
405
+ type="button"
406
+ disabled={
407
+ paused ||
408
+ !access.canUpdate ||
409
+ !history.data ||
410
+ history.data.mtwFreightInvoiceComparisonPage.items[0]?.id !== record.id ||
411
+ historyOffset !== 0
412
+ }
413
+ onClick={() => {
414
+ setEstimateId(record.estimate.id);
415
+ setCorrection(record);
416
+ setStarted(false);
417
+ setError(null);
418
+ }}
419
+ >
420
+ Prepare correction to this latest revision
421
+ </Button>
422
+ <p>
423
+ Refresh the first history page to pin its latest revision before correcting. The
424
+ server checks the exact predecessor again.
425
+ </p>
426
+ </>
427
+ )}
428
+ </>
429
+ )}
430
+ </div>
431
+ );
432
+ }
433
+
434
+ /** Retain the unresolved intent across context changes without displaying another context's source facts. */
435
+ export function InvoiceRequestRecovery({
436
+ pending,
437
+ scope,
438
+ busy,
439
+ canReconcile,
440
+ onRead,
441
+ }: {
442
+ pending: InvoiceIntent | null;
443
+ scope: InvoiceScope | null;
444
+ busy: boolean;
445
+ canReconcile: boolean;
446
+ onRead: () => void;
447
+ }) {
448
+ return (
449
+ <section className={reuseCard} aria-label="Original invoice request recovery">
450
+ <h2 className="font-semibold">Original save is unresolved</h2>
451
+ {pending && sameInvoiceScope(pending.scope, scope) ? (
452
+ <>
453
+ <p>
454
+ Invoice {pending.input.facts.invoiceReference ?? 'reference unknown'}; exact{' '}
455
+ {pending.estimate.kind.toLowerCase()} {pending.estimate.estimateReference}, version{' '}
456
+ {pending.estimate.version}. No automatic resend or new request is allowed.
457
+ </p>
458
+ <Button type="button" disabled={!canReconcile || busy} onClick={onRead}>
459
+ Read original request receipt
460
+ </Button>
461
+ <details>
462
+ <summary className="min-h-11 cursor-pointer">Retained original source intent</summary>
463
+ <pre className="max-w-full whitespace-pre-wrap break-all text-xs">
464
+ {JSON.stringify(pending.input, null, 2)}
465
+ </pre>
466
+ </details>
467
+ </>
468
+ ) : (
469
+ <p>
470
+ {pending
471
+ ? 'The original request is retained in another or unavailable context. Return to the original account, organization, workspace and project with read permission; its source facts remain hidden here.'
472
+ : 'The saved intent is malformed or unavailable. It remains blocked; do not discard it or start another request as proof of no effect.'}
473
+ </p>
474
+ )}
475
+ </section>
476
+ );
477
+ }
@@ -0,0 +1,25 @@
1
+ /** The expiry control is explicitly UTC, independent of the browser timezone. */
2
+ export const driverLicenseExpiryInput = (recorded: Date | undefined): string =>
3
+ recorded ? recorded.toISOString().slice(0, -1) : '';
4
+
5
+ /** Empty means unrecorded; never substitute noon, end of day or the current time. */
6
+ export const parseDriverLicenseExpiry = (value: string): string | null => {
7
+ if (value === '') return null;
8
+ const match = /^(\d{4}-\d\d-\d\dT\d\d:\d\d)(?::(\d\d)(?:\.(\d{1,3}))?)?$/.exec(value);
9
+ if (!match) throw new Error('Enter a valid licence expiry date and time in UTC.');
10
+ const canonical = `${match[1]}:${match[2] ?? '00'}.${(match[3] ?? '').padEnd(3, '0')}Z`;
11
+ const parsed = new Date(canonical);
12
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== canonical) {
13
+ throw new Error('Enter a valid licence expiry date and time in UTC.');
14
+ }
15
+ return canonical;
16
+ };
17
+
18
+ /** Omit unchanged updates so even server precision beyond this display is retained. */
19
+ export const driverLicenseExpiryUpdate = (
20
+ value: string,
21
+ recorded: Date | undefined
22
+ ): string | null | undefined => {
23
+ const next = parseDriverLicenseExpiry(value);
24
+ return next === (recorded?.toISOString() ?? null) ? undefined : next;
25
+ };