@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.
Files changed (49) hide show
  1. package/dist/components/CustomerPicker.js +52 -48
  2. package/dist/components/EntityCombobox.js +25 -23
  3. package/dist/components/OrderItemRow.js +123 -78
  4. package/dist/hooks/useCustomers.js +52 -17
  5. package/dist/hooks/useFleet.js +126 -44
  6. package/dist/hooks/useInventory.js +43 -7
  7. package/dist/hooks/useMultiModalShipments.js +30 -26
  8. package/dist/hooks/useRefetchOnLeaveEdit.js +10 -0
  9. package/dist/hooks/useRoutes.js +55 -15
  10. package/dist/movethewheels/components/CustomerPicker.d.ts +5 -1
  11. package/dist/movethewheels/components/EntityCombobox.d.ts +6 -3
  12. package/dist/movethewheels/components/OrderItemRow.d.ts +7 -1
  13. package/dist/movethewheels/hooks/useCustomers.d.ts +3 -0
  14. package/dist/movethewheels/hooks/useFleet.d.ts +6 -0
  15. package/dist/movethewheels/hooks/useInventory.d.ts +2 -0
  16. package/dist/movethewheels/hooks/useMultiModalShipments.d.ts +1 -0
  17. package/dist/movethewheels/hooks/useRefetchOnLeaveEdit.d.ts +7 -0
  18. package/dist/movethewheels/hooks/useRoutes.d.ts +3 -0
  19. package/dist/movethewheels/utils/orderValidation.d.ts +11 -0
  20. package/dist/pages/CreateOrderPage.js +349 -297
  21. package/dist/pages/DriverDetailsPage.js +258 -251
  22. package/dist/pages/DriversPage.js +3 -1
  23. package/dist/pages/MultiModalShipmentPage.js +139 -125
  24. package/dist/pages/OrderDetailsPage.js +132 -140
  25. package/dist/pages/ProductDetailsPage.js +22 -19
  26. package/dist/pages/RouteDetailsPage.js +284 -281
  27. package/dist/pages/TrackingPage.js +2 -1
  28. package/dist/pages/VehicleDetailsPage.js +28 -25
  29. package/dist/utils/orderValidation.js +18 -0
  30. package/package.json +1 -1
  31. package/src/movethewheels/components/CustomerPicker.tsx +12 -0
  32. package/src/movethewheels/components/EntityCombobox.tsx +30 -11
  33. package/src/movethewheels/components/OrderItemRow.tsx +44 -0
  34. package/src/movethewheels/hooks/useCustomers.ts +38 -0
  35. package/src/movethewheels/hooks/useFleet.ts +125 -5
  36. package/src/movethewheels/hooks/useInventory.ts +42 -0
  37. package/src/movethewheels/hooks/useMultiModalShipments.ts +9 -7
  38. package/src/movethewheels/hooks/useRefetchOnLeaveEdit.ts +18 -0
  39. package/src/movethewheels/hooks/useRoutes.ts +51 -0
  40. package/src/movethewheels/pages/CreateOrderPage.tsx +66 -2
  41. package/src/movethewheels/pages/DriverDetailsPage.tsx +14 -1
  42. package/src/movethewheels/pages/DriversPage.tsx +7 -1
  43. package/src/movethewheels/pages/MultiModalShipmentPage.tsx +15 -1
  44. package/src/movethewheels/pages/OrderDetailsPage.tsx +25 -25
  45. package/src/movethewheels/pages/ProductDetailsPage.tsx +2 -0
  46. package/src/movethewheels/pages/RouteDetailsPage.tsx +10 -4
  47. package/src/movethewheels/pages/TrackingPage.tsx +7 -2
  48. package/src/movethewheels/pages/VehicleDetailsPage.tsx +2 -0
  49. 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 { useEffect, useState } from 'react';
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 { vehicles, isLoading: vehiclesLoading } = useVehicles({
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 { drivers, isLoading: driversLoading } = useDrivers({
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
- required
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
- required
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 type="submit" size="lg" disabled={!trackingInput || isLoading}>
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
+ }