import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; import { PrismaService } from "src/prisma/prisma.service"; import { shift_select } from "src/time-and-attendance/utils/selects.utils"; import { Injectable } from "@nestjs/common"; import { Normalized } from "src/time-and-attendance/utils/type.utils"; import { Result } from "src/common/errors/result-error.factory"; import { EmployeeTimesheetResolver } from "src/common/mappers/timesheet.mapper"; import { toDateFromString, toStringFromHHmm, toStringFromDate, toDateFromHHmm, overlaps } from "src/common/utils/date-utils"; import { ShiftDto } from "src/time-and-attendance/shifts/dtos/shift-create.dto"; @Injectable() export class ShiftsUpdateDeleteService { constructor( private readonly prisma: PrismaService, private readonly typeResolver: BankCodesResolver, private readonly timesheetResolver: EmployeeTimesheetResolver, ) { } async updateOneOrManyShifts(shifts: ShiftDto[], email: string): Promise> { try { //verify if array is empty or not if (!Array.isArray(shifts) || shifts.length === 0) return { success: false, error: 'No data received' }; //check for overlap inside dto objects const overlap_check = await this.overlapChecker(shifts); if (!overlap_check.success) return overlap_check; //calls the update functions and await the return of successfull result or not const results = await Promise.allSettled(shifts.map(shift => this.updateShift(shift, email))); //return arrays of updated shifts or errors const updated_shifts: ShiftDto[] = []; const errors: string[] = []; //filters results into updated_shifts or errors arrays depending on the return from "allSettled" Promise for (const result of results) { if (result.status === 'fulfilled') { if (result.value.success) { updated_shifts.push(result.value.data); } else { errors.push(result.value.error); } } else { errors.push(result.reason instanceof Error ? result.reason.message : String(result.reason)); } } //verify if shifts were updated and returns an array of errors if needed if (updated_shifts.length === 0) return { success: false, error: errors.join(' | ') || 'No shift updated' }; // returns array of updated shifts return { success: true, data: updated_shifts } } catch (error) { return { success: false, error } } } //_________________________________________________________________ // UPDATE //_________________________________________________________________ async updateShift(dto: ShiftDto, email: string): Promise> { try { const timesheet = await this.timesheetResolver.findTimesheetIdByEmail(email, toDateFromString(dto.date)); if (!timesheet.success) return { success: false, error: timesheet.error } //finds original shift console.log('timesheet id found: ', timesheet.data.id); const original = await this.prisma.shifts.findFirst({ where: { id: dto.id, timesheet_id: timesheet.data.id }, select: shift_select, }); if (!original) return { success: false, error: `SHIFT_NOT_FOUND` }; //transform string format to date and HHmm const normed_shift = await this.normalizeShiftDto(dto); if (!normed_shift.success) return { success: false, error: normed_shift.error } if (normed_shift.data.end_time <= normed_shift.data.start_time) return { success: false, error: `INVALID_SHIFT` }; //finds bank_code_id using the type const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type); if (!bank_code.success) return { success: false, error: bank_code.error }; //updates sent to DB const updated = await this.prisma.shifts.update({ where: { id: original.id }, data: { date: normed_shift.data.date, start_time: normed_shift.data.start_time, end_time: normed_shift.data.end_time, bank_code_id: bank_code.data, comment: dto.comment, is_approved: dto.is_approved, is_remote: dto.is_remote, }, select: shift_select, }); if (!updated) return { success: false, error: 'INVALID_SHIFT' }; // builds an object to return for display in the frontend const shift: ShiftDto = { id: updated.id, timesheet_id: updated.timesheet_id, type: dto.type, date: toStringFromDate(updated.date), start_time: toStringFromHHmm(updated.start_time), end_time: toStringFromHHmm(updated.end_time), is_approved: updated.is_approved, is_remote: updated.is_remote, comment: updated.comment ?? '', } return { success: true, data: shift }; } catch (error) { return { success: false, error: `INVALID_SHIFT` }; } } //_________________________________________________________________ // DELETE //_________________________________________________________________ //finds shifts using shit_ids //blocs deletion if approved async deleteShift(shift_id: number): Promise> { try { return await this.prisma.$transaction(async (tx) => { const shift = await tx.shifts.findUnique({ where: { id: shift_id }, select: { id: true, date: true, timesheet_id: true }, }); if (!shift) return { success: false, error: `SHIFT_NOT_FOUND` }; await tx.shifts.delete({ where: { id: shift_id } }); return { success: true, data: shift.id }; }); } catch (error) { return { success: false, error: `SHIFT_NOT_FOUND` } } } //_________________________________________________________________ // helpers //_________________________________________________________________ private normalizeShiftDto = async (dto: ShiftDto): Promise> => { const bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type); if (!bank_code_id.success) return { success: false, error: 'INVALID_SHIFT' } return { success: true, data: { date: toDateFromString(dto.date), start_time: toDateFromHHmm(dto.start_time), end_time: toDateFromHHmm(dto.end_time), bank_code_id: bank_code_id.data } }; } private overlapChecker = async (shifts: ShiftDto[]): Promise> => { for (let i = 0; i < shifts.length; i++) { for (let j = i + 1; j < shifts.length; j++) { const shift_a = shifts[i]; const shift_b = shifts[j]; if (shift_a.date !== shift_b.date || shift_a.id === shift_b.id) continue; const has_overlap = overlaps( { start: toDateFromHHmm(shift_a.start_time), end: toDateFromHHmm(shift_a.end_time) }, { start: toDateFromHHmm(shift_b.start_time), end: toDateFromHHmm(shift_b.end_time) }, ); if (has_overlap) return { success: false, error: `SHIFT_OVERLAP` }; } } return { success: true, data: undefined } } }