import { Injectable } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { computeHours, computePeriod, listPayYear, payYearOfDate, toStringFromDate } from "src/common/utils/date-utils"; import { EmployeePeriodOverviewDto, PayPeriodDto, PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto"; import { Result } from "src/common/errors/result-error.factory"; import { mapPayPeriodToDto } from "src/time-and-attendance/pay-period/pay-periods.mapper"; @Injectable() export class PayPeriodsQueryService { constructor(private readonly prisma: PrismaService) { } async getOverview(pay_period_no: number): Promise> { const period = await this.prisma.payPeriods.findFirst({ where: { pay_period_no }, orderBy: { pay_year: "desc" }, }); if (!period) return { success: false, error: `PAY_PERIOD_NOT_FOUND` }; const overview = await this.buildOverview({ period_start: period.period_start, period_end: period.period_end, payday: period.payday, period_no: period.pay_period_no, pay_year: period.pay_year, label: period.label, }); if (!overview.success) return { success: false, error: 'INVALID_PAY_PERIOD' } return { success: true, data: overview.data } } 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 } } //find crew member associated with supervisor private async resolveCrew(supervisor_id: number, include_subtree: boolean): Promise, string>> { const result: Array<{ id: number; first_name: string; last_name: string; email: string; }> = []; let frontier = await this.prisma.employees.findMany({ where: { supervisor_id: supervisor_id }, select: { id: true, user: { select: { first_name: true, last_name: true, email: true } } }, }); result.push(...frontier.map(emp => ({ id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name, email: emp.user.email }))); if (!include_subtree) return { success: true, data: result }; while (frontier.length) { const parent_ids = frontier.map(emp => emp.id); const next = await this.prisma.employees.findMany({ where: { supervisor_id: { in: parent_ids } }, select: { id: true, user: { select: { first_name: true, last_name: true, email: true } } }, }); if (next.length === 0) break; result.push(...next.map(emp => ({ id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name, email: emp.user.email }))); frontier = next; } return { success: true, data: result }; } //fetchs crew emails async resolveCrewEmails(supervisor_id: number, include_subtree: boolean): Promise, string>> { const crew = await this.resolveCrew(supervisor_id, include_subtree); if (!crew.success) return { success: false, error: crew.error } return { success: true, data: new Set(crew.data.map(crew_member => crew_member.email).filter(Boolean)) } } async getCrewOverview(pay_year: number, period_no: number, email: string, include_subtree: boolean): Promise> { // 1) Search for the period const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no: period_no } }); if (!period) return { success: false, error: 'PAY_PERIOD_NOT_FOUND' } // 2) fetch supervisor const supervisor = await this.prisma.employees.findFirst({ where: { user: { email: email } }, select: { id: true, is_supervisor: true, }, }); if (!supervisor) return { success: false, error: 'EMPLOYEE_NOT_FOUND' } if (!supervisor.is_supervisor) return { success: false, error: 'INVALID_EMPLOYEE' } // 3)fetchs crew members const crew = await this.resolveCrew(supervisor.id, include_subtree); // [{ id, first_name, last_name }] if (!crew.success) return { success: false, error: crew.error } const crew_ids = crew.data.map(c => c.id); // seed names map for employee without data const seed_names = new Map( crew.data.map(crew => [ crew.id, { name: `${crew.first_name} ${crew.last_name}`.trim(), email: crew.email } ] ) ); const overview = await this.buildOverview({ period_no: period.pay_period_no, period_start: period.period_start, period_end: period.period_end, payday: period.payday, pay_year: period.pay_year, label: period.label, }, { filtered_employee_ids: crew_ids, seed_names }) if (!overview.success) return { success: false, error: 'INVALID_PAY_PERIOD' } // 4) overview build return { success: true, data: overview.data } } private async buildOverview( period: { period_start: string | Date; period_end: string | Date; payday: string | Date; period_no: number; pay_year: number; label: string; }, options?: { filtered_employee_ids?: number[]; seed_names?: Map } ): Promise> { const toDateString = (d: Date) => d.toISOString().slice(0, 10); const toMoney = (v: any) => (typeof v === "object" && "toNumber" in v ? v.toNumber() : (v as number)); const start = period.period_start instanceof Date ? period.period_start : new Date(`${period.period_start}T00:00:00.000Z`); const end = period.period_end instanceof Date ? period.period_end : new Date(`${period.period_end}T00:00:00.000Z`); const payd = period.payday instanceof Date ? period.payday : new Date(`${period.payday}T00:00:00.000Z`); //restrictEmployeeIds = filter for shifts and expenses by employees const where_employee = options?.filtered_employee_ids?.length ? { date: { gte: start, lte: end }, timesheet: { employee_id: { in: options.filtered_employee_ids } }, } : { date: { gte: start, lte: end } }; // SHIFTS (filtered by crew) const shifts = await this.prisma.shifts.findMany({ where: where_employee, select: { start_time: true, end_time: true, is_remote: true, timesheet: { select: { id: true, is_approved: true, employee: { select: { id: true, user: { select: { first_name: true, last_name: true, email: true, } }, } }, }, }, bank_code: { select: { categorie: true, type: true } }, }, }); // EXPENSES (filtered by crew) const expenses = await this.prisma.expenses.findMany({ where: where_employee, select: { amount: true, timesheet: { select: { id: true, is_approved: true, employee: { select: { id: true, user: { select: { first_name: true, last_name: true, email: true, } }, } }, } }, bank_code: { select: { categorie: true, modifier: true, type: true } }, }, }); const timesheet_id = shifts[0].timesheet.id ? expenses[0].timesheet.id : null; if (timesheet_id === null) return { success: false, error: 'INVALID_TIMESHEET' }; const by_employee = new Map(); const timesheet = await this.prisma.timesheets.findFirst({ where: { id: timesheet_id }, select: { is_approved: true, id: true, }, }); if (!timesheet) return { success: false, error: 'INVALID_TIMESHEET' }; // seed for employee without data if (options?.seed_names) { for (const [id, { name, email }] of options.seed_names.entries()) { by_employee.set(id, { email, employee_name: name, 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, }, total_hours: 0, expenses: 0, mileage: 0, is_approved: false, }); } } else { const all_employees = await this.prisma.employees.findMany({ include: { user: { select: { first_name: true, last_name: true, email: true }, }, }, }); for (const employee of all_employees) { let is_active = true; if (employee.last_work_day !== null) { is_active = this.checkForInactiveDate(employee.last_work_day) } by_employee.set(employee.id, { email: employee.user.email, employee_name: employee.user.first_name + ' ' + employee.user.last_name, is_active: is_active, regular_hours: 0, other_hours: { evening_hours: 0, emergency_hours: 0, overtime_hours: 0, sick_hours: 0, holiday_hours: 0, vacation_hours: 0, }, total_hours: 0, expenses: 0, mileage: 0, is_approved: timesheet.is_approved, }); } } const ensure = (id: number, name: string, email: string) => { if (!by_employee.has(id)) { by_employee.set(id, { email, employee_name: name, 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, }, total_hours: 0, expenses: 0, mileage: 0, is_approved: timesheet.is_approved, }); } return by_employee.get(id)!; }; for (const shift of shifts) { const employee = shift.timesheet.employee; const name = `${employee.user.first_name} ${employee.user.last_name}`.trim(); const record = ensure(employee.id, name, employee.user.email); const hours = computeHours(shift.start_time, shift.end_time); const type = (shift.bank_code?.type ?? '').toUpperCase(); switch (type) { case "EVENING": record.other_hours.evening_hours += hours; record.total_hours += hours; break; case "EMERGENCY": record.other_hours.emergency_hours += hours; record.total_hours += hours; break; case "OVERTIME": record.other_hours.overtime_hours += hours; record.total_hours += hours; break; case "SICK": record.other_hours.sick_hours += hours; record.total_hours += hours; break; case "HOLIDAY": record.other_hours.holiday_hours += hours; record.total_hours += hours; break; case "VACATION": record.other_hours.vacation_hours += hours; record.total_hours += hours; break; case "REGULAR": record.regular_hours = record.regular_hours += hours; record.total_hours += hours; break; } record.is_approved = record.is_approved && shift.timesheet.is_approved; } for (const expense of expenses) { const exp = expense.timesheet.employee; const name = `${exp.user.first_name} ${exp.user.last_name}`.trim(); const record = ensure(exp.id, name, exp.user.email); const amount = toMoney(expense.amount); record.expenses = Number((record.expenses += amount).toFixed(2)); const type = (expense.bank_code?.type || "").toUpperCase(); const rate = expense.bank_code?.modifier ?? 0; if (type === "MILEAGE" && rate > 0) { record.mileage = Number((record.mileage += Math.round((amount / rate) * 100) / 100).toFixed(2)); } record.is_approved = record.is_approved && expense.timesheet.is_approved; } const employees_overview = Array.from(by_employee.values()).sort((a, b) => a.employee_name.localeCompare(b.employee_name, "fr", { sensitivity: "base" }), ); return { success: true, data: { pay_period_no: period.period_no, pay_year: period.pay_year, payday: toDateString(payd), period_start: toDateString(start), period_end: toDateString(end), label: period.label, employees_overview, } }; } async getSupervisor(email: string) { return this.prisma.employees.findFirst({ where: { user: { email } }, select: { id: true, is_supervisor: true }, }); } async findAll(): Promise> { const currentPayYear = payYearOfDate(new Date()); return { success: true, data: listPayYear(currentPayYear).map(period => ({ pay_period_no: period.period_no, pay_year: period.pay_year, payday: period.payday, period_start: period.period_start, period_end: period.period_end, label: period.label, })) }; } async findOne(period_no: number): Promise> { const row = await this.prisma.payPeriods.findFirst({ where: { pay_period_no: period_no }, orderBy: { pay_year: "desc" }, }); if (!row) return { success: false, error: `PAY_PERIOD_NOT_FOUND` } return { success: true, data: mapPayPeriodToDto(row) }; } async findCurrent(date?: string): Promise> { const iso_day = date ?? new Date().toISOString().slice(0, 10); const pay_period = await this.findByDate(iso_day); if (!pay_period.success) return { success: false, error: 'INVALID_PAY_PERIOD' } return { success: true, data: pay_period.data } } async findOneByYearPeriod(pay_year: number, period_no: number): Promise> { const row = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no: period_no }, }); if (row) return { success: true, data: mapPayPeriodToDto(row) }; // fallback for outside of view periods const period = computePeriod(pay_year, period_no); return { success: true, data: { pay_period_no: period.period_no, pay_year: period.pay_year, period_start: period.period_start, payday: period.payday, period_end: period.period_end, label: period.label } } } //function to cherry pick a Date to find a period async findByDate(date: string): Promise> { const dt = new Date(date); const row = await this.prisma.payPeriods.findFirst({ where: { period_start: { lte: dt }, period_end: { gte: dt } }, }); if (row) return { success: true, data: mapPayPeriodToDto(row) }; //fallback for outwside view periods const pay_year = payYearOfDate(date); const periods = listPayYear(pay_year); const hit = periods.find(period => date >= period.period_start && date <= period.period_end); if (!hit) return { success: false, error: `PAY_PERIOD_NOT_FOUND` } return { success: true, data: { pay_period_no: hit.period_no, pay_year: hit.pay_year, period_start: hit.period_start, period_end: hit.period_end, payday: hit.payday, label: hit.label } } } async getPeriodWindow(pay_year: number, period_no: number) { return this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no: period_no }, select: { period_start: true, period_end: true }, }); } private checkForInactiveDate = (last_work_day: Date) => { const limit = new Date(last_work_day); limit.setDate(limit.getDate() + 14); if (limit >= new Date()) { return true; } return false; } }