targo-backend/src/modules/shifts/helpers/shifts.helpers.ts

140 lines
5.3 KiB
TypeScript

import { BadRequestException, UnprocessableEntityException, NotFoundException, ConflictException } from "@nestjs/common";
import { Prisma, Shifts } from "@prisma/client";
import { UpsertShiftDto } from "../dtos/upsert-shift.dto";
import { DayShiftResponse } from "../types-and-interfaces/shifts-upsert.types";
import { normalizeShiftPayload, overlaps } from "../utils/shifts.utils";
import { weekStartSunday, formatHHmm } from "./shifts-date-time-helpers";
import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils";
import { OvertimeService } from "src/modules/business-logics/services/overtime.service";
export type Tx = Prisma.TransactionClient;
export type Normalized = Awaited<ReturnType<typeof normalizeShiftPayload>>;
export class ShiftsHelpersService {
constructor(
private readonly bankTypeResolver: BankCodesResolver,
private readonly overtimeService: OvertimeService,
) { }
async ensureTimesheet(tx: Tx, employee_id: number, date_only: Date) {
const start_of_week = weekStartSunday(date_only);
console.log('start of week: ', start_of_week);
return tx.timesheets.findUnique({
where: { employee_id_start_date: { employee_id, start_date: start_of_week } },
select: { id: true },
});
}
async normalizeRequired(
raw: UpsertShiftDto['new_shift'] | UpsertShiftDto['old_shift'] | undefined | null,
label: 'old_shift' | 'new_shift' = 'new_shift',
): Promise<Normalized> {
if (!raw) throw new BadRequestException(`${label} is required`);
const norm = await normalizeShiftPayload(raw);
if (norm.end_time.getTime() <= norm.start_time.getTime()) {
throw new UnprocessableEntityException(` ${label}.end_time must be > ${label}.start_time`);
}
return norm;
}
async resolveBankIdRequired(tx: Tx, type: string, label: 'old_shift' | 'new_shift'): Promise<number> {
const found = await this.bankTypeResolver.findByType(type, tx);
const id = found?.id;
if (typeof id !== 'number') {
throw new NotFoundException(`bank code not found for ${label}.type: ${type ?? ''}`);
}
return id;
}
async getDayShifts(tx: Tx, timesheet_id: number, date_only: Date) {
return tx.shifts.findMany({
where: { timesheet_id, date: date_only },
include: { bank_code: true },
orderBy: { start_time: 'asc' },
});
}
async assertNoOverlap(
day_shifts: Array<Shifts & { bank_code: { type: string } | null }>,
new_norm: Normalized | undefined,
exclude_id?: number,
) {
if (!new_norm) return;
const conflicts = day_shifts.filter((s) => {
if (exclude_id && s.id === exclude_id) return false;
return overlaps(
new_norm.start_time.getTime(),
new_norm.end_time.getTime(),
s.start_time.getTime(),
s.end_time.getTime(),
);
});
if (conflicts.length) {
const payload = conflicts.map((s) => ({
start_time: formatHHmm(s.start_time),
end_time: formatHHmm(s.end_time),
type: s.bank_code?.type ?? 'UNKNOWN',
}));
throw new ConflictException({
error_code: 'SHIFT_OVERLAP',
message: 'New shift overlaps with existing shift(s)',
conflicts: payload,
});
}
}
async findExactOldShift(
tx: Tx,
params: {
timesheet_id: number;
date_only: Date;
norm: Normalized;
bank_code_id: number;
comment?: string;
},
) {
const { timesheet_id, date_only, norm, bank_code_id } = params;
return tx.shifts.findFirst({
where: {
timesheet_id,
date: date_only,
start_time: norm.start_time,
end_time: norm.end_time,
is_remote: norm.is_remote,
is_approved: norm.is_approved,
comment: norm.comment ?? null,
bank_code_id,
},
select: { id: true },
});
}
async afterWriteOvertimeAndLog(tx: Tx, employee_id: number, date_only: Date) {
// Switch regular → weekly overtime si > 40h
await this.overtimeService.transformRegularHoursToWeeklyOvertime(employee_id, date_only, tx);
const daily = await this.overtimeService.getDailyOvertimeHours(employee_id, date_only);
const weekly = await this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only);
// const [daily, weekly] = await Promise.all([
// this.overtimeService.getDailyOvertimeHoursForDay(employee_id, date_only),
// this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only),
// ]);
return { daily, weekly };
}
async mapDay(
fresh: Array<Shifts & { bank_code: { type: string } | null }>,
): Promise<DayShiftResponse[]> {
return fresh.map((s) => ({
start_time: formatHHmm(s.start_time),
end_time: formatHHmm(s.end_time),
type: s.bank_code?.type ?? 'UNKNOWN',
is_remote: s.is_remote,
comment: s.comment ?? null,
}));
}
}