@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.
- package/dist/{AIAssistantPage-3YOnHO9Y.js → AIAssistantPage-BmILdHgO.js} +16 -15
- package/dist/AnalyticsPage-BSlsiSDE.js +194 -0
- package/dist/{CreateOrderPage-DRAgGo9i.js → CreateOrderPage-DaCgVaO3.js} +40 -40
- package/dist/{CustomerDetailsPage-BCeMmOFe.js → CustomerDetailsPage-DErpbafW.js} +71 -71
- package/dist/CustomersPage-C1lRU6xs.js +205 -0
- package/dist/DashboardPage-Q5-5EKAU.js +542 -0
- package/dist/{DataTable-XvUcTjwi.js → DataTable-KjzbwnRL.js} +31 -31
- package/dist/{DocumentsCard-U2lSe-cn.js → DocumentsCard-T9y6cv4-.js} +25 -25
- package/dist/{DriverDetailsPage-BQrYd_Lp.js → DriverDetailsPage-DnHGkDC7.js} +64 -64
- package/dist/DriversPage-C0ujWVOn.js +221 -0
- package/dist/FinancePage-CA2XEgsQ.js +175 -0
- package/dist/{FleetPage-3ZHsB-TN.js → FleetPage-DrvXW7wK.js} +22 -22
- package/dist/{FormError-cuLV2jVD.js → FormError-CRsMP7TQ.js} +1 -1
- package/dist/{ImportExportPage-C6f7DqhM.js → ImportExportPage-Cac9HjFx.js} +12 -12
- package/dist/InventoryPage-Ezf4gqKI.js +259 -0
- package/dist/LiveTrackingPage-CzbVwDqK.js +429 -0
- package/dist/{MarketplacePage-Bh_2ns4v.js → MarketplacePage-BHDmSQmo.js} +116 -116
- package/dist/MetricCard-DQlDNqmK.js +191 -0
- package/dist/{Notice-CSoDYKw9.js → Notice-GmA7JDR3.js} +2 -2
- package/dist/{OrderDetailsPage-zCj0lW0j.js → OrderDetailsPage-CR0Wc1aN.js} +58 -58
- package/dist/OrdersListPage-C8Bu6wXM.js +386 -0
- package/dist/{PageLayout-D4LDHJJq.js → PageLayout-DX1Ou6V6.js} +164 -134
- package/dist/{ProductDetailsPage-wsxAQkA0.js → ProductDetailsPage-B_ns2MOe.js} +55 -55
- package/dist/ProductsPage-c92BsEoA.js +186 -0
- package/dist/ReportsPage-CPLXtLT2.js +377 -0
- package/dist/{RouteDetailsPage-DyMJE9AS.js → RouteDetailsPage-DOfcRiFv.js} +208 -181
- package/dist/{RoutesPage-Dtc669yc.js → RoutesPage-BZGublby.js} +12 -12
- package/dist/{SettingsPage-CpH9xhQg.js → SettingsPage-5cweX_xh.js} +15 -15
- package/dist/Sparkline-zSXvG6vA.js +16729 -0
- package/dist/{StatusBadge-C87mDpDP.js → StatusBadge-oZjownr1.js} +7 -7
- package/dist/TrackingPage-Ln9qVflK.js +368 -0
- package/dist/{VehicleDetailsPage-DldYNsYV.js → VehicleDetailsPage-CrkzE0dN.js} +53 -53
- package/dist/VehiclesPage-CTQWH7af.js +184 -0
- package/dist/{WarehouseDetailsPage-CMG7or6G.js → WarehouseDetailsPage-CwDLaG65.js} +64 -64
- package/dist/WarehousesPage-KW02DvZg.js +182 -0
- package/dist/{arrow-left-n48Y-Wis.js → arrow-left-DJDw_Yqv.js} +1 -1
- package/dist/{arrow-right-Q3XQpTPR.js → arrow-right-Cb2fLTPJ.js} +1 -1
- package/dist/{arrow-up-xNF1zJUG.js → arrow-up-PxaOIyiT.js} +1 -1
- package/dist/{box-Eu05PXDh.js → box-hSJVtvGf.js} +1 -1
- package/dist/boxes-DDnxEkT5.js +54 -0
- package/dist/{chart-column-ouzgxIv_.js → chart-column-xXgAZvRl.js} +1 -1
- package/dist/charts-CfCwQg6o.js +2018 -0
- package/dist/{chevron-left-B2_r0Elx.js → chevron-left-dhEGJYdh.js} +1 -1
- package/dist/{chevron-right-eV4LjaSi.js → chevron-right-Cud2NDvm.js} +1 -1
- package/dist/chunk-BdsCSvBP.js +20 -0
- package/dist/{circle-alert-Bzh2DBSF.js → circle-alert-Rx0p3D5w.js} +1 -1
- package/dist/circle-check-BCxRFdFo.js +13 -0
- package/dist/{circle-check-big-CGMSpQmD.js → circle-check-big-BcIJ8xjx.js} +1 -1
- package/dist/{clock-Bl3ptexL.js → clock-B2_YxJgB.js} +1 -1
- package/dist/{dollar-sign-CAVU95SO.js → dollar-sign-CQmAICv4.js} +1 -1
- package/dist/{download-C5NbvVDc.js → download-CG4Tponq.js} +1 -1
- package/dist/{file-text-D-1uvLZG.js → file-text-CzH-zMub.js} +1 -1
- package/dist/{filter-DUfUg6BO.js → filter-C0u2Lwfn.js} +1 -1
- package/dist/{fuel-B6_lnKDs.js → fuel-C2KELcM-.js} +1 -1
- package/dist/index.css +2 -0
- package/dist/index.js +214 -225
- package/dist/{map-C-M-FQLR.js → map-FuPu5aTw.js} +1 -1
- package/dist/{map-pin-Dznl5l40.js → map-pin-czpLv9uy.js} +1 -1
- package/dist/mapMarkers-NCV9MRKR.js +28 -0
- package/dist/maps-D9m7PKrs.js +5070 -0
- package/dist/{navigation-BJSJl-VG.js → navigation-D5_pdCSS.js} +1 -1
- package/dist/{package-CcRttV2N.js → package-B0sdi6lU.js} +1 -1
- package/dist/{phone-DVSZHt_j.js → phone-V2nuzPu9.js} +1 -1
- package/dist/{plus-C_XZDwBz.js → plus-BqQxkfoU.js} +1 -1
- package/dist/{refresh-cw-CfgN67l3.js → refresh-cw-CEOAPEcV.js} +1 -1
- package/dist/route-CZWcW-Dw.js +22 -0
- package/dist/{save-DJE8Fpdv.js → save-DDFvrxWl.js} +1 -1
- package/dist/{search-D_VyGcZT.js → search-D7Ya6jxj.js} +1 -1
- package/dist/{square-pen-D-6JYrVQ.js → square-pen-DoZZS4Lw.js} +1 -1
- package/dist/{star-BpRlUZuU.js → star-B7PgVu5j.js} +1 -1
- package/dist/{store-B-8HMuyZ.js → store-YVzcIG4A.js} +1 -1
- package/dist/{trash-2-8vmv-XV_.js → trash-2-BegnBzwI.js} +1 -1
- package/dist/{trending-up-Dz6PqPRj.js → trending-up-DBd3XNdb.js} +1 -1
- package/dist/{triangle-alert-D7Y3J_4z.js → triangle-alert-Bxk3Kgs6.js} +1 -1
- package/dist/{truck-D-y2IDzv.js → truck-Clrn0oHp.js} +1 -1
- package/dist/{upload-CHZofOqb.js → upload-Df5JmMwH.js} +1 -1
- package/dist/{useAssistant-DVW4QgQb.js → useAssistant-Cb8nnIVC.js} +11 -28
- package/dist/{useCustomers-BPW4__PR.js → useCustomers-Dq2IR2HQ.js} +1 -1
- package/dist/{useFleet-ChBkxAd6.js → useFleet-CapnWBcA.js} +1 -1
- package/dist/{useImportExport-DrPDHEDK.js → useImportExport-BoGzrlJy.js} +1 -1
- package/dist/{useInventory-CAAVMRp6.js → useInventory-Ci6M2d9f.js} +1 -1
- package/dist/{useOrders-B465S2nt.js → useOrders-BuQU1OHX.js} +1 -1
- package/dist/{useReports-jQ3GpNOm.js → useReports-DIx8W7CO.js} +1 -1
- package/dist/{useRoutes-hS3d5-xr.js → useRoutes-D7EvI_pm.js} +1 -1
- package/dist/{useSettings-BB8X2RLh.js → useSettings-BhX251UQ.js} +6 -6
- package/dist/{useTracking-RjGz9PZG.js → useTracking-C40jmZ6W.js} +3 -3
- package/dist/{user-DzfTn4dE.js → user-BlcJrbtE.js} +1 -1
- package/dist/{user-check-BEKRCi7v.js → user-check-DtnQmimP.js} +1 -1
- package/dist/{users-CCrq-w0C.js → users-Cvwko-Dx.js} +1 -1
- package/dist/{warehouse-CaqfmMKh.js → warehouse-LWLVTk6G.js} +1 -1
- package/dist/{wrench-q_rgJfaP.js → wrench-Cme0RbyR.js} +1 -1
- package/dist/{x-tMeDMcCw.js → x-D8C9Pg5V.js} +1 -1
- package/package.json +5 -1
- package/src/movethewheels/components/GlassPanel.tsx +71 -0
- package/src/movethewheels/components/MetricCard.tsx +171 -73
- package/src/movethewheels/components/StatusBadge.tsx +7 -6
- package/src/movethewheels/components/charts/ChartShell.tsx +78 -0
- package/src/movethewheels/components/charts/Sparkline.tsx +85 -0
- package/src/movethewheels/components/charts/StatusDonut.tsx +115 -0
- package/src/movethewheels/components/charts/TrendChart.tsx +179 -0
- package/src/movethewheels/components/charts/VolumeBar.tsx +97 -0
- package/src/movethewheels/components/charts/chartTokens.ts +43 -0
- package/src/movethewheels/components/charts/index.ts +9 -0
- package/src/movethewheels/components/index.ts +44 -1
- package/src/movethewheels/components/maps/LogisticsMap.tsx +94 -0
- package/src/movethewheels/components/maps/MapCanvas.tsx +118 -0
- package/src/movethewheels/components/maps/RouteMap.tsx +71 -0
- package/src/movethewheels/components/maps/TrackingMap.tsx +102 -0
- package/src/movethewheels/components/maps/index.ts +16 -0
- package/src/movethewheels/components/maps/leaflet-setup.ts +124 -0
- package/src/movethewheels/components/maps/map-markers.css +121 -0
- package/src/movethewheels/hooks/useAnalytics.ts +14 -4
- package/src/movethewheels/pages/AnalyticsPage.tsx +93 -73
- package/src/movethewheels/pages/CreateOrderPage.tsx +4 -4
- package/src/movethewheels/pages/CustomersPage.tsx +29 -20
- package/src/movethewheels/pages/DashboardPage.tsx +238 -12
- package/src/movethewheels/pages/DriverDetailsPage.tsx +1 -1
- package/src/movethewheels/pages/DriversPage.tsx +35 -24
- package/src/movethewheels/pages/FinancePage.tsx +68 -30
- package/src/movethewheels/pages/InventoryPage.tsx +15 -3
- package/src/movethewheels/pages/LiveTrackingPage.tsx +124 -16
- package/src/movethewheels/pages/OrdersListPage.tsx +46 -38
- package/src/movethewheels/pages/ProductsPage.tsx +28 -24
- package/src/movethewheels/pages/ReportsPage.tsx +23 -0
- package/src/movethewheels/pages/RouteDetailsPage.tsx +46 -7
- package/src/movethewheels/pages/SettingsPage.tsx +2 -2
- package/src/movethewheels/pages/TrackingPage.tsx +76 -6
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +1 -1
- package/src/movethewheels/pages/VehiclesPage.tsx +34 -24
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +1 -1
- package/src/movethewheels/pages/WarehousesPage.tsx +28 -22
- package/src/movethewheels/types/index.ts +1 -1
- package/src/movethewheels/utils/graphqlClient.ts +81 -31
- package/src/movethewheels/utils/mapMarkers.ts +58 -0
- package/dist/AnalyticsPage-CMpI1070.js +0 -215
- package/dist/CustomersPage-CaNBMXmM.js +0 -172
- package/dist/DashboardPage-DIKlXde3.js +0 -387
- package/dist/DriversPage-PYmdFiby.js +0 -176
- package/dist/FinancePage-Cwh9Bw4M.js +0 -136
- package/dist/InventoryPage-DpiqVu1v.js +0 -253
- package/dist/LiveTrackingPage-FAzzlVnX.js +0 -379
- package/dist/MetricCard-CVBVpu7o.js +0 -146
- package/dist/OrdersListPage-CAMsuUck.js +0 -384
- package/dist/ProductsPage-DC5qDLfz.js +0 -180
- package/dist/ReportsPage-BwUJ37eO.js +0 -356
- package/dist/TrackingPage-DaNEFG_h.js +0 -325
- package/dist/VehiclesPage-BFw13iMt.js +0 -176
- package/dist/WarehousesPage-Wkytnt86.js +0 -173
- package/dist/useAnalytics-D7HW2XRb.js +0 -347
- /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
|
-
|
|
355
|
-
<
|
|
356
|
-
|
|
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.
|
|
428
|
+
{t('movethewheels.tracking.location.noFix', 'No live position fix yet')}
|
|
359
429
|
</p>
|
|
360
430
|
</div>
|
|
361
|
-
|
|
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,
|
|
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 —
|
|
130
|
-
<
|
|
131
|
-
<p className="text-sm font-medium text-text-secondary">Fleet at a glance</p>
|
|
132
|
-
<
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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,
|
|
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 —
|
|
125
|
-
<
|
|
126
|
-
<p className="text-sm font-medium text-text-secondary">Network at a glance</p>
|
|
127
|
-
<
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
+
}
|