231 lines
9.2 KiB
JavaScript
Executable File
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
|