From 1e4ec836d31f211d63f704326f30d8efceae400a Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Fri, 8 Aug 2025 14:47:47 -0400 Subject: [PATCH] fix(notify): fixes and setup notify for daily overtime --- src/app.module.ts | 2 + src/common/utils/date-utils.ts | 34 ++++++++++++++ .../constants/notification.constants.ts | 4 ++ .../notifications.controller.ts | 4 +- .../{ => dtos}/notifications.types.ts | 0 .../notifications/notifications.module.ts | 4 +- .../{ => services}/notifications.service.ts | 2 +- src/modules/shifts/services/shifts.service.ts | 44 ++++++++++++++++++- 8 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 src/modules/notifications/constants/notification.constants.ts rename src/modules/notifications/{ => controllers}/notifications.controller.ts (82%) rename src/modules/notifications/{ => dtos}/notifications.types.ts (100%) rename src/modules/notifications/{ => services}/notifications.service.ts (96%) diff --git a/src/app.module.ts b/src/app.module.ts index 04ca6e5..b1d5850 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,7 @@ import { ExpensesModule } from './modules/expenses/expenses.module'; import { HealthModule } from './health/health.module'; import { HealthController } from './health/health.controller'; 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 { OvertimeService } from './modules/business-logics/services/overtime.service'; import { PayperiodsModule } from './modules/pay-periods/pay-periods.module'; @@ -33,6 +34,7 @@ import { UsersModule } from './modules/users-management/users.module'; ExpensesModule, HealthModule, LeaveRequestsModule, + NotificationsModule, OauthSessionsModule, PayperiodsModule, PrismaModule, diff --git a/src/common/utils/date-utils.ts b/src/common/utils/date-utils.ts index e8aeb69..e383f98 100644 --- a/src/common/utils/date-utils.ts +++ b/src/common/utils/date-utils.ts @@ -48,3 +48,37 @@ export function getWeekEnd(startOfWeek: Date): Date { export function getYearStart(date:Date): Date { 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'}); +// } diff --git a/src/modules/notifications/constants/notification.constants.ts b/src/modules/notifications/constants/notification.constants.ts new file mode 100644 index 0000000..49115e7 --- /dev/null +++ b/src/modules/notifications/constants/notification.constants.ts @@ -0,0 +1,4 @@ +export const NOTIF_TYPES = { + SHIFT_OVERTIME_DAILY: 'shift.overtime.daily', + +} as const; \ No newline at end of file diff --git a/src/modules/notifications/notifications.controller.ts b/src/modules/notifications/controllers/notifications.controller.ts similarity index 82% rename from src/modules/notifications/notifications.controller.ts rename to src/modules/notifications/controllers/notifications.controller.ts index 20db53b..e715105 100644 --- a/src/modules/notifications/notifications.controller.ts +++ b/src/modules/notifications/controllers/notifications.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get, Req, Sse, UseGuards, MessageEvent as NestMessageEvent } from "@nestjs/common"; -import { JwtAuthGuard } from "../authentication/guards/jwt-auth.guard"; -import { NotificationsService } from "./notifications.service"; +import { JwtAuthGuard } from "../../authentication/guards/jwt-auth.guard"; +import { NotificationsService } from "../services/notifications.service"; import { Observable } from "rxjs"; import { map } from 'rxjs/operators'; diff --git a/src/modules/notifications/notifications.types.ts b/src/modules/notifications/dtos/notifications.types.ts similarity index 100% rename from src/modules/notifications/notifications.types.ts rename to src/modules/notifications/dtos/notifications.types.ts diff --git a/src/modules/notifications/notifications.module.ts b/src/modules/notifications/notifications.module.ts index cc24a2c..704cdba 100644 --- a/src/modules/notifications/notifications.module.ts +++ b/src/modules/notifications/notifications.module.ts @@ -1,6 +1,6 @@ import { Module } from "@nestjs/common"; -import { NotificationsController } from "./notifications.controller"; -import { NotificationsService } from "./notifications.service"; +import { NotificationsController } from "./controllers/notifications.controller"; +import { NotificationsService } from "./services/notifications.service"; @Module({ providers: [NotificationsService], diff --git a/src/modules/notifications/notifications.service.ts b/src/modules/notifications/services/notifications.service.ts similarity index 96% rename from src/modules/notifications/notifications.service.ts rename to src/modules/notifications/services/notifications.service.ts index 3940f13..ffcc90c 100644 --- a/src/modules/notifications/notifications.service.ts +++ b/src/modules/notifications/services/notifications.service.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from "@nestjs/common"; import { Subject } from "rxjs"; -import { NotificationCard } from "./notifications.types"; +import { NotificationCard } from "../dtos/notifications.types"; @Injectable() export class NotificationsService { diff --git a/src/modules/shifts/services/shifts.service.ts b/src/modules/shifts/services/shifts.service.ts index 6a8ae99..fca99ca 100644 --- a/src/modules/shifts/services/shifts.service.ts +++ b/src/modules/shifts/services/shifts.service.ts @@ -5,19 +5,59 @@ import { Shifts, ShiftsArchive } from "@prisma/client"; import { UpdateShiftsDto } from "../dtos/update-shift.dto"; import { buildPrismaWhere } from "src/common/shared/build-prisma-where"; 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() export class ShiftsService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly notifs: NotificationsService, + ) {} async create(dto: CreateShiftDto): Promise { 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 }, include: { timesheet: { include: { employee: { include: { user: 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 {