@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.
- package/README.md +10 -0
- package/dist/MoveTheWheelsRoot.js +5 -0
- package/dist/MoveTheWheelsRoutes.js +25 -21
- package/dist/components/ContainerEvidencePicker.js +55 -55
- package/dist/components/FleetAlertsPanel.js +2 -2
- package/dist/components/freight-invoice/InvoiceForm.js +366 -0
- package/dist/components/freight-invoice/InvoiceResults.js +240 -0
- package/dist/generated/consolidation-operations.d.ts +195 -265
- package/dist/generated/container-condition-operations.d.ts +124 -187
- package/dist/generated/container-operations.d.ts +59 -85
- package/dist/generated/empty-reuse-operations.d.ts +165 -271
- package/dist/generated/free-time-operations.d.ts +120 -184
- package/dist/generated/freight-cost-operations.d.ts +307 -476
- package/dist/generated/freight-invoice-operations.d.ts +1447 -0
- package/dist/generated/logistics-procedure-operations.d.ts +100 -156
- package/dist/generated/pending-pick-operations.d.ts +769 -1025
- package/dist/generated/shipment-status-authority-operations.d.ts +82 -103
- package/dist/generated/stock-binding-operations.d.ts +188 -260
- package/dist/generated/stock-observation-operations.d.ts +103 -138
- package/dist/generated/tracking-quality-operations.d.ts +27 -41
- package/dist/generated/trip-utilization-operations.d.ts +137 -185
- package/dist/generated/wspace-operations.d.ts +628 -2272
- package/dist/generated/wspace-types.d.ts +4 -23
- package/dist/hooks/useInvoiceComparisonRecovery.js +118 -0
- package/dist/movethewheels/components/ContainerEvidencePicker.d.ts +2 -1
- package/dist/movethewheels/components/freight-invoice/InvoiceForm.d.ts +19 -0
- package/dist/movethewheels/components/freight-invoice/InvoiceResults.d.ts +9 -0
- package/dist/movethewheels/hooks/useFleet.d.ts +1 -1
- package/dist/movethewheels/hooks/useFleetIntelligence.d.ts +1 -1
- package/dist/movethewheels/hooks/useInvoiceComparisonRecovery.d.ts +22 -0
- package/dist/movethewheels/hooks/useProcedureSelection.d.ts +1 -3
- package/dist/movethewheels/pages/FreightInvoicePage.d.ts +10 -0
- package/dist/movethewheels/utils/driverLicenseExpiry.d.ts +6 -0
- package/dist/movethewheels/utils/invoiceComparison.d.ts +32 -0
- package/dist/pages/DriverDetailsPage.js +306 -282
- package/dist/pages/FreightEstimatePage.js +1 -0
- package/dist/pages/FreightInvoicePage.js +346 -0
- package/dist/src/generated/consolidation-operations.js +3961 -503
- package/dist/src/generated/container-condition-operations.js +2968 -206
- package/dist/src/generated/container-operations.js +997 -99
- package/dist/src/generated/empty-reuse-operations.js +4811 -747
- package/dist/src/generated/free-time-operations.js +2824 -233
- package/dist/src/generated/freight-cost-operations.js +4852 -812
- package/dist/src/generated/freight-invoice-operations.js +4894 -0
- package/dist/src/generated/logistics-procedure-operations.js +2707 -360
- package/dist/src/generated/pending-pick-operations.js +8244 -1196
- package/dist/src/generated/shipment-status-authority-operations.js +1157 -84
- package/dist/src/generated/stock-binding-operations.js +2779 -173
- package/dist/src/generated/stock-observation-operations.js +1274 -91
- package/dist/src/generated/tracking-quality-operations.js +407 -42
- package/dist/src/generated/trip-utilization-operations.js +1765 -138
- package/dist/src/generated/wspace-operations.js +428 -706
- package/dist/utils/driverLicenseExpiry.js +14 -0
- package/dist/utils/invoiceComparison.js +272 -0
- package/package.json +29 -29
- package/src/generated/consolidation-operations.ts +60 -559
- package/src/generated/container-condition-operations.ts +63 -265
- package/src/generated/container-operations.ts +37 -132
- package/src/generated/empty-reuse-operations.ts +83 -824
- package/src/generated/free-time-operations.ts +55 -284
- package/src/generated/freight-cost-operations.ts +42 -848
- package/src/generated/freight-invoice-operations.ts +830 -0
- package/src/generated/logistics-procedure-operations.ts +60 -414
- package/src/generated/pending-pick-operations.ts +48 -1239
- package/src/generated/shipment-status-authority-operations.ts +33 -113
- package/src/generated/stock-binding-operations.ts +29 -198
- package/src/generated/stock-observation-operations.ts +24 -111
- package/src/generated/tracking-quality-operations.ts +19 -57
- package/src/generated/trip-utilization-operations.ts +32 -166
- package/src/generated/wspace-operations.ts +284 -2906
- package/src/generated/wspace-types.ts +2 -8
- package/src/movethewheels/MoveTheWheelsRoot.tsx +6 -0
- package/src/movethewheels/MoveTheWheelsRoutes.tsx +2 -0
- package/src/movethewheels/components/ContainerEvidencePicker.tsx +12 -4
- package/src/movethewheels/components/FleetAlertsPanel.tsx +4 -2
- package/src/movethewheels/components/freight-invoice/InvoiceForm.tsx +412 -0
- package/src/movethewheels/components/freight-invoice/InvoiceResults.tsx +162 -0
- package/src/movethewheels/hooks/useFleet.ts +1 -1
- package/src/movethewheels/hooks/useFleetIntelligence.ts +5 -1
- package/src/movethewheels/hooks/useInvoiceComparisonRecovery.ts +227 -0
- package/src/movethewheels/pages/DriverDetailsPage.tsx +35 -9
- package/src/movethewheels/pages/FreightEstimatePage.tsx +1 -0
- package/src/movethewheels/pages/FreightInvoicePage.tsx +477 -0
- package/src/movethewheels/utils/driverLicenseExpiry.ts +25 -0
- package/src/movethewheels/utils/invoiceComparison.ts +520 -0
- package/src/operations/freight-invoice/comparison.graphql +40 -0
- package/src/operations/wspace/freight-invoice-evidence.graphql +6 -0
- 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:
|
|
29
|
-
_SecurityMainPlaceholder: { input:
|
|
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
|
|
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
|
-
{!
|
|
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
|
-
'
|
|
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
|
|
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
|
+
}
|