fix(modules): deprecate old methods and extract utils and helpers. created archival services.
This commit is contained in:
parent
79c5bec0ee
commit
57b74b1726
|
|
@ -1,92 +1,93 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { CreateCustomerDto } from '../dtos/create-customer.dto';
|
||||
import { Customers, Users } from '@prisma/client';
|
||||
import { UpdateCustomerDto } from '../dtos/update-customer.dto';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class CustomersService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async create(dto: CreateCustomerDto): Promise<Customers> {
|
||||
const {
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
phone_number,
|
||||
residence,
|
||||
invoice_id,
|
||||
} = dto;
|
||||
//_____________________________________________________________________________________________
|
||||
// Deprecated or unused methods
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
return this.prisma.$transaction(async (transaction) => {
|
||||
const user: Users = await transaction.users.create({
|
||||
data: {
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
phone_number,
|
||||
residence,
|
||||
},
|
||||
});
|
||||
return transaction.customers.create({
|
||||
data: {
|
||||
user_id: user.id,
|
||||
invoice_id,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
findAll(): Promise<Customers[]> {
|
||||
return this.prisma.customers.findMany({
|
||||
include: { user: true },
|
||||
})
|
||||
}
|
||||
|
||||
async findOne(id:number): Promise<Customers> {
|
||||
const customer = await this.prisma.customers.findUnique({
|
||||
where: { id },
|
||||
include: { user: true },
|
||||
});
|
||||
if(!customer) throw new NotFoundException(`Customer #${id} not found`);
|
||||
return customer;
|
||||
}
|
||||
|
||||
async update(id: number,dto: UpdateCustomerDto): Promise<Customers> {
|
||||
const customer = await this.findOne(id);
|
||||
|
||||
const {
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
phone_number,
|
||||
residence,
|
||||
invoice_id,
|
||||
} = dto;
|
||||
|
||||
return this.prisma.$transaction(async (transaction) => {
|
||||
await transaction.users.update({
|
||||
where: { id: customer.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 transaction.customers.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(invoice_id !== undefined && { invoice_id }),
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async remove(id: number): Promise<Customers> {
|
||||
await this.findOne(id);
|
||||
return this.prisma.customers.delete({ where: { id }});
|
||||
}
|
||||
// constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
// async create(dto: CreateCustomerDto): Promise<Customers> {
|
||||
// const {
|
||||
// first_name,
|
||||
// last_name,
|
||||
// email,
|
||||
// phone_number,
|
||||
// residence,
|
||||
// invoice_id,
|
||||
// } = dto;
|
||||
|
||||
// return this.prisma.$transaction(async (transaction) => {
|
||||
// const user: Users = await transaction.users.create({
|
||||
// data: {
|
||||
// first_name,
|
||||
// last_name,
|
||||
// email,
|
||||
// phone_number,
|
||||
// residence,
|
||||
// },
|
||||
// });
|
||||
// return transaction.customers.create({
|
||||
// data: {
|
||||
// user_id: user.id,
|
||||
// invoice_id,
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// findAll(): Promise<Customers[]> {
|
||||
// return this.prisma.customers.findMany({
|
||||
// include: { user: true },
|
||||
// })
|
||||
// }
|
||||
|
||||
// async findOne(id:number): Promise<Customers> {
|
||||
// const customer = await this.prisma.customers.findUnique({
|
||||
// where: { id },
|
||||
// include: { user: true },
|
||||
// });
|
||||
// if(!customer) throw new NotFoundException(`Customer #${id} not found`);
|
||||
// return customer;
|
||||
// }
|
||||
|
||||
// async update(id: number,dto: UpdateCustomerDto): Promise<Customers> {
|
||||
// const customer = await this.findOne(id);
|
||||
|
||||
// const {
|
||||
// first_name,
|
||||
// last_name,
|
||||
// email,
|
||||
// phone_number,
|
||||
// residence,
|
||||
// invoice_id,
|
||||
// } = dto;
|
||||
|
||||
// return this.prisma.$transaction(async (transaction) => {
|
||||
// await transaction.users.update({
|
||||
// where: { id: customer.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 transaction.customers.update({
|
||||
// where: { id },
|
||||
// data: {
|
||||
// ...(invoice_id !== undefined && { invoice_id }),
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// async remove(id: number): Promise<Customers> {
|
||||
// await this.findOne(id);
|
||||
// return this.prisma.customers.delete({ where: { id }});
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,21 @@
|
|||
import { Body,Controller,Delete,Get,NotFoundException,Param,ParseIntPipe,Patch,Post,UseGuards } from '@nestjs/common';
|
||||
import { Employees, Roles as RoleEnum } from '@prisma/client';
|
||||
import { Body,Controller,Get,NotFoundException,Param,Patch } from '@nestjs/common';
|
||||
import { EmployeesService } from '../services/employees.service';
|
||||
import { CreateEmployeeDto } from '../dtos/create-employee.dto';
|
||||
import { UpdateEmployeeDto } from '../dtos/update-employee.dto';
|
||||
import { RolesAllowed } from '../../../common/decorators/roles.decorators';
|
||||
import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { EmployeeListItemDto } from '../dtos/list-employee.dto';
|
||||
import { EmployeeProfileItemDto } from '../dtos/profil-employee.dto';
|
||||
import { EmployeesArchivalService } from '../services/employees-archival.service';
|
||||
|
||||
@ApiTags('Employees')
|
||||
@ApiBearerAuth('access-token')
|
||||
// @UseGuards()
|
||||
@Controller('employees')
|
||||
export class EmployeesController {
|
||||
constructor(private readonly employeesService: EmployeesService) {}
|
||||
|
||||
@Post()
|
||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({summary: 'Create employee' })
|
||||
@ApiResponse({ status: 201, description: 'Employee created', type: CreateEmployeeDto })
|
||||
@ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
||||
create(@Body() dto: CreateEmployeeDto): Promise<Employees> {
|
||||
return this.employeesService.create(dto);
|
||||
}
|
||||
constructor(
|
||||
private readonly employeesService: EmployeesService,
|
||||
private readonly archiveService: EmployeesArchivalService,
|
||||
) {}
|
||||
|
||||
@Get('employee-list')
|
||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING)
|
||||
|
|
@ -45,7 +38,7 @@ export class EmployeesController {
|
|||
// 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(email, dto);
|
||||
const result = await this.archiveService.patchEmployee(email, dto);
|
||||
if(!result) {
|
||||
throw new NotFoundException(`Employee with email: ${ email } is not found in active or archive.`)
|
||||
}
|
||||
|
|
@ -56,6 +49,14 @@ export class EmployeesController {
|
|||
// Deprecated or unused methods
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
// @Post()
|
||||
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({summary: 'Create employee' })
|
||||
// @ApiResponse({ status: 201, description: 'Employee created', type: CreateEmployeeDto })
|
||||
// @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
||||
// create(@Body() dto: CreateEmployeeDto): Promise<Employees> {
|
||||
// return this.employeesService.create(dto);
|
||||
// }
|
||||
// @Get()
|
||||
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING)
|
||||
// @ApiOperation({summary: 'Find all employees' })
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { EmployeesController } from './controllers/employees.controller';
|
||||
import { EmployeesService } from './services/employees.service';
|
||||
import { EmployeesArchivalService } from './services/employees-archival.service';
|
||||
|
||||
@Module({
|
||||
controllers: [EmployeesController],
|
||||
providers: [EmployeesService],
|
||||
exports: [EmployeesService],
|
||||
providers: [EmployeesService, EmployeesArchivalService],
|
||||
exports: [EmployeesService, EmployeesArchivalService],
|
||||
})
|
||||
export class EmployeesModule {}
|
||||
|
|
|
|||
173
src/modules/employees/services/employees-archival.service.ts
Normal file
173
src/modules/employees/services/employees-archival.service.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { Employees, EmployeesArchive, Users } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { UpdateEmployeeDto } from "../dtos/update-employee.dto";
|
||||
import { toDateOrUndefined, toDateOrNull } from "../utils/employee.utils";
|
||||
|
||||
@Injectable()
|
||||
export class EmployeesArchivalService {
|
||||
constructor(private readonly prisma: PrismaService) { }
|
||||
|
||||
async patchEmployee(email: string, dto: UpdateEmployeeDto): Promise<Employees | EmployeesArchive | null> {
|
||||
// 1) Tenter sur employés actifs
|
||||
const active = await this.prisma.employees.findFirst({
|
||||
where: { user: { email } },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
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 && 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: new_email,
|
||||
phone_number,
|
||||
residence,
|
||||
external_payroll_id,
|
||||
company_code,
|
||||
job_title,
|
||||
first_work_day,
|
||||
last_work_day,
|
||||
supervisor_id,
|
||||
is_supervisor,
|
||||
} = dto as any;
|
||||
|
||||
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 (transaction) => {
|
||||
if (
|
||||
first_name !== undefined ||
|
||||
last_name !== undefined ||
|
||||
new_email !== undefined ||
|
||||
phone_number !== undefined ||
|
||||
residence !== undefined
|
||||
) {
|
||||
await transaction.users.update({
|
||||
where: { id: active.user_id },
|
||||
data: {
|
||||
...(first_name !== undefined ? { first_name } : {}),
|
||||
...(last_name !== undefined ? { last_name } : {}),
|
||||
...(email !== undefined ? { email: new_email } : {}),
|
||||
...(phone_number !== undefined ? { phone_number } : {}),
|
||||
...(residence !== undefined ? { residence } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const updated = await transaction.employees.update({
|
||||
where: { id: active.id },
|
||||
data: {
|
||||
...(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.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: { user_id: user.id },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
if (archived) {
|
||||
// Condition de restauration : last_work_day === null ou first_work_day fourni
|
||||
const restore = dto.last_work_day === null || dto.first_work_day != null;
|
||||
if (restore) {
|
||||
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(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 => {
|
||||
//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,
|
||||
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,
|
||||
external_payroll_id: active.external_payroll_id,
|
||||
},
|
||||
include: { user: true }
|
||||
});
|
||||
//delete from employees table
|
||||
await transaction.employees.delete({ where: { id: active.id } });
|
||||
//return archived employee
|
||||
return archived
|
||||
});
|
||||
}
|
||||
|
||||
//transfers the employee from archive to the employees table
|
||||
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: {
|
||||
user_id: archived.user_id,
|
||||
company_code: archived.company_code,
|
||||
job_title: archived.job_title,
|
||||
first_work_day: archived.first_work_day,
|
||||
last_work_day: null,
|
||||
is_supervisor: archived.is_supervisor ?? false,
|
||||
external_payroll_id: archived.external_payroll_id,
|
||||
},
|
||||
});
|
||||
//deleting archived entry by id
|
||||
await transaction.employeesArchive.delete({ where: { id: archived.id } });
|
||||
|
||||
//return restored employee
|
||||
return restored;
|
||||
});
|
||||
}
|
||||
|
||||
//fetches all archived employees
|
||||
async findAllArchived(): Promise<EmployeesArchive[]> {
|
||||
return this.prisma.employeesArchive.findMany();
|
||||
}
|
||||
|
||||
//fetches an archived employee
|
||||
async findOneArchived(id: number): Promise<EmployeesArchive> {
|
||||
return this.prisma.employeesArchive.findUniqueOrThrow({ where: { id } });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -1,70 +1,12 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
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 { EmployeeProfileItemDto } from '../dtos/profil-employee.dto';
|
||||
|
||||
function toDateOrNull(v?: string | null): Date | null {
|
||||
if (!v) return null;
|
||||
const day = new Date(v);
|
||||
return isNaN(day.getTime()) ? null : day;
|
||||
}
|
||||
function toDateOrUndefined(v?: string | null): Date | undefined {
|
||||
const day = toDateOrNull(v ?? undefined);
|
||||
return day === null ? undefined : day;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class EmployeesService {
|
||||
constructor(private readonly prisma: PrismaService) { }
|
||||
|
||||
async create(dto: CreateEmployeeDto): Promise<Employees> {
|
||||
const {
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
phone_number,
|
||||
residence,
|
||||
external_payroll_id,
|
||||
company_code,
|
||||
job_title,
|
||||
first_work_day,
|
||||
last_work_day,
|
||||
is_supervisor,
|
||||
} = dto;
|
||||
|
||||
return this.prisma.$transaction(async (transaction) => {
|
||||
const user: Users = await transaction.users.create({
|
||||
data: {
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
phone_number,
|
||||
residence,
|
||||
},
|
||||
});
|
||||
return transaction.employees.create({
|
||||
data: {
|
||||
user_id: user.id,
|
||||
external_payroll_id,
|
||||
company_code,
|
||||
job_title,
|
||||
first_work_day,
|
||||
last_work_day,
|
||||
is_supervisor,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
findAll(): Promise<Employees[]> {
|
||||
return this.prisma.employees.findMany({
|
||||
include: { user: true },
|
||||
});
|
||||
}
|
||||
|
||||
findListEmployees(): Promise<EmployeeListItemDto[]> {
|
||||
return this.prisma.employees.findMany({
|
||||
select: {
|
||||
|
|
@ -91,27 +33,16 @@ export class EmployeesService {
|
|||
}).then(rows => rows.map(r => ({
|
||||
first_name: r.user.first_name,
|
||||
last_name: r.user.last_name,
|
||||
employee_full_name: `${r.user.first_name} ${r.user.last_name}`,
|
||||
email: r.user.email,
|
||||
supervisor_full_name: r.supervisor ? `${r.supervisor.user.first_name} ${r.supervisor.user.last_name}` : null,
|
||||
company_name: r.company_code,
|
||||
job_title: r.job_title,
|
||||
employee_full_name: `${r.user.first_name} ${r.user.last_name}`,
|
||||
supervisor_full_name: r.supervisor ? `${r.supervisor.user.first_name} ${r.supervisor.user.last_name}` : null,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
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 with email: ${email} not found`);
|
||||
}
|
||||
return emp;
|
||||
}
|
||||
|
||||
async findOneProfile(email: string): Promise<EmployeeProfileItemDto> {
|
||||
const emp = await this.prisma.employees.findFirst({
|
||||
|
|
@ -147,255 +78,155 @@ export class EmployeesService {
|
|||
return {
|
||||
first_name: emp.user.first_name,
|
||||
last_name: emp.user.last_name,
|
||||
employee_full_name: `${emp.user.first_name} ${emp.user.last_name}`,
|
||||
email: emp.user.email,
|
||||
residence: emp.user.residence,
|
||||
phone_number: emp.user.phone_number,
|
||||
supervisor_full_name: emp.supervisor ? `${emp.supervisor.user.first_name}, ${emp.supervisor.user.last_name}` : null,
|
||||
company_name: emp.company_code,
|
||||
job_title: emp.job_title,
|
||||
employee_full_name: `${emp.user.first_name} ${emp.user.last_name}`,
|
||||
first_work_day: emp.first_work_day.toISOString().slice(0, 10),
|
||||
last_work_day: emp.last_work_day ? emp.last_work_day.toISOString().slice(0, 10) : null,
|
||||
supervisor_full_name: emp.supervisor ? `${emp.supervisor.user.first_name}, ${emp.supervisor.user.last_name}` : null,
|
||||
};
|
||||
}
|
||||
|
||||
async update(
|
||||
email: string,
|
||||
dto: UpdateEmployeeDto,
|
||||
): Promise<Employees> {
|
||||
const emp = await this.findOne(email);
|
||||
//_____________________________________________________________________________________________
|
||||
// Deprecated or unused methods
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
const {
|
||||
first_name,
|
||||
last_name,
|
||||
phone_number,
|
||||
residence,
|
||||
external_payroll_id,
|
||||
company_code,
|
||||
job_title,
|
||||
first_work_day,
|
||||
last_work_day,
|
||||
is_supervisor,
|
||||
email: new_email,
|
||||
} = dto;
|
||||
// async create(dto: CreateEmployeeDto): Promise<Employees> {
|
||||
// const {
|
||||
// first_name,
|
||||
// last_name,
|
||||
// email,
|
||||
// phone_number,
|
||||
// residence,
|
||||
// external_payroll_id,
|
||||
// company_code,
|
||||
// job_title,
|
||||
// first_work_day,
|
||||
// last_work_day,
|
||||
// is_supervisor,
|
||||
// } = dto;
|
||||
|
||||
return this.prisma.$transaction(async (transaction) => {
|
||||
if(
|
||||
first_name !== undefined ||
|
||||
last_name !== undefined ||
|
||||
new_email !== undefined ||
|
||||
phone_number !== undefined ||
|
||||
residence !== undefined
|
||||
){
|
||||
await transaction.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 this.prisma.$transaction(async (transaction) => {
|
||||
// const user: Users = await transaction.users.create({
|
||||
// data: {
|
||||
// first_name,
|
||||
// last_name,
|
||||
// email,
|
||||
// phone_number,
|
||||
// residence,
|
||||
// },
|
||||
// });
|
||||
// return transaction.employees.create({
|
||||
// data: {
|
||||
// user_id: user.id,
|
||||
// external_payroll_id,
|
||||
// company_code,
|
||||
// job_title,
|
||||
// first_work_day,
|
||||
// last_work_day,
|
||||
// is_supervisor,
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
const updated = await transaction.employees.update({
|
||||
where: { id: emp.id },
|
||||
data: {
|
||||
...(external_payroll_id !== undefined && { external_payroll_id }),
|
||||
...(company_code !== undefined && { company_code }),
|
||||
...(first_work_day !== undefined && { first_work_day }),
|
||||
...(last_work_day !== undefined && { last_work_day }),
|
||||
...(job_title !== undefined && { job_title }),
|
||||
...(is_supervisor !== undefined && { is_supervisor }),
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
// findAll(): Promise<Employees[]> {
|
||||
// return this.prisma.employees.findMany({
|
||||
// include: { user: true },
|
||||
// });
|
||||
// }
|
||||
|
||||
// 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 with email: ${email} not found`);
|
||||
// }
|
||||
// return emp;
|
||||
// }
|
||||
|
||||
// async update(
|
||||
// email: string,
|
||||
// dto: UpdateEmployeeDto,
|
||||
// ): Promise<Employees> {
|
||||
// const emp = await this.findOne(email);
|
||||
|
||||
// const {
|
||||
// first_name,
|
||||
// last_name,
|
||||
// phone_number,
|
||||
// residence,
|
||||
// external_payroll_id,
|
||||
// company_code,
|
||||
// job_title,
|
||||
// first_work_day,
|
||||
// last_work_day,
|
||||
// is_supervisor,
|
||||
// email: new_email,
|
||||
// } = dto;
|
||||
|
||||
// return this.prisma.$transaction(async (transaction) => {
|
||||
// if(
|
||||
// first_name !== undefined ||
|
||||
// last_name !== undefined ||
|
||||
// new_email !== undefined ||
|
||||
// phone_number !== undefined ||
|
||||
// residence !== undefined
|
||||
// ){
|
||||
// await transaction.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 }),
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
// const updated = await transaction.employees.update({
|
||||
// where: { id: emp.id },
|
||||
// data: {
|
||||
// ...(external_payroll_id !== undefined && { external_payroll_id }),
|
||||
// ...(company_code !== undefined && { company_code }),
|
||||
// ...(first_work_day !== undefined && { first_work_day }),
|
||||
// ...(last_work_day !== undefined && { last_work_day }),
|
||||
// ...(job_title !== undefined && { job_title }),
|
||||
// ...(is_supervisor !== undefined && { is_supervisor }),
|
||||
// },
|
||||
// });
|
||||
// return updated;
|
||||
// });
|
||||
// }
|
||||
|
||||
|
||||
async remove(email: string): Promise<Employees> {
|
||||
// async remove(email: string): Promise<Employees> {
|
||||
|
||||
const emp = await this.findOne(email);
|
||||
// const emp = await this.findOne(email);
|
||||
|
||||
return this.prisma.$transaction(async (transaction) => {
|
||||
await transaction.employees.updateMany({
|
||||
where: { supervisor_id: emp.id },
|
||||
data: { supervisor_id: null },
|
||||
});
|
||||
const deleted_employee = await transaction.employees.delete({
|
||||
where: {id: emp.id },
|
||||
});
|
||||
await transaction.users.delete({
|
||||
where: { id: emp.user_id },
|
||||
});
|
||||
return deleted_employee;
|
||||
});
|
||||
}
|
||||
// return this.prisma.$transaction(async (transaction) => {
|
||||
// await transaction.employees.updateMany({
|
||||
// where: { supervisor_id: emp.id },
|
||||
// data: { supervisor_id: null },
|
||||
// });
|
||||
// const deleted_employee = await transaction.employees.delete({
|
||||
// where: {id: emp.id },
|
||||
// });
|
||||
// await transaction.users.delete({
|
||||
// where: { id: emp.user_id },
|
||||
// });
|
||||
// return deleted_employee;
|
||||
// });
|
||||
// }
|
||||
|
||||
|
||||
//archivation functions ******************************************************
|
||||
|
||||
async patchEmployee(email: string, dto: UpdateEmployeeDto): Promise<Employees | EmployeesArchive | null> {
|
||||
// 1) Tenter sur employés actifs
|
||||
const active = await this.prisma.employees.findFirst({
|
||||
where: { user: { email } },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
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 && 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: new_email,
|
||||
phone_number,
|
||||
residence,
|
||||
external_payroll_id,
|
||||
company_code,
|
||||
job_title,
|
||||
first_work_day,
|
||||
last_work_day,
|
||||
supervisor_id,
|
||||
is_supervisor,
|
||||
} = dto as any;
|
||||
|
||||
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 (transaction) => {
|
||||
if(
|
||||
first_name !== undefined ||
|
||||
last_name !== undefined ||
|
||||
new_email !== undefined ||
|
||||
phone_number !== undefined ||
|
||||
residence !== undefined
|
||||
) {
|
||||
await transaction.users.update({
|
||||
where: { id: active.user_id },
|
||||
data: {
|
||||
...(first_name !== undefined ? { first_name } : {}),
|
||||
...(last_name !== undefined ? { last_name } : {}),
|
||||
...(email !== undefined ? { email: new_email }: {}),
|
||||
...(phone_number !== undefined ? { phone_number } : {}),
|
||||
...(residence !== undefined ? { residence } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const updated = await transaction.employees.update({
|
||||
where: { id: active.id },
|
||||
data: {
|
||||
...(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.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: { user_id: user.id },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
if (archived) {
|
||||
// Condition de restauration : last_work_day === null ou first_work_day fourni
|
||||
const restore = dto.last_work_day === null || dto.first_work_day != null;
|
||||
if (restore) {
|
||||
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(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 => {
|
||||
//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: active.id } });
|
||||
//return archived employee
|
||||
return archived
|
||||
});
|
||||
}
|
||||
|
||||
//transfers the employee from archive to the employees table
|
||||
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: {
|
||||
user_id: archived.user_id,
|
||||
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,
|
||||
is_supervisor: archived.is_supervisor ?? false,
|
||||
},
|
||||
});
|
||||
//deleting archived entry by id
|
||||
await transaction.employeesArchive.delete({ where: { id: archived.id } });
|
||||
|
||||
//return restored employee
|
||||
return restored;
|
||||
});
|
||||
}
|
||||
|
||||
//fetches all archived employees
|
||||
async findAllArchived(): Promise<EmployeesArchive[]> {
|
||||
return this.prisma.employeesArchive.findMany();
|
||||
}
|
||||
|
||||
//fetches an archived employee
|
||||
async findOneArchived(id: number): Promise<EmployeesArchive> {
|
||||
return this.prisma.employeesArchive.findUniqueOrThrow({ where: { id } });
|
||||
}
|
||||
|
||||
}
|
||||
9
src/modules/employees/utils/employee.utils.ts
Normal file
9
src/modules/employees/utils/employee.utils.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export function toDateOrNull(v?: string | null): Date | null {
|
||||
if (!v) return null;
|
||||
const day = new Date(v);
|
||||
return isNaN(day.getTime()) ? null : day;
|
||||
}
|
||||
export function toDateOrUndefined(v?: string | null): Date | undefined {
|
||||
const day = toDateOrNull(v ?? undefined);
|
||||
return day === null ? undefined : day;
|
||||
}
|
||||
|
|
@ -1,13 +1,8 @@
|
|||
import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Put, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
|
||||
import { ExpensesQueryService } from "../services/expenses-query.service";
|
||||
import { CreateExpenseDto } from "../dtos/create-expense.dto";
|
||||
import { Expenses } from "@prisma/client";
|
||||
import { Body, Controller, Param, Put, } from "@nestjs/common";
|
||||
import { Roles as RoleEnum } from '.prisma/client';
|
||||
import { UpdateExpenseDto } from "../dtos/update-expense.dto";
|
||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||
import { ExpensesCommandService } from "../services/expenses-command.service";
|
||||
import { SearchExpensesDto } from "../dtos/search-expense.dto";
|
||||
import { UpsertExpenseDto } from "../dtos/upsert-expense.dto";
|
||||
import { UpsertExpenseResult } from "../types and interfaces/expenses.types.interfaces";
|
||||
|
||||
|
|
@ -17,7 +12,7 @@ import { UpsertExpenseResult } from "../types and interfaces/expenses.types.inte
|
|||
@Controller('Expenses')
|
||||
export class ExpensesController {
|
||||
constructor(
|
||||
private readonly query: ExpensesQueryService,
|
||||
// private readonly query: ExpensesQueryService,
|
||||
private readonly command: ExpensesCommandService,
|
||||
) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ import { ExpensesCommandService } from "./services/expenses-command.service";
|
|||
import { BankCodesRepo } from "./repos/bank-codes.repo";
|
||||
import { TimesheetsRepo } from "./repos/timesheets.repo";
|
||||
import { EmployeesRepo } from "./repos/employee.repo";
|
||||
import { ExpensesArchivalService } from "./services/expenses-archival.service";
|
||||
|
||||
@Module({
|
||||
imports: [BusinessLogicsModule],
|
||||
controllers: [ExpensesController],
|
||||
providers: [
|
||||
ExpensesQueryService,
|
||||
ExpensesArchivalService,
|
||||
ExpensesCommandService,
|
||||
BankCodesRepo,
|
||||
TimesheetsRepo,
|
||||
|
|
@ -19,6 +21,7 @@ import { EmployeesRepo } from "./repos/employee.repo";
|
|||
],
|
||||
exports: [
|
||||
ExpensesQueryService,
|
||||
ExpensesArchivalService,
|
||||
BankCodesRepo,
|
||||
TimesheetsRepo,
|
||||
EmployeesRepo,
|
||||
|
|
|
|||
62
src/modules/expenses/services/expenses-archival.service.ts
Normal file
62
src/modules/expenses/services/expenses-archival.service.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { ExpensesArchive } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class ExpensesArchivalService {
|
||||
constructor(private readonly prisma: PrismaService){}
|
||||
|
||||
async archiveOld(): Promise<void> {
|
||||
//fetches archived timesheet's Ids
|
||||
const archived_timesheets = await this.prisma.timesheetsArchive.findMany({
|
||||
select: { timesheet_id: true },
|
||||
});
|
||||
|
||||
const timesheet_ids = archived_timesheets.map(sheet => sheet.timesheet_id);
|
||||
if(timesheet_ids.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// copy/delete transaction
|
||||
await this.prisma.$transaction(async transaction => {
|
||||
//fetches expenses to move to archive
|
||||
const expenses_to_archive = await transaction.expenses.findMany({
|
||||
where: { timesheet_id: { in: timesheet_ids } },
|
||||
});
|
||||
if(expenses_to_archive.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
//copies sent to archive table
|
||||
await transaction.expensesArchive.createMany({
|
||||
data: expenses_to_archive.map(exp => ({
|
||||
expense_id: exp.id,
|
||||
timesheet_id: exp.timesheet_id,
|
||||
bank_code_id: exp.bank_code_id,
|
||||
date: exp.date,
|
||||
amount: exp.amount,
|
||||
attachment: exp.attachment,
|
||||
comment: exp.comment,
|
||||
is_approved: exp.is_approved,
|
||||
supervisor_comment: exp.supervisor_comment,
|
||||
})),
|
||||
});
|
||||
|
||||
//delete from expenses table
|
||||
await transaction.expenses.deleteMany({
|
||||
where: { id: { in: expenses_to_archive.map(exp => exp.id) } },
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
//fetches all archived timesheets
|
||||
async findAllArchived(): Promise<ExpensesArchive[]> {
|
||||
return this.prisma.expensesArchive.findMany();
|
||||
}
|
||||
|
||||
//fetches an archived timesheet
|
||||
async findOneArchived(id: number): Promise<ExpensesArchive> {
|
||||
return this.prisma.expensesArchive.findUniqueOrThrow({ where: { id } });
|
||||
}
|
||||
}
|
||||
|
|
@ -14,9 +14,11 @@ import {
|
|||
import { ExpenseResponse, UpsertAction } from "../types and interfaces/expenses.types.interfaces";
|
||||
import {
|
||||
assertAndTrimComment,
|
||||
computeAmountDecimal,
|
||||
computeMileageAmount,
|
||||
mapDbExpenseToDayResponse,
|
||||
normalizeType as normalizeTypeUtil
|
||||
normalizeType,
|
||||
parseAttachmentId
|
||||
} from "../utils/expenses.utils";
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -28,6 +30,10 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
|||
private readonly employeesRepo: EmployeesRepo,
|
||||
) { super(prisma); }
|
||||
|
||||
//_____________________________________________________________________________________________
|
||||
// APPROVAL TX-DELEGATE METHODS
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
protected get delegate() {
|
||||
return this.prisma.expenses;
|
||||
}
|
||||
|
|
@ -42,7 +48,9 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
|||
);
|
||||
}
|
||||
|
||||
//-------------------- Master CRUD function --------------------
|
||||
//_____________________________________________________________________________________________
|
||||
// MASTER CRUD FUNCTION
|
||||
//_____________________________________________________________________________________________
|
||||
readonly upsertExpensesByDate = async (email: string, date: string, dto: UpsertExpenseDto,
|
||||
): Promise<{ action:UpsertAction; day: ExpenseResponse[] }> => {
|
||||
|
||||
|
|
@ -82,7 +90,7 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
|||
});
|
||||
|
||||
return rows.map((r) =>
|
||||
this.mapDbToDayResponse({
|
||||
mapDbExpenseToDayResponse({
|
||||
date: r.date,
|
||||
amount: r.amount ?? 0,
|
||||
mileage: r.mileage ?? 0,
|
||||
|
|
@ -106,12 +114,12 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
|||
comment: string;
|
||||
attachment: number | null;
|
||||
}> => {
|
||||
const type = this.normalizeType(payload.type);
|
||||
const comment = this.assertAndTrimComment(payload.comment);
|
||||
const attachment = this.parseAttachmentId(payload.attachment);
|
||||
const type = normalizeType(payload.type);
|
||||
const comment = assertAndTrimComment(payload.comment);
|
||||
const attachment = parseAttachmentId(payload.attachment);
|
||||
|
||||
const { id: bank_code_id, modifier } = await this.lookupBankCodeOrThrow(tx, type);
|
||||
let amount = this.computeAmountDecimal(type, payload, modifier);
|
||||
const { id: bank_code_id, modifier } = await this.resolveBankCodeIdByType(tx, type);
|
||||
let amount = computeAmountDecimal(type, payload, modifier);
|
||||
let mileage: number | null = null;
|
||||
|
||||
if (type === 'MILEAGE') {
|
||||
|
|
@ -172,7 +180,9 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
|||
};
|
||||
|
||||
let action : UpsertAction;
|
||||
//-------------------- DELETE --------------------
|
||||
//_____________________________________________________________________________________________
|
||||
// DELETE
|
||||
//_____________________________________________________________________________________________
|
||||
if(old_expense && !new_expense) {
|
||||
const oldNorm = await normalizePayload(old_expense);
|
||||
const existing = await findExactOld(oldNorm);
|
||||
|
|
@ -185,7 +195,9 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
|||
await tx.expenses.delete({where: { id: existing.id } });
|
||||
action = 'delete';
|
||||
}
|
||||
//-------------------- CREATE --------------------
|
||||
//_____________________________________________________________________________________________
|
||||
// CREATE
|
||||
//_____________________________________________________________________________________________
|
||||
else if (!old_expense && new_expense) {
|
||||
const new_exp = await normalizePayload(new_expense);
|
||||
await tx.expenses.create({
|
||||
|
|
@ -202,7 +214,9 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
|||
});
|
||||
action = 'create';
|
||||
}
|
||||
//-------------------- UPDATE --------------------
|
||||
//_____________________________________________________________________________________________
|
||||
// UPDATE
|
||||
//_____________________________________________________________________________________________
|
||||
else if(old_expense && new_expense) {
|
||||
const oldNorm = await normalizePayload(old_expense);
|
||||
const existing = await findExactOld(oldNorm);
|
||||
|
|
@ -236,40 +250,9 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
//-------------------- helpers --------------------
|
||||
private readonly normalizeType = (type: string): string =>
|
||||
normalizeTypeUtil(type);
|
||||
|
||||
private readonly assertAndTrimComment = (comment: string): string =>
|
||||
assertAndTrimComment(comment);
|
||||
|
||||
private readonly parseAttachmentId = (value: unknown): number | null => {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
if (!Number.isInteger(value) || value <= 0) {
|
||||
throw new BadRequestException('Invalid attachment id');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed.length) return null;
|
||||
if (!/^\d+$/.test(trimmed)) throw new BadRequestException('Invalid attachment id');
|
||||
|
||||
const parsed = Number(trimmed);
|
||||
if (parsed <= 0) throw new BadRequestException('Invalid attachment id');
|
||||
|
||||
return parsed;
|
||||
}
|
||||
throw new BadRequestException('Invalid attachment id');
|
||||
};
|
||||
|
||||
//_____________________________________________________________________________________________
|
||||
// HELPERS
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
private readonly resolveEmployeeIdByEmail = async (email: string): Promise<number> =>
|
||||
this.employeesRepo.findIdByEmail(email);
|
||||
|
|
@ -280,34 +263,8 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
|||
return id;
|
||||
};
|
||||
|
||||
private readonly lookupBankCodeOrThrow = async ( transaction: Prisma.TransactionClient, type: string
|
||||
private readonly resolveBankCodeIdByType = async ( transaction: Prisma.TransactionClient, type: string
|
||||
): Promise<{id: number; modifier: number}> =>
|
||||
this.bankCodesRepo.findByType(type, transaction);
|
||||
|
||||
private readonly computeAmountDecimal = (
|
||||
type: string,
|
||||
payload: {
|
||||
amount?: number;
|
||||
mileage?: number;
|
||||
},
|
||||
modifier: number,
|
||||
): Prisma.Decimal => {
|
||||
if(type === 'MILEAGE') {
|
||||
const km = payload.mileage ?? 0;
|
||||
const amountNumber = computeMileageAmount(km, modifier);
|
||||
return new Prisma.Decimal(amountNumber);
|
||||
}
|
||||
return new Prisma.Decimal(payload.amount!);
|
||||
};
|
||||
|
||||
private readonly mapDbToDayResponse = (row: {
|
||||
date: Date;
|
||||
amount: Prisma.Decimal | number | string;
|
||||
mileage: Prisma.Decimal | number | string;
|
||||
comment: string;
|
||||
is_approved: boolean;
|
||||
bank_code: { type: string } | null;
|
||||
}): ExpenseResponse => mapDbExpenseToDayResponse(row);
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,148 +1,93 @@
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { CreateExpenseDto } from "../dtos/create-expense.dto";
|
||||
import { Expenses, ExpensesArchive } from "@prisma/client";
|
||||
import { UpdateExpenseDto } from "../dtos/update-expense.dto";
|
||||
import { MileageService } from "src/modules/business-logics/services/mileage.service";
|
||||
import { SearchExpensesDto } from "../dtos/search-expense.dto";
|
||||
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class ExpensesQueryService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly mileageService: MileageService,
|
||||
) {}
|
||||
// constructor(
|
||||
// private readonly prisma: PrismaService,
|
||||
// private readonly mileageService: MileageService,
|
||||
// ) {}
|
||||
|
||||
async create(dto: CreateExpenseDto): Promise<Expenses> {
|
||||
const { timesheet_id, bank_code_id, date, amount:rawAmount,
|
||||
comment, is_approved,supervisor_comment} = dto;
|
||||
//_____________________________________________________________________________________________
|
||||
// Deprecated or unused methods
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
// async create(dto: CreateExpenseDto): Promise<Expenses> {
|
||||
// const { timesheet_id, bank_code_id, date, amount:rawAmount,
|
||||
// comment, is_approved,supervisor_comment} = dto;
|
||||
// //fetches type and modifier
|
||||
// const bank_code = await this.prisma.bankCodes.findUnique({
|
||||
// where: { id: bank_code_id },
|
||||
// select: { type: true, modifier: true },
|
||||
// });
|
||||
// if(!bank_code) throw new NotFoundException(`bank_code #${bank_code_id} not found`);
|
||||
|
||||
// //if mileage -> service, otherwise the ratio is amount:1
|
||||
// let final_amount: number;
|
||||
// if(bank_code.type === 'mileage') {
|
||||
// final_amount = await this.mileageService.calculateReimbursement(rawAmount, bank_code_id);
|
||||
// }else {
|
||||
// final_amount = parseFloat( (rawAmount * bank_code.modifier).toFixed(2));
|
||||
// }
|
||||
|
||||
// return this.prisma.expenses.create({
|
||||
// data: {
|
||||
// timesheet_id,
|
||||
// bank_code_id,
|
||||
// date,
|
||||
// amount: final_amount,
|
||||
// comment,
|
||||
// is_approved,
|
||||
// supervisor_comment
|
||||
// },
|
||||
// include: { timesheet: { include: { employee: { include: { user: true }}}},
|
||||
// bank_code: true,
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
|
||||
// async findAll(filters: SearchExpensesDto): Promise<Expenses[]> {
|
||||
// const where = buildPrismaWhere(filters);
|
||||
// const expenses = await this.prisma.expenses.findMany({ where })
|
||||
// return expenses;
|
||||
// }
|
||||
|
||||
// async findOne(id: number): Promise<Expenses> {
|
||||
// const expense = await this.prisma.expenses.findUnique({
|
||||
// where: { id },
|
||||
// include: { timesheet: { include: { employee: { include: { user:true } } } },
|
||||
// bank_code: true,
|
||||
// },
|
||||
// });
|
||||
// if (!expense) {
|
||||
// throw new NotFoundException(`Expense #${id} not found`);
|
||||
// }
|
||||
// return expense;
|
||||
// }
|
||||
|
||||
// async update(id: number, dto: UpdateExpenseDto): Promise<Expenses> {
|
||||
// await this.findOne(id);
|
||||
// const { timesheet_id, bank_code_id, date, amount,
|
||||
// comment, is_approved, supervisor_comment} = dto;
|
||||
// return this.prisma.expenses.update({
|
||||
// where: { id },
|
||||
// data: {
|
||||
// ...(timesheet_id !== undefined && { timesheet_id}),
|
||||
// ...(bank_code_id !== undefined && { bank_code_id }),
|
||||
// ...(date !== undefined && { date }),
|
||||
// ...(amount !== undefined && { amount }),
|
||||
// ...(comment !== undefined && { comment }),
|
||||
// ...(is_approved !== undefined && { is_approved }),
|
||||
// ...(supervisor_comment !== undefined && { supervisor_comment }),
|
||||
// },
|
||||
// include: { timesheet: { include: { employee: { include: { user: true } } } },
|
||||
// bank_code: true,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
// async remove(id: number): Promise<Expenses> {
|
||||
// await this.findOne(id);
|
||||
// return this.prisma.expenses.delete({ where: { id } });
|
||||
// }
|
||||
|
||||
//fetches type and modifier
|
||||
const bank_code = await this.prisma.bankCodes.findUnique({
|
||||
where: { id: bank_code_id },
|
||||
select: { type: true, modifier: true },
|
||||
});
|
||||
if(!bank_code) {
|
||||
throw new NotFoundException(`bank_code #${bank_code_id} not found`)
|
||||
}
|
||||
|
||||
//if mileage -> service, otherwise the ratio is amount:1
|
||||
let final_amount: number;
|
||||
if(bank_code.type === 'mileage') {
|
||||
final_amount = await this.mileageService.calculateReimbursement(rawAmount, bank_code_id);
|
||||
}else {
|
||||
final_amount = parseFloat( (rawAmount * bank_code.modifier).toFixed(2));
|
||||
}
|
||||
|
||||
return this.prisma.expenses.create({
|
||||
data: { timesheet_id, bank_code_id, date, amount: final_amount, comment, is_approved, supervisor_comment},
|
||||
include: { timesheet: { include: { employee: { include: { user: true }}}},
|
||||
bank_code: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async findAll(filters: SearchExpensesDto): Promise<Expenses[]> {
|
||||
const where = buildPrismaWhere(filters);
|
||||
const expenses = await this.prisma.expenses.findMany({ where })
|
||||
return expenses;
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<Expenses> {
|
||||
const expense = await this.prisma.expenses.findUnique({
|
||||
where: { id },
|
||||
include: { timesheet: { include: { employee: { include: { user:true } } } },
|
||||
bank_code: true,
|
||||
},
|
||||
});
|
||||
if (!expense) {
|
||||
throw new NotFoundException(`Expense #${id} not found`);
|
||||
}
|
||||
return expense;
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateExpenseDto): Promise<Expenses> {
|
||||
await this.findOne(id);
|
||||
const { timesheet_id, bank_code_id, date, amount,
|
||||
comment, is_approved, supervisor_comment} = dto;
|
||||
return this.prisma.expenses.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(timesheet_id !== undefined && { timesheet_id}),
|
||||
...(bank_code_id !== undefined && { bank_code_id }),
|
||||
...(date !== undefined && { date }),
|
||||
...(amount !== undefined && { amount }),
|
||||
...(comment !== undefined && { comment }),
|
||||
...(is_approved !== undefined && { is_approved }),
|
||||
...(supervisor_comment !== undefined && { supervisor_comment }),
|
||||
},
|
||||
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
||||
bank_code: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async remove(id: number): Promise<Expenses> {
|
||||
await this.findOne(id);
|
||||
return this.prisma.expenses.delete({ where: { id } });
|
||||
}
|
||||
|
||||
|
||||
//archivation functions ******************************************************
|
||||
|
||||
async archiveOld(): Promise<void> {
|
||||
//fetches archived timesheet's Ids
|
||||
const archived_timesheets = await this.prisma.timesheetsArchive.findMany({
|
||||
select: { timesheet_id: true },
|
||||
});
|
||||
|
||||
const timesheet_ids = archived_timesheets.map(sheet => sheet.timesheet_id);
|
||||
if(timesheet_ids.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// copy/delete transaction
|
||||
await this.prisma.$transaction(async transaction => {
|
||||
//fetches expenses to move to archive
|
||||
const expenses_to_archive = await transaction.expenses.findMany({
|
||||
where: { timesheet_id: { in: timesheet_ids } },
|
||||
});
|
||||
if(expenses_to_archive.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
//copies sent to archive table
|
||||
await transaction.expensesArchive.createMany({
|
||||
data: expenses_to_archive.map(exp => ({
|
||||
expense_id: exp.id,
|
||||
timesheet_id: exp.timesheet_id,
|
||||
bank_code_id: exp.bank_code_id,
|
||||
date: exp.date,
|
||||
amount: exp.amount,
|
||||
attachment: exp.attachment,
|
||||
comment: exp.comment,
|
||||
is_approved: exp.is_approved,
|
||||
supervisor_comment: exp.supervisor_comment,
|
||||
})),
|
||||
});
|
||||
|
||||
//delete from expenses table
|
||||
await transaction.expenses.deleteMany({
|
||||
where: { id: { in: expenses_to_archive.map(exp => exp.id) } },
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
//fetches all archived timesheets
|
||||
async findAllArchived(): Promise<ExpensesArchive[]> {
|
||||
return this.prisma.expensesArchive.findMany();
|
||||
}
|
||||
|
||||
//fetches an archived timesheet
|
||||
async findOneArchived(id: number): Promise<ExpensesArchive> {
|
||||
return this.prisma.expensesArchive.findUniqueOrThrow({ where: { id } });
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,32 @@ export function toNumberSafe(value: DecimalLike): number {
|
|||
);
|
||||
}
|
||||
|
||||
export const parseAttachmentId = (value: unknown): number | null => {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
if (!Number.isInteger(value) || value <= 0) {
|
||||
throw new BadRequestException('Invalid attachment id');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed.length) return null;
|
||||
if (!/^\d+$/.test(trimmed)) throw new BadRequestException('Invalid attachment id');
|
||||
|
||||
const parsed = Number(trimmed);
|
||||
if (parsed <= 0) throw new BadRequestException('Invalid attachment id');
|
||||
|
||||
return parsed;
|
||||
}
|
||||
throw new BadRequestException('Invalid attachment id');
|
||||
};
|
||||
|
||||
|
||||
//map of a row for DayExpenseResponse
|
||||
export function mapDbExpenseToDayResponse(row: {
|
||||
|
|
@ -67,3 +93,19 @@ export function mapDbExpenseToDayResponse(row: {
|
|||
...(row.mileage !== null ? { mileage: toNum(row.mileage) }: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export const computeAmountDecimal = (
|
||||
type: string,
|
||||
payload: {
|
||||
amount?: number;
|
||||
mileage?: number;
|
||||
},
|
||||
modifier: number,
|
||||
): Prisma.Decimal => {
|
||||
if(type === 'MILEAGE') {
|
||||
const km = payload.mileage ?? 0;
|
||||
const amountNumber = computeMileageAmount(km, modifier);
|
||||
return new Prisma.Decimal(amountNumber);
|
||||
}
|
||||
return new Prisma.Decimal(payload.amount!);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
import { Body, Controller, Delete, Get, Header, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Put, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
|
||||
import { Shifts } from "@prisma/client";
|
||||
import { CreateShiftDto } from "../dtos/create-shift.dto";
|
||||
import { UpdateShiftsDto } from "../dtos/update-shift.dto";
|
||||
import { Body, Controller, Get, Header, Param, ParseBoolPipe, ParseIntPipe, Patch, Put, Query, } from "@nestjs/common";
|
||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||
import { Roles as RoleEnum } from '.prisma/client';
|
||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||
import { ShiftsCommandService } from "../services/shifts-command.service";
|
||||
import { SearchShiftsDto } from "../dtos/search-shift.dto";
|
||||
import { OverviewRow, ShiftsQueryService } from "../services/shifts-query.service";
|
||||
import { ShiftsQueryService } from "../services/shifts-query.service";
|
||||
import { GetShiftsOverviewDto } from "../dtos/get-shift-overview.dto";
|
||||
import { UpsertShiftDto } from "../dtos/upsert-shift.dto";
|
||||
import { OverviewRow } from "../types and interfaces/shifts-overview-row.interface";
|
||||
|
||||
@ApiTags('Shifts')
|
||||
@ApiBearerAuth('access-token')
|
||||
|
|
|
|||
|
|
@ -16,3 +16,10 @@ export function toDateOnlyUTC(input: string | Date): Date {
|
|||
const date = new Date(input);
|
||||
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
||||
}
|
||||
|
||||
export function formatHHmm(time: Date): string {
|
||||
const hh = String(time.getUTCHours()).padStart(2,'0');
|
||||
const mm = String(time.getUTCMinutes()).padStart(2,'0');
|
||||
return `${hh}:${mm}`;
|
||||
}
|
||||
|
||||
|
|
|
|||
59
src/modules/shifts/services/shifts-archival.service.ts
Normal file
59
src/modules/shifts/services/shifts-archival.service.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { ShiftsArchive } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
export class ShiftsArchivalService {
|
||||
constructor(private readonly prisma: PrismaService){}
|
||||
|
||||
async archiveOld(): Promise<void> {
|
||||
//fetches archived timesheet's Ids
|
||||
const archived_timesheets = await this.prisma.timesheetsArchive.findMany({
|
||||
select: { timesheet_id: true },
|
||||
});
|
||||
|
||||
const timesheet_ids = archived_timesheets.map(sheet => sheet.timesheet_id);
|
||||
if(timesheet_ids.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// copy/delete transaction
|
||||
await this.prisma.$transaction(async transaction => {
|
||||
//fetches shifts to move to archive
|
||||
const shifts_to_archive = await transaction.shifts.findMany({
|
||||
where: { timesheet_id: { in: timesheet_ids } },
|
||||
});
|
||||
if(shifts_to_archive.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
//copies sent to archive table
|
||||
await transaction.shiftsArchive.createMany({
|
||||
data: shifts_to_archive.map(shift => ({
|
||||
shift_id: shift.id,
|
||||
timesheet_id: shift.timesheet_id,
|
||||
bank_code_id: shift.bank_code_id,
|
||||
comment: shift.comment ?? undefined,
|
||||
date: shift.date,
|
||||
start_time: shift.start_time,
|
||||
end_time: shift.end_time,
|
||||
})),
|
||||
});
|
||||
|
||||
//delete from shifts table
|
||||
await transaction.shifts.deleteMany({
|
||||
where: { id: { in: shifts_to_archive.map(shift => shift.id) } },
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
//fetches all archived timesheets
|
||||
async findAllArchived(): Promise<ShiftsArchive[]> {
|
||||
return this.prisma.shiftsArchive.findMany();
|
||||
}
|
||||
|
||||
//fetches an archived timesheet
|
||||
async findOneArchived(id: number): Promise<ShiftsArchive> {
|
||||
return this.prisma.shiftsArchive.findUniqueOrThrow({ where: { id } });
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,25 +1,36 @@
|
|||
import { BadRequestException, ConflictException, Injectable, NotFoundException, UnprocessableEntityException } from "@nestjs/common";
|
||||
import { formatHHmm, toDateOnlyUTC, weekStartMondayUTC } from "../helpers/shifts-date-time-helpers";
|
||||
import { normalizeShiftPayload, overlaps, resolveBankCodeByType } from "../utils/shifts.utils";
|
||||
import { DayShiftResponse, UpsertAction } from "../types and interfaces/shifts-upsert.types";
|
||||
import { Prisma, Shifts } from "@prisma/client";
|
||||
import { UpsertShiftDto } from "../dtos/upsert-shift.dto";
|
||||
import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { ShiftPayloadDto, UpsertShiftDto } from "../dtos/upsert-shift.dto";
|
||||
import { timeFromHHMMUTC, toDateOnlyUTC, weekStartMondayUTC } from "../helpers/shifts-date-time-helpers";
|
||||
|
||||
type DayShiftResponse = {
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
type: string;
|
||||
is_remote: boolean;
|
||||
comment: string | null;
|
||||
}
|
||||
|
||||
type UpsertAction = 'created' | 'updated' | 'deleted';
|
||||
|
||||
@Injectable()
|
||||
export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
||||
constructor(prisma: PrismaService) { super(prisma); }
|
||||
|
||||
//create/update/delete master method
|
||||
//_____________________________________________________________________________________________
|
||||
// APPROVAL AND DELEGATE METHODS
|
||||
//_____________________________________________________________________________________________
|
||||
protected get delegate() {
|
||||
return this.prisma.shifts;
|
||||
}
|
||||
|
||||
protected delegateFor(transaction: Prisma.TransactionClient) {
|
||||
return transaction.shifts;
|
||||
}
|
||||
|
||||
async updateApproval(id: number, is_approved: boolean): Promise<Shifts> {
|
||||
return this.prisma.$transaction((transaction) =>
|
||||
this.updateApprovalWithTransaction(transaction, id, is_approved),
|
||||
);
|
||||
}
|
||||
|
||||
//_____________________________________________________________________________________________
|
||||
// MASTER CRUD METHOD
|
||||
//_____________________________________________________________________________________________
|
||||
async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto):
|
||||
Promise<{ action: UpsertAction; day: DayShiftResponse[] }> {
|
||||
const { old_shift, new_shift } = dto;
|
||||
|
|
@ -62,10 +73,10 @@ async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto)
|
|||
|
||||
//normalization of data to ensure a valid comparison between DB and payload
|
||||
const old_norm = dto.old_shift
|
||||
? this.normalize_shift_payload(dto.old_shift)
|
||||
? normalizeShiftPayload(dto.old_shift)
|
||||
: undefined;
|
||||
const new_norm = dto.new_shift
|
||||
? this.normalize_shift_payload(dto.new_shift)
|
||||
? normalizeShiftPayload(dto.new_shift)
|
||||
: undefined;
|
||||
|
||||
if (old_norm && old_norm.end_time.getTime() <= old_norm.start_time.getTime()) {
|
||||
|
|
@ -77,10 +88,10 @@ async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto)
|
|||
|
||||
//Resolve bank_code_id with type
|
||||
const old_bank_code_id = old_norm
|
||||
? await this.lookup_bank_code_id_or_throw(old_norm.type)
|
||||
? await resolveBankCodeByType(old_norm.type)
|
||||
: undefined;
|
||||
const new_bank_code_id = new_norm
|
||||
? await this.lookup_bank_code_id_or_throw(new_norm.type)
|
||||
? await resolveBankCodeByType(new_norm.type)
|
||||
: undefined;
|
||||
|
||||
//fetch all shifts in a single day
|
||||
|
|
@ -100,7 +111,7 @@ async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto)
|
|||
const result = await this.prisma.$transaction(async (transaction)=> {
|
||||
let action: UpsertAction;
|
||||
|
||||
const find_exact_old_shift = async ()=> {
|
||||
const findExactOldShift = async ()=> {
|
||||
if(!old_norm || old_bank_code_id === undefined) return undefined;
|
||||
const old_comment = old_norm.comment ?? null;
|
||||
|
||||
|
|
@ -119,12 +130,11 @@ async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto)
|
|||
};
|
||||
|
||||
//checks for overlaping shifts
|
||||
const assert_no_overlap = (exclude_shift_id?: number)=> {
|
||||
const assertNoOverlap = (exclude_shift_id?: number)=> {
|
||||
if (!new_norm) return;
|
||||
|
||||
const overlap_with = day_shifts.filter((shift)=> {
|
||||
if(exclude_shift_id && shift.id === exclude_shift_id) return false;
|
||||
return this.overlaps(
|
||||
return overlaps(
|
||||
new_norm.start_time.getTime(),
|
||||
new_norm.end_time.getTime(),
|
||||
shift.start_time.getTime(),
|
||||
|
|
@ -134,8 +144,8 @@ async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto)
|
|||
|
||||
if(overlap_with.length > 0) {
|
||||
const conflicts = overlap_with.map((shift)=> ({
|
||||
start_time: this.format_hhmm(shift.start_time),
|
||||
end_time: this.format_hhmm(shift.end_time),
|
||||
start_time: formatHHmm(shift.start_time),
|
||||
end_time: formatHHmm(shift.end_time),
|
||||
type: shift.bank_code?.type ?? 'UNKNOWN',
|
||||
}));
|
||||
throw new ConflictException({
|
||||
|
|
@ -146,9 +156,11 @@ async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto)
|
|||
}
|
||||
};
|
||||
|
||||
//_____________________________________________________________________________________________
|
||||
// DELETE
|
||||
//_____________________________________________________________________________________________
|
||||
if ( old_shift && !new_shift ) {
|
||||
const existing = await find_exact_old_shift();
|
||||
const existing = await findExactOldShift();
|
||||
if(!existing) {
|
||||
throw new NotFoundException({
|
||||
error_code: 'SHIFT_STALE',
|
||||
|
|
@ -158,9 +170,11 @@ async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto)
|
|||
await transaction.shifts.delete({ where: { id: existing.id } } );
|
||||
action = 'deleted';
|
||||
}
|
||||
//_____________________________________________________________________________________________
|
||||
// CREATE
|
||||
//_____________________________________________________________________________________________
|
||||
else if (!old_shift && new_shift) {
|
||||
assert_no_overlap();
|
||||
assertNoOverlap();
|
||||
await transaction.shifts.create({
|
||||
data: {
|
||||
timesheet_id: timesheet.id,
|
||||
|
|
@ -174,16 +188,18 @@ async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto)
|
|||
});
|
||||
action = 'created';
|
||||
}
|
||||
//_____________________________________________________________________________________________
|
||||
// UPDATE
|
||||
//_____________________________________________________________________________________________
|
||||
else if (old_shift && new_shift){
|
||||
const existing = await find_exact_old_shift();
|
||||
const existing = await findExactOldShift();
|
||||
if(!existing) {
|
||||
throw new NotFoundException({
|
||||
error_code: 'SHIFT_STALE',
|
||||
message: 'The shift was modified or deleted by someone else',
|
||||
});
|
||||
}
|
||||
assert_no_overlap(existing.id);
|
||||
assertNoOverlap(existing.id);
|
||||
await transaction.shifts.update({
|
||||
where: {
|
||||
id: existing.id
|
||||
|
|
@ -204,8 +220,8 @@ async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto)
|
|||
//Reload the day (truth source)
|
||||
const fresh_day = await transaction.shifts.findMany({
|
||||
where: {
|
||||
timesheet_id: timesheet.id,
|
||||
date: date_only,
|
||||
timesheet_id: timesheet.id,
|
||||
},
|
||||
include: {
|
||||
bank_code: true
|
||||
|
|
@ -218,8 +234,8 @@ async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto)
|
|||
return {
|
||||
action,
|
||||
day: fresh_day.map<DayShiftResponse>((shift)=> ({
|
||||
start_time: this.format_hhmm(shift.start_time),
|
||||
end_time: this.format_hhmm(shift.end_time),
|
||||
start_time: formatHHmm(shift.start_time),
|
||||
end_time: formatHHmm(shift.end_time),
|
||||
type: shift.bank_code?.type ?? 'UNKNOWN',
|
||||
is_remote: shift.is_remote,
|
||||
comment: shift.comment ?? null,
|
||||
|
|
@ -228,60 +244,4 @@ async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto)
|
|||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private normalize_shift_payload(payload: ShiftPayloadDto) {
|
||||
//normalize shift's infos
|
||||
const start_time = timeFromHHMMUTC(payload.start_time);
|
||||
const end_time = timeFromHHMMUTC(payload.end_time );
|
||||
const type = (payload.type || '').trim().toUpperCase();
|
||||
const is_remote = payload.is_remote === true;
|
||||
//normalize comment
|
||||
const raw_comment = payload.comment ?? null;
|
||||
const trimmed = typeof raw_comment === 'string' ? raw_comment.trim() : null;
|
||||
const comment = trimmed && trimmed.length > 0 ? trimmed: null;
|
||||
|
||||
return { start_time, end_time, type, is_remote, comment };
|
||||
}
|
||||
|
||||
private async lookup_bank_code_id_or_throw(type: string): Promise<number> {
|
||||
const bank = await this.prisma.bankCodes.findFirst({
|
||||
where: { type },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!bank) {
|
||||
throw new NotFoundException({ error_code: 'SHIFT_TYPE_UNKNOWN', message: `unknown shift type: ${type}` });
|
||||
}
|
||||
return bank.id;
|
||||
}
|
||||
|
||||
private overlaps(
|
||||
a_start_ms: number,
|
||||
a_end_ms: number,
|
||||
b_start_ms: number,
|
||||
b_end_ms: number,
|
||||
): boolean {
|
||||
return a_start_ms < b_end_ms && b_start_ms < a_end_ms;
|
||||
}
|
||||
|
||||
private format_hhmm(time: Date): string {
|
||||
const hh = String(time.getUTCHours()).padStart(2,'0');
|
||||
const mm = String(time.getUTCMinutes()).padStart(2,'0');
|
||||
return `${hh}:${mm}`;
|
||||
}
|
||||
|
||||
//approval methods
|
||||
|
||||
protected get delegate() {
|
||||
return this.prisma.shifts;
|
||||
}
|
||||
|
||||
protected delegateFor(transaction: Prisma.TransactionClient) {
|
||||
return transaction.shifts;
|
||||
}
|
||||
|
||||
async updateApproval(id: number, is_approved: boolean): Promise<Shifts> {
|
||||
return this.prisma.$transaction((transaction) =>
|
||||
this.updateApprovalWithTransaction(transaction, id, is_approved),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,10 @@
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { CreateShiftDto } from "../dtos/create-shift.dto";
|
||||
import { Shifts, ShiftsArchive } from "@prisma/client";
|
||||
import { UpdateShiftsDto } from "../dtos/update-shift.dto";
|
||||
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
|
||||
import { SearchShiftsDto } from "../dtos/search-shift.dto";
|
||||
import { NotificationsService } from "src/modules/notifications/services/notifications.service";
|
||||
import { computeHours, hoursBetweenSameDay } from "src/common/utils/date-utils";
|
||||
import { computeHours } from "src/common/utils/date-utils";
|
||||
import { OverviewRow } from "../types and interfaces/shifts-overview-row.interface";
|
||||
|
||||
const DAILY_LIMIT_HOURS = Number(process.env.DAILY_LIMIT_HOURS ?? 12);
|
||||
|
||||
export interface OverviewRow {
|
||||
full_name: string;
|
||||
supervisor: string;
|
||||
total_regular_hrs: number;
|
||||
total_evening_hrs: number;
|
||||
total_overtime_hrs: number;
|
||||
total_expenses: number;
|
||||
total_mileage: number;
|
||||
is_approved: boolean;
|
||||
}
|
||||
// const DAILY_LIMIT_HOURS = Number(process.env.DAILY_LIMIT_HOURS ?? 12);
|
||||
|
||||
@Injectable()
|
||||
export class ShiftsQueryService {
|
||||
|
|
@ -28,93 +13,6 @@ export class ShiftsQueryService {
|
|||
private readonly notifs: NotificationsService,
|
||||
) {}
|
||||
|
||||
async create(dto: CreateShiftDto): Promise<Shifts> {
|
||||
const { timesheet_id, bank_code_id, date, start_time, end_time, comment } = dto;
|
||||
|
||||
//shift creation
|
||||
const shift = await this.prisma.shifts.create({
|
||||
data: { timesheet_id, bank_code_id, date, start_time, end_time, comment },
|
||||
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
||||
bank_code: true,
|
||||
},
|
||||
});
|
||||
|
||||
//fetches all shifts of the same day to check for daily overtime
|
||||
const same_day_shifts = await this.prisma.shifts.findMany({
|
||||
where: { timesheet_id, date },
|
||||
select: { id: true, date: true, start_time: true, end_time: true },
|
||||
});
|
||||
|
||||
//sums hours of the day
|
||||
const total_hours = same_day_shifts.reduce((sum, s) => {
|
||||
return sum + hoursBetweenSameDay(s.date, s.start_time, s.end_time);
|
||||
}, 0 );
|
||||
|
||||
//Notify if total hours > 8 for a single day
|
||||
if(total_hours > DAILY_LIMIT_HOURS ) {
|
||||
const user_id = String(shift.timesheet.employee.user.id);
|
||||
const date_label = new Date(date).toLocaleDateString('fr-CA');
|
||||
this.notifs.notify(user_id, {
|
||||
type: 'shift.overtime.daily',
|
||||
severity: 'warn',
|
||||
message: `Tu viens de dépasser ${DAILY_LIMIT_HOURS.toFixed(2)}h pour la journée du ${date_label}
|
||||
(total: ${total_hours.toFixed(2)}h).`,
|
||||
ts: new Date().toISOString(),
|
||||
meta: {
|
||||
timesheet_id,
|
||||
date: new Date(date).toISOString(),
|
||||
total_hours,
|
||||
threshold: DAILY_LIMIT_HOURS,
|
||||
last_shift_id: shift.id
|
||||
},
|
||||
});
|
||||
}
|
||||
return shift;
|
||||
}
|
||||
|
||||
async findAll(filters: SearchShiftsDto): Promise <Shifts[]> {
|
||||
const where = buildPrismaWhere(filters);
|
||||
const shifts = await this.prisma.shifts.findMany({ where })
|
||||
return shifts;
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<Shifts> {
|
||||
const shift = await this.prisma.shifts.findUnique({
|
||||
where: { id },
|
||||
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
||||
bank_code: true,
|
||||
},
|
||||
});
|
||||
if(!shift) {
|
||||
throw new NotFoundException(`Shift #${id} not found`);
|
||||
}
|
||||
return shift;
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateShiftsDto): Promise<Shifts> {
|
||||
await this.findOne(id);
|
||||
const { timesheet_id, bank_code_id, date,start_time,end_time, comment} = dto;
|
||||
return this.prisma.shifts.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(timesheet_id !== undefined && { timesheet_id }),
|
||||
...(bank_code_id !== undefined && { bank_code_id }),
|
||||
...(date !== undefined && { date }),
|
||||
...(start_time !== undefined && { start_time }),
|
||||
...(end_time !== undefined && { end_time }),
|
||||
...(comment !== undefined && { comment }),
|
||||
},
|
||||
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
||||
bank_code: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async remove(id: number): Promise<Shifts> {
|
||||
await this.findOne(id);
|
||||
return this.prisma.shifts.delete({ where: { id } });
|
||||
}
|
||||
|
||||
async getSummary(period_id: number): Promise<OverviewRow[]> {
|
||||
//fetch pay-period to display
|
||||
const period = await this.prisma.payPeriods.findFirst({
|
||||
|
|
@ -214,58 +112,94 @@ export class ShiftsQueryService {
|
|||
return Array.from(mapRow.values()).sort((a,b) => a.full_name.localeCompare(b.full_name));
|
||||
}
|
||||
|
||||
//archivation functions ******************************************************
|
||||
//_____________________________________________________________________________________________
|
||||
// Deprecated or unused methods
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
async archiveOld(): Promise<void> {
|
||||
//fetches archived timesheet's Ids
|
||||
const archived_timesheets = await this.prisma.timesheetsArchive.findMany({
|
||||
select: { timesheet_id: true },
|
||||
});
|
||||
// async update(id: number, dto: UpdateShiftsDto): Promise<Shifts> {
|
||||
// await this.findOne(id);
|
||||
// const { timesheet_id, bank_code_id, date,start_time,end_time, comment} = dto;
|
||||
// return this.prisma.shifts.update({
|
||||
// where: { id },
|
||||
// data: {
|
||||
// ...(timesheet_id !== undefined && { timesheet_id }),
|
||||
// ...(bank_code_id !== undefined && { bank_code_id }),
|
||||
// ...(date !== undefined && { date }),
|
||||
// ...(start_time !== undefined && { start_time }),
|
||||
// ...(end_time !== undefined && { end_time }),
|
||||
// ...(comment !== undefined && { comment }),
|
||||
// },
|
||||
// include: { timesheet: { include: { employee: { include: { user: true } } } },
|
||||
// bank_code: true,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
const timesheet_ids = archived_timesheets.map(sheet => sheet.timesheet_id);
|
||||
if(timesheet_ids.length === 0) {
|
||||
return;
|
||||
}
|
||||
// async remove(id: number): Promise<Shifts> {
|
||||
// await this.findOne(id);
|
||||
// return this.prisma.shifts.delete({ where: { id } });
|
||||
// }
|
||||
|
||||
// copy/delete transaction
|
||||
await this.prisma.$transaction(async transaction => {
|
||||
//fetches shifts to move to archive
|
||||
const shifts_to_archive = await transaction.shifts.findMany({
|
||||
where: { timesheet_id: { in: timesheet_ids } },
|
||||
});
|
||||
if(shifts_to_archive.length === 0) {
|
||||
return;
|
||||
}
|
||||
// async create(dto: CreateShiftDto): Promise<Shifts> {
|
||||
// const { timesheet_id, bank_code_id, date, start_time, end_time, comment } = dto;
|
||||
|
||||
//copies sent to archive table
|
||||
await transaction.shiftsArchive.createMany({
|
||||
data: shifts_to_archive.map(shift => ({
|
||||
shift_id: shift.id,
|
||||
timesheet_id: shift.timesheet_id,
|
||||
bank_code_id: shift.bank_code_id,
|
||||
comment: shift.comment ?? undefined,
|
||||
date: shift.date,
|
||||
start_time: shift.start_time,
|
||||
end_time: shift.end_time,
|
||||
})),
|
||||
});
|
||||
// //shift creation
|
||||
// const shift = await this.prisma.shifts.create({
|
||||
// data: { timesheet_id, bank_code_id, date, start_time, end_time, comment },
|
||||
// include: { timesheet: { include: { employee: { include: { user: true } } } },
|
||||
// bank_code: true,
|
||||
// },
|
||||
// });
|
||||
|
||||
//delete from shifts table
|
||||
await transaction.shifts.deleteMany({
|
||||
where: { id: { in: shifts_to_archive.map(shift => shift.id) } },
|
||||
})
|
||||
// //fetches all shifts of the same day to check for daily overtime
|
||||
// const same_day_shifts = await this.prisma.shifts.findMany({
|
||||
// where: { timesheet_id, date },
|
||||
// select: { id: true, date: true, start_time: true, end_time: true },
|
||||
// });
|
||||
|
||||
})
|
||||
}
|
||||
// //sums hours of the day
|
||||
// const total_hours = same_day_shifts.reduce((sum, s) => {
|
||||
// return sum + hoursBetweenSameDay(s.date, s.start_time, s.end_time);
|
||||
// }, 0 );
|
||||
|
||||
//fetches all archived timesheets
|
||||
async findAllArchived(): Promise<ShiftsArchive[]> {
|
||||
return this.prisma.shiftsArchive.findMany();
|
||||
}
|
||||
// //Notify if total hours > 8 for a single day
|
||||
// if(total_hours > DAILY_LIMIT_HOURS ) {
|
||||
// const user_id = String(shift.timesheet.employee.user.id);
|
||||
// const date_label = new Date(date).toLocaleDateString('fr-CA');
|
||||
// this.notifs.notify(user_id, {
|
||||
// type: 'shift.overtime.daily',
|
||||
// severity: 'warn',
|
||||
// message: `Tu viens de dépasser ${DAILY_LIMIT_HOURS.toFixed(2)}h pour la journée du ${date_label}
|
||||
// (total: ${total_hours.toFixed(2)}h).`,
|
||||
// ts: new Date().toISOString(),
|
||||
// meta: {
|
||||
// timesheet_id,
|
||||
// date: new Date(date).toISOString(),
|
||||
// total_hours,
|
||||
// threshold: DAILY_LIMIT_HOURS,
|
||||
// last_shift_id: shift.id
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// return shift;
|
||||
// }
|
||||
// async findAll(filters: SearchShiftsDto): Promise <Shifts[]> {
|
||||
// const where = buildPrismaWhere(filters);
|
||||
// const shifts = await this.prisma.shifts.findMany({ where })
|
||||
// return shifts;
|
||||
// }
|
||||
|
||||
//fetches an archived timesheet
|
||||
async findOneArchived(id: number): Promise<ShiftsArchive> {
|
||||
return this.prisma.shiftsArchive.findUniqueOrThrow({ where: { id } });
|
||||
}
|
||||
// async findOne(id: number): Promise<Shifts> {
|
||||
// const shift = await this.prisma.shifts.findUnique({
|
||||
// where: { id },
|
||||
// include: { timesheet: { include: { employee: { include: { user: true } } } },
|
||||
// bank_code: true,
|
||||
// },
|
||||
// });
|
||||
// if(!shift) {
|
||||
// throw new NotFoundException(`Shift #${id} not found`);
|
||||
// }
|
||||
// return shift;
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
@ -4,11 +4,12 @@ import { BusinessLogicsModule } from 'src/modules/business-logics/business-logic
|
|||
import { ShiftsCommandService } from './services/shifts-command.service';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { ShiftsQueryService } from './services/shifts-query.service';
|
||||
import { ShiftsArchivalService } from './services/shifts-archival.service';
|
||||
|
||||
@Module({
|
||||
imports: [BusinessLogicsModule, NotificationsModule],
|
||||
controllers: [ShiftsController],
|
||||
providers: [ShiftsQueryService, ShiftsCommandService],
|
||||
exports: [ShiftsQueryService, ShiftsCommandService],
|
||||
providers: [ShiftsQueryService, ShiftsCommandService, ShiftsArchivalService],
|
||||
exports: [ShiftsQueryService, ShiftsCommandService, ShiftsArchivalService],
|
||||
})
|
||||
export class ShiftsModule {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
export interface OverviewRow {
|
||||
full_name: string;
|
||||
supervisor: string;
|
||||
total_regular_hrs: number;
|
||||
total_evening_hrs: number;
|
||||
total_overtime_hrs: number;
|
||||
total_expenses: number;
|
||||
total_mileage: number;
|
||||
is_approved: boolean;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export type DayShiftResponse = {
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
type: string;
|
||||
is_remote: boolean;
|
||||
comment: string | null;
|
||||
}
|
||||
|
||||
export type UpsertAction = 'created' | 'updated' | 'deleted';
|
||||
37
src/modules/shifts/utils/shifts.utils.ts
Normal file
37
src/modules/shifts/utils/shifts.utils.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { NotFoundException } from "@nestjs/common";
|
||||
import { ShiftPayloadDto } from "../dtos/upsert-shift.dto";
|
||||
import { timeFromHHMMUTC } from "../helpers/shifts-date-time-helpers";
|
||||
|
||||
export function overlaps(
|
||||
a_start_ms: number,
|
||||
a_end_ms: number,
|
||||
b_start_ms: number,
|
||||
b_end_ms: number,
|
||||
): boolean {
|
||||
return a_start_ms < b_end_ms && b_start_ms < a_end_ms;
|
||||
}
|
||||
|
||||
export function resolveBankCodeByType(type: string): Promise<number> {
|
||||
const bank = this.prisma.bankCodes.findFirst({
|
||||
where: { type },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!bank) {
|
||||
throw new NotFoundException({ error_code: 'SHIFT_TYPE_UNKNOWN', message: `unknown shift type: ${type}` });
|
||||
}
|
||||
return bank.id;
|
||||
}
|
||||
|
||||
export function normalizeShiftPayload(payload: ShiftPayloadDto) {
|
||||
//normalize shift's infos
|
||||
const start_time = timeFromHHMMUTC(payload.start_time);
|
||||
const end_time = timeFromHHMMUTC(payload.end_time );
|
||||
const type = (payload.type || '').trim().toUpperCase();
|
||||
const is_remote = payload.is_remote === true;
|
||||
//normalize comment
|
||||
const raw_comment = payload.comment ?? null;
|
||||
const trimmed = typeof raw_comment === 'string' ? raw_comment.trim() : null;
|
||||
const comment = trimmed && trimmed.length > 0 ? trimmed: null;
|
||||
|
||||
return { start_time, end_time, type, is_remote, comment };
|
||||
}
|
||||
52
src/modules/timesheets/services/timesheet-archive.service.ts
Normal file
52
src/modules/timesheets/services/timesheet-archive.service.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { TimesheetsArchive } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
|
||||
export class TimesheetArchiveService {
|
||||
constructor(private readonly prisma: PrismaService){}
|
||||
|
||||
async archiveOld(): Promise<void> {
|
||||
//calcul du cutoff pour archivation
|
||||
const cutoff = new Date();
|
||||
cutoff.setMonth(cutoff.getMonth() - 6)
|
||||
|
||||
await this.prisma.$transaction(async transaction => {
|
||||
//fetches all timesheets to cutoff
|
||||
const oldSheets = await transaction.timesheets.findMany({
|
||||
where: { shift: { some: { date: { lt: cutoff } } },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
employee_id: true,
|
||||
is_approved: true,
|
||||
},
|
||||
});
|
||||
if( oldSheets.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
//preping data for archivation
|
||||
const archiveDate = oldSheets.map(sheet => ({
|
||||
timesheet_id: sheet.id,
|
||||
employee_id: sheet.employee_id,
|
||||
is_approved: sheet.is_approved,
|
||||
}));
|
||||
|
||||
//copying data from timesheets table to archive table
|
||||
await transaction.timesheetsArchive.createMany({ data: archiveDate });
|
||||
|
||||
//removing data from timesheets table
|
||||
await transaction.timesheets.deleteMany({ where: { id: { in: oldSheets.map(s => s.id) } } });
|
||||
});
|
||||
}
|
||||
|
||||
//fetches all archived timesheets
|
||||
async findAllArchived(): Promise<TimesheetsArchive[]> {
|
||||
return this.prisma.timesheetsArchive.findMany();
|
||||
}
|
||||
|
||||
//fetches an archived timesheet
|
||||
async findOneArchived(id: number): Promise<TimesheetsArchive> {
|
||||
return this.prisma.timesheetsArchive.findUniqueOrThrow({ where: { id } });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Prisma, Timesheets } from "@prisma/client";
|
||||
import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
||||
|
|
@ -7,6 +6,7 @@ import { TimesheetsQueryService } from "./timesheets-query.service";
|
|||
import { CreateTimesheetDto } from "../dtos/create-timesheet.dto";
|
||||
import { TimesheetDto } from "../dtos/overview-timesheet.dto";
|
||||
import { getWeekEnd, getWeekStart } from "src/common/utils/date-utils";
|
||||
import { parseISODate, parseHHmm } from "../utils/timesheet.helpers";
|
||||
|
||||
@Injectable()
|
||||
export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
||||
|
|
@ -14,7 +14,9 @@ export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
|||
prisma: PrismaService,
|
||||
private readonly query: TimesheetsQueryService,
|
||||
) {super(prisma);}
|
||||
|
||||
//_____________________________________________________________________________________________
|
||||
// APPROVAL AND DELEGATE METHODS
|
||||
//_____________________________________________________________________________________________
|
||||
protected get delegate() {
|
||||
return this.prisma.timesheets;
|
||||
}
|
||||
|
|
@ -46,16 +48,7 @@ export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
|||
}
|
||||
|
||||
|
||||
//create shifts within timesheet's week - employee overview functions
|
||||
private parseISODate(iso: string): Date {
|
||||
const [ y, m, d ] = iso.split('-').map(Number);
|
||||
return new Date(y, (m ?? 1) - 1, d ?? 1);
|
||||
}
|
||||
|
||||
private parseHHmm(t: string): Date {
|
||||
const [ hh, mm ] = t.split(':').map(Number);
|
||||
return new Date(1970, 0, 1, hh || 0, mm || 0, 0, 0);
|
||||
}
|
||||
|
||||
async createWeekShiftsAndReturnOverview(
|
||||
email:string,
|
||||
|
|
@ -101,7 +94,7 @@ export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
|||
|
||||
//validations and insertions
|
||||
for(const shift of shifts) {
|
||||
const date = this.parseISODate(shift.date);
|
||||
const date = parseISODate(shift.date);
|
||||
if (date < start_week || date > end_week) throw new BadRequestException(`date ${shift.date} not in current week`);
|
||||
|
||||
const bank_code = await this.prisma.bankCodes.findFirst({
|
||||
|
|
@ -115,8 +108,8 @@ export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
|||
timesheet_id: timesheet.id,
|
||||
bank_code_id: bank_code.id,
|
||||
date: date,
|
||||
start_time: this.parseHHmm(shift.start_time),
|
||||
end_time: this.parseHHmm(shift.end_time),
|
||||
start_time: parseHHmm(shift.start_time),
|
||||
end_time: parseHHmm(shift.end_time),
|
||||
comment: shift.comment ?? null,
|
||||
is_approved: false,
|
||||
is_remote: false,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Timesheets, TimesheetsArchive } from '@prisma/client';
|
||||
import { buildPeriod, endOfDayUTC, toHHmm, toUTCDateOnly } from '../utils/timesheet.helpers';
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { buildPeriod, endOfDayUTC, toUTCDateOnly } from '../utils/timesheet.helpers';
|
||||
import { computeHours, formatDateISO, getWeekEnd, getWeekStart } from 'src/common/utils/date-utils';
|
||||
import { formatDateISO, getWeekEnd, getWeekStart } from 'src/common/utils/date-utils';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { OvertimeService } from 'src/modules/business-logics/services/overtime.service';
|
||||
import { TimesheetDto } from '../dtos/overview-timesheet.dto';
|
||||
|
|
@ -13,7 +12,7 @@ import type { ShiftRow, ExpenseRow } from '../utils/timesheet.helpers';
|
|||
export class TimesheetsQueryService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly overtime: OvertimeService,
|
||||
// private readonly overtime: OvertimeService,
|
||||
) {}
|
||||
|
||||
async findAll(year: number, period_no: number, email: string): Promise<TimesheetPeriodDto> {
|
||||
|
|
@ -67,7 +66,7 @@ export class TimesheetsQueryService {
|
|||
orderBy: { date: 'asc' },
|
||||
});
|
||||
|
||||
const to_num = (value: any) =>
|
||||
const toNum = (value: any) =>
|
||||
value && typeof value.toNumber === 'function' ? value.toNumber() :
|
||||
typeof value === 'number' ? value :
|
||||
value ? Number(value) : 0;
|
||||
|
|
@ -85,7 +84,7 @@ export class TimesheetsQueryService {
|
|||
|
||||
const expenses: ExpenseRow[] = raw_expenses.map(expense => ({
|
||||
date: expense.date,
|
||||
amount: to_num(expense.amount),
|
||||
amount: toNum(expense.amount),
|
||||
comment: expense.comment ?? '',
|
||||
supervisor_comment: expense.supervisor_comment ?? '',
|
||||
is_approved: expense.is_approved ?? true,
|
||||
|
|
@ -158,14 +157,14 @@ export class TimesheetsQueryService {
|
|||
}
|
||||
|
||||
//small helper to format hours:minutes
|
||||
const to_HH_mm = (date: Date) => date.toISOString().slice(11, 16);
|
||||
|
||||
|
||||
//maps all shifts of selected timesheet
|
||||
const shifts = timesheet.shift.map((shift_row) => ({
|
||||
bank_type: shift_row.bank_code?.type ?? '',
|
||||
date: formatDateISO(shift_row.date),
|
||||
start_time: to_HH_mm(shift_row.start_time),
|
||||
end_time: to_HH_mm(shift_row.end_time),
|
||||
start_time: toHHmm(shift_row.start_time),
|
||||
end_time: toHHmm(shift_row.end_time),
|
||||
comment: shift_row.comment ?? '',
|
||||
is_approved: shift_row.is_approved ?? false,
|
||||
is_remote: shift_row.is_remote ?? false,
|
||||
|
|
@ -176,10 +175,10 @@ export class TimesheetsQueryService {
|
|||
bank_type: exp.bank_code?.type ?? '',
|
||||
date: formatDateISO(exp.date),
|
||||
amount: Number(exp.amount) || 0,
|
||||
km: 0,
|
||||
comment: exp.comment ?? '',
|
||||
supervisor_comment: exp.supervisor_comment ?? '',
|
||||
is_approved: exp.is_approved ?? false,
|
||||
km: 0,
|
||||
supervisor_comment: exp.supervisor_comment ?? '',
|
||||
}));
|
||||
|
||||
return {
|
||||
|
|
@ -191,85 +190,40 @@ export class TimesheetsQueryService {
|
|||
expenses,
|
||||
} as TimesheetDto;
|
||||
}
|
||||
//_____________________________________________________________________________________________
|
||||
// Deprecated or unused methods
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
async findOne(id: number): Promise<any> {
|
||||
const timesheet = await this.prisma.timesheets.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
shift: { include: { bank_code: true } },
|
||||
expense: { include: { bank_code: true } },
|
||||
employee: { include: { user: true } },
|
||||
},
|
||||
});
|
||||
if(!timesheet) {
|
||||
throw new NotFoundException(`Timesheet #${id} not found`);
|
||||
}
|
||||
|
||||
const detailedShifts = timesheet.shift.map( s => {
|
||||
const hours = computeHours(s.start_time, s.end_time);
|
||||
const regularHours = Math.min(8, hours);
|
||||
const dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time);
|
||||
const payRegular = regularHours * s.bank_code.modifier;
|
||||
const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, s.bank_code.modifier);
|
||||
return { ...s, hours, payRegular, payOvertime };
|
||||
});
|
||||
const weeklyOvertimeHours = detailedShifts.length
|
||||
? await this.overtime.getWeeklyOvertimeHours(
|
||||
timesheet.employee_id,
|
||||
timesheet.shift[0].date): 0;
|
||||
return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours };
|
||||
}
|
||||
|
||||
async remove(id: number): Promise<Timesheets> {
|
||||
await this.findOne(id);
|
||||
return this.prisma.timesheets.delete({ where: { id } });
|
||||
}
|
||||
|
||||
|
||||
//archivation functions ******************************************************
|
||||
|
||||
async archiveOld(): Promise<void> {
|
||||
//calcul du cutoff pour archivation
|
||||
const cutoff = new Date();
|
||||
cutoff.setMonth(cutoff.getMonth() - 6)
|
||||
|
||||
await this.prisma.$transaction(async transaction => {
|
||||
//fetches all timesheets to cutoff
|
||||
const oldSheets = await transaction.timesheets.findMany({
|
||||
where: { shift: { some: { date: { lt: cutoff } } },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
employee_id: true,
|
||||
is_approved: true,
|
||||
},
|
||||
});
|
||||
if( oldSheets.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
//preping data for archivation
|
||||
const archiveDate = oldSheets.map(sheet => ({
|
||||
timesheet_id: sheet.id,
|
||||
employee_id: sheet.employee_id,
|
||||
is_approved: sheet.is_approved,
|
||||
}));
|
||||
|
||||
//copying data from timesheets table to archive table
|
||||
await transaction.timesheetsArchive.createMany({ data: archiveDate });
|
||||
|
||||
//removing data from timesheets table
|
||||
await transaction.timesheets.deleteMany({ where: { id: { in: oldSheets.map(s => s.id) } } });
|
||||
});
|
||||
}
|
||||
|
||||
//fetches all archived timesheets
|
||||
async findAllArchived(): Promise<TimesheetsArchive[]> {
|
||||
return this.prisma.timesheetsArchive.findMany();
|
||||
}
|
||||
|
||||
//fetches an archived timesheet
|
||||
async findOneArchived(id: number): Promise<TimesheetsArchive> {
|
||||
return this.prisma.timesheetsArchive.findUniqueOrThrow({ where: { id } });
|
||||
}
|
||||
// async findOne(id: number): Promise<any> {
|
||||
// const timesheet = await this.prisma.timesheets.findUnique({
|
||||
// where: { id },
|
||||
// include: {
|
||||
// shift: { include: { bank_code: true } },
|
||||
// expense: { include: { bank_code: true } },
|
||||
// employee: { include: { user: true } },
|
||||
// },
|
||||
// });
|
||||
// if(!timesheet) {
|
||||
// throw new NotFoundException(`Timesheet #${id} not found`);
|
||||
// }
|
||||
|
||||
// const detailedShifts = timesheet.shift.map( s => {
|
||||
// const hours = computeHours(s.start_time, s.end_time);
|
||||
// const regularHours = Math.min(8, hours);
|
||||
// const dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time);
|
||||
// const payRegular = regularHours * s.bank_code.modifier;
|
||||
// const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, s.bank_code.modifier);
|
||||
// return { ...s, hours, payRegular, payOvertime };
|
||||
// });
|
||||
// const weeklyOvertimeHours = detailedShifts.length
|
||||
// ? await this.overtime.getWeeklyOvertimeHours(
|
||||
// timesheet.employee_id,
|
||||
// timesheet.shift[0].date): 0;
|
||||
// return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours };
|
||||
// }
|
||||
|
||||
// async remove(id: number): Promise<Timesheets> {
|
||||
// await this.findOne(id);
|
||||
// return this.prisma.timesheets.delete({ where: { id } });
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TimesheetsController } from './controllers/timesheets.controller';
|
||||
import { TimesheetsQueryService } from './services/timesheets-query.service';
|
||||
import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module';
|
||||
import { TimesheetArchiveService } from './services/timesheet-archive.service';
|
||||
import { TimesheetsCommandService } from './services/timesheets-command.service';
|
||||
import { ShiftsCommandService } from '../shifts/services/shifts-command.service';
|
||||
import { ExpensesCommandService } from '../expenses/services/expenses-command.service';
|
||||
import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module';
|
||||
import { BankCodesRepo } from '../expenses/repos/bank-codes.repo';
|
||||
import { TimesheetsRepo } from '../expenses/repos/timesheets.repo';
|
||||
import { EmployeesRepo } from '../expenses/repos/employee.repo';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
@Module({
|
||||
imports: [BusinessLogicsModule],
|
||||
|
|
@ -17,10 +18,15 @@ import { EmployeesRepo } from '../expenses/repos/employee.repo';
|
|||
TimesheetsCommandService,
|
||||
ShiftsCommandService,
|
||||
ExpensesCommandService,
|
||||
TimesheetArchiveService,
|
||||
BankCodesRepo,
|
||||
TimesheetsRepo,
|
||||
EmployeesRepo,
|
||||
],
|
||||
exports: [TimesheetsQueryService],
|
||||
exports: [
|
||||
TimesheetsQueryService,
|
||||
TimesheetArchiveService,
|
||||
TimesheetsCommandService
|
||||
],
|
||||
})
|
||||
export class TimesheetsModule {}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,19 @@ export function dayKeyFromDate(date: Date, useUTC = true): DayKey {
|
|||
return DAY_KEYS[index];
|
||||
}
|
||||
|
||||
export const toHHmm = (date: Date) => date.toISOString().slice(11, 16);
|
||||
|
||||
//create shifts within timesheet's week - employee overview functions
|
||||
export function parseISODate(iso: string): Date {
|
||||
const [ y, m, d ] = iso.split('-').map(Number);
|
||||
return new Date(y, (m ?? 1) - 1, d ?? 1);
|
||||
}
|
||||
|
||||
export function parseHHmm(t: string): Date {
|
||||
const [ hh, mm ] = t.split(':').map(Number);
|
||||
return new Date(1970, 0, 1, hh || 0, mm || 0, 0, 0);
|
||||
}
|
||||
|
||||
//Date & Format
|
||||
const MS_PER_DAY = 86_400_000;
|
||||
const MS_PER_HOUR = 3_600_000;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user