@burdenoff/microfe-movethewheels 2026.703.3 → 2026.703.5
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-C3cPsWt-.js → AIAssistantPage-D2gaEL1U.js} +16 -15
- package/dist/AnalyticsPage-DZMlEoBi.js +194 -0
- package/dist/CreateOrderPage-CWHx8yys.js +490 -0
- package/dist/CustomerDetailsPage-nR4oZ_ju.js +615 -0
- package/dist/CustomersPage-Bbvwvn8k.js +205 -0
- package/dist/DashboardPage-DrEhNf7N.js +542 -0
- package/dist/DataTable-CqMUD4er.js +340 -0
- package/dist/DocumentsCard-edDkgnJj.js +169 -0
- package/dist/DriverDetailsPage-whEMOWs9.js +490 -0
- package/dist/DriversPage-CHF2ZEzd.js +221 -0
- package/dist/FinancePage-Dn9GrW-1.js +175 -0
- package/dist/{FleetPage-DqkFi3s3.js → FleetPage-CrtGPy-p.js} +36 -35
- package/dist/{FormError-BlOjnXOq.js → FormError-BKcb_wT-.js} +1 -1
- package/dist/{ImportExportPage-DKJRCezl.js → ImportExportPage-WYdwZomZ.js} +52 -72
- package/dist/InventoryPage-l5A1f753.js +259 -0
- package/dist/LiveTrackingPage-DhEaNKZ4.js +429 -0
- package/dist/MarketplacePage-B9RYOb1Y.js +426 -0
- package/dist/MetricCard-y3bWHKUQ.js +191 -0
- package/dist/Notice-DOO-tjpV.js +46 -0
- package/dist/OrderDetailsPage-CwYHwk1K.js +584 -0
- package/dist/OrdersListPage-DhZJoQeI.js +386 -0
- package/dist/{PageLayout-D4LDHJJq.js → PageLayout-BniRJaQ6.js} +1 -1
- package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-1Gm9JKy0.js} +56 -56
- package/dist/ProductsPage-C8sn-1tN.js +186 -0
- package/dist/ReportsPage-D1tb0dcz.js +377 -0
- package/dist/RouteDetailsPage-CkMyR9b_.js +610 -0
- package/dist/{RoutesPage-DMfoicM1.js → RoutesPage-QkENzsE5.js} +58 -47
- package/dist/{SettingsPage-Bn-v0mbQ.js → SettingsPage-DV3_tU_X.js} +16 -38
- package/dist/Sparkline-K2RMJ58u.js +16729 -0
- package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-BNHXYvZx.js} +7 -7
- package/dist/TrackingPage-BZrg4RCu.js +368 -0
- package/dist/VehicleDetailsPage-D-8XPZ2G.js +393 -0
- package/dist/VehiclesPage-D5BELbtM.js +184 -0
- package/dist/{WarehouseDetailsPage-L_6sWigM.js → WarehouseDetailsPage-Cmg-8Frn.js} +70 -70
- package/dist/WarehousesPage-C1Y1M867.js +182 -0
- package/dist/{arrow-left-n48Y-Wis.js → arrow-left-ClW4Isj7.js} +1 -1
- package/dist/arrow-right-CmKOmIkh.js +11 -0
- package/dist/arrow-up-BJijRLAZ.js +17 -0
- package/dist/{box-D9Pal4HM.js → box-BT3NsRQP.js} +1 -1
- package/dist/boxes-B_OGvQiW.js +54 -0
- package/dist/{chart-column-BFIJysDR.js → chart-column-DQTTdUPk.js} +1 -1
- package/dist/charts-Tj_PPTfA.js +2018 -0
- package/dist/chevron-left-B1rZyWWb.js +8 -0
- package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-BIcwUJZq.js} +1 -1
- package/dist/chunk-BdsCSvBP.js +20 -0
- package/dist/{circle-alert-iY24lSlp.js → circle-alert-CNkuDNc6.js} +1 -1
- package/dist/circle-check-Cs1OVjtV.js +13 -0
- package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-C-p4eNwn.js} +1 -1
- package/dist/{clock-DJZtdl8u.js → clock-dRGG4rkF.js} +1 -1
- package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-B6RoYHHz.js} +1 -1
- package/dist/{download-DRXxoiQR.js → download-DP04iWTd.js} +1 -1
- package/dist/{file-text-DWWY6_f5.js → file-text-BuDx4hZp.js} +1 -1
- package/dist/{filter-DloLC2eE.js → filter-BD483_8g.js} +1 -1
- package/dist/{wrench-CANXWxF-.js → fuel-DLH1ag04.js} +3 -6
- package/dist/index.css +2 -0
- package/dist/index.js +150 -172
- package/dist/{map-CkJaULb2.js → map-DNQn9fyY.js} +1 -1
- package/dist/{map-pin-BGiCvyCs.js → map-pin-CqN1bbT1.js} +1 -1
- package/dist/mapMarkers-NCV9MRKR.js +28 -0
- package/dist/maps-yzLqiK_C.js +5070 -0
- package/dist/{navigation-D36prnAn.js → navigation-CgGUt2tR.js} +1 -1
- package/dist/{package-BChUzpJS.js → package-DmRF7iQD.js} +1 -1
- package/dist/{phone-Ac9s3mo_.js → phone-Cl32EXKW.js} +1 -1
- package/dist/{plus-Z76q-FZm.js → plus-DlANgOFx.js} +1 -1
- package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-rW59D4b7.js} +1 -1
- package/dist/route-CWjPrtAN.js +22 -0
- package/dist/{save-CojywNhG.js → save-UDlog7hg.js} +1 -1
- package/dist/{search-CDGefqb3.js → search-DNIG5Ck7.js} +1 -1
- package/dist/{square-pen-CopE9GmB.js → square-pen-BcfB3Grd.js} +1 -1
- package/dist/{star-nHlLjDKR.js → star-CVZHFwDx.js} +1 -1
- package/dist/{store-BR9Voi5E.js → store-bwnmCK9f.js} +1 -1
- package/dist/{trash-2-BKhar6Hs.js → trash-2-B80G12d4.js} +1 -1
- package/dist/{trending-up-Dohgd4t_.js → trending-up-CH4R4KOa.js} +1 -1
- package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-CmS8AUU6.js} +1 -1
- package/dist/{truck-C0v_hbLR.js → truck-9rLDncav.js} +1 -1
- package/dist/upload-DanJY1Vo.js +21 -0
- package/dist/{useAssistant-D4v3oxD3.js → useAssistant-CnEwkTLm.js} +11 -28
- package/dist/{useCustomers-BmTNGxiI.js → useCustomers-ChJao1ET.js} +30 -27
- package/dist/{useFleet-DKqXZ16m.js → useFleet-g5Yri0G8.js} +82 -70
- package/dist/{useImportExport-DHr-SWSw.js → useImportExport-MYwIT3rE.js} +1 -1
- package/dist/{useInventory-DkyiVY_A.js → useInventory-BSOn-R1c.js} +84 -39
- package/dist/{useOrders-Cfk1GLiq.js → useOrders-BSiqo61w.js} +59 -48
- package/dist/{useReports-CTqZr-AB.js → useReports-fWESfsF9.js} +41 -35
- package/dist/{useRoutes-BQWO607N.js → useRoutes-zD8Yp323.js} +50 -42
- package/dist/{useSettings-CkDH_-gM.js → useSettings-B1lsWeZ1.js} +6 -6
- package/dist/{useTracking-B-va1kO3.js → useTracking-BKG0I2ln.js} +3 -3
- package/dist/{user-jyFU3lXk.js → user-DU51el3m.js} +1 -1
- package/dist/user-check-IMhk-qkX.js +20 -0
- package/dist/{users-DZ-ilS6B.js → users-B8KhJBGg.js} +1 -1
- package/dist/{warehouse-6m5kXMLu.js → warehouse-CcMMioeY.js} +1 -1
- package/dist/wrench-Dg-sgH2e.js +8 -0
- package/dist/x-C4ObLVEa.js +11 -0
- package/package.json +5 -1
- package/src/movethewheels/components/DataTable.tsx +252 -84
- package/src/movethewheels/components/DocumentsCard.tsx +102 -0
- package/src/movethewheels/components/GlassPanel.tsx +71 -0
- package/src/movethewheels/components/MetricCard.tsx +171 -73
- package/src/movethewheels/components/Notice.tsx +61 -0
- 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 +51 -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/index.ts +9 -0
- package/src/movethewheels/hooks/useAnalytics.ts +109 -4
- package/src/movethewheels/hooks/useCustomers.ts +11 -4
- package/src/movethewheels/hooks/useEntityDocuments.ts +132 -0
- package/src/movethewheels/hooks/useFleet.ts +26 -7
- package/src/movethewheels/hooks/useInventory.ts +70 -7
- package/src/movethewheels/hooks/useOrders.ts +32 -2
- package/src/movethewheels/hooks/useReports.ts +14 -4
- package/src/movethewheels/hooks/useRoutes.ts +20 -3
- package/src/movethewheels/pages/AnalyticsPage.tsx +93 -73
- package/src/movethewheels/pages/CreateOrderPage.tsx +48 -20
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +239 -33
- package/src/movethewheels/pages/CustomersPage.tsx +53 -28
- package/src/movethewheels/pages/DashboardPage.tsx +238 -12
- package/src/movethewheels/pages/DriverDetailsPage.tsx +17 -31
- package/src/movethewheels/pages/DriversPage.tsx +59 -31
- package/src/movethewheels/pages/FinancePage.tsx +103 -82
- package/src/movethewheels/pages/InventoryPage.tsx +15 -3
- package/src/movethewheels/pages/LiveTrackingPage.tsx +124 -16
- package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
- package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
- package/src/movethewheels/pages/OrdersListPage.tsx +118 -47
- package/src/movethewheels/pages/ProductsPage.tsx +52 -28
- package/src/movethewheels/pages/ReportsPage.tsx +88 -4
- package/src/movethewheels/pages/RouteDetailsPage.tsx +119 -19
- package/src/movethewheels/pages/RoutesPage.tsx +22 -6
- package/src/movethewheels/pages/SettingsPage.tsx +5 -21
- package/src/movethewheels/pages/TrackingPage.tsx +76 -6
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +4 -34
- package/src/movethewheels/pages/VehiclesPage.tsx +58 -27
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +1 -1
- package/src/movethewheels/pages/WarehousesPage.tsx +47 -36
- package/src/movethewheels/types/index.ts +9 -1
- package/src/movethewheels/utils/download.ts +62 -0
- package/src/movethewheels/utils/mapMarkers.ts +58 -0
- package/dist/AnalyticsPage-Dz6g2MvK.js +0 -215
- package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
- package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
- package/dist/CustomersPage-BKxMS1AV.js +0 -161
- package/dist/DashboardPage-BVjQi0EB.js +0 -387
- package/dist/DataTable-DmciIKKM.js +0 -249
- package/dist/DriverDetailsPage-CN3WQ0B9.js +0 -507
- package/dist/DriversPage-CH9rjQC-.js +0 -165
- package/dist/FinancePage-DEDykSOP.js +0 -158
- package/dist/InventoryPage-e0ze63iX.js +0 -253
- package/dist/LiveTrackingPage-DCX-K-LR.js +0 -379
- package/dist/MarketplacePage-CIQ97_zp.js +0 -247
- package/dist/MetricCard-BY0QrMD4.js +0 -146
- package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
- package/dist/OrdersListPage-Cn90P1Xx.js +0 -307
- package/dist/ProductsPage-QJfHHpqB.js +0 -169
- package/dist/ReportsPage-Zp53q3_8.js +0 -284
- package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
- package/dist/TrackingPage-W-jtdM3Y.js +0 -325
- package/dist/VehicleDetailsPage-Bceo5oz3.js +0 -410
- package/dist/VehiclesPage-ApKThYeG.js +0 -165
- package/dist/WarehousesPage-DCbXXXWE.js +0 -162
- package/dist/useAnalytics-DS8n0xgg.js +0 -297
- /package/dist/{formatters-CF2S2l1m.js → formatters-FWJDwTFv.js} +0 -0
|
@@ -7,7 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useState } from 'react';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
FileBarChart,
|
|
12
|
+
Download,
|
|
13
|
+
Calendar,
|
|
14
|
+
Filter,
|
|
15
|
+
Clock,
|
|
16
|
+
Plus,
|
|
17
|
+
ChevronLeft,
|
|
18
|
+
ChevronRight,
|
|
19
|
+
} from 'lucide-react';
|
|
11
20
|
import { PageLayout } from '../components/PageLayout';
|
|
12
21
|
import {
|
|
13
22
|
Card,
|
|
@@ -24,8 +33,11 @@ import {
|
|
|
24
33
|
Skeleton,
|
|
25
34
|
} from '../components/ui';
|
|
26
35
|
import { EmphasisPanel, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
36
|
+
import { VolumeBar } from '../components/charts';
|
|
27
37
|
import { useReports, type CreateReportInput } from '../hooks/useReports';
|
|
38
|
+
import { useAnalytics } from '../hooks/useAnalytics';
|
|
28
39
|
import { formatDate } from '../utils/formatters';
|
|
40
|
+
import { downloadFile, downloadJson, recordToCsv, slugifyForFilename } from '../utils/download';
|
|
29
41
|
|
|
30
42
|
const REPORT_TYPES: { value: string; label: string }[] = [
|
|
31
43
|
{ value: 'operations', label: 'Operations Summary' },
|
|
@@ -40,8 +52,16 @@ interface Feedback {
|
|
|
40
52
|
text: string;
|
|
41
53
|
}
|
|
42
54
|
|
|
55
|
+
const REPORTS_PAGE_SIZE = 10;
|
|
56
|
+
|
|
43
57
|
export default function ReportsPage() {
|
|
44
|
-
const
|
|
58
|
+
const [page, setPage] = useState(0);
|
|
59
|
+
const { reports, hasMore, isLoading, error, refetch, createReport, generateReport } = useReports({
|
|
60
|
+
limit: REPORTS_PAGE_SIZE,
|
|
61
|
+
offset: page * REPORTS_PAGE_SIZE,
|
|
62
|
+
});
|
|
63
|
+
const { analytics } = useAnalytics();
|
|
64
|
+
const previewData = analytics.trends.dailyOrders.slice(-7);
|
|
45
65
|
const [reportType, setReportType] = useState('operations');
|
|
46
66
|
const [busyId, setBusyId] = useState<string | null>(null);
|
|
47
67
|
const [creating, setCreating] = useState(false);
|
|
@@ -79,8 +99,21 @@ export default function ReportsPage() {
|
|
|
79
99
|
setBusyId(id);
|
|
80
100
|
setFeedback(null);
|
|
81
101
|
try {
|
|
82
|
-
await generateReport(id);
|
|
83
|
-
|
|
102
|
+
const generated = await generateReport(id);
|
|
103
|
+
// Turn the freshly generated report into a downloadable file. Prefer a
|
|
104
|
+
// flat CSV of the result payload; fall back to the full JSON document so a
|
|
105
|
+
// file always lands even when the result is nested/empty.
|
|
106
|
+
const baseName = `${slugifyForFilename(generated.report.name)}-${generated.generatedAt.slice(
|
|
107
|
+
0,
|
|
108
|
+
10
|
|
109
|
+
)}`;
|
|
110
|
+
const csv = recordToCsv(generated.result ?? null);
|
|
111
|
+
if (csv) {
|
|
112
|
+
downloadFile(`${baseName}.csv`, csv, 'text/csv');
|
|
113
|
+
} else {
|
|
114
|
+
downloadJson(`${baseName}.json`, generated);
|
|
115
|
+
}
|
|
116
|
+
setFeedback({ tone: 'success', text: 'Report generated and downloaded.' });
|
|
84
117
|
} catch (genError) {
|
|
85
118
|
setFeedback({
|
|
86
119
|
tone: 'error',
|
|
@@ -212,9 +245,60 @@ export default function ReportsPage() {
|
|
|
212
245
|
</Card>
|
|
213
246
|
))
|
|
214
247
|
)}
|
|
248
|
+
|
|
249
|
+
{!isLoading && !error && (reports.length > 0 || page > 0) && (
|
|
250
|
+
<div className="flex flex-wrap items-center justify-between gap-3 pt-1">
|
|
251
|
+
<span className="text-sm text-text-secondary">
|
|
252
|
+
Showing {reports.length === 0 ? 0 : page * REPORTS_PAGE_SIZE + 1} to{' '}
|
|
253
|
+
{page * REPORTS_PAGE_SIZE + reports.length}
|
|
254
|
+
</span>
|
|
255
|
+
<div className="flex items-center gap-1">
|
|
256
|
+
<Button
|
|
257
|
+
variant="outline"
|
|
258
|
+
size="icon"
|
|
259
|
+
className="h-8 w-8"
|
|
260
|
+
aria-label="Previous page"
|
|
261
|
+
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
|
262
|
+
disabled={page === 0}
|
|
263
|
+
>
|
|
264
|
+
<ChevronLeft className="h-4 w-4" />
|
|
265
|
+
</Button>
|
|
266
|
+
<span className="px-3 text-sm">Page {page + 1}</span>
|
|
267
|
+
<Button
|
|
268
|
+
variant="outline"
|
|
269
|
+
size="icon"
|
|
270
|
+
className="h-8 w-8"
|
|
271
|
+
aria-label="Next page"
|
|
272
|
+
onClick={() => setPage((p) => p + 1)}
|
|
273
|
+
disabled={!hasMore}
|
|
274
|
+
>
|
|
275
|
+
<ChevronRight className="h-4 w-4" />
|
|
276
|
+
</Button>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
215
280
|
</div>
|
|
216
281
|
|
|
217
282
|
<div className="space-y-6">
|
|
283
|
+
{/* Data preview — a real snapshot (recent order volume) of what a
|
|
284
|
+
generated report draws from. Honest empty state when no data. */}
|
|
285
|
+
<Card>
|
|
286
|
+
<CardHeader>
|
|
287
|
+
<CardTitle className="text-base">Data preview</CardTitle>
|
|
288
|
+
</CardHeader>
|
|
289
|
+
<CardContent>
|
|
290
|
+
<p className="mb-2 text-xs text-text-secondary">Order volume · last 7 days</p>
|
|
291
|
+
<VolumeBar
|
|
292
|
+
xKey="date"
|
|
293
|
+
height={160}
|
|
294
|
+
showLegend={false}
|
|
295
|
+
data={previewData}
|
|
296
|
+
series={[{ dataKey: 'orders', name: 'Orders' }]}
|
|
297
|
+
emptyMessage="No order data to preview yet"
|
|
298
|
+
/>
|
|
299
|
+
</CardContent>
|
|
300
|
+
</Card>
|
|
301
|
+
|
|
218
302
|
{/* Scheduled Reports — the page's single emphasis zone */}
|
|
219
303
|
<EmphasisPanel className="space-y-3">
|
|
220
304
|
<h2 className="flex items-center gap-2 text-base font-semibold text-text-primary">
|
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
Plus,
|
|
16
16
|
Trash2,
|
|
17
17
|
Save,
|
|
18
|
+
ArrowUp,
|
|
19
|
+
ArrowDown,
|
|
20
|
+
Fuel,
|
|
18
21
|
} from 'lucide-react';
|
|
19
22
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
20
23
|
import { StatusBadge } from '../components/StatusBadge';
|
|
@@ -34,13 +37,16 @@ import {
|
|
|
34
37
|
SelectValue,
|
|
35
38
|
} from '../components/ui';
|
|
36
39
|
import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
40
|
+
import { RouteMap, type RouteStop as MapRouteStop } from '../components/maps';
|
|
41
|
+
import { firstRealLatLng } from '../utils/mapMarkers';
|
|
37
42
|
import { useRoute, useRoutes } from '../hooks/useRoutes';
|
|
38
43
|
import { useFleet } from '../hooks/useFleet';
|
|
39
44
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
40
|
-
import { formatDate, formatAddress } from '../utils/formatters';
|
|
45
|
+
import { formatDate, formatAddress, formatCurrency } from '../utils/formatters';
|
|
41
46
|
import { joinPath } from '../utils/navigation';
|
|
42
47
|
import { ROUTE_STATUS_COLORS } from '../constants';
|
|
43
48
|
import { FormError } from '../components/FormError';
|
|
49
|
+
import { Notice, useNotice } from '../components/Notice';
|
|
44
50
|
import type { Route as RouteType, RouteStop } from '../types';
|
|
45
51
|
|
|
46
52
|
const ROUTE_STATUS_OPTIONS: { value: RouteType['status']; label: string }[] = [
|
|
@@ -90,6 +96,15 @@ function NewRouteForm() {
|
|
|
90
96
|
stops: prev.stops.map((stop, i) => (i === index ? { ...stop, [field]: value } : stop)),
|
|
91
97
|
}));
|
|
92
98
|
|
|
99
|
+
const moveStop = (index: number, direction: -1 | 1) =>
|
|
100
|
+
setForm((prev) => {
|
|
101
|
+
const target = index + direction;
|
|
102
|
+
if (target < 0 || target >= prev.stops.length) return prev;
|
|
103
|
+
const stops = [...prev.stops];
|
|
104
|
+
[stops[index], stops[target]] = [stops[target], stops[index]];
|
|
105
|
+
return { ...prev, stops };
|
|
106
|
+
});
|
|
107
|
+
|
|
93
108
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
94
109
|
e.preventDefault();
|
|
95
110
|
if (!form.name.trim()) {
|
|
@@ -152,8 +167,9 @@ function NewRouteForm() {
|
|
|
152
167
|
}
|
|
153
168
|
>
|
|
154
169
|
<PagePurpose icon={<Navigation size={18} />}>
|
|
155
|
-
Plan a new delivery route — name it, list the stops to visit, and optionally assign a
|
|
156
|
-
and driver. Once created you can optimize it to sequence the stops into the shortest
|
|
170
|
+
Plan a new delivery route — name it, list the stops to visit, and optionally assign a
|
|
171
|
+
vehicle and driver. Once created you can optimize it to sequence the stops into the shortest
|
|
172
|
+
run.
|
|
157
173
|
</PagePurpose>
|
|
158
174
|
|
|
159
175
|
<form onSubmit={handleSubmit} className="max-w-3xl space-y-6">
|
|
@@ -273,7 +289,27 @@ function NewRouteForm() {
|
|
|
273
289
|
</SelectContent>
|
|
274
290
|
</Select>
|
|
275
291
|
</div>
|
|
276
|
-
<div className="md:col-span-1 flex justify-end">
|
|
292
|
+
<div className="md:col-span-1 flex justify-end gap-1">
|
|
293
|
+
<Button
|
|
294
|
+
type="button"
|
|
295
|
+
variant="ghost"
|
|
296
|
+
size="icon"
|
|
297
|
+
onClick={() => moveStop(index, -1)}
|
|
298
|
+
disabled={index === 0}
|
|
299
|
+
aria-label={`Move stop ${index + 1} up`}
|
|
300
|
+
>
|
|
301
|
+
<ArrowUp size={16} />
|
|
302
|
+
</Button>
|
|
303
|
+
<Button
|
|
304
|
+
type="button"
|
|
305
|
+
variant="ghost"
|
|
306
|
+
size="icon"
|
|
307
|
+
onClick={() => moveStop(index, 1)}
|
|
308
|
+
disabled={index === form.stops.length - 1}
|
|
309
|
+
aria-label={`Move stop ${index + 1} down`}
|
|
310
|
+
>
|
|
311
|
+
<ArrowDown size={16} />
|
|
312
|
+
</Button>
|
|
277
313
|
<Button
|
|
278
314
|
type="button"
|
|
279
315
|
variant="ghost"
|
|
@@ -300,10 +336,28 @@ export default function RouteDetailsPage() {
|
|
|
300
336
|
const { basePath, navigate } = useMoveTheWheels();
|
|
301
337
|
const { route, isLoading, error, refetch } = useRoute(routeId);
|
|
302
338
|
const { optimizeRoute } = useRoutes();
|
|
339
|
+
const { message: notice, notify } = useNotice();
|
|
340
|
+
const [optimizeError, setOptimizeError] = useState<string | null>(null);
|
|
341
|
+
const [optimizing, setOptimizing] = useState(false);
|
|
303
342
|
|
|
304
343
|
const goBack = () => navigate?.(joinPath(basePath, 'routes'));
|
|
305
344
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
306
345
|
|
|
346
|
+
const handleOptimize = async () => {
|
|
347
|
+
if (!routeId) return;
|
|
348
|
+
setOptimizing(true);
|
|
349
|
+
setOptimizeError(null);
|
|
350
|
+
try {
|
|
351
|
+
await optimizeRoute(routeId);
|
|
352
|
+
notify('Route optimized — stops resequenced for the shortest run.');
|
|
353
|
+
refetch();
|
|
354
|
+
} catch (e) {
|
|
355
|
+
setOptimizeError(e instanceof Error ? e.message : 'Failed to optimize route.');
|
|
356
|
+
} finally {
|
|
357
|
+
setOptimizing(false);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
307
361
|
// Handle new route form
|
|
308
362
|
if (!routeId) {
|
|
309
363
|
return <NewRouteForm />;
|
|
@@ -312,14 +366,21 @@ export default function RouteDetailsPage() {
|
|
|
312
366
|
if (isLoading) return <LoadingState message="Loading route details..." />;
|
|
313
367
|
if (error || !route) return <ErrorState title="Route not found" onRetry={refetch} />;
|
|
314
368
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
369
|
+
// Parse coordinates off each waypoint's address; stops without a real coord are
|
|
370
|
+
// dropped from the map (but still listed below), so the plotted line is honest.
|
|
371
|
+
const mapStops: MapRouteStop[] = route.stops
|
|
372
|
+
.map((stop) => {
|
|
373
|
+
const coord = firstRealLatLng(stop.address);
|
|
374
|
+
return coord
|
|
375
|
+
? {
|
|
376
|
+
lat: coord.lat,
|
|
377
|
+
lng: coord.lng,
|
|
378
|
+
sequence: stop.sequence,
|
|
379
|
+
label: formatAddress(stop.address) || `Order #${stop.orderId || stop.sequence}`,
|
|
380
|
+
}
|
|
381
|
+
: null;
|
|
382
|
+
})
|
|
383
|
+
.filter((stop): stop is MapRouteStop => stop !== null);
|
|
323
384
|
|
|
324
385
|
return (
|
|
325
386
|
<PageLayout
|
|
@@ -332,8 +393,8 @@ export default function RouteDetailsPage() {
|
|
|
332
393
|
<ArrowLeft size={16} /> Back
|
|
333
394
|
</Button>
|
|
334
395
|
{!route.optimized && (
|
|
335
|
-
<Button variant="outline" onClick={handleOptimize}>
|
|
336
|
-
<Zap size={16} /> Optimize
|
|
396
|
+
<Button variant="outline" onClick={handleOptimize} disabled={optimizing}>
|
|
397
|
+
<Zap size={16} /> {optimizing ? 'Optimizing…' : 'Optimize'}
|
|
337
398
|
</Button>
|
|
338
399
|
)}
|
|
339
400
|
<Button>
|
|
@@ -387,6 +448,17 @@ export default function RouteDetailsPage() {
|
|
|
387
448
|
]}
|
|
388
449
|
/>
|
|
389
450
|
|
|
451
|
+
{optimizeError && (
|
|
452
|
+
<div className="mb-2">
|
|
453
|
+
<FormError message={optimizeError} />
|
|
454
|
+
</div>
|
|
455
|
+
)}
|
|
456
|
+
{notice && (
|
|
457
|
+
<div className="mb-2">
|
|
458
|
+
<Notice message={notice} />
|
|
459
|
+
</div>
|
|
460
|
+
)}
|
|
461
|
+
|
|
390
462
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
391
463
|
<div className="lg:col-span-2 space-y-6">
|
|
392
464
|
<Card>
|
|
@@ -394,12 +466,32 @@ export default function RouteDetailsPage() {
|
|
|
394
466
|
<CardTitle>Route Map</CardTitle>
|
|
395
467
|
</CardHeader>
|
|
396
468
|
<CardContent>
|
|
397
|
-
|
|
398
|
-
<
|
|
399
|
-
|
|
400
|
-
|
|
469
|
+
{mapStops.length > 0 ? (
|
|
470
|
+
<RouteMap
|
|
471
|
+
className="overflow-hidden rounded-lg border border-border-seam"
|
|
472
|
+
height={360}
|
|
473
|
+
stops={mapStops}
|
|
474
|
+
/>
|
|
475
|
+
) : (
|
|
476
|
+
<div className="rounded-lg border border-border-seam bg-bg-sunken/40 p-4">
|
|
477
|
+
<p className="mb-3 text-sm text-text-secondary">
|
|
478
|
+
These stops don't have map coordinates yet — showing the planned sequence
|
|
479
|
+
instead.
|
|
480
|
+
</p>
|
|
481
|
+
<ol className="space-y-2">
|
|
482
|
+
{route.stops.map((stop) => (
|
|
483
|
+
<li key={stop.id} className="flex items-start gap-3">
|
|
484
|
+
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-xs font-semibold text-primary">
|
|
485
|
+
{stop.sequence}
|
|
486
|
+
</span>
|
|
487
|
+
<span className="text-sm">
|
|
488
|
+
{formatAddress(stop.address) || `Order #${stop.orderId || stop.sequence}`}
|
|
489
|
+
</span>
|
|
490
|
+
</li>
|
|
491
|
+
))}
|
|
492
|
+
</ol>
|
|
401
493
|
</div>
|
|
402
|
-
|
|
494
|
+
)}
|
|
403
495
|
</CardContent>
|
|
404
496
|
</Card>
|
|
405
497
|
|
|
@@ -475,6 +567,14 @@ export default function RouteDetailsPage() {
|
|
|
475
567
|
<span className="text-sm font-normal"> min</span>
|
|
476
568
|
</p>
|
|
477
569
|
</div>
|
|
570
|
+
{route.fuelCost !== undefined && (
|
|
571
|
+
<div>
|
|
572
|
+
<p className="text-sm text-text-secondary flex items-center gap-1">
|
|
573
|
+
<Fuel size={14} /> Est. Fuel Cost
|
|
574
|
+
</p>
|
|
575
|
+
<p className="text-2xl font-bold">{formatCurrency(route.fuelCost)}</p>
|
|
576
|
+
</div>
|
|
577
|
+
)}
|
|
478
578
|
{route.actualDuration && (
|
|
479
579
|
<div>
|
|
480
580
|
<p className="text-sm text-text-secondary">Actual Duration</p>
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* Routes Page
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { useState } from 'react';
|
|
5
6
|
import { Plus, RefreshCw, Navigation } from 'lucide-react';
|
|
6
7
|
import { PageLayout } from '../components/PageLayout';
|
|
7
8
|
import { DataTable, type Column } from '../components/DataTable';
|
|
8
9
|
import { StatusBadge } from '../components/StatusBadge';
|
|
9
10
|
import { Badge, Button } from '../components/ui';
|
|
10
11
|
import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
11
|
-
import { useRoutes } from '../hooks/useRoutes';
|
|
12
|
+
import { useRoutes, useRouteStats } from '../hooks/useRoutes';
|
|
12
13
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
13
14
|
import { formatDate } from '../utils/formatters';
|
|
14
15
|
import { joinPath } from '../utils/navigation';
|
|
@@ -17,7 +18,14 @@ import type { Route } from '../types';
|
|
|
17
18
|
|
|
18
19
|
export default function RoutesPage() {
|
|
19
20
|
const { basePath, navigate } = useMoveTheWheels();
|
|
20
|
-
const
|
|
21
|
+
const [page, setPage] = useState(0);
|
|
22
|
+
const [pageSize, setPageSize] = useState(25);
|
|
23
|
+
const { routes, hasMore, isLoading, refetch } = useRoutes({
|
|
24
|
+
limit: pageSize,
|
|
25
|
+
offset: page * pageSize,
|
|
26
|
+
});
|
|
27
|
+
// Bounded aggregate for the tiles + subtitle — not the truncated page.
|
|
28
|
+
const { stats } = useRouteStats();
|
|
21
29
|
|
|
22
30
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
23
31
|
|
|
@@ -72,7 +80,7 @@ export default function RoutesPage() {
|
|
|
72
80
|
return (
|
|
73
81
|
<PageLayout
|
|
74
82
|
title="Routes"
|
|
75
|
-
subtitle={`${totalRoutes} routes`}
|
|
83
|
+
subtitle={`${stats.totalRoutes} routes`}
|
|
76
84
|
icon={<Navigation size={24} />}
|
|
77
85
|
headerActions={
|
|
78
86
|
<CTAOverflowMenu
|
|
@@ -96,18 +104,18 @@ export default function RoutesPage() {
|
|
|
96
104
|
<p className="text-sm font-medium text-text-secondary">Routes at a glance</p>
|
|
97
105
|
<div className="mt-3 grid grid-cols-3 gap-4">
|
|
98
106
|
<div>
|
|
99
|
-
<p className="text-2xl font-bold text-text-primary tabular-nums">{totalRoutes}</p>
|
|
107
|
+
<p className="text-2xl font-bold text-text-primary tabular-nums">{stats.totalRoutes}</p>
|
|
100
108
|
<p className="text-xs text-text-secondary">Total routes</p>
|
|
101
109
|
</div>
|
|
102
110
|
<div>
|
|
103
111
|
<p className="text-2xl font-bold text-text-primary tabular-nums">
|
|
104
|
-
{
|
|
112
|
+
{stats.inProgressRoutes}
|
|
105
113
|
</p>
|
|
106
114
|
<p className="text-xs text-text-secondary">In progress</p>
|
|
107
115
|
</div>
|
|
108
116
|
<div>
|
|
109
117
|
<p className="text-2xl font-bold text-text-primary tabular-nums">
|
|
110
|
-
{
|
|
118
|
+
{stats.optimizedRoutes}
|
|
111
119
|
</p>
|
|
112
120
|
<p className="text-xs text-text-secondary">Optimized</p>
|
|
113
121
|
</div>
|
|
@@ -123,6 +131,14 @@ export default function RoutesPage() {
|
|
|
123
131
|
searchFields={['name'] as (keyof Route)[]}
|
|
124
132
|
onRowClick={(row) => goTo(`routes/${row.id}`)}
|
|
125
133
|
getRowId={(row) => row.id}
|
|
134
|
+
pageIndex={page}
|
|
135
|
+
pageSize={pageSize}
|
|
136
|
+
onPageChange={setPage}
|
|
137
|
+
onPageSizeChange={(size) => {
|
|
138
|
+
setPageSize(size);
|
|
139
|
+
setPage(0);
|
|
140
|
+
}}
|
|
141
|
+
hasNextPage={hasMore}
|
|
126
142
|
emptyIllustration="empty-data"
|
|
127
143
|
emptyTitle="No routes yet"
|
|
128
144
|
emptyDescription="Create a route to batch deliveries into an optimized driving plan."
|
|
@@ -187,7 +187,7 @@ export default function SettingsPage() {
|
|
|
187
187
|
{
|
|
188
188
|
id: 'security',
|
|
189
189
|
label: 'Secure your account',
|
|
190
|
-
description: '
|
|
190
|
+
description: 'Enable two-factor authentication for an extra layer of security.',
|
|
191
191
|
icon: <Shield size={16} />,
|
|
192
192
|
onClick: () => setTab('security'),
|
|
193
193
|
},
|
|
@@ -222,7 +222,7 @@ export default function SettingsPage() {
|
|
|
222
222
|
<EmphasisPanel>
|
|
223
223
|
<h2 className="text-base font-semibold text-text-primary">Company Information</h2>
|
|
224
224
|
<p className="text-sm text-text-secondary mb-4">Basic company details</p>
|
|
225
|
-
<div className="grid grid-cols-2 gap-4">
|
|
225
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
226
226
|
<div>
|
|
227
227
|
<Label>Company Name</Label>
|
|
228
228
|
<Input
|
|
@@ -270,7 +270,7 @@ export default function SettingsPage() {
|
|
|
270
270
|
</CardTitle>
|
|
271
271
|
</CardHeader>
|
|
272
272
|
<CardContent className="space-y-4">
|
|
273
|
-
<div className="grid grid-cols-3 gap-4">
|
|
273
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
274
274
|
<div>
|
|
275
275
|
<Label>Timezone</Label>
|
|
276
276
|
<Select
|
|
@@ -352,26 +352,10 @@ export default function SettingsPage() {
|
|
|
352
352
|
<CardHeader>
|
|
353
353
|
<CardTitle>Password</CardTitle>
|
|
354
354
|
<CardDescription>
|
|
355
|
-
|
|
355
|
+
Your password and sign-in credentials are managed centrally by your workspace
|
|
356
|
+
identity provider — update them from your account security settings, not here.
|
|
356
357
|
</CardDescription>
|
|
357
358
|
</CardHeader>
|
|
358
|
-
<CardContent className="space-y-4">
|
|
359
|
-
<div>
|
|
360
|
-
<Label>Current Password</Label>
|
|
361
|
-
<Input type="password" autoComplete="current-password" />
|
|
362
|
-
</div>
|
|
363
|
-
<div className="grid grid-cols-2 gap-4">
|
|
364
|
-
<div>
|
|
365
|
-
<Label>New Password</Label>
|
|
366
|
-
<Input type="password" autoComplete="new-password" />
|
|
367
|
-
</div>
|
|
368
|
-
<div>
|
|
369
|
-
<Label>Confirm Password</Label>
|
|
370
|
-
<Input type="password" autoComplete="new-password" />
|
|
371
|
-
</div>
|
|
372
|
-
</div>
|
|
373
|
-
<Button variant="outline">Update Password</Button>
|
|
374
|
-
</CardContent>
|
|
375
359
|
</Card>
|
|
376
360
|
<Card>
|
|
377
361
|
<CardHeader>
|
|
@@ -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...')}
|