Merge branch 'main' of git.targo.ca:Targo/targo_backend

This commit is contained in:
Matthieu Haineault 2026-03-23 09:01:16 -04:00
commit c47dcb1f2f
9 changed files with 56 additions and 102 deletions

View File

@ -1,9 +1,9 @@
import { Controller, Get, Param, Query, Res } from "@nestjs/common"; import { Body, Controller, Param, Post, StreamableFile } from "@nestjs/common";
import { CsvExportService } from "./services/csv-exports.service"; import { CsvExportService } from "./services/csv-exports.service";
import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators"; import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
import { Modules as ModulesEnum } from "prisma/postgres/generated/prisma/client/postgres/client"; import { Modules as ModulesEnum } from "prisma/postgres/generated/prisma/client/postgres/client";
import { Response } from "express";
import { CsvGeneratorService } from "src/time-and-attendance/exports/services/csv-builder.service"; import { CsvGeneratorService } from "src/time-and-attendance/exports/services/csv-builder.service";
import type { CsvFilters } from "src/time-and-attendance/exports/export-csv-options.dto";
@Controller('exports') @Controller('exports')
export class CsvExportController { export class CsvExportController {
@ -13,44 +13,19 @@ export class CsvExportController {
) { } ) { }
@Get('csv/:year/:period_no') @Post('csv/:year/:period_no')
@ModuleAccessAllowed(ModulesEnum.employee_management) @ModuleAccessAllowed(ModulesEnum.employee_management)
async exportCsv( async exportCsv(
@Query('approved') approved: string,
@Query('shifts') shifts: string,
@Query('expenses') expenses: string,
@Query('holiday') holiday: string,
@Query('vacation') vacation: string,
@Query('targo') targo: string,
@Query('solucom') solucom: string,
@Param('year') year: number, @Param('year') year: number,
@Param('period_no') period_no: number, @Param('period_no') period_no: number,
@Res() response: Response, @Body() filters: CsvFilters,
): Promise<void> { ) {
const rows = await this.csvService.collectTransaction( const rows = await this.csvService.collectTransaction(year, period_no, filters);
year, const buffer = this.generator.generateCsv(rows);
period_no,
{
approved: approved === 'true',
types: {
shifts: shifts === 'true',
expenses: expenses === 'true',
holiday: holiday === 'true',
vacation: vacation === 'true',
},
companies: {
targo: targo === 'true',
solucom: solucom === 'true',
},
}
);
const csv_buffer = this.generator.generateCsv(rows);
response.set({ return new StreamableFile(buffer, {
'Content-Type': 'text/csv; charset=utf-8', type: 'text/csv',
'Content-Disposition': 'attachment; filename="export.csv"', disposition: 'attachment; filename=export.csv'
}); });
response.send(csv_buffer);
} }
} }

View File

@ -134,17 +134,8 @@ export const applyOvertimeRequalifications = (
return result; return result;
} }
export const resolveCompanyCodes = (companies: { targo: boolean; solucom: boolean; }): number[] => { export const resolveCompanyCode = (company: 'Targo' | 'Solucom'): number => {
const out: number[] = []; return company === 'Targo' ? 271583 : 271585;
if (companies.targo) {
const code_no = 271583;
out.push(code_no);
}
if (companies.solucom) {
const code_no = 271585;
out.push(code_no);
}
return out;
} }
export const computeWeekNumber = (start: Date, date: Date): number => { export const computeWeekNumber = (start: Date, date: Date): number => {

View File

@ -1,5 +1,6 @@
import { Transform } from "class-transformer"; import { Transform } from "class-transformer";
import { IsBoolean, IsInt, IsOptional, Max, Min } from "class-validator"; import { IsBoolean, IsInt, IsOptional, Max, Min } from "class-validator";
import { ShiftType } from "src/time-and-attendance/shifts/shift.dto";
const toBoolean = (v: any) => { const toBoolean = (v: any) => {
if (typeof v === 'boolean') return v; if (typeof v === 'boolean') return v;
@ -68,16 +69,8 @@ export interface CsvRow {
export type InternalCsvRow = CsvRow & { timesheet_id: number; shift_date: Date; } export type InternalCsvRow = CsvRow & { timesheet_id: number; shift_date: Date; }
export type Filters = { export type CsvFilters = {
types: { companyName: 'Targo' | 'Solucom';
shifts: boolean; includeExpenses: boolean;
expenses: boolean; shiftTypes: ShiftType[];
holiday: boolean;
vacation: boolean;
};
companies: {
targo: boolean;
solucom: boolean;
};
approved: boolean;
}; };

View File

@ -29,6 +29,6 @@ export class CsvGeneratorService {
row.dernier_jour_absence ?? '', row.dernier_jour_absence ?? '',
].join(';'); ].join(';');
}).join('\n'); }).join('\n');
return Buffer.from('\uFEFF' + body, 'utf8'); return Buffer.from(body);
} }
} }

View File

@ -1,11 +1,11 @@
import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service"; import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { Filters, CsvRow, InternalCsvRow } from "src/time-and-attendance/exports/export-csv-options.dto";
import { computeHours } from "src/common/utils/date-utils"; 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 { applyHolidayRequalifications, applyOvertimeRequalifications, computeWeekNumber, consolidateRowHoursAndAmountByType, formatDate, resolveCompanyCode } from "src/time-and-attendance/exports/csv-exports.utils";
import { HolidayService } from "src/time-and-attendance/domains/services/holiday.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"; import { select_csv_expense_lines, select_csv_shift_lines } from "src/time-and-attendance/utils/selects.utils";
import { BillableShiftType } from "src/time-and-attendance/shifts/shift.dto"; import type { CsvRow, InternalCsvRow, CsvFilters } from "src/time-and-attendance/exports/export-csv-options.dto";
import type { ShiftType } from "src/time-and-attendance/shifts/shift.dto";
@Injectable() @Injectable()
@ -35,39 +35,35 @@ export class CsvExportService {
async collectTransaction( async collectTransaction(
year: number, year: number,
period_no: number, period_no: number,
filters: Filters filters: CsvFilters
): Promise<CsvRow[]> { ): Promise<CsvRow[]> {
const BILLABLE_SHIFT_TYPES: BillableShiftType[] = []; const PTO_SHIFT_CODES = ['G104', 'G105', 'G109']; // [ HOLIDAY, SICK, VACATION ]
const HOLIDAY_SHIFT_CODE = 'G104';
if (filters.types.shifts) BILLABLE_SHIFT_TYPES.push('REGULAR', 'OVERTIME', 'EMERGENCY', 'EVENING', 'SICK'); const requestedShiftCodes = await this.resolveShiftTypeCode(filters.shiftTypes);
if (filters.types.holiday) BILLABLE_SHIFT_TYPES.push('HOLIDAY');
if (filters.types.vacation) BILLABLE_SHIFT_TYPES.push('VACATION');
const BILLABLE_SHIFT_CODES = await this.resolveShiftTypeCode(BILLABLE_SHIFT_TYPES); if (requestedShiftCodes.length < 1)
const PTO_SHIFT_CODES = await this.resolveShiftTypeCode(['VACATION', 'SICK', 'HOLIDAY']); throw new BadRequestException('NO_TYPE_SELECTED');
const HOLIDAY_SHIFT_CODE = await this.resolveShiftTypeCode(['HOLIDAY']);
const period = await this.prisma.payPeriods.findFirst({ const period = await this.prisma.payPeriods.findFirst({
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;
const company_codes = resolveCompanyCodes(filters.companies); const companyCode = resolveCompanyCode(filters.companyName);
if (company_codes.length === 0) throw new BadRequestException('NO_COMPANY_SELECTED');
if (Object.values(filters.types).every(type => type === false))
throw new BadRequestException('NO_TYPE_SELECTED');
const exportedShifts = await this.prisma.shifts.findMany({ const exportedShifts = await this.prisma.shifts.findMany({
where: { where: {
date: { gte: start, lte: end }, date: { gte: start, lte: end },
is_approved: true, is_approved: true,
timesheet: { employee: { company_code: { in: company_codes } } }, timesheet: { employee: { company_code: companyCode } },
bank_code: { bank_code: { in: BILLABLE_SHIFT_CODES } }, bank_code: { bank_code: { in: requestedShiftCodes } },
}, },
select: select_csv_shift_lines, select: select_csv_shift_lines,
}); });
@ -101,12 +97,12 @@ export class CsvExportService {
} }
}); });
if (filters.types.expenses) { if (filters.includeExpenses) {
const exportedExpenses = await this.prisma.expenses.findMany({ const exportedExpenses = await this.prisma.expenses.findMany({
where: { where: {
date: { gte: start, lte: end }, date: { gte: start, lte: end },
is_approved: true, is_approved: true,
timesheet: { employee: { company_code: { in: company_codes } } }, timesheet: { employee: { company_code: companyCode } },
}, },
select: select_csv_expense_lines, select: select_csv_expense_lines,
}); });
@ -149,7 +145,7 @@ export class CsvExportService {
a.employee_matricule - b.employee_matricule a.employee_matricule - b.employee_matricule
); );
const holiday_rows = await applyHolidayRequalifications(rows, this.holiday_service, HOLIDAY_SHIFT_CODE[0]); const holiday_rows = await applyHolidayRequalifications(rows, this.holiday_service, HOLIDAY_SHIFT_CODE);
const consolidated_rows = consolidateRowHoursAndAmountByType(holiday_rows); const consolidated_rows = consolidateRowHoursAndAmountByType(holiday_rows);
//requalifies regular hours into overtime when needed //requalifies regular hours into overtime when needed
const requalified_rows = applyOvertimeRequalifications(consolidated_rows); const requalified_rows = applyOvertimeRequalifications(consolidated_rows);
@ -157,7 +153,7 @@ export class CsvExportService {
return requalified_rows; return requalified_rows;
} }
resolveShiftTypeCode = async (shift_type: BillableShiftType[]): Promise<string[]> => { resolveShiftTypeCode = async (shift_type: ShiftType[]): Promise<string[]> => {
const billableBankCodes = await this.prisma.bankCodes.findMany({ const billableBankCodes = await this.prisma.bankCodes.findMany({
where: { type: { in: shift_type } }, where: { type: { in: shift_type } },
select: { select: {

View File

@ -10,7 +10,7 @@ export class PaidTimeOffController {
) { } ) { }
@Get('totals') @Get('totals')
@ModuleAccessAllowed('timesheets', 'timesheets_approval', 'employee_management') @ModuleAccessAllowed('timesheets')
async getPaidTimeOffTotalsForOneEmployee( async getPaidTimeOffTotalsForOneEmployee(
@Access('email') email: string, @Access('email') email: string,
@Query('email') employee_email?: string, @Query('email') employee_email?: string,

View File

@ -146,13 +146,7 @@ export class GetOverviewService {
switch (type) { switch (type) {
case "EVENING": case "EVENING":
if (total_weekly_hours + hours <= 40) {
record.other_hours.evening_hours += Math.min(hours, 8 - daily_hours); record.other_hours.evening_hours += Math.min(hours, 8 - daily_hours);
record.other_hours.overtime_hours += Math.max(daily_hours + hours - 8, 0);
} else {
record.other_hours.evening_hours += Math.max(40 - total_weekly_hours, 0);
record.other_hours.overtime_hours += Math.min(total_weekly_hours + hours - 40, hours);
}
total_weekly_hours += hours; total_weekly_hours += hours;
record.total_hours += hours; record.total_hours += hours;
break; break;
@ -169,12 +163,16 @@ export class GetOverviewService {
record.total_hours += hours; record.total_hours += hours;
total_weekly_hours += hours; total_weekly_hours += hours;
break; break;
case "VACATION": record.other_hours.vacation_hours += hours; case "VACATION":
record.other_hours.vacation_hours += hours;
total_weekly_hours += hours;
break; break;
case "REGULAR": case "REGULAR":
if (total_weekly_hours + hours <= 40) { if (total_weekly_hours + hours <= 40) {
record.regular_hours += Math.min(hours, 8 - daily_hours); record.regular_hours += hours;
record.other_hours.overtime_hours += Math.max(daily_hours + hours - 8, 0); // TODO: ADD DAILY OVERTIME CHECK HERE
// record.regular_hours += Math.min(hours, 8 - daily_hours);
// record.other_hours.overtime_hours += Math.max(daily_hours + hours - 8, 0);
} else { } else {
record.regular_hours += Math.max(40 - total_weekly_hours, 0); record.regular_hours += Math.max(40 - total_weekly_hours, 0);
record.other_hours.overtime_hours += Math.min(total_weekly_hours + hours - 40, hours); record.other_hours.overtime_hours += Math.min(total_weekly_hours + hours - 40, hours);

View File

@ -12,4 +12,4 @@ export class ShiftDto {
@IsOptional() @IsString() @MaxLength(280) comment?: string; @IsOptional() @IsString() @MaxLength(280) comment?: string;
} }
export type BillableShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'VACATION' | 'HOLIDAY' | 'SICK' | 'OVERTIME'; export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'VACATION' | 'HOLIDAY' | 'SICK';

View File

@ -82,6 +82,7 @@ export const mapOneTimesheet = (
weekly_hours[subgroup] += hours; weekly_hours[subgroup] += hours;
} }
// TODO: ADD DAILY OVERTIME CHECK HERE
// const dailyOvertimeOwed = Math.max(daily_hours.regular - timesheet.employee.daily_expected_hours, 0) // const dailyOvertimeOwed = Math.max(daily_hours.regular - timesheet.employee.daily_expected_hours, 0)
// daily_hours.overtime = dailyOvertimeOwed; // daily_hours.overtime = dailyOvertimeOwed;
// daily_hours.regular -= dailyOvertimeOwed; // daily_hours.regular -= dailyOvertimeOwed;