From 9e8f9a1932a694da6f75e9f429270986552313a2 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:05:31 -0600 Subject: [PATCH] feat: add TCO display component (refs #15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create TCODisplay component showing lifetime cost and cost per distance - Display cost breakdown (purchase, insurance, registration, fuel, maintenance) - Integrate into VehicleDetailPage right-justified next to vehicle details - Responsive layout: stacks vertically on mobile, side-by-side on desktop - Only shows when tcoEnabled is true 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../vehicles/components/TCODisplay.tsx | 134 ++++++++++++++++++ .../vehicles/pages/VehicleDetailPage.tsx | 13 +- 2 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 frontend/src/features/vehicles/components/TCODisplay.tsx diff --git a/frontend/src/features/vehicles/components/TCODisplay.tsx b/frontend/src/features/vehicles/components/TCODisplay.tsx new file mode 100644 index 0000000..42fe9fa --- /dev/null +++ b/frontend/src/features/vehicles/components/TCODisplay.tsx @@ -0,0 +1,134 @@ +/** + * @ai-summary TCO (Total Cost of Ownership) display component + * Right-justified display showing lifetime cost and cost per mile/km + */ + +import React, { useEffect, useState } from 'react'; +import { TCOResponse } from '../types/vehicles.types'; +import { vehiclesApi } from '../api/vehicles.api'; + +interface TCODisplayProps { + vehicleId: string; + tcoEnabled?: boolean; +} + +// Currency symbol mapping +const CURRENCY_SYMBOLS: Record = { + USD: '$', + EUR: '€', + GBP: '£', + CAD: 'CA$', + AUD: 'A$', +}; + +export const TCODisplay: React.FC = ({ vehicleId, tcoEnabled }) => { + const [tco, setTco] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!tcoEnabled) { + setTco(null); + return; + } + + const fetchTCO = async () => { + setIsLoading(true); + setError(null); + try { + const data = await vehiclesApi.getTCO(vehicleId); + setTco(data); + } catch (err: any) { + console.error('Failed to fetch TCO:', err); + setError('Unable to load TCO data'); + } finally { + setIsLoading(false); + } + }; + + fetchTCO(); + }, [vehicleId, tcoEnabled]); + + // Don't render if TCO is disabled + if (!tcoEnabled) { + return null; + } + + // Loading state + if (isLoading) { + return ( +
+
+
+
+
+
+ ); + } + + // Error state + if (error) { + return ( +
+ {error} +
+ ); + } + + // No data + if (!tco) { + return null; + } + + const currencySymbol = CURRENCY_SYMBOLS[tco.currencyCode] || tco.currencyCode; + + // Format currency with proper separators + const formatCurrency = (value: number): string => { + return value.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + }; + + return ( +
+
+ {currencySymbol}{formatCurrency(tco.lifetimeTotal)} +
+
+ Lifetime Total +
+ {tco.costPerDistance > 0 && ( + <> +
+ {currencySymbol}{formatCurrency(tco.costPerDistance)}/{tco.distanceUnit} +
+
+ Cost per {tco.distanceUnit} +
+ + )} + + {/* Cost breakdown tooltip/details */} +
+
+ {tco.purchasePrice > 0 && ( +
Purchase: {currencySymbol}{formatCurrency(tco.purchasePrice)}
+ )} + {tco.insuranceCosts > 0 && ( +
Insurance: {currencySymbol}{formatCurrency(tco.insuranceCosts)}
+ )} + {tco.registrationCosts > 0 && ( +
Registration: {currencySymbol}{formatCurrency(tco.registrationCosts)}
+ )} + {tco.fuelCosts > 0 && ( +
Fuel: {currencySymbol}{formatCurrency(tco.fuelCosts)}
+ )} + {tco.maintenanceCosts > 0 && ( +
Maintenance: {currencySymbol}{formatCurrency(tco.maintenanceCosts)}
+ )} +
+
+
+ ); +}; diff --git a/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx b/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx index 7c71609..65027a1 100644 --- a/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx +++ b/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx @@ -15,6 +15,7 @@ import { vehiclesApi } from '../api/vehicles.api'; import { Card } from '../../../shared-minimal/components/Card'; import { VehicleForm } from '../components/VehicleForm'; import { VehicleImage } from '../components/VehicleImage'; +import { TCODisplay } from '../components/TCODisplay'; import { useFuelLogs } from '../../fuel-logs/hooks/useFuelLogs'; import { FuelLogResponse, UpdateFuelLogRequest } from '../../fuel-logs/types/fuel-logs.types'; import { FuelLogEditDialog } from '../../fuel-logs/components/FuelLogEditDialog'; @@ -300,8 +301,8 @@ export const VehicleDetailPage: React.FC = () => { - - + + @@ -318,6 +319,14 @@ export const VehicleDetailPage: React.FC = () => { )} + {/* TCO Display - right-justified */} + + +