@burdenoff/microfe-movethewheels 2026.703.4 → 2026.703.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AIAssistantPage-3YOnHO9Y.js → AIAssistantPage-BmILdHgO.js} +16 -15
- package/dist/AnalyticsPage-BSlsiSDE.js +194 -0
- package/dist/{CreateOrderPage-DRAgGo9i.js → CreateOrderPage-DaCgVaO3.js} +40 -40
- package/dist/{CustomerDetailsPage-BCeMmOFe.js → CustomerDetailsPage-DErpbafW.js} +71 -71
- package/dist/CustomersPage-C1lRU6xs.js +205 -0
- package/dist/DashboardPage-Q5-5EKAU.js +542 -0
- package/dist/{DataTable-XvUcTjwi.js → DataTable-KjzbwnRL.js} +31 -31
- package/dist/{DocumentsCard-U2lSe-cn.js → DocumentsCard-T9y6cv4-.js} +25 -25
- package/dist/{DriverDetailsPage-BQrYd_Lp.js → DriverDetailsPage-DnHGkDC7.js} +64 -64
- package/dist/DriversPage-C0ujWVOn.js +221 -0
- package/dist/FinancePage-CA2XEgsQ.js +175 -0
- package/dist/{FleetPage-3ZHsB-TN.js → FleetPage-DrvXW7wK.js} +22 -22
- package/dist/{FormError-cuLV2jVD.js → FormError-CRsMP7TQ.js} +1 -1
- package/dist/{ImportExportPage-C6f7DqhM.js → ImportExportPage-Cac9HjFx.js} +12 -12
- package/dist/InventoryPage-Ezf4gqKI.js +259 -0
- package/dist/LiveTrackingPage-CzbVwDqK.js +429 -0
- package/dist/{MarketplacePage-Bh_2ns4v.js → MarketplacePage-BHDmSQmo.js} +116 -116
- package/dist/MetricCard-DQlDNqmK.js +191 -0
- package/dist/{Notice-CSoDYKw9.js → Notice-GmA7JDR3.js} +2 -2
- package/dist/{OrderDetailsPage-zCj0lW0j.js → OrderDetailsPage-CR0Wc1aN.js} +58 -58
- package/dist/OrdersListPage-C8Bu6wXM.js +386 -0
- package/dist/{PageLayout-D4LDHJJq.js → PageLayout-DX1Ou6V6.js} +164 -134
- package/dist/{ProductDetailsPage-wsxAQkA0.js → ProductDetailsPage-B_ns2MOe.js} +55 -55
- package/dist/ProductsPage-c92BsEoA.js +186 -0
- package/dist/ReportsPage-CPLXtLT2.js +377 -0
- package/dist/{RouteDetailsPage-DyMJE9AS.js → RouteDetailsPage-DOfcRiFv.js} +208 -181
- package/dist/{RoutesPage-Dtc669yc.js → RoutesPage-BZGublby.js} +12 -12
- package/dist/{SettingsPage-CpH9xhQg.js → SettingsPage-5cweX_xh.js} +15 -15
- package/dist/Sparkline-zSXvG6vA.js +16729 -0
- package/dist/{StatusBadge-C87mDpDP.js → StatusBadge-oZjownr1.js} +7 -7
- package/dist/TrackingPage-Ln9qVflK.js +368 -0
- package/dist/{VehicleDetailsPage-DldYNsYV.js → VehicleDetailsPage-CrkzE0dN.js} +53 -53
- package/dist/VehiclesPage-CTQWH7af.js +184 -0
- package/dist/{WarehouseDetailsPage-CMG7or6G.js → WarehouseDetailsPage-CwDLaG65.js} +64 -64
- package/dist/WarehousesPage-KW02DvZg.js +182 -0
- package/dist/{arrow-left-n48Y-Wis.js → arrow-left-DJDw_Yqv.js} +1 -1
- package/dist/{arrow-right-Q3XQpTPR.js → arrow-right-Cb2fLTPJ.js} +1 -1
- package/dist/{arrow-up-xNF1zJUG.js → arrow-up-PxaOIyiT.js} +1 -1
- package/dist/{box-Eu05PXDh.js → box-hSJVtvGf.js} +1 -1
- package/dist/boxes-DDnxEkT5.js +54 -0
- package/dist/{chart-column-ouzgxIv_.js → chart-column-xXgAZvRl.js} +1 -1
- package/dist/charts-CfCwQg6o.js +2018 -0
- package/dist/{chevron-left-B2_r0Elx.js → chevron-left-dhEGJYdh.js} +1 -1
- package/dist/{chevron-right-eV4LjaSi.js → chevron-right-Cud2NDvm.js} +1 -1
- package/dist/chunk-BdsCSvBP.js +20 -0
- package/dist/{circle-alert-Bzh2DBSF.js → circle-alert-Rx0p3D5w.js} +1 -1
- package/dist/circle-check-BCxRFdFo.js +13 -0
- package/dist/{circle-check-big-CGMSpQmD.js → circle-check-big-BcIJ8xjx.js} +1 -1
- package/dist/{clock-Bl3ptexL.js → clock-B2_YxJgB.js} +1 -1
- package/dist/{dollar-sign-CAVU95SO.js → dollar-sign-CQmAICv4.js} +1 -1
- package/dist/{download-C5NbvVDc.js → download-CG4Tponq.js} +1 -1
- package/dist/{file-text-D-1uvLZG.js → file-text-CzH-zMub.js} +1 -1
- package/dist/{filter-DUfUg6BO.js → filter-C0u2Lwfn.js} +1 -1
- package/dist/{fuel-B6_lnKDs.js → fuel-C2KELcM-.js} +1 -1
- package/dist/index.css +2 -0
- package/dist/index.js +214 -225
- package/dist/{map-C-M-FQLR.js → map-FuPu5aTw.js} +1 -1
- package/dist/{map-pin-Dznl5l40.js → map-pin-czpLv9uy.js} +1 -1
- package/dist/mapMarkers-NCV9MRKR.js +28 -0
- package/dist/maps-D9m7PKrs.js +5070 -0
- package/dist/{navigation-BJSJl-VG.js → navigation-D5_pdCSS.js} +1 -1
- package/dist/{package-CcRttV2N.js → package-B0sdi6lU.js} +1 -1
- package/dist/{phone-DVSZHt_j.js → phone-V2nuzPu9.js} +1 -1
- package/dist/{plus-C_XZDwBz.js → plus-BqQxkfoU.js} +1 -1
- package/dist/{refresh-cw-CfgN67l3.js → refresh-cw-CEOAPEcV.js} +1 -1
- package/dist/route-CZWcW-Dw.js +22 -0
- package/dist/{save-DJE8Fpdv.js → save-DDFvrxWl.js} +1 -1
- package/dist/{search-D_VyGcZT.js → search-D7Ya6jxj.js} +1 -1
- package/dist/{square-pen-D-6JYrVQ.js → square-pen-DoZZS4Lw.js} +1 -1
- package/dist/{star-BpRlUZuU.js → star-B7PgVu5j.js} +1 -1
- package/dist/{store-B-8HMuyZ.js → store-YVzcIG4A.js} +1 -1
- package/dist/{trash-2-8vmv-XV_.js → trash-2-BegnBzwI.js} +1 -1
- package/dist/{trending-up-Dz6PqPRj.js → trending-up-DBd3XNdb.js} +1 -1
- package/dist/{triangle-alert-D7Y3J_4z.js → triangle-alert-Bxk3Kgs6.js} +1 -1
- package/dist/{truck-D-y2IDzv.js → truck-Clrn0oHp.js} +1 -1
- package/dist/{upload-CHZofOqb.js → upload-Df5JmMwH.js} +1 -1
- package/dist/{useAssistant-DVW4QgQb.js → useAssistant-Cb8nnIVC.js} +11 -28
- package/dist/{useCustomers-BPW4__PR.js → useCustomers-Dq2IR2HQ.js} +1 -1
- package/dist/{useFleet-ChBkxAd6.js → useFleet-CapnWBcA.js} +1 -1
- package/dist/{useImportExport-DrPDHEDK.js → useImportExport-BoGzrlJy.js} +1 -1
- package/dist/{useInventory-CAAVMRp6.js → useInventory-Ci6M2d9f.js} +1 -1
- package/dist/{useOrders-B465S2nt.js → useOrders-BuQU1OHX.js} +1 -1
- package/dist/{useReports-jQ3GpNOm.js → useReports-DIx8W7CO.js} +1 -1
- package/dist/{useRoutes-hS3d5-xr.js → useRoutes-D7EvI_pm.js} +1 -1
- package/dist/{useSettings-BB8X2RLh.js → useSettings-BhX251UQ.js} +6 -6
- package/dist/{useTracking-RjGz9PZG.js → useTracking-C40jmZ6W.js} +3 -3
- package/dist/{user-DzfTn4dE.js → user-BlcJrbtE.js} +1 -1
- package/dist/{user-check-BEKRCi7v.js → user-check-DtnQmimP.js} +1 -1
- package/dist/{users-CCrq-w0C.js → users-Cvwko-Dx.js} +1 -1
- package/dist/{warehouse-CaqfmMKh.js → warehouse-LWLVTk6G.js} +1 -1
- package/dist/{wrench-q_rgJfaP.js → wrench-Cme0RbyR.js} +1 -1
- package/dist/{x-tMeDMcCw.js → x-D8C9Pg5V.js} +1 -1
- package/package.json +5 -1
- package/src/movethewheels/components/GlassPanel.tsx +71 -0
- package/src/movethewheels/components/MetricCard.tsx +171 -73
- package/src/movethewheels/components/StatusBadge.tsx +7 -6
- package/src/movethewheels/components/charts/ChartShell.tsx +78 -0
- package/src/movethewheels/components/charts/Sparkline.tsx +85 -0
- package/src/movethewheels/components/charts/StatusDonut.tsx +115 -0
- package/src/movethewheels/components/charts/TrendChart.tsx +179 -0
- package/src/movethewheels/components/charts/VolumeBar.tsx +97 -0
- package/src/movethewheels/components/charts/chartTokens.ts +43 -0
- package/src/movethewheels/components/charts/index.ts +9 -0
- package/src/movethewheels/components/index.ts +44 -1
- package/src/movethewheels/components/maps/LogisticsMap.tsx +94 -0
- package/src/movethewheels/components/maps/MapCanvas.tsx +118 -0
- package/src/movethewheels/components/maps/RouteMap.tsx +71 -0
- package/src/movethewheels/components/maps/TrackingMap.tsx +102 -0
- package/src/movethewheels/components/maps/index.ts +16 -0
- package/src/movethewheels/components/maps/leaflet-setup.ts +124 -0
- package/src/movethewheels/components/maps/map-markers.css +121 -0
- package/src/movethewheels/hooks/useAnalytics.ts +14 -4
- package/src/movethewheels/pages/AnalyticsPage.tsx +93 -73
- package/src/movethewheels/pages/CreateOrderPage.tsx +4 -4
- package/src/movethewheels/pages/CustomersPage.tsx +29 -20
- package/src/movethewheels/pages/DashboardPage.tsx +238 -12
- package/src/movethewheels/pages/DriverDetailsPage.tsx +1 -1
- package/src/movethewheels/pages/DriversPage.tsx +35 -24
- package/src/movethewheels/pages/FinancePage.tsx +68 -30
- package/src/movethewheels/pages/InventoryPage.tsx +15 -3
- package/src/movethewheels/pages/LiveTrackingPage.tsx +124 -16
- package/src/movethewheels/pages/OrdersListPage.tsx +46 -38
- package/src/movethewheels/pages/ProductsPage.tsx +28 -24
- package/src/movethewheels/pages/ReportsPage.tsx +23 -0
- package/src/movethewheels/pages/RouteDetailsPage.tsx +46 -7
- package/src/movethewheels/pages/SettingsPage.tsx +2 -2
- package/src/movethewheels/pages/TrackingPage.tsx +76 -6
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +1 -1
- package/src/movethewheels/pages/VehiclesPage.tsx +34 -24
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +1 -1
- package/src/movethewheels/pages/WarehousesPage.tsx +28 -22
- package/src/movethewheels/types/index.ts +1 -1
- package/src/movethewheels/utils/graphqlClient.ts +81 -31
- package/src/movethewheels/utils/mapMarkers.ts +58 -0
- package/dist/AnalyticsPage-CMpI1070.js +0 -215
- package/dist/CustomersPage-CaNBMXmM.js +0 -172
- package/dist/DashboardPage-DIKlXde3.js +0 -387
- package/dist/DriversPage-PYmdFiby.js +0 -176
- package/dist/FinancePage-Cwh9Bw4M.js +0 -136
- package/dist/InventoryPage-DpiqVu1v.js +0 -253
- package/dist/LiveTrackingPage-FAzzlVnX.js +0 -379
- package/dist/MetricCard-CVBVpu7o.js +0 -146
- package/dist/OrdersListPage-CAMsuUck.js +0 -384
- package/dist/ProductsPage-DC5qDLfz.js +0 -180
- package/dist/ReportsPage-BwUJ37eO.js +0 -356
- package/dist/TrackingPage-DaNEFG_h.js +0 -325
- package/dist/VehiclesPage-BFw13iMt.js +0 -176
- package/dist/WarehousesPage-Wkytnt86.js +0 -173
- package/dist/useAnalytics-D7HW2XRb.js +0 -347
- /package/dist/{formatters-BQr-aR5W.js → formatters-FWJDwTFv.js} +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { useId } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Area,
|
|
4
|
+
AreaChart,
|
|
5
|
+
CartesianGrid,
|
|
6
|
+
Legend,
|
|
7
|
+
Line,
|
|
8
|
+
LineChart,
|
|
9
|
+
ResponsiveContainer,
|
|
10
|
+
Tooltip,
|
|
11
|
+
XAxis,
|
|
12
|
+
YAxis,
|
|
13
|
+
} from 'recharts';
|
|
14
|
+
import { chartTokens, seriesColor } from './chartTokens';
|
|
15
|
+
import { ChartEmpty, ChartTooltip } from './ChartShell';
|
|
16
|
+
|
|
17
|
+
export interface TrendSeries {
|
|
18
|
+
/** Key into each data row. */
|
|
19
|
+
dataKey: string;
|
|
20
|
+
/** Legend / tooltip label. */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Explicit color (design-token string). Defaults to the categorical palette. */
|
|
23
|
+
color?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface TrendChartProps {
|
|
27
|
+
/** Row objects keyed by `xKey` + each series `dataKey`. */
|
|
28
|
+
data: Array<Record<string, string | number>>;
|
|
29
|
+
series: TrendSeries[];
|
|
30
|
+
/** Row key for the X axis (e.g. "date"). */
|
|
31
|
+
xKey: string;
|
|
32
|
+
variant?: 'area' | 'line';
|
|
33
|
+
height?: number;
|
|
34
|
+
className?: string;
|
|
35
|
+
showLegend?: boolean;
|
|
36
|
+
showGrid?: boolean;
|
|
37
|
+
emptyMessage?: string;
|
|
38
|
+
/** Optional Y/tooltip value formatter. */
|
|
39
|
+
valueFormatter?: (value: number | string, name: string) => string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* TrendChart — multi-series area/line chart for trends over time. Fully
|
|
44
|
+
* responsive (ResponsiveContainer), token-colored, with an honest empty state.
|
|
45
|
+
*/
|
|
46
|
+
export function TrendChart({
|
|
47
|
+
data,
|
|
48
|
+
series,
|
|
49
|
+
xKey,
|
|
50
|
+
variant = 'area',
|
|
51
|
+
height = 300,
|
|
52
|
+
className,
|
|
53
|
+
showLegend = true,
|
|
54
|
+
showGrid = true,
|
|
55
|
+
emptyMessage = 'No trend data yet',
|
|
56
|
+
valueFormatter,
|
|
57
|
+
}: TrendChartProps) {
|
|
58
|
+
const rawId = useId().replace(/:/g, '');
|
|
59
|
+
|
|
60
|
+
if (!data || data.length === 0 || series.length === 0) {
|
|
61
|
+
return <ChartEmpty message={emptyMessage} height={height} className={className} />;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const axisTick = { fill: chartTokens.axis, fontSize: 12 };
|
|
65
|
+
const colorFor = (s: TrendSeries, i: number) => s.color ?? seriesColor(i);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className={className} style={{ width: '100%', height }}>
|
|
69
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
70
|
+
{variant === 'area' ? (
|
|
71
|
+
<AreaChart data={data} margin={{ top: 8, right: 12, bottom: 4, left: -8 }}>
|
|
72
|
+
<defs>
|
|
73
|
+
{series.map((s, i) => (
|
|
74
|
+
<linearGradient
|
|
75
|
+
key={s.dataKey}
|
|
76
|
+
id={`${rawId}-${s.dataKey}`}
|
|
77
|
+
x1="0"
|
|
78
|
+
y1="0"
|
|
79
|
+
x2="0"
|
|
80
|
+
y2="1"
|
|
81
|
+
>
|
|
82
|
+
<stop offset="0%" stopColor={colorFor(s, i)} stopOpacity={0.28} />
|
|
83
|
+
<stop offset="100%" stopColor={colorFor(s, i)} stopOpacity={0.02} />
|
|
84
|
+
</linearGradient>
|
|
85
|
+
))}
|
|
86
|
+
</defs>
|
|
87
|
+
{showGrid && (
|
|
88
|
+
<CartesianGrid strokeDasharray="3 3" stroke={chartTokens.grid} vertical={false} />
|
|
89
|
+
)}
|
|
90
|
+
<XAxis
|
|
91
|
+
dataKey={xKey}
|
|
92
|
+
tick={axisTick}
|
|
93
|
+
tickLine={false}
|
|
94
|
+
axisLine={{ stroke: chartTokens.grid }}
|
|
95
|
+
minTickGap={24}
|
|
96
|
+
/>
|
|
97
|
+
<YAxis
|
|
98
|
+
tick={axisTick}
|
|
99
|
+
tickLine={false}
|
|
100
|
+
axisLine={false}
|
|
101
|
+
width={44}
|
|
102
|
+
tickFormatter={
|
|
103
|
+
valueFormatter ? (v: number) => valueFormatter(v, '') : undefined
|
|
104
|
+
}
|
|
105
|
+
/>
|
|
106
|
+
<Tooltip
|
|
107
|
+
cursor={{ stroke: chartTokens.axis, strokeOpacity: 0.3 }}
|
|
108
|
+
content={<ChartTooltip formatter={valueFormatter} />}
|
|
109
|
+
/>
|
|
110
|
+
{showLegend && (
|
|
111
|
+
<Legend
|
|
112
|
+
iconType="circle"
|
|
113
|
+
wrapperStyle={{ fontSize: 12, color: chartTokens.textSecondary }}
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
{series.map((s, i) => (
|
|
117
|
+
<Area
|
|
118
|
+
key={s.dataKey}
|
|
119
|
+
type="monotone"
|
|
120
|
+
dataKey={s.dataKey}
|
|
121
|
+
name={s.name}
|
|
122
|
+
stroke={colorFor(s, i)}
|
|
123
|
+
strokeWidth={2.5}
|
|
124
|
+
fill={`url(#${rawId}-${s.dataKey})`}
|
|
125
|
+
isAnimationActive={false}
|
|
126
|
+
activeDot={{ r: 4, strokeWidth: 0 }}
|
|
127
|
+
/>
|
|
128
|
+
))}
|
|
129
|
+
</AreaChart>
|
|
130
|
+
) : (
|
|
131
|
+
<LineChart data={data} margin={{ top: 8, right: 12, bottom: 4, left: -8 }}>
|
|
132
|
+
{showGrid && (
|
|
133
|
+
<CartesianGrid strokeDasharray="3 3" stroke={chartTokens.grid} vertical={false} />
|
|
134
|
+
)}
|
|
135
|
+
<XAxis
|
|
136
|
+
dataKey={xKey}
|
|
137
|
+
tick={axisTick}
|
|
138
|
+
tickLine={false}
|
|
139
|
+
axisLine={{ stroke: chartTokens.grid }}
|
|
140
|
+
minTickGap={24}
|
|
141
|
+
/>
|
|
142
|
+
<YAxis
|
|
143
|
+
tick={axisTick}
|
|
144
|
+
tickLine={false}
|
|
145
|
+
axisLine={false}
|
|
146
|
+
width={44}
|
|
147
|
+
tickFormatter={
|
|
148
|
+
valueFormatter ? (v: number) => valueFormatter(v, '') : undefined
|
|
149
|
+
}
|
|
150
|
+
/>
|
|
151
|
+
<Tooltip
|
|
152
|
+
cursor={{ stroke: chartTokens.axis, strokeOpacity: 0.3 }}
|
|
153
|
+
content={<ChartTooltip formatter={valueFormatter} />}
|
|
154
|
+
/>
|
|
155
|
+
{showLegend && (
|
|
156
|
+
<Legend
|
|
157
|
+
iconType="circle"
|
|
158
|
+
wrapperStyle={{ fontSize: 12, color: chartTokens.textSecondary }}
|
|
159
|
+
/>
|
|
160
|
+
)}
|
|
161
|
+
{series.map((s, i) => (
|
|
162
|
+
<Line
|
|
163
|
+
key={s.dataKey}
|
|
164
|
+
type="monotone"
|
|
165
|
+
dataKey={s.dataKey}
|
|
166
|
+
name={s.name}
|
|
167
|
+
stroke={colorFor(s, i)}
|
|
168
|
+
strokeWidth={2.5}
|
|
169
|
+
dot={false}
|
|
170
|
+
isAnimationActive={false}
|
|
171
|
+
activeDot={{ r: 4, strokeWidth: 0 }}
|
|
172
|
+
/>
|
|
173
|
+
))}
|
|
174
|
+
</LineChart>
|
|
175
|
+
)}
|
|
176
|
+
</ResponsiveContainer>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Bar,
|
|
3
|
+
BarChart,
|
|
4
|
+
CartesianGrid,
|
|
5
|
+
Legend,
|
|
6
|
+
ResponsiveContainer,
|
|
7
|
+
Tooltip,
|
|
8
|
+
XAxis,
|
|
9
|
+
YAxis,
|
|
10
|
+
} from 'recharts';
|
|
11
|
+
import { chartTokens, seriesColor } from './chartTokens';
|
|
12
|
+
import { ChartEmpty, ChartTooltip } from './ChartShell';
|
|
13
|
+
|
|
14
|
+
export interface VolumeSeries {
|
|
15
|
+
dataKey: string;
|
|
16
|
+
name: string;
|
|
17
|
+
color?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface VolumeBarProps {
|
|
21
|
+
data: Array<Record<string, string | number>>;
|
|
22
|
+
series: VolumeSeries[];
|
|
23
|
+
xKey: string;
|
|
24
|
+
height?: number;
|
|
25
|
+
className?: string;
|
|
26
|
+
showLegend?: boolean;
|
|
27
|
+
stacked?: boolean;
|
|
28
|
+
emptyMessage?: string;
|
|
29
|
+
valueFormatter?: (value: number | string, name: string) => string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* VolumeBar — grouped/stacked bar chart (e.g. weekly order volume, revenue by
|
|
34
|
+
* month). Responsive, token-colored, rounded bars, honest empty state.
|
|
35
|
+
*/
|
|
36
|
+
export function VolumeBar({
|
|
37
|
+
data,
|
|
38
|
+
series,
|
|
39
|
+
xKey,
|
|
40
|
+
height = 280,
|
|
41
|
+
className,
|
|
42
|
+
showLegend = true,
|
|
43
|
+
stacked = false,
|
|
44
|
+
emptyMessage = 'No volume data yet',
|
|
45
|
+
valueFormatter,
|
|
46
|
+
}: VolumeBarProps) {
|
|
47
|
+
if (!data || data.length === 0 || series.length === 0) {
|
|
48
|
+
return <ChartEmpty message={emptyMessage} height={height} className={className} />;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const axisTick = { fill: chartTokens.axis, fontSize: 12 };
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className={className} style={{ width: '100%', height }}>
|
|
55
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
56
|
+
<BarChart data={data} margin={{ top: 8, right: 12, bottom: 4, left: -8 }} barGap={4}>
|
|
57
|
+
<CartesianGrid strokeDasharray="3 3" stroke={chartTokens.grid} vertical={false} />
|
|
58
|
+
<XAxis
|
|
59
|
+
dataKey={xKey}
|
|
60
|
+
tick={axisTick}
|
|
61
|
+
tickLine={false}
|
|
62
|
+
axisLine={{ stroke: chartTokens.grid }}
|
|
63
|
+
/>
|
|
64
|
+
<YAxis
|
|
65
|
+
tick={axisTick}
|
|
66
|
+
tickLine={false}
|
|
67
|
+
axisLine={false}
|
|
68
|
+
width={44}
|
|
69
|
+
tickFormatter={valueFormatter ? (v: number) => valueFormatter(v, '') : undefined}
|
|
70
|
+
/>
|
|
71
|
+
<Tooltip
|
|
72
|
+
cursor={{ fill: chartTokens.axis, fillOpacity: 0.08 }}
|
|
73
|
+
content={<ChartTooltip formatter={valueFormatter} />}
|
|
74
|
+
/>
|
|
75
|
+
{showLegend && (
|
|
76
|
+
<Legend
|
|
77
|
+
iconType="circle"
|
|
78
|
+
wrapperStyle={{ fontSize: 12, color: chartTokens.textSecondary }}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
81
|
+
{series.map((s, i) => (
|
|
82
|
+
<Bar
|
|
83
|
+
key={s.dataKey}
|
|
84
|
+
dataKey={s.dataKey}
|
|
85
|
+
name={s.name}
|
|
86
|
+
fill={s.color ?? seriesColor(i)}
|
|
87
|
+
radius={stacked ? [0, 0, 0, 0] : [4, 4, 0, 0]}
|
|
88
|
+
stackId={stacked ? 'stack' : undefined}
|
|
89
|
+
maxBarSize={48}
|
|
90
|
+
isAnimationActive={false}
|
|
91
|
+
/>
|
|
92
|
+
))}
|
|
93
|
+
</BarChart>
|
|
94
|
+
</ResponsiveContainer>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart color tokens.
|
|
3
|
+
*
|
|
4
|
+
* Recharts needs explicit color strings on its series props (fill/stroke) where
|
|
5
|
+
* Tailwind token classes cannot reach the SVG. We source every one of those
|
|
6
|
+
* colors from the fe-libs design tokens via `var(--color-*)` so charts stay
|
|
7
|
+
* theme-aware (light/dark + product themes) and never introduce raw palette
|
|
8
|
+
* colors. The CSS variables resolve to full color values in the app-shell.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const chartTokens = {
|
|
12
|
+
/** Product brand orange — primary accent / first data series. */
|
|
13
|
+
brandPrimary: 'var(--color-brand-primary)',
|
|
14
|
+
/** Brand secondary (orange-hover) — used sparingly for gradients. */
|
|
15
|
+
brandSecondary: 'var(--color-brand-secondary)',
|
|
16
|
+
/** Brand blue — the natural counterpart series to the orange. */
|
|
17
|
+
brandBlue: 'var(--color-chart-1)',
|
|
18
|
+
/** Categorical series palette (theme-aware). */
|
|
19
|
+
series: [
|
|
20
|
+
'var(--color-brand-primary)',
|
|
21
|
+
'var(--color-chart-1)',
|
|
22
|
+
'var(--color-chart-2)',
|
|
23
|
+
'var(--color-chart-3)',
|
|
24
|
+
'var(--color-chart-4)',
|
|
25
|
+
'var(--color-chart-5)',
|
|
26
|
+
'var(--color-chart-6)',
|
|
27
|
+
'var(--color-chart-7)',
|
|
28
|
+
'var(--color-chart-8)',
|
|
29
|
+
],
|
|
30
|
+
grid: 'var(--color-chart-grid)',
|
|
31
|
+
axis: 'var(--color-chart-axis)',
|
|
32
|
+
positive: 'var(--color-chart-positive)',
|
|
33
|
+
negative: 'var(--color-chart-negative)',
|
|
34
|
+
surface: 'var(--color-bg-surface)',
|
|
35
|
+
border: 'var(--color-border-seam)',
|
|
36
|
+
textSecondary: 'var(--color-text-secondary)',
|
|
37
|
+
textPrimary: 'var(--color-text-primary)',
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
/** Resolve a categorical color by index, wrapping around the palette. */
|
|
41
|
+
export function seriesColor(index: number): string {
|
|
42
|
+
return chartTokens.series[index % chartTokens.series.length];
|
|
43
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart primitives (recharts) — token-colored, responsive, honest empty states.
|
|
3
|
+
*/
|
|
4
|
+
export { Sparkline, type SparklinePoint } from './Sparkline';
|
|
5
|
+
export { TrendChart, type TrendSeries } from './TrendChart';
|
|
6
|
+
export { VolumeBar, type VolumeSeries } from './VolumeBar';
|
|
7
|
+
export { StatusDonut, type DonutSlice } from './StatusDonut';
|
|
8
|
+
export { ChartEmpty, ChartTooltip } from './ChartShell';
|
|
9
|
+
export { chartTokens, seriesColor } from './chartTokens';
|
|
@@ -11,7 +11,50 @@ export * from './ui';
|
|
|
11
11
|
export { PageLayout, PageSection, EmptyState, LoadingState, ErrorState } from './PageLayout';
|
|
12
12
|
|
|
13
13
|
// Metric components
|
|
14
|
-
export {
|
|
14
|
+
export {
|
|
15
|
+
MetricCard,
|
|
16
|
+
MetricCardSkeleton,
|
|
17
|
+
MetricGrid,
|
|
18
|
+
type MetricAccent,
|
|
19
|
+
type MetricDelta,
|
|
20
|
+
} from './MetricCard';
|
|
21
|
+
|
|
22
|
+
// Surface primitive
|
|
23
|
+
export { GlassPanel } from './GlassPanel';
|
|
24
|
+
|
|
25
|
+
// Charts (recharts)
|
|
26
|
+
export {
|
|
27
|
+
Sparkline,
|
|
28
|
+
TrendChart,
|
|
29
|
+
VolumeBar,
|
|
30
|
+
StatusDonut,
|
|
31
|
+
ChartEmpty,
|
|
32
|
+
ChartTooltip,
|
|
33
|
+
chartTokens,
|
|
34
|
+
seriesColor,
|
|
35
|
+
type SparklinePoint,
|
|
36
|
+
type TrendSeries,
|
|
37
|
+
type VolumeSeries,
|
|
38
|
+
type DonutSlice,
|
|
39
|
+
} from './charts';
|
|
40
|
+
|
|
41
|
+
// Maps (react-leaflet + OpenStreetMap)
|
|
42
|
+
export {
|
|
43
|
+
LogisticsMap,
|
|
44
|
+
RouteMap,
|
|
45
|
+
TrackingMap,
|
|
46
|
+
MapCanvas,
|
|
47
|
+
createPinIcon,
|
|
48
|
+
createStopIcon,
|
|
49
|
+
createCurrentIcon,
|
|
50
|
+
isRealCoord,
|
|
51
|
+
type LogisticsMarker,
|
|
52
|
+
type LogisticsRoute,
|
|
53
|
+
type RouteStop,
|
|
54
|
+
type TrackPoint,
|
|
55
|
+
type TrackEndpoint,
|
|
56
|
+
type MapMarkerStatus,
|
|
57
|
+
} from './maps';
|
|
15
58
|
|
|
16
59
|
// Status components
|
|
17
60
|
export {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { Marker, Polyline, Popup } from 'react-leaflet';
|
|
3
|
+
import { MapCanvas } from './MapCanvas';
|
|
4
|
+
import { createPinIcon, isRealCoord, type MapMarkerStatus } from './leaflet-setup';
|
|
5
|
+
|
|
6
|
+
export interface LogisticsMarker {
|
|
7
|
+
id: string;
|
|
8
|
+
lat: number;
|
|
9
|
+
lng: number;
|
|
10
|
+
label: string;
|
|
11
|
+
status?: MapMarkerStatus;
|
|
12
|
+
sublabel?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LogisticsRoute {
|
|
16
|
+
points: Array<[number, number]>;
|
|
17
|
+
color?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface LogisticsMapProps {
|
|
21
|
+
markers: LogisticsMarker[];
|
|
22
|
+
routes?: LogisticsRoute[];
|
|
23
|
+
height?: number | string;
|
|
24
|
+
className?: string;
|
|
25
|
+
fitToMarkers?: boolean;
|
|
26
|
+
emptyMessage?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* LogisticsMap — an OSM map of brand-styled vehicle/asset markers plus optional
|
|
31
|
+
* route polylines. Auto-fits to the visible markers. Coordinates at null-island
|
|
32
|
+
* (0,0) or non-finite values are treated as "no location" and filtered out, so
|
|
33
|
+
* the map is never dumped in the ocean. Empty markers → honest empty state.
|
|
34
|
+
*/
|
|
35
|
+
export function LogisticsMap({
|
|
36
|
+
markers,
|
|
37
|
+
routes = [],
|
|
38
|
+
height = 420,
|
|
39
|
+
className,
|
|
40
|
+
fitToMarkers = true,
|
|
41
|
+
emptyMessage = 'No live locations yet',
|
|
42
|
+
}: LogisticsMapProps) {
|
|
43
|
+
const validMarkers = useMemo(
|
|
44
|
+
() => markers.filter((m) => isRealCoord(m.lat, m.lng)),
|
|
45
|
+
[markers]
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const validRoutes = useMemo(
|
|
49
|
+
() =>
|
|
50
|
+
routes
|
|
51
|
+
.map((r) => ({
|
|
52
|
+
...r,
|
|
53
|
+
points: r.points.filter(([lat, lng]) => isRealCoord(lat, lng)),
|
|
54
|
+
}))
|
|
55
|
+
.filter((r) => r.points.length >= 2),
|
|
56
|
+
[routes]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const fitPoints = useMemo<Array<[number, number]>>(() => {
|
|
60
|
+
const pts: Array<[number, number]> = validMarkers.map((m) => [m.lat, m.lng]);
|
|
61
|
+
validRoutes.forEach((r) => pts.push(...r.points));
|
|
62
|
+
return pts;
|
|
63
|
+
}, [validMarkers, validRoutes]);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<MapCanvas
|
|
67
|
+
points={fitPoints}
|
|
68
|
+
height={height}
|
|
69
|
+
className={className}
|
|
70
|
+
fitToMarkers={fitToMarkers}
|
|
71
|
+
emptyState={<p className="text-sm text-text-secondary">{emptyMessage}</p>}
|
|
72
|
+
>
|
|
73
|
+
{validRoutes.map((route, i) => (
|
|
74
|
+
<Polyline
|
|
75
|
+
key={i}
|
|
76
|
+
positions={route.points}
|
|
77
|
+
pathOptions={{
|
|
78
|
+
color: route.color ?? 'var(--color-chart-1)',
|
|
79
|
+
weight: 4,
|
|
80
|
+
opacity: 0.85,
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
))}
|
|
84
|
+
{validMarkers.map((m) => (
|
|
85
|
+
<Marker key={m.id} position={[m.lat, m.lng]} icon={createPinIcon(m.status ?? 'default')}>
|
|
86
|
+
<Popup>
|
|
87
|
+
<span className="font-semibold">{m.label}</span>
|
|
88
|
+
{m.sublabel && <span className="block text-text-secondary">{m.sublabel}</span>}
|
|
89
|
+
</Popup>
|
|
90
|
+
</Marker>
|
|
91
|
+
))}
|
|
92
|
+
</MapCanvas>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -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
|
+
}
|