feat(pay-period): add SSE for timesheet-approval when employees update timesheet and a user is on timesheet-approval

This commit is contained in:
Nic D 2026-01-21 10:21:58 -05:00
parent 646cb58e98
commit 68883b84e7
9 changed files with 79 additions and 7 deletions

View File

@ -35,8 +35,11 @@ export class ExpenseController {
@Delete('delete/:expense_id') @Delete('delete/:expense_id')
@ModuleAccessAllowed(ModulesEnum.timesheets) @ModuleAccessAllowed(ModulesEnum.timesheets)
remove(@Param('expense_id') expense_id: number): Promise<Result<number, string>> { remove(
return this.deleteService.deleteExpense(expense_id); @Param('expense_id') expense_id: number,
@Access('email') email: string,
): Promise<Result<number, string>> {
return this.deleteService.deleteExpense(expense_id, email);
} }
} }

View File

@ -6,6 +6,7 @@ import { toStringFromDate, weekStartSunday } from "src/common/utils/date-utils";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto"; import { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto";
import { normalizeAndParseExpenseDto } from "src/time-and-attendance/expenses/expense.utils"; 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"; import { expense_select } from "src/time-and-attendance/utils/selects.utils";
@Injectable() @Injectable()
@ -14,6 +15,7 @@ export class ExpenseCreateService {
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly emailResolver: EmailToIdResolver, private readonly emailResolver: EmailToIdResolver,
private readonly typeResolver: BankCodesResolver, private readonly typeResolver: BankCodesResolver,
private readonly payPeriodEventService: PayPeriodEventService,
) { } ) { }
//_________________________________________________________________ //_________________________________________________________________
@ -64,8 +66,15 @@ export class ExpenseCreateService {
attachment: expense.attachment ?? undefined, attachment: expense.attachment ?? undefined,
supervisor_comment: expense.supervisor_comment ?? 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) { } catch (error) {
return { success: false, error: 'INVALID_EXPENSE' }; return { success: false, error: 'INVALID_EXPENSE' };
} }

View File

@ -1,15 +1,40 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { Result } from "src/common/errors/result-error.factory"; 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 { PrismaService } from "src/prisma/prisma.service";
import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service";
@Injectable() @Injectable()
export class ExpenseDeleteService { export class ExpenseDeleteService {
constructor(private readonly prisma: PrismaService) { } constructor(
private readonly prisma: PrismaService,
private readonly payPeriodEventService: PayPeriodEventService,
private readonly emailResolver: EmailToIdResolver,
){}
//_________________________________________________________________ //_________________________________________________________________
// DELETE // DELETE
//_________________________________________________________________ //_________________________________________________________________
async deleteExpense(expense_id: number): Promise<Result<number, string>> { async deleteExpense(expense_id: number, email: string): Promise<Result<number, string>> {
// 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 { try {
await this.prisma.$transaction(async (tx) => { await this.prisma.$transaction(async (tx) => {
const expense = await tx.expenses.findUnique({ const expense = await tx.expenses.findUnique({
@ -21,6 +46,14 @@ export class ExpenseDeleteService {
await tx.expenses.delete({ where: { id: expense.id } }); await tx.expenses.delete({ where: { id: expense.id } });
return { success: true, data: 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 }; return { success: true, data: expense_id };
} catch (error) { } catch (error) {
return { success: false, error: `EXPENSE_NOT_FOUND` }; return { success: false, error: `EXPENSE_NOT_FOUND` };

View File

@ -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 { ExpenseDto } from "src/time-and-attendance/expenses/expense-create.dto";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { normalizeAndParseExpenseDto } from "src/time-and-attendance/expenses/expense.utils"; 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() @Injectable()
export class ExpenseUpdateService { export class ExpenseUpdateService {
@ -15,6 +16,7 @@ export class ExpenseUpdateService {
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly emailResolver: EmailToIdResolver, private readonly emailResolver: EmailToIdResolver,
private readonly typeResolver: BankCodesResolver, private readonly typeResolver: BankCodesResolver,
private readonly payPeriodEventService: PayPeriodEventService,
) { } ) { }
//_________________________________________________________________ //_________________________________________________________________
// UPDATE // UPDATE
@ -66,6 +68,17 @@ export class ExpenseUpdateService {
attachment: expense.attachment ?? undefined, attachment: expense.attachment ?? undefined,
supervisor_comment: expense.supervisor_comment ?? 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 }; return { success: true, data: updated };
} catch (error) { } catch (error) {
return { success: false, error: 'EXPENSE_NOT_FOUND' }; return { success: false, error: 'EXPENSE_NOT_FOUND' };

View File

@ -1,5 +1,5 @@
export class PayPeriodEvent { export class PayPeriodEvent {
employee_email: string; employee_email: string;
event_type: 'expense' | 'shift'; event_type: 'expense' | 'shift' | 'preset';
action: 'create' | 'update' | 'delete'; action: 'create' | 'update' | 'delete';
} }

View File

@ -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 { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.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 { 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, VacationService,
SickLeaveService, SickLeaveService,
BankedHoursService, BankedHoursService,
PayPeriodEventService,
], ],
exports: [ exports: [
SchedulePresetsGetService, SchedulePresetsGetService,

View File

@ -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 { ShiftDto } from "src/time-and-attendance/shifts/shift.dto";
import { WEEKDAY_MAP } from "src/time-and-attendance/schedule-presets/schedule-presets.dto"; import { WEEKDAY_MAP } from "src/time-and-attendance/schedule-presets/schedule-presets.dto";
import { $Enums, Prisma, SchedulePresetShifts } from "@prisma/client"; import { $Enums, Prisma, SchedulePresetShifts } from "@prisma/client";
import { PayPeriodEventService } from "src/time-and-attendance/pay-period/services/pay-period-event.service";
@Injectable() @Injectable()
@ -20,6 +21,7 @@ export class SchedulePresetsApplyService {
private readonly emailResolver: EmailToIdResolver, private readonly emailResolver: EmailToIdResolver,
private readonly shiftService: ShiftsCreateService, private readonly shiftService: ShiftsCreateService,
private readonly typeResolver: BankCodesResolver, private readonly typeResolver: BankCodesResolver,
private readonly payPeriodEventService: PayPeriodEventService,
) { } ) { }
async applyPresetToTimesheet(email: string, timesheet_id: number): Promise<Result<boolean, string>> { async applyPresetToTimesheet(email: string, timesheet_id: number): Promise<Result<boolean, string>> {
@ -119,6 +121,14 @@ export class SchedulePresetsApplyService {
await this.shiftService.createShift(employee_id.data, created_shift.data); 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 }; return { success: true, data: true };
} }

View File

@ -61,7 +61,7 @@ export class ShiftsUpdateService {
this.payPeriodEventService.emit({ this.payPeriodEventService.emit({
employee_email: email, employee_email: email,
event_type: 'shift', event_type: 'shift',
action: 'create' action: 'update'
}) })
} }

View File

@ -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 { 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 { 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 { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service";
import { PayPeriodEventService } from "./pay-period/services/pay-period-event.service";
@Module({ @Module({
@ -90,6 +91,7 @@ import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-pr
VacationService, VacationService,
BankedHoursService, BankedHoursService,
PaidTimeOFfBankHoursService, PaidTimeOFfBankHoursService,
PayPeriodEventService,
], ],
exports: [TimesheetApprovalService], exports: [TimesheetApprovalService],
}) export class TimeAndAttendanceModule { }; }) export class TimeAndAttendanceModule { };