targo-backend/src/time-and-attendance/domains/services/overtime.service.ts

104 lines
3.6 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { Prisma, PrismaClient } from '@prisma/client';
import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils';
import { PrismaService } from 'src/prisma/prisma.service';
import { DAILY_LIMIT_HOURS, WEEKLY_LIMIT_HOURS } from 'src/common/utils/constants.utils';
import { Result } from 'src/common/errors/result-error.factory';
type Tx = Prisma.TransactionClient | PrismaClient;
type WeekOvertimeSummary = {
week_start: string;
week_end: string;
week_total_hours: number;
weekly_overtime: number;
daily_overtime_kept: number;
total_overtime: number;
breakdown: Array<{
date: string;
day_hours: number;
day_overtime: number;
daily_kept: number;
running_total_before: number;
}>;
};
@Injectable()
export class OvertimeService {
private INCLUDED_TYPES = ['EMERGENCY', 'EVENING', 'OVERTIME', 'REGULAR'] as const; // included types for weekly overtime calculation
constructor(private prisma: PrismaService) { }
async getWeekOvertimeSummary(timesheet_id: number, date: Date, tx?: Tx): Promise<Result<WeekOvertimeSummary, string>> {
const db = tx ?? this.prisma;
const week_start = getWeekStart(date);
const week_end = getWeekEnd(week_start);
const shifts = await db.shifts.findMany({
where: {
timesheet_id,
date: { gte: week_start, lte: week_end },
bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } },
},
select: { date: true, start_time: true, end_time: true },
orderBy: [{ date: 'asc' }, { start_time: 'asc' }],
});
const day_totals = new Map<string, number>();
for (const shift of shifts) {
const key = shift.date.toISOString().slice(0, 10);
const hours = computeHours(shift.start_time, shift.end_time, 5);
day_totals.set(key, (day_totals.get(key) ?? 0) + hours);
}
const days: string[] = [];
for (let i = 0; i < 7; i++) {
const day = new Date(week_start.getTime() + i * 24 * 60 * 60 * 1000);
days.push(day.toISOString().slice(0, 10));
}
const week_total_hours = [...day_totals.values()].reduce((a, b) => a + b, 0);
const weekly_overtime = Math.max(0, week_total_hours - WEEKLY_LIMIT_HOURS);
let running = 0;
let daily_kept_sum = 0;
const breakdown: WeekOvertimeSummary['breakdown'] = [];
for (const key of days) {
const day_hours = day_totals.get(key) ?? 0;
const day_overtime = Math.max(0, day_hours - DAILY_LIMIT_HOURS);
const cap_before_40 = Math.max(0, WEEKLY_LIMIT_HOURS - running);
const daily_kept = Math.min(day_overtime, cap_before_40);
breakdown.push({
date: key,
day_hours,
day_overtime,
daily_kept,
running_total_before: running,
});
daily_kept_sum += daily_kept;
running += day_hours;
}
const total_overtime = weekly_overtime + daily_kept_sum;
return {
success: true,
data: {
week_start: week_start.toISOString().slice(0, 10),
week_end: week_end.toISOString().slice(0, 10),
week_total_hours,
weekly_overtime,
daily_overtime_kept: daily_kept_sum,
total_overtime,
breakdown,
}
};
}
}