197 lines
6.9 KiB
JavaScript
197 lines
6.9 KiB
JavaScript
import { z } from 'zod';
|
|
import { prisma } from '../index.js';
|
|
import PdfPrinter from 'pdfmake';
|
|
import ExcelJS from 'exceljs';
|
|
const reportSchema = z.object({
|
|
startDate: z.string(),
|
|
endDate: z.string(),
|
|
format: z.enum(['pdf', 'excel', 'csv']),
|
|
});
|
|
function formatDate(date) {
|
|
const d = new Date(date);
|
|
const day = String(d.getDate()).padStart(2, '0');
|
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
const year = d.getFullYear();
|
|
return `${day}.${month}.${year}`;
|
|
}
|
|
function formatTime(minutes) {
|
|
const hours = Math.floor(minutes / 60);
|
|
const mins = minutes % 60;
|
|
return `${hours}:${String(mins).padStart(2, '0')}`;
|
|
}
|
|
export async function reportsRoutes(fastify) {
|
|
fastify.addHook('onRequest', fastify.authenticate);
|
|
fastify.post('/generate', async (request, reply) => {
|
|
const user = request.user;
|
|
const result = reportSchema.safeParse(request.body);
|
|
if (!result.success) {
|
|
return reply.status(400).send({ error: 'Invalid input', details: result.error.errors });
|
|
}
|
|
const { startDate, endDate, format } = result.data;
|
|
const logs = await prisma.timeLog.findMany({
|
|
where: {
|
|
userId: user.id,
|
|
date: {
|
|
gte: new Date(startDate),
|
|
lte: new Date(endDate + 'T23:59:59'),
|
|
},
|
|
},
|
|
orderBy: { date: 'asc' },
|
|
});
|
|
// Calculate totals
|
|
const uniqueDates = new Set(logs.map(log => formatDate(log.date)));
|
|
const totalMinutes = logs.reduce((sum, log) => sum + log.minutes, 0);
|
|
const reportData = logs.map(log => ({
|
|
date: formatDate(log.date),
|
|
description: log.description,
|
|
time: formatTime(log.minutes),
|
|
}));
|
|
if (format === 'csv') {
|
|
const csvContent = generateCSV(reportData, uniqueDates.size, totalMinutes);
|
|
reply.header('Content-Type', 'text/csv; charset=utf-8');
|
|
reply.header('Content-Disposition', `attachment; filename="report_${startDate}_${endDate}.csv"`);
|
|
return reply.send(csvContent);
|
|
}
|
|
if (format === 'excel') {
|
|
const buffer = await generateExcel(reportData, uniqueDates.size, totalMinutes);
|
|
reply.header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
reply.header('Content-Disposition', `attachment; filename="report_${startDate}_${endDate}.xlsx"`);
|
|
return reply.send(buffer);
|
|
}
|
|
if (format === 'pdf') {
|
|
const buffer = await generatePDF(reportData, uniqueDates.size, totalMinutes, startDate, endDate);
|
|
reply.header('Content-Type', 'application/pdf');
|
|
reply.header('Content-Disposition', `attachment; filename="report_${startDate}_${endDate}.pdf"`);
|
|
return reply.send(buffer);
|
|
}
|
|
});
|
|
}
|
|
function generateCSV(data, uniqueDays, totalMinutes) {
|
|
const BOM = '\uFEFF';
|
|
const lines = [];
|
|
lines.push('Дата,Описание,Часы');
|
|
for (const row of data) {
|
|
const escapedDescription = `"${row.description.replace(/"/g, '""')}"`;
|
|
lines.push(`${row.date},${escapedDescription},${row.time}`);
|
|
}
|
|
lines.push('');
|
|
lines.push(`Total,${uniqueDays} дней,${formatTime(totalMinutes)}`);
|
|
return BOM + lines.join('\n');
|
|
}
|
|
async function generateExcel(data, uniqueDays, totalMinutes) {
|
|
const workbook = new ExcelJS.Workbook();
|
|
const sheet = workbook.addWorksheet('Отчёт');
|
|
// Headers
|
|
sheet.columns = [
|
|
{ header: 'Дата', key: 'date', width: 15 },
|
|
{ header: 'Описание', key: 'description', width: 50 },
|
|
{ header: 'Часы', key: 'time', width: 10 },
|
|
];
|
|
// Style headers
|
|
sheet.getRow(1).font = { bold: true };
|
|
sheet.getRow(1).fill = {
|
|
type: 'pattern',
|
|
pattern: 'solid',
|
|
fgColor: { argb: 'FFE0E0E0' },
|
|
};
|
|
// Data
|
|
for (const row of data) {
|
|
sheet.addRow(row);
|
|
}
|
|
// Empty row
|
|
sheet.addRow({});
|
|
// Total row
|
|
const totalRow = sheet.addRow({
|
|
date: 'Total',
|
|
description: `${uniqueDays} дней`,
|
|
time: formatTime(totalMinutes),
|
|
});
|
|
// Style total row
|
|
totalRow.font = { bold: true };
|
|
totalRow.fill = {
|
|
type: 'pattern',
|
|
pattern: 'solid',
|
|
fgColor: { argb: 'FF90EE90' },
|
|
};
|
|
const arrayBuffer = await workbook.xlsx.writeBuffer();
|
|
return Buffer.from(arrayBuffer);
|
|
}
|
|
async function generatePDF(data, uniqueDays, totalMinutes, startDate, endDate) {
|
|
const fonts = {
|
|
Roboto: {
|
|
normal: 'node_modules/pdfmake/build/vfs_fonts.js',
|
|
bold: 'node_modules/pdfmake/build/vfs_fonts.js',
|
|
},
|
|
};
|
|
const printer = new PdfPrinter(fonts);
|
|
const tableBody = [
|
|
[
|
|
{ text: 'Дата', style: 'tableHeader' },
|
|
{ text: 'Описание', style: 'tableHeader' },
|
|
{ text: 'Часы', style: 'tableHeader' },
|
|
],
|
|
];
|
|
for (const row of data) {
|
|
tableBody.push([row.date, row.description, row.time]);
|
|
}
|
|
// Total row
|
|
tableBody.push([
|
|
{ text: 'Total', style: 'total' },
|
|
{ text: `${uniqueDays} дней`, style: 'total' },
|
|
{ text: formatTime(totalMinutes), style: 'total' },
|
|
]);
|
|
const docDefinition = {
|
|
content: [
|
|
{ text: 'Отчёт по времени', style: 'header' },
|
|
{ text: `Период: ${startDate} - ${endDate}`, style: 'subheader' },
|
|
{ text: ' ' },
|
|
{
|
|
table: {
|
|
headerRows: 1,
|
|
widths: ['auto', '*', 'auto'],
|
|
body: tableBody,
|
|
},
|
|
layout: {
|
|
fillColor: (rowIndex) => {
|
|
if (rowIndex === 0)
|
|
return '#EEEEEE';
|
|
if (rowIndex === tableBody.length - 1)
|
|
return '#90EE90';
|
|
return null;
|
|
},
|
|
},
|
|
},
|
|
],
|
|
styles: {
|
|
header: {
|
|
fontSize: 18,
|
|
bold: true,
|
|
margin: [0, 0, 0, 10],
|
|
},
|
|
subheader: {
|
|
fontSize: 12,
|
|
margin: [0, 0, 0, 10],
|
|
},
|
|
tableHeader: {
|
|
bold: true,
|
|
fontSize: 11,
|
|
},
|
|
total: {
|
|
bold: true,
|
|
fontSize: 11,
|
|
},
|
|
},
|
|
defaultStyle: {
|
|
fontSize: 10,
|
|
},
|
|
};
|
|
return new Promise((resolve, reject) => {
|
|
const pdfDoc = printer.createPdfKitDocument(docDefinition);
|
|
const chunks = [];
|
|
pdfDoc.on('data', (chunk) => chunks.push(chunk));
|
|
pdfDoc.on('end', () => resolve(Buffer.concat(chunks)));
|
|
pdfDoc.on('error', reject);
|
|
pdfDoc.end();
|
|
});
|
|
}
|