diff --git a/.gitea/workflows/node-ci.yaml b/.gitea/workflows/node-ci.yaml new file mode 100644 index 0000000..480e91e --- /dev/null +++ b/.gitea/workflows/node-ci.yaml @@ -0,0 +1,111 @@ +name: Node-CI + + +on: + push: + branches: [main] + paths-ignore: + - '**.md' + - 'docs/**' + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-24.04 + steps: + - name: Gitea Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + - name: Notify Google Chat + if: ${{ failure() }} # Use always to ensure that the notification is also send on failure of former steps + uses: SimonScholz/google-chat-action@3b3519e5102dba8aa5046fd711c4b553586409bb # v1.1.0 + with: + webhookUrl: '${{ secrets.GOOGLE_CHAT_WEBHOOK }}' + jobStatus: ${{ job.status }} + title: Lint failed + + test: + runs-on: ubuntu-24.04 + steps: + - name: Gitea Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test -- --passWithNoTests + + - name: Notify Google Chat + if: ${{ failure() }} # Use always to ensure that the notification is also send on failure of former steps + uses: SimonScholz/google-chat-action@3b3519e5102dba8aa5046fd711c4b553586409bb # v1.1.0 + with: + webhookUrl: '${{ secrets.GOOGLE_CHAT_WEBHOOK }}' + jobStatus: ${{ job.status }} + title: Test failed + + build: + runs-on: ubuntu-24.04 + steps: + - name: Gitea Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Generate Prisma + run: npm run prisma:generate + + - name: Run build + run: npm run build + + - name: Create and deploy Container image + if: ${{ success() }} + run: | + VERSION_NUMBER=$(date +'%y%m%d.%H%M%S') + docker build -t git.targo.ca/targo/targo-backend-staging:2.${VERSION_NUMBER} . + docker tag git.targo.ca/targo/targo-backend-staging:2.${VERSION_NUMBER} git.targo.ca/targo/targo-backend-staging:latest + docker login -u ${{ secrets.CI_USER }} -p ${{ secrets.CI_PASSWORD }} git.targo.ca + docker push git.targo.ca/targo/targo-backend-staging:2.${VERSION_NUMBER} + docker push git.targo.ca/targo/targo-backend-staging:latest + curl --location 'https://n8napi.targo.ca/webhook/portainer' --header 'Authorization: Basic ${{ secrets.API_SECRET}}' --form 'stack="new_targo_app_staging"' + + - name: Notify Google Chat + if: ${{ failure() }} # Use always to ensure that the notification is also send on failure of former steps + uses: SimonScholz/google-chat-action@3b3519e5102dba8aa5046fd711c4b553586409bb # v1.1.0 + with: + webhookUrl: '${{ secrets.GOOGLE_CHAT_WEBHOOK }}' + jobStatus: ${{ job.status }} + title: Build failed diff --git a/Dockerfile b/Dockerfile index 48a2cb4..a86cee0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,52 +8,14 @@ WORKDIR /app RUN yarn global add @quasar/cli # Set the environment variables -ENV DATABASE_URL_PROD="postgresql://apptargo:6wLAZrb0HZnd3mrmqXiArPcqLyui0o9e@10.100.0.116/app_targo_db?schema=public" -ENV DATABASE_URL_STAGING="postgresql://apptargo:6wLAZrb0HZnd3mrmqXiArPcqLyui0o9e@10.100.0.116/app_targo_db_staging?schema=public" -ENV DATABASE_URL_DEV="postgresql://apptargo:6wLAZrb0HZnd3mrmqXiArPcqLyui0o9e@10.100.0.116/app_targo_db_dev?schema=public" - -# this section is for the mariadb connection setup, DEV VARIABLES ****** -#ENV DATABASE_URL_MARIADB= "mysql://matthieu:targo123@10.100.80.100:3306/testgc?schema=public" -#ENV DATABASE_HOST="10.100.80.100" -#ENV DATABASE_USER="matthieu" -#ENV DATABASE_PASSWORD="targo123" -#ENV DATABASE_NAME="testgc" - -ENV AUTHENTIK_ISSUER="https://auth.targo.ca/application/o/montargo/" -ENV AUTHENTIK_CLIENT_ID="KUmSmvpu2aDDy4JfNwas7XriNFtPcj2Ka2PyLO5v" -ENV AUTHENTIK_CLIENT_SECRET="N55BgX1mxT7eiY99LOo5zXr5cKz9FgTsaCA9MdC7D8ZuhOGqozvqtNXVGbpY1eCg2kkYwJeJLP89sQ8R4cYybIJI7EwKijb19bzZQpUPwBosWwG3irUwdTnZOyw8yW5i" - -ARG DB_URL -ARG CALLBACK_URL -ENV AUTHENTIK_CALLBACK_URL=$CALLBACK_URL -ENV DATABASE_URL=$DB_URL -#ENV AUTHENTIK_CALLBACK_URL="http://10.100.251.2:3420/auth/callback" -ENV AUTHENTIK_AUTH_URL="https://auth.targo.ca/application/o/authorize/" -ENV AUTHENTIK_TOKEN_URL="https://auth.targo.ca/application/o/token/" -ENV AUTHENTIK_USERINFO_URL="https://auth.targo.ca/application/o/userinfo/" - -ENV TARGO_FRONTEND_URI="http://10.100.251.2/" - ENV ATTACHMENTS_SERVER_ID="server" ENV ATTACHMENTS_ROOT=C:/ ENV MAX_UPLOAD_MB=25 ENV ALLOWED_MIME=image/jpeg,image/png,image/webp,application/pdf -# Copy package.json and package-lock.json to the working directory -COPY package*.json ./ - -# Install the application dependencies -RUN npm install - # Copy the rest of the application files COPY . . -# Generate Prisma client -RUN npm run prisma:generate - -# Build the NestJS application -RUN npm run build - # Expose the application port EXPOSE 3000 diff --git a/package.json b/package.json index e2363be..f733651 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "start:variants": "node dist/attachments/workers/variants.worker.js", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix || true", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", diff --git a/prisma/postgres/schema.prisma b/prisma/postgres/schema.prisma index 59ec36d..74749f9 100644 --- a/prisma/postgres/schema.prisma +++ b/prisma/postgres/schema.prisma @@ -16,6 +16,7 @@ model Users { phone_number String residence String? role Roles @default(EMPLOYEE) + notifications Notifications? @relation("UserNotification") employee Employees? @relation("UserEmployee") oauth_sessions OAuthSessions[] @relation("UserOAuthSessions") preferences Preferences? @relation("UserPreferences") @@ -24,6 +25,20 @@ model Users { @@map("users") } +model Notifications { + id Int @id @default(autoincrement()) + user_id String @unique @db.Uuid + affected_module Modules + subject String + description String + metadata Json @db.JsonB + created_at DateTime @default(now()) + viewed_at DateTime? + user Users @relation("UserNotification", fields: [user_id], references: [id]) + + @@map("notifications") +} + model userModuleAccess { id Int @id @default(autoincrement()) user_id String @unique @db.Uuid diff --git a/prisma/postgres/scripts/create-table-notifications.sql b/prisma/postgres/scripts/create-table-notifications.sql new file mode 100644 index 0000000..c4128de --- /dev/null +++ b/prisma/postgres/scripts/create-table-notifications.sql @@ -0,0 +1,15 @@ +CREATE TABLE notifications ( + id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + user_id uuid NOT NULL, + affected_module text, + subject text NOT NULL, + description text, + metadata jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + viewed_at timestamptz NULL, + + CONSTRAINT notifications_user_id_fkey + FOREIGN KEY (user_id) + REFERENCES users(id) + ON DELETE CASCADE +); \ No newline at end of file diff --git a/src/chatbot/chatbot.module.ts b/src/chatbot/chatbot.module.ts index 6dd97e1..4011c65 100644 --- a/src/chatbot/chatbot.module.ts +++ b/src/chatbot/chatbot.module.ts @@ -4,14 +4,13 @@ import { HttpModule } from '@nestjs/axios'; import { ChatbotService } from 'src/chatbot/chatbot.service'; @Module({ - imports: [ - HttpModule, - ], - controllers: [ - ChatbotController, - ], - providers: [ - ChatbotService, - ], + imports: [ + HttpModule.register({ + timeout: 5 * 60 * 1000, // 5 minutes in milliseconds + }) + ], + controllers: [ChatbotController], + providers: [ChatbotService], + exports: [], }) export class ChatbotModule { } diff --git a/src/chatbot/chatbot.service.ts b/src/chatbot/chatbot.service.ts index fc39bfb..abee33c 100644 --- a/src/chatbot/chatbot.service.ts +++ b/src/chatbot/chatbot.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; -import { ChatbotResponseDto, UserMessageDto } from 'src/chatbot/dtos/user-message.dto'; import { Message } from 'src/chatbot/dtos/dialog-message.dto'; +import { ChatbotResponseDto, UserMessageDto } from 'src/chatbot/dtos/user-message.dto'; @Injectable() export class ChatbotService { @@ -10,14 +10,40 @@ export class ChatbotService { sessionId: string = 'testing'; async pingExternalApi(body: UserMessageDto, email: string): Promise { - const { data } = await firstValueFrom(this.httpService.post( - 'https://n8nai.targo.ca/webhook/chatty-Mcbot', - { userInput: body.userInput, userId: email, sessionId: this.sessionId, pageContext: body.pageContext ?? undefined } - )) as ChatbotResponseDto; + try { + const response = await firstValueFrom(this.httpService.post( + 'https://n8nai.targo.ca/webhook/chatty-Mcbot', + { + userInput: body.userInput, + userId: email, + sessionId: this.sessionId, + pageContext: body.pageContext ?? undefined + } + ))as ChatbotResponseDto; - return { - text: data[0].output, - sent: false, - }; + if (!response.data) + return { + text: 'chatbot.error.NO_DATA_RECEIVED', + sent: false, + } + + if (!response.data[0].output) + return { + text: 'chatbot.error.NO_OUTPUT_RECEIVED', + sent: false, + } + + return { + text: response.data[0].output, + sent: false, + }; + } catch (error) { + console.error(error); + + return { + text: 'chatbot.error.GENERIC_RESPONSE_ERROR', + sent: false, + } + } } } diff --git a/src/main.ts b/src/main.ts index 5b0f683..3b9574c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -46,7 +46,7 @@ async function bootstrap() { // Enable CORS app.enableCors({ - origin: ['http://10.100.251.2:9011', 'http://10.100.251.2:9012', 'http://10.100.251.2:9013', 'http://localhost:9000', 'https://app.targo.ca', 'https://portail.targo.ca', 'https://staging.app.targo.ca'], + origin: ['http://10.100.251.2:9011', 'http://10.5.14.111:9012', 'http://10.100.251.2:9013', 'http://localhost:9000', 'https://app.targo.ca', 'https://portail.targo.ca','https://staging.app.targo.ca'], credentials: true, }); diff --git a/src/time-and-attendance/expenses/expense.controller.ts b/src/time-and-attendance/expenses/expense.controller.ts index 7a8caf4..3cc2ba3 100644 --- a/src/time-and-attendance/expenses/expense.controller.ts +++ b/src/time-and-attendance/expenses/expense.controller.ts @@ -19,11 +19,12 @@ export class ExpenseController { @Post('create') @ModuleAccessAllowed(ModulesEnum.timesheets) create( - @Access('email') email: string, - @Body() dto: ExpenseDto + @Body() dto: ExpenseDto, + @Access('email') email: string, + @Query('employee_email') employee_email?: string, ): Promise> { if (!email) throw new UnauthorizedException('Unauthorized User'); - return this.createService.createExpense(dto, email); + return this.createService.createExpense(dto, email, employee_email); } @Patch('update') diff --git a/src/time-and-attendance/expenses/services/expense-create.service.ts b/src/time-and-attendance/expenses/services/expense-create.service.ts index 5beb9bd..4da0de3 100644 --- a/src/time-and-attendance/expenses/services/expense-create.service.ts +++ b/src/time-and-attendance/expenses/services/expense-create.service.ts @@ -18,13 +18,19 @@ export class ExpenseCreateService { private readonly payPeriodEventService: PayPeriodEventService, ) { } + //_________________________________________________________________ + // CREATE + //_________________________________________________________________ async createExpense( - dto: ExpenseDto, - email: string + dto: ExpenseDto, + email: string, + employee_email?: string ): Promise> { try { + const accountEmail = employee_email ?? email; + //fetch employee_id using req.user.email - const employee_id = await this.emailResolver.findIdByEmail(email); + const employee_id = await this.emailResolver.findIdByEmail(accountEmail); if (!employee_id.success) return { success: false, error: employee_id.error }; //normalize strings and dates and Parse numbers @@ -68,7 +74,7 @@ export class ExpenseCreateService { // notify timesheet approval observers of changes this.payPeriodEventService.emit({ - employee_email: email, + employee_email: accountEmail, event_type: 'expense', action: 'create', }); diff --git a/src/time-and-attendance/exports/csv-exports.utils.ts b/src/time-and-attendance/exports/csv-exports.utils.ts index b212c48..ea30769 100644 --- a/src/time-and-attendance/exports/csv-exports.utils.ts +++ b/src/time-and-attendance/exports/csv-exports.utils.ts @@ -12,9 +12,9 @@ export const consolidateRowHoursAndAmountByType = (rows: InternalCsvRow[]): Inte for (const row of rows) { if (row.code === VACATION) { - map.set(`${row.code}|${row.shift_date}`, row); + map.set(`${row.code}|${row.shift_date.toString()}|${row.timesheet_id}`, row); } else { - const key = `${row.code}|${row.semaine_no}`; + const key = `${row.code}|${row.timesheet_id}`; if (!map.has(key)) { map.set(key, row); } else { diff --git a/src/time-and-attendance/exports/services/csv-exports.service.ts b/src/time-and-attendance/exports/services/csv-exports.service.ts index 2920f62..0b7184a 100644 --- a/src/time-and-attendance/exports/services/csv-exports.service.ts +++ b/src/time-and-attendance/exports/services/csv-exports.service.ts @@ -139,16 +139,13 @@ export class CsvExportService { dernier_jour_absence: undefined, }); }); - } // Sort shifts and expenses according to their bank codes - rows.sort((a, b) => { - if (a.code !== b.code) - return a.code - b.code; - - return 0; - }); + rows.sort((a, b) => + a.code - b.code || + a.employee_matricule - b.employee_matricule + ); const holiday_rows = await applyHolidayRequalifications(rows, this.holiday_service, HOLIDAY_SHIFT_CODE[0]); const consolidated_rows = consolidateRowHoursAndAmountByType(holiday_rows); diff --git a/src/time-and-attendance/timesheets/services/timesheet-employee-overview.service.ts b/src/time-and-attendance/timesheets/services/timesheet-employee-overview.service.ts index d1341f8..716cfe1 100644 --- a/src/time-and-attendance/timesheets/services/timesheet-employee-overview.service.ts +++ b/src/time-and-attendance/timesheets/services/timesheet-employee-overview.service.ts @@ -54,7 +54,7 @@ export class GetTimesheetsOverviewService { //find user infos using the employee_id const employee = await this.prisma.employees.findUnique({ where: { id: employee_id.data }, - include: { schedule_preset: true, user: true }, + select: { daily_expected_hours: true, schedule_preset: true, user: true }, }); if (!employee) return { success: false, error: `EMPLOYEE_NOT_FOUND` } @@ -68,7 +68,14 @@ export class GetTimesheetsOverviewService { const timesheets = await Promise.all(rows.map((timesheet) => mapOneTimesheet(timesheet))); if (!timesheets) return { success: false, error: 'INVALID_TIMESHEET' } - return { success: true, data: { has_preset_schedule, employee_fullname, timesheets } }; + const data: Timesheets = { + has_preset_schedule, + employee_fullname, + daily_expected_hours: employee.daily_expected_hours, + timesheets, + } + + return { success: true, data }; } catch (error) { console.error(error); return { success: false, error: 'TIMESHEET_NOT_FOUND' } diff --git a/src/time-and-attendance/timesheets/timesheet.dto.ts b/src/time-and-attendance/timesheets/timesheet.dto.ts index 0f5682a..56e5acd 100644 --- a/src/time-and-attendance/timesheets/timesheet.dto.ts +++ b/src/time-and-attendance/timesheets/timesheet.dto.ts @@ -11,6 +11,7 @@ export class TimesheetEntity { export class Timesheets { @IsBoolean() has_preset_schedule: boolean; @IsString() employee_fullname: string; + @IsInt() daily_expected_hours: number; @Type(() => Timesheet) timesheets: Timesheet[]; }