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"; @Injectable() export class CsvExportService { constructor(private readonly prisma: PrismaService) { } async collectTransaction( year: number, period_no: number, filters: Filters, // approved: boolean = true ): Promise { //fetch period 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; //fetch company codes from .env const company_codes = this.resolveCompanyCodes(filters.companies); if (company_codes.length === 0) throw new BadRequestException('No company selected'); //Flag types const { shifts: want_shifts, expenses: want_expense, holiday: want_holiday, vacation: want_vacation } = filters.types; if (!want_shifts && !want_expense && !want_holiday && !want_vacation) { throw new BadRequestException(' No export type selected '); } 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]; //Prisma queries const promises: Array> = []; if (want_shifts) { promises.push(this.prisma.shifts.findMany({ where: { date: { gte: start, lte: end }, ...approved_filter, bank_code: { bank_code: { notIn: code_filter } }, timesheet: { employee: { company_code: { in: company_codes } } }, }, select: { date: true, start_time: true, end_time: true, bank_code: { select: { bank_code: true } }, timesheet: { select: { employee: { select: { company_code: true, external_payroll_id: true, user: { select: { first_name: true, last_name: true } }, } }, } }, }, })); } else { promises.push(Promise.resolve([])); } if (want_holiday) { promises.push(this.prisma.shifts.findMany({ where: { date: { gte: start, lte: end }, ...approved_filter, bank_code: { bank_code: holiday_code }, timesheet: { employee: { company_code: { in: company_codes } } }, }, select: { date: true, start_time: true, end_time: true, bank_code: { select: { bank_code: true } }, timesheet: { select: { employee: { select: { company_code: true, external_payroll_id: true, user: { select: { first_name: true, last_name: true } }, } }, } }, }, })); } else { promises.push(Promise.resolve([])); } if (want_vacation) { promises.push(this.prisma.shifts.findMany({ where: { date: { gte: start, lte: end }, ...approved_filter, bank_code: { bank_code: vacation_code }, timesheet: { employee: { company_code: { in: company_codes } } }, }, select: { date: true, start_time: true, end_time: true, bank_code: { select: { bank_code: true } }, timesheet: { select: { employee: { select: { company_code: true, external_payroll_id: true, user: { select: { first_name: true, last_name: true } }, } }, } }, }, })); } else { promises.push(Promise.resolve([])); } if (want_expense) { promises.push(this.prisma.expenses.findMany({ where: { date: { gte: start, lte: end }, ...approved_filter, timesheet: { employee: { company_code: { in: company_codes } } }, }, select: { date: true, amount: true, bank_code: { select: { bank_code: true } }, timesheet: { select: { employee: { select: { company_code: true, external_payroll_id: true, user: { select: { first_name: true, last_name: true } }, } }, } }, }, })); } else { promises.push(Promise.resolve([])); } //array of arrays const [base_shifts, holiday_shifts, vacation_shifts, expenses] = await Promise.all(promises); //mapping const rows: CsvRow[] = []; const map_shifts = (shift: any, is_holiday: boolean) => { const employee = shift.timesheet.employee; const week = this.computeWeekNumber(start, shift.date); return { company_code: employee.company_code, external_payroll_id: employee.external_payroll_id, full_name: `${employee.first_name} ${employee.last_name}`, bank_code: shift.bank_code?.bank_code ?? '', quantity_hours: this.computeHours(shift.start_time, shift.end_time), amount: undefined, week_number: week, pay_date: this.formatDate(end), holiday_date: is_holiday ? this.formatDate(shift.date) : '', } as CsvRow; }; //final mapping of all shifts based filters for (const shift of base_shifts) rows.push(map_shifts(shift, false)); for (const shift of holiday_shifts) rows.push(map_shifts(shift, true)); for (const shift of vacation_shifts) rows.push(map_shifts(shift, false)); for (const expense of expenses) { const employee = expense.timesheet.employee; const week = this.computeWeekNumber(start, expense.date); rows.push({ company_code: employee.company_code, external_payroll_id: employee.external_payroll_id, full_name: `${employee.first_name} ${employee.last_name}`, bank_code: expense.bank_code?.bank_code ?? '', quantity_hours: undefined, amount: Number(expense.amount), week_number: week, pay_date: this.formatDate(end), holiday_date: '', }) } //Final mapping and sorts rows.sort((a, b) => { 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; if (a.bank_code !== b.bank_code) return a.bank_code.localeCompare(b.bank_code); return 0; }); return rows; } resolveHolidayTypeCode = async (holiday: string): Promise => { const holiday_code = await this.prisma.bankCodes.findFirst({ where: { type: holiday }, select: { bank_code: true, }, }); if (!holiday_code) throw new BadRequestException('Missing Holiday bank code'); return holiday_code.bank_code; } resolveVacationTypeCode = async (vacation: string): Promise => { const vacation_code = await this.prisma.bankCodes.findFirst({ where: { type: vacation }, select: { bank_code: true, }, }); if (!vacation_code) throw new BadRequestException('Missing Vacation bank code'); return vacation_code.bank_code; } 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'); 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'); out.push(code_no); } return out; } //csv builder and "mise en page" generateCsv(rows: CsvRow[]): Buffer { const header = [ 'company_code', 'external_payroll_id', 'full_name', 'bank_code', 'quantity_hours', 'amount', 'week_number', 'pay_date', 'holiday_date', ].join(',') + '\n'; const body = rows.map(row => { const full_name = `${String(row.full_name).replace(/"/g, '""')}`; const quantity_hours = (typeof row.quantity_hours === 'number') ? row.quantity_hours.toFixed(2) : ''; const amount = (typeof row.amount === 'number') ? row.amount.toFixed(2) : ''; return [ row.company_code, row.external_payroll_id, full_name, row.bank_code, quantity_hours, amount, row.week_number, row.pay_date, row.holiday_date ?? '', ].join(','); }).join('\n'); 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); return Math.floor(days / 7) + 1; } toUTC(date: Date) { return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); } private formatDate(d: Date): string { return d.toISOString().split('T')[0]; } }