/** * @ai-summary Pure function to compute per-vehicle health status from maintenance and document data */ import { MaintenanceSchedule } from '../../maintenance/types/maintenance.types'; import { DocumentRecord } from '../../documents/types/documents.types'; import { VehicleHealth, AttentionItem } from '../types'; /** * Compute health status and attention items for a single vehicle. * Pure function -- no React dependencies, easily unit-testable. */ export function computeVehicleHealth( schedules: MaintenanceSchedule[], documents: DocumentRecord[], ): { health: VehicleHealth; attentionItems: AttentionItem[] } { const now = new Date(); const items: AttentionItem[] = []; // Maintenance schedule attention items for (const schedule of schedules) { if (!schedule.nextDueDate || !schedule.isActive) continue; const dueDate = new Date(schedule.nextDueDate); const daysUntil = Math.floor((dueDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); const label = schedule.subtypes.length > 0 ? schedule.subtypes[0] : schedule.category.replace(/_/g, ' '); if (daysUntil < 0) { items.push({ label, urgency: 'overdue', daysUntilDue: daysUntil, source: 'maintenance' }); } else if (daysUntil <= 14) { items.push({ label, urgency: 'due-soon', daysUntilDue: daysUntil, source: 'maintenance' }); } else if (daysUntil <= 30) { items.push({ label, urgency: 'upcoming', daysUntilDue: daysUntil, source: 'maintenance' }); } } // Document expiry attention items (insurance, registration) for (const doc of documents) { if (!doc.expirationDate) continue; const expiryDate = new Date(doc.expirationDate); const daysUntil = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); const label = doc.documentType === 'insurance' ? 'Insurance' : 'Registration'; if (daysUntil < 0) { items.push({ label, urgency: 'overdue', daysUntilDue: daysUntil, source: 'document' }); } else if (daysUntil <= 14) { items.push({ label, urgency: 'due-soon', daysUntilDue: daysUntil, source: 'document' }); } else if (daysUntil <= 30) { items.push({ label, urgency: 'upcoming', daysUntilDue: daysUntil, source: 'document' }); } } // Sort: overdue first (most overdue at top), then due-soon by proximity, then upcoming const urgencyOrder = { overdue: 0, 'due-soon': 1, upcoming: 2 }; items.sort((a, b) => { const urgencyDiff = urgencyOrder[a.urgency] - urgencyOrder[b.urgency]; if (urgencyDiff !== 0) return urgencyDiff; return a.daysUntilDue - b.daysUntilDue; }); // Determine health color const hasOverdue = items.some(i => i.urgency === 'overdue'); const hasDueSoon = items.some(i => i.urgency === 'due-soon'); let health: VehicleHealth = 'green'; if (hasOverdue) health = 'red'; else if (hasDueSoon) health = 'yellow'; return { health, attentionItems: items.slice(0, 3) }; }