feat(CSV): export to CSV modul fit filters
This commit is contained in:
parent
5df657d773
commit
5aac046356
|
|
@ -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;
|
||||||
|
|
@ -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");
|
||||||
|
|
@ -25,7 +25,7 @@ model Users {
|
||||||
|
|
||||||
employee Employees? @relation("UserEmployee")
|
employee Employees? @relation("UserEmployee")
|
||||||
customer Customers? @relation("UserCustomer")
|
customer Customers? @relation("UserCustomer")
|
||||||
oauth_access_tokens OAuthAccessTokens[] @relation("UserOAuthAccessToken")
|
oauth_sessions OAuthSessions[] @relation("UserOAuthSessions")
|
||||||
employees_archive EmployeesArchive[] @relation("UsersToEmployeesToArchive")
|
employees_archive EmployeesArchive[] @relation("UsersToEmployeesToArchive")
|
||||||
customer_archive CustomersArchive[] @relation("UserToCustomersToArchive")
|
customer_archive CustomersArchive[] @relation("UserToCustomersToArchive")
|
||||||
|
|
||||||
|
|
@ -245,13 +245,14 @@ model ExpensesArchive {
|
||||||
@@map("expenses_archive")
|
@@map("expenses_archive")
|
||||||
}
|
}
|
||||||
|
|
||||||
model OAuthAccessTokens {
|
model OAuthSessions {
|
||||||
id String @id @default(cuid())
|
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
|
user_id String @db.Uuid
|
||||||
application String
|
application String
|
||||||
access_token String @unique
|
access_token String @unique
|
||||||
refresh_token String @unique
|
refresh_token String @unique
|
||||||
|
sid String @unique
|
||||||
access_token_expiry DateTime
|
access_token_expiry DateTime
|
||||||
refresh_token_expiry DateTime?
|
refresh_token_expiry DateTime?
|
||||||
is_revoked Boolean @default(false)
|
is_revoked Boolean @default(false)
|
||||||
|
|
@ -259,7 +260,7 @@ model OAuthAccessTokens {
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
updated_at DateTime?
|
updated_at DateTime?
|
||||||
|
|
||||||
@@map("refresh_tokens")
|
@@map("oauth_sessions")
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Roles {
|
enum Roles {
|
||||||
|
|
|
||||||
45
src/modules/exports/controllers/csv-exports.controller.ts
Normal file
45
src/modules/exports/controllers/csv-exports.controller.ts
Normal file
|
|
@ -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<Buffer> {
|
||||||
|
|
||||||
|
//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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
11
src/modules/exports/csv-exports.module.ts
Normal file
11
src/modules/exports/csv-exports.module.ts
Normal file
|
|
@ -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 {}
|
||||||
|
|
||||||
|
|
||||||
26
src/modules/exports/dtos/export-csv-options.dto.ts
Normal file
26
src/modules/exports/dtos/export-csv-options.dto.ts
Normal file
|
|
@ -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[];
|
||||||
|
}
|
||||||
188
src/modules/exports/services/csv-exports.service.ts
Normal file
188
src/modules/exports/services/csv-exports.service.ts
Normal file
|
|
@ -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<CsvRow[]> {
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user