diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d0d2c87..ebd0fa5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -23,20 +23,36 @@ model Users { residence String? role Roles @default(GUEST) - employee Employees? @relation("UserEmployee") - oauth_sessions OAuthSessions[] @relation("UserOAuthSessions") - preferences Preferences? @relation("UserPreferences") + employee Employees? @relation("UserEmployee") + oauth_sessions OAuthSessions[] @relation("UserOAuthSessions") + preferences Preferences? @relation("UserPreferences") + user_module_access userModuleAccess? @relation("UserModuleAccess") @@map("users") } +model userModuleAccess { + id Int @id @default(autoincrement()) + user Users @relation("UserModuleAccess", fields: [user_id], references: [id]) + user_id String @unique @db.Uuid + + timesheets Boolean @default(false) //wich allows an employee to enter shifts and expenses + timesheets_approval Boolean @default(false) //wich allows the approbation of timesheets by a supervisor or above + employee_list Boolean @default(false) //wich shows the lists of employee to show names, emails, titles and profile picture + employee_management Boolean @default(false) //wich offers CRUD for employees, schedule_presets and manage module access + personnal_profile Boolean @default(false) //wich governs profile details, preferances and dashboard access + blocked Boolean @default(false) + + @@map("user_module_access") +} + model Employees { id Int @id @default(autoincrement()) user Users @relation("UserEmployee", fields: [user_id], references: [id]) user_id String @unique @db.Uuid supervisor Employees? @relation("EmployeeSupervisor", fields: [supervisor_id], references: [id]) supervisor_id Int? - + external_payroll_id Int company_code Int first_work_day DateTime @db.Date @@ -44,11 +60,10 @@ model Employees { job_title String? is_supervisor Boolean @default(false) - - crew Employees[] @relation("EmployeeSupervisor") - timesheet Timesheets[] @relation("TimesheetEmployee") - leave_request LeaveRequests[] @relation("LeaveRequestEmployee") - schedule_presets SchedulePresets[] @relation("SchedulePreset") + crew Employees[] @relation("EmployeeSupervisor") + timesheet Timesheets[] @relation("TimesheetEmployee") + leave_request LeaveRequests[] @relation("LeaveRequestEmployee") + schedule_presets SchedulePresets[] @relation("SchedulePreset") @@map("employees") } @@ -65,7 +80,7 @@ model LeaveRequests { payable_hours Decimal? @db.Decimal(5, 2) requested_hours Decimal? @db.Decimal(5, 2) approval_status LeaveApprovalStatus @default(PENDING) - leave_type LeaveTypes + leave_type LeaveTypes archive LeaveRequestsArchive[] @relation("LeaveRequestToArchive") @@ -79,14 +94,14 @@ model LeaveRequestsArchive { leave_request LeaveRequests @relation("LeaveRequestToArchive", fields: [leave_request_id], references: [id]) leave_request_id Int - archived_at DateTime @default(now()) - employee_id Int - date DateTime @db.Date - payable_hours Decimal? @db.Decimal(5, 2) - requested_hours Decimal? @db.Decimal(5, 2) - comment String - leave_type LeaveTypes - approval_status LeaveApprovalStatus + archived_at DateTime @default(now()) + employee_id Int + date DateTime @db.Date + payable_hours Decimal? @db.Decimal(5, 2) + requested_hours Decimal? @db.Decimal(5, 2) + comment String + leave_type LeaveTypes + approval_status LeaveApprovalStatus @@unique([leave_request_id]) @@index([employee_id, date]) @@ -126,9 +141,9 @@ model TimesheetsArchive { timesheet Timesheets @relation("TimesheetsToArchive", fields: [timesheet_id], references: [id]) timesheet_id Int - employee_id Int - is_approved Boolean - archive_at DateTime @default(now()) + employee_id Int + is_approved Boolean + archive_at DateTime @default(now()) @@map("timesheets_archive") } @@ -139,7 +154,7 @@ model SchedulePresets { employee_id Int name String - is_default Boolean @default(false) + is_default Boolean @default(false) shifts SchedulePresetShifts[] @relation("SchedulePresetShiftsSchedulePreset") @@ -149,9 +164,9 @@ model SchedulePresets { model SchedulePresetShifts { id Int @id @default(autoincrement()) - preset SchedulePresets @relation("SchedulePresetShiftsSchedulePreset",fields: [preset_id], references: [id]) + preset SchedulePresets @relation("SchedulePresetShiftsSchedulePreset", fields: [preset_id], references: [id]) preset_id Int - bank_code BankCodes @relation("SchedulePresetShiftsBankCodes",fields: [bank_code_id], references: [id]) + bank_code BankCodes @relation("SchedulePresetShiftsBankCodes", fields: [bank_code_id], references: [id]) bank_code_id Int sort_order Int @@ -180,13 +195,14 @@ model Shifts { comment String? archive ShiftsArchive[] @relation("ShiftsToArchive") + @@unique([timesheet_id, date, start_time], name: "unique_ts_id_date_start_time") @@map("shifts") } model ShiftsArchive { - id Int @id @default(autoincrement()) - shift Shifts @relation("ShiftsToArchive", fields: [shift_id], references: [id]) + id Int @id @default(autoincrement()) + shift Shifts @relation("ShiftsToArchive", fields: [shift_id], references: [id]) shift_id Int date DateTime @db.Date @@ -216,39 +232,40 @@ model BankCodes { } model Expenses { - id Int @id @default(autoincrement()) - timesheet Timesheets @relation("ExpensesTimesheet", fields: [timesheet_id], references: [id]) - timesheet_id Int - bank_code BankCodes @relation("ExpenseBankCodes", fields: [bank_code_id], references: [id]) - bank_code_id Int - attachment_record Attachments? @relation("ExpenseAttachment", fields: [attachment], references: [id], onDelete: SetNull) - attachment Int? + id Int @id @default(autoincrement()) + timesheet Timesheets @relation("ExpensesTimesheet", fields: [timesheet_id], references: [id]) + timesheet_id Int + bank_code BankCodes @relation("ExpenseBankCodes", fields: [bank_code_id], references: [id]) + bank_code_id Int + attachment_record Attachments? @relation("ExpenseAttachment", fields: [attachment], references: [id], onDelete: SetNull) + attachment Int? date DateTime @db.Date - amount Decimal? @db.Decimal(12,2) - mileage Decimal? @db.Decimal(12,2) + amount Decimal? @db.Decimal(12, 2) + mileage Decimal? @db.Decimal(12, 2) comment String supervisor_comment String? is_approved Boolean @default(false) archive ExpensesArchive[] @relation("ExpensesToArchive") + @@unique([timesheet_id, date, amount, mileage], name: "unique_ts_id_date_amount_mileage") @@map("expenses") } model ExpensesArchive { - id Int @id @default(autoincrement()) - expense Expenses @relation("ExpensesToArchive", fields: [expense_id], references: [id]) - expense_id Int - attachment_record Attachments? @relation("ExpenseArchiveAttachment", fields: [attachment], references: [id], onDelete: SetNull) - attachment Int? + id Int @id @default(autoincrement()) + expense Expenses @relation("ExpensesToArchive", fields: [expense_id], references: [id]) + expense_id Int + attachment_record Attachments? @relation("ExpenseArchiveAttachment", fields: [attachment], references: [id], onDelete: SetNull) + attachment Int? timesheet_id Int archived_at DateTime @default(now()) bank_code_id Int date DateTime @db.Date - amount Decimal? @db.Decimal(12,2) - mileage Decimal? @db.Decimal(12,2) + amount Decimal? @db.Decimal(12, 2) + mileage Decimal? @db.Decimal(12, 2) comment String? is_approved Boolean supervisor_comment String? @@ -289,7 +306,7 @@ model Blobs { model Attachments { id Int @id @default(autoincrement()) - blob Blobs @relation("AttachmnentBlob",fields: [sha256], references: [sha256], onUpdate: Cascade) + blob Blobs @relation("AttachmnentBlob", fields: [sha256], references: [sha256], onUpdate: Cascade) sha256 String @db.Char(64) owner_type String //EXPENSES ou éventuellement autre chose comme scan ONU ou photos d'employés, etc @@ -304,7 +321,7 @@ model Attachments { expenses_archive ExpensesArchive[] @relation("ExpenseArchiveAttachment") AttachmentVariants AttachmentVariants[] @relation("attachmentVariantAttachment") - + @@index([owner_type, owner_id, created_at]) @@index([sha256]) @@map("attachments") @@ -313,7 +330,7 @@ model Attachments { model AttachmentVariants { id Int @id @default(autoincrement()) attachment_id Int - attachment Attachments @relation("attachmentVariantAttachment",fields: [attachment_id], references: [id], onDelete: Cascade) + attachment Attachments @relation("attachmentVariantAttachment", fields: [attachment_id], references: [id], onDelete: Cascade) variant String path String bytes Int @@ -326,18 +343,18 @@ model AttachmentVariants { } model Preferences { - id Int @id @default(autoincrement()) - user Users @relation("UserPreferences", fields: [user_id], references: [id]) - user_id String @unique @db.Uuid + id Int @id @default(autoincrement()) + user Users @relation("UserPreferences", fields: [user_id], references: [id]) + user_id String @unique @db.Uuid notifications Int @default(0) dark_mode Int @default(0) lang_switch Int @default(0) lefty_mode Int @default(0) - employee_list_display Int @default(0) - validation_display Int @default(0) - timesheet_display Int @default(0) + employee_list_display Int @default(0) + validation_display Int @default(0) + timesheet_display Int @default(0) @@map("preferences") } @@ -398,4 +415,3 @@ enum Weekday { FRI SAT } - diff --git a/src/identity-and-account/user-module-access/access.module.ts b/src/identity-and-account/user-module-access/access.module.ts new file mode 100644 index 0000000..f120104 --- /dev/null +++ b/src/identity-and-account/user-module-access/access.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { AccessUpdateService } from "src/identity-and-account/user-module-access/services/access-update.service"; +import { AccessController } from "src/identity-and-account/user-module-access/controllers/access.controller"; +import { AccessGetService } from "src/identity-and-account/user-module-access/services/access-get.service"; + +@Module({ + controllers: [AccessController], + providers: [AccessUpdateService, AccessGetService], + exports: [], +}) +export class AccessModule { } \ No newline at end of file diff --git a/src/identity-and-account/user-module-access/controllers/access.controller.ts b/src/identity-and-account/user-module-access/controllers/access.controller.ts new file mode 100644 index 0000000..d9e7a52 --- /dev/null +++ b/src/identity-and-account/user-module-access/controllers/access.controller.ts @@ -0,0 +1,40 @@ +import { Body, Controller, Get, Patch, Query, Req } from "@nestjs/common"; +import { ModuleAccess } from "src/identity-and-account/user-module-access/dtos/acces.dto"; +import { AccessGetService } from "src/identity-and-account/user-module-access/services/access-get.service"; +import { AccessUpdateService } from "src/identity-and-account/user-module-access/services/access-update.service"; + +@Controller() +export class AccessController { + constructor( + private readonly getService: AccessGetService, + private readonly updateService: AccessUpdateService, + ) { } + + @Get() + async findAccess( + @Req() req, + @Query('employee_email') employee_email?: string + ) { + const email = req.user?.email; + await this.getService.findModuleAccess(email, employee_email); + }; + + @Patch() + async updateAccess( + @Req() req, + @Body() dto: ModuleAccess, + @Query('employee_email') employee_email?: string + ) { + const email = req.user?.email; + await this.updateService.updateModuleAccess(email, dto, employee_email); + }; + + @Patch() + async revokeModuleAccess( + @Req() req, + @Query('employee_email') employee_email?: string + ) { + const email = req.user?.email; + await this.updateService.revokeModuleAccess(email, employee_email); + }; +} \ No newline at end of file diff --git a/src/identity-and-account/user-module-access/dtos/acces.dto.ts b/src/identity-and-account/user-module-access/dtos/acces.dto.ts new file mode 100644 index 0000000..a4d5a6f --- /dev/null +++ b/src/identity-and-account/user-module-access/dtos/acces.dto.ts @@ -0,0 +1,10 @@ +import { IsBoolean } from "class-validator"; + +export class ModuleAccess { + @IsBoolean() timesheets!: boolean; + @IsBoolean() timesheets_approval!: boolean; + @IsBoolean() employee_list!: boolean; + @IsBoolean() employee_management!: boolean; + @IsBoolean() personnal_profile!: boolean; + @IsBoolean() blocked!: boolean; +} \ No newline at end of file diff --git a/src/identity-and-account/user-module-access/services/access-get.service.ts b/src/identity-and-account/user-module-access/services/access-get.service.ts new file mode 100644 index 0000000..6f990f8 --- /dev/null +++ b/src/identity-and-account/user-module-access/services/access-get.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from "@nestjs/common"; +import { Result } from "src/common/errors/result-error.factory"; +import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; +import { ModuleAccess } from "src/identity-and-account/user-module-access/dtos/acces.dto"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class AccessGetService { + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver: EmailToIdResolver, + ) { } + + async findModuleAccess(email: string, employee_email?: string): Promise> { + const account_email = employee_email ?? email; + const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email); + if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; + + const access = await this.prisma.userModuleAccess.findUnique({ + where: { user_id: user_id.data }, + select: { + timesheets: true, + timesheets_approval: true, + employee_list: true, + employee_management: true, + personnal_profile: true, + blocked: true, + }, + }); + if (!access) return { success: false, error: 'MODULE_ACCESS_NOT_FOUND' }; + + const granted_access: ModuleAccess = { + timesheets: access.timesheets, + timesheets_approval: access.timesheets_approval, + employee_list: access.employee_list, + employee_management: access.employee_management, + personnal_profile: access.personnal_profile, + blocked: access.blocked, + }; + return { success: true, data: granted_access } + } +} \ No newline at end of file diff --git a/src/identity-and-account/user-module-access/services/access-update.service.ts b/src/identity-and-account/user-module-access/services/access-update.service.ts new file mode 100644 index 0000000..818b013 --- /dev/null +++ b/src/identity-and-account/user-module-access/services/access-update.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from "@nestjs/common"; +import { Result } from "src/common/errors/result-error.factory"; +import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; +import { ModuleAccess } from "src/identity-and-account/user-module-access/dtos/acces.dto"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class AccessUpdateService { + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver: EmailToIdResolver, + ) { } + + async updateModuleAccess(email: string, dto: ModuleAccess, employee_email?: string): Promise> { + const account_email = employee_email ?? email; + const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email); + if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; + + const orignal_access = await this.prisma.userModuleAccess.findUnique({ + where: { user_id: user_id.data }, + select: { + id: true, + timesheets: true, + timesheets_approval: true, + employee_list: true, + employee_management: true, + personnal_profile: true, + }, + }); + if (!orignal_access) return { success: false, error: 'MODULE_ACCESS_NOT_FOUND' }; + + await this.prisma.userModuleAccess.update({ + where: { id: orignal_access.id }, + data: { + timesheets: dto.timesheets, + timesheets_approval: dto.timesheets_approval, + employee_list: dto.employee_list, + employee_management: dto.employee_management, + personnal_profile: dto.personnal_profile, + } + }) + return { success: true, data: true }; + } + + async revokeModuleAccess(email: string, employee_email?: string): Promise> { + const account_email = employee_email ?? email; + const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email); + if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; + + const access = await this.prisma.userModuleAccess.findUnique({ + where: { user_id: user_id.data }, + select: { id: true }, + }); + if (!access) return { success: false, error: 'MODULE_ACCESS_NOT_FOUND' }; + + await this.prisma.userModuleAccess.update({ + where: { id: access.id }, + data: { + timesheets: false, + timesheets_approval: false, + employee_list: false, + employee_management: false, + personnal_profile: false, + blocked: true, + }, + }); + + return { success: true, data: true }; + } +} \ No newline at end of file diff --git a/src/time-and-attendance/schedule-presets/controller/schedule-presets.controller.ts b/src/time-and-attendance/schedule-presets/controller/schedule-presets.controller.ts index 3207f18..9bf003d 100644 --- a/src/time-and-attendance/schedule-presets/controller/schedule-presets.controller.ts +++ b/src/time-and-attendance/schedule-presets/controller/schedule-presets.controller.ts @@ -2,18 +2,19 @@ import { Controller, Param, Query, Body, Get, Post, ParseIntPipe, Delete, Patch, import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes"; import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; -// import { SchedulePresetsUpdateDto } from "src/time-and-attendance/schedule-presets/dtos/update-schedule-presets.dto"; import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service"; import { SchedulePresetsGetService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-get.service"; -import { SchedulePresetsUpsertService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service"; +import { SchedulePresetsCreateService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-create.service"; +import { SchedulePresetUpdateDeleteService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-update-delete.service"; @Controller('schedule-presets') @RolesAllowed(...GLOBAL_CONTROLLER_ROLES) export class SchedulePresetsController { constructor( - private readonly upsertService: SchedulePresetsUpsertService, + private readonly createService: SchedulePresetsCreateService, private readonly getService: SchedulePresetsGetService, private readonly applyPresetsService: SchedulePresetsApplyService, + private readonly updateDeleteService: SchedulePresetUpdateDeleteService, ) { } // used to create a schedule preset @@ -21,34 +22,33 @@ export class SchedulePresetsController { @RolesAllowed(...MANAGER_ROLES) async createPreset(@Req() req, @Body() dto: SchedulePresetsDto) { const email = req.user?.email; - return await this.upsertService.createPreset(email, dto); + return await this.createService.createPreset(email, dto); } - // //used to update an already existing schedule preset - // @Patch('update/:preset_id') - // @RolesAllowed(...MANAGER_ROLES) - // async updatePreset( - // @Param('preset_id', ParseIntPipe) preset_id: number, - // @Body() dto: SchedulePresetsUpdateDto - // ) { - // return await this.upsertService.updatePreset(preset_id, dto); - // } + //used to update an already existing schedule preset + @Patch('update/:preset_id') + @RolesAllowed(...MANAGER_ROLES) + async updatePreset( + @Param('preset_id', ParseIntPipe) preset_id: number, + @Body() dto: SchedulePresetsDto, + @Req() req, + ) { + const email = req.user?.email; + return await this.updateDeleteService.updatePreset(preset_id, dto, email); + } //used to delete a schedule preset - // @Delete('delete/:preset_id') - // @RolesAllowed(...MANAGER_ROLES) - // async deletePreset( - // @Param('preset_id', ParseIntPipe) preset_id: number) { - // return await this.upsertService.deletePreset(preset_id); - // } - + @Delete('delete/:preset_id') + @RolesAllowed(...MANAGER_ROLES) + async deletePreset(@Param('preset_id', ParseIntPipe) preset_id: number, @Req() req) { + const email = req.user?.email; + return await this.updateDeleteService.deletePreset(preset_id, email); + } //used to show the list of available schedule presets @Get('find-list') @RolesAllowed(...MANAGER_ROLES) - async findListById( - @Req() req - ) { + async findListById(@Req() req) { const email = req.user?.email; return this.getService.getSchedulePresets(email); } @@ -56,9 +56,9 @@ export class SchedulePresetsController { //used to apply a preset to a timesheet @Post('apply-presets') async applyPresets( - @Req() req, @Body('preset') preset_id: number, - @Body('start') start_date: string + @Body('start') start_date: string, + @Req() req ) { const email = req.user?.email; return this.applyPresetsService.applyToTimesheet(email, preset_id, start_date); diff --git a/src/time-and-attendance/schedule-presets/schedule-presets.module.ts b/src/time-and-attendance/schedule-presets/schedule-presets.module.ts index 1c7801d..09be865 100644 --- a/src/time-and-attendance/schedule-presets/schedule-presets.module.ts +++ b/src/time-and-attendance/schedule-presets/schedule-presets.module.ts @@ -1,21 +1,25 @@ import { Module } from "@nestjs/common"; -// import { SchedulePresetsController } from "src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller"; -// import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service"; -// import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service"; -// import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; +import { SchedulePresetsController } from "src/time-and-attendance/schedule-presets/controller/schedule-presets.controller"; +import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service"; +import { SchedulePresetsCreateService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-create.service"; +import { SchedulePresetsGetService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-get.service"; +import { SchedulePresetUpdateDeleteService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-update-delete.service"; + @Module({ - controllers: [/*SchedulePresetsController*/], - // providers: [ - // SchedulePresetsUpsertService, - // SchedulePresetsGetService, - // SchedulePresetsApplyService, - // ], - exports:[ - // SchedulePresetsUpsertService, - // SchedulePresetsGetService, - // SchedulePresetsApplyService, + controllers: [SchedulePresetsController], + providers: [ + SchedulePresetsCreateService, + SchedulePresetUpdateDeleteService, + SchedulePresetsGetService, + SchedulePresetsApplyService, ], -}) export class SchedulePresetsModule {} \ No newline at end of file + exports: [ + SchedulePresetsCreateService, + SchedulePresetUpdateDeleteService, + SchedulePresetsGetService, + SchedulePresetsApplyService, + ], +}) export class SchedulePresetsModule { } \ 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 new file mode 100644 index 0000000..6207ba4 --- /dev/null +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-create.service.ts @@ -0,0 +1,125 @@ +import { Injectable } from "@nestjs/common"; +import { Weekday } from "@prisma/client"; +import { PrismaService } from "src/prisma/prisma.service"; +import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; +import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; +import { Result } from "src/common/errors/result-error.factory"; +import { overlaps, toDateFromHHmm } from "src/common/utils/date-utils"; +import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; + +@Injectable() +export class SchedulePresetsCreateService { + constructor( + private readonly prisma: PrismaService, + private readonly typeResolver: BankCodesResolver, + private readonly emailResolver: EmailToIdResolver, + ) { } + //_________________________________________________________________ + // CREATE + //_________________________________________________________________ + async createPreset(email: string, dto: SchedulePresetsDto): Promise> { + try { + + //validate email and fetch employee_id + const employee_id = await this.emailResolver.findIdByEmail(email); + if (!employee_id.success) return { success: false, error: employee_id.error }; + + //validate new unique name + const existing = await this.prisma.schedulePresets.findFirst({ + where: { name: dto.name, employee_id: employee_id.data }, + select: { name: true }, + }); + if (!existing) return { success: false, error: 'INVALID_SCHEDULE_PRESET' }; + + const normalized_shifts = dto.preset_shifts.map((shift) => ({ + ...shift, + start: toDateFromHHmm(shift.start_time), + end: toDateFromHHmm(shift.end_time), + })); + + for (const preset_shifts of normalized_shifts) { + for (const other_shifts of normalized_shifts) { + //skip if same object or id week_day is not the same + if (preset_shifts === other_shifts) continue; + if (preset_shifts.week_day !== other_shifts.week_day) continue; + //check overlaping possibilities + const has_overlap = overlaps( + { start: preset_shifts.start, end: preset_shifts.end }, + { start: other_shifts.start, end: other_shifts.end }, + ) + if (has_overlap) return { success: false, error: 'SCHEDULE_PRESET_OVERLAP' }; + } + } + + //validate bank_code_id/type and map them + const bank_code_results = await Promise.all(dto.preset_shifts.map((shift) => + this.typeResolver.findBankCodeIDByType(shift.type), + )); + for (const result of bank_code_results) { + if (!result.success) return { success: false, error: 'INVALID_SCHEDULE_PRESET' } + } + + 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: { employee_id: employee_id.data, is_default: true }, + data: { is_default: false }, + }); + } + + await tx.schedulePresets.create({ + data: { + employee_id: employee_id.data, + name: dto.name, + is_default: dto.is_default ?? false, + shifts: { + create: dto.preset_shifts.map((shift, index) => { + //validated bank_codes sent as a Result Array to access its data + const result = bank_code_results[index] as { success: true, data: number }; + return { + week_day: shift.week_day, + sort_order: shift.sort_order, + start_time: toDateFromHHmm(shift.start_time), + end_time: toDateFromHHmm(shift.end_time), + is_remote: shift.is_remote ?? false, + bank_code: { + //connect uses the FK links to set the bank_code_id + connect: { id: result.data }, + }, + } + }), + }, + }, + }); + }); + return { success: true, data: true } + } catch (error) { + return { success: false, error: 'INVALID_SCHEDULE_PRESET'} + } + } + + + // //PRIVATE HELPERS + + //resolve bank_code_id using type and convert hours to TIME and valid shifts end/start + // private async normalizePresetShifts(preset_shift: SchedulePresetShiftsDto, schedul_preset: SchedulePresetsDto): Promise> { + + // const bank_code = await this.typeResolver.findIdAndModifierByType(preset_shift.type); + // if (!bank_code.success) return { success: false, error: 'INVALID_SCHEDULE_PRESET_SHIFT' }; + + // const start = await toDateFromHHmm(preset_shift.start_time); + // const end = await toDateFromHHmm(preset_shift.end_time); + + // //TODO: add a way to fetch + + + // const normalized_preset_shift:Normalized = { + // date: , + // start_time : start, + // end_time: end, + // bank_code_id: bank_code.data.id, + // } + // return { success: true data: normalized_preset_shift } + // } +} diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-get.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-get.service.ts index 405d8c7..e7cac2f 100644 --- a/src/time-and-attendance/schedule-presets/services/schedule-presets-get.service.ts +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-get.service.ts @@ -6,32 +6,33 @@ import { Result } from "src/common/errors/result-error.factory"; @Injectable() export class SchedulePresetsGetService { - constructor( + constructor( private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver, - ){} + ) { } async getSchedulePresets(email: string): Promise> { try { const employee_id = await this.emailResolver.findIdByEmail(email); - if(!employee_id.success) return { success: false, error: employee_id.error } + if (!employee_id.success) return { success: false, error: employee_id.error }; + const presets = await this.prisma.schedulePresets.findMany({ where: { employee_id: employee_id.data }, - orderBy: [{is_default: 'desc' }, { name: 'asc' }], + orderBy: [{ is_default: 'desc' }, { name: 'asc' }], include: { shifts: { - orderBy: [{week_day:'asc'}, { sort_order: 'asc'}], + orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }], include: { bank_code: { select: { type: true } } }, }, }, }); - const hhmm = (date: Date) => date.toISOString().slice(11,16); + const hhmm = (date: Date) => date.toISOString().slice(11, 16); const response: PresetResponse[] = presets.map((preset) => ({ id: preset.id, name: preset.name, is_default: preset.is_default, - shifts: preset.shifts.map((shift)=> ({ + shifts: preset.shifts.map((shift) => ({ week_day: shift.week_day, sort_order: shift.sort_order, start_time: hhmm(shift.start_time), @@ -40,10 +41,10 @@ export class SchedulePresetsGetService { type: shift.bank_code?.type, })), })); - return { success: true, data:response}; - } catch ( error) { - return { success: false, error: `Schedule presets for employee with email ${email} not found`}; + return { success: true, data: response }; + } catch (error) { + return { success: false, error: `SCHEDULE_PRESET_NOT_FOUND` }; } } - + } \ No newline at end of file diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-update-delete.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-update-delete.service.ts new file mode 100644 index 0000000..fef84de --- /dev/null +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-update-delete.service.ts @@ -0,0 +1,125 @@ +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 { overlaps, toDateFromHHmm } from "src/common/utils/date-utils"; +import { PrismaService } from "src/prisma/prisma.service"; +import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; + +@Injectable() +export class SchedulePresetUpdateDeleteService { + constructor( + private readonly prisma: PrismaService, + private readonly typeResolver: BankCodesResolver, + private readonly emailResolver: EmailToIdResolver, + ) { } + + //_________________________________________________________________ + // UPDATE + //_________________________________________________________________ + async updatePreset(preset_id: number, dto: SchedulePresetsDto, email: string): Promise> { + + const employee_id = await this.emailResolver.findIdByEmail(email); + if (!employee_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' } + //look for existing schedule_preset and return OG's data + const existing = await this.prisma.schedulePresets.findFirst({ + where: { id: preset_id, name: dto.name, employee_id: employee_id.data }, + select: { + id: true, + is_default: true, + employee_id: true, + shifts: true, + }, + }); + if (!existing) return { success: false, error: `SCHEDULE_PRESET_NOT_FOUND` }; + //normalized shifts start and end time to make an overlap check with other shifts + const normalized_shifts = dto.preset_shifts.map((shift) => ({ + ...shift, + start: toDateFromHHmm(shift.start_time), + end: toDateFromHHmm(shift.end_time), + })); + + for (const preset_shifts of normalized_shifts) { + for (const other_shifts of normalized_shifts) { + //skip if same object or id week_day is not the same + if (preset_shifts === other_shifts) continue; + if (preset_shifts.week_day !== other_shifts.week_day) continue; + //check overlaping possibilities + const has_overlap = overlaps( + { start: preset_shifts.start, end: preset_shifts.end }, + { start: other_shifts.start, end: other_shifts.end }, + ) + if (has_overlap) return { success: false, error: 'SCHEDULE_PRESET_OVERLAP' }; + } + } + //validate bank_code_id/type and map them + const bank_code_results = await Promise.all(dto.preset_shifts.map((shift) => + this.typeResolver.findBankCodeIDByType(shift.type), + )); + for (const result of bank_code_results) { + if (!result.success) return { success: false, error: 'INVALID_SCHEDULE_PRESET' } + } + + 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: { + employee_id: existing.employee_id, + is_default: true, + NOT: { id: existing.id }, + }, + data: { is_default: false }, + }); + } + //deletes old preset shifts to make place to new ones + await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } }); + + await tx.schedulePresets.update({ + where: { id: existing.id }, + data: { + name: dto.name, + is_default: dto.is_default ?? false, + shifts: { + create: dto.preset_shifts.map((shift, index) => { + //validated bank_codes sent as a Result Array to access its data + const result = bank_code_results[index] as { success: true, data: number }; + return { + week_day: shift.week_day, + sort_order: shift.sort_order, + start_time: toDateFromHHmm(shift.start_time), + end_time: toDateFromHHmm(shift.end_time), + is_remote: shift.is_remote ?? false, + bank_code: { + //connect uses the FK links to set the bank_code_id + connect: { id: result.data }, + }, + } + }), + }, + }, + }); + }); + return { success: true, data: true }; + } + + //_________________________________________________________________ + // DELETE + //_________________________________________________________________ + async deletePreset(preset_id: number, email: string): Promise> { + const employee_id = await this.emailResolver.findIdByEmail(email); + if (!employee_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' } + + const preset = await this.prisma.schedulePresets.findFirst({ + where: { id: preset_id, employee_id: employee_id.data }, + select: { id: true }, + }); + if (!preset) return { success: false, error: `SCHEDULE_PRESET_NOT_FOUND` }; + + await this.prisma.$transaction(async (tx) => { + await tx.schedulePresetShifts.deleteMany({ where: { preset_id: preset_id } }); + await tx.schedulePresets.delete({ where: { id: preset_id } }); + }); + return { success: true, data: true }; + } +} \ No newline at end of file diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service.ts deleted file mode 100644 index 21b5c14..0000000 --- a/src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { Weekday } from "@prisma/client"; -import { PrismaService } from "src/prisma/prisma.service"; -import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; -import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; -import { Result } from "src/common/errors/result-error.factory"; -import { overlaps, toDateFromHHmm } from "src/common/utils/date-utils"; -import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; - -@Injectable() -export class SchedulePresetsUpsertService { - constructor( - private readonly prisma: PrismaService, - private readonly typeResolver: BankCodesResolver, - private readonly emailResolver: EmailToIdResolver, - ) { } - //_________________________________________________________________ - // CREATE - //_________________________________________________________________ - async createPreset(email: string, dto: SchedulePresetsDto): Promise> { - //validate email and fetch employee_id - const employee_id = await this.emailResolver.findIdByEmail(email); - if (!employee_id.success) return { success: false, error: employee_id.error }; - - //validate new unique name - const existing = await this.prisma.schedulePresets.findFirst({ - where: { name: dto.name, employee_id: employee_id.data }, - select: { name: true }, - }); - if (!existing) return { success: false, error: 'INVALID_SCHEDULE_PRESET' }; - - const normalized_shifts = dto.preset_shifts.map((shift) => ({ - ...shift, - start: toDateFromHHmm(shift.start_time), - end: toDateFromHHmm(shift.end_time), - })); - - for (const preset_shifts of normalized_shifts) { - for (const other_shifts of normalized_shifts) { - //skip if same object or id week_day is not the same - if (preset_shifts === other_shifts) continue; - if (preset_shifts.week_day !== other_shifts.week_day) continue; - //check overlaping possibilities - const has_overlap = overlaps( - { start: preset_shifts.start, end: preset_shifts.end }, - { start: other_shifts.start, end: other_shifts.end }, - ) - if (has_overlap) return { success: false, error: 'SCHEDULE_PRESET_OVERLAP' }; - } - } - //validate bank_code_id/type and map them - const bank_code_results = await Promise.all(dto.preset_shifts.map((shift) => - this.typeResolver.findBankCodeIDByType(shift.type), - )); - for (const result of bank_code_results) { - if (!result.success) return { success: false, error: 'INVALID_SCHEDULE_PRESET' } - } - - 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: { employee_id: employee_id.data, is_default: true }, - data: { is_default: false }, - }); - } - - await tx.schedulePresets.create({ - data: { - employee_id: employee_id.data, - name: dto.name, - is_default: dto.is_default ?? false, - shifts: { - create: dto.preset_shifts.map((shift, index) => { - //validated bank_codes sent as a Result Array to access its data - const result = bank_code_results[index] as { success: true, data: number }; - return { - week_day: shift.week_day, - sort_order: shift.sort_order, - start_time: toDateFromHHmm(shift.start_time), - end_time: toDateFromHHmm(shift.end_time), - is_remote: shift.is_remote ?? false, - bank_code: { - //connect uses the FK links to set the bank_code_id - connect: { id: result.data }, - }, - } - }), - }, - }, - }); - }); - return { success: true, data: true } - } - - // //_________________________________________________________________ - // // UPDATE - // //_________________________________________________________________ - // async updatePreset(preset_id: number, dto: SchedulePresetsDto): Promise> { - // try { - // const existing = await this.prisma.schedulePresets.findFirst({ - // where: { id: preset_id }, - // select: { - // id: true, - // is_default: true, - // employee_id: true, - // }, - // }); - // if (!existing) return { success: false, error: `Preset "${dto.name}" not found` }; - - // const shifts_data = await this.normalizePresetShifts(dto); - // if (!shifts_data.success) return { success: false, error: 'An error occured during normalization' } - - // await this.prisma.$transaction(async (tx) => { - // if (typeof dto.is_default === 'boolean') { - // if (dto.is_default) { - // await tx.schedulePresets.updateMany({ - // where: { - // employee_id: existing.employee_id, - // is_default: true, - // NOT: { id: existing.id }, - // }, - // data: { is_default: false }, - // }); - // } - // await tx.schedulePresets.update({ - // where: { id: existing.id }, - // data: { - // is_default: dto.is_default, - // name: dto.name, - // }, - // }); - // } - // if (shifts_data.data.length <= 0) return { success: false, error: 'Preset shifts to update not found' }; - - // await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } }); - - // try { - // const create_many_data: Result = - // shifts_data.data.map((shift) => { - // if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') { - // return { success: false, error: `Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`} - // } - // const bank_code_id = shift.bank_code.connect.id; - // return { - // preset_id: existing.id, - // week_day: shift.week_day, - // sort_order: shift.sort_order, - // start_time: shift.start_time, - // end_time: shift.end_time, - // is_remote: shift.is_remote ?? false, - // bank_code_id: bank_code_id, - // }; - // }); - // if(!create_many_data.success) return { success: false, error: 'Invalid data'} - // await tx.schedulePresetShifts.createMany({ data: create_many_data.data }); - - // return { success: true, data: create_many_data } - // } catch (error) { - // return { success: false, error: 'An error occured. Invalid data detected. ' }; - // } - // }); - - // const saved = await this.prisma.schedulePresets.findUnique({ - // where: { id: existing.id }, - // include: { - // shifts: { - // orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }], - // include: { bank_code: { select: { type: true } } }, - // } - // }, - // }); - // if (!saved) return { success: false, error: `Preset with id: ${existing.id} not found` }; - - // const response_dto: SchedulePresetsDto = { - // id: saved.id, - // name: saved.name, - // is_default: saved.is_default, - // preset_shifts: saved.shifts.map((shift) => ({ - // preset_id: shift.preset_id, - // week_day: shift.week_day, - // sort_order: shift.sort_order, - // type: shift.bank_code.type, - // start_time: toHHmmFromDate(shift.start_time), - // end_time: toHHmmFromDate(shift.end_time), - // is_remote: shift.is_remote, - // })), - // }; - - // return { success: true, data: response_dto }; - // } catch (error) { - // return { success: false, error: 'An error occured during update. Invalid data' } - // } - // } - - // //_________________________________________________________________ - // // DELETE - // //_________________________________________________________________ - // async deletePreset(preset_id: number): Promise> { - // try { - // await this.prisma.$transaction(async (tx) => { - // const preset = await tx.schedulePresets.findFirst({ - // where: { id: preset_id }, - // select: { id: true }, - // }); - // if (!preset) return { success: false, error: `Preset with id ${preset_id} not found` }; - // await tx.schedulePresets.delete({ where: { id: preset_id } }); - - // return { success: true }; - // }); - // return { success: true, data: preset_id }; - - // } catch (error) { - // return { success: false, error: `Preset schedule with id ${preset_id} not found` }; - // } - // } - - // //PRIVATE HELPERS - - //resolve bank_code_id using type and convert hours to TIME and valid shifts end/start - // private async normalizePresetShifts(preset_shift: SchedulePresetShiftsDto, schedul_preset: SchedulePresetsDto): Promise> { - - // const bank_code = await this.typeResolver.findIdAndModifierByType(preset_shift.type); - // if (!bank_code.success) return { success: false, error: 'INVALID_SCHEDULE_PRESET_SHIFT' }; - - // const start = await toDateFromHHmm(preset_shift.start_time); - // const end = await toDateFromHHmm(preset_shift.end_time); - - // //TODO: add a way to fetch - - - // const normalized_preset_shift:Normalized = { - // date: , - // start_time : start, - // end_time: end, - // bank_code_id: bank_code.data.id, - // } - // return { success: true data: normalized_preset_shift } - // } -}