// Основное приложение
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;