From f765a99273452ba0980cd04963ac9857211c8eef Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 19 Aug 2025 14:49:47 -0400 Subject: [PATCH] refactor(pay-period): ajusted logics services and controller of model pay-periods --- docs/swagger/swagger-spec.json | 45 ++-- .../migration.sql | 66 +++--- prisma/schema.prisma | 13 +- .../services/holiday.service.ts | 2 + .../services/overtime.service.ts | 1 + .../services/sick-leave.service.ts | 1 + .../services/vacation.service.ts | 1 + .../controllers/csv-exports.controller.ts | 2 +- .../exports/services/csv-exports.service.ts | 128 +++++------ .../controllers/pay-periods.controller.ts | 25 +-- .../dtos/overview-pay-period.dto.ts | 16 +- .../pay-periods/dtos/pay-period.dto.ts | 12 +- .../pay-periods/mappers/pay-periods.mapper.ts | 19 +- .../services/pay-periods-command.service.ts | 22 +- .../services/pay-periods-query.service.ts | 199 +++++++++++------- .../pay-periods/utils/pay-year.util.ts | 22 +- .../shifts/services/shifts-query.service.ts | 8 +- 17 files changed, 325 insertions(+), 257 deletions(-) diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 72371bc..f9c2530 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -3035,22 +3035,27 @@ "PayPeriodDto": { "type": "object", "properties": { - "period_number": { + "pay_period_no": { "type": "number", "example": 1, "description": "numéro cyclique de la période entre 1 et 26" }, - "start_date": { + "period_start": { "type": "string", "example": "2023-12-17", "format": "date" }, - "end_date": { + "period_end": { "type": "string", "example": "2023-12-30", "format": "date" }, - "year": { + "payday": { + "type": "string", + "example": "2023-01-04", + "format": "date" + }, + "pay_year": { "type": "number", "example": 2023 }, @@ -3060,10 +3065,11 @@ } }, "required": [ - "period_number", - "start_date", - "end_date", - "year", + "pay_period_no", + "period_start", + "period_end", + "payday", + "pay_year", "label" ] }, @@ -3155,28 +3161,34 @@ "PayPeriodOverviewDto": { "type": "object", "properties": { - "period_number": { + "pay_period_no": { "type": "number", "example": 1, "description": "Period number (1–26)" }, - "year": { + "pay_year": { "type": "number", "example": 2023, "description": "Calendar year of the period" }, - "start_date": { + "period_start": { "type": "string", "example": "2023-12-17", "format": "date", "description": "Period start date (YYYY-MM-DD)" }, - "end_date": { + "period_end": { "type": "string", "example": "2023-12-30", "format": "date", "description": "Period end date (YYYY-MM-DD)" }, + "payday": { + "type": "string", + "example": "2023-12-30", + "format": "date", + "description": "Period pay day(YYYY-MM-DD)" + }, "label": { "type": "string", "example": "2023-12-17 → 2023-12-30", @@ -3191,10 +3203,11 @@ } }, "required": [ - "period_number", - "year", - "start_date", - "end_date", + "pay_period_no", + "pay_year", + "period_start", + "period_end", + "payday", "label", "employees_overview" ] diff --git a/prisma/migrations/20250724191659_create_pay_period_view/migration.sql b/prisma/migrations/20250724191659_create_pay_period_view/migration.sql index 2319d74..2fe5fd1 100644 --- a/prisma/migrations/20250724191659_create_pay_period_view/migration.sql +++ b/prisma/migrations/20250724191659_create_pay_period_view/migration.sql @@ -5,39 +5,41 @@ CREATE OR REPLACE VIEW pay_period AS WITH anchor AS ( - SELECT '2023-12-17'::date AS anchor_date - ), - current_pay_period AS( - SELECT - ((now()::date - anchor_date) % 14) +1 AS current_day_in_pay_period - FROM anchor - ), - bounds AS ( - SELECT - (now()::date - - INTERVAL '6 months' - - (current_day_in_pay_period || ' days')::INTERVAL - )::date AS start_bound, - (now()::date + INTERVAL '1 month' - - (current_day_in_pay_period || ' days')::INTERVAL - )::date AS end_bound, - anchor.anchor_date - FROM anchor - CROSS JOIN current_pay_period + SELECT '2023-12-17'::date AS anchor_sunday ), series AS ( SELECT - generate_series(bounds.start_bound, bounds.end_bound, '14 days') AS period_start, - bounds.anchor_date - FROM bounds + gs::date AS period_start, -- Dimanche + (gs + INTERVAL '13 days')::date AS period_end, -- Samedi + (gs + INTERVAL '18 days')::date AS payday -- Jeudi suivant pour viser l'année fiscale + FROM generate_series( + (SELECT anchor_sunday FROM anchor), + (CURRENT_DATE + INTERVAL '1 month')::date, + INTERVAL '14 days' + ) AS gs + ), + numbered AS ( + SELECT + period_start, + period_end, + payday, + EXTRACT(YEAR FROM payday)::int AS pay_year, + ROW_NUMBER() OVER ( + PARTITION BY EXTRACT(YEAR FROM payday) + ORDER BY payday + ) AS pay_period_no + FROM series ) -SELECT - ((row_number() OVER (ORDER BY period_start) - 1) % 26) + 1 AS period_number, - period_start AS start_date, - period_start + INTERVAL '13 days' AS end_date, - EXTRACT(YEAR FROM period_start)::int AS year, - period_start || ' -> ' || - to_char(period_start + INTERVAL '13 days', 'YYYY-MM-DD') - AS label -FROM series -ORDER BY period_start; + SELECT + pay_year, + pay_period_no, + period_start, + period_end, + payday, + to_char(period_start, 'YYYY-MM-DD') || '->' || + to_char(period_end, 'YYYY-MM-DD') AS label + FROM numbered + + WHERE payday BETWEEN (CURRENT_DATE - INTERVAL '6 months')::date + AND (CURRENT_DATE + INTERVAL '1 month')::date + ORDER BY period_start; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 58524d1..53c624b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -135,12 +135,13 @@ model LeaveRequestsArchive { //pay-period vue view PayPeriods { - period_number Int - start_date DateTime @db.Date - end_date DateTime @db.Date - year Int - label String - + pay_year Int + pay_period_no Int + payday DateTime @db.Date + period_start DateTime @db.Date + period_end DateTime @db.Date + label String + @@map("pay_period") } diff --git a/src/modules/business-logics/services/holiday.service.ts b/src/modules/business-logics/services/holiday.service.ts index 2d377b6..0e06f9a 100644 --- a/src/modules/business-logics/services/holiday.service.ts +++ b/src/modules/business-logics/services/holiday.service.ts @@ -8,6 +8,7 @@ export class HolidayService { constructor(private readonly prisma: PrismaService) {} + //switch employeeId for email private async computeHoursPrevious4Weeks(employeeId: number, holidayDate: Date): Promise { //sets the end of the window to 1ms before the week with the holiday const holidayWeekStart = getWeekStart(holidayDate); @@ -31,6 +32,7 @@ export class HolidayService { return dailyHours; } + //switch employeeId for email async calculateHolidayPay( employeeId: number, holidayDate: Date, modifier: number): Promise { const hours = await this.computeHoursPrevious4Weeks(employeeId, holidayDate); const dailyRate = Math.min(hours, 8); diff --git a/src/modules/business-logics/services/overtime.service.ts b/src/modules/business-logics/services/overtime.service.ts index b58c511..1bacabc 100644 --- a/src/modules/business-logics/services/overtime.service.ts +++ b/src/modules/business-logics/services/overtime.service.ts @@ -20,6 +20,7 @@ export class OvertimeService { } //calculate Weekly overtime + //switch employeeId for email async getWeeklyOvertimeHours(employeeId: number, refDate: Date): Promise { const weekStart = getWeekStart(refDate); const weekEnd = getWeekEnd(weekStart); diff --git a/src/modules/business-logics/services/sick-leave.service.ts b/src/modules/business-logics/services/sick-leave.service.ts index 1c8d9ee..3bf2fef 100644 --- a/src/modules/business-logics/services/sick-leave.service.ts +++ b/src/modules/business-logics/services/sick-leave.service.ts @@ -8,6 +8,7 @@ export class SickLeaveService { private readonly logger = new Logger(SickLeaveService.name); + //switch employeeId for email async calculateSickLeavePay(employeeId: number, referenceDate: Date, daysRequested: number, modifier: number): Promise { //sets the year to jan 1st to dec 31st const periodStart = getYearStart(referenceDate); diff --git a/src/modules/business-logics/services/vacation.service.ts b/src/modules/business-logics/services/vacation.service.ts index 759a0d6..1eb5498 100644 --- a/src/modules/business-logics/services/vacation.service.ts +++ b/src/modules/business-logics/services/vacation.service.ts @@ -15,6 +15,7 @@ export class VacationService { * @param modifier Coefficient of hours(1) * @returns amount of payable hours */ + //switch employeeId for email async calculateVacationPay(employeeId: number, startDate: Date, daysRequested: number, modifier: number): Promise { //fetch hiring date const employee = await this.prisma.employees.findUnique({ diff --git a/src/modules/exports/controllers/csv-exports.controller.ts b/src/modules/exports/controllers/csv-exports.controller.ts index f4948e4..f59e84c 100644 --- a/src/modules/exports/controllers/csv-exports.controller.ts +++ b/src/modules/exports/controllers/csv-exports.controller.ts @@ -29,7 +29,7 @@ export class CsvExportController { //filters by type const filtered = all.filter(r => { - switch (r.bankCode.toLocaleLowerCase()) { + switch (r.bank_code.toLocaleLowerCase()) { case 'holiday' : return types.includes(ExportType.HOLIDAY); case 'vacation' : return types.includes(ExportType.VACATION); case 'sick-leave': return types.includes(ExportType.SICK_LEAVE); diff --git a/src/modules/exports/services/csv-exports.service.ts b/src/modules/exports/services/csv-exports.service.ts index 77c5d70..5399d85 100644 --- a/src/modules/exports/services/csv-exports.service.ts +++ b/src/modules/exports/services/csv-exports.service.ts @@ -3,33 +3,33 @@ import { ExportCompany } from "../dtos/export-csv-options.dto"; import { Injectable, NotFoundException } from "@nestjs/common"; export interface CsvRow { - companyCode: number; - externalPayrollId: number; - fullName: string; - bankCode: string; - quantityHours?: number; + company_code: number; + external_payroll_id: number; + full_name: string; + bank_code: string; + quantity_hours?: number; amount?: number; - weekNumber: number; - payDate: string; - holidayDate?: string; + week_number: number; + pay_date: string; + holiday_date?: string; } @Injectable() export class CsvExportService { constructor(private readonly prisma: PrismaService) {} - async collectTransaction(periodId: number, companies: ExportCompany[]): Promise { + async collectTransaction(period_id: number, companies: ExportCompany[]): Promise { const companyCodes = companies.map(c => c === ExportCompany.TARGO ? 1 : 2); const period = await this.prisma.payPeriods.findFirst({ - where: { period_number: periodId }, + where: { pay_period_no: period_id }, }); if(!period) { - throw new NotFoundException(`Pay period ${periodId} not found`); + throw new NotFoundException(`Pay period ${period_id} not found`); } - const startDate = period.start_date; - const endDate = period.end_date; + const startDate = period.period_start; + const endDate = period.period_end; //fetching shifts const shifts = await this.prisma.shifts.findMany({ @@ -72,39 +72,39 @@ export class CsvExportService { const rows: CsvRow[] = []; //Shifts Mapping - for (const s of shifts) { - const emp = s.timesheet.employee; - const weekNumber = this.computeWeekNumber(startDate, s.date); - const hours = this.computeHours(s.start_time, s.end_time); + for (const shift of shifts) { + const emp = shift.timesheet.employee; + const week_number = this.computeWeekNumber(startDate, shift.date); + const hours = this.computeHours(shift.start_time, shift.end_time); rows.push({ - companyCode: emp.company_code, - externalPayrollId: emp.external_payroll_id, - fullName: `${emp.user.first_name} ${emp.user.last_name}`, - bankCode: s.bank_code.bank_code, - quantityHours: hours, + company_code: emp.company_code, + external_payroll_id: emp.external_payroll_id, + full_name: `${emp.user.first_name} ${emp.user.last_name}`, + bank_code: shift.bank_code.bank_code, + quantity_hours: hours, amount: undefined, - weekNumber, - payDate: this.formatDate(endDate), - holidayDate: undefined, + week_number, + pay_date: this.formatDate(endDate), + holiday_date: undefined, }); } //Expenses Mapping for (const e of expenses) { const emp = e.timesheet.employee; - const weekNumber = this.computeWeekNumber(startDate, e.date); + const week_number = this.computeWeekNumber(startDate, e.date); rows.push({ - companyCode: emp.company_code, - externalPayrollId: emp.external_payroll_id, - fullName: `${emp.user.first_name} ${emp.user.last_name}`, - bankCode: e.bank_code.bank_code, - quantityHours: undefined, + company_code: emp.company_code, + external_payroll_id: emp.external_payroll_id, + full_name: `${emp.user.first_name} ${emp.user.last_name}`, + bank_code: e.bank_code.bank_code, + quantity_hours: undefined, amount: Number(e.amount), - weekNumber, - payDate: this.formatDate(endDate), - holidayDate: undefined, + week_number, + pay_date: this.formatDate(endDate), + holiday_date: undefined, }); } @@ -115,56 +115,56 @@ export class CsvExportService { const start = l.start_date_time; const end = l.end_date_time ?? start; - const weekNumber = this.computeWeekNumber(startDate, start); + const week_number = this.computeWeekNumber(startDate, start); const hours = this.computeHours(start, end); rows.push({ - companyCode: emp.company_code, - externalPayrollId: emp.external_payroll_id, - fullName: `${emp.user.first_name} ${emp.user.last_name}`, - bankCode: l.bank_code.bank_code, - quantityHours: hours, + company_code: emp.company_code, + external_payroll_id: emp.external_payroll_id, + full_name: `${emp.user.first_name} ${emp.user.last_name}`, + bank_code: l.bank_code.bank_code, + quantity_hours: hours, amount: undefined, - weekNumber, - payDate: this.formatDate(endDate), - holidayDate: undefined, + week_number, + pay_date: this.formatDate(endDate), + holiday_date: undefined, }); } //Final Mapping and sorts return rows.sort((a,b) => { - if(a.externalPayrollId !== b.externalPayrollId) { - return a.externalPayrollId - b.externalPayrollId; + if(a.external_payroll_id !== b.external_payroll_id) { + return a.external_payroll_id - b.external_payroll_id; } - if(a.bankCode !== b.bankCode) { - return a.bankCode.localeCompare(b.bankCode); + if(a.bank_code !== b.bank_code) { + return a.bank_code.localeCompare(b.bank_code); } - return a.weekNumber - b.weekNumber; + return a.week_number - b.week_number; }); } generateCsv(rows: CsvRow[]): Buffer { const header = [ - 'companyCode', - 'externalPayrolId', - 'fullName', - 'bankCode', - 'quantityHours', + 'company_code', + 'external_payrol_id', + 'full_name', + 'bank_code', + 'quantity_hours', 'amount', - 'weekNumber', - 'payDate', - 'holidayDate', + 'week_number', + 'pay_date', + 'holiday_date', ].join(',') + '\n'; const body = rows.map(r => [ - r.companyCode, - r.externalPayrollId, - `${r.fullName.replace(/"/g, '""')}"`, - r.bankCode, - r.quantityHours?.toFixed(2) ?? '', - r.weekNumber, - r.payDate, - r.holidayDate ?? '', + r.company_code, + r.external_payroll_id, + `${r.full_name.replace(/"/g, '""')}"`, + r.bank_code, + r.quantity_hours?.toFixed(2) ?? '', + r.week_number, + r.pay_date, + r.holiday_date ?? '', ].join(',')).join('\n'); return Buffer.from('\uFEFF' + header + body, 'utf8'); diff --git a/src/modules/pay-periods/controllers/pay-periods.controller.ts b/src/modules/pay-periods/controllers/pay-periods.controller.ts index ca30c5e..b90b3a9 100644 --- a/src/modules/pay-periods/controllers/pay-periods.controller.ts +++ b/src/modules/pay-periods/controllers/pay-periods.controller.ts @@ -1,12 +1,10 @@ -import { Controller, ForbiddenException, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query } from "@nestjs/common"; +import { Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query } from "@nestjs/common"; import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger"; import { PayPeriodDto } from "../dtos/pay-period.dto"; import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto"; import { PayPeriodsQueryService } from "../services/pay-periods-query.service"; import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { Roles as RoleEnum } from '.prisma/client'; -import { Req } from '@nestjs/common'; -import { Request } from 'express'; import { PayPeriodsCommandService } from "../services/pay-periods-command.service"; import { PayPeriodBundleDto } from "../dtos/bundle-pay-period.dto"; @@ -54,9 +52,9 @@ export class PayPeriodsController { @ApiNotFoundResponse({ description: "Pay period not found" }) async findOneByYear( @Param("year", ParseIntPipe) year: number, - @Param("periodNumber", ParseIntPipe) periodNumber: number, + @Param("periodNumber", ParseIntPipe) period_no: number, ) { - return this.queryService.findOneByYearPeriod(year, periodNumber); + return this.queryService.findOneByYearPeriod(year, period_no); } @Patch("approval/:year/:periodNumber") @@ -67,10 +65,10 @@ export class PayPeriodsController { @ApiResponse({ status: 200, description: "Pay period approved" }) async approve( @Param("year", ParseIntPipe) year: number, - @Param("periodNumber", ParseIntPipe) periodNumber: number, + @Param("periodNumber", ParseIntPipe) period_no: number, ) { - await this.commandService.approvalPayPeriod(year, periodNumber); - return { message: `Pay-period ${year}-${periodNumber} approved` }; + await this.commandService.approvalPayPeriod(year, period_no); + return { message: `Pay-period ${year}-${period_no} approved` }; } @Get(':year/:periodNumber/:email') @@ -83,12 +81,11 @@ export class PayPeriodsController { @ApiNotFoundResponse({ description: 'Pay period not found' }) async getCrewOverview( @Param('year', ParseIntPipe) year: number, - @Param('periodNumber', ParseIntPipe) periodNumber: number, + @Param('periodNumber', ParseIntPipe) period_no: number, @Param('email') email: string, - @Query('includeSubtree', new ParseBoolPipe({ optional: true })) includeSubtree = false, - @Req() req: Request, + @Query('includeSubtree', new ParseBoolPipe({ optional: true })) include_subtree = false, ): Promise { - return this.queryService.getCrewOverview(year, periodNumber, email, includeSubtree); + return this.queryService.getCrewOverview(year, period_no, email, include_subtree); } @Get('overview/:year/:periodNumber') @@ -99,8 +96,8 @@ export class PayPeriodsController { @ApiNotFoundResponse({ description: 'Pay period not found' }) async getOverviewByYear( @Param('year', ParseIntPipe) year: number, - @Param('periodNumber', ParseIntPipe) periodNumber: number, + @Param('periodNumber', ParseIntPipe) period_no: number, ): Promise { - return this.queryService.getOverviewByYearPeriod(year, periodNumber); + return this.queryService.getOverviewByYearPeriod(year, period_no); } } diff --git a/src/modules/pay-periods/dtos/overview-pay-period.dto.ts b/src/modules/pay-periods/dtos/overview-pay-period.dto.ts index 81399f0..041fba3 100644 --- a/src/modules/pay-periods/dtos/overview-pay-period.dto.ts +++ b/src/modules/pay-periods/dtos/overview-pay-period.dto.ts @@ -3,10 +3,10 @@ import { EmployeePeriodOverviewDto } from './overview-employee-period.dto'; export class PayPeriodOverviewDto { @ApiProperty({ example: 1, description: 'Period number (1–26)' }) - period_number: number; + pay_period_no: number; @ApiProperty({ example: 2023, description: 'Calendar year of the period' }) - year: number; + pay_year: number; @ApiProperty({ example: '2023-12-17', @@ -14,7 +14,7 @@ export class PayPeriodOverviewDto { format: 'date', description: "Period start date (YYYY-MM-DD)", }) - start_date: string; + period_start: string; @ApiProperty({ example: '2023-12-30', @@ -22,7 +22,15 @@ export class PayPeriodOverviewDto { format: 'date', description: "Period end date (YYYY-MM-DD)", }) - end_date: string; + period_end: string; + + @ApiProperty({ + example: '2023-12-30', + type: String, + format: 'date', + description: "Period pay day(YYYY-MM-DD)", + }) + payday: string; @ApiProperty({ example: '2023-12-17 → 2023-12-30', diff --git a/src/modules/pay-periods/dtos/pay-period.dto.ts b/src/modules/pay-periods/dtos/pay-period.dto.ts index a7e31ba..4f7989b 100644 --- a/src/modules/pay-periods/dtos/pay-period.dto.ts +++ b/src/modules/pay-periods/dtos/pay-period.dto.ts @@ -3,18 +3,22 @@ import { ApiProperty } from "@nestjs/swagger"; export class PayPeriodDto { @ApiProperty({ example: 1, description: 'numéro cyclique de la période entre 1 et 26' }) - period_number: number; + pay_period_no: number; @ApiProperty({ example: '2023-12-17', type: String, format: 'date' }) - start_date: String; + period_start: string; @ApiProperty({ example: '2023-12-30', type: String, format: 'date' }) - end_date: String; + period_end: string; + + @ApiProperty({ example: '2023-01-04', + type: String, format: 'date' }) + payday: string; @ApiProperty({ example: 2023 }) - year: number; + pay_year: number; @ApiProperty({ example: '2023-12-17 → 2023-12-30' }) label: string; diff --git a/src/modules/pay-periods/mappers/pay-periods.mapper.ts b/src/modules/pay-periods/mappers/pay-periods.mapper.ts index 0ce3d12..55c78ca 100644 --- a/src/modules/pay-periods/mappers/pay-periods.mapper.ts +++ b/src/modules/pay-periods/mappers/pay-periods.mapper.ts @@ -1,18 +1,19 @@ import { PayPeriods } from "@prisma/client"; import { PayPeriodDto } from "../dtos/pay-period.dto"; -import { payYearOfDate } from "../utils/pay-year.util"; -const toDateString = (d: Date) => d.toISOString().slice(0, 10); // "YYYY-MM-DD" +const toDateString = (date: Date) => date.toISOString().slice(0, 10); // "YYYY-MM-DD" export function mapPayPeriodToDto(row: PayPeriods): PayPeriodDto { - const s = toDateString(row.start_date); - const e = toDateString(row.end_date); + const start = toDateString(row.period_start); + const end = toDateString(row.period_end); + const pay = toDateString(row.payday); return { - period_number: row.period_number, - start_date: toDateString(row.start_date), - end_date: toDateString(row.end_date), - year: payYearOfDate(s), - label: `${s} => ${e}`, + pay_period_no: row.pay_period_no, + period_start: toDateString(row.period_start), + period_end: toDateString(row.period_end), + payday:pay, + pay_year: new Date(pay).getFullYear(), + label: `${start} => ${end}`, }; } diff --git a/src/modules/pay-periods/services/pay-periods-command.service.ts b/src/modules/pay-periods/services/pay-periods-command.service.ts index f24b768..d8609ed 100644 --- a/src/modules/pay-periods/services/pay-periods-command.service.ts +++ b/src/modules/pay-periods/services/pay-periods-command.service.ts @@ -6,26 +6,26 @@ import { PrismaService } from "src/prisma/prisma.service"; export class PayPeriodsCommandService { constructor( private readonly prisma: PrismaService, - private readonly timesheetsApproval: TimesheetsCommandService, + private readonly timesheets_approval: TimesheetsCommandService, ) {} - async approvalPayPeriod(year: number , periodNumber: number): Promise { + async approvalPayPeriod(pay_year: number , period_no: number): Promise { const period = await this.prisma.payPeriods.findFirst({ - where: { year, period_number: periodNumber}, + where: { pay_year, pay_period_no: period_no}, }); - if (!period) throw new NotFoundException(`PayPeriod #${year}-${periodNumber} not found`); + if (!period) throw new NotFoundException(`PayPeriod #${pay_year}-${period_no} not found`); //fetches timesheet of selected period if the timesheet has atleast 1 shift or 1 expense - const timesheetList = await this.prisma.timesheets.findMany({ + const timesheet_ist = await this.prisma.timesheets.findMany({ where: { OR: [ - { shift: {some: { date: { gte: period.start_date, - lte: period.end_date, + { shift: {some: { date: { gte: period.period_start, + lte: period.period_end, }, }}, }, - { expense: { some: { date: { gte: period.start_date, - lte: period.end_date, + { expense: { some: { date: { gte: period.period_start, + lte: period.period_end, }, }}, }, @@ -36,8 +36,8 @@ export class PayPeriodsCommandService { //approval of both timesheet (cascading to the approval of related shifts and expenses) await this.prisma.$transaction(async (transaction)=> { - for(const {id} of timesheetList) { - await this.timesheetsApproval.updateApprovalWithTx(transaction,id, true); + for(const {id} of timesheet_ist) { + await this.timesheets_approval.updateApprovalWithTx(transaction,id, true); } }) } diff --git a/src/modules/pay-periods/services/pay-periods-query.service.ts b/src/modules/pay-periods/services/pay-periods-query.service.ts index b1bdf8a..b733a7d 100644 --- a/src/modules/pay-periods/services/pay-periods-query.service.ts +++ b/src/modules/pay-periods/services/pay-periods-query.service.ts @@ -9,53 +9,65 @@ import { mapPayPeriodToDto } from "../mappers/pay-periods.mapper"; @Injectable() export class PayPeriodsQueryService { - constructor( - private readonly prisma: PrismaService, - ) {} + constructor( private readonly prisma: PrismaService) {} - async getOverview(periodNumber: number): Promise { - const period = await this.prisma.payPeriods.findFirst({ - where: { period_number: periodNumber }, - orderBy: { year: "desc" }, - }); - if (!period) throw new NotFoundException(`Period #${periodNumber} not found`); - return this.buildOverview(period); - } - - async getOverviewByYearPeriod(year: number, periodNumber: number): Promise { - const period = computePeriod(year, periodNumber); + async getOverviewByYearPeriod(pay_year: number, period_no: number): Promise { + const period = computePeriod(pay_year, period_no); return this.buildOverview({ - start_date: period.start_date, - end_date : period.end_date, - period_number: period.period_number, - year: period.year, - label:period.label, + period_start: period.period_start, + period_end : period.period_end, + period_no : period.period_no, + pay_year : period.pay_year, + payday : period.payday, + label :period.label, } as any); } + async getOverview(pay_period_no: number): Promise { + const period = await this.prisma.payPeriods.findFirst({ + where: { pay_period_no }, + orderBy: { pay_year: "desc" }, + }); + if (!period) throw new NotFoundException(`Period #${pay_period_no} not found`); + + return this.buildOverview({ + period_start: period.period_start, + period_end : period.period_end, + payday : period.payday, + period_no : period.pay_period_no, + pay_year : period.pay_year, + label : period.label, + }); + } + private async buildOverview( - period: { start_date: string | Date; end_date: string | Date; period_number: number; year: number; label: string; }, - options?: { filteredEmployeeIds?: number[]; seedNames?: Map }, + period: { period_start: string | Date; period_end: string | Date; payday: string | Date; + period_no: number; pay_year: number; label: string; }, + options?: { filtered_employee_ids?: number[]; seed_names?: Map }, ): Promise { const toDateString = (d: Date) => d.toISOString().slice(0, 10); const toMoney = (v: any) => (typeof v === "object" && "toNumber" in v ? v.toNumber() : (v as number)); - const start = period.start_date instanceof Date - ? period.start_date - : new Date(`${period.start_date}T00:00:00.000Z`); + const start = period.period_start instanceof Date + ? period.period_start + : new Date(`${period.period_start}T00:00:00.000Z`); - const end = period.end_date instanceof Date - ? period.end_date - : new Date(`${period.end_date}T00:00:00.000Z`); + const end = period.period_end instanceof Date + ? period.period_end + : new Date(`${period.period_end}T00:00:00.000Z`); + + const payd = period.payday instanceof Date + ? period.payday + : new Date (`${period.payday}T00:00:00.000Z`); //restrictEmployeeIds = filter for shifts and expenses by employees - const whereEmployee = options?.filteredEmployeeIds?.length ? { employee_id: { in: options.filteredEmployeeIds } }: {}; + const where_employee = options?.filtered_employee_ids?.length ? { employee_id: { in: options.filtered_employee_ids } }: {}; // SHIFTS (filtered by crew) const shifts = await this.prisma.shifts.findMany({ where: { date: { gte: start, lte: end }, - timesheet: whereEmployee, + timesheet: where_employee, }, select: { start_time: true, @@ -79,7 +91,7 @@ export class PayPeriodsQueryService { const expenses = await this.prisma.expenses.findMany({ where: { date: { gte: start, lte: end }, - timesheet: whereEmployee, + timesheet: where_employee, }, select: { amount: true, @@ -97,12 +109,12 @@ export class PayPeriodsQueryService { }, }); - const byEmployee = new Map(); + const by_employee = new Map(); // seed for employee without data - if (options?.seedNames) { - for (const [id, name] of options.seedNames.entries()) { - byEmployee.set(id, { + if (options?.seed_names) { + for (const [id, name] of options.seed_names.entries()) { + by_employee.set(id, { employee_id: id, employee_name: name, regular_hours: 0, @@ -117,8 +129,8 @@ export class PayPeriodsQueryService { } const ensure = (id: number, name: string) => { - if (!byEmployee.has(id)) { - byEmployee.set(id, { + if (!by_employee.has(id)) { + by_employee.set(id, { employee_id: id, employee_name: name, regular_hours: 0, @@ -130,24 +142,24 @@ export class PayPeriodsQueryService { is_approved: true, }); } - return byEmployee.get(id)!; + return by_employee.get(id)!; }; for (const shift of shifts) { const employee = shift.timesheet.employee; const name = `${employee.user.first_name} ${employee.user.last_name}`.trim(); - const rec = ensure(employee.id, name); + const record = ensure(employee.id, name); const hours = computeHours(shift.start_time, shift.end_time); const categorie = (shift.bank_code?.categorie || "REGULAR").toUpperCase(); switch (categorie) { - case "EVENING": rec.evening_hours += hours; break; + case "EVENING": record.evening_hours += hours; break; case "EMERGENCY": - case "URGENT": rec.emergency_hours += hours; break; - case "OVERTIME": rec.overtime_hours += hours; break; - default: rec.regular_hours += hours; break; + case "URGENT": record.emergency_hours += hours; break; + case "OVERTIME": record.overtime_hours += hours; break; + default: record.regular_hours += hours; break; } - rec.is_approved = rec.is_approved && shift.timesheet.is_approved; + record.is_approved = record.is_approved && shift.timesheet.is_approved; } for (const expense of expenses) { @@ -166,25 +178,27 @@ export class PayPeriodsQueryService { record.is_approved = record.is_approved && expense.timesheet.is_approved; } - const employees_overview = Array.from(byEmployee.values()).sort((a, b) => + const employees_overview = Array.from(by_employee.values()).sort((a, b) => a.employee_name.localeCompare(b.employee_name, "fr", { sensitivity: "base" }), ); return { - period_number: period.period_number, - year: period.year, - start_date: toDateString(start), - end_date: toDateString(end), + pay_period_no: period.period_no, + pay_year: period.pay_year, + payday: toDateString(payd), + period_start: toDateString(start), + period_end: toDateString(end), label: period.label, employees_overview, }; } - async getCrewOverview(year: number, periodNumber: number, email: string, includeSubtree: boolean): Promise { + async getCrewOverview(pay_year: number, period_no: number, email: string, include_subtree: boolean): + Promise { // 1) Search for the period - const period = await this.prisma.payPeriods.findFirst({ where: { year, period_number: periodNumber } }); - if (!period) throw new NotFoundException(`Pay period ${year}-${periodNumber} not found`); + const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no: period_no } }); + if (!period) throw new NotFoundException(`Pay period ${pay_year}-${period_no} not found`); // 2) fetch supervisor const supervisor = await this.prisma.employees.findFirst({ @@ -199,34 +213,42 @@ export class PayPeriodsQueryService { if (!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor'); // 3)fetchs crew members - const crew = await this.resolveCrew(supervisor.id, includeSubtree); // [{ id, first_name, last_name }] - const crewIds = crew.map(c => c.id); + const crew = await this.resolveCrew(supervisor.id, include_subtree); // [{ id, first_name, last_name }] + const crew_ids = crew.map(c => c.id); // seed names map for employee without data - const seedNames = new Map(crew.map(c => [c.id, `${c.first_name} ${c.last_name}`.trim()])); + const seed_names = new Map(crew.map(crew => [crew.id, `${crew.first_name} ${crew.last_name}`.trim()])); // 4) overview build - return this.buildOverview(period, { filteredEmployeeIds: crewIds, seedNames }); + return this.buildOverview({ + period_no : period.pay_period_no, + period_start: period.period_start, + period_end : period.period_end, + payday : period.payday, + pay_year : period.pay_year, + label : period.label, + }, { filtered_employee_ids: crew_ids, seed_names }); } -private async resolveCrew(supervisorId: number, includeSubtree: boolean): Promise> { +private async resolveCrew(supervisor_id: number, include_subtree: boolean): + Promise> { const result: Array<{ id: number; first_name: string; last_name: string }> = []; let frontier = await this.prisma.employees.findMany({ - where: { supervisor_id: supervisorId }, + where: { supervisor_id: supervisor_id }, select: { id: true, user: { select: { first_name: true, last_name: true } } }, }); - result.push(...frontier.map(e => ({ id: e.id, first_name: e.user.first_name, last_name: e.user.last_name }))); + result.push(...frontier.map(emp => ({ id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name }))); - if (!includeSubtree) return result; + if (!include_subtree) return result; while (frontier.length) { - const parentIds = frontier.map(e => e.id); + const parent_ids = frontier.map(emp => emp.id); const next = await this.prisma.employees.findMany({ - where: { supervisor_id: { in: parentIds } }, + where: { supervisor_id: { in: parent_ids } }, select: { id: true, user: { select: { first_name: true, last_name: true } } }, }); if (next.length === 0) break; - result.push(...next.map(e => ({ id: e.id, first_name: e.user.first_name, last_name: e.user.last_name }))); + result.push(...next.map(emp => ({ id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name }))); frontier = next; } @@ -237,54 +259,69 @@ private async resolveCrew(supervisorId: number, includeSubtree: boolean): Promis async findAll(): Promise { const currentPayYear = payYearOfDate(new Date()); return listPayYear(currentPayYear).map(period =>({ - period_number: period.period_number, - year: period.year, - start_date: period.start_date, - end_date: period.end_date, + pay_period_no: period.period_no, + pay_year: period.pay_year, + payday: period.payday, + period_start: period.period_start, + period_end: period.period_end, label: period.label, })); } - async findOne(periodNumber: number): Promise { + async findOne(period_no: number): Promise { const row = await this.prisma.payPeriods.findFirst({ - where: { period_number: periodNumber }, - orderBy: { year: "desc" }, + where: { pay_period_no: period_no }, + orderBy: { pay_year: "desc" }, }); - if (!row) throw new NotFoundException(`Pay period #${periodNumber} not found`); + if (!row) throw new NotFoundException(`Pay period #${period_no} not found`); return mapPayPeriodToDto(row); } - async findOneByYearPeriod(year: number, periodNumber: number): Promise { + async findOneByYearPeriod(pay_year: number, period_no: number): Promise { const row = await this.prisma.payPeriods.findFirst({ - where: { year, period_number: periodNumber }, + where: { pay_year, pay_period_no: period_no }, }); if(row) return mapPayPeriodToDto(row); // fallback for outside of view periods - const p = computePeriod(year, periodNumber); - return {period_number: p.period_number, year: p.year, start_date: p.start_date, end_date: p.end_date, label: p.label} + const period = computePeriod(pay_year, period_no); + return { + pay_period_no: period.period_no, + pay_year: period.pay_year, + period_start: period.period_start, + payday: period.payday, + period_end: period.period_end, + label: period.label + } } //function to cherry pick a Date to find a period async findByDate(date: string): Promise { const dt = new Date(date); const row = await this.prisma.payPeriods.findFirst({ - where: { start_date: { lte: dt }, end_date: { gte: dt } }, + where: { period_start: { lte: dt }, period_end: { gte: dt } }, }); if(row) return mapPayPeriodToDto(row); //fallback for outwside view periods - const payYear = payYearOfDate(date); - const periods = listPayYear(payYear); - const hit = periods.find(p => date >= p.start_date && date <= p.end_date); + const pay_year = payYearOfDate(date); + const periods = listPayYear(pay_year); + const hit = periods.find(period => date >= period.period_start && date <= period.period_end); if(!hit) throw new NotFoundException(`No period found for ${date}`); - return { period_number: hit.period_number, year: hit.year, start_date: hit.start_date, end_date:hit.end_date, label: hit.label} + return { + pay_period_no: hit.period_no, + pay_year : hit.pay_year, + period_start : hit.period_start, + period_end : hit.period_end, + payday : hit.payday, + label : hit.label + } } async findCurrent(date?: string): Promise { - const isoDay = date ?? new Date().toISOString().slice(0,10); - return this.findByDate(isoDay); + const iso_day = date ?? new Date().toISOString().slice(0,10); + return this.findByDate(iso_day); } } diff --git a/src/modules/pay-periods/utils/pay-year.util.ts b/src/modules/pay-periods/utils/pay-year.util.ts index 68193b6..1330601 100644 --- a/src/modules/pay-periods/utils/pay-year.util.ts +++ b/src/modules/pay-periods/utils/pay-year.util.ts @@ -9,8 +9,6 @@ const toUTCDate = (iso: string | Date) => { }; export const toDateString = (d: Date) => d.toISOString().slice(0, 10); -const ANCHOR = toUTCDate(ANCHOR_ISO); - export function payYearOfDate(date: string | Date, anchorISO = ANCHOR_ISO): number { const ANCHOR = toUTCDate(anchorISO); const d = toUTCDate(date); @@ -19,23 +17,25 @@ export function payYearOfDate(date: string | Date, anchorISO = ANCHOR_ISO): numb return ANCHOR.getUTCFullYear() + 1 + cycles; } //compute labels for periods -export function computePeriod(payYear: number, periodNumber: number, anchorISO = ANCHOR_ISO) { +export function computePeriod(pay_year: number, period_no: number, anchorISO = ANCHOR_ISO) { const ANCHOR = toUTCDate(anchorISO); - const cycles = payYear - (ANCHOR.getUTCFullYear() + 1); - const offsetPeriods = cycles * PERIODS_PER_YEAR + (periodNumber - 1); + const cycles = pay_year - (ANCHOR.getUTCFullYear() + 1); + const offsetPeriods = cycles * PERIODS_PER_YEAR + (period_no - 1); const start = new Date(+ANCHOR + offsetPeriods * PERIOD_DAYS * MS_PER_DAY); const end = new Date(+start + (PERIOD_DAYS - 1) * MS_PER_DAY); + const pay = new Date(end.getTime() + 6 * MS_PER_DAY); return { - period_number: periodNumber, - year: payYear, - start_date: toDateString(start), - end_date: toDateString(end), + period_no: period_no, + pay_year: pay_year, + payday: toDateString(pay), + period_start: toDateString(start), + period_end: toDateString(end), label: `${toDateString(start)} → ${toDateString(end)}`, start, end, }; } //list of all 26 periods for a full year -export function listPayYear(payYear: number, anchorISO = ANCHOR_ISO) { - return Array.from({ length: PERIODS_PER_YEAR }, (_, i) => computePeriod(payYear, i + 1, anchorISO)); +export function listPayYear(pay_year: number, anchorISO = ANCHOR_ISO) { + return Array.from({ length: PERIODS_PER_YEAR }, (_, i) => computePeriod(pay_year, i + 1, anchorISO)); } diff --git a/src/modules/shifts/services/shifts-query.service.ts b/src/modules/shifts/services/shifts-query.service.ts index bfa18fe..ef51021 100644 --- a/src/modules/shifts/services/shifts-query.service.ts +++ b/src/modules/shifts/services/shifts-query.service.ts @@ -117,16 +117,16 @@ export class ShiftsQueryService { async getSummary(period_id: number): Promise { //fetch pay-period to display const period = await this.prisma.payPeriods.findFirst({ - where: { period_number: period_id }, + where: { pay_period_no: period_id }, }); if(!period) { throw new NotFoundException(`pay-period ${period_id} not found`); } - const { start_date, end_date } = period; + const { period_start, period_end } = period; //prepare shifts and expenses for display const shifts = await this.prisma.shifts.findMany({ - where: { date: { gte: start_date, lte: end_date } }, + where: { date: { gte: period_start, lte: period_end } }, include: { bank_code: true, timesheet: { include: { @@ -139,7 +139,7 @@ export class ShiftsQueryService { }); const expenses = await this.prisma.expenses.findMany({ - where: { date: { gte: start_date, lte: end_date } }, + where: { date: { gte: period_start, lte: period_end } }, include: { bank_code: true, timesheet: { include: { employee: {