@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.
- package/dist/{AIAssistantPage-C3cPsWt-.js → AIAssistantPage-3YOnHO9Y.js} +3 -3
- package/dist/{AnalyticsPage-Dz6g2MvK.js → AnalyticsPage-CMpI1070.js} +11 -11
- package/dist/CreateOrderPage-DRAgGo9i.js +490 -0
- package/dist/CustomerDetailsPage-BCeMmOFe.js +615 -0
- package/dist/{CustomersPage-2Y05LVyh.js → CustomersPage-CaNBMXmM.js} +64 -53
- package/dist/{DashboardPage-BVjQi0EB.js → DashboardPage-DIKlXde3.js} +16 -16
- package/dist/DataTable-XvUcTjwi.js +340 -0
- package/dist/DocumentsCard-U2lSe-cn.js +169 -0
- package/dist/{DriverDetailsPage-CN3WQ0B9.js → DriverDetailsPage-BQrYd_Lp.js} +178 -195
- package/dist/{DriversPage-B0Zpx8OQ.js → DriversPage-PYmdFiby.js} +65 -54
- package/dist/FinancePage-Cwh9Bw4M.js +136 -0
- package/dist/{FleetPage-DqkFi3s3.js → FleetPage-3ZHsB-TN.js} +23 -22
- package/dist/{FormError-BlOjnXOq.js → FormError-cuLV2jVD.js} +1 -1
- package/dist/{ImportExportPage-DKJRCezl.js → ImportExportPage-C6f7DqhM.js} +50 -70
- package/dist/{InventoryPage-DwfgCUQq.js → InventoryPage-DpiqVu1v.js} +9 -9
- package/dist/{LiveTrackingPage-DCX-K-LR.js → LiveTrackingPage-FAzzlVnX.js} +9 -9
- package/dist/MarketplacePage-Bh_2ns4v.js +426 -0
- package/dist/Notice-CSoDYKw9.js +46 -0
- package/dist/OrderDetailsPage-zCj0lW0j.js +584 -0
- package/dist/{OrdersListPage-BjDlCKJd.js → OrdersListPage-CAMsuUck.js} +166 -89
- package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-wsxAQkA0.js} +12 -12
- package/dist/{ProductsPage-C1G9h7Xd.js → ProductsPage-DC5qDLfz.js} +60 -49
- package/dist/ReportsPage-BwUJ37eO.js +356 -0
- package/dist/RouteDetailsPage-DyMJE9AS.js +583 -0
- package/dist/{RoutesPage-DMfoicM1.js → RoutesPage-Dtc669yc.js} +57 -46
- package/dist/{SettingsPage-Bn-v0mbQ.js → SettingsPage-CpH9xhQg.js} +5 -27
- package/dist/{TrackingPage-W-jtdM3Y.js → TrackingPage-DaNEFG_h.js} +10 -10
- package/dist/{VehicleDetailsPage-Bceo5oz3.js → VehicleDetailsPage-DldYNsYV.js} +125 -142
- package/dist/{VehiclesPage-VBtOep91.js → VehiclesPage-BFw13iMt.js} +61 -50
- package/dist/{WarehouseDetailsPage-L_6sWigM.js → WarehouseDetailsPage-CMG7or6G.js} +17 -17
- package/dist/{WarehousesPage-DCbXXXWE.js → WarehousesPage-Wkytnt86.js} +58 -47
- package/dist/arrow-right-Q3XQpTPR.js +11 -0
- package/dist/arrow-up-xNF1zJUG.js +17 -0
- package/dist/chevron-left-B2_r0Elx.js +8 -0
- package/dist/{wrench-CANXWxF-.js → fuel-B6_lnKDs.js} +2 -5
- package/dist/index.js +163 -174
- package/dist/upload-CHZofOqb.js +21 -0
- package/dist/{useAnalytics-DS8n0xgg.js → useAnalytics-D7HW2XRb.js} +52 -2
- package/dist/{useCustomers-BmTNGxiI.js → useCustomers-BPW4__PR.js} +29 -26
- package/dist/{useFleet-DKqXZ16m.js → useFleet-ChBkxAd6.js} +81 -69
- package/dist/{useInventory-DkyiVY_A.js → useInventory-CAAVMRp6.js} +83 -38
- package/dist/{useOrders-Cfk1GLiq.js → useOrders-B465S2nt.js} +58 -47
- package/dist/{useReports-CTqZr-AB.js → useReports-jQ3GpNOm.js} +40 -34
- package/dist/{useRoutes-BQWO607N.js → useRoutes-hS3d5-xr.js} +49 -41
- package/dist/{useTracking-B-va1kO3.js → useTracking-RjGz9PZG.js} +2 -2
- package/dist/user-check-BEKRCi7v.js +20 -0
- package/dist/wrench-q_rgJfaP.js +8 -0
- package/dist/x-tMeDMcCw.js +11 -0
- package/package.json +1 -1
- package/src/movethewheels/components/DataTable.tsx +252 -84
- package/src/movethewheels/components/DocumentsCard.tsx +102 -0
- package/src/movethewheels/components/Notice.tsx +61 -0
- package/src/movethewheels/components/index.ts +7 -0
- package/src/movethewheels/hooks/index.ts +9 -0
- package/src/movethewheels/hooks/useAnalytics.ts +95 -0
- package/src/movethewheels/hooks/useCustomers.ts +11 -4
- package/src/movethewheels/hooks/useEntityDocuments.ts +132 -0
- package/src/movethewheels/hooks/useFleet.ts +26 -7
- package/src/movethewheels/hooks/useInventory.ts +70 -7
- package/src/movethewheels/hooks/useOrders.ts +32 -2
- package/src/movethewheels/hooks/useReports.ts +14 -4
- package/src/movethewheels/hooks/useRoutes.ts +20 -3
- package/src/movethewheels/pages/AnalyticsPage.tsx +2 -2
- package/src/movethewheels/pages/CreateOrderPage.tsx +44 -16
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +239 -33
- package/src/movethewheels/pages/CustomersPage.tsx +27 -11
- package/src/movethewheels/pages/DriverDetailsPage.tsx +16 -30
- package/src/movethewheels/pages/DriversPage.tsx +26 -9
- package/src/movethewheels/pages/FinancePage.tsx +67 -84
- package/src/movethewheels/pages/InventoryPage.tsx +1 -1
- package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
- package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
- package/src/movethewheels/pages/OrdersListPage.tsx +77 -14
- package/src/movethewheels/pages/ProductsPage.tsx +27 -7
- package/src/movethewheels/pages/ReportsPage.tsx +65 -4
- package/src/movethewheels/pages/RouteDetailsPage.tsx +74 -13
- package/src/movethewheels/pages/RoutesPage.tsx +22 -6
- package/src/movethewheels/pages/SettingsPage.tsx +3 -19
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +3 -33
- package/src/movethewheels/pages/VehiclesPage.tsx +26 -5
- package/src/movethewheels/pages/WarehousesPage.tsx +22 -17
- package/src/movethewheels/types/index.ts +8 -0
- package/src/movethewheels/utils/download.ts +62 -0
- package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
- package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
- package/dist/DataTable-DmciIKKM.js +0 -249
- package/dist/FinancePage-DEDykSOP.js +0 -158
- package/dist/MarketplacePage-CIQ97_zp.js +0 -247
- package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
- package/dist/ReportsPage-Zp53q3_8.js +0 -284
- package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
- /package/dist/{MetricCard-BY0QrMD4.js → MetricCard-CVBVpu7o.js} +0 -0
- /package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-C87mDpDP.js} +0 -0
- /package/dist/{box-D9Pal4HM.js → box-Eu05PXDh.js} +0 -0
- /package/dist/{chart-column-BFIJysDR.js → chart-column-ouzgxIv_.js} +0 -0
- /package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-eV4LjaSi.js} +0 -0
- /package/dist/{circle-alert-iY24lSlp.js → circle-alert-Bzh2DBSF.js} +0 -0
- /package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-CGMSpQmD.js} +0 -0
- /package/dist/{clock-DJZtdl8u.js → clock-Bl3ptexL.js} +0 -0
- /package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-CAVU95SO.js} +0 -0
- /package/dist/{download-DRXxoiQR.js → download-C5NbvVDc.js} +0 -0
- /package/dist/{file-text-DWWY6_f5.js → file-text-D-1uvLZG.js} +0 -0
- /package/dist/{filter-DloLC2eE.js → filter-DUfUg6BO.js} +0 -0
- /package/dist/{formatters-CF2S2l1m.js → formatters-BQr-aR5W.js} +0 -0
- /package/dist/{map-CkJaULb2.js → map-C-M-FQLR.js} +0 -0
- /package/dist/{map-pin-BGiCvyCs.js → map-pin-Dznl5l40.js} +0 -0
- /package/dist/{navigation-D36prnAn.js → navigation-BJSJl-VG.js} +0 -0
- /package/dist/{package-BChUzpJS.js → package-CcRttV2N.js} +0 -0
- /package/dist/{phone-Ac9s3mo_.js → phone-DVSZHt_j.js} +0 -0
- /package/dist/{plus-Z76q-FZm.js → plus-C_XZDwBz.js} +0 -0
- /package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-CfgN67l3.js} +0 -0
- /package/dist/{save-CojywNhG.js → save-DJE8Fpdv.js} +0 -0
- /package/dist/{search-CDGefqb3.js → search-D_VyGcZT.js} +0 -0
- /package/dist/{square-pen-CopE9GmB.js → square-pen-D-6JYrVQ.js} +0 -0
- /package/dist/{star-nHlLjDKR.js → star-BpRlUZuU.js} +0 -0
- /package/dist/{store-BR9Voi5E.js → store-B-8HMuyZ.js} +0 -0
- /package/dist/{trash-2-BKhar6Hs.js → trash-2-8vmv-XV_.js} +0 -0
- /package/dist/{trending-up-Dohgd4t_.js → trending-up-Dz6PqPRj.js} +0 -0
- /package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-D7Y3J_4z.js} +0 -0
- /package/dist/{truck-C0v_hbLR.js → truck-D-y2IDzv.js} +0 -0
- /package/dist/{useAssistant-D4v3oxD3.js → useAssistant-DVW4QgQb.js} +0 -0
- /package/dist/{useImportExport-DHr-SWSw.js → useImportExport-DrPDHEDK.js} +0 -0
- /package/dist/{useSettings-CkDH_-gM.js → useSettings-BB8X2RLh.js} +0 -0
- /package/dist/{user-jyFU3lXk.js → user-DzfTn4dE.js} +0 -0
- /package/dist/{users-DZ-ilS6B.js → users-CCrq-w0C.js} +0 -0
- /package/dist/{warehouse-6m5kXMLu.js → warehouse-CaqfmMKh.js} +0 -0
|
@@ -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
|
|
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">
|
|
375
|
-
<p className="text-sm">{order.
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
|
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:
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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={`${
|
|
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
|
-
{
|
|
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">{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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">
|