import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service"; import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { computeHours } from "src/common/utils/date-utils"; import { applyHolidayRequalifications, applyOvertimeRequalifications, computeWeekNumber, consolidateRowHoursAndAmountByType, formatDate, resolveCompanyCode } from "src/time-and-attendance/exports/csv-exports.utils"; import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service"; import { select_csv_expense_lines, select_csv_shift_lines } from "src/time-and-attendance/utils/selects.utils"; import type { CsvRow, InternalCsvRow, CsvFilters } from "src/time-and-attendance/exports/export-csv-options.dto"; import type { ShiftType } from "src/time-and-attendance/shifts/shift.dto"; @Injectable() export class CsvExportService { constructor( private readonly prisma: PrismaPostgresService, private readonly holiday_service: HolidayService, ) { } /** * Gets all approved shifts and expenses within the year and period number provided, according * to company and types specified in the filters. Any holiday shifts will be adjusted based on * prior 4 weeks of work, where applicable. Vacation and Sick shifts will be split into separate * lines if they are more than one day apart. * * ex: A pay period goes from January 10 to 24. An employee has vacation shifts from * January 11 to * 13 and one shift on Jan 23, then two separate lines will be generated; one from Jan 11 to 13 * and one for Jan 23. * * @param year the year of the requested pay data * @param period_no the period number of the requested pay data * @param filters user-provided input that determines which company and types are exported * @returns The desired filtered data in semi-colon-separated format, grouped and sorted by * employee and by bank codes. */ async collectTransaction( year: number, period_no: number, filters: CsvFilters ): Promise { const PTO_SHIFT_CODES = ['G104', 'G105', 'G109']; // [ HOLIDAY, SICK, VACATION ] const HOLIDAY_SHIFT_CODE = 'G104'; const requestedShiftCodes = await this.resolveShiftTypeCode(filters.shiftTypes); if (requestedShiftCodes.length < 1) throw new BadRequestException('NO_TYPE_SELECTED'); const period = await this.prisma.payPeriods.findFirst({ where: { pay_year: year, pay_period_no: period_no }, select: { period_start: true, period_end: true }, }); if (!period) throw new NotFoundException(`Pay period ${year}-${period_no} not found`); const start = period.period_start; const end = period.period_end; const companyCode = resolveCompanyCode(filters.companyName); const exportedShifts = await this.prisma.shifts.findMany({ where: { date: { gte: start, lte: end }, is_approved: true, timesheet: { employee: { company_code: companyCode } }, bank_code: { bank_code: { in: requestedShiftCodes } }, }, select: select_csv_shift_lines, }); const rows: InternalCsvRow[] = exportedShifts.map(shift => { const employee = shift.timesheet.employee; const week = computeWeekNumber(start, shift.date); const type_transaction = shift.bank_code.bank_code.charAt(0); const code = Number(shift.bank_code.bank_code.slice(1,)); const isPTO = PTO_SHIFT_CODES.includes(shift.bank_code.bank_code); return { timesheet_id: shift.timesheet.id, shift_date: shift.date, compagnie_no: employee.company_code, employee_matricule: employee.external_payroll_id, releve: 0, type_transaction: type_transaction, code: code, quantite_hre: computeHours(shift.start_time, shift.end_time), taux_horaire: '', montant: undefined, semaine_no: week, division_no: undefined, service_no: undefined, departem_no: undefined, sous_departem_no: undefined, date_transaction: formatDate(end), premier_jour_absence: isPTO ? formatDate(shift.date) : '', dernier_jour_absence: isPTO ? formatDate(shift.date) : '', } }); if (filters.includeExpenses) { const exportedExpenses = await this.prisma.expenses.findMany({ where: { date: { gte: start, lte: end }, is_approved: true, timesheet: { employee: { company_code: companyCode } }, }, select: select_csv_expense_lines, }); exportedExpenses.map(expense => { const employee = expense.timesheet.employee; const type_transaction = expense.bank_code.bank_code.charAt(0); const code = Number(expense.bank_code.bank_code.slice(1,)) const week = computeWeekNumber(start, expense.date); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const amount = expense.bank_code.bank_code === 'G503' ? expense.mileage : expense.amount; const adjustedAmount = Number(amount) * expense.bank_code.modifier; rows.push({ timesheet_id: expense.timesheet.id, shift_date: expense.date, compagnie_no: employee.company_code, employee_matricule: employee.external_payroll_id, releve: 0, type_transaction: type_transaction, code: code, quantite_hre: undefined, taux_horaire: undefined, montant: adjustedAmount, semaine_no: week, division_no: undefined, service_no: undefined, departem_no: undefined, sous_departem_no: undefined, date_transaction: formatDate(end), premier_jour_absence: undefined, dernier_jour_absence: undefined, }); }); } // Sort shifts and expenses according to their bank codes rows.sort((a, b) => a.code - b.code || a.employee_matricule - b.employee_matricule ); const holiday_rows = await applyHolidayRequalifications(rows, this.holiday_service, HOLIDAY_SHIFT_CODE); const consolidated_rows = consolidateRowHoursAndAmountByType(holiday_rows); //requalifies regular hours into overtime when needed const requalified_rows = applyOvertimeRequalifications(consolidated_rows); return requalified_rows; } resolveShiftTypeCode = async (shift_type: ShiftType[]): Promise => { const billableBankCodes = await this.prisma.bankCodes.findMany({ where: { type: { in: shift_type } }, select: { bank_code: true, shifts: { select: { date: true, }, }, }, }); if (!billableBankCodes) throw new BadRequestException('Missing Shift bank code'); const shiftCodes: string[] = []; billableBankCodes.map(billableBankCode => shiftCodes.push(billableBankCode.bank_code)); return shiftCodes; } }