All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 3m22s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 51s
Deploy to Staging / Verify Staging (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
72 lines
2.9 KiB
TypeScript
72 lines
2.9 KiB
TypeScript
/**
|
|
* @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) };
|
|
}
|