@burdenoff/microfe-movethewheels 2026.703.4 → 2026.703.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/{AIAssistantPage-3YOnHO9Y.js → AIAssistantPage-BmILdHgO.js} +16 -15
  2. package/dist/AnalyticsPage-BSlsiSDE.js +194 -0
  3. package/dist/{CreateOrderPage-DRAgGo9i.js → CreateOrderPage-DaCgVaO3.js} +40 -40
  4. package/dist/{CustomerDetailsPage-BCeMmOFe.js → CustomerDetailsPage-DErpbafW.js} +71 -71
  5. package/dist/CustomersPage-C1lRU6xs.js +205 -0
  6. package/dist/DashboardPage-Q5-5EKAU.js +542 -0
  7. package/dist/{DataTable-XvUcTjwi.js → DataTable-KjzbwnRL.js} +31 -31
  8. package/dist/{DocumentsCard-U2lSe-cn.js → DocumentsCard-T9y6cv4-.js} +25 -25
  9. package/dist/{DriverDetailsPage-BQrYd_Lp.js → DriverDetailsPage-DnHGkDC7.js} +64 -64
  10. package/dist/DriversPage-C0ujWVOn.js +221 -0
  11. package/dist/FinancePage-CA2XEgsQ.js +175 -0
  12. package/dist/{FleetPage-3ZHsB-TN.js → FleetPage-DrvXW7wK.js} +22 -22
  13. package/dist/{FormError-cuLV2jVD.js → FormError-CRsMP7TQ.js} +1 -1
  14. package/dist/{ImportExportPage-C6f7DqhM.js → ImportExportPage-Cac9HjFx.js} +12 -12
  15. package/dist/InventoryPage-Ezf4gqKI.js +259 -0
  16. package/dist/LiveTrackingPage-CzbVwDqK.js +429 -0
  17. package/dist/{MarketplacePage-Bh_2ns4v.js → MarketplacePage-BHDmSQmo.js} +116 -116
  18. package/dist/MetricCard-DQlDNqmK.js +191 -0
  19. package/dist/{Notice-CSoDYKw9.js → Notice-GmA7JDR3.js} +2 -2
  20. package/dist/{OrderDetailsPage-zCj0lW0j.js → OrderDetailsPage-CR0Wc1aN.js} +58 -58
  21. package/dist/OrdersListPage-C8Bu6wXM.js +386 -0
  22. package/dist/{PageLayout-D4LDHJJq.js → PageLayout-DX1Ou6V6.js} +164 -134
  23. package/dist/{ProductDetailsPage-wsxAQkA0.js → ProductDetailsPage-B_ns2MOe.js} +55 -55
  24. package/dist/ProductsPage-c92BsEoA.js +186 -0
  25. package/dist/ReportsPage-CPLXtLT2.js +377 -0
  26. package/dist/{RouteDetailsPage-DyMJE9AS.js → RouteDetailsPage-DOfcRiFv.js} +208 -181
  27. package/dist/{RoutesPage-Dtc669yc.js → RoutesPage-BZGublby.js} +12 -12
  28. package/dist/{SettingsPage-CpH9xhQg.js → SettingsPage-5cweX_xh.js} +15 -15
  29. package/dist/Sparkline-zSXvG6vA.js +16729 -0
  30. package/dist/{StatusBadge-C87mDpDP.js → StatusBadge-oZjownr1.js} +7 -7
  31. package/dist/TrackingPage-Ln9qVflK.js +368 -0
  32. package/dist/{VehicleDetailsPage-DldYNsYV.js → VehicleDetailsPage-CrkzE0dN.js} +53 -53
  33. package/dist/VehiclesPage-CTQWH7af.js +184 -0
  34. package/dist/{WarehouseDetailsPage-CMG7or6G.js → WarehouseDetailsPage-CwDLaG65.js} +64 -64
  35. package/dist/WarehousesPage-KW02DvZg.js +182 -0
  36. package/dist/{arrow-left-n48Y-Wis.js → arrow-left-DJDw_Yqv.js} +1 -1
  37. package/dist/{arrow-right-Q3XQpTPR.js → arrow-right-Cb2fLTPJ.js} +1 -1
  38. package/dist/{arrow-up-xNF1zJUG.js → arrow-up-PxaOIyiT.js} +1 -1
  39. package/dist/{box-Eu05PXDh.js → box-hSJVtvGf.js} +1 -1
  40. package/dist/boxes-DDnxEkT5.js +54 -0
  41. package/dist/{chart-column-ouzgxIv_.js → chart-column-xXgAZvRl.js} +1 -1
  42. package/dist/charts-CfCwQg6o.js +2018 -0
  43. package/dist/{chevron-left-B2_r0Elx.js → chevron-left-dhEGJYdh.js} +1 -1
  44. package/dist/{chevron-right-eV4LjaSi.js → chevron-right-Cud2NDvm.js} +1 -1
  45. package/dist/chunk-BdsCSvBP.js +20 -0
  46. package/dist/{circle-alert-Bzh2DBSF.js → circle-alert-Rx0p3D5w.js} +1 -1
  47. package/dist/circle-check-BCxRFdFo.js +13 -0
  48. package/dist/{circle-check-big-CGMSpQmD.js → circle-check-big-BcIJ8xjx.js} +1 -1
  49. package/dist/{clock-Bl3ptexL.js → clock-B2_YxJgB.js} +1 -1
  50. package/dist/{dollar-sign-CAVU95SO.js → dollar-sign-CQmAICv4.js} +1 -1
  51. package/dist/{download-C5NbvVDc.js → download-CG4Tponq.js} +1 -1
  52. package/dist/{file-text-D-1uvLZG.js → file-text-CzH-zMub.js} +1 -1
  53. package/dist/{filter-DUfUg6BO.js → filter-C0u2Lwfn.js} +1 -1
  54. package/dist/{fuel-B6_lnKDs.js → fuel-C2KELcM-.js} +1 -1
  55. package/dist/index.css +2 -0
  56. package/dist/index.js +214 -225
  57. package/dist/{map-C-M-FQLR.js → map-FuPu5aTw.js} +1 -1
  58. package/dist/{map-pin-Dznl5l40.js → map-pin-czpLv9uy.js} +1 -1
  59. package/dist/mapMarkers-NCV9MRKR.js +28 -0
  60. package/dist/maps-D9m7PKrs.js +5070 -0
  61. package/dist/{navigation-BJSJl-VG.js → navigation-D5_pdCSS.js} +1 -1
  62. package/dist/{package-CcRttV2N.js → package-B0sdi6lU.js} +1 -1
  63. package/dist/{phone-DVSZHt_j.js → phone-V2nuzPu9.js} +1 -1
  64. package/dist/{plus-C_XZDwBz.js → plus-BqQxkfoU.js} +1 -1
  65. package/dist/{refresh-cw-CfgN67l3.js → refresh-cw-CEOAPEcV.js} +1 -1
  66. package/dist/route-CZWcW-Dw.js +22 -0
  67. package/dist/{save-DJE8Fpdv.js → save-DDFvrxWl.js} +1 -1
  68. package/dist/{search-D_VyGcZT.js → search-D7Ya6jxj.js} +1 -1
  69. package/dist/{square-pen-D-6JYrVQ.js → square-pen-DoZZS4Lw.js} +1 -1
  70. package/dist/{star-BpRlUZuU.js → star-B7PgVu5j.js} +1 -1
  71. package/dist/{store-B-8HMuyZ.js → store-YVzcIG4A.js} +1 -1
  72. package/dist/{trash-2-8vmv-XV_.js → trash-2-BegnBzwI.js} +1 -1
  73. package/dist/{trending-up-Dz6PqPRj.js → trending-up-DBd3XNdb.js} +1 -1
  74. package/dist/{triangle-alert-D7Y3J_4z.js → triangle-alert-Bxk3Kgs6.js} +1 -1
  75. package/dist/{truck-D-y2IDzv.js → truck-Clrn0oHp.js} +1 -1
  76. package/dist/{upload-CHZofOqb.js → upload-Df5JmMwH.js} +1 -1
  77. package/dist/{useAssistant-DVW4QgQb.js → useAssistant-Cb8nnIVC.js} +11 -28
  78. package/dist/{useCustomers-BPW4__PR.js → useCustomers-Dq2IR2HQ.js} +1 -1
  79. package/dist/{useFleet-ChBkxAd6.js → useFleet-CapnWBcA.js} +1 -1
  80. package/dist/{useImportExport-DrPDHEDK.js → useImportExport-BoGzrlJy.js} +1 -1
  81. package/dist/{useInventory-CAAVMRp6.js → useInventory-Ci6M2d9f.js} +1 -1
  82. package/dist/{useOrders-B465S2nt.js → useOrders-BuQU1OHX.js} +1 -1
  83. package/dist/{useReports-jQ3GpNOm.js → useReports-DIx8W7CO.js} +1 -1
  84. package/dist/{useRoutes-hS3d5-xr.js → useRoutes-D7EvI_pm.js} +1 -1
  85. package/dist/{useSettings-BB8X2RLh.js → useSettings-BhX251UQ.js} +6 -6
  86. package/dist/{useTracking-RjGz9PZG.js → useTracking-C40jmZ6W.js} +3 -3
  87. package/dist/{user-DzfTn4dE.js → user-BlcJrbtE.js} +1 -1
  88. package/dist/{user-check-BEKRCi7v.js → user-check-DtnQmimP.js} +1 -1
  89. package/dist/{users-CCrq-w0C.js → users-Cvwko-Dx.js} +1 -1
  90. package/dist/{warehouse-CaqfmMKh.js → warehouse-LWLVTk6G.js} +1 -1
  91. package/dist/{wrench-q_rgJfaP.js → wrench-Cme0RbyR.js} +1 -1
  92. package/dist/{x-tMeDMcCw.js → x-D8C9Pg5V.js} +1 -1
  93. package/package.json +5 -1
  94. package/src/movethewheels/components/GlassPanel.tsx +71 -0
  95. package/src/movethewheels/components/MetricCard.tsx +171 -73
  96. package/src/movethewheels/components/StatusBadge.tsx +7 -6
  97. package/src/movethewheels/components/charts/ChartShell.tsx +78 -0
  98. package/src/movethewheels/components/charts/Sparkline.tsx +85 -0
  99. package/src/movethewheels/components/charts/StatusDonut.tsx +115 -0
  100. package/src/movethewheels/components/charts/TrendChart.tsx +179 -0
  101. package/src/movethewheels/components/charts/VolumeBar.tsx +97 -0
  102. package/src/movethewheels/components/charts/chartTokens.ts +43 -0
  103. package/src/movethewheels/components/charts/index.ts +9 -0
  104. package/src/movethewheels/components/index.ts +44 -1
  105. package/src/movethewheels/components/maps/LogisticsMap.tsx +94 -0
  106. package/src/movethewheels/components/maps/MapCanvas.tsx +118 -0
  107. package/src/movethewheels/components/maps/RouteMap.tsx +71 -0
  108. package/src/movethewheels/components/maps/TrackingMap.tsx +102 -0
  109. package/src/movethewheels/components/maps/index.ts +16 -0
  110. package/src/movethewheels/components/maps/leaflet-setup.ts +124 -0
  111. package/src/movethewheels/components/maps/map-markers.css +121 -0
  112. package/src/movethewheels/hooks/useAnalytics.ts +14 -4
  113. package/src/movethewheels/pages/AnalyticsPage.tsx +93 -73
  114. package/src/movethewheels/pages/CreateOrderPage.tsx +4 -4
  115. package/src/movethewheels/pages/CustomersPage.tsx +29 -20
  116. package/src/movethewheels/pages/DashboardPage.tsx +238 -12
  117. package/src/movethewheels/pages/DriverDetailsPage.tsx +1 -1
  118. package/src/movethewheels/pages/DriversPage.tsx +35 -24
  119. package/src/movethewheels/pages/FinancePage.tsx +68 -30
  120. package/src/movethewheels/pages/InventoryPage.tsx +15 -3
  121. package/src/movethewheels/pages/LiveTrackingPage.tsx +124 -16
  122. package/src/movethewheels/pages/OrdersListPage.tsx +46 -38
  123. package/src/movethewheels/pages/ProductsPage.tsx +28 -24
  124. package/src/movethewheels/pages/ReportsPage.tsx +23 -0
  125. package/src/movethewheels/pages/RouteDetailsPage.tsx +46 -7
  126. package/src/movethewheels/pages/SettingsPage.tsx +2 -2
  127. package/src/movethewheels/pages/TrackingPage.tsx +76 -6
  128. package/src/movethewheels/pages/VehicleDetailsPage.tsx +1 -1
  129. package/src/movethewheels/pages/VehiclesPage.tsx +34 -24
  130. package/src/movethewheels/pages/WarehouseDetailsPage.tsx +1 -1
  131. package/src/movethewheels/pages/WarehousesPage.tsx +28 -22
  132. package/src/movethewheels/types/index.ts +1 -1
  133. package/src/movethewheels/utils/graphqlClient.ts +81 -31
  134. package/src/movethewheels/utils/mapMarkers.ts +58 -0
  135. package/dist/AnalyticsPage-CMpI1070.js +0 -215
  136. package/dist/CustomersPage-CaNBMXmM.js +0 -172
  137. package/dist/DashboardPage-DIKlXde3.js +0 -387
  138. package/dist/DriversPage-PYmdFiby.js +0 -176
  139. package/dist/FinancePage-Cwh9Bw4M.js +0 -136
  140. package/dist/InventoryPage-DpiqVu1v.js +0 -253
  141. package/dist/LiveTrackingPage-FAzzlVnX.js +0 -379
  142. package/dist/MetricCard-CVBVpu7o.js +0 -146
  143. package/dist/OrdersListPage-CAMsuUck.js +0 -384
  144. package/dist/ProductsPage-DC5qDLfz.js +0 -180
  145. package/dist/ReportsPage-BwUJ37eO.js +0 -356
  146. package/dist/TrackingPage-DaNEFG_h.js +0 -325
  147. package/dist/VehiclesPage-BFw13iMt.js +0 -176
  148. package/dist/WarehousesPage-Wkytnt86.js +0 -173
  149. package/dist/useAnalytics-D7HW2XRb.js +0 -347
  150. /package/dist/{formatters-BQr-aR5W.js → formatters-FWJDwTFv.js} +0 -0
@@ -0,0 +1,102 @@
1
+ import { useMemo } from 'react';
2
+ import { Marker, Polyline, Popup } from 'react-leaflet';
3
+ import { MapCanvas } from './MapCanvas';
4
+ import { createCurrentIcon, createPinIcon, isRealCoord } from './leaflet-setup';
5
+
6
+ export interface TrackPoint {
7
+ lat: number;
8
+ lng: number;
9
+ timestamp?: string;
10
+ }
11
+
12
+ export interface TrackEndpoint {
13
+ lat: number;
14
+ lng: number;
15
+ label?: string;
16
+ }
17
+
18
+ interface TrackingMapProps {
19
+ origin?: TrackEndpoint | null;
20
+ destination?: TrackEndpoint | null;
21
+ /** Ordered breadcrumb of positions the shipment has passed through. */
22
+ trail: TrackPoint[];
23
+ current?: TrackEndpoint | null;
24
+ height?: number | string;
25
+ className?: string;
26
+ /** Trail polyline color (design-token string). Defaults to brand blue. */
27
+ trailColor?: string;
28
+ emptyMessage?: string;
29
+ }
30
+
31
+ /**
32
+ * TrackingMap — renders a shipment's breadcrumb trail with origin/destination
33
+ * pins and a live "current position" dot. Everything is filtered for real
34
+ * coordinates so a missing GPS fix never plots at null-island.
35
+ */
36
+ export function TrackingMap({
37
+ origin,
38
+ destination,
39
+ trail,
40
+ current,
41
+ height = 360,
42
+ className,
43
+ trailColor = 'var(--color-chart-1)',
44
+ emptyMessage = 'No tracking positions yet',
45
+ }: TrackingMapProps) {
46
+ const trailLine = useMemo<Array<[number, number]>>(
47
+ () => trail.filter((p) => isRealCoord(p.lat, p.lng)).map((p) => [p.lat, p.lng]),
48
+ [trail]
49
+ );
50
+
51
+ const originOk = origin && isRealCoord(origin.lat, origin.lng) ? origin : null;
52
+ const destOk = destination && isRealCoord(destination.lat, destination.lng) ? destination : null;
53
+ const currentOk = current && isRealCoord(current.lat, current.lng) ? current : null;
54
+
55
+ const fitPoints = useMemo<Array<[number, number]>>(() => {
56
+ const pts: Array<[number, number]> = [...trailLine];
57
+ if (originOk) pts.push([originOk.lat, originOk.lng]);
58
+ if (destOk) pts.push([destOk.lat, destOk.lng]);
59
+ if (currentOk) pts.push([currentOk.lat, currentOk.lng]);
60
+ return pts;
61
+ }, [trailLine, originOk, destOk, currentOk]);
62
+
63
+ return (
64
+ <MapCanvas
65
+ points={fitPoints}
66
+ height={height}
67
+ className={className}
68
+ emptyState={<p className="text-sm text-text-secondary">{emptyMessage}</p>}
69
+ >
70
+ {trailLine.length >= 2 && (
71
+ <Polyline
72
+ positions={trailLine}
73
+ pathOptions={{ color: trailColor, weight: 4, opacity: 0.85, dashArray: '1 8', lineCap: 'round' }}
74
+ />
75
+ )}
76
+ {originOk && (
77
+ <Marker position={[originOk.lat, originOk.lng]} icon={createPinIcon('origin', { glyph: false })}>
78
+ <Popup>
79
+ <span className="font-semibold">{originOk.label ?? 'Origin'}</span>
80
+ </Popup>
81
+ </Marker>
82
+ )}
83
+ {destOk && (
84
+ <Marker
85
+ position={[destOk.lat, destOk.lng]}
86
+ icon={createPinIcon('destination', { glyph: false })}
87
+ >
88
+ <Popup>
89
+ <span className="font-semibold">{destOk.label ?? 'Destination'}</span>
90
+ </Popup>
91
+ </Marker>
92
+ )}
93
+ {currentOk && (
94
+ <Marker position={[currentOk.lat, currentOk.lng]} icon={createCurrentIcon()}>
95
+ <Popup>
96
+ <span className="font-semibold">{currentOk.label ?? 'Current position'}</span>
97
+ </Popup>
98
+ </Marker>
99
+ )}
100
+ </MapCanvas>
101
+ );
102
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Map primitives (react-leaflet + OpenStreetMap tiles) — brand-styled markers,
3
+ * mount-safe, responsive, honest empty states. Coordinates at null-island (0,0)
4
+ * or non-finite values are always filtered out.
5
+ */
6
+ export { LogisticsMap, type LogisticsMarker, type LogisticsRoute } from './LogisticsMap';
7
+ export { RouteMap, type RouteStop } from './RouteMap';
8
+ export { TrackingMap, type TrackPoint, type TrackEndpoint } from './TrackingMap';
9
+ export { MapCanvas } from './MapCanvas';
10
+ export {
11
+ createPinIcon,
12
+ createStopIcon,
13
+ createCurrentIcon,
14
+ isRealCoord,
15
+ type MapMarkerStatus,
16
+ } from './leaflet-setup';
@@ -0,0 +1,124 @@
1
+ import L from 'leaflet';
2
+ // Leaflet's stylesheet — imported once here so every map component inherits it.
3
+ import 'leaflet/dist/leaflet.css';
4
+ // Brand-styled marker CSS (pins, numbered stops, live dot).
5
+ import './map-markers.css';
6
+
7
+ /**
8
+ * OpenStreetMap raster tiles. There is no CSP blocking external tiles in the
9
+ * app-shell, so we use the standard OSM tile server.
10
+ */
11
+ export const OSM_TILE_URL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
12
+ export const OSM_ATTRIBUTION =
13
+ '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
14
+
15
+ export type MapMarkerStatus =
16
+ | 'active'
17
+ | 'in_transit'
18
+ | 'idle'
19
+ | 'delayed'
20
+ | 'delivered'
21
+ | 'origin'
22
+ | 'destination'
23
+ | 'default';
24
+
25
+ /**
26
+ * A coordinate is "real" only when it is finite and not the null-island (0,0)
27
+ * sentinel that backends emit for "unknown location". We never plot those.
28
+ */
29
+ export function isRealCoord(lat: unknown, lng: unknown): lat is number {
30
+ return (
31
+ typeof lat === 'number' &&
32
+ typeof lng === 'number' &&
33
+ Number.isFinite(lat) &&
34
+ Number.isFinite(lng) &&
35
+ !(lat === 0 && lng === 0) &&
36
+ Math.abs(lat) <= 90 &&
37
+ Math.abs(lng) <= 180
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Map a domain status to a fe-libs design token color (resolved via CSS var, so
43
+ * it stays theme-aware). No raw palette colors.
44
+ */
45
+ function statusColorToken(status: MapMarkerStatus): string {
46
+ switch (status) {
47
+ case 'delayed':
48
+ return 'var(--color-status-error-text)';
49
+ case 'idle':
50
+ return 'var(--color-status-warning-text)';
51
+ case 'delivered':
52
+ case 'destination':
53
+ return 'var(--color-status-success-text)';
54
+ case 'in_transit':
55
+ case 'active':
56
+ case 'origin':
57
+ default:
58
+ return 'var(--color-brand-primary)';
59
+ }
60
+ }
61
+
62
+ const TRUCK_GLYPH = `
63
+ <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="white"
64
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
65
+ <path d="M10 17h4V5H2v12h3"/><path d="M20 17h2v-3.34a4 4 0 0 0-1.17-2.83L19 9h-5v8h1"/>
66
+ <circle cx="7.5" cy="17.5" r="1.5"/><circle cx="17.5" cy="17.5" r="1.5"/>
67
+ </svg>`;
68
+
69
+ /**
70
+ * A brand-styled teardrop pin (orange by default) with a white truck glyph.
71
+ * Built as an HTML `divIcon` because Leaflet's default marker PNGs break under
72
+ * bundlers.
73
+ */
74
+ export function createPinIcon(
75
+ status: MapMarkerStatus = 'default',
76
+ options?: { pulse?: boolean; glyph?: boolean }
77
+ ): L.DivIcon {
78
+ const color = statusColorToken(status);
79
+ const pulse = options?.pulse ?? (status === 'in_transit' || status === 'active');
80
+ const glyph = options?.glyph ?? true;
81
+
82
+ const html = `
83
+ <div class="mtw-pin${pulse ? ' mtw-pin--pulse' : ''}" style="--mtw-pin-color:${color}">
84
+ <span class="mtw-pin__body">${glyph ? TRUCK_GLYPH : ''}</span>
85
+ </div>`;
86
+
87
+ return L.divIcon({
88
+ html,
89
+ className: 'mtw-pin-wrapper',
90
+ iconSize: [30, 38],
91
+ iconAnchor: [15, 36],
92
+ popupAnchor: [0, -32],
93
+ });
94
+ }
95
+
96
+ /**
97
+ * A small numbered circle marker for ordered route stops.
98
+ */
99
+ export function createStopIcon(sequence: number, color = 'var(--color-brand-primary)'): L.DivIcon {
100
+ const html = `
101
+ <div class="mtw-stop" style="--mtw-pin-color:${color}">
102
+ <span class="mtw-stop__num">${sequence}</span>
103
+ </div>`;
104
+ return L.divIcon({
105
+ html,
106
+ className: 'mtw-pin-wrapper',
107
+ iconSize: [28, 28],
108
+ iconAnchor: [14, 14],
109
+ popupAnchor: [0, -14],
110
+ });
111
+ }
112
+
113
+ /**
114
+ * A pulsing dot for the shipment's current/live position.
115
+ */
116
+ export function createCurrentIcon(color = 'var(--color-brand-primary)'): L.DivIcon {
117
+ const html = `<div class="mtw-current" style="--mtw-pin-color:${color}"><span></span></div>`;
118
+ return L.divIcon({
119
+ html,
120
+ className: 'mtw-pin-wrapper',
121
+ iconSize: [20, 20],
122
+ iconAnchor: [10, 10],
123
+ });
124
+ }
@@ -0,0 +1,121 @@
1
+ /* Brand-styled Leaflet markers for MoveTheWheels.
2
+ * Colors come from --mtw-pin-color, which each divIcon sets from a design token
3
+ * (var(--color-*)), so markers stay theme-aware with no raw palette values. */
4
+
5
+ .mtw-pin-wrapper {
6
+ background: transparent;
7
+ border: 0;
8
+ }
9
+
10
+ .mtw-pin {
11
+ position: relative;
12
+ width: 30px;
13
+ height: 38px;
14
+ filter: drop-shadow(0 4px 6px rgb(0 0 0 / 0.28));
15
+ }
16
+
17
+ .mtw-pin__body {
18
+ position: absolute;
19
+ inset: 0;
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ padding-bottom: 8px;
24
+ background: var(--mtw-pin-color, #f97316);
25
+ border: 2px solid rgb(255 255 255 / 0.9);
26
+ border-radius: 50% 50% 50% 0;
27
+ transform: rotate(-45deg);
28
+ box-sizing: border-box;
29
+ }
30
+
31
+ .mtw-pin__body > svg {
32
+ transform: rotate(45deg);
33
+ }
34
+
35
+ .mtw-pin--pulse::before {
36
+ content: '';
37
+ position: absolute;
38
+ left: 50%;
39
+ top: 13px;
40
+ width: 30px;
41
+ height: 30px;
42
+ transform: translate(-50%, -50%);
43
+ border-radius: 50%;
44
+ background: var(--mtw-pin-color, #f97316);
45
+ opacity: 0.45;
46
+ animation: mtw-pin-pulse 1.8s ease-out infinite;
47
+ }
48
+
49
+ @keyframes mtw-pin-pulse {
50
+ 0% {
51
+ transform: translate(-50%, -50%) scale(0.6);
52
+ opacity: 0.5;
53
+ }
54
+ 100% {
55
+ transform: translate(-50%, -50%) scale(2.4);
56
+ opacity: 0;
57
+ }
58
+ }
59
+
60
+ .mtw-stop {
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ width: 28px;
65
+ height: 28px;
66
+ border-radius: 9999px;
67
+ background: var(--mtw-pin-color, #f97316);
68
+ border: 2px solid rgb(255 255 255 / 0.95);
69
+ box-shadow: 0 2px 6px rgb(0 0 0 / 0.28);
70
+ }
71
+
72
+ .mtw-stop__num {
73
+ color: #fff;
74
+ font-size: 12px;
75
+ font-weight: 700;
76
+ line-height: 1;
77
+ }
78
+
79
+ .mtw-current {
80
+ position: relative;
81
+ width: 20px;
82
+ height: 20px;
83
+ }
84
+
85
+ .mtw-current > span {
86
+ position: absolute;
87
+ inset: 4px;
88
+ border-radius: 9999px;
89
+ background: var(--mtw-pin-color, #f97316);
90
+ border: 2px solid rgb(255 255 255 / 0.95);
91
+ box-shadow: 0 2px 6px rgb(0 0 0 / 0.3);
92
+ }
93
+
94
+ .mtw-current::after {
95
+ content: '';
96
+ position: absolute;
97
+ inset: 0;
98
+ border-radius: 9999px;
99
+ background: var(--mtw-pin-color, #f97316);
100
+ opacity: 0.4;
101
+ animation: mtw-pin-pulse 1.8s ease-out infinite;
102
+ }
103
+
104
+ @media (prefers-reduced-motion: reduce) {
105
+ .mtw-pin--pulse::before,
106
+ .mtw-current::after {
107
+ animation: none;
108
+ }
109
+ }
110
+
111
+ /* Keep Leaflet controls/attribution visually consistent with the app surface. */
112
+ .leaflet-container {
113
+ font-family: inherit;
114
+ background: var(--color-bg-sunken, #eef1f5);
115
+ }
116
+
117
+ .leaflet-container .leaflet-control-attribution {
118
+ background: rgb(255 255 255 / 0.75);
119
+ color: #4b5563;
120
+ font-size: 10px;
121
+ }
@@ -165,6 +165,7 @@ export function useAnalytics(_options: UseAnalyticsOptions = {}): UseAnalyticsRe
165
165
  const data = await executeGraphQL<{
166
166
  analytics: {
167
167
  orderTrend: Array<{ label: string; value: number }>;
168
+ deliveryTrend: Array<{ label: string; value: number }>;
168
169
  revenueTrend: Array<{ label: string; value: number }>;
169
170
  };
170
171
  dashboardMetrics: {
@@ -191,10 +192,19 @@ export function useAnalytics(_options: UseAnalyticsOptions = {}): UseAnalyticsRe
191
192
  customerSatisfaction: data.dashboardMetrics.customerSatisfaction,
192
193
  },
193
194
  trends: {
194
- dailyOrders: data.analytics.orderTrend.map((item) => ({
195
- date: item.label,
196
- orders: Math.round(item.value),
197
- })),
195
+ dailyOrders: (() => {
196
+ const deliveriesByLabel = new Map(
197
+ (data.analytics.deliveryTrend ?? []).map((item) => [
198
+ item.label,
199
+ Math.round(item.value),
200
+ ])
201
+ );
202
+ return data.analytics.orderTrend.map((item) => ({
203
+ date: item.label,
204
+ orders: Math.round(item.value),
205
+ deliveries: deliveriesByLabel.get(item.label) ?? 0,
206
+ }));
207
+ })(),
198
208
  revenue: data.analytics.revenueTrend.map((item) => ({
199
209
  month: item.label,
200
210
  revenue: item.value,
@@ -1,20 +1,49 @@
1
1
  /**
2
2
  * Analytics Page
3
+ *
4
+ * Real charts (recharts) fed entirely from Query.analytics / orderStats /
5
+ * fleetStats. Metrics we cannot source (e.g. per-lane volume) are simply not
6
+ * shown rather than faked.
3
7
  */
4
8
 
5
- import { BarChart3, TrendingUp, Package, Truck, DollarSign, Users } from 'lucide-react';
9
+ import { useMemo } from 'react';
10
+ import { BarChart3, TrendingUp, Package, DollarSign, Users, Star } from 'lucide-react';
6
11
  import { PageLayout } from '../components/PageLayout';
7
12
  import { MetricCard, MetricGrid, MetricCardSkeleton } from '../components/MetricCard';
8
13
  import { Card, CardContent, CardHeader, CardTitle, Progress, Button } from '../components/ui';
14
+ import { TrendChart, VolumeBar, StatusDonut, type DonutSlice } from '../components/charts';
9
15
  import { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
10
16
  import { useAnalytics, useOrderAnalytics, useFleetAnalytics } from '../hooks/useAnalytics';
11
17
  import { formatCurrency } from '../utils/formatters';
12
18
 
19
+ const statusLabel = (status: string) =>
20
+ status.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
21
+
13
22
  export default function AnalyticsPage() {
14
23
  const { analytics, isLoading, error, refetch } = useAnalytics();
15
24
  const { orderData } = useOrderAnalytics();
16
25
  const { fleetData } = useFleetAnalytics();
17
26
 
27
+ const orderSpark = useMemo(
28
+ () => analytics.trends.dailyOrders.map((d) => d.orders),
29
+ [analytics.trends.dailyOrders]
30
+ );
31
+
32
+ const revenueData = useMemo(
33
+ () => analytics.trends.revenue.map((r) => ({ month: r.month, revenue: r.revenue })),
34
+ [analytics.trends.revenue]
35
+ );
36
+
37
+ const statusSlices = useMemo<DonutSlice[]>(
38
+ () =>
39
+ orderData.byStatus
40
+ .filter((s) => s.count > 0)
41
+ .map((s) => ({ name: statusLabel(s.status), value: s.count })),
42
+ [orderData.byStatus]
43
+ );
44
+
45
+ const statusTotal = statusSlices.reduce((sum, s) => sum + s.value, 0);
46
+
18
47
  return (
19
48
  <PageLayout
20
49
  title="Analytics"
@@ -38,17 +67,19 @@ export default function AnalyticsPage() {
38
67
  </CardContent>
39
68
  </Card>
40
69
  ) : isLoading ? (
41
- <MetricGrid columns={5}>
42
- {Array.from({ length: 5 }).map((_, i) => (
70
+ <MetricGrid columns={4}>
71
+ {Array.from({ length: 4 }).map((_, i) => (
43
72
  <MetricCardSkeleton key={i} />
44
73
  ))}
45
74
  </MetricGrid>
46
75
  ) : (
47
- <MetricGrid columns={5}>
76
+ <MetricGrid columns={4}>
48
77
  <MetricCard
49
- label="Total Orders"
78
+ label="Order Volume"
50
79
  value={analytics.overview.totalOrders.toLocaleString()}
51
80
  icon={<Package size={20} />}
81
+ accent="brand"
82
+ sparklineData={orderSpark.length >= 2 ? orderSpark : undefined}
52
83
  />
53
84
  <MetricCard
54
85
  treatment="glass"
@@ -56,99 +87,83 @@ export default function AnalyticsPage() {
56
87
  label="Revenue"
57
88
  value={formatCurrency(analytics.overview.totalRevenue)}
58
89
  icon={<DollarSign size={20} />}
90
+ accent="success"
59
91
  />
60
92
  <MetricCard
61
- label="Active Vehicles"
62
- value={analytics.overview.activeVehicles}
63
- icon={<Truck size={20} />}
64
- />
65
- <MetricCard
66
- label="On-Time Rate"
93
+ label="On-Time Delivery"
67
94
  value={`${analytics.overview.onTimeDeliveryRate.toFixed(1)}%`}
68
95
  icon={<TrendingUp size={20} />}
69
- changeType={analytics.overview.onTimeDeliveryRate >= 90 ? 'positive' : 'neutral'}
96
+ accent="blue"
97
+ delta={
98
+ analytics.overview.onTimeDeliveryRate >= 90
99
+ ? { value: 'On target', direction: 'up' }
100
+ : { value: 'Below 90%', direction: 'down' }
101
+ }
70
102
  />
71
103
  <MetricCard
72
- label="Satisfaction"
104
+ label="Customer Satisfaction"
73
105
  value={analytics.overview.customerSatisfaction.toFixed(1)}
74
- icon={<Users size={20} />}
106
+ icon={<Star size={20} />}
107
+ accent="warning"
75
108
  subtitle="out of 5"
76
109
  />
77
110
  </MetricGrid>
78
111
  )}
79
112
 
113
+ {/* Order & delivery trend */}
114
+ <Card>
115
+ <CardHeader>
116
+ <CardTitle>Order &amp; delivery trend</CardTitle>
117
+ </CardHeader>
118
+ <CardContent>
119
+ <TrendChart
120
+ xKey="date"
121
+ height={300}
122
+ data={analytics.trends.dailyOrders}
123
+ series={[
124
+ { dataKey: 'orders', name: 'Orders' },
125
+ { dataKey: 'deliveries', name: 'Deliveries' },
126
+ ]}
127
+ emptyMessage="No order trend recorded yet"
128
+ />
129
+ </CardContent>
130
+ </Card>
131
+
80
132
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
81
133
  <Card>
82
134
  <CardHeader>
83
- <CardTitle>Order Trends</CardTitle>
135
+ <CardTitle>Revenue by month</CardTitle>
84
136
  </CardHeader>
85
137
  <CardContent>
86
- <div className="h-64 flex items-center justify-center bg-bg-sunken/50 rounded-lg">
87
- <div className="text-center text-text-secondary">
88
- <BarChart3 size={48} className="mx-auto mb-2 opacity-50" />
89
- <p>Order trend chart</p>
90
- <p className="text-xs">(Chart library integration needed)</p>
91
- </div>
92
- </div>
93
- <div className="mt-4 grid grid-cols-2 sm:grid-cols-4 gap-2">
94
- {orderData.trends.slice(-4).map((t) => (
95
- <div key={t.date} className="text-center p-2 bg-bg-sunken/50 rounded">
96
- <p className="text-xs text-text-secondary">
97
- {t.date.split('-').slice(1).join('/')}
98
- </p>
99
- <p className="font-medium">{t.orders}</p>
100
- </div>
101
- ))}
102
- </div>
138
+ <VolumeBar
139
+ xKey="month"
140
+ height={280}
141
+ showLegend={false}
142
+ data={revenueData}
143
+ series={[{ dataKey: 'revenue', name: 'Revenue' }]}
144
+ valueFormatter={(v) => formatCurrency(Number(v))}
145
+ emptyMessage="No revenue recorded yet"
146
+ />
103
147
  </CardContent>
104
148
  </Card>
105
149
 
106
150
  <Card>
107
151
  <CardHeader>
108
- <CardTitle>Revenue Trends</CardTitle>
152
+ <CardTitle>Orders by status</CardTitle>
109
153
  </CardHeader>
110
154
  <CardContent>
111
- <div className="h-64 flex items-center justify-center bg-bg-sunken/50 rounded-lg">
112
- <div className="text-center text-text-secondary">
113
- <TrendingUp size={48} className="mx-auto mb-2 opacity-50" />
114
- <p>Revenue trend chart</p>
115
- </div>
116
- </div>
117
- <div className="mt-4 grid grid-cols-2 sm:grid-cols-5 gap-2">
118
- {analytics.trends.revenue.map((r) => (
119
- <div key={r.month} className="text-center p-2 bg-bg-sunken/50 rounded">
120
- <p className="text-xs text-text-secondary">{r.month}</p>
121
- <p className="font-medium text-sm">{formatCurrency(r.revenue / 1000)}k</p>
122
- </div>
123
- ))}
124
- </div>
155
+ <StatusDonut
156
+ data={statusSlices}
157
+ height={240}
158
+ centerLabel={statusTotal.toLocaleString()}
159
+ centerSubLabel="orders"
160
+ emptyMessage="No orders to break down yet"
161
+ />
125
162
  </CardContent>
126
163
  </Card>
127
164
  </div>
128
165
 
129
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
130
- <Card>
131
- <CardHeader>
132
- <CardTitle>Orders by Status</CardTitle>
133
- </CardHeader>
134
- <CardContent className="space-y-3">
135
- {orderData.byStatus
136
- .filter((s) => s.count > 0)
137
- .map(({ status, count }) => (
138
- <div key={status} className="flex items-center justify-between">
139
- <span className="text-sm capitalize">{status.replace('_', ' ')}</span>
140
- <div className="flex items-center gap-2">
141
- <Progress
142
- value={(count / analytics.overview.totalOrders) * 100}
143
- className="w-20 h-2"
144
- />
145
- <span className="font-medium w-8 text-right">{count}</span>
146
- </div>
147
- </div>
148
- ))}
149
- </CardContent>
150
- </Card>
151
-
166
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
152
167
  <Card>
153
168
  <CardHeader>
154
169
  <CardTitle>Fleet Performance</CardTitle>
@@ -188,9 +203,14 @@ export default function AnalyticsPage() {
188
203
  {analytics.overview.customerSatisfaction.toFixed(1)}/5
189
204
  </p>
190
205
  </div>
191
- <div>
192
- <p className="text-sm text-text-secondary">Driver Ratings</p>
193
- <p className="text-2xl font-bold">{analytics.performance.driverRatings.toFixed(1)}/5</p>
206
+ <div className="flex items-center gap-2">
207
+ <Users size={16} className="text-text-secondary" />
208
+ <div>
209
+ <p className="text-sm text-text-secondary">Driver Ratings</p>
210
+ <p className="text-2xl font-bold">
211
+ {analytics.performance.driverRatings.toFixed(1)}/5
212
+ </p>
213
+ </div>
194
214
  </div>
195
215
  </EmphasisPanel>
196
216
  </div>
@@ -313,7 +313,7 @@ export default function CreateOrderPage() {
313
313
  placeholder="123 Main St"
314
314
  />
315
315
  </div>
316
- <div className="grid grid-cols-2 gap-4">
316
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
317
317
  <div className="space-y-2">
318
318
  <Label htmlFor="pickupCity">City</Label>
319
319
  <Input
@@ -333,7 +333,7 @@ export default function CreateOrderPage() {
333
333
  />
334
334
  </div>
335
335
  </div>
336
- <div className="grid grid-cols-2 gap-4">
336
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
337
337
  <div className="space-y-2">
338
338
  <Label htmlFor="pickupPostal">Postal Code</Label>
339
339
  <Input
@@ -371,7 +371,7 @@ export default function CreateOrderPage() {
371
371
  placeholder="456 Oak Ave"
372
372
  />
373
373
  </div>
374
- <div className="grid grid-cols-2 gap-4">
374
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
375
375
  <div className="space-y-2">
376
376
  <Label htmlFor="deliveryCity">City</Label>
377
377
  <Input
@@ -391,7 +391,7 @@ export default function CreateOrderPage() {
391
391
  />
392
392
  </div>
393
393
  </div>
394
- <div className="grid grid-cols-2 gap-4">
394
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
395
395
  <div className="space-y-2">
396
396
  <Label htmlFor="deliveryPostal">Postal Code</Label>
397
397
  <Input