refactor(pay-periods): refactored bulkApprove for crew to instead toggle approval of all shifts and expenses of a given employee

This commit is contained in:
Nicolas Drolet 2025-12-22 14:09:35 -05:00
parent 447d968f59
commit 58baaade5d
6 changed files with 75 additions and 75 deletions

View File

@ -9,7 +9,7 @@ export function computeHours(start: Date, end: Date, roundToMinutes?: number): n
const minutes = roundToMinutes ? const minutes = roundToMinutes ?
Math.round(totalMinutes / roundToMinutes) * roundToMinutes : Math.round(totalMinutes / roundToMinutes) * roundToMinutes :
totalMinutes; totalMinutes;
return +(minutes / 60).toFixed(2); return +(minutes / 60);
} }
//round the amount of hours to quarter //round the amount of hours to quarter

View File

@ -17,8 +17,8 @@ export class AuthController {
@Get('/callback') @Get('/callback')
@UseGuards(OIDCLoginGuard) @UseGuards(OIDCLoginGuard)
loginCallback(@Req() req: Request, @Res() res: Response) { loginCallback(@Req() req: Request, @Res() res: Response) {
res.redirect("http://10.100.251.2:9013/#/v1/login-success"); // res.redirect("http://10.100.251.2:9013/#/v1/login-success");
// res.redirect(process.env.REDIRECT_URL_DEV!); res.redirect(process.env.REDIRECT_URL_DEV!);
} }
@Get('/me') @Get('/me')

View File

@ -30,7 +30,6 @@ export abstract class AbstractUserService {
let module_access: Modules[] = []; let module_access: Modules[] = [];
if (user.user_module_access !== null) module_access = toKeysFromBoolean(user.user_module_access); if (user.user_module_access !== null) module_access = toKeysFromBoolean(user.user_module_access);
console.log('module access: ', module_access);
const clean_user = { const clean_user = {
first_name: user.first_name, first_name: user.first_name,

View File

@ -43,16 +43,22 @@ export class PayPeriodsController {
return this.queryService.findOneByYearPeriod(year, period_no); return this.queryService.findOneByYearPeriod(year, period_no);
} }
@Patch("crew/pay-period-approval") @Patch("pay-period-approval")
@ModuleAccessAllowed(ModulesEnum.timesheets_approval) @ModuleAccessAllowed(ModulesEnum.timesheets_approval)
async bulkApproval(@Access('email') email:string, @Body() dto: BulkCrewApprovalDto) { async bulkApproval(
if (!email) throw new UnauthorizedException(`Session infos not found`); @Body('email') email: string,
return this.commandService.bulkApproveCrew(email, dto); @Body('timesheet_ids') timesheet_ids: number[],
@Body('is_approved') is_approved: boolean,
): Promise<Result<{ shifts: number, expenses: number }, string>> {
if (!email) return {success: false, error: 'EMAIL_REQUIRED'};
if (!timesheet_ids || timesheet_ids.length < 1) return {success: false, error: 'TIMESHEET_ID_REQUIRED'};
if (!is_approved) return {success: false, error: 'APPROVAL_STATUS_REQUIRED'}
return this.commandService.bulkApproveEmployee(email, timesheet_ids, is_approved);
} }
@Get('crew/:year/:periodNumber') @Get('crew/:year/:periodNumber')
@ModuleAccessAllowed(ModulesEnum.timesheets_approval) @ModuleAccessAllowed(ModulesEnum.timesheets_approval)
async getCrewOverview(@Access('email') email:string, async getCrewOverview(@Access('email') email: string,
@Param('year', ParseIntPipe) year: number, @Param('year', ParseIntPipe) year: number,
@Param('periodNumber', ParseIntPipe) period_no: number, @Param('periodNumber', ParseIntPipe) period_no: number,
@Query('includeSubtree', new ParseBoolPipe({ optional: true })) include_subtree = false, @Query('includeSubtree', new ParseBoolPipe({ optional: true })) include_subtree = false,

View File

@ -1,9 +1,10 @@
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
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"; import { PayPeriodsQueryService } from "./pay-periods-query.service";
import { TimesheetApprovalService } from "src/time-and-attendance/timesheets/services/timesheet-approval.service"; import { TimesheetApprovalService } from "src/time-and-attendance/timesheets/services/timesheet-approval.service";
import { Result } from "src/common/errors/result-error.factory"; import { Result } from "src/common/errors/result-error.factory";
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
import { Prisma } from "@prisma/client";
//change promise to return result pattern //change promise to return result pattern
@ -13,63 +14,56 @@ export class PayPeriodsCommandService {
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly timesheetsApproval: TimesheetApprovalService, private readonly timesheetsApproval: TimesheetApprovalService,
private readonly query: PayPeriodsQueryService, private readonly query: PayPeriodsQueryService,
private readonly emailResolver: EmailToIdResolver,
) { } ) { }
//function to approve pay-periods according to selected crew members //function to approve pay-periods according to selected crew members
async bulkApproveCrew(email: string, dto: BulkCrewApprovalDto): Promise<Result<{ updated: number }, string>> { async bulkApproveEmployee(email: string, timesheet_ids: number[], is_approved: boolean): Promise<Result<{ shifts: number, expenses: number }, string>> {
const { include_subtree, items } = dto; let shifts: Prisma.BatchPayload;
if (!items?.length) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; let expenses: Prisma.BatchPayload;
//fetch and validate supervisor status //fetch employee id
const supervisor = await this.query.getSupervisor(email); const employee_id = await this.emailResolver.findIdByEmail(email);
if (!supervisor) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; if (!employee_id.success) return { success: false, error: employee_id.error }
if (!supervisor.is_supervisor) return { success: false, error: 'INVALID_EMPLOYEE' };
//fetches emails of crew members linked to supervisor try {
const crew_emails = await this.query.resolveCrewEmails(supervisor.id, include_subtree); shifts = await this.prisma.shifts.updateMany({
if (!crew_emails.success) return { success: false, error: 'INVALID_EMAIL' }; where: {
timesheet: {
id: { in: timesheet_ids },
employee_id: employee_id.data,
},
},
data: {
is_approved: is_approved,
}
});
for (const item of items) { expenses = await this.prisma.expenses.updateMany({
if (!crew_emails.data.has(item.employee_email)) { where: {
return { success: false, error: 'INVALID_EMPLOYEE' } timesheet: {
} id: { in: timesheet_ids },
employee_id: employee_id.data,
},
},
data: {
is_approved: is_approved,
}
});
await this.prisma.timesheets.updateMany({
where: {
id: { in: timesheet_ids},
employee_id: employee_id.data,
},
data: {
is_approved: is_approved,
}
})
} catch (_error) {
return { success: false, error: 'UNKNOWN_ERROR_VALIDATING' }
} }
const period_cache = new Map<string, { period_start: Date, period_end: Date }>(); return { success: true, data: { shifts: shifts.count, expenses: expenses.count}}
const getPeriod = async (year: number, period_no: number) => {
const key = `${year}-${period_no}`;
if (period_cache.has(key)) return period_cache.get(key)!;
const period = await this.query.getPeriodWindow(year, period_no);
if (!period) throw new NotFoundException(`Pay period ${year}-${period_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 timesheets = await transaction.timesheets.findMany({
where: {
employee: { user: { email: item.employee_email } },
OR: [
{ shift: { some: { date: { gte: period_start, lte: period_end } } } },
{ expense: { some: { date: { gte: period_start, lte: period_end } } } },
],
},
select: { id: true },
});
for (const { id } of timesheets) {
await this.timesheetsApproval.cascadeApprovalWithtx(transaction, id, item.approve);
updated++;
}
}
});
return { success: true, data: { updated } };
} }
} }

View File

@ -252,10 +252,11 @@ export class PayPeriodsQueryService {
for (const employee of all_employees) { for (const employee of all_employees) {
let is_active = true; let is_active = true;
if (employee.last_work_day !== null) { if (employee.last_work_day !== null) {
is_active = this.checkForInactiveDate(employee.last_work_day) is_active = this.checkForInactiveDate(employee.last_work_day)
} }
console.log('employee name: ', employee.user.first_name, employee.user.first_name, 'last work day: ', employee.last_work_day);
by_employee.set(employee.id, { by_employee.set(employee.id, {
email: employee.user.email, email: employee.user.email,
employee_name: employee.user.first_name + ' ' + employee.user.last_name, employee_name: employee.user.first_name + ' ' + employee.user.last_name,
@ -311,26 +312,26 @@ export class PayPeriodsQueryService {
const hours = computeHours(shift.start_time, shift.end_time); const hours = computeHours(shift.start_time, shift.end_time);
const type = (shift.bank_code?.type ?? '').toUpperCase(); const type = (shift.bank_code?.type ?? '').toUpperCase();
switch (type) { switch (type) {
case "EVENING": record.other_hours.evening_hours = Number((record.other_hours.evening_hours += hours).toFixed(2)); case "EVENING": record.other_hours.evening_hours += hours;
record.total_hours = Number((record.total_hours += hours).toFixed(2)); record.total_hours += hours;
break; break;
case "EMERGENCY": record.other_hours.emergency_hours = Number((record.other_hours.emergency_hours += hours).toFixed(2)); case "EMERGENCY": record.other_hours.emergency_hours += hours;
record.total_hours = Number((record.total_hours += hours).toFixed(2)); record.total_hours += hours;
break; break;
case "OVERTIME": record.other_hours.overtime_hours = Number((record.other_hours.overtime_hours += hours).toFixed(2)); case "OVERTIME": record.other_hours.overtime_hours += hours;
record.total_hours = Number((record.total_hours += hours).toFixed(2)); record.total_hours += hours;
break; break;
case "SICK": record.other_hours.sick_hours = Number((record.other_hours.sick_hours += hours).toFixed(2)); case "SICK": record.other_hours.sick_hours += hours;
record.total_hours = Number((record.total_hours += hours).toFixed(2)); record.total_hours += hours;
break; break;
case "HOLIDAY": record.other_hours.holiday_hours = Number((record.other_hours.holiday_hours += hours).toFixed(2)); case "HOLIDAY": record.other_hours.holiday_hours += hours;
record.total_hours = Number((record.total_hours += hours).toFixed(2)); record.total_hours += hours;
break; break;
case "VACATION": record.other_hours.vacation_hours = Number(record.other_hours.vacation_hours += hours); case "VACATION": record.other_hours.vacation_hours += hours;
record.total_hours = Number((record.total_hours += hours).toFixed(2)); record.total_hours += hours;
break; break;
case "REGULAR": record.regular_hours = Number((record.regular_hours += hours).toFixed(2)); case "REGULAR": record.regular_hours = record.regular_hours += hours;
record.total_hours = Number((record.total_hours += hours).toFixed(2)); record.total_hours += hours;
break; break;
} }