- 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.
92 lines
2.6 KiB
TypeScript
92 lines
2.6 KiB
TypeScript
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,
|
|
});
|
|
});
|
|
}
|