nuzhno-sdelat-sayt-ya-buhgal/app.js

566 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Основное приложение
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 =
`<div class="card"><h3>✅ Загружено сотрудников: ${result.imported}</h3><p>Всего в базе: ${result.total}</p></div>`;
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 =
`<div class="card"><h3>✅ Обработано платёжек: ${result.imported}</h3></div>`;
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 =>
`<div style="padding:8px 0;border-bottom:1px solid #eee;font-size:13px">
<span style="color:var(--gray-500)">${new Date(o.timestamp).toLocaleDateString('ru-RU')}</span>:
${o.description}
</div>`
).join('') : '<p style="color:var(--gray-500)">Операций пока нет</p>';
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 =>
`<div style="padding:8px 0;border-bottom:1px solid #eee;font-size:13px">
⚠️ ${d.number} — остаток ${formatMoney(d.amount - d.paidAmount)}
</div>`
).join('') : '<p style="color:var(--gray-500)">Нет проблемных записей</p>';
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 `
<tr>
<td>${e.tabNum}</td>
<td>${e.name}</td>
<td>${e.iin}</td>
<td>${formatMoney(e.salary)}</td>
<td>${decrees.length}</td>
<td>${formatMoney(paidThisMonth)}</td>
<td>
<button class="btn btn-secondary" onclick="editEmployee(${e.id})" style="padding:6px 12px;font-size:12px">✏️</button>
</td>
</tr>
`;
}).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 `
<tr>
<td>${d.number}</td>
<td>${new Date(d.date).toLocaleDateString('ru-RU')}</td>
<td>${emp ? emp.name : '❌ Не найден'}</td>
<td>
<span class="badge ${d.type === 'alimony' ? 'badge-ali' : 'badge-other'}">
${d.type === 'alimony' ? 'Алименты' : 'Штрафы'}
</span>
</td>
<td>${formatMoney(d.amount)}</td>
<td>${formatMoney(d.paidAmount || 0)}</td>
<td>${percent}%</td>
<td>
<span class="badge ${d.status === 'closed' ? 'badge-closed' : 'badge-active'}">
${d.status === 'closed' ? 'Закрыто' : 'Активно'}
</span>
</td>
<td>
<button class="btn btn-danger" onclick="deleteDecreeItem(${d.id})" style="padding:6px 12px;font-size:12px">🗑️</button>
</td>
</tr>
`;
}).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 `
<tr>
<td>${a.employee.name}</td>
<td>${formatMoney(a.employee.salary)}</td>
<td>${formatMoney(a.maxDeduction)}</td>
<td>${a.allocation.length}</td>
<td>${alimonyCount}</td>
<td>${otherCount}</td>
<td>${formatMoney(a.totalAllocated)}</td>
<td>${formatMoney(a.remaining)}</td>
</tr>
`;
}).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 `
<tr>
<td>${new Date(p.date).toLocaleDateString('ru-RU')}</td>
<td>${p.decreeNumber}</td>
<td>${decree ? (db.employees.find(e => e.id === decree.employeeId)?.name || '❌') : '❌'}</td>
<td>
<span class="badge ${decree && decree.type === 'alimony' ? 'badge-ali' : 'badge-other'}">
${decree && decree.type === 'alimony' ? 'Алименты' : 'Штрафы'}
</span>
</td>
<td>${formatMoney(p.amount)}</td>
<td><span class="badge badge-closed">✅</span></td>
</tr>
`;
}).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 `
<tr>
<td>${quarter}</td>
<td>${d.number}</td>
<td>${emp ? emp.name : '❌'}</td>
<td><span class="badge ${d.type === 'alimony' ? 'badge-ali' : 'badge-other'}">${d.type === 'alimony' ? 'Алименты' : 'Штрафы'}</span></td>
<td>${formatMoney(d.amount)}</td>
<td>${formatMoney(d.paidAmount || 0)}</td>
<td>${formatMoney(d.amount - (d.paidAmount || 0))}</td>
</tr>
`;
}).join('');
const stats = getStats();
document.getElementById('report-stats').innerHTML = `
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:12px">
<div style="padding:16px;background:var(--gray-100);border-radius:8px">
<div style="font-size:24px;font-weight:700">${stats.activeDecrees}</div>
<div style="font-size:13px;color:var(--gray-500)">Активных постановлений</div>
</div>
<div style="padding:16px;background:var(--gray-100);border-radius:8px">
<div style="font-size:24px;font-weight:700">${stats.closedDecrees}</div>
<div style="font-size:13px;color:var(--gray-500)">Закрыто за период</div>
</div>
</div>
`;
}
function populateEmployeeSelect() {
const select = document.getElementById('decree-employee');
const employees = getEmployees().sort((a, b) => a.name.localeCompare(b.name));
select.innerHTML = '<option value="">Выберите сотрудника</option>' +
employees.map(e => `<option value="${e.id}">${e.tabNum} — ${e.name}</option>`).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;