fix(schedule_preset) : added a check to remove preset_id for employees using the newly deleted preset

This commit is contained in:
Matthieu Haineault 2025-12-11 12:02:34 -05:00
parent acc128e5ea
commit be957d8180
5 changed files with 216 additions and 88 deletions

View File

@ -544,10 +544,83 @@
] ]
} }
}, },
"/exports/csv": { "/exports/csv/{year}/{period_no}": {
"get": { "get": {
"operationId": "CsvExportController_exportCsv", "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": { "responses": {
"200": { "200": {
"description": "" "description": ""

View File

@ -42,6 +42,7 @@ export class EmployeesGetService {
external_payroll_id: true, external_payroll_id: true,
first_work_day: true, first_work_day: true,
last_work_day: true, last_work_day: true,
schedule_preset_id: true,
} }
}).then(rows => rows.map(r => ({ }).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}`, supervisor_full_name: `${r.supervisor?.user.first_name} ${r.supervisor?.user.last_name}`,
first_work_day: toStringFromDate(r.first_work_day), first_work_day: toStringFromDate(r.first_work_day),
last_work_day: r.last_work_day ? toStringFromDate(r.last_work_day) : null, 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 }; return { success: true, data: employee_list };
}; };
@ -81,6 +83,7 @@ export class EmployeesGetService {
job_title: true, job_title: true,
external_payroll_id: true, external_payroll_id: true,
is_supervisor: true, is_supervisor: true,
schedule_preset_id: true,
supervisor: { supervisor: {
select: { select: {
id: true, user: { id: true, user: {
@ -110,6 +113,7 @@ export class EmployeesGetService {
phone_number: existing_profile.user.phone_number, phone_number: existing_profile.user.phone_number,
residence: existing_profile.user.phone_number, residence: existing_profile.user.phone_number,
first_work_day: toStringFromDate(existing_profile.first_work_day), first_work_day: toStringFromDate(existing_profile.first_work_day),
preset_id: existing_profile.schedule_preset_id ?? undefined,
}, },
}; };
}; };

View File

@ -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 { CsvExportService } from "./csv-exports.service";
import { ExportCsvOptionsDto } from "./export-csv-options.dto"; import { ExportCsvOptionsDto } from "./export-csv-options.dto";
import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators"; import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
@ -6,27 +6,37 @@ import { Modules as ModulesEnum } from ".prisma/client";
@Controller('exports') @Controller('exports')
export class CsvExportController { export class CsvExportController {
constructor(private readonly csvService: CsvExportService) {} constructor(private readonly csvService: CsvExportService) { }
@Get('csv') @Get('csv/:year/:period_no')
@ModuleAccessAllowed(ModulesEnum.employee_management) @ModuleAccessAllowed(ModulesEnum.employee_management)
@Header('Content-Type', 'text/csv; charset=utf-8') @Header('Content-Type', 'text/csv; charset=utf-8')
@Header('Content-Disposition', 'attachment; filename="export.csv"') @Header('Content-Disposition', 'attachment; filename="export.csv"')
async exportCsv(@Query() query: ExportCsvOptionsDto ): Promise<Buffer> { 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<Buffer> {
const rows = await this.csvService.collectTransaction( const rows = await this.csvService.collectTransaction(
query.year, year,
query.period_no, period_no,
{ {
approved: query.approved ?? true, approved: approved ?? false,
types: { types: {
shifts: query.shifts ?? true, shifts: shifts ?? false,
expenses: query.expenses ?? true, expenses: expenses ?? false,
holiday: query.holiday ?? true, holiday: holiday ?? false,
vacation: query.vacation ?? true, vacation: vacation ?? false,
}, },
companies: { companies: {
targo: query.targo ?? true, targo: targo ?? false,
solucom: query.solucom ?? true, solucom: solucom ?? false,
}, },
} }
); );

View File

@ -6,9 +6,9 @@ import { Filters, CsvRow } from "src/time-and-attendance/exports/export-csv-opti
@Injectable() @Injectable()
export class CsvExportService { export class CsvExportService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) { }
async collectTransaction( async collectTransaction(
year: number, year: number,
period_no: number, period_no: number,
filters: Filters, filters: Filters,
@ -19,34 +19,35 @@ export class CsvExportService {
where: { pay_year: year, pay_period_no: period_no }, where: { pay_year: year, pay_period_no: period_no },
select: { period_start: true, period_end: true }, 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 start = period.period_start;
const end = period.period_end; const end = period.period_end;
//fetch company codes from .env //fetch company codes from .env
const company_codes = this.resolveCompanyCodes(filters.companies); 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 //Flag types
const { shifts: want_shifts, expenses: want_expense, holiday: want_holiday, vacation: want_vacation } = filters.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 '); throw new BadRequestException(' No export type selected ');
} }
const approved_filter = filters.approved? { is_approved: true } : {}; const approved_filter = filters.approved ? { is_approved: true } : {};
const holiday_code = await this.resolveHolidayTypeCode('HOLIDAY');
const {holiday_code, vacation_code} = this.resolveLeaveCodes(); const vacation_code = await this.resolveVacationTypeCode('VACATION');
const code_filter = [holiday_code, vacation_code];
//Prisma queries //Prisma queries
const promises: Array<Promise<any[]>> = []; const promises: Array<Promise<any[]>> = [];
if (want_shifts) { if (want_shifts) {
promises.push( this.prisma.shifts.findMany({ promises.push(this.prisma.shifts.findMany({
where: { where: {
date: { gte: start, lte: end }, date: { gte: start, lte: end },
...approved_filter, ...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 } } }, timesheet: { employee: { company_code: { in: company_codes } } },
}, },
select: { select: {
@ -54,21 +55,25 @@ export class CsvExportService {
start_time: true, start_time: true,
end_time: true, end_time: true,
bank_code: { select: { bank_code: true } }, bank_code: { select: { bank_code: true } },
timesheet: { select: { timesheet: {
employee: { select: { select: {
company_code: true, employee: {
external_payroll_id: true, select: {
user: { select: { first_name: true, last_name: true } }, company_code: true,
}}, external_payroll_id: true,
}}, user: { select: { first_name: true, last_name: true } },
}
},
}
},
}, },
})); }));
} else { } else {
promises.push(Promise.resolve([])); promises.push(Promise.resolve([]));
} }
if(want_holiday) { if (want_holiday) {
promises.push( this.prisma.shifts.findMany({ promises.push(this.prisma.shifts.findMany({
where: { where: {
date: { gte: start, lte: end }, date: { gte: start, lte: end },
...approved_filter, ...approved_filter,
@ -80,21 +85,25 @@ export class CsvExportService {
start_time: true, start_time: true,
end_time: true, end_time: true,
bank_code: { select: { bank_code: true } }, bank_code: { select: { bank_code: true } },
timesheet: { select: { timesheet: {
employee: { select: { select: {
company_code: true, employee: {
external_payroll_id: true, select: {
user: { select: { first_name: true,last_name: true } }, company_code: true,
} }, external_payroll_id: true,
} }, user: { select: { first_name: true, last_name: true } },
}
},
}
},
}, },
})); }));
}else { } else {
promises.push(Promise.resolve([])); promises.push(Promise.resolve([]));
} }
if(want_vacation) { if (want_vacation) {
promises.push( this.prisma.shifts.findMany({ promises.push(this.prisma.shifts.findMany({
where: { where: {
date: { gte: start, lte: end }, date: { gte: start, lte: end },
...approved_filter, ...approved_filter,
@ -106,21 +115,25 @@ export class CsvExportService {
start_time: true, start_time: true,
end_time: true, end_time: true,
bank_code: { select: { bank_code: true } }, bank_code: { select: { bank_code: true } },
timesheet: { select: { timesheet: {
employee: { select: { select: {
company_code: true, employee: {
external_payroll_id: true, select: {
user: { select: { first_name: true,last_name: true } }, company_code: true,
} }, external_payroll_id: true,
} }, user: { select: { first_name: true, last_name: true } },
}
},
}
},
}, },
})); }));
}else { } else {
promises.push(Promise.resolve([])); promises.push(Promise.resolve([]));
} }
if(want_expense) { if (want_expense) {
promises.push( this.prisma.expenses.findMany({ promises.push(this.prisma.expenses.findMany({
where: { where: {
date: { gte: start, lte: end }, date: { gte: start, lte: end },
...approved_filter, ...approved_filter,
@ -130,13 +143,17 @@ export class CsvExportService {
date: true, date: true,
amount: true, amount: true,
bank_code: { select: { bank_code: true } }, bank_code: { select: { bank_code: true } },
timesheet: { select: { timesheet: {
employee: { select: { select: {
company_code: true, employee: {
external_payroll_id: true, select: {
user: { select: { first_name: true, last_name: true } }, company_code: true,
}}, external_payroll_id: true,
}}, user: { select: { first_name: true, last_name: true } },
}
},
}
},
}, },
})); }));
} else { } else {
@ -144,7 +161,7 @@ export class CsvExportService {
} }
//array of arrays //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 //mapping
const rows: CsvRow[] = []; const rows: CsvRow[] = [];
@ -154,18 +171,18 @@ export class CsvExportService {
return { return {
company_code: employee.company_code, company_code: employee.company_code,
external_payroll_id: employee.external_payroll_id, 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 ?? '', bank_code: shift.bank_code?.bank_code ?? '',
quantity_hours: this.computeHours(shift.start_time, shift.end_time), quantity_hours: this.computeHours(shift.start_time, shift.end_time),
amount: undefined, amount: undefined,
week_number: week, week_number: week,
pay_date: this.formatDate(end), pay_date: this.formatDate(end),
holiday_date: is_holiday? this.formatDate(shift.date) : '', holiday_date: is_holiday ? this.formatDate(shift.date) : '',
} as CsvRow; } as CsvRow;
}; };
//final mapping of all shifts based filters //final mapping of all shifts based filters
for (const shift of base_shifts) rows.push(map_shifts(shift, false)); 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 holiday_shifts) rows.push(map_shifts(shift, true));
for (const shift of vacation_shifts) rows.push(map_shifts(shift, false)); for (const shift of vacation_shifts) rows.push(map_shifts(shift, false));
for (const expense of expenses) { for (const expense of expenses) {
@ -174,7 +191,7 @@ export class CsvExportService {
rows.push({ rows.push({
company_code: employee.company_code, company_code: employee.company_code,
external_payroll_id: employee.external_payroll_id, 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 ?? '', bank_code: expense.bank_code?.bank_code ?? '',
quantity_hours: undefined, quantity_hours: undefined,
amount: Number(expense.amount), amount: Number(expense.amount),
@ -185,42 +202,55 @@ export class CsvExportService {
} }
//Final mapping and sorts //Final mapping and sorts
rows.sort((a,b) => { rows.sort((a, b) => {
if(a.external_payroll_id !== b.external_payroll_id) { if (a.external_payroll_id !== b.external_payroll_id) {
return 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)); const bk_code = String(a.bank_code).localeCompare(String(b.bank_code));
if(bk_code !== 0) return bk_code; if (bk_code !== 0) return bk_code;
if(a.bank_code !== b.bank_code) return a.bank_code.localeCompare(b.bank_code); if (a.bank_code !== b.bank_code) return a.bank_code.localeCompare(b.bank_code);
return 0; return 0;
}); });
return rows; return rows;
} }
resolveLeaveCodes(): { holiday_code: string; vacation_code: string; } { resolveHolidayTypeCode = async (holiday: string): Promise<string> => {
const holiday_code = process.env.HOLIDAY_CODE?.trim(); const holiday_code = await this.prisma.bankCodes.findFirst({
if(!holiday_code) throw new BadRequestException('Missing Holiday bank code'); 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(); return holiday_code.bank_code;
if(!vacation_code) throw new BadRequestException('Missing Vacation bank code'); }
return { holiday_code, vacation_code}; resolveVacationTypeCode = async (vacation: string): Promise<string> => {
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[] { resolveCompanyCodes(companies: { targo: boolean; solucom: boolean; }): number[] {
const out: number[] = []; const out: number[] = [];
if (companies.targo) { if (companies.targo) {
const code_no = parseInt(process.env.TARGO_NO ?? '', 10); 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); out.push(code_no);
} }
if (companies.solucom) { if (companies.solucom) {
const code_no = parseInt(process.env.SOLUCOM_NO ?? '', 10); 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); out.push(code_no);
} }
return out; return out;
} }
//csv builder and "mise en page" //csv builder and "mise en page"
@ -252,11 +282,11 @@ export class CsvExportService {
row.pay_date, row.pay_date,
row.holiday_date ?? '', row.holiday_date ?? '',
].join(','); ].join(',');
}).join('\n'); }).join('\n');
return Buffer.from('\uFEFF' + header + body, 'utf8'); return Buffer.from('\uFEFF' + header + body, 'utf8');
} }
private computeHours(start: Date, end: Date): number { private computeHours(start: Date, end: Date): number {
const diffMs = end.getTime() - start.getTime(); const diffMs = end.getTime() - start.getTime();
return +(diffMs / 1000 / 3600).toFixed(2); return +(diffMs / 1000 / 3600).toFixed(2);
@ -264,15 +294,15 @@ export class CsvExportService {
private computeWeekNumber(start: Date, date: Date): number { private computeWeekNumber(start: Date, date: Date): number {
const dayMS = 86400000; const dayMS = 86400000;
const days = Math.floor((this.toUTC(date).getTime() - this.toUTC(start).getTime())/ dayMS); const days = Math.floor((this.toUTC(date).getTime() - this.toUTC(start).getTime()) / dayMS);
return Math.floor(days / 7 ) + 1; return Math.floor(days / 7) + 1;
} }
toUTC(date: Date) { toUTC(date: Date) {
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); 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]; return d.toISOString().split('T')[0];
} }
} }

View File

@ -14,6 +14,17 @@ export class SchedulePresetDeleteService {
}); });
if (!preset) return { success: false, error: `SCHEDULE_PRESET_NOT_FOUND` }; 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 this.prisma.$transaction(async (tx) => {
await tx.schedulePresetShifts.deleteMany({ where: { preset_id: preset_id } }); await tx.schedulePresetShifts.deleteMany({ where: { preset_id: preset_id } });
await tx.schedulePresets.delete({ where: { id: preset_id } }); await tx.schedulePresets.delete({ where: { id: preset_id } });