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,56 +1,31 @@
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 { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
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 type { CsvFilters } from "src/time-and-attendance/exports/export-csv-options.dto";
@Controller('exports')
export class CsvExportController {
constructor(
private readonly csvService: CsvExportService,
private readonly generator: CsvGeneratorService,
) { }
@Get('csv/:year/:period_no')
@Post('csv/:year/:period_no')
@ModuleAccessAllowed(ModulesEnum.employee_management)
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('period_no') period_no: number,
@Res() response: Response,
): Promise<void> {
const rows = await this.csvService.collectTransaction(
year,
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);
@Body() filters: CsvFilters,
) {
const rows = await this.csvService.collectTransaction(year, period_no, filters);
const buffer = this.generator.generateCsv(rows);
response.set({
'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': 'attachment; filename="export.csv"',
return new StreamableFile(buffer, {
type: 'text/csv',
disposition: 'attachment; filename=export.csv'
});
response.send(csv_buffer);
}
}

View File

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

View File

@ -1,5 +1,6 @@
import { Transform } from "class-transformer";
import { IsBoolean, IsInt, IsOptional, Max, Min } from "class-validator";
import { ShiftType } from "src/time-and-attendance/shifts/shift.dto";
const toBoolean = (v: any) => {
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 Filters = {
types: {
shifts: boolean;
expenses: boolean;
holiday: boolean;
vacation: boolean;
};
companies: {
targo: boolean;
solucom: boolean;
};
approved: boolean;
export type CsvFilters = {
companyName: 'Targo' | 'Solucom';
includeExpenses: boolean;
shiftTypes: ShiftType[];
};

View File

@ -29,6 +29,6 @@ export class CsvGeneratorService {
row.dernier_jour_absence ?? '',
].join(';');
}).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 { 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 { 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 { 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()
@ -35,39 +35,35 @@ export class CsvExportService {
async collectTransaction(
year: number,
period_no: number,
filters: Filters
filters: CsvFilters
): Promise<CsvRow[]> {
const BILLABLE_SHIFT_TYPES: BillableShiftType[] = [];
if (filters.types.shifts) BILLABLE_SHIFT_TYPES.push('REGULAR', 'OVERTIME', 'EMERGENCY', 'EVENING', 'SICK');
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);
const PTO_SHIFT_CODES = await this.resolveShiftTypeCode(['VACATION', 'SICK', 'HOLIDAY']);
const HOLIDAY_SHIFT_CODE = await this.resolveShiftTypeCode(['HOLIDAY']);
const PTO_SHIFT_CODES = ['G104', 'G105', 'G109']; // [ HOLIDAY, SICK, VACATION ]
const HOLIDAY_SHIFT_CODE = 'G104';
const requestedShiftCodes = await this.resolveShiftTypeCode(filters.shiftTypes);
if (requestedShiftCodes.length < 1)
throw new BadRequestException('NO_TYPE_SELECTED');
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`);
if (!period)
throw new NotFoundException(`Pay period ${year}-${period_no} not found`);
const start = period.period_start;
const end = period.period_end;
const company_codes = resolveCompanyCodes(filters.companies);
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 companyCode = resolveCompanyCode(filters.companyName);
const exportedShifts = await this.prisma.shifts.findMany({
where: {
date: { gte: start, lte: end },
is_approved: true,
timesheet: { employee: { company_code: { in: company_codes } } },
bank_code: { bank_code: { in: BILLABLE_SHIFT_CODES } },
timesheet: { employee: { company_code: companyCode } },
bank_code: { bank_code: { in: requestedShiftCodes } },
},
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({
where: {
date: { gte: start, lte: end },
is_approved: true,
timesheet: { employee: { company_code: { in: company_codes } } },
timesheet: { employee: { company_code: companyCode } },
},
select: select_csv_expense_lines,
});
@ -149,7 +145,7 @@ export class CsvExportService {
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);
//requalifies regular hours into overtime when needed
const requalified_rows = applyOvertimeRequalifications(consolidated_rows);
@ -157,7 +153,7 @@ export class CsvExportService {
return requalified_rows;
}
resolveShiftTypeCode = async (shift_type: BillableShiftType[]): Promise<string[]> => {
resolveShiftTypeCode = async (shift_type: ShiftType[]): Promise<string[]> => {
const billableBankCodes = await this.prisma.bankCodes.findMany({
where: { type: { in: shift_type } },
select: {

View File

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

View File

@ -12,7 +12,7 @@ export class GetOverviewService {
) { }
async getOverviewByYearPeriod(
pay_year: number,
pay_year: number,
period_no: number
): Promise<Result<PayPeriodOverviewDto, string>> {
const period = computePeriod(pay_year, period_no);
@ -110,9 +110,9 @@ export class GetOverviewService {
}
const ensure = (
id: number,
first_name: string,
last_name: string,
id: number,
first_name: string,
last_name: string,
email: string
) => {
if (!by_employee.has(id)) {
@ -146,13 +146,7 @@ export class GetOverviewService {
switch (type) {
case "EVENING":
if (total_weekly_hours + hours <= 40) {
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);
}
record.other_hours.evening_hours += Math.min(hours, 8 - daily_hours);
total_weekly_hours += hours;
record.total_hours += hours;
break;
@ -169,12 +163,16 @@ export class GetOverviewService {
record.total_hours += hours;
total_weekly_hours += hours;
break;
case "VACATION": record.other_hours.vacation_hours += hours;
case "VACATION":
record.other_hours.vacation_hours += hours;
total_weekly_hours += hours;
break;
case "REGULAR":
if (total_weekly_hours + hours <= 40) {
record.regular_hours += Math.min(hours, 8 - daily_hours);
record.other_hours.overtime_hours += Math.max(daily_hours + hours - 8, 0);
record.regular_hours += hours;
// 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 {
record.regular_hours += Math.max(40 - total_weekly_hours, 0);
record.other_hours.overtime_hours += Math.min(total_weekly_hours + hours - 40, hours);
@ -213,7 +211,7 @@ export class GetOverviewService {
if (timesheets.length > 0)
record.is_approved = timesheets.every(timesheet => timesheet.is_approved);
record.is_active = is_active;
}

View File

@ -12,4 +12,4 @@ export class ShiftDto {
@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;
}
// TODO: ADD DAILY OVERTIME CHECK HERE
// const dailyOvertimeOwed = Math.max(daily_hours.regular - timesheet.employee.daily_expected_hours, 0)
// daily_hours.overtime = dailyOvertimeOwed;
// daily_hours.regular -= dailyOvertimeOwed;