refactor(module): refactor employees, archives and pay-period to use email instead of id and switch pay-period's requests to transaction

This commit is contained in:
Matthieu Haineault 2025-08-19 08:34:35 -04:00
parent fe87c36884
commit 7a9adeec69
10 changed files with 247 additions and 163 deletions

View File

@ -253,16 +253,16 @@
] ]
} }
}, },
"/employees/{id}": { "/employees/{email}": {
"get": { "get": {
"operationId": "EmployeesController_findOne", "operationId": "EmployeesController_findOne",
"parameters": [ "parameters": [
{ {
"name": "id", "name": "email",
"required": true, "required": true,
"in": "path", "in": "path",
"schema": { "schema": {
"type": "number" "type": "string"
} }
} }
], ],
@ -295,10 +295,10 @@
"operationId": "EmployeesController_remove", "operationId": "EmployeesController_remove",
"parameters": [ "parameters": [
{ {
"name": "id", "name": "email",
"required": true, "required": true,
"in": "path", "in": "path",
"description": "Identifier of the employee to delete", "description": "Email of the employee to delete",
"schema": { "schema": {
"type": "number" "type": "number"
} }
@ -326,10 +326,10 @@
"operationId": "EmployeesController_updateOrArchiveOrRestore", "operationId": "EmployeesController_updateOrArchiveOrRestore",
"parameters": [ "parameters": [
{ {
"name": "id", "name": "email",
"required": true, "required": true,
"in": "path", "in": "path",
"description": "Identifier of the employee", "description": "Email of the employee",
"schema": { "schema": {
"type": "number" "type": "number"
} }

View File

@ -0,0 +1,10 @@
/*
Warnings:
- You are about to drop the column `first_Work_Day` on the `employees_archive` table. All the data in the column will be lost.
- Added the required column `first_work_day` to the `employees_archive` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "public"."employees_archive" DROP COLUMN "first_Work_Day",
ADD COLUMN "first_work_day" DATE NOT NULL;

View File

@ -24,7 +24,7 @@ async function main() {
last_name: e.user.last_name, last_name: e.user.last_name,
external_payroll_id: e.external_payroll_id, external_payroll_id: e.external_payroll_id,
company_code: e.company_code, company_code: e.company_code,
first_Work_Day: e.first_work_day, first_work_day: e.first_work_day,
last_work_day: daysAgo(30), last_work_day: daysAgo(30),
supervisor_id: e.supervisor_id ?? null, supervisor_id: e.supervisor_id ?? null,
job_title: e.job_title, job_title: e.job_title,

View File

@ -69,7 +69,7 @@ model EmployeesArchive {
is_supervisor Boolean is_supervisor Boolean
external_payroll_id Int external_payroll_id Int
company_code Int company_code Int
first_Work_Day DateTime @db.Date first_work_day DateTime @db.Date
last_work_day DateTime @db.Date last_work_day DateTime @db.Date
supervisor_id Int? supervisor_id Int?
supervisor Employees? @relation("EmployeeSupervisorToArchive", fields: [supervisor_id], references: [id]) supervisor Employees? @relation("EmployeeSupervisorToArchive", fields: [supervisor_id], references: [id])

View File

@ -1,26 +1,52 @@
import { NotFoundException } from "@nestjs/common"; import { NotFoundException } from "@nestjs/common";
import { Prisma } from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
type UpdatableDelegate<T> = {
update(args: {
where: { id: number },
data: { is_approved: boolean },
}): Promise<T>;
};
//abstract class for approving or rejecting a shift, expense, timesheet or pay-period //abstract class for approving or rejecting a shift, expense, timesheet or pay-period
export abstract class BaseApprovalService<T> { export abstract class BaseApprovalService<T> {
protected constructor(protected readonly prisma: PrismaService) {} protected constructor(protected readonly prisma: PrismaService) {}
//returns the corresponding Prisma delegate //returns the corresponding Prisma delegate
protected abstract get delegate(): { protected abstract get delegate(): UpdatableDelegate<T>;
update(args: {where: {id: number };
data: { is_approved: boolean } protected abstract delegateFor(transaction: Prisma.TransactionClient): UpdatableDelegate<T>;
}): Promise<T>;
};
//standard update Aproval //standard update Aproval
async updateApproval(id: number, isApproved: boolean): Promise<T> { async updateApproval(id: number, isApproved: boolean): Promise<T> {
const entity = await this.delegate.update({ try{
return await this.delegate.update({
where: { id }, where: { id },
data: { is_approved: isApproved }, data: { is_approved: isApproved },
}); });
}catch (error: any) {
if(!entity) throw new NotFoundException(`Entity #${id} not found`); if (error instanceof PrismaClientKnownRequestError && error.code === "P2025") {
throw new NotFoundException(`Entity #${id} not found`);
return entity; }
throw error;
}
}
//approval with transaction to avoid many requests
async updateApprovalWithTx(transaction: Prisma.TransactionClient, id: number, isApproved: boolean): Promise<T> {
try {
return await this.delegateFor(transaction).update({
where: { id },
data: { is_approved: isApproved },
});
} catch (error: any ){
if(error instanceof PrismaClientKnownRequestError && error.code === 'P2025') {
throw new NotFoundException(`Entity #${id} not found`);
}
throw error;
}
} }
} }

View File

@ -42,13 +42,13 @@ export class EmployeesController {
return this.employeesService.findListEmployees(); return this.employeesService.findListEmployees();
} }
@Get(':id') @Get(':email')
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING ) //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING )
@ApiOperation({summary: 'Find employee' }) @ApiOperation({summary: 'Find employee' })
@ApiResponse({ status: 200, description: 'Employee found', type: CreateEmployeeDto }) @ApiResponse({ status: 200, description: 'Employee found', type: CreateEmployeeDto })
@ApiResponse({ status: 400, description: 'Employee not found' }) @ApiResponse({ status: 400, description: 'Employee not found' })
findOne(@Param('id', ParseIntPipe) id: number): Promise<Employees> { findOne(@Param('email', ParseIntPipe) email: string): Promise<Employees> {
return this.employeesService.findOne(id); return this.employeesService.findOne(email);
} }
@Get('profile/:email') @Get('profile/:email')
@ -60,31 +60,31 @@ export class EmployeesController {
return this.employeesService.findOneProfile(email); return this.employeesService.findOneProfile(email);
} }
@Delete(':id') @Delete(':email')
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR ) //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR )
@ApiOperation({summary: 'Delete employee' }) @ApiOperation({summary: 'Delete employee' })
@ApiParam({ name: 'id', type: Number, description: 'Identifier of the employee to delete' }) @ApiParam({ name: 'email', type: Number, description: 'Email of the employee to delete' })
@ApiResponse({ status: 204, description: 'Employee deleted' }) @ApiResponse({ status: 204, description: 'Employee deleted' })
@ApiResponse({ status: 404, description: 'Employee not found' }) @ApiResponse({ status: 404, description: 'Employee not found' })
remove(@Param('id', ParseIntPipe) id: number): Promise<Employees> { remove(@Param('email', ParseIntPipe) email: string): Promise<Employees> {
return this.employeesService.remove(id); return this.employeesService.remove(email);
} }
@Patch(':id') @Patch(':email')
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ApiBearerAuth('access-token') @ApiBearerAuth('access-token')
@ApiOperation({ summary: 'Update, archive or restore an employee' }) @ApiOperation({ summary: 'Update, archive or restore an employee' })
@ApiParam({ name: 'id', type: Number, description: 'Identifier of the employee' }) @ApiParam({ name: 'email', type: Number, description: 'Email of the employee' })
@ApiResponse({ status: 200, description: 'Employee updated or restored', type: CreateEmployeeDto }) @ApiResponse({ status: 200, description: 'Employee updated or restored', type: CreateEmployeeDto })
@ApiResponse({ status: 202, description: 'Employee archived successfully', type: CreateEmployeeDto }) @ApiResponse({ status: 202, description: 'Employee archived successfully', type: CreateEmployeeDto })
@ApiResponse({ status: 404, description: 'Employee not found in active or archive' }) @ApiResponse({ status: 404, description: 'Employee not found in active or archive' })
async updateOrArchiveOrRestore(@Param('id') id: string, @Body() dto: UpdateEmployeeDto,) { async updateOrArchiveOrRestore(@Param('email') email: string, @Body() dto: UpdateEmployeeDto,) {
// if last_work_day is set => archive the employee // if last_work_day is set => archive the employee
// else if employee is archived and first_work_day or last_work_day = null => restore // else if employee is archived and first_work_day or last_work_day = null => restore
//otherwise => standard update //otherwise => standard update
const result = await this.employeesService.patchEmployee(+id, dto); const result = await this.employeesService.patchEmployee(email, dto);
if(!result) { if(!result) {
throw new NotFoundException(`Employee #${ id } not found in active or archive.`) throw new NotFoundException(`Employee with email: ${ email } is not found in active or archive.`)
} }
return result; return result;
} }

View File

@ -4,7 +4,6 @@ import { CreateEmployeeDto } from '../dtos/create-employee.dto';
import { UpdateEmployeeDto } from '../dtos/update-employee.dto'; import { UpdateEmployeeDto } from '../dtos/update-employee.dto';
import { Employees, EmployeesArchive, Users } from '@prisma/client'; import { Employees, EmployeesArchive, Users } from '@prisma/client';
import { EmployeeListItemDto } from '../dtos/list-employee.dto'; import { EmployeeListItemDto } from '../dtos/list-employee.dto';
import { Roles as RoleEnum } from '@prisma/client';
import { EmployeeProfileItemDto } from '../dtos/profil-employee.dto'; import { EmployeeProfileItemDto } from '../dtos/profil-employee.dto';
function toDateOrNull(v?: string | null): Date | null { function toDateOrNull(v?: string | null): Date | null {
@ -100,13 +99,15 @@ export class EmployeesService {
); );
} }
async findOne(id: number): Promise<Employees> { async findOne(email: string): Promise<Employees> {
const emp = await this.prisma.employees.findUnique({ const emp = await this.prisma.employees.findFirst({
where: { id }, where: { user: { email } },
include: { user: true }, include: { user: true },
}); });
//add search for archived employees
if (!emp) { if (!emp) {
throw new NotFoundException(`Employee #${id} not found`); throw new NotFoundException(`Employee with email: ${email} not found`);
} }
return emp; return emp;
} }
@ -157,15 +158,14 @@ export class EmployeesService {
} }
async update( async update(
id: number, email: string,
dto: UpdateEmployeeDto, dto: UpdateEmployeeDto,
): Promise<Employees> { ): Promise<Employees> {
const emp = await this.findOne(id); const emp = await this.findOne(email);
const { const {
first_name, first_name,
last_name, last_name,
email,
phone_number, phone_number,
residence, residence,
external_payroll_id, external_payroll_id,
@ -173,10 +173,18 @@ async update(
job_title, job_title,
first_work_day, first_work_day,
last_work_day, last_work_day,
is_supervisor is_supervisor,
email: newEmail,
} = dto; } = dto;
return this.prisma.$transaction(async (tx) => { return this.prisma.$transaction(async (tx) => {
if(
first_name !== undefined ||
last_name !== undefined ||
newEmail !== undefined ||
phone_number !== undefined ||
residence !== undefined
){
await tx.users.update({ await tx.users.update({
where: { id: emp.user_id }, where: { id: emp.user_id },
data: { data: {
@ -187,9 +195,10 @@ async update(
...(residence !== undefined && { residence }), ...(residence !== undefined && { residence }),
}, },
}); });
}
return tx.employees.update({ const updated = await tx.employees.update({
where: { id }, where: { id: emp.id },
data: { data: {
...(external_payroll_id !== undefined && { external_payroll_id }), ...(external_payroll_id !== undefined && { external_payroll_id }),
...(company_code !== undefined && { company_code }), ...(company_code !== undefined && { company_code }),
@ -199,36 +208,51 @@ async update(
...(is_supervisor !== undefined && { is_supervisor }), ...(is_supervisor !== undefined && { is_supervisor }),
}, },
}); });
return updated;
}); });
} }
async remove(id: number): Promise<Employees> { async remove(email: string): Promise<Employees> {
await this.findOne(id);
return this.prisma.employees.delete({ where: { id } }); const emp = await this.findOne(email);
return this.prisma.$transaction(async (tx) => {
await tx.employees.updateMany({
where: { supervisor_id: emp.id },
data: { supervisor_id: null },
});
const deletedEmployee = await tx.employees.delete({
where: {id: emp.id },
});
await tx.users.delete({
where: { id: emp.user_id },
});
return deletedEmployee;
});
} }
//archivation functions ****************************************************** //archivation functions ******************************************************
async patchEmployee(id: number, dto: UpdateEmployeeDto): Promise<Employees | EmployeesArchive | null> { async patchEmployee(email: string, dto: UpdateEmployeeDto): Promise<Employees | EmployeesArchive | null> {
// 1) Tenter sur employés actifs // 1) Tenter sur employés actifs
const existing = await this.prisma.employees.findUnique({ const active = await this.prisma.employees.findFirst({
where: { id }, where: { user: { email } },
include: { user: true }, include: { user: true },
}); });
if (existing) { if (active) {
// Archivage : si on reçoit un last_work_day défini et que l'employé nest pas déjà terminé // Archivage : si on reçoit un last_work_day défini et que l'employé nest pas déjà terminé
if (dto.last_work_day !== undefined && existing.last_work_day == null && dto.last_work_day !== null) { if (dto.last_work_day !== undefined && active.last_work_day == null && dto.last_work_day !== null) {
return this.archiveOnTermination(existing, dto); return this.archiveOnTermination(active, dto);
} }
// Sinon, update standard (split Users/Employees) // Sinon, update standard (split Users/Employees)
const { const {
first_name, first_name,
last_name, last_name,
email, email: newEmail,
phone_number, phone_number,
residence, residence,
external_payroll_id, external_payroll_id,
@ -238,54 +262,59 @@ async patchEmployee(id: number, dto: UpdateEmployeeDto): Promise<Employees | Emp
last_work_day, last_work_day,
supervisor_id, supervisor_id,
is_supervisor, is_supervisor,
} = dto; } = dto as any;
const fw = toDateOrUndefined(first_work_day); const first_work_d = toDateOrUndefined(first_work_day);
const lw = (dto.hasOwnProperty('last_work_day')) const last_work_d = Object.prototype.hasOwnProperty('last_work_day')
? toDateOrNull(last_work_day ?? null) ? toDateOrNull(last_work_day ?? null)
: undefined; : undefined;
await this.prisma.$transaction(async (tx) => { await this.prisma.$transaction(async (tx) => {
const willUpdateUser = if(
first_name !== undefined || first_name !== undefined ||
last_name !== undefined || last_name !== undefined ||
email !== undefined || newEmail !== undefined ||
phone_number !== undefined || phone_number !== undefined ||
residence !== undefined; residence !== undefined
) {
if (willUpdateUser) {
await tx.users.update({ await tx.users.update({
where: { id: existing.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 } : {}), ...(email !== undefined ? { email: newEmail }: {}),
...(phone_number !== undefined ? { phone_number } : {}), ...(phone_number !== undefined ? { phone_number } : {}),
...(residence !== undefined ? { residence } : {}), ...(residence !== undefined ? { residence } : {}),
...(is_supervisor !== undefined ? { is_supervisor }: {}),
}, },
}); });
} }
await tx.employees.update({ const updated = await tx.employees.update({
where: { id }, where: { id: active.id },
data: { data: {
...(external_payroll_id !== undefined ? { external_payroll_id } : {}), ...(external_payroll_id !== undefined ? { external_payroll_id } : {}),
...(company_code !== undefined ? { company_code } : {}), ...(company_code !== undefined ? { company_code } : {}),
...(job_title !== undefined ? { job_title } : {}), ...(job_title !== undefined ? { job_title } : {}),
...(fw !== undefined ? { first_work_day: fw } : {}), ...(first_work_d !== undefined ? { first_work_day: first_work_d } : {}),
...(lw !== undefined ? { last_work_day: lw } : {}), ...(last_work_d !== undefined ? { last_work_day: last_work_d } : {}),
...(is_supervisor !== undefined ? { is_supervisor } : {}),
...(supervisor_id !== undefined ? { supervisor_id } : {}), ...(supervisor_id !== undefined ? { supervisor_id } : {}),
}, },
}); include: { user: true },
}); });
return this.prisma.employees.findUnique({ where: { id } }); return updated;
});
return this.prisma.employees.findFirst({ where: { user: {email} } });
} }
const user = await this.prisma.users.findUnique({where: {email}});
if(!user) return null;
// 2) Pas trouvé en actifs → regarder en archive (pour restauration) // 2) Pas trouvé en actifs → regarder en archive (pour restauration)
const archived = await this.prisma.employeesArchive.findFirst({ const archived = await this.prisma.employeesArchive.findFirst({
where: { employee_id: id }, where: { user_id: user.id },
include: { user: true }, include: { user: true },
}); });
@ -296,51 +325,57 @@ async patchEmployee(id: number, dto: UpdateEmployeeDto): Promise<Employees | Emp
return this.restoreEmployee(archived, dto); return this.restoreEmployee(archived, dto);
} }
} }
// 3) Ni actif, ni archivé → 404 dans le controller // 3) Ni actif, ni archivé → 404 dans le controller
return null; return null;
} }
//transfers the employee to archive and then delete from employees table //transfers the employee to archive and then delete from employees table
private async archiveOnTermination(existing: any, dto: UpdateEmployeeDto): Promise<any> { private async archiveOnTermination(active: Employees & {user: Users }, dto: UpdateEmployeeDto): Promise<EmployeesArchive> {
const last_work_d = toDateOrNull(dto.last_work_day!);
if(!last_work_d) throw new Error('invalide last_work_day for archive');
return this.prisma.$transaction(async transaction => { return this.prisma.$transaction(async transaction => {
//archive insertion //detach crew from supervisor if employee is a supervisor
await transaction.employees.updateMany({
where: { supervisor_id: active.id },
data: { supervisor_id: null },
})
const archived = await transaction.employeesArchive.create({ const archived = await transaction.employeesArchive.create({
data: { data: {
employee_id: existing.id, employee_id: active.id,
user_id: existing.user_id, user_id: active.user_id,
first_name: existing.first_name, first_name: active.user.first_name,
last_name: existing.last_name, last_name: active.user.last_name,
external_payroll_id: existing.external_payroll_id, external_payroll_id: active.external_payroll_id,
company_code: existing.company_code, company_code: active.company_code,
job_title: existing.job_title, job_title: active.job_title,
first_Work_Day: existing.first_Work_Day, first_work_day: active.first_work_day,
last_work_day: existing.last_work_day, last_work_day: last_work_d,
supervisor_id: existing.supervisor_id ?? null, supervisor_id: active.supervisor_id ?? null,
is_supervisor: existing.is_supervisor, is_supervisor: active.is_supervisor,
}, },
include: { user: true}
}); });
//delete from employees table //delete from employees table
await transaction.employees.delete({ where: { id: existing.id } }); await transaction.employees.delete({ where: { id: active.id } });
//return archived employee //return archived employee
return archived return archived
}); });
} }
//transfers the employee from archive to the employees table //transfers the employee from archive to the employees table
private async restoreEmployee(archived: any, dto: UpdateEmployeeDto): Promise<any> { private async restoreEmployee(archived: EmployeesArchive & { user:Users }, dto: UpdateEmployeeDto): Promise<Employees> {
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({
data: { data: {
id: archived.employee_id,
user_id: archived.user_id, user_id: archived.user_id,
external_payroll_id: dto.external_payroll_id ?? archived.external_payroll_id, external_payroll_id: archived.external_payroll_id,
company_code: dto.company_code ?? archived.company_code, company_code: archived.company_code,
job_title: dto.job_title ?? archived.job_title, job_title: archived.job_title,
first_work_day: dto.first_work_day ?? archived.first_Work_Day, first_work_day: archived.first_work_day,
last_work_day: null, last_work_day: null,
supervisor_id: dto.supervisor_id ?? archived.supervisor_id, is_supervisor: archived.is_supervisor ?? false,
}, },
}); });
//deleting archived entry by id //deleting archived entry by id

View File

@ -11,11 +11,11 @@ export class PayPeriodsCommandService {
async approvalPayPeriod(year: number , periodNumber: number): Promise<void> { async approvalPayPeriod(year: number , periodNumber: number): Promise<void> {
const period = await this.prisma.payPeriods.findFirst({ const period = await this.prisma.payPeriods.findFirst({
where: { period_number: periodNumber }, where: { year, period_number: periodNumber},
}); });
if (!period) throw new NotFoundException(`PayPeriod #${periodNumber} not found`); if (!period) throw new NotFoundException(`PayPeriod #${year}-${periodNumber} not found`);
//fetches timesheet of selected period if the timesheet as atleast 1 shift or 1 expense //fetches timesheet of selected period if the timesheet has atleast 1 shift or 1 expense
const timesheetList = await this.prisma.timesheets.findMany({ const timesheetList = await this.prisma.timesheets.findMany({
where: { where: {
OR: [ OR: [
@ -31,11 +31,14 @@ export class PayPeriodsCommandService {
}, },
], ],
}, },
select: { id: true },
}); });
//approval of both timesheet (cascading to the approval of related shifts and expenses) //approval of both timesheet (cascading to the approval of related shifts and expenses)
for(const timesheet of timesheetList) { await this.prisma.$transaction(async (transaction)=> {
await this.timesheetsApproval.updateApproval(timesheet.id, true); for(const {id} of timesheetList) {
} await this.timesheetsApproval.updateApprovalWithTx(transaction,id, true);
}
})
} }
} }

View File

@ -23,19 +23,19 @@ export class PayPeriodsQueryService {
} }
async getOverviewByYearPeriod(year: number, periodNumber: number): Promise<PayPeriodOverviewDto> { async getOverviewByYearPeriod(year: number, periodNumber: number): Promise<PayPeriodOverviewDto> {
const p = computePeriod(year, periodNumber); const period = computePeriod(year, periodNumber);
return this.buildOverview({ return this.buildOverview({
start_date: p.start_date, start_date: period.start_date,
end_date : p.end_date, end_date : period.end_date,
period_number: p.period_number, period_number: period.period_number,
year: p.year, year: period.year,
label:p.label, label:period.label,
} as any); } as any);
} }
private async buildOverview( private async buildOverview(
period: { start_date: string | Date; end_date: string | Date; period_number: number; year: number; label: string; }, period: { start_date: string | Date; end_date: string | Date; period_number: number; year: number; label: string; },
opts?: { restrictEmployeeIds?: number[]; seedNames?: Map<number, string> }, options?: { filteredEmployeeIds?: number[]; seedNames?: Map<number, string> },
): Promise<PayPeriodOverviewDto> { ): Promise<PayPeriodOverviewDto> {
const toDateString = (d: Date) => d.toISOString().slice(0, 10); const toDateString = (d: Date) => d.toISOString().slice(0, 10);
const toMoney = (v: any) => (typeof v === "object" && "toNumber" in v ? v.toNumber() : (v as number)); const toMoney = (v: any) => (typeof v === "object" && "toNumber" in v ? v.toNumber() : (v as number));
@ -48,7 +48,8 @@ export class PayPeriodsQueryService {
? period.end_date ? period.end_date
: new Date(`${period.end_date}T00:00:00.000Z`); : new Date(`${period.end_date}T00:00:00.000Z`);
const whereEmployee = opts?.restrictEmployeeIds?.length ? { employee_id: { in: opts.restrictEmployeeIds } }: {}; //restrictEmployeeIds = filter for shifts and expenses by employees
const whereEmployee = options?.filteredEmployeeIds?.length ? { employee_id: { in: options.filteredEmployeeIds } }: {};
// SHIFTS (filtered by crew) // SHIFTS (filtered by crew)
const shifts = await this.prisma.shifts.findMany({ const shifts = await this.prisma.shifts.findMany({
@ -99,8 +100,8 @@ export class PayPeriodsQueryService {
const byEmployee = new Map<number, EmployeePeriodOverviewDto>(); const byEmployee = new Map<number, EmployeePeriodOverviewDto>();
// seed for employee without data // seed for employee without data
if (opts?.seedNames) { if (options?.seedNames) {
for (const [id, name] of opts.seedNames.entries()) { for (const [id, name] of options.seedNames.entries()) {
byEmployee.set(id, { byEmployee.set(id, {
employee_id: id, employee_id: id,
employee_name: name, employee_name: name,
@ -132,37 +133,37 @@ export class PayPeriodsQueryService {
return byEmployee.get(id)!; return byEmployee.get(id)!;
}; };
for (const s of shifts) { for (const shift of shifts) {
const e = s.timesheet.employee; const employee = shift.timesheet.employee;
const name = `${e.user.first_name} ${e.user.last_name}`.trim(); const name = `${employee.user.first_name} ${employee.user.last_name}`.trim();
const rec = ensure(e.id, name); const rec = ensure(employee.id, name);
const hours = computeHours(s.start_time, s.end_time); const hours = computeHours(shift.start_time, shift.end_time);
const cat = (s.bank_code?.categorie || "REGULAR").toUpperCase(); const categorie = (shift.bank_code?.categorie || "REGULAR").toUpperCase();
switch (cat) { switch (categorie) {
case "EVENING": rec.evening_hours += hours; break; case "EVENING": rec.evening_hours += hours; break;
case "EMERGENCY": case "EMERGENCY":
case "URGENT": rec.emergency_hours += hours; break; case "URGENT": rec.emergency_hours += hours; break;
case "OVERTIME": rec.overtime_hours += hours; break; case "OVERTIME": rec.overtime_hours += hours; break;
default: rec.regular_hours += hours; break; default: rec.regular_hours += hours; break;
} }
rec.is_approved = rec.is_approved && s.timesheet.is_approved; rec.is_approved = rec.is_approved && shift.timesheet.is_approved;
} }
for (const ex of expenses) { for (const expense of expenses) {
const e = ex.timesheet.employee; const exp = expense.timesheet.employee;
const name = `${e.user.first_name} ${e.user.last_name}`.trim(); const name = `${exp.user.first_name} ${exp.user.last_name}`.trim();
const rec = ensure(e.id, name); const record = ensure(exp.id, name);
const amount = toMoney(ex.amount); const amount = toMoney(expense.amount);
rec.expenses += amount; record.expenses += amount;
const cat = (ex.bank_code?.categorie || "").toUpperCase(); const categorie = (expense.bank_code?.categorie || "").toUpperCase();
const rate = ex.bank_code?.modifier ?? 0; const rate = expense.bank_code?.modifier ?? 0;
if (cat === "MILEAGE" && rate > 0) { if (categorie === "MILEAGE" && rate > 0) {
rec.mileage += amount / rate; record.mileage += amount / rate;
} }
rec.is_approved = rec.is_approved && ex.timesheet.is_approved; record.is_approved = record.is_approved && expense.timesheet.is_approved;
} }
const employees_overview = Array.from(byEmployee.values()).sort((a, b) => const employees_overview = Array.from(byEmployee.values()).sort((a, b) =>
@ -199,7 +200,7 @@ export class PayPeriodsQueryService {
const seedNames = new Map<number, string>(crew.map(c => [c.id, `${c.first_name} ${c.last_name}`.trim()])); const seedNames = new Map<number, string>(crew.map(c => [c.id, `${c.first_name} ${c.last_name}`.trim()]));
// 4) overview build // 4) overview build
return this.buildOverview(period, { restrictEmployeeIds: crewIds, seedNames }); return this.buildOverview(period, { filteredEmployeeIds: crewIds, seedNames });
} }
private async resolveCrew(supervisorId: number, includeSubtree: boolean): Promise<Array<{ id: number; first_name: string; last_name: string }>> { private async resolveCrew(supervisorId: number, includeSubtree: boolean): Promise<Array<{ id: number; first_name: string; last_name: string }>> {

View File

@ -1,6 +1,6 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { Timesheets } from "@prisma/client"; import { Prisma, Timesheets } from "@prisma/client";
import { BaseApprovalService } from "src/common/shared/base-approval.service"; import { BaseApprovalService } from "src/common/shared/base-approval.service";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
@ -11,16 +11,25 @@ export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
protected get delegate() { protected get delegate() {
return this.prisma.timesheets; return this.prisma.timesheets;
} }
protected delegateFor(transaction: Prisma.TransactionClient) {
return transaction.timesheets;
}
async updateApproval(timesheetId: number, isApproved: boolean): Promise<Timesheets> { async updateApproval(id: number, isApproved: boolean): Promise<Timesheets> {
const timesheet = await super.updateApproval(timesheetId, isApproved); return this.prisma.$transaction((transaction) =>
this.updateApprovalWithTx(transaction, id, isApproved),
);
}
await this.prisma.shifts.updateMany({ async cascadeApprovalWithtx(transaction: Prisma.TransactionClient, timesheetId: number, isApproved: boolean): Promise<Timesheets> {
const timesheet = await this.updateApprovalWithTx(transaction, timesheetId, isApproved);
await transaction.shifts.updateMany({
where: { timesheet_id: timesheetId }, where: { timesheet_id: timesheetId },
data: { is_approved: isApproved }, data: { is_approved: isApproved },
}); });
await this.prisma.expenses.updateMany({ await transaction.expenses.updateManyAndReturn({
where: { timesheet_id: timesheetId }, where: { timesheet_id: timesheetId },
data: { is_approved: isApproved }, data: { is_approved: isApproved },
}); });