diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..2d07677 --- /dev/null +++ b/.env.development @@ -0,0 +1,38 @@ +DATABASE_URL="postgresql://apptargo:6wLAZrb0HZnd3mrmqXiArPcqLyui0o9e@10.100.0.116/app_targo_db_dev?schema=public" +DATABASE_URL_LEGACY="postgresql://genieacs:DnZHC3XezD7A8keEtaUocqPw@10.100.0.116/targo?schema=public" + + +AUTHENTIK_ISSUER="https://auth.targo.ca/application/o/montargo/" +AUTHENTIK_CLIENT_ID="KUmSmvpu2aDDy4JfNwas7XriNFtPcj2Ka2PyLO5v" +AUTHENTIK_CLIENT_SECRET="N55BgX1mxT7eiY99LOo5zXr5cKz9FgTsaCA9MdC7D8ZuhOGqozvqtNXVGbpY1eCg2kkYwJeJLP89sQ8R4cYybIJI7EwKijb19bzZQpUPwBosWwG3irUwdTnZOyw8yW5i" +AUTHENTIK_CALLBACK_URL="http://localhost:3000/auth/callback" +AUTHENTIK_AUTH_URL="https://auth.targo.ca/application/o/authorize/" +AUTHENTIK_TOKEN_URL="https://auth.targo.ca/application/o/token/" +AUTHENTIK_USERINFO_URL="https://auth.targo.ca/application/o/userinfo/" + +REDIRECT_URL_STAGING="http://10.100.251.2:9013/#/login-success" +REDIRECT_URL_DEV="http://localhost:9000/#/login-success" + +TARGO_FRONTEND_URI="http://localhost:9000/" + +ATTACHMENTS_SERVER_ID="server" +ATTACHMENTS_ROOT=C:/ + +#ATTACHMENT_SERVER_SECRET="*" +#ATTACHEMENT_SERVER_PASSWORD="enterpasswordhere" + +#attachments storage variables, manage max amount of MB per upload and types +MAX_UPLOAD_MB=25 +ALLOWED_MIME=image/jpeg,image/png,image/webp,application/pdf + +#attachment archive variables: +ARCHIVE_CRON=0 3 * * 1 #checkup every monday +ARCHIVE_BATCH_SIZE=1000 #max batch size to avoid large locks + +#attachment garbage collector variables: +GC_CRON=15 4 * * * #everyday at 04h15 +GC_BTACH_SIZE= 500 + +#attachment variants variables, REDIS and BULL variables: +REDIS_URL=redis://localhost:6379 +BULL_PREFIX=attachments \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..0fe05bd --- /dev/null +++ b/.env.production @@ -0,0 +1,37 @@ +DATABASE_URL="postgresql://apptargo:6wLAZrb0HZnd3mrmqXiArPcqLyui0o9e@10.100.0.116/app_targo_db?schema=public" + +AUTHENTIK_ISSUER="https://auth.targo.ca/application/o/montargo/" +AUTHENTIK_CLIENT_ID="KUmSmvpu2aDDy4JfNwas7XriNFtPcj2Ka2PyLO5v" +AUTHENTIK_CLIENT_SECRET="N55BgX1mxT7eiY99LOo5zXr5cKz9FgTsaCA9MdC7D8ZuhOGqozvqtNXVGbpY1eCg2kkYwJeJLP89sQ8R4cYybIJI7EwKijb19bzZQpUPwBosWwG3irUwdTnZOyw8yW5i" +AUTHENTIK_CALLBACK_URL="http://localhost:3000/auth/callback" +AUTHENTIK_AUTH_URL="https://auth.targo.ca/application/o/authorize/" +AUTHENTIK_TOKEN_URL="https://auth.targo.ca/application/o/token/" +AUTHENTIK_USERINFO_URL="https://auth.targo.ca/application/o/userinfo/" + +REDIRECT_URL_STAGING="http://10.100.251.2:9013/#/login-success" +REDIRECT_URL_DEV="http://localhost:9000/#/login-success" + +TARGO_FRONTEND_URI="http://localhost:9000/" + +ATTACHMENTS_SERVER_ID="server" +ATTACHMENTS_ROOT=C:/ + +#ATTACHMENT_SERVER_SECRET="*" +#ATTACHEMENT_SERVER_PASSWORD="enterpasswordhere" + +#attachments storage variables, manage max amount of MB per upload and types +MAX_UPLOAD_MB=25 +ALLOWED_MIME=image/jpeg,image/png,image/webp,application/pdf + +#attachment archive variables: +ARCHIVE_CRON=0 3 * * 1 #checkup every monday +ARCHIVE_BATCH_SIZE=1000 #max batch size to avoid large locks + +#attachment garbage collector variables: +GC_CRON=15 4 * * * #everyday at 04h15 +GC_BTACH_SIZE= 500 + +#attachment variants variables, REDIS and BULL variables: +REDIS_URL=redis://localhost:6379 +BULL_PREFIX=attachments + diff --git a/.env.staging b/.env.staging new file mode 100644 index 0000000..71ad9e3 --- /dev/null +++ b/.env.staging @@ -0,0 +1,29 @@ +DATABASE_URL="postgresql://apptargo:6wLAZrb0HZnd3mrmqXiArPcqLyui0o9e@10.100.0.116/app_targo_db_staging?schema=public" +DATABASE_URL_LEGACY="postgresql://genieacs:DnZHC3XezD7A8keEtaUocqPw@10.100.0.116/targo?schema=public" + +AUTHENTIK_ISSUER="https://auth.targo.ca/application/o/montargo/" +AUTHENTIK_CLIENT_ID="KUmSmvpu2aDDy4JfNwas7XriNFtPcj2Ka2PyLO5v" +AUTHENTIK_CLIENT_SECRET="N55BgX1mxT7eiY99LOo5zXr5cKz9FgTsaCA9MdC7D8ZuhOGqozvqtNXVGbpY1eCg2kkYwJeJLP89sQ8R4cYybIJI7EwKijb19bzZQpUPwBosWwG3irUwdTnZOyw8yW5i" +AUTHENTIK_CALLBACK_URL="http://localhost:3000/auth/callback" +AUTHENTIK_AUTH_URL="https://auth.targo.ca/application/o/authorize/" +AUTHENTIK_TOKEN_URL="https://auth.targo.ca/application/o/token/" +AUTHENTIK_USERINFO_URL="https://auth.targo.ca/application/o/userinfo/" + +REDIRECT_URL_STAGING="http://10.100.251.2:9013/#/login-success" +REDIRECT_URL_DEV="http://localhost:9000/#/login-success" + +TARGO_FRONTEND_URI="http://localhost:9000/" + +ATTACHMENTS_SERVER_ID="server" +ATTACHMENTS_ROOT=C:/ + +MAX_UPLOAD_MB=25 +ALLOWED_MIME=image/jpeg,image/png,image/webp,application/pdf + +#attachment garbage collector variables: +GC_CRON=15 4 * * * #everyday at 04h15 +GC_BTACH_SIZE= 500 + +#attachment variants variables, REDIS and BULL variables: +REDIS_URL=redis://localhost:6379 +BULL_PREFIX=attachments \ No newline at end of file diff --git a/src/time-and-attendance/domains/services/overtime.service.ts b/src/time-and-attendance/domains/services/overtime.service.ts index 211f4a5..ecfe3d2 100644 --- a/src/time-and-attendance/domains/services/overtime.service.ts +++ b/src/time-and-attendance/domains/services/overtime.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Prisma, PrismaClient } from '@prisma/client'; import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils'; import { PrismaService } from 'src/prisma/prisma.service'; diff --git a/src/time-and-attendance/exports/csv-exports.controller.ts b/src/time-and-attendance/exports/csv-exports.controller.ts index fde70ad..cb80e8f 100644 --- a/src/time-and-attendance/exports/csv-exports.controller.ts +++ b/src/time-and-attendance/exports/csv-exports.controller.ts @@ -1,12 +1,17 @@ import { Controller, Get, Param, Query, Res } from "@nestjs/common"; -import { CsvExportService } from "./csv-exports.service"; +import { CsvExportService } from "./services/csv-exports.service"; import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators"; import { Modules as ModulesEnum } from ".prisma/client"; import { Response } from "express"; +import { CsvGeneratorService } from "src/time-and-attendance/exports/services/csv-builder.service"; @Controller('exports') export class CsvExportController { - constructor(private readonly csvService: CsvExportService) { } + constructor( + private readonly csvService: CsvExportService, + private readonly generator: CsvGeneratorService, + + ) { } @Get('csv/:year/:period_no') @ModuleAccessAllowed(ModulesEnum.employee_management) @@ -39,7 +44,7 @@ export class CsvExportController { }, } ); - const csv_buffer = await this.csvService.generateCsv(rows); + const csv_buffer = await this.generator.generateCsv(rows); response.set({ 'Content-Type': 'text/csv; charset=utf-8', diff --git a/src/time-and-attendance/exports/csv-exports.module.ts b/src/time-and-attendance/exports/csv-exports.module.ts index 51f86c3..338de0b 100644 --- a/src/time-and-attendance/exports/csv-exports.module.ts +++ b/src/time-and-attendance/exports/csv-exports.module.ts @@ -1,11 +1,13 @@ import { Module } from "@nestjs/common"; import { CsvExportController } from "./csv-exports.controller"; -import { CsvExportService } from "./csv-exports.service"; +import { CsvExportService } from "./services/csv-exports.service"; +import { CsvGeneratorService } from "src/time-and-attendance/exports/services/csv-builder.service"; +import { OvertimeService } from "src/time-and-attendance/domains/services/overtime.service"; @Module({ - providers:[CsvExportService], + providers: [CsvExportService, CsvGeneratorService, OvertimeService], controllers: [CsvExportController], }) -export class CsvExportModule {} +export class CsvExportModule { } diff --git a/src/time-and-attendance/exports/csv-exports.utils.ts b/src/time-and-attendance/exports/csv-exports.utils.ts new file mode 100644 index 0000000..52ee9e7 --- /dev/null +++ b/src/time-and-attendance/exports/csv-exports.utils.ts @@ -0,0 +1,104 @@ +import { OvertimeService } from "src/time-and-attendance/domains/services/overtime.service"; +import { CsvRow, InternalCsvRow } from "src/time-and-attendance/exports/export-csv-options.dto"; + +const REGULAR = 'G1'; +const OVERTIME = 'G43'; + +export const consolidateRowHoursAndAmountByType = (rows: InternalCsvRow[]): InternalCsvRow[] => { + const map = new Map(); + + for (const row of rows) { + const key = `${row.timesheet_id}|${row.bank_code}|${row.week_number}|${row.shift_date.toISOString()}`; + if (!map.has(key)) { + map.set(key, { ...row }); + } else { + const existing = map.get(key)!; + existing.quantity_hours = (existing.quantity_hours ?? 0) + (row.quantity_hours ?? 0); + existing.amount = (existing.amount ?? 0) + (row.amount ?? 0); + } + } + return Array.from(map.values()); +} + +export const applyOvertimeRequalifications = async ( + consolidated_rows: InternalCsvRow[], + overtime_service: OvertimeService, +): Promise => { + const result: CsvRow[] = []; + //grouped by timesheet and week number + const grouped_rows = new Map(); + + for (const row of consolidated_rows) { + const key = `${row.timesheet_id}|${row.week_number}`; + if (!grouped_rows.has(key)) { + grouped_rows.set(key, []); + } + grouped_rows.get(key)!.push({ ...row }); + } + + for (const [, rows] of grouped_rows) { + //serves only to get the right timesheet_id and a date to find the "week_start of the getWeekOvertimeSummary" + const representative = rows[0]; + const summary = await overtime_service.getWeekOvertimeSummary(representative.timesheet_id, representative.shift_date); + if (!summary.success || summary.data.total_overtime <= 0) { + result.push(...rows); + continue; + } + + const overtime_hours = summary.data.total_overtime; + + const regular_hours = rows.find(r => r.bank_code === REGULAR); + if (!regular_hours || !regular_hours.quantity_hours) { + result.push(...rows); + continue; + } + + const deducted = Math.min(overtime_hours, regular_hours.quantity_hours); + + for (const row of rows) { + if (row === regular_hours) { + const remaining = regular_hours.quantity_hours - deducted; + if (remaining > 0) { + result.push({ ...regular_hours, quantity_hours: remaining }); + } + } else { + result.push(row); + } + } + + result.push({ + ...regular_hours, + bank_code: OVERTIME, + quantity_hours: deducted, + }); + } + + return result; +} + +export const resolveCompanyCodes = (companies: { targo: boolean; solucom: boolean; }): number[] => { + const out: number[] = []; + if (companies.targo) { + const code_no = 271583; + out.push(code_no); + } + if (companies.solucom) { + const code_no = 271585; + out.push(code_no); + } + return out; +} + +export const computeWeekNumber = (start: Date, date: Date): number => { + const dayMS = 86400000; + const days = Math.floor((toUTC(date).getTime() - toUTC(start).getTime()) / dayMS); + return Math.floor(days / 7) + 1; +} + +export const toUTC = (date: Date) => { + return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); +} + +export const formatDate = (d: Date): string => { + return d.toISOString().split('T')[0]; +} \ No newline at end of file diff --git a/src/time-and-attendance/exports/export-csv-options.dto.ts b/src/time-and-attendance/exports/export-csv-options.dto.ts index b9c1e4b..190be8d 100644 --- a/src/time-and-attendance/exports/export-csv-options.dto.ts +++ b/src/time-and-attendance/exports/export-csv-options.dto.ts @@ -58,6 +58,9 @@ export interface CsvRow { holiday_date?: string; } +export type InternalCsvRow = CsvRow & { timesheet_id: number; shift_date: Date; } + + export type Filters = { types: { shifts: boolean; diff --git a/src/time-and-attendance/exports/services/csv-builder.service.ts b/src/time-and-attendance/exports/services/csv-builder.service.ts new file mode 100644 index 0000000..c6b944e --- /dev/null +++ b/src/time-and-attendance/exports/services/csv-builder.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from "@nestjs/common"; +import { CsvRow } from "src/time-and-attendance/exports/export-csv-options.dto"; + +@Injectable() +export class CsvGeneratorService { + //csv builder and "mise en page" + generateCsv(rows: CsvRow[]): Buffer { + const header = [ + 'company_code', + 'external_payroll_id', + 'full_name', + 'bank_code', + 'quantity_hours', + 'amount', + 'week_number', + 'pay_date', + 'holiday_date', + ].join(';') + '\n'; + + const body = rows.map(row => { + const full_name = `${String(row.full_name).replace(/"/g, '""')}`; + const quantity_hours = (typeof row.quantity_hours === 'number') ? row.quantity_hours.toFixed(2) : ''; + const amount = (typeof row.amount === 'number') ? row.amount.toFixed(2) : ''; + return [ + row.company_code, + row.external_payroll_id, + full_name, + row.bank_code, + quantity_hours, + amount, + row.week_number, + row.pay_date, + row.holiday_date ?? '', + ].join(';'); + }).join('\n'); + return Buffer.from('\uFEFF' + header + body, 'utf8'); + } +} \ No newline at end of file diff --git a/src/time-and-attendance/exports/csv-exports.service.ts b/src/time-and-attendance/exports/services/csv-exports.service.ts similarity index 71% rename from src/time-and-attendance/exports/csv-exports.service.ts rename to src/time-and-attendance/exports/services/csv-exports.service.ts index 87a7a46..41f25a3 100644 --- a/src/time-and-attendance/exports/csv-exports.service.ts +++ b/src/time-and-attendance/exports/services/csv-exports.service.ts @@ -1,13 +1,16 @@ import { PrismaService } from "src/prisma/prisma.service"; import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -import { Filters, CsvRow } from "src/time-and-attendance/exports/export-csv-options.dto"; +import { Filters, CsvRow, InternalCsvRow } from "src/time-and-attendance/exports/export-csv-options.dto"; import { computeHours } from "src/common/utils/date-utils"; - - +import { applyOvertimeRequalifications, computeWeekNumber, consolidateRowHoursAndAmountByType, formatDate, resolveCompanyCodes } from "src/time-and-attendance/exports/csv-exports.utils"; +import { OvertimeService } from "src/time-and-attendance/domains/services/overtime.service"; @Injectable() export class CsvExportService { - constructor(private readonly prisma: PrismaService) { } + constructor( + private readonly prisma: PrismaService, + private readonly overtime_service: OvertimeService, + ) { } async collectTransaction( year: number, @@ -26,7 +29,7 @@ export class CsvExportService { const end = period.period_end; //fetch company codes - const company_codes = this.resolveCompanyCodes(filters.companies); + const company_codes = resolveCompanyCodes(filters.companies); if (company_codes.length === 0) throw new BadRequestException('No company selected'); //Flag types @@ -59,6 +62,7 @@ export class CsvExportService { bank_code: { select: { bank_code: true } }, timesheet: { select: { + id: true, employee: { select: { company_code: true, @@ -89,6 +93,7 @@ export class CsvExportService { bank_code: { select: { bank_code: true } }, timesheet: { select: { + id: true, employee: { select: { company_code: true, @@ -119,6 +124,7 @@ export class CsvExportService { bank_code: { select: { bank_code: true } }, timesheet: { select: { + id: true, employee: { select: { company_code: true, @@ -147,6 +153,7 @@ export class CsvExportService { bank_code: { select: { bank_code: true } }, timesheet: { select: { + id: true, employee: { select: { company_code: true, @@ -165,22 +172,24 @@ export class CsvExportService { //array of arrays const [base_shifts, holiday_shifts, vacation_shifts, expenses] = await Promise.all(promises); //mapping - const rows: CsvRow[] = []; + const rows: InternalCsvRow[] = []; - const map_shifts = (shift: any, is_holiday: boolean) => { + const map_shifts = (shift: any, is_holiday: boolean): InternalCsvRow => { const employee = shift.timesheet.employee; - const week = this.computeWeekNumber(start, shift.date); + const week = computeWeekNumber(start, shift.date); return { company_code: employee.company_code, external_payroll_id: employee.external_payroll_id, + timesheet_id: shift.timesheet.id, + shift_date: shift.date, full_name: `${employee.user.first_name} ${employee.user.last_name}`, bank_code: shift.bank_code?.bank_code ?? '', quantity_hours: computeHours(shift.start_time, shift.end_time), amount: undefined, week_number: week, - pay_date: this.formatDate(end), - holiday_date: is_holiday ? this.formatDate(shift.date) : '', - } as CsvRow; + pay_date: formatDate(end), + holiday_date: is_holiday ? formatDate(shift.date) : '', + } }; //final mapping of all shifts based filters for (const shift of base_shifts) rows.push(map_shifts(shift, false)); @@ -189,17 +198,19 @@ export class CsvExportService { for (const expense of expenses) { const employee = expense.timesheet.employee; - const week = this.computeWeekNumber(start, expense.date); + const week = computeWeekNumber(start, expense.date); rows.push({ company_code: employee.company_code, external_payroll_id: employee.external_payroll_id, + timesheet_id: expense.timesheet.id, full_name: `${employee.user.first_name} ${employee.user.last_name}`, bank_code: expense.bank_code?.bank_code ?? '', quantity_hours: undefined, amount: Number(expense.amount), week_number: week, - pay_date: this.formatDate(end), + pay_date: formatDate(end), holiday_date: '', + shift_date : expense.date, }) } @@ -214,32 +225,11 @@ export class CsvExportService { return 0; }); - const consolidated_rows = this.consolidateRowHoursAndAmountByType(rows) + const consolidated_rows = consolidateRowHoursAndAmountByType(rows); - return consolidated_rows; - } + const requalified_rows = await applyOvertimeRequalifications(consolidated_rows, this.overtime_service); - consolidateRowHoursAndAmountByType = (rows: CsvRow[]): CsvRow[] => { - type ConsolidateRow = CsvRow & { quantity_hours: number, amount: number }; - const shifts_map = new Map(); - - for (const row of rows) { - const key = `${row.company_code}|${row.external_payroll_id}|${row.full_name}|${row.bank_code}|${row.week_number}`; - const hours = row.quantity_hours ?? 0; - const amounts = row.amount ?? 0; - if (!shifts_map.has(key)) { - shifts_map.set(key, { - ...row, - quantity_hours: hours, - amount: amounts, - }); - } else { - const existing = shifts_map.get(key)!; - existing.quantity_hours += hours; - existing.amount += amounts; - } - } - return Array.from(shifts_map.values()); + return requalified_rows; } resolveHolidayTypeCode = async (holiday: string): Promise => { @@ -247,6 +237,11 @@ export class CsvExportService { where: { type: holiday }, select: { bank_code: true, + shifts: { + select: { + date: true, + }, + }, }, }); if (!holiday_code) throw new BadRequestException('Missing Holiday bank code'); @@ -259,69 +254,15 @@ export class CsvExportService { where: { type: vacation }, select: { bank_code: true, + shifts: { + select: { + date: true, + }, + }, }, }); if (!vacation_code) throw new BadRequestException('Missing Vacation bank code'); return vacation_code.bank_code; } - resolveCompanyCodes(companies: { targo: boolean; solucom: boolean; }): number[] { - const out: number[] = []; - if (companies.targo) { - const code_no = 271583; - out.push(code_no); - } - if (companies.solucom) { - const code_no = 271585; - out.push(code_no); - } - return out; - } - - //csv builder and "mise en page" - generateCsv(rows: CsvRow[]): Buffer { - const header = [ - 'company_code', - 'external_payroll_id', - 'full_name', - 'bank_code', - 'quantity_hours', - 'amount', - 'week_number', - 'pay_date', - 'holiday_date', - ].join(',') + '\n'; - - const body = rows.map(row => { - const full_name = `${String(row.full_name).replace(/"/g, '""')}`; - const quantity_hours = (typeof row.quantity_hours === 'number') ? row.quantity_hours.toFixed(2) : ''; - const amount = (typeof row.amount === 'number') ? row.amount.toFixed(2) : ''; - return [ - row.company_code, - row.external_payroll_id, - full_name, - row.bank_code, - quantity_hours, - amount, - row.week_number, - row.pay_date, - row.holiday_date ?? '', - ].join(','); - }).join('\n'); - return Buffer.from('\uFEFF' + header + body, 'utf8'); - } - - private computeWeekNumber(start: Date, date: Date): number { - const dayMS = 86400000; - const days = Math.floor((this.toUTC(date).getTime() - this.toUTC(start).getTime()) / dayMS); - return Math.floor(days / 7) + 1; - } - toUTC(date: Date) { - return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); - } - - private formatDate(d: Date): string { - return d.toISOString().split('T')[0]; - } - } diff --git a/src/time-and-attendance/time-and-attendance.module.ts b/src/time-and-attendance/time-and-attendance.module.ts index 767e06f..36fb698 100644 --- a/src/time-and-attendance/time-and-attendance.module.ts +++ b/src/time-and-attendance/time-and-attendance.module.ts @@ -22,7 +22,7 @@ import { PayPeriodsQueryService } from "src/time-and-attendance/pay-period/servi import { PayPeriodsCommandService } from "src/time-and-attendance/pay-period/services/pay-periods-command.service"; import { CsvExportModule } from "src/time-and-attendance/exports/csv-exports.module"; -import { CsvExportService } from "src/time-and-attendance/exports/csv-exports.service"; +import { CsvExportService } from "src/time-and-attendance/exports/services/csv-exports.service"; import { CsvExportController } from "src/time-and-attendance/exports/csv-exports.controller"; import { ShiftController } from "src/time-and-attendance/shifts/shift.controller"; @@ -38,6 +38,7 @@ import { SchedulePresetDeleteService } from "src/time-and-attendance/schedule-pr import { SchedulePresetUpdateService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-update.service"; import { SchedulePresetsCreateService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-create.service"; import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service"; +import { CsvGeneratorService } from "src/time-and-attendance/exports/services/csv-builder.service"; @Module({ imports: [ @@ -46,8 +47,8 @@ import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-pr TimesheetsModule, ExpensesModule, PayperiodsModule, - CsvExportModule, - SchedulePresetsModule, + CsvExportModule, + SchedulePresetsModule, ], controllers: [ TimesheetController, @@ -56,7 +57,7 @@ import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-pr ExpenseController, PayPeriodsController, CsvExportController, - + ], providers: [ GetTimesheetsOverviewService, @@ -78,6 +79,7 @@ import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-pr PayPeriodsQueryService, PayPeriodsCommandService, CsvExportService, + CsvGeneratorService, ], - exports: [TimesheetApprovalService ], + exports: [TimesheetApprovalService], }) export class TimeAndAttendanceModule { }; \ No newline at end of file