targo-backend/src/modules/schedule-presets/services/schedule-presets-apply.service.ts
2025-10-10 09:27:57 -04:00

128 lines
5.5 KiB
TypeScript

import { BadRequestException, ConflictException, Injectable, NotFoundException } from "@nestjs/common";
import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils";
import { PrismaService } from "src/prisma/prisma.service";
import { ApplyResult } from "../types/schedule-presets.types";
import { Prisma, Weekday } from "@prisma/client";
import { WEEKDAY } from "../mappers/schedule-presets.mappers";
@Injectable()
export class SchedulePresetsApplyService {
constructor(
private readonly prisma: PrismaService,
private readonly emailResolver: EmailToIdResolver,
) {}
async applyToTimesheet(
email: string,
preset_name: string,
start_date_iso: string,
): Promise<ApplyResult> {
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 employee_id = await this.emailResolver.findIdByEmail(email);
if(!employee_id) throw new NotFoundException(`Employee with email: ${email} not found`);
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<Weekday, typeof preset.shifts>();
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 };
}
}