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
This commit was merged in pull request #240.
This commit is contained in:
2026-05-16 01:56:10 +00: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>