diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index be46095..59d8045 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -29,125 +29,6 @@ ] } }, - "/bank-codes": { - "post": { - "operationId": "BankCodesControllers_create", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateBankCodeDto" - } - } - } - }, - "responses": { - "201": { - "description": "Bank code successfully created." - }, - "400": { - "description": "Invalid input data." - } - }, - "summary": "Create a new bank code", - "tags": [ - "BankCodesControllers" - ] - }, - "get": { - "operationId": "BankCodesControllers_findAll", - "parameters": [], - "responses": { - "200": { - "description": "List of bank codes." - } - }, - "summary": "Retrieve all bank codes", - "tags": [ - "BankCodesControllers" - ] - } - }, - "/bank-codes/{id}": { - "get": { - "operationId": "BankCodesControllers_findOne", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "404": { - "description": "Bank code not found." - } - }, - "summary": "Retrieve a bank code by its ID", - "tags": [ - "BankCodesControllers" - ] - }, - "patch": { - "operationId": "BankCodesControllers_update", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateBankCodeDto" - } - } - } - }, - "responses": { - "404": { - "description": "Bank code not found." - } - }, - "summary": "Update an existing bank code", - "tags": [ - "BankCodesControllers" - ] - }, - "delete": { - "operationId": "BankCodesControllers_remove", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "responses": { - "404": { - "description": "Bank code not found." - } - }, - "summary": "Delete a bank code", - "tags": [ - "BankCodesControllers" - ] - } - }, "/archives/employees": { "get": { "operationId": "EmployeesArchiveController_findOneArchived", @@ -1246,124 +1127,107 @@ ] } }, - "/oauth-access-tokens": { - "post": { - "operationId": "OauthAccessTokensController_create", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateOauthAccessTokenDto" - } - } - } - }, - "responses": { - "201": { - "description": "OAuth access token created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OAuthAccessTokenEntity" - } - } - } - }, - "400": { - "description": "Incomplete task or invalid data" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Create OAuth access token", - "tags": [ - "OAuth Access Tokens" - ] - }, + "/auth/login": { "get": { - "operationId": "OauthAccessTokensController_findAll", + "operationId": "AuthController_login", "parameters": [], "responses": { - "201": { - "description": "List of OAuth access token found", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OAuthAccessTokenEntity" - } - } - } - } - }, - "400": { - "description": "List of OAuth access token not found" + "200": { + "description": "" } }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find all OAuth access token", "tags": [ - "OAuth Access Tokens" + "Auth" ] } }, - "/oauth-access-tokens/{id}": { + "/auth/callback": { "get": { - "operationId": "OauthAccessTokensController_findOne", + "operationId": "AuthController_loginCallback", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Auth" + ] + } + }, + "/bank-codes": { + "post": { + "operationId": "BankCodesControllers_create", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBankCodeDto" + } + } + } + }, + "responses": { + "201": { + "description": "Bank code successfully created." + }, + "400": { + "description": "Invalid input data." + } + }, + "summary": "Create a new bank code", + "tags": [ + "BankCodesControllers" + ] + }, + "get": { + "operationId": "BankCodesControllers_findAll", + "parameters": [], + "responses": { + "200": { + "description": "List of bank codes." + } + }, + "summary": "Retrieve all bank codes", + "tags": [ + "BankCodesControllers" + ] + } + }, + "/bank-codes/{id}": { + "get": { + "operationId": "BankCodesControllers_findOne", "parameters": [ { "name": "id", "required": true, "in": "path", "schema": { - "type": "string" + "type": "number" } } ], "responses": { - "201": { - "description": "OAuth access token found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OAuthAccessTokenEntity" - } - } - } - }, - "400": { - "description": "OAuth access token not found" + "404": { + "description": "Bank code not found." } }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Find OAuth access token", + "summary": "Retrieve a bank code by its ID", "tags": [ - "OAuth Access Tokens" + "BankCodesControllers" ] }, "patch": { - "operationId": "OauthAccessTokensController_update", + "operationId": "BankCodesControllers_update", "parameters": [ { "name": "id", "required": true, "in": "path", "schema": { - "type": "string" + "type": "number" } } ], @@ -1372,71 +1236,41 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateOauthAccessTokenDto" + "$ref": "#/components/schemas/UpdateBankCodeDto" } } } }, "responses": { - "201": { - "description": "OAuth access token updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OAuthAccessTokenEntity" - } - } - } - }, - "400": { - "description": "OAuth access token not found" + "404": { + "description": "Bank code not found." } }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Update OAuth access token", + "summary": "Update an existing bank code", "tags": [ - "OAuth Access Tokens" + "BankCodesControllers" ] }, "delete": { - "operationId": "OauthAccessTokensController_remove", + "operationId": "BankCodesControllers_remove", "parameters": [ { "name": "id", "required": true, "in": "path", "schema": { - "type": "string" + "type": "number" } } ], "responses": { - "201": { - "description": "OAuth access token deleted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OAuthAccessTokenEntity" - } - } - } - }, - "400": { - "description": "OAuth access token not found" + "404": { + "description": "Bank code not found." } }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Delete OAuth access token", + "summary": "Delete a bank code", "tags": [ - "OAuth Access Tokens" + "BankCodesControllers" ] } }, @@ -1634,31 +1468,197 @@ ] } }, - "/auth/login": { - "get": { - "operationId": "AuthController_login", + "/oauth-access-tokens": { + "post": { + "operationId": "OauthAccessTokensController_create", "parameters": [], - "responses": { - "200": { - "description": "" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOauthAccessTokenDto" + } + } } }, + "responses": { + "201": { + "description": "OAuth access token created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAccessTokenEntity" + } + } + } + }, + "400": { + "description": "Incomplete task or invalid data" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Create OAuth access token", "tags": [ - "Auth" + "OAuth Access Tokens" + ] + }, + "get": { + "operationId": "OauthAccessTokensController_findAll", + "parameters": [], + "responses": { + "201": { + "description": "List of OAuth access token found", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OAuthAccessTokenEntity" + } + } + } + } + }, + "400": { + "description": "List of OAuth access token not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find all OAuth access token", + "tags": [ + "OAuth Access Tokens" ] } }, - "/auth/callback": { + "/oauth-access-tokens/{id}": { "get": { - "operationId": "AuthController_loginCallback", - "parameters": [], + "operationId": "OauthAccessTokensController_findOne", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], "responses": { - "200": { - "description": "" + "201": { + "description": "OAuth access token found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAccessTokenEntity" + } + } + } + }, + "400": { + "description": "OAuth access token not found" } }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find OAuth access token", "tags": [ - "Auth" + "OAuth Access Tokens" + ] + }, + "patch": { + "operationId": "OauthAccessTokensController_update", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOauthAccessTokenDto" + } + } + } + }, + "responses": { + "201": { + "description": "OAuth access token updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAccessTokenEntity" + } + } + } + }, + "400": { + "description": "OAuth access token not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Update OAuth access token", + "tags": [ + "OAuth Access Tokens" + ] + }, + "delete": { + "operationId": "OauthAccessTokensController_remove", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "OAuth access token deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAccessTokenEntity" + } + } + } + }, + "400": { + "description": "OAuth access token not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Delete OAuth access token", + "tags": [ + "OAuth Access Tokens" ] } }, @@ -1850,14 +1850,6 @@ } }, "schemas": { - "CreateBankCodeDto": { - "type": "object", - "properties": {} - }, - "UpdateBankCodeDto": { - "type": "object", - "properties": {} - }, "CreateEmployeeDto": { "type": "object", "properties": { @@ -2337,6 +2329,130 @@ } } }, + "CreateBankCodeDto": { + "type": "object", + "properties": {} + }, + "UpdateBankCodeDto": { + "type": "object", + "properties": {} + }, + "CreateCustomerDto": { + "type": "object", + "properties": { + "first_name": { + "type": "string", + "example": "Gandalf", + "description": "Customer`s first name" + }, + "last_name": { + "type": "string", + "example": "TheGray", + "description": "Customer`s last name" + }, + "email": { + "type": "string", + "example": "you_shall_not_pass@middleEarth.com", + "description": "Customer`s email" + }, + "phone_number": { + "type": "number", + "example": "8436637464", + "description": "Customer`s phone number" + }, + "residence": { + "type": "string", + "example": "1 Ringbearer`s way, Mount Doom city, ME, T1R 1N6 ", + "description": "Customer`s residence" + }, + "invoice_id": { + "type": "number", + "example": "4263253", + "description": "Customer`s invoice number" + } + }, + "required": [ + "first_name", + "last_name", + "email", + "phone_number" + ] + }, + "CustomerEntity": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1, + "description": "Unique ID of a customer(primary-key, auto-incremented)" + }, + "user_id": { + "type": "string", + "example": "0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d", + "description": "UUID of the user linked to that customer" + }, + "email": { + "type": "string", + "example": "you_shall_not_pass@middleEarth.com", + "description": "customer`s email (optional)" + }, + "phone_number": { + "type": "number", + "example": 8436637464, + "description": "customer`s phone number (numbers only)" + }, + "residence": { + "type": "string", + "example": "1 Ringbearer’s way, Mount Doom city, ME, T1R 1N6", + "description": "customer`s residence address (optional)" + }, + "invoice_id": { + "type": "number", + "example": 4263253, + "description": "customer`s invoice number (optionnal, unique)" + } + }, + "required": [ + "id", + "user_id", + "phone_number" + ] + }, + "UpdateCustomerDto": { + "type": "object", + "properties": { + "first_name": { + "type": "string", + "example": "Gandalf", + "description": "Customer`s first name" + }, + "last_name": { + "type": "string", + "example": "TheGray", + "description": "Customer`s last name" + }, + "email": { + "type": "string", + "example": "you_shall_not_pass@middleEarth.com", + "description": "Customer`s email" + }, + "phone_number": { + "type": "number", + "example": "8436637464", + "description": "Customer`s phone number" + }, + "residence": { + "type": "string", + "example": "1 Ringbearer`s way, Mount Doom city, ME, T1R 1N6 ", + "description": "Customer`s residence" + }, + "invoice_id": { + "type": "number", + "example": "4263253", + "description": "Customer`s invoice number" + } + } + }, "CreateOauthAccessTokenDto": { "type": "object", "properties": { @@ -2515,122 +2631,6 @@ } } }, - "CreateCustomerDto": { - "type": "object", - "properties": { - "first_name": { - "type": "string", - "example": "Gandalf", - "description": "Customer`s first name" - }, - "last_name": { - "type": "string", - "example": "TheGray", - "description": "Customer`s last name" - }, - "email": { - "type": "string", - "example": "you_shall_not_pass@middleEarth.com", - "description": "Customer`s email" - }, - "phone_number": { - "type": "number", - "example": "8436637464", - "description": "Customer`s phone number" - }, - "residence": { - "type": "string", - "example": "1 Ringbearer`s way, Mount Doom city, ME, T1R 1N6 ", - "description": "Customer`s residence" - }, - "invoice_id": { - "type": "number", - "example": "4263253", - "description": "Customer`s invoice number" - } - }, - "required": [ - "first_name", - "last_name", - "email", - "phone_number" - ] - }, - "CustomerEntity": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1, - "description": "Unique ID of a customer(primary-key, auto-incremented)" - }, - "user_id": { - "type": "string", - "example": "0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d", - "description": "UUID of the user linked to that customer" - }, - "email": { - "type": "string", - "example": "you_shall_not_pass@middleEarth.com", - "description": "customer`s email (optional)" - }, - "phone_number": { - "type": "number", - "example": 8436637464, - "description": "customer`s phone number (numbers only)" - }, - "residence": { - "type": "string", - "example": "1 Ringbearer’s way, Mount Doom city, ME, T1R 1N6", - "description": "customer`s residence address (optional)" - }, - "invoice_id": { - "type": "number", - "example": 4263253, - "description": "customer`s invoice number (optionnal, unique)" - } - }, - "required": [ - "id", - "user_id", - "phone_number" - ] - }, - "UpdateCustomerDto": { - "type": "object", - "properties": { - "first_name": { - "type": "string", - "example": "Gandalf", - "description": "Customer`s first name" - }, - "last_name": { - "type": "string", - "example": "TheGray", - "description": "Customer`s last name" - }, - "email": { - "type": "string", - "example": "you_shall_not_pass@middleEarth.com", - "description": "Customer`s email" - }, - "phone_number": { - "type": "number", - "example": "8436637464", - "description": "Customer`s phone number" - }, - "residence": { - "type": "string", - "example": "1 Ringbearer`s way, Mount Doom city, ME, T1R 1N6 ", - "description": "Customer`s residence" - }, - "invoice_id": { - "type": "number", - "example": "4263253", - "description": "Customer`s invoice number" - } - } - }, "PayPeriodEntity": { "type": "object", "properties": { diff --git a/src/modules/exports/controllers/exports.controller.ts b/src/modules/exports/controllers/exports.controller.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/exports/exports.module.ts b/src/modules/exports/exports.module.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/exports/services/exports.service.ts b/src/modules/exports/services/exports.service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/exports/templates/summary.csv.hbs b/src/modules/exports/templates/summary.csv.hbs deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/shifts/validation/controllers/shifts-validation.controller.ts b/src/modules/shifts/validation/controllers/shifts-validation.controller.ts new file mode 100644 index 0000000..f5c7426 --- /dev/null +++ b/src/modules/shifts/validation/controllers/shifts-validation.controller.ts @@ -0,0 +1,51 @@ +import { Controller, Get, Header, Query } from "@nestjs/common"; +import { ShiftsValidationService, ValidationRow } from "../services/shifts-validation.service"; +import { GetShiftsValidationDto } from "../dtos/get-shifts-validation.dto"; + +@Controller() +export class ShiftsValidationController { + constructor(private readonly shiftsValidationService: ShiftsValidationService) {} + + @Get() + async getSummary( @Query() query: GetShiftsValidationDto): Promise { + return this.shiftsValidationService.getSummary(query.periodId); + } + + @Get('export.csv') + @Header('Content-Type', 'text/csv; charset=utf-8') + @Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"') + async exportCsv(@Query() query: GetShiftsValidationDto): Promise{ + const rows = await this.shiftsValidationService.getSummary(query.periodId); + + //CSV Headers + const header = [ + 'fullName', + 'supervisor', + 'totalRegularHrs', + 'totalEveningHrs', + 'totalOvertimeHrs', + 'totalExpenses', + 'totalMileage', + 'isValidated' + ].join(',') + '\n'; + + //CSV rows + const body = rows.map(r => { + const esc = (str: string) => `"${str.replace(/"/g, '""')}"`; + + return [ + esc(r.fullName), + esc(r.supervisor), + r.totalRegularHrs.toFixed(2), + r.totalEveningHrs.toFixed(2), + r.totalOvertimeHrs.toFixed(2), + r.totalExpenses.toFixed(2), + r.totalMileage.toFixed(2), + r.isValidated, + ].join(','); + }).join('\n'); + + return Buffer.from(header + body, 'utf8'); + } + +} \ No newline at end of file diff --git a/src/modules/shifts/validation/dtos/get-shifts-validation.dto.ts b/src/modules/shifts/validation/dtos/get-shifts-validation.dto.ts new file mode 100644 index 0000000..44b656b --- /dev/null +++ b/src/modules/shifts/validation/dtos/get-shifts-validation.dto.ts @@ -0,0 +1,10 @@ +import { Type } from "class-transformer"; +import { IsInt, Min, Max } from "class-validator"; + +export class GetShiftsValidationDto { + @Type(()=> Number) + @IsInt() + @Min(1) + @Max(26) + periodId: number; +} \ No newline at end of file diff --git a/src/modules/shifts/validation/services/shifts-validation.service.ts b/src/modules/shifts/validation/services/shifts-validation.service.ts new file mode 100644 index 0000000..5d05f40 --- /dev/null +++ b/src/modules/shifts/validation/services/shifts-validation.service.ts @@ -0,0 +1,122 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; + +export interface ValidationRow { + fullName: string; + supervisor: string; + totalRegularHrs: number; + totalEveningHrs: number; + totalOvertimeHrs: number; + totalExpenses: number; + totalMileage: number; + isValidated: boolean; +} + +@Injectable() +export class ShiftsValidationService { + constructor(private readonly prisma: PrismaService) {} + + private computeHours(start: Date, end: Date): number { + const diffMs = end.getTime() - start.getTime(); + const hours = diffMs / 1000 / 3600; + return parseFloat(hours.toFixed(2)); + } + + async getSummary(periodId: number): Promise { + //fetch pay-period to display + const period = await this.prisma.payPeriods.findUnique({ + where: { period_number: periodId }, + }); + if(!period) { + throw new NotFoundException(`pay-period ${periodId} not found`); + } + const { start_date, end_date } = period; + + //prepare shifts and expenses for display + const shifts = await this.prisma.shifts.findMany({ + where: { date: { gte: start_date, lte: end_date } }, + include: { + bank_code: true, + timesheet: { include: { employee: { + include: { user:true, + supervisor: { include: { user: true } }, + } }, + } }, + }, + }); + + const expenses = await this.prisma.expenses.findMany({ + where: { date: { gte: start_date, lte: end_date } }, + include: { + bank_code: true, + timesheet: { include: { employee: { + include: { user:true, + supervisor: { include: { user:true } }, + } }, + } }, + }, + }); + + const mapRow = new Map(); + + for(const s of shifts) { + const employeeId = s.timesheet.employee.user_id; + const user = s.timesheet.employee.user; + const sup = s.timesheet.employee.supervisor?.user; + + let row = mapRow.get(employeeId); + if(!row) { + row = { + fullName: `${user.first_name} ${user.last_name}`, + supervisor: sup? `${sup.first_name} ${sup.last_name }` : '', + totalRegularHrs: 0, + totalEveningHrs: 0, + totalOvertimeHrs: 0, + totalExpenses: 0, + totalMileage: 0, + isValidated: false, + }; + } + const hours = this.computeHours(s.start_time, s.end_time); + + switch(s.bank_code.type) { + case 'regular' : row.totalRegularHrs += hours; + break; + case 'evening' : row.totalEveningHrs += hours; + break; + case 'overtime' : row.totalOvertimeHrs += hours; + break; + default: row.totalRegularHrs += hours; + } + mapRow.set(employeeId, row); + } + + for(const e of expenses) { + const employeeId = e.timesheet.employee.user_id; + const user = e.timesheet.employee.user; + const sup = e.timesheet.employee.supervisor?.user; + + let row = mapRow.get(employeeId); + if(!row) { + row = { + fullName: `${user.first_name} ${user.last_name}`, + supervisor: sup? `${sup.first_name} ${sup.last_name }` : '', + totalRegularHrs: 0, + totalEveningHrs: 0, + totalOvertimeHrs: 0, + totalExpenses: 0, + totalMileage: 0, + isValidated: false, + }; + } + const amount = Number(e.amount); + row.totalExpenses += amount; + if(e.bank_code.type === 'mileage') { + row.totalMileage += amount; + } + mapRow.set(employeeId, row); + } + //return by default the list of employee in ascending alphabetical order + return Array.from(mapRow.values()).sort((a,b) => a.fullName.localeCompare(b.fullName)); + } +} diff --git a/src/modules/shifts/validation/shifts-validation.service.ts b/src/modules/shifts/validation/shifts-validation.service.ts new file mode 100644 index 0000000..277d2ac --- /dev/null +++ b/src/modules/shifts/validation/shifts-validation.service.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { ShiftsValidationController } from "./controllers/shifts-validation.controller"; +import { ShiftsValidationService } from "./services/shifts-validation.service"; +import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module"; + +@Module({ + imports: [BusinessLogicsModule], + controllers: [ShiftsValidationController], + providers: [ShiftsValidationService], +}) +export class ShiftsValidationModule {} \ No newline at end of file