328 lines
12 KiB
TypeScript
328 lines
12 KiB
TypeScript
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 { computeHours } from "src/common/utils/date-utils";
|
|
|
|
|
|
|
|
@Injectable()
|
|
export class CsvExportService {
|
|
constructor(private readonly prisma: PrismaService) { }
|
|
|
|
async collectTransaction(
|
|
year: number,
|
|
period_no: number,
|
|
filters: Filters,
|
|
// approved: boolean = true
|
|
): Promise<CsvRow[]> {
|
|
//fetch period
|
|
const period = await this.prisma.payPeriods.findFirst({
|
|
where: { pay_year: year, pay_period_no: period_no },
|
|
select: { period_start: true, period_end: true },
|
|
});
|
|
if (!period) throw new NotFoundException(`Pay period ${year}-${period_no} not found`);
|
|
|
|
const start = period.period_start;
|
|
const end = period.period_end;
|
|
|
|
//fetch company codes
|
|
const company_codes = this.resolveCompanyCodes(filters.companies);
|
|
if (company_codes.length === 0) throw new BadRequestException('No company selected');
|
|
|
|
//Flag types
|
|
const { shifts: want_shifts, expenses: want_expense, holiday: want_holiday, vacation: want_vacation } = filters.types;
|
|
if (!want_shifts && !want_expense && !want_holiday && !want_vacation) {
|
|
throw new BadRequestException(' No export type selected ');
|
|
}
|
|
|
|
const approved_filter = filters.approved ? { is_approved: true } : {};
|
|
|
|
const holiday_code = await this.resolveHolidayTypeCode('HOLIDAY');
|
|
const vacation_code = await this.resolveVacationTypeCode('VACATION');
|
|
const leave_code_filter = [holiday_code, vacation_code];
|
|
|
|
//Prisma queries
|
|
const promises: Array<Promise<any[]>> = [];
|
|
|
|
if (want_shifts) {
|
|
promises.push(this.prisma.shifts.findMany({
|
|
where: {
|
|
date: { gte: start, lte: end },
|
|
...approved_filter,
|
|
bank_code: { bank_code: { notIn: leave_code_filter } },
|
|
timesheet: { employee: { company_code: { in: company_codes } } },
|
|
},
|
|
select: {
|
|
date: true,
|
|
start_time: true,
|
|
end_time: true,
|
|
bank_code: { select: { bank_code: true } },
|
|
timesheet: {
|
|
select: {
|
|
employee: {
|
|
select: {
|
|
company_code: true,
|
|
external_payroll_id: true,
|
|
user: { select: { first_name: true, last_name: true } },
|
|
}
|
|
},
|
|
}
|
|
},
|
|
},
|
|
}));
|
|
} else {
|
|
promises.push(Promise.resolve([]));
|
|
}
|
|
|
|
if (want_holiday) {
|
|
promises.push(this.prisma.shifts.findMany({
|
|
where: {
|
|
date: { gte: start, lte: end },
|
|
...approved_filter,
|
|
bank_code: { bank_code: holiday_code },
|
|
timesheet: { employee: { company_code: { in: company_codes } } },
|
|
},
|
|
select: {
|
|
date: true,
|
|
start_time: true,
|
|
end_time: true,
|
|
bank_code: { select: { bank_code: true } },
|
|
timesheet: {
|
|
select: {
|
|
employee: {
|
|
select: {
|
|
company_code: true,
|
|
external_payroll_id: true,
|
|
user: { select: { first_name: true, last_name: true } },
|
|
}
|
|
},
|
|
}
|
|
},
|
|
},
|
|
}));
|
|
} else {
|
|
promises.push(Promise.resolve([]));
|
|
}
|
|
|
|
if (want_vacation) {
|
|
promises.push(this.prisma.shifts.findMany({
|
|
where: {
|
|
date: { gte: start, lte: end },
|
|
...approved_filter,
|
|
bank_code: { bank_code: vacation_code },
|
|
timesheet: { employee: { company_code: { in: company_codes } } },
|
|
},
|
|
select: {
|
|
date: true,
|
|
start_time: true,
|
|
end_time: true,
|
|
bank_code: { select: { bank_code: true } },
|
|
timesheet: {
|
|
select: {
|
|
employee: {
|
|
select: {
|
|
company_code: true,
|
|
external_payroll_id: true,
|
|
user: { select: { first_name: true, last_name: true } },
|
|
}
|
|
},
|
|
}
|
|
},
|
|
},
|
|
}));
|
|
} else {
|
|
promises.push(Promise.resolve([]));
|
|
}
|
|
|
|
if (want_expense) {
|
|
promises.push(this.prisma.expenses.findMany({
|
|
where: {
|
|
date: { gte: start, lte: end },
|
|
...approved_filter,
|
|
timesheet: { employee: { company_code: { in: company_codes } } },
|
|
},
|
|
select: {
|
|
date: true,
|
|
amount: true,
|
|
bank_code: { select: { bank_code: true } },
|
|
timesheet: {
|
|
select: {
|
|
employee: {
|
|
select: {
|
|
company_code: true,
|
|
external_payroll_id: true,
|
|
user: { select: { first_name: true, last_name: true } },
|
|
}
|
|
},
|
|
}
|
|
},
|
|
},
|
|
}));
|
|
} else {
|
|
promises.push(Promise.resolve([]));
|
|
}
|
|
|
|
//array of arrays
|
|
const [base_shifts, holiday_shifts, vacation_shifts, expenses] = await Promise.all(promises);
|
|
//mapping
|
|
const rows: CsvRow[] = [];
|
|
|
|
const map_shifts = (shift: any, is_holiday: boolean) => {
|
|
const employee = shift.timesheet.employee;
|
|
const week = this.computeWeekNumber(start, shift.date);
|
|
return {
|
|
company_code: employee.company_code,
|
|
external_payroll_id: employee.external_payroll_id,
|
|
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;
|
|
};
|
|
//final mapping of all shifts based filters
|
|
for (const shift of base_shifts) rows.push(map_shifts(shift, false));
|
|
for (const shift of holiday_shifts) rows.push(map_shifts(shift, true));
|
|
for (const shift of vacation_shifts) rows.push(map_shifts(shift, false));
|
|
|
|
for (const expense of expenses) {
|
|
const employee = expense.timesheet.employee;
|
|
const week = this.computeWeekNumber(start, expense.date);
|
|
rows.push({
|
|
company_code: employee.company_code,
|
|
external_payroll_id: employee.external_payroll_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),
|
|
holiday_date: '',
|
|
})
|
|
}
|
|
|
|
//Final mapping and sorts
|
|
rows.sort((a, b) => {
|
|
if (a.external_payroll_id !== b.external_payroll_id) {
|
|
return a.external_payroll_id - b.external_payroll_id;
|
|
}
|
|
const bank_code = String(a.bank_code).localeCompare(String(b.bank_code));
|
|
if (bank_code !== 0) return bank_code;
|
|
if (a.bank_code !== b.bank_code) return a.bank_code.localeCompare(b.bank_code);
|
|
return 0;
|
|
});
|
|
|
|
const consolidated_rows = this.consolidateRowHoursAndAmountByType(rows)
|
|
|
|
return consolidated_rows;
|
|
}
|
|
|
|
consolidateRowHoursAndAmountByType = (rows: CsvRow[]): CsvRow[] => {
|
|
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> => {
|
|
const holiday_code = await this.prisma.bankCodes.findFirst({
|
|
where: { type: holiday },
|
|
select: {
|
|
bank_code: true,
|
|
},
|
|
});
|
|
if (!holiday_code) throw new BadRequestException('Missing Holiday bank code');
|
|
|
|
return holiday_code.bank_code;
|
|
}
|
|
|
|
resolveVacationTypeCode = async (vacation: string): Promise<string> => {
|
|
const vacation_code = await this.prisma.bankCodes.findFirst({
|
|
where: { type: vacation },
|
|
select: {
|
|
bank_code: 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];
|
|
}
|
|
|
|
}
|