Merge branch 'main' of git.targo.ca:Targo/targo_backend

This commit is contained in:
Nicolas Drolet 2025-11-03 15:43:58 -05:00
commit fef9ea0b74
95 changed files with 925 additions and 1350 deletions

View File

@ -91,30 +91,20 @@
"parameters": [ "parameters": [
{ {
"name": "date", "name": "date",
"required": false, "required": true,
"in": "query", "in": "query",
"description": "Override for resolving the current period",
"schema": { "schema": {
"example": "2025-08-11",
"type": "string" "type": "string"
} }
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Find current and all pay periods", "description": ""
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriodBundleDto"
}
}
}
} }
}, },
"summary": "Return current pay period and the full list",
"tags": [ "tags": [
"pay-periods" "PayPeriods"
] ]
} }
}, },
@ -133,22 +123,11 @@
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Pay period found for the selected date", "description": ""
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriodDto"
}
}
}
},
"404": {
"description": "Pay period not found for the selected date"
} }
}, },
"summary": "Resolve a period by a date within it",
"tags": [ "tags": [
"pay-periods" "PayPeriods"
] ]
} }
}, },
@ -161,7 +140,6 @@
"required": true, "required": true,
"in": "path", "in": "path",
"schema": { "schema": {
"example": 2024,
"type": "number" "type": "number"
} }
}, },
@ -169,35 +147,46 @@
"name": "periodNumber", "name": "periodNumber",
"required": true, "required": true,
"in": "path", "in": "path",
"description": "1..26",
"schema": { "schema": {
"example": 1,
"type": "number" "type": "number"
} }
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Pay period found", "description": ""
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriodDto"
}
}
}
},
"404": {
"description": "Pay period not found"
} }
}, },
"summary": "Find pay period by year and period number",
"tags": [ "tags": [
"pay-periods" "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": { "get": {
"operationId": "PayPeriodsController_getCrewOverview", "operationId": "PayPeriodsController_getCrewOverview",
"parameters": [ "parameters": [
@ -206,7 +195,6 @@
"required": true, "required": true,
"in": "path", "in": "path",
"schema": { "schema": {
"example": 2024,
"type": "number" "type": "number"
} }
}, },
@ -214,49 +202,18 @@
"name": "periodNumber", "name": "periodNumber",
"required": true, "required": true,
"in": "path", "in": "path",
"description": "1..26",
"schema": { "schema": {
"example": 1,
"type": "number" "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": { "responses": {
"200": { "200": {
"description": "Crew overview", "description": ""
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriodOverviewDto"
}
}
}
},
"404": {
"description": "Pay period not found"
} }
}, },
"summary": "Supervisor crew overview for a given pay period",
"tags": [ "tags": [
"pay-periods" "PayPeriods"
] ]
} }
}, },
@ -269,7 +226,6 @@
"required": true, "required": true,
"in": "path", "in": "path",
"schema": { "schema": {
"example": 2024,
"type": "number" "type": "number"
} }
}, },
@ -277,31 +233,49 @@
"name": "periodNumber", "name": "periodNumber",
"required": true, "required": true,
"in": "path", "in": "path",
"description": "1..26",
"schema": { "schema": {
"example": 1,
"type": "number" "type": "number"
} }
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Pay period overview found", "description": ""
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PayPeriodOverviewDto"
}
}
}
},
"404": {
"description": "Pay period not found"
} }
}, },
"summary": "Detailed view of a pay period by year + number",
"tags": [ "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": { "/shift/create": {
"post": { "post": {
"operationId": "ShiftController_createBatch", "operationId": "ShiftController_createBatch",
@ -454,19 +389,10 @@
] ]
} }
}, },
"/schedule-presets/create/{employee_id}": { "/schedule-presets/create": {
"post": { "post": {
"operationId": "SchedulePresetsController_createPreset", "operationId": "SchedulePresetsController_createPreset",
"parameters": [ "parameters": [],
{
"name": "employee_id",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": { "requestBody": {
"required": true, "required": true,
"content": { "content": {
@ -543,19 +469,10 @@
] ]
} }
}, },
"/schedule-presets/find/{employee_id}": { "/schedule-presets/find-list": {
"get": { "get": {
"operationId": "SchedulePresetsController_findListById", "operationId": "SchedulePresetsController_findListById",
"parameters": [ "parameters": [],
{
"name": "employee_id",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "" "description": ""
@ -566,18 +483,10 @@
] ]
} }
}, },
"/schedule-presets/apply-presets/{employee_id}": { "/schedule-presets/apply-presets": {
"post": { "post": {
"operationId": "SchedulePresetsController_applyPresets", "operationId": "SchedulePresetsController_applyPresets",
"parameters": [ "parameters": [
{
"name": "employee_id",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{ {
"name": "preset", "name": "preset",
"required": true, "required": true,
@ -605,19 +514,10 @@
] ]
} }
}, },
"/expense/{timesheet_id}": { "/expense/create": {
"post": { "post": {
"operationId": "ExpenseController_create", "operationId": "ExpenseController_create",
"parameters": [ "parameters": [],
{
"name": "timesheet_id",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": { "requestBody": {
"required": true, "required": true,
"content": { "content": {
@ -638,7 +538,7 @@
] ]
} }
}, },
"/expense": { "/expense/update": {
"patch": { "patch": {
"operationId": "ExpenseController_update", "operationId": "ExpenseController_update",
"parameters": [], "parameters": [],
@ -652,7 +552,7 @@
] ]
} }
}, },
"/expense/{expense_id}": { "/expense/delete/{expense_id}": {
"delete": { "delete": {
"operationId": "ExpenseController_remove", "operationId": "ExpenseController_remove",
"parameters": [ "parameters": [
@ -733,167 +633,9 @@
} }
}, },
"schemas": { "schemas": {
"PayPeriodDto": { "BulkCrewApprovalDto": {
"type": "object", "type": "object",
"properties": { "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 (126)"
},
"pay_year": {
"type": "number",
"example": 2023,
"description": "Calendar year of the period"
},
"period_start": {
"type": "string",
"example": "2023-12-17",
"format": "date",
"description": "Period start date (YYYY-MM-DD)"
},
"period_end": {
"type": "string",
"example": "2023-12-30",
"format": "date",
"description": "Period end date (YYYY-MM-DD)"
},
"payday": {
"type": "string",
"example": "2023-12-30",
"format": "date",
"description": "Period pay day(YYYY-MM-DD)"
},
"label": {
"type": "string",
"example": "2023-12-17 → 2023-12-30",
"description": "Human-readable label"
},
"employees_overview": {
"description": "Per-employee overview for the period",
"type": "array",
"items": {
"$ref": "#/components/schemas/EmployeePeriodOverviewDto"
}
}
},
"required": [
"pay_period_no",
"pay_year",
"period_start",
"period_end",
"payday",
"label",
"employees_overview"
]
}, },
"PreferencesDto": { "PreferencesDto": {
"type": "object", "type": "object",

49
package-lock.json generated
View File

@ -243,6 +243,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
@ -3113,6 +3114,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@ -3271,6 +3273,7 @@
"version": "11.1.7", "version": "11.1.7",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.7.tgz", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.7.tgz",
"integrity": "sha512-lwlObwGgIlpXSXYOTpfzdCepUyWomz6bv9qzGzzvpgspUxkj0Uz0fUJcvD44V8Ps7QhKW3lZBoYbXrH25UZrbA==", "integrity": "sha512-lwlObwGgIlpXSXYOTpfzdCepUyWomz6bv9qzGzzvpgspUxkj0Uz0fUJcvD44V8Ps7QhKW3lZBoYbXrH25UZrbA==",
"peer": true,
"dependencies": { "dependencies": {
"file-type": "21.0.0", "file-type": "21.0.0",
"iterare": "1.2.1", "iterare": "1.2.1",
@ -3316,6 +3319,7 @@
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.7.tgz", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.7.tgz",
"integrity": "sha512-TyXFOwjhHv/goSgJ8i20K78jwTM0iSpk9GBcC2h3mf4MxNy+znI8m7nWjfoACjTkb89cTwDQetfTHtSfGLLaiA==", "integrity": "sha512-TyXFOwjhHv/goSgJ8i20K78jwTM0iSpk9GBcC2h3mf4MxNy+znI8m7nWjfoACjTkb89cTwDQetfTHtSfGLLaiA==",
"hasInstallScript": true, "hasInstallScript": true,
"peer": true,
"dependencies": { "dependencies": {
"@nuxt/opencollective": "0.4.1", "@nuxt/opencollective": "0.4.1",
"fast-safe-stringify": "2.1.1", "fast-safe-stringify": "2.1.1",
@ -3395,6 +3399,7 @@
"version": "11.1.7", "version": "11.1.7",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.7.tgz", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.7.tgz",
"integrity": "sha512-5T+GLdvTiGPKB4/P4PM9ftKUKNHJy8ThEFhZA3vQnXVL7Vf0rDr07TfVTySVu+XTh85m1lpFVuyFM6u6wLNsRA==", "integrity": "sha512-5T+GLdvTiGPKB4/P4PM9ftKUKNHJy8ThEFhZA3vQnXVL7Vf0rDr07TfVTySVu+XTh85m1lpFVuyFM6u6wLNsRA==",
"peer": true,
"dependencies": { "dependencies": {
"cors": "2.8.5", "cors": "2.8.5",
"express": "5.1.0", "express": "5.1.0",
@ -3794,6 +3799,7 @@
"resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.6.0.tgz", "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.6.0.tgz",
"integrity": "sha512-Q5FsI3Cw0fGMXhmsg7c08i4EmXCrcl+WnAxb6LYOLHw4JFFC3yzmx9LaXZ7QMbA+JZXbigU2TirI7RAfO0Qlnw==", "integrity": "sha512-Q5FsI3Cw0fGMXhmsg7c08i4EmXCrcl+WnAxb6LYOLHw4JFFC3yzmx9LaXZ7QMbA+JZXbigU2TirI7RAfO0Qlnw==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@swc/counter": "^0.1.3", "@swc/counter": "^0.1.3",
"@xhmikosr/bin-wrapper": "^13.0.5", "@xhmikosr/bin-wrapper": "^13.0.5",
@ -3862,6 +3868,7 @@
"integrity": "sha512-CJSn2vstd17ddWIHBsjuD4OQnn9krQfaq6EO+w9YfId5DKznyPmzxAARlOXG99cC8/3Kli8ysKy6phL43bSr0w==", "integrity": "sha512-CJSn2vstd17ddWIHBsjuD4OQnn9krQfaq6EO+w9YfId5DKznyPmzxAARlOXG99cC8/3Kli8ysKy6phL43bSr0w==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"peer": true,
"dependencies": { "dependencies": {
"@swc/counter": "^0.1.3", "@swc/counter": "^0.1.3",
"@swc/types": "^0.1.23" "@swc/types": "^0.1.23"
@ -4198,6 +4205,7 @@
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@types/estree": "*", "@types/estree": "*",
"@types/json-schema": "*" "@types/json-schema": "*"
@ -4357,6 +4365,7 @@
"version": "22.17.2", "version": "22.17.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.2.tgz",
"integrity": "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==", "integrity": "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
@ -4537,6 +4546,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz",
"integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/types": "8.37.0", "@typescript-eslint/types": "8.37.0",
@ -5440,6 +5450,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -5452,7 +5463,6 @@
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": ">=10.13.0" "node": ">=10.13.0"
}, },
@ -5486,6 +5496,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@ -5953,6 +5964,7 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"peer": true,
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001726", "caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173", "electron-to-chromium": "^1.5.173",
@ -6231,6 +6243,7 @@
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"devOptional": true, "devOptional": true,
"peer": true,
"dependencies": { "dependencies": {
"readdirp": "^4.0.1" "readdirp": "^4.0.1"
}, },
@ -6283,12 +6296,14 @@
"node_modules/class-transformer": { "node_modules/class-transformer": {
"version": "0.5.1", "version": "0.5.1",
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "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": { "node_modules/class-validator": {
"version": "0.14.2", "version": "0.14.2",
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz",
"integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==",
"peer": true,
"dependencies": { "dependencies": {
"@types/validator": "^13.11.8", "@types/validator": "^13.11.8",
"libphonenumber-js": "^1.11.1", "libphonenumber-js": "^1.11.1",
@ -7151,6 +7166,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@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", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz",
"integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==",
"dev": true, "dev": true,
"peer": true,
"bin": { "bin": {
"eslint-config-prettier": "bin/cli.js" "eslint-config-prettier": "bin/cli.js"
}, },
@ -8717,6 +8734,7 @@
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@jest/core": "^29.7.0", "@jest/core": "^29.7.0",
"@jest/types": "^29.6.3", "@jest/types": "^29.6.3",
@ -10353,6 +10371,7 @@
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
"peer": true,
"dependencies": { "dependencies": {
"passport-strategy": "1.x.x", "passport-strategy": "1.x.x",
"pause": "0.0.1", "pause": "0.0.1",
@ -10646,6 +10665,7 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true, "dev": true,
"peer": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@ -10700,6 +10720,7 @@
"integrity": "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==", "integrity": "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"peer": true,
"dependencies": { "dependencies": {
"@prisma/config": "6.18.0", "@prisma/config": "6.18.0",
"@prisma/engines": "6.18.0" "@prisma/engines": "6.18.0"
@ -10919,7 +10940,8 @@
"node_modules/reflect-metadata": { "node_modules/reflect-metadata": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "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": { "node_modules/repeat-string": {
"version": "1.6.1", "version": "1.6.1",
@ -11108,6 +11130,7 @@
"version": "7.8.2", "version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
@ -11917,6 +11940,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@ -12225,6 +12249,7 @@
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@cspotcode/source-map-support": "^0.8.0", "@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7", "@tsconfig/node10": "^1.0.7",
@ -12382,6 +12407,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true, "devOptional": true,
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -12564,9 +12590,10 @@
} }
}, },
"node_modules/validator": { "node_modules/validator": {
"version": "13.15.15", "version": "13.15.20",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz",
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.10" "node": ">= 0.10"
} }
@ -12711,7 +12738,6 @@
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"ajv": "^8.0.0" "ajv": "^8.0.0"
}, },
@ -12729,7 +12755,6 @@
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3" "fast-deep-equal": "^3.1.3"
}, },
@ -12742,7 +12767,6 @@
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"esrecurse": "^4.3.0", "esrecurse": "^4.3.0",
"estraverse": "^4.1.1" "estraverse": "^4.1.1"
@ -12756,7 +12780,6 @@
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": ">=4.0" "node": ">=4.0"
} }
@ -12765,15 +12788,13 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true, "dev": true
"peer": true
}, },
"node_modules/webpack/node_modules/mime-db": { "node_modules/webpack/node_modules/mime-db": {
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@ -12783,7 +12804,6 @@
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"mime-db": "1.52.0" "mime-db": "1.52.0"
}, },
@ -12796,7 +12816,6 @@
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@types/json-schema": "^7.0.9", "@types/json-schema": "^7.0.9",
"ajv": "^8.9.0", "ajv": "^8.9.0",

View File

@ -14,8 +14,8 @@ import { ConfigModule } from '@nestjs/config';
import { APP_FILTER, APP_PIPE } from '@nestjs/core'; import { APP_FILTER, APP_PIPE } from '@nestjs/core';
import { HttpExceptionFilter } from './common/filters/http-exception.filter'; import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { ValidationError } from 'class-validator'; 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 { TimeAndAttendanceModule } from 'src/time-and-attendance/time-and-attendance.module';
import { PayperiodsModule } from 'src/time-and-attendance/pay-period/pay-periods.module';
@Module({ @Module({
imports: [ imports: [

View File

@ -1,10 +1,9 @@
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { CsvExportController } from "./controllers/csv-exports.controller"; import { CsvExportController } from "./controllers/csv-exports.controller";
import { CsvExportService } from "./services/csv-exports.service"; import { CsvExportService } from "./services/csv-exports.service";
import { SharedModule } from "../../time-and-attendance/modules/shared/shared.module";
@Module({ @Module({
providers:[CsvExportService, SharedModule], providers:[CsvExportService],
controllers: [CsvExportController], controllers: [CsvExportController],
}) })
export class CsvExportModule {} export class CsvExportModule {}

View File

@ -4,15 +4,18 @@ import { VacationService } from "./services/vacation.service";
import { HolidayService } from "./services/holiday.service"; import { HolidayService } from "./services/holiday.service";
import { MileageService } from "./services/mileage.service"; import { MileageService } from "./services/mileage.service";
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
@Module({ @Module({
imports:[],
providers: [ providers: [
HolidayService, HolidayService,
MileageService, MileageService,
OvertimeService, OvertimeService,
SickLeaveService, SickLeaveService,
VacationService VacationService,
EmailToIdResolver,
], ],
exports: [ exports: [
HolidayService, HolidayService,

View File

@ -1,9 +1,8 @@
import { Injectable, Logger, NotFoundException } from "@nestjs/common"; import { Injectable, Logger, NotFoundException } from "@nestjs/common";
import { computeHours, getWeekStart } from "src/common/utils/date-utils"; import { computeHours, getWeekStart } from "src/common/utils/date-utils";
import { PrismaService } from "../../../prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000; 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. 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 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 { export class HolidayService {
private readonly logger = new Logger(HolidayService.name); private readonly logger = new Logger(HolidayService.name);
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
//fetch employee_id by email private readonly emailResolver: EmailToIdResolver,
private async resolveEmployeeByEmail(email: string): Promise<number> { ) {}
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;
}
private async computeHoursPrevious4WeeksByEmail(email: string, holiday_date: Date): Promise<number> { private async computeHoursPrevious4WeeksByEmail(email: string, holiday_date: Date): Promise<number> {
const employee_id = await this.resolveEmployeeByEmail(email); const employee_id = await this.emailResolver.findIdByEmail(email);
return this.computeHoursPrevious4Weeks(employee_id, holiday_date); return this.computeHoursPrevious4Weeks(employee_id, holiday_date);
} }
private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise<number> { private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise<number> {
const holiday_week_start = getWeekStart(holiday_date); 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 window_end = new Date(holiday_week_start.getTime() - 1);
const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G700']; const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G700'];
@ -60,7 +50,7 @@ export class HolidayService {
let capped_total = 0; let capped_total = 0;
for(let offset = 1; offset <= 4; offset++) { 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 key = week_start.getTime();
const weekly_hours = hours_by_week.get(key) ?? 0; const weekly_hours = hours_by_week.get(key) ?? 0;
capped_total += Math.min(weekly_hours, 40); capped_total += Math.min(weekly_hours, 40);

View File

@ -1,5 +1,5 @@
import { BadRequestException, Injectable, Logger } from "@nestjs/common"; import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { PrismaService } from '../../../prisma/prisma.service'; import { PrismaService } from "src/prisma/prisma.service";
@Injectable() @Injectable()
export class MileageService { export class MileageService {

View File

@ -1,32 +1,15 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils'; 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() @Injectable()
export class OvertimeService { export class OvertimeService {
private logger = new Logger(OvertimeService.name); 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 private INCLUDED_TYPES = ['EMERGENCY', 'EVENING','OVERTIME','REGULAR'] as const; // included types for weekly overtime calculation
constructor(private prisma: PrismaService) {} 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 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 running = 0;
let daily_kept_sum = 0; let daily_kept_sum = 0;
@ -69,9 +52,9 @@ export class OvertimeService {
for (const key of days) { for (const key of days) {
const day_hours = day_totals.get(key) ?? 0; 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); const daily_kept = Math.min(day_overtime, cap_before_40);
breakdown.push({ breakdown.push({
@ -104,144 +87,4 @@ export class OvertimeService {
breakdown, breakdown,
}; };
} }
// //calculate daily overtime
// async getDailyOvertimeHours(timesheet_id: number, date: Date): Promise<number> {
// 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<number> {
// 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<void> {
// //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`);
// }
} }

View File

@ -1,6 +1,6 @@
import { getYearStart, roundToQuarterHour } from "src/common/utils/date-utils"; import { getYearStart, roundToQuarterHour } from "src/common/utils/date-utils";
import { Injectable, Logger } from "@nestjs/common"; import { Injectable, Logger } from "@nestjs/common";
import { PrismaService } from "../../../prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
@Injectable() @Injectable()
export class SickLeaveService { export class SickLeaveService {

View File

@ -1,5 +1,5 @@
import { Injectable, Logger, NotFoundException } from "@nestjs/common"; import { Injectable, Logger, NotFoundException } from "@nestjs/common";
import { PrismaService } from "../../../prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
@Injectable() @Injectable()
export class VacationService { export class VacationService {

View File

@ -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<CreateExpenseResult>{
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<UpdateExpenseResult>{
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);
}
}

View File

@ -1,5 +1,5 @@
import { OmitType, PartialType } from "@nestjs/swagger"; 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 ( export class updateExpenseDto extends PartialType (
OmitType(ExpenseDto, ['is_approved', 'timesheet_id'] as const) OmitType(ExpenseDto, ['is_approved', 'timesheet_id'] as const)

View File

@ -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 {}

View File

@ -1,34 +1,49 @@
import { CreateExpenseResult, UpdateExpensePayload, UpdateExpenseResult, DeleteExpenseResult, NormalizedExpense } from "src/time-and-attendance/utils/type.utils"; 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 { Injectable, NotFoundException } from "@nestjs/common";
import { expense_select } from "src/time-and-attendance/utils/selects.utils"; import { expense_select } from "src/time-and-attendance/utils/selects.utils";
import { PrismaService } from "src/prisma/prisma.service"; 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/expenses/dtos/expense-create.dto";
import { ExpenseDto } from "src/time-and-attendance/modules/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() @Injectable()
export class ExpenseUpsertService { export class ExpenseUpsertService {
constructor(private readonly prisma: PrismaService) { } constructor(
private readonly prisma: PrismaService,
private readonly emailResolver: EmailToIdResolver,
) { }
//_________________________________________________________________ //_________________________________________________________________
// CREATE // CREATE
//_________________________________________________________________ //_________________________________________________________________
async createExpense(timesheet_id: number, dto: ExpenseDto): Promise<CreateExpenseResult> { async createExpense( dto: ExpenseDto, email: string): Promise<CreateExpenseResult> {
try { try {
//fetch employee_id using req.user.email
const employee_id = await this.emailResolver.findIdByEmail(email);
//normalize strings and dates //normalize strings and dates
const normed_expense = this.normalizeExpenseDto(dto); const normed_expense = this.normalizeExpenseDto(dto);
//finds the timesheet using expense.date
const start_date = weekStartSunday(normed_expense.date);
//parse numbers //parse numbers
const parsed_amount = this.parseOptionalNumber(dto.amount, "amount"); const parsed_amount = this.parseOptionalNumber(dto.amount, "amount");
const parsed_mileage = this.parseOptionalNumber(dto.mileage, "mileage"); const parsed_mileage = this.parseOptionalNumber(dto.mileage, "mileage");
const parsed_attachment = this.parseOptionalNumber(dto.attachment, "attachment"); 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 //create a new expense
const expense = await this.prisma.expenses.create({ const expense = await this.prisma.expenses.create({
data: { data: {
timesheet_id, timesheet_id: timesheet.id,
bank_code_id: dto.bank_code_id, bank_code_id: dto.bank_code_id,
attachment: parsed_attachment, attachment: parsed_attachment,
date: normed_expense.date, date: normed_expense.date,

View File

@ -1,8 +1,7 @@
import { Body, Controller, Post } from "@nestjs/common"; import { Body, Controller, Post } from "@nestjs/common";
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import { LeaveRequestsService } from "../services/leave-request.service";
import { UpsertLeaveRequestDto } from "../dtos/upsert-leave-request.dto"; import { UpsertLeaveRequestDto } from "../dtos/upsert-leave-request.dto";
import { LeaveTypes } from "@prisma/client"; import { LeaveRequestsService } from "../services/leave-request.service";
@ApiTags('Leave Requests') @ApiTags('Leave Requests')
@ApiBearerAuth('access-token') @ApiBearerAuth('access-token')
@ -15,16 +14,7 @@ export class LeaveRequestController {
async upsertLeaveRequest(@Body() dto: UpsertLeaveRequestDto) { async upsertLeaveRequest(@Body() dto: UpsertLeaveRequestDto) {
const { action, leave_requests } = await this.leave_service.handle(dto); const { action, leave_requests } = await this.leave_service.handle(dto);
return { action, leave_requests }; return { action, leave_requests };
}q }
//TODO:
/*
@Get('archive')
findAllArchived(){...}
@Get('archive/:id')
findOneArchived(id){...}
*/
} }

View File

@ -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 {}

View File

@ -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 { 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; const toNum = (value?: Prisma.Decimal | null) => value ? Number(value) : undefined;

View File

@ -1,15 +1,16 @@
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; 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 { 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 { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
import { LeaveRequestsUtils } from "src/time-and-attendance/modules/leave-requests/utils/leave-request.util"; import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
import { BankCodesResolver } from "src/time-and-attendance/modules/shared/utils/resolve-bank-type-id.utils"; import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util";
import { EmailToIdResolver } from "src/time-and-attendance/modules/shared/utils/resolve-email-id.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 { HolidayService } from "src/time-and-attendance/domains/services/holiday.service"; import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service";
import { PrismaService } from "src/prisma/prisma.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() @Injectable()

View File

@ -1,18 +1,18 @@
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; 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 { 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 { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
import { mapRowToView } from "../mappers/leave-requests.mapper"; import { roundToQuarterHour } from "src/common/utils/date-utils";
import { PrismaService } from "src/prisma/prisma.service"; import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util";
import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service"; 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 { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service";
import { VacationService } from "src/time-and-attendance/domains/services/vacation.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 { HolidayService } from "src/time-and-attendance/domains/services/holiday.service";
import { BankCodesResolver } from "src/time-and-attendance/modules/shared/utils/resolve-bank-type-id.utils"; import { PrismaService } from "src/prisma/prisma.service";
import { EmailToIdResolver } from "src/time-and-attendance/modules/shared/utils/resolve-email-id.utils"; import { mapRowToView } from "../mappers/leave-requests.mapper";
import { LeaveRequestsUtils } from "src/time-and-attendance/modules/leave-requests/utils/leave-request.util"; import { normalizeDates, toDateOnly, toISODateKey } from "src/time-and-attendance/utils/date-time.utils";
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
@Injectable() @Injectable()
export class LeaveRequestsService { export class LeaveRequestsService {
constructor( constructor(

View File

@ -1,15 +1,16 @@
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; 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 { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
import { roundToQuarterHour } from "src/common/utils/date-utils"; import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
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 { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils"; 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() @Injectable()

View File

@ -1,15 +1,15 @@
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common"; 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 { 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 { 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() @Injectable()

View File

@ -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!);
}

View File

@ -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<CreateExpenseResult>{
return this.upsert_service.createExpense(timesheet_id, dto);
}
@Patch()
update(
@Body() body: { update :{ id: number; dto: updateExpenseDto }}): Promise<UpdateExpenseResult>{
return this.upsert_service.updateExpense(body.update);
}
@Delete(':expense_id')
remove(@Param('expense_id') expense_id: number) {
return this.upsert_service.deleteExpense(expense_id);
}
}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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!);
}

View File

@ -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<PayPeriodBundleDto> {
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<PayPeriodOverviewDto> {
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<PayPeriodOverviewDto> {
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<PayPeriodDto[]> {
// return this.queryService.findAll();
// }
}

View File

@ -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[];
}

View File

@ -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;
}

View File

@ -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 (126)' })
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[];
}

View File

@ -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;
}

View File

@ -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 {}

View File

@ -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<string, {period_start: Date, period_end: Date}>();
// 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};
// }
// }

View File

@ -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)))));

View File

@ -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;
}

View File

@ -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 };

View File

@ -1,4 +0,0 @@
export const PAY_PERIOD_SELECT = {
period_start: true,
period_end: true,
} as const;

View File

@ -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}];

View File

@ -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 {}

View File

@ -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);
}
}

View File

@ -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{}

View File

@ -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<CreateShiftResult[]> {
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<UpdateShiftResult[]>{
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);
}
}

View File

@ -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 {}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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<PayPeriodBundleDto> {
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<PayPeriodOverviewDto> {
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<PayPeriodOverviewDto> {
return this.queryService.getOverviewByYearPeriod(year, period_no);
}
}

View File

@ -1,5 +1,5 @@
import { Type } from "class-transformer"; 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 { export class BulkCrewApprovalItemDto {
@IsInt() @IsInt()
@ -16,9 +16,6 @@ export class BulkCrewApprovalItemDto {
} }
export class BulkCrewApprovalDto { export class BulkCrewApprovalDto {
@IsEmail()
supervisor_email: string;
@IsBoolean() @IsBoolean()
include_subtree: boolean = false; include_subtree: boolean = false;

View File

@ -0,0 +1,6 @@
import { PayPeriodDto } from "./pay-period.dto";
export class PayPeriodBundleDto {
current: PayPeriodDto;
periods: PayPeriodDto[];
}

View File

@ -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;
}

View File

@ -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[];
}

View File

@ -0,0 +1,8 @@
export class PayPeriodDto {
pay_period_no: number;
period_start: string;
period_end: string;
payday: string;
pay_year: number;
label: string;
}

View File

@ -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 {}

View File

@ -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<string, {period_start: Date, period_end: Date}>();
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};
}
}

View File

@ -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 { 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({ @Module({
imports: [ imports: [
BusinessLogicsModule, BusinessLogicsModule,
PayperiodsModule, PayperiodsModule,
SharedModule,
], ],
controllers: [ controllers: [
TimesheetController, TimesheetController,
ShiftController, ShiftController,
SchedulePresetsController, SchedulePresetsController,
ExpenseController, ExpenseController,
], ],
@ -34,6 +35,8 @@ import { Module } from "@nestjs/common";
SchedulePresetsUpsertService, SchedulePresetsUpsertService,
SchedulePresetsGetService, SchedulePresetsGetService,
SchedulePresetsApplyService, SchedulePresetsApplyService,
EmailToIdResolver,
BankCodesResolver,
], ],
exports: [], exports: [],
}) export class TimeAndAttendanceModule{}; }) export class TimeAndAttendanceModule { };

View File

@ -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);
}
}

View File

@ -1,5 +1,5 @@
import { ArrayMinSize, IsArray, IsBoolean, IsOptional, IsString } from "class-validator"; 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 { export class SchedulePresetsDto {
@IsString() @IsString()

View File

@ -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{}

View File

@ -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 { 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({ @Module({
imports: [SharedModule],
controllers: [SchedulePresetsController], controllers: [SchedulePresetsController],
providers: [ providers: [
SchedulePresetsUpsertService, SchedulePresetsUpsertService,

View File

@ -4,20 +4,19 @@ import { DATE_ISO_FORMAT } from "src/time-and-attendance/utils/constants.utils";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { ApplyResult } from "src/time-and-attendance/utils/type.utils"; import { ApplyResult } from "src/time-and-attendance/utils/type.utils";
import { WEEKDAY } from "src/time-and-attendance/utils/mappers.utils"; import { WEEKDAY } from "src/time-and-attendance/utils/mappers.utils";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
@Injectable() @Injectable()
export class SchedulePresetsApplyService { export class SchedulePresetsApplyService {
constructor( private readonly prisma: PrismaService) {} constructor( private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver) {}
async applyToTimesheet( async applyToTimesheet( email: string, preset_name: string, start_date_iso: string ): Promise<ApplyResult> {
employee_id: number,
preset_name: string,
start_date_iso: string,
): Promise<ApplyResult> {
if(!preset_name?.trim()) throw new BadRequestException('A preset_name is required'); 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'); 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({ const preset = await this.prisma.schedulePresets.findFirst({
where: { employee_id, name: preset_name }, where: { employee_id, name: preset_name },
include: { include: {

View File

@ -2,13 +2,18 @@ import { PresetResponse, ShiftResponse } from "src/time-and-attendance/utils/typ
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
@Injectable() @Injectable()
export class SchedulePresetsGetService { export class SchedulePresetsGetService {
constructor( private readonly prisma: PrismaService ){} constructor(
private readonly prisma: PrismaService,
private readonly emailResolver: EmailToIdResolver,
){}
async getSchedulePresets(employee_id: number): Promise<PresetResponse[]> { async getSchedulePresets(email: string): Promise<PresetResponse[]> {
try { try {
const employee_id = await this.emailResolver.findIdByEmail(email);
const presets = await this.prisma.schedulePresets.findMany({ const presets = await this.prisma.schedulePresets.findMany({
where: { employee_id }, where: { employee_id },
orderBy: [{is_default: 'desc' }, { name: 'asc' }], orderBy: [{is_default: 'desc' }, { name: 'asc' }],

View File

@ -1,24 +1,28 @@
import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common"; import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common";
import { CreatePresetResult, DeletePresetResult, UpdatePresetResult } from "src/time-and-attendance/utils/type.utils"; 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 { Prisma, Weekday } from "@prisma/client";
import { toHHmmFromDate } from "src/time-and-attendance/utils/date-time.utils"; import { toHHmmFromDate } from "src/time-and-attendance/utils/date-time.utils";
import { PrismaService } from "src/prisma/prisma.service"; 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() @Injectable()
export class SchedulePresetsUpsertService { export class SchedulePresetsUpsertService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly typeResolver : BankCodesResolver, private readonly typeResolver : BankCodesResolver,
private readonly emailResolver: EmailToIdResolver,
){} ){}
//_________________________________________________________________ //_________________________________________________________________
// CREATE // CREATE
//_________________________________________________________________ //_________________________________________________________________
async createPreset( employee_id: number, dto: SchedulePresetsDto): Promise<CreatePresetResult> { async createPreset( email: string, dto: SchedulePresetsDto): Promise<CreatePresetResult> {
try { try {
const shifts_data = await this.resolveAndBuildPresetShifts(dto); 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)=> { await this.prisma.$transaction(async (tx)=> {
if(dto.is_default) { if(dto.is_default) {
await tx.schedulePresets.updateMany({ await tx.schedulePresets.updateMany({

View File

@ -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<CreateShiftResult[]> {
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<UpdateShiftResult[]>{
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);
}
}

View File

@ -1,6 +1,6 @@
import { PartialType, OmitType } from "@nestjs/swagger"; 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 { IsInt } from "class-validator";
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
export class UpdateShiftDto extends PartialType( export class UpdateShiftDto extends PartialType(
// allows update using ShiftDto and preventing OmitType variables to be modified // allows update using ShiftDto and preventing OmitType variables to be modified

View File

@ -2,7 +2,7 @@ import { toStringFromDate, toStringFromHHmm } from "src/time-and-attendance/util
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { shift_select } from "src/time-and-attendance/utils/selects.utils"; 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";
/** /**

View File

@ -1,12 +1,13 @@
import { CreateShiftResult, NormedOk, NormedErr, UpdateShiftResult, UpdateShiftPayload, UpdateShiftChanges, Normalized } from "src/time-and-attendance/utils/type.utils"; 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 { Injectable, BadRequestException, ConflictException, NotFoundException } from "@nestjs/common";
import { OvertimeService } from "src/time-and-attendance/domains/services/overtime.service"; 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 { 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 { 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( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly overtime: OvertimeService, private readonly overtime: OvertimeService,
private readonly emailResolver: EmailToIdResolver,
) { } ) { }
//_________________________________________________________________ //_________________________________________________________________
@ -25,76 +27,140 @@ export class ShiftsUpsertService {
//checks for overlaping shifts //checks for overlaping shifts
//create new shifts //create new shifts
//calculate overtime //calculate overtime
async createShifts(dtos: ShiftDto[]): Promise<CreateShiftResult[]> { async createShifts(email: string, dtos: ShiftDto[]): Promise<CreateShiftResult[]> {
if (!Array.isArray(dtos) || dtos.length === 0) return []; if (!Array.isArray(dtos) || dtos.length === 0) return [];
const normed_shift: Array<NormedOk | NormedErr> = dtos.map((dto, index) => { const employee_id = await this.emailResolver.findIdByEmail(email);
try {
const normed = this.normalizeShiftDto(dto); const normed_shifts = await Promise.all(
if (normed.end_time <= normed.start_time) { dtos.map(async (dto, index) => {
return { index, error: new BadRequestException(`end_time must be greater than start_time (index ${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<number, number[]>(); const ok_items = normed_shifts.filter(
(item): item is NormedOk & { timesheet_id: number } => "normed" in item);
ok_items.forEach(({ index, normed }) => { const regroup_by_date = new Map<string, number[]>();
const d = normed.date; ok_items.forEach(({ index, normed, timesheet_id }) => {
const key = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); 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, []); if (!regroup_by_date.has(key)) regroup_by_date.set(key, []);
regroup_by_date.get(key)!.push(index); 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()) { for (const indices of regroup_by_date.values()) {
const ordered = indices const ordered = indices
.map(index => { .map(index => {
const item = normed_shift[index] as NormedOk; const item = normed_shifts[index] as NormedOk & { timesheet_id: number };
return { index: index, start: item.normed.start_time, end: item.normed.end_time }; return {
index: index,
start: item.normed.start_time,
end: item.normed.end_time
};
}) })
.sort((a, b) => a.start.getTime() - b.start.getTime()); .sort((a, b) => a.start.getTime() - b.start.getTime());
for (let j = 1; j < ordered.length; j++) { 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 })) { if (
const err = new ConflictException({ 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', error_code: 'SHIFT_OVERLAP_BATCH',
message: 'New shift overlaps with another shift in the same batch (same day).', message: 'New shift overlaps with another shift in the same batch (same day).',
}); });
return dtos.map((_dto, key) => return dtos.map((_dto, key) =>
indices.includes(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) => { 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<string, { start_time: Date; end_time: Date }[]>();
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 }; 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<number, { start_time: Date; end_time: Date }[]>();
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) { for (const item of ok_items) {
const { index, dto, normed } = item; const { index, dto, normed, timesheet_id } = item;
const dayKey = new Date(normed.date.getFullYear(), normed.date.getMonth(), normed.date.getDate()).getTime(); const day_key = new Date(normed.date.getFullYear(), normed.date.getMonth(), normed.date.getDate()).getTime();
const existing = existing_date.get(dayKey) ?? []; 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 })); const hit = existing.find(e => overlaps({ start: e.start_time, end: e.end_time }, { start: normed.start_time, end: normed.end_time }));
if (hit) { if (hit) {
results[index] = { results[index] = {
@ -114,7 +180,7 @@ export class ShiftsUpsertService {
const row = await tx.shifts.create({ const row = await tx.shifts.create({
data: { data: {
timesheet_id: dto.timesheet_id, timesheet_id: timesheet_id,
bank_code_id: dto.bank_code_id, bank_code_id: dto.bank_code_id,
date: normed.date, date: normed.date,
start_time: normed.start_time, 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.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 = { const shift: GetShiftDto = {
timesheet_id: row.timesheet_id, timesheet_id: timesheet_id,
bank_code_id: row.bank_code_id, bank_code_id: row.bank_code_id,
date: toStringFromDate(row.date), date: toStringFromDate(row.date),
start_time: toStringFromHHmm(row.start_time), start_time: toStringFromHHmm(row.start_time),

View File

@ -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 {}

View File

@ -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);
}
}

View File

@ -27,11 +27,11 @@ import { Injectable } from "@nestjs/common";
const timesheet = await this.updateApprovalWithTransaction(transaction, timesheetId, isApproved); const timesheet = await this.updateApprovalWithTransaction(transaction, timesheetId, isApproved);
await transaction.shifts.updateMany({ await transaction.shifts.updateMany({
where: { timesheet_id: timesheetId }, where: { timesheet_id: timesheetId },
data: { is_approved: isApproved }, data: { is_approved: isApproved },
}); });
await transaction.expenses.updateManyAndReturn({ await transaction.expenses.updateManyAndReturn({
where: { timesheet_id: timesheetId }, where: { timesheet_id: timesheetId },
data: { is_approved: isApproved }, data: { is_approved: isApproved },
}); });
return timesheet; return timesheet;
} }

View File

@ -3,19 +3,25 @@ import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/time-and-attendance/utils/co
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
import { TotalExpenses, TotalHours } from "src/time-and-attendance/utils/type.utils"; import { TotalExpenses, TotalHours } from "src/time-and-attendance/utils/type.utils";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
@Injectable() @Injectable()
export class GetTimesheetsOverviewService { export class GetTimesheetsOverviewService {
constructor(private readonly prisma: PrismaService) { } constructor(
private readonly prisma: PrismaService,
private readonly emailResolver : EmailToIdResolver,
) { }
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
// GET TIMESHEETS FOR A SELECTED EMPLOYEE // 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 //find period using year and period_no
const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_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`); 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 //loads the timesheets related to the fetched pay-period
const timesheet_range = { employee_id, start_date: { gte: period.period_start, lte: period.period_end } }; const timesheet_range = { employee_id, start_date: { gte: period.period_start, lte: period.period_end } };
let rows = await this.loadTimesheets(timesheet_range); let rows = await this.loadTimesheets(timesheet_range);

View File

@ -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 {}

View File

@ -6,6 +6,8 @@ export const ANCHOR_ISO = '2023-12-17';
export const PERIOD_DAYS = 14; export const PERIOD_DAYS = 14;
export const PERIODS_PER_YEAR = 26; export const PERIODS_PER_YEAR = 26;
export const MS_PER_DAY = 86_400_000; export const MS_PER_DAY = 86_400_000;
export const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000;
//REGEX CONSTANTS //REGEX CONSTANTS
export const DATE_ISO_FORMAT = /^\d{4}-\d{2}-\d{2}$/; export const DATE_ISO_FORMAT = /^\d{4}-\d{2}-\d{2}$/;

View File

@ -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"; 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 //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 }) => export const overlaps = (a: { start: Date; end: Date }, b: { start: Date; end: Date }) =>
!(a.end <= b.start || a.start >= b.end); !(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)))));

View File

@ -1,7 +1,7 @@
import { Prisma, PrismaClient } from "@prisma/client"; import { Prisma, PrismaClient } from "@prisma/client";
import { NotFoundException } from "@nestjs/common"; import { NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service"; 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; type Tx = Prisma.TransactionClient | PrismaClient;

View File

@ -47,3 +47,34 @@ export const leaveRequestsSelect = {
} }
}, },
} satisfies Prisma.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}];

View File

@ -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 { 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 = { export type TotalHours = {
regular: number; regular: number;
@ -79,4 +79,32 @@ export type ApplyResult = {
export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>; export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>;
export type UpsertAction = 'create' | 'update' | 'delete'; 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;
}