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": {
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
where: { id },
|
return await this.delegate.update({
|
||||||
data: { is_approved: isApproved },
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(!entity) throw new NotFoundException(`Entity #${id} not found`);
|
//approval with transaction to avoid many requests
|
||||||
|
async updateApprovalWithTx(transaction: Prisma.TransactionClient, id: number, isApproved: boolean): Promise<T> {
|
||||||
return entity;
|
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();
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
@ -156,16 +157,15 @@ 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,23 +173,32 @@ 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) => {
|
||||||
await tx.users.update({
|
if(
|
||||||
where: { id: emp.user_id },
|
first_name !== undefined ||
|
||||||
data: {
|
last_name !== undefined ||
|
||||||
...(first_name !== undefined && { first_name }),
|
newEmail !== undefined ||
|
||||||
...(last_name !== undefined && { last_name }),
|
phone_number !== undefined ||
|
||||||
...(email !== undefined && { email }),
|
residence !== undefined
|
||||||
...(phone_number !== undefined && { phone_number }),
|
){
|
||||||
...(residence !== undefined && { residence }),
|
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({
|
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é n’est pas déjà terminé
|
// 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) {
|
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 } : {}),
|
||||||
...(supervisor_id !== undefined ? { supervisor_id } : {}),
|
...(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)
|
// 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
|
||||||
const archived = await transaction.employeesArchive.create({
|
await transaction.employees.updateMany({
|
||||||
data: {
|
where: { supervisor_id: active.id },
|
||||||
employee_id: existing.id,
|
data: { supervisor_id: null },
|
||||||
user_id: existing.user_id,
|
})
|
||||||
first_name: existing.first_name,
|
const archived = await transaction.employeesArchive.create({
|
||||||
last_name: existing.last_name,
|
data: {
|
||||||
external_payroll_id: existing.external_payroll_id,
|
employee_id: active.id,
|
||||||
company_code: existing.company_code,
|
user_id: active.user_id,
|
||||||
job_title: existing.job_title,
|
first_name: active.user.first_name,
|
||||||
first_Work_Day: existing.first_Work_Day,
|
last_name: active.user.last_name,
|
||||||
last_work_day: existing.last_work_day,
|
external_payroll_id: active.external_payroll_id,
|
||||||
supervisor_id: existing.supervisor_id ?? null,
|
company_code: active.company_code,
|
||||||
is_supervisor: existing.is_supervisor,
|
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
|
//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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 }>> {
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user