diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index e9c9b27..4e6ef5a 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -91,30 +91,20 @@ "parameters": [ { "name": "date", - "required": false, + "required": true, "in": "query", - "description": "Override for resolving the current period", "schema": { - "example": "2025-08-11", "type": "string" } } ], "responses": { "200": { - "description": "Find current and all pay periods", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayPeriodBundleDto" - } - } - } + "description": "" } }, - "summary": "Return current pay period and the full list", "tags": [ - "pay-periods" + "PayPeriods" ] } }, @@ -133,22 +123,11 @@ ], "responses": { "200": { - "description": "Pay period found for the selected date", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayPeriodDto" - } - } - } - }, - "404": { - "description": "Pay period not found for the selected date" + "description": "" } }, - "summary": "Resolve a period by a date within it", "tags": [ - "pay-periods" + "PayPeriods" ] } }, @@ -161,7 +140,6 @@ "required": true, "in": "path", "schema": { - "example": 2024, "type": "number" } }, @@ -169,35 +147,46 @@ "name": "periodNumber", "required": true, "in": "path", - "description": "1..26", "schema": { - "example": 1, "type": "number" } } ], "responses": { "200": { - "description": "Pay period found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayPeriodDto" - } - } - } - }, - "404": { - "description": "Pay period not found" + "description": "" } }, - "summary": "Find pay period by year and period number", "tags": [ - "pay-periods" + "PayPeriods" ] } }, - "/pay-periods/{year}/{periodNumber}/{email}": { + "/pay-periods/crew/pay-period-approval": { + "patch": { + "operationId": "PayPeriodsController_bulkApproval", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkCrewApprovalDto" + } + } + } + }, + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "PayPeriods" + ] + } + }, + "/pay-periods/crew/{year}/{periodNumber}": { "get": { "operationId": "PayPeriodsController_getCrewOverview", "parameters": [ @@ -206,7 +195,6 @@ "required": true, "in": "path", "schema": { - "example": 2024, "type": "number" } }, @@ -214,49 +202,18 @@ "name": "periodNumber", "required": true, "in": "path", - "description": "1..26", "schema": { - "example": 1, "type": "number" } - }, - { - "name": "email", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "includeSubtree", - "required": false, - "in": "query", - "description": "Include indirect reports", - "schema": { - "example": false, - "type": "boolean" - } } ], "responses": { "200": { - "description": "Crew overview", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayPeriodOverviewDto" - } - } - } - }, - "404": { - "description": "Pay period not found" + "description": "" } }, - "summary": "Supervisor crew overview for a given pay period", "tags": [ - "pay-periods" + "PayPeriods" ] } }, @@ -269,7 +226,6 @@ "required": true, "in": "path", "schema": { - "example": 2024, "type": "number" } }, @@ -277,31 +233,49 @@ "name": "periodNumber", "required": true, "in": "path", - "description": "1..26", "schema": { - "example": 1, "type": "number" } } ], "responses": { "200": { - "description": "Pay period overview found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PayPeriodOverviewDto" - } - } - } - }, - "404": { - "description": "Pay period not found" + "description": "" } }, - "summary": "Detailed view of a pay period by year + number", "tags": [ - "pay-periods" + "PayPeriods" + ] + } + }, + "/timesheets": { + "get": { + "operationId": "TimesheetController_getTimesheetByIds", + "parameters": [ + { + "name": "year", + "required": true, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "period_number", + "required": true, + "in": "query", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Timesheet" ] } }, @@ -338,45 +312,6 @@ ] } }, - "/timesheets": { - "get": { - "operationId": "TimesheetController_getTimesheetByIds", - "parameters": [ - { - "name": "employee_email", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "year", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "period_number", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "Timesheet" - ] - } - }, "/shift/create": { "post": { "operationId": "ShiftController_createBatch", @@ -454,19 +389,10 @@ ] } }, - "/schedule-presets/create/{employee_id}": { + "/schedule-presets/create": { "post": { "operationId": "SchedulePresetsController_createPreset", - "parameters": [ - { - "name": "employee_id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], + "parameters": [], "requestBody": { "required": true, "content": { @@ -543,19 +469,10 @@ ] } }, - "/schedule-presets/find/{employee_id}": { + "/schedule-presets/find-list": { "get": { "operationId": "SchedulePresetsController_findListById", - "parameters": [ - { - "name": "employee_id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], + "parameters": [], "responses": { "200": { "description": "" @@ -566,18 +483,10 @@ ] } }, - "/schedule-presets/apply-presets/{employee_id}": { + "/schedule-presets/apply-presets": { "post": { "operationId": "SchedulePresetsController_applyPresets", "parameters": [ - { - "name": "employee_id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - }, { "name": "preset", "required": true, @@ -605,19 +514,10 @@ ] } }, - "/expense/{timesheet_id}": { + "/expense/create": { "post": { "operationId": "ExpenseController_create", - "parameters": [ - { - "name": "timesheet_id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], + "parameters": [], "requestBody": { "required": true, "content": { @@ -638,7 +538,7 @@ ] } }, - "/expense": { + "/expense/update": { "patch": { "operationId": "ExpenseController_update", "parameters": [], @@ -652,7 +552,7 @@ ] } }, - "/expense/{expense_id}": { + "/expense/delete/{expense_id}": { "delete": { "operationId": "ExpenseController_remove", "parameters": [ @@ -733,167 +633,9 @@ } }, "schemas": { - "PayPeriodDto": { + "BulkCrewApprovalDto": { "type": "object", - "properties": { - "pay_period_no": { - "type": "number", - "example": 1, - "description": "numéro cyclique de la période entre 1 et 26" - }, - "period_start": { - "type": "string", - "example": "2023-12-17", - "format": "date" - }, - "period_end": { - "type": "string", - "example": "2023-12-30", - "format": "date" - }, - "payday": { - "type": "string", - "example": "2023-01-04", - "format": "date" - }, - "pay_year": { - "type": "number", - "example": 2023 - }, - "label": { - "type": "string", - "example": "2023-12-17 → 2023-12-30" - } - }, - "required": [ - "pay_period_no", - "period_start", - "period_end", - "payday", - "pay_year", - "label" - ] - }, - "PayPeriodBundleDto": { - "type": "object", - "properties": { - "current": { - "description": "Current pay period (resolved from date)", - "allOf": [ - { - "$ref": "#/components/schemas/PayPeriodDto" - } - ] - }, - "periods": { - "description": "All pay periods", - "type": "array", - "items": { - "$ref": "#/components/schemas/PayPeriodDto" - } - } - }, - "required": [ - "current", - "periods" - ] - }, - "EmployeePeriodOverviewDto": { - "type": "object", - "properties": { - "employee_name": { - "type": "string", - "example": "Alex Dupont", - "description": "Nom complet de lemployé" - }, - "regular_hours": { - "type": "number", - "example": 40, - "description": "pay-period`s regular hours" - }, - "other_hours": { - "type": "object", - "example": 0, - "description": "pay-period`s other hours" - }, - "expenses": { - "type": "number", - "example": 420.69, - "description": "pay-period`s total expenses ($)" - }, - "mileage": { - "type": "number", - "example": 40, - "description": "pay-period total mileages (km)" - }, - "is_approved": { - "type": "boolean", - "example": true, - "description": "Tous les timesheets de la période sont approuvés pour cet employé" - } - }, - "required": [ - "employee_name", - "regular_hours", - "other_hours", - "expenses", - "mileage", - "is_approved" - ] - }, - "PayPeriodOverviewDto": { - "type": "object", - "properties": { - "pay_period_no": { - "type": "number", - "example": 1, - "description": "Period number (1–26)" - }, - "pay_year": { - "type": "number", - "example": 2023, - "description": "Calendar year of the period" - }, - "period_start": { - "type": "string", - "example": "2023-12-17", - "format": "date", - "description": "Period start date (YYYY-MM-DD)" - }, - "period_end": { - "type": "string", - "example": "2023-12-30", - "format": "date", - "description": "Period end date (YYYY-MM-DD)" - }, - "payday": { - "type": "string", - "example": "2023-12-30", - "format": "date", - "description": "Period pay day(YYYY-MM-DD)" - }, - "label": { - "type": "string", - "example": "2023-12-17 → 2023-12-30", - "description": "Human-readable label" - }, - "employees_overview": { - "description": "Per-employee overview for the period", - "type": "array", - "items": { - "$ref": "#/components/schemas/EmployeePeriodOverviewDto" - } - } - }, - "required": [ - "pay_period_no", - "pay_year", - "period_start", - "period_end", - "payday", - "label", - "employees_overview" - ] + "properties": {} }, "PreferencesDto": { "type": "object", diff --git a/package-lock.json b/package-lock.json index b3363fc..9b49166 100644 --- a/package-lock.json +++ b/package-lock.json @@ -243,6 +243,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -3113,6 +3114,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3271,6 +3273,7 @@ "version": "11.1.7", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.7.tgz", "integrity": "sha512-lwlObwGgIlpXSXYOTpfzdCepUyWomz6bv9qzGzzvpgspUxkj0Uz0fUJcvD44V8Ps7QhKW3lZBoYbXrH25UZrbA==", + "peer": true, "dependencies": { "file-type": "21.0.0", "iterare": "1.2.1", @@ -3316,6 +3319,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.7.tgz", "integrity": "sha512-TyXFOwjhHv/goSgJ8i20K78jwTM0iSpk9GBcC2h3mf4MxNy+znI8m7nWjfoACjTkb89cTwDQetfTHtSfGLLaiA==", "hasInstallScript": true, + "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -3395,6 +3399,7 @@ "version": "11.1.7", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.7.tgz", "integrity": "sha512-5T+GLdvTiGPKB4/P4PM9ftKUKNHJy8ThEFhZA3vQnXVL7Vf0rDr07TfVTySVu+XTh85m1lpFVuyFM6u6wLNsRA==", + "peer": true, "dependencies": { "cors": "2.8.5", "express": "5.1.0", @@ -3794,6 +3799,7 @@ "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.6.0.tgz", "integrity": "sha512-Q5FsI3Cw0fGMXhmsg7c08i4EmXCrcl+WnAxb6LYOLHw4JFFC3yzmx9LaXZ7QMbA+JZXbigU2TirI7RAfO0Qlnw==", "dev": true, + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@xhmikosr/bin-wrapper": "^13.0.5", @@ -3862,6 +3868,7 @@ "integrity": "sha512-CJSn2vstd17ddWIHBsjuD4OQnn9krQfaq6EO+w9YfId5DKznyPmzxAARlOXG99cC8/3Kli8ysKy6phL43bSr0w==", "dev": true, "hasInstallScript": true, + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.23" @@ -4198,6 +4205,7 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -4357,6 +4365,7 @@ "version": "22.17.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.2.tgz", "integrity": "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4537,6 +4546,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz", "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", @@ -5440,6 +5450,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5452,7 +5463,6 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, - "peer": true, "engines": { "node": ">=10.13.0" }, @@ -5486,6 +5496,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5953,6 +5964,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -6231,6 +6243,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "devOptional": true, + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -6283,12 +6296,14 @@ "node_modules/class-transformer": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", - "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "peer": true }, "node_modules/class-validator": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "peer": true, "dependencies": { "@types/validator": "^13.11.8", "libphonenumber-js": "^1.11.1", @@ -7151,6 +7166,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -7211,6 +7227,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", "dev": true, + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8717,6 +8734,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -10353,6 +10371,7 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -10646,6 +10665,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10700,6 +10720,7 @@ "integrity": "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==", "devOptional": true, "hasInstallScript": true, + "peer": true, "dependencies": { "@prisma/config": "6.18.0", "@prisma/engines": "6.18.0" @@ -10919,7 +10940,8 @@ "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "peer": true }, "node_modules/repeat-string": { "version": "1.6.1", @@ -11108,6 +11130,7 @@ "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -11917,6 +11940,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -12225,6 +12249,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12382,6 +12407,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12564,9 +12590,10 @@ } }, "node_modules/validator": { - "version": "13.15.15", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", - "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "version": "13.15.20", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", + "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -12711,7 +12738,6 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -12729,7 +12755,6 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -12742,7 +12767,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -12756,7 +12780,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, - "peer": true, "engines": { "node": ">=4.0" } @@ -12765,15 +12788,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "peer": true + "dev": true }, "node_modules/webpack/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "peer": true, "engines": { "node": ">= 0.6" } @@ -12783,7 +12804,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -12796,7 +12816,6 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/src/app.module.ts b/src/app.module.ts index ae2e569..dbe9dd9 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,8 +14,8 @@ import { ConfigModule } from '@nestjs/config'; import { APP_FILTER, APP_PIPE } from '@nestjs/core'; import { HttpExceptionFilter } from './common/filters/http-exception.filter'; import { ValidationError } from 'class-validator'; -import { PayperiodsModule } from './time-and-attendance/modules/pay-period/pay-periods.module'; import { TimeAndAttendanceModule } from 'src/time-and-attendance/time-and-attendance.module'; +import { PayperiodsModule } from 'src/time-and-attendance/pay-period/pay-periods.module'; @Module({ imports: [ diff --git a/src/modules/exports/csv-exports.module.ts b/src/modules/exports/csv-exports.module.ts index fa0c7f1..92a5a96 100644 --- a/src/modules/exports/csv-exports.module.ts +++ b/src/modules/exports/csv-exports.module.ts @@ -1,10 +1,9 @@ import { Module } from "@nestjs/common"; import { CsvExportController } from "./controllers/csv-exports.controller"; import { CsvExportService } from "./services/csv-exports.service"; -import { SharedModule } from "../../time-and-attendance/modules/shared/shared.module"; @Module({ - providers:[CsvExportService, SharedModule], + providers:[CsvExportService], controllers: [CsvExportController], }) export class CsvExportModule {} diff --git a/src/time-and-attendance/domains/business-logics.module.ts b/src/time-and-attendance/domains/business-logics.module.ts index 91774ec..4dc801e 100644 --- a/src/time-and-attendance/domains/business-logics.module.ts +++ b/src/time-and-attendance/domains/business-logics.module.ts @@ -4,15 +4,18 @@ import { VacationService } from "./services/vacation.service"; import { HolidayService } from "./services/holiday.service"; import { MileageService } from "./services/mileage.service"; import { Module } from "@nestjs/common"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; @Module({ + imports:[], providers: [ HolidayService, MileageService, OvertimeService, SickLeaveService, - VacationService + VacationService, + EmailToIdResolver, ], exports: [ HolidayService, diff --git a/src/time-and-attendance/domains/services/holiday.service.ts b/src/time-and-attendance/domains/services/holiday.service.ts index 15bf4d2..5a0c2b7 100644 --- a/src/time-and-attendance/domains/services/holiday.service.ts +++ b/src/time-and-attendance/domains/services/holiday.service.ts @@ -1,9 +1,8 @@ import { Injectable, Logger, NotFoundException } from "@nestjs/common"; import { computeHours, getWeekStart } from "src/common/utils/date-utils"; -import { PrismaService } from "../../../prisma/prisma.service"; - -const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000; - +import { PrismaService } from "src/prisma/prisma.service"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { MS_PER_WEEK } from "src/time-and-attendance/utils/constants.utils"; /* le calcul est 1/20 des 4 dernières semaines, précédent la semaine incluant le férier. Un maximum de 08h00 est allouable pour le férier @@ -15,28 +14,19 @@ const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000; export class HolidayService { private readonly logger = new Logger(HolidayService.name); - constructor(private readonly prisma: PrismaService) {} - - //fetch employee_id by email - private async resolveEmployeeByEmail(email: string): Promise { - const employee = await this.prisma.employees.findFirst({ - where: { - user: { email } - }, - select: { id: true }, - }); - if(!employee) throw new NotFoundException(`Employee with email : ${email} not found`); - return employee.id; - } + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver: EmailToIdResolver, + ) {} private async computeHoursPrevious4WeeksByEmail(email: string, holiday_date: Date): Promise { - const employee_id = await this.resolveEmployeeByEmail(email); + const employee_id = await this.emailResolver.findIdByEmail(email); return this.computeHoursPrevious4Weeks(employee_id, holiday_date); } private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise { const holiday_week_start = getWeekStart(holiday_date); - const window_start = new Date(holiday_week_start.getTime() - 4 * WEEK_IN_MS); + const window_start = new Date(holiday_week_start.getTime() - 4 * MS_PER_WEEK); const window_end = new Date(holiday_week_start.getTime() - 1); const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G700']; @@ -60,7 +50,7 @@ export class HolidayService { let capped_total = 0; for(let offset = 1; offset <= 4; offset++) { - const week_start = new Date(holiday_week_start.getTime() - offset * WEEK_IN_MS); + const week_start = new Date(holiday_week_start.getTime() - offset * MS_PER_WEEK); const key = week_start.getTime(); const weekly_hours = hours_by_week.get(key) ?? 0; capped_total += Math.min(weekly_hours, 40); diff --git a/src/time-and-attendance/domains/services/mileage.service.ts b/src/time-and-attendance/domains/services/mileage.service.ts index 030ce42..2b589a9 100644 --- a/src/time-and-attendance/domains/services/mileage.service.ts +++ b/src/time-and-attendance/domains/services/mileage.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common"; -import { PrismaService } from '../../../prisma/prisma.service'; +import { PrismaService } from "src/prisma/prisma.service"; @Injectable() export class MileageService { diff --git a/src/time-and-attendance/domains/services/overtime.service.ts b/src/time-and-attendance/domains/services/overtime.service.ts index be8c15c..e43c4d3 100644 --- a/src/time-and-attendance/domains/services/overtime.service.ts +++ b/src/time-and-attendance/domains/services/overtime.service.ts @@ -1,32 +1,15 @@ import { Injectable, Logger } from '@nestjs/common'; -import { PrismaService } from '../../../prisma/prisma.service'; import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils'; -import { Prisma, PrismaClient } from '@prisma/client'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { DAILY_LIMIT_HOURS, WEEKLY_LIMIT_HOURS } from 'src/time-and-attendance/utils/constants.utils'; +import { Tx, WeekOvertimeSummary } from 'src/time-and-attendance/utils/type.utils'; -type Tx = Prisma.TransactionClient | PrismaClient; - -export type WeekOvertimeSummary = { - week_start:string; - week_end: string; - week_total_hours: number; - weekly_overtime: number; - daily_overtime_kept: number; - total_overtime: number; - breakdown: Array<{ - date:string; - day_hours: number; - day_overtime: number; - daily_kept: number; - running_total_before: number; - }>; -}; @Injectable() export class OvertimeService { private logger = new Logger(OvertimeService.name); - private daily_max = 8; // maximum for regular hours per day - private weekly_max = 40; // maximum for regular hours per week + private INCLUDED_TYPES = ['EMERGENCY', 'EVENING','OVERTIME','REGULAR'] as const; // included types for weekly overtime calculation constructor(private prisma: PrismaService) {} @@ -61,7 +44,7 @@ export class OvertimeService { } 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); + const weekly_overtime = Math.max(0, week_total_hours - WEEKLY_LIMIT_HOURS); let running = 0; let daily_kept_sum = 0; @@ -69,9 +52,9 @@ export class OvertimeService { 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 day_overtime = Math.max(0, day_hours - DAILY_LIMIT_HOURS); - const cap_before_40 = Math.max(0, this.weekly_max - running); + const cap_before_40 = Math.max(0, WEEKLY_LIMIT_HOURS - running); const daily_kept = Math.min(day_overtime, cap_before_40); breakdown.push({ @@ -104,144 +87,4 @@ export class OvertimeService { breakdown, }; } - - // //calculate daily overtime - // async getDailyOvertimeHours(timesheet_id: number, date: Date): Promise { - // const shifts = await this.prisma.shifts.findMany({ - // where: { - // timesheet_id, - // date: date, - // bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } }, - // }, - // select: { start_time: true, end_time: true }, - // orderBy: [{ start_time: 'asc' }], - // }); - - // const total = shifts.map((shift)=> - // computeHours(shift.start_time, shift.end_time, 5)). - // reduce((sum, hours)=> sum + hours, 0); - - // const overtime = Math.max(0, total - this.daily_max); - - // this.logger.debug(`[OVERTIME]-[DAILY] total=${total.toFixed(2)}h, overtime= ${overtime.toFixed(2)}h`); - // return overtime; - // } - - // //calculate Weekly overtime - // async getWeeklyOvertimeHours(timesheet_id: number, ref_date: Date): Promise { - // const week_start = getWeekStart(ref_date); - // const week_end = getWeekEnd(week_start); - - // //fetches all shifts from INCLUDED_TYPES array - // const included_shifts = await this.prisma.shifts.findMany({ - // where: { - // timesheet_id, - // date: { gte:week_start, lte: week_end }, - // bank_code: { type: { in: this.INCLUDED_TYPES as unknown as string[] } }, - // }, - // select: { start_time: true, end_time: true }, - // orderBy: [{date: 'asc'}, {start_time:'asc'}], - // }); - - // //calculate total hours of those shifts minus weekly Max to find total overtime hours - // const total = included_shifts.map(shift => - // computeHours(shift.start_time, shift.end_time, 5)). - // reduce((sum, hours)=> sum+hours, 0); - - // const overtime = Math.max(0, total - this.weekly_max); - - // this.logger.debug(`[OVERTIME]-[WEEKLY] total=${total.toFixed(2)}h, overtime= ${overtime.toFixed(2)}h`); - // return overtime; - // } - - - // //transform REGULAR shifts to OVERTIME when exceed 40hrs of included_types of shift - // async transformRegularHoursToWeeklyOvertime( - // employee_id: number, - // ref_date: Date, - // tx?: Prisma.TransactionClient, - // ): Promise { - // //ensures the use of the transaction if needed. fallback to this.prisma if no transaction is detected. - // const db = tx ?? this.prisma; - - // //calculate weekly overtime - // const overtime_hours = await this.getWeeklyOvertimeHours(employee_id, ref_date); - // if(overtime_hours <= 0) return; - - // const convert_to_minutes = Math.round(overtime_hours * 60); - - // const [regular, overtime] = await Promise.all([ - // db.bankCodes.findFirst({where: { type: 'REGULAR' }, select: { id: true } }), - // db.bankCodes.findFirst({where: { type: 'OVERTIME'}, select: { id: true } }), - // ]); - // if(!regular || !overtime) return; - - // const week_start = getWeekStart(ref_date); - // const week_end = getWeekEnd(week_start); - - // //gets all regular shifts and order them by desc - // const regular_shifts_desc = await db.shifts.findMany({ - // where: { - // date: { gte:week_start, lte: week_end }, - // timesheet: { employee_id }, - // bank_code_id: regular.id, - // }, - // select: { - // id: true, - // timesheet_id: true, - // date: true, - // start_time: true, - // end_time: true, - // is_remote: true, - // comment: true, - // }, - // orderBy: [{date: 'desc'}, {start_time:'desc'}], - // }); - - // let remaining_minutes = convert_to_minutes; - - // for(const shift of regular_shifts_desc) { - // if(remaining_minutes <= 0) break; - - // const start = shift.start_time; - // const end = shift.end_time; - // const duration_in_minutes = Math.max(0, Math.round((end.getTime() - start.getTime())/60000)); - // if(duration_in_minutes === 0) continue; - - // if(duration_in_minutes <= remaining_minutes) { - // await db.shifts.update({ - // where: { id: shift.id }, - // data: { bank_code_id: overtime.id }, - // }); - // remaining_minutes -= duration_in_minutes; - // continue; - // } - // //sets the start_time of the new overtime shift - // const new_overtime_start = new Date(end.getTime() - remaining_minutes * 60000); - - // //shorten the regular shift - // await db.shifts.update({ - // where: { id: shift.id }, - // data: { end_time: new_overtime_start }, - // }); - - // //creates the new overtime shift to replace the shorten regular shift - // await db.shifts.create({ - // data: { - // timesheet_id: shift.timesheet_id, - // date: shift.date, - // start_time: new_overtime_start, - // end_time: end, - // is_remote: shift.is_remote, - // comment: shift.comment, - // bank_code_id: overtime.id, - // }, - // }); - // remaining_minutes = 0; - // } - // this.logger.debug(`[OVERTIME]-[WEEKLY]-[TRANSFORM] emp=${employee_id} - // week: ${week_start.toISOString().slice(0,10)}..${week_end.toISOString().slice(0,10)} - // converted= ${(convert_to_minutes-remaining_minutes)/60}h`); - // } - } diff --git a/src/time-and-attendance/domains/services/sick-leave.service.ts b/src/time-and-attendance/domains/services/sick-leave.service.ts index 6c00113..60a847e 100644 --- a/src/time-and-attendance/domains/services/sick-leave.service.ts +++ b/src/time-and-attendance/domains/services/sick-leave.service.ts @@ -1,6 +1,6 @@ import { getYearStart, roundToQuarterHour } from "src/common/utils/date-utils"; import { Injectable, Logger } from "@nestjs/common"; -import { PrismaService } from "../../../prisma/prisma.service"; +import { PrismaService } from "src/prisma/prisma.service"; @Injectable() export class SickLeaveService { diff --git a/src/time-and-attendance/domains/services/vacation.service.ts b/src/time-and-attendance/domains/services/vacation.service.ts index 9445149..2e3a214 100644 --- a/src/time-and-attendance/domains/services/vacation.service.ts +++ b/src/time-and-attendance/domains/services/vacation.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger, NotFoundException } from "@nestjs/common"; -import { PrismaService } from "../../../prisma/prisma.service"; +import { PrismaService } from "src/prisma/prisma.service"; @Injectable() export class VacationService { diff --git a/src/time-and-attendance/expenses/controllers/expense.controller.ts b/src/time-and-attendance/expenses/controllers/expense.controller.ts new file mode 100644 index 0000000..fb81eb2 --- /dev/null +++ b/src/time-and-attendance/expenses/controllers/expense.controller.ts @@ -0,0 +1,34 @@ +import { Controller, Post, Param, Body, Patch, Delete, Req, UnauthorizedException } from "@nestjs/common"; +import { CreateExpenseResult, UpdateExpenseResult } from "src/time-and-attendance/utils/type.utils"; +import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service"; +import { updateExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-update.dto"; +import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { Roles as RoleEnum } from '.prisma/client'; + +@Controller('expense') +export class ExpenseController { + constructor( private readonly upsert_service: ExpenseUpsertService ){} + + @Post('create') + @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + create( @Req() req, @Body() dto: ExpenseDto): Promise{ + const email = req.user?.email; + if(!email) throw new UnauthorizedException('Unauthorized User'); + return this.upsert_service.createExpense(dto, email); + } + + @Patch('update') + @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + update(@Body() body: { update :{ id: number; dto: updateExpenseDto }}): Promise{ + return this.upsert_service.updateExpense(body.update); + } + + @Delete('delete/:expense_id') + @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + remove(@Param('expense_id') expense_id: number) { + return this.upsert_service.deleteExpense(expense_id); + } +} + + diff --git a/src/time-and-attendance/modules/expenses/dtos/expense-create.dto.ts b/src/time-and-attendance/expenses/dtos/expense-create.dto.ts similarity index 100% rename from src/time-and-attendance/modules/expenses/dtos/expense-create.dto.ts rename to src/time-and-attendance/expenses/dtos/expense-create.dto.ts diff --git a/src/time-and-attendance/modules/expenses/dtos/expense-get.dto.ts b/src/time-and-attendance/expenses/dtos/expense-get.dto.ts similarity index 100% rename from src/time-and-attendance/modules/expenses/dtos/expense-get.dto.ts rename to src/time-and-attendance/expenses/dtos/expense-get.dto.ts diff --git a/src/time-and-attendance/modules/expenses/dtos/expense-update.dto.ts b/src/time-and-attendance/expenses/dtos/expense-update.dto.ts similarity index 65% rename from src/time-and-attendance/modules/expenses/dtos/expense-update.dto.ts rename to src/time-and-attendance/expenses/dtos/expense-update.dto.ts index 829f9c9..3dfe9b6 100644 --- a/src/time-and-attendance/modules/expenses/dtos/expense-update.dto.ts +++ b/src/time-and-attendance/expenses/dtos/expense-update.dto.ts @@ -1,5 +1,5 @@ import { OmitType, PartialType } from "@nestjs/swagger"; -import { ExpenseDto } from "src/time-and-attendance/modules/expenses/dtos/expense-create.dto"; +import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; export class updateExpenseDto extends PartialType ( OmitType(ExpenseDto, ['is_approved', 'timesheet_id'] as const) diff --git a/src/time-and-attendance/expenses/expenses.module.ts b/src/time-and-attendance/expenses/expenses.module.ts new file mode 100644 index 0000000..e6d737b --- /dev/null +++ b/src/time-and-attendance/expenses/expenses.module.ts @@ -0,0 +1,10 @@ +import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service"; +import { ExpenseController } from "src/time-and-attendance/expenses/controllers/expense.controller"; +import { Module } from "@nestjs/common"; + +@Module({ + controllers: [ ExpenseController ], + providers: [ ExpenseUpsertService ], +}) + +export class ExpensesModule {} \ No newline at end of file diff --git a/src/time-and-attendance/modules/expenses/services/expense-upsert.service.ts b/src/time-and-attendance/expenses/services/expense-upsert.service.ts similarity index 85% rename from src/time-and-attendance/modules/expenses/services/expense-upsert.service.ts rename to src/time-and-attendance/expenses/services/expense-upsert.service.ts index 64390af..7f16064 100644 --- a/src/time-and-attendance/modules/expenses/services/expense-upsert.service.ts +++ b/src/time-and-attendance/expenses/services/expense-upsert.service.ts @@ -1,34 +1,49 @@ - import { CreateExpenseResult, UpdateExpensePayload, UpdateExpenseResult, DeleteExpenseResult, NormalizedExpense } from "src/time-and-attendance/utils/type.utils"; -import { toDateFromString, toStringFromDate } from "src/time-and-attendance/utils/date-time.utils"; +import { toDateFromString, toStringFromDate, weekStartSunday } from "src/time-and-attendance/utils/date-time.utils"; import { Injectable, NotFoundException } from "@nestjs/common"; import { expense_select } from "src/time-and-attendance/utils/selects.utils"; import { PrismaService } from "src/prisma/prisma.service"; -import { GetExpenseDto } from "src/time-and-attendance/modules/expenses/dtos/expense-get.dto"; -import { ExpenseDto } from "src/time-and-attendance/modules/expenses/dtos/expense-create.dto"; +import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto"; +import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; @Injectable() export class ExpenseUpsertService { - constructor(private readonly prisma: PrismaService) { } + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver: EmailToIdResolver, + ) { } //_________________________________________________________________ // CREATE //_________________________________________________________________ - async createExpense(timesheet_id: number, dto: ExpenseDto): Promise { + async createExpense( dto: ExpenseDto, email: string): Promise { try { + //fetch employee_id using req.user.email + const employee_id = await this.emailResolver.findIdByEmail(email); + //normalize strings and dates const normed_expense = this.normalizeExpenseDto(dto); + //finds the timesheet using expense.date + const start_date = weekStartSunday(normed_expense.date); + //parse numbers const parsed_amount = this.parseOptionalNumber(dto.amount, "amount"); const parsed_mileage = this.parseOptionalNumber(dto.mileage, "mileage"); const parsed_attachment = this.parseOptionalNumber(dto.attachment, "attachment"); + const timesheet = await this.prisma.timesheets.findFirst({ + where: { start_date, employee_id }, + select: { id: true, employee_id: true }, + }); + if(!timesheet) throw new NotFoundException(`Timesheet with id ${dto.timesheet_id} not found`); + //create a new expense const expense = await this.prisma.expenses.create({ data: { - timesheet_id, + timesheet_id: timesheet.id, bank_code_id: dto.bank_code_id, attachment: parsed_attachment, date: normed_expense.date, diff --git a/src/time-and-attendance/modules/expenses/services/expenses-archival.service.ts b/src/time-and-attendance/expenses/services/expenses-archival.service.ts similarity index 100% rename from src/time-and-attendance/modules/expenses/services/expenses-archival.service.ts rename to src/time-and-attendance/expenses/services/expenses-archival.service.ts diff --git a/src/time-and-attendance/modules/leave-requests/controllers/leave-requests.controller.ts b/src/time-and-attendance/leave-requests/controllers/leave-requests.controller.ts similarity index 79% rename from src/time-and-attendance/modules/leave-requests/controllers/leave-requests.controller.ts rename to src/time-and-attendance/leave-requests/controllers/leave-requests.controller.ts index fc934ff..62a14c4 100644 --- a/src/time-and-attendance/modules/leave-requests/controllers/leave-requests.controller.ts +++ b/src/time-and-attendance/leave-requests/controllers/leave-requests.controller.ts @@ -1,8 +1,7 @@ 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 { LeaveRequestsService } from "../services/leave-request.service"; @ApiTags('Leave Requests') @ApiBearerAuth('access-token') @@ -15,16 +14,7 @@ export class LeaveRequestController { async upsertLeaveRequest(@Body() dto: UpsertLeaveRequestDto) { const { action, leave_requests } = await this.leave_service.handle(dto); return { action, leave_requests }; - }q - - //TODO: - /* - @Get('archive') - findAllArchived(){...} - - @Get('archive/:id') - findOneArchived(id){...} - */ + } } diff --git a/src/time-and-attendance/modules/leave-requests/dtos/leave-request-view.dto.ts b/src/time-and-attendance/leave-requests/dtos/leave-request-view.dto.ts similarity index 100% rename from src/time-and-attendance/modules/leave-requests/dtos/leave-request-view.dto.ts rename to src/time-and-attendance/leave-requests/dtos/leave-request-view.dto.ts diff --git a/src/time-and-attendance/modules/leave-requests/dtos/upsert-leave-request.dto.ts b/src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto.ts similarity index 100% rename from src/time-and-attendance/modules/leave-requests/dtos/upsert-leave-request.dto.ts rename to src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto.ts diff --git a/src/time-and-attendance/leave-requests/leave-requests.module.ts b/src/time-and-attendance/leave-requests/leave-requests.module.ts new file mode 100644 index 0000000..eff4f6d --- /dev/null +++ b/src/time-and-attendance/leave-requests/leave-requests.module.ts @@ -0,0 +1,16 @@ +import { LeaveRequestController } from "src/time-and-attendance/leave-requests/controllers/leave-requests.controller"; +import { LeaveRequestsService } from "src/time-and-attendance/leave-requests/services/leave-request.service"; +import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module"; +import { ShiftsModule } from "src/time-and-attendance/time-tracker/shifts/shifts.module"; +import { Module } from "@nestjs/common"; + +@Module({ + imports: [ + BusinessLogicsModule, + ShiftsModule, + ], + controllers: [LeaveRequestController], + providers: [LeaveRequestsService], +}) + +export class LeaveRequestsModule {} \ No newline at end of file diff --git a/src/time-and-attendance/modules/leave-requests/mappers/leave-requests-archive.mapper.ts b/src/time-and-attendance/leave-requests/mappers/leave-requests-archive.mapper.ts similarity index 100% rename from src/time-and-attendance/modules/leave-requests/mappers/leave-requests-archive.mapper.ts rename to src/time-and-attendance/leave-requests/mappers/leave-requests-archive.mapper.ts index 36d05fa..6a17723 100644 --- a/src/time-and-attendance/modules/leave-requests/mappers/leave-requests-archive.mapper.ts +++ b/src/time-and-attendance/leave-requests/mappers/leave-requests-archive.mapper.ts @@ -1,6 +1,6 @@ -import { Prisma } from "@prisma/client"; -import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; import { LeaveRequestArchiveRow } from "../utils/leave-requests-archive.select"; +import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; +import { Prisma } from "@prisma/client"; const toNum = (value?: Prisma.Decimal | null) => value ? Number(value) : undefined; diff --git a/src/time-and-attendance/modules/leave-requests/mappers/leave-requests.mapper.ts b/src/time-and-attendance/leave-requests/mappers/leave-requests.mapper.ts similarity index 100% rename from src/time-and-attendance/modules/leave-requests/mappers/leave-requests.mapper.ts rename to src/time-and-attendance/leave-requests/mappers/leave-requests.mapper.ts diff --git a/src/time-and-attendance/modules/leave-requests/services/holiday-leave-requests.service.ts b/src/time-and-attendance/leave-requests/services/holiday-leave-requests.service.ts similarity index 82% rename from src/time-and-attendance/modules/leave-requests/services/holiday-leave-requests.service.ts rename to src/time-and-attendance/leave-requests/services/holiday-leave-requests.service.ts index 148f4bf..ce4807b 100644 --- a/src/time-and-attendance/modules/leave-requests/services/holiday-leave-requests.service.ts +++ b/src/time-and-attendance/leave-requests/services/holiday-leave-requests.service.ts @@ -1,15 +1,16 @@ import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; -import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/modules/leave-requests/dtos/upsert-leave-request.dto"; +import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto"; import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client"; -import { normalizeDates, toDateOnly } from "src/time-and-attendance/modules/shared/helpers/date-time.helpers"; -import { LeaveRequestViewDto } from "src/time-and-attendance/modules/leave-requests/dtos/leave-request-view.dto"; import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; -import { LeaveRequestsUtils } from "src/time-and-attendance/modules/leave-requests/utils/leave-request.util"; -import { BankCodesResolver } from "src/time-and-attendance/modules/shared/utils/resolve-bank-type-id.utils"; -import { EmailToIdResolver } from "src/time-and-attendance/modules/shared/utils/resolve-email-id.utils"; +import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; +import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service"; import { PrismaService } from "src/prisma/prisma.service"; -import { mapRowToView } from "src/time-and-attendance/modules/leave-requests/mappers/leave-requests.mapper"; +import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; +import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils"; + @Injectable() diff --git a/src/time-and-attendance/modules/leave-requests/services/leave-request.service.ts b/src/time-and-attendance/leave-requests/services/leave-request.service.ts similarity index 96% rename from src/time-and-attendance/modules/leave-requests/services/leave-request.service.ts rename to src/time-and-attendance/leave-requests/services/leave-request.service.ts index 0fd0ceb..499c2d8 100644 --- a/src/time-and-attendance/modules/leave-requests/services/leave-request.service.ts +++ b/src/time-and-attendance/leave-requests/services/leave-request.service.ts @@ -1,18 +1,18 @@ 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 { LeaveApprovalStatus, LeaveTypes } from "@prisma/client"; +import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto"; -import { mapRowToView } from "../mappers/leave-requests.mapper"; -import { PrismaService } from "src/prisma/prisma.service"; -import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service"; +import { roundToQuarterHour } from "src/common/utils/date-utils"; +import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service"; import { VacationService } from "src/time-and-attendance/domains/services/vacation.service"; -import { normalizeDates, toDateOnly, toISODateKey } from "src/time-and-attendance/modules/shared/helpers/date-time.helpers"; -import { BankCodesResolver } from "src/time-and-attendance/modules/shared/utils/resolve-bank-type-id.utils"; -import { EmailToIdResolver } from "src/time-and-attendance/modules/shared/utils/resolve-email-id.utils"; -import { LeaveRequestsUtils } from "src/time-and-attendance/modules/leave-requests/utils/leave-request.util"; -import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; +import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service"; +import { PrismaService } from "src/prisma/prisma.service"; +import { mapRowToView } from "../mappers/leave-requests.mapper"; +import { normalizeDates, toDateOnly, toISODateKey } from "src/time-and-attendance/utils/date-time.utils"; @Injectable() export class LeaveRequestsService { constructor( diff --git a/src/time-and-attendance/modules/leave-requests/services/sick-leave-requests.service.ts b/src/time-and-attendance/leave-requests/services/sick-leave-requests.service.ts similarity index 88% rename from src/time-and-attendance/modules/leave-requests/services/sick-leave-requests.service.ts rename to src/time-and-attendance/leave-requests/services/sick-leave-requests.service.ts index 969d8a5..1cff7a8 100644 --- a/src/time-and-attendance/modules/leave-requests/services/sick-leave-requests.service.ts +++ b/src/time-and-attendance/leave-requests/services/sick-leave-requests.service.ts @@ -1,15 +1,16 @@ import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; +import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto"; import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client"; -import { roundToQuarterHour } from "src/common/utils/date-utils"; -import { PrismaService } from "src/prisma/prisma.service"; -import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service"; -import { LeaveRequestViewDto } from "src/time-and-attendance/modules/leave-requests/dtos/leave-request-view.dto"; -import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/modules/leave-requests/dtos/upsert-leave-request.dto"; -import { mapRowToView } from "src/time-and-attendance/modules/leave-requests/mappers/leave-requests.mapper"; -import { normalizeDates, toDateOnly } from "src/time-and-attendance/modules/shared/helpers/date-time.helpers"; -import { BankCodesResolver } from "src/time-and-attendance/modules/shared/utils/resolve-bank-type-id.utils"; -import { EmailToIdResolver } from "src/time-and-attendance/modules/shared/utils/resolve-email-id.utils"; +import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; +import { roundToQuarterHour } from "src/common/utils/date-utils"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service"; +import { PrismaService } from "src/prisma/prisma.service"; +import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; +import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils"; + @Injectable() diff --git a/src/time-and-attendance/modules/leave-requests/services/vacation-leave-requests.service.ts b/src/time-and-attendance/leave-requests/services/vacation-leave-requests.service.ts similarity index 88% rename from src/time-and-attendance/modules/leave-requests/services/vacation-leave-requests.service.ts rename to src/time-and-attendance/leave-requests/services/vacation-leave-requests.service.ts index 9f0afc7..2e71c39 100644 --- a/src/time-and-attendance/modules/leave-requests/services/vacation-leave-requests.service.ts +++ b/src/time-and-attendance/leave-requests/services/vacation-leave-requests.service.ts @@ -1,15 +1,15 @@ import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; +import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto"; import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client"; -import { roundToQuarterHour } from "src/common/utils/date-utils"; -import { PrismaService } from "src/prisma/prisma.service"; -import { VacationService } from "src/time-and-attendance/domains/services/vacation.service"; -import { LeaveRequestViewDto } from "src/time-and-attendance/modules/leave-requests/dtos/leave-request-view.dto"; -import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/modules/leave-requests/dtos/upsert-leave-request.dto"; -import { mapRowToView } from "src/time-and-attendance/modules/leave-requests/mappers/leave-requests.mapper"; -import { normalizeDates, toDateOnly } from "src/time-and-attendance/modules/shared/helpers/date-time.helpers"; -import { BankCodesResolver } from "src/time-and-attendance/modules/shared/utils/resolve-bank-type-id.utils"; -import { EmailToIdResolver } from "src/time-and-attendance/modules/shared/utils/resolve-email-id.utils"; import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; +import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; +import { roundToQuarterHour } from "src/common/utils/date-utils"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { VacationService } from "src/time-and-attendance/domains/services/vacation.service"; +import { PrismaService } from "src/prisma/prisma.service"; +import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; +import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils"; @Injectable() diff --git a/src/time-and-attendance/leave-requests/utils/leave-request.transform.ts b/src/time-and-attendance/leave-requests/utils/leave-request.transform.ts new file mode 100644 index 0000000..5ff49fd --- /dev/null +++ b/src/time-and-attendance/leave-requests/utils/leave-request.transform.ts @@ -0,0 +1,20 @@ +import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto"; +import { mapArchiveRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests-archive.mapper"; +import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper"; +import { LeaveRequestArchiveRow } from "src/time-and-attendance/leave-requests/utils/leave-requests-archive.select"; +import { LeaveRequestRow } from "src/time-and-attendance/utils/type.utils"; + + +/** Active (table leave_requests) : proxy to base mapper */ +export function mapRowToViewWithDays(row: LeaveRequestRow): LeaveRequestViewDto { + return mapRowToView(row); +} + +/** Archive (table leave_requests_archive) : proxy to base mapper */ +export function mapArchiveRowToViewWithDays( + row: LeaveRequestArchiveRow, + email: string, + employee_full_name?: string, +): LeaveRequestViewDto { + return mapArchiveRowToView(row, email, employee_full_name!); +} \ No newline at end of file diff --git a/src/time-and-attendance/modules/leave-requests/utils/leave-request.util.ts b/src/time-and-attendance/leave-requests/utils/leave-request.util.ts similarity index 100% rename from src/time-and-attendance/modules/leave-requests/utils/leave-request.util.ts rename to src/time-and-attendance/leave-requests/utils/leave-request.util.ts diff --git a/src/time-and-attendance/modules/leave-requests/utils/leave-requests-archive.select.ts b/src/time-and-attendance/leave-requests/utils/leave-requests-archive.select.ts similarity index 100% rename from src/time-and-attendance/modules/leave-requests/utils/leave-requests-archive.select.ts rename to src/time-and-attendance/leave-requests/utils/leave-requests-archive.select.ts diff --git a/src/time-and-attendance/modules/expenses/controllers/expense.controller.ts b/src/time-and-attendance/modules/expenses/controllers/expense.controller.ts deleted file mode 100644 index 572972d..0000000 --- a/src/time-and-attendance/modules/expenses/controllers/expense.controller.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Controller, Post, Param, ParseIntPipe, Body, Patch, Delete } from "@nestjs/common"; -import { CreateExpenseResult, UpdateExpenseResult } from "src/time-and-attendance/utils/type.utils"; -import { ExpenseUpsertService } from "src/time-and-attendance/modules/expenses/services/expense-upsert.service"; -import { updateExpenseDto } from "src/time-and-attendance/modules/expenses/dtos/expense-update.dto"; -import { ExpenseDto } from "src/time-and-attendance/modules/expenses/dtos/expense-create.dto"; - - -@Controller('expense') -export class ExpenseController { - constructor( private readonly upsert_service: ExpenseUpsertService ){} - - @Post(':timesheet_id') - create( - @Param('timesheet_id', ParseIntPipe) timesheet_id: number, - @Body() dto: ExpenseDto): Promise{ - return this.upsert_service.createExpense(timesheet_id, dto); - } - - @Patch() - update( - @Body() body: { update :{ id: number; dto: updateExpenseDto }}): Promise{ - return this.upsert_service.updateExpense(body.update); - } - - @Delete(':expense_id') - remove(@Param('expense_id') expense_id: number) { - return this.upsert_service.deleteExpense(expense_id); - } -} \ No newline at end of file diff --git a/src/time-and-attendance/modules/expenses/expenses.module.ts b/src/time-and-attendance/modules/expenses/expenses.module.ts deleted file mode 100644 index 6ffff02..0000000 --- a/src/time-and-attendance/modules/expenses/expenses.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ExpenseUpsertService } from "src/time-and-attendance/modules/expenses/services/expense-upsert.service"; -import { ExpenseController } from "src/time-and-attendance/modules/expenses/controllers/expense.controller"; -import { Module } from "@nestjs/common"; - -@Module({ - controllers: [ ExpenseController ], - providers: [ ExpenseUpsertService ], -}) - -export class ExpensesModule {} \ No newline at end of file diff --git a/src/time-and-attendance/modules/leave-requests/leave-requests.module.ts b/src/time-and-attendance/modules/leave-requests/leave-requests.module.ts deleted file mode 100644 index 21b9e23..0000000 --- a/src/time-and-attendance/modules/leave-requests/leave-requests.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { LeaveRequestController } from "src/time-and-attendance/modules/leave-requests/controllers/leave-requests.controller"; -import { LeaveRequestsService } from "src/time-and-attendance/modules/leave-requests/services/leave-request.service"; -import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module"; -import { ShiftsModule } from "src/time-and-attendance/modules/time-tracker/shifts/shifts.module"; -import { SharedModule } from "src/time-and-attendance/modules/shared/shared.module"; -import { Module } from "@nestjs/common"; - - -@Module({ - imports: [ - BusinessLogicsModule, - ShiftsModule, - SharedModule - ], - controllers: [LeaveRequestController], - providers: [LeaveRequestsService], -}) - -export class LeaveRequestsModule {} \ No newline at end of file diff --git a/src/time-and-attendance/modules/leave-requests/utils/leave-request.transform.ts b/src/time-and-attendance/modules/leave-requests/utils/leave-request.transform.ts deleted file mode 100644 index 042c796..0000000 --- a/src/time-and-attendance/modules/leave-requests/utils/leave-request.transform.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { LeaveRequestArchiveRow } from './leave-requests-archive.select'; -import { LeaveRequestViewDto } from '../dtos/leave-request-view.dto'; -import { mapArchiveRowToView } from '../mappers/leave-requests-archive.mapper'; -import { LeaveRequestRow } from 'src/time-and-attendance/utils/type.utils'; -import { mapRowToView } from '../mappers/leave-requests.mapper'; - -/** Active (table leave_requests) : proxy to base mapper */ -export function mapRowToViewWithDays(row: LeaveRequestRow): LeaveRequestViewDto { - return mapRowToView(row); -} - -/** Archive (table leave_requests_archive) : proxy to base mapper */ -export function mapArchiveRowToViewWithDays( - row: LeaveRequestArchiveRow, - email: string, - employee_full_name?: string, -): LeaveRequestViewDto { - return mapArchiveRowToView(row, email, employee_full_name!); -} \ No newline at end of file diff --git a/src/time-and-attendance/modules/pay-period/controllers/pay-periods.controller.ts b/src/time-and-attendance/modules/pay-period/controllers/pay-periods.controller.ts deleted file mode 100644 index 61026c6..0000000 --- a/src/time-and-attendance/modules/pay-period/controllers/pay-periods.controller.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Body, Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query } from "@nestjs/common"; -import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger"; -import { PayPeriodDto } from "../dtos/pay-period.dto"; -import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto"; -import { PayPeriodsQueryService } from "../services/pay-periods-query.service"; -import { RolesAllowed } from "src/common/decorators/roles.decorators"; -import { Roles as RoleEnum } from '.prisma/client'; -// import { PayPeriodsCommandService } from "../services/pay-periods-command.service"; -import { PayPeriodBundleDto } from "../dtos/bundle-pay-period.dto"; -import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto"; - -@ApiTags('pay-periods') -@Controller('pay-periods') -export class PayPeriodsController { - - constructor( - private readonly queryService: PayPeriodsQueryService, - // private readonly commandService: PayPeriodsCommandService, - ) {} - - @Get('current-and-all') - @ApiOperation({summary: 'Return current pay period and the full list'}) - @ApiQuery({name: 'date', required:false, example: '2025-08-11', description:'Override for resolving the current period'}) - @ApiResponse({status: 200, description:'Find current and all pay periods', type: PayPeriodBundleDto}) - async getCurrentAndAll(@Query('date') date?: string): Promise { - const [current, periods] = await Promise.all([ - this.queryService.findCurrent(date), - this.queryService.findAll(), - ]); - return { current, periods }; - } - - @Get("date/:date") - @ApiOperation({ summary: "Resolve a period by a date within it" }) - @ApiResponse({ status: 200, description: "Pay period found for the selected date", type: PayPeriodDto }) - @ApiNotFoundResponse({ description: "Pay period not found for the selected date" }) - async findByDate(@Param("date") date: string) { - return this.queryService.findByDate(date); - } - - @Get(":year/:periodNumber") - @ApiOperation({ summary: "Find pay period by year and period number" }) - @ApiParam({ name: "year", type: Number, example: 2024 }) - @ApiParam({ name: "periodNumber", type: Number, example: 1, description: "1..26" }) - @ApiResponse({ status: 200, description: "Pay period found", type: PayPeriodDto }) - @ApiNotFoundResponse({ description: "Pay period not found" }) - async findOneByYear( - @Param("year", ParseIntPipe) year: number, - @Param("periodNumber", ParseIntPipe) period_no: number, - ) { - return this.queryService.findOneByYearPeriod(year, period_no); - } - - // @Patch("crew/bulk-approval") - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) - // @ApiOperation({ summary: "Approve all selected timesheets in the period" }) - // @ApiResponse({ status: 200, description: "Pay period approved" }) - // async bulkApproval(@Body() dto: BulkCrewApprovalDto) { - // return this.commandService.bulkApproveCrew(dto); - // } - - @Get(':year/:periodNumber/:email') - //@RolesAllowed(RoleEnum.SUPERVISOR) - @ApiOperation({ summary: 'Supervisor crew overview for a given pay period' }) - @ApiParam({ name: 'year', type: Number, example: 2024 }) - @ApiParam({ name: 'periodNumber', type: Number, example: 1, description: '1..26' }) - @ApiQuery({ name: 'includeSubtree', required: false, type: Boolean, example: false, description: 'Include indirect reports' }) - @ApiResponse({ status: 200, description: 'Crew overview', type: PayPeriodOverviewDto }) - @ApiNotFoundResponse({ description: 'Pay period not found' }) - async getCrewOverview( - @Param('year', ParseIntPipe) year: number, - @Param('periodNumber', ParseIntPipe) period_no: number, - @Param('email') email: string, - @Query('includeSubtree', new ParseBoolPipe({ optional: true })) include_subtree = false, - ): Promise { - return this.queryService.getCrewOverview(year, period_no, email, include_subtree); - } - - @Get('overview/:year/:periodNumber') - @ApiOperation({ summary: 'Detailed view of a pay period by year + number' }) - @ApiParam({ name: 'year', type: Number, example: 2024 }) - @ApiParam({ name: 'periodNumber', type: Number, example: 1, description: '1..26' }) - @ApiResponse({ status: 200, description: 'Pay period overview found', type: PayPeriodOverviewDto }) - @ApiNotFoundResponse({ description: 'Pay period not found' }) - async getOverviewByYear( - @Param('year', ParseIntPipe) year: number, - @Param('periodNumber', ParseIntPipe) period_no: number, - ): Promise { - return this.queryService.getOverviewByYearPeriod(year, period_no); - } - - - //_____________________________________________________________________________________________ - // Deprecated or unused methods - //_____________________________________________________________________________________________ - - // @Get() - // @ApiOperation({ summary: 'Find all pay period' }) - // @ApiResponse({status: 200,description: 'List of pay period found', type: PayPeriodDto, isArray: true }) - // async findAll(): Promise { - // return this.queryService.findAll(); - // } -} diff --git a/src/time-and-attendance/modules/pay-period/dtos/bundle-pay-period.dto.ts b/src/time-and-attendance/modules/pay-period/dtos/bundle-pay-period.dto.ts deleted file mode 100644 index 9c5a61f..0000000 --- a/src/time-and-attendance/modules/pay-period/dtos/bundle-pay-period.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { PayPeriodDto } from "./pay-period.dto"; - -export class PayPeriodBundleDto { - - @ApiProperty({ type: PayPeriodDto, description: 'Current pay period (resolved from date)' }) - current: PayPeriodDto; - - @ApiProperty({ type: [PayPeriodDto], description: 'All pay periods' }) - periods: PayPeriodDto[]; -} \ No newline at end of file diff --git a/src/time-and-attendance/modules/pay-period/dtos/overview-employee-period.dto.ts b/src/time-and-attendance/modules/pay-period/dtos/overview-employee-period.dto.ts deleted file mode 100644 index 1ea6937..0000000 --- a/src/time-and-attendance/modules/pay-period/dtos/overview-employee-period.dto.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class EmployeePeriodOverviewDto { - // @ApiProperty({ - // example: 42, - // description: "Employees.id (clé primaire num.)", - // }) - // @Allow() - // @IsOptional() - // employee_id: number; - - - email: string; - - @ApiProperty({ - example: 'Alex Dupont', - description: 'Nom complet de lemployé', - }) - employee_name: string; - - @ApiProperty({ example: 40, description: 'pay-period`s regular hours' }) - regular_hours: number; - - @ApiProperty({ example: 0, description: 'pay-period`s other hours' }) - other_hours: { - evening_hours: number; - - emergency_hours: number; - - overtime_hours: number; - - sick_hours: number; - - holiday_hours: number; - - vacation_hours: number; - }; - - total_hours: number; - - @ApiProperty({ example: 420.69, description: 'pay-period`s total expenses ($)' }) - expenses: number; - - @ApiProperty({ example: 40, description: 'pay-period total mileages (km)' }) - mileage: number; - - @ApiProperty({ - example: true, - description: 'Tous les timesheets de la période sont approuvés pour cet employé', - }) - is_approved: boolean; - - is_remote: boolean; -} diff --git a/src/time-and-attendance/modules/pay-period/dtos/overview-pay-period.dto.ts b/src/time-and-attendance/modules/pay-period/dtos/overview-pay-period.dto.ts deleted file mode 100644 index 041fba3..0000000 --- a/src/time-and-attendance/modules/pay-period/dtos/overview-pay-period.dto.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { EmployeePeriodOverviewDto } from './overview-employee-period.dto'; - -export class PayPeriodOverviewDto { - @ApiProperty({ example: 1, description: 'Period number (1–26)' }) - pay_period_no: number; - - @ApiProperty({ example: 2023, description: 'Calendar year of the period' }) - pay_year: number; - - @ApiProperty({ - example: '2023-12-17', - type: String, - format: 'date', - description: "Period start date (YYYY-MM-DD)", - }) - period_start: string; - - @ApiProperty({ - example: '2023-12-30', - type: String, - format: 'date', - description: "Period end date (YYYY-MM-DD)", - }) - period_end: string; - - @ApiProperty({ - example: '2023-12-30', - type: String, - format: 'date', - description: "Period pay day(YYYY-MM-DD)", - }) - payday: string; - - @ApiProperty({ - example: '2023-12-17 → 2023-12-30', - description: 'Human-readable label', - }) - label: string; - - @ApiProperty({ - type: [EmployeePeriodOverviewDto], - description: 'Per-employee overview for the period', - }) - employees_overview: EmployeePeriodOverviewDto[]; -} diff --git a/src/time-and-attendance/modules/pay-period/dtos/pay-period.dto.ts b/src/time-and-attendance/modules/pay-period/dtos/pay-period.dto.ts deleted file mode 100644 index 4f7989b..0000000 --- a/src/time-and-attendance/modules/pay-period/dtos/pay-period.dto.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; - -export class PayPeriodDto { - @ApiProperty({ example: 1, - description: 'numéro cyclique de la période entre 1 et 26' }) - pay_period_no: number; - - @ApiProperty({ example: '2023-12-17', - type: String, format: 'date' }) - period_start: string; - - @ApiProperty({ example: '2023-12-30', - type: String, format: 'date' }) - period_end: string; - - @ApiProperty({ example: '2023-01-04', - type: String, format: 'date' }) - payday: string; - - @ApiProperty({ example: 2023 }) - pay_year: number; - - @ApiProperty({ example: '2023-12-17 → 2023-12-30' }) - label: string; -} \ No newline at end of file diff --git a/src/time-and-attendance/modules/pay-period/pay-periods.module.ts b/src/time-and-attendance/modules/pay-period/pay-periods.module.ts deleted file mode 100644 index 3670c72..0000000 --- a/src/time-and-attendance/modules/pay-period/pay-periods.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { PayPeriodsQueryService } from "./services/pay-periods-query.service"; -import { PayPeriodsController } from "./controllers/pay-periods.controller"; -import { Module } from "@nestjs/common"; - -@Module({ - controllers: [PayPeriodsController], - providers: [PayPeriodsQueryService], -}) - -export class PayperiodsModule {} \ No newline at end of file diff --git a/src/time-and-attendance/modules/pay-period/services/pay-periods-command.service.ts b/src/time-and-attendance/modules/pay-period/services/pay-periods-command.service.ts deleted file mode 100644 index 960e1ef..0000000 --- a/src/time-and-attendance/modules/pay-period/services/pay-periods-command.service.ts +++ /dev/null @@ -1,71 +0,0 @@ -// import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; -// import { PrismaService } from "src/prisma/prisma.service"; -// import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto"; -// import { PayPeriodsQueryService } from "./pay-periods-query.service"; -// import { TimesheetApprovalService } from "src/modules/timesheets/services/timesheet-approval.service"; - -// @Injectable() -// export class PayPeriodsCommandService { -// constructor( -// private readonly prisma: PrismaService, -// private readonly timesheets_approval: TimesheetApprovalService, -// private readonly query: PayPeriodsQueryService, -// ) {} - -// //function to approve pay-periods according to selected crew members -// async bulkApproveCrew(dto:BulkCrewApprovalDto): Promise<{updated: number}> { -// const { supervisor_email, include_subtree, items } = dto; -// if(!items?.length) throw new BadRequestException('no items to process'); - -// //fetch and validate supervisor status -// const supervisor = await this.query.getSupervisor(supervisor_email); -// if(!supervisor) throw new NotFoundException('No employee record linked to current user'); -// if(!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor'); - -// //fetches emails of crew members linked to supervisor -// const crew_emails = await this.query.resolveCrewEmails(supervisor.id, include_subtree); - - -// for(const item of items) { -// if(!crew_emails.has(item.employee_email)) { -// throw new ForbiddenException(`Employee ${item.employee_email} not in supervisor crew`); -// } -// } - -// const period_cache = new Map(); -// const getPeriod = async (y:number, no: number) => { -// const key = `${y}-${no}`; -// if(!period_cache.has(key)) return period_cache.get(key)!; -// const period = await this.query.getPeriodWindow(y,no); -// if(!period) throw new NotFoundException(`Pay period ${y}-${no} not found`); -// period_cache.set(key, period); -// return period; -// }; - -// let updated = 0; - -// await this.prisma.$transaction(async (transaction) => { -// for(const item of items) { -// const { period_start, period_end } = await getPeriod(item.pay_year, item.period_no); - -// const t_sheets = await transaction.timesheets.findMany({ -// where: { -// employee: { user: { email: item.employee_email } }, -// OR: [ -// {shift : { some: { date: { gte: period_start, lte: period_end } } } }, -// {expense: { some: { date: { gte: period_start, lte: period_end } } } }, -// ], -// }, -// select: { id: true }, -// }); - -// for(const { id } of t_sheets) { -// await this.timesheets_approval.updateApprovalWithTransaction(transaction, id, item.approve); -// updated++; -// } - -// } -// }); -// return {updated}; -// } -// } \ No newline at end of file diff --git a/src/time-and-attendance/modules/shared/helpers/date-time.helpers.ts b/src/time-and-attendance/modules/shared/helpers/date-time.helpers.ts deleted file mode 100644 index 2076530..0000000 --- a/src/time-and-attendance/modules/shared/helpers/date-time.helpers.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { BadRequestException } from "@nestjs/common"; - -export const hhmmFromLocal = (d: Date) => - `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; - -export const toDateOnly = (s: string): Date => { - if (/^\d{4}-\d{2}-\d{2}$/.test(s)) { - const y = Number(s.slice(0,4)); - const m = Number(s.slice(5,7)) - 1; - const d = Number(s.slice(8,10)); - return new Date(y, m, d, 0, 0, 0, 0); - } - const dt = new Date(s); - if (Number.isNaN(dt.getTime())) throw new BadRequestException(`Invalid date: ${s}`); - return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), 0,0,0,0); -}; - -// export const toStringFromDate = (d: Date) => -// `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; - - -export const toISOtoDateOnly = (iso: string): Date => { - const date = new Date(iso); - if (Number.isNaN(date.getTime())) { - throw new BadRequestException(`Invalid date: ${iso}`); - } - date.setHours(0, 0, 0, 0); - return date; -}; - -export const toISODateKey = (date: Date): string => date.toISOString().slice(0, 10); - -export const normalizeDates = (dates: string[]): string[] => - Array.from(new Set(dates.map((iso) => toISODateKey(toISOtoDateOnly(iso))))); \ No newline at end of file diff --git a/src/time-and-attendance/modules/shared/interfaces/shifts.interface.ts b/src/time-and-attendance/modules/shared/interfaces/shifts.interface.ts deleted file mode 100644 index 40f897e..0000000 --- a/src/time-and-attendance/modules/shared/interfaces/shifts.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface ShiftKey { - timesheet_id: number; - date: Date; - start_time: Date; - end_time: Date; - bank_code_id: number; - is_remote: boolean; - comment?: string | null; -} \ No newline at end of file diff --git a/src/time-and-attendance/modules/shared/selects/expenses.select.ts b/src/time-and-attendance/modules/shared/selects/expenses.select.ts deleted file mode 100644 index 540d98f..0000000 --- a/src/time-and-attendance/modules/shared/selects/expenses.select.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const EXPENSE_SELECT = { - date: true, - amount: true, - mileage: true, - comment: true, - is_approved: true, - supervisor_comment: true, - bank_code: { select: { type: true } }, -} as const; - -export const EXPENSE_ASC_ORDER = { date: 'asc' as const }; \ No newline at end of file diff --git a/src/time-and-attendance/modules/shared/selects/pay-periods.select.ts b/src/time-and-attendance/modules/shared/selects/pay-periods.select.ts deleted file mode 100644 index a76f09b..0000000 --- a/src/time-and-attendance/modules/shared/selects/pay-periods.select.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const PAY_PERIOD_SELECT = { - period_start: true, - period_end: true, -} as const; \ No newline at end of file diff --git a/src/time-and-attendance/modules/shared/selects/shifts.select.ts b/src/time-and-attendance/modules/shared/selects/shifts.select.ts deleted file mode 100644 index 8c738e1..0000000 --- a/src/time-and-attendance/modules/shared/selects/shifts.select.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const SHIFT_SELECT = { - date: true, - start_time: true, - end_time: true, - comment: true, - is_approved: true, - is_remote: true, - bank_code: {select: { type: true } }, -} as const; - -export const SHIFT_ASC_ORDER = [{date: 'asc' as const}, {start_time: 'asc' as const}]; - diff --git a/src/time-and-attendance/modules/shared/shared.module.ts b/src/time-and-attendance/modules/shared/shared.module.ts deleted file mode 100644 index 0e26d7b..0000000 --- a/src/time-and-attendance/modules/shared/shared.module.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { EmployeeTimesheetResolver } from "./utils/resolve-timesheet.utils"; -import { EmailToIdResolver } from "./utils/resolve-email-id.utils"; -import { BankCodesResolver } from "./utils/resolve-bank-type-id.utils"; -import { FullNameResolver } from "./utils/resolve-full-name.utils"; -import { PrismaModule } from "src/prisma/prisma.module"; -import { Module } from "@nestjs/common"; - -@Module({ -imports: [PrismaModule], -providers: [ - FullNameResolver, - EmailToIdResolver, - BankCodesResolver, - EmployeeTimesheetResolver, -], -exports: [ - FullNameResolver, - EmailToIdResolver, - BankCodesResolver, - EmployeeTimesheetResolver, -], -}) export class SharedModule {} \ No newline at end of file diff --git a/src/time-and-attendance/modules/time-tracker/schedule-presets/controller/schedule-presets.controller.ts b/src/time-and-attendance/modules/time-tracker/schedule-presets/controller/schedule-presets.controller.ts deleted file mode 100644 index a9d5c87..0000000 --- a/src/time-and-attendance/modules/time-tracker/schedule-presets/controller/schedule-presets.controller.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Controller, Param, Query, Body, Get, Post, BadRequestException, ParseIntPipe, Delete, Patch } from "@nestjs/common"; -import { SchedulePresetsUpsertService } from "src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; -import { SchedulePresetsApplyService } from "src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-apply.service"; -import { SchedulePresetsGetService } from "src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-get.service"; -import { SchedulePresetsUpdateDto } from "src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/update-schedule-presets.dto"; -import { SchedulePresetsDto } from "src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; - -@Controller('schedule-presets') -export class SchedulePresetsController { - constructor( - private readonly upsertService: SchedulePresetsUpsertService, - private readonly applyPresetsService: SchedulePresetsApplyService, - private readonly getService: SchedulePresetsGetService, - ){} - - //used to create a schedule preset - @Post('create/:employee_id') - async createPreset( - @Param('employee_id', ParseIntPipe) employee_id: number, - @Body() dto: SchedulePresetsDto, - ) { - return await this.upsertService.createPreset(employee_id, dto); - } - - //used to update an already existing schedule preset - @Patch('update/:preset_id') - async updatePreset( - @Param('preset_id', ParseIntPipe) preset_id: number, - @Body() dto: SchedulePresetsUpdateDto, - ) { - return await this.upsertService.updatePreset(preset_id, dto); - } - - //used to delete a schedule preset - @Delete('delete/:preset_id') - async deletePreset( - @Param('preset_id') preset_id: number, - ) { - return await this.upsertService.deletePreset(preset_id); - } - - - //used to show the list of available schedule presets - @Get('find/:employee_id') - async findListById( - @Param('employee_id', ParseIntPipe) employee_id: number, - ) { - return this.getService.getSchedulePresets(employee_id); - } - - - //used to apply a preset to a timesheet - @Post('/apply-presets/:employee_id') - async applyPresets( - @Param('employee_id') employee_id: number, - @Query('preset') preset_name: string, - @Query('start') start_date: string, - ) { - if(!preset_name?.trim()) throw new BadRequestException('Query "preset" is required'); - if(!start_date?.trim()) throw new BadRequestException('Query "start" is required YYYY-MM-DD'); - return this.applyPresetsService.applyToTimesheet(employee_id, preset_name, start_date); - } -} \ No newline at end of file diff --git a/src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/update-schedule-presets.dto.ts b/src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/update-schedule-presets.dto.ts deleted file mode 100644 index 2246c20..0000000 --- a/src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/update-schedule-presets.dto.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { SchedulePresetsDto } from "src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; - -export class SchedulePresetsUpdateDto extends SchedulePresetsDto{} \ No newline at end of file diff --git a/src/time-and-attendance/modules/time-tracker/shifts/controllers/shift.controller.ts b/src/time-and-attendance/modules/time-tracker/shifts/controllers/shift.controller.ts deleted file mode 100644 index 6928cc8..0000000 --- a/src/time-and-attendance/modules/time-tracker/shifts/controllers/shift.controller.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { BadRequestException, Body, Controller, Delete, Param, Patch, Post } from "@nestjs/common"; -import { CreateShiftResult, UpdateShiftResult } from "src/time-and-attendance/utils/type.utils"; -import { ShiftsUpsertService } from "src/time-and-attendance/modules/time-tracker/shifts/services/shifts-upsert.service"; -import { UpdateShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-update.dto"; -import { ShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-create.dto"; - - -@Controller('shift') -export class ShiftController { - constructor( private readonly upsert_service: ShiftsUpsertService ){} - - @Post('create') - createBatch( - @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(dtos) - } - - - //change Body to receive dtos - @Patch('update') - updateBatch( - @Body() dtos: UpdateShiftDto[]): Promise{ - const list = Array.isArray(dtos) ? dtos: []; - if(list.length === 0) throw new BadRequestException('Body is missing or invalid (update shifts)'); - return this.upsert_service.updateShifts(dtos); - } - - @Delete(':shift_id') - remove(@Param('shift_id') shift_id: number ) { - return this.upsert_service.deleteShift(shift_id); - } - -} diff --git a/src/time-and-attendance/modules/time-tracker/shifts/shifts.module.ts b/src/time-and-attendance/modules/time-tracker/shifts/shifts.module.ts deleted file mode 100644 index e031f31..0000000 --- a/src/time-and-attendance/modules/time-tracker/shifts/shifts.module.ts +++ /dev/null @@ -1,14 +0,0 @@ - -import { BusinessLogicsModule } from 'src/time-and-attendance/domains/business-logics.module'; -import { ShiftsUpsertService } from 'src/time-and-attendance/modules/time-tracker/shifts/services/shifts-upsert.service'; -import { ShiftsGetService } from 'src/time-and-attendance/modules/time-tracker/shifts/services/shifts-get.service'; -import { ShiftController } from 'src/time-and-attendance/modules/time-tracker/shifts/controllers/shift.controller'; -import { Module } from '@nestjs/common'; - -@Module({ - imports: [ BusinessLogicsModule ], - controllers: [ShiftController], - providers: [ ShiftsGetService, ShiftsUpsertService ], - exports: [ ShiftsUpsertService ], -}) -export class ShiftsModule {} diff --git a/src/time-and-attendance/modules/time-tracker/timesheets/controllers/timesheet.controller.ts b/src/time-and-attendance/modules/time-tracker/timesheets/controllers/timesheet.controller.ts deleted file mode 100644 index 9ec6d6e..0000000 --- a/src/time-and-attendance/modules/time-tracker/timesheets/controllers/timesheet.controller.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { GetTimesheetsOverviewService } from "src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-get-overview.service"; -import { BadRequestException, Controller, Get, Query} from "@nestjs/common"; -import { EmailToIdResolver } from "src/time-and-attendance/modules/shared/utils/resolve-email-id.utils"; - -@Controller('timesheets') -export class TimesheetController { - constructor( - private readonly timesheetOverview: GetTimesheetsOverviewService, - private readonly emailResolver: EmailToIdResolver, - ){} - - @Get() - async getTimesheetByIds( - @Query('employee_email') employee_email: string, - @Query('year') year: string, - @Query('period_number') period_number: string, - ) { - if (!employee_email || !year || !period_number) { - throw new BadRequestException('Query params "employee_email", "year" and eriod_number" are required.'); - } - const employee_id = await this.emailResolver.findIdByEmail(employee_email); - const pay_year = Number(year); - const period_num = Number(period_number); - return this.timesheetOverview.getTimesheetsForEmployeeByPeriod(employee_id, pay_year, period_num); - } -} diff --git a/src/time-and-attendance/modules/time-tracker/timesheets/timesheets.module.ts b/src/time-and-attendance/modules/time-tracker/timesheets/timesheets.module.ts deleted file mode 100644 index 595d660..0000000 --- a/src/time-and-attendance/modules/time-tracker/timesheets/timesheets.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { GetTimesheetsOverviewService } from 'src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-get-overview.service'; -import { TimesheetArchiveService } from 'src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-archive.service'; -import { TimesheetController } from 'src/time-and-attendance/modules/time-tracker/timesheets/controllers/timesheet.controller'; -import { SharedModule } from 'src/time-and-attendance/modules/shared/shared.module'; -import { Module } from '@nestjs/common'; - -@Module({ - imports: [SharedModule], - controllers: [TimesheetController], - providers: [ - TimesheetArchiveService, - GetTimesheetsOverviewService, - ], - exports: [], -}) -export class TimesheetsModule {} diff --git a/src/time-and-attendance/pay-period/controllers/pay-periods.controller.ts b/src/time-and-attendance/pay-period/controllers/pay-periods.controller.ts new file mode 100644 index 0000000..1d43791 --- /dev/null +++ b/src/time-and-attendance/pay-period/controllers/pay-periods.controller.ts @@ -0,0 +1,69 @@ +import { Body, Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query, Req, UnauthorizedException } from "@nestjs/common"; +import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto"; +import { PayPeriodsQueryService } from "../services/pay-periods-query.service"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { Roles as RoleEnum } from '.prisma/client'; +import { PayPeriodsCommandService } from "../services/pay-periods-command.service"; +import { PayPeriodBundleDto } from "../dtos/bundle-pay-period.dto"; +import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto"; + + +@Controller('pay-periods') +export class PayPeriodsController { + + constructor( + private readonly queryService: PayPeriodsQueryService, + private readonly commandService: PayPeriodsCommandService, + ) { } + + @Get('current-and-all') + async getCurrentAndAll(@Query('date') date?: string): Promise { + const [current, periods] = await Promise.all([ + this.queryService.findCurrent(date), + this.queryService.findAll(), + ]); + return { current, periods }; + } + + @Get("date/:date") + async findByDate(@Param("date") date: string) { + return this.queryService.findByDate(date); + } + + @Get(":year/:periodNumber") + async findOneByYear( + @Param("year", ParseIntPipe) year: number, + @Param("periodNumber", ParseIntPipe) period_no: number, + ) { + return this.queryService.findOneByYearPeriod(year, period_no); + } + + @Patch("crew/pay-period-approval") + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + async bulkApproval(@Req() req, @Body() dto: BulkCrewApprovalDto) { + const email = req.user?.email; + if(!email) throw new UnauthorizedException(`Session infos not found`); + return this.commandService.bulkApproveCrew(email, dto); + } + + @Get('crew/:year/:periodNumber') + @RolesAllowed(RoleEnum.SUPERVISOR) + async getCrewOverview(@Req() req, + @Param('year', ParseIntPipe) year: number, + @Param('periodNumber', ParseIntPipe) period_no: number, + @Query('includeSubtree', new ParseBoolPipe({ optional: true })) include_subtree = false, + ): Promise { + const email = req.user?.email; + if(!email) throw new UnauthorizedException(`Session infos not found`); + return this.queryService.getCrewOverview(year, period_no, email, include_subtree); + } + + @Get('overview/:year/:periodNumber') + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) + async getOverviewByYear( + @Param('year', ParseIntPipe) year: number, + @Param('periodNumber', ParseIntPipe) period_no: number, + ): Promise { + return this.queryService.getOverviewByYearPeriod(year, period_no); + } +} diff --git a/src/time-and-attendance/modules/pay-period/dtos/bulk-crew-approval.dto.ts b/src/time-and-attendance/pay-period/dtos/bulk-crew-approval.dto.ts similarity index 76% rename from src/time-and-attendance/modules/pay-period/dtos/bulk-crew-approval.dto.ts rename to src/time-and-attendance/pay-period/dtos/bulk-crew-approval.dto.ts index 3762ddb..4ba5527 100644 --- a/src/time-and-attendance/modules/pay-period/dtos/bulk-crew-approval.dto.ts +++ b/src/time-and-attendance/pay-period/dtos/bulk-crew-approval.dto.ts @@ -1,5 +1,5 @@ import { Type } from "class-transformer"; -import { IsArray, IsBoolean, IsEmail, IsInt, IsOptional, ValidateNested } from "class-validator"; +import { IsArray, IsBoolean, IsEmail, IsInt, ValidateNested } from "class-validator"; export class BulkCrewApprovalItemDto { @IsInt() @@ -16,9 +16,6 @@ export class BulkCrewApprovalItemDto { } export class BulkCrewApprovalDto { - @IsEmail() - supervisor_email: string; - @IsBoolean() include_subtree: boolean = false; diff --git a/src/time-and-attendance/pay-period/dtos/bundle-pay-period.dto.ts b/src/time-and-attendance/pay-period/dtos/bundle-pay-period.dto.ts new file mode 100644 index 0000000..ad84088 --- /dev/null +++ b/src/time-and-attendance/pay-period/dtos/bundle-pay-period.dto.ts @@ -0,0 +1,6 @@ +import { PayPeriodDto } from "./pay-period.dto"; + +export class PayPeriodBundleDto { + current: PayPeriodDto; + periods: PayPeriodDto[]; +} \ No newline at end of file diff --git a/src/time-and-attendance/pay-period/dtos/overview-employee-period.dto.ts b/src/time-and-attendance/pay-period/dtos/overview-employee-period.dto.ts new file mode 100644 index 0000000..4cae27b --- /dev/null +++ b/src/time-and-attendance/pay-period/dtos/overview-employee-period.dto.ts @@ -0,0 +1,23 @@ +export class EmployeePeriodOverviewDto { + email: string; + employee_name: string; + regular_hours: number; + other_hours: { + evening_hours: number; + + emergency_hours: number; + + overtime_hours: number; + + sick_hours: number; + + holiday_hours: number; + + vacation_hours: number; + }; + total_hours: number; + expenses: number; + mileage: number; + is_approved: boolean; + is_remote: boolean; +} diff --git a/src/time-and-attendance/pay-period/dtos/overview-pay-period.dto.ts b/src/time-and-attendance/pay-period/dtos/overview-pay-period.dto.ts new file mode 100644 index 0000000..3748eb8 --- /dev/null +++ b/src/time-and-attendance/pay-period/dtos/overview-pay-period.dto.ts @@ -0,0 +1,11 @@ +import { EmployeePeriodOverviewDto } from './overview-employee-period.dto'; + +export class PayPeriodOverviewDto { + pay_period_no: number; + pay_year: number; + period_start: string; + period_end: string; + payday: string; + label: string; + employees_overview: EmployeePeriodOverviewDto[]; +} diff --git a/src/time-and-attendance/pay-period/dtos/pay-period.dto.ts b/src/time-and-attendance/pay-period/dtos/pay-period.dto.ts new file mode 100644 index 0000000..a85f481 --- /dev/null +++ b/src/time-and-attendance/pay-period/dtos/pay-period.dto.ts @@ -0,0 +1,8 @@ +export class PayPeriodDto { + pay_period_no: number; + period_start: string; + period_end: string; + payday: string; + pay_year: number; + label: string; +} \ No newline at end of file diff --git a/src/time-and-attendance/modules/pay-period/mappers/pay-periods.mapper.ts b/src/time-and-attendance/pay-period/mappers/pay-periods.mapper.ts similarity index 100% rename from src/time-and-attendance/modules/pay-period/mappers/pay-periods.mapper.ts rename to src/time-and-attendance/pay-period/mappers/pay-periods.mapper.ts diff --git a/src/time-and-attendance/pay-period/pay-periods.module.ts b/src/time-and-attendance/pay-period/pay-periods.module.ts new file mode 100644 index 0000000..f971084 --- /dev/null +++ b/src/time-and-attendance/pay-period/pay-periods.module.ts @@ -0,0 +1,20 @@ +import { PayPeriodsQueryService } from "./services/pay-periods-query.service"; +import { PayPeriodsController } from "./controllers/pay-periods.controller"; +import { Module } from "@nestjs/common"; +import { PayPeriodsCommandService } from "src/time-and-attendance/pay-period/services/pay-periods-command.service"; +import { TimesheetsModule } from "src/time-and-attendance/time-tracker/timesheets/timesheets.module"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service"; + +@Module({ + imports:[TimesheetsModule], + controllers: [PayPeriodsController], + providers: [ + PayPeriodsQueryService, + PayPeriodsCommandService, + EmailToIdResolver, + TimesheetApprovalService, + ], +}) + +export class PayperiodsModule {} \ No newline at end of file diff --git a/src/time-and-attendance/pay-period/services/pay-periods-command.service.ts b/src/time-and-attendance/pay-period/services/pay-periods-command.service.ts new file mode 100644 index 0000000..6c48fe2 --- /dev/null +++ b/src/time-and-attendance/pay-period/services/pay-periods-command.service.ts @@ -0,0 +1,72 @@ +import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; +import { BulkCrewApprovalDto } from "../dtos/bulk-crew-approval.dto"; +import { PayPeriodsQueryService } from "./pay-periods-query.service"; +import { TimesheetApprovalService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service"; + +@Injectable() +export class PayPeriodsCommandService { + constructor( + private readonly prisma: PrismaService, + private readonly timesheets_approval: TimesheetApprovalService, + private readonly query: PayPeriodsQueryService, + ) {} + + //function to approve pay-periods according to selected crew members + async bulkApproveCrew(email: string, dto:BulkCrewApprovalDto): Promise<{updated: number}> { + const { include_subtree, items } = dto; + if(!items?.length) throw new BadRequestException('no items to process'); + + //fetch and validate supervisor status + const supervisor = await this.query.getSupervisor(email); + if(!supervisor) throw new NotFoundException('No employee record linked to current user'); + if(!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor'); + + //fetches emails of crew members linked to supervisor + const crew_emails = await this.query.resolveCrewEmails(supervisor.id, include_subtree); + + + for(const item of items) { + if(!crew_emails.has(item.employee_email)) { + throw new ForbiddenException(`Employee ${item.employee_email} not in supervisor crew`); + } + } + + const period_cache = new Map(); + const getPeriod = async (year:number, period_no: number) => { + const key = `${year}-${period_no}`; + if(period_cache.has(key)) return period_cache.get(key)!; + + const period = await this.query.getPeriodWindow(year,period_no); + if(!period) throw new NotFoundException(`Pay period ${year}-${period_no} not found`); + period_cache.set(key, period); + return period; + }; + + let updated = 0; + + await this.prisma.$transaction(async (transaction) => { + for(const item of items) { + const { period_start, period_end } = await getPeriod(item.pay_year, item.period_no); + + const t_sheets = await transaction.timesheets.findMany({ + where: { + employee: { user: { email: item.employee_email } }, + OR: [ + {shift : { some: { date: { gte: period_start, lte: period_end } } } }, + {expense: { some: { date: { gte: period_start, lte: period_end } } } }, + ], + }, + select: { id: true }, + }); + + for(const { id } of t_sheets) { + await this.timesheets_approval.cascadeApprovalWithtx(transaction, id, item.approve); + updated++; + } + + } + }); + return {updated}; + } +} \ No newline at end of file diff --git a/src/time-and-attendance/modules/pay-period/services/pay-periods-query.service.ts b/src/time-and-attendance/pay-period/services/pay-periods-query.service.ts similarity index 100% rename from src/time-and-attendance/modules/pay-period/services/pay-periods-query.service.ts rename to src/time-and-attendance/pay-period/services/pay-periods-query.service.ts diff --git a/src/time-and-attendance/time-and-attendance.module.ts b/src/time-and-attendance/time-and-attendance.module.ts index ff54d41..1d052df 100644 --- a/src/time-and-attendance/time-and-attendance.module.ts +++ b/src/time-and-attendance/time-and-attendance.module.ts @@ -1,28 +1,29 @@ -import { SchedulePresetsUpsertService } from "src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; -import { GetTimesheetsOverviewService } from "src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-get-overview.service"; -import { SchedulePresetsGetService } from "src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-get.service"; -import { SchedulePresetsApplyService } from "src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-apply.service"; -import { SchedulePresetsController } from "src/time-and-attendance/modules/time-tracker/schedule-presets/controller/schedule-presets.controller"; -import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module"; -import { ExpenseUpsertService } from "src/time-and-attendance/modules/expenses/services/expense-upsert.service"; -import { ShiftsUpsertService } from "src/time-and-attendance/modules/time-tracker/shifts/services/shifts-upsert.service"; -import { TimesheetController } from "src/time-and-attendance/modules/time-tracker/timesheets/controllers/timesheet.controller"; -import { ExpenseController } from "src/time-and-attendance/modules/expenses/controllers/expense.controller"; -import { PayperiodsModule } from "src/time-and-attendance/modules/pay-period/pay-periods.module"; -import { ShiftsGetService } from "src/time-and-attendance/modules/time-tracker/shifts/services/shifts-get.service"; -import { ShiftController } from "src/time-and-attendance/modules/time-tracker/shifts/controllers/shift.controller"; -import { SharedModule } from "src/time-and-attendance/modules/shared/shared.module"; + import { Module } from "@nestjs/common"; +import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module"; +import { ExpenseController } from "src/time-and-attendance/expenses/controllers/expense.controller"; +import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service"; +import { PayperiodsModule } from "src/time-and-attendance/pay-period/pay-periods.module"; +import { SchedulePresetsController } from "src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller"; +import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service"; +import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service"; +import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; +import { ShiftController } from "src/time-and-attendance/time-tracker/shifts/controllers/shift.controller"; +import { ShiftsGetService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-get.service"; +import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service"; +import { TimesheetController } from "src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller"; +import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; @Module({ imports: [ - BusinessLogicsModule, - PayperiodsModule, - SharedModule, + BusinessLogicsModule, + PayperiodsModule, ], controllers: [ - TimesheetController, - ShiftController, + TimesheetController, + ShiftController, SchedulePresetsController, ExpenseController, ], @@ -34,6 +35,8 @@ import { Module } from "@nestjs/common"; SchedulePresetsUpsertService, SchedulePresetsGetService, SchedulePresetsApplyService, + EmailToIdResolver, + BankCodesResolver, ], exports: [], -}) export class TimeAndAttendanceModule{}; \ No newline at end of file +}) export class TimeAndAttendanceModule { }; \ No newline at end of file diff --git a/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts b/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts new file mode 100644 index 0000000..042b8df --- /dev/null +++ b/src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller.ts @@ -0,0 +1,58 @@ +import { Controller, Param, Query, Body, Get, Post, BadRequestException, ParseIntPipe, Delete, Patch, Req } from "@nestjs/common"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; +import { SchedulePresetsUpdateDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/update-schedule-presets.dto"; +import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service"; +import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service"; +import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; +import { Roles as RoleEnum } from '.prisma/client'; + +@Controller('schedule-presets') +export class SchedulePresetsController { + constructor( + private readonly upsertService: SchedulePresetsUpsertService, + private readonly getService: SchedulePresetsGetService, + private readonly applyPresetsService: SchedulePresetsApplyService, + ){} + + //used to create a schedule preset + @Post('create') + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + async createPreset( @Req() req, @Body() dto: SchedulePresetsDto ) { + const email = req.user?.email; + return await this.upsertService.createPreset(email, dto); + } + + //used to update an already existing schedule preset + @Patch('update/:preset_id') + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + async updatePreset( @Param('preset_id', ParseIntPipe) preset_id: number,@Body() dto: SchedulePresetsUpdateDto ) { + return await this.upsertService.updatePreset(preset_id, dto); + } + + //used to delete a schedule preset + @Delete('delete/:preset_id') + @RolesAllowed(RoleEnum.ADMIN) + async deletePreset( @Param('preset_id') preset_id: number ) { + return await this.upsertService.deletePreset(preset_id); + } + + + //used to show the list of available schedule presets + @Get('find-list') + @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + async findListById( @Req() req) { + const email = req.user?.email; + return this.getService.getSchedulePresets(email); + } + + //used to apply a preset to a timesheet + @Post('apply-presets') + @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + async applyPresets( @Req() req, @Query('preset') preset_name: string, @Query('start') start_date: string ) { + const email = req.user?.email; + if(!preset_name?.trim()) throw new BadRequestException('Query "preset" is required'); + if(!start_date?.trim()) throw new BadRequestException('Query "start" is required YYYY-MM-DD'); + return this.applyPresetsService.applyToTimesheet(email, preset_name, start_date); + } +} \ No newline at end of file diff --git a/src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts b/src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts similarity index 100% rename from src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts rename to src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts diff --git a/src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/create-schedule-presets.dto.ts b/src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto.ts similarity index 82% rename from src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/create-schedule-presets.dto.ts rename to src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto.ts index 86d7704..fc6b7d8 100644 --- a/src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/create-schedule-presets.dto.ts +++ b/src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto.ts @@ -1,5 +1,5 @@ import { ArrayMinSize, IsArray, IsBoolean, IsOptional, IsString } from "class-validator"; -import { SchedulePresetShiftsDto } from "src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto"; +import { SchedulePresetShiftsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto"; export class SchedulePresetsDto { @IsString() diff --git a/src/time-and-attendance/time-tracker/schedule-presets/dtos/update-schedule-presets.dto.ts b/src/time-and-attendance/time-tracker/schedule-presets/dtos/update-schedule-presets.dto.ts new file mode 100644 index 0000000..b8b9cb6 --- /dev/null +++ b/src/time-and-attendance/time-tracker/schedule-presets/dtos/update-schedule-presets.dto.ts @@ -0,0 +1,4 @@ +import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; + + +export class SchedulePresetsUpdateDto extends SchedulePresetsDto{} \ No newline at end of file diff --git a/src/time-and-attendance/modules/time-tracker/schedule-presets/schedule-presets.module.ts b/src/time-and-attendance/time-tracker/schedule-presets/schedule-presets.module.ts similarity index 59% rename from src/time-and-attendance/modules/time-tracker/schedule-presets/schedule-presets.module.ts rename to src/time-and-attendance/time-tracker/schedule-presets/schedule-presets.module.ts index ac6c595..b17863a 100644 --- a/src/time-and-attendance/modules/time-tracker/schedule-presets/schedule-presets.module.ts +++ b/src/time-and-attendance/time-tracker/schedule-presets/schedule-presets.module.ts @@ -1,13 +1,12 @@ -import { SchedulePresetsUpsertService } from "src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; -import { SchedulePresetsApplyService } from "src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-apply.service"; -import { SchedulePresetsGetService } from "src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-get.service"; -import { SchedulePresetsController } from "src/time-and-attendance/modules/time-tracker/schedule-presets/controller/schedule-presets.controller"; -import { SharedModule } from "src/time-and-attendance/modules/shared/shared.module"; + import { Module } from "@nestjs/common"; +import { SchedulePresetsController } from "src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller"; +import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service"; +import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service"; +import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; @Module({ - imports: [SharedModule], controllers: [SchedulePresetsController], providers: [ SchedulePresetsUpsertService, diff --git a/src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts similarity index 92% rename from src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts rename to src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts index 75bdf1c..c055373 100644 --- a/src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts +++ b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service.ts @@ -4,20 +4,19 @@ import { DATE_ISO_FORMAT } from "src/time-and-attendance/utils/constants.utils"; import { PrismaService } from "src/prisma/prisma.service"; import { ApplyResult } from "src/time-and-attendance/utils/type.utils"; import { WEEKDAY } from "src/time-and-attendance/utils/mappers.utils"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; @Injectable() export class SchedulePresetsApplyService { - constructor( private readonly prisma: PrismaService) {} + constructor( private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver) {} - async applyToTimesheet( - employee_id: number, - preset_name: string, - start_date_iso: string, - ): Promise { + async applyToTimesheet( email: string, preset_name: string, start_date_iso: string ): Promise { if(!preset_name?.trim()) throw new BadRequestException('A preset_name is required'); if(!DATE_ISO_FORMAT.test(start_date_iso)) throw new BadRequestException('start_date must be of format :YYYY-MM-DD'); + const employee_id = await this.emailResolver.findIdByEmail(email); + const preset = await this.prisma.schedulePresets.findFirst({ where: { employee_id, name: preset_name }, include: { diff --git a/src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-get.service.ts b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts similarity index 81% rename from src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-get.service.ts rename to src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts index e73227d..3bb3050 100644 --- a/src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-get.service.ts +++ b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service.ts @@ -2,13 +2,18 @@ import { PresetResponse, ShiftResponse } from "src/time-and-attendance/utils/typ import { PrismaService } from "src/prisma/prisma.service"; import { Injectable } from "@nestjs/common"; import { Prisma } from "@prisma/client"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; @Injectable() export class SchedulePresetsGetService { - constructor( private readonly prisma: PrismaService ){} + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver: EmailToIdResolver, + ){} - async getSchedulePresets(employee_id: number): Promise { + async getSchedulePresets(email: string): Promise { try { + const employee_id = await this.emailResolver.findIdByEmail(email); const presets = await this.prisma.schedulePresets.findMany({ where: { employee_id }, orderBy: [{is_default: 'desc' }, { name: 'asc' }], diff --git a/src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts similarity index 94% rename from src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts rename to src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts index da45941..0eaef09 100644 --- a/src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts +++ b/src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service.ts @@ -1,24 +1,28 @@ import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common"; import { CreatePresetResult, DeletePresetResult, UpdatePresetResult } from "src/time-and-attendance/utils/type.utils"; -import { SchedulePresetsDto } from "src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; -import { BankCodesResolver } from "src/time-and-attendance/modules/shared/utils/resolve-bank-type-id.utils"; import { Prisma, Weekday } from "@prisma/client"; import { toHHmmFromDate } from "src/time-and-attendance/utils/date-time.utils"; import { PrismaService } from "src/prisma/prisma.service"; +import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils"; +import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; @Injectable() export class SchedulePresetsUpsertService { constructor( private readonly prisma: PrismaService, private readonly typeResolver : BankCodesResolver, + private readonly emailResolver: EmailToIdResolver, ){} //_________________________________________________________________ // CREATE //_________________________________________________________________ - async createPreset( employee_id: number, dto: SchedulePresetsDto): Promise { + async createPreset( email: string, dto: SchedulePresetsDto): Promise { try { const shifts_data = await this.resolveAndBuildPresetShifts(dto); - if(!shifts_data) throw new BadRequestException(`Employee with id: ${employee_id} or dto not found`); + const employee_id = await this.emailResolver.findIdByEmail(email); + if(!shifts_data) throw new BadRequestException(`Employee with email: ${email} or dto not found`); + await this.prisma.$transaction(async (tx)=> { if(dto.is_default) { await tx.schedulePresets.updateMany({ diff --git a/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts new file mode 100644 index 0000000..767191b --- /dev/null +++ b/src/time-and-attendance/time-tracker/shifts/controllers/shift.controller.ts @@ -0,0 +1,38 @@ +import { BadRequestException, Body, Controller, Delete, Param, Patch, Post, Req } from "@nestjs/common"; +import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; +import { UpdateShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto"; +import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service"; +import { CreateShiftResult, UpdateShiftResult } from "src/time-and-attendance/utils/type.utils"; +import { Roles as RoleEnum } from '.prisma/client'; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; + +@Controller('shift') +export class ShiftController { + constructor( private readonly upsert_service: ShiftsUpsertService ){} + + @Post('create') + @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + createBatch( @Req() req, @Body()dtos: ShiftDto[]): Promise { + const email = req.user?.email; + const list = Array.isArray(dtos) ? dtos : []; + if(list.length === 0) throw new BadRequestException('Body is missing or invalid (create shifts)'); + return this.upsert_service.createShifts(email, dtos) + } + + + //change Body to receive dtos + @Patch('update') + @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + updateBatch( @Body() dtos: UpdateShiftDto[]): Promise{ + const list = Array.isArray(dtos) ? dtos: []; + if(list.length === 0) throw new BadRequestException('Body is missing or invalid (update shifts)'); + return this.upsert_service.updateShifts(dtos); + } + + @Delete(':shift_id') + @RolesAllowed(RoleEnum.EMPLOYEE, RoleEnum.ACCOUNTING, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ADMIN) + remove(@Param('shift_id') shift_id: number ) { + return this.upsert_service.deleteShift(shift_id); + } + +} diff --git a/src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-create.dto.ts b/src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto.ts similarity index 100% rename from src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-create.dto.ts rename to src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto.ts diff --git a/src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-get.dto.ts b/src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto.ts similarity index 100% rename from src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-get.dto.ts rename to src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto.ts diff --git a/src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-update.dto.ts b/src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto.ts similarity index 76% rename from src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-update.dto.ts rename to src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto.ts index ce16f8e..0dd4505 100644 --- a/src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-update.dto.ts +++ b/src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto.ts @@ -1,6 +1,6 @@ import { PartialType, OmitType } from "@nestjs/swagger"; -import { ShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-create.dto"; import { IsInt } from "class-validator"; +import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; export class UpdateShiftDto extends PartialType( // allows update using ShiftDto and preventing OmitType variables to be modified diff --git a/src/time-and-attendance/modules/time-tracker/shifts/services/shifts-archival.service.ts b/src/time-and-attendance/time-tracker/shifts/services/shifts-archival.service.ts similarity index 100% rename from src/time-and-attendance/modules/time-tracker/shifts/services/shifts-archival.service.ts rename to src/time-and-attendance/time-tracker/shifts/services/shifts-archival.service.ts diff --git a/src/time-and-attendance/modules/time-tracker/shifts/services/shifts-get.service.ts b/src/time-and-attendance/time-tracker/shifts/services/shifts-get.service.ts similarity index 95% rename from src/time-and-attendance/modules/time-tracker/shifts/services/shifts-get.service.ts rename to src/time-and-attendance/time-tracker/shifts/services/shifts-get.service.ts index c1c2e12..d1eed96 100644 --- a/src/time-and-attendance/modules/time-tracker/shifts/services/shifts-get.service.ts +++ b/src/time-and-attendance/time-tracker/shifts/services/shifts-get.service.ts @@ -2,7 +2,7 @@ import { toStringFromDate, toStringFromHHmm } from "src/time-and-attendance/util import { Injectable, NotFoundException } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { shift_select } from "src/time-and-attendance/utils/selects.utils"; -import { GetShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-get.dto"; +import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto"; /** diff --git a/src/time-and-attendance/modules/time-tracker/shifts/services/shifts-upsert.service.ts b/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts similarity index 74% rename from src/time-and-attendance/modules/time-tracker/shifts/services/shifts-upsert.service.ts rename to src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts index e16797c..59dc811 100644 --- a/src/time-and-attendance/modules/time-tracker/shifts/services/shifts-upsert.service.ts +++ b/src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service.ts @@ -1,12 +1,13 @@ import { CreateShiftResult, NormedOk, NormedErr, UpdateShiftResult, UpdateShiftPayload, UpdateShiftChanges, Normalized } from "src/time-and-attendance/utils/type.utils"; -import { overlaps, toStringFromHHmm, toStringFromDate, toDateFromString, toHHmmFromString } from "src/time-and-attendance/utils/date-time.utils"; +import { overlaps, toStringFromHHmm, toStringFromDate, toDateFromString, toHHmmFromString, weekStartSunday } from "src/time-and-attendance/utils/date-time.utils"; import { Injectable, BadRequestException, ConflictException, NotFoundException } from "@nestjs/common"; import { OvertimeService } from "src/time-and-attendance/domains/services/overtime.service"; -import { UpdateShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-update.dto"; import { PrismaService } from "src/prisma/prisma.service"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; +import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; +import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto"; +import { UpdateShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto"; import { shift_select } from "src/time-and-attendance/utils/selects.utils"; -import { GetShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-get.dto"; -import { ShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-create.dto"; @@ -15,6 +16,7 @@ export class ShiftsUpsertService { constructor( private readonly prisma: PrismaService, private readonly overtime: OvertimeService, + private readonly emailResolver: EmailToIdResolver, ) { } //_________________________________________________________________ @@ -25,76 +27,140 @@ export class ShiftsUpsertService { //checks for overlaping shifts //create new shifts //calculate overtime - async createShifts(dtos: ShiftDto[]): Promise { + async createShifts(email: string, 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})`) }; + const employee_id = await this.emailResolver.findIdByEmail(email); + + const normed_shifts = await Promise.all( + dtos.map(async (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})` + ), + }; + } + + const start_date = weekStartSunday(normed.date); + + const timesheet = await this.prisma.timesheets.findFirst({ + where: { start_date, employee_id }, + select: { id: true }, + }); + if (!timesheet) { + return { + index, + error: new NotFoundException(`Timesheet not found`), + }; + + } + + return { + index, + dto, + normed, + timesheet_id: timesheet.id, + }; + } catch (error) { + return { index, error }; } - 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(); + const ok_items = normed_shifts.filter( + (item): item is NormedOk & { timesheet_id: number } => "normed" in item); - ok_items.forEach(({ index, normed }) => { - const d = normed.date; - const key = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); + const regroup_by_date = new Map(); + ok_items.forEach(({ index, normed, timesheet_id }) => { + const day = new Date(normed.date.getFullYear(), normed.date.getMonth(), normed.date.getDate()).getTime(); + const key = `${timesheet_id}|${day}`; if (!regroup_by_date.has(key)) regroup_by_date.set(key, []); regroup_by_date.get(key)!.push(index); }); + const timesheet_keys = Array.from(regroup_by_date.keys()).map((raw) => { + const [timesheet, day] = raw.split('|'); + return { + timesheet_id: Number(timesheet), + day: Number(day), + key: raw, + }; + }); + for (const indices of regroup_by_date.values()) { const ordered = indices .map(index => { - const item = normed_shift[index] as NormedOk; - return { index: index, start: item.normed.start_time, end: item.normed.end_time }; + const item = normed_shifts[index] as NormedOk & { timesheet_id: number }; + 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({ + if ( + overlaps( + { start: ordered[j - 1].start, end: ordered[j - 1].end }, + { start: ordered[j].start, end: ordered[j].end } + ) + ) { + const error = 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 CreateShiftResult) - : ({ ok: false, error: new BadRequestException('Batch aborted due to overlaps in another date group') }) + ? ({ + ok: false, + error + } as CreateShiftResult) + : ({ + ok: false, + error: new BadRequestException( + 'Batch aborted due to overlaps in another date group' + ), + }), ); } } } return this.prisma.$transaction(async (tx) => { - const results: CreateShiftResult[] = Array.from({ length: dtos.length }, () => ({ ok: false, error: new Error('uninitialized') })); + const results: CreateShiftResult[] = Array.from( + { length: dtos.length }, + () => ({ ok: false, error: new Error('uninitialized') })); + const existing_map = new Map(); - normed_shift.forEach((x, i) => { + for (const { timesheet_id, day, key } of timesheet_keys) { + const day_date = new Date(day); + const rows = await tx.shifts.findMany({ + where: { timesheet_id, date: day_date }, + select: { start_time: true, end_time: true }, + }); + existing_map.set( + key, + rows.map((row) => ({ start_time: row.start_time, end_time: row.end_time })), + ); + } + + normed_shifts.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: { 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 { index, dto, normed, timesheet_id } = item; + const day_key = new Date(normed.date.getFullYear(), normed.date.getMonth(), normed.date.getDate()).getTime(); + const map_key = `${timesheet_id}|${day_key}`; + let existing = existing_map.get(map_key); + if(!existing) { + existing = []; + existing_map.set(map_key, existing); + } const hit = existing.find(e => overlaps({ start: e.start_time, end: e.end_time }, { start: normed.start_time, end: normed.end_time })); if (hit) { results[index] = { @@ -114,7 +180,7 @@ export class ShiftsUpsertService { const row = await tx.shifts.create({ data: { - timesheet_id: dto.timesheet_id, + timesheet_id: timesheet_id, bank_code_id: dto.bank_code_id, date: normed.date, start_time: normed.start_time, @@ -126,10 +192,11 @@ export class ShiftsUpsertService { }); existing.push({ start_time: row.start_time, end_time: row.end_time }); + existing_map.set(map_key, existing); - const summary = await this.overtime.getWeekOvertimeSummary(dto.timesheet_id, normed.date, tx); + const summary = await this.overtime.getWeekOvertimeSummary(timesheet_id, normed.date, tx); const shift: GetShiftDto = { - timesheet_id: row.timesheet_id, + timesheet_id: timesheet_id, bank_code_id: row.bank_code_id, date: toStringFromDate(row.date), start_time: toStringFromHHmm(row.start_time), diff --git a/src/time-and-attendance/time-tracker/shifts/shifts.module.ts b/src/time-and-attendance/time-tracker/shifts/shifts.module.ts new file mode 100644 index 0000000..3dc407d --- /dev/null +++ b/src/time-and-attendance/time-tracker/shifts/shifts.module.ts @@ -0,0 +1,14 @@ + +import { BusinessLogicsModule } from 'src/time-and-attendance/domains/business-logics.module'; +import { Module } from '@nestjs/common'; +import { ShiftController } from 'src/time-and-attendance/time-tracker/shifts/controllers/shift.controller'; +import { ShiftsGetService } from 'src/time-and-attendance/time-tracker/shifts/services/shifts-get.service'; +import { ShiftsUpsertService } from 'src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service'; + +@Module({ + imports: [ BusinessLogicsModule ], + controllers: [ShiftController], + providers: [ ShiftsGetService, ShiftsUpsertService ], + exports: [ ShiftsUpsertService ], +}) +export class ShiftsModule {} diff --git a/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts b/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts new file mode 100644 index 0000000..ffde38b --- /dev/null +++ b/src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller.ts @@ -0,0 +1,19 @@ +import { Controller, Get, ParseIntPipe, Query, Req, UnauthorizedException} from "@nestjs/common"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service"; +import { Roles as RoleEnum } from '.prisma/client'; + + +@Controller('timesheets') +export class TimesheetController { + constructor( private readonly timesheetOverview: GetTimesheetsOverviewService ){} + + @Get() + @RolesAllowed(RoleEnum.SUPERVISOR, RoleEnum.HR, RoleEnum.ACCOUNTING, RoleEnum.ADMIN) + async getTimesheetByIds( + @Req() req, @Query('year', ParseIntPipe) year:number, @Query('period_number', ParseIntPipe) period_number: number) { + const email = req.user?.email; + if(!email) throw new UnauthorizedException('Unauthorized User');  + return this.timesheetOverview.getTimesheetsForEmployeeByPeriod(email, year, period_number); + } +} diff --git a/src/time-and-attendance/modules/time-tracker/timesheets/dtos/timesheet.dto.ts b/src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto.ts similarity index 100% rename from src/time-and-attendance/modules/time-tracker/timesheets/dtos/timesheet.dto.ts rename to src/time-and-attendance/time-tracker/timesheets/dtos/timesheet.dto.ts diff --git a/src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-approval.service.ts b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service.ts similarity index 94% rename from src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-approval.service.ts rename to src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service.ts index 088b5b7..84756c2 100644 --- a/src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-approval.service.ts +++ b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service.ts @@ -27,11 +27,11 @@ import { Injectable } from "@nestjs/common"; const timesheet = await this.updateApprovalWithTransaction(transaction, timesheetId, isApproved); await transaction.shifts.updateMany({ where: { timesheet_id: timesheetId }, - data: { is_approved: isApproved }, + data: { is_approved: isApproved }, }); await transaction.expenses.updateManyAndReturn({ where: { timesheet_id: timesheetId }, - data: { is_approved: isApproved }, + data: { is_approved: isApproved }, }); return timesheet; } diff --git a/src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-archive.service.ts b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-archive.service.ts similarity index 100% rename from src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-archive.service.ts rename to src/time-and-attendance/time-tracker/timesheets/services/timesheet-archive.service.ts diff --git a/src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-get-overview.service.ts b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts similarity index 95% rename from src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-get-overview.service.ts rename to src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts index e89d684..ef7fdd6 100644 --- a/src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-get-overview.service.ts +++ b/src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service.ts @@ -3,19 +3,25 @@ import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/time-and-attendance/utils/co import { Injectable, NotFoundException } from "@nestjs/common"; import { TotalExpenses, TotalHours } from "src/time-and-attendance/utils/type.utils"; import { PrismaService } from "src/prisma/prisma.service"; +import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils"; @Injectable() export class GetTimesheetsOverviewService { - constructor(private readonly prisma: PrismaService) { } + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver : EmailToIdResolver, + ) { } //----------------------------------------------------------------------------------- // GET TIMESHEETS FOR A SELECTED EMPLOYEE //----------------------------------------------------------------------------------- - async getTimesheetsForEmployeeByPeriod(employee_id: number, pay_year: number, pay_period_no: number) { + async getTimesheetsForEmployeeByPeriod(email: string, pay_year: number, pay_period_no: number) { //find period using year and period_no const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no } }); if (!period) throw new NotFoundException(`Pay period ${pay_year}-${pay_period_no} not found`); + //fetch the employee_id using the email + const employee_id = await this.emailResolver.findIdByEmail(email); //loads the timesheets related to the fetched pay-period const timesheet_range = { employee_id, start_date: { gte: period.period_start, lte: period.period_end } }; let rows = await this.loadTimesheets(timesheet_range); diff --git a/src/time-and-attendance/time-tracker/timesheets/timesheets.module.ts b/src/time-and-attendance/time-tracker/timesheets/timesheets.module.ts new file mode 100644 index 0000000..761a78e --- /dev/null +++ b/src/time-and-attendance/time-tracker/timesheets/timesheets.module.ts @@ -0,0 +1,20 @@ + +import { Module } from '@nestjs/common'; +import { TimesheetController } from 'src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller'; +import { TimesheetApprovalService } from 'src/time-and-attendance/time-tracker/timesheets/services/timesheet-approval.service'; +import { TimesheetArchiveService } from 'src/time-and-attendance/time-tracker/timesheets/services/timesheet-archive.service'; +import { GetTimesheetsOverviewService } from 'src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service'; +import { EmailToIdResolver } from 'src/time-and-attendance/utils/resolve-email-id.utils'; + +@Module({ + + controllers: [TimesheetController], + providers: [ + TimesheetArchiveService, + GetTimesheetsOverviewService, + TimesheetApprovalService, + EmailToIdResolver, + ], + exports: [], +}) +export class TimesheetsModule {} diff --git a/src/time-and-attendance/utils/constants.utils.ts b/src/time-and-attendance/utils/constants.utils.ts index e2f20fc..2a53b28 100644 --- a/src/time-and-attendance/utils/constants.utils.ts +++ b/src/time-and-attendance/utils/constants.utils.ts @@ -6,6 +6,8 @@ export const ANCHOR_ISO = '2023-12-17'; export const PERIOD_DAYS = 14; export const PERIODS_PER_YEAR = 26; export const MS_PER_DAY = 86_400_000; +export const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000; + //REGEX CONSTANTS export const DATE_ISO_FORMAT = /^\d{4}-\d{2}-\d{2}$/; diff --git a/src/time-and-attendance/utils/date-time.utils.ts b/src/time-and-attendance/utils/date-time.utils.ts index 463b26a..488bb7a 100644 --- a/src/time-and-attendance/utils/date-time.utils.ts +++ b/src/time-and-attendance/utils/date-time.utils.ts @@ -1,3 +1,4 @@ +import { BadRequestException } from "@nestjs/common"; import { ANCHOR_ISO, MS_PER_DAY, PERIODS_PER_YEAR, PERIOD_DAYS } from "src/time-and-attendance/utils/constants.utils"; //ensures the week starts from sunday @@ -88,4 +89,38 @@ export function listPayYear(pay_year: number, anchorISO = ANCHOR_ISO) { } export const overlaps = (a: { start: Date; end: Date }, b: { start: Date; end: Date }) => - !(a.end <= b.start || a.start >= b.end); \ No newline at end of file + !(a.end <= b.start || a.start >= b.end); + + +export const hhmmFromLocal = (d: Date) => + `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; + +export const toDateOnly = (s: string): Date => { + if (/^\d{4}-\d{2}-\d{2}$/.test(s)) { + const y = Number(s.slice(0,4)); + const m = Number(s.slice(5,7)) - 1; + const d = Number(s.slice(8,10)); + return new Date(y, m, d, 0, 0, 0, 0); + } + const dt = new Date(s); + if (Number.isNaN(dt.getTime())) throw new BadRequestException(`Invalid date: ${s}`); + return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), 0,0,0,0); +}; + +// export const toStringFromDate = (d: Date) => +// `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; + + +export const toISOtoDateOnly = (iso: string): Date => { + const date = new Date(iso); + if (Number.isNaN(date.getTime())) { + throw new BadRequestException(`Invalid date: ${iso}`); + } + date.setHours(0, 0, 0, 0); + return date; +}; + +export const toISODateKey = (date: Date): string => date.toISOString().slice(0, 10); + +export const normalizeDates = (dates: string[]): string[] => + Array.from(new Set(dates.map((iso) => toISODateKey(toISOtoDateOnly(iso))))); \ No newline at end of file diff --git a/src/time-and-attendance/modules/shared/utils/resolve-bank-type-id.utils.ts b/src/time-and-attendance/utils/resolve-bank-type-id.utils.ts similarity index 100% rename from src/time-and-attendance/modules/shared/utils/resolve-bank-type-id.utils.ts rename to src/time-and-attendance/utils/resolve-bank-type-id.utils.ts diff --git a/src/time-and-attendance/modules/shared/utils/resolve-email-id.utils.ts b/src/time-and-attendance/utils/resolve-email-id.utils.ts similarity index 100% rename from src/time-and-attendance/modules/shared/utils/resolve-email-id.utils.ts rename to src/time-and-attendance/utils/resolve-email-id.utils.ts diff --git a/src/time-and-attendance/modules/shared/utils/resolve-full-name.utils.ts b/src/time-and-attendance/utils/resolve-full-name.utils.ts similarity index 100% rename from src/time-and-attendance/modules/shared/utils/resolve-full-name.utils.ts rename to src/time-and-attendance/utils/resolve-full-name.utils.ts diff --git a/src/time-and-attendance/modules/shared/utils/resolve-shifts-id.utils.ts b/src/time-and-attendance/utils/resolve-shifts-id.utils.ts similarity index 93% rename from src/time-and-attendance/modules/shared/utils/resolve-shifts-id.utils.ts rename to src/time-and-attendance/utils/resolve-shifts-id.utils.ts index 4d9d313..e76d144 100644 --- a/src/time-and-attendance/modules/shared/utils/resolve-shifts-id.utils.ts +++ b/src/time-and-attendance/utils/resolve-shifts-id.utils.ts @@ -1,7 +1,7 @@ import { Prisma, PrismaClient } from "@prisma/client"; import { NotFoundException } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; -import { ShiftKey } from "../interfaces/shifts.interface"; +import { ShiftKey } from "src/time-and-attendance/utils/type.utils"; type Tx = Prisma.TransactionClient | PrismaClient; diff --git a/src/time-and-attendance/modules/shared/utils/resolve-timesheet.utils.ts b/src/time-and-attendance/utils/resolve-timesheet.utils.ts similarity index 100% rename from src/time-and-attendance/modules/shared/utils/resolve-timesheet.utils.ts rename to src/time-and-attendance/utils/resolve-timesheet.utils.ts diff --git a/src/time-and-attendance/utils/selects.utils.ts b/src/time-and-attendance/utils/selects.utils.ts index 3dbf127..fd4a356 100644 --- a/src/time-and-attendance/utils/selects.utils.ts +++ b/src/time-and-attendance/utils/selects.utils.ts @@ -47,3 +47,34 @@ export const leaveRequestsSelect = { } }, } satisfies Prisma.LeaveRequestsSelect; + + +export const EXPENSE_SELECT = { + date: true, + amount: true, + mileage: true, + comment: true, + is_approved: true, + supervisor_comment: true, + bank_code: { select: { type: true } }, +} as const; + +export const EXPENSE_ASC_ORDER = { date: 'asc' as const }; + +export const PAY_PERIOD_SELECT = { + period_start: true, + period_end: true, +} as const; + +export const SHIFT_SELECT = { + date: true, + start_time: true, + end_time: true, + comment: true, + is_approved: true, + is_remote: true, + bank_code: {select: { type: true } }, +} as const; + +export const SHIFT_ASC_ORDER = [{date: 'asc' as const}, {start_time: 'asc' as const}]; + diff --git a/src/time-and-attendance/utils/type.utils.ts b/src/time-and-attendance/utils/type.utils.ts index 0264f3f..412566a 100644 --- a/src/time-and-attendance/utils/type.utils.ts +++ b/src/time-and-attendance/utils/type.utils.ts @@ -1,12 +1,12 @@ -import { WeekOvertimeSummary } from "src/time-and-attendance/domains/services/overtime.service"; +import { Prisma, PrismaClient } from "@prisma/client"; +import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto"; +import { updateExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-update.dto"; +import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; +import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto"; +import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto"; +import { UpdateShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto"; import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; -import { UpdateShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-update.dto"; -import { GetShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-get.dto"; -import { ShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-create.dto"; -import { Prisma } from "@prisma/client"; -import { SchedulePresetsDto } from "src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/create-schedule-presets.dto"; -import { GetExpenseDto } from "src/time-and-attendance/modules/expenses/dtos/expense-get.dto"; -import { updateExpenseDto } from "src/time-and-attendance/modules/expenses/dtos/expense-update.dto"; + export type TotalHours = { regular: number; @@ -79,4 +79,32 @@ export type ApplyResult = { export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>; -export type UpsertAction = 'create' | 'update' | 'delete'; \ No newline at end of file +export type UpsertAction = 'create' | 'update' | 'delete'; + +export type Tx = Prisma.TransactionClient | PrismaClient; + +export type WeekOvertimeSummary = { + week_start:string; + week_end: string; + week_total_hours: number; + weekly_overtime: number; + daily_overtime_kept: number; + total_overtime: number; + breakdown: Array<{ + date:string; + day_hours: number; + day_overtime: number; + daily_kept: number; + running_total_before: number; + }>; +}; + +export interface ShiftKey { + timesheet_id: number; + date: Date; + start_time: Date; + end_time: Date; + bank_code_id: number; + is_remote: boolean; + comment?: string | null; +} \ No newline at end of file