Merge branch 'main' of git.targo.ca:Targo/targo_backend
This commit is contained in:
commit
fef9ea0b74
|
|
@ -91,30 +91,20 @@
|
|||
"parameters": [
|
||||
{
|
||||
"name": "date",
|
||||
"required": false,
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"description": "Override for resolving the current period",
|
||||
"schema": {
|
||||
"example": "2025-08-11",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Find current and all pay periods",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PayPeriodBundleDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"summary": "Return current pay period and the full list",
|
||||
"tags": [
|
||||
"pay-periods"
|
||||
"PayPeriods"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -133,22 +123,11 @@
|
|||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Pay period found for the selected date",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PayPeriodDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Pay period not found for the selected date"
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"summary": "Resolve a period by a date within it",
|
||||
"tags": [
|
||||
"pay-periods"
|
||||
"PayPeriods"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -161,7 +140,6 @@
|
|||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"example": 2024,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
|
|
@ -169,35 +147,46 @@
|
|||
"name": "periodNumber",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "1..26",
|
||||
"schema": {
|
||||
"example": 1,
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Pay period found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PayPeriodDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Pay period not found"
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"summary": "Find pay period by year and period number",
|
||||
"tags": [
|
||||
"pay-periods"
|
||||
"PayPeriods"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/pay-periods/{year}/{periodNumber}/{email}": {
|
||||
"/pay-periods/crew/pay-period-approval": {
|
||||
"patch": {
|
||||
"operationId": "PayPeriodsController_bulkApproval",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/BulkCrewApprovalDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"PayPeriods"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/pay-periods/crew/{year}/{periodNumber}": {
|
||||
"get": {
|
||||
"operationId": "PayPeriodsController_getCrewOverview",
|
||||
"parameters": [
|
||||
|
|
@ -206,7 +195,6 @@
|
|||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"example": 2024,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
|
|
@ -214,49 +202,18 @@
|
|||
"name": "periodNumber",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "1..26",
|
||||
"schema": {
|
||||
"example": 1,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "includeSubtree",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Include indirect reports",
|
||||
"schema": {
|
||||
"example": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Crew overview",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PayPeriodOverviewDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Pay period not found"
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"summary": "Supervisor crew overview for a given pay period",
|
||||
"tags": [
|
||||
"pay-periods"
|
||||
"PayPeriods"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -269,7 +226,6 @@
|
|||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"example": 2024,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
|
|
@ -277,31 +233,49 @@
|
|||
"name": "periodNumber",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "1..26",
|
||||
"schema": {
|
||||
"example": 1,
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Pay period overview found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PayPeriodOverviewDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Pay period not found"
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"summary": "Detailed view of a pay period by year + number",
|
||||
"tags": [
|
||||
"pay-periods"
|
||||
"PayPeriods"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/timesheets": {
|
||||
"get": {
|
||||
"operationId": "TimesheetController_getTimesheetByIds",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "year",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "period_number",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Timesheet"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -338,45 +312,6 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/timesheets": {
|
||||
"get": {
|
||||
"operationId": "TimesheetController_getTimesheetByIds",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "employee_email",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "year",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "period_number",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Timesheet"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/shift/create": {
|
||||
"post": {
|
||||
"operationId": "ShiftController_createBatch",
|
||||
|
|
@ -454,19 +389,10 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/schedule-presets/create/{employee_id}": {
|
||||
"/schedule-presets/create": {
|
||||
"post": {
|
||||
"operationId": "SchedulePresetsController_createPreset",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "employee_id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
|
|
@ -543,19 +469,10 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/schedule-presets/find/{employee_id}": {
|
||||
"/schedule-presets/find-list": {
|
||||
"get": {
|
||||
"operationId": "SchedulePresetsController_findListById",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "employee_id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
|
|
@ -566,18 +483,10 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/schedule-presets/apply-presets/{employee_id}": {
|
||||
"/schedule-presets/apply-presets": {
|
||||
"post": {
|
||||
"operationId": "SchedulePresetsController_applyPresets",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "employee_id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "preset",
|
||||
"required": true,
|
||||
|
|
@ -605,19 +514,10 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/expense/{timesheet_id}": {
|
||||
"/expense/create": {
|
||||
"post": {
|
||||
"operationId": "ExpenseController_create",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "timesheet_id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
|
|
@ -638,7 +538,7 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/expense": {
|
||||
"/expense/update": {
|
||||
"patch": {
|
||||
"operationId": "ExpenseController_update",
|
||||
"parameters": [],
|
||||
|
|
@ -652,7 +552,7 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/expense/{expense_id}": {
|
||||
"/expense/delete/{expense_id}": {
|
||||
"delete": {
|
||||
"operationId": "ExpenseController_remove",
|
||||
"parameters": [
|
||||
|
|
@ -733,167 +633,9 @@
|
|||
}
|
||||
},
|
||||
"schemas": {
|
||||
"PayPeriodDto": {
|
||||
"BulkCrewApprovalDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pay_period_no": {
|
||||
"type": "number",
|
||||
"example": 1,
|
||||
"description": "numéro cyclique de la période entre 1 et 26"
|
||||
},
|
||||
"period_start": {
|
||||
"type": "string",
|
||||
"example": "2023-12-17",
|
||||
"format": "date"
|
||||
},
|
||||
"period_end": {
|
||||
"type": "string",
|
||||
"example": "2023-12-30",
|
||||
"format": "date"
|
||||
},
|
||||
"payday": {
|
||||
"type": "string",
|
||||
"example": "2023-01-04",
|
||||
"format": "date"
|
||||
},
|
||||
"pay_year": {
|
||||
"type": "number",
|
||||
"example": 2023
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"example": "2023-12-17 → 2023-12-30"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pay_period_no",
|
||||
"period_start",
|
||||
"period_end",
|
||||
"payday",
|
||||
"pay_year",
|
||||
"label"
|
||||
]
|
||||
},
|
||||
"PayPeriodBundleDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"current": {
|
||||
"description": "Current pay period (resolved from date)",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/PayPeriodDto"
|
||||
}
|
||||
]
|
||||
},
|
||||
"periods": {
|
||||
"description": "All pay periods",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PayPeriodDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"current",
|
||||
"periods"
|
||||
]
|
||||
},
|
||||
"EmployeePeriodOverviewDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"employee_name": {
|
||||
"type": "string",
|
||||
"example": "Alex Dupont",
|
||||
"description": "Nom complet de lemployé"
|
||||
},
|
||||
"regular_hours": {
|
||||
"type": "number",
|
||||
"example": 40,
|
||||
"description": "pay-period`s regular hours"
|
||||
},
|
||||
"other_hours": {
|
||||
"type": "object",
|
||||
"example": 0,
|
||||
"description": "pay-period`s other hours"
|
||||
},
|
||||
"expenses": {
|
||||
"type": "number",
|
||||
"example": 420.69,
|
||||
"description": "pay-period`s total expenses ($)"
|
||||
},
|
||||
"mileage": {
|
||||
"type": "number",
|
||||
"example": 40,
|
||||
"description": "pay-period total mileages (km)"
|
||||
},
|
||||
"is_approved": {
|
||||
"type": "boolean",
|
||||
"example": true,
|
||||
"description": "Tous les timesheets de la période sont approuvés pour cet employé"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"employee_name",
|
||||
"regular_hours",
|
||||
"other_hours",
|
||||
"expenses",
|
||||
"mileage",
|
||||
"is_approved"
|
||||
]
|
||||
},
|
||||
"PayPeriodOverviewDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pay_period_no": {
|
||||
"type": "number",
|
||||
"example": 1,
|
||||
"description": "Period number (1–26)"
|
||||
},
|
||||
"pay_year": {
|
||||
"type": "number",
|
||||
"example": 2023,
|
||||
"description": "Calendar year of the period"
|
||||
},
|
||||
"period_start": {
|
||||
"type": "string",
|
||||
"example": "2023-12-17",
|
||||
"format": "date",
|
||||
"description": "Period start date (YYYY-MM-DD)"
|
||||
},
|
||||
"period_end": {
|
||||
"type": "string",
|
||||
"example": "2023-12-30",
|
||||
"format": "date",
|
||||
"description": "Period end date (YYYY-MM-DD)"
|
||||
},
|
||||
"payday": {
|
||||
"type": "string",
|
||||
"example": "2023-12-30",
|
||||
"format": "date",
|
||||
"description": "Period pay day(YYYY-MM-DD)"
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"example": "2023-12-17 → 2023-12-30",
|
||||
"description": "Human-readable label"
|
||||
},
|
||||
"employees_overview": {
|
||||
"description": "Per-employee overview for the period",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EmployeePeriodOverviewDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pay_period_no",
|
||||
"pay_year",
|
||||
"period_start",
|
||||
"period_end",
|
||||
"payday",
|
||||
"label",
|
||||
"employees_overview"
|
||||
]
|
||||
"properties": {}
|
||||
},
|
||||
"PreferencesDto": {
|
||||
"type": "object",
|
||||
|
|
|
|||
49
package-lock.json
generated
49
package-lock.json
generated
|
|
@ -243,6 +243,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
|
||||
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
|
|
@ -3113,6 +3114,7 @@
|
|||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
|
|
@ -3271,6 +3273,7 @@
|
|||
"version": "11.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.7.tgz",
|
||||
"integrity": "sha512-lwlObwGgIlpXSXYOTpfzdCepUyWomz6bv9qzGzzvpgspUxkj0Uz0fUJcvD44V8Ps7QhKW3lZBoYbXrH25UZrbA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"file-type": "21.0.0",
|
||||
"iterare": "1.2.1",
|
||||
|
|
@ -3316,6 +3319,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.7.tgz",
|
||||
"integrity": "sha512-TyXFOwjhHv/goSgJ8i20K78jwTM0iSpk9GBcC2h3mf4MxNy+znI8m7nWjfoACjTkb89cTwDQetfTHtSfGLLaiA==",
|
||||
"hasInstallScript": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@nuxt/opencollective": "0.4.1",
|
||||
"fast-safe-stringify": "2.1.1",
|
||||
|
|
@ -3395,6 +3399,7 @@
|
|||
"version": "11.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.7.tgz",
|
||||
"integrity": "sha512-5T+GLdvTiGPKB4/P4PM9ftKUKNHJy8ThEFhZA3vQnXVL7Vf0rDr07TfVTySVu+XTh85m1lpFVuyFM6u6wLNsRA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cors": "2.8.5",
|
||||
"express": "5.1.0",
|
||||
|
|
@ -3794,6 +3799,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.6.0.tgz",
|
||||
"integrity": "sha512-Q5FsI3Cw0fGMXhmsg7c08i4EmXCrcl+WnAxb6LYOLHw4JFFC3yzmx9LaXZ7QMbA+JZXbigU2TirI7RAfO0Qlnw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3",
|
||||
"@xhmikosr/bin-wrapper": "^13.0.5",
|
||||
|
|
@ -3862,6 +3868,7 @@
|
|||
"integrity": "sha512-CJSn2vstd17ddWIHBsjuD4OQnn9krQfaq6EO+w9YfId5DKznyPmzxAARlOXG99cC8/3Kli8ysKy6phL43bSr0w==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3",
|
||||
"@swc/types": "^0.1.23"
|
||||
|
|
@ -4198,6 +4205,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "*",
|
||||
"@types/json-schema": "*"
|
||||
|
|
@ -4357,6 +4365,7 @@
|
|||
"version": "22.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.2.tgz",
|
||||
"integrity": "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
|
|
@ -4537,6 +4546,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz",
|
||||
"integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.37.0",
|
||||
"@typescript-eslint/types": "8.37.0",
|
||||
|
|
@ -5440,6 +5450,7 @@
|
|||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -5452,7 +5463,6 @@
|
|||
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
|
||||
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
|
|
@ -5486,6 +5496,7 @@
|
|||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
|
|
@ -5953,6 +5964,7 @@
|
|||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001726",
|
||||
"electron-to-chromium": "^1.5.173",
|
||||
|
|
@ -6231,6 +6243,7 @@
|
|||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
|
|
@ -6283,12 +6296,14 @@
|
|||
"node_modules/class-transformer": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw=="
|
||||
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/class-validator": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz",
|
||||
"integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/validator": "^13.11.8",
|
||||
"libphonenumber-js": "^1.11.1",
|
||||
|
|
@ -7151,6 +7166,7 @@
|
|||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
|
||||
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
|
@ -7211,6 +7227,7 @@
|
|||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz",
|
||||
"integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
|
|
@ -8717,6 +8734,7 @@
|
|||
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
|
||||
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "^29.7.0",
|
||||
"@jest/types": "^29.6.3",
|
||||
|
|
@ -10353,6 +10371,7 @@
|
|||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
|
||||
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"passport-strategy": "1.x.x",
|
||||
"pause": "0.0.1",
|
||||
|
|
@ -10646,6 +10665,7 @@
|
|||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
|
|
@ -10700,6 +10720,7 @@
|
|||
"integrity": "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@prisma/config": "6.18.0",
|
||||
"@prisma/engines": "6.18.0"
|
||||
|
|
@ -10919,7 +10940,8 @@
|
|||
"node_modules/reflect-metadata": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
|
||||
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/repeat-string": {
|
||||
"version": "1.6.1",
|
||||
|
|
@ -11108,6 +11130,7 @@
|
|||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
|
|
@ -11917,6 +11940,7 @@
|
|||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
|
|
@ -12225,6 +12249,7 @@
|
|||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
|
|
@ -12382,6 +12407,7 @@
|
|||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -12564,9 +12590,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.15.15",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
|
||||
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
|
||||
"version": "13.15.20",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz",
|
||||
"integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
|
|
@ -12711,7 +12738,6 @@
|
|||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
|
|
@ -12729,7 +12755,6 @@
|
|||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
|
||||
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3"
|
||||
},
|
||||
|
|
@ -12742,7 +12767,6 @@
|
|||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esrecurse": "^4.3.0",
|
||||
"estraverse": "^4.1.1"
|
||||
|
|
@ -12756,7 +12780,6 @@
|
|||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
|
||||
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
|
|
@ -12765,15 +12788,13 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/webpack/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
|
|
@ -12783,7 +12804,6 @@
|
|||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
|
|
@ -12796,7 +12816,6 @@
|
|||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
||||
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"ajv": "^8.9.0",
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import { ConfigModule } from '@nestjs/config';
|
|||
import { APP_FILTER, APP_PIPE } from '@nestjs/core';
|
||||
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||
import { ValidationError } from 'class-validator';
|
||||
import { PayperiodsModule } from './time-and-attendance/modules/pay-period/pay-periods.module';
|
||||
import { TimeAndAttendanceModule } from 'src/time-and-attendance/time-and-attendance.module';
|
||||
import { PayperiodsModule } from 'src/time-and-attendance/pay-period/pay-periods.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { CsvExportController } from "./controllers/csv-exports.controller";
|
||||
import { CsvExportService } from "./services/csv-exports.service";
|
||||
import { SharedModule } from "../../time-and-attendance/modules/shared/shared.module";
|
||||
|
||||
@Module({
|
||||
providers:[CsvExportService, SharedModule],
|
||||
providers:[CsvExportService],
|
||||
controllers: [CsvExportController],
|
||||
})
|
||||
export class CsvExportModule {}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,18 @@ import { VacationService } from "./services/vacation.service";
|
|||
import { HolidayService } from "./services/holiday.service";
|
||||
import { MileageService } from "./services/mileage.service";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
|
||||
|
||||
@Module({
|
||||
imports:[],
|
||||
providers: [
|
||||
HolidayService,
|
||||
MileageService,
|
||||
OvertimeService,
|
||||
SickLeaveService,
|
||||
VacationService
|
||||
VacationService,
|
||||
EmailToIdResolver,
|
||||
],
|
||||
exports: [
|
||||
HolidayService,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
import { computeHours, getWeekStart } from "src/common/utils/date-utils";
|
||||
import { PrismaService } from "../../../prisma/prisma.service";
|
||||
|
||||
const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
import { MS_PER_WEEK } from "src/time-and-attendance/utils/constants.utils";
|
||||
/*
|
||||
le calcul est 1/20 des 4 dernières semaines, précédent la semaine incluant le férier.
|
||||
Un maximum de 08h00 est allouable pour le férier
|
||||
|
|
@ -15,28 +14,19 @@ const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
|
|||
export class HolidayService {
|
||||
private readonly logger = new Logger(HolidayService.name);
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
//fetch employee_id by email
|
||||
private async resolveEmployeeByEmail(email: string): Promise<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;
|
||||
}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly emailResolver: EmailToIdResolver,
|
||||
) {}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise<number> {
|
||||
const holiday_week_start = getWeekStart(holiday_date);
|
||||
const window_start = new Date(holiday_week_start.getTime() - 4 * WEEK_IN_MS);
|
||||
const window_start = new Date(holiday_week_start.getTime() - 4 * MS_PER_WEEK);
|
||||
const window_end = new Date(holiday_week_start.getTime() - 1);
|
||||
|
||||
const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G700'];
|
||||
|
|
@ -60,7 +50,7 @@ export class HolidayService {
|
|||
|
||||
let capped_total = 0;
|
||||
for(let offset = 1; offset <= 4; offset++) {
|
||||
const week_start = new Date(holiday_week_start.getTime() - offset * WEEK_IN_MS);
|
||||
const week_start = new Date(holiday_week_start.getTime() - offset * MS_PER_WEEK);
|
||||
const key = week_start.getTime();
|
||||
const weekly_hours = hours_by_week.get(key) ?? 0;
|
||||
capped_total += Math.min(weekly_hours, 40);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class MileageService {
|
||||
|
|
|
|||
|
|
@ -1,32 +1,15 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils';
|
||||
import { Prisma, PrismaClient } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { DAILY_LIMIT_HOURS, WEEKLY_LIMIT_HOURS } from 'src/time-and-attendance/utils/constants.utils';
|
||||
import { Tx, WeekOvertimeSummary } from 'src/time-and-attendance/utils/type.utils';
|
||||
|
||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||
|
||||
export type WeekOvertimeSummary = {
|
||||
week_start:string;
|
||||
week_end: string;
|
||||
week_total_hours: number;
|
||||
weekly_overtime: number;
|
||||
daily_overtime_kept: number;
|
||||
total_overtime: number;
|
||||
breakdown: Array<{
|
||||
date:string;
|
||||
day_hours: number;
|
||||
day_overtime: number;
|
||||
daily_kept: number;
|
||||
running_total_before: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class OvertimeService {
|
||||
|
||||
private logger = new Logger(OvertimeService.name);
|
||||
private daily_max = 8; // maximum for regular hours per day
|
||||
private weekly_max = 40; // maximum for regular hours per week
|
||||
|
||||
private INCLUDED_TYPES = ['EMERGENCY', 'EVENING','OVERTIME','REGULAR'] as const; // included types for weekly overtime calculation
|
||||
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
|
@ -61,7 +44,7 @@ export class OvertimeService {
|
|||
}
|
||||
|
||||
const week_total_hours = [ ...day_totals.values()].reduce((a,b) => a + b, 0);
|
||||
const weekly_overtime = Math.max(0, week_total_hours - this.weekly_max);
|
||||
const weekly_overtime = Math.max(0, week_total_hours - WEEKLY_LIMIT_HOURS);
|
||||
|
||||
let running = 0;
|
||||
let daily_kept_sum = 0;
|
||||
|
|
@ -69,9 +52,9 @@ export class OvertimeService {
|
|||
|
||||
for (const key of days) {
|
||||
const day_hours = day_totals.get(key) ?? 0;
|
||||
const day_overtime = Math.max(0, day_hours - this.daily_max);
|
||||
const day_overtime = Math.max(0, day_hours - DAILY_LIMIT_HOURS);
|
||||
|
||||
const cap_before_40 = Math.max(0, this.weekly_max - running);
|
||||
const cap_before_40 = Math.max(0, WEEKLY_LIMIT_HOURS - running);
|
||||
const daily_kept = Math.min(day_overtime, cap_before_40);
|
||||
|
||||
breakdown.push({
|
||||
|
|
@ -104,144 +87,4 @@ export class OvertimeService {
|
|||
breakdown,
|
||||
};
|
||||
}
|
||||
|
||||
// //calculate daily overtime
|
||||
// async getDailyOvertimeHours(timesheet_id: number, date: Date): Promise<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`);
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { getYearStart, roundToQuarterHour } from "src/common/utils/date-utils";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from "../../../prisma/prisma.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class SickLeaveService {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
import { PrismaService } from "../../../prisma/prisma.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class VacationService {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { OmitType, PartialType } from "@nestjs/swagger";
|
||||
import { ExpenseDto } from "src/time-and-attendance/modules/expenses/dtos/expense-create.dto";
|
||||
import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto";
|
||||
|
||||
export class updateExpenseDto extends PartialType (
|
||||
OmitType(ExpenseDto, ['is_approved', 'timesheet_id'] as const)
|
||||
10
src/time-and-attendance/expenses/expenses.module.ts
Normal file
10
src/time-and-attendance/expenses/expenses.module.ts
Normal 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 {}
|
||||
|
|
@ -1,34 +1,49 @@
|
|||
|
||||
import { CreateExpenseResult, UpdateExpensePayload, UpdateExpenseResult, DeleteExpenseResult, NormalizedExpense } from "src/time-and-attendance/utils/type.utils";
|
||||
import { toDateFromString, toStringFromDate } from "src/time-and-attendance/utils/date-time.utils";
|
||||
import { toDateFromString, toStringFromDate, weekStartSunday } from "src/time-and-attendance/utils/date-time.utils";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { expense_select } from "src/time-and-attendance/utils/selects.utils";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { GetExpenseDto } from "src/time-and-attendance/modules/expenses/dtos/expense-get.dto";
|
||||
import { ExpenseDto } from "src/time-and-attendance/modules/expenses/dtos/expense-create.dto";
|
||||
import { ExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-create.dto";
|
||||
import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class ExpenseUpsertService {
|
||||
constructor(private readonly prisma: PrismaService) { }
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly emailResolver: EmailToIdResolver,
|
||||
) { }
|
||||
|
||||
//_________________________________________________________________
|
||||
// CREATE
|
||||
//_________________________________________________________________
|
||||
async createExpense(timesheet_id: number, dto: ExpenseDto): Promise<CreateExpenseResult> {
|
||||
async createExpense( dto: ExpenseDto, email: string): Promise<CreateExpenseResult> {
|
||||
try {
|
||||
//fetch employee_id using req.user.email
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
|
||||
//normalize strings and dates
|
||||
const normed_expense = this.normalizeExpenseDto(dto);
|
||||
|
||||
//finds the timesheet using expense.date
|
||||
const start_date = weekStartSunday(normed_expense.date);
|
||||
|
||||
//parse numbers
|
||||
const parsed_amount = this.parseOptionalNumber(dto.amount, "amount");
|
||||
const parsed_mileage = this.parseOptionalNumber(dto.mileage, "mileage");
|
||||
const parsed_attachment = this.parseOptionalNumber(dto.attachment, "attachment");
|
||||
|
||||
const timesheet = await this.prisma.timesheets.findFirst({
|
||||
where: { start_date, employee_id },
|
||||
select: { id: true, employee_id: true },
|
||||
});
|
||||
if(!timesheet) throw new NotFoundException(`Timesheet with id ${dto.timesheet_id} not found`);
|
||||
|
||||
//create a new expense
|
||||
const expense = await this.prisma.expenses.create({
|
||||
data: {
|
||||
timesheet_id,
|
||||
timesheet_id: timesheet.id,
|
||||
bank_code_id: dto.bank_code_id,
|
||||
attachment: parsed_attachment,
|
||||
date: normed_expense.date,
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import { Body, Controller, Post } from "@nestjs/common";
|
||||
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||
import { LeaveRequestsService } from "../services/leave-request.service";
|
||||
import { UpsertLeaveRequestDto } from "../dtos/upsert-leave-request.dto";
|
||||
import { LeaveTypes } from "@prisma/client";
|
||||
import { LeaveRequestsService } from "../services/leave-request.service";
|
||||
|
||||
@ApiTags('Leave Requests')
|
||||
@ApiBearerAuth('access-token')
|
||||
|
|
@ -15,16 +14,7 @@ export class LeaveRequestController {
|
|||
async upsertLeaveRequest(@Body() dto: UpsertLeaveRequestDto) {
|
||||
const { action, leave_requests } = await this.leave_service.handle(dto);
|
||||
return { action, leave_requests };
|
||||
}q
|
||||
|
||||
//TODO:
|
||||
/*
|
||||
@Get('archive')
|
||||
findAllArchived(){...}
|
||||
|
||||
@Get('archive/:id')
|
||||
findOneArchived(id){...}
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||
import { LeaveRequestArchiveRow } from "../utils/leave-requests-archive.select";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
const toNum = (value?: Prisma.Decimal | null) => value ? Number(value) : undefined;
|
||||
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/modules/leave-requests/dtos/upsert-leave-request.dto";
|
||||
import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
|
||||
import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
|
||||
import { normalizeDates, toDateOnly } from "src/time-and-attendance/modules/shared/helpers/date-time.helpers";
|
||||
import { LeaveRequestViewDto } from "src/time-and-attendance/modules/leave-requests/dtos/leave-request-view.dto";
|
||||
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
||||
import { LeaveRequestsUtils } from "src/time-and-attendance/modules/leave-requests/utils/leave-request.util";
|
||||
import { BankCodesResolver } from "src/time-and-attendance/modules/shared/utils/resolve-bank-type-id.utils";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/modules/shared/utils/resolve-email-id.utils";
|
||||
import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
|
||||
import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util";
|
||||
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { mapRowToView } from "src/time-and-attendance/modules/leave-requests/mappers/leave-requests.mapper";
|
||||
import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper";
|
||||
import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils";
|
||||
|
||||
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
import { roundToQuarterHour } from "src/common/utils/date-utils";
|
||||
import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto";
|
||||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||
import { mapRowToView } from "../mappers/leave-requests.mapper";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service";
|
||||
import { roundToQuarterHour } from "src/common/utils/date-utils";
|
||||
import { LeaveRequestsUtils } from "src/time-and-attendance/leave-requests/utils/leave-request.util";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
||||
import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service";
|
||||
import { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
|
||||
import { normalizeDates, toDateOnly, toISODateKey } from "src/time-and-attendance/modules/shared/helpers/date-time.helpers";
|
||||
import { BankCodesResolver } from "src/time-and-attendance/modules/shared/utils/resolve-bank-type-id.utils";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/modules/shared/utils/resolve-email-id.utils";
|
||||
import { LeaveRequestsUtils } from "src/time-and-attendance/modules/leave-requests/utils/leave-request.util";
|
||||
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
||||
import { HolidayService } from "src/time-and-attendance/domains/services/holiday.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { mapRowToView } from "../mappers/leave-requests.mapper";
|
||||
import { normalizeDates, toDateOnly, toISODateKey } from "src/time-and-attendance/utils/date-time.utils";
|
||||
@Injectable()
|
||||
export class LeaveRequestsService {
|
||||
constructor(
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
|
||||
import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
|
||||
import { roundToQuarterHour } from "src/common/utils/date-utils";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service";
|
||||
import { LeaveRequestViewDto } from "src/time-and-attendance/modules/leave-requests/dtos/leave-request-view.dto";
|
||||
import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/modules/leave-requests/dtos/upsert-leave-request.dto";
|
||||
import { mapRowToView } from "src/time-and-attendance/modules/leave-requests/mappers/leave-requests.mapper";
|
||||
import { normalizeDates, toDateOnly } from "src/time-and-attendance/modules/shared/helpers/date-time.helpers";
|
||||
import { BankCodesResolver } from "src/time-and-attendance/modules/shared/utils/resolve-bank-type-id.utils";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/modules/shared/utils/resolve-email-id.utils";
|
||||
import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
|
||||
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
||||
import { roundToQuarterHour } from "src/common/utils/date-utils";
|
||||
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
import { SickLeaveService } from "src/time-and-attendance/domains/services/sick-leave.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper";
|
||||
import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils";
|
||||
|
||||
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/leave-requests/dtos/upsert-leave-request.dto";
|
||||
import { LeaveTypes, LeaveApprovalStatus } from "@prisma/client";
|
||||
import { roundToQuarterHour } from "src/common/utils/date-utils";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
|
||||
import { LeaveRequestViewDto } from "src/time-and-attendance/modules/leave-requests/dtos/leave-request-view.dto";
|
||||
import { UpsertLeaveRequestDto, UpsertResult } from "src/time-and-attendance/modules/leave-requests/dtos/upsert-leave-request.dto";
|
||||
import { mapRowToView } from "src/time-and-attendance/modules/leave-requests/mappers/leave-requests.mapper";
|
||||
import { normalizeDates, toDateOnly } from "src/time-and-attendance/modules/shared/helpers/date-time.helpers";
|
||||
import { BankCodesResolver } from "src/time-and-attendance/modules/shared/utils/resolve-bank-type-id.utils";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/modules/shared/utils/resolve-email-id.utils";
|
||||
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
||||
import { LeaveRequestViewDto } from "src/time-and-attendance/leave-requests/dtos/leave-request-view.dto";
|
||||
import { roundToQuarterHour } from "src/common/utils/date-utils";
|
||||
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
import { VacationService } from "src/time-and-attendance/domains/services/vacation.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { mapRowToView } from "src/time-and-attendance/leave-requests/mappers/leave-requests.mapper";
|
||||
import { normalizeDates, toDateOnly } from "src/time-and-attendance/utils/date-time.utils";
|
||||
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -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!);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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!);
|
||||
}
|
||||
|
|
@ -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();
|
||||
// }
|
||||
}
|
||||
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { EmployeePeriodOverviewDto } from './overview-employee-period.dto';
|
||||
|
||||
export class PayPeriodOverviewDto {
|
||||
@ApiProperty({ example: 1, description: 'Period number (1–26)' })
|
||||
pay_period_no: number;
|
||||
|
||||
@ApiProperty({ example: 2023, description: 'Calendar year of the period' })
|
||||
pay_year: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2023-12-17',
|
||||
type: String,
|
||||
format: 'date',
|
||||
description: "Period start date (YYYY-MM-DD)",
|
||||
})
|
||||
period_start: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2023-12-30',
|
||||
type: String,
|
||||
format: 'date',
|
||||
description: "Period end date (YYYY-MM-DD)",
|
||||
})
|
||||
period_end: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2023-12-30',
|
||||
type: String,
|
||||
format: 'date',
|
||||
description: "Period pay day(YYYY-MM-DD)",
|
||||
})
|
||||
payday: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2023-12-17 → 2023-12-30',
|
||||
description: 'Human-readable label',
|
||||
})
|
||||
label: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: [EmployeePeriodOverviewDto],
|
||||
description: 'Per-employee overview for the period',
|
||||
})
|
||||
employees_overview: EmployeePeriodOverviewDto[];
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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};
|
||||
// }
|
||||
// }
|
||||
|
|
@ -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)))));
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
export const PAY_PERIOD_SELECT = {
|
||||
period_start: true,
|
||||
period_end: true,
|
||||
} as const;
|
||||
|
|
@ -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}];
|
||||
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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{}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsBoolean, IsEmail, IsInt, IsOptional, ValidateNested } from "class-validator";
|
||||
import { IsArray, IsBoolean, IsEmail, IsInt, ValidateNested } from "class-validator";
|
||||
|
||||
export class BulkCrewApprovalItemDto {
|
||||
@IsInt()
|
||||
|
|
@ -16,9 +16,6 @@ export class BulkCrewApprovalItemDto {
|
|||
}
|
||||
|
||||
export class BulkCrewApprovalDto {
|
||||
@IsEmail()
|
||||
supervisor_email: string;
|
||||
|
||||
@IsBoolean()
|
||||
include_subtree: boolean = false;
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { PayPeriodDto } from "./pay-period.dto";
|
||||
|
||||
export class PayPeriodBundleDto {
|
||||
current: PayPeriodDto;
|
||||
periods: PayPeriodDto[];
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export class PayPeriodDto {
|
||||
pay_period_no: number;
|
||||
period_start: string;
|
||||
period_end: string;
|
||||
payday: string;
|
||||
pay_year: number;
|
||||
label: string;
|
||||
}
|
||||
20
src/time-and-attendance/pay-period/pay-periods.module.ts
Normal file
20
src/time-and-attendance/pay-period/pay-periods.module.ts
Normal 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 {}
|
||||
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,29 @@
|
|||
import { SchedulePresetsUpsertService } from "src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-upsert.service";
|
||||
import { GetTimesheetsOverviewService } from "src/time-and-attendance/modules/time-tracker/timesheets/services/timesheet-get-overview.service";
|
||||
import { SchedulePresetsGetService } from "src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-get.service";
|
||||
import { SchedulePresetsApplyService } from "src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-apply.service";
|
||||
import { SchedulePresetsController } from "src/time-and-attendance/modules/time-tracker/schedule-presets/controller/schedule-presets.controller";
|
||||
import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module";
|
||||
import { ExpenseUpsertService } from "src/time-and-attendance/modules/expenses/services/expense-upsert.service";
|
||||
import { ShiftsUpsertService } from "src/time-and-attendance/modules/time-tracker/shifts/services/shifts-upsert.service";
|
||||
import { TimesheetController } from "src/time-and-attendance/modules/time-tracker/timesheets/controllers/timesheet.controller";
|
||||
import { ExpenseController } from "src/time-and-attendance/modules/expenses/controllers/expense.controller";
|
||||
import { PayperiodsModule } from "src/time-and-attendance/modules/pay-period/pay-periods.module";
|
||||
import { ShiftsGetService } from "src/time-and-attendance/modules/time-tracker/shifts/services/shifts-get.service";
|
||||
import { ShiftController } from "src/time-and-attendance/modules/time-tracker/shifts/controllers/shift.controller";
|
||||
import { SharedModule } from "src/time-and-attendance/modules/shared/shared.module";
|
||||
|
||||
import { Module } from "@nestjs/common";
|
||||
import { BusinessLogicsModule } from "src/time-and-attendance/domains/business-logics.module";
|
||||
import { ExpenseController } from "src/time-and-attendance/expenses/controllers/expense.controller";
|
||||
import { ExpenseUpsertService } from "src/time-and-attendance/expenses/services/expense-upsert.service";
|
||||
import { PayperiodsModule } from "src/time-and-attendance/pay-period/pay-periods.module";
|
||||
import { SchedulePresetsController } from "src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller";
|
||||
import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service";
|
||||
import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service";
|
||||
import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service";
|
||||
import { ShiftController } from "src/time-and-attendance/time-tracker/shifts/controllers/shift.controller";
|
||||
import { ShiftsGetService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-get.service";
|
||||
import { ShiftsUpsertService } from "src/time-and-attendance/time-tracker/shifts/services/shifts-upsert.service";
|
||||
import { TimesheetController } from "src/time-and-attendance/time-tracker/timesheets/controllers/timesheet.controller";
|
||||
import { GetTimesheetsOverviewService } from "src/time-and-attendance/time-tracker/timesheets/services/timesheet-get-overview.service";
|
||||
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
BusinessLogicsModule,
|
||||
PayperiodsModule,
|
||||
SharedModule,
|
||||
BusinessLogicsModule,
|
||||
PayperiodsModule,
|
||||
],
|
||||
controllers: [
|
||||
TimesheetController,
|
||||
ShiftController,
|
||||
TimesheetController,
|
||||
ShiftController,
|
||||
SchedulePresetsController,
|
||||
ExpenseController,
|
||||
],
|
||||
|
|
@ -34,6 +35,8 @@ import { Module } from "@nestjs/common";
|
|||
SchedulePresetsUpsertService,
|
||||
SchedulePresetsGetService,
|
||||
SchedulePresetsApplyService,
|
||||
EmailToIdResolver,
|
||||
BankCodesResolver,
|
||||
],
|
||||
exports: [],
|
||||
}) export class TimeAndAttendanceModule{};
|
||||
}) export class TimeAndAttendanceModule { };
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { ArrayMinSize, IsArray, IsBoolean, IsOptional, IsString } from "class-validator";
|
||||
import { SchedulePresetShiftsDto } from "src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto";
|
||||
import { SchedulePresetShiftsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-preset-shifts.dto";
|
||||
|
||||
export class SchedulePresetsDto {
|
||||
@IsString()
|
||||
|
|
@ -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{}
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
import { SchedulePresetsUpsertService } from "src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-upsert.service";
|
||||
import { SchedulePresetsApplyService } from "src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-apply.service";
|
||||
import { SchedulePresetsGetService } from "src/time-and-attendance/modules/time-tracker/schedule-presets/services/schedule-presets-get.service";
|
||||
import { SchedulePresetsController } from "src/time-and-attendance/modules/time-tracker/schedule-presets/controller/schedule-presets.controller";
|
||||
import { SharedModule } from "src/time-and-attendance/modules/shared/shared.module";
|
||||
|
||||
import { Module } from "@nestjs/common";
|
||||
import { SchedulePresetsController } from "src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller";
|
||||
import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service";
|
||||
import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service";
|
||||
import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service";
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [SharedModule],
|
||||
controllers: [SchedulePresetsController],
|
||||
providers: [
|
||||
SchedulePresetsUpsertService,
|
||||
|
|
@ -4,20 +4,19 @@ import { DATE_ISO_FORMAT } from "src/time-and-attendance/utils/constants.utils";
|
|||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { ApplyResult } from "src/time-and-attendance/utils/type.utils";
|
||||
import { WEEKDAY } from "src/time-and-attendance/utils/mappers.utils";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class SchedulePresetsApplyService {
|
||||
constructor( private readonly prisma: PrismaService) {}
|
||||
constructor( private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver) {}
|
||||
|
||||
async applyToTimesheet(
|
||||
employee_id: number,
|
||||
preset_name: string,
|
||||
start_date_iso: string,
|
||||
): Promise<ApplyResult> {
|
||||
async applyToTimesheet( email: string, preset_name: string, start_date_iso: string ): Promise<ApplyResult> {
|
||||
if(!preset_name?.trim()) throw new BadRequestException('A preset_name is required');
|
||||
if(!DATE_ISO_FORMAT.test(start_date_iso)) throw new BadRequestException('start_date must be of format :YYYY-MM-DD');
|
||||
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
|
||||
const preset = await this.prisma.schedulePresets.findFirst({
|
||||
where: { employee_id, name: preset_name },
|
||||
include: {
|
||||
|
|
@ -2,13 +2,18 @@ import { PresetResponse, ShiftResponse } from "src/time-and-attendance/utils/typ
|
|||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
|
||||
@Injectable()
|
||||
export class SchedulePresetsGetService {
|
||||
constructor( private readonly prisma: PrismaService ){}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly emailResolver: EmailToIdResolver,
|
||||
){}
|
||||
|
||||
async getSchedulePresets(employee_id: number): Promise<PresetResponse[]> {
|
||||
async getSchedulePresets(email: string): Promise<PresetResponse[]> {
|
||||
try {
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
const presets = await this.prisma.schedulePresets.findMany({
|
||||
where: { employee_id },
|
||||
orderBy: [{is_default: 'desc' }, { name: 'asc' }],
|
||||
|
|
@ -1,24 +1,28 @@
|
|||
import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common";
|
||||
import { CreatePresetResult, DeletePresetResult, UpdatePresetResult } from "src/time-and-attendance/utils/type.utils";
|
||||
import { SchedulePresetsDto } from "src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/create-schedule-presets.dto";
|
||||
import { BankCodesResolver } from "src/time-and-attendance/modules/shared/utils/resolve-bank-type-id.utils";
|
||||
import { Prisma, Weekday } from "@prisma/client";
|
||||
import { toHHmmFromDate } from "src/time-and-attendance/utils/date-time.utils";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
||||
import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
|
||||
@Injectable()
|
||||
export class SchedulePresetsUpsertService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly typeResolver : BankCodesResolver,
|
||||
private readonly emailResolver: EmailToIdResolver,
|
||||
){}
|
||||
//_________________________________________________________________
|
||||
// CREATE
|
||||
//_________________________________________________________________
|
||||
async createPreset( employee_id: number, dto: SchedulePresetsDto): Promise<CreatePresetResult> {
|
||||
async createPreset( email: string, dto: SchedulePresetsDto): Promise<CreatePresetResult> {
|
||||
try {
|
||||
const shifts_data = await this.resolveAndBuildPresetShifts(dto);
|
||||
if(!shifts_data) throw new BadRequestException(`Employee with id: ${employee_id} or dto not found`);
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
if(!shifts_data) throw new BadRequestException(`Employee with email: ${email} or dto not found`);
|
||||
|
||||
await this.prisma.$transaction(async (tx)=> {
|
||||
if(dto.is_default) {
|
||||
await tx.schedulePresets.updateMany({
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { PartialType, OmitType } from "@nestjs/swagger";
|
||||
import { ShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-create.dto";
|
||||
import { IsInt } from "class-validator";
|
||||
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
|
||||
|
||||
export class UpdateShiftDto extends PartialType(
|
||||
// allows update using ShiftDto and preventing OmitType variables to be modified
|
||||
|
|
@ -2,7 +2,7 @@ import { toStringFromDate, toStringFromHHmm } from "src/time-and-attendance/util
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { shift_select } from "src/time-and-attendance/utils/selects.utils";
|
||||
import { GetShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-get.dto";
|
||||
import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto";
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
import { CreateShiftResult, NormedOk, NormedErr, UpdateShiftResult, UpdateShiftPayload, UpdateShiftChanges, Normalized } from "src/time-and-attendance/utils/type.utils";
|
||||
import { overlaps, toStringFromHHmm, toStringFromDate, toDateFromString, toHHmmFromString } from "src/time-and-attendance/utils/date-time.utils";
|
||||
import { overlaps, toStringFromHHmm, toStringFromDate, toDateFromString, toHHmmFromString, weekStartSunday } from "src/time-and-attendance/utils/date-time.utils";
|
||||
import { Injectable, BadRequestException, ConflictException, NotFoundException } from "@nestjs/common";
|
||||
import { OvertimeService } from "src/time-and-attendance/domains/services/overtime.service";
|
||||
import { UpdateShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-update.dto";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
|
||||
import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto";
|
||||
import { UpdateShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto";
|
||||
import { shift_select } from "src/time-and-attendance/utils/selects.utils";
|
||||
import { GetShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-get.dto";
|
||||
import { ShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-create.dto";
|
||||
|
||||
|
||||
|
||||
|
|
@ -15,6 +16,7 @@ export class ShiftsUpsertService {
|
|||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly overtime: OvertimeService,
|
||||
private readonly emailResolver: EmailToIdResolver,
|
||||
) { }
|
||||
|
||||
//_________________________________________________________________
|
||||
|
|
@ -25,76 +27,140 @@ export class ShiftsUpsertService {
|
|||
//checks for overlaping shifts
|
||||
//create new shifts
|
||||
//calculate overtime
|
||||
async createShifts(dtos: ShiftDto[]): Promise<CreateShiftResult[]> {
|
||||
async createShifts(email: string, dtos: ShiftDto[]): Promise<CreateShiftResult[]> {
|
||||
if (!Array.isArray(dtos) || dtos.length === 0) return [];
|
||||
|
||||
const normed_shift: Array<NormedOk | NormedErr> = dtos.map((dto, index) => {
|
||||
try {
|
||||
const normed = this.normalizeShiftDto(dto);
|
||||
if (normed.end_time <= normed.start_time) {
|
||||
return { index, error: new BadRequestException(`end_time must be greater than start_time (index ${index})`) };
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
|
||||
const normed_shifts = await Promise.all(
|
||||
dtos.map(async (dto, index) => {
|
||||
try {
|
||||
const normed = this.normalizeShiftDto(dto);
|
||||
if (normed.end_time <= normed.start_time) {
|
||||
return {
|
||||
index,
|
||||
error: new BadRequestException(
|
||||
`end_time must be greater than start_time (index ${index})`
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const start_date = weekStartSunday(normed.date);
|
||||
|
||||
const timesheet = await this.prisma.timesheets.findFirst({
|
||||
where: { start_date, employee_id },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!timesheet) {
|
||||
return {
|
||||
index,
|
||||
error: new NotFoundException(`Timesheet not found`),
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
index,
|
||||
dto,
|
||||
normed,
|
||||
timesheet_id: timesheet.id,
|
||||
};
|
||||
} catch (error) {
|
||||
return { index, error };
|
||||
}
|
||||
return { index, dto, normed };
|
||||
} catch (error) {
|
||||
return { index, error };
|
||||
}
|
||||
});
|
||||
const ok_items = normed_shift.filter((x): x is NormedOk => "normed" in x);
|
||||
}));
|
||||
|
||||
const regroup_by_date = new Map<number, number[]>();
|
||||
const ok_items = normed_shifts.filter(
|
||||
(item): item is NormedOk & { timesheet_id: number } => "normed" in item);
|
||||
|
||||
ok_items.forEach(({ index, normed }) => {
|
||||
const d = normed.date;
|
||||
const key = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
||||
const regroup_by_date = new Map<string, number[]>();
|
||||
ok_items.forEach(({ index, normed, timesheet_id }) => {
|
||||
const day = new Date(normed.date.getFullYear(), normed.date.getMonth(), normed.date.getDate()).getTime();
|
||||
const key = `${timesheet_id}|${day}`;
|
||||
if (!regroup_by_date.has(key)) regroup_by_date.set(key, []);
|
||||
regroup_by_date.get(key)!.push(index);
|
||||
});
|
||||
|
||||
const timesheet_keys = Array.from(regroup_by_date.keys()).map((raw) => {
|
||||
const [timesheet, day] = raw.split('|');
|
||||
return {
|
||||
timesheet_id: Number(timesheet),
|
||||
day: Number(day),
|
||||
key: raw,
|
||||
};
|
||||
});
|
||||
|
||||
for (const indices of regroup_by_date.values()) {
|
||||
const ordered = indices
|
||||
.map(index => {
|
||||
const item = normed_shift[index] as NormedOk;
|
||||
return { index: index, start: item.normed.start_time, end: item.normed.end_time };
|
||||
const item = normed_shifts[index] as NormedOk & { timesheet_id: number };
|
||||
return {
|
||||
index: index,
|
||||
start: item.normed.start_time,
|
||||
end: item.normed.end_time
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||
|
||||
for (let j = 1; j < ordered.length; j++) {
|
||||
if (overlaps({ start: ordered[j - 1].start, end: ordered[j - 1].end }, { start: ordered[j].start, end: ordered[j].end })) {
|
||||
const err = new ConflictException({
|
||||
if (
|
||||
overlaps(
|
||||
{ start: ordered[j - 1].start, end: ordered[j - 1].end },
|
||||
{ start: ordered[j].start, end: ordered[j].end }
|
||||
)
|
||||
) {
|
||||
const error = new ConflictException({
|
||||
error_code: 'SHIFT_OVERLAP_BATCH',
|
||||
message: 'New shift overlaps with another shift in the same batch (same day).',
|
||||
});
|
||||
return dtos.map((_dto, key) =>
|
||||
indices.includes(key)
|
||||
? ({ ok: false, error: err } as CreateShiftResult)
|
||||
: ({ ok: false, error: new BadRequestException('Batch aborted due to overlaps in another date group') })
|
||||
? ({
|
||||
ok: false,
|
||||
error
|
||||
} as CreateShiftResult)
|
||||
: ({
|
||||
ok: false,
|
||||
error: new BadRequestException(
|
||||
'Batch aborted due to overlaps in another date group'
|
||||
),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const results: CreateShiftResult[] = Array.from({ length: dtos.length }, () => ({ ok: false, error: new Error('uninitialized') }));
|
||||
const results: CreateShiftResult[] = Array.from(
|
||||
{ length: dtos.length },
|
||||
() => ({ ok: false, error: new Error('uninitialized') }));
|
||||
|
||||
const existing_map = new Map<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 };
|
||||
});
|
||||
|
||||
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) {
|
||||
const { index, dto, normed } = item;
|
||||
const dayKey = new Date(normed.date.getFullYear(), normed.date.getMonth(), normed.date.getDate()).getTime();
|
||||
const existing = existing_date.get(dayKey) ?? [];
|
||||
|
||||
const { index, dto, normed, timesheet_id } = item;
|
||||
const day_key = new Date(normed.date.getFullYear(), normed.date.getMonth(), normed.date.getDate()).getTime();
|
||||
const map_key = `${timesheet_id}|${day_key}`;
|
||||
let existing = existing_map.get(map_key);
|
||||
if(!existing) {
|
||||
existing = [];
|
||||
existing_map.set(map_key, existing);
|
||||
}
|
||||
const hit = existing.find(e => overlaps({ start: e.start_time, end: e.end_time }, { start: normed.start_time, end: normed.end_time }));
|
||||
if (hit) {
|
||||
results[index] = {
|
||||
|
|
@ -114,7 +180,7 @@ export class ShiftsUpsertService {
|
|||
|
||||
const row = await tx.shifts.create({
|
||||
data: {
|
||||
timesheet_id: dto.timesheet_id,
|
||||
timesheet_id: timesheet_id,
|
||||
bank_code_id: dto.bank_code_id,
|
||||
date: normed.date,
|
||||
start_time: normed.start_time,
|
||||
|
|
@ -126,10 +192,11 @@ export class ShiftsUpsertService {
|
|||
});
|
||||
|
||||
existing.push({ start_time: row.start_time, end_time: row.end_time });
|
||||
existing_map.set(map_key, existing);
|
||||
|
||||
const summary = await this.overtime.getWeekOvertimeSummary(dto.timesheet_id, normed.date, tx);
|
||||
const summary = await this.overtime.getWeekOvertimeSummary(timesheet_id, normed.date, tx);
|
||||
const shift: GetShiftDto = {
|
||||
timesheet_id: row.timesheet_id,
|
||||
timesheet_id: timesheet_id,
|
||||
bank_code_id: row.bank_code_id,
|
||||
date: toStringFromDate(row.date),
|
||||
start_time: toStringFromHHmm(row.start_time),
|
||||
14
src/time-and-attendance/time-tracker/shifts/shifts.module.ts
Normal file
14
src/time-and-attendance/time-tracker/shifts/shifts.module.ts
Normal 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 {}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -27,11 +27,11 @@ import { Injectable } from "@nestjs/common";
|
|||
const timesheet = await this.updateApprovalWithTransaction(transaction, timesheetId, isApproved);
|
||||
await transaction.shifts.updateMany({
|
||||
where: { timesheet_id: timesheetId },
|
||||
data: { is_approved: isApproved },
|
||||
data: { is_approved: isApproved },
|
||||
});
|
||||
await transaction.expenses.updateManyAndReturn({
|
||||
where: { timesheet_id: timesheetId },
|
||||
data: { is_approved: isApproved },
|
||||
data: { is_approved: isApproved },
|
||||
});
|
||||
return timesheet;
|
||||
}
|
||||
|
|
@ -3,19 +3,25 @@ import { NUMBER_OF_TIMESHEETS_TO_RETURN } from "src/time-and-attendance/utils/co
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { TotalExpenses, TotalHours } from "src/time-and-attendance/utils/type.utils";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
|
||||
@Injectable()
|
||||
export class GetTimesheetsOverviewService {
|
||||
constructor(private readonly prisma: PrismaService) { }
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly emailResolver : EmailToIdResolver,
|
||||
) { }
|
||||
|
||||
//-----------------------------------------------------------------------------------
|
||||
// GET TIMESHEETS FOR A SELECTED EMPLOYEE
|
||||
//-----------------------------------------------------------------------------------
|
||||
async getTimesheetsForEmployeeByPeriod(employee_id: number, pay_year: number, pay_period_no: number) {
|
||||
async getTimesheetsForEmployeeByPeriod(email: string, pay_year: number, pay_period_no: number) {
|
||||
//find period using year and period_no
|
||||
const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no } });
|
||||
if (!period) throw new NotFoundException(`Pay period ${pay_year}-${pay_period_no} not found`);
|
||||
|
||||
//fetch the employee_id using the email
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
//loads the timesheets related to the fetched pay-period
|
||||
const timesheet_range = { employee_id, start_date: { gte: period.period_start, lte: period.period_end } };
|
||||
let rows = await this.loadTimesheets(timesheet_range);
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -6,6 +6,8 @@ export const ANCHOR_ISO = '2023-12-17';
|
|||
export const PERIOD_DAYS = 14;
|
||||
export const PERIODS_PER_YEAR = 26;
|
||||
export const MS_PER_DAY = 86_400_000;
|
||||
export const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
|
||||
//REGEX CONSTANTS
|
||||
export const DATE_ISO_FORMAT = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { BadRequestException } from "@nestjs/common";
|
||||
import { ANCHOR_ISO, MS_PER_DAY, PERIODS_PER_YEAR, PERIOD_DAYS } from "src/time-and-attendance/utils/constants.utils";
|
||||
|
||||
//ensures the week starts from sunday
|
||||
|
|
@ -88,4 +89,38 @@ export function listPayYear(pay_year: number, anchorISO = ANCHOR_ISO) {
|
|||
}
|
||||
|
||||
export const overlaps = (a: { start: Date; end: Date }, b: { start: Date; end: Date }) =>
|
||||
!(a.end <= b.start || a.start >= b.end);
|
||||
!(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)))));
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { NotFoundException } from "@nestjs/common";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { ShiftKey } from "../interfaces/shifts.interface";
|
||||
import { ShiftKey } from "src/time-and-attendance/utils/type.utils";
|
||||
|
||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||
|
||||
|
|
@ -47,3 +47,34 @@ export const leaveRequestsSelect = {
|
|||
}
|
||||
},
|
||||
} satisfies Prisma.LeaveRequestsSelect;
|
||||
|
||||
|
||||
export const EXPENSE_SELECT = {
|
||||
date: true,
|
||||
amount: true,
|
||||
mileage: true,
|
||||
comment: true,
|
||||
is_approved: true,
|
||||
supervisor_comment: true,
|
||||
bank_code: { select: { type: true } },
|
||||
} as const;
|
||||
|
||||
export const EXPENSE_ASC_ORDER = { date: 'asc' as const };
|
||||
|
||||
export const PAY_PERIOD_SELECT = {
|
||||
period_start: true,
|
||||
period_end: true,
|
||||
} as const;
|
||||
|
||||
export const SHIFT_SELECT = {
|
||||
date: true,
|
||||
start_time: true,
|
||||
end_time: true,
|
||||
comment: true,
|
||||
is_approved: true,
|
||||
is_remote: true,
|
||||
bank_code: {select: { type: true } },
|
||||
} as const;
|
||||
|
||||
export const SHIFT_ASC_ORDER = [{date: 'asc' as const}, {start_time: 'asc' as const}];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { WeekOvertimeSummary } from "src/time-and-attendance/domains/services/overtime.service";
|
||||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { GetExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-get.dto";
|
||||
import { updateExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-update.dto";
|
||||
import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto";
|
||||
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
|
||||
import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto";
|
||||
import { UpdateShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto";
|
||||
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
||||
import { UpdateShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-update.dto";
|
||||
import { GetShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-get.dto";
|
||||
import { ShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-create.dto";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { SchedulePresetsDto } from "src/time-and-attendance/modules/time-tracker/schedule-presets/dtos/create-schedule-presets.dto";
|
||||
import { GetExpenseDto } from "src/time-and-attendance/modules/expenses/dtos/expense-get.dto";
|
||||
import { updateExpenseDto } from "src/time-and-attendance/modules/expenses/dtos/expense-update.dto";
|
||||
|
||||
|
||||
export type TotalHours = {
|
||||
regular: number;
|
||||
|
|
@ -79,4 +79,32 @@ export type ApplyResult = {
|
|||
|
||||
export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>;
|
||||
|
||||
export type UpsertAction = 'create' | 'update' | 'delete';
|
||||
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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user