@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.
- package/dist/components/CarbonFootprintPanel.js +1 -1
- package/dist/components/CustomerPicker.js +76 -65
- package/dist/components/DataTable.js +140 -139
- package/dist/components/DocumentsCard.js +14 -8
- package/dist/generated/wspace-types.d.ts +2719 -8
- package/dist/hooks/index.js +1 -1
- 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 +148 -125
- package/dist/hooks/useInventory.js +255 -186
- package/dist/hooks/useMultiModalShipments.js +35 -27
- package/dist/hooks/useOrders.js +139 -134
- package/dist/hooks/useRoutes.js +97 -75
- package/dist/hooks/useTracking.js +100 -88
- package/dist/index.js +6 -6
- package/dist/movethewheels/components/CustomerPicker.d.ts +3 -1
- 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 +48 -5
- package/dist/movethewheels/hooks/useRoutes.d.ts +18 -1
- package/dist/movethewheels/hooks/useTracking.d.ts +12 -11
- package/dist/movethewheels/types/index.d.ts +12 -5
- package/dist/pages/CreateOrderPage.js +191 -129
- 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 +190 -106
- 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 +324 -293
- package/dist/pages/RoutesPage.js +55 -48
- package/dist/pages/VehiclesPage.js +63 -56
- package/dist/pages/WarehousesPage.js +60 -53
- package/package.json +1 -1
- package/src/generated/wspace-types.ts +3136 -8
- package/src/movethewheels/components/CarbonFootprintPanel.tsx +0 -1
- package/src/movethewheels/components/CustomerPicker.tsx +14 -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 +66 -38
- 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 +48 -14
- package/src/movethewheels/hooks/useRoutes.ts +81 -24
- package/src/movethewheels/hooks/useTracking.ts +130 -98
- package/src/movethewheels/pages/CreateOrderPage.tsx +93 -2
- 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 +91 -6
- 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 +45 -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 +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 {
|
|
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({
|
|
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
|
|
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"
|
|
@@ -7,7 +7,17 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useState } from 'react';
|
|
10
|
-
import {
|
|
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
|
|
73
|
-
|
|
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>
|