This commit is contained in:
2026-10-29 16:51:26 +03:00
parent 70a20665ae
commit 4b2f0903b1
51 changed files with 6540 additions and 1212 deletions
+527 -92
View File
@@ -7,6 +7,7 @@ const axios = require('axios');
const { logger } = require('../../backend/src/config/logger');
const config = require('../src/config.json')
const { print } = require('pdf-to-printer');
const { generatePayslipPDF } = require('./pdfGenerator');
protocol.registerSchemesAsPrivileged([
{
@@ -51,6 +52,532 @@ try {
global.allowClose = false;
// ============================================
// 📄 IPC ОБРАБОТЧИКИ
// ============================================
// ✅ 1. Печать PDF (принимает URL или base64) - СКРЫТАЯ ПЕЧАТЬ
ipcMain.handle('print-pdf', async (event, data = {}) => {
let tempFile = null;
try {
if (!data.pdfUrl && !data.pdfBase64) {
throw new Error('URL или base64 PDF не передан');
}
let pdfBuffer;
// ✅ Если передан base64 — используем его
if (data.pdfBase64) {
logger.info('[PRINT] Печать PDF из base64');
pdfBuffer = Buffer.from(data.pdfBase64, 'base64');
logger.info('[PRINT] Размер PDF из base64:', pdfBuffer.length, 'байт');
}
// ✅ Если передан URL — скачиваем
else if (data.pdfUrl) {
const cleanUrl = data.pdfUrl.split('#')[0];
logger.info('[PRINT] Загрузка PDF из URL:', cleanUrl);
// Если это blob URL — получаем через fetch
if (cleanUrl.startsWith('blob:')) {
const response = await fetch(cleanUrl);
const blob = await response.blob();
pdfBuffer = Buffer.from(await blob.arrayBuffer());
} else {
const response = await axios.get(cleanUrl, {
responseType: 'arraybuffer',
timeout: 30000
});
pdfBuffer = Buffer.from(response.data);
}
logger.info('[PRINT] Размер PDF из URL:', pdfBuffer.length, 'байт');
}
if (!pdfBuffer || pdfBuffer.length < 100) {
throw new Error('PDF слишком маленький или пустой');
}
// ✅ ВРЕМЕННО СОХРАНЯЕМ ДЛЯ ПЕЧАТИ (pdf-to-printer требует файл)
const tempDir = app.getPath('temp');
tempFile = path.join(tempDir, `print_${Date.now()}.pdf`);
fs.writeFileSync(tempFile, pdfBuffer);
logger.info('[PRINT] Временный файл создан:', tempFile);
const printerName = data.deviceName || 'GP-80250 Series';
logger.info('[PRINT] СКРЫТАЯ печать через pdf-to-printer:', printerName);
// ✅ КЛЮЧЕВОЕ: silent: true — печать без диалога
await print(tempFile, {
printer: printerName,
silent: true,
printDialog: false
});
logger.info('[PRINT] ✅ Печать успешна');
// ✅ УДАЛЯЕМ ВРЕМЕННЫЙ ФАЙЛ
// try {
// if (tempFile && fs.existsSync(tempFile)) {
// fs.unlinkSync(tempFile);
// logger.info('[PRINT] Временный файл удален');
// }
// } catch (e) {
// logger.warn('[PRINT] Не удалось удалить временный файл:', e.message);
// }
return { success: true };
} catch (error) {
logger.error('[PRINT] ❌ Ошибка:', error.message);
try {
if (tempFile && fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
}
} catch (e) {}
return { success: false, error: error.message };
}
});
// ✅ 2. ОСНОВНАЯ ЛОГИКА ПЕЧАТИ HTML (оставлена для совместимости)
async function printHtmlDirectLogic(data) {
let tempHtml = null;
let tempPdf = null;
let printWindow = null;
let savedPdfPath = null;
try {
if (!data.html) {
throw new Error('HTML-код не передан');
}
logger.info('[PRINT-HTML] ========== СОХРАНЕНИЕ PDF ==========');
logger.info('[PRINT-HTML] Длина HTML:', data.html.length);
const desktopPath = app.getPath('desktop');
const documentsPath = app.getPath('documents');
let saveDir = documentsPath;
const possiblePaths = [
path.join(documentsPath, 'payslips'),
path.join(documentsPath, 'Расчетные листки'),
path.join(desktopPath, 'payslips'),
path.join(desktopPath, 'Расчетные листки'),
documentsPath
];
for (const dir of possiblePaths) {
if (fs.existsSync(dir)) {
saveDir = dir;
logger.info('[PRINT-HTML] Найдена папка для сохранения:', saveDir);
break;
}
}
const now = new Date();
const dateStr = now.getFullYear() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0') + '_' +
String(now.getHours()).padStart(2, '0') +
String(now.getMinutes()).padStart(2, '0') +
String(now.getSeconds()).padStart(2, '0');
const fileName = `test_${dateStr}.pdf`;
savedPdfPath = path.join(saveDir, fileName);
logger.info('[PRINT-HTML] PDF будет сохранен в:', savedPdfPath);
const tempDir = app.getPath('temp');
tempHtml = path.join(tempDir, `print_${Date.now()}.html`);
tempPdf = path.join(tempDir, `print_${Date.now()}.pdf`);
let htmlContent = data.html;
if (!htmlContent || htmlContent.trim().length < 10) {
logger.warn('[PRINT-HTML] HTML пустой, используем тестовый');
htmlContent = `
<div style="text-align:center;padding:20px;font-size:16pt;border:2px solid #000;">
<b>ТЕСТ ПЕЧАТИ</b>
<hr>
<div>Дата: ${new Date().toLocaleString()}</div>
<div>Табельный номер: TEST</div>
</div>
`;
}
const fullHtml = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Расчетный листок</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Courier New', monospace;
padding: 20px;
background: white;
color: black;
font-size: 12pt;
}
.header {
text-align: center;
font-weight: bold;
font-size: 16pt;
border-bottom: 2px solid #000;
padding-bottom: 10px;
margin-bottom: 10px;
}
.row {
display: flex;
justify-content: space-between;
padding: 4px 0;
border-bottom: 1px solid #eee;
}
.total {
font-weight: bold;
border-top: 2px solid #000;
padding-top: 10px;
margin-top: 10px;
font-size: 14pt;
}
.section-title {
font-weight: bold;
border-bottom: 1px dotted #000;
margin: 10px 0;
padding: 5px 0;
font-size: 14pt;
}
.item {
display: flex;
justify-content: space-between;
padding: 3px 0 3px 15px;
border-bottom: 1px solid #f0f0f0;
}
.footer {
text-align: center;
font-size: 10pt;
color: #666;
border-top: 1px dashed #000;
margin-top: 20px;
padding-top: 10px;
}
.debug-info {
color: #999;
font-size: 8pt;
margin-top: 10px;
padding: 5px;
border: 1px dashed #ccc;
}
</style>
</head>
<body>
${htmlContent}
<div class="debug-info">
Сгенерировано: ${new Date().toLocaleString()}
</div>
</body>
</html>`;
fs.writeFileSync(tempHtml, fullHtml, 'utf8');
logger.info('[PRINT-HTML] HTML файл создан:', tempHtml);
logger.info('[PRINT-HTML] Размер HTML:', fs.statSync(tempHtml).size, 'байт');
printWindow = new BrowserWindow({
width: 600,
height: 800,
show: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
webSecurity: false,
allowRunningInsecureContent: true,
}
});
printWindow.webContents.openDevTools();
logger.info('[PRINT-HTML] Загрузка HTML в окно...');
await printWindow.loadFile(tempHtml);
await new Promise((resolve) => {
printWindow.webContents.once('did-finish-load', () => {
logger.info('[PRINT-HTML] Событие did-finish-load');
resolve();
});
setTimeout(() => {
logger.info('[PRINT-HTML] Таймаут загрузки');
resolve();
}, 10000);
});
const contentCheck = await printWindow.webContents.executeJavaScript(`
(function() {
const body = document.body;
if (!body) return { hasBody: false, contentLength: 0 };
const text = body.textContent || '';
const html = body.innerHTML || '';
return {
hasBody: true,
contentLength: text.length,
htmlLength: html.length,
preview: text.substring(0, 200)
};
})()
`);
logger.info('[PRINT-HTML] Проверка контента:', contentCheck);
if (contentCheck.contentLength < 10) {
logger.warn('[PRINT-HTML] ⚠️ В окне нет контента!');
await printWindow.webContents.executeJavaScript(`
if (!document.body || document.body.textContent.length < 10) {
document.body.innerHTML = '<div style="padding:20px;font-size:20pt;color:red;">ОШИБКА: Контент не загрузился!</div>';
}
`);
}
await printWindow.webContents.executeJavaScript(`
document.body.style.display = 'block';
document.body.style.visibility = 'visible';
document.body.style.opacity = '1';
window.scrollTo(0, 0);
document.querySelectorAll('*').forEach(el => {
el.style.display = 'block';
el.style.visibility = 'visible';
el.style.opacity = '1';
});
console.log('Контент в окне:', document.body.textContent.length, 'символов');
console.log('HTML:', document.body.innerHTML.substring(0, 500));
`);
await new Promise(resolve => setTimeout(resolve, 2000));
const finalCheck = await printWindow.webContents.executeJavaScript(`
document.body ? document.body.textContent.length : 0
`);
logger.info('[PRINT-HTML] Финальная проверка контента:', finalCheck, 'символов');
logger.info('[PRINT-HTML] Конвертация в PDF...');
const pdfData = await printWindow.webContents.printToPDF({
pageSize: {
width: 226800,
height: 500000
},
margins: {
top: 0,
bottom: 0,
left: 0,
right: 0
},
printBackground: true,
landscape: false
});
fs.writeFileSync(tempPdf, pdfData);
logger.info('[PRINT-HTML] PDF создан, размер:', pdfData.length, 'байт');
const pdfBuffer = fs.readFileSync(tempPdf);
const isPdf = pdfBuffer.slice(0, 4).toString() === '%PDF';
logger.info('[PRINT-HTML] Это PDF?', isPdf);
if (!isPdf) {
logger.error('[PRINT-HTML] ⚠️ Файл НЕ является PDF!');
throw new Error('Созданный файл не является PDF');
}
if (printWindow) {
printWindow.close();
printWindow = null;
}
if (pdfData.length < 500) {
logger.warn('[PRINT-HTML] ⚠️ PDF слишком маленький:', pdfData.length, 'байт');
logger.info('[PRINT-HTML] Пробуем еще раз с большим ожиданием...');
const retryWindow = new BrowserWindow({
width: 600,
height: 800,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
webSecurity: false,
allowRunningInsecureContent: true,
}
});
await retryWindow.loadFile(tempHtml);
await new Promise(resolve => setTimeout(resolve, 3000));
const retryPdf = await retryWindow.webContents.printToPDF({
pageSize: { width: 226800, height: 500000 },
margins: { top: 0, bottom: 0, left: 0, right: 0 },
printBackground: true,
landscape: false
});
fs.writeFileSync(tempPdf, retryPdf);
logger.info('[PRINT-HTML] Повторный PDF создан, размер:', retryPdf.length, 'байт');
retryWindow.close();
}
fs.copyFileSync(tempPdf, savedPdfPath);
logger.info('[PRINT-HTML] ✅ PDF сохранен в:', savedPdfPath);
logger.info('[PRINT-HTML] Размер сохраненного PDF:', fs.statSync(savedPdfPath).size, 'байт');
const printerName = data.printerName || 'GP-80250 Series';
logger.info('[PRINT-HTML] Отправка PDF на печать, принтер:', printerName);
let printResult = null;
let printSuccess = false;
try {
logger.info('[PRINT-HTML] Попытка печати...');
printResult = await print(tempPdf, {
printer: printerName,
scale: 'fit',
paperSize: 'A4'
});
printSuccess = true;
logger.info('[PRINT-HTML] Печать успешна:', printResult);
} catch (err) {
logger.warn('[PRINT-HTML] Печать не удалась:', err.message);
try {
printResult = await print(tempPdf);
printSuccess = true;
logger.info('[PRINT-HTML] Печать успешна (без параметров)');
} catch (err2) {
logger.error('[PRINT-HTML] Все попытки печати не удались:', err2.message);
}
}
try {
if (tempHtml && fs.existsSync(tempHtml)) fs.unlinkSync(tempHtml);
if (tempPdf && fs.existsSync(tempPdf)) fs.unlinkSync(tempPdf);
} catch (e) {}
logger.info('[PRINT-HTML] ========== ЗАВЕРШЕНО ==========');
logger.info('[PRINT-HTML] ✅ PDF сохранен в:', savedPdfPath);
return {
success: true,
pdfPath: savedPdfPath,
printSuccess: printSuccess,
message: `PDF сохранен в: ${savedPdfPath}`
};
} catch (error) {
logger.error('[PRINT-HTML] ❌ Ошибка:', error.message);
logger.error('[PRINT-HTML] Стек:', error.stack);
try {
if (printWindow) printWindow.close();
if (tempHtml && fs.existsSync(tempHtml)) fs.unlinkSync(tempHtml);
if (tempPdf && fs.existsSync(tempPdf)) fs.unlinkSync(tempPdf);
} catch (e) {}
return { success: false, error: error.message };
}
}
// ✅ 3. Обработчик print-html-direct (для совместимости)
ipcMain.handle('print-html-direct', async (event, data) => {
return await printHtmlDirectLogic(data);
});
// ✅ 4. ОСНОВНОЙ ОБРАБОТЧИК - ГЕНЕРАЦИЯ PDF В ПАМЯТИ (БЕЗ СОХРАНЕНИЯ НА ДИСК)
ipcMain.handle('print-payslip-form', async (event, data = {}) => {
try {
logger.info('[PRINT-PAYSLIP] ========== ГЕНЕРАЦИЯ PDF В ПАМЯТИ ==========');
logger.info('[PRINT-PAYSLIP] tnom:', data.tnom);
logger.info('[PRINT-PAYSLIP] period:', data.period);
// --- 1. Получаем данные из бэкенда ---
let payslipData = null;
try {
const backendUrl = appConfig.backendUrl || 'http://localhost:3001';
const url = `${backendUrl}/api/payslip/data/${data.tnom}?period=${data.period}`;
logger.info('[PRINT-PAYSLIP] Запрос к бэкенду:', url);
const response = await axios.get(url, { timeout: 10000 });
payslipData = response.data;
logger.info('[PRINT-PAYSLIP] Данные получены');
} catch (error) {
logger.warn('[PRINT-PAYSLIP] Не удалось получить данные из бэкенда:', error.message);
// Заглушка на случай, если бэкенд не отвечает
payslipData = {
fullName: 'Иванов Иван Иванович',
tabNumber: data.tnom || '00001',
department: 'Отдел разработки',
position: 'Инженер-программист',
daysWorked: '22',
month: 'Ноябрь',
year: '2025',
period: data.period || '112025',
accrued: [
{ code: '01', description: 'Оклад', amount: 2891.87 },
{ code: '02', description: 'Премия', amount: 0 }
],
totalAccrued: 2891.87,
deductions: [
{ code: '101', description: 'НДФЛ', amount: 376.36 },
{ code: '102', description: 'Профсоюз', amount: 85.50 }
],
totalDeductions: 461.86,
totalPaid: 2430.01,
bankTransfers: [
{ bank: 'Сбербанк', amount: 2430.01 }
]
};
}
// --- 2. Генерируем PDF в памяти (Buffer) ---
const pdfBuffer = await generatePayslipPDF(payslipData);
logger.info('[PRINT-PAYSLIP] PDF сгенерирован, размер:', pdfBuffer.length, 'байт');
if (pdfBuffer.length < 100) {
throw new Error('Сгенерированный PDF слишком маленький');
}
// --- 3. Преобразуем Buffer в base64 для передачи через IPC ---
const pdfBase64 = pdfBuffer.toString('base64');
logger.info('[PRINT-PAYSLIP] PDF сконвертирован в base64, длина:', pdfBase64.length);
// --- 4. Возвращаем base64 PDF ---
return {
success: true,
pdfBase64: pdfBase64,
fileName: `${data.tnom || 'payslip'}.pdf`
};
} catch (error) {
logger.error('[PRINT-PAYSLIP] ❌ Ошибка:', error.message);
logger.error('[PRINT-PAYSLIP] Стек:', error.stack);
return { success: false, error: error.message };
}
});
// ✅ 5. Обработчик получения списка принтеров
ipcMain.handle('get-printers', async () => {
return [
{
name: 'GP-80250 Series',
displayName: 'GP-80250 Series',
isDefault: true,
status: 'ready'
}
];
});
// ✅ 6. Обработчик получения конфига
ipcMain.handle('get-app-config', async (event) => {
return appConfig;
});
// ============================================
// 🚀 СОЗДАНИЕ ГЛАВНОГО ОКНА
// ============================================
function createWindow() {
const preloadPath = path.join(__dirname, 'preload.js');
try {
@@ -90,94 +617,6 @@ function createWindow() {
});
}
// ============================================
// 📄 ПЕЧАТЬ PDF ЧЕРЕЗ pdf-to-printer
// ============================================
ipcMain.handle('print-pdf', async (event, data = {}) => {
let tempFile = null;
try {
if (!data.pdfUrl) {
throw new Error('URL файла PDF не передан');
}
const cleanUrl = data.pdfUrl.split('#')[0];
logger.info('[PRINT] Загрузка PDF:', cleanUrl);
// ✅ Скачиваем PDF
const response = await axios.get(cleanUrl, {
responseType: 'arraybuffer',
timeout: 30000
});
const pdfBuffer = Buffer.from(response.data);
logger.info('[PRINT] Размер PDF:', pdfBuffer.length, 'байт');
if (pdfBuffer.length < 100) {
throw new Error('PDF слишком маленький или пустой');
}
// ✅ Сохраняем во временный файл
const tempDir = app.getPath('temp');
tempFile = path.join(tempDir, `print_${Date.now()}.pdf`);
fs.writeFileSync(tempFile, pdfBuffer);
logger.info('[PRINT] Временный файл создан:', tempFile);
// ✅ ПЕЧАТАЕМ ЧЕРЕЗ pdf-to-printer
const printerName = 'GP-80250 Series';
logger.info('[PRINT] Отправляем на печать через pdf-to-printer:', printerName);
// ✅ Опции печати
const options = {
printer: printerName,
// scale: 'fit', // Масштабирование
// paperSize: 'A4',
// landscape: false,
// copies: 1,
// silent: true // Тихая печать
};
const result = await print(tempFile, options);
logger.info('[PRINT] Результат печати:', result);
// ✅ Удаляем временный файл
try {
if (tempFile && fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
logger.info('[PRINT] Временный файл удален');
}
} catch (e) {}
logger.info('[PRINT] ✅ Документ отправлен на печать');
return true;
} catch (error) {
logger.error('[PRINT] ❌ Ошибка:', error.message);
try {
if (tempFile && fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
}
} catch (e) {}
throw error;
}
});
// ============================================
// 📄 ПОЛУЧЕНИЕ СПИСКА ПРИНТЕРОВ
// ============================================
ipcMain.handle('get-printers', async () => {
return [
{
name: 'GP-80250 Series',
displayName: 'GP-80250 Series',
isDefault: true,
status: 'ready'
}
];
});
// ============================================
// 🚀 ЗАПУСК
// ============================================
@@ -226,8 +665,4 @@ app.on('activate', () => {
app.on('will-quit', () => {
globalShortcut.unregisterAll();
});
ipcMain.handle('get-app-config', async (event) => {
return appConfig;
});
Binary file not shown.
Binary file not shown.
Binary file not shown.
+113
View File
@@ -0,0 +1,113 @@
// pdfGenerator.js - АЛЬТЕРНАТИВНАЯ ВЕРСИЯ (для pdfmake 0.1.72)
const pdfmake = require('pdfmake');
// Для версии 0.1.72 используется другой способ
const fonts = {
Roboto: {
normal: 'C:/Windows/Fonts/arial.ttf',
bold: 'C:/Windows/Fonts/arialbd.ttf',
italics: 'C:/Windows/Fonts/ariali.ttf',
bolditalics: 'C:/Windows/Fonts/arialbi.ttf'
}
};
// Для старой версии pdfmake
const printer = new pdfmake(fonts);
function buildDocDefinition(data) {
return {
pageSize: { width: 80 * 2.83465, height: 297 * 2.83465 },
pageMargins: [5, 5, 5, 5],
content: [
{ text: 'РАСЧЕТНЫЙ ЛИСТОК', style: 'header' },
{ text: `${data.month || ''} ${data.year || ''} (${data.period || ''})`, style: 'subheader' },
{ text: ' ', style: 'divider' },
{ text: `ФИО: ${data.fullName || ''}`, style: 'row' },
{ text: `Таб.№: ${data.tabNumber || ''}`, style: 'row' },
{ text: `Подразделение: ${data.department || ''}`, style: 'row' },
{ text: `Должность: ${data.position || ''}`, style: 'row' },
{ text: `Отработано: ${data.daysWorked || ''}`, style: 'row' },
{ text: ' ', style: 'divider' },
{ text: 'НАЧИСЛЕНО', style: 'sectionTitle' },
...(data.accrued || []).map(item => ({
columns: [
{ text: `${item.code || ''} ${item.description || ''}`, width: '*', style: 'itemDesc' },
{ text: (item.amount || 0).toFixed(2), alignment: 'right', style: 'itemAmount' }
],
style: 'itemRow'
})),
{ text: `ИТОГО НАЧИСЛЕНО: ${(data.totalAccrued || 0).toFixed(2)}`, style: 'totalRow' },
{ text: ' ', style: 'divider' },
{ text: 'УДЕРЖАНО', style: 'sectionTitle' },
...(data.deductions || []).map(item => ({
columns: [
{ text: `${item.code || ''} ${item.description || ''}`, width: '*', style: 'itemDesc' },
{ text: (item.amount || 0).toFixed(2), alignment: 'right', style: 'itemAmount' }
],
style: 'itemRow'
})),
{ text: `ИТОГО УДЕРЖАНО: ${(data.totalDeductions || 0).toFixed(2)}`, style: 'totalRow' },
{ text: ' ', style: 'divider' },
{ text: `К ВЫПЛАТЕ: ${(data.totalPaid || 0).toFixed(2)}`, style: 'totalRow big' },
{ text: ' ', style: 'divider' },
...(data.bankTransfers && data.bankTransfers.length > 0 ? [
{ text: 'БАНКОВСКИЕ ПЕРЕВОДЫ', style: 'sectionTitle' },
...data.bankTransfers.map(bank => ({
columns: [
{ text: bank.bank || '', width: '*', style: 'itemDesc' },
{ text: (bank.amount || 0).toFixed(2), alignment: 'right', style: 'itemAmount' }
],
style: 'itemRow'
}))
] : []),
...(data.other && data.other.length > 0 ? [
{ text: 'ПРОЧЕЕ', style: 'sectionTitle' },
...data.other.map(item => ({
columns: [
{ text: `${item.code || ''} ${item.description || ''}`, width: '*', style: 'itemDesc' },
{ text: (item.amount || 0).toFixed(2), alignment: 'right', style: 'itemAmount' }
],
style: 'itemRow'
})),
{ text: `ИТОГО ПРОЧЕЕ: ${(data.totalOther || 0).toFixed(2)}`, style: 'totalRow' }
] : []),
{ text: ' ', style: 'divider' },
{ text: `Сформировано: ${new Date().toLocaleString('ru-RU')}`, style: 'footer' }
],
styles: {
header: { fontSize: 14, bold: true, alignment: 'center', margin: [0, 0, 0, 4] },
subheader: { fontSize: 12, alignment: 'center', margin: [0, 0, 0, 4] },
row: { fontSize: 10, margin: [0, 2, 0, 2] },
divider: { margin: [0, 4, 0, 4], border: [false, true, false, false] },
sectionTitle: { fontSize: 12, bold: true, margin: [0, 6, 0, 2] },
itemRow: { fontSize: 10, margin: [0, 1, 0, 1] },
itemDesc: { fontSize: 10 },
itemAmount: { fontSize: 10, alignment: 'right' },
totalRow: { fontSize: 12, bold: true, margin: [0, 4, 0, 0] },
'totalRow big': { fontSize: 14, bold: true, margin: [0, 8, 0, 0] },
footer: { fontSize: 8, alignment: 'center', color: '#666', margin: [0, 8, 0, 0] }
}
};
}
function generatePayslipPDF(data) {
const docDefinition = buildDocDefinition(data);
const pdfDoc = printer.createPdfKitDocument(docDefinition);
return new Promise((resolve, reject) => {
const chunks = [];
pdfDoc.on('data', chunk => chunks.push(chunk));
pdfDoc.on('end', () => resolve(Buffer.concat(chunks)));
pdfDoc.on('error', reject);
pdfDoc.end();
});
}
module.exports = { generatePayslipPDF };
+2
View File
@@ -19,6 +19,8 @@ contextBridge.exposeInMainWorld(
},
printPdf: (options) => ipcRenderer.invoke('print-pdf', options),
getPrinters: () => ipcRenderer.invoke('get-printers'),
// ✅ НОВАЯ ФУНКЦИЯ: печать формы расчетного листка
printPayslipForm: (data) => ipcRenderer.invoke('print-payslip-form', data),
}
);