diff --git a/src/modules/leave-requests/leave-requests.module.ts b/src/modules/leave-requests/leave-requests.module.ts index 1b52938..8959e30 100644 --- a/src/modules/leave-requests/leave-requests.module.ts +++ b/src/modules/leave-requests/leave-requests.module.ts @@ -7,6 +7,7 @@ import { VacationLeaveRequestsService } from "./services/vacation-leave-requests import { SickLeaveRequestsService } from "./services/sick-leave-requests.service"; import { LeaveRequestsService } from "./services/leave-request.service"; import { ShiftsModule } from "../shifts/shifts.module"; +import { LeaveRequestsUtils } from "./utils/leave-request.util"; @Module({ imports: [BusinessLogicsModule, ShiftsModule], @@ -16,7 +17,8 @@ import { ShiftsModule } from "../shifts/shifts.module"; SickLeaveRequestsService, HolidayLeaveRequestsService, LeaveRequestsService, - PrismaService + PrismaService, + LeaveRequestsUtils, ], exports: [ LeaveRequestsService, diff --git a/src/modules/leave-requests/services/holiday-leave-requests.service.ts b/src/modules/leave-requests/services/holiday-leave-requests.service.ts index 580a190..025833c 100644 --- a/src/modules/leave-requests/services/holiday-leave-requests.service.ts +++ b/src/modules/leave-requests/services/holiday-leave-requests.service.ts @@ -1,40 +1,27 @@ -import { LeaveRequestsService, normalizeDates, toDateOnly } from './leave-request.service'; import { UpsertLeaveRequestDto, UpsertResult } from '../dtos/upsert-leave-request.dto'; import { LeaveRequestViewDto } from '../dtos/leave-request-view.dto'; -import { BadRequestException, Inject, Injectable, forwardRef } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { LeaveApprovalStatus, LeaveTypes } from '@prisma/client'; import { HolidayService } from 'src/modules/business-logics/services/holiday.service'; import { PrismaService } from 'src/prisma/prisma.service'; import { mapRowToView } from '../mappers/leave-requests.mapper'; import { leaveRequestsSelect } from '../utils/leave-requests.select'; +import { LeaveRequestsUtils, normalizeDates, toDateOnly } from '../utils/leave-request.util'; @Injectable() export class HolidayLeaveRequestsService { constructor( - private readonly holidayService: HolidayService, - @Inject(forwardRef(() => LeaveRequestsService)) private readonly leaveService: LeaveRequestsService, private readonly prisma: PrismaService, + private readonly holidayService: HolidayService, + private readonly leaveUtils: LeaveRequestsUtils, ) {} - //handle distribution to the right service according to the selected action - async handle(dto: UpsertLeaveRequestDto): Promise { - switch (dto.action) { - case 'create': - return this.createHoliday(dto); - case 'update': - return this.leaveService.update(dto, LeaveTypes.HOLIDAY); - case 'delete': - return this.leaveService.delete(dto, LeaveTypes.HOLIDAY); - default: - throw new BadRequestException(`Unknown action: ${dto.action}`); - } - } - - private async createHoliday(dto: UpsertLeaveRequestDto): Promise { + async create(dto: UpsertLeaveRequestDto): Promise { const email = dto.email.trim(); - const employee_id = await this.leaveService.resolveEmployeeIdByEmail(email); - const bank_code = await this.leaveService.resolveBankCodeByType(LeaveTypes.HOLIDAY); + const employee_id = await this.leaveUtils.resolveEmployeeIdByEmail(email); + const bank_code = await this.leaveUtils.resolveBankCodeByType(LeaveTypes.HOLIDAY); + if(!bank_code) throw new NotFoundException(`bank_code not found`); const dates = normalizeDates(dto.dates); if (!dates.length) throw new BadRequestException('Dates array must not be empty'); @@ -74,7 +61,7 @@ export class HolidayLeaveRequestsService { const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); if (row.approval_status === LeaveApprovalStatus.APPROVED) { - await this.leaveService.syncShift(email, employee_id, iso_date, hours,LeaveTypes.HOLIDAY, row.comment); + await this.leaveUtils.syncShift(email, employee_id, iso_date, hours,LeaveTypes.HOLIDAY, row.comment); } created.push({ ...mapRowToView(row), action: 'create' }); diff --git a/src/modules/leave-requests/services/leave-request.service.ts b/src/modules/leave-requests/services/leave-request.service.ts index 0351c38..d7e3239 100644 --- a/src/modules/leave-requests/services/leave-request.service.ts +++ b/src/modules/leave-requests/services/leave-request.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Inject, Injectable, NotFoundException, forwardRef } from "@nestjs/common"; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client"; import { roundToQuarterHour } from "src/common/utils/date-utils"; import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto"; @@ -11,137 +11,58 @@ import { VacationLeaveRequestsService } from "./vacation-leave-requests.service" import { HolidayService } from "src/modules/business-logics/services/holiday.service"; import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service"; import { VacationService } from "src/modules/business-logics/services/vacation.service"; -import { ShiftsCommandService } from "src/modules/shifts/services/shifts-command.service"; import { PrismaService } from "src/prisma/prisma.service"; +import { LeaveRequestsUtils, normalizeDates, toDateOnly, toISODateKey } from "../utils/leave-request.util"; @Injectable() export class LeaveRequestsService { constructor( - private readonly prisma: PrismaService, - private readonly holidayService: HolidayService, - private readonly sickLogic: SickLeaveService, - private readonly vacationLogic: VacationService, - @Inject(forwardRef(() => HolidayLeaveRequestsService)) private readonly holidayLeaveService: HolidayLeaveRequestsService, - @Inject(forwardRef(() => SickLeaveRequestsService)) private readonly sickLeaveService: SickLeaveRequestsService, - private readonly shiftsCommand: ShiftsCommandService, - @Inject(forwardRef(() => VacationLeaveRequestsService)) private readonly vacationLeaveService: VacationLeaveRequestsService, + private readonly prisma: PrismaService, + private readonly holidayLeaveService: HolidayLeaveRequestsService, + private readonly holidayService: HolidayService, + private readonly sickLogic: SickLeaveService, + private readonly sickLeaveService: SickLeaveRequestsService, + private readonly vacationLeaveService: VacationLeaveRequestsService, + private readonly vacationLogic: VacationService, + private readonly leaveUtils: LeaveRequestsUtils, ) {} + //handle distribution to the right service according to the selected type and action async handle(dto: UpsertLeaveRequestDto): Promise { switch (dto.type) { case LeaveTypes.HOLIDAY: - return this.holidayLeaveService.handle(dto); + if( dto.action === 'create'){ + return this.holidayLeaveService.create(dto); + } else if (dto.action === 'update') { + return this.update(dto, LeaveTypes.HOLIDAY); + } else if (dto.action === 'delete'){ + return this.delete(dto, LeaveTypes.HOLIDAY); + } case LeaveTypes.VACATION: - return this.vacationLeaveService.handle(dto); + if( dto.action === 'create'){ + return this.vacationLeaveService.create(dto); + } else if (dto.action === 'update') { + return this.update(dto, LeaveTypes.VACATION); + } else if (dto.action === 'delete'){ + return this.delete(dto, LeaveTypes.VACATION); + } case LeaveTypes.SICK: - return this.sickLeaveService.handle(dto); + if( dto.action === 'create'){ + return this.sickLeaveService.create(dto); + } else if (dto.action === 'update') { + return this.update(dto, LeaveTypes.SICK); + } else if (dto.action === 'delete'){ + return this.delete(dto, LeaveTypes.SICK); + } default: - throw new BadRequestException(`Unsupported leave type: ${dto.type}`); + throw new BadRequestException(`Unsupported leave type: ${dto.type} or action: ${dto.action}`); } } - async resolveEmployeeIdByEmail(email: string): Promise { - const employee = await this.prisma.employees.findFirst({ - where: { user: { email } }, - select: { id: true }, - }); - if (!employee) { - throw new NotFoundException(`Employee with email ${email} not found`); - } - return employee.id; - } - - async resolveBankCodeByType(type: LeaveTypes) { - const bankCode = await this.prisma.bankCodes.findFirst({ - where: { type }, - select: { id: true, bank_code: true, modifier: true }, - }); - if (!bankCode) { - throw new BadRequestException(`Bank code type "${type}" not found`); - } - return bankCode; - } - - async syncShift( - email: string, - employee_id: number, - iso_date: string, - hours: number, - type: LeaveTypes, - comment?: string, - ) { - if (hours <= 0) return; - - const duration_minutes = Math.round(hours * 60); - if (duration_minutes > 8 * 60) { - throw new BadRequestException("Amount of hours cannot exceed 8 hours per day."); - } - - const start_minutes = 8 * 60; - const end_minutes = start_minutes + duration_minutes; - const toHHmm = (total: number) => - `${String(Math.floor(total / 60)).padStart(2, "0")}:${String(total % 60).padStart(2, "0")}`; - - const existing = await this.prisma.shifts.findFirst({ - where: { - date: new Date(iso_date), - bank_code: { type }, - timesheet: { employee_id: employee_id }, - }, - include: { bank_code: true }, - }); - - await this.shiftsCommand.upsertShiftsByDate(email, iso_date, { - old_shift: existing - ? { - start_time: existing.start_time.toISOString().slice(11, 16), - end_time: existing.end_time.toISOString().slice(11, 16), - type: existing.bank_code?.type ?? type, - is_remote: existing.is_remote, - comment: existing.comment ?? undefined, - } - : undefined, - new_shift: { - start_time: toHHmm(start_minutes), - end_time: toHHmm(end_minutes), - is_remote: existing?.is_remote ?? false, - comment: comment ?? existing?.comment ?? "", - type: type, - }, - }); - } - - async removeShift( - email: string, - employee_id: number, - iso_date: string, - type: LeaveTypes, - ) { - const existing = await this.prisma.shifts.findFirst({ - where: { - date: new Date(iso_date), - bank_code: { type }, - timesheet: { employee_id: employee_id }, - }, - include: { bank_code: true }, - }); - if (!existing) return; - - await this.shiftsCommand.upsertShiftsByDate(email, iso_date, { - old_shift: { - start_time: existing.start_time.toISOString().slice(11, 16), - end_time: existing.end_time.toISOString().slice(11, 16), - type: existing.bank_code?.type ?? type, - is_remote: existing.is_remote, - comment: existing.comment ?? undefined, - }, - }); - } - async delete(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise { - const email = dto.email.trim(); - const employee_id = await this.resolveEmployeeIdByEmail(email); - const dates = normalizeDates(dto.dates); + const email = dto.email.trim(); + const dates = normalizeDates(dto.dates); + const employee_id = await this.leaveUtils.resolveEmployeeIdByEmail(email); if (!dates.length) throw new BadRequestException("Dates array must not be empty"); const rows = await this.prisma.leaveRequests.findMany({ @@ -161,7 +82,7 @@ export class LeaveRequestsService { for (const row of rows) { if (row.approval_status === LeaveApprovalStatus.APPROVED) { const iso = toISODateKey(row.date); - await this.removeShift(email, employee_id, iso, type); + await this.leaveUtils.removeShift(email, employee_id, iso, type); } } @@ -174,31 +95,30 @@ export class LeaveRequestsService { } async update(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise { - const email = dto.email.trim(); - const employee_id = await this.resolveEmployeeIdByEmail(email); - const bank_code = await this.resolveBankCodeByType(type); - const modifier = Number(bank_code.modifier ?? 1); - const dates = normalizeDates(dto.dates); + const email = dto.email.trim(); + const employee_id = await this.leaveUtils.resolveEmployeeIdByEmail(email); + const bank_code = await this.leaveUtils.resolveBankCodeByType(type); + if(!bank_code) throw new NotFoundException(`bank_code not found`); + const modifier = Number(bank_code.modifier ?? 1); + const dates = normalizeDates(dto.dates); if (!dates.length) { throw new BadRequestException("Dates array must not be empty"); } const entries = await Promise.all( dates.map(async (iso_date) => { - const date = toDateOnly(iso_date); + const date = toDateOnly(iso_date); const existing = await this.prisma.leaveRequests.findUnique({ where: { leave_per_employee_date: { employee_id: employee_id, - leave_type: type, + leave_type: type, date, }, }, select: leaveRequestsSelect, }); - if (!existing) { - throw new NotFoundException(`No Leave request found for ${iso_date}`); - } + if (!existing) throw new NotFoundException(`No Leave request found for ${iso_date}`); return { iso_date, date, existing }; }), ); @@ -212,7 +132,7 @@ export class LeaveRequestsService { ? Number(firstExisting.requested_hours) : 8; const requested_hours_per_day = dto.requested_hours ?? fallbackRequested; - const reference_date = entries.reduce( + const reference_date = entries.reduce( (latest, entry) => (entry.date > latest ? entry.date : latest), entries[0].date, ); @@ -227,9 +147,9 @@ export class LeaveRequestsService { const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); for (const { iso_date, existing } of entries) { - const previous_status = existing.approval_status; - const payable = Math.min(remaining_payable_hours, daily_payable_cap); - const payable_rounded = roundToQuarterHour(Math.max(0, payable)); + const previous_status = existing.approval_status; + const payable = Math.min(remaining_payable_hours, daily_payable_cap); + const payable_rounded = roundToQuarterHour(Math.max(0, payable)); remaining_payable_hours = roundToQuarterHour( Math.max(0, remaining_payable_hours - payable_rounded), ); @@ -237,30 +157,28 @@ export class LeaveRequestsService { const row = await this.prisma.leaveRequests.update({ where: { id: existing.id }, data: { - comment: dto.comment ?? existing.comment, + comment: dto.comment ?? existing.comment, requested_hours: requested_hours_per_day, - payable_hours: payable_rounded, - bank_code_id: bank_code.id, + payable_hours: payable_rounded, + bank_code_id: bank_code.id, approval_status: dto.approval_status ?? existing.approval_status, }, select: leaveRequestsSelect, }); const was_approved = previous_status === LeaveApprovalStatus.APPROVED; - const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED; - const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); + const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED; + const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); if (!was_approved && is_approved) { - await this.syncShift(email, employee_id, iso_date, hours, type, row.comment); + await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); } else if (was_approved && !is_approved) { - await this.removeShift(email, employee_id, iso_date, type); + await this.leaveUtils.removeShift(email, employee_id, iso_date, type); } else if (was_approved && is_approved) { - await this.syncShift(email, employee_id, iso_date, hours, type, row.comment); + await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); } - updated.push({ ...mapRowToView(row), action: "update" }); } - return { action: "update", leave_requests: updated }; } @@ -296,44 +214,30 @@ export class LeaveRequestsService { const row = await this.prisma.leaveRequests.update({ where: { id: existing.id }, data: { - comment: dto.comment ?? existing.comment, requested_hours, - payable_hours: payable, - bank_code_id: bank_code.id, + comment: dto.comment ?? existing.comment, + payable_hours: payable, + bank_code_id: bank_code.id, approval_status: dto.approval_status ?? existing.approval_status, }, select: leaveRequestsSelect, }); const was_approved = previous_status === LeaveApprovalStatus.APPROVED; - const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED; - const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); + const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED; + const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); if (!was_approved && is_approved) { - await this.syncShift(email, employee_id, iso_date, hours, type, row.comment); + await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); } else if (was_approved && !is_approved) { - await this.removeShift(email, employee_id, iso_date, type); + await this.leaveUtils.removeShift(email, employee_id, iso_date, type); } else if (was_approved && is_approved) { - await this.syncShift(email, employee_id, iso_date, hours, type, row.comment); + await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); } - updated.push({ ...mapRowToView(row), action: "update" }); } - return { action: "update", leave_requests: updated }; } } -export const toDateOnly = (iso: string): Date => { - const date = new Date(iso); - if (Number.isNaN(date.getTime())) { - throw new BadRequestException(`Invalid date: ${iso}`); - } - date.setHours(0, 0, 0, 0); - return date; -}; -export const toISODateKey = (date: Date): string => date.toISOString().slice(0, 10); - -export const normalizeDates = (dates: string[]): string[] => - Array.from(new Set(dates.map((iso) => toISODateKey(toDateOnly(iso))))); diff --git a/src/modules/leave-requests/services/sick-leave-requests.service.ts b/src/modules/leave-requests/services/sick-leave-requests.service.ts index e489c56..cde2013 100644 --- a/src/modules/leave-requests/services/sick-leave-requests.service.ts +++ b/src/modules/leave-requests/services/sick-leave-requests.service.ts @@ -1,40 +1,28 @@ -import { LeaveRequestsService, normalizeDates, toDateOnly } from "./leave-request.service"; -import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto"; +import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto"; import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; -import { BadRequestException, Inject, Injectable, forwardRef } from "@nestjs/common"; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client"; import { leaveRequestsSelect } from "../utils/leave-requests.select"; import { mapRowToView } from "../mappers/leave-requests.mapper"; import { PrismaService } from "src/prisma/prisma.service"; import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service"; import { roundToQuarterHour } from "src/common/utils/date-utils"; +import { LeaveRequestsUtils, normalizeDates, toDateOnly } from "../utils/leave-request.util"; @Injectable() export class SickLeaveRequestsService { constructor( private readonly prisma: PrismaService, - @Inject(forwardRef(() => LeaveRequestsService)) private readonly leaveService: LeaveRequestsService, private readonly sickService: SickLeaveService, + private readonly leaveUtils: LeaveRequestsUtils, ) {} - //handle distribution to the right service according to the selected action - async handle(dto: UpsertLeaveRequestDto): Promise { - switch (dto.action) { - case "create": - return this.createSick(dto); - case "update": - return this.leaveService.update(dto, LeaveTypes.SICK); - case "delete": - return this.leaveService.delete(dto, LeaveTypes.SICK); - default: - throw new BadRequestException(`Unknown action: ${dto.action}`); - } - } - - private async createSick(dto: UpsertLeaveRequestDto): Promise { + async create(dto: UpsertLeaveRequestDto): Promise { const email = dto.email.trim(); - const employee_id = await this.leaveService.resolveEmployeeIdByEmail(email); - const bank_code = await this.leaveService.resolveBankCodeByType(LeaveTypes.SICK); + const employee_id = await this.leaveUtils.resolveEmployeeIdByEmail(email); + const bank_code = await this.leaveUtils.resolveBankCodeByType(LeaveTypes.SICK); + if(!bank_code) throw new NotFoundException(`bank_code not found`); + const modifier = bank_code.modifier ?? 1; const dates = normalizeDates(dto.dates); if (!dates.length) throw new BadRequestException("Dates array must not be empty"); @@ -94,7 +82,7 @@ export class SickLeaveRequestsService { const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); if (row.approval_status === LeaveApprovalStatus.APPROVED) { - await this.leaveService.syncShift(email, employee_id, iso, hours,LeaveTypes.SICK, row.comment); + await this.leaveUtils.syncShift(email, employee_id, iso, hours,LeaveTypes.SICK, row.comment); } created.push({ ...mapRowToView(row), action: "create" }); diff --git a/src/modules/leave-requests/services/vacation-leave-requests.service.ts b/src/modules/leave-requests/services/vacation-leave-requests.service.ts index 8128862..31d1081 100644 --- a/src/modules/leave-requests/services/vacation-leave-requests.service.ts +++ b/src/modules/leave-requests/services/vacation-leave-requests.service.ts @@ -1,39 +1,29 @@ -import { LeaveRequestsService, normalizeDates, toDateOnly } from "./leave-request.service"; + import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto"; import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; -import { BadRequestException, Inject, Injectable, forwardRef } from "@nestjs/common"; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client"; import { VacationService } from "src/modules/business-logics/services/vacation.service"; import { PrismaService } from "src/prisma/prisma.service"; import { mapRowToView } from "../mappers/leave-requests.mapper"; import { leaveRequestsSelect } from "../utils/leave-requests.select"; import { roundToQuarterHour } from "src/common/utils/date-utils"; +import { LeaveRequestsUtils, normalizeDates, toDateOnly } from "../utils/leave-request.util"; @Injectable() export class VacationLeaveRequestsService { constructor( private readonly prisma: PrismaService, - @Inject(forwardRef(() => LeaveRequestsService)) private readonly leaveService: LeaveRequestsService, private readonly vacationService: VacationService, + private readonly leaveUtils: LeaveRequestsUtils, ) {} - async handle(dto: UpsertLeaveRequestDto): Promise { - switch (dto.action) { - case "create": - return this.createVacation(dto); - case "update": - return this.leaveService.update(dto, LeaveTypes.VACATION); - case "delete": - return this.leaveService.delete(dto, LeaveTypes.VACATION); - default: - throw new BadRequestException(`Unknown action: ${dto.action}`); - } - } - - private async createVacation(dto: UpsertLeaveRequestDto): Promise { + async create(dto: UpsertLeaveRequestDto): Promise { const email = dto.email.trim(); - const employee_id = await this.leaveService.resolveEmployeeIdByEmail(email); - const bank_code = await this.leaveService.resolveBankCodeByType(LeaveTypes.VACATION); + const employee_id = await this.leaveUtils.resolveEmployeeIdByEmail(email); + const bank_code = await this.leaveUtils.resolveBankCodeByType(LeaveTypes.VACATION); + if(!bank_code) throw new NotFoundException(`bank_code not found`); + const modifier = bank_code.modifier ?? 1; const dates = normalizeDates(dto.dates); const requested_hours_per_day = dto.requested_hours ?? 8; @@ -89,7 +79,7 @@ export class VacationLeaveRequestsService { const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); if (row.approval_status === LeaveApprovalStatus.APPROVED) { - await this.leaveService.syncShift(email, employee_id, iso, hours, LeaveTypes.VACATION, row.comment); + await this.leaveUtils.syncShift(email, employee_id, iso, hours, LeaveTypes.VACATION, row.comment); } created.push({ ...mapRowToView(row), action: "create" }); } diff --git a/src/modules/leave-requests/utils/leave-request.util.ts b/src/modules/leave-requests/utils/leave-request.util.ts new file mode 100644 index 0000000..746a568 --- /dev/null +++ b/src/modules/leave-requests/utils/leave-request.util.ts @@ -0,0 +1,124 @@ +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { LeaveTypes } from "@prisma/client"; +import { ShiftsCommandService } from "src/modules/shifts/services/shifts-command.service"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class LeaveRequestsUtils { + constructor( + private readonly prisma: PrismaService, + private readonly shiftsCommand: ShiftsCommandService, + ){} + + async resolveEmployeeIdByEmail(email: string): Promise { + const employee = await this.prisma.employees.findFirst({ + where: { user: { email } }, + select: { id: true }, + }); + if (!employee) { + throw new NotFoundException(`Employee with email ${email} not found`); + } + return employee.id; + } + + async resolveBankCodeByType(type: LeaveTypes) { + const bankCode = await this.prisma.bankCodes.findFirst({ + where: { type }, + select: { id: true, bank_code: true, modifier: true }, + }); + if (!bankCode) throw new BadRequestException(`Bank code type "${type}" not found`); + return bankCode; + } + + async syncShift( + email: string, + employee_id: number, + iso_date: string, + hours: number, + type: LeaveTypes, + comment?: string, + ) { + if (hours <= 0) return; + + const duration_minutes = Math.round(hours * 60); + if (duration_minutes > 8 * 60) { + throw new BadRequestException("Amount of hours cannot exceed 8 hours per day."); + } + + const start_minutes = 8 * 60; + const end_minutes = start_minutes + duration_minutes; + const toHHmm = (total: number) => + `${String(Math.floor(total / 60)).padStart(2, "0")}:${String(total % 60).padStart(2, "0")}`; + + const existing = await this.prisma.shifts.findFirst({ + where: { + date: new Date(iso_date), + bank_code: { type }, + timesheet: { employee_id: employee_id }, + }, + include: { bank_code: true }, + }); + + await this.shiftsCommand.upsertShiftsByDate(email, iso_date, { + old_shift: existing + ? { + start_time: existing.start_time.toISOString().slice(11, 16), + end_time: existing.end_time.toISOString().slice(11, 16), + type: existing.bank_code?.type ?? type, + is_remote: existing.is_remote, + comment: existing.comment ?? undefined, + } + : undefined, + new_shift: { + start_time: toHHmm(start_minutes), + end_time: toHHmm(end_minutes), + is_remote: existing?.is_remote ?? false, + comment: comment ?? existing?.comment ?? "", + type: type, + }, + }); + } + + async removeShift( + email: string, + employee_id: number, + iso_date: string, + type: LeaveTypes, + ) { + const existing = await this.prisma.shifts.findFirst({ + where: { + date: new Date(iso_date), + bank_code: { type }, + timesheet: { employee_id: employee_id }, + }, + include: { bank_code: true }, + }); + if (!existing) return; + + await this.shiftsCommand.upsertShiftsByDate(email, iso_date, { + old_shift: { + start_time: existing.start_time.toISOString().slice(11, 16), + end_time: existing.end_time.toISOString().slice(11, 16), + type: existing.bank_code?.type ?? type, + is_remote: existing.is_remote, + comment: existing.comment ?? undefined, + }, + }); + } + +} + + +export const toDateOnly = (iso: string): Date => { + const date = new Date(iso); + if (Number.isNaN(date.getTime())) { + throw new BadRequestException(`Invalid date: ${iso}`); + } + date.setHours(0, 0, 0, 0); + return date; +}; + +export const toISODateKey = (date: Date): string => date.toISOString().slice(0, 10); + +export const normalizeDates = (dates: string[]): string[] => + Array.from(new Set(dates.map((iso) => toISODateKey(toDateOnly(iso))))); \ No newline at end of file