@burdenoff/microfe-movethewheels 2026.925.1 → 2026.925.3
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/StatusBadge.js +31 -31
- package/dist/constants/index.js +19 -5
- package/dist/hooks/useCustomers.js +0 -3
- package/dist/hooks/useFleet.js +197 -175
- package/dist/hooks/useInventory.js +144 -145
- package/dist/hooks/useRoutes.js +5 -14
- package/dist/movethewheels/components/StatusBadge.d.ts +3 -1
- package/dist/movethewheels/constants/index.d.ts +2 -0
- package/dist/movethewheels/hooks/useFleet.d.ts +53 -1
- package/dist/movethewheels/hooks/useInventory.d.ts +10 -0
- package/dist/movethewheels/types/index.d.ts +29 -108
- package/dist/node_modules/lucide-react/dist/esm/icons/sticky-note.js +11 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/user-x.js +30 -0
- package/dist/pages/CustomerDetailsPage.js +666 -344
- package/dist/pages/CustomersPage.js +71 -80
- package/dist/pages/DriverDetailsPage.js +702 -421
- package/dist/pages/DriversPage.js +106 -117
- package/dist/pages/FleetPage.js +5 -13
- package/dist/pages/InventoryPage.js +4 -4
- package/dist/pages/OrderDetailsPage.js +279 -274
- package/dist/pages/RouteDetailsPage.js +300 -263
- package/dist/pages/RoutesPage.js +130 -67
- package/dist/pages/VehicleDetailsPage.js +562 -287
- package/dist/pages/VehiclesPage.js +119 -70
- package/dist/pages/WarehouseDetailsPage.js +410 -370
- package/dist/pages/WarehousesPage.js +131 -82
- package/package.json +1 -1
- package/src/movethewheels/components/StatusBadge.tsx +16 -2
- package/src/movethewheels/constants/index.ts +21 -0
- package/src/movethewheels/hooks/useCustomers.ts +0 -3
- package/src/movethewheels/hooks/useFleet.ts +236 -62
- package/src/movethewheels/hooks/useInventory.ts +76 -32
- package/src/movethewheels/hooks/useRoutes.ts +17 -12
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +376 -109
- package/src/movethewheels/pages/CustomersPage.tsx +6 -15
- package/src/movethewheels/pages/DriverDetailsPage.tsx +516 -156
- package/src/movethewheels/pages/DriversPage.tsx +28 -11
- package/src/movethewheels/pages/FleetPage.tsx +9 -11
- package/src/movethewheels/pages/InventoryPage.tsx +14 -5
- package/src/movethewheels/pages/OrderDetailsPage.tsx +12 -7
- package/src/movethewheels/pages/RouteDetailsPage.tsx +89 -41
- package/src/movethewheels/pages/RoutesPage.tsx +66 -2
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +282 -20
- package/src/movethewheels/pages/VehiclesPage.tsx +68 -3
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +213 -135
- package/src/movethewheels/pages/WarehousesPage.tsx +77 -16
- package/src/movethewheels/types/index.ts +30 -108
|
@@ -4,7 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
import { useEffect, useState } from 'react';
|
|
6
6
|
import { useParams, useLocation } from 'react-router';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
Truck,
|
|
9
|
+
ArrowLeft,
|
|
10
|
+
MapPin,
|
|
11
|
+
Fuel,
|
|
12
|
+
UserRound,
|
|
13
|
+
Edit,
|
|
14
|
+
Save,
|
|
15
|
+
Mail,
|
|
16
|
+
Phone,
|
|
17
|
+
Trash2,
|
|
18
|
+
} from 'lucide-react';
|
|
8
19
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
9
20
|
import { StatusBadge, VehicleTypeBadge } from '../components/StatusBadge';
|
|
10
21
|
import {
|
|
@@ -24,8 +35,20 @@ import {
|
|
|
24
35
|
Avatar,
|
|
25
36
|
AvatarFallback,
|
|
26
37
|
} from '../components/ui';
|
|
27
|
-
import {
|
|
28
|
-
|
|
38
|
+
import {
|
|
39
|
+
EmphasisPanel,
|
|
40
|
+
NextSteps,
|
|
41
|
+
PagePurpose,
|
|
42
|
+
ResponsiveDialog,
|
|
43
|
+
ResponsiveDialogContent,
|
|
44
|
+
ResponsiveDialogHeader,
|
|
45
|
+
ResponsiveDialogFooter,
|
|
46
|
+
ResponsiveDialogTitle,
|
|
47
|
+
ResponsiveDialogDescription,
|
|
48
|
+
} from '@burdenoff/fe-libs/ui';
|
|
49
|
+
import { useVehicle, useVehicleDrivers, useFleet, toApiVehicleStatus } from '../hooks/useFleet';
|
|
50
|
+
import type { VehicleStatus } from '../types';
|
|
51
|
+
import { VEHICLE_STATUS_LABELS } from '../constants';
|
|
29
52
|
import { useRecordVehicleService } from '../hooks/useFleetIntelligence';
|
|
30
53
|
import { VehicleServiceCard } from '../components/VehicleServiceCard';
|
|
31
54
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
@@ -47,9 +70,19 @@ const VEHICLE_TYPE_OPTIONS = [
|
|
|
47
70
|
{ value: 'DRONE', label: 'Drone' },
|
|
48
71
|
];
|
|
49
72
|
|
|
73
|
+
const VEHICLE_STATUS_OPTIONS: Array<{ value: VehicleStatus; label: string }> = (
|
|
74
|
+
Object.entries(VEHICLE_STATUS_LABELS) as Array<[VehicleStatus, string]>
|
|
75
|
+
).map(([value, label]) => ({ value, label }));
|
|
76
|
+
|
|
77
|
+
const OWNERSHIP_TYPE_OPTIONS = [
|
|
78
|
+
{ value: 'owned', label: 'Owned' },
|
|
79
|
+
{ value: 'leased', label: 'Leased' },
|
|
80
|
+
{ value: 'rented', label: 'Rented' },
|
|
81
|
+
];
|
|
82
|
+
|
|
50
83
|
function NewVehicleForm({ editId }: { editId?: string }) {
|
|
51
84
|
const { basePath, navigate } = useMoveTheWheels();
|
|
52
|
-
const { createVehicle, updateVehicle } = useFleet();
|
|
85
|
+
const { createVehicle, updateVehicle, deleteVehicle } = useFleet();
|
|
53
86
|
const { vehicle } = useVehicle(editId);
|
|
54
87
|
const isEditing = !!editId;
|
|
55
88
|
const [form, setForm] = useState({
|
|
@@ -58,9 +91,20 @@ function NewVehicleForm({ editId }: { editId?: string }) {
|
|
|
58
91
|
make: '',
|
|
59
92
|
model: '',
|
|
60
93
|
licensePlate: '',
|
|
94
|
+
ownershipType: '',
|
|
95
|
+
year: '',
|
|
96
|
+
status: 'available' as VehicleStatus,
|
|
97
|
+
fuelType: '',
|
|
98
|
+
fuelEfficiencyLPer100Km: '',
|
|
99
|
+
capacityWeight: '',
|
|
100
|
+
capacityVolume: '',
|
|
101
|
+
capacityPallets: '',
|
|
61
102
|
});
|
|
62
103
|
const [submitting, setSubmitting] = useState(false);
|
|
63
104
|
const [error, setError] = useState<string | null>(null);
|
|
105
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
106
|
+
const [deleting, setDeleting] = useState(false);
|
|
107
|
+
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
64
108
|
|
|
65
109
|
useEffect(() => {
|
|
66
110
|
if (editId && vehicle) {
|
|
@@ -70,6 +114,17 @@ function NewVehicleForm({ editId }: { editId?: string }) {
|
|
|
70
114
|
make: vehicle.make ?? '',
|
|
71
115
|
model: vehicle.model ?? '',
|
|
72
116
|
licensePlate: vehicle.licensePlate ?? '',
|
|
117
|
+
ownershipType: vehicle.ownershipType ?? '',
|
|
118
|
+
year: vehicle.year ? String(vehicle.year) : '',
|
|
119
|
+
status: vehicle.status,
|
|
120
|
+
fuelType: vehicle.fuelType ?? '',
|
|
121
|
+
fuelEfficiencyLPer100Km:
|
|
122
|
+
vehicle.fuelEfficiencyLPer100Km !== undefined
|
|
123
|
+
? String(vehicle.fuelEfficiencyLPer100Km)
|
|
124
|
+
: '',
|
|
125
|
+
capacityWeight: vehicle.capacity.weight ? String(vehicle.capacity.weight) : '',
|
|
126
|
+
capacityVolume: vehicle.capacity.volume ? String(vehicle.capacity.volume) : '',
|
|
127
|
+
capacityPallets: vehicle.capacity.pallets ? String(vehicle.capacity.pallets) : '',
|
|
73
128
|
});
|
|
74
129
|
}
|
|
75
130
|
}, [editId, vehicle]);
|
|
@@ -85,23 +140,33 @@ function NewVehicleForm({ editId }: { editId?: string }) {
|
|
|
85
140
|
setSubmitting(true);
|
|
86
141
|
setError(null);
|
|
87
142
|
try {
|
|
143
|
+
const payload = {
|
|
144
|
+
vehicleNumber: form.vehicleNumber.trim(),
|
|
145
|
+
type: form.type,
|
|
146
|
+
make: form.make.trim() || undefined,
|
|
147
|
+
model: form.model.trim() || undefined,
|
|
148
|
+
licensePlate: form.licensePlate.trim() || undefined,
|
|
149
|
+
ownershipType: form.ownershipType || undefined,
|
|
150
|
+
year: form.year ? Number(form.year) : undefined,
|
|
151
|
+
status: toApiVehicleStatus(form.status),
|
|
152
|
+
fuelType: form.fuelType || undefined,
|
|
153
|
+
fuelEfficiencyLPer100Km: form.fuelEfficiencyLPer100Km
|
|
154
|
+
? Number(form.fuelEfficiencyLPer100Km)
|
|
155
|
+
: undefined,
|
|
156
|
+
capacity:
|
|
157
|
+
form.capacityWeight || form.capacityVolume || form.capacityPallets
|
|
158
|
+
? {
|
|
159
|
+
weight: form.capacityWeight ? Number(form.capacityWeight) : undefined,
|
|
160
|
+
volume: form.capacityVolume ? Number(form.capacityVolume) : undefined,
|
|
161
|
+
pallets: form.capacityPallets ? Number(form.capacityPallets) : undefined,
|
|
162
|
+
}
|
|
163
|
+
: undefined,
|
|
164
|
+
};
|
|
88
165
|
if (editId) {
|
|
89
|
-
const updated = await updateVehicle(editId,
|
|
90
|
-
vehicleNumber: form.vehicleNumber.trim(),
|
|
91
|
-
type: form.type,
|
|
92
|
-
make: form.make.trim() || undefined,
|
|
93
|
-
model: form.model.trim() || undefined,
|
|
94
|
-
licensePlate: form.licensePlate.trim() || undefined,
|
|
95
|
-
});
|
|
166
|
+
const updated = await updateVehicle(editId, payload);
|
|
96
167
|
navigate?.(joinPath(basePath, `fleet/vehicles/${updated.id}`));
|
|
97
168
|
} else {
|
|
98
|
-
const created = await createVehicle(
|
|
99
|
-
vehicleNumber: form.vehicleNumber.trim(),
|
|
100
|
-
type: form.type,
|
|
101
|
-
make: form.make.trim() || undefined,
|
|
102
|
-
model: form.model.trim() || undefined,
|
|
103
|
-
licensePlate: form.licensePlate.trim() || undefined,
|
|
104
|
-
});
|
|
169
|
+
const created = await createVehicle(payload);
|
|
105
170
|
navigate?.(joinPath(basePath, `fleet/vehicles/${created.id}`));
|
|
106
171
|
}
|
|
107
172
|
} catch (err) {
|
|
@@ -111,6 +176,21 @@ function NewVehicleForm({ editId }: { editId?: string }) {
|
|
|
111
176
|
}
|
|
112
177
|
};
|
|
113
178
|
|
|
179
|
+
const handleDelete = async () => {
|
|
180
|
+
if (!editId) return;
|
|
181
|
+
setDeleting(true);
|
|
182
|
+
setDeleteError(null);
|
|
183
|
+
try {
|
|
184
|
+
await deleteVehicle(editId);
|
|
185
|
+
setDeleteOpen(false);
|
|
186
|
+
navigate?.(joinPath(basePath, 'fleet/vehicles'));
|
|
187
|
+
} catch (err) {
|
|
188
|
+
setDeleteError(err instanceof Error ? err.message : 'Failed to delete vehicle.');
|
|
189
|
+
} finally {
|
|
190
|
+
setDeleting(false);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
114
194
|
return (
|
|
115
195
|
<PageLayout
|
|
116
196
|
title={isEditing ? 'Edit Vehicle' : 'Add Vehicle'}
|
|
@@ -207,9 +287,175 @@ function NewVehicleForm({ editId }: { editId?: string }) {
|
|
|
207
287
|
/>
|
|
208
288
|
</div>
|
|
209
289
|
</div>
|
|
290
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
291
|
+
<div className="space-y-2">
|
|
292
|
+
<Label htmlFor="ownershipType">Ownership</Label>
|
|
293
|
+
<Select
|
|
294
|
+
value={form.ownershipType || undefined}
|
|
295
|
+
onValueChange={(value) => setForm((prev) => ({ ...prev, ownershipType: value }))}
|
|
296
|
+
>
|
|
297
|
+
<SelectTrigger id="ownershipType">
|
|
298
|
+
<SelectValue placeholder="Select ownership" />
|
|
299
|
+
</SelectTrigger>
|
|
300
|
+
<SelectContent>
|
|
301
|
+
{OWNERSHIP_TYPE_OPTIONS.map((option) => (
|
|
302
|
+
<SelectItem key={option.value} value={option.value}>
|
|
303
|
+
{option.label}
|
|
304
|
+
</SelectItem>
|
|
305
|
+
))}
|
|
306
|
+
</SelectContent>
|
|
307
|
+
</Select>
|
|
308
|
+
</div>
|
|
309
|
+
<div className="space-y-2">
|
|
310
|
+
<Label htmlFor="year">Year</Label>
|
|
311
|
+
<Input
|
|
312
|
+
id="year"
|
|
313
|
+
type="number"
|
|
314
|
+
min={1980}
|
|
315
|
+
max={new Date().getFullYear() + 1}
|
|
316
|
+
value={form.year}
|
|
317
|
+
onChange={(e) => setForm((prev) => ({ ...prev, year: e.target.value }))}
|
|
318
|
+
placeholder={String(new Date().getFullYear())}
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
321
|
+
<div className="space-y-2">
|
|
322
|
+
<Label htmlFor="vehicleStatus">Status</Label>
|
|
323
|
+
<Select
|
|
324
|
+
value={form.status}
|
|
325
|
+
onValueChange={(value) =>
|
|
326
|
+
setForm((prev) => ({ ...prev, status: value as VehicleStatus }))
|
|
327
|
+
}
|
|
328
|
+
>
|
|
329
|
+
<SelectTrigger id="vehicleStatus">
|
|
330
|
+
<SelectValue />
|
|
331
|
+
</SelectTrigger>
|
|
332
|
+
<SelectContent>
|
|
333
|
+
{VEHICLE_STATUS_OPTIONS.map((option) => (
|
|
334
|
+
<SelectItem key={option.value} value={option.value}>
|
|
335
|
+
{option.label}
|
|
336
|
+
</SelectItem>
|
|
337
|
+
))}
|
|
338
|
+
</SelectContent>
|
|
339
|
+
</Select>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
343
|
+
<div className="space-y-2">
|
|
344
|
+
<Label htmlFor="fuelType">Fuel Type</Label>
|
|
345
|
+
<Input
|
|
346
|
+
id="fuelType"
|
|
347
|
+
value={form.fuelType}
|
|
348
|
+
onChange={(e) => setForm((prev) => ({ ...prev, fuelType: e.target.value }))}
|
|
349
|
+
placeholder="Diesel"
|
|
350
|
+
/>
|
|
351
|
+
</div>
|
|
352
|
+
<div className="space-y-2">
|
|
353
|
+
<Label htmlFor="fuelEfficiency">Fuel Efficiency (L/100km)</Label>
|
|
354
|
+
<Input
|
|
355
|
+
id="fuelEfficiency"
|
|
356
|
+
type="number"
|
|
357
|
+
min={0}
|
|
358
|
+
step={0.1}
|
|
359
|
+
value={form.fuelEfficiencyLPer100Km}
|
|
360
|
+
onChange={(e) =>
|
|
361
|
+
setForm((prev) => ({ ...prev, fuelEfficiencyLPer100Km: e.target.value }))
|
|
362
|
+
}
|
|
363
|
+
placeholder="12.5"
|
|
364
|
+
/>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
368
|
+
<div className="space-y-2">
|
|
369
|
+
<Label htmlFor="capacityWeight">Capacity — Weight (kg)</Label>
|
|
370
|
+
<Input
|
|
371
|
+
id="capacityWeight"
|
|
372
|
+
type="number"
|
|
373
|
+
min={0}
|
|
374
|
+
value={form.capacityWeight}
|
|
375
|
+
onChange={(e) => setForm((prev) => ({ ...prev, capacityWeight: e.target.value }))}
|
|
376
|
+
placeholder="5000"
|
|
377
|
+
/>
|
|
378
|
+
</div>
|
|
379
|
+
<div className="space-y-2">
|
|
380
|
+
<Label htmlFor="capacityVolume">Capacity — Volume (m³)</Label>
|
|
381
|
+
<Input
|
|
382
|
+
id="capacityVolume"
|
|
383
|
+
type="number"
|
|
384
|
+
min={0}
|
|
385
|
+
step={0.1}
|
|
386
|
+
value={form.capacityVolume}
|
|
387
|
+
onChange={(e) => setForm((prev) => ({ ...prev, capacityVolume: e.target.value }))}
|
|
388
|
+
placeholder="18"
|
|
389
|
+
/>
|
|
390
|
+
</div>
|
|
391
|
+
<div className="space-y-2">
|
|
392
|
+
<Label htmlFor="capacityPallets">Capacity — Pallets</Label>
|
|
393
|
+
<Input
|
|
394
|
+
id="capacityPallets"
|
|
395
|
+
type="number"
|
|
396
|
+
min={0}
|
|
397
|
+
value={form.capacityPallets}
|
|
398
|
+
onChange={(e) =>
|
|
399
|
+
setForm((prev) => ({ ...prev, capacityPallets: e.target.value }))
|
|
400
|
+
}
|
|
401
|
+
placeholder="12"
|
|
402
|
+
/>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
210
405
|
</div>
|
|
211
406
|
</EmphasisPanel>
|
|
407
|
+
|
|
408
|
+
{isEditing && (
|
|
409
|
+
<Card className="border-status-error-border">
|
|
410
|
+
<CardHeader>
|
|
411
|
+
<CardTitle className="flex items-center gap-2 text-status-error-text">
|
|
412
|
+
<Trash2 size={18} /> Delete Vehicle
|
|
413
|
+
</CardTitle>
|
|
414
|
+
</CardHeader>
|
|
415
|
+
<CardContent>
|
|
416
|
+
<p className="mb-3 text-sm text-text-secondary">
|
|
417
|
+
Removes this vehicle from the active fleet. It can no longer be assigned to drivers,
|
|
418
|
+
orders, or routes. This cannot be undone from here.
|
|
419
|
+
</p>
|
|
420
|
+
<Button
|
|
421
|
+
type="button"
|
|
422
|
+
variant="outline"
|
|
423
|
+
className="border-status-error-border text-status-error-text hover:bg-status-error-bg-subtle"
|
|
424
|
+
onClick={() => setDeleteOpen(true)}
|
|
425
|
+
>
|
|
426
|
+
<Trash2 size={16} /> Delete this vehicle
|
|
427
|
+
</Button>
|
|
428
|
+
</CardContent>
|
|
429
|
+
</Card>
|
|
430
|
+
)}
|
|
212
431
|
</form>
|
|
432
|
+
|
|
433
|
+
<ResponsiveDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
|
434
|
+
<ResponsiveDialogContent>
|
|
435
|
+
<ResponsiveDialogHeader>
|
|
436
|
+
<ResponsiveDialogTitle>Delete this vehicle?</ResponsiveDialogTitle>
|
|
437
|
+
<ResponsiveDialogDescription>
|
|
438
|
+
This removes {form.vehicleNumber || 'this vehicle'} from the active fleet. It cannot
|
|
439
|
+
be undone from here.
|
|
440
|
+
</ResponsiveDialogDescription>
|
|
441
|
+
</ResponsiveDialogHeader>
|
|
442
|
+
<div className="px-1">
|
|
443
|
+
<FormError message={deleteError} />
|
|
444
|
+
</div>
|
|
445
|
+
<ResponsiveDialogFooter>
|
|
446
|
+
<Button variant="outline" onClick={() => setDeleteOpen(false)} disabled={deleting}>
|
|
447
|
+
Cancel
|
|
448
|
+
</Button>
|
|
449
|
+
<Button
|
|
450
|
+
className="bg-status-error-text text-text-inverse hover:opacity-90"
|
|
451
|
+
onClick={handleDelete}
|
|
452
|
+
disabled={deleting}
|
|
453
|
+
>
|
|
454
|
+
{deleting ? 'Deleting...' : 'Delete vehicle'}
|
|
455
|
+
</Button>
|
|
456
|
+
</ResponsiveDialogFooter>
|
|
457
|
+
</ResponsiveDialogContent>
|
|
458
|
+
</ResponsiveDialog>
|
|
213
459
|
</PageLayout>
|
|
214
460
|
);
|
|
215
461
|
}
|
|
@@ -245,7 +491,7 @@ export default function VehicleDetailsPage() {
|
|
|
245
491
|
return (
|
|
246
492
|
<PageLayout
|
|
247
493
|
title={vehicle.vehicleNumber}
|
|
248
|
-
subtitle={`${vehicle.make} ${vehicle.model} (${vehicle.year})`}
|
|
494
|
+
subtitle={`${vehicle.make} ${vehicle.model}${vehicle.year ? ` (${vehicle.year})` : ''}`}
|
|
249
495
|
icon={<Truck size={24} />}
|
|
250
496
|
headerActions={
|
|
251
497
|
<div className="flex gap-2">
|
|
@@ -322,7 +568,7 @@ export default function VehicleDetailsPage() {
|
|
|
322
568
|
</div>
|
|
323
569
|
<div>
|
|
324
570
|
<p className="text-sm text-text-secondary">Year</p>
|
|
325
|
-
<p className="font-medium">{vehicle.year}</p>
|
|
571
|
+
<p className="font-medium">{vehicle.year ?? '—'}</p>
|
|
326
572
|
</div>
|
|
327
573
|
<div>
|
|
328
574
|
<p className="text-sm text-text-secondary">License Plate</p>
|
|
@@ -394,6 +640,22 @@ export default function VehicleDetailsPage() {
|
|
|
394
640
|
</p>
|
|
395
641
|
</div>
|
|
396
642
|
)}
|
|
643
|
+
{(vehicle.fuelType || vehicle.fuelEfficiencyLPer100Km !== undefined) && (
|
|
644
|
+
<div className="grid grid-cols-2 gap-4">
|
|
645
|
+
{vehicle.fuelType && (
|
|
646
|
+
<div>
|
|
647
|
+
<p className="text-sm text-text-secondary">Fuel Type</p>
|
|
648
|
+
<p className="font-medium">{vehicle.fuelType}</p>
|
|
649
|
+
</div>
|
|
650
|
+
)}
|
|
651
|
+
{vehicle.fuelEfficiencyLPer100Km !== undefined && (
|
|
652
|
+
<div>
|
|
653
|
+
<p className="text-sm text-text-secondary">Efficiency</p>
|
|
654
|
+
<p className="font-medium">{vehicle.fuelEfficiencyLPer100Km} L/100km</p>
|
|
655
|
+
</div>
|
|
656
|
+
)}
|
|
657
|
+
</div>
|
|
658
|
+
)}
|
|
397
659
|
</CardContent>
|
|
398
660
|
</Card>
|
|
399
661
|
|
|
@@ -11,24 +11,44 @@ import {
|
|
|
11
11
|
CircleCheck,
|
|
12
12
|
Route as RouteIcon,
|
|
13
13
|
Wrench,
|
|
14
|
+
Filter,
|
|
14
15
|
} from 'lucide-react';
|
|
15
16
|
import { PageLayout } from '../components/PageLayout';
|
|
16
17
|
import { MetricCard, MetricGrid } from '../components/MetricCard';
|
|
17
18
|
import { DataTable, type Column } from '../components/DataTable';
|
|
18
19
|
import { StatusBadge, VehicleTypeBadge } from '../components/StatusBadge';
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
Progress,
|
|
22
|
+
Card,
|
|
23
|
+
CardContent,
|
|
24
|
+
Select,
|
|
25
|
+
SelectContent,
|
|
26
|
+
SelectItem,
|
|
27
|
+
SelectTrigger,
|
|
28
|
+
SelectValue,
|
|
29
|
+
Button,
|
|
30
|
+
} from '../components/ui';
|
|
20
31
|
import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
21
32
|
import { useVehicles, useFleetStats } from '../hooks/useFleet';
|
|
22
33
|
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
23
34
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
24
35
|
import { joinPath } from '../utils/navigation';
|
|
25
|
-
import
|
|
36
|
+
import { VEHICLE_STATUS_LABELS } from '../constants';
|
|
37
|
+
import type { Vehicle, VehicleStatus } from '../types';
|
|
38
|
+
|
|
39
|
+
const STATUS_OPTIONS: Array<{ value: VehicleStatus | 'all'; label: string }> = [
|
|
40
|
+
{ value: 'all', label: 'All statuses' },
|
|
41
|
+
...(Object.entries(VEHICLE_STATUS_LABELS) as Array<[VehicleStatus, string]>).map(
|
|
42
|
+
([value, label]) => ({ value, label })
|
|
43
|
+
),
|
|
44
|
+
];
|
|
26
45
|
|
|
27
46
|
export default function VehiclesPage() {
|
|
28
47
|
const { basePath, navigate } = useMoveTheWheels();
|
|
29
48
|
const [page, setPage] = useState(0);
|
|
30
49
|
const [pageSize, setPageSize] = useState(25);
|
|
31
50
|
const [search, setSearch] = useState('');
|
|
51
|
+
const [statusFilter, setStatusFilter] = useState<VehicleStatus | 'all'>('all');
|
|
32
52
|
const debouncedSearch = useDebouncedValue(search);
|
|
33
53
|
const {
|
|
34
54
|
vehicles,
|
|
@@ -41,7 +61,9 @@ export default function VehiclesPage() {
|
|
|
41
61
|
limit: pageSize,
|
|
42
62
|
offset: page * pageSize,
|
|
43
63
|
search: debouncedSearch || undefined,
|
|
64
|
+
status: statusFilter === 'all' ? undefined : statusFilter,
|
|
44
65
|
});
|
|
66
|
+
const filtersActive = statusFilter !== 'all' || search.length > 0;
|
|
45
67
|
// Fleet totals + tiles from the live aggregate, not the current page.
|
|
46
68
|
const { stats } = useFleetStats();
|
|
47
69
|
const totalVehicles = stats.totalVehicles;
|
|
@@ -74,7 +96,7 @@ export default function VehiclesPage() {
|
|
|
74
96
|
<p className="font-medium">
|
|
75
97
|
{row.make} {row.model}
|
|
76
98
|
</p>
|
|
77
|
-
<p className="text-sm text-text-secondary">{row.year}</p>
|
|
99
|
+
<p className="text-sm text-text-secondary">{row.year ?? '—'}</p>
|
|
78
100
|
</div>
|
|
79
101
|
),
|
|
80
102
|
},
|
|
@@ -178,6 +200,49 @@ export default function VehiclesPage() {
|
|
|
178
200
|
</MetricGrid>
|
|
179
201
|
</div>
|
|
180
202
|
|
|
203
|
+
{/* Filters */}
|
|
204
|
+
<Card>
|
|
205
|
+
<CardContent className="p-4">
|
|
206
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
207
|
+
<div className="flex items-center gap-2">
|
|
208
|
+
<Filter size={16} className="text-text-secondary" />
|
|
209
|
+
<span className="text-sm font-medium">Filters:</span>
|
|
210
|
+
</div>
|
|
211
|
+
<Select
|
|
212
|
+
value={statusFilter}
|
|
213
|
+
onValueChange={(value) => {
|
|
214
|
+
setStatusFilter(value as VehicleStatus | 'all');
|
|
215
|
+
setPage(0);
|
|
216
|
+
}}
|
|
217
|
+
>
|
|
218
|
+
<SelectTrigger className="w-[180px]">
|
|
219
|
+
<SelectValue placeholder="Status" />
|
|
220
|
+
</SelectTrigger>
|
|
221
|
+
<SelectContent>
|
|
222
|
+
{STATUS_OPTIONS.map((option) => (
|
|
223
|
+
<SelectItem key={option.value} value={option.value}>
|
|
224
|
+
{option.label}
|
|
225
|
+
</SelectItem>
|
|
226
|
+
))}
|
|
227
|
+
</SelectContent>
|
|
228
|
+
</Select>
|
|
229
|
+
{filtersActive && (
|
|
230
|
+
<Button
|
|
231
|
+
variant="ghost"
|
|
232
|
+
size="sm"
|
|
233
|
+
onClick={() => {
|
|
234
|
+
setStatusFilter('all');
|
|
235
|
+
setSearch('');
|
|
236
|
+
setPage(0);
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
Clear filters
|
|
240
|
+
</Button>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
243
|
+
</CardContent>
|
|
244
|
+
</Card>
|
|
245
|
+
|
|
181
246
|
<DataTable
|
|
182
247
|
columns={columns}
|
|
183
248
|
data={vehicles}
|