Initial Commit

This commit is contained in:
Eric Gullickson
2025-09-17 16:09:15 -05:00
parent 0cdb9803de
commit a052040e3a
373 changed files with 437090 additions and 6773 deletions

View File

@@ -0,0 +1,35 @@
import { apiClient } from '../../../core/api/client';
import { CreateFuelLogRequest, FuelLogResponse, EnhancedFuelStats, FuelType, FuelGradeOption } from '../types/fuel-logs.types';
export const fuelLogsApi = {
async create(data: CreateFuelLogRequest): Promise<FuelLogResponse> {
const res = await apiClient.post('/fuel-logs', data);
return res.data;
},
async getUserFuelLogs(): Promise<FuelLogResponse[]> {
const res = await apiClient.get('/fuel-logs');
return res.data;
},
async getFuelLogsByVehicle(vehicleId: string): Promise<FuelLogResponse[]> {
const res = await apiClient.get(`/fuel-logs/vehicle/${vehicleId}`);
return res.data;
},
async getVehicleStats(vehicleId: string): Promise<EnhancedFuelStats> {
const res = await apiClient.get(`/fuel-logs/vehicle/${vehicleId}/stats`);
return res.data;
},
async getFuelTypes(): Promise<{ value: FuelType; label: string; grades: FuelGradeOption[] }[]> {
const res = await apiClient.get('/fuel-logs/fuel-types');
return res.data.fuelTypes;
},
async getFuelGrades(fuelType: FuelType): Promise<FuelGradeOption[]> {
const res = await apiClient.get(`/fuel-logs/fuel-grades/${fuelType}`);
return res.data.grades;
}
};

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { Card, CardContent, Typography, Box, Chip } from '@mui/material';
import { UnitSystem } from '../types/fuel-logs.types';
interface Props {
fuelUnits?: number;
costPerUnit?: number;
calculatedCost: number;
unitSystem?: UnitSystem;
}
export const CostCalculator: React.FC<Props> = ({ fuelUnits, costPerUnit, calculatedCost, unitSystem = 'imperial' }) => {
const unitLabel = unitSystem === 'imperial' ? 'gallons' : 'liters';
// Ensure we have valid numbers
const safeUnits = typeof fuelUnits === 'number' && !isNaN(fuelUnits) ? fuelUnits : 0;
const safeCostPerUnit = typeof costPerUnit === 'number' && !isNaN(costPerUnit) ? costPerUnit : 0;
const safeCost = typeof calculatedCost === 'number' && !isNaN(calculatedCost) ? calculatedCost : 0;
if (!fuelUnits || !costPerUnit || safeUnits <= 0 || safeCostPerUnit <= 0) {
return (
<Card variant="outlined"><CardContent><Typography variant="body2" color="text.secondary">Enter fuel amount and cost per unit to see total cost.</Typography></CardContent></Card>
);
}
return (
<Card variant="outlined">
<CardContent>
<Box display="flex" justifyContent="space-between" alignItems="center" 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" justifyContent="space-between" alignItems="center">
<Typography variant="body2">{safeUnits.toFixed(3)} {unitLabel} × ${safeCostPerUnit.toFixed(3)}</Typography>
<Typography variant="h6" color="primary.main" fontWeight={700}>${safeCost.toFixed(2)}</Typography>
</Box>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { TextField, InputAdornment, FormHelperText, Box } from '@mui/material';
import { UnitSystem, DistanceType } from '../types/fuel-logs.types';
interface Props {
type: DistanceType;
value?: number;
onChange: (value: number) => void;
unitSystem?: UnitSystem;
error?: string;
disabled?: boolean;
}
export const DistanceInput: React.FC<Props> = ({ type, value, onChange, unitSystem = 'imperial', error, disabled }) => {
const units = unitSystem === 'imperial' ? 'miles' : 'kilometers';
const label = type === 'odometer' ? `Odometer (${units})` : `Trip Distance (${units})`;
return (
<Box>
<TextField
label={label}
type="number"
value={value ?? ''}
onChange={(e) => onChange(parseFloat(e.target.value) || 0)}
fullWidth
error={!!error}
disabled={disabled}
inputProps={{ step: type === 'trip' ? 0.1 : 1, min: 0 }}
InputProps={{ endAdornment: <InputAdornment position="end">{units}</InputAdornment> }}
/>
{error && <FormHelperText error>{error}</FormHelperText>}
</Box>
);
};

View File

@@ -0,0 +1,161 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { Grid, Card, CardHeader, CardContent, TextField, Switch, FormControlLabel, Box, Button, CircularProgress } from '@mui/material';
import { 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 } from '../types/fuel-logs.types';
const schema = z.object({
vehicleId: z.string().uuid(),
dateTime: z.string().min(1),
odometerReading: z.coerce.number().positive().optional(),
tripDistance: z.coerce.number().positive().optional(),
fuelType: z.nativeEnum(FuelType),
fuelGrade: z.union([z.string(), z.null()]).optional(),
fuelUnits: z.coerce.number().positive(),
costPerUnit: z.coerce.number().positive(),
locationData: z.any().optional(),
notes: z.string().max(500).optional(),
}).refine((d) => (d.odometerReading && d.odometerReading > 0) || (d.tripDistance && d.tripDistance > 0), {
message: 'Either odometer reading or trip distance is required'
}).refine((d) => !(d.odometerReading && d.tripDistance), {
message: 'Cannot specify both odometer reading and trip distance'
});
export const FuelLogForm: React.FC<{ onSuccess?: () => void; initial?: Partial<CreateFuelLogRequest> }> = ({ onSuccess, initial }) => {
const { userSettings } = useUserSettings();
const { createFuelLog, isLoading } = useFuelLogs();
const [useOdometer, setUseOdometer] = useState(false);
const { control, handleSubmit, watch, setValue, formState: { errors, isValid } } = useForm<CreateFuelLogRequest>({
resolver: zodResolver(schema),
mode: 'onChange',
defaultValues: {
dateTime: new Date().toISOString().slice(0, 16),
fuelType: FuelType.GASOLINE,
...initial
} as any
});
const watched = watch(['fuelUnits', 'costPerUnit']);
const [fuelUnitsRaw, costPerUnitRaw] = watched as [string | number | undefined, string | number | undefined];
// Convert to numbers for calculation
const fuelUnits = typeof fuelUnitsRaw === 'string' ? parseFloat(fuelUnitsRaw) : fuelUnitsRaw;
const costPerUnit = typeof costPerUnitRaw === 'string' ? parseFloat(costPerUnitRaw) : costPerUnitRaw;
const calculatedCost = useMemo(() => {
const units = fuelUnits && !isNaN(fuelUnits) ? fuelUnits : 0;
const cost = costPerUnit && !isNaN(costPerUnit) ? costPerUnit : 0;
return units > 0 && cost > 0 ? units * cost : 0;
}, [fuelUnits, costPerUnit]);
const onSubmit = async (data: CreateFuelLogRequest) => {
const payload: CreateFuelLogRequest = {
...data,
odometerReading: useOdometer ? data.odometerReading : undefined,
tripDistance: useOdometer ? undefined : data.tripDistance,
};
await createFuelLog(payload);
onSuccess?.();
};
useEffect(() => {
if (useOdometer) setValue('tripDistance', undefined as any);
else setValue('odometerReading', undefined as any);
}, [useOdometer, setValue]);
return (
<Card>
<CardHeader title="Add Fuel Log" subheader={<UnitSystemDisplay unitSystem={userSettings?.unitSystem} showLabel="Displaying in" />} />
<CardContent>
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Controller name="vehicleId" control={control} render={({ field }) => (
<VehicleSelector value={field.value} onChange={field.onChange} error={errors.vehicleId?.message} required />
)} />
</Grid>
<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>
<Grid item xs={12} sm={6}>
<FormControlLabel control={<Switch checked={useOdometer} onChange={(e) => setUseOdometer(e.target.checked)} />} label={`Use ${useOdometer ? 'Odometer' : 'Trip Distance'}`} />
</Grid>
<Grid item xs={12} sm={6}>
<Controller name={useOdometer ? 'odometerReading' : 'tripDistance'} control={control} render={({ field }) => (
<DistanceInput type={useOdometer ? 'odometer' : 'trip'} value={field.value as any} onChange={field.onChange as any} unitSystem={userSettings?.unitSystem} error={useOdometer ? (errors.odometerReading?.message as any) : (errors.tripDistance?.message as any)} />
)} />
</Grid>
<Grid item xs={12}>
<Controller name="fuelType" control={control} render={({ field: fuelTypeField }) => (
<Controller name="fuelGrade" control={control} render={({ field: fuelGradeField }) => (
<FuelTypeSelector fuelType={fuelTypeField.value} fuelGrade={fuelGradeField.value as any} onFuelTypeChange={fuelTypeField.onChange} onFuelGradeChange={fuelGradeField.onChange as any} error={(errors.fuelType?.message as any) || (errors.fuelGrade?.message as any)} />
)} />
)} />
</Grid>
<Grid item xs={12} sm={6}>
<Controller name="fuelUnits" control={control} render={({ field }) => (
<TextField
{...field}
value={field.value ?? ''}
onChange={(e) => field.onChange(e.target.value)}
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>
<Grid item xs={12} sm={6}>
<Controller name="costPerUnit" control={control} render={({ field }) => (
<TextField
{...field}
value={field.value ?? ''}
onChange={(e) => field.onChange(e.target.value)}
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}
/>
)} />
</Grid>
<Grid item xs={12}>
<CostCalculator fuelUnits={fuelUnits} costPerUnit={costPerUnit} calculatedCost={calculatedCost} unitSystem={userSettings?.unitSystem} />
</Grid>
<Grid item xs={12}>
<Controller name="locationData" control={control} render={({ field }) => (
<LocationInput value={field.value as any} onChange={field.onChange as any} placeholder="Station location (optional)" />
)} />
</Grid>
<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} />
)} />
</Grid>
<Grid item xs={12}>
<Box display="flex" gap={2} justifyContent="flex-end">
<Button type="submit" variant="contained" disabled={!isValid || isLoading} startIcon={isLoading ? <CircularProgress size={18} /> : undefined}>Add Fuel Log</Button>
</Box>
</Grid>
</Grid>
</form>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { Card, CardContent, Typography, List, ListItem, ListItemText, Chip, Box } from '@mui/material';
import { FuelLogResponse } from '../types/fuel-logs.types';
export const FuelLogsList: React.FC<{ logs?: FuelLogResponse[] }>= ({ logs }) => {
if (!logs || logs.length === 0) {
return (
<Card variant="outlined"><CardContent><Typography variant="body2" color="text.secondary">No fuel logs yet.</Typography></CardContent></Card>
);
}
return (
<List>
{logs.map((log) => (
<ListItem key={log.id} divider>
<ListItemText
primary={`${new Date(log.dateTime).toLocaleString()} $${(log.totalCost || 0).toFixed(2)}`}
secondary={`${(log.fuelUnits || 0).toFixed(3)} @ $${(log.costPerUnit || 0).toFixed(3)}${log.odometerReading ? `Odo: ${log.odometerReading}` : `Trip: ${log.tripDistance}`}`}
/>
{log.efficiency && typeof log.efficiency === 'number' && !isNaN(log.efficiency) && (
<Box><Chip label={`${log.efficiency.toFixed(1)} ${log.efficiencyLabel}`} size="small" color="primary" /></Box>
)}
</ListItem>
))}
</List>
);
};

View File

@@ -0,0 +1,38 @@
import React, { useMemo } from 'react';
import { Card, CardContent, Grid, Typography } from '@mui/material';
import { FuelLogResponse } from '../types/fuel-logs.types';
import { useUnits } from '../../../core/units/UnitsContext';
export const FuelStatsCard: React.FC<{ logs?: FuelLogResponse[] }> = ({ logs }) => {
const { unitSystem } = useUnits();
const stats = useMemo(() => {
if (!logs || logs.length === 0) return { count: 0, totalUnits: 0, totalCost: 0 };
const totalUnits = logs.reduce((s, l) => s + (l.fuelUnits || 0), 0);
const totalCost = logs.reduce((s, l) => s + (l.totalCost || 0), 0);
return { count: logs.length, totalUnits, totalCost };
}, [logs]);
const unitLabel = unitSystem === 'imperial' ? 'gallons' : 'liters';
return (
<Card variant="outlined">
<CardContent>
<Grid container spacing={2}>
<Grid item xs={4}>
<Typography variant="overline" color="text.secondary">Logs</Typography>
<Typography variant="h6">{stats.count}</Typography>
</Grid>
<Grid item xs={4}>
<Typography variant="overline" color="text.secondary">Total Fuel</Typography>
<Typography variant="h6">{(stats.totalUnits || 0).toFixed(2)} {unitLabel}</Typography>
</Grid>
<Grid item xs={4}>
<Typography variant="overline" color="text.secondary">Total Cost</Typography>
<Typography variant="h6">${(stats.totalCost || 0).toFixed(2)}</Typography>
</Grid>
</Grid>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,57 @@
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 Props {
fuelType: FuelType;
fuelGrade?: FuelGrade;
onFuelTypeChange: (fuelType: FuelType) => void;
onFuelGradeChange: (fuelGrade?: FuelGrade) => void;
error?: string;
disabled?: boolean;
}
export const FuelTypeSelector: React.FC<Props> = ({ fuelType, fuelGrade, onFuelTypeChange, onFuelGradeChange, error, disabled }) => {
const { fuelGrades, isLoading } = useFuelGrades(fuelType);
useEffect(() => {
if (fuelGrade && fuelGrades && !fuelGrades.some(g => g.value === fuelGrade)) {
onFuelGradeChange(undefined);
}
}, [fuelType, fuelGrades, fuelGrade, onFuelGradeChange]);
useEffect(() => {
if (!fuelGrade && fuelGrades && fuelGrades.length > 0) {
onFuelGradeChange(fuelGrades[0].value as FuelGrade);
}
}, [fuelGrades, fuelGrade, onFuelGradeChange]);
return (
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<FormControl fullWidth error={!!error}>
<InputLabel>Fuel Type</InputLabel>
<Select value={fuelType} label="Fuel Type" onChange={(e) => onFuelTypeChange(e.target.value as FuelType)} 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>
<Grid item xs={12} sm={6}>
<FormControl fullWidth disabled={disabled || isLoading || fuelType === FuelType.ELECTRIC}>
<InputLabel>Fuel Grade</InputLabel>
<Select value={fuelGrade || ''} label="Fuel Grade" onChange={(e) => onFuelGradeChange(e.target.value as FuelGrade)}>
{fuelGrades?.map((g) => (
<MenuItem key={g.value || 'none'} value={g.value || ''}>{g.label}</MenuItem>
))}
</Select>
{fuelType !== FuelType.ELECTRIC && <FormHelperText>{isLoading ? 'Loading grades…' : 'Select a grade'}</FormHelperText>}
</FormControl>
</Grid>
</Grid>
);
};

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { TextField } from '@mui/material';
import { LocationData } from '../types/fuel-logs.types';
interface Props {
value?: LocationData;
onChange: (value?: LocationData) => void;
placeholder?: string;
}
export const LocationInput: React.FC<Props> = ({ value, onChange, placeholder }) => {
return (
<TextField
label="Location (optional)"
placeholder={placeholder}
fullWidth
value={value?.stationName || value?.address || ''}
onChange={(e) => onChange({ ...(value || {}), stationName: e.target.value })}
/>
);
};

View File

@@ -0,0 +1,14 @@
import React from 'react';
import { Typography } from '@mui/material';
import { UnitSystem } from '../types/fuel-logs.types';
export const UnitSystemDisplay: React.FC<{ unitSystem?: UnitSystem; showLabel?: string }> = ({ unitSystem, showLabel }) => {
if (!unitSystem) return null;
const label = unitSystem === 'imperial' ? 'Imperial (miles, gallons, MPG)' : 'Metric (km, liters, L/100km)';
return (
<Typography variant="caption" color="text.secondary">
{showLabel ? `${showLabel} ` : ''}{label}
</Typography>
);
};

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { FormControl, InputLabel, Select, MenuItem, FormHelperText, Box, Typography } from '@mui/material';
import DirectionsCarIcon from '@mui/icons-material/DirectionsCar';
import { useVehicles } from '../../vehicles/hooks/useVehicles';
import type { Vehicle } from '../../vehicles/types/vehicles.types';
interface Props {
value?: string;
onChange: (vehicleId: string) => void;
error?: string;
required?: boolean;
disabled?: boolean;
}
export const VehicleSelector: React.FC<Props> = ({ value, onChange, error, required, disabled }) => {
const { data: vehicles, isLoading } = useVehicles();
if (!isLoading && (vehicles?.length || 0) === 0) {
return (
<Box p={2} borderRadius={1} bgcolor={'background.default'}>
<Typography variant="body2" color="text.secondary">
You need to add a vehicle before creating fuel logs.
</Typography>
</Box>
);
}
return (
<FormControl fullWidth error={!!error} required={required}>
<InputLabel>Select Vehicle</InputLabel>
<Select value={value || ''} onChange={(e) => onChange(e.target.value as string)} label="Select Vehicle" disabled={disabled}>
{vehicles?.map((v: Vehicle) => (
<MenuItem key={v.id} value={v.id}>
<Box display="flex" alignItems="center" gap={1}>
<DirectionsCarIcon fontSize="small" />
<Typography variant="body2">{`${v.year || ''} ${v.make || ''} ${v.model || ''}`.trim()}</Typography>
</Box>
</MenuItem>
))}
</Select>
{error && <FormHelperText>{error}</FormHelperText>}
</FormControl>
);
};

View File

@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query';
import { fuelLogsApi } from '../api/fuel-logs.api';
import { FuelType, FuelGradeOption } from '../types/fuel-logs.types';
export const useFuelGrades = (fuelType: FuelType) => {
const { data, isLoading, error } = useQuery<FuelGradeOption[]>({
queryKey: ['fuelGrades', fuelType],
queryFn: () => fuelLogsApi.getFuelGrades(fuelType),
});
return { fuelGrades: data || [], isLoading, error };
};

View File

@@ -0,0 +1,36 @@
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();
const logsQuery = useQuery<FuelLogResponse[]>({
queryKey: ['fuelLogs', vehicleId || 'all'],
queryFn: () => (vehicleId ? fuelLogsApi.getFuelLogsByVehicle(vehicleId) : fuelLogsApi.getUserFuelLogs()),
});
const statsQuery = useQuery<EnhancedFuelStats>({
queryKey: ['fuelLogsStats', vehicleId],
queryFn: () => fuelLogsApi.getVehicleStats(vehicleId!),
enabled: !!vehicleId,
});
const createMutation = useMutation({
mutationFn: (data: CreateFuelLogRequest) => fuelLogsApi.create(data),
onSuccess: (_res, variables) => {
queryClient.invalidateQueries({ queryKey: ['fuelLogs', variables.vehicleId] });
queryClient.invalidateQueries({ queryKey: ['fuelLogsStats', variables.vehicleId] });
},
});
return {
fuelLogs: logsQuery.data,
isLoading: logsQuery.isLoading || createMutation.isPending,
error: logsQuery.error,
stats: statsQuery.data,
isStatsLoading: statsQuery.isLoading,
createFuelLog: createMutation.mutateAsync,
};
};

View File

@@ -0,0 +1,15 @@
import { useUnits } from '../../../core/units/UnitsContext';
import { UnitSystem } from '../types/fuel-logs.types';
export const useUserSettings = () => {
const { unitSystem } = useUnits();
// Placeholder for future: fetch currency/timezone from a settings API
return {
userSettings: {
unitSystem: unitSystem as UnitSystem,
currencyCode: 'USD',
timeZone: 'UTC',
},
};
};

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { Grid, Typography } from '@mui/material';
import { FuelLogForm } from '../components/FuelLogForm';
import { FuelLogsList } from '../components/FuelLogsList';
import { useFuelLogs } from '../hooks/useFuelLogs';
import { FuelStatsCard } from '../components/FuelStatsCard';
export const FuelLogsPage: React.FC = () => {
const { fuelLogs } = useFuelLogs();
return (
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<FuelLogForm />
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="h6" gutterBottom>Recent Fuel Logs</Typography>
<FuelLogsList logs={fuelLogs} />
<Typography variant="h6" sx={{ mt: 3 }} gutterBottom>Summary</Typography>
<FuelStatsCard logs={fuelLogs} />
</Grid>
</Grid>
);
};

View File

@@ -0,0 +1,72 @@
/**
* @ai-summary Types for enhanced fuel logs UI
*/
export type UnitSystem = 'imperial' | 'metric';
export enum FuelType {
GASOLINE = 'gasoline',
DIESEL = 'diesel',
ELECTRIC = 'electric'
}
export type FuelGrade = '87' | '88' | '89' | '91' | '93' | '#1' | '#2' | null;
export interface LocationData {
address?: string;
coordinates?: { latitude: number; longitude: number };
googlePlaceId?: string;
stationName?: string;
}
export type DistanceType = 'odometer' | 'trip';
export interface CreateFuelLogRequest {
vehicleId: string;
dateTime: string;
odometerReading?: number;
tripDistance?: number;
fuelType: FuelType;
fuelGrade?: FuelGrade;
fuelUnits: number;
costPerUnit: number;
locationData?: LocationData;
notes?: string;
}
export interface FuelLogResponse {
id: string;
userId: string;
vehicleId: string;
dateTime: string;
odometerReading?: number;
tripDistance?: number;
fuelType: FuelType;
fuelGrade?: FuelGrade;
fuelUnits: number;
costPerUnit: number;
totalCost: number;
locationData?: LocationData;
efficiency?: number;
efficiencyLabel: string;
notes?: string;
createdAt: string;
updatedAt: string;
}
export interface EnhancedFuelStats {
logCount: number;
totalFuelUnits: number;
totalCost: number;
averageCostPerUnit: number;
totalDistance: number;
averageEfficiency: number;
unitLabels: { fuelUnits: string; distanceUnits: string; efficiencyUnits: string };
}
export interface FuelGradeOption {
value: FuelGrade;
label: string;
description?: string;
}