fix(notify): fixes and setup notify for daily overtime

This commit is contained in:
Matthieu Haineault 2025-08-08 14:47:47 -04:00
parent dc8c4d048c
commit 1e4ec836d3
8 changed files with 87 additions and 7 deletions

View File

@ -12,6 +12,7 @@ import { ExpensesModule } from './modules/expenses/expenses.module';
import { HealthModule } from './health/health.module'; import { HealthModule } from './health/health.module';
import { HealthController } from './health/health.controller'; import { HealthController } from './health/health.controller';
import { LeaveRequestsModule } from './modules/leave-requests/leave-requests.module'; import { LeaveRequestsModule } from './modules/leave-requests/leave-requests.module';
import { NotificationsModule } from './modules/notifications/notifications.module';
import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.module'; import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.module';
import { OvertimeService } from './modules/business-logics/services/overtime.service'; import { OvertimeService } from './modules/business-logics/services/overtime.service';
import { PayperiodsModule } from './modules/pay-periods/pay-periods.module'; import { PayperiodsModule } from './modules/pay-periods/pay-periods.module';
@ -33,6 +34,7 @@ import { UsersModule } from './modules/users-management/users.module';
ExpensesModule, ExpensesModule,
HealthModule, HealthModule,
LeaveRequestsModule, LeaveRequestsModule,
NotificationsModule,
OauthSessionsModule, OauthSessionsModule,
PayperiodsModule, PayperiodsModule,
PrismaModule, PrismaModule,

View File

@ -48,3 +48,37 @@ export function getWeekEnd(startOfWeek: Date): Date {
export function getYearStart(date:Date): Date { export function getYearStart(date:Date): Date {
return new Date(date.getFullYear(),0,1,0,0,0,0); return new Date(date.getFullYear(),0,1,0,0,0,0);
} }
//cloning methods (helps with notify for overtime in a single day)
// export function toDateOnly(day: Date): Date {
// const d = new Date(day);
// d.setHours(0, 0, 0, 0);
// return d;
// }
// export function composeDateWithTime(day: Date, timeOnly: Date): Date {
// const base = toDateOnly(day);
// base.setHours(timeOnly.getHours(),
// timeOnly.getMinutes(),
// timeOnly.getSeconds(),
// timeOnly.getMilliseconds());
// return base;
// }
export function hoursBetweenSameDay(day: Date, startTime: Date, endTime: Date): number {
const start = new Date(day); start.setHours(startTime.getHours(),
startTime.getMinutes(),
startTime.getSeconds(),
startTime.getMilliseconds());
const end = new Date(day); end.setHours(endTime.getHours(),
endTime.getMinutes(),
endTime.getSeconds(),
endTime.getMilliseconds());
const ms = Math.max(0, end.getTime() - start.getTime());
return ms / 3_600_000; // decimal hours
}
// //utils to print Date into locale date format
// export function fmtDayLocal(day:Date, locale = 'fr-CA'): string {
// return new Date(day).toLocaleDateString(locale, { year: 'numeric', month: '2-digit', day: '2-digit'});
// }

View File

@ -0,0 +1,4 @@
export const NOTIF_TYPES = {
SHIFT_OVERTIME_DAILY: 'shift.overtime.daily',
} as const;

View File

@ -1,7 +1,7 @@
import { Controller, Get, Req, Sse, UseGuards, import { Controller, Get, Req, Sse, UseGuards,
MessageEvent as NestMessageEvent } from "@nestjs/common"; MessageEvent as NestMessageEvent } from "@nestjs/common";
import { JwtAuthGuard } from "../authentication/guards/jwt-auth.guard"; import { JwtAuthGuard } from "../../authentication/guards/jwt-auth.guard";
import { NotificationsService } from "./notifications.service"; import { NotificationsService } from "../services/notifications.service";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';

View File

@ -1,6 +1,6 @@
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { NotificationsController } from "./notifications.controller"; import { NotificationsController } from "./controllers/notifications.controller";
import { NotificationsService } from "./notifications.service"; import { NotificationsService } from "./services/notifications.service";
@Module({ @Module({
providers: [NotificationsService], providers: [NotificationsService],

View File

@ -1,6 +1,6 @@
import { Injectable, Logger } from "@nestjs/common"; import { Injectable, Logger } from "@nestjs/common";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { NotificationCard } from "./notifications.types"; import { NotificationCard } from "../dtos/notifications.types";
@Injectable() @Injectable()
export class NotificationsService { export class NotificationsService {

View File

@ -5,19 +5,59 @@ import { Shifts, ShiftsArchive } from "@prisma/client";
import { UpdateShiftsDto } from "../dtos/update-shift.dto"; import { UpdateShiftsDto } from "../dtos/update-shift.dto";
import { buildPrismaWhere } from "src/common/shared/build-prisma-where"; import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
import { SearchShiftsDto } from "../dtos/search-shifts.dto"; import { SearchShiftsDto } from "../dtos/search-shifts.dto";
import { NotificationsService } from "src/modules/notifications/services/notifications.service";
import { hoursBetweenSameDay } from "src/common/utils/date-utils";
const DAILY_LIMIT_HOURS = Number(process.env.DAILY_LIMIT_HOURS ?? 8);
@Injectable() @Injectable()
export class ShiftsService { export class ShiftsService {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
private readonly notifs: NotificationsService,
) {}
async create(dto: CreateShiftDto): Promise<Shifts> { async create(dto: CreateShiftDto): Promise<Shifts> {
const { timesheet_id, bank_code_id, date, start_time, end_time } = dto; const { timesheet_id, bank_code_id, date, start_time, end_time } = dto;
return this.prisma.shifts.create({
//shift creation
const shift = await this.prisma.shifts.create({
data: { timesheet_id, bank_code_id, date, start_time, end_time }, data: { timesheet_id, bank_code_id, date, start_time, end_time },
include: { timesheet: { include: { employee: { include: { user: true } } } }, include: { timesheet: { include: { employee: { include: { user: true } } } },
bank_code: true, bank_code: true,
}, },
}); });
//fetches all shifts of the same day to check for daily overtime
const sameDayShifts = await this.prisma.shifts.findMany({
where: { timesheet_id, date },
select: { id: true, date: true, start_time: true, end_time: true },
});
//sums hours of the day
const totalHours = sameDayShifts.reduce((sum, s) => {
return sum + hoursBetweenSameDay(s.date, s.start_time, s.end_time);
}, 0 );
//Notify if total hours > 8 for a single day
if(totalHours > DAILY_LIMIT_HOURS ) {
const userId = String(shift.timesheet.employee.user.id);
const dateLabel = new Date(date).toLocaleDateString('fr-CA');
this.notifs.notify(userId, {
type: 'shift.overtime.daily',
severity: 'warn',
message: `Tu viens de dépasser ${DAILY_LIMIT_HOURS.toFixed(2)}h pour la journée du ${dateLabel} (total: ${totalHours.toFixed(2)}h).`,
ts: new Date().toISOString(),
meta: {
timesheet_id,
date: new Date(date).toISOString(),
totalHours,
threshold: DAILY_LIMIT_HOURS,
lastShiftId: shift.id
},
});
}
return shift;
} }
async findAll(filters: SearchShiftsDto): Promise <Shifts[]> { async findAll(filters: SearchShiftsDto): Promise <Shifts[]> {