File

apps/recallassess/recallassess-api/src/api/admin/report-log/services/report-log.service.ts

Extends

BNestBaseModuleService

Index

Properties
Methods

Methods

Async add
add(data: any)

Override add method to remove foreign key fields before passing to Prisma Prisma only accepts relation objects (participant, course, etc.), not raw foreign keys

Parameters :
Name Type Optional
data any No
Returns : Promise<any>
Async getDetail
getDetail(id: number)

Override getDetail to normalize Decimal fields in related entities This prevents DecimalError when Decimal fields are undefined Follows the same pattern as learning-group.service.ts

Parameters :
Name Type Optional
id number No
Async getPdfUrl
getPdfUrl(id: number)

Get PDF URL for a report log Extracts S3 path from the report log title and returns presigned URL

Parameters :
Name Type Optional Description
id number No

Report log ID

Returns : Promise<literal type>

Presigned S3 URL for the PDF

Properties

Protected mediaUtilService
Type : BNestMediaUtilService
Decorators :
@Inject()
import { BNestBaseModuleService } from "@bish-nest/core/data/module-service/base-module.service";
import { BNestMediaUtilService } from "@bish-nest/core/services";
import { DetailResponseDataInterface } from "@bish-nest/core/interfaces/detail-response-data.interface";
import { Inject, Injectable, NotFoundException, UnprocessableEntityException } from "@nestjs/common";
import { plainToInstance } from "class-transformer";

@Injectable()
export class ReportLogService extends BNestBaseModuleService {
  @Inject() protected mediaUtilService!: BNestMediaUtilService;
  /**
   * Override add method to remove foreign key fields before passing to Prisma
   * Prisma only accepts relation objects (participant, course, etc.), not raw foreign keys
   */
  async add(data: any): Promise<any> {
    // Create a copy of data without the foreign key fields
    // The transformed relation fields are already present
    const {
      participant_id,
      course_id,
      assessment_id,
      course_group_id,
      email_log_id,
      ...prismaData
    } = data;

    // Call parent add method with cleaned data
    return super.add(prismaData);
  }

  /**
   * Override getDetail to normalize Decimal fields in related entities
   * This prevents DecimalError when Decimal fields are undefined
   * Follows the same pattern as learning-group.service.ts
   */
  async getDetail(id: number): Promise<DetailResponseDataInterface<unknown>> {
    const moduleCurrentCfg = this.gVars.moduleCurrentCfg;

    // Load report log with all relations
    const reportLog = await this.prisma.client.reportLog.findUnique({
      where: { id },
      include: {
        participant: true,
        course: true,
        assessment: true,
        courseGroup: true,
        emailLog: true,
        userCreatedBy: {
          select: {
            id: true,
            first_name: true,
            last_name: true,
          },
        },
        userUpdatedBy: {
          select: {
            id: true,
            first_name: true,
            last_name: true,
          },
        },
      },
    });

    if (!reportLog) {
      const msg = "The record you are looking for is not found.";
      throw new UnprocessableEntityException(msg);
    }

    // Keep the raw Prisma object shape and normalize Decimal fields
    // Normalize completion_percentage in courseGroup if present
    let normalizedCourseGroup = reportLog.courseGroup;
    if (reportLog.courseGroup) {
      const completionPercentage =
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        (reportLog.courseGroup as any).completion_percentage != null
          ? Number((reportLog.courseGroup as any).completion_percentage)
          : null;

      normalizedCourseGroup = {
        ...reportLog.courseGroup,
        completion_percentage: completionPercentage,
      } as any;

      // Normalize completion_percentage in learningGroupParticipants if present
      if ((reportLog.courseGroup as any).learningGroupParticipants) {
        const normalizedParticipants = (
          (reportLog.courseGroup as any).learningGroupParticipants || []
        ).map((participant: any) => ({
          ...participant,
          completion_percentage:
            participant.completion_percentage != null
              ? Number(participant.completion_percentage)
              : null,
        }));

        normalizedCourseGroup = {
          ...normalizedCourseGroup,
          learningGroupParticipants: normalizedParticipants,
        } as any;
      }
    }

    // Normalize individual_quotient in assessment if present (through assessmentParticipants)
    let normalizedAssessment = reportLog.assessment;
    if (reportLog.assessment && (reportLog.assessment as any).assessmentParticipants) {
      const normalizedAssessmentParticipants = (
        (reportLog.assessment as any).assessmentParticipants || []
      ).map((ap: any) => ({
        ...ap,
        individual_quotient:
          ap.individual_quotient != null ? Number(ap.individual_quotient) : null,
      }));

      normalizedAssessment = {
        ...reportLog.assessment,
        assessmentParticipants: normalizedAssessmentParticipants,
      } as any;
    }

    // Normalize individual_quotient in participant if present (through assessmentParticipants)
    let normalizedParticipant = reportLog.participant;
    if (reportLog.participant && (reportLog.participant as any).assessmentParticipants) {
      const normalizedParticipantAssessmentParticipants = (
        (reportLog.participant as any).assessmentParticipants || []
      ).map((ap: any) => ({
        ...ap,
        individual_quotient:
          ap.individual_quotient != null ? Number(ap.individual_quotient) : null,
      }));

      normalizedParticipant = {
        ...reportLog.participant,
        assessmentParticipants: normalizedParticipantAssessmentParticipants,
      } as any;
    }

    const sanitizedData: Record<string, unknown> = {
      ...reportLog,
      courseGroup: normalizedCourseGroup,
      assessment: normalizedAssessment,
      participant: normalizedParticipant,
    };

    // Transform to DTO for scalar fields (id, name, dates, etc.)
    const data = plainToInstance(moduleCurrentCfg.detailDto, sanitizedData);

    return this.moduleMethods.getReturnDataForDetail(data);
  }

  /**
   * Get PDF URL for a report log
   * Extracts S3 path from the report log title and returns presigned URL
   * @param id Report log ID
   * @returns Presigned S3 URL for the PDF
   */
  async getPdfUrl(id: number): Promise<{ pdfUrl: string }> {
    const reportLog = await this.prisma.client.reportLog.findUnique({
      where: { id },
    });

    if (!reportLog) {
      throw new NotFoundException(`Report log with ID ${id} not found`);
    }

    // Extract S3 filename from title (format: "Report Title [filename.pdf]")
    const titleMatch = reportLog.title.match(/\[([^\]]+\.pdf)\]/);
    if (!titleMatch || !titleMatch[1]) {
      throw new NotFoundException(
        `PDF filename not found in report log title. The report log may not have a PDF associated with it.`,
      );
    }

    const filename = titleMatch[1];

    // Reconstruct the S3 path from the filename
    // Filename format: {participantId}-{pre|post}-bat-{timestamp}.pdf
    // Path uses course_group_id from the report log plus the filename from the title.
    const learningGroupId = reportLog.course_group_id;

    if (!learningGroupId) {
      throw new NotFoundException(
        `Learning group ID not found in report log. Cannot determine S3 path.`,
      );
    }

    // Construct the full S3 path
    const s3Path = `private/report-log/${learningGroupId}/document/${filename}`;

    // Get presigned URL from media service
    const pdfUrl = await this.mediaUtilService.getS3Url(s3Path, false);

    return { pdfUrl };
  }
}

results matching ""

    No results matching ""