@burdenoff/microfe-movethewheels 2026.925.2 → 2026.929.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.
Files changed (43) hide show
  1. package/dist/components/StatusBadge.js +34 -31
  2. package/dist/constants/index.js +12 -5
  3. package/dist/hooks/useCustomers.js +0 -3
  4. package/dist/hooks/useFleet.js +148 -135
  5. package/dist/hooks/useInventory.js +146 -146
  6. package/dist/hooks/useRoutes.js +5 -14
  7. package/dist/movethewheels/components/StatusBadge.d.ts +3 -1
  8. package/dist/movethewheels/constants/index.d.ts +1 -0
  9. package/dist/movethewheels/hooks/useFleet.d.ts +17 -4
  10. package/dist/movethewheels/hooks/useInventory.d.ts +11 -0
  11. package/dist/movethewheels/types/index.d.ts +19 -75
  12. package/dist/pages/CustomerDetailsPage.js +666 -344
  13. package/dist/pages/CustomersPage.js +71 -80
  14. package/dist/pages/DriverDetailsPage.js +2 -2
  15. package/dist/pages/DriversPage.js +2 -2
  16. package/dist/pages/FleetPage.js +1 -3
  17. package/dist/pages/InventoryPage.js +5 -5
  18. package/dist/pages/RouteDetailsPage.js +273 -248
  19. package/dist/pages/RoutesPage.js +130 -67
  20. package/dist/pages/VehicleDetailsPage.js +527 -389
  21. package/dist/pages/VehiclesPage.js +120 -71
  22. package/dist/pages/WarehouseDetailsPage.js +411 -368
  23. package/dist/pages/WarehousesPage.js +132 -83
  24. package/package.json +1 -1
  25. package/src/movethewheels/components/StatusBadge.tsx +15 -7
  26. package/src/movethewheels/constants/index.ts +9 -0
  27. package/src/movethewheels/hooks/useCustomers.ts +0 -3
  28. package/src/movethewheels/hooks/useFleet.ts +89 -25
  29. package/src/movethewheels/hooks/useInventory.ts +77 -33
  30. package/src/movethewheels/hooks/useRoutes.ts +17 -12
  31. package/src/movethewheels/pages/CustomerDetailsPage.tsx +376 -109
  32. package/src/movethewheels/pages/CustomersPage.tsx +6 -15
  33. package/src/movethewheels/pages/DriverDetailsPage.tsx +3 -1
  34. package/src/movethewheels/pages/DriversPage.tsx +5 -1
  35. package/src/movethewheels/pages/FleetPage.tsx +2 -1
  36. package/src/movethewheels/pages/InventoryPage.tsx +18 -6
  37. package/src/movethewheels/pages/RouteDetailsPage.tsx +74 -34
  38. package/src/movethewheels/pages/RoutesPage.tsx +66 -2
  39. package/src/movethewheels/pages/VehicleDetailsPage.tsx +185 -23
  40. package/src/movethewheels/pages/VehiclesPage.tsx +72 -5
  41. package/src/movethewheels/pages/WarehouseDetailsPage.tsx +217 -138
  42. package/src/movethewheels/pages/WarehousesPage.tsx +81 -17
  43. package/src/movethewheels/types/index.ts +28 -75
@@ -141,7 +141,11 @@ export default function DriversPage() {
141
141
  cell: (row) => (
142
142
  <div className="text-sm">
143
143
  <p>{row.licenseType}</p>
144
- <p className="text-text-secondary">Exp: {formatDate(row.licenseExpiry, 'short')}</p>
144
+ <p className="text-text-secondary">
145
+ {row.licenseExpiry
146
+ ? `Exp: ${formatDate(row.licenseExpiry, 'short')}`
147
+ : 'Expiry not recorded'}
148
+ </p>
145
149
  </div>
146
150
  ),
147
151
  },
@@ -244,7 +244,8 @@ export default function FleetPage() {
244
244
  <VehicleTypeBadge type={vehicle.type} size="sm" />
245
245
  </div>
246
246
  <p className="text-sm text-text-secondary">
247
- {vehicle.make} {vehicle.model} ({vehicle.year})
247
+ {vehicle.make} {vehicle.model}
248
+ {vehicle.year ? ` (${vehicle.year})` : ''}
248
249
  </p>
249
250
  </div>
250
251
  </div>
@@ -53,7 +53,7 @@ interface StockRow {
53
53
  onHand: number;
54
54
  allocated: number;
55
55
  available: number;
56
- reorderLevel: number;
56
+ reorderLevel?: number;
57
57
  stockState: 'out_of_stock' | 'low' | 'in_stock';
58
58
  }
59
59
 
@@ -140,10 +140,15 @@ export default function InventoryPage() {
140
140
  allocated: item.reservedQuantity,
141
141
  available: item.availableQuantity,
142
142
  reorderLevel: item.reorderLevel,
143
+ // Matches the backend's lowStock rule (inventory.service.ts): only
144
+ // flag "low" when a reorder point is actually configured — items with
145
+ // none set are never considered low stock, even at 0 available,
146
+ // otherwise this table disagreed with the dashboard's low-stock count
147
+ // for the same item (BOFF-7909 follow-up).
143
148
  stockState:
144
149
  item.quantity === 0
145
150
  ? 'out_of_stock'
146
- : item.availableQuantity <= item.reorderLevel
151
+ : item.reorderLevel !== undefined && item.availableQuantity <= item.reorderLevel
147
152
  ? 'low'
148
153
  : 'in_stock',
149
154
  }));
@@ -196,7 +201,7 @@ export default function InventoryPage() {
196
201
  header: t('movethewheels.inventory.stock.reorderPoint', 'Reorder point'),
197
202
  accessorKey: 'reorderLevel',
198
203
  sortable: true,
199
- cell: (row) => (row.reorderLevel > 0 ? row.reorderLevel.toLocaleString() : '—'),
204
+ cell: (row) => (row.reorderLevel !== undefined ? row.reorderLevel.toLocaleString() : '—'),
200
205
  },
201
206
  {
202
207
  id: 'state',
@@ -501,8 +506,12 @@ export default function InventoryPage() {
501
506
  </p>
502
507
  </div>
503
508
  <div className="text-right">
504
- <Badge variant={warehouse.isActive ? 'default' : 'secondary'}>
505
- {warehouse.isActive
509
+ <Badge
510
+ variant={
511
+ warehouse.status.toUpperCase() === 'ACTIVE' ? 'default' : 'secondary'
512
+ }
513
+ >
514
+ {warehouse.status.toUpperCase() === 'ACTIVE'
506
515
  ? t('movethewheels.inventory.warehouses.active', 'Active')
507
516
  : t('movethewheels.inventory.warehouses.inactive', 'Inactive')}
508
517
  </Badge>
@@ -512,7 +521,10 @@ export default function InventoryPage() {
512
521
  </p>
513
522
  <Progress
514
523
  value={
515
- (warehouse.capacity.usedSpace / warehouse.capacity.totalSpace) * 100
524
+ warehouse.capacity.totalSpace > 0
525
+ ? (warehouse.capacity.usedSpace / warehouse.capacity.totalSpace) *
526
+ 100
527
+ : 0
516
528
  }
517
529
  className="w-24 h-2"
518
530
  />
@@ -37,7 +37,17 @@ import {
37
37
  SelectTrigger,
38
38
  SelectValue,
39
39
  } from '../components/ui';
40
- import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
40
+ import {
41
+ EmphasisPanel,
42
+ NextSteps,
43
+ PagePurpose,
44
+ ResponsiveDialog,
45
+ ResponsiveDialogContent,
46
+ ResponsiveDialogHeader,
47
+ ResponsiveDialogFooter,
48
+ ResponsiveDialogTitle,
49
+ ResponsiveDialogDescription,
50
+ } from '@burdenoff/fe-libs/ui';
41
51
  import { RouteMap, type RouteStop as MapRouteStop } from '../components/maps';
42
52
  import { firstRealLatLng } from '../utils/mapMarkers';
43
53
  import { EntityCombobox } from '../components/EntityCombobox';
@@ -425,7 +435,7 @@ export default function RouteDetailsPage() {
425
435
  const isEditRoute = useLocation().pathname.endsWith('/edit');
426
436
  const { basePath, navigate } = useMoveTheWheels();
427
437
  const { route, isLoading, error, refetch } = useRoute(routeId);
428
- const { optimizeRoute, updateRoute } = useRoutes();
438
+ const { optimizeRoute, updateRoute, deleteRoute } = useRoutes();
429
439
  const suggestions = useAssignmentSuggestions(routeId);
430
440
  const { t: translate } = useI18n();
431
441
  const t = (key: string, fallback: string): string => {
@@ -435,10 +445,27 @@ export default function RouteDetailsPage() {
435
445
  const { message: notice, notify } = useNotice();
436
446
  const [optimizeError, setOptimizeError] = useState<string | null>(null);
437
447
  const [optimizing, setOptimizing] = useState(false);
448
+ const [deleteOpen, setDeleteOpen] = useState(false);
449
+ const [deleting, setDeleting] = useState(false);
450
+ const [deleteError, setDeleteError] = useState<string | null>(null);
438
451
 
439
452
  const goBack = () => navigate?.(joinPath(basePath, 'routes'));
440
453
  const goTo = (path: string) => navigate?.(joinPath(basePath, path));
441
454
 
455
+ const handleDelete = async () => {
456
+ if (!routeId) return;
457
+ setDeleting(true);
458
+ setDeleteError(null);
459
+ try {
460
+ await deleteRoute(routeId);
461
+ setDeleteOpen(false);
462
+ goBack();
463
+ } catch (err) {
464
+ setDeleteError(err instanceof Error ? err.message : 'Failed to delete route.');
465
+ setDeleting(false);
466
+ }
467
+ };
468
+
442
469
  const handleOptimize = async () => {
443
470
  if (!routeId) return;
444
471
  setOptimizing(true);
@@ -717,18 +744,6 @@ export default function RouteDetailsPage() {
717
744
  </CardTitle>
718
745
  </CardHeader>
719
746
  <CardContent className="space-y-3">
720
- {route.startTime && (
721
- <div>
722
- <p className="text-sm text-text-secondary">Started</p>
723
- <p>{formatDate(route.startTime, 'full')}</p>
724
- </div>
725
- )}
726
- {route.endTime && (
727
- <div>
728
- <p className="text-sm text-text-secondary">Completed</p>
729
- <p>{formatDate(route.endTime, 'full')}</p>
730
- </div>
731
- )}
732
747
  <div>
733
748
  <p className="text-sm text-text-secondary">Created</p>
734
749
  <p>{formatDate(route.createdAt, 'full')}</p>
@@ -736,28 +751,53 @@ export default function RouteDetailsPage() {
736
751
  </CardContent>
737
752
  </Card>
738
753
 
739
- {route.trafficConditions.length > 0 && (
740
- <Card>
741
- <CardHeader>
742
- <CardTitle>Traffic Conditions</CardTitle>
743
- </CardHeader>
744
- <CardContent>
745
- {route.trafficConditions.map((tc, i) => (
746
- <div
747
- key={i}
748
- className={`p-2 rounded mb-2 ${tc.condition === 'severe' ? 'bg-status-error-bg-subtle' : tc.condition === 'heavy' ? 'bg-status-warning-bg-subtle' : 'bg-status-warning-bg-subtle'}`}
749
- >
750
- <p className="text-sm font-medium capitalize">{tc.condition} traffic</p>
751
- <p className="text-xs text-text-secondary">
752
- {tc.location} - +{tc.delay} min delay
753
- </p>
754
- </div>
755
- ))}
756
- </CardContent>
757
- </Card>
758
- )}
754
+ <Card className="border-status-error-border">
755
+ <CardHeader>
756
+ <CardTitle className="flex items-center gap-2 text-status-error-text">
757
+ <Trash2 size={18} /> Delete Route
758
+ </CardTitle>
759
+ </CardHeader>
760
+ <CardContent>
761
+ <p className="mb-3 text-sm text-text-secondary">
762
+ Removes this route. It can no longer be dispatched. This cannot be undone from here.
763
+ </p>
764
+ <Button
765
+ variant="outline"
766
+ className="border-status-error-border text-status-error-text hover:bg-status-error-bg-subtle"
767
+ onClick={() => setDeleteOpen(true)}
768
+ >
769
+ <Trash2 size={16} /> Delete this route
770
+ </Button>
771
+ </CardContent>
772
+ </Card>
759
773
  </div>
760
774
  </div>
775
+
776
+ <ResponsiveDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
777
+ <ResponsiveDialogContent>
778
+ <ResponsiveDialogHeader>
779
+ <ResponsiveDialogTitle>Delete this route?</ResponsiveDialogTitle>
780
+ <ResponsiveDialogDescription>
781
+ This removes {route.name} and cannot be undone from here.
782
+ </ResponsiveDialogDescription>
783
+ </ResponsiveDialogHeader>
784
+ <div className="px-1">
785
+ <FormError message={deleteError} />
786
+ </div>
787
+ <ResponsiveDialogFooter>
788
+ <Button variant="outline" onClick={() => setDeleteOpen(false)} disabled={deleting}>
789
+ Cancel
790
+ </Button>
791
+ <Button
792
+ className="bg-status-error-text text-text-inverse hover:opacity-90"
793
+ onClick={handleDelete}
794
+ disabled={deleting}
795
+ >
796
+ {deleting ? 'Deleting...' : 'Delete route'}
797
+ </Button>
798
+ </ResponsiveDialogFooter>
799
+ </ResponsiveDialogContent>
800
+ </ResponsiveDialog>
761
801
  </PageLayout>
762
802
  );
763
803
  }
@@ -3,11 +3,21 @@
3
3
  */
4
4
 
5
5
  import { useState } from 'react';
6
- import { Plus, RefreshCw, Navigation } from 'lucide-react';
6
+ import { Plus, RefreshCw, Navigation, Filter } from 'lucide-react';
7
7
  import { PageLayout } from '../components/PageLayout';
8
8
  import { DataTable, type Column } from '../components/DataTable';
9
9
  import { StatusBadge } from '../components/StatusBadge';
10
- import { Badge, Button } from '../components/ui';
10
+ import {
11
+ Badge,
12
+ Button,
13
+ Card,
14
+ CardContent,
15
+ Select,
16
+ SelectContent,
17
+ SelectItem,
18
+ SelectTrigger,
19
+ SelectValue,
20
+ } from '../components/ui';
11
21
  import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
12
22
  import { useRoutes, useRouteStats } from '../hooks/useRoutes';
13
23
  import { useDebouncedValue } from '../hooks/useDebouncedValue';
@@ -17,17 +27,28 @@ import { joinPath } from '../utils/navigation';
17
27
  import { ROUTE_STATUS_COLORS } from '../constants';
18
28
  import type { Route } from '../types';
19
29
 
30
+ const STATUS_OPTIONS: Array<{ value: Route['status'] | 'all'; label: string }> = [
31
+ { value: 'all', label: 'All statuses' },
32
+ { value: 'planned', label: 'Planned' },
33
+ { value: 'in_progress', label: 'In Progress' },
34
+ { value: 'completed', label: 'Completed' },
35
+ { value: 'cancelled', label: 'Cancelled' },
36
+ ];
37
+
20
38
  export default function RoutesPage() {
21
39
  const { basePath, navigate } = useMoveTheWheels();
22
40
  const [page, setPage] = useState(0);
23
41
  const [pageSize, setPageSize] = useState(25);
24
42
  const [search, setSearch] = useState('');
43
+ const [statusFilter, setStatusFilter] = useState<Route['status'] | 'all'>('all');
25
44
  const debouncedSearch = useDebouncedValue(search);
26
45
  const { routes, totalRoutes, hasMore, isLoading, error, refetch } = useRoutes({
27
46
  limit: pageSize,
28
47
  offset: page * pageSize,
29
48
  search: debouncedSearch || undefined,
49
+ status: statusFilter === 'all' ? undefined : statusFilter,
30
50
  });
51
+ const filtersActive = statusFilter !== 'all' || search.length > 0;
31
52
  // Bounded aggregate for the tiles + subtitle — not the truncated page.
32
53
  const { stats } = useRouteStats();
33
54
 
@@ -126,6 +147,49 @@ export default function RoutesPage() {
126
147
  </div>
127
148
  </EmphasisPanel>
128
149
 
150
+ {/* Filters */}
151
+ <Card>
152
+ <CardContent className="p-4">
153
+ <div className="flex flex-wrap items-center gap-4">
154
+ <div className="flex items-center gap-2">
155
+ <Filter size={16} className="text-text-secondary" />
156
+ <span className="text-sm font-medium">Filters:</span>
157
+ </div>
158
+ <Select
159
+ value={statusFilter}
160
+ onValueChange={(value) => {
161
+ setStatusFilter(value as Route['status'] | 'all');
162
+ setPage(0);
163
+ }}
164
+ >
165
+ <SelectTrigger className="w-[180px]">
166
+ <SelectValue placeholder="Status" />
167
+ </SelectTrigger>
168
+ <SelectContent>
169
+ {STATUS_OPTIONS.map((option) => (
170
+ <SelectItem key={option.value} value={option.value}>
171
+ {option.label}
172
+ </SelectItem>
173
+ ))}
174
+ </SelectContent>
175
+ </Select>
176
+ {filtersActive && (
177
+ <Button
178
+ variant="ghost"
179
+ size="sm"
180
+ onClick={() => {
181
+ setStatusFilter('all');
182
+ setSearch('');
183
+ setPage(0);
184
+ }}
185
+ >
186
+ Clear filters
187
+ </Button>
188
+ )}
189
+ </div>
190
+ </CardContent>
191
+ </Card>
192
+
129
193
  <DataTable
130
194
  columns={columns}
131
195
  data={routes}
@@ -4,7 +4,18 @@
4
4
 
5
5
  import { useEffect, useState } from 'react';
6
6
  import { useParams, useLocation } from 'react-router';
7
- import { Truck, ArrowLeft, MapPin, Fuel, UserRound, Edit, Save, Mail, Phone } from 'lucide-react';
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,9 +35,20 @@ import {
24
35
  Avatar,
25
36
  AvatarFallback,
26
37
  } from '../components/ui';
27
- import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
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';
28
49
  import { useVehicle, useVehicleDrivers, useFleet, toApiVehicleStatus } from '../hooks/useFleet';
29
- import type { VehicleStatus } from '../types';
50
+ import type { VehicleFuelType, VehicleStatus } from '../types';
51
+ import { VEHICLE_STATUS_LABELS } from '../constants';
30
52
  import { useRecordVehicleService } from '../hooks/useFleetIntelligence';
31
53
  import { VehicleServiceCard } from '../components/VehicleServiceCard';
32
54
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
@@ -48,11 +70,16 @@ const VEHICLE_TYPE_OPTIONS = [
48
70
  { value: 'DRONE', label: 'Drone' },
49
71
  ];
50
72
 
51
- const VEHICLE_STATUS_OPTIONS: Array<{ value: VehicleStatus; label: string }> = [
52
- { value: 'available', label: 'Available' },
53
- { value: 'assigned', label: 'In Transit' },
54
- { value: 'maintenance', label: 'Maintenance' },
55
- { value: 'out_of_service', label: 'Out of Service' },
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 FUEL_TYPE_OPTIONS: Array<{ value: VehicleFuelType; label: string }> = [
78
+ { value: 'DIESEL', label: 'Diesel' },
79
+ { value: 'PETROL', label: 'Petrol' },
80
+ { value: 'CNG', label: 'CNG' },
81
+ { value: 'ELECTRIC', label: 'Electric' },
82
+ { value: 'HYBRID', label: 'Hybrid' },
56
83
  ];
57
84
 
58
85
  const OWNERSHIP_TYPE_OPTIONS = [
@@ -63,7 +90,7 @@ const OWNERSHIP_TYPE_OPTIONS = [
63
90
 
64
91
  function NewVehicleForm({ editId }: { editId?: string }) {
65
92
  const { basePath, navigate } = useMoveTheWheels();
66
- const { createVehicle, updateVehicle } = useFleet();
93
+ const { createVehicle, updateVehicle, deleteVehicle } = useFleet();
67
94
  const { vehicle } = useVehicle(editId);
68
95
  const isEditing = !!editId;
69
96
  const [form, setForm] = useState({
@@ -75,11 +102,17 @@ function NewVehicleForm({ editId }: { editId?: string }) {
75
102
  ownershipType: '',
76
103
  year: '',
77
104
  status: 'available' as VehicleStatus,
78
- fuelType: '',
105
+ fuelType: '' as VehicleFuelType | '',
79
106
  fuelEfficiencyLPer100Km: '',
107
+ capacityWeight: '',
108
+ capacityVolume: '',
109
+ capacityPallets: '',
80
110
  });
81
111
  const [submitting, setSubmitting] = useState(false);
82
112
  const [error, setError] = useState<string | null>(null);
113
+ const [deleteOpen, setDeleteOpen] = useState(false);
114
+ const [deleting, setDeleting] = useState(false);
115
+ const [deleteError, setDeleteError] = useState<string | null>(null);
83
116
 
84
117
  useEffect(() => {
85
118
  if (editId && vehicle) {
@@ -97,6 +130,9 @@ function NewVehicleForm({ editId }: { editId?: string }) {
97
130
  vehicle.fuelEfficiencyLPer100Km !== undefined
98
131
  ? String(vehicle.fuelEfficiencyLPer100Km)
99
132
  : '',
133
+ capacityWeight: vehicle.capacity.weight ? String(vehicle.capacity.weight) : '',
134
+ capacityVolume: vehicle.capacity.volume ? String(vehicle.capacity.volume) : '',
135
+ capacityPallets: vehicle.capacity.pallets ? String(vehicle.capacity.pallets) : '',
100
136
  });
101
137
  }
102
138
  }, [editId, vehicle]);
@@ -125,6 +161,14 @@ function NewVehicleForm({ editId }: { editId?: string }) {
125
161
  fuelEfficiencyLPer100Km: form.fuelEfficiencyLPer100Km
126
162
  ? Number(form.fuelEfficiencyLPer100Km)
127
163
  : undefined,
164
+ capacity:
165
+ form.capacityWeight || form.capacityVolume || form.capacityPallets
166
+ ? {
167
+ weight: form.capacityWeight ? Number(form.capacityWeight) : undefined,
168
+ volume: form.capacityVolume ? Number(form.capacityVolume) : undefined,
169
+ pallets: form.capacityPallets ? Number(form.capacityPallets) : undefined,
170
+ }
171
+ : undefined,
128
172
  };
129
173
  if (editId) {
130
174
  const updated = await updateVehicle(editId, payload);
@@ -140,6 +184,21 @@ function NewVehicleForm({ editId }: { editId?: string }) {
140
184
  }
141
185
  };
142
186
 
187
+ const handleDelete = async () => {
188
+ if (!editId) return;
189
+ setDeleting(true);
190
+ setDeleteError(null);
191
+ try {
192
+ await deleteVehicle(editId);
193
+ setDeleteOpen(false);
194
+ navigate?.(joinPath(basePath, 'fleet/vehicles'));
195
+ } catch (err) {
196
+ setDeleteError(err instanceof Error ? err.message : 'Failed to delete vehicle.');
197
+ } finally {
198
+ setDeleting(false);
199
+ }
200
+ };
201
+
143
202
  return (
144
203
  <PageLayout
145
204
  title={isEditing ? 'Edit Vehicle' : 'Add Vehicle'}
@@ -291,12 +350,26 @@ function NewVehicleForm({ editId }: { editId?: string }) {
291
350
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
292
351
  <div className="space-y-2">
293
352
  <Label htmlFor="fuelType">Fuel Type</Label>
294
- <Input
295
- id="fuelType"
296
- value={form.fuelType}
297
- onChange={(e) => setForm((prev) => ({ ...prev, fuelType: e.target.value }))}
298
- placeholder="Diesel"
299
- />
353
+ <Select
354
+ value={form.fuelType || undefined}
355
+ onValueChange={(value) => {
356
+ const fuelType = FUEL_TYPE_OPTIONS.find(
357
+ (option) => option.value === value
358
+ )?.value;
359
+ setForm((prev) => ({ ...prev, fuelType: fuelType ?? '' }));
360
+ }}
361
+ >
362
+ <SelectTrigger id="fuelType">
363
+ <SelectValue placeholder="Select fuel type" />
364
+ </SelectTrigger>
365
+ <SelectContent>
366
+ {FUEL_TYPE_OPTIONS.map((option) => (
367
+ <SelectItem key={option.value} value={option.value}>
368
+ {option.label}
369
+ </SelectItem>
370
+ ))}
371
+ </SelectContent>
372
+ </Select>
300
373
  </div>
301
374
  <div className="space-y-2">
302
375
  <Label htmlFor="fuelEfficiency">Fuel Efficiency (L/100km)</Label>
@@ -313,9 +386,98 @@ function NewVehicleForm({ editId }: { editId?: string }) {
313
386
  />
314
387
  </div>
315
388
  </div>
389
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
390
+ <div className="space-y-2">
391
+ <Label htmlFor="capacityWeight">Capacity — Weight (kg)</Label>
392
+ <Input
393
+ id="capacityWeight"
394
+ type="number"
395
+ min={0}
396
+ value={form.capacityWeight}
397
+ onChange={(e) => setForm((prev) => ({ ...prev, capacityWeight: e.target.value }))}
398
+ placeholder="5000"
399
+ />
400
+ </div>
401
+ <div className="space-y-2">
402
+ <Label htmlFor="capacityVolume">Capacity — Volume (m³)</Label>
403
+ <Input
404
+ id="capacityVolume"
405
+ type="number"
406
+ min={0}
407
+ step={0.1}
408
+ value={form.capacityVolume}
409
+ onChange={(e) => setForm((prev) => ({ ...prev, capacityVolume: e.target.value }))}
410
+ placeholder="18"
411
+ />
412
+ </div>
413
+ <div className="space-y-2">
414
+ <Label htmlFor="capacityPallets">Capacity — Pallets</Label>
415
+ <Input
416
+ id="capacityPallets"
417
+ type="number"
418
+ min={0}
419
+ value={form.capacityPallets}
420
+ onChange={(e) =>
421
+ setForm((prev) => ({ ...prev, capacityPallets: e.target.value }))
422
+ }
423
+ placeholder="12"
424
+ />
425
+ </div>
426
+ </div>
316
427
  </div>
317
428
  </EmphasisPanel>
429
+
430
+ {isEditing && (
431
+ <Card className="border-status-error-border">
432
+ <CardHeader>
433
+ <CardTitle className="flex items-center gap-2 text-status-error-text">
434
+ <Trash2 size={18} /> Delete Vehicle
435
+ </CardTitle>
436
+ </CardHeader>
437
+ <CardContent>
438
+ <p className="mb-3 text-sm text-text-secondary">
439
+ Removes this vehicle from the active fleet. It can no longer be assigned to drivers,
440
+ orders, or routes. This cannot be undone from here.
441
+ </p>
442
+ <Button
443
+ type="button"
444
+ variant="outline"
445
+ className="border-status-error-border text-status-error-text hover:bg-status-error-bg-subtle"
446
+ onClick={() => setDeleteOpen(true)}
447
+ >
448
+ <Trash2 size={16} /> Delete this vehicle
449
+ </Button>
450
+ </CardContent>
451
+ </Card>
452
+ )}
318
453
  </form>
454
+
455
+ <ResponsiveDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
456
+ <ResponsiveDialogContent>
457
+ <ResponsiveDialogHeader>
458
+ <ResponsiveDialogTitle>Delete this vehicle?</ResponsiveDialogTitle>
459
+ <ResponsiveDialogDescription>
460
+ This removes {form.vehicleNumber || 'this vehicle'} from the active fleet. It cannot
461
+ be undone from here.
462
+ </ResponsiveDialogDescription>
463
+ </ResponsiveDialogHeader>
464
+ <div className="px-1">
465
+ <FormError message={deleteError} />
466
+ </div>
467
+ <ResponsiveDialogFooter>
468
+ <Button variant="outline" onClick={() => setDeleteOpen(false)} disabled={deleting}>
469
+ Cancel
470
+ </Button>
471
+ <Button
472
+ className="bg-status-error-text text-text-inverse hover:opacity-90"
473
+ onClick={handleDelete}
474
+ disabled={deleting}
475
+ >
476
+ {deleting ? 'Deleting...' : 'Delete vehicle'}
477
+ </Button>
478
+ </ResponsiveDialogFooter>
479
+ </ResponsiveDialogContent>
480
+ </ResponsiveDialog>
319
481
  </PageLayout>
320
482
  );
321
483
  }
@@ -351,7 +513,7 @@ export default function VehicleDetailsPage() {
351
513
  return (
352
514
  <PageLayout
353
515
  title={vehicle.vehicleNumber}
354
- subtitle={`${vehicle.make} ${vehicle.model} (${vehicle.year})`}
516
+ subtitle={`${vehicle.make} ${vehicle.model}${vehicle.year ? ` (${vehicle.year})` : ''}`}
355
517
  icon={<Truck size={24} />}
356
518
  headerActions={
357
519
  <div className="flex gap-2">
@@ -428,7 +590,7 @@ export default function VehicleDetailsPage() {
428
590
  </div>
429
591
  <div>
430
592
  <p className="text-sm text-text-secondary">Year</p>
431
- <p className="font-medium">{vehicle.year}</p>
593
+ <p className="font-medium">{vehicle.year ?? '—'}</p>
432
594
  </div>
433
595
  <div>
434
596
  <p className="text-sm text-text-secondary">License Plate</p>
@@ -450,15 +612,13 @@ export default function VehicleDetailsPage() {
450
612
  <div>
451
613
  <p className="text-sm text-text-secondary">Weight</p>
452
614
  <p className="text-2xl font-bold">
453
- {vehicle.capacity.weight}
454
- <span className="text-sm font-normal"> kg</span>
615
+ {vehicle.capacity.weight !== undefined ? `${vehicle.capacity.weight} kg` : '—'}
455
616
  </p>
456
617
  </div>
457
618
  <div>
458
619
  <p className="text-sm text-text-secondary">Volume</p>
459
620
  <p className="text-2xl font-bold">
460
- {vehicle.capacity.volume}
461
- <span className="text-sm font-normal"> m³</span>
621
+ {vehicle.capacity.volume !== undefined ? `${vehicle.capacity.volume} m³` : '—'}
462
622
  </p>
463
623
  </div>
464
624
  {vehicle.capacity.pallets && (
@@ -601,7 +761,9 @@ export default function VehicleDetailsPage() {
601
761
  {vehicle.currentLocation.address || 'Location updating...'}
602
762
  </p>
603
763
  <p className="text-xs text-text-secondary mt-1">
604
- Updated {formatDate(vehicle.currentLocation.timestamp || new Date(), 'relative')}
764
+ {vehicle.currentLocation.timestamp
765
+ ? `Updated ${formatDate(vehicle.currentLocation.timestamp, 'relative')}`
766
+ : 'Update time not recorded'}
605
767
  </p>
606
768
  </CardContent>
607
769
  </Card>