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