@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.
Files changed (77) hide show
  1. package/dist/components/CarbonFootprintPanel.js +1 -1
  2. package/dist/components/CustomerPicker.js +76 -65
  3. package/dist/components/DataTable.js +140 -139
  4. package/dist/components/DocumentsCard.js +14 -8
  5. package/dist/generated/wspace-types.d.ts +1043 -5
  6. package/dist/hooks/useAnalytics.js +21 -1
  7. package/dist/hooks/useCustomers.js +94 -78
  8. package/dist/hooks/useDebouncedValue.js +11 -0
  9. package/dist/hooks/useEntityDocuments.js +28 -18
  10. package/dist/hooks/useEpod.js +24 -17
  11. package/dist/hooks/useFleet.js +146 -125
  12. package/dist/hooks/useInventory.js +255 -186
  13. package/dist/hooks/useMultiModalShipments.js +35 -27
  14. package/dist/hooks/useOrders.js +73 -73
  15. package/dist/hooks/useRoutes.js +97 -75
  16. package/dist/hooks/useTracking.js +100 -88
  17. package/dist/movethewheels/components/CustomerPicker.d.ts +3 -1
  18. package/dist/movethewheels/components/DataTable.d.ts +5 -1
  19. package/dist/movethewheels/hooks/useCustomers.d.ts +0 -1
  20. package/dist/movethewheels/hooks/useDebouncedValue.d.ts +1 -0
  21. package/dist/movethewheels/hooks/useEntityDocuments.d.ts +2 -0
  22. package/dist/movethewheels/hooks/useEpod.d.ts +6 -1
  23. package/dist/movethewheels/hooks/useFleet.d.ts +49 -5
  24. package/dist/movethewheels/hooks/useInventory.d.ts +24 -17
  25. package/dist/movethewheels/hooks/useMultiModalShipments.d.ts +6 -1
  26. package/dist/movethewheels/hooks/useOrders.d.ts +33 -5
  27. package/dist/movethewheels/hooks/useRoutes.d.ts +2 -1
  28. package/dist/movethewheels/hooks/useTracking.d.ts +12 -11
  29. package/dist/movethewheels/types/index.d.ts +7 -5
  30. package/dist/pages/CreateOrderPage.js +207 -201
  31. package/dist/pages/CustomersPage.js +61 -54
  32. package/dist/pages/DeliveryCapturePage.js +131 -109
  33. package/dist/pages/DriversPage.js +100 -92
  34. package/dist/pages/InventoryPage.js +239 -236
  35. package/dist/pages/MultiModalShipmentPage.js +163 -130
  36. package/dist/pages/OrderDetailsPage.js +224 -208
  37. package/dist/pages/OrdersListPage.js +66 -60
  38. package/dist/pages/ProductDetailsPage.js +241 -200
  39. package/dist/pages/ProductsPage.js +59 -52
  40. package/dist/pages/RouteDetailsPage.js +343 -321
  41. package/dist/pages/RoutesPage.js +55 -48
  42. package/dist/pages/VehiclesPage.js +63 -56
  43. package/dist/pages/WarehousesPage.js +60 -53
  44. package/package.json +1 -1
  45. package/src/generated/wspace-types.ts +1188 -5
  46. package/src/movethewheels/components/CarbonFootprintPanel.tsx +0 -1
  47. package/src/movethewheels/components/CustomerPicker.tsx +14 -0
  48. package/src/movethewheels/components/DataTable.tsx +34 -18
  49. package/src/movethewheels/components/DocumentsCard.tsx +6 -1
  50. package/src/movethewheels/components/maps/leaflet-setup.ts +1 -8
  51. package/src/movethewheels/hooks/useAnalytics.ts +27 -0
  52. package/src/movethewheels/hooks/useCustomers.ts +64 -24
  53. package/src/movethewheels/hooks/useDebouncedValue.ts +12 -0
  54. package/src/movethewheels/hooks/useEntityDocuments.ts +43 -25
  55. package/src/movethewheels/hooks/useEpod.ts +14 -7
  56. package/src/movethewheels/hooks/useFleet.ts +58 -37
  57. package/src/movethewheels/hooks/useFleetIntelligence.ts +1 -4
  58. package/src/movethewheels/hooks/useInventory.ts +211 -90
  59. package/src/movethewheels/hooks/useMultiModalShipments.ts +15 -11
  60. package/src/movethewheels/hooks/useOrders.ts +13 -14
  61. package/src/movethewheels/hooks/useRoutes.ts +80 -23
  62. package/src/movethewheels/hooks/useTracking.ts +130 -98
  63. package/src/movethewheels/pages/CreateOrderPage.tsx +9 -1
  64. package/src/movethewheels/pages/CustomersPage.tsx +11 -1
  65. package/src/movethewheels/pages/DeliveryCapturePage.tsx +31 -1
  66. package/src/movethewheels/pages/DriversPage.tsx +24 -12
  67. package/src/movethewheels/pages/InventoryPage.tsx +57 -32
  68. package/src/movethewheels/pages/MultiModalShipmentPage.tsx +52 -3
  69. package/src/movethewheels/pages/OrderDetailsPage.tsx +17 -1
  70. package/src/movethewheels/pages/OrdersListPage.tsx +20 -3
  71. package/src/movethewheels/pages/ProductDetailsPage.tsx +52 -3
  72. package/src/movethewheels/pages/ProductsPage.tsx +11 -1
  73. package/src/movethewheels/pages/RouteDetailsPage.tsx +35 -5
  74. package/src/movethewheels/pages/RoutesPage.tsx +11 -1
  75. package/src/movethewheels/pages/VehiclesPage.tsx +19 -2
  76. package/src/movethewheels/pages/WarehousesPage.tsx +11 -1
  77. 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 { jobs, isLoading, error, refetch } = useMyPendingEpodDeliveryJobs();
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 { drivers, isLoading, error, refetch } = useDrivers({
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: 500 });
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 visibleDrivers = useMemo(
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={visibleDrivers}
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={filtersActive ? undefined : totalDrivers}
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 { inventory, products, warehouses, isLoading, error, refetch } = useInventory();
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
- const productById = new Map(products.map((product) => [product.id, product]));
117
- const warehouseById = new Map(warehouses.map((warehouse) => [warehouse.id, warehouse]));
118
- return inventory.map((item) => {
119
- const product = productById.get(item.productId);
120
- return {
121
- id: item.id,
122
- productId: item.productId,
123
- productName: product?.name ?? unknownProductLabel,
124
- sku: product?.sku ?? '—',
125
- warehouseName: warehouseById.get(item.warehouseId)?.name ?? '—',
126
- onHand: item.quantity,
127
- allocated: item.reservedQuantity,
128
- available: item.availableQuantity,
129
- reorderLevel: item.reorderLevel,
130
- stockState:
131
- item.quantity === 0
132
- ? 'out_of_stock'
133
- : item.availableQuantity <= item.reorderLevel
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')} ({lowStockItems.length}{' '}
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.slice(0, 5).map((item) => {
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
- {product?.name ||
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:')} {product?.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 { Ship, Plane, Truck, TrainFront, Plus, Trash2, Calculator } from 'lucide-react';
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 { shipments, isLoading, error, calculateFreightRate, planMultiModalShipment } =
73
- useMultiModalShipments();
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 { vehicles, drivers } = useFleet();
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 { orders, hasMore, isLoading, error, refetch } = useOrders({
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={filtersActive ? undefined : totalOrders}
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 { Package, ArrowLeft, Edit, Box, DollarSign, Tag, Save, Warehouse } from 'lucide-react';
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
- const totalAvailable = stockLevels.reduce((sum, level) => sum + level.available, 0);
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 { products, hasMore, isLoading, error, refetch } = useProducts({
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"