targo-backend/src/time-and-attendance/pay-period/services/pay-periods-build-overview.service.ts

256 lines
10 KiB
TypeScript

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<Result<PayPeriodOverviewDto, string>> {
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<Result<PayPeriodOverviewDto, string>> {
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<number, EmployeePeriodOverviewDto>();
// 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,
});
}