@burdenoff/microfe-movethewheels 2026.929.4 → 2026.930.2
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/CustomerPicker.js +52 -48
- package/dist/components/EntityCombobox.js +25 -23
- package/dist/components/OrderItemRow.js +123 -78
- package/dist/hooks/useCustomers.js +52 -17
- package/dist/hooks/useFleet.js +126 -44
- package/dist/hooks/useInventory.js +43 -7
- package/dist/hooks/useMultiModalShipments.js +30 -26
- package/dist/hooks/useRefetchOnLeaveEdit.js +10 -0
- package/dist/hooks/useRoutes.js +55 -15
- package/dist/movethewheels/components/CustomerPicker.d.ts +5 -1
- package/dist/movethewheels/components/EntityCombobox.d.ts +6 -3
- package/dist/movethewheels/components/OrderItemRow.d.ts +7 -1
- package/dist/movethewheels/hooks/useCustomers.d.ts +3 -0
- package/dist/movethewheels/hooks/useFleet.d.ts +6 -0
- package/dist/movethewheels/hooks/useInventory.d.ts +2 -0
- package/dist/movethewheels/hooks/useMultiModalShipments.d.ts +1 -0
- package/dist/movethewheels/hooks/useRefetchOnLeaveEdit.d.ts +7 -0
- package/dist/movethewheels/hooks/useRoutes.d.ts +3 -0
- package/dist/movethewheels/utils/orderValidation.d.ts +11 -0
- package/dist/pages/CreateOrderPage.js +349 -297
- package/dist/pages/DriverDetailsPage.js +258 -251
- package/dist/pages/DriversPage.js +3 -1
- package/dist/pages/MultiModalShipmentPage.js +139 -125
- package/dist/pages/OrderDetailsPage.js +132 -140
- package/dist/pages/ProductDetailsPage.js +22 -19
- package/dist/pages/RouteDetailsPage.js +284 -281
- package/dist/pages/TrackingPage.js +2 -1
- package/dist/pages/VehicleDetailsPage.js +28 -25
- package/dist/utils/orderValidation.js +18 -0
- package/package.json +1 -1
- package/src/movethewheels/components/CustomerPicker.tsx +12 -0
- package/src/movethewheels/components/EntityCombobox.tsx +30 -11
- package/src/movethewheels/components/OrderItemRow.tsx +44 -0
- package/src/movethewheels/hooks/useCustomers.ts +38 -0
- package/src/movethewheels/hooks/useFleet.ts +125 -5
- package/src/movethewheels/hooks/useInventory.ts +42 -0
- package/src/movethewheels/hooks/useMultiModalShipments.ts +9 -7
- package/src/movethewheels/hooks/useRefetchOnLeaveEdit.ts +18 -0
- package/src/movethewheels/hooks/useRoutes.ts +51 -0
- package/src/movethewheels/pages/CreateOrderPage.tsx +66 -2
- package/src/movethewheels/pages/DriverDetailsPage.tsx +14 -1
- package/src/movethewheels/pages/DriversPage.tsx +7 -1
- package/src/movethewheels/pages/MultiModalShipmentPage.tsx +15 -1
- package/src/movethewheels/pages/OrderDetailsPage.tsx +25 -25
- package/src/movethewheels/pages/ProductDetailsPage.tsx +2 -0
- package/src/movethewheels/pages/RouteDetailsPage.tsx +10 -4
- package/src/movethewheels/pages/TrackingPage.tsx +7 -2
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +2 -0
- package/src/movethewheels/utils/orderValidation.ts +53 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Displays detailed information about a specific order.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { useState } from 'react';
|
|
8
8
|
import { useParams } from 'react-router';
|
|
9
9
|
import {
|
|
10
10
|
Package,
|
|
@@ -105,15 +105,27 @@ export default function OrderDetailsPage() {
|
|
|
105
105
|
const [advancing, setAdvancing] = useState(false);
|
|
106
106
|
const [actionError, setActionError] = useState<string | null>(null);
|
|
107
107
|
const debouncedFleetSearch = useDebouncedValue(fleetSearch);
|
|
108
|
-
const {
|
|
108
|
+
const {
|
|
109
|
+
vehicles,
|
|
110
|
+
isLoading: vehiclesLoading,
|
|
111
|
+
isLoadingMore: vehiclesLoadingMore,
|
|
112
|
+
hasMore: vehiclesHaveMore,
|
|
113
|
+
totalVehicles,
|
|
114
|
+
loadMore: loadMoreVehicles,
|
|
115
|
+
} = useVehicles({
|
|
109
116
|
limit: 20,
|
|
110
117
|
search: debouncedFleetSearch || undefined,
|
|
111
|
-
driverId: selectedDriver || undefined,
|
|
112
118
|
});
|
|
113
|
-
const {
|
|
119
|
+
const {
|
|
120
|
+
drivers,
|
|
121
|
+
isLoading: driversLoading,
|
|
122
|
+
isLoadingMore: driversLoadingMore,
|
|
123
|
+
hasMore: driversHaveMore,
|
|
124
|
+
totalDrivers,
|
|
125
|
+
loadMore: loadMoreDrivers,
|
|
126
|
+
} = useDrivers({
|
|
114
127
|
limit: 20,
|
|
115
128
|
search: debouncedFleetSearch || undefined,
|
|
116
|
-
vehicleId: selectedVehicle || undefined,
|
|
117
129
|
});
|
|
118
130
|
// Resolved independently of the (searched, 20-row) lists above so the
|
|
119
131
|
// trigger still shows the right label once a pick scrolls out of the
|
|
@@ -132,26 +144,6 @@ export default function OrderDetailsPage() {
|
|
|
132
144
|
(driver) => driver.status === 'available' || driver.id === order?.driverId
|
|
133
145
|
);
|
|
134
146
|
|
|
135
|
-
useEffect(() => {
|
|
136
|
-
if (
|
|
137
|
-
!driversLoading &&
|
|
138
|
-
selectedDriver &&
|
|
139
|
-
!drivers.some((driver) => driver.id === selectedDriver)
|
|
140
|
-
) {
|
|
141
|
-
setSelectedDriver('');
|
|
142
|
-
}
|
|
143
|
-
}, [drivers, driversLoading, selectedDriver]);
|
|
144
|
-
|
|
145
|
-
useEffect(() => {
|
|
146
|
-
if (
|
|
147
|
-
!vehiclesLoading &&
|
|
148
|
-
selectedVehicle &&
|
|
149
|
-
!vehicles.some((vehicle) => vehicle.id === selectedVehicle)
|
|
150
|
-
) {
|
|
151
|
-
setSelectedVehicle('');
|
|
152
|
-
}
|
|
153
|
-
}, [selectedVehicle, vehicles, vehiclesLoading]);
|
|
154
|
-
|
|
155
147
|
const handleAssign = async () => {
|
|
156
148
|
if (!orderId || !order) {
|
|
157
149
|
setActionError('This order is no longer available. Refresh and try again.');
|
|
@@ -607,6 +599,10 @@ export default function OrderDetailsPage() {
|
|
|
607
599
|
)}
|
|
608
600
|
selectedOption={selectedVehicleRecord}
|
|
609
601
|
selectedLoading={selectedVehicleLoading}
|
|
602
|
+
hasMore={vehiclesHaveMore}
|
|
603
|
+
totalCount={totalVehicles}
|
|
604
|
+
onLoadMore={loadMoreVehicles}
|
|
605
|
+
loadingMore={vehiclesLoadingMore}
|
|
610
606
|
required
|
|
611
607
|
placeholder={
|
|
612
608
|
order.vehicleId ? `Current: ${order.vehicleId}` : 'Select a vehicle'
|
|
@@ -634,6 +630,10 @@ export default function OrderDetailsPage() {
|
|
|
634
630
|
)}
|
|
635
631
|
selectedOption={selectedDriverRecord}
|
|
636
632
|
selectedLoading={selectedDriverLoading}
|
|
633
|
+
hasMore={driversHaveMore}
|
|
634
|
+
totalCount={totalDrivers}
|
|
635
|
+
onLoadMore={loadMoreDrivers}
|
|
636
|
+
loadingMore={driversLoadingMore}
|
|
637
637
|
required
|
|
638
638
|
placeholder={
|
|
639
639
|
order.driverId ? `Current: ${order.driverId}` : 'Select a driver'
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
ResponsiveDialogDescription,
|
|
43
43
|
} from '@burdenoff/fe-libs/ui';
|
|
44
44
|
import { useProduct, useInventory, useProductStockLevels } from '../hooks/useInventory';
|
|
45
|
+
import { useRefetchOnLeaveEdit } from '../hooks/useRefetchOnLeaveEdit';
|
|
45
46
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
46
47
|
import { formatCurrency } from '../utils/formatters';
|
|
47
48
|
import { joinPath } from '../utils/navigation';
|
|
@@ -326,6 +327,7 @@ export default function ProductDetailsPage() {
|
|
|
326
327
|
const { product, isLoading, error, refetch } = useProduct(productId);
|
|
327
328
|
const { deleteProduct } = useInventory();
|
|
328
329
|
const isEditRoute = useLocation().pathname.endsWith('/edit');
|
|
330
|
+
useRefetchOnLeaveEdit(isEditRoute, refetch);
|
|
329
331
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
330
332
|
const [deleting, setDeleting] = useState(false);
|
|
331
333
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
@@ -53,6 +53,7 @@ import { EntityCombobox } from '../components/EntityCombobox';
|
|
|
53
53
|
import { useRoute, useRoutes } from '../hooks/useRoutes';
|
|
54
54
|
import { useVehicles, useVehicle, useDrivers, useDriver } from '../hooks/useFleet';
|
|
55
55
|
import { useAssignmentSuggestions } from '../hooks/useFleetIntelligence';
|
|
56
|
+
import { useRefetchOnLeaveEdit } from '../hooks/useRefetchOnLeaveEdit';
|
|
56
57
|
import { AssignmentSuggestions } from '../components/AssignmentSuggestions';
|
|
57
58
|
import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';
|
|
58
59
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
@@ -95,20 +96,22 @@ function NewRouteForm({ editId }: { editId?: string }) {
|
|
|
95
96
|
isLoading: vehiclesLoading,
|
|
96
97
|
hasMore: vehiclesHaveMore,
|
|
97
98
|
totalVehicles,
|
|
99
|
+
loadMore: loadMoreVehicles,
|
|
100
|
+
isLoadingMore: vehiclesLoadingMore,
|
|
98
101
|
} = useVehicles({
|
|
99
102
|
limit: 20,
|
|
100
103
|
search: vehicleSearch || undefined,
|
|
101
|
-
driverId: form.driverId || undefined,
|
|
102
104
|
});
|
|
103
105
|
const {
|
|
104
106
|
drivers,
|
|
105
107
|
isLoading: driversLoading,
|
|
106
108
|
hasMore: driversHaveMore,
|
|
107
109
|
totalDrivers,
|
|
110
|
+
loadMore: loadMoreDrivers,
|
|
111
|
+
isLoadingMore: driversLoadingMore,
|
|
108
112
|
} = useDrivers({
|
|
109
113
|
limit: 20,
|
|
110
114
|
search: driverSearch || undefined,
|
|
111
|
-
vehicleId: form.vehicleId || undefined,
|
|
112
115
|
});
|
|
113
116
|
// The selected vehicle/driver may fall outside the current search page (e.g. when
|
|
114
117
|
// editing a route or after the assignee no longer matches the search term); fetch
|
|
@@ -314,7 +317,8 @@ function NewRouteForm({ editId }: { editId?: string }) {
|
|
|
314
317
|
searchPlaceholder="Search vehicle number or type…"
|
|
315
318
|
hasMore={vehiclesHaveMore}
|
|
316
319
|
totalCount={totalVehicles}
|
|
317
|
-
|
|
320
|
+
onLoadMore={loadMoreVehicles}
|
|
321
|
+
loadingMore={vehiclesLoadingMore}
|
|
318
322
|
placeholder="Assign later"
|
|
319
323
|
emptyText="No available vehicles."
|
|
320
324
|
/>
|
|
@@ -340,7 +344,8 @@ function NewRouteForm({ editId }: { editId?: string }) {
|
|
|
340
344
|
searchPlaceholder="Search driver name or employee ID…"
|
|
341
345
|
hasMore={driversHaveMore}
|
|
342
346
|
totalCount={totalDrivers}
|
|
343
|
-
|
|
347
|
+
onLoadMore={loadMoreDrivers}
|
|
348
|
+
loadingMore={driversLoadingMore}
|
|
344
349
|
placeholder="Assign later"
|
|
345
350
|
emptyText="No available drivers."
|
|
346
351
|
/>
|
|
@@ -432,6 +437,7 @@ export default function RouteDetailsPage() {
|
|
|
432
437
|
const isEditRoute = useLocation().pathname.endsWith('/edit');
|
|
433
438
|
const { basePath, navigate } = useMoveTheWheels();
|
|
434
439
|
const { route, isLoading, error, refetch } = useRoute(routeId);
|
|
440
|
+
useRefetchOnLeaveEdit(isEditRoute, refetch);
|
|
435
441
|
const { optimizeRoute, updateRoute, deleteRoute } = useRoutes();
|
|
436
442
|
const suggestions = useAssignmentSuggestions(routeId);
|
|
437
443
|
const { t: translate } = useI18n();
|
|
@@ -120,7 +120,7 @@ export default function TrackingPage() {
|
|
|
120
120
|
{/* Search Form */}
|
|
121
121
|
<Card>
|
|
122
122
|
<CardContent className="p-6">
|
|
123
|
-
<form onSubmit={handleSearch} className="flex gap-4">
|
|
123
|
+
<form onSubmit={handleSearch} className="flex flex-col gap-4 sm:flex-row">
|
|
124
124
|
<div className="flex-1 relative">
|
|
125
125
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-text-secondary" />
|
|
126
126
|
<Input
|
|
@@ -133,7 +133,12 @@ export default function TrackingPage() {
|
|
|
133
133
|
className="pl-10 h-12 text-lg"
|
|
134
134
|
/>
|
|
135
135
|
</div>
|
|
136
|
-
<Button
|
|
136
|
+
<Button
|
|
137
|
+
type="submit"
|
|
138
|
+
size="lg"
|
|
139
|
+
className="w-full sm:w-auto"
|
|
140
|
+
disabled={!trackingInput || isLoading}
|
|
141
|
+
>
|
|
137
142
|
{isLoading
|
|
138
143
|
? t('movethewheels.tracking.search.searching', 'Searching...')
|
|
139
144
|
: t('movethewheels.tracking.search.track', 'Track')}
|
|
@@ -50,6 +50,7 @@ import { useVehicle, useVehicleDrivers, useFleet, toApiVehicleStatus } from '../
|
|
|
50
50
|
import type { VehicleFuelType, VehicleStatus } from '../types';
|
|
51
51
|
import { VEHICLE_STATUS_LABELS } from '../constants';
|
|
52
52
|
import { useRecordVehicleService } from '../hooks/useFleetIntelligence';
|
|
53
|
+
import { useRefetchOnLeaveEdit } from '../hooks/useRefetchOnLeaveEdit';
|
|
53
54
|
import { VehicleServiceCard } from '../components/VehicleServiceCard';
|
|
54
55
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
55
56
|
import { formatDate, getInitials } from '../utils/formatters';
|
|
@@ -494,6 +495,7 @@ export default function VehicleDetailsPage() {
|
|
|
494
495
|
return value === key ? fallback : value;
|
|
495
496
|
};
|
|
496
497
|
const isEditRoute = useLocation().pathname.endsWith('/edit');
|
|
498
|
+
useRefetchOnLeaveEdit(isEditRoute, refetch);
|
|
497
499
|
// Who is behind the wheel — resolved through DriverFilterInput.vehicleId, the
|
|
498
500
|
// same link `assignDriverVehicle` writes.
|
|
499
501
|
const {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { OrderItem } from '../types';
|
|
2
|
+
|
|
3
|
+
export type OrderItemField =
|
|
4
|
+
'name' | 'sku' | 'quantity' | 'weight' | 'value' | 'length' | 'width' | 'height';
|
|
5
|
+
|
|
6
|
+
export type OrderItemFieldErrors = Partial<Record<OrderItemField, string>>;
|
|
7
|
+
|
|
8
|
+
const isFiniteNonNegative = (value: number): boolean => Number.isFinite(value) && value >= 0;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validates one delivery-job cargo line without imposing pricing or order
|
|
12
|
+
* semantics that belong to the service. A catalog product may supply the name;
|
|
13
|
+
* every line still carries its SKU because cargo-item records require one.
|
|
14
|
+
*/
|
|
15
|
+
export function validateOrderItem(item: OrderItem): OrderItemFieldErrors {
|
|
16
|
+
const errors: OrderItemFieldErrors = {};
|
|
17
|
+
const hasProduct = item.productId.trim().length > 0;
|
|
18
|
+
const hasName = item.name.trim().length > 0;
|
|
19
|
+
|
|
20
|
+
if (!hasProduct && !hasName) {
|
|
21
|
+
errors.name = 'Enter an item name or select a catalog product.';
|
|
22
|
+
}
|
|
23
|
+
if (item.sku.trim().length === 0) {
|
|
24
|
+
errors.sku = 'SKU is required for every shipment item.';
|
|
25
|
+
}
|
|
26
|
+
if (!Number.isFinite(item.quantity) || item.quantity <= 0) {
|
|
27
|
+
errors.quantity = 'Quantity must be a finite number greater than 0.';
|
|
28
|
+
}
|
|
29
|
+
if (!isFiniteNonNegative(item.weight)) {
|
|
30
|
+
errors.weight = 'Weight must be a finite number of 0 or more.';
|
|
31
|
+
}
|
|
32
|
+
if (!isFiniteNonNegative(item.value)) {
|
|
33
|
+
errors.value = 'Value must be a finite number of 0 or more.';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const dimensions = item.dimensions;
|
|
37
|
+
for (const field of ['length', 'width', 'height'] as const) {
|
|
38
|
+
if (!dimensions || !isFiniteNonNegative(dimensions[field])) {
|
|
39
|
+
errors[field] =
|
|
40
|
+
`${field[0].toUpperCase()}${field.slice(1)} must be a finite number of 0 or more.`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return errors;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function validateOrderItems(items: OrderItem[]): Record<string, OrderItemFieldErrors> {
|
|
48
|
+
return Object.fromEntries(
|
|
49
|
+
items
|
|
50
|
+
.map((item) => [item.id, validateOrderItem(item)] as const)
|
|
51
|
+
.filter(([, errors]) => Object.keys(errors).length > 0)
|
|
52
|
+
);
|
|
53
|
+
}
|