@burdenoff/microfe-movethewheels 2026.703.4 → 2026.703.6

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 (150) hide show
  1. package/dist/{AIAssistantPage-3YOnHO9Y.js → AIAssistantPage-BmILdHgO.js} +16 -15
  2. package/dist/AnalyticsPage-BSlsiSDE.js +194 -0
  3. package/dist/{CreateOrderPage-DRAgGo9i.js → CreateOrderPage-DaCgVaO3.js} +40 -40
  4. package/dist/{CustomerDetailsPage-BCeMmOFe.js → CustomerDetailsPage-DErpbafW.js} +71 -71
  5. package/dist/CustomersPage-C1lRU6xs.js +205 -0
  6. package/dist/DashboardPage-Q5-5EKAU.js +542 -0
  7. package/dist/{DataTable-XvUcTjwi.js → DataTable-KjzbwnRL.js} +31 -31
  8. package/dist/{DocumentsCard-U2lSe-cn.js → DocumentsCard-T9y6cv4-.js} +25 -25
  9. package/dist/{DriverDetailsPage-BQrYd_Lp.js → DriverDetailsPage-DnHGkDC7.js} +64 -64
  10. package/dist/DriversPage-C0ujWVOn.js +221 -0
  11. package/dist/FinancePage-CA2XEgsQ.js +175 -0
  12. package/dist/{FleetPage-3ZHsB-TN.js → FleetPage-DrvXW7wK.js} +22 -22
  13. package/dist/{FormError-cuLV2jVD.js → FormError-CRsMP7TQ.js} +1 -1
  14. package/dist/{ImportExportPage-C6f7DqhM.js → ImportExportPage-Cac9HjFx.js} +12 -12
  15. package/dist/InventoryPage-Ezf4gqKI.js +259 -0
  16. package/dist/LiveTrackingPage-CzbVwDqK.js +429 -0
  17. package/dist/{MarketplacePage-Bh_2ns4v.js → MarketplacePage-BHDmSQmo.js} +116 -116
  18. package/dist/MetricCard-DQlDNqmK.js +191 -0
  19. package/dist/{Notice-CSoDYKw9.js → Notice-GmA7JDR3.js} +2 -2
  20. package/dist/{OrderDetailsPage-zCj0lW0j.js → OrderDetailsPage-CR0Wc1aN.js} +58 -58
  21. package/dist/OrdersListPage-C8Bu6wXM.js +386 -0
  22. package/dist/{PageLayout-D4LDHJJq.js → PageLayout-DX1Ou6V6.js} +164 -134
  23. package/dist/{ProductDetailsPage-wsxAQkA0.js → ProductDetailsPage-B_ns2MOe.js} +55 -55
  24. package/dist/ProductsPage-c92BsEoA.js +186 -0
  25. package/dist/ReportsPage-CPLXtLT2.js +377 -0
  26. package/dist/{RouteDetailsPage-DyMJE9AS.js → RouteDetailsPage-DOfcRiFv.js} +208 -181
  27. package/dist/{RoutesPage-Dtc669yc.js → RoutesPage-BZGublby.js} +12 -12
  28. package/dist/{SettingsPage-CpH9xhQg.js → SettingsPage-5cweX_xh.js} +15 -15
  29. package/dist/Sparkline-zSXvG6vA.js +16729 -0
  30. package/dist/{StatusBadge-C87mDpDP.js → StatusBadge-oZjownr1.js} +7 -7
  31. package/dist/TrackingPage-Ln9qVflK.js +368 -0
  32. package/dist/{VehicleDetailsPage-DldYNsYV.js → VehicleDetailsPage-CrkzE0dN.js} +53 -53
  33. package/dist/VehiclesPage-CTQWH7af.js +184 -0
  34. package/dist/{WarehouseDetailsPage-CMG7or6G.js → WarehouseDetailsPage-CwDLaG65.js} +64 -64
  35. package/dist/WarehousesPage-KW02DvZg.js +182 -0
  36. package/dist/{arrow-left-n48Y-Wis.js → arrow-left-DJDw_Yqv.js} +1 -1
  37. package/dist/{arrow-right-Q3XQpTPR.js → arrow-right-Cb2fLTPJ.js} +1 -1
  38. package/dist/{arrow-up-xNF1zJUG.js → arrow-up-PxaOIyiT.js} +1 -1
  39. package/dist/{box-Eu05PXDh.js → box-hSJVtvGf.js} +1 -1
  40. package/dist/boxes-DDnxEkT5.js +54 -0
  41. package/dist/{chart-column-ouzgxIv_.js → chart-column-xXgAZvRl.js} +1 -1
  42. package/dist/charts-CfCwQg6o.js +2018 -0
  43. package/dist/{chevron-left-B2_r0Elx.js → chevron-left-dhEGJYdh.js} +1 -1
  44. package/dist/{chevron-right-eV4LjaSi.js → chevron-right-Cud2NDvm.js} +1 -1
  45. package/dist/chunk-BdsCSvBP.js +20 -0
  46. package/dist/{circle-alert-Bzh2DBSF.js → circle-alert-Rx0p3D5w.js} +1 -1
  47. package/dist/circle-check-BCxRFdFo.js +13 -0
  48. package/dist/{circle-check-big-CGMSpQmD.js → circle-check-big-BcIJ8xjx.js} +1 -1
  49. package/dist/{clock-Bl3ptexL.js → clock-B2_YxJgB.js} +1 -1
  50. package/dist/{dollar-sign-CAVU95SO.js → dollar-sign-CQmAICv4.js} +1 -1
  51. package/dist/{download-C5NbvVDc.js → download-CG4Tponq.js} +1 -1
  52. package/dist/{file-text-D-1uvLZG.js → file-text-CzH-zMub.js} +1 -1
  53. package/dist/{filter-DUfUg6BO.js → filter-C0u2Lwfn.js} +1 -1
  54. package/dist/{fuel-B6_lnKDs.js → fuel-C2KELcM-.js} +1 -1
  55. package/dist/index.css +2 -0
  56. package/dist/index.js +214 -225
  57. package/dist/{map-C-M-FQLR.js → map-FuPu5aTw.js} +1 -1
  58. package/dist/{map-pin-Dznl5l40.js → map-pin-czpLv9uy.js} +1 -1
  59. package/dist/mapMarkers-NCV9MRKR.js +28 -0
  60. package/dist/maps-D9m7PKrs.js +5070 -0
  61. package/dist/{navigation-BJSJl-VG.js → navigation-D5_pdCSS.js} +1 -1
  62. package/dist/{package-CcRttV2N.js → package-B0sdi6lU.js} +1 -1
  63. package/dist/{phone-DVSZHt_j.js → phone-V2nuzPu9.js} +1 -1
  64. package/dist/{plus-C_XZDwBz.js → plus-BqQxkfoU.js} +1 -1
  65. package/dist/{refresh-cw-CfgN67l3.js → refresh-cw-CEOAPEcV.js} +1 -1
  66. package/dist/route-CZWcW-Dw.js +22 -0
  67. package/dist/{save-DJE8Fpdv.js → save-DDFvrxWl.js} +1 -1
  68. package/dist/{search-D_VyGcZT.js → search-D7Ya6jxj.js} +1 -1
  69. package/dist/{square-pen-D-6JYrVQ.js → square-pen-DoZZS4Lw.js} +1 -1
  70. package/dist/{star-BpRlUZuU.js → star-B7PgVu5j.js} +1 -1
  71. package/dist/{store-B-8HMuyZ.js → store-YVzcIG4A.js} +1 -1
  72. package/dist/{trash-2-8vmv-XV_.js → trash-2-BegnBzwI.js} +1 -1
  73. package/dist/{trending-up-Dz6PqPRj.js → trending-up-DBd3XNdb.js} +1 -1
  74. package/dist/{triangle-alert-D7Y3J_4z.js → triangle-alert-Bxk3Kgs6.js} +1 -1
  75. package/dist/{truck-D-y2IDzv.js → truck-Clrn0oHp.js} +1 -1
  76. package/dist/{upload-CHZofOqb.js → upload-Df5JmMwH.js} +1 -1
  77. package/dist/{useAssistant-DVW4QgQb.js → useAssistant-Cb8nnIVC.js} +11 -28
  78. package/dist/{useCustomers-BPW4__PR.js → useCustomers-Dq2IR2HQ.js} +1 -1
  79. package/dist/{useFleet-ChBkxAd6.js → useFleet-CapnWBcA.js} +1 -1
  80. package/dist/{useImportExport-DrPDHEDK.js → useImportExport-BoGzrlJy.js} +1 -1
  81. package/dist/{useInventory-CAAVMRp6.js → useInventory-Ci6M2d9f.js} +1 -1
  82. package/dist/{useOrders-B465S2nt.js → useOrders-BuQU1OHX.js} +1 -1
  83. package/dist/{useReports-jQ3GpNOm.js → useReports-DIx8W7CO.js} +1 -1
  84. package/dist/{useRoutes-hS3d5-xr.js → useRoutes-D7EvI_pm.js} +1 -1
  85. package/dist/{useSettings-BB8X2RLh.js → useSettings-BhX251UQ.js} +6 -6
  86. package/dist/{useTracking-RjGz9PZG.js → useTracking-C40jmZ6W.js} +3 -3
  87. package/dist/{user-DzfTn4dE.js → user-BlcJrbtE.js} +1 -1
  88. package/dist/{user-check-BEKRCi7v.js → user-check-DtnQmimP.js} +1 -1
  89. package/dist/{users-CCrq-w0C.js → users-Cvwko-Dx.js} +1 -1
  90. package/dist/{warehouse-CaqfmMKh.js → warehouse-LWLVTk6G.js} +1 -1
  91. package/dist/{wrench-q_rgJfaP.js → wrench-Cme0RbyR.js} +1 -1
  92. package/dist/{x-tMeDMcCw.js → x-D8C9Pg5V.js} +1 -1
  93. package/package.json +5 -1
  94. package/src/movethewheels/components/GlassPanel.tsx +71 -0
  95. package/src/movethewheels/components/MetricCard.tsx +171 -73
  96. package/src/movethewheels/components/StatusBadge.tsx +7 -6
  97. package/src/movethewheels/components/charts/ChartShell.tsx +78 -0
  98. package/src/movethewheels/components/charts/Sparkline.tsx +85 -0
  99. package/src/movethewheels/components/charts/StatusDonut.tsx +115 -0
  100. package/src/movethewheels/components/charts/TrendChart.tsx +179 -0
  101. package/src/movethewheels/components/charts/VolumeBar.tsx +97 -0
  102. package/src/movethewheels/components/charts/chartTokens.ts +43 -0
  103. package/src/movethewheels/components/charts/index.ts +9 -0
  104. package/src/movethewheels/components/index.ts +44 -1
  105. package/src/movethewheels/components/maps/LogisticsMap.tsx +94 -0
  106. package/src/movethewheels/components/maps/MapCanvas.tsx +118 -0
  107. package/src/movethewheels/components/maps/RouteMap.tsx +71 -0
  108. package/src/movethewheels/components/maps/TrackingMap.tsx +102 -0
  109. package/src/movethewheels/components/maps/index.ts +16 -0
  110. package/src/movethewheels/components/maps/leaflet-setup.ts +124 -0
  111. package/src/movethewheels/components/maps/map-markers.css +121 -0
  112. package/src/movethewheels/hooks/useAnalytics.ts +14 -4
  113. package/src/movethewheels/pages/AnalyticsPage.tsx +93 -73
  114. package/src/movethewheels/pages/CreateOrderPage.tsx +4 -4
  115. package/src/movethewheels/pages/CustomersPage.tsx +29 -20
  116. package/src/movethewheels/pages/DashboardPage.tsx +238 -12
  117. package/src/movethewheels/pages/DriverDetailsPage.tsx +1 -1
  118. package/src/movethewheels/pages/DriversPage.tsx +35 -24
  119. package/src/movethewheels/pages/FinancePage.tsx +68 -30
  120. package/src/movethewheels/pages/InventoryPage.tsx +15 -3
  121. package/src/movethewheels/pages/LiveTrackingPage.tsx +124 -16
  122. package/src/movethewheels/pages/OrdersListPage.tsx +46 -38
  123. package/src/movethewheels/pages/ProductsPage.tsx +28 -24
  124. package/src/movethewheels/pages/ReportsPage.tsx +23 -0
  125. package/src/movethewheels/pages/RouteDetailsPage.tsx +46 -7
  126. package/src/movethewheels/pages/SettingsPage.tsx +2 -2
  127. package/src/movethewheels/pages/TrackingPage.tsx +76 -6
  128. package/src/movethewheels/pages/VehicleDetailsPage.tsx +1 -1
  129. package/src/movethewheels/pages/VehiclesPage.tsx +34 -24
  130. package/src/movethewheels/pages/WarehouseDetailsPage.tsx +1 -1
  131. package/src/movethewheels/pages/WarehousesPage.tsx +28 -22
  132. package/src/movethewheels/types/index.ts +1 -1
  133. package/src/movethewheels/utils/graphqlClient.ts +81 -31
  134. package/src/movethewheels/utils/mapMarkers.ts +58 -0
  135. package/dist/AnalyticsPage-CMpI1070.js +0 -215
  136. package/dist/CustomersPage-CaNBMXmM.js +0 -172
  137. package/dist/DashboardPage-DIKlXde3.js +0 -387
  138. package/dist/DriversPage-PYmdFiby.js +0 -176
  139. package/dist/FinancePage-Cwh9Bw4M.js +0 -136
  140. package/dist/InventoryPage-DpiqVu1v.js +0 -253
  141. package/dist/LiveTrackingPage-FAzzlVnX.js +0 -379
  142. package/dist/MetricCard-CVBVpu7o.js +0 -146
  143. package/dist/OrdersListPage-CAMsuUck.js +0 -384
  144. package/dist/ProductsPage-DC5qDLfz.js +0 -180
  145. package/dist/ReportsPage-BwUJ37eO.js +0 -356
  146. package/dist/TrackingPage-DaNEFG_h.js +0 -325
  147. package/dist/VehiclesPage-BFw13iMt.js +0 -176
  148. package/dist/WarehousesPage-Wkytnt86.js +0 -173
  149. package/dist/useAnalytics-D7HW2XRb.js +0 -347
  150. /package/dist/{formatters-BQr-aR5W.js → formatters-FWJDwTFv.js} +0 -0
@@ -4,13 +4,15 @@
4
4
  * Track orders by tracking number or order ID.
5
5
  */
6
6
 
7
- import { useState } from 'react';
7
+ import { useMemo, useState } from 'react';
8
8
  import { useSearchParams } from 'react-router-dom';
9
9
  import { MapPin, Search, Package, Truck, Clock, CheckCircle, AlertCircle } from 'lucide-react';
10
10
  import { PageLayout, EmptyState } from '../components/PageLayout';
11
11
  import { StatusBadge } from '../components/StatusBadge';
12
12
  import { Card, CardContent, CardHeader, CardTitle, Button, Input } from '../components/ui';
13
13
  import { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
14
+ import { TrackingMap, type TrackEndpoint, type TrackPoint } from '../components/maps';
15
+ import { firstRealLatLng } from '../utils/mapMarkers';
14
16
  import { useOrderTracking } from '../hooks/useTracking';
15
17
  import { formatDate, formatAddress } from '../utils/formatters';
16
18
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
@@ -42,6 +44,42 @@ export default function TrackingPage() {
42
44
  }
43
45
  };
44
46
 
47
+ // Real geographic view of the shipment: pickup → delivery endpoints, a
48
+ // breadcrumb trail from timeline events that carry coordinates, and the live
49
+ // current position. Everything is coordinate-filtered so a shipment with no
50
+ // GPS data honestly shows the map's empty state instead of a fake route.
51
+ const mapModel = useMemo(() => {
52
+ if (!order) return null;
53
+ const originCoord = firstRealLatLng(order.pickupAddress);
54
+ const destCoord = firstRealLatLng(order.deliveryAddress);
55
+ const origin: TrackEndpoint | null = originCoord
56
+ ? { ...originCoord, label: `Pickup · ${order.pickupAddress.city || 'Origin'}` }
57
+ : null;
58
+ const destination: TrackEndpoint | null = destCoord
59
+ ? { ...destCoord, label: `Delivery · ${order.deliveryAddress.city || 'Destination'}` }
60
+ : null;
61
+
62
+ const trail: TrackPoint[] = order.timeline
63
+ .map((event): TrackPoint | null => {
64
+ const coord = event.location ? firstRealLatLng(event.location) : null;
65
+ return coord
66
+ ? { lat: coord.lat, lng: coord.lng, timestamp: new Date(event.timestamp).toISOString() }
67
+ : null;
68
+ })
69
+ .filter((point): point is TrackPoint => point !== null);
70
+
71
+ const currentCoord = tracking ? firstRealLatLng(tracking.currentLocation) : null;
72
+ const current: TrackEndpoint | null = currentCoord
73
+ ? {
74
+ ...currentCoord,
75
+ label: tracking?.currentLocation.address || 'Current position',
76
+ }
77
+ : null;
78
+
79
+ const hasAny = Boolean(origin || destination || current || trail.length > 0);
80
+ return { origin, destination, trail, current, hasAny };
81
+ }, [order, tracking]);
82
+
45
83
  return (
46
84
  <PageLayout
47
85
  title={t('movethewheels.tracking.title', 'Track Shipment')}
@@ -173,6 +211,30 @@ export default function TrackingPage() {
173
211
  </div>
174
212
  </EmphasisPanel>
175
213
 
214
+ {/* Route Map — real origin/destination + tracking trail + live position */}
215
+ <Card>
216
+ <CardHeader>
217
+ <CardTitle className="flex items-center gap-2">
218
+ <MapPin size={18} />
219
+ {t('movethewheels.tracking.routeMap.title', 'Route map')}
220
+ </CardTitle>
221
+ </CardHeader>
222
+ <CardContent>
223
+ <TrackingMap
224
+ className="overflow-hidden rounded-lg border border-border-seam"
225
+ height={320}
226
+ origin={mapModel?.origin ?? null}
227
+ destination={mapModel?.destination ?? null}
228
+ trail={mapModel?.trail ?? []}
229
+ current={mapModel?.current ?? null}
230
+ emptyMessage={t(
231
+ 'movethewheels.tracking.routeMap.empty',
232
+ 'No location coordinates recorded for this shipment yet'
233
+ )}
234
+ />
235
+ </CardContent>
236
+ </Card>
237
+
176
238
  {/* Progress Timeline */}
177
239
  <Card>
178
240
  <CardHeader>
@@ -351,14 +413,22 @@ export default function TrackingPage() {
351
413
  </CardTitle>
352
414
  </CardHeader>
353
415
  <CardContent>
354
- <div className="aspect-video bg-bg-sunken rounded-lg flex items-center justify-center mb-4">
355
- <div className="text-center text-text-secondary">
356
- <MapPin size={32} className="mx-auto mb-2" />
416
+ {mapModel?.current ? (
417
+ <TrackingMap
418
+ className="mb-4 overflow-hidden rounded-lg border border-border-seam"
419
+ height={180}
420
+ current={mapModel.current}
421
+ destination={mapModel.destination ?? null}
422
+ trail={mapModel.trail}
423
+ />
424
+ ) : (
425
+ <div className="mb-4 rounded-lg bg-bg-sunken p-4 text-center text-text-secondary">
426
+ <MapPin size={28} className="mx-auto mb-2 opacity-60" />
357
427
  <p className="text-sm">
358
- {t('movethewheels.tracking.location.mapView', 'Map View')}
428
+ {t('movethewheels.tracking.location.noFix', 'No live position fix yet')}
359
429
  </p>
360
430
  </div>
361
- </div>
431
+ )}
362
432
  <p className="text-sm">
363
433
  {tracking.currentLocation.address ||
364
434
  t('movethewheels.tracking.location.updating', 'Location updating...')}
@@ -266,7 +266,7 @@ export default function VehicleDetailsPage() {
266
266
  <CardTitle>Capacity</CardTitle>
267
267
  </CardHeader>
268
268
  <CardContent>
269
- <div className="grid grid-cols-3 gap-6">
269
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
270
270
  <div>
271
271
  <p className="text-sm text-text-secondary">Weight</p>
272
272
  <p className="text-2xl font-bold">
@@ -3,12 +3,13 @@
3
3
  */
4
4
 
5
5
  import { useState } from 'react';
6
- import { Truck, Plus, RefreshCw, Download } from 'lucide-react';
6
+ import { Truck, Plus, RefreshCw, Download, CircleCheck, Route as RouteIcon, Wrench } from 'lucide-react';
7
7
  import { PageLayout } from '../components/PageLayout';
8
+ import { MetricCard, MetricGrid } from '../components/MetricCard';
8
9
  import { DataTable, type Column } from '../components/DataTable';
9
10
  import { StatusBadge, VehicleTypeBadge } from '../components/StatusBadge';
10
11
  import { Progress } from '../components/ui';
11
- import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
12
+ import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
12
13
  import { useVehicles, useFleetStats } from '../hooks/useFleet';
13
14
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
14
15
  import { joinPath } from '../utils/navigation';
@@ -126,28 +127,37 @@ export default function VehiclesPage() {
126
127
  an order.
127
128
  </PagePurpose>
128
129
 
129
- {/* Fleet at a glance — the page's single emphasis zone */}
130
- <EmphasisPanel>
131
- <p className="text-sm font-medium text-text-secondary">Fleet at a glance</p>
132
- <div className="mt-3 grid grid-cols-3 gap-4">
133
- <div>
134
- <p className="text-2xl font-bold text-text-primary tabular-nums">{totalVehicles}</p>
135
- <p className="text-xs text-text-secondary">Total vehicles</p>
136
- </div>
137
- <div>
138
- <p className="text-2xl font-bold text-text-primary tabular-nums">
139
- {stats.vehiclesByStatus.available}
140
- </p>
141
- <p className="text-xs text-text-secondary">Available now</p>
142
- </div>
143
- <div>
144
- <p className="text-2xl font-bold text-text-primary tabular-nums">
145
- {stats.vehiclesByStatus.maintenance}
146
- </p>
147
- <p className="text-xs text-text-secondary">In maintenance</p>
148
- </div>
149
- </div>
150
- </EmphasisPanel>
130
+ {/* Fleet at a glance — premium stat cards */}
131
+ <div>
132
+ <p className="mb-3 text-sm font-medium text-text-secondary">Fleet at a glance</p>
133
+ <MetricGrid columns={4}>
134
+ <MetricCard
135
+ label="Total vehicles"
136
+ value={totalVehicles}
137
+ icon={<Truck size={20} />}
138
+ accent="brand"
139
+ />
140
+ <MetricCard
141
+ label="Available now"
142
+ value={stats.vehiclesByStatus.available}
143
+ icon={<CircleCheck size={20} />}
144
+ accent="success"
145
+ subtitle="Ready to dispatch"
146
+ />
147
+ <MetricCard
148
+ label="On assignment"
149
+ value={stats.vehiclesByStatus.assigned}
150
+ icon={<RouteIcon size={20} />}
151
+ accent="blue"
152
+ />
153
+ <MetricCard
154
+ label="In maintenance"
155
+ value={stats.vehiclesByStatus.maintenance}
156
+ icon={<Wrench size={20} />}
157
+ accent="warning"
158
+ />
159
+ </MetricGrid>
160
+ </div>
151
161
 
152
162
  <DataTable
153
163
  columns={columns}
@@ -326,7 +326,7 @@ export default function WarehouseDetailsPage() {
326
326
  {warehouse.capacity.totalSpace.toLocaleString()} sq ft
327
327
  </p>
328
328
  </div>
329
- <div className="grid grid-cols-2 gap-4">
329
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
330
330
  <div>
331
331
  <p className="text-sm text-text-secondary">Pallets Used</p>
332
332
  <p className="text-2xl font-bold">
@@ -3,11 +3,12 @@
3
3
  */
4
4
 
5
5
  import { useState } from 'react';
6
- import { Warehouse, Plus, RefreshCw } from 'lucide-react';
6
+ import { Warehouse, Plus, RefreshCw, CircleCheck, Gauge } from 'lucide-react';
7
7
  import { PageLayout } from '../components/PageLayout';
8
+ import { MetricCard, MetricGrid } from '../components/MetricCard';
8
9
  import { DataTable, type Column } from '../components/DataTable';
9
10
  import { Badge, Progress } from '../components/ui';
10
- import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
11
+ import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
11
12
  import { useWarehouses, useWarehouseStats } from '../hooks/useInventory';
12
13
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
13
14
  import { joinPath } from '../utils/navigation';
@@ -121,26 +122,31 @@ export default function WarehousesPage() {
121
122
  to balance stock, spot capacity crunches, and decide where to fulfil an order from.
122
123
  </PagePurpose>
123
124
 
124
- {/* Network at a glance — the page's single emphasis zone */}
125
- <EmphasisPanel>
126
- <p className="text-sm font-medium text-text-secondary">Network at a glance</p>
127
- <div className="mt-3 grid grid-cols-3 gap-4">
128
- <div>
129
- <p className="text-2xl font-bold text-text-primary tabular-nums">{stats.total}</p>
130
- <p className="text-xs text-text-secondary">Total sites</p>
131
- </div>
132
- <div>
133
- <p className="text-2xl font-bold text-text-primary tabular-nums">{stats.active}</p>
134
- <p className="text-xs text-text-secondary">Active</p>
135
- </div>
136
- <div>
137
- <p className="text-2xl font-bold text-text-primary tabular-nums">
138
- {stats.total > 0 ? `${Math.round(stats.avgUtilization * 100)}%` : '—'}
139
- </p>
140
- <p className="text-xs text-text-secondary">Avg utilization</p>
141
- </div>
142
- </div>
143
- </EmphasisPanel>
125
+ {/* Network at a glance — premium stat cards */}
126
+ <div>
127
+ <p className="mb-3 text-sm font-medium text-text-secondary">Network at a glance</p>
128
+ <MetricGrid columns={3}>
129
+ <MetricCard
130
+ label="Total sites"
131
+ value={stats.total}
132
+ icon={<Warehouse size={20} />}
133
+ accent="brand"
134
+ />
135
+ <MetricCard
136
+ label="Active"
137
+ value={stats.active}
138
+ icon={<CircleCheck size={20} />}
139
+ accent="success"
140
+ subtitle="Fulfilling orders"
141
+ />
142
+ <MetricCard
143
+ label="Avg utilization"
144
+ value={stats.total > 0 ? `${Math.round(stats.avgUtilization * 100)}%` : '—'}
145
+ icon={<Gauge size={20} />}
146
+ accent="blue"
147
+ />
148
+ </MetricGrid>
149
+ </div>
144
150
 
145
151
  <DataTable
146
152
  columns={columns}
@@ -691,7 +691,7 @@ export interface Analytics {
691
691
  customerSatisfaction: number;
692
692
  };
693
693
  trends: {
694
- dailyOrders: Array<{ date: string; orders: number }>;
694
+ dailyOrders: Array<{ date: string; orders: number; deliveries: number }>;
695
695
  revenue: Array<{ month: string; revenue: number }>;
696
696
  };
697
697
  fleet: {
@@ -19,45 +19,95 @@ export function resolveGraphqlUrl(apiGatewayUrl?: string): string {
19
19
  return `${apiGatewayUrl}/workspaces/graphql`;
20
20
  }
21
21
 
22
+ const MAX_RETRIES = 3;
23
+
24
+ /**
25
+ * A transient failure is a momentary gateway↔service connectivity blip (the
26
+ * workspace service runs a lean replica pool with no PgBouncer, so a burst of
27
+ * concurrent dashboard queries can briefly exhaust connections and the gateway
28
+ * returns an upstream-connect / 5xx / reset error). These are safe to retry;
29
+ * genuine validation / auth / business errors are not.
30
+ */
31
+ function isTransientMessage(message?: string): boolean {
32
+ if (!message) return false;
33
+ return /upstream connect|connection refused|reset before headers|delayed connect|econnrefused|econnreset|socket hang up|service unavailable|bad gateway|gateway timeout|temporarily unavailable|please try again|failed to fetch|networkerror|load failed/i.test(
34
+ message
35
+ );
36
+ }
37
+
38
+ function isTransientStatus(status: number): boolean {
39
+ // 502/503/504 are gateway/upstream problems; 429 is backpressure. All retryable.
40
+ return status === 429 || status === 502 || status === 503 || status === 504;
41
+ }
42
+
43
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
44
+
22
45
  export async function executeGraphQL<TData>(
23
46
  options: GraphQLRequestOptions,
24
47
  query: string,
25
48
  variables?: Record<string, unknown>
26
49
  ): Promise<TData> {
27
- const response = await fetch(resolveGraphqlUrl(options.apiGatewayUrl), {
28
- method: 'POST',
29
- headers: {
30
- 'content-type': 'application/json',
31
- ...(options.authToken ? { Authorization: `Bearer ${options.authToken}` } : {}),
32
- ...(options.workspaceToken
33
- ? { 'X-Workspace-Authorization': `Bearer ${options.workspaceToken}` }
34
- : {}),
35
- ...(options.workspaceId ? { 'x-workspace-id': options.workspaceId } : {}),
36
- ...(options.tenantId ? { 'x-tenant-id': options.tenantId } : {}),
37
- ...(options.actorId ? { 'x-actor-id': options.actorId } : {}),
38
- ...(options.actorId ? { 'x-actor-type': 'user' } : {}),
39
- },
40
- body: JSON.stringify({ query, variables }),
41
- });
42
-
43
- if (!response.ok) {
44
- throw new Error(`GraphQL request failed with status ${response.status}`);
45
- }
46
-
47
- const payload = (await response.json()) as {
48
- data?: TData;
49
- errors?: Array<{ message?: string }>;
50
+ const url = resolveGraphqlUrl(options.apiGatewayUrl);
51
+ const headers: Record<string, string> = {
52
+ 'content-type': 'application/json',
53
+ ...(options.authToken ? { Authorization: `Bearer ${options.authToken}` } : {}),
54
+ ...(options.workspaceToken
55
+ ? { 'X-Workspace-Authorization': `Bearer ${options.workspaceToken}` }
56
+ : {}),
57
+ ...(options.workspaceId ? { 'x-workspace-id': options.workspaceId } : {}),
58
+ ...(options.tenantId ? { 'x-tenant-id': options.tenantId } : {}),
59
+ ...(options.actorId ? { 'x-actor-id': options.actorId } : {}),
60
+ ...(options.actorId ? { 'x-actor-type': 'user' } : {}),
50
61
  };
62
+ const body = JSON.stringify({ query, variables });
51
63
 
52
- if (payload.errors?.length) {
53
- throw new Error(
54
- payload.errors.map((item) => item.message ?? 'Unknown GraphQL error').join('; ')
55
- );
56
- }
64
+ let lastError: Error = new Error('GraphQL request failed');
65
+
66
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
67
+ const canRetry = attempt < MAX_RETRIES;
68
+ // Exponential backoff with jitter before each retry (not the first attempt).
69
+ if (attempt > 0) await sleep(Math.min(250 * 2 ** (attempt - 1), 2000) + Math.floor(Math.random() * 150));
70
+
71
+ try {
72
+ const response = await fetch(url, { method: 'POST', headers, body });
73
+
74
+ if (!response.ok) {
75
+ lastError = new Error(`GraphQL request failed with status ${response.status}`);
76
+ if (canRetry && isTransientStatus(response.status)) continue;
77
+ throw lastError;
78
+ }
79
+
80
+ const payload = (await response.json()) as {
81
+ data?: TData;
82
+ errors?: Array<{ message?: string }>;
83
+ };
84
+
85
+ if (payload.errors?.length) {
86
+ const message = payload.errors
87
+ .map((item) => item.message ?? 'Unknown GraphQL error')
88
+ .join('; ');
89
+ lastError = new Error(message);
90
+ // The gateway surfaces upstream-connect failures as a 200 body error —
91
+ // retry those, but never retry a real validation/auth/business error.
92
+ if (canRetry && payload.errors.some((item) => isTransientMessage(item.message))) continue;
93
+ throw lastError;
94
+ }
95
+
96
+ if (!payload.data) {
97
+ lastError = new Error('GraphQL response did not include data');
98
+ if (canRetry) continue;
99
+ throw lastError;
100
+ }
57
101
 
58
- if (!payload.data) {
59
- throw new Error('GraphQL response did not include data');
102
+ return payload.data;
103
+ } catch (error) {
104
+ // A thrown fetch is a network error (TypeError "Failed to fetch") — transient.
105
+ const message = error instanceof Error ? error.message : String(error);
106
+ lastError = error instanceof Error ? error : new Error(message);
107
+ if (canRetry && (isTransientMessage(message) || error instanceof TypeError)) continue;
108
+ throw lastError;
109
+ }
60
110
  }
61
111
 
62
- return payload.data;
112
+ throw lastError;
63
113
  }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared helpers for turning domain records into map markers. Pure functions —
3
+ * no Leaflet import, so they can be reused by any page without pulling map CSS.
4
+ */
5
+ import type { MapMarkerStatus } from '../components/maps';
6
+ import type { Address, Location } from '../types';
7
+
8
+ /**
9
+ * Map an order/shipment status onto the marker's visual status (drives pin color
10
+ * and glyph). Anything unrecognised falls back to a neutral pin.
11
+ */
12
+ export function deliveryMarkerStatus(status: string | undefined): MapMarkerStatus {
13
+ switch ((status ?? '').toLowerCase()) {
14
+ case 'in_transit':
15
+ return 'in_transit';
16
+ case 'out_for_delivery':
17
+ return 'active';
18
+ case 'delivered':
19
+ return 'delivered';
20
+ case 'failed':
21
+ case 'delayed':
22
+ case 'cancelled':
23
+ return 'delayed';
24
+ case 'assigned':
25
+ case 'picked_up':
26
+ case 'confirmed':
27
+ return 'active';
28
+ default:
29
+ return 'default';
30
+ }
31
+ }
32
+
33
+ /**
34
+ * First location among the candidates that carries a real (non-null-island)
35
+ * coordinate, returned as a plain `{ lat, lng }`. Returns null when none do.
36
+ * Kept coordinate-shape-agnostic so callers pass whichever object they have.
37
+ */
38
+ export function firstRealLatLng(
39
+ ...candidates: Array<Location | Address | null | undefined>
40
+ ): { lat: number; lng: number } | null {
41
+ for (const candidate of candidates) {
42
+ if (!candidate) continue;
43
+ const lat = candidate.latitude;
44
+ const lng = candidate.longitude;
45
+ if (
46
+ typeof lat === 'number' &&
47
+ typeof lng === 'number' &&
48
+ Number.isFinite(lat) &&
49
+ Number.isFinite(lng) &&
50
+ !(lat === 0 && lng === 0) &&
51
+ Math.abs(lat) <= 90 &&
52
+ Math.abs(lng) <= 180
53
+ ) {
54
+ return { lat, lng };
55
+ }
56
+ }
57
+ return null;
58
+ }