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();
});