138 lines
5.2 KiB
TypeScript
138 lines
5.2 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";
|
|
import { UpsertAction } from "src/modules/shared/types/upsert-actions.types";
|
|
|
|
|
|
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);
|
|
return tx.timesheets.upsert({
|
|
where: { employee_id_start_date: { employee_id, start_date: start_of_week } },
|
|
update: {},
|
|
create: { 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, action: UpsertAction) {
|
|
// Switch regular → weekly overtime si > 40h
|
|
await this.overtimeService.transformRegularHoursToWeeklyOvertime(employee_id, date_only, tx);
|
|
const [daily, weekly] = await Promise.all([
|
|
this.overtimeService.getDailyOvertimeHoursForDay(employee_id, date_only),
|
|
this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only),
|
|
]);
|
|
}
|
|
|
|
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,
|
|
}));
|
|
}
|
|
}
|
|
|