Compare commits

..

3 Commits

Author SHA1 Message Date
9f5c81a14e Merge pull request 'fix: Vehicle summary screen does not display maintenance records (#239)' (#240) from issue-239-vehicle-summary-maintenance into main
All checks were successful
Deploy to Staging / Build Images (push) Successful in 10s
Deploy to Staging / Deploy to Staging (push) Successful in 42s
Deploy to Staging / Verify Staging (push) Successful in 3s
Deploy to Staging / Notify Staging Ready (push) Successful in 3s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
Reviewed-on: #240
2026-05-16 01:56:10 +00:00
Eric Gullickson
55b8b67a6e fix: coerce maintenance cost to number for amount column (refs #239)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 1m11s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 42s
Deploy to Staging / Verify Staging (pull_request) Successful in 4s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 3s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Postgres numeric columns come back as strings via node-postgres, so
typeof rec.cost === 'number' was false and the amount column rendered
as '—'. Coerce with Number() (matching the pattern in
MaintenanceRecordsList) so the cost displays as a dollar amount.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:51:58 -05:00
Eric Gullickson
a49f419eab 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>
2026-05-15 20:45:42 -05:00
2 changed files with 62 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 { 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,36 @@ 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(' • ');
// Backend returns numeric/decimal columns as strings via node-postgres; coerce.
const costNum = rec.cost != null ? Number(rec.cost) : NaN;
const amount = Number.isFinite(costNum) ? `$${costNum.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 +275,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 +294,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>

View File

@@ -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,26 @@ 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(' • ');
// Backend returns numeric/decimal columns as strings via node-postgres; coerce.
const costNum = rec.cost != null ? Number(rec.cost) : NaN;
const amount = Number.isFinite(costNum) ? `$${costNum.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 +497,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>