@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.
- 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-cost/FreightCommercialEditor.js +217 -0
- package/dist/components/freight-cost/FreightCommercialFactEditor.js +217 -0
- package/dist/components/freight-cost/FreightCommercialHistory.js +115 -0
- package/dist/components/freight-cost/FreightDraftForm.js +106 -91
- package/dist/components/freight-cost/FreightHistory.js +42 -40
- 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 +955 -165
- 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/useFreightRecoveryAction.js +20 -20
- package/dist/hooks/useInvoiceComparisonRecovery.js +118 -0
- package/dist/movethewheels/components/ContainerEvidencePicker.d.ts +2 -1
- package/dist/movethewheels/components/freight-cost/FreightCommercialEditor.d.ts +8 -0
- package/dist/movethewheels/components/freight-cost/FreightCommercialFactEditor.d.ts +8 -0
- package/dist/movethewheels/components/freight-cost/FreightCommercialHistory.d.ts +4 -0
- package/dist/movethewheels/components/freight-cost/FreightDraftForm.d.ts +4 -2
- package/dist/movethewheels/components/freight-cost/FreightHistory.d.ts +2 -2
- 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/freightCommercial.d.ts +44 -0
- package/dist/movethewheels/utils/invoiceComparison.d.ts +32 -0
- package/dist/pages/DriverDetailsPage.js +306 -282
- package/dist/pages/FreightEstimatePage.js +97 -92
- 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 -323
- 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/freightAcknowledgement.js +6 -5
- package/dist/utils/freightCommercial.js +194 -0
- package/dist/utils/freightRecovery.js +22 -22
- package/dist/utils/invoiceComparison.js +272 -0
- package/package.json +30 -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 +373 -349
- 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-cost/FreightCommercialEditor.tsx +236 -0
- package/src/movethewheels/components/freight-cost/FreightCommercialFactEditor.tsx +258 -0
- package/src/movethewheels/components/freight-cost/FreightCommercialHistory.tsx +90 -0
- package/src/movethewheels/components/freight-cost/FreightDraftForm.tsx +33 -4
- package/src/movethewheels/components/freight-cost/FreightHistory.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/useFreightRecoveryAction.ts +2 -1
- package/src/movethewheels/hooks/useInvoiceComparisonRecovery.ts +227 -0
- package/src/movethewheels/pages/DriverDetailsPage.tsx +35 -9
- package/src/movethewheels/pages/FreightEstimatePage.tsx +15 -7
- package/src/movethewheels/pages/FreightInvoicePage.tsx +477 -0
- package/src/movethewheels/utils/driverLicenseExpiry.ts +25 -0
- package/src/movethewheels/utils/freightAcknowledgement.ts +3 -1
- package/src/movethewheels/utils/freightCommercial.ts +388 -0
- package/src/movethewheels/utils/freightRecovery.ts +6 -2
- package/src/movethewheels/utils/invoiceComparison.ts +520 -0
- package/src/operations/freight-cost/commercial.graphql +483 -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
|
@@ -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: (
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
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:
|
|
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
|
+
}
|