const { app, BrowserWindow, protocol, ipcMain, globalShortcut } = require('electron'); const path = require('path'); require('dotenv').config({path: path.join(__dirname, 'config.env')}) const isDev = require('electron-is-dev'); const fs = require('fs'); 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([ { scheme: 'app', privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true } } ]); if (!Promise.withResolvers) { Promise.withResolvers = function () { let resolve, reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; }; } let mainWindow; let appConfig = {}; const configPath = isDev ? path.join(__dirname, '../src/config.json') : path.join(process.resourcesPath, 'config.json'); try { logger.info(`[Config] Trying to load config from: ${configPath}`); if (fs.existsSync(configPath)) { appConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); logger.info('[Config] External config loaded successfully:', appConfig); } else { logger.warn('[Config] External config file not found. Using default values.'); } } catch (error) { logger.error('[Config] Could not load external config file:', error); } 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 = `
ТЕСТ ПЕЧАТИ
Дата: ${new Date().toLocaleString()}
Табельный номер: TEST
`; } const fullHtml = ` Расчетный листок ${htmlContent}
Сгенерировано: ${new Date().toLocaleString()}
`; 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 = '
ОШИБКА: Контент не загрузился!
'; } `); } 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 { fs.accessSync(preloadPath); logger.debug(`[DEBUG] Preload script found at: ${preloadPath}`); } catch (e) { logger.error(`[FATAL] Preload script not found at: ${preloadPath}`); app.quit(); return; } mainWindow = new BrowserWindow({ kiosk: true, titleBarStyle: 'hidden', alwaysOnTop: false, webPreferences: { preload: preloadPath, contextIsolation: true, nodeIntegration: false }, }); mainWindow.loadURL( isDev ? `${config.frontUrl}` : `file://${path.join(__dirname, '../build/index.html')}` ); mainWindow.on('close', (e) => { if (!global.allowClose) { e.preventDefault(); } }); mainWindow.on('minimize', (e) => { e.preventDefault(); }); } // ============================================ // 🚀 ЗАПУСК // ============================================ app.whenReady().then(() => { createWindow(); global.allowMinimize = false; globalShortcut.register('Control+Alt+Backspace', () => { global.allowMinimize = true; if (mainWindow) mainWindow.minimize(); }); mainWindow.on('minimize', function (event) { if (global.allowMinimize) { global.allowMinimize = false; return; } event.preventDefault(); setTimeout(() => { if (mainWindow.isMinimized()) { mainWindow.restore(); mainWindow.focus(); } }, 10); }); globalShortcut.register('Control+Alt+Enter', () => { global.allowClose = true; app.quit(); }); globalShortcut.register('F11', () => {}); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (mainWindow === null) { createWindow(); } }); app.on('will-quit', () => { globalShortcut.unregisterAll(); });