@burdenoff/microfe-movethewheels 2026.703.3 → 2026.703.4
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-3YOnHO9Y.js} +3 -3
- package/dist/{AnalyticsPage-Dz6g2MvK.js → AnalyticsPage-CMpI1070.js} +11 -11
- package/dist/CreateOrderPage-DRAgGo9i.js +490 -0
- package/dist/CustomerDetailsPage-BCeMmOFe.js +615 -0
- package/dist/{CustomersPage-BKxMS1AV.js → CustomersPage-CaNBMXmM.js} +64 -53
- package/dist/{DashboardPage-BVjQi0EB.js → DashboardPage-DIKlXde3.js} +16 -16
- package/dist/DataTable-XvUcTjwi.js +340 -0
- package/dist/DocumentsCard-U2lSe-cn.js +169 -0
- package/dist/{DriverDetailsPage-CN3WQ0B9.js → DriverDetailsPage-BQrYd_Lp.js} +178 -195
- package/dist/{DriversPage-CH9rjQC-.js → DriversPage-PYmdFiby.js} +65 -54
- package/dist/FinancePage-Cwh9Bw4M.js +136 -0
- package/dist/{FleetPage-DqkFi3s3.js → FleetPage-3ZHsB-TN.js} +23 -22
- package/dist/{FormError-BlOjnXOq.js → FormError-cuLV2jVD.js} +1 -1
- package/dist/{ImportExportPage-DKJRCezl.js → ImportExportPage-C6f7DqhM.js} +50 -70
- package/dist/{InventoryPage-e0ze63iX.js → InventoryPage-DpiqVu1v.js} +8 -8
- package/dist/{LiveTrackingPage-DCX-K-LR.js → LiveTrackingPage-FAzzlVnX.js} +9 -9
- package/dist/MarketplacePage-Bh_2ns4v.js +426 -0
- package/dist/Notice-CSoDYKw9.js +46 -0
- package/dist/OrderDetailsPage-zCj0lW0j.js +584 -0
- package/dist/{OrdersListPage-Cn90P1Xx.js → OrdersListPage-CAMsuUck.js} +166 -89
- package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-wsxAQkA0.js} +12 -12
- package/dist/{ProductsPage-QJfHHpqB.js → ProductsPage-DC5qDLfz.js} +60 -49
- package/dist/ReportsPage-BwUJ37eO.js +356 -0
- package/dist/RouteDetailsPage-DyMJE9AS.js +583 -0
- package/dist/{RoutesPage-DMfoicM1.js → RoutesPage-Dtc669yc.js} +57 -46
- package/dist/{SettingsPage-Bn-v0mbQ.js → SettingsPage-CpH9xhQg.js} +5 -27
- package/dist/{TrackingPage-W-jtdM3Y.js → TrackingPage-DaNEFG_h.js} +10 -10
- package/dist/{VehicleDetailsPage-Bceo5oz3.js → VehicleDetailsPage-DldYNsYV.js} +125 -142
- package/dist/{VehiclesPage-ApKThYeG.js → VehiclesPage-BFw13iMt.js} +61 -50
- package/dist/{WarehouseDetailsPage-L_6sWigM.js → WarehouseDetailsPage-CMG7or6G.js} +17 -17
- package/dist/{WarehousesPage-DCbXXXWE.js → WarehousesPage-Wkytnt86.js} +58 -47
- package/dist/arrow-right-Q3XQpTPR.js +11 -0
- package/dist/arrow-up-xNF1zJUG.js +17 -0
- package/dist/chevron-left-B2_r0Elx.js +8 -0
- package/dist/{wrench-CANXWxF-.js → fuel-B6_lnKDs.js} +2 -5
- package/dist/index.js +163 -174
- package/dist/upload-CHZofOqb.js +21 -0
- package/dist/{useAnalytics-DS8n0xgg.js → useAnalytics-D7HW2XRb.js} +52 -2
- package/dist/{useCustomers-BmTNGxiI.js → useCustomers-BPW4__PR.js} +29 -26
- package/dist/{useFleet-DKqXZ16m.js → useFleet-ChBkxAd6.js} +81 -69
- package/dist/{useInventory-DkyiVY_A.js → useInventory-CAAVMRp6.js} +83 -38
- package/dist/{useOrders-Cfk1GLiq.js → useOrders-B465S2nt.js} +58 -47
- package/dist/{useReports-CTqZr-AB.js → useReports-jQ3GpNOm.js} +40 -34
- package/dist/{useRoutes-BQWO607N.js → useRoutes-hS3d5-xr.js} +49 -41
- package/dist/{useTracking-B-va1kO3.js → useTracking-RjGz9PZG.js} +2 -2
- package/dist/user-check-BEKRCi7v.js +20 -0
- package/dist/wrench-q_rgJfaP.js +8 -0
- package/dist/x-tMeDMcCw.js +11 -0
- package/package.json +1 -1
- package/src/movethewheels/components/DataTable.tsx +252 -84
- package/src/movethewheels/components/DocumentsCard.tsx +102 -0
- package/src/movethewheels/components/Notice.tsx +61 -0
- package/src/movethewheels/components/index.ts +7 -0
- package/src/movethewheels/hooks/index.ts +9 -0
- package/src/movethewheels/hooks/useAnalytics.ts +95 -0
- 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 +2 -2
- package/src/movethewheels/pages/CreateOrderPage.tsx +44 -16
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +239 -33
- package/src/movethewheels/pages/CustomersPage.tsx +27 -11
- package/src/movethewheels/pages/DriverDetailsPage.tsx +16 -30
- package/src/movethewheels/pages/DriversPage.tsx +26 -9
- package/src/movethewheels/pages/FinancePage.tsx +67 -84
- package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
- package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
- package/src/movethewheels/pages/OrdersListPage.tsx +76 -13
- package/src/movethewheels/pages/ProductsPage.tsx +27 -7
- package/src/movethewheels/pages/ReportsPage.tsx +65 -4
- package/src/movethewheels/pages/RouteDetailsPage.tsx +74 -13
- package/src/movethewheels/pages/RoutesPage.tsx +22 -6
- package/src/movethewheels/pages/SettingsPage.tsx +3 -19
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +3 -33
- package/src/movethewheels/pages/VehiclesPage.tsx +26 -5
- package/src/movethewheels/pages/WarehousesPage.tsx +22 -17
- package/src/movethewheels/types/index.ts +8 -0
- package/src/movethewheels/utils/download.ts +62 -0
- package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
- package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
- package/dist/DataTable-DmciIKKM.js +0 -249
- package/dist/FinancePage-DEDykSOP.js +0 -158
- package/dist/MarketplacePage-CIQ97_zp.js +0 -247
- package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
- package/dist/ReportsPage-Zp53q3_8.js +0 -284
- package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
- /package/dist/{MetricCard-BY0QrMD4.js → MetricCard-CVBVpu7o.js} +0 -0
- /package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-C87mDpDP.js} +0 -0
- /package/dist/{box-D9Pal4HM.js → box-Eu05PXDh.js} +0 -0
- /package/dist/{chart-column-BFIJysDR.js → chart-column-ouzgxIv_.js} +0 -0
- /package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-eV4LjaSi.js} +0 -0
- /package/dist/{circle-alert-iY24lSlp.js → circle-alert-Bzh2DBSF.js} +0 -0
- /package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-CGMSpQmD.js} +0 -0
- /package/dist/{clock-DJZtdl8u.js → clock-Bl3ptexL.js} +0 -0
- /package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-CAVU95SO.js} +0 -0
- /package/dist/{download-DRXxoiQR.js → download-C5NbvVDc.js} +0 -0
- /package/dist/{file-text-DWWY6_f5.js → file-text-D-1uvLZG.js} +0 -0
- /package/dist/{filter-DloLC2eE.js → filter-DUfUg6BO.js} +0 -0
- /package/dist/{formatters-CF2S2l1m.js → formatters-BQr-aR5W.js} +0 -0
- /package/dist/{map-CkJaULb2.js → map-C-M-FQLR.js} +0 -0
- /package/dist/{map-pin-BGiCvyCs.js → map-pin-Dznl5l40.js} +0 -0
- /package/dist/{navigation-D36prnAn.js → navigation-BJSJl-VG.js} +0 -0
- /package/dist/{package-BChUzpJS.js → package-CcRttV2N.js} +0 -0
- /package/dist/{phone-Ac9s3mo_.js → phone-DVSZHt_j.js} +0 -0
- /package/dist/{plus-Z76q-FZm.js → plus-C_XZDwBz.js} +0 -0
- /package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-CfgN67l3.js} +0 -0
- /package/dist/{save-CojywNhG.js → save-DJE8Fpdv.js} +0 -0
- /package/dist/{search-CDGefqb3.js → search-D_VyGcZT.js} +0 -0
- /package/dist/{square-pen-CopE9GmB.js → square-pen-D-6JYrVQ.js} +0 -0
- /package/dist/{star-nHlLjDKR.js → star-BpRlUZuU.js} +0 -0
- /package/dist/{store-BR9Voi5E.js → store-B-8HMuyZ.js} +0 -0
- /package/dist/{trash-2-BKhar6Hs.js → trash-2-8vmv-XV_.js} +0 -0
- /package/dist/{trending-up-Dohgd4t_.js → trending-up-Dz6PqPRj.js} +0 -0
- /package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-D7Y3J_4z.js} +0 -0
- /package/dist/{truck-C0v_hbLR.js → truck-D-y2IDzv.js} +0 -0
- /package/dist/{useAssistant-D4v3oxD3.js → useAssistant-DVW4QgQb.js} +0 -0
- /package/dist/{useImportExport-DHr-SWSw.js → useImportExport-DrPDHEDK.js} +0 -0
- /package/dist/{useSettings-CkDH_-gM.js → useSettings-BB8X2RLh.js} +0 -0
- /package/dist/{user-jyFU3lXk.js → user-DzfTn4dE.js} +0 -0
- /package/dist/{users-DZ-ilS6B.js → users-CCrq-w0C.js} +0 -0
- /package/dist/{warehouse-6m5kXMLu.js → warehouse-CaqfmMKh.js} +0 -0
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
Plus,
|
|
16
16
|
Trash2,
|
|
17
17
|
Save,
|
|
18
|
+
ArrowUp,
|
|
19
|
+
ArrowDown,
|
|
20
|
+
Fuel,
|
|
18
21
|
} from 'lucide-react';
|
|
19
22
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
20
23
|
import { StatusBadge } from '../components/StatusBadge';
|
|
@@ -37,10 +40,11 @@ import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
|
37
40
|
import { useRoute, useRoutes } from '../hooks/useRoutes';
|
|
38
41
|
import { useFleet } from '../hooks/useFleet';
|
|
39
42
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
40
|
-
import { formatDate, formatAddress } from '../utils/formatters';
|
|
43
|
+
import { formatDate, formatAddress, formatCurrency } from '../utils/formatters';
|
|
41
44
|
import { joinPath } from '../utils/navigation';
|
|
42
45
|
import { ROUTE_STATUS_COLORS } from '../constants';
|
|
43
46
|
import { FormError } from '../components/FormError';
|
|
47
|
+
import { Notice, useNotice } from '../components/Notice';
|
|
44
48
|
import type { Route as RouteType, RouteStop } from '../types';
|
|
45
49
|
|
|
46
50
|
const ROUTE_STATUS_OPTIONS: { value: RouteType['status']; label: string }[] = [
|
|
@@ -90,6 +94,15 @@ function NewRouteForm() {
|
|
|
90
94
|
stops: prev.stops.map((stop, i) => (i === index ? { ...stop, [field]: value } : stop)),
|
|
91
95
|
}));
|
|
92
96
|
|
|
97
|
+
const moveStop = (index: number, direction: -1 | 1) =>
|
|
98
|
+
setForm((prev) => {
|
|
99
|
+
const target = index + direction;
|
|
100
|
+
if (target < 0 || target >= prev.stops.length) return prev;
|
|
101
|
+
const stops = [...prev.stops];
|
|
102
|
+
[stops[index], stops[target]] = [stops[target], stops[index]];
|
|
103
|
+
return { ...prev, stops };
|
|
104
|
+
});
|
|
105
|
+
|
|
93
106
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
94
107
|
e.preventDefault();
|
|
95
108
|
if (!form.name.trim()) {
|
|
@@ -273,7 +286,27 @@ function NewRouteForm() {
|
|
|
273
286
|
</SelectContent>
|
|
274
287
|
</Select>
|
|
275
288
|
</div>
|
|
276
|
-
<div className="md:col-span-1 flex justify-end">
|
|
289
|
+
<div className="md:col-span-1 flex justify-end gap-1">
|
|
290
|
+
<Button
|
|
291
|
+
type="button"
|
|
292
|
+
variant="ghost"
|
|
293
|
+
size="icon"
|
|
294
|
+
onClick={() => moveStop(index, -1)}
|
|
295
|
+
disabled={index === 0}
|
|
296
|
+
aria-label={`Move stop ${index + 1} up`}
|
|
297
|
+
>
|
|
298
|
+
<ArrowUp size={16} />
|
|
299
|
+
</Button>
|
|
300
|
+
<Button
|
|
301
|
+
type="button"
|
|
302
|
+
variant="ghost"
|
|
303
|
+
size="icon"
|
|
304
|
+
onClick={() => moveStop(index, 1)}
|
|
305
|
+
disabled={index === form.stops.length - 1}
|
|
306
|
+
aria-label={`Move stop ${index + 1} down`}
|
|
307
|
+
>
|
|
308
|
+
<ArrowDown size={16} />
|
|
309
|
+
</Button>
|
|
277
310
|
<Button
|
|
278
311
|
type="button"
|
|
279
312
|
variant="ghost"
|
|
@@ -300,10 +333,28 @@ export default function RouteDetailsPage() {
|
|
|
300
333
|
const { basePath, navigate } = useMoveTheWheels();
|
|
301
334
|
const { route, isLoading, error, refetch } = useRoute(routeId);
|
|
302
335
|
const { optimizeRoute } = useRoutes();
|
|
336
|
+
const { message: notice, notify } = useNotice();
|
|
337
|
+
const [optimizeError, setOptimizeError] = useState<string | null>(null);
|
|
338
|
+
const [optimizing, setOptimizing] = useState(false);
|
|
303
339
|
|
|
304
340
|
const goBack = () => navigate?.(joinPath(basePath, 'routes'));
|
|
305
341
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
306
342
|
|
|
343
|
+
const handleOptimize = async () => {
|
|
344
|
+
if (!routeId) return;
|
|
345
|
+
setOptimizing(true);
|
|
346
|
+
setOptimizeError(null);
|
|
347
|
+
try {
|
|
348
|
+
await optimizeRoute(routeId);
|
|
349
|
+
notify('Route optimized — stops resequenced for the shortest run.');
|
|
350
|
+
refetch();
|
|
351
|
+
} catch (e) {
|
|
352
|
+
setOptimizeError(e instanceof Error ? e.message : 'Failed to optimize route.');
|
|
353
|
+
} finally {
|
|
354
|
+
setOptimizing(false);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
307
358
|
// Handle new route form
|
|
308
359
|
if (!routeId) {
|
|
309
360
|
return <NewRouteForm />;
|
|
@@ -312,15 +363,6 @@ export default function RouteDetailsPage() {
|
|
|
312
363
|
if (isLoading) return <LoadingState message="Loading route details..." />;
|
|
313
364
|
if (error || !route) return <ErrorState title="Route not found" onRetry={refetch} />;
|
|
314
365
|
|
|
315
|
-
const handleOptimize = async () => {
|
|
316
|
-
try {
|
|
317
|
-
await optimizeRoute(route.id);
|
|
318
|
-
refetch();
|
|
319
|
-
} catch (e) {
|
|
320
|
-
console.error(e);
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
|
|
324
366
|
return (
|
|
325
367
|
<PageLayout
|
|
326
368
|
title={route.name}
|
|
@@ -332,8 +374,8 @@ export default function RouteDetailsPage() {
|
|
|
332
374
|
<ArrowLeft size={16} /> Back
|
|
333
375
|
</Button>
|
|
334
376
|
{!route.optimized && (
|
|
335
|
-
<Button variant="outline" onClick={handleOptimize}>
|
|
336
|
-
<Zap size={16} /> Optimize
|
|
377
|
+
<Button variant="outline" onClick={handleOptimize} disabled={optimizing}>
|
|
378
|
+
<Zap size={16} /> {optimizing ? 'Optimizing…' : 'Optimize'}
|
|
337
379
|
</Button>
|
|
338
380
|
)}
|
|
339
381
|
<Button>
|
|
@@ -387,6 +429,17 @@ export default function RouteDetailsPage() {
|
|
|
387
429
|
]}
|
|
388
430
|
/>
|
|
389
431
|
|
|
432
|
+
{optimizeError && (
|
|
433
|
+
<div className="mb-2">
|
|
434
|
+
<FormError message={optimizeError} />
|
|
435
|
+
</div>
|
|
436
|
+
)}
|
|
437
|
+
{notice && (
|
|
438
|
+
<div className="mb-2">
|
|
439
|
+
<Notice message={notice} />
|
|
440
|
+
</div>
|
|
441
|
+
)}
|
|
442
|
+
|
|
390
443
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
391
444
|
<div className="lg:col-span-2 space-y-6">
|
|
392
445
|
<Card>
|
|
@@ -475,6 +528,14 @@ export default function RouteDetailsPage() {
|
|
|
475
528
|
<span className="text-sm font-normal"> min</span>
|
|
476
529
|
</p>
|
|
477
530
|
</div>
|
|
531
|
+
{route.fuelCost !== undefined && (
|
|
532
|
+
<div>
|
|
533
|
+
<p className="text-sm text-text-secondary flex items-center gap-1">
|
|
534
|
+
<Fuel size={14} /> Est. Fuel Cost
|
|
535
|
+
</p>
|
|
536
|
+
<p className="text-2xl font-bold">{formatCurrency(route.fuelCost)}</p>
|
|
537
|
+
</div>
|
|
538
|
+
)}
|
|
478
539
|
{route.actualDuration && (
|
|
479
540
|
<div>
|
|
480
541
|
<p className="text-sm text-text-secondary">Actual Duration</p>
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* Routes Page
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { useState } from 'react';
|
|
5
6
|
import { Plus, RefreshCw, Navigation } from 'lucide-react';
|
|
6
7
|
import { PageLayout } from '../components/PageLayout';
|
|
7
8
|
import { DataTable, type Column } from '../components/DataTable';
|
|
8
9
|
import { StatusBadge } from '../components/StatusBadge';
|
|
9
10
|
import { Badge, Button } from '../components/ui';
|
|
10
11
|
import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
11
|
-
import { useRoutes } from '../hooks/useRoutes';
|
|
12
|
+
import { useRoutes, useRouteStats } from '../hooks/useRoutes';
|
|
12
13
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
13
14
|
import { formatDate } from '../utils/formatters';
|
|
14
15
|
import { joinPath } from '../utils/navigation';
|
|
@@ -17,7 +18,14 @@ import type { Route } from '../types';
|
|
|
17
18
|
|
|
18
19
|
export default function RoutesPage() {
|
|
19
20
|
const { basePath, navigate } = useMoveTheWheels();
|
|
20
|
-
const
|
|
21
|
+
const [page, setPage] = useState(0);
|
|
22
|
+
const [pageSize, setPageSize] = useState(25);
|
|
23
|
+
const { routes, hasMore, isLoading, refetch } = useRoutes({
|
|
24
|
+
limit: pageSize,
|
|
25
|
+
offset: page * pageSize,
|
|
26
|
+
});
|
|
27
|
+
// Bounded aggregate for the tiles + subtitle — not the truncated page.
|
|
28
|
+
const { stats } = useRouteStats();
|
|
21
29
|
|
|
22
30
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
23
31
|
|
|
@@ -72,7 +80,7 @@ export default function RoutesPage() {
|
|
|
72
80
|
return (
|
|
73
81
|
<PageLayout
|
|
74
82
|
title="Routes"
|
|
75
|
-
subtitle={`${totalRoutes} routes`}
|
|
83
|
+
subtitle={`${stats.totalRoutes} routes`}
|
|
76
84
|
icon={<Navigation size={24} />}
|
|
77
85
|
headerActions={
|
|
78
86
|
<CTAOverflowMenu
|
|
@@ -96,18 +104,18 @@ export default function RoutesPage() {
|
|
|
96
104
|
<p className="text-sm font-medium text-text-secondary">Routes at a glance</p>
|
|
97
105
|
<div className="mt-3 grid grid-cols-3 gap-4">
|
|
98
106
|
<div>
|
|
99
|
-
<p className="text-2xl font-bold text-text-primary tabular-nums">{totalRoutes}</p>
|
|
107
|
+
<p className="text-2xl font-bold text-text-primary tabular-nums">{stats.totalRoutes}</p>
|
|
100
108
|
<p className="text-xs text-text-secondary">Total routes</p>
|
|
101
109
|
</div>
|
|
102
110
|
<div>
|
|
103
111
|
<p className="text-2xl font-bold text-text-primary tabular-nums">
|
|
104
|
-
{
|
|
112
|
+
{stats.inProgressRoutes}
|
|
105
113
|
</p>
|
|
106
114
|
<p className="text-xs text-text-secondary">In progress</p>
|
|
107
115
|
</div>
|
|
108
116
|
<div>
|
|
109
117
|
<p className="text-2xl font-bold text-text-primary tabular-nums">
|
|
110
|
-
{
|
|
118
|
+
{stats.optimizedRoutes}
|
|
111
119
|
</p>
|
|
112
120
|
<p className="text-xs text-text-secondary">Optimized</p>
|
|
113
121
|
</div>
|
|
@@ -123,6 +131,14 @@ export default function RoutesPage() {
|
|
|
123
131
|
searchFields={['name'] as (keyof Route)[]}
|
|
124
132
|
onRowClick={(row) => goTo(`routes/${row.id}`)}
|
|
125
133
|
getRowId={(row) => row.id}
|
|
134
|
+
pageIndex={page}
|
|
135
|
+
pageSize={pageSize}
|
|
136
|
+
onPageChange={setPage}
|
|
137
|
+
onPageSizeChange={(size) => {
|
|
138
|
+
setPageSize(size);
|
|
139
|
+
setPage(0);
|
|
140
|
+
}}
|
|
141
|
+
hasNextPage={hasMore}
|
|
126
142
|
emptyIllustration="empty-data"
|
|
127
143
|
emptyTitle="No routes yet"
|
|
128
144
|
emptyDescription="Create a route to batch deliveries into an optimized driving plan."
|
|
@@ -187,7 +187,7 @@ export default function SettingsPage() {
|
|
|
187
187
|
{
|
|
188
188
|
id: 'security',
|
|
189
189
|
label: 'Secure your account',
|
|
190
|
-
description: '
|
|
190
|
+
description: 'Enable two-factor authentication for an extra layer of security.',
|
|
191
191
|
icon: <Shield size={16} />,
|
|
192
192
|
onClick: () => setTab('security'),
|
|
193
193
|
},
|
|
@@ -352,26 +352,10 @@ export default function SettingsPage() {
|
|
|
352
352
|
<CardHeader>
|
|
353
353
|
<CardTitle>Password</CardTitle>
|
|
354
354
|
<CardDescription>
|
|
355
|
-
|
|
355
|
+
Your password and sign-in credentials are managed centrally by your workspace
|
|
356
|
+
identity provider — update them from your account security settings, not here.
|
|
356
357
|
</CardDescription>
|
|
357
358
|
</CardHeader>
|
|
358
|
-
<CardContent className="space-y-4">
|
|
359
|
-
<div>
|
|
360
|
-
<Label>Current Password</Label>
|
|
361
|
-
<Input type="password" autoComplete="current-password" />
|
|
362
|
-
</div>
|
|
363
|
-
<div className="grid grid-cols-2 gap-4">
|
|
364
|
-
<div>
|
|
365
|
-
<Label>New Password</Label>
|
|
366
|
-
<Input type="password" autoComplete="new-password" />
|
|
367
|
-
</div>
|
|
368
|
-
<div>
|
|
369
|
-
<Label>Confirm Password</Label>
|
|
370
|
-
<Input type="password" autoComplete="new-password" />
|
|
371
|
-
</div>
|
|
372
|
-
</div>
|
|
373
|
-
<Button variant="outline">Update Password</Button>
|
|
374
|
-
</CardContent>
|
|
375
359
|
</Card>
|
|
376
360
|
<Card>
|
|
377
361
|
<CardHeader>
|
|
@@ -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' },
|
|
@@ -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,29 @@
|
|
|
2
2
|
* Vehicles Page - List of all vehicles
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { useState } from 'react';
|
|
5
6
|
import { Truck, Plus, RefreshCw, Download } from 'lucide-react';
|
|
6
7
|
import { PageLayout } from '../components/PageLayout';
|
|
7
8
|
import { DataTable, type Column } from '../components/DataTable';
|
|
8
9
|
import { StatusBadge, VehicleTypeBadge } from '../components/StatusBadge';
|
|
9
10
|
import { Progress } from '../components/ui';
|
|
10
11
|
import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
11
|
-
import { useVehicles } from '../hooks/useFleet';
|
|
12
|
+
import { useVehicles, useFleetStats } from '../hooks/useFleet';
|
|
12
13
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
13
14
|
import { joinPath } from '../utils/navigation';
|
|
14
15
|
import type { Vehicle } from '../types';
|
|
15
16
|
|
|
16
17
|
export default function VehiclesPage() {
|
|
17
18
|
const { basePath, navigate } = useMoveTheWheels();
|
|
18
|
-
const
|
|
19
|
+
const [page, setPage] = useState(0);
|
|
20
|
+
const [pageSize, setPageSize] = useState(25);
|
|
21
|
+
const { vehicles, isLoading, refetch } = useVehicles({
|
|
22
|
+
limit: pageSize,
|
|
23
|
+
offset: page * pageSize,
|
|
24
|
+
});
|
|
25
|
+
// Fleet totals + tiles from the live aggregate, not the current page.
|
|
26
|
+
const { stats } = useFleetStats();
|
|
27
|
+
const totalVehicles = stats.totalVehicles;
|
|
19
28
|
|
|
20
29
|
const goTo = (path: string) => {
|
|
21
30
|
if (navigate) {
|
|
@@ -102,7 +111,11 @@ export default function VehiclesPage() {
|
|
|
102
111
|
}}
|
|
103
112
|
actions={[
|
|
104
113
|
{ label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
|
|
105
|
-
{
|
|
114
|
+
{
|
|
115
|
+
label: 'Export',
|
|
116
|
+
icon: <Download size={16} />,
|
|
117
|
+
onSelect: () => goTo('import-export'),
|
|
118
|
+
},
|
|
106
119
|
]}
|
|
107
120
|
/>
|
|
108
121
|
}
|
|
@@ -123,13 +136,13 @@ export default function VehiclesPage() {
|
|
|
123
136
|
</div>
|
|
124
137
|
<div>
|
|
125
138
|
<p className="text-2xl font-bold text-text-primary tabular-nums">
|
|
126
|
-
{
|
|
139
|
+
{stats.vehiclesByStatus.available}
|
|
127
140
|
</p>
|
|
128
141
|
<p className="text-xs text-text-secondary">Available now</p>
|
|
129
142
|
</div>
|
|
130
143
|
<div>
|
|
131
144
|
<p className="text-2xl font-bold text-text-primary tabular-nums">
|
|
132
|
-
{
|
|
145
|
+
{stats.vehiclesByStatus.maintenance}
|
|
133
146
|
</p>
|
|
134
147
|
<p className="text-xs text-text-secondary">In maintenance</p>
|
|
135
148
|
</div>
|
|
@@ -145,6 +158,14 @@ export default function VehiclesPage() {
|
|
|
145
158
|
searchFields={['vehicleNumber', 'make', 'model'] as (keyof Vehicle)[]}
|
|
146
159
|
onRowClick={(row) => goTo(`fleet/vehicles/${row.id}`)}
|
|
147
160
|
getRowId={(row) => row.id}
|
|
161
|
+
pageIndex={page}
|
|
162
|
+
pageSize={pageSize}
|
|
163
|
+
onPageChange={setPage}
|
|
164
|
+
onPageSizeChange={(size) => {
|
|
165
|
+
setPageSize(size);
|
|
166
|
+
setPage(0);
|
|
167
|
+
}}
|
|
168
|
+
totalItems={totalVehicles}
|
|
148
169
|
emptyIllustration="empty-data"
|
|
149
170
|
emptyTitle="No vehicles yet"
|
|
150
171
|
emptyDescription="Add vehicles to your fleet to start assigning them to deliveries."
|
|
@@ -2,19 +2,27 @@
|
|
|
2
2
|
* Warehouses Page
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { useState } from 'react';
|
|
5
6
|
import { Warehouse, Plus, RefreshCw } from 'lucide-react';
|
|
6
7
|
import { PageLayout } from '../components/PageLayout';
|
|
7
8
|
import { DataTable, type Column } from '../components/DataTable';
|
|
8
9
|
import { Badge, Progress } from '../components/ui';
|
|
9
10
|
import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
10
|
-
import { useWarehouses } from '../hooks/useInventory';
|
|
11
|
+
import { useWarehouses, useWarehouseStats } from '../hooks/useInventory';
|
|
11
12
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
12
13
|
import { joinPath } from '../utils/navigation';
|
|
13
14
|
import type { Warehouse as WarehouseType } from '../types';
|
|
14
15
|
|
|
15
16
|
export default function WarehousesPage() {
|
|
16
17
|
const { basePath, navigate } = useMoveTheWheels();
|
|
17
|
-
const
|
|
18
|
+
const [page, setPage] = useState(0);
|
|
19
|
+
const [pageSize, setPageSize] = useState(25);
|
|
20
|
+
const { warehouses, hasMore, isLoading, refetch } = useWarehouses({
|
|
21
|
+
limit: pageSize,
|
|
22
|
+
offset: page * pageSize,
|
|
23
|
+
});
|
|
24
|
+
// Bounded aggregate for the tiles + subtitle — not the truncated page.
|
|
25
|
+
const { stats } = useWarehouseStats();
|
|
18
26
|
|
|
19
27
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
20
28
|
|
|
@@ -95,7 +103,7 @@ export default function WarehousesPage() {
|
|
|
95
103
|
return (
|
|
96
104
|
<PageLayout
|
|
97
105
|
title="Warehouses"
|
|
98
|
-
subtitle={`${
|
|
106
|
+
subtitle={`${stats.total} warehouses`}
|
|
99
107
|
icon={<Warehouse size={24} />}
|
|
100
108
|
headerActions={
|
|
101
109
|
<CTAOverflowMenu
|
|
@@ -118,27 +126,16 @@ export default function WarehousesPage() {
|
|
|
118
126
|
<p className="text-sm font-medium text-text-secondary">Network at a glance</p>
|
|
119
127
|
<div className="mt-3 grid grid-cols-3 gap-4">
|
|
120
128
|
<div>
|
|
121
|
-
<p className="text-2xl font-bold text-text-primary tabular-nums">{
|
|
129
|
+
<p className="text-2xl font-bold text-text-primary tabular-nums">{stats.total}</p>
|
|
122
130
|
<p className="text-xs text-text-secondary">Total sites</p>
|
|
123
131
|
</div>
|
|
124
132
|
<div>
|
|
125
|
-
<p className="text-2xl font-bold text-text-primary tabular-nums">
|
|
126
|
-
{warehouses.filter((w) => w.isActive).length}
|
|
127
|
-
</p>
|
|
133
|
+
<p className="text-2xl font-bold text-text-primary tabular-nums">{stats.active}</p>
|
|
128
134
|
<p className="text-xs text-text-secondary">Active</p>
|
|
129
135
|
</div>
|
|
130
136
|
<div>
|
|
131
137
|
<p className="text-2xl font-bold text-text-primary tabular-nums">
|
|
132
|
-
{
|
|
133
|
-
? `${Math.round(
|
|
134
|
-
(warehouses.reduce(
|
|
135
|
-
(sum, w) => sum + w.capacity.usedSpace / w.capacity.totalSpace,
|
|
136
|
-
0
|
|
137
|
-
) /
|
|
138
|
-
warehouses.length) *
|
|
139
|
-
100
|
|
140
|
-
)}%`
|
|
141
|
-
: '—'}
|
|
138
|
+
{stats.total > 0 ? `${Math.round(stats.avgUtilization * 100)}%` : '—'}
|
|
142
139
|
</p>
|
|
143
140
|
<p className="text-xs text-text-secondary">Avg utilization</p>
|
|
144
141
|
</div>
|
|
@@ -154,6 +151,14 @@ export default function WarehousesPage() {
|
|
|
154
151
|
searchFields={['name', 'code'] as (keyof WarehouseType)[]}
|
|
155
152
|
onRowClick={(row) => goTo(`inventory/warehouses/${row.id}`)}
|
|
156
153
|
getRowId={(row) => row.id}
|
|
154
|
+
pageIndex={page}
|
|
155
|
+
pageSize={pageSize}
|
|
156
|
+
onPageChange={setPage}
|
|
157
|
+
onPageSizeChange={(size) => {
|
|
158
|
+
setPageSize(size);
|
|
159
|
+
setPage(0);
|
|
160
|
+
}}
|
|
161
|
+
hasNextPage={hasMore}
|
|
157
162
|
emptyIllustration="empty-data"
|
|
158
163
|
emptyTitle="No warehouses yet"
|
|
159
164
|
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;
|
|
@@ -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
|
+
}
|