From dda15928712153d7b90bde35e013843b3132e58d Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Mon, 12 Jan 2026 09:54:38 -0500 Subject: [PATCH] clean(pre-prod): did some cleaning --- src/common/errors/result-error.factory.ts | 8 - src/common/shared/role-groupes.ts | 20 -- src/common/utils/date-utils.ts | 8 + .../employees/employee-detailed.dto.ts | 2 +- .../services/employees-archival.service.ts | 173 ----------- .../services/employees-update.service.ts | 3 +- ....controller.ts => help-page.controller.ts} | 2 +- ...ome-page.module.ts => help-page.module.ts} | 4 +- ...e-page.service.ts => help-page.service.ts} | 0 .../identity-and-account.module.ts | 6 +- .../bank-codes/bank-codes.service.ts | 13 +- .../domains/paid-time-off.dto.ts | 19 -- .../domains/services/holiday.service.ts | 2 +- .../domains/services/overtime.service.ts | 2 +- .../expenses/expense-create.dto.ts | 2 +- .../expenses/expense.controller.ts | 16 +- .../expenses/expense.utils.ts | 42 +++ .../expenses/expenses.module.ts | 17 +- .../services/expense-create.service.ts | 73 +++++ .../services/expense-delete.service.ts | 29 ++ .../services/expense-update.service.ts | 75 +++++ .../services/expense-upsert.service.ts | 188 ------------ .../services/expenses-archival.service.ts | 62 ---- .../paid-time-off/paid-time-off.dto.ts | 22 +- .../schedule-presets-create.service.ts | 8 - .../services/shifts-archival.service.ts | 71 ----- .../shifts/services/shifts-create.service.ts | 8 - .../shifts/services/shifts-get.service.ts | 58 ---- .../shifts/shifts.module.ts | 2 - .../time-and-attendance.module.ts | 24 +- .../services/timesheet-archive.service.ts | 49 --- .../timesheet-employee-overview.service.ts | 125 ++++++++ .../timesheet-get-overview.service.ts | 290 ------------------ .../timesheets/timesheet.controller.ts | 2 +- .../timesheets/timesheet.dto.ts | 5 +- .../timesheets/timesheet.mapper.ts | 174 +++++++++++ .../timesheets/timesheets.module.ts | 2 +- src/time-and-attendance/utils/type.utils.ts | 2 +- 38 files changed, 614 insertions(+), 994 deletions(-) delete mode 100644 src/common/shared/role-groupes.ts delete mode 100644 src/identity-and-account/employees/services/employees-archival.service.ts rename src/identity-and-account/help/{home-page.controller.ts => help-page.controller.ts} (97%) rename src/identity-and-account/help/{home-page.module.ts => help-page.module.ts} (90%) rename src/identity-and-account/help/{home-page.service.ts => help-page.service.ts} (100%) delete mode 100644 src/time-and-attendance/domains/paid-time-off.dto.ts create mode 100644 src/time-and-attendance/expenses/expense.utils.ts create mode 100644 src/time-and-attendance/expenses/services/expense-create.service.ts create mode 100644 src/time-and-attendance/expenses/services/expense-delete.service.ts create mode 100644 src/time-and-attendance/expenses/services/expense-update.service.ts delete mode 100644 src/time-and-attendance/expenses/services/expense-upsert.service.ts delete mode 100644 src/time-and-attendance/expenses/services/expenses-archival.service.ts delete mode 100644 src/time-and-attendance/shifts/services/shifts-archival.service.ts delete mode 100644 src/time-and-attendance/shifts/services/shifts-get.service.ts delete mode 100644 src/time-and-attendance/timesheets/services/timesheet-archive.service.ts create mode 100644 src/time-and-attendance/timesheets/services/timesheet-employee-overview.service.ts delete mode 100644 src/time-and-attendance/timesheets/services/timesheet-get-overview.service.ts create mode 100644 src/time-and-attendance/timesheets/timesheet.mapper.ts diff --git a/src/common/errors/result-error.factory.ts b/src/common/errors/result-error.factory.ts index 0cf318a..5f7eec6 100644 --- a/src/common/errors/result-error.factory.ts +++ b/src/common/errors/result-error.factory.ts @@ -2,11 +2,3 @@ export type Result = | { success: true; data: T } | { success: false; error: E }; -// const success = (data: T): Result => { -// return { success: true, data }; -// } - -// const failure = (error: E): Result => { -// return { success: false, error }; -// } - diff --git a/src/common/shared/role-groupes.ts b/src/common/shared/role-groupes.ts deleted file mode 100644 index dadeaa9..0000000 --- a/src/common/shared/role-groupes.ts +++ /dev/null @@ -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, -// ] \ No newline at end of file diff --git a/src/common/utils/date-utils.ts b/src/common/utils/date-utils.ts index cdedd53..00f4a3d 100644 --- a/src/common/utils/date-utils.ts +++ b/src/common/utils/date-utils.ts @@ -146,4 +146,12 @@ export const overlaps = (a: { start: Date; end: Date, date?: Date; }, b: { start export const is_same_week_day = (date: Date, week_day: Weekday): boolean => { 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`; } \ No newline at end of file diff --git a/src/identity-and-account/employees/employee-detailed.dto.ts b/src/identity-and-account/employees/employee-detailed.dto.ts index 5441fdb..8b7a252 100644 --- a/src/identity-and-account/employees/employee-detailed.dto.ts +++ b/src/identity-and-account/employees/employee-detailed.dto.ts @@ -1,6 +1,6 @@ import { IsArray, IsBoolean, IsDateString, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator'; 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 { @IsString() @IsNotEmpty() first_name: string; diff --git a/src/identity-and-account/employees/services/employees-archival.service.ts b/src/identity-and-account/employees/services/employees-archival.service.ts deleted file mode 100644 index 2d1235b..0000000 --- a/src/identity-and-account/employees/services/employees-archival.service.ts +++ /dev/null @@ -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 { -// // 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 { -// // 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 { -// // // 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 { -// // return this.prisma.employeesArchive.findMany(); -// // } - -// // //fetches an archived employee -// // async findOneArchived(id: number): Promise { -// // return this.prisma.employeesArchive.findUniqueOrThrow({ where: { id } }); -// // } - -// } - diff --git a/src/identity-and-account/employees/services/employees-update.service.ts b/src/identity-and-account/employees/services/employees-update.service.ts index 19c1504..c5b9fb9 100644 --- a/src/identity-and-account/employees/services/employees-update.service.ts +++ b/src/identity-and-account/employees/services/employees-update.service.ts @@ -8,8 +8,7 @@ import { Result } from "src/common/errors/result-error.factory"; import { toCompanyCodeFromString } from "src/identity-and-account/employees/employee.utils"; import { EmployeeDetailedUpsertDto } from "src/identity-and-account/employees/employee-detailed.dto"; 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() export class EmployeesUpdateService { constructor( diff --git a/src/identity-and-account/help/home-page.controller.ts b/src/identity-and-account/help/help-page.controller.ts similarity index 97% rename from src/identity-and-account/help/home-page.controller.ts rename to src/identity-and-account/help/help-page.controller.ts index 50bbe5f..f3287b9 100644 --- a/src/identity-and-account/help/home-page.controller.ts +++ b/src/identity-and-account/help/help-page.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get } from "@nestjs/common"; 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 { Access } from "src/common/decorators/module-access.decorators"; diff --git a/src/identity-and-account/help/home-page.module.ts b/src/identity-and-account/help/help-page.module.ts similarity index 90% rename from src/identity-and-account/help/home-page.module.ts rename to src/identity-and-account/help/help-page.module.ts index 03b0e98..b1d91e5 100644 --- a/src/identity-and-account/help/home-page.module.ts +++ b/src/identity-and-account/help/help-page.module.ts @@ -1,7 +1,7 @@ import { Module } from "@nestjs/common"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; -import { HomePageController } from "src/identity-and-account/help/home-page.controller"; -import { HomePageService } from "src/identity-and-account/help/home-page.service"; +import { HomePageController } from "src/identity-and-account/help/help-page.controller"; +import { HomePageService } from "src/identity-and-account/help/help-page.service"; @Module({ controllers: [HomePageController], diff --git a/src/identity-and-account/help/home-page.service.ts b/src/identity-and-account/help/help-page.service.ts similarity index 100% rename from src/identity-and-account/help/home-page.service.ts rename to src/identity-and-account/help/help-page.service.ts diff --git a/src/identity-and-account/identity-and-account.module.ts b/src/identity-and-account/identity-and-account.module.ts index 8031a91..6c238b7 100644 --- a/src/identity-and-account/identity-and-account.module.ts +++ b/src/identity-and-account/identity-and-account.module.ts @@ -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 { UsersService } from "src/identity-and-account/users-management/services/users.service"; 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 { EmployeesUpdateService } from "src/identity-and-account/employees/services/employees-update.service"; -import { HomePageController } from "src/identity-and-account/help/home-page.controller"; -import { HomePageService } from "src/identity-and-account/help/home-page.service"; +import { HomePageController } from "src/identity-and-account/help/help-page.controller"; +import { HomePageService } from "src/identity-and-account/help/help-page.service"; @Module({ imports: [ diff --git a/src/time-and-attendance/bank-codes/bank-codes.service.ts b/src/time-and-attendance/bank-codes/bank-codes.service.ts index 387713c..0b130a0 100644 --- a/src/time-and-attendance/bank-codes/bank-codes.service.ts +++ b/src/time-and-attendance/bank-codes/bank-codes.service.ts @@ -17,10 +17,10 @@ export class BankCodesService { bank_code: dto.bank_code, }, }); + return { success: true, data: true }; } catch (error) { return { success: false, error: 'INVALID_BANK_CODE' + error }; } - return { success: true, data: true }; } findAll() { @@ -44,4 +44,15 @@ export class BankCodesService { } } + async delete(id: number): Promise> { + try { + await this.prisma.bankCodes.delete({ + where: { id }, + }); + return { success: true, data: true }; + } catch (error) { + return { success: false, error: 'INVALID_BANK_CODE_ID' + error }; + } + } + } \ No newline at end of file diff --git a/src/time-and-attendance/domains/paid-time-off.dto.ts b/src/time-and-attendance/domains/paid-time-off.dto.ts deleted file mode 100644 index a9f5476..0000000 --- a/src/time-and-attendance/domains/paid-time-off.dto.ts +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/src/time-and-attendance/domains/services/holiday.service.ts b/src/time-and-attendance/domains/services/holiday.service.ts index 8e0f0fd..1f322b7 100644 --- a/src/time-and-attendance/domains/services/holiday.service.ts +++ b/src/time-and-attendance/domains/services/holiday.service.ts @@ -30,7 +30,7 @@ export class HolidayService { const window_start = new Date(holiday_week_start.getTime() - 4 * MS_PER_WEEK); 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({ where: { timesheet: { employee_id: employee_id }, diff --git a/src/time-and-attendance/domains/services/overtime.service.ts b/src/time-and-attendance/domains/services/overtime.service.ts index 45d1da7..71ba5a5 100644 --- a/src/time-and-attendance/domains/services/overtime.service.ts +++ b/src/time-and-attendance/domains/services/overtime.service.ts @@ -27,7 +27,7 @@ type WeekOvertimeSummary = { @Injectable() 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) { } diff --git a/src/time-and-attendance/expenses/expense-create.dto.ts b/src/time-and-attendance/expenses/expense-create.dto.ts index 88b2b94..e27a7cd 100644 --- a/src/time-and-attendance/expenses/expense-create.dto.ts +++ b/src/time-and-attendance/expenses/expense-create.dto.ts @@ -1,5 +1,5 @@ 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 { @IsInt() id: number; diff --git a/src/time-and-attendance/expenses/expense.controller.ts b/src/time-and-attendance/expenses/expense.controller.ts index 418ce19..737228e 100644 --- a/src/time-and-attendance/expenses/expense.controller.ts +++ b/src/time-and-attendance/expenses/expense.controller.ts @@ -1,20 +1,26 @@ 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 { Result } from "src/common/errors/result-error.factory"; import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators"; import { Modules as ModulesEnum } from ".prisma/client"; 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') export class ExpenseController { - constructor(private readonly upsert_service: ExpenseUpsertService) { } + constructor( + private readonly updateService: ExpenseUpdateService, + private readonly createService: ExpenseCreateService, + private readonly deleteService: ExpenseDeleteService, + ) { } @Post('create') @ModuleAccessAllowed(ModulesEnum.timesheets) create(@Access('email') email: string, @Body() dto: ExpenseDto): Promise> { if (!email) throw new UnauthorizedException('Unauthorized User'); - return this.upsert_service.createExpense(dto, email); + return this.createService.createExpense(dto, email); } @Patch('update') @@ -24,13 +30,13 @@ export class ExpenseController { @Access('email') email: string, @Query('employee_email') employee_email?: string, ): Promise> { - return this.upsert_service.updateExpense(dto, email, employee_email); + return this.updateService.updateExpense(dto, email, employee_email); } @Delete('delete/:expense_id') @ModuleAccessAllowed(ModulesEnum.timesheets) remove(@Param('expense_id') expense_id: number): Promise> { - return this.upsert_service.deleteExpense(expense_id); + return this.deleteService.deleteExpense(expense_id); } } diff --git a/src/time-and-attendance/expenses/expense.utils.ts b/src/time-and-attendance/expenses/expense.utils.ts new file mode 100644 index 0000000..0cd4fc9 --- /dev/null +++ b/src/time-and-attendance/expenses/expense.utils.ts @@ -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> => { + 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, + } + }; +} \ No newline at end of file diff --git a/src/time-and-attendance/expenses/expenses.module.ts b/src/time-and-attendance/expenses/expenses.module.ts index d9554b8..99870cd 100644 --- a/src/time-and-attendance/expenses/expenses.module.ts +++ b/src/time-and-attendance/expenses/expenses.module.ts @@ -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 { Module } from "@nestjs/common"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; import { BankCodesResolver } from "src/common/mappers/bank-type-id.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({ - controllers: [ ExpenseController ], - providers: [ ExpenseUpsertService, EmailToIdResolver, BankCodesResolver, EmployeeTimesheetResolver ], + controllers: [ExpenseController], + providers: [ + ExpenseCreateService, + ExpenseUpdateService, + ExpenseDeleteService, + EmailToIdResolver, + BankCodesResolver, + EmployeeTimesheetResolver, + ], }) -export class ExpensesModule {} \ No newline at end of file +export class ExpensesModule { } \ No newline at end of file diff --git a/src/time-and-attendance/expenses/services/expense-create.service.ts b/src/time-and-attendance/expenses/services/expense-create.service.ts new file mode 100644 index 0000000..6222c83 --- /dev/null +++ b/src/time-and-attendance/expenses/services/expense-create.service.ts @@ -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> { + 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' }; + } + } +} \ No newline at end of file diff --git a/src/time-and-attendance/expenses/services/expense-delete.service.ts b/src/time-and-attendance/expenses/services/expense-delete.service.ts new file mode 100644 index 0000000..9ad7dd8 --- /dev/null +++ b/src/time-and-attendance/expenses/services/expense-delete.service.ts @@ -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> { + 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` }; + } + } +} \ No newline at end of file diff --git a/src/time-and-attendance/expenses/services/expense-update.service.ts b/src/time-and-attendance/expenses/services/expense-update.service.ts new file mode 100644 index 0000000..3f2e1e5 --- /dev/null +++ b/src/time-and-attendance/expenses/services/expense-update.service.ts @@ -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> { + 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' }; + } + } +} + diff --git a/src/time-and-attendance/expenses/services/expense-upsert.service.ts b/src/time-and-attendance/expenses/services/expense-upsert.service.ts deleted file mode 100644 index 6d3d5d1..0000000 --- a/src/time-and-attendance/expenses/services/expense-upsert.service.ts +++ /dev/null @@ -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> { - 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> { - 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> { - 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> => { - 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; - }; -} \ No newline at end of file diff --git a/src/time-and-attendance/expenses/services/expenses-archival.service.ts b/src/time-and-attendance/expenses/services/expenses-archival.service.ts deleted file mode 100644 index 0c354d6..0000000 --- a/src/time-and-attendance/expenses/services/expenses-archival.service.ts +++ /dev/null @@ -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 { - //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 { - return this.prisma.expensesArchive.findMany(); - } - - //fetches an archived timesheet - async findOneArchived(id: number): Promise { - return this.prisma.expensesArchive.findUniqueOrThrow({ where: { id } }); - } -} \ No newline at end of file diff --git a/src/time-and-attendance/paid-time-off/paid-time-off.dto.ts b/src/time-and-attendance/paid-time-off/paid-time-off.dto.ts index f9f4f3b..0405be8 100644 --- a/src/time-and-attendance/paid-time-off/paid-time-off.dto.ts +++ b/src/time-and-attendance/paid-time-off/paid-time-off.dto.ts @@ -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']; @@ -7,4 +10,21 @@ export const paid_time_off_mapping: Record 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(); + } +} \ No newline at end of file diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-create.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-create.service.ts index 06be417..456a903 100644 --- a/src/time-and-attendance/schedule-presets/services/schedule-presets-create.service.ts +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-create.service.ts @@ -55,14 +55,6 @@ export class SchedulePresetsCreateService { } 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({ data: { name: dto.name, diff --git a/src/time-and-attendance/shifts/services/shifts-archival.service.ts b/src/time-and-attendance/shifts/services/shifts-archival.service.ts deleted file mode 100644 index a5a833f..0000000 --- a/src/time-and-attendance/shifts/services/shifts-archival.service.ts +++ /dev/null @@ -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 { - //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 { - return this.prisma.shiftsArchive.findMany(); - } - - //fetches an archived timesheet - async findOneArchived(id: number): Promise { - return this.prisma.shiftsArchive.findUniqueOrThrow({ where: { id } }); - } - -} \ No newline at end of file diff --git a/src/time-and-attendance/shifts/services/shifts-create.service.ts b/src/time-and-attendance/shifts/services/shifts-create.service.ts index 9a7c2e5..646e2ee 100644 --- a/src/time-and-attendance/shifts/services/shifts-create.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-create.service.ts @@ -179,12 +179,4 @@ export class ShiftsCreateService { 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`; - } } diff --git a/src/time-and-attendance/shifts/services/shifts-get.service.ts b/src/time-and-attendance/shifts/services/shifts-get.service.ts deleted file mode 100644 index 8f459de..0000000 --- a/src/time-and-attendance/shifts/services/shifts-get.service.ts +++ /dev/null @@ -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 { - // 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; - // }); - - - - // } -} \ No newline at end of file diff --git a/src/time-and-attendance/shifts/shifts.module.ts b/src/time-and-attendance/shifts/shifts.module.ts index 6b079cd..b6c090e 100644 --- a/src/time-and-attendance/shifts/shifts.module.ts +++ b/src/time-and-attendance/shifts/shifts.module.ts @@ -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 { 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 { 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'; @Module({ @@ -26,7 +25,6 @@ import { PaidTimeOFfBankHoursService } from 'src/time-and-attendance/paid-time-o ShiftsCreateService, ShiftsUpdateService, ShiftsDeleteService, - ShiftsGetService, ], }) export class ShiftsModule { } diff --git a/src/time-and-attendance/time-and-attendance.module.ts b/src/time-and-attendance/time-and-attendance.module.ts index d736a85..c5d1420 100644 --- a/src/time-and-attendance/time-and-attendance.module.ts +++ b/src/time-and-attendance/time-and-attendance.module.ts @@ -1,14 +1,19 @@ - import { Module } from "@nestjs/common"; 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 { 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 { TimesheetController } from "src/time-and-attendance/timesheets/timesheet.controller"; 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 { 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 { 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 { ShiftController } from "src/time-and-attendance/shifts/shift.controller"; 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 { 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 { 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 { 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({ imports: [ @@ -66,11 +67,12 @@ import { PaidTimeOFfBankHoursService } from "src/time-and-attendance/paid-time-o ], providers: [ GetTimesheetsOverviewService, - ShiftsGetService, ShiftsCreateService, ShiftsUpdateService, ShiftsDeleteService, - ExpenseUpsertService, + ExpenseCreateService, + ExpenseUpdateService, + ExpenseDeleteService, SchedulePresetsGetService, SchedulePresetDeleteService, SchedulePresetsApplyService, diff --git a/src/time-and-attendance/timesheets/services/timesheet-archive.service.ts b/src/time-and-attendance/timesheets/services/timesheet-archive.service.ts deleted file mode 100644 index 33fbf76..0000000 --- a/src/time-and-attendance/timesheets/services/timesheet-archive.service.ts +++ /dev/null @@ -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 { -// //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 { -// return this.prisma.timesheetsArchive.findMany(); -// } - -// //fetches an archived timesheet -// async findOneArchived(id: number): Promise { -// return this.prisma.timesheetsArchive.findUniqueOrThrow({ where: { id } }); -// } -// } \ No newline at end of file diff --git a/src/time-and-attendance/timesheets/services/timesheet-employee-overview.service.ts b/src/time-and-attendance/timesheets/services/timesheet-employee-overview.service.ts new file mode 100644 index 0000000..17dd885 --- /dev/null +++ b/src/time-and-attendance/timesheets/services/timesheet-employee-overview.service.ts @@ -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> { + 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!; + } +} diff --git a/src/time-and-attendance/timesheets/services/timesheet-get-overview.service.ts b/src/time-and-attendance/timesheets/services/timesheet-get-overview.service.ts deleted file mode 100644 index 566d546..0000000 --- a/src/time-and-attendance/timesheets/services/timesheet-get-overview.service.ts +++ /dev/null @@ -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> { - 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 { - //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[]>(); - 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[]>(); - 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'; -} diff --git a/src/time-and-attendance/timesheets/timesheet.controller.ts b/src/time-and-attendance/timesheets/timesheet.controller.ts index 448f459..9496775 100644 --- a/src/time-and-attendance/timesheets/timesheet.controller.ts +++ b/src/time-and-attendance/timesheets/timesheet.controller.ts @@ -1,5 +1,5 @@ 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 { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators"; import { Modules as ModulesEnum } from ".prisma/client"; diff --git a/src/time-and-attendance/timesheets/timesheet.dto.ts b/src/time-and-attendance/timesheets/timesheet.dto.ts index 61055a0..4616f59 100644 --- a/src/time-and-attendance/timesheets/timesheet.dto.ts +++ b/src/time-and-attendance/timesheets/timesheet.dto.ts @@ -38,6 +38,8 @@ export class TotalHours { @Type(() => Number) vacation: number; @Type(() => Number) holiday: number; @Type(() => Number) sick: number; + @Type(() => Number) banking: number; + @Type(() => Number) withdraw_banked: number; } export class TotalExpenses { @Type(() => Number) expenses: number; @@ -68,4 +70,5 @@ export class Expense { @IsString() @IsOptional() attachment?: string; @IsOptional() @IsInt() id?: number | null; @IsString() @IsOptional() supervisor_comment?: string | null; -} \ No newline at end of file +} + diff --git a/src/time-and-attendance/timesheets/timesheet.mapper.ts b/src/time-and-attendance/timesheets/timesheet.mapper.ts new file mode 100644 index 0000000..f1e018d --- /dev/null +++ b/src/time-and-attendance/timesheets/timesheet.mapper.ts @@ -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 => { + //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[]>(); + 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[]>(); + 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; +}; \ No newline at end of file diff --git a/src/time-and-attendance/timesheets/timesheets.module.ts b/src/time-and-attendance/timesheets/timesheets.module.ts index 093b06d..1654841 100644 --- a/src/time-and-attendance/timesheets/timesheets.module.ts +++ b/src/time-and-attendance/timesheets/timesheets.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { TimesheetController } from 'src/time-and-attendance/timesheets/timesheet.controller'; 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'; @Module({ diff --git a/src/time-and-attendance/utils/type.utils.ts b/src/time-and-attendance/utils/type.utils.ts index 485866e..f801018 100644 --- a/src/time-and-attendance/utils/type.utils.ts +++ b/src/time-and-attendance/utils/type.utils.ts @@ -14,7 +14,7 @@ export type NormalizedExpense = { amount?: number | Prisma.Decimal | null; mileage?: number | Prisma.Decimal | null; attachment?: number; - bank_code_id: number; + // bank_code_id: number; }; export type NormalizedLeaveRequest = {