@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.
Files changed (79) 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 +2719 -8
  6. package/dist/hooks/index.js +1 -1
  7. package/dist/hooks/useAnalytics.js +21 -1
  8. package/dist/hooks/useCustomers.js +94 -78
  9. package/dist/hooks/useDebouncedValue.js +11 -0
  10. package/dist/hooks/useEntityDocuments.js +28 -18
  11. package/dist/hooks/useEpod.js +24 -17
  12. package/dist/hooks/useFleet.js +148 -125
  13. package/dist/hooks/useInventory.js +255 -186
  14. package/dist/hooks/useMultiModalShipments.js +35 -27
  15. package/dist/hooks/useOrders.js +139 -134
  16. package/dist/hooks/useRoutes.js +97 -75
  17. package/dist/hooks/useTracking.js +100 -88
  18. package/dist/index.js +6 -6
  19. package/dist/movethewheels/components/CustomerPicker.d.ts +3 -1
  20. package/dist/movethewheels/components/DataTable.d.ts +5 -1
  21. package/dist/movethewheels/hooks/useCustomers.d.ts +0 -1
  22. package/dist/movethewheels/hooks/useDebouncedValue.d.ts +1 -0
  23. package/dist/movethewheels/hooks/useEntityDocuments.d.ts +2 -0
  24. package/dist/movethewheels/hooks/useEpod.d.ts +6 -1
  25. package/dist/movethewheels/hooks/useFleet.d.ts +49 -5
  26. package/dist/movethewheels/hooks/useInventory.d.ts +24 -17
  27. package/dist/movethewheels/hooks/useMultiModalShipments.d.ts +6 -1
  28. package/dist/movethewheels/hooks/useOrders.d.ts +48 -5
  29. package/dist/movethewheels/hooks/useRoutes.d.ts +18 -1
  30. package/dist/movethewheels/hooks/useTracking.d.ts +12 -11
  31. package/dist/movethewheels/types/index.d.ts +12 -5
  32. package/dist/pages/CreateOrderPage.js +191 -129
  33. package/dist/pages/CustomersPage.js +61 -54
  34. package/dist/pages/DeliveryCapturePage.js +131 -109
  35. package/dist/pages/DriversPage.js +100 -92
  36. package/dist/pages/InventoryPage.js +239 -236
  37. package/dist/pages/MultiModalShipmentPage.js +163 -130
  38. package/dist/pages/OrderDetailsPage.js +190 -106
  39. package/dist/pages/OrdersListPage.js +66 -60
  40. package/dist/pages/ProductDetailsPage.js +241 -200
  41. package/dist/pages/ProductsPage.js +59 -52
  42. package/dist/pages/RouteDetailsPage.js +324 -293
  43. package/dist/pages/RoutesPage.js +55 -48
  44. package/dist/pages/VehiclesPage.js +63 -56
  45. package/dist/pages/WarehousesPage.js +60 -53
  46. package/package.json +1 -1
  47. package/src/generated/wspace-types.ts +3136 -8
  48. package/src/movethewheels/components/CarbonFootprintPanel.tsx +0 -1
  49. package/src/movethewheels/components/CustomerPicker.tsx +14 -0
  50. package/src/movethewheels/components/DataTable.tsx +34 -18
  51. package/src/movethewheels/components/DocumentsCard.tsx +6 -1
  52. package/src/movethewheels/components/maps/leaflet-setup.ts +1 -8
  53. package/src/movethewheels/hooks/useAnalytics.ts +27 -0
  54. package/src/movethewheels/hooks/useCustomers.ts +64 -24
  55. package/src/movethewheels/hooks/useDebouncedValue.ts +12 -0
  56. package/src/movethewheels/hooks/useEntityDocuments.ts +43 -25
  57. package/src/movethewheels/hooks/useEpod.ts +14 -7
  58. package/src/movethewheels/hooks/useFleet.ts +66 -38
  59. package/src/movethewheels/hooks/useFleetIntelligence.ts +1 -4
  60. package/src/movethewheels/hooks/useInventory.ts +211 -90
  61. package/src/movethewheels/hooks/useMultiModalShipments.ts +15 -11
  62. package/src/movethewheels/hooks/useOrders.ts +48 -14
  63. package/src/movethewheels/hooks/useRoutes.ts +81 -24
  64. package/src/movethewheels/hooks/useTracking.ts +130 -98
  65. package/src/movethewheels/pages/CreateOrderPage.tsx +93 -2
  66. package/src/movethewheels/pages/CustomersPage.tsx +11 -1
  67. package/src/movethewheels/pages/DeliveryCapturePage.tsx +31 -1
  68. package/src/movethewheels/pages/DriversPage.tsx +24 -12
  69. package/src/movethewheels/pages/InventoryPage.tsx +57 -32
  70. package/src/movethewheels/pages/MultiModalShipmentPage.tsx +52 -3
  71. package/src/movethewheels/pages/OrderDetailsPage.tsx +91 -6
  72. package/src/movethewheels/pages/OrdersListPage.tsx +20 -3
  73. package/src/movethewheels/pages/ProductDetailsPage.tsx +52 -3
  74. package/src/movethewheels/pages/ProductsPage.tsx +11 -1
  75. package/src/movethewheels/pages/RouteDetailsPage.tsx +45 -5
  76. package/src/movethewheels/pages/RoutesPage.tsx +11 -1
  77. package/src/movethewheels/pages/VehiclesPage.tsx +19 -2
  78. package/src/movethewheels/pages/WarehousesPage.tsx +11 -1
  79. 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 { vehicles, drivers } = useFleet();
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 { 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"
@@ -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 { vehicles, drivers } = useFleet();
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
- {vehicles.map((vehicle) => (
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
- {drivers.map((driver) => (
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 { routes, hasMore, isLoading, error, refetch } = useRoutes({
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 { vehicles, isLoading, error, refetch } = useVehicles({
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={totalVehicles}
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 { warehouses, hasMore, isLoading, error, refetch } = useWarehouses({
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
- * Client-side quick filter for orders that still need a driver assigned
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