Merge branch 'main' of git.targo.ca:Targo/targo_backend into dev/matthieu/tickets

This commit is contained in:
Matthieu Haineault 2026-02-18 08:57:48 -05:00
commit 455a57fef6
9 changed files with 261 additions and 295 deletions

View File

@ -1,7 +1,6 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { computeHours, getWeekStart } from "src/common/utils/date-utils"; import { computeHours, getWeekStart } from "src/common/utils/date-utils";
import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service"; import { PrismaPostgresService } from "prisma/postgres/prisma-postgres.service";
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
import { MS_PER_WEEK } from "src/common/utils/constants.utils"; import { MS_PER_WEEK } from "src/common/utils/constants.utils";
import { Result } from "src/common/errors/result-error.factory"; import { Result } from "src/common/errors/result-error.factory";
/* /*
@ -15,25 +14,39 @@ import { Result } from "src/common/errors/result-error.factory";
export class HolidayService { export class HolidayService {
constructor( constructor(
private readonly prisma: PrismaPostgresService, private readonly prisma: PrismaPostgresService,
private readonly emailResolver: EmailToIdResolver,
) { } ) { }
private async computeHoursPrevious4WeeksByEmail(email: string, holiday_date: Date): Promise<Result<number, string>> { /**
const employee_id = await this.emailResolver.findIdByEmail(email); * Calculates the amount of hours to be paid for a given employee on the provided holiday date.
if (!employee_id.success) return { success: false, error: employee_id.error }; *
return this.computeHoursPrevious4Weeks(employee_id.data, holiday_date); * @param employee_id id of the employee whom the holiday shift belongs to
} * @param holiday_date date that the holiday was taken on
*
private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise<Result<number, string>> { * @returns Average daily hours worked in the past four weeks as a `number`, to a maximum of `8`
*/
private async computeHoursPrevious4Weeks(external_payroll_id: number, company_code: number, holiday_date: Date): Promise<Result<number, string>> {
try { try {
const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G305', 'G700', 'G720'];
const holiday_week_start = getWeekStart(holiday_date); const holiday_week_start = getWeekStart(holiday_date);
const window_start = new Date(holiday_week_start.getTime() - 4 * MS_PER_WEEK); const window_start = new Date(holiday_week_start.getTime() - 4 * MS_PER_WEEK);
const window_end = new Date(holiday_week_start.getTime() - 1); const window_end = new Date(holiday_week_start.getTime() - 1);
const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G305', 'G700', 'G720']; const employee = await this.prisma.employees.findFirst({
where: {
external_payroll_id,
company_code,
},
select: {
id: true,
}
});
if (!employee)
return {success: false, error: 'EMPLOYEE_NOT_FOUND'};
const shifts = await this.prisma.shifts.findMany({ const shifts = await this.prisma.shifts.findMany({
where: { where: {
timesheet: { employee_id: employee_id }, timesheet: { employee_id: employee.id },
date: { gte: window_start, lte: window_end }, date: { gte: window_start, lte: window_end },
bank_code: { bank_code: { in: valid_codes } }, bank_code: { bank_code: { in: valid_codes } },
}, },
@ -43,7 +56,10 @@ export class HolidayService {
const hours_by_week = new Map<number, number>(); const hours_by_week = new Map<number, number>();
for (const shift of shifts) { for (const shift of shifts) {
const hours = computeHours(shift.start_time, shift.end_time); const hours = computeHours(shift.start_time, shift.end_time);
if (hours <= 0) continue;
if (hours <= 0)
continue;
const shift_week_start = getWeekStart(shift.date); const shift_week_start = getWeekStart(shift.date);
const key = shift_week_start.getTime(); const key = shift_week_start.getTime();
hours_by_week.set(key, (hours_by_week.get(key) ?? 0) + hours); hours_by_week.set(key, (hours_by_week.get(key) ?? 0) + hours);
@ -64,11 +80,12 @@ export class HolidayService {
} }
} }
async calculateHolidayPay(email: string, holiday_date: Date, modifier: number): Promise<Result<number, string>> { async calculateHolidayPay(employeePayrollID: number, companyCode: number, holiday_date: Date): Promise<Result<number, string>> {
const average_daily_hours = await this.computeHoursPrevious4WeeksByEmail(email, holiday_date); const average_daily_hours = await this.computeHoursPrevious4Weeks(employeePayrollID, companyCode, holiday_date);
if (!average_daily_hours.success) return { success: false, error: average_daily_hours.error }; if (!average_daily_hours.success) return { success: false, error: average_daily_hours.error };
const daily_rate = (Math.min(average_daily_hours.data, 8)) * modifier; const daily_rate = (Math.min(average_daily_hours.data, 8));
return { success: true, data: daily_rate }; return { success: true, data: daily_rate };
} }
} }

View File

@ -2,20 +2,26 @@ import { HolidayService } from "src/time-and-attendance/domains/services/holiday
import { OvertimeService } from "src/time-and-attendance/domains/services/overtime.service"; import { OvertimeService } from "src/time-and-attendance/domains/services/overtime.service";
import { CsvRow, InternalCsvRow } from "src/time-and-attendance/exports/export-csv-options.dto"; import { CsvRow, InternalCsvRow } from "src/time-and-attendance/exports/export-csv-options.dto";
const REGULAR = 'G1'; // You made a helper to pull bank codes from the db, but omitted to use it here... curious.
const OVERTIME = 'G43'; const REGULAR = 1;
const OVERTIME = 43;
const VACATION = 109;
export const consolidateRowHoursAndAmountByType = (rows: InternalCsvRow[]): InternalCsvRow[] => { export const consolidateRowHoursAndAmountByType = (rows: InternalCsvRow[]): InternalCsvRow[] => {
const map = new Map<string, InternalCsvRow>(); const map = new Map<string, InternalCsvRow>();
for (const row of rows) { for (const row of rows) {
const key = `${row.timesheet_id}|${row.bank_code}|${row.week_number}`; if (row.code = VACATION) {
map.set(`${row.code}|${row.shift_date}`, row);
} else {
const key = `${row.code}|${row.semaine_no}`;
if (!map.has(key)) { if (!map.has(key)) {
map.set(key, { ...row }); map.set(key, row);
} else { } else {
const existing = map.get(key)!; const existing = map.get(key)!;
existing.quantity_hours = (existing.quantity_hours ?? 0) + (row.quantity_hours ?? 0); existing.quantite_hre = (existing.quantite_hre ?? 0) + (row.quantite_hre ?? 0);
existing.amount = (existing.amount ?? 0) + (row.amount ?? 0); existing.montant = (existing.montant ?? 0) + (row.montant ?? 0);
}
} }
} }
return Array.from(map.values()); return Array.from(map.values());
@ -24,26 +30,29 @@ export const consolidateRowHoursAndAmountByType = (rows: InternalCsvRow[]): Inte
export const applyHolidayRequalifications = async ( export const applyHolidayRequalifications = async (
rows: InternalCsvRow[], rows: InternalCsvRow[],
holiday_service: HolidayService, holiday_service: HolidayService,
holiday_bank_code: string,
): Promise<InternalCsvRow[]> => { ): Promise<InternalCsvRow[]> => {
const result: InternalCsvRow[] = []; const result: InternalCsvRow[] = [];
const HOLIDAY_BANK_CODE = Number(holiday_bank_code.slice(1,));
for (const row of rows) { for (const row of rows) {
if (row.bank_code !== 'G104') { if (row.code !== HOLIDAY_BANK_CODE) {
result.push(row); result.push(row);
continue; continue;
} }
if (!row.holiday_date || !row.email) { if (!row.premier_jour_absence || !row.dernier_jour_absence || !row.employee_matricule || !row.compagnie_no) {
result.push(row); result.push(row);
continue; continue;
} }
const calculated = await holiday_service.calculateHolidayPay(row.email, new Date(row.holiday_date), 1); const calculated = await holiday_service.calculateHolidayPay(row.employee_matricule, row.compagnie_no, new Date(row.premier_jour_absence));
if (!calculated.success) { if (!calculated.success) {
result.push({ ...row, quantity_hours: 0 }); result.push({ ...row, quantite_hre: 0 });
continue; continue;
} }
result.push({ ...row, quantity_hours: calculated.data }); result.push({ ...row, quantite_hre: calculated.data });
} }
return result; return result;
} }
@ -57,7 +66,7 @@ export const applyOvertimeRequalifications = async (
const grouped_rows = new Map<string, InternalCsvRow[]>(); const grouped_rows = new Map<string, InternalCsvRow[]>();
for (const row of consolidated_rows) { 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)) { if (!grouped_rows.has(key)) {
grouped_rows.set(key, []); grouped_rows.set(key, []);
} }
@ -75,19 +84,19 @@ export const applyOvertimeRequalifications = async (
const overtime_hours = summary.data.total_overtime; const overtime_hours = summary.data.total_overtime;
const regular_hours = rows.find(r => r.bank_code === REGULAR); const regular_hours = rows.find(r => r.code === REGULAR);
if (!regular_hours || !regular_hours.quantity_hours) { if (!regular_hours || !regular_hours.quantite_hre) {
result.push(...rows); result.push(...rows);
continue; 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) { for (const row of rows) {
if (row === regular_hours) { if (row === regular_hours) {
const remaining = regular_hours.quantity_hours - deducted; const remaining = regular_hours.quantite_hre - deducted;
if (remaining > 0) { if (remaining > 0) {
result.push({ ...regular_hours, quantity_hours: remaining }); result.push({ ...regular_hours, quantite_hre: remaining });
} }
} else { } else {
result.push(row); result.push(row);
@ -96,12 +105,10 @@ export const applyOvertimeRequalifications = async (
result.push({ result.push({
...regular_hours, ...regular_hours,
bank_code: OVERTIME, code: OVERTIME,
quantity_hours: deducted, quantite_hre: deducted,
}); });
} }
// return consolidateRowHoursAndAmountByType(result);
return result; return result;
} }

View File

@ -47,18 +47,25 @@ export class ExportCsvOptionsDto {
} }
export interface CsvRow { export interface CsvRow {
company_code: number; compagnie_no: number;
external_payroll_id: number; employee_matricule: number;
full_name: string; releve: number;
bank_code: string; type_transaction: string;
quantity_hours?: number; code: number;
amount?: number; quantite_hre: number | undefined;
week_number: number; taux_horaire: string | undefined;
pay_date: string; montant: number | undefined;
holiday_date?: string; semaine_no: number;
division_no: number | undefined;
service_no: number | undefined;
departem_no: number | undefined;
sous_departem_no: number | undefined;
date_transaction: string;
premier_jour_absence: string | undefined;
dernier_jour_absence: string | undefined;
} }
export type InternalCsvRow = CsvRow & { timesheet_id: number; shift_date: Date; email: string; } export type InternalCsvRow = CsvRow & { timesheet_id: number; shift_date: Date; }
export type Filters = { export type Filters = {

View File

@ -5,34 +5,28 @@ import { CsvRow } from "src/time-and-attendance/exports/export-csv-options.dto";
export class CsvGeneratorService { export class CsvGeneratorService {
//csv builder and "mise en page" //csv builder and "mise en page"
generateCsv(rows: CsvRow[]): Buffer { 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 body = rows.map(row => {
const full_name = `${String(row.full_name).replace(/"/g, '""')}`; const quantity_hours = (typeof row.quantite_hre === 'number') ? row.quantite_hre.toFixed(2) : '';
const quantity_hours = (typeof row.quantity_hours === 'number') ? row.quantity_hours.toFixed(2) : ''; const amount = (typeof row.montant === 'number') ? row.montant.toFixed(2) : '';
const amount = (typeof row.amount === 'number') ? row.amount.toFixed(2) : '';
return [ return [
row.company_code, row.compagnie_no,
row.external_payroll_id, row.employee_matricule,
full_name, row.releve ?? '',
row.bank_code, row.type_transaction,
row.code,
quantity_hours, quantity_hours,
row.taux_horaire ?? '',
amount, amount,
row.week_number, row.semaine_no,
row.pay_date, row.division_no ?? '',
row.holiday_date ?? '', row.service_no ?? '',
row.departem_no ?? '',
row.sous_departem_no ?? '',
row.date_transaction,
row.premier_jour_absence ?? '',
row.dernier_jour_absence ?? '',
].join(';'); ].join(';');
}).join('\n'); }).join('\n');
return Buffer.from('\uFEFF' + header + body, 'utf8'); return Buffer.from('\uFEFF' + body, 'utf8');
} }
} }

View File

@ -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 { 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 { OvertimeService } from "src/time-and-attendance/domains/services/overtime.service";
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 { BillableShiftType } from "src/time-and-attendance/shifts/shift.types";
@Injectable() @Injectable()
export class CsvExportService { export class CsvExportService {
@ -14,13 +16,34 @@ export class CsvExportService {
private readonly holiday_service: HolidayService, private readonly holiday_service: HolidayService,
) { } ) { }
async collectTransaction( /**
year: number, * Gets all approved shifts and expenses within the year and period number provided, according
period_no: number, * to company and types specified in the filters. Any holiday shifts will be adjusted based on
filters: Filters, * prior 4 weeks of work, where applicable. Vacation and Sick shifts will be split into separate
// approved: boolean = true * lines if they are more than one day apart.
): Promise<CsvRow[]> { *
//fetch period * ex: A pay period goes from January 10 to 24. An employee has vacation shifts from
* January 11 to
* 13 and one shift on Jan 23, then two separate lines will be generated; one from Jan 11 to 13
* and one for Jan 23.
*
* @param year the year of the requested pay data
* @param period_no the period number of the requested pay data
* @param filters user-provided input that determines which company and types are exported
* @returns The desired filtered data in semi-colon-separated format, grouped and sorted by
* employee and by bank codes.
*/
async collectTransaction(year: number, period_no: number, filters: Filters): 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 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 },
@ -30,222 +53,110 @@ export class CsvExportService {
const start = period.period_start; const start = period.period_start;
const end = period.period_end; const end = period.period_end;
//fetch company codes
const company_codes = resolveCompanyCodes(filters.companies); const company_codes = 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 if (Object.values(filters.types).every(type => type === false))
const { shifts: want_shifts, expenses: want_expense, holiday: want_holiday, vacation: want_vacation } = filters.types; throw new BadRequestException('NO_TYPE_SELECTED');
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 exportedShifts = await this.prisma.shifts.findMany({
const holiday_code = await this.resolveHolidayTypeCode('HOLIDAY');
const vacation_code = await this.resolveVacationTypeCode('VACATION');
const leave_code_filter = [holiday_code, vacation_code];
//Prisma queries
const promises: Array<Promise<any[]>> = [];
if (want_shifts) {
promises.push(this.prisma.shifts.findMany({
where: { where: {
date: { gte: start, lte: end }, date: { gte: start, lte: end },
...approved_filter, is_approved: true,
bank_code: { bank_code: { notIn: leave_code_filter } },
timesheet: { employee: { company_code: { in: company_codes } } }, timesheet: { employee: { company_code: { in: company_codes } } },
bank_code: { bank_code: { in: BILLABLE_SHIFT_CODES } },
}, },
select: { select: 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,
user: { select: { first_name: true, last_name: true, email: true } },
}
},
}
},
},
}));
} else {
promises.push(Promise.resolve([]));
}
if (want_holiday) { const rows: InternalCsvRow[] = exportedShifts.map(shift => {
promises.push(this.prisma.shifts.findMany({ const employee = shift!.timesheet.employee;
where: { const week = computeWeekNumber(start, shift!.date);
date: { gte: start, lte: end }, const type_transaction = shift!.bank_code.bank_code.charAt(0);
...approved_filter, const code = Number(shift!.bank_code.bank_code.slice(1,));
bank_code: { bank_code: holiday_code }, const isPTO = PTO_SHIFT_CODES.includes(shift.bank_code.bank_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_vacation) {
promises.push(this.prisma.shifts.findMany({
where: {
date: { gte: start, lte: end },
...approved_filter,
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 } },
}
},
}
},
},
}));
} else {
promises.push(Promise.resolve([]));
}
//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);
return { return {
company_code: employee.company_code,
external_payroll_id: employee.external_payroll_id,
timesheet_id: shift.timesheet.id, timesheet_id: shift.timesheet.id,
email: shift.timesheet.employee.user.email,
shift_date: shift.date, shift_date: shift.date,
full_name: `${employee.user.first_name} ${employee.user.last_name}`, compagnie_no: employee.company_code,
bank_code: shift.bank_code?.bank_code ?? '', employee_matricule: employee.external_payroll_id,
quantity_hours: computeHours(shift.start_time, shift.end_time), releve: 0,
amount: undefined, type_transaction: type_transaction,
week_number: week, code: code,
pay_date: formatDate(end), quantite_hre: computeHours(shift!.start_time, shift!.end_time),
holiday_date: is_holiday ? formatDate(shift.date) : '', taux_horaire: '',
montant: undefined,
semaine_no: week,
division_no: undefined,
service_no: undefined,
departem_no: undefined,
sous_departem_no: undefined,
date_transaction: formatDate(end),
premier_jour_absence: isPTO ? formatDate(shift!.date) : '',
dernier_jour_absence: isPTO ? 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));
for (const expense of expenses) { if (filters.types.expenses) {
const exportedExpenses = await 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,
});
exportedExpenses.map(expense => {
const employee = expense.timesheet.employee; 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); const week = computeWeekNumber(start, expense.date);
rows.push({ rows.push({
company_code: employee.company_code,
external_payroll_id: employee.external_payroll_id,
timesheet_id: expense.timesheet.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, 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,
sous_departem_no: undefined,
date_transaction: formatDate(end),
premier_jour_absence: undefined,
dernier_jour_absence: undefined,
});
});
} }
//Final mapping and sorts // Sort shifts and expenses according to their bank codes
rows.sort((a, b) => { rows.sort((a, b) => {
if (a.external_payroll_id !== b.external_payroll_id) { if (a.code !== b.code)
return a.external_payroll_id - b.external_payroll_id; return a.code - b.code;
}
const bank_code = String(a.bank_code).localeCompare(String(b.bank_code));
if (bank_code !== 0) return bank_code;
if (a.bank_code !== b.bank_code) return a.bank_code.localeCompare(b.bank_code);
return 0; return 0;
}); });
const holiday_rows = await applyHolidayRequalifications(rows, this.holiday_service, HOLIDAY_SHIFT_CODE[0]);
//requalifies the real amount paid for holidays
const holiday_rows = await applyHolidayRequalifications(rows, this.holiday_service);
//groups hours by bank_code
const consolidated_rows = await consolidateRowHoursAndAmountByType(holiday_rows); const consolidated_rows = await consolidateRowHoursAndAmountByType(holiday_rows);
//requalifies regular hours into overtime when needed //requalifies regular hours into overtime when needed
const requalified_rows = await applyOvertimeRequalifications(consolidated_rows, this.overtime_service); const requalified_rows = await applyOvertimeRequalifications(consolidated_rows, this.overtime_service);
return requalified_rows; return requalified_rows;
} }
resolveHolidayTypeCode = async (holiday: string): Promise<string> => { resolveShiftTypeCode = async (shift_type: BillableShiftType[]): Promise<string[]> => {
const holiday_code = await this.prisma.bankCodes.findFirst({ const billableBankCodes = await this.prisma.bankCodes.findMany({
where: { type: holiday }, where: { type: { in: shift_type } },
select: { select: {
bank_code: true, bank_code: true,
shifts: { shifts: {
@ -255,25 +166,11 @@ export class CsvExportService {
}, },
}, },
}); });
if (!holiday_code) throw new BadRequestException('Missing Holiday bank code'); if (!billableBankCodes) throw new BadRequestException('Missing Shift bank code');
return holiday_code.bank_code; const shiftCodes: string[] = [];
} billableBankCodes.map(billableBankCode => shiftCodes.push(billableBankCode.bank_code));
return shiftCodes;
resolveVacationTypeCode = async (vacation: string): Promise<string> => {
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;
} }
} }

View File

@ -145,8 +145,14 @@ export class ShiftsCreateService {
if (!result.success) return { success: false, error: result.error }; if (!result.success) return { success: false, error: result.error };
const valid_hours = result.data; const valid_hours = result.data;
const valid_hour_amount = Math.floor(result.data);
const valid_minute_amount = Math.round(60 * (valid_hours - valid_hour_amount));
adjusted_end_time = new Date(normed_shift.data.start_time); adjusted_end_time = new Date(normed_shift.data.start_time);
adjusted_end_time.setHours(adjusted_end_time.getHours() + valid_hours); adjusted_end_time.setHours(
adjusted_end_time.getHours() + valid_hour_amount,
adjusted_end_time.getMinutes() + valid_minute_amount,
);
} }
//sends data for creation of a shift in db //sends data for creation of a shift in db

View File

@ -0,0 +1 @@
export type BillableShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'VACATION' | 'HOLIDAY' | 'SICK' | 'OVERTIME';

View File

@ -70,6 +70,7 @@ export class GetTimesheetsOverviewService {
return { success: true, data: { has_preset_schedule, employee_fullname, timesheets } }; return { success: true, data: { has_preset_schedule, employee_fullname, timesheets } };
} catch (error) { } catch (error) {
console.error(error);
return { success: false, error: 'TIMESHEET_NOT_FOUND' } return { success: false, error: 'TIMESHEET_NOT_FOUND' }
} }
} }

View File

@ -83,3 +83,39 @@ export const select_employee_timesheet = {
}, },
}, },
}; };
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: {
id: true,
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,
}
},
}
},
}