@burdenoff/microfe-movethewheels 2026.1016.2 → 2026.1016.3

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 (31) hide show
  1. package/dist/components/freight-cost/FreightCommercialEditor.js +217 -0
  2. package/dist/components/freight-cost/FreightCommercialFactEditor.js +217 -0
  3. package/dist/components/freight-cost/FreightCommercialHistory.js +115 -0
  4. package/dist/components/freight-cost/FreightDraftForm.js +106 -91
  5. package/dist/components/freight-cost/FreightHistory.js +42 -40
  6. package/dist/generated/freight-cost-operations.d.ts +959 -0
  7. package/dist/hooks/useFreightRecoveryAction.js +20 -20
  8. package/dist/movethewheels/components/freight-cost/FreightCommercialEditor.d.ts +8 -0
  9. package/dist/movethewheels/components/freight-cost/FreightCommercialFactEditor.d.ts +8 -0
  10. package/dist/movethewheels/components/freight-cost/FreightCommercialHistory.d.ts +4 -0
  11. package/dist/movethewheels/components/freight-cost/FreightDraftForm.d.ts +4 -2
  12. package/dist/movethewheels/components/freight-cost/FreightHistory.d.ts +2 -2
  13. package/dist/movethewheels/utils/freightCommercial.d.ts +44 -0
  14. package/dist/pages/FreightEstimatePage.js +96 -92
  15. package/dist/src/generated/freight-cost-operations.js +495 -6
  16. package/dist/utils/freightAcknowledgement.js +6 -5
  17. package/dist/utils/freightCommercial.js +194 -0
  18. package/dist/utils/freightRecovery.js +22 -22
  19. package/package.json +4 -3
  20. package/src/generated/freight-cost-operations.ts +831 -1
  21. package/src/movethewheels/components/freight-cost/FreightCommercialEditor.tsx +236 -0
  22. package/src/movethewheels/components/freight-cost/FreightCommercialFactEditor.tsx +258 -0
  23. package/src/movethewheels/components/freight-cost/FreightCommercialHistory.tsx +90 -0
  24. package/src/movethewheels/components/freight-cost/FreightDraftForm.tsx +33 -4
  25. package/src/movethewheels/components/freight-cost/FreightHistory.tsx +4 -2
  26. package/src/movethewheels/hooks/useFreightRecoveryAction.ts +2 -1
  27. package/src/movethewheels/pages/FreightEstimatePage.tsx +14 -7
  28. package/src/movethewheels/utils/freightAcknowledgement.ts +3 -1
  29. package/src/movethewheels/utils/freightCommercial.ts +388 -0
  30. package/src/movethewheels/utils/freightRecovery.ts +6 -2
  31. package/src/operations/freight-cost/commercial.graphql +483 -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
+ }
@@ -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: (facts: MtwFreightFactsInput, evidence: SelectedEvidence[]) => Promise<void>;
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
- void onSave(structuredClone(facts), structuredClone(evidence)).catch((failure) =>
69
- setError(failure instanceof Error ? failure.message : 'Unable to save this cost record.')
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 type { FreightVersionQuery } from '../../../generated/freight-cost-operations';
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: FreightVersionQuery['mtwFreightEstimateVersion'];
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">