print_add
This commit is contained in:
+9
-4
@@ -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.
@@ -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
@@ -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);
|
||||
|
||||
// ============================================
|
||||
// 🚀 ЗАПУСК СЕРВЕРА
|
||||
// ============================================
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user