@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,6 +4,7 @@
4
4
  * Displays detailed information about a specific order.
5
5
  */
6
6
 
7
+ import { useState } from 'react';
7
8
  import { useParams } from 'react-router-dom';
8
9
  import {
9
10
  Package,
@@ -19,6 +20,9 @@ import {
19
20
  CheckCircle,
20
21
  Edit,
21
22
  Printer,
23
+ ArrowRight,
24
+ XCircle,
25
+ UserCheck,
22
26
  } from 'lucide-react';
23
27
  import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
24
28
  import {
@@ -35,6 +39,12 @@ import {
35
39
  Button,
36
40
  Badge,
37
41
  Separator,
42
+ Label,
43
+ Select,
44
+ SelectContent,
45
+ SelectItem,
46
+ SelectTrigger,
47
+ SelectValue,
38
48
  Table,
39
49
  TableBody,
40
50
  TableCell,
@@ -43,15 +53,106 @@ import {
43
53
  TableRow,
44
54
  } from '../components/ui';
45
55
  import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
46
- import { useOrder } from '../hooks/useOrders';
56
+ import { useOrder, useOrders } from '../hooks/useOrders';
57
+ import { useFleet } from '../hooks/useFleet';
58
+ import { FormError } from '../components/FormError';
59
+ import { Notice, useNotice } from '../components/Notice';
60
+ import { DocumentsCard } from '../components/DocumentsCard';
47
61
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
48
62
  import { formatCurrency, formatDate, formatAddress } from '../utils/formatters';
49
63
  import { joinPath } from '../utils/navigation';
64
+ import type { OrderStatus } from '../types';
65
+
66
+ /**
67
+ * Forward order-status pipeline. `updateOrderStatus` walks this sequence one
68
+ * step at a time; CANCELLED is offered separately as a terminal action.
69
+ */
70
+ const STATUS_FLOW: OrderStatus[] = [
71
+ 'pending',
72
+ 'confirmed',
73
+ 'assigned',
74
+ 'picked_up',
75
+ 'in_transit',
76
+ 'out_for_delivery',
77
+ 'delivered',
78
+ ];
79
+
80
+ const STATUS_LABELS: Record<OrderStatus, string> = {
81
+ pending: 'Pending',
82
+ confirmed: 'Confirmed',
83
+ assigned: 'Assigned',
84
+ picked_up: 'Picked Up',
85
+ in_transit: 'In Transit',
86
+ out_for_delivery: 'Out for Delivery',
87
+ delivered: 'Delivered',
88
+ cancelled: 'Cancelled',
89
+ failed: 'Failed',
90
+ };
91
+
92
+ function nextStatus(current: OrderStatus): OrderStatus | null {
93
+ const index = STATUS_FLOW.indexOf(current);
94
+ if (index === -1 || index >= STATUS_FLOW.length - 1) return null;
95
+ return STATUS_FLOW[index + 1];
96
+ }
50
97
 
51
98
  export default function OrderDetailsPage() {
52
99
  const { orderId } = useParams<{ orderId: string }>();
53
100
  const { basePath, navigate } = useMoveTheWheels();
54
101
  const { order, isLoading, error, refetch } = useOrder(orderId);
102
+ const { updateOrder, updateOrderStatus } = useOrders();
103
+ const { vehicles, drivers } = useFleet();
104
+ const { message: notice, notify } = useNotice();
105
+
106
+ const [selectedVehicle, setSelectedVehicle] = useState('');
107
+ const [selectedDriver, setSelectedDriver] = useState('');
108
+ const [assigning, setAssigning] = useState(false);
109
+ const [advancing, setAdvancing] = useState(false);
110
+ const [actionError, setActionError] = useState<string | null>(null);
111
+
112
+ const availableVehicles = vehicles.filter(
113
+ (vehicle) => vehicle.status === 'available' || vehicle.id === order?.vehicleId
114
+ );
115
+ const availableDrivers = drivers.filter(
116
+ (driver) => driver.status === 'available' || driver.id === order?.driverId
117
+ );
118
+
119
+ const handleAssign = async () => {
120
+ if (!orderId || (!selectedVehicle && !selectedDriver)) {
121
+ setActionError('Pick a vehicle and/or a driver to assign.');
122
+ return;
123
+ }
124
+ setAssigning(true);
125
+ setActionError(null);
126
+ try {
127
+ await updateOrder(orderId, {
128
+ vehicleId: selectedVehicle || undefined,
129
+ driverId: selectedDriver || undefined,
130
+ });
131
+ notify('Assignment updated.');
132
+ setSelectedVehicle('');
133
+ setSelectedDriver('');
134
+ refetch();
135
+ } catch (err) {
136
+ setActionError(err instanceof Error ? err.message : 'Failed to update assignment.');
137
+ } finally {
138
+ setAssigning(false);
139
+ }
140
+ };
141
+
142
+ const handleStatusChange = async (status: OrderStatus, successMessage: string) => {
143
+ if (!orderId) return;
144
+ setAdvancing(true);
145
+ setActionError(null);
146
+ try {
147
+ await updateOrderStatus(orderId, status);
148
+ notify(successMessage);
149
+ refetch();
150
+ } catch (err) {
151
+ setActionError(err instanceof Error ? err.message : 'Failed to update status.');
152
+ } finally {
153
+ setAdvancing(false);
154
+ }
155
+ };
55
156
 
56
157
  const goTo = (path: string) => {
57
158
  if (navigate) {
@@ -360,7 +461,7 @@ export default function OrderDetailsPage() {
360
461
  </div>
361
462
  </EmphasisPanel>
362
463
 
363
- {/* Assignment Info */}
464
+ {/* Assignment — assign a vehicle & driver and advance the status */}
364
465
  <Card>
365
466
  <CardHeader>
366
467
  <CardTitle className="flex items-center gap-2">
@@ -370,18 +471,118 @@ export default function OrderDetailsPage() {
370
471
  </CardHeader>
371
472
  <CardContent>
372
473
  <div className="space-y-4">
474
+ {actionError && <FormError message={actionError} />}
475
+ {notice && <Notice message={notice} />}
476
+
373
477
  <div>
374
- <p className="text-sm text-text-secondary">Driver</p>
375
- <p className="text-sm">{order.driverId || 'Not assigned'}</p>
478
+ <p className="text-sm text-text-secondary">Tracking Number</p>
479
+ <p className="text-sm font-mono">{order.trackingNumber || '—'}</p>
376
480
  </div>
377
- <div>
378
- <p className="text-sm text-text-secondary">Vehicle</p>
379
- <p className="text-sm">{order.vehicleId || 'Not assigned'}</p>
481
+
482
+ <Separator />
483
+
484
+ <div className="space-y-3">
485
+ <div className="space-y-1.5">
486
+ <Label htmlFor="assign-vehicle">Vehicle</Label>
487
+ <Select value={selectedVehicle} onValueChange={setSelectedVehicle}>
488
+ <SelectTrigger id="assign-vehicle">
489
+ <SelectValue
490
+ placeholder={
491
+ order.vehicleId ? `Current: ${order.vehicleId}` : 'Select a vehicle'
492
+ }
493
+ />
494
+ </SelectTrigger>
495
+ <SelectContent>
496
+ {availableVehicles.length === 0 ? (
497
+ <SelectItem value="none" disabled>
498
+ No available vehicles
499
+ </SelectItem>
500
+ ) : (
501
+ availableVehicles.map((vehicle) => (
502
+ <SelectItem key={vehicle.id} value={vehicle.id}>
503
+ {vehicle.vehicleNumber} ({vehicle.type})
504
+ </SelectItem>
505
+ ))
506
+ )}
507
+ </SelectContent>
508
+ </Select>
509
+ </div>
510
+
511
+ <div className="space-y-1.5">
512
+ <Label htmlFor="assign-driver">Driver</Label>
513
+ <Select value={selectedDriver} onValueChange={setSelectedDriver}>
514
+ <SelectTrigger id="assign-driver">
515
+ <SelectValue
516
+ placeholder={
517
+ order.driverId ? `Current: ${order.driverId}` : 'Select a driver'
518
+ }
519
+ />
520
+ </SelectTrigger>
521
+ <SelectContent>
522
+ {availableDrivers.length === 0 ? (
523
+ <SelectItem value="none" disabled>
524
+ No available drivers
525
+ </SelectItem>
526
+ ) : (
527
+ availableDrivers.map((driver) => (
528
+ <SelectItem key={driver.id} value={driver.id}>
529
+ {driver.fullName} ({driver.employeeId})
530
+ </SelectItem>
531
+ ))
532
+ )}
533
+ </SelectContent>
534
+ </Select>
535
+ </div>
536
+
537
+ <Button
538
+ className="w-full"
539
+ onClick={handleAssign}
540
+ disabled={assigning || (!selectedVehicle && !selectedDriver)}
541
+ >
542
+ <UserCheck size={16} />
543
+ {assigning ? 'Assigning…' : 'Assign driver & vehicle'}
544
+ </Button>
380
545
  </div>
381
- <div>
382
- <p className="text-sm text-text-secondary">Tracking Number</p>
383
- <p className="text-sm font-mono">{order.trackingNumber}</p>
546
+
547
+ <Separator />
548
+
549
+ <div className="space-y-2">
550
+ <p className="text-sm text-text-secondary">
551
+ Current status: <span className="font-medium">{STATUS_LABELS[order.status]}</span>
552
+ </p>
553
+ {(() => {
554
+ const upcoming = nextStatus(order.status);
555
+ if (!upcoming) return null;
556
+ return (
557
+ <Button
558
+ variant="outline"
559
+ className="w-full"
560
+ onClick={() =>
561
+ handleStatusChange(
562
+ upcoming,
563
+ `Order advanced to ${STATUS_LABELS[upcoming]}.`
564
+ )
565
+ }
566
+ disabled={advancing}
567
+ >
568
+ <ArrowRight size={16} />
569
+ {advancing ? 'Updating…' : `Advance to ${STATUS_LABELS[upcoming]}`}
570
+ </Button>
571
+ );
572
+ })()}
573
+ {order.status !== 'cancelled' && order.status !== 'delivered' && (
574
+ <Button
575
+ variant="outline"
576
+ className="w-full text-status-error-text"
577
+ onClick={() => handleStatusChange('cancelled', 'Order cancelled.')}
578
+ disabled={advancing}
579
+ >
580
+ <XCircle size={16} />
581
+ Cancel order
582
+ </Button>
583
+ )}
384
584
  </div>
585
+
385
586
  <Button
386
587
  variant="outline"
387
588
  className="w-full"
@@ -394,6 +595,9 @@ export default function OrderDetailsPage() {
394
595
  </CardContent>
395
596
  </Card>
396
597
 
598
+ {/* Documents — real files via wspace-files-svc */}
599
+ <DocumentsCard entityType="order" entityId={order.id} />
600
+
397
601
  {/* Special Instructions */}
398
602
  {order.specialInstructions && (
399
603
  <Card>
@@ -5,11 +5,23 @@
5
5
  */
6
6
 
7
7
  import { useState } from 'react';
8
- import { Package, Plus, Filter, Download, RefreshCw } from 'lucide-react';
8
+ import {
9
+ Package,
10
+ Plus,
11
+ Filter,
12
+ Download,
13
+ RefreshCw,
14
+ UserPlus,
15
+ UserCheck,
16
+ Clock,
17
+ Truck,
18
+ CheckCircle2,
19
+ } from 'lucide-react';
9
20
  import { PageLayout } from '../components/PageLayout';
21
+ import { MetricCard, MetricGrid } from '../components/MetricCard';
10
22
  import { DataTable, type Column } from '../components/DataTable';
11
23
  import { StatusBadge, PriorityBadge, TypeBadge } from '../components/StatusBadge';
12
- import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
24
+ import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
13
25
  import {
14
26
  Button,
15
27
  Card,
@@ -21,7 +33,7 @@ import {
21
33
  SelectValue,
22
34
  Badge,
23
35
  } from '../components/ui';
24
- import { useOrders } from '../hooks/useOrders';
36
+ import { useOrders, useOrderStats } from '../hooks/useOrders';
25
37
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
26
38
  import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';
27
39
  import { formatCurrency, formatDate } from '../utils/formatters';
@@ -37,21 +49,36 @@ export default function OrdersListPage() {
37
49
  const { basePath, navigate } = useMoveTheWheels();
38
50
  const [statusFilter, setStatusFilter] = useState<OrderStatus | 'all'>('all');
39
51
  const [typeFilter, setTypeFilter] = useState<'b2b' | 'b2c' | 'all'>('all');
52
+ const [unassignedOnly, setUnassignedOnly] = useState(false);
53
+ const [page, setPage] = useState(0);
54
+ const [pageSize, setPageSize] = useState(25);
55
+
56
+ const filtersActive = statusFilter !== 'all' || typeFilter !== 'all' || unassignedOnly;
40
57
 
41
- const { orders, totalOrders, isLoading, refetch } = useOrders({
58
+ const { orders, hasMore, isLoading, refetch } = useOrders({
42
59
  filters: {
43
60
  status: statusFilter === 'all' ? undefined : statusFilter,
44
61
  orderType: typeFilter === 'all' ? undefined : typeFilter,
62
+ unassigned: unassignedOnly || undefined,
45
63
  },
46
- limit: 50,
64
+ limit: pageSize,
65
+ offset: page * pageSize,
47
66
  });
48
67
 
68
+ // Pipeline totals + "at a glance" tiles come from the live aggregate, never
69
+ // from the truncated current page.
70
+ const { stats } = useOrderStats();
71
+ const totalOrders = stats.total;
72
+
49
73
  const goTo = (path: string) => {
50
74
  if (navigate) {
51
75
  navigate(joinPath(basePath, path));
52
76
  }
53
77
  };
54
78
 
79
+ // Reset to the first page whenever the active filter set changes.
80
+ const resetPage = () => setPage(0);
81
+
55
82
  const columns: Column<Order>[] = [
56
83
  {
57
84
  id: 'orderNumber',
@@ -129,6 +156,29 @@ export default function OrdersListPage() {
129
156
  className: 'min-w-[100px] whitespace-nowrap',
130
157
  cell: (row) => formatDate(row.createdAt, 'relative'),
131
158
  },
159
+ {
160
+ id: 'driver',
161
+ header: t('movethewheels.orders.table.driver', 'Driver'),
162
+ className: 'min-w-[120px] whitespace-nowrap',
163
+ cell: (row) =>
164
+ row.driverId ? (
165
+ <span className="inline-flex items-center gap-1 text-sm text-status-success-text">
166
+ <UserCheck size={14} /> {t('movethewheels.orders.table.assigned', 'Assigned')}
167
+ </span>
168
+ ) : (
169
+ <Button
170
+ variant="outline"
171
+ size="sm"
172
+ onClick={(e) => {
173
+ e.stopPropagation();
174
+ goTo(`orders/${row.id}`);
175
+ }}
176
+ >
177
+ <UserPlus size={14} className="mr-1" />
178
+ {t('movethewheels.orders.table.assign', 'Assign')}
179
+ </Button>
180
+ ),
181
+ },
132
182
  ];
133
183
 
134
184
  const statusOptions: { value: OrderStatus | 'all'; label: string }[] = [
@@ -181,47 +231,40 @@ export default function OrdersListPage() {
181
231
  )}
182
232
  </PagePurpose>
183
233
 
184
- {/* Pipeline at a glance — the page's single emphasis zone */}
185
- <EmphasisPanel>
186
- <p className="text-sm font-medium text-text-secondary">
234
+ {/* Pipeline at a glance — premium stat cards */}
235
+ <div>
236
+ <p className="mb-3 text-sm font-medium text-text-secondary">
187
237
  {t('movethewheels.orders.glance.title', 'Pipeline at a glance')}
188
238
  </p>
189
- <div className="mt-3 grid grid-cols-2 sm:grid-cols-4 gap-4">
190
- <div>
191
- <p className="text-2xl font-bold text-text-primary tabular-nums">{totalOrders}</p>
192
- <p className="text-xs text-text-secondary">
193
- {t('movethewheels.orders.glance.total', 'Total orders')}
194
- </p>
195
- </div>
196
- <div>
197
- <p className="text-2xl font-bold text-text-primary tabular-nums">
198
- {orders.filter((o) => o.status === 'pending').length}
199
- </p>
200
- <p className="text-xs text-text-secondary">
201
- {t('movethewheels.orders.glance.pending', 'Pending')}
202
- </p>
203
- </div>
204
- <div>
205
- <p className="text-2xl font-bold text-text-primary tabular-nums">
206
- {
207
- orders.filter((o) => o.status === 'in_transit' || o.status === 'out_for_delivery')
208
- .length
209
- }
210
- </p>
211
- <p className="text-xs text-text-secondary">
212
- {t('movethewheels.orders.glance.inTransit', 'In transit')}
213
- </p>
214
- </div>
215
- <div>
216
- <p className="text-2xl font-bold text-text-primary tabular-nums">
217
- {orders.filter((o) => o.status === 'delivered').length}
218
- </p>
219
- <p className="text-xs text-text-secondary">
220
- {t('movethewheels.orders.glance.delivered', 'Delivered')}
221
- </p>
222
- </div>
223
- </div>
224
- </EmphasisPanel>
239
+ <MetricGrid columns={4}>
240
+ <MetricCard
241
+ label={t('movethewheels.orders.glance.total', 'Total orders')}
242
+ value={totalOrders}
243
+ icon={<Package size={20} />}
244
+ accent="brand"
245
+ />
246
+ <MetricCard
247
+ label={t('movethewheels.orders.glance.pending', 'Pending')}
248
+ value={stats.byStatus.pending}
249
+ icon={<Clock size={20} />}
250
+ accent="warning"
251
+ subtitle={t('movethewheels.orders.glance.pendingSub', 'Awaiting fulfilment')}
252
+ />
253
+ <MetricCard
254
+ label={t('movethewheels.orders.glance.inTransit', 'In transit')}
255
+ value={stats.byStatus.in_transit + stats.byStatus.out_for_delivery}
256
+ icon={<Truck size={20} />}
257
+ accent="blue"
258
+ subtitle={t('movethewheels.orders.glance.inTransitSub', 'On the road now')}
259
+ />
260
+ <MetricCard
261
+ label={t('movethewheels.orders.glance.delivered', 'Delivered')}
262
+ value={stats.byStatus.delivered}
263
+ icon={<CheckCircle2 size={20} />}
264
+ accent="success"
265
+ />
266
+ </MetricGrid>
267
+ </div>
225
268
 
226
269
  {/* Filters */}
227
270
  <Card>
@@ -235,7 +278,10 @@ export default function OrdersListPage() {
235
278
  </div>
236
279
  <Select
237
280
  value={statusFilter}
238
- onValueChange={(value) => setStatusFilter(value as OrderStatus | 'all')}
281
+ onValueChange={(value) => {
282
+ setStatusFilter(value as OrderStatus | 'all');
283
+ resetPage();
284
+ }}
239
285
  >
240
286
  <SelectTrigger className="w-[180px]">
241
287
  <SelectValue placeholder={t('movethewheels.orders.filters.status', 'Status')} />
@@ -250,7 +296,10 @@ export default function OrdersListPage() {
250
296
  </Select>
251
297
  <Select
252
298
  value={typeFilter}
253
- onValueChange={(value) => setTypeFilter(value as 'b2b' | 'b2c' | 'all')}
299
+ onValueChange={(value) => {
300
+ setTypeFilter(value as 'b2b' | 'b2c' | 'all');
301
+ resetPage();
302
+ }}
254
303
  >
255
304
  <SelectTrigger className="w-[150px]">
256
305
  <SelectValue placeholder={t('movethewheels.orders.filters.type', 'Type')} />
@@ -263,13 +312,26 @@ export default function OrdersListPage() {
263
312
  <SelectItem value="b2c">B2C</SelectItem>
264
313
  </SelectContent>
265
314
  </Select>
266
- {(statusFilter !== 'all' || typeFilter !== 'all') && (
315
+ <Button
316
+ variant={unassignedOnly ? 'default' : 'outline'}
317
+ size="sm"
318
+ onClick={() => {
319
+ setUnassignedOnly((prev) => !prev);
320
+ resetPage();
321
+ }}
322
+ >
323
+ <UserPlus size={14} className="mr-1" />
324
+ {t('movethewheels.orders.filters.unassigned', 'Needs driver')}
325
+ </Button>
326
+ {filtersActive && (
267
327
  <Button
268
328
  variant="ghost"
269
329
  size="sm"
270
330
  onClick={() => {
271
331
  setStatusFilter('all');
272
332
  setTypeFilter('all');
333
+ setUnassignedOnly(false);
334
+ resetPage();
273
335
  }}
274
336
  >
275
337
  {t('movethewheels.orders.filters.clear', 'Clear filters')}
@@ -289,6 +351,15 @@ export default function OrdersListPage() {
289
351
  searchFields={['orderNumber', 'customerInfo'] as (keyof Order)[]}
290
352
  onRowClick={(row) => goTo(`orders/${row.id}`)}
291
353
  getRowId={(row) => row.id}
354
+ pageIndex={page}
355
+ pageSize={pageSize}
356
+ onPageChange={setPage}
357
+ onPageSizeChange={(size) => {
358
+ setPageSize(size);
359
+ setPage(0);
360
+ }}
361
+ totalItems={filtersActive ? undefined : totalOrders}
362
+ hasNextPage={hasMore}
292
363
  emptyMessage={t('movethewheels.orders.empty', 'No orders found')}
293
364
  emptyIllustration="empty-data"
294
365
  emptyTitle={t('movethewheels.orders.emptyTitle', 'No orders yet')}
@@ -2,12 +2,14 @@
2
2
  * Products Page - List of all products
3
3
  */
4
4
 
5
- import { Package, Plus, RefreshCw, Download } from 'lucide-react';
5
+ import { useState } from 'react';
6
+ import { Package, Plus, RefreshCw, Download, AlertTriangle, Thermometer } 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 } from '../components/ui';
9
- import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
10
- import { useProducts } from '../hooks/useInventory';
11
+ import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
12
+ import { useProducts, useProductStats } from '../hooks/useInventory';
11
13
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
12
14
  import { formatCurrency } from '../utils/formatters';
13
15
  import { joinPath } from '../utils/navigation';
@@ -15,7 +17,14 @@ import type { Product } from '../types';
15
17
 
16
18
  export default function ProductsPage() {
17
19
  const { basePath, navigate } = useMoveTheWheels();
18
- const { products, totalProducts, isLoading, refetch } = useProducts();
20
+ const [page, setPage] = useState(0);
21
+ const [pageSize, setPageSize] = useState(25);
22
+ const { products, hasMore, isLoading, refetch } = useProducts({
23
+ limit: pageSize,
24
+ offset: page * pageSize,
25
+ });
26
+ // Bounded aggregate for the tiles + subtitle — not the truncated page.
27
+ const { stats } = useProductStats();
19
28
 
20
29
  const goTo = (path: string) => navigate?.(joinPath(basePath, path));
21
30
 
@@ -94,7 +103,7 @@ export default function ProductsPage() {
94
103
  return (
95
104
  <PageLayout
96
105
  title="Products"
97
- subtitle={`${totalProducts} products`}
106
+ subtitle={`${stats.total} products`}
98
107
  icon={<Package size={24} />}
99
108
  headerActions={
100
109
  <CTAOverflowMenu
@@ -105,7 +114,11 @@ export default function ProductsPage() {
105
114
  }}
106
115
  actions={[
107
116
  { label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
108
- { label: 'Export', icon: <Download size={16} />, onSelect: () => goTo('import-export') },
117
+ {
118
+ label: 'Export',
119
+ icon: <Download size={16} />,
120
+ onSelect: () => goTo('import-export'),
121
+ },
109
122
  ]}
110
123
  />
111
124
  }
@@ -116,28 +129,31 @@ export default function ProductsPage() {
116
129
  routed — keep them accurate so shipments are quoted and handled correctly.
117
130
  </PagePurpose>
118
131
 
119
- {/* Catalog at a glance — the page's single emphasis zone */}
120
- <EmphasisPanel>
121
- <p className="text-sm font-medium text-text-secondary">Catalog at a glance</p>
122
- <div className="mt-3 grid grid-cols-3 gap-4">
123
- <div>
124
- <p className="text-2xl font-bold text-text-primary tabular-nums">{totalProducts}</p>
125
- <p className="text-xs text-text-secondary">Total products</p>
126
- </div>
127
- <div>
128
- <p className="text-2xl font-bold text-text-primary tabular-nums">
129
- {products.filter((p) => p.fragile || p.hazardous).length}
130
- </p>
131
- <p className="text-xs text-text-secondary">Special handling</p>
132
- </div>
133
- <div>
134
- <p className="text-2xl font-bold text-text-primary tabular-nums">
135
- {products.filter((p) => p.temperatureControlled).length}
136
- </p>
137
- <p className="text-xs text-text-secondary">Temp-controlled</p>
138
- </div>
139
- </div>
140
- </EmphasisPanel>
132
+ {/* Catalog at a glance — premium stat cards */}
133
+ <div>
134
+ <p className="mb-3 text-sm font-medium text-text-secondary">Catalog at a glance</p>
135
+ <MetricGrid columns={3}>
136
+ <MetricCard
137
+ label="Total products"
138
+ value={stats.total}
139
+ icon={<Package size={20} />}
140
+ accent="brand"
141
+ />
142
+ <MetricCard
143
+ label="Special handling"
144
+ value={stats.specialHandling}
145
+ icon={<AlertTriangle size={20} />}
146
+ accent="warning"
147
+ subtitle="Fragile or hazardous"
148
+ />
149
+ <MetricCard
150
+ label="Temp-controlled"
151
+ value={stats.temperatureControlled}
152
+ icon={<Thermometer size={20} />}
153
+ accent="blue"
154
+ />
155
+ </MetricGrid>
156
+ </div>
141
157
 
142
158
  <DataTable
143
159
  columns={columns}
@@ -148,6 +164,14 @@ export default function ProductsPage() {
148
164
  searchFields={['name', 'sku', 'category'] as (keyof Product)[]}
149
165
  onRowClick={(row) => goTo(`inventory/products/${row.id}`)}
150
166
  getRowId={(row) => row.id}
167
+ pageIndex={page}
168
+ pageSize={pageSize}
169
+ onPageChange={setPage}
170
+ onPageSizeChange={(size) => {
171
+ setPageSize(size);
172
+ setPage(0);
173
+ }}
174
+ hasNextPage={hasMore}
151
175
  emptyIllustration="empty-files"
152
176
  emptyTitle="No products yet"
153
177
  emptyDescription="Add products to your catalog so they can be added to orders and stocked in warehouses."