@burdenoff/microfe-movethewheels 2026.1010.2 → 2026.1011.1
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/dist/MoveTheWheelsRoot.js +7 -1
- package/dist/MoveTheWheelsRoutes.js +33 -29
- package/dist/generated/tracking-quality-operations.d.ts +4302 -0
- package/dist/hooks/useTrackingEventQuality.js +34 -0
- package/dist/movethewheels/hooks/useTrackingEventQuality.d.ts +13 -0
- package/dist/movethewheels/pages/TrackingEventQualityPage.d.ts +1 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/radio-tower.js +36 -0
- package/dist/pages/TrackingEventQualityPage.js +219 -0
- package/dist/src/generated/tracking-quality-operations.js +44 -0
- package/package.json +15 -7
- package/patches/README.md +25 -0
- package/patches/braces@3.0.3.patch +87 -0
- package/src/generated/tracking-quality-operations.ts +5323 -0
- package/src/movethewheels/MoveTheWheelsRoot.tsx +8 -0
- package/src/movethewheels/MoveTheWheelsRoutes.tsx +2 -0
- package/src/movethewheels/hooks/useTrackingEventQuality.ts +68 -0
- package/src/movethewheels/pages/TrackingEventQualityPage.tsx +239 -0
- package/src/operations/tracking-quality/quality.graphql +15 -0
|
@@ -92,6 +92,14 @@ const NavRegistrar: FC<Pick<MoveTheWheelsRootProps, 'registerNavItems' | 'basePa
|
|
|
92
92
|
path: joinPath(basePath, 'tracking'),
|
|
93
93
|
icon: <Map size={16} />,
|
|
94
94
|
order: 3,
|
|
95
|
+
children: [
|
|
96
|
+
{
|
|
97
|
+
id: 'mtw-tracking-quality',
|
|
98
|
+
label: tr('movethewheels.root.nav.trackingQuality', 'Tracking event quality'),
|
|
99
|
+
path: joinPath(basePath, 'tracking/event-quality'),
|
|
100
|
+
icon: <Map size={16} />,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
95
103
|
},
|
|
96
104
|
{
|
|
97
105
|
id: 'mtw-consolidation',
|
|
@@ -55,6 +55,7 @@ const OrdersListPage = lazy(() => import('./pages/OrdersListPage'));
|
|
|
55
55
|
const OrderDetailsPage = lazy(() => import('./pages/OrderDetailsPage'));
|
|
56
56
|
const CreateOrderPage = lazy(() => import('./pages/CreateOrderPage'));
|
|
57
57
|
const TrackingPage = lazy(() => import('./pages/TrackingPage'));
|
|
58
|
+
const TrackingEventQualityPage = lazy(() => import('./pages/TrackingEventQualityPage'));
|
|
58
59
|
const LiveTrackingPage = lazy(() => import('./pages/LiveTrackingPage'));
|
|
59
60
|
const FleetPage = lazy(() => import('./pages/FleetPage'));
|
|
60
61
|
const VehiclesPage = lazy(() => import('./pages/VehiclesPage'));
|
|
@@ -108,6 +109,7 @@ export function MoveTheWheelsRoutes() {
|
|
|
108
109
|
{/* Tracking */}
|
|
109
110
|
<Route path="tracking" element={<TrackingPage />} />
|
|
110
111
|
<Route path="tracking/live" element={<LiveTrackingPage />} />
|
|
112
|
+
<Route path="tracking/event-quality" element={<TrackingEventQualityPage />} />
|
|
111
113
|
|
|
112
114
|
{/* Driver ePOD capture (BOFF-4399) */}
|
|
113
115
|
<Route path="deliveries/mine" element={<DeliveryCapturePage />} />
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TrackingQualityIntegrationsDocument,
|
|
3
|
+
TrackingQualityDeliveriesDocument,
|
|
4
|
+
TrackingQualityShipmentDocument,
|
|
5
|
+
} from '../../generated/tracking-quality-operations';
|
|
6
|
+
export {
|
|
7
|
+
TrackingQualityIntegrationsDocument as QualityIntegrationsDocument,
|
|
8
|
+
TrackingQualityDeliveriesDocument as QualityDeliveriesDocument,
|
|
9
|
+
TrackingQualityShipmentDocument as QualityShipmentDocument,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface TrackingUpdateOutcome {
|
|
13
|
+
index: number;
|
|
14
|
+
externalEventId: string | null;
|
|
15
|
+
shipmentReference: string;
|
|
16
|
+
shipmentId: string | null;
|
|
17
|
+
status: 'ACCEPTED' | 'DUPLICATE' | 'CONFLICT' | 'REJECTED';
|
|
18
|
+
errorCode: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const statuses = new Set(['ACCEPTED', 'DUPLICATE', 'CONFLICT', 'REJECTED']);
|
|
22
|
+
const codes = new Set([
|
|
23
|
+
'KEY_CONFLICT',
|
|
24
|
+
'BUDGET_EXHAUSTED',
|
|
25
|
+
'INTEGRATION_UNAVAILABLE',
|
|
26
|
+
'SHIPMENT_UNAVAILABLE',
|
|
27
|
+
'UPDATE_REJECTED',
|
|
28
|
+
]);
|
|
29
|
+
const bounded = (value: unknown, max: number): value is string =>
|
|
30
|
+
typeof value === 'string' && value.length <= max;
|
|
31
|
+
const nullable = (value: unknown, max: number): value is string | null =>
|
|
32
|
+
value === null || bounded(value, max);
|
|
33
|
+
|
|
34
|
+
/** JSON scalar decoding fails closed; legacy delivery history has no outcomes. */
|
|
35
|
+
export function trackingOutcomes(value: unknown): TrackingUpdateOutcome[] | null {
|
|
36
|
+
if (!Array.isArray(value) || value.length > 5) return null;
|
|
37
|
+
const result: TrackingUpdateOutcome[] = [];
|
|
38
|
+
for (const item of value) {
|
|
39
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item)) return null;
|
|
40
|
+
const row: Record<string, unknown> = item;
|
|
41
|
+
if (
|
|
42
|
+
!Number.isInteger(row.index) ||
|
|
43
|
+
typeof row.index !== 'number' ||
|
|
44
|
+
row.index !== result.length ||
|
|
45
|
+
row.index < 0 ||
|
|
46
|
+
row.index > 4 ||
|
|
47
|
+
!nullable(row.externalEventId, 255) ||
|
|
48
|
+
!bounded(row.shipmentReference, 255) ||
|
|
49
|
+
!nullable(row.shipmentId, 255) ||
|
|
50
|
+
typeof row.status !== 'string' ||
|
|
51
|
+
!statuses.has(row.status) ||
|
|
52
|
+
(row.status === 'CONFLICT' && row.shipmentId !== null) ||
|
|
53
|
+
!(row.errorCode === null || (typeof row.errorCode === 'string' && codes.has(row.errorCode)))
|
|
54
|
+
) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
// The finite status/code guards justify narrowing the untyped JSON scalar.
|
|
58
|
+
result.push({
|
|
59
|
+
index: row.index,
|
|
60
|
+
externalEventId: row.externalEventId,
|
|
61
|
+
shipmentReference: row.shipmentReference,
|
|
62
|
+
shipmentId: row.shipmentId,
|
|
63
|
+
status: row.status as TrackingUpdateOutcome['status'],
|
|
64
|
+
errorCode: row.errorCode,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { RadioTower } from 'lucide-react';
|
|
3
|
+
import { PageLayout } from '../components/PageLayout';
|
|
4
|
+
import { Button } from '../components/ui';
|
|
5
|
+
import { reuseCard, reuseSelect, ReuseState } from '../components/ReuseControls';
|
|
6
|
+
import { useConditionQuery } from '../hooks/useContainerCondition';
|
|
7
|
+
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
8
|
+
import {
|
|
9
|
+
QualityIntegrationsDocument,
|
|
10
|
+
QualityDeliveriesDocument,
|
|
11
|
+
QualityShipmentDocument,
|
|
12
|
+
trackingOutcomes,
|
|
13
|
+
} from '../hooks/useTrackingEventQuality';
|
|
14
|
+
import type {
|
|
15
|
+
TrackingQualityIntegrationsQuery,
|
|
16
|
+
TrackingQualityDeliveriesQuery,
|
|
17
|
+
TrackingQualityShipmentQuery,
|
|
18
|
+
} from '../../generated/tracking-quality-operations';
|
|
19
|
+
|
|
20
|
+
const LIMIT = 20;
|
|
21
|
+
function Pager({
|
|
22
|
+
offset,
|
|
23
|
+
length,
|
|
24
|
+
busy,
|
|
25
|
+
onChange,
|
|
26
|
+
}: {
|
|
27
|
+
offset: number;
|
|
28
|
+
length: number | undefined;
|
|
29
|
+
busy: boolean;
|
|
30
|
+
onChange: (offset: number) => void;
|
|
31
|
+
}) {
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
34
|
+
<Button
|
|
35
|
+
variant="outline"
|
|
36
|
+
disabled={busy || offset === 0}
|
|
37
|
+
onClick={() => onChange(Math.max(0, offset - LIMIT))}
|
|
38
|
+
>
|
|
39
|
+
Previous page
|
|
40
|
+
</Button>
|
|
41
|
+
<span>{length === undefined ? 'Page unavailable' : `${length} records on this page`}</span>
|
|
42
|
+
<Button
|
|
43
|
+
variant="outline"
|
|
44
|
+
disabled={busy || length !== LIMIT || offset >= 9980}
|
|
45
|
+
onClick={() => onChange(offset + LIMIT)}
|
|
46
|
+
>
|
|
47
|
+
Next page
|
|
48
|
+
</Button>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default function TrackingEventQualityPage() {
|
|
54
|
+
const { workspaceId, tenantId, currentUser, apiGatewayUrl } = useMoveTheWheels();
|
|
55
|
+
return (
|
|
56
|
+
<PageLayout
|
|
57
|
+
title="Tracking event quality"
|
|
58
|
+
subtitle="Review delivery results before investigating a shipment."
|
|
59
|
+
icon={<RadioTower aria-hidden="true" />}
|
|
60
|
+
>
|
|
61
|
+
<QualityWorkbench key={`${workspaceId}:${tenantId}:${currentUser?.id}:${apiGatewayUrl}`} />
|
|
62
|
+
</PageLayout>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
function QualityWorkbench() {
|
|
66
|
+
const [integrationOffset, setIntegrationOffset] = useState(0);
|
|
67
|
+
const [selected, setSelected] = useState('');
|
|
68
|
+
const [offset, setOffset] = useState(0);
|
|
69
|
+
const [revision, setRevision] = useState(0);
|
|
70
|
+
const [shipmentId, setShipmentId] = useState<string | null>(null);
|
|
71
|
+
const integrations = useConditionQuery<TrackingQualityIntegrationsQuery>(
|
|
72
|
+
QualityIntegrationsDocument,
|
|
73
|
+
{ pagination: { limit: LIMIT, offset: integrationOffset } },
|
|
74
|
+
revision
|
|
75
|
+
);
|
|
76
|
+
const deliveries = useConditionQuery<TrackingQualityDeliveriesQuery>(
|
|
77
|
+
QualityDeliveriesDocument,
|
|
78
|
+
selected ? { id: selected, pagination: { limit: LIMIT, offset } } : null,
|
|
79
|
+
revision
|
|
80
|
+
);
|
|
81
|
+
const options = integrations.data?.integrations;
|
|
82
|
+
const records = deliveries.data?.integration?.events;
|
|
83
|
+
return (
|
|
84
|
+
<div className="space-y-6">
|
|
85
|
+
<p>
|
|
86
|
+
Matching event keys and payloads create no additional tracking event. A changed payload with
|
|
87
|
+
the same key is a conflict. Updates without a key can create new events.
|
|
88
|
+
</p>
|
|
89
|
+
<p>
|
|
90
|
+
Each delivery admits at most five updates. An oversized or malformed delivery is rejected
|
|
91
|
+
before updates are recorded. Delivery history can include older records without item
|
|
92
|
+
results.
|
|
93
|
+
</p>
|
|
94
|
+
<section className={reuseCard} aria-label="Integration selection">
|
|
95
|
+
<ReuseState loading={integrations.loading} error={integrations.error} />
|
|
96
|
+
{options?.length === 0 && <p>No integrations on this page.</p>}
|
|
97
|
+
<label className="block">
|
|
98
|
+
Integration
|
|
99
|
+
<select
|
|
100
|
+
className={reuseSelect}
|
|
101
|
+
value={selected}
|
|
102
|
+
disabled={!options || integrations.loading}
|
|
103
|
+
onChange={(event) => {
|
|
104
|
+
setSelected(event.target.value);
|
|
105
|
+
setOffset(0);
|
|
106
|
+
setShipmentId(null);
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<option value="">Choose an integration</option>
|
|
110
|
+
{options?.map((integration) => (
|
|
111
|
+
<option key={integration.id} value={integration.id}>
|
|
112
|
+
{integration.name} · {integration.provider} · {integration.status}
|
|
113
|
+
</option>
|
|
114
|
+
))}
|
|
115
|
+
</select>
|
|
116
|
+
</label>
|
|
117
|
+
<Pager
|
|
118
|
+
offset={integrationOffset}
|
|
119
|
+
length={options?.length}
|
|
120
|
+
busy={integrations.loading}
|
|
121
|
+
onChange={(next) => {
|
|
122
|
+
setIntegrationOffset(next);
|
|
123
|
+
setSelected('');
|
|
124
|
+
setOffset(0);
|
|
125
|
+
setShipmentId(null);
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
</section>
|
|
129
|
+
{selected && (
|
|
130
|
+
<section className={reuseCard} aria-label="Tracking delivery history">
|
|
131
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
132
|
+
<h2 className="text-lg font-semibold">Delivery history</h2>
|
|
133
|
+
<Button
|
|
134
|
+
variant="outline"
|
|
135
|
+
disabled={deliveries.loading}
|
|
136
|
+
onClick={() => setRevision((value) => value + 1)}
|
|
137
|
+
>
|
|
138
|
+
Refresh results
|
|
139
|
+
</Button>
|
|
140
|
+
</div>
|
|
141
|
+
<ReuseState loading={deliveries.loading} error={deliveries.error} />
|
|
142
|
+
{deliveries.data?.integration === null && (
|
|
143
|
+
<p role="status">Integration unavailable in this workspace.</p>
|
|
144
|
+
)}
|
|
145
|
+
{records?.length === 0 && <p>No deliveries on this page.</p>}
|
|
146
|
+
{records?.map((delivery) => {
|
|
147
|
+
const outcomes = trackingOutcomes(delivery.outcomes);
|
|
148
|
+
return (
|
|
149
|
+
<article key={delivery.id} className="space-y-3 rounded-lg border p-4">
|
|
150
|
+
<h3 className="font-semibold">
|
|
151
|
+
{delivery.status} · {new Date(delivery.receivedAt).toLocaleString()}
|
|
152
|
+
</h3>
|
|
153
|
+
{outcomes === null ? (
|
|
154
|
+
<p>Per-update results unavailable for this delivery.</p>
|
|
155
|
+
) : outcomes.length === 0 ? (
|
|
156
|
+
<p>
|
|
157
|
+
No normalized updates were attempted. This is not a count of raw sender items.
|
|
158
|
+
</p>
|
|
159
|
+
) : (
|
|
160
|
+
<ol className="space-y-3">
|
|
161
|
+
{outcomes.map((outcome) => (
|
|
162
|
+
<li key={outcome.index} className="break-words rounded-md bg-bg-surface p-3">
|
|
163
|
+
<p>
|
|
164
|
+
Update {outcome.index + 1}: <strong>{outcome.status}</strong>
|
|
165
|
+
</p>
|
|
166
|
+
<p>Event key: {outcome.externalEventId ?? 'Missing — not deduplicated'}</p>
|
|
167
|
+
<p>Shipment reference: {outcome.shipmentReference || 'Not recorded'}</p>
|
|
168
|
+
{outcome.errorCode && (
|
|
169
|
+
<p>Reason: {outcome.errorCode.replaceAll('_', ' ').toLowerCase()}</p>
|
|
170
|
+
)}
|
|
171
|
+
{outcome.status === 'CONFLICT' && (
|
|
172
|
+
<p>The earlier receipt stays unchanged; this update adds no event.</p>
|
|
173
|
+
)}
|
|
174
|
+
{outcome.shipmentId && (
|
|
175
|
+
<Button
|
|
176
|
+
variant="outline"
|
|
177
|
+
onClick={() => setShipmentId(outcome.shipmentId)}
|
|
178
|
+
>
|
|
179
|
+
View shipment
|
|
180
|
+
</Button>
|
|
181
|
+
)}
|
|
182
|
+
</li>
|
|
183
|
+
))}
|
|
184
|
+
</ol>
|
|
185
|
+
)}
|
|
186
|
+
</article>
|
|
187
|
+
);
|
|
188
|
+
})}
|
|
189
|
+
<Pager
|
|
190
|
+
offset={offset}
|
|
191
|
+
length={records?.length}
|
|
192
|
+
busy={deliveries.loading}
|
|
193
|
+
onChange={setOffset}
|
|
194
|
+
/>
|
|
195
|
+
</section>
|
|
196
|
+
)}
|
|
197
|
+
{shipmentId && (
|
|
198
|
+
<QualityShipment key={shipmentId} id={shipmentId} onClose={() => setShipmentId(null)} />
|
|
199
|
+
)}
|
|
200
|
+
<p className="text-sm text-text-secondary">
|
|
201
|
+
These results describe recorded ingestion, not physical shipment truth. Ordering, late
|
|
202
|
+
updates and provider corrections need separate policies.
|
|
203
|
+
</p>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function QualityShipment({ id, onClose }: { id: string; onClose: () => void }) {
|
|
209
|
+
const query = useConditionQuery<TrackingQualityShipmentQuery>(QualityShipmentDocument, { id });
|
|
210
|
+
return (
|
|
211
|
+
<section className={reuseCard} aria-label="Shipment tracking summary">
|
|
212
|
+
<h2 className="text-lg font-semibold">Shipment tracking summary</h2>
|
|
213
|
+
<Button variant="outline" onClick={onClose}>
|
|
214
|
+
Close shipment summary
|
|
215
|
+
</Button>
|
|
216
|
+
<ReuseState loading={query.loading} error={query.error} />
|
|
217
|
+
{query.data?.shipment === null && <p>Shipment unavailable in this workspace.</p>}
|
|
218
|
+
{query.data?.shipment && (
|
|
219
|
+
<>
|
|
220
|
+
<p>
|
|
221
|
+
{query.data.shipment.shipmentNumber} · {query.data.shipment.status}
|
|
222
|
+
</p>
|
|
223
|
+
<p>Tracking number: {query.data.shipment.trackingNumber ?? 'Not recorded'}</p>
|
|
224
|
+
<p>
|
|
225
|
+
{query.data.shipment.trackingEvents.length} tracking events on the first page (up to
|
|
226
|
+
20).
|
|
227
|
+
</p>
|
|
228
|
+
<ul>
|
|
229
|
+
{query.data.shipment.trackingEvents.map((event) => (
|
|
230
|
+
<li key={event.id}>
|
|
231
|
+
{event.eventType} · {new Date(event.timestamp).toLocaleString()}
|
|
232
|
+
</li>
|
|
233
|
+
))}
|
|
234
|
+
</ul>
|
|
235
|
+
</>
|
|
236
|
+
)}
|
|
237
|
+
</section>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
query TrackingQualityIntegrations($pagination: PaginationInput!) {
|
|
2
|
+
integrations(pagination: $pagination) { id name provider status }
|
|
3
|
+
}
|
|
4
|
+
query TrackingQualityDeliveries($id: ID!, $pagination: PaginationInput!) {
|
|
5
|
+
integration(id: $id) {
|
|
6
|
+
id name provider status
|
|
7
|
+
events(pagination: $pagination) { id status receivedAt processedAt outcomes }
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
query TrackingQualityShipment($id: ID!) {
|
|
11
|
+
shipment(id: $id) {
|
|
12
|
+
id shipmentNumber status trackingNumber
|
|
13
|
+
trackingEvents(pagination: {limit: 20, offset: 0}) { id eventType timestamp }
|
|
14
|
+
}
|
|
15
|
+
}
|