diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 4f6c274..718184d 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -29,99 +29,31 @@ ] } }, - "/archives/employees": { + "/auth/v1/login": { "get": { - "operationId": "EmployeesArchiveController_findOneArchived", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], + "operationId": "AuthController_login", + "parameters": [], "responses": { "200": { - "description": "Archived employee found" + "description": "" } }, - "summary": "Fetch employee in archives with its Id", "tags": [ - "Employee Archives" + "Auth" ] } }, - "/archives/expenses": { + "/auth/callback": { "get": { - "operationId": "ExpensesArchiveController_findOneArchived", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], + "operationId": "AuthController_loginCallback", + "parameters": [], "responses": { "200": { - "description": "Archived expense found" + "description": "" } }, - "summary": "Fetch expense in archives with its Id", "tags": [ - "Expense Archives" - ] - } - }, - "/archives/shifts": { - "get": { - "operationId": "ShiftsArchiveController_findOneArchived", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "200": { - "description": "Archived shift found" - } - }, - "summary": "Fetch shift in archives with its Id", - "tags": [ - "Shift Archives" - ] - } - }, - "/archives/timesheets": { - "get": { - "operationId": "TimesheetsArchiveController_findOneArchived", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "200": { - "description": "Archived timesheet found" - } - }, - "summary": "Fetch timesheet in archives with its Id", - "tags": [ - "Timesheet Archives" + "Auth" ] } }, @@ -221,6 +153,8 @@ ] } }, +<<<<<<< HEAD +======= "/employees/profile/{email}": { "get": { "operationId": "EmployeesController_findOneProfile", @@ -635,6 +569,7 @@ ] } }, +>>>>>>> 88f7c0cb0e3824f6faed91058a7d55a9cca048a7 "/notifications/summary": { "get": { "operationId": "NotificationsController_summary", @@ -663,6 +598,8 @@ ] } }, +<<<<<<< HEAD +======= "/leave-requests/upsert": { "post": { "operationId": "LeaveRequestController_upsertLeaveRequest", @@ -734,6 +671,7 @@ ] } }, +>>>>>>> 88f7c0cb0e3824f6faed91058a7d55a9cca048a7 "/oauth-sessions": { "post": { "operationId": "OauthSessionsController_create", @@ -928,251 +866,6 @@ ] } }, - "/pay-periods/current-and-all": { - "get": { - "operationId": "PayPeriodsController_getCurrentAndAll", - "parameters": [ - { - "name": "date", - "required": false, - "in": "query", - "description": "Override for resolving the current period", - "schema": { - "example": "2025-08-11", - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Find current and all pay periods", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayPeriodBundleDto" - } - } - } - } - }, - "summary": "Return current pay period and the full list", - "tags": [ - "pay-periods" - ] - } - }, - "/pay-periods/date/{date}": { - "get": { - "operationId": "PayPeriodsController_findByDate", - "parameters": [ - { - "name": "date", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Pay period found for the selected date", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayPeriodDto" - } - } - } - }, - "404": { - "description": "Pay period not found for the selected date" - } - }, - "summary": "Resolve a period by a date within it", - "tags": [ - "pay-periods" - ] - } - }, - "/pay-periods/{year}/{periodNumber}": { - "get": { - "operationId": "PayPeriodsController_findOneByYear", - "parameters": [ - { - "name": "year", - "required": true, - "in": "path", - "schema": { - "example": 2024, - "type": "number" - } - }, - { - "name": "periodNumber", - "required": true, - "in": "path", - "description": "1..26", - "schema": { - "example": 1, - "type": "number" - } - } - ], - "responses": { - "200": { - "description": "Pay period found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayPeriodDto" - } - } - } - }, - "404": { - "description": "Pay period not found" - } - }, - "summary": "Find pay period by year and period number", - "tags": [ - "pay-periods" - ] - } - }, - "/pay-periods/crew/bulk-approval": { - "patch": { - "operationId": "PayPeriodsController_bulkApproval", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkCrewApprovalDto" - } - } - } - }, - "responses": { - "200": { - "description": "Pay period approved" - } - }, - "summary": "Approve all selected timesheets in the period", - "tags": [ - "pay-periods" - ] - } - }, - "/pay-periods/{year}/{periodNumber}/{email}": { - "get": { - "operationId": "PayPeriodsController_getCrewOverview", - "parameters": [ - { - "name": "year", - "required": true, - "in": "path", - "schema": { - "example": 2024, - "type": "number" - } - }, - { - "name": "periodNumber", - "required": true, - "in": "path", - "description": "1..26", - "schema": { - "example": 1, - "type": "number" - } - }, - { - "name": "email", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "includeSubtree", - "required": false, - "in": "query", - "description": "Include indirect reports", - "schema": { - "example": false, - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "Crew overview", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayPeriodOverviewDto" - } - } - } - }, - "404": { - "description": "Pay period not found" - } - }, - "summary": "Supervisor crew overview for a given pay period", - "tags": [ - "pay-periods" - ] - } - }, - "/pay-periods/overview/{year}/{periodNumber}": { - "get": { - "operationId": "PayPeriodsController_getOverviewByYear", - "parameters": [ - { - "name": "year", - "required": true, - "in": "path", - "schema": { - "example": 2024, - "type": "number" - } - }, - { - "name": "periodNumber", - "required": true, - "in": "path", - "description": "1..26", - "schema": { - "example": 1, - "type": "number" - } - } - ], - "responses": { - "200": { - "description": "Pay period overview found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayPeriodOverviewDto" - } - } - } - }, - "404": { - "description": "Pay period not found" - } - }, - "summary": "Detailed view of a pay period by year + number", - "tags": [ - "pay-periods" - ] - } - }, "/preferences/{email}": { "patch": { "operationId": "PreferencesController_updatePreferences", @@ -1306,6 +999,123 @@ "SchedulePresets" ] } + }, + "/shift": { + "get": { + "operationId": "ShiftController_getShiftsByIds", + "parameters": [ + { + "name": "shift_ids", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Shift" + ] + }, + "patch": { + "operationId": "ShiftController_updateBatch", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Shift" + ] + } + }, + "/shift/{timesheet_id}": { + "post": { + "operationId": "ShiftController_createBatch", + "parameters": [ + { + "name": "timesheet_id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "Shift" + ] + } + }, + "/shift/{shift_id}": { + "delete": { + "operationId": "ShiftController_remove", + "parameters": [ + { + "name": "shift_id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Shift" + ] + } + }, + "/timesheets": { + "get": { + "operationId": "TimesheetController_getTimesheetByIds", + "parameters": [ + { + "name": "timesheet_ids", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Timesheet" + ] + } } }, "info": { @@ -1517,6 +1327,8 @@ "first_work_day" ] }, +<<<<<<< HEAD +======= "EmployeeProfileItemDto": { "type": "object", "properties": {} @@ -1537,6 +1349,7 @@ "type": "object", "properties": {} }, +>>>>>>> 88f7c0cb0e3824f6faed91058a7d55a9cca048a7 "CreateOauthSessionDto": { "type": "object", "properties": { @@ -1645,172 +1458,6 @@ } } }, - "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" - ] - }, - "BulkCrewApprovalDto": { - "type": "object", - "properties": {} - }, - "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": {} diff --git a/package-lock.json b/package-lock.json index 1166ca0..1180132 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,17 +17,20 @@ "@nestjs/platform-express": "^11.1.6", "@nestjs/schedule": "^6.0.0", "@nestjs/swagger": "^11.2.0", - "@prisma/client": "^6.14.0", + "@prisma/client": "^6.17.1", + "bullmq": "^5.58.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "express-session": "^1.18.2", "file-type": "^21.0.0", + "ioredis": "^5.7.0", "multer": "^2.0.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "passport-openidconnect": "^0.1.2", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "sharp": "^0.34.3" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -51,7 +54,7 @@ "globals": "^16.0.0", "jest": "^29.7.0", "prettier": "^3.4.2", - "prisma": "^6.17.0", + "prisma": "^6.17.1", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "ts-jest": "^29.2.5", @@ -723,6 +726,15 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", @@ -1343,6 +1355,411 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz", + "integrity": "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@inquirer/checkbox": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.9.tgz", @@ -1389,14 +1806,14 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.14", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.14.tgz", - "integrity": "sha512-Ma+ZpOJPewtIYl6HZHZckeX1STvDnHTCB2GVINNUlSEn2Am6LddWwfPkIGY0IUFVjUUrr/93XlBwTK6mfLjf0A==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.0.tgz", + "integrity": "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==", "dev": true, "dependencies": { - "@inquirer/figures": "^1.0.12", - "@inquirer/type": "^3.0.7", - "ansi-escapes": "^4.3.2", + "@inquirer/ansi": "^1.0.1", + "@inquirer/figures": "^1.0.14", + "@inquirer/type": "^3.0.9", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", @@ -1416,14 +1833,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.14.tgz", - "integrity": "sha512-yd2qtLl4QIIax9DTMZ1ZN2pFrrj+yL3kgIWxm34SS6uwCr0sIhsNyudUjAo5q3TqI03xx4SEBkUJqZuAInp9uA==", + "version": "4.2.21", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.21.tgz", + "integrity": "sha512-MjtjOGjr0Kh4BciaFShYpZ1s9400idOdvQ5D7u7lE6VztPFoyLcVNE5dXBmEEIQq5zi4B9h2kU+q7AVBxJMAkQ==", "dev": true, "dependencies": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7", - "external-editor": "^3.1.0" + "@inquirer/core": "^10.3.0", + "@inquirer/external-editor": "^1.0.2", + "@inquirer/type": "^3.0.9" }, "engines": { "node": ">=18" @@ -1459,10 +1876,47 @@ } } }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", + "integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==", + "dev": true, + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/@inquirer/figures": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", - "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.14.tgz", + "integrity": "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==", "dev": true, "engines": { "node": ">=18" @@ -1631,9 +2085,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", - "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.9.tgz", + "integrity": "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==", "dev": true, "engines": { "node": ">=18" @@ -1647,6 +2101,11 @@ } } }, + "node_modules/@ioredis/commands": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz", + "integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==" + }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -2244,6 +2703,78 @@ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==" }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@napi-rs/nice": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.4.tgz", @@ -2737,9 +3268,9 @@ } }, "node_modules/@nestjs/common": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.3.tgz", - "integrity": "sha512-ogEK+GriWodIwCw6buQ1rpcH4Kx+G7YQ9EwuPySI3rS05pSdtQ++UhucjusSI9apNidv+QURBztJkRecwwJQXg==", + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz", + "integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==", "dependencies": { "file-type": "21.0.0", "iterare": "1.2.1", @@ -2781,9 +3312,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.3.tgz", - "integrity": "sha512-5lTni0TCh8x7bXETRD57pQFnKnEg1T6M+VLE7wAmyQRIecKQU+2inRGZD+A4v2DC1I04eA0WffP0GKLxjOKlzw==", + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.6.tgz", + "integrity": "sha512-siWX7UDgErisW18VTeJA+x+/tpNZrJewjTBsRPF3JVxuWRuAB1kRoiJcxHgln8Lb5UY9NdvklITR84DUEXD0Cg==", "hasInstallScript": true, "dependencies": { "@nuxt/opencollective": "0.4.1", @@ -2821,11 +3352,11 @@ } }, "node_modules/@nestjs/jwt": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz", - "integrity": "sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.1.tgz", + "integrity": "sha512-HXSsc7SAnCnjA98TsZqrE7trGtHDnYXWp4Ffy6LwSmck1QvbGYdMzBquXofX5l6tIRpeY4Qidl2Ti2CVG77Pdw==", "dependencies": { - "@types/jsonwebtoken": "9.0.7", + "@types/jsonwebtoken": "9.0.10", "jsonwebtoken": "9.0.2" }, "peerDependencies": { @@ -2881,11 +3412,11 @@ } }, "node_modules/@nestjs/schedule": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.0.tgz", - "integrity": "sha512-aQySMw6tw2nhitELXd3EiRacQRgzUKD9mFcUZVOJ7jPLqIBvXOyvRWLsK9SdurGA+jjziAlMef7iB5ZEFFoQpw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.1.tgz", + "integrity": "sha512-v3yO6cSPAoBSSyH67HWnXHzuhPhSNZhRmLY38JvCt2sqY8sPMOODpcU1D79iUMFf7k16DaMEbL4Mgx61ZhiC8Q==", "dependencies": { - "cron": "4.3.0" + "cron": "4.3.3" }, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", @@ -2985,16 +3516,16 @@ } }, "node_modules/@nestjs/swagger": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.0.tgz", - "integrity": "sha512-5wolt8GmpNcrQv34tIPUtPoV1EeFbCetm40Ij3+M0FNNnf2RJ3FyWfuQvI8SBlcJyfaounYVTKzKHreFXsUyOg==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.1.tgz", + "integrity": "sha512-1MS7xf0pzc1mofG53xrrtrurnziafPUHkqzRm4YUVPA/egeiMaSerQBD/feiAeQ2BnX0WiLsTX4HQFO0icvOjQ==", "dependencies": { "@microsoft/tsdoc": "0.15.1", "@nestjs/mapped-types": "2.1.0", "js-yaml": "4.1.0", "lodash": "4.17.21", - "path-to-regexp": "8.2.0", - "swagger-ui-dist": "5.21.0" + "path-to-regexp": "8.3.0", + "swagger-ui-dist": "5.29.4" }, "peerDependencies": { "@fastify/static": "^8.0.0", @@ -3016,10 +3547,19 @@ } } }, + "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.3", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.3.tgz", - "integrity": "sha512-CeXG6/eEqgFIkPkmU00y18Dd3DLOIDFhPItzJK1SWckKo6IhcnfoRJzGx75bmuvUMjb51j6An96S/+MJ2ty9jA==", + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.6.tgz", + "integrity": "sha512-srYzzDNxGvVCe1j0SpTS9/ix75PKt6Sn6iMaH1rpJ6nj2g8vwNrhK0CoJJXvpCYgrnI+2WES2pprYnq8rAMYHA==", "dev": true, "dependencies": { "tslib": "2.8.1" @@ -3127,9 +3667,9 @@ } }, "node_modules/@prisma/client": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.14.0.tgz", - "integrity": "sha512-8E/Nk3eL5g7RQIg/LUj1ICyDmhD053STjxrPxUtCRybs2s/2sOEcx9NpITuAOPn07HEpWBfhAVe1T/HYWXUPOw==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.17.1.tgz", + "integrity": "sha512-zL58jbLzYamjnNnmNA51IOZdbk5ci03KviXCuB0Tydc9btH2kDWsi1pQm2VecviRTM7jGia0OPPkgpGnT3nKvw==", "hasInstallScript": true, "engines": { "node": ">=18.18" @@ -3148,9 +3688,9 @@ } }, "node_modules/@prisma/config": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.0.tgz", - "integrity": "sha512-k8tuChKpkO/Vj7ZEzaQMNflNGbaW4X0r8+PC+W2JaqVRdiS2+ORSv1SrDwNxsb8YyzIQJucXqLGZbgxD97ZhsQ==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.1.tgz", + "integrity": "sha512-fs8wY6DsvOCzuiyWVckrVs1LOcbY4LZNz8ki4uUIQ28jCCzojTGqdLhN2Jl5lDnC1yI8/gNIKpsWDM8pLhOdwA==", "devOptional": true, "dependencies": { "c12": "3.1.0", @@ -3160,48 +3700,48 @@ } }, "node_modules/@prisma/debug": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.0.tgz", - "integrity": "sha512-eE2CB32nr1hRqyLVnOAVY6c//iSJ/PN+Yfoa/2sEzLGpORaCg61d+nvdAkYSh+6Y2B8L4BVyzkRMANLD6nnC2g==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.1.tgz", + "integrity": "sha512-Vf7Tt5Wh9XcndpbmeotuqOMLWPTjEKCsgojxXP2oxE1/xYe7PtnP76hsouG9vis6fctX+TxgmwxTuYi/+xc7dQ==", "devOptional": true }, "node_modules/@prisma/engines": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.0.tgz", - "integrity": "sha512-XhE9v3hDQTNgCYMjogcCYKi7HCEkZf9WwTGuXy8cmY8JUijvU0ap4M7pGLx4pBblkp5EwUsYzw1YLtH7yi0GZw==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.1.tgz", + "integrity": "sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/debug": "6.17.0", - "@prisma/engines-version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a", - "@prisma/fetch-engine": "6.17.0", - "@prisma/get-platform": "6.17.0" + "@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" } }, "node_modules/@prisma/engines-version": { - "version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a.tgz", - "integrity": "sha512-G0VU4uFDreATgTz4sh3dTtU2C+jn+J6c060ixavWZaUaSRZsNQhSPW26lbfez7GHzR02RGCdqs5UcSuGBC3yLw==", + "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==", "devOptional": true }, "node_modules/@prisma/fetch-engine": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.0.tgz", - "integrity": "sha512-YSl5R3WIAPrmshYPkaaszOsBIWRAovOCHn3y7gkTNGG51LjYW4pi6PFNkGouW6CA06qeTjTbGrDRCgFjnmVWDg==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.1.tgz", + "integrity": "sha512-AYZiHOs184qkDMiTeshyJCtyL4yERkjfTkJiSJdYuSfc24m94lTNL5+GFinZ6vVz+ktX4NJzHKn1zIFzGTWrWg==", "devOptional": true, "dependencies": { - "@prisma/debug": "6.17.0", - "@prisma/engines-version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a", - "@prisma/get-platform": "6.17.0" + "@prisma/debug": "6.17.1", + "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", + "@prisma/get-platform": "6.17.1" } }, "node_modules/@prisma/get-platform": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.0.tgz", - "integrity": "sha512-3tEKChrnlmLXPd870oiVfRvj7vVKuxqP349hYaMDsbV4TZd3+lFqw8KTI2Tbq5DopamfNuNqhVCj+R6ZxKKYGQ==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.1.tgz", + "integrity": "sha512-AKEn6fsfz0r482S5KRDFlIGEaq9wLNcgalD1adL+fPcFFblIKs1sD81kY/utrHdqKuVC6E1XSRpegDK3ZLL4Qg==", "devOptional": true, "dependencies": { - "@prisma/debug": "6.17.0" + "@prisma/debug": "6.17.1" } }, "node_modules/@scarf/scarf": { @@ -3783,17 +4323,18 @@ "dev": true }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", - "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", "dependencies": { + "@types/ms": "*", "@types/node": "*" } }, "node_modules/@types/luxon": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz", - "integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==" + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==" }, "node_modules/@types/methods": { "version": "1.1.4", @@ -3807,6 +4348,11 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, "node_modules/@types/multer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", @@ -5493,6 +6039,20 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/bullmq": { + "version": "5.58.0", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.58.0.tgz", + "integrity": "sha512-WIjvoSQ9jprId2gAZaPMQu3jaAkRCN8Wjj/pR39knwjULB7asB6XoSTqvnSbOsfyHMKln8el0MRvRJVY9VdmFA==", + "dependencies": { + "cron-parser": "^4.9.0", + "ioredis": "^5.4.1", + "msgpackr": "^1.11.2", + "node-abort-controller": "^3.1.1", + "semver": "^7.5.4", + "tslib": "^2.0.0", + "uuid": "^9.0.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -5670,9 +6230,9 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", + "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", "dev": true }, "node_modules/chokidar": { @@ -5853,6 +6413,14 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -5869,11 +6437,22 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -5884,8 +6463,16 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } }, "node_modules/combined-stream": { "version": "1.0.8", @@ -6088,17 +6675,28 @@ "dev": true }, "node_modules/cron": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.0.tgz", - "integrity": "sha512-ciiYNLfSlF9MrDqnbMdRWFiA6oizSF7kA1osPP9lRzNu0Uu+AWog1UKy7SkckiDY2irrNjeO6qLyKnXC8oxmrw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.3.tgz", + "integrity": "sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw==", "dependencies": { - "@types/luxon": "~3.6.0", - "luxon": "~3.6.0" + "@types/luxon": "~3.7.0", + "luxon": "~3.7.0" }, "engines": { "node": ">=18.x" } }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6230,6 +6828,14 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6244,6 +6850,14 @@ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", "devOptional": true }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -6940,32 +7554,6 @@ "node": ">=4" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/external-editor/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", @@ -7864,6 +8452,29 @@ "kind-of": "^6.0.2" } }, + "node_modules/ioredis": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", + "integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==", + "dependencies": { + "@ioredis/commands": "^1.3.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -8977,11 +9588,21 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -9062,9 +9683,9 @@ } }, "node_modules/luxon": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", - "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "engines": { "node": ">=12" } @@ -9294,6 +9915,35 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/msgpackr": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", + "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, "node_modules/multer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", @@ -9386,8 +10036,7 @@ "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", - "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", - "dev": true + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" }, "node_modules/node-emoji": { "version": "1.11.0", @@ -9404,6 +10053,20 @@ "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", "devOptional": true }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -9603,15 +10266,6 @@ "node": ">=8" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-cancelable": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", @@ -10049,14 +10703,14 @@ } }, "node_modules/prisma": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.0.tgz", - "integrity": "sha512-rcvldz98r+2bVCs0MldQCBaaVJRCj9Ew4IqphLATF89OJcSzwRQpwnKXR+W2+2VjK7/o2x3ffu5+2N3Muu6Dbw==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.1.tgz", + "integrity": "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/config": "6.17.0", - "@prisma/engines": "6.17.0" + "@prisma/config": "6.17.1", + "@prisma/engines": "6.17.1" }, "bin": { "prisma": "build/index.js" @@ -10251,6 +10905,25 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -10598,6 +11271,47 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10699,6 +11413,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -10793,6 +11520,11 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -11085,9 +11817,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz", - "integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==", + "version": "5.29.4", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.4.tgz", + "integrity": "sha512-gJFDz/gyLOCQtWwAgqs6Rk78z9ONnqTnlW11gimG9nLap8drKa3AJBKpzIQMIjl5PD2Ix+Tn+mc/tfoT2tgsng==", "dependencies": { "@scarf/scarf": "=1.4.0" } @@ -11349,18 +12081,6 @@ "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", "devOptional": true }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -11819,6 +12539,18 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index 635a791..c8db806 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", + "start:variants": "node dist/attachments/workers/variants.worker.js", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", @@ -48,17 +49,20 @@ "@nestjs/platform-express": "^11.1.6", "@nestjs/schedule": "^6.0.0", "@nestjs/swagger": "^11.2.0", - "@prisma/client": "^6.14.0", + "@prisma/client": "^6.17.1", + "bullmq": "^5.58.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "express-session": "^1.18.2", "file-type": "^21.0.0", + "ioredis": "^5.7.0", "multer": "^2.0.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "passport-openidconnect": "^0.1.2", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "sharp": "^0.34.3" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -82,7 +86,7 @@ "globals": "^16.0.0", "jest": "^29.7.0", "prettier": "^3.4.2", - "prisma": "^6.17.0", + "prisma": "^6.17.1", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "ts-jest": "^29.2.5", diff --git a/prisma/migrations/20250825173419_attachment_variants_model/migration.sql b/prisma/migrations/20250825173419_attachment_variants_model/migration.sql new file mode 100644 index 0000000..e91621a --- /dev/null +++ b/prisma/migrations/20250825173419_attachment_variants_model/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "public"."attachment_variants" ( + "id" SERIAL NOT NULL, + "attachment_id" INTEGER NOT NULL, + "variant" TEXT NOT NULL, + "patch" TEXT NOT NULL, + "bytes" INTEGER NOT NULL, + "width" INTEGER, + "height" INTEGER, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "attachment_variants_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "attachment_variants_attachment_id_variant_key" ON "public"."attachment_variants"("attachment_id", "variant"); + +-- AddForeignKey +ALTER TABLE "public"."attachment_variants" ADD CONSTRAINT "attachment_variants_attachment_id_fkey" FOREIGN KEY ("attachment_id") REFERENCES "public"."attachments"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index afb3fda..1d62fbd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -340,14 +340,14 @@ model Blobs { refcount Int @default(0) created_at DateTime @default(now()) - attachments Attachments[] + attachments Attachments[] @relation("AttachmnentBlob") @@map("blobs") } model Attachments { id Int @id @default(autoincrement()) - blob Blobs @relation(fields: [sha256], references: [sha256], onUpdate: Cascade) + blob Blobs @relation("AttachmnentBlob",fields: [sha256], references: [sha256], onUpdate: Cascade) sha256 String @db.Char(64) owner_type String //EXPENSES ou éventuellement autre chose comme scan ONU ou photos d'employés, etc @@ -361,11 +361,28 @@ model Attachments { expenses Expenses[] @relation("ExpenseAttachment") expenses_archive ExpensesArchive[] @relation("ExpenseArchiveAttachment") + AttachmentVariants AttachmentVariants[] @relation("attachmentVariantAttachment") + @@index([owner_type, owner_id, created_at]) @@index([sha256]) @@map("attachments") } +model AttachmentVariants { + id Int @id @default(autoincrement()) + attachment_id Int + attachment Attachments @relation("attachmentVariantAttachment",fields: [attachment_id], references: [id], onDelete: Cascade) + variant String + patch String + bytes Int + width Int? + height Int? + created_at DateTime @default(now()) + + @@unique([attachment_id, variant]) + @@map("attachment_variants") +} + model Preferences { id Int @id @default(autoincrement()) user Users @relation("UserPreferences", fields: [user_id], references: [id]) diff --git a/src/app.module.ts b/src/app.module.ts index daf8229..04f2348 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,21 +1,21 @@ 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 { 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 { 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 { 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 { 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 { PayperiodsModule } from './modules/pay-periods/pay-periods.module'; +// import { PayperiodsModule } from './modules/pay-periods/pay-periods.module'; import { PreferencesModule } from './modules/preferences/preferences.module'; import { PrismaModule } from './prisma/prisma.module'; import { ScheduleModule } from '@nestjs/schedule'; @@ -30,7 +30,7 @@ import { SchedulePresetsModule } from './modules/schedule-presets/schedule-prese @Module({ imports: [ - ArchivalModule, + // ArchivalModule, AuthenticationModule, BankCodesModule, BusinessLogicsModule, @@ -38,12 +38,12 @@ import { SchedulePresetsModule } from './modules/schedule-presets/schedule-prese // CsvExportModule, CustomersModule, EmployeesModule, - ExpensesModule, + // ExpensesModule, HealthModule, - LeaveRequestsModule, + // LeaveRequestsModule, NotificationsModule, OauthSessionsModule, - PayperiodsModule, + // PayperiodsModule, PreferencesModule, PrismaModule, ScheduleModule.forRoot(), //cronjobs diff --git a/src/modules/archival/archival.module.ts b/src/modules/archival/archival.module.ts index 03f1bf9..e48e908 100644 --- a/src/modules/archival/archival.module.ts +++ b/src/modules/archival/archival.module.ts @@ -1,34 +1,34 @@ -import { Module } from "@nestjs/common"; -import { ScheduleModule } from "@nestjs/schedule"; -import { TimesheetsModule } from "../timesheets/timesheets.module"; -import { ExpensesModule } from "../expenses/expenses.module"; -import { ShiftsModule } from "../shifts/shifts.module"; -import { LeaveRequestsModule } from "../leave-requests/leave-requests.module"; -import { ArchivalService } from "./services/archival.service"; -import { EmployeesArchiveController } from "./controllers/employees-archive.controller"; -import { ExpensesArchiveController } from "./controllers/expenses-archive.controller"; -import { LeaveRequestsArchiveController } from "./controllers/leave-requests-archive.controller"; -import { ShiftsArchiveController } from "./controllers/shifts-archive.controller"; -import { TimesheetsArchiveController } from "./controllers/timesheets-archive.controller"; -import { EmployeesModule } from "../employees/employees.module"; +// import { Module } from "@nestjs/common"; +// import { ScheduleModule } from "@nestjs/schedule"; +// import { TimesheetsModule } from "../timesheets/timesheets.module"; +// import { ExpensesModule } from "../expenses/expenses.module"; +// import { ShiftsModule } from "../shifts/shifts.module"; +// import { LeaveRequestsModule } from "../leave-requests/leave-requests.module"; +// import { ArchivalService } from "./services/archival.service"; +// import { EmployeesArchiveController } from "./controllers/employees-archive.controller"; +// import { ExpensesArchiveController } from "./controllers/expenses-archive.controller"; +// import { LeaveRequestsArchiveController } from "./controllers/leave-requests-archive.controller"; +// import { ShiftsArchiveController } from "./controllers/shifts-archive.controller"; +// import { TimesheetsArchiveController } from "./controllers/timesheets-archive.controller"; +// import { EmployeesModule } from "../employees/employees.module"; -@Module({ - imports: [ - EmployeesModule, - ScheduleModule, - TimesheetsModule, - ExpensesModule, - ShiftsModule, - LeaveRequestsModule, - ], - providers: [ArchivalService], - controllers: [ - EmployeesArchiveController, - ExpensesArchiveController, - LeaveRequestsArchiveController, - ShiftsArchiveController, - TimesheetsArchiveController, - ], -}) +// @Module({ +// imports: [ +// EmployeesModule, +// ScheduleModule, +// TimesheetsModule, +// ExpensesModule, +// ShiftsModule, +// LeaveRequestsModule, +// ], +// providers: [ArchivalService], +// controllers: [ +// EmployeesArchiveController, +// ExpensesArchiveController, +// LeaveRequestsArchiveController, +// ShiftsArchiveController, +// TimesheetsArchiveController, +// ], +// }) -export class ArchivalModule {} \ No newline at end of file +// export class ArchivalModule {} \ No newline at end of file diff --git a/src/modules/attachments/attachments.module.ts b/src/modules/attachments/attachments.module.ts new file mode 100644 index 0000000..ee8883f --- /dev/null +++ b/src/modules/attachments/attachments.module.ts @@ -0,0 +1,19 @@ +import { ScheduleModule } from "@nestjs/schedule"; +import { PrismaService } from "src/prisma/prisma.service"; +import { ArchivalAttachmentService } from "./services/archival-attachment.service"; +import { Module } from "@nestjs/common"; +import { GarbargeCollectorService } from "./services/garbage-collector.service"; + +@Module({ + imports: [ScheduleModule.forRoot()], + providers: [ + PrismaService, + ArchivalAttachmentService, + GarbargeCollectorService, + ], + exports: [ + ArchivalAttachmentService, + GarbargeCollectorService + ], +}) +export class ArchivalAttachmentModule {} \ No newline at end of file diff --git a/src/modules/attachments/controllers/attachments.controller.ts b/src/modules/attachments/controllers/attachments.controller.ts index 1defed3..3a75d6a 100644 --- a/src/modules/attachments/controllers/attachments.controller.ts +++ b/src/modules/attachments/controllers/attachments.controller.ts @@ -2,11 +2,14 @@ import { FileInterceptor } from "@nestjs/platform-express"; import { DiskStorageService } from "../services/disk-storage.service"; import { Controller,NotFoundException, UseInterceptors, Post, Get, Param, Res, - UploadedFile, BadRequestException, UnsupportedMediaTypeException, Body, Delete + UploadedFile, BadRequestException, UnsupportedMediaTypeException, Body, Delete, + Query, + DefaultValuePipe, + ParseIntPipe } from "@nestjs/common"; import { maxUploadBytes, allowedMimes } from "../config/upload.config"; import { memoryStorage } from 'multer'; -import { fileTypeFromBuffer } from "file-type"; +import { fileTypeFromBuffer, fileTypeFromFile } from "file-type"; import { Readable } from "node:stream"; import { PrismaService } from "src/prisma/prisma.service"; import { UploadMetaAttachmentsDto } from "../dtos/upload-meta-attachments.dto"; @@ -15,27 +18,35 @@ import * as path from 'node:path'; import { promises as fsp } from 'node:fs'; import { createReadStream } from "node:fs"; import { Response } from 'express'; +import { VariantsQueue } from "../services/variants.queue"; +import { AdminSearchDto } from "../dtos/admin-search.dto"; @Controller('attachments') export class AttachmentsController { constructor( private readonly disk: DiskStorageService, private readonly prisma: PrismaService, + private readonly variantsQ: VariantsQueue, ) {} @Get(':id') - async getById(@Param('id') id: string, @Res() res: Response) { + async getById( + @Param('id') id: string, + @Query('variant') variant: string | undefined, + @Res() res: Response, + ) { const num_id = Number(id); if(!Number.isFinite(num_id)) throw new NotFoundException('Invalid id'); - const att = await this.prisma.attachments.findUnique({ + const attachment = await this.prisma.attachments.findUnique({ where: { id: num_id }, include: { blob: true }, }); - if (!att) throw new NotFoundException(); + if (!attachment) throw new NotFoundException(); - - const abs = path.join(resolveAttachmentsRoot(), att.blob.storage_path); + const relative = variant ? `${attachment.blob.storage_path}.${variant}` : attachment.blob.storage_path; + const abs = path.join(resolveAttachmentsRoot(), relative); + let stat; try { stat = await fsp.stat(abs); @@ -43,9 +54,14 @@ export class AttachmentsController { throw new NotFoundException('File not found'); } - res.set('Content-Type', att.blob.mime); + let mime = attachment.blob.mime; + try { + const kind = await fileTypeFromFile(abs); + if(kind?.mime) mime = kind.mime; + } catch {} + res.set('Content-Type', mime); res.set('Content-Length', String(stat.size)); - res.set('ETag', `"sha256-${att.blob.sha256}"`); + res.set('ETag', `"sha256-${attachment.blob.sha256}${variant ? '.'+variant : ''}"`); res.set('Last-Modified', stat.mtime.toUTCString()); res.set('Cache-Control', 'private, max-age=31536000, immutable'); res.set('X-Content-Type-Options', 'nosniff'); @@ -53,7 +69,17 @@ export class AttachmentsController { createReadStream(abs).pipe(res); } - // DEV version, uncomment once connected to DB and distant server + @Get('variants/:id') + async listVariants(@Param('id')id: string) { + const num_id = Number(id); + if(!Number.isFinite(num_id)) throw new NotFoundException('Invalid variant id'); + 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 }, + }); + } + @Delete(':id') async remove(@Param('id') id: string) { const result = await this.prisma.$transaction(async (tx) => { @@ -136,6 +162,8 @@ export class AttachmentsController { return att; }); + await this.variantsQ.enqueue(attachment.id, detected_mime); + return { ok: true, id: attachment.id, @@ -148,4 +176,39 @@ export class AttachmentsController { owner_id: attachment.owner_id, }; } -} \ No newline at end of file + + @Get('/admin/search') + async adminSearch( + @Query() query: AdminSearchDto ) { + const where: any = {}; + if (query.owner_type) where.owner_type = query.owner_type; + if (query.owner_id) where.owner_id = query.owner_id; + + if (query.date_from || query.date_to) { + where.created_at = {}; + if (query.date_from) where.created_at.gte = new Date(query.date_from + 'T00:00:00Z'); + if (query.date_to) where.created_at.lte = new Date(query.date_to + 'T23:59:59Z'); + } + + const page = query.page ?? 1; + const page_size = query.page_size ?? 50; + const skip = (page - 1)* page_size; + const take = page_size; + + const [items, total] = await this.prisma.$transaction([ + this.prisma.attachments.findMany({ + where, + orderBy: { created_at: 'desc' }, + skip, take, + include: { + blob: { + select: { mime: true, size: true, storage_path: true, sha256: true }, + }, + }, + }), + this.prisma.attachments.count({ where }), + ]); + + return { page, page_size: take, total, items }; + } +} diff --git a/src/modules/attachments/dtos/admin-search.dto.ts b/src/modules/attachments/dtos/admin-search.dto.ts new file mode 100644 index 0000000..ca1edc6 --- /dev/null +++ b/src/modules/attachments/dtos/admin-search.dto.ts @@ -0,0 +1,34 @@ +import { Type } from "class-transformer"; +import { IsInt, IsISO8601, IsOptional, IsString, Max, Min } from "class-validator"; + +export class AdminSearchDto { + + @IsOptional() + @IsString() + owner_type?: string; + + @IsOptional() + @IsString() + owner_id?: string; + + @IsOptional() + @IsISO8601() + date_from?: string; + + @IsOptional() + @IsISO8601() + date_to?: string; + + @IsOptional() + @Type(()=> Number) + @IsInt() + @Min(1) + page?: number = 1; + + @IsOptional() + @Type(()=> Number) + @IsInt() + @Min(1) + @Max(200) + page_size?: number = 50; +} \ No newline at end of file diff --git a/src/modules/attachments/services/archival-attachment.service.ts b/src/modules/attachments/services/archival-attachment.service.ts new file mode 100644 index 0000000..501923d --- /dev/null +++ b/src/modules/attachments/services/archival-attachment.service.ts @@ -0,0 +1,60 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { Cron } from "@nestjs/schedule"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class ArchivalAttachmentService { + private readonly logger = new Logger(ArchivalAttachmentService.name) + private readonly batch_size = Number(process.env.ARCHIVE_BATCH_SIZE || 1000); + private readonly cron_expression = process.env.ARCHIVE_CRON || '0 3 * * 1'; + + constructor( private readonly prisma: PrismaService) {} + + private startOfYear(): Date { + const now = new Date(); + return new Date(Date.UTC(now.getUTCFullYear(), 0, 1, 0, 0, 0, 0)); + } + + @Cron(function (this: ArchivalAttachmentService) { return this.cron_expression; } as any) + async runScheduled() { + await this.archiveCutoffToStartOfYear(); + } + + //archive everything before current year + async archiveCutoffToStartOfYear() { + const cutoff = this.startOfYear(); + this.logger.log(`Archival: cutoff=${cutoff.toISOString()} batch=${this.batch_size}`); + + let moved = 0, total = 0, i = 0; + do { + moved = await this.archiveBatch(cutoff, this.batch_size); + total += moved; + i++; + if(moved > 0) this.logger.log(`Batch #${i}: moved ${moved}`); + }while (moved === this.batch_size); + + this.logger.log(`Archival done: total moved : ${total}`); + return { moved: total }; + } + + //only moves table content to archive and not blobs. + private async archiveBatch(cutoff: Date, batch_size: number): Promise { + const moved = await this.prisma.$executeRaw` + WITH moved AS ( + DELETE FROM "attachments" + WHERE id IN ( + SELECT id FROM "attachments" + WHERE created_at < ${cutoff} + ORDER BY id + LIMIT ${batch_size} + ) + RETURNING id, sha256, owner_type, owner_id, original_name, status, retention_policy, created_by, created_at + ) + INSERT INTO archive.attachments_archive + (id, sha256, owner_type, owner_id, original_name, status, retention_policy, created_by, created_at) + SELECT * FROM moved; + `; + return Number(moved) || 0; + } + +} \ No newline at end of file diff --git a/src/modules/attachments/services/disk-storage.service.ts b/src/modules/attachments/services/disk-storage.service.ts index 0ef8e22..0c485c6 100644 --- a/src/modules/attachments/services/disk-storage.service.ts +++ b/src/modules/attachments/services/disk-storage.service.ts @@ -12,7 +12,7 @@ export class DiskStorageService { private casPath(hash: string) { const a = hash.slice(0,2), b = hash.slice(2,4); - return `sha256/${a}/${b}/${hash}`; //relatif pour stockage dans la DB + return `sha256/${a}/${b}/${hash}`; } //chemin absolue du storage diff --git a/src/modules/attachments/services/garbage-collector.service.ts b/src/modules/attachments/services/garbage-collector.service.ts new file mode 100644 index 0000000..13e8c21 --- /dev/null +++ b/src/modules/attachments/services/garbage-collector.service.ts @@ -0,0 +1,78 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { Cron } from "@nestjs/schedule"; +import { PrismaService } from 'src/prisma/prisma.service'; +import * as path from 'node:path'; +import { promises as fsp } from 'node:fs'; +import { resolveAttachmentsRoot } from "src/config/attachment.config"; + +@Injectable() +export class GarbargeCollectorService { + private readonly logger = new Logger(GarbargeCollectorService.name); + + //.env refs + private readonly batch_size = Number(process.env.GC_BATCH_SIZE || 500); + private readonly cron_expression = process.env.GC_CRON || '15 4 * * *'; // everyday at 04:15 AM + + //fetchs root of storage + private readonly root = resolveAttachmentsRoot(); + + constructor(private readonly prisma: PrismaService) {} + + //planif for the Cronjob + @Cron(function(this:GarbargeCollectorService) { return this.cron_expression; } as any) + async runScheduled() { + await this.collect(); + } + + //Manage Garbage collecting by batch of elements until a batch != full + async collect() { + let total = 0, round = 0; + //infinit loop (;;) with break + for(;;) { + round++; + const num = await this.collectBatch(); + total += num; + this.logger.log(`Garbage Collector round #${round} removed ${num}`); + if(num < this.batch_size) break; //breaks if not a full batch + } + this.logger.log(`Garbage Collecting done: total removed ${total}`); + return { removed:total }; + } + + //Manage a single lot of orphan blobs + private async collectBatch(): Promise { + const blobs = await this.prisma.blobs.findMany({ + where: { refcount: { lte: 0 } }, + select: { sha256: true, storage_path: true }, + take: this.batch_size, + }); + if(blobs.length === 0) return 0; + + // delete original file and all its variants in the same file + await Promise.all( + blobs.map(async (blob)=> { + const absolute_path = path.join(this.root, blob.storage_path); + await this.deleteFileIfExists(absolute_path); //tries to delete original file if found + + const dir = path.dirname(absolute_path); + const base = path.basename(absolute_path); + try { + const entries = await fsp.readdir(dir, { withFileTypes: true}); + const targets = entries.filter(entry => entry.isFile() && entry.name.startsWith(base + '.')) + .map(entry => path.join(dir, entry.name)); + //deletes all variants + await Promise.all(targets.map(target => this.deleteFileIfExists(target))); + } catch {} + }) + ); + //deletes blobs lignes if file is deleted + const hashes = blobs.map(blob => blob.sha256); + await this.prisma.blobs.deleteMany({where: { sha256: { in: hashes } } }); + return blobs.length; + } + + //helper: deletes path if exists and ignore errors + private async deleteFileIfExists(path: string) { + try { await fsp.unlink(path); } catch {} + } +} \ No newline at end of file diff --git a/src/modules/attachments/services/variants.queue.ts b/src/modules/attachments/services/variants.queue.ts new file mode 100644 index 0000000..07c48c0 --- /dev/null +++ b/src/modules/attachments/services/variants.queue.ts @@ -0,0 +1,20 @@ +import { Queue } from "bullmq"; + +export class VariantsQueue { + private queue : Queue; + + constructor() { + const name = `${process.env.BULL_PREFIX || 'attachments'}:variants`; + this.queue = new Queue(name, { connection: { url: process.env.REDIS_URL! } }); + } + + enqueue(attachment_id: number, mime: string) { + if(!mime.startsWith('image/')) { + return Promise.resolve(); + } + return this.queue.add('generate', + { attachment_id, mime }, + { attempts: 3, backoff: { type: 'exponential', delay:2000 } } + ); + } +} diff --git a/src/modules/attachments/workers/variants.worker.ts b/src/modules/attachments/workers/variants.worker.ts new file mode 100644 index 0000000..de3ceb3 --- /dev/null +++ b/src/modules/attachments/workers/variants.worker.ts @@ -0,0 +1,54 @@ +import 'dotenv/config'; +import { Worker } from 'bullmq'; +import sharp from 'sharp'; +import { PrismaClient } from '@prisma/client'; +import * as path from 'node:path'; +import { promises as fsp } from 'node:fs'; +import { resolveAttachmentsRoot } from 'src/config/attachment.config'; + +const prisma = new PrismaClient(); +const q_name = `${process.env.BULL_PREFIX || 'attachments'}:variants`; +const root = resolveAttachmentsRoot(); + +const variants = [ + { name: 'thumb.jpeg', build: (s:sharp.Sharp) => s.rotate().jpeg({quality:80}).resize({width:128}) }, + { name: '256w.webp' , build: (s:sharp.Sharp) => s.rotate().webp({quality:80}).resize({width:256}) }, + { name: '1024w.webp', build: (s:sharp.Sharp) => s.rotate().webp({quality:82}).resize({width:1024}) }, +] + +new Worker(q_name, async job => { + const attachment_id: number = job.data.attachmentId ?? job.data.attachment_id; + if (!attachment_id) return; + + const attachment = await prisma.attachments.findUnique({ + where: { id: attachment_id }, + include: { blob: true }, + }); + if(!attachment) return; + + const source_abs = path.join(root, attachment.blob.storage_path); + + for(const variant of variants) { + const relative = `${attachment.blob.storage_path}.${variant.name}`; + const out_Abs = path.join(root, relative); + + //try for idem paths + try{ await fsp.stat(out_Abs); continue; } catch{} + + await fsp.mkdir(path.dirname(out_Abs), { recursive: true }); + + //generate variant + await variant.build(sharp(source_abs)).toFile(out_Abs); + + //meta data of generated variant file + const meta = await sharp(out_Abs).metadata(); + const bytes = (await fsp.stat(out_Abs)).size; + await prisma.attachmentVariants.upsert({ + where: { attachment_id_variant: { attachment_id: attachment_id, variant: variant.name } }, + update: { path: relative, bytes, width: meta.width ?? null, height: meta.height ?? null }, + create: { path: relative, bytes, width: meta.width ?? null, height: meta.height ?? null, attachment_id: attachment_id, variant: variant.name }, + } as any ); + } +}, { + connection: { url: process.env.REDIS_URL }, concurrency: 3 } +); \ No newline at end of file diff --git a/src/modules/business-logics/services/overtime.service.ts b/src/modules/business-logics/services/overtime.service.ts index 24c7a75..be8c15c 100644 --- a/src/modules/business-logics/services/overtime.service.ts +++ b/src/modules/business-logics/services/overtime.service.ts @@ -1,152 +1,247 @@ import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../../../prisma/prisma.service'; import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils'; -import { Prisma } from '@prisma/client'; +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 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) {} - //calculate daily overtime - async getDailyOvertimeHours(employee_id: number, date: Date): Promise { - const shifts = await this.prisma.shifts.findMany({ - where: { date: date, timesheet: { employee_id: employee_id } }, - select: { start_time: true, end_time: true }, - }); - 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(employee_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: { - date: { gte:week_start, lte: week_end }, - timesheet: { employee_id }, - 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. + async getWeekOvertimeSummary( timesheet_id: number, date: Date, tx?: Tx ): Promise{ 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_start = getWeekStart(date); const week_end = getWeekEnd(week_start); - //gets all regular shifts and order them by desc - const regular_shifts_desc = await db.shifts.findMany({ + const shifts = await db.shifts.findMany({ where: { - date: { gte:week_start, lte: week_end }, - timesheet: { employee_id }, - bank_code_id: regular.id, + timesheet_id, + date: { gte: week_start, lte: week_end }, + bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } }, }, - 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'}], + select: { date: true, start_time: true, end_time: true }, + orderBy: [{date: 'asc'}, {start_time: 'asc'}], }); - 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; + 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); } - 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`); + + 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, + }; } - //apply modifier to overtime hours - // calculateOvertimePay(overtime_hours: number, modifier: number): number { - // const pay = overtime_hours * modifier; - // this.logger.debug(`Overtime payable hours = ${pay.toFixed(2)} (hours ${overtime_hours}, modifier ${modifier})`); + // //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' }], + // }); - // return pay; + // 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/expenses/controllers/expenses.controller.ts b/src/modules/expenses/controllers/expenses.controller.ts index 11bef7f..aea3161 100644 --- a/src/modules/expenses/controllers/expenses.controller.ts +++ b/src/modules/expenses/controllers/expenses.controller.ts @@ -1,95 +1,95 @@ -import { Body, Controller, Get, Param, Put, } from "@nestjs/common"; -import { Roles as RoleEnum } from '.prisma/client'; -import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; -import { RolesAllowed } from "src/common/decorators/roles.decorators"; -import { ExpensesCommandService } from "../services/expenses-command.service"; -import { UpsertExpenseDto } from "../dtos/upsert-expense.dto"; -import { UpsertExpenseResult } from "../types and interfaces/expenses.types.interfaces"; -import { DayExpensesDto } from "src/modules/timesheets/dtos/timesheet-period.dto"; -import { ExpensesQueryService } from "../services/expenses-query.service"; +// import { Body, Controller, Get, Param, Put, } from "@nestjs/common"; +// import { Roles as RoleEnum } from '.prisma/client'; +// import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +// import { RolesAllowed } from "src/common/decorators/roles.decorators"; +// import { ExpensesCommandService } from "../services/expenses-command.service"; +// import { UpsertExpenseDto } from "../dtos/upsert-expense.dto"; +// import { UpsertExpenseResult } from "../types and interfaces/expenses.types.interfaces"; +// import { DayExpensesDto } from "src/modules/timesheets/~misc_deprecated-files/timesheet-period.dto"; +// import { ExpensesQueryService } from "../services/expenses-query.service"; -@ApiTags('Expenses') -@ApiBearerAuth('access-token') -// @UseGuards() -@Controller('Expenses') -export class ExpensesController { - constructor( - private readonly query: ExpensesQueryService, - private readonly command: ExpensesCommandService, - ) {} +// @ApiTags('Expenses') +// @ApiBearerAuth('access-token') +// // @UseGuards() +// @Controller('Expenses') +// export class ExpensesController { +// constructor( +// private readonly query: ExpensesQueryService, +// private readonly command: ExpensesCommandService, +// ) {} - @Put('upsert/:email/:date') - async upsert_by_date( - @Param('email') email: string, - @Param('date') date: string, - @Body() dto: UpsertExpenseDto, - ): Promise { - return this.command.upsertExpensesByDate(email, date, dto); - } +// @Put('upsert/:email/:date') +// async upsert_by_date( +// @Param('email') email: string, +// @Param('date') date: string, +// @Body() dto: UpsertExpenseDto, +// ): Promise { +// return this.command.upsertExpensesByDate(email, date, dto); +// } - @Get('list/:email/:year/:period_no') - async findExpenseListByPayPeriodAndEmail( - @Param('email') email:string, - @Param('year') year: number, - @Param('period_no') period_no: number, - ): Promise { - return this.query.findExpenseListByPayPeriodAndEmail(email, year, period_no); - } +// @Get('list/:email/:year/:period_no') +// async findExpenseListByPayPeriodAndEmail( +// @Param('email') email:string, +// @Param('year') year: number, +// @Param('period_no') period_no: number, +// ): Promise { +// return this.query.findExpenseListByPayPeriodAndEmail(email, year, period_no); +// } - //_____________________________________________________________________________________________ - // Deprecated or unused methods - //_____________________________________________________________________________________________ +// //_____________________________________________________________________________________________ +// // Deprecated or unused methods +// //_____________________________________________________________________________________________ - // @Post() - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) - // @ApiOperation({ summary: 'Create expense' }) - // @ApiResponse({ status: 201, description: 'Expense created',type: CreateExpenseDto }) - // @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) - // create(@Body() dto: CreateExpenseDto): Promise { - // return this.query.create(dto); - // } +// // @Post() +// // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) +// // @ApiOperation({ summary: 'Create expense' }) +// // @ApiResponse({ status: 201, description: 'Expense created',type: CreateExpenseDto }) +// // @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) +// // create(@Body() dto: CreateExpenseDto): Promise { +// // return this.query.create(dto); +// // } - // @Get() - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) - // @ApiOperation({ summary: 'Find all expenses' }) - // @ApiResponse({ status: 201, description: 'List of expenses found',type: CreateExpenseDto, isArray: true }) - // @ApiResponse({ status: 400, description: 'List of expenses not found' }) - // @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) - // findAll(@Query() filters: SearchExpensesDto): Promise { - // return this.query.findAll(filters); - // } +// // @Get() +// // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) +// // @ApiOperation({ summary: 'Find all expenses' }) +// // @ApiResponse({ status: 201, description: 'List of expenses found',type: CreateExpenseDto, isArray: true }) +// // @ApiResponse({ status: 400, description: 'List of expenses not found' }) +// // @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) +// // findAll(@Query() filters: SearchExpensesDto): Promise { +// // return this.query.findAll(filters); +// // } - // @Get(':id') - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) - // @ApiOperation({ summary: 'Find expense' }) - // @ApiResponse({ status: 201, description: 'Expense found',type: CreateExpenseDto }) - // @ApiResponse({ status: 400, description: 'Expense not found' }) - // findOne(@Param('id', ParseIntPipe) id: number): Promise { - // return this.query.findOne(id); - // } +// // @Get(':id') +// // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) +// // @ApiOperation({ summary: 'Find expense' }) +// // @ApiResponse({ status: 201, description: 'Expense found',type: CreateExpenseDto }) +// // @ApiResponse({ status: 400, description: 'Expense not found' }) +// // findOne(@Param('id', ParseIntPipe) id: number): Promise { +// // return this.query.findOne(id); +// // } - // @Patch(':id') - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) - // @ApiOperation({ summary: 'Expense shift' }) - // @ApiResponse({ status: 201, description: 'Expense updated',type: CreateExpenseDto }) - // @ApiResponse({ status: 400, description: 'Expense not found' }) - // update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateExpenseDto) { - // return this.query.update(id,dto); - // } +// // @Patch(':id') +// // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) +// // @ApiOperation({ summary: 'Expense shift' }) +// // @ApiResponse({ status: 201, description: 'Expense updated',type: CreateExpenseDto }) +// // @ApiResponse({ status: 400, description: 'Expense not found' }) +// // update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateExpenseDto) { +// // return this.query.update(id,dto); +// // } - // @Delete(':id') - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) - // @ApiOperation({ summary: 'Delete expense' }) - // @ApiResponse({ status: 201, description: 'Expense deleted',type: CreateExpenseDto }) - // @ApiResponse({ status: 400, description: 'Expense not found' }) - // remove(@Param('id', ParseIntPipe) id: number): Promise { - // return this.query.remove(id); - // } +// // @Delete(':id') +// // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) +// // @ApiOperation({ summary: 'Delete expense' }) +// // @ApiResponse({ status: 201, description: 'Expense deleted',type: CreateExpenseDto }) +// // @ApiResponse({ status: 400, description: 'Expense not found' }) +// // remove(@Param('id', ParseIntPipe) id: number): Promise { +// // return this.query.remove(id); +// // } - // @Patch('approval/:id') - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) - // async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) { - // return this.command.updateApproval(id, isApproved); - // } +// // @Patch('approval/:id') +// // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) +// // async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) { +// // return this.command.updateApproval(id, isApproved); +// // } -} \ No newline at end of file +// } \ No newline at end of file diff --git a/src/modules/expenses/expenses.module.ts b/src/modules/expenses/expenses.module.ts index 6201b91..490ec9e 100644 --- a/src/modules/expenses/expenses.module.ts +++ b/src/modules/expenses/expenses.module.ts @@ -1,23 +1,23 @@ -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"; +// 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, - ], -}) +// @Module({ +// imports: [BusinessLogicsModule, SharedModule], +// controllers: [ExpensesController], +// providers: [ +// ExpensesQueryService, +// ExpensesArchivalService, +// ExpensesCommandService, +// ], +// exports: [ +// ExpensesQueryService, +// ExpensesArchivalService, +// ], +// }) -export class ExpensesModule {} \ No newline at end of file +// export class ExpensesModule {} \ No newline at end of file diff --git a/src/modules/expenses/services/expenses-command.service.ts b/src/modules/expenses/services/expenses-command.service.ts index 1511eb0..723bb7a 100644 --- a/src/modules/expenses/services/expenses-command.service.ts +++ b/src/modules/expenses/services/expenses-command.service.ts @@ -1,250 +1,249 @@ -import { BaseApprovalService } from "src/common/shared/base-approval.service"; -import { Expenses, Prisma } from "@prisma/client"; -import { PrismaService } from "src/prisma/prisma.service"; -import { UpsertExpenseDto } from "../dtos/upsert-expense.dto"; -import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; -import { ExpenseResponse, UpsertAction } from "../types and interfaces/expenses.types.interfaces"; -import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; -import { EmployeeTimesheetResolver } from "src/modules/shared/utils/resolve-timesheet.utils"; -import { - BadRequestException, - Injectable, - NotFoundException -} from "@nestjs/common"; -import { - assertAndTrimComment, - computeAmountDecimal, - computeMileageAmount, - mapDbExpenseToDayResponse, - normalizeType, - parseAttachmentId -} from "../utils/expenses.utils"; -import { toDateOnly } from "src/modules/shifts/helpers/shifts-date-time-helpers"; +// import { BaseApprovalService } from "src/common/shared/base-approval.service"; +// import { Expenses, Prisma } from "@prisma/client"; +// import { PrismaService } from "src/prisma/prisma.service"; +// import { UpsertExpenseDto } from "../dtos/upsert-expense.dto"; +// import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; +// import { ExpenseResponse, UpsertAction } from "../types and interfaces/expenses.types.interfaces"; +// import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; +// import { EmployeeTimesheetResolver } from "src/modules/shared/utils/resolve-timesheet.utils"; +// import { +// BadRequestException, +// Injectable, +// NotFoundException +// } from "@nestjs/common"; +// import { +// assertAndTrimComment, +// computeAmountDecimal, +// computeMileageAmount, +// mapDbExpenseToDayResponse, +// normalizeType, +// parseAttachmentId +// } from "../utils/expenses.utils"; -@Injectable() -export class ExpensesCommandService extends BaseApprovalService { - constructor( - prisma: PrismaService, - private readonly bankCodesResolver: BankCodesResolver, - private readonly timesheetsResolver: EmployeeTimesheetResolver, - private readonly emailResolver: EmailToIdResolver, - ) { super(prisma); } +// @Injectable() +// export class ExpensesCommandService extends BaseApprovalService { +// constructor( +// prisma: PrismaService, +// private readonly bankCodesResolver: BankCodesResolver, +// private readonly timesheetsResolver: EmployeeTimesheetResolver, +// private readonly emailResolver: EmailToIdResolver, +// ) { super(prisma); } - //_____________________________________________________________________________________________ - // APPROVAL TX-DELEGATE METHODS - //_____________________________________________________________________________________________ +// //_____________________________________________________________________________________________ +// // APPROVAL TX-DELEGATE METHODS +// //_____________________________________________________________________________________________ - protected get delegate() { - return this.prisma.expenses; - } +// protected get delegate() { +// return this.prisma.expenses; +// } - protected delegateFor(transaction: Prisma.TransactionClient){ - return transaction.expenses; - } +// protected delegateFor(transaction: Prisma.TransactionClient){ +// return transaction.expenses; +// } - async updateApproval(id: number, isApproved: boolean): Promise { - return this.prisma.$transaction((transaction) => - this.updateApprovalWithTransaction(transaction, id, isApproved), - ); - } +// async updateApproval(id: number, isApproved: boolean): Promise { +// return this.prisma.$transaction((transaction) => +// this.updateApprovalWithTransaction(transaction, id, isApproved), +// ); +// } - //_____________________________________________________________________________________________ - // MASTER CRUD FUNCTION - //_____________________________________________________________________________________________ - readonly upsertExpensesByDate = async (email: string, date: string, dto: UpsertExpenseDto, - ): Promise<{ action:UpsertAction; day: ExpenseResponse[] }> => { +// //_____________________________________________________________________________________________ +// // MASTER CRUD FUNCTION +// //_____________________________________________________________________________________________ +// readonly upsertExpensesByDate = async (email: string, date: string, dto: UpsertExpenseDto, +// ): Promise<{ action:UpsertAction; day: ExpenseResponse[] }> => { - //validates if there is an existing expense, at least 1 old or new - const { old_expense, new_expense } = dto ?? {}; - if(!old_expense && !new_expense) throw new BadRequestException('At least one expense must be provided'); +// //validates if there is an existing expense, at least 1 old or new +// const { old_expense, new_expense } = dto ?? {}; +// if(!old_expense && !new_expense) throw new BadRequestException('At least one expense must be provided'); - //validate date format - const date_only = toDateOnly(date); - if(Number.isNaN(date_only.getTime())) throw new BadRequestException('Invalid date format (expected: YYYY-MM-DD)'); +// //validate date format +// const date_only = toDateOnly(date); +// if(Number.isNaN(date_only.getTime())) throw new BadRequestException('Invalid date format (expected: YYYY-MM-DD)'); - //resolve employee_id by email - const employee_id = await this.emailResolver.findIdByEmail(email); +// //resolve employee_id by email +// const employee_id = await this.emailResolver.findIdByEmail(email); - //make sure a timesheet existes - const timesheet_id = await this.timesheetsResolver.findTimesheetIdByEmail(email, date_only); - if(!timesheet_id) throw new NotFoundException(`no timesheet found for employee #${employee_id}`) - const {id} = timesheet_id; +// //make sure a timesheet existes +// const timesheet_id = await this.timesheetsResolver.findTimesheetIdByEmail(email, date_only); +// if(!timesheet_id) throw new NotFoundException(`no timesheet found for employee #${employee_id}`) +// const {id} = timesheet_id; - return this.prisma.$transaction(async (tx) => { - const loadDay = async (): Promise => { - const rows = await tx.expenses.findMany({ - where: { - timesheet_id: id, - date: date_only, - }, - include: { - bank_code: { - select: { - type: true, - }, - }, - }, - orderBy: [{ date: 'asc' }, { id: 'asc' }], - }); +// return this.prisma.$transaction(async (tx) => { +// const loadDay = async (): Promise => { +// const rows = await tx.expenses.findMany({ +// where: { +// timesheet_id: id, +// date: date_only, +// }, +// include: { +// bank_code: { +// select: { +// type: true, +// }, +// }, +// }, +// orderBy: [{ date: 'asc' }, { id: 'asc' }], +// }); - return rows.map((r) => - mapDbExpenseToDayResponse({ - date: r.date, - amount: r.amount ?? 0, - mileage: r.mileage ?? 0, - comment: r.comment, - is_approved: r.is_approved, - bank_code: r.bank_code, - })); - }; +// return rows.map((r) => +// mapDbExpenseToDayResponse({ +// date: r.date, +// amount: r.amount ?? 0, +// mileage: r.mileage ?? 0, +// comment: r.comment, +// is_approved: r.is_approved, +// bank_code: r.bank_code, +// })); +// }; - const normalizePayload = async (payload: { - type: string; - amount?: number; - mileage?: number; - comment: string; - attachment?: string | number; - }): Promise<{ - type: string; - bank_code_id: number; - amount: Prisma.Decimal; - mileage: number | null; - comment: string; - attachment: number | null; - }> => { - const type = normalizeType(payload.type); - const comment = assertAndTrimComment(payload.comment); - const attachment = parseAttachmentId(payload.attachment); +// const normalizePayload = async (payload: { +// type: string; +// amount?: number; +// mileage?: number; +// comment: string; +// attachment?: string | number; +// }): Promise<{ +// type: string; +// bank_code_id: number; +// amount: Prisma.Decimal; +// mileage: number | null; +// comment: string; +// attachment: number | null; +// }> => { +// const type = normalizeType(payload.type); +// const comment = assertAndTrimComment(payload.comment); +// const attachment = parseAttachmentId(payload.attachment); - const { id: bank_code_id, modifier } = await this.bankCodesResolver.findByType(type); - let amount = computeAmountDecimal(type, payload, modifier); - let mileage: number | null = null; +// const { id: bank_code_id, modifier } = await this.bankCodesResolver.findByType(type); +// let amount = computeAmountDecimal(type, payload, modifier); +// let mileage: number | null = null; - if (type === 'MILEAGE') { - mileage = Number(payload.mileage ?? 0); - if (!(mileage > 0)) { - throw new BadRequestException('Mileage required and must be > 0 for type MILEAGE'); - } +// if (type === 'MILEAGE') { +// mileage = Number(payload.mileage ?? 0); +// if (!(mileage > 0)) { +// throw new BadRequestException('Mileage required and must be > 0 for type MILEAGE'); +// } - const amountNumber = computeMileageAmount(mileage, modifier); - amount = new Prisma.Decimal(amountNumber); +// const amountNumber = computeMileageAmount(mileage, modifier); +// amount = new Prisma.Decimal(amountNumber); - } else { - if (!(typeof payload.amount === 'number' && payload.amount >= 0)) { - throw new BadRequestException('Amount required for non-MILEAGE expense'); - } - amount = new Prisma.Decimal(payload.amount); - } +// } else { +// if (!(typeof payload.amount === 'number' && payload.amount >= 0)) { +// throw new BadRequestException('Amount required for non-MILEAGE expense'); +// } +// amount = new Prisma.Decimal(payload.amount); +// } - if (attachment !== null) { - const attachment_row = await tx.attachments.findUnique({ - where: { id: attachment }, - select: { status: true }, - }); - if (!attachment_row || attachment_row.status !== 'ACTIVE') { - throw new BadRequestException('Attachment not found or inactive'); - } - } +// if (attachment !== null) { +// const attachment_row = await tx.attachments.findUnique({ +// where: { id: attachment }, +// select: { status: true }, +// }); +// if (!attachment_row || attachment_row.status !== 'ACTIVE') { +// throw new BadRequestException('Attachment not found or inactive'); +// } +// } - return { - type, - bank_code_id, - amount, - mileage, - comment, - attachment - }; - }; +// return { +// type, +// bank_code_id, +// amount, +// mileage, +// comment, +// attachment +// }; +// }; - const findExactOld = async (norm: { - bank_code_id: number; - amount: Prisma.Decimal; - mileage: number | null; - comment: string; - attachment: number | null; - }) => { - return tx.expenses.findFirst({ - where: { - timesheet_id: id, - date: date_only, - bank_code_id: norm.bank_code_id, - amount: norm.amount, - comment: norm.comment, - attachment: norm.attachment, - ...(norm.mileage !== null ? { mileage: norm.mileage } : { mileage: null }), - }, - select: { id: true }, - }); - }; +// const findExactOld = async (norm: { +// bank_code_id: number; +// amount: Prisma.Decimal; +// mileage: number | null; +// comment: string; +// attachment: number | null; +// }) => { +// return tx.expenses.findFirst({ +// where: { +// timesheet_id: id, +// date: date_only, +// bank_code_id: norm.bank_code_id, +// amount: norm.amount, +// comment: norm.comment, +// attachment: norm.attachment, +// ...(norm.mileage !== null ? { mileage: norm.mileage } : { mileage: null }), +// }, +// select: { id: true }, +// }); +// }; - let action : UpsertAction; - //_____________________________________________________________________________________________ - // DELETE - //_____________________________________________________________________________________________ - if(old_expense && !new_expense) { - const old_norm = await normalizePayload(old_expense); - const existing = await findExactOld(old_norm); - if(!existing) { - throw new NotFoundException({ - error_code: 'EXPENSE_STALE', - message: 'The expense was modified or deleted by someone else', - }); - } - await tx.expenses.delete({where: { id: existing.id } }); - action = 'delete'; - } - //_____________________________________________________________________________________________ - // CREATE - //_____________________________________________________________________________________________ - else if (!old_expense && new_expense) { - const new_exp = await normalizePayload(new_expense); - await tx.expenses.create({ - data: { - timesheet_id: id, - date: date_only, - bank_code_id: new_exp.bank_code_id, - amount: new_exp.amount, - mileage: new_exp.mileage, - comment: new_exp.comment, - attachment: new_exp.attachment, - is_approved: false, - }, - }); - action = 'create'; - } - //_____________________________________________________________________________________________ - // UPDATE - //_____________________________________________________________________________________________ - else if(old_expense && new_expense) { - const old_norm = await normalizePayload(old_expense); - const existing = await findExactOld(old_norm); - if(!existing) { - throw new NotFoundException({ - error_code: 'EXPENSE_STALE', - message: 'The expense was modified or deleted by someone else', - }); - } +// let action : UpsertAction; +// //_____________________________________________________________________________________________ +// // DELETE +// //_____________________________________________________________________________________________ +// if(old_expense && !new_expense) { +// const old_norm = await normalizePayload(old_expense); +// const existing = await findExactOld(old_norm); +// if(!existing) { +// throw new NotFoundException({ +// error_code: 'EXPENSE_STALE', +// message: 'The expense was modified or deleted by someone else', +// }); +// } +// await tx.expenses.delete({where: { id: existing.id } }); +// action = 'delete'; +// } +// //_____________________________________________________________________________________________ +// // CREATE +// //_____________________________________________________________________________________________ +// else if (!old_expense && new_expense) { +// const new_exp = await normalizePayload(new_expense); +// await tx.expenses.create({ +// data: { +// timesheet_id: id, +// date: date_only, +// bank_code_id: new_exp.bank_code_id, +// amount: new_exp.amount, +// mileage: new_exp.mileage, +// comment: new_exp.comment, +// attachment: new_exp.attachment, +// is_approved: false, +// }, +// }); +// action = 'create'; +// } +// //_____________________________________________________________________________________________ +// // UPDATE +// //_____________________________________________________________________________________________ +// else if(old_expense && new_expense) { +// const old_norm = await normalizePayload(old_expense); +// const existing = await findExactOld(old_norm); +// if(!existing) { +// throw new NotFoundException({ +// error_code: 'EXPENSE_STALE', +// message: 'The expense was modified or deleted by someone else', +// }); +// } - const new_exp = await normalizePayload(new_expense); - await tx.expenses.update({ - where: { id: existing.id }, - data: { - bank_code_id: new_exp.bank_code_id, - amount: new_exp.amount, - mileage: new_exp.mileage, - comment: new_exp.comment, - attachment: new_exp.attachment, - }, - }); - action = 'update'; - } - else { - throw new BadRequestException('Invalid upsert combination'); - } +// const new_exp = await normalizePayload(new_expense); +// await tx.expenses.update({ +// where: { id: existing.id }, +// data: { +// bank_code_id: new_exp.bank_code_id, +// amount: new_exp.amount, +// mileage: new_exp.mileage, +// comment: new_exp.comment, +// attachment: new_exp.attachment, +// }, +// }); +// action = 'update'; +// } +// else { +// throw new BadRequestException('Invalid upsert combination'); +// } - const day = await loadDay(); +// const day = await loadDay(); - return { action, day }; - }); - } -} \ No newline at end of file +// return { action, day }; +// }); +// } +// } \ No newline at end of file diff --git a/src/modules/expenses/services/expenses-query.service.ts b/src/modules/expenses/services/expenses-query.service.ts index 9a56fa8..d95f1a7 100644 --- a/src/modules/expenses/services/expenses-query.service.ts +++ b/src/modules/expenses/services/expenses-query.service.ts @@ -1,174 +1,171 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; -import { PrismaService } from "src/prisma/prisma.service"; -import { DayExpensesDto as ExpenseListResponseDto, ExpenseDto } from "src/modules/timesheets/dtos/timesheet-period.dto"; -import { round2, toUTCDateOnly } from "src/modules/timesheets/utils-helpers-others/timesheet.helpers"; -import { EXPENSE_TYPES } from "src/modules/timesheets/utils-helpers-others/timesheet.types"; -import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; +// import { Injectable, NotFoundException } from "@nestjs/common"; +// import { PrismaService } from "src/prisma/prisma.service"; +// import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; -@Injectable() -export class ExpensesQueryService { - constructor( - private readonly prisma: PrismaService, - private readonly employeeRepo: EmailToIdResolver, - ) {} +// @Injectable() +// export class ExpensesQueryService { +// constructor( +// private readonly prisma: PrismaService, +// private readonly employeeRepo: EmailToIdResolver, +// ) {} - //fetchs all expenses for a selected employee using email, pay-period-year and number - async findExpenseListByPayPeriodAndEmail( - email: string, - year: number, - period_no: number - ): Promise { - //fetch employe_id using email - const employee_id = await this.employeeRepo.findIdByEmail(email); - if(!employee_id) throw new NotFoundException(`Employee with email: ${email} not found`); +// //fetchs all expenses for a selected employee using email, pay-period-year and number +// async findExpenseListByPayPeriodAndEmail( +// email: string, +// year: number, +// period_no: number +// ): Promise { +// //fetch employe_id using email +// const employee_id = await this.employeeRepo.findIdByEmail(email); +// if(!employee_id) throw new NotFoundException(`Employee with email: ${email} not found`); - //fetch pay-period using year and period_no - const pay_period = await this.prisma.payPeriods.findFirst({ - where: { - pay_year: year, - pay_period_no: period_no - }, - select: { period_start: true, period_end: true }, - }); - if(!pay_period) throw new NotFoundException(`Pay period ${year}- ${period_no} not found`); +// //fetch pay-period using year and period_no +// const pay_period = await this.prisma.payPeriods.findFirst({ +// where: { +// pay_year: year, +// pay_period_no: period_no +// }, +// select: { period_start: true, period_end: true }, +// }); +// if(!pay_period) throw new NotFoundException(`Pay period ${year}- ${period_no} not found`); - const start = toUTCDateOnly(pay_period.period_start); - const end = toUTCDateOnly(pay_period.period_end); +// const start = toUTCDateOnly(pay_period.period_start); +// const end = toUTCDateOnly(pay_period.period_end); - //sets rows data - const rows = await this.prisma.expenses.findMany({ - where: { - date: { gte: start, lte: end }, - timesheet: { is: { employee_id } }, - }, - orderBy: { date: 'asc'}, - select: { - amount: true, - mileage: true, - comment: true, - is_approved: true, - supervisor_comment: true, - bank_code: {select: { type: true } }, - }, - }); +// //sets rows data +// const rows = await this.prisma.expenses.findMany({ +// where: { +// date: { gte: start, lte: end }, +// timesheet: { is: { employee_id } }, +// }, +// orderBy: { date: 'asc'}, +// select: { +// amount: true, +// mileage: true, +// comment: true, +// is_approved: true, +// supervisor_comment: true, +// bank_code: {select: { type: true } }, +// }, +// }); - //declare return values - const expenses: ExpenseDto[] = []; - let total_amount = 0; - let total_mileage = 0; +// //declare return values +// const expenses: ExpenseDto[] = []; +// let total_amount = 0; +// let total_mileage = 0; - //set rows - for(const row of rows) { - const type = (row.bank_code?.type ?? '').toUpperCase(); - const amount = round2(Number(row.amount ?? 0)); - const mileage = round2(Number(row.mileage ?? 0)); +// //set rows +// for(const row of rows) { +// const type = (row.bank_code?.type ?? '').toUpperCase(); +// const amount = round2(Number(row.amount ?? 0)); +// const mileage = round2(Number(row.mileage ?? 0)); - if(type === EXPENSE_TYPES.MILEAGE) { - total_mileage += mileage; - } else { - total_amount += amount; - } +// if(type === EXPENSE_TYPES.MILEAGE) { +// total_mileage += mileage; +// } else { +// total_amount += amount; +// } - //fills rows array - expenses.push({ - type, - amount, - mileage, - comment: row.comment ?? '', - is_approved: row.is_approved ?? false, - supervisor_comment: row.supervisor_comment ?? '', - }); - } +// //fills rows array +// expenses.push({ +// type, +// amount, +// mileage, +// comment: row.comment ?? '', +// is_approved: row.is_approved ?? false, +// supervisor_comment: row.supervisor_comment ?? '', +// }); +// } - return { - expenses, - total_expense: round2(total_amount), - total_mileage: round2(total_mileage), - }; -} +// return { +// expenses, +// total_expense: round2(total_amount), +// total_mileage: round2(total_mileage), +// }; +// } - //_____________________________________________________________________________________________ - // Deprecated or unused methods - //_____________________________________________________________________________________________ +// //_____________________________________________________________________________________________ +// // Deprecated or unused methods +// //_____________________________________________________________________________________________ - // async create(dto: CreateExpenseDto): Promise { - // const { timesheet_id, bank_code_id, date, amount:rawAmount, - // comment, is_approved,supervisor_comment} = dto; - // //fetches type and modifier - // const bank_code = await this.prisma.bankCodes.findUnique({ - // where: { id: bank_code_id }, - // select: { type: true, modifier: true }, - // }); - // if(!bank_code) throw new NotFoundException(`bank_code #${bank_code_id} not found`); +// // async create(dto: CreateExpenseDto): Promise { +// // const { timesheet_id, bank_code_id, date, amount:rawAmount, +// // comment, is_approved,supervisor_comment} = dto; +// // //fetches type and modifier +// // const bank_code = await this.prisma.bankCodes.findUnique({ +// // where: { id: bank_code_id }, +// // select: { type: true, modifier: true }, +// // }); +// // if(!bank_code) throw new NotFoundException(`bank_code #${bank_code_id} not found`); - // //if mileage -> service, otherwise the ratio is amount:1 - // let final_amount: number; - // if(bank_code.type === 'mileage') { - // final_amount = await this.mileageService.calculateReimbursement(rawAmount, bank_code_id); - // }else { - // final_amount = parseFloat( (rawAmount * bank_code.modifier).toFixed(2)); - // } +// // //if mileage -> service, otherwise the ratio is amount:1 +// // let final_amount: number; +// // if(bank_code.type === 'mileage') { +// // final_amount = await this.mileageService.calculateReimbursement(rawAmount, bank_code_id); +// // }else { +// // final_amount = parseFloat( (rawAmount * bank_code.modifier).toFixed(2)); +// // } - // return this.prisma.expenses.create({ - // data: { - // timesheet_id, - // bank_code_id, - // date, - // amount: final_amount, - // comment, - // is_approved, - // supervisor_comment - // }, - // include: { timesheet: { include: { employee: { include: { user: true }}}}, - // bank_code: true, - // }, - // }) - // } +// // return this.prisma.expenses.create({ +// // data: { +// // timesheet_id, +// // bank_code_id, +// // date, +// // amount: final_amount, +// // comment, +// // is_approved, +// // supervisor_comment +// // }, +// // include: { timesheet: { include: { employee: { include: { user: true }}}}, +// // bank_code: true, +// // }, +// // }) +// // } - // async findAll(filters: SearchExpensesDto): Promise { - // const where = buildPrismaWhere(filters); - // const expenses = await this.prisma.expenses.findMany({ where }) - // return expenses; - // } +// // async findAll(filters: SearchExpensesDto): Promise { +// // const where = buildPrismaWhere(filters); +// // const expenses = await this.prisma.expenses.findMany({ where }) +// // return expenses; +// // } - // async findOne(id: number): Promise { - // const expense = await this.prisma.expenses.findUnique({ - // where: { id }, - // include: { timesheet: { include: { employee: { include: { user:true } } } }, - // bank_code: true, - // }, - // }); - // if (!expense) { - // throw new NotFoundException(`Expense #${id} not found`); - // } - // return expense; - // } +// // async findOne(id: number): Promise { +// // const expense = await this.prisma.expenses.findUnique({ +// // where: { id }, +// // include: { timesheet: { include: { employee: { include: { user:true } } } }, +// // bank_code: true, +// // }, +// // }); +// // if (!expense) { +// // throw new NotFoundException(`Expense #${id} not found`); +// // } +// // return expense; +// // } - // async update(id: number, dto: UpdateExpenseDto): Promise { - // await this.findOne(id); - // const { timesheet_id, bank_code_id, date, amount, - // comment, is_approved, supervisor_comment} = dto; - // return this.prisma.expenses.update({ - // where: { id }, - // data: { - // ...(timesheet_id !== undefined && { timesheet_id}), - // ...(bank_code_id !== undefined && { bank_code_id }), - // ...(date !== undefined && { date }), - // ...(amount !== undefined && { amount }), - // ...(comment !== undefined && { comment }), - // ...(is_approved !== undefined && { is_approved }), - // ...(supervisor_comment !== undefined && { supervisor_comment }), - // }, - // include: { timesheet: { include: { employee: { include: { user: true } } } }, - // bank_code: true, - // }, - // }); - // } +// // async update(id: number, dto: UpdateExpenseDto): Promise { +// // await this.findOne(id); +// // const { timesheet_id, bank_code_id, date, amount, +// // comment, is_approved, supervisor_comment} = dto; +// // return this.prisma.expenses.update({ +// // where: { id }, +// // data: { +// // ...(timesheet_id !== undefined && { timesheet_id}), +// // ...(bank_code_id !== undefined && { bank_code_id }), +// // ...(date !== undefined && { date }), +// // ...(amount !== undefined && { amount }), +// // ...(comment !== undefined && { comment }), +// // ...(is_approved !== undefined && { is_approved }), +// // ...(supervisor_comment !== undefined && { supervisor_comment }), +// // }, +// // include: { timesheet: { include: { employee: { include: { user: true } } } }, +// // bank_code: true, +// // }, +// // }); +// // } - // async remove(id: number): Promise { - // await this.findOne(id); - // return this.prisma.expenses.delete({ where: { id } }); - // } +// // async remove(id: number): Promise { +// // await this.findOne(id); +// // return this.prisma.expenses.delete({ where: { id } }); +// // } -} \ No newline at end of file +// } \ No newline at end of file diff --git a/src/modules/leave-requests/controllers/leave-requests.controller.ts b/src/modules/leave-requests/controllers/leave-requests.controller.ts index fc934ff..7ecce7e 100644 --- a/src/modules/leave-requests/controllers/leave-requests.controller.ts +++ b/src/modules/leave-requests/controllers/leave-requests.controller.ts @@ -1,30 +1,30 @@ -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"; +// 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){} +// @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 +// @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(){...} +// //TODO: +// /* +// @Get('archive') +// findAllArchived(){...} - @Get('archive/:id') - findOneArchived(id){...} - */ +// @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 index 714dad8..03ad546 100644 --- a/src/modules/leave-requests/leave-requests.module.ts +++ b/src/modules/leave-requests/leave-requests.module.ts @@ -1,29 +1,29 @@ -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"; +// 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, - ], -}) +// @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 +// 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 index 309bfbb..ecfa8cc 100644 --- a/src/modules/leave-requests/services/holiday-leave-requests.service.ts +++ b/src/modules/leave-requests/services/holiday-leave-requests.service.ts @@ -1,78 +1,78 @@ -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'; +// 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, - ) {} +// @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'); +// 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[] = []; +// const created: LeaveRequestViewDto[] = []; - for (const iso_date of dates) { - const date = toDateOnly(iso_date); +// 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 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 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); - } +// 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' }); - } +// created.push({ ...mapRowToView(row), action: 'create' }); +// } - return { action: 'create', leave_requests: created }; - } -} +// 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 index d5e3eb7..7b0c82e 100644 --- a/src/modules/leave-requests/services/leave-request.service.ts +++ b/src/modules/leave-requests/services/leave-request.service.ts @@ -1,248 +1,248 @@ -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"; +// 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, - ) {} +// @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}`); - } - } +// //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"); +// 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, - }); +// 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(", ")}`); - } +// 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); - } - } +// 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) } }, - }); +// 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 }; - } +// 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"); - } +// 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 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[] = []; +// 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); +// 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), - ); +// 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 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); +// 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 }; - } +// 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; +// 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; - } +// 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 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); +// 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 }; - } -} +// 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 index dc513fa..a4554b1 100644 --- a/src/modules/leave-requests/services/sick-leave-requests.service.ts +++ b/src/modules/leave-requests/services/sick-leave-requests.service.ts @@ -1,98 +1,98 @@ -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"; +// 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, - ) {} +// @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`); +// 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 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 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[] = []; +// 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}`); - } +// 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 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 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); - } +// 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" }); - } +// created.push({ ...mapRowToView(row), action: "create" }); +// } - return { action: "create", leave_requests: created }; - } -} +// 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 index 8d90b6f..34223f5 100644 --- a/src/modules/leave-requests/services/vacation-leave-requests.service.ts +++ b/src/modules/leave-requests/services/vacation-leave-requests.service.ts @@ -1,93 +1,93 @@  -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"; +// 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, - ) {} +// @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`); +// 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 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 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[] = []; +// 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}`); +// 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 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 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 }; - } -} +// 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.util.ts b/src/modules/leave-requests/utils/leave-request.util.ts index 53b1ba4..d01ccf2 100644 --- a/src/modules/leave-requests/utils/leave-request.util.ts +++ b/src/modules/leave-requests/utils/leave-request.util.ts @@ -1,105 +1,104 @@ -import { hhmmFromLocal, toDateOnly, toStringFromDate } from "src/modules/shared/helpers/date-time.helpers"; -import { BadRequestException, Injectable } from "@nestjs/common"; -import { ShiftsCommandService } from "src/modules/shifts/services/shifts-command.service"; -import { PrismaService } from "src/prisma/prisma.service"; -import { LeaveTypes } from "@prisma/client"; -import { UpsertAction } from "src/modules/shared/types/upsert-actions.types"; +// 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, - ){} +// @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; +// 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 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 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 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'; +// 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, - }, - }); - } +// 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; +// 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, - }, - }); - } +// 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/pay-periods/pay-periods.module.ts b/src/modules/pay-periods/pay-periods.module.ts index f85d416..5f73d93 100644 --- a/src/modules/pay-periods/pay-periods.module.ts +++ b/src/modules/pay-periods/pay-periods.module.ts @@ -1,33 +1,27 @@ -import { PrismaModule } from "src/prisma/prisma.module"; -import { PayPeriodsController } from "./controllers/pay-periods.controller"; -import { Module } from "@nestjs/common"; -import { PayPeriodsCommandService } from "./services/pay-periods-command.service"; -import { PayPeriodsQueryService } from "./services/pay-periods-query.service"; -import { TimesheetsModule } from "../timesheets/timesheets.module"; -import { TimesheetsCommandService } from "../timesheets/services/timesheets-command.service"; -import { ExpensesCommandService } from "../expenses/services/expenses-command.service"; -import { ShiftsCommandService } from "../shifts/services/shifts-command.service"; -import { SharedModule } from "../shared/shared.module"; -import { PrismaService } from "src/prisma/prisma.service"; -import { BusinessLogicsModule } from "../business-logics/business-logics.module"; -import { ShiftsHelpersService } from "../shifts/helpers/shifts.helpers"; +// import { PrismaModule } from "src/prisma/prisma.module"; +// import { PayPeriodsController } from "./controllers/pay-periods.controller"; +// import { Module } from "@nestjs/common"; +// import { PayPeriodsCommandService } from "./services/pay-periods-command.service"; +// import { PayPeriodsQueryService } from "./services/pay-periods-query.service"; +// import { TimesheetsModule } from "../timesheets/timesheets.module"; +// import { ExpensesCommandService } from "../expenses/services/expenses-command.service"; +// 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, - PayPeriodsCommandService, - TimesheetsCommandService, - ExpensesCommandService, - ShiftsCommandService, - PrismaService, - ShiftsHelpersService, - ], - controllers: [PayPeriodsController], - exports: [ - PayPeriodsQueryService, - PayPeriodsCommandService, - ] -}) +// @Module({ +// imports: [PrismaModule, TimesheetsModule, SharedModule, BusinessLogicsModule], +// providers: [ +// PayPeriodsQueryService, +// PayPeriodsCommandService, +// ExpensesCommandService, +// PrismaService, +// ], +// controllers: [PayPeriodsController], +// exports: [ +// PayPeriodsQueryService, +// PayPeriodsCommandService, +// ] +// }) -export class PayperiodsModule {} \ No newline at end of file +// 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 index df9bfed..a1a4b5a 100644 --- a/src/modules/pay-periods/services/pay-periods-command.service.ts +++ b/src/modules/pay-periods/services/pay-periods-command.service.ts @@ -1,14 +1,14 @@ import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; -import { TimesheetsCommandService } from "src/modules/timesheets/services/timesheets-command.service"; 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: TimesheetsCommandService, + private readonly timesheets_approval: TimesheetApprovalService, private readonly query: PayPeriodsQueryService, ) {} diff --git a/src/modules/shifts/controllers/shift.controller.ts b/src/modules/shifts/controllers/shift.controller.ts new file mode 100644 index 0000000..bffa9cb --- /dev/null +++ b/src/modules/shifts/controllers/shift.controller.ts @@ -0,0 +1,49 @@ +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/controllers/shifts.controller.ts b/src/modules/shifts/controllers/shifts.controller.ts deleted file mode 100644 index 26918dc..0000000 --- a/src/modules/shifts/controllers/shifts.controller.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Body, Controller, Delete, Get, Header, Param, ParseBoolPipe, ParseIntPipe, Patch, Put, Query, } from "@nestjs/common"; -import { RolesAllowed } from "src/common/decorators/roles.decorators"; -import { Roles as RoleEnum } from '.prisma/client'; -import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; -import { ShiftsCommandService } from "../services/shifts-command.service"; -import { ShiftsQueryService } from "../services/shifts-query.service"; -import { GetShiftsOverviewDto } from "../dtos/get-shift-overview.dto"; -import { ShiftPayloadDto, UpsertShiftDto } from "../dtos/upsert-shift.dto"; -import { OverviewRow } from "../types-and-interfaces/shifts-overview-row.interface"; -import { UpsertAction } from "src/modules/shared/types/upsert-actions.types"; - -@ApiTags('Shifts') -@ApiBearerAuth('access-token') -// @UseGuards() -@Controller('shifts') -export class ShiftsController { - constructor( - private readonly shiftsService: ShiftsQueryService, - private readonly shiftsCommandService: ShiftsCommandService, - ){} - - @Put('upsert/:email') - async upsert_by_date( - @Param('email') email_param: string, - @Query('action') action: UpsertAction, - @Body() payload: UpsertShiftDto, - ) { - return this.shiftsCommandService.upsertShifts(email_param, action, payload); - } - - @Delete('delete/:email/:date') - async remove( - @Param('email') email: string, - @Param('date') date: string, - @Body() payload: UpsertShiftDto, - ) { - return this.shiftsCommandService.deleteShift(email, date, payload); - } - - @Patch('approval/:id') - //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) - async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) { - return this.shiftsCommandService.updateApproval(id, isApproved); - } - - @Get('summary') - async getSummary( @Query() query: GetShiftsOverviewDto): Promise { - return this.shiftsService.getSummary(query.period_id); - } - - @Get('export.csv') - @Header('Content-Type', 'text/csv; charset=utf-8') - @Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"') - async exportCsv(@Query() query: GetShiftsOverviewDto): Promise{ - const rows = await this.shiftsService.getSummary(query.period_id); - //CSV Headers - const header = [ - 'full_name', - 'supervisor', - 'total_regular_hrs', - 'total_evening_hrs', - 'total_overtime_hrs', - 'total_expenses', - 'total_mileage', - 'is_validated' - ].join(',') + '\n'; - - //CSV rows - const body = rows.map(r => { - const esc = (str: string) => `"${str.replace(/"/g, '""')}"`; - - return [ - esc(r.full_name), - esc(r.supervisor), - r.total_regular_hrs.toFixed(2), - r.total_evening_hrs.toFixed(2), - r.total_overtime_hrs.toFixed(2), - r.total_expenses.toFixed(2), - r.total_mileage.toFixed(2), - r.is_approved, - ].join(','); - }).join('\n'); - - return Buffer.from('\uFEFF' + header + body, 'utf8'); - } - -} \ No newline at end of file diff --git a/src/modules/shifts/dtos/get-shift-overview.dto.ts b/src/modules/shifts/dtos/get-shift-overview.dto.ts deleted file mode 100644 index e8ccdd2..0000000 --- a/src/modules/shifts/dtos/get-shift-overview.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Type } from "class-transformer"; -import { IsInt, Min, Max } from "class-validator"; - -export class GetShiftsOverviewDto { - @Type(()=> Number) - @IsInt() - @Min(1) - @Max(26) - period_id: number; -} \ No newline at end of file diff --git a/src/modules/shifts/dtos/get-shift.dto.ts b/src/modules/shifts/dtos/get-shift.dto.ts new file mode 100644 index 0000000..21b479f --- /dev/null +++ b/src/modules/shifts/dtos/get-shift.dto.ts @@ -0,0 +1,11 @@ +export class GetShiftDto { + timesheet_id: number; + bank_code_id: number; + date: string; + start_time: string; + end_time: string; + is_remote: boolean; + is_approved: boolean; + comment?: string; +} + diff --git a/src/modules/shifts/dtos/shift.dto.ts b/src/modules/shifts/dtos/shift.dto.ts new file mode 100644 index 0000000..65413da --- /dev/null +++ b/src/modules/shifts/dtos/shift.dto.ts @@ -0,0 +1,15 @@ +import { IsBoolean, IsInt, IsOptional, IsString, MaxLength } from "class-validator"; + +export class ShiftDto { + @IsInt() timesheet_id!: number; + @IsInt() bank_code_id!: number; + + @IsString() date!: string; + @IsString() start_time!: string; + @IsString() end_time!: string; + + @IsBoolean() is_approved!: boolean; + @IsBoolean() is_remote!: boolean; + + @IsOptional() @IsString() @MaxLength(280) comment?: string; +} \ 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 new file mode 100644 index 0000000..ebbbd13 --- /dev/null +++ b/src/modules/shifts/dtos/update-shift.dto.ts @@ -0,0 +1,7 @@ +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/dtos/upsert-shift.dto.ts b/src/modules/shifts/dtos/upsert-shift.dto.ts deleted file mode 100644 index 7809571..0000000 --- a/src/modules/shifts/dtos/upsert-shift.dto.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Type } from "class-transformer"; -import { IsBoolean, IsOptional, IsString, Matches, MaxLength, ValidateNested } from "class-validator"; - -export const COMMENT_MAX_LENGTH = 280; - -export class ShiftPayloadDto { - - @Matches(/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/) - date!: string; - - @Matches(/^([01]\d|2[0-3]):([0-5]\d)$/) - start_time!: string; - - @Matches(/^([01]\d|2[0-3]):([0-5]\d)$/) - end_time!: string; - - @IsString() - type!: string; - - @IsBoolean() - is_remote!: boolean; - - @IsBoolean() - is_approved!: boolean; - - @IsOptional() - @IsString() - @MaxLength(COMMENT_MAX_LENGTH) - comment?: string; -}; - -export class UpsertShiftDto { - - @IsOptional() - @ValidateNested() - @Type(()=> ShiftPayloadDto) - old_shift?: ShiftPayloadDto; - - @IsOptional() - @ValidateNested() - @Type(()=> ShiftPayloadDto) - new_shift?: ShiftPayloadDto; -}; \ 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 index 8bcd610..1ed0854 100644 --- a/src/modules/shifts/helpers/shifts-date-time-helpers.ts +++ b/src/modules/shifts/helpers/shifts-date-time-helpers.ts @@ -1,15 +1,3 @@ -export function timeFromHHMM(hhmm: string): Date { - const [h, m] = hhmm.split(':').map(Number); - return new Date(1970, 0, 1, h, m, 0, 0); -} - -export function toDateOnly(ymd: string): Date { - const y = Number(ymd.slice(0, 4)); - const m = Number(ymd.slice(5, 7)) - 1; - const d = Number(ymd.slice(8, 10)); - return new Date(y, m, d, 0, 0, 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 @@ -18,8 +6,26 @@ export function weekStartSunday(date_local: Date): Date { return start; } -export function formatHHmm(t: Date): string { - const hh = String(t.getHours()).padStart(2, '0'); - const mm = String(t.getMinutes()).padStart(2, '0'); +//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/helpers/shifts.helpers.ts b/src/modules/shifts/helpers/shifts.helpers.ts deleted file mode 100644 index bc017ff..0000000 --- a/src/modules/shifts/helpers/shifts.helpers.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { BadRequestException, UnprocessableEntityException, NotFoundException, ConflictException } from "@nestjs/common"; -import { Prisma, Shifts } from "@prisma/client"; -import { UpsertShiftDto } from "../dtos/upsert-shift.dto"; -import { DayShiftResponse } from "../types-and-interfaces/shifts-upsert.types"; -import { normalizeShiftPayload, overlaps } from "../utils/shifts.utils"; -import { weekStartSunday, formatHHmm } from "./shifts-date-time-helpers"; -import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; -import { OvertimeService } from "src/modules/business-logics/services/overtime.service"; - - -export type Tx = Prisma.TransactionClient; -export type Normalized = Awaited>; - -export class ShiftsHelpersService { - - constructor( - private readonly bankTypeResolver: BankCodesResolver, - private readonly overtimeService: OvertimeService, - ) { } - - async ensureTimesheet(tx: Tx, employee_id: number, date_only: Date) { - const start_of_week = weekStartSunday(date_only); - console.log('start of week: ', start_of_week); - return tx.timesheets.findUnique({ - where: { employee_id_start_date: { employee_id, start_date: start_of_week } }, - select: { id: true }, - }); - } - - async normalizeRequired( - raw: UpsertShiftDto['new_shift'] | UpsertShiftDto['old_shift'] | undefined | null, - label: 'old_shift' | 'new_shift' = 'new_shift', - ): Promise { - if (!raw) throw new BadRequestException(`${label} is required`); - const norm = await normalizeShiftPayload(raw); - if (norm.end_time.getTime() <= norm.start_time.getTime()) { - throw new UnprocessableEntityException(` ${label}.end_time must be > ${label}.start_time`); - } - return norm; - } - - async resolveBankIdRequired(tx: Tx, type: string, label: 'old_shift' | 'new_shift'): Promise { - const found = await this.bankTypeResolver.findByType(type, tx); - const id = found?.id; - if (typeof id !== 'number') { - throw new NotFoundException(`bank code not found for ${label}.type: ${type ?? ''}`); - } - return id; - } - - async getDayShifts(tx: Tx, timesheet_id: number, date_only: Date) { - return tx.shifts.findMany({ - where: { timesheet_id, date: date_only }, - include: { bank_code: true }, - orderBy: { start_time: 'asc' }, - }); - } - - async assertNoOverlap( - day_shifts: Array, - new_norm: Normalized | undefined, - exclude_id?: number, - ) { - if (!new_norm) return; - const conflicts = day_shifts.filter((s) => { - if (exclude_id && s.id === exclude_id) return false; - return overlaps( - new_norm.start_time.getTime(), - new_norm.end_time.getTime(), - s.start_time.getTime(), - s.end_time.getTime(), - ); - }); - if (conflicts.length) { - const payload = conflicts.map((s) => ({ - start_time: formatHHmm(s.start_time), - end_time: formatHHmm(s.end_time), - type: s.bank_code?.type ?? 'UNKNOWN', - })); - throw new ConflictException({ - error_code: 'SHIFT_OVERLAP', - message: 'New shift overlaps with existing shift(s)', - conflicts: payload, - }); - } - } - - - async findExactOldShift( - tx: Tx, - params: { - timesheet_id: number; - date_only: Date; - norm: Normalized; - bank_code_id: number; - comment?: string; - }, - ) { - const { timesheet_id, date_only, norm, bank_code_id } = params; - return tx.shifts.findFirst({ - where: { - timesheet_id, - date: date_only, - start_time: norm.start_time, - end_time: norm.end_time, - is_remote: norm.is_remote, - is_approved: norm.is_approved, - comment: norm.comment ?? null, - bank_code_id, - }, - select: { id: true }, - }); - } - - async afterWriteOvertimeAndLog(tx: Tx, employee_id: number, date_only: Date) { - // Switch regular → weekly overtime si > 40h - await this.overtimeService.transformRegularHoursToWeeklyOvertime(employee_id, date_only, tx); - const daily = await this.overtimeService.getDailyOvertimeHours(employee_id, date_only); - const weekly = await this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only); - // const [daily, weekly] = await Promise.all([ - // this.overtimeService.getDailyOvertimeHoursForDay(employee_id, date_only), - // this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only), - // ]); - return { daily, weekly }; - } - - async mapDay( - fresh: Array, - ): Promise { - return fresh.map((s) => ({ - start_time: formatHHmm(s.start_time), - end_time: formatHHmm(s.end_time), - type: s.bank_code?.type ?? 'UNKNOWN', - is_remote: s.is_remote, - comment: s.comment ?? null, - })); - } -} - diff --git a/src/modules/shifts/services/shifts-command.service.ts b/src/modules/shifts/services/shifts-command.service.ts deleted file mode 100644 index b39a56f..0000000 --- a/src/modules/shifts/services/shifts-command.service.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common"; -import { DayShiftResponse } from "../types-and-interfaces/shifts-upsert.types"; -import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; -import { Prisma, Shifts } from "@prisma/client"; -import { UpsertShiftDto } from "../dtos/upsert-shift.dto"; -import { BaseApprovalService } from "src/common/shared/base-approval.service"; -import { PrismaService } from "src/prisma/prisma.service"; -import { toDateOnly } from "../helpers/shifts-date-time-helpers"; -import { UpsertAction } from "src/modules/shared/types/upsert-actions.types"; -import { ShiftsHelpersService } from "../helpers/shifts.helpers"; -import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; - -@Injectable() -export class ShiftsCommandService extends BaseApprovalService { - private readonly logger = new Logger(ShiftsCommandService.name); - - constructor( - prisma: PrismaService, - private readonly emailResolver: EmailToIdResolver, - private readonly typeResolver: BankCodesResolver, - private readonly helpersService: ShiftsHelpersService, - ) { super(prisma); } - - //_____________________________________________________________________________________________ - // APPROVAL AND DELEGATE METHODS - //_____________________________________________________________________________________________ - protected get delegate() { - return this.prisma.shifts; - } - - protected delegateFor(transaction: Prisma.TransactionClient) { - return transaction.shifts; - } - - async updateApproval(id: number, is_approved: boolean): Promise { - return this.prisma.$transaction((transaction) => - this.updateApprovalWithTransaction(transaction, id, is_approved), - ); - } - - //_____________________________________________________________________________________________ - // MASTER CRUD METHOD - //_____________________________________________________________________________________________ - async upsertShifts( - email: string, - action: UpsertAction, - dto: UpsertShiftDto, - ): Promise<{ - action: UpsertAction; - day: DayShiftResponse[]; - }> { - if (!dto.old_shift && !dto.new_shift) throw new BadRequestException('At least one of old or new shift must be provided'); - - const date = dto.new_shift?.date ?? dto.old_shift?.date; - if (!date) throw new BadRequestException("A date (YYYY-MM-DD) must be provided in old_shift or new_shift"); - if (dto.old_shift?.date && dto.new_shift?.date && dto.old_shift.date !== dto.new_shift.date) { - throw new BadRequestException('old_shift.date and new_shift.date must be identical'); - } - - const employee_id = await this.emailResolver.findIdByEmail(email);//resolve employee id using email - - if(action === 'create') { - if(!dto.new_shift || dto.old_shift) { - throw new BadRequestException(`Only new_shift must be provided for create`); - } - return this.createShift(employee_id, date, dto); - } - if(action === 'update'){ - if(!dto.old_shift || !dto.new_shift) { - throw new BadRequestException(`Both new_shift and old_shift must be provided for update`); - } - return this.updateShift(employee_id, date, dto); - } - throw new BadRequestException(`Unknown action: ${action}`); - } - - //_________________________________________________________________ - // CREATE - //_________________________________________________________________ - private async createShift( - employee_id: number, - date_iso: string, - dto: UpsertShiftDto, - ): Promise<{action: UpsertAction; day: DayShiftResponse[]}> { - return this.prisma.$transaction(async (tx) => { - const date_only = toDateOnly(date_iso); - const timesheet = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); - if(!timesheet) throw new NotFoundException('Timesheet not found') - const new_norm_shift = await this.helpersService.normalizeRequired(dto.new_shift); - const new_bank_code_id = await this.helpersService.resolveBankIdRequired(tx, new_norm_shift.type, 'new_shift'); - - const day_shifts = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); - - await this.helpersService.assertNoOverlap(day_shifts, new_norm_shift); - - await tx.shifts.create({ - data: { - timesheet_id: timesheet.id, - date: date_only, - start_time: new_norm_shift.start_time, - end_time: new_norm_shift.end_time, - is_remote: new_norm_shift.is_remote, - is_approved: new_norm_shift.is_approved, - comment: new_norm_shift.comment ?? null, - bank_code_id: new_bank_code_id, - }, - }); - await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only); - const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); - return { action: 'create', day: await this.helpersService.mapDay(fresh_shift)}; - }); - } - - //_________________________________________________________________ - // UPDATE - //_________________________________________________________________ - private async updateShift( - employee_id: number, - date_iso: string, - dto: UpsertShiftDto, - ): Promise<{ action: UpsertAction; day: DayShiftResponse[];}>{ - return this.prisma.$transaction(async (tx) => { - const date_only = toDateOnly(date_iso); - const timesheet = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); - if(!timesheet) throw new NotFoundException('Timesheet not found') - - const old_norm_shift = await this.helpersService.normalizeRequired(dto.old_shift, 'old_shift'); - const new_norm_shift = await this.helpersService.normalizeRequired(dto.new_shift, 'new_shift'); - - const old_bank_code = await this.typeResolver.findByType(old_norm_shift.type); - const new_bank_code = await this.typeResolver.findByType(new_norm_shift.type); - - const day_shifts = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); - const existing = await this.helpersService.findExactOldShift(tx, { - timesheet_id: timesheet.id, - date_only, - norm: old_norm_shift, - bank_code_id: old_bank_code.id, - }); - if(!existing) throw new NotFoundException('[SHIFT_STALE]- The shift was modified or deleted by someone else'); - - await this.helpersService.assertNoOverlap(day_shifts, new_norm_shift, existing.id); - - await tx.shifts.update({ - where: { id: existing.id }, - data: { - start_time: new_norm_shift.start_time, - end_time: new_norm_shift.end_time, - is_remote: new_norm_shift.is_remote, - comment: new_norm_shift.comment ?? null, - bank_code_id: new_bank_code.id, - }, - }); - await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only); - const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); - return { action: 'update', day: await this.helpersService.mapDay(fresh_shift)}; - }); - - } - - //_________________________________________________________________ - // DELETE - //_________________________________________________________________ - async deleteShift( - email: string, - date_iso: string, - dto: UpsertShiftDto, - ){ - return this.prisma.$transaction(async (tx) => { - const date_only = toDateOnly(date_iso); //converts to Date format - const employee_id = await this.emailResolver.findIdByEmail(email); - - const timesheet = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); - if(!timesheet) throw new NotFoundException('Timesheet not found') - const norm_shift = await this.helpersService.normalizeRequired(dto.old_shift, 'old_shift'); - const bank_code_id = await this.typeResolver.findByType(norm_shift.type); - - console.log('timesheet_id: ', timesheet.id ); - console.log('date: ', date_only); - console.log('bank code id: ', bank_code_id.id); - console.log('normalized old shift: ', norm_shift); - - const existing = await this.helpersService.findExactOldShift(tx, { - timesheet_id: timesheet.id, - date_only, - norm: norm_shift, - bank_code_id: bank_code_id.id, - }); - if(!existing) throw new NotFoundException('[SHIFT_STALE]- The shift was modified or deleted by someone else'); - - await tx.shifts.delete({ where: { id: existing.id } }); - - await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only); - }); - } -} - diff --git a/src/modules/shifts/services/shifts-get.service.ts b/src/modules/shifts/services/shifts-get.service.ts new file mode 100644 index 0000000..1a79f49 --- /dev/null +++ b/src/modules/shifts/services/shifts-get.service.ts @@ -0,0 +1,56 @@ +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-query.service.ts b/src/modules/shifts/services/shifts-query.service.ts deleted file mode 100644 index 68006df..0000000 --- a/src/modules/shifts/services/shifts-query.service.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; -import { PrismaService } from "src/prisma/prisma.service"; -import { NotificationsService } from "src/modules/notifications/services/notifications.service"; -import { computeHours } from "src/common/utils/date-utils"; -import { OverviewRow } from "../types-and-interfaces/shifts-overview-row.interface"; - -// const DAILY_LIMIT_HOURS = Number(process.env.DAILY_LIMIT_HOURS ?? 12); - -@Injectable() -export class ShiftsQueryService { - constructor( - private readonly prisma: PrismaService, - private readonly notifs: NotificationsService, - ) {} - - async getSummary(period_id: number): Promise { - //fetch pay-period to display - const period = await this.prisma.payPeriods.findFirst({ - where: { pay_period_no: period_id }, - }); - if(!period) { - throw new NotFoundException(`pay-period ${period_id} not found`); - } - const { period_start, period_end } = period; - - //prepare shifts and expenses for display - const shifts = await this.prisma.shifts.findMany({ - where: { date: { gte: period_start, lte: period_end } }, - include: { - bank_code: true, - timesheet: { include: { - employee: { include: { - user:true, - supervisor: { include: { user: true } }, - } }, - } }, - }, - }); - - const expenses = await this.prisma.expenses.findMany({ - where: { date: { gte: period_start, lte: period_end } }, - include: { - bank_code: true, - timesheet: { include: { employee: { - include: { user:true, - supervisor: { include: { user:true } }, - } }, - } }, - }, - }); - - const mapRow = new Map(); - - for(const shift of shifts) { - const employeeId = shift.timesheet.employee.user_id; - const user = shift.timesheet.employee.user; - const sup = shift.timesheet.employee.supervisor?.user; - - let row = mapRow.get(employeeId); - if(!row) { - row = { - full_name: `${user.first_name} ${user.last_name}`, - supervisor: sup? `${sup.first_name} ${sup.last_name }` : '', - total_regular_hrs: 0, - total_evening_hrs: 0, - total_overtime_hrs: 0, - total_expenses: 0, - total_mileage: 0, - is_approved: false, - }; - } - const hours = computeHours(shift.start_time, shift.end_time); - - switch(shift.bank_code.type) { - case 'regular' : row.total_regular_hrs += hours; - break; - case 'evening' : row.total_evening_hrs += hours; - break; - case 'overtime' : row.total_overtime_hrs += hours; - break; - default: row.total_regular_hrs += hours; - } - mapRow.set(employeeId, row); - } - - for(const exp of expenses) { - const employee_id = exp.timesheet.employee.user_id; - const user = exp.timesheet.employee.user; - const sup = exp.timesheet.employee.supervisor?.user; - - let row = mapRow.get(employee_id); - if(!row) { - row = { - full_name: `${user.first_name} ${user.last_name}`, - supervisor: sup? `${sup.first_name} ${sup.last_name }` : '', - total_regular_hrs: 0, - total_evening_hrs: 0, - total_overtime_hrs: 0, - total_expenses: 0, - total_mileage: 0, - is_approved: false, - }; - } - const amount = Number(exp.amount); - row.total_expenses += amount; - if(exp.bank_code.type === 'mileage') { - row.total_mileage += amount; - } - mapRow.set(employee_id, row); - } - //return by default the list of employee in ascending alphabetical order - return Array.from(mapRow.values()).sort((a,b) => a.full_name.localeCompare(b.full_name)); - } -} \ 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 new file mode 100644 index 0000000..9ed8457 --- /dev/null +++ b/src/modules/shifts/services/shifts-upsert.service.ts @@ -0,0 +1,367 @@ +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 shift 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 index d6df6c8..4c20e0d 100644 --- a/src/modules/shifts/shifts.module.ts +++ b/src/modules/shifts/shifts.module.ts @@ -1,30 +1,24 @@ -import { Module } from '@nestjs/common'; -import { ShiftsController } from './controllers/shifts.controller'; -import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module'; -import { ShiftsCommandService } from './services/shifts-command.service'; -import { NotificationsModule } from '../notifications/notifications.module'; -import { ShiftsQueryService } from './services/shifts-query.service'; 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 { ShiftsHelpersService } from './helpers/shifts.helpers'; +import { Module } from '@nestjs/common'; @Module({ imports: [ - BusinessLogicsModule, - NotificationsModule, + BusinessLogicsModule, + NotificationsModule, SharedModule, ], - controllers: [ShiftsController], - providers: [ - ShiftsQueryService, - ShiftsCommandService, + controllers: [ShiftController], + providers: [ ShiftsArchivalService, - ShiftsHelpersService, - ], - exports: [ - ShiftsQueryService, - ShiftsCommandService, - ShiftsArchivalService, + ShiftsGetService, + ShiftsUpsertService, ], + exports: [ ShiftsUpsertService, ShiftsGetService ], }) export class ShiftsModule {} diff --git a/src/modules/shifts/types-and-interfaces/shifts-overview-row.interface.ts b/src/modules/shifts/types-and-interfaces/shifts-overview-row.interface.ts deleted file mode 100644 index 145885b..0000000 --- a/src/modules/shifts/types-and-interfaces/shifts-overview-row.interface.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface OverviewRow { - full_name: string; - supervisor: string; - total_regular_hrs: number; - total_evening_hrs: number; - total_overtime_hrs: number; - total_expenses: number; - total_mileage: number; - is_approved: boolean; -} \ No newline at end of file diff --git a/src/modules/shifts/types-and-interfaces/shifts-upsert.types.ts b/src/modules/shifts/types-and-interfaces/shifts-upsert.types.ts deleted file mode 100644 index 733ea72..0000000 --- a/src/modules/shifts/types-and-interfaces/shifts-upsert.types.ts +++ /dev/null @@ -1,17 +0,0 @@ -export type DayShiftResponse = { - start_time: string; - end_time: string; - type: string; - is_remote: boolean; - comment: string | null; -} - -export type ShiftPayload = { - date: string; - start_time: string; - end_time: string; - type: string; - is_remote: boolean; - is_approved: boolean; - comment?: string | null; -} \ No newline at end of file diff --git a/src/modules/shifts/utils/shifts.utils.ts b/src/modules/shifts/utils/shifts.utils.ts deleted file mode 100644 index 38935de..0000000 --- a/src/modules/shifts/utils/shifts.utils.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { NotFoundException } from "@nestjs/common"; - -export function overlaps( - a_start_ms: number, - a_end_ms: number, - b_start_ms: number, - b_end_ms: number, -): boolean { - return a_start_ms < b_end_ms && b_start_ms < a_end_ms; -} - -export function resolveBankCodeByType(type: string): Promise { - const bank = this.prisma.bankCodes.findFirst({ - where: { type }, - select: { id: true }, - }); - if (!bank) { - throw new NotFoundException({ error_code: 'SHIFT_TYPE_UNKNOWN', message: `unknown shift type: ${type}` }); - } - return bank.id; -} - -export function normalizeShiftPayload(payload: { - date: string, - start_time: string, - end_time: string, - type: string, - is_remote: boolean, - is_approved: boolean, - comment?: string | null, -}) { - //normalize shift's infos - const date = payload.date?.trim(); - const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date ?? ''); - if (!m) throw new Error(`Invalid date format (expected YYYY-MM-DD): "${payload.date}"`); - const year = Number(m[1]), mo = Number(m[2]), d = Number(m[3]); - - const asLocalDateOn = (input: string): Date => { - // HH:mm ? - const hm = /^(\d{2}):(\d{2})$/.exec((input ?? '').trim()); - if (hm) return new Date(Date.UTC(1970, 0, 1, Number(hm[1]), Number(hm[2]))); - const iso = new Date(input); - if (isNaN(iso.getTime())) throw new Error(`Invalid time: "${input}"`); - return new Date(Date.UTC(1970, 0, 1, iso.getHours(), iso.getMinutes(), iso.getSeconds())); - }; - - const start_time = asLocalDateOn(payload.start_time); - const end_time = asLocalDateOn(payload.end_time); - - const type = (payload.type || '').trim().toUpperCase(); - const is_remote = payload.is_remote; - const is_approved = payload.is_approved; - //normalize comment - const trimmed = typeof payload.comment === 'string' ? payload.comment.trim() : null; - const comment = trimmed && trimmed.length > 0 ? trimmed : null; - - return { date, start_time, end_time, type, is_remote, is_approved, comment }; -} \ No newline at end of file diff --git a/src/modules/shifts/~misc_deprecated-files/get-shift-overview.dto.ts b/src/modules/shifts/~misc_deprecated-files/get-shift-overview.dto.ts new file mode 100644 index 0000000..0921a48 --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/get-shift-overview.dto.ts @@ -0,0 +1,10 @@ +// import { Type } from "class-transformer"; +// import { IsInt, Min, Max } from "class-validator"; + +// export class GetShiftsOverviewDto { +// @Type(()=> Number) +// @IsInt() +// @Min(1) +// @Max(26) +// period_id: number; +// } \ No newline at end of file diff --git a/src/modules/shifts/~misc_deprecated-files/shifts-command.service.ts b/src/modules/shifts/~misc_deprecated-files/shifts-command.service.ts new file mode 100644 index 0000000..5544309 --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/shifts-command.service.ts @@ -0,0 +1,194 @@ +// import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common"; +// import { DayShiftResponse } from "../types-and-interfaces/shifts-upsert.types"; +// import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; +// import { Prisma, Shifts } from "@prisma/client"; +// import { UpsertShiftDto } from "../dtos/upsert-shift.dto"; +// import { BaseApprovalService } from "src/common/shared/base-approval.service"; +// import { PrismaService } from "src/prisma/prisma.service"; +// import { toDateOnly } from "../helpers/shifts-date-time-helpers"; +// import { UpsertAction } from "src/modules/shared/types/upsert-actions.types"; +// import { ShiftsHelpersService } from "../helpers/shifts.helpers"; +// import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; + +// @Injectable() +// export class ShiftsCommandService extends BaseApprovalService { +// private readonly logger = new Logger(ShiftsCommandService.name); + +// constructor( +// prisma: PrismaService, +// private readonly emailResolver: EmailToIdResolver, +// private readonly typeResolver: BankCodesResolver, +// private readonly helpersService: ShiftsHelpersService, +// ) { super(prisma); } + +// //_____________________________________________________________________________________________ +// // APPROVAL AND DELEGATE METHODS +// //_____________________________________________________________________________________________ +// protected get delegate() { +// return this.prisma.shifts; +// } + +// protected delegateFor(transaction: Prisma.TransactionClient) { +// return transaction.shifts; +// } + +// async updateApproval(id: number, is_approved: boolean): Promise { +// return this.prisma.$transaction((transaction) => +// this.updateApprovalWithTransaction(transaction, id, is_approved), +// ); +// } + + +// //TODO: modifier le Master Crud pour recevoir l'ensemble des shifts de la pay-period et trier sur l'action 'create'| 'update' | 'delete' +// //_____________________________________________________________________________________________ +// // MASTER CRUD METHOD +// //_____________________________________________________________________________________________ +// async upsertShifts( +// email: string, +// action: UpsertAction, +// dto: UpsertShiftDto, +// ): Promise<{ +// action: UpsertAction; +// day: DayShiftResponse[]; +// }> { +// if (!dto.old_shift && !dto.new_shift) throw new BadRequestException('At least one of old or new shift must be provided'); + +// const date = dto.new_shift?.date ?? dto.old_shift?.date; +// if (!date) throw new BadRequestException("A date (YYYY-MM-DD) must be provided in old_shift or new_shift"); +// if (dto.old_shift?.date && dto.new_shift?.date && dto.old_shift.date !== dto.new_shift.date) { +// throw new BadRequestException('old_shift.date and new_shift.date must be identical'); +// } + +// const employee_id = await this.emailResolver.findIdByEmail(email);//resolve employee id using email + +// if(action === 'create') { +// if(!dto.new_shift || dto.old_shift) { +// throw new BadRequestException(`Only new_shift must be provided for create`); +// } +// return this.createShift(employee_id, date, dto); +// } +// if(action === 'update'){ +// if(!dto.old_shift || !dto.new_shift) { +// throw new BadRequestException(`Both new_shift and old_shift must be provided for update`); +// } +// return this.updateShift(employee_id, date, dto); +// } +// throw new BadRequestException(`Unknown action: ${action}`); +// } + +// //_________________________________________________________________ +// // CREATE +// //_________________________________________________________________ +// private async createShift( +// employee_id: number, +// date_iso: string, +// dto: UpsertShiftDto, +// ): Promise<{action: UpsertAction; day: DayShiftResponse[]}> { +// return this.prisma.$transaction(async (tx) => { +// const date_only = toDateOnly(date_iso); +// const timesheet = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); +// if(!timesheet) throw new NotFoundException('Timesheet not found') +// const new_norm_shift = await this.helpersService.normalizeRequired(dto.new_shift); +// const new_bank_code_id = await this.helpersService.resolveBankIdRequired(tx, new_norm_shift.type, 'new_shift'); + +// const day_shifts = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); + +// await this.helpersService.assertNoOverlap(day_shifts, new_norm_shift); + +// await tx.shifts.create({ +// data: { +// timesheet_id: timesheet.id, +// date: date_only, +// start_time: new_norm_shift.start_time, +// end_time: new_norm_shift.end_time, +// is_remote: new_norm_shift.is_remote, +// is_approved: new_norm_shift.is_approved, +// comment: new_norm_shift.comment ?? null, +// bank_code_id: new_bank_code_id, +// }, +// }); +// await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only); +// const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); +// return { action: 'create', day: await this.helpersService.mapDay(fresh_shift)}; +// }); +// } + +// //_________________________________________________________________ +// // UPDATE +// //_________________________________________________________________ +// private async updateShift( +// employee_id: number, +// date_iso: string, +// dto: UpsertShiftDto, +// ): Promise<{ action: UpsertAction; day: DayShiftResponse[];}>{ +// return this.prisma.$transaction(async (tx) => { +// const date_only = toDateOnly(date_iso); +// const timesheet = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); +// if(!timesheet) throw new NotFoundException('Timesheet not found') + +// const old_norm_shift = await this.helpersService.normalizeRequired(dto.old_shift, 'old_shift'); +// const new_norm_shift = await this.helpersService.normalizeRequired(dto.new_shift, 'new_shift'); + +// const old_bank_code = await this.typeResolver.findByType(old_norm_shift.type); +// const new_bank_code = await this.typeResolver.findByType(new_norm_shift.type); + +// const day_shifts = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); +// const existing = await this.helpersService.findExactOldShift(tx, { +// timesheet_id: timesheet.id, +// date_only, +// norm: old_norm_shift, +// bank_code_id: old_bank_code.id, +// }); +// if(!existing) throw new NotFoundException('[SHIFT_STALE]- The shift was modified or deleted by someone else'); + +// await this.helpersService.assertNoOverlap(day_shifts, new_norm_shift, existing.id); + +// await tx.shifts.update({ +// where: { id: existing.id }, +// data: { +// start_time: new_norm_shift.start_time, +// end_time: new_norm_shift.end_time, +// is_remote: new_norm_shift.is_remote, +// comment: new_norm_shift.comment ?? null, +// bank_code_id: new_bank_code.id, +// }, +// }); +// await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only); +// const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); +// return { action: 'update', day: await this.helpersService.mapDay(fresh_shift)}; +// }); + +// } + +// //_________________________________________________________________ +// // DELETE +// //_________________________________________________________________ +// async deleteShift( +// email: string, +// date_iso: string, +// dto: UpsertShiftDto, +// ){ +// return this.prisma.$transaction(async (tx) => { +// const date_only = toDateOnly(date_iso); //converts to Date format +// const employee_id = await this.emailResolver.findIdByEmail(email); + +// const timesheet = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); +// if(!timesheet) throw new NotFoundException('Timesheet not found') +// const norm_shift = await this.helpersService.normalizeRequired(dto.old_shift, 'old_shift'); +// const bank_code_id = await this.typeResolver.findByType(norm_shift.type); + +// const existing = await this.helpersService.findExactOldShift(tx, { +// timesheet_id: timesheet.id, +// date_only, +// norm: norm_shift, +// bank_code_id: bank_code_id.id, +// }); +// if(!existing) throw new NotFoundException('[SHIFT_STALE]- The shift was modified or deleted by someone else'); + +// await tx.shifts.delete({ where: { id: existing.id } }); + +// await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only); +// }); +// } +// } + diff --git a/src/modules/shifts/~misc_deprecated-files/shifts-overview-row.interface.ts b/src/modules/shifts/~misc_deprecated-files/shifts-overview-row.interface.ts new file mode 100644 index 0000000..b82576e --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/shifts-overview-row.interface.ts @@ -0,0 +1,10 @@ +// export interface OverviewRow { +// full_name: string; +// supervisor: string; +// total_regular_hrs: number; +// total_evening_hrs: number; +// total_overtime_hrs: number; +// total_expenses: number; +// total_mileage: number; +// is_approved: boolean; +// } \ No newline at end of file diff --git a/src/modules/shifts/~misc_deprecated-files/shifts-query.service.ts b/src/modules/shifts/~misc_deprecated-files/shifts-query.service.ts new file mode 100644 index 0000000..be978f0 --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/shifts-query.service.ts @@ -0,0 +1,114 @@ +// import { Injectable, NotFoundException } from "@nestjs/common"; +// import { PrismaService } from "src/prisma/prisma.service"; +// import { NotificationsService } from "src/modules/notifications/services/notifications.service"; +// import { computeHours } from "src/common/utils/date-utils"; +// import { OverviewRow } from "../types-and-interfaces/shifts-overview-row.interface"; + +// // const DAILY_LIMIT_HOURS = Number(process.env.DAILY_LIMIT_HOURS ?? 12); + +// @Injectable() +// export class ShiftsQueryService { +// constructor( +// private readonly prisma: PrismaService, +// private readonly notifs: NotificationsService, +// ) {} + +// async getSummary(period_id: number): Promise { +// //fetch pay-period to display +// const period = await this.prisma.payPeriods.findFirst({ +// where: { pay_period_no: period_id }, +// }); +// if(!period) { +// throw new NotFoundException(`pay-period ${period_id} not found`); +// } +// const { period_start, period_end } = period; + +// //prepare shifts and expenses for display +// const shifts = await this.prisma.shifts.findMany({ +// where: { date: { gte: period_start, lte: period_end } }, +// include: { +// bank_code: true, +// timesheet: { include: { +// employee: { include: { +// user:true, +// supervisor: { include: { user: true } }, +// } }, +// } }, +// }, +// }); + +// const expenses = await this.prisma.expenses.findMany({ +// where: { date: { gte: period_start, lte: period_end } }, +// include: { +// bank_code: true, +// timesheet: { include: { employee: { +// include: { user:true, +// supervisor: { include: { user:true } }, +// } }, +// } }, +// }, +// }); + +// const mapRow = new Map(); + +// for(const shift of shifts) { +// const employeeId = shift.timesheet.employee.user_id; +// const user = shift.timesheet.employee.user; +// const sup = shift.timesheet.employee.supervisor?.user; + +// let row = mapRow.get(employeeId); +// if(!row) { +// row = { +// full_name: `${user.first_name} ${user.last_name}`, +// supervisor: sup? `${sup.first_name} ${sup.last_name }` : '', +// total_regular_hrs: 0, +// total_evening_hrs: 0, +// total_overtime_hrs: 0, +// total_expenses: 0, +// total_mileage: 0, +// is_approved: false, +// }; +// } +// const hours = computeHours(shift.start_time, shift.end_time); + +// switch(shift.bank_code.type) { +// case 'regular' : row.total_regular_hrs += hours; +// break; +// case 'evening' : row.total_evening_hrs += hours; +// break; +// case 'overtime' : row.total_overtime_hrs += hours; +// break; +// default: row.total_regular_hrs += hours; +// } +// mapRow.set(employeeId, row); +// } + +// for(const exp of expenses) { +// const employee_id = exp.timesheet.employee.user_id; +// const user = exp.timesheet.employee.user; +// const sup = exp.timesheet.employee.supervisor?.user; + +// let row = mapRow.get(employee_id); +// if(!row) { +// row = { +// full_name: `${user.first_name} ${user.last_name}`, +// supervisor: sup? `${sup.first_name} ${sup.last_name }` : '', +// total_regular_hrs: 0, +// total_evening_hrs: 0, +// total_overtime_hrs: 0, +// total_expenses: 0, +// total_mileage: 0, +// is_approved: false, +// }; +// } +// const amount = Number(exp.amount); +// row.total_expenses += amount; +// if(exp.bank_code.type === 'mileage') { +// row.total_mileage += amount; +// } +// mapRow.set(employee_id, row); +// } +// //return by default the list of employee in ascending alphabetical order +// return Array.from(mapRow.values()).sort((a,b) => a.full_name.localeCompare(b.full_name)); +// } +// } \ No newline at end of file diff --git a/src/modules/shifts/~misc_deprecated-files/shifts-upsert.types.ts b/src/modules/shifts/~misc_deprecated-files/shifts-upsert.types.ts new file mode 100644 index 0000000..ea21afe --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/shifts-upsert.types.ts @@ -0,0 +1,17 @@ +// export type DayShiftResponse = { +// start_time: string; +// end_time: string; +// type: string; +// is_remote: boolean; +// comment: string | null; +// } + +// export type ShiftPayload = { +// date: string; +// start_time: string; +// end_time: string; +// type: string; +// is_remote: boolean; +// is_approved: boolean; +// comment?: string | null; +// } \ No newline at end of file diff --git a/src/modules/shifts/~misc_deprecated-files/shifts.controller.ts b/src/modules/shifts/~misc_deprecated-files/shifts.controller.ts new file mode 100644 index 0000000..88a1637 --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/shifts.controller.ts @@ -0,0 +1,87 @@ +// import { Body, Controller, Delete, Get, Header, Param, ParseBoolPipe, ParseIntPipe, Patch, Put, Query, } from "@nestjs/common"; +// import { RolesAllowed } from "src/common/decorators/roles.decorators"; +// import { Roles as RoleEnum } from '.prisma/client'; +// import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +// import { ShiftsCommandService } from "../services/shifts-command.service"; +// import { ShiftsQueryService } from "../services/shifts-query.service"; +// import { GetShiftsOverviewDto } from "../dtos/get-shift-overview.dto"; +// import { ShiftPayloadDto, UpsertShiftDto } from "../dtos/upsert-shift.dto"; +// import { OverviewRow } from "../types-and-interfaces/shifts-overview-row.interface"; +// import { UpsertAction } from "src/modules/shared/types/upsert-actions.types"; + +// @ApiTags('Shifts') +// @ApiBearerAuth('access-token') +// // @UseGuards() +// @Controller('shifts') +// export class ShiftsController { +// constructor( +// private readonly shiftsService: ShiftsQueryService, +// private readonly shiftsCommandService: ShiftsCommandService, +// ){} + +// @Put('upsert/:email') +// async upsert_by_date( +// @Param('email') email_param: string, +// @Query('action') action: UpsertAction, +// @Body() payload: UpsertShiftDto, +// ) { +// return this.shiftsCommandService.upsertShifts(email_param, action, payload); +// } + +// @Delete('delete/:email/:date') +// async remove( +// @Param('email') email: string, +// @Param('date') date: string, +// @Body() payload: UpsertShiftDto, +// ) { +// return this.shiftsCommandService.deleteShift(email, date, payload); +// } + +// @Patch('approval/:id') +// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) +// async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) { +// return this.shiftsCommandService.updateApproval(id, isApproved); +// } + +// @Get('summary') +// async getSummary( @Query() query: GetShiftsOverviewDto): Promise { +// return this.shiftsService.getSummary(query.period_id); +// } + +// @Get('export.csv') +// @Header('Content-Type', 'text/csv; charset=utf-8') +// @Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"') +// async exportCsv(@Query() query: GetShiftsOverviewDto): Promise{ +// const rows = await this.shiftsService.getSummary(query.period_id); +// //CSV Headers +// const header = [ +// 'full_name', +// 'supervisor', +// 'total_regular_hrs', +// 'total_evening_hrs', +// 'total_overtime_hrs', +// 'total_expenses', +// 'total_mileage', +// 'is_validated' +// ].join(',') + '\n'; + +// //CSV rows +// const body = rows.map(r => { +// const esc = (str: string) => `"${str.replace(/"/g, '""')}"`; + +// return [ +// esc(r.full_name), +// esc(r.supervisor), +// r.total_regular_hrs.toFixed(2), +// r.total_evening_hrs.toFixed(2), +// r.total_overtime_hrs.toFixed(2), +// r.total_expenses.toFixed(2), +// r.total_mileage.toFixed(2), +// r.is_approved, +// ].join(','); +// }).join('\n'); + +// return Buffer.from('\uFEFF' + header + body, 'utf8'); +// } + +// } \ No newline at end of file diff --git a/src/modules/shifts/~misc_deprecated-files/shifts.helpers.ts b/src/modules/shifts/~misc_deprecated-files/shifts.helpers.ts new file mode 100644 index 0000000..a0e1e1c --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/shifts.helpers.ts @@ -0,0 +1,103 @@ +// import { BadRequestException, UnprocessableEntityException, NotFoundException, ConflictException } from "@nestjs/common"; +// import { Prisma, Shifts } from "@prisma/client"; +// import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; +// import { OvertimeService } from "src/modules/business-logics/services/overtime.service"; + +// export type Tx = Prisma.TransactionClient; +// export type Normalized = Awaited>; + +// export class ShiftsHelpersService { + +// constructor( +// private readonly bankTypeResolver: BankCodesResolver, +// private readonly overtimeService: OvertimeService, +// ) { } + +// async ensureTimesheet(tx: Tx, employee_id: number, date_only: Date) { +// const start_of_week = weekStartSunday(date_only); +// return tx.timesheets.findUnique({ +// where: { employee_id_start_date: { employee_id, start_date: start_of_week } }, +// select: { id: true }, +// }); +// } + +// async normalizeRequired( +// raw: UpsertShiftDto['new_shift'] | UpsertShiftDto['old_shift'] | undefined | null, +// label: 'old_shift' | 'new_shift' = 'new_shift', +// ): Promise { +// if (!raw) throw new BadRequestException(`${label} is required`); +// const norm = await normalizeShiftPayload(raw); +// if (norm.end_time.getTime() <= norm.start_time.getTime()) { +// throw new UnprocessableEntityException(` ${label}.end_time must be > ${label}.start_time`); +// } +// return norm; +// } + +// async resolveBankIdRequired(tx: Tx, type: string, label: 'old_shift' | 'new_shift'): Promise { +// const found = await this.bankTypeResolver.findByType(type, tx); +// const id = found?.id; +// if (typeof id !== 'number') { +// throw new NotFoundException(`bank code not found for ${label}.type: ${type ?? ''}`); +// } +// return id; +// } + +// async getDayShifts(tx: Tx, timesheet_id: number, date_only: Date) { +// return tx.shifts.findMany({ +// where: { timesheet_id, date: date_only }, +// include: { bank_code: true }, +// orderBy: { start_time: 'asc' }, +// }); +// } + +// async findExactOldShift( +// tx: Tx, +// params: { +// timesheet_id: number; +// date_only: Date; +// norm: Normalized; +// bank_code_id: number; +// comment?: string; +// }, +// ) { +// const { timesheet_id, date_only, norm, bank_code_id } = params; +// return tx.shifts.findFirst({ +// where: { +// timesheet_id, +// date: date_only, +// start_time: norm.start_time, +// end_time: norm.end_time, +// is_remote: norm.is_remote, +// is_approved: norm.is_approved, +// comment: norm.comment ?? null, +// bank_code_id, +// }, +// select: { id: true }, +// }); +// } + +// async afterWriteOvertimeAndLog(tx: Tx, employee_id: number, date_only: Date) { +// // Switch regular → weekly overtime si > 40h +// await this.overtimeService.transformRegularHoursToWeeklyOvertime(employee_id, date_only, tx); +// const daily = await this.overtimeService.getDailyOvertimeHours(employee_id, date_only); +// const weekly = await this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only); +// // const [daily, weekly] = await Promise.all([ +// // this.overtimeService.getDailyOvertimeHoursForDay(employee_id, date_only), +// // this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only), +// // ]); +// return { daily, weekly }; +// } + +// async mapDay( +// fresh: Array, +// ): Promise { +// return fresh.map((s) => ({ +// start_time: toStringFromHHmm(s.start_time), +// end_time: toStringFromHHmm(s.end_time), +// type: s.bank_code?.type ?? 'UNKNOWN', +// is_remote: s.is_remote, +// comment: s.comment ?? null, +// })); +// } +// } + diff --git a/src/modules/shifts/~misc_deprecated-files/shifts.utils.ts b/src/modules/shifts/~misc_deprecated-files/shifts.utils.ts new file mode 100644 index 0000000..bfa5402 --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/shifts.utils.ts @@ -0,0 +1,58 @@ +// import { NotFoundException } from "@nestjs/common"; + +// export function overlaps( +// a_start_ms: number, +// a_end_ms: number, +// b_start_ms: number, +// b_end_ms: number, +// ): boolean { +// return a_start_ms < b_end_ms && b_start_ms < a_end_ms; +// } + +// export function resolveBankCodeByType(type: string): Promise { +// const bank = this.prisma.bankCodes.findFirst({ +// where: { type }, +// select: { id: true }, +// }); +// if (!bank) { +// throw new NotFoundException({ error_code: 'SHIFT_TYPE_UNKNOWN', message: `unknown shift type: ${type}` }); +// } +// return bank.id; +// } + +// export function normalizeShiftPayload(payload: { +// date: string, +// start_time: string, +// end_time: string, +// type: string, +// is_remote: boolean, +// is_approved: boolean, +// comment?: string | null, +// }) { +// //normalize shift's infos +// const date = payload.date?.trim(); +// const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date ?? ''); +// if (!m) throw new Error(`Invalid date format (expected YYYY-MM-DD): "${payload.date}"`); +// const year = Number(m[1]), mo = Number(m[2]), d = Number(m[3]); + +// const asLocalDateOn = (input: string): Date => { +// // HH:mm ? +// const hm = /^(\d{2}):(\d{2})$/.exec((input ?? '').trim()); +// if (hm) return new Date(year, mo - 1, d, Number(hm[1]), Number(hm[2]), 0, 0); +// const iso = new Date(input); +// if (isNaN(iso.getTime())) throw new Error(`Invalid time: "${input}"`); +// return new Date(year, mo - 1, d, iso.getHours(), iso.getMinutes(), iso.getSeconds(), iso.getMilliseconds()); +// }; + +// const start_time = asLocalDateOn(payload.start_time); +// const end_time = asLocalDateOn(payload.end_time); + +// const type = (payload.type || '').trim().toUpperCase(); +// const is_remote = payload.is_remote; +// const is_approved = payload.is_approved; +// //normalize comment +// const trimmed = typeof payload.comment === 'string' ? payload.comment.trim() : null; +// const comment = trimmed && trimmed.length > 0 ? trimmed : null; + +// return { date, start_time, end_time, type, is_remote, is_approved, comment }; +// } \ No newline at end of file diff --git a/src/modules/shifts/~misc_deprecated-files/upsert-shift.dto.ts b/src/modules/shifts/~misc_deprecated-files/upsert-shift.dto.ts new file mode 100644 index 0000000..eab1abe --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/upsert-shift.dto.ts @@ -0,0 +1,43 @@ +// import { Type } from "class-transformer"; +// import { IsBoolean, IsOptional, IsString, Matches, MaxLength, ValidateNested } from "class-validator"; + +// export const COMMENT_MAX_LENGTH = 280; + +// export class ShiftPayloadDto { + +// @Matches(/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/) +// date!: string; + +// @Matches(/^([01]\d|2[0-3]):([0-5]\d)$/) +// start_time!: string; + +// @Matches(/^([01]\d|2[0-3]):([0-5]\d)$/) +// end_time!: string; + +// @IsString() +// type!: string; + +// @IsBoolean() +// is_remote!: boolean; + +// @IsBoolean() +// is_approved!: boolean; + +// @IsOptional() +// @IsString() +// @MaxLength(COMMENT_MAX_LENGTH) +// comment?: string; +// }; + +// export class UpsertShiftDto { + +// @IsOptional() +// @ValidateNested() +// @Type(()=> ShiftPayloadDto) +// old_shift?: ShiftPayloadDto; + +// @IsOptional() +// @ValidateNested() +// @Type(()=> ShiftPayloadDto) +// new_shift?: ShiftPayloadDto; +// }; \ No newline at end of file diff --git a/src/modules/timesheets/controllers/timesheet.controller.ts b/src/modules/timesheets/controllers/timesheet.controller.ts new file mode 100644 index 0000000..83f28c0 --- /dev/null +++ b/src/modules/timesheets/controllers/timesheet.controller.ts @@ -0,0 +1,17 @@ +import { GetTimesheetsOverviewService } from "../services/timesheet-get-overview.service"; +import { Controller, Get, Query} from "@nestjs/common"; + +@Controller('timesheets') +export class TimesheetController { + constructor(private readonly timesheetOverview: GetTimesheetsOverviewService){} + + @Get() + async getTimesheetByIds( + @Query('timesheet_ids') timesheet_ids: string ) { + const parsed = timesheet_ids.split(/,\s*/).map(value => Number(value)).filter(Number.isFinite); + return this.timesheetOverview.getTimesheetsByIds(parsed); + } + + + +} \ No newline at end of file diff --git a/src/modules/timesheets/controllers/timesheets.controller.ts b/src/modules/timesheets/controllers/timesheets.controller.ts deleted file mode 100644 index 4abca29..0000000 --- a/src/modules/timesheets/controllers/timesheets.controller.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { BadRequestException, Body, Controller, Get, Param, ParseIntPipe, Post, Query } from '@nestjs/common'; -import { TimesheetsQueryService } from '../services/timesheets-query.service'; -import { CreateWeekShiftsDto } from '../dtos/create-timesheet.dto'; -import { RolesAllowed } from "src/common/decorators/roles.decorators"; -import { Roles as RoleEnum } from '.prisma/client'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; -import { TimesheetsCommandService } from '../services/timesheets-command.service'; -import { TimesheetMap } from '../utils-helpers-others/timesheet.types'; -import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto'; - - -@ApiTags('Timesheets') -@ApiBearerAuth('access-token') -// @UseGuards() -@Controller('timesheets') -export class TimesheetsController { - constructor( - private readonly timesheetsQuery: TimesheetsQueryService, - private readonly timesheetsCommand: TimesheetsCommandService, - ) {} - - @Get() - //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) - async getPeriodByQuery( - @Query('year', ParseIntPipe ) year: number, - @Query('period_no', ParseIntPipe ) period_no: number, - @Query('email') email?: string - ): Promise { - if(!email || !(email = email.trim())) throw new BadRequestException('Query param "email" is mandatory for this route.'); - return this.timesheetsQuery.findAll(year, period_no, email); - } - - @Get('/:email') - async getByEmail( - @Param('email') email: string, - @Query('offset') offset?: string, - ): Promise { - const week_offset = Number.isFinite(Number(offset)) ? Number(offset) : 0; - return this.timesheetsQuery.getTimesheetByEmail(email, week_offset); - } - - @Post('shifts/:email') - async createTimesheetShifts( - @Param('email') email: string, - @Body() dto: CreateWeekShiftsDto, - @Query('offset') offset?: string, - ): Promise { - const week_offset = Number.isFinite(Number(offset)) ? Number(offset) : 0; - return this.timesheetsCommand.createWeekShiftsAndReturnOverview(email, dto.shifts, week_offset); - } -} diff --git a/src/modules/timesheets/dtos/create-timesheet.dto.ts b/src/modules/timesheets/dtos/create-timesheet.dto.ts deleted file mode 100644 index a6fd0b2..0000000 --- a/src/modules/timesheets/dtos/create-timesheet.dto.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Type } from "class-transformer"; -import { IsArray, IsOptional, IsString, Length, Matches, ValidateNested } from "class-validator"; - -export class CreateTimesheetDto { - - @IsString() - @Matches(/^\d{4}-\d{2}-\d{2}$/) - date!: string; - - @IsString() - @Length(1,64) - type!: string; - - @IsString() - @Matches(/^\d{2}:\d{2}$/) - start_time!: string; - - @IsString() - @Matches(/^\d{2}:\d{2}$/) - end_time!: string; - - @IsOptional() - @IsString() - @Length(0,512) - comment?: string; -} - -export class CreateWeekShiftsDto { - @IsArray() - @ValidateNested({each:true}) - @Type(()=> CreateTimesheetDto) - shifts!: CreateTimesheetDto[]; -} diff --git a/src/modules/timesheets/dtos/search-timesheet.dto.ts b/src/modules/timesheets/dtos/search-timesheet.dto.ts deleted file mode 100644 index a4bf613..0000000 --- a/src/modules/timesheets/dtos/search-timesheet.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Type } from "class-transformer"; -import { IsBoolean, IsInt, IsOptional } from "class-validator"; - - -export class SearchTimesheetDto { - @IsOptional() - @Type(() => Number) - @IsInt() - timesheet_id?: number; - - @IsOptional() - @Type(()=> Number) - @IsInt() - employee_id?: number; - - @IsOptional() - @Type(()=> Boolean) - @IsBoolean() - is_approved?: boolean; -} \ No newline at end of file diff --git a/src/modules/timesheets/dtos/timesheet-period.dto.ts b/src/modules/timesheets/dtos/timesheet-period.dto.ts deleted file mode 100644 index 333716b..0000000 --- a/src/modules/timesheets/dtos/timesheet-period.dto.ts +++ /dev/null @@ -1,75 +0,0 @@ -export class TimesheetDto { - start_day: string; - end_day: string; - label: string; - shifts: ShiftDto[]; - expenses: ExpenseDto[] - is_approved: boolean; -} - -export class ShiftDto { - date: string; - type: string; - start_time: string; - end_time : string; - comment: string; - is_approved: boolean; - is_remote: boolean; -} - -export class ExpenseDto { - type: string; - amount: number; - mileage: number; - comment: string; - is_approved: boolean; - supervisor_comment: string; -} - -export type DayShiftsDto = ShiftDto[]; - -export class DetailedShifts { - shifts: DayShiftsDto; - regular_hours: number; - evening_hours: number; - overtime_hours: number; - emergency_hours: number; - comment: string; - short_date: string; - break_durations?: number; -} - -export class DayExpensesDto { - expenses: ExpenseDto[] = []; - total_mileage: number; - total_expense: number; -} - -export class WeekDto { - is_approved: boolean; - shifts: { - sun: DetailedShifts; - mon: DetailedShifts; - tue: DetailedShifts; - wed: DetailedShifts; - thu: DetailedShifts; - fri: DetailedShifts; - sat: DetailedShifts; - } - expenses: { - sun: DayExpensesDto; - mon: DayExpensesDto; - tue: DayExpensesDto; - wed: DayExpensesDto; - thu: DayExpensesDto; - fri: DayExpensesDto; - sat: DayExpensesDto; - } -} - -export class TimesheetPeriodDto { - weeks: WeekDto[]; - employee_full_name: string; -} - - diff --git a/src/modules/shared/classes/timesheet.dto.ts b/src/modules/timesheets/dtos/timesheet.dto.ts similarity index 83% rename from src/modules/shared/classes/timesheet.dto.ts rename to src/modules/timesheets/dtos/timesheet.dto.ts index 4a3f307..3d30c8b 100644 --- a/src/modules/shared/classes/timesheet.dto.ts +++ b/src/modules/timesheets/dtos/timesheet.dto.ts @@ -1,11 +1,11 @@ -export class Session { - user_id: number; - +export class Timesheets { + employee_fullname: string; + timesheets: Timesheet[]; } - -export class Timesheets { +export class Timesheet { timesheet_id: number; + is_approved: boolean; days: TimesheetDay[]; weekly_hours: TotalHours[]; weekly_expenses: TotalExpenses[]; @@ -30,15 +30,15 @@ export class TotalHours { } export class TotalExpenses { expenses: number; - perd_diem: number; + per_diem: number; on_call: number; mileage: number; } export class Shift { - date: Date; - start_time: Date; - end_time: Date; + date: string; + start_time: string; + end_time: string; type: string; is_remote: boolean; is_approved: boolean; diff --git a/src/modules/timesheets/helpers/timesheets-date-time-helpers.ts b/src/modules/timesheets/helpers/timesheets-date-time-helpers.ts new file mode 100644 index 0000000..4a77edf --- /dev/null +++ b/src/modules/timesheets/helpers/timesheets-date-time-helpers.ts @@ -0,0 +1,26 @@ +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}`; +} \ No newline at end of file diff --git a/src/modules/timesheets/services/timesheet-approval.service.ts b/src/modules/timesheets/services/timesheet-approval.service.ts new file mode 100644 index 0000000..c22d0dc --- /dev/null +++ b/src/modules/timesheets/services/timesheet-approval.service.ts @@ -0,0 +1,38 @@ +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-get-overview.service.ts b/src/modules/timesheets/services/timesheet-get-overview.service.ts new file mode 100644 index 0000000..8cf22a8 --- /dev/null +++ b/src/modules/timesheets/services/timesheet-get-overview.service.ts @@ -0,0 +1,192 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; +import { sevenDaysFrom, toDateFromString, toHHmmFromDate, toStringFromDate } from "../helpers/timesheets-date-time-helpers"; + +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; +}; + +@Injectable() +export class GetTimesheetsOverviewService { + constructor(private readonly prisma: PrismaService) { } + + async getTimesheetsByIds(timesheet_ids: number[]) { + if (!Array.isArray(timesheet_ids) || timesheet_ids.length === 0) throw new NotFoundException(`Timesheet_ids are missing`); + + //fetch all needed data using timesheet ids + const rows = await this.prisma.timesheets.findMany({ + where: { id: { in: timesheet_ids } }, + include: { + employee: { include: { user: true } }, + shift: { include: { bank_code: true } }, + expense: { include: { bank_code: true, attachment_record: true } }, + }, + orderBy: { start_date: 'asc' }, + }); + + if (rows.length === 0) throw new NotFoundException('Timesheet(s) not found'); + + //build full name + const user = rows[0].employee.user; + const employee_fullname = `${user.first_name} ${user.last_name}`.trim(); + + const timesheets = rows.map((timesheet) => this.mapOneTimesheet(timesheet)); + return { employee_fullname, timesheets }; + } + + + //----------------------------------------------------------------------------------- + // MAPPERS & HELPERS + //----------------------------------------------------------------------------------- + private mapOneTimesheet(timesheet: any) { + //converts string to UTC date format + const start = toDateFromString(timesheet.start_date); + const day_dates = sevenDaysFrom(start); + + //map of shifts by days + const shifts_by_date = new Map(); + for (const shift of timesheet.shift) { + const date = toStringFromDate(shift.date); + const arr = shifts_by_date.get(date) ?? []; + arr.push(shift); + shifts_by_date.set(date, arr); + } + //map of expenses by days + const expenses_by_date = new Map(); + for (const expense of timesheet.expense) { + const date = toStringFromDate(expense.date); + const arr = expenses_by_date.get(date) ?? []; + arr.push(expense); + expenses_by_date.set(date, arr); + } + //weekly totals + const weekly_hours: TotalHours[] = [emptyHours()]; + const weekly_expenses: TotalExpenses[] = [emptyExpenses()]; + + //map of days + const days = day_dates.map((date) => { + const date_iso = toStringFromDate(date); + const shifts_source = shifts_by_date.get(date_iso) ?? []; + const expenses_source = expenses_by_date.get(date_iso) ?? []; + //inner map of shifts + const shifts = shifts_source.map((shift) => ({ + 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, + comment: shift.comment ?? null, + })); + + //inner map of expenses + const expenses = expenses_source.map((expense) => ({ + date: toStringFromDate(expense.date), + amount: expense.amount ? Number(expense.amount) : undefined, + mileage: expense.mileage ? Number(expense.mileage) : undefined, + expense_id: expense.id ?? null, + attachment: expense.attachment_record ? String(expense.attachment_record.id) : undefined, + is_approved: expense.is_approved ?? false, + comment: expense.comment ?? '', + supervisor_comment: expense.supervisor_comment, + })); + + //daily totals + const daily_hours = [emptyHours()]; + const daily_expenses = [emptyExpenses()]; + + //totals by shift types + for (const shift of shifts_source) { + const hours = diffOfHours(shift.start_time, shift.end_time); + const subgroup = hoursSubGroupFromBankCode(shift.bank_code); + daily_hours[0][subgroup] += hours; + weekly_hours[0][subgroup] += hours; + } + + //totals by expense types + for (const expense of expenses_source) { + const subgroup = expenseSubgroupFromBankCode(expense.bank_code); + if (subgroup === 'mileage') { + const mileage = num(expense.mileage); + daily_expenses[0].mileage += mileage; + weekly_expenses[0].mileage += mileage; + } else if (subgroup === 'per_diem') { + const amount = num(expense.amount); + daily_expenses[0].per_diem += amount; + weekly_expenses[0].per_diem += amount; + } else if (subgroup === 'on_call') { + const amount = num(expense.amount); + daily_expenses[0].on_call += amount; + weekly_expenses[0].on_call += amount; + } else { + const amount = num(expense.amount); + daily_expenses[0].expenses += amount; + weekly_expenses[0].expenses += amount; + } + } + return { + date: date_iso, + shifts, + expenses, + daily_hours, + daily_expenses, + }; + }); + return { + timesheet_id: timesheet.id, + is_approved: timesheet.is_approved ?? false, + days, + weekly_hours, + weekly_expenses, + }; + } +} + +const emptyHours = (): TotalHours => { + return { regular: 0, evening: 0, emergency: 0, overtime: 0, vacation: 0, holiday: 0, sick: 0 }; +} +const emptyExpenses = (): TotalExpenses => { + return { expenses: 0, per_diem: 0, on_call: 0, mileage: 0 }; +} + +const diffOfHours = (a: Date, b: Date): number => { + const ms = new Date(b).getTime() - new Date(a).getTime(); + return Math.max(0, Math.round((ms / 36e5) * 1000) / 1000); +} + +const num = (value: any): number => { + return value ? Number(value) : 0; +} + +const hoursSubGroupFromBankCode = (bank_code: any): keyof TotalHours => { + const type = bank_code.type; + if (type.includes('EVENING')) return 'evening'; + if (type.includes('EMERGENCY')) return 'emergency'; + if (type.includes('OVERTIME')) return 'overtime'; + if (type.includes('VACATION')) return 'vacation'; + if (type.includes('HOLIDAY')) return 'holiday'; + if (type.includes('SICK')) return 'sick'; + return 'regular' +} + +const expenseSubgroupFromBankCode = (bank_code: any): keyof TotalExpenses => { + const type = bank_code.type; + if (type.includes('MILEAGE')) return 'mileage'; + if (type.includes('PER_DIEM')) return 'per_diem'; + if (type.includes('ON_CALL')) return 'on_call'; + return 'expenses'; +} diff --git a/src/modules/timesheets/services/timesheets-command.service.ts b/src/modules/timesheets/services/timesheets-command.service.ts deleted file mode 100644 index cb3e82b..0000000 --- a/src/modules/timesheets/services/timesheets-command.service.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -import { EmployeeTimesheetResolver } from "src/modules/shared/utils/resolve-timesheet.utils"; -import { getWeekEnd, getWeekStart } from "src/common/utils/date-utils"; -import { parseISODate, parseHHmm } from "../utils-helpers-others/timesheet.helpers"; -import { TimesheetsQueryService } from "./timesheets-query.service"; -import { BaseApprovalService } from "src/common/shared/base-approval.service"; -import { Prisma, Timesheets } from "@prisma/client"; -import { CreateTimesheetDto } from "../dtos/create-timesheet.dto"; -import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; -import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; -import { PrismaService } from "src/prisma/prisma.service"; -import { TimesheetMap } from "../utils-helpers-others/timesheet.types"; -import { Shift, Expense } from "src/modules/shared/classes/timesheet.dto"; - -@Injectable() -export class TimesheetsCommandService extends BaseApprovalService{ - constructor( - prisma: PrismaService, - private readonly query: TimesheetsQueryService, - private readonly emailResolver: EmailToIdResolver, - private readonly timesheetResolver: EmployeeTimesheetResolver, - private readonly bankTypeResolver: BankCodesResolver, - ) {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; - } - -/**_____________________________________________________________________________________________ - create/update/delete shifts and expenses from 1 or many timesheet(s) - - -this function receives an email and an array of timesheets - - -this function will find the timesheets with all shifts and expenses - -this function will calculate total hours, total expenses, filtered by types, - cumulate in daily and weekly. - - -the timesheet_id will be determined using the employee email - -with the timesheet_id, all shifts and expenses will be fetched - - -with shift_id and expense_id, this function will compare both - datas from the DB and from the body of the function and then: - -it will create a shift if no shift is found in the DB - -it will update a shift if a shift is found in the DB - -it will delete a shift if a shift is found and no data is received from the frontend - - This function will be used for the Timesheet Page for an employee to enter, modify or delete and entry - This function will also be used in the modal of the timesheet validation page to - allow a supervisor to enter, modify or delete and entry of a selected employee -_____________________________________________________________________________________________*/ - -async findTimesheetsByEmailAndPayPeriod(email: string, year: number, period_no: number, timesheets: Timesheets[]): Promise { - const employee_id = await this.emailResolver.findIdByEmail(email); - - - return timesheets; -} - -async upsertOrDeleteShiftsByEmailAndDate(email:string, shift_ids: Shift[]) {} - -async upsertOrDeleteExpensesByEmailAndDate(email:string, expenses_id: Expense[]) {} - - - - - -//_____________________________________________________________________________________________ -// -//_____________________________________________________________________________________________ - - async createWeekShiftsAndReturnOverview( - email:string, - shifts: CreateTimesheetDto[], - week_offset = 0, - ): Promise { - //fetchs employee matchint user's email - const employee_id = await this.emailResolver.findIdByEmail(email); - if(!employee_id) throw new NotFoundException(`employee for ${ email } not found`); - - //insure that the week starts on sunday and finishes on saturday - const base = new Date(); - base.setDate(base.getDate() + week_offset * 7); - const start_week = getWeekStart(base, 0); - const end_week = getWeekEnd(start_week); - - const timesheet = await this.timesheetResolver.findTimesheetIdByEmail(email, base) - if(!timesheet) throw new NotFoundException(`no timesheet found for employe ${employee_id}`); - - //validations and insertions - for(const shift of shifts) { - const date = parseISODate(shift.date); - if (date < start_week || date > end_week) throw new BadRequestException(`date ${shift.date} not in current week`); - - const bank_code = await this.bankTypeResolver.findByType(shift.type) - if(!bank_code) throw new BadRequestException(`Invalid bank_code type: ${shift.type}`); - - await this.prisma.shifts.create({ - data: { - timesheet_id: timesheet.id, - bank_code_id: bank_code.id, - date: date, - start_time: parseHHmm(shift.start_time), - end_time: parseHHmm(shift.end_time), - comment: shift.comment ?? null, - is_approved: false, - is_remote: false, - }, - }); - } - return this.query.getTimesheetByEmail(email, week_offset); - } -} \ No newline at end of file diff --git a/src/modules/timesheets/services/timesheets-query.service.ts b/src/modules/timesheets/services/timesheets-query.service.ts deleted file mode 100644 index 67149ef..0000000 --- a/src/modules/timesheets/services/timesheets-query.service.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { makeEmptyTimesheet, mapExpenseRow, mapShiftRow } from '../utils-helpers-others/timesheet.mappers'; -import { buildPeriod, computeWeekRange } from '../utils-helpers-others/timesheet.utils'; -import { TimesheetSelectorsService } from '../utils-helpers-others/timesheet.selectors'; -import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto'; -import { toRangeFromPeriod } from '../utils-helpers-others/timesheet.helpers'; -import { EmailToIdResolver } from 'src/modules/shared/utils/resolve-email-id.utils'; -import { FullNameResolver } from 'src/modules/shared/utils/resolve-full-name.utils'; -import { PrismaService } from 'src/prisma/prisma.service'; -import { TimesheetMap } from '../utils-helpers-others/timesheet.types'; -import { Injectable } from '@nestjs/common'; - - -@Injectable() -export class TimesheetsQueryService { - constructor( - private readonly prisma: PrismaService, - private readonly emailResolver: EmailToIdResolver, - private readonly fullNameResolver: FullNameResolver, - private readonly selectors: TimesheetSelectorsService, - ) {} - - async findAll(year: number, period_no: number, email: string): Promise { - const employee_id = await this.emailResolver.findIdByEmail(email); //finds the employee using email - const full_name = await this.fullNameResolver.resolveFullName(employee_id); //finds the employee full name using employee_id - const period = await this.selectors.getPayPeriod(year, period_no);//finds the pay period using year and period_no - const{ from, to } = toRangeFromPeriod(period); //finds start and end dates - //finds all shifts from selected period - const [raw_shifts, raw_expenses] = await Promise.all([ - this.selectors.getShifts(employee_id, from, to), - this.selectors.getExpenses(employee_id, from, to), - ]); - // data mapping - const shifts = raw_shifts.map(mapShiftRow); - const expenses = raw_expenses.map(mapExpenseRow); - - return buildPeriod(period.period_start, period.period_end, shifts , expenses, full_name); - } - - - - async getTimesheetByEmail(email: string, week_offset = 0): Promise { - const employee_id = await this.emailResolver.findIdByEmail(email); //finds the employee using email - const { start, start_day, end_day, label } = computeWeekRange(week_offset); - const timesheet = await this.selectors.getTimesheetWithShiftsAndExpenses(employee_id, start); //fetch timesheet shifts and expenses - if(!timesheet) return makeEmptyTimesheet({ start_day, end_day, label}); - - //maps all shifts of selected timesheet - const shifts = timesheet.shift.map(mapShiftRow); - const expenses = timesheet.expense.map(mapExpenseRow); - - - return { start_day, end_day, label, shifts, expenses, is_approved: timesheet.is_approved}; - } -} diff --git a/src/modules/timesheets/timesheets.module.ts b/src/modules/timesheets/timesheets.module.ts index e51c30f..32d2c52 100644 --- a/src/modules/timesheets/timesheets.module.ts +++ b/src/modules/timesheets/timesheets.module.ts @@ -1,34 +1,24 @@ -import { TimesheetsController } from './controllers/timesheets.controller'; -import { TimesheetsQueryService } from './services/timesheets-query.service'; -import { TimesheetArchiveService } from './services/timesheet-archive.service'; -import { TimesheetsCommandService } from './services/timesheets-command.service'; -import { ShiftsCommandService } from '../shifts/services/shifts-command.service'; -import { ExpensesCommandService } from '../expenses/services/expenses-command.service'; -import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module'; -import { SharedModule } from '../shared/shared.module'; -import { Module } from '@nestjs/common'; -import { TimesheetSelectorsService } from './utils-helpers-others/timesheet.selectors'; -import { ShiftsHelpersService } from '../shifts/helpers/shifts.helpers'; +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: [TimesheetsController], + controllers: [TimesheetController], providers: [ - TimesheetsQueryService, - TimesheetsCommandService, - ShiftsCommandService, - ExpensesCommandService, - TimesheetArchiveService, - TimesheetSelectorsService, - ShiftsHelpersService, + TimesheetArchiveService, + GetTimesheetsOverviewService, ], exports: [ - TimesheetsQueryService, - TimesheetArchiveService, - TimesheetsCommandService + ], }) export class TimesheetsModule {} diff --git a/src/modules/timesheets/utils-helpers-others/timesheet.helpers.ts b/src/modules/timesheets/utils-helpers-others/timesheet.helpers.ts deleted file mode 100644 index 9e0ca82..0000000 --- a/src/modules/timesheets/utils-helpers-others/timesheet.helpers.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { MS_PER_DAY } from "src/modules/shared/constants/date-time.constant"; -import { DAY_KEYS, DayKey } from "././timesheet.types"; - -export function toUTCDateOnly(date: Date | string): Date { - const d = new Date(date); - return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); -} - -export function addDays(date:Date, days: number): Date { - return new Date(date.getTime() + days * MS_PER_DAY); -} - -export function endOfDayUTC(date: Date | string): Date { - const d = toUTCDateOnly(date); - return new Date(d.getTime() + MS_PER_DAY - 1); -} - -export function isBetweenUTC(date: Date, start: Date, end_inclusive: Date): boolean { - const time = date.getTime(); - return time >= start.getTime() && time <= end_inclusive.getTime(); -} - -export function toTimeString(date: Date): string { - const hours = String(date.getUTCHours()).padStart(2,'0'); - const minutes = String(date.getUTCMinutes()).padStart(2,'0'); - return `${hours}:${minutes}`; -} - -export function round2(num: number) { - return Math.round(num * 100) / 100; -} - -export function shortDate(date:Date): string { - const mm = String(date.getUTCMonth()+1).padStart(2,'0'); - const dd = String(date.getUTCDate()).padStart(2,'0'); - return `${mm}/${dd}`; -} - -export function dayKeyFromDate(date: Date, useUTC = true): DayKey { - const index = useUTC ? date.getUTCDay() : date.getDay(); // 0=Sunday..6=Saturday - return DAY_KEYS[index]; -} - -export const toHHmm = (date: Date) => date.toISOString().slice(11, 16); - -export function parseISODate(iso: string): Date { - const [ y, m, d ] = iso.split('-').map(Number); - return new Date(y, (m ?? 1) - 1, d ?? 1); -} - -export function parseHHmm(t: string): Date { - const [ hh, mm ] = t.split(':').map(Number); - return new Date(1970, 0, 1, hh || 0, mm || 0, 0, 0); -} - -export const toNum = (value: any) => - value && typeof value.toNumber === 'function' ? value.toNumber() : - typeof value === 'number' ? value : - value ? Number(value) : 0; - - -export const upper = (s?: string | null) => String(s ?? '').toUpperCase(); - -export const toRangeFromPeriod = (period: { period_start: Date; period_end: Date }) => ({ - from: toUTCDateOnly(period.period_start), - to: endOfDayUTC(period.period_end), -}); \ No newline at end of file diff --git a/src/modules/timesheets/utils-helpers-others/timesheet.mappers.ts b/src/modules/timesheets/utils-helpers-others/timesheet.mappers.ts deleted file mode 100644 index 6a0b928..0000000 --- a/src/modules/timesheets/utils-helpers-others/timesheet.mappers.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { DayExpensesDto, WeekDto, DetailedShifts, TimesheetPeriodDto } from "../dtos/timesheet-period.dto"; -import { ShiftRow, ExpenseRow, ExpensesAmount, TimesheetMap } from "./timesheet.types"; -import { addDays, shortDate, toNum, upper } from "./timesheet.helpers"; -import { Prisma } from "@prisma/client"; - - -//mappers -export const mapShiftRow = (shift: { - date: Date; - start_time: Date; - end_time: Date; - comment?: string | null; - is_approved: boolean; - is_remote: boolean; - bank_code: { type: string }; -}): ShiftRow => ({ - date: shift.date, - start_time: shift.start_time, - end_time: shift.end_time, - comment: shift.comment ?? '', - is_approved: shift.is_approved, - is_remote: shift.is_remote, - type: upper(shift.bank_code.type), -}); - -export const mapExpenseRow = (expense: { - date: Date; - amount: Prisma.Decimal | number | null; - mileage: Prisma.Decimal | number | null; - comment?: string | null; - is_approved: boolean; - supervisor_comment?: string|null; - bank_code: { type: string }, -}): ExpenseRow => ({ - date: expense.date, - amount: toNum(expense.amount), - mileage: toNum(expense.mileage), - comment: expense.comment ?? '', - is_approved: expense.is_approved, - supervisor_comment: expense.supervisor_comment ?? '', - type: upper(expense.bank_code.type), -}); - -// Factories -export function makeEmptyDayExpenses(): DayExpensesDto { - return { - expenses: [], - total_expense: -1, - total_mileage: -1, - }; -} - -export function makeEmptyWeek(week_start: Date): WeekDto { - const make_empty_shifts = (offset: number): DetailedShifts => ({ - shifts: [], - regular_hours: 0, - evening_hours: 0, - emergency_hours: 0, - overtime_hours: 0, - comment: '', - short_date: shortDate(addDays(week_start, offset)), - break_durations: 0, - }); - return { - is_approved: true, - shifts: { - sun: make_empty_shifts(0), - mon: make_empty_shifts(1), - tue: make_empty_shifts(2), - wed: make_empty_shifts(3), - thu: make_empty_shifts(4), - fri: make_empty_shifts(5), - sat: make_empty_shifts(6), - }, - expenses: { - sun: makeEmptyDayExpenses(), - mon: makeEmptyDayExpenses(), - tue: makeEmptyDayExpenses(), - wed: makeEmptyDayExpenses(), - thu: makeEmptyDayExpenses(), - fri: makeEmptyDayExpenses(), - sat: makeEmptyDayExpenses(), - }, - }; -} - -export function makeEmptyPeriod(): TimesheetPeriodDto { - return { weeks: [makeEmptyWeek(new Date()), makeEmptyWeek(new Date())], employee_full_name: '' }; -} - -export const makeAmounts = (): ExpensesAmount => ({ - expense: 0, - mileage: 0, -}); - -export function makeEmptyTimesheet(params: { - start_day: string; - end_day: string; - label: string; - is_approved?: boolean; -}): TimesheetMap { - const { start_day, end_day, label, is_approved = false } = params; - return { - start_day, - end_day, - label, - shifts: [], - expenses: [], - is_approved, - }; -} \ No newline at end of file diff --git a/src/modules/timesheets/utils-helpers-others/timesheet.selectors.ts b/src/modules/timesheets/utils-helpers-others/timesheet.selectors.ts deleted file mode 100644 index ec082ad..0000000 --- a/src/modules/timesheets/utils-helpers-others/timesheet.selectors.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { EXPENSE_ASC_ORDER, EXPENSE_SELECT } from "../../shared/selects/expenses.select"; -import { Injectable, NotFoundException } from "@nestjs/common"; -import { SHIFT_ASC_ORDER, SHIFT_SELECT } from "../../shared/selects/shifts.select"; -import { PAY_PERIOD_SELECT } from "../../shared/selects/pay-periods.select"; -import { PrismaService } from "src/prisma/prisma.service"; - -@Injectable() -export class TimesheetSelectorsService { - constructor(readonly prisma: PrismaService){} - - async getPayPeriod(pay_year: number, pay_period_no: number) { - const period = await this.prisma.payPeriods.findFirst({ - where: { pay_year, pay_period_no }, - select: PAY_PERIOD_SELECT , - }); - if(!period) throw new NotFoundException(`period ${pay_year}-${pay_period_no} not found`); - return period; - } - - async getShifts(employee_id: number, from: Date, to: Date) { - return this.prisma.shifts.findMany({ - where: {timesheet: { is: { employee_id } }, date: { gte: from, lte: to } }, - select: SHIFT_SELECT, - orderBy: SHIFT_ASC_ORDER, - }); - } - - async getExpenses(employee_id: number, from: Date, to: Date) { - return this.prisma.expenses.findMany({ - where: { timesheet: {is: { employee_id } }, date: { gte: from, lte: to } }, - select: EXPENSE_SELECT, - orderBy: EXPENSE_ASC_ORDER, - }); - } - - async getTimesheetWithShiftsAndExpenses(employee_id: number, start_date_week: Date) { - return this.prisma.timesheets.findUnique({ - where: { employee_id_start_date: { employee_id, start_date: start_date_week } }, - select: { - is_approved: true, - shift: { select: SHIFT_SELECT, orderBy: SHIFT_ASC_ORDER }, - expense: { select: EXPENSE_SELECT, orderBy: EXPENSE_ASC_ORDER }, - }, - }); - } -} \ No newline at end of file diff --git a/src/modules/timesheets/utils-helpers-others/timesheet.types.ts b/src/modules/timesheets/utils-helpers-others/timesheet.types.ts deleted file mode 100644 index 6fcc951..0000000 --- a/src/modules/timesheets/utils-helpers-others/timesheet.types.ts +++ /dev/null @@ -1,74 +0,0 @@ -export type ShiftRow = { - date: Date; - start_time: Date; - end_time: Date; - comment: string; - is_approved?: boolean; - is_remote: boolean; - type: string -}; -export type ExpenseRow = { - date: Date; - amount: number; - mileage?: number | null; - comment: string; - type: string; - is_approved?: boolean; - supervisor_comment: string; -}; - -export type TimesheetMap = { - start_day: string; - end_day: string; - label: string; - shifts: ShiftRow[]; - expenses: ExpenseRow[] - is_approved: boolean; -} - -// Types -export const SHIFT_TYPES = { - REGULAR: 'REGULAR', - EVENING: 'EVENING', - OVERTIME: 'OVERTIME', - EMERGENCY: 'EMERGENCY', - HOLIDAY: 'HOLIDAY', - VACATION: 'VACATION', - SICK: 'SICK', -} as const; - -export const EXPENSE_TYPES = { - MILEAGE: 'MILEAGE', - EXPENSE: 'EXPENSES', - PER_DIEM: 'PER_DIEM', - ON_CALL: 'ON_CALL', -} as const; - -//makes the strings indexes for arrays -export const DAY_KEYS = ['sun','mon','tue','wed','thu','fri','sat'] as const; -export type DayKey = typeof DAY_KEYS[number]; - -//shifts's hour by type -export type ShiftsHours = { - regular: number; - evening: number; - overtime: number; - emergency: number; - sick: number; - vacation: number; - holiday: number; -}; -export const make_hours = (): ShiftsHours => ({ - regular: 0, - evening: 0, - overtime: 0, - emergency: 0, - sick: 0, - vacation: 0, - holiday: 0, -}); - -export type ExpensesAmount = { - expense: number; - mileage: number; -}; \ No newline at end of file diff --git a/src/modules/timesheets/utils-helpers-others/timesheet.utils.ts b/src/modules/timesheets/utils-helpers-others/timesheet.utils.ts deleted file mode 100644 index d6e691f..0000000 --- a/src/modules/timesheets/utils-helpers-others/timesheet.utils.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { - DayKey, DAY_KEYS, EXPENSE_TYPES, ExpenseRow, - SHIFT_TYPES, ShiftRow, make_hours, ShiftsHours, ExpensesAmount -} from "./timesheet.types"; -import { - isBetweenUTC, dayKeyFromDate, toTimeString, round2, - toUTCDateOnly, endOfDayUTC, addDays -} from "./timesheet.helpers"; -import { WeekDto, ShiftDto, TimesheetPeriodDto, DayExpensesDto, ExpenseDto } from "../dtos/timesheet-period.dto"; -import { getWeekStart, getWeekEnd, formatDateISO } from "src/common/utils/date-utils"; -import { makeAmounts, makeEmptyWeek } from "./timesheet.mappers"; -import { toDateString } from "src/modules/pay-periods/utils/pay-year.util"; -import { MS_PER_HOUR } from "src/modules/shared/constants/date-time.constant"; - - export function computeWeekRange(week_offset = 0){ - //sets current week Sunday -> Saturday - const base = new Date(); - const offset = new Date(base); - offset.setDate(offset.getDate() + (week_offset * 7)); - - const start = getWeekStart(offset, 0); - const end = getWeekEnd(start); - const start_day = formatDateISO(start); - const end_day = formatDateISO(end); - const label = `${(start_day)}.${(end_day)}`; - - return { start, end, start_day, end_day, label } - }; - -export function buildWeek( - week_start: Date, - week_end: Date, - shifts: ShiftRow[], - expenses: ExpenseRow[], - ): WeekDto { - const week = makeEmptyWeek(week_start); - let all_approved = true; - - const day_times: Record> = DAY_KEYS.reduce((acc, key) => { - acc[key] = []; return acc; - }, {} as Record>); - - const day_hours: Record = DAY_KEYS.reduce((acc, key) => { - acc[key] = make_hours(); return acc; - }, {} as Record); - - const day_amounts: Record = DAY_KEYS.reduce((acc, key) => { - acc[key] = makeAmounts(); return acc; - }, {} as Record); - - const day_expense_rows: Record = DAY_KEYS.reduce((acc, key) => { - acc[key] = { - expenses: [{ - type: '', - amount: -1, - mileage: -1, - comment: '', - is_approved: false, - supervisor_comment: '', - }], - total_expense: -1, - total_mileage: -1, - }; - return acc; - }, {} as Record); - - //regroup hours per type of shifts - const week_shifts = shifts.filter(shift => isBetweenUTC(shift.date, week_start, week_end)); - for (const shift of week_shifts) { - const key = dayKeyFromDate(shift.date, true); - week.shifts[key].shifts.push({ - date: toDateString(shift.date), - type: shift.type, - start_time: toTimeString(shift.start_time), - end_time: toTimeString(shift.end_time), - comment: shift.comment, - is_approved: shift.is_approved ?? true, - is_remote: shift.is_remote, - } as ShiftDto); - - day_times[key].push({ start: shift.start_time, end: shift.end_time}); - - const duration = Math.max(0, (shift.end_time.getTime() - shift.start_time.getTime())/ MS_PER_HOUR); - const type = (shift.type || '').toUpperCase(); - - if ( type === SHIFT_TYPES.REGULAR) day_hours[key].regular += duration; - else if( type === SHIFT_TYPES.EVENING) day_hours[key].evening += duration; - else if( type === SHIFT_TYPES.EMERGENCY) day_hours[key].emergency += duration; - else if( type === SHIFT_TYPES.OVERTIME) day_hours[key].overtime += duration; - else if( type === SHIFT_TYPES.SICK) day_hours[key].sick += duration; - else if( type === SHIFT_TYPES.VACATION) day_hours[key].vacation += duration; - else if( type === SHIFT_TYPES.HOLIDAY) day_hours[key].holiday += duration; - - all_approved = all_approved && (shift.is_approved ?? true ); - } - - //regroupe amounts to type of expenses - const week_expenses = expenses.filter(expense => isBetweenUTC(expense.date, week_start, week_end)); - for (const expense of week_expenses) { - const key = dayKeyFromDate(expense.date, true); - const type = (expense.type || '').toUpperCase(); - - const row: ExpenseDto = { - type, - amount: round2(expense.amount ?? 0), - mileage: round2(expense.mileage ?? 0), - comment: expense.comment ?? '', - is_approved: expense.is_approved ?? true, - supervisor_comment: expense.supervisor_comment ?? '', - }; - - day_expense_rows[key].expenses.push(row); - - if(type === EXPENSE_TYPES.MILEAGE) { - day_amounts[key].mileage += row.mileage ?? 0; - } else { - day_amounts[key].expense += row.amount; - } - - all_approved = all_approved && row.is_approved; - } - - for (const key of DAY_KEYS) { - //return exposed dto data - week.shifts[key].regular_hours = round2(day_hours[key].regular); - week.shifts[key].evening_hours = round2(day_hours[key].evening); - week.shifts[key].overtime_hours = round2(day_hours[key].overtime); - week.shifts[key].emergency_hours = round2(day_hours[key].emergency); - - //calculate gaps between shifts - const times = day_times[key].sort((a,b) => a.start.getTime() - b.start.getTime()); - let gaps = 0; - for (let i = 1; i < times.length; i++) { - const gap = (times[i].start.getTime() - times[i - 1].end.getTime()) / MS_PER_HOUR; - if(gap > 0) gaps += gap; - } - week.shifts[key].break_durations = round2(gaps); - - //daily totals - const totals = day_amounts[key]; - - day_expense_rows[key].total_mileage = round2(totals.mileage); - day_expense_rows[key].total_expense = round2(totals.expense); - } - - week.is_approved = all_approved; - return week; -} - -export function buildPeriod( - period_start: Date, - period_end: Date, - shifts: ShiftRow[], - expenses: ExpenseRow[], - employeeFullName = '' -): TimesheetPeriodDto { - const week1_start = toUTCDateOnly(period_start); - const week1_end = endOfDayUTC(addDays(week1_start, 6)); - const week2_start = toUTCDateOnly(addDays(week1_start, 7)); - const week2_end = endOfDayUTC(period_end); - - const weeks: WeekDto[] = [ - buildWeek(week1_start, week1_end, shifts, expenses), - buildWeek(week2_start, week2_end, shifts, expenses), - ]; - - return { - weeks, - employee_full_name: employeeFullName, - }; -} \ No newline at end of file diff --git a/src/modules/timesheets/~misc_deprecated-files/create-timesheet.dto.ts b/src/modules/timesheets/~misc_deprecated-files/create-timesheet.dto.ts new file mode 100644 index 0000000..ceb3807 --- /dev/null +++ b/src/modules/timesheets/~misc_deprecated-files/create-timesheet.dto.ts @@ -0,0 +1,33 @@ +// import { Type } from "class-transformer"; +// import { IsArray, IsOptional, IsString, Length, Matches, ValidateNested } from "class-validator"; + +// export class CreateTimesheetDto { + +// @IsString() +// @Matches(/^\d{4}-\d{2}-\d{2}$/) +// date!: string; + +// @IsString() +// @Length(1,64) +// type!: string; + +// @IsString() +// @Matches(/^\d{2}:\d{2}$/) +// start_time!: string; + +// @IsString() +// @Matches(/^\d{2}:\d{2}$/) +// end_time!: string; + +// @IsOptional() +// @IsString() +// @Length(0,512) +// comment?: string; +// } + +// export class CreateWeekShiftsDto { +// @IsArray() +// @ValidateNested({each:true}) +// @Type(()=> CreateTimesheetDto) +// shifts!: CreateTimesheetDto[]; +// } diff --git a/src/modules/timesheets/~misc_deprecated-files/search-timesheet.dto.ts b/src/modules/timesheets/~misc_deprecated-files/search-timesheet.dto.ts new file mode 100644 index 0000000..6e1ba12 --- /dev/null +++ b/src/modules/timesheets/~misc_deprecated-files/search-timesheet.dto.ts @@ -0,0 +1,20 @@ +// import { Type } from "class-transformer"; +// import { IsBoolean, IsInt, IsOptional } from "class-validator"; + + +// export class SearchTimesheetDto { +// @IsOptional() +// @Type(() => Number) +// @IsInt() +// timesheet_id?: number; + +// @IsOptional() +// @Type(()=> Number) +// @IsInt() +// employee_id?: number; + +// @IsOptional() +// @Type(()=> Boolean) +// @IsBoolean() +// is_approved?: boolean; +// } \ No newline at end of file diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheet-period.dto.ts b/src/modules/timesheets/~misc_deprecated-files/timesheet-period.dto.ts new file mode 100644 index 0000000..004026e --- /dev/null +++ b/src/modules/timesheets/~misc_deprecated-files/timesheet-period.dto.ts @@ -0,0 +1,75 @@ +// export class TimesheetDto { +// start_day: string; +// end_day: string; +// label: string; +// shifts: ShiftDto[]; +// expenses: ExpenseDto[] +// is_approved: boolean; +// } + +// export class ShiftDto { +// date: string; +// type: string; +// start_time: string; +// end_time : string; +// comment: string; +// is_approved: boolean; +// is_remote: boolean; +// } + +// export class ExpenseDto { +// type: string; +// amount: number; +// mileage: number; +// comment: string; +// is_approved: boolean; +// supervisor_comment: string; +// } + +// export type DayShiftsDto = ShiftDto[]; + +// export class DetailedShifts { +// shifts: DayShiftsDto; +// regular_hours: number; +// evening_hours: number; +// overtime_hours: number; +// emergency_hours: number; +// comment: string; +// short_date: string; +// break_durations?: number; +// } + +// export class DayExpensesDto { +// expenses: ExpenseDto[] = []; +// total_mileage: number; +// total_expense: number; +// } + +// export class WeekDto { +// is_approved: boolean; +// shifts: { +// sun: DetailedShifts; +// mon: DetailedShifts; +// tue: DetailedShifts; +// wed: DetailedShifts; +// thu: DetailedShifts; +// fri: DetailedShifts; +// sat: DetailedShifts; +// } +// expenses: { +// sun: DayExpensesDto; +// mon: DayExpensesDto; +// tue: DayExpensesDto; +// wed: DayExpensesDto; +// thu: DayExpensesDto; +// fri: DayExpensesDto; +// sat: DayExpensesDto; +// } +// } + +// export class TimesheetPeriodDto { +// weeks: WeekDto[]; +// employee_full_name: string; +// } + + diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheet.helpers.ts b/src/modules/timesheets/~misc_deprecated-files/timesheet.helpers.ts new file mode 100644 index 0000000..930a277 --- /dev/null +++ b/src/modules/timesheets/~misc_deprecated-files/timesheet.helpers.ts @@ -0,0 +1,67 @@ +// import { MS_PER_DAY } from "src/modules/shared/constants/date-time.constant"; +// import { DAY_KEYS, DayKey } from "./timesheet.types"; + +// export function toUTCDateOnly(date: Date | string): Date { +// const d = new Date(date); +// return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); +// } + +// export function addDays(date:Date, days: number): Date { +// return new Date(date.getTime() + days * MS_PER_DAY); +// } + +// export function endOfDayUTC(date: Date | string): Date { +// const d = toUTCDateOnly(date); +// return new Date(d.getTime() + MS_PER_DAY - 1); +// } + +// export function isBetweenUTC(date: Date, start: Date, end_inclusive: Date): boolean { +// const time = date.getTime(); +// return time >= start.getTime() && time <= end_inclusive.getTime(); +// } + +// export function toTimeString(date: Date): string { +// const hours = String(date.getUTCHours()).padStart(2,'0'); +// const minutes = String(date.getUTCMinutes()).padStart(2,'0'); +// return `${hours}:${minutes}`; +// } + +// export function round2(num: number) { +// return Math.round(num * 100) / 100; +// } + +// export function shortDate(date:Date): string { +// const mm = String(date.getUTCMonth()+1).padStart(2,'0'); +// const dd = String(date.getUTCDate()).padStart(2,'0'); +// return `${mm}/${dd}`; +// } + +// export function dayKeyFromDate(date: Date, useUTC = true): DayKey { +// const index = useUTC ? date.getUTCDay() : date.getDay(); // 0=Sunday..6=Saturday +// return DAY_KEYS[index]; +// } + +// export const toHHmm = (date: Date) => date.toISOString().slice(11, 16); + +// export function parseISODate(iso: string): Date { +// const [ y, m, d ] = iso.split('-').map(Number); +// return new Date(y, (m ?? 1) - 1, d ?? 1); +// } + +// export function parseHHmm(t: string): Date { +// const [ hh, mm ] = t.split(':').map(Number); +// return new Date(1970, 0, 1, hh || 0, mm || 0, 0, 0); +// } + +// export const toNum = (value: any) => +// value && typeof value.toNumber === 'function' ? value.toNumber() : +// typeof value === 'number' ? value : +// value ? Number(value) : 0; + + +// export const upper = (s?: string | null) => String(s ?? '').toUpperCase(); + +// export const toRangeFromPeriod = (period: { period_start: Date; period_end: Date }) => ({ +// from: toUTCDateOnly(period.period_start), +// to: endOfDayUTC(period.period_end), +// }); \ No newline at end of file diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheet.mappers.ts b/src/modules/timesheets/~misc_deprecated-files/timesheet.mappers.ts new file mode 100644 index 0000000..64a21f8 --- /dev/null +++ b/src/modules/timesheets/~misc_deprecated-files/timesheet.mappers.ts @@ -0,0 +1,111 @@ +// import { DayExpensesDto, WeekDto, DetailedShifts, TimesheetPeriodDto } from "../dtos/timesheet-period.dto"; +// import { ShiftRow, ExpenseRow, ExpensesAmount, TimesheetMap } from "./timesheet.types"; +// import { addDays, shortDate, toNum, upper } from "./timesheet.helpers"; +// import { Prisma } from "@prisma/client"; + + +// //mappers +// export const mapShiftRow = (shift: { +// date: Date; +// start_time: Date; +// end_time: Date; +// comment?: string | null; +// is_approved: boolean; +// is_remote: boolean; +// bank_code: { type: string }; +// }): ShiftRow => ({ +// date: shift.date, +// start_time: shift.start_time, +// end_time: shift.end_time, +// comment: shift.comment ?? '', +// is_approved: shift.is_approved, +// is_remote: shift.is_remote, +// type: upper(shift.bank_code.type), +// }); + +// export const mapExpenseRow = (expense: { +// date: Date; +// amount: Prisma.Decimal | number | null; +// mileage: Prisma.Decimal | number | null; +// comment?: string | null; +// is_approved: boolean; +// supervisor_comment?: string|null; +// bank_code: { type: string }, +// }): ExpenseRow => ({ +// date: expense.date, +// amount: toNum(expense.amount), +// mileage: toNum(expense.mileage), +// comment: expense.comment ?? '', +// is_approved: expense.is_approved, +// supervisor_comment: expense.supervisor_comment ?? '', +// type: upper(expense.bank_code.type), +// }); + +// // Factories +// export function makeEmptyDayExpenses(): DayExpensesDto { +// return { +// expenses: [], +// total_expense: -1, +// total_mileage: -1, +// }; +// } + +// export function makeEmptyWeek(week_start: Date): WeekDto { +// const make_empty_shifts = (offset: number): DetailedShifts => ({ +// shifts: [], +// regular_hours: 0, +// evening_hours: 0, +// emergency_hours: 0, +// overtime_hours: 0, +// comment: '', +// short_date: shortDate(addDays(week_start, offset)), +// break_durations: 0, +// }); +// return { +// is_approved: true, +// shifts: { +// sun: make_empty_shifts(0), +// mon: make_empty_shifts(1), +// tue: make_empty_shifts(2), +// wed: make_empty_shifts(3), +// thu: make_empty_shifts(4), +// fri: make_empty_shifts(5), +// sat: make_empty_shifts(6), +// }, +// expenses: { +// sun: makeEmptyDayExpenses(), +// mon: makeEmptyDayExpenses(), +// tue: makeEmptyDayExpenses(), +// wed: makeEmptyDayExpenses(), +// thu: makeEmptyDayExpenses(), +// fri: makeEmptyDayExpenses(), +// sat: makeEmptyDayExpenses(), +// }, +// }; +// } + +// export function makeEmptyPeriod(): TimesheetPeriodDto { +// return { weeks: [makeEmptyWeek(new Date()), makeEmptyWeek(new Date())], employee_full_name: '' }; +// } + +// export const makeAmounts = (): ExpensesAmount => ({ +// expense: 0, +// mileage: 0, +// }); + +// export function makeEmptyTimesheet(params: { +// start_day: string; +// end_day: string; +// label: string; +// is_approved?: boolean; +// }): TimesheetMap { +// const { start_day, end_day, label, is_approved = false } = params; +// return { +// start_day, +// end_day, +// label, +// shifts: [], +// expenses: [], +// is_approved, +// }; +// } \ No newline at end of file diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheet.selectors.ts b/src/modules/timesheets/~misc_deprecated-files/timesheet.selectors.ts new file mode 100644 index 0000000..698f5a7 --- /dev/null +++ b/src/modules/timesheets/~misc_deprecated-files/timesheet.selectors.ts @@ -0,0 +1,46 @@ +// import { EXPENSE_ASC_ORDER, EXPENSE_SELECT } from "../../../shared/selects/expenses.select"; +// import { Injectable, NotFoundException } from "@nestjs/common"; +// import { SHIFT_ASC_ORDER, SHIFT_SELECT } from "../../../shared/selects/shifts.select"; +// import { PAY_PERIOD_SELECT } from "../../../shared/selects/pay-periods.select"; +// import { PrismaService } from "src/prisma/prisma.service"; + +// @Injectable() +// export class TimesheetSelectorsService { +// constructor(readonly prisma: PrismaService){} + +// async getPayPeriod(pay_year: number, pay_period_no: number) { +// const period = await this.prisma.payPeriods.findFirst({ +// where: { pay_year, pay_period_no }, +// select: PAY_PERIOD_SELECT , +// }); +// if(!period) throw new NotFoundException(`period ${pay_year}-${pay_period_no} not found`); +// return period; +// } + +// async getShifts(employee_id: number, from: Date, to: Date) { +// return this.prisma.shifts.findMany({ +// where: {timesheet: { is: { employee_id } }, date: { gte: from, lte: to } }, +// select: SHIFT_SELECT, +// orderBy: SHIFT_ASC_ORDER, +// }); +// } + +// async getExpenses(employee_id: number, from: Date, to: Date) { +// return this.prisma.expenses.findMany({ +// where: { timesheet: {is: { employee_id } }, date: { gte: from, lte: to } }, +// select: EXPENSE_SELECT, +// orderBy: EXPENSE_ASC_ORDER, +// }); +// } + +// async getTimesheetWithShiftsAndExpenses(employee_id: number, start_date_week: Date) { +// return this.prisma.timesheets.findUnique({ +// where: { employee_id_start_date: { employee_id, start_date: start_date_week } }, +// select: { +// is_approved: true, +// shift: { select: SHIFT_SELECT, orderBy: SHIFT_ASC_ORDER }, +// expense: { select: EXPENSE_SELECT, orderBy: EXPENSE_ASC_ORDER }, +// }, +// }); +// } +// } \ No newline at end of file diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheet.types.ts b/src/modules/timesheets/~misc_deprecated-files/timesheet.types.ts new file mode 100644 index 0000000..1d1d0be --- /dev/null +++ b/src/modules/timesheets/~misc_deprecated-files/timesheet.types.ts @@ -0,0 +1,74 @@ +// export type ShiftRow = { +// date: Date; +// start_time: Date; +// end_time: Date; +// comment: string; +// is_approved?: boolean; +// is_remote: boolean; +// type: string +// }; +// export type ExpenseRow = { +// date: Date; +// amount: number; +// mileage?: number | null; +// comment: string; +// type: string; +// is_approved?: boolean; +// supervisor_comment: string; +// }; + +// export type TimesheetMap = { +// start_day: string; +// end_day: string; +// label: string; +// shifts: ShiftRow[]; +// expenses: ExpenseRow[] +// is_approved: boolean; +// } + +// // Types +// export const SHIFT_TYPES = { +// REGULAR: 'REGULAR', +// EVENING: 'EVENING', +// OVERTIME: 'OVERTIME', +// EMERGENCY: 'EMERGENCY', +// HOLIDAY: 'HOLIDAY', +// VACATION: 'VACATION', +// SICK: 'SICK', +// } as const; + +// export const EXPENSE_TYPES = { +// MILEAGE: 'MILEAGE', +// EXPENSE: 'EXPENSES', +// PER_DIEM: 'PER_DIEM', +// ON_CALL: 'ON_CALL', +// } as const; + +// //makes the strings indexes for arrays +// export const DAY_KEYS = ['sun','mon','tue','wed','thu','fri','sat'] as const; +// export type DayKey = typeof DAY_KEYS[number]; + +// //shifts's hour by type +// export type ShiftsHours = { +// regular: number; +// evening: number; +// overtime: number; +// emergency: number; +// sick: number; +// vacation: number; +// holiday: number; +// }; +// export const make_hours = (): ShiftsHours => ({ +// regular: 0, +// evening: 0, +// overtime: 0, +// emergency: 0, +// sick: 0, +// vacation: 0, +// holiday: 0, +// }); + +// export type ExpensesAmount = { +// expense: number; +// mileage: number; +// }; \ No newline at end of file diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheet.utils.ts b/src/modules/timesheets/~misc_deprecated-files/timesheet.utils.ts new file mode 100644 index 0000000..b1ca369 --- /dev/null +++ b/src/modules/timesheets/~misc_deprecated-files/timesheet.utils.ts @@ -0,0 +1,171 @@ +// import { +// DayKey, DAY_KEYS, EXPENSE_TYPES, ExpenseRow, +// SHIFT_TYPES, ShiftRow, make_hours, ShiftsHours, ExpensesAmount +// } from "./timesheet.types"; +// import { +// isBetweenUTC, dayKeyFromDate, toTimeString, round2, +// toUTCDateOnly, endOfDayUTC, addDays +// } from "./timesheet.helpers"; +// import { WeekDto, ShiftDto, TimesheetPeriodDto, DayExpensesDto, ExpenseDto } from "../dtos/timesheet-period.dto"; +// import { getWeekStart, getWeekEnd, formatDateISO } from "src/common/utils/date-utils"; +// import { makeAmounts, makeEmptyWeek } from "./timesheet.mappers"; +// import { toDateString } from "src/modules/pay-periods/utils/pay-year.util"; +// import { MS_PER_HOUR } from "src/modules/shared/constants/date-time.constant"; + +// export function computeWeekRange(week_offset = 0){ +// //sets current week Sunday -> Saturday +// const base = new Date(); +// const offset = new Date(base); +// offset.setDate(offset.getDate() + (week_offset * 7)); + +// const start = getWeekStart(offset, 0); +// const end = getWeekEnd(start); +// const start_day = formatDateISO(start); +// const end_day = formatDateISO(end); +// const label = `${(start_day)}.${(end_day)}`; + +// return { start, end, start_day, end_day, label } +// }; + +// export function buildWeek( +// week_start: Date, +// week_end: Date, +// shifts: ShiftRow[], +// expenses: ExpenseRow[], +// ): WeekDto { +// const week = makeEmptyWeek(week_start); +// let all_approved = true; + +// const day_times: Record> = DAY_KEYS.reduce((acc, key) => { +// acc[key] = []; return acc; +// }, {} as Record>); + +// const day_hours: Record = DAY_KEYS.reduce((acc, key) => { +// acc[key] = make_hours(); return acc; +// }, {} as Record); + +// const day_amounts: Record = DAY_KEYS.reduce((acc, key) => { +// acc[key] = makeAmounts(); return acc; +// }, {} as Record); + +// const day_expense_rows: Record = DAY_KEYS.reduce((acc, key) => { +// acc[key] = { +// expenses: [{ +// type: '', +// amount: -1, +// mileage: -1, +// comment: '', +// is_approved: false, +// supervisor_comment: '', +// }], +// total_expense: -1, +// total_mileage: -1, +// }; +// return acc; +// }, {} as Record); + +// //regroup hours per type of shifts +// const week_shifts = shifts.filter(shift => isBetweenUTC(shift.date, week_start, week_end)); +// for (const shift of week_shifts) { +// const key = dayKeyFromDate(shift.date, true); +// week.shifts[key].shifts.push({ +// date: toDateString(shift.date), +// type: shift.type, +// start_time: toTimeString(shift.start_time), +// end_time: toTimeString(shift.end_time), +// comment: shift.comment, +// is_approved: shift.is_approved ?? true, +// is_remote: shift.is_remote, +// } as ShiftDto); + +// day_times[key].push({ start: shift.start_time, end: shift.end_time}); + +// const duration = Math.max(0, (shift.end_time.getTime() - shift.start_time.getTime())/ MS_PER_HOUR); +// const type = (shift.type || '').toUpperCase(); + +// if ( type === SHIFT_TYPES.REGULAR) day_hours[key].regular += duration; +// else if( type === SHIFT_TYPES.EVENING) day_hours[key].evening += duration; +// else if( type === SHIFT_TYPES.EMERGENCY) day_hours[key].emergency += duration; +// else if( type === SHIFT_TYPES.OVERTIME) day_hours[key].overtime += duration; +// else if( type === SHIFT_TYPES.SICK) day_hours[key].sick += duration; +// else if( type === SHIFT_TYPES.VACATION) day_hours[key].vacation += duration; +// else if( type === SHIFT_TYPES.HOLIDAY) day_hours[key].holiday += duration; + +// all_approved = all_approved && (shift.is_approved ?? true ); +// } + +// //regroupe amounts to type of expenses +// const week_expenses = expenses.filter(expense => isBetweenUTC(expense.date, week_start, week_end)); +// for (const expense of week_expenses) { +// const key = dayKeyFromDate(expense.date, true); +// const type = (expense.type || '').toUpperCase(); + +// const row: ExpenseDto = { +// type, +// amount: round2(expense.amount ?? 0), +// mileage: round2(expense.mileage ?? 0), +// comment: expense.comment ?? '', +// is_approved: expense.is_approved ?? true, +// supervisor_comment: expense.supervisor_comment ?? '', +// }; + +// day_expense_rows[key].expenses.push(row); + +// if(type === EXPENSE_TYPES.MILEAGE) { +// day_amounts[key].mileage += row.mileage ?? 0; +// } else { +// day_amounts[key].expense += row.amount; +// } + +// all_approved = all_approved && row.is_approved; +// } + +// for (const key of DAY_KEYS) { +// //return exposed dto data +// week.shifts[key].regular_hours = round2(day_hours[key].regular); +// week.shifts[key].evening_hours = round2(day_hours[key].evening); +// week.shifts[key].overtime_hours = round2(day_hours[key].overtime); +// week.shifts[key].emergency_hours = round2(day_hours[key].emergency); + +// //calculate gaps between shifts +// const times = day_times[key].sort((a,b) => a.start.getTime() - b.start.getTime()); +// let gaps = 0; +// for (let i = 1; i < times.length; i++) { +// const gap = (times[i].start.getTime() - times[i - 1].end.getTime()) / MS_PER_HOUR; +// if(gap > 0) gaps += gap; +// } +// week.shifts[key].break_durations = round2(gaps); + +// //daily totals +// const totals = day_amounts[key]; + +// day_expense_rows[key].total_mileage = round2(totals.mileage); +// day_expense_rows[key].total_expense = round2(totals.expense); +// } + +// week.is_approved = all_approved; +// return week; +// } + +// export function buildPeriod( +// period_start: Date, +// period_end: Date, +// shifts: ShiftRow[], +// expenses: ExpenseRow[], +// employeeFullName = '' +// ): TimesheetPeriodDto { +// const week1_start = toUTCDateOnly(period_start); +// const week1_end = endOfDayUTC(addDays(week1_start, 6)); +// const week2_start = toUTCDateOnly(addDays(week1_start, 7)); +// const week2_end = endOfDayUTC(period_end); + +// const weeks: WeekDto[] = [ +// buildWeek(week1_start, week1_end, shifts, expenses), +// buildWeek(week2_start, week2_end, shifts, expenses), +// ]; + +// return { +// weeks, +// employee_full_name: employeeFullName, +// }; +// } \ No newline at end of file diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheets-command.service.ts b/src/modules/timesheets/~misc_deprecated-files/timesheets-command.service.ts new file mode 100644 index 0000000..9fb3b52 --- /dev/null +++ b/src/modules/timesheets/~misc_deprecated-files/timesheets-command.service.ts @@ -0,0 +1,137 @@ +// import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +// import { EmployeeTimesheetResolver } from "src/modules/shared/utils/resolve-timesheet.utils"; +// import { getWeekEnd, getWeekStart } from "src/common/utils/date-utils"; +// import { parseISODate, parseHHmm } from "./utils-helpers-others/timesheet.helpers"; +// import { TimesheetsQueryService } from "./timesheets-query.service"; +// import { BaseApprovalService } from "src/common/shared/base-approval.service"; +// import { Prisma, Timesheets } from "@prisma/client"; +// import { CreateTimesheetDto } from "./create-timesheet.dto"; +// import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; +// import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; +// import { PrismaService } from "src/prisma/prisma.service"; +// import { TimesheetMap } from "./utils-helpers-others/timesheet.types"; +// import { Shift, Expense } from "../dtos/timesheet.dto"; + +// @Injectable() +// export class TimesheetsCommandService extends BaseApprovalService{ +// constructor( +// prisma: PrismaService, +// private readonly query: TimesheetsQueryService, +// private readonly emailResolver: EmailToIdResolver, +// private readonly timesheetResolver: EmployeeTimesheetResolver, +// private readonly bankTypeResolver: BankCodesResolver, +// ) {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; +// } + +// /**_____________________________________________________________________________________________ +// create/update/delete shifts and expenses from 1 or many timesheet(s) + +// -this function receives an email and an array of timesheets + +// -this function will find the timesheets with all shifts and expenses +// -this function will calculate total hours, total expenses, filtered by types, +// cumulate in daily and weekly. + +// -the timesheet_id will be determined using the employee email +// -with the timesheet_id, all shifts and expenses will be fetched + +// -with shift_id and expense_id, this function will compare both +// datas from the DB and from the body of the function and then: +// -it will create a shift if no shift is found in the DB +// -it will update a shift if a shift is found in the DB +// -it will delete a shift if a shift is found and no data is received from the frontend + +// This function will be used for the Timesheet Page for an employee to enter, modify or delete and entry +// This function will also be used in the modal of the timesheet validation page to +// allow a supervisor to enter, modify or delete and entry of a selected employee +// _____________________________________________________________________________________________*/ + +// async findTimesheetsByEmailAndPayPeriod(email: string, year: number, period_no: number, timesheets: Timesheets[]): Promise { +// const employee_id = await this.emailResolver.findIdByEmail(email); + + +// return timesheets; +// } + +// async upsertOrDeleteShiftsByEmailAndDate(email:string, shift_ids: Shift[]) {} + +// async upsertOrDeleteExpensesByEmailAndDate(email:string, expenses_id: Expense[]) {} + + + + + +// //_____________________________________________________________________________________________ +// // +// //_____________________________________________________________________________________________ + +// async createWeekShiftsAndReturnOverview( +// email:string, +// shifts: CreateTimesheetDto[], +// week_offset = 0, +// ): Promise { +// //fetchs employee matchint user's email +// const employee_id = await this.emailResolver.findIdByEmail(email); +// if(!employee_id) throw new NotFoundException(`employee for ${ email } not found`); + +// //insure that the week starts on sunday and finishes on saturday +// const base = new Date(); +// base.setDate(base.getDate() + week_offset * 7); +// const start_week = getWeekStart(base, 0); +// const end_week = getWeekEnd(start_week); + +// const timesheet = await this.timesheetResolver.findTimesheetIdByEmail(email, base) +// if(!timesheet) throw new NotFoundException(`no timesheet found for employe ${employee_id}`); + +// //validations and insertions +// for(const shift of shifts) { +// const date = parseISODate(shift.date); +// if (date < start_week || date > end_week) throw new BadRequestException(`date ${shift.date} not in current week`); + +// const bank_code = await this.bankTypeResolver.findByType(shift.type) +// if(!bank_code) throw new BadRequestException(`Invalid bank_code type: ${shift.type}`); + +// await this.prisma.shifts.create({ +// data: { +// timesheet_id: timesheet.id, +// bank_code_id: bank_code.id, +// date: date, +// start_time: parseHHmm(shift.start_time), +// end_time: parseHHmm(shift.end_time), +// comment: shift.comment ?? null, +// is_approved: false, +// is_remote: false, +// }, +// }); +// } +// return this.query.getTimesheetByEmail(email, week_offset); +// } +// } \ No newline at end of file diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheets-query.service.ts b/src/modules/timesheets/~misc_deprecated-files/timesheets-query.service.ts new file mode 100644 index 0000000..7999702 --- /dev/null +++ b/src/modules/timesheets/~misc_deprecated-files/timesheets-query.service.ts @@ -0,0 +1,54 @@ +// import { makeEmptyTimesheet, mapExpenseRow, mapShiftRow } from './utils-helpers-others/timesheet.mappers'; +// import { buildPeriod, computeWeekRange } from './utils-helpers-others/timesheet.utils'; +// import { TimesheetSelectorsService } from './utils-helpers-others/timesheet.selectors'; +// import { TimesheetPeriodDto } from './timesheet-period.dto'; +// import { toRangeFromPeriod } from './utils-helpers-others/timesheet.helpers'; +// import { EmailToIdResolver } from 'src/modules/shared/utils/resolve-email-id.utils'; +// import { FullNameResolver } from 'src/modules/shared/utils/resolve-full-name.utils'; +// import { PrismaService } from 'src/prisma/prisma.service'; +// import { TimesheetMap } from './utils-helpers-others/timesheet.types'; +// import { Injectable } from '@nestjs/common'; + + +// @Injectable() +// export class TimesheetsQueryService { +// constructor( +// private readonly prisma: PrismaService, +// private readonly emailResolver: EmailToIdResolver, +// private readonly fullNameResolver: FullNameResolver, +// private readonly selectors: TimesheetSelectorsService, +// ) {} + +// async findAll(year: number, period_no: number, email: string): Promise { +// const employee_id = await this.emailResolver.findIdByEmail(email); //finds the employee using email +// const full_name = await this.fullNameResolver.resolveFullName(employee_id); //finds the employee full name using employee_id +// const period = await this.selectors.getPayPeriod(year, period_no);//finds the pay period using year and period_no +// const{ from, to } = toRangeFromPeriod(period); //finds start and end dates +// //finds all shifts from selected period +// const [raw_shifts, raw_expenses] = await Promise.all([ +// this.selectors.getShifts(employee_id, from, to), +// this.selectors.getExpenses(employee_id, from, to), +// ]); +// // data mapping +// const shifts = raw_shifts.map(mapShiftRow); +// const expenses = raw_expenses.map(mapExpenseRow); + +// return buildPeriod(period.period_start, period.period_end, shifts , expenses, full_name); +// } + + + +// async getTimesheetByEmail(email: string, week_offset = 0): Promise { +// const employee_id = await this.emailResolver.findIdByEmail(email); //finds the employee using email +// const { start, start_day, end_day, label } = computeWeekRange(week_offset); +// const timesheet = await this.selectors.getTimesheetWithShiftsAndExpenses(employee_id, start); //fetch timesheet shifts and expenses +// if(!timesheet) return makeEmptyTimesheet({ start_day, end_day, label}); + +// //maps all shifts of selected timesheet +// const shifts = timesheet.shift.map(mapShiftRow); +// const expenses = timesheet.expense.map(mapExpenseRow); + + +// return { start_day, end_day, label, shifts, expenses, is_approved: timesheet.is_approved}; +// } +// } diff --git a/src/modules/timesheets/~misc_deprecated-files/timesheets.controller.ts b/src/modules/timesheets/~misc_deprecated-files/timesheets.controller.ts new file mode 100644 index 0000000..8ba1dee --- /dev/null +++ b/src/modules/timesheets/~misc_deprecated-files/timesheets.controller.ts @@ -0,0 +1,51 @@ +// import { BadRequestException, Body, Controller, Get, Param, ParseIntPipe, Post, Query } from '@nestjs/common'; +// import { TimesheetsQueryService } from './timesheets-query.service'; +// import { CreateWeekShiftsDto } from './create-timesheet.dto'; +// import { RolesAllowed } from "src/common/decorators/roles.decorators"; +// import { Roles as RoleEnum } from '.prisma/client'; +// import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +// import { TimesheetsCommandService } from './timesheets-command.service'; +// import { TimesheetPeriodDto } from './timesheet-period.dto'; +// import { TimesheetMap } from './timesheet.types'; + + +// @ApiTags('Timesheets') +// @ApiBearerAuth('access-token') +// // @UseGuards() +// @Controller('timesheets') +// export class TimesheetsController { +// constructor( +// private readonly timesheetsQuery: TimesheetsQueryService, +// private readonly timesheetsCommand: TimesheetsCommandService, +// ) {} + +// @Get() +// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) +// async getPeriodByQuery( +// @Query('year', ParseIntPipe ) year: number, +// @Query('period_no', ParseIntPipe ) period_no: number, +// @Query('email') email?: string +// ): Promise { +// if(!email || !(email = email.trim())) throw new BadRequestException('Query param "email" is mandatory for this route.'); +// return this.timesheetsQuery.findAll(year, period_no, email); +// } + +// @Get('/:email') +// async getByEmail( +// @Param('email') email: string, +// @Query('offset') offset?: string, +// ): Promise { +// const week_offset = Number.isFinite(Number(offset)) ? Number(offset) : 0; +// return this.timesheetsQuery.getTimesheetByEmail(email, week_offset); +// } + +// @Post('shifts/:email') +// async createTimesheetShifts( +// @Param('email') email: string, +// @Body() dto: CreateWeekShiftsDto, +// @Query('offset') offset?: string, +// ): Promise { +// const week_offset = Number.isFinite(Number(offset)) ? Number(offset) : 0; +// return this.timesheetsCommand.createWeekShiftsAndReturnOverview(email, dto.shifts, week_offset); +// } +// }