diff --git a/frontend/README.md b/frontend/README.md index 9955dbc..4c7ffc7 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -13,6 +13,7 @@ - `src/App.tsx`, `src/main.tsx` — app entry. - `src/features/*` — feature pages/components/hooks. - `src/core/*` — auth, api, store, hooks, query config, utils. +- `src/core/utils/vehicleDisplay.ts` — shared vehicle display helpers: `getVehicleLabel()` (display name with fallback chain) and `getVehicleSubtitle()` (Year Make Model formatting). - `src/shared-minimal/*` — shared UI components and theme. ## Mobile + Desktop (required) diff --git a/frontend/src/core/utils/vehicleDisplay.ts b/frontend/src/core/utils/vehicleDisplay.ts new file mode 100644 index 0000000..2828e79 --- /dev/null +++ b/frontend/src/core/utils/vehicleDisplay.ts @@ -0,0 +1,27 @@ +/** Vehicle-like object with minimal fields for display purposes */ +export interface VehicleLike { + year?: number | null; + make?: string | null; + model?: string | null; + trimLevel?: string | null; + nickname?: string | null; + vin?: string | null; + id?: string | null; +} + +/** Primary display name with fallback chain: nickname -> year/make/model -> VIN -> ID */ +export const getVehicleLabel = (vehicle: VehicleLike | undefined): string => { + if (!vehicle) return 'Unknown Vehicle'; + if (vehicle.nickname?.trim()) return vehicle.nickname.trim(); + const parts = [vehicle.year, vehicle.make, vehicle.model, vehicle.trimLevel].filter(Boolean); + if (parts.length > 0) return parts.join(' '); + if (vehicle.vin) return vehicle.vin; + return vehicle.id ? `${vehicle.id.substring(0, 8)}...` : 'Unknown Vehicle'; +}; + +/** Subtitle line: "Year Make Model" with null safety. Returns empty string if insufficient data. */ +export const getVehicleSubtitle = (vehicle: VehicleLike | undefined): string => { + if (!vehicle) return ''; + const parts = [vehicle.year?.toString(), vehicle.make, vehicle.model].filter(Boolean); + return parts.length >= 2 ? parts.join(' ') : ''; +}; diff --git a/frontend/src/features/admin/mobile/AdminUsersMobileScreen.tsx b/frontend/src/features/admin/mobile/AdminUsersMobileScreen.tsx index 0c552b0..4cfc876 100644 --- a/frontend/src/features/admin/mobile/AdminUsersMobileScreen.tsx +++ b/frontend/src/features/admin/mobile/AdminUsersMobileScreen.tsx @@ -24,6 +24,7 @@ import { SubscriptionTier, ListUsersParams, } from '../types/admin.types'; +import { getVehicleSubtitle } from '@/core/utils/vehicleDisplay'; // Modal component for dialogs interface ModalProps { @@ -128,7 +129,7 @@ const UserVehiclesList: React.FC<{ auth0Sub: string; isOpen: boolean }> = ({ aut
{data.vehicles.map((vehicle, idx) => (
- {vehicle.year} {vehicle.make} {vehicle.model} + {getVehicleSubtitle(vehicle) || 'Unknown Vehicle'}
))}
diff --git a/frontend/src/features/dashboard/components/VehicleAttention.tsx b/frontend/src/features/dashboard/components/VehicleAttention.tsx index 2ee6b67..89fbabc 100644 --- a/frontend/src/features/dashboard/components/VehicleAttention.tsx +++ b/frontend/src/features/dashboard/components/VehicleAttention.tsx @@ -9,6 +9,7 @@ import ErrorRoundedIcon from '@mui/icons-material/ErrorRounded'; import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded'; import ScheduleRoundedIcon from '@mui/icons-material/ScheduleRounded'; import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard'; +import { getVehicleLabel } from '@/core/utils/vehicleDisplay'; import { VehicleNeedingAttention } from '../types'; interface VehicleAttentionProps { @@ -104,7 +105,7 @@ export const VehicleAttention: React.FC = ({ vehicles, on mb: 0.5, }} > - {vehicle.nickname || `${vehicle.year} ${vehicle.make} ${vehicle.model}`} + {getVehicleLabel(vehicle)}

{vehicle.reason} diff --git a/frontend/src/features/documents/CLAUDE.md b/frontend/src/features/documents/CLAUDE.md index b3b0e9c..c115134 100644 --- a/frontend/src/features/documents/CLAUDE.md +++ b/frontend/src/features/documents/CLAUDE.md @@ -12,7 +12,6 @@ Document management UI with maintenance manual extraction. Handles file uploads, | `mobile/` | Mobile-specific document layout | Mobile UI | | `pages/` | DocumentsPage, DocumentDetailPage | Page layout | | `types/` | TypeScript type definitions | Type changes | -| `utils/` | Utility functions (vehicle label formatting) | Helper logic | ## Key Files diff --git a/frontend/src/features/documents/mobile/DocumentsMobileScreen.tsx b/frontend/src/features/documents/mobile/DocumentsMobileScreen.tsx index 56040e8..aa09092 100644 --- a/frontend/src/features/documents/mobile/DocumentsMobileScreen.tsx +++ b/frontend/src/features/documents/mobile/DocumentsMobileScreen.tsx @@ -10,7 +10,7 @@ import { AddDocumentDialog } from '../components/AddDocumentDialog'; import { ExpirationBadge } from '../components/ExpirationBadge'; import { DocumentCardMetadata } from '../components/DocumentCardMetadata'; import { useVehicles } from '../../vehicles/hooks/useVehicles'; -import { getVehicleLabel } from '../utils/vehicleLabel'; +import { getVehicleLabel } from '@/core/utils/vehicleDisplay'; export const DocumentsMobileScreen: React.FC = () => { console.log('[DocumentsMobileScreen] Component initializing'); diff --git a/frontend/src/features/documents/pages/DocumentDetailPage.tsx b/frontend/src/features/documents/pages/DocumentDetailPage.tsx index 7cf4d5a..680a9a8 100644 --- a/frontend/src/features/documents/pages/DocumentDetailPage.tsx +++ b/frontend/src/features/documents/pages/DocumentDetailPage.tsx @@ -12,7 +12,7 @@ import { EditDocumentDialog } from '../components/EditDocumentDialog'; import { ExpirationBadge } from '../components/ExpirationBadge'; import { DocumentCardMetadata } from '../components/DocumentCardMetadata'; import { useVehicle, useVehicles } from '../../vehicles/hooks/useVehicles'; -import { getVehicleLabel } from '../utils/vehicleLabel'; +import { getVehicleLabel } from '@/core/utils/vehicleDisplay'; export const DocumentDetailPage: React.FC = () => { const { id } = useParams<{ id: string }>(); diff --git a/frontend/src/features/documents/pages/DocumentsPage.tsx b/frontend/src/features/documents/pages/DocumentsPage.tsx index 9f44d19..93bb0a6 100644 --- a/frontend/src/features/documents/pages/DocumentsPage.tsx +++ b/frontend/src/features/documents/pages/DocumentsPage.tsx @@ -21,7 +21,7 @@ import { ExpirationBadge } from '../components/ExpirationBadge'; import type { DocumentRecord } from '../types/documents.types'; import { DocumentCardMetadata } from '../components/DocumentCardMetadata'; import { useVehicles } from '../../vehicles/hooks/useVehicles'; -import { getVehicleLabel } from '../utils/vehicleLabel'; +import { getVehicleLabel } from '@/core/utils/vehicleDisplay'; export const DocumentsPage: React.FC = () => { const { isAuthenticated, isLoading: authLoading, loginWithRedirect } = useAuth0(); diff --git a/frontend/src/features/documents/utils/vehicleLabel.ts b/frontend/src/features/documents/utils/vehicleLabel.ts deleted file mode 100644 index b040fbb..0000000 --- a/frontend/src/features/documents/utils/vehicleLabel.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Vehicle } from '../../vehicles/types/vehicles.types'; - -export const getVehicleLabel = (vehicle: Vehicle | undefined): string => { - if (!vehicle) return 'Unknown Vehicle'; - if (vehicle.nickname?.trim()) return vehicle.nickname.trim(); - const parts = [vehicle.year, vehicle.make, vehicle.model, vehicle.trimLevel].filter(Boolean); - const primary = parts.join(' ').trim(); - if (primary.length > 0) return primary; - if (vehicle.vin?.length > 0) return vehicle.vin; - return vehicle.id.slice(0, 8) + '...'; -}; diff --git a/frontend/src/features/email-ingestion/components/ResolveAssociationDialog.tsx b/frontend/src/features/email-ingestion/components/ResolveAssociationDialog.tsx index 0c6425a..f5ef8eb 100644 --- a/frontend/src/features/email-ingestion/components/ResolveAssociationDialog.tsx +++ b/frontend/src/features/email-ingestion/components/ResolveAssociationDialog.tsx @@ -23,6 +23,7 @@ import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded'; import { useVehicles } from '../../vehicles/hooks/useVehicles'; import { useResolveAssociation } from '../hooks/usePendingAssociations'; import type { PendingVehicleAssociation } from '../types/email-ingestion.types'; +import { getVehicleLabel } from '@/core/utils/vehicleDisplay'; interface ResolveAssociationDialogProps { open: boolean; @@ -166,9 +167,7 @@ export const ResolveAssociationDialog: React.FC = {vehicles.map((vehicle) => { const isSelected = selectedVehicleId === vehicle.id; - const vehicleName = vehicle.nickname - || [vehicle.year, vehicle.make, vehicle.model].filter(Boolean).join(' ') - || 'Unnamed Vehicle'; + const vehicleName = getVehicleLabel(vehicle); return ( { const vehicle = vehicles?.find((v: Vehicle) => v.id === record.vehicleId); - if (!vehicle) return 'Unknown Vehicle'; - if (vehicle.nickname?.trim()) return vehicle.nickname.trim(); - const parts = [vehicle.year, vehicle.make, vehicle.model, vehicle.trimLevel].filter(Boolean); - return parts.length > 0 ? parts.join(' ') : 'Vehicle'; + return getVehicleLabel(vehicle); })()} helperText="Vehicle cannot be changed when editing" /> diff --git a/frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx b/frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx index 755373d..cd2d699 100644 --- a/frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx +++ b/frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx @@ -46,6 +46,7 @@ import { useTierAccess } from '../../../core/hooks/useTierAccess'; import { UpgradeRequiredDialog } from '../../../shared-minimal/components/UpgradeRequiredDialog'; import { documentsApi } from '../../documents/api/documents.api'; import toast from 'react-hot-toast'; +import { getVehicleSubtitle } from '@/core/utils/vehicleDisplay'; const schema = z.object({ vehicle_id: z.string().uuid({ message: 'Please select a vehicle' }), @@ -279,7 +280,7 @@ export const MaintenanceRecordForm: React.FC = () => { {vehicles && vehicles.length > 0 ? ( vehicles.map((vehicle) => ( - {vehicle.year} {vehicle.make} {vehicle.model} + {getVehicleSubtitle(vehicle) || 'Unknown Vehicle'} )) ) : ( diff --git a/frontend/src/features/maintenance/components/MaintenanceScheduleEditDialog.tsx b/frontend/src/features/maintenance/components/MaintenanceScheduleEditDialog.tsx index 2b94d2a..3af3644 100644 --- a/frontend/src/features/maintenance/components/MaintenanceScheduleEditDialog.tsx +++ b/frontend/src/features/maintenance/components/MaintenanceScheduleEditDialog.tsx @@ -39,6 +39,7 @@ import { import { SubtypeCheckboxGroup } from './SubtypeCheckboxGroup'; import { useVehicles } from '../../vehicles/hooks/useVehicles'; import type { Vehicle } from '../../vehicles/types/vehicles.types'; +import { getVehicleLabel } from '@/core/utils/vehicleDisplay'; interface MaintenanceScheduleEditDialogProps { open: boolean; @@ -206,10 +207,7 @@ export const MaintenanceScheduleEditDialog: React.FC { const vehicle = vehicles?.find((v: Vehicle) => v.id === schedule.vehicleId); - if (!vehicle) return 'Unknown Vehicle'; - if (vehicle.nickname?.trim()) return vehicle.nickname.trim(); - const parts = [vehicle.year, vehicle.make, vehicle.model, vehicle.trimLevel].filter(Boolean); - return parts.length > 0 ? parts.join(' ') : 'Vehicle'; + return getVehicleLabel(vehicle); })()} helperText="Vehicle cannot be changed when editing" /> diff --git a/frontend/src/features/maintenance/components/MaintenanceScheduleForm.tsx b/frontend/src/features/maintenance/components/MaintenanceScheduleForm.tsx index a1836c7..f9ccc98 100644 --- a/frontend/src/features/maintenance/components/MaintenanceScheduleForm.tsx +++ b/frontend/src/features/maintenance/components/MaintenanceScheduleForm.tsx @@ -42,6 +42,7 @@ import { getCategoryDisplayName, } from '../types/maintenance.types'; import toast from 'react-hot-toast'; +import { getVehicleSubtitle } from '@/core/utils/vehicleDisplay'; const schema = z .object({ @@ -214,7 +215,7 @@ export const MaintenanceScheduleForm: React.FC = () => { {vehicles && vehicles.length > 0 ? ( vehicles.map((vehicle) => ( - {vehicle.year} {vehicle.make} {vehicle.model} + {getVehicleSubtitle(vehicle) || 'Unknown Vehicle'} )) ) : ( diff --git a/frontend/src/features/maintenance/mobile/MaintenanceMobileScreen.tsx b/frontend/src/features/maintenance/mobile/MaintenanceMobileScreen.tsx index edc5e39..5bc618c 100644 --- a/frontend/src/features/maintenance/mobile/MaintenanceMobileScreen.tsx +++ b/frontend/src/features/maintenance/mobile/MaintenanceMobileScreen.tsx @@ -16,6 +16,7 @@ import { MaintenanceScheduleForm } from '../components/MaintenanceScheduleForm'; import { MaintenanceSchedulesList } from '../components/MaintenanceSchedulesList'; import { MaintenanceScheduleEditDialog } from '../components/MaintenanceScheduleEditDialog'; import type { MaintenanceRecordResponse, UpdateMaintenanceRecordRequest, MaintenanceScheduleResponse, UpdateScheduleRequest } from '../types/maintenance.types'; +import { getVehicleSubtitle } from '@/core/utils/vehicleDisplay'; export const MaintenanceMobileScreen: React.FC = () => { const queryClient = useQueryClient(); @@ -125,7 +126,7 @@ export const MaintenanceMobileScreen: React.FC = () => { {vehicles && vehicles.length > 0 ? ( vehicles.map((vehicle) => ( - {vehicle.year} {vehicle.make} {vehicle.model} + {getVehicleSubtitle(vehicle) || 'Unknown Vehicle'} )) ) : ( diff --git a/frontend/src/features/maintenance/pages/MaintenancePage.tsx b/frontend/src/features/maintenance/pages/MaintenancePage.tsx index ff0a241..ca513e7 100644 --- a/frontend/src/features/maintenance/pages/MaintenancePage.tsx +++ b/frontend/src/features/maintenance/pages/MaintenancePage.tsx @@ -16,6 +16,7 @@ import { useMaintenanceRecords } from '../hooks/useMaintenanceRecords'; import { useVehicles } from '../../vehicles/hooks/useVehicles'; import { FormSuspense } from '../../../components/SuspenseWrappers'; import type { MaintenanceRecordResponse, UpdateMaintenanceRecordRequest, MaintenanceScheduleResponse, UpdateScheduleRequest } from '../types/maintenance.types'; +import { getVehicleSubtitle } from '@/core/utils/vehicleDisplay'; export const MaintenancePage: React.FC = () => { const { data: vehicles, isLoading: isLoadingVehicles } = useVehicles(); @@ -156,7 +157,7 @@ export const MaintenancePage: React.FC = () => { {vehicles && vehicles.length > 0 ? ( vehicles.map((vehicle) => ( - {vehicle.year} {vehicle.make} {vehicle.model} + {getVehicleSubtitle(vehicle) || 'Unknown Vehicle'} )) ) : ( diff --git a/frontend/src/features/settings/mobile/MobileSettingsScreen.tsx b/frontend/src/features/settings/mobile/MobileSettingsScreen.tsx index b2c1ae9..c8183a1 100644 --- a/frontend/src/features/settings/mobile/MobileSettingsScreen.tsx +++ b/frontend/src/features/settings/mobile/MobileSettingsScreen.tsx @@ -7,6 +7,7 @@ import { useSettings } from '../hooks/useSettings'; import { useProfile, useUpdateProfile } from '../hooks/useProfile'; import { useExportUserData } from '../hooks/useExportUserData'; import { useVehicles } from '../../vehicles/hooks/useVehicles'; +import { getVehicleSubtitle } from '@/core/utils/vehicleDisplay'; import { useSubscription } from '../../subscription/hooks/useSubscription'; import { useAdminAccess } from '../../../core/auth/useAdminAccess'; import { useNavigationStore } from '../../../core/store'; @@ -373,7 +374,7 @@ export const MobileSettingsScreen: React.FC = () => { className="p-3 bg-slate-50 dark:bg-nero rounded-lg" >

- {vehicle.year} {vehicle.make} {vehicle.model} + {getVehicleSubtitle(vehicle) || 'Unknown Vehicle'}

{vehicle.nickname && (

{vehicle.nickname}

diff --git a/frontend/src/features/subscription/components/VehicleSelectionDialog.tsx b/frontend/src/features/subscription/components/VehicleSelectionDialog.tsx index e0bf2fc..afb4191 100644 --- a/frontend/src/features/subscription/components/VehicleSelectionDialog.tsx +++ b/frontend/src/features/subscription/components/VehicleSelectionDialog.tsx @@ -13,6 +13,7 @@ import { Box, } from '@mui/material'; import type { SubscriptionTier } from '../types/subscription.types'; +import { getVehicleLabel } from '@/core/utils/vehicleDisplay'; interface Vehicle { id: string; @@ -70,13 +71,6 @@ export const VehicleSelectionDialog = ({ onConfirm(selectedVehicleIds); }; - const getVehicleLabel = (vehicle: Vehicle): string => { - if (vehicle.nickname) { - return vehicle.nickname; - } - const parts = [vehicle.year, vehicle.make, vehicle.model].filter(Boolean); - return parts.join(' ') || 'Unknown Vehicle'; - }; const canConfirm = selectedVehicleIds.length > 0 && selectedVehicleIds.length <= maxSelections; diff --git a/frontend/src/features/vehicles/components/VehicleCard.tsx b/frontend/src/features/vehicles/components/VehicleCard.tsx index 05ba0ea..276b85d 100644 --- a/frontend/src/features/vehicles/components/VehicleCard.tsx +++ b/frontend/src/features/vehicles/components/VehicleCard.tsx @@ -9,6 +9,7 @@ import DeleteIcon from '@mui/icons-material/Delete'; import { Vehicle } from '../types/vehicles.types'; import { useUnits } from '../../../core/units/UnitsContext'; import { VehicleImage } from './VehicleImage'; +import { getVehicleLabel } from '@/core/utils/vehicleDisplay'; interface VehicleCardProps { vehicle: Vehicle; @@ -24,8 +25,7 @@ export const VehicleCard: React.FC = ({ onSelect, }) => { const { formatDistance } = useUnits(); - const displayName = vehicle.nickname || - [vehicle.year, vehicle.make, vehicle.model, vehicle.trimLevel].filter(Boolean).join(' '); + const displayName = getVehicleLabel(vehicle); return ( = ({ onLogFuel, onEdit }) => { - const displayName = vehicle.nickname || - [vehicle.year, vehicle.make, vehicle.model, vehicle.trimLevel].filter(Boolean).join(' ') || 'Vehicle'; + const displayName = getVehicleLabel(vehicle); const displayModel = vehicle.model || 'Unknown Model'; const [recordFilter, setRecordFilter] = useState<'All' | 'Fuel Logs' | 'Maintenance' | 'Documents'>('All'); diff --git a/frontend/src/features/vehicles/mobile/VehicleMobileCard.tsx b/frontend/src/features/vehicles/mobile/VehicleMobileCard.tsx index 8f87617..14aec11 100644 --- a/frontend/src/features/vehicles/mobile/VehicleMobileCard.tsx +++ b/frontend/src/features/vehicles/mobile/VehicleMobileCard.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { Card, CardActionArea, Box, Typography } from '@mui/material'; import { Vehicle } from '../types/vehicles.types'; import { VehicleImage } from '../components/VehicleImage'; +import { getVehicleLabel } from '@/core/utils/vehicleDisplay'; interface VehicleMobileCardProps { vehicle: Vehicle; @@ -18,8 +19,7 @@ export const VehicleMobileCard: React.FC = ({ onClick, compact = false }) => { - const displayName = vehicle.nickname || - [vehicle.year, vehicle.make, vehicle.model, vehicle.trimLevel].filter(Boolean).join(' ') || 'Vehicle'; + const displayName = getVehicleLabel(vehicle); const displayModel = vehicle.model || 'Unknown Model'; return ( diff --git a/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx b/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx index 5e8fa0d..da384cc 100644 --- a/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx +++ b/frontend/src/features/vehicles/pages/VehicleDetailPage.tsx @@ -12,6 +12,7 @@ import LocalGasStationIcon from '@mui/icons-material/LocalGasStation'; import BuildIcon from '@mui/icons-material/Build'; import DeleteIcon from '@mui/icons-material/Delete'; import { Vehicle } from '../types/vehicles.types'; +import { getVehicleLabel, getVehicleSubtitle } from '@/core/utils/vehicleDisplay'; import { vehiclesApi } from '../api/vehicles.api'; import { Card } from '../../../shared-minimal/components/Card'; import { VehicleForm } from '../components/VehicleForm'; @@ -224,8 +225,7 @@ export const VehicleDetailPage: React.FC = () => { ); } - const displayName = vehicle.nickname || - [vehicle.year, vehicle.make, vehicle.model, vehicle.trimLevel].filter(Boolean).join(' ') || 'Vehicle'; + const displayName = getVehicleLabel(vehicle); const handleRowClick = (recId: string, type: VehicleRecord['type']) => { if (type === 'Fuel Logs') { @@ -373,8 +373,7 @@ export const VehicleDetailPage: React.FC = () => { Vehicle Details - {vehicle.year} {vehicle.make} {vehicle.model} - {vehicle.trimLevel && ` ${vehicle.trimLevel}`} + {getVehicleSubtitle(vehicle) || 'Unknown Vehicle'} {vehicle.vin && ( diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 5c22daf..c7860f7 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -11,6 +11,7 @@ import { useAdminAccess } from '../core/auth/useAdminAccess'; import { useProfile, useUpdateProfile } from '../features/settings/hooks/useProfile'; import { useExportUserData } from '../features/settings/hooks/useExportUserData'; import { useVehicles } from '../features/vehicles/hooks/useVehicles'; +import { getVehicleSubtitle } from '@/core/utils/vehicleDisplay'; import { useSubscription } from '../features/subscription/hooks/useSubscription'; import { useTheme } from '../shared-minimal/theme/ThemeContext'; import { DeleteAccountDialog } from '../features/settings/components/DeleteAccountDialog'; @@ -375,7 +376,7 @@ export const SettingsPage: React.FC = () => { {index > 0 && }