From a49f419eab302a9b7cac5866fa498099b00cc499 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 15 May 2026 20:45:42 -0500 Subject: [PATCH 1/2] fix: show maintenance records on vehicle summary screen (refs #239) 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) --- .../vehicles/mobile/VehicleDetailMobile.tsx | 36 +++++++++++++++++-- .../vehicles/pages/VehicleDetailPage.tsx | 30 +++++++++++++--- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx b/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx index 7f9d16f..bef18a7 100644 --- a/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx +++ b/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx @@ -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 = ({ 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(null); const [showMaintenanceDialog, setShowMaintenanceDialog] = useState(false); @@ -124,8 +128,34 @@ export const VehicleDetailMobile: 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 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 = ({
- {isFuelLoading ? 'Loading…' : `${filteredRecords.length} record${filteredRecords.length === 1 ? '' : 's'}`} + {(isFuelLoading || isMaintenanceLoading) ? 'Loading…' : `${filteredRecords.length} record${filteredRecords.length === 1 ? '' : 's'}`} Filter @@ -262,7 +292,7 @@ export const VehicleDetailMobile: React.FC = ({ - {isFuelLoading ? ( + {(isFuelLoading || isMaintenanceLoading) ? ( Loading records… diff --git a/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx b/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx index 179aef4..65e7e0f 100644 --- a/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx +++ b/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx @@ -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 = () => { - {(isFuelLoading || isDocumentsLoading) && ( + {(isFuelLoading || isDocumentsLoading || isMaintenanceLoading) && ( Loading records… )} - {!isFuelLoading && !isDocumentsLoading && filteredRecords.length === 0 && ( + {!isFuelLoading && !isDocumentsLoading && !isMaintenanceLoading && filteredRecords.length === 0 && ( No records found for this filter. )} - {!isFuelLoading && !isDocumentsLoading && filteredRecords.map((rec) => ( - handleRowClick(rec.id, rec.type)}> + {!isFuelLoading && !isDocumentsLoading && !isMaintenanceLoading && filteredRecords.map((rec) => ( + handleRowClick(rec.id, rec.type)}> {dayjs(rec.date).format('M/D/YYYY')} {rec.type} {rec.summary} -- 2.49.1 From 55b8b67a6e602a29181aae7a4b5b102a6efa8b7d Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 15 May 2026 20:51:58 -0500 Subject: [PATCH 2/2] fix: coerce maintenance cost to number for amount column (refs #239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx | 4 +++- frontend/src/features/vehicles/pages/VehicleDetailPage.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx b/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx index bef18a7..b241054 100644 --- a/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx +++ b/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx @@ -142,7 +142,9 @@ export const VehicleDetailMobile: React.FC = ({ 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; + // 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', diff --git a/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx b/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx index 65e7e0f..4bdce5b 100644 --- a/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx +++ b/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx @@ -148,7 +148,9 @@ export const VehicleDetailPage: React.FC = () => { 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; + // 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 }); } } -- 2.49.1