292 lines
10 KiB
JavaScript
292 lines
10 KiB
JavaScript
const http = require('http');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const XLSX = require('xlsx');
|
||
const formidable = require('formidable');
|
||
|
||
const PORT = process.env.PORT || 3000;
|
||
const DATA_FILE = path.join(__dirname, 'data.json');
|
||
|
||
function readData() {
|
||
try {
|
||
return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
|
||
} catch (e) {
|
||
return { employees: [], decrees: [], payments: [], operations: [] };
|
||
}
|
||
}
|
||
|
||
function writeData(data) {
|
||
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
|
||
}
|
||
|
||
function parseBody(req) {
|
||
return new Promise((resolve) => {
|
||
let s = '';
|
||
req.on('data', (c) => (s += c));
|
||
req.on('end', () => {
|
||
try {
|
||
resolve(JSON.parse(s || '{}'));
|
||
} catch (_) {
|
||
resolve({});
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function sendJSON(res, data, status = 200) {
|
||
res.writeHead(status, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify(data));
|
||
}
|
||
|
||
http.createServer(async (req, res) => {
|
||
const url = req.url.split('?')[0];
|
||
|
||
if (req.method === 'GET' && url === '/api/data') {
|
||
const data = readData();
|
||
return sendJSON(res, data);
|
||
}
|
||
|
||
if (req.method === 'POST' && url === '/api/data') {
|
||
const body = await parseBody(req);
|
||
writeData(body);
|
||
return sendJSON(res, { ok: true });
|
||
}
|
||
|
||
if (req.method === 'POST' && url === '/api/upload-sap') {
|
||
const form = formidable({ uploadDir: __dirname, keepExtensions: true });
|
||
form.parse(req, async (err, fields, files) => {
|
||
if (err) {
|
||
return sendJSON(res, { error: err.message }, 400);
|
||
}
|
||
|
||
const file = files.file && files.file[0];
|
||
if (!file) {
|
||
return sendJSON(res, { error: 'Нет файла' }, 400);
|
||
}
|
||
|
||
try {
|
||
const wb = XLSX.readFile(file.filepath);
|
||
const sheet = wb.Sheets[wb.SheetNames[0]];
|
||
const data = XLSX.utils.sheet_to_json(sheet);
|
||
|
||
fs.unlinkSync(file.filepath);
|
||
|
||
const currentData = readData();
|
||
let imported = 0;
|
||
|
||
data.forEach(row => {
|
||
const tabNum = row['Табельный номер'] || row['Таб.номер'] || row['tab_num'];
|
||
if (tabNum) {
|
||
const existing = currentData.employees.find(e => e.tabNum === String(tabNum));
|
||
if (existing) {
|
||
existing.name = row['ФИО'] || row['FIO'] || existing.name;
|
||
existing.iin = row['ИИН'] || row['IIN'] || existing.iin;
|
||
existing.salary = parseFloat(row['ЧистыйДоход'] || row['Чистый доход'] || row['salary'] || existing.salary);
|
||
} else {
|
||
currentData.employees.push({
|
||
id: Date.now() + Math.random(),
|
||
tabNum: String(tabNum),
|
||
name: row['ФИО'] || row['FIO'] || 'Не указано',
|
||
iin = row['ИИН'] || row['IIN'] || '',
|
||
salary: parseFloat(row['ЧистыйДоход'] || row['Чистый доход'] || row['salary'] || 0)
|
||
});
|
||
imported++;
|
||
}
|
||
}
|
||
});
|
||
|
||
writeData(currentData);
|
||
sendJSON(res, { ok: true, imported, total: currentData.employees.length });
|
||
} catch (err) {
|
||
sendJSON(res, { error: err.message }, 400);
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (req.method === 'POST' && url === '/api/upload-payment') {
|
||
const form = formidable({ uploadDir: __dirname, keepExtensions: true });
|
||
form.parse(req, async (err, fields, files) => {
|
||
if (err) {
|
||
return sendJSON(res, { error: err.message }, 400);
|
||
}
|
||
|
||
const file = files.file && files.file[0];
|
||
if (!file) {
|
||
return sendJSON(res, { error: 'Нет файла' }, 400);
|
||
}
|
||
|
||
try {
|
||
const wb = XLSX.readFile(file.filepath);
|
||
const sheet = wb.Sheets[wb.SheetNames[0]];
|
||
const data = XLSX.utils.sheet_to_json(sheet);
|
||
|
||
fs.unlinkSync(file.filepath);
|
||
|
||
const currentData = readData();
|
||
let imported = 0;
|
||
|
||
data.forEach(row => {
|
||
const decreeNumber = row['Номер постановления'] || row['decree_number'];
|
||
if (decreeNumber) {
|
||
const decree = currentData.decrees.find(d => d.number === String(decreeNumber));
|
||
if (decree) {
|
||
currentData.payments.push({
|
||
id: Date.now() + Math.random(),
|
||
decreeId: decree.id,
|
||
decreeNumber: String(decreeNumber),
|
||
amount: parseFloat(row['Сумма'] || row['amount'] || 0),
|
||
date: row['Дата'] || row['date'] || new Date().toISOString().split('T')[0],
|
||
period: fields.period || new Date().toISOString().slice(0, 7)
|
||
});
|
||
|
||
decree.paidAmount = (decree.paidAmount || 0) + parseFloat(row['Сумма'] || row['amount'] || 0);
|
||
if (decree.paidAmount >= decree.amount) {
|
||
decree.status = 'closed';
|
||
}
|
||
imported++;
|
||
}
|
||
}
|
||
});
|
||
|
||
writeData(currentData);
|
||
sendJSON(res, { ok: true, imported });
|
||
} catch (err) {
|
||
sendJSON(res, { error: err.message }, 400);
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (req.method === 'POST' && url === '/api/export-bank') {
|
||
const body = await parseBody(req);
|
||
const data = readData();
|
||
|
||
const exportData = [];
|
||
data.employees.forEach(emp => {
|
||
const decrees = data.decrees.filter(d => d.employeeId === emp.id && d.status === 'active');
|
||
if (decrees.length > 0) {
|
||
const maxDeduction = emp.salary * 0.5;
|
||
const alimonyDecrees = decrees.filter(d => d.type === 'alimony');
|
||
const otherDecrees = decrees.filter(d => d.type === 'other');
|
||
|
||
let allocated = 0;
|
||
alimonyDecrees.forEach(d => {
|
||
const remaining = d.amount - (d.paidAmount || 0);
|
||
const amount = Math.min(remaining, maxDeduction * 0.5);
|
||
if (amount > 0) {
|
||
exportData.push({
|
||
'Таб.номер': emp.tabNum,
|
||
'ФИО': emp.name,
|
||
'ИИН': emp.iin,
|
||
'Постановление': d.number,
|
||
'Тип': 'Алименты',
|
||
'Сумма удержания': amount.toFixed(2)
|
||
});
|
||
allocated += amount;
|
||
}
|
||
});
|
||
|
||
const remainingForOther = maxDeduction - allocated;
|
||
if (remainingForOther > 0 && otherDecrees.length > 0) {
|
||
const perOther = remainingForOther / otherDecrees.length;
|
||
otherDecrees.forEach(d => {
|
||
const remaining = d.amount - (d.paidAmount || 0);
|
||
const amount = Math.min(remaining, perOther);
|
||
if (amount > 0) {
|
||
exportData.push({
|
||
'Таб.номер': emp.tabNum,
|
||
'ФИО': emp.name,
|
||
'ИИН': emp.iin,
|
||
'Постановление': d.number,
|
||
'Тип': 'Штрафы',
|
||
'Сумма удержания': amount.toFixed(2)
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
if (exportData.length === 0) {
|
||
return sendJSON(res, { error: 'Нет данных' }, 400);
|
||
}
|
||
|
||
const ws = XLSX.utils.json_to_sheet(exportData);
|
||
const wb = XLSX.utils.book_new();
|
||
XLSX.utils.book_append_sheet(wb, ws, 'Банк');
|
||
|
||
const filename = `bank_export_${body.period || new Date().toISOString().slice(0, 7)}.xlsx`;
|
||
const filepath = path.join(__dirname, filename);
|
||
XLSX.writeFile(wb, filepath);
|
||
|
||
sendJSON(res, { ok: true, filename, count: exportData.length });
|
||
}
|
||
|
||
if (req.method === 'POST' && url === '/api/export-report') {
|
||
const body = await parseBody(req);
|
||
const data = readData();
|
||
|
||
const exportData = data.decrees.map(d => {
|
||
const emp = data.employees.find(e => e.id === d.employeeId);
|
||
return {
|
||
'Квартал': body.quarter || '',
|
||
'Постановление': d.number,
|
||
'Дата': new Date(d.date).toLocaleDateString('ru-RU'),
|
||
'Сотрудник': emp ? emp.name : '',
|
||
'ИИН': emp ? emp.iin : '',
|
||
'Тип': d.type === 'alimony' ? 'Алименты' : 'Штрафы',
|
||
'Сумма долга': d.amount.toFixed(2),
|
||
'Выплачено': (d.paidAmount || 0).toFixed(2),
|
||
'Остаток': (d.amount - (d.paidAmount || 0)).toFixed(2),
|
||
'Статус': d.status === 'closed' ? 'Закрыто' : 'Активно'
|
||
};
|
||
});
|
||
|
||
const ws = XLSX.utils.json_to_sheet(exportData);
|
||
const wb = XLSX.utils.book_new();
|
||
XLSX.utils.book_append_sheet(wb, ws, 'Отчёт');
|
||
|
||
const filename = `report_${body.quarter || new Date().toISOString().slice(0, 7)}.xlsx`;
|
||
const filepath = path.join(__dirname, filename);
|
||
XLSX.writeFile(wb, filepath);
|
||
|
||
sendJSON(res, { ok: true, filename, count: exportData.length });
|
||
}
|
||
|
||
if (req.method === 'GET' && url.startsWith('/api/download/')) {
|
||
const filename = decodeURIComponent(url.replace('/api/download/', ''));
|
||
const filepath = path.join(__dirname, filename);
|
||
|
||
if (fs.existsSync(filepath)) {
|
||
res.writeHead(200, {
|
||
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
'Content-Disposition': `attachment; filename="${filename}"`
|
||
});
|
||
fs.createReadStream(filepath).pipe(res);
|
||
|
||
setTimeout(() => {
|
||
try { fs.unlinkSync(filepath); } catch (e) {}
|
||
}, 60000);
|
||
} else {
|
||
sendJSON(res, { error: 'Файл не найден' }, 404);
|
||
}
|
||
return;
|
||
}
|
||
|
||
let file = url === '/' ? '/index.html' : url;
|
||
const full = path.join(__dirname, file);
|
||
|
||
if (fs.existsSync(full) && fs.statSync(full).isFile()) {
|
||
const ext = path.extname(full);
|
||
const type = ext === '.css' ? 'text/css' : ext === '.js' ? 'application/javascript' : 'text/html';
|
||
res.writeHead(200, { 'Content-Type': type + '; charset=utf-8' });
|
||
res.end(fs.readFileSync(full));
|
||
} else {
|
||
res.writeHead(404);
|
||
res.end('Not found');
|
||
}
|
||
}).listen(PORT, () => {
|
||
console.log('Сервер запущен на порту ' + PORT);
|
||
});
|