Files
geo/backend/dist/services/pdf.service.js
2026-02-04 00:11:19 +05:00

231 lines
9.2 KiB
JavaScript
Executable File

"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