feat: add absence notification and analytics features
- Implemented AbsenceNotifyView for notifying absence with session details. - Created AnalyticsView for displaying group analytics and managing students at risk. - Added ClassAttendanceView for marking attendance in classes. - Defined new schemas for analytics and attendance in shared package.
This commit is contained in:
79
apps/backend/test/analytics-risk.test.ts
Normal file
79
apps/backend/test/analytics-risk.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
classifyRisk,
|
||||
lastAttendedAt,
|
||||
monthlyAttendanceRate,
|
||||
trailingAbsentStreak,
|
||||
} from '@/modules/analytics/lib/risk';
|
||||
|
||||
const day = 24 * 60 * 60 * 1000;
|
||||
const at = (offsetDays: number) => new Date(Date.now() - offsetDays * day);
|
||||
|
||||
function entry(status: 'PRESENT' | 'ABSENT' | 'EXCUSED', offsetDays: number) {
|
||||
return { status, occurredAt: at(offsetDays) };
|
||||
}
|
||||
|
||||
describe('trailingAbsentStreak', () => {
|
||||
it('devuelve 0 sin registros', () => {
|
||||
expect(trailingAbsentStreak([])).toBe(0);
|
||||
});
|
||||
|
||||
it('cuenta ausencias consecutivas sin justificar desde la más reciente', () => {
|
||||
expect(trailingAbsentStreak([entry('ABSENT', 0)])).toBe(1);
|
||||
expect(trailingAbsentStreak([entry('ABSENT', 0), entry('ABSENT', 7), entry('ABSENT', 14)])).toBe(3);
|
||||
});
|
||||
|
||||
it('rompe la cadena con PRESENT o EXCUSED', () => {
|
||||
expect(
|
||||
trailingAbsentStreak([entry('ABSENT', 0), entry('ABSENT', 7), entry('PRESENT', 14)]),
|
||||
).toBe(2);
|
||||
expect(trailingAbsentStreak([entry('EXCUSED', 0), entry('ABSENT', 7)])).toBe(0);
|
||||
expect(trailingAbsentStreak([entry('PRESENT', 0)])).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lastAttendedAt', () => {
|
||||
it('devuelve la clase PRESENT más reciente', () => {
|
||||
const result = lastAttendedAt([
|
||||
entry('PRESENT', 0),
|
||||
entry('ABSENT', 7),
|
||||
entry('ABSENT', 14),
|
||||
]);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.getTime()).toBe(at(0).getTime());
|
||||
});
|
||||
|
||||
it('devuelve null si nunca asistió', () => {
|
||||
expect(lastAttendedAt([entry('ABSENT', 0), entry('EXCUSED', 7)])).toBeNull();
|
||||
expect(lastAttendedAt([])).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('monthlyAttendanceRate', () => {
|
||||
it('razón de PRESENT sobre el total de sesiones', () => {
|
||||
expect(monthlyAttendanceRate([entry('PRESENT', 0), entry('PRESENT', 7), entry('ABSENT', 14)], 5)).toBe(40);
|
||||
});
|
||||
|
||||
it('devuelve 0 sin sesiones o sin presentes', () => {
|
||||
expect(monthlyAttendanceRate([], 3)).toBe(0);
|
||||
expect(monthlyAttendanceRate([entry('ABSENT', 0)], 3)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyRisk', () => {
|
||||
it('HIGH con 3 o más ausencias consecutivas', () => {
|
||||
expect(classifyRisk(3, 80)).toBe('HIGH');
|
||||
expect(classifyRisk(5, 20)).toBe('HIGH');
|
||||
});
|
||||
|
||||
it('MEDIUM con 2 ausencias consecutivas o asistencia menor al 50%', () => {
|
||||
expect(classifyRisk(2, 90)).toBe('MEDIUM');
|
||||
expect(classifyRisk(1, 40)).toBe('MEDIUM');
|
||||
expect(classifyRisk(2, 40)).toBe('MEDIUM');
|
||||
});
|
||||
|
||||
it('sin riesgo si no cumple ningún umbral', () => {
|
||||
expect(classifyRisk(1, 60)).toBeNull();
|
||||
expect(classifyRisk(0, 60)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user