@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,90 @@
1
+ import type { FreightCommercialVersionQuery } from '../../../generated/freight-cost-operations';
2
+ import { commercialSlots } from '../../utils/freightCommercial';
3
+ import { freightLabel } from '../../utils/freightDraft';
4
+
5
+ export function FreightCommercialHistory({
6
+ capture,
7
+ }: {
8
+ capture: FreightCommercialVersionQuery['mtwFreightEstimateVersion']['commercialCapture'];
9
+ }) {
10
+ if (capture === undefined)
11
+ return <p role="alert">Commercial context is unavailable in this reply.</p>;
12
+ if (capture === null) return <p>Commercial context was not captured with this version.</p>;
13
+ return (
14
+ <section className="min-w-0 space-y-4 break-words rounded border p-4">
15
+ <h3 className="font-semibold">
16
+ Recorded commercial context — schema {capture.schemaVersion}
17
+ </h3>
18
+ <p className="text-sm">
19
+ These are source declarations, not quote acceptance, verified authority or a complete
20
+ commercial checklist.
21
+ </p>
22
+ <div className="grid min-w-0 gap-3 md:grid-cols-2">
23
+ {commercialSlots.map((slot) => {
24
+ const fact = capture[slot];
25
+ return (
26
+ <article key={slot} className="space-y-2 rounded border p-3">
27
+ <h4 className="font-medium">
28
+ {freightLabel(slot)} — {freightLabel(fact.status)}
29
+ </h4>
30
+ {fact.reason && <p>{fact.reason}</p>}
31
+ {typeof fact.value === 'string' ? (
32
+ <p>{fact.value}</p>
33
+ ) : (
34
+ fact.value && (
35
+ <dl>
36
+ {Object.entries(fact.value)
37
+ .filter(([key]) => key !== '__typename')
38
+ .map(([key, value]) => (
39
+ <div key={key}>
40
+ <dt className="inline font-medium">{key}: </dt>
41
+ <dd className="inline">
42
+ {value === null || value === undefined ? 'Not recorded' : String(value)}
43
+ </dd>
44
+ </div>
45
+ ))}
46
+ </dl>
47
+ )
48
+ )}
49
+ {fact.provenance && (
50
+ <div className="text-sm">
51
+ <p>
52
+ Declared source {fact.provenance.sourceReference} /{' '}
53
+ {fact.provenance.sourceRevision}; observed{' '}
54
+ {fact.provenance.observedAt ?? 'Unknown'}.
55
+ </p>
56
+ {fact.provenance.evidenceLinks.map((link) => (
57
+ <p key={JSON.stringify([link.fileId, link.versionId])}>
58
+ Exact Files link: {link.fileId}; version {link.versionId}.
59
+ </p>
60
+ ))}
61
+ </div>
62
+ )}
63
+ </article>
64
+ );
65
+ })}
66
+ </div>
67
+ <section className="space-y-2">
68
+ <h4 className="font-medium">
69
+ Server-recorded owner context (separate from declared facts)
70
+ </h4>
71
+ <p>
72
+ Shipment {capture.ownerShipment.shipmentId}; revision at {capture.ownerShipment.updatedAt}
73
+ ; mode {capture.ownerShipment.primaryMode}.
74
+ </p>
75
+ {capture.ownerLegs.length === 0 && (
76
+ <p>No legs selected for this capture. This does not mean the shipment has no legs.</p>
77
+ )}
78
+ {capture.ownerLegs.map((leg) => (
79
+ <p key={leg.id}>
80
+ Leg {leg.legNumber} ({leg.id}) / {leg.mode}; revision {leg.updatedAt}; carrier{' '}
81
+ {leg.carrierName ?? 'Not recorded'} ({leg.carrierId ?? 'No carrier reference'}); weight{' '}
82
+ {leg.weightKg ?? 'Not recorded'} kg; volume {leg.volumeCbm ?? 'Not recorded'} m³;
83
+ equipment {leg.containerType ?? 'Not recorded'} × {leg.containerCount ?? 'Not recorded'}
84
+ .
85
+ </p>
86
+ ))}
87
+ </section>
88
+ </section>
89
+ );
90
+ }
@@ -1,4 +1,7 @@
1
1
  import { useRef, useState } from 'react';
2
+ import type { CommercialCapture, CommercialEditorState } from '../../utils/freightCommercial';
3
+ import { commercialCaptureInput, commercialCanonical } from '../../utils/freightCommercial';
4
+ import { FreightCommercialEditor } from './FreightCommercialEditor';
2
5
  import type {
3
6
  MtwFreightFactsInput,
4
7
  PreviewFreightCostsQuery,
@@ -18,15 +21,22 @@ import {
18
21
  } from '../../utils/freightDraft';
19
22
  export function FreightDraftForm({
20
23
  disabled,
24
+ shipmentId,
21
25
  onPreview,
22
26
  onSave,
23
27
  }: {
24
28
  disabled: boolean;
29
+ shipmentId: string;
25
30
  onPreview: (
26
31
  facts: MtwFreightFactsInput
27
32
  ) => Promise<PreviewFreightCostsQuery['previewMtwFreightTotal']>;
28
- onSave: (facts: MtwFreightFactsInput, evidence: SelectedEvidence[]) => Promise<void>;
33
+ onSave: (
34
+ facts: MtwFreightFactsInput,
35
+ evidence: SelectedEvidence[],
36
+ commercialCapture?: CommercialCapture
37
+ ) => Promise<void>;
29
38
  }) {
39
+ const [commercial, setCommercial] = useState<CommercialEditorState | null>(null);
30
40
  const [facts, setFacts] = useState(newFreightFacts);
31
41
  const [evidence, setEvidence] = useState<SelectedEvidence[]>([]);
32
42
  const [preview, setPreview] = useState<{
@@ -65,9 +75,22 @@ export function FreightDraftForm({
65
75
  return;
66
76
  }
67
77
  setError(null);
68
- void onSave(structuredClone(facts), structuredClone(evidence)).catch((failure) =>
69
- setError(failure instanceof Error ? failure.message : 'Unable to save this cost record.')
70
- );
78
+ try {
79
+ if (commercial && commercial.reviewed !== commercialCanonical(commercial.selectedLegs))
80
+ throw new Error('Explicitly review the commercial leg selection before saving.');
81
+ const capture = commercial
82
+ ? commercialCaptureInput(commercial.facts, commercial.selectedLegs, evidence)
83
+ : undefined;
84
+ void onSave(structuredClone(facts), structuredClone(evidence), capture).catch((failure) =>
85
+ setError(
86
+ failure instanceof Error ? failure.message : 'Unable to save this cost record.'
87
+ )
88
+ );
89
+ } catch (failure) {
90
+ setError(
91
+ failure instanceof Error ? failure.message : 'Review commercial facts before saving.'
92
+ );
93
+ }
71
94
  }}
72
95
  >
73
96
  <fieldset disabled={disabled || previewing} className="space-y-5">
@@ -194,6 +217,12 @@ export function FreightDraftForm({
194
217
  )}
195
218
  <FreightFxEditor value={facts.fx} onChange={(fx) => patch({ fx })} />
196
219
  </details>
220
+ <FreightCommercialEditor
221
+ value={commercial}
222
+ onChange={setCommercial}
223
+ shipmentId={shipmentId}
224
+ evidence={evidence}
225
+ />
197
226
  <ContainerEvidencePicker
198
227
  value={evidence}
199
228
  onChange={setEvidence}
@@ -1,10 +1,11 @@
1
- import type { FreightVersionQuery } from '../../../generated/freight-cost-operations';
1
+ import { FreightCommercialHistory } from './FreightCommercialHistory';
2
+ import type { FreightCommercialVersionQuery } from '../../../generated/freight-cost-operations';
2
3
  import { FreightTotals } from './FreightTotals';
3
4
  import { freightLabel, freightMoney } from '../../utils/freightDraft';
4
5
  export function FreightHistory({
5
6
  record,
6
7
  }: {
7
- record: FreightVersionQuery['mtwFreightEstimateVersion'];
8
+ record: FreightCommercialVersionQuery['mtwFreightEstimateVersion'];
8
9
  }) {
9
10
  return (
10
11
  <article className="min-w-0 space-y-4 break-words rounded border p-4">
@@ -28,6 +29,7 @@ export function FreightHistory({
28
29
  Price at {record.facts.pricingAt}; recorded {record.recordedAt} by {record.createdBy} (
29
30
  {freightLabel(record.actorProvenance)}).
30
31
  </p>
32
+ <FreightCommercialHistory capture={record.commercialCapture} />
31
33
  <FreightTotals value={record.totals} currency={record.facts.reportingCurrency} />
32
34
  <details>
33
35
  <summary className="min-h-11 cursor-pointer font-medium">
@@ -0,0 +1,412 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import type {
3
+ CompareMtwFreightInvoiceInput,
4
+ InvoiceEstimatePinFragment,
5
+ InvoiceEstimateVersionQuery,
6
+ InvoiceReceiptFragment,
7
+ InvoiceStructuralPreviewQuery,
8
+ MtwFreightInvoiceFactsInput,
9
+ } from '../../../generated/freight-invoice-operations';
10
+ import { ContainerEvidencePicker, type SelectedEvidence } from '../ContainerEvidencePicker';
11
+ import { Button } from '../ui';
12
+ import { FreightCurrency, FreightSelect, FreightText } from '../freight-cost/FreightFields';
13
+ import {
14
+ invoiceCanonical,
15
+ invoiceFactsInput,
16
+ matchesInvoicePreview,
17
+ } from '../../utils/invoiceComparison';
18
+ import { fixedFreightMinor, freightMoney } from '../../utils/freightDraft';
19
+ import { InvoiceResults } from './InvoiceResults';
20
+
21
+ export type InvoiceEstimate = InvoiceEstimateVersionQuery['mtwFreightEstimateVersion'];
22
+ function blankFacts(): MtwFreightInvoiceFactsInput {
23
+ return {
24
+ schemaVersion: 1,
25
+ supplierReference: null,
26
+ supplierLabel: null,
27
+ invoiceReference: null,
28
+ sourceReference: '',
29
+ sourceRevision: '',
30
+ invoiceDate: null,
31
+ currency: { code: '', minorUnitDigits: 2 },
32
+ rows: [
33
+ {
34
+ id: crypto.randomUUID(),
35
+ description: '',
36
+ estimateLineId: null,
37
+ currency: { code: '', minorUnitDigits: 2 },
38
+ payerReference: null,
39
+ payerLabel: null,
40
+ quantity: null,
41
+ unit: null,
42
+ unitRateMajor: null,
43
+ amountMinor: null,
44
+ zeroReason: null,
45
+ },
46
+ ],
47
+ };
48
+ }
49
+ export function invoiceEstimatePin(estimate: InvoiceEstimate): InvoiceEstimatePinFragment {
50
+ return {
51
+ id: estimate.id,
52
+ estimateReference: estimate.estimateReference,
53
+ version: estimate.version,
54
+ kind: estimate.facts.kind,
55
+ sourceReference: estimate.facts.sourceReference,
56
+ sourceRevision: estimate.facts.sourceRevision,
57
+ quotedAt: estimate.facts.quotedAt,
58
+ validFrom: estimate.facts.validFrom,
59
+ validUntil: estimate.facts.validUntil,
60
+ acceptance: 'NOT_ESTABLISHED',
61
+ };
62
+ }
63
+ export function InvoiceForm({
64
+ estimate,
65
+ previous,
66
+ disabled,
67
+ canRecord,
68
+ onPreview,
69
+ onSave,
70
+ }: {
71
+ estimate: InvoiceEstimate;
72
+ previous: InvoiceReceiptFragment | null;
73
+ disabled: boolean;
74
+ canRecord: boolean;
75
+ onPreview: (input: {
76
+ shipmentId: string;
77
+ estimateVersionId: string;
78
+ facts: MtwFreightInvoiceFactsInput;
79
+ evidence: { fileId: string; versionId: string }[];
80
+ }) => Promise<InvoiceStructuralPreviewQuery['previewMtwFreightInvoice']>;
81
+ onSave: (
82
+ input: Omit<CompareMtwFreightInvoiceInput, 'idempotencyKey'>,
83
+ pin: InvoiceEstimatePinFragment
84
+ ) => Promise<InvoiceReceiptFragment | null | undefined>;
85
+ }) {
86
+ const [facts, setFacts] = useState<MtwFreightInvoiceFactsInput>(() =>
87
+ previous ? structuredClone(previous.facts) : blankFacts()
88
+ ),
89
+ [evidence, setEvidence] = useState<SelectedEvidence[]>([]),
90
+ [reason, setReason] = useState('');
91
+ const [preview, setPreview] = useState<{
92
+ body: string;
93
+ value: InvoiceStructuralPreviewQuery['previewMtwFreightInvoice'];
94
+ } | null>(null),
95
+ [error, setError] = useState<string | null>(null),
96
+ [previewing, setPreviewing] = useState(false);
97
+ const form = useRef<HTMLFormElement | null>(null);
98
+ const alive = useRef(true),
99
+ running = useRef(false);
100
+ useEffect(() => {
101
+ alive.current = true;
102
+ return () => {
103
+ alive.current = false;
104
+ };
105
+ }, []);
106
+ // The owning workbench is keyed by canonical context; no old draft is rebound to a new owner.
107
+ const body = invoiceCanonical({
108
+ facts,
109
+ evidence: evidence.map(({ fileId, versionId }) => ({ fileId, versionId })),
110
+ estimateId: estimate.id,
111
+ });
112
+ const current = preview?.body === body ? preview.value : null;
113
+ const patch = (v: Partial<MtwFreightInvoiceFactsInput>) => setFacts({ ...facts, ...v });
114
+ const changeRow = (id: string, v: Partial<MtwFreightInvoiceFactsInput['rows'][number]>) =>
115
+ patch({ rows: facts.rows.map((r) => (r.id === id ? { ...r, ...v } : r)) });
116
+ const previewNow = async () => {
117
+ if (running.current || disabled || !form.current?.reportValidity()) return;
118
+ running.current = true;
119
+ setPreviewing(true);
120
+ setError(null);
121
+ const original = body;
122
+ try {
123
+ const normalized = invoiceFactsInput(facts),
124
+ value = await onPreview({
125
+ shipmentId: estimate.shipmentId,
126
+ estimateVersionId: estimate.id,
127
+ facts: normalized,
128
+ evidence: evidence.map(({ fileId, versionId }) => ({ fileId, versionId })),
129
+ });
130
+ if (!matchesInvoicePreview(value, normalized, invoiceEstimatePin(estimate)))
131
+ throw new Error(
132
+ 'The preview did not match these reported facts and exact estimate. Nothing was sent.'
133
+ );
134
+ if (alive.current) setPreview({ body: original, value });
135
+ } catch (failure) {
136
+ if (alive.current)
137
+ setError(
138
+ failure instanceof Error ? failure.message : 'Unable to preview the reported facts.'
139
+ );
140
+ } finally {
141
+ running.current = false;
142
+ if (alive.current) setPreviewing(false);
143
+ }
144
+ };
145
+ return (
146
+ <form
147
+ ref={form}
148
+ className="min-w-0 space-y-5"
149
+ aria-label="Manual invoice comparison"
150
+ onSubmit={(event) => {
151
+ event.preventDefault();
152
+ if (!current || current.recordable !== true || disabled || !canRecord || running.current)
153
+ return;
154
+ setError(null);
155
+ try {
156
+ const normalized = invoiceFactsInput(facts);
157
+ void onSave(
158
+ {
159
+ shipmentId: estimate.shipmentId,
160
+ estimateVersionId: estimate.id,
161
+ facts: normalized,
162
+ evidence: evidence.map(({ fileId, versionId }) => ({ fileId, versionId })),
163
+ expectedRevision: previous?.revision ?? 0,
164
+ expectedPreviousComparisonId: previous?.id ?? null,
165
+ correctionReason: previous ? reason.trim() || null : null,
166
+ },
167
+ invoiceEstimatePin(estimate)
168
+ ).catch(() =>
169
+ setError('The original request remains retained; inspect read-only recovery.')
170
+ );
171
+ } catch (failure) {
172
+ setError(
173
+ failure instanceof Error
174
+ ? failure.message
175
+ : 'Review the reported source fields. Nothing was sent.'
176
+ );
177
+ }
178
+ }}
179
+ >
180
+ <fieldset disabled={disabled || previewing} className="min-w-0 space-y-5">
181
+ <legend className="text-lg font-semibold">
182
+ {previous ? 'Append an immutable correction' : 'Reported supplier invoice'}
183
+ </legend>
184
+ <p>
185
+ Blank means unknown, never zero. This is a manual comparison, not approval or payment. No
186
+ FX, credits or split allocation.
187
+ </p>
188
+ <p>
189
+ Exact pinned {estimate.facts.kind.toLowerCase()}: {estimate.estimateReference} / version{' '}
190
+ {estimate.version}; {estimate.facts.title}. Source validity: {estimate.validityAtRead}.
191
+ Acceptance is not established.
192
+ </p>
193
+ <div className="grid min-w-0 gap-3 md:grid-cols-2">
194
+ <FreightText
195
+ label="Supplier source reference"
196
+ value={facts.supplierReference ?? ''}
197
+ disabled={!!previous}
198
+ onChange={(supplierReference) => patch({ supplierReference })}
199
+ />
200
+ <FreightText
201
+ label="Supplier label"
202
+ value={facts.supplierLabel ?? ''}
203
+ onChange={(supplierLabel) => patch({ supplierLabel })}
204
+ />
205
+ <FreightText
206
+ label="Invoice reference"
207
+ value={facts.invoiceReference ?? ''}
208
+ disabled={!!previous}
209
+ onChange={(invoiceReference) => patch({ invoiceReference })}
210
+ />
211
+ <FreightText
212
+ label="Invoice source reference"
213
+ value={facts.sourceReference}
214
+ required
215
+ onChange={(sourceReference) => patch({ sourceReference })}
216
+ />
217
+ <FreightText
218
+ label="Invoice source revision"
219
+ value={facts.sourceRevision}
220
+ required
221
+ onChange={(sourceRevision) => patch({ sourceRevision })}
222
+ />
223
+ <FreightText
224
+ label="Invoice source date (optional)"
225
+ type="date"
226
+ value={facts.invoiceDate ?? ''}
227
+ onChange={(invoiceDate) => patch({ invoiceDate: invoiceDate || null })}
228
+ />
229
+ </div>
230
+ <FreightCurrency
231
+ label="Invoice currency"
232
+ value={facts.currency}
233
+ onChange={(currency) =>
234
+ patch({
235
+ currency,
236
+ rows: facts.rows.map((r) => ({
237
+ ...r,
238
+ currency,
239
+ amountMinor: null,
240
+ zeroReason: null,
241
+ })),
242
+ })
243
+ }
244
+ />
245
+ {facts.rows.map((row, index) => (
246
+ <fieldset key={row.id} className="min-w-0 space-y-3 rounded border p-3">
247
+ <legend className="font-semibold">Reported row {index + 1}</legend>
248
+ <FreightText
249
+ label={`Row ${index + 1} description`}
250
+ value={row.description}
251
+ required
252
+ onChange={(description) => changeRow(row.id, { description })}
253
+ />
254
+ <FreightSelect
255
+ label={`Row ${index + 1} exact estimate mapping`}
256
+ value={row.estimateLineId ?? ''}
257
+ onChange={(estimateLineId) =>
258
+ changeRow(row.id, { estimateLineId: estimateLineId || null })
259
+ }
260
+ >
261
+ <option value="">Unmatched invoice row</option>
262
+ {estimate.facts.lines.map((line) => (
263
+ <option key={line.id} value={line.id}>
264
+ {line.description} / {line.id} / {line.disposition}
265
+ </option>
266
+ ))}
267
+ </FreightSelect>
268
+ <div className="grid min-w-0 gap-3 md:grid-cols-2">
269
+ <FreightText
270
+ label={`Row ${index + 1} payer source reference`}
271
+ value={row.payerReference ?? ''}
272
+ onChange={(payerReference) => changeRow(row.id, { payerReference })}
273
+ />
274
+ <FreightText
275
+ label={`Row ${index + 1} payer label`}
276
+ value={row.payerLabel ?? ''}
277
+ onChange={(payerLabel) => changeRow(row.id, { payerLabel })}
278
+ />
279
+ <label className="block text-sm">
280
+ Row {index + 1} supplied amount ({row.currency.code || 'declare currency'})
281
+ <input
282
+ key={`${row.currency.code}:${row.currency.minorUnitDigits}`}
283
+ className="mt-1 min-h-11 w-full rounded border p-2"
284
+ aria-label={`Row ${index + 1} supplied amount`}
285
+ inputMode="decimal"
286
+ defaultValue={
287
+ row.amountMinor == null
288
+ ? ''
289
+ : freightMoney(row.amountMinor, row.currency.minorUnitDigits)
290
+ }
291
+ onChange={(event) => {
292
+ try {
293
+ changeRow(row.id, {
294
+ amountMinor: fixedFreightMinor(
295
+ event.target.value,
296
+ row.currency.minorUnitDigits
297
+ ),
298
+ });
299
+ event.target.setCustomValidity('');
300
+ } catch (failure) {
301
+ changeRow(row.id, { amountMinor: null });
302
+ event.target.setCustomValidity(
303
+ failure instanceof Error ? failure.message : 'Invalid sourced amount.'
304
+ );
305
+ }
306
+ }}
307
+ />
308
+ </label>
309
+ <FreightText
310
+ label={`Row ${index + 1} zero source reason`}
311
+ value={row.zeroReason ?? ''}
312
+ onChange={(zeroReason) => changeRow(row.id, { zeroReason })}
313
+ />
314
+ <FreightText
315
+ label={`Row ${index + 1} supplied quantity`}
316
+ value={row.quantity ?? ''}
317
+ onChange={(quantity) => changeRow(row.id, { quantity })}
318
+ />
319
+ <FreightText
320
+ label={`Row ${index + 1} explicit unit`}
321
+ value={row.unit ?? ''}
322
+ onChange={(unit) => changeRow(row.id, { unit })}
323
+ />
324
+ <FreightText
325
+ label={`Row ${index + 1} supplied unit rate`}
326
+ value={row.unitRateMajor ?? ''}
327
+ onChange={(unitRateMajor) => changeRow(row.id, { unitRateMajor })}
328
+ />
329
+ </div>
330
+ <p>
331
+ {facts.currency.code || 'currency unknown'} / decimal places{' '}
332
+ {facts.currency.minorUnitDigits}. Amount stays as supplied, not quantity × rate.
333
+ </p>
334
+ <Button
335
+ type="button"
336
+ variant="outline"
337
+ disabled={facts.rows.length === 1}
338
+ onClick={() => patch({ rows: facts.rows.filter((r) => r.id !== row.id) })}
339
+ >
340
+ Remove row {index + 1}
341
+ </Button>
342
+ </fieldset>
343
+ ))}
344
+ <Button
345
+ type="button"
346
+ variant="outline"
347
+ disabled={facts.rows.length >= 100}
348
+ onClick={() =>
349
+ patch({
350
+ rows: [
351
+ ...facts.rows,
352
+ {
353
+ id: crypto.randomUUID(),
354
+ description: '',
355
+ estimateLineId: null,
356
+ currency: { ...facts.currency },
357
+ payerReference: null,
358
+ payerLabel: null,
359
+ quantity: null,
360
+ unit: null,
361
+ unitRateMajor: null,
362
+ amountMinor: null,
363
+ zeroReason: null,
364
+ },
365
+ ],
366
+ })
367
+ }
368
+ >
369
+ Add reported row
370
+ </Button>
371
+ <ContainerEvidencePicker
372
+ currentOnly
373
+ value={evidence}
374
+ onChange={setEvidence}
375
+ description="Choose current readable ACTIVE Files versions with an integrity fingerprint. The server rechecks eligibility when recording; no binary is uploaded here."
376
+ />
377
+ {previous && (
378
+ <FreightText
379
+ label="Correction source reason"
380
+ value={reason}
381
+ required={false}
382
+ onChange={setReason}
383
+ />
384
+ )}
385
+ <div className="flex flex-wrap gap-3">
386
+ <Button type="button" variant="outline" onClick={() => void previewNow()}>
387
+ Preview reported comparison
388
+ </Button>
389
+ <Button
390
+ type="submit"
391
+ disabled={
392
+ !canRecord ||
393
+ current?.recordable !== true ||
394
+ !evidence.length ||
395
+ (!!previous && !reason.trim())
396
+ }
397
+ >
398
+ Record immutable comparison
399
+ </Button>
400
+ </div>
401
+ </fieldset>
402
+ {!canRecord && (
403
+ <p>
404
+ Shipment update permission is required to record; structural preview remains read-only.
405
+ </p>
406
+ )}
407
+ {previewing && <p role="status">Comparing the exact recorded facts…</p>}
408
+ {error && <p role="alert">{error}</p>}
409
+ {current && <InvoiceResults value={current} />}
410
+ </form>
411
+ );
412
+ }