@burdenoff/microfe-movethewheels 2026.1016.2 → 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 (113) 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-cost/FreightCommercialEditor.js +217 -0
  7. package/dist/components/freight-cost/FreightCommercialFactEditor.js +217 -0
  8. package/dist/components/freight-cost/FreightCommercialHistory.js +115 -0
  9. package/dist/components/freight-cost/FreightDraftForm.js +106 -91
  10. package/dist/components/freight-cost/FreightHistory.js +42 -40
  11. package/dist/components/freight-invoice/InvoiceForm.js +366 -0
  12. package/dist/components/freight-invoice/InvoiceResults.js +240 -0
  13. package/dist/generated/consolidation-operations.d.ts +195 -265
  14. package/dist/generated/container-condition-operations.d.ts +124 -187
  15. package/dist/generated/container-operations.d.ts +59 -85
  16. package/dist/generated/empty-reuse-operations.d.ts +165 -271
  17. package/dist/generated/free-time-operations.d.ts +120 -184
  18. package/dist/generated/freight-cost-operations.d.ts +955 -165
  19. package/dist/generated/freight-invoice-operations.d.ts +1447 -0
  20. package/dist/generated/logistics-procedure-operations.d.ts +100 -156
  21. package/dist/generated/pending-pick-operations.d.ts +769 -1025
  22. package/dist/generated/shipment-status-authority-operations.d.ts +82 -103
  23. package/dist/generated/stock-binding-operations.d.ts +188 -260
  24. package/dist/generated/stock-observation-operations.d.ts +103 -138
  25. package/dist/generated/tracking-quality-operations.d.ts +27 -41
  26. package/dist/generated/trip-utilization-operations.d.ts +137 -185
  27. package/dist/generated/wspace-operations.d.ts +628 -2272
  28. package/dist/generated/wspace-types.d.ts +4 -23
  29. package/dist/hooks/useFreightRecoveryAction.js +20 -20
  30. package/dist/hooks/useInvoiceComparisonRecovery.js +118 -0
  31. package/dist/movethewheels/components/ContainerEvidencePicker.d.ts +2 -1
  32. package/dist/movethewheels/components/freight-cost/FreightCommercialEditor.d.ts +8 -0
  33. package/dist/movethewheels/components/freight-cost/FreightCommercialFactEditor.d.ts +8 -0
  34. package/dist/movethewheels/components/freight-cost/FreightCommercialHistory.d.ts +4 -0
  35. package/dist/movethewheels/components/freight-cost/FreightDraftForm.d.ts +4 -2
  36. package/dist/movethewheels/components/freight-cost/FreightHistory.d.ts +2 -2
  37. package/dist/movethewheels/components/freight-invoice/InvoiceForm.d.ts +19 -0
  38. package/dist/movethewheels/components/freight-invoice/InvoiceResults.d.ts +9 -0
  39. package/dist/movethewheels/hooks/useFleet.d.ts +1 -1
  40. package/dist/movethewheels/hooks/useFleetIntelligence.d.ts +1 -1
  41. package/dist/movethewheels/hooks/useInvoiceComparisonRecovery.d.ts +22 -0
  42. package/dist/movethewheels/hooks/useProcedureSelection.d.ts +1 -3
  43. package/dist/movethewheels/pages/FreightInvoicePage.d.ts +10 -0
  44. package/dist/movethewheels/utils/driverLicenseExpiry.d.ts +6 -0
  45. package/dist/movethewheels/utils/freightCommercial.d.ts +44 -0
  46. package/dist/movethewheels/utils/invoiceComparison.d.ts +32 -0
  47. package/dist/pages/DriverDetailsPage.js +306 -282
  48. package/dist/pages/FreightEstimatePage.js +97 -92
  49. package/dist/pages/FreightInvoicePage.js +346 -0
  50. package/dist/src/generated/consolidation-operations.js +3961 -503
  51. package/dist/src/generated/container-condition-operations.js +2968 -206
  52. package/dist/src/generated/container-operations.js +997 -99
  53. package/dist/src/generated/empty-reuse-operations.js +4811 -747
  54. package/dist/src/generated/free-time-operations.js +2824 -233
  55. package/dist/src/generated/freight-cost-operations.js +4852 -323
  56. package/dist/src/generated/freight-invoice-operations.js +4894 -0
  57. package/dist/src/generated/logistics-procedure-operations.js +2707 -360
  58. package/dist/src/generated/pending-pick-operations.js +8244 -1196
  59. package/dist/src/generated/shipment-status-authority-operations.js +1157 -84
  60. package/dist/src/generated/stock-binding-operations.js +2779 -173
  61. package/dist/src/generated/stock-observation-operations.js +1274 -91
  62. package/dist/src/generated/tracking-quality-operations.js +407 -42
  63. package/dist/src/generated/trip-utilization-operations.js +1765 -138
  64. package/dist/src/generated/wspace-operations.js +428 -706
  65. package/dist/utils/driverLicenseExpiry.js +14 -0
  66. package/dist/utils/freightAcknowledgement.js +6 -5
  67. package/dist/utils/freightCommercial.js +194 -0
  68. package/dist/utils/freightRecovery.js +22 -22
  69. package/dist/utils/invoiceComparison.js +272 -0
  70. package/package.json +30 -29
  71. package/src/generated/consolidation-operations.ts +60 -559
  72. package/src/generated/container-condition-operations.ts +63 -265
  73. package/src/generated/container-operations.ts +37 -132
  74. package/src/generated/empty-reuse-operations.ts +83 -824
  75. package/src/generated/free-time-operations.ts +55 -284
  76. package/src/generated/freight-cost-operations.ts +373 -349
  77. package/src/generated/freight-invoice-operations.ts +830 -0
  78. package/src/generated/logistics-procedure-operations.ts +60 -414
  79. package/src/generated/pending-pick-operations.ts +48 -1239
  80. package/src/generated/shipment-status-authority-operations.ts +33 -113
  81. package/src/generated/stock-binding-operations.ts +29 -198
  82. package/src/generated/stock-observation-operations.ts +24 -111
  83. package/src/generated/tracking-quality-operations.ts +19 -57
  84. package/src/generated/trip-utilization-operations.ts +32 -166
  85. package/src/generated/wspace-operations.ts +284 -2906
  86. package/src/generated/wspace-types.ts +2 -8
  87. package/src/movethewheels/MoveTheWheelsRoot.tsx +6 -0
  88. package/src/movethewheels/MoveTheWheelsRoutes.tsx +2 -0
  89. package/src/movethewheels/components/ContainerEvidencePicker.tsx +12 -4
  90. package/src/movethewheels/components/FleetAlertsPanel.tsx +4 -2
  91. package/src/movethewheels/components/freight-cost/FreightCommercialEditor.tsx +236 -0
  92. package/src/movethewheels/components/freight-cost/FreightCommercialFactEditor.tsx +258 -0
  93. package/src/movethewheels/components/freight-cost/FreightCommercialHistory.tsx +90 -0
  94. package/src/movethewheels/components/freight-cost/FreightDraftForm.tsx +33 -4
  95. package/src/movethewheels/components/freight-cost/FreightHistory.tsx +4 -2
  96. package/src/movethewheels/components/freight-invoice/InvoiceForm.tsx +412 -0
  97. package/src/movethewheels/components/freight-invoice/InvoiceResults.tsx +162 -0
  98. package/src/movethewheels/hooks/useFleet.ts +1 -1
  99. package/src/movethewheels/hooks/useFleetIntelligence.ts +5 -1
  100. package/src/movethewheels/hooks/useFreightRecoveryAction.ts +2 -1
  101. package/src/movethewheels/hooks/useInvoiceComparisonRecovery.ts +227 -0
  102. package/src/movethewheels/pages/DriverDetailsPage.tsx +35 -9
  103. package/src/movethewheels/pages/FreightEstimatePage.tsx +15 -7
  104. package/src/movethewheels/pages/FreightInvoicePage.tsx +477 -0
  105. package/src/movethewheels/utils/driverLicenseExpiry.ts +25 -0
  106. package/src/movethewheels/utils/freightAcknowledgement.ts +3 -1
  107. package/src/movethewheels/utils/freightCommercial.ts +388 -0
  108. package/src/movethewheels/utils/freightRecovery.ts +6 -2
  109. package/src/movethewheels/utils/invoiceComparison.ts +520 -0
  110. package/src/operations/freight-cost/commercial.graphql +483 -0
  111. package/src/operations/freight-invoice/comparison.graphql +40 -0
  112. package/src/operations/wspace/freight-invoice-evidence.graphql +6 -0
  113. 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
+ };
@@ -1,3 +1,4 @@
1
+ import { matchesCommercialCapture } from './freightCommercial';
1
2
  /** Validate the minimum immutable receipt identity before resolving a retained request. */
2
3
  export function matchesFreightAcknowledgement(
3
4
  result: unknown,
@@ -26,6 +27,7 @@ export function matchesFreightAcknowledgement(
26
27
  input.expectedVersion >= 0 &&
27
28
  receipt.version === input.expectedVersion + 1 &&
28
29
  'previousVersionId' in receipt &&
29
- receipt.previousVersionId === input.expectedPreviousVersionId
30
+ receipt.previousVersionId === input.expectedPreviousVersionId &&
31
+ (input.commercialCapture == null || matchesCommercialCapture(receipt, input))
30
32
  );
31
33
  }