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; } }