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:
@@ -0,0 +1,83 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user