@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.
Files changed (173) hide show
  1. package/dist/{AIAssistantPage-C3cPsWt-.js → AIAssistantPage-D2gaEL1U.js} +16 -15
  2. package/dist/AnalyticsPage-DZMlEoBi.js +194 -0
  3. package/dist/CreateOrderPage-CWHx8yys.js +490 -0
  4. package/dist/CustomerDetailsPage-nR4oZ_ju.js +615 -0
  5. package/dist/CustomersPage-Bbvwvn8k.js +205 -0
  6. package/dist/DashboardPage-DrEhNf7N.js +542 -0
  7. package/dist/DataTable-CqMUD4er.js +340 -0
  8. package/dist/DocumentsCard-edDkgnJj.js +169 -0
  9. package/dist/DriverDetailsPage-whEMOWs9.js +490 -0
  10. package/dist/DriversPage-CHF2ZEzd.js +221 -0
  11. package/dist/FinancePage-Dn9GrW-1.js +175 -0
  12. package/dist/{FleetPage-DqkFi3s3.js → FleetPage-CrtGPy-p.js} +36 -35
  13. package/dist/{FormError-BlOjnXOq.js → FormError-BKcb_wT-.js} +1 -1
  14. package/dist/{ImportExportPage-DKJRCezl.js → ImportExportPage-WYdwZomZ.js} +52 -72
  15. package/dist/InventoryPage-l5A1f753.js +259 -0
  16. package/dist/LiveTrackingPage-DhEaNKZ4.js +429 -0
  17. package/dist/MarketplacePage-B9RYOb1Y.js +426 -0
  18. package/dist/MetricCard-y3bWHKUQ.js +191 -0
  19. package/dist/Notice-DOO-tjpV.js +46 -0
  20. package/dist/OrderDetailsPage-CwYHwk1K.js +584 -0
  21. package/dist/OrdersListPage-DhZJoQeI.js +386 -0
  22. package/dist/{PageLayout-D4LDHJJq.js → PageLayout-BniRJaQ6.js} +1 -1
  23. package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-1Gm9JKy0.js} +56 -56
  24. package/dist/ProductsPage-C8sn-1tN.js +186 -0
  25. package/dist/ReportsPage-D1tb0dcz.js +377 -0
  26. package/dist/RouteDetailsPage-CkMyR9b_.js +610 -0
  27. package/dist/{RoutesPage-DMfoicM1.js → RoutesPage-QkENzsE5.js} +58 -47
  28. package/dist/{SettingsPage-Bn-v0mbQ.js → SettingsPage-DV3_tU_X.js} +16 -38
  29. package/dist/Sparkline-K2RMJ58u.js +16729 -0
  30. package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-BNHXYvZx.js} +7 -7
  31. package/dist/TrackingPage-BZrg4RCu.js +368 -0
  32. package/dist/VehicleDetailsPage-D-8XPZ2G.js +393 -0
  33. package/dist/VehiclesPage-D5BELbtM.js +184 -0
  34. package/dist/{WarehouseDetailsPage-L_6sWigM.js → WarehouseDetailsPage-Cmg-8Frn.js} +70 -70
  35. package/dist/WarehousesPage-C1Y1M867.js +182 -0
  36. package/dist/{arrow-left-n48Y-Wis.js → arrow-left-ClW4Isj7.js} +1 -1
  37. package/dist/arrow-right-CmKOmIkh.js +11 -0
  38. package/dist/arrow-up-BJijRLAZ.js +17 -0
  39. package/dist/{box-D9Pal4HM.js → box-BT3NsRQP.js} +1 -1
  40. package/dist/boxes-B_OGvQiW.js +54 -0
  41. package/dist/{chart-column-BFIJysDR.js → chart-column-DQTTdUPk.js} +1 -1
  42. package/dist/charts-Tj_PPTfA.js +2018 -0
  43. package/dist/chevron-left-B1rZyWWb.js +8 -0
  44. package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-BIcwUJZq.js} +1 -1
  45. package/dist/chunk-BdsCSvBP.js +20 -0
  46. package/dist/{circle-alert-iY24lSlp.js → circle-alert-CNkuDNc6.js} +1 -1
  47. package/dist/circle-check-Cs1OVjtV.js +13 -0
  48. package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-C-p4eNwn.js} +1 -1
  49. package/dist/{clock-DJZtdl8u.js → clock-dRGG4rkF.js} +1 -1
  50. package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-B6RoYHHz.js} +1 -1
  51. package/dist/{download-DRXxoiQR.js → download-DP04iWTd.js} +1 -1
  52. package/dist/{file-text-DWWY6_f5.js → file-text-BuDx4hZp.js} +1 -1
  53. package/dist/{filter-DloLC2eE.js → filter-BD483_8g.js} +1 -1
  54. package/dist/{wrench-CANXWxF-.js → fuel-DLH1ag04.js} +3 -6
  55. package/dist/index.css +2 -0
  56. package/dist/index.js +150 -172
  57. package/dist/{map-CkJaULb2.js → map-DNQn9fyY.js} +1 -1
  58. package/dist/{map-pin-BGiCvyCs.js → map-pin-CqN1bbT1.js} +1 -1
  59. package/dist/mapMarkers-NCV9MRKR.js +28 -0
  60. package/dist/maps-yzLqiK_C.js +5070 -0
  61. package/dist/{navigation-D36prnAn.js → navigation-CgGUt2tR.js} +1 -1
  62. package/dist/{package-BChUzpJS.js → package-DmRF7iQD.js} +1 -1
  63. package/dist/{phone-Ac9s3mo_.js → phone-Cl32EXKW.js} +1 -1
  64. package/dist/{plus-Z76q-FZm.js → plus-DlANgOFx.js} +1 -1
  65. package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-rW59D4b7.js} +1 -1
  66. package/dist/route-CWjPrtAN.js +22 -0
  67. package/dist/{save-CojywNhG.js → save-UDlog7hg.js} +1 -1
  68. package/dist/{search-CDGefqb3.js → search-DNIG5Ck7.js} +1 -1
  69. package/dist/{square-pen-CopE9GmB.js → square-pen-BcfB3Grd.js} +1 -1
  70. package/dist/{star-nHlLjDKR.js → star-CVZHFwDx.js} +1 -1
  71. package/dist/{store-BR9Voi5E.js → store-bwnmCK9f.js} +1 -1
  72. package/dist/{trash-2-BKhar6Hs.js → trash-2-B80G12d4.js} +1 -1
  73. package/dist/{trending-up-Dohgd4t_.js → trending-up-CH4R4KOa.js} +1 -1
  74. package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-CmS8AUU6.js} +1 -1
  75. package/dist/{truck-C0v_hbLR.js → truck-9rLDncav.js} +1 -1
  76. package/dist/upload-DanJY1Vo.js +21 -0
  77. package/dist/{useAssistant-D4v3oxD3.js → useAssistant-CnEwkTLm.js} +11 -28
  78. package/dist/{useCustomers-BmTNGxiI.js → useCustomers-ChJao1ET.js} +30 -27
  79. package/dist/{useFleet-DKqXZ16m.js → useFleet-g5Yri0G8.js} +82 -70
  80. package/dist/{useImportExport-DHr-SWSw.js → useImportExport-MYwIT3rE.js} +1 -1
  81. package/dist/{useInventory-DkyiVY_A.js → useInventory-BSOn-R1c.js} +84 -39
  82. package/dist/{useOrders-Cfk1GLiq.js → useOrders-BSiqo61w.js} +59 -48
  83. package/dist/{useReports-CTqZr-AB.js → useReports-fWESfsF9.js} +41 -35
  84. package/dist/{useRoutes-BQWO607N.js → useRoutes-zD8Yp323.js} +50 -42
  85. package/dist/{useSettings-CkDH_-gM.js → useSettings-B1lsWeZ1.js} +6 -6
  86. package/dist/{useTracking-B-va1kO3.js → useTracking-BKG0I2ln.js} +3 -3
  87. package/dist/{user-jyFU3lXk.js → user-DU51el3m.js} +1 -1
  88. package/dist/user-check-IMhk-qkX.js +20 -0
  89. package/dist/{users-DZ-ilS6B.js → users-B8KhJBGg.js} +1 -1
  90. package/dist/{warehouse-6m5kXMLu.js → warehouse-CcMMioeY.js} +1 -1
  91. package/dist/wrench-Dg-sgH2e.js +8 -0
  92. package/dist/x-C4ObLVEa.js +11 -0
  93. package/package.json +5 -1
  94. package/src/movethewheels/components/DataTable.tsx +252 -84
  95. package/src/movethewheels/components/DocumentsCard.tsx +102 -0
  96. package/src/movethewheels/components/GlassPanel.tsx +71 -0
  97. package/src/movethewheels/components/MetricCard.tsx +171 -73
  98. package/src/movethewheels/components/Notice.tsx +61 -0
  99. package/src/movethewheels/components/StatusBadge.tsx +7 -6
  100. package/src/movethewheels/components/charts/ChartShell.tsx +78 -0
  101. package/src/movethewheels/components/charts/Sparkline.tsx +85 -0
  102. package/src/movethewheels/components/charts/StatusDonut.tsx +115 -0
  103. package/src/movethewheels/components/charts/TrendChart.tsx +179 -0
  104. package/src/movethewheels/components/charts/VolumeBar.tsx +97 -0
  105. package/src/movethewheels/components/charts/chartTokens.ts +43 -0
  106. package/src/movethewheels/components/charts/index.ts +9 -0
  107. package/src/movethewheels/components/index.ts +51 -1
  108. package/src/movethewheels/components/maps/LogisticsMap.tsx +94 -0
  109. package/src/movethewheels/components/maps/MapCanvas.tsx +118 -0
  110. package/src/movethewheels/components/maps/RouteMap.tsx +71 -0
  111. package/src/movethewheels/components/maps/TrackingMap.tsx +102 -0
  112. package/src/movethewheels/components/maps/index.ts +16 -0
  113. package/src/movethewheels/components/maps/leaflet-setup.ts +124 -0
  114. package/src/movethewheels/components/maps/map-markers.css +121 -0
  115. package/src/movethewheels/hooks/index.ts +9 -0
  116. package/src/movethewheels/hooks/useAnalytics.ts +109 -4
  117. package/src/movethewheels/hooks/useCustomers.ts +11 -4
  118. package/src/movethewheels/hooks/useEntityDocuments.ts +132 -0
  119. package/src/movethewheels/hooks/useFleet.ts +26 -7
  120. package/src/movethewheels/hooks/useInventory.ts +70 -7
  121. package/src/movethewheels/hooks/useOrders.ts +32 -2
  122. package/src/movethewheels/hooks/useReports.ts +14 -4
  123. package/src/movethewheels/hooks/useRoutes.ts +20 -3
  124. package/src/movethewheels/pages/AnalyticsPage.tsx +93 -73
  125. package/src/movethewheels/pages/CreateOrderPage.tsx +48 -20
  126. package/src/movethewheels/pages/CustomerDetailsPage.tsx +239 -33
  127. package/src/movethewheels/pages/CustomersPage.tsx +53 -28
  128. package/src/movethewheels/pages/DashboardPage.tsx +238 -12
  129. package/src/movethewheels/pages/DriverDetailsPage.tsx +17 -31
  130. package/src/movethewheels/pages/DriversPage.tsx +59 -31
  131. package/src/movethewheels/pages/FinancePage.tsx +103 -82
  132. package/src/movethewheels/pages/InventoryPage.tsx +15 -3
  133. package/src/movethewheels/pages/LiveTrackingPage.tsx +124 -16
  134. package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
  135. package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
  136. package/src/movethewheels/pages/OrdersListPage.tsx +118 -47
  137. package/src/movethewheels/pages/ProductsPage.tsx +52 -28
  138. package/src/movethewheels/pages/ReportsPage.tsx +88 -4
  139. package/src/movethewheels/pages/RouteDetailsPage.tsx +119 -19
  140. package/src/movethewheels/pages/RoutesPage.tsx +22 -6
  141. package/src/movethewheels/pages/SettingsPage.tsx +5 -21
  142. package/src/movethewheels/pages/TrackingPage.tsx +76 -6
  143. package/src/movethewheels/pages/VehicleDetailsPage.tsx +4 -34
  144. package/src/movethewheels/pages/VehiclesPage.tsx +58 -27
  145. package/src/movethewheels/pages/WarehouseDetailsPage.tsx +1 -1
  146. package/src/movethewheels/pages/WarehousesPage.tsx +47 -36
  147. package/src/movethewheels/types/index.ts +9 -1
  148. package/src/movethewheels/utils/download.ts +62 -0
  149. package/src/movethewheels/utils/mapMarkers.ts +58 -0
  150. package/dist/AnalyticsPage-Dz6g2MvK.js +0 -215
  151. package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
  152. package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
  153. package/dist/CustomersPage-BKxMS1AV.js +0 -161
  154. package/dist/DashboardPage-BVjQi0EB.js +0 -387
  155. package/dist/DataTable-DmciIKKM.js +0 -249
  156. package/dist/DriverDetailsPage-CN3WQ0B9.js +0 -507
  157. package/dist/DriversPage-CH9rjQC-.js +0 -165
  158. package/dist/FinancePage-DEDykSOP.js +0 -158
  159. package/dist/InventoryPage-e0ze63iX.js +0 -253
  160. package/dist/LiveTrackingPage-DCX-K-LR.js +0 -379
  161. package/dist/MarketplacePage-CIQ97_zp.js +0 -247
  162. package/dist/MetricCard-BY0QrMD4.js +0 -146
  163. package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
  164. package/dist/OrdersListPage-Cn90P1Xx.js +0 -307
  165. package/dist/ProductsPage-QJfHHpqB.js +0 -169
  166. package/dist/ReportsPage-Zp53q3_8.js +0 -284
  167. package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
  168. package/dist/TrackingPage-W-jtdM3Y.js +0 -325
  169. package/dist/VehicleDetailsPage-Bceo5oz3.js +0 -410
  170. package/dist/VehiclesPage-ApKThYeG.js +0 -165
  171. package/dist/WarehousesPage-DCbXXXWE.js +0 -162
  172. package/dist/useAnalytics-DS8n0xgg.js +0 -297
  173. /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, 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' },
@@ -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
- <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,30 @@
2
2
  * Vehicles Page - List of all vehicles
3
3
  */
4
4
 
5
- import { Truck, Plus, RefreshCw, Download } from 'lucide-react';
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, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
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 { vehicles, totalVehicles, isLoading, refetch } = useVehicles();
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
- { label: 'Export', icon: <Download size={16} />, onSelect: () => goTo('import-export') },
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 — the page's single emphasis zone */}
117
- <EmphasisPanel>
118
- <p className="text-sm font-medium text-text-secondary">Fleet at a glance</p>
119
- <div className="mt-3 grid grid-cols-3 gap-4">
120
- <div>
121
- <p className="text-2xl font-bold text-text-primary tabular-nums">{totalVehicles}</p>
122
- <p className="text-xs text-text-secondary">Total vehicles</p>
123
- </div>
124
- <div>
125
- <p className="text-2xl font-bold text-text-primary tabular-nums">
126
- {vehicles.filter((v) => v.status === 'available').length}
127
- </p>
128
- <p className="text-xs text-text-secondary">Available now</p>
129
- </div>
130
- <div>
131
- <p className="text-2xl font-bold text-text-primary tabular-nums">
132
- {vehicles.filter((v) => v.status === 'maintenance').length}
133
- </p>
134
- <p className="text-xs text-text-secondary">In maintenance</p>
135
- </div>
136
- </div>
137
- </EmphasisPanel>
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 { Warehouse, Plus, RefreshCw } from 'lucide-react';
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, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
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 { warehouses, totalWarehouses, isLoading, refetch } = useWarehouses();
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={`${totalWarehouses} warehouses`}
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 — the page's single emphasis zone */}
117
- <EmphasisPanel>
118
- <p className="text-sm font-medium text-text-secondary">Network at a glance</p>
119
- <div className="mt-3 grid grid-cols-3 gap-4">
120
- <div>
121
- <p className="text-2xl font-bold text-text-primary tabular-nums">{totalWarehouses}</p>
122
- <p className="text-xs text-text-secondary">Total sites</p>
123
- </div>
124
- <div>
125
- <p className="text-2xl font-bold text-text-primary tabular-nums">
126
- {warehouses.filter((w) => w.isActive).length}
127
- </p>
128
- <p className="text-xs text-text-secondary">Active</p>
129
- </div>
130
- <div>
131
- <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
- : '—'}
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
+ }