diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 142cb63..0e437b8 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -53,7 +53,8 @@ "Bash(for feature in vehicles fuel-logs maintenance stations)", "Bash(ls:*)", "Bash(cp:*)", - "Bash(openssl:*)" + "Bash(openssl:*)", + "Bash(npm run type-check:*)" ], "deny": [] } diff --git a/backend/src/features/fuel-logs/domain/efficiency-calculation.service.ts b/backend/src/features/fuel-logs/domain/efficiency-calculation.service.ts index 7d87b1b..0afd326 100644 --- a/backend/src/features/fuel-logs/domain/efficiency-calculation.service.ts +++ b/backend/src/features/fuel-logs/domain/efficiency-calculation.service.ts @@ -37,7 +37,7 @@ export class EfficiencyCalculationService { return null; } - const value = UnitConversionService.calculateEfficiency(distance, currentLog.fuelUnits, unitSystem); + const value = UnitConversionService.calculateEfficiency(distance, currentLog.fuelUnits); const labels = UnitConversionService.getUnitLabels(unitSystem); return { value, unitSystem, label: labels.efficiencyUnits, calculationMethod: method }; } diff --git a/backend/src/features/fuel-logs/domain/fuel-logs.service.ts b/backend/src/features/fuel-logs/domain/fuel-logs.service.ts index 700b722..bb28d0d 100644 --- a/backend/src/features/fuel-logs/domain/fuel-logs.service.ts +++ b/backend/src/features/fuel-logs/domain/fuel-logs.service.ts @@ -85,7 +85,17 @@ export class FuelLogsService { if (cached) return cached; const rows = await this.repository.findByVehicleIdEnhanced(vehicleId); - const response = rows.map(r => this.toEnhancedResponse(r, undefined, unitSystem)); + const response = rows.map(r => { + const efficiency = EfficiencyCalculationService.calculateEfficiency( + { + tripDistance: r.trip_distance ?? undefined, + fuelUnits: r.fuel_units ?? undefined + }, + null, // No previous odometer needed for trip distance calculation + unitSystem + ); + return this.toEnhancedResponse(r, efficiency?.value ?? undefined, unitSystem); + }); await cacheService.set(cacheKey, response, this.cacheTTL); return response; } @@ -96,7 +106,17 @@ export class FuelLogsService { const cached = await cacheService.get(cacheKey); if (cached) return cached; const rows = await this.repository.findByUserIdEnhanced(userId); - const response = rows.map(r => this.toEnhancedResponse(r, undefined, unitSystem)); + const response = rows.map(r => { + const efficiency = EfficiencyCalculationService.calculateEfficiency( + { + tripDistance: r.trip_distance ?? undefined, + fuelUnits: r.fuel_units ?? undefined + }, + null, // No previous odometer needed for trip distance calculation + unitSystem + ); + return this.toEnhancedResponse(r, efficiency?.value ?? undefined, unitSystem); + }); await cacheService.set(cacheKey, response, this.cacheTTL); return response; } @@ -106,7 +126,17 @@ export class FuelLogsService { if (!row) throw new Error('Fuel log not found'); if (row.user_id !== userId) throw new Error('Unauthorized'); const { unitSystem } = await UserSettingsService.getUserSettings(userId); - return this.toEnhancedResponse(row, undefined, unitSystem); + + const efficiency = EfficiencyCalculationService.calculateEfficiency( + { + tripDistance: row.trip_distance ?? undefined, + fuelUnits: row.fuel_units ?? undefined + }, + null, // No previous odometer needed for trip distance calculation + unitSystem + ); + + return this.toEnhancedResponse(row, efficiency?.value ?? undefined, unitSystem); } async updateFuelLog(id: string, data: EnhancedUpdateFuelLogRequest, userId: string): Promise { diff --git a/backend/src/features/fuel-logs/domain/unit-conversion.service.ts b/backend/src/features/fuel-logs/domain/unit-conversion.service.ts index bc98419..779755b 100644 --- a/backend/src/features/fuel-logs/domain/unit-conversion.service.ts +++ b/backend/src/features/fuel-logs/domain/unit-conversion.service.ts @@ -3,28 +3,29 @@ import { UnitSystem as CoreUnitSystem } from '../../../shared-minimal/utils/unit export type UnitSystem = CoreUnitSystem; export class UnitConversionService { - private static readonly MPG_TO_L100KM = 235.214; + private static readonly KM_PER_MILE = 1.60934; + private static readonly GALLONS_TO_LITERS = 3.78541; static getUnitLabels(unitSystem: UnitSystem) { return unitSystem === 'metric' - ? { fuelUnits: 'liters', distanceUnits: 'kilometers', efficiencyUnits: 'L/100km' } - : { fuelUnits: 'gallons', distanceUnits: 'miles', efficiencyUnits: 'mpg' }; + ? { fuelUnits: 'liters', distanceUnits: 'kilometers', efficiencyUnits: 'km/L' } + : { fuelUnits: 'gallons', distanceUnits: 'miles', efficiencyUnits: 'MPG' }; } - static calculateEfficiency(distance: number, fuelUnits: number, unitSystem: UnitSystem): number { + static calculateEfficiency(distance: number, fuelUnits: number): number { if (fuelUnits <= 0 || distance <= 0) return 0; - return unitSystem === 'metric' - ? (fuelUnits / distance) * 100 - : distance / fuelUnits; + return distance / fuelUnits; // Both km/L and MPG use distance/fuel formula } static convertEfficiency(efficiency: number, from: UnitSystem, to: UnitSystem): number { if (from === to) return efficiency; if (from === 'imperial' && to === 'metric') { - return efficiency > 0 ? this.MPG_TO_L100KM / efficiency : 0; + // MPG to km/L: MPG * (gallons_to_liters / km_per_mile) + return efficiency * (this.GALLONS_TO_LITERS / this.KM_PER_MILE); } if (from === 'metric' && to === 'imperial') { - return efficiency > 0 ? this.MPG_TO_L100KM / efficiency : 0; + // km/L to MPG: km/L * (km_per_mile / gallons_to_liters) + return efficiency * (this.KM_PER_MILE / this.GALLONS_TO_LITERS); } return efficiency; } diff --git a/frontend/package.json b/frontend/package.json index 96e2d19..eab6e5f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,11 +22,12 @@ "react-hook-form": "^7.48.2", "@hookform/resolvers": "^3.3.2", "zod": "^3.22.4", - "date-fns": "^3.0.0", + "date-fns": "^2.30.0", "clsx": "^2.0.0", "react-hot-toast": "^2.4.1", "framer-motion": "^11.0.0", "@mui/material": "^5.15.0", + "@mui/x-date-pickers": "^6.19.0", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@emotion/cache": "^11.11.0", diff --git a/frontend/src/core/units/units.types.ts b/frontend/src/core/units/units.types.ts index cd2fe02..6e7b877 100644 --- a/frontend/src/core/units/units.types.ts +++ b/frontend/src/core/units/units.types.ts @@ -6,7 +6,7 @@ export type UnitSystem = 'imperial' | 'metric'; export type DistanceUnit = 'miles' | 'km'; export type VolumeUnit = 'gallons' | 'liters'; -export type FuelEfficiencyUnit = 'mpg' | 'l100km'; +export type FuelEfficiencyUnit = 'mpg' | 'kml'; export interface UnitPreferences { system: UnitSystem; @@ -21,4 +21,4 @@ export interface UserPreferences { unitSystem: UnitSystem; createdAt: string; updatedAt: string; -} \ No newline at end of file +} diff --git a/frontend/src/core/units/units.utils.ts b/frontend/src/core/units/units.utils.ts index 85e6628..84d02f8 100644 --- a/frontend/src/core/units/units.utils.ts +++ b/frontend/src/core/units/units.utils.ts @@ -10,7 +10,9 @@ const MILES_TO_KM = 1.60934; const KM_TO_MILES = 0.621371; const GALLONS_TO_LITERS = 3.78541; const LITERS_TO_GALLONS = 0.264172; -const MPG_TO_L100KM_FACTOR = 235.214; +// For km/L conversion +const MPG_TO_KML = 1.60934 / 3.78541; // ≈ 0.425144 +const KML_TO_MPG = 3.78541 / 1.60934; // ≈ 2.352145 // Distance Conversions export function convertDistance(value: number, fromUnit: DistanceUnit, toUnit: DistanceUnit): number { @@ -60,12 +62,12 @@ export function convertVolumeBySystem(gallons: number, toSystem: UnitSystem): nu export function convertFuelEfficiency(value: number, fromUnit: FuelEfficiencyUnit, toUnit: FuelEfficiencyUnit): number { if (fromUnit === toUnit) return value; - if (fromUnit === 'mpg' && toUnit === 'l100km') { - return value === 0 ? 0 : MPG_TO_L100KM_FACTOR / value; + if (fromUnit === 'mpg' && toUnit === 'kml') { + return value * MPG_TO_KML; } - - if (fromUnit === 'l100km' && toUnit === 'mpg') { - return value === 0 ? 0 : MPG_TO_L100KM_FACTOR / value; + + if (fromUnit === 'kml' && toUnit === 'mpg') { + return value * KML_TO_MPG; } return value; @@ -73,7 +75,7 @@ export function convertFuelEfficiency(value: number, fromUnit: FuelEfficiencyUni export function convertFuelEfficiencyBySystem(mpg: number, toSystem: UnitSystem): number { if (toSystem === 'metric') { - return convertFuelEfficiency(mpg, 'mpg', 'l100km'); + return convertFuelEfficiency(mpg, 'mpg', 'kml'); } return mpg; } @@ -109,7 +111,7 @@ export function formatVolume(value: number, unit: VolumeUnit, precision = 2): st export function formatFuelEfficiency(value: number, unit: FuelEfficiencyUnit, precision = 1): string { if (typeof value !== 'number' || isNaN(value)) { - return unit === 'mpg' ? '0 MPG' : '0 L/100km'; + return unit === 'mpg' ? '0 MPG' : '0 km/L'; } const rounded = parseFloat(value.toFixed(precision)); @@ -117,7 +119,7 @@ export function formatFuelEfficiency(value: number, unit: FuelEfficiencyUnit, pr if (unit === 'mpg') { return `${rounded} MPG`; } else { - return `${rounded} L/100km`; + return `${rounded} km/L`; } } @@ -166,8 +168,8 @@ export function formatVolumeBySystem(gallons: number, system: UnitSystem, precis export function formatFuelEfficiencyBySystem(mpg: number, system: UnitSystem, precision = 1): string { if (system === 'metric') { - const l100km = convertFuelEfficiencyBySystem(mpg, system); - return formatFuelEfficiency(l100km, 'l100km', precision); + const kml = convertFuelEfficiencyBySystem(mpg, system); + return formatFuelEfficiency(kml, 'kml', precision); } return formatFuelEfficiency(mpg, 'mpg', precision); } @@ -190,5 +192,5 @@ export function getVolumeUnit(system: UnitSystem): VolumeUnit { } export function getFuelEfficiencyUnit(system: UnitSystem): FuelEfficiencyUnit { - return system === 'metric' ? 'l100km' : 'mpg'; -} \ No newline at end of file + return system === 'metric' ? 'kml' : 'mpg'; +} diff --git a/frontend/src/features/fuel-logs/components/FuelLogEditDialog.tsx b/frontend/src/features/fuel-logs/components/FuelLogEditDialog.tsx index 5e6b888..f373479 100644 --- a/frontend/src/features/fuel-logs/components/FuelLogEditDialog.tsx +++ b/frontend/src/features/fuel-logs/components/FuelLogEditDialog.tsx @@ -15,6 +15,9 @@ import { Typography, useMediaQuery } from '@mui/material'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; import { FuelLogResponse, UpdateFuelLogRequest, FuelType } from '../types/fuel-logs.types'; import { useFuelGrades } from '../hooks/useFuelGrades'; @@ -130,41 +133,40 @@ export const FuelLogEditDialog: React.FC = ({ ); } - // Format datetime for input (datetime-local expects YYYY-MM-DDTHH:mm format) - const formatDateTimeForInput = (isoString: string) => { - const date = new Date(isoString); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - return `${year}-${month}-${day}T${hours}:${minutes}`; - }; return ( - - Edit Fuel Log - + + + Edit Fuel Log + {/* Date and Time */} - handleInputChange('dateTime', new Date(e.target.value).toISOString())} - InputLabelProps={{ shrink: true }} + value={formData.dateTime ? new Date(formData.dateTime) : null} + onChange={(newValue) => handleInputChange('dateTime', newValue?.toISOString() || '')} + format="MM/dd/yyyy hh:mm a" + slotProps={{ + textField: { + fullWidth: true, + sx: { + '& .MuiOutlinedInput-root': { + minHeight: '56px', // Ensure consistent height for mobile touch targets + } + } + } + }} /> @@ -284,5 +286,6 @@ export const FuelLogEditDialog: React.FC = ({ + ); }; diff --git a/frontend/src/features/fuel-logs/components/FuelLogForm.tsx b/frontend/src/features/fuel-logs/components/FuelLogForm.tsx index 42aad88..f238560 100644 --- a/frontend/src/features/fuel-logs/components/FuelLogForm.tsx +++ b/frontend/src/features/fuel-logs/components/FuelLogForm.tsx @@ -2,7 +2,10 @@ import React, { useEffect, useMemo, useState, useRef, memo } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; -import { Grid, Card, CardHeader, CardContent, TextField, Switch, FormControlLabel, Box, Button, CircularProgress } from '@mui/material'; +import { Grid, Card, CardHeader, CardContent, TextField, Box, Button, CircularProgress, ToggleButton, ToggleButtonGroup } from '@mui/material'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; import { VehicleSelector } from './VehicleSelector'; import { DistanceInput } from './DistanceInput'; import { FuelTypeSelector } from './FuelTypeSelector'; @@ -94,6 +97,19 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial return units > 0 && cost > 0 ? units * cost : 0; }, [fuelUnits, costPerUnit]); + // Watch for distance and fuel units to calculate efficiency + const watchedDistance = watch(useOdometer ? 'odometerReading' : 'tripDistance'); + const distanceValue = typeof watchedDistance === 'string' ? parseFloat(watchedDistance) : watchedDistance; + + const calculatedEfficiency = useMemo(() => { + const distance = distanceValue && !isNaN(distanceValue) ? distanceValue : 0; + const units = fuelUnits && !isNaN(fuelUnits) ? fuelUnits : 0; + if (distance > 0 && units > 0) { + return distance / units; + } + return 0; + }, [distanceValue, fuelUnits]); + const onSubmit = async (data: CreateFuelLogRequest) => { const payload: CreateFuelLogRequest = { ...data, @@ -123,29 +139,107 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial }, [useOdometer, setValue]); return ( - - } /> - -
- + + + } /> + + + + {/* Row 1: Select Vehicle */} ( )} /> + + {/* Row 2: Date/Time | MPG/km/L */} ( - + field.onChange(newValue?.toISOString() || '')} + format="MM/dd/yyyy hh:mm a" + slotProps={{ + textField: { + fullWidth: true, + error: !!errors.dateTime, + helperText: errors.dateTime?.message, + sx: { + '& .MuiOutlinedInput-root': { + minHeight: '56px', + } + } + } + }} + /> )} /> - setUseOdometer(e.target.checked)} />} label={`Use ${useOdometer ? 'Odometer' : 'Trip Distance'}`} /> + 0 ? calculatedEfficiency.toFixed(3) : ''} + fullWidth + InputProps={{ + readOnly: true, + sx: { + backgroundColor: 'grey.50', + '& .MuiOutlinedInput-input': { + cursor: 'default', + }, + }, + }} + helperText="Calculated from distance ÷ fuel amount" + sx={{ + '& .MuiOutlinedInput-root': { + minHeight: '56px', + } + }} + /> + + {/* Row 3: Odometer | Distance Input Method */} ( )} /> + + { + if (newValue !== null) { + setUseOdometer(newValue === 'odometer'); + } + }} + aria-label="distance input method" + fullWidth + sx={{ + height: '56px', // Match input field height + '& .MuiToggleButton-root': { + textTransform: 'none', + fontWeight: 500, + borderRadius: '8px', + height: '56px', // Ensure button height matches + '&.Mui-selected': { + backgroundColor: 'primary.main', + color: 'primary.contrastText', + '&:hover': { + backgroundColor: 'primary.dark', + }, + }, + }, + }} + > + + Trip Distance + + + Odometer Reading + + + ( ( @@ -205,6 +299,7 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial + ); }; diff --git a/frontend/src/features/fuel-logs/components/FuelLogsList.tsx b/frontend/src/features/fuel-logs/components/FuelLogsList.tsx index 2853461..1fbe3a9 100644 --- a/frontend/src/features/fuel-logs/components/FuelLogsList.tsx +++ b/frontend/src/features/fuel-logs/components/FuelLogsList.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { Card, CardContent, @@ -20,6 +20,7 @@ import { import { Edit, Delete } from '@mui/icons-material'; import { FuelLogResponse } from '../types/fuel-logs.types'; import { fuelLogsApi } from '../api/fuel-logs.api'; +import { useUnits } from '../../../core/units/UnitsContext'; interface FuelLogsListProps { logs?: FuelLogResponse[]; @@ -30,10 +31,26 @@ interface FuelLogsListProps { export const FuelLogsList: React.FC = ({ logs, onEdit, onDelete }) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + const { unitSystem, convertDistance, convertVolume } = useUnits(); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [logToDelete, setLogToDelete] = useState(null); const [isDeleting, setIsDeleting] = useState(false); + // Precompute previous odometer per log for delta calculations + const prevOdoById = useMemo(() => { + const map = new Map(); + if (!Array.isArray(logs)) return map; + const asc = [...logs].sort((a, b) => new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime()); + let lastOdo: number | undefined = undefined; + for (const l of asc) { + map.set(l.id, lastOdo); + if (typeof l.odometerReading === 'number' && !isNaN(l.odometerReading)) { + lastOdo = l.odometerReading; + } + } + return map; + }, [logs]); + const handleDeleteClick = (log: FuelLogResponse) => { setLogToDelete(log); setDeleteDialogOpen(true); @@ -110,6 +127,31 @@ export const FuelLogsList: React.FC = ({ logs, onEdit, onDele ? `Trip: ${log.tripDistance}` : 'No distance'; + // Compute local efficiency if not provided + let localEffLabel: string | null = null; + try { + const gallons = typeof log.fuelUnits === 'number' ? log.fuelUnits : undefined; + let miles: number | undefined = + typeof log.tripDistance === 'number' && !isNaN(log.tripDistance) + ? log.tripDistance + : undefined; + if (miles === undefined && typeof log.odometerReading === 'number') { + const prev = prevOdoById.get(log.id); + if (typeof prev === 'number' && log.odometerReading > prev) { + miles = log.odometerReading - prev; + } + } + if (typeof miles === 'number' && typeof gallons === 'number' && gallons > 0) { + if (unitSystem === 'metric') { + const km = convertDistance(miles); + const liters = convertVolume(gallons); + if (liters > 0) localEffLabel = `${(km / liters).toFixed(1)} km/L`; + } else { + localEffLabel = `${(miles / gallons).toFixed(1)} MPG`; + } + } + } catch {} + return ( = ({ logs, onEdit, onDele secondary={`${fuelUnits} @ $${costPerUnit} • ${distanceText}`} sx={{ flex: 1, minWidth: 0 }} /> - {log.efficiency && - typeof log.efficiency === 'number' && - !isNaN(log.efficiency) && - log.efficiencyLabel && ( + {(log.efficiency && typeof log.efficiency === 'number' && !isNaN(log.efficiency) && log.efficiencyLabel) || localEffLabel ? ( - )} + ) : null} = ({ logs, onEdit, onDele ); }; - diff --git a/frontend/src/features/fuel-logs/components/UnitSystemDisplay.tsx b/frontend/src/features/fuel-logs/components/UnitSystemDisplay.tsx index fcf68e8..774245d 100644 --- a/frontend/src/features/fuel-logs/components/UnitSystemDisplay.tsx +++ b/frontend/src/features/fuel-logs/components/UnitSystemDisplay.tsx @@ -4,11 +4,10 @@ import { UnitSystem } from '../types/fuel-logs.types'; export const UnitSystemDisplay: React.FC<{ unitSystem?: UnitSystem; showLabel?: string }> = ({ unitSystem, showLabel }) => { if (!unitSystem) return null; - const label = unitSystem === 'imperial' ? 'Imperial (miles, gallons, MPG)' : 'Metric (km, liters, L/100km)'; + const label = unitSystem === 'imperial' ? 'Imperial (miles, gallons, MPG)' : 'Metric (km, liters, km/L)'; return ( {showLabel ? `${showLabel} ` : ''}{label} ); }; - diff --git a/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx b/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx index e616d45..6163506 100644 --- a/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx +++ b/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx @@ -3,7 +3,7 @@ */ import React, { useMemo, useState } from 'react'; -import { Box, Typography, Button, Card, CardContent, Divider, FormControl, InputLabel, Select, MenuItem, List, ListItem, ListItemText } from '@mui/material'; +import { Box, Typography, Button, Card, CardContent, Divider, FormControl, InputLabel, Select, MenuItem, List, ListItem } from '@mui/material'; import { useQueryClient } from '@tanstack/react-query'; import { Vehicle } from '../types/vehicles.types'; import { useFuelLogs } from '../../fuel-logs/hooks/useFuelLogs'; @@ -57,6 +57,7 @@ export const VehicleDetailMobile: React.FC = ({ const { fuelLogs, isLoading: isFuelLoading } = useFuelLogs(vehicle.id); const queryClient = useQueryClient(); const [editingLog, setEditingLog] = useState(null); + // Unit conversions are now handled by the backend type VehicleRecord = { id: string; @@ -64,19 +65,71 @@ export const VehicleDetailMobile: React.FC = ({ date: string; // ISO summary: string; amount?: string; + secondary?: string; }; const records: VehicleRecord[] = useMemo(() => { const list: VehicleRecord[] = []; if (fuelLogs && Array.isArray(fuelLogs)) { + // Build a map of prior odometer readings to compute trip distance when missing + const logsAsc = [...(fuelLogs as FuelLogResponse[])].sort( + (a, b) => new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime() + ); + const prevOdoById = new Map(); + let lastOdo: number | undefined = undefined; + for (const l of logsAsc) { + prevOdoById.set(l.id, lastOdo); + if (typeof l.odometerReading === 'number' && !isNaN(l.odometerReading)) { + lastOdo = l.odometerReading; + } + } + for (const log of fuelLogs as FuelLogResponse[]) { - const parts: string[] = []; - if (log.fuelUnits) parts.push(`${Number(log.fuelUnits).toFixed(3)} units`); - if (log.fuelType) parts.push(`${log.fuelType}${log.fuelGrade ? ' ' + log.fuelGrade : ''}`); - if (log.efficiencyLabel) parts.push(log.efficiencyLabel); - const summary = parts.join(' • '); + // Use efficiency from API response (backend calculates this properly) + let efficiency = ''; + if (typeof log.efficiency === 'number' && log.efficiency > 0) { + // DEBUG: Log what the backend is actually returning + console.log('🔍 Fuel Log API Response:', { + efficiency: log.efficiency, + efficiencyLabel: log.efficiencyLabel, + logId: log.id + }); + // Backend returns efficiency in correct units based on user preference + efficiency = `${log.efficiencyLabel || 'MPG'}: ${log.efficiency.toFixed(3)}`; + } + + // Secondary line components + const secondaryParts: string[] = []; + + // Grade label (prefer grade only) + if (log.fuelGrade) { + secondaryParts.push(`Grade: ${log.fuelGrade}`); + } else if (log.fuelType) { + const ft = String(log.fuelType); + secondaryParts.push(ft.charAt(0).toUpperCase() + ft.slice(1)); + } + + // Date + secondaryParts.push(new Date(log.dateTime).toLocaleDateString()); + + // Type + secondaryParts.push('Fuel Log'); + + const summary = efficiency; // Primary line shows MPG/km/L from backend + const secondary = secondaryParts.join(' • '); // Secondary line shows Grade • Date • Type const amount = (typeof log.totalCost === 'number') ? `$${log.totalCost.toFixed(2)}` : undefined; - list.push({ id: log.id, type: 'Fuel Logs', date: log.dateTime, summary, amount }); + + // DEBUG: Log the final display values + console.log('🎯 Display Summary:', { summary, efficiency, logId: log.id }); + + list.push({ + id: log.id, + type: 'Fuel Logs', + date: log.dateTime, + summary, + amount, + secondary + }); } } return list.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); @@ -212,13 +265,21 @@ export const VehicleDetailMobile: React.FC = ({ {filteredRecords.map(rec => ( openEditLog(rec.id, rec.type)}> - - - {rec.amount || '—'} - + + {/* Primary line: MPG/km-L and amount */} + + + {rec.summary || 'MPG: —'} + + + {rec.amount || '—'} + + + {/* Secondary line: Grade • Date • Type */} + + {rec.secondary || `${new Date(rec.date).toLocaleDateString()} • ${rec.type}`} + + ))} diff --git a/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx b/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx index ca8e706..73f7436 100644 --- a/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx +++ b/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx @@ -4,7 +4,7 @@ import React, { useMemo, useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { Box, Typography, Button as MuiButton, Divider, FormControl, InputLabel, Select, MenuItem, Table, TableHead, TableRow, TableCell, TableBody } from '@mui/material'; +import { Box, Typography, Button as MuiButton, Divider, FormControl, InputLabel, Select, MenuItem, Table, TableHead, TableRow, TableCell, TableBody, Dialog, DialogTitle, DialogContent, useMediaQuery } from '@mui/material'; import { useQueryClient } from '@tanstack/react-query'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import EditIcon from '@mui/icons-material/Edit'; @@ -17,6 +17,8 @@ import { VehicleForm } from '../components/VehicleForm'; import { useFuelLogs } from '../../fuel-logs/hooks/useFuelLogs'; import { FuelLogResponse, UpdateFuelLogRequest } from '../../fuel-logs/types/fuel-logs.types'; import { FuelLogEditDialog } from '../../fuel-logs/components/FuelLogEditDialog'; +import { FuelLogForm } from '../../fuel-logs/components/FuelLogForm'; +// Unit conversions now handled by backend import { fuelLogsApi } from '../../fuel-logs/api/fuel-logs.api'; const DetailField: React.FC<{ @@ -49,6 +51,9 @@ export const VehicleDetailPage: React.FC = () => { const { fuelLogs, isLoading: isFuelLoading } = useFuelLogs(id); const queryClient = useQueryClient(); const [editingLog, setEditingLog] = useState(null); + const [showAddDialog, setShowAddDialog] = useState(false); + const isSmallScreen = useMediaQuery('(max-width:600px)'); + // Unit conversions now handled by backend // Define records list hooks BEFORE any early returns to keep hooks order stable type VehicleRecord = { @@ -62,11 +67,35 @@ export const VehicleDetailPage: React.FC = () => { const records: VehicleRecord[] = useMemo(() => { const list: VehicleRecord[] = []; if (fuelLogs && Array.isArray(fuelLogs)) { + // Build a map of prior odometer readings to compute trip distance when missing + const logsAsc = [...(fuelLogs as FuelLogResponse[])].sort( + (a, b) => new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime() + ); + const prevOdoById = new Map(); + let lastOdo: number | undefined = undefined; + for (const l of logsAsc) { + prevOdoById.set(l.id, lastOdo); + if (typeof l.odometerReading === 'number' && !isNaN(l.odometerReading)) { + lastOdo = l.odometerReading; + } + } + for (const log of fuelLogs as FuelLogResponse[]) { const parts: string[] = []; - if (log.fuelUnits) parts.push(`${Number(log.fuelUnits).toFixed(3)} units`); - if (log.fuelType) parts.push(`${log.fuelType}${log.fuelGrade ? ' ' + log.fuelGrade : ''}`); - if (log.efficiencyLabel) parts.push(log.efficiencyLabel); + + // Efficiency: Use backend calculation (primary display) + if (typeof log.efficiency === 'number' && log.efficiency > 0) { + parts.push(`${log.efficiencyLabel || 'MPG'}: ${log.efficiency.toFixed(3)}`); + } + + // Grade label (secondary display) + if (log.fuelGrade) { + parts.push(`Grade: ${log.fuelGrade}`); + } else if (log.fuelType) { + const ft = String(log.fuelType); + parts.push(ft.charAt(0).toUpperCase() + ft.slice(1)); + } + const summary = parts.join(' • '); const amount = (typeof log.totalCost === 'number') ? `$${log.totalCost.toFixed(2)}` : undefined; list.push({ id: log.id, type: 'Fuel Logs', date: log.dateTime, summary, amount }); @@ -240,10 +269,11 @@ export const VehicleDetailPage: React.FC = () => { - } sx={{ borderRadius: '999px' }} + onClick={() => setShowAddDialog(true)} > Add Fuel Log @@ -343,7 +373,7 @@ export const VehicleDetailPage: React.FC = () => { )} {!isFuelLoading && filteredRecords.map((rec) => ( handleRowClick(rec.id, rec.type)}> - {new Date(rec.date).toLocaleString()} + {new Date(rec.date).toLocaleDateString()} {rec.type} {rec.summary} {rec.amount || '—'} @@ -360,6 +390,33 @@ export const VehicleDetailPage: React.FC = () => { onClose={handleCloseEdit} onSave={handleSaveEdit} /> + + {/* Add Fuel Log Dialog */} + setShowAddDialog(false)} + maxWidth="md" + fullWidth + fullScreen={isSmallScreen} + PaperProps={{ + sx: { maxHeight: '90vh' } + }} + > + Add Fuel Log + + + { + setShowAddDialog(false); + // Refresh fuel logs data + queryClient.invalidateQueries({ queryKey: ['fuelLogs', vehicle?.id] }); + queryClient.invalidateQueries({ queryKey: ['fuelLogs'] }); + }} + /> + + +
);