diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index b5bb7c6..fe774db 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -215,114 +215,6 @@ ] } }, - "/schedule-presets/create": { - "post": { - "operationId": "SchedulePresetsController_createPreset", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SchedulePresetsDto" - } - } - } - }, - "responses": { - "201": { - "description": "" - } - }, - "tags": [ - "SchedulePresets" - ] - } - }, - "/schedule-presets/update/{preset_id}": { - "patch": { - "operationId": "SchedulePresetsController_updatePreset", - "parameters": [ - { - "name": "preset_id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SchedulePresetsUpdateDto" - } - } - } - }, - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "SchedulePresets" - ] - } - }, - "/schedule-presets/delete/{preset_id}": { - "delete": { - "operationId": "SchedulePresetsController_deletePreset", - "parameters": [ - { - "name": "preset_id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "SchedulePresets" - ] - } - }, - "/schedule-presets/find-list": { - "get": { - "operationId": "SchedulePresetsController_findListById", - "parameters": [], - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "SchedulePresets" - ] - } - }, - "/schedule-presets/apply-presets": { - "post": { - "operationId": "SchedulePresetsController_applyPresets", - "parameters": [], - "responses": { - "201": { - "description": "" - } - }, - "tags": [ - "SchedulePresets" - ] - } - }, "/expense/create": { "post": { "operationId": "ExpenseController_create", @@ -727,14 +619,6 @@ } }, "schemas": { - "SchedulePresetsDto": { - "type": "object", - "properties": {} - }, - "SchedulePresetsUpdateDto": { - "type": "object", - "properties": {} - }, "ExpenseDto": { "type": "object", "properties": {} diff --git a/src/common/mappers/bank-type-id.mapper.ts b/src/common/mappers/bank-type-id.mapper.ts index f887abf..be35579 100644 --- a/src/common/mappers/bank-type-id.mapper.ts +++ b/src/common/mappers/bank-type-id.mapper.ts @@ -17,7 +17,7 @@ export class BankCodesResolver { where: { type }, select: { id: true, modifier: true }, }); - if (!bank) return { success: false, error: `Unknown bank code type: ${type}` }; + if (!bank) return { success: false, error: `INVALID_TYPE` }; return { success: true, data: { id: bank.id, modifier: bank.modifier } }; }; @@ -30,7 +30,7 @@ export class BankCodesResolver { where: { type }, select: { id: true }, }); - if (!bank_code) return { success: false, error: `Unkown bank type: ${type}` }; + if (!bank_code) return { success: false, error: `INVALID_TYPE` }; return { success: true, data: bank_code.id }; } @@ -42,7 +42,7 @@ export class BankCodesResolver { where: { id: bank_code_id }, select: { type: true }, }); - if (!bank_code) return { success: false, error: `Type with id : ${bank_code_id} not found` } + if (!bank_code) return { success: false, error: `INVALID_TYPE` } return { success: true, data: bank_code.type }; } diff --git a/src/common/mappers/email-id.mapper.ts b/src/common/mappers/email-id.mapper.ts index 731c817..eccdcfb 100644 --- a/src/common/mappers/email-id.mapper.ts +++ b/src/common/mappers/email-id.mapper.ts @@ -18,7 +18,7 @@ export class EmailToIdResolver { where: { user: { email } }, select: { id: true }, }); - if (!employee) return { success: false, error: `Employee with email:${email} not found` }; + if (!employee) return { success: false, error: `EMPLOYEE_NOT_FOUND` }; return { data: employee.id, success: true }; } @@ -30,7 +30,7 @@ export class EmailToIdResolver { where: { email }, select: { id: true }, }); - if (!user) return { success: false, error: `User with email:${email} not found` }; + if (!user) return { success: false, error: `EMPLOYEE_NOT_FOUND` }; return { success: true, data: user.id }; } } \ No newline at end of file diff --git a/src/common/mappers/full-name.mapper.ts b/src/common/mappers/full-name.mapper.ts index ebb3df5..e193e03 100644 --- a/src/common/mappers/full-name.mapper.ts +++ b/src/common/mappers/full-name.mapper.ts @@ -15,7 +15,7 @@ export class FullNameResolver { where: { id: employee_id }, select: { user: { select: {first_name: true, last_name: true} } }, }); - if(!employee) return { success: false, error: `Unknown user with id ${employee_id}`} + if(!employee) return { success: false, error: `INVALID_EMPLOYEE`} const full_name = ( employee.user.first_name + " " + employee.user.last_name ) || " "; return {success: true, data: full_name }; diff --git a/src/common/mappers/shifts-id.mapper.ts b/src/common/mappers/shifts-id.mapper.ts index 35ae5e2..c50d703 100644 --- a/src/common/mappers/shifts-id.mapper.ts +++ b/src/common/mappers/shifts-id.mapper.ts @@ -32,7 +32,7 @@ export class ShiftIdResolver { }, select: { id: true }, }); - if (!shift) return { success: false, error: `shift not found` } + if (!shift) return { success: false, error: `SHIFT_NOT_FOUND` } return { success: true, data: shift.id }; }; diff --git a/src/common/mappers/timesheet.mapper.ts b/src/common/mappers/timesheet.mapper.ts index 85ef223..7474bf9 100644 --- a/src/common/mappers/timesheet.mapper.ts +++ b/src/common/mappers/timesheet.mapper.ts @@ -25,7 +25,7 @@ export class EmployeeTimesheetResolver { where: { employee_id : employee_id.data, start_date: start_date }, select: { id: true }, }); - if(!timesheet) throw new NotFoundException(`timesheet not found`); + if(!timesheet) throw new NotFoundException(`TIMESHEET_NOT_FOUND`); return { success: true, data: {id: timesheet.id} }; } } \ 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 9f32e46..b23d282 100644 --- a/src/time-and-attendance/expenses/controllers/expense.controller.ts +++ b/src/time-and-attendance/expenses/controllers/expense.controller.ts @@ -3,8 +3,7 @@ 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"; +import { Result } from "src/common/errors/result-error.factory"; @Controller('expense') @RolesAllowed(...GLOBAL_CONTROLLER_ROLES) diff --git a/src/time-and-attendance/expenses/dtos/expense-entity.dto.ts b/src/time-and-attendance/expenses/dtos/expense-entity.dto.ts deleted file mode 100644 index cf9a8d6..0000000 --- a/src/time-and-attendance/expenses/dtos/expense-entity.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -// import { BankCodeEntity } from "src/modules/bank-codes/dtos/bank-code-entity"; - -export class ExpenseEntity { - id: number; - timesheet_id: number; - bank_code_id: number; - attachment?:number | null; - date: Date; - amount?: number | null; - mileage?:number | null; - comment: string; - supervisor_comment?:string | null; - is_approved: boolean; - // bank_code?: BankCodeEntity; -} \ No newline at end of file diff --git a/src/time-and-attendance/expenses/dtos/expense-get.dto.ts b/src/time-and-attendance/expenses/dtos/expense-get.dto.ts deleted file mode 100644 index 773a1a7..0000000 --- a/src/time-and-attendance/expenses/dtos/expense-get.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class GetExpenseDto { - id: number; - timesheet_id: number; - bank_code_id: number; - attachment?: number; - date: string; - comment: string; - mileage?: number; - amount?: number; - supervisor_comment?: string; - is_approved: boolean; -} \ No newline at end of file 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 0945443..f29df61 100644 --- a/src/time-and-attendance/expenses/services/expense-upsert.service.ts +++ b/src/time-and-attendance/expenses/services/expense-upsert.service.ts @@ -38,7 +38,7 @@ export class ExpenseUpsertService { where: { start_date, employee_id: employee_id.data }, select: { id: true, employee_id: true }, }); - if (!timesheet) return { success: false, error: `Timesheet with id : ${dto.timesheet_id} not found` }; + if (!timesheet) return { success: false, error: `TIMESHEET_NOT_FOUND` }; //create a new expense const expense = await this.prisma.expenses.create({ @@ -50,7 +50,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` }; + if (!expense) return { success: false, error: `INVALID_EXPENSE` }; //build an object to return to the frontend to display const created: ExpenseDto = { @@ -65,7 +65,7 @@ export class ExpenseUpsertService { return { success: true, data: created }; } catch (error) { - return { success: false, error: `An error occured during creation. Expense not created : ` + error }; + return { success: false, error: 'INVALID_EXPENSE' }; } } @@ -88,15 +88,14 @@ export class ExpenseUpsertService { where: { start_date: new_timesheet_start_date, employee_id: employee_id.data }, select: timesheet_select, }); - if (!timesheet) return { success: false, error: `Timesheet ${dto.timesheet_id} not found` } - + if (!timesheet) return { success: false, error: `TIMESHEET_NOT_FOUND` } //checks for modifications const data: Prisma.ExpensesUpdateInput = { ...normed_expense.data, is_approved: dto.is_approved, }; - if (!data) return { success: false, error: `An error occured during normalization. Expense with id: ${dto.id} is invalid` } + if (!data) return { success: false, error: `INVALID_EXPENSE` } //push updates and get updated datas const expense = await this.prisma.expenses.update({ @@ -104,7 +103,7 @@ export class ExpenseUpsertService { data, select: expense_select, }); - if (!expense) return { success: false, error: `An error occured during update. Expense with id: ${dto.id} was not updated` } + if (!expense) return { success: false, error: 'INVALID_EXPENSE' } //build an object to return to the frontend const updated: ExpenseDto = { @@ -118,7 +117,7 @@ export class ExpenseUpsertService { }; return { success: true, data: updated }; } catch (error) { - return { success: false, error: (`Expense with id: ${dto.id} generated an error:` + error) }; + return { success: false, error: 'EXPENSE_NOT_FOUND' }; } } //_________________________________________________________________ @@ -131,20 +130,14 @@ export class ExpenseUpsertService { where: { id: expense_id }, select: { id: true }, }); - if (!expense) return { - success: false, - error: `An error occured during removal. Expense with id :${expense_id} was not found ` - }; + if (!expense) return { success: false, error: `EXPENSE_NOT_FOUND` }; await tx.expenses.delete({ where: { id: expense.id } }); return { success: true, data: expense.id }; }); return { success: true, data: expense_id }; } catch (error) { - return { - success: false, - error: `An error occured during removal. Expense with id :${expense_id} generated an error: ` + error - }; + return { success: false, error: `EXPENSE_NOT_FOUND` }; } } @@ -163,7 +156,7 @@ export class ExpenseUpsertService { const date = toDateFromString(dto.date); const type = await this.typeResolver.findBankCodeIDByType(dto.type); - if (!type.success) return { success: false, error: 'Bank-type not found' } + if (!type.success) return { success: false, error: 'INVALID_EXPENSE_TYPE' } return { success: true, diff --git a/src/time-and-attendance/pay-period/controllers/pay-periods.controller.ts b/src/time-and-attendance/pay-period/controllers/pay-periods.controller.ts index 421acb8..dc02a87 100644 --- a/src/time-and-attendance/pay-period/controllers/pay-periods.controller.ts +++ b/src/time-and-attendance/pay-period/controllers/pay-periods.controller.ts @@ -2,13 +2,14 @@ import { Body, Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto"; import { PayPeriodsQueryService } from "../services/pay-periods-query.service"; import { RolesAllowed } from "src/common/decorators/roles.decorators"; -import { Roles as RoleEnum } from '.prisma/client'; import { PayPeriodsCommandService } from "../services/pay-periods-command.service"; import { PayPeriodBundleDto } from "../dtos/bundle-pay-period.dto"; import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto"; - +import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes"; +import { Result } from "src/common/errors/result-error.factory"; @Controller('pay-periods') +@RolesAllowed(...GLOBAL_CONTROLLER_ROLES) export class PayPeriodsController { constructor( @@ -17,12 +18,14 @@ export class PayPeriodsController { ) { } @Get('current-and-all') - async getCurrentAndAll(@Query('date') date?: string): Promise { - const [current, periods] = await Promise.all([ - this.queryService.findCurrent(date), - this.queryService.findAll(), - ]); - return { current, periods }; + async getCurrentAndAll(@Query('date') date?: string): Promise> { + const current = await this.queryService.findCurrent(date); + if (!current.success) return { success: false, error: 'INVALID_PAY_PERIOD' }; + + const periods = await this.queryService.findAll(); + if (!periods.success) return { success: false, error: 'INVALID_PAY_PERIOD' }; + + return { success: true, data: { current: current.data, periods: periods.data } }; } @Get("date/:date") @@ -39,31 +42,30 @@ export class PayPeriodsController { } @Patch("crew/pay-period-approval") - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + @RolesAllowed(...MANAGER_ROLES) async bulkApproval(@Req() req, @Body() dto: BulkCrewApprovalDto) { const email = req.user?.email; - if(!email) throw new UnauthorizedException(`Session infos not found`); + if (!email) throw new UnauthorizedException(`Session infos not found`); return this.commandService.bulkApproveCrew(email, dto); } @Get('crew/:year/:periodNumber') - @RolesAllowed(RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + @RolesAllowed(...MANAGER_ROLES) async getCrewOverview(@Req() req, @Param('year', ParseIntPipe) year: number, @Param('periodNumber', ParseIntPipe) period_no: number, @Query('includeSubtree', new ParseBoolPipe({ optional: true })) include_subtree = false, - ): Promise { + ): Promise> { const email = req.user?.email; - if(!email) throw new UnauthorizedException(`Session infos not found`); + if (!email) throw new UnauthorizedException(`Session infos not found`); return this.queryService.getCrewOverview(year, period_no, email, include_subtree); } @Get('overview/:year/:periodNumber') - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) async getOverviewByYear( @Param('year', ParseIntPipe) year: number, @Param('periodNumber', ParseIntPipe) period_no: number, - ): Promise { + ): Promise> { return this.queryService.getOverviewByYearPeriod(year, period_no); } } diff --git a/src/time-and-attendance/pay-period/pay-periods.module.ts b/src/time-and-attendance/pay-period/pay-periods.module.ts index 9c0327d..d7c25be 100644 --- a/src/time-and-attendance/pay-period/pay-periods.module.ts +++ b/src/time-and-attendance/pay-period/pay-periods.module.ts @@ -2,7 +2,7 @@ import { PayPeriodsQueryService } from "./services/pay-periods-query.service"; import { PayPeriodsController } from "./controllers/pay-periods.controller"; import { Module } from "@nestjs/common"; import { PayPeriodsCommandService } from "src/time-and-attendance/pay-period/services/pay-periods-command.service"; -import { TimesheetsModule } from "src/time-and-attendance/time-tracker/timesheets/timesheets.module"; +import { TimesheetsModule } from "src/time-and-attendance/timesheets/timesheets.module"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; @Module({ diff --git a/src/time-and-attendance/pay-period/services/pay-periods-command.service.ts b/src/time-and-attendance/pay-period/services/pay-periods-command.service.ts index ede4caa..fb0d2d8 100644 --- a/src/time-and-attendance/pay-period/services/pay-periods-command.service.ts +++ b/src/time-and-attendance/pay-period/services/pay-periods-command.service.ts @@ -1,44 +1,47 @@ -import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; +import { Injectable, NotFoundException } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto"; import { PayPeriodsQueryService } from "./pay-periods-query.service"; -import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service"; +import { TimesheetApprovalService } from "src/time-and-attendance/timesheets/services/timesheet-approval.service"; +import { Result } from "src/common/errors/result-error.factory"; + +//change promise to return result pattern @Injectable() export class PayPeriodsCommandService { constructor( private readonly prisma: PrismaService, private readonly timesheetsApproval: TimesheetApprovalService, private readonly query: PayPeriodsQueryService, - ) {} + ) { } //function to approve pay-periods according to selected crew members - async bulkApproveCrew(email: string, dto:BulkCrewApprovalDto): Promise<{updated: number}> { + async bulkApproveCrew(email: string, dto: BulkCrewApprovalDto): Promise> { const { include_subtree, items } = dto; - if(!items?.length) throw new BadRequestException('no items to process'); + if (!items?.length) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; //fetch and validate supervisor status const supervisor = await this.query.getSupervisor(email); - if(!supervisor) throw new NotFoundException('No employee record linked to current user'); - if(!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor'); + if (!supervisor) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; + if (!supervisor.is_supervisor) return { success: false, error: 'INVALID_EMPLOYEE' }; //fetches emails of crew members linked to supervisor const crew_emails = await this.query.resolveCrewEmails(supervisor.id, include_subtree); + if (!crew_emails.success) return { success: false, error: 'INVALID_EMAIL' }; - - for(const item of items) { - if(!crew_emails.has(item.employee_email)) { - throw new ForbiddenException(`Employee ${item.employee_email} not in supervisor crew`); + for (const item of items) { + if (!crew_emails.data.has(item.employee_email)) { + return { success: false, error: 'INVALID_EMPLOYEE' } } } - const period_cache = new Map(); - const getPeriod = async (year:number, period_no: number) => { + const period_cache = new Map(); + const getPeriod = async (year: number, period_no: number) => { const key = `${year}-${period_no}`; - if(period_cache.has(key)) return period_cache.get(key)!; + if (period_cache.has(key)) return period_cache.get(key)!; - const period = await this.query.getPeriodWindow(year,period_no); - if(!period) throw new NotFoundException(`Pay period ${year}-${period_no} not found`); + const period = await this.query.getPeriodWindow(year, period_no); + if (!period) throw new NotFoundException(`Pay period ${year}-${period_no} not found`); period_cache.set(key, period); return period; }; @@ -46,27 +49,27 @@ export class PayPeriodsCommandService { let updated = 0; await this.prisma.$transaction(async (transaction) => { - for(const item of items) { + for (const item of items) { const { period_start, period_end } = await getPeriod(item.pay_year, item.period_no); const timesheets = await transaction.timesheets.findMany({ - where: { + where: { employee: { user: { email: item.employee_email } }, OR: [ - {shift : { some: { date: { gte: period_start, lte: period_end } } } }, - {expense: { some: { date: { gte: period_start, lte: period_end } } } }, + { shift: { some: { date: { gte: period_start, lte: period_end } } } }, + { expense: { some: { date: { gte: period_start, lte: period_end } } } }, ], }, select: { id: true }, }); - for(const { id } of timesheets) { + for (const { id } of timesheets) { await this.timesheetsApproval.cascadeApprovalWithtx(transaction, id, item.approve); updated++; } - + } }); - return {updated}; + return { success: true, data: { updated } }; } } \ No newline at end of file diff --git a/src/time-and-attendance/pay-period/services/pay-periods-query.service.ts b/src/time-and-attendance/pay-period/services/pay-periods-query.service.ts index 2048b3e..8978651 100644 --- a/src/time-and-attendance/pay-period/services/pay-periods-query.service.ts +++ b/src/time-and-attendance/pay-period/services/pay-periods-query.service.ts @@ -1,23 +1,24 @@ -import { ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; +import { Injectable } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { computeHours, computePeriod, listPayYear, payYearOfDate } from "src/common/utils/date-utils"; import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto"; import { EmployeePeriodOverviewDto } from "../dtos/overview-employee-period.dto"; import { PayPeriodDto } from "../dtos/pay-period.dto"; import { mapPayPeriodToDto } from "../mappers/pay-periods.mapper"; -import { Prisma } from "@prisma/client"; +import { Result } from "src/common/errors/result-error.factory"; + @Injectable() export class PayPeriodsQueryService { constructor(private readonly prisma: PrismaService) { } - async getOverview(pay_period_no: number): Promise { + async getOverview(pay_period_no: number): Promise> { const period = await this.prisma.payPeriods.findFirst({ where: { pay_period_no }, orderBy: { pay_year: "desc" }, }); - if (!period) throw new NotFoundException(`Period #${pay_period_no} not found`); + if (!period) return { success: false, error: `PAY_PERIOD_NOT_FOUND` }; - return this.buildOverview({ + const overview = await this.buildOverview({ period_start: period.period_start, period_end: period.period_end, payday: period.payday, @@ -25,23 +26,28 @@ export class PayPeriodsQueryService { pay_year: period.pay_year, label: period.label, }); + if (!overview.success) return { success: false, error: 'INVALID_PAY_PERIOD' } + + return { success: true, data: overview.data } } - async getOverviewByYearPeriod(pay_year: number, period_no: number): Promise { + async getOverviewByYearPeriod(pay_year: number, period_no: number): Promise> { const period = computePeriod(pay_year, period_no); - return this.buildOverview({ + const overview = await this.buildOverview({ period_start: period.period_start, period_end: period.period_end, period_no: period.period_no, pay_year: period.pay_year, payday: period.payday, label: period.label, - } as any); + }); + if (!overview.success) return { success: false, error: 'INVALID_PAY_PERIOD' } + return { success: true, data: overview.data } } //find crew member associated with supervisor private async resolveCrew(supervisor_id: number, include_subtree: boolean): - Promise> { + Promise, string>> { const result: Array<{ id: number; first_name: string; last_name: string; email: string; }> = []; let frontier = await this.prisma.employees.findMany({ @@ -52,7 +58,7 @@ export class PayPeriodsQueryService { id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name, email: emp.user.email }))); - if (!include_subtree) return result; + if (!include_subtree) return { success: true, data: result }; while (frontier.length) { const parent_ids = frontier.map(emp => emp.id); @@ -66,20 +72,21 @@ export class PayPeriodsQueryService { }))); frontier = next; } - return result; + return { success: true, data: result }; } //fetchs crew emails - async resolveCrewEmails(supervisor_id: number, include_subtree: boolean): Promise> { + async resolveCrewEmails(supervisor_id: number, include_subtree: boolean): Promise, string>> { const crew = await this.resolveCrew(supervisor_id, include_subtree); - return new Set(crew.map(crew_member => crew_member.email).filter(Boolean)); + if (!crew.success) return { success: false, error: crew.error } + return { success: true, data: new Set(crew.data.map(crew_member => crew_member.email).filter(Boolean)) } } async getCrewOverview(pay_year: number, period_no: number, email: string, include_subtree: boolean): - Promise { + Promise> { // 1) Search for the period const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no: period_no } }); - if (!period) throw new NotFoundException(`Pay period ${pay_year}-${period_no} not found`); + if (!period) return { success: false, error: 'PAY_PERIOD_NOT_FOUND' } // 2) fetch supervisor const supervisor = await this.prisma.employees.findFirst({ @@ -90,15 +97,16 @@ export class PayPeriodsQueryService { }, }); - if (!supervisor) throw new NotFoundException('No employee record linked to current user'); - if (!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor'); + if (!supervisor) return { success: false, error: 'EMPLOYEE_NOT_FOUND' } + if (!supervisor.is_supervisor) return { success: false, error: 'INVALID_EMPLOYEE' } // 3)fetchs crew members const crew = await this.resolveCrew(supervisor.id, include_subtree); // [{ id, first_name, last_name }] - const crew_ids = crew.map(c => c.id); + if (!crew.success) return { success: false, error: crew.error } + const crew_ids = crew.data.map(c => c.id); // seed names map for employee without data const seed_names = new Map( - crew.map(crew => [ + crew.data.map(crew => [ crew.id, { name: `${crew.first_name} ${crew.last_name}`.trim(), @@ -107,17 +115,18 @@ export class PayPeriodsQueryService { ] ) ); - - // 4) overview build - return this.buildOverview({ + const overview = await this.buildOverview({ period_no: period.pay_period_no, period_start: period.period_start, period_end: period.period_end, payday: period.payday, pay_year: period.pay_year, label: period.label, - //add is_approved - }, { filtered_employee_ids: crew_ids, seed_names }); + }, { filtered_employee_ids: crew_ids, seed_names }) + if (!overview.success) return { success: false, error: 'INVALID_PAY_PERIOD' } + + // 4) overview build + return { success: true, data: overview.data } } private async buildOverview( @@ -126,7 +135,7 @@ export class PayPeriodsQueryService { period_no: number; pay_year: number; label: string; }, //add is_approved options?: { filtered_employee_ids?: number[]; seed_names?: Map } - ): Promise { + ): Promise> { const toDateString = (d: Date) => d.toISOString().slice(0, 10); const toMoney = (v: any) => (typeof v === "object" && "toNumber" in v ? v.toNumber() : (v as number)); @@ -344,13 +353,16 @@ export class PayPeriodsQueryService { ); return { - pay_period_no: period.period_no, - pay_year: period.pay_year, - payday: toDateString(payd), - period_start: toDateString(start), - period_end: toDateString(end), - label: period.label, - employees_overview, + success: true, + data: { + pay_period_no: period.period_no, + pay_year: period.pay_year, + payday: toDateString(payd), + period_start: toDateString(start), + period_end: toDateString(end), + label: period.label, + employees_overview, + } }; } @@ -361,72 +373,82 @@ export class PayPeriodsQueryService { }); } - async findAll(): Promise { + async findAll(): Promise> { const currentPayYear = payYearOfDate(new Date()); - return listPayYear(currentPayYear).map(period => ({ - pay_period_no: period.period_no, - pay_year: period.pay_year, - payday: period.payday, - period_start: period.period_start, - period_end: period.period_end, - label: period.label, - //add is_approved - })); + return { + success: true, + data: listPayYear(currentPayYear).map(period => ({ + pay_period_no: period.period_no, + pay_year: period.pay_year, + payday: period.payday, + period_start: period.period_start, + period_end: period.period_end, + label: period.label, + })) + }; } - async findOne(period_no: number): Promise { + async findOne(period_no: number): Promise> { const row = await this.prisma.payPeriods.findFirst({ where: { pay_period_no: period_no }, orderBy: { pay_year: "desc" }, }); - if (!row) throw new NotFoundException(`Pay period #${period_no} not found`); - return mapPayPeriodToDto(row); + if (!row) return { success: false, error: `PAY_PERIOD_NOT_FOUND` } + return { success: true, data: mapPayPeriodToDto(row) }; } - async findCurrent(date?: string): Promise { + async findCurrent(date?: string): Promise> { const iso_day = date ?? new Date().toISOString().slice(0, 10); - return this.findByDate(iso_day); + const pay_period = await this.findByDate(iso_day); + if (!pay_period.success) return { success: false, error: 'INVALID_PAY_PERIOD' } + return { success: true, data: pay_period.data } } - async findOneByYearPeriod(pay_year: number, period_no: number): Promise { + async findOneByYearPeriod(pay_year: number, period_no: number): Promise> { const row = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no: period_no }, }); - if (row) return mapPayPeriodToDto(row); + if (row) return { success: true, data: mapPayPeriodToDto(row) }; // fallback for outside of view periods const period = computePeriod(pay_year, period_no); return { - pay_period_no: period.period_no, - pay_year: period.pay_year, - period_start: period.period_start, - payday: period.payday, - period_end: period.period_end, - label: period.label + success: true, + data: { + pay_period_no: period.period_no, + pay_year: period.pay_year, + period_start: period.period_start, + payday: period.payday, + period_end: period.period_end, + label: period.label + } } } //function to cherry pick a Date to find a period - async findByDate(date: string): Promise { + async findByDate(date: string): Promise> { const dt = new Date(date); const row = await this.prisma.payPeriods.findFirst({ where: { period_start: { lte: dt }, period_end: { gte: dt } }, }); - if (row) return mapPayPeriodToDto(row); + if (row) return { success: true, data: mapPayPeriodToDto(row) }; //fallback for outwside view periods const pay_year = payYearOfDate(date); const periods = listPayYear(pay_year); const hit = periods.find(period => date >= period.period_start && date <= period.period_end); - if (!hit) throw new NotFoundException(`No period found for ${date}`); + if (!hit) return { success: false, error: `PAY_PERIOD_NOT_FOUND` } return { - pay_period_no: hit.period_no, - pay_year: hit.pay_year, - period_start: hit.period_start, - period_end: hit.period_end, - payday: hit.payday, - label: hit.label + success: true, + data: { + pay_period_no: hit.period_no, + pay_year: hit.pay_year, + period_start: hit.period_start, + period_end: hit.period_end, + payday: hit.payday, + label: hit.label + } } } diff --git a/src/time-and-attendance/schedule-presets/controller/schedule-presets.controller.ts b/src/time-and-attendance/schedule-presets/controller/schedule-presets.controller.ts new file mode 100644 index 0000000..1bc6630 --- /dev/null +++ b/src/time-and-attendance/schedule-presets/controller/schedule-presets.controller.ts @@ -0,0 +1,64 @@ +// import { Controller, Param, Query, Body, Get, Post, ParseIntPipe, Delete, Patch, Req } from "@nestjs/common"; +// import { RolesAllowed } from "src/common/decorators/roles.decorators"; +// import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes"; +// import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; +// import { SchedulePresetsUpdateDto } from "src/time-and-attendance/schedule-presets/dtos/update-schedule-presets.dto"; +// import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service"; +// import { SchedulePresetsGetService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-get.service"; +// import { SchedulePresetsUpsertService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service"; + +// @Controller('schedule-presets') +// @RolesAllowed(...GLOBAL_CONTROLLER_ROLES) +// export class SchedulePresetsController { +// constructor( +// private readonly upsertService: SchedulePresetsUpsertService, +// private readonly getService: SchedulePresetsGetService, +// private readonly applyPresetsService: SchedulePresetsApplyService, +// ) { } + +// //used to create a schedule preset +// @Post('create') +// @RolesAllowed(...MANAGER_ROLES) +// async createPreset(@Req() req, @Body() dto: SchedulePresetsDto) { +// const email = req.user?.email; +// return await this.upsertService.createPreset(email, dto); +// } + +// // //used to update an already existing schedule preset +// // @Patch('update/:preset_id') +// // @RolesAllowed(...MANAGER_ROLES) +// // async updatePreset( +// // @Param('preset_id', ParseIntPipe) preset_id: number, +// // @Body() dto: SchedulePresetsUpdateDto +// // ) { +// // return await this.upsertService.updatePreset(preset_id, dto); +// // } + +// //used to delete a schedule preset +// @Delete('delete/:preset_id') +// @RolesAllowed(...MANAGER_ROLES) +// async deletePreset( +// @Param('preset_id', ParseIntPipe) preset_id: number) { +// return await this.upsertService.deletePreset(preset_id); +// } + + +// //used to show the list of available schedule presets +// @Get('find-list') +// @RolesAllowed(...MANAGER_ROLES) +// async findListById(@Req() req) { +// const email = req.user?.email; +// return this.getService.getSchedulePresets(email); +// } + +// //used to apply a preset to a timesheet +// @Post('apply-presets') +// async applyPresets( +// @Req() req, +// @Body('preset') preset_id: number, +// @Body('start') start_date: string +// ) { +// const email = req.user?.email; +// return this.applyPresetsService.applyToTimesheet(email, preset_id, start_date); +// } +// } \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts b/src/time-and-attendance/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts similarity index 100% rename from src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts rename to src/time-and-attendance/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts diff --git a/src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto.ts b/src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto.ts similarity index 85% rename from src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto.ts rename to src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto.ts index fb58f69..7064ee4 100644 --- a/src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto.ts +++ b/src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto.ts @@ -1,5 +1,5 @@ import { ArrayMinSize, IsArray, IsBoolean, IsInt, IsOptional, IsString } from "class-validator"; -import { SchedulePresetShiftsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto"; +import { SchedulePresetShiftsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-preset-shifts.dto"; export class SchedulePresetsDto { diff --git a/src/time-and-attendance/schedule-presets/schedule-presets.module.ts b/src/time-and-attendance/schedule-presets/schedule-presets.module.ts new file mode 100644 index 0000000..1c7801d --- /dev/null +++ b/src/time-and-attendance/schedule-presets/schedule-presets.module.ts @@ -0,0 +1,21 @@ + +import { Module } from "@nestjs/common"; +// import { SchedulePresetsController } from "src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller"; +// import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service"; +// import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service"; +// import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; + + +@Module({ + controllers: [/*SchedulePresetsController*/], + // providers: [ + // SchedulePresetsUpsertService, + // SchedulePresetsGetService, + // SchedulePresetsApplyService, + // ], + exports:[ + // SchedulePresetsUpsertService, + // SchedulePresetsGetService, + // SchedulePresetsApplyService, + ], +}) export class SchedulePresetsModule {} \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts similarity index 98% rename from src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts rename to src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts index a564ae2..bc9a51e 100644 --- a/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts @@ -1,4 +1,4 @@ -import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common"; +import { Injectable } from "@nestjs/common"; import { Weekday, Prisma } from "@prisma/client"; import { DATE_ISO_FORMAT, WEEKDAY } from "src/common/utils/constants.utils"; import { PrismaService } from "src/prisma/prisma.service"; diff --git a/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-get.service.ts similarity index 100% rename from src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts rename to src/time-and-attendance/schedule-presets/services/schedule-presets-get.service.ts diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service.ts new file mode 100644 index 0000000..7644f90 --- /dev/null +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service.ts @@ -0,0 +1,227 @@ +// import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common"; +// import { Prisma, Weekday } from "@prisma/client"; +// import { PrismaService } from "src/prisma/prisma.service"; +// import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; +// import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; +// import { Result } from "src/common/errors/result-error.factory"; +// import { toHHmmFromDate, toDateFromString } from "src/common/utils/date-utils"; +// import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; + +// @Injectable() +// export class SchedulePresetsUpsertService { +// constructor( +// private readonly prisma: PrismaService, +// private readonly typeResolver: BankCodesResolver, +// private readonly emailResolver: EmailToIdResolver, +// ) { } +// //_________________________________________________________________ +// // CREATE +// //_________________________________________________________________ +// async createPreset(email: string, dto: SchedulePresetsDto): Promise> { +// try { +// const shifts_data = await this.normalizePresetShifts(dto); +// 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 }; + +// const created = await this.prisma.$transaction(async (tx) => { +// if (dto.is_default) { +// await tx.schedulePresets.updateMany({ +// 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.data }, +// }, +// }); +// return { success: true, data: created } +// } +// }); +// 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> { +// try { +// const existing = await this.prisma.schedulePresets.findFirst({ +// where: { id: preset_id }, +// select: { +// id: true, +// is_default: true, +// employee_id: true, +// }, +// }); +// if (!existing) return { success: false, error: `Preset "${dto.name}" not found` }; + +// const shifts_data = await this.normalizePresetShifts(dto); +// if (!shifts_data.success) return { success: false, error: 'An error occured during normalization' } + +// await this.prisma.$transaction(async (tx) => { +// if (typeof dto.is_default === 'boolean') { +// if (dto.is_default) { +// await tx.schedulePresets.updateMany({ +// where: { +// employee_id: existing.employee_id, +// is_default: true, +// NOT: { id: existing.id }, +// }, +// data: { is_default: false }, +// }); +// } +// await tx.schedulePresets.update({ +// where: { id: existing.id }, +// data: { +// is_default: dto.is_default, +// name: dto.name, +// }, +// }); +// } +// if (shifts_data.data.length <= 0) return { success: false, error: 'Preset shifts to update not found' }; + +// await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } }); + +// try { +// const create_many_data: Result = +// shifts_data.data.map((shift) => { +// if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') { +// return { success: false, error: `Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`} +// } +// const bank_code_id = shift.bank_code.connect.id; +// return { +// preset_id: existing.id, +// week_day: shift.week_day, +// sort_order: shift.sort_order, +// start_time: shift.start_time, +// end_time: shift.end_time, +// is_remote: shift.is_remote ?? false, +// bank_code_id: bank_code_id, +// }; +// }); +// if(!create_many_data.success) return { success: false, error: 'Invalid data'} +// await tx.schedulePresetShifts.createMany({ data: create_many_data.data }); + +// return { success: true, data: create_many_data } +// } catch (error) { +// return { success: false, error: 'An error occured. Invalid data detected. ' }; +// } +// }); + +// const saved = await this.prisma.schedulePresets.findUnique({ +// where: { id: existing.id }, +// include: { +// shifts: { +// orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }], +// include: { bank_code: { select: { type: true } } }, +// } +// }, +// }); +// if (!saved) return { success: false, error: `Preset with id: ${existing.id} not found` }; + +// const response_dto: SchedulePresetsDto = { +// id: saved.id, +// name: saved.name, +// is_default: saved.is_default, +// preset_shifts: saved.shifts.map((shift) => ({ +// preset_id: shift.preset_id, +// week_day: shift.week_day, +// sort_order: shift.sort_order, +// type: shift.bank_code.type, +// start_time: toHHmmFromDate(shift.start_time), +// end_time: toHHmmFromDate(shift.end_time), +// is_remote: shift.is_remote, +// })), +// }; + +// return { success: true, data: response_dto }; +// } catch (error) { +// return { success: false, error: 'An error occured during update. Invalid data' } +// } +// } + +// //_________________________________________________________________ +// // DELETE +// //_________________________________________________________________ +// async deletePreset(preset_id: number): Promise> { +// try { +// await this.prisma.$transaction(async (tx) => { +// const preset = await tx.schedulePresets.findFirst({ +// where: { id: preset_id }, +// select: { id: true }, +// }); +// if (!preset) return { success: false, error: `Preset with id ${preset_id} not found` }; +// await tx.schedulePresets.delete({ where: { id: preset_id } }); + +// return { success: true }; +// }); +// return { success: true, data: preset_id }; + +// } catch (error) { +// return { success: false, error: `Preset schedule with id ${preset_id} not found` }; +// } +// } + +// //PRIVATE HELPERS + +// //resolve bank_code_id using type and convert hours to TIME and valid shifts end/start +// private async normalizePresetShifts( +// dto: SchedulePresetsDto +// ): Promise> { +// if (!dto.preset_shifts?.length) return { success: false, error: `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 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(); +// for (const shift of dto.preset_shifts) { +// const key = `${shift.week_day}:${shift.sort_order}`; +// if (pair_set.has(key)) { +// return { success: false, error: `Duplicate shift for day/order (${shift.week_day}, ${shift.sort_order})` } +// } +// pair_set.add(key); +// } + +// const items = await dto.preset_shifts.map((shift) => { +// try { +// const bank_code_id = bank_code_set.get(shift.type); +// if (!bank_code_id) return { success: false, error: `Bank code not found for type ${shift.type}` } +// if (!shift.start_time || !shift.end_time) { +// return { success: false, error: `start_time and end_time are required for (${shift.week_day}, ${shift.sort_order})` } +// } +// const start = toDateFromString(shift.start_time); +// const end = toDateFromString(shift.end_time); +// if (end.getTime() <= start.getTime()) { +// return { success: false, error: `end_time must be > start_time ( day: ${shift.week_day}, order: ${shift.sort_order})` } +// } +// return { +// sort_order: shift.sort_order, +// start_time: start, +// end_time: end, +// is_remote: !!shift.is_remote, +// week_day: shift.week_day as Weekday, +// bank_code: { connect: { id: bank_code_id } }, +// } + +// } catch (error) { +// return { success: false, error: '' } +// } +// }); +// return { success: true, data: items}; +// } +// } diff --git a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts b/src/time-and-attendance/shifts/controllers/shift.controller.ts similarity index 82% rename from src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts rename to src/time-and-attendance/shifts/controllers/shift.controller.ts index 3c796c5..c0fa32d 100644 --- a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts +++ b/src/time-and-attendance/shifts/controllers/shift.controller.ts @@ -1,10 +1,10 @@ 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 { 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"; +import { ShiftDto } from "src/time-and-attendance/shifts/dtos/shift-create.dto"; +import { ShiftsCreateService } from "src/time-and-attendance/shifts/services/shifts-create.service"; +import { ShiftsUpdateDeleteService } from "src/time-and-attendance/shifts/services/shifts-update-delete.service"; @Controller('shift') diff --git a/src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto.ts b/src/time-and-attendance/shifts/dtos/shift-create.dto.ts similarity index 100% rename from src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto.ts rename to src/time-and-attendance/shifts/dtos/shift-create.dto.ts diff --git a/src/time-and-attendance/time-tracker/shifts/services/shifts-archival.service.ts b/src/time-and-attendance/shifts/services/shifts-archival.service.ts similarity index 100% rename from src/time-and-attendance/time-tracker/shifts/services/shifts-archival.service.ts rename to src/time-and-attendance/shifts/services/shifts-archival.service.ts diff --git a/src/time-and-attendance/time-tracker/shifts/services/shifts-create.service.ts b/src/time-and-attendance/shifts/services/shifts-create.service.ts similarity index 78% rename from src/time-and-attendance/time-tracker/shifts/services/shifts-create.service.ts rename to src/time-and-attendance/shifts/services/shifts-create.service.ts index 534df5c..060a761 100644 --- a/src/time-and-attendance/time-tracker/shifts/services/shifts-create.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-create.service.ts @@ -4,9 +4,9 @@ import { timesheet_select } from "src/time-and-attendance/utils/selects.utils"; import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; 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"; import { toStringFromHHmm, toStringFromDate, toDateFromString, overlaps, toHHmmFromString } from "src/common/utils/date-utils"; +import { ShiftDto } from "src/time-and-attendance/shifts/dtos/shift-create.dto"; @Injectable() export class ShiftsCreateService { @@ -63,26 +63,14 @@ export class ShiftsCreateService { try { //transform string format to date and HHmm const normed_shift = await this.normalizeShiftDto(dto); - 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.data.start_time)},` - + `end_time: ${toStringFromHHmm(normed_shift.data.end_time)},` - + `date: ${toStringFromDate(normed_shift.data.date)}.` - } + if (!normed_shift.success) return { success: false, error: normed_shift.error }; + if (normed_shift.data.end_time <= normed_shift.data.start_time) return { success: false, error: `INVALID_SHIFT_TIME` }; //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.data.start_time)},` - + `end_time: ${toStringFromHHmm(normed_shift.data.end_time)},` - + `date: ${toStringFromDate(normed_shift.data.date)}.` - } + if (!timesheet) return { success: false, error: `INVALID_TIMESHEET` }; //finds bank_code_id using the type const bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type); if (!bank_code_id.success) return { success: false, error: bank_code_id.error }; @@ -102,13 +90,7 @@ export class ShiftsCreateService { { start: existing_start, end: existing_end, date: existing_date }, ); if (has_overlap) { - return { - success: false, - error: `SHIFT_OVERLAP` - + `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.data.date)})`, - } + return { success: false, error: `SHIFT_OVERLAP` }; } } @@ -139,7 +121,7 @@ export class ShiftsCreateService { } return { success: true, data: shift }; } catch (error) { - return { success: false, error: `An error occured during creation, invalid data` }; + return { success: false, error: `INVALID_SHIFT` }; } } @@ -148,13 +130,14 @@ export class ShiftsCreateService { //_________________________________________________________________ //converts all string hours and date to Date and HHmm formats 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 bank_code_id = await this.typeResolver.findBankCodeIDByType(dto.type); + if (!bank_code_id.success) return { success: false, error: 'INVALID_SHIFT' } + //TODO: validate date and time to ensure "banana" is not accepted using an if statement and a REGEX const date = toDateFromString(dto.date); const start_time = toHHmmFromString(dto.start_time); const end_time = toHHmmFromString(dto.end_time); - - return { success: true, data: {date, start_time, end_time, bank_code_id: bank_code_id.data} }; + + return { success: true, data: { date, start_time, end_time, bank_code_id: bank_code_id.data } }; } } diff --git a/src/time-and-attendance/time-tracker/shifts/services/shifts-get.service.ts b/src/time-and-attendance/shifts/services/shifts-get.service.ts similarity index 100% rename from src/time-and-attendance/time-tracker/shifts/services/shifts-get.service.ts rename to src/time-and-attendance/shifts/services/shifts-get.service.ts diff --git a/src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service.ts b/src/time-and-attendance/shifts/services/shifts-update-delete.service.ts similarity index 83% rename from src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service.ts rename to src/time-and-attendance/shifts/services/shifts-update-delete.service.ts index 47344bf..85f4d87 100644 --- a/src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-update-delete.service.ts @@ -3,10 +3,10 @@ import { PrismaService } from "src/prisma/prisma.service"; import { shift_select } from "src/time-and-attendance/utils/selects.utils"; import { Injectable } from "@nestjs/common"; import { Normalized } from "src/time-and-attendance/utils/type.utils"; -import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; import { Result } from "src/common/errors/result-error.factory"; import { EmployeeTimesheetResolver } from "src/common/mappers/timesheet.mapper"; import { toDateFromString, toStringFromHHmm, toStringFromDate, toHHmmFromString, overlaps } from "src/common/utils/date-utils"; +import { ShiftDto } from "src/time-and-attendance/shifts/dtos/shift-create.dto"; @Injectable() export class ShiftsUpdateDeleteService { @@ -67,18 +67,12 @@ export class ShiftsUpdateDeleteService { where: { id: dto.id, timesheet_id: timesheet.data.id }, select: shift_select, }); - if (!original) return { success: false, error: `Shift with id: ${dto.id} not found` }; + if (!original) return { success: false, error: `SHIFT_NOT_FOUND` }; //transform string format to date and HHmm const normed_shift = await this.normalizeShiftDto(dto); - 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.data.start_time)},` - + `end_time: ${toStringFromHHmm(normed_shift.data.end_time)},` - + `date: ${toStringFromDate(normed_shift.data.date)}.` - }; + if (!normed_shift.success) return { success: false, error: normed_shift.error } + if (normed_shift.data.end_time <= normed_shift.data.start_time) return { success: false, error: `INVALID_SHIFT` }; //finds bank_code_id using the type const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type); @@ -98,7 +92,7 @@ export class ShiftsUpdateDeleteService { }, select: shift_select, }); - if (!updated) return { success: false, error: ' An error occured during update, Invalid Datas' }; + if (!updated) return { success: false, error: 'INVALID_SHIFT' }; // builds an object to return for display in the frontend const shift: ShiftDto = { @@ -115,7 +109,7 @@ export class ShiftsUpdateDeleteService { return { success: true, data: shift }; } catch (error) { - return { success: false, error: `An error occured during update. Invalid Data` }; + return { success: false, error: `INVALID_SHIFT` }; } } @@ -131,13 +125,13 @@ export class ShiftsUpdateDeleteService { 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 ` }; + if (!shift) return { success: false, error: `SHIFT_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` } + return { success: false, error: `SHIFT_NOT_FOUND` } } } @@ -146,7 +140,7 @@ export class ShiftsUpdateDeleteService { //_________________________________________________________________ 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' } + if (!bank_code_id.success) return { success: false, error: 'INVALID_SHIFT' } return { success: true, data: { @@ -163,21 +157,13 @@ export class ShiftsUpdateDeleteService { for (let j = i + 1; j < shifts.length; j++) { const shift_a = shifts[i]; const shift_b = shifts[j]; - + if (shift_a.date !== shift_b.date || shift_a.id === shift_b.id) continue; const has_overlap = overlaps( { start: toHHmmFromString(shift_a.start_time), end: toHHmmFromString(shift_a.end_time) }, { start: toHHmmFromString(shift_b.start_time), end: toHHmmFromString(shift_b.end_time) }, ); - if (has_overlap) { - return { - success: false, - error: `SHIFT_OVERLAP` - + `new shift: ${shift_a.start_time}–${shift_a.end_time} ` - + `existing shift: ${shift_b.start_time}–${shift_b.end_time} ` - + `date: ${shift_a.date})`, - } - } + if (has_overlap) return { success: false, error: `SHIFT_OVERLAP` }; } } return { success: true, data: undefined } diff --git a/src/time-and-attendance/time-tracker/shifts/shifts.module.ts b/src/time-and-attendance/shifts/shifts.module.ts similarity index 51% rename from src/time-and-attendance/time-tracker/shifts/shifts.module.ts rename to src/time-and-attendance/shifts/shifts.module.ts index a68a467..7599e92 100644 --- a/src/time-and-attendance/time-tracker/shifts/shifts.module.ts +++ b/src/time-and-attendance/shifts/shifts.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; -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 { ShiftsUpdateDeleteService } from 'src/time-and-attendance/time-tracker/shifts/services/shifts-update-delete.service'; +import { ShiftController } from 'src/time-and-attendance/shifts/controllers/shift.controller'; +import { ShiftsCreateService } from 'src/time-and-attendance/shifts/services/shifts-create.service'; +import { ShiftsUpdateDeleteService } from 'src/time-and-attendance/shifts/services/shifts-update-delete.service'; @Module({ controllers: [ShiftController], diff --git a/src/time-and-attendance/time-and-attendance.module.ts b/src/time-and-attendance/time-and-attendance.module.ts index 0ff6657..2364b9a 100644 --- a/src/time-and-attendance/time-and-attendance.module.ts +++ b/src/time-and-attendance/time-and-attendance.module.ts @@ -4,18 +4,11 @@ import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-l import { ExpenseController } from "src/time-and-attendance/expenses/controllers/expense.controller"; import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service"; import { PayperiodsModule } from "src/time-and-attendance/pay-period/pay-periods.module"; -import { SchedulePresetsController } from "src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller"; -import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service"; -import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service"; -import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; -import { 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 { 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"; -import { TimesheetsModule } from "src/time-and-attendance/time-tracker/timesheets/timesheets.module"; + +import { TimesheetController } from "src/time-and-attendance/timesheets/controllers/timesheet.controller"; +import { TimesheetApprovalService } from "src/time-and-attendance/timesheets/services/timesheet-approval.service"; +import { GetTimesheetsOverviewService } from "src/time-and-attendance/timesheets/services/timesheet-get-overview.service"; +import { TimesheetsModule } from "src/time-and-attendance/timesheets/timesheets.module"; import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; import { EmployeeTimesheetResolver } from "src/common/mappers/timesheet.mapper"; @@ -26,6 +19,12 @@ import { PayPeriodsCommandService } from "src/time-and-attendance/pay-period/ser import { CsvExportModule } from "src/modules/exports/csv-exports.module"; import { CsvExportService } from "src/modules/exports/services/csv-exports.service"; import { CsvExportController } from "src/modules/exports/controllers/csv-exports.controller"; +import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service"; +import { SchedulePresetsGetService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-get.service"; +import { ShiftController } from "src/time-and-attendance/shifts/controllers/shift.controller"; +import { ShiftsCreateService } from "src/time-and-attendance/shifts/services/shifts-create.service"; +import { ShiftsGetService } from "src/time-and-attendance/shifts/services/shifts-get.service"; +import { ShiftsUpdateDeleteService } from "src/time-and-attendance/shifts/services/shifts-update-delete.service"; @Module({ imports: [ @@ -39,7 +38,7 @@ import { CsvExportController } from "src/modules/exports/controllers/csv-exports controllers: [ TimesheetController, ShiftController, - SchedulePresetsController, + // SchedulePresetsController, ExpenseController, PayPeriodsController, CsvExportController, @@ -51,7 +50,7 @@ import { CsvExportController } from "src/modules/exports/controllers/csv-exports ShiftsCreateService, ShiftsUpdateDeleteService, ExpenseUpsertService, - SchedulePresetsUpsertService, + // SchedulePresetsUpsertService, SchedulePresetsGetService, SchedulePresetsApplyService, EmailToIdResolver, diff --git a/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts b/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts deleted file mode 100644 index a4a7b9e..0000000 --- a/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Controller, Param, Query, Body, Get, Post, ParseIntPipe, Delete, Patch, Req } from "@nestjs/common"; -import { RolesAllowed } from "src/common/decorators/roles.decorators"; -import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; -import { SchedulePresetsUpdateDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/update-schedule-presets.dto"; -import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service"; -import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service"; -import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; -import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes"; - -@Controller('schedule-presets') -@RolesAllowed(...GLOBAL_CONTROLLER_ROLES) -export class SchedulePresetsController { - constructor( - private readonly upsertService: SchedulePresetsUpsertService, - private readonly getService: SchedulePresetsGetService, - private readonly applyPresetsService: SchedulePresetsApplyService, - ) { } - - //used to create a schedule preset - @Post('create') - @RolesAllowed(...MANAGER_ROLES) - async createPreset(@Req() req, @Body() dto: SchedulePresetsDto) { - const email = req.user?.email; - return await this.upsertService.createPreset(email, dto); - } - - //used to update an already existing schedule preset - @Patch('update/:preset_id') - @RolesAllowed(...MANAGER_ROLES) - async updatePreset( - @Param('preset_id', ParseIntPipe) preset_id: number, - @Body() dto: SchedulePresetsUpdateDto - ) { - return await this.upsertService.updatePreset(preset_id, dto); - } - - //used to delete a schedule preset - @Delete('delete/:preset_id') - @RolesAllowed(...MANAGER_ROLES) - async deletePreset( - @Param('preset_id', ParseIntPipe) preset_id: number) { - return await this.upsertService.deletePreset(preset_id); - } - - - //used to show the list of available schedule presets - @Get('find-list') - @RolesAllowed(...MANAGER_ROLES) - async findListById(@Req() req) { - const email = req.user?.email; - return this.getService.getSchedulePresets(email); - } - - //used to apply a preset to a timesheet - @Post('apply-presets') - async applyPresets( - @Req() req, - @Body('preset') preset_id: number, - @Body('start') start_date: string - ) { - const email = req.user?.email; - return this.applyPresetsService.applyToTimesheet(email, preset_id, start_date); - } -} \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/schedule-presets/dtos/update-schedule-presets.dto.ts b/src/time-and-attendance/time-tracker/schedule-presets/dtos/update-schedule-presets.dto.ts deleted file mode 100644 index b8b9cb6..0000000 --- a/src/time-and-attendance/time-tracker/schedule-presets/dtos/update-schedule-presets.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; - - -export class SchedulePresetsUpdateDto extends SchedulePresetsDto{} \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/schedule-presets/schedule-presets.module.ts b/src/time-and-attendance/time-tracker/schedule-presets/schedule-presets.module.ts deleted file mode 100644 index b17863a..0000000 --- a/src/time-and-attendance/time-tracker/schedule-presets/schedule-presets.module.ts +++ /dev/null @@ -1,21 +0,0 @@ - -import { Module } from "@nestjs/common"; -import { SchedulePresetsController } from "src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller"; -import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service"; -import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service"; -import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; - - -@Module({ - controllers: [SchedulePresetsController], - providers: [ - SchedulePresetsUpsertService, - SchedulePresetsGetService, - SchedulePresetsApplyService, - ], - exports:[ - SchedulePresetsUpsertService, - SchedulePresetsGetService, - SchedulePresetsApplyService, - ], -}) export class SchedulePresetsModule {} \ No newline at end of file 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 deleted file mode 100644 index 01717e9..0000000 --- a/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common"; -import { Prisma, Weekday } from "@prisma/client"; -import { PrismaService } from "src/prisma/prisma.service"; -import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; -import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; -import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; -import { Result } from "src/common/errors/result-error.factory"; -import { toHHmmFromDate, toDateFromString } from "src/common/utils/date-utils"; - -@Injectable() -export class SchedulePresetsUpsertService { - constructor( - private readonly prisma: PrismaService, - private readonly typeResolver: BankCodesResolver, - private readonly emailResolver: EmailToIdResolver, - ) { } - //_________________________________________________________________ - // CREATE - //_________________________________________________________________ - async createPreset(email: string, dto: SchedulePresetsDto): Promise> { - try { - const shifts_data = await this.normalizePresetShifts(dto); - 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 }; - - const created = await this.prisma.$transaction(async (tx) => { - if (dto.is_default) { - await tx.schedulePresets.updateMany({ - 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.data }, - }, - }); - return { success: true, data: created } - } - }); - 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> { - try { - const existing = await this.prisma.schedulePresets.findFirst({ - where: { id: preset_id }, - select: { - id: true, - is_default: true, - employee_id: true, - }, - }); - if (!existing) return { success: false, error: `Preset "${dto.name}" not found` }; - - const shifts_data = await this.normalizePresetShifts(dto); - if(!shifts_data.success) return { success: false, error: 'An error occured during normalization'} - - await this.prisma.$transaction(async (tx) => { - if (typeof dto.is_default === 'boolean') { - if (dto.is_default) { - await tx.schedulePresets.updateMany({ - where: { - employee_id: existing.employee_id, - is_default: true, - NOT: { id: existing.id }, - }, - data: { is_default: false }, - }); - } - await tx.schedulePresets.update({ - where: { id: existing.id }, - data: { - is_default: dto.is_default, - name: dto.name, - }, - }); - } - if (shifts_data.data.length <= 0) return { success: false, error: 'Preset shifts to update not found' }; - - await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } }); - - // try { - // const create_many_data: Result = - // shifts_data.data.map((shift) => { - // if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') { - // return { success: false, error: `Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`} - // } - // const bank_code_id = shift.bank_code.connect.id; - // return { - // preset_id: existing.id, - // week_day: shift.week_day, - // sort_order: shift.sort_order, - // start_time: shift.start_time, - // end_time: shift.end_time, - // is_remote: shift.is_remote ?? false, - // bank_code_id: bank_code_id, - // }; - // }); - // if(!create_many_data.success) return { success: false, error: 'Invalid data'} - // await tx.schedulePresetShifts.createMany({ data: create_many_data.data }); - - // return { success: true, data: create_many_data } - // } catch (error) { - // return { success: false, error: 'An error occured. Invalid data detected. ' }; - // } - }); - - const saved = await this.prisma.schedulePresets.findUnique({ - where: { id: existing.id }, - include: { - shifts: { - orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }], - include: { bank_code: { select: { type: true } } }, - } - }, - }); - if (!saved) return { success: false, error: `Preset with id: ${existing.id} not found` }; - - const response_dto: SchedulePresetsDto = { - id: saved.id, - name: saved.name, - is_default: saved.is_default, - preset_shifts: saved.shifts.map((shift) => ({ - preset_id: shift.preset_id, - week_day: shift.week_day, - sort_order: shift.sort_order, - type: shift.bank_code.type, - start_time: toHHmmFromDate(shift.start_time), - end_time: toHHmmFromDate(shift.end_time), - is_remote: shift.is_remote, - })), - }; - - return { success: true, data: response_dto }; - } catch (error) { - return { success: false, error: 'An error occured during update. Invalid data' } - } - } - - //_________________________________________________________________ - // DELETE - //_________________________________________________________________ - async deletePreset(preset_id: number): Promise> { - try { - await this.prisma.$transaction(async (tx) => { - const preset = await tx.schedulePresets.findFirst({ - where: { id: preset_id }, - select: { id: true }, - }); - if (!preset) return { success: false, error: `Preset with id ${preset_id} not found` }; - await tx.schedulePresets.delete({ where: { id: preset_id } }); - - return { success: true }; - }); - return { success: true, data: preset_id }; - - } catch (error) { - return { success: false, error: `Preset schedule with id ${preset_id} not found` }; - } - } - - //PRIVATE HELPERS - - //resolve bank_code_id using type and convert hours to TIME and valid shifts end/start - private async normalizePresetShifts( - 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))); - const bank_code_set = new Map(); - - for (const type of types) { - 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(); - for (const shift of dto.preset_shifts) { - const key = `${shift.week_day}:${shift.sort_order}`; - if (pair_set.has(key)) { - throw new ConflictException(`Duplicate shift for day/order (${shift.week_day}, ${shift.sort_order})`); - } - pair_set.add(key); - } - - const items: Prisma.SchedulePresetShiftsCreateWithoutPresetInput[] = dto.preset_shifts.map((shift) => { - const bank_code_id = bank_code_set.get(shift.type); - if (!bank_code_id) throw new NotFoundException(`Bank code not found for type ${shift.type}`); - 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 = 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})`); - } - - return { - week_day: shift.week_day as Weekday, - sort_order: shift.sort_order, - bank_code: { connect: { id: bank_code_id } }, - start_time: start, - end_time: end, - is_remote: !!shift.is_remote, - }; - }); - return { success: true, data: items }; - } -} diff --git a/src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto.ts b/src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto.ts deleted file mode 100644 index c5fd877..0000000 --- a/src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class GetShiftDto { - shift_id: number; - timesheet_id: number; - type: string; - date: string; - start_time: string; - end_time: string; - is_remote: boolean; - is_approved: boolean; - comment?: string; -} - diff --git a/src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto.ts b/src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto.ts deleted file mode 100644 index 24d26bc..0000000 --- a/src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto.ts +++ /dev/null @@ -1,67 +0,0 @@ -export class TimesheetEntity { - id: number; - employee_id: number; - start_date: Date; - is_approved: boolean; -} - -export class Timesheets { - employee_fullname: string; - timesheets: Timesheet[]; -} - -export class Timesheet { - timesheet_id: number; - is_approved: boolean; - days: TimesheetDay[]; - weekly_hours: TotalHours; - weekly_expenses: TotalExpenses; -} - -export class TimesheetDay { - date: string; - shifts: Shift[]; - expenses: Expense[]; - daily_hours: TotalHours; - daily_expenses: TotalExpenses; -} - -export class TotalHours { - regular: number; - evening: number; - emergency: number; - overtime: number; - vacation: number; - holiday: number; - sick: number; -} -export class TotalExpenses { - expenses: number; - per_diem: number; - on_call: number; - mileage: number; -} - -export class Shift { - timesheet_id: number; - date: string; - start_time: string; - end_time: string; - type: string; - is_remote: boolean; - is_approved: boolean; - shift_id?: number | null; - comment?: string | null; -} - -export class Expense { - date: string; - is_approved: boolean; - type: string; - comment: string; - amount?: number; - mileage?: number; - attachment?: string; - id?: number | null; - supervisor_comment?: string | null; -} \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts b/src/time-and-attendance/timesheets/controllers/timesheet.controller.ts similarity index 91% rename from src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts rename to src/time-and-attendance/timesheets/controllers/timesheet.controller.ts index 6549695..9f8d6fb 100644 --- a/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts +++ b/src/time-and-attendance/timesheets/controllers/timesheet.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query, Req, UnauthorizedException } from "@nestjs/common"; import { RolesAllowed } from "src/common/decorators/roles.decorators"; -import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service"; -import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service"; +import { GetTimesheetsOverviewService } from "src/time-and-attendance/timesheets/services/timesheet-get-overview.service"; +import { TimesheetApprovalService } from "src/time-and-attendance/timesheets/services/timesheet-approval.service"; import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes"; diff --git a/src/time-and-attendance/timesheets/dtos/timesheet.dto.ts b/src/time-and-attendance/timesheets/dtos/timesheet.dto.ts new file mode 100644 index 0000000..a391684 --- /dev/null +++ b/src/time-and-attendance/timesheets/dtos/timesheet.dto.ts @@ -0,0 +1,70 @@ +import { Type } from "class-transformer"; +import { IsBoolean, IsDate, IsInt, IsOptional, IsString } from "class-validator"; + +export class TimesheetEntity { + @IsInt() id: number; + @IsInt() employee_id: number; + @IsDate() start_date: Date; + @IsBoolean() is_approved: boolean; +} + +export class Timesheets { + @IsString() employee_fullname: string; + @Type(() => Timesheet) timesheets: Timesheet[]; +} + +export class Timesheet { + @IsInt() timesheet_id: number; + @IsBoolean() is_approved: boolean; + @Type(() => TimesheetDay) days: TimesheetDay[]; + @Type(() => TotalHours) weekly_hours: TotalHours[]; + @Type(() => TotalExpenses) weekly_expenses: TotalExpenses[]; +} + +export class TimesheetDay { + @IsString() date: string; + @Type(() => Shift) shifts: Shift[]; + @Type(() => Expense) expenses: Expense[]; + @Type(() => TotalHours) daily_hours: TotalHours[]; + @Type(() => TotalExpenses) daily_expenses: TotalExpenses[]; +} + +export class TotalHours { + @Type(() => Number) regular: number; + @Type(() => Number) evening: number; + @Type(() => Number) emergency: number; + @Type(() => Number) overtime: number; + @Type(() => Number) vacation: number; + @Type(() => Number) holiday: number; + @Type(() => Number) sick: number; +} +export class TotalExpenses { + @Type(() => Number) expenses: number; + @Type(() => Number) per_diem: number; + @Type(() => Number) on_call: number; + @Type(() => Number) mileage: number; +} + +export class Shift { + @IsInt() timesheet_id: number; + @IsString() date: string; + @IsString() start_time: string; + @IsString() end_time: string; + @IsString() type: string; + @IsBoolean() is_remote: boolean; + @IsBoolean() is_approved: boolean; + @IsInt() @IsOptional() shift_id?: number | null; + @IsString() @IsOptional() comment?: string | null; +} + +export class Expense { + @IsString() date: string; + @IsBoolean() is_approved: boolean; + @IsString() type: string; + @IsString() comment: string; + @Type(() => Number) @IsOptional() amount?: number; + @Type(() => Number) @IsOptional() mileage?: number; + @IsString() @IsOptional() attachment?: string; + @IsOptional() @IsInt() id?: number | null; + @IsString() @IsOptional() supervisor_comment?: string | null; +} \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service.ts b/src/time-and-attendance/timesheets/services/timesheet-approval.service.ts similarity index 100% rename from src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service.ts rename to src/time-and-attendance/timesheets/services/timesheet-approval.service.ts diff --git a/src/time-and-attendance/time-tracker/timesheets/services/timesheet-archive.service.ts b/src/time-and-attendance/timesheets/services/timesheet-archive.service.ts similarity index 100% rename from src/time-and-attendance/time-tracker/timesheets/services/timesheet-archive.service.ts rename to src/time-and-attendance/timesheets/services/timesheet-archive.service.ts diff --git a/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts b/src/time-and-attendance/timesheets/services/timesheet-get-overview.service.ts similarity index 95% rename from src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts rename to src/time-and-attendance/timesheets/services/timesheet-get-overview.service.ts index 56853fe..6c8b008 100644 --- a/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts +++ b/src/time-and-attendance/timesheets/services/timesheet-get-overview.service.ts @@ -3,7 +3,7 @@ import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/common/utils/constants.utils import { Injectable } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; -import { Timesheet, Timesheets } from "src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto"; +import { Timesheet, Timesheets } from "src/time-and-attendance/timesheets/dtos/timesheet.dto"; import { Result } from "src/common/errors/result-error.factory"; import { Prisma } from "@prisma/client"; import { toDateFromString, sevenDaysFrom, toStringFromDate, toHHmmFromDate } from "src/common/utils/date-utils"; @@ -41,11 +41,11 @@ export class GetTimesheetsOverviewService { //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` }; + if (!period) return { success: false, error: `PAY_PERIOD_NOT_FOUND` }; //fetch the employee_id using the email const employee_id = await this.emailResolver.findIdByEmail(account_email); - if (!employee_id.success) return { success: false, error: `employee with email: ${account_email} not found` + employee_id.error } + if (!employee_id.success) return { success: false, error: employee_id.error } //loads the timesheets related to the fetched pay-period let rows = await this.loadTimesheets(employee_id.data, period.period_start, period.period_end); @@ -73,7 +73,7 @@ export class GetTimesheetsOverviewService { where: { id: employee_id.data }, include: { user: true }, }); - if (!employee) return { success: false, error: `Employee #${employee_id} not found` } + if (!employee) return { success: false, error: `EMPLOYEE_NOT_FOUND` } //builds employee full name const user = employee.user; @@ -82,11 +82,11 @@ export class GetTimesheetsOverviewService { //maps all timesheet's infos const timesheets = await Promise.all(rows.map((timesheet) => this.mapOneTimesheet(timesheet))); - if (!timesheets) return { success: false, error: 'an error occured during the mapping of a timesheet' } + if (!timesheets) return { success: false, error: 'INVALID_TIMESHEET' } return { success: true, data: { employee_fullname, timesheets } }; } catch (error) { - return { success: false, error: 'timesheet failed to load: ' + pay_year + pay_period_no } + return { success: false, error: 'TIMESHEET_NOT_FOUND' } } } diff --git a/src/time-and-attendance/time-tracker/timesheets/timesheets.module.ts b/src/time-and-attendance/timesheets/timesheets.module.ts similarity index 73% rename from src/time-and-attendance/time-tracker/timesheets/timesheets.module.ts rename to src/time-and-attendance/timesheets/timesheets.module.ts index 6590750..78457f4 100644 --- a/src/time-and-attendance/time-tracker/timesheets/timesheets.module.ts +++ b/src/time-and-attendance/timesheets/timesheets.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; -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'; +import { TimesheetController } from 'src/time-and-attendance/timesheets/controllers/timesheet.controller'; +import { TimesheetApprovalService } from 'src/time-and-attendance/timesheets/services/timesheet-approval.service'; +import { GetTimesheetsOverviewService } from 'src/time-and-attendance/timesheets/services/timesheet-get-overview.service'; import { EmailToIdResolver } from 'src/common/mappers/email-id.mapper'; @Module({