import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common"; import { Weekday, Prisma } from "@prisma/client"; import { DATE_ISO_FORMAT } from "src/time-and-attendance/utils/constants.utils"; import { PrismaService } from "src/prisma/prisma.service"; import { ApplyResult } from "src/time-and-attendance/utils/type.utils"; import { WEEKDAY } from "src/time-and-attendance/utils/mappers.utils"; @Injectable() export class SchedulePresetsApplyService { constructor( private readonly prisma: PrismaService) {} async applyToTimesheet( employee_id: number, preset_name: string, start_date_iso: string, ): Promise { if(!preset_name?.trim()) throw new BadRequestException('A preset_name is required'); if(!DATE_ISO_FORMAT.test(start_date_iso)) throw new BadRequestException('start_date must be of format :YYYY-MM-DD'); const preset = await this.prisma.schedulePresets.findFirst({ where: { employee_id, name: preset_name }, include: { shifts: { orderBy: [{ week_day: 'asc'}, { sort_order: 'asc'}], select: { week_day: true, sort_order: true, start_time: true, end_time: true, is_remote: true, bank_code_id: true, }, }, }, }); if(!preset) throw new NotFoundException(`Preset ${preset} not found`); const start_date = new Date(`${start_date_iso}T00:00:00.000Z`); const timesheet = await this.prisma.timesheets.upsert({ where: { employee_id_start_date: { employee_id, start_date: start_date} }, update: {}, create: { employee_id, start_date: start_date }, select: { id: true }, }); //index shifts by weekday const index_by_day = new Map(); for (const shift of preset.shifts) { const list = index_by_day.get(shift.week_day) ?? []; list.push(shift); index_by_day.set(shift.week_day, list); } const addDays = (date: Date, days: number) => new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + days)); const overlaps = (aStart: Date, aEnd: Date, bStart: Date, bEnd: Date) => aStart.getTime() < bEnd.getTime() && aEnd.getTime() > bStart.getTime(); let created = 0; let skipped = 0; await this.prisma.$transaction(async (tx) => { for(let i = 0; i < 7; i++) { const date = addDays(start_date, i); const week_day = WEEKDAY[date.getUTCDay()]; const shifts = index_by_day.get(week_day) ?? []; if(shifts.length === 0) continue; const existing = await tx.shifts.findMany({ where: { timesheet_id: timesheet.id, date: date }, orderBy: { start_time: 'asc' }, select: { start_time: true, end_time: true, bank_code_id: true, is_remote: true, comment: true, }, }); const payload: Prisma.ShiftsCreateManyInput[] = []; for(const shift of shifts) { if(shift.end_time.getTime() <= shift.start_time.getTime()) { throw new ConflictException(`Invalid time range in preset day: ${week_day}, order: ${shift.sort_order}`); } const conflict = existing.find((existe)=> overlaps( shift.start_time, shift.end_time , existe.start_time, existe.end_time, )); if(conflict) { throw new ConflictException({ error_code: 'SHIFT_OVERLAP_WITH_EXISTING', mesage: `Preset shift overlaps existing shift on ${start_date} + ${i}(week day ${week_day})`, conflict: { existing_start: conflict.start_time.toISOString().slice(11,16), existing_end: conflict.end_time.toISOString().slice(11,16), }, }); } payload.push({ timesheet_id: timesheet.id, date: date, start_time: shift.start_time, end_time: shift.end_time, is_remote: shift.is_remote, comment: null, bank_code_id: shift.bank_code_id, }); } if(payload.length) { const response = await tx.shifts.createMany({ data: payload, skipDuplicates: true }); created += response.count; skipped += payload.length - response.count; } } }); return { timesheet_id: timesheet.id, created, skipped }; } }