fix(csv): added overtime calculation and requalification of regular time unto overtime when daily or weekly maxes are reached
This commit is contained in:
parent
124a80f9bf
commit
379104147c
38
.env.development
Normal file
38
.env.development
Normal file
|
|
@ -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
|
||||||
37
.env.production
Normal file
37
.env.production
Normal file
|
|
@ -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
|
||||||
|
|
||||||
29
.env.staging
Normal file
29
.env.staging
Normal file
|
|
@ -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
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Prisma, PrismaClient } from '@prisma/client';
|
import { Prisma, PrismaClient } from '@prisma/client';
|
||||||
import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils';
|
import { getWeekStart, getWeekEnd, computeHours } from 'src/common/utils/date-utils';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import { Controller, Get, Param, Query, Res } from "@nestjs/common";
|
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 { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
|
||||||
import { Modules as ModulesEnum } from ".prisma/client";
|
import { Modules as ModulesEnum } from ".prisma/client";
|
||||||
import { Response } from "express";
|
import { Response } from "express";
|
||||||
|
import { CsvGeneratorService } from "src/time-and-attendance/exports/services/csv-builder.service";
|
||||||
|
|
||||||
@Controller('exports')
|
@Controller('exports')
|
||||||
export class CsvExportController {
|
export class CsvExportController {
|
||||||
constructor(private readonly csvService: CsvExportService) { }
|
constructor(
|
||||||
|
private readonly csvService: CsvExportService,
|
||||||
|
private readonly generator: CsvGeneratorService,
|
||||||
|
|
||||||
|
) { }
|
||||||
|
|
||||||
@Get('csv/:year/:period_no')
|
@Get('csv/:year/:period_no')
|
||||||
@ModuleAccessAllowed(ModulesEnum.employee_management)
|
@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({
|
response.set({
|
||||||
'Content-Type': 'text/csv; charset=utf-8',
|
'Content-Type': 'text/csv; charset=utf-8',
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { CsvExportController } from "./csv-exports.controller";
|
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({
|
@Module({
|
||||||
providers:[CsvExportService],
|
providers: [CsvExportService, CsvGeneratorService, OvertimeService],
|
||||||
controllers: [CsvExportController],
|
controllers: [CsvExportController],
|
||||||
})
|
})
|
||||||
export class CsvExportModule {}
|
export class CsvExportModule { }
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
104
src/time-and-attendance/exports/csv-exports.utils.ts
Normal file
104
src/time-and-attendance/exports/csv-exports.utils.ts
Normal file
|
|
@ -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<string, InternalCsvRow>();
|
||||||
|
|
||||||
|
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<CsvRow[]> => {
|
||||||
|
const result: CsvRow[] = [];
|
||||||
|
//grouped by timesheet and week number
|
||||||
|
const grouped_rows = new Map<string, InternalCsvRow[]>();
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
@ -58,6 +58,9 @@ export interface CsvRow {
|
||||||
holiday_date?: string;
|
holiday_date?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InternalCsvRow = CsvRow & { timesheet_id: number; shift_date: Date; }
|
||||||
|
|
||||||
|
|
||||||
export type Filters = {
|
export type Filters = {
|
||||||
types: {
|
types: {
|
||||||
shifts: boolean;
|
shifts: boolean;
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
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 { 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()
|
@Injectable()
|
||||||
export class CsvExportService {
|
export class CsvExportService {
|
||||||
constructor(private readonly prisma: PrismaService) { }
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly overtime_service: OvertimeService,
|
||||||
|
) { }
|
||||||
|
|
||||||
async collectTransaction(
|
async collectTransaction(
|
||||||
year: number,
|
year: number,
|
||||||
|
|
@ -26,7 +29,7 @@ export class CsvExportService {
|
||||||
const end = period.period_end;
|
const end = period.period_end;
|
||||||
|
|
||||||
//fetch company codes
|
//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');
|
if (company_codes.length === 0) throw new BadRequestException('No company selected');
|
||||||
|
|
||||||
//Flag types
|
//Flag types
|
||||||
|
|
@ -59,6 +62,7 @@ export class CsvExportService {
|
||||||
bank_code: { select: { bank_code: true } },
|
bank_code: { select: { bank_code: true } },
|
||||||
timesheet: {
|
timesheet: {
|
||||||
select: {
|
select: {
|
||||||
|
id: true,
|
||||||
employee: {
|
employee: {
|
||||||
select: {
|
select: {
|
||||||
company_code: true,
|
company_code: true,
|
||||||
|
|
@ -89,6 +93,7 @@ export class CsvExportService {
|
||||||
bank_code: { select: { bank_code: true } },
|
bank_code: { select: { bank_code: true } },
|
||||||
timesheet: {
|
timesheet: {
|
||||||
select: {
|
select: {
|
||||||
|
id: true,
|
||||||
employee: {
|
employee: {
|
||||||
select: {
|
select: {
|
||||||
company_code: true,
|
company_code: true,
|
||||||
|
|
@ -119,6 +124,7 @@ export class CsvExportService {
|
||||||
bank_code: { select: { bank_code: true } },
|
bank_code: { select: { bank_code: true } },
|
||||||
timesheet: {
|
timesheet: {
|
||||||
select: {
|
select: {
|
||||||
|
id: true,
|
||||||
employee: {
|
employee: {
|
||||||
select: {
|
select: {
|
||||||
company_code: true,
|
company_code: true,
|
||||||
|
|
@ -147,6 +153,7 @@ export class CsvExportService {
|
||||||
bank_code: { select: { bank_code: true } },
|
bank_code: { select: { bank_code: true } },
|
||||||
timesheet: {
|
timesheet: {
|
||||||
select: {
|
select: {
|
||||||
|
id: true,
|
||||||
employee: {
|
employee: {
|
||||||
select: {
|
select: {
|
||||||
company_code: true,
|
company_code: true,
|
||||||
|
|
@ -165,22 +172,24 @@ export class CsvExportService {
|
||||||
//array of arrays
|
//array of arrays
|
||||||
const [base_shifts, holiday_shifts, vacation_shifts, expenses] = await Promise.all(promises);
|
const [base_shifts, holiday_shifts, vacation_shifts, expenses] = await Promise.all(promises);
|
||||||
//mapping
|
//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 employee = shift.timesheet.employee;
|
||||||
const week = this.computeWeekNumber(start, shift.date);
|
const week = computeWeekNumber(start, shift.date);
|
||||||
return {
|
return {
|
||||||
company_code: employee.company_code,
|
company_code: employee.company_code,
|
||||||
external_payroll_id: employee.external_payroll_id,
|
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}`,
|
full_name: `${employee.user.first_name} ${employee.user.last_name}`,
|
||||||
bank_code: shift.bank_code?.bank_code ?? '',
|
bank_code: shift.bank_code?.bank_code ?? '',
|
||||||
quantity_hours: computeHours(shift.start_time, shift.end_time),
|
quantity_hours: computeHours(shift.start_time, shift.end_time),
|
||||||
amount: undefined,
|
amount: undefined,
|
||||||
week_number: week,
|
week_number: week,
|
||||||
pay_date: this.formatDate(end),
|
pay_date: formatDate(end),
|
||||||
holiday_date: is_holiday ? this.formatDate(shift.date) : '',
|
holiday_date: is_holiday ? formatDate(shift.date) : '',
|
||||||
} as CsvRow;
|
}
|
||||||
};
|
};
|
||||||
//final mapping of all shifts based filters
|
//final mapping of all shifts based filters
|
||||||
for (const shift of base_shifts) rows.push(map_shifts(shift, false));
|
for (const shift of base_shifts) rows.push(map_shifts(shift, false));
|
||||||
|
|
@ -189,17 +198,19 @@ export class CsvExportService {
|
||||||
|
|
||||||
for (const expense of expenses) {
|
for (const expense of expenses) {
|
||||||
const employee = expense.timesheet.employee;
|
const employee = expense.timesheet.employee;
|
||||||
const week = this.computeWeekNumber(start, expense.date);
|
const week = computeWeekNumber(start, expense.date);
|
||||||
rows.push({
|
rows.push({
|
||||||
company_code: employee.company_code,
|
company_code: employee.company_code,
|
||||||
external_payroll_id: employee.external_payroll_id,
|
external_payroll_id: employee.external_payroll_id,
|
||||||
|
timesheet_id: expense.timesheet.id,
|
||||||
full_name: `${employee.user.first_name} ${employee.user.last_name}`,
|
full_name: `${employee.user.first_name} ${employee.user.last_name}`,
|
||||||
bank_code: expense.bank_code?.bank_code ?? '',
|
bank_code: expense.bank_code?.bank_code ?? '',
|
||||||
quantity_hours: undefined,
|
quantity_hours: undefined,
|
||||||
amount: Number(expense.amount),
|
amount: Number(expense.amount),
|
||||||
week_number: week,
|
week_number: week,
|
||||||
pay_date: this.formatDate(end),
|
pay_date: formatDate(end),
|
||||||
holiday_date: '',
|
holiday_date: '',
|
||||||
|
shift_date : expense.date,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,32 +225,11 @@ export class CsvExportService {
|
||||||
return 0;
|
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[] => {
|
return requalified_rows;
|
||||||
type ConsolidateRow = CsvRow & { quantity_hours: number, amount: number };
|
|
||||||
const shifts_map = new Map<string, ConsolidateRow>();
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveHolidayTypeCode = async (holiday: string): Promise<string> => {
|
resolveHolidayTypeCode = async (holiday: string): Promise<string> => {
|
||||||
|
|
@ -247,6 +237,11 @@ export class CsvExportService {
|
||||||
where: { type: holiday },
|
where: { type: holiday },
|
||||||
select: {
|
select: {
|
||||||
bank_code: true,
|
bank_code: true,
|
||||||
|
shifts: {
|
||||||
|
select: {
|
||||||
|
date: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!holiday_code) throw new BadRequestException('Missing Holiday bank code');
|
if (!holiday_code) throw new BadRequestException('Missing Holiday bank code');
|
||||||
|
|
@ -259,69 +254,15 @@ export class CsvExportService {
|
||||||
where: { type: vacation },
|
where: { type: vacation },
|
||||||
select: {
|
select: {
|
||||||
bank_code: true,
|
bank_code: true,
|
||||||
|
shifts: {
|
||||||
|
select: {
|
||||||
|
date: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!vacation_code) throw new BadRequestException('Missing Vacation bank code');
|
if (!vacation_code) throw new BadRequestException('Missing Vacation bank code');
|
||||||
return vacation_code.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];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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 { 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 { 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 { CsvExportController } from "src/time-and-attendance/exports/csv-exports.controller";
|
||||||
|
|
||||||
import { ShiftController } from "src/time-and-attendance/shifts/shift.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 { 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 { 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 { 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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -46,8 +47,8 @@ import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-pr
|
||||||
TimesheetsModule,
|
TimesheetsModule,
|
||||||
ExpensesModule,
|
ExpensesModule,
|
||||||
PayperiodsModule,
|
PayperiodsModule,
|
||||||
CsvExportModule,
|
CsvExportModule,
|
||||||
SchedulePresetsModule,
|
SchedulePresetsModule,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
TimesheetController,
|
TimesheetController,
|
||||||
|
|
@ -56,7 +57,7 @@ import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-pr
|
||||||
ExpenseController,
|
ExpenseController,
|
||||||
PayPeriodsController,
|
PayPeriodsController,
|
||||||
CsvExportController,
|
CsvExportController,
|
||||||
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
GetTimesheetsOverviewService,
|
GetTimesheetsOverviewService,
|
||||||
|
|
@ -78,6 +79,7 @@ import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-pr
|
||||||
PayPeriodsQueryService,
|
PayPeriodsQueryService,
|
||||||
PayPeriodsCommandService,
|
PayPeriodsCommandService,
|
||||||
CsvExportService,
|
CsvExportService,
|
||||||
|
CsvGeneratorService,
|
||||||
],
|
],
|
||||||
exports: [TimesheetApprovalService ],
|
exports: [TimesheetApprovalService],
|
||||||
}) export class TimeAndAttendanceModule { };
|
}) export class TimeAndAttendanceModule { };
|
||||||
Loading…
Reference in New Issue
Block a user