@burdenoff/microfe-movethewheels 2026.1031.1 → 2026.1031.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/MoveTheWheelsRoot.js +144 -136
  2. package/dist/MoveTheWheelsRoutes.js +61 -57
  3. package/dist/components/ShipmentRiskDialog.js +228 -0
  4. package/dist/components/TrackingAlertList.js +149 -0
  5. package/dist/generated/tracking-quality-operations.d.ts +1154 -2
  6. package/dist/generated/wspace-types.d.ts +2895 -50
  7. package/dist/hooks/useEntityImport.js +68 -0
  8. package/dist/hooks/useShipmentRisk.js +72 -0
  9. package/dist/hooks/useTracking.js +96 -82
  10. package/dist/movethewheels/components/ShipmentRiskDialog.d.ts +20 -0
  11. package/dist/movethewheels/components/TrackingAlertList.d.ts +14 -0
  12. package/dist/movethewheels/hooks/useEntityImport.d.ts +19 -0
  13. package/dist/movethewheels/hooks/useShipmentRisk.d.ts +41 -0
  14. package/dist/movethewheels/pages/ImportExportPage.d.ts +9 -0
  15. package/dist/movethewheels/types/index.d.ts +15 -1
  16. package/dist/movethewheels/utils/entityImport.d.ts +109 -0
  17. package/dist/movethewheels/utils/trackingAlerts.d.ts +36 -0
  18. package/dist/node_modules/lucide-react/dist/esm/icons/arrow-down-up.js +22 -0
  19. package/dist/node_modules/lucide-react/dist/esm/icons/file-spreadsheet.js +30 -0
  20. package/dist/node_modules/lucide-react/dist/esm/icons/file-up.js +22 -0
  21. package/dist/pages/CustomersPage.js +88 -79
  22. package/dist/pages/DashboardPage.js +147 -151
  23. package/dist/pages/DriversPage.js +115 -106
  24. package/dist/pages/ImportExportPage.js +435 -0
  25. package/dist/pages/LiveTrackingPage.js +169 -169
  26. package/dist/pages/ProductsPage.js +85 -76
  27. package/dist/pages/VehiclesPage.js +99 -90
  28. package/dist/pages/WarehousesPage.js +97 -88
  29. package/dist/utils/entityImport.js +73 -0
  30. package/dist/utils/trackingAlerts.js +86 -0
  31. package/package.json +4 -2
  32. package/src/generated/tracking-quality-operations.ts +1519 -1
  33. package/src/generated/wspace-types.ts +3248 -51
  34. package/src/movethewheels/MoveTheWheelsRoot.tsx +8 -0
  35. package/src/movethewheels/MoveTheWheelsRoutes.tsx +4 -0
  36. package/src/movethewheels/components/ShipmentRiskDialog.tsx +315 -0
  37. package/src/movethewheels/components/TrackingAlertList.tsx +188 -0
  38. package/src/movethewheels/hooks/useEntityImport.ts +108 -0
  39. package/src/movethewheels/hooks/useShipmentRisk.ts +195 -0
  40. package/src/movethewheels/hooks/useTracking.ts +66 -22
  41. package/src/movethewheels/pages/CustomersPage.tsx +6 -1
  42. package/src/movethewheels/pages/DashboardPage.tsx +10 -20
  43. package/src/movethewheels/pages/DriversPage.tsx +6 -0
  44. package/src/movethewheels/pages/ImportExportPage.tsx +483 -0
  45. package/src/movethewheels/pages/LiveTrackingPage.tsx +20 -22
  46. package/src/movethewheels/pages/ProductsPage.tsx +14 -1
  47. package/src/movethewheels/pages/VehiclesPage.tsx +6 -0
  48. package/src/movethewheels/pages/WarehousesPage.tsx +6 -1
  49. package/src/movethewheels/types/index.ts +16 -1
  50. package/src/movethewheels/utils/entityImport.ts +239 -0
  51. package/src/movethewheels/utils/trackingAlerts.ts +151 -0
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Shipment delivery-risk forecast (BOFF-10397).
3
+ *
4
+ * Reads `mtwShipmentRiskForecast` for one shipment — the deterministic
5
+ * forecast wspace-movethewheels-svc computes from the shipment's legs,
6
+ * routes, tracking events, vehicle and carrier history — together with the
7
+ * few shipment fields the recommended actions need. The actions call the
8
+ * mutation the forecast names (`updateShipment` to re-promise,
9
+ * `addTrackingEvent` to record a tracking update); reassigning a vehicle
10
+ * happens on the order page.
11
+ */
12
+
13
+ import { useCallback, useEffect, useMemo, useState } from 'react';
14
+ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
15
+ import { executeGraphQL } from '../utils/graphqlClient';
16
+ import type { RiskSignalCode } from '../utils/trackingAlerts';
17
+
18
+ export type RiskLevel = 'NONE' | 'LOW' | 'MEDIUM' | 'HIGH';
19
+ export type RiskRecommendedAction =
20
+ 'NONE' | 'MONITOR' | 'REPROMISE_DELIVERY' | 'REQUEST_TRACKING_UPDATE' | 'REASSIGN_VEHICLE';
21
+
22
+ export interface RiskSignal {
23
+ code: RiskSignalCode;
24
+ weight: number;
25
+ minutes: number | null;
26
+ ratio: number | null;
27
+ }
28
+
29
+ export interface ShipmentRiskForecast {
30
+ shipmentId: string;
31
+ shipmentNumber: string;
32
+ status: string;
33
+ method: string;
34
+ promisedDelivery: string | null;
35
+ predictedDeliveryEarliest: string | null;
36
+ predictedDeliveryLatest: string | null;
37
+ predictedSlipMinutes: number;
38
+ riskScore: number;
39
+ riskLevel: RiskLevel;
40
+ atRisk: boolean;
41
+ breached: boolean;
42
+ signals: RiskSignal[];
43
+ recommendedAction: RiskRecommendedAction;
44
+ recommendedMutation: string | null;
45
+ evaluatedAt: string;
46
+ }
47
+
48
+ export interface RiskShipment {
49
+ id: string;
50
+ deliveryJobId: string | null;
51
+ estimatedDelivery: string | null;
52
+ }
53
+
54
+ const SHIPMENT_RISK_QUERY = `
55
+ query ShipmentRiskForecast($shipmentId: ID!) {
56
+ mtwShipmentRiskForecast(shipmentId: $shipmentId) {
57
+ shipmentId
58
+ shipmentNumber
59
+ status
60
+ method
61
+ promisedDelivery
62
+ predictedDeliveryEarliest
63
+ predictedDeliveryLatest
64
+ predictedSlipMinutes
65
+ riskScore
66
+ riskLevel
67
+ atRisk
68
+ breached
69
+ signals {
70
+ code
71
+ weight
72
+ minutes
73
+ ratio
74
+ }
75
+ recommendedAction
76
+ recommendedMutation
77
+ evaluatedAt
78
+ }
79
+ shipment(id: $shipmentId) {
80
+ id
81
+ deliveryJobId
82
+ estimatedDelivery
83
+ }
84
+ }
85
+ `;
86
+
87
+ const REPROMISE_SHIPMENT_MUTATION = `
88
+ mutation RepromiseShipment($id: ID!, $input: UpdateShipmentInput!) {
89
+ updateShipment(id: $id, input: $input) {
90
+ id
91
+ estimatedDelivery
92
+ }
93
+ }
94
+ `;
95
+
96
+ const RECORD_TRACKING_UPDATE_MUTATION = `
97
+ mutation RecordTrackingUpdate($input: AddTrackingEventInput!) {
98
+ addTrackingEvent(input: $input) {
99
+ id
100
+ timestamp
101
+ }
102
+ }
103
+ `;
104
+
105
+ function useGraphqlOptions() {
106
+ const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
107
+ useMoveTheWheels();
108
+ return useMemo(
109
+ () => ({
110
+ apiGatewayUrl,
111
+ authToken,
112
+ workspaceToken,
113
+ workspaceId,
114
+ tenantId,
115
+ actorId: currentUser?.id,
116
+ }),
117
+ [apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
118
+ );
119
+ }
120
+
121
+ export function useShipmentRisk(shipmentId: string | undefined) {
122
+ const graphQLOptions = useGraphqlOptions();
123
+ const [forecast, setForecast] = useState<ShipmentRiskForecast | null>(null);
124
+ const [shipment, setShipment] = useState<RiskShipment | null>(null);
125
+ const [isLoading, setIsLoading] = useState(false);
126
+ const [error, setError] = useState<Error | null>(null);
127
+
128
+ const load = useCallback(async () => {
129
+ if (!shipmentId) {
130
+ setForecast(null);
131
+ setShipment(null);
132
+ return;
133
+ }
134
+ setIsLoading(true);
135
+ setError(null);
136
+ try {
137
+ const data = await executeGraphQL<{
138
+ mtwShipmentRiskForecast: ShipmentRiskForecast;
139
+ shipment: RiskShipment | null;
140
+ }>(graphQLOptions, SHIPMENT_RISK_QUERY, { shipmentId });
141
+ setForecast(data.mtwShipmentRiskForecast);
142
+ setShipment(data.shipment);
143
+ } catch (fetchError) {
144
+ setError(
145
+ fetchError instanceof Error ? fetchError : new Error('Failed to load the risk forecast')
146
+ );
147
+ } finally {
148
+ setIsLoading(false);
149
+ }
150
+ }, [graphQLOptions, shipmentId]);
151
+
152
+ useEffect(() => {
153
+ void load();
154
+ }, [load]);
155
+
156
+ /** Move the shipment's promised delivery to `estimatedDelivery` (ISO date-time). */
157
+ const repromise = useCallback(
158
+ async (estimatedDelivery: string): Promise<void> => {
159
+ if (!shipmentId) return;
160
+ await executeGraphQL(
161
+ graphQLOptions,
162
+ REPROMISE_SHIPMENT_MUTATION,
163
+ { id: shipmentId, input: { estimatedDelivery } },
164
+ 'SINGLE_ATTEMPT'
165
+ );
166
+ await load();
167
+ },
168
+ [graphQLOptions, load, shipmentId]
169
+ );
170
+
171
+ /** Record a fresh in-transit tracking update reported by the driver or carrier. */
172
+ const recordTrackingUpdate = useCallback(
173
+ async (details: string): Promise<void> => {
174
+ if (!shipmentId) return;
175
+ await executeGraphQL(
176
+ graphQLOptions,
177
+ RECORD_TRACKING_UPDATE_MUTATION,
178
+ { input: { shipmentId, eventType: 'IN_TRANSIT', details } },
179
+ 'SINGLE_ATTEMPT'
180
+ );
181
+ await load();
182
+ },
183
+ [graphQLOptions, load, shipmentId]
184
+ );
185
+
186
+ return {
187
+ forecast,
188
+ shipment,
189
+ isLoading,
190
+ error,
191
+ refetch: () => void load(),
192
+ repromise,
193
+ recordTrackingUpdate,
194
+ };
195
+ }
@@ -13,6 +13,11 @@ import {
13
13
  useDrivers,
14
14
  } from './useFleet';
15
15
  import { mapOrder, type ApiOrder, useOrder } from './useOrders';
16
+ import {
17
+ mapTrackingAlert,
18
+ sortTrackingAlerts,
19
+ type ApiTrackingAlert,
20
+ } from '../utils/trackingAlerts';
16
21
 
17
22
  interface UseTrackingOptions {
18
23
  entityId?: string;
@@ -205,10 +210,27 @@ const ACTIVE_DELIVERIES_QUERY = `
205
210
  const TRACKING_ALERTS_QUERY = `
206
211
  query GetTrackingAlerts {
207
212
  trackingAlerts {
213
+ id
208
214
  shipmentId
209
215
  shipmentNumber
210
216
  alertType
211
217
  message
218
+ riskScore
219
+ promisedDelivery
220
+ predictedDelivery
221
+ signals
222
+ acknowledged
223
+ acknowledgedAt
224
+ }
225
+ }
226
+ `;
227
+
228
+ const ACKNOWLEDGE_TRACKING_ALERT_MUTATION = `
229
+ mutation AcknowledgeMtwTrackingAlert($shipmentId: ID!, $alertType: MtwTrackingAlertType!) {
230
+ acknowledgeMtwTrackingAlert(shipmentId: $shipmentId, alertType: $alertType) {
231
+ id
232
+ acknowledged
233
+ acknowledgedAt
212
234
  }
213
235
  }
214
236
  `;
@@ -631,24 +653,11 @@ export function useTrackingAlerts() {
631
653
  setIsLoading(true);
632
654
  setError(null);
633
655
  try {
634
- const data = await executeGraphQL<{
635
- trackingAlerts: Array<{
636
- shipmentId: string;
637
- shipmentNumber: string;
638
- alertType: string;
639
- message: string;
640
- }>;
641
- }>(graphQLOptions, TRACKING_ALERTS_QUERY);
642
-
643
- setAlerts(
644
- data.trackingAlerts.map((alert) => ({
645
- id: `${alert.shipmentId}-${alert.alertType}`,
646
- type: 'delay',
647
- severity: 'medium',
648
- message: `${alert.shipmentNumber}: ${alert.message}`,
649
- acknowledged: false,
650
- }))
656
+ const data = await executeGraphQL<{ trackingAlerts: ApiTrackingAlert[] }>(
657
+ graphQLOptions,
658
+ TRACKING_ALERTS_QUERY
651
659
  );
660
+ setAlerts(sortTrackingAlerts(data.trackingAlerts.map(mapTrackingAlert)));
652
661
  } catch (fetchError) {
653
662
  setError(
654
663
  fetchError instanceof Error ? fetchError : new Error('Failed to load tracking alerts')
@@ -664,11 +673,46 @@ export function useTrackingAlerts() {
664
673
 
665
674
  const unacknowledgedAlerts = useMemo(() => alerts.filter((item) => !item.acknowledged), [alerts]);
666
675
 
667
- const acknowledgeAlert = useCallback(async (alertId: string): Promise<void> => {
668
- setAlerts((previous) =>
669
- previous.map((item) => (item.id === alertId ? { ...item, acknowledged: true } : item))
670
- );
671
- }, []);
676
+ /**
677
+ * Persist the acknowledgement server-side (BOFF-10397) so it survives a
678
+ * reload and is shared with the rest of the workspace. The alert is hidden
679
+ * optimistically and restored if the mutation fails.
680
+ */
681
+ const acknowledgeAlert = useCallback(
682
+ async (alertId: string): Promise<void> => {
683
+ const target = alerts.find((item) => item.id === alertId);
684
+ if (!target?.shipmentId || !target.kind) return;
685
+ setAlerts((previous) =>
686
+ previous.map((item) => (item.id === alertId ? { ...item, acknowledged: true } : item))
687
+ );
688
+ try {
689
+ const data = await executeGraphQL<{
690
+ acknowledgeMtwTrackingAlert: {
691
+ id: string;
692
+ acknowledged: boolean;
693
+ acknowledgedAt: string;
694
+ };
695
+ }>(
696
+ graphQLOptions,
697
+ ACKNOWLEDGE_TRACKING_ALERT_MUTATION,
698
+ // Idempotent upsert server-side, so transient retries are safe.
699
+ { shipmentId: target.shipmentId, alertType: target.kind }
700
+ );
701
+ const acknowledgedAt = new Date(data.acknowledgeMtwTrackingAlert.acknowledgedAt);
702
+ setAlerts((previous) =>
703
+ previous.map((item) =>
704
+ item.id === alertId ? { ...item, acknowledged: true, acknowledgedAt } : item
705
+ )
706
+ );
707
+ } catch (ackError) {
708
+ setAlerts((previous) =>
709
+ previous.map((item) => (item.id === alertId ? { ...item, acknowledged: false } : item))
710
+ );
711
+ throw ackError instanceof Error ? ackError : new Error('Failed to acknowledge alert');
712
+ }
713
+ },
714
+ [alerts, graphQLOptions]
715
+ );
672
716
 
673
717
  return {
674
718
  alerts,
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { useState } from 'react';
6
- import { Users, Plus, RefreshCw, Download, Star, Building2 } from 'lucide-react';
6
+ import { Users, Plus, RefreshCw, Download, Star, Building2, Upload } from 'lucide-react';
7
7
  import { PageLayout } from '../components/PageLayout';
8
8
  import { MetricCard, MetricGrid } from '../components/MetricCard';
9
9
  import { DataTable, type Column } from '../components/DataTable';
@@ -111,6 +111,11 @@ export default function CustomersPage() {
111
111
  }}
112
112
  actions={[
113
113
  { label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
114
+ {
115
+ label: 'Import',
116
+ icon: <Upload size={16} />,
117
+ onSelect: () => goTo('import-export?entity=CUSTOMER'),
118
+ },
114
119
  {
115
120
  label: isExporting ? 'Exporting…' : 'Export',
116
121
  icon: <Download size={16} />,
@@ -21,6 +21,7 @@ import {
21
21
  import { PageLayout, PageSection } from '../components/PageLayout';
22
22
  import { MetricCard, MetricGrid, type MetricDelta } from '../components/MetricCard';
23
23
  import { StatusBadge } from '../components/StatusBadge';
24
+ import { TrackingAlertList } from '../components/TrackingAlertList';
24
25
  import { LogisticsMap, type LogisticsMarker } from '../components/maps';
25
26
  import { VolumeBar } from '../components/charts';
26
27
  import { deliveryMarkerStatus, firstRealLatLng } from '../utils/mapMarkers';
@@ -67,7 +68,7 @@ export default function DashboardPage() {
67
68
  const { stats: orderStats } = useOrderStats();
68
69
  const { stats: fleetStats } = useFleetStats();
69
70
  const { activeDeliveries } = useLiveTracking();
70
- const { unacknowledgedAlerts } = useTrackingAlerts();
71
+ const { unacknowledgedAlerts, acknowledgeAlert, refetch: refetchAlerts } = useTrackingAlerts();
71
72
 
72
73
  const orderSpark = useMemo(
73
74
  () => analytics.trends.dailyOrders.map((d) => d.orders),
@@ -265,25 +266,14 @@ export default function DashboardPage() {
265
266
  </CardHeader>
266
267
  <CardContent>
267
268
  {unacknowledgedAlerts.length > 0 ? (
268
- <div className="space-y-3">
269
- {unacknowledgedAlerts.slice(0, 4).map((alert) => (
270
- <div
271
- key={alert.id}
272
- className={`rounded-lg p-3 text-sm ${
273
- alert.severity === 'critical' || alert.severity === 'high'
274
- ? 'bg-status-error-bg-subtle'
275
- : 'bg-status-warning-bg-subtle'
276
- }`}
277
- >
278
- <p className="font-medium">{alert.message}</p>
279
- <p className="mt-1 text-xs text-text-secondary">
280
- {alert.timestamp
281
- ? formatDate(alert.timestamp, 'relative')
282
- : 'Time not recorded'}
283
- </p>
284
- </div>
285
- ))}
286
- </div>
269
+ <TrackingAlertList
270
+ alerts={unacknowledgedAlerts}
271
+ limit={4}
272
+ t={t}
273
+ onAcknowledge={acknowledgeAlert}
274
+ onOpenOrder={(deliveryJobId) => goTo(`orders/${deliveryJobId}`)}
275
+ onChanged={refetchAlerts}
276
+ />
287
277
  ) : (
288
278
  <p className="py-4 text-center text-sm text-text-secondary">
289
279
  {t(
@@ -14,6 +14,7 @@ import {
14
14
  UserMinus,
15
15
  Filter,
16
16
  Truck,
17
+ Upload,
17
18
  } from 'lucide-react';
18
19
  import { PageLayout } from '../components/PageLayout';
19
20
  import { MetricCard, MetricGrid } from '../components/MetricCard';
@@ -231,6 +232,11 @@ export default function DriversPage() {
231
232
  }}
232
233
  actions={[
233
234
  { label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
235
+ {
236
+ label: 'Import',
237
+ icon: <Upload size={16} />,
238
+ onSelect: () => goTo('import-export?entity=DRIVER'),
239
+ },
234
240
  {
235
241
  label: isExporting ? 'Exporting…' : 'Export',
236
242
  icon: <Download size={16} />,