@burdenoff/microfe-movethewheels 2026.929.3 → 2026.929.4
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/generated/wspace-types.d.ts +1547 -64
- package/dist/hooks/useCustomers.js +3 -9
- package/dist/hooks/useFleet.js +146 -149
- package/dist/hooks/useInventory.js +178 -153
- package/dist/hooks/useRoutes.js +9 -9
- package/dist/hooks/useTracking.js +14 -16
- package/dist/movethewheels/hooks/useFleet.d.ts +9 -2
- package/dist/movethewheels/hooks/useInventory.d.ts +13 -12
- package/dist/movethewheels/hooks/useRoutes.d.ts +4 -1
- package/dist/movethewheels/types/index.d.ts +33 -23
- package/dist/movethewheels/utils/formatters.d.ts +1 -1
- 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 +7 -3
- package/dist/pages/DriversPage.js +4 -4
- package/dist/pages/FleetPage.js +1 -1
- package/dist/pages/InventoryPage.js +11 -12
- package/dist/pages/LiveTrackingPage.js +4 -4
- package/dist/pages/ProductDetailsPage.js +408 -249
- package/dist/pages/ProductsPage.js +5 -5
- package/dist/pages/RouteDetailsPage.js +8 -10
- package/dist/pages/RoutesPage.js +2 -2
- package/dist/pages/TrackingPage.js +14 -8
- package/dist/pages/WarehouseDetailsPage.js +166 -163
- package/dist/pages/WarehousesPage.js +29 -21
- package/package.json +1 -1
- package/src/generated/wspace-types.ts +1762 -64
- package/src/movethewheels/hooks/useCustomers.ts +7 -9
- package/src/movethewheels/hooks/useFleet.ts +89 -31
- package/src/movethewheels/hooks/useInventory.ts +143 -49
- package/src/movethewheels/hooks/useRoutes.ts +36 -16
- package/src/movethewheels/hooks/useTracking.ts +23 -15
- 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 +13 -3
- package/src/movethewheels/pages/DriversPage.tsx +1 -1
- 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/ProductDetailsPage.tsx +228 -49
- package/src/movethewheels/pages/ProductsPage.tsx +13 -5
- package/src/movethewheels/pages/RouteDetailsPage.tsx +16 -8
- package/src/movethewheels/pages/RoutesPage.tsx +4 -2
- package/src/movethewheels/pages/TrackingPage.tsx +25 -10
- 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
|
@@ -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',
|
|
@@ -195,16 +195,14 @@ function NewRouteForm({ editId }: { editId?: string }) {
|
|
|
195
195
|
country: '',
|
|
196
196
|
},
|
|
197
197
|
type: stop.type,
|
|
198
|
-
scheduledTime: new Date(),
|
|
199
|
-
duration: 0,
|
|
200
198
|
status: 'pending',
|
|
201
199
|
}));
|
|
202
200
|
if (isEditing) {
|
|
203
201
|
const updated = await updateRoute(editId, {
|
|
204
202
|
name: form.name.trim(),
|
|
205
203
|
status: form.status,
|
|
206
|
-
vehicleId: form.vehicleId ||
|
|
207
|
-
driverId: form.driverId ||
|
|
204
|
+
vehicleId: form.vehicleId || null,
|
|
205
|
+
driverId: form.driverId || null,
|
|
208
206
|
stops,
|
|
209
207
|
});
|
|
210
208
|
navigate?.(joinPath(basePath, `routes/${updated.id}`));
|
|
@@ -542,8 +540,12 @@ export default function RouteDetailsPage() {
|
|
|
542
540
|
headerContent={
|
|
543
541
|
<div className="flex gap-2">
|
|
544
542
|
<StatusBadge status={route.status} customColors={ROUTE_STATUS_COLORS[route.status]} />
|
|
545
|
-
<Badge variant={route.optimized ? 'default' : 'secondary'}>
|
|
546
|
-
{route.optimized
|
|
543
|
+
<Badge variant={route.optimized === true ? 'default' : 'secondary'}>
|
|
544
|
+
{route.optimized === undefined
|
|
545
|
+
? 'Optimization unknown'
|
|
546
|
+
: route.optimized
|
|
547
|
+
? 'Optimized'
|
|
548
|
+
: 'Not Optimized'}
|
|
547
549
|
</Badge>
|
|
548
550
|
</div>
|
|
549
551
|
}
|
|
@@ -668,14 +670,20 @@ export default function RouteDetailsPage() {
|
|
|
668
670
|
</div>
|
|
669
671
|
<div className="text-right">
|
|
670
672
|
<p className="text-sm">
|
|
671
|
-
|
|
673
|
+
{stop.scheduledTime
|
|
674
|
+
? `Scheduled: ${formatDate(stop.scheduledTime, 'time')}`
|
|
675
|
+
: 'Scheduled: Not provided'}
|
|
672
676
|
</p>
|
|
673
677
|
{stop.actualTime && (
|
|
674
678
|
<p className="text-sm text-status-success-text">
|
|
675
679
|
Actual: {formatDate(stop.actualTime, 'time')}
|
|
676
680
|
</p>
|
|
677
681
|
)}
|
|
678
|
-
<p className="text-xs text-text-secondary">
|
|
682
|
+
<p className="text-xs text-text-secondary">
|
|
683
|
+
{stop.duration !== undefined
|
|
684
|
+
? `${stop.duration} min stop`
|
|
685
|
+
: 'Duration not provided'}
|
|
686
|
+
</p>
|
|
679
687
|
</div>
|
|
680
688
|
</div>
|
|
681
689
|
</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
|
|
|
@@ -222,7 +222,7 @@ export default function TrackingPage() {
|
|
|
222
222
|
{t('movethewheels.tracking.details.lastUpdated', 'Last Updated')}
|
|
223
223
|
</p>
|
|
224
224
|
<p className="font-medium">
|
|
225
|
-
{tracking
|
|
225
|
+
{tracking?.lastUpdated
|
|
226
226
|
? formatDate(tracking.lastUpdated, 'relative')
|
|
227
227
|
: t('movethewheels.tracking.details.noUpdates', 'No tracking updates yet')}
|
|
228
228
|
</p>
|
|
@@ -416,7 +416,9 @@ export default function TrackingPage() {
|
|
|
416
416
|
<div>
|
|
417
417
|
<p className="text-sm font-medium">{alert.message}</p>
|
|
418
418
|
<p className="text-xs text-text-secondary">
|
|
419
|
-
{
|
|
419
|
+
{alert.timestamp
|
|
420
|
+
? formatDate(alert.timestamp, 'relative')
|
|
421
|
+
: t('movethewheels.tracking.alerts.timeUnknown', 'Time not recorded')}
|
|
420
422
|
{alert.resolvedAt &&
|
|
421
423
|
` - ${t('movethewheels.tracking.alerts.resolved', 'Resolved')}`}
|
|
422
424
|
</p>
|
|
@@ -456,18 +458,31 @@ export default function TrackingPage() {
|
|
|
456
458
|
</p>
|
|
457
459
|
</div>
|
|
458
460
|
)}
|
|
459
|
-
{tracking
|
|
461
|
+
{tracking?.currentLocation ? (
|
|
460
462
|
<>
|
|
461
463
|
<p className="text-sm">
|
|
462
464
|
{tracking.currentLocation.address ||
|
|
463
465
|
t('movethewheels.tracking.location.updating', 'Location updating...')}
|
|
464
466
|
</p>
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
467
|
+
{tracking.currentLocation.timestamp ? (
|
|
468
|
+
<p className="text-xs text-text-secondary mt-1">
|
|
469
|
+
{t('movethewheels.tracking.location.updated', 'Updated')}{' '}
|
|
470
|
+
{formatDate(tracking.currentLocation.timestamp, 'relative')}
|
|
471
|
+
</p>
|
|
472
|
+
) : (
|
|
473
|
+
<p className="text-xs text-text-secondary mt-1">
|
|
474
|
+
{t(
|
|
475
|
+
'movethewheels.tracking.location.timeUnknown',
|
|
476
|
+
'Update time not recorded'
|
|
477
|
+
)}
|
|
478
|
+
</p>
|
|
479
|
+
)}
|
|
469
480
|
</>
|
|
470
|
-
)
|
|
481
|
+
) : tracking ? (
|
|
482
|
+
<p className="text-sm text-text-secondary">
|
|
483
|
+
{t('movethewheels.tracking.location.unknown', 'Location not recorded')}
|
|
484
|
+
</p>
|
|
485
|
+
) : null}
|
|
471
486
|
</CardContent>
|
|
472
487
|
</Card>
|
|
473
488
|
|