apps/recallassess/recallassess-api/src/api/admin/report/reports/participant-progress/participant-progress-filters.service.ts
Participant Progress reports — single-participant detail plus cohort summary rows. Shared filter valuelists live in ParticipantScopeFiltersService (used by this report’s routes and other reports).
Methods |
constructor(prisma: BNestPrismaService)
|
||||||
|
Parameters :
|
| Async getParticipantProgressDetail |
getParticipantProgressDetail(participantId: number, courseId: number)
|
|
Single enrollment row for participant + course (not cancelled).
Returns :
Promise<ParticipantProgressDto | null>
|
import { bnestPlainToDto, dateToIsoString, decimalToNumber } from "@bish-nest/core";
import { BNestPrismaService } from "@bish-nest/core/services";
import { Injectable } from "@nestjs/common";
import {
AssessmentType,
ELearningProgressStatus,
ParticipantLearningProgressStatus,
Prisma,
} from "@prisma/client";
import type { ParticipantProgressSummaryRowDto } from "../participant-progress-summary/participant-progress-summary.dto";
import { ParticipantProgressDto } from "./participant-progress.dto";
const ENROLLMENT_STATUS_LABELS: Record<ParticipantLearningProgressStatus, string> = {
[ParticipantLearningProgressStatus.PENDING_INVITE]: "Pending invite",
[ParticipantLearningProgressStatus.INVITED]: "Invited",
[ParticipantLearningProgressStatus.ACCEPTED]: "Accepted",
[ParticipantLearningProgressStatus.PRE_BAT]: "Pre-assessment (BAT)",
[ParticipantLearningProgressStatus.E_LEARNING]: "E-learning",
[ParticipantLearningProgressStatus.POST_BAT]: "Post-assessment (BAT)",
[ParticipantLearningProgressStatus.COMPLETED]: "Completed",
};
const ELEARNING_STATUS_LABELS: Record<ELearningProgressStatus, string> = {
[ELearningProgressStatus.NOT_STARTED]: "Not started",
[ELearningProgressStatus.IN_PROGRESS]: "In progress",
[ELearningProgressStatus.COMPLETED]: "Completed",
};
/**
* Participant Progress reports — single-participant detail plus cohort summary rows. Shared filter valuelists live in
* {@link ParticipantScopeFiltersService} (used by this report’s routes and other reports).
*/
@Injectable()
export class ParticipantProgressFiltersService {
constructor(private readonly prisma: BNestPrismaService) {}
/** Single enrollment row for participant + course (not cancelled). */
async getParticipantProgressDetail(
participantId: number,
courseId: number,
): Promise<ParticipantProgressDto | null> {
const enrollment = await this.prisma.client.learningGroupParticipant.findFirst({
where: {
participant_id: participantId,
course_id: courseId,
cancelled: false,
},
include: {
participant: {
select: {
first_name: true,
last_name: true,
email: true,
company: { select: { name: true } },
},
},
course: {
select: {
title: true,
assessment_id: true,
},
},
learningGroup: { select: { name: true } },
},
});
if (!enrollment) {
return null;
}
const p = enrollment.participant;
const participantName =
`${p.first_name ?? ""} ${p.last_name ?? ""}`.trim() || p.email || `Participant #${participantId}`;
const eLearning = await this.prisma.client.eLearningParticipant.findFirst({
where: {
participant_id: participantId,
course_id: courseId,
learning_group_id: enrollment.learning_group_id,
},
select: {
status: true,
progress_percentage: true,
course_modules_completed: true,
total_course_modules: true,
start_date: true,
complete_date: true,
},
});
let preIpq: number | null = null;
let preAssessmentDate: string | null = null;
let postIpq: number | null = null;
let postAssessmentDate: string | null = null;
const assessmentId = enrollment.course.assessment_id;
if (assessmentId != null) {
const [preRow, postRow] = await Promise.all([
this.prisma.client.assessmentParticipant.findFirst({
where: {
assessment_id: assessmentId,
participant_id: participantId,
assessment_type: AssessmentType.PRE_BAT,
},
select: {
individual_quotient: true,
assessment_completion_date: true,
},
}),
this.prisma.client.assessmentParticipant.findFirst({
where: {
assessment_id: assessmentId,
participant_id: participantId,
assessment_type: AssessmentType.POST_BAT,
},
select: {
individual_quotient: true,
assessment_completion_date: true,
},
}),
]);
preIpq = decimalToNumber(preRow?.individual_quotient);
preAssessmentDate = dateToIsoString(preRow?.assessment_completion_date ?? null);
postIpq = decimalToNumber(postRow?.individual_quotient);
postAssessmentDate = dateToIsoString(postRow?.assessment_completion_date ?? null);
}
const elStatus = eLearning?.status ?? ELearningProgressStatus.NOT_STARTED;
const plain = {
participantName,
participantEmail: p.email ?? null,
companyName: p.company?.name ?? null,
courseTitle: enrollment.course.title,
learningGroupName: enrollment.learningGroup.name,
enrollmentStatus: String(enrollment.status),
enrollmentStatusLabel: ENROLLMENT_STATUS_LABELS[enrollment.status] ?? String(enrollment.status),
overallCompletionPercentage: decimalToNumber(enrollment.completion_percentage),
invitedAt: dateToIsoString(enrollment.invited_at),
acceptedAt: dateToIsoString(enrollment.accepted_at),
preBat: {
enrollmentCompletedAt: dateToIsoString(enrollment.pre_bat_completed_at),
assessmentCompletedAt: preAssessmentDate,
ipq: preIpq,
},
eLearning: {
status: String(elStatus),
statusLabel: ELEARNING_STATUS_LABELS[elStatus] ?? String(elStatus),
progressPercentage: decimalToNumber(eLearning?.progress_percentage),
modulesCompleted: eLearning?.course_modules_completed ?? null,
totalModules: eLearning?.total_course_modules ?? null,
startedAt: dateToIsoString(eLearning?.start_date ?? null),
completedAt: dateToIsoString(eLearning?.complete_date ?? null),
},
knowledgeReview: {
completedAt: dateToIsoString(enrollment.knowledge_review_completed_at),
},
postBat: {
assessmentCompletedAt: postAssessmentDate,
ipq: postIpq,
},
courseCompletedAt: dateToIsoString(enrollment.completed_at),
};
return bnestPlainToDto(plain, ParticipantProgressDto);
}
/** Active enrollments for summary table; batched reads for e-learning and BAT assessments. */
async listParticipantProgressSummary(filters: {
companyId: number;
courseId: number;
learningGroupId?: number;
participantId?: number;
}): Promise<ParticipantProgressSummaryRowDto[]> {
const where: Prisma.LearningGroupParticipantWhereInput = {
cancelled: false,
course_id: filters.courseId,
participant: { company_id: filters.companyId },
...(filters.learningGroupId != null ? { learning_group_id: filters.learningGroupId } : {}),
...(filters.participantId != null ? { participant_id: filters.participantId } : {}),
};
const enrollments = await this.prisma.client.learningGroupParticipant.findMany({
where,
include: {
participant: {
select: {
first_name: true,
last_name: true,
email: true,
},
},
course: {
select: {
title: true,
assessment_id: true,
},
},
learningGroup: { select: { name: true } },
},
orderBy: [{ participant: { last_name: "asc" } }, { participant: { first_name: "asc" } }],
});
if (enrollments.length === 0) {
return [];
}
const orEl: Prisma.ELearningParticipantWhereInput[] = enrollments.map((e) => ({
participant_id: e.participant_id,
course_id: e.course_id,
learning_group_id: e.learning_group_id,
}));
const eLearningRows = await this.prisma.client.eLearningParticipant.findMany({
where: { OR: orEl },
select: {
participant_id: true,
course_id: true,
learning_group_id: true,
status: true,
progress_percentage: true,
},
});
const elKey = (participantId: number, courseId: number, lgId: number) =>
`${participantId}_${courseId}_${lgId}`;
const elMap = new Map<string, (typeof eLearningRows)[0]>();
for (const row of eLearningRows) {
elMap.set(elKey(row.participant_id, row.course_id, row.learning_group_id), row);
}
const assessmentId = enrollments[0].course.assessment_id;
const participantIds = [...new Set(enrollments.map((e) => e.participant_id))];
const preByParticipant = new Map<number, { individual_quotient: Prisma.Decimal | null }>();
const postByParticipant = new Map<number, { individual_quotient: Prisma.Decimal | null }>();
if (assessmentId != null) {
const apRows = await this.prisma.client.assessmentParticipant.findMany({
where: {
assessment_id: assessmentId,
participant_id: { in: participantIds },
assessment_type: { in: [AssessmentType.PRE_BAT, AssessmentType.POST_BAT] },
},
select: {
participant_id: true,
assessment_type: true,
individual_quotient: true,
},
});
for (const r of apRows) {
if (r.assessment_type === AssessmentType.PRE_BAT) {
preByParticipant.set(r.participant_id, r);
} else if (r.assessment_type === AssessmentType.POST_BAT) {
postByParticipant.set(r.participant_id, r);
}
}
}
return enrollments.map((e) => {
const p = e.participant;
const participantName =
`${p.first_name ?? ""} ${p.last_name ?? ""}`.trim() || p.email || `Participant #${e.participant_id}`;
const el = elMap.get(elKey(e.participant_id, e.course_id, e.learning_group_id)) ?? null;
const elStatus = el?.status ?? ELearningProgressStatus.NOT_STARTED;
const preRow = preByParticipant.get(e.participant_id);
const postRow = postByParticipant.get(e.participant_id);
return {
participant_id: e.participant_id,
participant_name: participantName,
participant_email: p.email ?? null,
course_title: e.course.title,
learning_group_name: e.learningGroup.name,
enrollment_status_label: ENROLLMENT_STATUS_LABELS[e.status] ?? e.status,
overall_completion_percentage: decimalToNumber(e.completion_percentage),
pre_bat_ipq: decimalToNumber(preRow?.individual_quotient),
e_learning_status_label: ELEARNING_STATUS_LABELS[elStatus] ?? elStatus,
e_learning_progress_percentage: decimalToNumber(el?.progress_percentage),
post_bat_ipq: decimalToNumber(postRow?.individual_quotient),
course_completed_at: dateToIsoString(e.completed_at),
};
});
}
}