diff --git a/src/time-and-attendance/expenses/expenses.module.ts b/src/time-and-attendance/expenses/expenses.module.ts index 99870cd..c0206a7 100644 --- a/src/time-and-attendance/expenses/expenses.module.ts +++ b/src/time-and-attendance/expenses/expenses.module.ts @@ -6,6 +6,7 @@ import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; import { EmployeeTimesheetResolver } from "src/common/mappers/timesheet.mapper"; import { ExpenseDeleteService } from "src/time-and-attendance/expenses/services/expense-delete.service"; import { ExpenseCreateService } from "src/time-and-attendance/expenses/services/expense-create.service"; +import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service"; @Module({ controllers: [ExpenseController], @@ -16,6 +17,7 @@ import { ExpenseCreateService } from "src/time-and-attendance/expenses/services/ EmailToIdResolver, BankCodesResolver, EmployeeTimesheetResolver, + PayPeriodEventService, ], }) diff --git a/src/time-and-attendance/pay-period/dtos/pay-period-event.dto.ts b/src/time-and-attendance/pay-period/dtos/pay-period-event.dto.ts new file mode 100644 index 0000000..4bf21a0 --- /dev/null +++ b/src/time-and-attendance/pay-period/dtos/pay-period-event.dto.ts @@ -0,0 +1,5 @@ +export class PayPeriodEvent { + employee_email: string; + event_type: 'expense' | 'shift'; + action: 'create' | 'update' | 'delete'; +} \ No newline at end of file diff --git a/src/time-and-attendance/pay-period/pay-periods.controller.ts b/src/time-and-attendance/pay-period/pay-periods.controller.ts index 41f7ddf..b8b7dab 100644 --- a/src/time-and-attendance/pay-period/pay-periods.controller.ts +++ b/src/time-and-attendance/pay-period/pay-periods.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, ParseIntPipe, Patch } from "@nestjs/common"; +import { Body, Controller, Get, Param, MessageEvent, ParseIntPipe, Patch, Sse } from "@nestjs/common"; import { PayPeriodOverviewDto } from "./dtos/overview-pay-period.dto"; import { PayPeriodsQueryService } from "./services/pay-periods-query.service"; import { PayPeriodsCommandService } from "./services/pay-periods-command.service"; @@ -6,6 +6,8 @@ import { Result } from "src/common/errors/result-error.factory"; import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators"; import { Modules as ModulesEnum } from ".prisma/client"; import { GetOverviewService } from "src/time-and-attendance/pay-period/services/pay-periods-build-overview.service"; +import { map, Observable } from "rxjs"; +import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service"; @Controller('pay-periods') export class PayPeriodsController { @@ -14,8 +16,14 @@ export class PayPeriodsController { private readonly queryService: PayPeriodsQueryService, private readonly getOverviewService: GetOverviewService, private readonly commandService: PayPeriodsCommandService, + private readonly payPeriodEventService: PayPeriodEventService, ) { } + @Sse("subscribe") + sse(): Observable { + return this.payPeriodEventService.stream().pipe(map(event => ({ data: event, }))); + } + @Get("date/:date") @ModuleAccessAllowed(ModulesEnum.timesheets) async findByDate(@Param("date") date: string) { @@ -38,9 +46,9 @@ export class PayPeriodsController { @Body('timesheet_ids') timesheet_ids: number[], @Body('is_approved') is_approved: boolean, ): Promise> { - if (!email) return {success: false, error: 'EMAIL_REQUIRED'}; - if (!timesheet_ids || timesheet_ids.length < 1) return {success: false, error: 'TIMESHEET_ID_REQUIRED'}; - if (is_approved === null) return {success: false, error: 'APPROVAL_STATUS_REQUIRED'} + if (!email) return { success: false, error: 'EMAIL_REQUIRED' }; + if (!timesheet_ids || timesheet_ids.length < 1) return { success: false, error: 'TIMESHEET_ID_REQUIRED' }; + if (is_approved === null) return { success: false, error: 'APPROVAL_STATUS_REQUIRED' } return this.commandService.bulkApproveEmployee(email, timesheet_ids, is_approved); } 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 3e375db..eaba3ce 100644 --- a/src/time-and-attendance/pay-period/pay-periods.module.ts +++ b/src/time-and-attendance/pay-period/pay-periods.module.ts @@ -5,6 +5,7 @@ import { PayPeriodsCommandService } from "src/time-and-attendance/pay-period/ser import { TimesheetsModule } from "src/time-and-attendance/timesheets/timesheets.module"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; import { GetOverviewService } from "src/time-and-attendance/pay-period/services/pay-periods-build-overview.service"; +import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service"; @Module({ imports:[TimesheetsModule], @@ -13,6 +14,7 @@ import { GetOverviewService } from "src/time-and-attendance/pay-period/services/ PayPeriodsQueryService, PayPeriodsCommandService, GetOverviewService, + PayPeriodEventService, EmailToIdResolver, ], }) diff --git a/src/time-and-attendance/pay-period/services/pay-period-event.service.ts b/src/time-and-attendance/pay-period/services/pay-period-event.service.ts new file mode 100644 index 0000000..f895103 --- /dev/null +++ b/src/time-and-attendance/pay-period/services/pay-period-event.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from "@nestjs/common"; +import { Observable, Subject } from "rxjs"; +import { PayPeriodEvent } from "src/time-and-attendance/pay-period/dtos/pay-period-event.dto"; + +@Injectable() +export class PayPeriodEventService { + private readonly pay_period_events$ = new Subject(); + + emit(event: PayPeriodEvent) { + this.pay_period_events$.next(event); + } + + stream(): Observable { + return this.pay_period_events$.asObservable(); + } +} \ No newline at end of file diff --git a/src/time-and-attendance/shifts/services/shifts-create.service.ts b/src/time-and-attendance/shifts/services/shifts-create.service.ts index 9482525..9ae63d7 100644 --- a/src/time-and-attendance/shifts/services/shifts-create.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-create.service.ts @@ -11,6 +11,8 @@ import { VacationService } from "src/time-and-attendance/domains/services/vacati import { BankedHoursService } from "src/time-and-attendance/domains/services/banking-hours.service"; import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service"; import { paid_time_off_types } from "src/time-and-attendance/paid-time-off/paid-time-off.dto"; +import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service"; +import { Modules } from "@prisma/client"; @Injectable() export class ShiftsCreateService { @@ -21,15 +23,16 @@ export class ShiftsCreateService { private readonly vacationService: VacationService, private readonly bankingService: BankedHoursService, private readonly sickService: SickLeaveService, + private readonly payPeriodEventService: PayPeriodEventService, ) { } //_________________________________________________________________ // CREATE WRAPPER FUNCTION FOR ONE OR MANY INPUT //_________________________________________________________________ - async createOneOrManyShifts(email: string, shifts: ShiftDto[]): Promise> { + async createOneOrManyShifts(email: string, shifts: ShiftDto[], is_from_timesheet: boolean = true): Promise> { try { //verify if array is empty or not - if (!Array.isArray(shifts) || shifts.length === 0) return { success: false, error: 'No data received' }; + 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); @@ -57,6 +60,15 @@ export class ShiftsCreateService { //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' }; + // push to event service to notify timesheet-approval subscribers of change + if (is_from_timesheet) { + this.payPeriodEventService.emit({ + employee_email: email, + event_type: 'shift', + action: 'create' + }) + } + // returns array of created shifts return { success: true, data: true } } catch (error) { @@ -150,6 +162,7 @@ export class ShiftsCreateService { comment: dto.comment ?? '', }, }); + //builds an object to return for display in the frontend const shift: ShiftDto = { id: created_shift.id, diff --git a/src/time-and-attendance/shifts/services/shifts-delete.service.ts b/src/time-and-attendance/shifts/services/shifts-delete.service.ts index a0c161b..cdf8878 100644 --- a/src/time-and-attendance/shifts/services/shifts-delete.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-delete.service.ts @@ -1,14 +1,18 @@ import { Injectable } from "@nestjs/common"; import { Result } from "src/common/errors/result-error.factory"; +import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; import { computeHours } from "src/common/utils/date-utils"; import { PrismaService } from "src/prisma/prisma.service"; import { PaidTimeOFfBankHoursService } from "src/time-and-attendance/paid-time-off/paid-time-off.service"; +import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service"; @Injectable() export class ShiftsDeleteService { constructor( private readonly prisma: PrismaService, private readonly paidTimeOffService: PaidTimeOFfBankHoursService, + private readonly emailResolver: EmailToIdResolver, + private readonly payPeriodEventService: PayPeriodEventService, ) { } //_________________________________________________________________ // DELETE @@ -16,8 +20,29 @@ export class ShiftsDeleteService { //finds shifts using shit_ids //ajust paid-time-off banks //blocs deletion if approved - async deleteShift(shift_id: number): Promise> { + async deleteShift(shift_id: number, email: string, is_from_timesheet: boolean = true): Promise> { try { + + //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 }; + + // check if shift actually belongs to employee + const shift = await this.prisma.shifts.findUnique({ + where: { id: shift_id }, + select: { + timesheet: { + select: { + employee_id: true, + } + } + } + }); + + if (!shift || shift.timesheet.employee_id !== employee_id.data) + return { success: false, error: 'SHIFT_NOT_FOUND'} + + // return deletion result return await this.prisma.$transaction(async (tx) => { const shift = await tx.shifts.findUnique({ where: { id: shift_id }, @@ -31,9 +56,9 @@ export class ShiftsDeleteService { bank_code: { select: { type: true } }, }, }); - + if (!shift) return { success: false, error: `SHIFT_NOT_FOUND` }; - if (shift.is_approved) return { success: false, error: 'APPROUVED_SHIFT' }; + if (shift.is_approved) return { success: false, error: 'APPROVED_SHIFT' }; //call to ajust paid_time_off hour banks await this.paidTimeOffService.updatePaidTimeoffBankHoursWhenShiftDelete( @@ -43,6 +68,16 @@ export class ShiftsDeleteService { shift.timesheet.employee_id ); await tx.shifts.delete({ where: { id: shift_id } }); + + // push to event service to notify timesheet-approval subscribers of change + if (is_from_timesheet) { + this.payPeriodEventService.emit({ + employee_email: email, + event_type: 'shift', + action: 'delete' + }) + } + return { success: true, data: shift.id }; }); } catch (error) { diff --git a/src/time-and-attendance/shifts/services/shifts-update.service.ts b/src/time-and-attendance/shifts/services/shifts-update.service.ts index 2c9f0fc..75c1b09 100644 --- a/src/time-and-attendance/shifts/services/shifts-update.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-update.service.ts @@ -12,6 +12,7 @@ import { ShiftDto } from "src/time-and-attendance/shifts/shift.dto"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; import { PaidTimeOFfBankHoursService } from "src/time-and-attendance/paid-time-off/paid-time-off.service"; import { paid_time_off_types } from "src/time-and-attendance/paid-time-off/paid-time-off.dto"; +import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service"; @Injectable() export class ShiftsUpdateService { @@ -21,9 +22,10 @@ export class ShiftsUpdateService { private readonly timesheetResolver: EmployeeTimesheetResolver, private readonly emailResolver: EmailToIdResolver, private readonly paidTimeOffService: PaidTimeOFfBankHoursService, + private readonly payPeriodEventService: PayPeriodEventService, ) { } - async updateOneOrManyShifts(shifts: ShiftDto[], email: string): Promise> { + async updateOneOrManyShifts(shifts: ShiftDto[], email: string, is_from_timesheet: boolean = true): Promise> { try { //verify if array is empty or not if (!Array.isArray(shifts) || shifts.length === 0) return { success: false, error: 'No data received' }; @@ -54,6 +56,15 @@ export class ShiftsUpdateService { //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' }; + // push to event service to notify timesheet-approval subscribers of change + if (is_from_timesheet) { + this.payPeriodEventService.emit({ + employee_email: email, + event_type: 'shift', + action: 'create' + }) + } + // returns array of updated shifts return { success: true, data: true } } catch (error) { diff --git a/src/time-and-attendance/shifts/shift.controller.ts b/src/time-and-attendance/shifts/shift.controller.ts index abc4c0b..dc0ff25 100644 --- a/src/time-and-attendance/shifts/shift.controller.ts +++ b/src/time-and-attendance/shifts/shift.controller.ts @@ -26,8 +26,8 @@ export class ShiftController { @Post('create/:email') @ModuleAccessAllowed(ModulesEnum.timesheets_approval) - createBatchByTimesheetsApproval(@Param('email') email:string, @Body() dtos: ShiftDto[]): Promise> { - return this.create_service.createOneOrManyShifts(email,dtos); + createBatchByTimesheetsApproval(@Param('email') email: string, @Body() dtos: ShiftDto[]): Promise> { + return this.create_service.createOneOrManyShifts(email, dtos, false); } @Patch('update') @@ -36,10 +36,21 @@ export class ShiftController { return this.update_service.updateOneOrManyShifts(dtos, email); } - @Delete(':shift_id') - @ModuleAccessAllowed(ModulesEnum.timesheets) - remove(@Param('shift_id') shift_id: number): Promise> { - return this.delete_service.deleteShift(shift_id); + @Patch('update/:email') + @ModuleAccessAllowed(ModulesEnum.timesheets_approval) + updateBatchByTimesheetApproval(@Param('email') email: string, @Body() dtos: ShiftDto[]): Promise> { + return this.update_service.updateOneOrManyShifts(dtos, email, false); } + @Delete(':shift_id') + @ModuleAccessAllowed(ModulesEnum.timesheets) + remove(@Access('email') email: string, @Param('shift_id') shift_id: number): Promise> { + return this.delete_service.deleteShift(shift_id, email); + } + + @Delete(':shift_id/:email') + @ModuleAccessAllowed(ModulesEnum.timesheets) + removeByTimesheetApproval(@Param('shift_id') shift_id: number, @Param('email') email: string): Promise> { + return this.delete_service.deleteShift(shift_id, email, false); + } } diff --git a/src/time-and-attendance/shifts/shifts.module.ts b/src/time-and-attendance/shifts/shifts.module.ts index b6c090e..5d979e0 100644 --- a/src/time-and-attendance/shifts/shifts.module.ts +++ b/src/time-and-attendance/shifts/shifts.module.ts @@ -9,6 +9,7 @@ import { VacationService } from 'src/time-and-attendance/domains/services/vacati import { BankedHoursService } from 'src/time-and-attendance/domains/services/banking-hours.service'; import { PaidTimeOffModule } from 'src/time-and-attendance/paid-time-off/paid-time-off.module'; import { PaidTimeOFfBankHoursService } from 'src/time-and-attendance/paid-time-off/paid-time-off.service'; +import { PayPeriodEventService } from 'src/time-and-attendance/pay-period/services/pay-period-event.service'; @Module({ imports: [PaidTimeOffModule], @@ -20,6 +21,7 @@ import { PaidTimeOFfBankHoursService } from 'src/time-and-attendance/paid-time-o VacationService, BankedHoursService, PaidTimeOFfBankHoursService, + PayPeriodEventService, ], exports: [ ShiftsCreateService,