penisularhr / src / modules / ot / ot-record.service.ts
ot-record.service.ts
Raw
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import moment from 'moment';
import { type FindOptionsWhere, Repository } from 'typeorm';

import { type PageDto } from '../../common/dto/page.dto';
import { IncentiveName, OperateType } from '../../constants';
import {
  ActivityRecordSettingNotActiveException,
  ActivityRecordSettingNotFoundException,
  DuplicateOtRecordException,
  EmployeeAlreadyResignedException,
  EmployeeNotActiveException,
  EmployeeNotFoundException,
  OtSettingNotFoundException,
} from '../../exceptions';
import { ActivityRecordSettingService } from '../activity-record/activity-record-setting.service';
import { AuditLogService } from '../audit-log/audit-log.service';
import { type EmployeeEntity } from '../employee/employee.entity';
import { EmployeeService } from '../employee/employee.service';
import { IncentiveRecordService } from '../incentive/incentive-record.service';
import { PublicHolidayService } from '../public-holiday/public-holiday.service';
import { type GetByEmployeeDto } from '../report/dtos/get-report-by-employee.dto';
import { type GetOtDto } from '../report/dtos/get-report-ot.dto';
import { type UserEntity } from '../user/user.entity';
import { type CreateOtRecordDto } from './dtos/create-ot-record.dto';
import { type OtRecordPageOptionsDto } from './dtos/get-ot-record-page.dto';
import { type OtRecordDto } from './dtos/ot-record.dto';
import { type UpdateOtRecordDto } from './dtos/update-ot-record.dto';
import { OtRecordEntity } from './ot-record.entity';
import { OtSettingService } from './ot-settings.service';

@Injectable()
export class OtRecordService {
  constructor(
    @InjectRepository(OtRecordEntity)
    private otRecordReposiory: Repository<OtRecordEntity>,
    @Inject(forwardRef(() => IncentiveRecordService))
    private incentiveRecordService: IncentiveRecordService,
    @Inject(forwardRef(() => ActivityRecordSettingService))
    private activitySettingService: ActivityRecordSettingService,
    @Inject(forwardRef(() => PublicHolidayService))
    private publicHolidayService: PublicHolidayService,
    private otSettingService: OtSettingService,
    private employeeService: EmployeeService,
    private auditLogService: AuditLogService,
  ) {}

  async findOne(
    findData: FindOptionsWhere<OtRecordEntity>,
  ): Promise<OtRecordEntity | null> {
    const queryBuilder = this.otRecordReposiory.createQueryBuilder('otRecord');

    queryBuilder.where(findData);

    queryBuilder
      .leftJoin('otRecord.employee', 'employee')
      .addSelect(['employee.name']);

    return queryBuilder.getOne();
  }

  async findMany(
    pageOptionsDto: OtRecordPageOptionsDto,
  ): Promise<PageDto<OtRecordDto>> {
    const { date, employeeName, activityName, order, id } = pageOptionsDto;

    const queryBuilder = this.otRecordReposiory.createQueryBuilder('otRecord');

    queryBuilder.leftJoin('otRecord.employee', 'employee');
    queryBuilder.addSelect(['employee.name']);

    queryBuilder.leftJoin('otRecord.activitySetting', 'activitySetting');
    queryBuilder.addSelect(['activitySetting.name']);

    if (id) {
      queryBuilder.andWhere('otRecord.id = :id', { id });
    }

    if (date) {
      queryBuilder.andWhere('otRecord.date = :date', { date });
    }

    if (employeeName) {
      queryBuilder.andWhere('employee.name ILIKE :employeeName', {
        employeeName: `%${employeeName}%`,
      });
    }

    if (activityName) {
      queryBuilder.andWhere('activitySetting.name ILIKE :activityName', {
        activityName: `%${activityName}%`,
      });
    }

    queryBuilder.orderBy('otRecord.date', order);

    const [items, pageMetaDto] = await queryBuilder.paginate(pageOptionsDto);

    return items.toPageDto(pageMetaDto);
  }

  async findReportMany(dto: GetOtDto) {
    const { activityName, employeeName, monthFrom, monthTo, order, year } = dto;

    const currentYear = year ?? moment.utc().year();

    const queryBuilder = this.otRecordReposiory.createQueryBuilder('otRecord');

    queryBuilder
      .leftJoin('otRecord.employee', 'employee')
      .addSelect(['employee.name']);

    queryBuilder.leftJoin('otRecord.activitySetting', 'activitySetting');
    queryBuilder.addSelect(['activitySetting.name']);

    if (employeeName) {
      queryBuilder.andWhere('employee.name = :employeeName', { employeeName });
    }

    if (activityName) {
      queryBuilder.andWhere('activitySetting.name = :activityName', {
        activityName,
      });
    }

    let fromDate: Date | undefined;
    let toDate: Date | undefined;

    if (monthFrom) {
      fromDate = moment
        .utc({
          year: currentYear,
          month: monthFrom - 1,
          date: 1,
        })
        .toDate();

      queryBuilder.andWhere('otRecord.date >= :fromDate', { fromDate });
    }

    if (monthTo) {
      toDate = moment
        .utc({ year: currentYear, month: monthTo - 1, date: 1 })
        .add(1, 'months')
        .toDate();

      queryBuilder.andWhere('otRecord.date < :toDate', { toDate });
    }

    queryBuilder.orderBy('otRecord.date', order);

    const response = await queryBuilder.getMany();

    return response.map((el) => el.toDto());
  }

  // eslint-disable-next-line sonarjs/cognitive-complexity
  async createOtRecord(
    dto: CreateOtRecordDto,
    user: UserEntity,
  ): Promise<OtRecordEntity> {
    const { date, hour, employeeName, activityName, dailyRate } = dto;

    const recordDate = moment.utc(date).startOf('date');

    // 1. Check if employee exist
    const employeeEntity = await this.employeeService.findOne({
      name: employeeName,
    });

    if (!employeeEntity) {
      throw new EmployeeNotFoundException();
    }

    if (!employeeEntity.isActive) {
      throw new EmployeeNotActiveException();
    }

    if (
      employeeEntity.dateResign &&
      recordDate.toDate() > employeeEntity.dateResign
    ) {
      throw new EmployeeAlreadyResignedException();
    }

    // 2. Check if activity exist
    const activitySetting = await this.activitySettingService.findOne({
      name: activityName,
    });

    if (!activitySetting) {
      throw new ActivityRecordSettingNotFoundException();
    }

    if (!activitySetting.isActive) {
      throw new ActivityRecordSettingNotActiveException();
    }

    if (
      activitySetting.activateUntil &&
      recordDate.toDate() > activitySetting.activateUntil
    ) {
      throw new ActivityRecordSettingNotActiveException();
    }

    // 3. Check if current record exist
    const existingRecord = await this.findOne({
      date: recordDate.toDate(),
      employee: { id: employeeEntity.id },
      activitySetting: { id: activitySetting.id },
    });

    if (existingRecord) {
      throw new DuplicateOtRecordException();
    }

    // 4. Check if ot setting exist
    const otSettingEntity = await this.otSettingService.findOne();

    if (!otSettingEntity) {
      throw new OtSettingNotFoundException();
    }

    // 5. Calculate rate
    let ratePer = otSettingEntity.normalRatePer;

    const publicHoliday = await this.publicHolidayService.findOne({
      date: recordDate.toDate(),
    });

    //Set to public holiday rate if true
    if (publicHoliday) {
      ratePer = otSettingEntity.publicHolidayRatePer;
    }

    //Set to sunday rate if true
    if (recordDate.day() === 0) {
      ratePer = otSettingEntity.sundayRatePer;
    }

    // 6. Create Record
    let toCreateRate = employeeEntity.dailyRateAmount;

    if (recordDate.month() === 1) {
      toCreateRate =
        Math.round(
          (employeeEntity.basicSalary * 100) / recordDate.daysInMonth(),
        ) / 100; //If February, then apportion to lesser days
    }

    if (dailyRate && dailyRate > 0) {
      toCreateRate = dailyRate;
    }

    const otRecordEntity = this.otRecordReposiory.create({
      employee: employeeEntity,
      activitySetting,
      dailyRate: toCreateRate,
      hour,
      date: recordDate.toDate(),
      ratePer,
    });
    const createdEntity = await this.otRecordReposiory.save(otRecordEntity);

    await this.auditLogService.create({
      itemId: createdEntity.id,
      newValue: JSON.stringify(createdEntity),
      oldValue: null,
      operateType: OperateType.CREATE,
      summaryChanges: 'CREATE ot record',
      tableName: 'otRecord',
      user,
    });

    await this.incentiveRecordService.createOrUpdateIncentiveByFunction(
      employeeEntity,
      {
        date: recordDate.toDate(),
        incentiveName: IncentiveName.MEAL,
      },
    );

    return otRecordEntity;
  }

  async updateOtRecord(
    otRecordEntity: OtRecordEntity,
    dto: UpdateOtRecordDto,
    user: UserEntity,
  ): Promise<OtRecordEntity> {
    const { dailyRate, hour, remark } = dto;

    const oldValue = JSON.stringify({ ...otRecordEntity });
    let summaryChanges = 'UPDATE ';

    // 1. Check if employee exist
    const employeeEntity = await this.employeeService.findOne({
      name: otRecordEntity.employee.name,
    });

    if (!employeeEntity) {
      throw new EmployeeNotFoundException();
    }

    if (dailyRate !== undefined) {
      summaryChanges +=
        otRecordEntity.dailyRate === dailyRate
          ? ''
          : `Daily Rate: ${otRecordEntity.dailyRate} -> ${dailyRate}, `;
      otRecordEntity.dailyRate = dailyRate;
    }

    if (hour !== undefined) {
      summaryChanges +=
        otRecordEntity.hour === hour
          ? ''
          : `Hour: ${otRecordEntity.hour} -> ${hour}, `;
      otRecordEntity.hour = hour;
    }

    if (remark !== undefined) {
      summaryChanges +=
        otRecordEntity.remark === remark
          ? ''
          : `Remark: ${otRecordEntity.remark} -> ${remark}, `;
      otRecordEntity.remark = remark;
    }

    const updatedEntity = await this.otRecordReposiory.save(otRecordEntity);

    await this.auditLogService.create({
      itemId: updatedEntity.id,
      newValue: JSON.stringify(updatedEntity),
      oldValue,
      operateType: OperateType.UPDATE,
      summaryChanges,
      tableName: 'otRecord',
      user,
    });

    await this.incentiveRecordService.createOrUpdateIncentiveByFunction(
      employeeEntity,
      {
        date: otRecordEntity.date,
        incentiveName: IncentiveName.MEAL,
      },
    );

    return otRecordEntity;
  }

  async deleteOtRecord(
    otRecordEntity: OtRecordEntity,
    user: UserEntity,
  ): Promise<void> {
    const employeeEntity = await this.employeeService.findOne({
      name: otRecordEntity.employee.name,
    });

    if (!employeeEntity) {
      throw new EmployeeNotFoundException();
    }

    await this.auditLogService.create({
      itemId: otRecordEntity.id,
      newValue: null,
      oldValue: JSON.stringify(otRecordEntity),
      operateType: OperateType.DELETE,
      summaryChanges: 'DELETE ot record',
      tableName: 'otRecord',
      user,
    });

    await this.incentiveRecordService.deleteIncentiveRecord(
      employeeEntity,
      {
        date: otRecordEntity.date,
        incentiveName: IncentiveName.MEAL,
      },
      user,
    );

    await this.otRecordReposiory.remove(otRecordEntity);
  }

  async getOtQuantity(date: Date, employee: EmployeeEntity) {
    const queryBuilder = this.otRecordReposiory
      .createQueryBuilder('otRecord')
      .select('SUM(otRecord.hour)', 'quantity')
      .leftJoin('otRecord.employee', 'employee')
      .addSelect(['employee.id'])
      .where('otRecord.date = :date', { date })
      .andWhere('employee.id = :id', { id: employee.id })
      .groupBy('employee.id');

    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const returnData = await queryBuilder.getRawOne();

    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
    return returnData ? (returnData.quantity as unknown as number) : 0;
  }

  async getSummaryOTByEmployee(
    dto: GetByEmployeeDto,
    employee: EmployeeEntity,
  ) {
    const { monthFrom, monthTo, year } = dto;

    const currentYear = year ?? moment.utc().year();

    const queryBuilder = this.otRecordReposiory
      .createQueryBuilder('otRecord')
      .select(
        'SUM(otRecord.hour * otRecord.ratePer * otRecord.dailyRate / 8)',
        'sumAmount',
      )
      .leftJoin('otRecord.employee', 'employee')
      .addSelect('employee.id', 'employeeId')
      .where('employee.id = :id', { id: employee.id });

    if (monthFrom) {
      const fromDate = moment
        .utc({
          year: currentYear,
          month: monthFrom - 1,
          date: 1,
        })
        .toDate();

      queryBuilder.andWhere('otRecord.date >= :fromDate', { fromDate });
    }

    if (monthTo) {
      const toDate = moment
        .utc({ year: currentYear, month: monthTo - 1, date: 1 })
        .add(1, 'months')
        .toDate();

      queryBuilder.andWhere('otRecord.date < :toDate', { toDate });
    }

    queryBuilder.groupBy('employee.id');

    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    let returnData = await queryBuilder.getRawOne();

    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    returnData = returnData
      ? {
          ...returnData,
          // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
          sumAmount: Number(returnData.sumAmount),
        }
      : {
          employeeId: employee.id,
          sumAmount: 0,
        };

    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
    return returnData;
  }
}