@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
@@ -1,11 +1,5 @@
1
- // @ts-nocheck
2
1
  export type Maybe<T> = T | null;
3
2
  export type InputMaybe<T> = Maybe<T>;
4
- export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
5
- export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
6
- export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
7
- export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
8
- export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
9
3
  /** All built-in and custom scalars, mapped to their actual values */
10
4
  export type Scalars = {
11
5
  ID: { input: string; output: string; }
@@ -25,8 +19,8 @@ export type Scalars = {
25
19
  JSONObject: { input: Record<string, unknown>; output: Record<string, unknown>; }
26
20
  /** Upload scalar (file upload) */
27
21
  Upload: { input: File; output: File; }
28
- _ScalarsPlaceholder: { input: any; output: any; }
29
- _SecurityMainPlaceholder: { input: any; output: any; }
22
+ _ScalarsPlaceholder: { input: unknown; output: unknown; }
23
+ _SecurityMainPlaceholder: { input: unknown; output: unknown; }
30
24
  };
31
25
 
32
26
  export type A2AConnection = {
@@ -265,6 +265,12 @@ const NavRegistrar: FC<Pick<MoveTheWheelsRootProps, 'registerNavItems' | 'basePa
265
265
  icon: <DollarSign size={16} />,
266
266
  order: 9,
267
267
  children: [
268
+ {
269
+ id: 'mtw-freight-invoice-comparisons',
270
+ label: tr('movethewheels.root.nav.freightInvoices', 'Freight invoice comparisons'),
271
+ path: joinPath(basePath, 'finance/freight-invoice-comparisons'),
272
+ icon: <DollarSign size={16} />,
273
+ },
268
274
  {
269
275
  id: 'mtw-freight-estimates',
270
276
  label: tr('movethewheels.root.nav.freightEstimates', 'Freight cost estimates'),
@@ -75,6 +75,7 @@ const RouteDetailsPage = lazy(() => import('./pages/RouteDetailsPage'));
75
75
  const AnalyticsPage = lazy(() => import('./pages/AnalyticsPage'));
76
76
  const FinancePage = lazy(() => import('./pages/FinancePage'));
77
77
  const FreightEstimatePage = lazy(() => import('./pages/FreightEstimatePage'));
78
+ const FreightInvoicePage = lazy(() => import('./pages/FreightInvoicePage'));
78
79
  const ConsolidationPage = lazy(() => import('./pages/ConsolidationPage'));
79
80
  const LogisticsProceduresPage = lazy(() => import('./pages/LogisticsProceduresPage'));
80
81
  const MarketplacePage = lazy(() => import('./pages/MarketplacePage'));
@@ -169,6 +170,7 @@ export function MoveTheWheelsRoutes() {
169
170
  {/* Finance */}
170
171
  <Route path="finance" element={<FinancePage />} />
171
172
  <Route path="finance/freight-estimates" element={<FreightEstimatePage />} />
173
+ <Route path="finance/freight-invoice-comparisons" element={<FreightInvoicePage />} />
172
174
 
173
175
  {/* Marketplace */}
174
176
  <Route path="marketplace" element={<MarketplacePage />} />
@@ -1,6 +1,8 @@
1
1
  import { useState } from 'react';
2
2
  import {
3
3
  ContainerEvidenceFilesDocument,
4
+ InvoiceEvidenceVersionsDocument,
5
+ type InvoiceEvidenceVersionsQuery,
4
6
  ContainerEvidenceVersionsDocument,
5
7
  type ContainerEvidenceFilesQuery,
6
8
  type ContainerEvidenceVersionsQuery,
@@ -16,11 +18,13 @@ export interface SelectedEvidence extends MtwContainerEvidenceInput {
16
18
  export function ContainerEvidencePicker({
17
19
  value,
18
20
  onChange,
21
+ currentOnly = false,
19
22
  description = 'Choose exact versions from shared Files. Evidence integrity does not confirm the issuer or certify the container.',
20
23
  }: {
21
24
  value: SelectedEvidence[];
22
25
  onChange: (value: SelectedEvidence[]) => void;
23
26
  description?: string;
27
+ currentOnly?: boolean;
24
28
  }) {
25
29
  const [fileId, setFileId] = useState('');
26
30
  const [fileOffset, setFileOffset] = useState(0);
@@ -28,8 +32,8 @@ export function ContainerEvidencePicker({
28
32
  const files = useConditionQuery<ContainerEvidenceFilesQuery>(ContainerEvidenceFilesDocument, {
29
33
  pagination: { limit: 25, offset: fileOffset },
30
34
  });
31
- const versions = useConditionQuery<ContainerEvidenceVersionsQuery>(
32
- ContainerEvidenceVersionsDocument,
35
+ const versions = useConditionQuery<ContainerEvidenceVersionsQuery | InvoiceEvidenceVersionsQuery>(
36
+ currentOnly ? InvoiceEvidenceVersionsDocument : ContainerEvidenceVersionsDocument,
33
37
  fileId ? { fileId, pagination: { limit: 25, offset: versionOffset } } : null
34
38
  );
35
39
  return (
@@ -87,7 +91,9 @@ export function ContainerEvidencePicker({
87
91
  {versions.loading && <p role="status">Loading file versions…</p>}
88
92
  {versions.data?.getFileVersions.versions.map((version) => {
89
93
  const selected = value.some((item) => item.versionId === version.id);
90
- const valid = /^[a-fA-F0-9]{64}$/.test(version.sha256Hash ?? '');
94
+ const current =
95
+ !currentOnly || ('isCurrentVersion' in version && version.isCurrentVersion === true);
96
+ const valid = current && /^[a-fA-F0-9]{64}$/.test(version.sha256Hash ?? '');
91
97
  const label = `${version.name} — version ${version.versionLabel ?? version.versionNumber}`;
92
98
  return (
93
99
  <label key={version.id} className="flex gap-2 text-sm">
@@ -104,7 +110,9 @@ export function ContainerEvidencePicker({
104
110
  }
105
111
  />
106
112
  {label}
107
- {!valid && ' (integrity fingerprint unavailable)'}
113
+ {!current
114
+ ? ' (not confirmed current)'
115
+ : !valid && ' (integrity fingerprint unavailable)'}
108
116
  </label>
109
117
  );
110
118
  })}
@@ -99,7 +99,7 @@ export function FleetAlertsPanel({
99
99
  <p className="text-sm text-text-secondary">
100
100
  {t(
101
101
  'movethewheels.fleet.alerts.allClear',
102
- 'All clear — every licence is valid and no vehicle is due for service.'
102
+ 'No recorded fleet alerts. This does not certify legal driving eligibility or duty-hours availability.'
103
103
  )}
104
104
  </p>
105
105
  </CardContent>
@@ -133,7 +133,9 @@ export function FleetAlertsPanel({
133
133
  showDot={false}
134
134
  />
135
135
  <span className="flex-1 min-w-0">
136
- <span className="block text-sm font-medium truncate">{alert.message}</span>
136
+ <span className="block text-sm font-medium whitespace-normal break-words">
137
+ {alert.message}
138
+ </span>
137
139
  {detail && <span className="block text-xs text-text-secondary">{detail}</span>}
138
140
  </span>
139
141
  <ChevronRight size={16} aria-hidden="true" className="shrink-0" />
@@ -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
+ }