@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.
Files changed (173) hide show
  1. package/dist/{AIAssistantPage-C3cPsWt-.js → AIAssistantPage-D2gaEL1U.js} +16 -15
  2. package/dist/AnalyticsPage-DZMlEoBi.js +194 -0
  3. package/dist/CreateOrderPage-CWHx8yys.js +490 -0
  4. package/dist/CustomerDetailsPage-nR4oZ_ju.js +615 -0
  5. package/dist/CustomersPage-Bbvwvn8k.js +205 -0
  6. package/dist/DashboardPage-DrEhNf7N.js +542 -0
  7. package/dist/DataTable-CqMUD4er.js +340 -0
  8. package/dist/DocumentsCard-edDkgnJj.js +169 -0
  9. package/dist/DriverDetailsPage-whEMOWs9.js +490 -0
  10. package/dist/DriversPage-CHF2ZEzd.js +221 -0
  11. package/dist/FinancePage-Dn9GrW-1.js +175 -0
  12. package/dist/{FleetPage-DqkFi3s3.js → FleetPage-CrtGPy-p.js} +36 -35
  13. package/dist/{FormError-BlOjnXOq.js → FormError-BKcb_wT-.js} +1 -1
  14. package/dist/{ImportExportPage-DKJRCezl.js → ImportExportPage-WYdwZomZ.js} +52 -72
  15. package/dist/InventoryPage-l5A1f753.js +259 -0
  16. package/dist/LiveTrackingPage-DhEaNKZ4.js +429 -0
  17. package/dist/MarketplacePage-B9RYOb1Y.js +426 -0
  18. package/dist/MetricCard-y3bWHKUQ.js +191 -0
  19. package/dist/Notice-DOO-tjpV.js +46 -0
  20. package/dist/OrderDetailsPage-CwYHwk1K.js +584 -0
  21. package/dist/OrdersListPage-DhZJoQeI.js +386 -0
  22. package/dist/{PageLayout-D4LDHJJq.js → PageLayout-BniRJaQ6.js} +1 -1
  23. package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-1Gm9JKy0.js} +56 -56
  24. package/dist/ProductsPage-C8sn-1tN.js +186 -0
  25. package/dist/ReportsPage-D1tb0dcz.js +377 -0
  26. package/dist/RouteDetailsPage-CkMyR9b_.js +610 -0
  27. package/dist/{RoutesPage-DMfoicM1.js → RoutesPage-QkENzsE5.js} +58 -47
  28. package/dist/{SettingsPage-Bn-v0mbQ.js → SettingsPage-DV3_tU_X.js} +16 -38
  29. package/dist/Sparkline-K2RMJ58u.js +16729 -0
  30. package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-BNHXYvZx.js} +7 -7
  31. package/dist/TrackingPage-BZrg4RCu.js +368 -0
  32. package/dist/VehicleDetailsPage-D-8XPZ2G.js +393 -0
  33. package/dist/VehiclesPage-D5BELbtM.js +184 -0
  34. package/dist/{WarehouseDetailsPage-L_6sWigM.js → WarehouseDetailsPage-Cmg-8Frn.js} +70 -70
  35. package/dist/WarehousesPage-C1Y1M867.js +182 -0
  36. package/dist/{arrow-left-n48Y-Wis.js → arrow-left-ClW4Isj7.js} +1 -1
  37. package/dist/arrow-right-CmKOmIkh.js +11 -0
  38. package/dist/arrow-up-BJijRLAZ.js +17 -0
  39. package/dist/{box-D9Pal4HM.js → box-BT3NsRQP.js} +1 -1
  40. package/dist/boxes-B_OGvQiW.js +54 -0
  41. package/dist/{chart-column-BFIJysDR.js → chart-column-DQTTdUPk.js} +1 -1
  42. package/dist/charts-Tj_PPTfA.js +2018 -0
  43. package/dist/chevron-left-B1rZyWWb.js +8 -0
  44. package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-BIcwUJZq.js} +1 -1
  45. package/dist/chunk-BdsCSvBP.js +20 -0
  46. package/dist/{circle-alert-iY24lSlp.js → circle-alert-CNkuDNc6.js} +1 -1
  47. package/dist/circle-check-Cs1OVjtV.js +13 -0
  48. package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-C-p4eNwn.js} +1 -1
  49. package/dist/{clock-DJZtdl8u.js → clock-dRGG4rkF.js} +1 -1
  50. package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-B6RoYHHz.js} +1 -1
  51. package/dist/{download-DRXxoiQR.js → download-DP04iWTd.js} +1 -1
  52. package/dist/{file-text-DWWY6_f5.js → file-text-BuDx4hZp.js} +1 -1
  53. package/dist/{filter-DloLC2eE.js → filter-BD483_8g.js} +1 -1
  54. package/dist/{wrench-CANXWxF-.js → fuel-DLH1ag04.js} +3 -6
  55. package/dist/index.css +2 -0
  56. package/dist/index.js +150 -172
  57. package/dist/{map-CkJaULb2.js → map-DNQn9fyY.js} +1 -1
  58. package/dist/{map-pin-BGiCvyCs.js → map-pin-CqN1bbT1.js} +1 -1
  59. package/dist/mapMarkers-NCV9MRKR.js +28 -0
  60. package/dist/maps-yzLqiK_C.js +5070 -0
  61. package/dist/{navigation-D36prnAn.js → navigation-CgGUt2tR.js} +1 -1
  62. package/dist/{package-BChUzpJS.js → package-DmRF7iQD.js} +1 -1
  63. package/dist/{phone-Ac9s3mo_.js → phone-Cl32EXKW.js} +1 -1
  64. package/dist/{plus-Z76q-FZm.js → plus-DlANgOFx.js} +1 -1
  65. package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-rW59D4b7.js} +1 -1
  66. package/dist/route-CWjPrtAN.js +22 -0
  67. package/dist/{save-CojywNhG.js → save-UDlog7hg.js} +1 -1
  68. package/dist/{search-CDGefqb3.js → search-DNIG5Ck7.js} +1 -1
  69. package/dist/{square-pen-CopE9GmB.js → square-pen-BcfB3Grd.js} +1 -1
  70. package/dist/{star-nHlLjDKR.js → star-CVZHFwDx.js} +1 -1
  71. package/dist/{store-BR9Voi5E.js → store-bwnmCK9f.js} +1 -1
  72. package/dist/{trash-2-BKhar6Hs.js → trash-2-B80G12d4.js} +1 -1
  73. package/dist/{trending-up-Dohgd4t_.js → trending-up-CH4R4KOa.js} +1 -1
  74. package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-CmS8AUU6.js} +1 -1
  75. package/dist/{truck-C0v_hbLR.js → truck-9rLDncav.js} +1 -1
  76. package/dist/upload-DanJY1Vo.js +21 -0
  77. package/dist/{useAssistant-D4v3oxD3.js → useAssistant-CnEwkTLm.js} +11 -28
  78. package/dist/{useCustomers-BmTNGxiI.js → useCustomers-ChJao1ET.js} +30 -27
  79. package/dist/{useFleet-DKqXZ16m.js → useFleet-g5Yri0G8.js} +82 -70
  80. package/dist/{useImportExport-DHr-SWSw.js → useImportExport-MYwIT3rE.js} +1 -1
  81. package/dist/{useInventory-DkyiVY_A.js → useInventory-BSOn-R1c.js} +84 -39
  82. package/dist/{useOrders-Cfk1GLiq.js → useOrders-BSiqo61w.js} +59 -48
  83. package/dist/{useReports-CTqZr-AB.js → useReports-fWESfsF9.js} +41 -35
  84. package/dist/{useRoutes-BQWO607N.js → useRoutes-zD8Yp323.js} +50 -42
  85. package/dist/{useSettings-CkDH_-gM.js → useSettings-B1lsWeZ1.js} +6 -6
  86. package/dist/{useTracking-B-va1kO3.js → useTracking-BKG0I2ln.js} +3 -3
  87. package/dist/{user-jyFU3lXk.js → user-DU51el3m.js} +1 -1
  88. package/dist/user-check-IMhk-qkX.js +20 -0
  89. package/dist/{users-DZ-ilS6B.js → users-B8KhJBGg.js} +1 -1
  90. package/dist/{warehouse-6m5kXMLu.js → warehouse-CcMMioeY.js} +1 -1
  91. package/dist/wrench-Dg-sgH2e.js +8 -0
  92. package/dist/x-C4ObLVEa.js +11 -0
  93. package/package.json +5 -1
  94. package/src/movethewheels/components/DataTable.tsx +252 -84
  95. package/src/movethewheels/components/DocumentsCard.tsx +102 -0
  96. package/src/movethewheels/components/GlassPanel.tsx +71 -0
  97. package/src/movethewheels/components/MetricCard.tsx +171 -73
  98. package/src/movethewheels/components/Notice.tsx +61 -0
  99. package/src/movethewheels/components/StatusBadge.tsx +7 -6
  100. package/src/movethewheels/components/charts/ChartShell.tsx +78 -0
  101. package/src/movethewheels/components/charts/Sparkline.tsx +85 -0
  102. package/src/movethewheels/components/charts/StatusDonut.tsx +115 -0
  103. package/src/movethewheels/components/charts/TrendChart.tsx +179 -0
  104. package/src/movethewheels/components/charts/VolumeBar.tsx +97 -0
  105. package/src/movethewheels/components/charts/chartTokens.ts +43 -0
  106. package/src/movethewheels/components/charts/index.ts +9 -0
  107. package/src/movethewheels/components/index.ts +51 -1
  108. package/src/movethewheels/components/maps/LogisticsMap.tsx +94 -0
  109. package/src/movethewheels/components/maps/MapCanvas.tsx +118 -0
  110. package/src/movethewheels/components/maps/RouteMap.tsx +71 -0
  111. package/src/movethewheels/components/maps/TrackingMap.tsx +102 -0
  112. package/src/movethewheels/components/maps/index.ts +16 -0
  113. package/src/movethewheels/components/maps/leaflet-setup.ts +124 -0
  114. package/src/movethewheels/components/maps/map-markers.css +121 -0
  115. package/src/movethewheels/hooks/index.ts +9 -0
  116. package/src/movethewheels/hooks/useAnalytics.ts +109 -4
  117. package/src/movethewheels/hooks/useCustomers.ts +11 -4
  118. package/src/movethewheels/hooks/useEntityDocuments.ts +132 -0
  119. package/src/movethewheels/hooks/useFleet.ts +26 -7
  120. package/src/movethewheels/hooks/useInventory.ts +70 -7
  121. package/src/movethewheels/hooks/useOrders.ts +32 -2
  122. package/src/movethewheels/hooks/useReports.ts +14 -4
  123. package/src/movethewheels/hooks/useRoutes.ts +20 -3
  124. package/src/movethewheels/pages/AnalyticsPage.tsx +93 -73
  125. package/src/movethewheels/pages/CreateOrderPage.tsx +48 -20
  126. package/src/movethewheels/pages/CustomerDetailsPage.tsx +239 -33
  127. package/src/movethewheels/pages/CustomersPage.tsx +53 -28
  128. package/src/movethewheels/pages/DashboardPage.tsx +238 -12
  129. package/src/movethewheels/pages/DriverDetailsPage.tsx +17 -31
  130. package/src/movethewheels/pages/DriversPage.tsx +59 -31
  131. package/src/movethewheels/pages/FinancePage.tsx +103 -82
  132. package/src/movethewheels/pages/InventoryPage.tsx +15 -3
  133. package/src/movethewheels/pages/LiveTrackingPage.tsx +124 -16
  134. package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
  135. package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
  136. package/src/movethewheels/pages/OrdersListPage.tsx +118 -47
  137. package/src/movethewheels/pages/ProductsPage.tsx +52 -28
  138. package/src/movethewheels/pages/ReportsPage.tsx +88 -4
  139. package/src/movethewheels/pages/RouteDetailsPage.tsx +119 -19
  140. package/src/movethewheels/pages/RoutesPage.tsx +22 -6
  141. package/src/movethewheels/pages/SettingsPage.tsx +5 -21
  142. package/src/movethewheels/pages/TrackingPage.tsx +76 -6
  143. package/src/movethewheels/pages/VehicleDetailsPage.tsx +4 -34
  144. package/src/movethewheels/pages/VehiclesPage.tsx +58 -27
  145. package/src/movethewheels/pages/WarehouseDetailsPage.tsx +1 -1
  146. package/src/movethewheels/pages/WarehousesPage.tsx +47 -36
  147. package/src/movethewheels/types/index.ts +9 -1
  148. package/src/movethewheels/utils/download.ts +62 -0
  149. package/src/movethewheels/utils/mapMarkers.ts +58 -0
  150. package/dist/AnalyticsPage-Dz6g2MvK.js +0 -215
  151. package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
  152. package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
  153. package/dist/CustomersPage-BKxMS1AV.js +0 -161
  154. package/dist/DashboardPage-BVjQi0EB.js +0 -387
  155. package/dist/DataTable-DmciIKKM.js +0 -249
  156. package/dist/DriverDetailsPage-CN3WQ0B9.js +0 -507
  157. package/dist/DriversPage-CH9rjQC-.js +0 -165
  158. package/dist/FinancePage-DEDykSOP.js +0 -158
  159. package/dist/InventoryPage-e0ze63iX.js +0 -253
  160. package/dist/LiveTrackingPage-DCX-K-LR.js +0 -379
  161. package/dist/MarketplacePage-CIQ97_zp.js +0 -247
  162. package/dist/MetricCard-BY0QrMD4.js +0 -146
  163. package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
  164. package/dist/OrdersListPage-Cn90P1Xx.js +0 -307
  165. package/dist/ProductsPage-QJfHHpqB.js +0 -169
  166. package/dist/ReportsPage-Zp53q3_8.js +0 -284
  167. package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
  168. package/dist/TrackingPage-W-jtdM3Y.js +0 -325
  169. package/dist/VehicleDetailsPage-Bceo5oz3.js +0 -410
  170. package/dist/VehiclesPage-ApKThYeG.js +0 -165
  171. package/dist/WarehousesPage-DCbXXXWE.js +0 -162
  172. package/dist/useAnalytics-DS8n0xgg.js +0 -297
  173. /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 { FileBarChart, Download, Calendar, Filter, Clock, Plus } from 'lucide-react';
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 { reports, isLoading, error, refetch, createReport, generateReport } = useReports();
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
- setFeedback({ tone: 'success', text: 'Report generated.' });
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 vehicle
156
- and driver. Once created you can optimize it to sequence the stops into the shortest run.
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
- const handleOptimize = async () => {
316
- try {
317
- await optimizeRoute(route.id);
318
- refetch();
319
- } catch (e) {
320
- console.error(e);
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
- <div className="aspect-video bg-bg-sunken rounded-lg flex items-center justify-center">
398
- <div className="text-center text-text-secondary">
399
- <MapPin size={48} className="mx-auto mb-2 opacity-50" />
400
- <p>Route map visualization</p>
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&apos;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
- </div>
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 { routes, totalRoutes, isLoading, refetch } = useRoutes();
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
- {routes.filter((r) => r.status === 'in_progress').length}
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
- {routes.filter((r) => r.optimized).length}
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: 'Update your password and enable two-factor authentication.',
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
- Passwords are managed by your workspace identity provider.
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
- <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...')}