@burdenoff/microfe-movethewheels 2026.920.2 → 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 (82) hide show
  1. package/dist/components/CarbonFootprintPanel.js +1 -1
  2. package/dist/components/CustomerPicker.js +271 -0
  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 +19 -0
  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/movethewheels/utils/navigation.d.ts +14 -0
  31. package/dist/pages/CreateOrderPage.js +228 -217
  32. package/dist/pages/CustomerDetailsPage.js +220 -220
  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 +224 -208
  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 +343 -321
  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/dist/utils/navigation.js +22 -1
  47. package/package.json +1 -1
  48. package/src/generated/wspace-types.ts +1188 -5
  49. package/src/movethewheels/components/CarbonFootprintPanel.tsx +0 -1
  50. package/src/movethewheels/components/CustomerPicker.tsx +321 -0
  51. package/src/movethewheels/components/DataTable.tsx +34 -18
  52. package/src/movethewheels/components/DocumentsCard.tsx +6 -1
  53. package/src/movethewheels/components/maps/leaflet-setup.ts +1 -8
  54. package/src/movethewheels/hooks/useAnalytics.ts +27 -0
  55. package/src/movethewheels/hooks/useCustomers.ts +64 -24
  56. package/src/movethewheels/hooks/useDebouncedValue.ts +12 -0
  57. package/src/movethewheels/hooks/useEntityDocuments.ts +43 -25
  58. package/src/movethewheels/hooks/useEpod.ts +14 -7
  59. package/src/movethewheels/hooks/useFleet.ts +58 -37
  60. package/src/movethewheels/hooks/useFleetIntelligence.ts +1 -4
  61. package/src/movethewheels/hooks/useInventory.ts +211 -90
  62. package/src/movethewheels/hooks/useMultiModalShipments.ts +15 -11
  63. package/src/movethewheels/hooks/useOrders.ts +13 -14
  64. package/src/movethewheels/hooks/useRoutes.ts +80 -23
  65. package/src/movethewheels/hooks/useTracking.ts +130 -98
  66. package/src/movethewheels/pages/CreateOrderPage.tsx +92 -31
  67. package/src/movethewheels/pages/CustomerDetailsPage.tsx +15 -6
  68. package/src/movethewheels/pages/CustomersPage.tsx +11 -1
  69. package/src/movethewheels/pages/DeliveryCapturePage.tsx +31 -1
  70. package/src/movethewheels/pages/DriversPage.tsx +24 -12
  71. package/src/movethewheels/pages/InventoryPage.tsx +57 -32
  72. package/src/movethewheels/pages/MultiModalShipmentPage.tsx +52 -3
  73. package/src/movethewheels/pages/OrderDetailsPage.tsx +17 -1
  74. package/src/movethewheels/pages/OrdersListPage.tsx +20 -3
  75. package/src/movethewheels/pages/ProductDetailsPage.tsx +52 -3
  76. package/src/movethewheels/pages/ProductsPage.tsx +11 -1
  77. package/src/movethewheels/pages/RouteDetailsPage.tsx +35 -5
  78. package/src/movethewheels/pages/RoutesPage.tsx +11 -1
  79. package/src/movethewheels/pages/VehiclesPage.tsx +19 -2
  80. package/src/movethewheels/pages/WarehousesPage.tsx +11 -1
  81. package/src/movethewheels/types/index.ts +7 -5
  82. package/src/movethewheels/utils/navigation.ts +32 -0
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { useEffect, useState } from 'react';
8
- import { useParams } from 'react-router';
8
+ import { useParams, useSearchParams } from 'react-router';
9
9
  import { Package, ArrowLeft, Save, Plus, Trash2, Users, MapPin, CalendarClock } from 'lucide-react';
10
10
  import { PageLayout, EmptyState } from '../components/PageLayout';
11
11
  import { FormError } from '../components/FormError';
@@ -27,9 +27,12 @@ import {
27
27
  } from '../components/ui';
28
28
  import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
29
29
  import { useOrder, useOrders } from '../hooks/useOrders';
30
- import { useCustomers } from '../hooks/useCustomers';
30
+ import { useCustomer, useCustomers } from '../hooks/useCustomers';
31
+ import { useDebouncedValue } from '../hooks/useDebouncedValue';
31
32
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
32
- import { joinPath } from '../utils/navigation';
33
+ import { joinPath, buildCustomerCreatePath } from '../utils/navigation';
34
+ import { CustomerPicker } from '../components/CustomerPicker';
35
+ import type { Customer } from '../types';
33
36
 
34
37
  /**
35
38
  * `<input type="datetime-local">` needs a `YYYY-MM-DDTHH:mm` string in LOCAL
@@ -49,15 +52,30 @@ function toDateTimeLocalValue(date: Date | undefined): string {
49
52
 
50
53
  export default function CreateOrderPage() {
51
54
  const { orderId } = useParams<{ orderId: string }>();
55
+ const [searchParams] = useSearchParams();
56
+ const prefilledCustomerId = searchParams.get('customerId')?.trim() || undefined;
52
57
  const { basePath, navigate } = useMoveTheWheels();
53
58
  const { order } = useOrder(orderId);
54
59
  const { createOrder, updateOrder } = useOrders();
55
- const { customers } = useCustomers({ limit: 100 });
60
+ const [customerSearch, setCustomerSearch] = useState('');
61
+ const debouncedCustomerSearch = useDebouncedValue(customerSearch);
62
+ const {
63
+ customers,
64
+ createCustomer,
65
+ isLoading: customersLoading,
66
+ error: customersError,
67
+ refetch: refetchCustomers,
68
+ } = useCustomers({
69
+ limit: 20,
70
+ filters: { search: debouncedCustomerSearch || undefined },
71
+ });
72
+ const { customer: linkedCustomer } = useCustomer(prefilledCustomerId ?? order?.customerId);
56
73
 
57
74
  const isEditing = !!orderId && !!order;
58
75
 
76
+ const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(null);
59
77
  const [formData, setFormData] = useState({
60
- customerId: order?.customerId || '',
78
+ customerId: prefilledCustomerId || order?.customerId || '',
61
79
  priority: order?.priority || 'normal',
62
80
  pickupAddress: {
63
81
  street: order?.pickupAddress.street || '',
@@ -81,6 +99,18 @@ export default function CreateOrderPage() {
81
99
  const [isSubmitting, setIsSubmitting] = useState(false);
82
100
  const [formError, setFormError] = useState<string | null>(null);
83
101
 
102
+ useEffect(() => {
103
+ if (!linkedCustomer) {
104
+ return;
105
+ }
106
+ setSelectedCustomer((previous) =>
107
+ previous?.id === linkedCustomer.id ? previous : linkedCustomer
108
+ );
109
+ setFormData((previous) =>
110
+ previous.customerId ? previous : { ...previous, customerId: linkedCustomer.id }
111
+ );
112
+ }, [linkedCustomer]);
113
+
84
114
  // Prefill the form from the loaded order when editing. `order` arrives
85
115
  // asynchronously after the initial render, so the useState initializers alone
86
116
  // leave the fields blank until this syncs them.
@@ -112,15 +142,24 @@ export default function CreateOrderPage() {
112
142
  }, [order]);
113
143
 
114
144
  const goBack = () => {
115
- if (navigate) {
116
- navigate(joinPath(basePath, 'orders'));
145
+ if (!navigate) {
146
+ return;
117
147
  }
148
+ if (!isEditing && prefilledCustomerId) {
149
+ navigate(joinPath(basePath, `customers/${prefilledCustomerId}`));
150
+ return;
151
+ }
152
+ navigate(joinPath(basePath, 'orders'));
118
153
  };
119
154
 
155
+ const getSelectedCustomer = () =>
156
+ selectedCustomer ?? customers.find((customer) => customer.id === formData.customerId);
157
+
120
158
  const validate = (): string | null => {
121
- const selectedCustomer = customers.find((c) => c.id === formData.customerId);
122
- if (!formData.customerId || !selectedCustomer) {
123
- return 'Select a customer for this order.';
159
+ if (!formData.customerId || !getSelectedCustomer()) {
160
+ return customersError
161
+ ? 'Customers could not be loaded. Retry or create a customer before continuing.'
162
+ : 'Select a customer for this order.';
124
163
  }
125
164
  if (!formData.pickupAddress.street.trim()) {
126
165
  return 'Enter a pickup street address.';
@@ -147,9 +186,13 @@ export default function CreateOrderPage() {
147
186
  return;
148
187
  }
149
188
 
150
- const selectedCustomer = customers.find((c) => c.id === formData.customerId);
189
+ const selectedCustomer = getSelectedCustomer();
151
190
  if (!selectedCustomer) {
152
- setFormError('Select a customer for this order.');
191
+ setFormError(
192
+ customersError
193
+ ? 'Customers could not be loaded. Retry or create a customer before continuing.'
194
+ : 'Select a customer for this order.'
195
+ );
153
196
  return;
154
197
  }
155
198
 
@@ -242,6 +285,14 @@ export default function CreateOrderPage() {
242
285
  }));
243
286
  };
244
287
 
288
+ const goToCustomerCreate = () => {
289
+ if (!navigate) {
290
+ return;
291
+ }
292
+ const orderPath = isEditing && orderId ? `orders/${orderId}/edit` : 'orders/new';
293
+ navigate(joinPath(basePath, buildCustomerCreatePath(orderPath)));
294
+ };
295
+
245
296
  return (
246
297
  <PageLayout
247
298
  title={isEditing ? 'Edit Order' : 'Create Order'}
@@ -253,7 +304,7 @@ export default function CreateOrderPage() {
253
304
  <ArrowLeft size={16} />
254
305
  Cancel
255
306
  </Button>
256
- <Button onClick={handleSubmit} disabled={isSubmitting}>
307
+ <Button onClick={handleSubmit} disabled={isSubmitting || customersLoading}>
257
308
  <Save size={16} />
258
309
  {isSubmitting ? 'Saving...' : isEditing ? 'Save Changes' : 'Create Order'}
259
310
  </Button>
@@ -304,24 +355,34 @@ export default function CreateOrderPage() {
304
355
  <h2 className="text-base font-semibold text-text-primary mb-4">Order Details</h2>
305
356
  <div className="space-y-4">
306
357
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
307
- <div className="space-y-2">
308
- <Label htmlFor="customer">Customer</Label>
309
- <Select
310
- value={formData.customerId}
311
- onValueChange={(value) => updateField('customerId', value)}
312
- >
313
- <SelectTrigger id="customer">
314
- <SelectValue placeholder="Select customer" />
315
- </SelectTrigger>
316
- <SelectContent>
317
- {customers.map((customer) => (
318
- <SelectItem key={customer.id} value={customer.id}>
319
- {customer.name} ({customer.email})
320
- </SelectItem>
321
- ))}
322
- </SelectContent>
323
- </Select>
324
- </div>
358
+ <CustomerPicker
359
+ customers={customers}
360
+ value={formData.customerId}
361
+ onValueChange={(value) => {
362
+ if (!value) {
363
+ return;
364
+ }
365
+ updateField('customerId', value);
366
+ setSelectedCustomer(
367
+ customers.find((customer) => customer.id === value) ??
368
+ (selectedCustomer?.id === value ? selectedCustomer : null)
369
+ );
370
+ setFormError(null);
371
+ }}
372
+ onCreated={(customer) => {
373
+ setSelectedCustomer(customer);
374
+ updateField('customerId', customer.id);
375
+ setFormError(null);
376
+ }}
377
+ createCustomer={createCustomer}
378
+ isLoading={customersLoading}
379
+ error={customersError}
380
+ onRetry={refetchCustomers}
381
+ onOpenCustomers={goToCustomerCreate}
382
+ selectedCustomer={selectedCustomer}
383
+ searchQuery={customerSearch}
384
+ onSearchQueryChange={setCustomerSearch}
385
+ />
325
386
  <div className="space-y-2">
326
387
  <Label htmlFor="priority">Priority</Label>
327
388
  <Select
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { useEffect, useState } from 'react';
6
- import { useParams } from 'react-router';
6
+ import { useParams, useSearchParams } from 'react-router';
7
7
  import {
8
8
  Users,
9
9
  ArrowLeft,
@@ -40,7 +40,7 @@ import { useCustomer, useCustomers } from '../hooks/useCustomers';
40
40
  import { useOrders } from '../hooks/useOrders';
41
41
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
42
42
  import { formatAddress, formatCurrency, formatDate } from '../utils/formatters';
43
- import { joinPath } from '../utils/navigation';
43
+ import { joinPath, buildOrderCustomerPath, getSafeCustomerReturnPath } from '../utils/navigation';
44
44
  import { FormError } from '../components/FormError';
45
45
  import { Notice, useNotice } from '../components/Notice';
46
46
 
@@ -62,6 +62,8 @@ function safeHttpUrl(raw: string | undefined): string | null {
62
62
 
63
63
  function NewCustomerForm() {
64
64
  const { basePath, navigate } = useMoveTheWheels();
65
+ const [searchParams] = useSearchParams();
66
+ const returnTo = getSafeCustomerReturnPath(searchParams.get('returnTo'));
65
67
  const { createCustomer } = useCustomers();
66
68
  const [form, setForm] = useState({
67
69
  name: '',
@@ -91,7 +93,10 @@ function NewCustomerForm() {
91
93
  phone: form.phone.trim() || undefined,
92
94
  company: form.company.trim() || undefined,
93
95
  });
94
- navigate?.(joinPath(basePath, `customers/${created.id}`));
96
+ const destination = returnTo
97
+ ? buildOrderCustomerPath(returnTo, created.id)
98
+ : `customers/${created.id}`;
99
+ navigate?.(joinPath(basePath, destination));
95
100
  } catch (err) {
96
101
  console.error('Failed to create customer:', err);
97
102
  setError(err instanceof Error ? err.message : 'Failed to create customer. Please try again.');
@@ -330,7 +335,7 @@ export default function CustomerDetailsPage() {
330
335
  label: 'Book a delivery for this customer',
331
336
  description: 'Start a new order pre-filled for this account.',
332
337
  icon: <FileText size={16} />,
333
- onClick: () => goTo('orders/new'),
338
+ onClick: () => goTo(buildOrderCustomerPath('orders/new', customer.id)),
334
339
  },
335
340
  {
336
341
  id: 'edit',
@@ -475,7 +480,11 @@ export default function CustomerDetailsPage() {
475
480
  <CardTitle className="flex items-center gap-2">
476
481
  <Package size={18} /> Orders
477
482
  </CardTitle>
478
- <Button variant="outline" size="sm" onClick={() => goTo('orders/new')}>
483
+ <Button
484
+ variant="outline"
485
+ size="sm"
486
+ onClick={() => goTo(buildOrderCustomerPath('orders/new', customer.id))}
487
+ >
479
488
  <FileText size={14} /> New order
480
489
  </Button>
481
490
  </CardHeader>
@@ -508,7 +517,7 @@ export default function CustomerDetailsPage() {
508
517
  <p className="text-text-secondary">No orders yet for this customer</p>
509
518
  <button
510
519
  type="button"
511
- onClick={() => goTo('orders/new')}
520
+ onClick={() => goTo(buildOrderCustomerPath('orders/new', customer.id))}
512
521
  className="mt-1 text-sm text-primary hover:underline"
513
522
  >
514
523
  Book their first delivery
@@ -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"