import { Injectable } from "@nestjs/common"; import { Result } from "src/common/errors/result-error.factory"; import { computeHours, computePeriod, toDateFromString, toStringFromDate } from "src/common/utils/date-utils"; import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service"; import { EmployeePeriodOverviewDto, Overview, PayPeriodOverviewDto } from "src/time-and-attendance/pay-period/dtos/overview-pay-period.dto"; @Injectable() export class GetOverviewService { constructor( private readonly prisma: PrismaPostgresService, ) { } async getOverviewByYearPeriod(pay_year: number, period_no: number): Promise> { const period = computePeriod(pay_year, period_no); const overview = await this.buildOverview({ period_start: period.period_start, period_end: period.period_end, period_no: period.period_no, pay_year: period.pay_year, payday: period.payday, label: period.label, }); if (!overview.success) return { success: false, error: 'INVALID_PAY_PERIOD' } return { success: true, data: overview.data } } async buildOverview(overview: Overview): Promise> { const employee_overviews = await this.prisma.client.employees.findMany({ where: { OR: [ { last_work_day: { gte: toDateFromString(overview.period_start) } }, { last_work_day: null }, ] }, select: { id: true, last_work_day: true, user: { select: { first_name: true, last_name: true, email: true, }, }, supervisor: { select: { user: { select: { first_name: true, last_name: true, email: true, }, }, }, }, timesheet: { where: { start_date: { gte: toDateFromString(overview.period_start), lte: toDateFromString(overview.period_end) }, }, select: { id: true, is_approved: true, start_date: true, shift: { select: { start_time: true, end_time: true, date: true, bank_code: { select: { type: true } }, }, orderBy: { date: 'asc' }, }, expense: { select: { amount: true, bank_code: { select: { type: true, modifier: true } }, }, }, }, }, }, orderBy: { user: { first_name: 'asc' } }, }); const by_employee = new Map(); // seed for employee without data if (overview.options?.seed_names) { for (const [id, { first_name, last_name, email }] of overview.options.seed_names.entries()) { by_employee.set(id, this.createEmployeeSeeds(email, first_name, last_name)); } } else { for (const employee of employee_overviews) { const record = this.createEmployeeSeeds( employee.user.email, employee.user.first_name, employee.user.last_name, employee.supervisor?.user ?? null, ); by_employee.set(employee.id, record); } } const ensure = (id: number, first_name: string, last_name: string, email: string) => { if (!by_employee.has(id)) { by_employee.set(id, this.createEmployeeSeeds(email, first_name, last_name)); } return by_employee.get(id)!; }; for (const employee of employee_overviews) { const record = ensure( employee.id, employee.user.first_name, employee.user.last_name, employee.user.email ); for (const timesheet of employee.timesheet) { let total_weekly_hours: number = 0; let daily_hours: number = 0; let previous_shift_date: string = ''; //totals by types for shifts for (const shift of timesheet.shift) { const hours = computeHours(shift.start_time, shift.end_time); const type = (shift.bank_code?.type ?? '').toUpperCase(); if (previous_shift_date !== toStringFromDate(shift.date)) { previous_shift_date = toStringFromDate(shift.date); daily_hours = 0; } switch (type) { case "EVENING": if (total_weekly_hours + hours <= 40) { record.other_hours.evening_hours += Math.min(hours, 8 - daily_hours); record.other_hours.overtime_hours += Math.max(daily_hours + hours - 8, 0); } else { record.other_hours.evening_hours += Math.max(40 - total_weekly_hours, 0); record.other_hours.overtime_hours += Math.min(total_weekly_hours + hours - 40, hours); } total_weekly_hours += hours; record.total_hours += hours; break; case "EMERGENCY": record.other_hours.emergency_hours += hours; record.total_hours += hours; total_weekly_hours += hours; break; case "SICK": record.other_hours.sick_hours += hours; break; case "HOLIDAY": record.other_hours.holiday_hours += hours; record.total_hours += hours; total_weekly_hours += hours; break; case "VACATION": record.other_hours.vacation_hours += hours; break; case "REGULAR": if (total_weekly_hours + hours <= 40) { record.regular_hours += Math.min(hours, 8 - daily_hours); record.other_hours.overtime_hours += Math.max(daily_hours + hours - 8, 0); } else { record.regular_hours += Math.max(40 - total_weekly_hours, 0); record.other_hours.overtime_hours += Math.min(total_weekly_hours + hours - 40, hours); } total_weekly_hours += hours; record.total_hours += hours; break; } daily_hours += hours; } //totals by type for expenses for (const expense of timesheet.expense) { const amount = Number(expense.amount) record.expenses = Number((record.expenses + amount).toFixed(2)); const type = (expense.bank_code?.type || "").toUpperCase(); const rate = expense.bank_code?.modifier ?? 1; const mileage = amount / rate; if (type === "MILEAGE" && rate > 0) { record.mileage = Number((record.mileage += Math.round(mileage)).toFixed(2)); } } record.weekly_hours.push(total_weekly_hours); } } for (const employee of employee_overviews) { const record = by_employee.get(employee.id); if (!record) continue; const timesheets = employee.timesheet; const has_data = timesheets.some(timesheet => timesheet.shift.length > 0 || timesheet.expense.length > 0); const cutoff_date = new Date(); cutoff_date.setDate(cutoff_date.getDate() + 14); const is_active = employee.last_work_day ? employee.last_work_day.getTime() >= cutoff_date.getTime() : true; record.is_approved = has_data && timesheets.every(timesheet => timesheet.is_approved === true); record.is_active = is_active; } const employees_overview = Array.from(by_employee.values()).sort((a, b) => a.employee_first_name.localeCompare(b.employee_first_name, "fr", { sensitivity: "base" }), ); return { success: true, data: { pay_period_no: overview.period_no, pay_year: overview.pay_year, payday: overview.payday, period_start: (overview.period_start), period_end: overview.period_end, label: overview.label, employees_overview, } }; } createEmployeeSeeds = ( email: string, employee_first_name: string, employee_last_name: string, supervisor: { first_name: string; last_name: string; email: string; } | null = null, ): EmployeePeriodOverviewDto => ({ email, employee_first_name, employee_last_name, supervisor: supervisor ?? null, is_active: true, regular_hours: 0, other_hours: { evening_hours: 0, emergency_hours: 0, overtime_hours: 0, sick_hours: 0, holiday_hours: 0, vacation_hours: 0, }, weekly_hours: [], total_hours: 0, expenses: 0, mileage: 0, is_approved: false, }); }