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

32 KiB
Raw Blame History

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