From 383a01639aa71e5e3f13fcff5ee77e2fad2d9e89 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 11 Nov 2025 14:51:50 -0500 Subject: [PATCH 1/3] feat(Result): added the result pattern to handle errors at any level. Ajusted modules --- src/common/errors/result-error.factory.ts | 12 + .../domains/services/holiday.service.ts | 87 ++-- .../controllers/expense.controller.ts | 12 +- .../services/expense-upsert.service.ts | 61 +-- .../controllers/leave-requests.controller.ts | 32 +- .../leave-requests/leave-requests.module.ts | 28 +- .../holiday-leave-requests.service.ts | 139 +++--- .../services/leave-request.service.ts | 438 ++++++++--------- .../services/sick-leave-requests.service.ts | 170 +++---- .../vacation-leave-requests.service.ts | 162 +++--- .../time-and-attendance.module.ts | 6 +- .../schedule-presets-apply.service.ts | 69 +-- .../services/schedule-presets-get.service.ts | 13 +- .../schedule-presets-upsert.service.ts | 116 ++--- .../shifts/controllers/shift.controller.ts | 23 +- .../shifts/services/shifts-create.service.ts | 155 ++++++ .../services/shifts-update-delete.service.ts | 144 ++++++ .../shifts/services/shifts-upsert.service.ts | 463 ------------------ .../time-tracker/shifts/shifts.module.ts | 7 +- .../timesheet-get-overview.service.ts | 87 ++-- .../utils/date-time.utils.ts | 2 +- .../utils/resolve-email-id.utils.ts | 20 +- .../utils/resolve-timesheet.utils.ts | 8 +- 23 files changed, 1069 insertions(+), 1185 deletions(-) create mode 100644 src/common/errors/result-error.factory.ts create mode 100644 src/time-and-attendance/time-tracker/shifts/services/shifts-create.service.ts create mode 100644 src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service.ts delete mode 100644 src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts diff --git a/src/common/errors/result-error.factory.ts b/src/common/errors/result-error.factory.ts new file mode 100644 index 0000000..b498545 --- /dev/null +++ b/src/common/errors/result-error.factory.ts @@ -0,0 +1,12 @@ +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/time-and-attendance/domains/services/holiday.service.ts b/src/time-and-attendance/domains/services/holiday.service.ts index 5a0c2b7..e19ae64 100644 --- a/src/time-and-attendance/domains/services/holiday.service.ts +++ b/src/time-and-attendance/domains/services/holiday.service.ts @@ -1,8 +1,9 @@ -import { Injectable, Logger, NotFoundException } from "@nestjs/common"; +import { Injectable } from "@nestjs/common"; import { computeHours, getWeekStart } from "src/common/utils/date-utils"; import { PrismaService } from "src/prisma/prisma.service"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; import { MS_PER_WEEK } from "src/time-and-attendance/utils/constants.utils"; +import { Result } from "src/common/errors/result-error.factory"; /* le calcul est 1/20 des 4 dernières semaines, précédent la semaine incluant le férier. Un maximum de 08h00 est allouable pour le férier @@ -12,58 +13,62 @@ import { MS_PER_WEEK } from "src/time-and-attendance/utils/constants.utils"; @Injectable() export class HolidayService { - private readonly logger = new Logger(HolidayService.name); - constructor( private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver, - ) {} + ) { } - private async computeHoursPrevious4WeeksByEmail(email: string, holiday_date: Date): Promise { + private async computeHoursPrevious4WeeksByEmail(email: string, holiday_date: Date): Promise> { const employee_id = await this.emailResolver.findIdByEmail(email); - return this.computeHoursPrevious4Weeks(employee_id, holiday_date); + if (!employee_id.success) return { success: false, error: employee_id.error }; + return this.computeHoursPrevious4Weeks(employee_id.data, holiday_date); } - private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise { - const holiday_week_start = getWeekStart(holiday_date); - const window_start = new Date(holiday_week_start.getTime() - 4 * MS_PER_WEEK); - const window_end = new Date(holiday_week_start.getTime() - 1); + private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise> { + try { + const holiday_week_start = getWeekStart(holiday_date); + 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', 'G700']; - const shifts = await this.prisma.shifts.findMany({ - where: { - timesheet: { employee_id: employee_id }, - date: { gte: window_start, lte: window_end }, - bank_code: { bank_code: { in: valid_codes } }, - }, - select: { date: true, start_time: true, end_time: true }, - }); + const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G700']; + const shifts = await this.prisma.shifts.findMany({ + where: { + timesheet: { employee_id: employee_id }, + date: { gte: window_start, lte: window_end }, + bank_code: { bank_code: { in: valid_codes } }, + }, + select: { date: true, start_time: true, end_time: true }, + }); - const hours_by_week = new Map(); - for(const shift of shifts) { - const hours = computeHours(shift.start_time, shift.end_time); - if(hours <= 0) continue; - const shift_week_start = getWeekStart(shift.date); - const key = shift_week_start.getTime(); - hours_by_week.set(key, (hours_by_week.get(key) ?? 0) + hours); + const hours_by_week = new Map(); + for (const shift of shifts) { + const hours = computeHours(shift.start_time, shift.end_time); + if (hours <= 0) continue; + const shift_week_start = getWeekStart(shift.date); + const key = shift_week_start.getTime(); + hours_by_week.set(key, (hours_by_week.get(key) ?? 0) + hours); + } + + let capped_total = 0; + for (let offset = 1; offset <= 4; offset++) { + const week_start = new Date(holiday_week_start.getTime() - offset * MS_PER_WEEK); + const key = week_start.getTime(); + const weekly_hours = hours_by_week.get(key) ?? 0; + capped_total += Math.min(weekly_hours, 40); + } + + const average_daily_hours = capped_total / 20; + return { success: true, data: average_daily_hours }; + } catch (error) { + return { success: false, error: `an error occureded during holiday calculation` } } - - let capped_total = 0; - for(let offset = 1; offset <= 4; offset++) { - const week_start = new Date(holiday_week_start.getTime() - offset * MS_PER_WEEK); - const key = week_start.getTime(); - const weekly_hours = hours_by_week.get(key) ?? 0; - capped_total += Math.min(weekly_hours, 40); - } - - const average_daily_hours = capped_total / 20; - return average_daily_hours; } - async calculateHolidayPay( email: string, holiday_date: Date, modifier: number): Promise { + async calculateHolidayPay(email: string, holiday_date: Date, modifier: number): Promise> { const average_daily_hours = await this.computeHoursPrevious4WeeksByEmail(email, holiday_date); - const daily_rate = Math.min(average_daily_hours, 8); - this.logger.debug(`Holiday pay calculation: cappedHoursPerDay= ${average_daily_hours.toFixed(2)}, appliedDailyRate= ${daily_rate.toFixed(2)}`); - return daily_rate * modifier; + if (!average_daily_hours.success) return { success: false, error: average_daily_hours.error }; + + const daily_rate = (Math.min(average_daily_hours.data, 8)) * modifier; + return { success: true, data: daily_rate }; } } \ No newline at end of file diff --git a/src/time-and-attendance/expenses/controllers/expense.controller.ts b/src/time-and-attendance/expenses/controllers/expense.controller.ts index 9d9b08d..fe37b0e 100644 --- a/src/time-and-attendance/expenses/controllers/expense.controller.ts +++ b/src/time-and-attendance/expenses/controllers/expense.controller.ts @@ -4,26 +4,28 @@ import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/ import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { GLOBAL_CONTROLLER_ROLES } from "src/common/shared/role-groupes"; +import { Result } from "src/common/errors/result-error.factory"; +import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto"; @Controller('expense') @RolesAllowed(...GLOBAL_CONTROLLER_ROLES) export class ExpenseController { - constructor( private readonly upsert_service: ExpenseUpsertService ){} + constructor(private readonly upsert_service: ExpenseUpsertService) { } @Post('create') - create( @Req() req, @Body() dto: ExpenseDto): Promise{ + create(@Req() req, @Body() dto: ExpenseDto): Promise> { const email = req.user?.email; - if(!email) throw new UnauthorizedException('Unauthorized User'); + if (!email) throw new UnauthorizedException('Unauthorized User'); return this.upsert_service.createExpense(dto, email); } @Patch('update') - update(@Body() dto: ExpenseDto): Promise{ + update(@Body() dto: ExpenseDto): Promise> { return this.upsert_service.updateExpense(dto); } @Delete('delete/:expense_id') - remove(@Param('expense_id') expense_id: number) { + remove(@Param('expense_id') expense_id: number): Promise> { return this.upsert_service.deleteExpense(expense_id); } } diff --git a/src/time-and-attendance/expenses/services/expense-upsert.service.ts b/src/time-and-attendance/expenses/services/expense-upsert.service.ts index 89c2b44..6a52f76 100644 --- a/src/time-and-attendance/expenses/services/expense-upsert.service.ts +++ b/src/time-and-attendance/expenses/services/expense-upsert.service.ts @@ -1,12 +1,13 @@ -import { CreateExpenseResult, DeleteExpenseResult, NormalizedExpense } from "src/time-and-attendance/utils/type.utils"; import { toDateFromString, toStringFromDate, weekStartSunday } from "src/time-and-attendance/utils/date-time.utils"; -import { Injectable, NotFoundException } from "@nestjs/common"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { NormalizedExpense } from "src/time-and-attendance/utils/type.utils"; import { expense_select } from "src/time-and-attendance/utils/selects.utils"; import { PrismaService } from "src/prisma/prisma.service"; -import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto"; -import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; import { ExpenseEntity } from "src/time-and-attendance/expenses/dtos/expense-entity.dto"; +import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; +import { Injectable } from "@nestjs/common"; +import { Result } from "src/common/errors/result-error.factory"; @Injectable() @@ -19,10 +20,11 @@ export class ExpenseUpsertService { //_________________________________________________________________ // CREATE //_________________________________________________________________ - async createExpense(dto: ExpenseDto, email: string): Promise { + 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 = this.normalizeAndParseExpenseDto(dto); @@ -30,10 +32,10 @@ export class ExpenseUpsertService { //finds the timesheet using expense.date by finding the sunday const start_date = weekStartSunday(normed_expense.date); const timesheet = await this.prisma.timesheets.findFirst({ - where: { start_date, employee_id }, + where: { start_date, employee_id: employee_id.data }, select: { id: true, employee_id: true }, }); - if (!timesheet) throw new NotFoundException(`Timesheet with id ${dto.timesheet_id} not found`); + if (!timesheet) return { success: false, error: `Timesheet with id : ${dto.timesheet_id} not found` }; //create a new expense const expense = await this.prisma.expenses.create({ @@ -46,6 +48,7 @@ export class ExpenseUpsertService { //return the newly created expense with id select: expense_select, }); + if (!expense) return { success: false, error: `An error occured during creation. Expense is invalid` }; //build an object to return to the frontend to display const created: GetExpenseDto = { @@ -56,17 +59,17 @@ export class ExpenseUpsertService { attachment: expense.attachment ?? undefined, supervisor_comment: expense.supervisor_comment ?? undefined, }; - return { ok: true, data: created } + return { success: true, data: created }; } catch (error) { - return { ok: false, error: error } + return { success: false, error: `An error occured during creation. Expense not created : ` + error }; } } //_________________________________________________________________ // UPDATE //_________________________________________________________________ - async updateExpense(dto: ExpenseDto): Promise { + async updateExpense(dto: ExpenseDto): Promise> { try { //normalize string , date format and parse numbers const normed_expense = this.normalizeAndParseExpenseDto(dto); @@ -79,6 +82,7 @@ export class ExpenseUpsertService { bank_code_id: dto.bank_code_id, is_approved: dto.is_approved, }; + if (!data) return { success: false, error: `An error occured during normalization. Expense with id: ${dto.id} is invalid` } //push updates and get updated datas const expense = await this.prisma.expenses.update({ @@ -86,6 +90,7 @@ export class ExpenseUpsertService { data, select: expense_select, }); + if (!expense) return { success: false, error: `An error occured during update. Expense with id: ${data.id} was not updated` } //build an object to return to the frontend const updated: GetExpenseDto = { @@ -96,29 +101,29 @@ export class ExpenseUpsertService { attachment: expense.attachment ?? undefined, supervisor_comment: expense.supervisor_comment ?? undefined, }; - return updated; + return { success: true, data: updated }; } catch (error) { - return error; + return { success: false, error: (`Expense with id: ${dto.id} generated an error:` + error) }; } } //_________________________________________________________________ // DELETE //_________________________________________________________________ - async deleteExpense(expense_id: number): Promise { + 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) throw new NotFoundException(`Expense with id: ${expense_id} not found`); + if (!expense) return { success: false, error: `An error occured during removal. Expense with id :${expense_id} was not found ` }; await tx.expenses.delete({ where: { id: expense_id } }); return { success: true }; }); - return { ok: true, id: expense_id }; + return { success: true, data: expense_id }; } catch (error) { - return { ok: false, id: expense_id, error }; + return { success: false, error: `An error occured during removal. Expense with id :${expense_id} generated an error: ` + error }; } } @@ -127,20 +132,20 @@ export class ExpenseUpsertService { //_________________________________________________________________ //makes sure that comments are the right length the date is of Date type private normalizeAndParseExpenseDto(dto: ExpenseDto): NormalizedExpense { - const parsed_attachment = this.parseOptionalNumber(dto.attachment, "attachment"); - const parsed_mileage = this.parseOptionalNumber(dto.mileage, "mileage"); - const parsed_amount = this.parseOptionalNumber(dto.amount, "amount"); - const comment = this.truncate280(dto.comment); + const parsed_attachment = this.parseOptionalNumber(dto.attachment, "attachment"); + const parsed_mileage = this.parseOptionalNumber(dto.mileage, "mileage"); + const parsed_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; + ? this.truncate280(dto.supervisor_comment.trim()) : undefined; const date = toDateFromString(dto.date); - return { - date, - comment, - supervisor_comment, - parsed_amount, - parsed_attachment, - parsed_mileage + return { + date, + comment, + supervisor_comment, + parsed_amount, + parsed_attachment, + parsed_mileage }; } diff --git a/src/time-and-attendance/leave-requests/controllers/leave-requests.controller.ts b/src/time-and-attendance/leave-requests/controllers/leave-requests.controller.ts index 62a14c4..2094ba4 100644 --- a/src/time-and-attendance/leave-requests/controllers/leave-requests.controller.ts +++ b/src/time-and-attendance/leave-requests/controllers/leave-requests.controller.ts @@ -1,20 +1,20 @@ -import { Body, Controller, Post } from "@nestjs/common"; -import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; -import { UpsertLeaveRequestDto } from "../dtos/upsert-leave-request.dto"; -import { LeaveRequestsService } from "../services/leave-request.service"; +// import { Body, Controller, Post } from "@nestjs/common"; +// import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +// import { UpsertLeaveRequestDto } from "../dtos/upsert-leave-request.dto"; +// import { LeaveRequestsService } from "../services/leave-request.service"; -@ApiTags('Leave Requests') -@ApiBearerAuth('access-token') -// @UseGuards() -@Controller('leave-requests') -export class LeaveRequestController { - constructor(private readonly leave_service: LeaveRequestsService){} +// @ApiTags('Leave Requests') +// @ApiBearerAuth('access-token') +// // @UseGuards() +// @Controller('leave-requests') +// export class LeaveRequestController { +// constructor(private readonly leave_service: LeaveRequestsService){} - @Post('upsert') - async upsertLeaveRequest(@Body() dto: UpsertLeaveRequestDto) { - const { action, leave_requests } = await this.leave_service.handle(dto); - return { action, leave_requests }; - } +// @Post('upsert') +// async upsertLeaveRequest(@Body() dto: UpsertLeaveRequestDto) { +// const { action, leave_requests } = await this.leave_service.handle(dto); +// return { action, leave_requests }; +// } -} +// } diff --git a/src/time-and-attendance/leave-requests/leave-requests.module.ts b/src/time-and-attendance/leave-requests/leave-requests.module.ts index eff4f6d..186441a 100644 --- a/src/time-and-attendance/leave-requests/leave-requests.module.ts +++ b/src/time-and-attendance/leave-requests/leave-requests.module.ts @@ -1,16 +1,16 @@ -import { LeaveRequestController } from "src/time-and-attendance/leave-requests/controllers/leave-requests.controller"; -import { LeaveRequestsService } from "src/time-and-attendance/leave-requests/services/leave-request.service"; -import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module"; -import { ShiftsModule } from "src/time-and-attendance/time-tracker/shifts/shifts.module"; -import { Module } from "@nestjs/common"; +// import { LeaveRequestController } from "src/time-and-attendance/leave-requests/controllers/leave-requests.controller"; +// import { LeaveRequestsService } from "src/time-and-attendance/leave-requests/services/leave-request.service"; +// import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module"; +// import { ShiftsModule } from "src/time-and-attendance/time-tracker/shifts/shifts.module"; +// import { Module } from "@nestjs/common"; -@Module({ - imports: [ - BusinessLogicsModule, - ShiftsModule, - ], - controllers: [LeaveRequestController], - providers: [LeaveRequestsService], -}) +// @Module({ +// imports: [ +// BusinessLogicsModule, +// ShiftsModule, +// ], +// controllers: [LeaveRequestController], +// providers: [LeaveRequestsService], +// }) -export class LeaveRequestsModule {} \ No newline at end of file +// export class LeaveRequestsModule {} \ No newline at end of file diff --git a/src/time-and-attendance/leave-requests/services/holiday-leave-requests.service.ts b/src/time-and-attendance/leave-requests/services/holiday-leave-requests.service.ts index cf18c2e..ef9bf52 100644 --- a/src/time-and-attendance/leave-requests/services/holiday-leave-requests.service.ts +++ b/src/time-and-attendance/leave-requests/services/holiday-leave-requests.service.ts @@ -1,79 +1,84 @@ -import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; -import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto"; -import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client"; -import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; -import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; -import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util"; -import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; -import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; -import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service"; -import { PrismaService } from "src/prisma/prisma.service"; -import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; -import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils"; +// import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; +// import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto"; +// import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client"; +// import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; +// import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; +// import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util"; +// import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +// import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +// import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service"; +// import { PrismaService } from "src/prisma/prisma.service"; +// import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; +// import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils"; +// import { Result } from "src/common/errors/result-error.factory"; -@Injectable() -export class HolidayLeaveRequestsService { - constructor( - private readonly prisma: PrismaService, - private readonly holidayService: HolidayService, - private readonly leaveUtils: LeaveRequestsUtils, - private readonly emailResolver: EmailToIdResolver, - private readonly typeResolver: BankCodesResolver, - ) {} +// @Injectable() +// export class HolidayLeaveRequestsService { +// constructor( +// private readonly prisma: PrismaService, +// private readonly holidayService: HolidayService, +// private readonly leaveUtils: LeaveRequestsUtils, +// private readonly emailResolver: EmailToIdResolver, +// private readonly typeResolver: BankCodesResolver, +// ) { } - async create(dto: UpsertLeaveRequestDto): Promise { - const email = dto.email.trim(); - const employee_id = await this.emailResolver.findIdByEmail(email); - const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.HOLIDAY); - const dates = normalizeDates(dto.dates); - if (!bank_code) throw new NotFoundException(`bank_code not found`); - if (!dates.length) throw new BadRequestException('Dates array must not be empty'); +// async create(dto: UpsertLeaveRequestDto): Promise> { +// const email = dto.email.trim(); +// const employee_id = await this.emailResolver.findIdByEmail(email); +// if (!employee_id.success) return { success: false, error: employee_id.error } - const created: LeaveRequestViewDto[] = []; +// const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.HOLIDAY); +// const dates = normalizeDates(dto.dates); +// if (!bank_code) throw new NotFoundException(`bank_code not found`); +// if (!dates.length) throw new BadRequestException('Dates array must not be empty'); - for (const iso_date of dates) { - const date = toDateOnly(iso_date); +// const created: LeaveRequestViewDto[] = []; - const existing = await this.prisma.leaveRequests.findUnique({ - where: { - leave_per_employee_date: { - employee_id: employee_id, - leave_type: LeaveTypes.HOLIDAY, - date, - }, - }, - select: { id: true }, - }); - if (existing) { - throw new BadRequestException(`Holiday request already exists for ${iso_date}`); - } +// for (const iso_date of dates) { +// const date = toDateOnly(iso_date); - const payable = await this.holidayService.calculateHolidayPay(email, date, bank_code.modifier); - const row = await this.prisma.leaveRequests.create({ - data: { - employee_id: employee_id, - bank_code_id: bank_code.id, - leave_type: LeaveTypes.HOLIDAY, - date, - comment: dto.comment ?? '', - requested_hours: dto.requested_hours ?? 8, - payable_hours: payable, - approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING, - }, - select: leaveRequestsSelect, - }); +// const existing = await this.prisma.leaveRequests.findUnique({ +// where: { +// leave_per_employee_date: { +// employee_id: employee_id.data, +// leave_type: LeaveTypes.HOLIDAY, +// date, +// }, +// }, +// select: { id: true }, +// }); +// if (existing) { +// throw new BadRequestException(`Holiday request already exists for ${iso_date}`); +// } - const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); - if (row.approval_status === LeaveApprovalStatus.APPROVED) { - await this.leaveUtils.syncShift(email, employee_id, iso_date, hours,LeaveTypes.HOLIDAY, row.comment); - } +// const payable = await this.holidayService.calculateHolidayPay(email, date, bank_code.modifier); +// if (!payable) return { success: false, error: `An error occured during calculation` }; - created.push({ ...mapRowToView(row), action: 'create' }); - } +// const row = await this.prisma.leaveRequests.create({ +// data: { +// employee_id: employee_id.data, +// bank_code_id: bank_code.id, +// leave_type: LeaveTypes.HOLIDAY, +// date, +// comment: dto.comment ?? '', +// requested_hours: dto.requested_hours ?? 8, +// payable_hours: payable, +// approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING, +// }, +// select: leaveRequestsSelect, +// }); - return { action: 'create', leave_requests: created }; - } -} +// const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); +// if (row.approval_status === LeaveApprovalStatus.APPROVED) { +// await this.leaveUtils.syncShift(email, employee_id.data, iso_date, hours, LeaveTypes.HOLIDAY, row.comment); +// } + +// created.push({ ...mapRowToView(row), action: 'create' }); +// } + +// return { success: true, data: { action: 'create', leave_requests: created } }; +// } +// } diff --git a/src/time-and-attendance/leave-requests/services/leave-request.service.ts b/src/time-and-attendance/leave-requests/services/leave-request.service.ts index 8b9d7d9..bd7a6b2 100644 --- a/src/time-and-attendance/leave-requests/services/leave-request.service.ts +++ b/src/time-and-attendance/leave-requests/services/leave-request.service.ts @@ -1,241 +1,241 @@ -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto"; -import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client"; -import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; -import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; -import { roundToQuarterHour } from "src/common/utils/date-utils"; -import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util"; -import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; -import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; -import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service"; -import { VacationService } from "src/time-and-attendance/domains/services/vacation.service"; -import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service"; -import { PrismaService } from "src/prisma/prisma.service"; -import { mapRowToView } from "../mappers/leave-requests.mapper"; -import { normalizeDates, toDateOnly, toISODateKey } from "src/time-and-attendance/utils/date-time.utils"; -@Injectable() -export class LeaveRequestsService { - constructor( - private readonly prisma: PrismaService, - private readonly holidayService: HolidayService, - private readonly sickLogic: SickLeaveService, - private readonly vacationLogic: VacationService, - private readonly leaveUtils: LeaveRequestsUtils, - private readonly emailResolver: EmailToIdResolver, - private readonly typeResolver: BankCodesResolver, - ) {} +// import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +// import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto"; +// import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client"; +// import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; +// import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; +// import { roundToQuarterHour } from "src/common/utils/date-utils"; +// import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util"; +// import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +// import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +// import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service"; +// import { VacationService } from "src/time-and-attendance/domains/services/vacation.service"; +// import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service"; +// import { PrismaService } from "src/prisma/prisma.service"; +// import { mapRowToView } from "../mappers/leave-requests.mapper"; +// import { normalizeDates, toDateOnly, toISODateKey } from "src/time-and-attendance/utils/date-time.utils"; +// @Injectable() +// export class LeaveRequestsService { +// constructor( +// private readonly prisma: PrismaService, +// private readonly holidayService: HolidayService, +// private readonly sickLogic: SickLeaveService, +// private readonly vacationLogic: VacationService, +// private readonly leaveUtils: LeaveRequestsUtils, +// private readonly emailResolver: EmailToIdResolver, +// private readonly typeResolver: BankCodesResolver, +// ) {} - // handle distribution to the right service according to the selected type and action - async handle(dto: UpsertLeaveRequestDto): Promise { - switch (dto.type) { - case LeaveTypes.HOLIDAY: - if( dto.action === 'create'){ - // return this.holidayService.create(dto); - } else if (dto.action === 'update') { - return this.update(dto, LeaveTypes.HOLIDAY); - } else if (dto.action === 'delete'){ - return this.delete(dto, LeaveTypes.HOLIDAY); - } - case LeaveTypes.VACATION: - if( dto.action === 'create'){ - // return this.vacationService.create(dto); - } else if (dto.action === 'update') { - return this.update(dto, LeaveTypes.VACATION); - } else if (dto.action === 'delete'){ - return this.delete(dto, LeaveTypes.VACATION); - } - case LeaveTypes.SICK: - if( dto.action === 'create'){ - // return this.sickLeaveService.create(dto); - } else if (dto.action === 'update') { - return this.update(dto, LeaveTypes.SICK); - } else if (dto.action === 'delete'){ - return this.delete(dto, LeaveTypes.SICK); - } - default: - throw new BadRequestException(`Unsupported leave type: ${dto.type} or action: ${dto.action}`); - } - } +// // handle distribution to the right service according to the selected type and action +// async handle(dto: UpsertLeaveRequestDto): Promise { +// switch (dto.type) { +// case LeaveTypes.HOLIDAY: +// if( dto.action === 'create'){ +// // return this.holidayService.create(dto); +// } else if (dto.action === 'update') { +// return this.update(dto, LeaveTypes.HOLIDAY); +// } else if (dto.action === 'delete'){ +// return this.delete(dto, LeaveTypes.HOLIDAY); +// } +// case LeaveTypes.VACATION: +// if( dto.action === 'create'){ +// // return this.vacationService.create(dto); +// } else if (dto.action === 'update') { +// return this.update(dto, LeaveTypes.VACATION); +// } else if (dto.action === 'delete'){ +// return this.delete(dto, LeaveTypes.VACATION); +// } +// case LeaveTypes.SICK: +// if( dto.action === 'create'){ +// // return this.sickLeaveService.create(dto); +// } else if (dto.action === 'update') { +// return this.update(dto, LeaveTypes.SICK); +// } else if (dto.action === 'delete'){ +// return this.delete(dto, LeaveTypes.SICK); +// } +// default: +// throw new BadRequestException(`Unsupported leave type: ${dto.type} or action: ${dto.action}`); +// } +// } - async delete(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise { - const email = dto.email.trim(); - const dates = normalizeDates(dto.dates); - const employee_id = await this.emailResolver.findIdByEmail(email); - if (!dates.length) throw new BadRequestException("Dates array must not be empty"); +// async delete(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise { +// const email = dto.email.trim(); +// const dates = normalizeDates(dto.dates); +// const employee_id = await this.emailResolver.findIdByEmail(email); +// if (!dates.length) throw new BadRequestException("Dates array must not be empty"); - const rows = await this.prisma.leaveRequests.findMany({ - where: { - employee_id: employee_id, - leave_type: type, - date: { in: dates.map((d) => toDateOnly(d)) }, - }, - select: leaveRequestsSelect, - }); +// const rows = await this.prisma.leaveRequests.findMany({ +// where: { +// employee_id: employee_id, +// leave_type: type, +// date: { in: dates.map((d) => toDateOnly(d)) }, +// }, +// select: leaveRequestsSelect, +// }); - if (rows.length !== dates.length) { - const missing = dates.filter((isoDate) => !rows.some((row) => toISODateKey(row.date) === isoDate)); - throw new NotFoundException(`No Leave request found for: ${missing.join(", ")}`); - } +// if (rows.length !== dates.length) { +// const missing = dates.filter((isoDate) => !rows.some((row) => toISODateKey(row.date) === isoDate)); +// throw new NotFoundException(`No Leave request found for: ${missing.join(", ")}`); +// } - for (const row of rows) { - if (row.approval_status === LeaveApprovalStatus.APPROVED) { - const iso = toISODateKey(row.date); - await this.leaveUtils.removeShift(email, employee_id, iso, type); - } - } +// for (const row of rows) { +// if (row.approval_status === LeaveApprovalStatus.APPROVED) { +// const iso = toISODateKey(row.date); +// await this.leaveUtils.removeShift(email, employee_id, iso, type); +// } +// } - await this.prisma.leaveRequests.deleteMany({ - where: { id: { in: rows.map((row) => row.id) } }, - }); +// await this.prisma.leaveRequests.deleteMany({ +// where: { id: { in: rows.map((row) => row.id) } }, +// }); - const deleted = rows.map((row) => ({ ...mapRowToView(row), action: "delete" as const })); - return { action: "delete", leave_requests: deleted }; - } +// const deleted = rows.map((row) => ({ ...mapRowToView(row), action: "delete" as const })); +// return { action: "delete", leave_requests: deleted }; +// } - async update(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise { - const email = dto.email.trim(); - const employee_id = await this.emailResolver.findIdByEmail(email); - const bank_code = await this.typeResolver.findIdAndModifierByType(type); - if(!bank_code) throw new NotFoundException(`bank_code not found`); - const modifier = Number(bank_code.modifier ?? 1); - const dates = normalizeDates(dto.dates); - if (!dates.length) { - throw new BadRequestException("Dates array must not be empty"); - } +// async update(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise { +// const email = dto.email.trim(); +// const employee_id = await this.emailResolver.findIdByEmail(email); +// const bank_code = await this.typeResolver.findIdAndModifierByType(type); +// if(!bank_code) throw new NotFoundException(`bank_code not found`); +// const modifier = Number(bank_code.modifier ?? 1); +// const dates = normalizeDates(dto.dates); +// if (!dates.length) { +// throw new BadRequestException("Dates array must not be empty"); +// } - const entries = await Promise.all( - dates.map(async (iso_date) => { - const date = toDateOnly(iso_date); - const existing = await this.prisma.leaveRequests.findUnique({ - where: { - leave_per_employee_date: { - employee_id: employee_id, - leave_type: type, - date, - }, - }, - select: leaveRequestsSelect, - }); - if (!existing) throw new NotFoundException(`No Leave request found for ${iso_date}`); - return { iso_date, date, existing }; - }), - ); +// const entries = await Promise.all( +// dates.map(async (iso_date) => { +// const date = toDateOnly(iso_date); +// const existing = await this.prisma.leaveRequests.findUnique({ +// where: { +// leave_per_employee_date: { +// employee_id: employee_id, +// leave_type: type, +// date, +// }, +// }, +// select: leaveRequestsSelect, +// }); +// if (!existing) throw new NotFoundException(`No Leave request found for ${iso_date}`); +// return { iso_date, date, existing }; +// }), +// ); - const updated: LeaveRequestViewDto[] = []; +// const updated: LeaveRequestViewDto[] = []; - if (type === LeaveTypes.SICK) { - const firstExisting = entries[0].existing; - const fallbackRequested = - firstExisting.requested_hours !== null && firstExisting.requested_hours !== undefined - ? Number(firstExisting.requested_hours) - : 8; - const requested_hours_per_day = dto.requested_hours ?? fallbackRequested; - const reference_date = entries.reduce( - (latest, entry) => (entry.date > latest ? entry.date : latest), - entries[0].date, - ); - const total_payable_hours = await this.sickLogic.calculateSickLeavePay( - employee_id, - reference_date, - entries.length, - requested_hours_per_day, - modifier, - ); - let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours)); - const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); +// if (type === LeaveTypes.SICK) { +// const firstExisting = entries[0].existing; +// const fallbackRequested = +// firstExisting.requested_hours !== null && firstExisting.requested_hours !== undefined +// ? Number(firstExisting.requested_hours) +// : 8; +// const requested_hours_per_day = dto.requested_hours ?? fallbackRequested; +// const reference_date = entries.reduce( +// (latest, entry) => (entry.date > latest ? entry.date : latest), +// entries[0].date, +// ); +// const total_payable_hours = await this.sickLogic.calculateSickLeavePay( +// employee_id, +// reference_date, +// entries.length, +// requested_hours_per_day, +// modifier, +// ); +// let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours)); +// const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); - for (const { iso_date, existing } of entries) { - const previous_status = existing.approval_status; - const payable = Math.min(remaining_payable_hours, daily_payable_cap); - const payable_rounded = roundToQuarterHour(Math.max(0, payable)); - remaining_payable_hours = roundToQuarterHour( - Math.max(0, remaining_payable_hours - payable_rounded), - ); +// for (const { iso_date, existing } of entries) { +// const previous_status = existing.approval_status; +// const payable = Math.min(remaining_payable_hours, daily_payable_cap); +// const payable_rounded = roundToQuarterHour(Math.max(0, payable)); +// remaining_payable_hours = roundToQuarterHour( +// Math.max(0, remaining_payable_hours - payable_rounded), +// ); - const row = await this.prisma.leaveRequests.update({ - where: { id: existing.id }, - data: { - comment: dto.comment ?? existing.comment, - requested_hours: requested_hours_per_day, - payable_hours: payable_rounded, - bank_code_id: bank_code.id, - approval_status: dto.approval_status ?? existing.approval_status, - }, - select: leaveRequestsSelect, - }); +// const row = await this.prisma.leaveRequests.update({ +// where: { id: existing.id }, +// data: { +// comment: dto.comment ?? existing.comment, +// requested_hours: requested_hours_per_day, +// payable_hours: payable_rounded, +// bank_code_id: bank_code.id, +// approval_status: dto.approval_status ?? existing.approval_status, +// }, +// select: leaveRequestsSelect, +// }); - const was_approved = previous_status === LeaveApprovalStatus.APPROVED; - const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED; - const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); +// const was_approved = previous_status === LeaveApprovalStatus.APPROVED; +// const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED; +// const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); - if (!was_approved && is_approved) { - await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); - } else if (was_approved && !is_approved) { - await this.leaveUtils.removeShift(email, employee_id, iso_date, type); - } else if (was_approved && is_approved) { - await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); - } - updated.push({ ...mapRowToView(row), action: "update" }); - } - return { action: "update", leave_requests: updated }; - } +// if (!was_approved && is_approved) { +// await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); +// } else if (was_approved && !is_approved) { +// await this.leaveUtils.removeShift(email, employee_id, iso_date, type); +// } else if (was_approved && is_approved) { +// await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); +// } +// updated.push({ ...mapRowToView(row), action: "update" }); +// } +// return { action: "update", leave_requests: updated }; +// } - for (const { iso_date, date, existing } of entries) { - const previous_status = existing.approval_status; - const fallbackRequested = - existing.requested_hours !== null && existing.requested_hours !== undefined - ? Number(existing.requested_hours) - : 8; - const requested_hours = dto.requested_hours ?? fallbackRequested; +// for (const { iso_date, date, existing } of entries) { +// const previous_status = existing.approval_status; +// const fallbackRequested = +// existing.requested_hours !== null && existing.requested_hours !== undefined +// ? Number(existing.requested_hours) +// : 8; +// const requested_hours = dto.requested_hours ?? fallbackRequested; - let payable: number; - switch (type) { - case LeaveTypes.HOLIDAY: - payable = await this.holidayService.calculateHolidayPay(email, date, modifier); - break; - case LeaveTypes.VACATION: { - const days_requested = requested_hours / 8; - payable = await this.vacationLogic.calculateVacationPay( - employee_id, - date, - Math.max(0, days_requested), - modifier, - ); - break; - } - default: - payable = existing.payable_hours !== null && existing.payable_hours !== undefined - ? Number(existing.payable_hours) - : requested_hours; - } +// let payable: number; +// switch (type) { +// case LeaveTypes.HOLIDAY: +// payable = await this.holidayService.calculateHolidayPay(email, date, modifier); +// break; +// case LeaveTypes.VACATION: { +// const days_requested = requested_hours / 8; +// payable = await this.vacationLogic.calculateVacationPay( +// employee_id, +// date, +// Math.max(0, days_requested), +// modifier, +// ); +// break; +// } +// default: +// payable = existing.payable_hours !== null && existing.payable_hours !== undefined +// ? Number(existing.payable_hours) +// : requested_hours; +// } - const row = await this.prisma.leaveRequests.update({ - where: { id: existing.id }, - data: { - requested_hours, - comment: dto.comment ?? existing.comment, - payable_hours: payable, - bank_code_id: bank_code.id, - approval_status: dto.approval_status ?? existing.approval_status, - }, - select: leaveRequestsSelect, - }); +// const row = await this.prisma.leaveRequests.update({ +// where: { id: existing.id }, +// data: { +// requested_hours, +// comment: dto.comment ?? existing.comment, +// payable_hours: payable, +// bank_code_id: bank_code.id, +// approval_status: dto.approval_status ?? existing.approval_status, +// }, +// select: leaveRequestsSelect, +// }); - const was_approved = previous_status === LeaveApprovalStatus.APPROVED; - const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED; - const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); +// const was_approved = previous_status === LeaveApprovalStatus.APPROVED; +// const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED; +// const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); - if (!was_approved && is_approved) { - await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); - } else if (was_approved && !is_approved) { - await this.leaveUtils.removeShift(email, employee_id, iso_date, type); - } else if (was_approved && is_approved) { - await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); - } - updated.push({ ...mapRowToView(row), action: "update" }); - } - return { action: "update", leave_requests: updated }; - } -} +// if (!was_approved && is_approved) { +// await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); +// } else if (was_approved && !is_approved) { +// await this.leaveUtils.removeShift(email, employee_id, iso_date, type); +// } else if (was_approved && is_approved) { +// await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); +// } +// updated.push({ ...mapRowToView(row), action: "update" }); +// } +// return { action: "update", leave_requests: updated }; +// } +// } diff --git a/src/time-and-attendance/leave-requests/services/sick-leave-requests.service.ts b/src/time-and-attendance/leave-requests/services/sick-leave-requests.service.ts index 0b35f88..0668f50 100644 --- a/src/time-and-attendance/leave-requests/services/sick-leave-requests.service.ts +++ b/src/time-and-attendance/leave-requests/services/sick-leave-requests.service.ts @@ -1,98 +1,98 @@ -import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; -import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto"; -import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client"; -import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; -import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; -import { roundToQuarterHour } from "src/common/utils/date-utils"; -import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; -import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; -import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service"; -import { PrismaService } from "src/prisma/prisma.service"; -import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; -import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils"; +// import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; +// import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto"; +// import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client"; +// import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; +// import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; +// import { roundToQuarterHour } from "src/common/utils/date-utils"; +// import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +// import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +// import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service"; +// import { PrismaService } from "src/prisma/prisma.service"; +// import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; +// import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils"; -@Injectable() -export class SickLeaveRequestsService { - constructor( - private readonly prisma: PrismaService, - private readonly sickService: SickLeaveService, - private readonly emailResolver: EmailToIdResolver, - private readonly typeResolver: BankCodesResolver, - ) {} +// @Injectable() +// export class SickLeaveRequestsService { +// constructor( +// private readonly prisma: PrismaService, +// private readonly sickService: SickLeaveService, +// private readonly emailResolver: EmailToIdResolver, +// private readonly typeResolver: BankCodesResolver, +// ) {} - async create(dto: UpsertLeaveRequestDto): Promise { - const email = dto.email.trim(); - const employee_id = await this.emailResolver.findIdByEmail(email); - const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.SICK); - if(!bank_code) throw new NotFoundException(`bank_code not found`); +// async create(dto: UpsertLeaveRequestDto): Promise { +// const email = dto.email.trim(); +// const employee_id = await this.emailResolver.findIdByEmail(email); +// const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.SICK); +// if(!bank_code) throw new NotFoundException(`bank_code not found`); - const modifier = bank_code.modifier ?? 1; - const dates = normalizeDates(dto.dates); - if (!dates.length) throw new BadRequestException("Dates array must not be empty"); - const requested_hours_per_day = dto.requested_hours ?? 8; +// const modifier = bank_code.modifier ?? 1; +// const dates = normalizeDates(dto.dates); +// if (!dates.length) throw new BadRequestException("Dates array must not be empty"); +// const requested_hours_per_day = dto.requested_hours ?? 8; - const entries = dates.map((iso) => ({ iso, date: toDateOnly(iso) })); - const reference_date = entries.reduce( - (latest, entry) => (entry.date > latest ? entry.date : latest), - entries[0].date, - ); - const total_payable_hours = await this.sickService.calculateSickLeavePay( - employee_id, - reference_date, - entries.length, - requested_hours_per_day, - modifier, - ); - let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours)); - const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); +// const entries = dates.map((iso) => ({ iso, date: toDateOnly(iso) })); +// const reference_date = entries.reduce( +// (latest, entry) => (entry.date > latest ? entry.date : latest), +// entries[0].date, +// ); +// const total_payable_hours = await this.sickService.calculateSickLeavePay( +// employee_id, +// reference_date, +// entries.length, +// requested_hours_per_day, +// modifier, +// ); +// let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours)); +// const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); - const created: LeaveRequestViewDto[] = []; +// const created: LeaveRequestViewDto[] = []; - for (const { iso, date } of entries) { - const existing = await this.prisma.leaveRequests.findUnique({ - where: { - leave_per_employee_date: { - employee_id: employee_id, - leave_type: LeaveTypes.SICK, - date, - }, - }, - select: { id: true }, - }); - if (existing) { - throw new BadRequestException(`Sick request already exists for ${iso}`); - } +// for (const { iso, date } of entries) { +// const existing = await this.prisma.leaveRequests.findUnique({ +// where: { +// leave_per_employee_date: { +// employee_id: employee_id, +// leave_type: LeaveTypes.SICK, +// date, +// }, +// }, +// select: { id: true }, +// }); +// if (existing) { +// throw new BadRequestException(`Sick request already exists for ${iso}`); +// } - const payable = Math.min(remaining_payable_hours, daily_payable_cap); - const payable_rounded = roundToQuarterHour(Math.max(0, payable)); - remaining_payable_hours = roundToQuarterHour( - Math.max(0, remaining_payable_hours - payable_rounded), - ); +// const payable = Math.min(remaining_payable_hours, daily_payable_cap); +// const payable_rounded = roundToQuarterHour(Math.max(0, payable)); +// remaining_payable_hours = roundToQuarterHour( +// Math.max(0, remaining_payable_hours - payable_rounded), +// ); - const row = await this.prisma.leaveRequests.create({ - data: { - employee_id: employee_id, - bank_code_id: bank_code.id, - leave_type: LeaveTypes.SICK, - comment: dto.comment ?? "", - requested_hours: requested_hours_per_day, - payable_hours: payable_rounded, - approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING, - date, - }, - select: leaveRequestsSelect, - }); +// const row = await this.prisma.leaveRequests.create({ +// data: { +// employee_id: employee_id, +// bank_code_id: bank_code.id, +// leave_type: LeaveTypes.SICK, +// comment: dto.comment ?? "", +// requested_hours: requested_hours_per_day, +// payable_hours: payable_rounded, +// approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING, +// date, +// }, +// select: leaveRequestsSelect, +// }); - const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); - if (row.approval_status === LeaveApprovalStatus.APPROVED) { - // await this.leaveUtils.syncShift(email, employee_id, iso, hours,LeaveTypes.SICK, row.comment); - } +// const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); +// if (row.approval_status === LeaveApprovalStatus.APPROVED) { +// // await this.leaveUtils.syncShift(email, employee_id, iso, hours,LeaveTypes.SICK, row.comment); +// } - created.push({ ...mapRowToView(row), action: "create" }); - } +// created.push({ ...mapRowToView(row), action: "create" }); +// } - return { action: "create", leave_requests: created }; - } -} +// return { action: "create", leave_requests: created }; +// } +// } diff --git a/src/time-and-attendance/leave-requests/services/vacation-leave-requests.service.ts b/src/time-and-attendance/leave-requests/services/vacation-leave-requests.service.ts index 63e0077..a823d75 100644 --- a/src/time-and-attendance/leave-requests/services/vacation-leave-requests.service.ts +++ b/src/time-and-attendance/leave-requests/services/vacation-leave-requests.service.ts @@ -1,91 +1,91 @@ -import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; -import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto"; -import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client"; -import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; -import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; -import { roundToQuarterHour } from "src/common/utils/date-utils"; -import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; -import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; -import { VacationService } from "src/time-and-attendance/domains/services/vacation.service"; -import { PrismaService } from "src/prisma/prisma.service"; -import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; -import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils"; +// import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; +// import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto"; +// import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client"; +// import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; +// import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; +// import { roundToQuarterHour } from "src/common/utils/date-utils"; +// import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +// import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +// import { VacationService } from "src/time-and-attendance/domains/services/vacation.service"; +// import { PrismaService } from "src/prisma/prisma.service"; +// import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; +// import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils"; -@Injectable() -export class VacationLeaveRequestsService { - constructor( - private readonly prisma: PrismaService, - private readonly vacationService: VacationService, - private readonly emailResolver: EmailToIdResolver, - private readonly typeResolver: BankCodesResolver, - ) {} +// @Injectable() +// export class VacationLeaveRequestsService { +// constructor( +// private readonly prisma: PrismaService, +// private readonly vacationService: VacationService, +// private readonly emailResolver: EmailToIdResolver, +// private readonly typeResolver: BankCodesResolver, +// ) {} - async create(dto: UpsertLeaveRequestDto): Promise { - const email = dto.email.trim(); - const employee_id = await this.emailResolver.findIdByEmail(email); - const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.VACATION); - if(!bank_code) throw new NotFoundException(`bank_code not found`); +// async create(dto: UpsertLeaveRequestDto): Promise { +// const email = dto.email.trim(); +// const employee_id = await this.emailResolver.findIdByEmail(email); +// const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.VACATION); +// if(!bank_code) throw new NotFoundException(`bank_code not found`); - const modifier = bank_code.modifier ?? 1; - const dates = normalizeDates(dto.dates); - const requested_hours_per_day = dto.requested_hours ?? 8; - if (!dates.length) throw new BadRequestException("Dates array must not be empty"); +// const modifier = bank_code.modifier ?? 1; +// const dates = normalizeDates(dto.dates); +// const requested_hours_per_day = dto.requested_hours ?? 8; +// if (!dates.length) throw new BadRequestException("Dates array must not be empty"); - const entries = dates - .map((iso) => ({ iso, date: toDateOnly(iso) })) - .sort((a, b) => a.date.getTime() - b.date.getTime()); - const start_date = entries[0].date; - const total_payable_hours = await this.vacationService.calculateVacationPay( - employee_id, - start_date, - entries.length, - modifier, - ); - let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours)); - const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); +// const entries = dates +// .map((iso) => ({ iso, date: toDateOnly(iso) })) +// .sort((a, b) => a.date.getTime() - b.date.getTime()); +// const start_date = entries[0].date; +// const total_payable_hours = await this.vacationService.calculateVacationPay( +// employee_id, +// start_date, +// entries.length, +// modifier, +// ); +// let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours)); +// const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); - const created: LeaveRequestViewDto[] = []; +// const created: LeaveRequestViewDto[] = []; - for (const { iso, date } of entries) { - const existing = await this.prisma.leaveRequests.findUnique({ - where: { - leave_per_employee_date: { - employee_id: employee_id, - leave_type: LeaveTypes.VACATION, - date, - }, - }, - select: { id: true }, - }); - if (existing) throw new BadRequestException(`Vacation request already exists for ${iso}`); +// for (const { iso, date } of entries) { +// const existing = await this.prisma.leaveRequests.findUnique({ +// where: { +// leave_per_employee_date: { +// employee_id: employee_id, +// leave_type: LeaveTypes.VACATION, +// date, +// }, +// }, +// select: { id: true }, +// }); +// if (existing) throw new BadRequestException(`Vacation request already exists for ${iso}`); - const payable = Math.min(remaining_payable_hours, daily_payable_cap); - const payable_rounded = roundToQuarterHour(Math.max(0, payable)); - remaining_payable_hours = roundToQuarterHour( - Math.max(0, remaining_payable_hours - payable_rounded), - ); +// const payable = Math.min(remaining_payable_hours, daily_payable_cap); +// const payable_rounded = roundToQuarterHour(Math.max(0, payable)); +// remaining_payable_hours = roundToQuarterHour( +// Math.max(0, remaining_payable_hours - payable_rounded), +// ); - const row = await this.prisma.leaveRequests.create({ - data: { - employee_id: employee_id, - bank_code_id: bank_code.id, - payable_hours: payable_rounded, - requested_hours: requested_hours_per_day, - leave_type: LeaveTypes.VACATION, - comment: dto.comment ?? "", - approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING, - date, - }, - select: leaveRequestsSelect, - }); +// const row = await this.prisma.leaveRequests.create({ +// data: { +// employee_id: employee_id, +// bank_code_id: bank_code.id, +// payable_hours: payable_rounded, +// requested_hours: requested_hours_per_day, +// leave_type: LeaveTypes.VACATION, +// comment: dto.comment ?? "", +// approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING, +// date, +// }, +// select: leaveRequestsSelect, +// }); - const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); - if (row.approval_status === LeaveApprovalStatus.APPROVED) { - // await this.leaveUtils.syncShift(email, employee_id, iso, hours, LeaveTypes.VACATION, row.comment); - } - created.push({ ...mapRowToView(row), action: "create" }); - } - return { action: "create", leave_requests: created }; - } -} +// const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); +// if (row.approval_status === LeaveApprovalStatus.APPROVED) { +// // await this.leaveUtils.syncShift(email, employee_id, iso, hours, LeaveTypes.VACATION, row.comment); +// } +// created.push({ ...mapRowToView(row), action: "create" }); +// } +// return { action: "create", leave_requests: created }; +// } +// } diff --git a/src/time-and-attendance/time-and-attendance.module.ts b/src/time-and-attendance/time-and-attendance.module.ts index 5d03b59..7bba6bf 100644 --- a/src/time-and-attendance/time-and-attendance.module.ts +++ b/src/time-and-attendance/time-and-attendance.module.ts @@ -9,8 +9,9 @@ import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracke 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 { ShiftController } from "src/time-and-attendance/time-tracker/shifts/controllers/shift.controller"; +import { ShiftsCreateService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-create.service"; import { ShiftsGetService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-get.service"; -import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service"; +import { ShiftsUpdateDeleteService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service"; import { TimesheetController } from "src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller"; import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service"; import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service"; @@ -33,7 +34,8 @@ import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-i providers: [ GetTimesheetsOverviewService, ShiftsGetService, - ShiftsUpsertService, + ShiftsCreateService, + ShiftsUpdateDeleteService, ExpenseUpsertService, SchedulePresetsUpsertService, SchedulePresetsGetService, diff --git a/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts index 122c30c..ba56edd 100644 --- a/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts +++ b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts @@ -5,23 +5,24 @@ import { PrismaService } from "src/prisma/prisma.service"; import { ApplyResult } from "src/time-and-attendance/utils/type.utils"; import { WEEKDAY } from "src/time-and-attendance/utils/mappers.utils"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { Result } from "src/common/errors/result-error.factory"; @Injectable() export class SchedulePresetsApplyService { - constructor( private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver) {} + constructor(private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver) { } - async applyToTimesheet( email: string, id: number, start_date_iso: string ): Promise { - if(!id) throw new BadRequestException(`Schedule preset with id: ${id} not found`); - if(!DATE_ISO_FORMAT.test(start_date_iso)) throw new BadRequestException('start_date must be of format :YYYY-MM-DD'); + async applyToTimesheet(email: string, id: number, start_date_iso: string): Promise> { + if (!DATE_ISO_FORMAT.test(start_date_iso)) return { success: false, error: 'start_date must be of format :YYYY-MM-DD' }; const employee_id = await this.emailResolver.findIdByEmail(email); - + if (!employee_id.success) return { success: false, error: employee_id.error } + const preset = await this.prisma.schedulePresets.findFirst({ - where: { employee_id, id }, - include: { + where: { employee_id: employee_id.data, id }, + include: { shifts: { - orderBy: [{ week_day: 'asc'}, { sort_order: 'asc'}], + orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }], select: { id: true, week_day: true, @@ -34,13 +35,14 @@ export class SchedulePresetsApplyService { }, }, }); - if(!preset) throw new NotFoundException(`Preset ${preset} not found`); + if (!preset) return { success: false, error: `Schedule preset with id: ${id} not found` }; + const start_date = new Date(`${start_date_iso}T00:00:00.000Z`); - const timesheet = await this.prisma.timesheets.upsert({ - where: { employee_id_start_date: { employee_id, start_date: start_date} }, + const timesheet = await this.prisma.timesheets.upsert({ + where: { employee_id_start_date: { employee_id: employee_id.data, start_date: start_date } }, update: {}, - create: { employee_id, start_date: start_date }, + create: { employee_id: employee_id.data, start_date: start_date }, select: { id: true }, }); @@ -52,22 +54,22 @@ export class SchedulePresetsApplyService { index_by_day.set(shift.week_day, list); } - const addDays = (date: Date, days: number) => + const addDays = (date: Date, days: number) => new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + days)); const overlaps = (aStart: Date, aEnd: Date, bStart: Date, bEnd: Date) => aStart.getTime() < bEnd.getTime() && aEnd.getTime() > bStart.getTime(); - + let created = 0; let skipped = 0; await this.prisma.$transaction(async (tx) => { - for(let i = 0; i < 7; i++) { + for (let i = 0; i < 7; i++) { const date = addDays(start_date, i); const week_day = WEEKDAY[date.getUTCDay()]; const shifts = index_by_day.get(week_day) ?? []; - - if(shifts.length === 0) continue; + + if (shifts.length === 0) continue; const existing = await tx.shifts.findMany({ where: { timesheet_id: timesheet.id, date: date }, @@ -83,24 +85,23 @@ export class SchedulePresetsApplyService { const payload: Prisma.ShiftsCreateManyInput[] = []; - for(const shift of shifts) { - if(shift.end_time.getTime() <= shift.start_time.getTime()) { - throw new ConflictException(`Invalid time range in preset day: ${week_day}, order: ${shift.sort_order}`); + for (const shift of shifts) { + if (shift.end_time.getTime() <= shift.start_time.getTime()) { + return { + success: false, + error: `Invalid time range in preset day: ${week_day}, order: ${shift.sort_order}` + }; } - const conflict = existing.find((existe)=> overlaps( - shift.start_time, shift.end_time , + const conflict = existing.find((existe) => overlaps( + shift.start_time, shift.end_time, existe.start_time, existe.end_time, )); - if(conflict) { - throw new ConflictException({ - error_code: 'SHIFT_OVERLAP_WITH_EXISTING', - mesage: `Preset shift overlaps existing shift on ${start_date} + ${i}(week day ${week_day})`, - conflict: { - existing_start: conflict.start_time.toISOString().slice(11,16), - existing_end: conflict.end_time.toISOString().slice(11,16), - }, - }); - } + if (conflict) + return { + success: false, + error: `[SHIFT_OVERLAP] :Preset shift overlaps existing shift on ${start_date} + ${i}(week day ${week_day}) ` + }; + payload.push({ timesheet_id: timesheet.id, date: date, @@ -111,13 +112,13 @@ export class SchedulePresetsApplyService { bank_code_id: shift.bank_code_id, }); } - if(payload.length) { + if (payload.length) { const response = await tx.shifts.createMany({ data: payload, skipDuplicates: true }); created += response.count; skipped += payload.length - response.count; } } }); - return { timesheet_id: timesheet.id, created, skipped }; + return { success: true, data: { timesheet_id: timesheet.id, created, skipped } }; } } \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts index 3bb3050..fa1d7b9 100644 --- a/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts +++ b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts @@ -3,6 +3,7 @@ import { PrismaService } from "src/prisma/prisma.service"; import { Injectable } from "@nestjs/common"; import { Prisma } from "@prisma/client"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { Result } from "src/common/errors/result-error.factory"; @Injectable() export class SchedulePresetsGetService { @@ -11,11 +12,12 @@ export class SchedulePresetsGetService { private readonly emailResolver: EmailToIdResolver, ){} - async getSchedulePresets(email: string): Promise { + 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 } const presets = await this.prisma.schedulePresets.findMany({ - where: { employee_id }, + where: { employee_id: employee_id.data }, orderBy: [{is_default: 'desc' }, { name: 'asc' }], include: { shifts: { @@ -39,10 +41,9 @@ export class SchedulePresetsGetService { type: shift.bank_code?.type, })), })); - return response; - } catch ( error: unknown) { - if(error instanceof Prisma.PrismaClientKnownRequestError) {} - throw error; + return { success: true, data:response}; + } catch ( error) { + return { success: false, error: `Schedule presets for employee with email ${email} not found`}; } } diff --git a/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts index c590d57..f7d2a5c 100644 --- a/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts +++ b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts @@ -1,11 +1,12 @@ import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common"; import { CreatePresetResult, DeletePresetResult, UpdatePresetResult } from "src/time-and-attendance/utils/type.utils"; import { Prisma, Weekday } from "@prisma/client"; -import { toHHmmFromDate } from "src/time-and-attendance/utils/date-time.utils"; +import { toDateFromString, toHHmmFromDate } from "src/time-and-attendance/utils/date-time.utils"; import { PrismaService } from "src/prisma/prisma.service"; import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { Result } from "src/common/errors/result-error.factory"; @Injectable() export class SchedulePresetsUpsertService { @@ -17,41 +18,42 @@ export class SchedulePresetsUpsertService { //_________________________________________________________________ // CREATE //_________________________________________________________________ - async createPreset(email: string, dto: SchedulePresetsDto): Promise { + async createPreset(email: string, dto: SchedulePresetsDto): Promise> { try { - const shifts_data = await this.resolveAndBuildPresetShifts(dto); - const employee_id = await this.emailResolver.findIdByEmail(email); - if (!shifts_data) throw new BadRequestException(`Employee with email: ${email} or dto not found`); + const shifts_data = await this.normalizePresetShifts(dto); + if (!shifts_data) return { success: false, error: `Employee with email: ${email} or dto not found` }; - await this.prisma.$transaction(async (tx) => { + const employee_id = await this.emailResolver.findIdByEmail(email); + if (!employee_id.success) return { success: false, error: employee_id.error }; + + const created = await this.prisma.$transaction(async (tx) => { if (dto.is_default) { await tx.schedulePresets.updateMany({ - where: { is_default: true, employee_id }, + where: { is_default: true, employee_id: employee_id.data }, data: { is_default: false }, }); + await tx.schedulePresets.create({ + data: { + id: dto.id, + employee_id: employee_id.data, + name: dto.name, + is_default: !!dto.is_default, + shifts: { create: shifts_data }, + }, + }); + return { success: true, data: created } } - const created = await tx.schedulePresets.create({ - data: { - id: dto.id, - employee_id, - name: dto.name, - is_default: !!dto.is_default, - shifts: { create: shifts_data }, - }, - }); - return created; }); - return { ok: true }; - - } catch (error: unknown) { - return { ok: false, error }; + return { success: true, data: created } + } catch (error) { + return { success: false, error: ' An error occured during create. Invalid Schedule data' }; } } //_________________________________________________________________ // UPDATE //_________________________________________________________________ - async updatePreset(preset_id: number, dto: SchedulePresetsDto): Promise { + async updatePreset(preset_id: number, dto: SchedulePresetsDto): Promise> { try { const existing = await this.prisma.schedulePresets.findFirst({ where: { id: preset_id }, @@ -61,9 +63,10 @@ export class SchedulePresetsUpsertService { employee_id: true, }, }); - if (!existing) throw new NotFoundException(`Preset "${dto.name}" not found`); + if (!existing) return { success: false, error: `Preset "${dto.name}" not found` }; + + const shifts_data = await this.normalizePresetShifts(dto); - const shifts_data = await this.resolveAndBuildPresetShifts(dto); await this.prisma.$transaction(async (tx) => { if (typeof dto.is_default === 'boolean') { if (dto.is_default) { @@ -84,27 +87,33 @@ export class SchedulePresetsUpsertService { }, }); } - if (shifts_data.length <= 0) throw new BadRequestException('Preset shifts to update not found'); + if (shifts_data.length <= 0) return { success: false, error: 'Preset shifts to update not found' }; await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } }); - const create_many_data: Prisma.SchedulePresetShiftsCreateManyInput[] = - shifts_data.map((shift) => { - if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') { - throw new NotFoundException(`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, - }; - }); - await tx.schedulePresetShifts.createMany({ data: create_many_data }); + try { + const create_many_data: Prisma.SchedulePresetShiftsCreateManyInput[] = + shifts_data.map((shift) => { + if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') { + throw new NotFoundException(`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, + }; + }); + await tx.schedulePresetShifts.createMany({ data: create_many_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({ @@ -116,7 +125,7 @@ export class SchedulePresetsUpsertService { } }, }); - if (!saved) throw new NotFoundException(`Preset with id: ${existing.id} not found`); + if (!saved) return { success: false, error: `Preset with id: ${existing.id} not found` }; const response_dto: SchedulePresetsDto = { id: saved.id, @@ -133,42 +142,40 @@ export class SchedulePresetsUpsertService { })), }; - return { ok: true, id: existing.id, data: response_dto }; + return { success: true, data: response_dto }; } catch (error) { - return { ok: false, id: preset_id, error } + return { success: false, error: 'An error occured during update. Invalid data' } } } //_________________________________________________________________ // DELETE //_________________________________________________________________ - async deletePreset(preset_id: number): Promise { + 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) throw new NotFoundException(`Preset with id ${preset_id} not found`); + 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 { ok: true, id: preset_id }; + return { success: true, data: preset_id }; } catch (error) { - if (error) throw new NotFoundException(`Preset schedule with id ${preset_id} not found`); - return { ok: false, id: preset_id, 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 resolveAndBuildPresetShifts( + private async normalizePresetShifts( dto: SchedulePresetsDto ): Promise { - if (!dto.preset_shifts?.length) throw new NotFoundException(`Empty or preset shifts not found`); const types = Array.from(new Set(dto.preset_shifts.map((shift) => shift.type))); @@ -178,7 +185,6 @@ export class SchedulePresetsUpsertService { const { id } = await this.typeResolver.findIdAndModifierByType(type); bank_code_set.set(type, id) } - const toTime = (hhmm: string) => new Date(`1970-01-01T${hhmm}:00.000Z`); const pair_set = new Set(); for (const shift of dto.preset_shifts) { @@ -195,8 +201,8 @@ export class SchedulePresetsUpsertService { if (!shift.start_time || !shift.end_time) { throw new BadRequestException(`start_time and end_time are required for (${shift.week_day}, ${shift.sort_order})`); } - const start = toTime(shift.start_time); - const end = toTime(shift.end_time); + const start = toDateFromString(shift.start_time); + const end = toDateFromString(shift.end_time); if (end.getTime() <= start.getTime()) { throw new ConflictException(`end_time must be > start_time ( day: ${shift.week_day}, order: ${shift.sort_order})`); } diff --git a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts index 06b159d..e765ffe 100644 --- a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts +++ b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts @@ -1,36 +1,35 @@ -import { BadRequestException, Body, Controller, Delete, Param, Patch, Post, Req } from "@nestjs/common"; +import { Body, Controller, Delete, Param, Patch, Post, Req } from "@nestjs/common"; import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; -import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service"; import { CreateShiftResult, UpdateShiftResult } from "src/time-and-attendance/utils/type.utils"; import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { GLOBAL_CONTROLLER_ROLES } from "src/common/shared/role-groupes"; +import { Result } from "src/common/errors/result-error.factory"; +import { ShiftsCreateService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-create.service"; +import { ShiftsUpdateDeleteService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service"; @Controller('shift') @RolesAllowed(...GLOBAL_CONTROLLER_ROLES) export class ShiftController { constructor( - private readonly upsert_service: ShiftsUpsertService, + private readonly create_service: ShiftsCreateService, + private readonly update_delete_service: ShiftsUpdateDeleteService, ){} @Post('create') - createBatch( @Req() req, @Body()dtos: ShiftDto[]): Promise { + createBatch( @Req() req, @Body()dtos: ShiftDto[]): Promise> { const email = req.user?.email; - const list = Array.isArray(dtos) ? dtos : []; - if(list.length === 0) throw new BadRequestException('Body is missing or invalid (create shifts)'); - return this.upsert_service.createShifts(email, dtos) + return this.create_service.createOneOrManyShifts(email, dtos) } @Patch('update') - updateBatch( @Body() dtos: ShiftDto[]): Promise{ - const list = Array.isArray(dtos) ? dtos: []; - if(list.length === 0) throw new BadRequestException('Body is missing or invalid (update shifts)'); - return this.upsert_service.updateShifts(dtos); + updateBatch( @Body() dtos: ShiftDto[]): Promise>{ + return this.update_delete_service.updateOneOrManyShifts(dtos); } @Delete(':shift_id') remove(@Param('shift_id') shift_id: number ) { - return this.upsert_service.deleteShift(shift_id); + return this.update_delete_service.deleteShift(shift_id); } } diff --git a/src/time-and-attendance/time-tracker/shifts/services/shifts-create.service.ts b/src/time-and-attendance/time-tracker/shifts/services/shifts-create.service.ts new file mode 100644 index 0000000..94cdd44 --- /dev/null +++ b/src/time-and-attendance/time-tracker/shifts/services/shifts-create.service.ts @@ -0,0 +1,155 @@ +import { Normalized } from "src/time-and-attendance/utils/type.utils"; +import { overlaps, toStringFromHHmm, toStringFromDate, toDateFromString, toHHmmFromString } from "src/time-and-attendance/utils/date-time.utils"; +import { Injectable } from "@nestjs/common"; +import { timesheet_select } from "src/time-and-attendance/utils/selects.utils"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { PrismaService } from "src/prisma/prisma.service"; +import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; +import { Result } from "src/common/errors/result-error.factory"; + +@Injectable() +export class ShiftsCreateService { + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver: EmailToIdResolver, + private readonly typeResolver: BankCodesResolver, + ) { } + + //_________________________________________________________________ + // CREATE WRAPPER FUNCTION FOR ONE OR MANY INPUT + //_________________________________________________________________ + async createOneOrManyShifts(email: string, shifts: ShiftDto[]): Promise> { + try { + //verify if array is empty or not + if (!Array.isArray(shifts) || shifts.length === 0) return { success: false, error: 'No data received' }; + + //verify if email is valid or not + const employee_id = await this.emailResolver.findIdByEmail(email); + if (!employee_id.success) return { success: false, error: employee_id.error }; + + //calls the create functions and await the return of successfull result or not + const results = await Promise.allSettled(shifts.map(shift => this.createShift(employee_id.data, shift))); + + //return arrays of created shifts or errors + const created_shifts: ShiftDto[] = []; + const errors: string[] = []; + + //filters results into created_shifts or errors arrays depending on the return from "allSettled" Promise + for (const result of results) { + if (result.status === 'fulfilled') { + if (result.value.success) { + created_shifts.push(result.value.data); + } else { + errors.push(result.value.error); + } + } else { + errors.push(result.reason instanceof Error ? result.reason.message : String(result.reason)); + } + } + //verify if shifts were created and returns an array of errors if needed + if (created_shifts.length === 0) return { success: false, error: errors.join(' | ') || 'No shift created' }; + + // returns array of created shifts + return { success: true, data: created_shifts } + } catch (error) { + return { success: false, error } + } + } + //_________________________________________________________________ + // CREATE + //_________________________________________________________________ + async createShift(employee_id: number, dto: ShiftDto): Promise> { + try { + //transform string format to date and HHmm + const normed_shift = await this.normalizeShiftDto(dto); + if (normed_shift.end_time <= normed_shift.start_time) return { + success: false, + error: `INVALID_SHIFT - ` + + `start_time: ${toStringFromHHmm(normed_shift.start_time)},` + + `end_time: ${toStringFromHHmm(normed_shift.end_time)},` + + `date: ${toStringFromDate(normed_shift.date)}.` + } + //fetch the right timesheet + const timesheet = await this.prisma.timesheets.findUnique({ + where: { id: dto.timesheet_id, employee_id }, + select: timesheet_select, + }); + if (!timesheet) return { + success: false, + error: `INVALID_TIMESHEET -` + + `start_time: ${toStringFromHHmm(normed_shift.start_time)},` + + `end_time: ${toStringFromHHmm(normed_shift.end_time)},` + + `date: ${toStringFromDate(normed_shift.date)}.` + } + //finds bank_code_id using the type + const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type); + + //fetchs existing shifts from DB to check for overlaps + const existing_shifts = await this.prisma.shifts.findMany({ + where: { timesheet_id: timesheet.id, date: normed_shift.date }, + select: { id: true, date: true, start_time: true, end_time: true }, + }); + for (const existing of existing_shifts) { + const existing_start = await toDateFromString(existing.start_time); + const existing_end = await toDateFromString(existing.end_time); + const existing_date = await toDateFromString(existing.date); + + const has_overlap = overlaps( + { start: normed_shift.start_time, end: normed_shift.end_time, date: normed_shift.date }, + { start: existing_start, end: existing_end, date: existing_date }, + ); + if (has_overlap) { + return { + success: false, + error: `SHIFT_OVERLAP` + + `new shift: ${toStringFromHHmm(normed_shift.start_time)}–${toStringFromHHmm(normed_shift.end_time)} ` + + `existing shift: ${toStringFromHHmm(existing.start_time)}–${toStringFromHHmm(existing.end_time)} ` + + `date: ${toStringFromDate(normed_shift.date)})`, + } + } + } + + //sends data for creation of a shift in db + const created_shift = await this.prisma.shifts.create({ + data: { + timesheet_id: timesheet.id, + bank_code_id: bank_code.id, + date: normed_shift.date, + start_time: normed_shift.start_time, + end_time: normed_shift.end_time, + is_approved: dto.is_approved, + is_remote: dto.is_remote, + comment: dto.comment ?? '', + }, + }); + //builds an object to return for display in the frontend + const shift: ShiftDto = { + id: created_shift.id, + timesheet_id: timesheet.id, + type: dto.type, + date: toStringFromDate(created_shift.date), + start_time: toStringFromHHmm(created_shift.start_time), + end_time: toStringFromHHmm(created_shift.end_time), + is_approved: created_shift.is_approved, + is_remote: created_shift.is_remote, + comment: created_shift.comment ?? '', + } + return { success: true, data: shift }; + } catch (error) { + return { success: false, error: `An error occured during creation, invalid data` }; + } + } + + //_________________________________________________________________ + // LOCAL HELPERS + //_________________________________________________________________ + //converts all string hours and date to Date and HHmm formats + private normalizeShiftDto = async (dto: ShiftDto): Promise => { + const { id: bank_code_id } = await this.typeResolver.findBankCodeIDByType(dto.type); + const date = toDateFromString(dto.date); + const start_time = toHHmmFromString(dto.start_time); + const end_time = toHHmmFromString(dto.end_time); + return { date, start_time, end_time, bank_code_id: bank_code_id }; + } +} diff --git a/src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service.ts b/src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service.ts new file mode 100644 index 0000000..3757ae6 --- /dev/null +++ b/src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service.ts @@ -0,0 +1,144 @@ +import { BadRequestException, NotFoundException, ConflictException } from "@nestjs/common"; +import { Result } from "src/common/errors/result-error.factory"; +import { PrismaService } from "src/prisma/prisma.service"; +import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; +import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto"; +import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto"; +import { ShiftsCreateService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-create.service"; +import { toDateFromString, toHHmmFromString, overlaps, toStringFromHHmm, toStringFromDate } from "src/time-and-attendance/utils/date-time.utils"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +import { shift_select } from "src/time-and-attendance/utils/selects.utils"; +import { UpdateShiftResult, Normalized } from "src/time-and-attendance/utils/type.utils"; + +export class ShiftsUpdateDeleteService { + constructor( + private readonly prisma: PrismaService, + private readonly typeResolver: BankCodesResolver, + ) { } + + async updateOneOrManyShifts(shifts: ShiftDto[]): Promise> { + try { + //verify if array is empty or not + if (!Array.isArray(shifts) || shifts.length === 0) return { success: false, error: 'No data received' }; + + //calls the update functions and await the return of successfull result or not + const results = await Promise.allSettled(shifts.map(shift => this.updateShifts(shift))); + + //return arrays of updated shifts or errors + const updated_shifts: ShiftDto[] = []; + const errors: string[] = []; + + //filters results into updated_shifts or errors arrays depending on the return from "allSettled" Promise + for (const result of results) { + if (result.status === 'fulfilled') { + if (result.value.success) { + updated_shifts.push(result.value.data); + } else { + errors.push(result.value.error); + } + } else { + errors.push(result.reason instanceof Error ? result.reason.message : String(result.reason)); + } + } + //verify if shifts were updated and returns an array of errors if needed + if (updated_shifts.length === 0) return { success: false, error: errors.join(' | ') || 'No shift updated' }; + + // returns array of updated shifts + return { success: true, data: updated_shifts } + } catch (error) { + return { success: false, error } + } + } + //_________________________________________________________________ + // UPDATE + //_________________________________________________________________ + async updateShifts(dto: ShiftDto): Promise> { + try { + //finds original shift + const original = await this.prisma.shifts.findFirst({ + where: { id: dto.id }, + select: shift_select, + }); + if (!original) return { success: false, error: `Shift with id: ${dto.id} not found` }; + + //transform string format to date and HHmm + const normed_shift = await this.normalizeShiftDto(dto); + if (normed_shift.end_time <= normed_shift.start_time) return { + success: false, + error: `INVALID_SHIFT - ` + + `start_time: ${toStringFromHHmm(normed_shift.start_time)},` + + `end_time: ${toStringFromHHmm(normed_shift.end_time)},` + + `date: ${toStringFromDate(normed_shift.date)}.` + }; + + //finds bank_code_id using the type + const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type); + + //updates sent to DB + const updated = await this.prisma.shifts.update({ + where: { id: original.id }, + data: { + date: normed_shift.date, + start_time: normed_shift.start_time, + end_time: normed_shift.end_time, + bank_code_id: bank_code.id, + comment: dto.comment, + is_approved: dto.is_approved, + is_remote: dto.is_remote, + }, + select: shift_select, + }); + if(!updated) return {success: false, error: ' An error occured during update, Invalid Datas'}; + + // builds an object to return for display in the frontend + const shift: ShiftDto = { + id: updated.id, + timesheet_id: updated.timesheet_id, + type: dto.type, + date: toStringFromDate(updated.date), + start_time: toStringFromHHmm(updated.start_time), + end_time: toStringFromHHmm(updated.end_time), + is_approved: updated.is_approved, + is_remote: updated.is_remote, + comment: updated.comment ?? '', + } + return { success: true, data: shift }; + + } catch (error) { + return { success: false, error: `An error occured during update. Invalid Data` }; + } + } + + //_________________________________________________________________ + // DELETE + //_________________________________________________________________ + //finds shifts using shit_ids + //blocs deletion if approved + async deleteShift(shift_id: number): Promise> { + try { + return await this.prisma.$transaction(async (tx) => { + const shift = await tx.shifts.findUnique({ + where: { id: shift_id }, + select: { id: true, date: true, timesheet_id: true }, + }); + if (!shift) return { success: false, error: `shift with id ${shift_id} not found ` }; + + await tx.shifts.delete({ where: { id: shift_id } }); + return { success: true, data: shift.id }; + }); + } catch (error) { + return { success: false, error: `INVALID_SHIFT, shift with id ${shift_id} not found` } + } + } + + //_________________________________________________________________ + // helpers + //_________________________________________________________________ + private normalizeShiftDto = async (dto: ShiftDto): Promise => { + const { id: bank_code_id } = await this.typeResolver.findBankCodeIDByType(dto.type); + const date = toDateFromString(dto.date); + const start_time = toHHmmFromString(dto.start_time); + const end_time = toHHmmFromString(dto.end_time); + return { date, start_time, end_time, bank_code_id: bank_code_id }; + } +} \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts b/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts deleted file mode 100644 index 7a857b2..0000000 --- a/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts +++ /dev/null @@ -1,463 +0,0 @@ -import { CreateShiftResult, NormedOk, UpdateShiftResult, Normalized } from "src/time-and-attendance/utils/type.utils"; -import { overlaps, toStringFromHHmm, toStringFromDate, toDateFromString, toHHmmFromString } from "src/time-and-attendance/utils/date-time.utils"; -import { Injectable, BadRequestException, ConflictException, NotFoundException } from "@nestjs/common"; -import { shift_select, timesheet_select } from "src/time-and-attendance/utils/selects.utils"; -import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; -import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; -import { PrismaService } from "src/prisma/prisma.service"; -import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto"; -import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto"; -import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; - -@Injectable() -export class ShiftsUpsertService { - constructor( - private readonly prisma: PrismaService, - private readonly emailResolver: EmailToIdResolver, - private readonly typeResolver: BankCodesResolver, - ) { } - - //_________________________________________________________________ - // CREATE - //_________________________________________________________________ - //normalized frontend data to match DB - //loads all shifts from a selected day to check for overlaping shifts - //checks for overlaping shifts - //create new shifts - async createShifts(email: string, dtos: ShiftDto[]): Promise { - if (!Array.isArray(dtos) || dtos.length === 0) return []; - - const employee_id = await this.emailResolver.findIdByEmail(email); - const results: CreateShiftResult[] = []; - const normed_shifts: (NormedOk | undefined)[] = await Promise.all(dtos.map(async (dto, index) => { - try { - const normed = await this.normalizeShiftDto(dto); - if (normed.end_time <= normed.start_time) { - const error = { - error_code: 'SHIFT_OVERLAP', - conflicts: { - start_time: toStringFromHHmm(normed.start_time), - end_time: toStringFromHHmm(normed.end_time), - date: toStringFromDate(normed.date), - }, - }; - results.push({ ok: false, error }); - } - - const timesheet = await this.prisma.timesheets.findUnique({ - where: { id: dto.timesheet_id, employee_id }, - select: timesheet_select, - }); - if (!timesheet) { - const error = { - error_code: 'INVALID_TIMESHEET', - conflicts: { - start_time: toStringFromHHmm(normed.start_time), - end_time: toStringFromHHmm(normed.end_time), - date: toStringFromDate(normed.date), - }, - }; - results.push({ ok: false, error }); - return; - } - const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type); - const date = await toDateFromString(dto.date); - const start_time = await toHHmmFromString(dto.start_time); - const end_time = await toHHmmFromString(dto.end_time); - const entity: ShiftEntity = { - timesheet_id: timesheet.id, - bank_code_id: bank_code.id, - date, - start_time, - end_time, - id: dto.id, - is_approved: dto.is_approved, - is_remote: dto.is_remote, - }; - - return { - index, - dto: entity, - normed, - timesheet_id: timesheet.id, - }; - } catch (error) { - results.push({ ok: false, error }); - return; - } - - })); - - const ok_items = normed_shifts.filter((item) => item !== undefined); - - - const regroup_by_date = new Map(); - ok_items.forEach(({ index, normed, timesheet_id }) => { - const day = new Date(normed.date.getUTCFullYear(), normed.date.getUTCMonth(), normed.date.getUTCDate()).getTime(); - const key = `${timesheet_id}|${day}`; - if (!regroup_by_date.has(key)) regroup_by_date.set(key, []); - regroup_by_date.get(key)!.push(index); - }); - - const timesheet_keys = Array.from(regroup_by_date.keys()).map((raw) => { - const [timesheet, day] = raw.split('|'); - return { - timesheet_id: Number(timesheet), - day: Number(day), - key: raw, - }; - }); - - for (const indices of regroup_by_date.values()) { - const ordered = indices - .map(index => { - const item = normed_shifts[index] as NormedOk & { timesheet_id: number }; - return { - index: index, - start: item.normed.start_time, - end: item.normed.end_time, - date: item.normed.date, - }; - }) - .sort((a, b) => a.start.getTime() - b.start.getTime()); - - for (let j = 1; j < ordered.length; j++) { - if ( - overlaps( - { start: ordered[j - 1].start, end: ordered[j - 1].end, date: ordered[j - 1].date }, - { start: ordered[j].start, end: ordered[j].end, date: ordered[j].date }, - ) - ) { - const error = new ConflictException({ - error_code: 'SHIFT_OVERLAP', - conflicts: { - start_time: toStringFromHHmm(ordered[j].start), - end_time: toStringFromHHmm(ordered[j].end), - date: toStringFromDate(ordered[j].date), - }, - }); - return dtos.map((_dto, key) => - indices.includes(key) - ? ({ ok: false, error } as CreateShiftResult) - : ({ ok: false, error }), - ); - } - } - } - return this.prisma.$transaction(async (tx) => { - const results: CreateShiftResult[] = Array.from( - { length: dtos.length }, - () => ({ ok: false, error: new Error('uninitialized') })); - - const existing_map = new Map(); - - for (const { timesheet_id, day, key } of timesheet_keys) { - const day_date = new Date(day); - const rows = await tx.shifts.findMany({ - where: { timesheet_id, date: day_date }, - select: { start_time: true, end_time: true, id: true, date: true }, - }); - existing_map.set(key, rows.map((row) => ({ start_time: row.start_time, end_time: row.end_time, date: row.date }))); - } - - ok_items.forEach((x, i) => { - if ("error" in x) results[i] = { ok: false, error: x.error }; - }); - - for (const item of ok_items) { - const { index, dto, normed, timesheet_id } = item; - const day_key = new Date(normed.date.getUTCFullYear(), normed.date.getUTCMonth(), normed.date.getUTCDate()).getTime(); - const map_key = `${timesheet_id}|${day_key}`; - let existing = existing_map.get(map_key); - if (!existing) { - existing = []; - existing_map.set(map_key, existing); - } - const hit = existing.find(exist => overlaps({ start: exist.start_time, end: exist.end_time, date: exist.date }, - { start: normed.start_time, end: normed.end_time, date: normed.date })); - if (hit) { - results[index] = { - ok: false, - error: { - error_code: 'SHIFT_OVERLAP', - conflicts: { - start_time: toStringFromHHmm(hit.start_time), - end_time: toStringFromHHmm(hit.end_time), - date: toStringFromDate(hit.date), - }, - }, - }; - continue; - } - - const row = await tx.shifts.create({ - data: { - timesheet_id: timesheet_id, - bank_code_id: normed.bank_code_id, - date: normed.date, - start_time: normed.start_time, - end_time: normed.end_time, - is_remote: dto.is_remote, - comment: dto.comment ?? undefined, - }, - select: shift_select, - }); - const normalizeHHmm = (value: Date) => toHHmmFromString(toStringFromHHmm(value)); - const normalized_row = { - start_time: normalizeHHmm(row.start_time), - end_time: normalizeHHmm(row.end_time), - date: toDateFromString(row.date), - }; - existing.push(normalized_row); - existing_map.set(map_key, existing); - - - const { type: bank_type } = await this.typeResolver.findTypeByBankCodeId(row.bank_code_id); - - const shift: GetShiftDto = { - shift_id: row.id, - timesheet_id: timesheet_id, - type: bank_type, - date: toStringFromDate(row.date), - start_time: toStringFromHHmm(row.start_time), - end_time: toStringFromHHmm(row.end_time), - is_remote: row.is_remote, - is_approved: false, - comment: row.comment ?? undefined, - }; - results[index] = { ok: true, data: shift }; - } - - return results; - }); - } - - //_________________________________________________________________ - // UPDATE - //_________________________________________________________________ - // finds existing shifts in DB - // verify if shifts are already approved - // normalized Date and Time format to string - // check for valid start and end times - // check for overlaping possibility - // buil a set of data to manipulate modified data only - // update shifts in DB - // return an updated version to display - async updateShifts(dtos: ShiftDto[]): Promise { - if (!Array.isArray(dtos) || dtos.length === 0) throw new BadRequestException({ error_code: 'SHIFT_MISSING' }); - - const updates: ShiftEntity[] = await Promise.all(dtos.map(async (item) => { - try { - const date = await toDateFromString(item.date); - const start_time = await toHHmmFromString(item.start_time); - const end_time = await toHHmmFromString(item.end_time); - const bank_code = await this.typeResolver.findBankCodeIDByType(item.type); - return { - id: item.id, - timesheet_id: item.timesheet_id, - bank_code_id: bank_code.id, - date, - start_time, - end_time, - is_remote: item.is_remote, - is_approved: item.is_approved, - } - } catch (error) { - throw new BadRequestException('INVALID_SHIFT'); - } - })); - - return this.prisma.$transaction(async (tx) => { - - const shift_ids = updates.map(update_shift => update_shift.id); - const rows = await tx.shifts.findMany({ - where: { id: { in: shift_ids } }, - select: shift_select, - }); - const regroup_id = new Map(rows.map(r => [r.id, r])); - - for (const update of updates) { - const existing = regroup_id.get(update.id); - if (!existing) { - return updates.map(exist => exist.id === update.id - ? ({ ok: false, id: update.id, error: new NotFoundException({ error_code: 'SHIFT_MISSING' }) } as UpdateShiftResult) - : ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) }) - ); - } - if (existing.is_approved) { - return updates.map(exist => exist.id === update.id - ? ({ ok: false, id: update.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) } as UpdateShiftResult) - : ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) }) - ); - } - } - - const planned_updates = updates.map(update => { - const exist_shift = regroup_id.get(update.id)!; - const normed: Normalized = { - date: update.date, - start_time: update.start_time, - end_time: update.end_time, - bank_code_id: update.bank_code_id, - }; - return { update, exist_shift, normed }; - }); - - const groups = new Map(); - function key(timesheet: number, d: Date) { - const day_date = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()); - return `${timesheet}|${day_date.getTime()}`; - } - - const unique_pairs = new Map(); - for (const { exist_shift, normed } of planned_updates) { - unique_pairs.set(key(exist_shift.timesheet_id, exist_shift.date), { timesheet_id: exist_shift.timesheet_id, date: exist_shift.date }); - unique_pairs.set(key(exist_shift.timesheet_id, normed.date), { timesheet_id: exist_shift.timesheet_id, date: normed.date }); - } - - for (const group of unique_pairs.values()) { - const day_date = new Date(group.date.getUTCFullYear(), group.date.getUTCMonth(), group.date.getUTCDate()); - const existing = await tx.shifts.findMany({ - where: { timesheet_id: group.timesheet_id, date: day_date }, - select: { id: true, start_time: true, end_time: true, date: true }, - }); - groups.set(key(group.timesheet_id, day_date), { - existing: existing.map(row => ({ - id: row.id, - start: row.start_time, - end: row.end_time, - date: row.date, - })), incoming: planned_updates - }); - } - - for (const planned of planned_updates) { - const keys = key(planned.exist_shift.timesheet_id, planned.normed.date); - const group = groups.get(keys)!; - - const conflict = group.existing.find(row => - row.id !== planned.exist_shift.id && overlaps({ start: row.start, end: row.end, date: row.date }, - { start: planned.normed.start_time, end: planned.normed.end_time, date: planned.normed.date }) - ); - if (conflict) { - return updates.map(exist => exist.id === planned.exist_shift.id - ? ({ - ok: false, id: exist.id, error: { - error_code: 'SHIFT_OVERLAP', - conflicts: { - start_time: toStringFromHHmm(conflict.start), - end_time: toStringFromHHmm(conflict.end), - date: toStringFromDate(conflict.date), - }, - } - } as UpdateShiftResult) - : ({ ok: false, id: exist.id, error: new BadRequestException('Batch aborted due to overlap in another update') }) - ); - } - } - - const regoup_by_day = new Map(); - for (const planned of planned_updates) { - const keys = key(planned.exist_shift.timesheet_id, planned.normed.date); - if (!regoup_by_day.has(keys)) regoup_by_day.set(keys, []); - regoup_by_day.get(keys)!.push({ - id: planned.exist_shift.id, - start: planned.normed.start_time, - end: planned.normed.end_time, - date: planned.normed.date - }); - } - - for (const arr of regoup_by_day.values()) { - arr.sort((a, b) => a.start.getTime() - b.start.getTime()); - for (let i = 1; i < arr.length; i++) { - if (overlaps( - { start: arr[i - 1].start, end: arr[i - 1].end, date: arr[i - 1].date }, - { start: arr[i].start, end: arr[i].end, date: arr[i].date }) - ) { - const error = { - error_code: 'SHIFT_OVERLAP', - conflicts: { - start_time: toStringFromHHmm(arr[i].start), - end_time: toStringFromHHmm(arr[i].end), - date: toStringFromDate(arr[i].date), - }, - - }; - return updates.map(exist => ({ ok: false, id: exist.id, error: error })); - } - } - } - - const results: UpdateShiftResult[] = []; - for (const planned of planned_updates) { - try { - const data: Partial = { - bank_code_id: planned.normed.bank_code_id, - date: planned.normed.date, - start_time: planned.normed.start_time, - end_time: planned.normed.end_time, - is_remote: planned.update.is_remote, - is_approved: planned.exist_shift.is_approved, - comment: planned.update.comment, - }; - - const row = await tx.shifts.update({ - where: { id: planned.exist_shift.id }, - data, - select: shift_select, - }); - - const type = await this.typeResolver.findTypeByBankCodeId(row.bank_code_id); - - const dto: GetShiftDto = { - shift_id: row.id, - timesheet_id: row.timesheet_id, - type: type.type, - date: toStringFromDate(row.date), - start_time: toStringFromHHmm(row.start_time), - end_time: toStringFromHHmm(row.end_time), - is_approved: row.is_approved, - is_remote: row.is_remote, - comment: row.comment ?? undefined, - }; - - results.push({ ok: true, id: planned.exist_shift.id, data: dto }); - } catch (error) { - throw new BadRequestException('INVALID_SHIFT'); - } - } - return results; - }); - } - - //_________________________________________________________________ - // DELETE - //_________________________________________________________________ - //finds shifts using shit_ids - //blocs deletion if approved - async deleteShift(shift_id: number) { - return await this.prisma.$transaction(async (tx) => { - const shift = await tx.shifts.findUnique({ - where: { id: shift_id }, - select: { id: true, date: true, timesheet_id: true }, - }); - if (!shift) throw new ConflictException({ error_code: 'SHIFT_INVALID' }); - - await tx.shifts.delete({ where: { id: shift_id } }); - return { success: true }; - }); - } - - //_________________________________________________________________ - // LOCAL HELPERS - //_________________________________________________________________ - //converts all string hours and date to Date and HHmm formats - private normalizeShiftDto = async (dto: ShiftDto): Promise => { - const { id: bank_code_id } = await this.typeResolver.findBankCodeIDByType(dto.type); - const date = toDateFromString(dto.date); - const start_time = toHHmmFromString(dto.start_time); - const end_time = toHHmmFromString(dto.end_time); - return { date, start_time, end_time, bank_code_id: bank_code_id }; - } -} diff --git a/src/time-and-attendance/time-tracker/shifts/shifts.module.ts b/src/time-and-attendance/time-tracker/shifts/shifts.module.ts index 6d3773e..a68a467 100644 --- a/src/time-and-attendance/time-tracker/shifts/shifts.module.ts +++ b/src/time-and-attendance/time-tracker/shifts/shifts.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { ShiftController } from 'src/time-and-attendance/time-tracker/shifts/controllers/shift.controller'; -import { ShiftsUpsertService } from 'src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service'; +import { ShiftsCreateService } from 'src/time-and-attendance/time-tracker/shifts/services/shifts-create.service'; +import { ShiftsUpdateDeleteService } from 'src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service'; @Module({ controllers: [ShiftController], - providers: [ ShiftsUpsertService ], - exports: [ ShiftsUpsertService ], + providers: [ ShiftsCreateService, ShiftsUpdateDeleteService ], + exports: [ ShiftsCreateService, ShiftsUpdateDeleteService ], }) export class ShiftsModule {} diff --git a/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts index 6810bfa..2d8c5ae 100644 --- a/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts +++ b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts @@ -4,61 +4,68 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { TotalExpenses, TotalHours } from "src/time-and-attendance/utils/type.utils"; import { PrismaService } from "src/prisma/prisma.service"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { Timesheets } from "src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto"; +import { Result } from "src/common/errors/result-error.factory"; @Injectable() export class GetTimesheetsOverviewService { constructor( private readonly prisma: PrismaService, - private readonly emailResolver : EmailToIdResolver, + private readonly emailResolver: EmailToIdResolver, ) { } //----------------------------------------------------------------------------------- // GET TIMESHEETS FOR A SELECTED EMPLOYEE //----------------------------------------------------------------------------------- - async getTimesheetsForEmployeeByPeriod(email: string, pay_year: number, pay_period_no: number) { - //find period using year and period_no - const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no } }); - if (!period) throw new NotFoundException(`Pay period ${pay_year}-${pay_period_no} not found`); + async getTimesheetsForEmployeeByPeriod(email: string, pay_year: number, pay_period_no: number): Promise> { + try { //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 ${pay_year}-${pay_period_no} not found`}; - //fetch the employee_id using the email - const employee_id = await this.emailResolver.findIdByEmail(email); - //loads the timesheets related to the fetched pay-period - const timesheet_range = { employee_id, start_date: { gte: period.period_start, lte: period.period_end } }; - let rows = await this.loadTimesheets(timesheet_range); + //fetch the employee_id using the email + const employee_id = await this.emailResolver.findIdByEmail(email); + if (!employee_id.success) return { success: false, error: employee_id.error } - //Normalized dates from pay-period - 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); + //loads the timesheets related to the fetched pay-period + const timesheet_range = { employee_id, start_date: { gte: period.period_start, lte: period.period_end } }; + let rows = await this.loadTimesheets(timesheet_range); - if (week_start.getTime() > normalized_end.getTime()) break; + //Normalized dates from pay-period + const normalized_start = toDateFromString(period.period_start); + const normalized_end = toDateFromString(period.period_end); - const exists = rows.some( - (row) => toDateFromString(row.start_date).getTime() === week_start.getTime() - ); - if (!exists) await this.ensureTimesheet(employee_id, week_start); + //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 exists = rows.some( + (row) => toDateFromString(row.start_date).getTime() === week_start.getTime() + ); + if (!exists) await this.ensureTimesheet(employee_id.data, week_start); + } + rows = await this.loadTimesheets(timesheet_range); + + + //find user infos using the employee_id + const employee = await this.prisma.employees.findUnique({ + where: { id: employee_id.data }, + include: { user: true }, + }); + if (!employee) return { success: false, error:`Employee #${employee_id} not found`} + + //builds employee full name + const user = employee.user; + const employee_fullname = `${user.first_name} ${user.last_name}`.trim(); + + //maps all timesheet's infos + const timesheets = rows.map((timesheet) => this.mapOneTimesheet(timesheet)); + return { success: true, data: { employee_fullname, timesheets } }; + } catch (error) { + return { success: false, error} } - rows = await this.loadTimesheets(timesheet_range); - - - //find user infos using the employee_id - const employee = await this.prisma.employees.findUnique({ - where: { id: employee_id }, - include: { user: true }, - }); - if (!employee) throw new NotFoundException(`Employee #${employee_id} not found`); - - //builds employee full name - const user = employee.user; - const employee_fullname = `${user.first_name} ${user.last_name}`.trim(); - - //maps all timesheet's infos - const timesheets = rows.map((timesheet) => this.mapOneTimesheet(timesheet)); - return { employee_fullname, timesheets }; } diff --git a/src/time-and-attendance/utils/date-time.utils.ts b/src/time-and-attendance/utils/date-time.utils.ts index 9628970..dd2d4cc 100644 --- a/src/time-and-attendance/utils/date-time.utils.ts +++ b/src/time-and-attendance/utils/date-time.utils.ts @@ -39,7 +39,7 @@ export const toHHmmFromDate = (input: Date | string): string => { return `${hh}:${mm}`; } -//converts Date format to string +//converts to Date format from string export const toDateFromString = (ymd: string | Date): Date => { return new Date(ymd); } diff --git a/src/time-and-attendance/utils/resolve-email-id.utils.ts b/src/time-and-attendance/utils/resolve-email-id.utils.ts index 543f377..14e0bc0 100644 --- a/src/time-and-attendance/utils/resolve-email-id.utils.ts +++ b/src/time-and-attendance/utils/resolve-email-id.utils.ts @@ -1,35 +1,35 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; import { Prisma, PrismaClient } from "@prisma/client"; import { PrismaService } from "src/prisma/prisma.service"; +import { Injectable } from "@nestjs/common"; +import { Result } from "src/common/errors/result-error.factory"; type Tx = Prisma.TransactionClient | PrismaClient; @Injectable() export class EmailToIdResolver { - constructor(private readonly prisma: PrismaService) {} + constructor(private readonly prisma: PrismaService) { } // find employee_id using email - readonly findIdByEmail = async ( email: string, client?: Tx - ): Promise => { + readonly findIdByEmail = async (email: string, client?: Tx): Promise> => { const db = client ?? this.prisma; const employee = await db.employees.findFirst({ where: { user: { email } }, select: { id: true }, }); - if(!employee)throw new NotFoundException(`Employee with email: ${email} not found`); - return employee.id; + if (!employee) return { success: false, error: `Employee with email:${email} not found` }; + return { data: employee.id, success: true }; } - + // find user_id using email readonly resolveUserIdWithEmail = async (email: string, client?: Tx - ): Promise => { + ): Promise> => { const db = client ?? this.prisma; const user = await db.users.findFirst({ where: { email }, select: { id: true }, }); - if(!user) throw new NotFoundException(`User with email ${ email } not found`); - return user.id; + if (!user) return { success: false, error: `User with email:${email} not found` }; + return { success: true, data: user.id }; } } \ No newline at end of file diff --git a/src/time-and-attendance/utils/resolve-timesheet.utils.ts b/src/time-and-attendance/utils/resolve-timesheet.utils.ts index f556a8e..52b62be 100644 --- a/src/time-and-attendance/utils/resolve-timesheet.utils.ts +++ b/src/time-and-attendance/utils/resolve-timesheet.utils.ts @@ -3,6 +3,7 @@ import { Prisma, PrismaClient } from "@prisma/client"; import { weekStartSunday } from "src/time-and-attendance/utils/date-time.utils"; import { PrismaService } from "src/prisma/prisma.service"; import { EmailToIdResolver } from "./resolve-email-id.utils"; +import { Result } from "src/common/errors/result-error.factory"; type Tx = Prisma.TransactionClient | PrismaClient; @@ -14,15 +15,16 @@ export class EmployeeTimesheetResolver { private readonly emailResolver: EmailToIdResolver, ) {} - readonly findTimesheetIdByEmail = async (email: string, date: Date, client?: Tx): Promise<{id: number}> => { + readonly findTimesheetIdByEmail = async (email: string, date: Date, client?: Tx): Promise> => { const db = client ?? this.prisma; const employee_id = await this.emailResolver.findIdByEmail(email); + if(!employee_id.success) return { success: false, error: employee_id.error} const start_date = weekStartSunday(date); const timesheet = await db.timesheets.findFirst({ - where: { employee_id : employee_id, start_date: start_date }, + where: { employee_id : employee_id.data, start_date: start_date }, select: { id: true }, }); if(!timesheet) throw new NotFoundException(`timesheet not found`); - return { id: timesheet.id }; + return { success: true, data: {id: timesheet.id} }; } } \ No newline at end of file From a8d53ab0aa16cf4800d7c70abd93afe668f3eb2b Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 11 Nov 2025 14:52:34 -0500 Subject: [PATCH 2/3] fix(shifts): added Promise to remove function of the shift controller --- .../time-tracker/shifts/controllers/shift.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts index e765ffe..6028dd5 100644 --- a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts +++ b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts @@ -28,7 +28,7 @@ export class ShiftController { } @Delete(':shift_id') - remove(@Param('shift_id') shift_id: number ) { + remove(@Param('shift_id') shift_id: number ): Promise> { return this.update_delete_service.deleteShift(shift_id); } From 1d9eaeab30060e353ca6393cdfd011f0b560fc6e Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 12 Nov 2025 09:16:37 -0500 Subject: [PATCH 3/3] feat(Result): ajusted return values to match Result pattern. --- npm | 0 src/common/errors/result-error.factory.ts | 12 +-- .../domains/services/overtime.service.ts | 20 ++++- .../controllers/expense.controller.ts | 1 - .../services/expense-upsert.service.ts | 3 +- .../mappers/leave-requests.mapper.ts | 4 +- .../utils/leave-request.transform.ts | 4 +- .../services/schedule-presets-get.service.ts | 1 - .../schedule-presets-upsert.service.ts | 64 ++++++++-------- .../shifts/controllers/shift.controller.ts | 1 - .../shifts/services/shifts-create.service.ts | 43 ++++++----- .../services/shifts-update-delete.service.ts | 73 +++++++++++++------ .../timesheet-get-overview.service.ts | 30 ++++++-- .../utils/resolve-bank-type-id.utils.ts | 33 +++++---- .../utils/resolve-full-name.utils.ts | 9 ++- .../utils/resolve-shifts-id.utils.ts | 20 +++-- .../utils/selects.utils.ts | 30 -------- src/time-and-attendance/utils/type.utils.ts | 71 ------------------ targo-backend@0.0.1 | 0 tsx | 0 20 files changed, 205 insertions(+), 214 deletions(-) delete mode 100644 npm delete mode 100644 targo-backend@0.0.1 delete mode 100644 tsx diff --git a/npm b/npm deleted file mode 100644 index e69de29..0000000 diff --git a/src/common/errors/result-error.factory.ts b/src/common/errors/result-error.factory.ts index b498545..0cf318a 100644 --- a/src/common/errors/result-error.factory.ts +++ b/src/common/errors/result-error.factory.ts @@ -2,11 +2,11 @@ export type Result = | { success: true; data: T } | { success: false; error: E }; -const success = (data: T): Result => { - return { success: true, data }; -} +// const success = (data: T): Result => { +// return { success: true, data }; +// } -const failure = (error: E): Result => { - return { success: false, error }; -} +// const failure = (error: E): Result => { +// return { success: false, error }; +// } diff --git a/src/time-and-attendance/domains/services/overtime.service.ts b/src/time-and-attendance/domains/services/overtime.service.ts index e43c4d3..acff2c4 100644 --- a/src/time-and-attendance/domains/services/overtime.service.ts +++ b/src/time-and-attendance/domains/services/overtime.service.ts @@ -1,10 +1,28 @@ import { Injectable, Logger } from '@nestjs/common'; +import { Prisma, PrismaClient } from '@prisma/client'; import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils'; import { PrismaService } from 'src/prisma/prisma.service'; import { DAILY_LIMIT_HOURS, WEEKLY_LIMIT_HOURS } from 'src/time-and-attendance/utils/constants.utils'; -import { Tx, WeekOvertimeSummary } from 'src/time-and-attendance/utils/type.utils'; +type Tx = Prisma.TransactionClient | PrismaClient; + +type WeekOvertimeSummary = { + week_start:string; + week_end: string; + week_total_hours: number; + weekly_overtime: number; + daily_overtime_kept: number; + total_overtime: number; + breakdown: Array<{ + date:string; + day_hours: number; + day_overtime: number; + daily_kept: number; + running_total_before: number; + }>; +}; + @Injectable() export class OvertimeService { diff --git a/src/time-and-attendance/expenses/controllers/expense.controller.ts b/src/time-and-attendance/expenses/controllers/expense.controller.ts index fe37b0e..88357c1 100644 --- a/src/time-and-attendance/expenses/controllers/expense.controller.ts +++ b/src/time-and-attendance/expenses/controllers/expense.controller.ts @@ -1,5 +1,4 @@ import { Controller, Post, Param, Body, Patch, Delete, Req, UnauthorizedException } from "@nestjs/common"; -import { CreateExpenseResult } from "src/time-and-attendance/utils/type.utils"; import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service"; import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; import { RolesAllowed } from "src/common/decorators/roles.decorators"; diff --git a/src/time-and-attendance/expenses/services/expense-upsert.service.ts b/src/time-and-attendance/expenses/services/expense-upsert.service.ts index 6a52f76..19be304 100644 --- a/src/time-and-attendance/expenses/services/expense-upsert.service.ts +++ b/src/time-and-attendance/expenses/services/expense-upsert.service.ts @@ -1,6 +1,6 @@ import { toDateFromString, toStringFromDate, weekStartSunday } from "src/time-and-attendance/utils/date-time.utils"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; -import { NormalizedExpense } from "src/time-and-attendance/utils/type.utils"; +// import { NormalizedExpense } from "src/time-and-attendance/utils/type.utils"; import { expense_select } from "src/time-and-attendance/utils/selects.utils"; import { PrismaService } from "src/prisma/prisma.service"; import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto"; @@ -8,6 +8,7 @@ import { ExpenseEntity } from "src/time-and-attendance/expenses/dtos/expense-ent import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; import { Injectable } from "@nestjs/common"; import { Result } from "src/common/errors/result-error.factory"; +import { NormalizedExpense } from "src/time-and-attendance/utils/type.utils"; @Injectable() diff --git a/src/time-and-attendance/leave-requests/mappers/leave-requests.mapper.ts b/src/time-and-attendance/leave-requests/mappers/leave-requests.mapper.ts index 9f823fc..ad772a0 100644 --- a/src/time-and-attendance/leave-requests/mappers/leave-requests.mapper.ts +++ b/src/time-and-attendance/leave-requests/mappers/leave-requests.mapper.ts @@ -1,6 +1,8 @@ import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; -import { LeaveRequestRow } from "src/time-and-attendance/utils/type.utils"; import { Prisma } from "@prisma/client"; +import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; + +type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>; const toNum = (value?: Prisma.Decimal | null) => value !== null && value !== undefined ? Number(value) : undefined; diff --git a/src/time-and-attendance/leave-requests/utils/leave-request.transform.ts b/src/time-and-attendance/leave-requests/utils/leave-request.transform.ts index 5ff49fd..47f76dc 100644 --- a/src/time-and-attendance/leave-requests/utils/leave-request.transform.ts +++ b/src/time-and-attendance/leave-requests/utils/leave-request.transform.ts @@ -1,9 +1,11 @@ +import { Prisma } from "@prisma/client"; import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; import { mapArchiveRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests-archive.mapper"; import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; import { LeaveRequestArchiveRow } from "src/time-and-attendance/leave-requests/utils/leave-requests-archive.select"; -import { LeaveRequestRow } from "src/time-and-attendance/utils/type.utils"; +import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; +export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>; /** Active (table leave_requests) : proxy to base mapper */ export function mapRowToViewWithDays(row: LeaveRequestRow): LeaveRequestViewDto { diff --git a/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts index fa1d7b9..b15765f 100644 --- a/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts +++ b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts @@ -1,7 +1,6 @@ import { PresetResponse, ShiftResponse } from "src/time-and-attendance/utils/type.utils"; import { PrismaService } from "src/prisma/prisma.service"; import { Injectable } from "@nestjs/common"; -import { Prisma } from "@prisma/client"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; import { Result } from "src/common/errors/result-error.factory"; diff --git a/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts index f7d2a5c..c55f2db 100644 --- a/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts +++ b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts @@ -1,5 +1,4 @@ import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common"; -import { CreatePresetResult, DeletePresetResult, UpdatePresetResult } from "src/time-and-attendance/utils/type.utils"; import { Prisma, Weekday } from "@prisma/client"; import { toDateFromString, toHHmmFromDate } from "src/time-and-attendance/utils/date-time.utils"; import { PrismaService } from "src/prisma/prisma.service"; @@ -21,7 +20,7 @@ export class SchedulePresetsUpsertService { async createPreset(email: string, dto: SchedulePresetsDto): Promise> { try { const shifts_data = await this.normalizePresetShifts(dto); - if (!shifts_data) return { success: false, error: `Employee with email: ${email} or dto not found` }; + if (!shifts_data.success) return { success: false, error: `Employee with email: ${email} or dto not found` }; const employee_id = await this.emailResolver.findIdByEmail(email); if (!employee_id.success) return { success: false, error: employee_id.error }; @@ -38,7 +37,7 @@ export class SchedulePresetsUpsertService { employee_id: employee_id.data, name: dto.name, is_default: !!dto.is_default, - shifts: { create: shifts_data }, + shifts: { create: shifts_data.data }, }, }); return { success: true, data: created } @@ -66,6 +65,7 @@ export class SchedulePresetsUpsertService { 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') { @@ -87,33 +87,34 @@ export class SchedulePresetsUpsertService { }, }); } - if (shifts_data.length <= 0) return { success: false, error: 'Preset shifts to update not found' }; + 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: Prisma.SchedulePresetShiftsCreateManyInput[] = - shifts_data.map((shift) => { - if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') { - throw new NotFoundException(`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, - }; - }); - await tx.schedulePresetShifts.createMany({ data: create_many_data }); - - return { success: true, data: create_many_data } - } catch (error) { - return { success: false, error: 'An error occured. Invalid data detected. ' }; - } + // 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({ @@ -175,15 +176,16 @@ export class SchedulePresetsUpsertService { //resolve bank_code_id using type and convert hours to TIME and valid shifts end/start private async normalizePresetShifts( dto: SchedulePresetsDto - ): Promise { + ): Promise> { if (!dto.preset_shifts?.length) throw new NotFoundException(`Empty or preset shifts not found`); const types = Array.from(new Set(dto.preset_shifts.map((shift) => shift.type))); const bank_code_set = new Map(); for (const type of types) { - const { id } = await this.typeResolver.findIdAndModifierByType(type); - bank_code_set.set(type, id) + const bank_code = await this.typeResolver.findIdAndModifierByType(type); + if (!bank_code.success) return { success: false, error: 'Bank_code not found' } + bank_code_set.set(type, bank_code.data.id); } const pair_set = new Set(); @@ -216,6 +218,6 @@ export class SchedulePresetsUpsertService { is_remote: !!shift.is_remote, }; }); - return items; + return { success: true, data: items }; } } diff --git a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts index 6028dd5..789117f 100644 --- a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts +++ b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts @@ -1,6 +1,5 @@ import { Body, Controller, Delete, Param, Patch, Post, Req } from "@nestjs/common"; import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; -import { CreateShiftResult, UpdateShiftResult } from "src/time-and-attendance/utils/type.utils"; import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { GLOBAL_CONTROLLER_ROLES } from "src/common/shared/role-groupes"; import { Result } from "src/common/errors/result-error.factory"; diff --git a/src/time-and-attendance/time-tracker/shifts/services/shifts-create.service.ts b/src/time-and-attendance/time-tracker/shifts/services/shifts-create.service.ts index 94cdd44..76c7bdf 100644 --- a/src/time-and-attendance/time-tracker/shifts/services/shifts-create.service.ts +++ b/src/time-and-attendance/time-tracker/shifts/services/shifts-create.service.ts @@ -63,12 +63,13 @@ export class ShiftsCreateService { try { //transform string format to date and HHmm const normed_shift = await this.normalizeShiftDto(dto); - if (normed_shift.end_time <= normed_shift.start_time) return { + if(!normed_shift.success) return { success: false, error: 'An error occured during normalization' } + if (normed_shift.data.end_time <= normed_shift.data.start_time) return { success: false, error: `INVALID_SHIFT - ` - + `start_time: ${toStringFromHHmm(normed_shift.start_time)},` - + `end_time: ${toStringFromHHmm(normed_shift.end_time)},` - + `date: ${toStringFromDate(normed_shift.date)}.` + + `start_time: ${toStringFromHHmm(normed_shift.data.start_time)},` + + `end_time: ${toStringFromHHmm(normed_shift.data.end_time)},` + + `date: ${toStringFromDate(normed_shift.data.date)}.` } //fetch the right timesheet const timesheet = await this.prisma.timesheets.findUnique({ @@ -78,16 +79,17 @@ export class ShiftsCreateService { if (!timesheet) return { success: false, error: `INVALID_TIMESHEET -` - + `start_time: ${toStringFromHHmm(normed_shift.start_time)},` - + `end_time: ${toStringFromHHmm(normed_shift.end_time)},` - + `date: ${toStringFromDate(normed_shift.date)}.` + + `start_time: ${toStringFromHHmm(normed_shift.data.start_time)},` + + `end_time: ${toStringFromHHmm(normed_shift.data.end_time)},` + + `date: ${toStringFromDate(normed_shift.data.date)}.` } //finds bank_code_id using the type - const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type); + const bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type); + if (!bank_code_id.success) return { success: false, error: bank_code_id.error }; //fetchs existing shifts from DB to check for overlaps const existing_shifts = await this.prisma.shifts.findMany({ - where: { timesheet_id: timesheet.id, date: normed_shift.date }, + where: { timesheet_id: timesheet.id, date: normed_shift.data.date }, select: { id: true, date: true, start_time: true, end_time: true }, }); for (const existing of existing_shifts) { @@ -96,16 +98,16 @@ export class ShiftsCreateService { const existing_date = await toDateFromString(existing.date); const has_overlap = overlaps( - { start: normed_shift.start_time, end: normed_shift.end_time, date: normed_shift.date }, + { start: normed_shift.data.start_time, end: normed_shift.data.end_time, date: normed_shift.data.date }, { start: existing_start, end: existing_end, date: existing_date }, ); if (has_overlap) { return { success: false, error: `SHIFT_OVERLAP` - + `new shift: ${toStringFromHHmm(normed_shift.start_time)}–${toStringFromHHmm(normed_shift.end_time)} ` + + `new shift: ${toStringFromHHmm(normed_shift.data.start_time)}–${toStringFromHHmm(normed_shift.data.end_time)} ` + `existing shift: ${toStringFromHHmm(existing.start_time)}–${toStringFromHHmm(existing.end_time)} ` - + `date: ${toStringFromDate(normed_shift.date)})`, + + `date: ${toStringFromDate(normed_shift.data.date)})`, } } } @@ -114,10 +116,10 @@ export class ShiftsCreateService { const created_shift = await this.prisma.shifts.create({ data: { timesheet_id: timesheet.id, - bank_code_id: bank_code.id, - date: normed_shift.date, - start_time: normed_shift.start_time, - end_time: normed_shift.end_time, + bank_code_id: bank_code_id.data, + date: normed_shift.data.date, + start_time: normed_shift.data.start_time, + end_time: normed_shift.data.end_time, is_approved: dto.is_approved, is_remote: dto.is_remote, comment: dto.comment ?? '', @@ -145,11 +147,14 @@ export class ShiftsCreateService { // LOCAL HELPERS //_________________________________________________________________ //converts all string hours and date to Date and HHmm formats - private normalizeShiftDto = async (dto: ShiftDto): Promise => { - const { id: bank_code_id } = await this.typeResolver.findBankCodeIDByType(dto.type); + private normalizeShiftDto = async (dto: ShiftDto): Promise> => { + const bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type); + if(!bank_code_id.success) return { success: false, error: 'Bank_code not found'} + const date = toDateFromString(dto.date); const start_time = toHHmmFromString(dto.start_time); const end_time = toHHmmFromString(dto.end_time); - return { date, start_time, end_time, bank_code_id: bank_code_id }; + + return { success: true, data: {date, start_time, end_time, bank_code_id: bank_code_id.data} }; } } diff --git a/src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service.ts b/src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service.ts index 3757ae6..100b080 100644 --- a/src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service.ts +++ b/src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service.ts @@ -1,14 +1,10 @@ -import { BadRequestException, NotFoundException, ConflictException } from "@nestjs/common"; import { Result } from "src/common/errors/result-error.factory"; import { PrismaService } from "src/prisma/prisma.service"; import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; -import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto"; -import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto"; -import { ShiftsCreateService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-create.service"; -import { toDateFromString, toHHmmFromString, overlaps, toStringFromHHmm, toStringFromDate } from "src/time-and-attendance/utils/date-time.utils"; +import { toDateFromString, toHHmmFromString, toStringFromHHmm, toStringFromDate, overlaps } from "src/time-and-attendance/utils/date-time.utils"; import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; import { shift_select } from "src/time-and-attendance/utils/selects.utils"; -import { UpdateShiftResult, Normalized } from "src/time-and-attendance/utils/type.utils"; +import { Normalized } from "src/time-and-attendance/utils/type.utils"; export class ShiftsUpdateDeleteService { constructor( @@ -22,7 +18,7 @@ export class ShiftsUpdateDeleteService { if (!Array.isArray(shifts) || shifts.length === 0) return { success: false, error: 'No data received' }; //calls the update functions and await the return of successfull result or not - const results = await Promise.allSettled(shifts.map(shift => this.updateShifts(shift))); + const results = await Promise.allSettled(shifts.map(shift => this.updateShift(shift))); //return arrays of updated shifts or errors const updated_shifts: ShiftDto[] = []; @@ -52,7 +48,7 @@ export class ShiftsUpdateDeleteService { //_________________________________________________________________ // UPDATE //_________________________________________________________________ - async updateShifts(dto: ShiftDto): Promise> { + async updateShift(dto: ShiftDto): Promise> { try { //finds original shift const original = await this.prisma.shifts.findFirst({ @@ -63,32 +59,36 @@ export class ShiftsUpdateDeleteService { //transform string format to date and HHmm const normed_shift = await this.normalizeShiftDto(dto); - if (normed_shift.end_time <= normed_shift.start_time) return { + if (!normed_shift.success) return { success: false, error: 'An error occured during normalization' } + if (normed_shift.data.end_time <= normed_shift.data.start_time) return { success: false, error: `INVALID_SHIFT - ` - + `start_time: ${toStringFromHHmm(normed_shift.start_time)},` - + `end_time: ${toStringFromHHmm(normed_shift.end_time)},` - + `date: ${toStringFromDate(normed_shift.date)}.` + + `start_time: ${toStringFromHHmm(normed_shift.data.start_time)},` + + `end_time: ${toStringFromHHmm(normed_shift.data.end_time)},` + + `date: ${toStringFromDate(normed_shift.data.date)}.` }; - + const overlap_check = await this.overlapChecker(normed_shift.data); + if(!overlap_check.success) return { success: false, error: 'Invalid shift, overlaps with existing shifts'} + //finds bank_code_id using the type const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type); + if (!bank_code.success) return { success: false, error: 'No bank_code_id found' }; //updates sent to DB const updated = await this.prisma.shifts.update({ where: { id: original.id }, data: { - date: normed_shift.date, - start_time: normed_shift.start_time, - end_time: normed_shift.end_time, - bank_code_id: bank_code.id, + date: normed_shift.data.date, + start_time: normed_shift.data.start_time, + end_time: normed_shift.data.end_time, + bank_code_id: bank_code.data, comment: dto.comment, is_approved: dto.is_approved, is_remote: dto.is_remote, }, select: shift_select, }); - if(!updated) return {success: false, error: ' An error occured during update, Invalid Datas'}; + if (!updated) return { success: false, error: ' An error occured during update, Invalid Datas' }; // builds an object to return for display in the frontend const shift: ShiftDto = { @@ -134,11 +134,42 @@ export class ShiftsUpdateDeleteService { //_________________________________________________________________ // helpers //_________________________________________________________________ - private normalizeShiftDto = async (dto: ShiftDto): Promise => { - const { id: bank_code_id } = await this.typeResolver.findBankCodeIDByType(dto.type); + private normalizeShiftDto = async (dto: ShiftDto): Promise> => { + const bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type); + if (!bank_code_id.success) return { success: false, error: 'Bank_code not found' } + const date = toDateFromString(dto.date); const start_time = toHHmmFromString(dto.start_time); const end_time = toHHmmFromString(dto.end_time); - return { date, start_time, end_time, bank_code_id: bank_code_id }; + + return { success: true, data: { date, start_time, end_time, bank_code_id: bank_code_id.data } }; + } + + private overlapChecker = async (dto: Normalized): Promise> => { + + const existing_shifts = await this.prisma.shifts.findMany({ + where: { date: dto.date }, + select: { id: true, date: true, start_time: true, end_time: true }, + }); + for (const existing of existing_shifts) { + const existing_start = toDateFromString(existing.start_time); + const existing_end = toDateFromString(existing.end_time); + const existing_date = toDateFromString(existing.date); + + const has_overlap = overlaps( + { start: dto.start_time, end: dto.end_time, date: dto.date }, + { start: existing_start, end: existing_end, date: existing_date }, + ); + if (has_overlap) { + return { + success: false, + error: `SHIFT_OVERLAP` + + `new shift: ${toStringFromHHmm(dto.start_time)}–${toStringFromHHmm(dto.end_time)} ` + + `existing shift: ${toStringFromHHmm(existing.start_time)}–${toStringFromHHmm(existing.end_time)} ` + + `date: ${toStringFromDate(dto.date)})`, + } + } + } + return { success: true, data: undefined } } } \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts index 2d8c5ae..b54ea1e 100644 --- a/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts +++ b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts @@ -1,12 +1,28 @@ import { sevenDaysFrom, toStringFromDate, toHHmmFromDate, toDateFromString } from "src/time-and-attendance/utils/date-time.utils"; import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/time-and-attendance/utils/constants.utils"; -import { Injectable, NotFoundException } from "@nestjs/common"; -import { TotalExpenses, TotalHours } from "src/time-and-attendance/utils/type.utils"; +import { Injectable } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; import { Timesheets } from "src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto"; import { Result } from "src/common/errors/result-error.factory"; +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( @@ -61,14 +77,14 @@ export class GetTimesheetsOverviewService { const employee_fullname = `${user.first_name} ${user.last_name}`.trim(); //maps all timesheet's infos - const timesheets = rows.map((timesheet) => this.mapOneTimesheet(timesheet)); - return { success: true, data: { employee_fullname, timesheets } }; + const timesheets = await Promise.all(rows.map((timesheet) => this.mapOneTimesheet(timesheet))); + + return { success: true, data:{ employee_fullname, timesheets} }; } catch (error) { return { success: false, error} } } - //----------------------------------------------------------------------------------- // MAPPERS & HELPERS //----------------------------------------------------------------------------------- @@ -111,11 +127,14 @@ export class GetTimesheetsOverviewService { 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, @@ -139,6 +158,7 @@ export class GetTimesheetsOverviewService { is_approved: expense.is_approved ?? false, comment: expense.comment ?? '', supervisor_comment: expense.supervisor_comment, + type: expense.type, })); //daily totals diff --git a/src/time-and-attendance/utils/resolve-bank-type-id.utils.ts b/src/time-and-attendance/utils/resolve-bank-type-id.utils.ts index 017c49f..725a84f 100644 --- a/src/time-and-attendance/utils/resolve-bank-type-id.utils.ts +++ b/src/time-and-attendance/utils/resolve-bank-type-id.utils.ts @@ -1,44 +1,47 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { Prisma, PrismaClient } from "@prisma/client"; +import { Result } from "src/common/errors/result-error.factory"; import { PrismaService } from "src/prisma/prisma.service"; type Tx = Prisma.TransactionClient | PrismaClient; @Injectable() export class BankCodesResolver { - constructor(private readonly prisma: PrismaService) {} + constructor(private readonly prisma: PrismaService) { } //find id and modifier by type - readonly findIdAndModifierByType = async ( type: string, client?: Tx - ): Promise<{id:number; modifier: number }> => { + readonly findIdAndModifierByType = async (type: string, client?: Tx + ): Promise> => { const db = client ?? this.prisma; const bank = await db.bankCodes.findFirst({ where: { type }, select: { id: true, modifier: true }, }); + if (!bank) return { success: false, error: `Unknown bank code type: ${type}` }; - if(!bank) throw new NotFoundException(`Unknown bank code type: ${type}`); - return { id: bank.id, modifier: bank.modifier }; + return { success: true, data: { id: bank.id, modifier: bank.modifier } }; }; //finds only id by type - readonly findBankCodeIDByType = async (type: string, client?: Tx) => { + readonly findBankCodeIDByType = async (type: string, client?: Tx): Promise> => { const db = client ?? this.prisma; - const bank_code_id = await db.bankCodes.findFirst({ + const bank_code = await db.bankCodes.findFirst({ where: { type }, - select: {id: true}, + select: { id: true }, }); - if(!bank_code_id) throw new NotFoundException(`Unkown bank type: ${type}`); - return bank_code_id; + if (!bank_code) return { success: false, error:`Unkown bank type: ${type}`}; + + return { success: true, data: bank_code.id}; } - - readonly findTypeByBankCodeId = async (bank_code_id: number, client?: Tx) => { + + readonly findTypeByBankCodeId = async (bank_code_id: number, client?: Tx): Promise> => { const db = client ?? this.prisma; - const type = await db.bankCodes.findFirst({ + const bank_code = await db.bankCodes.findFirst({ where: { id: bank_code_id }, select: { type: true }, }); - if(!type) throw new NotFoundException(`Type with id : ${bank_code_id} not found`); - return type; + if (!bank_code) return {success: false, error: `Type with id : ${bank_code_id} not found` } + + return {success: true, data: bank_code.type}; } } \ No newline at end of file diff --git a/src/time-and-attendance/utils/resolve-full-name.utils.ts b/src/time-and-attendance/utils/resolve-full-name.utils.ts index ef6669b..ebb3df5 100644 --- a/src/time-and-attendance/utils/resolve-full-name.utils.ts +++ b/src/time-and-attendance/utils/resolve-full-name.utils.ts @@ -1,5 +1,6 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; +import { Injectable } from "@nestjs/common"; import { Prisma, PrismaClient } from "@prisma/client"; +import { Result } from "src/common/errors/result-error.factory"; import { PrismaService } from "src/prisma/prisma.service"; type Tx = Prisma.TransactionClient | PrismaClient; @@ -8,15 +9,15 @@ type Tx = Prisma.TransactionClient | PrismaClient; export class FullNameResolver { constructor(private readonly prisma: PrismaService){} - readonly resolveFullName = async (employee_id: number, client?: Tx): Promise =>{ + readonly resolveFullName = async (employee_id: number, client?: Tx): Promise> =>{ const db = client ?? this.prisma; const employee = await db.employees.findUnique({ where: { id: employee_id }, select: { user: { select: {first_name: true, last_name: true} } }, }); - if(!employee) throw new NotFoundException(`Unknown user with name: ${employee_id}`) + if(!employee) return { success: false, error: `Unknown user with id ${employee_id}`} const full_name = ( employee.user.first_name + " " + employee.user.last_name ) || " "; - return full_name; + return {success: true, data: full_name }; } } \ No newline at end of file diff --git a/src/time-and-attendance/utils/resolve-shifts-id.utils.ts b/src/time-and-attendance/utils/resolve-shifts-id.utils.ts index e76d144..350069a 100644 --- a/src/time-and-attendance/utils/resolve-shifts-id.utils.ts +++ b/src/time-and-attendance/utils/resolve-shifts-id.utils.ts @@ -1,14 +1,23 @@ import { Prisma, PrismaClient } from "@prisma/client"; -import { NotFoundException } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; -import { ShiftKey } from "src/time-and-attendance/utils/type.utils"; +import { Result } from "src/common/errors/result-error.factory"; type Tx = Prisma.TransactionClient | PrismaClient; +interface ShiftKey { + timesheet_id: number; + date: Date; + start_time: Date; + end_time: Date; + bank_code_id: number; + is_remote: boolean; + comment?: string | null; +} + export class ShiftIdResolver { constructor(private readonly prisma: PrismaService) {} - readonly findShiftIdByData = async ( key: ShiftKey, client?: Tx ): Promise<{id:number}> => { + readonly findShiftIdByData = async ( key: ShiftKey, client?: Tx ): Promise> => { const db = client ?? this.prisma; const shift = await db.shifts.findFirst({ where: { @@ -23,7 +32,8 @@ export class ShiftIdResolver { select: { id: true }, }); - if(!shift) throw new NotFoundException(`shift not found`); - return { id: shift.id }; + if(!shift) return { success: false, error: `shift not found`} + + return { success: true, data: shift.id }; }; } \ No newline at end of file diff --git a/src/time-and-attendance/utils/selects.utils.ts b/src/time-and-attendance/utils/selects.utils.ts index 98a27ec..7938519 100644 --- a/src/time-and-attendance/utils/selects.utils.ts +++ b/src/time-and-attendance/utils/selects.utils.ts @@ -51,36 +51,6 @@ export const leaveRequestsSelect = { }, } satisfies Prisma.LeaveRequestsSelect; - -export const EXPENSE_SELECT = { - date: true, - amount: true, - mileage: true, - comment: true, - is_approved: true, - supervisor_comment: true, - bank_code: { select: { type: true } }, -} as const; - -export const EXPENSE_ASC_ORDER = { date: 'asc' as const }; - -export const PAY_PERIOD_SELECT = { - period_start: true, - period_end: true, -} as const; - -export const SHIFT_SELECT = { - date: true, - start_time: true, - end_time: true, - comment: true, - is_approved: true, - is_remote: true, - bank_code: {select: { type: true } }, -} as const; - -export const SHIFT_ASC_ORDER = [{date: 'asc' as const}, {start_time: 'asc' as const}]; - export const timesheet_select = { id: true, employee_id: true, diff --git a/src/time-and-attendance/utils/type.utils.ts b/src/time-and-attendance/utils/type.utils.ts index b38ecc8..959c6c8 100644 --- a/src/time-and-attendance/utils/type.utils.ts +++ b/src/time-and-attendance/utils/type.utils.ts @@ -1,47 +1,9 @@ -import { Prisma, PrismaClient } from "@prisma/client"; -import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto"; -import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; -import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto"; -import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto"; -import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.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; -}; - export type Normalized = { date: Date; start_time: Date; end_time: Date; bank_code_id: number; }; -export type CreateShiftResult = { ok: true; data: GetShiftDto } | { ok: false; error: any }; -export type UpdateShiftResult = { ok: true; id: number; data: GetShiftDto } | { ok: false; id: number; error: any }; -export type NormedOk = { - index: number; - dto: ShiftEntity; - normed: Normalized; - timesheet_id: number; -}; - -export type DeletePresetResult = { ok: true; id: number; } | { ok: false; id: number; error: any }; -export type CreatePresetResult = { ok: true; } | { ok: false; error: any }; -export type UpdatePresetResult = { ok: true; id: number; data: SchedulePresetsDto } | { ok: false; id: number; error: any }; - export type NormalizedExpense = { date: Date; @@ -51,9 +13,6 @@ export type NormalizedExpense = { parsed_mileage?: number; parsed_attachment?: number; }; -export type CreateExpenseResult = { ok: true; data: GetExpenseDto } | { ok: false; error: any }; -export type DeleteExpenseResult = { ok: true; id: number; } | { ok: false; id: number; error: any }; - export type ShiftResponse = { week_day: string; @@ -76,33 +35,3 @@ export type ApplyResult = { created: number; skipped: number; } - -export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>; - -export type Tx = Prisma.TransactionClient | PrismaClient; - -export type WeekOvertimeSummary = { - week_start:string; - week_end: string; - week_total_hours: number; - weekly_overtime: number; - daily_overtime_kept: number; - total_overtime: number; - breakdown: Array<{ - date:string; - day_hours: number; - day_overtime: number; - daily_kept: number; - running_total_before: number; - }>; -}; - -export interface ShiftKey { - timesheet_id: number; - date: Date; - start_time: Date; - end_time: Date; - bank_code_id: number; - is_remote: boolean; - comment?: string | null; -} \ No newline at end of file diff --git a/targo-backend@0.0.1 b/targo-backend@0.0.1 deleted file mode 100644 index e69de29..0000000 diff --git a/tsx b/tsx deleted file mode 100644 index e69de29..0000000