// Основное приложение let currentFilter = 'all'; let currentDecreeType = 'all'; document.addEventListener('DOMContentLoaded', () => { initNavigation(); initUploadZones(); initFilters(); initForms(); renderAll(); document.getElementById('current-period').addEventListener('change', (e) => { setCurrentPeriod(e.target.value); renderAll(); }); }); function initNavigation() { document.querySelectorAll('.nav button').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.nav button').forEach(b => b.classList.remove('active')); btn.classList.add('active'); document.querySelectorAll('.section').forEach(s => s.classList.remove('active')); document.getElementById(btn.dataset.section).classList.add('active'); renderAll(); }); }); } function initUploadZones() { const sapZone = document.getElementById('sap-upload'); const sapFile = document.getElementById('sap-file'); sapZone.addEventListener('click', () => sapFile.click()); sapFile.addEventListener('change', handleSapUpload); sapZone.addEventListener('dragover', (e) => { e.preventDefault(); sapZone.classList.add('dragover'); }); sapZone.addEventListener('dragleave', () => { sapZone.classList.remove('dragover'); }); sapZone.addEventListener('drop', (e) => { e.preventDefault(); sapZone.classList.remove('dragover'); const files = e.dataTransfer.files; if (files.length) { processSapFile(files[0]); } }); const paymentZone = document.getElementById('payment-upload'); const paymentFile = document.getElementById('payment-file'); paymentZone.addEventListener('click', () => paymentFile.click()); paymentFile.addEventListener('change', handlePaymentUpload); paymentZone.addEventListener('dragover', (e) => { e.preventDefault(); paymentZone.classList.add('dragover'); }); paymentZone.addEventListener('dragleave', () => { paymentZone.classList.remove('dragover'); }); paymentZone.addEventListener('drop', (e) => { e.preventDefault(); paymentZone.classList.remove('dragover'); const files = e.dataTransfer.files; if (files.length) { processPaymentFile(files[0]); } }); } function initFilters() { document.querySelectorAll('#employees .chip').forEach(chip => { chip.addEventListener('click', () => { document.querySelectorAll('#employees .chip').forEach(c => c.classList.remove('active')); chip.classList.add('active'); currentFilter = chip.dataset.filter; renderEmployees(); }); }); document.querySelectorAll('#decrees .chip').forEach(chip => { chip.addEventListener('click', () => { document.querySelectorAll('#decrees .chip').forEach(c => c.classList.remove('active')); chip.classList.add('active'); currentDecreeType = chip.dataset.type; renderDecrees(); }); }); document.getElementById('emp-search').addEventListener('input', renderEmployees); document.getElementById('decree-search').addEventListener('input', renderDecrees); } function initForms() { document.getElementById('decree-form').addEventListener('submit', (e) => { e.preventDefault(); const decree = { number: document.getElementById('decree-number').value.trim(), date: document.getElementById('decree-date').value, employeeId: parseInt(document.getElementById('decree-employee').value), type: document.getElementById('decree-type').value, amount: parseFloat(document.getElementById('decree-amount').value), comment: document.getElementById('decree-comment').value.trim() }; addDecree(decree); closeModal('decree-modal'); document.getElementById('decree-form').reset(); renderAll(); showToast('Постановление добавлено', 'success'); }); document.getElementById('employee-form').addEventListener('submit', (e) => { e.preventDefault(); const id = parseInt(document.getElementById('edit-emp-id').value); const updates = { tabNum: document.getElementById('edit-emp-tabnum').value.trim(), name: document.getElementById('edit-emp-name').value.trim(), iin: document.getElementById('edit-emp-iin').value.trim(), salary: parseFloat(document.getElementById('edit-emp-salary').value) }; updateEmployee(id, updates); closeModal('employee-modal'); renderAll(); showToast('Сотрудник обновлён', 'success'); }); } async function handleSapUpload(e) { const file = e.target.files[0]; if (file) { try { const formData = new FormData(); formData.append('file', file); const resp = await fetch('/api/upload-sap', { method: 'POST', body: formData }); const result = await resp.json(); if (result.ok) { document.getElementById('sap-preview').innerHTML = `

✅ Загружено сотрудников: ${result.imported}

Всего в базе: ${result.total}

`; showToast(`Загружено ${result.imported} сотрудников`, 'success'); renderAll(); } else { showToast('Ошибка: ' + result.error, 'error'); } } catch (err) { showToast('Ошибка загрузки: ' + err.message, 'error'); } } } async function handlePaymentUpload(e) { const file = e.target.files[0]; if (file) { try { const formData = new FormData(); formData.append('file', file); formData.append('period', getCurrentPeriod()); const resp = await fetch('/api/upload-payment', { method: 'POST', body: formData }); const result = await resp.json(); if (result.ok) { document.getElementById('payment-preview').innerHTML = `

✅ Обработано платёжек: ${result.imported}

`; showToast(`Обработано ${result.imported} выплат`, 'success'); renderAll(); } else { showToast('Ошибка: ' + result.error, 'error'); } } catch (err) { showToast('Ошибка загрузки: ' + err.message, 'error'); } } } function parseExcel(arrayBuffer) { const data = new Uint8Array(arrayBuffer); let str = ''; for (let i = 0; i < data.length; i++) { str += String.fromCharCode(data[i]); } const rows = str.split(/\r?\n/).filter(r => r.trim()); if (rows.length < 2) return []; const headers = rows[0].split(/[\t;]/).map(h => h.trim().replace(/^"/, '').replace(/"$/, '')); const result = []; for (let i = 1; i < rows.length; i++) { const values = rows[i].split(/[\t;]/).map(v => v.trim().replace(/^"/, '').replace(/"$/, '')); const row = {}; headers.forEach((h, idx) => { row[h] = values[idx] || ''; }); result.push(row); } return result; } function renderAll() { renderDashboard(); renderEmployees(); renderDecrees(); renderAnalytics(); renderPayments(); renderReports(); populateEmployeeSelect(); } function renderDashboard() { const stats = getStats(); document.getElementById('stat-employees').textContent = stats.employees; document.getElementById('stat-decrees').textContent = stats.activeDecrees; document.getElementById('stat-total-debt').textContent = formatMoney(stats.totalDebt); document.getElementById('stat-paid').textContent = formatMoney(stats.paidThisMonth); const ops = getRecentOperations(5); const opsHtml = ops.length ? ops.map(o => `
${new Date(o.timestamp).toLocaleDateString('ru-RU')}: ${o.description}
` ).join('') : '

Операций пока нет

'; document.getElementById('recent-ops').innerHTML = opsHtml; const attention = db.decrees.filter(d => { const remaining = d.amount - (d.paidAmount || 0); return d.status === 'active' && remaining > 0 && remaining < 10000; }); const attentionHtml = attention.length ? attention.map(d => `
⚠️ ${d.number} — остаток ${formatMoney(d.amount - d.paidAmount)}
` ).join('') : '

Нет проблемных записей

'; document.getElementById('attention-list').innerHTML = attentionHtml; } function renderEmployees() { const search = document.getElementById('emp-search').value.toLowerCase(); let employees = getEmployees(); if (currentFilter === 'active') { employees = employees.filter(e => getDecreesForEmployee(e.id).length > 0); } else if (currentFilter === 'no-decree') { employees = employees.filter(e => getDecreesForEmployee(e.id).length === 0); } if (search) { employees = employees.filter(e => e.name.toLowerCase().includes(search) || e.iin.includes(search) || e.tabNum.toLowerCase().includes(search) ); } const tbody = document.querySelector('#employees-table tbody'); tbody.innerHTML = employees.map(e => { const decrees = getDecreesForEmployee(e.id); const paidThisMonth = decrees.reduce((sum, d) => sum + (d.paidAmount || 0), 0); return ` ${e.tabNum} ${e.name} ${e.iin} ${formatMoney(e.salary)} ${decrees.length} ${formatMoney(paidThisMonth)} `; }).join(''); } function renderDecrees() { const search = document.getElementById('decree-search').value.toLowerCase(); let decrees = getDecrees(currentDecreeType); if (search) { decrees = decrees.filter(d => { const emp = db.employees.find(e => e.id === d.employeeId); return d.number.toLowerCase().includes(search) || (emp && emp.name.toLowerCase().includes(search)); }); } const tbody = document.querySelector('#decrees-table tbody'); tbody.innerHTML = decrees.map(d => { const emp = db.employees.find(e => e.id === d.employeeId); const remaining = d.amount - (d.paidAmount || 0); const percent = d.amount > 0 ? ((d.paidAmount || 0) / d.amount * 100).toFixed(1) : 0; return ` ${d.number} ${new Date(d.date).toLocaleDateString('ru-RU')} ${emp ? emp.name : '❌ Не найден'} ${d.type === 'alimony' ? 'Алименты' : 'Штрафы'} ${formatMoney(d.amount)} ${formatMoney(d.paidAmount || 0)} ${percent}% ${d.status === 'closed' ? 'Закрыто' : 'Активно'} `; }).join(''); } function renderAnalytics() { const employees = getEmployees(); const tbody = document.querySelector('#analytics-table tbody'); const analytics = employees.map(e => calculateDeductionsForEmployee(e)).filter(a => a); tbody.innerHTML = analytics.map(a => { const alimonyCount = a.allocation.filter(x => x.decree.type === 'alimony').length; const otherCount = a.allocation.filter(x => x.decree.type === 'other').length; return ` ${a.employee.name} ${formatMoney(a.employee.salary)} ${formatMoney(a.maxDeduction)} ${a.allocation.length} ${alimonyCount} ${otherCount} ${formatMoney(a.totalAllocated)} ${formatMoney(a.remaining)} `; }).join(''); } function renderPayments() { const payments = getPayments(); const tbody = document.querySelector('#payments-table tbody'); tbody.innerHTML = payments.map(p => { const decree = db.decrees.find(d => d.id === p.decreeId); return ` ${new Date(p.date).toLocaleDateString('ru-RU')} ${p.decreeNumber} ${decree ? (db.employees.find(e => e.id === decree.employeeId)?.name || '❌') : '❌'} ${decree && decree.type === 'alimony' ? 'Алименты' : 'Штрафы'} ${formatMoney(p.amount)} ✅ `; }).join(''); } function renderReports() { const quarter = document.getElementById('report-quarter').value; const [q, year] = quarter.split('-'); const quarterNum = parseInt(q.replace('Q', '')); const startMonth = (quarterNum - 1) * 3 + 1; const tbody = document.querySelector('#report-table tbody'); const decrees = getDecrees(); tbody.innerHTML = decrees.map(d => { const emp = db.employees.find(e => e.id === d.employeeId); return ` ${quarter} ${d.number} ${emp ? emp.name : '❌'} ${d.type === 'alimony' ? 'Алименты' : 'Штрафы'} ${formatMoney(d.amount)} ${formatMoney(d.paidAmount || 0)} ${formatMoney(d.amount - (d.paidAmount || 0))} `; }).join(''); const stats = getStats(); document.getElementById('report-stats').innerHTML = `
${stats.activeDecrees}
Активных постановлений
${stats.closedDecrees}
Закрыто за период
`; } function populateEmployeeSelect() { const select = document.getElementById('decree-employee'); const employees = getEmployees().sort((a, b) => a.name.localeCompare(b.name)); select.innerHTML = '' + employees.map(e => ``).join(''); } function calculateDeductions() { const employees = getEmployees(); let total = 0; employees.forEach(e => { const result = calculateDeductionsForEmployee(e); if (result) { result.allocation.forEach(a => { const remaining = a.decree.amount - (a.decree.paidAmount || 0); if (remaining > 0) { a.decree.paidAmount = (a.decree.paidAmount || 0) + a.allocated; if (a.decree.paidAmount >= a.decree.amount) { a.decree.status = 'closed'; } total += a.allocated; } }); } }); saveData(); renderAll(); showToast(`Рассчитано удержаний на сумму ${formatMoney(total)}`, 'success'); } async function exportBankExcel() { try { const resp = await fetch('/api/export-bank', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ period: getCurrentPeriod() }) }); const result = await resp.json(); if (result.ok) { window.location.href = '/api/download/' + encodeURIComponent(result.filename); showToast(`Файл для банка готов: ${result.count} записей`, 'success'); } else { showToast('Ошибка: ' + result.error, 'error'); } } catch (err) { showToast('Ошибка экспорта: ' + err.message, 'error'); } } function generateReport() { renderReports(); showToast('Отчёт сформирован', 'success'); } async function exportReportExcel() { const quarter = document.getElementById('report-quarter').value; try { const resp = await fetch('/api/export-report', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ quarter }) }); const result = await resp.json(); if (result.ok) { window.location.href = '/api/download/' + encodeURIComponent(result.filename); showToast(`Отчёт готов: ${result.count} записей`, 'success'); } else { showToast('Ошибка: ' + result.error, 'error'); } } catch (err) { showToast('Ошибка экспорта: ' + err.message, 'error'); } } function editEmployee(id) { const emp = db.employees.find(e => e.id === id); if (emp) { document.getElementById('edit-emp-id').value = emp.id; document.getElementById('edit-emp-tabnum').value = emp.tabNum; document.getElementById('edit-emp-name').value = emp.name; document.getElementById('edit-emp-iin').value = emp.iin; document.getElementById('edit-emp-salary').value = emp.salary; openModal('employee-modal'); } } function deleteDecreeItem(id) { if (confirm('Удалить это постановление?')) { deleteDecree(id); renderAll(); showToast('Постановление удалено', 'success'); } } function openModal(id) { document.getElementById(id).classList.add('open'); } function closeModal(id) { document.getElementById(id).classList.remove('open'); } function formatMoney(amount) { return new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'KZT', minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(amount || 0); } function showToast(message, type = 'info') { const toast = document.getElementById('toast'); toast.textContent = message; toast.className = 'toast show ' + type; setTimeout(() => { toast.classList.remove('show'); }, 3000); } window.calculateDeductions = calculateDeductions; window.exportBankExcel = exportBankExcel; window.generateReport = generateReport; window.exportReportExcel = exportReportExcel; window.editEmployee = editEmployee; window.deleteDecreeItem = deleteDecreeItem; window.openModal = openModal; window.closeModal = closeModal;