File

apps/recallassess/recallassess-api/src/api/admin/report/reports/participant-progress/participant-progress-filters.service.ts

Description

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).

Index

Methods

Constructor

constructor(prisma: BNestPrismaService)
Parameters :
Name Type Optional
prisma BNestPrismaService No

Methods

Async getParticipantProgressDetail
getParticipantProgressDetail(participantId: number, courseId: number)

Single enrollment row for participant + course (not cancelled).

Parameters :
Name Type Optional
participantId number No
courseId number No
Async listParticipantProgressSummary
listParticipantProgressSummary(filters: literal type)

Active enrollments for summary table; batched reads for e-learning and BAT assessments.

Parameters :
Name Type Optional
filters literal type No
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),
      };
    });
  }
}

results matching ""

    No results matching ""