@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,477 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useSearchParams } from 'react-router';
|
|
3
|
+
import { FileText } from 'lucide-react';
|
|
4
|
+
import { PermissionProvider } from '@burdenoff/fe-libs/shared/providers/shell';
|
|
5
|
+
import { PageLayout } from '../components/PageLayout';
|
|
6
|
+
import { Button } from '../components/ui';
|
|
7
|
+
import { PinnedSelect, ReusePager, ReuseState, reuseCard } from '../components/ReuseControls';
|
|
8
|
+
import { FreightText } from '../components/freight-cost/FreightFields';
|
|
9
|
+
import { InvoiceForm } from '../components/freight-invoice/InvoiceForm';
|
|
10
|
+
import { InvoiceHistoryRecord } from '../components/freight-invoice/InvoiceResults';
|
|
11
|
+
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
12
|
+
import { useShipmentPermissions } from '../hooks/useShipmentPermissions';
|
|
13
|
+
import { useConditionApi, useConditionQuery } from '../hooks/useContainerCondition';
|
|
14
|
+
import {
|
|
15
|
+
useInvoiceComparisonRecovery,
|
|
16
|
+
useInvoiceComparisonScope,
|
|
17
|
+
} from '../hooks/useInvoiceComparisonRecovery';
|
|
18
|
+
import {
|
|
19
|
+
InvoiceShipmentsDocument,
|
|
20
|
+
InvoiceEstimatePageDocument,
|
|
21
|
+
InvoiceEstimateVersionDocument,
|
|
22
|
+
InvoiceComparisonHistoryDocument,
|
|
23
|
+
InvoiceComparisonExactDocument,
|
|
24
|
+
InvoiceStructuralPreviewDocument,
|
|
25
|
+
type InvoiceShipmentsQuery,
|
|
26
|
+
type InvoiceEstimatePageQuery,
|
|
27
|
+
type InvoiceEstimateVersionQuery,
|
|
28
|
+
type InvoiceComparisonHistoryQuery,
|
|
29
|
+
type InvoiceComparisonExactQuery,
|
|
30
|
+
type InvoiceReceiptFragment,
|
|
31
|
+
type InvoiceStructuralPreviewQuery,
|
|
32
|
+
} from '../../generated/freight-invoice-operations';
|
|
33
|
+
import {
|
|
34
|
+
sameInvoiceScope,
|
|
35
|
+
type InvoiceIntent,
|
|
36
|
+
type InvoiceScope,
|
|
37
|
+
} from '../utils/invoiceComparison';
|
|
38
|
+
|
|
39
|
+
export default function FreightInvoicePage() {
|
|
40
|
+
const owner = useMoveTheWheels();
|
|
41
|
+
return (
|
|
42
|
+
<PageLayout
|
|
43
|
+
title="Freight invoice comparisons"
|
|
44
|
+
subtitle="Compare manually reported source facts with one immutable estimate version."
|
|
45
|
+
icon={<FileText aria-hidden="true" />}
|
|
46
|
+
>
|
|
47
|
+
<PermissionProvider
|
|
48
|
+
key={`${owner.workspaceId}:${owner.tenantId}:${owner.currentUser?.id}:${owner.apiGatewayUrl}`}
|
|
49
|
+
scopeId={owner.workspaceId ?? null}
|
|
50
|
+
gateway="workspace"
|
|
51
|
+
>
|
|
52
|
+
<InvoiceAccess />
|
|
53
|
+
</PermissionProvider>
|
|
54
|
+
</PageLayout>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
function InvoiceAccess() {
|
|
58
|
+
const access = useShipmentPermissions(),
|
|
59
|
+
scope = useInvoiceComparisonScope(access.canRead),
|
|
60
|
+
actions = useInvoiceComparisonRecovery(scope, () => access.allows('update'));
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
{!access.canRead && (
|
|
64
|
+
<p role="status">
|
|
65
|
+
{access.loading
|
|
66
|
+
? 'Checking shipment read permission…'
|
|
67
|
+
: access.failed
|
|
68
|
+
? 'Shipment permissions are unavailable. Reload to inspect them.'
|
|
69
|
+
: 'Shipment read permission in the current workspace is required.'}
|
|
70
|
+
</p>
|
|
71
|
+
)}
|
|
72
|
+
<InvoiceWorkbench key={actions.scopeKey} access={access} scope={scope} actions={actions} />
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function InvoiceWorkbench({
|
|
77
|
+
access,
|
|
78
|
+
scope,
|
|
79
|
+
actions,
|
|
80
|
+
}: {
|
|
81
|
+
access: ReturnType<typeof useShipmentPermissions>;
|
|
82
|
+
scope: ReturnType<typeof useInvoiceComparisonScope>;
|
|
83
|
+
actions: ReturnType<typeof useInvoiceComparisonRecovery>;
|
|
84
|
+
}) {
|
|
85
|
+
const request = useConditionApi(),
|
|
86
|
+
[params, setParams] = useSearchParams();
|
|
87
|
+
const [shipment, setShipment] = useState(
|
|
88
|
+
() => params.get('invoiceShipment')?.slice(0, 255) ?? ''
|
|
89
|
+
),
|
|
90
|
+
[shipmentOffset, setShipmentOffset] = useState(0),
|
|
91
|
+
[estimateOffset, setEstimateOffset] = useState(0);
|
|
92
|
+
const [estimateId, setEstimateId] = useState(''),
|
|
93
|
+
[started, setStarted] = useState(false),
|
|
94
|
+
[correction, setCorrection] = useState<InvoiceReceiptFragment | null>(null);
|
|
95
|
+
const [identity, setIdentity] = useState(
|
|
96
|
+
() => params.get('invoiceIdentity')?.slice(0, 255) ?? ''
|
|
97
|
+
),
|
|
98
|
+
[identityEntry, setIdentityEntry] = useState(identity),
|
|
99
|
+
[historyOffset, setHistoryOffset] = useState(0),
|
|
100
|
+
[selectedId, setSelectedId] = useState('');
|
|
101
|
+
const [known, setKnown] = useState<InvoiceReceiptFragment | null>(null),
|
|
102
|
+
[error, setError] = useState<string | null>(null);
|
|
103
|
+
const enabled = !!scope && access.canRead,
|
|
104
|
+
paused = actions.busy || actions.uncertain;
|
|
105
|
+
const shipments = useConditionQuery<InvoiceShipmentsQuery>(
|
|
106
|
+
InvoiceShipmentsDocument,
|
|
107
|
+
enabled ? { pagination: { limit: 25, offset: shipmentOffset } } : null,
|
|
108
|
+
actions.revision
|
|
109
|
+
);
|
|
110
|
+
const estimates = useConditionQuery<InvoiceEstimatePageQuery>(
|
|
111
|
+
InvoiceEstimatePageDocument,
|
|
112
|
+
enabled && shipment
|
|
113
|
+
? { shipmentId: shipment, pagination: { limit: 25, offset: estimateOffset } }
|
|
114
|
+
: null,
|
|
115
|
+
actions.revision
|
|
116
|
+
);
|
|
117
|
+
const exact = useConditionQuery<InvoiceEstimateVersionQuery>(
|
|
118
|
+
InvoiceEstimateVersionDocument,
|
|
119
|
+
enabled && shipment && estimateId ? { shipmentId: shipment, id: estimateId } : null,
|
|
120
|
+
actions.revision
|
|
121
|
+
);
|
|
122
|
+
const history = useConditionQuery<InvoiceComparisonHistoryQuery>(
|
|
123
|
+
InvoiceComparisonHistoryDocument,
|
|
124
|
+
enabled && shipment && identity
|
|
125
|
+
? {
|
|
126
|
+
shipmentId: shipment,
|
|
127
|
+
invoiceIdentityId: identity,
|
|
128
|
+
pagination: { limit: 10, offset: historyOffset },
|
|
129
|
+
}
|
|
130
|
+
: null,
|
|
131
|
+
actions.revision
|
|
132
|
+
);
|
|
133
|
+
const comparison = useConditionQuery<InvoiceComparisonExactQuery>(
|
|
134
|
+
InvoiceComparisonExactDocument,
|
|
135
|
+
enabled && shipment && selectedId ? { shipmentId: shipment, id: selectedId } : null,
|
|
136
|
+
actions.revision
|
|
137
|
+
);
|
|
138
|
+
const record = comparison.data?.mtwFreightInvoiceComparison ?? known;
|
|
139
|
+
const updateHistory = (row: InvoiceReceiptFragment) => {
|
|
140
|
+
setShipment(row.shipmentId);
|
|
141
|
+
setKnown(row);
|
|
142
|
+
setSelectedId(row.id);
|
|
143
|
+
setIdentity(row.invoiceIdentityId);
|
|
144
|
+
setIdentityEntry(row.invoiceIdentityId);
|
|
145
|
+
setHistoryOffset(0);
|
|
146
|
+
setStarted(false);
|
|
147
|
+
setCorrection(null);
|
|
148
|
+
setParams(
|
|
149
|
+
(previous) => {
|
|
150
|
+
const next = new URLSearchParams(previous);
|
|
151
|
+
next.set('invoiceShipment', row.shipmentId);
|
|
152
|
+
next.set('invoiceIdentity', row.invoiceIdentityId);
|
|
153
|
+
return next;
|
|
154
|
+
},
|
|
155
|
+
{ replace: true }
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
const options =
|
|
159
|
+
shipments.data?.shipments.map((s) => ({
|
|
160
|
+
id: s.id,
|
|
161
|
+
label: `${s.shipmentNumber} — ${s.primaryMode} — ${s.status}`,
|
|
162
|
+
})) ?? [];
|
|
163
|
+
if (shipment && !options.some((s) => s.id === shipment))
|
|
164
|
+
options.unshift({ id: shipment, label: 'Selected shipment outside this loaded page' });
|
|
165
|
+
const shipmentPage = shipments.data
|
|
166
|
+
? {
|
|
167
|
+
total: shipments.data.shipmentsCount,
|
|
168
|
+
limit: 25,
|
|
169
|
+
offset: shipmentOffset,
|
|
170
|
+
hasMore: shipmentOffset + shipments.data.shipments.length < shipments.data.shipmentsCount,
|
|
171
|
+
nextOffset: shipmentOffset + 25,
|
|
172
|
+
}
|
|
173
|
+
: undefined;
|
|
174
|
+
const selected = exact.data?.mtwFreightEstimateVersion;
|
|
175
|
+
return (
|
|
176
|
+
<div className="min-w-0 space-y-6 break-words">
|
|
177
|
+
<p className="rounded border p-4 text-sm">
|
|
178
|
+
Record only what the supplier source reports. Unknown is not zero. This comparison
|
|
179
|
+
establishes no quote acceptance, liability, invoice approval or payment. Corrections append
|
|
180
|
+
a new immutable revision.
|
|
181
|
+
</p>
|
|
182
|
+
{actions.uncertain && (
|
|
183
|
+
<InvoiceRequestRecovery
|
|
184
|
+
pending={actions.pending}
|
|
185
|
+
scope={scope}
|
|
186
|
+
busy={actions.busy}
|
|
187
|
+
canReconcile={actions.canReconcile}
|
|
188
|
+
onRead={() =>
|
|
189
|
+
void actions.reconcile().then((row) => {
|
|
190
|
+
if (row) updateHistory(row);
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
/>
|
|
194
|
+
)}
|
|
195
|
+
{actions.feedback && (
|
|
196
|
+
<p role={actions.feedback.error ? 'alert' : 'status'}>{actions.feedback.message}</p>
|
|
197
|
+
)}
|
|
198
|
+
{enabled && (
|
|
199
|
+
<>
|
|
200
|
+
<section className={reuseCard}>
|
|
201
|
+
<h2 className="text-lg font-semibold">Choose the exact recorded estimate</h2>
|
|
202
|
+
<PinnedSelect
|
|
203
|
+
label="Invoice shipment"
|
|
204
|
+
value={shipment}
|
|
205
|
+
disabled={paused || started}
|
|
206
|
+
options={options}
|
|
207
|
+
onChange={(id) => {
|
|
208
|
+
setShipment(id);
|
|
209
|
+
setEstimateId('');
|
|
210
|
+
setEstimateOffset(0);
|
|
211
|
+
setIdentity('');
|
|
212
|
+
setIdentityEntry('');
|
|
213
|
+
setSelectedId('');
|
|
214
|
+
setKnown(null);
|
|
215
|
+
setCorrection(null);
|
|
216
|
+
setStarted(false);
|
|
217
|
+
setParams(
|
|
218
|
+
(previous) => {
|
|
219
|
+
const next = new URLSearchParams(previous);
|
|
220
|
+
next.set('invoiceShipment', id);
|
|
221
|
+
next.delete('invoiceIdentity');
|
|
222
|
+
return next;
|
|
223
|
+
},
|
|
224
|
+
{ replace: true }
|
|
225
|
+
);
|
|
226
|
+
}}
|
|
227
|
+
/>
|
|
228
|
+
<ReuseState loading={shipments.loading} error={shipments.error} />
|
|
229
|
+
<ReusePager
|
|
230
|
+
page={shipmentPage}
|
|
231
|
+
busy={paused || started || shipments.loading}
|
|
232
|
+
offset={shipmentOffset}
|
|
233
|
+
onChange={setShipmentOffset}
|
|
234
|
+
/>
|
|
235
|
+
{shipment && (
|
|
236
|
+
<>
|
|
237
|
+
<PinnedSelect
|
|
238
|
+
label="Exact estimate version"
|
|
239
|
+
value={estimateId}
|
|
240
|
+
disabled={paused || started}
|
|
241
|
+
options={
|
|
242
|
+
estimates.data?.mtwFreightEstimatePage.items.map((v) => ({
|
|
243
|
+
id: v.id,
|
|
244
|
+
label: `${v.estimateReference} / version ${v.version} / ${v.facts.kind} / ${v.facts.title}`,
|
|
245
|
+
})) ?? []
|
|
246
|
+
}
|
|
247
|
+
onChange={(id) => setEstimateId(id)}
|
|
248
|
+
/>
|
|
249
|
+
<ReuseState loading={estimates.loading} error={estimates.error} />
|
|
250
|
+
<ReusePager
|
|
251
|
+
page={estimates.data?.mtwFreightEstimatePage}
|
|
252
|
+
busy={paused || started || estimates.loading}
|
|
253
|
+
offset={estimateOffset}
|
|
254
|
+
onChange={setEstimateOffset}
|
|
255
|
+
/>
|
|
256
|
+
</>
|
|
257
|
+
)}
|
|
258
|
+
<ReuseState loading={exact.loading} error={exact.error} />
|
|
259
|
+
{selected && (
|
|
260
|
+
<p>
|
|
261
|
+
Pinned {selected.facts.kind}: {selected.estimateReference} version{' '}
|
|
262
|
+
{selected.version}. Source {selected.facts.sourceReference}/
|
|
263
|
+
{selected.facts.sourceRevision}; source validity {selected.validityAtRead}. This
|
|
264
|
+
does not establish acceptance.
|
|
265
|
+
</p>
|
|
266
|
+
)}
|
|
267
|
+
<div className="flex flex-wrap gap-3">
|
|
268
|
+
<Button
|
|
269
|
+
type="button"
|
|
270
|
+
disabled={
|
|
271
|
+
paused ||
|
|
272
|
+
!selected ||
|
|
273
|
+
selected.shipmentId !== shipment ||
|
|
274
|
+
exact.loading ||
|
|
275
|
+
!!exact.error
|
|
276
|
+
}
|
|
277
|
+
onClick={() => {
|
|
278
|
+
setStarted(true);
|
|
279
|
+
setError(null);
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
{correction
|
|
283
|
+
? `Start correction draft after revision ${correction.revision}`
|
|
284
|
+
: 'Start manual invoice draft'}
|
|
285
|
+
</Button>
|
|
286
|
+
{started && (
|
|
287
|
+
<Button
|
|
288
|
+
type="button"
|
|
289
|
+
variant="outline"
|
|
290
|
+
disabled={paused}
|
|
291
|
+
onClick={() => {
|
|
292
|
+
setStarted(false);
|
|
293
|
+
setCorrection(null);
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
Discard unsent draft
|
|
297
|
+
</Button>
|
|
298
|
+
)}
|
|
299
|
+
<Button
|
|
300
|
+
type="button"
|
|
301
|
+
variant="outline"
|
|
302
|
+
disabled={actions.busy}
|
|
303
|
+
onClick={actions.refresh}
|
|
304
|
+
>
|
|
305
|
+
Refresh recorded data
|
|
306
|
+
</Button>
|
|
307
|
+
</div>
|
|
308
|
+
</section>
|
|
309
|
+
{started && selected && selected.shipmentId === shipment && (
|
|
310
|
+
<InvoiceForm
|
|
311
|
+
key={`${selected.id}:${correction?.id ?? 'first'}`}
|
|
312
|
+
estimate={selected}
|
|
313
|
+
previous={correction}
|
|
314
|
+
disabled={paused || exact.loading || !!exact.error}
|
|
315
|
+
canRecord={access.canUpdate}
|
|
316
|
+
onPreview={async (input) => {
|
|
317
|
+
const result = await request<InvoiceStructuralPreviewQuery>(
|
|
318
|
+
InvoiceStructuralPreviewDocument,
|
|
319
|
+
{ input }
|
|
320
|
+
);
|
|
321
|
+
return result.previewMtwFreightInvoice;
|
|
322
|
+
}}
|
|
323
|
+
onSave={async (input, pin) => {
|
|
324
|
+
const result = await actions.run(input, pin);
|
|
325
|
+
if (result) updateHistory(result);
|
|
326
|
+
return result;
|
|
327
|
+
}}
|
|
328
|
+
/>
|
|
329
|
+
)}
|
|
330
|
+
{error && <p role="alert">{error}</p>}
|
|
331
|
+
{shipment && (
|
|
332
|
+
<section className={reuseCard}>
|
|
333
|
+
<h2 className="text-lg font-semibold">Recorded invoice history</h2>
|
|
334
|
+
<p>
|
|
335
|
+
A confirmed save opens its invoice history here. Bookmark this page to return to it.
|
|
336
|
+
Complete invoice browsing is not available in this view. A missing page never proves
|
|
337
|
+
that an unresolved save had no effect.
|
|
338
|
+
</p>
|
|
339
|
+
<details>
|
|
340
|
+
<summary className="min-h-11 cursor-pointer">
|
|
341
|
+
Advanced: open a known receipt identity
|
|
342
|
+
</summary>
|
|
343
|
+
<FreightText
|
|
344
|
+
label="Known invoice identity ID"
|
|
345
|
+
value={identityEntry}
|
|
346
|
+
disabled={actions.busy}
|
|
347
|
+
onChange={setIdentityEntry}
|
|
348
|
+
/>
|
|
349
|
+
<Button
|
|
350
|
+
type="button"
|
|
351
|
+
variant="outline"
|
|
352
|
+
disabled={actions.busy || !identityEntry.trim()}
|
|
353
|
+
onClick={() => {
|
|
354
|
+
setIdentity(identityEntry.trim());
|
|
355
|
+
setHistoryOffset(0);
|
|
356
|
+
setSelectedId('');
|
|
357
|
+
setKnown(null);
|
|
358
|
+
setParams(
|
|
359
|
+
(previous) => {
|
|
360
|
+
const next = new URLSearchParams(previous);
|
|
361
|
+
next.set('invoiceShipment', shipment);
|
|
362
|
+
next.set('invoiceIdentity', identityEntry.trim());
|
|
363
|
+
return next;
|
|
364
|
+
},
|
|
365
|
+
{ replace: true }
|
|
366
|
+
);
|
|
367
|
+
}}
|
|
368
|
+
>
|
|
369
|
+
Read identity history
|
|
370
|
+
</Button>
|
|
371
|
+
</details>
|
|
372
|
+
{identity && (
|
|
373
|
+
<>
|
|
374
|
+
<ReuseState loading={history.loading} error={history.error} />
|
|
375
|
+
{history.data?.mtwFreightInvoiceComparisonPage.items.map((row) => (
|
|
376
|
+
<Button
|
|
377
|
+
key={row.id}
|
|
378
|
+
className="h-auto max-w-full whitespace-normal break-words text-left"
|
|
379
|
+
type="button"
|
|
380
|
+
variant="outline"
|
|
381
|
+
onClick={() => {
|
|
382
|
+
setSelectedId(row.id);
|
|
383
|
+
setKnown(null);
|
|
384
|
+
}}
|
|
385
|
+
>
|
|
386
|
+
{row.facts.supplierLabel ?? row.facts.supplierReference} / invoice{' '}
|
|
387
|
+
{row.facts.invoiceReference}, revision {row.revision} / {row.recordedAt}
|
|
388
|
+
</Button>
|
|
389
|
+
))}
|
|
390
|
+
<ReusePager
|
|
391
|
+
page={history.data?.mtwFreightInvoiceComparisonPage}
|
|
392
|
+
busy={history.loading || actions.busy}
|
|
393
|
+
offset={historyOffset}
|
|
394
|
+
onChange={setHistoryOffset}
|
|
395
|
+
/>
|
|
396
|
+
</>
|
|
397
|
+
)}
|
|
398
|
+
</section>
|
|
399
|
+
)}
|
|
400
|
+
<ReuseState loading={comparison.loading} error={comparison.error} />
|
|
401
|
+
{record && record.shipmentId === shipment && (
|
|
402
|
+
<>
|
|
403
|
+
<InvoiceHistoryRecord record={record} />
|
|
404
|
+
<Button
|
|
405
|
+
type="button"
|
|
406
|
+
disabled={
|
|
407
|
+
paused ||
|
|
408
|
+
!access.canUpdate ||
|
|
409
|
+
!history.data ||
|
|
410
|
+
history.data.mtwFreightInvoiceComparisonPage.items[0]?.id !== record.id ||
|
|
411
|
+
historyOffset !== 0
|
|
412
|
+
}
|
|
413
|
+
onClick={() => {
|
|
414
|
+
setEstimateId(record.estimate.id);
|
|
415
|
+
setCorrection(record);
|
|
416
|
+
setStarted(false);
|
|
417
|
+
setError(null);
|
|
418
|
+
}}
|
|
419
|
+
>
|
|
420
|
+
Prepare correction to this latest revision
|
|
421
|
+
</Button>
|
|
422
|
+
<p>
|
|
423
|
+
Refresh the first history page to pin its latest revision before correcting. The
|
|
424
|
+
server checks the exact predecessor again.
|
|
425
|
+
</p>
|
|
426
|
+
</>
|
|
427
|
+
)}
|
|
428
|
+
</>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Retain the unresolved intent across context changes without displaying another context's source facts. */
|
|
435
|
+
export function InvoiceRequestRecovery({
|
|
436
|
+
pending,
|
|
437
|
+
scope,
|
|
438
|
+
busy,
|
|
439
|
+
canReconcile,
|
|
440
|
+
onRead,
|
|
441
|
+
}: {
|
|
442
|
+
pending: InvoiceIntent | null;
|
|
443
|
+
scope: InvoiceScope | null;
|
|
444
|
+
busy: boolean;
|
|
445
|
+
canReconcile: boolean;
|
|
446
|
+
onRead: () => void;
|
|
447
|
+
}) {
|
|
448
|
+
return (
|
|
449
|
+
<section className={reuseCard} aria-label="Original invoice request recovery">
|
|
450
|
+
<h2 className="font-semibold">Original save is unresolved</h2>
|
|
451
|
+
{pending && sameInvoiceScope(pending.scope, scope) ? (
|
|
452
|
+
<>
|
|
453
|
+
<p>
|
|
454
|
+
Invoice {pending.input.facts.invoiceReference ?? 'reference unknown'}; exact{' '}
|
|
455
|
+
{pending.estimate.kind.toLowerCase()} {pending.estimate.estimateReference}, version{' '}
|
|
456
|
+
{pending.estimate.version}. No automatic resend or new request is allowed.
|
|
457
|
+
</p>
|
|
458
|
+
<Button type="button" disabled={!canReconcile || busy} onClick={onRead}>
|
|
459
|
+
Read original request receipt
|
|
460
|
+
</Button>
|
|
461
|
+
<details>
|
|
462
|
+
<summary className="min-h-11 cursor-pointer">Retained original source intent</summary>
|
|
463
|
+
<pre className="max-w-full whitespace-pre-wrap break-all text-xs">
|
|
464
|
+
{JSON.stringify(pending.input, null, 2)}
|
|
465
|
+
</pre>
|
|
466
|
+
</details>
|
|
467
|
+
</>
|
|
468
|
+
) : (
|
|
469
|
+
<p>
|
|
470
|
+
{pending
|
|
471
|
+
? 'The original request is retained in another or unavailable context. Return to the original account, organization, workspace and project with read permission; its source facts remain hidden here.'
|
|
472
|
+
: 'The saved intent is malformed or unavailable. It remains blocked; do not discard it or start another request as proof of no effect.'}
|
|
473
|
+
</p>
|
|
474
|
+
)}
|
|
475
|
+
</section>
|
|
476
|
+
);
|
|
477
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** The expiry control is explicitly UTC, independent of the browser timezone. */
|
|
2
|
+
export const driverLicenseExpiryInput = (recorded: Date | undefined): string =>
|
|
3
|
+
recorded ? recorded.toISOString().slice(0, -1) : '';
|
|
4
|
+
|
|
5
|
+
/** Empty means unrecorded; never substitute noon, end of day or the current time. */
|
|
6
|
+
export const parseDriverLicenseExpiry = (value: string): string | null => {
|
|
7
|
+
if (value === '') return null;
|
|
8
|
+
const match = /^(\d{4}-\d\d-\d\dT\d\d:\d\d)(?::(\d\d)(?:\.(\d{1,3}))?)?$/.exec(value);
|
|
9
|
+
if (!match) throw new Error('Enter a valid licence expiry date and time in UTC.');
|
|
10
|
+
const canonical = `${match[1]}:${match[2] ?? '00'}.${(match[3] ?? '').padEnd(3, '0')}Z`;
|
|
11
|
+
const parsed = new Date(canonical);
|
|
12
|
+
if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== canonical) {
|
|
13
|
+
throw new Error('Enter a valid licence expiry date and time in UTC.');
|
|
14
|
+
}
|
|
15
|
+
return canonical;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Omit unchanged updates so even server precision beyond this display is retained. */
|
|
19
|
+
export const driverLicenseExpiryUpdate = (
|
|
20
|
+
value: string,
|
|
21
|
+
recorded: Date | undefined
|
|
22
|
+
): string | null | undefined => {
|
|
23
|
+
const next = parseDriverLicenseExpiry(value);
|
|
24
|
+
return next === (recorded?.toISOString() ?? null) ? undefined : next;
|
|
25
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { matchesCommercialCapture } from './freightCommercial';
|
|
1
2
|
/** Validate the minimum immutable receipt identity before resolving a retained request. */
|
|
2
3
|
export function matchesFreightAcknowledgement(
|
|
3
4
|
result: unknown,
|
|
@@ -26,6 +27,7 @@ export function matchesFreightAcknowledgement(
|
|
|
26
27
|
input.expectedVersion >= 0 &&
|
|
27
28
|
receipt.version === input.expectedVersion + 1 &&
|
|
28
29
|
'previousVersionId' in receipt &&
|
|
29
|
-
receipt.previousVersionId === input.expectedPreviousVersionId
|
|
30
|
+
receipt.previousVersionId === input.expectedPreviousVersionId &&
|
|
31
|
+
(input.commercialCapture == null || matchesCommercialCapture(receipt, input))
|
|
30
32
|
);
|
|
31
33
|
}
|