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