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:
91
apps/backend/src/modules/classes/lib/notify.ts
Normal file
91
apps/backend/src/modules/classes/lib/notify.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { AttendanceStatus } from '@generated/prisma/client';
|
||||
import type { NotifyAbsenceResult, ProblemDetails, Result } from '@gruperly/shared';
|
||||
import { err, ok } from '@gruperly/shared';
|
||||
import { notFoundResourceProblem, validationProblem } from '@/http/problem-builders';
|
||||
import type { UnitOfWork } from '@/lib/prisma';
|
||||
import { type ClassesDb, sessionNotFound } from './helpers';
|
||||
|
||||
type RegisterAbsenceDeps = {
|
||||
db: Pick<ClassesDb, 'classSession' | 'attendee'>;
|
||||
unitOfWork: UnitOfWork;
|
||||
};
|
||||
|
||||
type RegisterAbsenceInput = {
|
||||
attendeeId: string;
|
||||
classSessionId: string;
|
||||
now: Date;
|
||||
releaseHours: number;
|
||||
};
|
||||
|
||||
// Núcleo compartido del aviso de ausencia (profesor autenticado y alumno con token):
|
||||
// - Marca la asistencia como EXCUSED (upsert idempotente).
|
||||
// - Si la anticipación alcanza `releaseHours`, crea/conserva un SlotRelease para liberar el cupo.
|
||||
export async function registerAbsence(
|
||||
deps: RegisterAbsenceDeps,
|
||||
input: RegisterAbsenceInput,
|
||||
): Promise<Result<NotifyAbsenceResult, ProblemDetails>> {
|
||||
const { db, unitOfWork } = deps;
|
||||
const { attendeeId, classSessionId, now, releaseHours } = input;
|
||||
|
||||
const session = await db.classSession.findUnique({ where: { id: classSessionId } });
|
||||
if (!session) {
|
||||
return err(sessionNotFound(classSessionId));
|
||||
}
|
||||
|
||||
const attendee = await db.attendee.findUnique({ where: { id: attendeeId } });
|
||||
if (!attendee) {
|
||||
return err(notFoundResourceProblem('Attendee', attendeeId));
|
||||
}
|
||||
if (attendee.groupId !== session.groupId) {
|
||||
return err(
|
||||
validationProblem({ detail: 'El alumno no pertenece al grupo de esta clase.' }),
|
||||
);
|
||||
}
|
||||
|
||||
const anticipationMs = session.startsAt.getTime() - now.getTime();
|
||||
const slotReleased = anticipationMs >= releaseHours * 60 * 60 * 1000;
|
||||
|
||||
return unitOfWork.executeResult(async (tx) => {
|
||||
await tx.attendance.upsert({
|
||||
where: {
|
||||
attendeeId_classSessionId: {
|
||||
attendeeId,
|
||||
classSessionId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
attendeeId,
|
||||
classSessionId,
|
||||
status: AttendanceStatus.EXCUSED,
|
||||
markedAt: now,
|
||||
},
|
||||
update: {
|
||||
status: AttendanceStatus.EXCUSED,
|
||||
markedAt: now,
|
||||
},
|
||||
});
|
||||
|
||||
if (slotReleased) {
|
||||
await tx.slotRelease.upsert({
|
||||
where: {
|
||||
attendeeId_classSessionId: {
|
||||
attendeeId,
|
||||
classSessionId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
attendeeId,
|
||||
classSessionId,
|
||||
isClaimed: false,
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
}
|
||||
|
||||
return ok({
|
||||
classSessionId,
|
||||
notified: true,
|
||||
slotReleased,
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user