@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.
Files changed (88) 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-invoice/InvoiceForm.js +366 -0
  7. package/dist/components/freight-invoice/InvoiceResults.js +240 -0
  8. package/dist/generated/consolidation-operations.d.ts +195 -265
  9. package/dist/generated/container-condition-operations.d.ts +124 -187
  10. package/dist/generated/container-operations.d.ts +59 -85
  11. package/dist/generated/empty-reuse-operations.d.ts +165 -271
  12. package/dist/generated/free-time-operations.d.ts +120 -184
  13. package/dist/generated/freight-cost-operations.d.ts +307 -476
  14. package/dist/generated/freight-invoice-operations.d.ts +1447 -0
  15. package/dist/generated/logistics-procedure-operations.d.ts +100 -156
  16. package/dist/generated/pending-pick-operations.d.ts +769 -1025
  17. package/dist/generated/shipment-status-authority-operations.d.ts +82 -103
  18. package/dist/generated/stock-binding-operations.d.ts +188 -260
  19. package/dist/generated/stock-observation-operations.d.ts +103 -138
  20. package/dist/generated/tracking-quality-operations.d.ts +27 -41
  21. package/dist/generated/trip-utilization-operations.d.ts +137 -185
  22. package/dist/generated/wspace-operations.d.ts +628 -2272
  23. package/dist/generated/wspace-types.d.ts +4 -23
  24. package/dist/hooks/useInvoiceComparisonRecovery.js +118 -0
  25. package/dist/movethewheels/components/ContainerEvidencePicker.d.ts +2 -1
  26. package/dist/movethewheels/components/freight-invoice/InvoiceForm.d.ts +19 -0
  27. package/dist/movethewheels/components/freight-invoice/InvoiceResults.d.ts +9 -0
  28. package/dist/movethewheels/hooks/useFleet.d.ts +1 -1
  29. package/dist/movethewheels/hooks/useFleetIntelligence.d.ts +1 -1
  30. package/dist/movethewheels/hooks/useInvoiceComparisonRecovery.d.ts +22 -0
  31. package/dist/movethewheels/hooks/useProcedureSelection.d.ts +1 -3
  32. package/dist/movethewheels/pages/FreightInvoicePage.d.ts +10 -0
  33. package/dist/movethewheels/utils/driverLicenseExpiry.d.ts +6 -0
  34. package/dist/movethewheels/utils/invoiceComparison.d.ts +32 -0
  35. package/dist/pages/DriverDetailsPage.js +306 -282
  36. package/dist/pages/FreightEstimatePage.js +1 -0
  37. package/dist/pages/FreightInvoicePage.js +346 -0
  38. package/dist/src/generated/consolidation-operations.js +3961 -503
  39. package/dist/src/generated/container-condition-operations.js +2968 -206
  40. package/dist/src/generated/container-operations.js +997 -99
  41. package/dist/src/generated/empty-reuse-operations.js +4811 -747
  42. package/dist/src/generated/free-time-operations.js +2824 -233
  43. package/dist/src/generated/freight-cost-operations.js +4852 -812
  44. package/dist/src/generated/freight-invoice-operations.js +4894 -0
  45. package/dist/src/generated/logistics-procedure-operations.js +2707 -360
  46. package/dist/src/generated/pending-pick-operations.js +8244 -1196
  47. package/dist/src/generated/shipment-status-authority-operations.js +1157 -84
  48. package/dist/src/generated/stock-binding-operations.js +2779 -173
  49. package/dist/src/generated/stock-observation-operations.js +1274 -91
  50. package/dist/src/generated/tracking-quality-operations.js +407 -42
  51. package/dist/src/generated/trip-utilization-operations.js +1765 -138
  52. package/dist/src/generated/wspace-operations.js +428 -706
  53. package/dist/utils/driverLicenseExpiry.js +14 -0
  54. package/dist/utils/invoiceComparison.js +272 -0
  55. package/package.json +29 -29
  56. package/src/generated/consolidation-operations.ts +60 -559
  57. package/src/generated/container-condition-operations.ts +63 -265
  58. package/src/generated/container-operations.ts +37 -132
  59. package/src/generated/empty-reuse-operations.ts +83 -824
  60. package/src/generated/free-time-operations.ts +55 -284
  61. package/src/generated/freight-cost-operations.ts +42 -848
  62. package/src/generated/freight-invoice-operations.ts +830 -0
  63. package/src/generated/logistics-procedure-operations.ts +60 -414
  64. package/src/generated/pending-pick-operations.ts +48 -1239
  65. package/src/generated/shipment-status-authority-operations.ts +33 -113
  66. package/src/generated/stock-binding-operations.ts +29 -198
  67. package/src/generated/stock-observation-operations.ts +24 -111
  68. package/src/generated/tracking-quality-operations.ts +19 -57
  69. package/src/generated/trip-utilization-operations.ts +32 -166
  70. package/src/generated/wspace-operations.ts +284 -2906
  71. package/src/generated/wspace-types.ts +2 -8
  72. package/src/movethewheels/MoveTheWheelsRoot.tsx +6 -0
  73. package/src/movethewheels/MoveTheWheelsRoutes.tsx +2 -0
  74. package/src/movethewheels/components/ContainerEvidencePicker.tsx +12 -4
  75. package/src/movethewheels/components/FleetAlertsPanel.tsx +4 -2
  76. package/src/movethewheels/components/freight-invoice/InvoiceForm.tsx +412 -0
  77. package/src/movethewheels/components/freight-invoice/InvoiceResults.tsx +162 -0
  78. package/src/movethewheels/hooks/useFleet.ts +1 -1
  79. package/src/movethewheels/hooks/useFleetIntelligence.ts +5 -1
  80. package/src/movethewheels/hooks/useInvoiceComparisonRecovery.ts +227 -0
  81. package/src/movethewheels/pages/DriverDetailsPage.tsx +35 -9
  82. package/src/movethewheels/pages/FreightEstimatePage.tsx +1 -0
  83. package/src/movethewheels/pages/FreightInvoicePage.tsx +477 -0
  84. package/src/movethewheels/utils/driverLicenseExpiry.ts +25 -0
  85. package/src/movethewheels/utils/invoiceComparison.ts +520 -0
  86. package/src/operations/freight-invoice/comparison.graphql +40 -0
  87. package/src/operations/wspace/freight-invoice-evidence.graphql +6 -0
  88. 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
- 'LICENSE_EXPIRED' | 'LICENSE_EXPIRING' | 'MAINTENANCE_OVERDUE' | 'MAINTENANCE_DUE_SOON';
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: toDateInput(driver.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: fromDateInput(form.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
- <Label htmlFor="licenseExpiry">License Expiry</Label>
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="date"
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
- A driver&apos;s full profile — licensing, assigned vehicle, documents and HR record. Use it
769
- to confirm someone is road-ready and compliant before assigning them an order or route.
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
- ? `Expires: ${formatDate(driver.licenseExpiry)}`
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} —{' '}