print_add

This commit is contained in:
2026-09-01 11:47:15 +03:00
parent 1fe0d67731
commit 70a20665ae
38 changed files with 5494 additions and 889 deletions
+9 -4
View File
@@ -1,5 +1,10 @@
{
"payslipsPath": "\\\\PC-20010\\payslips2",
"payslipsPath": "./payslips",
"networkCredentials": {
"username": "",
"password": "",
"domain": "WORKGROUP"
},
"server": {
"host": "localhost",
"port": "3001"
@@ -51,9 +56,9 @@
}
],
"payslips": [
"Петров_ПП_2026-08.pdf",
"Сидорова_АС_2026-08.pdf",
"Иванов_ИИ_2026-08.pdf"
"001.pdf",
"002.pdf",
"003.pdf"
]
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+117
View File
@@ -0,0 +1,117 @@
const express = require('express');
const path = require('path');
const fs = require('fs').promises;
const { logger } = require('../config/logger');
const { parsePayslipExcel } = require('../services/excelParser');
const { generatePayslipForm } = require('../services/formGenerator');
const router = express.Router();
/**
* GET /api/payslip/form/:tnom
* Получить HTML-форму расчетного листка
*/
router.get('/form/:tnom', async (req, res) => {
try {
const { tnom } = req.params;
const { period } = req.query;
logger.info(`[PayslipForm] Запрос формы для ${tnom}, период: ${period || 'не указан'}`);
// 1. Находим Excel-файл
const configPath = path.join(__dirname, '../../config.json');
const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
let payslipBasePath = config.payslipsPath || './payslips';
if (!path.isAbsolute(payslipBasePath)) {
payslipBasePath = path.join(__dirname, '../..', payslipBasePath);
}
let excelPath;
if (period) {
excelPath = path.join(payslipBasePath, period, `${tnom}.xlsx`);
} else {
excelPath = path.join(payslipBasePath, `${tnom}.xlsx`);
}
logger.info(`[PayslipForm] Путь к Excel: ${excelPath}`);
// 2. Проверяем существование файла
try {
await fs.access(excelPath);
} catch (err) {
logger.warn(`[PayslipForm] Excel не найден: ${excelPath}`);
return res.status(404).json({
message: 'Excel-файл расчетного листка не найден',
path: excelPath
});
}
// 3. Парсим Excel
const data = parsePayslipExcel(excelPath);
logger.info(`[PayslipForm] Данные извлечены:`, {
fullName: data.fullName,
tabNumber: data.tabNumber,
totalAccrued: data.totalAccrued,
totalPaid: data.totalPaid
});
// 4. Генерируем HTML-форму
const html = generatePayslipForm(data);
// 5. Отправляем HTML
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.send(html);
} catch (error) {
logger.error('[PayslipForm] Ошибка:', error.message);
res.status(500).json({
message: 'Ошибка при формировании расчетного листка',
error: error.message
});
}
});
/**
* GET /api/payslip/form-data/:tnom
* Получить данные из Excel в JSON (для отладки)
*/
router.get('/form-data/:tnom', async (req, res) => {
try {
const { tnom } = req.params;
const { period } = req.query;
// Находим Excel-файл
const configPath = path.join(__dirname, '../../config.json');
const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
let payslipBasePath = config.payslipsPath || './payslips';
if (!path.isAbsolute(payslipBasePath)) {
payslipBasePath = path.join(__dirname, '../..', payslipBasePath);
}
let excelPath;
if (period) {
excelPath = path.join(payslipBasePath, period, `${tnom}.xlsx`);
} else {
excelPath = path.join(payslipBasePath, `${tnom}.xlsx`);
}
// Проверяем существование
try {
await fs.access(excelPath);
} catch (err) {
return res.status(404).json({ message: 'Excel не найден' });
}
// Парсим и возвращаем JSON
const data = parsePayslipExcel(excelPath);
res.json(data);
} catch (error) {
logger.error('[PayslipForm] Ошибка получения данных:', error.message);
res.status(500).json({ error: error.message });
}
});
module.exports = router;
+299 -106
View File
@@ -27,10 +27,11 @@ async function readJson(filePath) {
}
async function pathExists(filePath) {
console.log('🔍 Проверка пути:', filePath);
try {
await fsPromises.access(filePath);
return true;
} catch {
} catch (err) {
return false;
}
}
@@ -69,7 +70,9 @@ async function connectToNetworkShare(config) {
// ============================================
const app = express();
app.use(cors());
app.use(express.json());
app.use(function(req, res, next) {
@@ -81,7 +84,31 @@ app.use(function(req, res, next) {
next();
});
app.use(helmet());
// ✅ ИСПРАВЛЕННАЯ НАСТРОЙКА HELMET (разрешаем iframe)
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
// РАЗРЕШАЕМ встраивание в iframe с localhost:3000
frameAncestors: ["'self'", "http://localhost:3000", "http://127.0.0.1:3000"],
// Для PDF файлов
frameSrc: ["'self'", "blob:"],
// Другие необходимые директивы
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "blob:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'self'", "blob:"],
mediaSrc: ["'self'"],
},
},
// Отключаем лишние политики для разработки
crossOriginEmbedderPolicy: false,
crossOriginOpenerPolicy: false,
crossOriginResourcePolicy: { policy: "cross-origin" },
}));
app.use('/api/', limiter);
// ============================================
@@ -342,6 +369,269 @@ app.post('/api/rates/update-from-nbrb', async function(req, res) {
}
});
// ============================================
// 📄 РАСЧЕТНЫЕ ЛИСТКИ (ОБНОВЛЕННЫЕ ЭНДПОИНТЫ)
// ============================================
// ✅ НОВАЯ ФУНКЦИЯ: получает путь к файлу с учетом периода (месяца)
async function getPayslipFilePath(tnom, period = null) {
const configPath = path.join(__dirname, '../config.json');
const config = await readJson(configPath);
let payslipBasePath = config.payslipsPath;
if (!path.isAbsolute(payslipBasePath)) {
payslipBasePath = path.join(__dirname, '..', payslipBasePath);
}
let filePath;
if (period) {
// ✅ Если передан период — ищем в подпапке
filePath = path.join(payslipBasePath, period, tnom + '.pdf');
console.log(`📁 Поиск в подпапке: ${period} -> ${filePath}`);
} else {
// ✅ Если период не передан — ищем в корне (для совместимости)
filePath = path.join(payslipBasePath, tnom + '.pdf');
console.log(`📁 Поиск в корне: ${filePath}`);
}
return filePath;
}
// ✅ ОБНОВЛЕННАЯ ПРОВЕРКА наличия расчетного листка (с period)
app.get('/api/payslip/check/:tnom', async function(req, res) {
try {
var tnom = req.params.tnom;
var period = req.query.period || null; // ✅ Берем period из query-параметров
console.log('==========================================');
console.log(`📁 [PAYSLIP CHECK] Табельный номер: ${tnom}, Период: ${period || 'не указан'}`);
const filePath = await getPayslipFilePath(tnom, period);
const exists = await pathExists(filePath);
console.log(`📁 exists: ${exists}`);
console.log('==========================================');
res.json({
exists: exists,
tnom: tnom,
period: period,
path: filePath,
fileName: tnom + '.pdf'
});
} catch (err) {
logger.error('Ошибка проверки расчетного листка:', err);
res.status(500).json({ error: err.message });
}
});
// ✅ ОБНОВЛЕННОЕ ПОЛУЧЕНИЕ URL (с period)
app.get('/api/payslip/url/:tnom', async function(req, res) {
try {
var tnom = req.params.tnom;
var period = req.query.period || null; // ✅ Берем period из query-параметров
console.log('==========================================');
console.log(`📁 [PAYSLIP URL] Табельный номер: ${tnom}, Период: ${period || 'не указан'}`);
const filePath = await getPayslipFilePath(tnom, period);
console.log(`📁 Полный путь к файлу: ${filePath}`);
const exists = await pathExists(filePath);
console.log(`📁 exists: ${exists}`);
console.log('==========================================');
if (exists) {
// ✅ Формируем URL для получения файла
let fileUrl = `/api/payslip/tnom/${tnom}`;
if (period) {
fileUrl += `?period=${period}`; // ✅ Добавляем period в query
}
logger.info(`[PAYSLIP URL] Файл найден для ${tnom} (период: ${period || 'корень'})`);
res.json({
exists: true,
url: fileUrl,
tnom: tnom,
period: period,
fileName: tnom + '.pdf'
});
} else {
logger.warn(`[PAYSLIP URL] Файл не найден для ${tnom} (период: ${period || 'корень'})`);
res.json({
exists: false,
message: 'Расчетный листок не найден',
tnom: tnom,
period: period
});
}
} catch (err) {
logger.error('Ошибка получения URL расчетного листка:', err);
res.status(500).json({ error: err.message });
}
});
// ✅ ОБНОВЛЕННАЯ ОТДАЧА РАСЧЕТНОГО ЛИСТКА (с period)
app.get('/api/payslip/tnom/:tnom', async function(req, res) {
try {
var tnom = req.params.tnom;
var period = req.query.period || null; // ✅ Берем period из query-параметров
console.log('==========================================');
console.log(`📁 [PAYSLIP] Запрос к /api/payslip/tnom/${tnom}, Период: ${period || 'не указан'}`);
const filePath = await getPayslipFilePath(tnom, period);
console.log(`📁 Полный путь к файлу: ${filePath}`);
// ✅ ЯВНО проверяем существование файла
try {
await fsPromises.access(filePath);
console.log('✅ Файл найден, начинаем отправку...');
} catch (err) {
console.log('❌ Файл НЕ найден:', err.message);
return res.status(404).json({
message: 'Расчетный листок не найден',
tnom: tnom,
period: period,
path: filePath,
error: err.message
});
}
// ✅ Добавляем правильные заголовки для PDF
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'inline; filename="' + tnom + '.pdf"');
// ✅ Отправляем файл с обработкой ошибок
res.sendFile(filePath, function(err) {
if (err) {
console.error('❌ Ошибка при отправке файла:', err);
if (!res.headersSent) {
return res.status(500).json({
message: 'Ошибка при отправке файла',
error: err.message
});
}
} else {
console.log('✅ Файл успешно отправлен');
}
});
} catch (err) {
console.error('❌ Ошибка получения расчетного листка:', err);
res.status(500).json({ error: err.message });
}
});
// ============================================
// 🗄️ СТАРЫЕ ЭНДПОИНТЫ РАСЧЕТНЫХ ЛИСТКОВ (для обратной совместимости)
// ============================================
app.get('/api/payslip/:period/:fileName', async function(req, res) {
try {
var period = req.params.period;
var fileName = req.params.fileName;
var configPath = path.join(__dirname, '../config.json');
var config = await readJson(configPath);
var payslipBasePath = config.payslipsPath;
logger.info('Payslip request - Period: ' + period + ', File: ' + fileName);
logger.info('Base path from config: ' + payslipBasePath);
var networkConnected = await connectToNetworkShare(config);
logger.info('Network share connection for payslip: ' + networkConnected);
var finalPayslipPath;
if (path.isAbsolute(payslipBasePath)) {
finalPayslipPath = path.join(payslipBasePath, period, fileName);
} else {
var projectRoot = path.resolve(__dirname, '../../');
finalPayslipPath = path.join(projectRoot, payslipBasePath, period, fileName);
}
logger.info('Final payslip path: ' + finalPayslipPath);
try {
var basePathExists = await pathExists(payslipBasePath);
logger.info('Base path exists: ' + basePathExists);
if (basePathExists) {
var basePathStats = await fsPromises.stat(payslipBasePath);
logger.info('Base path stats: ' + JSON.stringify({
isDirectory: basePathStats.isDirectory(),
permissions: basePathStats.mode,
size: basePathStats.size
}));
var basePathContents = await fsPromises.readdir(payslipBasePath);
logger.info('Base path contents: ' + JSON.stringify(basePathContents));
var periodPath = path.join(payslipBasePath, period);
var periodPathExists = await pathExists(periodPath);
logger.info('Period path exists: ' + periodPathExists + ' (' + periodPath + ')');
if (periodPathExists) {
var periodPathContents = await fsPromises.readdir(periodPath);
logger.info('Period path contents: ' + JSON.stringify(periodPathContents));
}
}
} catch (basePathError) {
logger.error('Error checking base path', {
error: basePathError.message,
stack: basePathError.stack
});
}
if (await pathExists(finalPayslipPath)) {
logger.info('Payslip found, sending file: ' + finalPayslipPath);
res.sendFile(finalPayslipPath);
} else {
logger.warn('Payslip not found: ' + finalPayslipPath);
res.status(404).json({
message: 'Расчетный листок не найден',
path: finalPayslipPath,
period: period,
fileName: fileName,
basePath: payslipBasePath,
networkConnected: networkConnected
});
}
} catch (err) {
logger.error('Error getting payslip', { error: err.message, stack: err.stack });
res.status(500).json({ message: 'Ошибка сервера' });
}
});
app.get('/api/payslip/:fileName', async function(req, res) {
try {
var fileName = req.params.fileName;
var configPath = path.join(__dirname, '../config.json');
var config = await readJson(configPath);
var payslipBasePath = config.payslipsPath;
var finalPayslipPath;
if (path.isAbsolute(payslipBasePath)) {
finalPayslipPath = path.join(payslipBasePath, fileName);
} else {
var projectRoot = path.resolve(__dirname, '../../');
finalPayslipPath = path.join(projectRoot, payslipBasePath, fileName);
}
logger.info('Attempting to find payslip at resolved path: ' + finalPayslipPath);
if (await pathExists(finalPayslipPath)) {
res.sendFile(finalPayslipPath);
} else {
logger.warn('Payslip not found: ' + finalPayslipPath);
res.status(404).json({ message: 'Расчетный листок не найден' });
}
} catch (err) {
logger.error('Error getting payslip', { error: err.message, stack: err.stack });
res.status(500).json({ message: 'Ошибка сервера' });
}
});
// ============================================
// 🗄️ ДРУГИЕ ЭНДПОИНТЫ
// ============================================
@@ -459,110 +749,6 @@ app.get('/api/test-network', async function(req, res) {
}
});
app.get('/api/payslip/:period/:fileName', async function(req, res) {
try {
var period = req.params.period;
var fileName = req.params.fileName;
var configPath = path.join(__dirname, '../config.json');
var config = await readJson(configPath);
var payslipBasePath = config.payslipsPath;
logger.info('Payslip request - Period: ' + period + ', File: ' + fileName);
logger.info('Base path from config: ' + payslipBasePath);
var networkConnected = await connectToNetworkShare(config);
logger.info('Network share connection for payslip: ' + networkConnected);
var finalPayslipPath;
if (path.isAbsolute(payslipBasePath)) {
finalPayslipPath = path.join(payslipBasePath, period, fileName);
} else {
var projectRoot = path.resolve(__dirname, '../../');
finalPayslipPath = path.join(projectRoot, payslipBasePath, period, fileName);
}
logger.info('Final payslip path: ' + finalPayslipPath);
try {
var basePathExists = await pathExists(payslipBasePath);
logger.info('Base path exists: ' + basePathExists);
if (basePathExists) {
var basePathStats = await fsPromises.stat(payslipBasePath);
logger.info('Base path stats: ' + JSON.stringify({
isDirectory: basePathStats.isDirectory(),
permissions: basePathStats.mode,
size: basePathStats.size
}));
var basePathContents = await fsPromises.readdir(payslipBasePath);
logger.info('Base path contents: ' + JSON.stringify(basePathContents));
var periodPath = path.join(payslipBasePath, period);
var periodPathExists = await pathExists(periodPath);
logger.info('Period path exists: ' + periodPathExists + ' (' + periodPath + ')');
if (periodPathExists) {
var periodPathContents = await fsPromises.readdir(periodPath);
logger.info('Period path contents: ' + JSON.stringify(periodPathContents));
}
}
} catch (basePathError) {
logger.error('Error checking base path', {
error: basePathError.message,
stack: basePathError.stack
});
}
if (await pathExists(finalPayslipPath)) {
logger.info('Payslip found, sending file: ' + finalPayslipPath);
res.sendFile(finalPayslipPath);
} else {
logger.warn('Payslip not found: ' + finalPayslipPath);
res.status(404).json({
message: 'Расчетный листок не найден',
path: finalPayslipPath,
period: period,
fileName: fileName,
basePath: payslipBasePath,
networkConnected: networkConnected
});
}
} catch (err) {
logger.error('Error getting payslip', { error: err.message, stack: err.stack });
res.status(500).json({ message: 'Ошибка сервера' });
}
});
app.get('/api/payslip/:fileName', async function(req, res) {
try {
var fileName = req.params.fileName;
var configPath = path.join(__dirname, '../config.json');
var config = await readJson(configPath);
var payslipBasePath = config.payslipsPath;
var finalPayslipPath;
if (path.isAbsolute(payslipBasePath)) {
finalPayslipPath = path.join(payslipBasePath, fileName);
} else {
var projectRoot = path.resolve(__dirname, '../../');
finalPayslipPath = path.join(projectRoot, payslipBasePath, fileName);
}
logger.info('Attempting to find payslip at resolved path: ' + finalPayslipPath);
if (await pathExists(finalPayslipPath)) {
res.sendFile(finalPayslipPath);
} else {
logger.warn('Payslip not found: ' + finalPayslipPath);
res.status(404).json({ message: 'Расчетный листок не найден' });
}
} catch (err) {
logger.error('Error getting payslip', { error: err.message, stack: err.stack });
res.status(500).json({ message: 'Ошибка сервера' });
}
});
app.get('/api/db-status', async function(req, res) {
try {
var db = require('./config/db');
@@ -583,6 +769,13 @@ app.get('/api/time', function(req, res) {
});
});
// ============================================
// 📄 РАСЧЕТНЫЙ ЛИСТОК (ФОРМА ДЛЯ ПЕЧАТИ)
// ============================================
const payslipFormRoutes = require('./routes/payslipForm');
app.use('/api/payslip', payslipFormRoutes);
// ============================================
// 🚀 ЗАПУСК СЕРВЕРА
// ============================================
+192
View File
@@ -0,0 +1,192 @@
const XLSX = require('xlsx');
const fs = require('fs');
const { logger } = require('../config/logger');
/**
* Парсит Excel-файл расчетного листка
* @param {string} filePath - путь к Excel-файлу
* @returns {Object} структурированные данные
*/
function parsePayslipExcel(filePath) {
try {
logger.info('[ExcelParser] Начинаем парсинг:', filePath);
if (!fs.existsSync(filePath)) {
throw new Error(`Файл не найден: ${filePath}`);
}
// Читаем Excel
const workbook = XLSX.readFile(filePath);
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
logger.info('[ExcelParser] Загружено строк:', data.length);
// Инициализируем структуру данных
const result = {
fullName: '',
department: '',
position: '',
tabNumber: '',
daysWorked: '',
month: '',
year: '',
accrued: [],
deductions: [],
other: [],
totalAccrued: 0,
totalDeductions: 0,
totalPaid: 0,
totalOther: 0,
bankTransfers: [],
advance: '',
accountNumber: ''
};
// Парсим построчно
data.forEach((row, index) => {
const colA = String(row[0] || '').trim();
const colB = String(row[1] || '').trim();
const colC = String(row[2] || '').trim();
const colD = String(row[3] || '').trim();
const colE = String(row[4] || '').trim();
const colF = String(row[5] || '').trim();
const colG = String(row[6] || '').trim();
// Строка 2: ФИО и Табельный номер (индекс 1)
if (index === 1) {
if (colB) result.fullName = colB;
if (colF) result.tabNumber = colF.replace('Таб.номер', '').trim();
}
// Строка 3: Подразделение
if (index === 2 && colB) {
result.department = colB;
}
// Строка 4: Должность
if (index === 3 && colB) {
result.position = colB;
}
// Строка 5: Дни отработано
if (index === 4 && colD) {
result.daysWorked = colD.replace('Отработано дней:', '').trim();
}
// Строка 0: Месяц и год (заголовок)
if (index === 0 && colC) {
const monthMatch = colC.match(/за\s+(\w+)\s+(\d{4})/);
if (monthMatch) {
result.month = monthMatch[1];
result.year = monthMatch[2];
}
}
// Строка 11: Лицевой счет
if (index === 10 && colB && colB.includes('Лиц счет')) {
result.accountNumber = colB.replace('Лиц счет =', '').trim();
}
// Парсим начисления (строки 14-17, индекс 13-16)
if (index >= 13 && index <= 16 && colB && colG) {
const code = colA || '';
const description = colB;
const period = colC || '';
const days = colD || '';
const hours = colE || '';
const amount = parseFloat(colG.replace(',', '.')) || 0;
if (description && !description.includes('ВСЕГО')) {
result.accrued.push({
code,
description,
period,
days,
hours,
amount
});
}
}
// Строка 17: ВСЕГО НАЧИСЛЕНО (индекс 17)
if (index === 17 && colG) {
result.totalAccrued = parseFloat(colG.replace(',', '.')) || 0;
}
// Парсим удержания (строки 19-22, индекс 18-21)
if (index >= 18 && index <= 21 && colB && colG) {
const code = colA || '';
const description = colB;
const period = colC || '';
const days = colD || '';
const hours = colE || '';
const amount = parseFloat(colG.replace(',', '.')) || 0;
if (description && !description.includes('ВСЕГО')) {
result.deductions.push({
code,
description,
period,
days,
hours,
amount
});
}
}
// Строка 22: ВСЕГО УДЕРЖАНО
if (index === 22 && colG) {
result.totalDeductions = parseFloat(colG.replace(',', '.')) || 0;
}
// Строка 23: Аванс
if (index === 23 && colB && colB.includes('Аванс')) {
result.advance = colF || '';
}
// Строки 24-25: Банковские переводы
if (index >= 24 && index <= 25 && colB && colC) {
const bankName = colB.replace('Отчислено в банк', '').trim();
const amount = parseFloat(colC.replace(',', '.')) || 0;
if (bankName && amount > 0) {
result.bankTransfers.push({ bank: bankName, amount });
}
}
// Строка 26: ВСЕГО ВЫПЛАЧЕНО
if (index === 26 && colG) {
result.totalPaid = parseFloat(colG.replace(',', '.')) || 0;
}
// Строка 28: Прочее
if (index >= 28 && index <= 29 && colB && colG) {
const code = colA || '';
const description = colB;
const amount = parseFloat(colG.replace(',', '.')) || 0;
if (description && !description.includes('ВСЕГО')) {
result.other.push({
code,
description,
amount
});
}
}
// Строка 30: ВСЕГО ПРОЧЕЕ
if (index === 30 && colG) {
result.totalOther = parseFloat(colG.replace(',', '.')) || 0;
}
});
logger.info('[ExcelParser] Парсинг завершен успешно');
return result;
} catch (error) {
logger.error('[ExcelParser] Ошибка парсинга:', error.message);
throw error;
}
}
module.exports = { parsePayslipExcel };
+258
View File
@@ -0,0 +1,258 @@
const { logger } = require('../config/logger');
/**
* Генерирует HTML-форму расчетного листка для термопринтера
* @param {Object} data - данные из Excel
* @returns {string} HTML-код формы
*/
function generatePayslipForm(data) {
try {
logger.info('[FormGenerator] Генерация HTML-формы');
const monthNames = {
'Январь': '01', 'Февраль': '02', 'Март': '03', 'Апрель': '04',
'Май': '05', 'Июнь': '06', 'Июль': '07', 'Август': '08',
'Сентябрь': '09', 'Октябрь': '10', 'Ноябрь': '11', 'Декабрь': '12'
};
const monthNum = monthNames[data.month] || '00';
const period = `${monthNum}${data.year || '2026'}`;
// Формируем HTML для термопринтера (80мм ширина)
let html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Расчетный листок</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 80mm;
margin: 0 auto;
padding: 8px 4px;
font-family: 'Courier New', Courier, monospace;
font-size: 11px;
color: #000;
background: #fff;
}
.header {
text-align: center;
font-size: 14px;
font-weight: bold;
padding-bottom: 4px;
border-bottom: 1px dashed #000;
margin-bottom: 4px;
}
.sub-header {
text-align: center;
font-size: 12px;
padding-bottom: 4px;
border-bottom: 1px dashed #000;
margin-bottom: 4px;
}
.row {
display: flex;
justify-content: space-between;
padding: 2px 0;
font-size: 11px;
}
.row-label {
font-weight: bold;
}
.row-value {
text-align: right;
}
.divider {
border-top: 1px dashed #000;
margin: 4px 0;
padding-top: 4px;
}
.total-row {
display: flex;
justify-content: space-between;
padding: 3px 0;
font-weight: bold;
font-size: 12px;
border-top: 1px solid #000;
margin-top: 2px;
}
.section-title {
font-weight: bold;
font-size: 12px;
padding: 4px 0 2px 0;
border-bottom: 1px dotted #000;
margin-bottom: 2px;
}
.item-row {
display: flex;
justify-content: space-between;
padding: 1px 0;
font-size: 10px;
padding-left: 4px;
}
.item-desc {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-amount {
text-align: right;
min-width: 50px;
}
.footer {
text-align: center;
font-size: 9px;
color: #666;
border-top: 1px dashed #000;
margin-top: 6px;
padding-top: 4px;
}
.bank-row {
display: flex;
justify-content: space-between;
padding: 1px 0;
font-size: 10px;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 1px 0;
font-size: 10px;
}
@media print {
body { width: 100%; }
}
</style>
</head>
<body>
<div class="header">
📄 РАСЧЕТНЫЙ ЛИСТОК
</div>
<div class="sub-header">
${data.month || ''} ${data.year || ''} (${period})
</div>
<!-- Информация о сотруднике -->
<div class="info-row">
<span>ФИО:</span>
<span>${data.fullName || ''}</span>
</div>
<div class="info-row">
<span>Таб.№:</span>
<span>${data.tabNumber || ''}</span>
</div>
<div class="info-row">
<span>Подразделение:</span>
<span style="font-size:9px;max-width:120px;overflow:hidden;">${data.department || ''}</span>
</div>
<div class="info-row">
<span>Должность:</span>
<span style="font-size:9px;max-width:120px;overflow:hidden;">${data.position || ''}</span>
</div>
<div class="info-row">
<span>Отработано:</span>
<span>${data.daysWorked || ''}</span>
</div>
<div class="divider"></div>
<!-- Начисления -->
<div class="section-title">📈 НАЧИСЛЕНО</div>
${data.accrued.map(item => `
<div class="item-row">
<span class="item-desc">${item.code || ''} ${item.description || ''}</span>
<span class="item-amount">${item.amount.toFixed(2)}</span>
</div>
`).join('')}
<div class="total-row">
<span>ИТОГО НАЧИСЛЕНО:</span>
<span>${data.totalAccrued.toFixed(2)}</span>
</div>
<div class="divider"></div>
<!-- Удержания -->
<div class="section-title">📉 УДЕРЖАНО</div>
${data.deductions.map(item => `
<div class="item-row">
<span class="item-desc">${item.code || ''} ${item.description || ''}</span>
<span class="item-amount">${item.amount.toFixed(2)}</span>
</div>
`).join('')}
<div class="total-row">
<span>ИТОГО УДЕРЖАНО:</span>
<span>${data.totalDeductions.toFixed(2)}</span>
</div>
<div class="divider"></div>
<!-- Итоги -->
<div class="total-row" style="border-top:2px solid #000;font-size:14px;">
<span>💰 К ВЫПЛАТЕ:</span>
<span>${data.totalPaid.toFixed(2)}</span>
</div>
${data.advance ? `
<div class="row">
<span>Аванс:</span>
<span>${data.advance}</span>
</div>
` : ''}
<!-- Банковские переводы -->
${data.bankTransfers.length > 0 ? `
<div class="divider"></div>
<div class="section-title">🏦 БАНКОВСКИЕ ПЕРЕВОДЫ</div>
${data.bankTransfers.map(bank => `
<div class="bank-row">
<span>${bank.bank || ''}</span>
<span>${bank.amount.toFixed(2)}</span>
</div>
`).join('')}
` : ''}
<!-- Лицевой счет -->
${data.accountNumber ? `
<div class="divider"></div>
<div class="info-row" style="font-size:9px;color:#666;">
<span>Лицевой счет:</span>
<span style="font-size:8px;">${data.accountNumber}</span>
</div>
` : ''}
<!-- Прочее -->
${data.other.length > 0 ? `
<div class="divider"></div>
<div class="section-title">📌 ПРОЧЕЕ</div>
${data.other.map(item => `
<div class="item-row">
<span class="item-desc">${item.code || ''} ${item.description || ''}</span>
<span class="item-amount">${item.amount.toFixed(2)}</span>
</div>
`).join('')}
<div class="row">
<span>ИТОГО ПРОЧЕЕ:</span>
<span>${data.totalOther.toFixed(2)}</span>
</div>
` : ''}
<div class="footer">
Сформировано: ${new Date().toLocaleString('ru-RU')}
<br>
Документ действителен при предъявлении
</div>
</body>
</html>`;
logger.info('[FormGenerator] HTML-форма сгенерирована');
return html;
} catch (error) {
logger.error('[FormGenerator] Ошибка генерации:', error.message);
throw error;
}
}
module.exports = { generatePayslipForm };