Files
motovaultpro/docs/changes/fuel-logs-v1/FUEL-LOGS-PHASE-4.md
Eric Gullickson a052040e3a Initial Commit
2025-09-17 16:09:15 -05:00

1080 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<CreateFuelLogRequest>;
}
export const FuelLogForm: React.FC<FuelLogFormProps> = ({
onSuccess,
onCancel,
initialData
}) => {
const { createFuelLog, isLoading } = useFuelLogs();
const { userSettings } = useUserSettings();
const [distanceType, setDistanceType] = useState<DistanceType>('trip');
const [calculatedCost, setCalculatedCost] = useState<number>(0);
const {
control,
handleSubmit,
watch,
setValue,
formState: { errors, isValid }
} = useForm<CreateFuelLogRequest>({
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 (
<Card>
<CardHeader
title="Add Fuel Log"
subheader={
<UnitSystemDisplay
unitSystem={userSettings?.unitSystem}
showLabel="Displaying in"
/>
}
/>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container spacing={3}>
{/* Vehicle Selection */}
<Grid item xs={12}>
<Controller
name="vehicleId"
control={control}
render={({ field }) => (
<VehicleSelector
value={field.value}
onChange={field.onChange}
error={errors.vehicleId?.message}
required
/>
)}
/>
</Grid>
{/* Date/Time */}
<Grid item xs={12} sm={6}>
<Controller
name="dateTime"
control={control}
render={({ field }) => (
<TextField
{...field}
label="Date & Time"
type="datetime-local"
fullWidth
error={!!errors.dateTime}
helperText={errors.dateTime?.message}
InputLabelProps={{ shrink: true }}
/>
)}
/>
</Grid>
{/* Distance Type Toggle */}
<Grid item xs={12} sm={6}>
<FormControlLabel
control={
<Switch
checked={distanceType === 'odometer'}
onChange={(e) => {
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'}`}
/>
</Grid>
{/* Distance Input */}
<Grid item xs={12} sm={6}>
<Controller
name={distanceType === 'odometer' ? 'odometerReading' : 'tripDistance'}
control={control}
render={({ field }) => (
<DistanceInput
type={distanceType}
value={field.value}
onChange={field.onChange}
unitSystem={userSettings?.unitSystem}
error={distanceType === 'odometer' ? errors.odometerReading?.message : errors.tripDistance?.message}
/>
)}
/>
</Grid>
{/* Fuel Type & Grade */}
<Grid item xs={12} sm={6}>
<Controller
name="fuelType"
control={control}
render={({ field: fuelTypeField }) => (
<Controller
name="fuelGrade"
control={control}
render={({ field: fuelGradeField }) => (
<FuelTypeSelector
fuelType={fuelTypeField.value}
fuelGrade={fuelGradeField.value}
onFuelTypeChange={fuelTypeField.onChange}
onFuelGradeChange={fuelGradeField.onChange}
error={errors.fuelType?.message || errors.fuelGrade?.message}
/>
)}
/>
)}
/>
</Grid>
{/* Fuel Amount */}
<Grid item xs={12} sm={6}>
<Controller
name="fuelUnits"
control={control}
render={({ field }) => (
<TextField
{...field}
label={`Fuel Amount (${userSettings?.unitSystem === 'imperial' ? 'gallons' : 'liters'})`}
type="number"
inputProps={{ step: 0.001, min: 0.001 }}
fullWidth
error={!!errors.fuelUnits}
helperText={errors.fuelUnits?.message}
/>
)}
/>
</Grid>
{/* Cost Per Unit */}
<Grid item xs={12} sm={6}>
<Controller
name="costPerUnit"
control={control}
render={({ field }) => (
<TextField
{...field}
label={`Cost Per ${userSettings?.unitSystem === 'imperial' ? 'Gallon' : 'Liter'}`}
type="number"
inputProps={{ step: 0.001, min: 0.001 }}
fullWidth
error={!!errors.costPerUnit}
helperText={errors.costPerUnit?.message}
InputProps={{
startAdornment: <InputAdornment position="start">$</InputAdornment>
}}
/>
)}
/>
</Grid>
{/* Real-time Total Cost Display */}
<Grid item xs={12}>
<CostCalculator
fuelUnits={fuelUnits}
costPerUnit={costPerUnit}
calculatedCost={calculatedCost}
unitSystem={userSettings?.unitSystem}
/>
</Grid>
{/* Location */}
<Grid item xs={12}>
<Controller
name="locationData"
control={control}
render={({ field }) => (
<LocationInput
value={field.value}
onChange={field.onChange}
placeholder="Station location (optional)"
/>
)}
/>
</Grid>
{/* Notes */}
<Grid item xs={12}>
<Controller
name="notes"
control={control}
render={({ field }) => (
<TextField
{...field}
label="Notes (optional)"
multiline
rows={3}
fullWidth
error={!!errors.notes}
helperText={errors.notes?.message}
placeholder="Additional notes about this fuel log..."
/>
)}
/>
</Grid>
{/* Form Actions */}
<Grid item xs={12}>
<Box display="flex" gap={2} justifyContent="flex-end">
{onCancel && (
<Button variant="outlined" onClick={onCancel}>
Cancel
</Button>
)}
<Button
type="submit"
variant="contained"
disabled={!isValid || isLoading}
startIcon={isLoading ? <CircularProgress size={20} /> : undefined}
>
{isLoading ? 'Adding...' : 'Add Fuel Log'}
</Button>
</Box>
</Grid>
</Grid>
</form>
</CardContent>
</Card>
);
};
```
### 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<FuelTypeSelectorProps> = ({
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 (
<Grid container spacing={2}>
{/* Fuel Type */}
<Grid item xs={12} sm={6}>
<FormControl fullWidth error={!!error}>
<InputLabel>Fuel Type</InputLabel>
<Select
value={fuelType || ''}
onChange={(e) => onFuelTypeChange(e.target.value as FuelType)}
label="Fuel Type"
disabled={disabled}
>
<MenuItem value={FuelType.GASOLINE}>Gasoline</MenuItem>
<MenuItem value={FuelType.DIESEL}>Diesel</MenuItem>
<MenuItem value={FuelType.ELECTRIC}>Electric</MenuItem>
</Select>
{error && <FormHelperText>{error}</FormHelperText>}
</FormControl>
</Grid>
{/* Fuel Grade (conditional) */}
<Grid item xs={12} sm={6}>
<FormControl fullWidth disabled={disabled || isLoading || fuelType === FuelType.ELECTRIC}>
<InputLabel>
Fuel Grade {fuelType === FuelType.ELECTRIC ? '(N/A for Electric)' : ''}
</InputLabel>
<Select
value={fuelGrade || ''}
onChange={(e) => onFuelGradeChange(e.target.value as FuelGrade)}
label="Fuel Grade"
>
{fuelGrades?.map((grade) => (
<MenuItem key={grade.value} value={grade.value}>
{grade.label}
</MenuItem>
))}
</Select>
{fuelType !== FuelType.ELECTRIC && (
<FormHelperText>
{isLoading ? 'Loading grades...' : 'Select appropriate fuel grade'}
</FormHelperText>
)}
</FormControl>
</Grid>
</Grid>
);
};
```
### 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<VehicleSelectorProps> = ({
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 (
<FormControl fullWidth disabled>
<InputLabel>Loading vehicles...</InputLabel>
<Select value="" label="Loading vehicles...">
<MenuItem value="">
<em>Loading...</em>
</MenuItem>
</Select>
</FormControl>
);
}
if (!vehicles || vehicles.length === 0) {
return (
<Box
sx={{
p: 2,
border: 1,
borderColor: 'divider',
borderRadius: 1,
bgcolor: 'background.paper'
}}
>
<Box display="flex" alignItems="center" gap={1} mb={1}>
<DirectionsCar color="action" />
<Typography variant="body2" color="text.secondary">
No vehicles found
</Typography>
</Box>
<Typography variant="caption" color="text.secondary">
You need to add a vehicle before creating fuel logs.{' '}
<Link href="/vehicles/add" color="primary">
Add your first vehicle
</Link>
</Typography>
</Box>
);
}
return (
<FormControl fullWidth error={!!error} required={required}>
<InputLabel>Select Vehicle</InputLabel>
<Select
value={value || ''}
onChange={(e) => onChange(e.target.value)}
label="Select Vehicle"
disabled={disabled}
renderValue={(selected) => {
const selectedVehicle = vehicles.find(v => v.id === selected);
return selectedVehicle ? (
<Box display="flex" alignItems="center" gap={1}>
<DirectionsCar fontSize="small" />
{formatVehicleDisplay(selectedVehicle)}
</Box>
) : '';
}}
>
{vehicles.map((vehicle) => (
<MenuItem key={vehicle.id} value={vehicle.id}>
<Box display="flex" alignItems="center" gap={1}>
<DirectionsCar fontSize="small" />
<Box>
<Typography variant="body2">
{formatVehicleDisplay(vehicle)}
</Typography>
{vehicle.licensePlate && (
<Typography variant="caption" color="text.secondary">
{vehicle.licensePlate}
</Typography>
)}
</Box>
</Box>
</MenuItem>
))}
</Select>
{error && <FormHelperText>{error}</FormHelperText>}
</FormControl>
);
};
```
### 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<DistanceInputProps> = ({
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 (
<Box>
<TextField
label={getLabel()}
type="number"
value={value || ''}
onChange={(e) => onChange(parseFloat(e.target.value) || 0)}
fullWidth
error={!!error}
disabled={disabled}
placeholder={getPlaceholder()}
inputProps={{
step: type === 'trip' ? 0.1 : 1,
min: 0
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getUnits()}
</InputAdornment>
)
}}
/>
<FormHelperText error={!!error}>
{getHelperText()}
</FormHelperText>
{type === 'odometer' && (
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5 }}>
💡 Tip: Use trip distance if you don't want to track odometer readings
</Typography>
)}
</Box>
);
};
```
### 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<CostCalculatorProps> = ({
fuelUnits,
costPerUnit,
calculatedCost,
unitSystem = UnitSystem.IMPERIAL
}) => {
const unitLabel = unitSystem === UnitSystem.IMPERIAL ? 'gallons' : 'liters';
if (!fuelUnits || !costPerUnit) {
return (
<Card variant="outlined" sx={{ bgcolor: 'background.default' }}>
<CardContent sx={{ py: 2 }}>
<Typography variant="body2" color="text.secondary" align="center">
Enter fuel amount and cost per unit to see total cost
</Typography>
</CardContent>
</Card>
);
}
return (
<Card variant="outlined" sx={{ bgcolor: 'primary.50', borderColor: 'primary.200' }}>
<CardContent sx={{ py: 2 }}>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
<Typography variant="body2" color="text.secondary">
Cost Calculation
</Typography>
<Chip
label="Real-time"
size="small"
color="primary"
variant="outlined"
/>
</Box>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
<Typography variant="body2">
{fuelUnits.toFixed(3)} {unitLabel} × ${costPerUnit.toFixed(3)}
</Typography>
<Typography variant="h6" color="primary.main" fontWeight="bold">
${calculatedCost.toFixed(2)}
</Typography>
</Box>
<Divider sx={{ my: 1 }} />
<Typography variant="caption" color="text.secondary">
Total cost will be automatically calculated
</Typography>
</CardContent>
</Card>
);
};
```
## 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<string>('');
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 (
<Container maxWidth="md">
<Box py={3}>
<FuelLogForm
initialData={{ vehicleId: selectedVehicleId }}
onSuccess={() => setShowAddForm(false)}
onCancel={() => setShowAddForm(false)}
/>
</Box>
</Container>
);
}
return (
<Container maxWidth="lg">
<Box py={3}>
{/* Header */}
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
<Typography variant="h4" component="h1">
Fuel Logs
</Typography>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => setShowAddForm(true)}
disabled={!selectedVehicleId}
>
Add Fuel Log
</Button>
</Box>
{/* Vehicle Selection */}
<Card sx={{ mb: 3 }}>
<CardContent>
<Grid container spacing={2} alignItems="center">
<Grid item xs={12} md={8}>
<VehicleSelector
value={selectedVehicleId}
onChange={setSelectedVehicleId}
required
/>
</Grid>
<Grid item xs={12} md={4}>
<Box display="flex" gap={1} alignItems="center">
<AnalyticsIcon color="action" />
<Typography variant="body2" color="text.secondary">
{fuelLogs?.length || 0} fuel logs recorded
</Typography>
</Box>
</Grid>
</Grid>
</CardContent>
</Card>
{/* Content Tabs */}
{selectedVehicleId && (
<>
<Box borderBottom={1} borderColor="divider" mb={3}>
<Tabs value={activeTab} onChange={handleTabChange}>
<Tab label="Fuel Logs" />
<Tab label="Statistics" />
</Tabs>
</Box>
{/* Tab Panels */}
{activeTab === 0 && (
<FuelLogsList
vehicleId={selectedVehicleId}
fuelLogs={fuelLogs}
isLoading={isLoading}
onEdit={(logId) => {
// Navigate to edit form or open modal
console.log('Edit fuel log:', logId);
}}
/>
)}
{activeTab === 1 && (
<FuelStatsCard vehicleId={selectedVehicleId} />
)}
</>
)}
{/* Empty State */}
{!selectedVehicleId && vehicles && vehicles.length > 1 && (
<Card>
<CardContent sx={{ textAlign: 'center', py: 6 }}>
<Typography variant="h6" gutterBottom>
Select a Vehicle
</Typography>
<Typography color="text.secondary">
Choose a vehicle to view and manage its fuel logs
</Typography>
</CardContent>
</Card>
)}
</Box>
</Container>
);
};
```
## 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)