"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PdfService = void 0; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const pdfkit_1 = __importDefault(require("pdfkit")); const FONT_FILES = { regular: 'pt-sans-cyrillic-400-normal.woff', bold: 'pt-sans-cyrillic-700-normal.woff', }; class PdfService { /** Resolve path to @fontsource/pt-sans/files (tries require.resolve, then cwd). */ getFontPaths() { let dir; try { dir = path_1.default.dirname(require.resolve('@fontsource/pt-sans/package.json')); } catch { dir = path_1.default.join(process.cwd(), 'node_modules', '@fontsource', 'pt-sans'); } return { regular: path_1.default.join(dir, 'files', FONT_FILES.regular), bold: path_1.default.join(dir, 'files', FONT_FILES.bold), }; } /** Read font buffer from first path that exists. */ readFontBuffer(paths) { for (const p of paths) { if (fs_1.default.existsSync(p)) return fs_1.default.readFileSync(p); } throw new Error(`Cyrillic font not found. Tried: ${paths.join(', ')}. Run "npm install" in backend and ensure @fontsource/pt-sans is installed.`); } async generateEstimatePdf(estimate) { return new Promise((resolve, reject) => { try { const doc = new pdfkit_1.default({ size: 'A4', margin: 40, bufferPages: true, }); const chunks = []; doc.on('data', (chunk) => chunks.push(chunk)); doc.on('end', () => resolve(Buffer.concat(chunks))); doc.on('error', reject); // Register Cyrillic fonts (PT Sans) — pass Buffer so path/encoding issues are avoided const { regular: pathRegular, bold: pathBold } = this.getFontPaths(); const cwd = process.cwd(); const cwdFiles = path_1.default.join(cwd, 'node_modules', '@fontsource', 'pt-sans', 'files'); const parentFiles = path_1.default.join(cwd, '..', 'node_modules', '@fontsource', 'pt-sans', 'files'); const fontRegularBuffer = this.readFontBuffer([ pathRegular, path_1.default.join(cwdFiles, FONT_FILES.regular), path_1.default.join(parentFiles, FONT_FILES.regular), ]); const fontBoldBuffer = this.readFontBuffer([ pathBold, path_1.default.join(cwdFiles, FONT_FILES.bold), path_1.default.join(parentFiles, FONT_FILES.bold), ]); doc.registerFont('PTSans', fontRegularBuffer); doc.registerFont('PTSansBold', fontBoldBuffer); // Header this.addHeader(doc, estimate); // Items table this.addItemsTable(doc, estimate); // Totals section this.addTotals(doc, estimate); // Footer this.addFooter(doc, estimate); doc.end(); } catch (error) { reject(error); } }); } addHeader(doc, estimate) { doc.fontSize(14).font('PTSansBold'); doc.text(`Исполнительная смета №${estimate.number}`, { align: 'center' }); doc.moveDown(0.5); doc.fontSize(12).font('PTSans'); doc.text(`${estimate.direction.name} на объекте:`, { align: 'center' }); doc.moveDown(0.5); doc.fontSize(11); doc.text(`«${estimate.objectName}»`, { align: 'center' }); doc.moveDown(1); // Customer and Executor const leftX = 40; const rightX = 300; doc.fontSize(10); doc.text('Наименование организации Заказчика:', leftX, doc.y); doc.text(`«${estimate.customer}»`, rightX, doc.y - 10); doc.moveDown(0.5); doc.text('Наименование организации Исполнителя:', leftX, doc.y); doc.text(`«${estimate.executor}»`, rightX, doc.y - 10); doc.moveDown(1); } addItemsTable(doc, estimate) { const tableTop = doc.y; const tableLeft = 40; const colWidths = [30, 200, 100, 50, 50, 85]; // №, Наименование, Обоснование, Цена, Объем, Стоимость doc.fontSize(8).font('PTSansBold'); // Table header let x = tableLeft; const headers = ['№', 'Наименование работ', 'Обоснование', 'Цена', 'Объем', 'Стоимость']; headers.forEach((header, i) => { doc.text(header, x + 2, tableTop, { width: colWidths[i] - 4, align: 'center' }); x += colWidths[i]; }); doc.moveTo(tableLeft, tableTop - 5) .lineTo(tableLeft + colWidths.reduce((a, b) => a + b, 0), tableTop - 5) .stroke(); let y = tableTop + 15; doc.moveTo(tableLeft, y) .lineTo(tableLeft + colWidths.reduce((a, b) => a + b, 0), y) .stroke(); y += 5; doc.font('PTSans').fontSize(7); // Group items by section const sections = { field: [], office: [], laboratory: [], other: [], }; estimate.items.forEach(item => { const section = sections[item.sectionType] || sections.other; section.push(item); }); // Field works if (sections.field.length > 0) { doc.font('PTSansBold').fontSize(8); doc.text('Полевые работы', tableLeft + 2, y); y += 12; doc.font('PTSans').fontSize(7); sections.field.forEach(item => { y = this.addItemRow(doc, item, y, tableLeft, colWidths); }); } // Office works if (sections.office.length > 0) { doc.font('PTSansBold').fontSize(8); doc.text('Камеральные работы', tableLeft + 2, y); y += 12; doc.font('PTSans').fontSize(7); sections.office.forEach(item => { y = this.addItemRow(doc, item, y, tableLeft, colWidths); }); } // Laboratory works if (sections.laboratory.length > 0) { doc.font('PTSansBold').fontSize(8); doc.text('Лабораторные работы', tableLeft + 2, y); y += 12; doc.font('PTSans').fontSize(7); sections.laboratory.forEach(item => { y = this.addItemRow(doc, item, y, tableLeft, colWidths); }); } // Other items if (sections.other.length > 0) { sections.other.forEach(item => { y = this.addItemRow(doc, item, y, tableLeft, colWidths); }); } // Subtotal line doc.moveTo(tableLeft, y) .lineTo(tableLeft + colWidths.reduce((a, b) => a + b, 0), y) .stroke(); doc.y = y + 10; } addItemRow(doc, item, y, tableLeft, colWidths) { const rowHeight = 20; let x = tableLeft; // Check for page break if (y > 750) { doc.addPage(); y = 40; } // Row data const cells = [ String(item.orderNumber), item.workName.substring(0, 60), (item.justification || '').substring(0, 30), this.formatNumber(Number(item.basePrice)), this.formatNumber(Number(item.quantity)), this.formatNumber(Number(item.totalPrice)), ]; cells.forEach((cell, i) => { doc.text(cell, x + 2, y, { width: colWidths[i] - 4, align: i > 2 ? 'right' : 'left' }); x += colWidths[i]; }); return y + rowHeight; } addTotals(doc, estimate) { const leftX = 300; const rightX = 480; doc.moveDown(1); doc.font('PTSans').fontSize(9); estimate.totals.forEach(total => { doc.text(total.label, leftX, doc.y, { continued: true }); if (total.description) { doc.text(` (${total.description})`, { continued: true }); } doc.text(''); doc.text(this.formatCurrency(Number(total.resultValue)), rightX, doc.y - 10, { align: 'right' }); doc.moveDown(0.3); }); } addFooter(doc, estimate) { doc.moveDown(2); doc.font('PTSans').fontSize(9); doc.text('Выполнил: _____________________', 40); } formatNumber(num) { return num.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } formatCurrency(num) { return num.toLocaleString('ru-RU', { style: 'currency', currency: 'RUB', minimumFractionDigits: 2, }); } } exports.PdfService = PdfService; //# sourceMappingURL=pdf.service.js.map