32 KiB
32 KiB
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
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
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
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
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
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
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
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
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