@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
@@ -6,7 +6,17 @@
6
6
 
7
7
  import { useEffect, useState } from 'react';
8
8
  import { useParams, useSearchParams } from 'react-router';
9
- import { Package, ArrowLeft, Save, Plus, Trash2, Users, MapPin, CalendarClock } from 'lucide-react';
9
+ import {
10
+ Package,
11
+ ArrowLeft,
12
+ Save,
13
+ Plus,
14
+ Trash2,
15
+ Users,
16
+ MapPin,
17
+ CalendarClock,
18
+ Route as RouteIcon,
19
+ } from 'lucide-react';
10
20
  import { PageLayout, EmptyState } from '../components/PageLayout';
11
21
  import { FormError } from '../components/FormError';
12
22
  import {
@@ -28,6 +38,8 @@ import {
28
38
  import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
29
39
  import { useOrder, useOrders } from '../hooks/useOrders';
30
40
  import { useCustomer, useCustomers } from '../hooks/useCustomers';
41
+ import { useRoutes } from '../hooks/useRoutes';
42
+ import { useDebouncedValue } from '../hooks/useDebouncedValue';
31
43
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
32
44
  import { joinPath, buildCustomerCreatePath } from '../utils/navigation';
33
45
  import { CustomerPicker } from '../components/CustomerPicker';
@@ -56,14 +68,27 @@ export default function CreateOrderPage() {
56
68
  const { basePath, navigate } = useMoveTheWheels();
57
69
  const { order } = useOrder(orderId);
58
70
  const { createOrder, updateOrder } = useOrders();
71
+ const [customerSearch, setCustomerSearch] = useState('');
72
+ const debouncedCustomerSearch = useDebouncedValue(customerSearch);
59
73
  const {
60
74
  customers,
61
75
  createCustomer,
62
76
  isLoading: customersLoading,
63
77
  error: customersError,
64
78
  refetch: refetchCustomers,
65
- } = useCustomers({ limit: 100 });
79
+ } = useCustomers({
80
+ limit: 20,
81
+ filters: { search: debouncedCustomerSearch || undefined },
82
+ });
66
83
  const { customer: linkedCustomer } = useCustomer(prefilledCustomerId ?? order?.customerId);
84
+ const {
85
+ routes,
86
+ isLoading: routesLoading,
87
+ error: routesError,
88
+ } = useRoutes({
89
+ status: 'planned',
90
+ limit: 20,
91
+ });
67
92
 
68
93
  const isEditing = !!orderId && !!order;
69
94
 
@@ -71,6 +96,7 @@ export default function CreateOrderPage() {
71
96
  const [formData, setFormData] = useState({
72
97
  customerId: prefilledCustomerId || order?.customerId || '',
73
98
  priority: order?.priority || 'normal',
99
+ routeId: order?.routeId || '',
74
100
  pickupAddress: {
75
101
  street: order?.pickupAddress.street || '',
76
102
  city: order?.pickupAddress.city || '',
@@ -115,6 +141,7 @@ export default function CreateOrderPage() {
115
141
  setFormData({
116
142
  customerId: order.customerId || '',
117
143
  priority: order.priority || 'normal',
144
+ routeId: order.routeId || '',
118
145
  pickupAddress: {
119
146
  street: order.pickupAddress.street || '',
120
147
  city: order.pickupAddress.city || '',
@@ -206,6 +233,7 @@ export default function CreateOrderPage() {
206
233
  items: formData.items,
207
234
  // `null` clears an existing slot on edit; the service field is nullable.
208
235
  scheduledDelivery: formData.scheduledDelivery ? new Date(formData.scheduledDelivery) : null,
236
+ routeId: formData.routeId || undefined,
209
237
  specialInstructions: formData.specialInstructions,
210
238
  };
211
239
 
@@ -246,6 +274,35 @@ export default function CreateOrderPage() {
246
274
  }));
247
275
  };
248
276
 
277
+ const selectSavedRoute = (value: string) => {
278
+ if (value === 'none') {
279
+ updateField('routeId', '');
280
+ return;
281
+ }
282
+ const route = routes.find((candidate) => candidate.id === value);
283
+ if (!route) return;
284
+
285
+ const pickup = route.stops.find((stop) => stop.type === 'pickup')?.address;
286
+ const delivery = [...route.stops].reverse().find((stop) => stop.type === 'delivery')?.address;
287
+ const mergeAddress = (current: typeof formData.pickupAddress, saved: typeof pickup) =>
288
+ saved
289
+ ? {
290
+ street: current.street.trim() ? current.street : saved.street,
291
+ city: current.city.trim() ? current.city : saved.city,
292
+ state: current.state.trim() ? current.state : saved.state,
293
+ postalCode: current.postalCode.trim() ? current.postalCode : saved.postalCode,
294
+ country: current.country.trim() ? current.country : saved.country,
295
+ }
296
+ : current;
297
+
298
+ setFormData((previous) => ({
299
+ ...previous,
300
+ routeId: route.id,
301
+ pickupAddress: mergeAddress(previous.pickupAddress, pickup),
302
+ deliveryAddress: mergeAddress(previous.deliveryAddress, delivery),
303
+ }));
304
+ };
305
+
249
306
  const addItem = () => {
250
307
  setFormData((prev) => ({
251
308
  ...prev,
@@ -374,6 +431,8 @@ export default function CreateOrderPage() {
374
431
  onRetry={refetchCustomers}
375
432
  onOpenCustomers={goToCustomerCreate}
376
433
  selectedCustomer={selectedCustomer}
434
+ searchQuery={customerSearch}
435
+ onSearchQueryChange={setCustomerSearch}
377
436
  />
378
437
  <div className="space-y-2">
379
438
  <Label htmlFor="priority">Priority</Label>
@@ -396,6 +455,38 @@ export default function CreateOrderPage() {
396
455
  </div>
397
456
  </EmphasisPanel>
398
457
 
458
+ {/* Saved route — optional shortcut; manual addresses remain authoritative. */}
459
+ <Card>
460
+ <CardHeader>
461
+ <CardTitle className="flex items-center gap-2">
462
+ <RouteIcon size={18} />
463
+ Saved Route (Optional)
464
+ </CardTitle>
465
+ </CardHeader>
466
+ <CardContent className="space-y-2">
467
+ <Select value={formData.routeId || 'none'} onValueChange={selectSavedRoute}>
468
+ <SelectTrigger id="saved-route">
469
+ <SelectValue placeholder="Choose a saved route" />
470
+ </SelectTrigger>
471
+ <SelectContent>
472
+ <SelectItem value="none">No saved route</SelectItem>
473
+ {routes.map((route) => (
474
+ <SelectItem key={route.id} value={route.id}>
475
+ {route.name} · {route.stops.length} stops
476
+ </SelectItem>
477
+ ))}
478
+ </SelectContent>
479
+ </Select>
480
+ <p className="text-xs text-text-secondary">
481
+ {routesLoading
482
+ ? 'Loading planned routes…'
483
+ : routesError
484
+ ? 'Saved routes are unavailable; enter pickup and delivery addresses manually.'
485
+ : 'A saved route fills any blank address fields. You can always edit both addresses below.'}
486
+ </p>
487
+ </CardContent>
488
+ </Card>
489
+
399
490
  {/* Addresses */}
400
491
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
401
492
  {/* Pickup Address */}
@@ -11,6 +11,7 @@ import { TypeBadge } from '../components/StatusBadge';
11
11
  import { Badge, Avatar, AvatarFallback, Button } from '../components/ui';
12
12
  import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
13
13
  import { useCustomers, useCustomerStats } from '../hooks/useCustomers';
14
+ import { useDebouncedValue } from '../hooks/useDebouncedValue';
14
15
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
15
16
  import { formatDate } from '../utils/formatters';
16
17
  import { joinPath } from '../utils/navigation';
@@ -20,9 +21,12 @@ export default function CustomersPage() {
20
21
  const { basePath, navigate } = useMoveTheWheels();
21
22
  const [page, setPage] = useState(0);
22
23
  const [pageSize, setPageSize] = useState(25);
23
- const { customers, hasMore, isLoading, error, refetch } = useCustomers({
24
+ const [search, setSearch] = useState('');
25
+ const debouncedSearch = useDebouncedValue(search);
26
+ const { customers, totalCustomers, hasMore, isLoading, error, refetch } = useCustomers({
24
27
  limit: pageSize,
25
28
  offset: page * pageSize,
29
+ filters: { search: debouncedSearch },
26
30
  });
27
31
  // Bounded aggregate for the tiles + subtitle — never the truncated page.
28
32
  const { stats } = useCustomerStats();
@@ -156,6 +160,11 @@ export default function CustomersPage() {
156
160
  searchable
157
161
  searchPlaceholder="Search customers..."
158
162
  searchFields={['name', 'email', 'company'] as (keyof Customer)[]}
163
+ searchQuery={search}
164
+ onSearchQueryChange={(query) => {
165
+ setSearch(query);
166
+ setPage(0);
167
+ }}
159
168
  onRowClick={(row) => goTo(`customers/${row.id}`)}
160
169
  getRowId={(row) => row.id}
161
170
  pageIndex={page}
@@ -165,6 +174,7 @@ export default function CustomersPage() {
165
174
  setPageSize(size);
166
175
  setPage(0);
167
176
  }}
177
+ totalItems={totalCustomers}
168
178
  hasNextPage={hasMore}
169
179
  emptyIllustration="empty-people"
170
180
  emptyTitle="No customers yet"
@@ -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>