# Phase 4: Frontend Implementation ## Overview Create comprehensive React components for the enhanced fuel logs feature with dynamic forms, real-time calculations, Imperial/Metric support, and mobile-optimized UI. ## Prerequisites - ✅ Phase 1-3 completed (database, business logic, API) - All backend services tested and functional - API endpoints available and documented ## Component Architecture ### Main Components Structure ``` frontend/src/features/fuel-logs/ ├── components/ │ ├── FuelLogForm.tsx # Main form component │ ├── VehicleSelector.tsx # Vehicle dropdown │ ├── DistanceInput.tsx # Trip/odometer toggle │ ├── FuelTypeSelector.tsx # Fuel type with cascading grades │ ├── UnitSystemDisplay.tsx # Imperial/Metric formatting │ ├── LocationInput.tsx # Future Google Maps integration │ ├── CostCalculator.tsx # Real-time cost calculation │ └── FuelLogsList.tsx # Enhanced logs display ├── hooks/ │ ├── useFuelLogs.tsx # Fuel logs API integration │ ├── useFuelGrades.tsx # Dynamic fuel grades │ ├── useUserSettings.tsx # Unit system preferences │ └── useFormValidation.tsx # Enhanced form validation ├── utils/ │ ├── unitConversion.ts # Frontend unit utilities │ ├── fuelGradeUtils.ts # Fuel grade helpers │ └── formValidation.ts # Client-side validation ├── types/ │ └── fuel-logs.types.ts # Frontend type definitions └── pages/ ├── FuelLogsPage.tsx # Main fuel logs page └── FuelLogDetailPage.tsx # Individual log details ``` ## Core Components ### Enhanced Fuel Log Form **File**: `frontend/src/features/fuel-logs/components/FuelLogForm.tsx` ```tsx import React, { useState, useEffect } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; import { Box, Button, Card, CardContent, CardHeader, Typography, Alert, Grid, Switch, FormControlLabel } from '@mui/material'; import { VehicleSelector } from './VehicleSelector'; import { DistanceInput } from './DistanceInput'; import { FuelTypeSelector } from './FuelTypeSelector'; import { UnitSystemDisplay } from './UnitSystemDisplay'; import { LocationInput } from './LocationInput'; import { CostCalculator } from './CostCalculator'; import { useFuelLogs } from '../hooks/useFuelLogs'; import { useUserSettings } from '../hooks/useUserSettings'; import { CreateFuelLogRequest, FuelType, DistanceType } from '../types/fuel-logs.types'; // Validation schema const createFuelLogSchema = yup.object({ vehicleId: yup.string().required('Vehicle is required'), dateTime: yup.date().max(new Date(), 'Cannot create logs in the future').required('Date/time is required'), distanceType: yup.string().oneOf(['odometer', 'trip']).required(), odometerReading: yup.number().when('distanceType', { is: 'odometer', then: yup.number().min(1, 'Odometer must be positive').required('Odometer reading is required'), otherwise: yup.number().nullable() }), tripDistance: yup.number().when('distanceType', { is: 'trip', then: yup.number().min(0.1, 'Trip distance must be positive').required('Trip distance is required'), otherwise: yup.number().nullable() }), fuelType: yup.string().oneOf(Object.values(FuelType)).required('Fuel type is required'), fuelGrade: yup.string().nullable(), fuelUnits: yup.number().min(0.01, 'Fuel amount must be positive').required('Fuel amount is required'), costPerUnit: yup.number().min(0.01, 'Cost per unit must be positive').required('Cost per unit is required'), locationData: yup.object().nullable(), notes: yup.string().max(500, 'Notes cannot exceed 500 characters') }); interface FuelLogFormProps { onSuccess?: () => void; onCancel?: () => void; initialData?: Partial; } export const FuelLogForm: React.FC = ({ onSuccess, onCancel, initialData }) => { const { createFuelLog, isLoading } = useFuelLogs(); const { userSettings } = useUserSettings(); const [distanceType, setDistanceType] = useState('trip'); const [calculatedCost, setCalculatedCost] = useState(0); const { control, handleSubmit, watch, setValue, formState: { errors, isValid } } = useForm({ resolver: yupResolver(createFuelLogSchema), defaultValues: { dateTime: new Date().toISOString().slice(0, 16), // Current datetime distanceType: 'trip', fuelType: FuelType.GASOLINE, ...initialData }, mode: 'onChange' }); // Watch form values for real-time calculations const watchedValues = watch(['fuelUnits', 'costPerUnit', 'fuelType']); const [fuelUnits, costPerUnit, fuelType] = watchedValues; // Real-time cost calculation useEffect(() => { if (fuelUnits && costPerUnit) { const cost = fuelUnits * costPerUnit; setCalculatedCost(cost); } }, [fuelUnits, costPerUnit]); const onSubmit = async (data: CreateFuelLogRequest) => { try { // Prepare submission data const submitData = { ...data, totalCost: calculatedCost, // Clear unused distance field odometerReading: distanceType === 'odometer' ? data.odometerReading : undefined, tripDistance: distanceType === 'trip' ? data.tripDistance : undefined }; await createFuelLog(submitData); onSuccess?.(); } catch (error) { console.error('Failed to create fuel log:', error); } }; return ( } />
{/* Vehicle Selection */} ( )} /> {/* Date/Time */} ( )} /> {/* Distance Type Toggle */} { const newType = e.target.checked ? 'odometer' : 'trip'; setDistanceType(newType); setValue('distanceType', newType); // Clear the unused field if (newType === 'odometer') { setValue('tripDistance', undefined); } else { setValue('odometerReading', undefined); } }} /> } label={`Use ${distanceType === 'odometer' ? 'Odometer Reading' : 'Trip Distance'}`} /> {/* Distance Input */} ( )} /> {/* Fuel Type & Grade */} ( ( )} /> )} /> {/* Fuel Amount */} ( )} /> {/* Cost Per Unit */} ( $ }} /> )} /> {/* Real-time Total Cost Display */} {/* Location */} ( )} /> {/* Notes */} ( )} /> {/* Form Actions */} {onCancel && ( )}
); }; ``` ### Dynamic Fuel Type Selector **File**: `frontend/src/features/fuel-logs/components/FuelTypeSelector.tsx` ```tsx import React, { useEffect } from 'react'; import { FormControl, InputLabel, Select, MenuItem, Grid, FormHelperText } from '@mui/material'; import { FuelType, FuelGrade } from '../types/fuel-logs.types'; import { useFuelGrades } from '../hooks/useFuelGrades'; interface FuelTypeSelectorProps { fuelType: FuelType; fuelGrade?: FuelGrade; onFuelTypeChange: (fuelType: FuelType) => void; onFuelGradeChange: (fuelGrade?: FuelGrade) => void; error?: string; disabled?: boolean; } export const FuelTypeSelector: React.FC = ({ fuelType, fuelGrade, onFuelTypeChange, onFuelGradeChange, error, disabled = false }) => { const { fuelGrades, isLoading } = useFuelGrades(fuelType); // Clear fuel grade when fuel type changes and grades don't include current grade useEffect(() => { if (fuelGrade && fuelGrades && !fuelGrades.some(grade => grade.value === fuelGrade)) { onFuelGradeChange(undefined); } }, [fuelType, fuelGrades, fuelGrade, onFuelGradeChange]); // Auto-select default grade when fuel type changes useEffect(() => { if (!fuelGrade && fuelGrades && fuelGrades.length > 0) { // Auto-select first grade (typically the most common) onFuelGradeChange(fuelGrades[0].value); } }, [fuelGrades, fuelGrade, onFuelGradeChange]); return ( {/* Fuel Type */} Fuel Type {error && {error}} {/* Fuel Grade (conditional) */} Fuel Grade {fuelType === FuelType.ELECTRIC ? '(N/A for Electric)' : ''} {fuelType !== FuelType.ELECTRIC && ( {isLoading ? 'Loading grades...' : 'Select appropriate fuel grade'} )} ); }; ``` ### Vehicle Selector Component **File**: `frontend/src/features/fuel-logs/components/VehicleSelector.tsx` ```tsx import React from 'react'; import { FormControl, InputLabel, Select, MenuItem, FormHelperText, Box, Typography } from '@mui/material'; import { DirectionsCar } from '@mui/icons-material'; import { useVehicles } from '../../vehicles/hooks/useVehicles'; import { Vehicle } from '../../vehicles/types/vehicle.types'; interface VehicleSelectorProps { value: string; onChange: (vehicleId: string) => void; error?: string; required?: boolean; disabled?: boolean; } export const VehicleSelector: React.FC = ({ value, onChange, error, required = false, disabled = false }) => { const { vehicles, isLoading } = useVehicles(); const formatVehicleDisplay = (vehicle: Vehicle): string => { const parts = [vehicle.year, vehicle.make, vehicle.model, vehicle.nickname] .filter(Boolean); return parts.join(' '); }; if (isLoading) { return ( Loading vehicles... ); } if (!vehicles || vehicles.length === 0) { return ( No vehicles found You need to add a vehicle before creating fuel logs.{' '} Add your first vehicle ); } return ( Select Vehicle {error && {error}} ); }; ``` ### Distance Input Component **File**: `frontend/src/features/fuel-logs/components/DistanceInput.tsx` ```tsx import React from 'react'; import { TextField, InputAdornment, FormHelperText, Box, Typography } from '@mui/material'; import { UnitSystem, DistanceType } from '../types/fuel-logs.types'; interface DistanceInputProps { type: DistanceType; value?: number; onChange: (value: number) => void; unitSystem?: UnitSystem; error?: string; disabled?: boolean; } export const DistanceInput: React.FC = ({ type, value, onChange, unitSystem = UnitSystem.IMPERIAL, error, disabled = false }) => { const getUnits = () => { return unitSystem === UnitSystem.IMPERIAL ? 'miles' : 'kilometers'; }; const getLabel = () => { if (type === 'odometer') { return `Odometer Reading (${getUnits()})`; } return `Trip Distance (${getUnits()})`; }; const getHelperText = () => { if (error) return error; if (type === 'odometer') { return 'Current odometer reading on your vehicle'; } return 'Distance traveled since last fuel log'; }; const getPlaceholder = () => { if (type === 'odometer') { return unitSystem === UnitSystem.IMPERIAL ? '125,000' : '201,168'; } return unitSystem === UnitSystem.IMPERIAL ? '300' : '483'; }; return ( onChange(parseFloat(e.target.value) || 0)} fullWidth error={!!error} disabled={disabled} placeholder={getPlaceholder()} inputProps={{ step: type === 'trip' ? 0.1 : 1, min: 0 }} InputProps={{ endAdornment: ( {getUnits()} ) }} /> {getHelperText()} {type === 'odometer' && ( 💡 Tip: Use trip distance if you don't want to track odometer readings )} ); }; ``` ### Real-time Cost Calculator **File**: `frontend/src/features/fuel-logs/components/CostCalculator.tsx` ```tsx import React from 'react'; import { Box, Typography, Card, CardContent, Divider, Chip } from '@mui/material'; import { UnitSystem } from '../types/fuel-logs.types'; interface CostCalculatorProps { fuelUnits?: number; costPerUnit?: number; calculatedCost: number; unitSystem?: UnitSystem; } export const CostCalculator: React.FC = ({ fuelUnits, costPerUnit, calculatedCost, unitSystem = UnitSystem.IMPERIAL }) => { const unitLabel = unitSystem === UnitSystem.IMPERIAL ? 'gallons' : 'liters'; if (!fuelUnits || !costPerUnit) { return ( Enter fuel amount and cost per unit to see total cost ); } return ( Cost Calculation {fuelUnits.toFixed(3)} {unitLabel} × ${costPerUnit.toFixed(3)} ${calculatedCost.toFixed(2)} Total cost will be automatically calculated ); }; ``` ## Custom Hooks ### Fuel Logs API Hook **File**: `frontend/src/features/fuel-logs/hooks/useFuelLogs.tsx` ```tsx import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { fuelLogsApi } from '../api/fuel-logs.api'; import { CreateFuelLogRequest, FuelLogResponse, EnhancedFuelStats } from '../types/fuel-logs.types'; export const useFuelLogs = (vehicleId?: string) => { const queryClient = useQueryClient(); // Fetch user's fuel logs const { data: fuelLogs, isLoading, error } = useQuery({ queryKey: ['fuelLogs', vehicleId], queryFn: () => vehicleId ? fuelLogsApi.getFuelLogsByVehicle(vehicleId) : fuelLogsApi.getUserFuelLogs(), enabled: true }); // Create fuel log mutation const createMutation = useMutation({ mutationFn: (data: CreateFuelLogRequest) => fuelLogsApi.createFuelLog(data), onSuccess: () => { // Invalidate relevant queries queryClient.invalidateQueries({ queryKey: ['fuelLogs'] }); queryClient.invalidateQueries({ queryKey: ['fuelStats'] }); queryClient.invalidateQueries({ queryKey: ['vehicles'] }); // For odometer updates } }); // Update fuel log mutation const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: UpdateFuelLogRequest }) => fuelLogsApi.updateFuelLog(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['fuelLogs'] }); queryClient.invalidateQueries({ queryKey: ['fuelStats'] }); } }); // Delete fuel log mutation const deleteMutation = useMutation({ mutationFn: (id: string) => fuelLogsApi.deleteFuelLog(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['fuelLogs'] }); queryClient.invalidateQueries({ queryKey: ['fuelStats'] }); } }); return { fuelLogs, isLoading, error, createFuelLog: createMutation.mutateAsync, updateFuelLog: updateMutation.mutateAsync, deleteFuelLog: deleteMutation.mutateAsync, isCreating: createMutation.isPending, isUpdating: updateMutation.isPending, isDeleting: deleteMutation.isPending }; }; export const useFuelStats = (vehicleId: string) => { return useQuery({ queryKey: ['fuelStats', vehicleId], queryFn: () => fuelLogsApi.getVehicleStats(vehicleId), enabled: !!vehicleId }); }; ``` ### Dynamic Fuel Grades Hook **File**: `frontend/src/features/fuel-logs/hooks/useFuelGrades.tsx` ```tsx import { useQuery } from '@tanstack/react-query'; import { fuelLogsApi } from '../api/fuel-logs.api'; import { FuelType } from '../types/fuel-logs.types'; export const useFuelGrades = (fuelType?: FuelType) => { const { data: fuelGrades, isLoading, error } = useQuery({ queryKey: ['fuelGrades', fuelType], queryFn: () => fuelLogsApi.getFuelGrades(fuelType!), enabled: !!fuelType, staleTime: 1000 * 60 * 60, // 1 hour (grades don't change often) }); return { fuelGrades: fuelGrades?.grades || [], isLoading, error }; }; export const useFuelTypes = () => { return useQuery({ queryKey: ['fuelTypes'], queryFn: () => fuelLogsApi.getAllFuelTypes(), staleTime: 1000 * 60 * 60 * 24, // 24 hours (fuel types are static) }); }; ``` ## Main Pages ### Fuel Logs Page **File**: `frontend/src/features/fuel-logs/pages/FuelLogsPage.tsx` ```tsx import React, { useState } from 'react'; import { Container, Typography, Box, Button, Tabs, Tab, Grid, Card, CardContent } from '@mui/material'; import { Add as AddIcon, Analytics as AnalyticsIcon } from '@mui/icons-material'; import { FuelLogForm } from '../components/FuelLogForm'; import { FuelLogsList } from '../components/FuelLogsList'; import { FuelStatsCard } from '../components/FuelStatsCard'; import { VehicleSelector } from '../components/VehicleSelector'; import { useFuelLogs } from '../hooks/useFuelLogs'; import { useVehicles } from '../../vehicles/hooks/useVehicles'; export const FuelLogsPage: React.FC = () => { const [selectedVehicleId, setSelectedVehicleId] = useState(''); const [activeTab, setActiveTab] = useState(0); const [showAddForm, setShowAddForm] = useState(false); const { vehicles } = useVehicles(); const { fuelLogs, isLoading } = useFuelLogs(selectedVehicleId || undefined); // Auto-select first vehicle if only one exists React.useEffect(() => { if (vehicles && vehicles.length === 1 && !selectedVehicleId) { setSelectedVehicleId(vehicles[0].id); } }, [vehicles, selectedVehicleId]); const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { setActiveTab(newValue); }; if (showAddForm) { return ( setShowAddForm(false)} onCancel={() => setShowAddForm(false)} /> ); } return ( {/* Header */} Fuel Logs {/* Vehicle Selection */} {fuelLogs?.length || 0} fuel logs recorded {/* Content Tabs */} {selectedVehicleId && ( <> {/* Tab Panels */} {activeTab === 0 && ( { // Navigate to edit form or open modal console.log('Edit fuel log:', logId); }} /> )} {activeTab === 1 && ( )} )} {/* Empty State */} {!selectedVehicleId && vehicles && vehicles.length > 1 && ( Select a Vehicle Choose a vehicle to view and manage its fuel logs )} ); }; ``` ## Success Criteria ### Phase 4 Complete When: - ✅ Enhanced fuel log form fully functional - ✅ Dynamic fuel type/grade selection working - ✅ Imperial/Metric units display correctly - ✅ Real-time cost calculation working - ✅ Trip distance vs odometer toggle functional - ✅ Vehicle selection integrated - ✅ Mobile-responsive design - ✅ Form validation comprehensive - ✅ API integration complete - ✅ Error handling robust ### Ready for Phase 5 When: - All React components tested and functional - User interface intuitive and mobile-friendly - Form validation catching all user errors - API integration stable and performant - Ready for location service integration --- **Next Phase**: [Phase 5 - Future Integration Preparation](FUEL-LOGS-PHASE-5.md)