first commit
This commit is contained in:
196
backend/dist/routes/reports.js
vendored
Normal file
196
backend/dist/routes/reports.js
vendored
Normal file
@@ -0,0 +1,196 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user