@burdenoff/microfe-movethewheels 2026.920.3 → 2026.923.1
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/components/CarbonFootprintPanel.js +1 -1
- package/dist/components/CustomerPicker.js +76 -65
- package/dist/components/DataTable.js +140 -139
- package/dist/components/DocumentsCard.js +14 -8
- package/dist/generated/wspace-types.d.ts +2719 -8
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useAnalytics.js +21 -1
- package/dist/hooks/useCustomers.js +94 -78
- package/dist/hooks/useDebouncedValue.js +11 -0
- package/dist/hooks/useEntityDocuments.js +28 -18
- package/dist/hooks/useEpod.js +24 -17
- package/dist/hooks/useFleet.js +148 -125
- package/dist/hooks/useInventory.js +255 -186
- package/dist/hooks/useMultiModalShipments.js +35 -27
- package/dist/hooks/useOrders.js +139 -134
- package/dist/hooks/useRoutes.js +97 -75
- package/dist/hooks/useTracking.js +100 -88
- package/dist/index.js +6 -6
- package/dist/movethewheels/components/CustomerPicker.d.ts +3 -1
- package/dist/movethewheels/components/DataTable.d.ts +5 -1
- package/dist/movethewheels/hooks/useCustomers.d.ts +0 -1
- package/dist/movethewheels/hooks/useDebouncedValue.d.ts +1 -0
- package/dist/movethewheels/hooks/useEntityDocuments.d.ts +2 -0
- package/dist/movethewheels/hooks/useEpod.d.ts +6 -1
- package/dist/movethewheels/hooks/useFleet.d.ts +49 -5
- package/dist/movethewheels/hooks/useInventory.d.ts +24 -17
- package/dist/movethewheels/hooks/useMultiModalShipments.d.ts +6 -1
- package/dist/movethewheels/hooks/useOrders.d.ts +48 -5
- package/dist/movethewheels/hooks/useRoutes.d.ts +18 -1
- package/dist/movethewheels/hooks/useTracking.d.ts +12 -11
- package/dist/movethewheels/types/index.d.ts +12 -5
- package/dist/pages/CreateOrderPage.js +191 -129
- package/dist/pages/CustomersPage.js +61 -54
- package/dist/pages/DeliveryCapturePage.js +131 -109
- package/dist/pages/DriversPage.js +100 -92
- package/dist/pages/InventoryPage.js +239 -236
- package/dist/pages/MultiModalShipmentPage.js +163 -130
- package/dist/pages/OrderDetailsPage.js +190 -106
- package/dist/pages/OrdersListPage.js +66 -60
- package/dist/pages/ProductDetailsPage.js +241 -200
- package/dist/pages/ProductsPage.js +59 -52
- package/dist/pages/RouteDetailsPage.js +324 -293
- package/dist/pages/RoutesPage.js +55 -48
- package/dist/pages/VehiclesPage.js +63 -56
- package/dist/pages/WarehousesPage.js +60 -53
- package/package.json +1 -1
- package/src/generated/wspace-types.ts +3136 -8
- package/src/movethewheels/components/CarbonFootprintPanel.tsx +0 -1
- package/src/movethewheels/components/CustomerPicker.tsx +14 -0
- package/src/movethewheels/components/DataTable.tsx +34 -18
- package/src/movethewheels/components/DocumentsCard.tsx +6 -1
- package/src/movethewheels/components/maps/leaflet-setup.ts +1 -8
- package/src/movethewheels/hooks/useAnalytics.ts +27 -0
- package/src/movethewheels/hooks/useCustomers.ts +64 -24
- package/src/movethewheels/hooks/useDebouncedValue.ts +12 -0
- package/src/movethewheels/hooks/useEntityDocuments.ts +43 -25
- package/src/movethewheels/hooks/useEpod.ts +14 -7
- package/src/movethewheels/hooks/useFleet.ts +66 -38
- package/src/movethewheels/hooks/useFleetIntelligence.ts +1 -4
- package/src/movethewheels/hooks/useInventory.ts +211 -90
- package/src/movethewheels/hooks/useMultiModalShipments.ts +15 -11
- package/src/movethewheels/hooks/useOrders.ts +48 -14
- package/src/movethewheels/hooks/useRoutes.ts +81 -24
- package/src/movethewheels/hooks/useTracking.ts +130 -98
- package/src/movethewheels/pages/CreateOrderPage.tsx +93 -2
- package/src/movethewheels/pages/CustomersPage.tsx +11 -1
- package/src/movethewheels/pages/DeliveryCapturePage.tsx +31 -1
- package/src/movethewheels/pages/DriversPage.tsx +24 -12
- package/src/movethewheels/pages/InventoryPage.tsx +57 -32
- package/src/movethewheels/pages/MultiModalShipmentPage.tsx +52 -3
- package/src/movethewheels/pages/OrderDetailsPage.tsx +91 -6
- package/src/movethewheels/pages/OrdersListPage.tsx +20 -3
- package/src/movethewheels/pages/ProductDetailsPage.tsx +52 -3
- package/src/movethewheels/pages/ProductsPage.tsx +11 -1
- package/src/movethewheels/pages/RouteDetailsPage.tsx +45 -5
- package/src/movethewheels/pages/RoutesPage.tsx +11 -1
- package/src/movethewheels/pages/VehiclesPage.tsx +19 -2
- package/src/movethewheels/pages/WarehousesPage.tsx +11 -1
- package/src/movethewheels/types/index.ts +12 -5
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Displays detailed information about a specific order.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { useState } from 'react';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
8
|
import { useParams } from 'react-router';
|
|
9
9
|
import {
|
|
10
10
|
Package,
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
Button,
|
|
35
35
|
Badge,
|
|
36
36
|
Separator,
|
|
37
|
+
Input,
|
|
37
38
|
Label,
|
|
38
39
|
Select,
|
|
39
40
|
SelectContent,
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
51
52
|
import { useOrder, useOrders } from '../hooks/useOrders';
|
|
52
53
|
import { useFleet } from '../hooks/useFleet';
|
|
54
|
+
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
53
55
|
import { FormError } from '../components/FormError';
|
|
54
56
|
import { Notice, useNotice } from '../components/Notice';
|
|
55
57
|
import { DocumentsCard } from '../components/DocumentsCard';
|
|
@@ -96,15 +98,28 @@ export default function OrderDetailsPage() {
|
|
|
96
98
|
const { basePath, navigate } = useMoveTheWheels();
|
|
97
99
|
const { order, isLoading, error, refetch } = useOrder(orderId);
|
|
98
100
|
const { updateOrder, updateOrderStatus } = useOrders();
|
|
99
|
-
const
|
|
100
|
-
const { message: notice, notify } = useNotice();
|
|
101
|
-
|
|
101
|
+
const [fleetSearch, setFleetSearch] = useState('');
|
|
102
102
|
const [selectedVehicle, setSelectedVehicle] = useState('');
|
|
103
103
|
const [selectedDriver, setSelectedDriver] = useState('');
|
|
104
104
|
const [assigning, setAssigning] = useState(false);
|
|
105
105
|
const [advancing, setAdvancing] = useState(false);
|
|
106
106
|
const [actionError, setActionError] = useState<string | null>(null);
|
|
107
|
-
|
|
107
|
+
const debouncedFleetSearch = useDebouncedValue(fleetSearch);
|
|
108
|
+
const { vehicles, isLoading: vehiclesLoading } = useFleet({
|
|
109
|
+
limit: 20,
|
|
110
|
+
filters: {
|
|
111
|
+
search: debouncedFleetSearch || undefined,
|
|
112
|
+
driverId: selectedDriver || undefined,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const { drivers, isLoading: driversLoading } = useFleet({
|
|
116
|
+
limit: 20,
|
|
117
|
+
filters: {
|
|
118
|
+
search: debouncedFleetSearch || undefined,
|
|
119
|
+
vehicleId: selectedVehicle || undefined,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
const { message: notice, notify } = useNotice();
|
|
108
123
|
const availableVehicles = vehicles.filter(
|
|
109
124
|
(vehicle) => vehicle.status === 'available' || vehicle.id === order?.vehicleId
|
|
110
125
|
);
|
|
@@ -112,6 +127,26 @@ export default function OrderDetailsPage() {
|
|
|
112
127
|
(driver) => driver.status === 'available' || driver.id === order?.driverId
|
|
113
128
|
);
|
|
114
129
|
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (
|
|
132
|
+
!driversLoading &&
|
|
133
|
+
selectedDriver &&
|
|
134
|
+
!drivers.some((driver) => driver.id === selectedDriver)
|
|
135
|
+
) {
|
|
136
|
+
setSelectedDriver('');
|
|
137
|
+
}
|
|
138
|
+
}, [drivers, driversLoading, selectedDriver]);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (
|
|
142
|
+
!vehiclesLoading &&
|
|
143
|
+
selectedVehicle &&
|
|
144
|
+
!vehicles.some((vehicle) => vehicle.id === selectedVehicle)
|
|
145
|
+
) {
|
|
146
|
+
setSelectedVehicle('');
|
|
147
|
+
}
|
|
148
|
+
}, [selectedVehicle, vehicles, vehiclesLoading]);
|
|
149
|
+
|
|
115
150
|
const handleAssign = async () => {
|
|
116
151
|
if (!orderId || (!selectedVehicle && !selectedDriver)) {
|
|
117
152
|
setActionError('Pick a vehicle and/or a driver to assign.');
|
|
@@ -187,7 +222,7 @@ export default function OrderDetailsPage() {
|
|
|
187
222
|
<ArrowLeft size={16} />
|
|
188
223
|
Back
|
|
189
224
|
</Button>
|
|
190
|
-
<Button variant="outline">
|
|
225
|
+
<Button variant="outline" onClick={() => window.print()}>
|
|
191
226
|
<Printer size={16} />
|
|
192
227
|
Print
|
|
193
228
|
</Button>
|
|
@@ -333,6 +368,47 @@ export default function OrderDetailsPage() {
|
|
|
333
368
|
</CardContent>
|
|
334
369
|
</Card>
|
|
335
370
|
|
|
371
|
+
{/* Saved route */}
|
|
372
|
+
<Card>
|
|
373
|
+
<CardHeader>
|
|
374
|
+
<CardTitle className="flex items-center gap-2">
|
|
375
|
+
<Truck size={18} />
|
|
376
|
+
Saved Route
|
|
377
|
+
</CardTitle>
|
|
378
|
+
</CardHeader>
|
|
379
|
+
<CardContent>
|
|
380
|
+
{order.route ? (
|
|
381
|
+
<div className="space-y-3">
|
|
382
|
+
<div className="flex items-center justify-between gap-3">
|
|
383
|
+
<div>
|
|
384
|
+
<p className="font-medium">{order.route.name}</p>
|
|
385
|
+
<p className="text-sm text-text-secondary">
|
|
386
|
+
{order.route.stops.length} stops ·{' '}
|
|
387
|
+
{order.route.status.replace('_', ' ').toLowerCase()}
|
|
388
|
+
</p>
|
|
389
|
+
</div>
|
|
390
|
+
<Badge variant="outline">Reusable route</Badge>
|
|
391
|
+
</div>
|
|
392
|
+
<div className="space-y-2">
|
|
393
|
+
{order.route.stops.map((stop, index) => (
|
|
394
|
+
<div key={stop.id} className="flex items-start gap-2 text-sm">
|
|
395
|
+
<span className="text-text-secondary">{index + 1}.</span>
|
|
396
|
+
<span>
|
|
397
|
+
<span className="font-medium capitalize">{stop.type}</span> ·{' '}
|
|
398
|
+
{formatAddress(stop.address)}
|
|
399
|
+
</span>
|
|
400
|
+
</div>
|
|
401
|
+
))}
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
) : (
|
|
405
|
+
<p className="text-sm text-text-secondary">
|
|
406
|
+
No saved route was selected for this order.
|
|
407
|
+
</p>
|
|
408
|
+
)}
|
|
409
|
+
</CardContent>
|
|
410
|
+
</Card>
|
|
411
|
+
|
|
336
412
|
{/* Order Items */}
|
|
337
413
|
<Card>
|
|
338
414
|
<CardHeader>
|
|
@@ -477,6 +553,15 @@ export default function OrderDetailsPage() {
|
|
|
477
553
|
<Separator />
|
|
478
554
|
|
|
479
555
|
<div className="space-y-3">
|
|
556
|
+
<div className="space-y-1.5">
|
|
557
|
+
<Label htmlFor="assign-fleet-search">Search fleet</Label>
|
|
558
|
+
<Input
|
|
559
|
+
id="assign-fleet-search"
|
|
560
|
+
value={fleetSearch}
|
|
561
|
+
onChange={(event) => setFleetSearch(event.target.value)}
|
|
562
|
+
placeholder="Vehicle number, driver name or ID"
|
|
563
|
+
/>
|
|
564
|
+
</div>
|
|
480
565
|
<div className="space-y-1.5">
|
|
481
566
|
<Label htmlFor="assign-vehicle">Vehicle</Label>
|
|
482
567
|
<Select value={selectedVehicle} onValueChange={setSelectedVehicle}>
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
Badge,
|
|
35
35
|
} from '../components/ui';
|
|
36
36
|
import { useOrders, useOrderStats } from '../hooks/useOrders';
|
|
37
|
+
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
37
38
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
38
39
|
import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';
|
|
39
40
|
import { formatCurrency, formatDate } from '../utils/formatters';
|
|
@@ -51,13 +52,23 @@ export default function OrdersListPage() {
|
|
|
51
52
|
const [unassignedOnly, setUnassignedOnly] = useState(false);
|
|
52
53
|
const [page, setPage] = useState(0);
|
|
53
54
|
const [pageSize, setPageSize] = useState(25);
|
|
55
|
+
const [search, setSearch] = useState('');
|
|
56
|
+
const debouncedSearch = useDebouncedValue(search);
|
|
54
57
|
|
|
55
|
-
const filtersActive = statusFilter !== 'all' || unassignedOnly;
|
|
58
|
+
const filtersActive = statusFilter !== 'all' || unassignedOnly || search.length > 0;
|
|
56
59
|
|
|
57
|
-
const {
|
|
60
|
+
const {
|
|
61
|
+
orders,
|
|
62
|
+
totalOrders: totalMatchingOrders,
|
|
63
|
+
hasMore,
|
|
64
|
+
isLoading,
|
|
65
|
+
error,
|
|
66
|
+
refetch,
|
|
67
|
+
} = useOrders({
|
|
58
68
|
filters: {
|
|
59
69
|
status: statusFilter === 'all' ? undefined : statusFilter,
|
|
60
70
|
unassigned: unassignedOnly || undefined,
|
|
71
|
+
search: debouncedSearch || undefined,
|
|
61
72
|
},
|
|
62
73
|
limit: pageSize,
|
|
63
74
|
offset: page * pageSize,
|
|
@@ -303,6 +314,7 @@ export default function OrdersListPage() {
|
|
|
303
314
|
onClick={() => {
|
|
304
315
|
setStatusFilter('all');
|
|
305
316
|
setUnassignedOnly(false);
|
|
317
|
+
setSearch('');
|
|
306
318
|
resetPage();
|
|
307
319
|
}}
|
|
308
320
|
>
|
|
@@ -323,6 +335,11 @@ export default function OrdersListPage() {
|
|
|
323
335
|
searchable
|
|
324
336
|
searchPlaceholder={t('movethewheels.orders.searchPlaceholder', 'Search orders...')}
|
|
325
337
|
searchFields={['orderNumber', 'customerInfo'] as (keyof Order)[]}
|
|
338
|
+
searchQuery={search}
|
|
339
|
+
onSearchQueryChange={(query) => {
|
|
340
|
+
setSearch(query);
|
|
341
|
+
setPage(0);
|
|
342
|
+
}}
|
|
326
343
|
onRowClick={(row) => goTo(`orders/${row.id}`)}
|
|
327
344
|
getRowId={(row) => row.id}
|
|
328
345
|
pageIndex={page}
|
|
@@ -332,7 +349,7 @@ export default function OrdersListPage() {
|
|
|
332
349
|
setPageSize(size);
|
|
333
350
|
setPage(0);
|
|
334
351
|
}}
|
|
335
|
-
totalItems={
|
|
352
|
+
totalItems={totalMatchingOrders}
|
|
336
353
|
hasNextPage={hasMore}
|
|
337
354
|
emptyMessage={t('movethewheels.orders.empty', 'No orders found')}
|
|
338
355
|
emptyIllustration="empty-data"
|
|
@@ -4,7 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
import { useEffect, useState } from 'react';
|
|
6
6
|
import { useLocation, useParams } from 'react-router';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
Package,
|
|
9
|
+
ArrowLeft,
|
|
10
|
+
Edit,
|
|
11
|
+
Box,
|
|
12
|
+
DollarSign,
|
|
13
|
+
Tag,
|
|
14
|
+
Save,
|
|
15
|
+
Warehouse,
|
|
16
|
+
ChevronLeft,
|
|
17
|
+
ChevronRight,
|
|
18
|
+
} from 'lucide-react';
|
|
8
19
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
9
20
|
import {
|
|
10
21
|
Card,
|
|
@@ -192,12 +203,23 @@ export default function ProductDetailsPage() {
|
|
|
192
203
|
const { product, isLoading, error, refetch } = useProduct(productId);
|
|
193
204
|
const isEditRoute = useLocation().pathname.endsWith('/edit');
|
|
194
205
|
// Where this item is actually stocked — inventoryItems(filter: { cargoItemId }).
|
|
206
|
+
const [stockPage, setStockPage] = useState(0);
|
|
207
|
+
const stockPageSize = 20;
|
|
195
208
|
const {
|
|
196
209
|
levels: stockLevels,
|
|
210
|
+
totalLevels: totalStockLevels,
|
|
211
|
+
totalAvailable,
|
|
212
|
+
hasMore: hasMoreStock,
|
|
197
213
|
isLoading: stockLoading,
|
|
198
214
|
error: stockError,
|
|
199
|
-
} = useProductStockLevels(isEditRoute ? undefined : productId
|
|
200
|
-
|
|
215
|
+
} = useProductStockLevels(isEditRoute ? undefined : productId, {
|
|
216
|
+
limit: stockPageSize,
|
|
217
|
+
offset: stockPage * stockPageSize,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
setStockPage(0);
|
|
222
|
+
}, [productId]);
|
|
201
223
|
|
|
202
224
|
const goBack = () => navigate?.(joinPath(basePath, 'inventory/products'));
|
|
203
225
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
@@ -391,6 +413,33 @@ export default function ProductDetailsPage() {
|
|
|
391
413
|
</span>
|
|
392
414
|
</div>
|
|
393
415
|
))}
|
|
416
|
+
{stockPage > 0 || hasMoreStock ? (
|
|
417
|
+
<div className="flex items-center justify-between gap-2 border-t border-border-seam pt-3">
|
|
418
|
+
<span className="text-xs text-text-secondary">
|
|
419
|
+
{stockPage * stockPageSize + 1}–
|
|
420
|
+
{Math.min(stockPage * stockPageSize + stockLevels.length, totalStockLevels)}{' '}
|
|
421
|
+
of {totalStockLevels}
|
|
422
|
+
</span>
|
|
423
|
+
<div className="flex gap-2">
|
|
424
|
+
<Button
|
|
425
|
+
variant="outline"
|
|
426
|
+
size="sm"
|
|
427
|
+
disabled={stockPage === 0 || stockLoading}
|
|
428
|
+
onClick={() => setStockPage((page) => Math.max(0, page - 1))}
|
|
429
|
+
>
|
|
430
|
+
<ChevronLeft size={14} /> Previous
|
|
431
|
+
</Button>
|
|
432
|
+
<Button
|
|
433
|
+
variant="outline"
|
|
434
|
+
size="sm"
|
|
435
|
+
disabled={!hasMoreStock || stockLoading}
|
|
436
|
+
onClick={() => setStockPage((page) => page + 1)}
|
|
437
|
+
>
|
|
438
|
+
Next <ChevronRight size={14} />
|
|
439
|
+
</Button>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
) : null}
|
|
394
443
|
<div className="flex items-baseline justify-between gap-3 border-t border-border-seam pt-3">
|
|
395
444
|
<p className="text-sm text-text-secondary">Available across the network</p>
|
|
396
445
|
<span className="font-bold">{totalAvailable.toLocaleString()}</span>
|
|
@@ -10,6 +10,7 @@ import { DataTable, type Column } from '../components/DataTable';
|
|
|
10
10
|
import { Badge } from '../components/ui';
|
|
11
11
|
import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
12
12
|
import { useProducts, useProductStats } from '../hooks/useInventory';
|
|
13
|
+
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
13
14
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
14
15
|
import { formatCurrency } from '../utils/formatters';
|
|
15
16
|
import { joinPath } from '../utils/navigation';
|
|
@@ -19,9 +20,12 @@ export default function ProductsPage() {
|
|
|
19
20
|
const { basePath, navigate } = useMoveTheWheels();
|
|
20
21
|
const [page, setPage] = useState(0);
|
|
21
22
|
const [pageSize, setPageSize] = useState(25);
|
|
22
|
-
const
|
|
23
|
+
const [search, setSearch] = useState('');
|
|
24
|
+
const debouncedSearch = useDebouncedValue(search);
|
|
25
|
+
const { products, totalProducts, hasMore, isLoading, error, refetch } = useProducts({
|
|
23
26
|
limit: pageSize,
|
|
24
27
|
offset: page * pageSize,
|
|
28
|
+
search: debouncedSearch,
|
|
25
29
|
});
|
|
26
30
|
// Bounded aggregate for the tiles + subtitle — not the truncated page.
|
|
27
31
|
const { stats } = useProductStats();
|
|
@@ -164,6 +168,11 @@ export default function ProductsPage() {
|
|
|
164
168
|
searchable
|
|
165
169
|
searchPlaceholder="Search products..."
|
|
166
170
|
searchFields={['name', 'sku', 'category'] as (keyof Product)[]}
|
|
171
|
+
searchQuery={search}
|
|
172
|
+
onSearchQueryChange={(query) => {
|
|
173
|
+
setSearch(query);
|
|
174
|
+
setPage(0);
|
|
175
|
+
}}
|
|
167
176
|
onRowClick={(row) => goTo(`inventory/products/${row.id}`)}
|
|
168
177
|
getRowId={(row) => row.id}
|
|
169
178
|
pageIndex={page}
|
|
@@ -173,6 +182,7 @@ export default function ProductsPage() {
|
|
|
173
182
|
setPageSize(size);
|
|
174
183
|
setPage(0);
|
|
175
184
|
}}
|
|
185
|
+
totalItems={totalProducts}
|
|
176
186
|
hasNextPage={hasMore}
|
|
177
187
|
emptyIllustration="empty-files"
|
|
178
188
|
emptyTitle="No products yet"
|
|
@@ -40,17 +40,26 @@ import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
|
40
40
|
import { RouteMap, type RouteStop as MapRouteStop } from '../components/maps';
|
|
41
41
|
import { firstRealLatLng } from '../utils/mapMarkers';
|
|
42
42
|
import { useRoute, useRoutes } from '../hooks/useRoutes';
|
|
43
|
-
import { useFleet } from '../hooks/useFleet';
|
|
43
|
+
import { useFleet, useVehicle, useDriver } from '../hooks/useFleet';
|
|
44
44
|
import { useAssignmentSuggestions } from '../hooks/useFleetIntelligence';
|
|
45
45
|
import { AssignmentSuggestions } from '../components/AssignmentSuggestions';
|
|
46
46
|
import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';
|
|
47
47
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
48
|
+
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
48
49
|
import { formatDate, formatAddress, formatCurrency } from '../utils/formatters';
|
|
49
50
|
import { joinPath } from '../utils/navigation';
|
|
50
51
|
import { ROUTE_STATUS_COLORS } from '../constants';
|
|
51
52
|
import { FormError } from '../components/FormError';
|
|
52
53
|
import { Notice, useNotice } from '../components/Notice';
|
|
53
|
-
import type { Route as RouteType, RouteStop } from '../types';
|
|
54
|
+
import type { Route as RouteType, RouteStop, Vehicle, Driver } from '../types';
|
|
55
|
+
|
|
56
|
+
/** Keeps a currently selected fleet item visible even when it falls outside the current search page. */
|
|
57
|
+
function mergeSelected<T extends { id: string }>(items: T[], selected: T | null | undefined): T[] {
|
|
58
|
+
if (!selected || items.some((item) => item.id === selected.id)) {
|
|
59
|
+
return items;
|
|
60
|
+
}
|
|
61
|
+
return [selected, ...items];
|
|
62
|
+
}
|
|
54
63
|
|
|
55
64
|
const ROUTE_STATUS_OPTIONS: { value: RouteType['status']; label: string }[] = [
|
|
56
65
|
{ value: 'planned', label: 'Planned' },
|
|
@@ -63,7 +72,8 @@ function NewRouteForm({ editId }: { editId?: string }) {
|
|
|
63
72
|
const { basePath, navigate } = useMoveTheWheels();
|
|
64
73
|
const { createRoute, updateRoute } = useRoutes();
|
|
65
74
|
const { route } = useRoute(editId);
|
|
66
|
-
const
|
|
75
|
+
const [fleetSearch, setFleetSearch] = useState('');
|
|
76
|
+
const debouncedFleetSearch = useDebouncedValue(fleetSearch);
|
|
67
77
|
const isEditing = !!editId;
|
|
68
78
|
const [form, setForm] = useState<{
|
|
69
79
|
name: string;
|
|
@@ -78,6 +88,27 @@ function NewRouteForm({ editId }: { editId?: string }) {
|
|
|
78
88
|
driverId: '',
|
|
79
89
|
stops: [{ address: '', type: 'delivery' }],
|
|
80
90
|
});
|
|
91
|
+
const { vehicles } = useFleet({
|
|
92
|
+
limit: 20,
|
|
93
|
+
filters: {
|
|
94
|
+
search: debouncedFleetSearch || undefined,
|
|
95
|
+
driverId: form.driverId || undefined,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const { drivers } = useFleet({
|
|
99
|
+
limit: 20,
|
|
100
|
+
filters: {
|
|
101
|
+
search: debouncedFleetSearch || undefined,
|
|
102
|
+
vehicleId: form.vehicleId || undefined,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
// The selected vehicle/driver may fall outside the current search page (e.g. when
|
|
106
|
+
// editing a route or after the assignee no longer matches the search term); fetch
|
|
107
|
+
// them directly so the Select can still render the current selection.
|
|
108
|
+
const { vehicle: selectedVehicle } = useVehicle(form.vehicleId || undefined);
|
|
109
|
+
const { driver: selectedDriver } = useDriver(form.driverId || undefined);
|
|
110
|
+
const vehicleOptions: Vehicle[] = mergeSelected(vehicles, selectedVehicle);
|
|
111
|
+
const driverOptions: Driver[] = mergeSelected(drivers, selectedDriver);
|
|
81
112
|
const [submitting, setSubmitting] = useState(false);
|
|
82
113
|
const [error, setError] = useState<string | null>(null);
|
|
83
114
|
|
|
@@ -253,6 +284,15 @@ function NewRouteForm({ editId }: { editId?: string }) {
|
|
|
253
284
|
</Select>
|
|
254
285
|
</div>
|
|
255
286
|
</div>
|
|
287
|
+
<div className="space-y-2">
|
|
288
|
+
<Label htmlFor="fleet-search">Search fleet</Label>
|
|
289
|
+
<Input
|
|
290
|
+
id="fleet-search"
|
|
291
|
+
value={fleetSearch}
|
|
292
|
+
onChange={(e) => setFleetSearch(e.target.value)}
|
|
293
|
+
placeholder="Vehicle number, driver name or ID"
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
256
296
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
257
297
|
<div className="space-y-2">
|
|
258
298
|
<Label htmlFor="vehicle">Vehicle (optional)</Label>
|
|
@@ -264,7 +304,7 @@ function NewRouteForm({ editId }: { editId?: string }) {
|
|
|
264
304
|
<SelectValue placeholder="Assign later" />
|
|
265
305
|
</SelectTrigger>
|
|
266
306
|
<SelectContent>
|
|
267
|
-
{
|
|
307
|
+
{vehicleOptions.map((vehicle) => (
|
|
268
308
|
<SelectItem key={vehicle.id} value={vehicle.id}>
|
|
269
309
|
{vehicle.vehicleNumber} ({vehicle.type})
|
|
270
310
|
</SelectItem>
|
|
@@ -282,7 +322,7 @@ function NewRouteForm({ editId }: { editId?: string }) {
|
|
|
282
322
|
<SelectValue placeholder="Assign later" />
|
|
283
323
|
</SelectTrigger>
|
|
284
324
|
<SelectContent>
|
|
285
|
-
{
|
|
325
|
+
{driverOptions.map((driver) => (
|
|
286
326
|
<SelectItem key={driver.id} value={driver.id}>
|
|
287
327
|
{driver.fullName} ({driver.employeeId})
|
|
288
328
|
</SelectItem>
|
|
@@ -10,6 +10,7 @@ import { StatusBadge } from '../components/StatusBadge';
|
|
|
10
10
|
import { Badge, Button } from '../components/ui';
|
|
11
11
|
import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
12
12
|
import { useRoutes, useRouteStats } from '../hooks/useRoutes';
|
|
13
|
+
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
13
14
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
14
15
|
import { formatDate } from '../utils/formatters';
|
|
15
16
|
import { joinPath } from '../utils/navigation';
|
|
@@ -20,9 +21,12 @@ export default function RoutesPage() {
|
|
|
20
21
|
const { basePath, navigate } = useMoveTheWheels();
|
|
21
22
|
const [page, setPage] = useState(0);
|
|
22
23
|
const [pageSize, setPageSize] = useState(25);
|
|
23
|
-
const
|
|
24
|
+
const [search, setSearch] = useState('');
|
|
25
|
+
const debouncedSearch = useDebouncedValue(search);
|
|
26
|
+
const { routes, totalRoutes, hasMore, isLoading, error, refetch } = useRoutes({
|
|
24
27
|
limit: pageSize,
|
|
25
28
|
offset: page * pageSize,
|
|
29
|
+
search: debouncedSearch || undefined,
|
|
26
30
|
});
|
|
27
31
|
// Bounded aggregate for the tiles + subtitle — not the truncated page.
|
|
28
32
|
const { stats } = useRouteStats();
|
|
@@ -131,6 +135,11 @@ export default function RoutesPage() {
|
|
|
131
135
|
searchable
|
|
132
136
|
searchPlaceholder="Search routes..."
|
|
133
137
|
searchFields={['name'] as (keyof Route)[]}
|
|
138
|
+
searchQuery={search}
|
|
139
|
+
onSearchQueryChange={(query) => {
|
|
140
|
+
setSearch(query);
|
|
141
|
+
setPage(0);
|
|
142
|
+
}}
|
|
134
143
|
onRowClick={(row) => goTo(`routes/${row.id}`)}
|
|
135
144
|
getRowId={(row) => row.id}
|
|
136
145
|
pageIndex={page}
|
|
@@ -140,6 +149,7 @@ export default function RoutesPage() {
|
|
|
140
149
|
setPageSize(size);
|
|
141
150
|
setPage(0);
|
|
142
151
|
}}
|
|
152
|
+
totalItems={totalRoutes}
|
|
143
153
|
hasNextPage={hasMore}
|
|
144
154
|
emptyIllustration="empty-data"
|
|
145
155
|
emptyTitle="No routes yet"
|
|
@@ -19,6 +19,7 @@ import { StatusBadge, VehicleTypeBadge } from '../components/StatusBadge';
|
|
|
19
19
|
import { Progress } from '../components/ui';
|
|
20
20
|
import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
21
21
|
import { useVehicles, useFleetStats } from '../hooks/useFleet';
|
|
22
|
+
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
22
23
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
23
24
|
import { joinPath } from '../utils/navigation';
|
|
24
25
|
import type { Vehicle } from '../types';
|
|
@@ -27,9 +28,19 @@ export default function VehiclesPage() {
|
|
|
27
28
|
const { basePath, navigate } = useMoveTheWheels();
|
|
28
29
|
const [page, setPage] = useState(0);
|
|
29
30
|
const [pageSize, setPageSize] = useState(25);
|
|
30
|
-
const
|
|
31
|
+
const [search, setSearch] = useState('');
|
|
32
|
+
const debouncedSearch = useDebouncedValue(search);
|
|
33
|
+
const {
|
|
34
|
+
vehicles,
|
|
35
|
+
totalVehicles: filteredVehicleCount,
|
|
36
|
+
hasMore,
|
|
37
|
+
isLoading,
|
|
38
|
+
error,
|
|
39
|
+
refetch,
|
|
40
|
+
} = useVehicles({
|
|
31
41
|
limit: pageSize,
|
|
32
42
|
offset: page * pageSize,
|
|
43
|
+
search: debouncedSearch || undefined,
|
|
33
44
|
});
|
|
34
45
|
// Fleet totals + tiles from the live aggregate, not the current page.
|
|
35
46
|
const { stats } = useFleetStats();
|
|
@@ -176,6 +187,11 @@ export default function VehiclesPage() {
|
|
|
176
187
|
searchable
|
|
177
188
|
searchPlaceholder="Search vehicles..."
|
|
178
189
|
searchFields={['vehicleNumber', 'make', 'model'] as (keyof Vehicle)[]}
|
|
190
|
+
searchQuery={search}
|
|
191
|
+
onSearchQueryChange={(query) => {
|
|
192
|
+
setSearch(query);
|
|
193
|
+
setPage(0);
|
|
194
|
+
}}
|
|
179
195
|
onRowClick={(row) => goTo(`fleet/vehicles/${row.id}`)}
|
|
180
196
|
getRowId={(row) => row.id}
|
|
181
197
|
pageIndex={page}
|
|
@@ -185,7 +201,8 @@ export default function VehiclesPage() {
|
|
|
185
201
|
setPageSize(size);
|
|
186
202
|
setPage(0);
|
|
187
203
|
}}
|
|
188
|
-
totalItems={
|
|
204
|
+
totalItems={filteredVehicleCount}
|
|
205
|
+
hasNextPage={hasMore}
|
|
189
206
|
emptyIllustration="empty-data"
|
|
190
207
|
emptyTitle="No vehicles yet"
|
|
191
208
|
emptyDescription="Add vehicles to your fleet to start assigning them to deliveries."
|
|
@@ -10,6 +10,7 @@ import { DataTable, type Column } from '../components/DataTable';
|
|
|
10
10
|
import { Badge, Progress } from '../components/ui';
|
|
11
11
|
import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
12
12
|
import { useWarehouses, useWarehouseStats } from '../hooks/useInventory';
|
|
13
|
+
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
13
14
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
14
15
|
import { joinPath } from '../utils/navigation';
|
|
15
16
|
import type { Warehouse as WarehouseType } from '../types';
|
|
@@ -18,9 +19,12 @@ export default function WarehousesPage() {
|
|
|
18
19
|
const { basePath, navigate } = useMoveTheWheels();
|
|
19
20
|
const [page, setPage] = useState(0);
|
|
20
21
|
const [pageSize, setPageSize] = useState(25);
|
|
21
|
-
const
|
|
22
|
+
const [search, setSearch] = useState('');
|
|
23
|
+
const debouncedSearch = useDebouncedValue(search);
|
|
24
|
+
const { warehouses, totalWarehouses, hasMore, isLoading, error, refetch } = useWarehouses({
|
|
22
25
|
limit: pageSize,
|
|
23
26
|
offset: page * pageSize,
|
|
27
|
+
search: debouncedSearch,
|
|
24
28
|
});
|
|
25
29
|
// Bounded aggregate for the tiles + subtitle — not the truncated page.
|
|
26
30
|
const { stats } = useWarehouseStats();
|
|
@@ -157,6 +161,11 @@ export default function WarehousesPage() {
|
|
|
157
161
|
searchable
|
|
158
162
|
searchPlaceholder="Search warehouses..."
|
|
159
163
|
searchFields={['name', 'code'] as (keyof WarehouseType)[]}
|
|
164
|
+
searchQuery={search}
|
|
165
|
+
onSearchQueryChange={(query) => {
|
|
166
|
+
setSearch(query);
|
|
167
|
+
setPage(0);
|
|
168
|
+
}}
|
|
160
169
|
onRowClick={(row) => goTo(`inventory/warehouses/${row.id}`)}
|
|
161
170
|
getRowId={(row) => row.id}
|
|
162
171
|
pageIndex={page}
|
|
@@ -166,6 +175,7 @@ export default function WarehousesPage() {
|
|
|
166
175
|
setPageSize(size);
|
|
167
176
|
setPage(0);
|
|
168
177
|
}}
|
|
178
|
+
totalItems={totalWarehouses}
|
|
169
179
|
hasNextPage={hasMore}
|
|
170
180
|
emptyIllustration="empty-data"
|
|
171
181
|
emptyTitle="No warehouses yet"
|
|
@@ -280,6 +280,8 @@ export interface Order {
|
|
|
280
280
|
actualDelivery?: Date;
|
|
281
281
|
driverId?: string;
|
|
282
282
|
vehicleId?: string;
|
|
283
|
+
routeId?: string;
|
|
284
|
+
route?: Route;
|
|
283
285
|
trackingNumber: string;
|
|
284
286
|
specialInstructions?: string;
|
|
285
287
|
pricing: OrderPricing;
|
|
@@ -502,7 +504,10 @@ export interface InventoryMovement {
|
|
|
502
504
|
export interface Inventory {
|
|
503
505
|
id: string;
|
|
504
506
|
productId: string;
|
|
507
|
+
productName?: string;
|
|
508
|
+
productSku?: string;
|
|
505
509
|
warehouseId: string;
|
|
510
|
+
warehouseName?: string;
|
|
506
511
|
zoneId: string;
|
|
507
512
|
quantity: number;
|
|
508
513
|
reservedQuantity: number;
|
|
@@ -746,23 +751,25 @@ export interface OrderFilter {
|
|
|
746
751
|
dateFrom?: Date;
|
|
747
752
|
dateTo?: Date;
|
|
748
753
|
priority?: string;
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
* (driverId is null). The backend DeliveryJobFilterInput has no null-driver
|
|
752
|
-
* predicate, so this is applied to the fetched page after mapping.
|
|
753
|
-
*/
|
|
754
|
+
search?: string;
|
|
755
|
+
/** Server-side predicate for orders that still need a driver assigned. */
|
|
754
756
|
unassigned?: boolean;
|
|
755
757
|
}
|
|
756
758
|
|
|
757
759
|
export interface FleetFilter {
|
|
758
760
|
vehicleType?: VehicleType;
|
|
759
761
|
status?: VehicleStatus;
|
|
762
|
+
/** Filter vehicles to those linked to this driver. */
|
|
760
763
|
driverId?: string;
|
|
764
|
+
/** Filter drivers to those linked to this vehicle. */
|
|
765
|
+
vehicleId?: string;
|
|
766
|
+
search?: string;
|
|
761
767
|
}
|
|
762
768
|
|
|
763
769
|
export interface InventoryFilter {
|
|
764
770
|
warehouseId?: string;
|
|
765
771
|
productId?: string;
|
|
772
|
+
search?: string;
|
|
766
773
|
lowStock?: boolean;
|
|
767
774
|
}
|
|
768
775
|
|