@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
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useState } from 'react';
|
|
6
|
-
import { Users, Plus, RefreshCw, Download, Star } from 'lucide-react';
|
|
6
|
+
import { Users, Plus, RefreshCw, Download, Star, UserCheck, Building2 } from 'lucide-react';
|
|
7
7
|
import { PageLayout } from '../components/PageLayout';
|
|
8
|
+
import { MetricCard, MetricGrid } from '../components/MetricCard';
|
|
8
9
|
import { DataTable, type Column } from '../components/DataTable';
|
|
9
10
|
import { TypeBadge } from '../components/StatusBadge';
|
|
10
11
|
import { Badge, Avatar, AvatarFallback, Button } from '../components/ui';
|
|
11
|
-
import { CTAOverflowMenu,
|
|
12
|
+
import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
12
13
|
import { useCustomers, useCustomerStats } from '../hooks/useCustomers';
|
|
13
14
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
14
15
|
import { formatDate } from '../utils/formatters';
|
|
@@ -119,24 +120,32 @@ export default function CustomersPage() {
|
|
|
119
120
|
next shipment.
|
|
120
121
|
</PagePurpose>
|
|
121
122
|
|
|
122
|
-
{/* Customer base at a glance —
|
|
123
|
-
<
|
|
124
|
-
<p className="text-sm font-medium text-text-secondary">Customer base at a glance</p>
|
|
125
|
-
<
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
123
|
+
{/* Customer base at a glance — premium stat cards */}
|
|
124
|
+
<div>
|
|
125
|
+
<p className="mb-3 text-sm font-medium text-text-secondary">Customer base at a glance</p>
|
|
126
|
+
<MetricGrid columns={3}>
|
|
127
|
+
<MetricCard
|
|
128
|
+
label="Total customers"
|
|
129
|
+
value={stats.total}
|
|
130
|
+
icon={<Users size={20} />}
|
|
131
|
+
accent="brand"
|
|
132
|
+
/>
|
|
133
|
+
<MetricCard
|
|
134
|
+
label="Active"
|
|
135
|
+
value={stats.active}
|
|
136
|
+
icon={<UserCheck size={20} />}
|
|
137
|
+
accent="success"
|
|
138
|
+
subtitle="Currently transacting"
|
|
139
|
+
/>
|
|
140
|
+
<MetricCard
|
|
141
|
+
label="Business accounts"
|
|
142
|
+
value={stats.b2b}
|
|
143
|
+
icon={<Building2 size={20} />}
|
|
144
|
+
accent="blue"
|
|
145
|
+
subtitle="B2B relationships"
|
|
146
|
+
/>
|
|
147
|
+
</MetricGrid>
|
|
148
|
+
</div>
|
|
140
149
|
|
|
141
150
|
<DataTable
|
|
142
151
|
columns={columns}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Main overview page with key metrics and quick actions.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { useMemo } from 'react';
|
|
7
8
|
import {
|
|
8
9
|
Package,
|
|
9
10
|
Truck,
|
|
@@ -14,10 +15,14 @@ import {
|
|
|
14
15
|
TrendingUp,
|
|
15
16
|
MapPin,
|
|
16
17
|
ChevronRight,
|
|
18
|
+
Activity,
|
|
17
19
|
} from 'lucide-react';
|
|
18
20
|
import { PageLayout, PageSection } from '../components/PageLayout';
|
|
19
|
-
import { MetricCard, MetricGrid } from '../components/MetricCard';
|
|
21
|
+
import { MetricCard, MetricGrid, type MetricDelta } from '../components/MetricCard';
|
|
20
22
|
import { StatusBadge } from '../components/StatusBadge';
|
|
23
|
+
import { LogisticsMap, type LogisticsMarker } from '../components/maps';
|
|
24
|
+
import { VolumeBar } from '../components/charts';
|
|
25
|
+
import { deliveryMarkerStatus, firstRealLatLng } from '../utils/mapMarkers';
|
|
21
26
|
import {
|
|
22
27
|
Card,
|
|
23
28
|
CardContent,
|
|
@@ -28,15 +33,26 @@ import {
|
|
|
28
33
|
Progress,
|
|
29
34
|
} from '../components/ui';
|
|
30
35
|
import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
31
|
-
import { useDashboardMetrics } from '../hooks/useAnalytics';
|
|
36
|
+
import { useAnalytics, useDashboardMetrics } from '../hooks/useAnalytics';
|
|
32
37
|
import { useOrders, useOrderStats } from '../hooks/useOrders';
|
|
33
38
|
import { useFleetStats } from '../hooks/useFleet';
|
|
34
|
-
import { useLiveTracking } from '../hooks/useTracking';
|
|
39
|
+
import { useLiveTracking, useTrackingAlerts } from '../hooks/useTracking';
|
|
35
40
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
36
41
|
import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';
|
|
37
42
|
import { formatCurrency, formatDate } from '../utils/formatters';
|
|
38
43
|
import { joinPath } from '../utils/navigation';
|
|
39
44
|
|
|
45
|
+
/** Percentage delta across a real series (first non-zero → last). */
|
|
46
|
+
function seriesDelta(values: number[], invert = false): MetricDelta | undefined {
|
|
47
|
+
if (values.length < 2) return undefined;
|
|
48
|
+
const first = values.find((v) => v > 0);
|
|
49
|
+
const last = values[values.length - 1];
|
|
50
|
+
if (first == null || first === 0) return undefined;
|
|
51
|
+
const pct = ((last - first) / Math.abs(first)) * 100;
|
|
52
|
+
const direction = pct > 0.5 ? 'up' : pct < -0.5 ? 'down' : 'flat';
|
|
53
|
+
return { value: `${pct >= 0 ? '+' : ''}${pct.toFixed(1)}%`, direction, invert };
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
export default function DashboardPage() {
|
|
41
57
|
const { t: translate } = useI18n();
|
|
42
58
|
const t = (key: string, fallback: string): string => {
|
|
@@ -45,10 +61,69 @@ export default function DashboardPage() {
|
|
|
45
61
|
};
|
|
46
62
|
const { basePath, navigate } = useMoveTheWheels();
|
|
47
63
|
const { metrics } = useDashboardMetrics();
|
|
64
|
+
const { analytics } = useAnalytics();
|
|
48
65
|
const { orders } = useOrders({ limit: 5 });
|
|
49
66
|
const { stats: orderStats } = useOrderStats();
|
|
50
67
|
const { stats: fleetStats } = useFleetStats();
|
|
51
68
|
const { activeDeliveries } = useLiveTracking();
|
|
69
|
+
const { unacknowledgedAlerts } = useTrackingAlerts();
|
|
70
|
+
|
|
71
|
+
const orderSpark = useMemo(
|
|
72
|
+
() => analytics.trends.dailyOrders.map((d) => d.orders),
|
|
73
|
+
[analytics.trends.dailyOrders]
|
|
74
|
+
);
|
|
75
|
+
const deliverySpark = useMemo(
|
|
76
|
+
() => analytics.trends.dailyOrders.map((d) => d.deliveries),
|
|
77
|
+
[analytics.trends.dailyOrders]
|
|
78
|
+
);
|
|
79
|
+
const revenueSpark = useMemo(
|
|
80
|
+
() => analytics.trends.revenue.map((r) => r.revenue),
|
|
81
|
+
[analytics.trends.revenue]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Weekly order volume — last 7 real trend points (orders + deliveries).
|
|
85
|
+
const weeklyVolume = useMemo(
|
|
86
|
+
() => analytics.trends.dailyOrders.slice(-7),
|
|
87
|
+
[analytics.trends.dailyOrders]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Live map markers built from the best real coordinate per active delivery.
|
|
91
|
+
const mapMarkers = useMemo<LogisticsMarker[]>(
|
|
92
|
+
() =>
|
|
93
|
+
activeDeliveries.flatMap((d) => {
|
|
94
|
+
if (!d.order) return [];
|
|
95
|
+
const coord = firstRealLatLng(
|
|
96
|
+
d.vehicle?.currentLocation,
|
|
97
|
+
d.driver?.currentLocation,
|
|
98
|
+
d.tracking?.currentLocation
|
|
99
|
+
);
|
|
100
|
+
if (!coord) return [];
|
|
101
|
+
return [
|
|
102
|
+
{
|
|
103
|
+
id: d.order.id,
|
|
104
|
+
lat: coord.lat,
|
|
105
|
+
lng: coord.lng,
|
|
106
|
+
label: d.order.orderNumber,
|
|
107
|
+
status: deliveryMarkerStatus(d.order.status),
|
|
108
|
+
sublabel: d.driver?.fullName || d.vehicle?.vehicleNumber || undefined,
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
}),
|
|
112
|
+
[activeDeliveries]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Recent activity — the latest orders as a real, timestamped feed.
|
|
116
|
+
const recentActivity = useMemo(
|
|
117
|
+
() =>
|
|
118
|
+
[...orders]
|
|
119
|
+
.sort(
|
|
120
|
+
(a, b) =>
|
|
121
|
+
new Date(b.updatedAt ?? b.createdAt).getTime() -
|
|
122
|
+
new Date(a.updatedAt ?? a.createdAt).getTime()
|
|
123
|
+
)
|
|
124
|
+
.slice(0, 5),
|
|
125
|
+
[orders]
|
|
126
|
+
);
|
|
52
127
|
|
|
53
128
|
const goTo = (path: string) => {
|
|
54
129
|
if (navigate) {
|
|
@@ -94,27 +169,38 @@ export default function DashboardPage() {
|
|
|
94
169
|
)}
|
|
95
170
|
</PagePurpose>
|
|
96
171
|
|
|
97
|
-
{/* Key Metrics */}
|
|
172
|
+
{/* Key Metrics — premium control-tower stat cards with real trend deltas */}
|
|
98
173
|
<MetricGrid columns={4}>
|
|
99
174
|
<MetricCard
|
|
100
175
|
label={t('movethewheels.dashboard.metrics.totalOrders', 'Total Orders')}
|
|
101
176
|
value={metrics.orders.total}
|
|
102
177
|
icon={<Package size={20} />}
|
|
178
|
+
accent="brand"
|
|
179
|
+
sparklineData={orderSpark.length >= 2 ? orderSpark : undefined}
|
|
180
|
+
delta={seriesDelta(orderSpark)}
|
|
103
181
|
onClick={() => goTo('orders')}
|
|
104
182
|
/>
|
|
105
183
|
<MetricCard
|
|
106
184
|
label={t('movethewheels.dashboard.metrics.activeDeliveries', 'Active Deliveries')}
|
|
107
185
|
value={metrics.orders.inTransit}
|
|
108
186
|
icon={<Truck size={20} />}
|
|
187
|
+
accent="blue"
|
|
109
188
|
subtitle={`${metrics.orders.pending} ${t('movethewheels.dashboard.metrics.pending', 'pending')}`}
|
|
189
|
+
sparklineData={deliverySpark.length >= 2 ? deliverySpark : undefined}
|
|
190
|
+
delta={seriesDelta(deliverySpark)}
|
|
110
191
|
onClick={() => goTo('tracking')}
|
|
111
192
|
/>
|
|
112
193
|
<MetricCard
|
|
113
|
-
label={t('movethewheels.dashboard.metrics.
|
|
114
|
-
value={`${metrics.
|
|
115
|
-
icon={<
|
|
116
|
-
|
|
117
|
-
|
|
194
|
+
label={t('movethewheels.dashboard.metrics.onTimeDelivery', 'On-Time Delivery')}
|
|
195
|
+
value={`${metrics.performance.onTimeDeliveryRate.toFixed(1)}%`}
|
|
196
|
+
icon={<TrendingUp size={20} />}
|
|
197
|
+
accent="success"
|
|
198
|
+
delta={
|
|
199
|
+
metrics.performance.onTimeDeliveryRate >= 90
|
|
200
|
+
? { value: 'On target', direction: 'up' }
|
|
201
|
+
: { value: 'Below 90%', direction: 'down' }
|
|
202
|
+
}
|
|
203
|
+
onClick={() => goTo('analytics')}
|
|
118
204
|
/>
|
|
119
205
|
<MetricCard
|
|
120
206
|
treatment="glass"
|
|
@@ -122,17 +208,157 @@ export default function DashboardPage() {
|
|
|
122
208
|
label={t('movethewheels.dashboard.metrics.revenue', 'Revenue')}
|
|
123
209
|
value={formatCurrency(metrics.revenue.total)}
|
|
124
210
|
icon={<DollarSign size={20} />}
|
|
211
|
+
accent="success"
|
|
212
|
+
sparklineData={revenueSpark.length >= 2 ? revenueSpark : undefined}
|
|
213
|
+
delta={seriesDelta(revenueSpark)}
|
|
125
214
|
onClick={() => goTo('analytics')}
|
|
126
215
|
/>
|
|
127
216
|
</MetricGrid>
|
|
128
217
|
|
|
218
|
+
{/* Control Tower — live map + exceptions + recent activity */}
|
|
219
|
+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
220
|
+
<Card className="overflow-hidden lg:col-span-2">
|
|
221
|
+
<CardHeader className="flex flex-row items-center justify-between">
|
|
222
|
+
<CardTitle className="flex items-center gap-2">
|
|
223
|
+
<MapPin size={18} /> {t('movethewheels.dashboard.liveMap.title', 'Live Map')}
|
|
224
|
+
</CardTitle>
|
|
225
|
+
<Button variant="ghost" size="sm" onClick={() => goTo('tracking/live')}>
|
|
226
|
+
{t('movethewheels.dashboard.liveMap.open', 'Open live tracking')}
|
|
227
|
+
<ChevronRight size={16} />
|
|
228
|
+
</Button>
|
|
229
|
+
</CardHeader>
|
|
230
|
+
<CardContent className="p-0">
|
|
231
|
+
<LogisticsMap
|
|
232
|
+
markers={mapMarkers}
|
|
233
|
+
height={460}
|
|
234
|
+
emptyMessage={
|
|
235
|
+
activeDeliveries.length === 0
|
|
236
|
+
? t('movethewheels.dashboard.liveMap.noActive', 'No active deliveries right now')
|
|
237
|
+
: t(
|
|
238
|
+
'movethewheels.dashboard.liveMap.awaitingGps',
|
|
239
|
+
'Awaiting live GPS positions for active deliveries'
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
/>
|
|
243
|
+
</CardContent>
|
|
244
|
+
</Card>
|
|
245
|
+
|
|
246
|
+
<div className="space-y-6">
|
|
247
|
+
{/* Exception Alerts */}
|
|
248
|
+
<Card>
|
|
249
|
+
<CardHeader>
|
|
250
|
+
<CardTitle className="flex items-center gap-2">
|
|
251
|
+
<AlertTriangle size={18} className="text-status-warning-text" />
|
|
252
|
+
{t('movethewheels.dashboard.exceptions.title', 'Exception Alerts')}
|
|
253
|
+
{unacknowledgedAlerts.length > 0 && (
|
|
254
|
+
<Badge variant="destructive" className="ml-auto">
|
|
255
|
+
{unacknowledgedAlerts.length}
|
|
256
|
+
</Badge>
|
|
257
|
+
)}
|
|
258
|
+
</CardTitle>
|
|
259
|
+
</CardHeader>
|
|
260
|
+
<CardContent>
|
|
261
|
+
{unacknowledgedAlerts.length > 0 ? (
|
|
262
|
+
<div className="space-y-3">
|
|
263
|
+
{unacknowledgedAlerts.slice(0, 4).map((alert) => (
|
|
264
|
+
<div
|
|
265
|
+
key={alert.id}
|
|
266
|
+
className={`rounded-lg p-3 text-sm ${
|
|
267
|
+
alert.severity === 'critical' || alert.severity === 'high'
|
|
268
|
+
? 'bg-status-error-bg-subtle'
|
|
269
|
+
: 'bg-status-warning-bg-subtle'
|
|
270
|
+
}`}
|
|
271
|
+
>
|
|
272
|
+
<p className="font-medium">{alert.message}</p>
|
|
273
|
+
<p className="mt-1 text-xs text-text-secondary">
|
|
274
|
+
{formatDate(alert.timestamp, 'relative')}
|
|
275
|
+
</p>
|
|
276
|
+
</div>
|
|
277
|
+
))}
|
|
278
|
+
</div>
|
|
279
|
+
) : (
|
|
280
|
+
<p className="py-4 text-center text-sm text-text-secondary">
|
|
281
|
+
{t(
|
|
282
|
+
'movethewheels.dashboard.exceptions.none',
|
|
283
|
+
'No exceptions — all deliveries on track'
|
|
284
|
+
)}
|
|
285
|
+
</p>
|
|
286
|
+
)}
|
|
287
|
+
</CardContent>
|
|
288
|
+
</Card>
|
|
289
|
+
|
|
290
|
+
{/* Recent Activity */}
|
|
291
|
+
<Card>
|
|
292
|
+
<CardHeader>
|
|
293
|
+
<CardTitle className="flex items-center gap-2">
|
|
294
|
+
<Activity size={18} />
|
|
295
|
+
{t('movethewheels.dashboard.activity.title', 'Recent Activity')}
|
|
296
|
+
</CardTitle>
|
|
297
|
+
</CardHeader>
|
|
298
|
+
<CardContent>
|
|
299
|
+
{recentActivity.length > 0 ? (
|
|
300
|
+
<ul className="space-y-3">
|
|
301
|
+
{recentActivity.map((order) => (
|
|
302
|
+
<li key={order.id} className="flex items-start gap-3">
|
|
303
|
+
<span
|
|
304
|
+
aria-hidden
|
|
305
|
+
className="mt-1.5 h-2 w-2 shrink-0 rounded-full bg-action-primary-bg"
|
|
306
|
+
/>
|
|
307
|
+
<div className="min-w-0 flex-1">
|
|
308
|
+
<p className="truncate text-sm">
|
|
309
|
+
<span className="font-medium">{order.orderNumber}</span>{' '}
|
|
310
|
+
<span className="text-text-secondary capitalize">
|
|
311
|
+
{order.status.replace(/_/g, ' ')}
|
|
312
|
+
</span>
|
|
313
|
+
</p>
|
|
314
|
+
<p className="text-xs text-text-secondary">
|
|
315
|
+
{formatDate(order.updatedAt ?? order.createdAt, 'relative')}
|
|
316
|
+
</p>
|
|
317
|
+
</div>
|
|
318
|
+
</li>
|
|
319
|
+
))}
|
|
320
|
+
</ul>
|
|
321
|
+
) : (
|
|
322
|
+
<p className="py-4 text-center text-sm text-text-secondary">
|
|
323
|
+
{t('movethewheels.dashboard.activity.none', 'No recent activity yet')}
|
|
324
|
+
</p>
|
|
325
|
+
)}
|
|
326
|
+
</CardContent>
|
|
327
|
+
</Card>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
{/* Weekly order volume */}
|
|
332
|
+
<Card>
|
|
333
|
+
<CardHeader>
|
|
334
|
+
<CardTitle>
|
|
335
|
+
{t('movethewheels.dashboard.weeklyVolume.title', 'Weekly order volume')}
|
|
336
|
+
</CardTitle>
|
|
337
|
+
</CardHeader>
|
|
338
|
+
<CardContent>
|
|
339
|
+
<VolumeBar
|
|
340
|
+
xKey="date"
|
|
341
|
+
height={240}
|
|
342
|
+
data={weeklyVolume}
|
|
343
|
+
series={[
|
|
344
|
+
{ dataKey: 'orders', name: 'Orders' },
|
|
345
|
+
{ dataKey: 'deliveries', name: 'Deliveries' },
|
|
346
|
+
]}
|
|
347
|
+
emptyMessage={t(
|
|
348
|
+
'movethewheels.dashboard.weeklyVolume.empty',
|
|
349
|
+
'No order volume recorded yet'
|
|
350
|
+
)}
|
|
351
|
+
/>
|
|
352
|
+
</CardContent>
|
|
353
|
+
</Card>
|
|
354
|
+
|
|
129
355
|
{/* Quick Stats Row */}
|
|
130
356
|
<MetricGrid columns={4}>
|
|
131
357
|
<MetricCard
|
|
132
358
|
size="sm"
|
|
133
|
-
label={t('movethewheels.dashboard.
|
|
134
|
-
value={`${metrics.
|
|
135
|
-
|
|
359
|
+
label={t('movethewheels.dashboard.metrics.fleetUtilization', 'Fleet Utilization')}
|
|
360
|
+
value={`${metrics.fleet.activeVehicles}/${metrics.fleet.totalVehicles}`}
|
|
361
|
+
subtitle={`${metrics.fleet.availableDrivers} ${t('movethewheels.dashboard.metrics.driversAvailable', 'drivers available')}`}
|
|
136
362
|
/>
|
|
137
363
|
<MetricCard
|
|
138
364
|
size="sm"
|
|
@@ -374,7 +374,7 @@ export default function DriverDetailsPage() {
|
|
|
374
374
|
{formatCurrency(driver.earnings.totalEarnings)}
|
|
375
375
|
</p>
|
|
376
376
|
</div>
|
|
377
|
-
<div className="grid grid-cols-2 gap-3">
|
|
377
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
378
378
|
<div>
|
|
379
379
|
<p className="text-sm text-text-secondary">This Week</p>
|
|
380
380
|
<p className="font-medium">{formatCurrency(driver.earnings.weeklyEarnings)}</p>
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useState } from 'react';
|
|
6
|
-
import { Users, Plus, RefreshCw, Download, Star } from 'lucide-react';
|
|
6
|
+
import { Users, Plus, RefreshCw, Download, Star, UserCheck, Coffee, UserMinus } from 'lucide-react';
|
|
7
7
|
import { PageLayout } from '../components/PageLayout';
|
|
8
|
+
import { MetricCard, MetricGrid } from '../components/MetricCard';
|
|
8
9
|
import { DataTable, type Column } from '../components/DataTable';
|
|
9
10
|
import { StatusBadge } from '../components/StatusBadge';
|
|
10
11
|
import { Avatar, AvatarFallback } from '../components/ui';
|
|
11
|
-
import { CTAOverflowMenu,
|
|
12
|
+
import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
12
13
|
import { useDrivers, useFleetStats } from '../hooks/useFleet';
|
|
13
14
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
14
15
|
import { formatDate } from '../utils/formatters';
|
|
@@ -123,28 +124,38 @@ export default function DriversPage() {
|
|
|
123
124
|
assigning them to an order or route.
|
|
124
125
|
</PagePurpose>
|
|
125
126
|
|
|
126
|
-
{/* Workforce at a glance —
|
|
127
|
-
<
|
|
128
|
-
<p className="text-sm font-medium text-text-secondary">Workforce at a glance</p>
|
|
129
|
-
<
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
127
|
+
{/* Workforce at a glance — premium stat cards */}
|
|
128
|
+
<div>
|
|
129
|
+
<p className="mb-3 text-sm font-medium text-text-secondary">Workforce at a glance</p>
|
|
130
|
+
<MetricGrid columns={4}>
|
|
131
|
+
<MetricCard
|
|
132
|
+
label="On duty"
|
|
133
|
+
value={stats.driversByStatus.available + stats.driversByStatus.busy}
|
|
134
|
+
icon={<UserCheck size={20} />}
|
|
135
|
+
accent="success"
|
|
136
|
+
subtitle="Available or delivering"
|
|
137
|
+
/>
|
|
138
|
+
<MetricCard
|
|
139
|
+
label="On break"
|
|
140
|
+
value={stats.driversByStatus.on_break}
|
|
141
|
+
icon={<Coffee size={20} />}
|
|
142
|
+
accent="warning"
|
|
143
|
+
/>
|
|
144
|
+
<MetricCard
|
|
145
|
+
label="Off duty"
|
|
146
|
+
value={stats.driversByStatus.offline}
|
|
147
|
+
icon={<UserMinus size={20} />}
|
|
148
|
+
accent="neutral"
|
|
149
|
+
/>
|
|
150
|
+
<MetricCard
|
|
151
|
+
label="Avg rating"
|
|
152
|
+
value={stats.avgDriverRating > 0 ? stats.avgDriverRating.toFixed(1) : '—'}
|
|
153
|
+
icon={<Star size={20} />}
|
|
154
|
+
accent="brand"
|
|
155
|
+
subtitle={`${totalDrivers} drivers`}
|
|
156
|
+
/>
|
|
157
|
+
</MetricGrid>
|
|
158
|
+
</div>
|
|
148
159
|
|
|
149
160
|
<DataTable
|
|
150
161
|
columns={columns}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Finance Page
|
|
3
3
|
*
|
|
4
|
-
* Shows only REAL figures returned by Query.financeMetrics (revenue,
|
|
5
|
-
* open invoices)
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Shows only REAL figures returned by Query.financeMetrics (revenue, delivered
|
|
5
|
+
* revenue, fuel cost, open invoices) plus the real revenue trend series — no
|
|
6
|
+
* simulated paid/pending splits and no fabricated cost categories. Invoicing and
|
|
7
|
+
* settlement are handled by the platform Billing module, which this page
|
|
8
|
+
* deep-links to rather than re-implementing an Invoice ledger here.
|
|
8
9
|
*/
|
|
9
10
|
|
|
11
|
+
import { useMemo } from 'react';
|
|
10
12
|
import { DollarSign, TrendingUp, CreditCard, FileText, Fuel, ArrowRight } from 'lucide-react';
|
|
11
13
|
import { PageLayout } from '../components/PageLayout';
|
|
12
14
|
import { MetricCard, MetricGrid } from '../components/MetricCard';
|
|
13
15
|
import { Card, CardContent, CardHeader, CardTitle, Button } from '../components/ui';
|
|
16
|
+
import { TrendChart, VolumeBar, chartTokens } from '../components/charts';
|
|
14
17
|
import { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
15
18
|
import { useAnalytics, useFinanceMetrics } from '../hooks/useAnalytics';
|
|
16
19
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
@@ -24,6 +27,24 @@ export default function FinancePage() {
|
|
|
24
27
|
// Deep-link to the host Billing module (mounted at /billing in the app shell).
|
|
25
28
|
const openBilling = () => navigate('/billing');
|
|
26
29
|
|
|
30
|
+
const revenueSeries = useMemo(
|
|
31
|
+
() => analytics.trends.revenue.map((r) => ({ month: r.month, revenue: r.revenue })),
|
|
32
|
+
[analytics.trends.revenue]
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Real dollar figures only — total revenue, money already collected on delivered
|
|
36
|
+
// orders, and fuel spend. No invented category breakdown.
|
|
37
|
+
const economics = useMemo(
|
|
38
|
+
() => [
|
|
39
|
+
{ label: 'Total revenue', amount: metrics.totalRevenue },
|
|
40
|
+
{ label: 'Collected', amount: metrics.deliveredRevenue },
|
|
41
|
+
{ label: 'Fuel cost', amount: metrics.fuelCost },
|
|
42
|
+
],
|
|
43
|
+
[metrics.totalRevenue, metrics.deliveredRevenue, metrics.fuelCost]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const hasEconomics = economics.some((e) => e.amount > 0);
|
|
47
|
+
|
|
27
48
|
return (
|
|
28
49
|
<PageLayout
|
|
29
50
|
title="Finance"
|
|
@@ -31,7 +52,7 @@ export default function FinancePage() {
|
|
|
31
52
|
icon={<DollarSign size={24} />}
|
|
32
53
|
headerActions={
|
|
33
54
|
<Button variant="outline" onClick={openBilling}>
|
|
34
|
-
<FileText size={16} /> Invoices & Billing
|
|
55
|
+
<FileText size={16} /> Invoices & Billing
|
|
35
56
|
</Button>
|
|
36
57
|
}
|
|
37
58
|
>
|
|
@@ -48,59 +69,54 @@ export default function FinancePage() {
|
|
|
48
69
|
label="Total Revenue"
|
|
49
70
|
value={formatCurrency(metrics.totalRevenue)}
|
|
50
71
|
icon={<DollarSign size={20} />}
|
|
72
|
+
accent="success"
|
|
51
73
|
/>
|
|
52
74
|
<MetricCard
|
|
53
75
|
label="Collected (Delivered)"
|
|
54
76
|
value={formatCurrency(metrics.deliveredRevenue)}
|
|
55
77
|
icon={<TrendingUp size={20} />}
|
|
56
|
-
|
|
78
|
+
accent="brand"
|
|
57
79
|
/>
|
|
58
80
|
<MetricCard
|
|
59
81
|
label="Fuel Cost"
|
|
60
82
|
value={formatCurrency(metrics.fuelCost)}
|
|
61
83
|
icon={<Fuel size={20} />}
|
|
62
|
-
|
|
84
|
+
accent="warning"
|
|
63
85
|
/>
|
|
64
86
|
<MetricCard
|
|
65
87
|
label="Open Invoices"
|
|
66
88
|
value={String(metrics.openInvoices)}
|
|
67
89
|
icon={<CreditCard size={20} />}
|
|
90
|
+
accent="blue"
|
|
68
91
|
/>
|
|
69
92
|
</MetricGrid>
|
|
70
93
|
|
|
71
|
-
<div className="grid grid-cols-1 lg:grid-cols-
|
|
72
|
-
<Card>
|
|
94
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
95
|
+
<Card className="lg:col-span-2">
|
|
73
96
|
<CardHeader>
|
|
74
|
-
<CardTitle>Revenue
|
|
97
|
+
<CardTitle>Revenue trend</CardTitle>
|
|
75
98
|
</CardHeader>
|
|
76
99
|
<CardContent>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
))}
|
|
88
|
-
</div>
|
|
89
|
-
) : (
|
|
90
|
-
<p className="text-text-secondary text-center py-6">
|
|
91
|
-
No revenue recorded yet. Revenue appears here as orders are delivered.
|
|
92
|
-
</p>
|
|
93
|
-
)}
|
|
100
|
+
<TrendChart
|
|
101
|
+
xKey="month"
|
|
102
|
+
variant="area"
|
|
103
|
+
height={280}
|
|
104
|
+
showLegend={false}
|
|
105
|
+
data={revenueSeries}
|
|
106
|
+
series={[{ dataKey: 'revenue', name: 'Revenue', color: chartTokens.brandBlue }]}
|
|
107
|
+
valueFormatter={(v) => formatCurrency(Number(v))}
|
|
108
|
+
emptyMessage="No revenue recorded yet. Revenue appears here as orders are delivered."
|
|
109
|
+
/>
|
|
94
110
|
</CardContent>
|
|
95
111
|
</Card>
|
|
96
112
|
|
|
97
113
|
{/* Invoices & Settlement — reuse of the Billing module */}
|
|
98
114
|
<EmphasisPanel className="space-y-4">
|
|
99
|
-
<h2 className="text-base font-semibold text-text-primary">Invoices & Settlement</h2>
|
|
115
|
+
<h2 className="text-base font-semibold text-text-primary">Invoices & Settlement</h2>
|
|
100
116
|
<p className="text-sm text-text-secondary">
|
|
101
117
|
Invoicing, payments and settlement run through the platform Billing module — the single
|
|
102
|
-
source of truth for what's been billed and paid. MoveTheWheels doesn't keep
|
|
103
|
-
own invoice ledger.
|
|
118
|
+
source of truth for what's been billed and paid. MoveTheWheels doesn't keep
|
|
119
|
+
its own invoice ledger.
|
|
104
120
|
</p>
|
|
105
121
|
<div className="flex items-center justify-between p-4 bg-status-warning-bg/10 rounded-lg">
|
|
106
122
|
<div className="flex items-center gap-3">
|
|
@@ -123,6 +139,28 @@ export default function FinancePage() {
|
|
|
123
139
|
</Button>
|
|
124
140
|
</EmphasisPanel>
|
|
125
141
|
</div>
|
|
142
|
+
|
|
143
|
+
<Card>
|
|
144
|
+
<CardHeader>
|
|
145
|
+
<CardTitle>Revenue vs cost</CardTitle>
|
|
146
|
+
</CardHeader>
|
|
147
|
+
<CardContent>
|
|
148
|
+
{hasEconomics ? (
|
|
149
|
+
<VolumeBar
|
|
150
|
+
xKey="label"
|
|
151
|
+
height={260}
|
|
152
|
+
showLegend={false}
|
|
153
|
+
data={economics}
|
|
154
|
+
series={[{ dataKey: 'amount', name: 'Amount' }]}
|
|
155
|
+
valueFormatter={(v) => formatCurrency(Number(v))}
|
|
156
|
+
/>
|
|
157
|
+
) : (
|
|
158
|
+
<p className="py-8 text-center text-sm text-text-secondary">
|
|
159
|
+
Revenue and cost figures appear here as orders are delivered.
|
|
160
|
+
</p>
|
|
161
|
+
)}
|
|
162
|
+
</CardContent>
|
|
163
|
+
</Card>
|
|
126
164
|
</PageLayout>
|
|
127
165
|
);
|
|
128
166
|
}
|