From 2847c4cdf1329f46376000c45ab7a3e58c9ef1bb Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 29 Jul 2025 10:43:18 -0400 Subject: [PATCH 01/21] feat(cron): added schedule dependencie and base setup for CRON jobs. --- prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 11144cc..8ce585d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -43,7 +43,7 @@ model Employees { supervisor Employees? @relation("EmployeeSupervisor", fields: [supervisor_id], references: [id]) supervisor_id Int? - managed_employees Employees[] @relation("EmployeeSupervisor") + managed_employees Employees[] @relation("EmployeeSupervisor") //changer pour crew à la prochaine MaJ archive EmployeesArchive[] @relation("EmployeeToArchive") timesheet Timesheets[] @relation("TimesheetEmployee") From a7c8b6201297fe9f1a1e61874694c6adf5b1c34e Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 29 Jul 2025 10:43:58 -0400 Subject: [PATCH 02/21] feat(cron): added schedule dependencie and base setup for CRON jobs. --- package-lock.json | 44 +++++++++++++++++++++++++++++++++++++++++--- package.json | 1 + src/app.module.ts | 2 ++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3510c72..1fe56c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@nestjs/jwt": "^11.0.0", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", + "@nestjs/schedule": "^6.0.0", "@nestjs/swagger": "^11.2.0", "@prisma/client": "^6.11.1", "class-transformer": "^0.5.1", @@ -849,9 +850,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "dependencies": { "@eslint/core": "^0.15.1", @@ -2460,6 +2461,18 @@ "@nestjs/core": "^11.0.0" } }, + "node_modules/@nestjs/schedule": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.0.tgz", + "integrity": "sha512-aQySMw6tw2nhitELXd3EiRacQRgzUKD9mFcUZVOJ7jPLqIBvXOyvRWLsK9SdurGA+jjziAlMef7iB5ZEFFoQpw==", + "dependencies": { + "cron": "4.3.0" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/schematics": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.5.tgz", @@ -3349,6 +3362,11 @@ "@types/node": "*" } }, + "node_modules/@types/luxon": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz", + "integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -5577,6 +5595,18 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cron": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.0.tgz", + "integrity": "sha512-ciiYNLfSlF9MrDqnbMdRWFiA6oizSF7kA1osPP9lRzNu0Uu+AWog1UKy7SkckiDY2irrNjeO6qLyKnXC8oxmrw==", + "dependencies": { + "@types/luxon": "~3.6.0", + "luxon": "~3.6.0" + }, + "engines": { + "node": ">=18.x" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8401,6 +8431,14 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", diff --git a/package.json b/package.json index 838060b..0741841 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@nestjs/jwt": "^11.0.0", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", + "@nestjs/schedule": "^6.0.0", "@nestjs/swagger": "^11.2.0", "@prisma/client": "^6.11.1", "class-transformer": "^0.5.1", diff --git a/src/app.module.ts b/src/app.module.ts index a870b3e..34a59a8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -16,9 +16,11 @@ import { AuthenticationModule } from './modules/authentication/auth.module'; import { ExpensesModule } from './modules/expenses/expenses.module'; import { ExpenseCodesModule } from './modules/expense-codes/expense-codes.module'; import { PayperiodsModule } from './modules/pay-periods/pay-periods.module'; +import { ScheduleModule } from '@nestjs/schedule'; @Module({ imports: [ + ScheduleModule.forRoot(), PrismaModule, HealthModule, UsersModule, From 5274bf41c170180706b294fd7d675b808cd0dccb Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 29 Jul 2025 14:54:19 -0400 Subject: [PATCH 03/21] feat(archival): setup services and modules for archivation options via Cron job. small fixes to schema.prisma --- docs/swagger/swagger-spec.json | 2525 ++++++++--------- .../migration.sql | 2 + prisma/schema.prisma | 4 +- src/app.module.ts | 2 + src/main.ts | 5 + src/modules/archival/archival.module.ts | 20 + .../archival/services/archival.service.ts | 40 + .../controllers/employees.controller.ts | 14 +- .../employees/dtos/update-employee.dto.ts | 6 +- .../employees/services/employees.service.ts | 82 + src/modules/expenses/expenses.module.ts | 3 +- .../expenses/services/expenses.service.ts | 48 +- .../leave-requests/leave-requests.module.ts | 1 + .../services/leave-request.service.ts | 32 + src/modules/shifts/services/shifts.service.ts | 44 + src/modules/shifts/shifts.module.ts | 3 +- .../timesheets/services/timesheets.service.ts | 40 + src/modules/timesheets/timesheets.module.ts | 3 +- 18 files changed, 1598 insertions(+), 1276 deletions(-) create mode 100644 prisma/migrations/20250729165951_fix_employees_crew_and_default_now_in_shifts_archive/migration.sql create mode 100644 src/modules/archival/archival.module.ts create mode 100644 src/modules/archival/services/archival.service.ts diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 73f1761..2d6d040 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -29,6 +29,782 @@ ] } }, + "/timesheets": { + "post": { + "operationId": "TimesheetsController_create", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTimesheetDto" + } + } + } + }, + "responses": { + "201": { + "description": "Timesheet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimesheetEntity" + } + } + } + }, + "400": { + "description": "Incomplete task or invalid data" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Create timesheet", + "tags": [ + "Timesheets" + ] + }, + "get": { + "operationId": "TimesheetsController_findAll", + "parameters": [], + "responses": { + "201": { + "description": "List of timesheet found", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimesheetEntity" + } + } + } + } + }, + "400": { + "description": "List of timesheets not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find all timesheets", + "tags": [ + "Timesheets" + ] + } + }, + "/timesheets/{id}": { + "get": { + "operationId": "TimesheetsController_findOne", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "Timesheet found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimesheetEntity" + } + } + } + }, + "400": { + "description": "Timesheet not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find timesheet", + "tags": [ + "Timesheets" + ] + }, + "patch": { + "operationId": "TimesheetsController_update", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTimesheetDto" + } + } + } + }, + "responses": { + "201": { + "description": "Timesheet updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimesheetEntity" + } + } + } + }, + "400": { + "description": "Timesheet not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Update timesheet", + "tags": [ + "Timesheets" + ] + }, + "delete": { + "operationId": "TimesheetsController_remove", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "Timesheet deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimesheetEntity" + } + } + } + }, + "400": { + "description": "Timesheet not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Delete timesheet", + "tags": [ + "Timesheets" + ] + } + }, + "/Expenses": { + "post": { + "operationId": "ExpensesController_create", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateExpenseDto" + } + } + } + }, + "responses": { + "201": { + "description": "Expense created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExpenseEntity" + } + } + } + }, + "400": { + "description": "Incomplete task or invalid data" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Create expense", + "tags": [ + "Expenses" + ] + }, + "get": { + "operationId": "ExpensesController_findAll", + "parameters": [], + "responses": { + "201": { + "description": "List of expenses found", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExpenseEntity" + } + } + } + } + }, + "400": { + "description": "List of expenses not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find all expenses", + "tags": [ + "Expenses" + ] + } + }, + "/Expenses/{id}": { + "get": { + "operationId": "ExpensesController_findOne", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "Expense found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExpenseEntity" + } + } + } + }, + "400": { + "description": "Expense not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find expense", + "tags": [ + "Expenses" + ] + }, + "patch": { + "operationId": "ExpensesController_update", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateExpenseDto" + } + } + } + }, + "responses": { + "201": { + "description": "Expense updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExpenseEntity" + } + } + } + }, + "400": { + "description": "Expense not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Expense shift", + "tags": [ + "Expenses" + ] + }, + "delete": { + "operationId": "ExpensesController_remove", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "Expense deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExpenseEntity" + } + } + } + }, + "400": { + "description": "Expense not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Delete expense", + "tags": [ + "Expenses" + ] + } + }, + "/shifts": { + "post": { + "operationId": "ShiftsController_create", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateShiftDto" + } + } + } + }, + "responses": { + "201": { + "description": "Shift created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShiftEntity" + } + } + } + }, + "400": { + "description": "Incomplete task or invalid data" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Create shift", + "tags": [ + "Shifts" + ] + }, + "get": { + "operationId": "ShiftsController_findAll", + "parameters": [], + "responses": { + "201": { + "description": "List of shifts found", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ShiftEntity" + } + } + } + } + }, + "400": { + "description": "List of shifts not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find all shifts", + "tags": [ + "Shifts" + ] + } + }, + "/shifts/{id}": { + "get": { + "operationId": "ShiftsController_findOne", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "Shift found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShiftEntity" + } + } + } + }, + "400": { + "description": "Shift not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find shift", + "tags": [ + "Shifts" + ] + }, + "patch": { + "operationId": "ShiftsController_update", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateShiftsDto" + } + } + } + }, + "responses": { + "201": { + "description": "Shift updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShiftEntity" + } + } + } + }, + "400": { + "description": "Shift not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Update shift", + "tags": [ + "Shifts" + ] + }, + "delete": { + "operationId": "ShiftsController_remove", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "Shift deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShiftEntity" + } + } + } + }, + "400": { + "description": "Shift not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Delete shift", + "tags": [ + "Shifts" + ] + } + }, + "/leave-requests": { + "post": { + "operationId": "LeaveRequestController_create", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateLeaveRequestsDto" + } + } + } + }, + "responses": { + "201": { + "description": "Leave request created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeaveRequestEntity" + } + } + } + }, + "400": { + "description": "Incomplete task or invalid data" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Create leave request", + "tags": [ + "Leave Requests" + ] + }, + "get": { + "operationId": "LeaveRequestController_findAll", + "parameters": [], + "responses": { + "201": { + "description": "List of Leave requests found", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LeaveRequestEntity" + } + } + } + } + }, + "400": { + "description": "List of leave request not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find all leave request", + "tags": [ + "Leave Requests" + ] + } + }, + "/leave-requests/{id}": { + "get": { + "operationId": "LeaveRequestController_findOne", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "Leave request found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeaveRequestEntity" + } + } + } + }, + "400": { + "description": "Leave request not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find leave request", + "tags": [ + "Leave Requests" + ] + }, + "patch": { + "operationId": "LeaveRequestController_update", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateLeaveRequestsDto" + } + } + } + }, + "responses": { + "201": { + "description": "Leave request updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeaveRequestEntity" + } + } + } + }, + "400": { + "description": "Leave request not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Update leave request", + "tags": [ + "Leave Requests" + ] + }, + "delete": { + "operationId": "LeaveRequestController_remove", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "Leave request deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeaveRequestEntity" + } + } + } + }, + "400": { + "description": "Leave request not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Delete leave request", + "tags": [ + "Leave Requests" + ] + } + }, "/oauth-access-tokens": { "post": { "operationId": "OauthAccessTokensController_create", @@ -527,14 +1303,14 @@ ] }, "patch": { - "operationId": "EmployeesController_update", + "operationId": "EmployeesController_updateOrArchiveOrRestore", "parameters": [ { "name": "id", "required": true, "in": "path", "schema": { - "type": "number" + "type": "string" } } ], @@ -549,18 +1325,8 @@ } }, "responses": { - "201": { - "description": "Employee updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmployeeEntity" - } - } - } - }, - "400": { - "description": "Employee not found" + "200": { + "description": "" } }, "security": [ @@ -568,7 +1334,6 @@ "access-token": [] } ], - "summary": "Update employee", "tags": [ "Employees" ] @@ -611,394 +1376,6 @@ ] } }, - "/leave-requests": { - "post": { - "operationId": "LeaveRequestController_create", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateLeaveRequestsDto" - } - } - } - }, - "responses": { - "201": { - "description": "Leave request created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LeaveRequestEntity" - } - } - } - }, - "400": { - "description": "Incomplete task or invalid data" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Create leave request", - "tags": [ - "Leave Requests" - ] - }, - "get": { - "operationId": "LeaveRequestController_findAll", - "parameters": [], - "responses": { - "201": { - "description": "List of Leave requests found", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LeaveRequestEntity" - } - } - } - } - }, - "400": { - "description": "List of leave request not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find all leave request", - "tags": [ - "Leave Requests" - ] - } - }, - "/leave-requests/{id}": { - "get": { - "operationId": "LeaveRequestController_findOne", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "201": { - "description": "Leave request found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LeaveRequestEntity" - } - } - } - }, - "400": { - "description": "Leave request not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find leave request", - "tags": [ - "Leave Requests" - ] - }, - "patch": { - "operationId": "LeaveRequestController_update", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateLeaveRequestsDto" - } - } - } - }, - "responses": { - "201": { - "description": "Leave request updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LeaveRequestEntity" - } - } - } - }, - "400": { - "description": "Leave request not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Update leave request", - "tags": [ - "Leave Requests" - ] - }, - "delete": { - "operationId": "LeaveRequestController_remove", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "201": { - "description": "Leave request deleted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LeaveRequestEntity" - } - } - } - }, - "400": { - "description": "Leave request not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Delete leave request", - "tags": [ - "Leave Requests" - ] - } - }, - "/Expenses": { - "post": { - "operationId": "ExpensesController_create", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateExpenseDto" - } - } - } - }, - "responses": { - "201": { - "description": "Expense created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExpenseEntity" - } - } - } - }, - "400": { - "description": "Incomplete task or invalid data" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Create expense", - "tags": [ - "Expenses" - ] - }, - "get": { - "operationId": "ExpensesController_findAll", - "parameters": [], - "responses": { - "201": { - "description": "List of expenses found", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ExpenseEntity" - } - } - } - } - }, - "400": { - "description": "List of expenses not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find all expenses", - "tags": [ - "Expenses" - ] - } - }, - "/Expenses/{id}": { - "get": { - "operationId": "ExpensesController_findOne", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "201": { - "description": "Expense found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExpenseEntity" - } - } - } - }, - "400": { - "description": "Expense not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find expense", - "tags": [ - "Expenses" - ] - }, - "patch": { - "operationId": "ExpensesController_update", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateExpenseDto" - } - } - } - }, - "responses": { - "201": { - "description": "Expense updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExpenseEntity" - } - } - } - }, - "400": { - "description": "Expense not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Expense shift", - "tags": [ - "Expenses" - ] - }, - "delete": { - "operationId": "ExpensesController_remove", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "201": { - "description": "Expense deleted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExpenseEntity" - } - } - } - }, - "400": { - "description": "Expense not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Delete expense", - "tags": [ - "Expenses" - ] - } - }, "/expense-codes": { "post": { "operationId": "ExpenseCodesController_create", @@ -1387,394 +1764,6 @@ ] } }, - "/shifts": { - "post": { - "operationId": "ShiftsController_create", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateShiftDto" - } - } - } - }, - "responses": { - "201": { - "description": "Shift created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ShiftEntity" - } - } - } - }, - "400": { - "description": "Incomplete task or invalid data" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Create shift", - "tags": [ - "Shifts" - ] - }, - "get": { - "operationId": "ShiftsController_findAll", - "parameters": [], - "responses": { - "201": { - "description": "List of shifts found", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ShiftEntity" - } - } - } - } - }, - "400": { - "description": "List of shifts not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find all shifts", - "tags": [ - "Shifts" - ] - } - }, - "/shifts/{id}": { - "get": { - "operationId": "ShiftsController_findOne", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "201": { - "description": "Shift found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ShiftEntity" - } - } - } - }, - "400": { - "description": "Shift not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find shift", - "tags": [ - "Shifts" - ] - }, - "patch": { - "operationId": "ShiftsController_update", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateShiftsDto" - } - } - } - }, - "responses": { - "201": { - "description": "Shift updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ShiftEntity" - } - } - } - }, - "400": { - "description": "Shift not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Update shift", - "tags": [ - "Shifts" - ] - }, - "delete": { - "operationId": "ShiftsController_remove", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "201": { - "description": "Shift deleted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ShiftEntity" - } - } - } - }, - "400": { - "description": "Shift not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Delete shift", - "tags": [ - "Shifts" - ] - } - }, - "/timesheets": { - "post": { - "operationId": "TimesheetsController_create", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTimesheetDto" - } - } - } - }, - "responses": { - "201": { - "description": "Timesheet created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TimesheetEntity" - } - } - } - }, - "400": { - "description": "Incomplete task or invalid data" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Create timesheet", - "tags": [ - "Timesheets" - ] - }, - "get": { - "operationId": "TimesheetsController_findAll", - "parameters": [], - "responses": { - "201": { - "description": "List of timesheet found", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TimesheetEntity" - } - } - } - } - }, - "400": { - "description": "List of timesheets not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find all timesheets", - "tags": [ - "Timesheets" - ] - } - }, - "/timesheets/{id}": { - "get": { - "operationId": "TimesheetsController_findOne", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "201": { - "description": "Timesheet found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TimesheetEntity" - } - } - } - }, - "400": { - "description": "Timesheet not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find timesheet", - "tags": [ - "Timesheets" - ] - }, - "patch": { - "operationId": "TimesheetsController_update", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateTimesheetDto" - } - } - } - }, - "responses": { - "201": { - "description": "Timesheet updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TimesheetEntity" - } - } - } - }, - "400": { - "description": "Timesheet not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Update timesheet", - "tags": [ - "Timesheets" - ] - }, - "delete": { - "operationId": "TimesheetsController_remove", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "201": { - "description": "Timesheet deleted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TimesheetEntity" - } - } - } - }, - "400": { - "description": "Timesheet not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Delete timesheet", - "tags": [ - "Timesheets" - ] - } - }, "/auth/login": { "get": { "operationId": "AuthController_login", @@ -1991,6 +1980,483 @@ } }, "schemas": { + "CreateTimesheetDto": { + "type": "object", + "properties": { + "employee_id": { + "type": "number", + "example": "426433", + "description": "employee`s ID number of linked timsheet" + }, + "is_approved": { + "type": "boolean", + "example": "True or False or Pending or Denied or Cancelled or Escalated", + "description": "Timesheet`s approval status" + } + }, + "required": [ + "employee_id", + "is_approved" + ] + }, + "TimesheetEntity": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1, + "description": "timesheet`s unique ID (auto-generated)" + }, + "employee_id": { + "type": "number", + "example": 426433, + "description": "employee`s ID number of linked timsheet" + }, + "is_approved": { + "type": "boolean", + "example": true, + "description": "Timesheet`s approval status" + } + }, + "required": [ + "id", + "employee_id", + "is_approved" + ] + }, + "UpdateTimesheetDto": { + "type": "object", + "properties": { + "employee_id": { + "type": "number", + "example": "426433", + "description": "employee`s ID number of linked timsheet" + }, + "is_approved": { + "type": "boolean", + "example": "True or False or Pending or Denied or Cancelled or Escalated", + "description": "Timesheet`s approval status" + } + } + }, + "CreateExpenseDto": { + "type": "object", + "properties": { + "timesheet_id": { + "type": "number", + "example": "Th3F3110w5h1pX2024", + "description": "ID number for a set timesheet" + }, + "expense_code_id": { + "type": "number", + "example": "0n3R1n962Ru13xX", + "description": "ID number of an expense code (link with shift-codes)" + }, + "date": { + "format": "date-time", + "type": "string", + "example": "20/10/3018", + "description": "Date where the expense was made" + }, + "amount": { + "type": "number", + "example": "280 000 000,00", + "description": "Amount of the expense" + }, + "description": { + "type": "string", + "example": "Spent for mileage between A and B", + "description": "explain`s why the expense was made" + }, + "is_approved": { + "type": "boolean", + "example": "True or False or Pending or Denied or Cancelled or Escalated", + "description": "Expense`s approval status" + }, + "supervisor_comment": { + "type": "string", + "example": "Asked X to go there as an emergency response", + "description": "Supervisro`s justification for the spending of an employee" + } + }, + "required": [ + "timesheet_id", + "expense_code_id", + "date", + "amount", + "description", + "is_approved", + "supervisor_comment" + ] + }, + "ExpenseEntity": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1, + "description": "Unique ID of the expense (auto-generated)" + }, + "timesheet_id": { + "type": "number", + "example": 101, + "description": "ID number for a set timesheet" + }, + "expense": { + "type": "number", + "example": 7, + "description": "ID number of an expense code (link with expense-codes)" + }, + "date": { + "format": "date-time", + "type": "string", + "example": "3018-10-20T00:00:00.000Z", + "description": "Date where the expense was made" + }, + "is_approuved": { + "type": "boolean", + "example": "DENIED, APPROUVED, PENDING, etc...", + "description": "validation status" + }, + "description": { + "type": "string", + "example": "Spent for mileage between A and B", + "description": "explain`s why the expense was made" + }, + "supervisor_comment": { + "type": "string", + "example": "Asked X to go there as an emergency response", + "description": "Supervisro`s justification for the spending of an employee" + } + }, + "required": [ + "id", + "timesheet_id", + "expense", + "date", + "is_approuved", + "description", + "supervisor_comment" + ] + }, + "UpdateExpenseDto": { + "type": "object", + "properties": { + "timesheet_id": { + "type": "number", + "example": "Th3F3110w5h1pX2024", + "description": "ID number for a set timesheet" + }, + "expense_code_id": { + "type": "number", + "example": "0n3R1n962Ru13xX", + "description": "ID number of an expense code (link with shift-codes)" + }, + "date": { + "format": "date-time", + "type": "string", + "example": "20/10/3018", + "description": "Date where the expense was made" + }, + "amount": { + "type": "number", + "example": "280 000 000,00", + "description": "Amount of the expense" + }, + "description": { + "type": "string", + "example": "Spent for mileage between A and B", + "description": "explain`s why the expense was made" + }, + "is_approved": { + "type": "boolean", + "example": "True or False or Pending or Denied or Cancelled or Escalated", + "description": "Expense`s approval status" + }, + "supervisor_comment": { + "type": "string", + "example": "Asked X to go there as an emergency response", + "description": "Supervisro`s justification for the spending of an employee" + } + } + }, + "CreateShiftDto": { + "type": "object", + "properties": { + "timesheet_id": { + "type": "number", + "example": "Th3F3110w5h1pX2024", + "description": "ID number for a set timesheet" + }, + "shift_code_id": { + "type": "number", + "example": "0n3R1n962Ru13xX", + "description": "ID number of a shift code (link with shift-codes)" + }, + "date": { + "format": "date-time", + "type": "string", + "example": "20/10/3018", + "description": "Date where the shift takes place" + }, + "start_time": { + "format": "date-time", + "type": "string", + "example": "08:00", + "description": "Start time of the said shift" + }, + "end_time": { + "format": "date-time", + "type": "string", + "example": "17:00", + "description": "End time of the said shift" + }, + "description": { + "type": "string", + "example": "Called for an emergency at X` place", + "description": "justify the purpose of the shift" + } + }, + "required": [ + "timesheet_id", + "shift_code_id", + "date", + "start_time", + "end_time", + "description" + ] + }, + "ShiftEntity": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1, + "description": "Unique ID of the shift (auto-generated)" + }, + "timesheet_id": { + "type": "number", + "example": 101, + "description": "ID number for a set timesheet" + }, + "shift_code_id": { + "type": "number", + "example": 7, + "description": "ID number of a shift code (link with shift-codes)" + }, + "date": { + "format": "date-time", + "type": "string", + "example": "3018-10-20T00:00:00.000Z", + "description": "Date where the shift takes place" + }, + "start_time": { + "format": "date-time", + "type": "string", + "example": "3018-10-20T08:00:00.000Z", + "description": "Start time of the said shift" + }, + "end_time": { + "format": "date-time", + "type": "string", + "example": "3018-10-20T17:00:00.000Z", + "description": "End time of the said shift" + } + }, + "required": [ + "id", + "timesheet_id", + "shift_code_id", + "date", + "start_time", + "end_time" + ] + }, + "UpdateShiftsDto": { + "type": "object", + "properties": { + "timesheet_id": { + "type": "number", + "example": "Th3F3110w5h1pX2024", + "description": "ID number for a set timesheet" + }, + "shift_code_id": { + "type": "number", + "example": "0n3R1n962Ru13xX", + "description": "ID number of a shift code (link with shift-codes)" + }, + "date": { + "format": "date-time", + "type": "string", + "example": "20/10/3018", + "description": "Date where the shift takes place" + }, + "start_time": { + "format": "date-time", + "type": "string", + "example": "08:00", + "description": "Start time of the said shift" + }, + "end_time": { + "format": "date-time", + "type": "string", + "example": "17:00", + "description": "End time of the said shift" + }, + "description": { + "type": "string", + "example": "Called for an emergency at X` place", + "description": "justify the purpose of the shift" + } + } + }, + "CreateLeaveRequestsDto": { + "type": "object", + "properties": { + "employee_id": { + "type": "number", + "example": "4655867", + "description": "Employee`s id" + }, + "leave_type": { + "type": "string", + "example": "Sick or Vacation or Unpaid or Bereavement or Parental or Legal", + "description": "type of leave request for an accounting perception" + }, + "start_date_time": { + "format": "date-time", + "type": "string", + "example": "22/06/2463", + "description": "Leave request`s start date" + }, + "end_date_time": { + "format": "date-time", + "type": "string", + "example": "25/03/3019", + "description": "Leave request`s end date" + }, + "comment": { + "type": "string", + "example": "My precious", + "description": "Leave request`s comment" + }, + "approval_status": { + "type": "string", + "example": "True or False or Pending or Denied or Cancelled or Escalated", + "description": "Leave request`s approval status" + } + }, + "required": [ + "employee_id", + "leave_type", + "start_date_time", + "end_date_time", + "comment", + "approval_status" + ] + }, + "LeaveRequestEntity": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1, + "description": "Leave request`s unique id(auto-incremented)" + }, + "employee_id": { + "type": "number", + "example": 42, + "description": "ID of concerned employee" + }, + "leave_type": { + "type": "string", + "example": "SICK", + "enum": [ + "SICK", + "VACATION", + "UNPAID", + "BEREAVEMENT", + "PARENTAL", + "LEGAL" + ], + "description": "type of leave request for an accounting perception" + }, + "start_date_time": { + "format": "date-time", + "type": "string", + "example": "22/06/2463", + "description": "Leave request`s start date" + }, + "end_date_time": { + "format": "date-time", + "type": "string", + "example": "25/03/3019", + "description": "Leave request`s end date (optionnal)" + }, + "comment": { + "type": "string", + "example": "My precious", + "description": "Leave request employee`s comment" + }, + "approval_status": { + "type": "string", + "example": "PENDING", + "enum": [ + "PENDING", + "APPROVED", + "DENIED", + "CANCELLED", + "ESCALATED" + ], + "description": "Leave request`s approval status" + } + }, + "required": [ + "id", + "employee_id", + "leave_type", + "start_date_time", + "comment", + "approval_status" + ] + }, + "UpdateLeaveRequestsDto": { + "type": "object", + "properties": { + "employee_id": { + "type": "number", + "example": "4655867", + "description": "Employee`s id" + }, + "leave_type": { + "type": "string", + "example": "Sick or Vacation or Unpaid or Bereavement or Parental or Legal", + "description": "type of leave request for an accounting perception" + }, + "start_date_time": { + "format": "date-time", + "type": "string", + "example": "22/06/2463", + "description": "Leave request`s start date" + }, + "end_date_time": { + "format": "date-time", + "type": "string", + "example": "25/03/3019", + "description": "Leave request`s end date" + }, + "comment": { + "type": "string", + "example": "My precious", + "description": "Leave request`s comment" + }, + "approval_status": { + "type": "string", + "example": "True or False or Pending or Denied or Cancelled or Escalated", + "description": "Leave request`s approval status" + } + } + }, "CreateOauthAccessTokenDto": { "type": "object", "properties": { @@ -2442,294 +2908,6 @@ } } }, - "CreateLeaveRequestsDto": { - "type": "object", - "properties": { - "employee_id": { - "type": "number", - "example": "4655867", - "description": "Employee`s id" - }, - "leave_type": { - "type": "string", - "example": "Sick or Vacation or Unpaid or Bereavement or Parental or Legal", - "description": "type of leave request for an accounting perception" - }, - "start_date_time": { - "format": "date-time", - "type": "string", - "example": "22/06/2463", - "description": "Leave request`s start date" - }, - "end_date_time": { - "format": "date-time", - "type": "string", - "example": "25/03/3019", - "description": "Leave request`s end date" - }, - "comment": { - "type": "string", - "example": "My precious", - "description": "Leave request`s comment" - }, - "approval_status": { - "type": "string", - "example": "True or False or Pending or Denied or Cancelled or Escalated", - "description": "Leave request`s approval status" - } - }, - "required": [ - "employee_id", - "leave_type", - "start_date_time", - "end_date_time", - "comment", - "approval_status" - ] - }, - "LeaveRequestEntity": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1, - "description": "Leave request`s unique id(auto-incremented)" - }, - "employee_id": { - "type": "number", - "example": 42, - "description": "ID of concerned employee" - }, - "leave_type": { - "type": "string", - "example": "SICK", - "enum": [ - "SICK", - "VACATION", - "UNPAID", - "BEREAVEMENT", - "PARENTAL", - "LEGAL" - ], - "description": "type of leave request for an accounting perception" - }, - "start_date_time": { - "format": "date-time", - "type": "string", - "example": "22/06/2463", - "description": "Leave request`s start date" - }, - "end_date_time": { - "format": "date-time", - "type": "string", - "example": "25/03/3019", - "description": "Leave request`s end date (optionnal)" - }, - "comment": { - "type": "string", - "example": "My precious", - "description": "Leave request employee`s comment" - }, - "approval_status": { - "type": "string", - "example": "PENDING", - "enum": [ - "PENDING", - "APPROVED", - "DENIED", - "CANCELLED", - "ESCALATED" - ], - "description": "Leave request`s approval status" - } - }, - "required": [ - "id", - "employee_id", - "leave_type", - "start_date_time", - "comment", - "approval_status" - ] - }, - "UpdateLeaveRequestsDto": { - "type": "object", - "properties": { - "employee_id": { - "type": "number", - "example": "4655867", - "description": "Employee`s id" - }, - "leave_type": { - "type": "string", - "example": "Sick or Vacation or Unpaid or Bereavement or Parental or Legal", - "description": "type of leave request for an accounting perception" - }, - "start_date_time": { - "format": "date-time", - "type": "string", - "example": "22/06/2463", - "description": "Leave request`s start date" - }, - "end_date_time": { - "format": "date-time", - "type": "string", - "example": "25/03/3019", - "description": "Leave request`s end date" - }, - "comment": { - "type": "string", - "example": "My precious", - "description": "Leave request`s comment" - }, - "approval_status": { - "type": "string", - "example": "True or False or Pending or Denied or Cancelled or Escalated", - "description": "Leave request`s approval status" - } - } - }, - "CreateExpenseDto": { - "type": "object", - "properties": { - "timesheet_id": { - "type": "number", - "example": "Th3F3110w5h1pX2024", - "description": "ID number for a set timesheet" - }, - "expense_code_id": { - "type": "number", - "example": "0n3R1n962Ru13xX", - "description": "ID number of an expense code (link with shift-codes)" - }, - "date": { - "format": "date-time", - "type": "string", - "example": "20/10/3018", - "description": "Date where the expense was made" - }, - "amount": { - "type": "number", - "example": "280 000 000,00", - "description": "Amount of the expense" - }, - "description": { - "type": "string", - "example": "Spent for mileage between A and B", - "description": "explain`s why the expense was made" - }, - "is_approved": { - "type": "boolean", - "example": "True or False or Pending or Denied or Cancelled or Escalated", - "description": "Expense`s approval status" - }, - "supervisor_comment": { - "type": "string", - "example": "Asked X to go there as an emergency response", - "description": "Supervisro`s justification for the spending of an employee" - } - }, - "required": [ - "timesheet_id", - "expense_code_id", - "date", - "amount", - "description", - "is_approved", - "supervisor_comment" - ] - }, - "ExpenseEntity": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1, - "description": "Unique ID of the expense (auto-generated)" - }, - "timesheet_id": { - "type": "number", - "example": 101, - "description": "ID number for a set timesheet" - }, - "expense": { - "type": "number", - "example": 7, - "description": "ID number of an expense code (link with expense-codes)" - }, - "date": { - "format": "date-time", - "type": "string", - "example": "3018-10-20T00:00:00.000Z", - "description": "Date where the expense was made" - }, - "is_approuved": { - "type": "boolean", - "example": "DENIED, APPROUVED, PENDING, etc...", - "description": "validation status" - }, - "description": { - "type": "string", - "example": "Spent for mileage between A and B", - "description": "explain`s why the expense was made" - }, - "supervisor_comment": { - "type": "string", - "example": "Asked X to go there as an emergency response", - "description": "Supervisro`s justification for the spending of an employee" - } - }, - "required": [ - "id", - "timesheet_id", - "expense", - "date", - "is_approuved", - "description", - "supervisor_comment" - ] - }, - "UpdateExpenseDto": { - "type": "object", - "properties": { - "timesheet_id": { - "type": "number", - "example": "Th3F3110w5h1pX2024", - "description": "ID number for a set timesheet" - }, - "expense_code_id": { - "type": "number", - "example": "0n3R1n962Ru13xX", - "description": "ID number of an expense code (link with shift-codes)" - }, - "date": { - "format": "date-time", - "type": "string", - "example": "20/10/3018", - "description": "Date where the expense was made" - }, - "amount": { - "type": "number", - "example": "280 000 000,00", - "description": "Amount of the expense" - }, - "description": { - "type": "string", - "example": "Spent for mileage between A and B", - "description": "explain`s why the expense was made" - }, - "is_approved": { - "type": "boolean", - "example": "True or False or Pending or Denied or Cancelled or Escalated", - "description": "Expense`s approval status" - }, - "supervisor_comment": { - "type": "string", - "example": "Asked X to go there as an emergency response", - "description": "Supervisro`s justification for the spending of an employee" - } - } - }, "CreateExpenseCodeDto": { "type": "object", "properties": { @@ -2848,195 +3026,6 @@ } } }, - "CreateShiftDto": { - "type": "object", - "properties": { - "timesheet_id": { - "type": "number", - "example": "Th3F3110w5h1pX2024", - "description": "ID number for a set timesheet" - }, - "shift_code_id": { - "type": "number", - "example": "0n3R1n962Ru13xX", - "description": "ID number of a shift code (link with shift-codes)" - }, - "date": { - "format": "date-time", - "type": "string", - "example": "20/10/3018", - "description": "Date where the shift takes place" - }, - "start_time": { - "format": "date-time", - "type": "string", - "example": "08:00", - "description": "Start time of the said shift" - }, - "end_time": { - "format": "date-time", - "type": "string", - "example": "17:00", - "description": "End time of the said shift" - }, - "description": { - "type": "string", - "example": "Called for an emergency at X` place", - "description": "justify the purpose of the shift" - } - }, - "required": [ - "timesheet_id", - "shift_code_id", - "date", - "start_time", - "end_time", - "description" - ] - }, - "ShiftEntity": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1, - "description": "Unique ID of the shift (auto-generated)" - }, - "timesheet_id": { - "type": "number", - "example": 101, - "description": "ID number for a set timesheet" - }, - "shift_code_id": { - "type": "number", - "example": 7, - "description": "ID number of a shift code (link with shift-codes)" - }, - "date": { - "format": "date-time", - "type": "string", - "example": "3018-10-20T00:00:00.000Z", - "description": "Date where the shift takes place" - }, - "start_time": { - "format": "date-time", - "type": "string", - "example": "3018-10-20T08:00:00.000Z", - "description": "Start time of the said shift" - }, - "end_time": { - "format": "date-time", - "type": "string", - "example": "3018-10-20T17:00:00.000Z", - "description": "End time of the said shift" - } - }, - "required": [ - "id", - "timesheet_id", - "shift_code_id", - "date", - "start_time", - "end_time" - ] - }, - "UpdateShiftsDto": { - "type": "object", - "properties": { - "timesheet_id": { - "type": "number", - "example": "Th3F3110w5h1pX2024", - "description": "ID number for a set timesheet" - }, - "shift_code_id": { - "type": "number", - "example": "0n3R1n962Ru13xX", - "description": "ID number of a shift code (link with shift-codes)" - }, - "date": { - "format": "date-time", - "type": "string", - "example": "20/10/3018", - "description": "Date where the shift takes place" - }, - "start_time": { - "format": "date-time", - "type": "string", - "example": "08:00", - "description": "Start time of the said shift" - }, - "end_time": { - "format": "date-time", - "type": "string", - "example": "17:00", - "description": "End time of the said shift" - }, - "description": { - "type": "string", - "example": "Called for an emergency at X` place", - "description": "justify the purpose of the shift" - } - } - }, - "CreateTimesheetDto": { - "type": "object", - "properties": { - "employee_id": { - "type": "number", - "example": "426433", - "description": "employee`s ID number of linked timsheet" - }, - "is_approved": { - "type": "boolean", - "example": "True or False or Pending or Denied or Cancelled or Escalated", - "description": "Timesheet`s approval status" - } - }, - "required": [ - "employee_id", - "is_approved" - ] - }, - "TimesheetEntity": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1, - "description": "timesheet`s unique ID (auto-generated)" - }, - "employee_id": { - "type": "number", - "example": 426433, - "description": "employee`s ID number of linked timsheet" - }, - "is_approved": { - "type": "boolean", - "example": true, - "description": "Timesheet`s approval status" - } - }, - "required": [ - "id", - "employee_id", - "is_approved" - ] - }, - "UpdateTimesheetDto": { - "type": "object", - "properties": { - "employee_id": { - "type": "number", - "example": "426433", - "description": "employee`s ID number of linked timsheet" - }, - "is_approved": { - "type": "boolean", - "example": "True or False or Pending or Denied or Cancelled or Escalated", - "description": "Timesheet`s approval status" - } - } - }, "PayPeriodEntity": { "type": "object", "properties": { diff --git a/prisma/migrations/20250729165951_fix_employees_crew_and_default_now_in_shifts_archive/migration.sql b/prisma/migrations/20250729165951_fix_employees_crew_and_default_now_in_shifts_archive/migration.sql new file mode 100644 index 0000000..1998c52 --- /dev/null +++ b/prisma/migrations/20250729165951_fix_employees_crew_and_default_now_in_shifts_archive/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "shifts_archive" ALTER COLUMN "archive_at" SET DEFAULT CURRENT_TIMESTAMP; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8ce585d..8363ea0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -43,7 +43,7 @@ model Employees { supervisor Employees? @relation("EmployeeSupervisor", fields: [supervisor_id], references: [id]) supervisor_id Int? - managed_employees Employees[] @relation("EmployeeSupervisor") //changer pour crew à la prochaine MaJ + crew Employees[] @relation("EmployeeSupervisor") archive EmployeesArchive[] @relation("EmployeeToArchive") timesheet Timesheets[] @relation("TimesheetEmployee") @@ -183,7 +183,7 @@ model ShiftsArchive { id Int @id @default(autoincrement()) shift Shifts @relation("ShiftsToArchive", fields: [shift_id], references: [id]) shift_id Int - archive_at DateTime + archive_at DateTime @default(now()) timesheet_id Int shift_code_id Int description String? diff --git a/src/app.module.ts b/src/app.module.ts index 34a59a8..e874e70 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -17,10 +17,12 @@ import { ExpensesModule } from './modules/expenses/expenses.module'; import { ExpenseCodesModule } from './modules/expense-codes/expense-codes.module'; import { PayperiodsModule } from './modules/pay-periods/pay-periods.module'; import { ScheduleModule } from '@nestjs/schedule'; +import { ArchivalModule } from './modules/archival/archival.module'; @Module({ imports: [ ScheduleModule.forRoot(), + ArchivalModule, PrismaModule, HealthModule, UsersModule, diff --git a/src/main.ts b/src/main.ts index 4c9552d..0bdbc78 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,9 @@ import 'reflect-metadata'; +//import and if case for @nestjs/schedule Cron jobs +import * as nodeCrypto from 'crypto'; +if(!(globalThis as any).crypto) { + (globalThis as any).crypto = nodeCrypto; +} import { ModuleRef, NestFactory, Reflector } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; diff --git a/src/modules/archival/archival.module.ts b/src/modules/archival/archival.module.ts new file mode 100644 index 0000000..926cf3f --- /dev/null +++ b/src/modules/archival/archival.module.ts @@ -0,0 +1,20 @@ +import { Module } from "@nestjs/common"; +import { ScheduleModule } from "@nestjs/schedule"; +import { TimesheetsModule } from "../timesheets/timesheets.module"; +import { ExpensesModule } from "../expenses/expenses.module"; +import { ShiftsModule } from "../shifts/shifts.module"; +import { LeaveRequestsModule } from "../leave-requests/leave-requests.module"; +import { ArchivalService } from "./services/archival.service"; + +@Module({ + imports: [ + ScheduleModule, + TimesheetsModule, + ExpensesModule, + ShiftsModule, + LeaveRequestsModule, + ], + providers: [ArchivalService], +}) + +export class ArchivalModule {} \ No newline at end of file diff --git a/src/modules/archival/services/archival.service.ts b/src/modules/archival/services/archival.service.ts new file mode 100644 index 0000000..989c6dd --- /dev/null +++ b/src/modules/archival/services/archival.service.ts @@ -0,0 +1,40 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { Cron, Timeout } from "@nestjs/schedule"; +import { ExpensesService } from "src/modules/expenses/services/expenses.service"; +import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-request.service"; +import { ShiftsService } from "src/modules/shifts/services/shifts.service"; +import { TimesheetsService } from "src/modules/timesheets/services/timesheets.service"; + +@Injectable() +export class ArchivalService { + private readonly logger = new Logger(ArchivalService.name); + + constructor( + private readonly timesheetsService: TimesheetsService, + private readonly expensesService: ExpensesService, + private readonly shiftsService: ShiftsService, + private readonly leaveRequestsService: LeaveRequestsService, + ) {} + + @Cron('0 0 3 * * 1', {timeZone:'America/Toronto'}) // chaque premier lundi du mois à 03h00 + async handleMonthlyArchival() { + const today = new Date(); + const dayOfMonth = today.getDate(); + + if (dayOfMonth > 7) { + this.logger.log('Archive {awaiting 1st monday of the month for archivation process}') + return; + } + + this.logger.log('monthly archivation in process'); + try { + await this.timesheetsService.archiveOld(); + await this.expensesService.archiveOld(); + await this.shiftsService.archiveOld(); + await this.leaveRequestsService.archiveExpired(); + this.logger.log('archivation process done'); + } catch (err) { + this.logger.log('an error occured during archivation ', err); + } + } +} \ No newline at end of file diff --git a/src/modules/employees/controllers/employees.controller.ts b/src/modules/employees/controllers/employees.controller.ts index c736182..2b30577 100644 --- a/src/modules/employees/controllers/employees.controller.ts +++ b/src/modules/employees/controllers/employees.controller.ts @@ -1,4 +1,4 @@ -import { Body,Controller,Delete,Get,Param,ParseIntPipe,Patch,Post,UseGuards } from '@nestjs/common'; +import { Body,Controller,Delete,Get,NotFoundException,Param,ParseIntPipe,Patch,Post,UseGuards } from '@nestjs/common'; import { Employees, Roles as RoleEnum } from '@prisma/client'; import { EmployeesService } from '../services/employees.service'; import { CreateEmployeeDto } from '../dtos/create-employee.dto'; @@ -62,4 +62,16 @@ export class EmployeesController { remove(@Param('id', ParseIntPipe) id: number): Promise { return this.employeesService.remove(id); } + + @Patch(':id') + async updateOrArchiveOrRestore( + @Param('id') id: string, + @Body() dto: UpdateEmployeeDto, + ) { + const result = await this.employeesService.patchEmployee(+id, dto); + if(!result) { + throw new NotFoundException(`Employee #${ id } not found in active or archive.`) + } + return result; + } } diff --git a/src/modules/employees/dtos/update-employee.dto.ts b/src/modules/employees/dtos/update-employee.dto.ts index f4096a1..c0558fb 100644 --- a/src/modules/employees/dtos/update-employee.dto.ts +++ b/src/modules/employees/dtos/update-employee.dto.ts @@ -1,4 +1,8 @@ import { PartialType } from '@nestjs/swagger'; import { CreateEmployeeDto } from './create-employee.dto'; -export class UpdateEmployeeDto extends PartialType(CreateEmployeeDto) {} +export class UpdateEmployeeDto extends PartialType(CreateEmployeeDto) { + first_work_day?: Date; + last_work_day?: Date; + supervisor_id?: number; +} diff --git a/src/modules/employees/services/employees.service.ts b/src/modules/employees/services/employees.service.ts index 5d945bd..9a16821 100644 --- a/src/modules/employees/services/employees.service.ts +++ b/src/modules/employees/services/employees.service.ts @@ -107,4 +107,86 @@ async update( await this.findOne(id); return this.prisma.employees.delete({ where: { id } }); } + + + //archivation function + async patchEmployee(id: number, dto: UpdateEmployeeDto): Promise { + //fetching existing employee + const existing = await this.prisma.employees.findUnique({ + where: { id }, + include: { user: true, archive: true }, + }); + if (existing) { + //verify last_work_day is not null => trigger archivation + if(dto.last_work_day != undefined && existing.last_work_day == null) { + return this.archiveOnTermination(existing, dto); + } + //if null => regular update + return this.prisma.employees.update({ + where: { id }, + data: dto, + }); + } + //if not found => fetch archives side for restoration + const archived = await this.prisma.employeesArchive.findFirst({ + where: { employee_id: id }, + include: { employee: true, user: true }, + }); + if (archived) { + //conditions for restoration + const restore = dto.last_work_day === null || dto.first_work_day != null; + if(restore) { + return this.restoreEmployee(archived, dto); + } + } + //if neither activated nor archivated => 404 + return null; + } + + //transfers the employee to archive and then delete from employees table + private async archiveOnTermination(existing: any, dto: UpdateEmployeeDto): Promise { + return this.prisma.$transaction(async transaction => { + //archive insertion + const archived = await transaction.employeesArchive.create({ + data: { + employee_id: existing.id, + user_id: existing.user_id, + first_name: existing.first_name, + last_name: existing.last_name, + external_payroll_id: existing.external_payroll_id, + company_code: existing.company_code, + first_Work_Day: existing.first_Work_Day, + last_work_day: existing.last_work_day, + supervisor_id: existing.supervisor_id ?? null, + }, + }); + //delete from employees table + await transaction.employees.delete({ where: { id: existing.id } }); + //return archived employee + return archived + }); + } + + //transfers the employee from archive to the employees table + private async restoreEmployee(archived: any, dto: UpdateEmployeeDto): Promise { + return this.prisma.$transaction(async transaction => { + //restores the archived employee into the employees table + const restored = await transaction.employees.create({ + data: { + id: archived.employee_id, + user_id: archived.user_id, + external_payroll_id: dto.external_payroll_id ?? archived.external_payroll_id, + company_code: dto.company_code ?? archived.company_code, + first_work_day: dto.first_work_day ?? archived.first_Work_Day, + last_work_day: null, + supervisor_id: dto.supervisor_id ?? archived.supervisor_id, + }, + }); + //deleting archived entry by id + await transaction.employeesArchive.delete({ where: { id: archived.id } }); + + //return restored employee + return restored; + }); + } } diff --git a/src/modules/expenses/expenses.module.ts b/src/modules/expenses/expenses.module.ts index 3432561..08cf449 100644 --- a/src/modules/expenses/expenses.module.ts +++ b/src/modules/expenses/expenses.module.ts @@ -5,7 +5,8 @@ import { ExpensesService } from "./services/expenses.service"; @Module({ controllers: [ExpensesController], - providers: [ExpensesService, PrismaService] + providers: [ExpensesService, PrismaService], + exports: [ ExpensesService ], }) export class ExpensesModule {} \ No newline at end of file diff --git a/src/modules/expenses/services/expenses.service.ts b/src/modules/expenses/services/expenses.service.ts index e92be77..2be085e 100644 --- a/src/modules/expenses/services/expenses.service.ts +++ b/src/modules/expenses/services/expenses.service.ts @@ -85,5 +85,51 @@ export class ExpensesService { async remove(id: number): Promise { await this.findOne(id); return this.prisma.expenses.delete({ where: { id } }); - } + } + + + //archivation function + async archiveOld(): Promise { + //fetches archived timesheet's Ids + const archivedTimesheets = await this.prisma.timesheetsArchive.findMany({ + select: { timesheet_id: true }, + }); + + const timesheetIds = archivedTimesheets.map(sheet => sheet.timesheet_id); + if(timesheetIds.length === 0) { + return; + } + + // copy/delete transaction + await this.prisma.$transaction(async transaction => { + //fetches expenses to move to archive + const expensesToArchive = await transaction.expenses.findMany({ + where: { timesheet_id: { in: timesheetIds } }, + }); + if(expensesToArchive.length === 0) { + return; + } + + //copies sent to archive table + await transaction.expensesArchive.createMany({ + data: expensesToArchive.map(exp => ({ + expense_id: exp.id, + timesheet_id: exp.timesheet_id, + expense_code_id: exp.expense_code_id, + date: exp.date, + amount: exp.amount, + attachement: exp.attachement, + description: exp.description, + is_approved: exp.is_approved, + supervisor_comment: exp.supervisor_comment, + })), + }); + + //delete from expenses table + await transaction.expenses.deleteMany({ + where: { id: { in: expensesToArchive.map(exp => exp.id) } }, + }) + + }) + } } \ No newline at end of file diff --git a/src/modules/leave-requests/leave-requests.module.ts b/src/modules/leave-requests/leave-requests.module.ts index 7ea2f65..1973165 100644 --- a/src/modules/leave-requests/leave-requests.module.ts +++ b/src/modules/leave-requests/leave-requests.module.ts @@ -6,6 +6,7 @@ import { Module } from "@nestjs/common"; @Module({ controllers: [LeaveRequestController], providers: [ LeaveRequestsService, PrismaService], + exports: [ LeaveRequestsService], }) export class LeaveRequestsModule {} \ No newline at end of file diff --git a/src/modules/leave-requests/services/leave-request.service.ts b/src/modules/leave-requests/services/leave-request.service.ts index 0974320..2d2bd84 100644 --- a/src/modules/leave-requests/services/leave-request.service.ts +++ b/src/modules/leave-requests/services/leave-request.service.ts @@ -106,4 +106,36 @@ export class LeaveRequestsService { where: { id }, }); } + + //archivation function + async archiveExpired(): Promise { + const now = new Date(); + + await this.prisma.$transaction(async transaction => { + //fetches expired leave requests + const expired = await transaction.leaveRequests.findMany({ + where: { end_date_time: { lt: now } }, + }); + if(expired.length === 0) { + return; + } + //copy unto archive table + await transaction.leaveRequestsArchive.createMany({ + data: expired.map(request => ({ + leave_request_id: request.id, + employee_id: request.employee_id, + leave_type: request.leave_type, + start_date_time: request.start_date_time, + end_date_time: request.end_date_time, + comment: request.comment, + approval_status: request.approval_status, + })), + }); + //delete from leave_requests table + await transaction.leaveRequests.deleteMany({ + where: { id: { in: expired.map(request => request.id ) } }, + }); + }); + + } } \ No newline at end of file diff --git a/src/modules/shifts/services/shifts.service.ts b/src/modules/shifts/services/shifts.service.ts index 38547d6..161e05c 100644 --- a/src/modules/shifts/services/shifts.service.ts +++ b/src/modules/shifts/services/shifts.service.ts @@ -86,4 +86,48 @@ export class ShiftsService { await this.findOne(id); return this.prisma.shifts.delete({ where: { id } }); } + + //archivation function + async archiveOld(): Promise { + //fetches archived timesheet's Ids + const archivedTimesheets = await this.prisma.timesheetsArchive.findMany({ + select: { timesheet_id: true }, + }); + + const timesheetIds = archivedTimesheets.map(sheet => sheet.timesheet_id); + if(timesheetIds.length === 0) { + return; + } + + // copy/delete transaction + await this.prisma.$transaction(async transaction => { + //fetches shifts to move to archive + const shiftsToArchive = await transaction.shifts.findMany({ + where: { timesheet_id: { in: timesheetIds } }, + }); + if(shiftsToArchive.length === 0) { + return; + } + + //copies sent to archive table + await transaction.shiftsArchive.createMany({ + data: shiftsToArchive.map(shift => ({ + shift_id: shift.id, + timesheet_id: shift.timesheet_id, + shift_code_id: shift.shift_code_id, + description: shift.description ?? undefined, + date: shift.date, + start_time: shift.start_time, + end_time: shift.end_time, + })), + }); + + //delete from shifts table + await transaction.shifts.deleteMany({ + where: { id: { in: shiftsToArchive.map(shift => shift.id) } }, + }) + + }) + } + } \ No newline at end of file diff --git a/src/modules/shifts/shifts.module.ts b/src/modules/shifts/shifts.module.ts index 6c05370..53924f0 100644 --- a/src/modules/shifts/shifts.module.ts +++ b/src/modules/shifts/shifts.module.ts @@ -5,6 +5,7 @@ import { PrismaService } from 'src/prisma/prisma.service'; @Module({ controllers: [ShiftsController], - providers: [ShiftsService, PrismaService] + providers: [ShiftsService, PrismaService], + exports: [ShiftsService], }) export class ShiftsModule {} diff --git a/src/modules/timesheets/services/timesheets.service.ts b/src/modules/timesheets/services/timesheets.service.ts index 31279b2..84db6e4 100644 --- a/src/modules/timesheets/services/timesheets.service.ts +++ b/src/modules/timesheets/services/timesheets.service.ts @@ -75,4 +75,44 @@ export class TimesheetsService { } + //Archivation function + async archiveOld(): Promise { + //calcul du cutoff pour archivation + const cutoff = new Date(); + cutoff.setMonth(cutoff.getMonth() - 6) + + await this.prisma.$transaction(async transaction => { + //fetches all timesheets to cutoff + const oldSheets = await transaction.timesheets.findMany({ + where: { shift: { every: { date: { lt: cutoff } }, + }, + }, + select: { + id: true, + employee_id: true, + is_approved: true, + }, + }); + if( oldSheets.length === 0) { + return; + } + + //preping data for archivation + const archiveDate = oldSheets.map(sheet => ({ + timesheet_id: sheet.id, + employee_id: sheet.employee_id, + is_approved: sheet.is_approved, + })); + + //copying data from timesheets table to archive table + await transaction.timesheetsArchive.createMany({ + data: archiveDate, + }); + + //removing data from timesheets table + await transaction.timesheets.deleteMany({ + where: { id: { in: oldSheets.map(s => s.id) } }, + }); + }); + } } diff --git a/src/modules/timesheets/timesheets.module.ts b/src/modules/timesheets/timesheets.module.ts index 6cfeb69..11da5c8 100644 --- a/src/modules/timesheets/timesheets.module.ts +++ b/src/modules/timesheets/timesheets.module.ts @@ -4,6 +4,7 @@ import { TimesheetsService } from './services/timesheets.service'; @Module({ controllers: [TimesheetsController], - providers: [TimesheetsService] + providers: [TimesheetsService], + exports: [TimesheetsService], }) export class TimesheetsModule {} From 9762790fbc70e088a578fcd3fc720fa8cb887dc1 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 29 Jul 2025 16:21:37 -0400 Subject: [PATCH 04/21] feat(swagger): swagger docs for update function from employees.controller.ts and update.dto. small fixes to archival.service.ts --- docs/swagger/swagger-spec.json | 93 ++++++++++++------- .../archival/services/archival.service.ts | 9 +- .../controllers/employees.controller.ts | 39 ++++---- .../employees/dtos/update-employee.dto.ts | 13 ++- 4 files changed, 90 insertions(+), 64 deletions(-) diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 2d6d040..3810127 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -1236,7 +1236,7 @@ "operationId": "EmployeesController_findAll", "parameters": [], "responses": { - "201": { + "200": { "description": "List of employees found", "content": { "application/json": { @@ -1278,7 +1278,7 @@ } ], "responses": { - "201": { + "200": { "description": "Employee found", "content": { "application/json": { @@ -1302,6 +1302,37 @@ "Employees" ] }, + "delete": { + "operationId": "EmployeesController_remove", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Identifier of the employee to delete", + "schema": { + "type": "number" + } + } + ], + "responses": { + "204": { + "description": "Employee deleted" + }, + "404": { + "description": "Employee not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Delete employee", + "tags": [ + "Employees" + ] + }, "patch": { "operationId": "EmployeesController_updateOrArchiveOrRestore", "parameters": [ @@ -1309,8 +1340,9 @@ "name": "id", "required": true, "in": "path", + "description": "Identifier of the employee", "schema": { - "type": "string" + "type": "number" } } ], @@ -1326,33 +1358,7 @@ }, "responses": { "200": { - "description": "" - } - }, - "security": [ - { - "access-token": [] - } - ], - "tags": [ - "Employees" - ] - }, - "delete": { - "operationId": "EmployeesController_remove", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "201": { - "description": "Employee deleted", + "description": "Employee updated or restored", "content": { "application/json": { "schema": { @@ -1361,16 +1367,29 @@ } } }, - "400": { - "description": "Employee not found" + "202": { + "description": "Employee archived successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmployeeEntity" + } + } + } + }, + "404": { + "description": "Employee not found in active or archive" } }, "security": [ + { + "access-token": [] + }, { "access-token": [] } ], - "summary": "Delete employee", + "summary": "Update, archive or restore an employee", "tags": [ "Employees" ] @@ -2898,13 +2917,17 @@ "format": "date-time", "type": "string", "example": "23/09/3018", - "description": "Employee`s first working day" + "description": "New hire date or undefined" }, "last_work_day": { "format": "date-time", "type": "string", "example": "25/03/3019", - "description": "Employee`s last working day" + "description": "Termination date (null to restore)" + }, + "supervisor_id": { + "type": "number", + "description": "Supervisor ID" } } }, diff --git a/src/modules/archival/services/archival.service.ts b/src/modules/archival/services/archival.service.ts index 989c6dd..2a8b497 100644 --- a/src/modules/archival/services/archival.service.ts +++ b/src/modules/archival/services/archival.service.ts @@ -1,5 +1,6 @@ import { Injectable, Logger } from "@nestjs/common"; -import { Cron, Timeout } from "@nestjs/schedule"; +import { Cron } from "@nestjs/schedule"; +import { ApiInternalServerErrorResponse, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { ExpensesService } from "src/modules/expenses/services/expenses.service"; import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-request.service"; import { ShiftsService } from "src/modules/shifts/services/shifts.service"; @@ -15,14 +16,14 @@ export class ArchivalService { private readonly shiftsService: ShiftsService, private readonly leaveRequestsService: LeaveRequestsService, ) {} - + @Cron('0 0 3 * * 1', {timeZone:'America/Toronto'}) // chaque premier lundi du mois à 03h00 async handleMonthlyArchival() { const today = new Date(); const dayOfMonth = today.getDate(); if (dayOfMonth > 7) { - this.logger.log('Archive {awaiting 1st monday of the month for archivation process}') + this.logger.warn('Archive {awaiting 1st monday of the month for archivation process}') return; } @@ -34,7 +35,7 @@ export class ArchivalService { await this.leaveRequestsService.archiveExpired(); this.logger.log('archivation process done'); } catch (err) { - this.logger.log('an error occured during archivation ', err); + this.logger.error('an error occured during archivation process ', err); } } } \ No newline at end of file diff --git a/src/modules/employees/controllers/employees.controller.ts b/src/modules/employees/controllers/employees.controller.ts index 2b30577..f0ba727 100644 --- a/src/modules/employees/controllers/employees.controller.ts +++ b/src/modules/employees/controllers/employees.controller.ts @@ -4,7 +4,7 @@ import { EmployeesService } from '../services/employees.service'; import { CreateEmployeeDto } from '../dtos/create-employee.dto'; import { UpdateEmployeeDto } from '../dtos/update-employee.dto'; import { RolesAllowed } from '../../../common/decorators/roles.decorators'; -import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { JwtAuthGuard } from 'src/modules/authentication/guards/jwt-auth.guard'; import { EmployeeEntity } from '../dtos/swagger-entities/employees.entity'; @@ -27,7 +27,7 @@ export class EmployeesController { @Get() @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING) @ApiOperation({summary: 'Find all employees' }) - @ApiResponse({ status: 201, description: 'List of employees found', type: EmployeeEntity, isArray: true }) + @ApiResponse({ status: 200, description: 'List of employees found', type: EmployeeEntity, isArray: true }) @ApiResponse({ status: 400, description: 'List of employees not found' }) findAll(): Promise { return this.employeesService.findAll(); @@ -36,38 +36,35 @@ export class EmployeesController { @Get(':id') @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING ) @ApiOperation({summary: 'Find employee' }) - @ApiResponse({ status: 201, description: 'Employee found', type: EmployeeEntity }) + @ApiResponse({ status: 200, description: 'Employee found', type: EmployeeEntity }) @ApiResponse({ status: 400, description: 'Employee not found' }) findOne(@Param('id', ParseIntPipe) id: number): Promise { return this.employeesService.findOne(id); } - @Patch(':id') - @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING ) - @ApiOperation({summary: 'Update employee' }) - @ApiResponse({ status: 201, description: 'Employee updated', type: EmployeeEntity }) - @ApiResponse({ status: 400, description: 'Employee not found' }) - update( - @Param('id', ParseIntPipe) id: number, - @Body() dto: UpdateEmployeeDto, - ): Promise { - return this.employeesService.update(id, dto); - } - @Delete(':id') @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR ) @ApiOperation({summary: 'Delete employee' }) - @ApiResponse({ status: 201, description: 'Employee deleted', type: EmployeeEntity }) - @ApiResponse({ status: 400, description: 'Employee not found' }) + @ApiParam({ name: 'id', type: Number, description: 'Identifier of the employee to delete' }) + @ApiResponse({ status: 204, description: 'Employee deleted' }) + @ApiResponse({ status: 404, description: 'Employee not found' }) remove(@Param('id', ParseIntPipe) id: number): Promise { return this.employeesService.remove(id); } @Patch(':id') - async updateOrArchiveOrRestore( - @Param('id') id: string, - @Body() dto: UpdateEmployeeDto, - ) { + @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + @ApiBearerAuth('access-token') + @ApiOperation({ summary: 'Update, archive or restore an employee' }) + @ApiParam({ name: 'id', type: Number, description: 'Identifier of the employee' }) + @ApiResponse({ status: 200, description: 'Employee updated or restored', type: EmployeeEntity }) + @ApiResponse({ status: 202, description: 'Employee archived successfully', type: EmployeeEntity }) + @ApiResponse({ status: 404, description: 'Employee not found in active or archive' }) + @Patch(':id') + async updateOrArchiveOrRestore(@Param('id') id: string, @Body() dto: UpdateEmployeeDto,) { + // if last_work_day is set => archive the employee + // else if employee is archived and first_work_day or last_work_day = null => restore + //otherwise => standard update const result = await this.employeesService.patchEmployee(+id, dto); if(!result) { throw new NotFoundException(`Employee #${ id } not found in active or archive.`) diff --git a/src/modules/employees/dtos/update-employee.dto.ts b/src/modules/employees/dtos/update-employee.dto.ts index c0558fb..01b44de 100644 --- a/src/modules/employees/dtos/update-employee.dto.ts +++ b/src/modules/employees/dtos/update-employee.dto.ts @@ -1,8 +1,13 @@ -import { PartialType } from '@nestjs/swagger'; +import { ApiProperty, PartialType } from '@nestjs/swagger'; import { CreateEmployeeDto } from './create-employee.dto'; export class UpdateEmployeeDto extends PartialType(CreateEmployeeDto) { - first_work_day?: Date; - last_work_day?: Date; - supervisor_id?: number; + @ApiProperty({ required: false, type: Date, description: 'New hire date or undefined' }) + first_work_day?: Date; + + @ApiProperty({ required: false, type: Date, description: 'Termination date (null to restore)' }) + last_work_day?: Date; + + @ApiProperty({ required: false, type: Number, description: 'Supervisor ID' }) + supervisor_id?: number; } From 2247566d7371f9b634c468c3f69f7d13d47f809a Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 29 Jul 2025 16:34:46 -0400 Subject: [PATCH 05/21] fix(docs): minor swagger fix --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3510c72..17ad3fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -849,9 +849,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "dependencies": { "@eslint/core": "^0.15.1", From 1f494db0e3b9e237d1577e8b3d49c4faea715be5 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 29 Jul 2025 16:35:31 -0400 Subject: [PATCH 06/21] Merge branch 'dev/setup/prisma/archive/MatthieuH' of git.targo.ca:Targo/targo_backend From b7bae49ce79eb9e9cc5484ead4f18e22fd75fd3b Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 29 Jul 2025 16:36:47 -0400 Subject: [PATCH 07/21] fix(cron): updated dependencies, run npm install --- src/modules/archival/services/archival.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/archival/services/archival.service.ts b/src/modules/archival/services/archival.service.ts index 2a8b497..7d8c6d7 100644 --- a/src/modules/archival/services/archival.service.ts +++ b/src/modules/archival/services/archival.service.ts @@ -1,6 +1,5 @@ import { Injectable, Logger } from "@nestjs/common"; import { Cron } from "@nestjs/schedule"; -import { ApiInternalServerErrorResponse, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { ExpensesService } from "src/modules/expenses/services/expenses.service"; import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-request.service"; import { ShiftsService } from "src/modules/shifts/services/shifts.service"; From 09a213d5dddfe4ce288faf50821c08ae206a1ddc Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 30 Jul 2025 10:07:24 -0400 Subject: [PATCH 08/21] feat(archival): added controllers to add a search option for archive tables for admin, hr and supervisors --- src/modules/archival/archival.module.ts | 12 +++++++ .../employees-archive.controller.ts | 34 +++++++++++++++++++ .../expenses-archive.controller.ts | 33 ++++++++++++++++++ .../leave-requests-archive.controller.ts | 33 ++++++++++++++++++ .../controllers/shifts-archive.controller.ts | 33 ++++++++++++++++++ .../timesheets-archive.controller.ts | 34 +++++++++++++++++++ .../employees/services/employees.service.ts | 16 +++++++-- .../expenses/services/expenses.service.ts | 15 ++++++-- .../services/leave-request.service.ts | 14 ++++++-- src/modules/shifts/services/shifts.service.ts | 15 ++++++-- .../timesheets/services/timesheets.service.ts | 15 ++++++-- 11 files changed, 244 insertions(+), 10 deletions(-) create mode 100644 src/modules/archival/controllers/employees-archive.controller.ts create mode 100644 src/modules/archival/controllers/expenses-archive.controller.ts create mode 100644 src/modules/archival/controllers/leave-requests-archive.controller.ts create mode 100644 src/modules/archival/controllers/shifts-archive.controller.ts create mode 100644 src/modules/archival/controllers/timesheets-archive.controller.ts diff --git a/src/modules/archival/archival.module.ts b/src/modules/archival/archival.module.ts index 926cf3f..6c2dd28 100644 --- a/src/modules/archival/archival.module.ts +++ b/src/modules/archival/archival.module.ts @@ -5,6 +5,11 @@ import { ExpensesModule } from "../expenses/expenses.module"; import { ShiftsModule } from "../shifts/shifts.module"; import { LeaveRequestsModule } from "../leave-requests/leave-requests.module"; import { ArchivalService } from "./services/archival.service"; +import { EmployeesArchiveController } from "./controllers/employees-archive.controller"; +import { ExpensesArchiveController } from "./controllers/expenses-archive.controller"; +import { LeaveRequestsArchiveController } from "./controllers/leave-requests-archive.controller"; +import { ShiftsArchiveController } from "./controllers/shifts-archive.controller"; +import { TimesheetsArchiveController } from "./controllers/timesheets-archive.controller"; @Module({ imports: [ @@ -15,6 +20,13 @@ import { ArchivalService } from "./services/archival.service"; LeaveRequestsModule, ], providers: [ArchivalService], + controllers: [ + EmployeesArchiveController, + ExpensesArchiveController, + LeaveRequestsArchiveController, + ShiftsArchiveController, + TimesheetsArchiveController, + ] }) export class ArchivalModule {} \ No newline at end of file diff --git a/src/modules/archival/controllers/employees-archive.controller.ts b/src/modules/archival/controllers/employees-archive.controller.ts new file mode 100644 index 0000000..07ec791 --- /dev/null +++ b/src/modules/archival/controllers/employees-archive.controller.ts @@ -0,0 +1,34 @@ +import { Controller, Get, NotFoundException, Param, ParseIntPipe, UseGuards } from "@nestjs/common"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { EmployeesArchive, Roles as RoleEnum } from '@prisma/client'; +import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard"; +import { EmployeesService } from "src/modules/employees/services/employees.service"; + +@ApiTags('Employee Archives') +@UseGuards(JwtAuthGuard) +@Controller('archives/employees') +export class EmployeesArchiveController { + constructor(private readonly employeesService: EmployeesService) {} + + @Get() + @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + @ApiOperation({ summary: 'List of archived employees'}) + @ApiResponse({ status: 200, description: 'List of archived employees', isArray: true }) + async findAllArchived(): Promise { + return this.employeesService.findAllArchived(); + } + + @Get() + @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR,RoleEnum.SUPERVISOR) + @ApiOperation({ summary: 'Fetch employee in archives with its Id'}) + @ApiResponse({ status: 200, description: 'Archived employee found'}) + async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise { + try{ + return await this.employeesService.findOneArchived(id); + }catch { + throw new NotFoundException(`Archived employee #${id} not found`); + } + } + +} \ No newline at end of file diff --git a/src/modules/archival/controllers/expenses-archive.controller.ts b/src/modules/archival/controllers/expenses-archive.controller.ts new file mode 100644 index 0000000..01b544a --- /dev/null +++ b/src/modules/archival/controllers/expenses-archive.controller.ts @@ -0,0 +1,33 @@ +import { UseGuards, Controller, Get, Param, ParseIntPipe, NotFoundException } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiResponse } from "@nestjs/swagger"; +import { ExpensesArchive,Roles as RoleEnum } from "@prisma/client"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard"; +import { ExpensesService } from "src/modules/expenses/services/expenses.service"; + +@ApiTags('Expense Archives') +@UseGuards(JwtAuthGuard) +@Controller('archives/expenses') +export class ExpensesArchiveController { + constructor(private readonly expensesService: ExpensesService) {} + + @Get() + @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + @ApiOperation({ summary: 'List of archived expenses'}) + @ApiResponse({ status: 200, description: 'List of archived expenses', isArray: true }) + async findAllArchived(): Promise { + return this.expensesService.findAllArchived(); + } + + @Get() + @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + @ApiOperation({ summary: 'Fetch expense in archives with its Id'}) + @ApiResponse({ status: 200, description: 'Archived expense found'}) + async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise { + try{ + return await this.expensesService.findOneArchived(id); + }catch { + throw new NotFoundException(`Archived expense #${id} not found`); + } + } +} \ No newline at end of file diff --git a/src/modules/archival/controllers/leave-requests-archive.controller.ts b/src/modules/archival/controllers/leave-requests-archive.controller.ts new file mode 100644 index 0000000..c7fe0ca --- /dev/null +++ b/src/modules/archival/controllers/leave-requests-archive.controller.ts @@ -0,0 +1,33 @@ +import { Get, Param, ParseIntPipe, NotFoundException, Controller, UseGuards } from "@nestjs/common"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { LeaveRequestsArchive, Roles as RoleEnum } from "@prisma/client"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard"; +import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-request.service"; + +@ApiTags('LeaveRequests Archives') +@UseGuards(JwtAuthGuard) +@Controller('archives/leaveRequests') +export class LeaveRequestsArchiveController { + constructor(private readonly leaveRequestsService: LeaveRequestsService) {} + + @Get() + @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + @ApiOperation({ summary: 'List of archived leaveRequests'}) + @ApiResponse({ status: 200, description: 'List of archived leaveRequests', isArray: true }) + async findAllArchived(): Promise { + return this.leaveRequestsService.findAllArchived(); + } + + @Get() + @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + @ApiOperation({ summary: 'Fetch leaveRequest in archives with its Id'}) + @ApiResponse({ status: 200, description: 'Archived leaveRequest found'}) + async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise { + try{ + return await this.leaveRequestsService.findOneArchived(id); + }catch { + throw new NotFoundException(`Archived leaveRequest #${id} not found`); + } + } +} \ No newline at end of file diff --git a/src/modules/archival/controllers/shifts-archive.controller.ts b/src/modules/archival/controllers/shifts-archive.controller.ts new file mode 100644 index 0000000..d8fc568 --- /dev/null +++ b/src/modules/archival/controllers/shifts-archive.controller.ts @@ -0,0 +1,33 @@ +import { Get, Param, ParseIntPipe, NotFoundException, Controller, UseGuards } from "@nestjs/common"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { ShiftsArchive, Roles as RoleEnum } from "@prisma/client"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard"; +import { ShiftsService } from "src/modules/shifts/services/shifts.service"; + +@ApiTags('Shift Archives') +@UseGuards(JwtAuthGuard) +@Controller('archives/shifts') +export class ShiftsArchiveController { + constructor(private readonly shiftsService:ShiftsService) {} + + @Get() + @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + @ApiOperation({ summary: 'List of archived shifts'}) + @ApiResponse({ status: 200, description: 'List of archived shifts', isArray: true }) + async findAllArchived(): Promise { + return this.shiftsService.findAllArchived(); + } + + @Get() + @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR,RoleEnum.SUPERVISOR) + @ApiOperation({ summary: 'Fetch shift in archives with its Id'}) + @ApiResponse({ status: 200, description: 'Archived shift found'}) + async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise { + try{ + return await this.shiftsService.findOneArchived(id); + }catch { + throw new NotFoundException(`Archived shift #${id} not found`); + } + } +} \ No newline at end of file diff --git a/src/modules/archival/controllers/timesheets-archive.controller.ts b/src/modules/archival/controllers/timesheets-archive.controller.ts new file mode 100644 index 0000000..0fcb8e5 --- /dev/null +++ b/src/modules/archival/controllers/timesheets-archive.controller.ts @@ -0,0 +1,34 @@ +import { Controller, Get, NotFoundException, Param, ParseIntPipe, UseGuards } from "@nestjs/common"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { TimesheetsArchive, Roles as RoleEnum } from '@prisma/client'; +import { TimesheetsService } from "src/modules/timesheets/services/timesheets.service"; +import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard"; + +@ApiTags('Timesheet Archives') +@UseGuards(JwtAuthGuard) +@Controller('archives/timesheets') +export class TimesheetsArchiveController { + constructor(private readonly timesheetsService: TimesheetsService) {} + + @Get() + @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + @ApiOperation({ summary: 'List of archived timesheets'}) + @ApiResponse({ status: 200, description: 'List of archived timesheets', isArray: true }) + async findAllArchived(): Promise { + return this.timesheetsService.findAllArchived(); + } + + @Get() + @RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + @ApiOperation({ summary: 'Fetch timesheet in archives with its Id'}) + @ApiResponse({ status: 200, description: 'Archived timesheet found'}) + async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise { + try{ + return await this.timesheetsService.findOneArchived(id); + }catch { + throw new NotFoundException(`Archived timesheet #${id} not found`); + } + } + +} \ No newline at end of file diff --git a/src/modules/employees/services/employees.service.ts b/src/modules/employees/services/employees.service.ts index 9a16821..183c1b0 100644 --- a/src/modules/employees/services/employees.service.ts +++ b/src/modules/employees/services/employees.service.ts @@ -2,7 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from 'src/prisma/prisma.service'; import { CreateEmployeeDto } from '../dtos/create-employee.dto'; import { UpdateEmployeeDto } from '../dtos/update-employee.dto'; -import { Employees, Users } from '@prisma/client'; +import { Employees, EmployeesArchive, Users } from '@prisma/client'; @Injectable() export class EmployeesService { @@ -109,7 +109,8 @@ async update( } - //archivation function + //archivation functions ****************************************************** + async patchEmployee(id: number, dto: UpdateEmployeeDto): Promise { //fetching existing employee const existing = await this.prisma.employees.findUnique({ @@ -189,4 +190,15 @@ async update( return restored; }); } + + //fetches all archived employees + async findAllArchived(): Promise { + return this.prisma.employeesArchive.findMany(); + } + + //fetches an archived employee + async findOneArchived(id: number): Promise { + return this.prisma.employeesArchive.findUniqueOrThrow({ where: { id } }); + } + } diff --git a/src/modules/expenses/services/expenses.service.ts b/src/modules/expenses/services/expenses.service.ts index 2be085e..9f8c0e2 100644 --- a/src/modules/expenses/services/expenses.service.ts +++ b/src/modules/expenses/services/expenses.service.ts @@ -1,7 +1,7 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { CreateExpenseDto } from "../dtos/create-expense"; -import { Expenses } from "@prisma/client"; +import { Expenses, ExpensesArchive } from "@prisma/client"; import { UpdateExpenseDto } from "../dtos/update-expense"; @Injectable() @@ -88,7 +88,8 @@ export class ExpensesService { } - //archivation function + //archivation functions ****************************************************** + async archiveOld(): Promise { //fetches archived timesheet's Ids const archivedTimesheets = await this.prisma.timesheetsArchive.findMany({ @@ -132,4 +133,14 @@ export class ExpensesService { }) } + + //fetches all archived timesheets + async findAllArchived(): Promise { + return this.prisma.expensesArchive.findMany(); + } + + //fetches an archived timesheet + async findOneArchived(id: number): Promise { + return this.prisma.expensesArchive.findUniqueOrThrow({ where: { id } }); + } } \ No newline at end of file diff --git a/src/modules/leave-requests/services/leave-request.service.ts b/src/modules/leave-requests/services/leave-request.service.ts index 2d2bd84..e8315a7 100644 --- a/src/modules/leave-requests/services/leave-request.service.ts +++ b/src/modules/leave-requests/services/leave-request.service.ts @@ -1,7 +1,7 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto"; -import { LeaveRequests } from "@prisma/client"; +import { LeaveRequests, LeaveRequestsArchive } from "@prisma/client"; import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto"; @Injectable() @@ -107,7 +107,8 @@ export class LeaveRequestsService { }); } - //archivation function + //archivation functions ****************************************************** + async archiveExpired(): Promise { const now = new Date(); @@ -136,6 +137,15 @@ export class LeaveRequestsService { where: { id: { in: expired.map(request => request.id ) } }, }); }); + } + //fetches all archived employees + async findAllArchived(): Promise { + return this.prisma.leaveRequestsArchive.findMany(); + } + + //fetches an archived employee + async findOneArchived(id: number): Promise { + return this.prisma.leaveRequestsArchive.findUniqueOrThrow({ where: { id } }); } } \ No newline at end of file diff --git a/src/modules/shifts/services/shifts.service.ts b/src/modules/shifts/services/shifts.service.ts index 161e05c..2844bcb 100644 --- a/src/modules/shifts/services/shifts.service.ts +++ b/src/modules/shifts/services/shifts.service.ts @@ -1,7 +1,7 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { CreateShiftDto } from "../dtos/create-shifts.dto"; -import { Shifts } from "@prisma/client"; +import { Shifts, ShiftsArchive } from "@prisma/client"; import { UpdateShiftsDto } from "../dtos/update-shifts.dto"; @Injectable() @@ -87,7 +87,8 @@ export class ShiftsService { return this.prisma.shifts.delete({ where: { id } }); } - //archivation function + //archivation functions ****************************************************** + async archiveOld(): Promise { //fetches archived timesheet's Ids const archivedTimesheets = await this.prisma.timesheetsArchive.findMany({ @@ -130,4 +131,14 @@ export class ShiftsService { }) } + //fetches all archived timesheets + async findAllArchived(): Promise { + return this.prisma.shiftsArchive.findMany(); + } + + //fetches an archived timesheet + async findOneArchived(id: number): Promise { + return this.prisma.shiftsArchive.findUniqueOrThrow({ where: { id } }); + } + } \ No newline at end of file diff --git a/src/modules/timesheets/services/timesheets.service.ts b/src/modules/timesheets/services/timesheets.service.ts index 84db6e4..c8fcff3 100644 --- a/src/modules/timesheets/services/timesheets.service.ts +++ b/src/modules/timesheets/services/timesheets.service.ts @@ -1,7 +1,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from 'src/prisma/prisma.service'; import { CreateTimesheetDto } from '../dtos/create-timesheet.dto'; -import { Timesheets } from '@prisma/client'; +import { Timesheets, TimesheetsArchive } from '@prisma/client'; import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto'; @Injectable() @@ -75,7 +75,8 @@ export class TimesheetsService { } - //Archivation function +//archivation functions ****************************************************** + async archiveOld(): Promise { //calcul du cutoff pour archivation const cutoff = new Date(); @@ -115,4 +116,14 @@ export class TimesheetsService { }); }); } + + //fetches all archived timesheets + async findAllArchived(): Promise { + return this.prisma.timesheetsArchive.findMany(); + } + + //fetches an archived timesheet + async findOneArchived(id: number): Promise { + return this.prisma.timesheetsArchive.findUniqueOrThrow({ where: { id } }); + } } From 106454bede1f75eaa90b19909eb428019b84b98f Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 30 Jul 2025 10:24:53 -0400 Subject: [PATCH 09/21] fix(imports): small import fix to archival.module.ts --- docs/swagger/swagger-spec.json | 846 ++++++++++++---------- src/modules/archival/archival.module.ts | 2 + src/modules/employees/employees.module.ts | 1 + 3 files changed, 486 insertions(+), 363 deletions(-) diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 3810127..62fbba4 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -29,6 +29,328 @@ ] } }, + "/archives/employees": { + "get": { + "operationId": "EmployeesArchiveController_findOneArchived", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Archived employee found" + } + }, + "summary": "Fetch employee in archives with its Id", + "tags": [ + "Employee Archives" + ] + } + }, + "/archives/expenses": { + "get": { + "operationId": "ExpensesArchiveController_findOneArchived", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Archived expense found" + } + }, + "summary": "Fetch expense in archives with its Id", + "tags": [ + "Expense Archives" + ] + } + }, + "/archives/leaveRequests": { + "get": { + "operationId": "LeaveRequestsArchiveController_findOneArchived", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Archived leaveRequest found" + } + }, + "summary": "Fetch leaveRequest in archives with its Id", + "tags": [ + "LeaveRequests Archives" + ] + } + }, + "/archives/shifts": { + "get": { + "operationId": "ShiftsArchiveController_findOneArchived", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Archived shift found" + } + }, + "summary": "Fetch shift in archives with its Id", + "tags": [ + "Shift Archives" + ] + } + }, + "/archives/timesheets": { + "get": { + "operationId": "TimesheetsArchiveController_findOneArchived", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Archived timesheet found" + } + }, + "summary": "Fetch timesheet in archives with its Id", + "tags": [ + "Timesheet Archives" + ] + } + }, + "/employees": { + "post": { + "operationId": "EmployeesController_create", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEmployeeDto" + } + } + } + }, + "responses": { + "201": { + "description": "Employee created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmployeeEntity" + } + } + } + }, + "400": { + "description": "Incomplete task or invalid data" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Create employee", + "tags": [ + "Employees" + ] + }, + "get": { + "operationId": "EmployeesController_findAll", + "parameters": [], + "responses": { + "200": { + "description": "List of employees found", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EmployeeEntity" + } + } + } + } + }, + "400": { + "description": "List of employees not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find all employees", + "tags": [ + "Employees" + ] + } + }, + "/employees/{id}": { + "get": { + "operationId": "EmployeesController_findOne", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Employee found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmployeeEntity" + } + } + } + }, + "400": { + "description": "Employee not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find employee", + "tags": [ + "Employees" + ] + }, + "delete": { + "operationId": "EmployeesController_remove", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Identifier of the employee to delete", + "schema": { + "type": "number" + } + } + ], + "responses": { + "204": { + "description": "Employee deleted" + }, + "404": { + "description": "Employee not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Delete employee", + "tags": [ + "Employees" + ] + }, + "patch": { + "operationId": "EmployeesController_updateOrArchiveOrRestore", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Identifier of the employee", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEmployeeDto" + } + } + } + }, + "responses": { + "200": { + "description": "Employee updated or restored", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmployeeEntity" + } + } + } + }, + "202": { + "description": "Employee archived successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmployeeEntity" + } + } + } + }, + "404": { + "description": "Employee not found in active or archive" + } + }, + "security": [ + { + "access-token": [] + }, + { + "access-token": [] + } + ], + "summary": "Update, archive or restore an employee", + "tags": [ + "Employees" + ] + } + }, "/timesheets": { "post": { "operationId": "TimesheetsController_create", @@ -1193,208 +1515,6 @@ ] } }, - "/employees": { - "post": { - "operationId": "EmployeesController_create", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateEmployeeDto" - } - } - } - }, - "responses": { - "201": { - "description": "Employee created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmployeeEntity" - } - } - } - }, - "400": { - "description": "Incomplete task or invalid data" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Create employee", - "tags": [ - "Employees" - ] - }, - "get": { - "operationId": "EmployeesController_findAll", - "parameters": [], - "responses": { - "200": { - "description": "List of employees found", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EmployeeEntity" - } - } - } - } - }, - "400": { - "description": "List of employees not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find all employees", - "tags": [ - "Employees" - ] - } - }, - "/employees/{id}": { - "get": { - "operationId": "EmployeesController_findOne", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "200": { - "description": "Employee found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmployeeEntity" - } - } - } - }, - "400": { - "description": "Employee not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find employee", - "tags": [ - "Employees" - ] - }, - "delete": { - "operationId": "EmployeesController_remove", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "description": "Identifier of the employee to delete", - "schema": { - "type": "number" - } - } - ], - "responses": { - "204": { - "description": "Employee deleted" - }, - "404": { - "description": "Employee not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Delete employee", - "tags": [ - "Employees" - ] - }, - "patch": { - "operationId": "EmployeesController_updateOrArchiveOrRestore", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "description": "Identifier of the employee", - "schema": { - "type": "number" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateEmployeeDto" - } - } - } - }, - "responses": { - "200": { - "description": "Employee updated or restored", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmployeeEntity" - } - } - } - }, - "202": { - "description": "Employee archived successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmployeeEntity" - } - } - } - }, - "404": { - "description": "Employee not found in active or archive" - } - }, - "security": [ - { - "access-token": [] - }, - { - "access-token": [] - } - ], - "summary": "Update, archive or restore an employee", - "tags": [ - "Employees" - ] - } - }, "/expense-codes": { "post": { "operationId": "ExpenseCodesController_create", @@ -1999,6 +2119,167 @@ } }, "schemas": { + "CreateEmployeeDto": { + "type": "object", + "properties": { + "first_name": { + "type": "string", + "example": "Frodo", + "description": "Employee`s first name" + }, + "last_name": { + "type": "string", + "example": "Baggins", + "description": "Employee`s last name" + }, + "email": { + "type": "string", + "example": "i_cant_do_this_sam@targointernet.com", + "description": "Employee`s email" + }, + "phone_number": { + "type": "number", + "example": "82538437464", + "description": "Employee`s phone number" + }, + "residence": { + "type": "string", + "example": "1 Bagshot Row, Hobbiton, The Shire, Middle-earth", + "description": "Employee`s residence" + }, + "external_payroll_id": { + "type": "number", + "example": "BagginsF7464", + "description": "Employee`s payroll id" + }, + "company_code": { + "type": "number", + "example": "335567447", + "description": "Employee`s company code" + }, + "first_work_day": { + "format": "date-time", + "type": "string", + "example": "23/09/3018", + "description": "Employee`s first working day" + }, + "last_work_day": { + "format": "date-time", + "type": "string", + "example": "25/03/3019", + "description": "Employee`s last working day" + } + }, + "required": [ + "first_name", + "last_name", + "email", + "phone_number", + "external_payroll_id", + "company_code", + "first_work_day" + ] + }, + "EmployeeEntity": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1, + "description": "Unique ID of an employee(primary-key, auto-incremented)" + }, + "user_id": { + "type": "string", + "example": "0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d", + "description": "UUID of the user linked to that employee" + }, + "external_payroll_id": { + "type": "number", + "example": 7464, + "description": "external ID for the pay system" + }, + "company_code": { + "type": "number", + "example": 335567447, + "description": "company code" + }, + "first_work_day": { + "format": "date-time", + "type": "string", + "example": "3018-09-23T00:00:00.000Z", + "description": "Employee first day at work" + }, + "last_work_day": { + "format": "date-time", + "type": "string", + "example": "3019-03-25T00:00:00.000Z", + "description": "Employee last day at work" + } + }, + "required": [ + "id", + "user_id", + "external_payroll_id", + "company_code", + "first_work_day" + ] + }, + "UpdateEmployeeDto": { + "type": "object", + "properties": { + "first_name": { + "type": "string", + "example": "Frodo", + "description": "Employee`s first name" + }, + "last_name": { + "type": "string", + "example": "Baggins", + "description": "Employee`s last name" + }, + "email": { + "type": "string", + "example": "i_cant_do_this_sam@targointernet.com", + "description": "Employee`s email" + }, + "phone_number": { + "type": "number", + "example": "82538437464", + "description": "Employee`s phone number" + }, + "residence": { + "type": "string", + "example": "1 Bagshot Row, Hobbiton, The Shire, Middle-earth", + "description": "Employee`s residence" + }, + "external_payroll_id": { + "type": "number", + "example": "BagginsF7464", + "description": "Employee`s payroll id" + }, + "company_code": { + "type": "number", + "example": "335567447", + "description": "Employee`s company code" + }, + "first_work_day": { + "format": "date-time", + "type": "string", + "example": "23/09/3018", + "description": "New hire date or undefined" + }, + "last_work_day": { + "format": "date-time", + "type": "string", + "example": "25/03/3019", + "description": "Termination date (null to restore)" + }, + "supervisor_id": { + "type": "number", + "description": "Supervisor ID" + } + } + }, "CreateTimesheetDto": { "type": "object", "properties": { @@ -2770,167 +3051,6 @@ } } }, - "CreateEmployeeDto": { - "type": "object", - "properties": { - "first_name": { - "type": "string", - "example": "Frodo", - "description": "Employee`s first name" - }, - "last_name": { - "type": "string", - "example": "Baggins", - "description": "Employee`s last name" - }, - "email": { - "type": "string", - "example": "i_cant_do_this_sam@targointernet.com", - "description": "Employee`s email" - }, - "phone_number": { - "type": "number", - "example": "82538437464", - "description": "Employee`s phone number" - }, - "residence": { - "type": "string", - "example": "1 Bagshot Row, Hobbiton, The Shire, Middle-earth", - "description": "Employee`s residence" - }, - "external_payroll_id": { - "type": "number", - "example": "BagginsF7464", - "description": "Employee`s payroll id" - }, - "company_code": { - "type": "number", - "example": "335567447", - "description": "Employee`s company code" - }, - "first_work_day": { - "format": "date-time", - "type": "string", - "example": "23/09/3018", - "description": "Employee`s first working day" - }, - "last_work_day": { - "format": "date-time", - "type": "string", - "example": "25/03/3019", - "description": "Employee`s last working day" - } - }, - "required": [ - "first_name", - "last_name", - "email", - "phone_number", - "external_payroll_id", - "company_code", - "first_work_day" - ] - }, - "EmployeeEntity": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1, - "description": "Unique ID of an employee(primary-key, auto-incremented)" - }, - "user_id": { - "type": "string", - "example": "0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d", - "description": "UUID of the user linked to that employee" - }, - "external_payroll_id": { - "type": "number", - "example": 7464, - "description": "external ID for the pay system" - }, - "company_code": { - "type": "number", - "example": 335567447, - "description": "company code" - }, - "first_work_day": { - "format": "date-time", - "type": "string", - "example": "3018-09-23T00:00:00.000Z", - "description": "Employee first day at work" - }, - "last_work_day": { - "format": "date-time", - "type": "string", - "example": "3019-03-25T00:00:00.000Z", - "description": "Employee last day at work" - } - }, - "required": [ - "id", - "user_id", - "external_payroll_id", - "company_code", - "first_work_day" - ] - }, - "UpdateEmployeeDto": { - "type": "object", - "properties": { - "first_name": { - "type": "string", - "example": "Frodo", - "description": "Employee`s first name" - }, - "last_name": { - "type": "string", - "example": "Baggins", - "description": "Employee`s last name" - }, - "email": { - "type": "string", - "example": "i_cant_do_this_sam@targointernet.com", - "description": "Employee`s email" - }, - "phone_number": { - "type": "number", - "example": "82538437464", - "description": "Employee`s phone number" - }, - "residence": { - "type": "string", - "example": "1 Bagshot Row, Hobbiton, The Shire, Middle-earth", - "description": "Employee`s residence" - }, - "external_payroll_id": { - "type": "number", - "example": "BagginsF7464", - "description": "Employee`s payroll id" - }, - "company_code": { - "type": "number", - "example": "335567447", - "description": "Employee`s company code" - }, - "first_work_day": { - "format": "date-time", - "type": "string", - "example": "23/09/3018", - "description": "New hire date or undefined" - }, - "last_work_day": { - "format": "date-time", - "type": "string", - "example": "25/03/3019", - "description": "Termination date (null to restore)" - }, - "supervisor_id": { - "type": "number", - "description": "Supervisor ID" - } - } - }, "CreateExpenseCodeDto": { "type": "object", "properties": { diff --git a/src/modules/archival/archival.module.ts b/src/modules/archival/archival.module.ts index 6c2dd28..7a8b73a 100644 --- a/src/modules/archival/archival.module.ts +++ b/src/modules/archival/archival.module.ts @@ -10,9 +10,11 @@ import { ExpensesArchiveController } from "./controllers/expenses-archive.contro import { LeaveRequestsArchiveController } from "./controllers/leave-requests-archive.controller"; import { ShiftsArchiveController } from "./controllers/shifts-archive.controller"; import { TimesheetsArchiveController } from "./controllers/timesheets-archive.controller"; +import { EmployeesModule } from "../employees/employees.module"; @Module({ imports: [ + EmployeesModule, ScheduleModule, TimesheetsModule, ExpensesModule, diff --git a/src/modules/employees/employees.module.ts b/src/modules/employees/employees.module.ts index 6e07446..22e5cc6 100644 --- a/src/modules/employees/employees.module.ts +++ b/src/modules/employees/employees.module.ts @@ -5,5 +5,6 @@ import { EmployeesService } from './services/employees.service'; @Module({ controllers: [EmployeesController], providers: [EmployeesService], + exports: [EmployeesService], }) export class EmployeesModule {} From ec98adbcc3dd9efa4c220d8ad0255f2de8e61249 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 30 Jul 2025 12:57:54 -0400 Subject: [PATCH 10/21] feat(prisma): merged shift_codes and expense_codes into bank_codes --- prisma/schema.prisma | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8363ea0..fcca23f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -102,6 +102,8 @@ model LeaveRequests { id Int @id @default(autoincrement()) employee Employees @relation("LeaveRequestEmployee", fields: [employee_id], references: [id]) employee_id Int + bank_code BankCodes? @relation("LeaveRequestBankCodes", fields: [bank_code_id], references: [id]) + bank_code_id Int leave_type LeaveTypes start_date_time DateTime @db.Date end_date_time DateTime? @db.Date @@ -130,7 +132,7 @@ model LeaveRequestsArchive { //pay-period vue view PayPeriods { - period_number Int @id + period_number Int start_date DateTime @db.Date end_date DateTime @db.Date year Int @@ -167,8 +169,8 @@ model Shifts { id Int @id @default(autoincrement()) timesheet Timesheets @relation("ShiftTimesheet", fields: [timesheet_id], references: [id]) timesheet_id Int - shift_code ShiftCodes @relation("ShiftShiftCode", fields: [shift_code_id], references: [id]) - shift_code_id Int + shift_code BankCodes @relation("ShiftBankCodes", fields: [bank_code_id], references: [id]) + bank_code_id Int description String? date DateTime @db.Date start_time DateTime @db.Time(0) @@ -194,22 +196,26 @@ model ShiftsArchive { @@map("shifts_archive") } -model ShiftCodes { - id Int @id @default(autoincrement()) - shift_type String - bank_code String +model BankCodes { + id Int @id @default(autoincrement()) + type String + categorie String + modifier Float + bank_code String - shift Shifts[] @relation("ShiftShiftCode") + shifts Shifts[] @relation("ShiftBankCodes") + expenses Expenses[] @relation("ExpenseBankCodes") + leaveRequests LeaveRequests[] @relation("LeaveRequestBankCodes") - @@map("shift_codes") + @@map("bank_codes") } model Expenses { id Int @id @default(autoincrement()) timesheet Timesheets @relation("ExpensesTimesheet", fields: [timesheet_id], references: [id]) timesheet_id Int - expense_code ExpenseCodes @relation("ExpenseExpenseCode", fields: [expense_code_id], references: [id]) - expense_code_id Int + bank_code BankCodes @relation("ExpenseBankCodes", fields: [bank_code_id], references: [id]) + bank_code_id Int date DateTime @db.Date amount Decimal @db.Money attachement String? @@ -239,16 +245,6 @@ model ExpensesArchive { @@map("expenses_archive") } -model ExpenseCodes { - id Int @id @default(autoincrement()) - expense_type String - bank_code String - - expense Expenses[] @relation("ExpenseExpenseCode") - - @@map("expense_codes") -} - model OAuthAccessTokens { id String @id @default(cuid()) user Users @relation("UserOAuthAccessToken", fields: [user_id], references: [id]) From f85a213561dfddbf5887cf6b97f648e0b5e0ded3 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 30 Jul 2025 13:15:46 -0400 Subject: [PATCH 11/21] fix(prisma): small pay-period fix --- .../migration.sql | 54 +++++++++++++ prisma/schema.prisma | 76 +++++++++---------- 2 files changed, 92 insertions(+), 38 deletions(-) create mode 100644 prisma/migrations/20250730171515_consolidate_bank_codes/migration.sql diff --git a/prisma/migrations/20250730171515_consolidate_bank_codes/migration.sql b/prisma/migrations/20250730171515_consolidate_bank_codes/migration.sql new file mode 100644 index 0000000..71d2dbf --- /dev/null +++ b/prisma/migrations/20250730171515_consolidate_bank_codes/migration.sql @@ -0,0 +1,54 @@ +/* + Warnings: + + - You are about to drop the column `expense_code_id` on the `expenses` table. All the data in the column will be lost. + - You are about to drop the column `shift_code_id` on the `shifts` table. All the data in the column will be lost. + - You are about to drop the `expense_codes` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `shift_codes` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `bank_code_id` to the `expenses` table without a default value. This is not possible if the table is not empty. + - Added the required column `bank_code_id` to the `leave_requests` table without a default value. This is not possible if the table is not empty. + - Added the required column `bank_code_id` to the `shifts` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "expenses" DROP CONSTRAINT "expenses_expense_code_id_fkey"; + +-- DropForeignKey +ALTER TABLE "shifts" DROP CONSTRAINT "shifts_shift_code_id_fkey"; + +-- AlterTable +ALTER TABLE "expenses" DROP COLUMN "expense_code_id", +ADD COLUMN "bank_code_id" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "leave_requests" ADD COLUMN "bank_code_id" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "shifts" DROP COLUMN "shift_code_id", +ADD COLUMN "bank_code_id" INTEGER NOT NULL; + +-- DropTable +DROP TABLE "expense_codes"; + +-- DropTable +DROP TABLE "shift_codes"; + +-- CreateTable +CREATE TABLE "bank_codes" ( + "id" SERIAL NOT NULL, + "type" TEXT NOT NULL, + "categorie" TEXT NOT NULL, + "modifier" DOUBLE PRECISION NOT NULL, + "bank_code" TEXT NOT NULL, + + CONSTRAINT "bank_codes_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "leave_requests" ADD CONSTRAINT "leave_requests_bank_code_id_fkey" FOREIGN KEY ("bank_code_id") REFERENCES "bank_codes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "shifts" ADD CONSTRAINT "shifts_bank_code_id_fkey" FOREIGN KEY ("bank_code_id") REFERENCES "bank_codes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "expenses" ADD CONSTRAINT "expenses_bank_code_id_fkey" FOREIGN KEY ("bank_code_id") REFERENCES "bank_codes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fcca23f..dabd4ce 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -5,7 +5,7 @@ // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" previewFeatures = ["views"] } @@ -26,8 +26,8 @@ model Users { employee Employees? @relation("UserEmployee") customer Customers? @relation("UserCustomer") oauth_access_tokens OAuthAccessTokens[] @relation("UserOAuthAccessToken") - employees_archive EmployeesArchive[] @relation("UsersToEmployeesToArchive") - customer_archive CustomersArchive[] @relation("UserToCustomersToArchive") + employees_archive EmployeesArchive[] @relation("UsersToEmployeesToArchive") + customer_archive CustomersArchive[] @relation("UserToCustomersToArchive") @@map("users") } @@ -41,9 +41,9 @@ model Employees { first_work_day DateTime @db.Date last_work_day DateTime? @db.Date - supervisor Employees? @relation("EmployeeSupervisor", fields: [supervisor_id], references: [id]) - supervisor_id Int? - crew Employees[] @relation("EmployeeSupervisor") + supervisor Employees? @relation("EmployeeSupervisor", fields: [supervisor_id], references: [id]) + supervisor_id Int? + crew Employees[] @relation("EmployeeSupervisor") archive EmployeesArchive[] @relation("EmployeeToArchive") timesheet Timesheets[] @relation("TimesheetEmployee") @@ -54,20 +54,20 @@ model Employees { } model EmployeesArchive { - id Int @id @default(autoincrement()) - employee Employees @relation("EmployeeToArchive", fields: [employee_id], references: [id]) - employee_id Int - archived_at DateTime @default(now()) + id Int @id @default(autoincrement()) + employee Employees @relation("EmployeeToArchive", fields: [employee_id], references: [id]) + employee_id Int + archived_at DateTime @default(now()) - user_id String @db.Uuid - user Users @relation("UsersToEmployeesToArchive",fields: [user_id], references: [id]) - first_name String - last_name String + user_id String @db.Uuid + user Users @relation("UsersToEmployeesToArchive", fields: [user_id], references: [id]) + first_name String + last_name String external_payroll_id Int company_code Int - first_Work_Day DateTime @db.Date - last_work_day DateTime @db.Date + first_Work_Day DateTime @db.Date + last_work_day DateTime @db.Date supervisor_id Int? supervisor Employees? @relation("EmployeeSupervisorToArchive", fields: [supervisor_id], references: [id]) @@ -93,7 +93,7 @@ model CustomersArchive { user_id String @db.Uuid user Users @relation("UserToCustomersToArchive", fields: [user_id], references: [id]) - invoice_id Int? @unique + invoice_id Int? @unique @@map("customers_archive") } @@ -102,7 +102,7 @@ model LeaveRequests { id Int @id @default(autoincrement()) employee Employees @relation("LeaveRequestEmployee", fields: [employee_id], references: [id]) employee_id Int - bank_code BankCodes? @relation("LeaveRequestBankCodes", fields: [bank_code_id], references: [id]) + bank_code BankCodes? @relation("LeaveRequestBankCodes", fields: [bank_code_id], references: [id]) bank_code_id Int leave_type LeaveTypes start_date_time DateTime @db.Date @@ -132,7 +132,7 @@ model LeaveRequestsArchive { //pay-period vue view PayPeriods { - period_number Int + period_number Int @id start_date DateTime @db.Date end_date DateTime @db.Date year Int @@ -166,15 +166,15 @@ model TimesheetsArchive { } model Shifts { - id Int @id @default(autoincrement()) - timesheet Timesheets @relation("ShiftTimesheet", fields: [timesheet_id], references: [id]) - timesheet_id Int - shift_code BankCodes @relation("ShiftBankCodes", fields: [bank_code_id], references: [id]) - bank_code_id Int - description String? - date DateTime @db.Date - start_time DateTime @db.Time(0) - end_time DateTime @db.Time(0) + id Int @id @default(autoincrement()) + timesheet Timesheets @relation("ShiftTimesheet", fields: [timesheet_id], references: [id]) + timesheet_id Int + shift_code BankCodes @relation("ShiftBankCodes", fields: [bank_code_id], references: [id]) + bank_code_id Int + description String? + date DateTime @db.Date + start_time DateTime @db.Time(0) + end_time DateTime @db.Time(0) archive ShiftsArchive[] @relation("ShiftsToArchive") @@ -197,30 +197,30 @@ model ShiftsArchive { } model BankCodes { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) type String categorie String modifier Float bank_code String - shifts Shifts[] @relation("ShiftBankCodes") - expenses Expenses[] @relation("ExpenseBankCodes") - leaveRequests LeaveRequests[] @relation("LeaveRequestBankCodes") + shifts Shifts[] @relation("ShiftBankCodes") + expenses Expenses[] @relation("ExpenseBankCodes") + leaveRequests LeaveRequests[] @relation("LeaveRequestBankCodes") @@map("bank_codes") } model Expenses { - id Int @id @default(autoincrement()) - timesheet Timesheets @relation("ExpensesTimesheet", fields: [timesheet_id], references: [id]) + id Int @id @default(autoincrement()) + timesheet Timesheets @relation("ExpensesTimesheet", fields: [timesheet_id], references: [id]) timesheet_id Int - bank_code BankCodes @relation("ExpenseBankCodes", fields: [bank_code_id], references: [id]) + bank_code BankCodes @relation("ExpenseBankCodes", fields: [bank_code_id], references: [id]) bank_code_id Int - date DateTime @db.Date - amount Decimal @db.Money + date DateTime @db.Date + amount Decimal @db.Money attachement String? description String? - is_approved Boolean @default(false) + is_approved Boolean @default(false) supervisor_comment String? archive ExpensesArchive[] @relation("ExpensesToArchive") From f874d2c5c64191f8083deece2ead644b0fcbd972 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 30 Jul 2025 14:39:43 -0400 Subject: [PATCH 12/21] BREAKING CHANGE(bank_codes): shift-codes table and expenses-codes table deleted and refactoring of modules calling "x-codes" to use bank-codes --- .../migration.sql | 16 +++ prisma/schema.prisma | 26 ++--- src/modules/bank-codes/bank-codes.module.ts | 12 ++ .../controllers/bank-codes.controller.ts | 46 ++++++++ .../bank-codes/dtos/create-bank-codes.ts | 20 ++++ .../swagger-entities/bank_codes.entity.ts | 34 ++++++ .../bank-codes/dtos/update-bank-codes.ts | 4 + .../services/bank-codes.services.ts | 38 ++++++ .../controllers/expense-codes.controller.ts | 64 ---------- .../expense-codes/dtos/create-expense-code.ts | 21 ---- .../swagger-entities/expense-codes.entity.ts | 21 ---- .../expense-codes/dtos/update-expense-code.ts | 4 - .../expense-codes/expense-codes.module.ts | 11 -- .../services/expense-codes.service.ts | 99 ---------------- src/modules/expenses/dtos/create-expense.ts | 31 +---- .../dtos/swagger-entities/expenses.entity.ts | 4 +- .../expenses/services/expenses.service.ts | 16 +-- .../controllers/shift-codes.controller.ts | 64 ---------- .../dtos/create-shift-codes.dto.ts | 21 ---- .../swagger-entities/shift-codes.entity.ts | 21 ---- .../dtos/update-shift-codes.dto.ts | 4 - .../services/shift-codes.service.ts | 109 ------------------ src/modules/shift-codes/shift-codes.module.ts | 11 -- src/modules/shifts/dtos/create-shifts.dto.ts | 27 +---- .../dtos/swagger-entities/shift.entity.ts | 4 +- src/modules/shifts/services/shifts.service.ts | 16 +-- 26 files changed, 205 insertions(+), 539 deletions(-) create mode 100644 prisma/migrations/20250730183752_fix_bank_codes_relations/migration.sql create mode 100644 src/modules/bank-codes/bank-codes.module.ts create mode 100644 src/modules/bank-codes/controllers/bank-codes.controller.ts create mode 100644 src/modules/bank-codes/dtos/create-bank-codes.ts create mode 100644 src/modules/bank-codes/dtos/swagger-entities/bank_codes.entity.ts create mode 100644 src/modules/bank-codes/dtos/update-bank-codes.ts create mode 100644 src/modules/bank-codes/services/bank-codes.services.ts delete mode 100644 src/modules/expense-codes/controllers/expense-codes.controller.ts delete mode 100644 src/modules/expense-codes/dtos/create-expense-code.ts delete mode 100644 src/modules/expense-codes/dtos/swagger-entities/expense-codes.entity.ts delete mode 100644 src/modules/expense-codes/dtos/update-expense-code.ts delete mode 100644 src/modules/expense-codes/expense-codes.module.ts delete mode 100644 src/modules/expense-codes/services/expense-codes.service.ts delete mode 100644 src/modules/shift-codes/controllers/shift-codes.controller.ts delete mode 100644 src/modules/shift-codes/dtos/create-shift-codes.dto.ts delete mode 100644 src/modules/shift-codes/dtos/swagger-entities/shift-codes.entity.ts delete mode 100644 src/modules/shift-codes/dtos/update-shift-codes.dto.ts delete mode 100644 src/modules/shift-codes/services/shift-codes.service.ts delete mode 100644 src/modules/shift-codes/shift-codes.module.ts diff --git a/prisma/migrations/20250730183752_fix_bank_codes_relations/migration.sql b/prisma/migrations/20250730183752_fix_bank_codes_relations/migration.sql new file mode 100644 index 0000000..0ee1569 --- /dev/null +++ b/prisma/migrations/20250730183752_fix_bank_codes_relations/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - You are about to drop the column `expense_code_id` on the `expenses_archive` table. All the data in the column will be lost. + - You are about to drop the column `shift_code_id` on the `shifts_archive` table. All the data in the column will be lost. + - Added the required column `bank_code_id` to the `expenses_archive` table without a default value. This is not possible if the table is not empty. + - Added the required column `bank_code_id` to the `shifts_archive` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "expenses_archive" DROP COLUMN "expense_code_id", +ADD COLUMN "bank_code_id" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "shifts_archive" DROP COLUMN "shift_code_id", +ADD COLUMN "bank_code_id" INTEGER NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dabd4ce..edb3ca1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -132,7 +132,7 @@ model LeaveRequestsArchive { //pay-period vue view PayPeriods { - period_number Int @id + period_number Int @id //do not try to fix it, Prisma is working on a fix for views start_date DateTime @db.Date end_date DateTime @db.Date year Int @@ -169,7 +169,7 @@ model Shifts { id Int @id @default(autoincrement()) timesheet Timesheets @relation("ShiftTimesheet", fields: [timesheet_id], references: [id]) timesheet_id Int - shift_code BankCodes @relation("ShiftBankCodes", fields: [bank_code_id], references: [id]) + bank_code BankCodes @relation("ShiftBankCodes", fields: [bank_code_id], references: [id]) bank_code_id Int description String? date DateTime @db.Date @@ -182,16 +182,16 @@ model Shifts { } model ShiftsArchive { - id Int @id @default(autoincrement()) - shift Shifts @relation("ShiftsToArchive", fields: [shift_id], references: [id]) - shift_id Int - archive_at DateTime @default(now()) - timesheet_id Int - shift_code_id Int - description String? - date DateTime @db.Date - start_time DateTime @db.Time(0) - end_time DateTime @db.Time(0) + id Int @id @default(autoincrement()) + shift Shifts @relation("ShiftsToArchive", fields: [shift_id], references: [id]) + shift_id Int + archive_at DateTime @default(now()) + timesheet_id Int + bank_code_id Int + description String? + date DateTime @db.Date + start_time DateTime @db.Time(0) + end_time DateTime @db.Time(0) @@map("shifts_archive") } @@ -234,7 +234,7 @@ model ExpensesArchive { expense_id Int timesheet_id Int archived_at DateTime @default(now()) - expense_code_id Int + bank_code_id Int date DateTime @db.Date amount Decimal @db.Money attachement String? diff --git a/src/modules/bank-codes/bank-codes.module.ts b/src/modules/bank-codes/bank-codes.module.ts new file mode 100644 index 0000000..a8c4ced --- /dev/null +++ b/src/modules/bank-codes/bank-codes.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; + +import { PrismaService } from "src/prisma/prisma.service"; +import { BankCodesControllers } from "./controllers/bank-codes.controller"; +import { BankCodesService } from "./services/bank-codes.services"; + +@Module({ + controllers: [BankCodesControllers], + providers: [BankCodesService, PrismaService], +}) + +export class ShiftCodesModule {} \ No newline at end of file diff --git a/src/modules/bank-codes/controllers/bank-codes.controller.ts b/src/modules/bank-codes/controllers/bank-codes.controller.ts new file mode 100644 index 0000000..f8a44fa --- /dev/null +++ b/src/modules/bank-codes/controllers/bank-codes.controller.ts @@ -0,0 +1,46 @@ +import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from "@nestjs/common"; +import { BankCodesService } from "../services/bank-codes.services"; +import { CreateBankCodeDto } from "../dtos/create-bank-codes"; +import { UpdateBankCodeDto } from "../dtos/update-bank-codes"; +import { ApiBadRequestResponse, ApiNotFoundResponse, ApiOperation, ApiResponse } from "@nestjs/swagger"; + +@Controller('bank-codes') +export class BankCodesControllers { + constructor(private readonly bankCodesService: BankCodesService) {} + + @Post() + @ApiOperation({ summary: 'Create a new bank code' }) + @ApiResponse({ status: 201, description: 'Bank code successfully created.' }) + @ApiBadRequestResponse({ description: 'Invalid input data.' }) + create(@Body() dto: CreateBankCodeDto) { + return this.bankCodesService.create(dto); + } + + @Get() + @ApiOperation({ summary: 'Retrieve all bank codes' }) + @ApiResponse({ status: 200, description: 'List of bank codes.' }) + findAll() { + return this.bankCodesService.findAll(); + } + + @Get(':id') + @ApiOperation({ summary: 'Retrieve a bank code by its ID' }) + @ApiNotFoundResponse({ description: 'Bank code not found.' }) + findOne(@Param('id', ParseIntPipe) id: number){ + return this.bankCodesService.findOne(id); + } + + @Patch(':id') + @ApiOperation({ summary: 'Update an existing bank code' }) + @ApiNotFoundResponse({ description: 'Bank code not found.' }) + update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateBankCodeDto) { + return this.bankCodesService.update(id, dto) + } + + @Delete(':id') + @ApiOperation({ summary: 'Delete a bank code' }) + @ApiNotFoundResponse({ description: 'Bank code not found.' }) + remove(@Param('id', ParseIntPipe) id: number) { + return this.bankCodesService.remove(id); + } +} \ No newline at end of file diff --git a/src/modules/bank-codes/dtos/create-bank-codes.ts b/src/modules/bank-codes/dtos/create-bank-codes.ts new file mode 100644 index 0000000..fe3807d --- /dev/null +++ b/src/modules/bank-codes/dtos/create-bank-codes.ts @@ -0,0 +1,20 @@ +import { IsNotEmpty, IsNumber, IsString } from "class-validator"; + +export class CreateBankCodeDto { + + @IsString() + @IsNotEmpty() + type: string; + + @IsString() + @IsNotEmpty() + categorie: string; + + @IsNumber() + @IsNotEmpty() + modifier: number; + + @IsString() + @IsNotEmpty() + bank_code: string; +} \ No newline at end of file diff --git a/src/modules/bank-codes/dtos/swagger-entities/bank_codes.entity.ts b/src/modules/bank-codes/dtos/swagger-entities/bank_codes.entity.ts new file mode 100644 index 0000000..c3f6686 --- /dev/null +++ b/src/modules/bank-codes/dtos/swagger-entities/bank_codes.entity.ts @@ -0,0 +1,34 @@ +import { ApiProperty } from "@nestjs/swagger"; + +export class BankCodesEntity { + + @ApiProperty({ + example: 1, + description: 'Unique ID of a bank-code (auto-generated)', + }) + id: number; + + @ApiProperty({ + example: 'regular, vacation, emergency, sick, parental, etc', + description: 'Type of codes', + }) + type: string; + + @ApiProperty({ + example: 'shift, expense, leave', + description: 'categorie of the related code', + }) + categorie: string; + + @ApiProperty({ + example: '0, 0.72, 1, 1.5, 2', + description: 'modifier number to apply to salary', + }) + modifier: number; + + @ApiProperty({ + example: 'G1, G345, G501, G43, G700', + description: 'codes given by the bank', + }) + bank_code: string; +} \ No newline at end of file diff --git a/src/modules/bank-codes/dtos/update-bank-codes.ts b/src/modules/bank-codes/dtos/update-bank-codes.ts new file mode 100644 index 0000000..658ba81 --- /dev/null +++ b/src/modules/bank-codes/dtos/update-bank-codes.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateBankCodeDto } from "./create-bank-codes"; + +export class UpdateBankCodeDto extends PartialType(CreateBankCodeDto) {} \ No newline at end of file diff --git a/src/modules/bank-codes/services/bank-codes.services.ts b/src/modules/bank-codes/services/bank-codes.services.ts new file mode 100644 index 0000000..71031de --- /dev/null +++ b/src/modules/bank-codes/services/bank-codes.services.ts @@ -0,0 +1,38 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; +import { CreateBankCodeDto } from "../dtos/create-bank-codes"; +import { BankCodes } from "@prisma/client"; +import { UpdateBankCodeDto } from "../dtos/update-bank-codes"; + +@Injectable() +export class BankCodesService { + constructor(private readonly prisma: PrismaService) {} + + async create(dto: CreateBankCodeDto): Promise{ + return this.prisma.bankCodes.create({ data: dto }) + } + + findAll() { + return this.prisma.bankCodes.findMany(); + } + + async findOne(id: number) { + const bankCode = await this.prisma.bankCodes.findUnique({ where: {id} }); + + if(!bankCode) { + throw new NotFoundException(`Bank Code #${id} not found`); + } + + return bankCode; + } + + async update(id:number, dto: UpdateBankCodeDto) { + await this.prisma.bankCodes.update({ where: { id }, data: dto }); + } + + async remove(id: number) { + await this.findOne(id); + return this.prisma.bankCodes.delete({ where: {id} }); + } + +} \ No newline at end of file diff --git a/src/modules/expense-codes/controllers/expense-codes.controller.ts b/src/modules/expense-codes/controllers/expense-codes.controller.ts deleted file mode 100644 index e120ca8..0000000 --- a/src/modules/expense-codes/controllers/expense-codes.controller.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common"; -import { ExpenseCodesService } from "../services/expense-codes.service"; -import { ApiTags, ApiBearerAuth, ApiOperation, ApiResponse } from "@nestjs/swagger"; -import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard"; -import { ExpenseCodes } from "@prisma/client"; -import { Roles as RoleEnum } from '.prisma/client'; -import { RolesAllowed } from "src/common/decorators/roles.decorators"; -import { CreateExpenseCodeDto } from "../dtos/create-expense-code"; -import { ExpenseCodesEntity } from "../dtos/swagger-entities/expense-codes.entity"; -import { UpdateExpenseCodeDto } from "../dtos/update-expense-code"; - -@ApiTags('Expense Codes') -@ApiBearerAuth('access-token') -@UseGuards(JwtAuthGuard) -@Controller('expense-codes') -export class ExpenseCodesController { - constructor(private readonly expenseCodesService: ExpenseCodesService) {} - - @Post() - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR) - @ApiOperation({ summary: 'Create expense code' }) - @ApiResponse({ status: 201, description: 'Expense code created',type: ExpenseCodesEntity }) - @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) - create(@Body()dto: CreateExpenseCodeDto): Promise { - return this.expenseCodesService.create(dto); - } - - @Get() - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR) - @ApiOperation({ summary: 'Find all expense codes' }) - @ApiResponse({ status: 201, description: 'List of expense codes found',type: ExpenseCodesEntity, isArray: true }) - @ApiResponse({ status: 400, description: 'List of expense codes not found' }) - findAll(): Promise { - return this.expenseCodesService.findAll(); - } - - @Get(':id') - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR) - @ApiOperation({ summary: 'Find expense code' }) - @ApiResponse({ status: 201, description: 'Expense code found',type: ExpenseCodesEntity }) - @ApiResponse({ status: 400, description: 'Expense code not found' }) - findOne(@Param('id', ParseIntPipe) id: number): Promise { - return this.expenseCodesService.findOne(id); - } - - @Patch(':id') - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR) - @ApiOperation({ summary: 'Update expense code' }) - @ApiResponse({ status: 201, description: 'Expense code updated',type: ExpenseCodesEntity }) - @ApiResponse({ status: 400, description: 'Expense code not found' }) - update(@Param('id', ParseIntPipe) id: number, - @Body() dto: UpdateExpenseCodeDto): Promise { - return this.expenseCodesService.update(id,dto); - } - - @Delete(':id') - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR) - @ApiOperation({ summary: 'Delete expense code' }) - @ApiResponse({ status: 201, description: 'Expense code deleted',type: ExpenseCodesEntity }) - @ApiResponse({ status: 400, description: 'Expense code not found' }) - remove(@Param('id', ParseIntPipe)id: number): Promise { - return this.expenseCodesService.remove(id); - } -} \ No newline at end of file diff --git a/src/modules/expense-codes/dtos/create-expense-code.ts b/src/modules/expense-codes/dtos/create-expense-code.ts deleted file mode 100644 index 7e8bf90..0000000 --- a/src/modules/expense-codes/dtos/create-expense-code.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsNotEmpty, IsString } from "class-validator"; - -export class CreateExpenseCodeDto { - - @ApiProperty({ - example:'mileage, overnight, etc...', - description: 'Type of expenses for an account perception', - }) - @IsString() - @IsNotEmpty() - expense_type: string; - - @ApiProperty({ - example: 'G500, G501, etc...', - description: 'bank`s code related to the type of expense', - }) - @IsString() - @IsNotEmpty() - bank_code: string; -} \ No newline at end of file diff --git a/src/modules/expense-codes/dtos/swagger-entities/expense-codes.entity.ts b/src/modules/expense-codes/dtos/swagger-entities/expense-codes.entity.ts deleted file mode 100644 index 08a5aec..0000000 --- a/src/modules/expense-codes/dtos/swagger-entities/expense-codes.entity.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; - -export class ExpenseCodesEntity { - @ApiProperty({ - example: 1, - description: 'Unique ID of a expense-code (auto-generated)', - }) - id: number; - - @ApiProperty({ - example: 'Mileage', - description: 'Type of expenses for an account perception', - }) - shift_type: string; - - @ApiProperty({ - example: 'G501', - description: 'bank`s code related to the type of expense', - }) - bank_code: string; -} \ No newline at end of file diff --git a/src/modules/expense-codes/dtos/update-expense-code.ts b/src/modules/expense-codes/dtos/update-expense-code.ts deleted file mode 100644 index f2413e3..0000000 --- a/src/modules/expense-codes/dtos/update-expense-code.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from "@nestjs/swagger"; -import { CreateExpenseCodeDto } from "./create-expense-code"; - -export class UpdateExpenseCodeDto extends PartialType(CreateExpenseCodeDto) {} \ No newline at end of file diff --git a/src/modules/expense-codes/expense-codes.module.ts b/src/modules/expense-codes/expense-codes.module.ts deleted file mode 100644 index 92e5d77..0000000 --- a/src/modules/expense-codes/expense-codes.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from "@nestjs/common"; -import { PrismaService } from "src/prisma/prisma.service"; -import { ExpenseCodesController } from "./controllers/expense-codes.controller"; -import { ExpenseCodesService } from "./services/expense-codes.service"; - -@Module({ - controllers: [ExpenseCodesController], - providers: [ExpenseCodesService, PrismaService] -}) - -export class ExpenseCodesModule {} \ No newline at end of file diff --git a/src/modules/expense-codes/services/expense-codes.service.ts b/src/modules/expense-codes/services/expense-codes.service.ts deleted file mode 100644 index 4428aaa..0000000 --- a/src/modules/expense-codes/services/expense-codes.service.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; -import { PrismaService } from "src/prisma/prisma.service"; -import { CreateExpenseCodeDto } from "../dtos/create-expense-code"; -import { ExpenseCodes } from "@prisma/client"; -import { UpdateExpenseCodeDto } from "../dtos/update-expense-code"; - -@Injectable() -export class ExpenseCodesService { - constructor(private readonly prisma: PrismaService) {} - - async create(dto: CreateExpenseCodeDto): Promise { - const { expense_type, bank_code } = dto; - return this.prisma.expenseCodes.create({ - data: { expense_type, bank_code }, - include: { - expense: { - include: { - timesheet: { - include: { - employee: { include: { user: true } }, - }, - }, - }, - }, - }, - }); - } - - findAll(): Promise { - return this.prisma.expenseCodes.findMany({ - include: { - expense: { - include: { - timesheet: { - include: { - employee: { - include: { user: true } - }, - }, - }, - }, - }, - }, - }); - } - - async findOne(id: number): Promise { - const record = await this.prisma.expenseCodes.findUnique({ - where: { id }, - include: { - expense: { - include: { - timesheet: { - include: { - employee: { - include: { user:true }, - }, - }, - }, - }, - }, - }, - }); - if(!record) { - throw new NotFoundException(`ExpenseCode #${id} not found`); - } - return record; - } - - async update(id: number, dto: UpdateExpenseCodeDto): Promise { - await this.findOne(id); - const { expense_type, bank_code } = dto; - return this.prisma.expenseCodes.update({ - where: { id }, - data: { - ...(expense_type !== undefined && { expense_type }), - ...(bank_code !== undefined && { bank_code }), - }, - include: { - expense: { - include: { - timesheet: { - include: { - employee: { - include: { user: true }, - }, - }, - }, - }, - }, - }, - }); - } - - async remove(id: number): Promise { - await this.findOne(id); - return this.prisma.expenseCodes.delete({ where: { id } }); - } -} \ No newline at end of file diff --git a/src/modules/expenses/dtos/create-expense.ts b/src/modules/expenses/dtos/create-expense.ts index b2037b8..a9a02c4 100644 --- a/src/modules/expenses/dtos/create-expense.ts +++ b/src/modules/expenses/dtos/create-expense.ts @@ -1,61 +1,32 @@ -import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsBoolean, IsDate, IsDateString, IsInt, IsOptional, IsString } from "class-validator"; export class CreateExpenseDto { - @ApiProperty({ - example: 'Th3F3110w5h1pX2024', - description: 'ID number for a set timesheet', - }) @Type(()=> Number) @IsInt() timesheet_id: number; - @ApiProperty({ - example: '0n3R1n962Ru13xX', - description: 'ID number of an expense code (link with shift-codes)', - }) @Type(() => Number) @IsInt() - expense_code_id: number; + bank_code_id: number; - @ApiProperty({ - example: '20/10/3018', - description: 'Date where the expense was made', - }) @IsDateString() @Type(() => Date) @IsDate() date: Date; - @ApiProperty({ - example: '280 000 000,00', - description: 'Amount of the expense', - }) @Type(() => Number) @IsInt() amount: number - @ApiProperty({ - example:'Spent for mileage between A and B', - description:'explain`s why the expense was made' - }) @IsString() description?: string; - @ApiProperty({ - example: 'True or False or Pending or Denied or Cancelled or Escalated', - description: 'Expense`s approval status', - }) @IsOptional() @IsBoolean() is_approved?: boolean; - @ApiProperty({ - example:'Asked X to go there as an emergency response', - description:'Supervisro`s justification for the spending of an employee' - }) @IsString() supervisor_comment?: string; } diff --git a/src/modules/expenses/dtos/swagger-entities/expenses.entity.ts b/src/modules/expenses/dtos/swagger-entities/expenses.entity.ts index 2fbdbcd..3cd3983 100644 --- a/src/modules/expenses/dtos/swagger-entities/expenses.entity.ts +++ b/src/modules/expenses/dtos/swagger-entities/expenses.entity.ts @@ -16,9 +16,9 @@ export class ExpenseEntity { @ApiProperty({ example: 7, - description: 'ID number of an expense code (link with expense-codes)', + description: 'ID number of an bank code (link with bank-codes)', }) - expense: number; + bank_code_id: number; @ApiProperty({ example: '3018-10-20T00:00:00.000Z', diff --git a/src/modules/expenses/services/expenses.service.ts b/src/modules/expenses/services/expenses.service.ts index 9f8c0e2..a5b00b2 100644 --- a/src/modules/expenses/services/expenses.service.ts +++ b/src/modules/expenses/services/expenses.service.ts @@ -9,17 +9,17 @@ export class ExpensesService { constructor(private readonly prisma: PrismaService) {} async create(dto: CreateExpenseDto): Promise { - const { timesheet_id, expense_code_id, date, amount, + const { timesheet_id, bank_code_id, date, amount, description, is_approved,supervisor_comment} = dto; return this.prisma.expenses.create({ - data: { timesheet_id, expense_code_id,date,amount,description,is_approved,supervisor_comment}, + data: { timesheet_id, bank_code_id,date,amount,description,is_approved,supervisor_comment}, include: { timesheet: { include: { employee: { include: { user: true } }, }, }, - expense_code: true, + bank_code: true, }, }); } @@ -45,7 +45,7 @@ export class ExpensesService { employee: { include: { user:true } }, }, }, - expense_code: true, + bank_code: true, }, }); if (!expense) { @@ -56,13 +56,13 @@ export class ExpensesService { async update(id: number, dto: UpdateExpenseDto): Promise { await this.findOne(id); - const { timesheet_id, expense_code_id, date, amount, + const { timesheet_id, bank_code_id, date, amount, description, is_approved, supervisor_comment} = dto; return this.prisma.expenses.update({ where: { id }, data: { ...(timesheet_id !== undefined && { timesheet_id}), - ...(expense_code_id !== undefined && { expense_code_id }), + ...(bank_code_id !== undefined && { bank_code_id }), ...(date !== undefined && { date }), ...(amount !== undefined && { amount }), ...(description !== undefined && { description }), @@ -77,7 +77,7 @@ export class ExpensesService { }, }, }, - expense_code: true, + bank_code: true, }, }); } @@ -116,7 +116,7 @@ export class ExpensesService { data: expensesToArchive.map(exp => ({ expense_id: exp.id, timesheet_id: exp.timesheet_id, - expense_code_id: exp.expense_code_id, + bank_code_id: exp.bank_code_id, date: exp.date, amount: exp.amount, attachement: exp.attachement, diff --git a/src/modules/shift-codes/controllers/shift-codes.controller.ts b/src/modules/shift-codes/controllers/shift-codes.controller.ts deleted file mode 100644 index 0b34a68..0000000 --- a/src/modules/shift-codes/controllers/shift-codes.controller.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common"; -import { ShiftCodesService } from "../services/shift-codes.service"; -import { CreateShiftCodeDto } from "../dtos/create-shift-codes.dto"; -import { ShiftCodes } from "@prisma/client"; -import { UpdateShiftCodeDto } from "../dtos/update-shift-codes.dto"; -import { RolesAllowed } from "src/common/decorators/roles.decorators"; -import { Roles as RoleEnum } from '.prisma/client'; -import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; -import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard"; -import { ShiftCodesEntity } from "../dtos/swagger-entities/shift-codes.entity"; - -@ApiTags('Shift Codes') -@ApiBearerAuth('access-token') -@UseGuards(JwtAuthGuard) -@Controller('shift-codes') -export class ShiftCodesController { - constructor(private readonly shiftCodesService: ShiftCodesService) {} - - @Post() - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR) - @ApiOperation({ summary: 'Create shift code' }) - @ApiResponse({ status: 201, description: 'Shift code created',type: ShiftCodesEntity }) - @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) - create(@Body()dto: CreateShiftCodeDto): Promise { - return this.shiftCodesService.create(dto); - } - - @Get() - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR) - @ApiOperation({ summary: 'Find all shift codes' }) - @ApiResponse({ status: 201, description: 'List of shift codes found',type: ShiftCodesEntity, isArray: true }) - @ApiResponse({ status: 400, description: 'List of shift codes not found' }) - findAll(): Promise { - return this.shiftCodesService.findAll(); - } - - @Get(':id') - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR) - @ApiOperation({ summary: 'Find shift code' }) - @ApiResponse({ status: 201, description: 'Shift code found',type: ShiftCodesEntity }) - @ApiResponse({ status: 400, description: 'Shift code not found' }) - findOne(@Param('id', ParseIntPipe) id: number): Promise { - return this.shiftCodesService.findOne(id); - } - - @Patch(':id') - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR) - @ApiOperation({ summary: 'Update shift code' }) - @ApiResponse({ status: 201, description: 'Shift code updated',type: ShiftCodesEntity }) - @ApiResponse({ status: 400, description: 'Shift code not found' }) - update(@Param('id', ParseIntPipe) id: number, - @Body() dto: UpdateShiftCodeDto): Promise { - return this.shiftCodesService.update(id,dto); - } - - @Delete(':id') - @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR) - @ApiOperation({ summary: 'Delete shift code' }) - @ApiResponse({ status: 201, description: 'Shift code deleted',type: ShiftCodesEntity }) - @ApiResponse({ status: 400, description: 'Shift code not found' }) - remove(@Param('id', ParseIntPipe)id: number): Promise { - return this.shiftCodesService.remove(id); - } -} diff --git a/src/modules/shift-codes/dtos/create-shift-codes.dto.ts b/src/modules/shift-codes/dtos/create-shift-codes.dto.ts deleted file mode 100644 index 5cdff72..0000000 --- a/src/modules/shift-codes/dtos/create-shift-codes.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsNotEmpty, IsString } from "class-validator"; - -export class CreateShiftCodeDto { - - @ApiProperty({ - example: 'Regular or Night or Emergency, etc...', - description: 'Type of shifts for an account perception', - }) - @IsString() - @IsNotEmpty() - shift_type: string; - - @ApiProperty({ - example: 'G1, G2, G3, etc...', - description: 'bank`s code related to the type of shift', - }) - @IsString() - @IsNotEmpty() - bank_code: string; -} \ No newline at end of file diff --git a/src/modules/shift-codes/dtos/swagger-entities/shift-codes.entity.ts b/src/modules/shift-codes/dtos/swagger-entities/shift-codes.entity.ts deleted file mode 100644 index 6a90b6b..0000000 --- a/src/modules/shift-codes/dtos/swagger-entities/shift-codes.entity.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class ShiftCodesEntity { - @ApiProperty({ - example: 1, - description: 'Unique ID of a shift-code (auto-generated)', - }) - id: number; - - @ApiProperty({ - example: 'Night', - description: 'Type of shifts for an account perception', - }) - shift_type: string; - - @ApiProperty({ - example: 'G2', - description: 'bank`s code related to the type of shift', - }) - bank_code: string; -} diff --git a/src/modules/shift-codes/dtos/update-shift-codes.dto.ts b/src/modules/shift-codes/dtos/update-shift-codes.dto.ts deleted file mode 100644 index bb956c6..0000000 --- a/src/modules/shift-codes/dtos/update-shift-codes.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateShiftCodeDto } from './create-shift-codes.dto'; - -export class UpdateShiftCodeDto extends PartialType(CreateShiftCodeDto) {} diff --git a/src/modules/shift-codes/services/shift-codes.service.ts b/src/modules/shift-codes/services/shift-codes.service.ts deleted file mode 100644 index c88b958..0000000 --- a/src/modules/shift-codes/services/shift-codes.service.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; -import { PrismaService } from "src/prisma/prisma.service"; -import { CreateShiftCodeDto } from "../dtos/create-shift-codes.dto"; -import { ShiftCodes } from "@prisma/client"; -import { UpdateShiftCodeDto } from "../dtos/update-shift-codes.dto"; - -@Injectable() -export class ShiftCodesService { - constructor(private readonly prisma: PrismaService) {} - - async create(dto: CreateShiftCodeDto): Promise { - const { shift_type, bank_code } = dto; - return this.prisma.shiftCodes.create({ - data: { shift_type, bank_code }, - include: { - shift: { - include: { - timesheet: { - include: { - employee: { - include: { - user: true, - }, - }, - }, - }, - }, - }, - }, - }); - } - - findAll(): Promise { - return this.prisma.shiftCodes.findMany({ - include: { - shift: { - include: { - timesheet: { - include: { - employee: { - include: { - user: true - } - } - } - } - }, - }, - }, - }); - } - - async findOne(id: number): Promise { - const record = await this.prisma.shiftCodes.findUnique({ - where: { id }, - include: { - shift: { - include: { - timesheet: { - include: { - employee: { - include: { - user:true, - } - } - } - } - } - } - } - }); - if(!record) { - throw new NotFoundException(`ShiftCode #${id} not found`); - } - return record; - } - - async update(id: number, dto: UpdateShiftCodeDto): Promise { - await this.findOne(id); - const { shift_type, bank_code } = dto; - return this.prisma.shiftCodes.update({ - where: { id }, - data: { - ...(shift_type !== undefined && { shift_type }), - ...(bank_code !== undefined && { bank_code }), - }, - include: { - shift: { - include: { - timesheet: { - include: { - employee: { - include: { - user: true - } - } - } - }, - }, - }, - }, - }); - } - - async remove(id:number): Promise{ - await this.findOne(id); - return this.prisma.shiftCodes.delete({ where: { id }}); - } -} \ No newline at end of file diff --git a/src/modules/shift-codes/shift-codes.module.ts b/src/modules/shift-codes/shift-codes.module.ts deleted file mode 100644 index a8574ae..0000000 --- a/src/modules/shift-codes/shift-codes.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from "@nestjs/common"; -import { ShiftCodesController } from "./controllers/shift-codes.controller"; -import { ShiftCodesService } from "./services/shift-codes.service"; -import { PrismaService } from "src/prisma/prisma.service"; - -@Module({ - controllers: [ShiftCodesController], - providers: [ShiftCodesService, PrismaService], -}) - -export class ShiftCodesModule {} \ No newline at end of file diff --git a/src/modules/shifts/dtos/create-shifts.dto.ts b/src/modules/shifts/dtos/create-shifts.dto.ts index f17403d..0e5241d 100644 --- a/src/modules/shifts/dtos/create-shifts.dto.ts +++ b/src/modules/shifts/dtos/create-shifts.dto.ts @@ -1,56 +1,31 @@ -import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsDate, IsDateString, IsInt, IsString } from "class-validator"; export class CreateShiftDto { - @ApiProperty({ - example: 'Th3F3110w5h1pX2024', - description: 'ID number for a set timesheet', - }) @Type(() => Number) @IsInt() timesheet_id: number; - @ApiProperty({ - example: '0n3R1n962Ru13xX', - description: 'ID number of a shift code (link with shift-codes)', - }) @Type(() => Number) @IsInt() - shift_code_id: number; + bank_code_id: number; - @ApiProperty({ - example: '20/10/3018', - description: 'Date where the shift takes place', - }) @IsDateString() @Type(() => Date) @IsDate() date: Date; - @ApiProperty({ - example: '08:00', - description: 'Start time of the said shift', - }) @IsDateString() @Type(() => Date) @IsDate() start_time: Date; - @ApiProperty({ - example: '17:00', - description: 'End time of the said shift', - }) @IsDateString() @Type(() => Date) @IsDate() end_time: Date; - @ApiProperty({ - example:'Called for an emergency at X` place', - description:'justify the purpose of the shift' - }) @IsString() description: string; } diff --git a/src/modules/shifts/dtos/swagger-entities/shift.entity.ts b/src/modules/shifts/dtos/swagger-entities/shift.entity.ts index 787dfea..54efc06 100644 --- a/src/modules/shifts/dtos/swagger-entities/shift.entity.ts +++ b/src/modules/shifts/dtos/swagger-entities/shift.entity.ts @@ -15,9 +15,9 @@ export class ShiftEntity { @ApiProperty({ example: 7, - description: 'ID number of a shift code (link with shift-codes)', + description: 'ID number of a shift code (link with bank-codes)', }) - shift_code_id: number; + bank_code_id: number; @ApiProperty({ example: '3018-10-20T00:00:00.000Z', diff --git a/src/modules/shifts/services/shifts.service.ts b/src/modules/shifts/services/shifts.service.ts index 2844bcb..e7c3942 100644 --- a/src/modules/shifts/services/shifts.service.ts +++ b/src/modules/shifts/services/shifts.service.ts @@ -9,16 +9,16 @@ export class ShiftsService { constructor(private readonly prisma: PrismaService) {} async create(dto: CreateShiftDto): Promise { - const { timesheet_id, shift_code_id, date, start_time, end_time } = dto; + const { timesheet_id, bank_code_id, date, start_time, end_time } = dto; return this.prisma.shifts.create({ - data: { timesheet_id, shift_code_id, date, start_time, end_time }, + data: { timesheet_id, bank_code_id, date, start_time, end_time }, include: { timesheet: { include: { employee: { include: { user: true } }, }, }, - shift_code: true, + bank_code: true, }, }); } @@ -48,7 +48,7 @@ export class ShiftsService { }, }, }, - shift_code: true, + bank_code: true, }, }); if(!shift) { @@ -59,12 +59,12 @@ export class ShiftsService { async update(id: number, dto: UpdateShiftsDto): Promise { await this.findOne(id); - const { timesheet_id, shift_code_id, date,start_time,end_time} = dto; + const { timesheet_id, bank_code_id, date,start_time,end_time} = dto; return this.prisma.shifts.update({ where: { id }, data: { ...(timesheet_id !== undefined && { timesheet_id }), - ...(shift_code_id !== undefined && { shift_code_id }), + ...(bank_code_id !== undefined && { bank_code_id }), ...(date !== undefined && { date }), ...(start_time !== undefined && { start_time }), ...(end_time !== undefined && { end_time }), @@ -77,7 +77,7 @@ export class ShiftsService { }, }, }, - shift_code: true, + bank_code: true, }, }); } @@ -115,7 +115,7 @@ export class ShiftsService { data: shiftsToArchive.map(shift => ({ shift_id: shift.id, timesheet_id: shift.timesheet_id, - shift_code_id: shift.shift_code_id, + bank_code_id: shift.bank_code_id, description: shift.description ?? undefined, date: shift.date, start_time: shift.start_time, From cbb863ac27da3c8ab9a3f935ee11f8814be668fc Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 30 Jul 2025 14:44:58 -0400 Subject: [PATCH 13/21] fix(dto): small fix to leave-request dto to inlcude bank_code_id --- docs/swagger/swagger-spec.json | 812 +++--------------- src/app.module.ts | 6 +- src/modules/bank-codes/bank-codes.module.ts | 2 +- .../dtos/create-leave-requests.dto.ts | 4 + .../services/leave-request.service.ts | 2 + 5 files changed, 146 insertions(+), 680 deletions(-) diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 62fbba4..be46095 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -29,6 +29,125 @@ ] } }, + "/bank-codes": { + "post": { + "operationId": "BankCodesControllers_create", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBankCodeDto" + } + } + } + }, + "responses": { + "201": { + "description": "Bank code successfully created." + }, + "400": { + "description": "Invalid input data." + } + }, + "summary": "Create a new bank code", + "tags": [ + "BankCodesControllers" + ] + }, + "get": { + "operationId": "BankCodesControllers_findAll", + "parameters": [], + "responses": { + "200": { + "description": "List of bank codes." + } + }, + "summary": "Retrieve all bank codes", + "tags": [ + "BankCodesControllers" + ] + } + }, + "/bank-codes/{id}": { + "get": { + "operationId": "BankCodesControllers_findOne", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "404": { + "description": "Bank code not found." + } + }, + "summary": "Retrieve a bank code by its ID", + "tags": [ + "BankCodesControllers" + ] + }, + "patch": { + "operationId": "BankCodesControllers_update", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateBankCodeDto" + } + } + } + }, + "responses": { + "404": { + "description": "Bank code not found." + } + }, + "summary": "Update an existing bank code", + "tags": [ + "BankCodesControllers" + ] + }, + "delete": { + "operationId": "BankCodesControllers_remove", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "404": { + "description": "Bank code not found." + } + }, + "summary": "Delete a bank code", + "tags": [ + "BankCodesControllers" + ] + } + }, "/archives/employees": { "get": { "operationId": "EmployeesArchiveController_findOneArchived", @@ -1515,394 +1634,6 @@ ] } }, - "/expense-codes": { - "post": { - "operationId": "ExpenseCodesController_create", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateExpenseCodeDto" - } - } - } - }, - "responses": { - "201": { - "description": "Expense code created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExpenseCodesEntity" - } - } - } - }, - "400": { - "description": "Incomplete task or invalid data" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Create expense code", - "tags": [ - "Expense Codes" - ] - }, - "get": { - "operationId": "ExpenseCodesController_findAll", - "parameters": [], - "responses": { - "201": { - "description": "List of expense codes found", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ExpenseCodesEntity" - } - } - } - } - }, - "400": { - "description": "List of expense codes not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find all expense codes", - "tags": [ - "Expense Codes" - ] - } - }, - "/expense-codes/{id}": { - "get": { - "operationId": "ExpenseCodesController_findOne", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "201": { - "description": "Expense code found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExpenseCodesEntity" - } - } - } - }, - "400": { - "description": "Expense code not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find expense code", - "tags": [ - "Expense Codes" - ] - }, - "patch": { - "operationId": "ExpenseCodesController_update", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateExpenseCodeDto" - } - } - } - }, - "responses": { - "201": { - "description": "Expense code updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExpenseCodesEntity" - } - } - } - }, - "400": { - "description": "Expense code not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Update expense code", - "tags": [ - "Expense Codes" - ] - }, - "delete": { - "operationId": "ExpenseCodesController_remove", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "201": { - "description": "Expense code deleted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExpenseCodesEntity" - } - } - } - }, - "400": { - "description": "Expense code not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Delete expense code", - "tags": [ - "Expense Codes" - ] - } - }, - "/shift-codes": { - "post": { - "operationId": "ShiftCodesController_create", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateShiftCodeDto" - } - } - } - }, - "responses": { - "201": { - "description": "Shift code created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ShiftCodesEntity" - } - } - } - }, - "400": { - "description": "Incomplete task or invalid data" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Create shift code", - "tags": [ - "Shift Codes" - ] - }, - "get": { - "operationId": "ShiftCodesController_findAll", - "parameters": [], - "responses": { - "201": { - "description": "List of shift codes found", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ShiftCodesEntity" - } - } - } - } - }, - "400": { - "description": "List of shift codes not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find all shift codes", - "tags": [ - "Shift Codes" - ] - } - }, - "/shift-codes/{id}": { - "get": { - "operationId": "ShiftCodesController_findOne", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "201": { - "description": "Shift code found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ShiftCodesEntity" - } - } - } - }, - "400": { - "description": "Shift code not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find shift code", - "tags": [ - "Shift Codes" - ] - }, - "patch": { - "operationId": "ShiftCodesController_update", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateShiftCodeDto" - } - } - } - }, - "responses": { - "201": { - "description": "Shift code updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ShiftCodesEntity" - } - } - } - }, - "400": { - "description": "Shift code not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Update shift code", - "tags": [ - "Shift Codes" - ] - }, - "delete": { - "operationId": "ShiftCodesController_remove", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "201": { - "description": "Shift code deleted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ShiftCodesEntity" - } - } - } - }, - "400": { - "description": "Shift code not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Delete shift code", - "tags": [ - "Shift Codes" - ] - } - }, "/auth/login": { "get": { "operationId": "AuthController_login", @@ -2119,6 +1850,14 @@ } }, "schemas": { + "CreateBankCodeDto": { + "type": "object", + "properties": {} + }, + "UpdateBankCodeDto": { + "type": "object", + "properties": {} + }, "CreateEmployeeDto": { "type": "object", "properties": { @@ -2341,53 +2080,7 @@ }, "CreateExpenseDto": { "type": "object", - "properties": { - "timesheet_id": { - "type": "number", - "example": "Th3F3110w5h1pX2024", - "description": "ID number for a set timesheet" - }, - "expense_code_id": { - "type": "number", - "example": "0n3R1n962Ru13xX", - "description": "ID number of an expense code (link with shift-codes)" - }, - "date": { - "format": "date-time", - "type": "string", - "example": "20/10/3018", - "description": "Date where the expense was made" - }, - "amount": { - "type": "number", - "example": "280 000 000,00", - "description": "Amount of the expense" - }, - "description": { - "type": "string", - "example": "Spent for mileage between A and B", - "description": "explain`s why the expense was made" - }, - "is_approved": { - "type": "boolean", - "example": "True or False or Pending or Denied or Cancelled or Escalated", - "description": "Expense`s approval status" - }, - "supervisor_comment": { - "type": "string", - "example": "Asked X to go there as an emergency response", - "description": "Supervisro`s justification for the spending of an employee" - } - }, - "required": [ - "timesheet_id", - "expense_code_id", - "date", - "amount", - "description", - "is_approved", - "supervisor_comment" - ] + "properties": {} }, "ExpenseEntity": { "type": "object", @@ -2402,10 +2095,10 @@ "example": 101, "description": "ID number for a set timesheet" }, - "expense": { + "bank_code_id": { "type": "number", "example": 7, - "description": "ID number of an expense code (link with expense-codes)" + "description": "ID number of an bank code (link with bank-codes)" }, "date": { "format": "date-time", @@ -2432,7 +2125,7 @@ "required": [ "id", "timesheet_id", - "expense", + "bank_code_id", "date", "is_approuved", "description", @@ -2441,90 +2134,11 @@ }, "UpdateExpenseDto": { "type": "object", - "properties": { - "timesheet_id": { - "type": "number", - "example": "Th3F3110w5h1pX2024", - "description": "ID number for a set timesheet" - }, - "expense_code_id": { - "type": "number", - "example": "0n3R1n962Ru13xX", - "description": "ID number of an expense code (link with shift-codes)" - }, - "date": { - "format": "date-time", - "type": "string", - "example": "20/10/3018", - "description": "Date where the expense was made" - }, - "amount": { - "type": "number", - "example": "280 000 000,00", - "description": "Amount of the expense" - }, - "description": { - "type": "string", - "example": "Spent for mileage between A and B", - "description": "explain`s why the expense was made" - }, - "is_approved": { - "type": "boolean", - "example": "True or False or Pending or Denied or Cancelled or Escalated", - "description": "Expense`s approval status" - }, - "supervisor_comment": { - "type": "string", - "example": "Asked X to go there as an emergency response", - "description": "Supervisro`s justification for the spending of an employee" - } - } + "properties": {} }, "CreateShiftDto": { "type": "object", - "properties": { - "timesheet_id": { - "type": "number", - "example": "Th3F3110w5h1pX2024", - "description": "ID number for a set timesheet" - }, - "shift_code_id": { - "type": "number", - "example": "0n3R1n962Ru13xX", - "description": "ID number of a shift code (link with shift-codes)" - }, - "date": { - "format": "date-time", - "type": "string", - "example": "20/10/3018", - "description": "Date where the shift takes place" - }, - "start_time": { - "format": "date-time", - "type": "string", - "example": "08:00", - "description": "Start time of the said shift" - }, - "end_time": { - "format": "date-time", - "type": "string", - "example": "17:00", - "description": "End time of the said shift" - }, - "description": { - "type": "string", - "example": "Called for an emergency at X` place", - "description": "justify the purpose of the shift" - } - }, - "required": [ - "timesheet_id", - "shift_code_id", - "date", - "start_time", - "end_time", - "description" - ] + "properties": {} }, "ShiftEntity": { "type": "object", @@ -2539,10 +2153,10 @@ "example": 101, "description": "ID number for a set timesheet" }, - "shift_code_id": { + "bank_code_id": { "type": "number", "example": 7, - "description": "ID number of a shift code (link with shift-codes)" + "description": "ID number of a shift code (link with bank-codes)" }, "date": { "format": "date-time", @@ -2566,7 +2180,7 @@ "required": [ "id", "timesheet_id", - "shift_code_id", + "bank_code_id", "date", "start_time", "end_time" @@ -2574,41 +2188,7 @@ }, "UpdateShiftsDto": { "type": "object", - "properties": { - "timesheet_id": { - "type": "number", - "example": "Th3F3110w5h1pX2024", - "description": "ID number for a set timesheet" - }, - "shift_code_id": { - "type": "number", - "example": "0n3R1n962Ru13xX", - "description": "ID number of a shift code (link with shift-codes)" - }, - "date": { - "format": "date-time", - "type": "string", - "example": "20/10/3018", - "description": "Date where the shift takes place" - }, - "start_time": { - "format": "date-time", - "type": "string", - "example": "08:00", - "description": "Start time of the said shift" - }, - "end_time": { - "format": "date-time", - "type": "string", - "example": "17:00", - "description": "End time of the said shift" - }, - "description": { - "type": "string", - "example": "Called for an emergency at X` place", - "description": "justify the purpose of the shift" - } - } + "properties": {} }, "CreateLeaveRequestsDto": { "type": "object", @@ -3051,124 +2631,6 @@ } } }, - "CreateExpenseCodeDto": { - "type": "object", - "properties": { - "expense_type": { - "type": "string", - "example": "mileage, overnight, etc...", - "description": "Type of expenses for an account perception" - }, - "bank_code": { - "type": "string", - "example": "G500, G501, etc...", - "description": "bank`s code related to the type of expense" - } - }, - "required": [ - "expense_type", - "bank_code" - ] - }, - "ExpenseCodesEntity": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1, - "description": "Unique ID of a expense-code (auto-generated)" - }, - "shift_type": { - "type": "string", - "example": "Mileage", - "description": "Type of expenses for an account perception" - }, - "bank_code": { - "type": "string", - "example": "G501", - "description": "bank`s code related to the type of expense" - } - }, - "required": [ - "id", - "shift_type", - "bank_code" - ] - }, - "UpdateExpenseCodeDto": { - "type": "object", - "properties": { - "expense_type": { - "type": "string", - "example": "mileage, overnight, etc...", - "description": "Type of expenses for an account perception" - }, - "bank_code": { - "type": "string", - "example": "G500, G501, etc...", - "description": "bank`s code related to the type of expense" - } - } - }, - "CreateShiftCodeDto": { - "type": "object", - "properties": { - "shift_type": { - "type": "string", - "example": "Regular or Night or Emergency, etc...", - "description": "Type of shifts for an account perception" - }, - "bank_code": { - "type": "string", - "example": "G1, G2, G3, etc...", - "description": "bank`s code related to the type of shift" - } - }, - "required": [ - "shift_type", - "bank_code" - ] - }, - "ShiftCodesEntity": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1, - "description": "Unique ID of a shift-code (auto-generated)" - }, - "shift_type": { - "type": "string", - "example": "Night", - "description": "Type of shifts for an account perception" - }, - "bank_code": { - "type": "string", - "example": "G2", - "description": "bank`s code related to the type of shift" - } - }, - "required": [ - "id", - "shift_type", - "bank_code" - ] - }, - "UpdateShiftCodeDto": { - "type": "object", - "properties": { - "shift_type": { - "type": "string", - "example": "Regular or Night or Emergency, etc...", - "description": "Type of shifts for an account perception" - }, - "bank_code": { - "type": "string", - "example": "G1, G2, G3, etc...", - "description": "bank`s code related to the type of shift" - } - } - }, "PayPeriodEntity": { "type": "object", "properties": { diff --git a/src/app.module.ts b/src/app.module.ts index e874e70..1a4c3a7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -9,19 +9,19 @@ import { OauthAccessTokensModule } from './modules/oauth-access-tokens/oauth-acc import { CustomersModule } from './modules/customers/customers.module'; import { EmployeesModule } from './modules/employees/employees.module'; import { LeaveRequestsModule } from './modules/leave-requests/leave-requests.module'; -import { ShiftCodesModule } from './modules/shift-codes/shift-codes.module'; import { ShiftsModule } from './modules/shifts/shifts.module'; import { TimesheetsModule } from './modules/timesheets/timesheets.module'; import { AuthenticationModule } from './modules/authentication/auth.module'; import { ExpensesModule } from './modules/expenses/expenses.module'; -import { ExpenseCodesModule } from './modules/expense-codes/expense-codes.module'; import { PayperiodsModule } from './modules/pay-periods/pay-periods.module'; import { ScheduleModule } from '@nestjs/schedule'; import { ArchivalModule } from './modules/archival/archival.module'; +import { BankCodesModule } from './modules/bank-codes/bank-codes.module'; @Module({ imports: [ ScheduleModule.forRoot(), + BankCodesModule, ArchivalModule, PrismaModule, HealthModule, @@ -31,8 +31,6 @@ import { ArchivalModule } from './modules/archival/archival.module'; EmployeesModule, LeaveRequestsModule, ExpensesModule, - ExpenseCodesModule, - ShiftCodesModule, ShiftsModule, TimesheetsModule, AuthenticationModule, diff --git a/src/modules/bank-codes/bank-codes.module.ts b/src/modules/bank-codes/bank-codes.module.ts index a8c4ced..bda3b9b 100644 --- a/src/modules/bank-codes/bank-codes.module.ts +++ b/src/modules/bank-codes/bank-codes.module.ts @@ -9,4 +9,4 @@ import { BankCodesService } from "./services/bank-codes.services"; providers: [BankCodesService, PrismaService], }) -export class ShiftCodesModule {} \ No newline at end of file +export class BankCodesModule {} \ No newline at end of file diff --git a/src/modules/leave-requests/dtos/create-leave-requests.dto.ts b/src/modules/leave-requests/dtos/create-leave-requests.dto.ts index 703cf91..24cb91e 100644 --- a/src/modules/leave-requests/dtos/create-leave-requests.dto.ts +++ b/src/modules/leave-requests/dtos/create-leave-requests.dto.ts @@ -12,6 +12,10 @@ export class CreateLeaveRequestsDto { @IsInt() employee_id: number; + @Type(()=> Number) + @IsInt() + bank_code_id: number; + @ApiProperty({ example: 'Sick or Vacation or Unpaid or Bereavement or Parental or Legal', description: 'type of leave request for an accounting perception', diff --git a/src/modules/leave-requests/services/leave-request.service.ts b/src/modules/leave-requests/services/leave-request.service.ts index e8315a7..a8bfafe 100644 --- a/src/modules/leave-requests/services/leave-request.service.ts +++ b/src/modules/leave-requests/services/leave-request.service.ts @@ -11,6 +11,7 @@ export class LeaveRequestsService { async create(dto: CreateLeaveRequestsDto): Promise { const { employee_id, + bank_code_id, leave_type, start_date_time, end_date_time, @@ -21,6 +22,7 @@ export class LeaveRequestsService { return this.prisma.leaveRequests.create({ data: { employee_id, + bank_code_id, leave_type, start_date_time, end_date_time, From e91fad51059ec734d322e697e6fc5dd5e2a40984 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 30 Jul 2025 14:49:31 -0400 Subject: [PATCH 14/21] fix(module): small fix to include bank_code in find methods of leave-requests service and a small typo --- .../archival/controllers/leave-requests-archive.controller.ts | 2 +- src/modules/archival/services/archival.service.ts | 2 +- .../leave-requests/controllers/leave-requests.controller.ts | 2 +- src/modules/leave-requests/leave-requests.module.ts | 2 +- .../{leave-request.service.ts => leave-requests.service.ts} | 2 ++ 5 files changed, 6 insertions(+), 4 deletions(-) rename src/modules/leave-requests/services/{leave-request.service.ts => leave-requests.service.ts} (98%) diff --git a/src/modules/archival/controllers/leave-requests-archive.controller.ts b/src/modules/archival/controllers/leave-requests-archive.controller.ts index c7fe0ca..b8f8399 100644 --- a/src/modules/archival/controllers/leave-requests-archive.controller.ts +++ b/src/modules/archival/controllers/leave-requests-archive.controller.ts @@ -3,7 +3,7 @@ import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { LeaveRequestsArchive, Roles as RoleEnum } from "@prisma/client"; import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { JwtAuthGuard } from "src/modules/authentication/guards/jwt-auth.guard"; -import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-request.service"; +import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-requests.service"; @ApiTags('LeaveRequests Archives') @UseGuards(JwtAuthGuard) diff --git a/src/modules/archival/services/archival.service.ts b/src/modules/archival/services/archival.service.ts index 7d8c6d7..db91238 100644 --- a/src/modules/archival/services/archival.service.ts +++ b/src/modules/archival/services/archival.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from "@nestjs/common"; import { Cron } from "@nestjs/schedule"; import { ExpensesService } from "src/modules/expenses/services/expenses.service"; -import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-request.service"; +import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-requests.service"; import { ShiftsService } from "src/modules/shifts/services/shifts.service"; import { TimesheetsService } from "src/modules/timesheets/services/timesheets.service"; diff --git a/src/modules/leave-requests/controllers/leave-requests.controller.ts b/src/modules/leave-requests/controllers/leave-requests.controller.ts index a71c49a..9a27b4b 100644 --- a/src/modules/leave-requests/controllers/leave-requests.controller.ts +++ b/src/modules/leave-requests/controllers/leave-requests.controller.ts @@ -1,5 +1,5 @@ import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common"; -import { LeaveRequestsService } from "../services/leave-request.service"; +import { LeaveRequestsService } from "../services/leave-requests.service"; import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto"; import { LeaveRequests } from "@prisma/client"; import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto"; diff --git a/src/modules/leave-requests/leave-requests.module.ts b/src/modules/leave-requests/leave-requests.module.ts index 1973165..62b994f 100644 --- a/src/modules/leave-requests/leave-requests.module.ts +++ b/src/modules/leave-requests/leave-requests.module.ts @@ -1,6 +1,6 @@ import { PrismaService } from "src/prisma/prisma.service"; import { LeaveRequestController } from "./controllers/leave-requests.controller"; -import { LeaveRequestsService } from "./services/leave-request.service"; +import { LeaveRequestsService } from "./services/leave-requests.service"; import { Module } from "@nestjs/common"; @Module({ diff --git a/src/modules/leave-requests/services/leave-request.service.ts b/src/modules/leave-requests/services/leave-requests.service.ts similarity index 98% rename from src/modules/leave-requests/services/leave-request.service.ts rename to src/modules/leave-requests/services/leave-requests.service.ts index a8bfafe..476a487 100644 --- a/src/modules/leave-requests/services/leave-request.service.ts +++ b/src/modules/leave-requests/services/leave-requests.service.ts @@ -47,6 +47,7 @@ export class LeaveRequestsService { user: true }, }, + bank_code: true, }, }); } @@ -60,6 +61,7 @@ export class LeaveRequestsService { user: true }, }, + bank_code: true, }, }); if(!req) { From 75615f7c33338b66c511242016ebe993770409f2 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Thu, 31 Jul 2025 10:16:25 -0400 Subject: [PATCH 15/21] feat(business-logic): base setup for business logic implementation, overtime.service and updated timesheets.service to returned overtime infos. --- src/app.module.ts | 3 +- src/business-logic/holiday.service.ts | 0 src/business-logic/overtime.service.ts | 82 ++++++++++++++++ src/business-logic/sick-leave.service.ts | 0 src/business-logic/vacation.service.ts | 0 .../timesheets/services/timesheets.service.ts | 97 +++++++++++-------- 6 files changed, 142 insertions(+), 40 deletions(-) create mode 100644 src/business-logic/holiday.service.ts create mode 100644 src/business-logic/overtime.service.ts create mode 100644 src/business-logic/sick-leave.service.ts create mode 100644 src/business-logic/vacation.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 1a4c3a7..b9c689c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -17,6 +17,7 @@ import { PayperiodsModule } from './modules/pay-periods/pay-periods.module'; import { ScheduleModule } from '@nestjs/schedule'; import { ArchivalModule } from './modules/archival/archival.module'; import { BankCodesModule } from './modules/bank-codes/bank-codes.module'; +import { OvertimeService } from './business-logic/overtime.service'; @Module({ imports: [ @@ -37,6 +38,6 @@ import { BankCodesModule } from './modules/bank-codes/bank-codes.module'; PayperiodsModule, ], controllers: [AppController, HealthController], - providers: [AppService], + providers: [AppService, OvertimeService], }) export class AppModule {} diff --git a/src/business-logic/holiday.service.ts b/src/business-logic/holiday.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/business-logic/overtime.service.ts b/src/business-logic/overtime.service.ts new file mode 100644 index 0000000..b99122b --- /dev/null +++ b/src/business-logic/overtime.service.ts @@ -0,0 +1,82 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from 'src/prisma/prisma.service'; + +@Injectable() +export class OvertimeService { + + private logger = new Logger(OvertimeService.name); + private dailyMax = 8; // maximum for regular hours per day + private weeklyMax = 40; //maximum for regular hours per week + + constructor(private prisma: PrismaService) {} + + // calculate decimal hours rounded to nearest 5 min + computedHours(start: Date, end: Date): number { + const durationMs = end.getTime() - start.getTime(); + const totalMinutes = durationMs / 60000; + + //rounded to 5 min + const rounded = Math.round(totalMinutes / 5) * 5; + const hours = rounded / 60; + this.logger.debug(`computedHours: raw=${totalMinutes.toFixed(1)}min rounded = ${rounded}min (${hours.toFixed(2)}h)`); + return hours; + } + + //calculate Daily overtime + getDailyOvertimeHours(start: Date, end: Date): number { + const hours = this.computedHours(start, end); + const overtime = Math.max(0, hours - this.dailyMax); + this.logger.debug(`getDailyOvertimeHours : ${overtime.toFixed(2)}h (threshold ${this.dailyMax})`); + return overtime; + } + + //sets first day of the week to be sunday + private getWeekStart(date:Date): Date { + const d = new Date(date); + const day = d.getDay(); // return sunday = 0, monday = 1, etc + d.setDate(d.getDate() - day); + d.setHours(0,0,0,0,); // puts start of the week at sunday morning at 00:00 + return d; + } + + //sets last day of the week to be saturday + private getWeekEnd(startDate:Date): Date { + const d = new Date(startDate); + d.setDate(d.getDate() +6); //sets last day to be saturday + d.setHours(23,59,59,999); //puts end of the week at saturday night at 00:00 minus 1ms + return d; + } + + //calculate Weekly overtime + async getWeeklyOvertimeHours(employeeId: number, refDate: Date): Promise { + const weekStart = this.getWeekStart(refDate); + const weekEnd = this.getWeekEnd(weekStart); + + //fetches all shifts containing hours + const shifts = await this.prisma.shifts.findMany({ + where: { timesheet: { employee_id: employeeId, shift: { + every: {date: { gte: weekStart, lte: weekEnd } } + }, + }, + }, + select: { start_time: true, end_time: true }, + }); + + //calculate total hours of those shifts minus weekly Max to find total overtime hours + const total = shifts.map(shift => this.computedHours(shift.start_time, shift.end_time)) + .reduce((sum, hours)=> sum+hours, 0); + const overtime = Math.max(0, total - this.weeklyMax); + + this.logger.debug(`weekly total = ${total.toFixed(2)}h, weekly Overtime= ${overtime.toFixed(2)}h`); + return overtime; + } + + //apply modifier to overtime hours + calculateOvertimePay(overtimeHours: number, modifier: number): number { + const pay = overtimeHours * modifier; + this.logger.debug(`Overtime payable hours = ${pay.toFixed(2)} (hours ${overtimeHours}, modifier ${modifier})`); + + return pay; + } + +} diff --git a/src/business-logic/sick-leave.service.ts b/src/business-logic/sick-leave.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/business-logic/vacation.service.ts b/src/business-logic/vacation.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/timesheets/services/timesheets.service.ts b/src/modules/timesheets/services/timesheets.service.ts index c8fcff3..4cc519d 100644 --- a/src/modules/timesheets/services/timesheets.service.ts +++ b/src/modules/timesheets/services/timesheets.service.ts @@ -3,51 +3,80 @@ import { PrismaService } from 'src/prisma/prisma.service'; import { CreateTimesheetDto } from '../dtos/create-timesheet.dto'; import { Timesheets, TimesheetsArchive } from '@prisma/client'; import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto'; +import { OvertimeService } from 'src/business-logic/overtime.service'; @Injectable() export class TimesheetsService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly overtime: OvertimeService, + ) {} async create(dto : CreateTimesheetDto): Promise { const { employee_id, is_approved } = dto; return this.prisma.timesheets.create({ - data: { - employee_id, - is_approved: is_approved ?? false, - }, + data: { employee_id, is_approved: is_approved ?? false }, include: { - employee: { - include: { user: true } + employee: { include: { user: true } }, }, }); } - findAll(): Promise { - return this.prisma.timesheets.findMany({ - include: { - employee: { - include: { user: true }, - }, + async findAll(): Promise { + const list = await this.prisma.timesheets.findMany({ + include: { + shift: { include: { bank_code: true } }, + expense: { include: { bank_code: true } }, + employee: { include: { user : true } }, }, }); + + return Promise.all( + list.map(async timesheet => { + const detailedShifts = timesheet.shift.map(s => { + const hours = this.overtime.computedHours(s.start_time, s.end_time); + const regularHours = Math.min(8, hours); + const dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time); + const payRegular = regularHours * s.bank_code.modifier; + const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, s.bank_code.modifier); + return { ...s, hours, payRegular, payOvertime }; + }); + const weeklyOvertimeHours = detailedShifts.length + ? await this.overtime.getWeeklyOvertimeHours( + timesheet.employee_id, + timesheet.shift[0].date): 0; + return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours }; + }) + ); } - async findOne(id: number): Promise { - const record = await this.prisma.timesheets.findUnique({ + async findOne(id: number): Promise { + const timesheet = await this.prisma.timesheets.findUnique({ where: { id }, - include: { - employee: { - include: { - user:true - } - }, - }, + include: { + shift: { include: { bank_code: true } }, + expense: { include: { bank_code: true } }, + employee: { include: { user: true } }, + }, }); - if(!record) { + if(!timesheet) { throw new NotFoundException(`Timesheet #${id} not found`); } - return record; + + const detailedShifts = timesheet.shift.map( s => { + const hours = this.overtime.computedHours(s.start_time, s.end_time); + const regularHours = Math.min(8, hours); + const dailyOvertime = this.overtime.getDailyOvertimeHours(s.start_time, s.end_time); + const payRegular = regularHours * s.bank_code.modifier; + const payOvertime = this.overtime.calculateOvertimePay(dailyOvertime, s.bank_code.modifier); + return { ...s, hours, payRegular, payOvertime }; + }); + const weeklyOvertimeHours = detailedShifts.length + ? await this.overtime.getWeeklyOvertimeHours( + timesheet.employee_id, + timesheet.shift[0].date): 0; + return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours }; } async update(id: number, dto:UpdateTimesheetDto): Promise { @@ -59,19 +88,14 @@ export class TimesheetsService { ...(employee_id !== undefined && { employee_id }), ...(is_approved !== undefined && { is_approved }), }, - include: { - employee: { - include: { user: true } - }, + include: { employee: { include: { user: true } }, }, }); } async remove(id: number): Promise { await this.findOne(id); - return this.prisma.timesheets.delete({ - where: { id }, - }); + return this.prisma.timesheets.delete({ where: { id } }); } @@ -85,8 +109,7 @@ export class TimesheetsService { await this.prisma.$transaction(async transaction => { //fetches all timesheets to cutoff const oldSheets = await transaction.timesheets.findMany({ - where: { shift: { every: { date: { lt: cutoff } }, - }, + where: { shift: { every: { date: { lt: cutoff } } }, }, select: { id: true, @@ -106,14 +129,10 @@ export class TimesheetsService { })); //copying data from timesheets table to archive table - await transaction.timesheetsArchive.createMany({ - data: archiveDate, - }); + await transaction.timesheetsArchive.createMany({ data: archiveDate }); //removing data from timesheets table - await transaction.timesheets.deleteMany({ - where: { id: { in: oldSheets.map(s => s.id) } }, - }); + await transaction.timesheets.deleteMany({ where: { id: { in: oldSheets.map(s => s.id) } } }); }); } From c6ff3139f289db58835cbb16f0d96ba5246ef8ec Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Thu, 31 Jul 2025 10:41:15 -0400 Subject: [PATCH 16/21] fix(import): small fix to timesheets.module --- src/modules/timesheets/timesheets.module.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/modules/timesheets/timesheets.module.ts b/src/modules/timesheets/timesheets.module.ts index 11da5c8..7979a2b 100644 --- a/src/modules/timesheets/timesheets.module.ts +++ b/src/modules/timesheets/timesheets.module.ts @@ -1,10 +1,14 @@ import { Module } from '@nestjs/common'; import { TimesheetsController } from './controllers/timesheets.controller'; import { TimesheetsService } from './services/timesheets.service'; +import { OvertimeService } from 'src/business-logic/overtime.service'; @Module({ controllers: [TimesheetsController], - providers: [TimesheetsService], + providers: [ + TimesheetsService, + OvertimeService, + ], exports: [TimesheetsService], }) export class TimesheetsModule {} From 2e6bafeb18013654583ab5ea8b9e24639a1e06f7 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Thu, 31 Jul 2025 13:09:19 -0400 Subject: [PATCH 17/21] feat(business-logic): holiday implementation for leave Requests --- src/business-logic/holiday.service.ts | 56 +++++++++ .../leave-requests/leave-requests.module.ts | 7 +- .../services/leave-requests.service.ts | 107 ++++++++---------- 3 files changed, 108 insertions(+), 62 deletions(-) diff --git a/src/business-logic/holiday.service.ts b/src/business-logic/holiday.service.ts index e69de29..235cc8e 100644 --- a/src/business-logic/holiday.service.ts +++ b/src/business-logic/holiday.service.ts @@ -0,0 +1,56 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class HolidayService { + private readonly logger = new Logger(HolidayService.name); + + constructor(private readonly prisma: PrismaService) {} + + //return the sunday of the current week that includes the holiday + private getWeekStart(date: Date): Date { + const day = new Date(date); + const offset = day.getDay(); + day.setDate(day.getDate() - offset); + day.setHours(0,0,0,0); + return day; + } + + //rounds minutes to 5s + private computeHours(start: Date, end: Date): number { + const durationMS = end.getTime() - start.getTime(); + const totalMinutes = durationMS / 60000; + const rounded = Math.round(totalMinutes / 5) * 5; + return rounded / 60; + } + + private async computeHoursPrevious4Weeks(employeeId: number, holidayDate: Date): Promise { + //sets the end of the window to 1ms before the week with the holiday + const holidayWeekStart = this.getWeekStart(holidayDate); + const windowEnd = new Date(holidayWeekStart.getTime() - 1); + //sets the start of the window to 28 days ( 4 completed weeks ) before the week with the holiday + const windowStart = new Date(windowEnd.getTime() - 28 * 24 * 60 * 60000 + 1 ) + + const validCodes = ['G1', 'G45', 'G56', 'G104', 'G105', 'G700']; + //fetches all shift of the employee in said window ( 4 completed weeks ) + const shifts = await this.prisma.shifts.findMany({ + where: { timesheet: { employee_id: employeeId } , + date: { gte: windowStart, lte: windowEnd }, + bank_code: { bank_code: { in: validCodes } }, + }, + select: { date: true, start_time: true, end_time: true }, + }); + + const totalHours = shifts.map(s => this.computeHours(s.start_time, s.end_time)).reduce((sum, h)=> sum + h, 0); + const dailyHours = totalHours / 20; + + return dailyHours; + } + + async calculateHolidayPay( employeeId: number, holidayDate: Date, modifier: number): Promise { + const hours = await this. computeHoursPrevious4Weeks(employeeId, holidayDate); + const dailyRate = Math.min(hours, 8); + this.logger.debug(`Holiday pay calculation: hours=${hours.toFixed(2)}`); + return dailyRate * modifier; + } +} \ No newline at end of file diff --git a/src/modules/leave-requests/leave-requests.module.ts b/src/modules/leave-requests/leave-requests.module.ts index 62b994f..27394bf 100644 --- a/src/modules/leave-requests/leave-requests.module.ts +++ b/src/modules/leave-requests/leave-requests.module.ts @@ -2,10 +2,15 @@ import { PrismaService } from "src/prisma/prisma.service"; import { LeaveRequestController } from "./controllers/leave-requests.controller"; import { LeaveRequestsService } from "./services/leave-requests.service"; import { Module } from "@nestjs/common"; +import { HolidayService } from "src/business-logic/holiday.service"; @Module({ controllers: [LeaveRequestController], - providers: [ LeaveRequestsService, PrismaService], + providers: [ + LeaveRequestsService, + PrismaService, + HolidayService, + ], exports: [ LeaveRequestsService], }) diff --git a/src/modules/leave-requests/services/leave-requests.service.ts b/src/modules/leave-requests/services/leave-requests.service.ts index 476a487..4e54e49 100644 --- a/src/modules/leave-requests/services/leave-requests.service.ts +++ b/src/modules/leave-requests/services/leave-requests.service.ts @@ -3,87 +3,78 @@ import { PrismaService } from "src/prisma/prisma.service"; import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto"; import { LeaveRequests, LeaveRequestsArchive } from "@prisma/client"; import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto"; +import { HolidayService } from "src/business-logic/holiday.service"; @Injectable() export class LeaveRequestsService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly holidayService: HolidayService, + ) {} async create(dto: CreateLeaveRequestsDto): Promise { - const { - employee_id, - bank_code_id, - leave_type, - start_date_time, - end_date_time, - comment, - approval_status, - } = dto; + const { employee_id, bank_code_id, leave_type, start_date_time, + end_date_time, comment, approval_status } = dto; return this.prisma.leaveRequests.create({ - data: { - employee_id, - bank_code_id, - leave_type, - start_date_time, - end_date_time, - comment, - approval_status: approval_status ?? undefined, - }, - include: { - employee: { - include: { - user: true - }, - }, + data: { employee_id, bank_code_id, leave_type, start_date_time, + end_date_time, comment, approval_status: approval_status ?? undefined }, + include: { employee: { include: { user: true } } }, }); } - findAll(): Promise { - return this.prisma.leaveRequests.findMany({ - include: { - employee: { - include: { - user: true - }, - }, - bank_code: true, + async findAll(): Promise { + const list = await this.prisma.leaveRequests.findMany({ + include: { employee: { include: { user: true } }, + bank_code: true, }, }); + + return Promise.all( + list.map(async request => { + if(request.bank_code?.type === 'holiday') { + const cost = await this.holidayService.calculateHolidayPay( + request.employee_id, + request.start_date_time, + request.bank_code.modifier + ); + return { ...request, cost }; + } + return request; + }), + ); } - async findOne(id:number): Promise { - const req = await this.prisma.leaveRequests.findUnique({ + async findOne(id:number): Promise { + const request = await this.prisma.leaveRequests.findUnique({ where: { id }, - include: { - employee: { - include: { - user: true - }, - }, - bank_code: true, + include: { employee: { include: { user: true } }, + bank_code: true, }, }); - if(!req) { + if(!request) { throw new NotFoundException(`LeaveRequest #${id} not found`); } - return req; - } + //search for leave type. if holiday + if (request.bank_code?.type === 'holiday') { + const cost = await this.holidayService.calculateHolidayPay( + request.employee_id, + request.start_date_time, + request.bank_code.modifier, + ); + return { ...request, cost }; + } + return request; + } async update( id: number, dto: UpdateLeaveRequestsDto, ): Promise { await this.findOne(id); - const { - employee_id, - leave_type, - start_date_time, - end_date_time, - comment, - approval_status, - } = dto; + const { employee_id, leave_type, start_date_time, end_date_time, comment, approval_status } = dto; return this.prisma.leaveRequests.update({ where: { id }, data: { @@ -94,13 +85,7 @@ export class LeaveRequestsService { ...(comment !== undefined && { comment }), ...(approval_status == undefined && { approval_status }), }, - include: { - employee: { - include: { - user:true - }, - }, - }, + include: { employee: { include: { user:true } } }, }); } From 5766715d779808ffbfdcbfb3b9f0bbc496b075e9 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Thu, 31 Jul 2025 16:37:57 -0400 Subject: [PATCH 18/21] feat(business-logic): implementation of vacation.service.ts, sick-leave.service.ts and update leave-requests. service --- src/business-logic/sick-leave.service.ts | 61 ++++++++++ src/business-logic/vacation.service.ts | 85 ++++++++++++++ .../leave-requests/leave-requests.module.ts | 4 + .../services/leave-requests.service.ts | 111 ++++++++++++------ 4 files changed, 227 insertions(+), 34 deletions(-) diff --git a/src/business-logic/sick-leave.service.ts b/src/business-logic/sick-leave.service.ts index e69de29..2057e05 100644 --- a/src/business-logic/sick-leave.service.ts +++ b/src/business-logic/sick-leave.service.ts @@ -0,0 +1,61 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class SickLeaveService { + constructor(private readonly prisma: PrismaService) {} + + private readonly logger = new Logger(SickLeaveService.name); + + async calculateSickLeavePay(employeeId: number, startDate: Date, daysRequested: number, modifier: number): Promise { + //sets the year to jan 1st to dec 31st + const periodStart = new Date(startDate.getFullYear(), 0, 1); + const periodEnd = startDate; + + //fetches all shifts of a selected employee + const shifts = await this.prisma.shifts.findMany({ + where: { + timesheet: { employee_id: employeeId }, + date: { gte: periodStart, lte: periodEnd}, + }, + select: { date: true }, + }); + + //count the amount of worked days + const workedDates = new Set( + shifts.map(shift => shift.date.toISOString().slice(0,10)) + ); + const daysWorked = workedDates.size; + this.logger.debug(`Sick leave: days worked= ${daysWorked} in ${periodStart.toDateString()} -> ${periodEnd.toDateString()}`); + + //less than 30 worked days returns 0 + if (daysWorked < 30) { + return 0; + } + + //default 3 days allowed after 30 worked days + let acquiredDays = 3; + + //identify the date of the 30th worked day + const orderedDates = Array.from(workedDates).sort(); + const thresholdDate = new Date(orderedDates[29]); // index 29 is the 30th day + + //calculate each completed month, starting the 1st of the next month + const firstBonusDate = new Date(thresholdDate.getFullYear(), thresholdDate.getMonth() +1, 1); + let months = (periodEnd.getFullYear() - firstBonusDate.getFullYear()) * 12 + + (periodEnd.getMonth() - firstBonusDate.getMonth()) + 1; + if(months < 0) months = 0; + acquiredDays += months; + + //cap of 10 days + if (acquiredDays > 10) acquiredDays = 10; + + this.logger.debug(`Sick leave: threshold Date = ${thresholdDate.toDateString()}, bonusMonths = ${months}, acquired Days = ${acquiredDays}`); + + const payableDays = Math.min(acquiredDays, daysRequested); + const rawHours = payableDays * 8 * modifier; + const rounded = Math.round(rawHours * 4) / 4; + this.logger.debug(`Sick leave pay: days= ${payableDays}, modifier= ${modifier}, hours= ${rounded}`); + return rounded; + } +} \ No newline at end of file diff --git a/src/business-logic/vacation.service.ts b/src/business-logic/vacation.service.ts index e69de29..094b64d 100644 --- a/src/business-logic/vacation.service.ts +++ b/src/business-logic/vacation.service.ts @@ -0,0 +1,85 @@ +import { Injectable, Logger, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class VacationService { + constructor(private readonly prisma: PrismaService) {} + + private readonly logger = new Logger(VacationService.name); +/** + * Calculate the ammount allowed for vacation days. + * + * @param employeeId employee ID + * @param startDate first day of vacation + * @param daysRequested number of days requested + * @param modifier Coefficient of hours(1) + * @returns amount of payable hours + */ + async calculateVacationPay(employeeId: number, startDate: Date, daysRequested: number, modifier: number): Promise { + //fetch hiring date + const employee = await this.prisma.employees.findUnique({ + where: { id: employeeId }, + select: { first_work_day: true }, + }); + if(!employee) { + throw new NotFoundException(`Employee #${employeeId} not found`); + } + const hireDate = employee.first_work_day; + + //sets "year" to may 1st to april 30th + //check if hiring date is in may or later, we use hiring year, otherwise we use the year before + const yearOfRequest = startDate.getMonth() >= 4 + ? startDate.getFullYear() : startDate.getFullYear() -1; + const periodStart = new Date(yearOfRequest, 4, 1); //may = 4 + const periodEnd = new Date(yearOfRequest + 1, 4, 0); //day 0 of may == april 30th + + this.logger.debug(`Vacation period for request: ${periodStart.toDateString()} -> ${periodEnd.toDateString()}`); + + //steps to reach to get more vacation weeks in years + const checkpoint = [5, 10, 15]; + const anniversaries = checkpoint.map(years => { + const anniversaryDate = new Date(hireDate); + anniversaryDate.setFullYear(anniversaryDate.getFullYear() + years); + return anniversaryDate; + }).filter(d => d>= periodStart && d <= periodEnd).sort((a,b) => a.getTime() - b.getTime()); + + this.logger.debug(`anniversatries steps during the period: ${anniversaries.map(date => date.toDateString()).join(',') || 'aucun'}`); + + const boundaries = [periodStart, ...anniversaries,periodEnd]; + //calculate prorata per segment + let totalVacationDays = 0; + const msPerDay = 1000 * 60 * 60 * 24; + + for (let i = 0; i < boundaries.length -1; i++) { + const segmentStart = boundaries[i]; + const segmentEnd = boundaries[i+1]; + + //number of days in said segment + const daysInSegment = Math.round((segmentEnd.getTime() - segmentStart.getTime())/ msPerDay); + const yearsSinceHire = (segmentStart.getFullYear() - hireDate.getFullYear()) - + (segmentStart < new Date(segmentStart.getFullYear(), hireDate.getMonth()) ? 1 : 0); + let allocDays: number; + if(yearsSinceHire < 5) allocDays = 10; + else if(yearsSinceHire < 10) allocDays = 15; + else if(yearsSinceHire < 15) allocDays = 20; + else allocDays = 25; + + //prorata for said segment + const prorata = (allocDays / 365) * daysInSegment; + totalVacationDays += prorata; + } + //compares allowed vacation pools with requested days + const payableDays = Math.min(totalVacationDays, daysRequested); + + + const rawHours = payableDays * 8 * modifier; + const roundedHours = Math.round(rawHours * 4) / 4; + this.logger.debug(`Vacation pay: entitledDays=${totalVacationDays.toFixed(2)}, requestedDays=${daysRequested}, + payableDays=${payableDays.toFixed(2)}, hours=${roundedHours}`); + + return roundedHours; + } + + + +} \ No newline at end of file diff --git a/src/modules/leave-requests/leave-requests.module.ts b/src/modules/leave-requests/leave-requests.module.ts index 27394bf..f000f18 100644 --- a/src/modules/leave-requests/leave-requests.module.ts +++ b/src/modules/leave-requests/leave-requests.module.ts @@ -3,6 +3,8 @@ import { LeaveRequestController } from "./controllers/leave-requests.controller" import { LeaveRequestsService } from "./services/leave-requests.service"; import { Module } from "@nestjs/common"; import { HolidayService } from "src/business-logic/holiday.service"; +import { VacationService } from "src/business-logic/vacation.service"; +import { SickLeaveService } from "src/business-logic/sick-leave.service"; @Module({ controllers: [LeaveRequestController], @@ -10,6 +12,8 @@ import { HolidayService } from "src/business-logic/holiday.service"; LeaveRequestsService, PrismaService, HolidayService, + VacationService, + SickLeaveService, ], exports: [ LeaveRequestsService], }) diff --git a/src/modules/leave-requests/services/leave-requests.service.ts b/src/modules/leave-requests/services/leave-requests.service.ts index 4e54e49..9ba05cc 100644 --- a/src/modules/leave-requests/services/leave-requests.service.ts +++ b/src/modules/leave-requests/services/leave-requests.service.ts @@ -1,15 +1,19 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto"; import { LeaveRequests, LeaveRequestsArchive } from "@prisma/client"; import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto"; import { HolidayService } from "src/business-logic/holiday.service"; +import { VacationService } from "src/business-logic/vacation.service"; +import { SickLeaveService } from "src/business-logic/sick-leave.service"; @Injectable() export class LeaveRequestsService { constructor( private readonly prisma: PrismaService, private readonly holidayService: HolidayService, + private readonly vacationService: VacationService, + private readonly sickLeaveService: SickLeaveService ) {} async create(dto: CreateLeaveRequestsDto): Promise { @@ -20,7 +24,9 @@ export class LeaveRequestsService { data: { employee_id, bank_code_id, leave_type, start_date_time, end_date_time, comment, approval_status: approval_status ?? undefined }, - include: { employee: { include: { user: true } } }, + include: { employee: { include: { user: true } }, + bank_code: true + }, }); } @@ -31,18 +37,38 @@ export class LeaveRequestsService { }, }); + const msPerDay = 1000 * 60 * 60 * 24; + return Promise.all( list.map(async request => { - if(request.bank_code?.type === 'holiday') { - const cost = await this.holidayService.calculateHolidayPay( - request.employee_id, - request.start_date_time, - request.bank_code.modifier - ); - return { ...request, cost }; + // end_date fallback + const endDate = request.end_date_time ?? request.start_date_time; + + //Requested days + const diffDays = Math.round((endDate.getTime() - request.start_date_time.getTime()) / msPerDay) +1; + + // modifier fallback/validation + if (!request.bank_code || request.bank_code.modifier == null) { + throw new BadRequestException(`Modifier manquant pour bank_code_id=${request.bank_code_id}`); } - return request; - }), + const modifier = request.bank_code.modifier; + + let cost: number; + switch (request.bank_code.type) { + case 'holiday' : + cost = await this.holidayService.calculateHolidayPay( request.employee_id, request.start_date_time, modifier ); + break; + case 'vacation' : + cost = await this.vacationService.calculateVacationPay( request.employee_id, request.start_date_time,diffDays, modifier ); + break; + case 'sick' : + cost = await this.sickLeaveService.calculateSickLeavePay( request.employee_id, request.start_date_time, diffDays, modifier ); + break; + default: + cost = diffDays * modifier; + } + return {...request, daysRequested: diffDays, cost }; + }) ); } @@ -56,17 +82,34 @@ export class LeaveRequestsService { if(!request) { throw new NotFoundException(`LeaveRequest #${id} not found`); } - - //search for leave type. if holiday - if (request.bank_code?.type === 'holiday') { - const cost = await this.holidayService.calculateHolidayPay( - request.employee_id, - request.start_date_time, - request.bank_code.modifier, - ); - return { ...request, cost }; + //validation and fallback for end_date_time + const endDate = request.end_date_time ?? request.start_date_time; + + //calculate included days + const msPerDay = 1000 * 60 * 60 * 24; + const diffDays = Math.floor((endDate.getTime() - request.start_date_time.getTime())/ msPerDay) + 1; + + if (!request.bank_code || request.bank_code.modifier == null) { + throw new BadRequestException(`Modifier missing for code ${request.bank_code_id}`); } - return request; + const modifier = request.bank_code.modifier; + + //calculate cost based on bank_code types + let cost = diffDays * modifier; + switch(request.bank_code.type) { + case 'holiday': + cost = await this.holidayService.calculateHolidayPay( request.employee_id, request.start_date_time, modifier ); + break; + case 'vacation': + cost = await this.vacationService.calculateVacationPay( request.employee_id, request.start_date_time, diffDays, modifier ); + break; + case 'sick': + cost = await this.sickLeaveService.calculateSickLeavePay( request.employee_id, request.start_date_time, diffDays, modifier ); + break; + default: + cost = diffDays * modifier; + } + return {...request, daysRequested: diffDays, cost }; } async update( @@ -78,12 +121,12 @@ export class LeaveRequestsService { return this.prisma.leaveRequests.update({ where: { id }, data: { - ...(employee_id !== undefined && { employee_id }), - ...(leave_type !== undefined && { leave_type } ), + ...(employee_id !== undefined && { employee_id }), + ...(leave_type !== undefined && { leave_type } ), ...(start_date_time !== undefined && { start_date_time }), - ...(end_date_time !== undefined && { end_date_time }), - ...(comment !== undefined && { comment }), - ...(approval_status == undefined && { approval_status }), + ...(end_date_time !== undefined && { end_date_time }), + ...(comment !== undefined && { comment }), + ...(approval_status !== undefined && { approval_status }), }, include: { employee: { include: { user:true } } }, }); @@ -112,14 +155,14 @@ export class LeaveRequestsService { //copy unto archive table await transaction.leaveRequestsArchive.createMany({ data: expired.map(request => ({ - leave_request_id: request.id, - employee_id: request.employee_id, - leave_type: request.leave_type, - start_date_time: request.start_date_time, - end_date_time: request.end_date_time, - comment: request.comment, - approval_status: request.approval_status, - })), + leave_request_id: request.id, + employee_id: request.employee_id, + leave_type: request.leave_type, + start_date_time: request.start_date_time, + end_date_time: request.end_date_time, + comment: request.comment, + approval_status: request.approval_status, + })), }); //delete from leave_requests table await transaction.leaveRequests.deleteMany({ From 2d69cfdb86a0622fd9c217dafdc7ac7530a89294 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Fri, 1 Aug 2025 14:54:09 -0400 Subject: [PATCH 19/21] feat(business-logic): implementation of business-logics services into their respective modules. --- src/app.module.ts | 20 ++--- src/business-logics/business-logics.module.ts | 32 ++++++++ .../services/after-hours.service.ts | 79 +++++++++++++++++++ .../services}/holiday.service.ts | 2 +- .../services/mileage.service.ts | 36 +++++++++ .../services}/overtime.service.ts | 2 +- .../services}/sick-leave.service.ts | 2 +- .../services}/vacation.service.ts | 6 +- src/modules/bank-codes/bank-codes.module.ts | 1 - src/modules/expenses/expenses.module.ts | 4 +- .../expenses/services/expenses.service.ts | 66 ++++++++-------- .../leave-requests/leave-requests.module.ts | 16 +--- .../services/leave-requests.service.ts | 6 +- src/modules/shifts/services/shifts.service.ts | 35 +------- src/modules/shifts/shifts.module.ts | 5 +- .../timesheets/services/timesheets.service.ts | 2 +- src/modules/timesheets/timesheets.module.ts | 8 +- 17 files changed, 218 insertions(+), 104 deletions(-) create mode 100644 src/business-logics/business-logics.module.ts create mode 100644 src/business-logics/services/after-hours.service.ts rename src/{business-logic => business-logics/services}/holiday.service.ts (97%) create mode 100644 src/business-logics/services/mileage.service.ts rename src/{business-logic => business-logics/services}/overtime.service.ts (98%) rename src/{business-logic => business-logics/services}/sick-leave.service.ts (97%) rename src/{business-logic => business-logics/services}/vacation.service.ts (98%) diff --git a/src/app.module.ts b/src/app.module.ts index b9c689c..bb77024 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -17,25 +17,27 @@ import { PayperiodsModule } from './modules/pay-periods/pay-periods.module'; import { ScheduleModule } from '@nestjs/schedule'; import { ArchivalModule } from './modules/archival/archival.module'; import { BankCodesModule } from './modules/bank-codes/bank-codes.module'; -import { OvertimeService } from './business-logic/overtime.service'; +import { OvertimeService } from './business-logics/services/overtime.service'; +import { BusinessLogicsModule } from './business-logics/business-logics.module'; @Module({ imports: [ ScheduleModule.forRoot(), - BankCodesModule, ArchivalModule, - PrismaModule, - HealthModule, - UsersModule, - OauthAccessTokensModule, + AuthenticationModule, + BankCodesModule, + BusinessLogicsModule, CustomersModule, EmployeesModule, - LeaveRequestsModule, ExpensesModule, + HealthModule, + LeaveRequestsModule, + OauthAccessTokensModule, + PayperiodsModule, + PrismaModule, ShiftsModule, TimesheetsModule, - AuthenticationModule, - PayperiodsModule, + UsersModule, ], controllers: [AppController, HealthController], providers: [AppService, OvertimeService], diff --git a/src/business-logics/business-logics.module.ts b/src/business-logics/business-logics.module.ts new file mode 100644 index 0000000..f84330d --- /dev/null +++ b/src/business-logics/business-logics.module.ts @@ -0,0 +1,32 @@ +import { Module } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; +//import { AfterHoursService } from "./services/after-hours.service"; +import { HolidayService } from "./services/holiday.service"; +import { OvertimeService } from "./services/overtime.service"; +import { SickLeaveService } from "./services/sick-leave.service"; +import { VacationService } from "./services/vacation.service"; +import { MileageService } from "./services/mileage.service"; + + +//AfterHours is not used, need to clarify infos before implementing into shifts.service +@Module({ + providers: [ + PrismaService, + //AfterHoursService, + HolidayService, + MileageService, + OvertimeService, + SickLeaveService, + VacationService + ], + exports: [ + //AfterHoursService, + HolidayService, + MileageService, + OvertimeService, + SickLeaveService, + VacationService, + ], +}) + +export class BusinessLogicsModule {} \ No newline at end of file diff --git a/src/business-logics/services/after-hours.service.ts b/src/business-logics/services/after-hours.service.ts new file mode 100644 index 0000000..d9aeadf --- /dev/null +++ b/src/business-logics/services/after-hours.service.ts @@ -0,0 +1,79 @@ +import { BadRequestException, Injectable, Logger } from "@nestjs/common"; +import { PrismaService } from "../../prisma/prisma.service"; + + +//THIS SERVICE IS NOT USED RULES TO BE DETERMINED WITH MIKE/HR/ACCOUNTING +@Injectable() +export class AfterHoursService { + private readonly logger = new Logger(AfterHoursService.name); + private static readonly BUSINESS_START = 7; + private static readonly BUSINESS_END = 18; + private static readonly ROUND_MINUTES = 15; + + constructor(private readonly prisma: PrismaService) {} + + + private getPreBusinessMinutes(start: Date, end: Date): number { + const bizStart = new Date(start); + bizStart.setHours(AfterHoursService.BUSINESS_START, 0,0,0); + + if (end>= start || start >= bizStart) { + return 0; + } + + const segmentEnd = end < bizStart ? end : bizStart; + const minutes = (segmentEnd.getTime() - start.getTime()) / 60000; + + this.logger.debug(`getPreBusinessMintutes -> ${minutes.toFixed(1)}min`); + return minutes; + + } + + private getPostBusinessMinutes(start: Date, end: Date): number { + const bizEnd = new Date(start); + bizEnd.setHours(AfterHoursService.BUSINESS_END,0,0,0); + + if( end <= bizEnd ) { + return 0; + } + + const segmentStart = start > bizEnd ? start : bizEnd; + const minutes = (end.getTime() - segmentStart.getTime()) / 60000; + + this.logger.debug(`getPostBusinessMintutes -> ${minutes.toFixed(1)}min`); + return minutes; + + } + + private roundToNearestQUarterMinute(minutes: number): number { + const rounded = Math.round(minutes / AfterHoursService.ROUND_MINUTES) + * AfterHoursService.ROUND_MINUTES; + this.logger.debug(`roundToNearestQuarterMinute -> raw=${minutes.toFixed(1)}min, rounded= ${rounded}min`); + return rounded; + } + + public computeAfterHours(start: Date, end:Date): number { + if(end.getTime() <= start.getTime()) { + throw new BadRequestException('The end cannot be before the starting of the shift'); + } + + if (start.toDateString() !== end.toDateString()) { + throw new BadRequestException('you cannot enter a shift that start in a day and end in the next' + + 'You must create 2 instances, one on the first day and the second during the next day.'); + } + + const preMin = this.getPreBusinessMinutes(start, end); + const postMin = this.getPostBusinessMinutes(start, end); + const rawAftermin = preMin + postMin; + + const roundedMin = this.roundToNearestQUarterMinute(rawAftermin); + + const hours = roundedMin / 60; + const result = parseFloat(hours.toFixed(2)); + + this.logger.debug(`computeAfterHours -> rawAfterMin= ${rawAftermin.toFixed(1)}min, + + rounded = ${roundedMin}min, hours = ${result.toFixed(2)}`); + return result; + } +} + diff --git a/src/business-logic/holiday.service.ts b/src/business-logics/services/holiday.service.ts similarity index 97% rename from src/business-logic/holiday.service.ts rename to src/business-logics/services/holiday.service.ts index 235cc8e..dece70c 100644 --- a/src/business-logic/holiday.service.ts +++ b/src/business-logics/services/holiday.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from "@nestjs/common"; -import { PrismaService } from "src/prisma/prisma.service"; +import { PrismaService } from "../../prisma/prisma.service"; @Injectable() export class HolidayService { diff --git a/src/business-logics/services/mileage.service.ts b/src/business-logics/services/mileage.service.ts new file mode 100644 index 0000000..b233812 --- /dev/null +++ b/src/business-logics/services/mileage.service.ts @@ -0,0 +1,36 @@ +import { BadRequestException, Injectable, Logger } from "@nestjs/common"; +import { PrismaService } from "../../prisma/prisma.service"; +import { Decimal } from "@prisma/client/runtime/library"; + +@Injectable() +export class MileageService { + private readonly logger = new Logger(MileageService.name); + + constructor(private readonly prisma: PrismaService) {} + + public async calculateReimbursement(amount: number, bankCodeId: number): Promise { + if(amount < 0) { + throw new BadRequestException(`The amount most be higher than 0`); + } + + //fetch modifier + const bankCode = await this.prisma.bankCodes.findUnique({ + where: { id: bankCodeId }, + select: { modifier: true, type: true }, + }); + + if(!bankCode) { + throw new BadRequestException(`bank_code ${bankCodeId} not found`); + } + if(bankCode.type !== 'mileage') { + this.logger.warn(`bank_code ${bankCodeId} of type ${bankCode.type} is used for mileage`) + } + + //calculate total amount to reimburs + const reimboursement = amount * bankCode.modifier; + const result = parseFloat(reimboursement.toFixed(2)); + this.logger.debug(`calculateReimbursement -> amount= ${amount}, modifier= ${bankCode.modifier}, total= ${result}`); + return result; + } + +} \ No newline at end of file diff --git a/src/business-logic/overtime.service.ts b/src/business-logics/services/overtime.service.ts similarity index 98% rename from src/business-logic/overtime.service.ts rename to src/business-logics/services/overtime.service.ts index b99122b..5d685a5 100644 --- a/src/business-logic/overtime.service.ts +++ b/src/business-logics/services/overtime.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { PrismaService } from '../../prisma/prisma.service'; @Injectable() export class OvertimeService { diff --git a/src/business-logic/sick-leave.service.ts b/src/business-logics/services/sick-leave.service.ts similarity index 97% rename from src/business-logic/sick-leave.service.ts rename to src/business-logics/services/sick-leave.service.ts index 2057e05..fcbe5e8 100644 --- a/src/business-logic/sick-leave.service.ts +++ b/src/business-logics/services/sick-leave.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from "@nestjs/common"; -import { PrismaService } from "src/prisma/prisma.service"; +import { PrismaService } from "../../prisma/prisma.service"; @Injectable() export class SickLeaveService { diff --git a/src/business-logic/vacation.service.ts b/src/business-logics/services/vacation.service.ts similarity index 98% rename from src/business-logic/vacation.service.ts rename to src/business-logics/services/vacation.service.ts index 094b64d..f726b6e 100644 --- a/src/business-logic/vacation.service.ts +++ b/src/business-logics/services/vacation.service.ts @@ -1,12 +1,12 @@ import { Injectable, Logger, NotFoundException } from "@nestjs/common"; -import { PrismaService } from "src/prisma/prisma.service"; +import { PrismaService } from "../../prisma/prisma.service"; @Injectable() export class VacationService { constructor(private readonly prisma: PrismaService) {} - private readonly logger = new Logger(VacationService.name); -/** + + /** * Calculate the ammount allowed for vacation days. * * @param employeeId employee ID diff --git a/src/modules/bank-codes/bank-codes.module.ts b/src/modules/bank-codes/bank-codes.module.ts index bda3b9b..0030ec7 100644 --- a/src/modules/bank-codes/bank-codes.module.ts +++ b/src/modules/bank-codes/bank-codes.module.ts @@ -1,5 +1,4 @@ import { Module } from "@nestjs/common"; - import { PrismaService } from "src/prisma/prisma.service"; import { BankCodesControllers } from "./controllers/bank-codes.controller"; import { BankCodesService } from "./services/bank-codes.services"; diff --git a/src/modules/expenses/expenses.module.ts b/src/modules/expenses/expenses.module.ts index 08cf449..0171a3e 100644 --- a/src/modules/expenses/expenses.module.ts +++ b/src/modules/expenses/expenses.module.ts @@ -2,10 +2,12 @@ import { PrismaService } from "src/prisma/prisma.service"; import { ExpensesController } from "./controllers/expenses.controller"; import { Module } from "@nestjs/common"; import { ExpensesService } from "./services/expenses.service"; +import { BusinessLogicsModule } from "src/business-logics/business-logics.module"; @Module({ + imports: [BusinessLogicsModule], controllers: [ExpensesController], - providers: [ExpensesService, PrismaService], + providers: [ExpensesService], exports: [ ExpensesService ], }) diff --git a/src/modules/expenses/services/expenses.service.ts b/src/modules/expenses/services/expenses.service.ts index a5b00b2..6b803ca 100644 --- a/src/modules/expenses/services/expenses.service.ts +++ b/src/modules/expenses/services/expenses.service.ts @@ -3,48 +3,55 @@ import { PrismaService } from "src/prisma/prisma.service"; import { CreateExpenseDto } from "../dtos/create-expense"; import { Expenses, ExpensesArchive } from "@prisma/client"; import { UpdateExpenseDto } from "../dtos/update-expense"; +import { MileageService } from "src/business-logics/services/mileage.service"; @Injectable() export class ExpensesService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly mileageService: MileageService, + ) {} async create(dto: CreateExpenseDto): Promise { - const { timesheet_id, bank_code_id, date, amount, + const { timesheet_id, bank_code_id, date, amount:rawAmount, description, is_approved,supervisor_comment} = dto; - return this.prisma.expenses.create({ - data: { timesheet_id, bank_code_id,date,amount,description,is_approved,supervisor_comment}, - include: { - timesheet: { - include: { - employee: { include: { user: true } }, - }, - }, - bank_code: true, - }, + + + //fetches type and modifier + const bankCode = await this.prisma.bankCodes.findUnique({ + where: { id: bank_code_id }, + select: { type: true, modifier: true }, }); + if(!bankCode) { + throw new NotFoundException(`bank_code #${bank_code_id} not found`) + } + + //if mileage -> service, otherwise the ratio is amount:1 + let finalAmount: number; + if(bankCode.type === 'mileage') { + finalAmount = await this.mileageService.calculateReimbursement(rawAmount, bank_code_id); + }else { + finalAmount = parseFloat( (rawAmount * bankCode.modifier).toFixed(2)); + } + + return this.prisma.expenses.create({ + data: { timesheet_id, bank_code_id, date, amount: finalAmount, description, is_approved, supervisor_comment}, + include: { timesheet: { include: { employee: { include: { user: true }}}}, + bank_code: true, + }, + }) } findAll(): Promise { return this.prisma.expenses.findMany({ - include: { - timesheet: { - include: { - employee: { include: { user: true } }, - }, - }, - }, + include: { timesheet: { include: { employee: { include: { user: true } } } } }, }); } async findOne(id: number): Promise { const expense = await this.prisma.expenses.findUnique({ where: { id }, - include: { - timesheet: { - include: { - employee: { include: { user:true } }, - }, - }, + include: { timesheet: { include: { employee: { include: { user:true } } } }, bank_code: true, }, }); @@ -69,14 +76,7 @@ export class ExpensesService { ...(is_approved !== undefined && { is_approved }), ...(supervisor_comment !== undefined && { supervisor_comment }), }, - include: { - timesheet: { - include: { - employee: { - include: { user: true } - }, - }, - }, + include: { timesheet: { include: { employee: { include: { user: true } } } }, bank_code: true, }, }); diff --git a/src/modules/leave-requests/leave-requests.module.ts b/src/modules/leave-requests/leave-requests.module.ts index f000f18..df86084 100644 --- a/src/modules/leave-requests/leave-requests.module.ts +++ b/src/modules/leave-requests/leave-requests.module.ts @@ -1,21 +1,13 @@ -import { PrismaService } from "src/prisma/prisma.service"; import { LeaveRequestController } from "./controllers/leave-requests.controller"; import { LeaveRequestsService } from "./services/leave-requests.service"; import { Module } from "@nestjs/common"; -import { HolidayService } from "src/business-logic/holiday.service"; -import { VacationService } from "src/business-logic/vacation.service"; -import { SickLeaveService } from "src/business-logic/sick-leave.service"; +import { BusinessLogicsModule } from "src/business-logics/business-logics.module"; @Module({ + imports: [BusinessLogicsModule], controllers: [LeaveRequestController], - providers: [ - LeaveRequestsService, - PrismaService, - HolidayService, - VacationService, - SickLeaveService, - ], - exports: [ LeaveRequestsService], + providers: [LeaveRequestsService], + exports: [LeaveRequestsService], }) export class LeaveRequestsModule {} \ No newline at end of file diff --git a/src/modules/leave-requests/services/leave-requests.service.ts b/src/modules/leave-requests/services/leave-requests.service.ts index 9ba05cc..3abcbe0 100644 --- a/src/modules/leave-requests/services/leave-requests.service.ts +++ b/src/modules/leave-requests/services/leave-requests.service.ts @@ -3,9 +3,9 @@ import { PrismaService } from "src/prisma/prisma.service"; import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto"; import { LeaveRequests, LeaveRequestsArchive } from "@prisma/client"; import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto"; -import { HolidayService } from "src/business-logic/holiday.service"; -import { VacationService } from "src/business-logic/vacation.service"; -import { SickLeaveService } from "src/business-logic/sick-leave.service"; +import { HolidayService } from "src/business-logics/services/holiday.service"; +import { SickLeaveService } from "src/business-logics/services/sick-leave.service"; +import { VacationService } from "src/business-logics/services/vacation.service"; @Injectable() export class LeaveRequestsService { diff --git a/src/modules/shifts/services/shifts.service.ts b/src/modules/shifts/services/shifts.service.ts index e7c3942..d1961c0 100644 --- a/src/modules/shifts/services/shifts.service.ts +++ b/src/modules/shifts/services/shifts.service.ts @@ -12,12 +12,7 @@ export class ShiftsService { const { timesheet_id, bank_code_id, date, start_time, end_time } = dto; return this.prisma.shifts.create({ data: { timesheet_id, bank_code_id, date, start_time, end_time }, - include: { - timesheet: { - include: { - employee: { include: { user: true } }, - }, - }, + include: { timesheet: { include: { employee: { include: { user: true } } } }, bank_code: true, }, }); @@ -25,29 +20,14 @@ export class ShiftsService { findAll(): Promise { return this.prisma.shifts.findMany({ - include: { - timesheet: { - include: { - employee: { - include: { user:true } - }, - }, - }, - }, + include: { timesheet: { include: { employee: { include: { user:true } } } } }, }); } async findOne(id: number): Promise { const shift = await this.prisma.shifts.findUnique({ where: { id }, - include: { - timesheet: { - include: { - employee: { - include: { user: true } - }, - }, - }, + include: { timesheet: { include: { employee: { include: { user: true } } } }, bank_code: true, }, }); @@ -69,14 +49,7 @@ export class ShiftsService { ...(start_time !== undefined && { start_time }), ...(end_time !== undefined && { end_time }), }, - include: { - timesheet: { - include: { - employee: { - include: { user: true } - }, - }, - }, + include: { timesheet: { include: { employee: { include: { user: true } } } }, bank_code: true, }, }); diff --git a/src/modules/shifts/shifts.module.ts b/src/modules/shifts/shifts.module.ts index 53924f0..bec2737 100644 --- a/src/modules/shifts/shifts.module.ts +++ b/src/modules/shifts/shifts.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { ShiftsController } from './controllers/shifts.controller'; import { ShiftsService } from './services/shifts.service'; -import { PrismaService } from 'src/prisma/prisma.service'; +import { BusinessLogicsModule } from 'src/business-logics/business-logics.module'; @Module({ + imports: [BusinessLogicsModule], controllers: [ShiftsController], - providers: [ShiftsService, PrismaService], + providers: [ShiftsService], exports: [ShiftsService], }) export class ShiftsModule {} diff --git a/src/modules/timesheets/services/timesheets.service.ts b/src/modules/timesheets/services/timesheets.service.ts index 4cc519d..4beea6b 100644 --- a/src/modules/timesheets/services/timesheets.service.ts +++ b/src/modules/timesheets/services/timesheets.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from 'src/prisma/prisma.service'; import { CreateTimesheetDto } from '../dtos/create-timesheet.dto'; import { Timesheets, TimesheetsArchive } from '@prisma/client'; import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto'; -import { OvertimeService } from 'src/business-logic/overtime.service'; +import { OvertimeService } from 'src/business-logics/services/overtime.service'; @Injectable() export class TimesheetsService { diff --git a/src/modules/timesheets/timesheets.module.ts b/src/modules/timesheets/timesheets.module.ts index 7979a2b..1ab62dd 100644 --- a/src/modules/timesheets/timesheets.module.ts +++ b/src/modules/timesheets/timesheets.module.ts @@ -1,14 +1,12 @@ import { Module } from '@nestjs/common'; import { TimesheetsController } from './controllers/timesheets.controller'; import { TimesheetsService } from './services/timesheets.service'; -import { OvertimeService } from 'src/business-logic/overtime.service'; +import { BusinessLogicsModule } from 'src/business-logics/business-logics.module'; @Module({ + imports: [BusinessLogicsModule], controllers: [TimesheetsController], - providers: [ - TimesheetsService, - OvertimeService, - ], + providers: [ TimesheetsService ], exports: [TimesheetsService], }) export class TimesheetsModule {} From 36be6fb2f139988c909993a317dea6359c77ef11 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Fri, 1 Aug 2025 16:17:58 -0400 Subject: [PATCH 20/21] fix(modules): small ajustment to the structure --- src/app.module.ts | 4 ++-- src/{ => modules}/business-logics/business-logics.module.ts | 0 .../business-logics/services/after-hours.service.ts | 2 +- .../business-logics/services/holiday.service.ts | 2 +- .../business-logics/services/mileage.service.ts | 2 +- .../business-logics/services/overtime.service.ts | 2 +- .../business-logics/services/sick-leave.service.ts | 2 +- .../business-logics/services/vacation.service.ts | 2 +- src/modules/expenses/expenses.module.ts | 2 +- src/modules/expenses/services/expenses.service.ts | 2 +- src/modules/exports/controllers/exports.controller.ts | 0 src/modules/exports/exports.module.ts | 0 src/modules/exports/services/exports.service.ts | 0 src/modules/exports/templates/summary.csv.hbs | 0 src/modules/leave-requests/leave-requests.module.ts | 2 +- .../leave-requests/services/leave-requests.service.ts | 6 +++--- src/modules/shifts/shifts.module.ts | 2 +- src/modules/timesheets/services/timesheets.service.ts | 2 +- src/modules/timesheets/timesheets.module.ts | 2 +- 19 files changed, 17 insertions(+), 17 deletions(-) rename src/{ => modules}/business-logics/business-logics.module.ts (100%) rename src/{ => modules}/business-logics/services/after-hours.service.ts (97%) rename src/{ => modules}/business-logics/services/holiday.service.ts (97%) rename src/{ => modules}/business-logics/services/mileage.service.ts (95%) rename src/{ => modules}/business-logics/services/overtime.service.ts (98%) rename src/{ => modules}/business-logics/services/sick-leave.service.ts (97%) rename src/{ => modules}/business-logics/services/vacation.service.ts (98%) create mode 100644 src/modules/exports/controllers/exports.controller.ts create mode 100644 src/modules/exports/exports.module.ts create mode 100644 src/modules/exports/services/exports.service.ts create mode 100644 src/modules/exports/templates/summary.csv.hbs diff --git a/src/app.module.ts b/src/app.module.ts index bb77024..5f49b87 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -17,8 +17,8 @@ import { PayperiodsModule } from './modules/pay-periods/pay-periods.module'; import { ScheduleModule } from '@nestjs/schedule'; import { ArchivalModule } from './modules/archival/archival.module'; import { BankCodesModule } from './modules/bank-codes/bank-codes.module'; -import { OvertimeService } from './business-logics/services/overtime.service'; -import { BusinessLogicsModule } from './business-logics/business-logics.module'; +import { OvertimeService } from './modules/business-logics/services/overtime.service'; +import { BusinessLogicsModule } from './modules/business-logics/business-logics.module'; @Module({ imports: [ diff --git a/src/business-logics/business-logics.module.ts b/src/modules/business-logics/business-logics.module.ts similarity index 100% rename from src/business-logics/business-logics.module.ts rename to src/modules/business-logics/business-logics.module.ts diff --git a/src/business-logics/services/after-hours.service.ts b/src/modules/business-logics/services/after-hours.service.ts similarity index 97% rename from src/business-logics/services/after-hours.service.ts rename to src/modules/business-logics/services/after-hours.service.ts index d9aeadf..0f857fd 100644 --- a/src/business-logics/services/after-hours.service.ts +++ b/src/modules/business-logics/services/after-hours.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common"; -import { PrismaService } from "../../prisma/prisma.service"; +import { PrismaService } from "../../../prisma/prisma.service"; //THIS SERVICE IS NOT USED RULES TO BE DETERMINED WITH MIKE/HR/ACCOUNTING diff --git a/src/business-logics/services/holiday.service.ts b/src/modules/business-logics/services/holiday.service.ts similarity index 97% rename from src/business-logics/services/holiday.service.ts rename to src/modules/business-logics/services/holiday.service.ts index dece70c..cf898ad 100644 --- a/src/business-logics/services/holiday.service.ts +++ b/src/modules/business-logics/services/holiday.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from "@nestjs/common"; -import { PrismaService } from "../../prisma/prisma.service"; +import { PrismaService } from "../../../prisma/prisma.service"; @Injectable() export class HolidayService { diff --git a/src/business-logics/services/mileage.service.ts b/src/modules/business-logics/services/mileage.service.ts similarity index 95% rename from src/business-logics/services/mileage.service.ts rename to src/modules/business-logics/services/mileage.service.ts index b233812..bf0590c 100644 --- a/src/business-logics/services/mileage.service.ts +++ b/src/modules/business-logics/services/mileage.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common"; -import { PrismaService } from "../../prisma/prisma.service"; +import { PrismaService } from "../../../prisma/prisma.service"; import { Decimal } from "@prisma/client/runtime/library"; @Injectable() diff --git a/src/business-logics/services/overtime.service.ts b/src/modules/business-logics/services/overtime.service.ts similarity index 98% rename from src/business-logics/services/overtime.service.ts rename to src/modules/business-logics/services/overtime.service.ts index 5d685a5..8107c11 100644 --- a/src/business-logics/services/overtime.service.ts +++ b/src/modules/business-logics/services/overtime.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; -import { PrismaService } from '../../prisma/prisma.service'; +import { PrismaService } from '../../../prisma/prisma.service'; @Injectable() export class OvertimeService { diff --git a/src/business-logics/services/sick-leave.service.ts b/src/modules/business-logics/services/sick-leave.service.ts similarity index 97% rename from src/business-logics/services/sick-leave.service.ts rename to src/modules/business-logics/services/sick-leave.service.ts index fcbe5e8..a36bf29 100644 --- a/src/business-logics/services/sick-leave.service.ts +++ b/src/modules/business-logics/services/sick-leave.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from "@nestjs/common"; -import { PrismaService } from "../../prisma/prisma.service"; +import { PrismaService } from "../../../prisma/prisma.service"; @Injectable() export class SickLeaveService { diff --git a/src/business-logics/services/vacation.service.ts b/src/modules/business-logics/services/vacation.service.ts similarity index 98% rename from src/business-logics/services/vacation.service.ts rename to src/modules/business-logics/services/vacation.service.ts index f726b6e..759a0d6 100644 --- a/src/business-logics/services/vacation.service.ts +++ b/src/modules/business-logics/services/vacation.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger, NotFoundException } from "@nestjs/common"; -import { PrismaService } from "../../prisma/prisma.service"; +import { PrismaService } from "../../../prisma/prisma.service"; @Injectable() export class VacationService { diff --git a/src/modules/expenses/expenses.module.ts b/src/modules/expenses/expenses.module.ts index 0171a3e..527d783 100644 --- a/src/modules/expenses/expenses.module.ts +++ b/src/modules/expenses/expenses.module.ts @@ -2,7 +2,7 @@ import { PrismaService } from "src/prisma/prisma.service"; import { ExpensesController } from "./controllers/expenses.controller"; import { Module } from "@nestjs/common"; import { ExpensesService } from "./services/expenses.service"; -import { BusinessLogicsModule } from "src/business-logics/business-logics.module"; +import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module"; @Module({ imports: [BusinessLogicsModule], diff --git a/src/modules/expenses/services/expenses.service.ts b/src/modules/expenses/services/expenses.service.ts index 6b803ca..48b05b3 100644 --- a/src/modules/expenses/services/expenses.service.ts +++ b/src/modules/expenses/services/expenses.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from "src/prisma/prisma.service"; import { CreateExpenseDto } from "../dtos/create-expense"; import { Expenses, ExpensesArchive } from "@prisma/client"; import { UpdateExpenseDto } from "../dtos/update-expense"; -import { MileageService } from "src/business-logics/services/mileage.service"; +import { MileageService } from "src/modules/business-logics/services/mileage.service"; @Injectable() export class ExpensesService { diff --git a/src/modules/exports/controllers/exports.controller.ts b/src/modules/exports/controllers/exports.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/exports/exports.module.ts b/src/modules/exports/exports.module.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/exports/services/exports.service.ts b/src/modules/exports/services/exports.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/exports/templates/summary.csv.hbs b/src/modules/exports/templates/summary.csv.hbs new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/leave-requests/leave-requests.module.ts b/src/modules/leave-requests/leave-requests.module.ts index df86084..7762846 100644 --- a/src/modules/leave-requests/leave-requests.module.ts +++ b/src/modules/leave-requests/leave-requests.module.ts @@ -1,7 +1,7 @@ import { LeaveRequestController } from "./controllers/leave-requests.controller"; import { LeaveRequestsService } from "./services/leave-requests.service"; import { Module } from "@nestjs/common"; -import { BusinessLogicsModule } from "src/business-logics/business-logics.module"; +import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module"; @Module({ imports: [BusinessLogicsModule], diff --git a/src/modules/leave-requests/services/leave-requests.service.ts b/src/modules/leave-requests/services/leave-requests.service.ts index 3abcbe0..2cd2bed 100644 --- a/src/modules/leave-requests/services/leave-requests.service.ts +++ b/src/modules/leave-requests/services/leave-requests.service.ts @@ -3,9 +3,9 @@ import { PrismaService } from "src/prisma/prisma.service"; import { CreateLeaveRequestsDto } from "../dtos/create-leave-requests.dto"; import { LeaveRequests, LeaveRequestsArchive } from "@prisma/client"; import { UpdateLeaveRequestsDto } from "../dtos/update-leave-requests.dto"; -import { HolidayService } from "src/business-logics/services/holiday.service"; -import { SickLeaveService } from "src/business-logics/services/sick-leave.service"; -import { VacationService } from "src/business-logics/services/vacation.service"; +import { HolidayService } from "src/modules/business-logics/services/holiday.service"; +import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service"; +import { VacationService } from "src/modules/business-logics/services/vacation.service"; @Injectable() export class LeaveRequestsService { diff --git a/src/modules/shifts/shifts.module.ts b/src/modules/shifts/shifts.module.ts index bec2737..09d63a1 100644 --- a/src/modules/shifts/shifts.module.ts +++ b/src/modules/shifts/shifts.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { ShiftsController } from './controllers/shifts.controller'; import { ShiftsService } from './services/shifts.service'; -import { BusinessLogicsModule } from 'src/business-logics/business-logics.module'; +import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module'; @Module({ imports: [BusinessLogicsModule], diff --git a/src/modules/timesheets/services/timesheets.service.ts b/src/modules/timesheets/services/timesheets.service.ts index 4beea6b..48b5703 100644 --- a/src/modules/timesheets/services/timesheets.service.ts +++ b/src/modules/timesheets/services/timesheets.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from 'src/prisma/prisma.service'; import { CreateTimesheetDto } from '../dtos/create-timesheet.dto'; import { Timesheets, TimesheetsArchive } from '@prisma/client'; import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto'; -import { OvertimeService } from 'src/business-logics/services/overtime.service'; +import { OvertimeService } from 'src/modules/business-logics/services/overtime.service'; @Injectable() export class TimesheetsService { diff --git a/src/modules/timesheets/timesheets.module.ts b/src/modules/timesheets/timesheets.module.ts index 1ab62dd..debf169 100644 --- a/src/modules/timesheets/timesheets.module.ts +++ b/src/modules/timesheets/timesheets.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { TimesheetsController } from './controllers/timesheets.controller'; import { TimesheetsService } from './services/timesheets.service'; -import { BusinessLogicsModule } from 'src/business-logics/business-logics.module'; +import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module'; @Module({ imports: [BusinessLogicsModule], From ee059429f800859ce22caf019eac0760eeb6bdf8 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Mon, 4 Aug 2025 11:25:45 -0400 Subject: [PATCH 21/21] feat(module): added shifts-validation module. service to export to csv file --- docs/swagger/swagger-spec.json | 766 +++++++++--------- .../exports/controllers/exports.controller.ts | 0 src/modules/exports/exports.module.ts | 0 .../exports/services/exports.service.ts | 0 src/modules/exports/templates/summary.csv.hbs | 0 .../shifts-validation.controller.ts | 51 ++ .../dtos/get-shifts-validation.dto.ts | 10 + .../services/shifts-validation.service.ts | 122 +++ .../validation/shifts-validation.service.ts | 11 + 9 files changed, 577 insertions(+), 383 deletions(-) delete mode 100644 src/modules/exports/controllers/exports.controller.ts delete mode 100644 src/modules/exports/exports.module.ts delete mode 100644 src/modules/exports/services/exports.service.ts delete mode 100644 src/modules/exports/templates/summary.csv.hbs create mode 100644 src/modules/shifts/validation/controllers/shifts-validation.controller.ts create mode 100644 src/modules/shifts/validation/dtos/get-shifts-validation.dto.ts create mode 100644 src/modules/shifts/validation/services/shifts-validation.service.ts create mode 100644 src/modules/shifts/validation/shifts-validation.service.ts diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index be46095..59d8045 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -29,125 +29,6 @@ ] } }, - "/bank-codes": { - "post": { - "operationId": "BankCodesControllers_create", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateBankCodeDto" - } - } - } - }, - "responses": { - "201": { - "description": "Bank code successfully created." - }, - "400": { - "description": "Invalid input data." - } - }, - "summary": "Create a new bank code", - "tags": [ - "BankCodesControllers" - ] - }, - "get": { - "operationId": "BankCodesControllers_findAll", - "parameters": [], - "responses": { - "200": { - "description": "List of bank codes." - } - }, - "summary": "Retrieve all bank codes", - "tags": [ - "BankCodesControllers" - ] - } - }, - "/bank-codes/{id}": { - "get": { - "operationId": "BankCodesControllers_findOne", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "404": { - "description": "Bank code not found." - } - }, - "summary": "Retrieve a bank code by its ID", - "tags": [ - "BankCodesControllers" - ] - }, - "patch": { - "operationId": "BankCodesControllers_update", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateBankCodeDto" - } - } - } - }, - "responses": { - "404": { - "description": "Bank code not found." - } - }, - "summary": "Update an existing bank code", - "tags": [ - "BankCodesControllers" - ] - }, - "delete": { - "operationId": "BankCodesControllers_remove", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "404": { - "description": "Bank code not found." - } - }, - "summary": "Delete a bank code", - "tags": [ - "BankCodesControllers" - ] - } - }, "/archives/employees": { "get": { "operationId": "EmployeesArchiveController_findOneArchived", @@ -1246,124 +1127,107 @@ ] } }, - "/oauth-access-tokens": { - "post": { - "operationId": "OauthAccessTokensController_create", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateOauthAccessTokenDto" - } - } - } - }, - "responses": { - "201": { - "description": "OAuth access token created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OAuthAccessTokenEntity" - } - } - } - }, - "400": { - "description": "Incomplete task or invalid data" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Create OAuth access token", - "tags": [ - "OAuth Access Tokens" - ] - }, + "/auth/login": { "get": { - "operationId": "OauthAccessTokensController_findAll", + "operationId": "AuthController_login", "parameters": [], "responses": { - "201": { - "description": "List of OAuth access token found", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OAuthAccessTokenEntity" - } - } - } - } - }, - "400": { - "description": "List of OAuth access token not found" + "200": { + "description": "" } }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find all OAuth access token", "tags": [ - "OAuth Access Tokens" + "Auth" ] } }, - "/oauth-access-tokens/{id}": { + "/auth/callback": { "get": { - "operationId": "OauthAccessTokensController_findOne", + "operationId": "AuthController_loginCallback", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Auth" + ] + } + }, + "/bank-codes": { + "post": { + "operationId": "BankCodesControllers_create", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBankCodeDto" + } + } + } + }, + "responses": { + "201": { + "description": "Bank code successfully created." + }, + "400": { + "description": "Invalid input data." + } + }, + "summary": "Create a new bank code", + "tags": [ + "BankCodesControllers" + ] + }, + "get": { + "operationId": "BankCodesControllers_findAll", + "parameters": [], + "responses": { + "200": { + "description": "List of bank codes." + } + }, + "summary": "Retrieve all bank codes", + "tags": [ + "BankCodesControllers" + ] + } + }, + "/bank-codes/{id}": { + "get": { + "operationId": "BankCodesControllers_findOne", "parameters": [ { "name": "id", "required": true, "in": "path", "schema": { - "type": "string" + "type": "number" } } ], "responses": { - "201": { - "description": "OAuth access token found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OAuthAccessTokenEntity" - } - } - } - }, - "400": { - "description": "OAuth access token not found" + "404": { + "description": "Bank code not found." } }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find OAuth access token", + "summary": "Retrieve a bank code by its ID", "tags": [ - "OAuth Access Tokens" + "BankCodesControllers" ] }, "patch": { - "operationId": "OauthAccessTokensController_update", + "operationId": "BankCodesControllers_update", "parameters": [ { "name": "id", "required": true, "in": "path", "schema": { - "type": "string" + "type": "number" } } ], @@ -1372,71 +1236,41 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateOauthAccessTokenDto" + "$ref": "#/components/schemas/UpdateBankCodeDto" } } } }, "responses": { - "201": { - "description": "OAuth access token updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OAuthAccessTokenEntity" - } - } - } - }, - "400": { - "description": "OAuth access token not found" + "404": { + "description": "Bank code not found." } }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Update OAuth access token", + "summary": "Update an existing bank code", "tags": [ - "OAuth Access Tokens" + "BankCodesControllers" ] }, "delete": { - "operationId": "OauthAccessTokensController_remove", + "operationId": "BankCodesControllers_remove", "parameters": [ { "name": "id", "required": true, "in": "path", "schema": { - "type": "string" + "type": "number" } } ], "responses": { - "201": { - "description": "OAuth access token deleted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OAuthAccessTokenEntity" - } - } - } - }, - "400": { - "description": "OAuth access token not found" + "404": { + "description": "Bank code not found." } }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Delete OAuth access token", + "summary": "Delete a bank code", "tags": [ - "OAuth Access Tokens" + "BankCodesControllers" ] } }, @@ -1634,31 +1468,197 @@ ] } }, - "/auth/login": { - "get": { - "operationId": "AuthController_login", + "/oauth-access-tokens": { + "post": { + "operationId": "OauthAccessTokensController_create", "parameters": [], - "responses": { - "200": { - "description": "" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOauthAccessTokenDto" + } + } } }, + "responses": { + "201": { + "description": "OAuth access token created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAccessTokenEntity" + } + } + } + }, + "400": { + "description": "Incomplete task or invalid data" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Create OAuth access token", "tags": [ - "Auth" + "OAuth Access Tokens" + ] + }, + "get": { + "operationId": "OauthAccessTokensController_findAll", + "parameters": [], + "responses": { + "201": { + "description": "List of OAuth access token found", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OAuthAccessTokenEntity" + } + } + } + } + }, + "400": { + "description": "List of OAuth access token not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find all OAuth access token", + "tags": [ + "OAuth Access Tokens" ] } }, - "/auth/callback": { + "/oauth-access-tokens/{id}": { "get": { - "operationId": "AuthController_loginCallback", - "parameters": [], + "operationId": "OauthAccessTokensController_findOne", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], "responses": { - "200": { - "description": "" + "201": { + "description": "OAuth access token found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAccessTokenEntity" + } + } + } + }, + "400": { + "description": "OAuth access token not found" } }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find OAuth access token", "tags": [ - "Auth" + "OAuth Access Tokens" + ] + }, + "patch": { + "operationId": "OauthAccessTokensController_update", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOauthAccessTokenDto" + } + } + } + }, + "responses": { + "201": { + "description": "OAuth access token updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAccessTokenEntity" + } + } + } + }, + "400": { + "description": "OAuth access token not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Update OAuth access token", + "tags": [ + "OAuth Access Tokens" + ] + }, + "delete": { + "operationId": "OauthAccessTokensController_remove", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "OAuth access token deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAccessTokenEntity" + } + } + } + }, + "400": { + "description": "OAuth access token not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Delete OAuth access token", + "tags": [ + "OAuth Access Tokens" ] } }, @@ -1850,14 +1850,6 @@ } }, "schemas": { - "CreateBankCodeDto": { - "type": "object", - "properties": {} - }, - "UpdateBankCodeDto": { - "type": "object", - "properties": {} - }, "CreateEmployeeDto": { "type": "object", "properties": { @@ -2337,6 +2329,130 @@ } } }, + "CreateBankCodeDto": { + "type": "object", + "properties": {} + }, + "UpdateBankCodeDto": { + "type": "object", + "properties": {} + }, + "CreateCustomerDto": { + "type": "object", + "properties": { + "first_name": { + "type": "string", + "example": "Gandalf", + "description": "Customer`s first name" + }, + "last_name": { + "type": "string", + "example": "TheGray", + "description": "Customer`s last name" + }, + "email": { + "type": "string", + "example": "you_shall_not_pass@middleEarth.com", + "description": "Customer`s email" + }, + "phone_number": { + "type": "number", + "example": "8436637464", + "description": "Customer`s phone number" + }, + "residence": { + "type": "string", + "example": "1 Ringbearer`s way, Mount Doom city, ME, T1R 1N6 ", + "description": "Customer`s residence" + }, + "invoice_id": { + "type": "number", + "example": "4263253", + "description": "Customer`s invoice number" + } + }, + "required": [ + "first_name", + "last_name", + "email", + "phone_number" + ] + }, + "CustomerEntity": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1, + "description": "Unique ID of a customer(primary-key, auto-incremented)" + }, + "user_id": { + "type": "string", + "example": "0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d", + "description": "UUID of the user linked to that customer" + }, + "email": { + "type": "string", + "example": "you_shall_not_pass@middleEarth.com", + "description": "customer`s email (optional)" + }, + "phone_number": { + "type": "number", + "example": 8436637464, + "description": "customer`s phone number (numbers only)" + }, + "residence": { + "type": "string", + "example": "1 Ringbearer’s way, Mount Doom city, ME, T1R 1N6", + "description": "customer`s residence address (optional)" + }, + "invoice_id": { + "type": "number", + "example": 4263253, + "description": "customer`s invoice number (optionnal, unique)" + } + }, + "required": [ + "id", + "user_id", + "phone_number" + ] + }, + "UpdateCustomerDto": { + "type": "object", + "properties": { + "first_name": { + "type": "string", + "example": "Gandalf", + "description": "Customer`s first name" + }, + "last_name": { + "type": "string", + "example": "TheGray", + "description": "Customer`s last name" + }, + "email": { + "type": "string", + "example": "you_shall_not_pass@middleEarth.com", + "description": "Customer`s email" + }, + "phone_number": { + "type": "number", + "example": "8436637464", + "description": "Customer`s phone number" + }, + "residence": { + "type": "string", + "example": "1 Ringbearer`s way, Mount Doom city, ME, T1R 1N6 ", + "description": "Customer`s residence" + }, + "invoice_id": { + "type": "number", + "example": "4263253", + "description": "Customer`s invoice number" + } + } + }, "CreateOauthAccessTokenDto": { "type": "object", "properties": { @@ -2515,122 +2631,6 @@ } } }, - "CreateCustomerDto": { - "type": "object", - "properties": { - "first_name": { - "type": "string", - "example": "Gandalf", - "description": "Customer`s first name" - }, - "last_name": { - "type": "string", - "example": "TheGray", - "description": "Customer`s last name" - }, - "email": { - "type": "string", - "example": "you_shall_not_pass@middleEarth.com", - "description": "Customer`s email" - }, - "phone_number": { - "type": "number", - "example": "8436637464", - "description": "Customer`s phone number" - }, - "residence": { - "type": "string", - "example": "1 Ringbearer`s way, Mount Doom city, ME, T1R 1N6 ", - "description": "Customer`s residence" - }, - "invoice_id": { - "type": "number", - "example": "4263253", - "description": "Customer`s invoice number" - } - }, - "required": [ - "first_name", - "last_name", - "email", - "phone_number" - ] - }, - "CustomerEntity": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1, - "description": "Unique ID of a customer(primary-key, auto-incremented)" - }, - "user_id": { - "type": "string", - "example": "0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d", - "description": "UUID of the user linked to that customer" - }, - "email": { - "type": "string", - "example": "you_shall_not_pass@middleEarth.com", - "description": "customer`s email (optional)" - }, - "phone_number": { - "type": "number", - "example": 8436637464, - "description": "customer`s phone number (numbers only)" - }, - "residence": { - "type": "string", - "example": "1 Ringbearer’s way, Mount Doom city, ME, T1R 1N6", - "description": "customer`s residence address (optional)" - }, - "invoice_id": { - "type": "number", - "example": 4263253, - "description": "customer`s invoice number (optionnal, unique)" - } - }, - "required": [ - "id", - "user_id", - "phone_number" - ] - }, - "UpdateCustomerDto": { - "type": "object", - "properties": { - "first_name": { - "type": "string", - "example": "Gandalf", - "description": "Customer`s first name" - }, - "last_name": { - "type": "string", - "example": "TheGray", - "description": "Customer`s last name" - }, - "email": { - "type": "string", - "example": "you_shall_not_pass@middleEarth.com", - "description": "Customer`s email" - }, - "phone_number": { - "type": "number", - "example": "8436637464", - "description": "Customer`s phone number" - }, - "residence": { - "type": "string", - "example": "1 Ringbearer`s way, Mount Doom city, ME, T1R 1N6 ", - "description": "Customer`s residence" - }, - "invoice_id": { - "type": "number", - "example": "4263253", - "description": "Customer`s invoice number" - } - } - }, "PayPeriodEntity": { "type": "object", "properties": { diff --git a/src/modules/exports/controllers/exports.controller.ts b/src/modules/exports/controllers/exports.controller.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/exports/exports.module.ts b/src/modules/exports/exports.module.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/exports/services/exports.service.ts b/src/modules/exports/services/exports.service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/exports/templates/summary.csv.hbs b/src/modules/exports/templates/summary.csv.hbs deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/shifts/validation/controllers/shifts-validation.controller.ts b/src/modules/shifts/validation/controllers/shifts-validation.controller.ts new file mode 100644 index 0000000..f5c7426 --- /dev/null +++ b/src/modules/shifts/validation/controllers/shifts-validation.controller.ts @@ -0,0 +1,51 @@ +import { Controller, Get, Header, Query } from "@nestjs/common"; +import { ShiftsValidationService, ValidationRow } from "../services/shifts-validation.service"; +import { GetShiftsValidationDto } from "../dtos/get-shifts-validation.dto"; + +@Controller() +export class ShiftsValidationController { + constructor(private readonly shiftsValidationService: ShiftsValidationService) {} + + @Get() + async getSummary( @Query() query: GetShiftsValidationDto): Promise { + return this.shiftsValidationService.getSummary(query.periodId); + } + + @Get('export.csv') + @Header('Content-Type', 'text/csv; charset=utf-8') + @Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"') + async exportCsv(@Query() query: GetShiftsValidationDto): Promise{ + const rows = await this.shiftsValidationService.getSummary(query.periodId); + + //CSV Headers + const header = [ + 'fullName', + 'supervisor', + 'totalRegularHrs', + 'totalEveningHrs', + 'totalOvertimeHrs', + 'totalExpenses', + 'totalMileage', + 'isValidated' + ].join(',') + '\n'; + + //CSV rows + const body = rows.map(r => { + const esc = (str: string) => `"${str.replace(/"/g, '""')}"`; + + return [ + esc(r.fullName), + esc(r.supervisor), + r.totalRegularHrs.toFixed(2), + r.totalEveningHrs.toFixed(2), + r.totalOvertimeHrs.toFixed(2), + r.totalExpenses.toFixed(2), + r.totalMileage.toFixed(2), + r.isValidated, + ].join(','); + }).join('\n'); + + return Buffer.from(header + body, 'utf8'); + } + +} \ No newline at end of file diff --git a/src/modules/shifts/validation/dtos/get-shifts-validation.dto.ts b/src/modules/shifts/validation/dtos/get-shifts-validation.dto.ts new file mode 100644 index 0000000..44b656b --- /dev/null +++ b/src/modules/shifts/validation/dtos/get-shifts-validation.dto.ts @@ -0,0 +1,10 @@ +import { Type } from "class-transformer"; +import { IsInt, Min, Max } from "class-validator"; + +export class GetShiftsValidationDto { + @Type(()=> Number) + @IsInt() + @Min(1) + @Max(26) + periodId: number; +} \ No newline at end of file diff --git a/src/modules/shifts/validation/services/shifts-validation.service.ts b/src/modules/shifts/validation/services/shifts-validation.service.ts new file mode 100644 index 0000000..5d05f40 --- /dev/null +++ b/src/modules/shifts/validation/services/shifts-validation.service.ts @@ -0,0 +1,122 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; + +export interface ValidationRow { + fullName: string; + supervisor: string; + totalRegularHrs: number; + totalEveningHrs: number; + totalOvertimeHrs: number; + totalExpenses: number; + totalMileage: number; + isValidated: boolean; +} + +@Injectable() +export class ShiftsValidationService { + constructor(private readonly prisma: PrismaService) {} + + private computeHours(start: Date, end: Date): number { + const diffMs = end.getTime() - start.getTime(); + const hours = diffMs / 1000 / 3600; + return parseFloat(hours.toFixed(2)); + } + + async getSummary(periodId: number): Promise { + //fetch pay-period to display + const period = await this.prisma.payPeriods.findUnique({ + where: { period_number: periodId }, + }); + if(!period) { + throw new NotFoundException(`pay-period ${periodId} not found`); + } + const { start_date, end_date } = period; + + //prepare shifts and expenses for display + const shifts = await this.prisma.shifts.findMany({ + where: { date: { gte: start_date, lte: end_date } }, + include: { + bank_code: true, + timesheet: { include: { employee: { + include: { user:true, + supervisor: { include: { user: true } }, + } }, + } }, + }, + }); + + const expenses = await this.prisma.expenses.findMany({ + where: { date: { gte: start_date, lte: end_date } }, + include: { + bank_code: true, + timesheet: { include: { employee: { + include: { user:true, + supervisor: { include: { user:true } }, + } }, + } }, + }, + }); + + const mapRow = new Map(); + + for(const s of shifts) { + const employeeId = s.timesheet.employee.user_id; + const user = s.timesheet.employee.user; + const sup = s.timesheet.employee.supervisor?.user; + + let row = mapRow.get(employeeId); + if(!row) { + row = { + fullName: `${user.first_name} ${user.last_name}`, + supervisor: sup? `${sup.first_name} ${sup.last_name }` : '', + totalRegularHrs: 0, + totalEveningHrs: 0, + totalOvertimeHrs: 0, + totalExpenses: 0, + totalMileage: 0, + isValidated: false, + }; + } + const hours = this.computeHours(s.start_time, s.end_time); + + switch(s.bank_code.type) { + case 'regular' : row.totalRegularHrs += hours; + break; + case 'evening' : row.totalEveningHrs += hours; + break; + case 'overtime' : row.totalOvertimeHrs += hours; + break; + default: row.totalRegularHrs += hours; + } + mapRow.set(employeeId, row); + } + + for(const e of expenses) { + const employeeId = e.timesheet.employee.user_id; + const user = e.timesheet.employee.user; + const sup = e.timesheet.employee.supervisor?.user; + + let row = mapRow.get(employeeId); + if(!row) { + row = { + fullName: `${user.first_name} ${user.last_name}`, + supervisor: sup? `${sup.first_name} ${sup.last_name }` : '', + totalRegularHrs: 0, + totalEveningHrs: 0, + totalOvertimeHrs: 0, + totalExpenses: 0, + totalMileage: 0, + isValidated: false, + }; + } + const amount = Number(e.amount); + row.totalExpenses += amount; + if(e.bank_code.type === 'mileage') { + row.totalMileage += amount; + } + mapRow.set(employeeId, row); + } + //return by default the list of employee in ascending alphabetical order + return Array.from(mapRow.values()).sort((a,b) => a.fullName.localeCompare(b.fullName)); + } +} diff --git a/src/modules/shifts/validation/shifts-validation.service.ts b/src/modules/shifts/validation/shifts-validation.service.ts new file mode 100644 index 0000000..277d2ac --- /dev/null +++ b/src/modules/shifts/validation/shifts-validation.service.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { ShiftsValidationController } from "./controllers/shifts-validation.controller"; +import { ShiftsValidationService } from "./services/shifts-validation.service"; +import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module"; + +@Module({ + imports: [BusinessLogicsModule], + controllers: [ShiftsValidationController], + providers: [ShiftsValidationService], +}) +export class ShiftsValidationModule {} \ No newline at end of file