File

apps/recallassess/recallassess-api/src/api/admin/course-module/course-module.service.ts

Extends

BNestBaseModuleService

Index

Methods

Constructor

constructor(prisma: BNestPrismaService, systemLogService: SystemLogService)
Parameters :
Name Type Optional
prisma BNestPrismaService No
systemLogService SystemLogService No

Methods

Async add
add(data: any)

Override add method

Parameters :
Name Type Optional
data any No
Returns : unknown
Async delete
delete(id: number)

Override delete method

Parameters :
Name Type Optional
id number No
Returns : Promise<void>
Private Async getCourseEnrollmentCountForModule
getCourseEnrollmentCountForModule(courseModuleId: number)

Get course enrollment count from LearningGroupParticipant for the course that contains this module. Resolves course_id via CourseModulePage, then counts learningGroupParticipants (same as course enrollment_count logic).

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

Override getDetail to add course_enrollment_count (enrollments in the course that contains this module).

Parameters :
Name Type Optional
id number No
Returns : Promise<DetailResponseDataInterface<any>>
Async getList
getList(paginationOptions: PaginationOptions)

Override getList so that advanced search by course_id is applied via relation (CourseModule has no course_id column; filter via courseModulePages.some.course_id).

Parameters :
Name Type Optional
paginationOptions PaginationOptions No
Returns : Promise<ListResponseDataInterface<any>>
Async getModulesByCourse
getModulesByCourse(courseId: number, excludeFromBat: unknown)

Course modules relevant to a course: linked via CourseModulePage, plus any module already used on AssessmentQuestion for this course (covers courses without pages seeded yet).

Parameters :
Name Type Optional Default value
courseId number No
excludeFromBat unknown No false
Returns : Promise<any[]>
Async save
save(id: number, data: any)

Override save method

Parameters :
Name Type Optional
id number No
data any No
Returns : Promise<SaveResponseDataInterface<any>>
import { SystemLogService } from "@api/shared/services";
import { buildListResponseData } from "@bish-nest/core";
import { BNestBaseModuleService } from "@bish-nest/core/data/module-service/base-module.service";
import { PaginationOptions } from "@bish-nest/core/data/pagination/pagination-options.interface";
import { DetailResponseDataInterface } from "@bish-nest/core/interfaces/detail-response-data.interface";
import { ListResponseDataInterface } from "@bish-nest/core/interfaces/list-response-data.interface";
import { SaveResponseDataInterface } from "@bish-nest/core/interfaces/save-response-data.interface";
import { BNestPrismaService } from "@bish-nest/core/services";
import { Injectable, NotFoundException } from "@nestjs/common";
import { SystemLogEntityType } from "@prisma/client";
import { plainToInstance } from "class-transformer";
import { cloneDeep } from "lodash";

@Injectable()
export class CourseModuleService extends BNestBaseModuleService {
  constructor(
    protected prisma: BNestPrismaService,
    private readonly systemLogService: SystemLogService,
  ) {
    super();
  }
  /**
   * Override getDetail to add course_enrollment_count (enrollments in the course that contains this module).
   */
  async getDetail(id: number): Promise<DetailResponseDataInterface<any>> {
    const detailData = await this.moduleMethods.getDetailData(id);

    const courseEnrollmentCount = await this.getCourseEnrollmentCountForModule(id);
    const detailWithCount = {
      ...detailData,
      course_enrollment_count: courseEnrollmentCount,
      course_has_enrollments: courseEnrollmentCount > 0,
    };

    const moduleCurrentCfg = this.gVars.moduleCurrentCfg;
    const transformedData = plainToInstance(moduleCurrentCfg.detailDto, detailWithCount, {
      excludeExtraneousValues: true,
    });

    return this.moduleMethods.getReturnDataForDetail(transformedData);
  }

  /**
   * Get course enrollment count from LearningGroupParticipant for the course that contains this module.
   * Resolves course_id via CourseModulePage, then counts learningGroupParticipants (same as course enrollment_count logic).
   */
  private async getCourseEnrollmentCountForModule(courseModuleId: number): Promise<number> {
    const page = await this.prisma.client.courseModulePage.findFirst({
      where: { course_module_id: courseModuleId },
      select: { course_id: true },
    });
    const courseId = page?.course_id;
    if (courseId == null) return 0;
    return this.prisma.client.learningGroupParticipant.count({
      where: { course_id: courseId },
    });
  }

  /**
   * Override add method
   */
  async add(data: any) {
    const repo = this.commonMethods.getRepo(this.gVars.moduleCurrentCfg.repoName);

    let dataAdd = plainToInstance(this.gVars.moduleCurrentCfg.addDto, data);
    // Use scalar audit columns to avoid relation create errors
    dataAdd = await this.dbUtilService.addCreatedById(dataAdd);

    const newRow = await repo.create({ data: dataAdd });
    const addResponse = this.moduleMethods.getReturnDataForAdd(newRow);

    // Log the creation
    await this.systemLogService.logInsert(
      SystemLogEntityType.COURSE_MODULE,
      newRow.id,
      newRow as Record<string, unknown>,
    );

    return addResponse;
  }

  /**
   * Override save method
   */
  async save(id: number, data: any): Promise<SaveResponseDataInterface<any>> {
    // Get old data before update
    const oldCourseModule = await this.prisma.client.courseModule.findUnique({
      where: { id },
    });

    if (!oldCourseModule) {
      throw new NotFoundException(`Course module with ID ${id} not found`);
    }

    const repo = this.commonMethods.getRepo(this.gVars.moduleCurrentCfg.repoName);

    let dataSave = plainToInstance(this.gVars.moduleCurrentCfg.saveDto, data);
    // Use scalar audit columns to avoid relation update errors
    dataSave = await this.dbUtilService.addUpdatedById(dataSave);

    await repo.update({ where: { id }, data: dataSave });

    // Get updated data directly from database for logging
    const updatedCourseModule = await this.prisma.client.courseModule.findUnique({
      where: { id },
    });

    if (!updatedCourseModule) {
      throw new NotFoundException(`Course module with ID ${id} not found after update`);
    }

    // Calculate changed fields and log the update
    const changedFields = SystemLogService.calculateChangedFields(
      oldCourseModule as Record<string, unknown>,
      updatedCourseModule as Record<string, unknown>,
    );

    await this.systemLogService.logUpdate(
      SystemLogEntityType.COURSE_MODULE,
      id,
      oldCourseModule as Record<string, unknown>,
      updatedCourseModule as Record<string, unknown>,
      changedFields,
    );

    return await this.getDetail(id);
  }

  /**
   * Override delete method
   */
  async delete(id: number): Promise<void> {
    // Get course module data before deletion
    const courseModule = await this.prisma.client.courseModule.findUnique({
      where: { id },
    });

    if (!courseModule) {
      throw new NotFoundException(`Course module with ID ${id} not found`);
    }

    // Call parent delete method
    await super.delete(id);

    // Log the deletion
    await this.systemLogService.logDelete(
      SystemLogEntityType.COURSE_MODULE,
      id,
      courseModule as Record<string, unknown>,
    );
  }

  /**
   * Override getList so that advanced search by course_id is applied via relation
   * (CourseModule has no course_id column; filter via courseModulePages.some.course_id).
   */
  async getList(paginationOptions: PaginationOptions): Promise<ListResponseDataInterface<any>> {
    const modifiedPagination = cloneDeep(paginationOptions) as PaginationOptions & { where: any };
    // Default list sort to sort_order asc so display order matches intended sequence
    const orderBy = (modifiedPagination as any).orderBy;
    if (!orderBy || orderBy === "created_at") {
      (modifiedPagination as any).orderBy = "sort_order";
      (modifiedPagination as any).sortOrder = "asc";
    }

    const whereRaw = (modifiedPagination as any).where;
    const courseIdEntry =
      whereRaw && typeof whereRaw === "object" && whereRaw["course_id"]
        ? (whereRaw["course_id"] as { operator?: string; value?: string })
        : null;

    if (modifiedPagination.where && courseIdEntry) {
      delete modifiedPagination.where["course_id"];
    }

    let whereCondition = this.moduleMethods.getWhereCondition(modifiedPagination);

    if (courseIdEntry && courseIdEntry.value !== undefined && courseIdEntry.value !== "") {
      const courseIdNum = Number.parseInt(String(courseIdEntry.value), 10);
      if (!Number.isNaN(courseIdNum)) {
        const relationFilter = {
          courseModulePages: {
            some: { course_id: courseIdNum },
          },
        };
        whereCondition = whereCondition || { AND: [] };
        const and = Array.isArray(whereCondition.AND) ? whereCondition.AND : [whereCondition];
        (whereCondition as any).AND = [...and, relationFilter];
      }
    }

    const findParams = this.moduleMethods.getFindParams(modifiedPagination);
    const countParams: Record<string, unknown> = {};
    if (whereCondition) {
      findParams["where"] = whereCondition;
      countParams["where"] = whereCondition;
    }

    const moduleCurrentCfg = this.gVars.moduleCurrentCfg;
    const [rawData, totalCount] = await this.moduleMethods.getListData(
      moduleCurrentCfg.repoName,
      findParams,
      countParams,
    );

    const { page, limit } = paginationOptions;
    let data: any[] = rawData;
    if (moduleCurrentCfg.listDto) {
      data = rawData.map((row: any) =>
        plainToInstance(moduleCurrentCfg.listDto, row, { excludeExtraneousValues: true }),
      );
    }
    return buildListResponseData(data, page || 1, limit || 10, totalCount);
  }

  /**
   * Course modules relevant to a course: linked via CourseModulePage, plus any module
   * already used on AssessmentQuestion for this course (covers courses without pages seeded yet).
   */
  async getModulesByCourse(courseId: number, excludeFromBat = false): Promise<any[]> {
    const pages = await this.prisma.client.courseModulePage.findMany({
      where: { course_id: courseId },
      select: { course_module_id: true },
      distinct: ["course_module_id"],
    });
    const moduleIdsFromPages = pages.map((page) => page.course_module_id);

    const fromAssessmentQuestions = await this.prisma.client.assessmentQuestion.findMany({
      where: {
        course_id: courseId,
        course_module_id: { not: null },
      },
      select: { course_module_id: true },
      distinct: ["course_module_id"],
    });
    const moduleIdsFromQuestions = fromAssessmentQuestions
      .map((q) => q.course_module_id)
      .filter((id): id is number => id != null);

    const moduleIds = [...new Set([...moduleIdsFromPages, ...moduleIdsFromQuestions])];

    if (moduleIds.length === 0) {
      return [];
    }

    const modules = await this.prisma.client.courseModule.findMany({
      where: {
        id: { in: moduleIds },
        exclude_from_bat: excludeFromBat ? false : undefined,
      },
      orderBy: { sort_order: "asc" },
    });

    return modules;
  }
}

results matching ""

    No results matching ""