Files
open-isp/views/public_check_billing.ejs
oxpoe eaf05f2811 OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana
Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor,
admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS.

Kredit & lisensi (ISC):
- Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak
  atas base kode dan repo yang bermanfaat.
- Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder.
- Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
2026-10-09 05:51:21 +07:00

227 lines
15 KiB
Plaintext

<!DOCTYPE html>
<html lang="<%= lang || 'id' %>" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title><%= t('customer.check_billing.title', 'Cek Tagihan') %> | <%= settings.company_header || 'OPEN-ISP' %></title>
<meta name="theme-color" content="#0A0D14">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="/vendor/bootstrap-icons/bootstrap-icons.min.css">
<link rel="stylesheet" href="/css/landing.css?v=4">
<link rel="stylesheet" href="/css/auth.css?v=4">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/img/icons/icon-192.png">
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon.png">
<script>document.documentElement.classList.add('js');</script>
</head>
<body>
<header class="site-header">
<div class="container inner">
<a class="brand" href="/" aria-label="<%= t('customer.aria_home', 'Beranda') %>">
<img class="bmark" src="<%= brandLogo %>" alt="<%= brandName %>"><span class="bname"><%= brandName %></span>
</a>
<div class="header-actions" style="margin-left:auto;display:flex;align-items:center;gap:6px">
<button class="icon-btn" onclick="toggleTheme()" aria-label="<%= t('customer.aria_theme', 'Ganti tema') %>"><i class="bi bi-moon-stars-fill"></i></button>
<% (typeof availableLangs !== 'undefined' && availableLangs ? availableLangs : ['id','en']).forEach(function(L){ %>
<a href="/lang/<%= L %>" class="lbtn lbtn-ghost lbtn-sm" style="padding:4px 8px;<%= (lang === L) ? 'font-weight:800;color:var(--primary);opacity:1' : 'opacity:.65' %>"><%= String(L).toUpperCase() %></a>
<% }); %>
<a class="lbtn lbtn-ghost lbtn-sm" href="/customer/login"><i class="bi bi-box-arrow-in-right"></i> <%= t('customer.header_login', 'Login') %></a>
</div>
</div>
</header>
<main class="bill-wrap">
<div class="bill-card reveal">
<div class="bill-head">
<div>
<h1><i class="bi bi-receipt" style="color:var(--primary)"></i> <%= t('customer.check_billing.title', 'Cek Tagihan') %></h1>
<div class="sub"><%= settings.company_header || 'OPEN-ISP' %> — <%= t('customer.check_billing.subtitle', 'tanpa perlu login') %></div>
</div>
</div>
<div class="bill-body">
<form method="GET" action="/customer/check-billing">
<div class="search-row">
<div class="field">
<label for="q"><i class="bi bi-search"></i> <%= t('customer.check_billing.search_label', 'Cari pelanggan') %></label>
<div class="wrap"><i class="bi bi-search lead-ic"></i>
<input class="inp" id="q" name="q" value="<%= query || '' %>" placeholder="<%= t('customer.check_billing.search_ph', 'Nama / No. WA / username PPPoE / ID') %>" required></div>
</div>
<button class="lbtn lbtn-primary" type="submit" style="height:46px"><i class="bi bi-search"></i> <%= t('customer.check_billing.search_btn', 'Cek') %></button>
</div>
<div class="form-note"><%= t('customer.check_billing.search_note', 'Bisa pakai: Nama pelanggan, Nomor WhatsApp, PPPoE Username, GenieACS Tag, atau ID pelanggan.') %></div>
</form>
<% if (error) { %><div class="auth-alert err" style="margin-top:16px"><i class="bi bi-exclamation-triangle"></i> <%= error %></div><% } %>
<% if (info) { %><div class="auth-alert ok" style="margin-top:16px"><i class="bi bi-check-circle"></i> <%= info %></div><% } %>
<% if (query && !customer && (!matches || matches.length === 0)) { %>
<div class="auth-alert warn" style="margin-top:16px"><i class="bi bi-info-circle"></i> <%= t('customer.check_billing.not_found', 'Pelanggan tidak ditemukan. Pastikan input sudah benar.') %></div>
<% } %>
<% if (!customer && matches && matches.length > 0) { %>
<div class="sec-title"><i class="bi bi-people"></i> <%= t('customer.check_billing.results', 'Hasil Pencarian') %></div>
<div class="bill-sub" style="margin-bottom:10px"><%= t('customer.check_billing.results_hint', 'Pilih pelanggan untuk melihat detail tagihan') %></div>
<div class="srch-wrap">
<table class="btbl">
<thead><tr><th><%= t('customer.check_billing.col_customer', 'Pelanggan') %></th><th class="num"><%= t('customer.check_billing.unpaid', 'Belum Bayar') %></th><th class="num"><%= t('customer.check_billing.col_total', 'Total') %></th><th></th></tr></thead>
<tbody>
<% matches.forEach(function(m){ %>
<tr>
<td>
<div style="font-weight:700"><%= m.customer_name %></div>
<div class="bill-sub">ID: <%= m.customer_id %><% if (m.customer_phone) { %> • WA: <%= m.customer_phone %><% } %></div>
</td>
<td class="num" style="font-weight:700"><%= Number(m.unpaid_count || 0) %></td>
<td class="num" style="font-weight:700">Rp <%= Number(m.total_amount || 0).toLocaleString('id-ID') %></td>
<td style="text-align:right"><a class="lbtn lbtn-ghost lbtn-sm" href="/customer/check-billing?q=<%= encodeURIComponent(String(m.customer_id)) %>"><%= t('customer.check_billing.view', 'Lihat') %></a></td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } %>
<% if (customer) { %>
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap;margin-top:18px">
<div>
<div style="font-weight:700;font-size:15px"><%= customer.name %></div>
<div class="bill-sub">ID: <%= customer.id %><% if (customer.phone) { %> • WA: <%= customer.phone %><% } %><% if (customer.package_name) { %> • Paket: <%= customer.package_name %><% } %></div>
</div>
<div>
<% const unpaidCount = unpaidInvoices ? unpaidInvoices.length : 0; %>
<% if (unpaidCount > 0) { %>
<span class="badge bd"><i class="bi bi-exclamation-circle"></i> <%= unpaidCount %> <%= t('customer.check_billing.unpaid', 'Belum Bayar') %></span>
<% } else { %>
<span class="badge bs"><i class="bi bi-check-circle"></i> <%= t('customer.check_billing.paid', 'Lunas') %></span>
<% } %>
</div>
</div>
<% const monthNames = ['','Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agu','Sep','Okt','Nov','Des']; %>
<% if (!unpaidInvoices || unpaidInvoices.length === 0) { %>
<div class="auth-alert ok" style="margin-top:16px"><i class="bi bi-check-circle"></i> <%= t('customer.check_billing.all_paid', 'Semua tagihan sudah lunas.') %></div>
<% } else { %>
<div class="sec-title"><i class="bi bi-hourglass-split"></i> <%= t('customer.check_billing.unpaid_bills', 'Tagihan Belum Dibayar') %></div>
<div class="srch-wrap">
<table class="btbl">
<thead><tr><th><%= t('customer.check_billing.col_period', 'Periode') %></th><th class="num"><%= t('customer.check_billing.col_amount', 'Nominal') %></th><th class="num" style="width:70px"><%= t('customer.check_billing.col_select', 'Pilih') %></th></tr></thead>
<tbody>
<% unpaidInvoices.forEach(function(inv, idx){ %>
<tr>
<td>
<div style="font-weight:700"><%= monthNames[Number(inv.period_month)] %> <%= inv.period_year %></div>
<div class="bill-sub">INV-<%= inv.id %></div>
</td>
<td class="num" style="font-weight:700">Rp <%= Number(inv.amount || 0).toLocaleString('id-ID') %></td>
<td class="num">
<input class="inv-radio" type="radio" name="inv_select" value="<%= inv.id %>"
data-inv-id="<%= inv.id %>" data-amount="<%= Number(inv.amount || 0) %>"
data-token="<%= (invoiceTokens && invoiceTokens[String(inv.id)]) ? invoiceTokens[String(inv.id)] : '' %>"
<%= idx === 0 ? 'checked' : '' %> <%= idx === 0 ? 'required' : '' %>>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<div style="margin-top:18px;padding-top:16px;border-top:1px solid var(--border)">
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:12px">
<div class="sec-title" style="margin:0"><i class="bi bi-credit-card"></i> <%= t('customer.check_billing.payment', 'Pembayaran') %></div>
<div class="bill-sub"><span id="pay_inv_text"></span> <span id="pay_amount_text"></span></div>
</div>
<form id="payForm" method="POST" action="/customer/public/payment/create/0" class="stack">
<input type="hidden" name="token" id="pay_token" value="">
<input type="hidden" name="method" id="pay_method" value="">
<input type="hidden" name="method" value="QRIS_STATIC">
<div class="pay-grid">
<div class="pay-item" style="cursor:default">
<div class="pay-icon"><i class="bi bi-wallet2"></i></div>
<div class="pay-info">
<span class="pay-name"><%= t('customer.check_billing.pay_method', 'QRIS / Transfer Bank / E-Wallet') %></span>
<span class="pay-sub"><span class="pay-chip"><%= t('customer.check_billing.pay_note', 'Metode dipilih di halaman pembayaran') %></span></span>
</div>
</div>
</div>
<label class="chk">
<input type="checkbox" value="1" id="tos_pay" name="tos" required>
<span><%= t('customer.tos_agree', 'Saya setuju dengan') %> <a href="/customer/tos" target="_blank" rel="noopener"><%= t('customer.tos_terms', 'Syarat &amp; Ketentuan (TOS)') %></a></span>
</label>
<button id="payBtn" class="lbtn lbtn-primary" type="submit" disabled><i class="bi bi-credit-card"></i> <%= t('customer.check_billing.pay', 'Bayar') %></button>
</form>
</div>
<script>
(function () {
var invRadios = Array.prototype.slice.call(document.querySelectorAll('.inv-radio'));
var payMethodRadios = Array.prototype.slice.call(document.querySelectorAll('input[name="payment_method"]'));
var payForm = document.getElementById('payForm');
var payToken = document.getElementById('pay_token');
var payMethod = document.getElementById('pay_method');
var payBtn = document.getElementById('payBtn');
var invText = document.getElementById('pay_inv_text');
var amtText = document.getElementById('pay_amount_text');
function updatePaymentForm() {
if (!payForm || !payToken || !payMethod || !payBtn) return;
var selectedInv = invRadios.filter(function(r){ return r.checked; })[0];
if (!selectedInv) { payBtn.disabled = true; return; }
var invId = String(selectedInv.dataset.invId || selectedInv.value || '').trim();
var token = String(selectedInv.dataset.token || '').trim();
var amount = Number(selectedInv.dataset.amount || 0) || 0;
payForm.action = '/customer/public/payment/create/' + encodeURIComponent(invId);
payToken.value = token;
payMethod.value = 'QRIS_STATIC';
if (invText) invText.textContent = invId ? ('INV-' + invId + ' • ') : '';
if (amtText) amtText.textContent = amount ? ('Rp ' + amount.toLocaleString('id-ID')) : '';
payBtn.disabled = !invId || !token;
}
invRadios.forEach(function(r){ r.addEventListener('change', updatePaymentForm); });
payMethodRadios.forEach(function(r){ r.addEventListener('change', updatePaymentForm); });
updatePaymentForm();
})();
</script>
<% } %>
<% const paidInvoices = (invoices || []).filter(function(i){ return i.status === 'paid'; }); %>
<% if (paidInvoices.length > 0) { %>
<div class="sec-title" style="margin-top:22px"><i class="bi bi-clock-history"></i> <%= t('customer.check_billing.history', 'Riwayat (Lunas)') %></div>
<div class="bill-sub" style="margin-bottom:8px"><%= t('customer.check_billing.showing', 'Menampilkan') %> <%= Math.min(paidInvoices.length, 10) %> <%= t('customer.check_billing.data_latest', 'data terbaru') %></div>
<div class="srch-wrap">
<table class="btbl">
<thead><tr><th><%= t('customer.check_billing.col_period', 'Periode') %></th><th class="num"><%= t('customer.check_billing.col_amount', 'Nominal') %></th><th><%= t('customer.check_billing.col_date', 'Tanggal') %></th></tr></thead>
<tbody>
<% paidInvoices.slice(0, 10).forEach(function(inv){ %>
<tr>
<td><div style="font-weight:700"><%= monthNames[Number(inv.period_month)] %> <%= inv.period_year %></div><div class="bill-sub">INV-<%= inv.id %></div></td>
<td class="num">Rp <%= Number(inv.amount || 0).toLocaleString('id-ID') %></td>
<td class="bill-sub"><%= inv.paid_at ? new Date(inv.paid_at).toLocaleString('id-ID') : '-' %></td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } %>
<% } %>
<div class="bill-foot">&copy; <%= new Date().getFullYear() %> <%= settings.company_header || '' %></div>
</div>
</div>
</main>
<script>
(function(){ var s = localStorage.getItem('landing-theme') || 'dark'; document.documentElement.setAttribute('data-theme', s); })();
function toggleTheme(){ var el=document.documentElement; var t=el.getAttribute('data-theme')==='dark'?'light':'dark'; el.setAttribute('data-theme',t); localStorage.setItem('landing-theme',t); }
(function(){ var els=document.querySelectorAll('.reveal'); if(!('IntersectionObserver' in window)){els.forEach(function(e){e.classList.add('in')});return;} var io=new IntersectionObserver(function(en){en.forEach(function(x){if(x.isIntersecting){x.target.classList.add('in');io.unobserve(x.target);}});},{threshold:.1}); els.forEach(function(e){io.observe(e);}); })();
if('serviceWorker' in navigator){ navigator.serviceWorker.register('/sw.js',{scope:'/customer/'}).then(function(r){try{r.update();}catch(e){}}).catch(function(){}); }
</script>
</body>
</html>