Files
2026-10-29 16:51:26 +03:00

668 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = `
<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 {
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();
});