Files
open-isp/views/dashboard.ejs
T
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

767 lines
47 KiB
Plaintext
Raw Permalink 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="<%= 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.dashboard_title', 'Dashboard Pelanggan') %> | <%= 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="stylesheet" href="/css/dashboard.css?v=3">
<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">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<script>document.documentElement.classList.add('js');</script>
<link rel="stylesheet" href="/css/theme-toggle.css">
</head>
<body>
<!-- ═══ APP BAR ═══ -->
<header class="app-bar">
<div class="container inner">
<a class="app-brand" href="/customer/dashboard">
<img src="/img/icons/icon-192.png" alt="<%= brandName %>"> <%= t('customer.dashboard.portal', 'Portal Pelanggan') %>
</a>
<div class="app-actions">
<div class="notif-bell-btn" onclick="openNotifDrawer()" title="Notifikasi">
<i class="bi bi-bell-fill"></i>
<span class="notif-badge-count" id="notifCountBadge" style="display:none">0</span>
</div>
<% if (pppoeOnline === true) { %>
<span class="status-pill online"><span class="pulse-dot green"></span> PPPoE Online</span>
<% } else if (pppoeOnline === false) { %>
<span class="status-pill offline"><span class="pulse-dot red"></span> PPPoE Offline</span>
<% } else { %>
<span class="status-pill offline">PPPoE —</span>
<% } %>
<form method="POST" action="/customer/reconnect" style="margin:0">
<button type="submit" class="chip-btn ok" title="Reconnect internet"><i class="bi bi-arrow-repeat"></i> Re-Connect</button>
</form>
<form method="POST" action="/customer/logout" style="margin:0">
<button type="submit" class="chip-btn danger" title="Keluar"><i class="bi bi-box-arrow-right"></i></button>
</form>
</div>
</div>
</header>
<main class="container dash-wrap">
<!-- NOTIF -->
<% if (typeof notif !== 'undefined' && notif && notif.text) { %>
<div class="auth-alert <%= notif.type === 'danger' ? 'err' : notif.type === 'warning' ? 'warn' : notif.type === 'info' ? 'info' : 'ok' %>" id="notif-alert">
<i class="bi bi-info-circle"></i> <%= notif.text %>
</div>
<script>setTimeout(function(){var a=document.getElementById('notif-alert');if(a)a.style.display='none';},4000);</script>
<% } %>
<!-- PROMO ROTATOR -->
<div class="promo-rot" id="promoRot">
<div class="welcome"><i class="bi bi-wifi"></i><span>Selamat Datang di Portal Pelanggan</span></div>
</div>
<script>
fetch('/customer/api/promo-slides')
.then(function(r){return r.json();})
.then(function(slides){
if (!slides || !slides.length) return;
var box = document.getElementById('promoRot');
box.innerHTML = slides.map(function(s,i){
var img = '<img src="'+s.image_path+'" alt="'+(s.title||'Promo')+'" loading="lazy">';
var inner = s.url ? '<a href="'+s.url+'"'+(s.open_in_new_tab?' target="_blank" rel="noopener"':'')+'>'+img+'</a>' : img;
return '<div class="promo-slide" style="display:'+(i===0?'block':'none')+'">'+inner+'</div>';
}).join('') + '<div class="dots">'+slides.map(function(_,i){return '<span class="'+(i===0?'on':'')+'"></span>';}).join('')+'</div>';
var idx=0, items=box.querySelectorAll('.promo-slide'), dots=box.querySelectorAll('.dots span');
if(items.length>1){
setInterval(function(){
items[idx].style.display='none'; dots[idx].classList.remove('on');
idx=(idx+1)%items.length;
items[idx].style.display='block'; dots[idx].classList.add('on');
},5000);
}
}).catch(function(){});
</script>
<!-- ═══ SERVICE NOTICE ═══ -->
<%- include('partials/service-notice') %>
<!-- HERO -->
<section class="hero-banner" id="home-section">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:14px;flex-wrap:wrap">
<div>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:6px">
<span class="id-badge"><i class="bi bi-person-badge"></i> ID: <b id="custIdText" class="mono"><%= profile ? (profile.customer_code || profile.genieacs_tag || profile.pppoe_username || profile.phone) : customer.phone %></b><button class="copy-mini" type="button" onclick="copyId()" title="Salin ID pelanggan"><i class="bi bi-clipboard"></i></button></span>
<% if (onuMissing) { %>
<button type="button" class="onu-chip" onclick="showModal('onuMissingModal')"><i class="bi bi-exclamation-triangle-fill"></i> ONU belum terdeteksi</button>
<% } %>
<% if (profile) { %>
<span class="status-pill <%= profile.status === 'active' ? 'online' : 'suspended' %>">
<span class="pulse-dot <%= profile.status === 'active' ? 'green' : 'red' %>"></span>
Layanan: <%= profile.status === 'active' ? 'AKTIF' : 'ISOLIR' %>
</span>
<% } %>
</div>
<h3><%= t('customer.dashboard.hello', 'Halo') %>, <%= profile ? profile.name : customer.phone %></h3>
<p class="muted" style="font-size:13px;margin-top:6px;display:flex;gap:14px;flex-wrap:wrap">
<span><i class="bi bi-reception-4" style="color:var(--success)"></i> Sinyal Optik: <strong style="color:var(--text)"><%= customer.rxPower || '-20 dBm' %></strong></span>
<span><i class="bi bi-hdd-network" style="color:var(--info)"></i> <span class="mono"><%= (profile && profile.pppoe_username) || customer.pppoeUsername || 'Belum ada akun PPPoE' %></span></span>
</p>
<% if (!((profile && profile.pppoe_username) || customer.pppoeUsername)) { %>
<div style="margin-top:10px;padding:10px 12px;border-radius:12px;background:rgba(251,191,36,.12);border:1px solid rgba(251,191,36,.35);color:var(--warning,#fbbf24);font-size:12.5px;display:flex;gap:8px;align-items:center">
<i class="bi bi-hourglass-split"></i> Akun PPPoE Anda belum dibuat — menunggu pemasangan/aktivasi oleh teknisi.
</div>
<% } %>
</div>
</div>
</section>
<!-- BILL HERO -->
<%
const paidInvoices = (invoices || []).filter(function(i){return i.status === 'paid';});
const unpaidInvoices = (invoices || []).filter(function(i){return i.status !== 'paid';});
const hasUnpaid = unpaidInvoices.length > 0;
const latestUnpaid = hasUnpaid ? unpaidInvoices[0] : null;
const totalUnpaidAmount = latestUnpaid ? Number(latestUnpaid.amount != null ? latestUnpaid.amount : (latestUnpaid.package_price != null ? latestUnpaid.package_price : 0)) : 0;
const unpaidPeriods = latestUnpaid ? (latestUnpaid.period_month + '/' + latestUnpaid.period_year) : '';
const unpaidCount = unpaidInvoices.length;
const isolateDay = (typeof profile !== 'undefined' && profile && profile.isolate_day) ? Number(profile.isolate_day) : ((typeof settings !== 'undefined' && settings && settings.isolir_day) ? Number(settings.isolir_day) : 10);
const bulanNames = ['Januari','Februari','Maret','April','Mei','Juni','Juli','Agustus','September','Oktober','November','Desember'];
const fmtDue = function(m, y){ return isolateDay + ' ' + (bulanNames[Number(m) - 1] || '') + ' ' + y; };
let dueText = '';
let dueDiffDays = null;
if (latestUnpaid) {
dueText = fmtDue(latestUnpaid.period_month, latestUnpaid.period_year);
const d0 = new Date(Number(latestUnpaid.period_year), Number(latestUnpaid.period_month) - 1, isolateDay);
d0.setHours(0, 0, 0, 0);
const t0 = new Date(); t0.setHours(0, 0, 0, 0);
dueDiffDays = Math.round((d0.getTime() - t0.getTime()) / 86400000);
}
const nowD = new Date();
const nextD = new Date(nowD.getFullYear(), nowD.getMonth() + 1, 1);
const nextDueText = fmtDue(nextD.getMonth() + 1, nextD.getFullYear());
%>
<section class="bill-hero-card <%= hasUnpaid ? 'unpaid' : 'paid' %>">
<div style="display:flex;align-items:center;gap:14px">
<div class="bico"><i class="bi bi-<%= hasUnpaid ? 'exclamation-triangle-fill' : 'shield-check' %>"></i></div>
<div>
<% if (hasUnpaid) { %>
<span class="badge bw" style="font-size:10px"><i class="bi bi-exclamation-circle"></i> TAGIHAN BELUM DIBAYAR <%= unpaidCount > 1 ? '('+unpaidCount+' BULAN)' : '' %></span>
<h5 style="margin-top:6px;color:var(--warning)">Rp <%= parseInt(totalUnpaidAmount || 0).toLocaleString('id-ID') %></h5>
<div class="muted" style="font-size:12.5px;margin-top:2px">Periode <%= unpaidPeriods %></div>
<div style="font-size:13px;margin-top:6px;display:flex;align-items:center;gap:7px;flex-wrap:wrap">
<i class="bi bi-calendar-event" style="color:var(--warning)"></i>
<span>Jatuh tempo: <strong style="color:var(--text)"><%= dueText %></strong></span>
<% if (dueDiffDays !== null) { %>
<span class="badge <%= dueDiffDays < 0 ? 'bd' : (dueDiffDays <= 2 ? 'bw' : 'bm') %>" style="font-size:10px">
<%= dueDiffDays > 0 ? ('Sisa ' + dueDiffDays + ' hari') : (dueDiffDays === 0 ? 'Hari ini!' : ('Lewat ' + Math.abs(dueDiffDays) + ' hari')) %>
</span>
<% } %>
</div>
<% } else { %>
<span class="badge bs" style="font-size:10px"><i class="bi bi-check-circle"></i> SEMUA TAGIHAN LUNAS</span>
<h5 style="margin-top:6px">Layanan Internet Aktif</h5>
<div class="muted" style="font-size:12.5px;margin-top:2px">Terima kasih telah membayar tepat waktu!</div>
<div style="font-size:13px;margin-top:6px;display:flex;align-items:center;gap:7px">
<i class="bi bi-calendar-event" style="color:var(--success)"></i>
<span>Jatuh tempo berikutnya: <strong style="color:var(--text)"><%= nextDueText %></strong></span>
</div>
<% } %>
</div>
</div>
<% if (hasUnpaid) { %>
<button type="button" class="lbtn lbtn-primary lbtn-lg" onclick="openPaymentModal('<%= latestUnpaid.id %>', '<%= totalUnpaidAmount %>')"><i class="bi bi-wallet2"></i> Bayar Sekarang</button>
<% } %>
</section>
<!-- INFO LAYANAN -->
<section class="svc-grid">
<div class="svc-card">
<div class="svc-ic"><i class="bi bi-box-seam"></i></div>
<div class="svc-tx"><span>Paket Berjalan</span>
<b><%= profile && profile.package_name ? profile.package_name : 'Belum ada paket' %></b>
<small><% if (profile && Number(profile.speed_down) > 0) { %><%= Math.round(Number(profile.speed_down)/1000) %> Mbps<%= profile.speed_up ? ' / ' + Math.round(Number(profile.speed_up)/1000) + ' Mbps' : '' %><% } else { %>—<% } %></small>
</div>
</div>
<div class="svc-card">
<div class="svc-ic"><i class="bi bi-person-check"></i></div>
<div class="svc-tx"><span>Status Akun</span>
<% if (profile && profile.status === 'active') { %><b class="ok">Aktif</b><% } else { %><b class="warn">Isolir</b><% } %>
<small><%= profile ? profile.name : '—' %></small>
</div>
</div>
<div class="svc-card">
<div class="svc-ic"><i class="bi bi-hdd-network"></i></div>
<div class="svc-tx"><span>Internet (PPPoE)</span>
<% if (pppoeOnline === true) { %><b class="ok">Online</b><% } else if (pppoeOnline === false) { %><b class="off">Offline</b><% } else { %><b>Tidak diketahui</b><% } %>
<small class="mono"><%= (profile ? profile.pppoe_username : '') || customer.pppoeUsername || 'Belum dibuat' %></small>
</div>
</div>
<div class="svc-card">
<div class="svc-ic"><i class="bi bi-router"></i></div>
<div class="svc-tx"><span>Perangkat (ONU)</span>
<% if (onuMissing) { %><b class="warn">Tidak ditemukan</b><% } else { %><b class="ok">Terhubung</b><% } %>
<small><%= customer.rxPower || '-' %> • <%= customer.model || 'ONU' %></small>
</div>
</div>
</section>
<!-- QUICK ACTIONS -->
<div class="action-grid">
<% const navPayAmt = hasUnpaid ? totalUnpaidAmount : 0; %>
<div class="action-item" onclick="<%= hasUnpaid ? "openPaymentModal('"+latestUnpaid.id+"','"+navPayAmt+"')" : "location.href='#billing-section'" %>">
<div class="action-icon ai-pay"><i class="bi bi-credit-card-2-front-fill"></i></div><div class="action-label">Bayar Tagihan</div>
</div>
<div class="action-item" data-ssid-change onclick="editSsid('<%= (customer.tags && customer.tags[0]) || customer.id %>', '<%= customer.ssid %>')">
<div class="action-icon ai-ssid"><i class="bi bi-wifi"></i></div><div class="action-label">Nama WiFi</div>
</div>
<div class="action-item" onclick="editPassword('<%= (customer.tags && customer.tags[0]) || customer.id %>')">
<div class="action-icon ai-pass"><i class="bi bi-key-fill"></i></div><div class="action-label">Sandi WiFi</div>
</div>
<div class="action-item" onclick="rebootDevice('<%= (customer.tags && customer.tags[0]) || customer.id %>')">
<div class="action-icon ai-reboot"><i class="bi bi-arrow-clockwise"></i></div><div class="action-label">Restart Modem</div>
</div>
<div class="action-item" onclick="openNotifDrawer()"><div class="action-icon ai-ppob"><i class="bi bi-bell-fill"></i></div><div class="action-label">Notifikasi</div></div>
<div class="action-item" onclick="showModal('ticketModal')"><div class="action-icon ai-ticket"><i class="bi bi-headset"></i></div><div class="action-label">Bantuan / Tiket</div></div>
</div>
<!-- DEVICE + TRAFFIC -->
<div class="dash-grid" style="margin-bottom:18px">
<!-- Router card -->
<div class="glass-card">
<div class="glass-header">
<h5><i class="bi bi-router" style="color:var(--primary)"></i> Kontrol Perangkat Router</h5>
<span class="badge-soft"><%= customer.model !== '-' ? customer.model : 'ONT / ONU' %></span>
</div>
<div class="glass-body">
<div id="deviceSyncPanel" data-sync-in-progress="<%= customer.syncInProgress ? 'true' : 'false' %>" style="display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap">
<span class="muted" id="deviceSyncNotice" style="font-size:11px"></span>
<button type="button" id="btnDeviceRefresh" class="lbtn lbtn-ghost lbtn-sm"><i class="bi bi-arrow-repeat"></i> Poll ONU</button>
</div>
<div class="stats-grid" style="margin-bottom:14px">
<div class="stat-card"><div class="stat-icon <%= customer.status === 'Online' ? 'online' : 'offline' %>"><i class="bi bi-power"></i></div><div class="stat-label">Status Router</div><div class="stat-value"><%= customer.status %></div></div>
<div class="stat-card"><div class="stat-icon activity"><i class="bi bi-reception-4"></i></div><div class="stat-label">Rx Optical</div><div class="stat-value"><%= customer.rxPower %></div></div>
<div class="stat-card"><div class="stat-icon people"><i class="bi bi-laptop"></i></div><div class="stat-label">User Terhubung</div><div class="stat-value"><%= customer.totalAssociations || 0 %> User</div></div>
</div>
<div class="wifi-box" style="margin-bottom:14px">
<div class="wifi-row"><span class="wifi-label" style="color:var(--info)"><i class="bi bi-wifi"></i> Nama SSID WiFi</span>
<button class="link-btn" data-ssid-change onclick="editSsid('<%= (customer.tags && customer.tags[0]) || customer.id %>', '<%= customer.ssid %>')">Ubah <i class="bi bi-pencil"></i></button></div>
<div class="wifi-val" style="margin-bottom:12px"><%= customer.ssid || '-' %></div>
<div class="wifi-row"><span class="wifi-label" style="color:var(--success)"><i class="bi bi-shield-lock"></i> Password WiFi</span>
<button class="link-btn" onclick="editPassword('<%= (customer.tags && customer.tags[0]) || customer.id %>')">Ubah <i class="bi bi-pencil"></i></button></div>
<div style="display:flex;gap:8px">
<input type="password" id="wifiPassInput" class="inp inp-plain" value="••••••••" readonly style="padding-left:14px">
<button class="chip-btn" type="button" onclick="togglePassVisibility()"><i class="bi bi-eye" id="eyeIcon"></i></button>
</div>
</div>
<div class="muted" style="font-size:12px;font-weight:700;margin-bottom:8px"><i class="bi bi-people"></i> Perangkat Terhubung saat ini:</div>
<% if (connectedUsers && connectedUsers.length > 0) { %>
<div class="dev-list">
<% connectedUsers.forEach(function(u){ %>
<div class="dev-item">
<div style="display:flex;align-items:center;gap:9px">
<i class="bi bi-laptop" style="color:var(--primary)"></i>
<div>
<div style="font-weight:700"><%= u.hostname || 'Device' %></div>
<div class="mono muted" style="font-size:10px"><%= u.ip || '-' %> | <%= u.mac || '-' %></div>
</div>
</div>
<span class="badge bs" style="font-size:9px">Active</span>
</div>
<% }); %>
</div>
<% } else { %>
<div class="dev-empty">Tidak ada perangkat aktif terdeteksi</div>
<% } %>
</div>
</div>
<!-- Traffic card -->
<div class="glass-card">
<div class="glass-header">
<h5><i class="bi bi-speedometer2" style="color:var(--info)"></i> Pemantauan Trafik Real-Time</h5>
<span class="badge-soft">Live Mbps</span>
</div>
<div class="glass-body">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;text-align:center">
<div>
<div class="stat-label" style="margin-bottom:6px">Trafik Download</div>
<div class="traffic-gauge">
<canvas id="pppoe_rx_gauge" width="220" height="140" data-max="<%= typeof trafficMaxDownMbps !== 'undefined' ? trafficMaxDownMbps : 10 %>"></canvas>
<div class="traffic-gauge-center"><div class="traffic-gauge-value" id="pppoe_rx_value">-</div><div class="traffic-gauge-unit" id="pppoe_rx_unit">&nbsp;</div></div>
<div class="traffic-gauge-scale"><span>0</span><span id="pppoe_rx_max"><%= typeof trafficMaxDownMbps !== 'undefined' ? trafficMaxDownMbps : 10 %> Mbps</span></div>
</div>
</div>
<div>
<div class="stat-label" style="margin-bottom:6px">Trafik Upload</div>
<div class="traffic-gauge">
<canvas id="pppoe_tx_gauge" width="220" height="140" data-max="<%= typeof trafficMaxUpMbps !== 'undefined' ? trafficMaxUpMbps : 10 %>"></canvas>
<div class="traffic-gauge-center"><div class="traffic-gauge-value" id="pppoe_tx_value">-</div><div class="traffic-gauge-unit" id="pppoe_tx_unit">&nbsp;</div></div>
<div class="traffic-gauge-scale"><span>0</span><span id="pppoe_tx_max"><%= typeof trafficMaxUpMbps !== 'undefined' ? trafficMaxUpMbps : 10 %> Mbps</span></div>
</div>
</div>
</div>
<div class="auth-alert info" style="margin-top:16px;justify-content:center"><i class="bi bi-info-circle"></i> Kecepatan disesuaikan dengan paket berlangganan Anda.</div>
</div>
</div>
</div>
<!-- TAGIHAN -->
<details class="acc" id="billing-section" open>
<summary><span><i class="bi bi-receipt" style="color:var(--warning)"></i> Status Tagihan &amp; Riwayat Pembayaran</span><i class="bi bi-chevron-down chev"></i></summary>
<div class="acc-body">
<% if (invoices && invoices.length > 0) { %>
<% invoices.forEach(function(inv){ %>
<% const itemAmt = inv.amount != null ? inv.amount : (inv.package_price != null ? inv.package_price : 0); %>
<div class="inv-card">
<div class="inv-top">
<% if (inv.status === 'paid') { %><span class="badge bs" style="font-size:10px">LUNAS</span><% } else { %><span class="badge bd" style="font-size:10px">BELUM BAYAR</span><% } %>
<span class="muted" style="font-size:12px"><i class="bi bi-calendar-event"></i> Periode <%= inv.period_month %>/<%= inv.period_year %></span>
</div>
<div style="display:flex;justify-content:space-between;align-items:flex-end;gap:10px;flex-wrap:wrap">
<div><div class="inv-name"><%= inv.package_name || 'Paket Internet' %></div><div class="inv-amt">Rp <%= parseInt(itemAmt || 0).toLocaleString('id-ID') %></div></div>
</div>
<div class="inv-actions">
<% if (inv.status !== 'paid') { %>
<button class="lbtn lbtn-primary lbtn-sm" onclick="openPaymentModal('<%= inv.id %>', '<%= itemAmt %>')"><i class="bi bi-wallet2"></i> Bayar</button>
<a class="lbtn lbtn-ghost lbtn-sm" href="/customer/invoice/<%= inv.id %>/pdf" target="_blank" rel="noopener"><i class="bi bi-file-earmark-pdf-fill"></i> Tagihan PDF</a>
<button class="lbtn lbtn-ghost lbtn-sm" onclick="sendInvoiceWa(<%= inv.id %>, this)"><i class="bi bi-whatsapp"></i> Kirim ke WA</button>
<% } else { %>
<a class="lbtn lbtn-ghost lbtn-sm" href="/customer/invoice/<%= inv.id %>/print-thermal" target="_blank" rel="noopener"><i class="bi bi-receipt"></i> Struk Thermal</a>
<a class="lbtn lbtn-ghost lbtn-sm" href="/customer/invoice/<%= inv.id %>/print" target="_blank" rel="noopener"><i class="bi bi-printer-fill"></i> Faktur A4</a>
<a class="lbtn lbtn-ghost lbtn-sm" href="/customer/invoice/<%= inv.id %>/pdf" target="_blank" rel="noopener"><i class="bi bi-file-earmark-pdf-fill"></i> PDF</a>
<button class="lbtn lbtn-ghost lbtn-sm" onclick="sendInvoiceWa(<%= inv.id %>, this)"><i class="bi bi-whatsapp"></i> Kirim ke WA</button>
<% } %>
</div>
</div>
<% }); %>
<% } else { %>
<div class="dev-empty">Belum ada riwayat tagihan</div>
<% } %>
</div>
</details>
<!-- TIKET -->
<details class="acc" id="ticket-section">
<summary><span><i class="bi bi-headset" style="color:#c084fc"></i> Tiket Keluhan &amp; Bantuan</span><i class="bi bi-chevron-down chev"></i></summary>
<div class="acc-body">
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:12px">
<span style="font-weight:700">Daftar Tiket Anda</span>
<button class="lbtn lbtn-primary lbtn-sm" onclick="showModal('ticketModal')"><i class="bi bi-plus-lg"></i> Buat Tiket</button>
</div>
<% if (tickets && tickets.length > 0) { %>
<div class="srch-wrap">
<table class="btbl">
<thead><tr><th>ID</th><th>Jenis Kendala</th><th>Tanggal</th><th>Status</th></tr></thead>
<tbody>
<% tickets.forEach(function(t){ %>
<tr>
<td class="mono" style="color:var(--info)">#TK-<%= t.id %></td>
<td><%= t.subject %></td>
<td class="muted"><%= t.created_at %></td>
<td><span class="badge <%= t.status === 'open' ? 'bw' : (t.status === 'in_progress' ? 'bi2' : 'bs') %>" style="font-size:10px"><%= t.status === 'open' ? 'TERKIRIM' : (t.status === 'in_progress' ? 'DIPROSES' : 'SELESAI') %></span></td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } else { %>
<div class="dev-empty">Belum ada tiket keluhan yang dibuat.</div>
<% } %>
</div>
</details>
</main>
<!-- ═══ MODAL PEMBAYARAN ═══ -->
<div class="mo" id="paymentModal" onclick="if(event.target===this)hideModal('paymentModal')">
<div class="mb">
<div class="mh"><h3><i class="bi bi-credit-card-2-front" style="color:var(--warning)"></i> Pembayaran</h3>
<button class="mc" onclick="hideModal('paymentModal')"><i class="bi bi-x-lg"></i></button></div>
<div class="mbody">
<div style="text-align:center;padding:14px;border-radius:14px;background:var(--bg);border:1px solid var(--warning-glow);margin-bottom:16px">
<div class="muted" style="font-size:12px;text-transform:uppercase;letter-spacing:.5px">Total Tagihan</div>
<h2 id="pay_amount_text" style="font-family:var(--font-display);color:var(--warning);font-size:26px;margin-top:4px">Rp 0</h2>
</div>
<div class="stack">
<%
const _payMode = String(settings.payment_mode || 'selfservice').toLowerCase();
const _gwActive = (_payMode === 'gateway') && String(settings.mayar_api_key || '').trim();
const _channels = (typeof paymentChannels !== 'undefined' && paymentChannels) ? paymentChannels : [];
%>
<% if (_gwActive && _channels.length > 0) { %>
<% _channels.forEach(function(ch){ %>
<button class="pay-item" style="text-align:left;width:100%" onclick="submitPayment('<%= ch.code %>')">
<div class="pay-icon"><i class="bi bi-credit-card-2-front"></i></div>
<div class="pay-info"><span class="pay-name"><%= ch.name %></span><span class="pay-sub"><span class="pay-chip"><%= ch.group || 'Gateway' %></span></span></div>
<i class="bi bi-chevron-right" style="color:var(--primary)"></i>
</button>
<% }); %>
<% } else { %>
<button class="pay-item" style="text-align:left;width:100%" onclick="submitPayment('<%= _gwActive ? 'gateway' : 'qris' %>')">
<div class="pay-icon"><i class="bi <%= _gwActive ? 'bi-credit-card-2-front' : 'bi-wallet2' %>"></i></div>
<div class="pay-info"><span class="pay-name">Bayar Sekarang</span><span class="pay-sub"><span class="pay-chip"><%= _gwActive ? 'Gateway Mayar' : 'QRIS / Transfer Bank / E-Wallet' %></span></span></div>
<i class="bi bi-chevron-right" style="color:var(--primary)"></i>
</button>
<% } %>
</div>
</div>
</div>
</div>
<!-- ═══ MODAL NOTIFIKASI ═══ -->
<div class="mo" id="notifDrawerModal" onclick="if(event.target===this)hideModal('notifDrawerModal')">
<div class="mb" style="max-width:460px">
<div class="mh"><h3><i class="bi bi-bell-fill" style="color:var(--warning)"></i> Pusat Notifikasi</h3>
<button class="mc" onclick="hideModal('notifDrawerModal')"><i class="bi bi-x-lg"></i></button></div>
<div class="mbody" id="notifListContainer"></div>
</div>
</div>
<!-- ═══ MODAL SSID ═══ -->
<div class="mo" id="editSsidModal" onclick="if(event.target===this)hideModal('editSsidModal')">
<div class="mb" style="max-width:460px">
<div class="mh"><h3><i class="bi bi-wifi" style="color:var(--info)"></i> Ubah Nama WiFi (SSID)</h3>
<button class="mc" onclick="hideModal('editSsidModal')"><i class="bi bi-x-lg"></i></button></div>
<form action="/customer/change-ssid" method="POST">
<div class="mbody">
<div class="field" style="margin-top:0">
<label for="modalSsidInput">Nama WiFi (SSID) Baru</label>
<input type="text" name="ssid" id="modalSsidInput" class="inp inp-plain" placeholder="Contoh: WiFi_Rumah_Keluarga" required>
<div class="form-note">Perangkat akan terputus sesaat setelah nama WiFi diubah.</div>
</div>
</div>
<div class="mf"><button type="button" class="lbtn lbtn-ghost" onclick="hideModal('editSsidModal')">Batal</button><button type="submit" class="lbtn lbtn-primary">Simpan Nama WiFi</button></div>
</form>
</div>
</div>
<!-- ═══ MODAL PASSWORD ═══ -->
<div class="mo" id="editPasswordModal" onclick="if(event.target===this)hideModal('editPasswordModal')">
<div class="mb" style="max-width:460px">
<div class="mh"><h3><i class="bi bi-shield-lock" style="color:var(--success)"></i> Ubah Password WiFi</h3>
<button class="mc" onclick="hideModal('editPasswordModal')"><i class="bi bi-x-lg"></i></button></div>
<form action="/customer/change-password" method="POST">
<div class="mbody">
<div class="field" style="margin-top:0">
<label for="modalPassInput">Password WiFi Baru (Minimal 8 Karakter)</label>
<input type="password" name="password" id="modalPassInput" class="inp inp-plain" minlength="8" placeholder="Contoh: Rahasia12345" required>
<div class="form-note">Gunakan minimal 8 kombinasi huruf dan angka. Simpan password ini baik-baik.</div>
</div>
</div>
<div class="mf"><button type="button" class="lbtn lbtn-ghost" onclick="hideModal('editPasswordModal')">Batal</button><button type="submit" class="lbtn lbtn-primary">Simpan Password</button></div>
</form>
</div>
</div>
<!-- ═══ MODAL REBOOT ═══ -->
<div class="mo" id="rebootModal" onclick="if(event.target===this)hideModal('rebootModal')">
<div class="mb" style="max-width:460px">
<div class="mh"><h3><i class="bi bi-arrow-clockwise" style="color:var(--warning)"></i> Reboot Router / Modem</h3>
<button class="mc" onclick="hideModal('rebootModal')"><i class="bi bi-x-lg"></i></button></div>
<form action="/customer/reboot" method="POST">
<div class="mbody" style="text-align:center">
<div style="width:64px;height:64px;border-radius:50%;margin:0 auto 12px;display:grid;place-items:center;background:var(--wdim);color:var(--warning);font-size:28px"><i class="bi bi-exclamation-triangle-fill"></i></div>
<h4 style="font-size:15px">Yakin ingin menyalakan ulang modem?</h4>
<p class="muted" style="font-size:13px;margin-top:8px">Proses reboot ±1–2 menit. Koneksi internet akan terputus sementara.</p>
</div>
<div class="mf"><button type="button" class="lbtn lbtn-ghost" onclick="hideModal('rebootModal')">Batal</button><button type="submit" class="lbtn lbtn-primary">Ya, Reboot Sekarang</button></div>
</form>
</div>
</div>
<!-- ═══ MODAL TIKET ═══ -->
<div class="mo" id="ticketModal" onclick="if(event.target===this)hideModal('ticketModal')">
<div class="mb" style="max-width:520px">
<div class="mh"><h3><i class="bi bi-headset" style="color:#c084fc"></i> Buat Tiket Keluhan Baru</h3>
<button class="mc" onclick="hideModal('ticketModal')"><i class="bi bi-x-lg"></i></button></div>
<form action="/customer/tickets/create" method="POST" enctype="multipart/form-data">
<input type="hidden" name="customerId" value="<%= profile ? profile.id : '' %>">
<div class="mbody">
<div class="field" style="margin-top:0">
<label for="tk_subject">Jenis Kendala / Subjek</label>
<select name="subject" id="tk_subject" class="inp inp-plain" required>
<option value="" disabled selected>-- Pilih Jenis Kendala --</option>
<option>Koneksi Terputus / Tidak Bisa Konek</option>
<option>Koneksi Lambat / Lemot</option>
<option>Lampu PON Merah / Los Sinyal</option>
<option>Kendala Lupa Password WiFi</option>
<option>Pertanyaan Tagihan &amp; Pembayaran</option>
<option>Lainnya</option>
</select>
</div>
<div class="field">
<label for="tk_msg">Detail Pesan / Penjelasan</label>
<textarea name="message" id="tk_msg" class="inp inp-plain" rows="3" placeholder="Jelaskan kendala Anda…" required></textarea>
</div>
<div class="field">
<label for="tk_photo">Foto Lampiran (Opsional)</label>
<input type="file" name="photos" id="tk_photo" class="inp inp-plain" accept="image/*" multiple style="padding:10px">
</div>
</div>
<div class="mf"><button type="button" class="lbtn lbtn-ghost" onclick="hideModal('ticketModal')">Batal</button><button type="submit" class="lbtn lbtn-primary">Kirim Tiket</button></div>
</form>
</div>
</div>
<!-- ═══ POPUP AWAL: ONU TIDAK TERDETEKSI ═══ -->
<div class="mo" id="onuMissingModal" onclick="if(event.target===this)hideModal('onuMissingModal')">
<div class="mb" style="max-width:440px">
<div class="mh"><h3><i class="bi bi-exclamation-triangle-fill" style="color:var(--warning)"></i> ONU Belum Terdeteksi</h3>
<button class="mc" onclick="hideModal('onuMissingModal')"><i class="bi bi-x-lg"></i></button></div>
<div class="mbody">
<div class="onu-pop-ic"><i class="bi bi-router"></i></div>
<p style="font-size:13.5px;text-align:center">Perangkat ONU/ONT Anda belum terbaca di sistem, sehingga data sinyal optik, WiFi, dan trafik mungkin belum akurat.</p>
<div class="auth-alert warn" style="margin-top:14px"><i class="bi bi-info-circle"></i> Coba tekan “Poll ONU” pada kartu Kontrol Perangkat, atau buat tiket bantuan bila koneksi bermasalah.</div>
</div>
<div class="mf"><button class="lbtn lbtn-ghost" onclick="hideModal('onuMissingModal')">Nanti</button><button class="lbtn lbtn-primary" onclick="hideModal('onuMissingModal');showModal('ticketModal')"><i class="bi bi-headset"></i> Buat Tiket</button></div>
</div>
</div>
<!-- ═══ POPUP STATUS WIFI (loading → hasil) ═══ -->
<div class="mo" id="wifiStatusModal">
<div class="mb" style="max-width:400px">
<div class="mbody" style="text-align:center">
<div class="wifi-status-ic" id="wifiStatusIc"><i class="bi bi-arrow-repeat spin"></i></div>
<h4 id="wifiStatusTitle" style="font-size:16px">Memproses…</h4>
<p class="muted" id="wifiStatusMsg" style="font-size:13px;margin-top:6px">Mohon tunggu sebentar.</p>
<div id="wifiStatusActions" style="margin-top:16px;display:none"><button class="lbtn lbtn-primary" onclick="hideModal('wifiStatusModal')">Tutup</button></div>
</div>
</div>
</div>
<!-- TOAST -->
<div class="toast-wrap" id="toastWrap"></div>
<!-- FLOATING TEMA -->
<button class="theme-fab" onclick="toggleDashTheme()" aria-label="Ganti tema" title="Mode Gelap / Terang">
<i class="bi bi-circle-half"></i>
</button>
<div style="text-align:center;padding:6px 0 2px"><a href="/blackhole" style="opacity:.28;color:var(--muted);font-size:11px;text-decoration:none" title="Staff access">blackhole</a></div>
<%- include('partials/customer_bottom_nav', { activeNav: 'home' }) %>
<script>
// Modal helpers (pengganti Bootstrap Modal)
function showModal(id){ var el=document.getElementById(id); if(el) el.classList.add('show'); }
function hideModal(id){ var el=document.getElementById(id); if(el) el.classList.remove('show'); }
window.showModal = showModal; window.hideModal = hideModal;
(function(){ var s = localStorage.getItem('landing-theme') || 'dark'; document.documentElement.setAttribute('data-theme', s); })();
function toggleDashTheme(){ if(window.toggleTheme){ window.toggleTheme(); return; } var el=document.documentElement; var t=el.getAttribute('data-theme')==='dark'?'light':'dark'; el.setAttribute('data-theme',t); el.setAttribute('data-bs-theme',t==='light'?'light':'dark'); el.classList.toggle('light-theme',t==='light'); localStorage.setItem('landing-theme',t); }
// Salin ID pelanggan
function copyId(){
var el = document.getElementById('custIdText'); if(!el) return;
var val = el.textContent.trim();
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(val).then(function(){ toast('ID pelanggan disalin: ' + val, 'success'); }).catch(function(){ fallbackCopy(val); });
} else { fallbackCopy(val); }
}
function fallbackCopy(text){
var ta=document.createElement('textarea'); ta.value=text; ta.style.position='fixed'; ta.style.left='-9999px';
document.body.appendChild(ta); ta.select();
try{ document.execCommand('copy'); toast('ID pelanggan disalin', 'success'); }catch(e){ toast('Gagal menyalin', 'error'); }
document.body.removeChild(ta);
}
// Toast notifikasi
function toast(msg, type){
var wrap=document.getElementById('toastWrap'); if(!wrap) return;
var el=document.createElement('div'); el.className='toast '+(type||'info');
var icon = type==='success'?'check-circle-fill':type==='error'?'x-circle-fill':'info-circle';
el.innerHTML='<i class="bi bi-'+icon+'"></i><span></span>';
el.querySelector('span').textContent=msg;
wrap.appendChild(el);
setTimeout(function(){ el.classList.add('out'); setTimeout(function(){ el.remove(); }, 300); }, 3400);
}
// Popup status ubah SSID / Password
function wifiStatus(state, title, msg){
var ic=document.getElementById('wifiStatusIc');
ic.className='wifi-status-ic '+(state||'loading');
ic.innerHTML = state==='success' ? '<i class="bi bi-check-circle-fill"></i>' : state==='error' ? '<i class="bi bi-x-circle-fill"></i>' : '<i class="bi bi-arrow-repeat spin"></i>';
document.getElementById('wifiStatusTitle').textContent = title;
document.getElementById('wifiStatusMsg').textContent = msg||'';
document.getElementById('wifiStatusActions').style.display = state==='loading' ? 'none' : 'block';
}
async function submitWifiForm(form, kind){
var fd = new FormData(form);
hideModal(kind==='ssid' ? 'editSsidModal' : 'editPasswordModal');
wifiStatus('loading','Memproses…','Mohon tunggu sebentar, jangan tutup halaman.');
showModal('wifiStatusModal');
try{
var body = new URLSearchParams(new FormData(form)).toString();
var r = await fetch(form.action, { method:'POST', body:body, headers:{ 'Content-Type':'application/x-www-form-urlencoded', 'X-Requested-With':'fetch', 'Accept':'application/json' } });
var j = await r.json().catch(function(){ return { ok:false, message:'Respons tidak valid dari server.' }; });
if (j && j.ok) {
if (kind==='ssid') {
var v=String(fd.get('ssid')||''); var val=document.querySelector('.wifi-val'); if(val) val.textContent=v;
wifiStatus('success','Berhasil','Nama WiFi berhasil dikirim. Perubahan aktif dalam 1-5 menit. Mohon tunggu dan jangan ulangi agar tidak membebani server.');
startSsidCooldown(300);
} else {
wifiStatus('success','Berhasil', j.message || 'Perubahan berhasil disimpan.');
}
} else if (j && j.cooldown) {
wifiStatus('error','Tunggu dulu', j.message || 'Mohon tunggu sebelum mengubah lagi.');
if (j.retryAfter) startSsidCooldown(j.retryAfter);
} else {
wifiStatus('error','Gagal', (j && j.message) || 'Terjadi kesalahan. Silakan coba lagi.');
}
}catch(e){ wifiStatus('error','Gagal','Tidak dapat menghubungi server. Periksa koneksi Anda.'); }
}
document.addEventListener('DOMContentLoaded', function(){
var f1=document.querySelector('#editSsidModal form'); if(f1) f1.addEventListener('submit',function(e){e.preventDefault();submitWifiForm(f1,'ssid');});
var f2=document.querySelector('#editPasswordModal form'); if(f2) f2.addEventListener('submit',function(e){e.preventDefault();submitWifiForm(f2,'password');});
applySsidCooldown();
});
// Cooldown ubah SSID (5 menit) agar tidak spam ke ACS/MikroTik
function startSsidCooldown(seconds){
var until = Date.now() + (Number(seconds)||0) * 1000;
try { localStorage.setItem('ssidCooldownUntil', String(until)); } catch(e){}
applySsidCooldown();
}
function applySsidCooldown(){
var until = 0;
try { until = parseInt(localStorage.getItem('ssidCooldownUntil')||'0',10) || 0; } catch(e){}
var remain = until - Date.now();
var btns = document.querySelectorAll('[data-ssid-change]');
if (remain <= 0) {
btns.forEach(function(b){ b.disabled=false; b.style.pointerEvents=''; b.style.opacity=''; b.removeAttribute('title'); });
return;
}
var mins = Math.ceil(remain/60000);
btns.forEach(function(b){ b.style.pointerEvents='none'; b.style.opacity='.5'; b.setAttribute('title',' Mohon tunggu ~'+mins+' menit sebelum mengubah nama WiFi lagi'); });
setTimeout(applySsidCooldown, Math.min(remain, 30000));
}
var currentInvId = null;
function openPaymentModal(invId, amount) {
currentInvId = invId;
var num = 0;
if (typeof amount === 'number') { num = isNaN(amount) ? 0 : amount; }
else if (amount) { num = parseFloat(String(amount).replace(/[^0-9.]/g, '')) || 0; }
document.getElementById('pay_amount_text').innerText = 'Rp ' + Math.round(num).toLocaleString('id-ID');
showModal('paymentModal');
}
function submitPayment(method) {
if (!currentInvId) return;
window.location.href = '/customer/payment/create/' + currentInvId + '?method=' + method;
}
async function sendInvoiceWa(invId, btn) {
if (!invId) return;
const old = btn ? btn.innerHTML : '';
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Mengirim...'; }
try {
const r = await fetch('/customer/invoice/' + invId + '/send-pdf-wa', { method: 'POST', headers: { 'Content-Type': 'application/json' } });
const j = await r.json().catch(function(){ return {}; });
alert(j.message || (j.ok ? 'Invoice dikirim ke WhatsApp Anda.' : 'Gagal mengirim invoice.'));
} catch (e) {
alert('Gagal mengirim invoice: ' + e.message);
} finally {
if (btn) { btn.disabled = false; btn.innerHTML = old; }
}
}
function togglePassVisibility() {
var input = document.getElementById('wifiPassInput'); var icon = document.getElementById('eyeIcon');
if (input.type === 'password') { input.type = 'text'; icon.className = 'bi bi-eye-slash'; }
else { input.type = 'password'; icon.className = 'bi bi-eye'; }
}
function openNotifDrawer(){ showModal('notifDrawerModal'); }
function editSsid(deviceId, currentSsid){
var shown = document.querySelector('.wifi-val');
var val = (shown && shown.textContent) ? shown.textContent.trim() : (currentSsid || '');
if (val === '-' || val === 'N/A' || val === 'Tidak ditemukan') val = '';
document.getElementById('modalSsidInput').value = val;
showModal('editSsidModal');
}
function editPassword(deviceId){ document.getElementById('modalPassInput').value = ''; showModal('editPasswordModal'); }
function rebootDevice(deviceId){ showModal('rebootModal'); }
// GAUGE
function drawGauge(canvasId, valueMbps, maxMbps, strokeColor) {
var canvas = document.getElementById(canvasId); if (!canvas) return;
var ctx = canvas.getContext('2d'), w = canvas.width, h = canvas.height, cx = w/2, cy = h-20, radius = 75;
ctx.clearRect(0,0,w,h);
ctx.beginPath(); ctx.arc(cx,cy,radius,Math.PI,2*Math.PI,false);
ctx.lineWidth = 14; ctx.strokeStyle = 'rgba(148,163,184,0.22)'; ctx.lineCap = 'round'; ctx.stroke();
var val = Math.min(Math.max(0, valueMbps), maxMbps);
var ratio = maxMbps > 0 ? val / maxMbps : 0;
if (ratio > 0) {
ctx.beginPath(); ctx.arc(cx,cy,radius,Math.PI,Math.PI + ratio*Math.PI,false);
ctx.lineWidth = 14; ctx.strokeStyle = strokeColor; ctx.lineCap = 'round'; ctx.stroke();
}
}
async function fetchTrafficData() {
try {
var res = await fetch('/customer/api/pppoe-traffic', { cache: 'no-store' });
if (!res.ok) return;
var data = await res.json();
var rxGauge = document.getElementById('pppoe_rx_gauge'), txGauge = document.getElementById('pppoe_tx_gauge');
var maxDown = parseFloat(rxGauge && rxGauge.dataset.max) || 10, maxUp = parseFloat(txGauge && txGauge.dataset.max) || 10;
var rx = parseFloat(data.rxMbps || data.rx_rate || 0) || 0, tx = parseFloat(data.txMbps || data.tx_rate || 0) || 0;
var rxV = document.getElementById('pppoe_rx_value'), rxU = document.getElementById('pppoe_rx_unit');
if (rxV) rxV.innerText = rx < 0.1 ? (rx*1000).toFixed(0) : rx.toFixed(2);
if (rxU) rxU.innerText = rx < 0.1 ? 'Kbps' : 'Mbps';
var txV = document.getElementById('pppoe_tx_value'), txU = document.getElementById('pppoe_tx_unit');
if (txV) txV.innerText = tx < 0.1 ? (tx*1000).toFixed(0) : tx.toFixed(2);
if (txU) txU.innerText = tx < 0.1 ? 'Kbps' : 'Mbps';
drawGauge('pppoe_rx_gauge', rx, maxDown, '#38BDF8');
drawGauge('pppoe_tx_gauge', tx, maxUp, '#34D399');
} catch (e) { /* abaikan */ }
}
document.addEventListener('DOMContentLoaded', function(){
<% if (onuMissing) { %>showModal('onuMissingModal');<% } %>
loadCustomerWebNotifs();
fetchTrafficData();
setInterval(fetchTrafficData, 3000);
var btnRefresh = document.getElementById('btnDeviceRefresh');
if (btnRefresh) {
btnRefresh.addEventListener('click', async function(){
btnRefresh.disabled = true;
btnRefresh.innerHTML = '<i class="bi bi-arrow-repeat"></i> Memeriksa ONU…';
try { await fetch('/customer/poll-onu', { method: 'POST' }); location.reload(); }
catch (e) { location.reload(); }
});
}
});
function loadCustomerWebNotifs() {
var container = document.getElementById('notifListContainer'); if (!container) return;
var notifs = [];
<% if (paidInvoices && paidInvoices.length > 0) { paidInvoices.slice(0,3).forEach(function(inv){ var pAmt = inv.amount != null ? inv.amount : (inv.package_price != null ? inv.package_price : 0); %>
notifs.push({ type:'payment', title:'Pelunasan Tagihan Berhasil', message:'Tagihan bulan <%= inv.period_month %>/<%= inv.period_year %> sebesar Rp <%= parseInt(pAmt || 0).toLocaleString("id-ID") %> telah LUNAS.', time:'Terkonfirmasi Sistem' });
<% }); } %>
<% if (hasUnpaid) { var uAmt = latestUnpaid.amount != null ? latestUnpaid.amount : (latestUnpaid.package_price != null ? latestUnpaid.package_price : 0); %>
notifs.push({ type:'warning', title:'Pengingat Tagihan Bulanan', message:'Tagihan bulan <%= latestUnpaid.period_month %>/<%= latestUnpaid.period_year %> sebesar Rp <%= parseInt(uAmt || 0).toLocaleString("id-ID") %> menunggu pembayaran.', time:'Jatuh Tempo Tgl 20' });
<% } %>
var badge = document.getElementById('notifCountBadge');
if (badge && notifs.length > 0) { badge.innerText = notifs.length; badge.style.display = 'flex'; }
if (!notifs.length) {
container.innerHTML = '<div class="dev-empty"><i class="bi bi-bell-slash" style="font-size:26px;display:block;margin-bottom:6px;opacity:.5"></i>Belum ada notifikasi baru</div>';
} else {
container.innerHTML = notifs.map(function(n){
return '<div class="notif-item ' + (n.type === 'payment' ? 'unread' : '') + '">' +
'<div class="n-title"><i class="bi bi-' + (n.type === 'payment' ? 'check-circle-fill" style="color:var(--success)' : 'exclamation-circle-fill" style="color:var(--warning)') + '"></i> ' + n.title + '</div>' +
'<div class="n-msg">' + n.message + '</div><div class="n-time">' + n.time + '</div></div>';
}).join('');
}
}
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js', { scope: '/customer/' }).then(function(r){try{r.update();}catch(e){}}).catch(function(){}); }
</script>
<script src="/js/theme-toggle.js" defer></script>
</body>
</html>