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
This commit is contained in:
Nic D 2026-03-18 09:24:46 -04:00
parent b25558d71c
commit 9143d1a79e
6 changed files with 51 additions and 87 deletions

View File

@ -1,9 +1,10 @@
import { Controller, Get, Param, Query, Res } from "@nestjs/common";
import { Body, Controller, Param, Post, Res, StreamableFile } from "@nestjs/common";
import { CsvExportService } from "./services/csv-exports.service";
import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
import { Modules as ModulesEnum } from "prisma/postgres/generated/prisma/client/postgres/client";
import { Response } from "express";
// import { Response } from "express";
import { CsvGeneratorService } from "src/time-and-attendance/exports/services/csv-builder.service";
import type { CsvFilters } from "src/time-and-attendance/exports/export-csv-options.dto";
@Controller('exports')
export class CsvExportController {
@ -13,44 +14,27 @@ export class CsvExportController {
) { }
@Get('csv/:year/:period_no')
@Post('csv/:year/:period_no')
@ModuleAccessAllowed(ModulesEnum.employee_management)
async exportCsv(
@Query('approved') approved: string,
@Query('shifts') shifts: string,
@Query('expenses') expenses: string,
@Query('holiday') holiday: string,
@Query('vacation') vacation: string,
@Query('targo') targo: string,
@Query('solucom') solucom: string,
@Param('year') year: number,
@Param('period_no') period_no: number,
@Res() response: Response,
): Promise<void> {
const rows = await this.csvService.collectTransaction(
year,
period_no,
{
approved: approved === 'true',
types: {
shifts: shifts === 'true',
expenses: expenses === 'true',
holiday: holiday === 'true',
vacation: vacation === 'true',
},
companies: {
targo: targo === 'true',
solucom: solucom === 'true',
},
}
);
const csv_buffer = this.generator.generateCsv(rows);
@Body() filters: CsvFilters,
// @Res() response: Response,
) {
const rows = await this.csvService.collectTransaction(year, period_no, filters);
const buffer = this.generator.generateCsv(rows);
response.set({
'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': 'attachment; filename="export.csv"',
});
response.send(csv_buffer);
}
return new StreamableFile(buffer, {
type: 'text/csv',
disposition: 'attachment; filename=export.csv'
})
// response.set({
// 'Content-Type': 'text/csv',
// 'Content-Disposition': 'attachment; filename="export.csv"',
// });
// response.send(blob);
}
}

View File

@ -109,17 +109,8 @@ export const applyOvertimeRequalifications = (
return result;
}
export const resolveCompanyCodes = (companies: { targo: boolean; solucom: boolean; }): number[] => {
const out: number[] = [];
if (companies.targo) {
const code_no = 271583;
out.push(code_no);
}
if (companies.solucom) {
const code_no = 271585;
out.push(code_no);
}
return out;
export const resolveCompanyCode = (company: 'Targo' | 'Solucom'): number => {
return company === 'Targo' ? 271583 : 271585;
}
export const computeWeekNumber = (start: Date, date: Date): number => {

View File

@ -1,5 +1,6 @@
import { Transform } from "class-transformer";
import { IsBoolean, IsInt, IsOptional, Max, Min } from "class-validator";
import { ShiftType } from "src/time-and-attendance/shifts/shift.dto";
const toBoolean = (v: any) => {
if (typeof v === 'boolean') return v;
@ -68,16 +69,8 @@ export interface CsvRow {
export type InternalCsvRow = CsvRow & { timesheet_id: number; shift_date: Date; }
export type Filters = {
types: {
shifts: boolean;
expenses: boolean;
holiday: boolean;
vacation: boolean;
};
companies: {
targo: boolean;
solucom: boolean;
};
approved: boolean;
export type CsvFilters = {
companyName: 'Targo' | 'Solucom';
includeExpenses: boolean;
shiftTypes: ShiftType[];
};

View File

@ -29,6 +29,6 @@ export class CsvGeneratorService {
row.dernier_jour_absence ?? '',
].join(';');
}).join('\n');
return Buffer.from('\uFEFF' + body, 'utf8');
return Buffer.from(body);
}
}

View File

@ -1,11 +1,11 @@
import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { Filters, CsvRow, InternalCsvRow } from "src/time-and-attendance/exports/export-csv-options.dto";
import { computeHours } from "src/common/utils/date-utils";
import { applyHolidayRequalifications, applyOvertimeRequalifications, computeWeekNumber, consolidateRowHoursAndAmountByType, formatDate, resolveCompanyCodes } from "src/time-and-attendance/exports/csv-exports.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 { BillableShiftType } from "src/time-and-attendance/shifts/shift.dto";
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()
@ -35,39 +35,35 @@ export class CsvExportService {
async collectTransaction(
year: number,
period_no: number,
filters: Filters
filters: CsvFilters
): Promise<CsvRow[]> {
const BILLABLE_SHIFT_TYPES: BillableShiftType[] = [];
const PTO_SHIFT_CODES = ['G104', 'G105', 'G109']; // [ HOLIDAY, SICK, VACATION ]
const HOLIDAY_SHIFT_CODE = 'G104';
if (filters.types.shifts) BILLABLE_SHIFT_TYPES.push('REGULAR', 'OVERTIME', 'EMERGENCY', 'EVENING', 'SICK');
if (filters.types.holiday) BILLABLE_SHIFT_TYPES.push('HOLIDAY');
if (filters.types.vacation) BILLABLE_SHIFT_TYPES.push('VACATION');
const requestedShiftCodes = await this.resolveShiftTypeCode(filters.shiftTypes);
const BILLABLE_SHIFT_CODES = await this.resolveShiftTypeCode(BILLABLE_SHIFT_TYPES);
const PTO_SHIFT_CODES = await this.resolveShiftTypeCode(['VACATION', 'SICK', 'HOLIDAY']);
const HOLIDAY_SHIFT_CODE = await this.resolveShiftTypeCode(['HOLIDAY']);
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`);
if (!period)
throw new NotFoundException(`Pay period ${year}-${period_no} not found`);
const start = period.period_start;
const end = period.period_end;
const company_codes = resolveCompanyCodes(filters.companies);
if (company_codes.length === 0) throw new BadRequestException('NO_COMPANY_SELECTED');
if (Object.values(filters.types).every(type => type === false))
throw new BadRequestException('NO_TYPE_SELECTED');
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: { in: company_codes } } },
bank_code: { bank_code: { in: BILLABLE_SHIFT_CODES } },
timesheet: { employee: { company_code: companyCode } },
bank_code: { bank_code: { in: requestedShiftCodes } },
},
select: select_csv_shift_lines,
});
@ -101,12 +97,12 @@ export class CsvExportService {
}
});
if (filters.types.expenses) {
if (filters.includeExpenses) {
const exportedExpenses = await this.prisma.expenses.findMany({
where: {
date: { gte: start, lte: end },
is_approved: true,
timesheet: { employee: { company_code: { in: company_codes } } },
timesheet: { employee: { company_code: companyCode } },
},
select: select_csv_expense_lines,
});
@ -149,7 +145,7 @@ export class CsvExportService {
a.employee_matricule - b.employee_matricule
);
const holiday_rows = await applyHolidayRequalifications(rows, this.holiday_service, HOLIDAY_SHIFT_CODE[0]);
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);
@ -157,7 +153,7 @@ export class CsvExportService {
return requalified_rows;
}
resolveShiftTypeCode = async (shift_type: BillableShiftType[]): Promise<string[]> => {
resolveShiftTypeCode = async (shift_type: ShiftType[]): Promise<string[]> => {
const billableBankCodes = await this.prisma.bankCodes.findMany({
where: { type: { in: shift_type } },
select: {

View File

@ -12,4 +12,4 @@ export class ShiftDto {
@IsOptional() @IsString() @MaxLength(280) comment?: string;
}
export type BillableShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'VACATION' | 'HOLIDAY' | 'SICK' | 'OVERTIME';
export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'VACATION' | 'HOLIDAY' | 'SICK';