Please create wpdatachart first.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Portal PBB Desa Sirnasari - Admin Mode</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
<!-- Pustaka untuk Ekspor XLSX -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<!-- Pustaka untuk Ekspor PDF -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.25/jspdf.plugin.autotable.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
body { font-family: 'Plus Jakarta Sans', sans-serif; background-color: #f8fafc; }
.search-gradient { background: linear-gradient(135deg, #059669 0%, #10b981 100%); }
.admin-gradient { background: linear-gradient(135deg, #1e293b 0%, #334155 100%); }
.fade-in { animation: fadeIn 0.3s ease-out forwards; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.spinner { border: 4px solid rgba(16, 185, 129, 0.1); width: 40px; height: 40px; border-radius: 50%; border-left-color: #10B981; animation: spin 0.8s linear infinite; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body class="text-slate-900 overflow-x-hidden">
<!-- NAVIGATION BAR -->
<nav class="fixed top-0 w-full z-[60] px-6 py-4 flex justify-between items-center transition-all" id="mainNav">
<div class="flex items-center gap-2 cursor-pointer" onclick="showPage('search')">
<div class="w-10 h-10 bg-white rounded-xl flex items-center justify-center shadow-lg border border-emerald-50">
<i class="fas fa-landmark text-emerald-600" id="navIcon"></i>
</div>
<span class="text-white font-bold text-sm tracking-tight hidden sm:block" id="navTitle">Desa Sirnasari</span>
</div>
<div class="flex gap-2">
<div id="adminBadge" class="hidden bg-amber-500 text-white px-3 py-1 rounded-lg text-[10px] font-black items-center gap-1 shadow-lg">
<i class="fas fa-user-check"></i> ADMIN MODE
</div>
<button id="cartBtn" onclick="showPage('payment')" class="bg-white text-emerald-700 px-4 py-2 rounded-xl shadow-lg font-bold text-xs flex items-center gap-2 border-2 border-emerald-100 hidden">
<i class="fas fa-shopping-cart"></i> <span id="cartCount">0</span>
</button>
<button id="adminToggleBtn" onclick="toggleAdminPanel()" class="bg-white/20 backdrop-blur-md text-white px-4 py-2 rounded-xl border border-white/30 text-xs font-bold flex items-center gap-2">
<i class="fas fa-user-shield"></i> <span id="adminBtnText">Admin</span>
</button>
</div>
</nav>
<!-- UI PENCARIAN & DASHBOARD -->
<div id="page-search" class="page-container">
<header id="mainHeader" class="search-gradient pt-28 pb-48 px-6 relative overflow-hidden transition-all duration-500">
<div class="max-w-4xl mx-auto text-center relative z-10">
<h1 class="text-4xl md:text-5xl font-black text-white mb-4 tracking-tight" id="headerTitle">Cek Pajak PBB Anda</h1>
<p class="text-emerald-50 opacity-90 max-w-xl mx-auto text-sm md:text-base">Gunakan NOP atau Nama Wajib Pajak untuk melihat rincian tagihan secara presisi.</p>
</div>
<div class="absolute bottom-0 left-0 w-full overflow-hidden leading-[0]">
<svg viewBox="0 0 1200 120" preserveAspectRatio="none" class="relative block w-full h-20 fill-slate-50">
<path d="M321.39,56.44c58-10.79,114.16-30.13,172-41.86,82.39-16.72,168.19-17.73,250.45-.39C823.78,31,906.67,72,985.66,92.83c70.05,18.48,146.53,26.09,214.34,3V120H0V95.8C57.21,103.4,115,106.3,172,97.23,235,87.2,284.18,72.71,321.39,56.44Z"></path>
</svg>
</div>
</header>
<main class="max-w-4xl mx-auto px-6 -mt-32 relative z-20 pb-20">
<!-- Capaian Umum -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div class="bg-white rounded-3xl p-6 shadow-xl border border-emerald-50 flex items-center gap-6">
<div class="relative w-24 h-24 flex-shrink-0">
<svg class="w-full h-full" viewBox="0 0 100 100">
<circle class="text-slate-100 stroke-current" stroke-width="10" fill="transparent" r="40" cx="50" cy="50"></circle>
<circle id="mainProgressCircle" class="text-emerald-500 stroke-current" stroke-width="10" stroke-linecap="round" fill="transparent" r="40" cx="50" cy="50" style="stroke-dasharray: 251.2; stroke-dashoffset: 251.2; transition: 1s ease; transform: rotate(-90deg); transform-origin: 50% 50%;"></circle>
</svg>
<div class="absolute inset-0 flex items-center justify-center">
<span id="mainPercentText" class="text-xl font-black text-slate-800">0%</span>
</div>
</div>
<div>
<p class="text-[10px] font-black text-emerald-600 uppercase tracking-widest mb-1">Capaian Desa</p>
<h2 id="mainRealizedText" class="text-2xl font-black text-slate-800 tracking-tight">Rp 0</h2>
<p class="text-[10px] text-slate-400 font-medium">Target: <span id="mainTargetText">Rp 0</span></p>
</div>
</div>
<div class="bg-slate-800 rounded-3xl p-6 shadow-xl text-white flex items-center gap-5">
<div class="w-14 h-14 bg-emerald-500 rounded-2xl flex items-center justify-center text-2xl"><i class="fas fa-trophy"></i></div>
<div>
<p class="text-[10px] font-bold text-emerald-400 uppercase tracking-widest mb-1">Kolektor Teratas</p>
<h3 id="bestCollectorName" class="text-lg font-black tracking-tight">Menghitung...</h3>
<p class="text-[10px] text-slate-400 font-medium tracking-wide">Capaian terbesar saat ini</p>
</div>
</div>
</div>
<!-- ANALYTICS PANEL (Hanya Admin) -->
<div id="adminStatsArea" class="hidden mb-8 space-y-6 fade-in">
<div class="bg-white rounded-3xl p-8 shadow-xl border border-slate-100">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4 border-b pb-6">
<div>
<h3 class="text-xl font-black text-slate-800">Analisis Capaian Pajak</h3>
<p class="text-xs text-slate-400">Data real-time berdasarkan setoran kolektor</p>
</div>
<div class="flex flex-wrap items-center gap-2">
<input type="month" id="reportMonth" class="text-xs p-2 bg-slate-50 border rounded-lg outline-none font-bold min-w-[140px]">
<div class="flex gap-2">
<button onclick="exportReport('xlsx')" class="bg-emerald-600 text-white px-3 py-2 rounded-lg text-[10px] font-black flex items-center gap-2 hover:bg-emerald-700 transition-all">
<i class="fas fa-file-excel"></i> XLSX
</button>
<button onclick="exportReport('pdf')" class="bg-rose-600 text-white px-3 py-2 rounded-lg text-[10px] font-black flex items-center gap-2 hover:bg-rose-700 transition-all">
<i class="fas fa-file-pdf"></i> PDF
</button>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mt-6">
<div class="space-y-4">
<h4 class="text-[10px] font-black text-slate-400 uppercase tracking-widest border-b pb-2">Target Per Kolektor</h4>
<div id="collectorStatsList" class="space-y-4"></div>
</div>
<div class="md:col-span-2">
<h4 class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-4 text-center">Tren Capaian Bulanan</h4>
<div class="h-48 relative">
<canvas id="trendChart"></canvas>
</div>
<div class="grid grid-cols-2 gap-4 mt-6">
<div class="bg-slate-50 p-4 rounded-2xl border border-slate-100">
<p class="text-[9px] font-bold text-slate-400 uppercase mb-1">Minggu Ini</p>
<p id="weekRealization" class="text-lg font-black text-emerald-600">Rp 0</p>
</div>
<div class="bg-slate-50 p-4 rounded-2xl border border-slate-100">
<p class="text-[9px] font-bold text-slate-400 uppercase mb-1">Bulan Ini</p>
<p id="monthRealization" class="text-lg font-black text-slate-800">Rp 0</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-white/80 backdrop-blur rounded-3xl shadow-2xl p-2 mb-8 border-2 border-white">
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-6 flex items-center pointer-events-none">
<i class="fas fa-search text-emerald-500 text-lg"></i>
</div>
<input type="text" id="searchInput"
class="w-full pl-14 pr-6 py-5 bg-transparent rounded-2xl text-lg font-medium focus:outline-none"
placeholder="Ketik Nama atau NOP...">
</div>
</div>
<div id="resultsArea">
<div id="loadingIndicator" class="text-center py-20 hidden"><div class="spinner mx-auto mb-4"></div></div>
<div id="resultsList" class="space-y-4"></div>
</div>
</main>
</div>
<!-- HALAMAN PEMBAYARAN -->
<div id="page-payment" class="page-container hidden pt-28 pb-20 px-6">
<div class="max-w-4xl mx-auto">
<div class="flex items-center gap-4 mb-8">
<button onclick="showPage('search')" class="w-10 h-10 bg-white rounded-xl shadow-md flex items-center justify-center text-slate-600"><i class="fas fa-arrow-left"></i></button>
<h1 class="text-3xl font-black text-slate-800 tracking-tight">Pembayaran</h1>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="md:col-span-2 space-y-6">
<div class="bg-white p-6 rounded-3xl shadow-sm border border-slate-100">
<h3 class="font-black text-slate-800 mb-4 flex items-center gap-2 text-sm"><i class="fas fa-list-check text-emerald-600"></i> Rincian NOP</h3>
<div id="checkoutList" class="space-y-3"></div>
<div class="mt-6 pt-6 border-t border-dashed border-slate-200 flex justify-between items-center">
<span class="text-xs font-bold text-slate-500">Total <span id="totalNopCount">0</span> NOP</span>
<span class="text-xl font-black text-emerald-600" id="checkoutTotalAmount">Rp 0</span>
</div>
</div>
</div>
<div class="space-y-6">
<div class="bg-white p-6 rounded-3xl shadow-sm border border-slate-100">
<h3 class="font-black text-slate-800 mb-4 text-sm">Metode Pembayaran</h3>
<p class="text-xs text-slate-500 mb-4">Pilih rekening tujuan transfer untuk verifikasi cepat:</p>
<div class="space-y-3">
<label class="block p-4 border-2 border-slate-100 rounded-2xl cursor-pointer hover:border-emerald-500 transition-all group">
<input type="radio" name="collectorOption" value="azmi" class="hidden" onclick="updatePaymentInfo('azmi')">
<p class="text-xs font-black text-slate-800 group-hover:text-emerald-600">Azmi Zakaria</p>
<p class="text-[10px] text-slate-400">Kasi Pemerintahan</p>
</label>
<label class="block p-4 border-2 border-slate-100 rounded-2xl cursor-pointer hover:border-emerald-500 transition-all group">
<input type="radio" name="collectorOption" value="aslam" class="hidden" onclick="updatePaymentInfo('aslam')">
<p class="text-xs font-black text-slate-800 group-hover:text-emerald-600">Aslam Jihadudien</p>
<p class="text-[10px] text-slate-400">Kadus</p>
</label>
</div>
<div id="paymentInfoArea" class="mt-6 bg-emerald-50 p-4 rounded-2xl hidden fade-in">
<p class="text-[10px] font-black text-emerald-800 uppercase mb-2">Info Rekening Mandiri</p>
<div class="flex justify-between items-center mb-1">
<span id="accNumber" class="font-black text-emerald-900 tracking-wider"></span>
<button onclick="copyAcc()" class="text-emerald-600 text-xs"><i class="fas fa-copy"></i></button>
</div>
<p id="accName" class="text-[10px] text-emerald-700"></p>
</div>
</div>
<button onclick="confirmFinalPayment()" class="w-full bg-emerald-600 hover:bg-emerald-700 text-white py-5 rounded-3xl font-black shadow-xl transition-all flex items-center justify-center gap-3">
<i class="fab fa-whatsapp text-xl"></i> KONFIRMASI WA
</button>
</div>
</div>
</div>
</div>
<!-- MODAL VERIFIKASI ADMIN -->
<div id="verifyModal" class="fixed inset-0 z-[100] flex items-center justify-center bg-slate-900/60 backdrop-blur-sm hidden p-6">
<div class="bg-white w-full max-w-md rounded-3xl p-8 shadow-2xl fade-in">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-black">Verifikasi Pembayaran</h3>
<button onclick="toggleModal('verifyModal', false)" class="text-slate-400 hover:text-rose-500"><i class="fas fa-times"></i></button>
</div>
<div class="mb-6 p-4 bg-emerald-50 rounded-2xl border border-emerald-100">
<p class="text-[10px] font-bold text-emerald-600 uppercase mb-1">Data Objek Pajak</p>
<h4 id="vNopName" class="font-black text-slate-800">Nama WP</h4>
<p id="vNopNumber" class="text-xs text-slate-500 font-mono tracking-tight">00000000000000000</p>
<p id="vNopAmount" class="text-sm font-black text-emerald-600 mt-2">Rp 0</p>
</div>
<div class="space-y-4">
<div>
<label class="text-[10px] font-black text-slate-400 uppercase ml-2 mb-1 block">Petugas Penyetor</label>
<select id="vCollectorName" class="w-full p-4 bg-slate-50 border rounded-xl outline-none focus:ring-2 focus:ring-emerald-500 text-sm font-bold">
<option value="Azmi Zakaria">Azmi Zakaria</option>
<option value="Aslam Jihadudien">Aslam Jihadudien</option>
<option value="Feri Firmansyah">Feri Firmansyah</option>
<option value="Heru Khoerudin">Heru Khoerudin</option>
<option value="RT/LEMBAGA">RT/LEMBAGA</option>
<option value="Kolektor Umum">Kolektor Umum</option>
</select>
</div>
<div>
<label class="text-[10px] font-black text-slate-400 uppercase ml-2 mb-1 block">Tanggal Setoran</label>
<input type="date" id="vDepositDate" class="w-full p-4 bg-slate-50 border rounded-xl outline-none focus:ring-2 focus:ring-emerald-500 text-sm font-bold">
</div>
<button onclick="processVerification()" class="w-full bg-emerald-600 text-white py-4 rounded-2xl font-black hover:bg-emerald-700 transition-all shadow-lg flex items-center justify-center gap-2">
<i class="fas fa-check-double"></i> KONFIRMASI LUNAS
</button>
</div>
</div>
</div>
<!-- MODAL LOGIN ADMIN -->
<div id="loginModal" class="fixed inset-0 z-[100] flex items-center justify-center bg-slate-900/60 backdrop-blur-sm hidden p-6">
<div class="bg-white w-full max-w-sm rounded-3xl p-8 text-center fade-in shadow-2xl">
<div class="w-16 h-16 bg-slate-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
<i class="fas fa-lock text-slate-400 text-2xl"></i>
</div>
<h3 class="text-xl font-black mb-2">Panel Kontrol Admin</h3>
<div class="space-y-4">
<input type="password" id="adminPinInput" placeholder="PIN" maxlength="6"
class="w-full p-4 bg-slate-50 border rounded-xl text-center text-2xl font-black tracking-[1em] outline-none">
<button onclick="loginAdmin()" class="w-full bg-slate-800 text-white py-4 rounded-xl font-bold">Buka Akses</button>
</div>
<button onclick="toggleModal('loginModal', false)" class="mt-6 text-slate-400 text-xs font-bold uppercase">Batal</button>
</div>
</div>
<script>
const manualCsvUrl = 'https://docs.google.com/spreadsheets/d/1szsh95Jn2MAiCNBJKdFdGu2Tg3kzQEezl-0_6NJ-Vf0/gviz/tq?tqx=out:csv';
let masterData = [];
let cart = [];
let isAdmin = false;
let selectedCollector = null;
let selectedItemForVerify = null;
let trendChart = null;
document.addEventListener('DOMContentLoaded', () => {
fetchData();
document.getElementById('searchInput').addEventListener('input', handleSearch);
document.getElementById('reportMonth').value = new Date().toISOString().slice(0, 7);
});
function fetchData() {
document.getElementById('loadingIndicator').classList.remove('hidden');
Papa.parse(manualCsvUrl, {
download: true,
header: false,
complete: (results) => {
masterData = results.data.slice(1);
document.getElementById('loadingIndicator').classList.add('hidden');
calculateProgress();
if(isAdmin) updateAdminDashboard();
}
});
}
function calculateProgress() {
let target = 0, realized = 0;
const cols = {};
masterData.forEach(r => {
const nom = parseInt((r[5]||'0').replace(/[^0-9]/g, '')) || 0;
target += nom;
if((r[6]||'').toUpperCase().includes('LUNAS')) {
realized += nom;
const colName = r[7] || 'Tanpa Kolektor';
cols[colName] = (cols[colName] || 0) + nom;
}
});
const pct = target > 0 ? (realized/target*100).toFixed(1) : 0;
document.getElementById('mainPercentText').innerText = pct + '%';
document.getElementById('mainProgressCircle').style.strokeDashoffset = 251.2 - (pct/100 * 251.2);
document.getElementById('mainRealizedText').innerText = formatIDR(realized);
document.getElementById('mainTargetText').innerText = formatIDR(target);
let bestN = "Belum ada", bestV = 0;
Object.entries(cols).forEach(([n, v]) => { if(v > bestV) { bestV = v; bestN = n; } });
document.getElementById('bestCollectorName').innerText = bestN;
}
function handleSearch(e) {
const term = e.target.value.toLowerCase().trim();
const list = document.getElementById('resultsList');
if (term.length < 2) { list.innerHTML = ''; return; }
const filtered = masterData.filter(r => (r[1]||'').includes(term) || (r[2]||'').toLowerCase().includes(term)).slice(0, 15);
renderResults(filtered);
}
function renderResults(data) {
const list = document.getElementById('resultsList');
list.innerHTML = '';
data.forEach(r => {
const nop = r[1],
nama = r[2],
lt = r[3]||'0',
lb = r[4]||'0',
nom = parseInt((r[5]||'0').replace(/[^0-9]/g,''))||0,
status = r[6]||'HUTANG',
kolektor = r[7] || '-',
alamat = r[8]||'-',
tahun = r[9]||'2024';
const isLunas = status.toUpperCase().includes('LUNAS');
const card = document.createElement('div');
card.className = 'bg-white rounded-3xl p-6 border border-slate-100 shadow-sm fade-in overflow-hidden';
let actionBtn = isAdmin
? (!isLunas
? `<button onclick="openVerifyModal('${nop}', '${nama}', ${nom}, '${kolektor}')" class="w-full bg-emerald-600 text-white py-3 rounded-xl text-[10px] font-black mt-4">VERIFIKASI LUNAS</button>`
: `<div class="w-full bg-slate-100 text-slate-400 py-3 rounded-xl text-[10px] font-black text-center mt-4 uppercase">TERVERIFIKASI (${r[10] || '-'})</div>`)
: `<div class="grid grid-cols-2 gap-2 mt-4">
<button onclick="contactCollector('${nop}', '${nama}', ${nom})" class="bg-slate-100 text-slate-600 py-3 rounded-xl text-[10px] font-black"><i class="fab fa-whatsapp"></i> INFO</button>
${!isLunas ? `<button onclick="addToCheckout('${nop}', '${nama}', ${nom})" class="bg-emerald-600 text-white py-3 rounded-xl text-[10px] font-black">BAYAR</button>` : ''}
</div>`;
card.innerHTML = `
<div class="flex flex-col">
<div class="flex justify-between items-start mb-4 border-b border-slate-50 pb-3">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="text-[9px] font-black text-white bg-slate-800 px-2 py-0.5 rounded-md uppercase">${tahun}</span>
<span class="text-[9px] font-bold text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-md tracking-wider">${nop}</span>
</div>
<h3 class="text-xl font-black text-slate-800 leading-tight">${nama}</h3>
<p class="text-[10px] text-slate-400 font-bold uppercase truncate mt-1"><i class="fas fa-map-marker-alt"></i> ${alamat}</p>
</div>
<span class="px-3 py-1.5 rounded-xl text-[10px] font-black uppercase ${isLunas ? 'bg-emerald-500 text-white' : 'bg-rose-500 text-white'}">
${status}
</span>
</div>
<div class="grid grid-cols-2 gap-3 mb-4">
<div class="bg-slate-50 p-3 rounded-2xl border border-slate-100">
<p class="text-[8px] font-black text-slate-400 uppercase mb-1">LT / LB</p>
<p class="text-xs font-black text-slate-700">${lt} / ${lb} m²</p>
</div>
<div class="bg-slate-50 p-3 rounded-2xl border border-slate-100">
<p class="text-[8px] font-black text-slate-400 uppercase mb-1">Nominal Pajak</p>
<p class="text-xs font-black text-emerald-600">${formatIDR(nom)}</p>
</div>
<div class="bg-slate-50 p-3 rounded-2xl border border-slate-100 col-span-2">
<p class="text-[8px] font-black text-slate-400 uppercase mb-1">Kolektor Terdaftar</p>
<p class="text-xs font-black ${kolektor !== '-' ? 'text-emerald-700' : 'text-slate-400'}">${kolektor}</p>
</div>
</div>
${actionBtn}
</div>
`;
list.appendChild(card);
});
}
function updateAdminDashboard() {
const colStats = {};
const weeklyData = [0, 0, 0, 0];
const currentMonth = new Date().getMonth();
let monthTotal = 0;
masterData.forEach(r => {
const nom = parseInt((r[5]||'0').replace(/[^0-9]/g, '')) || 0;
const status = (r[6]||'').toUpperCase();
const colName = r[7] || 'Umum';
if(!colStats[colName]) colStats[colName] = { target: 0, real: 0 };
colStats[colName].target += nom;
if (status.includes('LUNAS')) {
colStats[colName].real += nom;
const dateStr = r[10];
if(dateStr) {
const d = new Date(dateStr);
if(!isNaN(d) && d.getMonth() === currentMonth) {
monthTotal += nom;
const weekIdx = Math.min(3, Math.floor(d.getDate() / 8));
weeklyData[weekIdx] += nom;
}
}
}
});
const list = document.getElementById('collectorStatsList');
list.innerHTML = '';
Object.entries(colStats).forEach(([name, data]) => {
const pct = data.target > 0 ? (data.real/data.target*100).toFixed(0) : 0;
const div = document.createElement('div');
div.innerHTML = `
<div class="flex justify-between text-[9px] mb-1.5">
<span class="font-black text-slate-700">${name}</span>
<span class="font-black text-emerald-600">${pct}%</span>
</div>
<div class="w-full bg-slate-100 h-2 rounded-full overflow-hidden">
<div class="bg-emerald-500 h-full transition-all duration-1000" style="width: ${pct}%"></div>
</div>
<div class="flex justify-between mt-1 text-[8px] text-slate-400 font-bold">
<span>Real: ${formatIDR(data.real)}</span>
<span>Target: ${formatIDR(data.target)}</span>
</div>
`;
list.appendChild(div);
});
document.getElementById('monthRealization').innerText = formatIDR(monthTotal);
renderTrendChart(weeklyData);
}
/**
* FUNGSI EKSPOR MULTI-FORMAT (XLSX & PDF)
*/
function exportReport(format) {
const reportMonth = document.getElementById('reportMonth').value;
const [year, month] = reportMonth.split('-');
const monthNames = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"];
const periodLabel = `${monthNames[month-1]} ${year}`;
// Filter data lunas pada bulan yang dipilih
const reportData = masterData.filter(r => {
if (!r[10]) return false;
const d = new Date(r[10]);
return d.getFullYear() == year && (d.getMonth() + 1) == month && (r[6]||'').toUpperCase().includes('LUNAS');
});
if (reportData.length === 0) {
showToast(`Tidak ada data setoran lunas untuk periode ${periodLabel}.`);
return;
}
if (format === 'xlsx') {
const excelData = reportData.map((r, i) => ({
"No": i + 1,
"NOP": r[1],
"Nama Wajib Pajak": r[2],
"Nominal": parseInt(r[5].replace(/[^0-9]/g,'')),
"Penyetor": r[7],
"Tgl Setor": r[10]
}));
const ws = XLSX.utils.json_to_sheet(excelData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Data Setoran");
ws['!cols'] = [{wch:5}, {wch:20}, {wch:30}, {wch:15}, {wch:20}, {wch:15}];
XLSX.writeFile(wb, `Laporan_PBB_Sirnasari_${reportMonth}.xlsx`);
showToast("Laporan XLSX berhasil diunduh.");
}
else if (format === 'pdf') {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
// Header PDF
doc.setFontSize(16); doc.setFont("helvetica", "bold");
doc.text("PEMERINTAH DESA SIRNASARI", 105, 15, { align: "center" });
doc.setFontSize(12); doc.setFont("helvetica", "normal");
doc.text(`LAPORAN SETORAN PBB - PERIODE ${periodLabel.toUpperCase()}`, 105, 22, { align: "center" });
doc.setLineWidth(0.5); doc.line(20, 25, 190, 25);
const tableData = reportData.map((r, i) => [
i + 1,
r[1],
r[2],
formatIDR(parseInt(r[5].replace(/[^0-9]/g,''))),
r[7],
r[10]
]);
doc.autoTable({
startY: 30,
head: [['No', 'NOP', 'Nama Wajib Pajak', 'Nominal', 'Penyetor', 'Tgl Setor']],
body: tableData,
theme: 'grid',
headStyles: { fillColor: [16, 185, 129], fontSize: 9 },
styles: { fontSize: 8 },
columnStyles: { 3: { halign: 'right' } }
});
const finalY = doc.lastAutoTable.finalY || 30;
const totalNominal = reportData.reduce((s, r) => s + (parseInt(r[5].replace(/[^0-9]/g,'')) || 0), 0);
doc.setFontSize(10); doc.setFont("helvetica", "bold");
doc.text(`TOTAL SETORAN: ${formatIDR(totalNominal)}`, 190, finalY + 10, { align: "right" });
doc.save(`Laporan_PBB_Sirnasari_${reportMonth}.pdf`);
showToast("Laporan PDF berhasil diunduh.");
}
}
function openVerifyModal(nop, nama, nom, currentCollector) {
selectedItemForVerify = { nop, nama, nom };
document.getElementById('vNopName').innerText = nama;
document.getElementById('vNopNumber').innerText = nop;
document.getElementById('vNopAmount').innerText = formatIDR(nom);
document.getElementById('vDepositDate').valueAsDate = new Date();
const select = document.getElementById('vCollectorName');
if (currentCollector !== '-') {
const options = Array.from(select.options);
const exists = options.some(opt => opt.value === currentCollector);
if (exists) select.value = currentCollector;
}
toggleModal('verifyModal', true);
}
function processVerification() {
const col = document.getElementById('vCollectorName').value;
const tgl = document.getElementById('vDepositDate').value;
if(!col || !tgl) return;
const idx = masterData.findIndex(r => r[1] === selectedItemForVerify.nop);
if(idx !== -1) {
masterData[idx][6] = "LUNAS";
masterData[idx][7] = col;
masterData[idx][10] = tgl;
showToast(`PBB ${selectedItemForVerify.nama} Berhasil Diverifikasi.`);
calculateProgress();
updateAdminDashboard();
toggleModal('verifyModal', false);
document.getElementById('resultsList').innerHTML = '';
document.getElementById('searchInput').value = '';
}
}
function toggleAdminPanel() {
if (isAdmin) {
isAdmin = false;
document.getElementById('adminBadge').classList.add('hidden');
document.getElementById('adminStatsArea').classList.add('hidden');
document.getElementById('mainHeader').classList.replace('admin-gradient', 'search-gradient');
document.getElementById('adminBtnText').innerText = "Admin";
showToast("Sesi Admin Berakhir.");
document.getElementById('resultsList').innerHTML = '';
document.getElementById('searchInput').value = '';
} else {
toggleModal('loginModal', true);
}
}
function loginAdmin() {
const pin = document.getElementById('adminPinInput').value;
if (pin === "611127") {
isAdmin = true;
toggleModal('loginModal', false);
document.getElementById('adminBadge').classList.remove('hidden');
document.getElementById('adminBadge').classList.add('flex');
document.getElementById('adminStatsArea').classList.remove('hidden');
document.getElementById('mainHeader').classList.replace('search-gradient', 'admin-gradient');
document.getElementById('adminBtnText').innerText = "Logout";
updateAdminDashboard();
showToast("Admin Berhasil Login");
const searchVal = document.getElementById('searchInput').value;
if(searchVal.length >= 2) {
const filtered = masterData.filter(r => (r[1]||'').includes(searchVal) || (r[2]||'').toLowerCase().includes(searchVal)).slice(0, 15);
renderResults(filtered);
}
} else {
showToast("PIN Salah!");
}
document.getElementById('adminPinInput').value = '';
}
function renderTrendChart(data) {
const ctx = document.getElementById('trendChart').getContext('2d');
if (trendChart) trendChart.destroy();
trendChart = new Chart(ctx, {
type: 'line',
data: {
labels: ['Mgg 1', 'Mgg 2', 'Mgg 3', 'Mgg 4'],
datasets: [{
data: data,
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderWidth: 3,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: { y: { display: false }, x: { grid: { display: false } } }
}
});
}
function addToCheckout(nop, nama, nominal) {
if(!cart.find(i => i.nop === nop)) {
cart.push({nop, nama, nominal});
updateCheckoutUI();
showPage('payment');
} else {
showPage('payment');
}
}
function updateCheckoutUI() {
const list = document.getElementById('checkoutList');
const totalDisplay = document.getElementById('checkoutTotalAmount');
const countDisplay = document.getElementById('totalNopCount');
const cartBtn = document.getElementById('cartBtn');
const cartCount = document.getElementById('cartCount');
list.innerHTML = '';
let total = 0;
cart.forEach((item, idx) => {
total += item.nominal;
const div = document.createElement('div');
div.className = 'flex justify-between items-center p-3 bg-slate-50 rounded-xl border border-slate-100';
div.innerHTML = `<div class="text-[10px]"><p class="font-black text-slate-800">${item.nama}</p><p class="text-slate-400">${item.nop}</p></div>
<div class="flex items-center gap-3"><span class="text-[10px] font-black text-emerald-600">${formatIDR(item.nominal)}</span>
<button onclick="removeFromCart(${idx})" class="text-rose-400"><i class="fas fa-times-circle"></i></button></div>`;
list.appendChild(div);
});
totalDisplay.innerText = formatIDR(total);
countDisplay.innerText = cart.length;
cartCount.innerText = cart.length;
cartBtn.classList.toggle('hidden', cart.length === 0);
}
function removeFromCart(idx) {
cart.splice(idx, 1);
updateCheckoutUI();
if(cart.length === 0) showPage('search');
}
function updatePaymentInfo(type) {
const area = document.getElementById('paymentInfoArea');
area.classList.remove('hidden');
if(type === 'azmi') {
selectedCollector = { name: "Azmi Zakaria", acc: "1770016939453" };
document.getElementById('accNumber').innerText = "1770016939453";
document.getElementById('accName').innerText = "A/N Azmi Zakaria";
} else {
selectedCollector = { name: "Aslam Jihadudien", acc: "1770013956724" };
document.getElementById('accNumber').innerText = "1770013956724";
document.getElementById('accName').innerText = "A/N Aslam Jihadudien";
}
}
function confirmFinalPayment() {
if(!selectedCollector) { showToast("Pilih metode bayar!"); return; }
const total = cart.reduce((s, i) => s + i.nominal, 0);
const nops = cart.map(i => `- ${i.nama} (${i.nop})`).join('\n');
const msg = `KONFIRMASI BAYAR PBB\n\nTotal: ${formatIDR(total)}\n\nNops:\n${nops}\n\nTujuan: ${selectedCollector.name}`;
window.open(`https://wa.me/628211611127?text=${encodeURIComponent(msg)}`);
}
function showPage(id) {
document.querySelectorAll('.page-container').forEach(p => p.classList.add('hidden'));
document.getElementById('page-' + id).classList.remove('hidden');
window.scrollTo(0,0);
}
function toggleModal(id, show) { document.getElementById(id).classList.toggle('hidden', !show); }
function formatIDR(v) { return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(v); }
function showToast(m) {
const t = document.createElement('div');
t.className = 'fixed bottom-10 left-1/2 -translate-x-1/2 z-[200] bg-slate-800 text-white px-6 py-3 rounded-2xl font-bold text-[10px] shadow-2xl fade-in';
t.innerText = m;
document.body.appendChild(t);
setTimeout(() => t.remove(), 3000);
}
function copyAcc() {
const acc = document.getElementById('accNumber').innerText;
const el = document.createElement('textarea');
el.value = acc;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
showToast("Rekening disalin!");
}
function contactCollector(nop, nama, nom) {
const msg = `Halo, saya ingin menanyakan status pajak PBB:\nNama: ${nama}\nNOP: ${nop}\nNominal: ${formatIDR(nom)}`;
window.open(`https://wa.me/628211611127?text=${encodeURIComponent(msg)}`);
}
</script>
</body>
</html>