@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
|
@@ -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,236 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { OptionalFields } from '@burdenoff/fe-libs/ui';
|
|
3
|
+
import {
|
|
4
|
+
FreightCommercialLegsDocument,
|
|
5
|
+
type FreightCommercialLegsQuery,
|
|
6
|
+
} from '../../../generated/freight-cost-operations';
|
|
7
|
+
import { useConditionQuery } from '../../hooks/useContainerCondition';
|
|
8
|
+
import {
|
|
9
|
+
commercialSlots,
|
|
10
|
+
commercialCanonical,
|
|
11
|
+
newCommercialEditor,
|
|
12
|
+
type CommercialEditorState,
|
|
13
|
+
} from '../../utils/freightCommercial';
|
|
14
|
+
import { FreightCommercialFactEditor } from './FreightCommercialFactEditor';
|
|
15
|
+
import { Button } from '../ui';
|
|
16
|
+
import { ReuseState } from '../ReuseControls';
|
|
17
|
+
import type { SelectedEvidence } from '../ContainerEvidencePicker';
|
|
18
|
+
|
|
19
|
+
export function FreightCommercialEditor({
|
|
20
|
+
value,
|
|
21
|
+
onChange,
|
|
22
|
+
shipmentId,
|
|
23
|
+
evidence,
|
|
24
|
+
}: {
|
|
25
|
+
value: CommercialEditorState | null;
|
|
26
|
+
onChange: (value: CommercialEditorState | null) => void;
|
|
27
|
+
shipmentId: string;
|
|
28
|
+
evidence: SelectedEvidence[];
|
|
29
|
+
}) {
|
|
30
|
+
return (
|
|
31
|
+
<OptionalFields
|
|
32
|
+
title="Commercial context (optional)"
|
|
33
|
+
description="Source declarations only. Off by default; no quote acceptance or completeness is inferred."
|
|
34
|
+
defaultOpen={false}
|
|
35
|
+
>
|
|
36
|
+
<label className="flex min-h-11 items-center gap-2">
|
|
37
|
+
<input
|
|
38
|
+
type="checkbox"
|
|
39
|
+
checked={value !== null}
|
|
40
|
+
onChange={(event) => onChange(event.target.checked ? newCommercialEditor() : null)}
|
|
41
|
+
/>
|
|
42
|
+
Include commercial context
|
|
43
|
+
</label>
|
|
44
|
+
{value && (
|
|
45
|
+
<CommercialFields
|
|
46
|
+
value={value}
|
|
47
|
+
onChange={onChange}
|
|
48
|
+
shipmentId={shipmentId}
|
|
49
|
+
evidence={evidence}
|
|
50
|
+
/>
|
|
51
|
+
)}
|
|
52
|
+
</OptionalFields>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
function CommercialFields({
|
|
56
|
+
value,
|
|
57
|
+
onChange,
|
|
58
|
+
shipmentId,
|
|
59
|
+
evidence,
|
|
60
|
+
}: {
|
|
61
|
+
value: CommercialEditorState;
|
|
62
|
+
onChange: (value: CommercialEditorState) => void;
|
|
63
|
+
shipmentId: string;
|
|
64
|
+
evidence: SelectedEvidence[];
|
|
65
|
+
}) {
|
|
66
|
+
const [revision, setRevision] = useState(0);
|
|
67
|
+
const [offset, setOffset] = useState(0);
|
|
68
|
+
const legs = useConditionQuery<FreightCommercialLegsQuery>(
|
|
69
|
+
FreightCommercialLegsDocument,
|
|
70
|
+
{ shipmentId, pagination: { limit: 25, offset } },
|
|
71
|
+
revision
|
|
72
|
+
);
|
|
73
|
+
const current = legs.data?.shipment?.id === shipmentId ? legs.data.shipment.legs : undefined;
|
|
74
|
+
// Only this page is loaded. Off-page pins retain their original revisions; the server checks all selected pins.
|
|
75
|
+
const matching =
|
|
76
|
+
!!current &&
|
|
77
|
+
current.every(
|
|
78
|
+
(leg) =>
|
|
79
|
+
leg.shipmentId === shipmentId &&
|
|
80
|
+
!value.selectedLegs.some(
|
|
81
|
+
(pin) => pin.id === leg.id && pin.expectedUpdatedAt !== leg.updatedAt
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
const changePage = (next: number) => {
|
|
85
|
+
onChange({ ...value, reviewed: null });
|
|
86
|
+
setOffset(next);
|
|
87
|
+
};
|
|
88
|
+
return (
|
|
89
|
+
<div className="min-w-0 space-y-5">
|
|
90
|
+
<p className="text-sm">
|
|
91
|
+
All eight facts start Unknown: “Not recorded for this cost version”. Edit that explanation
|
|
92
|
+
when useful. Sourced and not-applicable declarations require explicit provenance; shipment
|
|
93
|
+
details are never copied into these facts.
|
|
94
|
+
</p>
|
|
95
|
+
<section className="space-y-3 rounded border p-3">
|
|
96
|
+
<h3 className="font-semibold">Review selected shipment legs</h3>
|
|
97
|
+
<p className="text-sm">
|
|
98
|
+
Select only the legs covered by this source. No selection does not mean all legs. Review
|
|
99
|
+
the loaded selection explicitly before saving.
|
|
100
|
+
</p>
|
|
101
|
+
<ReuseState loading={legs.loading} error={legs.error} />
|
|
102
|
+
{current?.map((leg) => {
|
|
103
|
+
const selected = value.selectedLegs.some((pin) => pin.id === leg.id);
|
|
104
|
+
return (
|
|
105
|
+
<label key={leg.id} className="flex min-h-11 items-center gap-2">
|
|
106
|
+
<input
|
|
107
|
+
type="checkbox"
|
|
108
|
+
checked={selected}
|
|
109
|
+
disabled={!selected && value.selectedLegs.length >= 16}
|
|
110
|
+
onChange={() =>
|
|
111
|
+
onChange({
|
|
112
|
+
...value,
|
|
113
|
+
reviewed: null,
|
|
114
|
+
selectedLegs: selected
|
|
115
|
+
? value.selectedLegs.filter((pin) => pin.id !== leg.id)
|
|
116
|
+
: [...value.selectedLegs, { id: leg.id, expectedUpdatedAt: leg.updatedAt }],
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
/>
|
|
120
|
+
<span>
|
|
121
|
+
Leg {leg.legNumber} — {leg.mode}; carrier {leg.carrierName ?? 'not recorded'};
|
|
122
|
+
updated {leg.updatedAt}
|
|
123
|
+
</span>
|
|
124
|
+
</label>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
{current?.length === 0 && <p>No legs on this loaded page.</p>}
|
|
128
|
+
<p className="text-sm">
|
|
129
|
+
Leg page {Math.floor(offset / 25) + 1}; at most 25 records per page. Off-page selections
|
|
130
|
+
keep their original revisions.
|
|
131
|
+
</p>
|
|
132
|
+
<div className="flex flex-wrap gap-3">
|
|
133
|
+
<Button
|
|
134
|
+
type="button"
|
|
135
|
+
variant="outline"
|
|
136
|
+
disabled={offset === 0 || legs.loading}
|
|
137
|
+
onClick={() => changePage(Math.max(0, offset - 25))}
|
|
138
|
+
>
|
|
139
|
+
Previous leg page
|
|
140
|
+
</Button>
|
|
141
|
+
<Button
|
|
142
|
+
type="button"
|
|
143
|
+
variant="outline"
|
|
144
|
+
disabled={legs.loading || !!legs.error || current?.length !== 25}
|
|
145
|
+
onClick={() => changePage(offset + 25)}
|
|
146
|
+
>
|
|
147
|
+
Next leg page
|
|
148
|
+
</Button>
|
|
149
|
+
</div>
|
|
150
|
+
{value.selectedLegs.map((pin) => (
|
|
151
|
+
<div key={pin.id} className="flex flex-wrap items-center gap-2 break-all text-sm">
|
|
152
|
+
<p>
|
|
153
|
+
Selected leg {pin.id}; pinned revision {pin.expectedUpdatedAt}.
|
|
154
|
+
</p>
|
|
155
|
+
<Button
|
|
156
|
+
type="button"
|
|
157
|
+
variant="outline"
|
|
158
|
+
onClick={() =>
|
|
159
|
+
onChange({
|
|
160
|
+
...value,
|
|
161
|
+
reviewed: null,
|
|
162
|
+
selectedLegs: value.selectedLegs.filter((row) => row.id !== pin.id),
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
>
|
|
166
|
+
Remove selected leg {pin.id}
|
|
167
|
+
</Button>
|
|
168
|
+
</div>
|
|
169
|
+
))}
|
|
170
|
+
<p>{value.selectedLegs.length} of 16 legs selected.</p>
|
|
171
|
+
{!matching && value.selectedLegs.length > 0 && (
|
|
172
|
+
<p role="alert">
|
|
173
|
+
A selected leg on this page changed or the page does not match this shipment. Explicitly
|
|
174
|
+
remove it and review the current selection; no revision is rebound automatically.
|
|
175
|
+
</p>
|
|
176
|
+
)}
|
|
177
|
+
<div className="flex flex-wrap gap-3">
|
|
178
|
+
<Button
|
|
179
|
+
type="button"
|
|
180
|
+
variant="outline"
|
|
181
|
+
onClick={() => {
|
|
182
|
+
onChange({ ...value, reviewed: null });
|
|
183
|
+
setRevision((n) => n + 1);
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
Refresh legs for review
|
|
187
|
+
</Button>
|
|
188
|
+
<Button
|
|
189
|
+
type="button"
|
|
190
|
+
variant="outline"
|
|
191
|
+
disabled={legs.loading || !!legs.error || !matching}
|
|
192
|
+
onClick={() =>
|
|
193
|
+
onChange({ ...value, reviewed: commercialCanonical(value.selectedLegs) })
|
|
194
|
+
}
|
|
195
|
+
>
|
|
196
|
+
I reviewed this leg selection
|
|
197
|
+
</Button>
|
|
198
|
+
</div>
|
|
199
|
+
{value.reviewed === commercialCanonical(value.selectedLegs) && matching && (
|
|
200
|
+
<p role="status">
|
|
201
|
+
This explicit selection of IDs and revisions is reviewed. The server checks every
|
|
202
|
+
selected leg on save, including off-page pins.
|
|
203
|
+
</p>
|
|
204
|
+
)}
|
|
205
|
+
</section>
|
|
206
|
+
{(
|
|
207
|
+
[
|
|
208
|
+
['origin', 'destination'],
|
|
209
|
+
['carrier', 'service', 'cargo'],
|
|
210
|
+
['weight', 'volume', 'equipment'],
|
|
211
|
+
] as const
|
|
212
|
+
).map((group, index) => (
|
|
213
|
+
<section key={group[0]} className="space-y-3">
|
|
214
|
+
<h3 className="font-semibold">
|
|
215
|
+
{['Locations', 'Carrier, service and cargo', 'Measurements and equipment'][index]}
|
|
216
|
+
</h3>
|
|
217
|
+
<div className="grid min-w-0 gap-3 md:grid-cols-2">
|
|
218
|
+
{group.map((slot) => (
|
|
219
|
+
<FreightCommercialFactEditor
|
|
220
|
+
key={slot}
|
|
221
|
+
slot={slot}
|
|
222
|
+
value={value.facts[slot]}
|
|
223
|
+
evidence={evidence}
|
|
224
|
+
onChange={(fact) => onChange({ ...value, facts: { ...value.facts, [slot]: fact } })}
|
|
225
|
+
/>
|
|
226
|
+
))}
|
|
227
|
+
</div>
|
|
228
|
+
</section>
|
|
229
|
+
))}
|
|
230
|
+
<p className="text-sm">
|
|
231
|
+
{commercialSlots.length} declared facts. Owner shipment and leg snapshots are recorded
|
|
232
|
+
separately by the server.
|
|
233
|
+
</p>
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Button } from '../ui';
|
|
3
|
+
import { FreightSelect, FreightText } from './FreightFields';
|
|
4
|
+
import { freightInstant, freightLocalTime } from '../../utils/freightDraft';
|
|
5
|
+
import {
|
|
6
|
+
newCommercialFact,
|
|
7
|
+
type CommercialSlot,
|
|
8
|
+
type CommercialFactDraft,
|
|
9
|
+
} from '../../utils/freightCommercial';
|
|
10
|
+
import type { SelectedEvidence } from '../ContainerEvidencePicker';
|
|
11
|
+
|
|
12
|
+
export function FreightCommercialFactEditor({
|
|
13
|
+
slot,
|
|
14
|
+
value,
|
|
15
|
+
onChange,
|
|
16
|
+
evidence,
|
|
17
|
+
}: {
|
|
18
|
+
slot: CommercialSlot;
|
|
19
|
+
value: CommercialFactDraft;
|
|
20
|
+
onChange: (value: CommercialFactDraft) => void;
|
|
21
|
+
evidence: SelectedEvidence[];
|
|
22
|
+
}) {
|
|
23
|
+
const [editingReason, setEditingReason] = useState(false);
|
|
24
|
+
const label = slot[0].toUpperCase() + slot.slice(1);
|
|
25
|
+
const patch = (values: Partial<CommercialFactDraft>) => onChange({ ...value, ...values });
|
|
26
|
+
return (
|
|
27
|
+
<fieldset className="min-w-0 space-y-3 rounded border p-3">
|
|
28
|
+
<legend className="px-1 font-semibold">{label}</legend>
|
|
29
|
+
<FreightSelect
|
|
30
|
+
label={`${label} status`}
|
|
31
|
+
value={value.status}
|
|
32
|
+
onChange={(status) => {
|
|
33
|
+
if (status === 'UNKNOWN' || status === 'SOURCED' || status === 'NOT_APPLICABLE') {
|
|
34
|
+
setEditingReason(false);
|
|
35
|
+
onChange(newCommercialFact(status));
|
|
36
|
+
}
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<option value="UNKNOWN">Unknown — not recorded</option>
|
|
40
|
+
<option value="SOURCED">Sourced</option>
|
|
41
|
+
<option value="NOT_APPLICABLE">Not applicable</option>
|
|
42
|
+
</FreightSelect>
|
|
43
|
+
{value.status === 'UNKNOWN' && !editingReason && (
|
|
44
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
45
|
+
<p>{value.reason}</p>
|
|
46
|
+
<Button type="button" variant="outline" onClick={() => setEditingReason(true)}>
|
|
47
|
+
Edit {label.toLowerCase()} reason
|
|
48
|
+
</Button>
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
{(value.status === 'NOT_APPLICABLE' || (value.status === 'UNKNOWN' && editingReason)) && (
|
|
52
|
+
<FreightText
|
|
53
|
+
label={`${label} reason`}
|
|
54
|
+
value={value.reason}
|
|
55
|
+
required
|
|
56
|
+
onChange={(reason) => patch({ reason })}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
{value.status === 'SOURCED' && (
|
|
60
|
+
<CommercialFactValue slot={slot} label={label} value={value} patch={patch} />
|
|
61
|
+
)}
|
|
62
|
+
{value.status !== 'UNKNOWN' && (
|
|
63
|
+
<CommercialFactSource label={label} value={value} patch={patch} evidence={evidence} />
|
|
64
|
+
)}
|
|
65
|
+
</fieldset>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface FactFieldsProps {
|
|
70
|
+
label: string;
|
|
71
|
+
value: CommercialFactDraft;
|
|
72
|
+
patch: (values: Partial<CommercialFactDraft>) => void;
|
|
73
|
+
}
|
|
74
|
+
function CommercialFactValue({
|
|
75
|
+
slot,
|
|
76
|
+
label,
|
|
77
|
+
value,
|
|
78
|
+
patch,
|
|
79
|
+
}: FactFieldsProps & { slot: CommercialSlot }) {
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
{(slot === 'origin' || slot === 'destination') && (
|
|
83
|
+
<>
|
|
84
|
+
<FreightSelect
|
|
85
|
+
label={`${label} label kind`}
|
|
86
|
+
value={value.kind}
|
|
87
|
+
onChange={(kind) => patch({ kind })}
|
|
88
|
+
>
|
|
89
|
+
<option value="">Choose a label kind</option>
|
|
90
|
+
{[
|
|
91
|
+
'ADDRESS_LABEL',
|
|
92
|
+
'PORT_LABEL',
|
|
93
|
+
'TERMINAL_LABEL',
|
|
94
|
+
'POSTCODE_LABEL',
|
|
95
|
+
'CUSTOM_LABEL',
|
|
96
|
+
].map((kind) => (
|
|
97
|
+
<option key={kind} value={kind}>
|
|
98
|
+
{kind.replaceAll('_', ' ').toLowerCase()}
|
|
99
|
+
</option>
|
|
100
|
+
))}
|
|
101
|
+
</FreightSelect>
|
|
102
|
+
<FreightText
|
|
103
|
+
label={`${label} jurisdiction label (optional)`}
|
|
104
|
+
value={value.jurisdiction}
|
|
105
|
+
onChange={(jurisdiction) => patch({ jurisdiction })}
|
|
106
|
+
/>
|
|
107
|
+
</>
|
|
108
|
+
)}
|
|
109
|
+
{slot === 'carrier' && (
|
|
110
|
+
<FreightText
|
|
111
|
+
label="Carrier reference"
|
|
112
|
+
value={value.reference}
|
|
113
|
+
required
|
|
114
|
+
onChange={(reference) => patch({ reference })}
|
|
115
|
+
/>
|
|
116
|
+
)}
|
|
117
|
+
{slot !== 'weight' && slot !== 'volume' && (
|
|
118
|
+
<FreightText
|
|
119
|
+
label={
|
|
120
|
+
slot === 'cargo'
|
|
121
|
+
? 'Cargo classification label'
|
|
122
|
+
: slot === 'equipment'
|
|
123
|
+
? 'Equipment type label'
|
|
124
|
+
: `${label} label`
|
|
125
|
+
}
|
|
126
|
+
value={value.label}
|
|
127
|
+
required
|
|
128
|
+
onChange={(label) => patch({ label })}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
{slot === 'cargo' && (
|
|
132
|
+
<FreightText
|
|
133
|
+
label="Cargo description"
|
|
134
|
+
value={value.description}
|
|
135
|
+
required
|
|
136
|
+
onChange={(description) => patch({ description })}
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
139
|
+
{(slot === 'weight' || slot === 'volume') && (
|
|
140
|
+
<>
|
|
141
|
+
<FreightText
|
|
142
|
+
label={`${label} amount`}
|
|
143
|
+
value={value.amount}
|
|
144
|
+
required
|
|
145
|
+
onChange={(amount) =>
|
|
146
|
+
patch({ amount, zeroReason: amount === '0' ? value.zeroReason : '' })
|
|
147
|
+
}
|
|
148
|
+
/>
|
|
149
|
+
<FreightSelect
|
|
150
|
+
label={`${label} recorded unit`}
|
|
151
|
+
value={value.unit}
|
|
152
|
+
onChange={(unit) => patch({ unit })}
|
|
153
|
+
>
|
|
154
|
+
<option value="">Choose a unit</option>
|
|
155
|
+
{(slot === 'weight' ? ['KG', 'TONNE', 'LB'] : ['M3', 'FT3']).map((unit) => (
|
|
156
|
+
<option key={unit} value={unit}>
|
|
157
|
+
{unit}
|
|
158
|
+
</option>
|
|
159
|
+
))}
|
|
160
|
+
</FreightSelect>
|
|
161
|
+
{value.amount === '0' && (
|
|
162
|
+
<FreightText
|
|
163
|
+
label={`${label} recorded zero reason`}
|
|
164
|
+
value={value.zeroReason}
|
|
165
|
+
required
|
|
166
|
+
onChange={(zeroReason) => patch({ zeroReason })}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
<p className="text-sm">Record the source unit. No conversion is inferred.</p>
|
|
170
|
+
</>
|
|
171
|
+
)}
|
|
172
|
+
{slot === 'equipment' && (
|
|
173
|
+
<FreightText
|
|
174
|
+
label="Equipment count"
|
|
175
|
+
value={value.count}
|
|
176
|
+
required
|
|
177
|
+
onChange={(count) => patch({ count })}
|
|
178
|
+
/>
|
|
179
|
+
)}
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
function CommercialFactSource({
|
|
184
|
+
label,
|
|
185
|
+
value,
|
|
186
|
+
patch,
|
|
187
|
+
evidence,
|
|
188
|
+
}: FactFieldsProps & { evidence: SelectedEvidence[] }) {
|
|
189
|
+
return (
|
|
190
|
+
<>
|
|
191
|
+
<h4 className="font-medium">{label} source declaration</h4>
|
|
192
|
+
<FreightText
|
|
193
|
+
label={`${label} source reference`}
|
|
194
|
+
value={value.sourceReference}
|
|
195
|
+
required
|
|
196
|
+
onChange={(sourceReference) => patch({ sourceReference })}
|
|
197
|
+
/>
|
|
198
|
+
<FreightText
|
|
199
|
+
label={`${label} source revision`}
|
|
200
|
+
value={value.sourceRevision}
|
|
201
|
+
required
|
|
202
|
+
onChange={(sourceRevision) => patch({ sourceRevision })}
|
|
203
|
+
/>
|
|
204
|
+
<FreightText
|
|
205
|
+
label={`${label} observed at (local time, optional)`}
|
|
206
|
+
type="datetime-local"
|
|
207
|
+
value={value.observedAt ? freightLocalTime(value.observedAt) : ''}
|
|
208
|
+
onChange={(time) => patch({ observedAt: time ? freightInstant(time) : '' })}
|
|
209
|
+
/>
|
|
210
|
+
<p className="text-sm">
|
|
211
|
+
Blank observation time is recorded as Unknown. No date is inferred from the upload or file
|
|
212
|
+
check.
|
|
213
|
+
</p>
|
|
214
|
+
<p className="text-sm">
|
|
215
|
+
Choose 1–2 versions from the evidence selected below for this record. This declares the
|
|
216
|
+
source; it does not verify its authority.
|
|
217
|
+
</p>
|
|
218
|
+
{evidence.map((item) => {
|
|
219
|
+
const selected = value.evidenceLinks.some(
|
|
220
|
+
(link) => link.fileId === item.fileId && link.versionId === item.versionId
|
|
221
|
+
);
|
|
222
|
+
return (
|
|
223
|
+
<label
|
|
224
|
+
key={item.versionId}
|
|
225
|
+
className="flex min-h-11 items-center gap-2 break-words text-sm"
|
|
226
|
+
>
|
|
227
|
+
<input
|
|
228
|
+
type="checkbox"
|
|
229
|
+
checked={selected}
|
|
230
|
+
disabled={!selected && value.evidenceLinks.length >= 2}
|
|
231
|
+
onChange={() =>
|
|
232
|
+
patch({
|
|
233
|
+
evidenceLinks: selected
|
|
234
|
+
? value.evidenceLinks.filter(
|
|
235
|
+
(link) =>
|
|
236
|
+
!(link.fileId === item.fileId && link.versionId === item.versionId)
|
|
237
|
+
)
|
|
238
|
+
: [...value.evidenceLinks, { fileId: item.fileId, versionId: item.versionId }],
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
/>
|
|
242
|
+
{item.label}
|
|
243
|
+
</label>
|
|
244
|
+
);
|
|
245
|
+
})}
|
|
246
|
+
{!evidence.length && <p>Select an exact Files version in Evidence for this record.</p>}
|
|
247
|
+
{value.evidenceLinks.some(
|
|
248
|
+
(link) =>
|
|
249
|
+
!evidence.some((item) => item.fileId === link.fileId && item.versionId === link.versionId)
|
|
250
|
+
) && (
|
|
251
|
+
<p role="alert">
|
|
252
|
+
A linked evidence version was removed. Select the fact status again and review its source
|
|
253
|
+
links before saving.
|
|
254
|
+
</p>
|
|
255
|
+
)}
|
|
256
|
+
</>
|
|
257
|
+
);
|
|
258
|
+
}
|