clean(pre-prod): did some cleaning
This commit is contained in:
parent
5d6f6bfd0a
commit
dda1592871
|
|
@ -2,11 +2,3 @@ export type Result<T, E> =
|
||||||
| { success: true; data: T }
|
| { success: true; data: T }
|
||||||
| { success: false; error: E };
|
| { success: false; error: E };
|
||||||
|
|
||||||
// const success = <T>(data: T): Result<T, never> => {
|
|
||||||
// return { success: true, data };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const failure = <E>(error: E): Result<never, E> => {
|
|
||||||
// return { success: false, error };
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
// import { Roles as RoleEnum } from ".prisma/client";
|
|
||||||
|
|
||||||
// export const GLOBAL_CONTROLLER_ROLES: readonly RoleEnum[] = [
|
|
||||||
// RoleEnum.EMPLOYEE,
|
|
||||||
// RoleEnum.ACCOUNTING,
|
|
||||||
// RoleEnum.HR,
|
|
||||||
// RoleEnum.SUPERVISOR,
|
|
||||||
// RoleEnum.ADMIN,
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// export const MANAGER_ROLES: readonly RoleEnum[] = [
|
|
||||||
// RoleEnum.HR,
|
|
||||||
// RoleEnum.SUPERVISOR,
|
|
||||||
// RoleEnum.ADMIN,
|
|
||||||
// ]
|
|
||||||
|
|
||||||
// export const PAY_SERVICE: readonly RoleEnum[] = [
|
|
||||||
// RoleEnum.HR,
|
|
||||||
// RoleEnum.ACCOUNTING,
|
|
||||||
// ]
|
|
||||||
|
|
@ -147,3 +147,11 @@ export const overlaps = (a: { start: Date; end: Date, date?: Date; }, b: { start
|
||||||
export const is_same_week_day = (date: Date, week_day: Weekday): boolean => {
|
export const is_same_week_day = (date: Date, week_day: Weekday): boolean => {
|
||||||
return date.getUTCDay() !== WEEKDAY_MAP[week_day];
|
return date.getUTCDay() !== WEEKDAY_MAP[week_day];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const addHourstoDateString = (start_time: string, hours: number): string => {
|
||||||
|
const start = toDateFromHHmm(start_time);
|
||||||
|
const end = new Date(start.getTime() + hours * 60 * 60 * 1000);
|
||||||
|
const hh = String(end.getUTCHours()).padStart(2, '0');
|
||||||
|
const mm = String(end.getUTCMinutes()).padStart(2, '0');
|
||||||
|
return `${hh}:${mm}:00`;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { IsArray, IsBoolean, IsDateString, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator';
|
import { IsArray, IsBoolean, IsDateString, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { PaidTimeOffDto } from 'src/time-and-attendance/domains/paid-time-off.dto';
|
import { PaidTimeOffDto } from 'src/time-and-attendance/paid-time-off/paid-time-off.dto';
|
||||||
|
|
||||||
export class EmployeeDetailedDto {
|
export class EmployeeDetailedDto {
|
||||||
@IsString() @IsNotEmpty() first_name: string;
|
@IsString() @IsNotEmpty() first_name: string;
|
||||||
|
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
// import { Injectable } from "@nestjs/common";
|
|
||||||
// import { Employees, Users } from "@prisma/client";
|
|
||||||
// // import { UpdateEmployeeDto } from "src/identity-and-account/employees/dtos/update-employee.dto";
|
|
||||||
// import { toDateOrUndefined, toDateOrNull } from "src/identity-and-account/employees/utils/employee.utils";
|
|
||||||
// import { PrismaService } from "src/prisma/prisma.service";
|
|
||||||
|
|
||||||
// @Injectable()
|
|
||||||
// export class EmployeesArchivalService {
|
|
||||||
// constructor(private readonly prisma: PrismaService) { }
|
|
||||||
|
|
||||||
// async patchEmployee(email: string, dto: UpdateEmployeeDto): Promise<Employees | 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 } });
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
@ -8,8 +8,7 @@ import { Result } from "src/common/errors/result-error.factory";
|
||||||
import { toCompanyCodeFromString } from "src/identity-and-account/employees/employee.utils";
|
import { toCompanyCodeFromString } from "src/identity-and-account/employees/employee.utils";
|
||||||
import { EmployeeDetailedUpsertDto } from "src/identity-and-account/employees/employee-detailed.dto";
|
import { EmployeeDetailedUpsertDto } from "src/identity-and-account/employees/employee-detailed.dto";
|
||||||
import { toBooleanFromString } from "src/identity-and-account/employees/services/employees-get.service";
|
import { toBooleanFromString } from "src/identity-and-account/employees/services/employees-get.service";
|
||||||
import { PaidTimeOffDto } from "src/time-and-attendance/domains/paid-time-off.dto";
|
import { PaidTimeOffDto } from "src/time-and-attendance/paid-time-off/paid-time-off.dto";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmployeesUpdateService {
|
export class EmployeesUpdateService {
|
||||||
constructor(
|
constructor(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Controller, Get } from "@nestjs/common";
|
import { Controller, Get } from "@nestjs/common";
|
||||||
import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
|
import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
|
||||||
import { HomePageService } from "src/identity-and-account/help/home-page.service";
|
import { HomePageService } from "src/identity-and-account/help/help-page.service";
|
||||||
import { Modules as ModulesEnum } from ".prisma/client";
|
import { Modules as ModulesEnum } from ".prisma/client";
|
||||||
import { Access } from "src/common/decorators/module-access.decorators";
|
import { Access } from "src/common/decorators/module-access.decorators";
|
||||||
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
|
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
|
||||||
import { HomePageController } from "src/identity-and-account/help/home-page.controller";
|
import { HomePageController } from "src/identity-and-account/help/help-page.controller";
|
||||||
import { HomePageService } from "src/identity-and-account/help/home-page.service";
|
import { HomePageService } from "src/identity-and-account/help/help-page.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [HomePageController],
|
controllers: [HomePageController],
|
||||||
|
|
@ -12,11 +12,11 @@ import { AccessGetService } from "src/identity-and-account/user-module-access/se
|
||||||
import { AccessUpdateService } from "src/identity-and-account/user-module-access/services/module-access-update.service";
|
import { AccessUpdateService } from "src/identity-and-account/user-module-access/services/module-access-update.service";
|
||||||
import { UsersService } from "src/identity-and-account/users-management/services/users.service";
|
import { UsersService } from "src/identity-and-account/users-management/services/users.service";
|
||||||
import { UsersModule } from "src/identity-and-account/users-management/users.module";
|
import { UsersModule } from "src/identity-and-account/users-management/users.module";
|
||||||
import { HomePageModule } from "src/identity-and-account/help/home-page.module";
|
import { HomePageModule } from "src/identity-and-account/help/help-page.module";
|
||||||
import { EmployeesCreateService } from "src/identity-and-account/employees/services/employees-create.service";
|
import { EmployeesCreateService } from "src/identity-and-account/employees/services/employees-create.service";
|
||||||
import { EmployeesUpdateService } from "src/identity-and-account/employees/services/employees-update.service";
|
import { EmployeesUpdateService } from "src/identity-and-account/employees/services/employees-update.service";
|
||||||
import { HomePageController } from "src/identity-and-account/help/home-page.controller";
|
import { HomePageController } from "src/identity-and-account/help/help-page.controller";
|
||||||
import { HomePageService } from "src/identity-and-account/help/home-page.service";
|
import { HomePageService } from "src/identity-and-account/help/help-page.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,10 @@ export class BankCodesService {
|
||||||
bank_code: dto.bank_code,
|
bank_code: dto.bank_code,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return { success: true, data: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: 'INVALID_BANK_CODE' + error };
|
return { success: false, error: 'INVALID_BANK_CODE' + error };
|
||||||
}
|
}
|
||||||
return { success: true, data: true };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll() {
|
findAll() {
|
||||||
|
|
@ -44,4 +44,15 @@ export class BankCodesService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async delete(id: number): Promise<Result<boolean, string>> {
|
||||||
|
try {
|
||||||
|
await this.prisma.bankCodes.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
return { success: true, data: true };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: 'INVALID_BANK_CODE_ID' + error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { Type } from "class-transformer";
|
|
||||||
import { IsString, IsInt, IsNotEmpty, IsOptional } from "class-validator";
|
|
||||||
|
|
||||||
export class PaidTimeOffDto {
|
|
||||||
@IsInt() id: number;
|
|
||||||
@IsInt() @IsNotEmpty() employee_id: number;
|
|
||||||
@IsOptional() @Type(() => Number) vacation_hours?: number;
|
|
||||||
@IsOptional() @Type(() => Number) sick_hours?: number;
|
|
||||||
@IsOptional() @Type(() => Number) banked_hours?: number;
|
|
||||||
@IsString() @IsOptional() last_updated?: string | null;
|
|
||||||
|
|
||||||
constructor(employee_id: number) {
|
|
||||||
this.employee_id = employee_id;
|
|
||||||
this.banked_hours = 0;
|
|
||||||
this.sick_hours = 0;
|
|
||||||
this.vacation_hours = 0;
|
|
||||||
this.last_updated = new Date().toISOString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -30,7 +30,7 @@ export class HolidayService {
|
||||||
const window_start = new Date(holiday_week_start.getTime() - 4 * MS_PER_WEEK);
|
const window_start = new Date(holiday_week_start.getTime() - 4 * MS_PER_WEEK);
|
||||||
const window_end = new Date(holiday_week_start.getTime() - 1);
|
const window_end = new Date(holiday_week_start.getTime() - 1);
|
||||||
|
|
||||||
const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G305', 'G700'];
|
const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G305', 'G700', 'G720'];
|
||||||
const shifts = await this.prisma.shifts.findMany({
|
const shifts = await this.prisma.shifts.findMany({
|
||||||
where: {
|
where: {
|
||||||
timesheet: { employee_id: employee_id },
|
timesheet: { employee_id: employee_id },
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ type WeekOvertimeSummary = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OvertimeService {
|
export class OvertimeService {
|
||||||
|
|
||||||
private INCLUDED_TYPES = ['EMERGENCY', 'EVENING', 'OVERTIME', 'REGULAR', 'HOLIDAY'] as const; // included types for weekly overtime calculation
|
private INCLUDED_TYPES = ['EMERGENCY', 'EVENING', 'OVERTIME', 'REGULAR', 'HOLIDAY', 'BANKING'] as const; // included types for weekly overtime calculation
|
||||||
|
|
||||||
constructor(private prisma: PrismaService) { }
|
constructor(private prisma: PrismaService) { }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsBoolean, IsDecimal, IsInt, IsOptional, IsString, MaxLength } from "class-validator";
|
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength } from "class-validator";
|
||||||
|
|
||||||
export class ExpenseDto {
|
export class ExpenseDto {
|
||||||
@IsInt() id: number;
|
@IsInt() id: number;
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,26 @@
|
||||||
import { Controller, Post, Param, Body, Patch, Delete, Req, UnauthorizedException, Query } from "@nestjs/common";
|
import { Controller, Post, Param, Body, Patch, Delete, Req, UnauthorizedException, Query } from "@nestjs/common";
|
||||||
import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service";
|
|
||||||
import { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto";
|
import { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto";
|
||||||
import { Result } from "src/common/errors/result-error.factory";
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
|
import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
|
||||||
import { Modules as ModulesEnum } from ".prisma/client";
|
import { Modules as ModulesEnum } from ".prisma/client";
|
||||||
import { Access } from "src/common/decorators/module-access.decorators";
|
import { Access } from "src/common/decorators/module-access.decorators";
|
||||||
|
import { ExpenseUpdateService } from "src/time-and-attendance/expenses/services/expense-update.service";
|
||||||
|
import { ExpenseCreateService } from "src/time-and-attendance/expenses/services/expense-create.service";
|
||||||
|
import { ExpenseDeleteService } from "src/time-and-attendance/expenses/services/expense-delete.service";
|
||||||
|
|
||||||
@Controller('expense')
|
@Controller('expense')
|
||||||
export class ExpenseController {
|
export class ExpenseController {
|
||||||
constructor(private readonly upsert_service: ExpenseUpsertService) { }
|
constructor(
|
||||||
|
private readonly updateService: ExpenseUpdateService,
|
||||||
|
private readonly createService: ExpenseCreateService,
|
||||||
|
private readonly deleteService: ExpenseDeleteService,
|
||||||
|
) { }
|
||||||
|
|
||||||
@Post('create')
|
@Post('create')
|
||||||
@ModuleAccessAllowed(ModulesEnum.timesheets)
|
@ModuleAccessAllowed(ModulesEnum.timesheets)
|
||||||
create(@Access('email') email: string, @Body() dto: ExpenseDto): Promise<Result<ExpenseDto, string>> {
|
create(@Access('email') email: string, @Body() dto: ExpenseDto): Promise<Result<ExpenseDto, string>> {
|
||||||
if (!email) throw new UnauthorizedException('Unauthorized User');
|
if (!email) throw new UnauthorizedException('Unauthorized User');
|
||||||
return this.upsert_service.createExpense(dto, email);
|
return this.createService.createExpense(dto, email);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('update')
|
@Patch('update')
|
||||||
|
|
@ -24,13 +30,13 @@ export class ExpenseController {
|
||||||
@Access('email') email: string,
|
@Access('email') email: string,
|
||||||
@Query('employee_email') employee_email?: string,
|
@Query('employee_email') employee_email?: string,
|
||||||
): Promise<Result<ExpenseDto, string>> {
|
): Promise<Result<ExpenseDto, string>> {
|
||||||
return this.upsert_service.updateExpense(dto, email, employee_email);
|
return this.updateService.updateExpense(dto, email, employee_email);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('delete/:expense_id')
|
@Delete('delete/:expense_id')
|
||||||
@ModuleAccessAllowed(ModulesEnum.timesheets)
|
@ModuleAccessAllowed(ModulesEnum.timesheets)
|
||||||
remove(@Param('expense_id') expense_id: number): Promise<Result<number, string>> {
|
remove(@Param('expense_id') expense_id: number): Promise<Result<number, string>> {
|
||||||
return this.upsert_service.deleteExpense(expense_id);
|
return this.deleteService.deleteExpense(expense_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
42
src/time-and-attendance/expenses/expense.utils.ts
Normal file
42
src/time-and-attendance/expenses/expense.utils.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
import { toDateFromString } from "src/common/utils/date-utils";
|
||||||
|
import { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto";
|
||||||
|
import { NormalizedExpense } from "src/time-and-attendance/utils/type.utils";
|
||||||
|
|
||||||
|
//makes sure that a string cannot exceed 280 chars
|
||||||
|
export const truncate280 = (input: string): string => {
|
||||||
|
return input.length > 280 ? input.slice(0, 280) : input;
|
||||||
|
}
|
||||||
|
|
||||||
|
//makes sure that the type of data of numeric values is valid
|
||||||
|
export const parseOptionalNumber = (value: unknown, field: string) => {
|
||||||
|
if (value == null) return undefined;
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (Number.isNaN(parsed)) throw new Error(`Invalid value : ${value} for ${field}`);
|
||||||
|
return parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
//makes sure that comments are the right length the date is of Date type
|
||||||
|
export const normalizeAndParseExpenseDto = async (dto: ExpenseDto): Promise<Result<NormalizedExpense, string>> => {
|
||||||
|
const attachment = parseOptionalNumber(dto.attachment, "attachment");
|
||||||
|
const mileage = parseOptionalNumber(dto.mileage, "mileage");
|
||||||
|
const amount = parseOptionalNumber(dto.amount, "amount");
|
||||||
|
|
||||||
|
const comment = truncate280(dto.comment);
|
||||||
|
const supervisor_comment = dto.supervisor_comment && dto.supervisor_comment.trim()
|
||||||
|
? truncate280(dto.supervisor_comment.trim()) : undefined;
|
||||||
|
|
||||||
|
const date = toDateFromString(dto.date);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
date,
|
||||||
|
comment,
|
||||||
|
supervisor_comment,
|
||||||
|
amount,
|
||||||
|
attachment,
|
||||||
|
mileage,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,22 @@
|
||||||
import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service";
|
import { ExpenseUpdateService } from "src/time-and-attendance/expenses/services/expense-update.service";
|
||||||
import { ExpenseController } from "src/time-and-attendance/expenses/expense.controller";
|
import { ExpenseController } from "src/time-and-attendance/expenses/expense.controller";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
|
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
|
||||||
import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper";
|
import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper";
|
||||||
import { EmployeeTimesheetResolver } from "src/common/mappers/timesheet.mapper";
|
import { EmployeeTimesheetResolver } from "src/common/mappers/timesheet.mapper";
|
||||||
|
import { ExpenseDeleteService } from "src/time-and-attendance/expenses/services/expense-delete.service";
|
||||||
|
import { ExpenseCreateService } from "src/time-and-attendance/expenses/services/expense-create.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [ ExpenseController ],
|
controllers: [ExpenseController],
|
||||||
providers: [ ExpenseUpsertService, EmailToIdResolver, BankCodesResolver, EmployeeTimesheetResolver ],
|
providers: [
|
||||||
|
ExpenseCreateService,
|
||||||
|
ExpenseUpdateService,
|
||||||
|
ExpenseDeleteService,
|
||||||
|
EmailToIdResolver,
|
||||||
|
BankCodesResolver,
|
||||||
|
EmployeeTimesheetResolver,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
export class ExpensesModule {}
|
export class ExpensesModule { }
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper";
|
||||||
|
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
|
||||||
|
import { toStringFromDate, weekStartSunday } from "src/common/utils/date-utils";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto";
|
||||||
|
import { normalizeAndParseExpenseDto } from "src/time-and-attendance/expenses/expense.utils";
|
||||||
|
import { expense_select } from "src/time-and-attendance/utils/selects.utils";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExpenseCreateService {
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly emailResolver: EmailToIdResolver,
|
||||||
|
private readonly typeResolver: BankCodesResolver,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
//_________________________________________________________________
|
||||||
|
// CREATE
|
||||||
|
//_________________________________________________________________
|
||||||
|
async createExpense(dto: ExpenseDto, email: string): Promise<Result<ExpenseDto, string>> {
|
||||||
|
try {
|
||||||
|
//fetch employee_id using req.user.email
|
||||||
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
|
if (!employee_id.success) return { success: false, error: employee_id.error };
|
||||||
|
|
||||||
|
//normalize strings and dates and Parse numbers
|
||||||
|
const normed_expense = await normalizeAndParseExpenseDto(dto);
|
||||||
|
if (!normed_expense.success) return { success: false, error: normed_expense.error }
|
||||||
|
|
||||||
|
const type = await this.typeResolver.findBankCodeIDByType(dto.type);
|
||||||
|
if (!type.success) return { success: false, error: 'INVALID_EXPENSE_TYPE' }
|
||||||
|
|
||||||
|
//finds the timesheet using expense.date by finding the sunday
|
||||||
|
const start_date = weekStartSunday(normed_expense.data.date);
|
||||||
|
|
||||||
|
const timesheet = await this.prisma.timesheets.findFirst({
|
||||||
|
where: { start_date, employee_id: employee_id.data },
|
||||||
|
select: { id: true, employee_id: true },
|
||||||
|
});
|
||||||
|
if (!timesheet) return { success: false, error: `TIMESHEET_NOT_FOUND` };
|
||||||
|
|
||||||
|
//create a new expense
|
||||||
|
const expense = await this.prisma.expenses.create({
|
||||||
|
data: {
|
||||||
|
...normed_expense.data,
|
||||||
|
bank_code_id: type.data,
|
||||||
|
timesheet_id: timesheet.id,
|
||||||
|
is_approved: dto.is_approved,
|
||||||
|
},
|
||||||
|
//return the newly created expense with id
|
||||||
|
select: expense_select,
|
||||||
|
});
|
||||||
|
if (!expense) return { success: false, error: `INVALID_EXPENSE` };
|
||||||
|
|
||||||
|
//build an object to return to the frontend to display
|
||||||
|
const created: ExpenseDto = {
|
||||||
|
...expense,
|
||||||
|
type: dto.type,
|
||||||
|
date: toStringFromDate(expense.date),
|
||||||
|
amount: expense.amount?.toNumber() ?? undefined,
|
||||||
|
mileage: expense.mileage?.toNumber(),
|
||||||
|
attachment: expense.attachment ?? undefined,
|
||||||
|
supervisor_comment: expense.supervisor_comment ?? undefined,
|
||||||
|
};
|
||||||
|
return { success: true, data: created };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: 'INVALID_EXPENSE' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExpenseDeleteService {
|
||||||
|
constructor(private readonly prisma: PrismaService) { }
|
||||||
|
|
||||||
|
//_________________________________________________________________
|
||||||
|
// DELETE
|
||||||
|
//_________________________________________________________________
|
||||||
|
async deleteExpense(expense_id: number): Promise<Result<number, string>> {
|
||||||
|
try {
|
||||||
|
await this.prisma.$transaction(async (tx) => {
|
||||||
|
const expense = await tx.expenses.findUnique({
|
||||||
|
where: { id: expense_id },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (!expense) return { success: false, error: `EXPENSE_NOT_FOUND` };
|
||||||
|
|
||||||
|
await tx.expenses.delete({ where: { id: expense.id } });
|
||||||
|
return { success: true, data: expense.id };
|
||||||
|
});
|
||||||
|
return { success: true, data: expense_id };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: `EXPENSE_NOT_FOUND` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { weekStartSunday, toStringFromDate, toDateFromString } from "src/common/utils/date-utils";
|
||||||
|
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
|
||||||
|
import { expense_select, timesheet_select } from "src/time-and-attendance/utils/selects.utils";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper";
|
||||||
|
import { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { normalizeAndParseExpenseDto } from "src/time-and-attendance/expenses/expense.utils";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExpenseUpdateService {
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly emailResolver: EmailToIdResolver,
|
||||||
|
private readonly typeResolver: BankCodesResolver,
|
||||||
|
) { }
|
||||||
|
//_________________________________________________________________
|
||||||
|
// UPDATE
|
||||||
|
//_________________________________________________________________
|
||||||
|
async updateExpense(dto: ExpenseDto, email: string, employee_email?: string): Promise<Result<ExpenseDto, string>> {
|
||||||
|
try {
|
||||||
|
const account_email = employee_email ?? email;
|
||||||
|
//fetch employee_id using req.user.email
|
||||||
|
const employee_id = await this.emailResolver.findIdByEmail(account_email);
|
||||||
|
if (!employee_id.success) return { success: false, error: employee_id.error };
|
||||||
|
//normalize string , date format and parse numbers
|
||||||
|
const normed_expense = await normalizeAndParseExpenseDto(dto);
|
||||||
|
if (!normed_expense.success) return { success: false, error: normed_expense.error }
|
||||||
|
|
||||||
|
const type = await this.typeResolver.findBankCodeIDByType(dto.type);
|
||||||
|
if (!type.success) return { success: false, error: 'INVALID_EXPENSE_TYPE' }
|
||||||
|
//added timesheet_id modification check according to the new date
|
||||||
|
const new_timesheet_start_date = weekStartSunday(toDateFromString(dto.date));
|
||||||
|
|
||||||
|
const timesheet = await this.prisma.timesheets.findFirst({
|
||||||
|
where: { start_date: new_timesheet_start_date, employee_id: employee_id.data },
|
||||||
|
select: timesheet_select,
|
||||||
|
});
|
||||||
|
if (!timesheet) return { success: false, error: `TIMESHEET_NOT_FOUND` }
|
||||||
|
|
||||||
|
//checks for modifications
|
||||||
|
const data: Prisma.ExpensesUpdateInput = {
|
||||||
|
...normed_expense.data,
|
||||||
|
bank_code: { connect: { id: type.data } },
|
||||||
|
is_approved: dto.is_approved,
|
||||||
|
};
|
||||||
|
if (!data) return { success: false, error: `INVALID_EXPENSE` }
|
||||||
|
|
||||||
|
//push updates and get updated datas
|
||||||
|
const expense = await this.prisma.expenses.update({
|
||||||
|
where: { id: dto.id, timesheet_id: timesheet.id },
|
||||||
|
data,
|
||||||
|
select: expense_select,
|
||||||
|
});
|
||||||
|
if (!expense) return { success: false, error: 'INVALID_EXPENSE' }
|
||||||
|
|
||||||
|
//build an object to return to the frontend
|
||||||
|
const updated: ExpenseDto = {
|
||||||
|
...expense,
|
||||||
|
type: expense.bank_code.type,
|
||||||
|
date: toStringFromDate(expense.date),
|
||||||
|
amount: expense.amount?.toNumber(),
|
||||||
|
mileage: expense.mileage?.toNumber(),
|
||||||
|
attachment: expense.attachment ?? undefined,
|
||||||
|
supervisor_comment: expense.supervisor_comment ?? undefined,
|
||||||
|
};
|
||||||
|
return { success: true, data: updated };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: 'EXPENSE_NOT_FOUND' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,188 +0,0 @@
|
||||||
import { weekStartSunday, toStringFromDate, toDateFromString } from "src/common/utils/date-utils";
|
|
||||||
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
|
|
||||||
import { expense_select, timesheet_select } from "src/time-and-attendance/utils/selects.utils";
|
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
|
||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { Result } from "src/common/errors/result-error.factory";
|
|
||||||
import { NormalizedExpense } from "src/time-and-attendance/utils/type.utils";
|
|
||||||
import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper";
|
|
||||||
import { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto";
|
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ExpenseUpsertService {
|
|
||||||
constructor(
|
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly emailResolver: EmailToIdResolver,
|
|
||||||
private readonly typeResolver: BankCodesResolver,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
//_________________________________________________________________
|
|
||||||
// CREATE
|
|
||||||
//_________________________________________________________________
|
|
||||||
async createExpense(dto: ExpenseDto, email: string): Promise<Result<ExpenseDto, string>> {
|
|
||||||
try {
|
|
||||||
//fetch employee_id using req.user.email
|
|
||||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
|
||||||
if (!employee_id.success) return { success: false, error: employee_id.error };
|
|
||||||
|
|
||||||
//normalize strings and dates and Parse numbers
|
|
||||||
const normed_expense = await this.normalizeAndParseExpenseDto(dto);
|
|
||||||
if (!normed_expense.success) return { success: false, error: normed_expense.error }
|
|
||||||
|
|
||||||
//finds the timesheet using expense.date by finding the sunday
|
|
||||||
const start_date = weekStartSunday(normed_expense.data.date);
|
|
||||||
|
|
||||||
const timesheet = await this.prisma.timesheets.findFirst({
|
|
||||||
where: { start_date, employee_id: employee_id.data },
|
|
||||||
select: { id: true, employee_id: true },
|
|
||||||
});
|
|
||||||
if (!timesheet) return { success: false, error: `TIMESHEET_NOT_FOUND` };
|
|
||||||
|
|
||||||
//create a new expense
|
|
||||||
const expense = await this.prisma.expenses.create({
|
|
||||||
data: {
|
|
||||||
...normed_expense.data,
|
|
||||||
timesheet_id: timesheet.id,
|
|
||||||
is_approved: dto.is_approved,
|
|
||||||
},
|
|
||||||
//return the newly created expense with id
|
|
||||||
select: expense_select,
|
|
||||||
});
|
|
||||||
if (!expense) return { success: false, error: `INVALID_EXPENSE` };
|
|
||||||
|
|
||||||
//build an object to return to the frontend to display
|
|
||||||
const created: ExpenseDto = {
|
|
||||||
...expense,
|
|
||||||
type: dto.type,
|
|
||||||
date: toStringFromDate(expense.date),
|
|
||||||
amount: expense.amount?.toNumber() ?? undefined,
|
|
||||||
mileage: expense.mileage?.toNumber(),
|
|
||||||
attachment: expense.attachment ?? undefined,
|
|
||||||
supervisor_comment: expense.supervisor_comment ?? undefined,
|
|
||||||
};
|
|
||||||
return { success: true, data: created };
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: 'INVALID_EXPENSE' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//_________________________________________________________________
|
|
||||||
// UPDATE
|
|
||||||
//_________________________________________________________________
|
|
||||||
async updateExpense(dto: ExpenseDto, email: string, employee_email?: string): Promise<Result<ExpenseDto, string>> {
|
|
||||||
try {
|
|
||||||
const account_email = employee_email ?? email;
|
|
||||||
//fetch employee_id using req.user.email
|
|
||||||
const employee_id = await this.emailResolver.findIdByEmail(account_email);
|
|
||||||
if (!employee_id.success) return { success: false, error: employee_id.error };
|
|
||||||
//normalize string , date format and parse numbers
|
|
||||||
const normed_expense = await this.normalizeAndParseExpenseDto(dto);
|
|
||||||
if (!normed_expense.success) return { success: false, error: normed_expense.error }
|
|
||||||
|
|
||||||
//added timesheet_id modification check according to the new date
|
|
||||||
const new_timesheet_start_date = weekStartSunday(toDateFromString(dto.date));
|
|
||||||
|
|
||||||
const timesheet = await this.prisma.timesheets.findFirst({
|
|
||||||
where: { start_date: new_timesheet_start_date, employee_id: employee_id.data },
|
|
||||||
select: timesheet_select,
|
|
||||||
});
|
|
||||||
if (!timesheet) return { success: false, error: `TIMESHEET_NOT_FOUND` }
|
|
||||||
|
|
||||||
//checks for modifications
|
|
||||||
const data: Prisma.ExpensesUpdateInput = {
|
|
||||||
...normed_expense.data,
|
|
||||||
is_approved: dto.is_approved,
|
|
||||||
};
|
|
||||||
if (!data) return { success: false, error: `INVALID_EXPENSE` }
|
|
||||||
|
|
||||||
//push updates and get updated datas
|
|
||||||
const expense = await this.prisma.expenses.update({
|
|
||||||
where: { id: dto.id, timesheet_id: timesheet.id },
|
|
||||||
data,
|
|
||||||
select: expense_select,
|
|
||||||
});
|
|
||||||
if (!expense) return { success: false, error: 'INVALID_EXPENSE' }
|
|
||||||
|
|
||||||
//build an object to return to the frontend
|
|
||||||
const updated: ExpenseDto = {
|
|
||||||
...expense,
|
|
||||||
type: expense.bank_code.type,
|
|
||||||
date: toStringFromDate(expense.date),
|
|
||||||
amount: expense.amount?.toNumber(),
|
|
||||||
mileage: expense.mileage?.toNumber(),
|
|
||||||
attachment: expense.attachment ?? undefined,
|
|
||||||
supervisor_comment: expense.supervisor_comment ?? undefined,
|
|
||||||
};
|
|
||||||
return { success: true, data: updated };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: 'EXPENSE_NOT_FOUND' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//_________________________________________________________________
|
|
||||||
// DELETE
|
|
||||||
//_________________________________________________________________
|
|
||||||
async deleteExpense(expense_id: number): Promise<Result<number, string>> {
|
|
||||||
try {
|
|
||||||
await this.prisma.$transaction(async (tx) => {
|
|
||||||
const expense = await tx.expenses.findUnique({
|
|
||||||
where: { id: expense_id },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
if (!expense) return { success: false, error: `EXPENSE_NOT_FOUND` };
|
|
||||||
|
|
||||||
await tx.expenses.delete({ where: { id: expense.id } });
|
|
||||||
return { success: true, data: expense.id };
|
|
||||||
});
|
|
||||||
return { success: true, data: expense_id };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: `EXPENSE_NOT_FOUND` };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//_________________________________________________________________
|
|
||||||
// LOCAL HELPERS
|
|
||||||
//_________________________________________________________________
|
|
||||||
//makes sure that comments are the right length the date is of Date type
|
|
||||||
private normalizeAndParseExpenseDto = async (dto: ExpenseDto): Promise<Result<NormalizedExpense, string>> => {
|
|
||||||
const attachment = this.parseOptionalNumber(dto.attachment, "attachment");
|
|
||||||
const mileage = this.parseOptionalNumber(dto.mileage, "mileage");
|
|
||||||
const amount = this.parseOptionalNumber(dto.amount, "amount");
|
|
||||||
|
|
||||||
const comment = this.truncate280(dto.comment);
|
|
||||||
const supervisor_comment = dto.supervisor_comment && dto.supervisor_comment.trim()
|
|
||||||
? this.truncate280(dto.supervisor_comment.trim()) : undefined;
|
|
||||||
|
|
||||||
const date = toDateFromString(dto.date);
|
|
||||||
const type = await this.typeResolver.findBankCodeIDByType(dto.type);
|
|
||||||
if (!type.success) return { success: false, error: 'INVALID_EXPENSE_TYPE' }
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
date,
|
|
||||||
comment,
|
|
||||||
supervisor_comment,
|
|
||||||
amount,
|
|
||||||
attachment,
|
|
||||||
mileage,
|
|
||||||
bank_code_id: type.data,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//makes sure that a string cannot exceed 280 chars
|
|
||||||
private truncate280 = (input: string): string => {
|
|
||||||
return input.length > 280 ? input.slice(0, 280) : input;
|
|
||||||
}
|
|
||||||
|
|
||||||
//makes sure that the type of data of numeric values is valid
|
|
||||||
private parseOptionalNumber = (value: unknown, field: string) => {
|
|
||||||
if (value == null) return undefined;
|
|
||||||
const parsed = Number(value);
|
|
||||||
if (Number.isNaN(parsed)) throw new Error(`Invalid value : ${value} for ${field}`);
|
|
||||||
return parsed;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import { ExpensesArchive } from "@prisma/client";
|
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
|
||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
|
|
||||||
@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 } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
import { IsString, IsInt, IsNotEmpty, IsOptional } from "class-validator";
|
||||||
|
|
||||||
|
|
||||||
export const paid_time_off_types: string[] = ['SICK', 'VACATION', 'BANKING', 'WITHDRAW_BANKED'];
|
export const paid_time_off_types: string[] = ['SICK', 'VACATION', 'BANKING', 'WITHDRAW_BANKED'];
|
||||||
|
|
||||||
|
|
@ -8,3 +11,20 @@ export const paid_time_off_mapping: Record<string, { field: string; invert_logic
|
||||||
WITHDRAW_BANKED: { field: 'banked_hours', invert_logic: true, operation: 'increment' },
|
WITHDRAW_BANKED: { field: 'banked_hours', invert_logic: true, operation: 'increment' },
|
||||||
BANKING: { field: 'banked_hours', invert_logic: false, operation: 'decrement' },
|
BANKING: { field: 'banked_hours', invert_logic: false, operation: 'decrement' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export class PaidTimeOffDto {
|
||||||
|
@IsInt() id: number;
|
||||||
|
@IsInt() @IsNotEmpty() employee_id: number;
|
||||||
|
@IsOptional() @Type(() => Number) vacation_hours?: number;
|
||||||
|
@IsOptional() @Type(() => Number) sick_hours?: number;
|
||||||
|
@IsOptional() @Type(() => Number) banked_hours?: number;
|
||||||
|
@IsString() @IsOptional() last_updated?: string | null;
|
||||||
|
|
||||||
|
constructor(employee_id: number) {
|
||||||
|
this.employee_id = employee_id;
|
||||||
|
this.banked_hours = 0;
|
||||||
|
this.sick_hours = 0;
|
||||||
|
this.vacation_hours = 0;
|
||||||
|
this.last_updated = new Date().toISOString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -55,14 +55,6 @@ export class SchedulePresetsCreateService {
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prisma.$transaction(async (tx) => {
|
await this.prisma.$transaction(async (tx) => {
|
||||||
//check if employee chose this preset has a default preset and ensure all others are false
|
|
||||||
// if (dto.is_default) {
|
|
||||||
// await tx.schedulePresets.updateMany({
|
|
||||||
// where: { is_default: true },
|
|
||||||
// data: { is_default: false },
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
await tx.schedulePresets.create({
|
await tx.schedulePresets.create({
|
||||||
data: {
|
data: {
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
|
||||||
import { ShiftsArchive } from "@prisma/client";
|
|
||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* _____________________________________________________________________________________
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* This service is not used. Will be use to atrchive a list of shifts using a cron job.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* _____________________________________________________________________________________
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
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 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -179,12 +179,4 @@ export class ShiftsCreateService {
|
||||||
|
|
||||||
return { success: true, data: { date, start_time, end_time, bank_code_id: bank_code_id.data } };
|
return { success: true, data: { date, start_time, end_time, bank_code_id: bank_code_id.data } };
|
||||||
}
|
}
|
||||||
|
|
||||||
private addHourstoDateString = (start_time: string, hours: number): string => {
|
|
||||||
const start = toDateFromHHmm(start_time);
|
|
||||||
const end = new Date(start.getTime() + hours * 60 * 60 * 1000);
|
|
||||||
const hh = String(end.getUTCHours()).padStart(2, '0');
|
|
||||||
const mm = String(end.getUTCMinutes()).padStart(2, '0');
|
|
||||||
return `${hh}:${mm}:00`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
|
|
||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* _____________________________________________________________________________________
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* This service is not used. Could be use to show a list of shifts.
|
|
||||||
*
|
|
||||||
* For the moment, the module Timesheets is used to display shifts, filtered by employee
|
|
||||||
*
|
|
||||||
* _____________________________________________________________________________________
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ShiftsGetService {
|
|
||||||
constructor(
|
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
){}
|
|
||||||
|
|
||||||
//fetch a shift using shift_id and return all that shift's info
|
|
||||||
// async getShiftByShiftId(shift_ids: number[]): Promise<GetShiftDto[]> {
|
|
||||||
// if(!Array.isArray(shift_ids) || shift_ids.length === 0) return [];
|
|
||||||
|
|
||||||
// const rows = await this.prisma.shifts.findMany({
|
|
||||||
// where: { id: { in: shift_ids } },
|
|
||||||
// select: shift_select,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if(rows.length !== shift_ids.length) {
|
|
||||||
// const found_ids = new Set(rows.map(row => row.id));
|
|
||||||
// const missing_ids = shift_ids.filter(id => !found_ids.has(id));
|
|
||||||
// throw new NotFoundException(`Shift(s) not found: ${ missing_ids.join(", ")}`);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const row_by_id = new Map(rows.map(row => [row.id, row]));
|
|
||||||
|
|
||||||
// return shift_ids.map((id) => {
|
|
||||||
// const shift = row_by_id.get(id)!;
|
|
||||||
// return {
|
|
||||||
// timesheet_id: shift.timesheet_id,
|
|
||||||
// type: shift.bank_code.type,
|
|
||||||
// date: toStringFromDate(shift.date),
|
|
||||||
// start_time: toStringFromHHmm(shift.start_time),
|
|
||||||
// end_time: toStringFromHHmm(shift.end_time),
|
|
||||||
// is_remote: shift.is_remote,
|
|
||||||
// is_approved: shift.is_approved,
|
|
||||||
// comment: shift.comment ?? undefined,
|
|
||||||
// } satisfies GetShiftDto;
|
|
||||||
// });
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { ShiftsUpdateService } from 'src/time-and-attendance/shifts/services/shi
|
||||||
import { VacationService } from 'src/time-and-attendance/domains/services/vacation.service';
|
import { VacationService } from 'src/time-and-attendance/domains/services/vacation.service';
|
||||||
import { BankedHoursService } from 'src/time-and-attendance/domains/services/banking-hours.service';
|
import { BankedHoursService } from 'src/time-and-attendance/domains/services/banking-hours.service';
|
||||||
import { PaidTimeOffModule } from 'src/time-and-attendance/paid-time-off/paid-time-off.module';
|
import { PaidTimeOffModule } from 'src/time-and-attendance/paid-time-off/paid-time-off.module';
|
||||||
import { ShiftsGetService } from 'src/time-and-attendance/shifts/services/shifts-get.service';
|
|
||||||
import { PaidTimeOFfBankHoursService } from 'src/time-and-attendance/paid-time-off/paid-time-off.service';
|
import { PaidTimeOFfBankHoursService } from 'src/time-and-attendance/paid-time-off/paid-time-off.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|
@ -26,7 +25,6 @@ import { PaidTimeOFfBankHoursService } from 'src/time-and-attendance/paid-time-o
|
||||||
ShiftsCreateService,
|
ShiftsCreateService,
|
||||||
ShiftsUpdateService,
|
ShiftsUpdateService,
|
||||||
ShiftsDeleteService,
|
ShiftsDeleteService,
|
||||||
ShiftsGetService,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ShiftsModule { }
|
export class ShiftsModule { }
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
|
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module";
|
import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module";
|
||||||
|
import { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
|
||||||
|
import { BankedHoursService } from "src/time-and-attendance/domains/services/banking-hours.service";
|
||||||
|
import { PaidTimeOffModule } from "src/time-and-attendance/paid-time-off/paid-time-off.module";
|
||||||
|
import { PaidTimeOFfBankHoursService } from "src/time-and-attendance/paid-time-off/paid-time-off.service";
|
||||||
|
|
||||||
import { ExpenseController } from "src/time-and-attendance/expenses/expense.controller";
|
import { ExpenseController } from "src/time-and-attendance/expenses/expense.controller";
|
||||||
import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service";
|
import { ExpenseCreateService } from "src/time-and-attendance/expenses/services/expense-create.service";
|
||||||
|
import { ExpenseUpdateService } from "src/time-and-attendance/expenses/services/expense-update.service";
|
||||||
|
import { ExpenseDeleteService } from "src/time-and-attendance/expenses/services/expense-delete.service";
|
||||||
import { ExpensesModule } from "src/time-and-attendance/expenses/expenses.module";
|
import { ExpensesModule } from "src/time-and-attendance/expenses/expenses.module";
|
||||||
|
|
||||||
import { TimesheetController } from "src/time-and-attendance/timesheets/timesheet.controller";
|
import { TimesheetController } from "src/time-and-attendance/timesheets/timesheet.controller";
|
||||||
import { TimesheetApprovalService } from "src/time-and-attendance/timesheets/services/timesheet-approval.service";
|
import { TimesheetApprovalService } from "src/time-and-attendance/timesheets/services/timesheet-approval.service";
|
||||||
import { GetTimesheetsOverviewService } from "src/time-and-attendance/timesheets/services/timesheet-get-overview.service";
|
import { GetTimesheetsOverviewService } from "src/time-and-attendance/timesheets/services/timesheet-employee-overview.service";
|
||||||
import { TimesheetsModule } from "src/time-and-attendance/timesheets/timesheets.module";
|
import { TimesheetsModule } from "src/time-and-attendance/timesheets/timesheets.module";
|
||||||
|
|
||||||
import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper";
|
import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper";
|
||||||
|
|
@ -23,11 +28,11 @@ import { PayPeriodsCommandService } from "src/time-and-attendance/pay-period/ser
|
||||||
|
|
||||||
import { CsvExportModule } from "src/time-and-attendance/exports/csv-exports.module";
|
import { CsvExportModule } from "src/time-and-attendance/exports/csv-exports.module";
|
||||||
import { CsvExportService } from "src/time-and-attendance/exports/services/csv-exports.service";
|
import { CsvExportService } from "src/time-and-attendance/exports/services/csv-exports.service";
|
||||||
|
import { CsvGeneratorService } from "src/time-and-attendance/exports/services/csv-builder.service";
|
||||||
import { CsvExportController } from "src/time-and-attendance/exports/csv-exports.controller";
|
import { CsvExportController } from "src/time-and-attendance/exports/csv-exports.controller";
|
||||||
|
|
||||||
import { ShiftController } from "src/time-and-attendance/shifts/shift.controller";
|
import { ShiftController } from "src/time-and-attendance/shifts/shift.controller";
|
||||||
import { ShiftsCreateService } from "src/time-and-attendance/shifts/services/shifts-create.service";
|
import { ShiftsCreateService } from "src/time-and-attendance/shifts/services/shifts-create.service";
|
||||||
import { ShiftsGetService } from "src/time-and-attendance/shifts/services/shifts-get.service";
|
|
||||||
import { ShiftsUpdateService } from "src/time-and-attendance/shifts/services/shifts-update.service";
|
import { ShiftsUpdateService } from "src/time-and-attendance/shifts/services/shifts-update.service";
|
||||||
import { ShiftsDeleteService } from "src/time-and-attendance/shifts/services/shifts-delete.service";
|
import { ShiftsDeleteService } from "src/time-and-attendance/shifts/services/shifts-delete.service";
|
||||||
|
|
||||||
|
|
@ -38,11 +43,7 @@ import { SchedulePresetDeleteService } from "src/time-and-attendance/schedule-pr
|
||||||
import { SchedulePresetUpdateService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-update.service";
|
import { SchedulePresetUpdateService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-update.service";
|
||||||
import { SchedulePresetsCreateService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-create.service";
|
import { SchedulePresetsCreateService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-create.service";
|
||||||
import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service";
|
import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service";
|
||||||
import { CsvGeneratorService } from "src/time-and-attendance/exports/services/csv-builder.service";
|
|
||||||
import { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
|
|
||||||
import { BankedHoursService } from "src/time-and-attendance/domains/services/banking-hours.service";
|
|
||||||
import { PaidTimeOffModule } from "src/time-and-attendance/paid-time-off/paid-time-off.module";
|
|
||||||
import { PaidTimeOFfBankHoursService } from "src/time-and-attendance/paid-time-off/paid-time-off.service";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -66,11 +67,12 @@ import { PaidTimeOFfBankHoursService } from "src/time-and-attendance/paid-time-o
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
GetTimesheetsOverviewService,
|
GetTimesheetsOverviewService,
|
||||||
ShiftsGetService,
|
|
||||||
ShiftsCreateService,
|
ShiftsCreateService,
|
||||||
ShiftsUpdateService,
|
ShiftsUpdateService,
|
||||||
ShiftsDeleteService,
|
ShiftsDeleteService,
|
||||||
ExpenseUpsertService,
|
ExpenseCreateService,
|
||||||
|
ExpenseUpdateService,
|
||||||
|
ExpenseDeleteService,
|
||||||
SchedulePresetsGetService,
|
SchedulePresetsGetService,
|
||||||
SchedulePresetDeleteService,
|
SchedulePresetDeleteService,
|
||||||
SchedulePresetsApplyService,
|
SchedulePresetsApplyService,
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
// 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 } });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
|
||||||
|
import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/common/utils/constants.utils";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
|
||||||
|
import { Timesheet, Timesheets } from "src/time-and-attendance/timesheets/timesheet.dto";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { toDateFromString, sevenDaysFrom, toStringFromDate, toHHmmFromDate } from "src/common/utils/date-utils";
|
||||||
|
import { mapOneTimesheet } from "src/time-and-attendance/timesheets/timesheet.mapper";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetTimesheetsOverviewService {
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly emailResolver: EmailToIdResolver,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------------
|
||||||
|
// GET TIMESHEETS FOR A SELECTED EMPLOYEE
|
||||||
|
//-----------------------------------------------------------------------------------
|
||||||
|
async getTimesheetsForEmployeeByPeriod(email: string, pay_year: number, pay_period_no: number, employee_email?: string): Promise<Result<Timesheets, string>> {
|
||||||
|
try {
|
||||||
|
const account_email = employee_email ?? email;
|
||||||
|
|
||||||
|
//find period using year and period_no
|
||||||
|
const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no } });
|
||||||
|
if (!period) return { success: false, error: `PAY_PERIOD_NOT_FOUND` };
|
||||||
|
|
||||||
|
//fetch the employee_id using the email
|
||||||
|
const employee_id = await this.emailResolver.findIdByEmail(account_email);
|
||||||
|
if (!employee_id.success) return { success: false, error: employee_id.error }
|
||||||
|
|
||||||
|
//loads the timesheets related to the fetched pay-period
|
||||||
|
let rows = await this.loadTimesheets(employee_id.data, period.period_start, period.period_end);
|
||||||
|
|
||||||
|
//Normalized dates from pay-period strings
|
||||||
|
const normalized_start = toDateFromString(period.period_start);
|
||||||
|
const normalized_end = toDateFromString(period.period_end);
|
||||||
|
|
||||||
|
//creates empty timesheet to make sure to return desired amount of timesheet
|
||||||
|
for (let i = 0; i < NUMBER_OF_TIMESHEETS_TO_RETURN; i++) {
|
||||||
|
const week_start = new Date(normalized_start);
|
||||||
|
week_start.setUTCDate(week_start.getUTCDate() + i * 7);
|
||||||
|
|
||||||
|
if (week_start.getTime() > normalized_end.getTime()) break;
|
||||||
|
|
||||||
|
const has_existing_timesheets = rows.some(
|
||||||
|
(row) => toDateFromString(row.start_date).getTime() === week_start.getTime()
|
||||||
|
);
|
||||||
|
if (!has_existing_timesheets) await this.ensureTimesheet(employee_id.data, week_start);
|
||||||
|
}
|
||||||
|
rows = await this.loadTimesheets(employee_id.data, period.period_start, period.period_end);
|
||||||
|
|
||||||
|
//find user infos using the employee_id
|
||||||
|
const employee = await this.prisma.employees.findUnique({
|
||||||
|
where: { id: employee_id.data },
|
||||||
|
include: { schedule_preset: true, user: true },
|
||||||
|
});
|
||||||
|
if (!employee) return { success: false, error: `EMPLOYEE_NOT_FOUND` }
|
||||||
|
|
||||||
|
//builds employee details
|
||||||
|
const has_preset_schedule = employee.schedule_preset !== null;
|
||||||
|
const user = employee.user;
|
||||||
|
const employee_fullname = `${user.first_name} ${user.last_name}`.trim();
|
||||||
|
|
||||||
|
|
||||||
|
//maps all timesheet's infos
|
||||||
|
const timesheets = await Promise.all(rows.map((timesheet) => mapOneTimesheet(timesheet)));
|
||||||
|
if (!timesheets) return { success: false, error: 'INVALID_TIMESHEET' }
|
||||||
|
|
||||||
|
return { success: true, data: { has_preset_schedule, employee_fullname, timesheets } };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: 'TIMESHEET_NOT_FOUND' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------------
|
||||||
|
// MAPPERS & HELPERS
|
||||||
|
//-----------------------------------------------------------------------------------
|
||||||
|
//fetch timesheet's infos
|
||||||
|
private async loadTimesheets(employee_id: number, period_start: Date, period_end: Date) {
|
||||||
|
return this.prisma.timesheets.findMany({
|
||||||
|
where: { employee_id, start_date: { gte: period_start, lte: period_end } },
|
||||||
|
include: {
|
||||||
|
employee: { include: { user: true } },
|
||||||
|
shift: { include: { bank_code: true }, orderBy: { start_time: 'asc' } },
|
||||||
|
expense: { include: { bank_code: true, attachment_record: true }, orderBy: [{ date: 'asc' }, { bank_code_id: 'desc' }] },
|
||||||
|
},
|
||||||
|
orderBy: { start_date: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureTimesheet = async (employee_id: number, start_date: Date | string) => {
|
||||||
|
const start = toDateFromString(start_date);
|
||||||
|
|
||||||
|
let row = await this.prisma.timesheets.findFirst({
|
||||||
|
where: { employee_id, start_date: start },
|
||||||
|
include: {
|
||||||
|
employee: { include: { user: true } },
|
||||||
|
shift: { include: { bank_code: true } },
|
||||||
|
expense: { include: { bank_code: true, attachment_record: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (row) return row;
|
||||||
|
|
||||||
|
await this.prisma.timesheets.create({
|
||||||
|
data: {
|
||||||
|
employee_id,
|
||||||
|
start_date: start,
|
||||||
|
is_approved: false
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
row = await this.prisma.timesheets.findFirst({
|
||||||
|
where: { employee_id, start_date: start },
|
||||||
|
include: {
|
||||||
|
employee: { include: { user: true } },
|
||||||
|
shift: { include: { bank_code: true } },
|
||||||
|
expense: { include: { bank_code: true, attachment_record: true, } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return row!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,290 +0,0 @@
|
||||||
|
|
||||||
import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/common/utils/constants.utils";
|
|
||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
|
||||||
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
|
|
||||||
import { Timesheet, Timesheets } from "src/time-and-attendance/timesheets/timesheet.dto";
|
|
||||||
import { Result } from "src/common/errors/result-error.factory";
|
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
import { toDateFromString, sevenDaysFrom, toStringFromDate, toHHmmFromDate } from "src/common/utils/date-utils";
|
|
||||||
|
|
||||||
export type TotalHours = {
|
|
||||||
regular: number;
|
|
||||||
evening: number;
|
|
||||||
emergency: number;
|
|
||||||
overtime: number;
|
|
||||||
vacation: number;
|
|
||||||
holiday: number;
|
|
||||||
sick: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TotalExpenses = {
|
|
||||||
expenses: number;
|
|
||||||
per_diem: number;
|
|
||||||
on_call: number;
|
|
||||||
mileage: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class GetTimesheetsOverviewService {
|
|
||||||
constructor(
|
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly emailResolver: EmailToIdResolver,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------------
|
|
||||||
// GET TIMESHEETS FOR A SELECTED EMPLOYEE
|
|
||||||
//-----------------------------------------------------------------------------------
|
|
||||||
async getTimesheetsForEmployeeByPeriod(email: string, pay_year: number, pay_period_no: number, employee_email?: string): Promise<Result<Timesheets, string>> {
|
|
||||||
try {
|
|
||||||
const account_email = employee_email ?? email;
|
|
||||||
|
|
||||||
//find period using year and period_no
|
|
||||||
const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no } });
|
|
||||||
if (!period) return { success: false, error: `PAY_PERIOD_NOT_FOUND` };
|
|
||||||
|
|
||||||
//fetch the employee_id using the email
|
|
||||||
const employee_id = await this.emailResolver.findIdByEmail(account_email);
|
|
||||||
if (!employee_id.success) return { success: false, error: employee_id.error }
|
|
||||||
|
|
||||||
//loads the timesheets related to the fetched pay-period
|
|
||||||
let rows = await this.loadTimesheets(employee_id.data, period.period_start, period.period_end);
|
|
||||||
|
|
||||||
//Normalized dates from pay-period strings
|
|
||||||
const normalized_start = toDateFromString(period.period_start);
|
|
||||||
const normalized_end = toDateFromString(period.period_end);
|
|
||||||
|
|
||||||
//creates empty timesheet to make sure to return desired amount of timesheet
|
|
||||||
for (let i = 0; i < NUMBER_OF_TIMESHEETS_TO_RETURN; i++) {
|
|
||||||
const week_start = new Date(normalized_start);
|
|
||||||
week_start.setUTCDate(week_start.getUTCDate() + i * 7);
|
|
||||||
|
|
||||||
if (week_start.getTime() > normalized_end.getTime()) break;
|
|
||||||
|
|
||||||
const has_existing_timesheets = rows.some(
|
|
||||||
(row) => toDateFromString(row.start_date).getTime() === week_start.getTime()
|
|
||||||
);
|
|
||||||
if (!has_existing_timesheets) await this.ensureTimesheet(employee_id.data, week_start);
|
|
||||||
}
|
|
||||||
rows = await this.loadTimesheets(employee_id.data, period.period_start, period.period_end);
|
|
||||||
|
|
||||||
//find user infos using the employee_id
|
|
||||||
const employee = await this.prisma.employees.findUnique({
|
|
||||||
where: { id: employee_id.data },
|
|
||||||
include: { schedule_preset: true, user: true },
|
|
||||||
});
|
|
||||||
if (!employee) return { success: false, error: `EMPLOYEE_NOT_FOUND` }
|
|
||||||
|
|
||||||
//builds employee details
|
|
||||||
const has_preset_schedule = employee.schedule_preset !== null;
|
|
||||||
const user = employee.user;
|
|
||||||
const employee_fullname = `${user.first_name} ${user.last_name}`.trim();
|
|
||||||
|
|
||||||
|
|
||||||
//maps all timesheet's infos
|
|
||||||
const timesheets = await Promise.all(rows.map((timesheet) => this.mapOneTimesheet(timesheet)));
|
|
||||||
if (!timesheets) return { success: false, error: 'INVALID_TIMESHEET' }
|
|
||||||
|
|
||||||
return { success: true, data: { has_preset_schedule, employee_fullname, timesheets } };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: 'TIMESHEET_NOT_FOUND' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------------
|
|
||||||
// MAPPERS & HELPERS
|
|
||||||
//-----------------------------------------------------------------------------------
|
|
||||||
//fetch timesheet's infos
|
|
||||||
private async loadTimesheets(employee_id: number, period_start: Date, period_end: Date) {
|
|
||||||
return this.prisma.timesheets.findMany({
|
|
||||||
where: { employee_id, start_date: { gte: period_start, lte: period_end } },
|
|
||||||
include: {
|
|
||||||
employee: { include: { user: true } },
|
|
||||||
shift: { include: { bank_code: true }, orderBy: { start_time: 'asc' } },
|
|
||||||
expense: { include: { bank_code: true, attachment_record: true }, orderBy: [{ date: 'asc' }, { bank_code_id: 'desc' }] },
|
|
||||||
},
|
|
||||||
orderBy: { start_date: 'asc' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async mapOneTimesheet(timesheet: Prisma.TimesheetsGetPayload<{
|
|
||||||
include: {
|
|
||||||
employee: { include: { user } },
|
|
||||||
shift: { include: { bank_code }, orderBy: { start_time: 'asc' } },
|
|
||||||
expense: { include: { bank_code, attachment_record } },
|
|
||||||
}
|
|
||||||
}>): Promise<Timesheet> {
|
|
||||||
//converts string to UTC date format
|
|
||||||
const start = toDateFromString(timesheet.start_date);
|
|
||||||
const day_dates = sevenDaysFrom(start);
|
|
||||||
|
|
||||||
//map of shifts by days
|
|
||||||
const shifts_by_date = new Map<string, Prisma.ShiftsGetPayload<{ include: { bank_code }, orderBy: { start_time: 'asc' } }>[]>();
|
|
||||||
for (const shift of timesheet.shift) {
|
|
||||||
const date_string = toStringFromDate(shift.date);
|
|
||||||
const arr = shifts_by_date.get(date_string) ?? [];
|
|
||||||
arr.push(shift);
|
|
||||||
shifts_by_date.set(date_string, arr);
|
|
||||||
}
|
|
||||||
//map of expenses by days
|
|
||||||
const expenses_by_date = new Map<string, Prisma.ExpensesGetPayload<{ include: { bank_code: {}, attachment_record } }>[]>();
|
|
||||||
for (const expense of timesheet.expense) {
|
|
||||||
const date_string = toStringFromDate(expense.date);
|
|
||||||
const arr = expenses_by_date.get(date_string) ?? [];
|
|
||||||
arr.push(expense);
|
|
||||||
expenses_by_date.set(date_string, arr);
|
|
||||||
}
|
|
||||||
//weekly totals
|
|
||||||
const weekly_hours: TotalHours = emptyHours();
|
|
||||||
const weekly_expenses: TotalExpenses = emptyExpenses();
|
|
||||||
|
|
||||||
//map of days
|
|
||||||
const days = day_dates.map((date) => {
|
|
||||||
const date_iso = toStringFromDate(date);
|
|
||||||
const shifts_source = shifts_by_date.get(date_iso) ?? [];
|
|
||||||
const expenses_source = expenses_by_date.get(date_iso) ?? [];
|
|
||||||
|
|
||||||
//inner map of shifts
|
|
||||||
const shifts = shifts_source.map((shift) => ({
|
|
||||||
timesheet_id: shift.timesheet_id,
|
|
||||||
date: toStringFromDate(shift.date),
|
|
||||||
start_time: toHHmmFromDate(shift.start_time),
|
|
||||||
end_time: toHHmmFromDate(shift.end_time),
|
|
||||||
type: shift.bank_code?.type ?? '',
|
|
||||||
is_remote: shift.is_remote ?? false,
|
|
||||||
is_approved: shift.is_approved ?? false,
|
|
||||||
id: shift.id ?? null,
|
|
||||||
comment: shift.comment ?? null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
//inner map of expenses
|
|
||||||
const expenses = expenses_source.map((expense) => ({
|
|
||||||
date: toStringFromDate(expense.date),
|
|
||||||
amount: expense.amount != null ? Number(expense.amount) : undefined,
|
|
||||||
mileage: expense.mileage != null ? Number(expense.mileage) : undefined,
|
|
||||||
id: expense.id ?? null,
|
|
||||||
timesheet_id: expense.timesheet_id,
|
|
||||||
attachment: expense.attachment_record ? String(expense.attachment_record.id) : undefined,
|
|
||||||
is_approved: expense.is_approved ?? false,
|
|
||||||
comment: expense.comment ?? '',
|
|
||||||
supervisor_comment: expense.supervisor_comment,
|
|
||||||
type: expense.bank_code.type,
|
|
||||||
}));
|
|
||||||
|
|
||||||
//daily totals
|
|
||||||
const daily_hours = emptyHours();
|
|
||||||
const daily_expenses = emptyExpenses();
|
|
||||||
|
|
||||||
//totals by shift types
|
|
||||||
for (const shift of shifts_source) {
|
|
||||||
const hours = diffOfHours(shift.start_time, shift.end_time);
|
|
||||||
const subgroup = hoursSubGroupFromBankCode(shift.bank_code);
|
|
||||||
daily_hours[subgroup] += hours;
|
|
||||||
weekly_hours[subgroup] += hours;
|
|
||||||
}
|
|
||||||
|
|
||||||
//totals by expense types
|
|
||||||
for (const expense of expenses_source) {
|
|
||||||
const subgroup = expenseSubgroupFromBankCode(expense.bank_code);
|
|
||||||
if (subgroup === 'mileage') {
|
|
||||||
const mileage = num(expense.mileage);
|
|
||||||
daily_expenses.mileage += mileage;
|
|
||||||
weekly_expenses.mileage += mileage;
|
|
||||||
} else if (subgroup === 'per_diem') {
|
|
||||||
const amount = num(expense.amount);
|
|
||||||
daily_expenses.per_diem += amount;
|
|
||||||
weekly_expenses.per_diem += amount;
|
|
||||||
} else if (subgroup === 'on_call') {
|
|
||||||
const amount = num(expense.amount);
|
|
||||||
daily_expenses.on_call += amount;
|
|
||||||
weekly_expenses.on_call += amount;
|
|
||||||
} else {
|
|
||||||
const amount = num(expense.amount);
|
|
||||||
daily_expenses.expenses += amount;
|
|
||||||
weekly_expenses.expenses += amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
date: date_iso,
|
|
||||||
shifts,
|
|
||||||
expenses,
|
|
||||||
daily_hours,
|
|
||||||
daily_expenses,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
timesheet_id: timesheet.id,
|
|
||||||
is_approved: timesheet.is_approved ?? false,
|
|
||||||
days,
|
|
||||||
weekly_hours,
|
|
||||||
weekly_expenses,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensureTimesheet = async (employee_id: number, start_date: Date | string) => {
|
|
||||||
const start = toDateFromString(start_date);
|
|
||||||
|
|
||||||
let row = await this.prisma.timesheets.findFirst({
|
|
||||||
where: { employee_id, start_date: start },
|
|
||||||
include: {
|
|
||||||
employee: { include: { user: true } },
|
|
||||||
shift: { include: { bank_code: true } },
|
|
||||||
expense: { include: { bank_code: true, attachment_record: true } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (row) return row;
|
|
||||||
|
|
||||||
await this.prisma.timesheets.create({
|
|
||||||
data: {
|
|
||||||
employee_id,
|
|
||||||
start_date: start,
|
|
||||||
is_approved: false
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
row = await this.prisma.timesheets.findFirst({
|
|
||||||
where: { employee_id, start_date: start },
|
|
||||||
include: {
|
|
||||||
employee: { include: { user: true } },
|
|
||||||
shift: { include: { bank_code: true } },
|
|
||||||
expense: { include: { bank_code: true, attachment_record: true, } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return row!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//filled array with default values
|
|
||||||
const emptyHours = (): TotalHours => { return { regular: 0, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0 } };
|
|
||||||
const emptyExpenses = (): TotalExpenses => { return { expenses: 0, per_diem: 0, on_call: 0, mileage: 0 } };
|
|
||||||
|
|
||||||
//calculate the differences of hours
|
|
||||||
const diffOfHours = (a: Date, b: Date): number => {
|
|
||||||
const ms = new Date(b).getTime() - new Date(a).getTime();
|
|
||||||
return Math.max(0, Math.round((ms / 36e5) * 1000) / 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
//validate numeric values
|
|
||||||
const num = (value: any): number => { return value ? Number(value) : 0 };
|
|
||||||
|
|
||||||
// shift's subgroup types
|
|
||||||
const hoursSubGroupFromBankCode = (bank_code: any): keyof TotalHours => {
|
|
||||||
const type = bank_code.type;
|
|
||||||
if (type.includes('EVENING')) return 'evening';
|
|
||||||
if (type.includes('EMERGENCY')) return 'emergency';
|
|
||||||
if (type.includes('OVERTIME')) return 'overtime';
|
|
||||||
if (type.includes('VACATION')) return 'vacation';
|
|
||||||
if (type.includes('HOLIDAY')) return 'holiday';
|
|
||||||
if (type.includes('SICK')) return 'sick';
|
|
||||||
return 'regular'
|
|
||||||
}
|
|
||||||
|
|
||||||
// expense's subgroup types
|
|
||||||
const expenseSubgroupFromBankCode = (bank_code: any): keyof TotalExpenses => {
|
|
||||||
const type = bank_code.type;
|
|
||||||
if (type.includes('MILEAGE')) return 'mileage';
|
|
||||||
if (type.includes('PER_DIEM')) return 'per_diem';
|
|
||||||
if (type.includes('ON_CALL')) return 'on_call';
|
|
||||||
return 'expenses';
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Body, Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query, Req, UnauthorizedException } from "@nestjs/common";
|
import { Body, Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query, Req, UnauthorizedException } from "@nestjs/common";
|
||||||
import { GetTimesheetsOverviewService } from "src/time-and-attendance/timesheets/services/timesheet-get-overview.service";
|
import { GetTimesheetsOverviewService } from "src/time-and-attendance/timesheets/services/timesheet-employee-overview.service";
|
||||||
import { TimesheetApprovalService } from "src/time-and-attendance/timesheets/services/timesheet-approval.service";
|
import { TimesheetApprovalService } from "src/time-and-attendance/timesheets/services/timesheet-approval.service";
|
||||||
import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
|
import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
|
||||||
import { Modules as ModulesEnum } from ".prisma/client";
|
import { Modules as ModulesEnum } from ".prisma/client";
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ export class TotalHours {
|
||||||
@Type(() => Number) vacation: number;
|
@Type(() => Number) vacation: number;
|
||||||
@Type(() => Number) holiday: number;
|
@Type(() => Number) holiday: number;
|
||||||
@Type(() => Number) sick: number;
|
@Type(() => Number) sick: number;
|
||||||
|
@Type(() => Number) banking: number;
|
||||||
|
@Type(() => Number) withdraw_banked: number;
|
||||||
}
|
}
|
||||||
export class TotalExpenses {
|
export class TotalExpenses {
|
||||||
@Type(() => Number) expenses: number;
|
@Type(() => Number) expenses: number;
|
||||||
|
|
@ -69,3 +71,4 @@ export class Expense {
|
||||||
@IsOptional() @IsInt() id?: number | null;
|
@IsOptional() @IsInt() id?: number | null;
|
||||||
@IsString() @IsOptional() supervisor_comment?: string | null;
|
@IsString() @IsOptional() supervisor_comment?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
174
src/time-and-attendance/timesheets/timesheet.mapper.ts
Normal file
174
src/time-and-attendance/timesheets/timesheet.mapper.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { toDateFromString, sevenDaysFrom, toStringFromDate, toHHmmFromDate } from "src/common/utils/date-utils";
|
||||||
|
import { Timesheet } from "src/time-and-attendance/timesheets/timesheet.dto";
|
||||||
|
|
||||||
|
export const mapOneTimesheet = async (timesheet: Prisma.TimesheetsGetPayload<{
|
||||||
|
include: {
|
||||||
|
employee: { include: { user } },
|
||||||
|
shift: { include: { bank_code }, orderBy: { start_time: 'asc' } },
|
||||||
|
expense: { include: { bank_code, attachment_record } },
|
||||||
|
}
|
||||||
|
}>): Promise<Timesheet> => {
|
||||||
|
//converts string to UTC date format
|
||||||
|
const start = toDateFromString(timesheet.start_date);
|
||||||
|
const day_dates = sevenDaysFrom(start);
|
||||||
|
|
||||||
|
//map of shifts by days
|
||||||
|
const shifts_by_date = new Map<string, Prisma.ShiftsGetPayload<{ include: { bank_code }, orderBy: { start_time: 'asc' } }>[]>();
|
||||||
|
for (const shift of timesheet.shift) {
|
||||||
|
const date_string = toStringFromDate(shift.date);
|
||||||
|
const arr = shifts_by_date.get(date_string) ?? [];
|
||||||
|
arr.push(shift);
|
||||||
|
shifts_by_date.set(date_string, arr);
|
||||||
|
}
|
||||||
|
//map of expenses by days
|
||||||
|
const expenses_by_date = new Map<string, Prisma.ExpensesGetPayload<{ include: { bank_code: {}, attachment_record } }>[]>();
|
||||||
|
for (const expense of timesheet.expense) {
|
||||||
|
const date_string = toStringFromDate(expense.date);
|
||||||
|
const arr = expenses_by_date.get(date_string) ?? [];
|
||||||
|
arr.push(expense);
|
||||||
|
expenses_by_date.set(date_string, arr);
|
||||||
|
}
|
||||||
|
//weekly totals
|
||||||
|
const weekly_hours: TotalHours = emptyHours();
|
||||||
|
const weekly_expenses: TotalExpenses = emptyExpenses();
|
||||||
|
|
||||||
|
//map of days
|
||||||
|
const days = day_dates.map((date) => {
|
||||||
|
const date_iso = toStringFromDate(date);
|
||||||
|
const shifts_source = shifts_by_date.get(date_iso) ?? [];
|
||||||
|
const expenses_source = expenses_by_date.get(date_iso) ?? [];
|
||||||
|
|
||||||
|
//inner map of shifts
|
||||||
|
const shifts = shifts_source.map((shift) => ({
|
||||||
|
timesheet_id: shift.timesheet_id,
|
||||||
|
date: toStringFromDate(shift.date),
|
||||||
|
start_time: toHHmmFromDate(shift.start_time),
|
||||||
|
end_time: toHHmmFromDate(shift.end_time),
|
||||||
|
type: shift.bank_code?.type ?? '',
|
||||||
|
is_remote: shift.is_remote ?? false,
|
||||||
|
is_approved: shift.is_approved ?? false,
|
||||||
|
id: shift.id ?? null,
|
||||||
|
comment: shift.comment ?? null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
//inner map of expenses
|
||||||
|
const expenses = expenses_source.map((expense) => ({
|
||||||
|
date: toStringFromDate(expense.date),
|
||||||
|
amount: expense.amount != null ? Number(expense.amount) : undefined,
|
||||||
|
mileage: expense.mileage != null ? Number(expense.mileage) : undefined,
|
||||||
|
id: expense.id ?? null,
|
||||||
|
timesheet_id: expense.timesheet_id,
|
||||||
|
attachment: expense.attachment_record ? String(expense.attachment_record.id) : undefined,
|
||||||
|
is_approved: expense.is_approved ?? false,
|
||||||
|
comment: expense.comment ?? '',
|
||||||
|
supervisor_comment: expense.supervisor_comment,
|
||||||
|
type: expense.bank_code.type,
|
||||||
|
}));
|
||||||
|
|
||||||
|
//daily totals
|
||||||
|
const daily_hours = emptyHours();
|
||||||
|
const daily_expenses = emptyExpenses();
|
||||||
|
|
||||||
|
//totals by shift types
|
||||||
|
for (const shift of shifts_source) {
|
||||||
|
const hours = diffOfHours(shift.start_time, shift.end_time);
|
||||||
|
const subgroup = hoursSubGroupFromBankCode(shift.bank_code);
|
||||||
|
daily_hours[subgroup] += hours;
|
||||||
|
weekly_hours[subgroup] += hours;
|
||||||
|
}
|
||||||
|
|
||||||
|
//totals by expense types
|
||||||
|
for (const expense of expenses_source) {
|
||||||
|
const subgroup = expenseSubgroupFromBankCode(expense.bank_code);
|
||||||
|
if (subgroup === 'mileage') {
|
||||||
|
const mileage = num(expense.mileage);
|
||||||
|
daily_expenses.mileage += mileage;
|
||||||
|
weekly_expenses.mileage += mileage;
|
||||||
|
} else if (subgroup === 'per_diem') {
|
||||||
|
const amount = num(expense.amount);
|
||||||
|
daily_expenses.per_diem += amount;
|
||||||
|
weekly_expenses.per_diem += amount;
|
||||||
|
} else if (subgroup === 'on_call') {
|
||||||
|
const amount = num(expense.amount);
|
||||||
|
daily_expenses.on_call += amount;
|
||||||
|
weekly_expenses.on_call += amount;
|
||||||
|
} else {
|
||||||
|
const amount = num(expense.amount);
|
||||||
|
daily_expenses.expenses += amount;
|
||||||
|
weekly_expenses.expenses += amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
date: date_iso,
|
||||||
|
shifts,
|
||||||
|
expenses,
|
||||||
|
daily_hours,
|
||||||
|
daily_expenses,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
timesheet_id: timesheet.id,
|
||||||
|
is_approved: timesheet.is_approved ?? false,
|
||||||
|
days,
|
||||||
|
weekly_hours,
|
||||||
|
weekly_expenses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//filled array with default values
|
||||||
|
const emptyHours = (): TotalHours => { return { regular: 0, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0, banking: 0, withdraw_banked: 0 } };
|
||||||
|
const emptyExpenses = (): TotalExpenses => { return { expenses: 0, per_diem: 0, on_call: 0, mileage: 0 } };
|
||||||
|
|
||||||
|
//calculate the differences of hours
|
||||||
|
const diffOfHours = (a: Date, b: Date): number => {
|
||||||
|
const ms = new Date(b).getTime() - new Date(a).getTime();
|
||||||
|
return Math.max(0, Math.round((ms / 36e5) * 1000) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
//validate numeric values
|
||||||
|
const num = (value: any): number => { return value ? Number(value) : 0 };
|
||||||
|
|
||||||
|
// shift's subgroup types
|
||||||
|
const hoursSubGroupFromBankCode = (bank_code: any): keyof TotalHours => {
|
||||||
|
const type = bank_code.type;
|
||||||
|
if (type.includes('EVENING')) return 'evening';
|
||||||
|
if (type.includes('EMERGENCY')) return 'emergency';
|
||||||
|
if (type.includes('OVERTIME')) return 'overtime';
|
||||||
|
if (type.includes('VACATION')) return 'vacation';
|
||||||
|
if (type.includes('HOLIDAY')) return 'holiday';
|
||||||
|
if (type.includes('SICK')) return 'sick';
|
||||||
|
if (type.includes('BANKING')) return 'banking';
|
||||||
|
if (type.includes('WITHDRAW_BANKED')) return 'withdraw_banked'
|
||||||
|
return 'regular'
|
||||||
|
}
|
||||||
|
|
||||||
|
// expense's subgroup types
|
||||||
|
const expenseSubgroupFromBankCode = (bank_code: any): keyof TotalExpenses => {
|
||||||
|
const type = bank_code.type;
|
||||||
|
if (type.includes('MILEAGE')) return 'mileage';
|
||||||
|
if (type.includes('PER_DIEM')) return 'per_diem';
|
||||||
|
if (type.includes('ON_CALL')) return 'on_call';
|
||||||
|
return 'expenses';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TotalHours = {
|
||||||
|
regular: number;
|
||||||
|
evening: number;
|
||||||
|
emergency: number;
|
||||||
|
overtime: number;
|
||||||
|
vacation: number;
|
||||||
|
holiday: number;
|
||||||
|
sick: number;
|
||||||
|
banking: number;
|
||||||
|
withdraw_banked: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TotalExpenses = {
|
||||||
|
expenses: number;
|
||||||
|
per_diem: number;
|
||||||
|
on_call: number;
|
||||||
|
mileage: number;
|
||||||
|
};
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TimesheetController } from 'src/time-and-attendance/timesheets/timesheet.controller';
|
import { TimesheetController } from 'src/time-and-attendance/timesheets/timesheet.controller';
|
||||||
import { TimesheetApprovalService } from 'src/time-and-attendance/timesheets/services/timesheet-approval.service';
|
import { TimesheetApprovalService } from 'src/time-and-attendance/timesheets/services/timesheet-approval.service';
|
||||||
import { GetTimesheetsOverviewService } from 'src/time-and-attendance/timesheets/services/timesheet-get-overview.service';
|
import { GetTimesheetsOverviewService } from 'src/time-and-attendance/timesheets/services/timesheet-employee-overview.service';
|
||||||
import { EmailToIdResolver } from 'src/common/mappers/email-id.mapper';
|
import { EmailToIdResolver } from 'src/common/mappers/email-id.mapper';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export type NormalizedExpense = {
|
||||||
amount?: number | Prisma.Decimal | null;
|
amount?: number | Prisma.Decimal | null;
|
||||||
mileage?: number | Prisma.Decimal | null;
|
mileage?: number | Prisma.Decimal | null;
|
||||||
attachment?: number;
|
attachment?: number;
|
||||||
bank_code_id: number;
|
// bank_code_id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NormalizedLeaveRequest = {
|
export type NormalizedLeaveRequest = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user