- 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
176 lines
7.7 KiB
TypeScript
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;
|
|
}
|
|
|
|
}
|