From be957d8180626133efad9a5e15c2cdf58502e52d Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Thu, 11 Dec 2025 12:02:34 -0500 Subject: [PATCH] fix(schedule_preset) : added a check to remove preset_id for employees using the newly deleted preset --- docs/swagger/swagger-spec.json | 77 +++++++- .../services/employees-get.service.ts | 4 + .../exports/csv-exports.controller.ts | 36 ++-- .../exports/csv-exports.service.ts | 176 ++++++++++-------- .../schedule-presets-delete.service.ts | 11 ++ 5 files changed, 216 insertions(+), 88 deletions(-) diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index b4c431e..0922280 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -544,10 +544,83 @@ ] } }, - "/exports/csv": { + "/exports/csv/{year}/{period_no}": { "get": { "operationId": "CsvExportController_exportCsv", - "parameters": [], + "parameters": [ + { + "name": "approved", + "required": true, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "shifts", + "required": true, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "expenses", + "required": true, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "holiday", + "required": true, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "vacation", + "required": true, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "targo", + "required": true, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "solucom", + "required": true, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "year", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "period_no", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], "responses": { "200": { "description": "" diff --git a/src/identity-and-account/employees/services/employees-get.service.ts b/src/identity-and-account/employees/services/employees-get.service.ts index 5b0587d..1bc3ba2 100644 --- a/src/identity-and-account/employees/services/employees-get.service.ts +++ b/src/identity-and-account/employees/services/employees-get.service.ts @@ -42,6 +42,7 @@ export class EmployeesGetService { external_payroll_id: true, first_work_day: true, last_work_day: true, + schedule_preset_id: true, } }).then(rows => rows.map(r => ({ @@ -56,6 +57,7 @@ export class EmployeesGetService { supervisor_full_name: `${r.supervisor?.user.first_name} ${r.supervisor?.user.last_name}`, first_work_day: toStringFromDate(r.first_work_day), last_work_day: r.last_work_day ? toStringFromDate(r.last_work_day) : null, + preset_id: r.schedule_preset_id ?? undefined, }))); return { success: true, data: employee_list }; }; @@ -81,6 +83,7 @@ export class EmployeesGetService { job_title: true, external_payroll_id: true, is_supervisor: true, + schedule_preset_id: true, supervisor: { select: { id: true, user: { @@ -110,6 +113,7 @@ export class EmployeesGetService { phone_number: existing_profile.user.phone_number, residence: existing_profile.user.phone_number, first_work_day: toStringFromDate(existing_profile.first_work_day), + preset_id: existing_profile.schedule_preset_id ?? undefined, }, }; }; diff --git a/src/time-and-attendance/exports/csv-exports.controller.ts b/src/time-and-attendance/exports/csv-exports.controller.ts index d9a9b0b..f963b6d 100644 --- a/src/time-and-attendance/exports/csv-exports.controller.ts +++ b/src/time-and-attendance/exports/csv-exports.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Header, Query} from "@nestjs/common"; +import { Controller, Get, Header, Param, Query } 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"; @@ -6,27 +6,37 @@ import { Modules as ModulesEnum } from ".prisma/client"; @Controller('exports') export class CsvExportController { - constructor(private readonly csvService: CsvExportService) {} + constructor(private readonly csvService: CsvExportService) { } - @Get('csv') + @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() query: ExportCsvOptionsDto ): Promise { + async exportCsv( + @Query('approved') approved: boolean, + @Query('shifts') shifts: boolean, + @Query('expenses') expenses: boolean, + @Query('holiday') holiday: boolean, + @Query('vacation') vacation: boolean, + @Query('targo') targo: boolean, + @Query('solucom') solucom: boolean, + @Param('year') year: number, + @Param('period_no') period_no: number, + ): Promise { const rows = await this.csvService.collectTransaction( - query.year, - query.period_no, + year, + period_no, { - approved: query.approved ?? true, + approved: approved ?? false, types: { - shifts: query.shifts ?? true, - expenses: query.expenses ?? true, - holiday: query.holiday ?? true, - vacation: query.vacation ?? true, + shifts: shifts ?? false, + expenses: expenses ?? false, + holiday: holiday ?? false, + vacation: vacation ?? false, }, companies: { - targo: query.targo ?? true, - solucom: query.solucom ?? true, + targo: targo ?? false, + solucom: solucom ?? false, }, } ); diff --git a/src/time-and-attendance/exports/csv-exports.service.ts b/src/time-and-attendance/exports/csv-exports.service.ts index c876636..43a0c7c 100644 --- a/src/time-and-attendance/exports/csv-exports.service.ts +++ b/src/time-and-attendance/exports/csv-exports.service.ts @@ -6,9 +6,9 @@ import { Filters, CsvRow } from "src/time-and-attendance/exports/export-csv-opti @Injectable() export class CsvExportService { - constructor(private readonly prisma: PrismaService) {} + constructor(private readonly prisma: PrismaService) { } - async collectTransaction( + async collectTransaction( year: number, period_no: number, filters: Filters, @@ -19,34 +19,35 @@ export class CsvExportService { 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; //fetch company codes from .env const company_codes = this.resolveCompanyCodes(filters.companies); - if(company_codes.length === 0) throw new BadRequestException('No company selected'); + 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) { + 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, vacation_code} = this.resolveLeaveCodes(); + 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({ + promises.push(this.prisma.shifts.findMany({ where: { date: { gte: start, lte: end }, ...approved_filter, - bank_code: { bank_code: { notIn: [ holiday_code, vacation_code ] } }, + bank_code: { bank_code: { notIn: code_filter } }, timesheet: { employee: { company_code: { in: company_codes } } }, }, select: { @@ -54,21 +55,25 @@ export class CsvExportService { 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 } }, - }}, - }}, + 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({ + if (want_holiday) { + promises.push(this.prisma.shifts.findMany({ where: { date: { gte: start, lte: end }, ...approved_filter, @@ -80,21 +85,25 @@ export class CsvExportService { 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 } }, - } }, - } }, + timesheet: { + select: { + employee: { + select: { + company_code: true, + external_payroll_id: true, + user: { select: { first_name: true, last_name: true } }, + } + }, + } + }, }, })); - }else { + } else { promises.push(Promise.resolve([])); } - if(want_vacation) { - promises.push( this.prisma.shifts.findMany({ + if (want_vacation) { + promises.push(this.prisma.shifts.findMany({ where: { date: { gte: start, lte: end }, ...approved_filter, @@ -106,21 +115,25 @@ export class CsvExportService { 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 } }, - } }, - } }, + timesheet: { + select: { + employee: { + select: { + company_code: true, + external_payroll_id: true, + user: { select: { first_name: true, last_name: true } }, + } + }, + } + }, }, })); - }else { + } else { promises.push(Promise.resolve([])); } - if(want_expense) { - promises.push( this.prisma.expenses.findMany({ + if (want_expense) { + promises.push(this.prisma.expenses.findMany({ where: { date: { gte: start, lte: end }, ...approved_filter, @@ -130,13 +143,17 @@ export class CsvExportService { 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 } }, - }}, - }}, + timesheet: { + select: { + employee: { + select: { + company_code: true, + external_payroll_id: true, + user: { select: { first_name: true, last_name: true } }, + } + }, + } + }, }, })); } else { @@ -144,7 +161,7 @@ export class CsvExportService { } //array of arrays - const [ base_shifts, holiday_shifts, vacation_shifts, expenses ] = await Promise.all(promises); + const [base_shifts, holiday_shifts, vacation_shifts, expenses] = await Promise.all(promises); //mapping const rows: CsvRow[] = []; @@ -154,18 +171,18 @@ 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.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) : '', + 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 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) { @@ -174,7 +191,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.first_name} ${employee.last_name}`, bank_code: expense.bank_code?.bank_code ?? '', quantity_hours: undefined, amount: Number(expense.amount), @@ -185,42 +202,55 @@ export class CsvExportService { } //Final mapping and sorts - rows.sort((a,b) => { - if(a.external_payroll_id !== b.external_payroll_id) { + 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); + 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; } - resolveLeaveCodes(): { holiday_code: string; vacation_code: string; } { - const holiday_code = process.env.HOLIDAY_CODE?.trim(); - if(!holiday_code) throw new BadRequestException('Missing Holiday bank code'); + 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'); - const vacation_code = process.env.VACATION_CODE?.trim(); - if(!vacation_code) throw new BadRequestException('Missing Vacation bank code'); + return holiday_code.bank_code; + } - return { holiday_code, vacation_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'); + 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'); + if (!Number.isFinite(code_no) || code_no <= 0) throw new BadRequestException('Invalid Solucom code in env'); out.push(code_no); } - return out; + return out; } //csv builder and "mise en page" @@ -252,11 +282,11 @@ export class CsvExportService { row.pay_date, row.holiday_date ?? '', ].join(','); - }).join('\n'); - return Buffer.from('\uFEFF' + header + body, 'utf8'); + }).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); @@ -264,15 +294,15 @@ export class CsvExportService { 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; + 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 { + private formatDate(d: Date): string { return d.toISOString().split('T')[0]; } - + } diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-delete.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-delete.service.ts index b8f1d0b..27c5544 100644 --- a/src/time-and-attendance/schedule-presets/services/schedule-presets-delete.service.ts +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-delete.service.ts @@ -14,6 +14,17 @@ export class SchedulePresetDeleteService { }); if (!preset) return { success: false, error: `SCHEDULE_PRESET_NOT_FOUND` }; + const employee_with_preset = await this.prisma.employees.findMany({ + where: { schedule_preset_id: preset.id }, + select: { + schedule_preset_id: true, + }, + }); + if(employee_with_preset.length > 0) { + for(const employee of employee_with_preset) { + employee.schedule_preset_id = null; + } + } await this.prisma.$transaction(async (tx) => { await tx.schedulePresetShifts.deleteMany({ where: { preset_id: preset_id } }); await tx.schedulePresets.delete({ where: { id: preset_id } });