@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
@@ -0,0 +1,118 @@
1
+ import { useEffect, useState, type ReactNode } from 'react';
2
+ import L from 'leaflet';
3
+ import { MapContainer, TileLayer, useMap } from 'react-leaflet';
4
+ import { cn } from '../../utils/cn';
5
+ import { OSM_ATTRIBUTION, OSM_TILE_URL } from './leaflet-setup';
6
+
7
+ /** SSR/lazy safety: only mount Leaflet (which touches the DOM) after mount. */
8
+ export function useIsMounted(): boolean {
9
+ const [mounted, setMounted] = useState(false);
10
+ useEffect(() => setMounted(true), []);
11
+ return mounted;
12
+ }
13
+
14
+ /**
15
+ * Imperatively fit the map to a set of [lat,lng] points whenever they change.
16
+ * Invalidates size on mount so tiles render correctly inside flex/grid parents.
17
+ */
18
+ export function FitBounds({
19
+ points,
20
+ padding = 48,
21
+ maxZoom = 13,
22
+ }: {
23
+ points: Array<[number, number]>;
24
+ padding?: number;
25
+ maxZoom?: number;
26
+ }) {
27
+ const map = useMap();
28
+
29
+ useEffect(() => {
30
+ const t = setTimeout(() => map.invalidateSize(), 0);
31
+ return () => clearTimeout(t);
32
+ }, [map]);
33
+
34
+ useEffect(() => {
35
+ if (points.length === 0) return;
36
+ if (points.length === 1) {
37
+ map.setView(points[0], Math.min(maxZoom, 12));
38
+ return;
39
+ }
40
+ const bounds = L.latLngBounds(points);
41
+ map.fitBounds(bounds, { padding: [padding, padding], maxZoom });
42
+ }, [map, points, padding, maxZoom]);
43
+
44
+ return null;
45
+ }
46
+
47
+ interface MapCanvasProps {
48
+ points: Array<[number, number]>;
49
+ height?: number | string;
50
+ className?: string;
51
+ fitToMarkers?: boolean;
52
+ /** Fallback center when there are no points but we still render the canvas. */
53
+ center?: [number, number];
54
+ zoom?: number;
55
+ children?: ReactNode;
56
+ /** Rendered instead of the map when there are no points. */
57
+ emptyState?: ReactNode;
58
+ }
59
+
60
+ /**
61
+ * MapCanvas — the shared OSM MapContainer used by every map component. Handles
62
+ * mount-safety, container sizing, rounded/overflow styling, tile layer, and
63
+ * auto-fit. Renders `emptyState` when there is nothing to plot.
64
+ */
65
+ export function MapCanvas({
66
+ points,
67
+ height = 420,
68
+ className,
69
+ fitToMarkers = true,
70
+ center,
71
+ zoom = 5,
72
+ children,
73
+ emptyState,
74
+ }: MapCanvasProps) {
75
+ const mounted = useIsMounted();
76
+
77
+ const wrapperClass = cn(
78
+ 'relative overflow-hidden rounded-2xl border border-border-seam bg-bg-sunken',
79
+ className
80
+ );
81
+ const style = { height, width: '100%' };
82
+
83
+ if (points.length === 0 && emptyState) {
84
+ return (
85
+ <div className={wrapperClass} style={style}>
86
+ <div className="flex h-full w-full items-center justify-center p-6 text-center">
87
+ {emptyState}
88
+ </div>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ if (!mounted) {
94
+ return (
95
+ <div className={wrapperClass} style={style} aria-busy="true">
96
+ <div className="h-full w-full motion-safe:animate-vc-shimmer bg-bg-sunken" />
97
+ </div>
98
+ );
99
+ }
100
+
101
+ const initialCenter: [number, number] = center ?? points[0] ?? [20, 0];
102
+
103
+ return (
104
+ <div className={wrapperClass} style={style}>
105
+ <MapContainer
106
+ center={initialCenter}
107
+ zoom={points.length > 0 ? zoom : 2}
108
+ scrollWheelZoom={false}
109
+ style={{ height: '100%', width: '100%' }}
110
+ className="z-0"
111
+ >
112
+ <TileLayer url={OSM_TILE_URL} attribution={OSM_ATTRIBUTION} />
113
+ {fitToMarkers && <FitBounds points={points} />}
114
+ {children}
115
+ </MapContainer>
116
+ </div>
117
+ );
118
+ }
@@ -0,0 +1,71 @@
1
+ import { useMemo } from 'react';
2
+ import { Marker, Polyline, Popup } from 'react-leaflet';
3
+ import { MapCanvas } from './MapCanvas';
4
+ import { createStopIcon, isRealCoord } from './leaflet-setup';
5
+
6
+ export interface RouteStop {
7
+ lat: number;
8
+ lng: number;
9
+ label: string;
10
+ sequence: number;
11
+ }
12
+
13
+ interface RouteMapProps {
14
+ stops: RouteStop[];
15
+ height?: number | string;
16
+ className?: string;
17
+ /** Polyline color (design-token string). Defaults to brand blue. */
18
+ routeColor?: string;
19
+ emptyMessage?: string;
20
+ }
21
+
22
+ /**
23
+ * RouteMap — numbered stop markers in sequence connected by a polyline. Filters
24
+ * out stops without a real coordinate and orders the line by `sequence`.
25
+ */
26
+ export function RouteMap({
27
+ stops,
28
+ height = 420,
29
+ className,
30
+ routeColor = 'var(--color-chart-1)',
31
+ emptyMessage = 'No stops with coordinates yet',
32
+ }: RouteMapProps) {
33
+ const ordered = useMemo(
34
+ () => [...stops].filter((s) => isRealCoord(s.lat, s.lng)).sort((a, b) => a.sequence - b.sequence),
35
+ [stops]
36
+ );
37
+
38
+ const line = useMemo<Array<[number, number]>>(
39
+ () => ordered.map((s) => [s.lat, s.lng]),
40
+ [ordered]
41
+ );
42
+
43
+ return (
44
+ <MapCanvas
45
+ points={line}
46
+ height={height}
47
+ className={className}
48
+ emptyState={<p className="text-sm text-text-secondary">{emptyMessage}</p>}
49
+ >
50
+ {line.length >= 2 && (
51
+ <Polyline
52
+ positions={line}
53
+ pathOptions={{ color: routeColor, weight: 4, opacity: 0.85 }}
54
+ />
55
+ )}
56
+ {ordered.map((s) => (
57
+ <Marker
58
+ key={`${s.sequence}-${s.lat}-${s.lng}`}
59
+ position={[s.lat, s.lng]}
60
+ icon={createStopIcon(s.sequence)}
61
+ >
62
+ <Popup>
63
+ <span className="font-semibold">
64
+ {s.sequence}. {s.label}
65
+ </span>
66
+ </Popup>
67
+ </Marker>
68
+ ))}
69
+ </MapCanvas>
70
+ );
71
+ }
@@ -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
+ }
@@ -41,6 +41,8 @@ export {
41
41
  useOrderAnalytics,
42
42
  useFleetAnalytics,
43
43
  usePerformanceAnalytics,
44
+ useFinanceMetrics,
45
+ type FinanceMetrics,
44
46
  } from './useAnalytics';
45
47
 
46
48
  // Tracking
@@ -65,3 +67,10 @@ export { useImportExport } from './useImportExport';
65
67
 
66
68
  // Module settings
67
69
  export { useModuleSettings } from './useSettings';
70
+
71
+ // Entity documents (reuse of wspace-files-svc)
72
+ export {
73
+ useEntityDocuments,
74
+ type EntityDocument,
75
+ type EntityDocumentType,
76
+ } from './useEntityDocuments';
@@ -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,
@@ -469,6 +479,101 @@ export function useFleetAnalytics() {
469
479
  return { fleetData, isLoading, error };
470
480
  }
471
481
 
482
+ const FINANCE_METRICS_QUERY = `
483
+ query GetMovethewheelsFinanceMetrics {
484
+ financeMetrics {
485
+ revenue {
486
+ totalRevenue
487
+ deliveredRevenue
488
+ averageOrderValue
489
+ }
490
+ fuelCost
491
+ openInvoices
492
+ }
493
+ }
494
+ `;
495
+
496
+ export interface FinanceMetrics {
497
+ totalRevenue: number;
498
+ deliveredRevenue: number;
499
+ averageOrderValue: number;
500
+ fuelCost: number;
501
+ openInvoices: number;
502
+ }
503
+
504
+ /**
505
+ * Real finance figures straight from Query.financeMetrics — no simulated splits.
506
+ * `deliveredRevenue` is money already collected on delivered orders; `openInvoices`
507
+ * is the count of orders still owing. Both come from the service, never inferred.
508
+ */
509
+ export function useFinanceMetrics() {
510
+ const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
511
+ useMoveTheWheels();
512
+ const [metrics, setMetrics] = useState<FinanceMetrics>({
513
+ totalRevenue: 0,
514
+ deliveredRevenue: 0,
515
+ averageOrderValue: 0,
516
+ fuelCost: 0,
517
+ openInvoices: 0,
518
+ });
519
+ const [isLoading, setIsLoading] = useState(false);
520
+ const [error, setError] = useState<Error | null>(null);
521
+
522
+ const graphQLOptions = useMemo(
523
+ () => ({
524
+ apiGatewayUrl,
525
+ authToken,
526
+ workspaceToken,
527
+ workspaceId,
528
+ tenantId,
529
+ actorId: currentUser?.id,
530
+ }),
531
+ [apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
532
+ );
533
+
534
+ const load = useCallback(async () => {
535
+ setIsLoading(true);
536
+ setError(null);
537
+ try {
538
+ const data = await executeGraphQL<{
539
+ financeMetrics: {
540
+ revenue: {
541
+ totalRevenue: number;
542
+ deliveredRevenue: number;
543
+ averageOrderValue: number;
544
+ };
545
+ fuelCost: number;
546
+ openInvoices: number;
547
+ };
548
+ }>(graphQLOptions, FINANCE_METRICS_QUERY);
549
+
550
+ setMetrics({
551
+ totalRevenue: data.financeMetrics.revenue.totalRevenue,
552
+ deliveredRevenue: data.financeMetrics.revenue.deliveredRevenue,
553
+ averageOrderValue: data.financeMetrics.revenue.averageOrderValue,
554
+ fuelCost: data.financeMetrics.fuelCost,
555
+ openInvoices: data.financeMetrics.openInvoices,
556
+ });
557
+ } catch (fetchError) {
558
+ setError(
559
+ fetchError instanceof Error ? fetchError : new Error('Failed to load finance metrics')
560
+ );
561
+ } finally {
562
+ setIsLoading(false);
563
+ }
564
+ }, [graphQLOptions]);
565
+
566
+ useEffect(() => {
567
+ void load();
568
+ }, [load]);
569
+
570
+ const refetch = useCallback(() => {
571
+ void load();
572
+ }, [load]);
573
+
574
+ return { metrics, isLoading, error, refetch };
575
+ }
576
+
472
577
  export function usePerformanceAnalytics() {
473
578
  const { analytics, isLoading, error } = useAnalytics();
474
579