From 5e40754c68ca4828fee65a1500b4e4da06166230 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:04:21 -0600 Subject: [PATCH] feat: add ownership cost fields to vehicle form (refs #15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CostInterval type and TCOResponse interface - Add TCO fields to Vehicle, CreateVehicleRequest, UpdateVehicleRequest - Add "Ownership Costs" section to VehicleForm with: - Purchase price and date - Insurance cost and interval - Registration cost and interval - TCO display toggle - Add getTCO API method - Mobile-responsive grid layout with 44px touch targets 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/features/vehicles/api/vehicles.api.ts | 10 +- .../vehicles/components/VehicleForm.tsx | 142 +++++++++++++++++- .../features/vehicles/types/vehicles.types.ts | 41 +++++ 3 files changed, 191 insertions(+), 2 deletions(-) diff --git a/frontend/src/features/vehicles/api/vehicles.api.ts b/frontend/src/features/vehicles/api/vehicles.api.ts index ed409c7..d24d6b0 100644 --- a/frontend/src/features/vehicles/api/vehicles.api.ts +++ b/frontend/src/features/vehicles/api/vehicles.api.ts @@ -3,7 +3,7 @@ */ import { apiClient } from '../../../core/api/client'; -import { Vehicle, CreateVehicleRequest, UpdateVehicleRequest, DecodedVehicleData } from '../types/vehicles.types'; +import { Vehicle, CreateVehicleRequest, UpdateVehicleRequest, DecodedVehicleData, TCOResponse } from '../types/vehicles.types'; // All requests (including dropdowns) use authenticated apiClient @@ -88,5 +88,13 @@ export const vehiclesApi = { decodeVin: async (vin: string): Promise => { const response = await apiClient.post('/vehicles/decode-vin', { vin }); return response.data; + }, + + /** + * Get Total Cost of Ownership data for a vehicle + */ + getTCO: async (vehicleId: string): Promise => { + const response = await apiClient.get(`/vehicles/${vehicleId}/tco`); + return response.data; } }; diff --git a/frontend/src/features/vehicles/components/VehicleForm.tsx b/frontend/src/features/vehicles/components/VehicleForm.tsx index 559d351..094becf 100644 --- a/frontend/src/features/vehicles/components/VehicleForm.tsx +++ b/frontend/src/features/vehicles/components/VehicleForm.tsx @@ -7,12 +7,19 @@ import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Button } from '../../../shared-minimal/components/Button'; -import { CreateVehicleRequest, Vehicle } from '../types/vehicles.types'; +import { CreateVehicleRequest, Vehicle, CostInterval } from '../types/vehicles.types'; import { vehiclesApi } from '../api/vehicles.api'; import { VehicleImageUpload } from './VehicleImageUpload'; import { useTierAccess } from '../../../core/hooks/useTierAccess'; import { UpgradeRequiredDialog } from '../../../shared-minimal/components/UpgradeRequiredDialog'; +// Cost interval options +const costIntervalOptions: { value: CostInterval; label: string }[] = [ + { value: 'monthly', label: 'Monthly' }, + { value: 'semi_annual', label: 'Semi-Annual (6 months)' }, + { value: 'annual', label: 'Annual' }, +]; + const vehicleSchema = z .object({ vin: z.string().max(17).nullable().optional().transform(val => val ?? undefined), @@ -28,6 +35,14 @@ const vehicleSchema = z color: z.string().nullable().optional(), licensePlate: z.string().nullable().optional(), odometerReading: z.number().min(0).nullable().optional(), + // TCO fields + purchasePrice: z.number().min(0).nullable().optional(), + purchaseDate: z.string().nullable().optional(), + insuranceCost: z.number().min(0).nullable().optional(), + insuranceInterval: z.enum(['monthly', 'semi_annual', 'annual']).nullable().optional(), + registrationCost: z.number().min(0).nullable().optional(), + registrationInterval: z.enum(['monthly', 'semi_annual', 'annual']).nullable().optional(), + tcoEnabled: z.boolean().nullable().optional(), }) .refine( (data) => { @@ -824,6 +839,131 @@ export const VehicleForm: React.FC = ({ /> + {/* Ownership Costs Section (TCO) */} +
+

+ Ownership Costs +

+

+ Track your total cost of ownership including purchase price and recurring costs. +

+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+ +

+ When enabled, shows lifetime cost and cost per mile/km on the vehicle detail page. +

+
+
+