diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..080b450 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +# Use the official Node.js image as the base image +FROM node:22 + +# Set the working directory inside the container +WORKDIR /app + +# Set the environment variables +ENV DATABASE_URL_PROD="postgresql://apptargo:6wLAZrb0HZnd3mrmqXiArPcqLyui0o9e@10.100.0.116/app_targo_db?schema=public" +ENV DATABASE_URL_STAGING="postgresql://apptargo:6wLAZrb0HZnd3mrmqXiArPcqLyui0o9e@10.100.0.116/app_targo_db_staging?schema=public" +ENV DATABASE_URL_DEV="postgresql://apptargo:6wLAZrb0HZnd3mrmqXiArPcqLyui0o9e@10.100.0.116/app_targo_db_dev?schema=public" + +ENV AUTHENTIK_ISSUER="https://auth.targo.ca/application/o/montargo/" +ENV AUTHENTIK_CLIENT_ID="KUmSmvpu2aDDy4JfNwas7XriNFtPcj2Ka2PyLO5v" +ENV AUTHENTIK_CLIENT_SECRET="N55BgX1mxT7eiY99LOo5zXr5cKz9FgTsaCA9MdC7D8ZuhOGqozvqtNXVGbpY1eCg2kkYwJeJLP89sQ8R4cYybIJI7EwKijb19bzZQpUPwBosWwG3irUwdTnZOyw8yW5i" +ENV AUTHENTIK_CALLBACK_URL="http://10.100.251.2:3420/auth/callback" +ENV AUTHENTIK_AUTH_URL="https://auth.targo.ca/application/o/authorize/" +ENV AUTHENTIK_TOKEN_URL="https://auth.targo.ca/application/o/token/" +ENV AUTHENTIK_USERINFO_URL="https://auth.targo.ca/application/o/userinfo/" + +ENV TARGO_FRONTEND_URI="http://10.100.251.2/" + +ENV ATTACHMENTS_SERVER_ID="server" +ENV ATTACHMENTS_ROOT=C:/ +ENV MAX_UPLOAD_MB=25 +ENV ALLOWED_MIME=image/jpeg,image/png,image/webp,application/pdf + +# Copy package.json and package-lock.json to the working directory +COPY package*.json ./ + +# Install the application dependencies +RUN npm install + +# Copy the rest of the application files +COPY . . + +# Generate Prisma client +RUN npx prisma generate + +# Build the NestJS application +RUN npm run build + +# Expose the application port +EXPOSE 3000 + +# Command to run the application +CMD ["node", "dist/src/main"] diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 15ab59d..a3d57b2 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -1,20 +1,6 @@ { "openapi": "3.0.0", "paths": { - "/": { - "get": { - "operationId": "AppController_getHello", - "parameters": [], - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "App" - ] - } - }, "/health": { "get": { "operationId": "HealthController_check", @@ -71,142 +57,6 @@ ] } }, - "/employees/employee-list": { - "get": { - "operationId": "EmployeesController_findListEmployees", - "parameters": [], - "responses": { - "200": { - "description": "List of employees with scoped info found", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EmployeeListItemDto" - } - } - } - } - }, - "400": { - "description": "List of employees with scoped info not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find all employees with scoped info", - "tags": [ - "Employees" - ] - } - }, - "/employees/{email}": { - "patch": { - "operationId": "EmployeesController_updateOrArchiveOrRestore", - "parameters": [ - { - "name": "email", - "required": true, - "in": "path", - "description": "Email 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/CreateEmployeeDto" - } - } - } - }, - "202": { - "description": "Employee archived successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateEmployeeDto" - } - } - } - }, - "404": { - "description": "Employee not found in active or archive" - } - }, - "security": [ - { - "access-token": [] - }, - { - "access-token": [] - } - ], - "summary": "Update, archive or restore an employee", - "tags": [ - "Employees" - ] - } - }, - "/employees/profile/{email}": { - "get": { - "operationId": "EmployeesController_findOneProfile", - "parameters": [ - { - "name": "email", - "required": true, - "in": "path", - "description": "Identifier of the employee", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Employee profile found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EmployeeProfileItemDto" - } - } - } - }, - "400": { - "description": "Employee profile not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find employee profile", - "tags": [ - "Employees" - ] - } - }, "/notifications/summary": { "get": { "operationId": "NotificationsController_summary", @@ -235,230 +85,26 @@ ] } }, - "/oauth-sessions": { - "post": { - "operationId": "OauthSessionsController_create", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateOauthSessionDto" - } - } - } - }, - "responses": { - "201": { - "description": "OAuth session created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateOauthSessionDto" - } - } - } - }, - "400": { - "description": "Incomplete task or invalid data" - } - }, - "security": [ - { - "sessions": [] - } - ], - "summary": "Create OAuth session", - "tags": [ - "OAuth Sessions" - ] - }, - "get": { - "operationId": "OauthSessionsController_findAll", - "parameters": [], - "responses": { - "201": { - "description": "List of OAuth session found", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CreateOauthSessionDto" - } - } - } - } - }, - "400": { - "description": "List of OAuth session not found" - } - }, - "security": [ - { - "sessions": [] - } - ], - "summary": "Find all OAuth session", - "tags": [ - "OAuth Sessions" - ] - } - }, - "/oauth-sessions/{id}": { - "get": { - "operationId": "OauthSessionsController_findOne", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "responses": { - "201": { - "description": "OAuth session found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateOauthSessionDto" - } - } - } - }, - "400": { - "description": "OAuth session not found" - } - }, - "security": [ - { - "sessions": [] - } - ], - "summary": "Find OAuth session", - "tags": [ - "OAuth Sessions" - ] - }, - "patch": { - "operationId": "OauthSessionsController_update", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOauthSessionDto" - } - } - } - }, - "responses": { - "201": { - "description": "OAuth session updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateOauthSessionDto" - } - } - } - }, - "400": { - "description": "OAuth session not found" - } - }, - "security": [ - { - "sessions": [] - } - ], - "summary": "Update OAuth session", - "tags": [ - "OAuth Sessions" - ] - }, - "delete": { - "operationId": "OauthSessionsController_remove", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "responses": { - "201": { - "description": "OAuth session deleted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateOauthSessionDto" - } - } - } - }, - "400": { - "description": "OAuth session not found" - } - }, - "security": [ - { - "sessions": [] - } - ], - "summary": "Delete OAuth session", - "tags": [ - "OAuth Sessions" - ] - } - }, "/pay-periods/current-and-all": { "get": { "operationId": "PayPeriodsController_getCurrentAndAll", "parameters": [ { "name": "date", - "required": false, + "required": true, "in": "query", - "description": "Override for resolving the current period", "schema": { - "example": "2025-08-11", "type": "string" } } ], "responses": { "200": { - "description": "Find current and all pay periods", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayPeriodBundleDto" - } - } - } + "description": "" } }, - "summary": "Return current pay period and the full list", "tags": [ - "pay-periods" + "PayPeriods" ] } }, @@ -477,22 +123,11 @@ ], "responses": { "200": { - "description": "Pay period found for the selected date", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayPeriodDto" - } - } - } - }, - "404": { - "description": "Pay period not found for the selected date" + "description": "" } }, - "summary": "Resolve a period by a date within it", "tags": [ - "pay-periods" + "PayPeriods" ] } }, @@ -505,7 +140,6 @@ "required": true, "in": "path", "schema": { - "example": 2024, "type": "number" } }, @@ -513,35 +147,46 @@ "name": "periodNumber", "required": true, "in": "path", - "description": "1..26", "schema": { - "example": 1, "type": "number" } } ], "responses": { "200": { - "description": "Pay period found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayPeriodDto" - } - } - } - }, - "404": { - "description": "Pay period not found" + "description": "" } }, - "summary": "Find pay period by year and period number", "tags": [ - "pay-periods" + "PayPeriods" ] } }, - "/pay-periods/{year}/{periodNumber}/{email}": { + "/pay-periods/crew/pay-period-approval": { + "patch": { + "operationId": "PayPeriodsController_bulkApproval", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkCrewApprovalDto" + } + } + } + }, + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "PayPeriods" + ] + } + }, + "/pay-periods/crew/{year}/{periodNumber}": { "get": { "operationId": "PayPeriodsController_getCrewOverview", "parameters": [ @@ -550,7 +195,6 @@ "required": true, "in": "path", "schema": { - "example": 2024, "type": "number" } }, @@ -558,49 +202,18 @@ "name": "periodNumber", "required": true, "in": "path", - "description": "1..26", "schema": { - "example": 1, "type": "number" } - }, - { - "name": "email", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "includeSubtree", - "required": false, - "in": "query", - "description": "Include indirect reports", - "schema": { - "example": false, - "type": "boolean" - } } ], "responses": { "200": { - "description": "Crew overview", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayPeriodOverviewDto" - } - } - } - }, - "404": { - "description": "Pay period not found" + "description": "" } }, - "summary": "Supervisor crew overview for a given pay period", "tags": [ - "pay-periods" + "PayPeriods" ] } }, @@ -613,7 +226,6 @@ "required": true, "in": "path", "schema": { - "example": 2024, "type": "number" } }, @@ -621,52 +233,31 @@ "name": "periodNumber", "required": true, "in": "path", - "description": "1..26", "schema": { - "example": 1, "type": "number" } } ], "responses": { "200": { - "description": "Pay period overview found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayPeriodOverviewDto" - } - } - } - }, - "404": { - "description": "Pay period not found" + "description": "" } }, - "summary": "Detailed view of a pay period by year + number", "tags": [ - "pay-periods" + "PayPeriods" ] } }, "/timesheets": { "get": { - "operationId": "TimesheetController_getTimesheetByIds", + "operationId": "TimesheetController_getTimesheetByPayPeriod", "parameters": [ - { - "name": "employee_email", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - }, { "name": "year", "required": true, "in": "query", "schema": { - "type": "string" + "type": "number" } }, { @@ -674,7 +265,7 @@ "required": true, "in": "query", "schema": { - "type": "string" + "type": "number" } } ], @@ -688,30 +279,9 @@ ] } }, - "/shift": { - "get": { - "operationId": "ShiftController_getShiftsByIds", - "parameters": [ - { - "name": "shift_ids", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "Shift" - ] - }, + "/timesheets/timesheet-approval": { "patch": { - "operationId": "ShiftController_updateBatch", + "operationId": "TimesheetController_approveTimesheet", "parameters": [], "responses": { "200": { @@ -719,23 +289,47 @@ } }, "tags": [ - "Shift" + "Timesheet" ] } }, - "/shift/{timesheet_id}": { - "post": { - "operationId": "ShiftController_createBatch", + "/preferences": { + "patch": { + "operationId": "PreferencesController_updatePreferences", "parameters": [ { - "name": "timesheet_id", + "name": "PreferencesDto", "required": true, - "in": "path", + "in": "body", "schema": { - "type": "number" + "$ref": "#/components/schemas/PreferencesDto" } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "number" + } + } + } + }, + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Preferences" + ] + } + }, + "/shift/create": { + "post": { + "operationId": "ShiftController_createBatch", + "parameters": [], "requestBody": { "required": true, "content": { @@ -759,6 +353,33 @@ ] } }, + "/shift/update": { + "patch": { + "operationId": "ShiftController_updateBatch", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Shift" + ] + } + }, "/shift/{shift_id}": { "delete": { "operationId": "ShiftController_remove", @@ -782,60 +403,10 @@ ] } }, - "/preferences/{email}": { - "patch": { - "operationId": "PreferencesController_updatePreferences", - "parameters": [ - { - "name": "email", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PreferencesDto" - } - } - } - }, - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "Preferences" - ] - } - }, - "/schedule-presets/{email}": { - "put": { - "operationId": "SchedulePresetsController_upsert", - "parameters": [ - { - "name": "email", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "action", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], + "/schedule-presets/create": { + "post": { + "operationId": "SchedulePresetsController_createPreset", + "parameters": [], "requestBody": { "required": true, "content": { @@ -846,6 +417,39 @@ } } }, + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "SchedulePresets" + ] + } + }, + "/schedule-presets/update/{preset_id}": { + "patch": { + "operationId": "SchedulePresetsController_updatePreset", + "parameters": [ + { + "name": "preset_id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchedulePresetsUpdateDto" + } + } + } + }, "responses": { "200": { "description": "" @@ -854,16 +458,18 @@ "tags": [ "SchedulePresets" ] - }, - "get": { - "operationId": "SchedulePresetsController_findListByEmail", + } + }, + "/schedule-presets/delete/{preset_id}": { + "delete": { + "operationId": "SchedulePresetsController_deletePreset", "parameters": [ { - "name": "email", + "name": "preset_id", "required": true, "in": "path", "schema": { - "type": "string" + "type": "number" } } ], @@ -877,35 +483,24 @@ ] } }, - "/schedule-presets/apply-presets/{email}": { + "/schedule-presets/find-list": { + "get": { + "operationId": "SchedulePresetsController_findListById", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "SchedulePresets" + ] + } + }, + "/schedule-presets/apply-presets": { "post": { "operationId": "SchedulePresetsController_applyPresets", - "parameters": [ - { - "name": "email", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "preset", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "start", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], + "parameters": [], "responses": { "201": { "description": "" @@ -915,6 +510,77 @@ "SchedulePresets" ] } + }, + "/expense/create": { + "post": { + "operationId": "ExpenseController_create", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExpenseDto" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "Expense" + ] + } + }, + "/expense/update": { + "patch": { + "operationId": "ExpenseController_update", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExpenseDto" + } + } + } + }, + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Expense" + ] + } + }, + "/expense/delete/{expense_id}": { + "delete": { + "operationId": "ExpenseController_remove", + "parameters": [ + { + "name": "expense_id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Expense" + ] + } } }, "info": { @@ -974,432 +640,10 @@ } }, "schemas": { - "EmployeeListItemDto": { + "BulkCrewApprovalDto": { "type": "object", "properties": {} }, - "UpdateEmployeeDto": { - "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" - }, - "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": "string", - "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": 7464, - "description": "external ID for the pay system" - }, - "company_code": { - "type": "number", - "example": 335567447, - "description": "Employee`s company code" - }, - "job_title": { - "type": "string", - "example": "technicient", - "description": "employee`s job title" - }, - "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" - } - } - }, - "CreateEmployeeDto": { - "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" - }, - "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": "string", - "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": 7464, - "description": "external ID for the pay system" - }, - "company_code": { - "type": "number", - "example": 335567447, - "description": "Employee`s company code" - }, - "job_title": { - "type": "string", - "example": "technicient", - "description": "employee`s job title" - }, - "first_work_day": { - "type": "string", - "example": "23/09/3018", - "description": "Employee`s first working day" - }, - "last_work_day": { - "type": "string", - "example": "25/03/3019", - "description": "Employee`s last working day" - } - }, - "required": [ - "id", - "user_id", - "first_name", - "last_name", - "email", - "phone_number", - "external_payroll_id", - "company_code", - "job_title", - "first_work_day" - ] - }, - "EmployeeProfileItemDto": { - "type": "object", - "properties": {} - }, - "CreateOauthSessionDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "example": "cklwi0vb70000z2z20q6f19qk", - "description": "Unique ID of an OAuth token (auto-generated)" - }, - "user_id": { - "type": "string", - "example": "S7A2U8R7O6N6", - "description": "User`s unique identification number" - }, - "application": { - "type": "string", - "example": "app.targo.ca", - "description": "URL in which the access token is used for" - }, - "access_token": { - "type": "string", - "example": "L5O6R4D3/O6F3#T8H4E3&R6I4N6G4S7 ...", - "description": "Access token" - }, - "refresh_token": { - "type": "string", - "example": "Th3731102h1p07Th3R1n92", - "description": "Refresh token" - }, - "access_token_expiry": { - "format": "date-time", - "type": "string", - "example": "25/12/3018", - "description": "Access token`s expiry date" - }, - "refresh_token_expiry": { - "format": "date-time", - "type": "string", - "example": "26/02/3019", - "description": "Refresh token`s expiry date" - }, - "scopes": { - "example": "access tolkiens, email, etc... ", - "description": "scopes of infos linked to the access token", - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "id", - "user_id", - "application", - "access_token", - "refresh_token", - "access_token_expiry" - ] - }, - "UpdateOauthSessionDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "example": "cklwi0vb70000z2z20q6f19qk", - "description": "Unique ID of an OAuth token (auto-generated)" - }, - "user_id": { - "type": "string", - "example": "S7A2U8R7O6N6", - "description": "User`s unique identification number" - }, - "application": { - "type": "string", - "example": "app.targo.ca", - "description": "URL in which the access token is used for" - }, - "access_token": { - "type": "string", - "example": "L5O6R4D3/O6F3#T8H4E3&R6I4N6G4S7 ...", - "description": "Access token" - }, - "refresh_token": { - "type": "string", - "example": "Th3731102h1p07Th3R1n92", - "description": "Refresh token" - }, - "access_token_expiry": { - "format": "date-time", - "type": "string", - "example": "25/12/3018", - "description": "Access token`s expiry date" - }, - "refresh_token_expiry": { - "format": "date-time", - "type": "string", - "example": "26/02/3019", - "description": "Refresh token`s expiry date" - }, - "scopes": { - "example": "access tolkiens, email, etc... ", - "description": "scopes of infos linked to the access token", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "PayPeriodDto": { - "type": "object", - "properties": { - "pay_period_no": { - "type": "number", - "example": 1, - "description": "numéro cyclique de la période entre 1 et 26" - }, - "period_start": { - "type": "string", - "example": "2023-12-17", - "format": "date" - }, - "period_end": { - "type": "string", - "example": "2023-12-30", - "format": "date" - }, - "payday": { - "type": "string", - "example": "2023-01-04", - "format": "date" - }, - "pay_year": { - "type": "number", - "example": 2023 - }, - "label": { - "type": "string", - "example": "2023-12-17 → 2023-12-30" - } - }, - "required": [ - "pay_period_no", - "period_start", - "period_end", - "payday", - "pay_year", - "label" - ] - }, - "PayPeriodBundleDto": { - "type": "object", - "properties": { - "current": { - "description": "Current pay period (resolved from date)", - "allOf": [ - { - "$ref": "#/components/schemas/PayPeriodDto" - } - ] - }, - "periods": { - "description": "All pay periods", - "type": "array", - "items": { - "$ref": "#/components/schemas/PayPeriodDto" - } - } - }, - "required": [ - "current", - "periods" - ] - }, - "EmployeePeriodOverviewDto": { - "type": "object", - "properties": { - "employee_name": { - "type": "string", - "example": "Alex Dupont", - "description": "Nom complet de lemployé" - }, - "regular_hours": { - "type": "number", - "example": 40, - "description": "pay-period`s regular hours" - }, - "other_hours": { - "type": "object", - "example": 0, - "description": "pay-period`s other hours" - }, - "expenses": { - "type": "number", - "example": 420.69, - "description": "pay-period`s total expenses ($)" - }, - "mileage": { - "type": "number", - "example": 40, - "description": "pay-period total mileages (km)" - }, - "is_approved": { - "type": "boolean", - "example": true, - "description": "Tous les timesheets de la période sont approuvés pour cet employé" - } - }, - "required": [ - "employee_name", - "regular_hours", - "other_hours", - "expenses", - "mileage", - "is_approved" - ] - }, - "PayPeriodOverviewDto": { - "type": "object", - "properties": { - "pay_period_no": { - "type": "number", - "example": 1, - "description": "Period number (1–26)" - }, - "pay_year": { - "type": "number", - "example": 2023, - "description": "Calendar year of the period" - }, - "period_start": { - "type": "string", - "example": "2023-12-17", - "format": "date", - "description": "Period start date (YYYY-MM-DD)" - }, - "period_end": { - "type": "string", - "example": "2023-12-30", - "format": "date", - "description": "Period end date (YYYY-MM-DD)" - }, - "payday": { - "type": "string", - "example": "2023-12-30", - "format": "date", - "description": "Period pay day(YYYY-MM-DD)" - }, - "label": { - "type": "string", - "example": "2023-12-17 → 2023-12-30", - "description": "Human-readable label" - }, - "employees_overview": { - "description": "Per-employee overview for the period", - "type": "array", - "items": { - "$ref": "#/components/schemas/EmployeePeriodOverviewDto" - } - } - }, - "required": [ - "pay_period_no", - "pay_year", - "period_start", - "period_end", - "payday", - "label", - "employees_overview" - ] - }, "PreferencesDto": { "type": "object", "properties": {} @@ -1407,6 +651,14 @@ "SchedulePresetsDto": { "type": "object", "properties": {} + }, + "SchedulePresetsUpdateDto": { + "type": "object", + "properties": {} + }, + "ExpenseDto": { + "type": "object", + "properties": {} } } } diff --git a/package-lock.json b/package-lock.json index 1180132..9b49166 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@nestjs/platform-express": "^11.1.6", "@nestjs/schedule": "^6.0.0", "@nestjs/swagger": "^11.2.0", - "@prisma/client": "^6.17.1", + "@prisma/client": "^6.18.0", "bullmq": "^5.58.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", @@ -54,7 +54,7 @@ "globals": "^16.0.0", "jest": "^29.7.0", "prettier": "^3.4.2", - "prisma": "^6.17.1", + "prisma": "^6.18.0", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "ts-jest": "^29.2.5", @@ -243,6 +243,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -3113,6 +3114,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3268,13 +3270,14 @@ } }, "node_modules/@nestjs/common": { - "version": "11.1.6", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz", - "integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==", + "version": "11.1.7", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.7.tgz", + "integrity": "sha512-lwlObwGgIlpXSXYOTpfzdCepUyWomz6bv9qzGzzvpgspUxkj0Uz0fUJcvD44V8Ps7QhKW3lZBoYbXrH25UZrbA==", + "peer": true, "dependencies": { "file-type": "21.0.0", "iterare": "1.2.1", - "load-esm": "1.0.2", + "load-esm": "1.0.3", "tslib": "2.8.1", "uid": "2.0.2" }, @@ -3312,15 +3315,16 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.6", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.6.tgz", - "integrity": "sha512-siWX7UDgErisW18VTeJA+x+/tpNZrJewjTBsRPF3JVxuWRuAB1kRoiJcxHgln8Lb5UY9NdvklITR84DUEXD0Cg==", + "version": "11.1.7", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.7.tgz", + "integrity": "sha512-TyXFOwjhHv/goSgJ8i20K78jwTM0iSpk9GBcC2h3mf4MxNy+znI8m7nWjfoACjTkb89cTwDQetfTHtSfGLLaiA==", "hasInstallScript": true, + "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", - "path-to-regexp": "8.2.0", + "path-to-regexp": "8.3.0", "tslib": "2.8.1", "uid": "2.0.2" }, @@ -3392,14 +3396,15 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.1.6", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.6.tgz", - "integrity": "sha512-HErwPmKnk+loTq8qzu1up+k7FC6Kqa8x6lJ4cDw77KnTxLzsCaPt+jBvOq6UfICmfqcqCCf3dKXg+aObQp+kIQ==", + "version": "11.1.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.7.tgz", + "integrity": "sha512-5T+GLdvTiGPKB4/P4PM9ftKUKNHJy8ThEFhZA3vQnXVL7Vf0rDr07TfVTySVu+XTh85m1lpFVuyFM6u6wLNsRA==", + "peer": true, "dependencies": { "cors": "2.8.5", "express": "5.1.0", "multer": "2.0.2", - "path-to-regexp": "8.2.0", + "path-to-regexp": "8.3.0", "tslib": "2.8.1" }, "funding": { @@ -3547,19 +3552,10 @@ } } }, - "node_modules/@nestjs/swagger/node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/@nestjs/testing": { - "version": "11.1.6", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.6.tgz", - "integrity": "sha512-srYzzDNxGvVCe1j0SpTS9/ix75PKt6Sn6iMaH1rpJ6nj2g8vwNrhK0CoJJXvpCYgrnI+2WES2pprYnq8rAMYHA==", + "version": "11.1.7", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.7.tgz", + "integrity": "sha512-QbtrgSlc3QVo6RHNxTTlyhaiobLLy8kvhOlgWHsoXRknybuRs7vZg4k5mo3ye6pITGeT3CrWIRpZjUsh5Wps5Q==", "dev": true, "dependencies": { "tslib": "2.8.1" @@ -3667,9 +3663,9 @@ } }, "node_modules/@prisma/client": { - "version": "6.17.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.17.1.tgz", - "integrity": "sha512-zL58jbLzYamjnNnmNA51IOZdbk5ci03KviXCuB0Tydc9btH2kDWsi1pQm2VecviRTM7jGia0OPPkgpGnT3nKvw==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.18.0.tgz", + "integrity": "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA==", "hasInstallScript": true, "engines": { "node": ">=18.18" @@ -3688,60 +3684,60 @@ } }, "node_modules/@prisma/config": { - "version": "6.17.1", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.1.tgz", - "integrity": "sha512-fs8wY6DsvOCzuiyWVckrVs1LOcbY4LZNz8ki4uUIQ28jCCzojTGqdLhN2Jl5lDnC1yI8/gNIKpsWDM8pLhOdwA==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.18.0.tgz", + "integrity": "sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ==", "devOptional": true, "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", - "effect": "3.16.12", + "effect": "3.18.4", "empathic": "2.0.0" } }, "node_modules/@prisma/debug": { - "version": "6.17.1", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.1.tgz", - "integrity": "sha512-Vf7Tt5Wh9XcndpbmeotuqOMLWPTjEKCsgojxXP2oxE1/xYe7PtnP76hsouG9vis6fctX+TxgmwxTuYi/+xc7dQ==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz", + "integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==", "devOptional": true }, "node_modules/@prisma/engines": { - "version": "6.17.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.1.tgz", - "integrity": "sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.18.0.tgz", + "integrity": "sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/debug": "6.17.1", - "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", - "@prisma/fetch-engine": "6.17.1", - "@prisma/get-platform": "6.17.1" + "@prisma/debug": "6.18.0", + "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", + "@prisma/fetch-engine": "6.18.0", + "@prisma/get-platform": "6.18.0" } }, "node_modules/@prisma/engines-version": { - "version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac.tgz", - "integrity": "sha512-17140E3huOuD9lMdJ9+SF/juOf3WR3sTJMVyyenzqUPbuH+89nPhSWcrY+Mf7tmSs6HvaO+7S+HkELinn6bhdg==", + "version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f.tgz", + "integrity": "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==", "devOptional": true }, "node_modules/@prisma/fetch-engine": { - "version": "6.17.1", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.1.tgz", - "integrity": "sha512-AYZiHOs184qkDMiTeshyJCtyL4yERkjfTkJiSJdYuSfc24m94lTNL5+GFinZ6vVz+ktX4NJzHKn1zIFzGTWrWg==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.18.0.tgz", + "integrity": "sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A==", "devOptional": true, "dependencies": { - "@prisma/debug": "6.17.1", - "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", - "@prisma/get-platform": "6.17.1" + "@prisma/debug": "6.18.0", + "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", + "@prisma/get-platform": "6.18.0" } }, "node_modules/@prisma/get-platform": { - "version": "6.17.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.1.tgz", - "integrity": "sha512-AKEn6fsfz0r482S5KRDFlIGEaq9wLNcgalD1adL+fPcFFblIKs1sD81kY/utrHdqKuVC6E1XSRpegDK3ZLL4Qg==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.18.0.tgz", + "integrity": "sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg==", "devOptional": true, "dependencies": { - "@prisma/debug": "6.17.1" + "@prisma/debug": "6.18.0" } }, "node_modules/@scarf/scarf": { @@ -3803,6 +3799,7 @@ "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.6.0.tgz", "integrity": "sha512-Q5FsI3Cw0fGMXhmsg7c08i4EmXCrcl+WnAxb6LYOLHw4JFFC3yzmx9LaXZ7QMbA+JZXbigU2TirI7RAfO0Qlnw==", "dev": true, + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@xhmikosr/bin-wrapper": "^13.0.5", @@ -3871,6 +3868,7 @@ "integrity": "sha512-CJSn2vstd17ddWIHBsjuD4OQnn9krQfaq6EO+w9YfId5DKznyPmzxAARlOXG99cC8/3Kli8ysKy6phL43bSr0w==", "dev": true, "hasInstallScript": true, + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.23" @@ -4207,6 +4205,7 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -4366,6 +4365,7 @@ "version": "22.17.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.2.tgz", "integrity": "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4546,6 +4546,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz", "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", @@ -5449,6 +5450,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5461,7 +5463,6 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, - "peer": true, "engines": { "node": ">=10.13.0" }, @@ -5495,6 +5496,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5962,6 +5964,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -6240,6 +6243,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "devOptional": true, + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -6292,12 +6296,14 @@ "node_modules/class-transformer": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", - "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "peer": true }, "node_modules/class-validator": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "peer": true, "dependencies": { "@types/validator": "^13.11.8", "libphonenumber-js": "^1.11.1", @@ -6953,9 +6959,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/effect": { - "version": "3.16.12", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", - "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", "devOptional": true, "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -7160,6 +7166,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -7220,6 +7227,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", "dev": true, + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8726,6 +8734,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -9542,9 +9551,9 @@ "dev": true }, "node_modules/load-esm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.2.tgz", - "integrity": "sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", "funding": [ { "type": "github", @@ -10362,6 +10371,7 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -10468,11 +10478,12 @@ } }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "engines": { - "node": ">=16" + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/path-type": { @@ -10654,6 +10665,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10703,14 +10715,15 @@ } }, "node_modules/prisma": { - "version": "6.17.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.1.tgz", - "integrity": "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.18.0.tgz", + "integrity": "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==", "devOptional": true, "hasInstallScript": true, + "peer": true, "dependencies": { - "@prisma/config": "6.17.1", - "@prisma/engines": "6.17.1" + "@prisma/config": "6.18.0", + "@prisma/engines": "6.18.0" }, "bin": { "prisma": "build/index.js" @@ -10927,7 +10940,8 @@ "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "peer": true }, "node_modules/repeat-string": { "version": "1.6.1", @@ -11116,6 +11130,7 @@ "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -11925,6 +11940,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -12233,6 +12249,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12390,6 +12407,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12572,9 +12590,10 @@ } }, "node_modules/validator": { - "version": "13.15.15", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", - "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "version": "13.15.20", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", + "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -12719,7 +12738,6 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -12737,7 +12755,6 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -12750,7 +12767,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -12764,7 +12780,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, - "peer": true, "engines": { "node": ">=4.0" } @@ -12773,15 +12788,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "peer": true + "dev": true }, "node_modules/webpack/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "peer": true, "engines": { "node": ">= 0.6" } @@ -12791,7 +12804,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -12804,7 +12816,6 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/package.json b/package.json index c8db806..589f94f 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@nestjs/platform-express": "^11.1.6", "@nestjs/schedule": "^6.0.0", "@nestjs/swagger": "^11.2.0", - "@prisma/client": "^6.17.1", + "@prisma/client": "^6.18.0", "bullmq": "^5.58.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", @@ -86,7 +86,7 @@ "globals": "^16.0.0", "jest": "^29.7.0", "prettier": "^3.4.2", - "prisma": "^6.17.1", + "prisma": "^6.18.0", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "ts-jest": "^29.2.5", diff --git a/prisma/mock-seeds-scripts/04-customers.ts b/prisma/mock-seeds-scripts/04-customers.ts index aaa63b6..b8319cf 100644 --- a/prisma/mock-seeds-scripts/04-customers.ts +++ b/prisma/mock-seeds-scripts/04-customers.ts @@ -1,28 +1,28 @@ -import { PrismaClient, Roles } from '@prisma/client'; +// import { PrismaClient, Roles } from '@prisma/client'; -const prisma = new PrismaClient(); +// const prisma = new PrismaClient(); -async function main() { - const customerUsers = await prisma.users.findMany({ - where: { role: Roles.CUSTOMER }, - orderBy: { email: 'asc' }, - }); +// async function main() { +// const customerUsers = await prisma.users.findMany({ +// where: { role: Roles.CUSTOMER }, +// orderBy: { email: 'asc' }, +// }); - let i = 0; - for (const u of customerUsers) { - await prisma.customers.upsert({ - where: { user_id: u.id }, - update: {}, - create: { - user_id: u.id, - invoice_id: i % 2 === 0 ? 100000 + i : null, // 1 sur 2 a un invoice_id - }, - }); - i++; - } +// let i = 0; +// for (const u of customerUsers) { +// await prisma.customers.upsert({ +// where: { user_id: u.id }, +// update: {}, +// create: { +// user_id: u.id, +// invoice_id: i % 2 === 0 ? 100000 + i : null, // 1 sur 2 a un invoice_id +// }, +// }); +// i++; +// } - const total = await prisma.customers.count(); - console.log(`✓ Customers: ${total} rows`); -} +// const total = await prisma.customers.count(); +// console.log(`✓ Customers: ${total} rows`); +// } -main().finally(() => prisma.$disconnect()); +// main().finally(() => prisma.$disconnect()); diff --git a/prisma/mock-seeds-scripts/05-employees-archive.ts b/prisma/mock-seeds-scripts/05-employees-archive.ts index 1687b23..b352a7f 100644 --- a/prisma/mock-seeds-scripts/05-employees-archive.ts +++ b/prisma/mock-seeds-scripts/05-employees-archive.ts @@ -1,45 +1,45 @@ -import { PrismaClient } from '@prisma/client'; +// import { PrismaClient } from '@prisma/client'; -if (process.env.SKIP_LEAVE_REQUESTS === 'true') { - console.log("⏭ Seed leave-requests ignoré (SKIP_LEAVE_REQUESTS=true)"); - process.exit(0); -} +// if (process.env.SKIP_LEAVE_REQUESTS === 'true') { +// console.log("⏭ Seed leave-requests ignoré (SKIP_LEAVE_REQUESTS=true)"); +// process.exit(0); +// } -const prisma = new PrismaClient(); +// const prisma = new PrismaClient(); -function daysAgo(n: number) { - const d = new Date(); - d.setDate(d.getDate() - n); - d.setHours(0,0,0,0); - return d; -} +// function daysAgo(n: number) { +// const d = new Date(); +// d.setDate(d.getDate() - n); +// d.setHours(0,0,0,0); +// return d; +// } -async function main() { - const employees = await prisma.employees.findMany({ - include: { user: true }, - take: 10, // archive 10 - }); +// async function main() { +// const employees = await prisma.employees.findMany({ +// include: { user: true }, +// take: 10, // archive 10 +// }); - for (const e of employees) { - await prisma.employeesArchive.create({ - data: { - employee_id: e.id, - user_id: e.user_id, - first_name: e.user.first_name, - last_name: e.user.last_name, - external_payroll_id: e.external_payroll_id, - company_code: e.company_code, - first_work_day: e.first_work_day, - last_work_day: daysAgo(30), - supervisor_id: e.supervisor_id ?? null, - job_title: e.job_title, - is_supervisor: e.is_supervisor, - }, - }); - } +// for (const e of employees) { +// await prisma.employeesArchive.create({ +// data: { +// employee_id: e.id, +// user_id: e.user_id, +// first_name: e.user.first_name, +// last_name: e.user.last_name, +// external_payroll_id: e.external_payroll_id, +// company_code: e.company_code, +// first_work_day: e.first_work_day, +// last_work_day: daysAgo(30), +// supervisor_id: e.supervisor_id ?? null, +// job_title: e.job_title, +// is_supervisor: e.is_supervisor, +// }, +// }); +// } - const total = await prisma.employeesArchive.count(); - console.log(`✓ EmployeesArchive: ${total} rows (added 10)`); -} +// const total = await prisma.employeesArchive.count(); +// console.log(`✓ EmployeesArchive: ${total} rows (added 10)`); +// } -main().finally(() => prisma.$disconnect()); +// main().finally(() => prisma.$disconnect()); diff --git a/prisma/mock-seeds-scripts/06-customers-archive.ts b/prisma/mock-seeds-scripts/06-customers-archive.ts index 31a7d01..0c2a382 100644 --- a/prisma/mock-seeds-scripts/06-customers-archive.ts +++ b/prisma/mock-seeds-scripts/06-customers-archive.ts @@ -1,35 +1,35 @@ -// prisma/mock-seeds-scripts/06-customers-archive.ts -import { PrismaClient } from '@prisma/client'; +// // prisma/mock-seeds-scripts/06-customers-archive.ts +// import { PrismaClient } from '@prisma/client'; -if (process.env.SKIP_LEAVE_REQUESTS === 'true') { - console.log("⏭ Seed leave-requests ignoré (SKIP_LEAVE_REQUESTS=true)"); - process.exit(0); -} +// if (process.env.SKIP_LEAVE_REQUESTS === 'true') { +// console.log("⏭ Seed leave-requests ignoré (SKIP_LEAVE_REQUESTS=true)"); +// process.exit(0); +// } -const prisma = new PrismaClient(); +// const prisma = new PrismaClient(); -async function main() { - const customers = await prisma.customers.findMany({ - orderBy: { id: 'asc' }, - take: 5, - }); +// async function main() { +// const customers = await prisma.customers.findMany({ +// orderBy: { id: 'asc' }, +// take: 5, +// }); - for (const c of customers) { - const invoiceId = 200000 + c.id; // déterministe, stable entre runs +// for (const c of customers) { +// const invoiceId = 200000 + c.id; // déterministe, stable entre runs - await prisma.customersArchive.upsert({ - where: { invoice_id: invoiceId }, // invoice_id est unique - update: {}, // idempotent - create: { - customer_id: c.id, - user_id: c.user_id, - invoice_id: invoiceId, - }, - }); - } +// await prisma.customersArchive.upsert({ +// where: { invoice_id: invoiceId }, // invoice_id est unique +// update: {}, // idempotent +// create: { +// customer_id: c.id, +// user_id: c.user_id, +// invoice_id: invoiceId, +// }, +// }); +// } - const total = await prisma.customersArchive.count(); - console.log(`✓ CustomersArchive upserted for ${customers.length} customers (total=${total})`); -} +// const total = await prisma.customersArchive.count(); +// console.log(`✓ CustomersArchive upserted for ${customers.length} customers (total=${total})`); +// } -main().finally(() => prisma.$disconnect()); +// main().finally(() => prisma.$disconnect()); diff --git a/prisma/mock-seeds-scripts/08-leave-requests-archive.ts b/prisma/mock-seeds-scripts/08-leave-requests-archive.ts index 45b1d43..0170635 100644 --- a/prisma/mock-seeds-scripts/08-leave-requests-archive.ts +++ b/prisma/mock-seeds-scripts/08-leave-requests-archive.ts @@ -1,87 +1,87 @@ -import { PrismaClient, LeaveApprovalStatus, LeaveTypes } from '@prisma/client'; +// import { PrismaClient, LeaveApprovalStatus, LeaveTypes } from '@prisma/client'; -if (process.env.SKIP_LEAVE_REQUESTS === 'true') { - console.log('?? Seed leave-requests ignor (SKIP_LEAVE_REQUESTS=true)'); - process.exit(0); -} +// if (process.env.SKIP_LEAVE_REQUESTS === 'true') { +// console.log('?? Seed leave-requests ignor� (SKIP_LEAVE_REQUESTS=true)'); +// process.exit(0); +// } -const prisma = new PrismaClient(); +// const prisma = new PrismaClient(); -function daysAgo(n: number) { - const d = new Date(); - d.setUTCDate(d.getUTCDate() - n); - d.setUTCHours(0, 0, 0, 0); - return d; -} +// function daysAgo(n: number) { +// const d = new Date(); +// d.setUTCDate(d.getUTCDate() - n); +// d.setUTCHours(0, 0, 0, 0); +// return d; +// } -async function main() { - const employees = await prisma.employees.findMany({ select: { id: true } }); - if (!employees.length) { - throw new Error('Aucun employ trouv. Excute le seed employees avant celui-ci.'); - } +// async function main() { +// const employees = await prisma.employees.findMany({ select: { id: true } }); +// if (!employees.length) { +// throw new Error('Aucun employ� trouv�. Ex�cute le seed employees avant celui-ci.'); +// } - const leaveCodes = await prisma.bankCodes.findMany({ - where: { type: { in: ['SICK', 'VACATION', 'HOLIDAY'] } }, - select: { id: true, type: true }, - }); - if (!leaveCodes.length) { - throw new Error("Aucun bank code trouv avec type in ('SICK','VACATION','HOLIDAY'). Vrifie ta table bank_codes."); - } +// const leaveCodes = await prisma.bankCodes.findMany({ +// where: { type: { in: ['SICK', 'VACATION', 'HOLIDAY'] } }, +// select: { id: true, type: true }, +// }); +// if (!leaveCodes.length) { +// throw new Error("Aucun bank code trouv� avec type in ('SICK','VACATION','HOLIDAY'). V�rifie ta table bank_codes."); +// } - const statuses = Object.values(LeaveApprovalStatus); - const created = [] as Array<{ id: number; employee_id: number; leave_type: LeaveTypes; date: Date; comment: string; approval_status: LeaveApprovalStatus; requested_hours: number; payable_hours: number | null }>; +// const statuses = Object.values(LeaveApprovalStatus); +// const created = [] as Array<{ id: number; employee_id: number; leave_type: LeaveTypes; date: Date; comment: string; approval_status: LeaveApprovalStatus; requested_hours: number; payable_hours: number | null }>; - const COUNT = 12; - for (let i = 0; i < COUNT; i++) { - const emp = employees[i % employees.length]; - const leaveCode = leaveCodes[Math.floor(Math.random() * leaveCodes.length)]; +// const COUNT = 12; +// for (let i = 0; i < COUNT; i++) { +// const emp = employees[i % employees.length]; +// const leaveCode = leaveCodes[Math.floor(Math.random() * leaveCodes.length)]; - const date = daysAgo(120 - i * 3); - const status = statuses[(i + 2) % statuses.length]; - const requestedHours = 4 + (i % 5); // 4 ? 8 h - const payableHours = status === LeaveApprovalStatus.APPROVED ? Math.min(requestedHours, 8) : null; +// const date = daysAgo(120 - i * 3); +// const status = statuses[(i + 2) % statuses.length]; +// const requestedHours = 4 + (i % 5); // 4 ? 8 h +// const payableHours = status === LeaveApprovalStatus.APPROVED ? Math.min(requestedHours, 8) : null; - const lr = await prisma.leaveRequests.create({ - data: { - employee_id: emp.id, - bank_code_id: leaveCode.id, - leave_type: leaveCode.type as LeaveTypes, - date, - comment: `Past leave #${i + 1} (${leaveCode.type})`, - approval_status: status, - requested_hours: requestedHours, - payable_hours: payableHours, - }, - }); +// const lr = await prisma.leaveRequests.create({ +// data: { +// employee_id: emp.id, +// bank_code_id: leaveCode.id, +// leave_type: leaveCode.type as LeaveTypes, +// date, +// comment: `Past leave #${i + 1} (${leaveCode.type})`, +// approval_status: status, +// requested_hours: requestedHours, +// payable_hours: payableHours, +// }, +// }); - created.push({ - id: lr.id, - employee_id: lr.employee_id, - leave_type: lr.leave_type, - date: lr.date, - comment: lr.comment, - approval_status: lr.approval_status, - requested_hours: requestedHours, - payable_hours: payableHours, - }); - } +// created.push({ +// id: lr.id, +// employee_id: lr.employee_id, +// leave_type: lr.leave_type, +// date: lr.date, +// comment: lr.comment, +// approval_status: lr.approval_status, +// requested_hours: requestedHours, +// payable_hours: payableHours, +// }); +// } - for (const lr of created) { - await prisma.leaveRequestsArchive.create({ - data: { - leave_request_id: lr.id, - employee_id: lr.employee_id, - leave_type: lr.leave_type, - date: lr.date, - comment: lr.comment, - approval_status: lr.approval_status, - requested_hours: lr.requested_hours, - payable_hours: lr.payable_hours, - }, - }); - } +// for (const lr of created) { +// await prisma.leaveRequestsArchive.create({ +// data: { +// leave_request_id: lr.id, +// employee_id: lr.employee_id, +// leave_type: lr.leave_type, +// date: lr.date, +// comment: lr.comment, +// approval_status: lr.approval_status, +// requested_hours: lr.requested_hours, +// payable_hours: lr.payable_hours, +// }, +// }); +// } - console.log(`? LeaveRequestsArchive: ${created.length} rows`); -} +// console.log(`? LeaveRequestsArchive: ${created.length} rows`); +// } -main().finally(() => prisma.$disconnect()); \ No newline at end of file +// main().finally(() => prisma.$disconnect()); \ No newline at end of file diff --git a/prisma/mock-seeds-scripts/11-shifts-archive.ts b/prisma/mock-seeds-scripts/11-shifts-archive.ts index 1c3f5e8..4259359 100644 --- a/prisma/mock-seeds-scripts/11-shifts-archive.ts +++ b/prisma/mock-seeds-scripts/11-shifts-archive.ts @@ -1,71 +1,71 @@ -import { PrismaClient } from '@prisma/client'; +// import { PrismaClient } from '@prisma/client'; -if (process.env.SKIP_LEAVE_REQUESTS === 'true') { - console.log("⏭ Seed leave-requests ignoré (SKIP_LEAVE_REQUESTS=true)"); - process.exit(0); -} +// if (process.env.SKIP_LEAVE_REQUESTS === 'true') { +// console.log("⏭ Seed leave-requests ignoré (SKIP_LEAVE_REQUESTS=true)"); +// process.exit(0); +// } -const prisma = new PrismaClient(); +// const prisma = new PrismaClient(); -function timeAt(h:number,m:number) { - return new Date(Date.UTC(1970,0,1,h,m,0)); -} -function daysAgo(n:number) { - const d = new Date(); - d.setUTCDate(d.getUTCDate() - n); - d.setUTCHours(0,0,0,0); - return d; -} +// function timeAt(h:number,m:number) { +// return new Date(Date.UTC(1970,0,1,h,m,0)); +// } +// function daysAgo(n:number) { +// const d = new Date(); +// d.setUTCDate(d.getUTCDate() - n); +// d.setUTCHours(0,0,0,0); +// return d; +// } -async function main() { - const bankCodes = await prisma.bankCodes.findMany({ where: { categorie: 'SHIFT' }, select: { id: true } }); - const employees = await prisma.employees.findMany({ select: { id: true } }); +// async function main() { +// const bankCodes = await prisma.bankCodes.findMany({ where: { categorie: 'SHIFT' }, select: { id: true } }); +// const employees = await prisma.employees.findMany({ select: { id: true } }); - for (const e of employees) { - const tss = await prisma.timesheets.findMany({ where: { employee_id: e.id }, select: { id: true } }); - if (!tss.length) continue; +// for (const e of employees) { +// const tss = await prisma.timesheets.findMany({ where: { employee_id: e.id }, select: { id: true } }); +// if (!tss.length) continue; - const createdShiftIds: number[] = []; - for (let i = 0; i < 8; i++) { - const ts = tss[i % tss.length]; - const bc = bankCodes[i % bankCodes.length]; - const date = daysAgo(200 + i); // bien dans le passé - const startH = 7 + (i % 4); // 7..10 - const endH = startH + 8; // 15..18 +// const createdShiftIds: number[] = []; +// for (let i = 0; i < 8; i++) { +// const ts = tss[i % tss.length]; +// const bc = bankCodes[i % bankCodes.length]; +// const date = daysAgo(200 + i); // bien dans le passé +// const startH = 7 + (i % 4); // 7..10 +// const endH = startH + 8; // 15..18 - const sh = await prisma.shifts.create({ - data: { - timesheet_id: ts.id, - bank_code_id: bc.id, - comment: `Archived-era shift ${i + 1} for emp ${e.id}`, - date, - start_time: timeAt(startH, 0), - end_time: timeAt(endH, 0), - is_approved: true, - }, - }); - createdShiftIds.push(sh.id); - } +// const sh = await prisma.shifts.create({ +// data: { +// timesheet_id: ts.id, +// bank_code_id: bc.id, +// comment: `Archived-era shift ${i + 1} for emp ${e.id}`, +// date, +// start_time: timeAt(startH, 0), +// end_time: timeAt(endH, 0), +// is_approved: true, +// }, +// }); +// createdShiftIds.push(sh.id); +// } - for (const sid of createdShiftIds) { - const s = await prisma.shifts.findUnique({ where: { id: sid } }); - if (!s) continue; - await prisma.shiftsArchive.create({ - data: { - shift_id: s.id, - timesheet_id: s.timesheet_id, - bank_code_id: s.bank_code_id, - comment: s.comment, - date: s.date, - start_time: s.start_time, - end_time: s.end_time, - }, - }); - } - } +// for (const sid of createdShiftIds) { +// const s = await prisma.shifts.findUnique({ where: { id: sid } }); +// if (!s) continue; +// await prisma.shiftsArchive.create({ +// data: { +// shift_id: s.id, +// timesheet_id: s.timesheet_id, +// bank_code_id: s.bank_code_id, +// comment: s.comment, +// date: s.date, +// start_time: s.start_time, +// end_time: s.end_time, +// }, +// }); +// } +// } - const total = await prisma.shiftsArchive.count(); - console.log(`✓ ShiftsArchive: ${total} rows total`); -} +// const total = await prisma.shiftsArchive.count(); +// console.log(`✓ ShiftsArchive: ${total} rows total`); +// } -main().finally(() => prisma.$disconnect()); +// main().finally(() => prisma.$disconnect()); diff --git a/prisma/mock-seeds-scripts/13-expenses-archive.ts b/prisma/mock-seeds-scripts/13-expenses-archive.ts index d8e35a2..c56520e 100644 --- a/prisma/mock-seeds-scripts/13-expenses-archive.ts +++ b/prisma/mock-seeds-scripts/13-expenses-archive.ts @@ -1,65 +1,65 @@ -// 13-expenses-archive.ts -import { PrismaClient, Expenses } from '@prisma/client'; +// // 13-expenses-archive.ts +// import { PrismaClient, Expenses } from '@prisma/client'; -if (process.env.SKIP_LEAVE_REQUESTS === 'true') { - console.log("⏭ Seed leave-requests ignoré (SKIP_LEAVE_REQUESTS=true)"); - process.exit(0); -} +// if (process.env.SKIP_LEAVE_REQUESTS === 'true') { +// console.log("⏭ Seed leave-requests ignoré (SKIP_LEAVE_REQUESTS=true)"); +// process.exit(0); +// } -const prisma = new PrismaClient(); +// const prisma = new PrismaClient(); -function daysAgo(n:number) { - const d = new Date(); - d.setUTCDate(d.getUTCDate() - n); - d.setUTCHours(0,0,0,0); - return d; -} +// function daysAgo(n:number) { +// const d = new Date(); +// d.setUTCDate(d.getUTCDate() - n); +// d.setUTCHours(0,0,0,0); +// return d; +// } -async function main() { - const expenseCodes = await prisma.bankCodes.findMany({ where: { categorie: 'EXPENSE' }, select: { id: true } }); - const timesheets = await prisma.timesheets.findMany({ select: { id: true } }); +// async function main() { +// const expenseCodes = await prisma.bankCodes.findMany({ where: { categorie: 'EXPENSE' }, select: { id: true } }); +// const timesheets = await prisma.timesheets.findMany({ select: { id: true } }); - // ✅ typer pour éviter never[] - const created: Expenses[] = []; +// // ✅ typer pour éviter never[] +// const created: Expenses[] = []; - for (let i = 0; i < 4; i++) { - const ts = timesheets[i % timesheets.length]; - const bc = expenseCodes[i % expenseCodes.length]; +// for (let i = 0; i < 4; i++) { +// const ts = timesheets[i % timesheets.length]; +// const bc = expenseCodes[i % expenseCodes.length]; - const exp = await prisma.expenses.create({ - data: { - timesheet_id: ts.id, - bank_code_id: bc.id, - date: daysAgo(60 + i), - amount: (20 + i * 3.5).toFixed(2), // ok: Decimal accepte string - attachment: null, - comment: `Old expense #${i + 1}`, - is_approved: true, - supervisor_comment: null, - }, - }); +// const exp = await prisma.expenses.create({ +// data: { +// timesheet_id: ts.id, +// bank_code_id: bc.id, +// date: daysAgo(60 + i), +// amount: (20 + i * 3.5).toFixed(2), // ok: Decimal accepte string +// attachment: null, +// comment: `Old expense #${i + 1}`, +// is_approved: true, +// supervisor_comment: null, +// }, +// }); - created.push(exp); - } +// created.push(exp); +// } - for (const e of created) { - await prisma.expensesArchive.create({ - data: { - expense_id: e.id, - timesheet_id: e.timesheet_id, - bank_code_id: e.bank_code_id, - date: e.date, - amount: e.amount, - attachment: e.attachment, - comment: e.comment, - is_approved: e.is_approved, - supervisor_comment: e.supervisor_comment, - }, - }); - } +// for (const e of created) { +// await prisma.expensesArchive.create({ +// data: { +// expense_id: e.id, +// timesheet_id: e.timesheet_id, +// bank_code_id: e.bank_code_id, +// date: e.date, +// amount: e.amount, +// attachment: e.attachment, +// comment: e.comment, +// is_approved: e.is_approved, +// supervisor_comment: e.supervisor_comment, +// }, +// }); +// } - const total = await prisma.expensesArchive.count(); - console.log(`✓ ExpensesArchive: ${total} total rows (added ${created.length})`); -} +// const total = await prisma.expensesArchive.count(); +// console.log(`✓ ExpensesArchive: ${total} total rows (added ${created.length})`); +// } -main().finally(() => prisma.$disconnect()); +// main().finally(() => prisma.$disconnect()); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 700354f..d129b59 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -24,10 +24,7 @@ model Users { role Roles @default(GUEST) employee Employees? @relation("UserEmployee") - customer Customers? @relation("UserCustomer") oauth_sessions OAuthSessions[] @relation("UserOAuthSessions") - employees_archive EmployeesArchive[] @relation("UsersToEmployeesToArchive") - customer_archive CustomersArchive[] @relation("UserToCustomersToArchive") preferences Preferences? @relation("UserPreferences") @@map("users") @@ -49,61 +46,13 @@ model Employees { crew Employees[] @relation("EmployeeSupervisor") - archive EmployeesArchive[] @relation("EmployeeToArchive") timesheet Timesheets[] @relation("TimesheetEmployee") leave_request LeaveRequests[] @relation("LeaveRequestEmployee") - supervisor_archive EmployeesArchive[] @relation("EmployeeSupervisorToArchive") schedule_presets SchedulePresets[] @relation("SchedulePreset") @@map("employees") } -model EmployeesArchive { - id Int @id @default(autoincrement()) - employee Employees @relation("EmployeeToArchive", fields: [employee_id], references: [id]) - employee_id Int - user_id String @db.Uuid - user Users @relation("UsersToEmployeesToArchive", fields: [user_id], references: [id]) - supervisor Employees? @relation("EmployeeSupervisorToArchive", fields: [supervisor_id], references: [id]) - supervisor_id Int? - - archived_at DateTime @default(now()) - first_name String - last_name String - job_title String? - is_supervisor Boolean - external_payroll_id Int - company_code Int - first_work_day DateTime @db.Date - last_work_day DateTime @db.Date - - @@map("employees_archive") -} - -model Customers { - id Int @id @default(autoincrement()) - user Users @relation("UserCustomer", fields: [user_id], references: [id]) - user_id String @unique @db.Uuid - invoice_id Int? @unique - - archive CustomersArchive[] @relation("CustomerToArchive") - - @@map("customers") -} - -model CustomersArchive { - id Int @id @default(autoincrement()) - customer Customers @relation("CustomerToArchive", fields: [customer_id], references: [id]) - customer_id Int - user Users @relation("UserToCustomersToArchive", fields: [user_id], references: [id]) - user_id String @db.Uuid - - archived_at DateTime @default(now()) - invoice_id Int? @unique - - @@map("customers_archive") -} - model LeaveRequests { id Int @id @default(autoincrement()) employee Employees @relation("LeaveRequestEmployee", fields: [employee_id], references: [id]) @@ -216,13 +165,6 @@ model SchedulePresetShifts { @@map("schedule_preset_shifts") } - - - - - - - model Shifts { id Int @id @default(autoincrement()) timesheet Timesheets @relation("ShiftTimesheet", fields: [timesheet_id], references: [id]) @@ -283,7 +225,7 @@ model Expenses { attachment Int? date DateTime @db.Date - amount Decimal @db.Money + amount Decimal? @db.Decimal(12,2) mileage Decimal? @db.Decimal(12,2) comment String supervisor_comment String? @@ -305,7 +247,7 @@ model ExpensesArchive { archived_at DateTime @default(now()) bank_code_id Int date DateTime @db.Date - amount Decimal? @db.Money + amount Decimal? @db.Decimal(12,2) mileage Decimal? @db.Decimal(12,2) comment String? is_approved Boolean @@ -373,7 +315,7 @@ model AttachmentVariants { attachment_id Int attachment Attachments @relation("attachmentVariantAttachment",fields: [attachment_id], references: [id], onDelete: Cascade) variant String - patch String + path String bytes Int width Int? height Int? diff --git a/src/app.controller.ts b/src/app.controller.ts index cce879e..0aa298e 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,12 +1,4 @@ import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; @Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} +export class AppController { } diff --git a/src/app.module.ts b/src/app.module.ts index 4179229..dbe9dd9 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,61 +1,39 @@ import { BadRequestException, Module, ValidationPipe } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; -// import { ArchivalModule } from './modules/archival/archival.module'; -import { AuthenticationModule } from './modules/authentication/auth.module'; -import { BankCodesModule } from './modules/bank-codes/bank-codes.module'; -import { BusinessLogicsModule } from './modules/business-logics/business-logics.module'; +import { AuthenticationModule } from './identity-and-account/authentication/auth.module'; // import { CsvExportModule } from './modules/exports/csv-exports.module'; -import { CustomersModule } from './modules/customers/customers.module'; -import { EmployeesModule } from './modules/employees/employees.module'; -// import { ExpensesModule } from './modules/expenses/expenses.module'; import { HealthModule } from './health/health.module'; import { HealthController } from './health/health.controller'; -// import { LeaveRequestsModule } from './modules/leave-requests/leave-requests.module'; import { NotificationsModule } from './modules/notifications/notifications.module'; -import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.module'; -import { OvertimeService } from './modules/business-logics/services/overtime.service'; -import { PreferencesModule } from './modules/preferences/preferences.module'; +import { PreferencesModule } from './identity-and-account/preferences/preferences.module'; import { PrismaModule } from './prisma/prisma.module'; import { ScheduleModule } from '@nestjs/schedule'; -import { ShiftsModule } from './modules/shifts/shifts.module'; -import { TimesheetsModule } from './modules/timesheets/timesheets.module'; -import { UsersModule } from './modules/users-management/users.module'; +import { UsersModule } from './identity-and-account/users-management/users.module'; import { ConfigModule } from '@nestjs/config'; import { APP_FILTER, APP_PIPE } from '@nestjs/core'; import { HttpExceptionFilter } from './common/filters/http-exception.filter'; import { ValidationError } from 'class-validator'; -import { SchedulePresetsModule } from './modules/schedule-presets/schedule-presets.module'; -import { PayperiodsModule } from './modules/pay-periods/pay-periods.module'; +import { TimeAndAttendanceModule } from 'src/time-and-attendance/time-and-attendance.module'; +import { PayperiodsModule } from 'src/time-and-attendance/pay-period/pay-periods.module'; @Module({ imports: [ - // ArchivalModule, AuthenticationModule, - BankCodesModule, - BusinessLogicsModule, ConfigModule.forRoot({isGlobal: true}), // CsvExportModule, - CustomersModule, - EmployeesModule, - // ExpensesModule, HealthModule, - // LeaveRequestsModule, NotificationsModule, - OauthSessionsModule, PayperiodsModule, PreferencesModule, PrismaModule, ScheduleModule.forRoot(), //cronjobs - SchedulePresetsModule, - ShiftsModule, - TimesheetsModule, + TimeAndAttendanceModule, UsersModule, ], controllers: [AppController, HealthController], providers: [ AppService, - OvertimeService, { provide: APP_FILTER, useClass: HttpExceptionFilter diff --git a/src/app.service.ts b/src/app.service.ts index 927d7cc..c45bea9 100644 --- a/src/app.service.ts +++ b/src/app.service.ts @@ -1,8 +1,4 @@ import { Injectable } from '@nestjs/common'; @Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} +export class AppService { } diff --git a/src/common/guards/ownership.guard.ts b/src/common/guards/ownership.guard.ts index 9fcba18..27928bc 100644 --- a/src/common/guards/ownership.guard.ts +++ b/src/common/guards/ownership.guard.ts @@ -1,4 +1,4 @@ -import { +import { CanActivate, Injectable, ExecutionContext, @@ -17,15 +17,15 @@ export class OwnershipGuard implements CanActivate { constructor( private reflector: Reflector, private moduleRef: ModuleRef, - ) {} + ) { } - async canActivate(context: ExecutionContext): Promise{ + async canActivate(context: ExecutionContext): Promise { const meta = this.reflector.get( OWNER_KEY, context.getHandler(), ); - if (!meta) + if (!meta) return true; - + const request = context.switchToHttp().getRequest(); const user = request.user; const resourceId = request.params[meta.idParam || 'id']; diff --git a/src/common/guards/roles.guard.ts b/src/common/guards/roles.guard.ts index 61889f7..9a9244d 100644 --- a/src/common/guards/roles.guard.ts +++ b/src/common/guards/roles.guard.ts @@ -17,7 +17,7 @@ interface RequestWithUser extends Request { @Injectable() export class RolesGuard implements CanActivate { - constructor(private reflector: Reflector) {} + constructor(private reflector: Reflector) { } /** * @swagger @@ -37,9 +37,9 @@ export class RolesGuard implements CanActivate { * or returns `false` if the user is not authenticated. */ canActivate(ctx: ExecutionContext): boolean { - const requiredRoles = this.reflector.get( + const requiredRoles = this.reflector.getAllAndOverride( ROLES_KEY, - ctx.getHandler(), + [ctx.getHandler(), ctx.getClass()], ); //for "deny-by-default" when role is wrong or unavailable if (!requiredRoles || requiredRoles.length === 0) { diff --git a/src/common/shared/base-approval.service.ts b/src/common/shared/base-approval.service.ts index e3660e1..ab44a0d 100644 --- a/src/common/shared/base-approval.service.ts +++ b/src/common/shared/base-approval.service.ts @@ -18,14 +18,14 @@ export abstract class BaseApprovalService { //returns the corresponding Prisma delegate protected abstract get delegate(): UpdatableDelegate; - protected abstract delegateFor(transaction: Prisma.TransactionClient): UpdatableDelegate; + protected abstract delegateFor(tx: Prisma.TransactionClient): UpdatableDelegate; //standard update Aproval - async updateApproval(id: number, isApproved: boolean): Promise { + async updateApproval(id: number, is_approved: boolean): Promise { try{ return await this.delegate.update({ where: { id }, - data: { is_approved: isApproved }, + data: { is_approved: is_approved }, }); }catch (error: any) { if (error instanceof PrismaClientKnownRequestError && error.code === "P2025") { @@ -36,11 +36,11 @@ export abstract class BaseApprovalService { } //approval with transaction to avoid many requests - async updateApprovalWithTransaction(transaction: Prisma.TransactionClient, id: number, isApproved: boolean): Promise { + async updateApprovalWithTransaction(tx: Prisma.TransactionClient, id: number, is_approved: boolean): Promise { try { - return await this.delegateFor(transaction).update({ + return await this.delegateFor(tx).update({ where: { id }, - data: { is_approved: isApproved }, + data: { is_approved: is_approved }, }); } catch (error: any ){ if(error instanceof PrismaClientKnownRequestError && error.code === 'P2025') { diff --git a/src/common/shared/role-groupes.ts b/src/common/shared/role-groupes.ts new file mode 100644 index 0000000..b67c432 --- /dev/null +++ b/src/common/shared/role-groupes.ts @@ -0,0 +1,15 @@ +import { Roles as RoleEnum } from ".prisma/client"; + +export const GLOBAL_CONTROLLER_ROLES: readonly RoleEnum[] = [ + RoleEnum.EMPLOYEE, + RoleEnum.ACCOUNTING, + RoleEnum.HR, + RoleEnum.SUPERVISOR, + RoleEnum.ADMIN, +]; + +export const MANAGER_ROLES: readonly RoleEnum[] = [ + RoleEnum.HR, + RoleEnum.SUPERVISOR, + RoleEnum.ADMIN, +] \ No newline at end of file diff --git a/src/modules/authentication/auth.module.ts b/src/identity-and-account/authentication/auth.module.ts similarity index 100% rename from src/modules/authentication/auth.module.ts rename to src/identity-and-account/authentication/auth.module.ts diff --git a/src/modules/authentication/controllers/auth.controller.ts b/src/identity-and-account/authentication/controllers/auth.controller.ts similarity index 90% rename from src/modules/authentication/controllers/auth.controller.ts rename to src/identity-and-account/authentication/controllers/auth.controller.ts index 248c4d1..43c9397 100644 --- a/src/modules/authentication/controllers/auth.controller.ts +++ b/src/identity-and-account/authentication/controllers/auth.controller.ts @@ -1,26 +1,27 @@ -import { Controller, Get, Req, Res, UnauthorizedException, UseGuards } from '@nestjs/common'; -import { OIDCLoginGuard } from '../guards/authentik-auth.guard'; -import { Request, Response } from 'express'; - -@Controller('auth') -export class AuthController { - - @UseGuards(OIDCLoginGuard) - @Get('/v1/login') - login() { } - - @Get('/callback') - @UseGuards(OIDCLoginGuard) - loginCallback(@Req() req: Request, @Res() res: Response) { - res.redirect('http://localhost:9000/#/login-success'); - } - - @Get('/me') - getProfile(@Req() req: Request) { - if (!req.user) { - throw new UnauthorizedException('Not logged in'); - } - return req.user; - } - -} +import { Controller, Get, Req, Res, UnauthorizedException, UseGuards } from '@nestjs/common'; +import { OIDCLoginGuard } from '../guards/authentik-auth.guard'; +import { Request, Response } from 'express'; + +@Controller('auth') +export class AuthController { + + @UseGuards(OIDCLoginGuard) + @Get('/v1/login') + login() { } + + @Get('/callback') + @UseGuards(OIDCLoginGuard) + loginCallback(@Req() req: Request, @Res() res: Response) { + // res.redirect('http://10.100.251.2:9011/#/login-success'); + res.redirect('http://localhost:9000/#/login-success'); + } + + @Get('/me') + getProfile(@Req() req: Request) { + if (!req.user) { + throw new UnauthorizedException('Not logged in'); + } + return req.user; + } + +} diff --git a/src/modules/authentication/guards/authentik-auth.guard.ts b/src/identity-and-account/authentication/guards/authentik-auth.guard.ts similarity index 97% rename from src/modules/authentication/guards/authentik-auth.guard.ts rename to src/identity-and-account/authentication/guards/authentik-auth.guard.ts index df9208c..ff4f44d 100644 --- a/src/modules/authentication/guards/authentik-auth.guard.ts +++ b/src/identity-and-account/authentication/guards/authentik-auth.guard.ts @@ -1,12 +1,12 @@ -import { ExecutionContext, Injectable } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; - -@Injectable() -export class OIDCLoginGuard extends AuthGuard('openidconnect') { - async canActivate(context: ExecutionContext) { - const result = (await super.canActivate(context)) as boolean; - const request = context.switchToHttp().getRequest(); - await super.logIn(request); - return result; - } +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class OIDCLoginGuard extends AuthGuard('openidconnect') { + async canActivate(context: ExecutionContext) { + const result = (await super.canActivate(context)) as boolean; + const request = context.switchToHttp().getRequest(); + await super.logIn(request); + return result; + } } \ No newline at end of file diff --git a/src/modules/authentication/serializers/express-session.serializer.ts b/src/identity-and-account/authentication/serializers/express-session.serializer.ts similarity index 100% rename from src/modules/authentication/serializers/express-session.serializer.ts rename to src/identity-and-account/authentication/serializers/express-session.serializer.ts diff --git a/src/modules/authentication/services/authentik-auth.service.ts b/src/identity-and-account/authentication/services/authentik-auth.service.ts similarity index 75% rename from src/modules/authentication/services/authentik-auth.service.ts rename to src/identity-and-account/authentication/services/authentik-auth.service.ts index 9d6514c..974a1eb 100644 --- a/src/modules/authentication/services/authentik-auth.service.ts +++ b/src/identity-and-account/authentication/services/authentik-auth.service.ts @@ -1,14 +1,14 @@ - -import { Injectable } from '@nestjs/common'; -import { UsersService } from 'src/modules/users-management/services/users.service'; - -@Injectable() -export class AuthentikAuthService { - constructor(private usersService: UsersService) {} - - async validateUser(user_email: string): Promise { - const user = await this.usersService.findOneByEmail(user_email); - - return user; - } -} + +import { Injectable } from '@nestjs/common'; +import { UsersService } from 'src/identity-and-account/users-management/services/users.service'; + +@Injectable() +export class AuthentikAuthService { + constructor(private usersService: UsersService) {} + + async validateUser(user_email: string): Promise { + const user = await this.usersService.findOneByEmail(user_email); + + return user; + } +} diff --git a/src/modules/authentication/strategies/authentik.strategy.ts b/src/identity-and-account/authentication/strategies/authentik.strategy.ts similarity index 97% rename from src/modules/authentication/strategies/authentik.strategy.ts rename to src/identity-and-account/authentication/strategies/authentik.strategy.ts index 98602c5..6609356 100644 --- a/src/modules/authentication/strategies/authentik.strategy.ts +++ b/src/identity-and-account/authentication/strategies/authentik.strategy.ts @@ -1,63 +1,63 @@ - -import { Strategy as OIDCStrategy, Profile, VerifyCallback } from 'passport-openidconnect'; -import { PassportStrategy } from '@nestjs/passport'; -import { Injectable } from '@nestjs/common'; -import { AuthentikAuthService } from '../services/authentik-auth.service'; -import { ValidationError } from 'class-validator'; - -export interface AuthentikPayload { - iss: string; // Issuer - sub: string; // Subject (user ID) - aud: string; // Audience (client ID) - exp: number; // Expiration time (Unix) - iat: number; // Issued at time (Unix) - auth_time: number; // Time of authentication (Unix) - acr?: string; // Auth Context Class Reference - amr?: string[]; // Auth Method References (e.g., ['pwd']) - email: string; - email_verified: boolean; - name?: string; - given_name?: string; - preferred_username?: string; - nickname?: string; - groups?: string[]; -} - -@Injectable() -export class AuthentikStrategy extends PassportStrategy(OIDCStrategy, 'openidconnect', 8) { - constructor(private authentikAuthService: AuthentikAuthService) { - super({ - issuer: process.env.AUTHENTIK_ISSUER || "ISSUER_MISSING", - clientID: process.env.AUTHENTIK_CLIENT_ID || 'CLIENT_ID_MISSING', - clientSecret: process.env.AUTHENTIK_CLIENT_SECRET || 'CLIENT_SECRET_MISSING', - callbackURL: process.env.AUTHENTIK_CALLBACK_URL || 'CALLBACK_URL_MISSING', - authorizationURL: process.env.AUTHENTIK_AUTH_URL || 'AUTH_URL_MISSING', - tokenURL: process.env.AUTHENTIK_TOKEN_URL || 'TOKEN_URL_MISSING', - userInfoURL: process.env.AUTHENTIK_USERINFO_URL || 'USERINFO_URL_MISSING', - scope: ['openid', 'email', 'profile', 'offline_access'], - },); - } - - async validate( - _issuer: string, - profile: Profile, - _context: any, - _idToken: string, - _accessToken: string, - _refreshToken: string, - _params: any, - cb: VerifyCallback, - ): Promise { - try { - const email = profile.emails?.[0]?.value; - if (!email) return cb(new Error('Missing email in OIDC profile'), false); - - const user = await this.authentikAuthService.validateUser(email); - if (!user) return cb(new Error('User not found'), false); - - return cb(null, user); - } catch (err) { - return cb(err, false); - } - } -} + +import { Strategy as OIDCStrategy, Profile, VerifyCallback } from 'passport-openidconnect'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { AuthentikAuthService } from '../services/authentik-auth.service'; +import { ValidationError } from 'class-validator'; + +export interface AuthentikPayload { + iss: string; // Issuer + sub: string; // Subject (user ID) + aud: string; // Audience (client ID) + exp: number; // Expiration time (Unix) + iat: number; // Issued at time (Unix) + auth_time: number; // Time of authentication (Unix) + acr?: string; // Auth Context Class Reference + amr?: string[]; // Auth Method References (e.g., ['pwd']) + email: string; + email_verified: boolean; + name?: string; + given_name?: string; + preferred_username?: string; + nickname?: string; + groups?: string[]; +} + +@Injectable() +export class AuthentikStrategy extends PassportStrategy(OIDCStrategy, 'openidconnect', 8) { + constructor(private authentikAuthService: AuthentikAuthService) { + super({ + issuer: process.env.AUTHENTIK_ISSUER || "ISSUER_MISSING", + clientID: process.env.AUTHENTIK_CLIENT_ID || 'CLIENT_ID_MISSING', + clientSecret: process.env.AUTHENTIK_CLIENT_SECRET || 'CLIENT_SECRET_MISSING', + callbackURL: process.env.AUTHENTIK_CALLBACK_URL || 'CALLBACK_URL_MISSING', + authorizationURL: process.env.AUTHENTIK_AUTH_URL || 'AUTH_URL_MISSING', + tokenURL: process.env.AUTHENTIK_TOKEN_URL || 'TOKEN_URL_MISSING', + userInfoURL: process.env.AUTHENTIK_USERINFO_URL || 'USERINFO_URL_MISSING', + scope: ['openid', 'email', 'profile', 'offline_access'], + },); + } + + async validate( + _issuer: string, + profile: Profile, + _context: any, + _idToken: string, + _accessToken: string, + _refreshToken: string, + _params: any, + cb: VerifyCallback, + ): Promise { + try { + const email = profile.emails?.[0]?.value; + if (!email) return cb(new Error('Missing email in OIDC profile'), false); + + const user = await this.authentikAuthService.validateUser(email); + if (!user) return cb(new Error('User not found'), false); + + return cb(null, user); + } catch (err) { + return cb(err, false); + } + } +} diff --git a/src/identity-and-account/employees/controllers/employees.controller.ts b/src/identity-and-account/employees/controllers/employees.controller.ts new file mode 100644 index 0000000..a3dd411 --- /dev/null +++ b/src/identity-and-account/employees/controllers/employees.controller.ts @@ -0,0 +1,52 @@ +import { Controller, Get, Patch, Param, Body, NotFoundException, Req, Post } from "@nestjs/common"; +import { Employees } from "@prisma/client"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes"; +import { CreateEmployeeDto } from "src/identity-and-account/employees/dtos/create-employee.dto"; +import { EmployeeListItemDto } from "src/identity-and-account/employees/dtos/list-employee.dto"; +import { EmployeeProfileItemDto } from "src/identity-and-account/employees/dtos/profil-employee.dto"; +import { UpdateEmployeeDto } from "src/identity-and-account/employees/dtos/update-employee.dto"; +import { EmployeesArchivalService } from "src/identity-and-account/employees/services/employees-archival.service"; +import { EmployeesService } from "src/identity-and-account/employees/services/employees.service"; + +@RolesAllowed(...GLOBAL_CONTROLLER_ROLES) +@Controller('employees') +export class EmployeesController { + constructor( + private readonly employeesService: EmployeesService, + private readonly archiveService: EmployeesArchivalService, + ) { } + + @Get('profile') + findOneProfile(@Req() req): Promise { + const email = req.user?.email; + return this.employeesService.findOneProfile(email); + } + + @Get('employee-list') + @RolesAllowed(...MANAGER_ROLES) + findListEmployees(): Promise { + return this.employeesService.findListEmployees(); + } + + @Patch() + @RolesAllowed(...MANAGER_ROLES) + async updateOrArchiveOrRestore(@Req() req, @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 email = req.user?.email; + const result = await this.archiveService.patchEmployee(email, dto); + if (!result) { + throw new NotFoundException(`Employee with email: ${email} is not found in active or archive.`) + } + return result; + } + + @Post() + @RolesAllowed(...MANAGER_ROLES) + create(@Body() dto: CreateEmployeeDto): Promise { + return this.employeesService.create(dto); + } + +} diff --git a/src/modules/employees/dtos/create-employee.dto.ts b/src/identity-and-account/employees/dtos/create-employee.dto.ts similarity index 100% rename from src/modules/employees/dtos/create-employee.dto.ts rename to src/identity-and-account/employees/dtos/create-employee.dto.ts diff --git a/src/modules/employees/dtos/list-employee.dto.ts b/src/identity-and-account/employees/dtos/list-employee.dto.ts similarity index 100% rename from src/modules/employees/dtos/list-employee.dto.ts rename to src/identity-and-account/employees/dtos/list-employee.dto.ts diff --git a/src/modules/employees/dtos/profil-employee.dto.ts b/src/identity-and-account/employees/dtos/profil-employee.dto.ts similarity index 100% rename from src/modules/employees/dtos/profil-employee.dto.ts rename to src/identity-and-account/employees/dtos/profil-employee.dto.ts diff --git a/src/modules/employees/dtos/update-employee.dto.ts b/src/identity-and-account/employees/dtos/update-employee.dto.ts similarity index 100% rename from src/modules/employees/dtos/update-employee.dto.ts rename to src/identity-and-account/employees/dtos/update-employee.dto.ts diff --git a/src/identity-and-account/employees/employees.module.ts b/src/identity-and-account/employees/employees.module.ts new file mode 100644 index 0000000..676f4b0 --- /dev/null +++ b/src/identity-and-account/employees/employees.module.ts @@ -0,0 +1,12 @@ +// import { Module } from '@nestjs/common'; +// import { EmployeesController } from './controllers/employees.controller'; +// import { EmployeesService } from './services/employees.service'; +// import { SharedModule } from '../../time-and-attendance/modules/shared/shared.module'; + +// @Module({ +// imports: [SharedModule], +// controllers: [EmployeesController], +// providers: [EmployeesService], +// exports: [EmployeesService ], +// }) +// export class EmployeesModule {} diff --git a/src/identity-and-account/employees/services/employees-archival.service.ts b/src/identity-and-account/employees/services/employees-archival.service.ts new file mode 100644 index 0000000..2aa184a --- /dev/null +++ b/src/identity-and-account/employees/services/employees-archival.service.ts @@ -0,0 +1,173 @@ +import { Injectable } from "@nestjs/common"; +import { Employees, Users } from "@prisma/client"; +import { UpdateEmployeeDto } from "src/identity-and-account/employees/dtos/update-employee.dto"; +import { toDateOrUndefined, toDateOrNull } from "src/identity-and-account/employees/utils/employee.utils"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class EmployeesArchivalService { + constructor(private readonly prisma: PrismaService) { } + + async patchEmployee(email: string, dto: UpdateEmployeeDto): Promise { + // 1) Tenter sur employés actifs + const active = await this.prisma.employees.findFirst({ + where: { user: { email } }, + include: { user: true }, + }); + + if (active) { + // Archivage : si on reçoit un last_work_day défini et que l'employé n’est pas déjà terminé + // if (dto.last_work_day !== undefined && active.last_work_day == null && dto.last_work_day !== null) { + // return this.archiveOnTermination(active, dto); + // } + + // Sinon, update standard (split Users/Employees) + const { + first_name, + last_name, + email: new_email, + phone_number, + residence, + external_payroll_id, + company_code, + job_title, + first_work_day, + last_work_day, + supervisor_id, + is_supervisor, + } = dto as any; + + const first_work_d = toDateOrUndefined(first_work_day); + const last_work_d = Object.prototype.hasOwnProperty('last_work_day') + ? toDateOrNull(last_work_day ?? null) + : undefined; + + await this.prisma.$transaction(async (transaction) => { + if ( + first_name !== undefined || + last_name !== undefined || + new_email !== undefined || + phone_number !== undefined || + residence !== undefined + ) { + await transaction.users.update({ + where: { id: active.user_id }, + data: { + ...(first_name !== undefined ? { first_name } : {}), + ...(last_name !== undefined ? { last_name } : {}), + ...(email !== undefined ? { email: new_email } : {}), + ...(phone_number !== undefined ? { phone_number } : {}), + ...(residence !== undefined ? { residence } : {}), + }, + }); + + } + + const updated = await transaction.employees.update({ + where: { id: active.id }, + data: { + ...(external_payroll_id !== undefined ? { external_payroll_id } : {}), + ...(company_code !== undefined ? { company_code } : {}), + ...(job_title !== undefined ? { job_title } : {}), + ...(first_work_d !== undefined ? { first_work_day: first_work_d } : {}), + ...(last_work_d !== undefined ? { last_work_day: last_work_d } : {}), + ...(is_supervisor !== undefined ? { is_supervisor } : {}), + ...(supervisor_id !== undefined ? { supervisor_id } : {}), + }, + include: { user: true }, + }); + + return updated; + }); + + return this.prisma.employees.findFirst({ where: { user: { email } } }); + } + + const user = await this.prisma.users.findUnique({ where: { email } }); + if (!user) return null; + // 2) Pas trouvé en actifs → regarder en archive (pour restauration) + // const archived = await this.prisma.employeesArchive.findFirst({ + // where: { user_id: user.id }, + // include: { user: true }, + // }); + + // if (archived) { + // // Condition de restauration : last_work_day === null ou first_work_day fourni + // const restore = dto.last_work_day === null || dto.first_work_day != null; + // if (restore) { + // return this.restoreEmployee(archived, dto); + // } + // } + // 3) Ni actif, ni archivé → 404 dans le controller + return null; + } + + //transfers the employee to archive and then delete from employees table + // private async archiveOnTermination(active: Employees & { user: Users }, dto: UpdateEmployeeDto): Promise { + // const last_work_d = toDateOrNull(dto.last_work_day!); + // if (!last_work_d) throw new Error('invalide last_work_day for archive'); + // return this.prisma.$transaction(async transaction => { + // //detach crew from supervisor if employee is a supervisor + // await transaction.employees.updateMany({ + // where: { supervisor_id: active.id }, + // data: { supervisor_id: null }, + // }) + // const archived = await transaction.employeesArchive.create({ + // data: { + // employee_id: active.id, + // user_id: active.user_id, + // first_name: active.user.first_name, + // last_name: active.user.last_name, + // company_code: active.company_code, + // job_title: active.job_title, + // first_work_day: active.first_work_day, + // last_work_day: last_work_d, + // supervisor_id: active.supervisor_id ?? null, + // is_supervisor: active.is_supervisor, + // external_payroll_id: active.external_payroll_id, + // }, + // include: { user: true } + // }); + // //delete from employees table + // await transaction.employees.delete({ where: { id: active.id } }); + // //return archived employee + // return archived + // }); + // } + + // //transfers the employee from archive to the employees table + // private async restoreEmployee(archived: EmployeesArchive & { user: Users }, dto: UpdateEmployeeDto): Promise { + // // const first_work_d = toDateOrUndefined(dto.first_work_day); + // return this.prisma.$transaction(async transaction => { + // //restores the archived employee into the employees table + // const restored = await transaction.employees.create({ + // data: { + // user_id: archived.user_id, + // company_code: archived.company_code, + // job_title: archived.job_title, + // first_work_day: archived.first_work_day, + // last_work_day: null, + // is_supervisor: archived.is_supervisor ?? false, + // external_payroll_id: archived.external_payroll_id, + // }, + // }); + // //deleting archived entry by id + // await transaction.employeesArchive.delete({ where: { id: archived.id } }); + + // //return restored employee + // 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/employees/services/employees.service.ts b/src/identity-and-account/employees/services/employees.service.ts similarity index 81% rename from src/modules/employees/services/employees.service.ts rename to src/identity-and-account/employees/services/employees.service.ts index 3627476..22cee41 100644 --- a/src/modules/employees/services/employees.service.ts +++ b/src/identity-and-account/employees/services/employees.service.ts @@ -1,7 +1,9 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { PrismaService } from 'src/prisma/prisma.service'; -import { EmployeeListItemDto } from '../dtos/list-employee.dto'; -import { EmployeeProfileItemDto } from '../dtos/profil-employee.dto'; +import { Injectable, NotFoundException } from "@nestjs/common"; +import { Employees, Users } from "@prisma/client"; +import { CreateEmployeeDto } from "src/identity-and-account/employees/dtos/create-employee.dto"; +import { EmployeeListItemDto } from "src/identity-and-account/employees/dtos/list-employee.dto"; +import { EmployeeProfileItemDto } from "src/identity-and-account/employees/dtos/profil-employee.dto"; +import { PrismaService } from "src/prisma/prisma.service"; @Injectable() export class EmployeesService { @@ -88,48 +90,50 @@ export class EmployeesService { }; } + async create(dto: CreateEmployeeDto): Promise { + const { + first_name, + last_name, + email, + phone_number, + residence, + external_payroll_id, + company_code, + job_title, + first_work_day, + last_work_day, + is_supervisor, + } = dto; + + return this.prisma.$transaction(async (transaction) => { + const user: Users = await transaction.users.create({ + data: { + first_name, + last_name, + email, + phone_number, + residence, + }, + }); + return transaction.employees.create({ + data: { + user_id: user.id, + external_payroll_id, + company_code, + job_title, + first_work_day, + last_work_day, + is_supervisor, + }, + }); + }); + } + //_____________________________________________________________________________________________ // Deprecated or unused methods //_____________________________________________________________________________________________ - // async create(dto: CreateEmployeeDto): Promise { - // const { - // first_name, - // last_name, - // email, - // phone_number, - // residence, - // external_payroll_id, - // company_code, - // job_title, - // first_work_day, - // last_work_day, - // is_supervisor, - // } = dto; - // return this.prisma.$transaction(async (transaction) => { - // const user: Users = await transaction.users.create({ - // data: { - // first_name, - // last_name, - // email, - // phone_number, - // residence, - // }, - // }); - // return transaction.employees.create({ - // data: { - // user_id: user.id, - // external_payroll_id, - // company_code, - // job_title, - // first_work_day, - // last_work_day, - // is_supervisor, - // }, - // }); - // }); - // } // findAll(): Promise { // return this.prisma.employees.findMany({ diff --git a/src/modules/employees/utils/employee.utils.ts b/src/identity-and-account/employees/utils/employee.utils.ts similarity index 100% rename from src/modules/employees/utils/employee.utils.ts rename to src/identity-and-account/employees/utils/employee.utils.ts diff --git a/src/modules/preferences/controllers/preferences.controller.ts b/src/identity-and-account/preferences/controllers/preferences.controller.ts similarity index 52% rename from src/modules/preferences/controllers/preferences.controller.ts rename to src/identity-and-account/preferences/controllers/preferences.controller.ts index ae5af16..a185837 100644 --- a/src/modules/preferences/controllers/preferences.controller.ts +++ b/src/identity-and-account/preferences/controllers/preferences.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Param, Patch } from "@nestjs/common"; +import { Body, Controller, Patch } from "@nestjs/common"; import { PreferencesService } from "../services/preferences.service"; import { PreferencesDto } from "../dtos/preferences.dto"; @@ -6,9 +6,12 @@ import { PreferencesDto } from "../dtos/preferences.dto"; export class PreferencesController { constructor(private readonly service: PreferencesService){} - @Patch(':email') - async updatePreferences(@Param('email') email: string, @Body()payload: PreferencesDto) { - return this.service.updatePreferences(email, payload); + @Patch() + async updatePreferences( + @Body() user_id: number, + @Body() payload: PreferencesDto + ) { + return this.service.updatePreferences(user_id, payload); } } \ No newline at end of file diff --git a/src/modules/preferences/dtos/preferences.dto.ts b/src/identity-and-account/preferences/dtos/preferences.dto.ts similarity index 100% rename from src/modules/preferences/dtos/preferences.dto.ts rename to src/identity-and-account/preferences/dtos/preferences.dto.ts diff --git a/src/modules/preferences/preferences.module.ts b/src/identity-and-account/preferences/preferences.module.ts similarity index 80% rename from src/modules/preferences/preferences.module.ts rename to src/identity-and-account/preferences/preferences.module.ts index 4fe0227..1873933 100644 --- a/src/modules/preferences/preferences.module.ts +++ b/src/identity-and-account/preferences/preferences.module.ts @@ -1,10 +1,8 @@ -import { Module } from "@nestjs/common"; import { PreferencesController } from "./controllers/preferences.controller"; import { PreferencesService } from "./services/preferences.service"; -import { SharedModule } from "../shared/shared.module"; +import { Module } from "@nestjs/common"; @Module({ - imports: [SharedModule], controllers: [ PreferencesController ], providers: [ PreferencesService ], exports: [ PreferencesService ], diff --git a/src/modules/preferences/services/preferences.service.ts b/src/identity-and-account/preferences/services/preferences.service.ts similarity index 65% rename from src/modules/preferences/services/preferences.service.ts rename to src/identity-and-account/preferences/services/preferences.service.ts index 6e4f169..4e3c108 100644 --- a/src/modules/preferences/services/preferences.service.ts +++ b/src/identity-and-account/preferences/services/preferences.service.ts @@ -1,20 +1,15 @@ -import { Injectable } from "@nestjs/common"; -import { Preferences } from "@prisma/client"; -import { PrismaService } from "src/prisma/prisma.service"; import { PreferencesDto } from "../dtos/preferences.dto"; -import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; +import { PrismaService } from "src/prisma/prisma.service"; +import { Preferences } from "@prisma/client"; +import { Injectable } from "@nestjs/common"; @Injectable() export class PreferencesService { - constructor( - private readonly prisma: PrismaService, - private readonly emailResolver: EmailToIdResolver , - ){} + constructor( private readonly prisma: PrismaService ){} - async updatePreferences(email: string, dto: PreferencesDto ): Promise { - const user_id = await this.emailResolver.resolveUserIdWithEmail(email); + async updatePreferences(user_id: number, dto: PreferencesDto ): Promise { return this.prisma.preferences.update({ - where: { user_id }, + where: { id: user_id }, data: { notifications: dto.notifications, dark_mode: dto.dark_mode, diff --git a/src/modules/users-management/dtos/user.dto.ts b/src/identity-and-account/users-management/dtos/user.dto.ts similarity index 100% rename from src/modules/users-management/dtos/user.dto.ts rename to src/identity-and-account/users-management/dtos/user.dto.ts diff --git a/src/identity-and-account/users-management/services/abstract-user.service.ts b/src/identity-and-account/users-management/services/abstract-user.service.ts new file mode 100644 index 0000000..9a2ffd1 --- /dev/null +++ b/src/identity-and-account/users-management/services/abstract-user.service.ts @@ -0,0 +1,41 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { Users } from '@prisma/client'; +import { PrismaService } from 'src/prisma/prisma.service'; + +@Injectable() +export abstract class AbstractUserService { + constructor(protected readonly prisma: PrismaService) { } + + findAll(): Promise { + return this.prisma.users.findMany(); + } + + async findOne(id: string): Promise { + const user = await this.prisma.users.findUnique({ where: { id } }); + if (!user) { + throw new NotFoundException(`User #${id} not found`); + } + return user; + } + + async findOneByEmail(email: string): Promise> { + const user = await this.prisma.users.findUnique({ where: { email } }); + if (!user) { + throw new NotFoundException(`No user with email #${email} exists`); + } + + const clean_user = { + first_name: user.first_name, + last_name: user.last_name, + email: user.email, + role: user.role, + } + + return clean_user; + } + + async remove(id: string): Promise { + await this.findOne(id); + return this.prisma.users.delete({ where: { id } }); + } +} diff --git a/src/modules/users-management/services/users.service.ts b/src/identity-and-account/users-management/services/users.service.ts similarity index 100% rename from src/modules/users-management/services/users.service.ts rename to src/identity-and-account/users-management/services/users.service.ts diff --git a/src/modules/users-management/users.module.ts b/src/identity-and-account/users-management/users.module.ts similarity index 100% rename from src/modules/users-management/users.module.ts rename to src/identity-and-account/users-management/users.module.ts diff --git a/src/main.ts b/src/main.ts index 88237b6..fbe5afd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,10 @@ 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; +if (!(globalThis as any).crypto) { + (globalThis as any).crypto = nodeCrypto; } -import { ensureAttachmentsTmpDir } from './config/attachment.fs'; +import { ensureAttachmentsTmpDir } from './config/attachment.fs'; import { resolveAttachmentsRoot } from './config/attachment.config';// log to be removed post dev import { ATT_TMP_DIR } from './config/attachment.config'; // log to be removed post dev @@ -19,73 +19,75 @@ import { writeFileSync } from 'fs'; import * as session from 'express-session'; import * as passport from 'passport'; +const SESSION_TOKEN_DURATION_MINUTES = 180 + async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule); - const reflector = app.get(Reflector); //setup Reflector for Roles() + const reflector = app.get(Reflector); //setup Reflector for Roles() - app.useGlobalGuards( - // new JwtAuthGuard(reflector), //Authentification JWT - new RolesGuard(reflector), //deny-by-default and Role-based Access Control - new OwnershipGuard(reflector, app.get(ModuleRef)), //Global use of OwnershipGuard, not implemented yet - ); + app.useGlobalGuards( + // new JwtAuthGuard(reflector), //Authentification JWT + new RolesGuard(reflector), //deny-by-default and Role-based Access Control + new OwnershipGuard(reflector, app.get(ModuleRef)), //Global use of OwnershipGuard, not implemented yet + ); - // Authentication and session - app.use(session({ - secret: 'This is a super secret dev secret that you cant share with anyone', - resave: false, - saveUninitialized: false, - rolling: true, - cookie: { - maxAge: 30 * 60 * 1000, - httpOnly: true, - } - })) - app.use(passport.initialize()); - app.use(passport.session()); + // Authentication and session + app.use(session({ + secret: 'This is a super secret dev secret that you cant share with anyone', + resave: false, + saveUninitialized: false, + rolling: true, + cookie: { + maxAge: SESSION_TOKEN_DURATION_MINUTES * 60 * 1000, // property maxAge requires milliseconds + httpOnly: true, + } + })) + app.use(passport.initialize()); + app.use(passport.session()); - // Enable CORS - app.enableCors({ - origin: 'http://localhost:9000', - credentials: true, - }); + // Enable CORS + app.enableCors({ + origin: ['http://10.100.251.2:9011', 'http://10.100.251.2:9012', 'http://10.100.251.2:9013', 'http://localhost:9000'], + credentials: true, + }); + + //swagger config + const config = new DocumentBuilder() + .setTitle('Targo_Backend') + .setDescription('Documentation de l`API REST pour Targo (NestJS + Prisma)') + .setVersion('1.0') + .addBearerAuth({ + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + name: 'Authorization', + description: 'Invalid JWT token', + in: 'header', + }, 'access-token') + .addTag('Users') + .addTag('Employees') + .addTag('Customers') + .addTag('Timesheets') + .addTag('Shifts') + .addTag('Leave Requests') + .addTag('Shift Codes') + .addTag('OAuth Access Tokens') + .addTag('Authorization') + .build(); - //swagger config - const config = new DocumentBuilder() - .setTitle('Targo_Backend') - .setDescription('Documentation de l`API REST pour Targo (NestJS + Prisma)') - .setVersion('1.0') - .addBearerAuth({ - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', - name: 'Authorization', - description: 'Invalid JWT token', - in: 'header', - }, 'access-token') - .addTag('Users') - .addTag('Employees') - .addTag('Customers') - .addTag('Timesheets') - .addTag('Shifts') - .addTag('Leave Requests') - .addTag('Shift Codes') - .addTag('OAuth Access Tokens') - .addTag('Authorization') - .build(); - //document builder for swagger docs const documentFactory = () => SwaggerModule.createDocument(app, config); const document = documentFactory() SwaggerModule.setup('api/docs', app, document); writeFileSync('./docs/swagger/swagger-spec.json', JSON.stringify(document, null, 2)); - + // logs to be removed post dev console.log('[ENV.ATTACHMENTS_ROOT]', process.env.ATTACHMENTS_ROOT); console.log('[resolveAttachmentsRoot()]', resolveAttachmentsRoot()); console.log('[ATT_TMP_DIR()]', ATT_TMP_DIR()); - await ensureAttachmentsTmpDir(); - await app.listen(process.env.PORT ?? 3000); + await ensureAttachmentsTmpDir(); + await app.listen(process.env.PORT ?? 3000); } bootstrap(); diff --git a/src/modules/archival/controllers/employees-archive.controller.ts b/src/modules/archival/controllers/employees-archive.controller.ts index 375a351..776c6fa 100644 --- a/src/modules/archival/controllers/employees-archive.controller.ts +++ b/src/modules/archival/controllers/employees-archive.controller.ts @@ -1,33 +1,33 @@ -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 { EmployeesArchivalService } from "src/modules/employees/services/employees-archival.service"; +// 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 { EmployeesArchivalService } from "src/modules/employees/services/employees-archival.service"; -@ApiTags('Employee Archives') -// @UseGuards() -@Controller('archives/employees') -export class EmployeesArchiveController { - constructor(private readonly employeesArchiveService: EmployeesArchivalService) {} +// @ApiTags('Employee Archives') +// // @UseGuards() +// @Controller('archives/employees') +// export class EmployeesArchiveController { +// constructor(private readonly employeesArchiveService: EmployeesArchivalService) {} - @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.employeesArchiveService.findAllArchived(); - } +// @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.employeesArchiveService.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.employeesArchiveService.findOneArchived(id); - }catch { - throw new NotFoundException(`Archived employee #${id} not found`); - } - } +// @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.employeesArchiveService.findOneArchived(id); +// }catch { +// throw new NotFoundException(`Archived employee #${id} not found`); +// } +// } -} \ No newline at end of file +// } \ 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 index e6bd935..ecfd7b8 100644 --- a/src/modules/archival/controllers/expenses-archive.controller.ts +++ b/src/modules/archival/controllers/expenses-archive.controller.ts @@ -1,32 +1,32 @@ -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 { ExpensesArchivalService } from "src/modules/expenses/services/expenses-archival.service"; +// 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 { ExpensesArchivalService } from "src/time-and-attendance/modules/expenses/services/expenses-archival.service"; -@ApiTags('Expense Archives') -// @UseGuards() -@Controller('archives/expenses') -export class ExpensesArchiveController { - constructor(private readonly expensesService: ExpensesArchivalService) {} +// @ApiTags('Expense Archives') +// // @UseGuards() +// @Controller('archives/expenses') +// export class ExpensesArchiveController { +// constructor(private readonly expensesService: ExpensesArchivalService) {} - @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: '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 +// @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 index 1c5e4be..ec1b046 100644 --- a/src/modules/archival/controllers/leave-requests-archive.controller.ts +++ b/src/modules/archival/controllers/leave-requests-archive.controller.ts @@ -1,7 +1,7 @@ -import { Controller } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +// import { Controller } from '@nestjs/common'; +// import { ApiTags } from '@nestjs/swagger'; -@ApiTags('LeaveRequests Archives') -// @UseGuards() -@Controller('archives/leaveRequests') -export class LeaveRequestsArchiveController {} \ No newline at end of file +// @ApiTags('LeaveRequests Archives') +// // @UseGuards() +// @Controller('archives/leaveRequests') +// export class LeaveRequestsArchiveController {} \ 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 index e8f92f2..f322fa1 100644 --- a/src/modules/archival/controllers/shifts-archive.controller.ts +++ b/src/modules/archival/controllers/shifts-archive.controller.ts @@ -1,32 +1,32 @@ -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 { ShiftsArchivalService } from "src/modules/shifts/services/shifts-archival.service"; +// 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 { ShiftsArchivalService } from "src/time-and-attendance/modules/time-tracker/shifts/services/shifts-archival.service"; -@ApiTags('Shift Archives') -// @UseGuards() -@Controller('archives/shifts') -export class ShiftsArchiveController { - constructor(private readonly shiftsService: ShiftsArchivalService) {} +// @ApiTags('Shift Archives') +// // @UseGuards() +// @Controller('archives/shifts') +// export class ShiftsArchiveController { +// constructor(private readonly shiftsService: ShiftsArchivalService) {} - @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: '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 +// @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 index 7505b66..888dc3c 100644 --- a/src/modules/archival/controllers/timesheets-archive.controller.ts +++ b/src/modules/archival/controllers/timesheets-archive.controller.ts @@ -1,33 +1,33 @@ -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 { TimesheetArchiveService } from "src/modules/timesheets/services/timesheet-archive.service"; +// 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 { TimesheetArchiveService } from "src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-archive.service"; -@ApiTags('Timesheet Archives') -// @UseGuards() -@Controller('archives/timesheets') -export class TimesheetsArchiveController { - constructor(private readonly timesheetsService: TimesheetArchiveService) {} +// @ApiTags('Timesheet Archives') +// // @UseGuards() +// @Controller('archives/timesheets') +// export class TimesheetsArchiveController { +// constructor(private readonly timesheetsService: TimesheetArchiveService) {} - @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: '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`); - } - } +// @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 +// } \ No newline at end of file diff --git a/src/modules/archival/services/archival.service.ts b/src/modules/archival/services/archival.service.ts index 66be2d0..ebec1e5 100644 --- a/src/modules/archival/services/archival.service.ts +++ b/src/modules/archival/services/archival.service.ts @@ -1,38 +1,38 @@ -import { Injectable, Logger } from "@nestjs/common"; -import { Cron } from "@nestjs/schedule"; -import { ExpensesArchivalService } from "src/modules/expenses/services/expenses-archival.service"; -import { ShiftsArchivalService } from "src/modules/shifts/services/shifts-archival.service"; -import { TimesheetArchiveService } from "src/modules/timesheets/services/timesheet-archive.service"; +// import { TimesheetArchiveService } from "src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-archive.service"; +// import { ExpensesArchivalService } from "src/time-and-attendance/modules/expenses/services/expenses-archival.service"; +// import { ShiftsArchivalService } from "src/time-and-attendance/modules/time-tracker/shifts/services/shifts-archival.service"; +// import { Injectable, Logger } from "@nestjs/common"; +// import { Cron } from "@nestjs/schedule"; -@Injectable() -export class ArchivalService { - private readonly logger = new Logger(ArchivalService.name); +// @Injectable() +// export class ArchivalService { +// private readonly logger = new Logger(ArchivalService.name); - constructor( - private readonly timesheetsService: TimesheetArchiveService, - private readonly expensesService: ExpensesArchivalService, - private readonly shiftsService: ShiftsArchivalService, - ) {} +// constructor( +// private readonly timesheetsService: TimesheetArchiveService, +// private readonly expensesService: ExpensesArchivalService, +// private readonly shiftsService: ShiftsArchivalService, +// ) {} - @Cron('0 0 3 * * 1', {timeZone:'America/Toronto'}) // chaque premier lundi du mois à 03h00 - async handleMonthlyArchival() { - const today = new Date(); - const dayOfMonth = today.getDate(); +// @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.warn('Archive {awaiting 1st monday of the month for archivation process}') - return; - } +// if (dayOfMonth > 7) { +// this.logger.warn('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.error('an error occured during archivation process ', err); - } - } -} \ No newline at end of file +// 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.error('an error occured during archivation process ', err); +// } +// } +// } \ No newline at end of file diff --git a/src/modules/attachments/attachments.module.ts b/src/modules/attachments/attachments.module.ts index ee8883f..9db15ea 100644 --- a/src/modules/attachments/attachments.module.ts +++ b/src/modules/attachments/attachments.module.ts @@ -1,15 +1,19 @@ -import { ScheduleModule } from "@nestjs/schedule"; -import { PrismaService } from "src/prisma/prisma.service"; -import { ArchivalAttachmentService } from "./services/archival-attachment.service"; +import { ArchivalAttachmentService } from "src/modules/attachments/services/archival-attachment.service"; +import { GarbargeCollectorService } from "src/modules/attachments/services/garbage-collector.service"; +import { AttachmentsController } from "src/modules/attachments/controllers/attachments.controller"; +import { DiskStorageService } from "src/modules/attachments/services/disk-storage.service"; +// import { ScheduleModule } from "@nestjs/schedule"; +import { VariantsQueue } from "src/modules/attachments/services/variants.queue"; import { Module } from "@nestjs/common"; -import { GarbargeCollectorService } from "./services/garbage-collector.service"; @Module({ - imports: [ScheduleModule.forRoot()], + // imports: [ScheduleModule.forRoot()], + controllers: [ AttachmentsController], providers: [ - PrismaService, ArchivalAttachmentService, GarbargeCollectorService, + DiskStorageService, + VariantsQueue, ], exports: [ ArchivalAttachmentService, diff --git a/src/modules/attachments/controllers/attachments.controller.ts b/src/modules/attachments/controllers/attachments.controller.ts index 3a75d6a..963bb5c 100644 --- a/src/modules/attachments/controllers/attachments.controller.ts +++ b/src/modules/attachments/controllers/attachments.controller.ts @@ -4,9 +4,7 @@ import { Controller,NotFoundException, UseInterceptors, Post, Get, Param, Res, UploadedFile, BadRequestException, UnsupportedMediaTypeException, Body, Delete, Query, - DefaultValuePipe, - ParseIntPipe - } from "@nestjs/common"; +} from "@nestjs/common"; import { maxUploadBytes, allowedMimes } from "../config/upload.config"; import { memoryStorage } from 'multer'; import { fileTypeFromBuffer, fileTypeFromFile } from "file-type"; @@ -76,7 +74,7 @@ export class AttachmentsController { return this.prisma.attachmentVariants.findMany({ where: { attachment_id: num_id }, orderBy: { variant: 'asc'}, - select: { variant: true, bytes: true, width: true, height: true, patch: true, created_at: true }, + select: { variant: true, bytes: true, width: true, height: true, path: true, created_at: true }, }); } diff --git a/src/modules/attachments/services/disk-storage.service.ts b/src/modules/attachments/services/disk-storage.service.ts index 0c485c6..53639ba 100644 --- a/src/modules/attachments/services/disk-storage.service.ts +++ b/src/modules/attachments/services/disk-storage.service.ts @@ -1,3 +1,4 @@ +import { Injectable } from '@nestjs/common'; import { createHash } from 'node:crypto'; import { promises as fsp } from 'node:fs'; import { createWriteStream, statSync, existsSync } from 'node:fs'; @@ -7,6 +8,7 @@ import { ATT_TMP_DIR, resolveAttachmentsRoot } from 'src/config/attachment.confi export type SaveResult = { sha256:string, storage_path:string, size:number}; +@Injectable() export class DiskStorageService { private root = resolveAttachmentsRoot(); @@ -38,7 +40,7 @@ export class DiskStorageService { const hash = createHash('sha256'); const tmpOut = createWriteStream(tmpPath); - input.on('date', (chunk) => hash.update(chunk)); + input.on('data', (chunk) => hash.update(chunk)); await pipeline(input, tmpOut); //await end of writing stream const sha = hash.digest('hex'); diff --git a/src/modules/attachments/services/variants.queue.ts b/src/modules/attachments/services/variants.queue.ts index 07c48c0..2c261f0 100644 --- a/src/modules/attachments/services/variants.queue.ts +++ b/src/modules/attachments/services/variants.queue.ts @@ -1,5 +1,7 @@ +import { Injectable } from "@nestjs/common"; import { Queue } from "bullmq"; +@Injectable() export class VariantsQueue { private queue : Queue; diff --git a/src/modules/bank-codes/bank-codes.module.ts b/src/modules/bank-codes/bank-codes.module.ts index 80ec6e3..816c440 100644 --- a/src/modules/bank-codes/bank-codes.module.ts +++ b/src/modules/bank-codes/bank-codes.module.ts @@ -1,11 +1,11 @@ -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.service"; +// 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.service"; -@Module({ - controllers: [BankCodesControllers], - providers: [BankCodesService, PrismaService], -}) +// @Module({ +// controllers: [BankCodesControllers], +// providers: [BankCodesService, PrismaService], +// }) -export class BankCodesModule {} \ No newline at end of file +// export class BankCodesModule {} \ 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 index 678336c..4588bbf 100644 --- a/src/modules/bank-codes/controllers/bank-codes.controller.ts +++ b/src/modules/bank-codes/controllers/bank-codes.controller.ts @@ -1,49 +1,49 @@ -import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from "@nestjs/common"; -import { BankCodesService } from "../services/bank-codes.service"; -import { CreateBankCodeDto } from "../dtos/create-bank-code.dto"; -import { UpdateBankCodeDto } from "../dtos/update-bank-code.dto"; -import { ApiBadRequestResponse, ApiNotFoundResponse, ApiOperation, ApiResponse } from "@nestjs/swagger"; +// import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from "@nestjs/common"; +// import { BankCodesService } from "../services/bank-codes.service"; +// import { CreateBankCodeDto } from "../dtos/create-bank-code.dto"; +// import { UpdateBankCodeDto } from "../dtos/update-bank-code.dto"; +// import { ApiBadRequestResponse, ApiNotFoundResponse, ApiOperation, ApiResponse } from "@nestjs/swagger"; -@Controller('bank-codes') -export class BankCodesControllers { - constructor(private readonly bankCodesService: BankCodesService) {} - //_____________________________________________________________________________________________ - // Deprecated or unused methods - //_____________________________________________________________________________________________ +// @Controller('bank-codes') +// export class BankCodesControllers { +// constructor(private readonly bankCodesService: BankCodesService) {} +// //_____________________________________________________________________________________________ +// // Deprecated or unused methods +// //_____________________________________________________________________________________________ - // @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); - // } +// // @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() +// // @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); - // } +// // @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) - // } +// // @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 +// // @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-code.dto.ts b/src/modules/bank-codes/dtos/create-bank-code.dto.ts index f2bec7b..a08020a 100644 --- a/src/modules/bank-codes/dtos/create-bank-code.dto.ts +++ b/src/modules/bank-codes/dtos/create-bank-code.dto.ts @@ -1,46 +1,46 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { Allow, IsNotEmpty, IsNumber, IsString } from "class-validator"; +// import { ApiProperty } from "@nestjs/swagger"; +// import { Type } from "class-transformer"; +// import { Allow, IsNotEmpty, IsNumber, IsString } from "class-validator"; -export class CreateBankCodeDto { - @ApiProperty({ - example: 1, - description: 'Unique ID of a bank-code (auto-generated)', - readOnly: true, - }) - @Allow() - id: number; +// export class CreateBankCodeDto { +// @ApiProperty({ +// example: 1, +// description: 'Unique ID of a bank-code (auto-generated)', +// readOnly: true, +// }) +// @Allow() +// id: number; - @ApiProperty({ - example: 'regular, vacation, emergency, sick, parental, etc', - description: 'Type of codes', - }) - @IsString() - @IsNotEmpty() - type: string; +// @ApiProperty({ +// example: 'regular, vacation, emergency, sick, parental, etc', +// description: 'Type of codes', +// }) +// @IsString() +// @IsNotEmpty() +// type: string; - @ApiProperty({ - example: 'shift, expense, leave', - description: 'categorie of the related code', - }) - @IsString() - @IsNotEmpty() - categorie: string; +// @ApiProperty({ +// example: 'shift, expense, leave', +// description: 'categorie of the related code', +// }) +// @IsString() +// @IsNotEmpty() +// categorie: string; - @ApiProperty({ - example: '0, 0.72, 1, 1.5, 2', - description: 'modifier number to apply to salary', - }) - @Type(()=> Number) - @IsNumber() - @IsNotEmpty() - modifier: number; +// @ApiProperty({ +// example: '0, 0.72, 1, 1.5, 2', +// description: 'modifier number to apply to salary', +// }) +// @Type(()=> Number) +// @IsNumber() +// @IsNotEmpty() +// modifier: number; - @ApiProperty({ - example: 'G1, G345, G501, G43, G700', - description: 'codes given by the bank', - }) - @IsString() - @IsNotEmpty() - bank_code: string; -} \ No newline at end of file +// @ApiProperty({ +// example: 'G1, G345, G501, G43, G700', +// description: 'codes given by the bank', +// }) +// @IsString() +// @IsNotEmpty() +// bank_code: string; +// } \ No newline at end of file diff --git a/src/modules/bank-codes/dtos/update-bank-code.dto.ts b/src/modules/bank-codes/dtos/update-bank-code.dto.ts index 4033484..884e544 100644 --- a/src/modules/bank-codes/dtos/update-bank-code.dto.ts +++ b/src/modules/bank-codes/dtos/update-bank-code.dto.ts @@ -1,4 +1,4 @@ -import { PartialType } from "@nestjs/swagger"; -import { CreateBankCodeDto } from "./create-bank-code.dto"; +// import { PartialType } from "@nestjs/swagger"; +// import { CreateBankCodeDto } from "./create-bank-code.dto"; -export class UpdateBankCodeDto extends PartialType(CreateBankCodeDto) {} \ No newline at end of file +// export class UpdateBankCodeDto extends PartialType(CreateBankCodeDto) {} \ No newline at end of file diff --git a/src/modules/bank-codes/services/bank-codes.service.ts b/src/modules/bank-codes/services/bank-codes.service.ts index 5e1549a..fbb8745 100644 --- a/src/modules/bank-codes/services/bank-codes.service.ts +++ b/src/modules/bank-codes/services/bank-codes.service.ts @@ -1,51 +1,51 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; -import { PrismaService } from "src/prisma/prisma.service"; -import { CreateBankCodeDto } from "../dtos/create-bank-code.dto"; -import { BankCodes } from "@prisma/client"; -import { UpdateBankCodeDto } from "../dtos/update-bank-code.dto"; +// import { Injectable, NotFoundException } from "@nestjs/common"; +// import { PrismaService } from "src/prisma/prisma.service"; +// import { CreateBankCodeDto } from "../dtos/create-bank-code.dto"; +// import { BankCodes } from "@prisma/client"; +// import { UpdateBankCodeDto } from "../dtos/update-bank-code.dto"; -@Injectable() -export class BankCodesService { - constructor(private readonly prisma: PrismaService) {} +// @Injectable() +// export class BankCodesService { +// constructor(private readonly prisma: PrismaService) {} - async create(dto: CreateBankCodeDto): Promise{ - return this.prisma.bankCodes.create({ - data: { - type: dto.type, - categorie: dto.categorie, - modifier: dto.modifier, - bank_code: dto.bank_code, - }, - }); - } +// async create(dto: CreateBankCodeDto): Promise{ +// return this.prisma.bankCodes.create({ +// data: { +// type: dto.type, +// categorie: dto.categorie, +// modifier: dto.modifier, +// bank_code: dto.bank_code, +// }, +// }); +// } - findAll() { - return this.prisma.bankCodes.findMany(); - } +// findAll() { +// return this.prisma.bankCodes.findMany(); +// } - async findOne(id: number) { - const bankCode = await this.prisma.bankCodes.findUnique({ where: {id} }); +// async findOne(id: number) { +// const bankCode = await this.prisma.bankCodes.findUnique({ where: {id} }); - if(!bankCode) throw new NotFoundException(`Bank Code #${id} not found`); +// if(!bankCode) throw new NotFoundException(`Bank Code #${id} not found`); - return bankCode; - } +// return bankCode; +// } - async update(id:number, dto: UpdateBankCodeDto) { - return await this.prisma.bankCodes.update({ - where: { id }, - data: { - type: dto.type, - categorie: dto.categorie, - modifier: dto.modifier as any, - bank_code: dto.bank_code, - }, - }); - } +// async update(id:number, dto: UpdateBankCodeDto) { +// return await this.prisma.bankCodes.update({ +// where: { id }, +// data: { +// type: dto.type, +// categorie: dto.categorie, +// modifier: dto.modifier as any, +// bank_code: dto.bank_code, +// }, +// }); +// } - async remove(id: number) { - await this.findOne(id); - return this.prisma.bankCodes.delete({ where: {id} }); - } +// async remove(id: number) { +// await this.findOne(id); +// return this.prisma.bankCodes.delete({ where: {id} }); +// } -} \ No newline at end of file +// } \ No newline at end of file diff --git a/src/modules/business-logics/services/after-hours.service.ts b/src/modules/business-logics/services/after-hours.service.ts deleted file mode 100644 index 2330b33..0000000 --- a/src/modules/business-logics/services/after-hours.service.ts +++ /dev/null @@ -1,79 +0,0 @@ -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 biz_start = new Date(start); - biz_start.setHours(AfterHoursService.BUSINESS_START, 0,0,0); - - if (end>= start || start >= biz_start) { - return 0; - } - - const segment_end = end < biz_start ? end : biz_start; - const minutes = (segment_end.getTime() - start.getTime()) / 60000; - - this.logger.debug(`getPreBusinessMintutes -> ${minutes.toFixed(1)}min`); - return minutes; - - } - - private getPostBusinessMinutes(start: Date, end: Date): number { - const biz_end = new Date(start); - biz_end.setHours(AfterHoursService.BUSINESS_END,0,0,0); - - if( end <= biz_end ) { - return 0; - } - - const segment_start = start > biz_end ? start : biz_end; - const minutes = (end.getTime() - segment_start.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 pre_min = this.getPreBusinessMinutes(start, end); - const post_min = this.getPostBusinessMinutes(start, end); - const raw_aftermin = pre_min + post_min; - - const rounded_min = this.roundToNearestQUarterMinute(raw_aftermin); - - const hours = rounded_min / 60; - const result = parseFloat(hours.toFixed(2)); - - this.logger.debug(`computeAfterHours -> raw_aftermin = ${raw_aftermin.toFixed(1)}min, + - rounded = ${rounded_min}min, hours = ${result.toFixed(2)}`); - return result; - } -} - diff --git a/src/modules/business-logics/services/overtime.service.ts b/src/modules/business-logics/services/overtime.service.ts deleted file mode 100644 index be8c15c..0000000 --- a/src/modules/business-logics/services/overtime.service.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { PrismaService } from '../../../prisma/prisma.service'; -import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils'; -import { Prisma, PrismaClient } from '@prisma/client'; - -type Tx = Prisma.TransactionClient | PrismaClient; - -export type WeekOvertimeSummary = { - week_start:string; - week_end: string; - week_total_hours: number; - weekly_overtime: number; - daily_overtime_kept: number; - total_overtime: number; - breakdown: Array<{ - date:string; - day_hours: number; - day_overtime: number; - daily_kept: number; - running_total_before: number; - }>; -}; - -@Injectable() -export class OvertimeService { - - private logger = new Logger(OvertimeService.name); - private daily_max = 8; // maximum for regular hours per day - private weekly_max = 40; // maximum for regular hours per week - private INCLUDED_TYPES = ['EMERGENCY', 'EVENING','OVERTIME','REGULAR'] as const; // included types for weekly overtime calculation - - constructor(private prisma: PrismaService) {} - - async getWeekOvertimeSummary( timesheet_id: number, date: Date, tx?: Tx ): Promise{ - const db = tx ?? this.prisma; - - const week_start = getWeekStart(date); - const week_end = getWeekEnd(week_start); - - const shifts = await db.shifts.findMany({ - where: { - timesheet_id, - date: { gte: week_start, lte: week_end }, - bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } }, - }, - select: { date: true, start_time: true, end_time: true }, - orderBy: [{date: 'asc'}, {start_time: 'asc'}], - }); - - const day_totals = new Map(); - for (const shift of shifts){ - const key = shift.date.toISOString().slice(0,10); - const hours = computeHours(shift.start_time, shift.end_time, 5); - day_totals.set(key, (day_totals.get(key) ?? 0) + hours); - } - - const days: string[] = []; - for(let i = 0; i < 7; i++){ - const day = new Date(week_start.getTime() + i * 24 * 60 * 60 * 1000); - days.push(day.toISOString().slice(0,10)); - } - - const week_total_hours = [ ...day_totals.values()].reduce((a,b) => a + b, 0); - const weekly_overtime = Math.max(0, week_total_hours - this.weekly_max); - - let running = 0; - let daily_kept_sum = 0; - const breakdown: WeekOvertimeSummary['breakdown'] = []; - - for (const key of days) { - const day_hours = day_totals.get(key) ?? 0; - const day_overtime = Math.max(0, day_hours - this.daily_max); - - const cap_before_40 = Math.max(0, this.weekly_max - running); - const daily_kept = Math.min(day_overtime, cap_before_40); - - breakdown.push({ - date: key, - day_hours, - day_overtime, - daily_kept, - running_total_before: running, - }); - - daily_kept_sum += daily_kept; - running += day_hours; - } - const total_overtime = weekly_overtime + daily_kept_sum; - - this.logger.debug( - `[OVERTIME][SUMMARY][ts=${timesheet_id}] week=${week_start.toISOString().slice(0,10)}..${week_end - .toISOString() - .slice(0,10)} week_total=${week_total_hours.toFixed(2)}h weekly=${weekly_overtime.toFixed( - 2, - )}h daily_kept=${daily_kept_sum.toFixed(2)}h total=${total_overtime.toFixed(2)}h`, - ); - return { - week_start: week_start.toISOString().slice(0, 10), - week_end: week_end.toISOString().slice(0, 10), - week_total_hours, - weekly_overtime, - daily_overtime_kept: daily_kept_sum, - total_overtime, - breakdown, - }; - } - - // //calculate daily overtime - // async getDailyOvertimeHours(timesheet_id: number, date: Date): Promise { - // const shifts = await this.prisma.shifts.findMany({ - // where: { - // timesheet_id, - // date: date, - // bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } }, - // }, - // select: { start_time: true, end_time: true }, - // orderBy: [{ start_time: 'asc' }], - // }); - - // const total = shifts.map((shift)=> - // computeHours(shift.start_time, shift.end_time, 5)). - // reduce((sum, hours)=> sum + hours, 0); - - // const overtime = Math.max(0, total - this.daily_max); - - // this.logger.debug(`[OVERTIME]-[DAILY] total=${total.toFixed(2)}h, overtime= ${overtime.toFixed(2)}h`); - // return overtime; - // } - - // //calculate Weekly overtime - // async getWeeklyOvertimeHours(timesheet_id: number, ref_date: Date): Promise { - // const week_start = getWeekStart(ref_date); - // const week_end = getWeekEnd(week_start); - - // //fetches all shifts from INCLUDED_TYPES array - // const included_shifts = await this.prisma.shifts.findMany({ - // where: { - // timesheet_id, - // date: { gte:week_start, lte: week_end }, - // bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } }, - // }, - // select: { start_time: true, end_time: true }, - // orderBy: [{date: 'asc'}, {start_time:'asc'}], - // }); - - // //calculate total hours of those shifts minus weekly Max to find total overtime hours - // const total = included_shifts.map(shift => - // computeHours(shift.start_time, shift.end_time, 5)). - // reduce((sum, hours)=> sum+hours, 0); - - // const overtime = Math.max(0, total - this.weekly_max); - - // this.logger.debug(`[OVERTIME]-[WEEKLY] total=${total.toFixed(2)}h, overtime= ${overtime.toFixed(2)}h`); - // return overtime; - // } - - - // //transform REGULAR shifts to OVERTIME when exceed 40hrs of included_types of shift - // async transformRegularHoursToWeeklyOvertime( - // employee_id: number, - // ref_date: Date, - // tx?: Prisma.TransactionClient, - // ): Promise { - // //ensures the use of the transaction if needed. fallback to this.prisma if no transaction is detected. - // const db = tx ?? this.prisma; - - // //calculate weekly overtime - // const overtime_hours = await this.getWeeklyOvertimeHours(employee_id, ref_date); - // if(overtime_hours <= 0) return; - - // const convert_to_minutes = Math.round(overtime_hours * 60); - - // const [regular, overtime] = await Promise.all([ - // db.bankCodes.findFirst({where: { type: 'REGULAR' }, select: { id: true } }), - // db.bankCodes.findFirst({where: { type: 'OVERTIME'}, select: { id: true } }), - // ]); - // if(!regular || !overtime) return; - - // const week_start = getWeekStart(ref_date); - // const week_end = getWeekEnd(week_start); - - // //gets all regular shifts and order them by desc - // const regular_shifts_desc = await db.shifts.findMany({ - // where: { - // date: { gte:week_start, lte: week_end }, - // timesheet: { employee_id }, - // bank_code_id: regular.id, - // }, - // select: { - // id: true, - // timesheet_id: true, - // date: true, - // start_time: true, - // end_time: true, - // is_remote: true, - // comment: true, - // }, - // orderBy: [{date: 'desc'}, {start_time:'desc'}], - // }); - - // let remaining_minutes = convert_to_minutes; - - // for(const shift of regular_shifts_desc) { - // if(remaining_minutes <= 0) break; - - // const start = shift.start_time; - // const end = shift.end_time; - // const duration_in_minutes = Math.max(0, Math.round((end.getTime() - start.getTime())/60000)); - // if(duration_in_minutes === 0) continue; - - // if(duration_in_minutes <= remaining_minutes) { - // await db.shifts.update({ - // where: { id: shift.id }, - // data: { bank_code_id: overtime.id }, - // }); - // remaining_minutes -= duration_in_minutes; - // continue; - // } - // //sets the start_time of the new overtime shift - // const new_overtime_start = new Date(end.getTime() - remaining_minutes * 60000); - - // //shorten the regular shift - // await db.shifts.update({ - // where: { id: shift.id }, - // data: { end_time: new_overtime_start }, - // }); - - // //creates the new overtime shift to replace the shorten regular shift - // await db.shifts.create({ - // data: { - // timesheet_id: shift.timesheet_id, - // date: shift.date, - // start_time: new_overtime_start, - // end_time: end, - // is_remote: shift.is_remote, - // comment: shift.comment, - // bank_code_id: overtime.id, - // }, - // }); - // remaining_minutes = 0; - // } - // this.logger.debug(`[OVERTIME]-[WEEKLY]-[TRANSFORM] emp=${employee_id} - // week: ${week_start.toISOString().slice(0,10)}..${week_end.toISOString().slice(0,10)} - // converted= ${(convert_to_minutes-remaining_minutes)/60}h`); - // } - -} diff --git a/src/modules/customers/controllers/customers.controller.ts b/src/modules/customers/controllers/customers.controller.ts deleted file mode 100644 index 713ebde..0000000 --- a/src/modules/customers/controllers/customers.controller.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from '@nestjs/common'; -import { CustomersService } from '../services/customers.service'; -import { Customers } from '@prisma/client'; -import { CreateCustomerDto } from '../dtos/create-customer.dto'; -import { UpdateCustomerDto } from '../dtos/update-customer.dto'; -import { RolesAllowed } from "src/common/decorators/roles.decorators"; -import { Roles as RoleEnum } from '.prisma/client'; -import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; - -@ApiTags('Customers') -@ApiBearerAuth('access-token') -// @UseGuards() -@Controller('customers') -export class CustomersController { - constructor(private readonly customersService: CustomersService) {} - -//_____________________________________________________________________________________________ -// Deprecated or unused methods -//_____________________________________________________________________________________________ - - // @Post() - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.SUPERVISOR) - // @ApiOperation({ summary: 'Create customer' }) - // @ApiResponse({ status: 201, description: 'Customer created', type: CreateCustomerDto }) - // @ApiResponse({ status: 400, description: 'Invalid task or invalid data' }) - // create(@Body() dto: CreateCustomerDto): Promise { - // return this.customersService.create(dto); - // } - - // @Get() - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) - // @ApiOperation({ summary: 'Find all customers' }) - // @ApiResponse({ status: 201, description: 'List of customers found', type: CreateCustomerDto, isArray: true }) - // @ApiResponse({ status: 400, description: 'List of customers not found' }) - // findAll(): Promise { - // return this.customersService.findAll(); - // } - - // @Get(':id') - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) - // @ApiOperation({ summary: 'Find customer' }) - // @ApiResponse({ status: 201, description: 'Customer found', type: CreateCustomerDto }) - // @ApiResponse({ status: 400, description: 'Customer not found' }) - // findOne(@Param('id', ParseIntPipe) id: number): Promise { - // return this.customersService.findOne(id); - // } - - // @Patch(':id') - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE,RoleEnum.SUPERVISOR) - // @ApiOperation({ summary: 'Update customer' }) - // @ApiResponse({ status: 201, description: 'Customer updated', type: CreateCustomerDto }) - // @ApiResponse({ status: 400, description: 'Customer not found' }) - // update( - // @Param('id', ParseIntPipe) id: number, - // @Body() dto: UpdateCustomerDto, - // ): Promise { - // return this.customersService.update(id, dto); - // } - - // @Delete(':id') - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.SUPERVISOR) - // @ApiOperation({ summary: 'Delete customer' }) - // @ApiResponse({ status: 201, description: 'Customer deleted', type: CreateCustomerDto }) - // @ApiResponse({ status: 400, description: 'Customer not found' }) - // remove(@Param('id', ParseIntPipe) id: number): Promise{ - // return this.customersService.remove(id); - // } -} diff --git a/src/modules/customers/customers.module.ts b/src/modules/customers/customers.module.ts deleted file mode 100644 index c9e3f9d..0000000 --- a/src/modules/customers/customers.module.ts +++ /dev/null @@ -1,10 +0,0 @@ - -import { Module } from '@nestjs/common'; -import { CustomersController } from './controllers/customers.controller'; -import { CustomersService } from './services/customers.service'; - -@Module({ - controllers:[CustomersController], - providers:[CustomersService], -}) -export class CustomersModule {} \ No newline at end of file diff --git a/src/modules/customers/dtos/create-customer.dto.ts b/src/modules/customers/dtos/create-customer.dto.ts deleted file mode 100644 index 8382f20..0000000 --- a/src/modules/customers/dtos/create-customer.dto.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { - Allow, - IsEmail, - IsInt, - IsNotEmpty, - IsOptional, - IsPositive, - IsString, - IsUUID, -} from "class-validator"; - -export class CreateCustomerDto { - @ApiProperty({ - example: 1, - description: 'Unique ID of a customer(primary-key, auto-incremented)', - }) - @Allow() - id?: number; - - @ApiProperty({ - example: '0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d', - description: 'UUID of the user linked to that customer', - }) - @IsUUID() - @IsOptional() - user_id?: string; - - @ApiProperty({ - example: 'Gandalf', - description: 'Customer`s first name', - }) - @IsString() - @IsNotEmpty() - first_name: string; - - @ApiProperty({ - example: 'TheGray', - description: 'Customer`s last name', - }) - @IsString() - @IsNotEmpty() - last_name: string; - - @ApiProperty({ - example: 'you_shall_not_pass@middleEarth.com', - description: 'Customer`s email', - }) - @IsEmail() - @IsOptional() - email: string; - - @ApiProperty({ - example: '8436637464', - description: 'Customer`s phone number', - }) - @IsString() - phone_number: string; - - @ApiProperty({ - example: '1 Ringbearer`s way, Mount Doom city, ME, T1R 1N6 ', - description: 'Customer`s residence', - required: false, - }) - @IsString() - @IsOptional() - residence?: string; - - @ApiProperty({ - example: '4263253', - description: 'Customer`s invoice number', - required: false, - }) - @Type(() => Number) - @IsInt() - @IsNotEmpty() - invoice_id: number; -} \ No newline at end of file diff --git a/src/modules/customers/dtos/update-customer.dto.ts b/src/modules/customers/dtos/update-customer.dto.ts deleted file mode 100644 index fc1ba39..0000000 --- a/src/modules/customers/dtos/update-customer.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from "@nestjs/swagger"; -import { CreateCustomerDto } from "./create-customer.dto"; - -export class UpdateCustomerDto extends PartialType(CreateCustomerDto) {} \ No newline at end of file diff --git a/src/modules/customers/services/customers.service.ts b/src/modules/customers/services/customers.service.ts deleted file mode 100644 index b0b68c8..0000000 --- a/src/modules/customers/services/customers.service.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class CustomersService { - -//_____________________________________________________________________________________________ -// Deprecated or unused methods -//_____________________________________________________________________________________________ - -// constructor(private readonly prisma: PrismaService) {} - -// async create(dto: CreateCustomerDto): Promise { -// const { -// first_name, -// last_name, -// email, -// phone_number, -// residence, -// invoice_id, -// } = dto; - -// return this.prisma.$transaction(async (transaction) => { -// const user: Users = await transaction.users.create({ -// data: { -// first_name, -// last_name, -// email, -// phone_number, -// residence, -// }, -// }); -// return transaction.customers.create({ -// data: { -// user_id: user.id, -// invoice_id, -// }, -// }); -// }); -// } - -// findAll(): Promise { -// return this.prisma.customers.findMany({ -// include: { user: true }, -// }) -// } - -// async findOne(id:number): Promise { -// const customer = await this.prisma.customers.findUnique({ -// where: { id }, -// include: { user: true }, -// }); -// if(!customer) throw new NotFoundException(`Customer #${id} not found`); -// return customer; -// } - -// async update(id: number,dto: UpdateCustomerDto): Promise { -// const customer = await this.findOne(id); - -// const { -// first_name, -// last_name, -// email, -// phone_number, -// residence, -// invoice_id, -// } = dto; - -// return this.prisma.$transaction(async (transaction) => { -// await transaction.users.update({ -// where: { id: customer.user_id }, -// data: { -// ...(first_name !== undefined && { first_name }), -// ...(last_name !== undefined && { last_name }), -// ...(email !== undefined && { email }), -// ...(phone_number !== undefined && { phone_number }), -// ...(residence !== undefined && { residence }), -// }, -// }); - -// return transaction.customers.update({ -// where: { id }, -// data: { -// ...(invoice_id !== undefined && { invoice_id }), -// }, -// }); -// }); -// } - -// async remove(id: number): Promise { -// await this.findOne(id); -// return this.prisma.customers.delete({ where: { id }}); -// } -} diff --git a/src/modules/employees/controllers/employees.controller.ts b/src/modules/employees/controllers/employees.controller.ts deleted file mode 100644 index e647547..0000000 --- a/src/modules/employees/controllers/employees.controller.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Body,Controller,Get,NotFoundException,Param,Patch } from '@nestjs/common'; -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, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { EmployeeListItemDto } from '../dtos/list-employee.dto'; -import { EmployeesArchivalService } from '../services/employees-archival.service'; -import { EmployeeProfileItemDto } from 'src/modules/employees/dtos/profil-employee.dto'; - -@ApiTags('Employees') -@ApiBearerAuth('access-token') -// @UseGuards() -@Controller('employees') -export class EmployeesController { - constructor( - private readonly employeesService: EmployeesService, - private readonly archiveService: EmployeesArchivalService, - ) {} - - @Get('employee-list') - //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING) - @ApiOperation({summary: 'Find all employees with scoped info' }) - @ApiResponse({ status: 200, description: 'List of employees with scoped info found', type: EmployeeListItemDto, isArray: true }) - @ApiResponse({ status: 400, description: 'List of employees with scoped info not found' }) - findListEmployees(): Promise { - return this.employeesService.findListEmployees(); - } - - @Patch(':email') - //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) - @ApiBearerAuth('access-token') - @ApiOperation({ summary: 'Update, archive or restore an employee' }) - @ApiParam({ name: 'email', type: Number, description: 'Email of the employee' }) - @ApiResponse({ status: 200, description: 'Employee updated or restored', type: CreateEmployeeDto }) - @ApiResponse({ status: 202, description: 'Employee archived successfully', type: CreateEmployeeDto }) - @ApiResponse({ status: 404, description: 'Employee not found in active or archive' }) - async updateOrArchiveOrRestore(@Param('email') email: 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.archiveService.patchEmployee(email, dto); - if(!result) { - throw new NotFoundException(`Employee with email: ${ email } is not found in active or archive.`) - } - return result; - } - - //_____________________________________________________________________________________________ - // Deprecated or unused methods - //_____________________________________________________________________________________________ - - // @Post() - // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) - // @ApiOperation({summary: 'Create employee' }) - // @ApiResponse({ status: 201, description: 'Employee created', type: CreateEmployeeDto }) - // @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) - // create(@Body() dto: CreateEmployeeDto): Promise { - // return this.employeesService.create(dto); - // } - // @Get() - // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING) - // @ApiOperation({summary: 'Find all employees' }) - // @ApiResponse({ status: 200, description: 'List of employees found', type: CreateEmployeeDto, isArray: true }) - // @ApiResponse({ status: 400, description: 'List of employees not found' }) - // findAll(): Promise { - // return this.employeesService.findAll(); - // } - - - // @Get(':email') - // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING ) - // @ApiOperation({summary: 'Find employee' }) - // @ApiResponse({ status: 200, description: 'Employee found', type: CreateEmployeeDto }) - // @ApiResponse({ status: 400, description: 'Employee not found' }) - // findOne(@Param('email', ParseIntPipe) email: string): Promise { - // return this.employeesService.findOne(email); - // } - - @Get('profile/:email') - @ApiOperation({summary: 'Find employee profile' }) - @ApiParam({ name: 'email', type: String, description: 'Identifier of the employee' }) - @ApiResponse({ status: 200, description: 'Employee profile found', type: EmployeeProfileItemDto }) - @ApiResponse({ status: 400, description: 'Employee profile not found' }) - findOneProfile(@Param('email') email: string): Promise { - return this.employeesService.findOneProfile(email); - } - - // @Delete(':email') - // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR ) - // @ApiOperation({summary: 'Delete employee' }) - // @ApiParam({ name: 'email', type: Number, description: 'Email of the employee to delete' }) - // @ApiResponse({ status: 204, description: 'Employee deleted' }) - // @ApiResponse({ status: 404, description: 'Employee not found' }) - // remove(@Param('email', ParseIntPipe) email: string): Promise { - // return this.employeesService.remove(email); - // } - -} diff --git a/src/modules/employees/employees.module.ts b/src/modules/employees/employees.module.ts deleted file mode 100644 index 0f0be93..0000000 --- a/src/modules/employees/employees.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { EmployeesController } from './controllers/employees.controller'; -import { EmployeesService } from './services/employees.service'; -import { EmployeesArchivalService } from './services/employees-archival.service'; -import { SharedModule } from '../shared/shared.module'; - -@Module({ - imports: [SharedModule], - controllers: [EmployeesController], - providers: [EmployeesService, EmployeesArchivalService], - exports: [EmployeesService, EmployeesArchivalService], -}) -export class EmployeesModule {} diff --git a/src/modules/employees/services/employees-archival.service.ts b/src/modules/employees/services/employees-archival.service.ts deleted file mode 100644 index b13fa74..0000000 --- a/src/modules/employees/services/employees-archival.service.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { Employees, EmployeesArchive, Users } from "@prisma/client"; -import { PrismaService } from "src/prisma/prisma.service"; -import { UpdateEmployeeDto } from "../dtos/update-employee.dto"; -import { toDateOrUndefined, toDateOrNull } from "../utils/employee.utils"; - -@Injectable() -export class EmployeesArchivalService { - constructor(private readonly prisma: PrismaService) { } - - async patchEmployee(email: string, dto: UpdateEmployeeDto): Promise { - // 1) Tenter sur employés actifs - const active = await this.prisma.employees.findFirst({ - where: { user: { email } }, - include: { user: true }, - }); - - if (active) { - // Archivage : si on reçoit un last_work_day défini et que l'employé n’est pas déjà terminé - if (dto.last_work_day !== undefined && active.last_work_day == null && dto.last_work_day !== null) { - return this.archiveOnTermination(active, dto); - } - - // Sinon, update standard (split Users/Employees) - const { - first_name, - last_name, - email: new_email, - phone_number, - residence, - external_payroll_id, - company_code, - job_title, - first_work_day, - last_work_day, - supervisor_id, - is_supervisor, - } = dto as any; - - const first_work_d = toDateOrUndefined(first_work_day); - const last_work_d = Object.prototype.hasOwnProperty('last_work_day') - ? toDateOrNull(last_work_day ?? null) - : undefined; - - await this.prisma.$transaction(async (transaction) => { - if ( - first_name !== undefined || - last_name !== undefined || - new_email !== undefined || - phone_number !== undefined || - residence !== undefined - ) { - await transaction.users.update({ - where: { id: active.user_id }, - data: { - ...(first_name !== undefined ? { first_name } : {}), - ...(last_name !== undefined ? { last_name } : {}), - ...(email !== undefined ? { email: new_email } : {}), - ...(phone_number !== undefined ? { phone_number } : {}), - ...(residence !== undefined ? { residence } : {}), - }, - }); - - } - - const updated = await transaction.employees.update({ - where: { id: active.id }, - data: { - ...(external_payroll_id !== undefined ? { external_payroll_id } : {}), - ...(company_code !== undefined ? { company_code } : {}), - ...(job_title !== undefined ? { job_title } : {}), - ...(first_work_d !== undefined ? { first_work_day: first_work_d } : {}), - ...(last_work_d !== undefined ? { last_work_day: last_work_d } : {}), - ...(is_supervisor !== undefined ? { is_supervisor } : {}), - ...(supervisor_id !== undefined ? { supervisor_id } : {}), - }, - include: { user: true }, - }); - - return updated; - }); - - return this.prisma.employees.findFirst({ where: { user: { email } } }); - } - - const user = await this.prisma.users.findUnique({ where: { email } }); - if (!user) return null; - // 2) Pas trouvé en actifs → regarder en archive (pour restauration) - const archived = await this.prisma.employeesArchive.findFirst({ - where: { user_id: user.id }, - include: { user: true }, - }); - - if (archived) { - // Condition de restauration : last_work_day === null ou first_work_day fourni - const restore = dto.last_work_day === null || dto.first_work_day != null; - if (restore) { - return this.restoreEmployee(archived, dto); - } - } - // 3) Ni actif, ni archivé → 404 dans le controller - return null; - } - - //transfers the employee to archive and then delete from employees table - private async archiveOnTermination(active: Employees & { user: Users }, dto: UpdateEmployeeDto): Promise { - const last_work_d = toDateOrNull(dto.last_work_day!); - if (!last_work_d) throw new Error('invalide last_work_day for archive'); - return this.prisma.$transaction(async transaction => { - //detach crew from supervisor if employee is a supervisor - await transaction.employees.updateMany({ - where: { supervisor_id: active.id }, - data: { supervisor_id: null }, - }) - const archived = await transaction.employeesArchive.create({ - data: { - employee_id: active.id, - user_id: active.user_id, - first_name: active.user.first_name, - last_name: active.user.last_name, - company_code: active.company_code, - job_title: active.job_title, - first_work_day: active.first_work_day, - last_work_day: last_work_d, - supervisor_id: active.supervisor_id ?? null, - is_supervisor: active.is_supervisor, - external_payroll_id: active.external_payroll_id, - }, - include: { user: true } - }); - //delete from employees table - await transaction.employees.delete({ where: { id: active.id } }); - //return archived employee - return archived - }); - } - - //transfers the employee from archive to the employees table - private async restoreEmployee(archived: EmployeesArchive & { user: Users }, dto: UpdateEmployeeDto): Promise { - // const first_work_d = toDateOrUndefined(dto.first_work_day); - return this.prisma.$transaction(async transaction => { - //restores the archived employee into the employees table - const restored = await transaction.employees.create({ - data: { - user_id: archived.user_id, - company_code: archived.company_code, - job_title: archived.job_title, - first_work_day: archived.first_work_day, - last_work_day: null, - is_supervisor: archived.is_supervisor ?? false, - external_payroll_id: archived.external_payroll_id, - }, - }); - //deleting archived entry by id - await transaction.employeesArchive.delete({ where: { id: archived.id } }); - - //return restored employee - 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/controllers/expense.controller.ts b/src/modules/expenses/controllers/expense.controller.ts deleted file mode 100644 index bb37634..0000000 --- a/src/modules/expenses/controllers/expense.controller.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Body, Controller, Param, ParseIntPipe, Post } from "@nestjs/common"; -import { PrismaService } from "src/prisma/prisma.service"; -import { ExpenseDto } from "../dtos/expense.dto"; -import { CreateResult, ExpenseUpsertService } from "../services/expense-upsert.service"; - - -@Controller('expense') -export class ExpenseController { - constructor( - private readonly prisma: PrismaService, - private readonly upsert_service: ExpenseUpsertService, - ){} - - - // @Post(':timesheet_id') - // create( - // @Param('timesheet_id', ParseIntPipe) timesheet_id: number, - // @Body() dto: ExpenseDto): Promise{ - // return this.upsert_service.createExpense(timesheet_id, dto); - // } -} \ No newline at end of file diff --git a/src/modules/expenses/dtos/update-expense.dto.ts b/src/modules/expenses/dtos/update-expense.dto.ts deleted file mode 100644 index fc709bb..0000000 --- a/src/modules/expenses/dtos/update-expense.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { OmitType, PartialType } from "@nestjs/swagger"; -import { ExpenseDto } from "./expense.dto"; - -export class updateExpenseDto extends PartialType ( - OmitType(ExpenseDto, ['is_approved', 'timesheet_id'] as const) -){} \ No newline at end of file diff --git a/src/modules/expenses/expenses.module.ts b/src/modules/expenses/expenses.module.ts deleted file mode 100644 index 490ec9e..0000000 --- a/src/modules/expenses/expenses.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -// import { ExpensesController } from "./controllers/expenses.controller"; -// import { Module } from "@nestjs/common"; -// import { ExpensesQueryService } from "./services/expenses-query.service"; -// import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module"; -// import { ExpensesCommandService } from "./services/expenses-command.service"; -// import { ExpensesArchivalService } from "./services/expenses-archival.service"; -// import { SharedModule } from "../shared/shared.module"; - -// @Module({ -// imports: [BusinessLogicsModule, SharedModule], -// controllers: [ExpensesController], -// providers: [ -// ExpensesQueryService, -// ExpensesArchivalService, -// ExpensesCommandService, -// ], -// exports: [ -// ExpensesQueryService, -// ExpensesArchivalService, -// ], -// }) - -// export class ExpensesModule {} \ No newline at end of file diff --git a/src/modules/expenses/helpers/expenses-date-time-helpers.ts b/src/modules/expenses/helpers/expenses-date-time-helpers.ts deleted file mode 100644 index ce14e69..0000000 --- a/src/modules/expenses/helpers/expenses-date-time-helpers.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const toDateFromString = (ymd: string): Date => { - return new Date(`${ymd}T00:00:00:000Z`); -} \ No newline at end of file diff --git a/src/modules/expenses/services/expense-upsert.service.ts b/src/modules/expenses/services/expense-upsert.service.ts deleted file mode 100644 index 5e47a1e..0000000 --- a/src/modules/expenses/services/expense-upsert.service.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { PrismaService } from "src/prisma/prisma.service"; -import { GetExpenseDto } from "../dtos/get-expense.dto"; -import { updateExpenseDto } from "../dtos/update-expense.dto"; -import { ExpenseDto } from "../dtos/expense.dto"; -import { toDateFromString } from "../helpers/expenses-date-time-helpers"; - -type Normalized = { date: Date; comment: string; supervisor_comment: string; }; - -export type CreateResult = { ok: true; data: GetExpenseDto } | { ok: false; error: any }; -export type UpdatePayload = { id: number; dto: updateExpenseDto }; -export type UpdateResult = { ok: true; id: number; data: GetExpenseDto } | { ok: false; id: number; error: any }; -export type DeleteResult = { ok: true; id: number; } | { ok: false; id: number; error: any }; - -type NormedOk = { dto: GetExpenseDto; normed: Normalized }; -type NormedErr = { error: any }; - -@Injectable() -export class ExpenseUpsertService { - constructor(private readonly prisma: PrismaService){} - - //_________________________________________________________________ - // CREATE - //_________________________________________________________________ - //normalized frontend data to match DB - async createExpense(timesheet_id: number, dto: ExpenseDto){ - const normed_expense = this.normalizeExpenseDto(dto) - - - } - - //_________________________________________________________________ - // LOCAL HELPERS - //_________________________________________________________________ - private normalizeExpenseDto(dto: ExpenseDto): Normalized { - const date = toDateFromString(dto.date); - const comment = this.truncate280(dto.comment); - const supervisor_comment = this.truncate280(dto.supervisor_comment? dto.supervisor_comment : ''); - return { date, comment, supervisor_comment }; - } - - //makes sure that a string cannot exceed 280 chars - private truncate280 = (input: string): string => { - return input.length > 280 ? input.slice(0, 280) : input; - } -} \ No newline at end of file diff --git a/src/modules/exports/csv-exports.module.ts b/src/modules/exports/csv-exports.module.ts index e034c9e..92a5a96 100644 --- a/src/modules/exports/csv-exports.module.ts +++ b/src/modules/exports/csv-exports.module.ts @@ -1,10 +1,9 @@ import { Module } from "@nestjs/common"; import { CsvExportController } from "./controllers/csv-exports.controller"; import { CsvExportService } from "./services/csv-exports.service"; -import { SharedModule } from "../shared/shared.module"; @Module({ - providers:[CsvExportService, SharedModule], + providers:[CsvExportService], controllers: [CsvExportController], }) export class CsvExportModule {} diff --git a/src/modules/leave-requests/controllers/leave-requests.controller.ts b/src/modules/leave-requests/controllers/leave-requests.controller.ts deleted file mode 100644 index 7ecce7e..0000000 --- a/src/modules/leave-requests/controllers/leave-requests.controller.ts +++ /dev/null @@ -1,30 +0,0 @@ -// import { Body, Controller, Post } from "@nestjs/common"; -// import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; -// import { LeaveRequestsService } from "../services/leave-request.service"; -// import { UpsertLeaveRequestDto } from "../dtos/upsert-leave-request.dto"; -// import { LeaveTypes } from "@prisma/client"; - -// @ApiTags('Leave Requests') -// @ApiBearerAuth('access-token') -// // @UseGuards() -// @Controller('leave-requests') -// export class LeaveRequestController { -// constructor(private readonly leave_service: LeaveRequestsService){} - -// @Post('upsert') -// async upsertLeaveRequest(@Body() dto: UpsertLeaveRequestDto) { -// const { action, leave_requests } = await this.leave_service.handle(dto); -// return { action, leave_requests }; -// }q - -// //TODO: -// /* -// @Get('archive') -// findAllArchived(){...} - -// @Get('archive/:id') -// findOneArchived(id){...} -// */ - -// } - diff --git a/src/modules/leave-requests/leave-requests.module.ts b/src/modules/leave-requests/leave-requests.module.ts deleted file mode 100644 index 03ad546..0000000 --- a/src/modules/leave-requests/leave-requests.module.ts +++ /dev/null @@ -1,29 +0,0 @@ -// import { PrismaService } from "src/prisma/prisma.service"; -// import { LeaveRequestController } from "./controllers/leave-requests.controller"; -// import { HolidayLeaveRequestsService } from "./services/holiday-leave-requests.service"; -// import { Module } from "@nestjs/common"; -// import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module"; -// import { VacationLeaveRequestsService } from "./services/vacation-leave-requests.service"; -// import { SickLeaveRequestsService } from "./services/sick-leave-requests.service"; -// import { LeaveRequestsService } from "./services/leave-request.service"; -// import { ShiftsModule } from "../shifts/shifts.module"; -// import { LeaveRequestsUtils } from "./utils/leave-request.util"; -// import { SharedModule } from "../shared/shared.module"; - -// @Module({ -// imports: [BusinessLogicsModule, ShiftsModule, SharedModule], -// controllers: [LeaveRequestController], -// providers: [ -// VacationLeaveRequestsService, -// SickLeaveRequestsService, -// HolidayLeaveRequestsService, -// LeaveRequestsService, -// PrismaService, -// LeaveRequestsUtils, -// ], -// exports: [ -// LeaveRequestsService, -// ], -// }) - -// export class LeaveRequestsModule {} \ No newline at end of file diff --git a/src/modules/leave-requests/services/holiday-leave-requests.service.ts b/src/modules/leave-requests/services/holiday-leave-requests.service.ts deleted file mode 100644 index ecfa8cc..0000000 --- a/src/modules/leave-requests/services/holiday-leave-requests.service.ts +++ /dev/null @@ -1,78 +0,0 @@ -// import { UpsertLeaveRequestDto, UpsertResult } from '../dtos/upsert-leave-request.dto'; -// import { LeaveRequestViewDto } from '../dtos/leave-request-view.dto'; -// import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; -// import { LeaveApprovalStatus, LeaveTypes } from '@prisma/client'; -// import { HolidayService } from 'src/modules/business-logics/services/holiday.service'; -// import { PrismaService } from 'src/prisma/prisma.service'; -// import { mapRowToView } from '../mappers/leave-requests.mapper'; -// import { leaveRequestsSelect } from '../utils/leave-requests.select'; -// import { LeaveRequestsUtils} from '../utils/leave-request.util'; -// import { normalizeDates, toDateOnly } from 'src/modules/shared/helpers/date-time.helpers'; -// import { BankCodesResolver } from 'src/modules/shared/utils/resolve-bank-type-id.utils'; -// import { EmailToIdResolver } from 'src/modules/shared/utils/resolve-email-id.utils'; - - -// @Injectable() -// export class HolidayLeaveRequestsService { -// constructor( -// private readonly prisma: PrismaService, -// private readonly holidayService: HolidayService, -// private readonly leaveUtils: LeaveRequestsUtils, -// private readonly emailResolver: EmailToIdResolver, -// private readonly typeResolver: BankCodesResolver, -// ) {} - -// async create(dto: UpsertLeaveRequestDto): Promise { -// const email = dto.email.trim(); -// const employee_id = await this.emailResolver.findIdByEmail(email); -// const bank_code = await this.typeResolver.findByType(LeaveTypes.HOLIDAY); -// if(!bank_code) throw new NotFoundException(`bank_code not found`); -// const dates = normalizeDates(dto.dates); -// if (!dates.length) throw new BadRequestException('Dates array must not be empty'); - -// const created: LeaveRequestViewDto[] = []; - -// for (const iso_date of dates) { -// const date = toDateOnly(iso_date); - -// const existing = await this.prisma.leaveRequests.findUnique({ -// where: { -// leave_per_employee_date: { -// employee_id: employee_id, -// leave_type: LeaveTypes.HOLIDAY, -// date, -// }, -// }, -// select: { id: true }, -// }); -// if (existing) { -// throw new BadRequestException(`Holiday request already exists for ${iso_date}`); -// } - -// const payable = await this.holidayService.calculateHolidayPay(email, date, bank_code.modifier); -// const row = await this.prisma.leaveRequests.create({ -// data: { -// employee_id: employee_id, -// bank_code_id: bank_code.id, -// leave_type: LeaveTypes.HOLIDAY, -// date, -// comment: dto.comment ?? '', -// requested_hours: dto.requested_hours ?? 8, -// payable_hours: payable, -// approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING, -// }, -// select: leaveRequestsSelect, -// }); - -// const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); -// if (row.approval_status === LeaveApprovalStatus.APPROVED) { -// await this.leaveUtils.syncShift(email, employee_id, iso_date, hours,LeaveTypes.HOLIDAY, row.comment); -// } - -// created.push({ ...mapRowToView(row), action: 'create' }); -// } - -// return { action: 'create', leave_requests: created }; -// } -// } - diff --git a/src/modules/leave-requests/services/leave-request.service.ts b/src/modules/leave-requests/services/leave-request.service.ts deleted file mode 100644 index 7b0c82e..0000000 --- a/src/modules/leave-requests/services/leave-request.service.ts +++ /dev/null @@ -1,248 +0,0 @@ -// import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -// import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client"; -// import { roundToQuarterHour } from "src/common/utils/date-utils"; -// import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto"; -// import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; -// import { mapRowToView } from "../mappers/leave-requests.mapper"; -// import { leaveRequestsSelect } from "../utils/leave-requests.select"; -// import { HolidayLeaveRequestsService } from "./holiday-leave-requests.service"; -// import { SickLeaveRequestsService } from "./sick-leave-requests.service"; -// import { VacationLeaveRequestsService } from "./vacation-leave-requests.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"; -// import { PrismaService } from "src/prisma/prisma.service"; -// import { LeaveRequestsUtils } from "../utils/leave-request.util"; -// import { normalizeDates, toDateOnly, toISODateKey } from "src/modules/shared/helpers/date-time.helpers"; -// import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; -// import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; - -// @Injectable() -// export class LeaveRequestsService { -// constructor( -// private readonly prisma: PrismaService, -// private readonly holidayLeaveService: HolidayLeaveRequestsService, -// private readonly holidayService: HolidayService, -// private readonly sickLogic: SickLeaveService, -// private readonly sickLeaveService: SickLeaveRequestsService, -// private readonly vacationLeaveService: VacationLeaveRequestsService, -// private readonly vacationLogic: VacationService, -// private readonly leaveUtils: LeaveRequestsUtils, -// private readonly emailResolver: EmailToIdResolver, -// private readonly typeResolver: BankCodesResolver, -// ) {} - -// //handle distribution to the right service according to the selected type and action -// async handle(dto: UpsertLeaveRequestDto): Promise { -// switch (dto.type) { -// case LeaveTypes.HOLIDAY: -// if( dto.action === 'create'){ -// return this.holidayLeaveService.create(dto); -// } else if (dto.action === 'update') { -// return this.update(dto, LeaveTypes.HOLIDAY); -// } else if (dto.action === 'delete'){ -// return this.delete(dto, LeaveTypes.HOLIDAY); -// } -// case LeaveTypes.VACATION: -// if( dto.action === 'create'){ -// return this.vacationLeaveService.create(dto); -// } else if (dto.action === 'update') { -// return this.update(dto, LeaveTypes.VACATION); -// } else if (dto.action === 'delete'){ -// return this.delete(dto, LeaveTypes.VACATION); -// } -// case LeaveTypes.SICK: -// if( dto.action === 'create'){ -// return this.sickLeaveService.create(dto); -// } else if (dto.action === 'update') { -// return this.update(dto, LeaveTypes.SICK); -// } else if (dto.action === 'delete'){ -// return this.delete(dto, LeaveTypes.SICK); -// } -// default: -// throw new BadRequestException(`Unsupported leave type: ${dto.type} or action: ${dto.action}`); -// } -// } - -// async delete(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise { -// const email = dto.email.trim(); -// const dates = normalizeDates(dto.dates); -// const employee_id = await this.emailResolver.findIdByEmail(email); -// if (!dates.length) throw new BadRequestException("Dates array must not be empty"); - -// const rows = await this.prisma.leaveRequests.findMany({ -// where: { -// employee_id: employee_id, -// leave_type: type, -// date: { in: dates.map((d) => toDateOnly(d)) }, -// }, -// select: leaveRequestsSelect, -// }); - -// if (rows.length !== dates.length) { -// const missing = dates.filter((isoDate) => !rows.some((row) => toISODateKey(row.date) === isoDate)); -// throw new NotFoundException(`No Leave request found for: ${missing.join(", ")}`); -// } - -// for (const row of rows) { -// if (row.approval_status === LeaveApprovalStatus.APPROVED) { -// const iso = toISODateKey(row.date); -// await this.leaveUtils.removeShift(email, employee_id, iso, type); -// } -// } - -// await this.prisma.leaveRequests.deleteMany({ -// where: { id: { in: rows.map((row) => row.id) } }, -// }); - -// const deleted = rows.map((row) => ({ ...mapRowToView(row), action: "delete" as const })); -// return { action: "delete", leave_requests: deleted }; -// } - -// async update(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise { -// const email = dto.email.trim(); -// const employee_id = await this.emailResolver.findIdByEmail(email); -// const bank_code = await this.typeResolver.findByType(type); -// if(!bank_code) throw new NotFoundException(`bank_code not found`); -// const modifier = Number(bank_code.modifier ?? 1); -// const dates = normalizeDates(dto.dates); -// if (!dates.length) { -// throw new BadRequestException("Dates array must not be empty"); -// } - -// const entries = await Promise.all( -// dates.map(async (iso_date) => { -// const date = toDateOnly(iso_date); -// const existing = await this.prisma.leaveRequests.findUnique({ -// where: { -// leave_per_employee_date: { -// employee_id: employee_id, -// leave_type: type, -// date, -// }, -// }, -// select: leaveRequestsSelect, -// }); -// if (!existing) throw new NotFoundException(`No Leave request found for ${iso_date}`); -// return { iso_date, date, existing }; -// }), -// ); - -// const updated: LeaveRequestViewDto[] = []; - -// if (type === LeaveTypes.SICK) { -// const firstExisting = entries[0].existing; -// const fallbackRequested = -// firstExisting.requested_hours !== null && firstExisting.requested_hours !== undefined -// ? Number(firstExisting.requested_hours) -// : 8; -// const requested_hours_per_day = dto.requested_hours ?? fallbackRequested; -// const reference_date = entries.reduce( -// (latest, entry) => (entry.date > latest ? entry.date : latest), -// entries[0].date, -// ); -// const total_payable_hours = await this.sickLogic.calculateSickLeavePay( -// employee_id, -// reference_date, -// entries.length, -// requested_hours_per_day, -// modifier, -// ); -// let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours)); -// const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); - -// for (const { iso_date, existing } of entries) { -// const previous_status = existing.approval_status; -// const payable = Math.min(remaining_payable_hours, daily_payable_cap); -// const payable_rounded = roundToQuarterHour(Math.max(0, payable)); -// remaining_payable_hours = roundToQuarterHour( -// Math.max(0, remaining_payable_hours - payable_rounded), -// ); - -// const row = await this.prisma.leaveRequests.update({ -// where: { id: existing.id }, -// data: { -// comment: dto.comment ?? existing.comment, -// requested_hours: requested_hours_per_day, -// payable_hours: payable_rounded, -// bank_code_id: bank_code.id, -// approval_status: dto.approval_status ?? existing.approval_status, -// }, -// select: leaveRequestsSelect, -// }); - -// const was_approved = previous_status === LeaveApprovalStatus.APPROVED; -// const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED; -// const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); - -// if (!was_approved && is_approved) { -// await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); -// } else if (was_approved && !is_approved) { -// await this.leaveUtils.removeShift(email, employee_id, iso_date, type); -// } else if (was_approved && is_approved) { -// await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); -// } -// updated.push({ ...mapRowToView(row), action: "update" }); -// } -// return { action: "update", leave_requests: updated }; -// } - -// for (const { iso_date, date, existing } of entries) { -// const previous_status = existing.approval_status; -// const fallbackRequested = -// existing.requested_hours !== null && existing.requested_hours !== undefined -// ? Number(existing.requested_hours) -// : 8; -// const requested_hours = dto.requested_hours ?? fallbackRequested; - -// let payable: number; -// switch (type) { -// case LeaveTypes.HOLIDAY: -// payable = await this.holidayService.calculateHolidayPay(email, date, modifier); -// break; -// case LeaveTypes.VACATION: { -// const days_requested = requested_hours / 8; -// payable = await this.vacationLogic.calculateVacationPay( -// employee_id, -// date, -// Math.max(0, days_requested), -// modifier, -// ); -// break; -// } -// default: -// payable = existing.payable_hours !== null && existing.payable_hours !== undefined -// ? Number(existing.payable_hours) -// : requested_hours; -// } - -// const row = await this.prisma.leaveRequests.update({ -// where: { id: existing.id }, -// data: { -// requested_hours, -// comment: dto.comment ?? existing.comment, -// payable_hours: payable, -// bank_code_id: bank_code.id, -// approval_status: dto.approval_status ?? existing.approval_status, -// }, -// select: leaveRequestsSelect, -// }); - -// const was_approved = previous_status === LeaveApprovalStatus.APPROVED; -// const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED; -// const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); - -// if (!was_approved && is_approved) { -// await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); -// } else if (was_approved && !is_approved) { -// await this.leaveUtils.removeShift(email, employee_id, iso_date, type); -// } else if (was_approved && is_approved) { -// await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); -// } -// updated.push({ ...mapRowToView(row), action: "update" }); -// } -// return { action: "update", leave_requests: updated }; -// } -// } - - diff --git a/src/modules/leave-requests/services/sick-leave-requests.service.ts b/src/modules/leave-requests/services/sick-leave-requests.service.ts deleted file mode 100644 index a4554b1..0000000 --- a/src/modules/leave-requests/services/sick-leave-requests.service.ts +++ /dev/null @@ -1,98 +0,0 @@ -// import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto"; -// import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; -// import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -// import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client"; -// import { leaveRequestsSelect } from "../utils/leave-requests.select"; -// import { mapRowToView } from "../mappers/leave-requests.mapper"; -// import { PrismaService } from "src/prisma/prisma.service"; -// import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service"; -// import { roundToQuarterHour } from "src/common/utils/date-utils"; -// import { LeaveRequestsUtils } from "../utils/leave-request.util"; -// import { normalizeDates, toDateOnly } from "src/modules/shared/helpers/date-time.helpers"; -// import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; -// import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; - -// @Injectable() -// export class SickLeaveRequestsService { -// constructor( -// private readonly prisma: PrismaService, -// private readonly sickService: SickLeaveService, -// private readonly leaveUtils: LeaveRequestsUtils, -// private readonly emailResolver: EmailToIdResolver, -// private readonly typeResolver: BankCodesResolver, -// ) {} - -// async create(dto: UpsertLeaveRequestDto): Promise { -// const email = dto.email.trim(); -// const employee_id = await this.emailResolver.findIdByEmail(email); -// const bank_code = await this.typeResolver.findByType(LeaveTypes.SICK); -// if(!bank_code) throw new NotFoundException(`bank_code not found`); - -// const modifier = bank_code.modifier ?? 1; -// const dates = normalizeDates(dto.dates); -// if (!dates.length) throw new BadRequestException("Dates array must not be empty"); -// const requested_hours_per_day = dto.requested_hours ?? 8; - -// const entries = dates.map((iso) => ({ iso, date: toDateOnly(iso) })); -// const reference_date = entries.reduce( -// (latest, entry) => (entry.date > latest ? entry.date : latest), -// entries[0].date, -// ); -// const total_payable_hours = await this.sickService.calculateSickLeavePay( -// employee_id, -// reference_date, -// entries.length, -// requested_hours_per_day, -// modifier, -// ); -// let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours)); -// const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); - -// const created: LeaveRequestViewDto[] = []; - -// for (const { iso, date } of entries) { -// const existing = await this.prisma.leaveRequests.findUnique({ -// where: { -// leave_per_employee_date: { -// employee_id: employee_id, -// leave_type: LeaveTypes.SICK, -// date, -// }, -// }, -// select: { id: true }, -// }); -// if (existing) { -// throw new BadRequestException(`Sick request already exists for ${iso}`); -// } - -// const payable = Math.min(remaining_payable_hours, daily_payable_cap); -// const payable_rounded = roundToQuarterHour(Math.max(0, payable)); -// remaining_payable_hours = roundToQuarterHour( -// Math.max(0, remaining_payable_hours - payable_rounded), -// ); - -// const row = await this.prisma.leaveRequests.create({ -// data: { -// employee_id: employee_id, -// bank_code_id: bank_code.id, -// leave_type: LeaveTypes.SICK, -// comment: dto.comment ?? "", -// requested_hours: requested_hours_per_day, -// payable_hours: payable_rounded, -// approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING, -// date, -// }, -// select: leaveRequestsSelect, -// }); - -// const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); -// if (row.approval_status === LeaveApprovalStatus.APPROVED) { -// await this.leaveUtils.syncShift(email, employee_id, iso, hours,LeaveTypes.SICK, row.comment); -// } - -// created.push({ ...mapRowToView(row), action: "create" }); -// } - -// return { action: "create", leave_requests: created }; -// } -// } diff --git a/src/modules/leave-requests/services/vacation-leave-requests.service.ts b/src/modules/leave-requests/services/vacation-leave-requests.service.ts deleted file mode 100644 index 34223f5..0000000 --- a/src/modules/leave-requests/services/vacation-leave-requests.service.ts +++ /dev/null @@ -1,93 +0,0 @@ - -// import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto"; -// import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; -// import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -// import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client"; -// import { VacationService } from "src/modules/business-logics/services/vacation.service"; -// import { PrismaService } from "src/prisma/prisma.service"; -// import { mapRowToView } from "../mappers/leave-requests.mapper"; -// import { leaveRequestsSelect } from "../utils/leave-requests.select"; -// import { roundToQuarterHour } from "src/common/utils/date-utils"; -// import { LeaveRequestsUtils } from "../utils/leave-request.util"; -// import { normalizeDates, toDateOnly } from "src/modules/shared/helpers/date-time.helpers"; -// import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; -// import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; - -// @Injectable() -// export class VacationLeaveRequestsService { -// constructor( -// private readonly prisma: PrismaService, -// private readonly vacationService: VacationService, -// private readonly leaveUtils: LeaveRequestsUtils, -// private readonly emailResolver: EmailToIdResolver, -// private readonly typeResolver: BankCodesResolver, -// ) {} - -// async create(dto: UpsertLeaveRequestDto): Promise { -// const email = dto.email.trim(); -// const employee_id = await this.emailResolver.findIdByEmail(email); -// const bank_code = await this.typeResolver.findByType(LeaveTypes.VACATION); -// if(!bank_code) throw new NotFoundException(`bank_code not found`); - -// const modifier = bank_code.modifier ?? 1; -// const dates = normalizeDates(dto.dates); -// const requested_hours_per_day = dto.requested_hours ?? 8; -// if (!dates.length) throw new BadRequestException("Dates array must not be empty"); - -// const entries = dates -// .map((iso) => ({ iso, date: toDateOnly(iso) })) -// .sort((a, b) => a.date.getTime() - b.date.getTime()); -// const start_date = entries[0].date; -// const total_payable_hours = await this.vacationService.calculateVacationPay( -// employee_id, -// start_date, -// entries.length, -// modifier, -// ); -// let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours)); -// const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); - -// const created: LeaveRequestViewDto[] = []; - -// for (const { iso, date } of entries) { -// const existing = await this.prisma.leaveRequests.findUnique({ -// where: { -// leave_per_employee_date: { -// employee_id: employee_id, -// leave_type: LeaveTypes.VACATION, -// date, -// }, -// }, -// select: { id: true }, -// }); -// if (existing) throw new BadRequestException(`Vacation request already exists for ${iso}`); - -// const payable = Math.min(remaining_payable_hours, daily_payable_cap); -// const payable_rounded = roundToQuarterHour(Math.max(0, payable)); -// remaining_payable_hours = roundToQuarterHour( -// Math.max(0, remaining_payable_hours - payable_rounded), -// ); - -// const row = await this.prisma.leaveRequests.create({ -// data: { -// employee_id: employee_id, -// bank_code_id: bank_code.id, -// payable_hours: payable_rounded, -// requested_hours: requested_hours_per_day, -// leave_type: LeaveTypes.VACATION, -// comment: dto.comment ?? "", -// approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING, -// date, -// }, -// select: leaveRequestsSelect, -// }); - -// const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); -// if (row.approval_status === LeaveApprovalStatus.APPROVED) { -// await this.leaveUtils.syncShift(email, employee_id, iso, hours, LeaveTypes.VACATION, row.comment); -// } -// created.push({ ...mapRowToView(row), action: "create" }); -// } -// return { action: "create", leave_requests: created }; -// } -// } diff --git a/src/modules/leave-requests/utils/leave-request.transform.ts b/src/modules/leave-requests/utils/leave-request.transform.ts deleted file mode 100644 index 63b9936..0000000 --- a/src/modules/leave-requests/utils/leave-request.transform.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { LeaveRequestViewDto } from '../dtos/leave-request-view.dto'; -import { mapArchiveRowToView } from '../mappers/leave-requests-archive.mapper'; -import { mapRowToView } from '../mappers/leave-requests.mapper'; -import { LeaveRequestArchiveRow } from './leave-requests-archive.select'; -import { LeaveRequestRow } from './leave-requests.select'; - -/** Active (table leave_requests) : proxy to base mapper */ -export function mapRowToViewWithDays(row: LeaveRequestRow): LeaveRequestViewDto { - return mapRowToView(row); -} - -/** Archive (table leave_requests_archive) : proxy to base mapper */ -export function mapArchiveRowToViewWithDays( - row: LeaveRequestArchiveRow, - email: string, - employee_full_name?: string, -): LeaveRequestViewDto { - return mapArchiveRowToView(row, email, employee_full_name!); -} \ No newline at end of file diff --git a/src/modules/leave-requests/utils/leave-request.util.ts b/src/modules/leave-requests/utils/leave-request.util.ts deleted file mode 100644 index d01ccf2..0000000 --- a/src/modules/leave-requests/utils/leave-request.util.ts +++ /dev/null @@ -1,104 +0,0 @@ -// import { hhmmFromLocal, toDateOnly, toStringFromDate } from "src/modules/shared/helpers/date-time.helpers"; -// import { BadRequestException, Injectable } from "@nestjs/common"; -// import { PrismaService } from "src/prisma/prisma.service"; -// import { LeaveTypes } from "@prisma/client"; -// import { UpsertAction } from "src/modules/shared/types/upsert-actions.types"; - -// @Injectable() -// export class LeaveRequestsUtils { -// constructor( -// private readonly prisma: PrismaService, -// private readonly shiftsCommand: ShiftsCommandService, -// ){} - -// async syncShift( -// email: string, -// employee_id: number, -// date: string, -// hours: number, -// type: LeaveTypes, -// comment?: string, -// ) { -// if (hours <= 0) return; - -// const duration_minutes = Math.round(hours * 60); -// if (duration_minutes > 8 * 60) { -// throw new BadRequestException("Amount of hours cannot exceed 8 hours per day."); -// } -// const date_only = toDateOnly(date); -// const yyyy_mm_dd = toStringFromDate(date_only); - - - -// const start_minutes = 8 * 60; -// const end_minutes = start_minutes + duration_minutes; -// const toHHmm = (total: number) => -// `${String(Math.floor(total / 60)).padStart(2, "0")}:${String(total % 60).padStart(2, "0")}`; - -// const existing = await this.prisma.shifts.findFirst({ -// where: { -// date: date_only, -// bank_code: { type }, -// timesheet: { employee_id: employee_id }, -// }, -// include: { bank_code: true }, -// }); - -// const action: UpsertAction = existing ? 'update' : 'create'; - -// await this.shiftsCommand.upsertShifts(email, action, { -// old_shift: existing -// ? { -// date: yyyy_mm_dd, -// start_time: existing.start_time.toISOString().slice(11, 16), -// end_time: existing.end_time.toISOString().slice(11, 16), -// type: existing.bank_code?.type ?? type, -// is_remote: existing.is_remote, -// is_approved:existing.is_approved, -// comment: existing.comment ?? undefined, -// } -// : undefined, -// new_shift: { -// date: yyyy_mm_dd, -// start_time: toHHmm(start_minutes), -// end_time: toHHmm(end_minutes), -// is_remote: existing?.is_remote ?? false, -// is_approved:existing?.is_approved ?? false, -// comment: comment ?? existing?.comment ?? "", -// type: type, -// }, -// }); -// } - -// async removeShift( -// email: string, -// employee_id: number, -// iso_date: string, -// type: LeaveTypes, -// ) { -// const date_only = toDateOnly(iso_date); -// const yyyy_mm_dd = toStringFromDate(date_only); -// const existing = await this.prisma.shifts.findFirst({ -// where: { -// date: date_only, -// bank_code: { type }, -// timesheet: { employee_id: employee_id }, -// }, -// include: { bank_code: true }, -// }); -// if (!existing) return; - -// await this.shiftsCommand.upsertShifts(email, 'delete', { -// old_shift: { -// date: yyyy_mm_dd, -// start_time: hhmmFromLocal(existing.start_time), -// end_time: hhmmFromLocal(existing.end_time), -// type: existing.bank_code?.type ?? type, -// is_remote: existing.is_remote, -// is_approved:existing.is_approved, -// comment: existing.comment ?? undefined, -// }, -// }); -// } - -// } diff --git a/src/modules/leave-requests/utils/leave-requests.select.ts b/src/modules/leave-requests/utils/leave-requests.select.ts deleted file mode 100644 index e48a930..0000000 --- a/src/modules/leave-requests/utils/leave-requests.select.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Prisma } from "@prisma/client"; - -//custom prisma select to avoid employee_id exposure -export const leaveRequestsSelect = { - id: true, - bank_code_id: true, - leave_type: true, - date: true, - payable_hours: true, - requested_hours: true, - comment: true, - approval_status: true, - employee: { select: { - id: true, - user: { select: { - email: true, - first_name: true, - last_name: true, - }}, - }}, -} satisfies Prisma.LeaveRequestsSelect; - -export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>; diff --git a/src/modules/oauth-sessions/controllers/oauth-sessions.controller.ts b/src/modules/oauth-sessions/controllers/oauth-sessions.controller.ts deleted file mode 100644 index 2e5b54c..0000000 --- a/src/modules/oauth-sessions/controllers/oauth-sessions.controller.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common'; -import { OAuthSessions } from '@prisma/client'; -import { RolesAllowed } from "src/common/decorators/roles.decorators"; -import { Roles as RoleEnum } from '.prisma/client'; -import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { CreateOauthSessionDto } from '../dtos/create-oauth-session.dto'; -import { OauthSessionsService } from '../services/oauth-sessions.service'; -import { UpdateOauthSessionDto } from '../dtos/update-oauth-session.dto'; - -@ApiTags('OAuth Sessions') -@ApiBearerAuth('sessions') -//@UseGuards(JwtAuthGuard) -@Controller('oauth-sessions') -export class OauthSessionsController { - constructor(private readonly oauthSessionsService: OauthSessionsService){} - - @Post() - // @RolesAllowed(RoleEnum.ADMIN) - @ApiOperation({summary: 'Create OAuth session' }) - @ApiResponse({ status: 201, description: 'OAuth session created', type: CreateOauthSessionDto }) - @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) - create(@Body()dto: CreateOauthSessionDto): Promise { - return this.oauthSessionsService.create(dto); - } - - @Get() - //@RolesAllowed(RoleEnum.ADMIN) - @ApiOperation({summary: 'Find all OAuth session' }) - @ApiResponse({ status: 201, description: 'List of OAuth session found', type: CreateOauthSessionDto, isArray: true }) - @ApiResponse({ status: 400, description: 'List of OAuth session not found' }) - findAll(): Promise { - return this.oauthSessionsService.findAll(); - } - - @Get(':id') - //@RolesAllowed(RoleEnum.ADMIN) - @ApiOperation({summary: 'Find OAuth session' }) - @ApiResponse({ status: 201, description: 'OAuth session found', type: CreateOauthSessionDto }) - @ApiResponse({ status: 400, description: 'OAuth session not found' }) - findOne(@Param('id') id: string): Promise { - return this.oauthSessionsService.findOne(id); - } - - @Patch(':id') - //@RolesAllowed(RoleEnum.ADMIN) - @ApiOperation({summary: 'Update OAuth session' }) - @ApiResponse({ status: 201, description: 'OAuth session updated', type: CreateOauthSessionDto }) - @ApiResponse({ status: 400, description: 'OAuth session not found' }) - update(@Param('id') id: string, @Body() dto: UpdateOauthSessionDto): Promise { - return this.oauthSessionsService.update(id,dto); - } - - @Delete(':id') - //@RolesAllowed(RoleEnum.ADMIN) - @ApiOperation({summary: 'Delete OAuth session' }) - @ApiResponse({ status: 201, description: 'OAuth session deleted', type: CreateOauthSessionDto }) - @ApiResponse({ status: 400, description: 'OAuth session not found' }) - remove(@Param('id') id: string): Promise { - return this.oauthSessionsService.remove(id); - } -} diff --git a/src/modules/oauth-sessions/dtos/create-oauth-session.dto.ts b/src/modules/oauth-sessions/dtos/create-oauth-session.dto.ts deleted file mode 100644 index ef36d90..0000000 --- a/src/modules/oauth-sessions/dtos/create-oauth-session.dto.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsArray, IsDate, IsOptional, IsString, IsUUID } from "class-validator"; - -export class CreateOauthSessionDto { - @ApiProperty({ - example: 'cklwi0vb70000z2z20q6f19qk', - description: 'Unique ID of an OAuth token (auto-generated)', - }) - id: string; - - @ApiProperty({ - example: 'S7A2U8R7O6N6', - description: 'User`s unique identification number', - }) - @IsUUID() - user_id: string; - - @ApiProperty({ - example: 'app.targo.ca', - description: 'URL in which the access token is used for', - }) - @IsString() - application: string; - - @IsString() - sid: string; - - @ApiProperty({ - example: 'L5O6R4D3/O6F3#T8H4E3&R6I4N6G4S7 ...', - description: 'Access token', - }) - @IsString() - access_token: string; - - @ApiProperty({ - example: 'Th3731102h1p07Th3R1n92', - description: 'Refresh token', - }) - @IsString() - refresh_token: string; - - @ApiProperty({ - example: '25/12/3018', - description: 'Access token`s expiry date', - }) - @Type(()=> Date) - @IsDate() - access_token_expiry: Date; - - @ApiProperty({ - example: '26/02/3019', - description: 'Refresh token`s expiry date', - required: false, - }) - @Type(()=> Date) - @IsDate() - @IsOptional() - refresh_token_expiry?: Date; - - @ApiProperty({ - example: 'access tolkiens, email, etc... ', - description: 'scopes of infos linked to the access token', - required: false, - }) - @IsArray() - @IsString() - @IsOptional() - scopes?: string[]; -} diff --git a/src/modules/oauth-sessions/dtos/update-oauth-session.dto.ts b/src/modules/oauth-sessions/dtos/update-oauth-session.dto.ts deleted file mode 100644 index 697efd7..0000000 --- a/src/modules/oauth-sessions/dtos/update-oauth-session.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from "@nestjs/swagger"; -import { CreateOauthSessionDto } from "./create-oauth-session.dto"; - -export class UpdateOauthSessionDto extends PartialType(CreateOauthSessionDto) {} diff --git a/src/modules/oauth-sessions/oauth-sessions.module.ts b/src/modules/oauth-sessions/oauth-sessions.module.ts deleted file mode 100644 index e68a481..0000000 --- a/src/modules/oauth-sessions/oauth-sessions.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { PrismaService } from 'src/prisma/prisma.service'; -import { OauthSessionsController } from './controllers/oauth-sessions.controller'; -import { OauthSessionsService } from './services/oauth-sessions.service'; - -@Module({ - controllers: [OauthSessionsController], - providers: [OauthSessionsService, PrismaService] -}) -export class OauthSessionsModule {} diff --git a/src/modules/oauth-sessions/services/oauth-sessions.service.ts b/src/modules/oauth-sessions/services/oauth-sessions.service.ts deleted file mode 100644 index c307682..0000000 --- a/src/modules/oauth-sessions/services/oauth-sessions.service.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { PrismaService } from 'src/prisma/prisma.service'; -import { CreateOauthSessionDto } from '../dtos/create-oauth-session.dto'; -import { OAuthSessions } from '@prisma/client'; -import { UpdateOauthSessionDto } from '../dtos/update-oauth-session.dto'; - -@Injectable() -export class OauthSessionsService { - constructor(private readonly prisma: PrismaService) {} - - async create(dto: CreateOauthSessionDto): Promise { - const { - user_id, - application, - access_token, - refresh_token, - sid, - access_token_expiry, - refresh_token_expiry, - scopes, - } = dto; - - return this.prisma.oAuthSessions.create({ - data: { - user_id, - application, - access_token, - refresh_token, - sid, - access_token_expiry, - refresh_token_expiry, - scopes, - }, - include: { user: true }, - }); - } - - findAll(): Promise { - return this.prisma.oAuthSessions.findMany({ - include: { user: true }, - }); - } - - async findOne(id: string): Promise { - const token = await this.prisma.oAuthSessions.findUnique({ - where: { id }, - include: { user: true }, - }); - if(!token) { - throw new NotFoundException(`token #${ id } not found`); - } - return token; - } - - async update(id: string, dto: UpdateOauthSessionDto): Promise { - await this.findOne(id); - const { - user_id, - application, - access_token, - refresh_token, - access_token_expiry, - refresh_token_expiry, - scopes, - } = dto; - - return this.prisma.oAuthSessions.update({ - where: { id }, - data: { - ...(user_id !== undefined && { user_id }), - ...(application !== undefined && { application }), - ...(access_token !== undefined && { access_token }), - ...(refresh_token !== undefined && { refresh_token }), - ...(access_token_expiry !== undefined && { access_token_expiry }), - ...(refresh_token_expiry !== undefined && { refresh_token_expiry }), - ...(scopes !== undefined && { scopes }), - }, - include: { user: true }, - }); - } - - async remove(id: string): Promise { - await this.findOne(id); - return this.prisma.oAuthSessions.delete({ where: { id }}); - } -} diff --git a/src/modules/pay-periods/controllers/pay-periods.controller.ts b/src/modules/pay-periods/controllers/pay-periods.controller.ts deleted file mode 100644 index 61026c6..0000000 --- a/src/modules/pay-periods/controllers/pay-periods.controller.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Body, Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query } from "@nestjs/common"; -import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger"; -import { PayPeriodDto } from "../dtos/pay-period.dto"; -import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto"; -import { PayPeriodsQueryService } from "../services/pay-periods-query.service"; -import { RolesAllowed } from "src/common/decorators/roles.decorators"; -import { Roles as RoleEnum } from '.prisma/client'; -// import { PayPeriodsCommandService } from "../services/pay-periods-command.service"; -import { PayPeriodBundleDto } from "../dtos/bundle-pay-period.dto"; -import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto"; - -@ApiTags('pay-periods') -@Controller('pay-periods') -export class PayPeriodsController { - - constructor( - private readonly queryService: PayPeriodsQueryService, - // private readonly commandService: PayPeriodsCommandService, - ) {} - - @Get('current-and-all') - @ApiOperation({summary: 'Return current pay period and the full list'}) - @ApiQuery({name: 'date', required:false, example: '2025-08-11', description:'Override for resolving the current period'}) - @ApiResponse({status: 200, description:'Find current and all pay periods', type: PayPeriodBundleDto}) - async getCurrentAndAll(@Query('date') date?: string): Promise { - const [current, periods] = await Promise.all([ - this.queryService.findCurrent(date), - this.queryService.findAll(), - ]); - return { current, periods }; - } - - @Get("date/:date") - @ApiOperation({ summary: "Resolve a period by a date within it" }) - @ApiResponse({ status: 200, description: "Pay period found for the selected date", type: PayPeriodDto }) - @ApiNotFoundResponse({ description: "Pay period not found for the selected date" }) - async findByDate(@Param("date") date: string) { - return this.queryService.findByDate(date); - } - - @Get(":year/:periodNumber") - @ApiOperation({ summary: "Find pay period by year and period number" }) - @ApiParam({ name: "year", type: Number, example: 2024 }) - @ApiParam({ name: "periodNumber", type: Number, example: 1, description: "1..26" }) - @ApiResponse({ status: 200, description: "Pay period found", type: PayPeriodDto }) - @ApiNotFoundResponse({ description: "Pay period not found" }) - async findOneByYear( - @Param("year", ParseIntPipe) year: number, - @Param("periodNumber", ParseIntPipe) period_no: number, - ) { - return this.queryService.findOneByYearPeriod(year, period_no); - } - - // @Patch("crew/bulk-approval") - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) - // @ApiOperation({ summary: "Approve all selected timesheets in the period" }) - // @ApiResponse({ status: 200, description: "Pay period approved" }) - // async bulkApproval(@Body() dto: BulkCrewApprovalDto) { - // return this.commandService.bulkApproveCrew(dto); - // } - - @Get(':year/:periodNumber/:email') - //@RolesAllowed(RoleEnum.SUPERVISOR) - @ApiOperation({ summary: 'Supervisor crew overview for a given pay period' }) - @ApiParam({ name: 'year', type: Number, example: 2024 }) - @ApiParam({ name: 'periodNumber', type: Number, example: 1, description: '1..26' }) - @ApiQuery({ name: 'includeSubtree', required: false, type: Boolean, example: false, description: 'Include indirect reports' }) - @ApiResponse({ status: 200, description: 'Crew overview', type: PayPeriodOverviewDto }) - @ApiNotFoundResponse({ description: 'Pay period not found' }) - async getCrewOverview( - @Param('year', ParseIntPipe) year: number, - @Param('periodNumber', ParseIntPipe) period_no: number, - @Param('email') email: string, - @Query('includeSubtree', new ParseBoolPipe({ optional: true })) include_subtree = false, - ): Promise { - return this.queryService.getCrewOverview(year, period_no, email, include_subtree); - } - - @Get('overview/:year/:periodNumber') - @ApiOperation({ summary: 'Detailed view of a pay period by year + number' }) - @ApiParam({ name: 'year', type: Number, example: 2024 }) - @ApiParam({ name: 'periodNumber', type: Number, example: 1, description: '1..26' }) - @ApiResponse({ status: 200, description: 'Pay period overview found', type: PayPeriodOverviewDto }) - @ApiNotFoundResponse({ description: 'Pay period not found' }) - async getOverviewByYear( - @Param('year', ParseIntPipe) year: number, - @Param('periodNumber', ParseIntPipe) period_no: number, - ): Promise { - return this.queryService.getOverviewByYearPeriod(year, period_no); - } - - - //_____________________________________________________________________________________________ - // Deprecated or unused methods - //_____________________________________________________________________________________________ - - // @Get() - // @ApiOperation({ summary: 'Find all pay period' }) - // @ApiResponse({status: 200,description: 'List of pay period found', type: PayPeriodDto, isArray: true }) - // async findAll(): Promise { - // return this.queryService.findAll(); - // } -} diff --git a/src/modules/pay-periods/dtos/bundle-pay-period.dto.ts b/src/modules/pay-periods/dtos/bundle-pay-period.dto.ts deleted file mode 100644 index 9c5a61f..0000000 --- a/src/modules/pay-periods/dtos/bundle-pay-period.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { PayPeriodDto } from "./pay-period.dto"; - -export class PayPeriodBundleDto { - - @ApiProperty({ type: PayPeriodDto, description: 'Current pay period (resolved from date)' }) - current: PayPeriodDto; - - @ApiProperty({ type: [PayPeriodDto], description: 'All pay periods' }) - periods: PayPeriodDto[]; -} \ No newline at end of file diff --git a/src/modules/pay-periods/dtos/overview-employee-period.dto.ts b/src/modules/pay-periods/dtos/overview-employee-period.dto.ts deleted file mode 100644 index 1ea6937..0000000 --- a/src/modules/pay-periods/dtos/overview-employee-period.dto.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class EmployeePeriodOverviewDto { - // @ApiProperty({ - // example: 42, - // description: "Employees.id (clé primaire num.)", - // }) - // @Allow() - // @IsOptional() - // employee_id: number; - - - email: string; - - @ApiProperty({ - example: 'Alex Dupont', - description: 'Nom complet de lemployé', - }) - employee_name: string; - - @ApiProperty({ example: 40, description: 'pay-period`s regular hours' }) - regular_hours: number; - - @ApiProperty({ example: 0, description: 'pay-period`s other hours' }) - other_hours: { - evening_hours: number; - - emergency_hours: number; - - overtime_hours: number; - - sick_hours: number; - - holiday_hours: number; - - vacation_hours: number; - }; - - total_hours: number; - - @ApiProperty({ example: 420.69, description: 'pay-period`s total expenses ($)' }) - expenses: number; - - @ApiProperty({ example: 40, description: 'pay-period total mileages (km)' }) - mileage: number; - - @ApiProperty({ - example: true, - description: 'Tous les timesheets de la période sont approuvés pour cet employé', - }) - is_approved: boolean; - - is_remote: boolean; -} diff --git a/src/modules/pay-periods/dtos/overview-pay-period.dto.ts b/src/modules/pay-periods/dtos/overview-pay-period.dto.ts deleted file mode 100644 index 041fba3..0000000 --- a/src/modules/pay-periods/dtos/overview-pay-period.dto.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { EmployeePeriodOverviewDto } from './overview-employee-period.dto'; - -export class PayPeriodOverviewDto { - @ApiProperty({ example: 1, description: 'Period number (1–26)' }) - pay_period_no: number; - - @ApiProperty({ example: 2023, description: 'Calendar year of the period' }) - pay_year: number; - - @ApiProperty({ - example: '2023-12-17', - type: String, - format: 'date', - description: "Period start date (YYYY-MM-DD)", - }) - period_start: string; - - @ApiProperty({ - example: '2023-12-30', - type: String, - format: 'date', - description: "Period end date (YYYY-MM-DD)", - }) - period_end: string; - - @ApiProperty({ - example: '2023-12-30', - type: String, - format: 'date', - description: "Period pay day(YYYY-MM-DD)", - }) - payday: string; - - @ApiProperty({ - example: '2023-12-17 → 2023-12-30', - description: 'Human-readable label', - }) - label: string; - - @ApiProperty({ - type: [EmployeePeriodOverviewDto], - description: 'Per-employee overview for the period', - }) - employees_overview: EmployeePeriodOverviewDto[]; -} diff --git a/src/modules/pay-periods/dtos/pay-period.dto.ts b/src/modules/pay-periods/dtos/pay-period.dto.ts deleted file mode 100644 index 4f7989b..0000000 --- a/src/modules/pay-periods/dtos/pay-period.dto.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; - -export class PayPeriodDto { - @ApiProperty({ example: 1, - description: 'numéro cyclique de la période entre 1 et 26' }) - pay_period_no: number; - - @ApiProperty({ example: '2023-12-17', - type: String, format: 'date' }) - period_start: string; - - @ApiProperty({ example: '2023-12-30', - type: String, format: 'date' }) - period_end: string; - - @ApiProperty({ example: '2023-01-04', - type: String, format: 'date' }) - payday: string; - - @ApiProperty({ example: 2023 }) - pay_year: number; - - @ApiProperty({ example: '2023-12-17 → 2023-12-30' }) - label: string; -} \ No newline at end of file diff --git a/src/modules/pay-periods/pay-periods.module.ts b/src/modules/pay-periods/pay-periods.module.ts deleted file mode 100644 index c614179..0000000 --- a/src/modules/pay-periods/pay-periods.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { PrismaModule } from "src/prisma/prisma.module"; -import { PayPeriodsController } from "./controllers/pay-periods.controller"; -import { Module } from "@nestjs/common"; -import { PayPeriodsQueryService } from "./services/pay-periods-query.service"; -import { TimesheetsModule } from "../timesheets/timesheets.module"; -import { SharedModule } from "../shared/shared.module"; -import { PrismaService } from "src/prisma/prisma.service"; -import { BusinessLogicsModule } from "../business-logics/business-logics.module"; - -@Module({ - imports: [PrismaModule, TimesheetsModule, SharedModule, BusinessLogicsModule], - providers: [ - PayPeriodsQueryService, - PrismaService, - ], - controllers: [PayPeriodsController], - exports: [ PayPeriodsQueryService ], -}) - -export class PayperiodsModule {} \ No newline at end of file diff --git a/src/modules/pay-periods/services/pay-periods-command.service.ts b/src/modules/pay-periods/services/pay-periods-command.service.ts deleted file mode 100644 index 960e1ef..0000000 --- a/src/modules/pay-periods/services/pay-periods-command.service.ts +++ /dev/null @@ -1,71 +0,0 @@ -// import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; -// import { PrismaService } from "src/prisma/prisma.service"; -// import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto"; -// import { PayPeriodsQueryService } from "./pay-periods-query.service"; -// import { TimesheetApprovalService } from "src/modules/timesheets/services/timesheet-approval.service"; - -// @Injectable() -// export class PayPeriodsCommandService { -// constructor( -// private readonly prisma: PrismaService, -// private readonly timesheets_approval: TimesheetApprovalService, -// private readonly query: PayPeriodsQueryService, -// ) {} - -// //function to approve pay-periods according to selected crew members -// async bulkApproveCrew(dto:BulkCrewApprovalDto): Promise<{updated: number}> { -// const { supervisor_email, include_subtree, items } = dto; -// if(!items?.length) throw new BadRequestException('no items to process'); - -// //fetch and validate supervisor status -// const supervisor = await this.query.getSupervisor(supervisor_email); -// if(!supervisor) throw new NotFoundException('No employee record linked to current user'); -// if(!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor'); - -// //fetches emails of crew members linked to supervisor -// const crew_emails = await this.query.resolveCrewEmails(supervisor.id, include_subtree); - - -// for(const item of items) { -// if(!crew_emails.has(item.employee_email)) { -// throw new ForbiddenException(`Employee ${item.employee_email} not in supervisor crew`); -// } -// } - -// const period_cache = new Map(); -// const getPeriod = async (y:number, no: number) => { -// const key = `${y}-${no}`; -// if(!period_cache.has(key)) return period_cache.get(key)!; -// const period = await this.query.getPeriodWindow(y,no); -// if(!period) throw new NotFoundException(`Pay period ${y}-${no} not found`); -// period_cache.set(key, period); -// return period; -// }; - -// let updated = 0; - -// await this.prisma.$transaction(async (transaction) => { -// for(const item of items) { -// const { period_start, period_end } = await getPeriod(item.pay_year, item.period_no); - -// const t_sheets = await transaction.timesheets.findMany({ -// where: { -// employee: { user: { email: item.employee_email } }, -// OR: [ -// {shift : { some: { date: { gte: period_start, lte: period_end } } } }, -// {expense: { some: { date: { gte: period_start, lte: period_end } } } }, -// ], -// }, -// select: { id: true }, -// }); - -// for(const { id } of t_sheets) { -// await this.timesheets_approval.updateApprovalWithTransaction(transaction, id, item.approve); -// updated++; -// } - -// } -// }); -// return {updated}; -// } -// } \ No newline at end of file diff --git a/src/modules/pay-periods/utils/pay-year.util.ts b/src/modules/pay-periods/utils/pay-year.util.ts deleted file mode 100644 index dd9a512..0000000 --- a/src/modules/pay-periods/utils/pay-year.util.ts +++ /dev/null @@ -1,41 +0,0 @@ -export const ANCHOR_ISO = '2023-12-17'; // ancre date -const PERIOD_DAYS = 14; -const PERIODS_PER_YEAR = 26; -const MS_PER_DAY = 86_400_000; - -const toUTCDate = (iso: string | Date) => { - const d = typeof iso === 'string' ? new Date(iso + 'T00:00:00.000Z') : iso; - return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); -}; -export const toDateString = (d: Date) => d.toISOString().slice(0, 10); - -export function payYearOfDate(date: string | Date, anchorISO = ANCHOR_ISO): number { - const ANCHOR = toUTCDate(anchorISO); - const d = toUTCDate(date); - const days = Math.floor((+d - +ANCHOR) / MS_PER_DAY); - const cycles = Math.floor(days / (PERIODS_PER_YEAR * PERIOD_DAYS)); - return ANCHOR.getUTCFullYear() + 1 + cycles; -} -//compute labels for periods -export function computePeriod(pay_year: number, period_no: number, anchorISO = ANCHOR_ISO) { - const ANCHOR = toUTCDate(anchorISO); - const cycles = pay_year - (ANCHOR.getUTCFullYear() + 1); - const offsetPeriods = cycles * PERIODS_PER_YEAR + (period_no - 1); - const start = new Date(+ANCHOR + offsetPeriods * PERIOD_DAYS * MS_PER_DAY); - const end = new Date(+start + (PERIOD_DAYS - 1) * MS_PER_DAY); - const pay = new Date(end.getTime() + 6 * MS_PER_DAY); - return { - period_no: period_no, - pay_year: pay_year, - payday: toDateString(pay), - period_start: toDateString(start), - period_end: toDateString(end), - label: `${toDateString(start)}.${toDateString(end)}`, - start, end, - }; -} - -//list of all 26 periods for a full year -export function listPayYear(pay_year: number, anchorISO = ANCHOR_ISO) { - return Array.from({ length: PERIODS_PER_YEAR }, (_, i) => computePeriod(pay_year, i + 1, anchorISO)); -} diff --git a/src/modules/schedule-presets/controller/schedule-presets.controller.ts b/src/modules/schedule-presets/controller/schedule-presets.controller.ts deleted file mode 100644 index b031c67..0000000 --- a/src/modules/schedule-presets/controller/schedule-presets.controller.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { BadRequestException, Body, Controller, Get, NotFoundException, Param, Post, Put, Query } from "@nestjs/common"; -import { SchedulePresetsDto } from "../dtos/create-schedule-presets.dto"; -import { SchedulePresetsCommandService } from "../services/schedule-presets-command.service"; -import { UpsertAction } from "src/modules/shared/types/upsert-actions.types"; -import { SchedulePresetsQueryService } from "../services/schedule-presets-query.service"; - -@Controller('schedule-presets') -export class SchedulePresetsController { - constructor( - private readonly commandService: SchedulePresetsCommandService, - private readonly queryService: SchedulePresetsQueryService, - ){} - - //used to create, update or delete a schedule preset - @Put(':email') - async upsert( - @Param('email') email: string, - @Query('action') action: UpsertAction, - @Body() dto: SchedulePresetsDto, - ) { - const actions: UpsertAction[] = ['create','update','delete']; - if(!actions) throw new NotFoundException(`No action found for ${actions}`) - return this.commandService.upsertSchedulePreset(email, action, dto); - } - - //used to show the list of available schedule presets - @Get(':email') - async findListByEmail( - @Param('email') email: string, - ) { - return this.queryService.findSchedulePresetsByEmail(email); - } - //used to apply a preset to a timesheet - @Post('/apply-presets/:email') - async applyPresets( - @Param('email') email: string, - @Query('preset') preset_name: string, - @Query('start') start_date: string, - ) { - if(!preset_name?.trim()) throw new BadRequestException('Query "preset" is required'); - if(!start_date?.trim()) throw new BadRequestException('Query "start" is required YYYY-MM-DD'); - return this.applyPresets(email, preset_name, start_date); - } -} \ No newline at end of file diff --git a/src/modules/schedule-presets/dtos/create-schedule-presets.dto.ts b/src/modules/schedule-presets/dtos/create-schedule-presets.dto.ts deleted file mode 100644 index 7bd822f..0000000 --- a/src/modules/schedule-presets/dtos/create-schedule-presets.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ArrayMinSize, IsArray, IsBoolean, IsEmail, IsOptional, IsString } from "class-validator"; -import { SchedulePresetShiftsDto } from "./create-schedule-preset-shifts.dto"; - -export class SchedulePresetsDto { - @IsString() - name!: string; - - @IsBoolean() - @IsOptional() - is_default: boolean; - - @IsArray() - @ArrayMinSize(1) - preset_shifts: SchedulePresetShiftsDto[]; -} \ No newline at end of file diff --git a/src/modules/schedule-presets/schedule-presets.module.ts b/src/modules/schedule-presets/schedule-presets.module.ts deleted file mode 100644 index 2e25a6d..0000000 --- a/src/modules/schedule-presets/schedule-presets.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Module } from "@nestjs/common"; -import { SchedulePresetsCommandService } from "./services/schedule-presets-command.service"; -import { SchedulePresetsQueryService } from "./services/schedule-presets-query.service"; -import { SchedulePresetsController } from "./controller/schedule-presets.controller"; -import { PrismaService } from "src/prisma/prisma.service"; -import { SchedulePresetsApplyService } from "./services/schedule-presets-apply.service"; -import { SharedModule } from "../shared/shared.module"; - -@Module({ - imports: [SharedModule], - controllers: [SchedulePresetsController], - providers: [ - PrismaService, - SchedulePresetsCommandService, - SchedulePresetsQueryService, - SchedulePresetsApplyService, - ], - exports:[ - SchedulePresetsCommandService, - SchedulePresetsQueryService, - SchedulePresetsApplyService, - ], -}) export class SchedulePresetsModule {} \ No newline at end of file diff --git a/src/modules/schedule-presets/services/schedule-presets-command.service.ts b/src/modules/schedule-presets/services/schedule-presets-command.service.ts deleted file mode 100644 index 0c2a8bb..0000000 --- a/src/modules/schedule-presets/services/schedule-presets-command.service.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { BadRequestException, ConflictException, Injectable, NotFoundException } from "@nestjs/common"; -import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; -import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; -import { UpsertAction } from "src/modules/shared/types/upsert-actions.types"; -import { PrismaService } from "src/prisma/prisma.service"; -import { SchedulePresetsDto } from "../dtos/create-schedule-presets.dto"; -import { Prisma, Weekday } from "@prisma/client"; - -@Injectable() -export class SchedulePresetsCommandService { - constructor( - private readonly prisma: PrismaService, - private readonly emailResolver: EmailToIdResolver, - private readonly typeResolver : BankCodesResolver, - ){} - - //_________________________________________________________________ - // MASTER CRUD FUNCTION - //_________________________________________________________________ - async upsertSchedulePreset( - email: string, - action: UpsertAction, - dto: SchedulePresetsDto, - ): Promise<{ - action: UpsertAction; - preset_id?: number; - total_items?: number; - }>{ - if(!dto.name?.trim()) throw new BadRequestException(`A Name is required`); - - //resolve employee_id using email - const employee_id = await this.emailResolver.findIdByEmail(email); - if(!employee_id) throw new NotFoundException(`employee with email: ${email} not found`); - - //DELETE - if(action === 'delete') { - return this.deletePreset(employee_id, dto.name); - } - - if(!Array.isArray(dto.preset_shifts) || dto.preset_shifts.length === 0) { - throw new BadRequestException(`Empty array, no detected shifts`); - } - const shifts_data = await this.resolveAndBuildPresetShifts(dto); - - //CREATE AND UPDATE - if(action === 'create') { - return this.createPreset(employee_id, dto, shifts_data); - } else if (action === 'update') { - return this.updatePreset(employee_id, dto, shifts_data); - } - throw new BadRequestException(`Unknown action: ${ action }`); - } - - //_________________________________________________________________ - // CREATE - //_________________________________________________________________ - private async createPreset( - employee_id: number, - dto: SchedulePresetsDto, - shifts_data: Prisma.SchedulePresetShiftsCreateWithoutPresetInput[], - ): Promise<{ - action: UpsertAction; - preset_id: number; - total_items: number; - }> { - try { - const result = await this.prisma.$transaction(async (tx)=> { - if(dto.is_default) { - await tx.schedulePresets.updateMany({ - where: { employee_id, is_default: true }, - data: { is_default: false }, - }); - } - const created = await tx.schedulePresets.create({ - data: { - employee_id, - name: dto.name, - is_default: !!dto.is_default, - shifts: { create: shifts_data}, - }, - include: { shifts: true }, - }); - return created; - }); - return { action: 'create', preset_id: result.id, total_items: result.shifts.length }; - } catch (error: unknown) { - if(error instanceof Prisma.PrismaClientKnownRequestError){ - if(error?.code === 'P2002') { - throw new ConflictException(`The name ${dto.name} is already used for another schedule preset`); - } - if (error.code === 'P2003' || error.code === 'P2011') { - throw new ConflictException('Invalid constraint on preset shifts'); - } - } - throw error; - } - } - - //_________________________________________________________________ - // UPDATE - //_________________________________________________________________ - private async updatePreset( - employee_id: number, - dto: SchedulePresetsDto, - shifts_data: Prisma.SchedulePresetShiftsCreateWithoutPresetInput[], - ): Promise<{ - action: UpsertAction; - preset_id?: number; - total_items?: number; - }> { - const existing = await this.prisma.schedulePresets.findFirst({ - where: { employee_id, name: dto.name }, - select: { id:true, is_default: true }, - }); - if(!existing) throw new NotFoundException(`Preset "${dto.name}" not found`); - - try { - const result = await this.prisma.$transaction(async (tx) => { - if(typeof dto.is_default === 'boolean'){ - if(dto.is_default) { - await tx.schedulePresets.updateMany({ - where: { employee_id, is_default: true, NOT: { id: existing.id } }, - data: { is_default: false }, - }); - } - await tx.schedulePresets.update({ - where: { id: existing.id }, - data: { is_default: dto.is_default }, - }); - } - - await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } }); - - const create_many_data: Prisma.SchedulePresetShiftsCreateManyInput[] = - shifts_data.map((shift)=> { - if(!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !=='number'){ - throw new NotFoundException(`Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`); - } - const bank_code_id = shift.bank_code.connect.id; - return { - preset_id: existing.id, - week_day: shift.week_day, - sort_order: shift.sort_order, - start_time: shift.start_time, - end_time: shift.end_time, - is_remote: shift.is_remote ?? false, - bank_code_id: bank_code_id, - }; - }); - await tx.schedulePresetShifts.createMany({data: create_many_data}); - - const count = await tx.schedulePresetShifts.count({ where: { preset_id: existing.id } }); - return { id: existing.id, total: count }; - }); - return { action: 'update', preset_id: result.id, total_items: result.total }; - } catch (error: unknown){ - if(error instanceof Prisma.PrismaClientKnownRequestError){ - if(error?.code === 'P2003' || error?.code === 'P2011') { - throw new ConflictException(`Invalid constraint on preset shifts`); - } - } - throw error; - } - } - - //_________________________________________________________________ - // DELETE - //_________________________________________________________________ - private async deletePreset( - employee_id: number, - name: string, - ): Promise<{ - action: UpsertAction; - preset_id?: number; - total_items?: number; - }> { - const existing = await this.prisma.schedulePresets.findFirst({ - where: { employee_id, name }, - select: { id: true }, - }); - if(!existing) throw new NotFoundException(`Preset "${name}" not found`); - await this.prisma.$transaction(async (tx) => { - await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } }); - await tx.schedulePresets.delete({where: { id: existing.id } }); - }); - return { action: 'delete', preset_id: existing.id, total_items: 0 }; - } - - //PRIVATE HELPER - //resolve bank_code_id using type and convert hours to TIME and valid shifts end/start - private async resolveAndBuildPresetShifts( - dto: SchedulePresetsDto - ): Promise{ - - if(!dto.preset_shifts?.length) throw new NotFoundException(`Empty or preset shifts not found`); - - const types = Array.from(new Set(dto.preset_shifts.map((shift)=> shift.type))); - const bank_code_set = new Map(); - - for (const type of types) { - const { id } = await this.typeResolver.findByType(type); - bank_code_set.set(type, id) - } - const toTime = (hhmm: string) => new Date(`1970-01-01T${hhmm}:00.000Z`); - - const pair_set = new Set(); - for (const shift of dto.preset_shifts) { - const key = `${shift.week_day}:${shift.sort_order}`; - if (pair_set.has(key)) { - throw new ConflictException(`Duplicate shift for day/order (${shift.week_day}, ${shift.sort_order})`); - } - pair_set.add(key); - } - - const items: Prisma.SchedulePresetShiftsCreateWithoutPresetInput[] = dto.preset_shifts.map((shift)=> { - const bank_code_id = bank_code_set.get(shift.type); - if(!bank_code_id) throw new NotFoundException(`Bank code not found for type ${shift.type}`); - if (!shift.start_time || !shift.end_time) { - throw new BadRequestException(`start_time and end_time are required for (${shift.week_day}, ${shift.sort_order})`); - } - const start = toTime(shift.start_time); - const end = toTime(shift.end_time); - if(end.getTime() <= start.getTime()) { - throw new ConflictException(`end_time must be > start_time ( day: ${shift.week_day}, order: ${shift.sort_order})`); - } - - return { - week_day: shift.week_day as Weekday, - sort_order: shift.sort_order, - bank_code: { connect: { id: bank_code_id} }, - start_time: start, - end_time: end, - is_remote: !!shift.is_remote, - }; - }); - return items; - } -} diff --git a/src/modules/schedule-presets/types/schedule-presets.types.ts b/src/modules/schedule-presets/types/schedule-presets.types.ts deleted file mode 100644 index ea2a3cd..0000000 --- a/src/modules/schedule-presets/types/schedule-presets.types.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type ShiftResponse = { - week_day: string; - sort_order: number; - start_time: string; - end_time: string; - is_remote: boolean; - type: string; -}; - -export type PresetResponse = { - id: number; - name: string; - is_default: boolean; - shifts: ShiftResponse[]; -} - -export type ApplyResult = { - timesheet_id: number; - created: number; - skipped: number; -} \ No newline at end of file diff --git a/src/modules/shared/constants/date-time.constant.ts b/src/modules/shared/constants/date-time.constant.ts deleted file mode 100644 index 9cf4f96..0000000 --- a/src/modules/shared/constants/date-time.constant.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MS_PER_DAY = 86_400_000; -export const MS_PER_HOUR = 3_600_000; \ No newline at end of file diff --git a/src/modules/shared/constants/regex.constant.ts b/src/modules/shared/constants/regex.constant.ts deleted file mode 100644 index 30f77c1..0000000 --- a/src/modules/shared/constants/regex.constant.ts +++ /dev/null @@ -1,2 +0,0 @@ -const HH_MM_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/; -const DATE_ISO_FORMAT = /^\d{4}-\d{2}-\d{2}$/; \ No newline at end of file diff --git a/src/modules/shared/constants/utils.constant.ts b/src/modules/shared/constants/utils.constant.ts deleted file mode 100644 index 271bbbf..0000000 --- a/src/modules/shared/constants/utils.constant.ts +++ /dev/null @@ -1 +0,0 @@ -export const COMMENT_MAX_LENGTH = 280; \ No newline at end of file diff --git a/src/modules/shared/helpers/date-time.helpers.ts b/src/modules/shared/helpers/date-time.helpers.ts deleted file mode 100644 index 6716321..0000000 --- a/src/modules/shared/helpers/date-time.helpers.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { BadRequestException } from "@nestjs/common"; - -export const hhmmFromLocal = (d: Date) => - `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; - -export const toDateOnly = (s: string): Date => { - if (/^\d{4}-\d{2}-\d{2}$/.test(s)) { - const y = Number(s.slice(0,4)); - const m = Number(s.slice(5,7)) - 1; - const d = Number(s.slice(8,10)); - return new Date(y, m, d, 0, 0, 0, 0); - } - const dt = new Date(s); - if (Number.isNaN(dt.getTime())) throw new BadRequestException(`Invalid date: ${s}`); - return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), 0,0,0,0); -}; - -export const toStringFromDate = (d: Date) => - `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; - - -export const toISOtoDateOnly = (iso: string): Date => { - const date = new Date(iso); - if (Number.isNaN(date.getTime())) { - throw new BadRequestException(`Invalid date: ${iso}`); - } - date.setHours(0, 0, 0, 0); - return date; -}; - -export const toISODateKey = (date: Date): string => date.toISOString().slice(0, 10); - -export const normalizeDates = (dates: string[]): string[] => - Array.from(new Set(dates.map((iso) => toISODateKey(toISOtoDateOnly(iso))))); \ No newline at end of file diff --git a/src/modules/shared/selects/expenses.select.ts b/src/modules/shared/selects/expenses.select.ts deleted file mode 100644 index 540d98f..0000000 --- a/src/modules/shared/selects/expenses.select.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const EXPENSE_SELECT = { - date: true, - amount: true, - mileage: true, - comment: true, - is_approved: true, - supervisor_comment: true, - bank_code: { select: { type: true } }, -} as const; - -export const EXPENSE_ASC_ORDER = { date: 'asc' as const }; \ No newline at end of file diff --git a/src/modules/shared/selects/pay-periods.select.ts b/src/modules/shared/selects/pay-periods.select.ts deleted file mode 100644 index a76f09b..0000000 --- a/src/modules/shared/selects/pay-periods.select.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const PAY_PERIOD_SELECT = { - period_start: true, - period_end: true, -} as const; \ No newline at end of file diff --git a/src/modules/shared/selects/shifts.select.ts b/src/modules/shared/selects/shifts.select.ts deleted file mode 100644 index 8c738e1..0000000 --- a/src/modules/shared/selects/shifts.select.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const SHIFT_SELECT = { - date: true, - start_time: true, - end_time: true, - comment: true, - is_approved: true, - is_remote: true, - bank_code: {select: { type: true } }, -} as const; - -export const SHIFT_ASC_ORDER = [{date: 'asc' as const}, {start_time: 'asc' as const}]; - diff --git a/src/modules/shared/shared.module.ts b/src/modules/shared/shared.module.ts deleted file mode 100644 index 8d4aa95..0000000 --- a/src/modules/shared/shared.module.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Module } from "@nestjs/common"; -import { EmailToIdResolver } from "./utils/resolve-email-id.utils"; -import { EmployeeTimesheetResolver } from "./utils/resolve-timesheet.utils"; -import { FullNameResolver } from "./utils/resolve-full-name.utils"; -import { BankCodesResolver } from "./utils/resolve-bank-type-id.utils"; -import { PrismaModule } from "src/prisma/prisma.module"; - -@Module({ -imports: [PrismaModule], -providers: [ - FullNameResolver, - EmailToIdResolver, - BankCodesResolver, - EmployeeTimesheetResolver, -], -exports: [ - FullNameResolver, - EmailToIdResolver, - BankCodesResolver, - EmployeeTimesheetResolver, -], -}) export class SharedModule {} \ No newline at end of file diff --git a/src/modules/shared/types/upsert-actions.types.ts b/src/modules/shared/types/upsert-actions.types.ts deleted file mode 100644 index 9342d75..0000000 --- a/src/modules/shared/types/upsert-actions.types.ts +++ /dev/null @@ -1 +0,0 @@ -export type UpsertAction = 'create' | 'update' | 'delete'; \ No newline at end of file diff --git a/src/modules/shared/utils/resolve-bank-type-id.utils.ts b/src/modules/shared/utils/resolve-bank-type-id.utils.ts deleted file mode 100644 index 039543f..0000000 --- a/src/modules/shared/utils/resolve-bank-type-id.utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; -import { Prisma, PrismaClient } from "@prisma/client"; -import { PrismaService } from "src/prisma/prisma.service"; - -type Tx = Prisma.TransactionClient | PrismaClient; - -@Injectable() -export class BankCodesResolver { - constructor(private readonly prisma: PrismaService) {} - - //find id and modifier by type - readonly findByType = async ( type: string, client?: Tx - ): Promise<{id:number; modifier: number }> => { - const db = client ?? this.prisma; - const bank = await db.bankCodes.findFirst({ - where: { type }, - select: { id: true, modifier: true }, - }); - - if(!bank) throw new NotFoundException(`Unknown bank code type: ${type}`); - return { id: bank.id, modifier: bank.modifier }; - }; -} \ No newline at end of file diff --git a/src/modules/shifts/controllers/shift.controller.ts b/src/modules/shifts/controllers/shift.controller.ts deleted file mode 100644 index bffa9cb..0000000 --- a/src/modules/shifts/controllers/shift.controller.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { BadRequestException, Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, Query } from "@nestjs/common"; -import { CreateResult, ShiftsUpsertService, UpdateResult } from "../services/shifts-upsert.service"; -import { updateShiftDto } from "../dtos/update-shift.dto"; -import { ShiftDto } from "../dtos/shift.dto"; -import { ShiftsGetService } from "../services/shifts-get.service"; - - -@Controller('shift') -export class ShiftController { - constructor( - private readonly upsert_service: ShiftsUpsertService, - private readonly get_service: ShiftsGetService - ){} - - @Get() - async getShiftsByIds( - @Query("shift_ids") shift_ids: string) { - const parsed = shift_ids.split(/,\s*/).map(value => Number(value)).filter(Number.isFinite); - return this.get_service.getShiftByShiftId(parsed); - } - - @Post(':timesheet_id') - createBatch( - @Param('timesheet_id', ParseIntPipe) timesheet_id: number, - @Body()dtos: ShiftDto[]): Promise { - const list = Array.isArray(dtos) ? dtos : []; - if(list.length === 0) throw new BadRequestException('Body is missing or invalid (create shifts)') - - return this.upsert_service.createShifts(timesheet_id, dtos) - } - - @Patch() - updateBatch( - @Body() body: { updates: { id: number; dto: updateShiftDto }[] }): Promise{ - const updates = Array.isArray(body?.updates) - ? body.updates.filter(update => Number.isFinite(update?.id) && typeof update.dto === "object") - : []; - if(updates.length === 0) { - throw new BadRequestException(`Body is missing or invalid (update shifts)`); - } - return this.upsert_service.updateShifts(updates); - } - - @Delete(':shift_id') - remove(@Param('shift_id') shift_id: number ) { - return this.upsert_service.deleteShift(shift_id); - } - -} \ No newline at end of file diff --git a/src/modules/shifts/dtos/update-shift.dto.ts b/src/modules/shifts/dtos/update-shift.dto.ts deleted file mode 100644 index ebbbd13..0000000 --- a/src/modules/shifts/dtos/update-shift.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { PartialType, OmitType } from "@nestjs/swagger"; -import { ShiftDto } from "./shift.dto"; - -export class updateShiftDto extends PartialType ( - //allows update using ShiftDto and preventing OmitType variables to be modified - OmitType(ShiftDto, [ 'is_approved', 'timesheet_id'] as const) -){} \ No newline at end of file diff --git a/src/modules/shifts/helpers/shifts-date-time-helpers.ts b/src/modules/shifts/helpers/shifts-date-time-helpers.ts deleted file mode 100644 index 1ed0854..0000000 --- a/src/modules/shifts/helpers/shifts-date-time-helpers.ts +++ /dev/null @@ -1,31 +0,0 @@ -export function weekStartSunday(date_local: Date): Date { - const start = new Date(Date.UTC(date_local.getFullYear(), date_local.getMonth(), date_local.getDate())); - const dow = start.getDay(); // 0 = dimanche - start.setDate(start.getDate() - dow); - start.setHours(0, 0, 0, 0); - return start; -} - -//converts string to HHmm format -export const toStringFromHHmm = (date: Date): string => { - const hh = date.getUTCHours().toString().padStart(2, '0'); - const mm = date.getUTCMinutes().toString().padStart(2, '0'); - return `${hh}:${mm}`; -} - -//converts string to Date format -export const toStringFromDate = (date: Date) => - date.toISOString().slice(0,10); - -//converts HHmm format to string -export const toHHmmFromString = (hhmm: string): Date => { - const [hh, mm] = hhmm.split(':').map(Number); - const date = new Date('1970-01-01T00:00:00.000Z'); - date.setUTCHours(hh, mm, 0, 0); - return new Date(date); -} - -//converts Date format to string -export const toDateFromString = (ymd: string): Date => { - return new Date(`${ymd}T00:00:00:000Z`); -} \ No newline at end of file diff --git a/src/modules/shifts/services/shifts-get.service.ts b/src/modules/shifts/services/shifts-get.service.ts deleted file mode 100644 index 1a79f49..0000000 --- a/src/modules/shifts/services/shifts-get.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; -import { PrismaService } from "src/prisma/prisma.service"; -import { GetShiftDto } from "../dtos/get-shift.dto"; -import { toStringFromDate, toStringFromHHmm } from "../helpers/shifts-date-time-helpers"; - -@Injectable() -export class ShiftsGetService { - constructor( - private readonly prisma: PrismaService, - ){} - - //fetch a shift using shift_id and return all that shift's info - async getShiftByShiftId(shift_ids: number[]): Promise { - if(!Array.isArray(shift_ids) || shift_ids.length === 0) return []; - - const rows = await this.prisma.shifts.findMany({ - where: { id: { in: shift_ids } }, - select: { - id: true, - timesheet_id: true, - bank_code_id: true, - date: true, - start_time: true, - end_time: true, - is_remote: true, - is_approved: true, - comment: true, - } - }); - - if(rows.length !== shift_ids.length) { - const found_ids = new Set(rows.map(row => row.id)); - const missing_ids = shift_ids.filter(id => !found_ids.has(id)); - throw new NotFoundException(`Shift(s) not found: ${ missing_ids.join(", ")}`); - } - - const row_by_id = new Map(rows.map(row => [row.id, row])); - - return shift_ids.map((id) => { - const shift = row_by_id.get(id)!; - return { - timesheet_id: shift.timesheet_id, - bank_code_id: shift.bank_code_id, - date: toStringFromDate(shift.date), - start_time: toStringFromHHmm(shift.start_time), - end_time: toStringFromHHmm(shift.end_time), - is_remote: shift.is_remote, - is_approved: shift.is_approved, - comment: shift.comment ?? undefined, - } satisfies GetShiftDto; - }); - - - - } -} \ No newline at end of file diff --git a/src/modules/shifts/services/shifts-upsert.service.ts b/src/modules/shifts/services/shifts-upsert.service.ts deleted file mode 100644 index e63380b..0000000 --- a/src/modules/shifts/services/shifts-upsert.service.ts +++ /dev/null @@ -1,367 +0,0 @@ -import { toDateFromString, toHHmmFromString, toStringFromDate, toStringFromHHmm } from "../helpers/shifts-date-time-helpers"; -import { BadRequestException, ConflictException, Injectable, NotFoundException } from "@nestjs/common"; -import { OvertimeService, WeekOvertimeSummary } from "src/modules/business-logics/services/overtime.service"; -import { updateShiftDto } from "../dtos/update-shift.dto"; -import { PrismaService } from "src/prisma/prisma.service"; -import { GetShiftDto } from "../dtos/get-shift.dto"; -import { ShiftDto } from "../dtos/shift.dto"; - -type Normalized = { date: Date; start_time: Date; end_time: Date; }; - -export type ShiftWithOvertimeDto = { - shift: GetShiftDto; - overtime: WeekOvertimeSummary; -}; - -export type CreateResult = { ok: true; data: ShiftWithOvertimeDto } | { ok: false; error: any }; -export type UpdatePayload = { id: number; dto: updateShiftDto }; -export type UpdateResult = { ok: true; id: number; data: ShiftWithOvertimeDto } | { ok: false; id: number; error: any }; -export type DeleteResult = { ok: true; id: number; overtime: WeekOvertimeSummary } | { ok: false; id: number; error: any }; - -type NormedOk = { index: number; dto: ShiftDto; normed: Normalized }; -type NormedErr = { index: number; error: any }; - -const overlaps = (a: { start: Date; end: Date }, b: { start: Date; end: Date }) => - !(a.end <= b.start || a.start >= b.end); - -@Injectable() -export class ShiftsUpsertService { - constructor( - private readonly prisma: PrismaService, - private readonly overtime: OvertimeService, - ) { } - - //_________________________________________________________________ - // CREATE - //_________________________________________________________________ - //normalized frontend data to match DB - //loads all shifts from a selected day to check for overlaping shifts - //checks for overlaping shifts - //create new shifts - //calculate overtime - async createShifts(timesheet_id: number, dtos: ShiftDto[]): Promise { - if (!Array.isArray(dtos) || dtos.length === 0) return []; - - const normed_shift: Array = dtos.map((dto, index) => { - try { - const normed = this.normalizeShiftDto(dto); - if (normed.end_time <= normed.start_time) { - return { index, error: new BadRequestException(`end_time must be greater than start_time (index ${index})`) }; - } - return { index, dto, normed }; - } catch (error) { - return { index, error }; - } - }); - const ok_items = normed_shift.filter((x): x is NormedOk => "normed" in x); - - const regroup_by_date = new Map(); - - ok_items.forEach(({ index, normed }) => { - const d = normed.date; - const key = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); - if (!regroup_by_date.has(key)) regroup_by_date.set(key, []); - regroup_by_date.get(key)!.push(index); - }); - - for (const indices of regroup_by_date.values()) { - const ordered = indices - .map(index => { - const item = normed_shift[index] as NormedOk; - return { index: index, start: item.normed.start_time, end: item.normed.end_time }; - }) - .sort((a, b) => a.start.getTime() - b.start.getTime()); - - for (let j = 1; j < ordered.length; j++) { - if (overlaps({ start: ordered[j - 1].start, end: ordered[j - 1].end }, { start: ordered[j].start, end: ordered[j].end })) { - const err = new ConflictException({ - error_code: 'SHIFT_OVERLAP_BATCH', - message: 'New shift overlaps with another shift in the same batch (same day).', - }); - return dtos.map((_dto, key) => - indices.includes(key) - ? ({ ok: false, error: err } as CreateResult) - : ({ ok: false, error: new BadRequestException('Batch aborted due to overlaps in another date group') }) - ); - } - } - } - return this.prisma.$transaction(async (tx) => { - const results: CreateResult[] = Array.from({ length: dtos.length }, () => ({ ok: false, error: new Error('uninitialized') })); - - - normed_shift.forEach((x, i) => { - if ("error" in x) results[i] = { ok: false, error: x.error }; - }); - - const unique_dates = Array.from(regroup_by_date.keys()).map(ms => new Date(ms)); - const existing_date = new Map(); - for (const d of unique_dates) { - const rows = await tx.shifts.findMany({ - where: { timesheet_id, date: d }, - select: { start_time: true, end_time: true }, - }); - existing_date.set(d.getTime(), rows.map(r => ({ start_time: r.start_time, end_time: r.end_time }))); - } - - for (const item of ok_items) { - const { index, dto, normed } = item; - const dayKey = new Date(normed.date.getFullYear(), normed.date.getMonth(), normed.date.getDate()).getTime(); - const existing = existing_date.get(dayKey) ?? []; - - const hit = existing.find(e => overlaps({ start: e.start_time, end: e.end_time }, { start: normed.start_time, end: normed.end_time })); - if (hit) { - results[index] = { - ok: false, - error: new ConflictException({ - error_code: 'SHIFT_OVERLAP', - message: 'New shift overlaps with existing shift(s)', - conflicts: [{ - start_time: toStringFromHHmm(hit.start_time), - end_time: toStringFromHHmm(hit.end_time), - type: 'UNKNOWN', - }], - }), - }; - continue; - } - - const row = await tx.shifts.create({ - data: { - timesheet_id, - bank_code_id: dto.bank_code_id, - date: normed.date, - start_time: normed.start_time, - end_time: normed.end_time, - is_remote: dto.is_remote, - comment: dto.comment ?? undefined, - }, - select: { - timesheet_id: true, bank_code_id: true, date: true, - start_time: true, end_time: true, is_remote: true, is_approved: true, comment: true, - }, - }); - - existing.push({ start_time: row.start_time, end_time: row.end_time }); - - const summary = await this.overtime.getWeekOvertimeSummary(timesheet_id, normed.date, tx); - const shift: GetShiftDto = { - timesheet_id: row.timesheet_id, - bank_code_id: row.bank_code_id, - date: toStringFromDate(row.date), - start_time: toStringFromHHmm(row.start_time), - end_time: toStringFromHHmm(row.end_time), - is_remote: row.is_remote, - is_approved: false, - comment: row.comment ?? undefined, - }; - results[index] = { ok: true, data: { shift, overtime: summary } }; - } - - return results; - }); - } - - //_________________________________________________________________ - // UPDATE - //_________________________________________________________________ - // finds existing shifts in DB - // verify if shifts are already approved - // normalized Date and Time format to string - // check for valid start and end times - // check for overlaping possibility - // buil a set of data to manipulate modified data only - // update shifts in DB - // recalculate overtime after update - // return an updated version to display - async updateShifts(updates: UpdatePayload[]): Promise { - if (!Array.isArray(updates) || updates.length === 0) return []; - - return this.prisma.$transaction(async (tx) => { - const shift_ids = updates.map(update_shift => update_shift.id); - const rows = await tx.shifts.findMany({ - where: { id: { in: shift_ids } }, - select: { - id: true, - timesheet_id: true, - bank_code_id: true, - date: true, - start_time: true, - end_time: true, - is_remote: true, - is_approved: true, - comment: true, - }, - }); - const regroup_id = new Map(rows.map(r => [r.id, r])); - - for (const update of updates) { - const existing = regroup_id.get(update.id); - if (!existing) { - return updates.map(exist => exist.id === update.id - ? ({ ok: false, id: update.id, error: new NotFoundException(`Shift with id: ${update.id} not found`) } as UpdateResult) - : ({ ok: false, id: exist.id, error: new BadRequestException('Batch aborted due to missing shift') })); - } - if (existing.is_approved) { - return updates.map(exist => exist.id === update.id - ? ({ ok: false, id: update.id, error: new BadRequestException('Approved shift cannot be updated') } as UpdateResult) - : ({ ok: false, id: exist.id, error: new BadRequestException('Batch aborted due to approved shift in update set') })); - } - } - - const planned_updates = updates.map(update => { - const exist_shift = regroup_id.get(update.id)!; - const date_string = update.dto.date ?? toStringFromDate(exist_shift.date); - const start_string = update.dto.start_time ?? toStringFromHHmm(exist_shift.start_time); - const end_string = update.dto.end_time ?? toStringFromHHmm(exist_shift.end_time); - const normed: Normalized = { - date: toDateFromString(date_string), - start_time: toHHmmFromString(start_string), - end_time: toHHmmFromString(end_string), - }; - return { update, exist_shift, normed }; - }); - - const groups = new Map(); - function key(timesheet: number, d: Date) { - const day_date = new Date(d.getFullYear(), d.getMonth(), d.getDate()); - return `${timesheet}|${day_date.getTime()}`; - } - - const unique_pairs = new Map(); - for (const { exist_shift, normed } of planned_updates) { - unique_pairs.set(key(exist_shift.timesheet_id, exist_shift.date), { timesheet_id: exist_shift.timesheet_id, date: exist_shift.date }); - unique_pairs.set(key(exist_shift.timesheet_id, normed.date), { timesheet_id: exist_shift.timesheet_id, date: normed.date }); - } - - for (const group of unique_pairs.values()) { - const day_date = new Date(group.date.getFullYear(), group.date.getMonth(), group.date.getDate()); - const existing = await tx.shifts.findMany({ - where: { timesheet_id: group.timesheet_id, date: day_date }, - select: { id: true, start_time: true, end_time: true }, - }); - groups.set(key(group.timesheet_id, day_date), { existing: existing.map(row => ({ id: row.id, start: row.start_time, end: row.end_time })), incoming: planned_updates }); - } - - for (const planned of planned_updates) { - const keys = key(planned.exist_shift.timesheet_id, planned.normed.date); - const group = groups.get(keys)!; - - const conflict = group.existing.find(row => - row.id !== planned.exist_shift.id && overlaps({ start: row.start, end: row.end }, { start: planned.normed.start_time, end: planned.normed.end_time }) - ); - if (conflict) { - return updates.map(exist => - exist.id === planned.exist_shift.id - ? ({ - ok: false, id: exist.id, error: new ConflictException({ - error_code: 'SHIFT_OVERLAP', - message: 'New shift overlaps with existing shift(s)', - conflicts: [{ start_time: toStringFromHHmm(conflict.start), end_time: toStringFromHHmm(conflict.end), type: 'UNKNOWN' }], - }) - } as UpdateResult) - : ({ ok: false, id: exist.id, error: new BadRequestException('Batch aborted due to overlap in another update') }) - ); - } - } - - const regoup_by_day = new Map(); - for (const planned of planned_updates) { - const keys = key(planned.exist_shift.timesheet_id, planned.normed.date); - if (!regoup_by_day.has(keys)) regoup_by_day.set(keys, []); - regoup_by_day.get(keys)!.push({ id: planned.exist_shift.id, start: planned.normed.start_time, end: planned.normed.end_time }); - } - for (const arr of regoup_by_day.values()) { - arr.sort((a, b) => a.start.getTime() - b.start.getTime()); - for (let i = 1; i < arr.length; i++) { - if (overlaps({ start: arr[i - 1].start, end: arr[i - 1].end }, { start: arr[i].start, end: arr[i].end })) { - const error = new ConflictException({ error_code: 'SHIFT_OVERLAP_BATCH', message: 'Overlaps between updates within the same day.' }); - return updates.map(exist => ({ ok: false, id: exist.id, error: error })); - } - } - } - - const results: UpdateResult[] = []; - for (const planned of planned_updates) { - const data: any = {}; - const { dto } = planned.update; - if (dto.date !== undefined) data.date = planned.normed.date; - if (dto.start_time !== undefined) data.start_time = planned.normed.start_time; - if (dto.end_time !== undefined) data.end_time = planned.normed.end_time; - if (dto.bank_code_id !== undefined) data.bank_code_id = dto.bank_code_id; - if (dto.is_remote !== undefined) data.is_remote = dto.is_remote; - if (dto.comment !== undefined) data.comment = dto.comment ?? null; - - const row = await tx.shifts.update({ - where: { id: planned.exist_shift.id }, - data, - select: { - timesheet_id: true, - bank_code_id: true, - date: true, - start_time: true, - end_time: true, - is_remote: true, - is_approved: true, - comment: true, - }, - }); - - const summary_new = await this.overtime.getWeekOvertimeSummary(row.timesheet_id, planned.exist_shift.date, tx); - if (row.date.getTime() !== planned.exist_shift.date.getTime()) { - await this.overtime.getWeekOvertimeSummary(row.timesheet_id, row.date, tx); - } - - const shift: GetShiftDto = { - timesheet_id: row.timesheet_id, - bank_code_id: row.bank_code_id, - date: toStringFromDate(row.date), - start_time: toStringFromHHmm(row.start_time), - end_time: toStringFromHHmm(row.end_time), - is_approved: row.is_approved, - is_remote: row.is_remote, - comment: row.comment ?? undefined, - }; - - results.push({ ok: true, id: planned.exist_shift.id, data: { shift, overtime: summary_new } }); - } - return results; - }); - - } - - //_________________________________________________________________ - // DELETE - //_________________________________________________________________ - //finds shifts using shit_ids - //recalc overtime shifts after delete - //blocs deletion if approved - async deleteShift(shift_id: number) { - return await this.prisma.$transaction(async (tx) =>{ - const shift = await tx.shifts.findUnique({ - where: { id: shift_id }, - select: { id: true, date: true, timesheet_id: true }, - }); - if(!shift) throw new NotFoundException(`Shift with id #${shift_id} not found`); - - await tx.shifts.delete({ where: { id: shift_id } }); - - const summary = await this.overtime.getWeekOvertimeSummary( shift.timesheet_id, shift.date, tx); - return { - success: true, - overtime: summary - }; - }); - } - - //_________________________________________________________________ - // LOCAL HELPERS - //_________________________________________________________________ - //converts all string hours and date to Date and HHmm formats - private normalizeShiftDto = (dto: ShiftDto): Normalized => { - const date = toDateFromString(dto.date); - const start_time = toHHmmFromString(dto.start_time); - const end_time = toHHmmFromString(dto.end_time); - return { date, start_time, end_time }; - } -} \ No newline at end of file diff --git a/src/modules/shifts/shifts.module.ts b/src/modules/shifts/shifts.module.ts deleted file mode 100644 index 4c20e0d..0000000 --- a/src/modules/shifts/shifts.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ShiftsArchivalService } from './services/shifts-archival.service'; -import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module'; -import { NotificationsModule } from '../notifications/notifications.module'; -import { ShiftsUpsertService } from './services/shifts-upsert.service'; -import { ShiftsGetService } from './services/shifts-get.service'; -import { ShiftController } from './controllers/shift.controller'; -import { SharedModule } from '../shared/shared.module'; -import { Module } from '@nestjs/common'; - -@Module({ - imports: [ - BusinessLogicsModule, - NotificationsModule, - SharedModule, - ], - controllers: [ShiftController], - providers: [ - ShiftsArchivalService, - ShiftsGetService, - ShiftsUpsertService, - ], - exports: [ ShiftsUpsertService, ShiftsGetService ], -}) -export class ShiftsModule {} diff --git a/src/modules/timesheets/controllers/timesheet.controller.ts b/src/modules/timesheets/controllers/timesheet.controller.ts deleted file mode 100644 index 1efb2fe..0000000 --- a/src/modules/timesheets/controllers/timesheet.controller.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; -import { GetTimesheetsOverviewService } from "../services/timesheet-get-overview.service"; -import { BadRequestException, Controller, Get, Query} from "@nestjs/common"; - -@Controller('timesheets') -export class TimesheetController { - constructor( - private readonly timesheetOverview: GetTimesheetsOverviewService, - private readonly emailResolver: EmailToIdResolver, - ){} - - @Get() - async getTimesheetByIds( - @Query('employee_email') employee_email: string, - @Query('year') year: string, - @Query('period_number') period_number: string, - ) { - if (!employee_email || !year || !period_number) { - throw new BadRequestException('Query params "employee_email", "year" and eriod_number" are required.'); - } - const employee_id = await this.emailResolver.findIdByEmail(employee_email); - const pay_year = Number(year); - const period_num = Number(period_number); - return this.timesheetOverview.getTimesheetsForEmployeeByPeriod(employee_id, pay_year, period_num); - } -} diff --git a/src/modules/timesheets/helpers/timesheets-date-time-helpers.ts b/src/modules/timesheets/helpers/timesheets-date-time-helpers.ts deleted file mode 100644 index fab7730..0000000 --- a/src/modules/timesheets/helpers/timesheets-date-time-helpers.ts +++ /dev/null @@ -1,36 +0,0 @@ -export function weekStartSunday(date_local: Date): Date { - const start = new Date(Date.UTC(date_local.getFullYear(), date_local.getMonth(), date_local.getDate())); - const dow = start.getDay(); - start.setDate(start.getDate() - dow); - start.setHours(0, 0, 0, 0); - return start; -} - -export const toDateFromString = ( date: Date | string):Date => { - const d = new Date(date); - return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); -} - -export const sevenDaysFrom = (date: Date | string): Date[] => { - return Array.from({length: 7 }, (_,i) => { - const d = new Date(date); - d.setUTCDate(d.getUTCDate() + i ); - return d; - }); -} - -export const toStringFromDate = (date: Date | string): string => { - const d = toDateFromString(date); - const year = d.getUTCFullYear(); - const month = String(d.getUTCMonth() + 1).padStart(2, '0'); - const day = String(d.getUTCDate()).padStart(2, '0'); - return `${year}-${month}-${d}`; -} - -export const toHHmmFromDate = (input: Date | string): string => { - const date = new Date(input); - const hh = String(date.getUTCHours()).padStart(2, '0'); - const mm = String(date.getUTCMinutes()).padStart(2, '0'); - return `${hh}:${mm}`; -} - diff --git a/src/modules/timesheets/services/timesheet-approval.service.ts b/src/modules/timesheets/services/timesheet-approval.service.ts deleted file mode 100644 index c22d0dc..0000000 --- a/src/modules/timesheets/services/timesheet-approval.service.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { Prisma, Timesheets } from "@prisma/client"; -import { BaseApprovalService } from "src/common/shared/base-approval.service"; -import { PrismaService } from "src/prisma/prisma.service"; - - @Injectable() - export class TimesheetApprovalService extends BaseApprovalService{ - constructor(prisma: PrismaService){super(prisma)} - //_____________________________________________________________________________________________ - // APPROVAL AND DELEGATE METHODS - //_____________________________________________________________________________________________ - protected get delegate() { - return this.prisma.timesheets; - } - - protected delegateFor(transaction: Prisma.TransactionClient) { - return transaction.timesheets; - } - - async updateApproval(id: number, isApproved: boolean): Promise { - return this.prisma.$transaction((transaction) => - this.updateApprovalWithTransaction(transaction, id, isApproved), - ); - } - - async cascadeApprovalWithtx(transaction: Prisma.TransactionClient, timesheetId: number, isApproved: boolean): Promise { - const timesheet = await this.updateApprovalWithTransaction(transaction, timesheetId, isApproved); - await transaction.shifts.updateMany({ - where: { timesheet_id: timesheetId }, - data: { is_approved: isApproved }, - }); - await transaction.expenses.updateManyAndReturn({ - where: { timesheet_id: timesheetId }, - data: { is_approved: isApproved }, - }); - return timesheet; - } - } \ No newline at end of file diff --git a/src/modules/timesheets/services/timesheet-archive.service.ts b/src/modules/timesheets/services/timesheet-archive.service.ts deleted file mode 100644 index c75bdc3..0000000 --- a/src/modules/timesheets/services/timesheet-archive.service.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { TimesheetsArchive } from "@prisma/client"; -import { PrismaService } from "src/prisma/prisma.service"; - -export class TimesheetArchiveService { - constructor(private readonly prisma: PrismaService){} - - 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: { some: { 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) } } }); - }); - } - - //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 } }); - } -} \ No newline at end of file diff --git a/src/modules/timesheets/timesheets.module.ts b/src/modules/timesheets/timesheets.module.ts deleted file mode 100644 index be44043..0000000 --- a/src/modules/timesheets/timesheets.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { GetTimesheetsOverviewService } from './services/timesheet-get-overview.service'; -import { TimesheetArchiveService } from './services/timesheet-archive.service'; -import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module'; -import { TimesheetController } from './controllers/timesheet.controller'; -import { SharedModule } from '../shared/shared.module'; -import { ShiftsModule } from '../shifts/shifts.module'; -import { Module } from '@nestjs/common'; - -@Module({ - imports: [ - BusinessLogicsModule, - SharedModule, - ShiftsModule, - ], - controllers: [TimesheetController], - providers: [ - TimesheetArchiveService, - GetTimesheetsOverviewService, - SharedModule, - ], - exports: [], -}) -export class TimesheetsModule {} diff --git a/src/modules/users-management/services/abstract-user.service.ts b/src/modules/users-management/services/abstract-user.service.ts deleted file mode 100644 index 4e0da2e..0000000 --- a/src/modules/users-management/services/abstract-user.service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { Users } from '@prisma/client'; -import { PrismaService } from 'src/prisma/prisma.service'; - -@Injectable() -export abstract class AbstractUserService { - constructor(protected readonly prisma: PrismaService) {} - - findAll(): Promise { - return this.prisma.users.findMany(); - } - - async findOne( id: string ): Promise { - const user = await this.prisma.users.findUnique({ where: { id } }); - if (!user) { - throw new NotFoundException(`User #${id} not found`); - } - return user; - } - - async findOneByEmail( email: string ): Promise> { - const user = await this.prisma.users.findUnique({ where: { email } }); - if (!user) { - throw new NotFoundException(`No user with email #${email} exists`); - } - - const clean_user = { - first_name: user.first_name, - last_name: user.last_name, - email: user.email, - role: user.role, - } - - return clean_user; - } - - async remove(id: string): Promise { - await this.findOne(id); - return this.prisma.users.delete({ where: { id } }); - } -} diff --git a/src/modules/business-logics/business-logics.module.ts b/src/time-and-attendance/domains/business-logics.module.ts similarity index 67% rename from src/modules/business-logics/business-logics.module.ts rename to src/time-and-attendance/domains/business-logics.module.ts index f84330d..4dc801e 100644 --- a/src/modules/business-logics/business-logics.module.ts +++ b/src/time-and-attendance/domains/business-logics.module.ts @@ -1,26 +1,23 @@ -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 { OvertimeService } from "./services/overtime.service"; import { VacationService } from "./services/vacation.service"; +import { HolidayService } from "./services/holiday.service"; import { MileageService } from "./services/mileage.service"; +import { Module } from "@nestjs/common"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; -//AfterHours is not used, need to clarify infos before implementing into shifts.service @Module({ + imports:[], providers: [ - PrismaService, - //AfterHoursService, HolidayService, MileageService, OvertimeService, SickLeaveService, - VacationService + VacationService, + EmailToIdResolver, ], exports: [ - //AfterHoursService, HolidayService, MileageService, OvertimeService, diff --git a/src/modules/business-logics/services/holiday.service.ts b/src/time-and-attendance/domains/services/holiday.service.ts similarity index 79% rename from src/modules/business-logics/services/holiday.service.ts rename to src/time-and-attendance/domains/services/holiday.service.ts index 15bf4d2..5a0c2b7 100644 --- a/src/modules/business-logics/services/holiday.service.ts +++ b/src/time-and-attendance/domains/services/holiday.service.ts @@ -1,9 +1,8 @@ import { Injectable, Logger, NotFoundException } from "@nestjs/common"; import { computeHours, getWeekStart } from "src/common/utils/date-utils"; -import { PrismaService } from "../../../prisma/prisma.service"; - -const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000; - +import { PrismaService } from "src/prisma/prisma.service"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { MS_PER_WEEK } from "src/time-and-attendance/utils/constants.utils"; /* le calcul est 1/20 des 4 dernières semaines, précédent la semaine incluant le férier. Un maximum de 08h00 est allouable pour le férier @@ -15,28 +14,19 @@ const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000; export class HolidayService { private readonly logger = new Logger(HolidayService.name); - constructor(private readonly prisma: PrismaService) {} - - //fetch employee_id by email - private async resolveEmployeeByEmail(email: string): Promise { - const employee = await this.prisma.employees.findFirst({ - where: { - user: { email } - }, - select: { id: true }, - }); - if(!employee) throw new NotFoundException(`Employee with email : ${email} not found`); - return employee.id; - } + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver: EmailToIdResolver, + ) {} private async computeHoursPrevious4WeeksByEmail(email: string, holiday_date: Date): Promise { - const employee_id = await this.resolveEmployeeByEmail(email); + const employee_id = await this.emailResolver.findIdByEmail(email); return this.computeHoursPrevious4Weeks(employee_id, holiday_date); } private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise { const holiday_week_start = getWeekStart(holiday_date); - const window_start = new Date(holiday_week_start.getTime() - 4 * WEEK_IN_MS); + const window_start = new Date(holiday_week_start.getTime() - 4 * MS_PER_WEEK); const window_end = new Date(holiday_week_start.getTime() - 1); const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G700']; @@ -60,7 +50,7 @@ export class HolidayService { let capped_total = 0; for(let offset = 1; offset <= 4; offset++) { - const week_start = new Date(holiday_week_start.getTime() - offset * WEEK_IN_MS); + const week_start = new Date(holiday_week_start.getTime() - offset * MS_PER_WEEK); const key = week_start.getTime(); const weekly_hours = hours_by_week.get(key) ?? 0; capped_total += Math.min(weekly_hours, 40); diff --git a/src/modules/business-logics/services/mileage.service.ts b/src/time-and-attendance/domains/services/mileage.service.ts similarity index 91% rename from src/modules/business-logics/services/mileage.service.ts rename to src/time-and-attendance/domains/services/mileage.service.ts index 8919532..2b589a9 100644 --- a/src/modules/business-logics/services/mileage.service.ts +++ b/src/time-and-attendance/domains/services/mileage.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common"; -import { PrismaService } from "../../../prisma/prisma.service"; -import { Decimal } from "@prisma/client/runtime/library"; +import { PrismaService } from "src/prisma/prisma.service"; @Injectable() export class MileageService { diff --git a/src/time-and-attendance/domains/services/overtime.service.ts b/src/time-and-attendance/domains/services/overtime.service.ts new file mode 100644 index 0000000..e43c4d3 --- /dev/null +++ b/src/time-and-attendance/domains/services/overtime.service.ts @@ -0,0 +1,90 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { DAILY_LIMIT_HOURS, WEEKLY_LIMIT_HOURS } from 'src/time-and-attendance/utils/constants.utils'; +import { Tx, WeekOvertimeSummary } from 'src/time-and-attendance/utils/type.utils'; + + +@Injectable() +export class OvertimeService { + + private logger = new Logger(OvertimeService.name); + + private INCLUDED_TYPES = ['EMERGENCY', 'EVENING','OVERTIME','REGULAR'] as const; // included types for weekly overtime calculation + + constructor(private prisma: PrismaService) {} + + async getWeekOvertimeSummary( timesheet_id: number, date: Date, tx?: Tx ): Promise{ + const db = tx ?? this.prisma; + + const week_start = getWeekStart(date); + const week_end = getWeekEnd(week_start); + + const shifts = await db.shifts.findMany({ + where: { + timesheet_id, + date: { gte: week_start, lte: week_end }, + bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } }, + }, + select: { date: true, start_time: true, end_time: true }, + orderBy: [{date: 'asc'}, {start_time: 'asc'}], + }); + + const day_totals = new Map(); + for (const shift of shifts){ + const key = shift.date.toISOString().slice(0,10); + const hours = computeHours(shift.start_time, shift.end_time, 5); + day_totals.set(key, (day_totals.get(key) ?? 0) + hours); + } + + const days: string[] = []; + for(let i = 0; i < 7; i++){ + const day = new Date(week_start.getTime() + i * 24 * 60 * 60 * 1000); + days.push(day.toISOString().slice(0,10)); + } + + const week_total_hours = [ ...day_totals.values()].reduce((a,b) => a + b, 0); + const weekly_overtime = Math.max(0, week_total_hours - WEEKLY_LIMIT_HOURS); + + let running = 0; + let daily_kept_sum = 0; + const breakdown: WeekOvertimeSummary['breakdown'] = []; + + for (const key of days) { + const day_hours = day_totals.get(key) ?? 0; + const day_overtime = Math.max(0, day_hours - DAILY_LIMIT_HOURS); + + const cap_before_40 = Math.max(0, WEEKLY_LIMIT_HOURS - running); + const daily_kept = Math.min(day_overtime, cap_before_40); + + breakdown.push({ + date: key, + day_hours, + day_overtime, + daily_kept, + running_total_before: running, + }); + + daily_kept_sum += daily_kept; + running += day_hours; + } + const total_overtime = weekly_overtime + daily_kept_sum; + + this.logger.debug( + `[OVERTIME][SUMMARY][ts=${timesheet_id}] week=${week_start.toISOString().slice(0,10)}..${week_end + .toISOString() + .slice(0,10)} week_total=${week_total_hours.toFixed(2)}h weekly=${weekly_overtime.toFixed( + 2, + )}h daily_kept=${daily_kept_sum.toFixed(2)}h total=${total_overtime.toFixed(2)}h`, + ); + return { + week_start: week_start.toISOString().slice(0, 10), + week_end: week_end.toISOString().slice(0, 10), + week_total_hours, + weekly_overtime, + daily_overtime_kept: daily_kept_sum, + total_overtime, + breakdown, + }; + } +} diff --git a/src/modules/business-logics/services/sick-leave.service.ts b/src/time-and-attendance/domains/services/sick-leave.service.ts similarity index 97% rename from src/modules/business-logics/services/sick-leave.service.ts rename to src/time-and-attendance/domains/services/sick-leave.service.ts index 6c00113..60a847e 100644 --- a/src/modules/business-logics/services/sick-leave.service.ts +++ b/src/time-and-attendance/domains/services/sick-leave.service.ts @@ -1,6 +1,6 @@ import { getYearStart, roundToQuarterHour } from "src/common/utils/date-utils"; import { Injectable, Logger } from "@nestjs/common"; -import { PrismaService } from "../../../prisma/prisma.service"; +import { PrismaService } from "src/prisma/prisma.service"; @Injectable() export class SickLeaveService { diff --git a/src/modules/business-logics/services/vacation.service.ts b/src/time-and-attendance/domains/services/vacation.service.ts similarity index 98% rename from src/modules/business-logics/services/vacation.service.ts rename to src/time-and-attendance/domains/services/vacation.service.ts index 9445149..2e3a214 100644 --- a/src/modules/business-logics/services/vacation.service.ts +++ b/src/time-and-attendance/domains/services/vacation.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger, NotFoundException } from "@nestjs/common"; -import { PrismaService } from "../../../prisma/prisma.service"; +import { PrismaService } from "src/prisma/prisma.service"; @Injectable() export class VacationService { diff --git a/src/time-and-attendance/expenses/controllers/expense.controller.ts b/src/time-and-attendance/expenses/controllers/expense.controller.ts new file mode 100644 index 0000000..9d9b08d --- /dev/null +++ b/src/time-and-attendance/expenses/controllers/expense.controller.ts @@ -0,0 +1,31 @@ +import { Controller, Post, Param, Body, Patch, Delete, Req, UnauthorizedException } from "@nestjs/common"; +import { CreateExpenseResult } from "src/time-and-attendance/utils/type.utils"; +import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service"; +import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { GLOBAL_CONTROLLER_ROLES } from "src/common/shared/role-groupes"; + +@Controller('expense') +@RolesAllowed(...GLOBAL_CONTROLLER_ROLES) +export class ExpenseController { + constructor( private readonly upsert_service: ExpenseUpsertService ){} + + @Post('create') + create( @Req() req, @Body() dto: ExpenseDto): Promise{ + const email = req.user?.email; + if(!email) throw new UnauthorizedException('Unauthorized User'); + return this.upsert_service.createExpense(dto, email); + } + + @Patch('update') + update(@Body() dto: ExpenseDto): Promise{ + return this.upsert_service.updateExpense(dto); + } + + @Delete('delete/:expense_id') + remove(@Param('expense_id') expense_id: number) { + return this.upsert_service.deleteExpense(expense_id); + } +} + + diff --git a/src/modules/expenses/dtos/expense.dto.ts b/src/time-and-attendance/expenses/dtos/expense-create.dto.ts similarity index 79% rename from src/modules/expenses/dtos/expense.dto.ts rename to src/time-and-attendance/expenses/dtos/expense-create.dto.ts index 51cc174..9b028cc 100644 --- a/src/modules/expenses/dtos/expense.dto.ts +++ b/src/time-and-attendance/expenses/dtos/expense-create.dto.ts @@ -1,11 +1,12 @@ import { IsBoolean, IsInt, IsOptional, IsString, MaxLength } from "class-validator"; export class ExpenseDto { + @IsInt() @IsOptional() id: number; @IsInt() bank_code_id!: number; @IsInt() timesheet_id!: number; - @IsString() @IsOptional() attachment?: string; + @IsInt() @IsOptional() attachment?: number; - @IsString() date!: string; + @IsString() date!: string; @IsInt() @IsOptional() amount?: number; @IsInt() @IsOptional() mileage?: number; @IsString() @MaxLength(280) comment!: string; diff --git a/src/time-and-attendance/expenses/dtos/expense-entity.dto.ts b/src/time-and-attendance/expenses/dtos/expense-entity.dto.ts new file mode 100644 index 0000000..f04c989 --- /dev/null +++ b/src/time-and-attendance/expenses/dtos/expense-entity.dto.ts @@ -0,0 +1,13 @@ + +export class ExpenseEntity { + id: number; + timesheet_id: number; + bank_code_id: number; + attachment?:number; + date: Date; + amount?: number; + mileage?:number; + comment: string; + supervisor_comment?:string; + is_approved: boolean; +} \ No newline at end of file diff --git a/src/modules/expenses/dtos/get-expense.dto.ts b/src/time-and-attendance/expenses/dtos/expense-get.dto.ts similarity index 85% rename from src/modules/expenses/dtos/get-expense.dto.ts rename to src/time-and-attendance/expenses/dtos/expense-get.dto.ts index 6c3056e..773a1a7 100644 --- a/src/modules/expenses/dtos/get-expense.dto.ts +++ b/src/time-and-attendance/expenses/dtos/expense-get.dto.ts @@ -1,7 +1,8 @@ export class GetExpenseDto { + id: number; timesheet_id: number; bank_code_id: number; - attachment?: string; + attachment?: number; date: string; comment: string; mileage?: number; diff --git a/src/time-and-attendance/expenses/expenses.module.ts b/src/time-and-attendance/expenses/expenses.module.ts new file mode 100644 index 0000000..e6d737b --- /dev/null +++ b/src/time-and-attendance/expenses/expenses.module.ts @@ -0,0 +1,10 @@ +import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service"; +import { ExpenseController } from "src/time-and-attendance/expenses/controllers/expense.controller"; +import { Module } from "@nestjs/common"; + +@Module({ + controllers: [ ExpenseController ], + providers: [ ExpenseUpsertService ], +}) + +export class ExpensesModule {} \ No newline at end of file diff --git a/src/time-and-attendance/expenses/services/expense-upsert.service.ts b/src/time-and-attendance/expenses/services/expense-upsert.service.ts new file mode 100644 index 0000000..89c2b44 --- /dev/null +++ b/src/time-and-attendance/expenses/services/expense-upsert.service.ts @@ -0,0 +1,159 @@ +import { CreateExpenseResult, DeleteExpenseResult, NormalizedExpense } from "src/time-and-attendance/utils/type.utils"; +import { toDateFromString, toStringFromDate, weekStartSunday } from "src/time-and-attendance/utils/date-time.utils"; +import { Injectable, NotFoundException } from "@nestjs/common"; +import { expense_select } from "src/time-and-attendance/utils/selects.utils"; +import { PrismaService } from "src/prisma/prisma.service"; +import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; +import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { ExpenseEntity } from "src/time-and-attendance/expenses/dtos/expense-entity.dto"; + + +@Injectable() +export class ExpenseUpsertService { + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver: EmailToIdResolver, + ) { } + + //_________________________________________________________________ + // CREATE + //_________________________________________________________________ + async createExpense(dto: ExpenseDto, email: string): Promise { + try { + //fetch employee_id using req.user.email + const employee_id = await this.emailResolver.findIdByEmail(email); + + //normalize strings and dates and Parse numbers + const normed_expense = this.normalizeAndParseExpenseDto(dto); + + //finds the timesheet using expense.date by finding the sunday + const start_date = weekStartSunday(normed_expense.date); + const timesheet = await this.prisma.timesheets.findFirst({ + where: { start_date, employee_id }, + select: { id: true, employee_id: true }, + }); + if (!timesheet) throw new NotFoundException(`Timesheet with id ${dto.timesheet_id} not found`); + + //create a new expense + const expense = await this.prisma.expenses.create({ + data: { + ...normed_expense, + timesheet_id: timesheet.id, + bank_code_id: dto.bank_code_id, + is_approved: dto.is_approved, + }, + //return the newly created expense with id + select: expense_select, + }); + + //build an object to return to the frontend to display + const created: GetExpenseDto = { + ...expense, + date: toStringFromDate(expense.date), + amount: expense.amount?.toNumber(), + mileage: expense.mileage?.toNumber(), + attachment: expense.attachment ?? undefined, + supervisor_comment: expense.supervisor_comment ?? undefined, + }; + return { ok: true, data: created } + + } catch (error) { + return { ok: false, error: error } + } + } + + //_________________________________________________________________ + // UPDATE + //_________________________________________________________________ + async updateExpense(dto: ExpenseDto): Promise { + try { + //normalize string , date format and parse numbers + const normed_expense = this.normalizeAndParseExpenseDto(dto); + + //checks for modifications + const data: ExpenseEntity = { + ...normed_expense, + id: dto.id, + timesheet_id: dto.timesheet_id, + bank_code_id: dto.bank_code_id, + is_approved: dto.is_approved, + }; + + //push updates and get updated datas + const expense = await this.prisma.expenses.update({ + where: { id: dto.id, timesheet_id: dto.timesheet_id }, + data, + select: expense_select, + }); + + //build an object to return to the frontend + const updated: GetExpenseDto = { + ...expense, + date: toStringFromDate(expense.date), + amount: expense.amount?.toNumber(), + mileage: expense.mileage?.toNumber(), + attachment: expense.attachment ?? undefined, + supervisor_comment: expense.supervisor_comment ?? undefined, + }; + return updated; + } catch (error) { + return error; + } + } + //_________________________________________________________________ + // DELETE + //_________________________________________________________________ + async deleteExpense(expense_id: number): Promise { + try { + await this.prisma.$transaction(async (tx) => { + const expense = await tx.expenses.findUnique({ + where: { id: expense_id }, + select: { id: true }, + }); + if (!expense) throw new NotFoundException(`Expense with id: ${expense_id} not found`); + + await tx.expenses.delete({ where: { id: expense_id } }); + return { success: true }; + }); + return { ok: true, id: expense_id }; + } catch (error) { + return { ok: false, id: expense_id, error }; + } + } + + //_________________________________________________________________ + // LOCAL HELPERS + //_________________________________________________________________ + //makes sure that comments are the right length the date is of Date type + private normalizeAndParseExpenseDto(dto: ExpenseDto): NormalizedExpense { + const parsed_attachment = this.parseOptionalNumber(dto.attachment, "attachment"); + const parsed_mileage = this.parseOptionalNumber(dto.mileage, "mileage"); + const parsed_amount = this.parseOptionalNumber(dto.amount, "amount"); + const comment = this.truncate280(dto.comment); + const supervisor_comment = dto.supervisor_comment && dto.supervisor_comment.trim() + ? this.truncate280(dto.supervisor_comment.trim()) : undefined; + const date = toDateFromString(dto.date); + return { + date, + comment, + supervisor_comment, + parsed_amount, + parsed_attachment, + parsed_mileage + }; + } + + //makes sure that a string cannot exceed 280 chars + private truncate280 = (input: string): string => { + return input.length > 280 ? input.slice(0, 280) : input; + } + + //makes sure that the type of data of numeric values is valid + private parseOptionalNumber = (value: unknown, field: string) => { + if (value == null) return undefined; + const parsed = Number(value); + if (Number.isNaN(parsed)) throw new Error(`Invalid value : ${value} for ${field}`); + return parsed; + }; +} \ No newline at end of file diff --git a/src/modules/expenses/services/expenses-archival.service.ts b/src/time-and-attendance/expenses/services/expenses-archival.service.ts similarity index 100% rename from src/modules/expenses/services/expenses-archival.service.ts rename to src/time-and-attendance/expenses/services/expenses-archival.service.ts index fc17c63..0c354d6 100644 --- a/src/modules/expenses/services/expenses-archival.service.ts +++ b/src/time-and-attendance/expenses/services/expenses-archival.service.ts @@ -1,6 +1,6 @@ -import { Injectable } from "@nestjs/common"; import { ExpensesArchive } from "@prisma/client"; import { PrismaService } from "src/prisma/prisma.service"; +import { Injectable } from "@nestjs/common"; @Injectable() export class ExpensesArchivalService { diff --git a/src/time-and-attendance/leave-requests/controllers/leave-requests.controller.ts b/src/time-and-attendance/leave-requests/controllers/leave-requests.controller.ts new file mode 100644 index 0000000..62a14c4 --- /dev/null +++ b/src/time-and-attendance/leave-requests/controllers/leave-requests.controller.ts @@ -0,0 +1,20 @@ +import { Body, Controller, Post } from "@nestjs/common"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { UpsertLeaveRequestDto } from "../dtos/upsert-leave-request.dto"; +import { LeaveRequestsService } from "../services/leave-request.service"; + +@ApiTags('Leave Requests') +@ApiBearerAuth('access-token') +// @UseGuards() +@Controller('leave-requests') +export class LeaveRequestController { + constructor(private readonly leave_service: LeaveRequestsService){} + + @Post('upsert') + async upsertLeaveRequest(@Body() dto: UpsertLeaveRequestDto) { + const { action, leave_requests } = await this.leave_service.handle(dto); + return { action, leave_requests }; + } + +} + diff --git a/src/modules/leave-requests/dtos/leave-request-view.dto.ts b/src/time-and-attendance/leave-requests/dtos/leave-request-view.dto.ts similarity index 100% rename from src/modules/leave-requests/dtos/leave-request-view.dto.ts rename to src/time-and-attendance/leave-requests/dtos/leave-request-view.dto.ts diff --git a/src/modules/leave-requests/dtos/upsert-leave-request.dto.ts b/src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto.ts similarity index 100% rename from src/modules/leave-requests/dtos/upsert-leave-request.dto.ts rename to src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto.ts diff --git a/src/time-and-attendance/leave-requests/leave-requests.module.ts b/src/time-and-attendance/leave-requests/leave-requests.module.ts new file mode 100644 index 0000000..eff4f6d --- /dev/null +++ b/src/time-and-attendance/leave-requests/leave-requests.module.ts @@ -0,0 +1,16 @@ +import { LeaveRequestController } from "src/time-and-attendance/leave-requests/controllers/leave-requests.controller"; +import { LeaveRequestsService } from "src/time-and-attendance/leave-requests/services/leave-request.service"; +import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module"; +import { ShiftsModule } from "src/time-and-attendance/time-tracker/shifts/shifts.module"; +import { Module } from "@nestjs/common"; + +@Module({ + imports: [ + BusinessLogicsModule, + ShiftsModule, + ], + controllers: [LeaveRequestController], + providers: [LeaveRequestsService], +}) + +export class LeaveRequestsModule {} \ No newline at end of file diff --git a/src/modules/leave-requests/mappers/leave-requests-archive.mapper.ts b/src/time-and-attendance/leave-requests/mappers/leave-requests-archive.mapper.ts similarity index 100% rename from src/modules/leave-requests/mappers/leave-requests-archive.mapper.ts rename to src/time-and-attendance/leave-requests/mappers/leave-requests-archive.mapper.ts index 36d05fa..6a17723 100644 --- a/src/modules/leave-requests/mappers/leave-requests-archive.mapper.ts +++ b/src/time-and-attendance/leave-requests/mappers/leave-requests-archive.mapper.ts @@ -1,6 +1,6 @@ -import { Prisma } from "@prisma/client"; -import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; import { LeaveRequestArchiveRow } from "../utils/leave-requests-archive.select"; +import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; +import { Prisma } from "@prisma/client"; const toNum = (value?: Prisma.Decimal | null) => value ? Number(value) : undefined; diff --git a/src/modules/leave-requests/mappers/leave-requests.mapper.ts b/src/time-and-attendance/leave-requests/mappers/leave-requests.mapper.ts similarity index 91% rename from src/modules/leave-requests/mappers/leave-requests.mapper.ts rename to src/time-and-attendance/leave-requests/mappers/leave-requests.mapper.ts index e93f94b..9f823fc 100644 --- a/src/modules/leave-requests/mappers/leave-requests.mapper.ts +++ b/src/time-and-attendance/leave-requests/mappers/leave-requests.mapper.ts @@ -1,6 +1,6 @@ -import { Prisma } from "@prisma/client"; import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; -import { LeaveRequestRow } from "../utils/leave-requests.select"; +import { LeaveRequestRow } from "src/time-and-attendance/utils/type.utils"; +import { Prisma } from "@prisma/client"; const toNum = (value?: Prisma.Decimal | null) => value !== null && value !== undefined ? Number(value) : undefined; diff --git a/src/time-and-attendance/leave-requests/services/holiday-leave-requests.service.ts b/src/time-and-attendance/leave-requests/services/holiday-leave-requests.service.ts new file mode 100644 index 0000000..cf18c2e --- /dev/null +++ b/src/time-and-attendance/leave-requests/services/holiday-leave-requests.service.ts @@ -0,0 +1,79 @@ +import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; +import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto"; +import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client"; +import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; +import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; +import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service"; +import { PrismaService } from "src/prisma/prisma.service"; +import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; +import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils"; + + + +@Injectable() +export class HolidayLeaveRequestsService { + constructor( + private readonly prisma: PrismaService, + private readonly holidayService: HolidayService, + private readonly leaveUtils: LeaveRequestsUtils, + private readonly emailResolver: EmailToIdResolver, + private readonly typeResolver: BankCodesResolver, + ) {} + + async create(dto: UpsertLeaveRequestDto): Promise { + const email = dto.email.trim(); + const employee_id = await this.emailResolver.findIdByEmail(email); + const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.HOLIDAY); + const dates = normalizeDates(dto.dates); + if (!bank_code) throw new NotFoundException(`bank_code not found`); + if (!dates.length) throw new BadRequestException('Dates array must not be empty'); + + const created: LeaveRequestViewDto[] = []; + + for (const iso_date of dates) { + const date = toDateOnly(iso_date); + + const existing = await this.prisma.leaveRequests.findUnique({ + where: { + leave_per_employee_date: { + employee_id: employee_id, + leave_type: LeaveTypes.HOLIDAY, + date, + }, + }, + select: { id: true }, + }); + if (existing) { + throw new BadRequestException(`Holiday request already exists for ${iso_date}`); + } + + const payable = await this.holidayService.calculateHolidayPay(email, date, bank_code.modifier); + const row = await this.prisma.leaveRequests.create({ + data: { + employee_id: employee_id, + bank_code_id: bank_code.id, + leave_type: LeaveTypes.HOLIDAY, + date, + comment: dto.comment ?? '', + requested_hours: dto.requested_hours ?? 8, + payable_hours: payable, + approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING, + }, + select: leaveRequestsSelect, + }); + + const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); + if (row.approval_status === LeaveApprovalStatus.APPROVED) { + await this.leaveUtils.syncShift(email, employee_id, iso_date, hours,LeaveTypes.HOLIDAY, row.comment); + } + + created.push({ ...mapRowToView(row), action: 'create' }); + } + + return { action: 'create', leave_requests: created }; + } +} + diff --git a/src/time-and-attendance/leave-requests/services/leave-request.service.ts b/src/time-and-attendance/leave-requests/services/leave-request.service.ts new file mode 100644 index 0000000..8b9d7d9 --- /dev/null +++ b/src/time-and-attendance/leave-requests/services/leave-request.service.ts @@ -0,0 +1,241 @@ +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto"; +import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client"; +import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; +import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; +import { roundToQuarterHour } from "src/common/utils/date-utils"; +import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service"; +import { VacationService } from "src/time-and-attendance/domains/services/vacation.service"; +import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service"; +import { PrismaService } from "src/prisma/prisma.service"; +import { mapRowToView } from "../mappers/leave-requests.mapper"; +import { normalizeDates, toDateOnly, toISODateKey } from "src/time-and-attendance/utils/date-time.utils"; +@Injectable() +export class LeaveRequestsService { + constructor( + private readonly prisma: PrismaService, + private readonly holidayService: HolidayService, + private readonly sickLogic: SickLeaveService, + private readonly vacationLogic: VacationService, + private readonly leaveUtils: LeaveRequestsUtils, + private readonly emailResolver: EmailToIdResolver, + private readonly typeResolver: BankCodesResolver, + ) {} + + // handle distribution to the right service according to the selected type and action + async handle(dto: UpsertLeaveRequestDto): Promise { + switch (dto.type) { + case LeaveTypes.HOLIDAY: + if( dto.action === 'create'){ + // return this.holidayService.create(dto); + } else if (dto.action === 'update') { + return this.update(dto, LeaveTypes.HOLIDAY); + } else if (dto.action === 'delete'){ + return this.delete(dto, LeaveTypes.HOLIDAY); + } + case LeaveTypes.VACATION: + if( dto.action === 'create'){ + // return this.vacationService.create(dto); + } else if (dto.action === 'update') { + return this.update(dto, LeaveTypes.VACATION); + } else if (dto.action === 'delete'){ + return this.delete(dto, LeaveTypes.VACATION); + } + case LeaveTypes.SICK: + if( dto.action === 'create'){ + // return this.sickLeaveService.create(dto); + } else if (dto.action === 'update') { + return this.update(dto, LeaveTypes.SICK); + } else if (dto.action === 'delete'){ + return this.delete(dto, LeaveTypes.SICK); + } + default: + throw new BadRequestException(`Unsupported leave type: ${dto.type} or action: ${dto.action}`); + } + } + + async delete(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise { + const email = dto.email.trim(); + const dates = normalizeDates(dto.dates); + const employee_id = await this.emailResolver.findIdByEmail(email); + if (!dates.length) throw new BadRequestException("Dates array must not be empty"); + + const rows = await this.prisma.leaveRequests.findMany({ + where: { + employee_id: employee_id, + leave_type: type, + date: { in: dates.map((d) => toDateOnly(d)) }, + }, + select: leaveRequestsSelect, + }); + + if (rows.length !== dates.length) { + const missing = dates.filter((isoDate) => !rows.some((row) => toISODateKey(row.date) === isoDate)); + throw new NotFoundException(`No Leave request found for: ${missing.join(", ")}`); + } + + for (const row of rows) { + if (row.approval_status === LeaveApprovalStatus.APPROVED) { + const iso = toISODateKey(row.date); + await this.leaveUtils.removeShift(email, employee_id, iso, type); + } + } + + await this.prisma.leaveRequests.deleteMany({ + where: { id: { in: rows.map((row) => row.id) } }, + }); + + const deleted = rows.map((row) => ({ ...mapRowToView(row), action: "delete" as const })); + return { action: "delete", leave_requests: deleted }; + } + + async update(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise { + const email = dto.email.trim(); + const employee_id = await this.emailResolver.findIdByEmail(email); + const bank_code = await this.typeResolver.findIdAndModifierByType(type); + if(!bank_code) throw new NotFoundException(`bank_code not found`); + const modifier = Number(bank_code.modifier ?? 1); + const dates = normalizeDates(dto.dates); + if (!dates.length) { + throw new BadRequestException("Dates array must not be empty"); + } + + const entries = await Promise.all( + dates.map(async (iso_date) => { + const date = toDateOnly(iso_date); + const existing = await this.prisma.leaveRequests.findUnique({ + where: { + leave_per_employee_date: { + employee_id: employee_id, + leave_type: type, + date, + }, + }, + select: leaveRequestsSelect, + }); + if (!existing) throw new NotFoundException(`No Leave request found for ${iso_date}`); + return { iso_date, date, existing }; + }), + ); + + const updated: LeaveRequestViewDto[] = []; + + if (type === LeaveTypes.SICK) { + const firstExisting = entries[0].existing; + const fallbackRequested = + firstExisting.requested_hours !== null && firstExisting.requested_hours !== undefined + ? Number(firstExisting.requested_hours) + : 8; + const requested_hours_per_day = dto.requested_hours ?? fallbackRequested; + const reference_date = entries.reduce( + (latest, entry) => (entry.date > latest ? entry.date : latest), + entries[0].date, + ); + const total_payable_hours = await this.sickLogic.calculateSickLeavePay( + employee_id, + reference_date, + entries.length, + requested_hours_per_day, + modifier, + ); + let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours)); + const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); + + for (const { iso_date, existing } of entries) { + const previous_status = existing.approval_status; + const payable = Math.min(remaining_payable_hours, daily_payable_cap); + const payable_rounded = roundToQuarterHour(Math.max(0, payable)); + remaining_payable_hours = roundToQuarterHour( + Math.max(0, remaining_payable_hours - payable_rounded), + ); + + const row = await this.prisma.leaveRequests.update({ + where: { id: existing.id }, + data: { + comment: dto.comment ?? existing.comment, + requested_hours: requested_hours_per_day, + payable_hours: payable_rounded, + bank_code_id: bank_code.id, + approval_status: dto.approval_status ?? existing.approval_status, + }, + select: leaveRequestsSelect, + }); + + const was_approved = previous_status === LeaveApprovalStatus.APPROVED; + const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED; + const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); + + if (!was_approved && is_approved) { + await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); + } else if (was_approved && !is_approved) { + await this.leaveUtils.removeShift(email, employee_id, iso_date, type); + } else if (was_approved && is_approved) { + await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); + } + updated.push({ ...mapRowToView(row), action: "update" }); + } + return { action: "update", leave_requests: updated }; + } + + for (const { iso_date, date, existing } of entries) { + const previous_status = existing.approval_status; + const fallbackRequested = + existing.requested_hours !== null && existing.requested_hours !== undefined + ? Number(existing.requested_hours) + : 8; + const requested_hours = dto.requested_hours ?? fallbackRequested; + + let payable: number; + switch (type) { + case LeaveTypes.HOLIDAY: + payable = await this.holidayService.calculateHolidayPay(email, date, modifier); + break; + case LeaveTypes.VACATION: { + const days_requested = requested_hours / 8; + payable = await this.vacationLogic.calculateVacationPay( + employee_id, + date, + Math.max(0, days_requested), + modifier, + ); + break; + } + default: + payable = existing.payable_hours !== null && existing.payable_hours !== undefined + ? Number(existing.payable_hours) + : requested_hours; + } + + const row = await this.prisma.leaveRequests.update({ + where: { id: existing.id }, + data: { + requested_hours, + comment: dto.comment ?? existing.comment, + payable_hours: payable, + bank_code_id: bank_code.id, + approval_status: dto.approval_status ?? existing.approval_status, + }, + select: leaveRequestsSelect, + }); + + const was_approved = previous_status === LeaveApprovalStatus.APPROVED; + const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED; + const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); + + if (!was_approved && is_approved) { + await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); + } else if (was_approved && !is_approved) { + await this.leaveUtils.removeShift(email, employee_id, iso_date, type); + } else if (was_approved && is_approved) { + await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment); + } + updated.push({ ...mapRowToView(row), action: "update" }); + } + return { action: "update", leave_requests: updated }; + } +} + + diff --git a/src/time-and-attendance/leave-requests/services/sick-leave-requests.service.ts b/src/time-and-attendance/leave-requests/services/sick-leave-requests.service.ts new file mode 100644 index 0000000..0b35f88 --- /dev/null +++ b/src/time-and-attendance/leave-requests/services/sick-leave-requests.service.ts @@ -0,0 +1,98 @@ +import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; +import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto"; +import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client"; +import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; +import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; +import { roundToQuarterHour } from "src/common/utils/date-utils"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service"; +import { PrismaService } from "src/prisma/prisma.service"; +import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; +import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils"; + + + +@Injectable() +export class SickLeaveRequestsService { + constructor( + private readonly prisma: PrismaService, + private readonly sickService: SickLeaveService, + private readonly emailResolver: EmailToIdResolver, + private readonly typeResolver: BankCodesResolver, + ) {} + + async create(dto: UpsertLeaveRequestDto): Promise { + const email = dto.email.trim(); + const employee_id = await this.emailResolver.findIdByEmail(email); + const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.SICK); + if(!bank_code) throw new NotFoundException(`bank_code not found`); + + const modifier = bank_code.modifier ?? 1; + const dates = normalizeDates(dto.dates); + if (!dates.length) throw new BadRequestException("Dates array must not be empty"); + const requested_hours_per_day = dto.requested_hours ?? 8; + + const entries = dates.map((iso) => ({ iso, date: toDateOnly(iso) })); + const reference_date = entries.reduce( + (latest, entry) => (entry.date > latest ? entry.date : latest), + entries[0].date, + ); + const total_payable_hours = await this.sickService.calculateSickLeavePay( + employee_id, + reference_date, + entries.length, + requested_hours_per_day, + modifier, + ); + let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours)); + const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); + + const created: LeaveRequestViewDto[] = []; + + for (const { iso, date } of entries) { + const existing = await this.prisma.leaveRequests.findUnique({ + where: { + leave_per_employee_date: { + employee_id: employee_id, + leave_type: LeaveTypes.SICK, + date, + }, + }, + select: { id: true }, + }); + if (existing) { + throw new BadRequestException(`Sick request already exists for ${iso}`); + } + + const payable = Math.min(remaining_payable_hours, daily_payable_cap); + const payable_rounded = roundToQuarterHour(Math.max(0, payable)); + remaining_payable_hours = roundToQuarterHour( + Math.max(0, remaining_payable_hours - payable_rounded), + ); + + const row = await this.prisma.leaveRequests.create({ + data: { + employee_id: employee_id, + bank_code_id: bank_code.id, + leave_type: LeaveTypes.SICK, + comment: dto.comment ?? "", + requested_hours: requested_hours_per_day, + payable_hours: payable_rounded, + approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING, + date, + }, + select: leaveRequestsSelect, + }); + + const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); + if (row.approval_status === LeaveApprovalStatus.APPROVED) { + // await this.leaveUtils.syncShift(email, employee_id, iso, hours,LeaveTypes.SICK, row.comment); + } + + created.push({ ...mapRowToView(row), action: "create" }); + } + + return { action: "create", leave_requests: created }; + } +} diff --git a/src/time-and-attendance/leave-requests/services/vacation-leave-requests.service.ts b/src/time-and-attendance/leave-requests/services/vacation-leave-requests.service.ts new file mode 100644 index 0000000..63e0077 --- /dev/null +++ b/src/time-and-attendance/leave-requests/services/vacation-leave-requests.service.ts @@ -0,0 +1,91 @@ +import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; +import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto"; +import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client"; +import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; +import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; +import { roundToQuarterHour } from "src/common/utils/date-utils"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { VacationService } from "src/time-and-attendance/domains/services/vacation.service"; +import { PrismaService } from "src/prisma/prisma.service"; +import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; +import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils"; + + +@Injectable() +export class VacationLeaveRequestsService { + constructor( + private readonly prisma: PrismaService, + private readonly vacationService: VacationService, + private readonly emailResolver: EmailToIdResolver, + private readonly typeResolver: BankCodesResolver, + ) {} + + async create(dto: UpsertLeaveRequestDto): Promise { + const email = dto.email.trim(); + const employee_id = await this.emailResolver.findIdByEmail(email); + const bank_code = await this.typeResolver.findIdAndModifierByType(LeaveTypes.VACATION); + if(!bank_code) throw new NotFoundException(`bank_code not found`); + + const modifier = bank_code.modifier ?? 1; + const dates = normalizeDates(dto.dates); + const requested_hours_per_day = dto.requested_hours ?? 8; + if (!dates.length) throw new BadRequestException("Dates array must not be empty"); + + const entries = dates + .map((iso) => ({ iso, date: toDateOnly(iso) })) + .sort((a, b) => a.date.getTime() - b.date.getTime()); + const start_date = entries[0].date; + const total_payable_hours = await this.vacationService.calculateVacationPay( + employee_id, + start_date, + entries.length, + modifier, + ); + let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours)); + const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier); + + const created: LeaveRequestViewDto[] = []; + + for (const { iso, date } of entries) { + const existing = await this.prisma.leaveRequests.findUnique({ + where: { + leave_per_employee_date: { + employee_id: employee_id, + leave_type: LeaveTypes.VACATION, + date, + }, + }, + select: { id: true }, + }); + if (existing) throw new BadRequestException(`Vacation request already exists for ${iso}`); + + const payable = Math.min(remaining_payable_hours, daily_payable_cap); + const payable_rounded = roundToQuarterHour(Math.max(0, payable)); + remaining_payable_hours = roundToQuarterHour( + Math.max(0, remaining_payable_hours - payable_rounded), + ); + + const row = await this.prisma.leaveRequests.create({ + data: { + employee_id: employee_id, + bank_code_id: bank_code.id, + payable_hours: payable_rounded, + requested_hours: requested_hours_per_day, + leave_type: LeaveTypes.VACATION, + comment: dto.comment ?? "", + approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING, + date, + }, + select: leaveRequestsSelect, + }); + + const hours = Number(row.payable_hours ?? row.requested_hours ?? 0); + if (row.approval_status === LeaveApprovalStatus.APPROVED) { + // await this.leaveUtils.syncShift(email, employee_id, iso, hours, LeaveTypes.VACATION, row.comment); + } + created.push({ ...mapRowToView(row), action: "create" }); + } + return { action: "create", leave_requests: created }; + } +} diff --git a/src/time-and-attendance/leave-requests/utils/leave-request.transform.ts b/src/time-and-attendance/leave-requests/utils/leave-request.transform.ts new file mode 100644 index 0000000..5ff49fd --- /dev/null +++ b/src/time-and-attendance/leave-requests/utils/leave-request.transform.ts @@ -0,0 +1,20 @@ +import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; +import { mapArchiveRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests-archive.mapper"; +import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; +import { LeaveRequestArchiveRow } from "src/time-and-attendance/leave-requests/utils/leave-requests-archive.select"; +import { LeaveRequestRow } from "src/time-and-attendance/utils/type.utils"; + + +/** Active (table leave_requests) : proxy to base mapper */ +export function mapRowToViewWithDays(row: LeaveRequestRow): LeaveRequestViewDto { + return mapRowToView(row); +} + +/** Archive (table leave_requests_archive) : proxy to base mapper */ +export function mapArchiveRowToViewWithDays( + row: LeaveRequestArchiveRow, + email: string, + employee_full_name?: string, +): LeaveRequestViewDto { + return mapArchiveRowToView(row, email, employee_full_name!); +} \ No newline at end of file diff --git a/src/time-and-attendance/leave-requests/utils/leave-request.util.ts b/src/time-and-attendance/leave-requests/utils/leave-request.util.ts new file mode 100644 index 0000000..1fdceaa --- /dev/null +++ b/src/time-and-attendance/leave-requests/utils/leave-request.util.ts @@ -0,0 +1,101 @@ + +import { BadRequestException, Injectable } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; +import { LeaveTypes } from "@prisma/client"; +import { toDateFromString, toStringFromDate } from "src/time-and-attendance/utils/date-time.utils"; +@Injectable() +export class LeaveRequestsUtils { + constructor( + private readonly prisma: PrismaService, + ){} + + async syncShift( + email: string, + employee_id: number, + date: string, + hours: number, + type: LeaveTypes, + comment?: string, + ) { + if (hours <= 0) return; + + const duration_minutes = Math.round(hours * 60); + if (duration_minutes > 8 * 60) { + throw new BadRequestException("Amount of hours cannot exceed 8 hours per day."); + } + const date_only = toDateFromString(date); + const yyyy_mm_dd = toStringFromDate(date_only); + + + + const start_minutes = 8 * 60; + const end_minutes = start_minutes + duration_minutes; + const toHHmm = (total: number) => + `${String(Math.floor(total / 60)).padStart(2, "0")}:${String(total % 60).padStart(2, "0")}`; + + const existing = await this.prisma.shifts.findFirst({ + where: { + date: date_only, + bank_code: { type }, + timesheet: { employee_id: employee_id }, + }, + include: { bank_code: true }, + }); + + + // await this.shiftsService.upsertShifts(email, action, { + // old_shift: existing + // ? { + // date: yyyy_mm_dd, + // start_time: existing.start_time.toISOString().slice(11, 16), + // end_time: existing.end_time.toISOString().slice(11, 16), + // type: existing.bank_code?.type ?? type, + // is_remote: existing.is_remote, + // is_approved:existing.is_approved, + // comment: existing.comment ?? undefined, + // } + // : undefined, + // new_shift: { + // date: yyyy_mm_dd, + // start_time: toHHmm(start_minutes), + // end_time: toHHmm(end_minutes), + // is_remote: existing?.is_remote ?? false, + // is_approved:existing?.is_approved ?? false, + // comment: comment ?? existing?.comment ?? "", + // type: type, + // }, + // }); + } + + async removeShift( + email: string, + employee_id: number, + iso_date: string, + type: LeaveTypes, + ) { + const date_only = toDateFromString(iso_date); + const yyyy_mm_dd = toStringFromDate(date_only); + const existing = await this.prisma.shifts.findFirst({ + where: { + date: date_only, + bank_code: { type }, + timesheet: { employee_id: employee_id }, + }, + include: { bank_code: true }, + }); + if (!existing) return; + + // await this.shiftsService.upsertShifts(email, 'delete', { + // old_shift: { + // date: yyyy_mm_dd, + // start_time: hhmmFromLocal(existing.start_time), + // end_time: hhmmFromLocal(existing.end_time), + // type: existing.bank_code?.type ?? type, + // is_remote: existing.is_remote, + // is_approved:existing.is_approved, + // comment: existing.comment ?? undefined, + // }, + // }); + } + +} diff --git a/src/modules/leave-requests/utils/leave-requests-archive.select.ts b/src/time-and-attendance/leave-requests/utils/leave-requests-archive.select.ts similarity index 100% rename from src/modules/leave-requests/utils/leave-requests-archive.select.ts rename to src/time-and-attendance/leave-requests/utils/leave-requests-archive.select.ts diff --git a/src/time-and-attendance/pay-period/controllers/pay-periods.controller.ts b/src/time-and-attendance/pay-period/controllers/pay-periods.controller.ts new file mode 100644 index 0000000..1d43791 --- /dev/null +++ b/src/time-and-attendance/pay-period/controllers/pay-periods.controller.ts @@ -0,0 +1,69 @@ +import { Body, Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query, Req, UnauthorizedException } from "@nestjs/common"; +import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto"; +import { PayPeriodsQueryService } from "../services/pay-periods-query.service"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { Roles as RoleEnum } from '.prisma/client'; +import { PayPeriodsCommandService } from "../services/pay-periods-command.service"; +import { PayPeriodBundleDto } from "../dtos/bundle-pay-period.dto"; +import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto"; + + +@Controller('pay-periods') +export class PayPeriodsController { + + constructor( + private readonly queryService: PayPeriodsQueryService, + private readonly commandService: PayPeriodsCommandService, + ) { } + + @Get('current-and-all') + async getCurrentAndAll(@Query('date') date?: string): Promise { + const [current, periods] = await Promise.all([ + this.queryService.findCurrent(date), + this.queryService.findAll(), + ]); + return { current, periods }; + } + + @Get("date/:date") + async findByDate(@Param("date") date: string) { + return this.queryService.findByDate(date); + } + + @Get(":year/:periodNumber") + async findOneByYear( + @Param("year", ParseIntPipe) year: number, + @Param("periodNumber", ParseIntPipe) period_no: number, + ) { + return this.queryService.findOneByYearPeriod(year, period_no); + } + + @Patch("crew/pay-period-approval") + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + async bulkApproval(@Req() req, @Body() dto: BulkCrewApprovalDto) { + const email = req.user?.email; + if(!email) throw new UnauthorizedException(`Session infos not found`); + return this.commandService.bulkApproveCrew(email, dto); + } + + @Get('crew/:year/:periodNumber') + @RolesAllowed(RoleEnum.SUPERVISOR) + async getCrewOverview(@Req() req, + @Param('year', ParseIntPipe) year: number, + @Param('periodNumber', ParseIntPipe) period_no: number, + @Query('includeSubtree', new ParseBoolPipe({ optional: true })) include_subtree = false, + ): Promise { + const email = req.user?.email; + if(!email) throw new UnauthorizedException(`Session infos not found`); + return this.queryService.getCrewOverview(year, period_no, email, include_subtree); + } + + @Get('overview/:year/:periodNumber') + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + async getOverviewByYear( + @Param('year', ParseIntPipe) year: number, + @Param('periodNumber', ParseIntPipe) period_no: number, + ): Promise { + return this.queryService.getOverviewByYearPeriod(year, period_no); + } +} diff --git a/src/modules/pay-periods/dtos/bulk-crew-approval.dto.ts b/src/time-and-attendance/pay-period/dtos/bulk-crew-approval.dto.ts similarity index 76% rename from src/modules/pay-periods/dtos/bulk-crew-approval.dto.ts rename to src/time-and-attendance/pay-period/dtos/bulk-crew-approval.dto.ts index 3762ddb..4ba5527 100644 --- a/src/modules/pay-periods/dtos/bulk-crew-approval.dto.ts +++ b/src/time-and-attendance/pay-period/dtos/bulk-crew-approval.dto.ts @@ -1,5 +1,5 @@ import { Type } from "class-transformer"; -import { IsArray, IsBoolean, IsEmail, IsInt, IsOptional, ValidateNested } from "class-validator"; +import { IsArray, IsBoolean, IsEmail, IsInt, ValidateNested } from "class-validator"; export class BulkCrewApprovalItemDto { @IsInt() @@ -16,9 +16,6 @@ export class BulkCrewApprovalItemDto { } export class BulkCrewApprovalDto { - @IsEmail() - supervisor_email: string; - @IsBoolean() include_subtree: boolean = false; diff --git a/src/time-and-attendance/pay-period/dtos/bundle-pay-period.dto.ts b/src/time-and-attendance/pay-period/dtos/bundle-pay-period.dto.ts new file mode 100644 index 0000000..ad84088 --- /dev/null +++ b/src/time-and-attendance/pay-period/dtos/bundle-pay-period.dto.ts @@ -0,0 +1,6 @@ +import { PayPeriodDto } from "./pay-period.dto"; + +export class PayPeriodBundleDto { + current: PayPeriodDto; + periods: PayPeriodDto[]; +} \ No newline at end of file diff --git a/src/time-and-attendance/pay-period/dtos/overview-employee-period.dto.ts b/src/time-and-attendance/pay-period/dtos/overview-employee-period.dto.ts new file mode 100644 index 0000000..4cae27b --- /dev/null +++ b/src/time-and-attendance/pay-period/dtos/overview-employee-period.dto.ts @@ -0,0 +1,23 @@ +export class EmployeePeriodOverviewDto { + email: string; + employee_name: string; + regular_hours: number; + other_hours: { + evening_hours: number; + + emergency_hours: number; + + overtime_hours: number; + + sick_hours: number; + + holiday_hours: number; + + vacation_hours: number; + }; + total_hours: number; + expenses: number; + mileage: number; + is_approved: boolean; + is_remote: boolean; +} diff --git a/src/time-and-attendance/pay-period/dtos/overview-pay-period.dto.ts b/src/time-and-attendance/pay-period/dtos/overview-pay-period.dto.ts new file mode 100644 index 0000000..3748eb8 --- /dev/null +++ b/src/time-and-attendance/pay-period/dtos/overview-pay-period.dto.ts @@ -0,0 +1,11 @@ +import { EmployeePeriodOverviewDto } from './overview-employee-period.dto'; + +export class PayPeriodOverviewDto { + pay_period_no: number; + pay_year: number; + period_start: string; + period_end: string; + payday: string; + label: string; + employees_overview: EmployeePeriodOverviewDto[]; +} diff --git a/src/time-and-attendance/pay-period/dtos/pay-period.dto.ts b/src/time-and-attendance/pay-period/dtos/pay-period.dto.ts new file mode 100644 index 0000000..a85f481 --- /dev/null +++ b/src/time-and-attendance/pay-period/dtos/pay-period.dto.ts @@ -0,0 +1,8 @@ +export class PayPeriodDto { + pay_period_no: number; + period_start: string; + period_end: string; + payday: string; + pay_year: number; + label: string; +} \ No newline at end of file diff --git a/src/modules/pay-periods/mappers/pay-periods.mapper.ts b/src/time-and-attendance/pay-period/mappers/pay-periods.mapper.ts similarity index 100% rename from src/modules/pay-periods/mappers/pay-periods.mapper.ts rename to src/time-and-attendance/pay-period/mappers/pay-periods.mapper.ts diff --git a/src/time-and-attendance/pay-period/pay-periods.module.ts b/src/time-and-attendance/pay-period/pay-periods.module.ts new file mode 100644 index 0000000..aa49f23 --- /dev/null +++ b/src/time-and-attendance/pay-period/pay-periods.module.ts @@ -0,0 +1,18 @@ +import { PayPeriodsQueryService } from "./services/pay-periods-query.service"; +import { PayPeriodsController } from "./controllers/pay-periods.controller"; +import { Module } from "@nestjs/common"; +import { PayPeriodsCommandService } from "src/time-and-attendance/pay-period/services/pay-periods-command.service"; +import { TimesheetsModule } from "src/time-and-attendance/time-tracker/timesheets/timesheets.module"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; + +@Module({ + imports:[TimesheetsModule], + controllers: [PayPeriodsController], + providers: [ + PayPeriodsQueryService, + PayPeriodsCommandService, + EmailToIdResolver, + ], +}) + +export class PayperiodsModule {} diff --git a/src/time-and-attendance/pay-period/services/pay-periods-command.service.ts b/src/time-and-attendance/pay-period/services/pay-periods-command.service.ts new file mode 100644 index 0000000..ede4caa --- /dev/null +++ b/src/time-and-attendance/pay-period/services/pay-periods-command.service.ts @@ -0,0 +1,72 @@ +import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; +import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto"; +import { PayPeriodsQueryService } from "./pay-periods-query.service"; +import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service"; + +@Injectable() +export class PayPeriodsCommandService { + constructor( + private readonly prisma: PrismaService, + private readonly timesheetsApproval: TimesheetApprovalService, + private readonly query: PayPeriodsQueryService, + ) {} + + //function to approve pay-periods according to selected crew members + async bulkApproveCrew(email: string, dto:BulkCrewApprovalDto): Promise<{updated: number}> { + const { include_subtree, items } = dto; + if(!items?.length) throw new BadRequestException('no items to process'); + + //fetch and validate supervisor status + const supervisor = await this.query.getSupervisor(email); + if(!supervisor) throw new NotFoundException('No employee record linked to current user'); + if(!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor'); + + //fetches emails of crew members linked to supervisor + const crew_emails = await this.query.resolveCrewEmails(supervisor.id, include_subtree); + + + for(const item of items) { + if(!crew_emails.has(item.employee_email)) { + throw new ForbiddenException(`Employee ${item.employee_email} not in supervisor crew`); + } + } + + const period_cache = new Map(); + const getPeriod = async (year:number, period_no: number) => { + const key = `${year}-${period_no}`; + if(period_cache.has(key)) return period_cache.get(key)!; + + const period = await this.query.getPeriodWindow(year,period_no); + if(!period) throw new NotFoundException(`Pay period ${year}-${period_no} not found`); + period_cache.set(key, period); + return period; + }; + + let updated = 0; + + await this.prisma.$transaction(async (transaction) => { + for(const item of items) { + const { period_start, period_end } = await getPeriod(item.pay_year, item.period_no); + + const timesheets = await transaction.timesheets.findMany({ + where: { + employee: { user: { email: item.employee_email } }, + OR: [ + {shift : { some: { date: { gte: period_start, lte: period_end } } } }, + {expense: { some: { date: { gte: period_start, lte: period_end } } } }, + ], + }, + select: { id: true }, + }); + + for(const { id } of timesheets) { + await this.timesheetsApproval.cascadeApprovalWithtx(transaction, id, item.approve); + updated++; + } + + } + }); + return {updated}; + } +} \ No newline at end of file diff --git a/src/modules/pay-periods/services/pay-periods-query.service.ts b/src/time-and-attendance/pay-period/services/pay-periods-query.service.ts similarity index 99% rename from src/modules/pay-periods/services/pay-periods-query.service.ts rename to src/time-and-attendance/pay-period/services/pay-periods-query.service.ts index 0e6aac0..86ce75e 100644 --- a/src/modules/pay-periods/services/pay-periods-query.service.ts +++ b/src/time-and-attendance/pay-period/services/pay-periods-query.service.ts @@ -3,9 +3,9 @@ import { PrismaService } from "src/prisma/prisma.service"; import { computeHours } from "src/common/utils/date-utils"; import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto"; import { EmployeePeriodOverviewDto } from "../dtos/overview-employee-period.dto"; -import { computePeriod, listPayYear, payYearOfDate } from "../utils/pay-year.util"; import { PayPeriodDto } from "../dtos/pay-period.dto"; import { mapPayPeriodToDto } from "../mappers/pay-periods.mapper"; +import { computePeriod, listPayYear, payYearOfDate } from "src/time-and-attendance/utils/date-time.utils"; @Injectable() export class PayPeriodsQueryService { diff --git a/src/time-and-attendance/time-and-attendance.module.ts b/src/time-and-attendance/time-and-attendance.module.ts new file mode 100644 index 0000000..5d03b59 --- /dev/null +++ b/src/time-and-attendance/time-and-attendance.module.ts @@ -0,0 +1,46 @@ + +import { Module } from "@nestjs/common"; +import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module"; +import { ExpenseController } from "src/time-and-attendance/expenses/controllers/expense.controller"; +import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service"; +import { PayperiodsModule } from "src/time-and-attendance/pay-period/pay-periods.module"; +import { SchedulePresetsController } from "src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller"; +import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service"; +import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service"; +import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; +import { ShiftController } from "src/time-and-attendance/time-tracker/shifts/controllers/shift.controller"; +import { ShiftsGetService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-get.service"; +import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service"; +import { TimesheetController } from "src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller"; +import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service"; +import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service"; +import { TimesheetsModule } from "src/time-and-attendance/time-tracker/timesheets/timesheets.module"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; + +@Module({ + imports: [ + BusinessLogicsModule, + PayperiodsModule, + TimesheetsModule, + ], + controllers: [ + TimesheetController, + ShiftController, + SchedulePresetsController, + ExpenseController, + ], + providers: [ + GetTimesheetsOverviewService, + ShiftsGetService, + ShiftsUpsertService, + ExpenseUpsertService, + SchedulePresetsUpsertService, + SchedulePresetsGetService, + SchedulePresetsApplyService, + EmailToIdResolver, + BankCodesResolver, + TimesheetApprovalService, + ], + exports: [TimesheetApprovalService ], +}) export class TimeAndAttendanceModule { }; \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts b/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts new file mode 100644 index 0000000..a4a7b9e --- /dev/null +++ b/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts @@ -0,0 +1,64 @@ +import { Controller, Param, Query, Body, Get, Post, ParseIntPipe, Delete, Patch, Req } from "@nestjs/common"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; +import { SchedulePresetsUpdateDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/update-schedule-presets.dto"; +import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service"; +import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service"; +import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; +import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes"; + +@Controller('schedule-presets') +@RolesAllowed(...GLOBAL_CONTROLLER_ROLES) +export class SchedulePresetsController { + constructor( + private readonly upsertService: SchedulePresetsUpsertService, + private readonly getService: SchedulePresetsGetService, + private readonly applyPresetsService: SchedulePresetsApplyService, + ) { } + + //used to create a schedule preset + @Post('create') + @RolesAllowed(...MANAGER_ROLES) + async createPreset(@Req() req, @Body() dto: SchedulePresetsDto) { + const email = req.user?.email; + return await this.upsertService.createPreset(email, dto); + } + + //used to update an already existing schedule preset + @Patch('update/:preset_id') + @RolesAllowed(...MANAGER_ROLES) + async updatePreset( + @Param('preset_id', ParseIntPipe) preset_id: number, + @Body() dto: SchedulePresetsUpdateDto + ) { + return await this.upsertService.updatePreset(preset_id, dto); + } + + //used to delete a schedule preset + @Delete('delete/:preset_id') + @RolesAllowed(...MANAGER_ROLES) + async deletePreset( + @Param('preset_id', ParseIntPipe) preset_id: number) { + return await this.upsertService.deletePreset(preset_id); + } + + + //used to show the list of available schedule presets + @Get('find-list') + @RolesAllowed(...MANAGER_ROLES) + async findListById(@Req() req) { + const email = req.user?.email; + return this.getService.getSchedulePresets(email); + } + + //used to apply a preset to a timesheet + @Post('apply-presets') + async applyPresets( + @Req() req, + @Body('preset') preset_id: number, + @Body('start') start_date: string + ) { + const email = req.user?.email; + return this.applyPresetsService.applyToTimesheet(email, preset_id, start_date); + } +} \ No newline at end of file diff --git a/src/modules/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts b/src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts similarity index 81% rename from src/modules/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts rename to src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts index 33c06cd..5e37dcb 100644 --- a/src/modules/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts +++ b/src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts @@ -1,10 +1,14 @@ import { IsBoolean, IsEnum, IsInt, IsOptional, IsString, Matches, Min } from "class-validator"; +import { HH_MM_REGEX } from "src/time-and-attendance/utils/constants.utils"; import { Weekday } from "@prisma/client"; export class SchedulePresetShiftsDto { @IsEnum(Weekday) week_day!: Weekday; + @IsInt() + preset_id!: number; + @IsInt() @Min(1) sort_order!: number; diff --git a/src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto.ts b/src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto.ts new file mode 100644 index 0000000..fb58f69 --- /dev/null +++ b/src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto.ts @@ -0,0 +1,19 @@ +import { ArrayMinSize, IsArray, IsBoolean, IsInt, IsOptional, IsString } from "class-validator"; +import { SchedulePresetShiftsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto"; + +export class SchedulePresetsDto { + + @IsInt() + id!: number; + + @IsString() + name!: string; + + @IsBoolean() + @IsOptional() + is_default: boolean; + + @IsArray() + @ArrayMinSize(1) + preset_shifts: SchedulePresetShiftsDto[]; +} \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/schedule-presets/dtos/update-schedule-presets.dto.ts b/src/time-and-attendance/time-tracker/schedule-presets/dtos/update-schedule-presets.dto.ts new file mode 100644 index 0000000..b8b9cb6 --- /dev/null +++ b/src/time-and-attendance/time-tracker/schedule-presets/dtos/update-schedule-presets.dto.ts @@ -0,0 +1,4 @@ +import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; + + +export class SchedulePresetsUpdateDto extends SchedulePresetsDto{} \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/schedule-presets/schedule-presets.module.ts b/src/time-and-attendance/time-tracker/schedule-presets/schedule-presets.module.ts new file mode 100644 index 0000000..b17863a --- /dev/null +++ b/src/time-and-attendance/time-tracker/schedule-presets/schedule-presets.module.ts @@ -0,0 +1,21 @@ + +import { Module } from "@nestjs/common"; +import { SchedulePresetsController } from "src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller"; +import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service"; +import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service"; +import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; + + +@Module({ + controllers: [SchedulePresetsController], + providers: [ + SchedulePresetsUpsertService, + SchedulePresetsGetService, + SchedulePresetsApplyService, + ], + exports:[ + SchedulePresetsUpsertService, + SchedulePresetsGetService, + SchedulePresetsApplyService, + ], +}) export class SchedulePresetsModule {} \ No newline at end of file diff --git a/src/modules/schedule-presets/services/schedule-presets-apply.service.ts b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts similarity index 84% rename from src/modules/schedule-presets/services/schedule-presets-apply.service.ts rename to src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts index fa2bce1..122c30c 100644 --- a/src/modules/schedule-presets/services/schedule-presets-apply.service.ts +++ b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts @@ -1,34 +1,29 @@ -import { BadRequestException, ConflictException, Injectable, NotFoundException } from "@nestjs/common"; -import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; +import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common"; +import { Weekday, Prisma } from "@prisma/client"; +import { DATE_ISO_FORMAT } from "src/time-and-attendance/utils/constants.utils"; import { PrismaService } from "src/prisma/prisma.service"; -import { ApplyResult } from "../types/schedule-presets.types"; -import { Prisma, Weekday } from "@prisma/client"; -import { WEEKDAY } from "../mappers/schedule-presets.mappers"; +import { ApplyResult } from "src/time-and-attendance/utils/type.utils"; +import { WEEKDAY } from "src/time-and-attendance/utils/mappers.utils"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; + @Injectable() export class SchedulePresetsApplyService { - constructor( - private readonly prisma: PrismaService, - private readonly emailResolver: EmailToIdResolver, - ) {} + constructor( private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver) {} - async applyToTimesheet( - email: string, - preset_name: string, - start_date_iso: string, - ): Promise { - if(!preset_name?.trim()) throw new BadRequestException('A preset_name is required'); + async applyToTimesheet( email: string, id: number, start_date_iso: string ): Promise { + if(!id) throw new BadRequestException(`Schedule preset with id: ${id} not found`); if(!DATE_ISO_FORMAT.test(start_date_iso)) throw new BadRequestException('start_date must be of format :YYYY-MM-DD'); const employee_id = await this.emailResolver.findIdByEmail(email); - if(!employee_id) throw new NotFoundException(`Employee with email: ${email} not found`); - + const preset = await this.prisma.schedulePresets.findFirst({ - where: { employee_id, name: preset_name }, + where: { employee_id, id }, include: { shifts: { orderBy: [{ week_day: 'asc'}, { sort_order: 'asc'}], select: { + id: true, week_day: true, sort_order: true, start_time: true, diff --git a/src/modules/schedule-presets/services/schedule-presets-query.service.ts b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts similarity index 73% rename from src/modules/schedule-presets/services/schedule-presets-query.service.ts rename to src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts index 7ccb0f0..3bb3050 100644 --- a/src/modules/schedule-presets/services/schedule-presets-query.service.ts +++ b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts @@ -1,21 +1,19 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; -import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; +import { PresetResponse, ShiftResponse } from "src/time-and-attendance/utils/type.utils"; import { PrismaService } from "src/prisma/prisma.service"; -import { PresetResponse, ShiftResponse } from "../types/schedule-presets.types"; +import { Injectable } from "@nestjs/common"; import { Prisma } from "@prisma/client"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; @Injectable() -export class SchedulePresetsQueryService { - constructor( +export class SchedulePresetsGetService { + constructor( private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver, - ){} - - async findSchedulePresetsByEmail(email:string): Promise { - const employee_id = await this.emailResolver.findIdByEmail(email); - if(!employee_id) throw new NotFoundException(`Employee with email: ${email} not found`); + ){} + async getSchedulePresets(email: string): Promise { try { + const employee_id = await this.emailResolver.findIdByEmail(email); const presets = await this.prisma.schedulePresets.findMany({ where: { employee_id }, orderBy: [{is_default: 'desc' }, { name: 'asc' }], diff --git a/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts new file mode 100644 index 0000000..c590d57 --- /dev/null +++ b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts @@ -0,0 +1,215 @@ +import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common"; +import { CreatePresetResult, DeletePresetResult, UpdatePresetResult } from "src/time-and-attendance/utils/type.utils"; +import { Prisma, Weekday } from "@prisma/client"; +import { toHHmmFromDate } from "src/time-and-attendance/utils/date-time.utils"; +import { PrismaService } from "src/prisma/prisma.service"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; + +@Injectable() +export class SchedulePresetsUpsertService { + constructor( + private readonly prisma: PrismaService, + private readonly typeResolver: BankCodesResolver, + private readonly emailResolver: EmailToIdResolver, + ) { } + //_________________________________________________________________ + // CREATE + //_________________________________________________________________ + async createPreset(email: string, dto: SchedulePresetsDto): Promise { + try { + const shifts_data = await this.resolveAndBuildPresetShifts(dto); + const employee_id = await this.emailResolver.findIdByEmail(email); + if (!shifts_data) throw new BadRequestException(`Employee with email: ${email} or dto not found`); + + await this.prisma.$transaction(async (tx) => { + if (dto.is_default) { + await tx.schedulePresets.updateMany({ + where: { is_default: true, employee_id }, + data: { is_default: false }, + }); + } + const created = await tx.schedulePresets.create({ + data: { + id: dto.id, + employee_id, + name: dto.name, + is_default: !!dto.is_default, + shifts: { create: shifts_data }, + }, + }); + return created; + }); + return { ok: true }; + + } catch (error: unknown) { + return { ok: false, error }; + } + } + + //_________________________________________________________________ + // UPDATE + //_________________________________________________________________ + async updatePreset(preset_id: number, dto: SchedulePresetsDto): Promise { + try { + const existing = await this.prisma.schedulePresets.findFirst({ + where: { id: preset_id }, + select: { + id: true, + is_default: true, + employee_id: true, + }, + }); + if (!existing) throw new NotFoundException(`Preset "${dto.name}" not found`); + + const shifts_data = await this.resolveAndBuildPresetShifts(dto); + await this.prisma.$transaction(async (tx) => { + if (typeof dto.is_default === 'boolean') { + if (dto.is_default) { + await tx.schedulePresets.updateMany({ + where: { + employee_id: existing.employee_id, + is_default: true, + NOT: { id: existing.id }, + }, + data: { is_default: false }, + }); + } + await tx.schedulePresets.update({ + where: { id: existing.id }, + data: { + is_default: dto.is_default, + name: dto.name, + }, + }); + } + if (shifts_data.length <= 0) throw new BadRequestException('Preset shifts to update not found'); + + await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } }); + + const create_many_data: Prisma.SchedulePresetShiftsCreateManyInput[] = + shifts_data.map((shift) => { + if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') { + throw new NotFoundException(`Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`); + } + const bank_code_id = shift.bank_code.connect.id; + return { + preset_id: existing.id, + week_day: shift.week_day, + sort_order: shift.sort_order, + start_time: shift.start_time, + end_time: shift.end_time, + is_remote: shift.is_remote ?? false, + bank_code_id: bank_code_id, + }; + }); + await tx.schedulePresetShifts.createMany({ data: create_many_data }); + }); + + const saved = await this.prisma.schedulePresets.findUnique({ + where: { id: existing.id }, + include: { + shifts: { + orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }], + include: { bank_code: { select: { type: true } } }, + } + }, + }); + if (!saved) throw new NotFoundException(`Preset with id: ${existing.id} not found`); + + const response_dto: SchedulePresetsDto = { + id: saved.id, + name: saved.name, + is_default: saved.is_default, + preset_shifts: saved.shifts.map((shift) => ({ + preset_id: shift.preset_id, + week_day: shift.week_day, + sort_order: shift.sort_order, + type: shift.bank_code.type, + start_time: toHHmmFromDate(shift.start_time), + end_time: toHHmmFromDate(shift.end_time), + is_remote: shift.is_remote, + })), + }; + + return { ok: true, id: existing.id, data: response_dto }; + } catch (error) { + return { ok: false, id: preset_id, error } + } + } + + //_________________________________________________________________ + // DELETE + //_________________________________________________________________ + async deletePreset(preset_id: number): Promise { + try { + await this.prisma.$transaction(async (tx) => { + const preset = await tx.schedulePresets.findFirst({ + where: { id: preset_id }, + select: { id: true }, + }); + if (!preset) throw new NotFoundException(`Preset with id ${preset_id} not found`); + await tx.schedulePresets.delete({ where: { id: preset_id } }); + + return { success: true }; + }); + return { ok: true, id: preset_id }; + + } catch (error) { + if (error) throw new NotFoundException(`Preset schedule with id ${preset_id} not found`); + return { ok: false, id: preset_id, error }; + } + } + + //PRIVATE HELPERS + + //resolve bank_code_id using type and convert hours to TIME and valid shifts end/start + private async resolveAndBuildPresetShifts( + dto: SchedulePresetsDto + ): Promise { + + if (!dto.preset_shifts?.length) throw new NotFoundException(`Empty or preset shifts not found`); + + const types = Array.from(new Set(dto.preset_shifts.map((shift) => shift.type))); + const bank_code_set = new Map(); + + for (const type of types) { + const { id } = await this.typeResolver.findIdAndModifierByType(type); + bank_code_set.set(type, id) + } + const toTime = (hhmm: string) => new Date(`1970-01-01T${hhmm}:00.000Z`); + + const pair_set = new Set(); + for (const shift of dto.preset_shifts) { + const key = `${shift.week_day}:${shift.sort_order}`; + if (pair_set.has(key)) { + throw new ConflictException(`Duplicate shift for day/order (${shift.week_day}, ${shift.sort_order})`); + } + pair_set.add(key); + } + + const items: Prisma.SchedulePresetShiftsCreateWithoutPresetInput[] = dto.preset_shifts.map((shift) => { + const bank_code_id = bank_code_set.get(shift.type); + if (!bank_code_id) throw new NotFoundException(`Bank code not found for type ${shift.type}`); + if (!shift.start_time || !shift.end_time) { + throw new BadRequestException(`start_time and end_time are required for (${shift.week_day}, ${shift.sort_order})`); + } + const start = toTime(shift.start_time); + const end = toTime(shift.end_time); + if (end.getTime() <= start.getTime()) { + throw new ConflictException(`end_time must be > start_time ( day: ${shift.week_day}, order: ${shift.sort_order})`); + } + + return { + week_day: shift.week_day as Weekday, + sort_order: shift.sort_order, + bank_code: { connect: { id: bank_code_id } }, + start_time: start, + end_time: end, + is_remote: !!shift.is_remote, + }; + }); + return items; + } +} diff --git a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts new file mode 100644 index 0000000..06b159d --- /dev/null +++ b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts @@ -0,0 +1,36 @@ +import { BadRequestException, Body, Controller, Delete, Param, Patch, Post, Req } from "@nestjs/common"; +import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; +import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service"; +import { CreateShiftResult, UpdateShiftResult } from "src/time-and-attendance/utils/type.utils"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { GLOBAL_CONTROLLER_ROLES } from "src/common/shared/role-groupes"; + + +@Controller('shift') +@RolesAllowed(...GLOBAL_CONTROLLER_ROLES) +export class ShiftController { + constructor( + private readonly upsert_service: ShiftsUpsertService, + ){} + + @Post('create') + createBatch( @Req() req, @Body()dtos: ShiftDto[]): Promise { + const email = req.user?.email; + const list = Array.isArray(dtos) ? dtos : []; + if(list.length === 0) throw new BadRequestException('Body is missing or invalid (create shifts)'); + return this.upsert_service.createShifts(email, dtos) + } + + @Patch('update') + updateBatch( @Body() dtos: ShiftDto[]): Promise{ + const list = Array.isArray(dtos) ? dtos: []; + if(list.length === 0) throw new BadRequestException('Body is missing or invalid (update shifts)'); + return this.upsert_service.updateShifts(dtos); + } + + @Delete(':shift_id') + remove(@Param('shift_id') shift_id: number ) { + return this.upsert_service.deleteShift(shift_id); + } + +} diff --git a/src/modules/shifts/dtos/shift.dto.ts b/src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto.ts similarity index 85% rename from src/modules/shifts/dtos/shift.dto.ts rename to src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto.ts index 65413da..c762b0f 100644 --- a/src/modules/shifts/dtos/shift.dto.ts +++ b/src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto.ts @@ -1,8 +1,9 @@ import { IsBoolean, IsInt, IsOptional, IsString, MaxLength } from "class-validator"; export class ShiftDto { + @IsInt() @IsOptional() id: number; @IsInt() timesheet_id!: number; - @IsInt() bank_code_id!: number; + @IsString() type!: string; @IsString() date!: string; @IsString() start_time!: string; diff --git a/src/modules/shared/interfaces/shifts.interface.ts b/src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto.ts similarity index 59% rename from src/modules/shared/interfaces/shifts.interface.ts rename to src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto.ts index 40f897e..f2b833f 100644 --- a/src/modules/shared/interfaces/shifts.interface.ts +++ b/src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto.ts @@ -1,9 +1,11 @@ -export interface ShiftKey { +export class ShiftEntity { + id: number; timesheet_id: number; + bank_code_id: number; date: Date; start_time: Date; end_time: Date; - bank_code_id: number; is_remote: boolean; - comment?: string | null; -} \ No newline at end of file + is_approved: boolean; + comment?: string; +} diff --git a/src/modules/shifts/dtos/get-shift.dto.ts b/src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto.ts similarity index 55% rename from src/modules/shifts/dtos/get-shift.dto.ts rename to src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto.ts index 21b479f..c5fd877 100644 --- a/src/modules/shifts/dtos/get-shift.dto.ts +++ b/src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto.ts @@ -1,10 +1,11 @@ export class GetShiftDto { + shift_id: number; timesheet_id: number; - bank_code_id: number; - date: string; + type: string; + date: string; start_time: string; - end_time: string; - is_remote: boolean; + end_time: string; + is_remote: boolean; is_approved: boolean; comment?: string; } diff --git a/src/modules/shifts/services/shifts-archival.service.ts b/src/time-and-attendance/time-tracker/shifts/services/shifts-archival.service.ts similarity index 85% rename from src/modules/shifts/services/shifts-archival.service.ts rename to src/time-and-attendance/time-tracker/shifts/services/shifts-archival.service.ts index 667ba3a..a5a833f 100644 --- a/src/modules/shifts/services/shifts-archival.service.ts +++ b/src/time-and-attendance/time-tracker/shifts/services/shifts-archival.service.ts @@ -1,6 +1,18 @@ -import { ShiftsArchive } from "@prisma/client"; import { PrismaService } from "src/prisma/prisma.service"; +import { ShiftsArchive } from "@prisma/client"; +import { Injectable } from "@nestjs/common"; +/** + * _____________________________________________________________________________________ + * + * + * This service is not used. Will be use to atrchive a list of shifts using a cron job. + * + * + * _____________________________________________________________________________________ + */ + +@Injectable() export class ShiftsArchivalService { constructor(private readonly prisma: PrismaService){} diff --git a/src/time-and-attendance/time-tracker/shifts/services/shifts-get.service.ts b/src/time-and-attendance/time-tracker/shifts/services/shifts-get.service.ts new file mode 100644 index 0000000..96ae516 --- /dev/null +++ b/src/time-and-attendance/time-tracker/shifts/services/shifts-get.service.ts @@ -0,0 +1,60 @@ +import { toStringFromDate, toStringFromHHmm } from "src/time-and-attendance/utils/date-time.utils"; +import { Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; +import { shift_select } from "src/time-and-attendance/utils/selects.utils"; +import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto"; + + +/** + * _____________________________________________________________________________________ + * + * + * This service is not used. Could be use to show a list of shifts. + * + * For the moment, the module Timesheets is used to display shifts, filtered by employee + * + * _____________________________________________________________________________________ + */ + + +@Injectable() +export class ShiftsGetService { + constructor( + private readonly prisma: PrismaService, + ){} + + //fetch a shift using shift_id and return all that shift's info + // async getShiftByShiftId(shift_ids: number[]): Promise { + // if(!Array.isArray(shift_ids) || shift_ids.length === 0) return []; + + // const rows = await this.prisma.shifts.findMany({ + // where: { id: { in: shift_ids } }, + // select: shift_select, + // }); + + // if(rows.length !== shift_ids.length) { + // const found_ids = new Set(rows.map(row => row.id)); + // const missing_ids = shift_ids.filter(id => !found_ids.has(id)); + // throw new NotFoundException(`Shift(s) not found: ${ missing_ids.join(", ")}`); + // } + + // const row_by_id = new Map(rows.map(row => [row.id, row])); + + // return shift_ids.map((id) => { + // const shift = row_by_id.get(id)!; + // return { + // timesheet_id: shift.timesheet_id, + // type: shift.bank_code.type, + // date: toStringFromDate(shift.date), + // start_time: toStringFromHHmm(shift.start_time), + // end_time: toStringFromHHmm(shift.end_time), + // is_remote: shift.is_remote, + // is_approved: shift.is_approved, + // comment: shift.comment ?? undefined, + // } satisfies GetShiftDto; + // }); + + + + // } +} \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts b/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts new file mode 100644 index 0000000..7a857b2 --- /dev/null +++ b/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts @@ -0,0 +1,463 @@ +import { CreateShiftResult, NormedOk, UpdateShiftResult, Normalized } from "src/time-and-attendance/utils/type.utils"; +import { overlaps, toStringFromHHmm, toStringFromDate, toDateFromString, toHHmmFromString } from "src/time-and-attendance/utils/date-time.utils"; +import { Injectable, BadRequestException, ConflictException, NotFoundException } from "@nestjs/common"; +import { shift_select, timesheet_select } from "src/time-and-attendance/utils/selects.utils"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { PrismaService } from "src/prisma/prisma.service"; +import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto"; +import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto"; +import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; + +@Injectable() +export class ShiftsUpsertService { + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver: EmailToIdResolver, + private readonly typeResolver: BankCodesResolver, + ) { } + + //_________________________________________________________________ + // CREATE + //_________________________________________________________________ + //normalized frontend data to match DB + //loads all shifts from a selected day to check for overlaping shifts + //checks for overlaping shifts + //create new shifts + async createShifts(email: string, dtos: ShiftDto[]): Promise { + if (!Array.isArray(dtos) || dtos.length === 0) return []; + + const employee_id = await this.emailResolver.findIdByEmail(email); + const results: CreateShiftResult[] = []; + const normed_shifts: (NormedOk | undefined)[] = await Promise.all(dtos.map(async (dto, index) => { + try { + const normed = await this.normalizeShiftDto(dto); + if (normed.end_time <= normed.start_time) { + const error = { + error_code: 'SHIFT_OVERLAP', + conflicts: { + start_time: toStringFromHHmm(normed.start_time), + end_time: toStringFromHHmm(normed.end_time), + date: toStringFromDate(normed.date), + }, + }; + results.push({ ok: false, error }); + } + + const timesheet = await this.prisma.timesheets.findUnique({ + where: { id: dto.timesheet_id, employee_id }, + select: timesheet_select, + }); + if (!timesheet) { + const error = { + error_code: 'INVALID_TIMESHEET', + conflicts: { + start_time: toStringFromHHmm(normed.start_time), + end_time: toStringFromHHmm(normed.end_time), + date: toStringFromDate(normed.date), + }, + }; + results.push({ ok: false, error }); + return; + } + const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type); + const date = await toDateFromString(dto.date); + const start_time = await toHHmmFromString(dto.start_time); + const end_time = await toHHmmFromString(dto.end_time); + const entity: ShiftEntity = { + timesheet_id: timesheet.id, + bank_code_id: bank_code.id, + date, + start_time, + end_time, + id: dto.id, + is_approved: dto.is_approved, + is_remote: dto.is_remote, + }; + + return { + index, + dto: entity, + normed, + timesheet_id: timesheet.id, + }; + } catch (error) { + results.push({ ok: false, error }); + return; + } + + })); + + const ok_items = normed_shifts.filter((item) => item !== undefined); + + + const regroup_by_date = new Map(); + ok_items.forEach(({ index, normed, timesheet_id }) => { + const day = new Date(normed.date.getUTCFullYear(), normed.date.getUTCMonth(), normed.date.getUTCDate()).getTime(); + const key = `${timesheet_id}|${day}`; + if (!regroup_by_date.has(key)) regroup_by_date.set(key, []); + regroup_by_date.get(key)!.push(index); + }); + + const timesheet_keys = Array.from(regroup_by_date.keys()).map((raw) => { + const [timesheet, day] = raw.split('|'); + return { + timesheet_id: Number(timesheet), + day: Number(day), + key: raw, + }; + }); + + for (const indices of regroup_by_date.values()) { + const ordered = indices + .map(index => { + const item = normed_shifts[index] as NormedOk & { timesheet_id: number }; + return { + index: index, + start: item.normed.start_time, + end: item.normed.end_time, + date: item.normed.date, + }; + }) + .sort((a, b) => a.start.getTime() - b.start.getTime()); + + for (let j = 1; j < ordered.length; j++) { + if ( + overlaps( + { start: ordered[j - 1].start, end: ordered[j - 1].end, date: ordered[j - 1].date }, + { start: ordered[j].start, end: ordered[j].end, date: ordered[j].date }, + ) + ) { + const error = new ConflictException({ + error_code: 'SHIFT_OVERLAP', + conflicts: { + start_time: toStringFromHHmm(ordered[j].start), + end_time: toStringFromHHmm(ordered[j].end), + date: toStringFromDate(ordered[j].date), + }, + }); + return dtos.map((_dto, key) => + indices.includes(key) + ? ({ ok: false, error } as CreateShiftResult) + : ({ ok: false, error }), + ); + } + } + } + return this.prisma.$transaction(async (tx) => { + const results: CreateShiftResult[] = Array.from( + { length: dtos.length }, + () => ({ ok: false, error: new Error('uninitialized') })); + + const existing_map = new Map(); + + for (const { timesheet_id, day, key } of timesheet_keys) { + const day_date = new Date(day); + const rows = await tx.shifts.findMany({ + where: { timesheet_id, date: day_date }, + select: { start_time: true, end_time: true, id: true, date: true }, + }); + existing_map.set(key, rows.map((row) => ({ start_time: row.start_time, end_time: row.end_time, date: row.date }))); + } + + ok_items.forEach((x, i) => { + if ("error" in x) results[i] = { ok: false, error: x.error }; + }); + + for (const item of ok_items) { + const { index, dto, normed, timesheet_id } = item; + const day_key = new Date(normed.date.getUTCFullYear(), normed.date.getUTCMonth(), normed.date.getUTCDate()).getTime(); + const map_key = `${timesheet_id}|${day_key}`; + let existing = existing_map.get(map_key); + if (!existing) { + existing = []; + existing_map.set(map_key, existing); + } + const hit = existing.find(exist => overlaps({ start: exist.start_time, end: exist.end_time, date: exist.date }, + { start: normed.start_time, end: normed.end_time, date: normed.date })); + if (hit) { + results[index] = { + ok: false, + error: { + error_code: 'SHIFT_OVERLAP', + conflicts: { + start_time: toStringFromHHmm(hit.start_time), + end_time: toStringFromHHmm(hit.end_time), + date: toStringFromDate(hit.date), + }, + }, + }; + continue; + } + + const row = await tx.shifts.create({ + data: { + timesheet_id: timesheet_id, + bank_code_id: normed.bank_code_id, + date: normed.date, + start_time: normed.start_time, + end_time: normed.end_time, + is_remote: dto.is_remote, + comment: dto.comment ?? undefined, + }, + select: shift_select, + }); + const normalizeHHmm = (value: Date) => toHHmmFromString(toStringFromHHmm(value)); + const normalized_row = { + start_time: normalizeHHmm(row.start_time), + end_time: normalizeHHmm(row.end_time), + date: toDateFromString(row.date), + }; + existing.push(normalized_row); + existing_map.set(map_key, existing); + + + const { type: bank_type } = await this.typeResolver.findTypeByBankCodeId(row.bank_code_id); + + const shift: GetShiftDto = { + shift_id: row.id, + timesheet_id: timesheet_id, + type: bank_type, + date: toStringFromDate(row.date), + start_time: toStringFromHHmm(row.start_time), + end_time: toStringFromHHmm(row.end_time), + is_remote: row.is_remote, + is_approved: false, + comment: row.comment ?? undefined, + }; + results[index] = { ok: true, data: shift }; + } + + return results; + }); + } + + //_________________________________________________________________ + // UPDATE + //_________________________________________________________________ + // finds existing shifts in DB + // verify if shifts are already approved + // normalized Date and Time format to string + // check for valid start and end times + // check for overlaping possibility + // buil a set of data to manipulate modified data only + // update shifts in DB + // return an updated version to display + async updateShifts(dtos: ShiftDto[]): Promise { + if (!Array.isArray(dtos) || dtos.length === 0) throw new BadRequestException({ error_code: 'SHIFT_MISSING' }); + + const updates: ShiftEntity[] = await Promise.all(dtos.map(async (item) => { + try { + const date = await toDateFromString(item.date); + const start_time = await toHHmmFromString(item.start_time); + const end_time = await toHHmmFromString(item.end_time); + const bank_code = await this.typeResolver.findBankCodeIDByType(item.type); + return { + id: item.id, + timesheet_id: item.timesheet_id, + bank_code_id: bank_code.id, + date, + start_time, + end_time, + is_remote: item.is_remote, + is_approved: item.is_approved, + } + } catch (error) { + throw new BadRequestException('INVALID_SHIFT'); + } + })); + + return this.prisma.$transaction(async (tx) => { + + const shift_ids = updates.map(update_shift => update_shift.id); + const rows = await tx.shifts.findMany({ + where: { id: { in: shift_ids } }, + select: shift_select, + }); + const regroup_id = new Map(rows.map(r => [r.id, r])); + + for (const update of updates) { + const existing = regroup_id.get(update.id); + if (!existing) { + return updates.map(exist => exist.id === update.id + ? ({ ok: false, id: update.id, error: new NotFoundException({ error_code: 'SHIFT_MISSING' }) } as UpdateShiftResult) + : ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) }) + ); + } + if (existing.is_approved) { + return updates.map(exist => exist.id === update.id + ? ({ ok: false, id: update.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) } as UpdateShiftResult) + : ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) }) + ); + } + } + + const planned_updates = updates.map(update => { + const exist_shift = regroup_id.get(update.id)!; + const normed: Normalized = { + date: update.date, + start_time: update.start_time, + end_time: update.end_time, + bank_code_id: update.bank_code_id, + }; + return { update, exist_shift, normed }; + }); + + const groups = new Map(); + function key(timesheet: number, d: Date) { + const day_date = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()); + return `${timesheet}|${day_date.getTime()}`; + } + + const unique_pairs = new Map(); + for (const { exist_shift, normed } of planned_updates) { + unique_pairs.set(key(exist_shift.timesheet_id, exist_shift.date), { timesheet_id: exist_shift.timesheet_id, date: exist_shift.date }); + unique_pairs.set(key(exist_shift.timesheet_id, normed.date), { timesheet_id: exist_shift.timesheet_id, date: normed.date }); + } + + for (const group of unique_pairs.values()) { + const day_date = new Date(group.date.getUTCFullYear(), group.date.getUTCMonth(), group.date.getUTCDate()); + const existing = await tx.shifts.findMany({ + where: { timesheet_id: group.timesheet_id, date: day_date }, + select: { id: true, start_time: true, end_time: true, date: true }, + }); + groups.set(key(group.timesheet_id, day_date), { + existing: existing.map(row => ({ + id: row.id, + start: row.start_time, + end: row.end_time, + date: row.date, + })), incoming: planned_updates + }); + } + + for (const planned of planned_updates) { + const keys = key(planned.exist_shift.timesheet_id, planned.normed.date); + const group = groups.get(keys)!; + + const conflict = group.existing.find(row => + row.id !== planned.exist_shift.id && overlaps({ start: row.start, end: row.end, date: row.date }, + { start: planned.normed.start_time, end: planned.normed.end_time, date: planned.normed.date }) + ); + if (conflict) { + return updates.map(exist => exist.id === planned.exist_shift.id + ? ({ + ok: false, id: exist.id, error: { + error_code: 'SHIFT_OVERLAP', + conflicts: { + start_time: toStringFromHHmm(conflict.start), + end_time: toStringFromHHmm(conflict.end), + date: toStringFromDate(conflict.date), + }, + } + } as UpdateShiftResult) + : ({ ok: false, id: exist.id, error: new BadRequestException('Batch aborted due to overlap in another update') }) + ); + } + } + + const regoup_by_day = new Map(); + for (const planned of planned_updates) { + const keys = key(planned.exist_shift.timesheet_id, planned.normed.date); + if (!regoup_by_day.has(keys)) regoup_by_day.set(keys, []); + regoup_by_day.get(keys)!.push({ + id: planned.exist_shift.id, + start: planned.normed.start_time, + end: planned.normed.end_time, + date: planned.normed.date + }); + } + + for (const arr of regoup_by_day.values()) { + arr.sort((a, b) => a.start.getTime() - b.start.getTime()); + for (let i = 1; i < arr.length; i++) { + if (overlaps( + { start: arr[i - 1].start, end: arr[i - 1].end, date: arr[i - 1].date }, + { start: arr[i].start, end: arr[i].end, date: arr[i].date }) + ) { + const error = { + error_code: 'SHIFT_OVERLAP', + conflicts: { + start_time: toStringFromHHmm(arr[i].start), + end_time: toStringFromHHmm(arr[i].end), + date: toStringFromDate(arr[i].date), + }, + + }; + return updates.map(exist => ({ ok: false, id: exist.id, error: error })); + } + } + } + + const results: UpdateShiftResult[] = []; + for (const planned of planned_updates) { + try { + const data: Partial = { + bank_code_id: planned.normed.bank_code_id, + date: planned.normed.date, + start_time: planned.normed.start_time, + end_time: planned.normed.end_time, + is_remote: planned.update.is_remote, + is_approved: planned.exist_shift.is_approved, + comment: planned.update.comment, + }; + + const row = await tx.shifts.update({ + where: { id: planned.exist_shift.id }, + data, + select: shift_select, + }); + + const type = await this.typeResolver.findTypeByBankCodeId(row.bank_code_id); + + const dto: GetShiftDto = { + shift_id: row.id, + timesheet_id: row.timesheet_id, + type: type.type, + date: toStringFromDate(row.date), + start_time: toStringFromHHmm(row.start_time), + end_time: toStringFromHHmm(row.end_time), + is_approved: row.is_approved, + is_remote: row.is_remote, + comment: row.comment ?? undefined, + }; + + results.push({ ok: true, id: planned.exist_shift.id, data: dto }); + } catch (error) { + throw new BadRequestException('INVALID_SHIFT'); + } + } + return results; + }); + } + + //_________________________________________________________________ + // DELETE + //_________________________________________________________________ + //finds shifts using shit_ids + //blocs deletion if approved + async deleteShift(shift_id: number) { + return await this.prisma.$transaction(async (tx) => { + const shift = await tx.shifts.findUnique({ + where: { id: shift_id }, + select: { id: true, date: true, timesheet_id: true }, + }); + if (!shift) throw new ConflictException({ error_code: 'SHIFT_INVALID' }); + + await tx.shifts.delete({ where: { id: shift_id } }); + return { success: true }; + }); + } + + //_________________________________________________________________ + // LOCAL HELPERS + //_________________________________________________________________ + //converts all string hours and date to Date and HHmm formats + private normalizeShiftDto = async (dto: ShiftDto): Promise => { + const { id: bank_code_id } = await this.typeResolver.findBankCodeIDByType(dto.type); + const date = toDateFromString(dto.date); + const start_time = toHHmmFromString(dto.start_time); + const end_time = toHHmmFromString(dto.end_time); + return { date, start_time, end_time, bank_code_id: bank_code_id }; + } +} diff --git a/src/time-and-attendance/time-tracker/shifts/shifts.module.ts b/src/time-and-attendance/time-tracker/shifts/shifts.module.ts new file mode 100644 index 0000000..6d3773e --- /dev/null +++ b/src/time-and-attendance/time-tracker/shifts/shifts.module.ts @@ -0,0 +1,11 @@ + +import { Module } from '@nestjs/common'; +import { ShiftController } from 'src/time-and-attendance/time-tracker/shifts/controllers/shift.controller'; +import { ShiftsUpsertService } from 'src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service'; + +@Module({ + controllers: [ShiftController], + providers: [ ShiftsUpsertService ], + exports: [ ShiftsUpsertService ], +}) +export class ShiftsModule {} diff --git a/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts b/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts new file mode 100644 index 0000000..96bb419 --- /dev/null +++ b/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts @@ -0,0 +1,36 @@ +import { Body, Controller, Get, ParseBoolPipe, ParseIntPipe, Patch, Query, Req, UnauthorizedException } from "@nestjs/common"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service"; +import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service"; +import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes"; + + +@Controller('timesheets') +@RolesAllowed(...GLOBAL_CONTROLLER_ROLES) +export class TimesheetController { + constructor( + private readonly timesheetOverview: GetTimesheetsOverviewService, + private readonly approvalService: TimesheetApprovalService, + ) { } + + @Get() + getTimesheetByPayPeriod( + @Req() req, + @Query('year', ParseIntPipe) year: number, + @Query('period_number', ParseIntPipe) period_number: number + ) { + const email = req.user?.email; + if (!email) throw new UnauthorizedException('Unauthorized User'); + return this.timesheetOverview.getTimesheetsForEmployeeByPeriod(email, year, period_number); + } + + @Patch('timesheet-approval') + @RolesAllowed(...MANAGER_ROLES) + approveTimesheet( + @Body('timesheet_id', ParseIntPipe) timesheet_id: number, + @Body('is_approved', ParseBoolPipe) is_approved: boolean, + ) { + return this.approvalService.approveTimesheetById(timesheet_id, is_approved); + } +} + diff --git a/src/modules/timesheets/dtos/timesheet.dto.ts b/src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto.ts similarity index 98% rename from src/modules/timesheets/dtos/timesheet.dto.ts rename to src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto.ts index 3d30c8b..7e72aac 100644 --- a/src/modules/timesheets/dtos/timesheet.dto.ts +++ b/src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto.ts @@ -36,6 +36,7 @@ export class TotalExpenses { } export class Shift { + timesheet_id: number; date: string; start_time: string; end_time: string; diff --git a/src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service.ts b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service.ts new file mode 100644 index 0000000..82f9c18 --- /dev/null +++ b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service.ts @@ -0,0 +1,59 @@ +import { BaseApprovalService } from "src/common/shared/base-approval.service"; +import { Prisma, Timesheets } from "@prisma/client"; +import { PrismaService } from "src/prisma/prisma.service"; +import { Injectable, NotFoundException } from "@nestjs/common"; +import { timesheet_select } from "src/time-and-attendance/utils/selects.utils"; + + @Injectable() + export class TimesheetApprovalService extends BaseApprovalService{ + constructor( + prisma: PrismaService, + ){super(prisma)} + + //_____________________________________________________________________________________________ + // APPROVAL AND DELEGATE METHODS + //_____________________________________________________________________________________________ + protected get delegate() { + return this.prisma.timesheets; + } + + protected delegateFor(tx: Prisma.TransactionClient) { + return tx.timesheets; + } + + async updateApproval(id: number, is_approved: boolean): Promise { + return this.prisma.$transaction((tx) => + this.updateApprovalWithTransaction(tx, id, is_approved), + ); + } + + async cascadeApprovalWithtx(tx: Prisma.TransactionClient, timesheet_id: number, is_approved: boolean): Promise { + const timesheet = await this.updateApprovalWithTransaction(tx, timesheet_id, is_approved); + await tx.shifts.updateMany({ + where: { timesheet_id: timesheet_id }, + data: { is_approved: is_approved }, + }); + await tx.expenses.updateManyAndReturn({ + where: { timesheet_id: timesheet_id }, + data: { is_approved: is_approved }, + }); + return timesheet; + } + + async approveTimesheetById( timesheet_id: number, is_approved: boolean){ + return this.prisma.$transaction(async (tx) => { + const timesheet = await tx.timesheets.findUnique({ + where: { id: timesheet_id }, + select: { id: true }, + }); + if(!timesheet) throw new NotFoundException(`Timesheet with id: ${timesheet_id} not found`); + + await this.cascadeApprovalWithtx(tx, timesheet_id, is_approved); + + return tx.timesheets.findUnique({ + where: { id: timesheet_id }, + select: timesheet_select, + }); + }); + } + } \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/timesheets/services/timesheet-archive.service.ts b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-archive.service.ts new file mode 100644 index 0000000..33fbf76 --- /dev/null +++ b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-archive.service.ts @@ -0,0 +1,49 @@ +// import { TimesheetsArchive } from "@prisma/client"; +// import { PrismaService } from "src/prisma/prisma.service"; + +// export class TimesheetArchiveService { +// constructor(private readonly prisma: PrismaService){} + +// 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: { some: { 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) } } }); +// }); +// } + +// //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 } }); +// } +// } \ No newline at end of file diff --git a/src/modules/timesheets/services/timesheet-get-overview.service.ts b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts similarity index 92% rename from src/modules/timesheets/services/timesheet-get-overview.service.ts rename to src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts index a3c3c91..6810bfa 100644 --- a/src/modules/timesheets/services/timesheet-get-overview.service.ts +++ b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts @@ -1,38 +1,27 @@ -import { sevenDaysFrom, toDateFromString, toHHmmFromDate, toStringFromDate } from "../helpers/timesheets-date-time-helpers"; +import { sevenDaysFrom, toStringFromDate, toHHmmFromDate, toDateFromString } from "src/time-and-attendance/utils/date-time.utils"; +import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/time-and-attendance/utils/constants.utils"; import { Injectable, NotFoundException } from "@nestjs/common"; +import { TotalExpenses, TotalHours } from "src/time-and-attendance/utils/type.utils"; import { PrismaService } from "src/prisma/prisma.service"; - -type TotalHours = { - regular: number; - evening: number; - emergency: number; - overtime: number; - vacation: number; - holiday: number; - sick: number; -}; - -type TotalExpenses = { - expenses: number; - per_diem: number; - on_call: number; - mileage: number; -}; - -const NUMBER_OF_TIMESHEETS_TO_RETURN = 2; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; @Injectable() export class GetTimesheetsOverviewService { - constructor(private readonly prisma: PrismaService) { } + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver : EmailToIdResolver, + ) { } //----------------------------------------------------------------------------------- // GET TIMESHEETS FOR A SELECTED EMPLOYEE //----------------------------------------------------------------------------------- - async getTimesheetsForEmployeeByPeriod(employee_id: number, pay_year: number, pay_period_no: number) { + async getTimesheetsForEmployeeByPeriod(email: string, pay_year: number, pay_period_no: number) { //find period using year and period_no const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no } }); if (!period) throw new NotFoundException(`Pay period ${pay_year}-${pay_period_no} not found`); + //fetch the employee_id using the email + const employee_id = await this.emailResolver.findIdByEmail(email); //loads the timesheets related to the fetched pay-period const timesheet_range = { employee_id, start_date: { gte: period.period_start, lte: period.period_end } }; let rows = await this.loadTimesheets(timesheet_range); @@ -122,13 +111,14 @@ export class GetTimesheetsOverviewService { const expenses_source = expenses_by_date.get(date_iso) ?? []; //inner map of shifts const shifts = shifts_source.map((shift) => ({ + timesheet_id: shift.timesheet_id, date: toStringFromDate(shift.date), start_time: toHHmmFromDate(shift.start_time), end_time: toHHmmFromDate(shift.end_time), type: shift.bank_code?.type ?? '', is_remote: shift.is_remote ?? false, is_approved: shift.is_approved ?? false, - shift_id: shift.id ?? null, + id: shift.id ?? null, comment: shift.comment ?? null, })); diff --git a/src/time-and-attendance/time-tracker/timesheets/timesheets.module.ts b/src/time-and-attendance/time-tracker/timesheets/timesheets.module.ts new file mode 100644 index 0000000..9e96b4d --- /dev/null +++ b/src/time-and-attendance/time-tracker/timesheets/timesheets.module.ts @@ -0,0 +1,17 @@ + +import { Module } from '@nestjs/common'; +import { TimesheetController } from 'src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller'; +import { TimesheetApprovalService } from 'src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service'; +import { GetTimesheetsOverviewService } from 'src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service'; +import { EmailToIdResolver } from 'src/time-and-attendance/utils/resolve-email-id.utils'; + +@Module({ + controllers: [TimesheetController], + providers: [ + GetTimesheetsOverviewService, + EmailToIdResolver, + TimesheetApprovalService, + ], + exports: [TimesheetApprovalService], +}) +export class TimesheetsModule {} diff --git a/src/time-and-attendance/utils/constants.utils.ts b/src/time-and-attendance/utils/constants.utils.ts new file mode 100644 index 0000000..2a53b28 --- /dev/null +++ b/src/time-and-attendance/utils/constants.utils.ts @@ -0,0 +1,14 @@ +export const NUMBER_OF_TIMESHEETS_TO_RETURN = 2; +export const DAILY_LIMIT_HOURS = 8; +export const WEEKLY_LIMIT_HOURS = 40; +export const PAY_PERIOD_ANCHOR = 2023-12-17; +export const ANCHOR_ISO = '2023-12-17'; +export const PERIOD_DAYS = 14; +export const PERIODS_PER_YEAR = 26; +export const MS_PER_DAY = 86_400_000; +export const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000; + + +//REGEX CONSTANTS +export const DATE_ISO_FORMAT = /^\d{4}-\d{2}-\d{2}$/; +export const HH_MM_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/; diff --git a/src/time-and-attendance/utils/date-time.utils.ts b/src/time-and-attendance/utils/date-time.utils.ts new file mode 100644 index 0000000..9628970 --- /dev/null +++ b/src/time-and-attendance/utils/date-time.utils.ts @@ -0,0 +1,126 @@ +import { BadRequestException } from "@nestjs/common"; +import { ANCHOR_ISO, MS_PER_DAY, PERIODS_PER_YEAR, PERIOD_DAYS } from "src/time-and-attendance/utils/constants.utils"; + +//ensures the week starts from sunday +export function weekStartSunday(date_local: Date): Date { + const start = new Date(Date.UTC(date_local.getFullYear(), date_local.getMonth(), date_local.getDate())); + const dow = start.getDay(); // 0 = dimanche + start.setDate(start.getDate() - dow); + start.setHours(0, 0, 0, 0); + return start; +} + +//converts string to HHmm format +export const toStringFromHHmm = (date: Date): string => { + const hh = date.getUTCHours().toString().padStart(2, '0'); + const mm = date.getUTCMinutes().toString().padStart(2, '0'); + return `${hh}:${mm}`; +} + +//converts string to Date format +export const toStringFromDate = (date: Date) => + date.toISOString().slice(0, 10); + + + +//converts HHmm format to string +export const toHHmmFromString = (hhmm: string): Date => { + const [hh, mm] = hhmm.split(':').map(Number); + const date = new Date('1970-01-01T00:00:00.000Z'); + date.setUTCHours(hh, mm, 0, 0); + return new Date(date); +} + +//converts string to HHmm format +export const toHHmmFromDate = (input: Date | string): string => { + const date = new Date(input); + const hh = String(date.getUTCHours()).padStart(2, '0'); + const mm = String(date.getUTCMinutes()).padStart(2, '0'); + return `${hh}:${mm}`; +} + +//converts Date format to string +export const toDateFromString = (ymd: string | Date): Date => { + return new Date(ymd); +} + +export const toUTCDateFromString = (iso: string | Date) => { + const d = typeof iso === 'string' ? new Date(iso + 'T00:00:00.000Z') : iso; + return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); +}; + +export const sevenDaysFrom = (date: Date | string): Date[] => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(date); + d.setUTCDate(d.getUTCDate() + i); + return d; + }); +} + +export function payYearOfDate(date: string | Date, anchorISO = ANCHOR_ISO): number { + const ANCHOR = toUTCDateFromString(anchorISO); + const d = toUTCDateFromString(date); + const days = Math.floor((+d - +ANCHOR) / MS_PER_DAY); + const cycles = Math.floor(days / (PERIODS_PER_YEAR * PERIOD_DAYS)); + return ANCHOR.getUTCFullYear() + 1 + cycles; +} +//compute labels for periods +export function computePeriod(pay_year: number, period_no: number, anchorISO = ANCHOR_ISO) { + const ANCHOR = toUTCDateFromString(anchorISO); + const cycles = pay_year - (ANCHOR.getUTCFullYear() + 1); + const offsetPeriods = cycles * PERIODS_PER_YEAR + (period_no - 1); + const start = new Date(+ANCHOR + offsetPeriods * PERIOD_DAYS * MS_PER_DAY); + const end = new Date(+start + (PERIOD_DAYS - 1) * MS_PER_DAY); + const pay = new Date(end.getTime() + 6 * MS_PER_DAY); + return { + period_no: period_no, + pay_year: pay_year, + payday: toStringFromDate(pay), + period_start: toStringFromDate(start), + period_end: toStringFromDate(end), + label: `${toStringFromDate(start)}.${toStringFromDate(end)}`, + start, end, + }; +} + +//list of all 26 periods for a full year +export function listPayYear(pay_year: number, anchorISO = ANCHOR_ISO) { + return Array.from({ length: PERIODS_PER_YEAR }, (_, i) => computePeriod(pay_year, i + 1, anchorISO)); +} + +export const overlaps = (a: { start: Date; end: Date, date?: Date; }, b: { start: Date; end: Date; date?: Date; }) => + ((a.date?.getTime() === b.date?.getTime()) && !(a.end <= b.start || a.start >= b.end)); + + +export const hhmmFromLocal = (d: Date) => + `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; + +export const toDateOnly = (s: string): Date => { + if (/^\d{4}-\d{2}-\d{2}$/.test(s)) { + const y = Number(s.slice(0,4)); + const m = Number(s.slice(5,7)) - 1; + const d = Number(s.slice(8,10)); + return new Date(y, m, d, 0, 0, 0, 0); + } + const dt = new Date(s); + if (Number.isNaN(dt.getTime())) throw new BadRequestException(`Invalid date: ${s}`); + return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), 0,0,0,0); +}; + +// export const toStringFromDate = (d: Date) => +// `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; + + +export const toISOtoDateOnly = (iso: string): Date => { + const date = new Date(iso); + if (Number.isNaN(date.getTime())) { + throw new BadRequestException(`Invalid date: ${iso}`); + } + date.setHours(0, 0, 0, 0); + return date; +}; + +export const toISODateKey = (date: Date): string => date.toISOString().slice(0, 10); + +export const normalizeDates = (dates: string[]): string[] => + Array.from(new Set(dates.map((iso) => toISODateKey(toISOtoDateOnly(iso))))); \ No newline at end of file diff --git a/src/modules/schedule-presets/mappers/schedule-presets.mappers.ts b/src/time-and-attendance/utils/mappers.utils.ts similarity index 100% rename from src/modules/schedule-presets/mappers/schedule-presets.mappers.ts rename to src/time-and-attendance/utils/mappers.utils.ts diff --git a/src/time-and-attendance/utils/resolve-bank-type-id.utils.ts b/src/time-and-attendance/utils/resolve-bank-type-id.utils.ts new file mode 100644 index 0000000..017c49f --- /dev/null +++ b/src/time-and-attendance/utils/resolve-bank-type-id.utils.ts @@ -0,0 +1,44 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { Prisma, PrismaClient } from "@prisma/client"; +import { PrismaService } from "src/prisma/prisma.service"; + +type Tx = Prisma.TransactionClient | PrismaClient; + +@Injectable() +export class BankCodesResolver { + constructor(private readonly prisma: PrismaService) {} + + //find id and modifier by type + readonly findIdAndModifierByType = async ( type: string, client?: Tx + ): Promise<{id:number; modifier: number }> => { + const db = client ?? this.prisma; + const bank = await db.bankCodes.findFirst({ + where: { type }, + select: { id: true, modifier: true }, + }); + + if(!bank) throw new NotFoundException(`Unknown bank code type: ${type}`); + return { id: bank.id, modifier: bank.modifier }; + }; + + //finds only id by type + readonly findBankCodeIDByType = async (type: string, client?: Tx) => { + const db = client ?? this.prisma; + const bank_code_id = await db.bankCodes.findFirst({ + where: { type }, + select: {id: true}, + }); + if(!bank_code_id) throw new NotFoundException(`Unkown bank type: ${type}`); + return bank_code_id; + } + + readonly findTypeByBankCodeId = async (bank_code_id: number, client?: Tx) => { + const db = client ?? this.prisma; + const type = await db.bankCodes.findFirst({ + where: { id: bank_code_id }, + select: { type: true }, + }); + if(!type) throw new NotFoundException(`Type with id : ${bank_code_id} not found`); + return type; + } +} \ No newline at end of file diff --git a/src/modules/shared/utils/resolve-email-id.utils.ts b/src/time-and-attendance/utils/resolve-email-id.utils.ts similarity index 100% rename from src/modules/shared/utils/resolve-email-id.utils.ts rename to src/time-and-attendance/utils/resolve-email-id.utils.ts diff --git a/src/modules/shared/utils/resolve-full-name.utils.ts b/src/time-and-attendance/utils/resolve-full-name.utils.ts similarity index 100% rename from src/modules/shared/utils/resolve-full-name.utils.ts rename to src/time-and-attendance/utils/resolve-full-name.utils.ts diff --git a/src/modules/shared/utils/resolve-shifts-id.utils.ts b/src/time-and-attendance/utils/resolve-shifts-id.utils.ts similarity index 93% rename from src/modules/shared/utils/resolve-shifts-id.utils.ts rename to src/time-and-attendance/utils/resolve-shifts-id.utils.ts index 4d9d313..e76d144 100644 --- a/src/modules/shared/utils/resolve-shifts-id.utils.ts +++ b/src/time-and-attendance/utils/resolve-shifts-id.utils.ts @@ -1,7 +1,7 @@ import { Prisma, PrismaClient } from "@prisma/client"; import { NotFoundException } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; -import { ShiftKey } from "../interfaces/shifts.interface"; +import { ShiftKey } from "src/time-and-attendance/utils/type.utils"; type Tx = Prisma.TransactionClient | PrismaClient; diff --git a/src/modules/shared/utils/resolve-timesheet.utils.ts b/src/time-and-attendance/utils/resolve-timesheet.utils.ts similarity index 92% rename from src/modules/shared/utils/resolve-timesheet.utils.ts rename to src/time-and-attendance/utils/resolve-timesheet.utils.ts index 61f4ce6..f556a8e 100644 --- a/src/modules/shared/utils/resolve-timesheet.utils.ts +++ b/src/time-and-attendance/utils/resolve-timesheet.utils.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { Prisma, PrismaClient } from "@prisma/client"; -import { weekStartSunday } from "src/modules/shifts/helpers/shifts-date-time-helpers"; +import { weekStartSunday } from "src/time-and-attendance/utils/date-time.utils"; import { PrismaService } from "src/prisma/prisma.service"; import { EmailToIdResolver } from "./resolve-email-id.utils"; diff --git a/src/time-and-attendance/utils/selects.utils.ts b/src/time-and-attendance/utils/selects.utils.ts new file mode 100644 index 0000000..98a27ec --- /dev/null +++ b/src/time-and-attendance/utils/selects.utils.ts @@ -0,0 +1,91 @@ +import { Prisma } from "@prisma/client"; + +export const expense_select = { + id: true, + timesheet_id: true, + bank_code_id: true, + attachment: true, + date: true, + amount: true, + mileage: true, + comment: true, + supervisor_comment: true, + is_approved: true, +} satisfies Prisma.ExpensesSelect; + +export const shift_select = { + id: true, + timesheet_id: true, + bank_code_id: true, + bank_code: { + select: { type: true }, + }, + date: true, + start_time: true, + end_time: true, + is_remote: true, + is_approved: true, + comment: true, +} satisfies Prisma.ShiftsSelect; + +export const leaveRequestsSelect = { + id: true, + bank_code_id: true, + leave_type: true, + date: true, + payable_hours: true, + requested_hours: true, + comment: true, + approval_status: true, + employee: { + select: { + id: true, + user: { + select: { + email: true, + first_name: true, + last_name: true, + }, + }, + } + }, +} satisfies Prisma.LeaveRequestsSelect; + + +export const EXPENSE_SELECT = { + date: true, + amount: true, + mileage: true, + comment: true, + is_approved: true, + supervisor_comment: true, + bank_code: { select: { type: true } }, +} as const; + +export const EXPENSE_ASC_ORDER = { date: 'asc' as const }; + +export const PAY_PERIOD_SELECT = { + period_start: true, + period_end: true, +} as const; + +export const SHIFT_SELECT = { + date: true, + start_time: true, + end_time: true, + comment: true, + is_approved: true, + is_remote: true, + bank_code: {select: { type: true } }, +} as const; + +export const SHIFT_ASC_ORDER = [{date: 'asc' as const}, {start_time: 'asc' as const}]; + +export const timesheet_select = { + id: true, + employee_id: true, + shift: true, + expense: true, + start_date: true, + is_approved: true, +} satisfies Prisma.TimesheetsSelect; \ No newline at end of file diff --git a/src/time-and-attendance/utils/type.utils.ts b/src/time-and-attendance/utils/type.utils.ts new file mode 100644 index 0000000..b38ecc8 --- /dev/null +++ b/src/time-and-attendance/utils/type.utils.ts @@ -0,0 +1,108 @@ +import { Prisma, PrismaClient } from "@prisma/client"; +import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto"; +import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; +import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto"; +import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-entity.dto"; +import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; + + +export type TotalHours = { + regular: number; + evening: number; + emergency: number; + overtime: number; + vacation: number; + holiday: number; + sick: number; +}; + +export type TotalExpenses = { + expenses: number; + per_diem: number; + on_call: number; + mileage: number; +}; + +export type Normalized = { + date: Date; + start_time: Date; + end_time: Date; + bank_code_id: number; +}; +export type CreateShiftResult = { ok: true; data: GetShiftDto } | { ok: false; error: any }; +export type UpdateShiftResult = { ok: true; id: number; data: GetShiftDto } | { ok: false; id: number; error: any }; +export type NormedOk = { + index: number; + dto: ShiftEntity; + normed: Normalized; + timesheet_id: number; +}; + +export type DeletePresetResult = { ok: true; id: number; } | { ok: false; id: number; error: any }; +export type CreatePresetResult = { ok: true; } | { ok: false; error: any }; +export type UpdatePresetResult = { ok: true; id: number; data: SchedulePresetsDto } | { ok: false; id: number; error: any }; + + +export type NormalizedExpense = { + date: Date; + comment: string; + supervisor_comment?: string; + parsed_amount?: number; + parsed_mileage?: number; + parsed_attachment?: number; +}; +export type CreateExpenseResult = { ok: true; data: GetExpenseDto } | { ok: false; error: any }; +export type DeleteExpenseResult = { ok: true; id: number; } | { ok: false; id: number; error: any }; + + +export type ShiftResponse = { + week_day: string; + sort_order: number; + start_time: string; + end_time: string; + is_remote: boolean; + type: string; +}; + +export type PresetResponse = { + id: number; + name: string; + is_default: boolean; + shifts: ShiftResponse[]; +} + +export type ApplyResult = { + timesheet_id: number; + created: number; + skipped: number; +} + +export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>; + +export type Tx = Prisma.TransactionClient | PrismaClient; + +export type WeekOvertimeSummary = { + week_start:string; + week_end: string; + week_total_hours: number; + weekly_overtime: number; + daily_overtime_kept: number; + total_overtime: number; + breakdown: Array<{ + date:string; + day_hours: number; + day_overtime: number; + daily_kept: number; + running_total_before: number; + }>; +}; + +export interface ShiftKey { + timesheet_id: number; + date: Date; + start_time: Date; + end_time: Date; + bank_code_id: number; + is_remote: boolean; + comment?: string | null; +} \ No newline at end of file diff --git a/src/~misc_deprecated-files/after-hours.service.ts b/src/~misc_deprecated-files/after-hours.service.ts new file mode 100644 index 0000000..c5e3425 --- /dev/null +++ b/src/~misc_deprecated-files/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 biz_start = new Date(start); +// biz_start.setHours(AfterHoursService.BUSINESS_START, 0,0,0); + +// if (end>= start || start >= biz_start) { +// return 0; +// } + +// const segment_end = end < biz_start ? end : biz_start; +// const minutes = (segment_end.getTime() - start.getTime()) / 60000; + +// this.logger.debug(`getPreBusinessMintutes -> ${minutes.toFixed(1)}min`); +// return minutes; + +// } + +// private getPostBusinessMinutes(start: Date, end: Date): number { +// const biz_end = new Date(start); +// biz_end.setHours(AfterHoursService.BUSINESS_END,0,0,0); + +// if( end <= biz_end ) { +// return 0; +// } + +// const segment_start = start > biz_end ? start : biz_end; +// const minutes = (end.getTime() - segment_start.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 pre_min = this.getPreBusinessMinutes(start, end); +// const post_min = this.getPostBusinessMinutes(start, end); +// const raw_aftermin = pre_min + post_min; + +// const rounded_min = this.roundToNearestQUarterMinute(raw_aftermin); + +// const hours = rounded_min / 60; +// const result = parseFloat(hours.toFixed(2)); + +// this.logger.debug(`computeAfterHours -> raw_aftermin = ${raw_aftermin.toFixed(1)}min, + +// rounded = ${rounded_min}min, hours = ${result.toFixed(2)}`); +// return result; +// } +// } + diff --git a/src/modules/expenses/~misc_deprecated-files/create-expense.dto.ts b/src/~misc_deprecated-files/create-expense.dto.ts similarity index 100% rename from src/modules/expenses/~misc_deprecated-files/create-expense.dto.ts rename to src/~misc_deprecated-files/create-expense.dto.ts diff --git a/src/modules/timesheets/~misc_deprecated-files/create-timesheet.dto.ts b/src/~misc_deprecated-files/create-timesheet.dto.ts similarity index 100% rename from src/modules/timesheets/~misc_deprecated-files/create-timesheet.dto.ts rename to src/~misc_deprecated-files/create-timesheet.dto.ts diff --git a/src/~misc_deprecated-files/date-time.constant.ts b/src/~misc_deprecated-files/date-time.constant.ts new file mode 100644 index 0000000..293a3d5 --- /dev/null +++ b/src/~misc_deprecated-files/date-time.constant.ts @@ -0,0 +1,2 @@ +// export const MS_PER_DAY = 86_400_000; +// export const MS_PER_HOUR = 3_600_000; \ No newline at end of file diff --git a/src/modules/expenses/~misc_deprecated-files/expenses-command.service.ts b/src/~misc_deprecated-files/expenses-command.service.ts similarity index 100% rename from src/modules/expenses/~misc_deprecated-files/expenses-command.service.ts rename to src/~misc_deprecated-files/expenses-command.service.ts diff --git a/src/modules/expenses/~misc_deprecated-files/expenses-query.service.ts b/src/~misc_deprecated-files/expenses-query.service.ts similarity index 100% rename from src/modules/expenses/~misc_deprecated-files/expenses-query.service.ts rename to src/~misc_deprecated-files/expenses-query.service.ts diff --git a/src/modules/expenses/~misc_deprecated-files/expenses.controller.ts b/src/~misc_deprecated-files/expenses.controller.ts similarity index 100% rename from src/modules/expenses/~misc_deprecated-files/expenses.controller.ts rename to src/~misc_deprecated-files/expenses.controller.ts diff --git a/src/modules/expenses/~misc_deprecated-files/expenses.types.interfaces.ts b/src/~misc_deprecated-files/expenses.types.interfaces.ts similarity index 100% rename from src/modules/expenses/~misc_deprecated-files/expenses.types.interfaces.ts rename to src/~misc_deprecated-files/expenses.types.interfaces.ts diff --git a/src/modules/expenses/~misc_deprecated-files/expenses.utils.ts b/src/~misc_deprecated-files/expenses.utils.ts similarity index 100% rename from src/modules/expenses/~misc_deprecated-files/expenses.utils.ts rename to src/~misc_deprecated-files/expenses.utils.ts diff --git a/src/modules/shifts/~misc_deprecated-files/get-shift-overview.dto.ts b/src/~misc_deprecated-files/get-shift-overview.dto.ts similarity index 100% rename from src/modules/shifts/~misc_deprecated-files/get-shift-overview.dto.ts rename to src/~misc_deprecated-files/get-shift-overview.dto.ts diff --git a/src/~misc_deprecated-files/leave-requests.select.ts b/src/~misc_deprecated-files/leave-requests.select.ts new file mode 100644 index 0000000..64453b4 --- /dev/null +++ b/src/~misc_deprecated-files/leave-requests.select.ts @@ -0,0 +1,23 @@ +// import { Prisma } from "@prisma/client"; + +// //custom prisma select to avoid employee_id exposure +// export const leaveRequestsSelect = { +// id: true, +// bank_code_id: true, +// leave_type: true, +// date: true, +// payable_hours: true, +// requested_hours: true, +// comment: true, +// approval_status: true, +// employee: { select: { +// id: true, +// user: { select: { +// email: true, +// first_name: true, +// last_name: true, +// }}, +// }}, +// } satisfies Prisma.LeaveRequestsSelect; + +// export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>; diff --git a/src/~misc_deprecated-files/regex.constant.ts b/src/~misc_deprecated-files/regex.constant.ts new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/~misc_deprecated-files/regex.constant.ts @@ -0,0 +1 @@ + diff --git a/src/modules/expenses/~misc_deprecated-files/search-expense.dto.ts b/src/~misc_deprecated-files/search-expense.dto.ts similarity index 100% rename from src/modules/expenses/~misc_deprecated-files/search-expense.dto.ts rename to src/~misc_deprecated-files/search-expense.dto.ts diff --git a/src/modules/timesheets/~misc_deprecated-files/search-timesheet.dto.ts b/src/~misc_deprecated-files/search-timesheet.dto.ts similarity index 100% rename from src/modules/timesheets/~misc_deprecated-files/search-timesheet.dto.ts rename to src/~misc_deprecated-files/search-timesheet.dto.ts diff --git a/src/modules/shifts/~misc_deprecated-files/shifts-command.service.ts b/src/~misc_deprecated-files/shifts-command.service.ts similarity index 100% rename from src/modules/shifts/~misc_deprecated-files/shifts-command.service.ts rename to src/~misc_deprecated-files/shifts-command.service.ts diff --git a/src/modules/shifts/~misc_deprecated-files/shifts-overview-row.interface.ts b/src/~misc_deprecated-files/shifts-overview-row.interface.ts similarity index 100% rename from src/modules/shifts/~misc_deprecated-files/shifts-overview-row.interface.ts rename to src/~misc_deprecated-files/shifts-overview-row.interface.ts diff --git a/src/modules/shifts/~misc_deprecated-files/shifts-query.service.ts b/src/~misc_deprecated-files/shifts-query.service.ts similarity index 100% rename from src/modules/shifts/~misc_deprecated-files/shifts-query.service.ts rename to src/~misc_deprecated-files/shifts-query.service.ts diff --git a/src/modules/shifts/~misc_deprecated-files/shifts-upsert.types.ts b/src/~misc_deprecated-files/shifts-upsert.types.ts similarity index 100% rename from src/modules/shifts/~misc_deprecated-files/shifts-upsert.types.ts rename to src/~misc_deprecated-files/shifts-upsert.types.ts diff --git a/src/modules/shifts/~misc_deprecated-files/shifts.controller.ts b/src/~misc_deprecated-files/shifts.controller.ts similarity index 100% rename from src/modules/shifts/~misc_deprecated-files/shifts.controller.ts rename to src/~misc_deprecated-files/shifts.controller.ts diff --git a/src/modules/shifts/~misc_deprecated-files/shifts.helpers.ts b/src/~misc_deprecated-files/shifts.helpers.ts similarity index 100% rename from src/modules/shifts/~misc_deprecated-files/shifts.helpers.ts rename to src/~misc_deprecated-files/shifts.helpers.ts diff --git a/src/modules/shifts/~misc_deprecated-files/shifts.utils.ts b/src/~misc_deprecated-files/shifts.utils.ts similarity index 100% rename from src/modules/shifts/~misc_deprecated-files/shifts.utils.ts rename to src/~misc_deprecated-files/shifts.utils.ts diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheet-period.dto.ts b/src/~misc_deprecated-files/timesheet-period.dto.ts similarity index 100% rename from src/modules/timesheets/~misc_deprecated-files/timesheet-period.dto.ts rename to src/~misc_deprecated-files/timesheet-period.dto.ts diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheet.helpers.ts b/src/~misc_deprecated-files/timesheet.helpers.ts similarity index 100% rename from src/modules/timesheets/~misc_deprecated-files/timesheet.helpers.ts rename to src/~misc_deprecated-files/timesheet.helpers.ts diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheet.mappers.ts b/src/~misc_deprecated-files/timesheet.mappers.ts similarity index 100% rename from src/modules/timesheets/~misc_deprecated-files/timesheet.mappers.ts rename to src/~misc_deprecated-files/timesheet.mappers.ts diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheet.selectors.ts b/src/~misc_deprecated-files/timesheet.selectors.ts similarity index 100% rename from src/modules/timesheets/~misc_deprecated-files/timesheet.selectors.ts rename to src/~misc_deprecated-files/timesheet.selectors.ts diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheet.types.ts b/src/~misc_deprecated-files/timesheet.types.ts similarity index 100% rename from src/modules/timesheets/~misc_deprecated-files/timesheet.types.ts rename to src/~misc_deprecated-files/timesheet.types.ts diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheet.utils.ts b/src/~misc_deprecated-files/timesheet.utils.ts similarity index 100% rename from src/modules/timesheets/~misc_deprecated-files/timesheet.utils.ts rename to src/~misc_deprecated-files/timesheet.utils.ts diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheets-command.service.ts b/src/~misc_deprecated-files/timesheets-command.service.ts similarity index 100% rename from src/modules/timesheets/~misc_deprecated-files/timesheets-command.service.ts rename to src/~misc_deprecated-files/timesheets-command.service.ts diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheets-query.service.ts b/src/~misc_deprecated-files/timesheets-query.service.ts similarity index 100% rename from src/modules/timesheets/~misc_deprecated-files/timesheets-query.service.ts rename to src/~misc_deprecated-files/timesheets-query.service.ts diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheets.controller.ts b/src/~misc_deprecated-files/timesheets.controller.ts similarity index 100% rename from src/modules/timesheets/~misc_deprecated-files/timesheets.controller.ts rename to src/~misc_deprecated-files/timesheets.controller.ts diff --git a/src/modules/expenses/~misc_deprecated-files/update-expense.dto.ts b/src/~misc_deprecated-files/update-expense.dto.ts similarity index 100% rename from src/modules/expenses/~misc_deprecated-files/update-expense.dto.ts rename to src/~misc_deprecated-files/update-expense.dto.ts diff --git a/src/modules/expenses/~misc_deprecated-files/upsert-expense.dto.ts b/src/~misc_deprecated-files/upsert-expense.dto.ts similarity index 100% rename from src/modules/expenses/~misc_deprecated-files/upsert-expense.dto.ts rename to src/~misc_deprecated-files/upsert-expense.dto.ts diff --git a/src/modules/shifts/~misc_deprecated-files/upsert-shift.dto.ts b/src/~misc_deprecated-files/upsert-shift.dto.ts similarity index 100% rename from src/modules/shifts/~misc_deprecated-files/upsert-shift.dto.ts rename to src/~misc_deprecated-files/upsert-shift.dto.ts diff --git a/src/~misc_deprecated-files/utils.constant.ts b/src/~misc_deprecated-files/utils.constant.ts new file mode 100644 index 0000000..005ac6c --- /dev/null +++ b/src/~misc_deprecated-files/utils.constant.ts @@ -0,0 +1 @@ +// export const COMMENT_MAX_LENGTH = 280; \ No newline at end of file