@burdenoff/microfe-movethewheels 2026.703.3 → 2026.703.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AIAssistantPage-C3cPsWt-.js → AIAssistantPage-D2gaEL1U.js} +16 -15
- package/dist/AnalyticsPage-DZMlEoBi.js +194 -0
- package/dist/CreateOrderPage-CWHx8yys.js +490 -0
- package/dist/CustomerDetailsPage-nR4oZ_ju.js +615 -0
- package/dist/CustomersPage-Bbvwvn8k.js +205 -0
- package/dist/DashboardPage-DrEhNf7N.js +542 -0
- package/dist/DataTable-CqMUD4er.js +340 -0
- package/dist/DocumentsCard-edDkgnJj.js +169 -0
- package/dist/DriverDetailsPage-whEMOWs9.js +490 -0
- package/dist/DriversPage-CHF2ZEzd.js +221 -0
- package/dist/FinancePage-Dn9GrW-1.js +175 -0
- package/dist/{FleetPage-DqkFi3s3.js → FleetPage-CrtGPy-p.js} +36 -35
- package/dist/{FormError-BlOjnXOq.js → FormError-BKcb_wT-.js} +1 -1
- package/dist/{ImportExportPage-DKJRCezl.js → ImportExportPage-WYdwZomZ.js} +52 -72
- package/dist/InventoryPage-l5A1f753.js +259 -0
- package/dist/LiveTrackingPage-DhEaNKZ4.js +429 -0
- package/dist/MarketplacePage-B9RYOb1Y.js +426 -0
- package/dist/MetricCard-y3bWHKUQ.js +191 -0
- package/dist/Notice-DOO-tjpV.js +46 -0
- package/dist/OrderDetailsPage-CwYHwk1K.js +584 -0
- package/dist/OrdersListPage-DhZJoQeI.js +386 -0
- package/dist/{PageLayout-D4LDHJJq.js → PageLayout-BniRJaQ6.js} +1 -1
- package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-1Gm9JKy0.js} +56 -56
- package/dist/ProductsPage-C8sn-1tN.js +186 -0
- package/dist/ReportsPage-D1tb0dcz.js +377 -0
- package/dist/RouteDetailsPage-CkMyR9b_.js +610 -0
- package/dist/{RoutesPage-DMfoicM1.js → RoutesPage-QkENzsE5.js} +58 -47
- package/dist/{SettingsPage-Bn-v0mbQ.js → SettingsPage-DV3_tU_X.js} +16 -38
- package/dist/Sparkline-K2RMJ58u.js +16729 -0
- package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-BNHXYvZx.js} +7 -7
- package/dist/TrackingPage-BZrg4RCu.js +368 -0
- package/dist/VehicleDetailsPage-D-8XPZ2G.js +393 -0
- package/dist/VehiclesPage-D5BELbtM.js +184 -0
- package/dist/{WarehouseDetailsPage-L_6sWigM.js → WarehouseDetailsPage-Cmg-8Frn.js} +70 -70
- package/dist/WarehousesPage-C1Y1M867.js +182 -0
- package/dist/{arrow-left-n48Y-Wis.js → arrow-left-ClW4Isj7.js} +1 -1
- package/dist/arrow-right-CmKOmIkh.js +11 -0
- package/dist/arrow-up-BJijRLAZ.js +17 -0
- package/dist/{box-D9Pal4HM.js → box-BT3NsRQP.js} +1 -1
- package/dist/boxes-B_OGvQiW.js +54 -0
- package/dist/{chart-column-BFIJysDR.js → chart-column-DQTTdUPk.js} +1 -1
- package/dist/charts-Tj_PPTfA.js +2018 -0
- package/dist/chevron-left-B1rZyWWb.js +8 -0
- package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-BIcwUJZq.js} +1 -1
- package/dist/chunk-BdsCSvBP.js +20 -0
- package/dist/{circle-alert-iY24lSlp.js → circle-alert-CNkuDNc6.js} +1 -1
- package/dist/circle-check-Cs1OVjtV.js +13 -0
- package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-C-p4eNwn.js} +1 -1
- package/dist/{clock-DJZtdl8u.js → clock-dRGG4rkF.js} +1 -1
- package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-B6RoYHHz.js} +1 -1
- package/dist/{download-DRXxoiQR.js → download-DP04iWTd.js} +1 -1
- package/dist/{file-text-DWWY6_f5.js → file-text-BuDx4hZp.js} +1 -1
- package/dist/{filter-DloLC2eE.js → filter-BD483_8g.js} +1 -1
- package/dist/{wrench-CANXWxF-.js → fuel-DLH1ag04.js} +3 -6
- package/dist/index.css +2 -0
- package/dist/index.js +150 -172
- package/dist/{map-CkJaULb2.js → map-DNQn9fyY.js} +1 -1
- package/dist/{map-pin-BGiCvyCs.js → map-pin-CqN1bbT1.js} +1 -1
- package/dist/mapMarkers-NCV9MRKR.js +28 -0
- package/dist/maps-yzLqiK_C.js +5070 -0
- package/dist/{navigation-D36prnAn.js → navigation-CgGUt2tR.js} +1 -1
- package/dist/{package-BChUzpJS.js → package-DmRF7iQD.js} +1 -1
- package/dist/{phone-Ac9s3mo_.js → phone-Cl32EXKW.js} +1 -1
- package/dist/{plus-Z76q-FZm.js → plus-DlANgOFx.js} +1 -1
- package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-rW59D4b7.js} +1 -1
- package/dist/route-CWjPrtAN.js +22 -0
- package/dist/{save-CojywNhG.js → save-UDlog7hg.js} +1 -1
- package/dist/{search-CDGefqb3.js → search-DNIG5Ck7.js} +1 -1
- package/dist/{square-pen-CopE9GmB.js → square-pen-BcfB3Grd.js} +1 -1
- package/dist/{star-nHlLjDKR.js → star-CVZHFwDx.js} +1 -1
- package/dist/{store-BR9Voi5E.js → store-bwnmCK9f.js} +1 -1
- package/dist/{trash-2-BKhar6Hs.js → trash-2-B80G12d4.js} +1 -1
- package/dist/{trending-up-Dohgd4t_.js → trending-up-CH4R4KOa.js} +1 -1
- package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-CmS8AUU6.js} +1 -1
- package/dist/{truck-C0v_hbLR.js → truck-9rLDncav.js} +1 -1
- package/dist/upload-DanJY1Vo.js +21 -0
- package/dist/{useAssistant-D4v3oxD3.js → useAssistant-CnEwkTLm.js} +11 -28
- package/dist/{useCustomers-BmTNGxiI.js → useCustomers-ChJao1ET.js} +30 -27
- package/dist/{useFleet-DKqXZ16m.js → useFleet-g5Yri0G8.js} +82 -70
- package/dist/{useImportExport-DHr-SWSw.js → useImportExport-MYwIT3rE.js} +1 -1
- package/dist/{useInventory-DkyiVY_A.js → useInventory-BSOn-R1c.js} +84 -39
- package/dist/{useOrders-Cfk1GLiq.js → useOrders-BSiqo61w.js} +59 -48
- package/dist/{useReports-CTqZr-AB.js → useReports-fWESfsF9.js} +41 -35
- package/dist/{useRoutes-BQWO607N.js → useRoutes-zD8Yp323.js} +50 -42
- package/dist/{useSettings-CkDH_-gM.js → useSettings-B1lsWeZ1.js} +6 -6
- package/dist/{useTracking-B-va1kO3.js → useTracking-BKG0I2ln.js} +3 -3
- package/dist/{user-jyFU3lXk.js → user-DU51el3m.js} +1 -1
- package/dist/user-check-IMhk-qkX.js +20 -0
- package/dist/{users-DZ-ilS6B.js → users-B8KhJBGg.js} +1 -1
- package/dist/{warehouse-6m5kXMLu.js → warehouse-CcMMioeY.js} +1 -1
- package/dist/wrench-Dg-sgH2e.js +8 -0
- package/dist/x-C4ObLVEa.js +11 -0
- package/package.json +5 -1
- package/src/movethewheels/components/DataTable.tsx +252 -84
- package/src/movethewheels/components/DocumentsCard.tsx +102 -0
- package/src/movethewheels/components/GlassPanel.tsx +71 -0
- package/src/movethewheels/components/MetricCard.tsx +171 -73
- package/src/movethewheels/components/Notice.tsx +61 -0
- package/src/movethewheels/components/StatusBadge.tsx +7 -6
- package/src/movethewheels/components/charts/ChartShell.tsx +78 -0
- package/src/movethewheels/components/charts/Sparkline.tsx +85 -0
- package/src/movethewheels/components/charts/StatusDonut.tsx +115 -0
- package/src/movethewheels/components/charts/TrendChart.tsx +179 -0
- package/src/movethewheels/components/charts/VolumeBar.tsx +97 -0
- package/src/movethewheels/components/charts/chartTokens.ts +43 -0
- package/src/movethewheels/components/charts/index.ts +9 -0
- package/src/movethewheels/components/index.ts +51 -1
- package/src/movethewheels/components/maps/LogisticsMap.tsx +94 -0
- package/src/movethewheels/components/maps/MapCanvas.tsx +118 -0
- package/src/movethewheels/components/maps/RouteMap.tsx +71 -0
- package/src/movethewheels/components/maps/TrackingMap.tsx +102 -0
- package/src/movethewheels/components/maps/index.ts +16 -0
- package/src/movethewheels/components/maps/leaflet-setup.ts +124 -0
- package/src/movethewheels/components/maps/map-markers.css +121 -0
- package/src/movethewheels/hooks/index.ts +9 -0
- package/src/movethewheels/hooks/useAnalytics.ts +109 -4
- package/src/movethewheels/hooks/useCustomers.ts +11 -4
- package/src/movethewheels/hooks/useEntityDocuments.ts +132 -0
- package/src/movethewheels/hooks/useFleet.ts +26 -7
- package/src/movethewheels/hooks/useInventory.ts +70 -7
- package/src/movethewheels/hooks/useOrders.ts +32 -2
- package/src/movethewheels/hooks/useReports.ts +14 -4
- package/src/movethewheels/hooks/useRoutes.ts +20 -3
- package/src/movethewheels/pages/AnalyticsPage.tsx +93 -73
- package/src/movethewheels/pages/CreateOrderPage.tsx +48 -20
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +239 -33
- package/src/movethewheels/pages/CustomersPage.tsx +53 -28
- package/src/movethewheels/pages/DashboardPage.tsx +238 -12
- package/src/movethewheels/pages/DriverDetailsPage.tsx +17 -31
- package/src/movethewheels/pages/DriversPage.tsx +59 -31
- package/src/movethewheels/pages/FinancePage.tsx +103 -82
- package/src/movethewheels/pages/InventoryPage.tsx +15 -3
- package/src/movethewheels/pages/LiveTrackingPage.tsx +124 -16
- package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
- package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
- package/src/movethewheels/pages/OrdersListPage.tsx +118 -47
- package/src/movethewheels/pages/ProductsPage.tsx +52 -28
- package/src/movethewheels/pages/ReportsPage.tsx +88 -4
- package/src/movethewheels/pages/RouteDetailsPage.tsx +119 -19
- package/src/movethewheels/pages/RoutesPage.tsx +22 -6
- package/src/movethewheels/pages/SettingsPage.tsx +5 -21
- package/src/movethewheels/pages/TrackingPage.tsx +76 -6
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +4 -34
- package/src/movethewheels/pages/VehiclesPage.tsx +58 -27
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +1 -1
- package/src/movethewheels/pages/WarehousesPage.tsx +47 -36
- package/src/movethewheels/types/index.ts +9 -1
- package/src/movethewheels/utils/download.ts +62 -0
- package/src/movethewheels/utils/mapMarkers.ts +58 -0
- package/dist/AnalyticsPage-Dz6g2MvK.js +0 -215
- package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
- package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
- package/dist/CustomersPage-BKxMS1AV.js +0 -161
- package/dist/DashboardPage-BVjQi0EB.js +0 -387
- package/dist/DataTable-DmciIKKM.js +0 -249
- package/dist/DriverDetailsPage-CN3WQ0B9.js +0 -507
- package/dist/DriversPage-CH9rjQC-.js +0 -165
- package/dist/FinancePage-DEDykSOP.js +0 -158
- package/dist/InventoryPage-e0ze63iX.js +0 -253
- package/dist/LiveTrackingPage-DCX-K-LR.js +0 -379
- package/dist/MarketplacePage-CIQ97_zp.js +0 -247
- package/dist/MetricCard-BY0QrMD4.js +0 -146
- package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
- package/dist/OrdersListPage-Cn90P1Xx.js +0 -307
- package/dist/ProductsPage-QJfHHpqB.js +0 -169
- package/dist/ReportsPage-Zp53q3_8.js +0 -284
- package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
- package/dist/TrackingPage-W-jtdM3Y.js +0 -325
- package/dist/VehicleDetailsPage-Bceo5oz3.js +0 -410
- package/dist/VehiclesPage-ApKThYeG.js +0 -165
- package/dist/WarehousesPage-DCbXXXWE.js +0 -162
- package/dist/useAnalytics-DS8n0xgg.js +0 -297
- /package/dist/{formatters-CF2S2l1m.js → formatters-FWJDwTFv.js} +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
|
|
2
|
+
import { cn } from '../../utils/cn';
|
|
3
|
+
import { seriesColor } from './chartTokens';
|
|
4
|
+
import { ChartEmpty, ChartTooltip } from './ChartShell';
|
|
5
|
+
|
|
6
|
+
export interface DonutSlice {
|
|
7
|
+
name: string;
|
|
8
|
+
value: number;
|
|
9
|
+
/** Optional explicit color (design-token string). */
|
|
10
|
+
color?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface StatusDonutProps {
|
|
14
|
+
data: DonutSlice[];
|
|
15
|
+
height?: number;
|
|
16
|
+
className?: string;
|
|
17
|
+
/** Big number rendered in the donut hole (e.g. a total). */
|
|
18
|
+
centerLabel?: string | number;
|
|
19
|
+
centerSubLabel?: string;
|
|
20
|
+
showLegend?: boolean;
|
|
21
|
+
emptyMessage?: string;
|
|
22
|
+
valueFormatter?: (value: number) => string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* StatusDonut — donut chart with a centered total and a token-styled legend.
|
|
27
|
+
* Responsive; renders an honest empty state when every slice is zero/absent.
|
|
28
|
+
*/
|
|
29
|
+
export function StatusDonut({
|
|
30
|
+
data,
|
|
31
|
+
height = 240,
|
|
32
|
+
className,
|
|
33
|
+
centerLabel,
|
|
34
|
+
centerSubLabel,
|
|
35
|
+
showLegend = true,
|
|
36
|
+
emptyMessage = 'No breakdown yet',
|
|
37
|
+
valueFormatter,
|
|
38
|
+
}: StatusDonutProps) {
|
|
39
|
+
const slices = (data ?? []).filter((d) => d.value > 0);
|
|
40
|
+
const total = slices.reduce((sum, d) => sum + d.value, 0);
|
|
41
|
+
|
|
42
|
+
if (slices.length === 0 || total === 0) {
|
|
43
|
+
return <ChartEmpty message={emptyMessage} height={height} className={className} />;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const colorOf = (slice: DonutSlice, i: number) => slice.color ?? seriesColor(i);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className={cn('flex flex-col gap-4 sm:flex-row sm:items-center', className)}>
|
|
50
|
+
<div className="relative mx-auto" style={{ width: height, height, maxWidth: '100%' }}>
|
|
51
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
52
|
+
<PieChart>
|
|
53
|
+
<Pie
|
|
54
|
+
data={slices}
|
|
55
|
+
dataKey="value"
|
|
56
|
+
nameKey="name"
|
|
57
|
+
innerRadius="62%"
|
|
58
|
+
outerRadius="100%"
|
|
59
|
+
paddingAngle={2}
|
|
60
|
+
stroke="none"
|
|
61
|
+
isAnimationActive={false}
|
|
62
|
+
>
|
|
63
|
+
{slices.map((slice, i) => (
|
|
64
|
+
<Cell key={slice.name} fill={colorOf(slice, i)} />
|
|
65
|
+
))}
|
|
66
|
+
</Pie>
|
|
67
|
+
<Tooltip
|
|
68
|
+
content={
|
|
69
|
+
<ChartTooltip
|
|
70
|
+
formatter={(v) =>
|
|
71
|
+
valueFormatter ? valueFormatter(Number(v)) : String(v)
|
|
72
|
+
}
|
|
73
|
+
/>
|
|
74
|
+
}
|
|
75
|
+
/>
|
|
76
|
+
</PieChart>
|
|
77
|
+
</ResponsiveContainer>
|
|
78
|
+
{centerLabel != null && (
|
|
79
|
+
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center">
|
|
80
|
+
<span className="text-2xl font-bold tabular-nums text-text-primary">
|
|
81
|
+
{centerLabel}
|
|
82
|
+
</span>
|
|
83
|
+
{centerSubLabel && (
|
|
84
|
+
<span className="text-xs text-text-secondary">{centerSubLabel}</span>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{showLegend && (
|
|
91
|
+
<ul className="flex flex-1 flex-col gap-2">
|
|
92
|
+
{slices.map((slice, i) => {
|
|
93
|
+
const pct = total > 0 ? Math.round((slice.value / total) * 100) : 0;
|
|
94
|
+
return (
|
|
95
|
+
<li key={slice.name} className="flex items-center gap-2 text-sm">
|
|
96
|
+
<span
|
|
97
|
+
aria-hidden="true"
|
|
98
|
+
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
|
99
|
+
style={{ background: colorOf(slice, i) }}
|
|
100
|
+
/>
|
|
101
|
+
<span className="text-text-secondary">{slice.name}</span>
|
|
102
|
+
<span className="ml-auto font-semibold tabular-nums text-text-primary">
|
|
103
|
+
{valueFormatter ? valueFormatter(slice.value) : slice.value}
|
|
104
|
+
</span>
|
|
105
|
+
<span className="w-10 text-right text-xs tabular-nums text-text-secondary">
|
|
106
|
+
{pct}%
|
|
107
|
+
</span>
|
|
108
|
+
</li>
|
|
109
|
+
);
|
|
110
|
+
})}
|
|
111
|
+
</ul>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -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 {
|
|
@@ -24,3 +67,10 @@ export {
|
|
|
24
67
|
|
|
25
68
|
// Table components
|
|
26
69
|
export { DataTable, type Column } from './DataTable';
|
|
70
|
+
|
|
71
|
+
// Feedback components
|
|
72
|
+
export { FormError } from './FormError';
|
|
73
|
+
export { Notice, useNotice } from './Notice';
|
|
74
|
+
|
|
75
|
+
// Documents (reuse of wspace-files-svc)
|
|
76
|
+
export { DocumentsCard } from './DocumentsCard';
|
|
@@ -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
|
+
}
|