diff --git a/frontend/src/features/dashboard/components/VehicleRosterCard.tsx b/frontend/src/features/dashboard/components/VehicleRosterCard.tsx new file mode 100644 index 0000000..41bf831 --- /dev/null +++ b/frontend/src/features/dashboard/components/VehicleRosterCard.tsx @@ -0,0 +1,103 @@ +/** + * @ai-summary Vehicle roster card component for dashboard fleet grid + * Displays vehicle image, health status, attention items, and odometer + */ + +import React from 'react'; +import { clsx } from 'clsx'; +import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard'; +import { VehicleImage } from '../../vehicles/components/VehicleImage'; +import { getVehicleLabel } from '../../../core/utils/vehicleDisplay'; +import { VehicleRosterData, AttentionItem } from '../types'; + +interface VehicleRosterCardProps { + data: VehicleRosterData; + onClick: (vehicleId: string) => void; +} + +const getHealthBadgeClass = (health: VehicleRosterData['health']): string => { + switch (health) { + case 'green': + return 'bg-emerald-500'; + case 'yellow': + return 'bg-amber-500'; + case 'red': + return 'bg-red-500'; + } +}; + +const getAttentionItemClass = (urgency: AttentionItem['urgency']): string => { + switch (urgency) { + case 'overdue': + return 'text-red-600 dark:text-red-400'; + case 'due-soon': + return 'text-amber-600 dark:text-amber-400'; + case 'upcoming': + return 'text-slate-500 dark:text-titanio'; + } +}; + +const formatAttentionStatus = (item: AttentionItem): string => { + if (item.urgency === 'overdue') { + return 'OVERDUE'; + } + return `${item.daysUntilDue} days`; +}; + +export const VehicleRosterCard: React.FC = ({ + data, + onClick, +}) => { + const { vehicle, health, attentionItems } = data; + const displayedItems = attentionItems.slice(0, 3); + + return ( + onClick(vehicle.id)}> + {/* Top row: Image, Label, Health Badge */} +
+ {/* Vehicle image container - clips the built-in mb-2 margin */} +
+ +
+ + {/* Vehicle label */} +
+
+ {getVehicleLabel(vehicle)} +
+
+ + {/* Health badge */} +
+
+ + {/* Attention items */} +
+ {displayedItems.length === 0 ? ( +
+ All clear +
+ ) : ( + displayedItems.map((item, index) => ( +
+ {item.label} - {formatAttentionStatus(item)} +
+ )) + )} +
+ + {/* Odometer */} +
+ {vehicle.odometerReading.toLocaleString()} mi +
+ + ); +};