@burdenoff/microfe-movethewheels 2026.1016.3 → 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-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 +307 -476
- 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/useInvoiceComparisonRecovery.js +118 -0
- package/dist/movethewheels/components/ContainerEvidencePicker.d.ts +2 -1
- 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/invoiceComparison.d.ts +32 -0
- package/dist/pages/DriverDetailsPage.js +306 -282
- package/dist/pages/FreightEstimatePage.js +1 -0
- 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 -812
- 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/invoiceComparison.js +272 -0
- package/package.json +29 -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 +42 -848
- 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-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/useInvoiceComparisonRecovery.ts +227 -0
- package/src/movethewheels/pages/DriverDetailsPage.tsx +35 -9
- package/src/movethewheels/pages/FreightEstimatePage.tsx +1 -0
- package/src/movethewheels/pages/FreightInvoicePage.tsx +477 -0
- package/src/movethewheels/utils/driverLicenseExpiry.ts +25 -0
- package/src/movethewheels/utils/invoiceComparison.ts +520 -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,162 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InvoiceReceiptFragment,
|
|
3
|
+
InvoiceStructuralPreviewQuery,
|
|
4
|
+
} from '../../../generated/freight-invoice-operations';
|
|
5
|
+
import { freightLabel } from '../../utils/freightDraft';
|
|
6
|
+
import { invoiceMoney, invoiceRowDifference } from '../../utils/invoiceComparison';
|
|
7
|
+
import { reuseCard } from '../ReuseControls';
|
|
8
|
+
|
|
9
|
+
type Preview = InvoiceStructuralPreviewQuery['previewMtwFreightInvoice'];
|
|
10
|
+
export function InvoiceResults({ value }: { value: Preview | InvoiceReceiptFragment }) {
|
|
11
|
+
const { estimate, outcome } = value;
|
|
12
|
+
return (
|
|
13
|
+
<section className={reuseCard} aria-label="Invoice comparison results">
|
|
14
|
+
<h2 className="text-lg font-semibold">Recorded-fact comparison</h2>
|
|
15
|
+
<p>
|
|
16
|
+
Exact {estimate.kind.toLowerCase()} {estimate.estimateReference}, version {estimate.version}
|
|
17
|
+
. Source {estimate.sourceReference} / {estimate.sourceRevision}.
|
|
18
|
+
</p>
|
|
19
|
+
<p>
|
|
20
|
+
Quote date {estimate.quotedAt ?? 'Unknown'}; source validity{' '}
|
|
21
|
+
{estimate.validFrom ?? 'Unknown'} to {estimate.validUntil ?? 'Unknown'}. Acceptance is not
|
|
22
|
+
established by this comparison.
|
|
23
|
+
</p>
|
|
24
|
+
{'recordable' in value && (
|
|
25
|
+
<p role="status">
|
|
26
|
+
Structural preview only.{' '}
|
|
27
|
+
{value.recordable
|
|
28
|
+
? 'Source structure is recordable; current permission and Files eligibility are checked when recording.'
|
|
29
|
+
: `Not recordable: ${value.blockers.map(freightLabel).join(', ')}.`}
|
|
30
|
+
</p>
|
|
31
|
+
)}
|
|
32
|
+
<p className="font-medium">
|
|
33
|
+
{outcome.allLinesMatched
|
|
34
|
+
? 'All explicitly compared rows and expected coverage agree under this method.'
|
|
35
|
+
: 'Differences or unknown coverage remain.'}
|
|
36
|
+
</p>
|
|
37
|
+
<p>
|
|
38
|
+
This is not an all-in cost, contracted-rate audit, liability finding, invoice approval or
|
|
39
|
+
payment.
|
|
40
|
+
</p>
|
|
41
|
+
{outcome.rows.map((row, index) => (
|
|
42
|
+
<article className="min-w-0 space-y-2 rounded border p-3" key={row.invoiceLineId}>
|
|
43
|
+
<h3 className="font-medium">
|
|
44
|
+
Row {index + 1}:{' '}
|
|
45
|
+
{value.facts.rows.find((source) => source.id === row.invoiceLineId)?.description ??
|
|
46
|
+
'Reported charge'}
|
|
47
|
+
</h3>
|
|
48
|
+
<p>
|
|
49
|
+
{row.estimateLineId
|
|
50
|
+
? 'Mapped to the selected estimate row.'
|
|
51
|
+
: 'No estimate row mapped.'}
|
|
52
|
+
</p>
|
|
53
|
+
<p>{row.flags.map(freightLabel).join(' · ')}</p>
|
|
54
|
+
{row.reasons.length > 0 && (
|
|
55
|
+
<p>Unknown reasons: {row.reasons.map(freightLabel).join(', ')}</p>
|
|
56
|
+
)}
|
|
57
|
+
<p>Comparable amount difference: {invoiceRowDifference(value, row.invoiceLineId)}</p>
|
|
58
|
+
</article>
|
|
59
|
+
))}
|
|
60
|
+
{outcome.missingExpectedLineIds.map((id) => (
|
|
61
|
+
<p key={id}>Missing expected line: {id}. An unknown value is not treated as zero.</p>
|
|
62
|
+
))}
|
|
63
|
+
{outcome.unknownExpectedCoverage.map((row) => (
|
|
64
|
+
<p key={row.estimateLineId}>
|
|
65
|
+
Unknown expected-charge coverage: {row.estimateLineId}. This is not a proven missing
|
|
66
|
+
charge.
|
|
67
|
+
</p>
|
|
68
|
+
))}
|
|
69
|
+
<section className="space-y-2">
|
|
70
|
+
<h3 className="font-semibold">Comparable pairs only</h3>
|
|
71
|
+
{outcome.comparableSubtotals.length === 0 && <p>No bounded comparable subtotal.</p>}
|
|
72
|
+
{outcome.comparableSubtotals.map((group) => (
|
|
73
|
+
<p key={JSON.stringify([group.currency, group.payerReference])}>
|
|
74
|
+
Payer{' '}
|
|
75
|
+
{value.facts.rows.find((row) => row.payerReference === group.payerReference)
|
|
76
|
+
?.payerLabel ?? group.payerReference}
|
|
77
|
+
: estimate {invoiceMoney(group.estimateMinor, group.currency)}, invoice{' '}
|
|
78
|
+
{invoiceMoney(group.invoiceMinor, group.currency)}, difference{' '}
|
|
79
|
+
{invoiceMoney(group.deltaMinor, group.currency)}. Included rows{' '}
|
|
80
|
+
{group.invoiceLineIds
|
|
81
|
+
.map(
|
|
82
|
+
(id) =>
|
|
83
|
+
value.facts.rows.find((row) => row.id === id)?.description ?? 'Reported charge'
|
|
84
|
+
)
|
|
85
|
+
.join(', ')}
|
|
86
|
+
. Other rows are omitted.
|
|
87
|
+
</p>
|
|
88
|
+
))}
|
|
89
|
+
</section>
|
|
90
|
+
{(
|
|
91
|
+
[
|
|
92
|
+
['Declared estimate subtotals', value.estimateDeclaredSubtotals],
|
|
93
|
+
['Declared invoice subtotals', value.invoiceDeclaredSubtotals],
|
|
94
|
+
] as const
|
|
95
|
+
).map(([label, groups]) => (
|
|
96
|
+
<section key={label} className="space-y-2">
|
|
97
|
+
<h3 className="font-semibold">{label}</h3>
|
|
98
|
+
{groups.map((group, index) => (
|
|
99
|
+
<p key={JSON.stringify([group.currency, group.payerReference, index])}>
|
|
100
|
+
Payer {group.payerLabel ?? 'Unknown'} ({group.payerReference ?? 'Unknown reference'});{' '}
|
|
101
|
+
known {invoiceMoney(group.knownAmountMinor, group.currency)}; total{' '}
|
|
102
|
+
{invoiceMoney(group.totalAmountMinor, group.currency)}.
|
|
103
|
+
</p>
|
|
104
|
+
))}
|
|
105
|
+
</section>
|
|
106
|
+
))}
|
|
107
|
+
</section>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
export function InvoiceHistoryRecord({ record }: { record: InvoiceReceiptFragment }) {
|
|
111
|
+
return (
|
|
112
|
+
<section className="min-w-0 space-y-4 break-words">
|
|
113
|
+
<h2 className="text-lg font-semibold">Immutable comparison revision {record.revision}</h2>
|
|
114
|
+
<p>
|
|
115
|
+
Invoice {record.facts.invoiceReference}; supplier{' '}
|
|
116
|
+
{record.facts.supplierLabel ?? 'Unknown label'} ({record.facts.supplierReference}). Source{' '}
|
|
117
|
+
{record.facts.sourceReference} / {record.facts.sourceRevision}; invoice date{' '}
|
|
118
|
+
{record.facts.invoiceDate ?? 'Unknown'}.
|
|
119
|
+
</p>
|
|
120
|
+
<p>
|
|
121
|
+
Recorded {record.recordedAt} by {record.createdBy}, {freightLabel(record.actorProvenance)};
|
|
122
|
+
on behalf of {record.onBehalfOf ?? 'Not declared'}. Method {record.methodVersion}.
|
|
123
|
+
</p>
|
|
124
|
+
{record.correctionReason && (
|
|
125
|
+
<p>
|
|
126
|
+
Correction reason: {record.correctionReason}; predecessor {record.previousComparisonId}.
|
|
127
|
+
</p>
|
|
128
|
+
)}
|
|
129
|
+
<details>
|
|
130
|
+
<summary className="min-h-11 cursor-pointer font-medium">
|
|
131
|
+
Original reported rows and exact evidence
|
|
132
|
+
</summary>
|
|
133
|
+
<div className="space-y-3">
|
|
134
|
+
{record.facts.rows.map((row) => (
|
|
135
|
+
<article key={row.id} className="rounded border p-3">
|
|
136
|
+
<h3>
|
|
137
|
+
{row.description} ({row.id})
|
|
138
|
+
</h3>
|
|
139
|
+
<p>
|
|
140
|
+
Mapping {row.estimateLineId ?? 'Unmatched'}; amount{' '}
|
|
141
|
+
{invoiceMoney(row.amountMinor, row.currency)}; payer {row.payerLabel ?? 'Unknown'} (
|
|
142
|
+
{row.payerReference ?? 'Unknown reference'}).
|
|
143
|
+
</p>
|
|
144
|
+
<p>
|
|
145
|
+
Quantity {row.quantity ?? 'Unknown'} {row.unit ?? 'unknown unit'}; supplied rate{' '}
|
|
146
|
+
{row.unitRateMajor ?? 'Unknown'}. Invoice amount was not recalculated.
|
|
147
|
+
</p>
|
|
148
|
+
{row.zeroReason && <p>Zero reason: {row.zeroReason}</p>}
|
|
149
|
+
</article>
|
|
150
|
+
))}
|
|
151
|
+
{record.evidence.map((e) => (
|
|
152
|
+
<p className="break-all" key={e.versionId}>
|
|
153
|
+
File {e.fileId}, exact version {e.versionId}; SHA256 {e.sha256Hash}; checked{' '}
|
|
154
|
+
{e.checkedAt}. Point-in-time eligibility only.
|
|
155
|
+
</p>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
</details>
|
|
159
|
+
<InvoiceResults value={record} />
|
|
160
|
+
</section>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -42,7 +42,7 @@ export interface CreateDriverInput {
|
|
|
42
42
|
phone?: string;
|
|
43
43
|
licenseNumber?: string;
|
|
44
44
|
licenseClass?: string;
|
|
45
|
-
licenseExpiry?: string;
|
|
45
|
+
licenseExpiry?: string | null;
|
|
46
46
|
/** DriverStatus enum value, e.g. `AVAILABLE`, `OFF_DUTY`. */
|
|
47
47
|
status?: string;
|
|
48
48
|
vehicleId?: string | null;
|
|
@@ -11,7 +11,11 @@ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
|
11
11
|
import { executeGraphQL } from '../utils/graphqlClient';
|
|
12
12
|
|
|
13
13
|
export type FleetAlertType =
|
|
14
|
-
|
|
14
|
+
| 'LICENSE_UNKNOWN'
|
|
15
|
+
| 'LICENSE_EXPIRED'
|
|
16
|
+
| 'LICENSE_EXPIRING'
|
|
17
|
+
| 'MAINTENANCE_OVERDUE'
|
|
18
|
+
| 'MAINTENANCE_DUE_SOON';
|
|
15
19
|
export type FleetAlertSeverity = 'CRITICAL' | 'WARNING';
|
|
16
20
|
export type FleetAlertSubjectType = 'VEHICLE' | 'DRIVER';
|
|
17
21
|
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useActiveContext } from '@burdenoff/fe-libs/shared/providers/shell';
|
|
3
|
+
import {
|
|
4
|
+
InvoiceComparisonExactDocument,
|
|
5
|
+
RecordInvoiceComparisonDocument,
|
|
6
|
+
RecoverInvoiceComparisonDocument,
|
|
7
|
+
type CompareMtwFreightInvoiceInput,
|
|
8
|
+
type InvoiceComparisonExactQuery,
|
|
9
|
+
type InvoiceEstimatePinFragment,
|
|
10
|
+
type InvoiceReceiptFragment,
|
|
11
|
+
type RecordInvoiceComparisonMutation,
|
|
12
|
+
type RecoverInvoiceComparisonQuery,
|
|
13
|
+
} from '../../generated/freight-invoice-operations';
|
|
14
|
+
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
15
|
+
import { resolveGraphqlUrl } from '../utils/graphqlClient';
|
|
16
|
+
import {
|
|
17
|
+
INVOICE_RECOVERY_STORAGE,
|
|
18
|
+
decodeInvoiceIntent,
|
|
19
|
+
encodeInvoiceIntent,
|
|
20
|
+
invoiceCanonical,
|
|
21
|
+
invoiceRecordInput,
|
|
22
|
+
matchesInvoiceReceipt,
|
|
23
|
+
sameInvoiceScope,
|
|
24
|
+
type InvoiceIntent,
|
|
25
|
+
type InvoiceScope,
|
|
26
|
+
} from '../utils/invoiceComparison';
|
|
27
|
+
import { useConditionApi } from './useContainerCondition';
|
|
28
|
+
|
|
29
|
+
function retained() {
|
|
30
|
+
try {
|
|
31
|
+
const raw = sessionStorage.getItem(INVOICE_RECOVERY_STORAGE),
|
|
32
|
+
intent = decodeInvoiceIntent(raw);
|
|
33
|
+
return { intent, blocked: raw !== null && intent === null };
|
|
34
|
+
} catch {
|
|
35
|
+
return { intent: null, blocked: true };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Canonical visible context only; never decode credentials or claim the full server context. */
|
|
39
|
+
export function useInvoiceComparisonScope(canRead: boolean): InvoiceScope | null {
|
|
40
|
+
const owner = useMoveTheWheels(),
|
|
41
|
+
active = useActiveContext();
|
|
42
|
+
if (
|
|
43
|
+
!canRead ||
|
|
44
|
+
!owner.currentUser?.id ||
|
|
45
|
+
!owner.workspaceId ||
|
|
46
|
+
active.workspaceId !== owner.workspaceId ||
|
|
47
|
+
!active.organizationId ||
|
|
48
|
+
(owner.tenantId && active.tenantId !== owner.tenantId)
|
|
49
|
+
)
|
|
50
|
+
return null;
|
|
51
|
+
return {
|
|
52
|
+
actorId: owner.currentUser.id,
|
|
53
|
+
workspaceId: owner.workspaceId,
|
|
54
|
+
tenantId: active.tenantId,
|
|
55
|
+
organizationId: active.organizationId,
|
|
56
|
+
projectId: active.projectId,
|
|
57
|
+
gateway: resolveGraphqlUrl(owner.apiGatewayUrl),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** One original business intent. UNKNOWN permits reads only, never a retry/new key or automatic resend. */
|
|
61
|
+
export function useInvoiceComparisonRecovery(
|
|
62
|
+
scope: InvoiceScope | null,
|
|
63
|
+
allowsWrite: () => boolean
|
|
64
|
+
) {
|
|
65
|
+
const request = useConditionApi('SINGLE_ATTEMPT'),
|
|
66
|
+
[initial] = useState(retained);
|
|
67
|
+
const [pending, setPending] = useState<InvoiceIntent | null>(initial.intent),
|
|
68
|
+
pendingRef = useRef(pending);
|
|
69
|
+
const [blocked, setBlocked] = useState(initial.blocked),
|
|
70
|
+
running = useRef(false);
|
|
71
|
+
const [busy, setBusy] = useState(false),
|
|
72
|
+
[revision, setRevision] = useState(0);
|
|
73
|
+
const scopeKey = invoiceCanonical(scope),
|
|
74
|
+
currentScope = useRef(scope);
|
|
75
|
+
const [feedback, setFeedback] = useState<{
|
|
76
|
+
scopeKey: string;
|
|
77
|
+
message: string;
|
|
78
|
+
error: boolean;
|
|
79
|
+
} | null>(null);
|
|
80
|
+
useLayoutEffect(() => {
|
|
81
|
+
currentScope.current = scope;
|
|
82
|
+
return () => {
|
|
83
|
+
currentScope.current = null;
|
|
84
|
+
};
|
|
85
|
+
}, [scopeKey]);
|
|
86
|
+
const tell = (message: string, error = true) => setFeedback({ scopeKey, message, error });
|
|
87
|
+
const confirm = (receipt: InvoiceReceiptFragment | null | undefined, intent: InvoiceIntent) => {
|
|
88
|
+
if (
|
|
89
|
+
!receipt ||
|
|
90
|
+
!matchesInvoiceReceipt(receipt, intent) ||
|
|
91
|
+
!sameInvoiceScope(intent.scope, currentScope.current)
|
|
92
|
+
)
|
|
93
|
+
throw new Error('UNCONFIRMED');
|
|
94
|
+
const saved = decodeInvoiceIntent(sessionStorage.getItem(INVOICE_RECOVERY_STORAGE));
|
|
95
|
+
if (!saved || invoiceCanonical(saved) !== invoiceCanonical(intent))
|
|
96
|
+
throw new Error('RETAINED_INTENT_CHANGED');
|
|
97
|
+
sessionStorage.removeItem(INVOICE_RECOVERY_STORAGE);
|
|
98
|
+
pendingRef.current = null;
|
|
99
|
+
setPending(null);
|
|
100
|
+
setBlocked(false);
|
|
101
|
+
setRevision((v) => v + 1);
|
|
102
|
+
tell('The exact immutable comparison receipt is confirmed.', false);
|
|
103
|
+
return receipt;
|
|
104
|
+
};
|
|
105
|
+
const run = async (
|
|
106
|
+
supplied: Omit<CompareMtwFreightInvoiceInput, 'idempotencyKey'>,
|
|
107
|
+
estimate: InvoiceEstimatePinFragment
|
|
108
|
+
) => {
|
|
109
|
+
if (running.current || pendingRef.current || blocked) {
|
|
110
|
+
tell('Resolve the original request before starting another.');
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (!scope || !sameInvoiceScope(scope, currentScope.current) || !allowsWrite()) {
|
|
114
|
+
tell(
|
|
115
|
+
'Current shipment update permission and workspace context are required. Nothing was sent.'
|
|
116
|
+
);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
let intent: InvoiceIntent;
|
|
120
|
+
try {
|
|
121
|
+
if (sessionStorage.getItem(INVOICE_RECOVERY_STORAGE) !== null) throw new Error('EXISTING');
|
|
122
|
+
intent = {
|
|
123
|
+
version: 1,
|
|
124
|
+
scope: structuredClone(scope),
|
|
125
|
+
input: invoiceRecordInput({ ...supplied, idempotencyKey: crypto.randomUUID() }),
|
|
126
|
+
estimate: structuredClone(estimate),
|
|
127
|
+
};
|
|
128
|
+
sessionStorage.setItem(INVOICE_RECOVERY_STORAGE, encodeInvoiceIntent(intent));
|
|
129
|
+
pendingRef.current = intent;
|
|
130
|
+
setPending(intent);
|
|
131
|
+
} catch {
|
|
132
|
+
tell(
|
|
133
|
+
'The original request could not be retained safely. Nothing was sent; reload and inspect any retained request.'
|
|
134
|
+
);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
running.current = true;
|
|
138
|
+
setBusy(true);
|
|
139
|
+
setFeedback(null);
|
|
140
|
+
try {
|
|
141
|
+
// No await occurs between this final scope/permission check and the one product dispatch.
|
|
142
|
+
if (!sameInvoiceScope(intent.scope, currentScope.current) || !allowsWrite()) {
|
|
143
|
+
const saved = decodeInvoiceIntent(sessionStorage.getItem(INVOICE_RECOVERY_STORAGE));
|
|
144
|
+
if (saved && invoiceCanonical(saved) === invoiceCanonical(intent)) {
|
|
145
|
+
sessionStorage.removeItem(INVOICE_RECOVERY_STORAGE);
|
|
146
|
+
pendingRef.current = null;
|
|
147
|
+
setPending(null);
|
|
148
|
+
}
|
|
149
|
+
tell('Context or permission changed before dispatch. Nothing was sent.');
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const result = await request<RecordInvoiceComparisonMutation>(
|
|
153
|
+
RecordInvoiceComparisonDocument,
|
|
154
|
+
{ input: intent.input }
|
|
155
|
+
);
|
|
156
|
+
return confirm(result.compareMtwFreightInvoice, intent);
|
|
157
|
+
} catch {
|
|
158
|
+
tell(
|
|
159
|
+
'Save outcome is UNKNOWN. Keep the original intent and use read-only recovery; do not resend it.'
|
|
160
|
+
);
|
|
161
|
+
return null;
|
|
162
|
+
} finally {
|
|
163
|
+
running.current = false;
|
|
164
|
+
setBusy(false);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const reconcile = async () => {
|
|
168
|
+
const intent = pendingRef.current;
|
|
169
|
+
if (running.current || !intent) return null;
|
|
170
|
+
if (!sameInvoiceScope(intent.scope, currentScope.current)) {
|
|
171
|
+
tell(
|
|
172
|
+
'Return to the original account, organization, workspace and project to inspect its request.'
|
|
173
|
+
);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
running.current = true;
|
|
177
|
+
setBusy(true);
|
|
178
|
+
setFeedback(null);
|
|
179
|
+
try {
|
|
180
|
+
const found = await request<RecoverInvoiceComparisonQuery>(RecoverInvoiceComparisonDocument, {
|
|
181
|
+
shipmentId: intent.input.shipmentId,
|
|
182
|
+
requestKey: intent.input.idempotencyKey,
|
|
183
|
+
});
|
|
184
|
+
const receipt = found.mtwFreightInvoiceComparisonByRequestKey;
|
|
185
|
+
if (!receipt) {
|
|
186
|
+
tell(
|
|
187
|
+
'No original-context receipt was returned. This does not prove no effect; the request stays UNKNOWN and cannot be resent.'
|
|
188
|
+
);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
if (!matchesInvoiceReceipt(receipt, intent)) throw new Error('MISMATCH');
|
|
192
|
+
if (!sameInvoiceScope(intent.scope, currentScope.current)) throw new Error('CONTEXT_CHANGED');
|
|
193
|
+
const exact = await request<InvoiceComparisonExactQuery>(InvoiceComparisonExactDocument, {
|
|
194
|
+
shipmentId: intent.input.shipmentId,
|
|
195
|
+
id: receipt.id,
|
|
196
|
+
});
|
|
197
|
+
if (
|
|
198
|
+
!matchesInvoiceReceipt(exact.mtwFreightInvoiceComparison, intent) ||
|
|
199
|
+
exact.mtwFreightInvoiceComparison.requestFingerprint !== receipt.requestFingerprint ||
|
|
200
|
+
invoiceCanonical(exact.mtwFreightInvoiceComparison) !== invoiceCanonical(receipt)
|
|
201
|
+
)
|
|
202
|
+
throw new Error('MISMATCH');
|
|
203
|
+
return confirm(exact.mtwFreightInvoiceComparison, intent);
|
|
204
|
+
} catch {
|
|
205
|
+
tell(
|
|
206
|
+
'Read-only recovery could not confirm the exact original receipt. Keep the intent; no save was sent.'
|
|
207
|
+
);
|
|
208
|
+
return null;
|
|
209
|
+
} finally {
|
|
210
|
+
running.current = false;
|
|
211
|
+
setBusy(false);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
run,
|
|
216
|
+
reconcile,
|
|
217
|
+
busy,
|
|
218
|
+
pending,
|
|
219
|
+
blocked,
|
|
220
|
+
uncertain: pending !== null || blocked,
|
|
221
|
+
revision,
|
|
222
|
+
scopeKey,
|
|
223
|
+
canReconcile: !!pending && sameInvoiceScope(pending.scope, scope),
|
|
224
|
+
feedback: feedback?.scopeKey === scopeKey ? feedback : null,
|
|
225
|
+
refresh: () => setRevision((v) => v + 1),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -61,6 +61,11 @@ import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
|
61
61
|
import { useRefetchOnLeaveEdit } from '../hooks/useRefetchOnLeaveEdit';
|
|
62
62
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
63
63
|
import { formatCurrency, formatDate } from '../utils/formatters';
|
|
64
|
+
import {
|
|
65
|
+
driverLicenseExpiryInput,
|
|
66
|
+
driverLicenseExpiryUpdate,
|
|
67
|
+
parseDriverLicenseExpiry,
|
|
68
|
+
} from '../utils/driverLicenseExpiry';
|
|
64
69
|
import { joinPath } from '../utils/navigation';
|
|
65
70
|
import { FormError } from '../components/FormError';
|
|
66
71
|
import { DocumentsCard } from '../components/DocumentsCard';
|
|
@@ -163,7 +168,7 @@ function NewDriverForm({ editId }: { editId?: string }) {
|
|
|
163
168
|
phone: driver.phone || '',
|
|
164
169
|
licenseNumber: driver.licenseNumber || '',
|
|
165
170
|
licenseClass: driver.licenseType || '',
|
|
166
|
-
licenseExpiry:
|
|
171
|
+
licenseExpiry: driverLicenseExpiryInput(driver.licenseExpiry),
|
|
167
172
|
status: driver.status,
|
|
168
173
|
vehicleId: driver.assignedVehicle || '',
|
|
169
174
|
vehicleIds: driver.vehicleIds,
|
|
@@ -186,7 +191,9 @@ function NewDriverForm({ editId }: { editId?: string }) {
|
|
|
186
191
|
phone: form.phone.trim() || undefined,
|
|
187
192
|
licenseNumber: form.licenseNumber.trim() || undefined,
|
|
188
193
|
licenseClass: form.licenseClass.trim() || undefined,
|
|
189
|
-
licenseExpiry:
|
|
194
|
+
licenseExpiry: isEditing
|
|
195
|
+
? driverLicenseExpiryUpdate(form.licenseExpiry, driver?.licenseExpiry)
|
|
196
|
+
: parseDriverLicenseExpiry(form.licenseExpiry),
|
|
190
197
|
status: toApiDriverStatus(form.status),
|
|
191
198
|
vehicleId: form.vehicleId || null,
|
|
192
199
|
vehicleIds: form.vehicleIds,
|
|
@@ -417,14 +424,33 @@ function NewDriverForm({ editId }: { editId?: string }) {
|
|
|
417
424
|
placeholder="Class A"
|
|
418
425
|
/>
|
|
419
426
|
</div>
|
|
420
|
-
<div className="space-y-2">
|
|
421
|
-
<
|
|
427
|
+
<div className="space-y-2 md:col-span-3">
|
|
428
|
+
<div className="flex items-center justify-between gap-2">
|
|
429
|
+
<Label htmlFor="licenseExpiry">Licence expiry (UTC)</Label>
|
|
430
|
+
{form.licenseExpiry && (
|
|
431
|
+
<Button
|
|
432
|
+
type="button"
|
|
433
|
+
variant="ghost"
|
|
434
|
+
size="sm"
|
|
435
|
+
aria-label="Clear recorded licence expiry"
|
|
436
|
+
onClick={() => setForm((prev) => ({ ...prev, licenseExpiry: '' }))}
|
|
437
|
+
>
|
|
438
|
+
Clear
|
|
439
|
+
</Button>
|
|
440
|
+
)}
|
|
441
|
+
</div>
|
|
422
442
|
<Input
|
|
423
443
|
id="licenseExpiry"
|
|
424
|
-
type="
|
|
444
|
+
type="datetime-local"
|
|
445
|
+
step="0.001"
|
|
446
|
+
aria-describedby="licenseExpiryHelp"
|
|
425
447
|
value={form.licenseExpiry}
|
|
426
448
|
onChange={(e) => setForm((prev) => ({ ...prev, licenseExpiry: e.target.value }))}
|
|
427
449
|
/>
|
|
450
|
+
<p id="licenseExpiryHelp" className="text-xs text-text-secondary">
|
|
451
|
+
Enter the recorded UTC time. Blank means expiry not recorded; clearing removes it.
|
|
452
|
+
A recorded expiry does not certify legal driving eligibility.
|
|
453
|
+
</p>
|
|
428
454
|
</div>
|
|
429
455
|
</div>
|
|
430
456
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
@@ -765,8 +791,8 @@ export default function DriverDetailsPage() {
|
|
|
765
791
|
}
|
|
766
792
|
>
|
|
767
793
|
<PagePurpose icon={<Users size={18} />}>
|
|
768
|
-
|
|
769
|
-
|
|
794
|
+
Review the recorded licensing, assigned vehicle, documents and HR facts before assignment.
|
|
795
|
+
These records do not certify legal driving eligibility or duty-hours availability.
|
|
770
796
|
</PagePurpose>
|
|
771
797
|
|
|
772
798
|
<NextSteps
|
|
@@ -822,9 +848,9 @@ export default function DriverDetailsPage() {
|
|
|
822
848
|
{driver.licenseNumber || '—'}
|
|
823
849
|
{driver.licenseType ? ` - ${driver.licenseType}` : ''}
|
|
824
850
|
</p>
|
|
825
|
-
<p className="text-sm text-text-secondary">
|
|
851
|
+
<p className="text-sm text-text-secondary break-all">
|
|
826
852
|
{driver.licenseExpiry
|
|
827
|
-
? `
|
|
853
|
+
? `Recorded expiry (UTC): ${driver.licenseExpiry.toISOString()}`
|
|
828
854
|
: 'Expiry not recorded'}
|
|
829
855
|
</p>
|
|
830
856
|
</div>
|
|
@@ -335,6 +335,7 @@ function FreightWorkbench({ actions }: { actions: ReturnType<typeof useFreightRe
|
|
|
335
335
|
key={row.id}
|
|
336
336
|
type="button"
|
|
337
337
|
variant="outline"
|
|
338
|
+
className="h-auto w-full max-w-full justify-start whitespace-normal break-all text-left"
|
|
338
339
|
onClick={() => setSelected(row.id)}
|
|
339
340
|
>
|
|
340
341
|
{row.facts.title} / {row.estimateReference} / version {row.version} —{' '}
|