@burdenoff/microfe-movethewheels 2026.703.2 → 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 (126) 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-2Y05LVyh.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-B0Zpx8OQ.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-DwfgCUQq.js → InventoryPage-DpiqVu1v.js} +9 -9
  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-BjDlCKJd.js → OrdersListPage-CAMsuUck.js} +166 -89
  21. package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-wsxAQkA0.js} +12 -12
  22. package/dist/{ProductsPage-C1G9h7Xd.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-VBtOep91.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/InventoryPage.tsx +1 -1
  71. package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
  72. package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
  73. package/src/movethewheels/pages/OrdersListPage.tsx +77 -14
  74. package/src/movethewheels/pages/ProductsPage.tsx +27 -7
  75. package/src/movethewheels/pages/ReportsPage.tsx +65 -4
  76. package/src/movethewheels/pages/RouteDetailsPage.tsx +74 -13
  77. package/src/movethewheels/pages/RoutesPage.tsx +22 -6
  78. package/src/movethewheels/pages/SettingsPage.tsx +3 -19
  79. package/src/movethewheels/pages/VehicleDetailsPage.tsx +3 -33
  80. package/src/movethewheels/pages/VehiclesPage.tsx +26 -5
  81. package/src/movethewheels/pages/WarehousesPage.tsx +22 -17
  82. package/src/movethewheels/types/index.ts +8 -0
  83. package/src/movethewheels/utils/download.ts +62 -0
  84. package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
  85. package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
  86. package/dist/DataTable-DmciIKKM.js +0 -249
  87. package/dist/FinancePage-DEDykSOP.js +0 -158
  88. package/dist/MarketplacePage-CIQ97_zp.js +0 -247
  89. package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
  90. package/dist/ReportsPage-Zp53q3_8.js +0 -284
  91. package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
  92. /package/dist/{MetricCard-BY0QrMD4.js → MetricCard-CVBVpu7o.js} +0 -0
  93. /package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-C87mDpDP.js} +0 -0
  94. /package/dist/{box-D9Pal4HM.js → box-Eu05PXDh.js} +0 -0
  95. /package/dist/{chart-column-BFIJysDR.js → chart-column-ouzgxIv_.js} +0 -0
  96. /package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-eV4LjaSi.js} +0 -0
  97. /package/dist/{circle-alert-iY24lSlp.js → circle-alert-Bzh2DBSF.js} +0 -0
  98. /package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-CGMSpQmD.js} +0 -0
  99. /package/dist/{clock-DJZtdl8u.js → clock-Bl3ptexL.js} +0 -0
  100. /package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-CAVU95SO.js} +0 -0
  101. /package/dist/{download-DRXxoiQR.js → download-C5NbvVDc.js} +0 -0
  102. /package/dist/{file-text-DWWY6_f5.js → file-text-D-1uvLZG.js} +0 -0
  103. /package/dist/{filter-DloLC2eE.js → filter-DUfUg6BO.js} +0 -0
  104. /package/dist/{formatters-CF2S2l1m.js → formatters-BQr-aR5W.js} +0 -0
  105. /package/dist/{map-CkJaULb2.js → map-C-M-FQLR.js} +0 -0
  106. /package/dist/{map-pin-BGiCvyCs.js → map-pin-Dznl5l40.js} +0 -0
  107. /package/dist/{navigation-D36prnAn.js → navigation-BJSJl-VG.js} +0 -0
  108. /package/dist/{package-BChUzpJS.js → package-CcRttV2N.js} +0 -0
  109. /package/dist/{phone-Ac9s3mo_.js → phone-DVSZHt_j.js} +0 -0
  110. /package/dist/{plus-Z76q-FZm.js → plus-C_XZDwBz.js} +0 -0
  111. /package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-CfgN67l3.js} +0 -0
  112. /package/dist/{save-CojywNhG.js → save-DJE8Fpdv.js} +0 -0
  113. /package/dist/{search-CDGefqb3.js → search-D_VyGcZT.js} +0 -0
  114. /package/dist/{square-pen-CopE9GmB.js → square-pen-D-6JYrVQ.js} +0 -0
  115. /package/dist/{star-nHlLjDKR.js → star-BpRlUZuU.js} +0 -0
  116. /package/dist/{store-BR9Voi5E.js → store-B-8HMuyZ.js} +0 -0
  117. /package/dist/{trash-2-BKhar6Hs.js → trash-2-8vmv-XV_.js} +0 -0
  118. /package/dist/{trending-up-Dohgd4t_.js → trending-up-Dz6PqPRj.js} +0 -0
  119. /package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-D7Y3J_4z.js} +0 -0
  120. /package/dist/{truck-C0v_hbLR.js → truck-D-y2IDzv.js} +0 -0
  121. /package/dist/{useAssistant-D4v3oxD3.js → useAssistant-DVW4QgQb.js} +0 -0
  122. /package/dist/{useImportExport-DHr-SWSw.js → useImportExport-DrPDHEDK.js} +0 -0
  123. /package/dist/{useSettings-CkDH_-gM.js → useSettings-BB8X2RLh.js} +0 -0
  124. /package/dist/{user-jyFU3lXk.js → user-DzfTn4dE.js} +0 -0
  125. /package/dist/{users-DZ-ilS6B.js → users-CCrq-w0C.js} +0 -0
  126. /package/dist/{warehouse-6m5kXMLu.js → warehouse-CaqfmMKh.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,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { useState } from 'react';
8
- import { Package, Plus, Filter, Download, RefreshCw } from 'lucide-react';
8
+ import { Package, Plus, Filter, Download, RefreshCw, UserPlus, UserCheck } from 'lucide-react';
9
9
  import { PageLayout } from '../components/PageLayout';
10
10
  import { DataTable, type Column } from '../components/DataTable';
11
11
  import { StatusBadge, PriorityBadge, TypeBadge } from '../components/StatusBadge';
@@ -21,7 +21,7 @@ import {
21
21
  SelectValue,
22
22
  Badge,
23
23
  } from '../components/ui';
24
- import { useOrders } from '../hooks/useOrders';
24
+ import { useOrders, useOrderStats } from '../hooks/useOrders';
25
25
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
26
26
  import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';
27
27
  import { formatCurrency, formatDate } from '../utils/formatters';
@@ -37,21 +37,36 @@ export default function OrdersListPage() {
37
37
  const { basePath, navigate } = useMoveTheWheels();
38
38
  const [statusFilter, setStatusFilter] = useState<OrderStatus | 'all'>('all');
39
39
  const [typeFilter, setTypeFilter] = useState<'b2b' | 'b2c' | 'all'>('all');
40
+ const [unassignedOnly, setUnassignedOnly] = useState(false);
41
+ const [page, setPage] = useState(0);
42
+ const [pageSize, setPageSize] = useState(25);
40
43
 
41
- const { orders, totalOrders, isLoading, refetch } = useOrders({
44
+ const filtersActive = statusFilter !== 'all' || typeFilter !== 'all' || unassignedOnly;
45
+
46
+ const { orders, hasMore, isLoading, refetch } = useOrders({
42
47
  filters: {
43
48
  status: statusFilter === 'all' ? undefined : statusFilter,
44
49
  orderType: typeFilter === 'all' ? undefined : typeFilter,
50
+ unassigned: unassignedOnly || undefined,
45
51
  },
46
- limit: 50,
52
+ limit: pageSize,
53
+ offset: page * pageSize,
47
54
  });
48
55
 
56
+ // Pipeline totals + "at a glance" tiles come from the live aggregate, never
57
+ // from the truncated current page.
58
+ const { stats } = useOrderStats();
59
+ const totalOrders = stats.total;
60
+
49
61
  const goTo = (path: string) => {
50
62
  if (navigate) {
51
63
  navigate(joinPath(basePath, path));
52
64
  }
53
65
  };
54
66
 
67
+ // Reset to the first page whenever the active filter set changes.
68
+ const resetPage = () => setPage(0);
69
+
55
70
  const columns: Column<Order>[] = [
56
71
  {
57
72
  id: 'orderNumber',
@@ -129,6 +144,29 @@ export default function OrdersListPage() {
129
144
  className: 'min-w-[100px] whitespace-nowrap',
130
145
  cell: (row) => formatDate(row.createdAt, 'relative'),
131
146
  },
147
+ {
148
+ id: 'driver',
149
+ header: t('movethewheels.orders.table.driver', 'Driver'),
150
+ className: 'min-w-[120px] whitespace-nowrap',
151
+ cell: (row) =>
152
+ row.driverId ? (
153
+ <span className="inline-flex items-center gap-1 text-sm text-status-success-text">
154
+ <UserCheck size={14} /> {t('movethewheels.orders.table.assigned', 'Assigned')}
155
+ </span>
156
+ ) : (
157
+ <Button
158
+ variant="outline"
159
+ size="sm"
160
+ onClick={(e) => {
161
+ e.stopPropagation();
162
+ goTo(`orders/${row.id}`);
163
+ }}
164
+ >
165
+ <UserPlus size={14} className="mr-1" />
166
+ {t('movethewheels.orders.table.assign', 'Assign')}
167
+ </Button>
168
+ ),
169
+ },
132
170
  ];
133
171
 
134
172
  const statusOptions: { value: OrderStatus | 'all'; label: string }[] = [
@@ -168,7 +206,7 @@ export default function OrdersListPage() {
168
206
  {
169
207
  label: t('movethewheels.orders.actions.export', 'Export'),
170
208
  icon: <Download size={16} />,
171
- onSelect: () => {},
209
+ onSelect: () => goTo('import-export'),
172
210
  },
173
211
  ]}
174
212
  />
@@ -195,7 +233,7 @@ export default function OrdersListPage() {
195
233
  </div>
196
234
  <div>
197
235
  <p className="text-2xl font-bold text-text-primary tabular-nums">
198
- {orders.filter((o) => o.status === 'pending').length}
236
+ {stats.byStatus.pending}
199
237
  </p>
200
238
  <p className="text-xs text-text-secondary">
201
239
  {t('movethewheels.orders.glance.pending', 'Pending')}
@@ -203,10 +241,7 @@ export default function OrdersListPage() {
203
241
  </div>
204
242
  <div>
205
243
  <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
- }
244
+ {stats.byStatus.in_transit + stats.byStatus.out_for_delivery}
210
245
  </p>
211
246
  <p className="text-xs text-text-secondary">
212
247
  {t('movethewheels.orders.glance.inTransit', 'In transit')}
@@ -214,7 +249,7 @@ export default function OrdersListPage() {
214
249
  </div>
215
250
  <div>
216
251
  <p className="text-2xl font-bold text-text-primary tabular-nums">
217
- {orders.filter((o) => o.status === 'delivered').length}
252
+ {stats.byStatus.delivered}
218
253
  </p>
219
254
  <p className="text-xs text-text-secondary">
220
255
  {t('movethewheels.orders.glance.delivered', 'Delivered')}
@@ -235,7 +270,10 @@ export default function OrdersListPage() {
235
270
  </div>
236
271
  <Select
237
272
  value={statusFilter}
238
- onValueChange={(value) => setStatusFilter(value as OrderStatus | 'all')}
273
+ onValueChange={(value) => {
274
+ setStatusFilter(value as OrderStatus | 'all');
275
+ resetPage();
276
+ }}
239
277
  >
240
278
  <SelectTrigger className="w-[180px]">
241
279
  <SelectValue placeholder={t('movethewheels.orders.filters.status', 'Status')} />
@@ -250,7 +288,10 @@ export default function OrdersListPage() {
250
288
  </Select>
251
289
  <Select
252
290
  value={typeFilter}
253
- onValueChange={(value) => setTypeFilter(value as 'b2b' | 'b2c' | 'all')}
291
+ onValueChange={(value) => {
292
+ setTypeFilter(value as 'b2b' | 'b2c' | 'all');
293
+ resetPage();
294
+ }}
254
295
  >
255
296
  <SelectTrigger className="w-[150px]">
256
297
  <SelectValue placeholder={t('movethewheels.orders.filters.type', 'Type')} />
@@ -263,13 +304,26 @@ export default function OrdersListPage() {
263
304
  <SelectItem value="b2c">B2C</SelectItem>
264
305
  </SelectContent>
265
306
  </Select>
266
- {(statusFilter !== 'all' || typeFilter !== 'all') && (
307
+ <Button
308
+ variant={unassignedOnly ? 'default' : 'outline'}
309
+ size="sm"
310
+ onClick={() => {
311
+ setUnassignedOnly((prev) => !prev);
312
+ resetPage();
313
+ }}
314
+ >
315
+ <UserPlus size={14} className="mr-1" />
316
+ {t('movethewheels.orders.filters.unassigned', 'Needs driver')}
317
+ </Button>
318
+ {filtersActive && (
267
319
  <Button
268
320
  variant="ghost"
269
321
  size="sm"
270
322
  onClick={() => {
271
323
  setStatusFilter('all');
272
324
  setTypeFilter('all');
325
+ setUnassignedOnly(false);
326
+ resetPage();
273
327
  }}
274
328
  >
275
329
  {t('movethewheels.orders.filters.clear', 'Clear filters')}
@@ -289,6 +343,15 @@ export default function OrdersListPage() {
289
343
  searchFields={['orderNumber', 'customerInfo'] as (keyof Order)[]}
290
344
  onRowClick={(row) => goTo(`orders/${row.id}`)}
291
345
  getRowId={(row) => row.id}
346
+ pageIndex={page}
347
+ pageSize={pageSize}
348
+ onPageChange={setPage}
349
+ onPageSizeChange={(size) => {
350
+ setPageSize(size);
351
+ setPage(0);
352
+ }}
353
+ totalItems={filtersActive ? undefined : totalOrders}
354
+ hasNextPage={hasMore}
292
355
  emptyMessage={t('movethewheels.orders.empty', 'No orders found')}
293
356
  emptyIllustration="empty-data"
294
357
  emptyTitle={t('movethewheels.orders.emptyTitle', 'No orders yet')}
@@ -2,12 +2,13 @@
2
2
  * Products Page - List of all products
3
3
  */
4
4
 
5
+ import { useState } from 'react';
5
6
  import { Package, 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 { Badge } from '../components/ui';
9
10
  import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
10
- import { useProducts } from '../hooks/useInventory';
11
+ import { useProducts, useProductStats } from '../hooks/useInventory';
11
12
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
12
13
  import { formatCurrency } from '../utils/formatters';
13
14
  import { joinPath } from '../utils/navigation';
@@ -15,7 +16,14 @@ import type { Product } from '../types';
15
16
 
16
17
  export default function ProductsPage() {
17
18
  const { basePath, navigate } = useMoveTheWheels();
18
- const { products, totalProducts, isLoading, refetch } = useProducts();
19
+ const [page, setPage] = useState(0);
20
+ const [pageSize, setPageSize] = useState(25);
21
+ const { products, hasMore, isLoading, refetch } = useProducts({
22
+ limit: pageSize,
23
+ offset: page * pageSize,
24
+ });
25
+ // Bounded aggregate for the tiles + subtitle — not the truncated page.
26
+ const { stats } = useProductStats();
19
27
 
20
28
  const goTo = (path: string) => navigate?.(joinPath(basePath, path));
21
29
 
@@ -94,7 +102,7 @@ export default function ProductsPage() {
94
102
  return (
95
103
  <PageLayout
96
104
  title="Products"
97
- subtitle={`${totalProducts} products`}
105
+ subtitle={`${stats.total} products`}
98
106
  icon={<Package size={24} />}
99
107
  headerActions={
100
108
  <CTAOverflowMenu
@@ -105,7 +113,11 @@ export default function ProductsPage() {
105
113
  }}
106
114
  actions={[
107
115
  { label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
108
- { label: 'Export', icon: <Download size={16} />, onSelect: () => {} },
116
+ {
117
+ label: 'Export',
118
+ icon: <Download size={16} />,
119
+ onSelect: () => goTo('import-export'),
120
+ },
109
121
  ]}
110
122
  />
111
123
  }
@@ -121,18 +133,18 @@ export default function ProductsPage() {
121
133
  <p className="text-sm font-medium text-text-secondary">Catalog at a glance</p>
122
134
  <div className="mt-3 grid grid-cols-3 gap-4">
123
135
  <div>
124
- <p className="text-2xl font-bold text-text-primary tabular-nums">{totalProducts}</p>
136
+ <p className="text-2xl font-bold text-text-primary tabular-nums">{stats.total}</p>
125
137
  <p className="text-xs text-text-secondary">Total products</p>
126
138
  </div>
127
139
  <div>
128
140
  <p className="text-2xl font-bold text-text-primary tabular-nums">
129
- {products.filter((p) => p.fragile || p.hazardous).length}
141
+ {stats.specialHandling}
130
142
  </p>
131
143
  <p className="text-xs text-text-secondary">Special handling</p>
132
144
  </div>
133
145
  <div>
134
146
  <p className="text-2xl font-bold text-text-primary tabular-nums">
135
- {products.filter((p) => p.temperatureControlled).length}
147
+ {stats.temperatureControlled}
136
148
  </p>
137
149
  <p className="text-xs text-text-secondary">Temp-controlled</p>
138
150
  </div>
@@ -148,6 +160,14 @@ export default function ProductsPage() {
148
160
  searchFields={['name', 'sku', 'category'] as (keyof Product)[]}
149
161
  onRowClick={(row) => goTo(`inventory/products/${row.id}`)}
150
162
  getRowId={(row) => row.id}
163
+ pageIndex={page}
164
+ pageSize={pageSize}
165
+ onPageChange={setPage}
166
+ onPageSizeChange={(size) => {
167
+ setPageSize(size);
168
+ setPage(0);
169
+ }}
170
+ hasNextPage={hasMore}
151
171
  emptyIllustration="empty-files"
152
172
  emptyTitle="No products yet"
153
173
  emptyDescription="Add products to your catalog so they can be added to orders and stocked in warehouses."
@@ -7,7 +7,16 @@
7
7
  */
8
8
 
9
9
  import { useState } from 'react';
10
- import { FileBarChart, Download, Calendar, Filter, Clock, Plus } from 'lucide-react';
10
+ import {
11
+ FileBarChart,
12
+ Download,
13
+ Calendar,
14
+ Filter,
15
+ Clock,
16
+ Plus,
17
+ ChevronLeft,
18
+ ChevronRight,
19
+ } from 'lucide-react';
11
20
  import { PageLayout } from '../components/PageLayout';
12
21
  import {
13
22
  Card,
@@ -26,6 +35,7 @@ import {
26
35
  import { EmphasisPanel, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';
27
36
  import { useReports, type CreateReportInput } from '../hooks/useReports';
28
37
  import { formatDate } from '../utils/formatters';
38
+ import { downloadFile, downloadJson, recordToCsv, slugifyForFilename } from '../utils/download';
29
39
 
30
40
  const REPORT_TYPES: { value: string; label: string }[] = [
31
41
  { value: 'operations', label: 'Operations Summary' },
@@ -40,8 +50,14 @@ interface Feedback {
40
50
  text: string;
41
51
  }
42
52
 
53
+ const REPORTS_PAGE_SIZE = 10;
54
+
43
55
  export default function ReportsPage() {
44
- const { reports, isLoading, error, refetch, createReport, generateReport } = useReports();
56
+ const [page, setPage] = useState(0);
57
+ const { reports, hasMore, isLoading, error, refetch, createReport, generateReport } = useReports({
58
+ limit: REPORTS_PAGE_SIZE,
59
+ offset: page * REPORTS_PAGE_SIZE,
60
+ });
45
61
  const [reportType, setReportType] = useState('operations');
46
62
  const [busyId, setBusyId] = useState<string | null>(null);
47
63
  const [creating, setCreating] = useState(false);
@@ -79,8 +95,21 @@ export default function ReportsPage() {
79
95
  setBusyId(id);
80
96
  setFeedback(null);
81
97
  try {
82
- await generateReport(id);
83
- setFeedback({ tone: 'success', text: 'Report generated.' });
98
+ const generated = await generateReport(id);
99
+ // Turn the freshly generated report into a downloadable file. Prefer a
100
+ // flat CSV of the result payload; fall back to the full JSON document so a
101
+ // file always lands even when the result is nested/empty.
102
+ const baseName = `${slugifyForFilename(generated.report.name)}-${generated.generatedAt.slice(
103
+ 0,
104
+ 10
105
+ )}`;
106
+ const csv = recordToCsv(generated.result ?? null);
107
+ if (csv) {
108
+ downloadFile(`${baseName}.csv`, csv, 'text/csv');
109
+ } else {
110
+ downloadJson(`${baseName}.json`, generated);
111
+ }
112
+ setFeedback({ tone: 'success', text: 'Report generated and downloaded.' });
84
113
  } catch (genError) {
85
114
  setFeedback({
86
115
  tone: 'error',
@@ -212,6 +241,38 @@ export default function ReportsPage() {
212
241
  </Card>
213
242
  ))
214
243
  )}
244
+
245
+ {!isLoading && !error && (reports.length > 0 || page > 0) && (
246
+ <div className="flex flex-wrap items-center justify-between gap-3 pt-1">
247
+ <span className="text-sm text-text-secondary">
248
+ Showing {reports.length === 0 ? 0 : page * REPORTS_PAGE_SIZE + 1} to{' '}
249
+ {page * REPORTS_PAGE_SIZE + reports.length}
250
+ </span>
251
+ <div className="flex items-center gap-1">
252
+ <Button
253
+ variant="outline"
254
+ size="icon"
255
+ className="h-8 w-8"
256
+ aria-label="Previous page"
257
+ onClick={() => setPage((p) => Math.max(0, p - 1))}
258
+ disabled={page === 0}
259
+ >
260
+ <ChevronLeft className="h-4 w-4" />
261
+ </Button>
262
+ <span className="px-3 text-sm">Page {page + 1}</span>
263
+ <Button
264
+ variant="outline"
265
+ size="icon"
266
+ className="h-8 w-8"
267
+ aria-label="Next page"
268
+ onClick={() => setPage((p) => p + 1)}
269
+ disabled={!hasMore}
270
+ >
271
+ <ChevronRight className="h-4 w-4" />
272
+ </Button>
273
+ </div>
274
+ </div>
275
+ )}
215
276
  </div>
216
277
 
217
278
  <div className="space-y-6">