Files
gruperly/apps/backend/src/modules/analytics/features/get-attendee-history/use-case.ts
Jose Selesan 2e184845e1 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.
2026-09-23 16:24:45 -03:00

83 lines
2.4 KiB
TypeScript

import type { PrismaClient } from '@generated/prisma/client';
import type {
AttendanceStatus,
AttendeeHistory,
ProblemDetails,
Result,
} from '@gruperly/shared';
import { err, ok } from '@gruperly/shared';
import { notFoundResourceProblem } from '@/http/problem-builders';
import prisma from '@/lib/prisma';
import { type AnalyticsAccessDb, findGroupForUser } from '../../lib/access';
export const HISTORY_SESSIONS_LIMIT = 20;
type GetAttendeeHistoryDeps = {
db?: AnalyticsAccessDb & Pick<PrismaClient, 'classSession' | 'attendee' | 'attendance'>;
};
export class GetAttendeeHistory {
constructor(private readonly deps: GetAttendeeHistoryDeps = {}) {}
async execute(
groupId: string,
attendeeId: string,
userId: string,
): Promise<Result<AttendeeHistory, ProblemDetails>> {
const db = this.deps.db ?? prisma;
const access = await findGroupForUser(db, groupId, userId);
if (!access.ok) {
return access;
}
const attendee = await db.attendee.findFirst({
where: { id: attendeeId, groupId },
select: { id: true, fullName: true },
});
if (!attendee) {
return err(notFoundResourceProblem('Attendee', attendeeId));
}
const sessions = await db.classSession.findMany({
where: { groupId },
orderBy: { startsAt: 'desc' },
take: HISTORY_SESSIONS_LIMIT,
select: { id: true, startsAt: true },
});
const sessionIds = sessions.map((session) => session.id);
const attendances = sessionIds.length
? await db.attendance.findMany({
where: { attendeeId, classSessionId: { in: sessionIds } },
select: { classSessionId: true, status: true },
})
: [];
const statusBySession = new Map<string, AttendanceStatus>(
attendances.map((record) => [record.classSessionId, record.status]),
);
const orderedSessions = sessions
.slice()
.reverse()
.map((session) => ({
classSessionId: session.id,
startsAt: session.startsAt.toISOString(),
status: statusBySession.get(session.id) ?? null,
}));
const presentCount = attendances.filter((record) => record.status === 'PRESENT').length;
const attendanceRate =
sessions.length > 0 ? Math.round((presentCount / sessions.length) * 100) : 0;
return ok({
attendeeId: attendee.id,
fullName: attendee.fullName,
groupId,
groupName: access.value.name,
attendanceRate,
sessions: orderedSessions,
});
}
}