Files
motovaultpro/frontend/src/features/fuel-logs/components/FuelLogForm.tsx
Eric gullickson d4befe31d1 Debugging
2025-09-22 20:49:44 -05:00

182 lines
8.6 KiB
TypeScript

import React, { useEffect, useMemo, useState, useRef, memo } 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'
});
const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial<CreateFuelLogRequest> }> = ({ onSuccess, initial }) => {
const { userSettings } = useUserSettings();
const { createFuelLog, isLoading } = useFuelLogs();
const [useOdometer, setUseOdometer] = useState(false);
const formInitialized = useRef(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
});
// DEBUG: Log component renders and form state
console.log('[FuelLogForm] Render - formInitialized:', formInitialized.current, 'isLoading:', isLoading);
// Prevent form reset after initial load
useEffect(() => {
if (!formInitialized.current) {
formInitialized.current = true;
console.log('[FuelLogForm] Form initialized');
}
}, []);
// DEBUG: Watch for form value changes
const vehicleId = watch('vehicleId');
useEffect(() => {
console.log('[FuelLogForm] Vehicle ID changed:', vehicleId);
}, [vehicleId]);
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>
);
};
export const FuelLogForm = memo(FuelLogFormComponent);