580 lines
26 KiB
HTML
580 lines
26 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Личные финансы</title>
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||
<style>
|
||
:root {
|
||
--bg: #0f0f12;
|
||
--card: #1a1a1f;
|
||
--text: #e4e4e7;
|
||
--muted: #71717a;
|
||
--accent: #22c55e;
|
||
--negative: #ef4444;
|
||
--border: #27272a;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
margin: 0;
|
||
padding: 1.5rem;
|
||
line-height: 1.5;
|
||
}
|
||
h1 { font-size: 1.5rem; margin: 0 0 1.5rem; font-weight: 600; }
|
||
h2 { font-size: 1.1rem; margin: 0 0 0.75rem; color: var(--muted); font-weight: 500; }
|
||
.nav { display: flex; gap: 1rem; margin-bottom: 1.5rem; }
|
||
.nav a { color: var(--muted); text-decoration: none; }
|
||
.nav a.active { color: var(--accent); }
|
||
.nav a:hover { color: var(--text); }
|
||
.card {
|
||
background: var(--card);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
padding: 1.25rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 2rem; }
|
||
.metric { text-align: center; min-width: 0; padding: 0 0.5rem; }
|
||
.metric .value { font-size: 1.5rem; font-weight: 700; }
|
||
.metric .value.positive { color: var(--accent); }
|
||
.metric .value.negative { color: var(--negative); }
|
||
.metric .label { font-size: 0.8rem; color: var(--muted); margin-top: 0.25rem; }
|
||
table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
|
||
th, td { padding: 0.5rem 0.75rem; text-align: left; border-bottom: 1px solid var(--border); }
|
||
th { color: var(--muted); font-weight: 500; }
|
||
.amount.in { color: var(--accent); }
|
||
.amount.out { color: var(--negative); }
|
||
.page { display: none; }
|
||
.page.active { display: block; }
|
||
button, .btn {
|
||
background: var(--accent);
|
||
color: var(--bg);
|
||
border: none;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
}
|
||
button.secondary { background: var(--border); color: var(--text); }
|
||
button:hover { opacity: 0.9; }
|
||
input[type="file"] { margin: 0.5rem 0; }
|
||
.banks-list { list-style: none; padding: 0; margin: 0; }
|
||
.banks-list li {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 0.5rem 0; border-bottom: 1px solid var(--border);
|
||
}
|
||
.charts-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||
@media (max-width: 700px) { .charts-row { grid-template-columns: 1fr; } }
|
||
.chart-wrap { height: 220px; }
|
||
.error { color: var(--negative); font-size: 0.9rem; margin-top: 0.5rem; }
|
||
.tx-excluded { opacity: 0.6; }
|
||
input[type="checkbox"] { cursor: pointer; accent-color: var(--accent); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>Личные финансы</h1>
|
||
<nav class="nav">
|
||
<a href="#" data-page="dashboard" class="active">Дашборд</a>
|
||
<a href="#" data-page="settings">Настройки</a>
|
||
</nav>
|
||
|
||
<div id="page-dashboard" class="page active">
|
||
<div class="card filters-card">
|
||
<h2>Фильтры</h2>
|
||
<div class="filters-row" style="display:flex; flex-wrap:wrap; gap:1.5rem; align-items:flex-end;">
|
||
<div>
|
||
<label style="display:block; font-size:0.85rem; color:var(--muted); margin-bottom:0.25rem;">Период с</label>
|
||
<input type="date" id="filter-period-start" style="background:var(--bg); color:var(--text); border:1px solid var(--border); padding:0.4rem; border-radius:6px;" />
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.85rem; color:var(--muted); margin-bottom:0.25rem;">по</label>
|
||
<input type="date" id="filter-period-end" style="background:var(--bg); color:var(--text); border:1px solid var(--border); padding:0.4rem; border-radius:6px;" />
|
||
</div>
|
||
<div>
|
||
<span style="font-size:0.85rem; color:var(--muted); margin-right:0.5rem;">Банки:</span>
|
||
<span id="filter-banks"></span>
|
||
</div>
|
||
<label style="display:flex; align-items:center; gap:0.5rem; white-space:nowrap;">
|
||
<input type="checkbox" id="exclude-transfers-cb" />
|
||
<span>Не учитывать переводы</span>
|
||
</label>
|
||
<button type="button" id="filter-apply" class="secondary" style="padding:0.4rem 0.75rem;">Применить</button>
|
||
<button type="button" id="filter-reset" class="secondary" style="padding:0.4rem 0.75rem;">Сбросить</button>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Сводка</h2>
|
||
<div class="grid" id="summary-metrics"></div>
|
||
</div>
|
||
<div class="charts-row">
|
||
<div class="card">
|
||
<h2>Доход и расход по месяцам</h2>
|
||
<div class="chart-wrap"><canvas id="chart-income-expense"></canvas></div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Динамика остатка</h2>
|
||
<div class="chart-wrap"><canvas id="chart-balance"></canvas></div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Динамика копилки</h2>
|
||
<div class="chart-wrap"><canvas id="chart-savings"></canvas></div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Все операции</h2>
|
||
<div class="pagination-controls" style="display:flex; align-items:center; gap:1rem; margin-bottom:0.75rem; flex-wrap:wrap;">
|
||
<span>На странице:</span>
|
||
<select id="page-size" style="background:var(--card); color:var(--text); border:1px solid var(--border); padding:0.35rem 0.5rem; border-radius:6px;">
|
||
<option value="50">50</option>
|
||
<option value="100">100</option>
|
||
</select>
|
||
<span id="pagination-info" style="color:var(--muted); font-size:0.9rem;"></span>
|
||
<button type="button" id="btn-prev" class="secondary" style="padding:0.35rem 0.75rem;">Назад</button>
|
||
<button type="button" id="btn-next" class="secondary" style="padding:0.35rem 0.75rem;">Вперёд</button>
|
||
</div>
|
||
<div id="transactions-table"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="page-settings" class="page">
|
||
<div class="card">
|
||
<h2>Зарплатный банк</h2>
|
||
<p class="muted" style="font-size:0.9rem; color: var(--muted);">Входящие на выбранный банк считаются доходом.</p>
|
||
<ul class="banks-list" id="banks-list"></ul>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Доходы</h2>
|
||
<p class="muted" style="font-size:0.9rem; color: var(--muted);">Учёт дохода в сводке и на графиках.</p>
|
||
<label style="display:flex; align-items:center; gap:0.5rem; margin-bottom:0.75rem;">
|
||
<input type="checkbox" id="income-only-salary-card" />
|
||
<span>Считать доходы только по зарплатной карте</span>
|
||
</label>
|
||
<div id="income-salary-card-wrap" style="display:none; margin-top:0.5rem;">
|
||
<label style="font-size:0.9rem; color:var(--muted);">Зарплатная карта:</label>
|
||
<select id="income-salary-account" style="background:var(--bg); color:var(--text); border:1px solid var(--border); padding:0.4rem; border-radius:6px; margin-left:0.5rem; min-width:200px;">
|
||
<option value="">— выбрать карту —</option>
|
||
</select>
|
||
</div>
|
||
<button type="button" id="btn-save-income-settings" class="secondary" style="margin-top:0.75rem;">Сохранить</button>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Импорт выписок</h2>
|
||
<p>Загрузите PDF файл (Т-MM-YY.pdf) или импортируйте из папки «Выписки банков».</p>
|
||
<input type="file" id="file-input" accept=".pdf" />
|
||
<button type="button" id="btn-upload">Загрузить файл</button>
|
||
<span class="error" id="upload-error"></span>
|
||
<div style="margin-top:1rem;">
|
||
<button type="button" id="btn-import-folder" class="secondary">Импорт из папки</button>
|
||
<span id="import-result"></span>
|
||
<p style="font-size:0.85rem; color:var(--muted); margin-top:0.5rem;">Если добавлено 0 при повторном нажатии — перезапустите сервер (uvicorn) и нажмите снова.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const API = '';
|
||
async function get(url) {
|
||
const r = await fetch(API + url);
|
||
if (!r.ok) throw new Error(await r.text());
|
||
return r.json();
|
||
}
|
||
async function put(url) {
|
||
const r = await fetch(API + url, { method: 'PUT' });
|
||
if (!r.ok) throw new Error(await r.text());
|
||
return r.json();
|
||
}
|
||
async function patch(url, body) {
|
||
const r = await fetch(API + url, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||
if (!r.ok) throw new Error(await r.text());
|
||
return r.json();
|
||
}
|
||
function fmtMoney(n) {
|
||
return new Intl.NumberFormat('ru-RU', { style: 'decimal', minimumFractionDigits: 2 }).format(n);
|
||
}
|
||
function fmtDate(s) {
|
||
if (!s) return '';
|
||
return s.slice(0, 10).split('-').reverse().join('.');
|
||
}
|
||
|
||
let chartIncomeExpense, chartBalance, chartSavings;
|
||
let filterPeriodStart = '';
|
||
let filterPeriodEnd = '';
|
||
let filterBankIds = [];
|
||
|
||
function buildFilterQuery() {
|
||
const p = [];
|
||
if (filterPeriodStart) p.push('period_start=' + encodeURIComponent(filterPeriodStart));
|
||
if (filterPeriodEnd) p.push('period_end=' + encodeURIComponent(filterPeriodEnd));
|
||
if (filterBankIds.length) p.push('bank_ids=' + filterBankIds.join(','));
|
||
return p.length ? '?' + p.join('&') : '';
|
||
}
|
||
|
||
async function loadSummary() {
|
||
const data = await get('/api/balance/summary' + buildFilterQuery());
|
||
const el = document.getElementById('summary-metrics');
|
||
el.innerHTML = `
|
||
<div class="metric">
|
||
<div class="value ${data.total_balance >= 0 ? 'positive' : 'negative'}">${fmtMoney(data.total_balance)} ₽</div>
|
||
<div class="label">Суммарный баланс</div>
|
||
</div>
|
||
<div class="metric">
|
||
<div class="value positive">${fmtMoney(data.income)} ₽</div>
|
||
<div class="label">Доход (зарплатный счёт)</div>
|
||
</div>
|
||
<div class="metric">
|
||
<div class="value positive">${fmtMoney(data.debit)} ₽</div>
|
||
<div class="label">Дебет (приход)</div>
|
||
</div>
|
||
<div class="metric">
|
||
<div class="value negative">${fmtMoney(data.credit)} ₽</div>
|
||
<div class="label">Кредит (расход)</div>
|
||
</div>
|
||
<div class="metric">
|
||
<div class="value positive">${fmtMoney(data.savings_balance ?? 0)} ₽</div>
|
||
<div class="label">Копилка (накопления)</div>
|
||
</div>
|
||
<div class="metric">
|
||
<div class="value ${(data.net_flow ?? 0) >= 0 ? 'positive' : 'negative'}">${fmtMoney(data.net_flow ?? 0)} ₽</div>
|
||
<div class="label">За период: ${(data.net_flow ?? 0) >= 0 ? 'накопление' : 'траты'}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
let transactionsPage = 0;
|
||
let transactionsPageSize = 50;
|
||
|
||
async function loadTransactions() {
|
||
const offset = transactionsPage * transactionsPageSize;
|
||
const base = buildFilterQuery();
|
||
const data = await get('/api/transactions' + (base ? base + '&' : '?') + 'limit=' + transactionsPageSize + '&offset=' + offset);
|
||
const items = data.items || [];
|
||
const total = data.total ?? 0;
|
||
const el = document.getElementById('transactions-table');
|
||
const infoEl = document.getElementById('pagination-info');
|
||
if (!items.length && total === 0) {
|
||
el.innerHTML = '<p style="color:var(--muted)">Нет операций. Импортируйте выписки в Настройках.</p>';
|
||
infoEl.textContent = '';
|
||
document.getElementById('btn-prev').disabled = true;
|
||
document.getElementById('btn-next').disabled = true;
|
||
return;
|
||
}
|
||
const from = offset + 1;
|
||
const to = Math.min(offset + transactionsPageSize, total);
|
||
infoEl.textContent = 'Показано ' + from + '–' + to + ' из ' + total;
|
||
document.getElementById('btn-prev').disabled = transactionsPage === 0;
|
||
document.getElementById('btn-next').disabled = offset + items.length >= total;
|
||
el.innerHTML = `
|
||
<table>
|
||
<thead><tr><th title="Учитывать в расчёте баланса"><input type="checkbox" id="th-include-all" title="В расчёте" /></th><th>Банк</th><th>Дата</th><th>Счёт</th><th>Описание</th><th>Сумма</th></tr></thead>
|
||
<tbody>
|
||
${items.map(t => {
|
||
const included = !t.excluded_from_balance;
|
||
return `
|
||
<tr class="${included ? '' : 'tx-excluded'}" data-id="${t.id}">
|
||
<td><input type="checkbox" class="tx-include-cb" data-id="${t.id}" ${included ? 'checked' : ''} title="Учитывать в расчёте баланса" /></td>
|
||
<td>${t.bank_name || t.bank_code || ''}</td>
|
||
<td>${fmtDate(t.operation_date)}</td>
|
||
<td>***${t.external_id}</td>
|
||
<td>${(t.description || '').slice(0, 40)}${(t.description||'').length > 40 ? '…' : ''}</td>
|
||
<td class="amount ${t.amount >= 0 ? 'in' : 'out'}">${t.amount >= 0 ? '+' : ''}${fmtMoney(t.amount)} ₽</td>
|
||
</tr>
|
||
`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
el.querySelectorAll('.tx-include-cb').forEach(cb => {
|
||
cb.onclick = async () => {
|
||
const id = parseInt(cb.dataset.id, 10);
|
||
const excluded = !cb.checked;
|
||
try {
|
||
await patch('/api/transactions/' + id, { excluded_from_balance: excluded });
|
||
cb.closest('tr').classList.toggle('tx-excluded', excluded);
|
||
loadSummary();
|
||
loadCharts();
|
||
} catch (e) {
|
||
cb.checked = !cb.checked;
|
||
console.error(e);
|
||
}
|
||
};
|
||
});
|
||
const thAll = document.getElementById('th-include-all');
|
||
if (thAll) {
|
||
const includedCount = items.filter(t => !t.excluded_from_balance).length;
|
||
thAll.checked = includedCount === items.length;
|
||
thAll.indeterminate = includedCount > 0 && includedCount < items.length;
|
||
thAll.onclick = async () => {
|
||
const wantInclude = thAll.checked;
|
||
for (const row of el.querySelectorAll('tbody tr')) {
|
||
const id = parseInt(row.dataset.id, 10);
|
||
const cb = row.querySelector('.tx-include-cb');
|
||
if (cb.checked !== wantInclude) {
|
||
try {
|
||
await patch('/api/transactions/' + id, { excluded_from_balance: !wantInclude });
|
||
cb.checked = wantInclude;
|
||
row.classList.toggle('tx-excluded', !wantInclude);
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
}
|
||
loadSummary();
|
||
loadCharts();
|
||
};
|
||
}
|
||
}
|
||
|
||
function initPagination() {
|
||
document.getElementById('page-size').onchange = () => {
|
||
transactionsPageSize = parseInt(document.getElementById('page-size').value, 10);
|
||
transactionsPage = 0;
|
||
loadTransactions();
|
||
};
|
||
document.getElementById('btn-prev').onclick = () => {
|
||
if (transactionsPage > 0) { transactionsPage--; loadTransactions(); }
|
||
};
|
||
document.getElementById('btn-next').onclick = () => {
|
||
transactionsPage++; loadTransactions();
|
||
};
|
||
}
|
||
|
||
async function loadCharts() {
|
||
const chartQ = buildFilterQuery();
|
||
const [byMonth, dynamics, savingsDynamics] = await Promise.all([
|
||
get('/api/charts/income-expense' + chartQ),
|
||
get('/api/charts/balance-dynamics' + chartQ),
|
||
get('/api/charts/savings-dynamics' + chartQ)
|
||
]);
|
||
const ctx1 = document.getElementById('chart-income-expense').getContext('2d');
|
||
if (chartIncomeExpense) chartIncomeExpense.destroy();
|
||
chartIncomeExpense = new Chart(ctx1, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: (byMonth || []).map(x => x.month),
|
||
datasets: [
|
||
{ label: 'Доход', data: (byMonth || []).map(x => x.income), backgroundColor: 'rgba(34, 197, 94, 0.7)' },
|
||
{ label: 'Расход', data: (byMonth || []).map(x => x.credit), backgroundColor: 'rgba(239, 68, 68, 0.7)' }
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
scales: { x: { grid: { color: 'rgba(255,255,255,0.06)' } }, y: { grid: { color: 'rgba(255,255,255,0.06)' } } },
|
||
plugins: { legend: { labels: { color: '#e4e4e7' } } }
|
||
}
|
||
});
|
||
const ctx2 = document.getElementById('chart-balance').getContext('2d');
|
||
if (chartBalance) chartBalance.destroy();
|
||
chartBalance = new Chart(ctx2, {
|
||
type: 'line',
|
||
data: {
|
||
labels: (dynamics || []).map(x => x.date),
|
||
datasets: [{ label: 'Остаток (накопл.)', data: (dynamics || []).map(x => x.balance), borderColor: '#22c55e', fill: true, tension: 0.2 }]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
scales: { x: { grid: { color: 'rgba(255,255,255,0.06)' } }, y: { grid: { color: 'rgba(255,255,255,0.06)' } } },
|
||
plugins: { legend: { labels: { color: '#e4e4e7' } } }
|
||
}
|
||
});
|
||
const ctx3 = document.getElementById('chart-savings').getContext('2d');
|
||
if (chartSavings) chartSavings.destroy();
|
||
chartSavings = new Chart(ctx3, {
|
||
type: 'line',
|
||
data: {
|
||
labels: (savingsDynamics || []).map(x => x.date),
|
||
datasets: [{ label: 'Копилка (накопл.)', data: (savingsDynamics || []).map(x => x.savings), borderColor: '#eab308', fill: true, tension: 0.2 }]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
scales: { x: { grid: { color: 'rgba(255,255,255,0.06)' } }, y: { grid: { color: 'rgba(255,255,255,0.06)' } } },
|
||
plugins: { legend: { labels: { color: '#e4e4e7' } } }
|
||
}
|
||
});
|
||
}
|
||
|
||
async function ensureFilterBanks() {
|
||
if (document.querySelector('.filter-bank-cb')) return;
|
||
const banks = await get('/api/banks');
|
||
const el = document.getElementById('filter-banks');
|
||
if (!el || !banks.length) return;
|
||
el.innerHTML = banks.map(b => `<label style="margin-right:0.75rem; white-space:nowrap;"><input type="checkbox" class="filter-bank-cb" data-id="${b.id}" checked /> ${b.name}</label>`).join('');
|
||
}
|
||
|
||
function applyFilters() {
|
||
filterPeriodStart = document.getElementById('filter-period-start').value || '';
|
||
filterPeriodEnd = document.getElementById('filter-period-end').value || '';
|
||
const all = document.querySelectorAll('.filter-bank-cb');
|
||
const checked = document.querySelectorAll('.filter-bank-cb:checked');
|
||
filterBankIds = (all.length && checked.length < all.length) ? Array.from(checked).map(cb => parseInt(cb.dataset.id, 10)) : [];
|
||
transactionsPage = 0;
|
||
loadDashboard();
|
||
}
|
||
|
||
function resetFilters() {
|
||
document.getElementById('filter-period-start').value = '';
|
||
document.getElementById('filter-period-end').value = '';
|
||
document.querySelectorAll('.filter-bank-cb').forEach(cb => { cb.checked = true; });
|
||
filterPeriodStart = '';
|
||
filterPeriodEnd = '';
|
||
filterBankIds = [];
|
||
transactionsPage = 0;
|
||
loadDashboard();
|
||
}
|
||
|
||
async function loadExcludeTransfersCheckbox() {
|
||
try {
|
||
const data = await get('/api/settings/exclude-transfers');
|
||
const cb = document.getElementById('exclude-transfers-cb');
|
||
if (cb) cb.checked = data.exclude_transfers || false;
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
}
|
||
|
||
async function loadDashboard() {
|
||
try {
|
||
await ensureFilterBanks();
|
||
await loadExcludeTransfersCheckbox();
|
||
await loadSummary();
|
||
await loadTransactions();
|
||
await loadCharts();
|
||
} catch (e) {
|
||
console.error(e);
|
||
document.getElementById('summary-metrics').innerHTML = '<p class="error">Ошибка загрузки: ' + e.message + '</p>';
|
||
}
|
||
}
|
||
|
||
async function loadIncomeSettings() {
|
||
const data = await get('/api/settings/income');
|
||
const cb = document.getElementById('income-only-salary-card');
|
||
const wrapDiv = document.getElementById('income-salary-card-wrap');
|
||
const sel = document.getElementById('income-salary-account');
|
||
cb.checked = data.count_income_only_salary_card || false;
|
||
if (wrapDiv) wrapDiv.style.display = cb.checked ? 'block' : 'none';
|
||
sel.innerHTML = '<option value="">— выбрать карту —</option>' +
|
||
(data.salary_accounts || []).map(a => `<option value="${a.id}" ${a.id === data.salary_account_id ? 'selected' : ''}>***${a.external_id} ${a.name || ''}</option>`).join('');
|
||
if (cb.checked && data.salary_account_id && !sel.querySelector('option[value="' + data.salary_account_id + '"]')) {
|
||
const opt = document.createElement('option');
|
||
opt.value = data.salary_account_id;
|
||
opt.textContent = '*** (id ' + data.salary_account_id + ')';
|
||
opt.selected = true;
|
||
sel.appendChild(opt);
|
||
}
|
||
}
|
||
|
||
async function loadBanks() {
|
||
const banks = await get('/api/banks');
|
||
const el = document.getElementById('banks-list');
|
||
if (!banks.length) {
|
||
el.innerHTML = '<li style="color:var(--muted)">Нет банков. Импортируйте выписки Т-банка.</li>';
|
||
return;
|
||
}
|
||
el.innerHTML = banks.map(b => `
|
||
<li>
|
||
<span>${b.name} (${b.code}) ${b.is_salary ? ' ★ зарплатный' : ''}</span>
|
||
${!b.is_salary ? `<button type="button" data-bank-id="${b.id}" class="btn-set-salary secondary">Сделать зарплатным</button>` : ''}
|
||
</li>
|
||
`).join('');
|
||
el.querySelectorAll('.btn-set-salary').forEach(btn => {
|
||
btn.onclick = async () => {
|
||
await put('/api/banks/salary/' + btn.dataset.bankId);
|
||
loadBanks();
|
||
loadDashboard();
|
||
};
|
||
});
|
||
}
|
||
|
||
document.querySelectorAll('.nav a').forEach(a => {
|
||
a.onclick = (e) => {
|
||
e.preventDefault();
|
||
document.querySelectorAll('.nav a').forEach(x => x.classList.remove('active'));
|
||
document.querySelectorAll('.page').forEach(x => x.classList.remove('active'));
|
||
a.classList.add('active');
|
||
document.getElementById('page-' + a.dataset.page).classList.add('active');
|
||
if (a.dataset.page === 'settings') { loadBanks(); loadIncomeSettings(); }
|
||
else loadDashboard();
|
||
};
|
||
});
|
||
|
||
document.getElementById('btn-upload').onclick = async () => {
|
||
const input = document.getElementById('file-input');
|
||
const err = document.getElementById('upload-error');
|
||
if (!input.files?.length) { err.textContent = 'Выберите файл'; return; }
|
||
err.textContent = '';
|
||
const form = new FormData();
|
||
form.append('file', input.files[0]);
|
||
try {
|
||
const r = await fetch(API + '/api/import/upload', { method: 'POST', body: form });
|
||
const data = await r.json().catch(() => ({}));
|
||
if (!r.ok) throw new Error(data.detail || r.statusText);
|
||
err.style.color = 'var(--accent)';
|
||
err.textContent = data.parsed != null ? `Распознано: ${data.parsed}, добавлено: ${data.added}, дубликатов: ${data.skipped_duplicates}` : `Добавлено: ${data.added}, дубликатов: ${data.skipped_duplicates}`;
|
||
input.value = '';
|
||
loadDashboard();
|
||
} catch (e) {
|
||
err.style.color = 'var(--negative)';
|
||
err.textContent = e.message;
|
||
}
|
||
};
|
||
|
||
document.getElementById('btn-import-folder').onclick = async () => {
|
||
const res = document.getElementById('import-result');
|
||
res.textContent = ' Загрузка…';
|
||
try {
|
||
const r = await fetch(API + '/api/import/from-folder', { method: 'POST' });
|
||
const d = await r.json();
|
||
if (!r.ok) throw new Error(d.detail || r.statusText);
|
||
res.style.color = 'var(--accent)';
|
||
res.textContent = d.parsed != null ? ` Распознано: ${d.parsed}, добавлено: ${d.added}, дубликатов: ${d.skipped_duplicates}` : ` Добавлено: ${d.added}, дубликатов: ${d.skipped_duplicates}`;
|
||
loadDashboard();
|
||
loadBanks();
|
||
} catch (e) {
|
||
res.style.color = 'var(--negative)';
|
||
res.textContent = ' ' + e.message;
|
||
}
|
||
};
|
||
|
||
document.getElementById('filter-apply').onclick = applyFilters;
|
||
document.getElementById('filter-reset').onclick = resetFilters;
|
||
document.getElementById('exclude-transfers-cb').onchange = async function() {
|
||
try {
|
||
await fetch(API + '/api/settings/exclude-transfers', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ exclude_transfers: this.checked })
|
||
});
|
||
await loadSummary();
|
||
await loadCharts();
|
||
} catch (e) { console.error(e); }
|
||
};
|
||
document.getElementById('income-only-salary-card').onchange = function() {
|
||
document.getElementById('income-salary-card-wrap').style.display = this.checked ? 'block' : 'none';
|
||
};
|
||
document.getElementById('btn-save-income-settings').onclick = async function() {
|
||
const cb = document.getElementById('income-only-salary-card');
|
||
const sel = document.getElementById('income-salary-account');
|
||
const accountId = sel.value ? parseInt(sel.value, 10) : null;
|
||
try {
|
||
const r = await fetch(API + '/api/settings/income', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ count_income_only_salary_card: cb.checked, salary_account_id: accountId })
|
||
});
|
||
if (!r.ok) throw new Error('Ошибка сохранения');
|
||
await loadIncomeSettings();
|
||
loadDashboard();
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
};
|
||
initPagination();
|
||
loadDashboard();
|
||
</script>
|
||
</body>
|
||
</html>
|