feat(approval): selected approval by bulk for pay-period using crew view. renamed variables to use snake_case

This commit is contained in:
Matthieu Haineault 2025-08-20 15:33:17 -04:00
parent d44f8da99f
commit 3c8c999bfe
26 changed files with 569 additions and 472 deletions

View File

@ -2073,36 +2073,26 @@
] ]
} }
}, },
"/pay-periods/approval/{year}/{periodNumber}": { "/pay-periods/crew/bulk-approval": {
"patch": { "patch": {
"operationId": "PayPeriodsController_approve", "operationId": "PayPeriodsController_bulkApproval",
"parameters": [ "parameters": [],
{ "requestBody": {
"name": "year",
"required": true, "required": true,
"in": "path", "content": {
"application/json": {
"schema": { "schema": {
"example": 2024, "$ref": "#/components/schemas/BulkCrewApprovalDto"
"type": "number" }
}
} }
}, },
{
"name": "periodNumber",
"required": true,
"in": "path",
"description": "1..26",
"schema": {
"example": 1,
"type": "number"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "Pay period approved" "description": "Pay period approved"
} }
}, },
"summary": "Approve all timesheets with activity in the period", "summary": "Approve all selected timesheets in the period",
"tags": [ "tags": [
"pay-periods" "pay-periods"
] ]
@ -3097,6 +3087,10 @@
"periods" "periods"
] ]
}, },
"BulkCrewApprovalDto": {
"type": "object",
"properties": {}
},
"EmployeePeriodOverviewDto": { "EmployeePeriodOverviewDto": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -36,7 +36,7 @@ export abstract class BaseApprovalService<T> {
} }
//approval with transaction to avoid many requests //approval with transaction to avoid many requests
async updateApprovalWithTx(transaction: Prisma.TransactionClient, id: number, isApproved: boolean): Promise<T> { async updateApprovalWithTransaction(transaction: Prisma.TransactionClient, id: number, isApproved: boolean): Promise<T> {
try { try {
return await this.delegateFor(transaction).update({ return await this.delegateFor(transaction).update({
where: { id }, where: { id },

View File

@ -14,15 +14,15 @@ export class AfterHoursService {
private getPreBusinessMinutes(start: Date, end: Date): number { private getPreBusinessMinutes(start: Date, end: Date): number {
const bizStart = new Date(start); const biz_start = new Date(start);
bizStart.setHours(AfterHoursService.BUSINESS_START, 0,0,0); biz_start.setHours(AfterHoursService.BUSINESS_START, 0,0,0);
if (end>= start || start >= bizStart) { if (end>= start || start >= biz_start) {
return 0; return 0;
} }
const segmentEnd = end < bizStart ? end : bizStart; const segment_end = end < biz_start ? end : biz_start;
const minutes = (segmentEnd.getTime() - start.getTime()) / 60000; const minutes = (segment_end.getTime() - start.getTime()) / 60000;
this.logger.debug(`getPreBusinessMintutes -> ${minutes.toFixed(1)}min`); this.logger.debug(`getPreBusinessMintutes -> ${minutes.toFixed(1)}min`);
return minutes; return minutes;
@ -30,15 +30,15 @@ export class AfterHoursService {
} }
private getPostBusinessMinutes(start: Date, end: Date): number { private getPostBusinessMinutes(start: Date, end: Date): number {
const bizEnd = new Date(start); const biz_end = new Date(start);
bizEnd.setHours(AfterHoursService.BUSINESS_END,0,0,0); biz_end.setHours(AfterHoursService.BUSINESS_END,0,0,0);
if( end <= bizEnd ) { if( end <= biz_end ) {
return 0; return 0;
} }
const segmentStart = start > bizEnd ? start : bizEnd; const segment_start = start > biz_end ? start : biz_end;
const minutes = (end.getTime() - segmentStart.getTime()) / 60000; const minutes = (end.getTime() - segment_start.getTime()) / 60000;
this.logger.debug(`getPostBusinessMintutes -> ${minutes.toFixed(1)}min`); this.logger.debug(`getPostBusinessMintutes -> ${minutes.toFixed(1)}min`);
return minutes; return minutes;
@ -62,17 +62,17 @@ export class AfterHoursService {
'You must create 2 instances, one on the first day and the second during the next day.'); 'You must create 2 instances, one on the first day and the second during the next day.');
} }
const preMin = this.getPreBusinessMinutes(start, end); const pre_min = this.getPreBusinessMinutes(start, end);
const postMin = this.getPostBusinessMinutes(start, end); const post_min = this.getPostBusinessMinutes(start, end);
const rawAftermin = preMin + postMin; const raw_aftermin = pre_min + post_min;
const roundedMin = this.roundToNearestQUarterMinute(rawAftermin); const rounded_min = this.roundToNearestQUarterMinute(raw_aftermin);
const hours = roundedMin / 60; const hours = rounded_min / 60;
const result = parseFloat(hours.toFixed(2)); const result = parseFloat(hours.toFixed(2));
this.logger.debug(`computeAfterHours -> rawAfterMin= ${rawAftermin.toFixed(1)}min, + this.logger.debug(`computeAfterHours -> raw_aftermin = ${raw_aftermin.toFixed(1)}min, +
rounded = ${roundedMin}min, hours = ${result.toFixed(2)}`); rounded = ${rounded_min}min, hours = ${result.toFixed(2)}`);
return result; return result;
} }
} }

View File

@ -9,34 +9,34 @@ export class HolidayService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
//switch employeeId for email //switch employeeId for email
private async computeHoursPrevious4Weeks(employeeId: number, holidayDate: Date): Promise<number> { private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise<number> {
//sets the end of the window to 1ms before the week with the holiday //sets the end of the window to 1ms before the week with the holiday
const holidayWeekStart = getWeekStart(holidayDate); const holiday_week_start = getWeekStart(holiday_date);
const windowEnd = new Date(holidayWeekStart.getTime() - 1); const window_end = new Date(holiday_week_start.getTime() - 1);
//sets the start of the window to 28 days ( 4 completed weeks ) before the week with the holiday //sets the start of the window to 28 days ( 4 completed weeks ) before the week with the holiday
const windowStart = new Date(windowEnd.getTime() - 28 * 24 * 60 * 60000 + 1 ) const window_start = new Date(window_end.getTime() - 28 * 24 * 60 * 60000 + 1 )
const validCodes = ['G1', 'G45', 'G56', 'G104', 'G105', 'G700']; const valid_codes = ['G1', 'G45', 'G56', 'G104', 'G105', 'G700'];
//fetches all shift of the employee in said window ( 4 previous completed weeks ) //fetches all shift of the employee in said window ( 4 previous completed weeks )
const shifts = await this.prisma.shifts.findMany({ const shifts = await this.prisma.shifts.findMany({
where: { timesheet: { employee_id: employeeId } , where: { timesheet: { employee_id: employee_id } ,
date: { gte: windowStart, lte: windowEnd }, date: { gte: window_start, lte: window_end },
bank_code: { bank_code: { in: validCodes } }, bank_code: { bank_code: { in: valid_codes } },
}, },
select: { date: true, start_time: true, end_time: true }, select: { date: true, start_time: true, end_time: true },
}); });
const totalHours = shifts.map(s => computeHours(s.start_time, s.end_time)).reduce((sum, h)=> sum + h, 0); const total_hours = shifts.map(s => computeHours(s.start_time, s.end_time)).reduce((sum, h)=> sum + h, 0);
const dailyHours = totalHours / 20; const daily_hours = total_hours / 20;
return dailyHours; return daily_hours;
} }
//switch employeeId for email //switch employeeId for email
async calculateHolidayPay( employeeId: number, holidayDate: Date, modifier: number): Promise<number> { async calculateHolidayPay( employee_id: number, holiday_date: Date, modifier: number): Promise<number> {
const hours = await this.computeHoursPrevious4Weeks(employeeId, holidayDate); const hours = await this.computeHoursPrevious4Weeks(employee_id, holiday_date);
const dailyRate = Math.min(hours, 8); const daily_rate = Math.min(hours, 8);
this.logger.debug(`Holiday pay calculation: hours= ${hours.toFixed(2)}`); this.logger.debug(`Holiday pay calculation: hours= ${hours.toFixed(2)}`);
return dailyRate * modifier; return daily_rate * modifier;
} }
} }

View File

@ -8,28 +8,28 @@ export class MileageService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
public async calculateReimbursement(amount: number, bankCodeId: number): Promise<number> { public async calculateReimbursement(amount: number, bank_code_id: number): Promise<number> {
if(amount < 0) { if(amount < 0) {
throw new BadRequestException(`The amount most be higher than 0`); throw new BadRequestException(`The amount most be higher than 0`);
} }
//fetch modifier //fetch modifier
const bankCode = await this.prisma.bankCodes.findUnique({ const bank_code = await this.prisma.bankCodes.findUnique({
where: { id: bankCodeId }, where: { id: bank_code_id },
select: { modifier: true, type: true }, select: { modifier: true, type: true },
}); });
if(!bankCode) { if(!bank_code) {
throw new BadRequestException(`bank_code ${bankCodeId} not found`); throw new BadRequestException(`bank_code ${bank_code_id} not found`);
} }
if(bankCode.type !== 'mileage') { if(bank_code.type !== 'mileage') {
this.logger.warn(`bank_code ${bankCodeId} of type ${bankCode.type} is used for mileage`) this.logger.warn(`bank_code ${bank_code_id} of type ${bank_code.type} is used for mileage`)
} }
//calculate total amount to reimburs //calculate total amount to reimburs
const reimboursement = amount * bankCode.modifier; const reimboursement = amount * bank_code.modifier;
const result = parseFloat(reimboursement.toFixed(2)); const result = parseFloat(reimboursement.toFixed(2));
this.logger.debug(`calculateReimbursement -> amount= ${amount}, modifier= ${bankCode.modifier}, total= ${result}`); this.logger.debug(`calculateReimbursement -> amount= ${amount}, modifier= ${bank_code.modifier}, total= ${result}`);
return result; return result;
} }

View File

@ -6,29 +6,29 @@ import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-ut
export class OvertimeService { export class OvertimeService {
private logger = new Logger(OvertimeService.name); private logger = new Logger(OvertimeService.name);
private dailyMax = 8; // maximum for regular hours per day private daily_max = 12; // maximum for regular hours per day
private weeklyMax = 40; //maximum for regular hours per week private weekly_max = 80; //maximum for regular hours per week
constructor(private prisma: PrismaService) {} constructor(private prisma: PrismaService) {}
//calculate Daily overtime //calculate Daily overtime
getDailyOvertimeHours(start: Date, end: Date): number { getDailyOvertimeHours(start: Date, end: Date): number {
const hours = computeHours(start, end, 5); const hours = computeHours(start, end, 5);
const overtime = Math.max(0, hours - this.dailyMax); const overtime = Math.max(0, hours - this.daily_max);
this.logger.debug(`getDailyOvertimeHours : ${overtime.toFixed(2)}h (threshold ${this.dailyMax})`); this.logger.debug(`getDailyOvertimeHours : ${overtime.toFixed(2)}h (threshold ${this.daily_max})`);
return overtime; return overtime;
} }
//calculate Weekly overtime //calculate Weekly overtime
//switch employeeId for email //switch employeeId for email
async getWeeklyOvertimeHours(employeeId: number, refDate: Date): Promise<number> { async getWeeklyOvertimeHours(employeeId: number, refDate: Date): Promise<number> {
const weekStart = getWeekStart(refDate); const week_start = getWeekStart(refDate);
const weekEnd = getWeekEnd(weekStart); const week_end = getWeekEnd(week_start);
//fetches all shifts containing hours //fetches all shifts containing hours
const shifts = await this.prisma.shifts.findMany({ const shifts = await this.prisma.shifts.findMany({
where: { timesheet: { employee_id: employeeId, shift: { where: { timesheet: { employee_id: employeeId, shift: {
every: {date: { gte: weekStart, lte: weekEnd } } every: {date: { gte: week_start, lte: week_end } }
}, },
}, },
}, },
@ -38,16 +38,16 @@ export class OvertimeService {
//calculate total hours of those shifts minus weekly Max to find total overtime hours //calculate total hours of those shifts minus weekly Max to find total overtime hours
const total = shifts.map(shift => computeHours(shift.start_time, shift.end_time, 5)) const total = shifts.map(shift => computeHours(shift.start_time, shift.end_time, 5))
.reduce((sum, hours)=> sum+hours, 0); .reduce((sum, hours)=> sum+hours, 0);
const overtime = Math.max(0, total - this.weeklyMax); const overtime = Math.max(0, total - this.weekly_max);
this.logger.debug(`weekly total = ${total.toFixed(2)}h, weekly Overtime= ${overtime.toFixed(2)}h`); this.logger.debug(`weekly total = ${total.toFixed(2)}h, weekly Overtime= ${overtime.toFixed(2)}h`);
return overtime; return overtime;
} }
//apply modifier to overtime hours //apply modifier to overtime hours
calculateOvertimePay(overtimeHours: number, modifier: number): number { calculateOvertimePay(overtime_hours: number, modifier: number): number {
const pay = overtimeHours * modifier; const pay = overtime_hours * modifier;
this.logger.debug(`Overtime payable hours = ${pay.toFixed(2)} (hours ${overtimeHours}, modifier ${modifier})`); this.logger.debug(`Overtime payable hours = ${pay.toFixed(2)} (hours ${overtime_hours}, modifier ${modifier})`);
return pay; return pay;
} }

View File

@ -9,55 +9,58 @@ export class SickLeaveService {
private readonly logger = new Logger(SickLeaveService.name); private readonly logger = new Logger(SickLeaveService.name);
//switch employeeId for email //switch employeeId for email
async calculateSickLeavePay(employeeId: number, referenceDate: Date, daysRequested: number, modifier: number): Promise<number> { async calculateSickLeavePay(employee_id: number, reference_date: Date, days_requested: number, modifier: number):
Promise<number> {
//sets the year to jan 1st to dec 31st //sets the year to jan 1st to dec 31st
const periodStart = getYearStart(referenceDate); const period_start = getYearStart(reference_date);
const periodEnd = referenceDate; const period_end = reference_date;
//fetches all shifts of a selected employee //fetches all shifts of a selected employee
const shifts = await this.prisma.shifts.findMany({ const shifts = await this.prisma.shifts.findMany({
where: { where: {
timesheet: { employee_id: employeeId }, timesheet: { employee_id: employee_id },
date: { gte: periodStart, lte: periodEnd}, date: { gte: period_start, lte: period_end},
}, },
select: { date: true }, select: { date: true },
}); });
//count the amount of worked days //count the amount of worked days
const workedDates = new Set( const worked_dates = new Set(
shifts.map(shift => shift.date.toISOString().slice(0,10)) shifts.map(shift => shift.date.toISOString().slice(0,10))
); );
const daysWorked = workedDates.size; const days_worked = worked_dates.size;
this.logger.debug(`Sick leave: days worked= ${daysWorked} in ${periodStart.toDateString()} -> ${periodEnd.toDateString()}`); this.logger.debug(`Sick leave: days worked= ${days_worked} in ${period_start.toDateString()}
-> ${period_end.toDateString()}`);
//less than 30 worked days returns 0 //less than 30 worked days returns 0
if (daysWorked < 30) { if (days_worked < 30) {
return 0; return 0;
} }
//default 3 days allowed after 30 worked days //default 3 days allowed after 30 worked days
let acquiredDays = 3; let acquired_days = 3;
//identify the date of the 30th worked day //identify the date of the 30th worked day
const orderedDates = Array.from(workedDates).sort(); const ordered_dates = Array.from(worked_dates).sort();
const thresholdDate = new Date(orderedDates[29]); // index 29 is the 30th day const threshold_date = new Date(ordered_dates[29]); // index 29 is the 30th day
//calculate each completed month, starting the 1st of the next month //calculate each completed month, starting the 1st of the next month
const firstBonusDate = new Date(thresholdDate.getFullYear(), thresholdDate.getMonth() +1, 1); const first_bonus_date = new Date(threshold_date.getFullYear(), threshold_date.getMonth() +1, 1);
let months = (periodEnd.getFullYear() - firstBonusDate.getFullYear()) * 12 + let months = (period_end.getFullYear() - first_bonus_date.getFullYear()) * 12 +
(periodEnd.getMonth() - firstBonusDate.getMonth()) + 1; (period_end.getMonth() - first_bonus_date.getMonth()) + 1;
if(months < 0) months = 0; if(months < 0) months = 0;
acquiredDays += months; acquired_days += months;
//cap of 10 days //cap of 10 days
if (acquiredDays > 10) acquiredDays = 10; if (acquired_days > 10) acquired_days = 10;
this.logger.debug(`Sick leave: threshold Date = ${thresholdDate.toDateString()}, bonusMonths = ${months}, acquired Days = ${acquiredDays}`); this.logger.debug(`Sick leave: threshold Date = ${threshold_date.toDateString()}
, bonusMonths = ${months}, acquired Days = ${acquired_days}`);
const payableDays = Math.min(acquiredDays, daysRequested); const payable_days = Math.min(acquired_days, days_requested);
const rawHours = payableDays * 8 * modifier; const raw_hours = payable_days * 8 * modifier;
const rounded = roundToQuarterHour(rawHours) const rounded = roundToQuarterHour(raw_hours)
this.logger.debug(`Sick leave pay: days= ${payableDays}, modifier= ${modifier}, hours= ${rounded}`); this.logger.debug(`Sick leave pay: days= ${payable_days}, modifier= ${modifier}, hours= ${rounded}`);
return rounded; return rounded;
} }
} }

View File

@ -9,76 +9,76 @@ export class VacationService {
/** /**
* Calculate the ammount allowed for vacation days. * Calculate the ammount allowed for vacation days.
* *
* @param employeeId employee ID * @param employee_id employee ID
* @param startDate first day of vacation * @param startDate first day of vacation
* @param daysRequested number of days requested * @param daysRequested number of days requested
* @param modifier Coefficient of hours(1) * @param modifier Coefficient of hours(1)
* @returns amount of payable hours * @returns amount of payable hours
*/ */
//switch employeeId for email //switch employeeId for email
async calculateVacationPay(employeeId: number, startDate: Date, daysRequested: number, modifier: number): Promise<number> { async calculateVacationPay(employee_id: number, start_date: Date, days_requested: number, modifier: number): Promise<number> {
//fetch hiring date //fetch hiring date
const employee = await this.prisma.employees.findUnique({ const employee = await this.prisma.employees.findUnique({
where: { id: employeeId }, where: { id: employee_id },
select: { first_work_day: true }, select: { first_work_day: true },
}); });
if(!employee) { if(!employee) {
throw new NotFoundException(`Employee #${employeeId} not found`); throw new NotFoundException(`Employee #${employee_id} not found`);
} }
const hireDate = employee.first_work_day; const hire_date = employee.first_work_day;
//sets "year" to may 1st to april 30th //sets "year" to may 1st to april 30th
//check if hiring date is in may or later, we use hiring year, otherwise we use the year before //check if hiring date is in may or later, we use hiring year, otherwise we use the year before
const yearOfRequest = startDate.getMonth() >= 4 const year_of_request = start_date.getMonth() >= 4
? startDate.getFullYear() : startDate.getFullYear() -1; ? start_date.getFullYear() : start_date.getFullYear() -1;
const periodStart = new Date(yearOfRequest, 4, 1); //may = 4 const period_start = new Date(year_of_request, 4, 1); //may = 4
const periodEnd = new Date(yearOfRequest + 1, 4, 0); //day 0 of may == april 30th const period_end = new Date(year_of_request + 1, 4, 0); //day 0 of may == april 30th
this.logger.debug(`Vacation period for request: ${periodStart.toDateString()} -> ${periodEnd.toDateString()}`); this.logger.debug(`Vacation period for request: ${period_start.toDateString()} -> ${period_end.toDateString()}`);
//steps to reach to get more vacation weeks in years //steps to reach to get more vacation weeks in years
const checkpoint = [5, 10, 15]; const checkpoint = [5, 10, 15];
const anniversaries = checkpoint.map(years => { const anniversaries = checkpoint.map(years => {
const anniversaryDate = new Date(hireDate); const anniversary_date = new Date(hire_date);
anniversaryDate.setFullYear(anniversaryDate.getFullYear() + years); anniversary_date.setFullYear(anniversary_date.getFullYear() + years);
return anniversaryDate; return anniversary_date;
}).filter(d => d>= periodStart && d <= periodEnd).sort((a,b) => a.getTime() - b.getTime()); }).filter(d => d>= period_start && d <= period_end).sort((a,b) => a.getTime() - b.getTime());
this.logger.debug(`anniversatries steps during the period: ${anniversaries.map(date => date.toDateString()).join(',') || 'aucun'}`); this.logger.debug(`anniversatries steps during the period: ${anniversaries.map(date => date.toDateString()).join(',') || 'aucun'}`);
const boundaries = [periodStart, ...anniversaries,periodEnd]; const boundaries = [period_start, ...anniversaries,period_end];
//calculate prorata per segment //calculate prorata per segment
let totalVacationDays = 0; let total_vacation_days = 0;
const msPerDay = 1000 * 60 * 60 * 24; const ms_per_day = 1000 * 60 * 60 * 24;
for (let i = 0; i < boundaries.length -1; i++) { for (let i = 0; i < boundaries.length -1; i++) {
const segmentStart = boundaries[i]; const segment_start = boundaries[i];
const segmentEnd = boundaries[i+1]; const segment_end = boundaries[i+1];
//number of days in said segment //number of days in said segment
const daysInSegment = Math.round((segmentEnd.getTime() - segmentStart.getTime())/ msPerDay); const days_in_segment = Math.round((segment_end.getTime() - segment_start.getTime())/ ms_per_day);
const yearsSinceHire = (segmentStart.getFullYear() - hireDate.getFullYear()) - const years_since_hire = (segment_start.getFullYear() - hire_date.getFullYear()) -
(segmentStart < new Date(segmentStart.getFullYear(), hireDate.getMonth()) ? 1 : 0); (segment_start < new Date(segment_start.getFullYear(), hire_date.getMonth()) ? 1 : 0);
let allocDays: number; let alloc_days: number;
if(yearsSinceHire < 5) allocDays = 10; if(years_since_hire < 5) alloc_days = 10;
else if(yearsSinceHire < 10) allocDays = 15; else if(years_since_hire < 10) alloc_days = 15;
else if(yearsSinceHire < 15) allocDays = 20; else if(years_since_hire < 15) alloc_days = 20;
else allocDays = 25; else alloc_days = 25;
//prorata for said segment //prorata for said segment
const prorata = (allocDays / 365) * daysInSegment; const prorata = (alloc_days / 365) * days_in_segment;
totalVacationDays += prorata; total_vacation_days += prorata;
} }
//compares allowed vacation pools with requested days //compares allowed vacation pools with requested days
const payableDays = Math.min(totalVacationDays, daysRequested); const payable_days = Math.min(total_vacation_days, days_requested);
const rawHours = payableDays * 8 * modifier; const raw_hours = payable_days * 8 * modifier;
const roundedHours = Math.round(rawHours * 4) / 4; const rounded_hours = Math.round(raw_hours * 4) / 4;
this.logger.debug(`Vacation pay: entitledDays=${totalVacationDays.toFixed(2)}, requestedDays=${daysRequested}, this.logger.debug(`Vacation pay: entitledDays=${total_vacation_days.toFixed(2)}, requestedDays=${days_requested},
payableDays=${payableDays.toFixed(2)}, hours=${roundedHours}`); payableDays=${payable_days.toFixed(2)}, hours=${rounded_hours}`);
return roundedHours; return rounded_hours;
} }

View File

@ -18,8 +18,8 @@ export class CustomersService {
invoice_id, invoice_id,
} = dto; } = dto;
return this.prisma.$transaction(async (tx) => { return this.prisma.$transaction(async (transaction) => {
const user: Users = await tx.users.create({ const user: Users = await transaction.users.create({
data: { data: {
first_name, first_name,
last_name, last_name,
@ -28,7 +28,7 @@ export class CustomersService {
residence, residence,
}, },
}); });
return tx.customers.create({ return transaction.customers.create({
data: { data: {
user_id: user.id, user_id: user.id,
invoice_id, invoice_id,
@ -69,8 +69,8 @@ async update(
invoice_id, invoice_id,
} = dto; } = dto;
return this.prisma.$transaction(async (tx) => { return this.prisma.$transaction(async (transaction) => {
await tx.users.update({ await transaction.users.update({
where: { id: customer.user_id }, where: { id: customer.user_id },
data: { data: {
...(first_name !== undefined && { first_name }), ...(first_name !== undefined && { first_name }),
@ -81,7 +81,7 @@ async update(
}, },
}); });
return tx.customers.update({ return transaction.customers.update({
where: { id }, where: { id },
data: { data: {
...(invoice_id !== undefined && { invoice_id }), ...(invoice_id !== undefined && { invoice_id }),

View File

@ -8,12 +8,12 @@ import { EmployeeProfileItemDto } from '../dtos/profil-employee.dto';
function toDateOrNull(v?: string | null): Date | null { function toDateOrNull(v?: string | null): Date | null {
if (!v) return null; if (!v) return null;
const d = new Date(v); const day = new Date(v);
return isNaN(d.getTime()) ? null : d; return isNaN(day.getTime()) ? null : day;
} }
function toDateOrUndefined(v?: string | null): Date | undefined { function toDateOrUndefined(v?: string | null): Date | undefined {
const d = toDateOrNull(v ?? undefined); const day = toDateOrNull(v ?? undefined);
return d === null ? undefined : d; return day === null ? undefined : day;
} }
@Injectable() @Injectable()
@ -35,8 +35,8 @@ export class EmployeesService {
is_supervisor, is_supervisor,
} = dto; } = dto;
return this.prisma.$transaction(async (tx) => { return this.prisma.$transaction(async (transaction) => {
const user: Users = await tx.users.create({ const user: Users = await transaction.users.create({
data: { data: {
first_name, first_name,
last_name, last_name,
@ -45,7 +45,7 @@ export class EmployeesService {
residence, residence,
}, },
}); });
return tx.employees.create({ return transaction.employees.create({
data: { data: {
user_id: user.id, user_id: user.id,
external_payroll_id, external_payroll_id,
@ -176,18 +176,18 @@ export class EmployeesService {
first_work_day, first_work_day,
last_work_day, last_work_day,
is_supervisor, is_supervisor,
email: newEmail, email: new_email,
} = dto; } = dto;
return this.prisma.$transaction(async (tx) => { return this.prisma.$transaction(async (transaction) => {
if( if(
first_name !== undefined || first_name !== undefined ||
last_name !== undefined || last_name !== undefined ||
newEmail !== undefined || new_email !== undefined ||
phone_number !== undefined || phone_number !== undefined ||
residence !== undefined residence !== undefined
){ ){
await tx.users.update({ await transaction.users.update({
where: { id: emp.user_id }, where: { id: emp.user_id },
data: { data: {
...(first_name !== undefined && { first_name }), ...(first_name !== undefined && { first_name }),
@ -199,7 +199,7 @@ export class EmployeesService {
}); });
} }
const updated = await tx.employees.update({ const updated = await transaction.employees.update({
where: { id: emp.id }, where: { id: emp.id },
data: { data: {
...(external_payroll_id !== undefined && { external_payroll_id }), ...(external_payroll_id !== undefined && { external_payroll_id }),
@ -219,18 +219,18 @@ export class EmployeesService {
const emp = await this.findOne(email); const emp = await this.findOne(email);
return this.prisma.$transaction(async (tx) => { return this.prisma.$transaction(async (transaction) => {
await tx.employees.updateMany({ await transaction.employees.updateMany({
where: { supervisor_id: emp.id }, where: { supervisor_id: emp.id },
data: { supervisor_id: null }, data: { supervisor_id: null },
}); });
const deletedEmployee = await tx.employees.delete({ const deleted_employee = await transaction.employees.delete({
where: {id: emp.id }, where: {id: emp.id },
}); });
await tx.users.delete({ await transaction.users.delete({
where: { id: emp.user_id }, where: { id: emp.user_id },
}); });
return deletedEmployee; return deleted_employee;
}); });
} }
@ -254,7 +254,7 @@ async patchEmployee(email: string, dto: UpdateEmployeeDto): Promise<Employees |
const { const {
first_name, first_name,
last_name, last_name,
email: newEmail, email: new_email,
phone_number, phone_number,
residence, residence,
external_payroll_id, external_payroll_id,
@ -271,20 +271,20 @@ async patchEmployee(email: string, dto: UpdateEmployeeDto): Promise<Employees |
? toDateOrNull(last_work_day ?? null) ? toDateOrNull(last_work_day ?? null)
: undefined; : undefined;
await this.prisma.$transaction(async (tx) => { await this.prisma.$transaction(async (transaction) => {
if( if(
first_name !== undefined || first_name !== undefined ||
last_name !== undefined || last_name !== undefined ||
newEmail !== undefined || new_email !== undefined ||
phone_number !== undefined || phone_number !== undefined ||
residence !== undefined residence !== undefined
) { ) {
await tx.users.update({ await transaction.users.update({
where: { id: active.user_id }, where: { id: active.user_id },
data: { data: {
...(first_name !== undefined ? { first_name } : {}), ...(first_name !== undefined ? { first_name } : {}),
...(last_name !== undefined ? { last_name } : {}), ...(last_name !== undefined ? { last_name } : {}),
...(email !== undefined ? { email: newEmail }: {}), ...(email !== undefined ? { email: new_email }: {}),
...(phone_number !== undefined ? { phone_number } : {}), ...(phone_number !== undefined ? { phone_number } : {}),
...(residence !== undefined ? { residence } : {}), ...(residence !== undefined ? { residence } : {}),
}, },
@ -292,7 +292,7 @@ async patchEmployee(email: string, dto: UpdateEmployeeDto): Promise<Employees |
} }
const updated = await tx.employees.update({ const updated = await transaction.employees.update({
where: { id: active.id }, where: { id: active.id },
data: { data: {
...(external_payroll_id !== undefined ? { external_payroll_id } : {}), ...(external_payroll_id !== undefined ? { external_payroll_id } : {}),
@ -366,7 +366,7 @@ async patchEmployee(email: string, dto: UpdateEmployeeDto): Promise<Employees |
//transfers the employee from archive to the employees table //transfers the employee from archive to the employees table
private async restoreEmployee(archived: EmployeesArchive & { user:Users }, dto: UpdateEmployeeDto): Promise<Employees> { private async restoreEmployee(archived: EmployeesArchive & { user:Users }, dto: UpdateEmployeeDto): Promise<Employees> {
const first_work_d = toDateOrUndefined(dto.first_work_day); // const first_work_d = toDateOrUndefined(dto.first_work_day);
return this.prisma.$transaction(async transaction => { return this.prisma.$transaction(async transaction => {
//restores the archived employee into the employees table //restores the archived employee into the employees table
const restored = await transaction.employees.create({ const restored = await transaction.employees.create({

View File

@ -19,7 +19,7 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
async updateApproval(id: number, isApproved: boolean): Promise<Expenses> { async updateApproval(id: number, isApproved: boolean): Promise<Expenses> {
return this.prisma.$transaction((transaction) => return this.prisma.$transaction((transaction) =>
this.updateApprovalWithTx(transaction, id, isApproved), this.updateApprovalWithTransaction(transaction, id, isApproved),
); );
} }

View File

@ -20,24 +20,24 @@ export class ExpensesQueryService {
//fetches type and modifier //fetches type and modifier
const bankCode = await this.prisma.bankCodes.findUnique({ const bank_code = await this.prisma.bankCodes.findUnique({
where: { id: bank_code_id }, where: { id: bank_code_id },
select: { type: true, modifier: true }, select: { type: true, modifier: true },
}); });
if(!bankCode) { if(!bank_code) {
throw new NotFoundException(`bank_code #${bank_code_id} not found`) throw new NotFoundException(`bank_code #${bank_code_id} not found`)
} }
//if mileage -> service, otherwise the ratio is amount:1 //if mileage -> service, otherwise the ratio is amount:1
let finalAmount: number; let final_amount: number;
if(bankCode.type === 'mileage') { if(bank_code.type === 'mileage') {
finalAmount = await this.mileageService.calculateReimbursement(rawAmount, bank_code_id); final_amount = await this.mileageService.calculateReimbursement(rawAmount, bank_code_id);
}else { }else {
finalAmount = parseFloat( (rawAmount * bankCode.modifier).toFixed(2)); final_amount = parseFloat( (rawAmount * bank_code.modifier).toFixed(2));
} }
return this.prisma.expenses.create({ return this.prisma.expenses.create({
data: { timesheet_id, bank_code_id, date, amount: finalAmount, description, is_approved, supervisor_comment}, data: { timesheet_id, bank_code_id, date, amount: final_amount, description, is_approved, supervisor_comment},
include: { timesheet: { include: { employee: { include: { user: true }}}}, include: { timesheet: { include: { employee: { include: { user: true }}}},
bank_code: true, bank_code: true,
}, },
@ -94,28 +94,28 @@ export class ExpensesQueryService {
async archiveOld(): Promise<void> { async archiveOld(): Promise<void> {
//fetches archived timesheet's Ids //fetches archived timesheet's Ids
const archivedTimesheets = await this.prisma.timesheetsArchive.findMany({ const archived_timesheets = await this.prisma.timesheetsArchive.findMany({
select: { timesheet_id: true }, select: { timesheet_id: true },
}); });
const timesheetIds = archivedTimesheets.map(sheet => sheet.timesheet_id); const timesheet_ids = archived_timesheets.map(sheet => sheet.timesheet_id);
if(timesheetIds.length === 0) { if(timesheet_ids.length === 0) {
return; return;
} }
// copy/delete transaction // copy/delete transaction
await this.prisma.$transaction(async transaction => { await this.prisma.$transaction(async transaction => {
//fetches expenses to move to archive //fetches expenses to move to archive
const expensesToArchive = await transaction.expenses.findMany({ const expenses_to_archive = await transaction.expenses.findMany({
where: { timesheet_id: { in: timesheetIds } }, where: { timesheet_id: { in: timesheet_ids } },
}); });
if(expensesToArchive.length === 0) { if(expenses_to_archive.length === 0) {
return; return;
} }
//copies sent to archive table //copies sent to archive table
await transaction.expensesArchive.createMany({ await transaction.expensesArchive.createMany({
data: expensesToArchive.map(exp => ({ data: expenses_to_archive.map(exp => ({
expense_id: exp.id, expense_id: exp.id,
timesheet_id: exp.timesheet_id, timesheet_id: exp.timesheet_id,
bank_code_id: exp.bank_code_id, bank_code_id: exp.bank_code_id,
@ -130,7 +130,7 @@ export class ExpensesQueryService {
//delete from expenses table //delete from expenses table
await transaction.expenses.deleteMany({ await transaction.expenses.deleteMany({
where: { id: { in: expensesToArchive.map(exp => exp.id) } }, where: { id: { in: expenses_to_archive.map(exp => exp.id) } },
}) })
}) })

View File

@ -63,8 +63,8 @@ export class LeaveRequestController {
@Patch(':id/approval') @Patch(':id/approval')
updateApproval( @Param('id', ParseIntPipe) id: number, updateApproval( @Param('id', ParseIntPipe) id: number,
@Body('is_approved', ParseBoolPipe) isApproved: boolean): Promise<LeaveRequests> { @Body('is_approved', ParseBoolPipe) is_approved: boolean): Promise<LeaveRequests> {
const approvalStatus = isApproved ? const approvalStatus = is_approved ?
LeaveApprovalStatus.APPROVED : LeaveApprovalStatus.DENIED; LeaveApprovalStatus.APPROVED : LeaveApprovalStatus.DENIED;
return this.leaveRequetsService.update(id, { approval_status: approvalStatus }); return this.leaveRequetsService.update(id, { approval_status: approvalStatus });
} }

View File

@ -33,8 +33,8 @@ export class LeaveRequestsService {
} }
async findAll(filters: SearchLeaveRequestsDto): Promise<any[]> { async findAll(filters: SearchLeaveRequestsDto): Promise<any[]> {
const {start_date, end_date, ...otherFilters } = filters; const {start_date, end_date, ...other_filters } = filters;
const where: Record<string, any> = buildPrismaWhere(otherFilters); const where: Record<string, any> = buildPrismaWhere(other_filters);
if (start_date) { if (start_date) {
where.start_date_time = { ...(where.start_date_time ?? {}), gte: new Date(start_date) }; where.start_date_time = { ...(where.start_date_time ?? {}), gte: new Date(start_date) };
@ -50,15 +50,15 @@ export class LeaveRequestsService {
}, },
}); });
const msPerDay = 1000 * 60 * 60 * 24; const ms_per_day = 1000 * 60 * 60 * 24;
return Promise.all( return Promise.all(
list.map(async request => { list.map(async request => {
// end_date fallback // end_date fallback
const endDate = request.end_date_time ?? request.start_date_time; const end_date = request.end_date_time ?? request.start_date_time;
//Requested days //Requested days
const diffDays = Math.round((endDate.getTime() - request.start_date_time.getTime()) / msPerDay) +1; const diff_days = Math.round((end_date.getTime() - request.start_date_time.getTime()) / ms_per_day) +1;
// modifier fallback/validation // modifier fallback/validation
if (!request.bank_code || request.bank_code.modifier == null) { if (!request.bank_code || request.bank_code.modifier == null) {
@ -72,15 +72,15 @@ export class LeaveRequestsService {
cost = await this.holidayService.calculateHolidayPay( request.employee_id, request.start_date_time, modifier ); cost = await this.holidayService.calculateHolidayPay( request.employee_id, request.start_date_time, modifier );
break; break;
case 'vacation' : case 'vacation' :
cost = await this.vacationService.calculateVacationPay( request.employee_id, request.start_date_time,diffDays, modifier ); cost = await this.vacationService.calculateVacationPay( request.employee_id, request.start_date_time,diff_days, modifier );
break; break;
case 'sick' : case 'sick' :
cost = await this.sickLeaveService.calculateSickLeavePay( request.employee_id, request.start_date_time, diffDays, modifier ); cost = await this.sickLeaveService.calculateSickLeavePay( request.employee_id, request.start_date_time, diff_days, modifier );
break; break;
default: default:
cost = diffDays * modifier; cost = diff_days * modifier;
} }
return {...request, daysRequested: diffDays, cost }; return {...request, days_requested: diff_days, cost };
}) })
); );
} }
@ -96,11 +96,11 @@ export class LeaveRequestsService {
throw new NotFoundException(`LeaveRequest #${id} not found`); throw new NotFoundException(`LeaveRequest #${id} not found`);
} }
//validation and fallback for end_date_time //validation and fallback for end_date_time
const endDate = request.end_date_time ?? request.start_date_time; const end_Date = request.end_date_time ?? request.start_date_time;
//calculate included days //calculate included days
const msPerDay = 1000 * 60 * 60 * 24; const msPerDay = 1000 * 60 * 60 * 24;
const diffDays = Math.floor((endDate.getTime() - request.start_date_time.getTime())/ msPerDay) + 1; const diff_days = Math.floor((end_Date.getTime() - request.start_date_time.getTime())/ msPerDay) + 1;
if (!request.bank_code || request.bank_code.modifier == null) { if (!request.bank_code || request.bank_code.modifier == null) {
throw new BadRequestException(`Modifier missing for code ${request.bank_code_id}`); throw new BadRequestException(`Modifier missing for code ${request.bank_code_id}`);
@ -108,27 +108,24 @@ export class LeaveRequestsService {
const modifier = request.bank_code.modifier; const modifier = request.bank_code.modifier;
//calculate cost based on bank_code types //calculate cost based on bank_code types
let cost = diffDays * modifier; let cost = diff_days * modifier;
switch(request.bank_code.type) { switch(request.bank_code.type) {
case 'holiday': case 'holiday':
cost = await this.holidayService.calculateHolidayPay( request.employee_id, request.start_date_time, modifier ); cost = await this.holidayService.calculateHolidayPay( request.employee_id, request.start_date_time, modifier );
break; break;
case 'vacation': case 'vacation':
cost = await this.vacationService.calculateVacationPay( request.employee_id, request.start_date_time, diffDays, modifier ); cost = await this.vacationService.calculateVacationPay( request.employee_id, request.start_date_time, diff_days, modifier );
break; break;
case 'sick': case 'sick':
cost = await this.sickLeaveService.calculateSickLeavePay( request.employee_id, request.start_date_time, diffDays, modifier ); cost = await this.sickLeaveService.calculateSickLeavePay( request.employee_id, request.start_date_time, diff_days, modifier );
break; break;
default: default:
cost = diffDays * modifier; cost = diff_days * modifier;
} }
return {...request, daysRequested: diffDays, cost }; return {...request, days_requested: diff_days, cost };
} }
async update( async update(id: number, dto: UpdateLeaveRequestsDto): Promise<LeaveRequests> {
id: number,
dto: UpdateLeaveRequestsDto,
): Promise<LeaveRequests> {
await this.findOne(id); await this.findOne(id);
const { employee_id, leave_type, start_date_time, end_date_time, comment, approval_status } = dto; const { employee_id, leave_type, start_date_time, end_date_time, comment, approval_status } = dto;
return this.prisma.leaveRequests.update({ return this.prisma.leaveRequests.update({

View File

@ -11,47 +11,47 @@ export class NotificationsService {
private buffers = new Map<string, NotificationCard[]>(); private buffers = new Map<string, NotificationCard[]>();
private readonly BUFFER_MAX = Number(process.env.NOTIF_BUFFER_MAX ?? 50); private readonly BUFFER_MAX = Number(process.env.NOTIF_BUFFER_MAX ?? 50);
private getOrCreateStream(userId: string): Subject<NotificationCard> { private getOrCreateStream(user_id: string): Subject<NotificationCard> {
let stream = this.streams.get(userId); let stream = this.streams.get(user_id);
if (!stream){ if (!stream){
stream = new Subject<NotificationCard>(); stream = new Subject<NotificationCard>();
this.streams.set(userId, stream); this.streams.set(user_id, stream);
} }
return stream; return stream;
} }
private getOrCreateBuffer(userId: string){ private getOrCreateBuffer(user_id: string){
let buffer = this.buffers.get(userId); let buffer = this.buffers.get(user_id);
if(!buffer) { if(!buffer) {
buffer = []; buffer = [];
this.buffers.set(userId, buffer); this.buffers.set(user_id, buffer);
} }
return buffer; return buffer;
} }
//in-app pushes and keep a small history //in-app pushes and keep a small history
notify(userId: string, card: NotificationCard) { notify(user_id: string, card: NotificationCard) {
const buffer = this.getOrCreateBuffer(userId); const buffer = this.getOrCreateBuffer(user_id);
buffer.unshift(card); buffer.unshift(card);
if (buffer.length > this.BUFFER_MAX) { if (buffer.length > this.BUFFER_MAX) {
buffer.length = this.BUFFER_MAX; buffer.length = this.BUFFER_MAX;
} }
this.getOrCreateStream(userId).next(card); this.getOrCreateStream(user_id).next(card);
this.logger.debug(`Notification in-app => user: ${userId} (${card.type})`); this.logger.debug(`Notification in-app => user: ${user_id} (${card.type})`);
} }
//SSE flux for current user //SSE flux for current user
stream(userId: string) { stream(user_id: string) {
return this.getOrCreateStream(userId).asObservable(); return this.getOrCreateStream(user_id).asObservable();
} }
//return a summary of notifications kept in memory //return a summary of notifications kept in memory
async summary(userId: string): Promise<NotificationCard[]> { async summary(user_id: string): Promise<NotificationCard[]> {
return this.getOrCreateBuffer(userId); return this.getOrCreateBuffer(user_id);
} }
//clear buffers from memory //clear buffers from memory
clear(userId: string) { clear(user_id: string) {
this.buffers.set(userId, []); this.buffers.set(user_id, []);
} }
onModuleDestroy() { onModuleDestroy() {

View File

@ -1,4 +1,4 @@
import { Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query } from "@nestjs/common"; import { Body, Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query } from "@nestjs/common";
import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger"; import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger";
import { PayPeriodDto } from "../dtos/pay-period.dto"; import { PayPeriodDto } from "../dtos/pay-period.dto";
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto"; import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
@ -7,6 +7,7 @@ import { RolesAllowed } from "src/common/decorators/roles.decorators";
import { Roles as RoleEnum } from '.prisma/client'; import { Roles as RoleEnum } from '.prisma/client';
import { PayPeriodsCommandService } from "../services/pay-periods-command.service"; import { PayPeriodsCommandService } from "../services/pay-periods-command.service";
import { PayPeriodBundleDto } from "../dtos/bundle-pay-period.dto"; import { PayPeriodBundleDto } from "../dtos/bundle-pay-period.dto";
import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto";
@ApiTags('pay-periods') @ApiTags('pay-periods')
@Controller('pay-periods') @Controller('pay-periods')
@ -57,18 +58,12 @@ export class PayPeriodsController {
return this.queryService.findOneByYearPeriod(year, period_no); return this.queryService.findOneByYearPeriod(year, period_no);
} }
@Patch("approval/:year/:periodNumber") @Patch("crew/bulk-approval")
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ApiOperation({ summary: "Approve all timesheets with activity in the period" }) @ApiOperation({ summary: "Approve all selected timesheets in the period" })
@ApiParam({ name: "year", type: Number, example: 2024 })
@ApiParam({ name: "periodNumber", type: Number, example: 1, description: "1..26" })
@ApiResponse({ status: 200, description: "Pay period approved" }) @ApiResponse({ status: 200, description: "Pay period approved" })
async approve( async bulkApproval(@Body() dto: BulkCrewApprovalDto) {
@Param("year", ParseIntPipe) year: number, return this.commandService.bulkApproveCrew(dto);
@Param("periodNumber", ParseIntPipe) period_no: number,
) {
await this.commandService.approvalPayPeriod(year, period_no);
return { message: `Pay-period ${year}-${period_no} approved` };
} }
@Get(':year/:periodNumber/:email') @Get(':year/:periodNumber/:email')

View File

@ -0,0 +1,29 @@
import { Type } from "class-transformer";
import { IsArray, IsBoolean, IsEmail, IsInt, IsOptional, ValidateNested } from "class-validator";
export class BulkCrewApprovalItemDto {
@IsInt()
pay_year: number;
@IsInt()
period_no: number;
@IsEmail()
employee_email!: string;
@IsBoolean()
approve: boolean;
}
export class BulkCrewApprovalDto {
@IsEmail()
supervisor_email: string;
@IsBoolean()
include_subtree: boolean = false;
@IsArray()
@ValidateNested({each: true})
@Type(()=> BulkCrewApprovalItemDto)
items: BulkCrewApprovalItemDto[]
}

View File

@ -1,5 +1,4 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Allow, IsOptional } from 'class-validator';
export class EmployeePeriodOverviewDto { export class EmployeePeriodOverviewDto {
// @ApiProperty({ // @ApiProperty({

View File

@ -11,7 +11,6 @@ import { ShiftsCommandService } from "../shifts/services/shifts-command.service"
@Module({ @Module({
imports: [PrismaModule, TimesheetsModule], imports: [PrismaModule, TimesheetsModule],
providers: [ providers: [
PayPeriodsQueryService,
PayPeriodsQueryService, PayPeriodsQueryService,
PayPeriodsCommandService, PayPeriodsCommandService,
TimesheetsCommandService, TimesheetsCommandService,

View File

@ -1,44 +1,105 @@
import { Injectable, NotFoundException } from "@nestjs/common"; import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { TimesheetsCommandService } from "src/modules/timesheets/services/timesheets-command.service"; import { TimesheetsCommandService } from "src/modules/timesheets/services/timesheets-command.service";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto";
import { PayPeriodsQueryService } from "./pay-periods-query.service";
@Injectable() @Injectable()
export class PayPeriodsCommandService { export class PayPeriodsCommandService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly timesheets_approval: TimesheetsCommandService, private readonly timesheets_approval: TimesheetsCommandService,
private readonly query: PayPeriodsQueryService,
) {} ) {}
async approvalPayPeriod(pay_year: number , period_no: number): Promise<void> { //function to approve pay-periods according to selected crew members
const period = await this.prisma.payPeriods.findFirst({ async bulkApproveCrew(dto:BulkCrewApprovalDto): Promise<{updated: number}> {
where: { pay_year, pay_period_no: period_no}, const { supervisor_email, include_subtree, items } = dto;
}); if(!items?.length) throw new BadRequestException('no items to process');
if (!period) throw new NotFoundException(`PayPeriod #${pay_year}-${period_no} not found`);
//fetches timesheet of selected period if the timesheet has atleast 1 shift or 1 expense //fetch and validate supervisor status
const timesheet_ist = await this.prisma.timesheets.findMany({ const supervisor = await this.query.getSupervisor(supervisor_email);
if(!supervisor) throw new NotFoundException('No employee record linked to current user');
if(!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor');
//fetches emails of crew members linked to supervisor
const crew_emails = await this.query.resolveCrewEmails(supervisor.id, include_subtree);
for(const item of items) {
if(!crew_emails.has(item.employee_email)) {
throw new ForbiddenException(`Employee ${item.employee_email} not in supervisor crew`);
}
}
const period_cache = new Map<string, {period_start: Date, period_end: Date}>();
const getPeriod = async (y:number, no: number) => {
const key = `${y}-${no}`;
if(!period_cache.has(key)) return period_cache.get(key)!;
const period = await this.query.getPeriodWindow(y,no);
if(!period) throw new NotFoundException(`Pay period ${y}-${no} not found`);
period_cache.set(key, period);
return period;
};
let updated = 0;
await this.prisma.$transaction(async (transaction) => {
for(const item of items) {
const { period_start, period_end } = await getPeriod(item.pay_year, item.period_no);
const t_sheets = await transaction.timesheets.findMany({
where: { where: {
employee: { user: { email: item.employee_email } },
OR: [ OR: [
{ shift: {some: { date: { gte: period.period_start, {shift : { some: { date: { gte: period_start, lte: period_end } } } },
lte: period.period_end, {expense: { some: { date: { gte: period_start, lte: period_end } } } },
},
}},
},
{ expense: { some: { date: { gte: period.period_start,
lte: period.period_end,
},
}},
},
], ],
}, },
select: { id: true }, select: { id: true },
}); });
//approval of both timesheet (cascading to the approval of related shifts and expenses) for(const { id } of t_sheets) {
await this.prisma.$transaction(async (transaction)=> { await this.timesheets_approval.updateApprovalWithTransaction(transaction, id, item.approve);
for(const {id} of timesheet_ist) { updated++;
await this.timesheets_approval.updateApprovalWithTx(transaction,id, true);
} }
})
} }
});
return {updated};
}
//function to approve a single pay-period of a single employee (deprecated)
// async approvalPayPeriod(pay_year: number , period_no: number): Promise<void> {
// const period = await this.prisma.payPeriods.findFirst({
// where: { pay_year, pay_period_no: period_no},
// });
// if (!period) throw new NotFoundException(`PayPeriod #${pay_year}-${period_no} not found`);
// //fetches timesheet of selected period if the timesheet has atleast 1 shift or 1 expense
// const timesheet_ist = await this.prisma.timesheets.findMany({
// where: {
// OR: [
// { shift: {some: { date: { gte: period.period_start,
// lte: period.period_end,
// },
// }},
// },
// { expense: { some: { date: { gte: period.period_start,
// lte: period.period_end,
// },
// }},
// },
// ],
// },
// select: { id: true },
// });
// //approval of both timesheet (cascading to the approval of related shifts and expenses)
// await this.prisma.$transaction(async (transaction)=> {
// for(const {id} of timesheet_ist) {
// await this.timesheets_approval.updateApprovalWithTransaction(transaction,id, true);
// }
// })
// }
} }

View File

@ -11,18 +11,6 @@ import { mapPayPeriodToDto } from "../mappers/pay-periods.mapper";
export class PayPeriodsQueryService { export class PayPeriodsQueryService {
constructor( private readonly prisma: PrismaService) {} constructor( private readonly prisma: PrismaService) {}
async getOverviewByYearPeriod(pay_year: number, period_no: number): Promise<PayPeriodOverviewDto> {
const period = computePeriod(pay_year, period_no);
return this.buildOverview({
period_start: period.period_start,
period_end : period.period_end,
period_no : period.period_no,
pay_year : period.pay_year,
payday : period.payday,
label :period.label,
} as any);
}
async getOverview(pay_period_no: number): Promise<PayPeriodOverviewDto> { async getOverview(pay_period_no: number): Promise<PayPeriodOverviewDto> {
const period = await this.prisma.payPeriods.findFirst({ const period = await this.prisma.payPeriods.findFirst({
where: { pay_period_no }, where: { pay_period_no },
@ -40,6 +28,87 @@ export class PayPeriodsQueryService {
}); });
} }
async getOverviewByYearPeriod(pay_year: number, period_no: number): Promise<PayPeriodOverviewDto> {
const period = computePeriod(pay_year, period_no);
return this.buildOverview({
period_start: period.period_start,
period_end : period.period_end,
period_no : period.period_no,
pay_year : period.pay_year,
payday : period.payday,
label :period.label,
} as any);
}
private async resolveCrew(supervisor_id: number, include_subtree: boolean):
Promise<Array<{ id: number; first_name: string; last_name: string; email: string }>> {
const result: Array<{ id: number; first_name: string; last_name: string; email: string; }> = [];
let frontier = await this.prisma.employees.findMany({
where: { supervisor_id: supervisor_id },
select: { id: true, user: { select: { first_name: true, last_name: true, email: true } } },
});
result.push(...frontier.map(emp => ({
id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name, email: emp.user.email
})));
if (!include_subtree) return result;
while (frontier.length) {
const parent_ids = frontier.map(emp => emp.id);
const next = await this.prisma.employees.findMany({
where: { supervisor_id: { in: parent_ids } },
select: { id: true, user: { select: { first_name: true, last_name: true, email: true } } },
});
if (next.length === 0) break;
result.push(...next.map(emp => ({
id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name, email: emp.user.email
})));
frontier = next;
}
return result;
}
async resolveCrewEmails(supervisor_id: number, include_subtree: boolean): Promise<Set<string>> {
const crew = await this.resolveCrew(supervisor_id, include_subtree);
return new Set(crew.map(crew_member => crew_member.email).filter(Boolean));
}
async getCrewOverview(pay_year: number, period_no: number, email: string, include_subtree: boolean):
Promise<PayPeriodOverviewDto> {
// 1) Search for the period
const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no: period_no } });
if (!period) throw new NotFoundException(`Pay period ${pay_year}-${period_no} not found`);
// 2) fetch supervisor
const supervisor = await this.prisma.employees.findFirst({
where: { user: { email: email }},
select: {
id: true,
is_supervisor: true,
},
});
if (!supervisor) throw new NotFoundException('No employee record linked to current user');
if (!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor');
// 3)fetchs crew members
const crew = await this.resolveCrew(supervisor.id, include_subtree); // [{ id, first_name, last_name }]
const crew_ids = crew.map(c => c.id);
// seed names map for employee without data
const seed_names = new Map<number, string>(crew.map(crew => [crew.id, `${crew.first_name} ${crew.last_name}`.trim()]));
// 4) overview build
return this.buildOverview({
period_no : period.pay_period_no,
period_start: period.period_start,
period_end : period.period_end,
payday : period.payday,
pay_year : period.pay_year,
label : period.label,
}, { filtered_employee_ids: crew_ids, seed_names });
}
private async buildOverview( private async buildOverview(
period: { period_start: string | Date; period_end: string | Date; payday: string | Date; period: { period_start: string | Date; period_end: string | Date; payday: string | Date;
period_no: number; pay_year: number; label: string; }, period_no: number; pay_year: number; label: string; },
@ -130,10 +199,10 @@ export class PayPeriodsQueryService {
} }
} }
const ensure = (id: number, name: string) => { const ensure = (id: number, name: string, email: string) => {
if (!by_employee.has(id)) { if (!by_employee.has(id)) {
by_employee.set(id, { by_employee.set(id, {
email: '', email,
employee_name: name, employee_name: name,
regular_hours: 0, regular_hours: 0,
evening_hours: 0, evening_hours: 0,
@ -150,7 +219,7 @@ export class PayPeriodsQueryService {
for (const shift of shifts) { for (const shift of shifts) {
const employee = shift.timesheet.employee; const employee = shift.timesheet.employee;
const name = `${employee.user.first_name} ${employee.user.last_name}`.trim(); const name = `${employee.user.first_name} ${employee.user.last_name}`.trim();
const record = ensure(employee.id, name); const record = ensure(employee.id, name, employee.user.email);
const hours = computeHours(shift.start_time, shift.end_time); const hours = computeHours(shift.start_time, shift.end_time);
const categorie = (shift.bank_code?.categorie || "REGULAR").toUpperCase(); const categorie = (shift.bank_code?.categorie || "REGULAR").toUpperCase();
@ -167,7 +236,7 @@ export class PayPeriodsQueryService {
for (const expense of expenses) { for (const expense of expenses) {
const exp = expense.timesheet.employee; const exp = expense.timesheet.employee;
const name = `${exp.user.first_name} ${exp.user.last_name}`.trim(); const name = `${exp.user.first_name} ${exp.user.last_name}`.trim();
const record = ensure(exp.id, name); const record = ensure(exp.id, name, exp.user.email);
const amount = toMoney(expense.amount); const amount = toMoney(expense.amount);
record.expenses += amount; record.expenses += amount;
@ -195,69 +264,13 @@ export class PayPeriodsQueryService {
}; };
} }
async getSupervisor(email:string) {
async getCrewOverview(pay_year: number, period_no: number, email: string, include_subtree: boolean): return this.prisma.employees.findFirst({
Promise<PayPeriodOverviewDto> { where: { user: { email } },
// 1) Search for the period select: { id: true, is_supervisor: true },
const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no: period_no } });
if (!period) throw new NotFoundException(`Pay period ${pay_year}-${period_no} not found`);
// 2) fetch supervisor
const supervisor = await this.prisma.employees.findFirst({
where: { user: { email: email }},
select: {
id: true,
is_supervisor: true,
},
}); });
if (!supervisor) throw new NotFoundException('No employee record linked to current user');
if (!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor');
// 3)fetchs crew members
const crew = await this.resolveCrew(supervisor.id, include_subtree); // [{ id, first_name, last_name }]
const crew_ids = crew.map(c => c.id);
// seed names map for employee without data
const seed_names = new Map<number, string>(crew.map(crew => [crew.id, `${crew.first_name} ${crew.last_name}`.trim()]));
// 4) overview build
return this.buildOverview({
period_no : period.pay_period_no,
period_start: period.period_start,
period_end : period.period_end,
payday : period.payday,
pay_year : period.pay_year,
label : period.label,
}, { filtered_employee_ids: crew_ids, seed_names });
} }
private async resolveCrew(supervisor_id: number, include_subtree: boolean):
Promise<Array<{ id: number; first_name: string; last_name: string }>> {
const result: Array<{ id: number; first_name: string; last_name: string }> = [];
let frontier = await this.prisma.employees.findMany({
where: { supervisor_id: supervisor_id },
select: { id: true, user: { select: { first_name: true, last_name: true, email: true } } },
});
result.push(...frontier.map(emp => ({ id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name })));
if (!include_subtree) return result;
while (frontier.length) {
const parent_ids = frontier.map(emp => emp.id);
const next = await this.prisma.employees.findMany({
where: { supervisor_id: { in: parent_ids } },
select: { id: true, user: { select: { first_name: true, last_name: true, email: true } } },
});
if (next.length === 0) break;
result.push(...next.map(emp => ({ id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name, email: emp.user.email })));
frontier = next;
}
return result;
}
async findAll(): Promise<PayPeriodDto[]> { async findAll(): Promise<PayPeriodDto[]> {
const currentPayYear = payYearOfDate(new Date()); const currentPayYear = payYearOfDate(new Date());
return listPayYear(currentPayYear).map(period =>({ return listPayYear(currentPayYear).map(period =>({
@ -279,6 +292,11 @@ private async resolveCrew(supervisor_id: number, include_subtree: boolean):
return mapPayPeriodToDto(row); return mapPayPeriodToDto(row);
} }
async findCurrent(date?: string): Promise<PayPeriodDto> {
const iso_day = date ?? new Date().toISOString().slice(0,10);
return this.findByDate(iso_day);
}
async findOneByYearPeriod(pay_year: number, period_no: number): Promise<PayPeriodDto> { async findOneByYearPeriod(pay_year: number, period_no: number): Promise<PayPeriodDto> {
const row = await this.prisma.payPeriods.findFirst({ const row = await this.prisma.payPeriods.findFirst({
where: { pay_year, pay_period_no: period_no }, where: { pay_year, pay_period_no: period_no },
@ -319,11 +337,12 @@ private async resolveCrew(supervisor_id: number, include_subtree: boolean):
payday : hit.payday, payday : hit.payday,
label : hit.label label : hit.label
} }
} }
async findCurrent(date?: string): Promise<PayPeriodDto> { async getPeriodWindow(pay_year: number, period_no: number) {
const iso_day = date ?? new Date().toISOString().slice(0,10); return this.prisma.payPeriods.findFirst({
return this.findByDate(iso_day); where: {pay_year, pay_period_no: period_no },
select: { period_start: true, period_end: true },
});
} }
} }

View File

@ -86,14 +86,14 @@ export class ShiftsController {
//CSV Headers //CSV Headers
const header = [ const header = [
'fullName', 'full_name',
'supervisor', 'supervisor',
'totalRegularHrs', 'total_regular_hrs',
'totalEveningHrs', 'total_evening_hrs',
'totalOvertimeHrs', 'total_overtime_hrs',
'totalExpenses', 'total_expenses',
'totalMileage', 'total_mileage',
'isValidated' 'is_validated'
].join(',') + '\n'; ].join(',') + '\n';
//CSV rows //CSV rows
@ -101,14 +101,14 @@ export class ShiftsController {
const esc = (str: string) => `"${str.replace(/"/g, '""')}"`; const esc = (str: string) => `"${str.replace(/"/g, '""')}"`;
return [ return [
esc(r.fullName), esc(r.full_name),
esc(r.supervisor), esc(r.supervisor),
r.totalRegularHrs.toFixed(2), r.total_regular_hrs.toFixed(2),
r.totalEveningHrs.toFixed(2), r.total_evening_hrs.toFixed(2),
r.totalOvertimeHrs.toFixed(2), r.total_overtime_hrs.toFixed(2),
r.totalExpenses.toFixed(2), r.total_expenses.toFixed(2),
r.totalMileage.toFixed(2), r.total_mileage.toFixed(2),
r.isValidated, r.is_validated,
].join(','); ].join(',');
}).join('\n'); }).join('\n');

View File

@ -1,6 +1,6 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer"; import { Type } from "class-transformer";
import { Allow, IsDate, IsDateString, IsInt, IsString } from "class-validator"; import { Allow, IsDateString, IsInt, IsString } from "class-validator";
export class CreateShiftDto { export class CreateShiftDto {
@ApiProperty({ @ApiProperty({

View File

@ -15,9 +15,9 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
return transaction.shifts; return transaction.shifts;
} }
async updateApproval(id: number, isApproved: boolean): Promise<Shifts> { async updateApproval(id: number, is_approved: boolean): Promise<Shifts> {
return this.prisma.$transaction((transaction) => return this.prisma.$transaction((transaction) =>
this.updateApprovalWithTx(transaction, id, isApproved), this.updateApprovalWithTransaction(transaction, id, is_approved),
); );
} }

View File

@ -8,17 +8,17 @@ import { SearchShiftsDto } from "../dtos/search-shift.dto";
import { NotificationsService } from "src/modules/notifications/services/notifications.service"; import { NotificationsService } from "src/modules/notifications/services/notifications.service";
import { computeHours, hoursBetweenSameDay } from "src/common/utils/date-utils"; import { computeHours, hoursBetweenSameDay } from "src/common/utils/date-utils";
const DAILY_LIMIT_HOURS = Number(process.env.DAILY_LIMIT_HOURS ?? 8); const DAILY_LIMIT_HOURS = Number(process.env.DAILY_LIMIT_HOURS ?? 12);
export interface OverviewRow { export interface OverviewRow {
fullName: string; full_name: string;
supervisor: string; supervisor: string;
totalRegularHrs: number; total_regular_hrs: number;
totalEveningHrs: number; total_evening_hrs: number;
totalOvertimeHrs: number; total_overtime_hrs: number;
totalExpenses: number; total_expenses: number;
totalMileage: number; total_mileage: number;
isValidated: boolean; is_validated: boolean;
} }
@Injectable() @Injectable()
@ -40,31 +40,32 @@ export class ShiftsQueryService {
}); });
//fetches all shifts of the same day to check for daily overtime //fetches all shifts of the same day to check for daily overtime
const sameDayShifts = await this.prisma.shifts.findMany({ const same_day_shifts = await this.prisma.shifts.findMany({
where: { timesheet_id, date }, where: { timesheet_id, date },
select: { id: true, date: true, start_time: true, end_time: true }, select: { id: true, date: true, start_time: true, end_time: true },
}); });
//sums hours of the day //sums hours of the day
const totalHours = sameDayShifts.reduce((sum, s) => { const total_hours = same_day_shifts.reduce((sum, s) => {
return sum + hoursBetweenSameDay(s.date, s.start_time, s.end_time); return sum + hoursBetweenSameDay(s.date, s.start_time, s.end_time);
}, 0 ); }, 0 );
//Notify if total hours > 8 for a single day //Notify if total hours > 8 for a single day
if(totalHours > DAILY_LIMIT_HOURS ) { if(total_hours > DAILY_LIMIT_HOURS ) {
const userId = String(shift.timesheet.employee.user.id); const user_id = String(shift.timesheet.employee.user.id);
const dateLabel = new Date(date).toLocaleDateString('fr-CA'); const date_label = new Date(date).toLocaleDateString('fr-CA');
this.notifs.notify(userId, { this.notifs.notify(user_id, {
type: 'shift.overtime.daily', type: 'shift.overtime.daily',
severity: 'warn', severity: 'warn',
message: `Tu viens de dépasser ${DAILY_LIMIT_HOURS.toFixed(2)}h pour la journée du ${dateLabel} (total: ${totalHours.toFixed(2)}h).`, message: `Tu viens de dépasser ${DAILY_LIMIT_HOURS.toFixed(2)}h pour la journée du ${date_label}
(total: ${total_hours.toFixed(2)}h).`,
ts: new Date().toISOString(), ts: new Date().toISOString(),
meta: { meta: {
timesheet_id, timesheet_id,
date: new Date(date).toISOString(), date: new Date(date).toISOString(),
totalHours, total_hours,
threshold: DAILY_LIMIT_HOURS, threshold: DAILY_LIMIT_HOURS,
lastShiftId: shift.id last_shift_id: shift.id
}, },
}); });
} }
@ -152,93 +153,93 @@ export class ShiftsQueryService {
const mapRow = new Map<string, OverviewRow>(); const mapRow = new Map<string, OverviewRow>();
for(const s of shifts) { for(const shift of shifts) {
const employeeId = s.timesheet.employee.user_id; const employeeId = shift.timesheet.employee.user_id;
const user = s.timesheet.employee.user; const user = shift.timesheet.employee.user;
const sup = s.timesheet.employee.supervisor?.user; const sup = shift.timesheet.employee.supervisor?.user;
let row = mapRow.get(employeeId); let row = mapRow.get(employeeId);
if(!row) { if(!row) {
row = { row = {
fullName: `${user.first_name} ${user.last_name}`, full_name: `${user.first_name} ${user.last_name}`,
supervisor: sup? `${sup.first_name} ${sup.last_name }` : '', supervisor: sup? `${sup.first_name} ${sup.last_name }` : '',
totalRegularHrs: 0, total_regular_hrs: 0,
totalEveningHrs: 0, total_evening_hrs: 0,
totalOvertimeHrs: 0, total_overtime_hrs: 0,
totalExpenses: 0, total_expenses: 0,
totalMileage: 0, total_mileage: 0,
isValidated: false, is_validated: false,
}; };
} }
const hours = computeHours(s.start_time, s.end_time); const hours = computeHours(shift.start_time, shift.end_time);
switch(s.bank_code.type) { switch(shift.bank_code.type) {
case 'regular' : row.totalRegularHrs += hours; case 'regular' : row.total_regular_hrs += hours;
break; break;
case 'evening' : row.totalEveningHrs += hours; case 'evening' : row.total_evening_hrs += hours;
break; break;
case 'overtime' : row.totalOvertimeHrs += hours; case 'overtime' : row.total_overtime_hrs += hours;
break; break;
default: row.totalRegularHrs += hours; default: row.total_regular_hrs += hours;
} }
mapRow.set(employeeId, row); mapRow.set(employeeId, row);
} }
for(const e of expenses) { for(const exp of expenses) {
const employeeId = e.timesheet.employee.user_id; const employee_id = exp.timesheet.employee.user_id;
const user = e.timesheet.employee.user; const user = exp.timesheet.employee.user;
const sup = e.timesheet.employee.supervisor?.user; const sup = exp.timesheet.employee.supervisor?.user;
let row = mapRow.get(employeeId); let row = mapRow.get(employee_id);
if(!row) { if(!row) {
row = { row = {
fullName: `${user.first_name} ${user.last_name}`, full_name: `${user.first_name} ${user.last_name}`,
supervisor: sup? `${sup.first_name} ${sup.last_name }` : '', supervisor: sup? `${sup.first_name} ${sup.last_name }` : '',
totalRegularHrs: 0, total_regular_hrs: 0,
totalEveningHrs: 0, total_evening_hrs: 0,
totalOvertimeHrs: 0, total_overtime_hrs: 0,
totalExpenses: 0, total_expenses: 0,
totalMileage: 0, total_mileage: 0,
isValidated: false, is_validated: false,
}; };
} }
const amount = Number(e.amount); const amount = Number(exp.amount);
row.totalExpenses += amount; row.total_expenses += amount;
if(e.bank_code.type === 'mileage') { if(exp.bank_code.type === 'mileage') {
row.totalMileage += amount; row.total_mileage += amount;
} }
mapRow.set(employeeId, row); mapRow.set(employee_id, row);
} }
//return by default the list of employee in ascending alphabetical order //return by default the list of employee in ascending alphabetical order
return Array.from(mapRow.values()).sort((a,b) => a.fullName.localeCompare(b.fullName)); return Array.from(mapRow.values()).sort((a,b) => a.full_name.localeCompare(b.full_name));
} }
//archivation functions ****************************************************** //archivation functions ******************************************************
async archiveOld(): Promise<void> { async archiveOld(): Promise<void> {
//fetches archived timesheet's Ids //fetches archived timesheet's Ids
const archivedTimesheets = await this.prisma.timesheetsArchive.findMany({ const archived_timesheets = await this.prisma.timesheetsArchive.findMany({
select: { timesheet_id: true }, select: { timesheet_id: true },
}); });
const timesheetIds = archivedTimesheets.map(sheet => sheet.timesheet_id); const timesheet_ids = archived_timesheets.map(sheet => sheet.timesheet_id);
if(timesheetIds.length === 0) { if(timesheet_ids.length === 0) {
return; return;
} }
// copy/delete transaction // copy/delete transaction
await this.prisma.$transaction(async transaction => { await this.prisma.$transaction(async transaction => {
//fetches shifts to move to archive //fetches shifts to move to archive
const shiftsToArchive = await transaction.shifts.findMany({ const shifts_to_archive = await transaction.shifts.findMany({
where: { timesheet_id: { in: timesheetIds } }, where: { timesheet_id: { in: timesheet_ids } },
}); });
if(shiftsToArchive.length === 0) { if(shifts_to_archive.length === 0) {
return; return;
} }
//copies sent to archive table //copies sent to archive table
await transaction.shiftsArchive.createMany({ await transaction.shiftsArchive.createMany({
data: shiftsToArchive.map(shift => ({ data: shifts_to_archive.map(shift => ({
shift_id: shift.id, shift_id: shift.id,
timesheet_id: shift.timesheet_id, timesheet_id: shift.timesheet_id,
bank_code_id: shift.bank_code_id, bank_code_id: shift.bank_code_id,
@ -251,7 +252,7 @@ export class ShiftsQueryService {
//delete from shifts table //delete from shifts table
await transaction.shifts.deleteMany({ await transaction.shifts.deleteMany({
where: { id: { in: shiftsToArchive.map(shift => shift.id) } }, where: { id: { in: shifts_to_archive.map(shift => shift.id) } },
}) })
}) })

View File

@ -17,12 +17,12 @@ export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
async updateApproval(id: number, isApproved: boolean): Promise<Timesheets> { async updateApproval(id: number, isApproved: boolean): Promise<Timesheets> {
return this.prisma.$transaction((transaction) => return this.prisma.$transaction((transaction) =>
this.updateApprovalWithTx(transaction, id, isApproved), this.updateApprovalWithTransaction(transaction, id, isApproved),
); );
} }
async cascadeApprovalWithtx(transaction: Prisma.TransactionClient, timesheetId: number, isApproved: boolean): Promise<Timesheets> { async cascadeApprovalWithtx(transaction: Prisma.TransactionClient, timesheetId: number, isApproved: boolean): Promise<Timesheets> {
const timesheet = await this.updateApprovalWithTx(transaction, timesheetId, isApproved); const timesheet = await this.updateApprovalWithTransaction(transaction, timesheetId, isApproved);
await transaction.shifts.updateMany({ await transaction.shifts.updateMany({
where: { timesheet_id: timesheetId }, where: { timesheet_id: timesheetId },