@burdenoff/microfe-movethewheels 2026.920.3 → 2026.921.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 +1043 -5
- 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 +146 -125
- package/dist/hooks/useInventory.js +255 -186
- package/dist/hooks/useMultiModalShipments.js +35 -27
- package/dist/hooks/useOrders.js +73 -73
- package/dist/hooks/useRoutes.js +97 -75
- package/dist/hooks/useTracking.js +100 -88
- 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 +33 -5
- package/dist/movethewheels/hooks/useRoutes.d.ts +2 -1
- package/dist/movethewheels/hooks/useTracking.d.ts +12 -11
- package/dist/movethewheels/types/index.d.ts +7 -5
- package/dist/pages/CreateOrderPage.js +207 -201
- 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 +224 -208
- 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 +343 -321
- 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 +1188 -5
- 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 +58 -37
- 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 +13 -14
- package/src/movethewheels/hooks/useRoutes.ts +80 -23
- package/src/movethewheels/hooks/useTracking.ts +130 -98
- package/src/movethewheels/pages/CreateOrderPage.tsx +9 -1
- 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 +17 -1
- 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 +35 -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 +7 -5
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
Truck,
|
|
26
26
|
Type as TypeIcon,
|
|
27
27
|
Upload,
|
|
28
|
+
ChevronLeft,
|
|
29
|
+
ChevronRight,
|
|
28
30
|
} from 'lucide-react';
|
|
29
31
|
import { PageLayout } from '../components/PageLayout';
|
|
30
32
|
import {
|
|
@@ -248,13 +250,19 @@ function CaptureForm({
|
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
export default function DeliveryCapturePage() {
|
|
251
|
-
const
|
|
253
|
+
const [pageIndex, setPageIndex] = useState(0);
|
|
254
|
+
const pageSize = 20;
|
|
255
|
+
const { jobs, hasMore, isLoading, error, refetch } = useMyPendingEpodDeliveryJobs({
|
|
256
|
+
limit: pageSize,
|
|
257
|
+
offset: pageIndex * pageSize,
|
|
258
|
+
});
|
|
252
259
|
const [selectedJob, setSelectedJob] = useState<PendingEpodDeliveryJob | null>(null);
|
|
253
260
|
const { message, notify } = useNotice();
|
|
254
261
|
|
|
255
262
|
const handleCaptured = () => {
|
|
256
263
|
notify('Proof of delivery captured — delivery marked complete.');
|
|
257
264
|
setSelectedJob(null);
|
|
265
|
+
setPageIndex(0);
|
|
258
266
|
refetch();
|
|
259
267
|
};
|
|
260
268
|
|
|
@@ -309,6 +317,28 @@ export default function DeliveryCapturePage() {
|
|
|
309
317
|
</CardContent>
|
|
310
318
|
</Card>
|
|
311
319
|
))}
|
|
320
|
+
{(pageIndex > 0 || hasMore) && (
|
|
321
|
+
<div className="flex justify-end gap-2 pt-2">
|
|
322
|
+
<Button
|
|
323
|
+
type="button"
|
|
324
|
+
variant="outline"
|
|
325
|
+
size="sm"
|
|
326
|
+
disabled={pageIndex === 0 || isLoading}
|
|
327
|
+
onClick={() => setPageIndex((previous) => Math.max(0, previous - 1))}
|
|
328
|
+
>
|
|
329
|
+
<ChevronLeft size={16} /> Previous
|
|
330
|
+
</Button>
|
|
331
|
+
<Button
|
|
332
|
+
type="button"
|
|
333
|
+
variant="outline"
|
|
334
|
+
size="sm"
|
|
335
|
+
disabled={!hasMore || isLoading}
|
|
336
|
+
onClick={() => setPageIndex((previous) => previous + 1)}
|
|
337
|
+
>
|
|
338
|
+
Next <ChevronRight size={16} />
|
|
339
|
+
</Button>
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
312
342
|
</div>
|
|
313
343
|
) : (
|
|
314
344
|
<div className="text-center py-12">
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from '../components/ui';
|
|
34
34
|
import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
35
35
|
import { useDrivers, useDriverStatusCounts, useFleetStats, useVehicles } from '../hooks/useFleet';
|
|
36
|
+
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
36
37
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
37
38
|
import { formatDate } from '../utils/formatters';
|
|
38
39
|
import { joinPath } from '../utils/navigation';
|
|
@@ -50,12 +51,23 @@ export default function DriversPage() {
|
|
|
50
51
|
const { basePath, navigate } = useMoveTheWheels();
|
|
51
52
|
const [page, setPage] = useState(0);
|
|
52
53
|
const [pageSize, setPageSize] = useState(25);
|
|
54
|
+
const [search, setSearch] = useState('');
|
|
55
|
+
const debouncedSearch = useDebouncedValue(search);
|
|
53
56
|
const [statusFilter, setStatusFilter] = useState<DriverStatus | 'all'>('all');
|
|
54
57
|
const [licenseClassFilter, setLicenseClassFilter] = useState<string>('all');
|
|
55
|
-
const {
|
|
58
|
+
const {
|
|
59
|
+
drivers,
|
|
60
|
+
totalDrivers: filteredDriverCount,
|
|
61
|
+
hasMore,
|
|
62
|
+
isLoading,
|
|
63
|
+
error,
|
|
64
|
+
refetch,
|
|
65
|
+
} = useDrivers({
|
|
56
66
|
limit: pageSize,
|
|
57
67
|
offset: page * pageSize,
|
|
68
|
+
search: debouncedSearch || undefined,
|
|
58
69
|
status: statusFilter === 'all' ? undefined : statusFilter,
|
|
70
|
+
licenseClass: licenseClassFilter === 'all' ? undefined : licenseClassFilter,
|
|
59
71
|
});
|
|
60
72
|
// Workforce totals + tiles from the live aggregate, not the current page.
|
|
61
73
|
const { stats } = useFleetStats();
|
|
@@ -63,7 +75,7 @@ export default function DriversPage() {
|
|
|
63
75
|
const { counts } = useDriverStatusCounts();
|
|
64
76
|
// Drivers carry only `vehicleId`; the roster shows the fleet number, so the
|
|
65
77
|
// vehicle list is joined client-side into an id -> number lookup.
|
|
66
|
-
const { vehicles } = useVehicles({ limit:
|
|
78
|
+
const { vehicles } = useVehicles({ limit: 100 });
|
|
67
79
|
const vehicleNumberById = useMemo(
|
|
68
80
|
() => new Map(vehicles.map((vehicle) => [vehicle.id, vehicle.vehicleNumber])),
|
|
69
81
|
[vehicles]
|
|
@@ -79,14 +91,7 @@ export default function DriversPage() {
|
|
|
79
91
|
),
|
|
80
92
|
[drivers]
|
|
81
93
|
);
|
|
82
|
-
const
|
|
83
|
-
() =>
|
|
84
|
-
licenseClassFilter === 'all'
|
|
85
|
-
? drivers
|
|
86
|
-
: drivers.filter((driver) => driver.licenseType === licenseClassFilter),
|
|
87
|
-
[drivers, licenseClassFilter]
|
|
88
|
-
);
|
|
89
|
-
const filtersActive = statusFilter !== 'all' || licenseClassFilter !== 'all';
|
|
94
|
+
const filtersActive = statusFilter !== 'all' || licenseClassFilter !== 'all' || search.length > 0;
|
|
90
95
|
|
|
91
96
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
92
97
|
|
|
@@ -293,6 +298,7 @@ export default function DriversPage() {
|
|
|
293
298
|
onClick={() => {
|
|
294
299
|
setStatusFilter('all');
|
|
295
300
|
setLicenseClassFilter('all');
|
|
301
|
+
setSearch('');
|
|
296
302
|
setPage(0);
|
|
297
303
|
}}
|
|
298
304
|
>
|
|
@@ -305,13 +311,18 @@ export default function DriversPage() {
|
|
|
305
311
|
|
|
306
312
|
<DataTable
|
|
307
313
|
columns={columns}
|
|
308
|
-
data={
|
|
314
|
+
data={drivers}
|
|
309
315
|
isLoading={isLoading}
|
|
310
316
|
error={error}
|
|
311
317
|
onRetry={refetch}
|
|
312
318
|
searchable
|
|
313
319
|
searchPlaceholder="Search drivers..."
|
|
314
320
|
searchFields={['fullName', 'employeeId'] as (keyof Driver)[]}
|
|
321
|
+
searchQuery={search}
|
|
322
|
+
onSearchQueryChange={(query) => {
|
|
323
|
+
setSearch(query);
|
|
324
|
+
setPage(0);
|
|
325
|
+
}}
|
|
315
326
|
onRowClick={(row) => goTo(`fleet/drivers/${row.id}`)}
|
|
316
327
|
getRowId={(row) => row.id}
|
|
317
328
|
pageIndex={page}
|
|
@@ -321,7 +332,8 @@ export default function DriversPage() {
|
|
|
321
332
|
setPageSize(size);
|
|
322
333
|
setPage(0);
|
|
323
334
|
}}
|
|
324
|
-
totalItems={
|
|
335
|
+
totalItems={filteredDriverCount}
|
|
336
|
+
hasNextPage={hasMore}
|
|
325
337
|
emptyIllustration="empty-people"
|
|
326
338
|
emptyTitle={filtersActive ? 'No drivers match these filters' : 'No drivers yet'}
|
|
327
339
|
emptyDescription={
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Inventory Page - Overview of inventory management
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { useMemo } from 'react';
|
|
5
|
+
import { useMemo, useState } from 'react';
|
|
6
6
|
import {
|
|
7
7
|
Package,
|
|
8
8
|
Warehouse,
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
type InventoryTransactionType,
|
|
39
39
|
} from '../hooks/useInventory';
|
|
40
40
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
41
|
+
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
41
42
|
import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';
|
|
42
43
|
import { formatCurrency, formatDate } from '../utils/formatters';
|
|
43
44
|
import { joinPath } from '../utils/navigation';
|
|
@@ -90,7 +91,25 @@ export default function InventoryPage() {
|
|
|
90
91
|
return value === key ? fallback : value;
|
|
91
92
|
};
|
|
92
93
|
const { basePath, navigate } = useMoveTheWheels();
|
|
93
|
-
const
|
|
94
|
+
const [search, setSearch] = useState('');
|
|
95
|
+
const [pageIndex, setPageIndex] = useState(0);
|
|
96
|
+
const [pageSize, setPageSize] = useState(10);
|
|
97
|
+
const debouncedSearch = useDebouncedValue(search);
|
|
98
|
+
const {
|
|
99
|
+
inventory,
|
|
100
|
+
lowStockItems,
|
|
101
|
+
products,
|
|
102
|
+
warehouses,
|
|
103
|
+
totalItems,
|
|
104
|
+
hasMore,
|
|
105
|
+
isLoading,
|
|
106
|
+
error,
|
|
107
|
+
refetch,
|
|
108
|
+
} = useInventory({
|
|
109
|
+
search: debouncedSearch,
|
|
110
|
+
limit: pageSize,
|
|
111
|
+
offset: pageIndex * pageSize,
|
|
112
|
+
});
|
|
94
113
|
const { stats } = useInventoryStats();
|
|
95
114
|
const {
|
|
96
115
|
transactions,
|
|
@@ -105,37 +124,30 @@ export default function InventoryPage() {
|
|
|
105
124
|
action();
|
|
106
125
|
}
|
|
107
126
|
};
|
|
108
|
-
const lowStockItems = inventory.filter((inv) => inv.availableQuantity <= inv.reorderLevel);
|
|
109
|
-
|
|
110
127
|
const unknownProductLabel = t(
|
|
111
128
|
'movethewheels.inventory.lowStock.unknownProduct',
|
|
112
129
|
'Unknown Product'
|
|
113
130
|
);
|
|
114
131
|
|
|
115
132
|
const stockRows = useMemo<StockRow[]>(() => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
? 'low'
|
|
135
|
-
: 'in_stock',
|
|
136
|
-
};
|
|
137
|
-
});
|
|
138
|
-
}, [inventory, products, warehouses, unknownProductLabel]);
|
|
133
|
+
return inventory.map((item) => ({
|
|
134
|
+
id: item.id,
|
|
135
|
+
productId: item.productId,
|
|
136
|
+
productName: item.productName ?? unknownProductLabel,
|
|
137
|
+
sku: item.productSku ?? '—',
|
|
138
|
+
warehouseName: item.warehouseName ?? '—',
|
|
139
|
+
onHand: item.quantity,
|
|
140
|
+
allocated: item.reservedQuantity,
|
|
141
|
+
available: item.availableQuantity,
|
|
142
|
+
reorderLevel: item.reorderLevel,
|
|
143
|
+
stockState:
|
|
144
|
+
item.quantity === 0
|
|
145
|
+
? 'out_of_stock'
|
|
146
|
+
: item.availableQuantity <= item.reorderLevel
|
|
147
|
+
? 'low'
|
|
148
|
+
: 'in_stock',
|
|
149
|
+
}));
|
|
150
|
+
}, [inventory, unknownProductLabel]);
|
|
139
151
|
|
|
140
152
|
const stockColumns: Column<StockRow>[] = [
|
|
141
153
|
{
|
|
@@ -309,13 +321,12 @@ export default function InventoryPage() {
|
|
|
309
321
|
<EmphasisPanel className="space-y-4">
|
|
310
322
|
<h2 className="flex items-center gap-2 text-base font-semibold text-status-warning-text">
|
|
311
323
|
<AlertTriangle size={18} />
|
|
312
|
-
{t('movethewheels.inventory.lowStock.title', 'Low Stock Alert')} ({
|
|
324
|
+
{t('movethewheels.inventory.lowStock.title', 'Low Stock Alert')} ({stats.lowStockCount}{' '}
|
|
313
325
|
{t('movethewheels.inventory.lowStock.items', 'items')})
|
|
314
326
|
</h2>
|
|
315
327
|
<div>
|
|
316
328
|
<div className="space-y-3">
|
|
317
|
-
{lowStockItems.
|
|
318
|
-
const product = products.find((p) => p.id === item.productId);
|
|
329
|
+
{lowStockItems.map((item) => {
|
|
319
330
|
return (
|
|
320
331
|
<div
|
|
321
332
|
key={item.id}
|
|
@@ -323,11 +334,11 @@ export default function InventoryPage() {
|
|
|
323
334
|
>
|
|
324
335
|
<div>
|
|
325
336
|
<p className="font-medium">
|
|
326
|
-
{
|
|
337
|
+
{item.productName ??
|
|
327
338
|
t('movethewheels.inventory.lowStock.unknownProduct', 'Unknown Product')}
|
|
328
339
|
</p>
|
|
329
340
|
<p className="text-sm text-text-secondary">
|
|
330
|
-
{t('movethewheels.inventory.lowStock.sku', 'SKU:')} {
|
|
341
|
+
{t('movethewheels.inventory.lowStock.sku', 'SKU:')} {item.productSku ?? '—'}
|
|
331
342
|
</p>
|
|
332
343
|
</div>
|
|
333
344
|
<div className="text-right">
|
|
@@ -365,6 +376,20 @@ export default function InventoryPage() {
|
|
|
365
376
|
'Search product, SKU or warehouse...'
|
|
366
377
|
)}
|
|
367
378
|
searchFields={['productName', 'sku', 'warehouseName'] as (keyof StockRow)[]}
|
|
379
|
+
searchQuery={search}
|
|
380
|
+
onSearchQueryChange={(query) => {
|
|
381
|
+
setSearch(query);
|
|
382
|
+
setPageIndex(0);
|
|
383
|
+
}}
|
|
384
|
+
pageIndex={pageIndex}
|
|
385
|
+
pageSize={pageSize}
|
|
386
|
+
onPageChange={setPageIndex}
|
|
387
|
+
onPageSizeChange={(nextPageSize) => {
|
|
388
|
+
setPageSize(nextPageSize);
|
|
389
|
+
setPageIndex(0);
|
|
390
|
+
}}
|
|
391
|
+
totalItems={totalItems}
|
|
392
|
+
hasNextPage={hasMore}
|
|
368
393
|
onRowClick={(row) => goTo(`inventory/products/${row.productId}`)}
|
|
369
394
|
getRowId={(row) => row.id}
|
|
370
395
|
emptyIllustration="empty-data"
|
|
@@ -7,7 +7,17 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useState } from 'react';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
Ship,
|
|
12
|
+
Plane,
|
|
13
|
+
Truck,
|
|
14
|
+
TrainFront,
|
|
15
|
+
Plus,
|
|
16
|
+
Trash2,
|
|
17
|
+
Calculator,
|
|
18
|
+
ChevronLeft,
|
|
19
|
+
ChevronRight,
|
|
20
|
+
} from 'lucide-react';
|
|
11
21
|
import { PageLayout, PageSection, EmptyState, LoadingState } from '../components/PageLayout';
|
|
12
22
|
import { FormError } from '../components/FormError';
|
|
13
23
|
import {
|
|
@@ -69,8 +79,20 @@ function formatMoney(
|
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
export default function MultiModalShipmentPage() {
|
|
72
|
-
const
|
|
73
|
-
|
|
82
|
+
const [pageIndex, setPageIndex] = useState(0);
|
|
83
|
+
const pageSize = 10;
|
|
84
|
+
const {
|
|
85
|
+
shipments,
|
|
86
|
+
totalShipments,
|
|
87
|
+
hasMore,
|
|
88
|
+
isLoading,
|
|
89
|
+
error,
|
|
90
|
+
calculateFreightRate,
|
|
91
|
+
planMultiModalShipment,
|
|
92
|
+
} = useMultiModalShipments({
|
|
93
|
+
limit: pageSize,
|
|
94
|
+
offset: pageIndex * pageSize,
|
|
95
|
+
});
|
|
74
96
|
|
|
75
97
|
const [shipmentNumber, setShipmentNumber] = useState('');
|
|
76
98
|
const [originAddress, setOriginAddress] = useState('');
|
|
@@ -149,6 +171,7 @@ export default function MultiModalShipmentPage() {
|
|
|
149
171
|
setDestinationAddress('');
|
|
150
172
|
setLegs([emptyLeg()]);
|
|
151
173
|
setQuotes({});
|
|
174
|
+
setPageIndex(0);
|
|
152
175
|
} catch (submitError) {
|
|
153
176
|
setFormError(submitError instanceof Error ? submitError.message : 'Failed to plan shipment');
|
|
154
177
|
} finally {
|
|
@@ -544,6 +567,32 @@ export default function MultiModalShipmentPage() {
|
|
|
544
567
|
</CardContent>
|
|
545
568
|
</Card>
|
|
546
569
|
))}
|
|
570
|
+
{totalShipments > pageSize && (
|
|
571
|
+
<div className="flex items-center justify-between border-t border-border-seam pt-4">
|
|
572
|
+
<span className="text-sm text-text-secondary">
|
|
573
|
+
Showing {pageIndex * pageSize + 1}–
|
|
574
|
+
{Math.min((pageIndex + 1) * pageSize, totalShipments)} of {totalShipments}
|
|
575
|
+
</span>
|
|
576
|
+
<div className="flex gap-2">
|
|
577
|
+
<Button
|
|
578
|
+
variant="outline"
|
|
579
|
+
size="sm"
|
|
580
|
+
disabled={pageIndex === 0 || isLoading}
|
|
581
|
+
onClick={() => setPageIndex((previous) => Math.max(0, previous - 1))}
|
|
582
|
+
>
|
|
583
|
+
<ChevronLeft size={16} /> Previous
|
|
584
|
+
</Button>
|
|
585
|
+
<Button
|
|
586
|
+
variant="outline"
|
|
587
|
+
size="sm"
|
|
588
|
+
disabled={!hasMore || isLoading}
|
|
589
|
+
onClick={() => setPageIndex((previous) => previous + 1)}
|
|
590
|
+
>
|
|
591
|
+
Next <ChevronRight size={16} />
|
|
592
|
+
</Button>
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
)}
|
|
547
596
|
</div>
|
|
548
597
|
)}
|
|
549
598
|
</PageSection>
|
|
@@ -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,7 +98,12 @@ 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
|
|
101
|
+
const [fleetSearch, setFleetSearch] = useState('');
|
|
102
|
+
const debouncedFleetSearch = useDebouncedValue(fleetSearch);
|
|
103
|
+
const { vehicles, drivers } = useFleet({
|
|
104
|
+
limit: 20,
|
|
105
|
+
filters: { search: debouncedFleetSearch || undefined },
|
|
106
|
+
});
|
|
100
107
|
const { message: notice, notify } = useNotice();
|
|
101
108
|
|
|
102
109
|
const [selectedVehicle, setSelectedVehicle] = useState('');
|
|
@@ -477,6 +484,15 @@ export default function OrderDetailsPage() {
|
|
|
477
484
|
<Separator />
|
|
478
485
|
|
|
479
486
|
<div className="space-y-3">
|
|
487
|
+
<div className="space-y-1.5">
|
|
488
|
+
<Label htmlFor="assign-fleet-search">Search fleet</Label>
|
|
489
|
+
<Input
|
|
490
|
+
id="assign-fleet-search"
|
|
491
|
+
value={fleetSearch}
|
|
492
|
+
onChange={(event) => setFleetSearch(event.target.value)}
|
|
493
|
+
placeholder="Vehicle number, driver name or ID"
|
|
494
|
+
/>
|
|
495
|
+
</div>
|
|
480
496
|
<div className="space-y-1.5">
|
|
481
497
|
<Label htmlFor="assign-vehicle">Vehicle</Label>
|
|
482
498
|
<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"
|