From df390b41b6fe46e72e3cd0360ccc455890bb348c Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Thu, 12 Feb 2026 15:14:44 -0500 Subject: [PATCH] fix(csv): started to fix csv structure to match desjardins needs --- .../exports/csv-exports.utils.ts | 22 +- .../exports/export-csv-options.dto.ts | 25 +- .../exports/services/csv-builder.service.ts | 40 ++-- .../exports/services/csv-exports.service.ts | 226 ++++++------------ .../utils/selects.utils.ts | 37 ++- 5 files changed, 159 insertions(+), 191 deletions(-) diff --git a/src/time-and-attendance/exports/csv-exports.utils.ts b/src/time-and-attendance/exports/csv-exports.utils.ts index 32a9399..1a7dbdd 100644 --- a/src/time-and-attendance/exports/csv-exports.utils.ts +++ b/src/time-and-attendance/exports/csv-exports.utils.ts @@ -9,13 +9,13 @@ export const consolidateRowHoursAndAmountByType = (rows: InternalCsvRow[]): Inte const map = new Map(); for (const row of rows) { - const key = `${row.timesheet_id}|${row.bank_code}|${row.week_number}`; + const key = `${row.timesheet_id}|${row.bank_code}|${row.semaine_no}`; if (!map.has(key)) { map.set(key, { ...row }); } else { const existing = map.get(key)!; - existing.quantity_hours = (existing.quantity_hours ?? 0) + (row.quantity_hours ?? 0); - existing.amount = (existing.amount ?? 0) + (row.amount ?? 0); + existing.quantite_hre = (existing.quantite_hre ?? 0) + (row.quantite_hre ?? 0); + existing.montant = (existing.montant ?? 0) + (row.montant ?? 0); } } return Array.from(map.values()); @@ -39,11 +39,11 @@ export const applyHolidayRequalifications = async ( const calculated = await holiday_service.calculateHolidayPay(row.email, new Date(row.holiday_date), 1); if (!calculated.success) { - result.push({ ...row, quantity_hours: 0 }); + result.push({ ...row, quantite_hre: 0 }); continue; } - result.push({ ...row, quantity_hours: calculated.data }); + result.push({ ...row, quantite_hre: calculated.data }); } return result; } @@ -57,7 +57,7 @@ export const applyOvertimeRequalifications = async ( const grouped_rows = new Map(); for (const row of consolidated_rows) { - const key = `${row.timesheet_id}|${row.week_number}`; + const key = `${row.timesheet_id}|${row.semaine_no}`; if (!grouped_rows.has(key)) { grouped_rows.set(key, []); } @@ -76,18 +76,18 @@ export const applyOvertimeRequalifications = async ( const overtime_hours = summary.data.total_overtime; const regular_hours = rows.find(r => r.bank_code === REGULAR); - if (!regular_hours || !regular_hours.quantity_hours) { + if (!regular_hours || !regular_hours.quantite_hre) { result.push(...rows); continue; } - const deducted = Math.min(overtime_hours, regular_hours.quantity_hours); + const deducted = Math.min(overtime_hours, regular_hours.quantite_hre); for (const row of rows) { if (row === regular_hours) { - const remaining = regular_hours.quantity_hours - deducted; + const remaining = regular_hours.quantite_hre - deducted; if (remaining > 0) { - result.push({ ...regular_hours, quantity_hours: remaining }); + result.push({ ...regular_hours, quantite_hre: remaining }); } } else { result.push(row); @@ -97,7 +97,7 @@ export const applyOvertimeRequalifications = async ( result.push({ ...regular_hours, bank_code: OVERTIME, - quantity_hours: deducted, + quantite_hre: deducted, }); } 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 939e228..a9c241d 100644 --- a/src/time-and-attendance/exports/export-csv-options.dto.ts +++ b/src/time-and-attendance/exports/export-csv-options.dto.ts @@ -47,15 +47,22 @@ export class ExportCsvOptionsDto { } export interface CsvRow { - company_code: number; - external_payroll_id: number; - full_name: string; - bank_code: string; - quantity_hours?: number; - amount?: number; - week_number: number; - pay_date: string; - holiday_date?: string; + compagnie_no: number; + employee_matricule: number; + releve:number; + type_transaction:string; + code: number; + quantite_hre?: number; + taux_horaire?:string; + montant?: number; + semaine_no: number; + division_no?: number; + service_no?: number; + departem_no?:number; + sous_departem_no?:number; + date_transaction: string; + premier_jour_absence?: string; + dernier_jour_absence?:string; } export type InternalCsvRow = CsvRow & { timesheet_id: number; shift_date: Date; email: string; } 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 c6b944e..ccc9e17 100644 --- a/src/time-and-attendance/exports/services/csv-builder.service.ts +++ b/src/time-and-attendance/exports/services/csv-builder.service.ts @@ -5,34 +5,28 @@ import { CsvRow } from "src/time-and-attendance/exports/export-csv-options.dto"; export class CsvGeneratorService { //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) : ''; + const quantity_hours = (typeof row.quantite_hre === 'number') ? row.quantite_hre.toFixed(2) : ''; + const amount = (typeof row.montant === 'number') ? row.montant.toFixed(2) : ''; return [ - row.company_code, - row.external_payroll_id, - full_name, - row.bank_code, + row.compagnie_no, + row.employee_matricule, + row.releve ?? '', + row.type_transaction, + row.code, quantity_hours, + row.taux_horaire ?? '', amount, - row.week_number, - row.pay_date, - row.holiday_date ?? '', + row.semaine_no, + row.division_no ?? '', + row.service_no ?? '', + row.departem_no ?? '', + row.sous_departem_no ?? '', + row.date_transaction, + row.premier_jour_absence ?? '', + row.dernier_jour_absence ?? '', ].join(';'); }).join('\n'); - return Buffer.from('\uFEFF' + header + body, 'utf8'); + return Buffer.from('\uFEFF' + body, 'utf8'); } } \ 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 449dc84..43d8851 100644 --- a/src/time-and-attendance/exports/services/csv-exports.service.ts +++ b/src/time-and-attendance/exports/services/csv-exports.service.ts @@ -5,6 +5,8 @@ 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 { OvertimeService } from "src/time-and-attendance/domains/services/overtime.service"; 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"; + @Injectable() export class CsvExportService { @@ -40,11 +42,13 @@ export class CsvExportService { 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 leave_code_filter = [holiday_code, vacation_code]; + const holiday_code = await this.resolveShiftTypeCode('HOLIDAY'); + const vacation_code = await this.resolveShiftTypeCode('VACATION'); + const regular_code = await this.resolveShiftTypeCode('REGULAR'); + const evening_code = await this.resolveShiftTypeCode('EVENING'); + const emergency_code = await this.resolveShiftTypeCode('EMERGENCY'); + const sick_code = await this.resolveShiftTypeCode('SICK'); + const shift_code_filter = [regular_code, evening_code, emergency_code, sick_code]; //Prisma queries const promises: Array> = []; @@ -53,28 +57,11 @@ export class CsvExportService { promises.push(this.prisma.shifts.findMany({ where: { date: { gte: start, lte: end }, - ...approved_filter, - bank_code: { bank_code: { notIn: leave_code_filter } }, + is_approved: true, timesheet: { employee: { company_code: { in: company_codes } } }, + bank_code: { bank_code: { in: shift_code_filter } }, }, - select: { - date: true, - start_time: true, - end_time: true, - bank_code: { select: { bank_code: true } }, - timesheet: { - select: { - id: true, - employee: { - select: { - company_code: true, - external_payroll_id: true, - user: { select: { first_name: true, last_name: true, email: true } }, - } - }, - } - }, - }, + select: select_csv_shift_lines, })); } else { promises.push(Promise.resolve([])); @@ -84,28 +71,11 @@ export class CsvExportService { promises.push(this.prisma.shifts.findMany({ where: { date: { gte: start, lte: end }, - ...approved_filter, - bank_code: { bank_code: holiday_code }, + is_approved: true, timesheet: { employee: { company_code: { in: company_codes } } }, + bank_code: { bank_code: holiday_code }, }, - select: { - date: true, - start_time: true, - end_time: true, - bank_code: { select: { bank_code: true } }, - timesheet: { - select: { - id: true, - employee: { - select: { - company_code: true, - external_payroll_id: true, - user: { select: { first_name: true, last_name: true, email: true } }, - } - }, - } - }, - }, + select: select_csv_shift_lines, })); } else { promises.push(Promise.resolve([])); @@ -115,57 +85,11 @@ export class CsvExportService { promises.push(this.prisma.shifts.findMany({ where: { date: { gte: start, lte: end }, - ...approved_filter, + is_approved: true, 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: { - id: true, - employee: { - select: { - company_code: true, - external_payroll_id: true, - user: { select: { first_name: true, last_name: true, email: 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: { - id: true, - employee: { - select: { - company_code: true, - external_payroll_id: true, - user: { select: { first_name: true, last_name: true, email: true } }, - } - }, - } - }, - }, + select: select_csv_shift_lines, })); } else { promises.push(Promise.resolve([])); @@ -173,55 +97,80 @@ export class CsvExportService { //array of arrays const [base_shifts, holiday_shifts, vacation_shifts, expenses] = await Promise.all(promises); - //mapping - const rows: InternalCsvRow[] = []; - const map_shifts = (shift: any, is_holiday: boolean): InternalCsvRow => { - const employee = shift.timesheet.employee; - const week = computeWeekNumber(start, shift.date); + const rows: CsvRow[] = []; + + const map_shifts = (shift: any, is_holiday: boolean): CsvRow => { + 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,)) return { - company_code: employee.company_code, - external_payroll_id: employee.external_payroll_id, - timesheet_id: shift.timesheet.id, - email: shift.timesheet.employee.user.email, - shift_date: shift.date, - full_name: `${employee.user.first_name} ${employee.user.last_name}`, - bank_code: shift.bank_code?.bank_code ?? '', - quantity_hours: computeHours(shift.start_time, shift.end_time), - amount: undefined, - week_number: week, - pay_date: formatDate(end), - holiday_date: is_holiday ? formatDate(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, + date_transaction: formatDate(end), + premier_jour_absence: is_holiday ? formatDate(shift!.date) : '', + dernier_jour_absence: is_holiday ? formatDate(shift!.date) : '', } }; + //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)); + + if (want_expense) { + promises.push(this.prisma.expenses.findMany({ + where: { + date: { gte: start, lte: end }, + is_approved: true, + timesheet: { employee: { company_code: { in: company_codes } } }, + }, + select: select_csv_expense_lines, + })); + } else { + promises.push(Promise.resolve([])); + } + for (const expense of expenses) { 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); rows.push({ - company_code: employee.company_code, - external_payroll_id: employee.external_payroll_id, - timesheet_id: expense.timesheet.id, - email: '', - full_name: `${employee.user.first_name} ${employee.user.last_name}`, - bank_code: expense.bank_code?.bank_code ?? '', - quantity_hours: undefined, - amount: Number(expense.amount), - week_number: week, - pay_date: formatDate(end), - holiday_date: '', - 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: Number(expense.amount), + semaine_no: week, + division_no: undefined, + service_no: undefined, + departem_no: undefined, + date_transaction: formatDate(end), + premier_jour_absence: undefined, + dernier_jour_absence: undefined, + }); } //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; + if (a.compagnie_no !== b.compagnie_no) { + return a.compagnie_no - b.compagnie_no; } const bank_code = String(a.bank_code).localeCompare(String(b.bank_code)); if (bank_code !== 0) return bank_code; @@ -236,16 +185,15 @@ export class CsvExportService { //groups hours by bank_code const consolidated_rows = await consolidateRowHoursAndAmountByType(holiday_rows); - //requalifies regular hours into overtime when needed const requalified_rows = await applyOvertimeRequalifications(consolidated_rows, this.overtime_service); return requalified_rows; } - resolveHolidayTypeCode = async (holiday: string): Promise => { - const holiday_code = await this.prisma.bankCodes.findFirst({ - where: { type: holiday }, + resolveShiftTypeCode = async (shift_type: string): Promise => { + const shift_code = await this.prisma.bankCodes.findFirst({ + where: { type: shift_type }, select: { bank_code: true, shifts: { @@ -255,25 +203,9 @@ export class CsvExportService { }, }, }); - if (!holiday_code) throw new BadRequestException('Missing Holiday bank code'); + if (!shift_code) throw new BadRequestException('Missing Shift 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, - shifts: { - select: { - date: true, - }, - }, - }, - }); - if (!vacation_code) throw new BadRequestException('Missing Vacation bank code'); - return vacation_code.bank_code; + return shift_code.bank_code; } } diff --git a/src/time-and-attendance/utils/selects.utils.ts b/src/time-and-attendance/utils/selects.utils.ts index a760137..67671e2 100644 --- a/src/time-and-attendance/utils/selects.utils.ts +++ b/src/time-and-attendance/utils/selects.utils.ts @@ -82,4 +82,39 @@ export const select_employee_timesheet = { }, }, }, -}; \ No newline at end of file +}; + +export const select_csv_shift_lines = { + date: true, + start_time: true, + end_time: true, + bank_code: { select: { bank_code: true } }, + timesheet: { + select: { + id: true, + employee: { + select: { + company_code: true, + external_payroll_id: true, + }, + }, + }, + }, +} + +export const select_csv_expense_lines = { + date: true, + amount: true, + bank_code: { select: { bank_code: true } }, + timesheet: { + select: { + id: true, + employee: { + select: { + company_code: true, + external_payroll_id: true, + } + }, + } + }, +}