fix: show maintenance records on vehicle summary screen (refs #239)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 1m14s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 42s
Deploy to Staging / Verify Staging (pull_request) Successful in 3s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 4s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped

The Vehicle Records section on /garage/vehicles/:id never called
useMaintenanceRecords, so maintenance rows always rendered empty even
when records existed for the vehicle. Wire the existing hook into both
the desktop VehicleDetailPage and mobile VehicleDetailMobile, merge
records into the unified list with category + subtypes + shop name,
and include the maintenance loading state in the section's loading
and empty-state guards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-05-15 20:45:42 -05:00
parent f6e3963b99
commit a49f419eab
2 changed files with 58 additions and 8 deletions

View File

@@ -12,6 +12,9 @@ import { FuelLogResponse, UpdateFuelLogRequest } from '../../fuel-logs/types/fue
import { FuelLogEditDialog } from '../../fuel-logs/components/FuelLogEditDialog';
import { fuelLogsApi } from '../../fuel-logs/api/fuel-logs.api';
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 { OwnershipCostsList } from '../../ownership-costs';
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 { fuelLogs, isLoading: isFuelLoading } = useFuelLogs(vehicle.id);
const { records: maintenanceRecords, isRecordsLoading: isMaintenanceLoading } = useMaintenanceRecords(vehicle.id);
const queryClient = useQueryClient();
const [editingLog, setEditingLog] = useState<FuelLogResponse | null>(null);
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));
}, [fuelLogs]);
}, [fuelLogs, maintenanceRecords]);
const filteredRecords = useMemo(() => {
if (recordFilter === 'All') return records;
@@ -243,7 +273,7 @@ export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
<Section title="Vehicle Records">
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<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>
<FormControl size="small" sx={{ minWidth: 180 }}>
<InputLabel id="vehicle-records-filter">Filter</InputLabel>
@@ -262,7 +292,7 @@ export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
</Box>
<Card>
<CardContent sx={{ p: 0 }}>
{isFuelLoading ? (
{(isFuelLoading || isMaintenanceLoading) ? (
<Box sx={{ p: 2 }}>
<Typography color="text.secondary" variant="body2">Loading records</Typography>
</Box>

View File

@@ -23,6 +23,9 @@ import { FuelLogResponse, UpdateFuelLogRequest } from '../../fuel-logs/types/fue
import { FuelLogEditDialog } from '../../fuel-logs/components/FuelLogEditDialog';
import { FuelLogForm } from '../../fuel-logs/components/FuelLogForm';
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
import { fuelLogsApi } from '../../fuel-logs/api/fuel-logs.api';
import { OwnershipCostsList } from '../../ownership-costs';
@@ -60,6 +63,7 @@ export const VehicleDetailPage: React.FC = () => {
const { fuelLogs, isLoading: isFuelLoading } = useFuelLogs(id);
const { data: documents, isLoading: isDocumentsLoading } = useDocumentsByVehicle(id);
const { records: maintenanceRecords, isRecordsLoading: isMaintenanceLoading } = useMaintenanceRecords(id);
const { mutateAsync: deleteDocument } = useDeleteDocument();
const { mutateAsync: removeVehicleFromDocument } = useRemoveVehicleFromDocument();
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));
}, [fuelLogs, documents]);
}, [fuelLogs, documents, maintenanceRecords]);
const filteredRecords = useMemo(() => {
if (recordFilter === 'All') return records;
@@ -475,22 +495,22 @@ export const VehicleDetailPage: React.FC = () => {
</TableRow>
</TableHead>
<TableBody>
{(isFuelLoading || isDocumentsLoading) && (
{(isFuelLoading || isDocumentsLoading || isMaintenanceLoading) && (
<TableRow>
<TableCell colSpan={5}>
<Typography color="text.secondary">Loading records</Typography>
</TableCell>
</TableRow>
)}
{!isFuelLoading && !isDocumentsLoading && filteredRecords.length === 0 && (
{!isFuelLoading && !isDocumentsLoading && !isMaintenanceLoading && filteredRecords.length === 0 && (
<TableRow>
<TableCell colSpan={5}>
<Typography color="text.secondary">No records found for this filter.</Typography>
</TableCell>
</TableRow>
)}
{!isFuelLoading && !isDocumentsLoading && filteredRecords.map((rec) => (
<TableRow key={rec.id} hover sx={{ cursor: rec.type === 'Documents' ? 'default' : 'pointer' }} onClick={() => handleRowClick(rec.id, rec.type)}>
{!isFuelLoading && !isDocumentsLoading && !isMaintenanceLoading && filteredRecords.map((rec) => (
<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>{rec.type}</TableCell>
<TableCell>{rec.summary}</TableCell>