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
= ({ 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) => (
))
) : (
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) => (
))
) : (
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) => (
))
) : (
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) => (
))
) : (
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 && }