refactor(pay-period): refactor module to match required needs
This commit is contained in:
parent
91ef6685b4
commit
91b718237d
|
|
@ -545,6 +545,34 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/timesheets/{id}/approval": {
|
||||||
|
"patch": {
|
||||||
|
"operationId": "TimesheetsController_approve",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"access-token": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Timesheets"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/Expenses": {
|
"/Expenses": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "ExpensesController_create",
|
"operationId": "ExpensesController_create",
|
||||||
|
|
@ -1003,6 +1031,34 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/notifications/summary": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "NotificationsController_summary",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/notifications/stream": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "NotificationsController_stream",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/leave-requests": {
|
"/leave-requests": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "LeaveRequestController_create",
|
"operationId": "LeaveRequestController_create",
|
||||||
|
|
@ -1197,6 +1253,34 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/leave-requests/{id}/approval": {
|
||||||
|
"patch": {
|
||||||
|
"operationId": "LeaveRequestController_updateApproval",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"access-token": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Leave Requests"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/auth/v1/login": {
|
"/auth/v1/login": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "AuthController_login",
|
"operationId": "AuthController_login",
|
||||||
|
|
@ -1767,14 +1851,11 @@
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/PayPeriodEntity"
|
"$ref": "#/components/schemas/PayPeriodDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "List of pay period not found"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"summary": "Find all pay period",
|
"summary": "Find all pay period",
|
||||||
|
|
@ -1783,49 +1864,26 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/pay-periods/{periodNumber}": {
|
"/pay-periods/{year}/{periodNumber}/overview": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "PayPeriodsController_findOne",
|
"operationId": "PayPeriodsController_getOverviewByYear",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "periodNumber",
|
"name": "year",
|
||||||
"required": true,
|
"required": true,
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
"example": 2024,
|
||||||
"type": "number"
|
"type": "number"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Pay period found",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/PayPeriodEntity"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"400": {
|
|
||||||
"description": "Pay period not found"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"summary": "Find pay period",
|
|
||||||
"tags": [
|
|
||||||
"pay-periods"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/pay-periods/{periodNumber}/overview": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "PayPeriodsController_getOverview",
|
|
||||||
"parameters": [
|
|
||||||
{
|
{
|
||||||
"name": "periodNumber",
|
"name": "periodNumber",
|
||||||
"required": true,
|
"required": true,
|
||||||
"in": "path",
|
"in": "path",
|
||||||
|
"description": "1..26",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
"example": 1,
|
||||||
"type": "number"
|
"type": "number"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1841,11 +1899,56 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"404": {
|
||||||
"description": "Pay period not found"
|
"description": "Pay period not found"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"summary": "detailed view of a pay period",
|
"summary": "Detailed view of a pay period by year + number",
|
||||||
|
"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": [
|
"tags": [
|
||||||
"pay-periods"
|
"pay-periods"
|
||||||
]
|
]
|
||||||
|
|
@ -1870,16 +1973,106 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/PayPeriodEntity"
|
"$ref": "#/components/schemas/PayPeriodDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"404": {
|
||||||
"description": "Pay period not found for the selected date date"
|
"description": "Pay period not found for the selected date"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"summary": "cherry picking a date to find a period",
|
"summary": "Resolve a period by a date within it",
|
||||||
|
"tags": [
|
||||||
|
"pay-periods"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/pay-periods/{year}/{periodNumber}/approval": {
|
||||||
|
"patch": {
|
||||||
|
"operationId": "PayPeriodsController_approve",
|
||||||
|
"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 approved"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"summary": "Approve all timesheets with activity in the period",
|
||||||
|
"tags": [
|
||||||
|
"pay-periods"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/pay-periods/{year}/{periodNumber}/crew-overview": {
|
||||||
|
"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": "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": [
|
"tags": [
|
||||||
"pay-periods"
|
"pay-periods"
|
||||||
]
|
]
|
||||||
|
|
@ -2725,7 +2918,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"PayPeriodEntity": {
|
"PayPeriodDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"period_number": {
|
"period_number": {
|
||||||
|
|
@ -2764,30 +2957,60 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"employee_id": {
|
"employee_id": {
|
||||||
"type": "string",
|
"type": "number",
|
||||||
"example": "a1b2c3d4",
|
"example": 42,
|
||||||
"description": "Employee`s ID"
|
"description": "Employees.id (clé primaire num.)"
|
||||||
},
|
},
|
||||||
"employee_name": {
|
"employee_name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "Alex Dupont",
|
"example": "Alex Dupont",
|
||||||
"description": "Employee`s full name"
|
"description": "Nom complet de lemployé"
|
||||||
},
|
},
|
||||||
"total_hours": {
|
"regular_hours": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"example": 34,
|
"example": 40,
|
||||||
"description": "period`s total worked hours"
|
"description": "pay-period`s regular hours"
|
||||||
|
},
|
||||||
|
"evening_hours": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 0,
|
||||||
|
"description": "pay-period`s evening hours"
|
||||||
|
},
|
||||||
|
"emergency_hours": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 0,
|
||||||
|
"description": "pay-period`s emergency hours"
|
||||||
|
},
|
||||||
|
"overtime_hours": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 2,
|
||||||
|
"description": "pay-period`s overtime 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": {
|
"is_approved": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"example": true,
|
"example": true,
|
||||||
"description": "All timesheets are approved for this employee"
|
"description": "Tous les timesheets de la période sont approuvés pour cet employé"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"employee_id",
|
"employee_id",
|
||||||
"employee_name",
|
"employee_name",
|
||||||
"total_hours",
|
"regular_hours",
|
||||||
|
"evening_hours",
|
||||||
|
"emergency_hours",
|
||||||
|
"overtime_hours",
|
||||||
|
"expenses",
|
||||||
|
"mileage",
|
||||||
"is_approved"
|
"is_approved"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -2797,27 +3020,32 @@
|
||||||
"period_number": {
|
"period_number": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"example": 1,
|
"example": 1,
|
||||||
"description": "period`s number ( 1-26 )"
|
"description": "Period number (1–26)"
|
||||||
|
},
|
||||||
|
"year": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 2023,
|
||||||
|
"description": "Calendar year of the period"
|
||||||
},
|
},
|
||||||
"start_date": {
|
"start_date": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "2023-12-17",
|
"example": "2023-12-17",
|
||||||
"format": "date",
|
"format": "date",
|
||||||
"description": "Period`s starting date"
|
"description": "Period start date (YYYY-MM-DD)"
|
||||||
},
|
},
|
||||||
"end_date": {
|
"end_date": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "2023-12-30",
|
"example": "2023-12-30",
|
||||||
"format": "date",
|
"format": "date",
|
||||||
"description": "Period`s ending date"
|
"description": "Period end date (YYYY-MM-DD)"
|
||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "2023-12-17 → 2023-12-30",
|
"example": "2023-12-17 → 2023-12-30",
|
||||||
"description": "period`s label for showing"
|
"description": "Human-readable label"
|
||||||
},
|
},
|
||||||
"employees_overview": {
|
"employees_overview": {
|
||||||
"description": "Detailed view by employee for a chosen period",
|
"description": "Per-employee overview for the period",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/EmployeePeriodOverviewDto"
|
"$ref": "#/components/schemas/EmployeePeriodOverviewDto"
|
||||||
|
|
@ -2826,6 +3054,7 @@
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"period_number",
|
"period_number",
|
||||||
|
"year",
|
||||||
"start_date",
|
"start_date",
|
||||||
"end_date",
|
"end_date",
|
||||||
"label",
|
"label",
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import { Controller, Get, Req, Sse, UseGuards,
|
import { Controller, Get, Req, Sse,
|
||||||
MessageEvent as NestMessageEvent } from "@nestjs/common";
|
MessageEvent as NestMessageEvent } from "@nestjs/common";
|
||||||
import { JwtAuthGuard } from "../../authentication/guards/jwt-auth.guard";
|
|
||||||
import { NotificationsService } from "../services/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';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Controller('notifications')
|
@Controller('notifications')
|
||||||
export class NotificationsController {
|
export class NotificationsController {
|
||||||
constructor(private readonly notificationsService: NotificationsService) {}
|
constructor(private readonly notificationsService: NotificationsService) {}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { NotificationsController } from "./controllers/notifications.controller";
|
import { NotificationsController } from "./controllers/notifications.controller";
|
||||||
import { NotificationsService } from "./services/notifications.service";
|
import { NotificationsService } from "./services/notifications.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [NotificationsService],
|
providers: [NotificationsService],
|
||||||
controllers: [NotificationsController],
|
controllers: [NotificationsController],
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import { Controller, Get, Param, ParseIntPipe } from "@nestjs/common";
|
import { Controller, ForbiddenException, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query } from "@nestjs/common";
|
||||||
import { PayPeriods } from "@prisma/client";
|
import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
|
||||||
import { PayPeriodsService } from "../services/pay-periods.service";
|
import { PayPeriodsService } from "../services/pay-periods.service";
|
||||||
import { PayPeriodEntity } from "../dtos/swagger-entities/pay-period.entity";
|
import { PayPeriodDto } from "../dtos/pay-period.dto";
|
||||||
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
|
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
|
||||||
import { PayPeriodsOverviewService } from "../services/pay-periods-overview.service";
|
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";
|
||||||
|
|
||||||
@ApiTags('pay-periods')
|
@ApiTags('pay-periods')
|
||||||
@Controller('pay-periods')
|
@Controller('pay-periods')
|
||||||
|
|
@ -13,40 +16,88 @@ export class PayPeriodsController {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly payPeriodsService: PayPeriodsService,
|
private readonly payPeriodsService: PayPeriodsService,
|
||||||
private readonly overviewService: PayPeriodsOverviewService
|
private readonly queryService: PayPeriodsQueryService,
|
||||||
|
private readonly commandService: PayPeriodsCommandService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: 'Find all pay period' })
|
@ApiOperation({ summary: 'Find all pay period' })
|
||||||
@ApiResponse({status: 200,description: 'List of pay period found', type: PayPeriodEntity, isArray: true })
|
@ApiResponse({status: 200,description: 'List of pay period found', type: PayPeriodDto, isArray: true })
|
||||||
@ApiResponse({status: 400, description: 'List of pay period not found' })
|
async findAll(): Promise<PayPeriodDto[]> {
|
||||||
async findAll(): Promise<PayPeriods[]> {
|
|
||||||
return this.payPeriodsService.findAll();
|
return this.payPeriodsService.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':periodNumber')
|
@Get(':year/:periodNumber/overview')
|
||||||
@ApiOperation({ summary: 'Find pay period' })
|
@ApiOperation({ summary: 'Detailed view of a pay period by year + number' })
|
||||||
@ApiResponse({status: 200,description: 'Pay period found', type: PayPeriodEntity })
|
@ApiParam({ name: 'year', type: Number, example: 2024 })
|
||||||
@ApiResponse({status: 400, description: 'Pay period not found' })
|
@ApiParam({ name: 'periodNumber', type: Number, example: 1, description: '1..26' })
|
||||||
findOne(@Param('periodNumber', ParseIntPipe) periodNumber: number): Promise<PayPeriods | null> {
|
|
||||||
return this.payPeriodsService.findOne(periodNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':periodNumber/overview')
|
|
||||||
@ApiOperation({ summary: 'detailed view of a pay period'})
|
|
||||||
@ApiResponse({ status: 200, description: 'Pay period overview found', type: PayPeriodOverviewDto })
|
@ApiResponse({ status: 200, description: 'Pay period overview found', type: PayPeriodOverviewDto })
|
||||||
@ApiResponse({status: 400, description: 'Pay period not found' })
|
@ApiNotFoundResponse({ description: 'Pay period not found' })
|
||||||
async getOverview(@Param('periodNumber', ParseIntPipe) periodNumber: number):
|
async getOverviewByYear(
|
||||||
Promise<PayPeriodOverviewDto> {
|
@Param('year', ParseIntPipe) year: number,
|
||||||
return this.overviewService.getOverview(periodNumber);
|
@Param('periodNumber', ParseIntPipe) periodNumber: number,
|
||||||
|
): Promise<PayPeriodOverviewDto> {
|
||||||
|
return this.queryService.getOverviewByYearPeriod(year, periodNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('date/:date')
|
|
||||||
@ApiOperation({ summary: 'cherry picking a date to find a period'})
|
@Get(":year/:periodNumber")
|
||||||
@ApiResponse({status:200, description: 'Pay period found for the selected date', type: PayPeriodEntity })
|
@ApiOperation({ summary: "Find pay period by year and period number" })
|
||||||
@ApiResponse({status:400, description: 'Pay period not found for the selected date date' })
|
@ApiParam({ name: "year", type: Number, example: 2024 })
|
||||||
async findByDate(@Param('date') date: string ):
|
@ApiParam({ name: "periodNumber", type: Number, example: 1, description: "1..26" })
|
||||||
Promise<PayPeriodEntity> {
|
@ApiResponse({ status: 200, description: "Pay period found", type: PayPeriodDto })
|
||||||
|
@ApiNotFoundResponse({ description: "Pay period not found" })
|
||||||
|
async findOneByYear(
|
||||||
|
@Param("year", ParseIntPipe) year: number,
|
||||||
|
@Param("periodNumber", ParseIntPipe) periodNumber: number,
|
||||||
|
) {
|
||||||
|
return this.payPeriodsService.findOneByYearPeriod(year, periodNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("date/:date")
|
||||||
|
@ApiOperation({ summary: "Resolve a period by a date within it" })
|
||||||
|
@ApiResponse({ status: 200, description: "Pay period found for the selected date", type: PayPeriodDto })
|
||||||
|
@ApiNotFoundResponse({ description: "Pay period not found for the selected date" })
|
||||||
|
async findByDate(@Param("date") date: string) {
|
||||||
return this.payPeriodsService.findByDate(date);
|
return this.payPeriodsService.findByDate(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch(":year/:periodNumber/approval")
|
||||||
|
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
|
@ApiOperation({ summary: "Approve all timesheets with activity in the period" })
|
||||||
|
@ApiParam({ name: "year", type: Number, example: 2024 })
|
||||||
|
@ApiParam({ name: "periodNumber", type: Number, example: 1, description: "1..26" })
|
||||||
|
@ApiResponse({ status: 200, description: "Pay period approved" })
|
||||||
|
async approve(
|
||||||
|
@Param("year", ParseIntPipe) year: number,
|
||||||
|
@Param("periodNumber", ParseIntPipe) periodNumber: number,
|
||||||
|
) {
|
||||||
|
await this.commandService.approvalPayPeriod(year, periodNumber);
|
||||||
|
return { message: `Pay-period ${year}-${periodNumber} approved` };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':year/:periodNumber/crew-overview')
|
||||||
|
@RolesAllowed(RoleEnum.SUPERVISOR)
|
||||||
|
@ApiOperation({ summary: 'Supervisor crew overview for a given pay period' })
|
||||||
|
@ApiParam({ name: 'year', type: Number, example: 2024 })
|
||||||
|
@ApiParam({ name: 'periodNumber', type: Number, example: 1, description: '1..26' })
|
||||||
|
@ApiQuery({ name: 'includeSubtree', required: false, type: Boolean, example: false, description: 'Include indirect reports' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Crew overview', type: PayPeriodOverviewDto })
|
||||||
|
@ApiNotFoundResponse({ description: 'Pay period not found' })
|
||||||
|
async getCrewOverview(
|
||||||
|
@Param('year', ParseIntPipe) year: number,
|
||||||
|
@Param('periodNumber', ParseIntPipe) periodNumber: number,
|
||||||
|
@Query('includeSubtree', new ParseBoolPipe({ optional: true })) includeSubtree = false,
|
||||||
|
@Req() req: Request,
|
||||||
|
): Promise<PayPeriodOverviewDto> {
|
||||||
|
const rawUser = (req as any).user ?? {};
|
||||||
|
const userId: string | undefined = rawUser.id ?? rawUser.sub ?? rawUser.userId; //needs ajusting according to passport logic
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new ForbiddenException('Authenticated user not found on request');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.queryService.getCrewOverview(year, periodNumber, userId, includeSubtree);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,38 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class EmployeePeriodOverviewDto {
|
export class EmployeePeriodOverviewDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: 'a1b2c3d4',
|
example: 42,
|
||||||
description: "Employee`s ID",
|
description: "Employees.id (clé primaire num.)",
|
||||||
})
|
})
|
||||||
employee_id: string;
|
employee_id: number;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: 'Alex Dupont',
|
example: 'Alex Dupont',
|
||||||
description: 'Employee`s full name',
|
description: 'Nom complet de lemployé',
|
||||||
})
|
})
|
||||||
employee_name: string;
|
employee_name: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({ example: 40, description: 'pay-period`s regular hours' })
|
||||||
example: 34,
|
regular_hours: number;
|
||||||
description: 'period`s total worked hours',
|
|
||||||
})
|
@ApiProperty({ example: 0, description: 'pay-period`s evening hours' })
|
||||||
total_hours: number;
|
evening_hours: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 0, description: 'pay-period`s emergency hours' })
|
||||||
|
emergency_hours: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 2, description: 'pay-period`s overtime hours' })
|
||||||
|
overtime_hours: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 420.69, description: 'pay-period`s total expenses ($)' })
|
||||||
|
expenses: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 40, description: 'pay-period total mileages (km)' })
|
||||||
|
mileage: number;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: true,
|
example: true,
|
||||||
description: 'All timesheets are approved for this employee',
|
description: 'Tous les timesheets de la période sont approuvés pour cet employé',
|
||||||
})
|
})
|
||||||
is_approved: boolean;
|
is_approved: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -2,37 +2,37 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { EmployeePeriodOverviewDto } from './overview-employee-period.dto';
|
import { EmployeePeriodOverviewDto } from './overview-employee-period.dto';
|
||||||
|
|
||||||
export class PayPeriodOverviewDto {
|
export class PayPeriodOverviewDto {
|
||||||
@ApiProperty({
|
@ApiProperty({ example: 1, description: 'Period number (1–26)' })
|
||||||
example: 1,
|
|
||||||
description: 'period`s number ( 1-26 )',
|
|
||||||
})
|
|
||||||
period_number: number;
|
period_number: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 2023, description: 'Calendar year of the period' })
|
||||||
|
year: number;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: '2023-12-17',
|
example: '2023-12-17',
|
||||||
type: String,
|
type: String,
|
||||||
format: 'date',
|
format: 'date',
|
||||||
description: 'Period`s starting date',
|
description: "Period start date (YYYY-MM-DD)",
|
||||||
})
|
})
|
||||||
start_date: Date;
|
start_date: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: '2023-12-30',
|
example: '2023-12-30',
|
||||||
type: String,
|
type: String,
|
||||||
format: 'date',
|
format: 'date',
|
||||||
description: 'Period`s ending date',
|
description: "Period end date (YYYY-MM-DD)",
|
||||||
})
|
})
|
||||||
end_date: Date;
|
end_date: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: '2023-12-17 → 2023-12-30',
|
example: '2023-12-17 → 2023-12-30',
|
||||||
description: 'period`s label for showing',
|
description: 'Human-readable label',
|
||||||
})
|
})
|
||||||
label: string;
|
label: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: [EmployeePeriodOverviewDto],
|
type: [EmployeePeriodOverviewDto],
|
||||||
description: 'Detailed view by employee for a chosen period',
|
description: 'Per-employee overview for the period',
|
||||||
})
|
})
|
||||||
employees_overview: EmployeePeriodOverviewDto[];
|
employees_overview: EmployeePeriodOverviewDto[];
|
||||||
}
|
}
|
||||||
21
src/modules/pay-periods/dtos/pay-period.dto.ts
Normal file
21
src/modules/pay-periods/dtos/pay-period.dto.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2023-12-17',
|
||||||
|
type: String, format: 'date' })
|
||||||
|
start_date: String;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2023-12-30',
|
||||||
|
type: String, format: 'date' })
|
||||||
|
end_date: String;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 2023 })
|
||||||
|
year: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2023-12-17 → 2023-12-30' })
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
|
||||||
|
|
||||||
export class PayPeriodEntity {
|
|
||||||
@ApiProperty({
|
|
||||||
example: 1,
|
|
||||||
description: 'numéro cyclique de la période entre 1 et 26'
|
|
||||||
})
|
|
||||||
period_number: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
example: '2023-12-17',
|
|
||||||
type: String,
|
|
||||||
format: 'date'
|
|
||||||
})
|
|
||||||
start_date: Date;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
example: '2023-12-30',
|
|
||||||
type: String,
|
|
||||||
format: 'date'
|
|
||||||
})
|
|
||||||
end_date: Date;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
example: 2023
|
|
||||||
})
|
|
||||||
year: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
example: '2023-12-17 → 2023-12-30'
|
|
||||||
})
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
18
src/modules/pay-periods/mappers/pay-periods.mapper.ts
Normal file
18
src/modules/pay-periods/mappers/pay-periods.mapper.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { PayPeriods } from "@prisma/client";
|
||||||
|
import { PayPeriodDto } from "../dtos/pay-period.dto";
|
||||||
|
|
||||||
|
const toDateString = (d: Date) => d.toISOString().slice(0, 10); // "YYYY-MM-DD"
|
||||||
|
|
||||||
|
export function mapPayPeriodToDto(row: PayPeriods): PayPeriodDto {
|
||||||
|
return {
|
||||||
|
period_number: row.period_number,
|
||||||
|
start_date: toDateString(row.start_date),
|
||||||
|
end_date: toDateString(row.end_date),
|
||||||
|
year: row.year,
|
||||||
|
label: row.label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapMany(rows: PayPeriods[]): PayPeriodDto[] {
|
||||||
|
return rows.map(mapPayPeriodToDto);
|
||||||
|
}
|
||||||
|
|
@ -2,17 +2,29 @@ import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
import { PayPeriodsService } from "./services/pay-periods.service";
|
import { PayPeriodsService } from "./services/pay-periods.service";
|
||||||
import { PayPeriodsController } from "./controllers/pay-periods.controller";
|
import { PayPeriodsController } from "./controllers/pay-periods.controller";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { PayPeriodsOverviewService } from "./services/pay-periods-overview.service";
|
import { PayPeriodsCommandService } from "./services/pay-periods-command.service";
|
||||||
import { PayPeriodsApprovalService } from "./services/pay-periods-approval.service";
|
import { PayPeriodsQueryService } from "./services/pay-periods-query.service";
|
||||||
|
import { TimesheetsModule } from "../timesheets/timesheets.module";
|
||||||
|
import { TimesheetsApprovalService } from "../timesheets/services/timesheets-approval.service";
|
||||||
|
import { ExpensesApprovalService } from "../expenses/services/expenses-approval.service";
|
||||||
|
import { ShiftsApprovalService } from "../shifts/services/shifts-approval.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule, TimesheetsModule],
|
||||||
providers: [
|
providers: [
|
||||||
PayPeriodsService,
|
PayPeriodsService,
|
||||||
PayPeriodsOverviewService,
|
PayPeriodsQueryService,
|
||||||
PayPeriodsApprovalService,
|
PayPeriodsCommandService,
|
||||||
|
TimesheetsApprovalService,
|
||||||
|
ExpensesApprovalService,
|
||||||
|
ShiftsApprovalService,
|
||||||
],
|
],
|
||||||
controllers: [PayPeriodsController],
|
controllers: [PayPeriodsController],
|
||||||
|
exports: [
|
||||||
|
PayPeriodsQueryService,
|
||||||
|
PayPeriodsCommandService,
|
||||||
|
PayPeriodsService,
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class PayperiodsModule {}
|
export class PayperiodsModule {}
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import { NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { TimesheetsApprovalService } from "src/modules/timesheets/services/timesheets-approval.service";
|
import { TimesheetsApprovalService } from "src/modules/timesheets/services/timesheets-approval.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
export class PayPeriodsApprovalService {
|
@Injectable()
|
||||||
|
export class PayPeriodsCommandService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly timesheetsApproval: TimesheetsApprovalService,
|
private readonly timesheetsApproval: TimesheetsApprovalService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async approvaPayperdiod(periodNumber: number): Promise<void> {
|
async approvalPayPeriod(year: number , periodNumber: number): Promise<void> {
|
||||||
const period = await this.prisma.payPeriods.findUnique({
|
const period = await this.prisma.payPeriods.findUnique({
|
||||||
where: { period_number: periodNumber },
|
where: { period_number: periodNumber },
|
||||||
});
|
});
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
|
||||||
import { EmployeePeriodOverviewDto } from "../dtos/overview-employee-period.dto";
|
|
||||||
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
|
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
|
||||||
import { computeHours } from "src/common/utils/date-utils";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PayPeriodsOverviewService {
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
|
||||||
//function to get a full overview of a selected period filtered by employee ID
|
|
||||||
async getOverview(periodNumber: number): Promise<PayPeriodOverviewDto> {
|
|
||||||
//fetch the period
|
|
||||||
const period = await this.prisma.payPeriods.findUnique({
|
|
||||||
where: { period_number: periodNumber },
|
|
||||||
});
|
|
||||||
if(!period) {
|
|
||||||
throw new NotFoundException(`Period #${periodNumber} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
//fetch all included shifts for that period
|
|
||||||
const shifts = await this.prisma.shifts.findMany({
|
|
||||||
where: {
|
|
||||||
date: {
|
|
||||||
gte: period.start_date,
|
|
||||||
lte: period.end_date,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
timesheet: {
|
|
||||||
include: {
|
|
||||||
employee: { include: { user: true }},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//regroup by employee
|
|
||||||
const map = new Map<string, EmployeePeriodOverviewDto>();
|
|
||||||
for (const shift of shifts) {
|
|
||||||
const employee_record = shift.timesheet.employee;
|
|
||||||
const user = employee_record.user;
|
|
||||||
const employee_id = employee_record.user_id;
|
|
||||||
const employee_name = `${user.first_name} ${user.last_name}`;
|
|
||||||
const hours = computeHours(shift.start_time, shift.end_time);
|
|
||||||
|
|
||||||
//check if employee had prior shifts and adds hours of found shift to the total hours
|
|
||||||
if (map.has(employee_id)) {
|
|
||||||
const summary = map.get(employee_id)!;
|
|
||||||
summary.total_hours += hours;
|
|
||||||
//keeps is_approved false as long as a single shift is left un-validated
|
|
||||||
summary.is_approved = summary.is_approved && shift.timesheet.is_approved;
|
|
||||||
} else {
|
|
||||||
//if first shift of an employee is found, it adds a new entry
|
|
||||||
map.set(employee_id, {
|
|
||||||
employee_id: employee_id,
|
|
||||||
employee_name: employee_name,
|
|
||||||
total_hours: hours,
|
|
||||||
is_approved: shift.timesheet.is_approved,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
period_number: period.period_number,
|
|
||||||
start_date: period.start_date,
|
|
||||||
end_date: period.end_date,
|
|
||||||
label: period.label,
|
|
||||||
employees_overview: Array.from(map.values()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
208
src/modules/pay-periods/services/pay-periods-query.service.ts
Normal file
208
src/modules/pay-periods/services/pay-periods-query.service.ts
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
import { ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { computeHours } from "src/common/utils/date-utils";
|
||||||
|
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
|
||||||
|
import { EmployeePeriodOverviewDto } from "../dtos/overview-employee-period.dto";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PayPeriodsQueryService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async getOverview(periodNumber: number): Promise<PayPeriodOverviewDto> {
|
||||||
|
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<PayPeriodOverviewDto> {
|
||||||
|
const period = await this.prisma.payPeriods.findFirst({
|
||||||
|
where: { year, period_number: periodNumber },
|
||||||
|
});
|
||||||
|
if (!period) throw new NotFoundException(`Period ${year}-${periodNumber} not found`);
|
||||||
|
return this.buildOverview(period);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildOverview(
|
||||||
|
period: { start_date: Date; end_date: Date; period_number: number; year: number; label: string; },
|
||||||
|
opts?: { restrictEmployeeIds?: number[]; seedNames?: Map<number, string> },
|
||||||
|
): Promise<PayPeriodOverviewDto> {
|
||||||
|
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 whereEmployee = opts?.restrictEmployeeIds?.length
|
||||||
|
? { employee_id: { in: opts.restrictEmployeeIds } }
|
||||||
|
: {};
|
||||||
|
|
||||||
|
// SHIFTS (filtrés par crew si besoin)
|
||||||
|
const shifts = await this.prisma.shifts.findMany({
|
||||||
|
where: {
|
||||||
|
date: { gte: period.start_date, lte: period.end_date },
|
||||||
|
timesheet: whereEmployee,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
start_time: true,
|
||||||
|
end_time: true,
|
||||||
|
timesheet: {
|
||||||
|
select: {
|
||||||
|
is_approved: true,
|
||||||
|
employee: { select: { id: true, user: { select: { first_name: true, last_name: true } } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bank_code: { select: { categorie: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// EXPENSES (filtrés par crew si besoin)
|
||||||
|
const expenses = await this.prisma.expenses.findMany({
|
||||||
|
where: {
|
||||||
|
date: { gte: period.start_date, lte: period.end_date },
|
||||||
|
timesheet: whereEmployee,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
amount: true,
|
||||||
|
timesheet: {
|
||||||
|
select: {
|
||||||
|
is_approved: true,
|
||||||
|
employee: { select: { id: true, user: { select: { first_name: true, last_name: true } } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bank_code: { select: { categorie: true, modifier: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Agrégation
|
||||||
|
const byEmployee = new Map<number, EmployeePeriodOverviewDto>();
|
||||||
|
|
||||||
|
// seed pour employés sans données
|
||||||
|
if (opts?.seedNames) {
|
||||||
|
for (const [id, name] of opts.seedNames.entries()) {
|
||||||
|
byEmployee.set(id, {
|
||||||
|
employee_id: id,
|
||||||
|
employee_name: name,
|
||||||
|
regular_hours: 0,
|
||||||
|
evening_hours: 0,
|
||||||
|
emergency_hours: 0,
|
||||||
|
overtime_hours: 0,
|
||||||
|
expenses: 0,
|
||||||
|
mileage: 0,
|
||||||
|
is_approved: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensure = (id: number, name: string) => {
|
||||||
|
if (!byEmployee.has(id)) {
|
||||||
|
byEmployee.set(id, {
|
||||||
|
employee_id: id,
|
||||||
|
employee_name: name,
|
||||||
|
regular_hours: 0,
|
||||||
|
evening_hours: 0,
|
||||||
|
emergency_hours: 0,
|
||||||
|
overtime_hours: 0,
|
||||||
|
expenses: 0,
|
||||||
|
mileage: 0,
|
||||||
|
is_approved: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return byEmployee.get(id)!;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const s of shifts) {
|
||||||
|
const e = s.timesheet.employee;
|
||||||
|
const name = `${e.user.first_name} ${e.user.last_name}`.trim();
|
||||||
|
const rec = ensure(e.id, name);
|
||||||
|
|
||||||
|
const hours = computeHours(s.start_time, s.end_time);
|
||||||
|
const cat = (s.bank_code?.categorie || "REGULAR").toUpperCase();
|
||||||
|
switch (cat) {
|
||||||
|
case "EVENING": rec.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;
|
||||||
|
}
|
||||||
|
rec.is_approved = rec.is_approved && s.timesheet.is_approved;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ex of expenses) {
|
||||||
|
const e = ex.timesheet.employee;
|
||||||
|
const name = `${e.user.first_name} ${e.user.last_name}`.trim();
|
||||||
|
const rec = ensure(e.id, name);
|
||||||
|
|
||||||
|
const amount = toMoney(ex.amount);
|
||||||
|
rec.expenses += amount;
|
||||||
|
|
||||||
|
const cat = (ex.bank_code?.categorie || "").toUpperCase();
|
||||||
|
const rate = ex.bank_code?.modifier ?? 0;
|
||||||
|
if (cat === "MILEAGE" && rate > 0) {
|
||||||
|
rec.mileage += amount / rate;
|
||||||
|
}
|
||||||
|
rec.is_approved = rec.is_approved && ex.timesheet.is_approved;
|
||||||
|
}
|
||||||
|
|
||||||
|
const employees_overview = Array.from(byEmployee.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(period.start_date),
|
||||||
|
end_date: toDateString(period.end_date),
|
||||||
|
label: period.label,
|
||||||
|
employees_overview,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async getCrewOverview(year: number, periodNumber: number, userId: string, includeSubtree: boolean): Promise<PayPeriodOverviewDto> {
|
||||||
|
// 1) Trouver la période
|
||||||
|
const period = await this.prisma.payPeriods.findFirst({ where: { year, period_number: periodNumber } });
|
||||||
|
if (!period) throw new NotFoundException(`Pay period ${year}-${periodNumber} not found`);
|
||||||
|
|
||||||
|
// 2) Résoudre l'employé superviseur depuis l'utilisateur courant (Users.id -> Employees)
|
||||||
|
const supervisor = await this.prisma.employees.findUnique({
|
||||||
|
where: { user_id: userId },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (!supervisor) throw new ForbiddenException('No employee record linked to current user');
|
||||||
|
|
||||||
|
// 3) Récupérer la liste des employés du crew (directs ou sous-arbo complète)
|
||||||
|
const crew = await this.resolveCrew(supervisor.id, includeSubtree); // [{ id, first_name, last_name }]
|
||||||
|
const crewIds = crew.map(c => c.id);
|
||||||
|
// seed names map for employés sans données
|
||||||
|
const seedNames = new Map<number, string>(crew.map(c => [c.id, `${c.first_name} ${c.last_name}`.trim()]));
|
||||||
|
|
||||||
|
// 4) Construire l’overview filtré par ce crew
|
||||||
|
return this.buildOverview(period, { restrictEmployeeIds: crewIds, seedNames });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveCrew(supervisorId: number, includeSubtree: boolean): Promise<Array<{ id: number; first_name: string; last_name: string }>> {
|
||||||
|
const result: Array<{ id: number; first_name: string; last_name: string }> = [];
|
||||||
|
|
||||||
|
// niveau 1 (directs)
|
||||||
|
let frontier = await this.prisma.employees.findMany({
|
||||||
|
where: { supervisor_id: supervisorId },
|
||||||
|
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 })));
|
||||||
|
|
||||||
|
if (!includeSubtree) return result;
|
||||||
|
|
||||||
|
// BFS pour les niveaux suivants
|
||||||
|
while (frontier.length) {
|
||||||
|
const parentIds = frontier.map(e => e.id);
|
||||||
|
const next = await this.prisma.employees.findMany({
|
||||||
|
where: { supervisor_id: { in: parentIds } },
|
||||||
|
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 })));
|
||||||
|
frontier = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,49 +1,47 @@
|
||||||
import { Injectable, NotFoundException, Param, ParseIntPipe, Patch } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { PayPeriods } from "@prisma/client";
|
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { PayPeriodsApprovalService } from "./pay-periods-approval.service";
|
import { PayPeriodsCommandService } from "./pay-periods-command.service";
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { mapMany, mapPayPeriodToDto } from "../mappers/pay-periods.mapper";
|
||||||
import { Roles as RoleEnum } from '.prisma/client';
|
import { PayPeriodDto } from "../dtos/pay-period.dto";
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PayPeriodsService {
|
export class PayPeriodsService {
|
||||||
constructor(private readonly prisma: PrismaService,
|
constructor(private readonly prisma: PrismaService,
|
||||||
private readonly payperiodsApprovalService: PayPeriodsApprovalService
|
private readonly payperiodsApprovalService: PayPeriodsCommandService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findAll(): Promise<PayPeriods[]> {
|
async findAll(): Promise<PayPeriodDto[]> {
|
||||||
return this.prisma.payPeriods.findMany({
|
const rows = await this.prisma.payPeriods.findMany({
|
||||||
orderBy: { period_number: 'asc'},
|
orderBy: [{ year: 'desc'}, { period_number: "asc"}],
|
||||||
});
|
});
|
||||||
|
return mapMany(rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(periodNumber: number): Promise<PayPeriods | null> {
|
async findOne(periodNumber: number): Promise<PayPeriodDto> {
|
||||||
return this.prisma.payPeriods.findUnique({
|
const row = await this.prisma.payPeriods.findFirst({
|
||||||
where: { period_number: periodNumber },
|
where: { period_number: periodNumber },
|
||||||
|
orderBy: { year: "desc" },
|
||||||
});
|
});
|
||||||
|
if (!row) throw new NotFoundException(`Pay period #${periodNumber} not found`);
|
||||||
|
return mapPayPeriodToDto(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneByYearPeriod(year: number, periodNumber: number): Promise<PayPeriodDto> {
|
||||||
|
const row = await this.prisma.payPeriods.findFirst({
|
||||||
|
where: { year, period_number: periodNumber },
|
||||||
|
});
|
||||||
|
if (!row) throw new NotFoundException(`Pay period ${year}-${periodNumber} not found`);
|
||||||
|
return mapPayPeriodToDto(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
//function to cherry pick a Date to find a period
|
//function to cherry pick a Date to find a period
|
||||||
async findByDate(date:string): Promise<PayPeriods> {
|
async findByDate(date: string): Promise<PayPeriodDto> {
|
||||||
const dt = new Date(date);
|
const dt = new Date(date);
|
||||||
const period = await this.prisma.payPeriods.findFirst({
|
const row = await this.prisma.payPeriods.findFirst({
|
||||||
where: {
|
where: { start_date: { lte: dt }, end_date: { gte: dt } },
|
||||||
start_date: { lte: dt },
|
|
||||||
end_date: { gte: dt },
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if(!period) {
|
if (!row) throw new NotFoundException(`No period found for this date: ${date}`);
|
||||||
throw new NotFoundException(`No period found for this date: ${date}`);
|
return mapPayPeriodToDto(row);
|
||||||
}
|
}
|
||||||
return period;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch(':periodNumber/approval')
|
|
||||||
@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
|
||||||
async approve(@Param('periodNumber', ParseIntPipe) periodNumber: number): Promise<{message:string}> {
|
|
||||||
await this.payperiodsApprovalService.approvaPayperdiod(periodNumber);
|
|
||||||
return {message: `Pay-period #${periodNumber} approved`};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -5,11 +5,12 @@ import { BusinessLogicsModule } from 'src/modules/business-logics/business-logic
|
||||||
import { ShiftsOverviewController } from './controllers/shifts-overview.controller';
|
import { ShiftsOverviewController } from './controllers/shifts-overview.controller';
|
||||||
import { ShiftsOverviewService } from './services/shifts-overview.service';
|
import { ShiftsOverviewService } from './services/shifts-overview.service';
|
||||||
import { ShiftsApprovalService } from './services/shifts-approval.service';
|
import { ShiftsApprovalService } from './services/shifts-approval.service';
|
||||||
|
import { NotificationsModule } from '../notifications/notifications.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [BusinessLogicsModule],
|
imports: [BusinessLogicsModule, NotificationsModule],
|
||||||
controllers: [ShiftsController, ShiftsOverviewController],
|
controllers: [ShiftsController, ShiftsOverviewController],
|
||||||
providers: [ShiftsService, ShiftsOverviewService, ShiftsApprovalService],
|
providers: [ShiftsService, ShiftsOverviewService, ShiftsApprovalService],
|
||||||
exports: [ShiftsService, ShiftsOverviewService],
|
exports: [ShiftsService, ShiftsOverviewService, ShiftsApprovalService],
|
||||||
})
|
})
|
||||||
export class ShiftsModule {}
|
export class ShiftsModule {}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user