@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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { useState } from 'react';
|
|
6
6
|
import { useParams } from 'react-router-dom';
|
|
7
|
-
import { Truck, ArrowLeft, MapPin, Fuel, Wrench,
|
|
7
|
+
import { Truck, ArrowLeft, MapPin, Fuel, Wrench, Edit, Save } from 'lucide-react';
|
|
8
8
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
9
9
|
import { StatusBadge, VehicleTypeBadge } from '../components/StatusBadge';
|
|
10
10
|
import {
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
CardHeader,
|
|
14
14
|
CardTitle,
|
|
15
15
|
Button,
|
|
16
|
-
Badge,
|
|
17
16
|
Progress,
|
|
18
17
|
Input,
|
|
19
18
|
Label,
|
|
@@ -29,6 +28,7 @@ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
|
29
28
|
import { formatDate } from '../utils/formatters';
|
|
30
29
|
import { joinPath } from '../utils/navigation';
|
|
31
30
|
import { FormError } from '../components/FormError';
|
|
31
|
+
import { DocumentsCard } from '../components/DocumentsCard';
|
|
32
32
|
|
|
33
33
|
const VEHICLE_TYPE_OPTIONS = [
|
|
34
34
|
{ value: 'TRUCK', label: 'Truck' },
|
|
@@ -266,7 +266,7 @@ export default function VehicleDetailsPage() {
|
|
|
266
266
|
<CardTitle>Capacity</CardTitle>
|
|
267
267
|
</CardHeader>
|
|
268
268
|
<CardContent>
|
|
269
|
-
<div className="grid grid-cols-3 gap-6">
|
|
269
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
|
270
270
|
<div>
|
|
271
271
|
<p className="text-sm text-text-secondary">Weight</p>
|
|
272
272
|
<p className="text-2xl font-bold">
|
|
@@ -291,37 +291,7 @@ export default function VehicleDetailsPage() {
|
|
|
291
291
|
</CardContent>
|
|
292
292
|
</Card>
|
|
293
293
|
|
|
294
|
-
<
|
|
295
|
-
<CardHeader>
|
|
296
|
-
<CardTitle className="flex items-center gap-2">
|
|
297
|
-
<FileText size={18} /> Documents
|
|
298
|
-
</CardTitle>
|
|
299
|
-
</CardHeader>
|
|
300
|
-
<CardContent>
|
|
301
|
-
{vehicle.documents.length > 0 ? (
|
|
302
|
-
<div className="space-y-3">
|
|
303
|
-
{vehicle.documents.map((doc) => (
|
|
304
|
-
<div
|
|
305
|
-
key={doc.id}
|
|
306
|
-
className="flex items-center justify-between p-3 border rounded-lg"
|
|
307
|
-
>
|
|
308
|
-
<div>
|
|
309
|
-
<p className="font-medium capitalize">{doc.type.replace('_', ' ')}</p>
|
|
310
|
-
<p className="text-sm text-text-secondary">
|
|
311
|
-
Expires: {formatDate(doc.expiryDate)}
|
|
312
|
-
</p>
|
|
313
|
-
</div>
|
|
314
|
-
<Badge variant={doc.verified ? 'default' : 'secondary'}>
|
|
315
|
-
{doc.verified ? 'Verified' : 'Pending'}
|
|
316
|
-
</Badge>
|
|
317
|
-
</div>
|
|
318
|
-
))}
|
|
319
|
-
</div>
|
|
320
|
-
) : (
|
|
321
|
-
<p className="text-text-secondary text-center py-4">No documents uploaded</p>
|
|
322
|
-
)}
|
|
323
|
-
</CardContent>
|
|
324
|
-
</Card>
|
|
294
|
+
<DocumentsCard entityType="vehicle" entityId={vehicle.id} />
|
|
325
295
|
</div>
|
|
326
296
|
|
|
327
297
|
<div className="space-y-6">
|
|
@@ -2,20 +2,30 @@
|
|
|
2
2
|
* Vehicles Page - List of all vehicles
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { Truck, Plus, RefreshCw, Download, CircleCheck, Route as RouteIcon, Wrench } from 'lucide-react';
|
|
6
7
|
import { PageLayout } from '../components/PageLayout';
|
|
8
|
+
import { MetricCard, MetricGrid } from '../components/MetricCard';
|
|
7
9
|
import { DataTable, type Column } from '../components/DataTable';
|
|
8
10
|
import { StatusBadge, VehicleTypeBadge } from '../components/StatusBadge';
|
|
9
11
|
import { Progress } from '../components/ui';
|
|
10
|
-
import { CTAOverflowMenu,
|
|
11
|
-
import { useVehicles } from '../hooks/useFleet';
|
|
12
|
+
import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
13
|
+
import { useVehicles, useFleetStats } from '../hooks/useFleet';
|
|
12
14
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
13
15
|
import { joinPath } from '../utils/navigation';
|
|
14
16
|
import type { Vehicle } from '../types';
|
|
15
17
|
|
|
16
18
|
export default function VehiclesPage() {
|
|
17
19
|
const { basePath, navigate } = useMoveTheWheels();
|
|
18
|
-
const
|
|
20
|
+
const [page, setPage] = useState(0);
|
|
21
|
+
const [pageSize, setPageSize] = useState(25);
|
|
22
|
+
const { vehicles, isLoading, refetch } = useVehicles({
|
|
23
|
+
limit: pageSize,
|
|
24
|
+
offset: page * pageSize,
|
|
25
|
+
});
|
|
26
|
+
// Fleet totals + tiles from the live aggregate, not the current page.
|
|
27
|
+
const { stats } = useFleetStats();
|
|
28
|
+
const totalVehicles = stats.totalVehicles;
|
|
19
29
|
|
|
20
30
|
const goTo = (path: string) => {
|
|
21
31
|
if (navigate) {
|
|
@@ -102,7 +112,11 @@ export default function VehiclesPage() {
|
|
|
102
112
|
}}
|
|
103
113
|
actions={[
|
|
104
114
|
{ label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
|
|
105
|
-
{
|
|
115
|
+
{
|
|
116
|
+
label: 'Export',
|
|
117
|
+
icon: <Download size={16} />,
|
|
118
|
+
onSelect: () => goTo('import-export'),
|
|
119
|
+
},
|
|
106
120
|
]}
|
|
107
121
|
/>
|
|
108
122
|
}
|
|
@@ -113,28 +127,37 @@ export default function VehiclesPage() {
|
|
|
113
127
|
an order.
|
|
114
128
|
</PagePurpose>
|
|
115
129
|
|
|
116
|
-
{/* Fleet at a glance —
|
|
117
|
-
<
|
|
118
|
-
<p className="text-sm font-medium text-text-secondary">Fleet at a glance</p>
|
|
119
|
-
<
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
{/* Fleet at a glance — premium stat cards */}
|
|
131
|
+
<div>
|
|
132
|
+
<p className="mb-3 text-sm font-medium text-text-secondary">Fleet at a glance</p>
|
|
133
|
+
<MetricGrid columns={4}>
|
|
134
|
+
<MetricCard
|
|
135
|
+
label="Total vehicles"
|
|
136
|
+
value={totalVehicles}
|
|
137
|
+
icon={<Truck size={20} />}
|
|
138
|
+
accent="brand"
|
|
139
|
+
/>
|
|
140
|
+
<MetricCard
|
|
141
|
+
label="Available now"
|
|
142
|
+
value={stats.vehiclesByStatus.available}
|
|
143
|
+
icon={<CircleCheck size={20} />}
|
|
144
|
+
accent="success"
|
|
145
|
+
subtitle="Ready to dispatch"
|
|
146
|
+
/>
|
|
147
|
+
<MetricCard
|
|
148
|
+
label="On assignment"
|
|
149
|
+
value={stats.vehiclesByStatus.assigned}
|
|
150
|
+
icon={<RouteIcon size={20} />}
|
|
151
|
+
accent="blue"
|
|
152
|
+
/>
|
|
153
|
+
<MetricCard
|
|
154
|
+
label="In maintenance"
|
|
155
|
+
value={stats.vehiclesByStatus.maintenance}
|
|
156
|
+
icon={<Wrench size={20} />}
|
|
157
|
+
accent="warning"
|
|
158
|
+
/>
|
|
159
|
+
</MetricGrid>
|
|
160
|
+
</div>
|
|
138
161
|
|
|
139
162
|
<DataTable
|
|
140
163
|
columns={columns}
|
|
@@ -145,6 +168,14 @@ export default function VehiclesPage() {
|
|
|
145
168
|
searchFields={['vehicleNumber', 'make', 'model'] as (keyof Vehicle)[]}
|
|
146
169
|
onRowClick={(row) => goTo(`fleet/vehicles/${row.id}`)}
|
|
147
170
|
getRowId={(row) => row.id}
|
|
171
|
+
pageIndex={page}
|
|
172
|
+
pageSize={pageSize}
|
|
173
|
+
onPageChange={setPage}
|
|
174
|
+
onPageSizeChange={(size) => {
|
|
175
|
+
setPageSize(size);
|
|
176
|
+
setPage(0);
|
|
177
|
+
}}
|
|
178
|
+
totalItems={totalVehicles}
|
|
148
179
|
emptyIllustration="empty-data"
|
|
149
180
|
emptyTitle="No vehicles yet"
|
|
150
181
|
emptyDescription="Add vehicles to your fleet to start assigning them to deliveries."
|
|
@@ -326,7 +326,7 @@ export default function WarehouseDetailsPage() {
|
|
|
326
326
|
{warehouse.capacity.totalSpace.toLocaleString()} sq ft
|
|
327
327
|
</p>
|
|
328
328
|
</div>
|
|
329
|
-
<div className="grid grid-cols-2 gap-4">
|
|
329
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
330
330
|
<div>
|
|
331
331
|
<p className="text-sm text-text-secondary">Pallets Used</p>
|
|
332
332
|
<p className="text-2xl font-bold">
|
|
@@ -2,19 +2,28 @@
|
|
|
2
2
|
* Warehouses Page
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { Warehouse, Plus, RefreshCw, CircleCheck, Gauge } from 'lucide-react';
|
|
6
7
|
import { PageLayout } from '../components/PageLayout';
|
|
8
|
+
import { MetricCard, MetricGrid } from '../components/MetricCard';
|
|
7
9
|
import { DataTable, type Column } from '../components/DataTable';
|
|
8
10
|
import { Badge, Progress } from '../components/ui';
|
|
9
|
-
import { CTAOverflowMenu,
|
|
10
|
-
import { useWarehouses } from '../hooks/useInventory';
|
|
11
|
+
import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
12
|
+
import { useWarehouses, useWarehouseStats } from '../hooks/useInventory';
|
|
11
13
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
12
14
|
import { joinPath } from '../utils/navigation';
|
|
13
15
|
import type { Warehouse as WarehouseType } from '../types';
|
|
14
16
|
|
|
15
17
|
export default function WarehousesPage() {
|
|
16
18
|
const { basePath, navigate } = useMoveTheWheels();
|
|
17
|
-
const
|
|
19
|
+
const [page, setPage] = useState(0);
|
|
20
|
+
const [pageSize, setPageSize] = useState(25);
|
|
21
|
+
const { warehouses, hasMore, isLoading, refetch } = useWarehouses({
|
|
22
|
+
limit: pageSize,
|
|
23
|
+
offset: page * pageSize,
|
|
24
|
+
});
|
|
25
|
+
// Bounded aggregate for the tiles + subtitle — not the truncated page.
|
|
26
|
+
const { stats } = useWarehouseStats();
|
|
18
27
|
|
|
19
28
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
20
29
|
|
|
@@ -95,7 +104,7 @@ export default function WarehousesPage() {
|
|
|
95
104
|
return (
|
|
96
105
|
<PageLayout
|
|
97
106
|
title="Warehouses"
|
|
98
|
-
subtitle={`${
|
|
107
|
+
subtitle={`${stats.total} warehouses`}
|
|
99
108
|
icon={<Warehouse size={24} />}
|
|
100
109
|
headerActions={
|
|
101
110
|
<CTAOverflowMenu
|
|
@@ -113,37 +122,31 @@ export default function WarehousesPage() {
|
|
|
113
122
|
to balance stock, spot capacity crunches, and decide where to fulfil an order from.
|
|
114
123
|
</PagePurpose>
|
|
115
124
|
|
|
116
|
-
{/* Network at a glance —
|
|
117
|
-
<
|
|
118
|
-
<p className="text-sm font-medium text-text-secondary">Network at a glance</p>
|
|
119
|
-
<
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
: '—'}
|
|
142
|
-
</p>
|
|
143
|
-
<p className="text-xs text-text-secondary">Avg utilization</p>
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
</EmphasisPanel>
|
|
125
|
+
{/* Network at a glance — premium stat cards */}
|
|
126
|
+
<div>
|
|
127
|
+
<p className="mb-3 text-sm font-medium text-text-secondary">Network at a glance</p>
|
|
128
|
+
<MetricGrid columns={3}>
|
|
129
|
+
<MetricCard
|
|
130
|
+
label="Total sites"
|
|
131
|
+
value={stats.total}
|
|
132
|
+
icon={<Warehouse size={20} />}
|
|
133
|
+
accent="brand"
|
|
134
|
+
/>
|
|
135
|
+
<MetricCard
|
|
136
|
+
label="Active"
|
|
137
|
+
value={stats.active}
|
|
138
|
+
icon={<CircleCheck size={20} />}
|
|
139
|
+
accent="success"
|
|
140
|
+
subtitle="Fulfilling orders"
|
|
141
|
+
/>
|
|
142
|
+
<MetricCard
|
|
143
|
+
label="Avg utilization"
|
|
144
|
+
value={stats.total > 0 ? `${Math.round(stats.avgUtilization * 100)}%` : '—'}
|
|
145
|
+
icon={<Gauge size={20} />}
|
|
146
|
+
accent="blue"
|
|
147
|
+
/>
|
|
148
|
+
</MetricGrid>
|
|
149
|
+
</div>
|
|
147
150
|
|
|
148
151
|
<DataTable
|
|
149
152
|
columns={columns}
|
|
@@ -154,6 +157,14 @@ export default function WarehousesPage() {
|
|
|
154
157
|
searchFields={['name', 'code'] as (keyof WarehouseType)[]}
|
|
155
158
|
onRowClick={(row) => goTo(`inventory/warehouses/${row.id}`)}
|
|
156
159
|
getRowId={(row) => row.id}
|
|
160
|
+
pageIndex={page}
|
|
161
|
+
pageSize={pageSize}
|
|
162
|
+
onPageChange={setPage}
|
|
163
|
+
onPageSizeChange={(size) => {
|
|
164
|
+
setPageSize(size);
|
|
165
|
+
setPage(0);
|
|
166
|
+
}}
|
|
167
|
+
hasNextPage={hasMore}
|
|
157
168
|
emptyIllustration="empty-data"
|
|
158
169
|
emptyTitle="No warehouses yet"
|
|
159
170
|
emptyDescription="Add a warehouse to store inventory and fulfil orders from it."
|
|
@@ -526,6 +526,8 @@ export interface Route {
|
|
|
526
526
|
stops: RouteStop[];
|
|
527
527
|
distance: number;
|
|
528
528
|
estimatedDuration: number;
|
|
529
|
+
/** Estimated fuel cost for the route (svc MtwRoute.fuelCost). */
|
|
530
|
+
fuelCost?: number;
|
|
529
531
|
actualDuration?: number;
|
|
530
532
|
startTime?: Date;
|
|
531
533
|
endTime?: Date;
|
|
@@ -689,7 +691,7 @@ export interface Analytics {
|
|
|
689
691
|
customerSatisfaction: number;
|
|
690
692
|
};
|
|
691
693
|
trends: {
|
|
692
|
-
dailyOrders: Array<{ date: string; orders: number }>;
|
|
694
|
+
dailyOrders: Array<{ date: string; orders: number; deliveries: number }>;
|
|
693
695
|
revenue: Array<{ month: string; revenue: number }>;
|
|
694
696
|
};
|
|
695
697
|
fleet: {
|
|
@@ -718,6 +720,12 @@ export interface OrderFilter {
|
|
|
718
720
|
dateTo?: Date;
|
|
719
721
|
priority?: string;
|
|
720
722
|
orderType?: 'b2b' | 'b2c';
|
|
723
|
+
/**
|
|
724
|
+
* Client-side quick filter for orders that still need a driver assigned
|
|
725
|
+
* (driverId is null). The backend OrderFilterInput has no null-driver
|
|
726
|
+
* predicate, so this is applied to the fetched page after mapping.
|
|
727
|
+
*/
|
|
728
|
+
unassigned?: boolean;
|
|
721
729
|
}
|
|
722
730
|
|
|
723
731
|
export interface FleetFilter {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small browser download helpers used by report/export surfaces.
|
|
3
|
+
*
|
|
4
|
+
* These build an in-memory Blob and trigger a same-document download via a
|
|
5
|
+
* transient anchor — no network round-trip, so they work against data the MFE
|
|
6
|
+
* already holds in state.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Trigger a browser download of `content` as `filename`. */
|
|
10
|
+
export function downloadFile(filename: string, content: string, mimeType: string): void {
|
|
11
|
+
if (typeof document === 'undefined') return;
|
|
12
|
+
const blob = new Blob([content], { type: mimeType });
|
|
13
|
+
const url = URL.createObjectURL(blob);
|
|
14
|
+
const anchor = document.createElement('a');
|
|
15
|
+
anchor.href = url;
|
|
16
|
+
anchor.download = filename;
|
|
17
|
+
document.body.appendChild(anchor);
|
|
18
|
+
anchor.click();
|
|
19
|
+
anchor.remove();
|
|
20
|
+
URL.revokeObjectURL(url);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Download a pretty-printed JSON file. */
|
|
24
|
+
export function downloadJson(filename: string, data: unknown): void {
|
|
25
|
+
downloadFile(filename, JSON.stringify(data, null, 2), 'application/json');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Flatten a record of scalar values into a two-column (key,value) CSV. Nested
|
|
30
|
+
* objects/arrays are JSON-encoded into the value cell. Returns null when there
|
|
31
|
+
* is nothing tabular to emit.
|
|
32
|
+
*/
|
|
33
|
+
export function recordToCsv(record: Record<string, unknown> | null | undefined): string | null {
|
|
34
|
+
if (!record || typeof record !== 'object') return null;
|
|
35
|
+
const entries = Object.entries(record);
|
|
36
|
+
if (entries.length === 0) return null;
|
|
37
|
+
|
|
38
|
+
const escape = (value: string): string =>
|
|
39
|
+
/[",\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
|
|
40
|
+
|
|
41
|
+
const cellFor = (value: unknown): string => {
|
|
42
|
+
if (value == null) return '';
|
|
43
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
44
|
+
return String(value);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const lines = ['Field,Value'];
|
|
48
|
+
for (const [key, value] of entries) {
|
|
49
|
+
lines.push(`${escape(key)},${escape(cellFor(value))}`);
|
|
50
|
+
}
|
|
51
|
+
return lines.join('\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Turn an arbitrary label into a filesystem-safe slug for filenames. */
|
|
55
|
+
export function slugifyForFilename(label: string): string {
|
|
56
|
+
return (
|
|
57
|
+
label
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
60
|
+
.replace(/^-+|-+$/g, '') || 'report'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for turning domain records into map markers. Pure functions —
|
|
3
|
+
* no Leaflet import, so they can be reused by any page without pulling map CSS.
|
|
4
|
+
*/
|
|
5
|
+
import type { MapMarkerStatus } from '../components/maps';
|
|
6
|
+
import type { Address, Location } from '../types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Map an order/shipment status onto the marker's visual status (drives pin color
|
|
10
|
+
* and glyph). Anything unrecognised falls back to a neutral pin.
|
|
11
|
+
*/
|
|
12
|
+
export function deliveryMarkerStatus(status: string | undefined): MapMarkerStatus {
|
|
13
|
+
switch ((status ?? '').toLowerCase()) {
|
|
14
|
+
case 'in_transit':
|
|
15
|
+
return 'in_transit';
|
|
16
|
+
case 'out_for_delivery':
|
|
17
|
+
return 'active';
|
|
18
|
+
case 'delivered':
|
|
19
|
+
return 'delivered';
|
|
20
|
+
case 'failed':
|
|
21
|
+
case 'delayed':
|
|
22
|
+
case 'cancelled':
|
|
23
|
+
return 'delayed';
|
|
24
|
+
case 'assigned':
|
|
25
|
+
case 'picked_up':
|
|
26
|
+
case 'confirmed':
|
|
27
|
+
return 'active';
|
|
28
|
+
default:
|
|
29
|
+
return 'default';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* First location among the candidates that carries a real (non-null-island)
|
|
35
|
+
* coordinate, returned as a plain `{ lat, lng }`. Returns null when none do.
|
|
36
|
+
* Kept coordinate-shape-agnostic so callers pass whichever object they have.
|
|
37
|
+
*/
|
|
38
|
+
export function firstRealLatLng(
|
|
39
|
+
...candidates: Array<Location | Address | null | undefined>
|
|
40
|
+
): { lat: number; lng: number } | null {
|
|
41
|
+
for (const candidate of candidates) {
|
|
42
|
+
if (!candidate) continue;
|
|
43
|
+
const lat = candidate.latitude;
|
|
44
|
+
const lng = candidate.longitude;
|
|
45
|
+
if (
|
|
46
|
+
typeof lat === 'number' &&
|
|
47
|
+
typeof lng === 'number' &&
|
|
48
|
+
Number.isFinite(lat) &&
|
|
49
|
+
Number.isFinite(lng) &&
|
|
50
|
+
!(lat === 0 && lng === 0) &&
|
|
51
|
+
Math.abs(lat) <= 90 &&
|
|
52
|
+
Math.abs(lng) <= 180
|
|
53
|
+
) {
|
|
54
|
+
return { lat, lng };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|