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