@burdenoff/microfe-movethewheels 2026.929.3 → 2026.930.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/CustomerPicker.js +52 -48
- package/dist/components/EntityCombobox.js +25 -23
- package/dist/components/OrderItemRow.js +123 -78
- package/dist/generated/wspace-types.d.ts +1547 -64
- package/dist/hooks/useCustomers.js +55 -26
- package/dist/hooks/useFleet.js +236 -157
- package/dist/hooks/useInventory.js +214 -153
- package/dist/hooks/useMultiModalShipments.js +30 -26
- package/dist/hooks/useRoutes.js +64 -24
- package/dist/hooks/useTracking.js +14 -16
- 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 +15 -2
- package/dist/movethewheels/hooks/useInventory.d.ts +15 -12
- package/dist/movethewheels/hooks/useMultiModalShipments.d.ts +1 -0
- package/dist/movethewheels/hooks/useRoutes.d.ts +7 -1
- package/dist/movethewheels/types/index.d.ts +33 -23
- package/dist/movethewheels/utils/formatters.d.ts +1 -1
- package/dist/movethewheels/utils/orderValidation.d.ts +11 -0
- package/dist/pages/CreateOrderPage.js +349 -297
- package/dist/pages/CustomerDetailsPage.js +6 -6
- package/dist/pages/CustomersPage.js +2 -2
- package/dist/pages/DashboardPage.js +1 -1
- package/dist/pages/DriverDetailsPage.js +340 -332
- package/dist/pages/DriversPage.js +7 -5
- package/dist/pages/FleetPage.js +1 -1
- package/dist/pages/InventoryPage.js +11 -12
- package/dist/pages/LiveTrackingPage.js +4 -4
- package/dist/pages/MultiModalShipmentPage.js +139 -125
- package/dist/pages/OrderDetailsPage.js +132 -140
- package/dist/pages/ProductDetailsPage.js +408 -249
- package/dist/pages/ProductsPage.js +5 -5
- package/dist/pages/RouteDetailsPage.js +208 -210
- package/dist/pages/RoutesPage.js +2 -2
- package/dist/pages/TrackingPage.js +16 -9
- package/dist/pages/WarehouseDetailsPage.js +166 -163
- package/dist/pages/WarehousesPage.js +29 -21
- package/dist/utils/orderValidation.js +18 -0
- package/package.json +1 -1
- package/src/generated/wspace-types.ts +1762 -64
- 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 +45 -9
- package/src/movethewheels/hooks/useFleet.ts +214 -36
- package/src/movethewheels/hooks/useInventory.ts +185 -49
- package/src/movethewheels/hooks/useMultiModalShipments.ts +9 -7
- package/src/movethewheels/hooks/useRoutes.ts +87 -16
- package/src/movethewheels/hooks/useTracking.ts +23 -15
- package/src/movethewheels/pages/CreateOrderPage.tsx +66 -2
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +14 -8
- package/src/movethewheels/pages/CustomersPage.tsx +5 -2
- package/src/movethewheels/pages/DashboardPage.tsx +3 -1
- package/src/movethewheels/pages/DriverDetailsPage.tsx +25 -4
- package/src/movethewheels/pages/DriversPage.tsx +8 -2
- package/src/movethewheels/pages/FleetPage.tsx +1 -1
- package/src/movethewheels/pages/InventoryPage.tsx +20 -16
- package/src/movethewheels/pages/LiveTrackingPage.tsx +6 -4
- package/src/movethewheels/pages/MultiModalShipmentPage.tsx +15 -1
- package/src/movethewheels/pages/OrderDetailsPage.tsx +25 -25
- package/src/movethewheels/pages/ProductDetailsPage.tsx +228 -49
- package/src/movethewheels/pages/ProductsPage.tsx +13 -5
- package/src/movethewheels/pages/RouteDetailsPage.tsx +24 -12
- package/src/movethewheels/pages/RoutesPage.tsx +4 -2
- package/src/movethewheels/pages/TrackingPage.tsx +32 -12
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +42 -29
- package/src/movethewheels/pages/WarehousesPage.tsx +23 -15
- package/src/movethewheels/types/index.ts +36 -24
- package/src/movethewheels/utils/formatters.ts +1 -1
- package/src/movethewheels/utils/orderValidation.ts +53 -0
|
@@ -48,6 +48,7 @@ import { CustomerPicker } from '../components/CustomerPicker';
|
|
|
48
48
|
import { EntityCombobox } from '../components/EntityCombobox';
|
|
49
49
|
import { OrderItemRow } from '../components/OrderItemRow';
|
|
50
50
|
import type { Customer } from '../types';
|
|
51
|
+
import { validateOrderItems, type OrderItemFieldErrors } from '../utils/orderValidation';
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* `<input type="datetime-local">` needs a `YYYY-MM-DDTHH:mm` string in LOCAL
|
|
@@ -77,8 +78,12 @@ export default function CreateOrderPage() {
|
|
|
77
78
|
customers,
|
|
78
79
|
createCustomer,
|
|
79
80
|
isLoading: customersLoading,
|
|
81
|
+
isLoadingMore: customersLoadingMore,
|
|
80
82
|
error: customersError,
|
|
81
83
|
refetch: refetchCustomers,
|
|
84
|
+
loadMore: loadMoreCustomers,
|
|
85
|
+
hasMore: customersHaveMore,
|
|
86
|
+
totalCustomers,
|
|
82
87
|
} = useCustomers({
|
|
83
88
|
limit: 20,
|
|
84
89
|
filters: { search: customerSearch || undefined },
|
|
@@ -91,13 +96,22 @@ export default function CreateOrderPage() {
|
|
|
91
96
|
error: routesError,
|
|
92
97
|
hasMore: routesHaveMore,
|
|
93
98
|
totalRoutes,
|
|
99
|
+
loadMore: loadMoreRoutes,
|
|
100
|
+
isLoadingMore: routesLoadingMore,
|
|
94
101
|
} = useRoutes({
|
|
95
102
|
status: 'planned',
|
|
96
103
|
search: routeSearch || undefined,
|
|
97
104
|
limit: 20,
|
|
98
105
|
});
|
|
99
106
|
const [productSearch, setProductSearch] = useState('');
|
|
100
|
-
const {
|
|
107
|
+
const {
|
|
108
|
+
products,
|
|
109
|
+
isLoading: productsLoading,
|
|
110
|
+
isLoadingMore: productsLoadingMore,
|
|
111
|
+
hasMore: productsHaveMore,
|
|
112
|
+
totalProducts,
|
|
113
|
+
loadMore: loadMoreProducts,
|
|
114
|
+
} = useProducts({
|
|
101
115
|
search: productSearch || undefined,
|
|
102
116
|
limit: 20,
|
|
103
117
|
});
|
|
@@ -124,6 +138,7 @@ export default function CreateOrderPage() {
|
|
|
124
138
|
country: order?.deliveryAddress.country || 'USA',
|
|
125
139
|
},
|
|
126
140
|
items: order?.items || [],
|
|
141
|
+
trackingNumber: order?.trackingNumber || '',
|
|
127
142
|
scheduledDelivery: toDateTimeLocalValue(order?.scheduledDelivery),
|
|
128
143
|
specialInstructions: order?.specialInstructions || '',
|
|
129
144
|
});
|
|
@@ -137,6 +152,7 @@ export default function CreateOrderPage() {
|
|
|
137
152
|
|
|
138
153
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
139
154
|
const [formError, setFormError] = useState<string | null>(null);
|
|
155
|
+
const [itemErrors, setItemErrors] = useState<Record<string, OrderItemFieldErrors>>({});
|
|
140
156
|
|
|
141
157
|
useEffect(() => {
|
|
142
158
|
if (!linkedCustomer) {
|
|
@@ -176,6 +192,7 @@ export default function CreateOrderPage() {
|
|
|
176
192
|
country: order.deliveryAddress.country || 'USA',
|
|
177
193
|
},
|
|
178
194
|
items: order.items || [],
|
|
195
|
+
trackingNumber: order.trackingNumber || '',
|
|
179
196
|
scheduledDelivery: toDateTimeLocalValue(order.scheduledDelivery),
|
|
180
197
|
specialInstructions: order.specialInstructions || '',
|
|
181
198
|
});
|
|
@@ -210,6 +227,11 @@ export default function CreateOrderPage() {
|
|
|
210
227
|
if (formData.items.length === 0) {
|
|
211
228
|
return 'Add at least one item to the order.';
|
|
212
229
|
}
|
|
230
|
+
const nextItemErrors = validateOrderItems(formData.items);
|
|
231
|
+
setItemErrors(nextItemErrors);
|
|
232
|
+
if (Object.keys(nextItemErrors).length > 0) {
|
|
233
|
+
return 'Fix the highlighted shipment item fields before continuing.';
|
|
234
|
+
}
|
|
213
235
|
if (formData.scheduledDelivery && Number.isNaN(Date.parse(formData.scheduledDelivery))) {
|
|
214
236
|
return 'Enter a valid scheduled delivery date and time.';
|
|
215
237
|
}
|
|
@@ -251,6 +273,7 @@ export default function CreateOrderPage() {
|
|
|
251
273
|
deliveryAddress: formData.deliveryAddress,
|
|
252
274
|
items: formData.items,
|
|
253
275
|
// `null` clears an existing slot on edit; the service field is nullable.
|
|
276
|
+
trackingNumber: formData.trackingNumber.trim() || undefined,
|
|
254
277
|
scheduledDelivery: formData.scheduledDelivery ? new Date(formData.scheduledDelivery) : null,
|
|
255
278
|
routeId: formData.routeId || undefined,
|
|
256
279
|
specialInstructions: formData.specialInstructions,
|
|
@@ -346,6 +369,14 @@ export default function CreateOrderPage() {
|
|
|
346
369
|
};
|
|
347
370
|
|
|
348
371
|
const removeItem = (index: number) => {
|
|
372
|
+
const itemId = formData.items[index]?.id;
|
|
373
|
+
if (itemId) {
|
|
374
|
+
setItemErrors((previous) => {
|
|
375
|
+
const next = { ...previous };
|
|
376
|
+
delete next[itemId];
|
|
377
|
+
return next;
|
|
378
|
+
});
|
|
379
|
+
}
|
|
349
380
|
setFormData((prev) => ({
|
|
350
381
|
...prev,
|
|
351
382
|
items: prev.items.filter((_, i) => i !== index),
|
|
@@ -353,6 +384,14 @@ export default function CreateOrderPage() {
|
|
|
353
384
|
};
|
|
354
385
|
|
|
355
386
|
const updateItem = (index: number, field: string, value: unknown) => {
|
|
387
|
+
const itemId = formData.items[index]?.id;
|
|
388
|
+
if (itemId && itemErrors[itemId]) {
|
|
389
|
+
setItemErrors((previous) => {
|
|
390
|
+
const next = { ...previous };
|
|
391
|
+
delete next[itemId];
|
|
392
|
+
return next;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
356
395
|
setFormData((prev) => ({
|
|
357
396
|
...prev,
|
|
358
397
|
items: prev.items.map((item, i) => (i === index ? { ...item, [field]: value } : item)),
|
|
@@ -489,6 +528,10 @@ export default function CreateOrderPage() {
|
|
|
489
528
|
onOpenCustomers={goToCustomerCreate}
|
|
490
529
|
selectedCustomer={selectedCustomer}
|
|
491
530
|
onSearchQueryChange={setCustomerSearch}
|
|
531
|
+
hasMore={customersHaveMore}
|
|
532
|
+
totalCount={totalCustomers}
|
|
533
|
+
onLoadMore={loadMoreCustomers}
|
|
534
|
+
isLoadingMore={customersLoadingMore}
|
|
492
535
|
/>
|
|
493
536
|
<div className="space-y-2">
|
|
494
537
|
<Label htmlFor="priority">Priority</Label>
|
|
@@ -555,6 +598,8 @@ export default function CreateOrderPage() {
|
|
|
555
598
|
onSearchChange={setRouteSearch}
|
|
556
599
|
hasMore={routesHaveMore}
|
|
557
600
|
totalCount={totalRoutes}
|
|
601
|
+
onLoadMore={loadMoreRoutes}
|
|
602
|
+
loadingMore={routesLoadingMore}
|
|
558
603
|
/>
|
|
559
604
|
<p className="mt-2 text-xs text-text-secondary" role="status">
|
|
560
605
|
{routesLoading
|
|
@@ -782,6 +827,11 @@ export default function CreateOrderPage() {
|
|
|
782
827
|
index={index}
|
|
783
828
|
products={products}
|
|
784
829
|
productsLoading={productsLoading}
|
|
830
|
+
productsLoadingMore={productsLoadingMore}
|
|
831
|
+
productsHasMore={productsHaveMore}
|
|
832
|
+
productsTotalCount={totalProducts}
|
|
833
|
+
onLoadMoreProducts={loadMoreProducts}
|
|
834
|
+
errors={itemErrors[item.id]}
|
|
785
835
|
onUpdate={updateItem}
|
|
786
836
|
onRemove={removeItem}
|
|
787
837
|
/>
|
|
@@ -803,6 +853,20 @@ export default function CreateOrderPage() {
|
|
|
803
853
|
description="Optional delivery timing and instructions for the carrier."
|
|
804
854
|
>
|
|
805
855
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
856
|
+
<div className="space-y-2">
|
|
857
|
+
<Label htmlFor="trackingNumber">Tracking / reference number</Label>
|
|
858
|
+
<Input
|
|
859
|
+
id="trackingNumber"
|
|
860
|
+
value={formData.trackingNumber}
|
|
861
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
862
|
+
updateField('trackingNumber', e.target.value)
|
|
863
|
+
}
|
|
864
|
+
placeholder="Optional carrier reference"
|
|
865
|
+
/>
|
|
866
|
+
<p className="text-xs text-text-secondary">
|
|
867
|
+
Use this when a carrier or customer already supplied a tracking reference.
|
|
868
|
+
</p>
|
|
869
|
+
</div>
|
|
806
870
|
<div className="space-y-2">
|
|
807
871
|
<Label htmlFor="scheduledDelivery">Scheduled delivery</Label>
|
|
808
872
|
<Input
|
|
@@ -817,7 +881,7 @@ export default function CreateOrderPage() {
|
|
|
817
881
|
Leave blank to dispatch as soon as a driver is available.
|
|
818
882
|
</p>
|
|
819
883
|
</div>
|
|
820
|
-
<div className="space-y-2 lg:col-span-
|
|
884
|
+
<div className="space-y-2 lg:col-span-1">
|
|
821
885
|
<Label htmlFor="specialInstructions">Special instructions</Label>
|
|
822
886
|
<Textarea
|
|
823
887
|
id="specialInstructions"
|
|
@@ -333,10 +333,16 @@ function customerToForm(customer: {
|
|
|
333
333
|
phone: string;
|
|
334
334
|
company?: string;
|
|
335
335
|
type: 'b2b' | 'b2c';
|
|
336
|
-
address: {
|
|
336
|
+
address: Partial<{
|
|
337
|
+
street: string;
|
|
338
|
+
city: string;
|
|
339
|
+
state: string;
|
|
340
|
+
postalCode: string;
|
|
341
|
+
country: string;
|
|
342
|
+
}>;
|
|
337
343
|
businessLicense?: string;
|
|
338
344
|
taxId?: string;
|
|
339
|
-
rating
|
|
345
|
+
rating?: number;
|
|
340
346
|
}): CustomerFormState {
|
|
341
347
|
return {
|
|
342
348
|
name: customer.name,
|
|
@@ -344,11 +350,11 @@ function customerToForm(customer: {
|
|
|
344
350
|
phone: customer.phone,
|
|
345
351
|
company: customer.company ?? '',
|
|
346
352
|
type: customer.type,
|
|
347
|
-
street: customer.address.street,
|
|
348
|
-
city: customer.address.city,
|
|
349
|
-
state: customer.address.state,
|
|
350
|
-
postalCode: customer.address.postalCode,
|
|
351
|
-
country: customer.address.country,
|
|
353
|
+
street: customer.address.street ?? '',
|
|
354
|
+
city: customer.address.city ?? '',
|
|
355
|
+
state: customer.address.state ?? '',
|
|
356
|
+
postalCode: customer.address.postalCode ?? '',
|
|
357
|
+
country: customer.address.country ?? '',
|
|
352
358
|
businessLicense: customer.businessLicense ?? '',
|
|
353
359
|
taxId: customer.taxId ?? '',
|
|
354
360
|
rating: customer.rating ? String(customer.rating) : '',
|
|
@@ -480,7 +486,7 @@ export default function CustomerDetailsPage() {
|
|
|
480
486
|
headerContent={
|
|
481
487
|
<div className="flex items-center gap-4">
|
|
482
488
|
<TypeBadge type={customer.type} />
|
|
483
|
-
{customer.rating
|
|
489
|
+
{customer.rating !== undefined && (
|
|
484
490
|
<div className="flex items-center gap-1">
|
|
485
491
|
<Star size={16} className="text-status-warning-text fill-current" />
|
|
486
492
|
<span className="font-medium">{customer.rating.toFixed(1)}</span>
|
|
@@ -65,7 +65,10 @@ export default function CustomersPage() {
|
|
|
65
65
|
{
|
|
66
66
|
id: 'location',
|
|
67
67
|
header: 'Location',
|
|
68
|
-
cell: (row) =>
|
|
68
|
+
cell: (row) => {
|
|
69
|
+
const location = [row.address.city, row.address.state].filter(Boolean).join(', ');
|
|
70
|
+
return location || 'Not provided';
|
|
71
|
+
},
|
|
69
72
|
},
|
|
70
73
|
{
|
|
71
74
|
id: 'rating',
|
|
@@ -73,7 +76,7 @@ export default function CustomersPage() {
|
|
|
73
76
|
cell: (row) => (
|
|
74
77
|
<div className="flex items-center gap-1">
|
|
75
78
|
<Star size={14} className="text-status-warning-text fill-current" />
|
|
76
|
-
<span>{row.rating.toFixed(1)}</span>
|
|
79
|
+
<span>{row.rating !== undefined ? row.rating.toFixed(1) : 'Not provided'}</span>
|
|
77
80
|
</div>
|
|
78
81
|
),
|
|
79
82
|
},
|
|
@@ -277,7 +277,9 @@ export default function DashboardPage() {
|
|
|
277
277
|
>
|
|
278
278
|
<p className="font-medium">{alert.message}</p>
|
|
279
279
|
<p className="mt-1 text-xs text-text-secondary">
|
|
280
|
-
{
|
|
280
|
+
{alert.timestamp
|
|
281
|
+
? formatDate(alert.timestamp, 'relative')
|
|
282
|
+
: 'Time not recorded'}
|
|
281
283
|
</p>
|
|
282
284
|
</div>
|
|
283
285
|
))}
|
|
@@ -86,6 +86,7 @@ interface DriverFormState {
|
|
|
86
86
|
licenseExpiry: string;
|
|
87
87
|
status: DriverStatus;
|
|
88
88
|
vehicleId: string;
|
|
89
|
+
vehicleIds: string[];
|
|
89
90
|
rating: string;
|
|
90
91
|
notes: string;
|
|
91
92
|
hireDate: string;
|
|
@@ -106,6 +107,7 @@ const emptyForm: DriverFormState = {
|
|
|
106
107
|
licenseExpiry: '',
|
|
107
108
|
status: 'available',
|
|
108
109
|
vehicleId: '',
|
|
110
|
+
vehicleIds: [],
|
|
109
111
|
rating: '',
|
|
110
112
|
notes: '',
|
|
111
113
|
hireDate: '',
|
|
@@ -130,7 +132,14 @@ function NewDriverForm({ editId }: { editId?: string }) {
|
|
|
130
132
|
|
|
131
133
|
const [vehicleSearch, setVehicleSearch] = useState('');
|
|
132
134
|
const debouncedVehicleSearch = useDebouncedValue(vehicleSearch);
|
|
133
|
-
const {
|
|
135
|
+
const {
|
|
136
|
+
vehicles,
|
|
137
|
+
isLoading: vehiclesLoading,
|
|
138
|
+
isLoadingMore: vehiclesLoadingMore,
|
|
139
|
+
hasMore: vehiclesHaveMore,
|
|
140
|
+
totalVehicles,
|
|
141
|
+
loadMore: loadMoreVehicles,
|
|
142
|
+
} = useVehicles({
|
|
134
143
|
limit: 20,
|
|
135
144
|
search: debouncedVehicleSearch || undefined,
|
|
136
145
|
});
|
|
@@ -152,6 +161,7 @@ function NewDriverForm({ editId }: { editId?: string }) {
|
|
|
152
161
|
licenseExpiry: toDateInput(driver.licenseExpiry),
|
|
153
162
|
status: driver.status,
|
|
154
163
|
vehicleId: driver.assignedVehicle || '',
|
|
164
|
+
vehicleIds: driver.vehicleIds,
|
|
155
165
|
rating: driver.rating ? String(driver.rating) : '',
|
|
156
166
|
notes: driver.notes || '',
|
|
157
167
|
hireDate: toDateInput(driver.hireDate),
|
|
@@ -173,7 +183,8 @@ function NewDriverForm({ editId }: { editId?: string }) {
|
|
|
173
183
|
licenseClass: form.licenseClass.trim() || undefined,
|
|
174
184
|
licenseExpiry: fromDateInput(form.licenseExpiry),
|
|
175
185
|
status: toApiDriverStatus(form.status),
|
|
176
|
-
vehicleId: form.vehicleId ||
|
|
186
|
+
vehicleId: form.vehicleId || null,
|
|
187
|
+
vehicleIds: form.vehicleIds,
|
|
177
188
|
rating: form.rating.trim() ? Number(form.rating) : undefined,
|
|
178
189
|
notes: form.notes.trim() || undefined,
|
|
179
190
|
hireDate: fromDateInput(form.hireDate),
|
|
@@ -411,7 +422,13 @@ function NewDriverForm({ editId }: { editId?: string }) {
|
|
|
411
422
|
<EntityCombobox
|
|
412
423
|
id="vehicle"
|
|
413
424
|
value={form.vehicleId}
|
|
414
|
-
onChange={(value) =>
|
|
425
|
+
onChange={(value) =>
|
|
426
|
+
setForm((prev) => ({
|
|
427
|
+
...prev,
|
|
428
|
+
vehicleId: value,
|
|
429
|
+
vehicleIds: value ? [value] : [],
|
|
430
|
+
}))
|
|
431
|
+
}
|
|
415
432
|
options={vehicles}
|
|
416
433
|
loading={vehiclesLoading}
|
|
417
434
|
getId={(vehicle) => vehicle.id}
|
|
@@ -424,6 +441,10 @@ function NewDriverForm({ editId }: { editId?: string }) {
|
|
|
424
441
|
)}
|
|
425
442
|
selectedOption={selectedVehicle}
|
|
426
443
|
selectedLoading={selectedVehicleLoading}
|
|
444
|
+
hasMore={vehiclesHaveMore}
|
|
445
|
+
totalCount={totalVehicles}
|
|
446
|
+
onLoadMore={loadMoreVehicles}
|
|
447
|
+
loadingMore={vehiclesLoadingMore}
|
|
427
448
|
placeholder="Unassigned"
|
|
428
449
|
emptyText="No vehicles match that search."
|
|
429
450
|
/>
|
|
@@ -661,7 +682,7 @@ export default function DriverDetailsPage() {
|
|
|
661
682
|
customColors={{ bg: 'bg-status-warning-bg-subtle', text: 'text-status-warning-text' }}
|
|
662
683
|
/>
|
|
663
684
|
)}
|
|
664
|
-
{driver.rating
|
|
685
|
+
{driver.rating !== undefined && (
|
|
665
686
|
<div className="flex items-center gap-1">
|
|
666
687
|
<Star size={16} className="text-status-warning-text fill-current" />
|
|
667
688
|
<span className="font-medium">{driver.rating.toFixed(1)}</span>
|
|
@@ -170,7 +170,7 @@ export default function DriversPage() {
|
|
|
170
170
|
id: 'rating',
|
|
171
171
|
header: 'Rating',
|
|
172
172
|
cell: (row) =>
|
|
173
|
-
row.rating
|
|
173
|
+
row.rating !== undefined ? (
|
|
174
174
|
<div className="flex items-center gap-1">
|
|
175
175
|
<Star size={14} className="text-status-warning-text fill-current" />
|
|
176
176
|
<span className="font-medium">{row.rating.toFixed(1)}</span>
|
|
@@ -299,7 +299,13 @@ export default function DriversPage() {
|
|
|
299
299
|
))}
|
|
300
300
|
</SelectContent>
|
|
301
301
|
</Select>
|
|
302
|
-
<Select
|
|
302
|
+
<Select
|
|
303
|
+
value={licenseClassFilter}
|
|
304
|
+
onValueChange={(value) => {
|
|
305
|
+
setLicenseClassFilter(value);
|
|
306
|
+
setPage(0);
|
|
307
|
+
}}
|
|
308
|
+
>
|
|
303
309
|
<SelectTrigger className="w-[180px]">
|
|
304
310
|
<SelectValue placeholder="License class" />
|
|
305
311
|
</SelectTrigger>
|
|
@@ -308,7 +308,7 @@ export default function FleetPage() {
|
|
|
308
308
|
</div>
|
|
309
309
|
<div className="text-right">
|
|
310
310
|
<StatusBadge status={driver.status} type="driver" size="sm" />
|
|
311
|
-
{driver.rating
|
|
311
|
+
{driver.rating !== undefined && (
|
|
312
312
|
<div className="mt-1 flex items-center gap-1 justify-end">
|
|
313
313
|
<Star size={12} className="text-status-warning-text fill-current" />
|
|
314
314
|
<span className="text-sm font-medium">{driver.rating.toFixed(1)}</span>
|
|
@@ -502,32 +502,32 @@ export default function InventoryPage() {
|
|
|
502
502
|
<div>
|
|
503
503
|
<p className="font-medium">{warehouse.name}</p>
|
|
504
504
|
<p className="text-sm text-text-secondary">
|
|
505
|
-
{warehouse.address.city
|
|
505
|
+
{[warehouse.address.city, warehouse.address.state]
|
|
506
|
+
.filter(Boolean)
|
|
507
|
+
.join(', ') || 'Not provided'}
|
|
506
508
|
</p>
|
|
507
509
|
</div>
|
|
508
510
|
<div className="text-right">
|
|
509
511
|
<Badge
|
|
510
512
|
variant={
|
|
511
|
-
warehouse.status
|
|
513
|
+
warehouse.status?.toUpperCase() === 'ACTIVE' ? 'default' : 'secondary'
|
|
512
514
|
}
|
|
513
515
|
>
|
|
514
|
-
{warehouse.status
|
|
515
|
-
?
|
|
516
|
-
|
|
516
|
+
{warehouse.status
|
|
517
|
+
? warehouse.status.toUpperCase() === 'ACTIVE'
|
|
518
|
+
? t('movethewheels.inventory.warehouses.active', 'Active')
|
|
519
|
+
: t('movethewheels.inventory.warehouses.inactive', 'Inactive')
|
|
520
|
+
: 'Not provided'}
|
|
517
521
|
</Badge>
|
|
518
522
|
<div className="mt-2">
|
|
519
523
|
<p className="text-xs text-text-secondary">
|
|
520
524
|
{t('movethewheels.inventory.warehouses.utilization', 'Utilization')}
|
|
521
525
|
</p>
|
|
522
|
-
|
|
523
|
-
value={
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
: 0
|
|
528
|
-
}
|
|
529
|
-
className="w-24 h-2"
|
|
530
|
-
/>
|
|
526
|
+
{warehouse.utilization !== undefined ? (
|
|
527
|
+
<Progress value={warehouse.utilization} className="w-24 h-2" />
|
|
528
|
+
) : (
|
|
529
|
+
<p className="text-xs text-text-tertiary">Not provided</p>
|
|
530
|
+
)}
|
|
531
531
|
</div>
|
|
532
532
|
</div>
|
|
533
533
|
</div>
|
|
@@ -567,8 +567,12 @@ export default function InventoryPage() {
|
|
|
567
567
|
<p className="text-sm text-text-secondary">{product.sku}</p>
|
|
568
568
|
</div>
|
|
569
569
|
<div className="text-right">
|
|
570
|
-
<p className="font-medium">
|
|
571
|
-
|
|
570
|
+
<p className="font-medium">
|
|
571
|
+
{product.value !== undefined
|
|
572
|
+
? formatCurrency(product.value)
|
|
573
|
+
: 'Not provided'}
|
|
574
|
+
</p>
|
|
575
|
+
<Badge variant="outline">{product.category ?? 'Not provided'}</Badge>
|
|
572
576
|
</div>
|
|
573
577
|
</div>
|
|
574
578
|
</div>
|
|
@@ -83,7 +83,7 @@ export default function LiveTrackingPage() {
|
|
|
83
83
|
sublabel:
|
|
84
84
|
d.driver?.fullName ||
|
|
85
85
|
d.vehicle?.vehicleNumber ||
|
|
86
|
-
d.tracking?.currentLocation
|
|
86
|
+
d.tracking?.currentLocation?.address ||
|
|
87
87
|
undefined,
|
|
88
88
|
},
|
|
89
89
|
];
|
|
@@ -292,7 +292,7 @@ export default function LiveTrackingPage() {
|
|
|
292
292
|
: 'N/A'}
|
|
293
293
|
</p>
|
|
294
294
|
<p className="text-xs text-text-secondary">
|
|
295
|
-
{delivery.tracking?.currentLocation
|
|
295
|
+
{delivery.tracking?.currentLocation?.address ||
|
|
296
296
|
'Location updating...'}
|
|
297
297
|
</p>
|
|
298
298
|
</div>
|
|
@@ -352,7 +352,7 @@ export default function LiveTrackingPage() {
|
|
|
352
352
|
<div>
|
|
353
353
|
<p className="text-sm text-text-secondary">Current Location</p>
|
|
354
354
|
<p className="text-sm">
|
|
355
|
-
{selected.tracking?.currentLocation
|
|
355
|
+
{selected.tracking?.currentLocation?.address || 'Updating...'}
|
|
356
356
|
</p>
|
|
357
357
|
{(() => {
|
|
358
358
|
const coord = firstRealLatLng(
|
|
@@ -440,7 +440,9 @@ export default function LiveTrackingPage() {
|
|
|
440
440
|
>
|
|
441
441
|
<p className="font-medium">{alert.message}</p>
|
|
442
442
|
<p className="text-xs text-text-secondary mt-1">
|
|
443
|
-
{
|
|
443
|
+
{alert.timestamp
|
|
444
|
+
? formatDate(alert.timestamp, 'relative')
|
|
445
|
+
: 'Time not recorded'}
|
|
444
446
|
</p>
|
|
445
447
|
</div>
|
|
446
448
|
))}
|
|
@@ -81,6 +81,8 @@ function formatMoney(
|
|
|
81
81
|
export default function MultiModalShipmentPage() {
|
|
82
82
|
const [pageIndex, setPageIndex] = useState(0);
|
|
83
83
|
const pageSize = 10;
|
|
84
|
+
const [shipmentNumber, setShipmentNumber] = useState('');
|
|
85
|
+
const [shipmentSearch, setShipmentSearch] = useState('');
|
|
84
86
|
const {
|
|
85
87
|
shipments,
|
|
86
88
|
totalShipments,
|
|
@@ -92,9 +94,9 @@ export default function MultiModalShipmentPage() {
|
|
|
92
94
|
} = useMultiModalShipments({
|
|
93
95
|
limit: pageSize,
|
|
94
96
|
offset: pageIndex * pageSize,
|
|
97
|
+
search: shipmentSearch,
|
|
95
98
|
});
|
|
96
99
|
|
|
97
|
-
const [shipmentNumber, setShipmentNumber] = useState('');
|
|
98
100
|
const [originAddress, setOriginAddress] = useState('');
|
|
99
101
|
const [destinationAddress, setDestinationAddress] = useState('');
|
|
100
102
|
const [legs, setLegs] = useState<ShipmentLegDraft[]>([emptyLeg()]);
|
|
@@ -529,6 +531,18 @@ export default function MultiModalShipmentPage() {
|
|
|
529
531
|
</PageSection>
|
|
530
532
|
|
|
531
533
|
<PageSection title="Planned Multi-Modal Shipments">
|
|
534
|
+
<div className="mb-4 max-w-xl space-y-2">
|
|
535
|
+
<Label htmlFor="planned-shipment-search">Search planned shipments</Label>
|
|
536
|
+
<Input
|
|
537
|
+
id="planned-shipment-search"
|
|
538
|
+
value={shipmentSearch}
|
|
539
|
+
onChange={(event) => {
|
|
540
|
+
setShipmentSearch(event.target.value);
|
|
541
|
+
setPageIndex(0);
|
|
542
|
+
}}
|
|
543
|
+
placeholder="Search by shipment or tracking number"
|
|
544
|
+
/>
|
|
545
|
+
</div>
|
|
532
546
|
{isLoading ? (
|
|
533
547
|
<LoadingState message="Loading multi-modal shipments..." />
|
|
534
548
|
) : error ? (
|
|
@@ -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'
|