fix(pay-period): commented bulk approval and command service

This commit is contained in:
Matthieu Haineault 2025-10-22 10:16:38 -04:00
parent 0ce1191437
commit 9f27c83981
4 changed files with 584 additions and 207 deletions

View File

@ -429,6 +429,359 @@
]
}
},
"/pay-periods/current-and-all": {
"get": {
"operationId": "PayPeriodsController_getCurrentAndAll",
"parameters": [
{
"name": "date",
"required": false,
"in": "query",
"description": "Override for resolving the current period",
"schema": {
"example": "2025-08-11",
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Find current and all pay periods",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriodBundleDto"
}
}
}
}
},
"summary": "Return current pay period and the full list",
"tags": [
"pay-periods"
]
}
},
"/pay-periods/date/{date}": {
"get": {
"operationId": "PayPeriodsController_findByDate",
"parameters": [
{
"name": "date",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Pay period found for the selected date",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriodDto"
}
}
}
},
"404": {
"description": "Pay period not found for the selected date"
}
},
"summary": "Resolve a period by a date within it",
"tags": [
"pay-periods"
]
}
},
"/pay-periods/{year}/{periodNumber}": {
"get": {
"operationId": "PayPeriodsController_findOneByYear",
"parameters": [
{
"name": "year",
"required": true,
"in": "path",
"schema": {
"example": 2024,
"type": "number"
}
},
{
"name": "periodNumber",
"required": true,
"in": "path",
"description": "1..26",
"schema": {
"example": 1,
"type": "number"
}
}
],
"responses": {
"200": {
"description": "Pay period found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriodDto"
}
}
}
},
"404": {
"description": "Pay period not found"
}
},
"summary": "Find pay period by year and period number",
"tags": [
"pay-periods"
]
}
},
"/pay-periods/{year}/{periodNumber}/{email}": {
"get": {
"operationId": "PayPeriodsController_getCrewOverview",
"parameters": [
{
"name": "year",
"required": true,
"in": "path",
"schema": {
"example": 2024,
"type": "number"
}
},
{
"name": "periodNumber",
"required": true,
"in": "path",
"description": "1..26",
"schema": {
"example": 1,
"type": "number"
}
},
{
"name": "email",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
},
{
"name": "includeSubtree",
"required": false,
"in": "query",
"description": "Include indirect reports",
"schema": {
"example": false,
"type": "boolean"
}
}
],
"responses": {
"200": {
"description": "Crew overview",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriodOverviewDto"
}
}
}
},
"404": {
"description": "Pay period not found"
}
},
"summary": "Supervisor crew overview for a given pay period",
"tags": [
"pay-periods"
]
}
},
"/pay-periods/overview/{year}/{periodNumber}": {
"get": {
"operationId": "PayPeriodsController_getOverviewByYear",
"parameters": [
{
"name": "year",
"required": true,
"in": "path",
"schema": {
"example": 2024,
"type": "number"
}
},
{
"name": "periodNumber",
"required": true,
"in": "path",
"description": "1..26",
"schema": {
"example": 1,
"type": "number"
}
}
],
"responses": {
"200": {
"description": "Pay period overview found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriodOverviewDto"
}
}
}
},
"404": {
"description": "Pay period not found"
}
},
"summary": "Detailed view of a pay period by year + number",
"tags": [
"pay-periods"
]
}
},
"/timesheets": {
"get": {
"operationId": "TimesheetController_getTimesheetByIds",
"parameters": [
{
"name": "employee_email",
"required": true,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "year",
"required": true,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "period_number",
"required": true,
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"Timesheet"
]
}
},
"/shift": {
"get": {
"operationId": "ShiftController_getShiftsByIds",
"parameters": [
{
"name": "shift_ids",
"required": true,
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"Shift"
]
},
"patch": {
"operationId": "ShiftController_updateBatch",
"parameters": [],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"Shift"
]
}
},
"/shift/{timesheet_id}": {
"post": {
"operationId": "ShiftController_createBatch",
"parameters": [
{
"name": "timesheet_id",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"responses": {
"201": {
"description": ""
}
},
"tags": [
"Shift"
]
}
},
"/shift/{shift_id}": {
"delete": {
"operationId": "ShiftController_remove",
"parameters": [
{
"name": "shift_id",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"Shift"
]
}
},
"/preferences/{email}": {
"patch": {
"operationId": "PreferencesController_updatePreferences",
@ -562,139 +915,6 @@
"SchedulePresets"
]
}
},
"/shift": {
"get": {
"operationId": "ShiftController_getShiftsByIds",
"parameters": [
{
"name": "shift_ids",
"required": true,
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"Shift"
]
},
"patch": {
"operationId": "ShiftController_updateBatch",
"parameters": [],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"Shift"
]
}
},
"/shift/{timesheet_id}": {
"post": {
"operationId": "ShiftController_createBatch",
"parameters": [
{
"name": "timesheet_id",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"responses": {
"201": {
"description": ""
}
},
"tags": [
"Shift"
]
}
},
"/shift/{shift_id}": {
"delete": {
"operationId": "ShiftController_remove",
"parameters": [
{
"name": "shift_id",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"Shift"
]
}
},
"/timesheets": {
"get": {
"operationId": "TimesheetController_getTimesheetByIds",
"parameters": [
{
"name": "employee_email",
"required": true,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "year",
"required": true,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "period_number",
"required": true,
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"Timesheet"
]
}
}
},
"info": {
@ -1018,6 +1238,168 @@
}
}
},
"PayPeriodDto": {
"type": "object",
"properties": {
"pay_period_no": {
"type": "number",
"example": 1,
"description": "numéro cyclique de la période entre 1 et 26"
},
"period_start": {
"type": "string",
"example": "2023-12-17",
"format": "date"
},
"period_end": {
"type": "string",
"example": "2023-12-30",
"format": "date"
},
"payday": {
"type": "string",
"example": "2023-01-04",
"format": "date"
},
"pay_year": {
"type": "number",
"example": 2023
},
"label": {
"type": "string",
"example": "2023-12-17 → 2023-12-30"
}
},
"required": [
"pay_period_no",
"period_start",
"period_end",
"payday",
"pay_year",
"label"
]
},
"PayPeriodBundleDto": {
"type": "object",
"properties": {
"current": {
"description": "Current pay period (resolved from date)",
"allOf": [
{
"$ref": "#/components/schemas/PayPeriodDto"
}
]
},
"periods": {
"description": "All pay periods",
"type": "array",
"items": {
"$ref": "#/components/schemas/PayPeriodDto"
}
}
},
"required": [
"current",
"periods"
]
},
"EmployeePeriodOverviewDto": {
"type": "object",
"properties": {
"employee_name": {
"type": "string",
"example": "Alex Dupont",
"description": "Nom complet de lemployé"
},
"regular_hours": {
"type": "number",
"example": 40,
"description": "pay-period`s regular hours"
},
"other_hours": {
"type": "object",
"example": 0,
"description": "pay-period`s other hours"
},
"expenses": {
"type": "number",
"example": 420.69,
"description": "pay-period`s total expenses ($)"
},
"mileage": {
"type": "number",
"example": 40,
"description": "pay-period total mileages (km)"
},
"is_approved": {
"type": "boolean",
"example": true,
"description": "Tous les timesheets de la période sont approuvés pour cet employé"
}
},
"required": [
"employee_name",
"regular_hours",
"other_hours",
"expenses",
"mileage",
"is_approved"
]
},
"PayPeriodOverviewDto": {
"type": "object",
"properties": {
"pay_period_no": {
"type": "number",
"example": 1,
"description": "Period number (126)"
},
"pay_year": {
"type": "number",
"example": 2023,
"description": "Calendar year of the period"
},
"period_start": {
"type": "string",
"example": "2023-12-17",
"format": "date",
"description": "Period start date (YYYY-MM-DD)"
},
"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",
"description": "Human-readable label"
},
"employees_overview": {
"description": "Per-employee overview for the period",
"type": "array",
"items": {
"$ref": "#/components/schemas/EmployeePeriodOverviewDto"
}
}
},
"required": [
"pay_period_no",
"pay_year",
"period_start",
"period_end",
"payday",
"label",
"employees_overview"
]
},
"PreferencesDto": {
"type": "object",
"properties": {}

View File

@ -5,7 +5,7 @@ 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 { PayPeriodsCommandService } from "../services/pay-periods-command.service";
// import { PayPeriodsCommandService } from "../services/pay-periods-command.service";
import { PayPeriodBundleDto } from "../dtos/bundle-pay-period.dto";
import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto";
@ -15,7 +15,7 @@ export class PayPeriodsController {
constructor(
private readonly queryService: PayPeriodsQueryService,
private readonly commandService: PayPeriodsCommandService,
// private readonly commandService: PayPeriodsCommandService,
) {}
@Get('current-and-all')
@ -51,13 +51,13 @@ export class PayPeriodsController {
return this.queryService.findOneByYearPeriod(year, period_no);
}
@Patch("crew/bulk-approval")
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ApiOperation({ summary: "Approve all selected timesheets in the period" })
@ApiResponse({ status: 200, description: "Pay period approved" })
async bulkApproval(@Body() dto: BulkCrewApprovalDto) {
return this.commandService.bulkApproveCrew(dto);
}
// @Patch("crew/bulk-approval")
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
// @ApiOperation({ summary: "Approve all selected timesheets in the period" })
// @ApiResponse({ status: 200, description: "Pay period approved" })
// async bulkApproval(@Body() dto: BulkCrewApprovalDto) {
// return this.commandService.bulkApproveCrew(dto);
// }
@Get(':year/:periodNumber/:email')
//@RolesAllowed(RoleEnum.SUPERVISOR)

View File

@ -1,7 +1,6 @@
import { PrismaModule } from "src/prisma/prisma.module";
import { PayPeriodsController } from "./controllers/pay-periods.controller";
import { Module } from "@nestjs/common";
import { PayPeriodsCommandService } from "./services/pay-periods-command.service";
import { PayPeriodsQueryService } from "./services/pay-periods-query.service";
import { TimesheetsModule } from "../timesheets/timesheets.module";
import { SharedModule } from "../shared/shared.module";
@ -12,14 +11,10 @@ import { BusinessLogicsModule } from "../business-logics/business-logics.module"
imports: [PrismaModule, TimesheetsModule, SharedModule, BusinessLogicsModule],
providers: [
PayPeriodsQueryService,
PayPeriodsCommandService,
PrismaService,
],
controllers: [PayPeriodsController],
exports: [
PayPeriodsQueryService,
PayPeriodsCommandService,
]
exports: [ PayPeriodsQueryService ],
})
export class PayperiodsModule {}

View File

@ -1,71 +1,71 @@
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto";
import { PayPeriodsQueryService } from "./pay-periods-query.service";
import { TimesheetApprovalService } from "src/modules/timesheets/services/timesheet-approval.service";
// import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
// import { PrismaService } from "src/prisma/prisma.service";
// import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto";
// import { PayPeriodsQueryService } from "./pay-periods-query.service";
// import { TimesheetApprovalService } from "src/modules/timesheets/services/timesheet-approval.service";
@Injectable()
export class PayPeriodsCommandService {
constructor(
private readonly prisma: PrismaService,
private readonly timesheets_approval: TimesheetApprovalService,
private readonly query: PayPeriodsQueryService,
) {}
// @Injectable()
// export class PayPeriodsCommandService {
// constructor(
// private readonly prisma: PrismaService,
// private readonly timesheets_approval: TimesheetApprovalService,
// private readonly query: PayPeriodsQueryService,
// ) {}
//function to approve pay-periods according to selected crew members
async bulkApproveCrew(dto:BulkCrewApprovalDto): Promise<{updated: number}> {
const { supervisor_email, include_subtree, items } = dto;
if(!items?.length) throw new BadRequestException('no items to process');
// //function to approve pay-periods according to selected crew members
// async bulkApproveCrew(dto:BulkCrewApprovalDto): Promise<{updated: number}> {
// const { supervisor_email, include_subtree, items } = dto;
// if(!items?.length) throw new BadRequestException('no items to process');
//fetch and validate supervisor status
const supervisor = await this.query.getSupervisor(supervisor_email);
if(!supervisor) throw new NotFoundException('No employee record linked to current user');
if(!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor');
// //fetch and validate supervisor status
// const supervisor = await this.query.getSupervisor(supervisor_email);
// if(!supervisor) throw new NotFoundException('No employee record linked to current user');
// if(!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor');
//fetches emails of crew members linked to supervisor
const crew_emails = await this.query.resolveCrewEmails(supervisor.id, include_subtree);
// //fetches emails of crew members linked to supervisor
// const crew_emails = await this.query.resolveCrewEmails(supervisor.id, include_subtree);
for(const item of items) {
if(!crew_emails.has(item.employee_email)) {
throw new ForbiddenException(`Employee ${item.employee_email} not in supervisor crew`);
}
}
// for(const item of items) {
// if(!crew_emails.has(item.employee_email)) {
// throw new ForbiddenException(`Employee ${item.employee_email} not in supervisor crew`);
// }
// }
const period_cache = new Map<string, {period_start: Date, period_end: Date}>();
const getPeriod = async (y:number, no: number) => {
const key = `${y}-${no}`;
if(!period_cache.has(key)) return period_cache.get(key)!;
const period = await this.query.getPeriodWindow(y,no);
if(!period) throw new NotFoundException(`Pay period ${y}-${no} not found`);
period_cache.set(key, period);
return period;
};
// const period_cache = new Map<string, {period_start: Date, period_end: Date}>();
// const getPeriod = async (y:number, no: number) => {
// const key = `${y}-${no}`;
// if(!period_cache.has(key)) return period_cache.get(key)!;
// const period = await this.query.getPeriodWindow(y,no);
// if(!period) throw new NotFoundException(`Pay period ${y}-${no} not found`);
// period_cache.set(key, period);
// return period;
// };
let updated = 0;
// let updated = 0;
await this.prisma.$transaction(async (transaction) => {
for(const item of items) {
const { period_start, period_end } = await getPeriod(item.pay_year, item.period_no);
// await this.prisma.$transaction(async (transaction) => {
// for(const item of items) {
// const { period_start, period_end } = await getPeriod(item.pay_year, item.period_no);
const t_sheets = await transaction.timesheets.findMany({
where: {
employee: { user: { email: item.employee_email } },
OR: [
{shift : { some: { date: { gte: period_start, lte: period_end } } } },
{expense: { some: { date: { gte: period_start, lte: period_end } } } },
],
},
select: { id: true },
});
// const t_sheets = await transaction.timesheets.findMany({
// where: {
// employee: { user: { email: item.employee_email } },
// OR: [
// {shift : { some: { date: { gte: period_start, lte: period_end } } } },
// {expense: { some: { date: { gte: period_start, lte: period_end } } } },
// ],
// },
// select: { id: true },
// });
for(const { id } of t_sheets) {
await this.timesheets_approval.updateApprovalWithTransaction(transaction, id, item.approve);
updated++;
}
// for(const { id } of t_sheets) {
// await this.timesheets_approval.updateApprovalWithTransaction(transaction, id, item.approve);
// updated++;
// }
}
});
return {updated};
}
}
// }
// });
// return {updated};
// }
// }