From 9143d1a79e25c4827f2f784297000951a3e5a09e Mon Sep 17 00:00:00 2001 From: Nic D Date: Wed, 18 Mar 2026 09:24:46 -0400 Subject: [PATCH] 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 --- .../exports/csv-exports.controller.ts | 58 +++++++------------ .../exports/csv-exports.utils.ts | 13 +---- .../exports/export-csv-options.dto.ts | 17 ++---- .../exports/services/csv-builder.service.ts | 2 +- .../exports/services/csv-exports.service.ts | 46 +++++++-------- src/time-and-attendance/shifts/shift.dto.ts | 2 +- 6 files changed, 51 insertions(+), 87 deletions(-) diff --git a/src/time-and-attendance/exports/csv-exports.controller.ts b/src/time-and-attendance/exports/csv-exports.controller.ts index 6064319..5b95a37 100644 --- a/src/time-and-attendance/exports/csv-exports.controller.ts +++ b/src/time-and-attendance/exports/csv-exports.controller.ts @@ -1,56 +1,40 @@ -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 { constructor( private readonly csvService: CsvExportService, private readonly generator: CsvGeneratorService, - + ) { } - @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 { - 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); } - } \ No newline at end of file diff --git a/src/time-and-attendance/exports/csv-exports.utils.ts b/src/time-and-attendance/exports/csv-exports.utils.ts index 3b333e3..c931396 100644 --- a/src/time-and-attendance/exports/csv-exports.utils.ts +++ b/src/time-and-attendance/exports/csv-exports.utils.ts @@ -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 => { diff --git a/src/time-and-attendance/exports/export-csv-options.dto.ts b/src/time-and-attendance/exports/export-csv-options.dto.ts index 218ca2b..aa5698d 100644 --- a/src/time-and-attendance/exports/export-csv-options.dto.ts +++ b/src/time-and-attendance/exports/export-csv-options.dto.ts @@ -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[]; }; \ No newline at end of file diff --git a/src/time-and-attendance/exports/services/csv-builder.service.ts b/src/time-and-attendance/exports/services/csv-builder.service.ts index e0c8ea9..b4fbed7 100644 --- a/src/time-and-attendance/exports/services/csv-builder.service.ts +++ b/src/time-and-attendance/exports/services/csv-builder.service.ts @@ -29,6 +29,6 @@ export class CsvGeneratorService { row.dernier_jour_absence ?? '', ].join(';'); }).join('\n'); - return Buffer.from('\uFEFF' + body, 'utf8'); + return Buffer.from(body); } } \ No newline at end of file diff --git a/src/time-and-attendance/exports/services/csv-exports.service.ts b/src/time-and-attendance/exports/services/csv-exports.service.ts index 9f8291d..c5b7275 100644 --- a/src/time-and-attendance/exports/services/csv-exports.service.ts +++ b/src/time-and-attendance/exports/services/csv-exports.service.ts @@ -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 { - const BILLABLE_SHIFT_TYPES: BillableShiftType[] = []; - - 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 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']); + 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`); + + 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 => { + resolveShiftTypeCode = async (shift_type: ShiftType[]): Promise => { const billableBankCodes = await this.prisma.bankCodes.findMany({ where: { type: { in: shift_type } }, select: { diff --git a/src/time-and-attendance/shifts/shift.dto.ts b/src/time-and-attendance/shifts/shift.dto.ts index aac7a25..045498d 100644 --- a/src/time-and-attendance/shifts/shift.dto.ts +++ b/src/time-and-attendance/shifts/shift.dto.ts @@ -12,4 +12,4 @@ export class ShiftDto { @IsOptional() @IsString() @MaxLength(280) comment?: string; } -export type BillableShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'VACATION' | 'HOLIDAY' | 'SICK' | 'OVERTIME'; \ No newline at end of file +export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'VACATION' | 'HOLIDAY' | 'SICK'; \ No newline at end of file