@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
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
Warehouse,
|
|
16
16
|
ChevronLeft,
|
|
17
17
|
ChevronRight,
|
|
18
|
+
Trash2,
|
|
18
19
|
} from 'lucide-react';
|
|
19
20
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
20
21
|
import {
|
|
@@ -26,8 +27,20 @@ import {
|
|
|
26
27
|
Badge,
|
|
27
28
|
Input,
|
|
28
29
|
Label,
|
|
30
|
+
Textarea,
|
|
31
|
+
Checkbox,
|
|
29
32
|
} from '../components/ui';
|
|
30
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
EmphasisPanel,
|
|
35
|
+
NextSteps,
|
|
36
|
+
PagePurpose,
|
|
37
|
+
ResponsiveDialog,
|
|
38
|
+
ResponsiveDialogContent,
|
|
39
|
+
ResponsiveDialogHeader,
|
|
40
|
+
ResponsiveDialogFooter,
|
|
41
|
+
ResponsiveDialogTitle,
|
|
42
|
+
ResponsiveDialogDescription,
|
|
43
|
+
} from '@burdenoff/fe-libs/ui';
|
|
31
44
|
import { useProduct, useInventory, useProductStockLevels } from '../hooks/useInventory';
|
|
32
45
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
33
46
|
import { formatCurrency } from '../utils/formatters';
|
|
@@ -42,9 +55,18 @@ function NewProductForm({ editId }: { editId?: string }) {
|
|
|
42
55
|
const [form, setForm] = useState({
|
|
43
56
|
name: '',
|
|
44
57
|
sku: '',
|
|
58
|
+
description: '',
|
|
45
59
|
category: '',
|
|
46
60
|
brand: '',
|
|
61
|
+
length: '',
|
|
62
|
+
width: '',
|
|
63
|
+
height: '',
|
|
64
|
+
dimensionUnit: '',
|
|
47
65
|
weight: '',
|
|
66
|
+
value: '',
|
|
67
|
+
hazardous: false,
|
|
68
|
+
fragile: false,
|
|
69
|
+
temperatureControlled: false,
|
|
48
70
|
});
|
|
49
71
|
const [submitting, setSubmitting] = useState(false);
|
|
50
72
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -54,9 +76,18 @@ function NewProductForm({ editId }: { editId?: string }) {
|
|
|
54
76
|
setForm({
|
|
55
77
|
name: product.name ?? '',
|
|
56
78
|
sku: product.sku ?? '',
|
|
79
|
+
description: product.description ?? '',
|
|
57
80
|
category: product.category ?? '',
|
|
58
81
|
brand: product.brand ?? '',
|
|
59
|
-
|
|
82
|
+
length: product.dimensions?.length !== undefined ? String(product.dimensions.length) : '',
|
|
83
|
+
width: product.dimensions?.width !== undefined ? String(product.dimensions.width) : '',
|
|
84
|
+
height: product.dimensions?.height !== undefined ? String(product.dimensions.height) : '',
|
|
85
|
+
dimensionUnit: product.dimensions?.unit ?? '',
|
|
86
|
+
weight: product.weight !== undefined ? String(product.weight) : '',
|
|
87
|
+
value: product.value !== undefined ? String(product.value) : '',
|
|
88
|
+
hazardous: product.hazardous,
|
|
89
|
+
fragile: product.fragile,
|
|
90
|
+
temperatureControlled: product.temperatureControlled,
|
|
60
91
|
});
|
|
61
92
|
}
|
|
62
93
|
}, [editId, product]);
|
|
@@ -72,24 +103,42 @@ function NewProductForm({ editId }: { editId?: string }) {
|
|
|
72
103
|
setSubmitting(true);
|
|
73
104
|
setError(null);
|
|
74
105
|
try {
|
|
75
|
-
const
|
|
106
|
+
const parseNumber = (value: string): number | undefined => {
|
|
107
|
+
if (!value.trim()) return undefined;
|
|
108
|
+
const parsed = Number(value);
|
|
109
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
110
|
+
};
|
|
111
|
+
const dimensions =
|
|
112
|
+
form.length.trim() || form.width.trim() || form.height.trim() || form.dimensionUnit.trim()
|
|
113
|
+
? {
|
|
114
|
+
...(parseNumber(form.length) !== undefined
|
|
115
|
+
? { length: parseNumber(form.length) }
|
|
116
|
+
: {}),
|
|
117
|
+
...(parseNumber(form.width) !== undefined ? { width: parseNumber(form.width) } : {}),
|
|
118
|
+
...(parseNumber(form.height) !== undefined
|
|
119
|
+
? { height: parseNumber(form.height) }
|
|
120
|
+
: {}),
|
|
121
|
+
...(form.dimensionUnit.trim() ? { unit: form.dimensionUnit.trim() } : {}),
|
|
122
|
+
}
|
|
123
|
+
: undefined;
|
|
124
|
+
const input = {
|
|
125
|
+
name: form.name.trim(),
|
|
126
|
+
sku: form.sku.trim(),
|
|
127
|
+
description: form.description.trim() || (isEditing ? null : undefined),
|
|
128
|
+
category: form.category.trim() || (isEditing ? null : undefined),
|
|
129
|
+
brand: form.brand.trim() || (isEditing ? null : undefined),
|
|
130
|
+
dimensions: dimensions ?? (isEditing ? null : undefined),
|
|
131
|
+
weight: parseNumber(form.weight) ?? (isEditing ? null : undefined),
|
|
132
|
+
value: parseNumber(form.value) ?? (isEditing ? null : undefined),
|
|
133
|
+
hazardous: form.hazardous,
|
|
134
|
+
fragile: form.fragile,
|
|
135
|
+
temperatureControlled: form.temperatureControlled,
|
|
136
|
+
};
|
|
76
137
|
if (isEditing && editId) {
|
|
77
|
-
const updated = await updateProduct(editId,
|
|
78
|
-
name: form.name.trim(),
|
|
79
|
-
sku: form.sku.trim(),
|
|
80
|
-
category: form.category.trim() || undefined,
|
|
81
|
-
brand: form.brand.trim() || undefined,
|
|
82
|
-
weight: weight !== undefined && !Number.isNaN(weight) ? weight : undefined,
|
|
83
|
-
});
|
|
138
|
+
const updated = await updateProduct(editId, input);
|
|
84
139
|
navigate?.(joinPath(basePath, `inventory/products/${updated.id}`));
|
|
85
140
|
} else {
|
|
86
|
-
const created = await createProduct(
|
|
87
|
-
name: form.name.trim(),
|
|
88
|
-
sku: form.sku.trim(),
|
|
89
|
-
category: form.category.trim() || undefined,
|
|
90
|
-
brand: form.brand.trim() || undefined,
|
|
91
|
-
weight: weight !== undefined && !Number.isNaN(weight) ? weight : undefined,
|
|
92
|
-
});
|
|
141
|
+
const created = await createProduct(input);
|
|
93
142
|
navigate?.(joinPath(basePath, `inventory/products/${created.id}`));
|
|
94
143
|
}
|
|
95
144
|
} catch (err) {
|
|
@@ -177,6 +226,61 @@ function NewProductForm({ editId }: { editId?: string }) {
|
|
|
177
226
|
placeholder="Acme"
|
|
178
227
|
/>
|
|
179
228
|
</div>
|
|
229
|
+
</div>
|
|
230
|
+
<div className="space-y-2">
|
|
231
|
+
<Label htmlFor="description">Description</Label>
|
|
232
|
+
<Textarea
|
|
233
|
+
id="description"
|
|
234
|
+
value={form.description}
|
|
235
|
+
onChange={(e) => setForm((prev) => ({ ...prev, description: e.target.value }))}
|
|
236
|
+
placeholder="Describe the item"
|
|
237
|
+
rows={3}
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
241
|
+
{(
|
|
242
|
+
[
|
|
243
|
+
['length', 'Length'],
|
|
244
|
+
['width', 'Width'],
|
|
245
|
+
['height', 'Height'],
|
|
246
|
+
] as const
|
|
247
|
+
).map(([field, label]) => (
|
|
248
|
+
<div key={field} className="space-y-2">
|
|
249
|
+
<Label htmlFor={field}>{label}</Label>
|
|
250
|
+
<Input
|
|
251
|
+
id={field}
|
|
252
|
+
type="number"
|
|
253
|
+
min="0"
|
|
254
|
+
step="0.01"
|
|
255
|
+
value={form[field]}
|
|
256
|
+
onChange={(e) => setForm((prev) => ({ ...prev, [field]: e.target.value }))}
|
|
257
|
+
placeholder="Not provided"
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
))}
|
|
261
|
+
<div className="space-y-2">
|
|
262
|
+
<Label htmlFor="dimensionUnit">Dimension unit</Label>
|
|
263
|
+
<Input
|
|
264
|
+
id="dimensionUnit"
|
|
265
|
+
value={form.dimensionUnit}
|
|
266
|
+
onChange={(e) => setForm((prev) => ({ ...prev, dimensionUnit: e.target.value }))}
|
|
267
|
+
placeholder="cm"
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
272
|
+
<div className="space-y-2">
|
|
273
|
+
<Label htmlFor="value">Unit value</Label>
|
|
274
|
+
<Input
|
|
275
|
+
id="value"
|
|
276
|
+
type="number"
|
|
277
|
+
min="0"
|
|
278
|
+
step="0.01"
|
|
279
|
+
value={form.value}
|
|
280
|
+
onChange={(e) => setForm((prev) => ({ ...prev, value: e.target.value }))}
|
|
281
|
+
placeholder="Not provided"
|
|
282
|
+
/>
|
|
283
|
+
</div>
|
|
180
284
|
<div className="space-y-2">
|
|
181
285
|
<Label htmlFor="weight">Weight (kg)</Label>
|
|
182
286
|
<Input
|
|
@@ -186,10 +290,29 @@ function NewProductForm({ editId }: { editId?: string }) {
|
|
|
186
290
|
step="0.01"
|
|
187
291
|
value={form.weight}
|
|
188
292
|
onChange={(e) => setForm((prev) => ({ ...prev, weight: e.target.value }))}
|
|
189
|
-
placeholder="
|
|
293
|
+
placeholder="Not provided"
|
|
190
294
|
/>
|
|
191
295
|
</div>
|
|
192
296
|
</div>
|
|
297
|
+
<div className="flex flex-wrap gap-4">
|
|
298
|
+
{(
|
|
299
|
+
[
|
|
300
|
+
['fragile', 'Fragile'],
|
|
301
|
+
['hazardous', 'Hazardous'],
|
|
302
|
+
['temperatureControlled', 'Temperature controlled'],
|
|
303
|
+
] as const
|
|
304
|
+
).map(([field, label]) => (
|
|
305
|
+
<label key={field} className="flex items-center gap-2 text-sm">
|
|
306
|
+
<Checkbox
|
|
307
|
+
checked={form[field]}
|
|
308
|
+
onCheckedChange={(checked) =>
|
|
309
|
+
setForm((prev) => ({ ...prev, [field]: checked === true }))
|
|
310
|
+
}
|
|
311
|
+
/>
|
|
312
|
+
{label}
|
|
313
|
+
</label>
|
|
314
|
+
))}
|
|
315
|
+
</div>
|
|
193
316
|
</div>
|
|
194
317
|
</EmphasisPanel>
|
|
195
318
|
</form>
|
|
@@ -201,7 +324,11 @@ export default function ProductDetailsPage() {
|
|
|
201
324
|
const { productId } = useParams<{ productId: string }>();
|
|
202
325
|
const { basePath, navigate } = useMoveTheWheels();
|
|
203
326
|
const { product, isLoading, error, refetch } = useProduct(productId);
|
|
327
|
+
const { deleteProduct } = useInventory();
|
|
204
328
|
const isEditRoute = useLocation().pathname.endsWith('/edit');
|
|
329
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
330
|
+
const [deleting, setDeleting] = useState(false);
|
|
331
|
+
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
205
332
|
// Where this item is actually stocked — inventoryItems(filter: { cargoItemId }).
|
|
206
333
|
const [stockPage, setStockPage] = useState(0);
|
|
207
334
|
const stockPageSize = 20;
|
|
@@ -223,6 +350,21 @@ export default function ProductDetailsPage() {
|
|
|
223
350
|
|
|
224
351
|
const goBack = () => navigate?.(joinPath(basePath, 'inventory/products'));
|
|
225
352
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
353
|
+
const handleDelete = async () => {
|
|
354
|
+
if (!productId) return;
|
|
355
|
+
setDeleting(true);
|
|
356
|
+
setDeleteError(null);
|
|
357
|
+
try {
|
|
358
|
+
await deleteProduct(productId);
|
|
359
|
+
setDeleteOpen(false);
|
|
360
|
+
goBack();
|
|
361
|
+
} catch (deleteFailure) {
|
|
362
|
+
setDeleteError(
|
|
363
|
+
deleteFailure instanceof Error ? deleteFailure.message : 'Failed to delete product.'
|
|
364
|
+
);
|
|
365
|
+
setDeleting(false);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
226
368
|
|
|
227
369
|
if (!productId) return <NewProductForm />;
|
|
228
370
|
if (isEditRoute) return <NewProductForm editId={productId} />;
|
|
@@ -248,7 +390,7 @@ export default function ProductDetailsPage() {
|
|
|
248
390
|
}
|
|
249
391
|
headerContent={
|
|
250
392
|
<div className="flex gap-2">
|
|
251
|
-
<Badge variant="outline">{product.category}</Badge>
|
|
393
|
+
<Badge variant="outline">{product.category ?? 'Not provided'}</Badge>
|
|
252
394
|
{product.brand && <Badge variant="secondary">{product.brand}</Badge>}
|
|
253
395
|
{product.fragile && <Badge variant="destructive">Fragile</Badge>}
|
|
254
396
|
{product.hazardous && <Badge variant="destructive">Hazardous</Badge>}
|
|
@@ -289,7 +431,7 @@ export default function ProductDetailsPage() {
|
|
|
289
431
|
<CardTitle>Description</CardTitle>
|
|
290
432
|
</CardHeader>
|
|
291
433
|
<CardContent>
|
|
292
|
-
<p>{product.description}</p>
|
|
434
|
+
<p>{product.description ?? 'Not provided'}</p>
|
|
293
435
|
</CardContent>
|
|
294
436
|
</Card>
|
|
295
437
|
|
|
@@ -301,34 +443,24 @@ export default function ProductDetailsPage() {
|
|
|
301
443
|
</CardHeader>
|
|
302
444
|
<CardContent>
|
|
303
445
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
<
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
<span className="text-sm font-normal"> {product.dimensions.unit}</span>
|
|
323
|
-
</p>
|
|
324
|
-
</div>
|
|
325
|
-
<div>
|
|
326
|
-
<p className="text-sm text-text-secondary">Weight</p>
|
|
327
|
-
<p className="text-2xl font-bold">
|
|
328
|
-
{product.weight}
|
|
329
|
-
<span className="text-sm font-normal"> kg</span>
|
|
330
|
-
</p>
|
|
331
|
-
</div>
|
|
446
|
+
{(
|
|
447
|
+
[
|
|
448
|
+
['Length', product.dimensions?.length, product.dimensions?.unit],
|
|
449
|
+
['Width', product.dimensions?.width, product.dimensions?.unit],
|
|
450
|
+
['Height', product.dimensions?.height, product.dimensions?.unit],
|
|
451
|
+
['Weight', product.weight, 'kg'],
|
|
452
|
+
] as const
|
|
453
|
+
).map(([label, value, unit]) => (
|
|
454
|
+
<div key={label}>
|
|
455
|
+
<p className="text-sm text-text-secondary">{label}</p>
|
|
456
|
+
<p className="text-2xl font-bold">
|
|
457
|
+
{value !== undefined ? value : 'Not provided'}
|
|
458
|
+
{value !== undefined && unit && (
|
|
459
|
+
<span className="text-sm font-normal"> {unit}</span>
|
|
460
|
+
)}
|
|
461
|
+
</p>
|
|
462
|
+
</div>
|
|
463
|
+
))}
|
|
332
464
|
</div>
|
|
333
465
|
</CardContent>
|
|
334
466
|
</Card>
|
|
@@ -360,7 +492,9 @@ export default function ProductDetailsPage() {
|
|
|
360
492
|
<h2 className="flex items-center gap-2 text-base font-semibold text-text-primary mb-3">
|
|
361
493
|
<DollarSign size={18} /> Pricing
|
|
362
494
|
</h2>
|
|
363
|
-
<p className="text-3xl font-bold">
|
|
495
|
+
<p className="text-3xl font-bold">
|
|
496
|
+
{product.value !== undefined ? formatCurrency(product.value) : 'Not provided'}
|
|
497
|
+
</p>
|
|
364
498
|
<p className="text-sm text-text-secondary">Unit price</p>
|
|
365
499
|
</EmphasisPanel>
|
|
366
500
|
|
|
@@ -466,8 +600,53 @@ export default function ProductDetailsPage() {
|
|
|
466
600
|
)}
|
|
467
601
|
</CardContent>
|
|
468
602
|
</Card>
|
|
603
|
+
|
|
604
|
+
<Card className="border-status-error-border">
|
|
605
|
+
<CardHeader>
|
|
606
|
+
<CardTitle className="flex items-center gap-2 text-status-error-text">
|
|
607
|
+
<Trash2 size={18} /> Delete Product
|
|
608
|
+
</CardTitle>
|
|
609
|
+
</CardHeader>
|
|
610
|
+
<CardContent>
|
|
611
|
+
<p className="mb-3 text-sm text-text-secondary">
|
|
612
|
+
Removes this catalog item. Existing inventory records may prevent deletion.
|
|
613
|
+
</p>
|
|
614
|
+
<Button
|
|
615
|
+
variant="outline"
|
|
616
|
+
className="border-status-error-border text-status-error-text hover:bg-status-error-bg-subtle"
|
|
617
|
+
onClick={() => setDeleteOpen(true)}
|
|
618
|
+
>
|
|
619
|
+
<Trash2 size={16} /> Delete this product
|
|
620
|
+
</Button>
|
|
621
|
+
</CardContent>
|
|
622
|
+
</Card>
|
|
469
623
|
</div>
|
|
470
624
|
</div>
|
|
625
|
+
<ResponsiveDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
|
626
|
+
<ResponsiveDialogContent>
|
|
627
|
+
<ResponsiveDialogHeader>
|
|
628
|
+
<ResponsiveDialogTitle>Delete this product?</ResponsiveDialogTitle>
|
|
629
|
+
<ResponsiveDialogDescription>
|
|
630
|
+
This removes {product.name} from the catalog and cannot be undone from here.
|
|
631
|
+
</ResponsiveDialogDescription>
|
|
632
|
+
</ResponsiveDialogHeader>
|
|
633
|
+
<div className="px-1">
|
|
634
|
+
<FormError message={deleteError} />
|
|
635
|
+
</div>
|
|
636
|
+
<ResponsiveDialogFooter>
|
|
637
|
+
<Button variant="outline" onClick={() => setDeleteOpen(false)} disabled={deleting}>
|
|
638
|
+
Cancel
|
|
639
|
+
</Button>
|
|
640
|
+
<Button
|
|
641
|
+
className="bg-status-error-text text-text-inverse hover:opacity-90"
|
|
642
|
+
onClick={handleDelete}
|
|
643
|
+
disabled={deleting}
|
|
644
|
+
>
|
|
645
|
+
{deleting ? 'Deleting...' : 'Delete product'}
|
|
646
|
+
</Button>
|
|
647
|
+
</ResponsiveDialogFooter>
|
|
648
|
+
</ResponsiveDialogContent>
|
|
649
|
+
</ResponsiveDialog>
|
|
471
650
|
</PageLayout>
|
|
472
651
|
);
|
|
473
652
|
}
|
|
@@ -48,7 +48,9 @@ export default function ProductsPage() {
|
|
|
48
48
|
cell: (row) => (
|
|
49
49
|
<div>
|
|
50
50
|
<p className="font-medium">{row.name}</p>
|
|
51
|
-
<p className="text-sm text-text-secondary line-clamp-1">
|
|
51
|
+
<p className="text-sm text-text-secondary line-clamp-1">
|
|
52
|
+
{row.description ?? 'Not provided'}
|
|
53
|
+
</p>
|
|
52
54
|
</div>
|
|
53
55
|
),
|
|
54
56
|
},
|
|
@@ -57,27 +59,33 @@ export default function ProductsPage() {
|
|
|
57
59
|
header: 'Category',
|
|
58
60
|
accessorKey: 'category',
|
|
59
61
|
sortable: true,
|
|
60
|
-
cell: (row) => <Badge variant="outline">{row.category}</Badge>,
|
|
62
|
+
cell: (row) => <Badge variant="outline">{row.category ?? 'Not provided'}</Badge>,
|
|
61
63
|
},
|
|
62
64
|
{
|
|
63
65
|
id: 'dimensions',
|
|
64
66
|
header: 'Dimensions',
|
|
65
67
|
cell: (row) =>
|
|
66
|
-
|
|
68
|
+
row.dimensions
|
|
69
|
+
? `${row.dimensions.length ?? 'Not provided'}x${row.dimensions.width ?? 'Not provided'}x${row.dimensions.height ?? 'Not provided'}${row.dimensions.unit ? ` ${row.dimensions.unit}` : ''}`
|
|
70
|
+
: 'Not provided',
|
|
67
71
|
},
|
|
68
72
|
{
|
|
69
73
|
id: 'weight',
|
|
70
74
|
header: 'Weight',
|
|
71
75
|
accessorKey: 'weight',
|
|
72
76
|
sortable: true,
|
|
73
|
-
cell: (row) => `${row.weight} kg
|
|
77
|
+
cell: (row) => (row.weight !== undefined ? `${row.weight} kg` : 'Not provided'),
|
|
74
78
|
},
|
|
75
79
|
{
|
|
76
80
|
id: 'value',
|
|
77
81
|
header: 'Value',
|
|
78
82
|
accessorKey: 'value',
|
|
79
83
|
sortable: true,
|
|
80
|
-
cell: (row) =>
|
|
84
|
+
cell: (row) => (
|
|
85
|
+
<span className="font-medium">
|
|
86
|
+
{row.value !== undefined ? formatCurrency(row.value) : 'Not provided'}
|
|
87
|
+
</span>
|
|
88
|
+
),
|
|
81
89
|
},
|
|
82
90
|
{
|
|
83
91
|
id: 'flags',
|
|
@@ -95,20 +95,22 @@ function NewRouteForm({ editId }: { editId?: string }) {
|
|
|
95
95
|
isLoading: vehiclesLoading,
|
|
96
96
|
hasMore: vehiclesHaveMore,
|
|
97
97
|
totalVehicles,
|
|
98
|
+
loadMore: loadMoreVehicles,
|
|
99
|
+
isLoadingMore: vehiclesLoadingMore,
|
|
98
100
|
} = useVehicles({
|
|
99
101
|
limit: 20,
|
|
100
102
|
search: vehicleSearch || undefined,
|
|
101
|
-
driverId: form.driverId || undefined,
|
|
102
103
|
});
|
|
103
104
|
const {
|
|
104
105
|
drivers,
|
|
105
106
|
isLoading: driversLoading,
|
|
106
107
|
hasMore: driversHaveMore,
|
|
107
108
|
totalDrivers,
|
|
109
|
+
loadMore: loadMoreDrivers,
|
|
110
|
+
isLoadingMore: driversLoadingMore,
|
|
108
111
|
} = useDrivers({
|
|
109
112
|
limit: 20,
|
|
110
113
|
search: driverSearch || undefined,
|
|
111
|
-
vehicleId: form.vehicleId || undefined,
|
|
112
114
|
});
|
|
113
115
|
// The selected vehicle/driver may fall outside the current search page (e.g. when
|
|
114
116
|
// editing a route or after the assignee no longer matches the search term); fetch
|
|
@@ -195,16 +197,14 @@ function NewRouteForm({ editId }: { editId?: string }) {
|
|
|
195
197
|
country: '',
|
|
196
198
|
},
|
|
197
199
|
type: stop.type,
|
|
198
|
-
scheduledTime: new Date(),
|
|
199
|
-
duration: 0,
|
|
200
200
|
status: 'pending',
|
|
201
201
|
}));
|
|
202
202
|
if (isEditing) {
|
|
203
203
|
const updated = await updateRoute(editId, {
|
|
204
204
|
name: form.name.trim(),
|
|
205
205
|
status: form.status,
|
|
206
|
-
vehicleId: form.vehicleId ||
|
|
207
|
-
driverId: form.driverId ||
|
|
206
|
+
vehicleId: form.vehicleId || null,
|
|
207
|
+
driverId: form.driverId || null,
|
|
208
208
|
stops,
|
|
209
209
|
});
|
|
210
210
|
navigate?.(joinPath(basePath, `routes/${updated.id}`));
|
|
@@ -316,7 +316,8 @@ function NewRouteForm({ editId }: { editId?: string }) {
|
|
|
316
316
|
searchPlaceholder="Search vehicle number or type…"
|
|
317
317
|
hasMore={vehiclesHaveMore}
|
|
318
318
|
totalCount={totalVehicles}
|
|
319
|
-
|
|
319
|
+
onLoadMore={loadMoreVehicles}
|
|
320
|
+
loadingMore={vehiclesLoadingMore}
|
|
320
321
|
placeholder="Assign later"
|
|
321
322
|
emptyText="No available vehicles."
|
|
322
323
|
/>
|
|
@@ -342,7 +343,8 @@ function NewRouteForm({ editId }: { editId?: string }) {
|
|
|
342
343
|
searchPlaceholder="Search driver name or employee ID…"
|
|
343
344
|
hasMore={driversHaveMore}
|
|
344
345
|
totalCount={totalDrivers}
|
|
345
|
-
|
|
346
|
+
onLoadMore={loadMoreDrivers}
|
|
347
|
+
loadingMore={driversLoadingMore}
|
|
346
348
|
placeholder="Assign later"
|
|
347
349
|
emptyText="No available drivers."
|
|
348
350
|
/>
|
|
@@ -542,8 +544,12 @@ export default function RouteDetailsPage() {
|
|
|
542
544
|
headerContent={
|
|
543
545
|
<div className="flex gap-2">
|
|
544
546
|
<StatusBadge status={route.status} customColors={ROUTE_STATUS_COLORS[route.status]} />
|
|
545
|
-
<Badge variant={route.optimized ? 'default' : 'secondary'}>
|
|
546
|
-
{route.optimized
|
|
547
|
+
<Badge variant={route.optimized === true ? 'default' : 'secondary'}>
|
|
548
|
+
{route.optimized === undefined
|
|
549
|
+
? 'Optimization unknown'
|
|
550
|
+
: route.optimized
|
|
551
|
+
? 'Optimized'
|
|
552
|
+
: 'Not Optimized'}
|
|
547
553
|
</Badge>
|
|
548
554
|
</div>
|
|
549
555
|
}
|
|
@@ -668,14 +674,20 @@ export default function RouteDetailsPage() {
|
|
|
668
674
|
</div>
|
|
669
675
|
<div className="text-right">
|
|
670
676
|
<p className="text-sm">
|
|
671
|
-
|
|
677
|
+
{stop.scheduledTime
|
|
678
|
+
? `Scheduled: ${formatDate(stop.scheduledTime, 'time')}`
|
|
679
|
+
: 'Scheduled: Not provided'}
|
|
672
680
|
</p>
|
|
673
681
|
{stop.actualTime && (
|
|
674
682
|
<p className="text-sm text-status-success-text">
|
|
675
683
|
Actual: {formatDate(stop.actualTime, 'time')}
|
|
676
684
|
</p>
|
|
677
685
|
)}
|
|
678
|
-
<p className="text-xs text-text-secondary">
|
|
686
|
+
<p className="text-xs text-text-secondary">
|
|
687
|
+
{stop.duration !== undefined
|
|
688
|
+
? `${stop.duration} min stop`
|
|
689
|
+
: 'Duration not provided'}
|
|
690
|
+
</p>
|
|
679
691
|
</div>
|
|
680
692
|
</div>
|
|
681
693
|
</div>
|
|
@@ -88,8 +88,10 @@ export default function RoutesPage() {
|
|
|
88
88
|
id: 'optimized',
|
|
89
89
|
header: 'Optimized',
|
|
90
90
|
cell: (row) => (
|
|
91
|
-
<Badge
|
|
92
|
-
{row.optimized ? '
|
|
91
|
+
<Badge
|
|
92
|
+
variant={row.optimized === undefined ? 'outline' : row.optimized ? 'default' : 'outline'}
|
|
93
|
+
>
|
|
94
|
+
{row.optimized === undefined ? 'Unknown' : row.optimized ? 'Yes' : 'No'}
|
|
93
95
|
</Badge>
|
|
94
96
|
),
|
|
95
97
|
},
|
|
@@ -67,7 +67,7 @@ export default function TrackingPage() {
|
|
|
67
67
|
? {
|
|
68
68
|
lat: coord.lat,
|
|
69
69
|
lng: coord.lng,
|
|
70
|
-
timestamp: location.timestamp?.toISOString()
|
|
70
|
+
timestamp: location.timestamp?.toISOString(),
|
|
71
71
|
}
|
|
72
72
|
: null;
|
|
73
73
|
})
|
|
@@ -87,7 +87,7 @@ export default function TrackingPage() {
|
|
|
87
87
|
const current: TrackEndpoint | null = currentCoord
|
|
88
88
|
? {
|
|
89
89
|
...currentCoord,
|
|
90
|
-
label: tracking?.currentLocation
|
|
90
|
+
label: tracking?.currentLocation?.address || 'Current position',
|
|
91
91
|
}
|
|
92
92
|
: null;
|
|
93
93
|
|
|
@@ -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')}
|
|
@@ -222,7 +227,7 @@ export default function TrackingPage() {
|
|
|
222
227
|
{t('movethewheels.tracking.details.lastUpdated', 'Last Updated')}
|
|
223
228
|
</p>
|
|
224
229
|
<p className="font-medium">
|
|
225
|
-
{tracking
|
|
230
|
+
{tracking?.lastUpdated
|
|
226
231
|
? formatDate(tracking.lastUpdated, 'relative')
|
|
227
232
|
: t('movethewheels.tracking.details.noUpdates', 'No tracking updates yet')}
|
|
228
233
|
</p>
|
|
@@ -416,7 +421,9 @@ export default function TrackingPage() {
|
|
|
416
421
|
<div>
|
|
417
422
|
<p className="text-sm font-medium">{alert.message}</p>
|
|
418
423
|
<p className="text-xs text-text-secondary">
|
|
419
|
-
{
|
|
424
|
+
{alert.timestamp
|
|
425
|
+
? formatDate(alert.timestamp, 'relative')
|
|
426
|
+
: t('movethewheels.tracking.alerts.timeUnknown', 'Time not recorded')}
|
|
420
427
|
{alert.resolvedAt &&
|
|
421
428
|
` - ${t('movethewheels.tracking.alerts.resolved', 'Resolved')}`}
|
|
422
429
|
</p>
|
|
@@ -456,18 +463,31 @@ export default function TrackingPage() {
|
|
|
456
463
|
</p>
|
|
457
464
|
</div>
|
|
458
465
|
)}
|
|
459
|
-
{tracking
|
|
466
|
+
{tracking?.currentLocation ? (
|
|
460
467
|
<>
|
|
461
468
|
<p className="text-sm">
|
|
462
469
|
{tracking.currentLocation.address ||
|
|
463
470
|
t('movethewheels.tracking.location.updating', 'Location updating...')}
|
|
464
471
|
</p>
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
472
|
+
{tracking.currentLocation.timestamp ? (
|
|
473
|
+
<p className="text-xs text-text-secondary mt-1">
|
|
474
|
+
{t('movethewheels.tracking.location.updated', 'Updated')}{' '}
|
|
475
|
+
{formatDate(tracking.currentLocation.timestamp, 'relative')}
|
|
476
|
+
</p>
|
|
477
|
+
) : (
|
|
478
|
+
<p className="text-xs text-text-secondary mt-1">
|
|
479
|
+
{t(
|
|
480
|
+
'movethewheels.tracking.location.timeUnknown',
|
|
481
|
+
'Update time not recorded'
|
|
482
|
+
)}
|
|
483
|
+
</p>
|
|
484
|
+
)}
|
|
469
485
|
</>
|
|
470
|
-
)
|
|
486
|
+
) : tracking ? (
|
|
487
|
+
<p className="text-sm text-text-secondary">
|
|
488
|
+
{t('movethewheels.tracking.location.unknown', 'Location not recorded')}
|
|
489
|
+
</p>
|
|
490
|
+
) : null}
|
|
471
491
|
</CardContent>
|
|
472
492
|
</Card>
|
|
473
493
|
|