From d0c0e8c76bfa0c2dd465e694d22e36705eea47b4 Mon Sep 17 00:00:00 2001 From: Adil Date: Fri, 14 Aug 2026 10:25:51 +0000 Subject: [PATCH] =?UTF-8?q?v3:=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=B4=D0=BE=D0=BB=D0=B6=D0=BD=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2=20+=20=D0=B0=D0=B1=D0=BE=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D1=81=D0=BA=D0=B0=D1=8F=20=D0=BF=D0=BB=D0=B0=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vibe42-run.log | 4 ++ .vibe42-run.pid | 1 + index.html | 141 ++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 10 ++++ 4 files changed, 156 insertions(+) create mode 100644 .vibe42-run.log create mode 100644 .vibe42-run.pid create mode 100644 package-lock.json diff --git a/.vibe42-run.log b/.vibe42-run.log new file mode 100644 index 0000000..42cac69 --- /dev/null +++ b/.vibe42-run.log @@ -0,0 +1,4 @@ +--- run Fri Aug 14 10:08:27 AM UTC 2026 :: npm start --silent + +up to date in 401ms +Сервер запущен на порту 3000 diff --git a/.vibe42-run.pid b/.vibe42-run.pid new file mode 100644 index 0000000..8cf5c1a --- /dev/null +++ b/.vibe42-run.pid @@ -0,0 +1 @@ +86 diff --git a/index.html b/index.html index 0c4283b..4b001fb 100644 --- a/index.html +++ b/index.html @@ -61,6 +61,8 @@ th{font-weight:600;background:var(--gray-100)}
+ +
@@ -128,6 +130,42 @@ th{font-weight:600;background:var(--gray-100)} + + + + @@ -189,6 +228,7 @@ function renderSubscribers() { ${s.status === 'active' ? 'Активен' : s.status} + @@ -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 = 'Должников нет 🎉'; + return; + } + tbody.innerHTML = debtors.map(s => ` + + ${s.accountNumber} + ${s.name} + ${s.address} + ${s.phone} + ${s.balance.toFixed(2)} ₸ + ${s.status === 'blocked' ? 'Заблокирован' : 'Должник'} + + + + + + `).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() { diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9aa3373 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "starlink-subscribers", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "starlink-subscribers" + } + } +}