diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 97fdf7e..f0c9119 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -3,7 +3,7 @@ "paths": { "/": { "get": { - "operationId": "AppController_getHello", + "operationId": "ShiftsValidationController_getSummary", "parameters": [], "responses": { "200": { @@ -11,7 +11,7 @@ } }, "tags": [ - "App" + "ShiftsValidation" ] } }, @@ -545,7 +545,607 @@ ] } }, +<<<<<<< HEAD "/auth/v1/login": { +======= + "/Expenses": { + "post": { + "operationId": "ExpensesController_create", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateExpenseDto" + } + } + } + }, + "responses": { + "201": { + "description": "Expense created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExpenseEntity" + } + } + } + }, + "400": { + "description": "Incomplete task or invalid data" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Create expense", + "tags": [ + "Expenses" + ] + }, + "get": { + "operationId": "ExpensesController_findAll", + "parameters": [], + "responses": { + "201": { + "description": "List of expenses found", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExpenseEntity" + } + } + } + } + }, + "400": { + "description": "List of expenses not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find all expenses", + "tags": [ + "Expenses" + ] + } + }, + "/Expenses/{id}": { + "get": { + "operationId": "ExpensesController_findOne", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "Expense found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExpenseEntity" + } + } + } + }, + "400": { + "description": "Expense not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find expense", + "tags": [ + "Expenses" + ] + }, + "patch": { + "operationId": "ExpensesController_update", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateExpenseDto" + } + } + } + }, + "responses": { + "201": { + "description": "Expense updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExpenseEntity" + } + } + } + }, + "400": { + "description": "Expense not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Expense shift", + "tags": [ + "Expenses" + ] + }, + "delete": { + "operationId": "ExpensesController_remove", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "Expense deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExpenseEntity" + } + } + } + }, + "400": { + "description": "Expense not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Delete expense", + "tags": [ + "Expenses" + ] + } + }, + "/shifts": { + "post": { + "operationId": "ShiftsController_create", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateShiftDto" + } + } + } + }, + "responses": { + "201": { + "description": "Shift created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShiftEntity" + } + } + } + }, + "400": { + "description": "Incomplete task or invalid data" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Create shift", + "tags": [ + "Shifts" + ] + }, + "get": { + "operationId": "ShiftsController_findAll", + "parameters": [], + "responses": { + "201": { + "description": "List of shifts found", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ShiftEntity" + } + } + } + } + }, + "400": { + "description": "List of shifts not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find all shifts", + "tags": [ + "Shifts" + ] + } + }, + "/shifts/{id}": { + "get": { + "operationId": "ShiftsController_findOne", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "Shift found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShiftEntity" + } + } + } + }, + "400": { + "description": "Shift not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find shift", + "tags": [ + "Shifts" + ] + }, + "patch": { + "operationId": "ShiftsController_update", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateShiftsDto" + } + } + } + }, + "responses": { + "201": { + "description": "Shift updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShiftEntity" + } + } + } + }, + "400": { + "description": "Shift not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Update shift", + "tags": [ + "Shifts" + ] + }, + "delete": { + "operationId": "ShiftsController_remove", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "Shift deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShiftEntity" + } + } + } + }, + "400": { + "description": "Shift not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Delete shift", + "tags": [ + "Shifts" + ] + } + }, + "/export.csv": { + "get": { + "operationId": "ShiftsValidationController_exportCsv", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "ShiftsValidation" + ] + } + }, + "/leave-requests": { + "post": { + "operationId": "LeaveRequestController_create", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateLeaveRequestsDto" + } + } + } + }, + "responses": { + "201": { + "description": "Leave request created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeaveRequestEntity" + } + } + } + }, + "400": { + "description": "Incomplete task or invalid data" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Create leave request", + "tags": [ + "Leave Requests" + ] + }, + "get": { + "operationId": "LeaveRequestController_findAll", + "parameters": [], + "responses": { + "201": { + "description": "List of Leave requests found", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LeaveRequestEntity" + } + } + } + } + }, + "400": { + "description": "List of leave request not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find all leave request", + "tags": [ + "Leave Requests" + ] + } + }, + "/leave-requests/{id}": { + "get": { + "operationId": "LeaveRequestController_findOne", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "Leave request found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeaveRequestEntity" + } + } + } + }, + "400": { + "description": "Leave request not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Find leave request", + "tags": [ + "Leave Requests" + ] + }, + "patch": { + "operationId": "LeaveRequestController_update", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateLeaveRequestsDto" + } + } + } + }, + "responses": { + "201": { + "description": "Leave request updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeaveRequestEntity" + } + } + } + }, + "400": { + "description": "Leave request not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Update leave request", + "tags": [ + "Leave Requests" + ] + }, + "delete": { + "operationId": "LeaveRequestController_remove", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "Leave request deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeaveRequestEntity" + } + } + } + }, + "400": { + "description": "Leave request not found" + } + }, + "security": [ + { + "access-token": [] + } + ], + "summary": "Delete leave request", + "tags": [ + "Leave Requests" + ] + } + }, + "/auth/login": { +>>>>>>> b0406b3a4c00223b9430ef29b60a4775beca4328 "get": { "operationId": "AuthController_login", "parameters": [], @@ -886,27 +1486,27 @@ ] } }, - "/oauth-access-tokens": { + "/oauth-sessions": { "post": { - "operationId": "OauthAccessTokensController_create", + "operationId": "OauthSessionsController_create", "parameters": [], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateOauthAccessTokenDto" + "$ref": "#/components/schemas/CreateOauthSessionDto" } } } }, "responses": { "201": { - "description": "OAuth access token created", + "description": "OAuth session created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OAuthAccessTokenEntity" + "$ref": "#/components/schemas/OAuthSessionEntity" } } } @@ -917,49 +1517,49 @@ }, "security": [ { - "access-token": [] + "sessions": [] } ], - "summary": "Create OAuth access token", + "summary": "Create OAuth session", "tags": [ - "OAuth Access Tokens" + "OAuth Sessions" ] }, "get": { - "operationId": "OauthAccessTokensController_findAll", + "operationId": "OauthSessionsController_findAll", "parameters": [], "responses": { "201": { - "description": "List of OAuth access token found", + "description": "List of OAuth session found", "content": { "application/json": { "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/OAuthAccessTokenEntity" + "$ref": "#/components/schemas/OAuthSessionEntity" } } } } }, "400": { - "description": "List of OAuth access token not found" + "description": "List of OAuth session not found" } }, "security": [ { - "access-token": [] + "sessions": [] } ], - "summary": "Find all OAuth access token", + "summary": "Find all OAuth session", "tags": [ - "OAuth Access Tokens" + "OAuth Sessions" ] } }, - "/oauth-access-tokens/{id}": { + "/oauth-sessions/{id}": { "get": { - "operationId": "OauthAccessTokensController_findOne", + "operationId": "OauthSessionsController_findOne", "parameters": [ { "name": "id", @@ -972,31 +1572,31 @@ ], "responses": { "201": { - "description": "OAuth access token found", + "description": "OAuth session found", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OAuthAccessTokenEntity" + "$ref": "#/components/schemas/OAuthSessionEntity" } } } }, "400": { - "description": "OAuth access token not found" + "description": "OAuth session not found" } }, "security": [ { - "access-token": [] + "sessions": [] } ], - "summary": "Find OAuth access token", + "summary": "Find OAuth session", "tags": [ - "OAuth Access Tokens" + "OAuth Sessions" ] }, "patch": { - "operationId": "OauthAccessTokensController_update", + "operationId": "OauthSessionsController_update", "parameters": [ { "name": "id", @@ -1012,38 +1612,38 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateOauthAccessTokenDto" + "$ref": "#/components/schemas/UpdateOauthSessionDto" } } } }, "responses": { "201": { - "description": "OAuth access token updated", + "description": "OAuth session updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OAuthAccessTokenEntity" + "$ref": "#/components/schemas/OAuthSessionEntity" } } } }, "400": { - "description": "OAuth access token not found" + "description": "OAuth session not found" } }, "security": [ { - "access-token": [] + "sessions": [] } ], - "summary": "Update OAuth access token", + "summary": "Update OAuth session", "tags": [ - "OAuth Access Tokens" + "OAuth Sessions" ] }, "delete": { - "operationId": "OauthAccessTokensController_remove", + "operationId": "OauthSessionsController_remove", "parameters": [ { "name": "id", @@ -1056,27 +1656,27 @@ ], "responses": { "201": { - "description": "OAuth access token deleted", + "description": "OAuth session deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OAuthAccessTokenEntity" + "$ref": "#/components/schemas/OAuthSessionEntity" } } } }, "400": { - "description": "OAuth access token not found" + "description": "OAuth session not found" } }, "security": [ { - "access-token": [] + "sessions": [] } ], - "summary": "Delete OAuth access token", + "summary": "Delete OAuth session", "tags": [ - "OAuth Access Tokens" + "OAuth Sessions" ] } }, @@ -1871,7 +2471,7 @@ } } }, - "CreateOauthAccessTokenDto": { + "CreateOauthSessionDto": { "type": "object", "properties": { "user_id": { @@ -1923,7 +2523,7 @@ "access_token_expiry" ] }, - "OAuthAccessTokenEntity": { + "OAuthSessionEntity": { "type": "object", "properties": { "id": { @@ -2004,7 +2604,7 @@ "created_at" ] }, - "UpdateOauthAccessTokenDto": { + "UpdateOauthSessionDto": { "type": "object", "properties": { "user_id": { diff --git a/prisma/migrations/20250804192610_rename_o_auth_table/migration.sql b/prisma/migrations/20250804192610_rename_o_auth_table/migration.sql new file mode 100644 index 0000000..82c4572 --- /dev/null +++ b/prisma/migrations/20250804192610_rename_o_auth_table/migration.sql @@ -0,0 +1,37 @@ +/* + Warnings: + + - You are about to drop the `refresh_tokens` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "refresh_tokens" DROP CONSTRAINT "refresh_tokens_user_id_fkey"; + +-- DropTable +DROP TABLE "refresh_tokens"; + +-- CreateTable +CREATE TABLE "oauth_sessions" ( + "id" TEXT NOT NULL, + "user_id" UUID NOT NULL, + "application" TEXT NOT NULL, + "access_token" TEXT NOT NULL, + "refresh_token" TEXT NOT NULL, + "access_token_expiry" TIMESTAMP(3) NOT NULL, + "refresh_token_expiry" TIMESTAMP(3), + "is_revoked" BOOLEAN NOT NULL DEFAULT false, + "scopes" JSONB NOT NULL DEFAULT '[]', + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3), + + CONSTRAINT "oauth_sessions_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "oauth_sessions_access_token_key" ON "oauth_sessions"("access_token"); + +-- CreateIndex +CREATE UNIQUE INDEX "oauth_sessions_refresh_token_key" ON "oauth_sessions"("refresh_token"); + +-- AddForeignKey +ALTER TABLE "oauth_sessions" ADD CONSTRAINT "oauth_sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250804192837_added_sid_column_to_o_auth_sessions/migration.sql b/prisma/migrations/20250804192837_added_sid_column_to_o_auth_sessions/migration.sql new file mode 100644 index 0000000..abce895 --- /dev/null +++ b/prisma/migrations/20250804192837_added_sid_column_to_o_auth_sessions/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - A unique constraint covering the columns `[sid]` on the table `oauth_sessions` will be added. If there are existing duplicate values, this will fail. + - Added the required column `sid` to the `oauth_sessions` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "oauth_sessions" ADD COLUMN "sid" TEXT NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "oauth_sessions_sid_key" ON "oauth_sessions"("sid"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index edb3ca1..3d317ee 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -25,7 +25,7 @@ model Users { employee Employees? @relation("UserEmployee") customer Customers? @relation("UserCustomer") - oauth_access_tokens OAuthAccessTokens[] @relation("UserOAuthAccessToken") + oauth_sessions OAuthSessions[] @relation("UserOAuthSessions") employees_archive EmployeesArchive[] @relation("UsersToEmployeesToArchive") customer_archive CustomersArchive[] @relation("UserToCustomersToArchive") @@ -245,13 +245,14 @@ model ExpensesArchive { @@map("expenses_archive") } -model OAuthAccessTokens { +model OAuthSessions { id String @id @default(cuid()) - user Users @relation("UserOAuthAccessToken", fields: [user_id], references: [id]) + user Users @relation("UserOAuthSessions", fields: [user_id], references: [id]) user_id String @db.Uuid application String access_token String @unique refresh_token String @unique + sid String @unique access_token_expiry DateTime refresh_token_expiry DateTime? is_revoked Boolean @default(false) @@ -259,7 +260,7 @@ model OAuthAccessTokens { created_at DateTime @default(now()) updated_at DateTime? - @@map("refresh_tokens") + @@map("oauth_sessions") } enum Roles { diff --git a/src/app.module.ts b/src/app.module.ts index 5f49b87..c22aaab 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,7 +5,6 @@ import { PrismaModule } from './prisma/prisma.module'; import { HealthModule } from './health/health.module'; import { HealthController } from './health/health.controller'; import { UsersModule } from './modules/users-management/users.module'; -import { OauthAccessTokensModule } from './modules/oauth-access-tokens/oauth-access-tokens.module'; import { CustomersModule } from './modules/customers/customers.module'; import { EmployeesModule } from './modules/employees/employees.module'; import { LeaveRequestsModule } from './modules/leave-requests/leave-requests.module'; @@ -19,6 +18,8 @@ import { ArchivalModule } from './modules/archival/archival.module'; import { BankCodesModule } from './modules/bank-codes/bank-codes.module'; import { OvertimeService } from './modules/business-logics/services/overtime.service'; import { BusinessLogicsModule } from './modules/business-logics/business-logics.module'; +import { ShiftsValidationModule } from './modules/shifts/validation/shifts-validation.module'; +import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.module'; @Module({ imports: [ @@ -32,10 +33,11 @@ import { BusinessLogicsModule } from './modules/business-logics/business-logics. ExpensesModule, HealthModule, LeaveRequestsModule, - OauthAccessTokensModule, + OauthSessionsModule, PayperiodsModule, PrismaModule, ShiftsModule, + ShiftsValidationModule, TimesheetsModule, UsersModule, ], diff --git a/src/modules/exports/controllers/csv-exports.controller.ts b/src/modules/exports/controllers/csv-exports.controller.ts new file mode 100644 index 0000000..60de449 --- /dev/null +++ b/src/modules/exports/controllers/csv-exports.controller.ts @@ -0,0 +1,45 @@ +import { Controller, Get, Header, Query, UseGuards } from "@nestjs/common"; +import { RolesGuard } from "src/common/guards/roles.guard"; +import { Roles as RoleEnum } from '.prisma/client'; +import { CsvExportService } from "../services/csv-exports.service"; +import { ExportCompany, ExportCsvOptionsDto, ExportType } from "../dtos/export-csv-options.dto"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; + + +@Controller('exports') +@UseGuards(RolesGuard) +export class CsvExportController { + constructor(private readonly csvService: CsvExportService) {} + + @Get('csv') + @Header('Content-Type', 'text/csv; charset=utf-8') + @Header('Content-Dispoition', 'attachment; filename="export.csv"') + @RolesAllowed(RoleEnum.ADMIN, RoleEnum.ACCOUNTING, RoleEnum.HR) + async exportCsv(@Query() options: ExportCsvOptionsDto, + @Query('period') periodId: string ): Promise { + + //sets default values + const companies = options.companies && options.companies.length ? options.companies : + [ ExportCompany.TARGO, ExportCompany.SOLUCOM]; + const types = options.type && options.type.length ? options.type : + Object.values(ExportType); + + //collects all + const all = await this.csvService.collectTransaction(Number(periodId), companies); + + //filters by type + const filtered = all.filter(r => { + switch (r.bankCode.toLocaleLowerCase()) { + case 'holiday' : return types.includes(ExportType.HOLIDAY); + case 'vacation' : return types.includes(ExportType.VACATION); + case 'sick-leave': return types.includes(ExportType.SICK_LEAVE); + case 'expenses' : return types.includes(ExportType.EXPENSES); + default : return types.includes(ExportType.SHIFTS); + } + }); + + //generating the csv file + return this.csvService.generateCsv(filtered); + } + +} \ No newline at end of file diff --git a/src/modules/exports/csv-exports.module.ts b/src/modules/exports/csv-exports.module.ts new file mode 100644 index 0000000..92a5a96 --- /dev/null +++ b/src/modules/exports/csv-exports.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { CsvExportController } from "./controllers/csv-exports.controller"; +import { CsvExportService } from "./services/csv-exports.service"; + +@Module({ + providers:[CsvExportService], + controllers: [CsvExportController], +}) +export class CsvExportModule {} + + diff --git a/src/modules/exports/dtos/export-csv-options.dto.ts b/src/modules/exports/dtos/export-csv-options.dto.ts new file mode 100644 index 0000000..dc969ad --- /dev/null +++ b/src/modules/exports/dtos/export-csv-options.dto.ts @@ -0,0 +1,26 @@ +import { IsArray, IsEnum, IsOptional } from "class-validator"; + +export enum ExportType { + SHIFTS = 'Quart de travail', + EXPENSES = 'Depenses', + HOLIDAY = 'Ferie', + VACATION = 'Vacance', + SICK_LEAVE = 'Absence' +} + +export enum ExportCompany { + TARGO = 'Targo', + SOLUCOM = 'Solucom', +} + +export class ExportCsvOptionsDto { + @IsOptional() + @IsArray() + @IsEnum(ExportCompany, { each: true }) + companies?: ExportCompany[]; + + @IsOptional() + @IsArray() + @IsEnum(ExportType, { each: true }) + type?: ExportType[]; +} \ No newline at end of file diff --git a/src/modules/exports/services/csv-exports.service.ts b/src/modules/exports/services/csv-exports.service.ts new file mode 100644 index 0000000..30c15c1 --- /dev/null +++ b/src/modules/exports/services/csv-exports.service.ts @@ -0,0 +1,188 @@ +import { PrismaService } from "src/prisma/prisma.service"; +import { ExportCompany } from "../dtos/export-csv-options.dto"; +import { Injectable, NotFoundException } from "@nestjs/common"; + +export interface CsvRow { + companyCode: number; + externalPayrollId: number; + fullName: string; + bankCode: string; + quantityHours?: number; + amount?: number; + weekNumber: number; + payDate: string; + holidayDate?: string; +} + +@Injectable() +export class CsvExportService { + constructor(private readonly prisma: PrismaService) {} + + async collectTransaction(periodId: number, companies: ExportCompany[]): Promise { + const companyCodes = companies.map(c => c === ExportCompany.TARGO ? 1 : 2); + + const period = await this.prisma.payPeriods.findUnique({ + where: { period_number: periodId }, + }); + if(!period) { + throw new NotFoundException(`Pay period ${periodId} not found`); + } + + const startDate = period.start_date; + const endDate = period.end_date; + + //fetching shifts + const shifts = await this.prisma.shifts.findMany({ + where: { date: { gte: startDate, lte: endDate }, + timesheet: { employee: { company_code: { in: companyCodes} } }, + }, + include: { bank_code: true, + timesheet: { include: {employee: { include: { user:true, + supervisor: { include: { user:true } }, + }}, + }}, + }, + }); + + //fetching expenses + const expenses = await this.prisma.expenses.findMany({ + where: { date: { gte: startDate, lte: endDate }, + timesheet: { employee: { company_code: { in: companyCodes} } }, + }, + include: { bank_code: true, + timesheet: { include: { employee: { include: { user: true, + supervisor: { include: { user:true } }, + } }, + } }, + }, + }); + + //fetching leave requests + const leaves = await this.prisma.leaveRequests.findMany({ + where : { start_date_time: { gte: startDate, lte: endDate }, + employee: { company_code: { in: companyCodes } }, + }, + include: { bank_code: true, + employee: { include: { user: true, + supervisor: { include: { user: true } }, + }}, + }, + }); + + const rows: CsvRow[] = []; + + //Shifts Mapping + for (const s of shifts) { + const emp = s.timesheet.employee; + const weekNumber = this.computeWeekNumber(startDate, s.date); + const hours = this.computeHours(s.start_time, s.end_time); + + rows.push({ + companyCode: emp.company_code, + externalPayrollId: emp.external_payroll_id, + fullName: `${emp.user.first_name} ${emp.user.last_name}`, + bankCode: s.bank_code.bank_code, + quantityHours: hours, + amount: undefined, + weekNumber, + payDate: this.formatDate(endDate), + holidayDate: undefined, + }); + } + + //Expenses Mapping + for (const e of expenses) { + const emp = e.timesheet.employee; + const weekNumber = this.computeWeekNumber(startDate, e.date); + + rows.push({ + companyCode: emp.company_code, + externalPayrollId: emp.external_payroll_id, + fullName: `${emp.user.first_name} ${emp.user.last_name}`, + bankCode: e.bank_code.bank_code, + quantityHours: undefined, + amount: Number(e.amount), + weekNumber, + payDate: this.formatDate(endDate), + holidayDate: undefined, + }); + } + + //Leaves Mapping + for(const l of leaves) { + if(!l.bank_code) continue; + const emp = l.employee; + const start = l.start_date_time; + const end = l.end_date_time ?? start; + + const weekNumber = this.computeWeekNumber(startDate, start); + const hours = this.computeHours(start, end); + + rows.push({ + companyCode: emp.company_code, + externalPayrollId: emp.external_payroll_id, + fullName: `${emp.user.first_name} ${emp.user.last_name}`, + bankCode: l.bank_code.bank_code, + quantityHours: hours, + amount: undefined, + weekNumber, + payDate: this.formatDate(endDate), + holidayDate: undefined, + }); + } + + //Final Mapping and sorts + return rows.sort((a,b) => { + if(a.externalPayrollId !== b.externalPayrollId) { + return a.externalPayrollId - b.externalPayrollId; + } + if(a.bankCode !== b.bankCode) { + return a.bankCode.localeCompare(b.bankCode); + } + return a.weekNumber - b.weekNumber; + }); + } + + generateCsv(rows: CsvRow[]): Buffer { + const header = [ + 'companyCode', + 'externalPayrolId', + 'fullName', + 'bankCode', + 'quantityHours', + 'amount', + 'weekNumber', + 'payDate', + 'holidayDate', + ].join(',') + '\n'; + + const body = rows.map(r => [ + r.companyCode, + r.externalPayrollId, + `${r.fullName.replace(/"/g, '""')}"`, + r.bankCode, + r.quantityHours?.toFixed(2) ?? '', + r.weekNumber, + r.payDate, + r.holidayDate ?? '', + ].join(',')).join('\n'); + + return Buffer.from('\uFEFF' + header + body, 'utf8'); + } + + + private computeHours(start: Date, end: Date): number { + const diffMs = end.getTime() - start.getTime(); + return +(diffMs / 1000 / 3600).toFixed(2); + } + + private computeWeekNumber(start: Date, date: Date): number { + const days = Math.floor((date.getTime() - start.getTime()) / (1000*60*60*24)); + return Math.floor(days / 7 ) + 1; + } + + private formatDate(d:Date): string { + return d.toISOString().split('T')[0]; + } + +} diff --git a/src/modules/oauth-access-tokens/controllers/oauth-access-tokens.controller.ts b/src/modules/oauth-access-tokens/controllers/oauth-access-tokens.controller.ts deleted file mode 100644 index 156f463..0000000 --- a/src/modules/oauth-access-tokens/controllers/oauth-access-tokens.controller.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common'; -import { OauthAccessTokensService } from '../services/oauth-access-tokens.service'; -import { CreateOauthAccessTokenDto } from '../dtos/create-oauth-access-token.dto'; -import { OAuthAccessTokens } from '@prisma/client'; -import { UpdateOauthAccessTokenDto } from '../dtos/update-oauth-access-token.dto'; -import { RolesAllowed } from "src/common/decorators/roles.decorators"; -import { Roles as RoleEnum } from '.prisma/client'; -import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { JwtAuthGuard } from 'src/modules/authentication/guards/jwt-auth.guard'; -import { OAuthAccessTokenEntity } from '../dtos/swagger-entities/oauth-access-token.entity'; - -@ApiTags('OAuth Access Tokens') -@ApiBearerAuth('access-token') -@UseGuards(JwtAuthGuard) -@Controller('oauth-access-tokens') -export class OauthAccessTokensController { - constructor(private readonly oauthAccessTokensService: OauthAccessTokensService){} - - @Post() - @RolesAllowed(RoleEnum.ADMIN) - @ApiOperation({summary: 'Create OAuth access token' }) - @ApiResponse({ status: 201, description: 'OAuth access token created', type: OAuthAccessTokenEntity }) - @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) - create(@Body()dto: CreateOauthAccessTokenDto): Promise { - return this.oauthAccessTokensService.create(dto); - } - - @Get() - @RolesAllowed(RoleEnum.ADMIN) - @ApiOperation({summary: 'Find all OAuth access token' }) - @ApiResponse({ status: 201, description: 'List of OAuth access token found', type: OAuthAccessTokenEntity, isArray: true }) - @ApiResponse({ status: 400, description: 'List of OAuth access token not found' }) - findAll(): Promise { - return this.oauthAccessTokensService.findAll(); - } - - @Get(':id') - @RolesAllowed(RoleEnum.ADMIN) - @ApiOperation({summary: 'Find OAuth access token' }) - @ApiResponse({ status: 201, description: 'OAuth access token found', type: OAuthAccessTokenEntity }) - @ApiResponse({ status: 400, description: 'OAuth access token not found' }) - findOne(@Param('id') id: string): Promise { - return this.oauthAccessTokensService.findOne(id); - } - - @Patch(':id') - @RolesAllowed(RoleEnum.ADMIN) - @ApiOperation({summary: 'Update OAuth access token' }) - @ApiResponse({ status: 201, description: 'OAuth access token updated', type: OAuthAccessTokenEntity }) - @ApiResponse({ status: 400, description: 'OAuth access token not found' }) - update(@Param('id') id: string, @Body() dto: UpdateOauthAccessTokenDto): Promise { - return this.oauthAccessTokensService.update(id,dto); - } - - @Delete(':id') - @RolesAllowed(RoleEnum.ADMIN) - @ApiOperation({summary: 'Delete OAuth access token' }) - @ApiResponse({ status: 201, description: 'OAuth access token deleted', type: OAuthAccessTokenEntity }) - @ApiResponse({ status: 400, description: 'OAuth access token not found' }) - remove(@Param('id') id: string): Promise { - return this.oauthAccessTokensService.remove(id); - } -} diff --git a/src/modules/oauth-access-tokens/dtos/update-oauth-access-token.dto.ts b/src/modules/oauth-access-tokens/dtos/update-oauth-access-token.dto.ts deleted file mode 100644 index 055c338..0000000 --- a/src/modules/oauth-access-tokens/dtos/update-oauth-access-token.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from "@nestjs/swagger"; -import { CreateOauthAccessTokenDto } from "./create-oauth-access-token.dto"; - -export class UpdateOauthAccessTokenDto extends PartialType(CreateOauthAccessTokenDto) {} diff --git a/src/modules/oauth-access-tokens/oauth-access-tokens.module.ts b/src/modules/oauth-access-tokens/oauth-access-tokens.module.ts deleted file mode 100644 index ca33da5..0000000 --- a/src/modules/oauth-access-tokens/oauth-access-tokens.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { OauthAccessTokensController } from './controllers/oauth-access-tokens.controller'; -import { OauthAccessTokensService } from './services/oauth-access-tokens.service'; -import { PrismaService } from 'src/prisma/prisma.service'; - -@Module({ - controllers: [OauthAccessTokensController], - providers: [OauthAccessTokensService, PrismaService] -}) -export class OauthAccessTokensModule {} diff --git a/src/modules/oauth-sessions/controllers/oauth-sessions.controller.ts b/src/modules/oauth-sessions/controllers/oauth-sessions.controller.ts new file mode 100644 index 0000000..08d30d5 --- /dev/null +++ b/src/modules/oauth-sessions/controllers/oauth-sessions.controller.ts @@ -0,0 +1,63 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common'; +import { OAuthSessions } from '@prisma/client'; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { Roles as RoleEnum } from '.prisma/client'; +import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { JwtAuthGuard } from 'src/modules/authentication/guards/jwt-auth.guard'; +import { CreateOauthSessionDto } from '../dtos/create-oauth-sessions.dto'; +import { OauthSessionsService } from '../services/oauth-sessions.service'; +import { OAuthSessionEntity } from '../dtos/swagger-entities/oauth-sessions.entity'; +import { UpdateOauthSessionDto } from '../dtos/update-oauth-sessions.dto'; + +@ApiTags('OAuth Sessions') +@ApiBearerAuth('sessions') +@UseGuards(JwtAuthGuard) +@Controller('oauth-sessions') +export class OauthSessionsController { + constructor(private readonly oauthSessionsService: OauthSessionsService){} + + @Post() + @RolesAllowed(RoleEnum.ADMIN) + @ApiOperation({summary: 'Create OAuth session' }) + @ApiResponse({ status: 201, description: 'OAuth session created', type: OAuthSessionEntity }) + @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) + create(@Body()dto: CreateOauthSessionDto): Promise { + return this.oauthSessionsService.create(dto); + } + + @Get() + @RolesAllowed(RoleEnum.ADMIN) + @ApiOperation({summary: 'Find all OAuth session' }) + @ApiResponse({ status: 201, description: 'List of OAuth session found', type: OAuthSessionEntity, isArray: true }) + @ApiResponse({ status: 400, description: 'List of OAuth session not found' }) + findAll(): Promise { + return this.oauthSessionsService.findAll(); + } + + @Get(':id') + @RolesAllowed(RoleEnum.ADMIN) + @ApiOperation({summary: 'Find OAuth session' }) + @ApiResponse({ status: 201, description: 'OAuth session found', type: OAuthSessionEntity }) + @ApiResponse({ status: 400, description: 'OAuth session not found' }) + findOne(@Param('id') id: string): Promise { + return this.oauthSessionsService.findOne(id); + } + + @Patch(':id') + @RolesAllowed(RoleEnum.ADMIN) + @ApiOperation({summary: 'Update OAuth session' }) + @ApiResponse({ status: 201, description: 'OAuth session updated', type: OAuthSessionEntity }) + @ApiResponse({ status: 400, description: 'OAuth session not found' }) + update(@Param('id') id: string, @Body() dto: UpdateOauthSessionDto): Promise { + return this.oauthSessionsService.update(id,dto); + } + + @Delete(':id') + @RolesAllowed(RoleEnum.ADMIN) + @ApiOperation({summary: 'Delete OAuth session' }) + @ApiResponse({ status: 201, description: 'OAuth session deleted', type: OAuthSessionEntity }) + @ApiResponse({ status: 400, description: 'OAuth session not found' }) + remove(@Param('id') id: string): Promise { + return this.oauthSessionsService.remove(id); + } +} diff --git a/src/modules/oauth-access-tokens/dtos/create-oauth-access-token.dto.ts b/src/modules/oauth-sessions/dtos/create-oauth-sessions.dto.ts similarity index 95% rename from src/modules/oauth-access-tokens/dtos/create-oauth-access-token.dto.ts rename to src/modules/oauth-sessions/dtos/create-oauth-sessions.dto.ts index 8e6d1ef..46e098a 100644 --- a/src/modules/oauth-access-tokens/dtos/create-oauth-access-token.dto.ts +++ b/src/modules/oauth-sessions/dtos/create-oauth-sessions.dto.ts @@ -2,7 +2,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsArray, IsDate, IsOptional, IsString, IsUUID } from "class-validator"; -export class CreateOauthAccessTokenDto { +export class CreateOauthSessionDto { @ApiProperty({ example: 'S7A2U8R7O6N6', @@ -18,6 +18,9 @@ export class CreateOauthAccessTokenDto { @IsString() application: string; + @IsString() + sid: string; + @ApiProperty({ example: 'L5O6R4D3/O6F3#T8H4E3&R6I4N6G4S7 ...', description: 'Access token', diff --git a/src/modules/oauth-access-tokens/dtos/swagger-entities/oauth-access-token.entity.ts b/src/modules/oauth-sessions/dtos/swagger-entities/oauth-sessions.entity.ts similarity index 97% rename from src/modules/oauth-access-tokens/dtos/swagger-entities/oauth-access-token.entity.ts rename to src/modules/oauth-sessions/dtos/swagger-entities/oauth-sessions.entity.ts index e80e6d7..c5c4c28 100644 --- a/src/modules/oauth-access-tokens/dtos/swagger-entities/oauth-access-token.entity.ts +++ b/src/modules/oauth-sessions/dtos/swagger-entities/oauth-sessions.entity.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -export class OAuthAccessTokenEntity { +export class OAuthSessionEntity { @ApiProperty({ example: 'cklwi0vb70000z2z20q6f19qk', description: 'Unique ID of an OAuth token (auto-generated)', diff --git a/src/modules/oauth-sessions/dtos/update-oauth-sessions.dto.ts b/src/modules/oauth-sessions/dtos/update-oauth-sessions.dto.ts new file mode 100644 index 0000000..5a2abef --- /dev/null +++ b/src/modules/oauth-sessions/dtos/update-oauth-sessions.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateOauthSessionDto } from "./create-oauth-sessions.dto"; + +export class UpdateOauthSessionDto extends PartialType(CreateOauthSessionDto) {} diff --git a/src/modules/oauth-sessions/oauth-sessions.module.ts b/src/modules/oauth-sessions/oauth-sessions.module.ts new file mode 100644 index 0000000..e68a481 --- /dev/null +++ b/src/modules/oauth-sessions/oauth-sessions.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { OauthSessionsController } from './controllers/oauth-sessions.controller'; +import { OauthSessionsService } from './services/oauth-sessions.service'; + +@Module({ + controllers: [OauthSessionsController], + providers: [OauthSessionsService, PrismaService] +}) +export class OauthSessionsModule {} diff --git a/src/modules/oauth-access-tokens/services/oauth-access-tokens.service.ts b/src/modules/oauth-sessions/services/oauth-sessions.service.ts similarity index 67% rename from src/modules/oauth-access-tokens/services/oauth-access-tokens.service.ts rename to src/modules/oauth-sessions/services/oauth-sessions.service.ts index 7a7a6a5..353f128 100644 --- a/src/modules/oauth-access-tokens/services/oauth-access-tokens.service.ts +++ b/src/modules/oauth-sessions/services/oauth-sessions.service.ts @@ -1,30 +1,32 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from 'src/prisma/prisma.service'; -import { CreateOauthAccessTokenDto } from '../dtos/create-oauth-access-token.dto'; -import { OAuthAccessTokens } from '@prisma/client'; -import { UpdateOauthAccessTokenDto } from '../dtos/update-oauth-access-token.dto'; +import { CreateOauthSessionDto } from '../dtos/create-oauth-sessions.dto'; +import { OAuthSessions } from '@prisma/client'; +import { UpdateOauthSessionDto } from '../dtos/update-oauth-sessions.dto'; @Injectable() -export class OauthAccessTokensService { +export class OauthSessionsService { constructor(private readonly prisma: PrismaService) {} - async create(dto: CreateOauthAccessTokenDto): Promise { + async create(dto: CreateOauthSessionDto): Promise { const { user_id, application, access_token, refresh_token, + sid, access_token_expiry, refresh_token_expiry, scopes, } = dto; - return this.prisma.oAuthAccessTokens.create({ + return this.prisma.oAuthSessions.create({ data: { user_id, application, access_token, refresh_token, + sid, access_token_expiry, refresh_token_expiry, scopes, @@ -33,14 +35,14 @@ export class OauthAccessTokensService { }); } - findAll(): Promise { - return this.prisma.oAuthAccessTokens.findMany({ + findAll(): Promise { + return this.prisma.oAuthSessions.findMany({ include: { user: true }, }); } - async findOne(id: string): Promise { - const token = await this.prisma.oAuthAccessTokens.findUnique({ + async findOne(id: string): Promise { + const token = await this.prisma.oAuthSessions.findUnique({ where: { id }, include: { user: true }, }); @@ -50,7 +52,7 @@ export class OauthAccessTokensService { return token; } - async update(id: string, dto: UpdateOauthAccessTokenDto): Promise { + async update(id: string, dto: UpdateOauthSessionDto): Promise { await this.findOne(id); const { user_id, @@ -62,7 +64,7 @@ export class OauthAccessTokensService { scopes, } = dto; - return this.prisma.oAuthAccessTokens.update({ + return this.prisma.oAuthSessions.update({ where: { id }, data: { ...(user_id !== undefined && { user_id }), @@ -77,8 +79,8 @@ export class OauthAccessTokensService { }); } - async remove(id: string): Promise { + async remove(id: string): Promise { await this.findOne(id); - return this.prisma.oAuthAccessTokens.delete({ where: { id }}); + return this.prisma.oAuthSessions.delete({ where: { id }}); } } diff --git a/src/modules/shifts/shifts.module.ts b/src/modules/shifts/shifts.module.ts index 09d63a1..57cd331 100644 --- a/src/modules/shifts/shifts.module.ts +++ b/src/modules/shifts/shifts.module.ts @@ -2,9 +2,13 @@ import { Module } from '@nestjs/common'; import { ShiftsController } from './controllers/shifts.controller'; import { ShiftsService } from './services/shifts.service'; import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module'; +import { ShiftsValidationModule } from './validation/shifts-validation.module'; @Module({ - imports: [BusinessLogicsModule], + imports: [ + BusinessLogicsModule, + ShiftsValidationModule, + ], controllers: [ShiftsController], providers: [ShiftsService], exports: [ShiftsService], diff --git a/src/modules/shifts/validation/controllers/shifts-validation.controller.ts b/src/modules/shifts/validation/controllers/shifts-validation.controller.ts index f5c7426..298808f 100644 --- a/src/modules/shifts/validation/controllers/shifts-validation.controller.ts +++ b/src/modules/shifts/validation/controllers/shifts-validation.controller.ts @@ -45,7 +45,7 @@ export class ShiftsValidationController { ].join(','); }).join('\n'); - return Buffer.from(header + body, 'utf8'); + return Buffer.from('\uFEFF' + header + body, 'utf8'); } } \ No newline at end of file diff --git a/src/modules/shifts/validation/shifts-validation.service.ts b/src/modules/shifts/validation/shifts-validation.module.ts similarity index 100% rename from src/modules/shifts/validation/shifts-validation.service.ts rename to src/modules/shifts/validation/shifts-validation.module.ts