216 lines
7.9 KiB
HTML
216 lines
7.9 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Мой кошелёк — учёт доходов и расходов</title>
|
||
<style>
|
||
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7;--green:#10B981;--red:#EF4444}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{font:17px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:var(--white)}
|
||
.container{max-width:1140px;margin:0 auto;padding:40px 24px}
|
||
h1{font-size:36px;font-weight:800;margin-bottom:24px}
|
||
h2{font-size:24px;font-weight:700;margin-bottom:16px}
|
||
.card{background:var(--gray-100);border-radius:16px;padding:24px;margin-bottom:20px}
|
||
.form-row{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px}
|
||
input,select,button{padding:12px 16px;border-radius:8px;border:1px solid #ddd;font-size:16px}
|
||
input,select{flex:1;min-width:150px}
|
||
button{background:var(--ink);color:var(--white);border:none;cursor:pointer;font-weight:700}
|
||
button:hover{opacity:0.9}
|
||
.btn-danger{background:var(--red)}
|
||
.btn-success{background:var(--green)}
|
||
.summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:24px}
|
||
.summary-card{background:var(--ink);color:var(--white);padding:20px;border-radius:12px;text-align:center}
|
||
.summary-card.income{background:var(--green)}
|
||
.summary-card.expense{background:var(--red)}
|
||
.summary-value{font-size:32px;font-weight:800;margin:8px 0}
|
||
.summary-label{color:#9aa3b2;font-size:14px}
|
||
table{width:100%;border-collapse:collapse}
|
||
th,td{padding:12px;text-align:left;border-bottom:1px solid #eee}
|
||
th{font-weight:700;color:var(--gray-500)}
|
||
.type-income{color:var(--green);font-weight:700}
|
||
.type-expense{color:var(--red);font-weight:700}
|
||
.filters{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:20px}
|
||
.chip{padding:8px 16px;border-radius:20px;background:var(--gray-100);cursor:pointer;border:2px solid transparent}
|
||
.chip.active{background:var(--ink);color:var(--white)}
|
||
.chip:hover{border-color:var(--cyan)}
|
||
.no-data{text-align:center;color:var(--gray-500);padding:40px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>💰 Мой кошелёк</h1>
|
||
|
||
<div class="summary">
|
||
<div class="summary-card">
|
||
<div class="summary-label">Баланс</div>
|
||
<div class="summary-value" id="balance">0 ₸</div>
|
||
</div>
|
||
<div class="summary-card income">
|
||
<div class="summary-label">Доходы</div>
|
||
<div class="summary-value" id="income">0 ₸</div>
|
||
</div>
|
||
<div class="summary-card expense">
|
||
<div class="summary-label">Расходы</div>
|
||
<div class="summary-value" id="expense">0 ₸</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Добавить операцию</h2>
|
||
<form id="form">
|
||
<div class="form-row">
|
||
<select id="type" required>
|
||
<option value="expense">Расход</option>
|
||
<option value="income">Доход</option>
|
||
</select>
|
||
<input type="number" id="amount" placeholder="Сумма (₸)" required min="1">
|
||
<input type="text" id="category" placeholder="Категория (Еда, Транспорт...)" list="categories" required>
|
||
<datalist id="categories">
|
||
<option value="Еда">
|
||
<option value="Транспорт">
|
||
<option value="Жильё">
|
||
<option value="Развлечения">
|
||
<option value="Здоровье">
|
||
<option value="Одежда">
|
||
<option value="Продукты">
|
||
<option value="Кафе">
|
||
<option value="Такси">
|
||
<option value="Бензин">
|
||
<option value="Аренда">
|
||
<option value="Коммуналка">
|
||
<option value="Зарплата">
|
||
<option value="Подработка">
|
||
<option value="Подарок">
|
||
<option value="Другое">
|
||
</datalist>
|
||
<input type="date" id="date" required>
|
||
</div>
|
||
<div class="form-row">
|
||
<input type="text" id="comment" placeholder="Комментарий (необязательно)">
|
||
<button type="submit">Добавить</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>История операций</h2>
|
||
<div class="filters">
|
||
<div class="chip active" data-filter="all">Все</div>
|
||
<div class="chip" data-filter="income">Доходы</div>
|
||
<div class="chip" data-filter="expense">Расходы</div>
|
||
</div>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Дата</th>
|
||
<th>Категория</th>
|
||
<th>Комментарий</th>
|
||
<th>Сумма</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="list"></tbody>
|
||
</table>
|
||
<div id="noData" class="no-data" style="display:none">Пока нет операций</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let transactions = [];
|
||
let currentFilter = 'all';
|
||
|
||
document.getElementById('date').valueAsDate = new Date();
|
||
|
||
async function load() {
|
||
transactions = await fetch('/api/transactions').then(r => r.json());
|
||
render();
|
||
}
|
||
|
||
function render() {
|
||
const list = document.getElementById('list');
|
||
const noData = document.getElementById('noData');
|
||
|
||
const filtered = transactions.filter(t => {
|
||
if (currentFilter === 'all') return true;
|
||
return t.type === currentFilter;
|
||
}).sort((a, b) => new Date(b.date) - new Date(a.date));
|
||
|
||
if (filtered.length === 0) {
|
||
list.innerHTML = '';
|
||
noData.style.display = 'block';
|
||
} else {
|
||
noData.style.display = 'none';
|
||
list.innerHTML = filtered.map(t => `
|
||
<tr>
|
||
<td>${formatDate(t.date)}</td>
|
||
<td>${escapeHtml(t.category)}</td>
|
||
<td>${escapeHtml(t.comment || '')}</td>
|
||
<td class="type-${t.type}">${t.type === 'income' ? '+' : '-'}${formatMoney(t.amount)}</td>
|
||
<td><button class="btn-danger" style="padding:6px 12px;font-size:14px" onclick="remove(${t.id})">✕</button></td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
const income = transactions.filter(t => t.type === 'income').reduce((s, t) => s + t.amount, 0);
|
||
const expense = transactions.filter(t => t.type === 'expense').reduce((s, t) => s + t.amount, 0);
|
||
const balance = income - expense;
|
||
|
||
document.getElementById('income').textContent = '+' + formatMoney(income);
|
||
document.getElementById('expense').textContent = '-' + formatMoney(expense);
|
||
document.getElementById('balance').textContent = formatMoney(balance);
|
||
}
|
||
|
||
function formatMoney(amount) {
|
||
return amount.toLocaleString('ru-RU') + ' ₸';
|
||
}
|
||
|
||
function formatDate(dateStr) {
|
||
const d = new Date(dateStr);
|
||
return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: '2-digit' });
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function remove(id) {
|
||
if (confirm('Удалить эту запись?')) {
|
||
fetch('/api/transactions/' + id, { method: 'DELETE' }).then(() => load());
|
||
}
|
||
}
|
||
|
||
document.getElementById('form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const data = {
|
||
type: document.getElementById('type').value,
|
||
amount: document.getElementById('amount').value,
|
||
category: document.getElementById('category').value,
|
||
date: document.getElementById('date').value,
|
||
comment: document.getElementById('comment').value
|
||
};
|
||
await fetch('/api/transactions', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
document.getElementById('form').reset();
|
||
document.getElementById('date').valueAsDate = new Date();
|
||
load();
|
||
});
|
||
|
||
document.querySelectorAll('.chip').forEach(chip => {
|
||
chip.addEventListener('click', () => {
|
||
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
|
||
chip.classList.add('active');
|
||
currentFilter = chip.dataset.filter;
|
||
render();
|
||
});
|
||
});
|
||
|
||
load();
|
||
</script>
|
||
</body>
|
||
</html> |