food
This commit is contained in:
+527
-92
@@ -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.
@@ -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 };
|
||||
@@ -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),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user