@burdenoff/microfe-movethewheels 2026.924.2 → 2026.925.2

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 (47) hide show
  1. package/dist/components/OrderItemRow.js +195 -109
  2. package/dist/components/StatusBadge.js +31 -31
  3. package/dist/constants/index.js +11 -3
  4. package/dist/generated/wspace-operations.d.ts +7 -0
  5. package/dist/hooks/useFleet.js +85 -76
  6. package/dist/hooks/useOrders.js +8 -4
  7. package/dist/hooks/useTracking.js +127 -66
  8. package/dist/movethewheels/components/OrderItemRow.d.ts +1 -2
  9. package/dist/movethewheels/components/StatusBadge.d.ts +3 -1
  10. package/dist/movethewheels/constants/index.d.ts +1 -0
  11. package/dist/movethewheels/hooks/useFleet.d.ts +41 -1
  12. package/dist/movethewheels/hooks/useOrders.d.ts +1 -0
  13. package/dist/movethewheels/hooks/useTracking.d.ts +2 -2
  14. package/dist/movethewheels/types/index.d.ts +17 -39
  15. package/dist/node_modules/lucide-react/dist/esm/icons/sticky-note.js +11 -0
  16. package/dist/node_modules/lucide-react/dist/esm/icons/user-x.js +30 -0
  17. package/dist/pages/CreateOrderPage.js +597 -420
  18. package/dist/pages/DriverDetailsPage.js +702 -421
  19. package/dist/pages/DriversPage.js +106 -117
  20. package/dist/pages/FleetPage.js +4 -10
  21. package/dist/pages/OrderDetailsPage.js +290 -262
  22. package/dist/pages/OrdersListPage.js +4 -0
  23. package/dist/pages/RouteDetailsPage.js +264 -252
  24. package/dist/pages/TrackingPage.js +205 -195
  25. package/dist/pages/VehicleDetailsPage.js +403 -243
  26. package/dist/utils/mapMarkers.js +2 -1
  27. package/package.json +1 -1
  28. package/src/generated/wspace-operations.ts +10 -3
  29. package/src/movethewheels/components/OrderItemRow.tsx +162 -99
  30. package/src/movethewheels/components/StatusBadge.tsx +11 -2
  31. package/src/movethewheels/constants/index.ts +13 -0
  32. package/src/movethewheels/hooks/useFleet.ts +163 -44
  33. package/src/movethewheels/hooks/useOrders.ts +17 -3
  34. package/src/movethewheels/hooks/useTracking.ts +208 -27
  35. package/src/movethewheels/pages/CreateOrderPage.tsx +372 -263
  36. package/src/movethewheels/pages/DriverDetailsPage.tsx +516 -156
  37. package/src/movethewheels/pages/DriversPage.tsx +28 -11
  38. package/src/movethewheels/pages/FleetPage.tsx +7 -10
  39. package/src/movethewheels/pages/OrderDetailsPage.tsx +67 -30
  40. package/src/movethewheels/pages/OrdersListPage.tsx +1 -0
  41. package/src/movethewheels/pages/RouteDetailsPage.tsx +15 -7
  42. package/src/movethewheels/pages/TrackingPage.tsx +44 -14
  43. package/src/movethewheels/pages/VehicleDetailsPage.tsx +137 -15
  44. package/src/movethewheels/types/index.ts +17 -38
  45. package/src/movethewheels/utils/mapMarkers.ts +1 -0
  46. package/src/operations/wspace/movethewheels/queries/dashboard.graphql +5 -0
  47. package/src/operations/wspace/movethewheels/queries/logistics.graphql +2 -2
@@ -37,14 +37,17 @@ import { useDebouncedValue } from '../hooks/useDebouncedValue';
37
37
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
38
38
  import { formatDate } from '../utils/formatters';
39
39
  import { joinPath } from '../utils/navigation';
40
+ import { DRIVER_STATUS_LABELS } from '../constants';
40
41
  import type { Driver, DriverStatus } from '../types';
41
42
 
43
+ // Sourced from the same DRIVER_STATUS_LABELS map the roster table's
44
+ // StatusBadge renders from, so this list can never drift from the table
45
+ // again (BOFF-7909).
42
46
  const STATUS_OPTIONS: Array<{ value: DriverStatus | 'all'; label: string }> = [
43
47
  { value: 'all', label: 'All statuses' },
44
- { value: 'available', label: 'Available' },
45
- { value: 'busy', label: 'On delivery' },
46
- { value: 'on_break', label: 'On break' },
47
- { value: 'offline', label: 'Off duty' },
48
+ ...(Object.entries(DRIVER_STATUS_LABELS) as Array<[DriverStatus, string]>).map(
49
+ ([value, label]) => ({ value, label })
50
+ ),
48
51
  ];
49
52
 
50
53
  export default function DriversPage() {
@@ -74,8 +77,17 @@ export default function DriversPage() {
74
77
  // Exact ON_BREAK / OFF_DUTY / INACTIVE counts — fleetStats cannot split these.
75
78
  const { counts } = useDriverStatusCounts();
76
79
  // Drivers carry only `vehicleId`; the roster shows the fleet number, so the
77
- // vehicle list is joined client-side into an id -> number lookup.
78
- const { vehicles } = useVehicles({ limit: 100 });
80
+ // exact vehicles referenced on this page are looked up by id — this stays
81
+ // correct regardless of total fleet size (a flat top-N page would silently
82
+ // miss drivers assigned to a vehicle outside the first N, BOFF-7909).
83
+ const assignedVehicleIds = useMemo(
84
+ () =>
85
+ Array.from(
86
+ new Set(drivers.map((driver) => driver.assignedVehicle).filter(Boolean))
87
+ ) as string[],
88
+ [drivers]
89
+ );
90
+ const { vehicles } = useVehicles({ ids: assignedVehicleIds });
79
91
  const vehicleNumberById = useMemo(
80
92
  () => new Map(vehicles.map((vehicle) => [vehicle.id, vehicle.vehicleNumber])),
81
93
  [vehicles]
@@ -154,10 +166,10 @@ export default function DriversPage() {
154
166
  id: 'rating',
155
167
  header: 'Rating',
156
168
  cell: (row) =>
157
- row.ratings.overall > 0 ? (
169
+ row.rating > 0 ? (
158
170
  <div className="flex items-center gap-1">
159
171
  <Star size={14} className="text-status-warning-text fill-current" />
160
- <span className="font-medium">{row.ratings.overall.toFixed(1)}</span>
172
+ <span className="font-medium">{row.rating.toFixed(1)}</span>
161
173
  </div>
162
174
  ) : (
163
175
  <span className="text-sm text-text-secondary">Not rated</span>
@@ -181,10 +193,15 @@ export default function DriversPage() {
181
193
  },
182
194
  {
183
195
  id: 'joined',
184
- header: 'Join Date',
185
- accessorKey: 'joinDate',
196
+ header: 'Hire Date',
197
+ accessorKey: 'hireDate',
186
198
  sortable: true,
187
- cell: (row) => formatDate(row.joinDate, 'short'),
199
+ cell: (row) =>
200
+ row.hireDate ? (
201
+ formatDate(row.hireDate, 'short')
202
+ ) : (
203
+ <span className="text-text-secondary">Not recorded</span>
204
+ ),
188
205
  },
189
206
  ];
190
207
 
@@ -302,20 +302,17 @@ export default function FleetPage() {
302
302
  </Avatar>
303
303
  <div>
304
304
  <p className="font-medium">{driver.fullName}</p>
305
- <p className="text-sm text-text-secondary">
306
- {driver.employeeId} - {driver.completedOrders}{' '}
307
- {t('movethewheels.fleet.drivers.deliveries', 'deliveries')}
308
- </p>
305
+ <p className="text-sm text-text-secondary">{driver.employeeId}</p>
309
306
  </div>
310
307
  </div>
311
308
  <div className="text-right">
312
309
  <StatusBadge status={driver.status} type="driver" size="sm" />
313
- <div className="mt-1 flex items-center gap-1 justify-end">
314
- <Star size={12} className="text-status-warning-text fill-current" />
315
- <span className="text-sm font-medium">
316
- {driver.ratings.overall.toFixed(1)}
317
- </span>
318
- </div>
310
+ {driver.rating > 0 && (
311
+ <div className="mt-1 flex items-center gap-1 justify-end">
312
+ <Star size={12} className="text-status-warning-text fill-current" />
313
+ <span className="text-sm font-medium">{driver.rating.toFixed(1)}</span>
314
+ </div>
315
+ )}
319
316
  </div>
320
317
  </div>
321
318
  </div>
@@ -23,6 +23,7 @@ import {
23
23
  ArrowRight,
24
24
  XCircle,
25
25
  UserCheck,
26
+ Search,
26
27
  } from 'lucide-react';
27
28
  import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
28
29
  import { StatusBadge, PriorityBadge, PaymentStatusBadge } from '../components/StatusBadge';
@@ -79,10 +80,13 @@ const STATUS_LABELS: Record<OrderStatus, string> = {
79
80
  in_transit: 'In Transit',
80
81
  out_for_delivery: 'Out for Delivery',
81
82
  delivered: 'Delivered',
83
+ returned: 'Returned',
82
84
  cancelled: 'Cancelled',
83
85
  failed: 'Failed',
84
86
  };
85
87
 
88
+ const TERMINAL_STATUSES = new Set<OrderStatus>(['delivered', 'returned', 'cancelled', 'failed']);
89
+
86
90
  function nextStatus(current: OrderStatus): OrderStatus | null {
87
91
  const index = STATUS_FLOW.indexOf(current);
88
92
  if (index === -1 || index >= STATUS_FLOW.length - 1) return null;
@@ -149,18 +153,31 @@ export default function OrderDetailsPage() {
149
153
  }, [selectedVehicle, vehicles, vehiclesLoading]);
150
154
 
151
155
  const handleAssign = async () => {
152
- if (!orderId || (!selectedVehicle && !selectedDriver)) {
153
- setActionError('Pick a vehicle and/or a driver to assign.');
156
+ if (!orderId || !order) {
157
+ setActionError('This order is no longer available. Refresh and try again.');
158
+ return;
159
+ }
160
+
161
+ const vehicleId = selectedVehicle || order.vehicleId;
162
+ const driverId = selectedDriver || order.driverId;
163
+ if (!vehicleId || !driverId) {
164
+ setActionError('Select both a compatible vehicle and driver before dispatching.');
154
165
  return;
155
166
  }
167
+
168
+ const wasFullyAssigned = Boolean(order.vehicleId && order.driverId);
156
169
  setAssigning(true);
157
170
  setActionError(null);
158
171
  try {
159
- await updateOrder(orderId, {
160
- vehicleId: selectedVehicle || undefined,
161
- driverId: selectedDriver || undefined,
162
- });
163
- notify('Assignment updated.');
172
+ await updateOrder(orderId, { vehicleId, driverId });
173
+ if (!wasFullyAssigned && order.status === 'confirmed') {
174
+ await updateOrderStatus(orderId, 'assigned');
175
+ }
176
+ notify(
177
+ !wasFullyAssigned && order.status === 'confirmed'
178
+ ? 'Fleet assigned and order advanced to Assigned.'
179
+ : 'Assignment updated.'
180
+ );
164
181
  setSelectedVehicle('');
165
182
  setSelectedDriver('');
166
183
  refetch();
@@ -172,7 +189,13 @@ export default function OrderDetailsPage() {
172
189
  };
173
190
 
174
191
  const handleStatusChange = async (status: OrderStatus, successMessage: string) => {
175
- if (!orderId) return;
192
+ if (!orderId || !order) return;
193
+ if (status === 'assigned' && (!order.vehicleId || !order.driverId)) {
194
+ setActionError('Assign both a compatible vehicle and driver before advancing to Assigned.');
195
+ return;
196
+ }
197
+ if (TERMINAL_STATUSES.has(order.status)) return;
198
+
176
199
  setAdvancing(true);
177
200
  setActionError(null);
178
201
  try {
@@ -555,13 +578,17 @@ export default function OrderDetailsPage() {
555
578
 
556
579
  <div className="space-y-3">
557
580
  <div className="space-y-1.5">
558
- <Label htmlFor="assign-fleet-search">Search fleet</Label>
559
- <Input
560
- id="assign-fleet-search"
561
- value={fleetSearch}
562
- onChange={(event) => setFleetSearch(event.target.value)}
563
- placeholder="Vehicle number, driver name or ID"
564
- />
581
+ <Label htmlFor="assign-fleet-search">Search vehicles &amp; drivers</Label>
582
+ <div className="relative">
583
+ <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" />
584
+ <Input
585
+ id="assign-fleet-search"
586
+ value={fleetSearch}
587
+ onChange={(event) => setFleetSearch(event.target.value)}
588
+ placeholder="Vehicle number, driver name or employee ID"
589
+ className="pl-9"
590
+ />
591
+ </div>
565
592
  </div>
566
593
  <div className="space-y-1.5">
567
594
  <Label htmlFor="assign-vehicle">Vehicle</Label>
@@ -635,24 +662,34 @@ export default function OrderDetailsPage() {
635
662
  {(() => {
636
663
  const upcoming = nextStatus(order.status);
637
664
  if (!upcoming) return null;
665
+ const missingDispatchPair =
666
+ upcoming === 'assigned' && (!order.vehicleId || !order.driverId);
638
667
  return (
639
- <Button
640
- variant="outline"
641
- className="w-full"
642
- onClick={() =>
643
- handleStatusChange(
644
- upcoming,
645
- `Order advanced to ${STATUS_LABELS[upcoming]}.`
646
- )
647
- }
648
- disabled={advancing}
649
- >
650
- <ArrowRight size={16} />
651
- {advancing ? 'Updating…' : `Advance to ${STATUS_LABELS[upcoming]}`}
652
- </Button>
668
+ <>
669
+ <Button
670
+ variant="outline"
671
+ className="w-full"
672
+ onClick={() =>
673
+ handleStatusChange(
674
+ upcoming,
675
+ `Order advanced to ${STATUS_LABELS[upcoming]}.`
676
+ )
677
+ }
678
+ disabled={advancing || missingDispatchPair}
679
+ >
680
+ <ArrowRight size={16} />
681
+ {advancing ? 'Updating…' : `Advance to ${STATUS_LABELS[upcoming]}`}
682
+ </Button>
683
+ {missingDispatchPair && (
684
+ <p className="text-xs text-text-secondary">
685
+ Assign both a compatible vehicle and driver before dispatching this
686
+ order.
687
+ </p>
688
+ )}
689
+ </>
653
690
  );
654
691
  })()}
655
- {order.status !== 'cancelled' && order.status !== 'delivered' && (
692
+ {order.status !== 'cancelled' && !TERMINAL_STATUSES.has(order.status) && (
656
693
  <Button
657
694
  variant="outline"
658
695
  className="w-full text-status-error-text"
@@ -195,6 +195,7 @@ export default function OrdersListPage() {
195
195
  label: t('movethewheels.orders.status.outForDelivery', 'Out for Delivery'),
196
196
  },
197
197
  { value: 'delivered', label: t('movethewheels.orders.status.delivered', 'Delivered') },
198
+ { value: 'returned', label: t('movethewheels.orders.status.returned', 'Returned') },
198
199
  { value: 'cancelled', label: t('movethewheels.orders.status.cancelled', 'Cancelled') },
199
200
  { value: 'failed', label: t('movethewheels.orders.status.failed', 'Failed') },
200
201
  ];
@@ -18,6 +18,7 @@ import {
18
18
  ArrowUp,
19
19
  ArrowDown,
20
20
  Fuel,
21
+ Search,
21
22
  } from 'lucide-react';
22
23
  import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
23
24
  import { StatusBadge } from '../components/StatusBadge';
@@ -276,13 +277,20 @@ function NewRouteForm({ editId }: { editId?: string }) {
276
277
  </div>
277
278
  </div>
278
279
  <div className="space-y-2">
279
- <Label htmlFor="fleet-search">Search fleet</Label>
280
- <Input
281
- id="fleet-search"
282
- value={fleetSearch}
283
- onChange={(e) => setFleetSearch(e.target.value)}
284
- placeholder="Vehicle number, driver name or ID"
285
- />
280
+ <Label htmlFor="fleet-search">Search vehicles &amp; drivers</Label>
281
+ <div className="relative">
282
+ <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" />
283
+ <Input
284
+ id="fleet-search"
285
+ value={fleetSearch}
286
+ onChange={(e) => setFleetSearch(e.target.value)}
287
+ placeholder="Vehicle number, driver name or employee ID"
288
+ className="pl-9"
289
+ />
290
+ </div>
291
+ <p className="text-xs text-text-secondary">
292
+ Narrows both pickers below as you type — up to 20 matches at a time.
293
+ </p>
286
294
  </div>
287
295
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
288
296
  <div className="space-y-2">
@@ -60,7 +60,20 @@ export default function TrackingPage() {
60
60
  ? { ...destCoord, label: `Delivery · ${order.deliveryAddress.city || 'Destination'}` }
61
61
  : null;
62
62
 
63
- const trail: TrackPoint[] = order.timeline
63
+ const trackingTrail: TrackPoint[] = (tracking?.locationHistory ?? [])
64
+ .map((location): TrackPoint | null => {
65
+ const coord = firstRealLatLng(location);
66
+ return coord
67
+ ? {
68
+ lat: coord.lat,
69
+ lng: coord.lng,
70
+ timestamp: location.timestamp?.toISOString() ?? new Date().toISOString(),
71
+ }
72
+ : null;
73
+ })
74
+ .filter((point): point is TrackPoint => point !== null);
75
+
76
+ const statusTrail: TrackPoint[] = order.timeline
64
77
  .map((event): TrackPoint | null => {
65
78
  const coord = event.location ? firstRealLatLng(event.location) : null;
66
79
  return coord
@@ -69,6 +82,7 @@ export default function TrackingPage() {
69
82
  })
70
83
  .filter((point): point is TrackPoint => point !== null);
71
84
 
85
+ const trail = trackingTrail.length > 0 ? trackingTrail : statusTrail;
72
86
  const currentCoord = tracking ? firstRealLatLng(tracking.currentLocation) : null;
73
87
  const current: TrackEndpoint | null = currentCoord
74
88
  ? {
@@ -163,7 +177,7 @@ export default function TrackingPage() {
163
177
  )}
164
178
 
165
179
  {/* Tracking Results */}
166
- {order && tracking && (
180
+ {order && (
167
181
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
168
182
  {/* Main Tracking Info */}
169
183
  <div className="lg:col-span-2 space-y-6">
@@ -181,14 +195,14 @@ export default function TrackingPage() {
181
195
  <p className="text-sm text-text-secondary">
182
196
  {t('movethewheels.tracking.details.trackingNumber', 'Tracking Number')}
183
197
  </p>
184
- <p className="font-mono font-medium">{order.trackingNumber}</p>
198
+ <p className="font-mono font-medium">{order.trackingNumber || '—'}</p>
185
199
  </div>
186
200
  <div>
187
201
  <p className="text-sm text-text-secondary">
188
202
  {t('movethewheels.tracking.details.estimatedDelivery', 'Estimated Delivery')}
189
203
  </p>
190
204
  <p className="font-medium">
191
- {tracking.estimatedArrival
205
+ {tracking?.estimatedArrival
192
206
  ? formatDate(tracking.estimatedArrival, 'full')
193
207
  : order.scheduledDelivery
194
208
  ? formatDate(order.scheduledDelivery, 'full')
@@ -207,9 +221,21 @@ export default function TrackingPage() {
207
221
  <p className="text-sm text-text-secondary">
208
222
  {t('movethewheels.tracking.details.lastUpdated', 'Last Updated')}
209
223
  </p>
210
- <p className="font-medium">{formatDate(tracking.lastUpdated, 'relative')}</p>
224
+ <p className="font-medium">
225
+ {tracking
226
+ ? formatDate(tracking.lastUpdated, 'relative')
227
+ : t('movethewheels.tracking.details.noUpdates', 'No tracking updates yet')}
228
+ </p>
211
229
  </div>
212
230
  </div>
231
+ {!tracking && (
232
+ <p className="mt-4 rounded-card border border-border-subtle bg-bg-sunken p-3 text-sm text-text-secondary">
233
+ {t(
234
+ 'movethewheels.tracking.details.noUpdatesDescription',
235
+ 'The delivery was found, but no tracking updates have been recorded yet.'
236
+ )}
237
+ </p>
238
+ )}
213
239
  </EmphasisPanel>
214
240
 
215
241
  {/* Route Map — real origin/destination + tracking trail + live position */}
@@ -356,7 +382,7 @@ export default function TrackingPage() {
356
382
  </Card>
357
383
 
358
384
  {/* Alerts */}
359
- {tracking.alerts && tracking.alerts.length > 0 && (
385
+ {tracking?.alerts && tracking.alerts.length > 0 && (
360
386
  <Card>
361
387
  <CardHeader>
362
388
  <CardTitle className="flex items-center gap-2">
@@ -430,14 +456,18 @@ export default function TrackingPage() {
430
456
  </p>
431
457
  </div>
432
458
  )}
433
- <p className="text-sm">
434
- {tracking.currentLocation.address ||
435
- t('movethewheels.tracking.location.updating', 'Location updating...')}
436
- </p>
437
- <p className="text-xs text-text-secondary mt-1">
438
- {t('movethewheels.tracking.location.updated', 'Updated')}{' '}
439
- {formatDate(tracking.currentLocation.timestamp || new Date(), 'relative')}
440
- </p>
459
+ {tracking && (
460
+ <>
461
+ <p className="text-sm">
462
+ {tracking.currentLocation.address ||
463
+ t('movethewheels.tracking.location.updating', 'Location updating...')}
464
+ </p>
465
+ <p className="text-xs text-text-secondary mt-1">
466
+ {t('movethewheels.tracking.location.updated', 'Updated')}{' '}
467
+ {formatDate(tracking.currentLocation.timestamp || new Date(), 'relative')}
468
+ </p>
469
+ </>
470
+ )}
441
471
  </CardContent>
442
472
  </Card>
443
473
 
@@ -25,7 +25,8 @@ import {
25
25
  AvatarFallback,
26
26
  } from '../components/ui';
27
27
  import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
28
- import { useVehicle, useVehicleDrivers, useFleet } from '../hooks/useFleet';
28
+ import { useVehicle, useVehicleDrivers, useFleet, toApiVehicleStatus } from '../hooks/useFleet';
29
+ import type { VehicleStatus } from '../types';
29
30
  import { useRecordVehicleService } from '../hooks/useFleetIntelligence';
30
31
  import { VehicleServiceCard } from '../components/VehicleServiceCard';
31
32
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
@@ -47,6 +48,19 @@ const VEHICLE_TYPE_OPTIONS = [
47
48
  { value: 'DRONE', label: 'Drone' },
48
49
  ];
49
50
 
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' },
56
+ ];
57
+
58
+ const OWNERSHIP_TYPE_OPTIONS = [
59
+ { value: 'owned', label: 'Owned' },
60
+ { value: 'leased', label: 'Leased' },
61
+ { value: 'rented', label: 'Rented' },
62
+ ];
63
+
50
64
  function NewVehicleForm({ editId }: { editId?: string }) {
51
65
  const { basePath, navigate } = useMoveTheWheels();
52
66
  const { createVehicle, updateVehicle } = useFleet();
@@ -58,6 +72,11 @@ function NewVehicleForm({ editId }: { editId?: string }) {
58
72
  make: '',
59
73
  model: '',
60
74
  licensePlate: '',
75
+ ownershipType: '',
76
+ year: '',
77
+ status: 'available' as VehicleStatus,
78
+ fuelType: '',
79
+ fuelEfficiencyLPer100Km: '',
61
80
  });
62
81
  const [submitting, setSubmitting] = useState(false);
63
82
  const [error, setError] = useState<string | null>(null);
@@ -70,6 +89,14 @@ function NewVehicleForm({ editId }: { editId?: string }) {
70
89
  make: vehicle.make ?? '',
71
90
  model: vehicle.model ?? '',
72
91
  licensePlate: vehicle.licensePlate ?? '',
92
+ ownershipType: vehicle.ownershipType ?? '',
93
+ year: vehicle.year ? String(vehicle.year) : '',
94
+ status: vehicle.status,
95
+ fuelType: vehicle.fuelType ?? '',
96
+ fuelEfficiencyLPer100Km:
97
+ vehicle.fuelEfficiencyLPer100Km !== undefined
98
+ ? String(vehicle.fuelEfficiencyLPer100Km)
99
+ : '',
73
100
  });
74
101
  }
75
102
  }, [editId, vehicle]);
@@ -85,23 +112,25 @@ function NewVehicleForm({ editId }: { editId?: string }) {
85
112
  setSubmitting(true);
86
113
  setError(null);
87
114
  try {
115
+ const payload = {
116
+ vehicleNumber: form.vehicleNumber.trim(),
117
+ type: form.type,
118
+ make: form.make.trim() || undefined,
119
+ model: form.model.trim() || undefined,
120
+ licensePlate: form.licensePlate.trim() || undefined,
121
+ ownershipType: form.ownershipType || undefined,
122
+ year: form.year ? Number(form.year) : undefined,
123
+ status: toApiVehicleStatus(form.status),
124
+ fuelType: form.fuelType || undefined,
125
+ fuelEfficiencyLPer100Km: form.fuelEfficiencyLPer100Km
126
+ ? Number(form.fuelEfficiencyLPer100Km)
127
+ : undefined,
128
+ };
88
129
  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
- });
130
+ const updated = await updateVehicle(editId, payload);
96
131
  navigate?.(joinPath(basePath, `fleet/vehicles/${updated.id}`));
97
132
  } 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
- });
133
+ const created = await createVehicle(payload);
105
134
  navigate?.(joinPath(basePath, `fleet/vehicles/${created.id}`));
106
135
  }
107
136
  } catch (err) {
@@ -207,6 +236,83 @@ function NewVehicleForm({ editId }: { editId?: string }) {
207
236
  />
208
237
  </div>
209
238
  </div>
239
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
240
+ <div className="space-y-2">
241
+ <Label htmlFor="ownershipType">Ownership</Label>
242
+ <Select
243
+ value={form.ownershipType || undefined}
244
+ onValueChange={(value) => setForm((prev) => ({ ...prev, ownershipType: value }))}
245
+ >
246
+ <SelectTrigger id="ownershipType">
247
+ <SelectValue placeholder="Select ownership" />
248
+ </SelectTrigger>
249
+ <SelectContent>
250
+ {OWNERSHIP_TYPE_OPTIONS.map((option) => (
251
+ <SelectItem key={option.value} value={option.value}>
252
+ {option.label}
253
+ </SelectItem>
254
+ ))}
255
+ </SelectContent>
256
+ </Select>
257
+ </div>
258
+ <div className="space-y-2">
259
+ <Label htmlFor="year">Year</Label>
260
+ <Input
261
+ id="year"
262
+ type="number"
263
+ min={1980}
264
+ max={new Date().getFullYear() + 1}
265
+ value={form.year}
266
+ onChange={(e) => setForm((prev) => ({ ...prev, year: e.target.value }))}
267
+ placeholder={String(new Date().getFullYear())}
268
+ />
269
+ </div>
270
+ <div className="space-y-2">
271
+ <Label htmlFor="vehicleStatus">Status</Label>
272
+ <Select
273
+ value={form.status}
274
+ onValueChange={(value) =>
275
+ setForm((prev) => ({ ...prev, status: value as VehicleStatus }))
276
+ }
277
+ >
278
+ <SelectTrigger id="vehicleStatus">
279
+ <SelectValue />
280
+ </SelectTrigger>
281
+ <SelectContent>
282
+ {VEHICLE_STATUS_OPTIONS.map((option) => (
283
+ <SelectItem key={option.value} value={option.value}>
284
+ {option.label}
285
+ </SelectItem>
286
+ ))}
287
+ </SelectContent>
288
+ </Select>
289
+ </div>
290
+ </div>
291
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
292
+ <div className="space-y-2">
293
+ <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
+ />
300
+ </div>
301
+ <div className="space-y-2">
302
+ <Label htmlFor="fuelEfficiency">Fuel Efficiency (L/100km)</Label>
303
+ <Input
304
+ id="fuelEfficiency"
305
+ type="number"
306
+ min={0}
307
+ step={0.1}
308
+ value={form.fuelEfficiencyLPer100Km}
309
+ onChange={(e) =>
310
+ setForm((prev) => ({ ...prev, fuelEfficiencyLPer100Km: e.target.value }))
311
+ }
312
+ placeholder="12.5"
313
+ />
314
+ </div>
315
+ </div>
210
316
  </div>
211
317
  </EmphasisPanel>
212
318
  </form>
@@ -394,6 +500,22 @@ export default function VehicleDetailsPage() {
394
500
  </p>
395
501
  </div>
396
502
  )}
503
+ {(vehicle.fuelType || vehicle.fuelEfficiencyLPer100Km !== undefined) && (
504
+ <div className="grid grid-cols-2 gap-4">
505
+ {vehicle.fuelType && (
506
+ <div>
507
+ <p className="text-sm text-text-secondary">Fuel Type</p>
508
+ <p className="font-medium">{vehicle.fuelType}</p>
509
+ </div>
510
+ )}
511
+ {vehicle.fuelEfficiencyLPer100Km !== undefined && (
512
+ <div>
513
+ <p className="text-sm text-text-secondary">Efficiency</p>
514
+ <p className="font-medium">{vehicle.fuelEfficiencyLPer100Km} L/100km</p>
515
+ </div>
516
+ )}
517
+ </div>
518
+ )}
397
519
  </CardContent>
398
520
  </Card>
399
521