Files
motovaultpro/frontend/src/features/dashboard/components/VehicleAttention.tsx
Eric Gullickson 325cf08df0 fix: promote vehicle display utils to core with null safety (refs #165)
Create shared getVehicleLabel/getVehicleSubtitle in core/utils with
VehicleLike interface. Replace all direct year/make/model concatenation
across 17 consumer files to prevent null values in vehicle names.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 19:32:40 -06:00

163 lines
5.6 KiB
TypeScript

/**
* @ai-summary List of vehicles needing attention (overdue maintenance)
*/
import React from 'react';
import { Box, SvgIconProps } from '@mui/material';
import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded';
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 {
vehicles: VehicleNeedingAttention[];
onVehicleClick?: (vehicleId: string) => void;
}
export const VehicleAttention: React.FC<VehicleAttentionProps> = ({ vehicles, onVehicleClick }) => {
if (vehicles.length === 0) {
return (
<GlassCard padding="md">
<div className="text-center py-8">
<Box sx={{ color: 'success.main', mb: 1.5 }}>
<CheckCircleRoundedIcon sx={{ fontSize: 48 }} />
</Box>
<h3 className="text-lg font-semibold text-slate-800 dark:text-avus mb-2">
All Caught Up!
</h3>
<p className="text-sm text-slate-500 dark:text-titanio">
No vehicles need immediate attention
</p>
</div>
</GlassCard>
);
}
const priorityConfig: Record<string, { color: string; icon: React.ComponentType<SvgIconProps> }> = {
high: {
color: 'error.main',
icon: ErrorRoundedIcon,
},
medium: {
color: 'warning.main',
icon: WarningAmberRoundedIcon,
},
low: {
color: 'info.main',
icon: ScheduleRoundedIcon,
},
};
return (
<GlassCard padding="md">
<div className="mb-4">
<h3 className="text-lg font-semibold text-slate-800 dark:text-avus">
Needs Attention
</h3>
<p className="text-sm text-slate-500 dark:text-titanio">
Vehicles with overdue maintenance
</p>
</div>
<div className="space-y-3">
{vehicles.map((vehicle) => {
const config = priorityConfig[vehicle.priority];
const IconComponent = config.icon;
return (
<Box
key={vehicle.id}
onClick={() => onVehicleClick?.(vehicle.id)}
role={onVehicleClick ? 'button' : undefined}
tabIndex={onVehicleClick ? 0 : undefined}
onKeyDown={(e: React.KeyboardEvent) => {
if (onVehicleClick && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
onVehicleClick(vehicle.id);
}
}}
sx={{
p: 2,
borderRadius: 3,
bgcolor: 'action.hover',
border: '1px solid',
borderColor: 'divider',
cursor: onVehicleClick ? 'pointer' : 'default',
transition: 'all 0.2s',
'&:hover': onVehicleClick ? {
bgcolor: 'action.selected',
} : {},
}}
>
<div className="flex items-start gap-3">
<Box sx={{ flexShrink: 0, color: config.color }}>
<IconComponent sx={{ fontSize: 24 }} />
</Box>
<div className="flex-1 min-w-0">
<Box
component="h4"
sx={{
fontWeight: 600,
color: 'text.primary',
fontSize: '1rem',
mb: 0.5,
}}
>
{getVehicleLabel(vehicle)}
</Box>
<p className="text-sm text-slate-600 dark:text-titanio">
{vehicle.reason}
</p>
<Box
component="span"
sx={{
display: 'inline-block',
mt: 1,
px: 1.5,
py: 0.5,
borderRadius: 2,
fontSize: '0.75rem',
fontWeight: 500,
bgcolor: 'action.selected',
color: config.color,
}}
>
{vehicle.priority.toUpperCase()} PRIORITY
</Box>
</div>
</div>
</Box>
);
})}
</div>
</GlassCard>
);
};
export const VehicleAttentionSkeleton: React.FC = () => {
return (
<GlassCard padding="md">
<div className="mb-4">
<div className="h-6 bg-slate-100 dark:bg-slate-800 rounded animate-pulse w-32 mb-2" />
<div className="h-4 bg-slate-100 dark:bg-slate-800 rounded animate-pulse w-48" />
</div>
<div className="space-y-3">
{[1, 2].map((i) => (
<div key={i} className="p-4 rounded-xl bg-slate-50 dark:bg-slate-800">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 bg-slate-100 dark:bg-slate-700 rounded animate-pulse" />
<div className="flex-1 space-y-2">
<div className="h-5 bg-slate-100 dark:bg-slate-700 rounded animate-pulse w-3/4" />
<div className="h-4 bg-slate-100 dark:bg-slate-700 rounded animate-pulse w-1/2" />
<div className="h-6 bg-slate-100 dark:bg-slate-700 rounded animate-pulse w-24 mt-2" />
</div>
</div>
</div>
))}
</div>
</GlassCard>
);
};