@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.
- package/dist/components/CarbonFootprintPanel.js +1 -1
- package/dist/components/CustomerPicker.js +271 -0
- package/dist/components/DataTable.js +140 -139
- package/dist/components/DocumentsCard.js +14 -8
- package/dist/generated/wspace-types.d.ts +1043 -5
- package/dist/hooks/useAnalytics.js +21 -1
- package/dist/hooks/useCustomers.js +94 -78
- package/dist/hooks/useDebouncedValue.js +11 -0
- package/dist/hooks/useEntityDocuments.js +28 -18
- package/dist/hooks/useEpod.js +24 -17
- package/dist/hooks/useFleet.js +146 -125
- package/dist/hooks/useInventory.js +255 -186
- package/dist/hooks/useMultiModalShipments.js +35 -27
- package/dist/hooks/useOrders.js +73 -73
- package/dist/hooks/useRoutes.js +97 -75
- package/dist/hooks/useTracking.js +100 -88
- package/dist/movethewheels/components/CustomerPicker.d.ts +19 -0
- package/dist/movethewheels/components/DataTable.d.ts +5 -1
- package/dist/movethewheels/hooks/useCustomers.d.ts +0 -1
- package/dist/movethewheels/hooks/useDebouncedValue.d.ts +1 -0
- package/dist/movethewheels/hooks/useEntityDocuments.d.ts +2 -0
- package/dist/movethewheels/hooks/useEpod.d.ts +6 -1
- package/dist/movethewheels/hooks/useFleet.d.ts +49 -5
- package/dist/movethewheels/hooks/useInventory.d.ts +24 -17
- package/dist/movethewheels/hooks/useMultiModalShipments.d.ts +6 -1
- package/dist/movethewheels/hooks/useOrders.d.ts +33 -5
- package/dist/movethewheels/hooks/useRoutes.d.ts +2 -1
- package/dist/movethewheels/hooks/useTracking.d.ts +12 -11
- package/dist/movethewheels/types/index.d.ts +7 -5
- package/dist/movethewheels/utils/navigation.d.ts +14 -0
- package/dist/pages/CreateOrderPage.js +228 -217
- package/dist/pages/CustomerDetailsPage.js +220 -220
- package/dist/pages/CustomersPage.js +61 -54
- package/dist/pages/DeliveryCapturePage.js +131 -109
- package/dist/pages/DriversPage.js +100 -92
- package/dist/pages/InventoryPage.js +239 -236
- package/dist/pages/MultiModalShipmentPage.js +163 -130
- package/dist/pages/OrderDetailsPage.js +224 -208
- package/dist/pages/OrdersListPage.js +66 -60
- package/dist/pages/ProductDetailsPage.js +241 -200
- package/dist/pages/ProductsPage.js +59 -52
- package/dist/pages/RouteDetailsPage.js +343 -321
- package/dist/pages/RoutesPage.js +55 -48
- package/dist/pages/VehiclesPage.js +63 -56
- package/dist/pages/WarehousesPage.js +60 -53
- package/dist/utils/navigation.js +22 -1
- package/package.json +1 -1
- package/src/generated/wspace-types.ts +1188 -5
- package/src/movethewheels/components/CarbonFootprintPanel.tsx +0 -1
- package/src/movethewheels/components/CustomerPicker.tsx +321 -0
- package/src/movethewheels/components/DataTable.tsx +34 -18
- package/src/movethewheels/components/DocumentsCard.tsx +6 -1
- package/src/movethewheels/components/maps/leaflet-setup.ts +1 -8
- package/src/movethewheels/hooks/useAnalytics.ts +27 -0
- package/src/movethewheels/hooks/useCustomers.ts +64 -24
- package/src/movethewheels/hooks/useDebouncedValue.ts +12 -0
- package/src/movethewheels/hooks/useEntityDocuments.ts +43 -25
- package/src/movethewheels/hooks/useEpod.ts +14 -7
- package/src/movethewheels/hooks/useFleet.ts +58 -37
- package/src/movethewheels/hooks/useFleetIntelligence.ts +1 -4
- package/src/movethewheels/hooks/useInventory.ts +211 -90
- package/src/movethewheels/hooks/useMultiModalShipments.ts +15 -11
- package/src/movethewheels/hooks/useOrders.ts +13 -14
- package/src/movethewheels/hooks/useRoutes.ts +80 -23
- package/src/movethewheels/hooks/useTracking.ts +130 -98
- package/src/movethewheels/pages/CreateOrderPage.tsx +92 -31
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +15 -6
- package/src/movethewheels/pages/CustomersPage.tsx +11 -1
- package/src/movethewheels/pages/DeliveryCapturePage.tsx +31 -1
- package/src/movethewheels/pages/DriversPage.tsx +24 -12
- package/src/movethewheels/pages/InventoryPage.tsx +57 -32
- package/src/movethewheels/pages/MultiModalShipmentPage.tsx +52 -3
- package/src/movethewheels/pages/OrderDetailsPage.tsx +17 -1
- package/src/movethewheels/pages/OrdersListPage.tsx +20 -3
- package/src/movethewheels/pages/ProductDetailsPage.tsx +52 -3
- package/src/movethewheels/pages/ProductsPage.tsx +11 -1
- package/src/movethewheels/pages/RouteDetailsPage.tsx +35 -5
- package/src/movethewheels/pages/RoutesPage.tsx +11 -1
- package/src/movethewheels/pages/VehiclesPage.tsx +19 -2
- package/src/movethewheels/pages/WarehousesPage.tsx +11 -1
- package/src/movethewheels/types/index.ts +7 -5
- 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
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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 =
|
|
189
|
+
const selectedCustomer = getSelectedCustomer();
|
|
151
190
|
if (!selectedCustomer) {
|
|
152
|
-
setFormError(
|
|
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
|
-
<
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
253
|
+
const [pageIndex, setPageIndex] = useState(0);
|
|
254
|
+
const pageSize = 20;
|
|
255
|
+
const { jobs, hasMore, isLoading, error, refetch } = useMyPendingEpodDeliveryJobs({
|
|
256
|
+
limit: pageSize,
|
|
257
|
+
offset: pageIndex * pageSize,
|
|
258
|
+
});
|
|
252
259
|
const [selectedJob, setSelectedJob] = useState<PendingEpodDeliveryJob | null>(null);
|
|
253
260
|
const { message, notify } = useNotice();
|
|
254
261
|
|
|
255
262
|
const handleCaptured = () => {
|
|
256
263
|
notify('Proof of delivery captured — delivery marked complete.');
|
|
257
264
|
setSelectedJob(null);
|
|
265
|
+
setPageIndex(0);
|
|
258
266
|
refetch();
|
|
259
267
|
};
|
|
260
268
|
|
|
@@ -309,6 +317,28 @@ export default function DeliveryCapturePage() {
|
|
|
309
317
|
</CardContent>
|
|
310
318
|
</Card>
|
|
311
319
|
))}
|
|
320
|
+
{(pageIndex > 0 || hasMore) && (
|
|
321
|
+
<div className="flex justify-end gap-2 pt-2">
|
|
322
|
+
<Button
|
|
323
|
+
type="button"
|
|
324
|
+
variant="outline"
|
|
325
|
+
size="sm"
|
|
326
|
+
disabled={pageIndex === 0 || isLoading}
|
|
327
|
+
onClick={() => setPageIndex((previous) => Math.max(0, previous - 1))}
|
|
328
|
+
>
|
|
329
|
+
<ChevronLeft size={16} /> Previous
|
|
330
|
+
</Button>
|
|
331
|
+
<Button
|
|
332
|
+
type="button"
|
|
333
|
+
variant="outline"
|
|
334
|
+
size="sm"
|
|
335
|
+
disabled={!hasMore || isLoading}
|
|
336
|
+
onClick={() => setPageIndex((previous) => previous + 1)}
|
|
337
|
+
>
|
|
338
|
+
Next <ChevronRight size={16} />
|
|
339
|
+
</Button>
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
312
342
|
</div>
|
|
313
343
|
) : (
|
|
314
344
|
<div className="text-center py-12">
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from '../components/ui';
|
|
34
34
|
import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
35
35
|
import { useDrivers, useDriverStatusCounts, useFleetStats, useVehicles } from '../hooks/useFleet';
|
|
36
|
+
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
36
37
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
37
38
|
import { formatDate } from '../utils/formatters';
|
|
38
39
|
import { joinPath } from '../utils/navigation';
|
|
@@ -50,12 +51,23 @@ export default function DriversPage() {
|
|
|
50
51
|
const { basePath, navigate } = useMoveTheWheels();
|
|
51
52
|
const [page, setPage] = useState(0);
|
|
52
53
|
const [pageSize, setPageSize] = useState(25);
|
|
54
|
+
const [search, setSearch] = useState('');
|
|
55
|
+
const debouncedSearch = useDebouncedValue(search);
|
|
53
56
|
const [statusFilter, setStatusFilter] = useState<DriverStatus | 'all'>('all');
|
|
54
57
|
const [licenseClassFilter, setLicenseClassFilter] = useState<string>('all');
|
|
55
|
-
const {
|
|
58
|
+
const {
|
|
59
|
+
drivers,
|
|
60
|
+
totalDrivers: filteredDriverCount,
|
|
61
|
+
hasMore,
|
|
62
|
+
isLoading,
|
|
63
|
+
error,
|
|
64
|
+
refetch,
|
|
65
|
+
} = useDrivers({
|
|
56
66
|
limit: pageSize,
|
|
57
67
|
offset: page * pageSize,
|
|
68
|
+
search: debouncedSearch || undefined,
|
|
58
69
|
status: statusFilter === 'all' ? undefined : statusFilter,
|
|
70
|
+
licenseClass: licenseClassFilter === 'all' ? undefined : licenseClassFilter,
|
|
59
71
|
});
|
|
60
72
|
// Workforce totals + tiles from the live aggregate, not the current page.
|
|
61
73
|
const { stats } = useFleetStats();
|
|
@@ -63,7 +75,7 @@ export default function DriversPage() {
|
|
|
63
75
|
const { counts } = useDriverStatusCounts();
|
|
64
76
|
// Drivers carry only `vehicleId`; the roster shows the fleet number, so the
|
|
65
77
|
// vehicle list is joined client-side into an id -> number lookup.
|
|
66
|
-
const { vehicles } = useVehicles({ limit:
|
|
78
|
+
const { vehicles } = useVehicles({ limit: 100 });
|
|
67
79
|
const vehicleNumberById = useMemo(
|
|
68
80
|
() => new Map(vehicles.map((vehicle) => [vehicle.id, vehicle.vehicleNumber])),
|
|
69
81
|
[vehicles]
|
|
@@ -79,14 +91,7 @@ export default function DriversPage() {
|
|
|
79
91
|
),
|
|
80
92
|
[drivers]
|
|
81
93
|
);
|
|
82
|
-
const
|
|
83
|
-
() =>
|
|
84
|
-
licenseClassFilter === 'all'
|
|
85
|
-
? drivers
|
|
86
|
-
: drivers.filter((driver) => driver.licenseType === licenseClassFilter),
|
|
87
|
-
[drivers, licenseClassFilter]
|
|
88
|
-
);
|
|
89
|
-
const filtersActive = statusFilter !== 'all' || licenseClassFilter !== 'all';
|
|
94
|
+
const filtersActive = statusFilter !== 'all' || licenseClassFilter !== 'all' || search.length > 0;
|
|
90
95
|
|
|
91
96
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
92
97
|
|
|
@@ -293,6 +298,7 @@ export default function DriversPage() {
|
|
|
293
298
|
onClick={() => {
|
|
294
299
|
setStatusFilter('all');
|
|
295
300
|
setLicenseClassFilter('all');
|
|
301
|
+
setSearch('');
|
|
296
302
|
setPage(0);
|
|
297
303
|
}}
|
|
298
304
|
>
|
|
@@ -305,13 +311,18 @@ export default function DriversPage() {
|
|
|
305
311
|
|
|
306
312
|
<DataTable
|
|
307
313
|
columns={columns}
|
|
308
|
-
data={
|
|
314
|
+
data={drivers}
|
|
309
315
|
isLoading={isLoading}
|
|
310
316
|
error={error}
|
|
311
317
|
onRetry={refetch}
|
|
312
318
|
searchable
|
|
313
319
|
searchPlaceholder="Search drivers..."
|
|
314
320
|
searchFields={['fullName', 'employeeId'] as (keyof Driver)[]}
|
|
321
|
+
searchQuery={search}
|
|
322
|
+
onSearchQueryChange={(query) => {
|
|
323
|
+
setSearch(query);
|
|
324
|
+
setPage(0);
|
|
325
|
+
}}
|
|
315
326
|
onRowClick={(row) => goTo(`fleet/drivers/${row.id}`)}
|
|
316
327
|
getRowId={(row) => row.id}
|
|
317
328
|
pageIndex={page}
|
|
@@ -321,7 +332,8 @@ export default function DriversPage() {
|
|
|
321
332
|
setPageSize(size);
|
|
322
333
|
setPage(0);
|
|
323
334
|
}}
|
|
324
|
-
totalItems={
|
|
335
|
+
totalItems={filteredDriverCount}
|
|
336
|
+
hasNextPage={hasMore}
|
|
325
337
|
emptyIllustration="empty-people"
|
|
326
338
|
emptyTitle={filtersActive ? 'No drivers match these filters' : 'No drivers yet'}
|
|
327
339
|
emptyDescription={
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Inventory Page - Overview of inventory management
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { useMemo } from 'react';
|
|
5
|
+
import { useMemo, useState } from 'react';
|
|
6
6
|
import {
|
|
7
7
|
Package,
|
|
8
8
|
Warehouse,
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
type InventoryTransactionType,
|
|
39
39
|
} from '../hooks/useInventory';
|
|
40
40
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
41
|
+
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
41
42
|
import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';
|
|
42
43
|
import { formatCurrency, formatDate } from '../utils/formatters';
|
|
43
44
|
import { joinPath } from '../utils/navigation';
|
|
@@ -90,7 +91,25 @@ export default function InventoryPage() {
|
|
|
90
91
|
return value === key ? fallback : value;
|
|
91
92
|
};
|
|
92
93
|
const { basePath, navigate } = useMoveTheWheels();
|
|
93
|
-
const
|
|
94
|
+
const [search, setSearch] = useState('');
|
|
95
|
+
const [pageIndex, setPageIndex] = useState(0);
|
|
96
|
+
const [pageSize, setPageSize] = useState(10);
|
|
97
|
+
const debouncedSearch = useDebouncedValue(search);
|
|
98
|
+
const {
|
|
99
|
+
inventory,
|
|
100
|
+
lowStockItems,
|
|
101
|
+
products,
|
|
102
|
+
warehouses,
|
|
103
|
+
totalItems,
|
|
104
|
+
hasMore,
|
|
105
|
+
isLoading,
|
|
106
|
+
error,
|
|
107
|
+
refetch,
|
|
108
|
+
} = useInventory({
|
|
109
|
+
search: debouncedSearch,
|
|
110
|
+
limit: pageSize,
|
|
111
|
+
offset: pageIndex * pageSize,
|
|
112
|
+
});
|
|
94
113
|
const { stats } = useInventoryStats();
|
|
95
114
|
const {
|
|
96
115
|
transactions,
|
|
@@ -105,37 +124,30 @@ export default function InventoryPage() {
|
|
|
105
124
|
action();
|
|
106
125
|
}
|
|
107
126
|
};
|
|
108
|
-
const lowStockItems = inventory.filter((inv) => inv.availableQuantity <= inv.reorderLevel);
|
|
109
|
-
|
|
110
127
|
const unknownProductLabel = t(
|
|
111
128
|
'movethewheels.inventory.lowStock.unknownProduct',
|
|
112
129
|
'Unknown Product'
|
|
113
130
|
);
|
|
114
131
|
|
|
115
132
|
const stockRows = useMemo<StockRow[]>(() => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
? 'low'
|
|
135
|
-
: 'in_stock',
|
|
136
|
-
};
|
|
137
|
-
});
|
|
138
|
-
}, [inventory, products, warehouses, unknownProductLabel]);
|
|
133
|
+
return inventory.map((item) => ({
|
|
134
|
+
id: item.id,
|
|
135
|
+
productId: item.productId,
|
|
136
|
+
productName: item.productName ?? unknownProductLabel,
|
|
137
|
+
sku: item.productSku ?? '—',
|
|
138
|
+
warehouseName: item.warehouseName ?? '—',
|
|
139
|
+
onHand: item.quantity,
|
|
140
|
+
allocated: item.reservedQuantity,
|
|
141
|
+
available: item.availableQuantity,
|
|
142
|
+
reorderLevel: item.reorderLevel,
|
|
143
|
+
stockState:
|
|
144
|
+
item.quantity === 0
|
|
145
|
+
? 'out_of_stock'
|
|
146
|
+
: item.availableQuantity <= item.reorderLevel
|
|
147
|
+
? 'low'
|
|
148
|
+
: 'in_stock',
|
|
149
|
+
}));
|
|
150
|
+
}, [inventory, unknownProductLabel]);
|
|
139
151
|
|
|
140
152
|
const stockColumns: Column<StockRow>[] = [
|
|
141
153
|
{
|
|
@@ -309,13 +321,12 @@ export default function InventoryPage() {
|
|
|
309
321
|
<EmphasisPanel className="space-y-4">
|
|
310
322
|
<h2 className="flex items-center gap-2 text-base font-semibold text-status-warning-text">
|
|
311
323
|
<AlertTriangle size={18} />
|
|
312
|
-
{t('movethewheels.inventory.lowStock.title', 'Low Stock Alert')} ({
|
|
324
|
+
{t('movethewheels.inventory.lowStock.title', 'Low Stock Alert')} ({stats.lowStockCount}{' '}
|
|
313
325
|
{t('movethewheels.inventory.lowStock.items', 'items')})
|
|
314
326
|
</h2>
|
|
315
327
|
<div>
|
|
316
328
|
<div className="space-y-3">
|
|
317
|
-
{lowStockItems.
|
|
318
|
-
const product = products.find((p) => p.id === item.productId);
|
|
329
|
+
{lowStockItems.map((item) => {
|
|
319
330
|
return (
|
|
320
331
|
<div
|
|
321
332
|
key={item.id}
|
|
@@ -323,11 +334,11 @@ export default function InventoryPage() {
|
|
|
323
334
|
>
|
|
324
335
|
<div>
|
|
325
336
|
<p className="font-medium">
|
|
326
|
-
{
|
|
337
|
+
{item.productName ??
|
|
327
338
|
t('movethewheels.inventory.lowStock.unknownProduct', 'Unknown Product')}
|
|
328
339
|
</p>
|
|
329
340
|
<p className="text-sm text-text-secondary">
|
|
330
|
-
{t('movethewheels.inventory.lowStock.sku', 'SKU:')} {
|
|
341
|
+
{t('movethewheels.inventory.lowStock.sku', 'SKU:')} {item.productSku ?? '—'}
|
|
331
342
|
</p>
|
|
332
343
|
</div>
|
|
333
344
|
<div className="text-right">
|
|
@@ -365,6 +376,20 @@ export default function InventoryPage() {
|
|
|
365
376
|
'Search product, SKU or warehouse...'
|
|
366
377
|
)}
|
|
367
378
|
searchFields={['productName', 'sku', 'warehouseName'] as (keyof StockRow)[]}
|
|
379
|
+
searchQuery={search}
|
|
380
|
+
onSearchQueryChange={(query) => {
|
|
381
|
+
setSearch(query);
|
|
382
|
+
setPageIndex(0);
|
|
383
|
+
}}
|
|
384
|
+
pageIndex={pageIndex}
|
|
385
|
+
pageSize={pageSize}
|
|
386
|
+
onPageChange={setPageIndex}
|
|
387
|
+
onPageSizeChange={(nextPageSize) => {
|
|
388
|
+
setPageSize(nextPageSize);
|
|
389
|
+
setPageIndex(0);
|
|
390
|
+
}}
|
|
391
|
+
totalItems={totalItems}
|
|
392
|
+
hasNextPage={hasMore}
|
|
368
393
|
onRowClick={(row) => goTo(`inventory/products/${row.productId}`)}
|
|
369
394
|
getRowId={(row) => row.id}
|
|
370
395
|
emptyIllustration="empty-data"
|