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:
parent
fe87c36884
commit
7a9adeec69
|
|
@ -253,16 +253,16 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/employees/{id}": {
|
||||
"/employees/{email}": {
|
||||
"get": {
|
||||
"operationId": "EmployeesController_findOne",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"name": "email",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
@ -295,10 +295,10 @@
|
|||
"operationId": "EmployeesController_remove",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"name": "email",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "Identifier of the employee to delete",
|
||||
"description": "Email of the employee to delete",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
|
|
@ -326,10 +326,10 @@
|
|||
"operationId": "EmployeesController_updateOrArchiveOrRestore",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"name": "email",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "Identifier of the employee",
|
||||
"description": "Email of the employee",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -24,7 +24,7 @@ async function main() {
|
|||
last_name: e.user.last_name,
|
||||
external_payroll_id: e.external_payroll_id,
|
||||
company_code: e.company_code,
|
||||
first_Work_Day: e.first_work_day,
|
||||
first_work_day: e.first_work_day,
|
||||
last_work_day: daysAgo(30),
|
||||
supervisor_id: e.supervisor_id ?? null,
|
||||
job_title: e.job_title,
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ model EmployeesArchive {
|
|||
is_supervisor Boolean
|
||||
external_payroll_id Int
|
||||
company_code Int
|
||||
first_Work_Day DateTime @db.Date
|
||||
first_work_day DateTime @db.Date
|
||||
last_work_day DateTime @db.Date
|
||||
supervisor_id Int?
|
||||
supervisor Employees? @relation("EmployeeSupervisorToArchive", fields: [supervisor_id], references: [id])
|
||||
|
|
|
|||
|
|
@ -1,26 +1,52 @@
|
|||
import { NotFoundException } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
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
|
||||
export abstract class BaseApprovalService<T> {
|
||||
protected constructor(protected readonly prisma: PrismaService) {}
|
||||
|
||||
//returns the corresponding Prisma delegate
|
||||
protected abstract get delegate(): {
|
||||
update(args: {where: {id: number };
|
||||
data: { is_approved: boolean }
|
||||
}): Promise<T>;
|
||||
};
|
||||
protected abstract get delegate(): UpdatableDelegate<T>;
|
||||
|
||||
protected abstract delegateFor(transaction: Prisma.TransactionClient): UpdatableDelegate<T>;
|
||||
|
||||
//standard update Aproval
|
||||
async updateApproval(id: number, isApproved: boolean): Promise<T> {
|
||||
const entity = await this.delegate.update({
|
||||
where: { id },
|
||||
data: { is_approved: isApproved },
|
||||
});
|
||||
|
||||
if(!entity) throw new NotFoundException(`Entity #${id} not found`);
|
||||
|
||||
return entity;
|
||||
try{
|
||||
return await this.delegate.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,13 +42,13 @@ export class EmployeesController {
|
|||
return this.employeesService.findListEmployees();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Get(':email')
|
||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING )
|
||||
@ApiOperation({summary: 'Find employee' })
|
||||
@ApiResponse({ status: 200, description: 'Employee found', type: CreateEmployeeDto })
|
||||
@ApiResponse({ status: 400, description: 'Employee not found' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number): Promise<Employees> {
|
||||
return this.employeesService.findOne(id);
|
||||
findOne(@Param('email', ParseIntPipe) email: string): Promise<Employees> {
|
||||
return this.employeesService.findOne(email);
|
||||
}
|
||||
|
||||
@Get('profile/:email')
|
||||
|
|
@ -60,31 +60,31 @@ export class EmployeesController {
|
|||
return this.employeesService.findOneProfile(email);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Delete(':email')
|
||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR )
|
||||
@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: 404, description: 'Employee not found' })
|
||||
remove(@Param('id', ParseIntPipe) id: number): Promise<Employees> {
|
||||
return this.employeesService.remove(id);
|
||||
remove(@Param('email', ParseIntPipe) email: string): Promise<Employees> {
|
||||
return this.employeesService.remove(email);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Patch(':email')
|
||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiBearerAuth('access-token')
|
||||
@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: 202, description: 'Employee archived successfully', type: CreateEmployeeDto })
|
||||
@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
|
||||
// else if employee is archived and first_work_day or last_work_day = null => restore
|
||||
//otherwise => standard update
|
||||
const result = await this.employeesService.patchEmployee(+id, dto);
|
||||
const result = await this.employeesService.patchEmployee(email, dto);
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { CreateEmployeeDto } from '../dtos/create-employee.dto';
|
|||
import { UpdateEmployeeDto } from '../dtos/update-employee.dto';
|
||||
import { Employees, EmployeesArchive, Users } from '@prisma/client';
|
||||
import { EmployeeListItemDto } from '../dtos/list-employee.dto';
|
||||
import { Roles as RoleEnum } from '@prisma/client';
|
||||
import { EmployeeProfileItemDto } from '../dtos/profil-employee.dto';
|
||||
|
||||
function toDateOrNull(v?: string | null): Date | null {
|
||||
|
|
@ -100,13 +99,15 @@ export class EmployeesService {
|
|||
);
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<Employees> {
|
||||
const emp = await this.prisma.employees.findUnique({
|
||||
where: { id },
|
||||
async findOne(email: string): Promise<Employees> {
|
||||
const emp = await this.prisma.employees.findFirst({
|
||||
where: { user: { email } },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
//add search for archived employees
|
||||
if (!emp) {
|
||||
throw new NotFoundException(`Employee #${id} not found`);
|
||||
throw new NotFoundException(`Employee with email: ${email} not found`);
|
||||
}
|
||||
return emp;
|
||||
}
|
||||
|
|
@ -156,16 +157,15 @@ export class EmployeesService {
|
|||
};
|
||||
}
|
||||
|
||||
async update(
|
||||
id: number,
|
||||
async update(
|
||||
email: string,
|
||||
dto: UpdateEmployeeDto,
|
||||
): Promise<Employees> {
|
||||
const emp = await this.findOne(id);
|
||||
const emp = await this.findOne(email);
|
||||
|
||||
const {
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
phone_number,
|
||||
residence,
|
||||
external_payroll_id,
|
||||
|
|
@ -173,23 +173,32 @@ async update(
|
|||
job_title,
|
||||
first_work_day,
|
||||
last_work_day,
|
||||
is_supervisor
|
||||
is_supervisor,
|
||||
email: newEmail,
|
||||
} = dto;
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
await tx.users.update({
|
||||
where: { id: emp.user_id },
|
||||
data: {
|
||||
...(first_name !== undefined && { first_name }),
|
||||
...(last_name !== undefined && { last_name }),
|
||||
...(email !== undefined && { email }),
|
||||
...(phone_number !== undefined && { phone_number }),
|
||||
...(residence !== undefined && { residence }),
|
||||
},
|
||||
});
|
||||
if(
|
||||
first_name !== undefined ||
|
||||
last_name !== undefined ||
|
||||
newEmail !== undefined ||
|
||||
phone_number !== undefined ||
|
||||
residence !== undefined
|
||||
){
|
||||
await tx.users.update({
|
||||
where: { id: emp.user_id },
|
||||
data: {
|
||||
...(first_name !== undefined && { first_name }),
|
||||
...(last_name !== undefined && { last_name }),
|
||||
...(email !== undefined && { email }),
|
||||
...(phone_number !== undefined && { phone_number }),
|
||||
...(residence !== undefined && { residence }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return tx.employees.update({
|
||||
where: { id },
|
||||
const updated = await tx.employees.update({
|
||||
where: { id: emp.id },
|
||||
data: {
|
||||
...(external_payroll_id !== undefined && { external_payroll_id }),
|
||||
...(company_code !== undefined && { company_code }),
|
||||
|
|
@ -199,36 +208,51 @@ async update(
|
|||
...(is_supervisor !== undefined && { is_supervisor }),
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async remove(id: number): Promise<Employees> {
|
||||
await this.findOne(id);
|
||||
return this.prisma.employees.delete({ where: { id } });
|
||||
async remove(email: string): Promise<Employees> {
|
||||
|
||||
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 ******************************************************
|
||||
|
||||
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
|
||||
const existing = await this.prisma.employees.findUnique({
|
||||
where: { id },
|
||||
const active = await this.prisma.employees.findFirst({
|
||||
where: { user: { email } },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
if (active) {
|
||||
// Archivage : si on reçoit un last_work_day défini et que l'employé n’est pas déjà terminé
|
||||
if (dto.last_work_day !== undefined && existing.last_work_day == null && dto.last_work_day !== null) {
|
||||
return this.archiveOnTermination(existing, dto);
|
||||
if (dto.last_work_day !== undefined && active.last_work_day == null && dto.last_work_day !== null) {
|
||||
return this.archiveOnTermination(active, dto);
|
||||
}
|
||||
|
||||
// Sinon, update standard (split Users/Employees)
|
||||
const {
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
email: newEmail,
|
||||
phone_number,
|
||||
residence,
|
||||
external_payroll_id,
|
||||
|
|
@ -238,54 +262,59 @@ async patchEmployee(id: number, dto: UpdateEmployeeDto): Promise<Employees | Emp
|
|||
last_work_day,
|
||||
supervisor_id,
|
||||
is_supervisor,
|
||||
} = dto;
|
||||
} = dto as any;
|
||||
|
||||
const fw = toDateOrUndefined(first_work_day);
|
||||
const lw = (dto.hasOwnProperty('last_work_day'))
|
||||
const first_work_d = toDateOrUndefined(first_work_day);
|
||||
const last_work_d = Object.prototype.hasOwnProperty('last_work_day')
|
||||
? toDateOrNull(last_work_day ?? null)
|
||||
: undefined;
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const willUpdateUser =
|
||||
first_name !== undefined ||
|
||||
last_name !== undefined ||
|
||||
email !== undefined ||
|
||||
if(
|
||||
first_name !== undefined ||
|
||||
last_name !== undefined ||
|
||||
newEmail !== undefined ||
|
||||
phone_number !== undefined ||
|
||||
residence !== undefined;
|
||||
|
||||
if (willUpdateUser) {
|
||||
residence !== undefined
|
||||
) {
|
||||
await tx.users.update({
|
||||
where: { id: existing.user_id },
|
||||
where: { id: active.user_id },
|
||||
data: {
|
||||
...(first_name !== undefined ? { first_name } : {}),
|
||||
...(last_name !== undefined ? { last_name } : {}),
|
||||
...(email !== undefined ? { email } : {}),
|
||||
...(phone_number !== undefined ? { phone_number } : {}),
|
||||
...(residence !== undefined ? { residence } : {}),
|
||||
...(is_supervisor !== undefined ? { is_supervisor }: {}),
|
||||
...(first_name !== undefined ? { first_name } : {}),
|
||||
...(last_name !== undefined ? { last_name } : {}),
|
||||
...(email !== undefined ? { email: newEmail }: {}),
|
||||
...(phone_number !== undefined ? { phone_number } : {}),
|
||||
...(residence !== undefined ? { residence } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
await tx.employees.update({
|
||||
where: { id },
|
||||
const updated = await tx.employees.update({
|
||||
where: { id: active.id },
|
||||
data: {
|
||||
...(external_payroll_id !== undefined ? { external_payroll_id } : {}),
|
||||
...(company_code !== undefined ? { company_code } : {}),
|
||||
...(job_title !== undefined ? { job_title } : {}),
|
||||
...(fw !== undefined ? { first_work_day: fw } : {}),
|
||||
...(lw !== undefined ? { last_work_day: lw } : {}),
|
||||
...(supervisor_id !== undefined ? { supervisor_id } : {}),
|
||||
...(external_payroll_id !== undefined ? { external_payroll_id } : {}),
|
||||
...(company_code !== undefined ? { company_code } : {}),
|
||||
...(job_title !== undefined ? { job_title } : {}),
|
||||
...(first_work_d !== undefined ? { first_work_day: first_work_d } : {}),
|
||||
...(last_work_d !== undefined ? { last_work_day: last_work_d } : {}),
|
||||
...(is_supervisor !== undefined ? { is_supervisor } : {}),
|
||||
...(supervisor_id !== undefined ? { supervisor_id } : {}),
|
||||
},
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
return this.prisma.employees.findUnique({ where: { id } });
|
||||
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)
|
||||
const archived = await this.prisma.employeesArchive.findFirst({
|
||||
where: { employee_id: id },
|
||||
where: { user_id: user.id },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
|
|
@ -296,51 +325,57 @@ async patchEmployee(id: number, dto: UpdateEmployeeDto): Promise<Employees | Emp
|
|||
return this.restoreEmployee(archived, dto);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Ni actif, ni archivé → 404 dans le controller
|
||||
return null;
|
||||
}
|
||||
|
||||
//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 => {
|
||||
//archive insertion
|
||||
const archived = await transaction.employeesArchive.create({
|
||||
data: {
|
||||
employee_id: existing.id,
|
||||
user_id: existing.user_id,
|
||||
first_name: existing.first_name,
|
||||
last_name: existing.last_name,
|
||||
external_payroll_id: existing.external_payroll_id,
|
||||
company_code: existing.company_code,
|
||||
job_title: existing.job_title,
|
||||
first_Work_Day: existing.first_Work_Day,
|
||||
last_work_day: existing.last_work_day,
|
||||
supervisor_id: existing.supervisor_id ?? null,
|
||||
is_supervisor: existing.is_supervisor,
|
||||
},
|
||||
});
|
||||
//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({
|
||||
data: {
|
||||
employee_id: active.id,
|
||||
user_id: active.user_id,
|
||||
first_name: active.user.first_name,
|
||||
last_name: active.user.last_name,
|
||||
external_payroll_id: active.external_payroll_id,
|
||||
company_code: active.company_code,
|
||||
job_title: active.job_title,
|
||||
first_work_day: active.first_work_day,
|
||||
last_work_day: last_work_d,
|
||||
supervisor_id: active.supervisor_id ?? null,
|
||||
is_supervisor: active.is_supervisor,
|
||||
},
|
||||
include: { user: true}
|
||||
});
|
||||
//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
|
||||
return archived
|
||||
});
|
||||
}
|
||||
|
||||
//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 => {
|
||||
//restores the archived employee into the employees table
|
||||
const restored = await transaction.employees.create({
|
||||
data: {
|
||||
id: archived.employee_id,
|
||||
user_id: archived.user_id,
|
||||
external_payroll_id: dto.external_payroll_id ?? archived.external_payroll_id,
|
||||
company_code: dto.company_code ?? archived.company_code,
|
||||
job_title: dto.job_title ?? archived.job_title,
|
||||
first_work_day: dto.first_work_day ?? archived.first_Work_Day,
|
||||
external_payroll_id: archived.external_payroll_id,
|
||||
company_code: archived.company_code,
|
||||
job_title: archived.job_title,
|
||||
first_work_day: archived.first_work_day,
|
||||
last_work_day: null,
|
||||
supervisor_id: dto.supervisor_id ?? archived.supervisor_id,
|
||||
is_supervisor: archived.is_supervisor ?? false,
|
||||
},
|
||||
});
|
||||
//deleting archived entry by id
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ export class PayPeriodsCommandService {
|
|||
|
||||
async approvalPayPeriod(year: number , periodNumber: number): Promise<void> {
|
||||
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({
|
||||
where: {
|
||||
OR: [
|
||||
|
|
@ -31,11 +31,14 @@ export class PayPeriodsCommandService {
|
|||
},
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
//approval of both timesheet (cascading to the approval of related shifts and expenses)
|
||||
for(const timesheet of timesheetList) {
|
||||
await this.timesheetsApproval.updateApproval(timesheet.id, true);
|
||||
}
|
||||
await this.prisma.$transaction(async (transaction)=> {
|
||||
for(const {id} of timesheetList) {
|
||||
await this.timesheetsApproval.updateApprovalWithTx(transaction,id, true);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -23,19 +23,19 @@ export class PayPeriodsQueryService {
|
|||
}
|
||||
|
||||
async getOverviewByYearPeriod(year: number, periodNumber: number): Promise<PayPeriodOverviewDto> {
|
||||
const p = computePeriod(year, periodNumber);
|
||||
const period = computePeriod(year, periodNumber);
|
||||
return this.buildOverview({
|
||||
start_date: p.start_date,
|
||||
end_date : p.end_date,
|
||||
period_number: p.period_number,
|
||||
year: p.year,
|
||||
label:p.label,
|
||||
start_date: period.start_date,
|
||||
end_date : period.end_date,
|
||||
period_number: period.period_number,
|
||||
year: period.year,
|
||||
label:period.label,
|
||||
} as any);
|
||||
}
|
||||
|
||||
private async buildOverview(
|
||||
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> {
|
||||
const toDateString = (d: Date) => d.toISOString().slice(0, 10);
|
||||
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
|
||||
: 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)
|
||||
const shifts = await this.prisma.shifts.findMany({
|
||||
|
|
@ -99,8 +100,8 @@ export class PayPeriodsQueryService {
|
|||
const byEmployee = new Map<number, EmployeePeriodOverviewDto>();
|
||||
|
||||
// seed for employee without data
|
||||
if (opts?.seedNames) {
|
||||
for (const [id, name] of opts.seedNames.entries()) {
|
||||
if (options?.seedNames) {
|
||||
for (const [id, name] of options.seedNames.entries()) {
|
||||
byEmployee.set(id, {
|
||||
employee_id: id,
|
||||
employee_name: name,
|
||||
|
|
@ -132,37 +133,37 @@ export class PayPeriodsQueryService {
|
|||
return byEmployee.get(id)!;
|
||||
};
|
||||
|
||||
for (const s of shifts) {
|
||||
const e = s.timesheet.employee;
|
||||
const name = `${e.user.first_name} ${e.user.last_name}`.trim();
|
||||
const rec = ensure(e.id, name);
|
||||
for (const shift of shifts) {
|
||||
const employee = shift.timesheet.employee;
|
||||
const name = `${employee.user.first_name} ${employee.user.last_name}`.trim();
|
||||
const rec = ensure(employee.id, name);
|
||||
|
||||
const hours = computeHours(s.start_time, s.end_time);
|
||||
const cat = (s.bank_code?.categorie || "REGULAR").toUpperCase();
|
||||
switch (cat) {
|
||||
const hours = computeHours(shift.start_time, shift.end_time);
|
||||
const categorie = (shift.bank_code?.categorie || "REGULAR").toUpperCase();
|
||||
switch (categorie) {
|
||||
case "EVENING": rec.evening_hours += hours; break;
|
||||
case "EMERGENCY":
|
||||
case "URGENT": rec.emergency_hours += hours; break;
|
||||
case "OVERTIME": rec.overtime_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) {
|
||||
const e = ex.timesheet.employee;
|
||||
const name = `${e.user.first_name} ${e.user.last_name}`.trim();
|
||||
const rec = ensure(e.id, name);
|
||||
for (const expense of expenses) {
|
||||
const exp = expense.timesheet.employee;
|
||||
const name = `${exp.user.first_name} ${exp.user.last_name}`.trim();
|
||||
const record = ensure(exp.id, name);
|
||||
|
||||
const amount = toMoney(ex.amount);
|
||||
rec.expenses += amount;
|
||||
const amount = toMoney(expense.amount);
|
||||
record.expenses += amount;
|
||||
|
||||
const cat = (ex.bank_code?.categorie || "").toUpperCase();
|
||||
const rate = ex.bank_code?.modifier ?? 0;
|
||||
if (cat === "MILEAGE" && rate > 0) {
|
||||
rec.mileage += amount / rate;
|
||||
const categorie = (expense.bank_code?.categorie || "").toUpperCase();
|
||||
const rate = expense.bank_code?.modifier ?? 0;
|
||||
if (categorie === "MILEAGE" && rate > 0) {
|
||||
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) =>
|
||||
|
|
@ -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()]));
|
||||
|
||||
// 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 }>> {
|
||||
|
|
|
|||
|
|
@ -1,26 +1,35 @@
|
|||
|
||||
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 { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
||||
constructor(prisma: PrismaService) {super(prisma);}
|
||||
|
||||
|
||||
protected get delegate() {
|
||||
return this.prisma.timesheets;
|
||||
}
|
||||
protected delegateFor(transaction: Prisma.TransactionClient) {
|
||||
return transaction.timesheets;
|
||||
}
|
||||
|
||||
async updateApproval(timesheetId: number, isApproved: boolean): Promise<Timesheets> {
|
||||
const timesheet = await super.updateApproval(timesheetId, isApproved);
|
||||
async updateApproval(id: number, isApproved: boolean): Promise<Timesheets> {
|
||||
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 },
|
||||
data: { is_approved: isApproved },
|
||||
});
|
||||
|
||||
await this.prisma.expenses.updateMany({
|
||||
await transaction.expenses.updateManyAndReturn({
|
||||
where: { timesheet_id: timesheetId },
|
||||
data: { is_approved: isApproved },
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user