From 157a7908be6d8157591c279844b61ce7cae141c3 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Fri, 12 Dec 2025 08:25:31 -0500 Subject: [PATCH] refactor(csv): ajusted extract logics --- .../exports/csv-exports.controller.ts | 17 ++++-- .../exports/csv-exports.service.ts | 55 +++++++++++++------ 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/time-and-attendance/exports/csv-exports.controller.ts b/src/time-and-attendance/exports/csv-exports.controller.ts index f963b6d..c2d4f24 100644 --- a/src/time-and-attendance/exports/csv-exports.controller.ts +++ b/src/time-and-attendance/exports/csv-exports.controller.ts @@ -1,8 +1,8 @@ -import { Controller, Get, Header, Param, Query } from "@nestjs/common"; +import { Controller, Get, Param, Query, Res } from "@nestjs/common"; import { CsvExportService } from "./csv-exports.service"; -import { ExportCsvOptionsDto } from "./export-csv-options.dto"; import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators"; import { Modules as ModulesEnum } from ".prisma/client"; +import { Response } from "express"; @Controller('exports') export class CsvExportController { @@ -10,8 +10,6 @@ export class CsvExportController { @Get('csv/:year/:period_no') @ModuleAccessAllowed(ModulesEnum.employee_management) - @Header('Content-Type', 'text/csv; charset=utf-8') - @Header('Content-Disposition', 'attachment; filename="export.csv"') async exportCsv( @Query('approved') approved: boolean, @Query('shifts') shifts: boolean, @@ -22,7 +20,8 @@ export class CsvExportController { @Query('solucom') solucom: boolean, @Param('year') year: number, @Param('period_no') period_no: number, - ): Promise { + @Res() response: Response, + ): Promise { const rows = await this.csvService.collectTransaction( year, period_no, @@ -40,7 +39,13 @@ export class CsvExportController { }, } ); - return this.csvService.generateCsv(rows); + const csv_buffer = await this.csvService.generateCsv(rows); + + response.set({ + 'Content-Type': 'text/csv; charset=utf-8', + 'Content-Disposition': 'attachment; filename="export.csv"', + }); + response.send(csv_buffer); } } \ No newline at end of file diff --git a/src/time-and-attendance/exports/csv-exports.service.ts b/src/time-and-attendance/exports/csv-exports.service.ts index 43a0c7c..ac3161c 100644 --- a/src/time-and-attendance/exports/csv-exports.service.ts +++ b/src/time-and-attendance/exports/csv-exports.service.ts @@ -1,6 +1,7 @@ import { PrismaService } from "src/prisma/prisma.service"; import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { Filters, CsvRow } from "src/time-and-attendance/exports/export-csv-options.dto"; +import { computeHours } from "src/common/utils/date-utils"; @@ -35,9 +36,10 @@ export class CsvExportService { } const approved_filter = filters.approved ? { is_approved: true } : {}; + const holiday_code = await this.resolveHolidayTypeCode('HOLIDAY'); const vacation_code = await this.resolveVacationTypeCode('VACATION'); - const code_filter = [holiday_code, vacation_code]; + const leave_code_filter = [holiday_code, vacation_code]; //Prisma queries const promises: Array> = []; @@ -47,7 +49,7 @@ export class CsvExportService { where: { date: { gte: start, lte: end }, ...approved_filter, - bank_code: { bank_code: { notIn: code_filter } }, + bank_code: { bank_code: { notIn: leave_code_filter } }, timesheet: { employee: { company_code: { in: company_codes } } }, }, select: { @@ -171,9 +173,9 @@ export class CsvExportService { return { company_code: employee.company_code, external_payroll_id: employee.external_payroll_id, - full_name: `${employee.first_name} ${employee.last_name}`, + full_name: `${employee.user.first_name} ${employee.user.last_name}`, bank_code: shift.bank_code?.bank_code ?? '', - quantity_hours: this.computeHours(shift.start_time, shift.end_time), + quantity_hours: computeHours(shift.start_time, shift.end_time), amount: undefined, week_number: week, pay_date: this.formatDate(end), @@ -191,7 +193,7 @@ export class CsvExportService { rows.push({ company_code: employee.company_code, external_payroll_id: employee.external_payroll_id, - full_name: `${employee.first_name} ${employee.last_name}`, + full_name: `${employee.user.first_name} ${employee.user.last_name}`, bank_code: expense.bank_code?.bank_code ?? '', quantity_hours: undefined, amount: Number(expense.amount), @@ -206,13 +208,38 @@ export class CsvExportService { if (a.external_payroll_id !== b.external_payroll_id) { return a.external_payroll_id - b.external_payroll_id; } - const bk_code = String(a.bank_code).localeCompare(String(b.bank_code)); - if (bk_code !== 0) return bk_code; + const bank_code = String(a.bank_code).localeCompare(String(b.bank_code)); + if (bank_code !== 0) return bank_code; if (a.bank_code !== b.bank_code) return a.bank_code.localeCompare(b.bank_code); return 0; }); - return rows; + const consolidated_rows = this.consolidateRowHoursAndAmountByType(rows) + + return consolidated_rows; + } + + consolidateRowHoursAndAmountByType = (rows: CsvRow[]): CsvRow[] => { + type ConsolidateRow = CsvRow & { quantity_hours: number, amount: number }; + const shifts_map = new Map(); + + for (const row of rows) { + const key = `${row.company_code}|${row.external_payroll_id}|${row.full_name}|${row.bank_code}|${row.week_number}`; + const hours = row.quantity_hours ?? 0; + const amounts = row.amount ?? 0; + if (!shifts_map.has(key)) { + shifts_map.set(key, { + ...row, + quantity_hours: hours, + amount: amounts, + }); + } else { + const existing = shifts_map.get(key)!; + existing.quantity_hours += hours; + existing.amount += amounts; + } + } + return Array.from(shifts_map.values()); } resolveHolidayTypeCode = async (holiday: string): Promise => { @@ -241,13 +268,11 @@ export class CsvExportService { resolveCompanyCodes(companies: { targo: boolean; solucom: boolean; }): number[] { const out: number[] = []; if (companies.targo) { - const code_no = parseInt(process.env.TARGO_NO ?? '', 10); - if (!Number.isFinite(code_no) || code_no <= 0) throw new BadRequestException('Invalid Targo code in env'); + const code_no = 271583; out.push(code_no); } if (companies.solucom) { - const code_no = parseInt(process.env.SOLUCOM_NO ?? '', 10); - if (!Number.isFinite(code_no) || code_no <= 0) throw new BadRequestException('Invalid Solucom code in env'); + const code_no = 251585; out.push(code_no); } return out; @@ -286,12 +311,6 @@ export class CsvExportService { return Buffer.from('\uFEFF' + header + body, 'utf8'); } - - private computeHours(start: Date, end: Date): number { - const diffMs = end.getTime() - start.getTime(); - return +(diffMs / 1000 / 3600).toFixed(2); - } - private computeWeekNumber(start: Date, date: Date): number { const dayMS = 86400000; const days = Math.floor((this.toUTC(date).getTime() - this.toUTC(start).getTime()) / dayMS);