targo-backend/src/time-and-attendance/exports/services/csv-exports.service.ts
Nic D 9143d1a79e refactor(exports): streamline and correct response from export main route, modify service logic
- Backend now receives a new filter structure that requires fewer conversions and transformations.

export controller's exportCsv route now follows NestJS conventions to send data buffers to frontend without requiring a decoupling of response handling
2026-03-18 09:24:46 -04:00

176 lines
7.7 KiB
TypeScript

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<CsvRow[]> {
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<string[]> => {
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;
}
}