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>
163 lines
5.6 KiB
TypeScript
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>
|
|
);
|
|
};
|