v3: блокировка должников + абонентская плата
This commit is contained in:
parent
d5f442d0f3
commit
d0c0e8c76b
4
.vibe42-run.log
Normal file
4
.vibe42-run.log
Normal file
@ -0,0 +1,4 @@
|
||||
--- run Fri Aug 14 10:08:27 AM UTC 2026 :: npm start --silent
|
||||
|
||||
up to date in 401ms
|
||||
Сервер запущен на порту 3000
|
||||
1
.vibe42-run.pid
Normal file
1
.vibe42-run.pid
Normal file
@ -0,0 +1 @@
|
||||
86
|
||||
141
index.html
141
index.html
@ -61,6 +61,8 @@ th{font-weight:600;background:var(--gray-100)}
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="showTab('subscribers')">Абоненты</button>
|
||||
<button class="tab" onclick="showTab('payments')">Платежи</button>
|
||||
<button class="tab" onclick="showTab('debtors')">Должники</button>
|
||||
<button class="tab" onclick="showTab('settings')">Настройки</button>
|
||||
<button class="tab" onclick="showTab('reports')">Отчёты</button>
|
||||
</div>
|
||||
|
||||
@ -128,6 +130,42 @@ th{font-weight:600;background:var(--gray-100)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="debtors-tab" style="display:none">
|
||||
<div class="card">
|
||||
<h2>Должники</h2>
|
||||
<p style="margin-bottom:16px;color:var(--gray-500)">Абоненты с отрицательным балансом или заблокированные</p>
|
||||
<div class="actions" style="margin-bottom:16px">
|
||||
<button class="btn btn-red" onclick="blockAllDebtors()">Заблокировать всех должников</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Лиц. счёт</th>
|
||||
<th>ФИО</th>
|
||||
<th>Адрес</th>
|
||||
<th>Телефон</th>
|
||||
<th>Баланс</th>
|
||||
<th>Статус</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="debtorsTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-tab" style="display:none">
|
||||
<div class="card">
|
||||
<h2>Абонентская плата</h2>
|
||||
<p style="margin-bottom:16px;color:var(--gray-500)">Автоматическое начисление ежемесячной платы</p>
|
||||
<div class="form-row" style="max-width:400px">
|
||||
<input type="number" id="monthlyFee" placeholder="Сумма (₸)" value="5000">
|
||||
<button class="btn" onclick="chargeMonthlyFee()">Начислить всем абонентам</button>
|
||||
</div>
|
||||
<p style="margin-top:12px;color:var(--gray-500);font-size:13px">⚠️ Это спишет указанную сумму со счетов всех активных абонентов</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="reports-tab" style="display:none">
|
||||
<div class="card">
|
||||
<h2>Экспорт данных</h2>
|
||||
@ -135,6 +173,7 @@ th{font-weight:600;background:var(--gray-100)}
|
||||
<div class="actions">
|
||||
<button class="btn" onclick="exportSubscribers()">Скачать абоненты (CSV)</button>
|
||||
<button class="btn" onclick="exportPayments()">Скачать платежи (CSV)</button>
|
||||
<button class="btn" onclick="exportDebtors()">Скачать должники (CSV)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -189,6 +228,7 @@ function renderSubscribers() {
|
||||
<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 ${s.status === 'active' ? 'btn-red' : 'btn-green'}" onclick="toggleBlock(${s.id})">${s.status === 'active' ? 'Заблокировать' : 'Разблокировать'}</button>
|
||||
<button class="btn btn-sm btn-red" onclick="deleteSubscriber(${s.id})">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
@ -308,11 +348,112 @@ async function deleteSubscriber(id) {
|
||||
load();
|
||||
}
|
||||
|
||||
async function toggleBlock(id) {
|
||||
const s = subscribers.find(x => x.id === id);
|
||||
if (!s) return;
|
||||
const newStatus = s.status === 'active' ? 'blocked' : 'active';
|
||||
await fetch('/api/subscribers/' + id, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: newStatus })
|
||||
});
|
||||
load();
|
||||
}
|
||||
|
||||
function renderDebtors() {
|
||||
const debtors = subscribers.filter(s => s.balance < 0 || s.status === 'blocked');
|
||||
const tbody = document.getElementById('debtorsTable');
|
||||
if (debtors.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;padding:40px;color:var(--gray-500)">Должников нет 🎉</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = debtors.map(s => `
|
||||
<tr>
|
||||
<td><strong>${s.accountNumber}</strong></td>
|
||||
<td>${s.name}</td>
|
||||
<td>${s.address}</td>
|
||||
<td>${s.phone}</td>
|
||||
<td class="balance-negative">${s.balance.toFixed(2)} ₸</td>
|
||||
<td><span class="badge badge-red">${s.status === 'blocked' ? 'Заблокирован' : 'Должник'}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-green" onclick="toggleBlock(${s.id})">Разблокировать</button>
|
||||
<button class="btn btn-sm" onclick="editSubscriber(${s.id})">Изменить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function blockAllDebtors() {
|
||||
const debtors = subscribers.filter(s => s.balance < 0 && s.status === 'active');
|
||||
if (debtors.length === 0) {
|
||||
alert('Нет активных должников для блокировки');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Заблокировать ${debtors.length} должников? Это действие можно отменить вручную.`)) return;
|
||||
|
||||
for (const s of debtors) {
|
||||
await fetch('/api/subscribers/' + s.id, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: 'blocked' })
|
||||
});
|
||||
}
|
||||
load();
|
||||
alert(`Заблокировано ${debtors.length} должников`);
|
||||
}
|
||||
|
||||
async function chargeMonthlyFee() {
|
||||
const amount = parseFloat(document.getElementById('monthlyFee').value);
|
||||
if (!amount || amount <= 0) {
|
||||
alert('Введите корректную сумму абонентской платы');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Начислить абонентскую плату ${amount} ₸ всем активным абонентам? Это спишет сумму с их балансов.`)) return;
|
||||
|
||||
const activeSubs = subscribers.filter(s => s.status === 'active');
|
||||
for (const s of activeSubs) {
|
||||
await fetch('/api/subscribers/' + s.id, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ balance: (s.balance || 0) - amount })
|
||||
});
|
||||
}
|
||||
|
||||
// Записать как платеж (отрицательный)
|
||||
for (const s of activeSubs) {
|
||||
await fetch('/api/payments', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
subscriberId: s.id,
|
||||
amount: -amount,
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
comment: 'Абонентская плата'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
load();
|
||||
alert(`Абонентская плата ${amount} ₸ начислена ${activeSubs.length} абонентам`);
|
||||
}
|
||||
|
||||
function exportDebtors() {
|
||||
const debtors = subscribers.filter(s => s.balance < 0 || s.status === 'blocked');
|
||||
const head = ['Лиц.счёт', 'ФИО', 'Адрес', 'Телефон', 'Баланс', 'Статус'];
|
||||
const body = debtors.map(s => [s.accountNumber, s.name, s.address, s.phone, s.balance, s.status]);
|
||||
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 = 'dolzhniki_' + new Date().toISOString().slice(0,10) + '.csv';
|
||||
a.click();
|
||||
}
|
||||
|
||||
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';
|
||||
if (tab === 'debtors') renderDebtors();
|
||||
}
|
||||
|
||||
function exportSubscribers() {
|
||||
|
||||
10
package-lock.json
generated
Normal file
10
package-lock.json
generated
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "starlink-subscribers",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "starlink-subscribers"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user