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(); }); }