hochu-sdelat-stroitelstvo-po/index.html

347 lines
14 KiB
HTML
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.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Starlink — Учёт абонентов</title>
<style>
:root{--ink:#0F1218;--blue:#0066FF;--green:#10B981;--red:#EF4444;--gray-100:#F2F4F7;--gray-500:#5B6573}
*{box-sizing:border-box;margin:0;padding:0}
body{font:14px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--ink);background:#fff;padding:20px}
.container{max-width:1400px;margin:0 auto}
.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;flex-wrap:wrap;gap:16px}
.header h1{font:700 28px/1.2 sans-serif}
.stats{display:flex;gap:24px;flex-wrap:wrap}
.stat{background:var(--gray-100);padding:16px 24px;border-radius:8px}
.stat-num{font:800 24px/1 sans-serif;color:var(--blue)}
.stat-label{color:var(--gray-500);font-size:13px}
.btn{background:var(--blue);color:#fff;border:none;padding:12px 20px;border-radius:6px;font-weight:600;cursor:pointer;font-size:14px}
.btn:hover{opacity:0.9}
.btn-green{background:var(--green)}
.btn-red{background:var(--red)}
.btn-sm{padding:6px 12px;font-size:13px}
.card{background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:20px;margin-bottom:20px}
.card h2{font:700 20px/1.3 sans-serif;margin-bottom:16px}
.form-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:12px}
input,select,textarea{width:100%;padding:10px;border:1px solid #e0e0e0;border-radius:6px;font-size:14px}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--blue)}
table{width:100%;border-collapse:collapse;margin-top:16px}
th,td{padding:12px;text-align:left;border-bottom:1px solid #e0e0e0}
th{font-weight:600;background:var(--gray-100)}
.badge{display:inline-block;padding:4px 8px;border-radius:4px;font-size:12px;font-weight:600}
.badge-green{background:#D1FAE5;color:#065F46}
.badge-red{background:#FEE2E2;color:#991B1B}
.modal{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;z-index:1000}
.modal.on{display:flex}
.modal-content{background:#fff;padding:32px;border-radius:12px;max-width:500px;width:90%;max-height:90vh;overflow-y:auto}
.modal-content h3{font:700 20px/1.3 sans-serif;margin-bottom:20px}
.close{float:right;font-size:24px;cursor:pointer}
.tabs{display:flex;gap:8px;margin-bottom:20px;border-bottom:1px solid #e0e0e0;padding-bottom:12px}
.tab{padding:8px 16px;border:none;background:none;cursor:pointer;font-weight:600;color:var(--gray-500)}
.tab.active{color:var(--blue);border-bottom:2px solid var(--blue)}
.search{width:300px;padding:10px;border:1px solid #e0e0e0;border-radius:6px;margin-bottom:16px}
.actions{display:flex;gap:8px;flex-wrap:wrap}
.balance-positive{color:var(--green);font-weight:700}
.balance-negative{color:var(--red);font-weight:700}
@media(max-width:768px){.header{flex-direction:column;align-items:flex-start}.stats{width:100%}.search{width:100%}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📡 Starlink — Учёт абонентов</h1>
<div class="stats">
<div class="stat"><div class="stat-num" id="totalSubs">0</div><div class="stat-label">Абонентов</div></div>
<div class="stat"><div class="stat-num" id="activeSubs">0</div><div class="stat-label">Активных</div></div>
<div class="stat"><div class="stat-num" id="totalRevenue">0 ₸</div><div class="stat-label">Доход</div></div>
</div>
</div>
<div class="tabs">
<button class="tab active" onclick="showTab('subscribers')">Абоненты</button>
<button class="tab" onclick="showTab('payments')">Платежи</button>
<button class="tab" onclick="showTab('reports')">Отчёты</button>
</div>
<div id="subscribers-tab">
<div class="card">
<h2>Добавить абонента</h2>
<form id="addSubscriberForm">
<div class="form-row">
<input type="text" name="name" placeholder="ФИО" required>
<input type="text" name="address" placeholder="Адрес (село, улица, дом)" required>
<input type="tel" name="phone" placeholder="Телефон" required>
</div>
<button type="submit" class="btn">Добавить абонента</button>
</form>
</div>
<div class="card">
<h2>Все абоненты</h2>
<input type="text" class="search" id="searchSub" placeholder="Поиск по имени, адресу, телефону..." oninput="filterSubscribers()">
<table>
<thead>
<tr>
<th>Лиц. счёт</th>
<th>ФИО</th>
<th>Адрес</th>
<th>Телефон</th>
<th>Баланс</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="subscribersTable"></tbody>
</table>
</div>
</div>
<div id="payments-tab" style="display:none">
<div class="card">
<h2>Внести платёж</h2>
<form id="addPaymentForm">
<div class="form-row">
<select name="subscriberId" id="paymentSubscriber" required></select>
<input type="number" name="amount" placeholder="Сумма (₸)" required min="1">
<input type="date" name="date" required>
<input type="text" name="comment" placeholder="Комментарий">
</div>
<button type="submit" class="btn btn-green">Внести платёж</button>
</form>
</div>
<div class="card">
<h2>История платежей</h2>
<table>
<thead>
<tr>
<th>Дата</th>
<th>Лиц. счёт</th>
<th>Абонент</th>
<th>Сумма</th>
<th>Комментарий</th>
</tr>
</thead>
<tbody id="paymentsTable"></tbody>
</table>
</div>
</div>
<div id="reports-tab" style="display:none">
<div class="card">
<h2>Экспорт данных</h2>
<p style="margin-bottom:16px;color:var(--gray-500)">Выгрузить список абонентов и платежей в Excel для отчётности</p>
<div class="actions">
<button class="btn" onclick="exportSubscribers()">Скачать абоненты (CSV)</button>
<button class="btn" onclick="exportPayments()">Скачать платежи (CSV)</button>
</div>
</div>
</div>
</div>
<div class="modal" id="editModal">
<div class="modal-content">
<span class="close" onclick="closeModal()">&times;</span>
<h3>Редактировать абонента</h3>
<form id="editForm">
<input type="hidden" name="id">
<div class="form-row">
<input type="text" name="name" placeholder="ФИО" required>
<input type="text" name="address" placeholder="Адрес" required>
<input type="tel" name="phone" placeholder="Телефон" required>
</div>
<div class="form-row">
<select name="status">
<option value="active">Активен</option>
<option value="blocked">Заблокирован</option>
<option value="inactive">Отключен</option>
</select>
<input type="number" name="balance" placeholder="Баланс" step="0.01">
</div>
<button type="submit" class="btn" style="margin-top:16px">Сохранить</button>
</form>
</div>
</div>
<script>
let subscribers = [];
let payments = [];
async function load() {
subscribers = await fetch('/api/subscribers').then(r => r.json());
payments = await fetch('/api/payments').then(r => r.json());
renderSubscribers();
renderPayments();
updateStats();
populateSubscriberSelect();
}
function renderSubscribers() {
const tbody = document.getElementById('subscribersTable');
tbody.innerHTML = subscribers.map(s => `
<tr>
<td><strong>${s.accountNumber}</strong></td>
<td>${s.name}</td>
<td>${s.address}</td>
<td>${s.phone}</td>
<td class="${s.balance >= 0 ? 'balance-positive' : 'balance-negative'}">${s.balance.toFixed(2)} ₸</td>
<td><span class="badge ${s.status === 'active' ? 'badge-green' : 'badge-red'}">${s.status === 'active' ? 'Активен' : s.status}</span></td>
<td>
<button class="btn btn-sm" onclick="editSubscriber(${s.id})">Изменить</button>
<button class="btn btn-sm btn-red" onclick="deleteSubscriber(${s.id})">Удалить</button>
</td>
</tr>
`).join('');
}
function renderPayments() {
const tbody = document.getElementById('paymentsTable');
const rows = payments.slice().reverse().map(p => {
const sub = subscribers.find(s => s.id == p.subscriberId);
return `
<tr>
<td>${p.date}</td>
<td>${sub ? sub.accountNumber : '-'}</td>
<td>${sub ? sub.name : '-'}</td>
<td class="balance-positive">+${p.amount.toFixed(2)} ₸</td>
<td>${p.comment || '-'}</td>
</tr>
`;
});
tbody.innerHTML = rows.join('');
}
function updateStats() {
document.getElementById('totalSubs').textContent = subscribers.length;
document.getElementById('activeSubs').textContent = subscribers.filter(s => s.status === 'active').length;
const total = payments.reduce((sum, p) => sum + p.amount, 0);
document.getElementById('totalRevenue').textContent = total.toFixed(0) + ' ₸';
}
function populateSubscriberSelect() {
const select = document.getElementById('paymentSubscriber');
select.innerHTML = subscribers.map(s => `<option value="${s.id}">${s.accountNumber} — ${s.name}</option>`).join('');
}
function filterSubscribers() {
const q = document.getElementById('searchSub').value.toLowerCase();
const rows = document.querySelectorAll('#subscribersTable tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(q) ? '' : 'none';
});
}
document.getElementById('addSubscriberForm').addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target;
await fetch('/api/subscribers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: form.name.value,
address: form.address.value,
phone: form.phone.value
})
});
form.reset();
load();
});
document.getElementById('editForm').addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target;
const id = form.id.value;
await fetch('/api/subscribers/' + id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: form.name.value,
address: form.address.value,
phone: form.phone.value,
status: form.status.value,
balance: parseFloat(form.balance.value) || 0
})
});
closeModal();
load();
});
document.getElementById('addPaymentForm').addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target;
await fetch('/api/payments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
subscriberId: form.subscriberId.value,
amount: parseFloat(form.amount.value),
date: form.date.value,
comment: form.comment.value
})
});
form.reset();
load();
});
function editSubscriber(id) {
const s = subscribers.find(x => x.id === id);
if (!s) return;
const form = document.getElementById('editForm');
form.id.value = s.id;
form.name.value = s.name;
form.address.value = s.address;
form.phone.value = s.phone;
form.status.value = s.status;
form.balance.value = s.balance || 0;
document.getElementById('editModal').classList.add('on');
}
function closeModal() {
document.getElementById('editModal').classList.remove('on');
}
async function deleteSubscriber(id) {
if (!confirm('Удалить этого абонента? Это действие нельзя отменить.')) return;
await fetch('/api/subscribers/' + id, { method: 'DELETE' });
load();
}
function showTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('[id$="-tab"]').forEach(t => t.style.display = 'none');
event.target.classList.add('active');
document.getElementById(tab + '-tab').style.display = 'block';
}
function exportSubscribers() {
const head = ['Лиц.счёт', 'ФИО', 'Адрес', 'Телефон', 'Баланс', 'Статус', 'Дата подключения'];
const body = subscribers.map(s => [s.accountNumber, s.name, s.address, s.phone, s.balance, s.status, s.createdAt]);
const csv = '\uFEFF' + [head.join(';'), ...body.map(r => r.map(v => '"' + (v ?? '').toString().replace(/"/g, '""') + '"').join(';'))].join('\n');
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }));
a.download = 'abonenty_' + new Date().toISOString().slice(0,10) + '.csv';
a.click();
}
function exportPayments() {
const head = ['Дата', 'Лиц.счёт', 'ФИО', 'Сумма', 'Комментарий'];
const body = payments.map(p => {
const s = subscribers.find(x => x.id == p.subscriberId);
return [p.date, s ? s.accountNumber : '-', s ? s.name : '-', p.amount, p.comment];
});
const csv = '\uFEFF' + [head.join(';'), ...body.map(r => r.map(v => '"' + (v ?? '').toString().replace(/"/g, '""') + '"').join(';'))].join('\n');
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }));
a.download = 'platezhi_' + new Date().toISOString().slice(0,10) + '.csv';
a.click();
}
document.getElementById('paymentForm').date.value = new Date().toISOString().slice(0, 10);
load();
</script>
</body>
</html>