@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.
Files changed (125) hide show
  1. package/dist/{AIAssistantPage-C3cPsWt-.js → AIAssistantPage-3YOnHO9Y.js} +3 -3
  2. package/dist/{AnalyticsPage-Dz6g2MvK.js → AnalyticsPage-CMpI1070.js} +11 -11
  3. package/dist/CreateOrderPage-DRAgGo9i.js +490 -0
  4. package/dist/CustomerDetailsPage-BCeMmOFe.js +615 -0
  5. package/dist/{CustomersPage-BKxMS1AV.js → CustomersPage-CaNBMXmM.js} +64 -53
  6. package/dist/{DashboardPage-BVjQi0EB.js → DashboardPage-DIKlXde3.js} +16 -16
  7. package/dist/DataTable-XvUcTjwi.js +340 -0
  8. package/dist/DocumentsCard-U2lSe-cn.js +169 -0
  9. package/dist/{DriverDetailsPage-CN3WQ0B9.js → DriverDetailsPage-BQrYd_Lp.js} +178 -195
  10. package/dist/{DriversPage-CH9rjQC-.js → DriversPage-PYmdFiby.js} +65 -54
  11. package/dist/FinancePage-Cwh9Bw4M.js +136 -0
  12. package/dist/{FleetPage-DqkFi3s3.js → FleetPage-3ZHsB-TN.js} +23 -22
  13. package/dist/{FormError-BlOjnXOq.js → FormError-cuLV2jVD.js} +1 -1
  14. package/dist/{ImportExportPage-DKJRCezl.js → ImportExportPage-C6f7DqhM.js} +50 -70
  15. package/dist/{InventoryPage-e0ze63iX.js → InventoryPage-DpiqVu1v.js} +8 -8
  16. package/dist/{LiveTrackingPage-DCX-K-LR.js → LiveTrackingPage-FAzzlVnX.js} +9 -9
  17. package/dist/MarketplacePage-Bh_2ns4v.js +426 -0
  18. package/dist/Notice-CSoDYKw9.js +46 -0
  19. package/dist/OrderDetailsPage-zCj0lW0j.js +584 -0
  20. package/dist/{OrdersListPage-Cn90P1Xx.js → OrdersListPage-CAMsuUck.js} +166 -89
  21. package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-wsxAQkA0.js} +12 -12
  22. package/dist/{ProductsPage-QJfHHpqB.js → ProductsPage-DC5qDLfz.js} +60 -49
  23. package/dist/ReportsPage-BwUJ37eO.js +356 -0
  24. package/dist/RouteDetailsPage-DyMJE9AS.js +583 -0
  25. package/dist/{RoutesPage-DMfoicM1.js → RoutesPage-Dtc669yc.js} +57 -46
  26. package/dist/{SettingsPage-Bn-v0mbQ.js → SettingsPage-CpH9xhQg.js} +5 -27
  27. package/dist/{TrackingPage-W-jtdM3Y.js → TrackingPage-DaNEFG_h.js} +10 -10
  28. package/dist/{VehicleDetailsPage-Bceo5oz3.js → VehicleDetailsPage-DldYNsYV.js} +125 -142
  29. package/dist/{VehiclesPage-ApKThYeG.js → VehiclesPage-BFw13iMt.js} +61 -50
  30. package/dist/{WarehouseDetailsPage-L_6sWigM.js → WarehouseDetailsPage-CMG7or6G.js} +17 -17
  31. package/dist/{WarehousesPage-DCbXXXWE.js → WarehousesPage-Wkytnt86.js} +58 -47
  32. package/dist/arrow-right-Q3XQpTPR.js +11 -0
  33. package/dist/arrow-up-xNF1zJUG.js +17 -0
  34. package/dist/chevron-left-B2_r0Elx.js +8 -0
  35. package/dist/{wrench-CANXWxF-.js → fuel-B6_lnKDs.js} +2 -5
  36. package/dist/index.js +163 -174
  37. package/dist/upload-CHZofOqb.js +21 -0
  38. package/dist/{useAnalytics-DS8n0xgg.js → useAnalytics-D7HW2XRb.js} +52 -2
  39. package/dist/{useCustomers-BmTNGxiI.js → useCustomers-BPW4__PR.js} +29 -26
  40. package/dist/{useFleet-DKqXZ16m.js → useFleet-ChBkxAd6.js} +81 -69
  41. package/dist/{useInventory-DkyiVY_A.js → useInventory-CAAVMRp6.js} +83 -38
  42. package/dist/{useOrders-Cfk1GLiq.js → useOrders-B465S2nt.js} +58 -47
  43. package/dist/{useReports-CTqZr-AB.js → useReports-jQ3GpNOm.js} +40 -34
  44. package/dist/{useRoutes-BQWO607N.js → useRoutes-hS3d5-xr.js} +49 -41
  45. package/dist/{useTracking-B-va1kO3.js → useTracking-RjGz9PZG.js} +2 -2
  46. package/dist/user-check-BEKRCi7v.js +20 -0
  47. package/dist/wrench-q_rgJfaP.js +8 -0
  48. package/dist/x-tMeDMcCw.js +11 -0
  49. package/package.json +1 -1
  50. package/src/movethewheels/components/DataTable.tsx +252 -84
  51. package/src/movethewheels/components/DocumentsCard.tsx +102 -0
  52. package/src/movethewheels/components/Notice.tsx +61 -0
  53. package/src/movethewheels/components/index.ts +7 -0
  54. package/src/movethewheels/hooks/index.ts +9 -0
  55. package/src/movethewheels/hooks/useAnalytics.ts +95 -0
  56. package/src/movethewheels/hooks/useCustomers.ts +11 -4
  57. package/src/movethewheels/hooks/useEntityDocuments.ts +132 -0
  58. package/src/movethewheels/hooks/useFleet.ts +26 -7
  59. package/src/movethewheels/hooks/useInventory.ts +70 -7
  60. package/src/movethewheels/hooks/useOrders.ts +32 -2
  61. package/src/movethewheels/hooks/useReports.ts +14 -4
  62. package/src/movethewheels/hooks/useRoutes.ts +20 -3
  63. package/src/movethewheels/pages/AnalyticsPage.tsx +2 -2
  64. package/src/movethewheels/pages/CreateOrderPage.tsx +44 -16
  65. package/src/movethewheels/pages/CustomerDetailsPage.tsx +239 -33
  66. package/src/movethewheels/pages/CustomersPage.tsx +27 -11
  67. package/src/movethewheels/pages/DriverDetailsPage.tsx +16 -30
  68. package/src/movethewheels/pages/DriversPage.tsx +26 -9
  69. package/src/movethewheels/pages/FinancePage.tsx +67 -84
  70. package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
  71. package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
  72. package/src/movethewheels/pages/OrdersListPage.tsx +76 -13
  73. package/src/movethewheels/pages/ProductsPage.tsx +27 -7
  74. package/src/movethewheels/pages/ReportsPage.tsx +65 -4
  75. package/src/movethewheels/pages/RouteDetailsPage.tsx +74 -13
  76. package/src/movethewheels/pages/RoutesPage.tsx +22 -6
  77. package/src/movethewheels/pages/SettingsPage.tsx +3 -19
  78. package/src/movethewheels/pages/VehicleDetailsPage.tsx +3 -33
  79. package/src/movethewheels/pages/VehiclesPage.tsx +26 -5
  80. package/src/movethewheels/pages/WarehousesPage.tsx +22 -17
  81. package/src/movethewheels/types/index.ts +8 -0
  82. package/src/movethewheels/utils/download.ts +62 -0
  83. package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
  84. package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
  85. package/dist/DataTable-DmciIKKM.js +0 -249
  86. package/dist/FinancePage-DEDykSOP.js +0 -158
  87. package/dist/MarketplacePage-CIQ97_zp.js +0 -247
  88. package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
  89. package/dist/ReportsPage-Zp53q3_8.js +0 -284
  90. package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
  91. /package/dist/{MetricCard-BY0QrMD4.js → MetricCard-CVBVpu7o.js} +0 -0
  92. /package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-C87mDpDP.js} +0 -0
  93. /package/dist/{box-D9Pal4HM.js → box-Eu05PXDh.js} +0 -0
  94. /package/dist/{chart-column-BFIJysDR.js → chart-column-ouzgxIv_.js} +0 -0
  95. /package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-eV4LjaSi.js} +0 -0
  96. /package/dist/{circle-alert-iY24lSlp.js → circle-alert-Bzh2DBSF.js} +0 -0
  97. /package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-CGMSpQmD.js} +0 -0
  98. /package/dist/{clock-DJZtdl8u.js → clock-Bl3ptexL.js} +0 -0
  99. /package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-CAVU95SO.js} +0 -0
  100. /package/dist/{download-DRXxoiQR.js → download-C5NbvVDc.js} +0 -0
  101. /package/dist/{file-text-DWWY6_f5.js → file-text-D-1uvLZG.js} +0 -0
  102. /package/dist/{filter-DloLC2eE.js → filter-DUfUg6BO.js} +0 -0
  103. /package/dist/{formatters-CF2S2l1m.js → formatters-BQr-aR5W.js} +0 -0
  104. /package/dist/{map-CkJaULb2.js → map-C-M-FQLR.js} +0 -0
  105. /package/dist/{map-pin-BGiCvyCs.js → map-pin-Dznl5l40.js} +0 -0
  106. /package/dist/{navigation-D36prnAn.js → navigation-BJSJl-VG.js} +0 -0
  107. /package/dist/{package-BChUzpJS.js → package-CcRttV2N.js} +0 -0
  108. /package/dist/{phone-Ac9s3mo_.js → phone-DVSZHt_j.js} +0 -0
  109. /package/dist/{plus-Z76q-FZm.js → plus-C_XZDwBz.js} +0 -0
  110. /package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-CfgN67l3.js} +0 -0
  111. /package/dist/{save-CojywNhG.js → save-DJE8Fpdv.js} +0 -0
  112. /package/dist/{search-CDGefqb3.js → search-D_VyGcZT.js} +0 -0
  113. /package/dist/{square-pen-CopE9GmB.js → square-pen-D-6JYrVQ.js} +0 -0
  114. /package/dist/{star-nHlLjDKR.js → star-BpRlUZuU.js} +0 -0
  115. /package/dist/{store-BR9Voi5E.js → store-B-8HMuyZ.js} +0 -0
  116. /package/dist/{trash-2-BKhar6Hs.js → trash-2-8vmv-XV_.js} +0 -0
  117. /package/dist/{trending-up-Dohgd4t_.js → trending-up-Dz6PqPRj.js} +0 -0
  118. /package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-D7Y3J_4z.js} +0 -0
  119. /package/dist/{truck-C0v_hbLR.js → truck-D-y2IDzv.js} +0 -0
  120. /package/dist/{useAssistant-D4v3oxD3.js → useAssistant-DVW4QgQb.js} +0 -0
  121. /package/dist/{useImportExport-DHr-SWSw.js → useImportExport-DrPDHEDK.js} +0 -0
  122. /package/dist/{useSettings-CkDH_-gM.js → useSettings-BB8X2RLh.js} +0 -0
  123. /package/dist/{user-jyFU3lXk.js → user-DzfTn4dE.js} +0 -0
  124. /package/dist/{users-DZ-ilS6B.js → users-CCrq-w0C.js} +0 -0
  125. /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 { routes, totalRoutes, isLoading, refetch } = useRoutes();
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
- {routes.filter((r) => r.status === 'in_progress').length}
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
- {routes.filter((r) => r.optimized).length}
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: 'Update your password and enable two-factor authentication.',
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
- Passwords are managed by your workspace identity provider.
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, FileText, Edit, Save } from 'lucide-react';
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
- <Card>
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 { vehicles, totalVehicles, isLoading, refetch } = useVehicles();
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
- { label: 'Export', icon: <Download size={16} />, onSelect: () => goTo('import-export') },
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
- {vehicles.filter((v) => v.status === 'available').length}
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
- {vehicles.filter((v) => v.status === 'maintenance').length}
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 { warehouses, totalWarehouses, isLoading, refetch } = useWarehouses();
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={`${totalWarehouses} warehouses`}
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">{totalWarehouses}</p>
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
- {warehouses.length > 0
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
+ }