Compare commits
9 Commits
f6e3963b99
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e729d425fd | |||
|
|
2221819b4a | ||
| 77aeba0102 | |||
|
|
0d90829d31 | ||
| 0dd5746f60 | |||
|
|
fdc34aee2f | ||
| 9f5c81a14e | |||
|
|
55b8b67a6e | ||
|
|
a49f419eab |
@@ -247,6 +247,20 @@ export class FuelLogsRepository {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Coerces DECIMAL columns from node-postgres strings to numbers while
|
||||||
|
// preserving snake_case row keys used by the enhanced API service callers.
|
||||||
|
private mapEnhancedRow(row: any): any {
|
||||||
|
if (!row) return null;
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
fuel_units: row.fuel_units != null ? parseFloat(row.fuel_units) : row.fuel_units,
|
||||||
|
cost_per_unit: row.cost_per_unit != null ? parseFloat(row.cost_per_unit) : row.cost_per_unit,
|
||||||
|
total_cost: row.total_cost != null ? parseFloat(row.total_cost) : row.total_cost,
|
||||||
|
gallons: row.gallons != null ? parseFloat(row.gallons) : row.gallons,
|
||||||
|
price_per_gallon: row.price_per_gallon != null ? parseFloat(row.price_per_gallon) : row.price_per_gallon,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Enhanced API support (new schema)
|
// Enhanced API support (new schema)
|
||||||
async createEnhanced(data: {
|
async createEnhanced(data: {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -293,7 +307,7 @@ export class FuelLogsRepository {
|
|||||||
data.notes ?? null
|
data.notes ?? null
|
||||||
];
|
];
|
||||||
const res = await this.pool.query(query, values);
|
const res = await this.pool.query(query, values);
|
||||||
return res.rows[0] ?? null;
|
return this.mapEnhancedRow(res.rows[0] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByVehicleIdEnhanced(vehicleId: string): Promise<any[]> {
|
async findByVehicleIdEnhanced(vehicleId: string): Promise<any[]> {
|
||||||
@@ -301,7 +315,7 @@ export class FuelLogsRepository {
|
|||||||
`SELECT * FROM fuel_logs WHERE vehicle_id = $1 ORDER BY date_time DESC NULLS LAST, date DESC NULLS LAST, created_at DESC`,
|
`SELECT * FROM fuel_logs WHERE vehicle_id = $1 ORDER BY date_time DESC NULLS LAST, date DESC NULLS LAST, created_at DESC`,
|
||||||
[vehicleId]
|
[vehicleId]
|
||||||
);
|
);
|
||||||
return res.rows;
|
return res.rows.map(r => this.mapEnhancedRow(r));
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByUserIdEnhanced(userId: string): Promise<any[]> {
|
async findByUserIdEnhanced(userId: string): Promise<any[]> {
|
||||||
@@ -309,12 +323,12 @@ export class FuelLogsRepository {
|
|||||||
`SELECT * FROM fuel_logs WHERE user_id = $1 ORDER BY date_time DESC NULLS LAST, date DESC NULLS LAST, created_at DESC`,
|
`SELECT * FROM fuel_logs WHERE user_id = $1 ORDER BY date_time DESC NULLS LAST, date DESC NULLS LAST, created_at DESC`,
|
||||||
[userId]
|
[userId]
|
||||||
);
|
);
|
||||||
return res.rows;
|
return res.rows.map(r => this.mapEnhancedRow(r));
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByIdEnhanced(id: string): Promise<any | null> {
|
async findByIdEnhanced(id: string): Promise<any | null> {
|
||||||
const res = await this.pool.query(`SELECT * FROM fuel_logs WHERE id = $1`, [id]);
|
const res = await this.pool.query(`SELECT * FROM fuel_logs WHERE id = $1`, [id]);
|
||||||
return res.rows[0] ?? null;
|
return this.mapEnhancedRow(res.rows[0] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPreviousLogByOdometer(vehicleId: string, odometerReading: number): Promise<any | null> {
|
async getPreviousLogByOdometer(vehicleId: string, odometerReading: number): Promise<any | null> {
|
||||||
@@ -322,7 +336,7 @@ export class FuelLogsRepository {
|
|||||||
`SELECT * FROM fuel_logs WHERE vehicle_id = $1 AND odometer IS NOT NULL AND odometer < $2 ORDER BY odometer DESC LIMIT 1`,
|
`SELECT * FROM fuel_logs WHERE vehicle_id = $1 AND odometer IS NOT NULL AND odometer < $2 ORDER BY odometer DESC LIMIT 1`,
|
||||||
[vehicleId, odometerReading]
|
[vehicleId, odometerReading]
|
||||||
);
|
);
|
||||||
return res.rows[0] ?? null;
|
return this.mapEnhancedRow(res.rows[0] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLatestLogForVehicle(vehicleId: string): Promise<any | null> {
|
async getLatestLogForVehicle(vehicleId: string): Promise<any | null> {
|
||||||
@@ -330,7 +344,7 @@ export class FuelLogsRepository {
|
|||||||
`SELECT * FROM fuel_logs WHERE vehicle_id = $1 ORDER BY date_time DESC NULLS LAST, date DESC NULLS LAST, created_at DESC LIMIT 1`,
|
`SELECT * FROM fuel_logs WHERE vehicle_id = $1 ORDER BY date_time DESC NULLS LAST, date DESC NULLS LAST, created_at DESC LIMIT 1`,
|
||||||
[vehicleId]
|
[vehicleId]
|
||||||
);
|
);
|
||||||
return res.rows[0] ?? null;
|
return this.mapEnhancedRow(res.rows[0] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEnhanced(id: string, data: {
|
async updateEnhanced(id: string, data: {
|
||||||
@@ -416,6 +430,6 @@ export class FuelLogsRepository {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.rows[0];
|
return this.mapEnhancedRow(result.rows[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,8 +234,8 @@ export class FuelLogsService {
|
|||||||
return { logCount: 0, totalFuelUnits: 0, totalCost: 0, averageCostPerUnit: 0, totalDistance: 0, averageEfficiency: 0, unitLabels: labels };
|
return { logCount: 0, totalFuelUnits: 0, totalCost: 0, averageCostPerUnit: 0, totalDistance: 0, averageEfficiency: 0, unitLabels: labels };
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalFuelUnits = rows.reduce((s, r) => s + (Number(r.fuel_units) || 0), 0);
|
const totalFuelUnits = rows.reduce((s, r) => s + (r.fuel_units || 0), 0);
|
||||||
const totalCost = rows.reduce((s, r) => s + (Number(r.total_cost) || 0), 0);
|
const totalCost = rows.reduce((s, r) => s + (r.total_cost || 0), 0);
|
||||||
const averageCostPerUnit = totalFuelUnits > 0 ? totalCost / totalFuelUnits : 0;
|
const averageCostPerUnit = totalFuelUnits > 0 ? totalCost / totalFuelUnits : 0;
|
||||||
|
|
||||||
const sorted = [...rows].sort((a, b) => (new Date(b.date_time || b.date)).getTime() - (new Date(a.date_time || a.date)).getTime());
|
const sorted = [...rows].sort((a, b) => (new Date(b.date_time || b.date)).getTime() - (new Date(a.date_time || a.date)).getTime());
|
||||||
@@ -282,9 +282,9 @@ export class FuelLogsService {
|
|||||||
tripDistance: row.trip_distance ?? undefined,
|
tripDistance: row.trip_distance ?? undefined,
|
||||||
fuelType: row.fuel_type as FuelType,
|
fuelType: row.fuel_type as FuelType,
|
||||||
fuelGrade: row.fuel_grade ?? undefined,
|
fuelGrade: row.fuel_grade ?? undefined,
|
||||||
fuelUnits: Number(row.fuel_units),
|
fuelUnits: row.fuel_units ?? 0,
|
||||||
costPerUnit: Number(row.cost_per_unit),
|
costPerUnit: row.cost_per_unit ?? 0,
|
||||||
totalCost: Number(row.total_cost),
|
totalCost: row.total_cost ?? 0,
|
||||||
locationData: row.location_data ?? undefined,
|
locationData: row.location_data ?? undefined,
|
||||||
notes: row.notes ?? undefined,
|
notes: row.notes ?? undefined,
|
||||||
efficiency: efficiency,
|
efficiency: efficiency,
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export class MaintenanceRepository {
|
|||||||
subtypes: row.subtypes,
|
subtypes: row.subtypes,
|
||||||
date: row.date,
|
date: row.date,
|
||||||
odometerReading: row.odometer_reading,
|
odometerReading: row.odometer_reading,
|
||||||
cost: row.cost,
|
// node-postgres returns numeric/decimal columns as strings; coerce to honor the number type.
|
||||||
|
cost: row.cost != null ? Number(row.cost) : undefined,
|
||||||
shopName: row.shop_name,
|
shopName: row.shop_name,
|
||||||
notes: row.notes,
|
notes: row.notes,
|
||||||
receiptDocumentId: row.receipt_document_id,
|
receiptDocumentId: row.receipt_document_id,
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export class OwnershipCostsRepository {
|
|||||||
vehicleId: row.vehicle_id,
|
vehicleId: row.vehicle_id,
|
||||||
documentId: row.document_id,
|
documentId: row.document_id,
|
||||||
costType: row.cost_type,
|
costType: row.cost_type,
|
||||||
amount: row.amount,
|
// node-postgres returns numeric/decimal columns as strings; coerce to honor the number type.
|
||||||
|
amount: Number(row.amount),
|
||||||
description: row.description,
|
description: row.description,
|
||||||
periodStart: row.period_start,
|
periodStart: row.period_start,
|
||||||
periodEnd: row.period_end,
|
periodEnd: row.period_end,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const DistanceInput: React.FC<Props> = ({ type, value, onChange, unitSyst
|
|||||||
fullWidth
|
fullWidth
|
||||||
error={!!error}
|
error={!!error}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
inputProps={{ step: type === 'trip' ? 0.1 : 1, min: 0 }}
|
inputProps={{ step: type === 'trip' ? 0.1 : 1, min: 0, inputMode: 'decimal' }}
|
||||||
InputProps={{ endAdornment: <InputAdornment position="end">{units}</InputAdornment> }}
|
InputProps={{ endAdornment: <InputAdornment position="end">{units}</InputAdornment> }}
|
||||||
/>
|
/>
|
||||||
{error && <FormHelperText error>{error}</FormHelperText>}
|
{error && <FormHelperText error>{error}</FormHelperText>}
|
||||||
|
|||||||
@@ -247,60 +247,7 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
|
|||||||
)} />
|
)} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Row 2: Date/Time | MPG/L/100km */}
|
{/* Row 2: Mileage (Trip Distance / Odometer) | Input Method Toggle */}
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Controller name="dateTime" control={control} render={({ field }) => (
|
|
||||||
<DateTimePicker
|
|
||||||
label="Date & Time"
|
|
||||||
value={field.value ? dayjs(field.value) : null}
|
|
||||||
onChange={(newValue) => field.onChange(newValue?.toISOString() || '')}
|
|
||||||
format="MM/DD/YYYY hh:mm a"
|
|
||||||
slotProps={{
|
|
||||||
textField: {
|
|
||||||
fullWidth: true,
|
|
||||||
error: !!errors.dateTime,
|
|
||||||
helperText: errors.dateTime?.message,
|
|
||||||
sx: {
|
|
||||||
'& .MuiOutlinedInput-root': {
|
|
||||||
minHeight: '56px',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)} />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<TextField
|
|
||||||
label={`${userSettings?.unitSystem === 'metric' ? 'L/100km' : 'MPG'}`}
|
|
||||||
value={calculatedEfficiency > 0 ? calculatedEfficiency.toFixed(3) : ''}
|
|
||||||
fullWidth
|
|
||||||
InputProps={{
|
|
||||||
readOnly: true,
|
|
||||||
sx: (theme) => ({
|
|
||||||
backgroundColor: 'grey.50',
|
|
||||||
...theme.applyStyles('dark', {
|
|
||||||
backgroundColor: '#4C4E4D',
|
|
||||||
}),
|
|
||||||
'& .MuiOutlinedInput-input': {
|
|
||||||
cursor: 'default',
|
|
||||||
color: 'inherit',
|
|
||||||
...theme.applyStyles('dark', {
|
|
||||||
color: '#F2F3F6',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
helperText="Calculated from distance ÷ fuel amount"
|
|
||||||
sx={{
|
|
||||||
'& .MuiOutlinedInput-root': {
|
|
||||||
minHeight: '56px',
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Row 3: Odometer | Distance Input Method */}
|
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Controller name={useOdometer ? 'odometerReading' : 'tripDistance'} control={control} render={({ field }) => (
|
<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)} />
|
<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)} />
|
||||||
@@ -342,11 +289,21 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
|
|||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
|
||||||
<Controller name="fuelType" control={control} render={({ field: fuelTypeField }) => (
|
{/* Row 3: Cost Per Gallon | Fuel Amount */}
|
||||||
<Controller name="fuelGrade" control={control} render={({ field: fuelGradeField }) => (
|
<Grid item xs={12} sm={6}>
|
||||||
<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)} />
|
<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, inputMode: 'decimal' }}
|
||||||
|
fullWidth
|
||||||
|
error={!!errors.costPerUnit}
|
||||||
|
helperText={errors.costPerUnit?.message}
|
||||||
|
/>
|
||||||
)} />
|
)} />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
@@ -357,31 +314,27 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
|
|||||||
onChange={(e) => field.onChange(e.target.value)}
|
onChange={(e) => field.onChange(e.target.value)}
|
||||||
label={`Fuel Amount (${userSettings?.unitSystem === 'imperial' ? 'gallons' : 'liters'})`}
|
label={`Fuel Amount (${userSettings?.unitSystem === 'imperial' ? 'gallons' : 'liters'})`}
|
||||||
type="number"
|
type="number"
|
||||||
inputProps={{ step: 0.001, min: 0.001 }}
|
inputProps={{ step: 0.001, min: 0.001, inputMode: 'decimal' }}
|
||||||
fullWidth
|
fullWidth
|
||||||
error={!!errors.fuelUnits}
|
error={!!errors.fuelUnits}
|
||||||
helperText={errors.fuelUnits?.message}
|
helperText={errors.fuelUnits?.message}
|
||||||
/>
|
/>
|
||||||
)} />
|
)} />
|
||||||
</Grid>
|
</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}>
|
<Grid item xs={12}>
|
||||||
<CostCalculator fuelUnits={fuelUnits} costPerUnit={costPerUnit} calculatedCost={calculatedCost} />
|
<CostCalculator fuelUnits={fuelUnits} costPerUnit={costPerUnit} calculatedCost={calculatedCost} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* Row 4: Fuel Type / Fuel Grade (grade options filtered by type) */}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Row 5: Location */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Controller name="locationData" control={control} render={({ field }) => (
|
<Controller name="locationData" control={control} render={({ field }) => (
|
||||||
<StationPicker
|
<StationPicker
|
||||||
@@ -392,11 +345,68 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
|
|||||||
/>
|
/>
|
||||||
)} />
|
)} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* Row 6: MPG/L/100km (calculated, read-only) */}
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
label={`${userSettings?.unitSystem === 'metric' ? 'L/100km' : 'MPG'}`}
|
||||||
|
value={calculatedEfficiency > 0 ? calculatedEfficiency.toFixed(3) : ''}
|
||||||
|
fullWidth
|
||||||
|
InputProps={{
|
||||||
|
readOnly: true,
|
||||||
|
sx: (theme) => ({
|
||||||
|
backgroundColor: 'grey.50',
|
||||||
|
...theme.applyStyles('dark', {
|
||||||
|
backgroundColor: '#4C4E4D',
|
||||||
|
}),
|
||||||
|
'& .MuiOutlinedInput-input': {
|
||||||
|
cursor: 'default',
|
||||||
|
color: 'inherit',
|
||||||
|
...theme.applyStyles('dark', {
|
||||||
|
color: '#F2F3F6',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
helperText="Calculated from distance ÷ fuel amount"
|
||||||
|
sx={{
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
minHeight: '56px',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Row 7: Notes */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Controller name="notes" control={control} render={({ field }) => (
|
<Controller name="notes" control={control} render={({ field }) => (
|
||||||
<TextField {...field} label="Notes (optional)" multiline rows={3} fullWidth error={!!errors.notes} helperText={errors.notes?.message} />
|
<TextField {...field} label="Notes (optional)" multiline rows={3} fullWidth error={!!errors.notes} helperText={errors.notes?.message} />
|
||||||
)} />
|
)} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* Row 8: Date & Time (defaults to now, least-edited field) */}
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Controller name="dateTime" control={control} render={({ field }) => (
|
||||||
|
<DateTimePicker
|
||||||
|
label="Date & Time"
|
||||||
|
value={field.value ? dayjs(field.value) : null}
|
||||||
|
onChange={(newValue) => field.onChange(newValue?.toISOString() || '')}
|
||||||
|
format="MM/DD/YYYY hh:mm a"
|
||||||
|
slotProps={{
|
||||||
|
textField: {
|
||||||
|
fullWidth: true,
|
||||||
|
error: !!errors.dateTime,
|
||||||
|
helperText: errors.dateTime?.message,
|
||||||
|
sx: {
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
minHeight: '56px',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)} />
|
||||||
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Box display="flex" gap={2} justifyContent="flex-end">
|
<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>
|
<Button type="submit" variant="contained" disabled={!isValid || isLoading} startIcon={isLoading ? <CircularProgress size={18} /> : undefined}>Add Fuel Log</Button>
|
||||||
|
|||||||
@@ -115,16 +115,16 @@ export const MaintenanceRecordsList: React.FC<MaintenanceRecordsListProps> = ({
|
|||||||
{categoryDisplay} ({subtypeCount})
|
{categoryDisplay} ({subtypeCount})
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack direction="row" spacing={1} flexWrap="wrap" sx={{ mt: 1 }}>
|
<Stack direction="row" spacing={1} flexWrap="wrap" sx={{ mt: 1 }}>
|
||||||
{record.odometerReading && (
|
{record.odometerReading != null && (
|
||||||
<Chip
|
<Chip
|
||||||
label={`${Number(record.odometerReading).toLocaleString()} miles`}
|
label={`${record.odometerReading.toLocaleString()} miles`}
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{record.cost && (
|
{record.cost != null && (
|
||||||
<Chip
|
<Chip
|
||||||
label={`$${Number(record.cost).toFixed(2)}`}
|
label={`$${record.cost.toFixed(2)}`}
|
||||||
size="small"
|
size="small"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import { FuelLogResponse, UpdateFuelLogRequest } from '../../fuel-logs/types/fue
|
|||||||
import { FuelLogEditDialog } from '../../fuel-logs/components/FuelLogEditDialog';
|
import { FuelLogEditDialog } from '../../fuel-logs/components/FuelLogEditDialog';
|
||||||
import { fuelLogsApi } from '../../fuel-logs/api/fuel-logs.api';
|
import { fuelLogsApi } from '../../fuel-logs/api/fuel-logs.api';
|
||||||
import { MaintenanceRecordForm } from '../../maintenance/components/MaintenanceRecordForm';
|
import { MaintenanceRecordForm } from '../../maintenance/components/MaintenanceRecordForm';
|
||||||
|
import { useMaintenanceRecords } from '../../maintenance/hooks/useMaintenanceRecords';
|
||||||
|
import { getCategoryDisplayName } from '../../maintenance/types/maintenance.types';
|
||||||
|
import type { MaintenanceRecordResponse } from '../../maintenance/types/maintenance.types';
|
||||||
import { VehicleImage } from '../components/VehicleImage';
|
import { VehicleImage } from '../components/VehicleImage';
|
||||||
import { OwnershipCostsList } from '../../ownership-costs';
|
import { OwnershipCostsList } from '../../ownership-costs';
|
||||||
import { getVehicleLabel } from '@/core/utils/vehicleDisplay';
|
import { getVehicleLabel } from '@/core/utils/vehicleDisplay';
|
||||||
@@ -46,6 +49,7 @@ export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
|
|||||||
|
|
||||||
const [recordFilter, setRecordFilter] = useState<'All' | 'Fuel Logs' | 'Maintenance' | 'Documents'>('All');
|
const [recordFilter, setRecordFilter] = useState<'All' | 'Fuel Logs' | 'Maintenance' | 'Documents'>('All');
|
||||||
const { fuelLogs, isLoading: isFuelLoading } = useFuelLogs(vehicle.id);
|
const { fuelLogs, isLoading: isFuelLoading } = useFuelLogs(vehicle.id);
|
||||||
|
const { records: maintenanceRecords, isRecordsLoading: isMaintenanceLoading } = useMaintenanceRecords(vehicle.id);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [editingLog, setEditingLog] = useState<FuelLogResponse | null>(null);
|
const [editingLog, setEditingLog] = useState<FuelLogResponse | null>(null);
|
||||||
const [showMaintenanceDialog, setShowMaintenanceDialog] = useState(false);
|
const [showMaintenanceDialog, setShowMaintenanceDialog] = useState(false);
|
||||||
@@ -124,8 +128,34 @@ export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (maintenanceRecords && Array.isArray(maintenanceRecords)) {
|
||||||
|
for (const rec of maintenanceRecords as MaintenanceRecordResponse[]) {
|
||||||
|
const catLabel = getCategoryDisplayName(rec.category);
|
||||||
|
const subtypes = Array.isArray(rec.subtypes) ? rec.subtypes : [];
|
||||||
|
const subtypeText = subtypes.length
|
||||||
|
? `${subtypes.slice(0, 3).join(', ')}${subtypes.length > 3 ? ` +${subtypes.length - 3}` : ''}`
|
||||||
|
: '';
|
||||||
|
const summary = subtypeText ? `${catLabel} — ${subtypeText}` : catLabel;
|
||||||
|
const secondaryParts: string[] = [];
|
||||||
|
if (rec.shopName) secondaryParts.push(rec.shopName);
|
||||||
|
secondaryParts.push(new Date(rec.date).toLocaleDateString());
|
||||||
|
secondaryParts.push('Maintenance');
|
||||||
|
const secondary = secondaryParts.join(' • ');
|
||||||
|
const amount = typeof rec.cost === 'number' ? `$${rec.cost.toFixed(2)}` : undefined;
|
||||||
|
list.push({
|
||||||
|
id: rec.id,
|
||||||
|
type: 'Maintenance',
|
||||||
|
date: rec.date,
|
||||||
|
summary,
|
||||||
|
amount,
|
||||||
|
secondary
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return list.sort((a, b) => b.date.localeCompare(a.date));
|
return list.sort((a, b) => b.date.localeCompare(a.date));
|
||||||
}, [fuelLogs]);
|
}, [fuelLogs, maintenanceRecords]);
|
||||||
|
|
||||||
const filteredRecords = useMemo(() => {
|
const filteredRecords = useMemo(() => {
|
||||||
if (recordFilter === 'All') return records;
|
if (recordFilter === 'All') return records;
|
||||||
@@ -243,7 +273,7 @@ export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
|
|||||||
<Section title="Vehicle Records">
|
<Section title="Vehicle Records">
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{isFuelLoading ? 'Loading…' : `${filteredRecords.length} record${filteredRecords.length === 1 ? '' : 's'}`}
|
{(isFuelLoading || isMaintenanceLoading) ? 'Loading…' : `${filteredRecords.length} record${filteredRecords.length === 1 ? '' : 's'}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
<FormControl size="small" sx={{ minWidth: 180 }}>
|
<FormControl size="small" sx={{ minWidth: 180 }}>
|
||||||
<InputLabel id="vehicle-records-filter">Filter</InputLabel>
|
<InputLabel id="vehicle-records-filter">Filter</InputLabel>
|
||||||
@@ -262,7 +292,7 @@ export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent sx={{ p: 0 }}>
|
<CardContent sx={{ p: 0 }}>
|
||||||
{isFuelLoading ? (
|
{(isFuelLoading || isMaintenanceLoading) ? (
|
||||||
<Box sx={{ p: 2 }}>
|
<Box sx={{ p: 2 }}>
|
||||||
<Typography color="text.secondary" variant="body2">Loading records…</Typography>
|
<Typography color="text.secondary" variant="body2">Loading records…</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import { FuelLogResponse, UpdateFuelLogRequest } from '../../fuel-logs/types/fue
|
|||||||
import { FuelLogEditDialog } from '../../fuel-logs/components/FuelLogEditDialog';
|
import { FuelLogEditDialog } from '../../fuel-logs/components/FuelLogEditDialog';
|
||||||
import { FuelLogForm } from '../../fuel-logs/components/FuelLogForm';
|
import { FuelLogForm } from '../../fuel-logs/components/FuelLogForm';
|
||||||
import { MaintenanceRecordForm } from '../../maintenance/components/MaintenanceRecordForm';
|
import { MaintenanceRecordForm } from '../../maintenance/components/MaintenanceRecordForm';
|
||||||
|
import { useMaintenanceRecords } from '../../maintenance/hooks/useMaintenanceRecords';
|
||||||
|
import { getCategoryDisplayName } from '../../maintenance/types/maintenance.types';
|
||||||
|
import type { MaintenanceRecordResponse } from '../../maintenance/types/maintenance.types';
|
||||||
// Unit conversions now handled by backend
|
// Unit conversions now handled by backend
|
||||||
import { fuelLogsApi } from '../../fuel-logs/api/fuel-logs.api';
|
import { fuelLogsApi } from '../../fuel-logs/api/fuel-logs.api';
|
||||||
import { OwnershipCostsList } from '../../ownership-costs';
|
import { OwnershipCostsList } from '../../ownership-costs';
|
||||||
@@ -60,6 +63,7 @@ export const VehicleDetailPage: React.FC = () => {
|
|||||||
|
|
||||||
const { fuelLogs, isLoading: isFuelLoading } = useFuelLogs(id);
|
const { fuelLogs, isLoading: isFuelLoading } = useFuelLogs(id);
|
||||||
const { data: documents, isLoading: isDocumentsLoading } = useDocumentsByVehicle(id);
|
const { data: documents, isLoading: isDocumentsLoading } = useDocumentsByVehicle(id);
|
||||||
|
const { records: maintenanceRecords, isRecordsLoading: isMaintenanceLoading } = useMaintenanceRecords(id);
|
||||||
const { mutateAsync: deleteDocument } = useDeleteDocument();
|
const { mutateAsync: deleteDocument } = useDeleteDocument();
|
||||||
const { mutateAsync: removeVehicleFromDocument } = useRemoveVehicleFromDocument();
|
const { mutateAsync: removeVehicleFromDocument } = useRemoveVehicleFromDocument();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -133,8 +137,24 @@ export const VehicleDetailPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (maintenanceRecords && Array.isArray(maintenanceRecords)) {
|
||||||
|
for (const rec of maintenanceRecords as MaintenanceRecordResponse[]) {
|
||||||
|
const catLabel = getCategoryDisplayName(rec.category);
|
||||||
|
const subtypes = Array.isArray(rec.subtypes) ? rec.subtypes : [];
|
||||||
|
const subtypeText = subtypes.length
|
||||||
|
? `${subtypes.slice(0, 3).join(', ')}${subtypes.length > 3 ? ` +${subtypes.length - 3}` : ''}`
|
||||||
|
: '';
|
||||||
|
const parts: string[] = [catLabel];
|
||||||
|
if (subtypeText) parts.push(subtypeText);
|
||||||
|
if (rec.shopName) parts.push(rec.shopName);
|
||||||
|
const summary = parts.join(' • ');
|
||||||
|
const amount = typeof rec.cost === 'number' ? `$${rec.cost.toFixed(2)}` : undefined;
|
||||||
|
list.push({ id: rec.id, type: 'Maintenance', date: rec.date, summary, amount });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return list.sort((a, b) => b.date.localeCompare(a.date));
|
return list.sort((a, b) => b.date.localeCompare(a.date));
|
||||||
}, [fuelLogs, documents]);
|
}, [fuelLogs, documents, maintenanceRecords]);
|
||||||
|
|
||||||
const filteredRecords = useMemo(() => {
|
const filteredRecords = useMemo(() => {
|
||||||
if (recordFilter === 'All') return records;
|
if (recordFilter === 'All') return records;
|
||||||
@@ -475,22 +495,22 @@ export const VehicleDetailPage: React.FC = () => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(isFuelLoading || isDocumentsLoading) && (
|
{(isFuelLoading || isDocumentsLoading || isMaintenanceLoading) && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5}>
|
<TableCell colSpan={5}>
|
||||||
<Typography color="text.secondary">Loading records…</Typography>
|
<Typography color="text.secondary">Loading records…</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
{!isFuelLoading && !isDocumentsLoading && filteredRecords.length === 0 && (
|
{!isFuelLoading && !isDocumentsLoading && !isMaintenanceLoading && filteredRecords.length === 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5}>
|
<TableCell colSpan={5}>
|
||||||
<Typography color="text.secondary">No records found for this filter.</Typography>
|
<Typography color="text.secondary">No records found for this filter.</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
{!isFuelLoading && !isDocumentsLoading && filteredRecords.map((rec) => (
|
{!isFuelLoading && !isDocumentsLoading && !isMaintenanceLoading && filteredRecords.map((rec) => (
|
||||||
<TableRow key={rec.id} hover sx={{ cursor: rec.type === 'Documents' ? 'default' : 'pointer' }} onClick={() => handleRowClick(rec.id, rec.type)}>
|
<TableRow key={rec.id} hover sx={{ cursor: rec.type === 'Fuel Logs' ? 'pointer' : 'default' }} onClick={() => handleRowClick(rec.id, rec.type)}>
|
||||||
<TableCell>{dayjs(rec.date).format('M/D/YYYY')}</TableCell>
|
<TableCell>{dayjs(rec.date).format('M/D/YYYY')}</TableCell>
|
||||||
<TableCell>{rec.type}</TableCell>
|
<TableCell>{rec.type}</TableCell>
|
||||||
<TableCell>{rec.summary}</TableCell>
|
<TableCell>{rec.summary}</TableCell>
|
||||||
|
|||||||
Reference in New Issue
Block a user