export type AttendanceStreamEntry = { status: 'PRESENT' | 'ABSENT' | 'EXCUSED'; occurredAt: Date; }; export type RiskLevel = 'HIGH' | 'MEDIUM'; // Cuenta las ausencias consecutivas sin justificar desde la clase más reciente. // Recibe la lista ordenada de más reciente a más antigua; PRESENT y EXCUSED rompen la cadena. export function trailingAbsentStreak(entries: AttendanceStreamEntry[]): number { let streak = 0; for (const entry of entries) { if (entry.status !== 'ABSENT') { break; } streak += 1; } return streak; } // Fecha de la última clase a la que el alumno asistió (PRESENT), o null. export function lastAttendedAt(entries: AttendanceStreamEntry[]): Date | null { for (const entry of entries) { if (entry.status === 'PRESENT') { return entry.occurredAt; } } return null; } // Porcentaje de asistencia sobre el total de sesiones del período. // Las sesiones sin registrar cuentan como no asistidas. export function monthlyAttendanceRate( entries: AttendanceStreamEntry[], totalSessions: number, ): number { if (totalSessions <= 0) { return 0; } const present = entries.filter((entry) => entry.status === 'PRESENT').length; return Math.round((present / totalSessions) * 100); } // HIGH >= 3 ausencias consecutivas; MEDIUM == 2 consecutivas o asistencia mensual < 50%. export function classifyRisk( consecutiveAbsences: number, monthlyRate: number, ): RiskLevel | null { if (consecutiveAbsences >= 3) { return 'HIGH'; } if (consecutiveAbsences === 2 || monthlyRate < 50) { return 'MEDIUM'; } return null; }