@burdenoff/microfe-movethewheels 2026.1016.2 → 2026.1016.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +10 -0
  2. package/dist/MoveTheWheelsRoot.js +5 -0
  3. package/dist/MoveTheWheelsRoutes.js +25 -21
  4. package/dist/components/ContainerEvidencePicker.js +55 -55
  5. package/dist/components/FleetAlertsPanel.js +2 -2
  6. package/dist/components/freight-cost/FreightCommercialEditor.js +217 -0
  7. package/dist/components/freight-cost/FreightCommercialFactEditor.js +217 -0
  8. package/dist/components/freight-cost/FreightCommercialHistory.js +115 -0
  9. package/dist/components/freight-cost/FreightDraftForm.js +106 -91
  10. package/dist/components/freight-cost/FreightHistory.js +42 -40
  11. package/dist/components/freight-invoice/InvoiceForm.js +366 -0
  12. package/dist/components/freight-invoice/InvoiceResults.js +240 -0
  13. package/dist/generated/consolidation-operations.d.ts +195 -265
  14. package/dist/generated/container-condition-operations.d.ts +124 -187
  15. package/dist/generated/container-operations.d.ts +59 -85
  16. package/dist/generated/empty-reuse-operations.d.ts +165 -271
  17. package/dist/generated/free-time-operations.d.ts +120 -184
  18. package/dist/generated/freight-cost-operations.d.ts +955 -165
  19. package/dist/generated/freight-invoice-operations.d.ts +1447 -0
  20. package/dist/generated/logistics-procedure-operations.d.ts +100 -156
  21. package/dist/generated/pending-pick-operations.d.ts +769 -1025
  22. package/dist/generated/shipment-status-authority-operations.d.ts +82 -103
  23. package/dist/generated/stock-binding-operations.d.ts +188 -260
  24. package/dist/generated/stock-observation-operations.d.ts +103 -138
  25. package/dist/generated/tracking-quality-operations.d.ts +27 -41
  26. package/dist/generated/trip-utilization-operations.d.ts +137 -185
  27. package/dist/generated/wspace-operations.d.ts +628 -2272
  28. package/dist/generated/wspace-types.d.ts +4 -23
  29. package/dist/hooks/useFreightRecoveryAction.js +20 -20
  30. package/dist/hooks/useInvoiceComparisonRecovery.js +118 -0
  31. package/dist/movethewheels/components/ContainerEvidencePicker.d.ts +2 -1
  32. package/dist/movethewheels/components/freight-cost/FreightCommercialEditor.d.ts +8 -0
  33. package/dist/movethewheels/components/freight-cost/FreightCommercialFactEditor.d.ts +8 -0
  34. package/dist/movethewheels/components/freight-cost/FreightCommercialHistory.d.ts +4 -0
  35. package/dist/movethewheels/components/freight-cost/FreightDraftForm.d.ts +4 -2
  36. package/dist/movethewheels/components/freight-cost/FreightHistory.d.ts +2 -2
  37. package/dist/movethewheels/components/freight-invoice/InvoiceForm.d.ts +19 -0
  38. package/dist/movethewheels/components/freight-invoice/InvoiceResults.d.ts +9 -0
  39. package/dist/movethewheels/hooks/useFleet.d.ts +1 -1
  40. package/dist/movethewheels/hooks/useFleetIntelligence.d.ts +1 -1
  41. package/dist/movethewheels/hooks/useInvoiceComparisonRecovery.d.ts +22 -0
  42. package/dist/movethewheels/hooks/useProcedureSelection.d.ts +1 -3
  43. package/dist/movethewheels/pages/FreightInvoicePage.d.ts +10 -0
  44. package/dist/movethewheels/utils/driverLicenseExpiry.d.ts +6 -0
  45. package/dist/movethewheels/utils/freightCommercial.d.ts +44 -0
  46. package/dist/movethewheels/utils/invoiceComparison.d.ts +32 -0
  47. package/dist/pages/DriverDetailsPage.js +306 -282
  48. package/dist/pages/FreightEstimatePage.js +97 -92
  49. package/dist/pages/FreightInvoicePage.js +346 -0
  50. package/dist/src/generated/consolidation-operations.js +3961 -503
  51. package/dist/src/generated/container-condition-operations.js +2968 -206
  52. package/dist/src/generated/container-operations.js +997 -99
  53. package/dist/src/generated/empty-reuse-operations.js +4811 -747
  54. package/dist/src/generated/free-time-operations.js +2824 -233
  55. package/dist/src/generated/freight-cost-operations.js +4852 -323
  56. package/dist/src/generated/freight-invoice-operations.js +4894 -0
  57. package/dist/src/generated/logistics-procedure-operations.js +2707 -360
  58. package/dist/src/generated/pending-pick-operations.js +8244 -1196
  59. package/dist/src/generated/shipment-status-authority-operations.js +1157 -84
  60. package/dist/src/generated/stock-binding-operations.js +2779 -173
  61. package/dist/src/generated/stock-observation-operations.js +1274 -91
  62. package/dist/src/generated/tracking-quality-operations.js +407 -42
  63. package/dist/src/generated/trip-utilization-operations.js +1765 -138
  64. package/dist/src/generated/wspace-operations.js +428 -706
  65. package/dist/utils/driverLicenseExpiry.js +14 -0
  66. package/dist/utils/freightAcknowledgement.js +6 -5
  67. package/dist/utils/freightCommercial.js +194 -0
  68. package/dist/utils/freightRecovery.js +22 -22
  69. package/dist/utils/invoiceComparison.js +272 -0
  70. package/package.json +30 -29
  71. package/src/generated/consolidation-operations.ts +60 -559
  72. package/src/generated/container-condition-operations.ts +63 -265
  73. package/src/generated/container-operations.ts +37 -132
  74. package/src/generated/empty-reuse-operations.ts +83 -824
  75. package/src/generated/free-time-operations.ts +55 -284
  76. package/src/generated/freight-cost-operations.ts +373 -349
  77. package/src/generated/freight-invoice-operations.ts +830 -0
  78. package/src/generated/logistics-procedure-operations.ts +60 -414
  79. package/src/generated/pending-pick-operations.ts +48 -1239
  80. package/src/generated/shipment-status-authority-operations.ts +33 -113
  81. package/src/generated/stock-binding-operations.ts +29 -198
  82. package/src/generated/stock-observation-operations.ts +24 -111
  83. package/src/generated/tracking-quality-operations.ts +19 -57
  84. package/src/generated/trip-utilization-operations.ts +32 -166
  85. package/src/generated/wspace-operations.ts +284 -2906
  86. package/src/generated/wspace-types.ts +2 -8
  87. package/src/movethewheels/MoveTheWheelsRoot.tsx +6 -0
  88. package/src/movethewheels/MoveTheWheelsRoutes.tsx +2 -0
  89. package/src/movethewheels/components/ContainerEvidencePicker.tsx +12 -4
  90. package/src/movethewheels/components/FleetAlertsPanel.tsx +4 -2
  91. package/src/movethewheels/components/freight-cost/FreightCommercialEditor.tsx +236 -0
  92. package/src/movethewheels/components/freight-cost/FreightCommercialFactEditor.tsx +258 -0
  93. package/src/movethewheels/components/freight-cost/FreightCommercialHistory.tsx +90 -0
  94. package/src/movethewheels/components/freight-cost/FreightDraftForm.tsx +33 -4
  95. package/src/movethewheels/components/freight-cost/FreightHistory.tsx +4 -2
  96. package/src/movethewheels/components/freight-invoice/InvoiceForm.tsx +412 -0
  97. package/src/movethewheels/components/freight-invoice/InvoiceResults.tsx +162 -0
  98. package/src/movethewheels/hooks/useFleet.ts +1 -1
  99. package/src/movethewheels/hooks/useFleetIntelligence.ts +5 -1
  100. package/src/movethewheels/hooks/useFreightRecoveryAction.ts +2 -1
  101. package/src/movethewheels/hooks/useInvoiceComparisonRecovery.ts +227 -0
  102. package/src/movethewheels/pages/DriverDetailsPage.tsx +35 -9
  103. package/src/movethewheels/pages/FreightEstimatePage.tsx +15 -7
  104. package/src/movethewheels/pages/FreightInvoicePage.tsx +477 -0
  105. package/src/movethewheels/utils/driverLicenseExpiry.ts +25 -0
  106. package/src/movethewheels/utils/freightAcknowledgement.ts +3 -1
  107. package/src/movethewheels/utils/freightCommercial.ts +388 -0
  108. package/src/movethewheels/utils/freightRecovery.ts +6 -2
  109. package/src/movethewheels/utils/invoiceComparison.ts +520 -0
  110. package/src/operations/freight-cost/commercial.graphql +483 -0
  111. package/src/operations/freight-invoice/comparison.graphql +40 -0
  112. package/src/operations/wspace/freight-invoice-evidence.graphql +6 -0
  113. package/patches/braces@3.0.3.patch +0 -87
@@ -1,11 +1,5 @@
1
- // @ts-nocheck
2
1
  export type Maybe<T> = T | null;
3
2
  export type InputMaybe<T> = Maybe<T>;
4
- export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
5
- export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
6
- export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
7
- export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
8
- export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
9
3
  /** All built-in and custom scalars, mapped to their actual values */
10
4
  export type Scalars = {
11
5
  ID: { input: string; output: string; }
@@ -25,8 +19,8 @@ export type Scalars = {
25
19
  JSONObject: { input: Record<string, unknown>; output: Record<string, unknown>; }
26
20
  /** Upload scalar (file upload) */
27
21
  Upload: { input: File; output: File; }
28
- _ScalarsPlaceholder: { input: any; output: any; }
29
- _SecurityMainPlaceholder: { input: any; output: any; }
22
+ _ScalarsPlaceholder: { input: unknown; output: unknown; }
23
+ _SecurityMainPlaceholder: { input: unknown; output: unknown; }
30
24
  };
31
25
 
32
26
  export type A2AConnection = {
@@ -265,6 +265,12 @@ const NavRegistrar: FC<Pick<MoveTheWheelsRootProps, 'registerNavItems' | 'basePa
265
265
  icon: <DollarSign size={16} />,
266
266
  order: 9,
267
267
  children: [
268
+ {
269
+ id: 'mtw-freight-invoice-comparisons',
270
+ label: tr('movethewheels.root.nav.freightInvoices', 'Freight invoice comparisons'),
271
+ path: joinPath(basePath, 'finance/freight-invoice-comparisons'),
272
+ icon: <DollarSign size={16} />,
273
+ },
268
274
  {
269
275
  id: 'mtw-freight-estimates',
270
276
  label: tr('movethewheels.root.nav.freightEstimates', 'Freight cost estimates'),
@@ -75,6 +75,7 @@ const RouteDetailsPage = lazy(() => import('./pages/RouteDetailsPage'));
75
75
  const AnalyticsPage = lazy(() => import('./pages/AnalyticsPage'));
76
76
  const FinancePage = lazy(() => import('./pages/FinancePage'));
77
77
  const FreightEstimatePage = lazy(() => import('./pages/FreightEstimatePage'));
78
+ const FreightInvoicePage = lazy(() => import('./pages/FreightInvoicePage'));
78
79
  const ConsolidationPage = lazy(() => import('./pages/ConsolidationPage'));
79
80
  const LogisticsProceduresPage = lazy(() => import('./pages/LogisticsProceduresPage'));
80
81
  const MarketplacePage = lazy(() => import('./pages/MarketplacePage'));
@@ -169,6 +170,7 @@ export function MoveTheWheelsRoutes() {
169
170
  {/* Finance */}
170
171
  <Route path="finance" element={<FinancePage />} />
171
172
  <Route path="finance/freight-estimates" element={<FreightEstimatePage />} />
173
+ <Route path="finance/freight-invoice-comparisons" element={<FreightInvoicePage />} />
172
174
 
173
175
  {/* Marketplace */}
174
176
  <Route path="marketplace" element={<MarketplacePage />} />
@@ -1,6 +1,8 @@
1
1
  import { useState } from 'react';
2
2
  import {
3
3
  ContainerEvidenceFilesDocument,
4
+ InvoiceEvidenceVersionsDocument,
5
+ type InvoiceEvidenceVersionsQuery,
4
6
  ContainerEvidenceVersionsDocument,
5
7
  type ContainerEvidenceFilesQuery,
6
8
  type ContainerEvidenceVersionsQuery,
@@ -16,11 +18,13 @@ export interface SelectedEvidence extends MtwContainerEvidenceInput {
16
18
  export function ContainerEvidencePicker({
17
19
  value,
18
20
  onChange,
21
+ currentOnly = false,
19
22
  description = 'Choose exact versions from shared Files. Evidence integrity does not confirm the issuer or certify the container.',
20
23
  }: {
21
24
  value: SelectedEvidence[];
22
25
  onChange: (value: SelectedEvidence[]) => void;
23
26
  description?: string;
27
+ currentOnly?: boolean;
24
28
  }) {
25
29
  const [fileId, setFileId] = useState('');
26
30
  const [fileOffset, setFileOffset] = useState(0);
@@ -28,8 +32,8 @@ export function ContainerEvidencePicker({
28
32
  const files = useConditionQuery<ContainerEvidenceFilesQuery>(ContainerEvidenceFilesDocument, {
29
33
  pagination: { limit: 25, offset: fileOffset },
30
34
  });
31
- const versions = useConditionQuery<ContainerEvidenceVersionsQuery>(
32
- ContainerEvidenceVersionsDocument,
35
+ const versions = useConditionQuery<ContainerEvidenceVersionsQuery | InvoiceEvidenceVersionsQuery>(
36
+ currentOnly ? InvoiceEvidenceVersionsDocument : ContainerEvidenceVersionsDocument,
33
37
  fileId ? { fileId, pagination: { limit: 25, offset: versionOffset } } : null
34
38
  );
35
39
  return (
@@ -87,7 +91,9 @@ export function ContainerEvidencePicker({
87
91
  {versions.loading && <p role="status">Loading file versions…</p>}
88
92
  {versions.data?.getFileVersions.versions.map((version) => {
89
93
  const selected = value.some((item) => item.versionId === version.id);
90
- const valid = /^[a-fA-F0-9]{64}$/.test(version.sha256Hash ?? '');
94
+ const current =
95
+ !currentOnly || ('isCurrentVersion' in version && version.isCurrentVersion === true);
96
+ const valid = current && /^[a-fA-F0-9]{64}$/.test(version.sha256Hash ?? '');
91
97
  const label = `${version.name} — version ${version.versionLabel ?? version.versionNumber}`;
92
98
  return (
93
99
  <label key={version.id} className="flex gap-2 text-sm">
@@ -104,7 +110,9 @@ export function ContainerEvidencePicker({
104
110
  }
105
111
  />
106
112
  {label}
107
- {!valid && ' (integrity fingerprint unavailable)'}
113
+ {!current
114
+ ? ' (not confirmed current)'
115
+ : !valid && ' (integrity fingerprint unavailable)'}
108
116
  </label>
109
117
  );
110
118
  })}
@@ -99,7 +99,7 @@ export function FleetAlertsPanel({
99
99
  <p className="text-sm text-text-secondary">
100
100
  {t(
101
101
  'movethewheels.fleet.alerts.allClear',
102
- 'All clear — every licence is valid and no vehicle is due for service.'
102
+ 'No recorded fleet alerts. This does not certify legal driving eligibility or duty-hours availability.'
103
103
  )}
104
104
  </p>
105
105
  </CardContent>
@@ -133,7 +133,9 @@ export function FleetAlertsPanel({
133
133
  showDot={false}
134
134
  />
135
135
  <span className="flex-1 min-w-0">
136
- <span className="block text-sm font-medium truncate">{alert.message}</span>
136
+ <span className="block text-sm font-medium whitespace-normal break-words">
137
+ {alert.message}
138
+ </span>
137
139
  {detail && <span className="block text-xs text-text-secondary">{detail}</span>}
138
140
  </span>
139
141
  <ChevronRight size={16} aria-hidden="true" className="shrink-0" />
@@ -0,0 +1,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
+ }