From bf630acb7c1d144138fd19c78170519d97d33a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9rick=20Pruneau?= Date: Tue, 20 Jan 2026 09:02:02 -0500 Subject: [PATCH 1/6] replaced app2 by portail --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 70c84b6..e2af54c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -56,7 +56,7 @@ async function bootstrap() { // Enable CORS app.enableCors({ - origin: ['http://10.100.251.2:9011', 'http://10.100.251.2:9012', 'http://10.100.251.2:9013', 'http://localhost:9000', 'https://app.targo.ca', 'https://app2.targo.ca','https://staging.app.targo.ca'], + origin: ['http://10.100.251.2:9011', 'http://10.100.251.2:9012', 'http://10.100.251.2:9013', 'http://localhost:9000', 'https://app.targo.ca', 'https://portail.targo.ca','https://staging.app.targo.ca'], credentials: true, }); From 646cb58e98db118df3ed465cc1f7dd1d6815a381 Mon Sep 17 00:00:00 2001 From: Nicolas Drolet Date: Tue, 20 Jan 2026 16:28:17 -0500 Subject: [PATCH 2/6] feat(pay-period): begin integration of SSE route for live event-based feedback on timesheet update --- .../expenses/expenses.module.ts | 2 + .../pay-period/dtos/pay-period-event.dto.ts | 5 +++ .../pay-period/pay-periods.controller.ts | 16 ++++++-- .../pay-period/pay-periods.module.ts | 2 + .../services/pay-period-event.service.ts | 16 ++++++++ .../shifts/services/shifts-create.service.ts | 17 +++++++- .../shifts/services/shifts-delete.service.ts | 41 +++++++++++++++++-- .../shifts/services/shifts-update.service.ts | 13 +++++- .../shifts/shift.controller.ts | 23 ++++++++--- .../shifts/shifts.module.ts | 2 + 10 files changed, 121 insertions(+), 16 deletions(-) create mode 100644 src/time-and-attendance/pay-period/dtos/pay-period-event.dto.ts create mode 100644 src/time-and-attendance/pay-period/services/pay-period-event.service.ts 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, From 68883b84e7f53e6669477f4b8cef810a4f37a3ae Mon Sep 17 00:00:00 2001 From: Nic D Date: Wed, 21 Jan 2026 10:21:58 -0500 Subject: [PATCH 3/6] feat(pay-period): add SSE for timesheet-approval when employees update timesheet and a user is on timesheet-approval --- .../expenses/expense.controller.ts | 7 +++- .../services/expense-create.service.ts | 11 +++++- .../services/expense-delete.service.ts | 37 ++++++++++++++++++- .../services/expense-update.service.ts | 13 +++++++ .../pay-period/dtos/pay-period-event.dto.ts | 2 +- .../schedule-presets.module.ts | 2 + .../schedule-presets-apply.service.ts | 10 +++++ .../shifts/services/shifts-update.service.ts | 2 +- .../time-and-attendance.module.ts | 2 + 9 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/time-and-attendance/expenses/expense.controller.ts b/src/time-and-attendance/expenses/expense.controller.ts index 737228e..d910ad4 100644 --- a/src/time-and-attendance/expenses/expense.controller.ts +++ b/src/time-and-attendance/expenses/expense.controller.ts @@ -35,8 +35,11 @@ export class ExpenseController { @Delete('delete/:expense_id') @ModuleAccessAllowed(ModulesEnum.timesheets) - remove(@Param('expense_id') expense_id: number): Promise> { - return this.deleteService.deleteExpense(expense_id); + remove( + @Param('expense_id') expense_id: number, + @Access('email') email: string, + ): Promise> { + return this.deleteService.deleteExpense(expense_id, email); } } diff --git a/src/time-and-attendance/expenses/services/expense-create.service.ts b/src/time-and-attendance/expenses/services/expense-create.service.ts index 6222c83..298c58c 100644 --- a/src/time-and-attendance/expenses/services/expense-create.service.ts +++ b/src/time-and-attendance/expenses/services/expense-create.service.ts @@ -6,6 +6,7 @@ import { toStringFromDate, weekStartSunday } from "src/common/utils/date-utils"; import { PrismaService } from "src/prisma/prisma.service"; import { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto"; import { normalizeAndParseExpenseDto } from "src/time-and-attendance/expenses/expense.utils"; +import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service"; import { expense_select } from "src/time-and-attendance/utils/selects.utils"; @Injectable() @@ -14,6 +15,7 @@ export class ExpenseCreateService { private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver, private readonly typeResolver: BankCodesResolver, + private readonly payPeriodEventService: PayPeriodEventService, ) { } //_________________________________________________________________ @@ -64,8 +66,15 @@ export class ExpenseCreateService { attachment: expense.attachment ?? undefined, supervisor_comment: expense.supervisor_comment ?? undefined, }; - return { success: true, data: created }; + // notify timesheet approval observers of changes + this.payPeriodEventService.emit({ + employee_email: email, + event_type: 'expense', + action: 'create', + }); + + return { success: true, data: created }; } catch (error) { return { success: false, error: 'INVALID_EXPENSE' }; } diff --git a/src/time-and-attendance/expenses/services/expense-delete.service.ts b/src/time-and-attendance/expenses/services/expense-delete.service.ts index 9ad7dd8..01c3bfc 100644 --- a/src/time-and-attendance/expenses/services/expense-delete.service.ts +++ b/src/time-and-attendance/expenses/services/expense-delete.service.ts @@ -1,15 +1,40 @@ import { Injectable } from "@nestjs/common"; import { Result } from "src/common/errors/result-error.factory"; +import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; import { PrismaService } from "src/prisma/prisma.service"; +import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service"; @Injectable() export class ExpenseDeleteService { - constructor(private readonly prisma: PrismaService) { } + constructor( + private readonly prisma: PrismaService, + private readonly payPeriodEventService: PayPeriodEventService, + private readonly emailResolver: EmailToIdResolver, + ){} //_________________________________________________________________ // DELETE //_________________________________________________________________ - async deleteExpense(expense_id: number): Promise> { + async deleteExpense(expense_id: number, email: string): Promise> { + // get employee id of employee who made delete request + const employee = await this.emailResolver.findIdByEmail(email); + + if (!employee.success) return employee; + + // confirm ownership of expense to employee who made request + const expense = await this.prisma.expenses.findUnique({ + where: { id: expense_id}, + select: { + timesheet: { + select: { + employee_id: true, + } + } + } + }); + + if (!expense || expense.timesheet.employee_id !== employee.data) return { success: false, error: 'EXPENSE_NOT_FOUND'}; + try { await this.prisma.$transaction(async (tx) => { const expense = await tx.expenses.findUnique({ @@ -21,6 +46,14 @@ export class ExpenseDeleteService { await tx.expenses.delete({ where: { id: expense.id } }); return { success: true, data: expense.id }; }); + + // notify timesheet-approval observers of changes + this.payPeriodEventService.emit({ + employee_email: email, + event_type: 'expense', + action: 'delete', + }); + return { success: true, data: expense_id }; } catch (error) { return { success: false, error: `EXPENSE_NOT_FOUND` }; diff --git a/src/time-and-attendance/expenses/services/expense-update.service.ts b/src/time-and-attendance/expenses/services/expense-update.service.ts index 3f2e1e5..a5af26a 100644 --- a/src/time-and-attendance/expenses/services/expense-update.service.ts +++ b/src/time-and-attendance/expenses/services/expense-update.service.ts @@ -8,6 +8,7 @@ import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; import { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto"; import { Prisma } from "@prisma/client"; import { normalizeAndParseExpenseDto } from "src/time-and-attendance/expenses/expense.utils"; +import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service"; @Injectable() export class ExpenseUpdateService { @@ -15,6 +16,7 @@ export class ExpenseUpdateService { private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver, private readonly typeResolver: BankCodesResolver, + private readonly payPeriodEventService: PayPeriodEventService, ) { } //_________________________________________________________________ // UPDATE @@ -66,6 +68,17 @@ export class ExpenseUpdateService { attachment: expense.attachment ?? undefined, supervisor_comment: expense.supervisor_comment ?? undefined, }; + + // notify timesheet-approval observers of changes, but only if it came + // from timesheet and not timesheet-approval (no employee_email) + if (!employee_email) { + this.payPeriodEventService.emit({ + employee_email: email, + event_type: 'expense', + action: 'update', + }); + } + return { success: true, data: updated }; } catch (error) { return { success: false, error: 'EXPENSE_NOT_FOUND' }; 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 index 4bf21a0..89faff2 100644 --- 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 @@ -1,5 +1,5 @@ export class PayPeriodEvent { employee_email: string; - event_type: 'expense' | 'shift'; + event_type: 'expense' | 'shift' | 'preset'; action: 'create' | 'update' | 'delete'; } \ No newline at end of file diff --git a/src/time-and-attendance/schedule-presets/schedule-presets.module.ts b/src/time-and-attendance/schedule-presets/schedule-presets.module.ts index 56beb0a..04085d6 100644 --- a/src/time-and-attendance/schedule-presets/schedule-presets.module.ts +++ b/src/time-and-attendance/schedule-presets/schedule-presets.module.ts @@ -13,6 +13,7 @@ import { ShiftsCreateService } from "src/time-and-attendance/shifts/services/shi import { VacationService } from "src/time-and-attendance/domains/services/vacation.service"; import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service"; import { BankedHoursService } from "src/time-and-attendance/domains/services/banking-hours.service"; +import { PayPeriodEventService } from "../pay-period/services/pay-period-event.service"; @@ -30,6 +31,7 @@ import { BankedHoursService } from "src/time-and-attendance/domains/services/ban VacationService, SickLeaveService, BankedHoursService, + PayPeriodEventService, ], exports: [ SchedulePresetsGetService, diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts index 668418a..97ad01c 100644 --- a/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts @@ -11,6 +11,7 @@ import { timesheet_select } from "src/time-and-attendance/utils/selects.utils"; import { ShiftDto } from "src/time-and-attendance/shifts/shift.dto"; import { WEEKDAY_MAP } from "src/time-and-attendance/schedule-presets/schedule-presets.dto"; import { $Enums, Prisma, SchedulePresetShifts } from "@prisma/client"; +import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service"; @Injectable() @@ -20,6 +21,7 @@ export class SchedulePresetsApplyService { private readonly emailResolver: EmailToIdResolver, private readonly shiftService: ShiftsCreateService, private readonly typeResolver: BankCodesResolver, + private readonly payPeriodEventService: PayPeriodEventService, ) { } async applyPresetToTimesheet(email: string, timesheet_id: number): Promise> { @@ -119,6 +121,14 @@ export class SchedulePresetsApplyService { await this.shiftService.createShift(employee_id.data, created_shift.data); } + + // notify timesheet-approval observers of changes + this.payPeriodEventService.emit({ + employee_email: email, + event_type: 'preset', + action: 'create', + }) + return { success: true, data: true }; } 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 75c1b09..dcaaf3d 100644 --- a/src/time-and-attendance/shifts/services/shifts-update.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-update.service.ts @@ -61,7 +61,7 @@ export class ShiftsUpdateService { this.payPeriodEventService.emit({ employee_email: email, event_type: 'shift', - action: 'create' + action: 'update' }) } diff --git a/src/time-and-attendance/time-and-attendance.module.ts b/src/time-and-attendance/time-and-attendance.module.ts index c5d1420..7b7696c 100644 --- a/src/time-and-attendance/time-and-attendance.module.ts +++ b/src/time-and-attendance/time-and-attendance.module.ts @@ -43,6 +43,7 @@ import { SchedulePresetDeleteService } from "src/time-and-attendance/schedule-pr import { SchedulePresetUpdateService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-update.service"; import { SchedulePresetsCreateService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-create.service"; import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service"; +import { PayPeriodEventService } from "./pay-period/services/pay-period-event.service"; @Module({ @@ -90,6 +91,7 @@ import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-pr VacationService, BankedHoursService, PaidTimeOFfBankHoursService, + PayPeriodEventService, ], exports: [TimesheetApprovalService], }) export class TimeAndAttendanceModule { }; \ No newline at end of file From 0b31fc829b5647ab2a1009deead32c71b732aac0 Mon Sep 17 00:00:00 2001 From: "Nic D." <91558719+Venti-Bear@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:27:11 -0500 Subject: [PATCH 4/6] fix(auth): changes to how logout is handled, will now disconnect user from authentik for app only. --- .../controllers/auth.controller.ts | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/identity-and-account/authentication/controllers/auth.controller.ts b/src/identity-and-account/authentication/controllers/auth.controller.ts index cfc91ed..31e5f81 100644 --- a/src/identity-and-account/authentication/controllers/auth.controller.ts +++ b/src/identity-and-account/authentication/controllers/auth.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Req, Res, UnauthorizedException, UseGuards } from '@nestjs/common'; +import { Controller, Get, Post, Req, Res, UnauthorizedException, UseGuards } from '@nestjs/common'; import { OIDCLoginGuard } from '../guards/authentik-auth.guard'; import { Request, Response } from 'express'; import { UsersService } from 'src/identity-and-account/users-management/services/users.service'; @@ -8,19 +8,19 @@ import { Access } from 'src/common/decorators/module-access.decorators'; export class AuthController { constructor( private readonly usersService: UsersService, - ){} + ) { } @UseGuards(OIDCLoginGuard) - @Get('/v1/login') + @Get('v1/login') login() { } - @Get('/callback') + @Get('callback') @UseGuards(OIDCLoginGuard) loginCallback(@Req() req: Request, @Res() res: Response) { res.redirect(process.env.REDIRECT_URL_DEV!); } - @Get('/me') + @Get('me') async getProfile( @Access('email') email: string, @Req() req: Request) { @@ -30,4 +30,19 @@ export class AuthController { return this.usersService.findOneByEmail(email); } + @Post('logout') + logout( + @Req() request: Request, + @Res() response: Response, + ) { + request.session.destroy(error => { + if (error) { + console.error('error during logout: ', error, 'user: ', request.user); + } + + response.clearCookie('connect.sid', { + path: '/', + }); + }) + } } From 652bc1cba1a2546821e7c7ff466278523109b3f3 Mon Sep 17 00:00:00 2001 From: Nic D Date: Tue, 27 Jan 2026 16:12:28 -0500 Subject: [PATCH 5/6] feat(presets): add possibility for user with right permissions to apply presets to employee timesheets --- .../schedule-presets.controller.ts | 25 +++++++++++- .../schedule-presets-apply.service.ts | 40 +++++++++++++------ 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/time-and-attendance/schedule-presets/schedule-presets.controller.ts b/src/time-and-attendance/schedule-presets/schedule-presets.controller.ts index 7da9c20..5fd2cbf 100644 --- a/src/time-and-attendance/schedule-presets/schedule-presets.controller.ts +++ b/src/time-and-attendance/schedule-presets/schedule-presets.controller.ts @@ -50,11 +50,22 @@ export class SchedulePresetsController { @Post('apply-preset') @ModuleAccessAllowed(ModulesEnum.timesheets) async applyPresetToTimesheet( - @Access('email') email: string, @Body('timesheet_id') timesheet_id: number, + @Access('email') email: string, + @Body('timesheet_id') timesheet_id: number, ) { return await this.applyService.applyPresetToTimesheet(email, timesheet_id); } + @Post('apply-preset/:employeeEmail') + @ModuleAccessAllowed(ModulesEnum.timesheets_approval) + async applyPresetToTimesheetFromApproval( + @Access('email') email: string, + @Body('timesheet_id') timesheet_id: number, + @Param('employeeEmail') employee_email: string, + ) { + return await this.applyService.applyPresetToTimesheet(email, timesheet_id, employee_email); + } + @Post('apply-day-preset') @ModuleAccessAllowed(ModulesEnum.timesheets) async applyPresetToDay( @@ -65,4 +76,16 @@ export class SchedulePresetsController { ) { return await this.applyService.applyPresetToDay(email, timesheet_id, week_day_index, date); } + + @Post('apply-day-preset/:employeeEmail') + @ModuleAccessAllowed(ModulesEnum.timesheets_approval) + async applyPresetToDayFromApproval( + @Access('email') email: string, + @Body('timesheet_id') timesheet_id: number, + @Body('week_day_index') week_day_index: number, + @Body('date') date: string, + @Param('employeeEmail') employee_email: string, + ) { + return await this.applyService.applyPresetToDay(email, timesheet_id, week_day_index, date, employee_email); + } } \ No newline at end of file diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts index 97ad01c..5b3a756 100644 --- a/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts @@ -24,8 +24,9 @@ export class SchedulePresetsApplyService { private readonly payPeriodEventService: PayPeriodEventService, ) { } - async applyPresetToTimesheet(email: string, timesheet_id: number): Promise> { - const employee_id = await this.emailResolver.findIdByEmail(email); + async applyPresetToTimesheet(email: string, timesheet_id: number, employee_email?: string): Promise> { + const user_email = employee_email ?? email; + const employee_id = await this.emailResolver.findIdByEmail(user_email); if (!employee_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; const employee_default_schedule_preset = await this.prisma.employees.findFirst({ @@ -76,16 +77,27 @@ export class SchedulePresetsApplyService { created_shifts.push(shift.data); } - const response = await this.shiftService.createOneOrManyShifts(email, created_shifts); - if (response.success) + const response = await this.shiftService.createOneOrManyShifts(user_email, created_shifts); + if (response.success) { + // notify timesheet-approval observers of changes + if (!employee_email) { + this.payPeriodEventService.emit({ + employee_email: user_email, + event_type: 'preset', + action: 'create', + }) + } + return { success: true, data: true }; + } else return { success: false, error: 'There was an error applying presets for this week' }; } - async applyPresetToDay(email: string, timesheet_id: number, week_day_index: number, date: string): Promise> { - const employee_id = await this.emailResolver.findIdByEmail(email); + async applyPresetToDay(email: string, timesheet_id: number, week_day_index: number, date: string, employee_email?: string): Promise> { + const user_email = employee_email ?? email; + const employee_id = await this.emailResolver.findIdByEmail(user_email); if (!employee_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; const week_day = Object.keys(WEEKDAY_MAP)[week_day_index]; @@ -118,16 +130,18 @@ export class SchedulePresetsApplyService { if (!created_shift.success) return { success: false, error: 'SHIFT_CREATE_FAILED' } - + await this.shiftService.createShift(employee_id.data, created_shift.data); } // notify timesheet-approval observers of changes - this.payPeriodEventService.emit({ - employee_email: email, - event_type: 'preset', - action: 'create', - }) + if (!employee_email) { + this.payPeriodEventService.emit({ + employee_email: user_email, + event_type: 'preset', + action: 'create', + }) + } return { success: true, data: true }; } @@ -145,7 +159,7 @@ export class SchedulePresetsApplyService { is_approved: false, is_remote: preset.is_remote!, }; - + return { success: true, data: shift }; } } From 83f5da2e4ac92088a45c60ac40c58f1c3f712527 Mon Sep 17 00:00:00 2001 From: Nic D Date: Tue, 27 Jan 2026 16:33:27 -0500 Subject: [PATCH 6/6] fix(preset): fix issue where notification of creation was being sent for weekly preset application --- .../schedule-presets/schedule-presets.controller.ts | 8 ++++---- .../services/schedule-presets-apply.service.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/time-and-attendance/schedule-presets/schedule-presets.controller.ts b/src/time-and-attendance/schedule-presets/schedule-presets.controller.ts index 5fd2cbf..c699679 100644 --- a/src/time-and-attendance/schedule-presets/schedule-presets.controller.ts +++ b/src/time-and-attendance/schedule-presets/schedule-presets.controller.ts @@ -56,12 +56,12 @@ export class SchedulePresetsController { return await this.applyService.applyPresetToTimesheet(email, timesheet_id); } - @Post('apply-preset/:employeeEmail') + @Post('apply-preset/:email') @ModuleAccessAllowed(ModulesEnum.timesheets_approval) async applyPresetToTimesheetFromApproval( @Access('email') email: string, @Body('timesheet_id') timesheet_id: number, - @Param('employeeEmail') employee_email: string, + @Param('email') employee_email: string, ) { return await this.applyService.applyPresetToTimesheet(email, timesheet_id, employee_email); } @@ -77,14 +77,14 @@ export class SchedulePresetsController { return await this.applyService.applyPresetToDay(email, timesheet_id, week_day_index, date); } - @Post('apply-day-preset/:employeeEmail') + @Post('apply-day-preset/:email') @ModuleAccessAllowed(ModulesEnum.timesheets_approval) async applyPresetToDayFromApproval( @Access('email') email: string, @Body('timesheet_id') timesheet_id: number, @Body('week_day_index') week_day_index: number, @Body('date') date: string, - @Param('employeeEmail') employee_email: string, + @Param('email') employee_email: string, ) { return await this.applyService.applyPresetToDay(email, timesheet_id, week_day_index, date, employee_email); } diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts index 5b3a756..fb88d1f 100644 --- a/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts @@ -77,7 +77,7 @@ export class SchedulePresetsApplyService { created_shifts.push(shift.data); } - const response = await this.shiftService.createOneOrManyShifts(user_email, created_shifts); + const response = await this.shiftService.createOneOrManyShifts(user_email, created_shifts, false); if (response.success) { // notify timesheet-approval observers of changes if (!employee_email) {