<!DOCTYPE html>

<html lang="id">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>SIPANDA KARYA - SPAM DINAS PUPR KABUPATEN ACEH BARAT</title>

<!-- Bootstrap 5 CSS -->

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">

<!-- FontAwesome Icons -->

<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">

<!-- Leaflet Map CSS -->

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />

<!-- Chart.js Library -->

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<!-- HTML5-QRCode Scanner Library -->

<script src="https://unpkg.com/html5-qrcode"></script>

<!-- Leaflet Map JS -->

<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>

<style>

:root {

--primary-color: #0d6efd;

--sidebar-width: 260px;

}

body {

background-color: #f4f6f9;

font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;

overflow-x: hidden;

}

/* BACKROUND PALING DASAR LANDING PAGE */

#landingPage {

position: relative;

background: linear-gradient(rgba(15, 23, 42, 0.85), rgba(30, 41, 59, 0.9)),

url('https://lh3.googleusercontent.com/d/1TfVv_sYHvOj8DOFxYuzhx3RPAIfGXZDX') no-repeat center center fixed;

background-size: cover;

min-height: 100vh;

}

.hero-banner {

background: rgba(15, 23, 42, 0.4);

color: white;

padding: 60px 20px;

text-align: center;

border-bottom: 4px solid #0d6efd;

backdrop-filter: blur(2px);

}

#sidebar {

width: var(--sidebar-width);

position: fixed;

top: 0;

left: 0;

height: 100vh;

background: #1e293b;

color: #fff;

transition: all 0.3s ease-in-out;

z-index: 1040;

overflow-y: auto;

}

#sidebar .logo-box {

padding: 20px 15px;

text-align: center;

border-bottom: 1px solid #334155;

position: relative;

}

#sidebar .logo-box img {

max-height: 60px;

margin: 0 4px;

}

#sidebar .nav-link {

color: #94a3b8;

padding: 12px 20px;

border-radius: 8px;

margin: 4px 12px;

font-weight: 500;

}

#sidebar .nav-link:hover, #sidebar .nav-link.active {

color: #fff;

background-color: #2563eb;

}

#content {

margin-left: var(--sidebar-width);

padding: 25px;

transition: all 0.3s ease-in-out;

}

.top-navbar {

display: none;

background: #1e293b;

color: white;

padding: 10px 15px;

position: sticky;

top: 0;

z-index: 1030;

box-shadow: 0 2px 5px rgba(0,0,0,0.1);

}

#sidebarOverlay {

position: fixed;

top: 0;

left: 0;

width: 100vw;

height: 100vh;

background: rgba(0, 0, 0, 0.5);

z-index: 1035;

display: none;

opacity: 0;

transition: opacity 0.3s ease;

}

@media (max-width: 768px) {

.top-navbar {

display: flex;

justify-content: space-between;

align-items: center;

}

#sidebar {

transform: translateX(-100%);

}

#sidebar.show-sidebar {

transform: translateX(0);

}

#content {

margin-left: 0 !important;

padding: 15px;

}

#sidebarOverlay.active {

display: block;

opacity: 1;

}

}

.card {

border: none;

border-radius: 12px;

box-shadow: 0 4px 12px rgba(0,0,0,0.05);

}

.clickable-card {

cursor: pointer;

transition: transform 0.2s, box-shadow 0.2s;

}

.clickable-card:hover {

transform: translateY(-3px);

box-shadow: 0 6px 15px rgba(0,0,0,0.15) !important;

}

.kop-header {

border-bottom: 3px double #000;

padding-bottom: 10px;

margin-bottom: 20px;

}

#leafletMapPicker {

height: 280px;

width: 100%;

border-radius: 8px;

margin-top: 8px;

border: 1px solid #ced4da;

}

/* CSS PRINTER THERMAL 58MM */

@media print {

body * { visibility: hidden; }

#receiptPrint, #receiptPrint *, #houseStickerPrint, #houseStickerPrint * { visibility: visible; }

#receiptPrint, #houseStickerPrint {

position: absolute;

left: 0;

top: 0;

width: 58mm;

margin: 0;

padding: 2mm 1mm;

font-family: 'Courier New', monospace, sans-serif;

font-size: 8pt;

line-height: 1.15;

color: #000;

}

#printAreaReport, #printAreaReport * { visibility: visible; }

#printAreaReport {

position: absolute;

left: 0;

top: 0;

width: 100%;

margin: 0;

padding: 15mm;

background: #fff;

color: #000;

}

.no-print { display: none !important; }

}

</style>

</head>

<body>

<!-- LANDING PAGE PUBLIC DENGAN BACKGROUND COSTUM -->

<div id="landingPage">

<!-- Navbar Landing Page -->

<nav class="navbar navbar-expand-lg navbar-dark bg-dark bg-opacity-75 sticky-top shadow-sm backdrop-blur">

<div class="container">

<a class="navbar-brand d-flex align-items-center fw-bold" href="#">

<img src="https://lh3.googleusercontent.com/d/1VmYXYrdLuELiw3NVPdD2dEx6pwvaLXIT" alt="Logo Utama" height="40" class="me-2">

<span>SIPANDA KARYA</span>

</a>

<div class="ms-auto d-flex gap-2">

<button class="btn btn-outline-light rounded-pill px-3" onclick="scrollToCekTagihan()"><i class="fa-solid fa-magnifying-glass me-1"></i> Cek Tagihan</button>

<button class="btn btn-primary rounded-pill px-4 fw-bold" onclick="openLoginModal()"><i class="fa-solid fa-right-to-bracket me-1"></i> Login Petugas</button>

</div>

</div>

</nav>

<!-- Hero Banner -->

<div class="hero-banner">

<div class="container py-4">

<div class="d-flex justify-content-center align-items-center gap-3 mb-3">

<img src="https://lh3.googleusercontent.com/d/1VQVE8v1tUweaET-IARcQBArUUVfIFxd0" alt="Logo Pemkab Aceh Barat" style="max-height: 80px;">

<img src="https://lh3.googleusercontent.com/d/1G0wjXSO1ReKjzRmqsPC-BuV37M8hAH9S" alt="Logo Mitra SPAM" style="max-height: 80px;">

<img src="https://lh3.googleusercontent.com/d/1VmYXYrdLuELiw3NVPdD2dEx6pwvaLXIT" alt="Logo SIPANDA" style="max-height: 90px;">

</div>

<h2 class="fw-bold mb-2 text-white">SISTEM INFORMASI MONITORING & EVALUASI SPAM</h2>

<h4 class="text-info fw-semibold mb-3">DINAS PEKERJAAN UMUM DAN PENATAAN RUANG KABUPATEN ACEH BARAT</h4>

<p class="lead max-w-700 mx-auto text-light opacity-90" style="font-size: 15px;">

Transparansi dan Efisiensi Pelayanan Air Minum Swakelola Gampong / Desa Kabupaten Aceh Barat.

</p>

</div>

</div>

<!-- Main Container Landing -->

<div class="container py-5">

<div class="row justify-content-center">

<!-- Card Cek Tagihan Mandiri -->

<div class="col-md-7 mb-4" id="sectionCekTagihan">

<div class="card shadow border-0 rounded-4 p-4 bg-white bg-opacity-95">

<div class="text-center mb-4">

<span class="badge bg-primary-subtle text-primary fw-bold px-3 py-2 rounded-pill mb-2"><i class="fa-solid fa-water me-1"></i> LAYANAN PELANGGAN</span>

<h4 class="fw-bold text-dark">Cek Tagihan Air Mandiri</h4>

<p class="text-muted small">Masukkan ID Pelanggan Anda untuk melihat informasi penggunaan air & rincian tunggakan tagihan.</p>

</div>

<div class="input-group input-group-lg mb-3">

<input type="text" id="landingCekIdPelanggan" class="form-control" placeholder="Contoh: PLG-SSG-001" onkeyup="if(event.key === 'Enter') doLandingCekTagihan()">

<button class="btn btn-primary fw-bold px-4" type="button" id="btnLandingCekTagihan" onclick="doLandingCekTagihan()">

<i class="fa-solid fa-search me-1"></i> Periksa Tagihan

</button>

</div>

<div id="landingCekResultBox" class="d-none"></div>

</div>

</div>

<!-- Quick Info Banner -->

<div class="col-md-5 mb-4">

<div class="card shadow-sm border-0 rounded-4 p-4 bg-white bg-opacity-95 h-100">

<h5 class="fw-bold text-dark border-bottom pb-2 mb-3"><i class="fa-solid fa-circle-info text-primary me-2"></i>Informasi & Bantuan</h5>

<div class="mb-3">

<h6 class="fw-bold mb-1 text-primary"><i class="fa-solid fa-clock me-2"></i>Jam Operasional Loket</h6>

<p class="text-muted small mb-0">Senin - Jumat: 08:00 - 16:30 WIB<br>Kantor Keuchik Gampong Layanan SPAM</p>

</div>

<div class="mb-3">

<h6 class="fw-bold mb-1 text-primary"><i class="fa-solid fa-building me-2"></i>Pengelola SPAM</h6>

<p class="text-muted small mb-0">Kelompok Pengelola Sistem Penyediaan Air Minum (KPSPAM) Swakelola Bina Bersama Dinas PUPR Kabupaten Aceh Barat.</p>

</div>

<div class="alert alert-info border-0 mb-0 rounded-3">

<small class="fw-semibold"><i class="fa-solid fa-shield-halved me-1"></i> Pastikan selalu menyimpan bukti resi/struk pembayaran fisik resmi dari kasir.</small>

</div>

</div>

</div>

</div>

</div>

</div>

<!-- APP INTERACTION WRAPPER -->

<div id="mainApp" class="d-none">

<div class="top-navbar">

<div class="d-flex align-items-center">

<button class="btn btn-outline-light btn-sm me-2" onclick="toggleSidebar()">

<i class="fa-solid fa-bars fa-lg"></i>

</button>

<span class="fw-bold fs-6">SIPANDA KARYA</span>

</div>

<small class="text-info fw-semibold" id="topDesaBadge" style="font-size: 11px;">SEMUA DESA</small>

</div>

<div id="sidebarOverlay" onclick="toggleSidebar()"></div>

<!-- SIDEBAR NAVIGATION -->

<div id="sidebar">

<div class="logo-box">

<button class="btn-close btn-close-white position-absolute top-0 end-0 m-3 d-md-none" onclick="toggleSidebar()"></button>

<div class="d-flex justify-content-center align-items-center mb-2">

<img src="https://lh3.googleusercontent.com/d/1VQVE8v1tUweaET-IARcQBArUUVfIFxd0" alt="Logo Pemkab" style="max-height: 38px;">

<img src="https://lh3.googleusercontent.com/d/1G0wjXSO1ReKjzRmqsPC-BuV37M8hAH9S" alt="Logo SPAM" style="max-height: 38px;">

<img src="https://lh3.googleusercontent.com/d/1VmYXYrdLuELiw3NVPdD2dEx6pwvaLXIT" alt="Logo SIPANDA" style="max-height: 45px;">

</div>

<div class="fw-bold text-white fs-6">SIPANDA KARYA</div>

<div class="text-info fw-bold" id="sidebarDesaTitle" style="font-size: 11px;">SPAM SWAKELOLA DESA</div>

<small class="text-muted fs-8">PUPR KAB. ACEH BARAT</small>

</div>

<div class="px-3 py-2 text-muted small fw-bold">MENU UTAMA</div>

<nav class="nav flex-column mb-5">

<a class="nav-link active" id="nav-dashboard" href="#" onclick="showPage('dashboard')">

<i class="fa-solid fa-gauge me-2"></i> Dashboard

</a>

<a class="nav-link role-admin role-catat role-superadmin" id="nav-pelanggan" href="#" onclick="showPage('pelanggan')">

<i class="fa-solid fa-users me-2"></i> Data Pelanggan

</a>

<a class="nav-link role-admin role-superadmin" id="nav-tarif" href="#" onclick="showPage('tarif')">

<i class="fa-solid fa-tags me-2"></i> Master Tarif

</a>

<a class="nav-link role-admin role-catat role-superadmin" id="nav-pencatatan" href="#" onclick="showPage('pencatatan')">

<i class="fa-solid fa-faucet-drip me-2"></i> Catat Meter

</a>

<a class="nav-link role-admin role-kasir role-superadmin" id="nav-tagihan" href="#" onclick="showPage('tagihan')">

<i class="fa-solid fa-file-invoice-dollar me-2"></i> Data Tagihan

</a>

<a class="nav-link role-admin role-kasir role-superadmin" id="nav-laporan" href="#" onclick="showPage('laporan')">

<i class="fa-solid fa-file-pdf me-2"></i> Laporan Bulanan

</a>

<a class="nav-link role-admin role-superadmin" id="nav-users" href="#" onclick="showPage('users')">

<i class="fa-solid fa-user-gear me-2"></i> Manajemen Akun

</a>

</nav>

<div class="w-100 p-3 border-top border-secondary bg-dark">

<div class="d-flex align-items-center mb-2">

<i class="fa-solid fa-circle-user fa-2x text-primary me-2"></i>

<div>

<div id="userDisplayName" class="fw-bold text-white small">-</div>

<span id="userDisplayRole" class="badge bg-primary fs-8 me-1">-</span>

<span id="userDisplayDesa" class="badge bg-info text-dark fs-8">-</span>

</div>

</div>

<button class="btn btn-outline-danger btn-sm w-100 mt-2" onclick="logout()">

<i class="fa-solid fa-right-from-bracket me-1"></i> Keluar

</button>

</div>

</div>

<!-- MAIN CONTENT AREA -->

<div id="content">

<!-- DASHBOARD PAGE -->

<div id="page-dashboard" class="page-section">

<div class="mb-4">

<h4 class="fw-bold text-dark mb-1" id="dashWelcomeTitle">Selamat Datang</h4>

<small class="text-primary fw-semibold" id="dashSubTitle">SPAM SWAKELOLA DESA - DINAS PUPR KABUPATEN ACEH BARAT</small>

</div>

<div class="row g-3 mb-4">

<div class="col-md-3">

<div class="card p-3 bg-primary text-white shadow-sm">

<small class="text-white-50 fw-semibold">Total Pelanggan</small>

<h3 class="fw-bold mb-0 mt-1" id="dash-pelanggan">0</h3>

</div>

</div>

<div class="col-md-3">

<div class="card p-3 bg-warning text-dark shadow-sm clickable-card" onclick="filterTagihanStatus('Belum Lunas')">

<small class="text-dark-50 fw-semibold"><i class="fa-solid fa-hand-pointer me-1"></i> Tagihan Belum Lunas</small>

<h3 class="fw-bold mb-0 mt-1" id="dash-unpaid">0</h3>

</div>

</div>

<div class="col-md-3">

<div class="card p-3 bg-success text-white shadow-sm clickable-card" onclick="filterTagihanStatus('Lunas')">

<small class="text-white-50 fw-semibold"><i class="fa-solid fa-hand-pointer me-1"></i> Tagihan Lunas</small>

<h3 class="fw-bold mb-0 mt-1" id="dash-paid">0</h3>

</div>

</div>

<div class="col-md-3">

<div class="card p-3 bg-info text-white shadow-sm">

<small class="text-white-50 fw-semibold">Total Catat Bulan Ini</small>

<h3 class="fw-bold mb-0 mt-1" id="dash-catat">0</h3>

</div>

</div>

</div>

<div class="row mb-3">

<div class="col-md-4 ms-auto">

<div class="input-group input-group-sm">

<label class="input-group-text bg-white fw-bold text-dark border-secondary" for="dashPeriodeFilter"><i class="fa-solid fa-filter me-1 text-primary"></i> Filter Keuangan:</label>

<select class="form-select border-secondary fw-semibold" id="dashPeriodeFilter" onchange="renderDashboardFinance()">

<option value="ALL">Semua Periode</option>

</select>

</div>

</div>

</div>

<div class="row g-3 mb-4">

<div class="col-md-6">

<div class="card p-3 bg-dark text-white shadow-sm">

<div class="d-flex justify-content-between align-items-center">

<div>

<small class="text-white-50">Total Revenue / Pendapatan Terbayar</small>

<h2 class="fw-bold text-success mb-0 mt-1" id="dash-revenue">Rp 0</h2>

</div>

<i class="fa-solid fa-wallet fa-3x text-success opacity-50"></i>

</div>

</div>

</div>

<div class="col-md-6">

<div class="card p-3 bg-secondary text-white shadow-sm">

<div class="d-flex justify-content-between align-items-center">

<div>

<small class="text-white-50">Estimasi Piutang (Belum Terbayar)</small>

<h2 class="fw-bold text-warning mb-0 mt-1" id="dash-piutang">Rp 0</h2>

</div>

<i class="fa-solid fa-hand-holding-dollar fa-3x text-warning opacity-50"></i>

</div>

</div>

</div>

</div>

<div class="row g-3">

<div class="col-md-8">

<div class="card p-3 shadow-sm">

<h6 class="fw-bold text-dark mb-3"><i class="fa-solid fa-chart-line text-primary me-2"></i> Tren Pemakaian Air (m )</h6>

<div style="height: 250px;">

<canvas id="usageChart"></canvas>

</div>

</div>

</div>

<div class="col-md-4">

<div class="card p-3 shadow-sm">

<h6 class="fw-bold text-dark mb-3"><i class="fa-solid fa-clock-rotate-left text-primary me-2"></i> Pencatatan Terbaru</h6>

<ul class="list-group list-group-flush small" id="recentCatatList">

<li class="list-group-item text-muted text-center py-3">Belum ada data</li>

</ul>

</div>

</div>

</div>

</div>

<!-- DATA PELANGGAN PAGE -->

<div id="page-pelanggan" class="page-section d-none">

<div class="d-flex justify-content-between align-items-center mb-3">

<h4 class="fw-bold text-dark">Data Pelanggan</h4>

<button class="btn btn-primary" onclick="openModalPelanggan()"><i class="fa-solid fa-plus me-1"></i> Tambah Pelanggan</button>

</div>

<div class="card p-3 table-responsive">

<table class="table table-hover align-middle" id="tablePelanggan">

<thead>

<tr>

<th>ID</th>

<th>Nama Pelanggan</th>

<th>Alamat</th>

<th>Desa</th>

<th>Golongan</th>

<th>Koordinat</th>

<th>Status</th>

<th>Kode QR</th>

<th>Aksi</th>

</tr>

</thead>

<tbody></tbody>

</table>

</div>

</div>

<!-- MASTER TARIF PAGE -->

<div id="page-tarif" class="page-section d-none">

<div class="d-flex justify-content-between align-items-center mb-3">

<h4 class="fw-bold text-dark">Master Tarif</h4>

<button class="btn btn-primary" onclick="openModalTarif()"><i class="fa-solid fa-plus me-1"></i> Tambah Tarif</button>

</div>

<div class="card p-3 table-responsive">

<table class="table table-hover align-middle" id="tableTarif">

<thead>

<tr>

<th>Golongan</th>

<th>Tarif / m (Rp)</th>

<th>Biaya Admin (Rp)</th>

<th>Keterangan</th>

<th>Aksi</th>

</tr>

</thead>

<tbody></tbody>

</table>

</div>

</div>

<!-- PENCATATAN METER PAGE -->

<div id="page-pencatatan" class="page-section d-none">

<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">

<h4 class="fw-bold text-dark mb-0">Pencatatan Meter Air</h4>

<div>

<button class="btn btn-outline-primary me-2" onclick="startQRScanner()"><i class="fa-solid fa-qrcode me-1"></i> Scan QR Pelanggan</button>

<button class="btn btn-primary" onclick="openModalCatat()"><i class="fa-solid fa-plus me-1"></i> Input Catat Meter</button>

</div>

</div>

<div class="card p-2 mb-3">

<div class="input-group">

<span class="input-group-text bg-white border-end-0"><i class="fa-solid fa-magnifying-glass text-muted"></i></span>

<input type="text" id="searchCatat" class="form-control border-start-0" placeholder="Cari berdasarkan ID Catat, ID Pelanggan, Nama, Desa, Periode, Petugas..." onkeyup="renderCatat()">

</div>

</div>

<div id="qrScannerContainer" class="card p-3 mb-3 d-none text-center bg-dark text-white">

<div class="d-flex justify-content-between align-items-center mb-2">

<h6 class="mb-0 text-white"><i class="fa-solid fa-camera me-2"></i> Pindai Kode QR Pelanggan</h6>

<button class="btn btn-sm btn-danger" onclick="stopQRScanner()"><i class="fa-solid fa-xmark"></i> Tutup</button>

</div>

<div id="reader" style="width: 100%; max-width: 400px; margin: 0 auto;"></div>

</div>

<div class="card p-3 table-responsive">

<table class="table table-hover align-middle" id="tableCatat">

<thead>

<tr>

<th>ID Catat</th>

<th>Periode</th>

<th>ID Pelanggan</th>

<th>Nama Pelanggan</th>

<th>Desa</th>

<th>Meter Awal</th>

<th>Meter Akhir</th>

<th>Pemakaian (m )</th>

<th>Petugas</th>

<th>Tanggal</th>

<th>Aksi</th>

</tr>

</thead>

<tbody></tbody>

</table>

</div>

</div>

<!-- DATA TAGIHAN PAGE -->

<div id="page-tagihan" class="page-section d-none">

<div class="d-flex justify-content-between align-items-center mb-3">

<h4 class="fw-bold text-dark mb-0">Data Tagihan & Pembayaran</h4>

<div id="filterBadge" class="d-none">

<span class="badge bg-secondary p-2" id="filterStatusText">Filter Status</span>

<button class="btn btn-sm btn-outline-dark ms-1" onclick="renderTagihan()"><i class="fa-solid fa-xmark"></i> Reset Filter</button>

</div>

</div>

<div class="card p-2 mb-3">

<div class="input-group">

<span class="input-group-text bg-white border-end-0"><i class="fa-solid fa-magnifying-glass text-muted"></i></span>

<input type="text" id="searchTagihan" class="form-control border-start-0" placeholder="Cari berdasarkan ID Tagihan, ID Pelanggan, Nama, Desa, Periode, Status..." onkeyup="renderTagihan()">

</div>

</div>

<div class="card p-3 table-responsive">

<table class="table table-hover align-middle" id="tableTagihan">

<thead>

<tr>

<th>ID Tagihan</th>

<th>ID Plg</th>

<th>Nama Pelanggan</th>

<th>Desa</th>

<th>Periode</th>

<th>Pemakaian</th>

<th>Total Bayar (Rp)</th>

<th>Status</th>

<th>Tanggal Bayar</th>

<th>Aksi / Struk</th>

</tr>

</thead>

<tbody></tbody>

</table>

</div>

</div>

<!-- LAPORAN BULANAN PAGE -->

<div id="page-laporan" class="page-section d-none">

<div class="card p-3 mb-4 no-print shadow-sm">

<div class="row align-items-center g-3">

<div class="col-md-5">

<label class="form-label fw-bold text-dark"><i class="fa-solid fa-calendar-days text-primary me-2"></i> Pilih Bulan / Periode Laporan:</label>

<select id="selectPeriodeLaporan" class="form-select" onchange="renderLaporanBulanan()">

<option value="ALL">-- Semua Periode --</option>

</select>

</div>

<div class="col-md-7 text-md-end mt-4 mt-md-0">

<button class="btn btn-danger py-2 px-4 fw-bold" onclick="downloadLaporanPDF()">

<i class="fa-solid fa-file-pdf me-2"></i> Download / Cetak Laporan PDF

</button>

</div>

</div>

</div>

<div id="printAreaReport" class="card p-4 bg-white shadow-sm">

<div class="kop-header text-center">

<div class="d-flex justify-content-between align-items-center mb-2 px-3">

<img src="https://lh3.googleusercontent.com/d/1VQVE8v1tUweaET-IARcQBArUUVfIFxd0" alt="Logo Kabupaten Aceh Barat" style="max-height: 80px;">

<div class="text-center px-2">

<h5 class="fw-bold mb-0 text-dark">PEMERINTAH KABUPATEN ACEH BARAT</h5>

<h4 class="fw-bold mb-0 text-dark">DINAS PEKERJAAN UMUM DAN PENATAAN RUANG</h4>

<p class="mb-0 fw-semibold text-primary" id="lapSubHeaderTitle" style="font-size: 14px;">SPAM SWAKELOLA GAMPONG</p>

<small class="text-muted" style="font-size: 11px;">JALAN SISINGAMANGARAJA LR BKKBN DESA DRIEN RAMPAK KECAMATAN JOHAN PAHLAWAN KABUPATEN ACEH BARAT PROVINSI ACEH</small>

</div>

<img src="https://lh3.googleusercontent.com/d/1VmYXYrdLuELiw3NVPdD2dEx6pwvaLXIT" alt="Logo Utama SIPANDA" style="max-height: 80px;">

</div>

</div>

<div class="text-center my-3">

<h5 class="fw-bold text-decoration-underline mb-1">LAPORAN BULANAN REKAPITULASI PEMBAYARAN AIR</h5>

<p class="fw-semibold text-secondary mb-0" id="lblPeriodeLaporan">Periode: Semua Periode</p>

</div>

<div class="row text-center mb-4 g-2">

<div class="col-3">

<div class="border rounded p-2 bg-light">

<small class="text-muted d-block">Total Tagihan</small>

<strong id="lap-total-tagihan" class="fs-6">0</strong>

</div>

</div>

<div class="col-3">

<div class="border rounded p-2 bg-light">

<small class="text-muted d-block">Total Lunas</small>

<strong id="lap-total-lunas" class="text-success fs-6">0</strong>

</div>

</div>

<div class="col-3">

<div class="border rounded p-2 bg-light">

<small class="text-muted d-block">Total Belum Lunas</small>

<strong id="lap-total-unpaid" class="text-danger fs-6">0</strong>

</div>

</div>

<div class="col-3">

<div class="border rounded p-2 bg-light">

<small class="text-muted d-block">Total Pendapatan</small>

<strong id="lap-total-rupiah" class="text-primary fs-6">Rp 0</strong>

</div>

</div>

</div>

<div class="table-responsive">

<table class="table table-bordered align-middle text-center small" id="tableLaporan">

<thead class="table-light">

<tr>

<th style="width: 50px;">No</th>

<th>ID Tagihan</th>

<th>ID Plg</th>

<th>Nama Pelanggan</th>

<th>Desa</th>

<th>Periode</th>

<th>Pemakaian (m )</th>

<th>Biaya Air</th>

<th>Biaya Admin</th>

<th>Total Bayar</th>

<th>Status</th>

<th>Tgl Bayar</th>

<th>Kasir</th>

</tr>

</thead>

<tbody></tbody>

<tfoot class="fw-bold table-secondary">

<tr>

<td colspan="6" class="text-end">TOTAL REKAPITULASI:</td>

<td id="footM3">0 m </td>

<td id="footBiayaAir">Rp 0</td>

<td id="footBiayaAdmin">Rp 0</td>

<td id="footTotalBayar">Rp 0</td>

<td colspan="3"></td>

</tr>

</tfoot>

</table>

</div>

<div class="row mt-5 pt-3">

<div class="col-6 text-center"></div>

<div class="col-6 text-center">

<p class="mb-1">Meulaboh, <span id="lapTanggalCetak">-</span></p>

<p class="mb-5" id="lapPengelolaSignTitle">Pengelola SPAM Gampong</p>

<br>

<p class="fw-bold text-decoration-underline mb-0" id="lapNamaPengelola">Administrator / Kasir</p>

<small class="text-muted" id="lapKasirRoleLabel">Kasir / Pengelola SPAM</small>

</div>

</div>

</div>

</div>

<!-- MANAJEMEN USERS PAGE -->

<div id="page-users" class="page-section d-none">

<div class="d-flex justify-content-between align-items-center mb-3">

<h4 class="fw-bold text-dark">Manajemen Akun System</h4>

<button class="btn btn-primary" onclick="openModalUser()"><i class="fa-solid fa-plus me-1"></i> Tambah Akun</button>

</div>

<div class="card p-3 table-responsive">

<table class="table table-hover align-middle" id="tableUsers">

<thead>

<tr>

<th>Username</th>

<th>Nama Lengkap</th>

<th>Desa</th>

<th>Status</th>

<th>Role</th>

<th>Aksi</th>

</tr>

</thead>

<tbody></tbody>

</table>

</div>

</div>

</div>

</div>

<!-- MODAL LOGIN PETUGAS -->

<div class="modal fade" id="loginModal" tabindex="-1" data-bs-backdrop="static">

<div class="modal-dialog modal-dialog-centered">

<div class="modal-content rounded-4 border-0 shadow-lg">

<div class="modal-header border-0 pb-0">

<button type="button" class="btn-close" data-bs-dismiss="modal"></button>

</div>

<div class="modal-body p-4 text-center">

<div class="d-flex justify-content-center align-items-center gap-2 mb-3">

<img src="https://lh3.googleusercontent.com/d/1VQVE8v1tUweaET-IARcQBArUUVfIFxd0" alt="Logo Pemkab" style="max-height: 50px;">

<img src="https://lh3.googleusercontent.com/d/1G0wjXSO1ReKjzRmqsPC-BuV37M8hAH9S" alt="Logo SPAM" style="max-height: 50px;">

<img src="https://lh3.googleusercontent.com/d/1VmYXYrdLuELiw3NVPdD2dEx6pwvaLXIT" alt="Logo SIPANDA" style="max-height: 60px;">

</div>

<h5 class="fw-bold mb-1 text-dark">SIPANDA KARYA</h5>

<p class="text-primary fw-bold mb-1" style="font-size: 13px;">SPAM SWAKELOLA ACEH BARAT</p>

<p class="text-muted small mb-4">LOGIN PETUGAS & PENGELOLA SISTEM</p>

<div id="loginAlert" class="alert alert-danger d-none py-2 fs-7 text-start"></div>

<form onsubmit="event.preventDefault(); doLogin();" class="text-start">

<div class="mb-3">

<label class="form-label small fw-semibold">Username</label>

<input type="text" id="loginUsername" class="form-control" placeholder="Masukkan username" required>

</div>

<div class="mb-4">

<label class="form-label small fw-semibold">Password</label>

<input type="password" id="loginPassword" class="form-control" placeholder="Masukkan password" required>

</div>

<button type="submit" id="btnLogin" class="btn btn-primary w-100 py-2 fw-semibold">

<i class="fa-solid fa-right-to-bracket me-2"></i> Masuk Aplikasi

</button>

</form>

</div>

</div>

</div>

</div>

<!-- STRUK PRINT THERMAL PEMBAYARAN (58 MM) -->

<div id="receiptPrint" class="d-none">

<div style="text-align: center; margin-bottom: 4px;">

<img src="https://lh3.googleusercontent.com/d/1VmYXYrdLuELiw3NVPdD2dEx6pwvaLXIT" style="max-height: 32px; margin-bottom: 2px;" alt="Logo PUPR"><br>

<strong style="font-size: 9pt;">SIPANDA KARYA</strong><br>

<small style="font-size: 7.5pt;" id="rDesaHeader">SPAM SWAKELOLA DESA</small><br>

<small style="font-size: 7pt;">DINAS PUPR ACEH BARAT</small><br>

--------------------------------

</div>

<div style="font-size: 8pt; word-wrap: break-word;">

Tgl: <span id="rTgl">-</span><br>

No: <span id="rId">-</span><br>

Plg: <span id="rNama">-</span> (<span id="rIdPlg">-</span>)<br>

Periode: <span id="rPeriode">-</span><br>

--------------------------------<br>

Meter: <span id="rMtrAwal">0</span> - <span id="rMtrAkhir">0</span> (<span id="rPakai">0</span> m )<br>

Biaya Air: Rp <span id="rBiayaAir">0</span><br>

Biaya Admin: Rp <span id="rBiayaAdmin">0</span><br>

--------------------------------<br>

<strong style="font-size: 8.5pt;">TOTAL: Rp <span id="rTotal">0</span></strong><br>

STATUS: <strong id="rStatus">LUNAS</strong><br>

Kasir: <span id="rKasir">-</span><br>

--------------------------------

</div>

<div style="text-align: center; margin-top: 4px;">

<small style="font-size: 7pt;">Terima Kasih Atas Pembayaran Anda</small>

</div>

</div>

<!-- STRUK STIKER PELANGGAN PERUMAHAN (58 MM) -->

<div id="houseStickerPrint" class="d-none">

<div style="text-align: center; margin-bottom: 4px;">

<img src="https://lh3.googleusercontent.com/d/1VmYXYrdLuELiw3NVPdD2dEx6pwvaLXIT" style="max-height: 32px; margin-bottom: 2px;" alt="Logo PUPR"><br>

<strong style="font-size: 9pt;">SIPANDA KARYA</strong><br>

<small style="font-size: 7.5pt;" id="stkDesaHeader">SPAM SWAKELOLA DESA</small><br>

<small style="font-size: 7pt;">DINAS PUPR ACEH BARAT</small><br>

================================

</div>

<div style="font-size: 8pt; word-wrap: break-word;">

ID PLG : <strong id="stkId">-</strong><br>

NAMA : <span id="stkNama">-</span><br>

GOL : <span id="stkGol">-</span><br>

ALAMAT : <span id="stkAlamat">-</span><br>

</div>

<div style="text-align: center; margin: 6px 0;">

<img id="stkQrImg" src="" style="width: 110px; height: 110px; display: block; margin: 0 auto;" alt="QR Code"><br>

<small style="font-size: 7pt;">Pindai QR Untuk Catat Meter</small>

</div>

<div style="text-align: center;">

================================<br>

<small style="font-size: 6.5pt;">Tempelkan Di Dekat Meteran Air</small>

</div>

</div>

<!-- MODAL DYNAMIC WRAPPER FORM POP-UP -->

<div class="modal fade" id="formModal" tabindex="-1">

<div class="modal-dialog">

<div class="modal-content">

<div class="modal-header">

<h5 class="modal-title fw-bold" id="modalTitle">Form Data</h5>

<button type="button" class="btn-close" data-bs-dismiss="modal"></button>

</div>

<div class="modal-body" id="modalBody"></div>

<div class="modal-footer">

<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>

<button type="button" id="btnSaveData" class="btn btn-primary" onclick="submitForm()">Simpan Data</button>

</div>

</div>

</div>

</div>

<!-- Scripts -->

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>

<script>

let APPS_SCRIPT_URL = "https://script.google.com/macros/s/AKfycbz8vpMStWm057Sh9Uy-bFrm724-xhka7ngp7VAU2DtPTbtlFQaH4T-t0LKLX4PTF6vYBw/exec";

let currentUser = null;

let globalData = {

users: [],

pelanggan: [],

tarif: [],

pencatatan: [],

tagihan: []

};

let activeSheet = "";

let activeRowIndex = 0;

let usageChartInstance = null;

let html5QrCodeScanner = null;

let leafletMap = null;

let leafletMarker = null;

// LIST DESA/GAMPONG ENUM DI ACEH BARAT (TETAP UTUH & SAMA DENGAN DATABASE)

const LIST_DESA_ENUM = [

"Semua Desa",

"Suak Sigadeng",

"Kuala Pling",

"Pasi Pandan",

"Puuk",

"Gampong Baro",

"Paya Baro",

"Karang Hampa",

"Drien Rampak",

"Seuneubok",

"Lapang"

];

function scrollToCekTagihan() {

document.getElementById("sectionCekTagihan").scrollIntoView({ behavior: 'smooth' });

}

function openLoginModal() {

let modalEl = document.getElementById('loginModal');

let modal = bootstrap.Modal.getInstance(modalEl) || new bootstrap.Modal(modalEl);

modal.show();

}

// ADAPTER ENGINE: Mencegah Error JSON Netlify / Apps Script

function runGasMethod(actionName, paramsArray = [], successCb, failCb) {

if (typeof google !== 'undefined' && google.script && google.script.run) {

let runner = google.script.run

.withSuccessHandler(successCb)

.withFailureHandler(failCb || function(err){ console.error(err); });

if (typeof runner[actionName] === 'function') {

runner[actionName].apply(runner, paramsArray);

} else {

console.error("Fungsi Backend " + actionName + " tidak ditemukan!");

}

} else {

if (!APPS_SCRIPT_URL) {

APPS_SCRIPT_URL = prompt("Masukkan Web App URL Google Apps Script (/exec):", "");

if (!APPS_SCRIPT_URL) {

alert("Web App URL belum diisi!");

return;

}

}

let url = APPS_SCRIPT_URL + "?action=" + actionName;

paramsArray.forEach((p, idx) => {

let pVal = typeof p === 'object' ? JSON.stringify(p) : p;

url += "&param" + idx + "=" + encodeURIComponent(pVal);

});

fetch(url)

.then(res => res.json())

.then(data => successCb(data))

.catch(err => {

if (failCb) failCb(err);

else {

console.error("Error Fetch Netlify JSON:", err);

alert("Gagal koneksi server / Web App URL belum diatur dengan benar.");

}

});

}

}

// Helper Format Tanggal Indonesia

function formatDateToIndo(dateInput) {

if (!dateInput || dateInput === '-') return '-';

let d = new Date(dateInput);

if (isNaN(d.getTime())) return dateInput;

let tgl = String(d.getDate()).padStart(2, '0');

let namaBulan = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"];

let bln = namaBulan[d.getMonth()];

let thn = d.getFullYear();

return tgl + " " + bln + " " + thn;

}

function formatPeriodeBulanTahun(periodeRaw) {

if (!periodeRaw || periodeRaw === '-') return '-';

let str = String(periodeRaw).trim();

let parts = str.split('-');

if (parts.length >= 2) {

let yr = parts[0];

let mo = parseInt(parts[1], 10);

let namaBulan = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"];

if (mo >= 1 && mo <= 12) {

return namaBulan[mo - 1] + " " + yr;

}

}

return str;

}

function toggleSidebar() {

let sidebar = document.getElementById("sidebar");

let overlay = document.getElementById("sidebarOverlay");

sidebar.classList.toggle("show-sidebar");

overlay.classList.toggle("active");

}

function getVal(obj, keyNames, defaultVal = '') {

if (!obj) return defaultVal;

let keys = Object.keys(obj);

for (let name of keyNames) {

let match = keys.find(k => k.trim().toLowerCase() === name.trim().toLowerCase());

if (match && obj[match] !== undefined && obj[match] !== null) {

return obj[match];

}

}

return defaultVal;

}

function getNamaPelanggan(idPelanggan) {

let plg = globalData.pelanggan.find(p => getVal(p, ['ID_Pelanggan', 'ID Pelanggan', 'ID']) === idPelanggan);

return plg ? getVal(plg, ['Nama_Pelanggan', 'Nama Pelanggan', 'Nama']) : '-';

}

// HELPER FILTER AKSES DATA DESA BERDASARKAN ROLE USER (SUPER ADMIN VS ROLE LAIN)

function isRowVisibleByDesa(row) {

if (!currentUser) return true;

let role = String(currentUser.role || '').toLowerCase();

if (role === 'super admin' || role === 'superadmin') return true;

let userDesa = currentUser.desa || "Semua Desa";

if (userDesa === "Semua Desa" || userDesa === "") return true;

let itemDesa = getVal(row, ['Desa', 'Gampong', 'Lokasi'], 'Suak Sigadeng');

return String(itemDesa).toLowerCase() === String(userDesa).toLowerCase();

}

function doLandingCekTagihan() {

let inputID = document.getElementById("landingCekIdPelanggan").value.trim();

let btn = document.getElementById("btnLandingCekTagihan");

let resBox = document.getElementById("landingCekResultBox");

if (!inputID) {

alert("Masukkan ID Pelanggan terlebih dahulu!");

return;

}

btn.disabled = true;

btn.innerHTML = `<span class="spinner-border spinner-border-sm"></span> Cek...`;

resBox.classList.add("d-none");

runGasMethod("cekTagihanPelanggan", [inputID], function(res) {

btn.disabled = false;

btn.innerHTML = `<i class="fa-solid fa-search me-1"></i> Periksa Tagihan`;

if (res && res.success) {

let plg = res.pelanggan;

let html = `

<div class="card p-3 border-primary bg-white shadow-sm mt-3 text-start">

<div class="border-bottom pb-2 mb-2">

<span class="badge bg-primary float-end">${plg.golongan}</span>

<strong class="d-block text-dark fs-6">${plg.nama}</strong>

<small class="text-muted"><i class="fa-solid fa-id-card me-1"></i> ID: ${plg.id} | Desa ${plg.desa} - ${plg.alamat}</small>

</div>`;

if (res.hasTunggakan) {

html += `

<div class="alert alert-warning py-2 mb-2 fs-7">

<i class="fa-solid fa-triangle-exclamation text-danger me-1"></i>

Ditemukan <strong>${res.totalTunggakanBulan} Bulan</strong> tagihan tertunggak!

</div>

<div class="table-responsive mb-2">

<table class="table table-sm table-bordered text-center align-middle fs-7 mb-1">

<thead class="table-danger">

<tr>

<th>Periode</th>

<th>Pemakaian</th>

<th>Total Tagihan</th>

<th>Status</th>

</tr>

</thead>

<tbody>`;

res.tunggakanList.forEach(t => {

html += `

<tr>

<td><b>${formatPeriodeBulanTahun(t.periode)}</b></td>

<td>${t.pemakaian} m </td>

<td class="fw-bold text-danger">Rp ${Number(t.totalBayar).toLocaleString('id-ID')}</td>

<td><span class="badge bg-danger">${t.status}</span></td>

</tr>`;

});

html += `

</tbody>

</table>

</div>

<div class="d-flex justify-content-between align-items-center bg-light p-2 rounded border">

<span class="fw-bold text-dark small">Total Harus Dibayar:</span>

<strong class="text-danger fs-6">Rp ${Number(res.totalTunggakanRp).toLocaleString('id-ID')}</strong>

</div>`;

} else {

html += `

<div class="alert alert-success py-2 mb-0 text-center fs-7">

<i class="fa-solid fa-circle-check me-1"></i> Tidak ada tunggakan. Seluruh tagihan air Anda <strong>LUNAS</strong>!

</div>`;

}

if (res.riwayatLunas && res.riwayatLunas.length > 0) {

html += `

<div class="mt-2 text-start">

<small class="fw-bold text-secondary text-uppercase d-block mb-1" style="font-size:10px;">Riwayat Lunas Terakhir:</small>

<div class="d-flex flex-wrap gap-1">`;

res.riwayatLunas.slice(-3).forEach(r => {

html += `<span class="badge bg-success opacity-75" style="font-size:10px;"><i class="fa-solid fa-check me-1"></i>${formatPeriodeBulanTahun(r.periode)}</span>`;

});

html += `</div></div>`;

}

html += `</div>`;

resBox.innerHTML = html;

resBox.classList.remove("d-none");

} else {

resBox.innerHTML = `

<div class="alert alert-danger py-2 mt-3 fs-7">

<i class="fa-solid fa-circle-xmark me-1"></i> ${res ? res.message : "Data tidak ditemukan!"}

</div>`;

resBox.classList.remove("d-none");

}

});

}

function doLogin() {

let u = document.getElementById("loginUsername").value;

let p = document.getElementById("loginPassword").value;

let alertBox = document.getElementById("loginAlert");

let btn = document.getElementById("btnLogin");

if (!u || !p) {

alertBox.innerText = "Username dan Password tidak boleh kosong!";

alertBox.classList.remove("d-none");

return;

}

btn.disabled = true;

btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span> Memproses...`;

alertBox.classList.add("d-none");

runGasMethod("processLogin", [u, p], function(res) {

btn.disabled = false;

btn.innerHTML = `<i class="fa-solid fa-right-to-bracket me-2"></i> Masuk Aplikasi`;

if (res && res.success) {

currentUser = res.user;

let loginModalEl = document.getElementById('loginModal');

let modal = bootstrap.Modal.getInstance(loginModalEl);

if(modal) modal.hide();

document.getElementById("landingPage").classList.add("d-none");

document.getElementById("mainApp").classList.remove("d-none");

let namaUser = currentUser.nama || currentUser.username;

let desaUser = currentUser.desa || "Semua Desa";

document.getElementById("userDisplayName").innerText = namaUser;

document.getElementById("userDisplayRole").innerText = currentUser.role;

document.getElementById("userDisplayDesa").innerText = desaUser;

document.getElementById("topDesaBadge").innerText = desaUser.toUpperCase();

document.getElementById("sidebarDesaTitle").innerText = "DESA " + desaUser.toUpperCase();

document.getElementById("dashWelcomeTitle").innerText = "Selamat Datang, " + namaUser;

document.getElementById("dashSubTitle").innerText = "SPAM SWAKELOLA DESA " + desaUser.toUpperCase() + " - DINAS PUPR KABUPATEN ACEH BARAT";

applyRoleRestrictions();

loadAllData();

} else {

alertBox.innerText = res ? res.message : "Gagal login!";

alertBox.classList.remove("d-none");

}

}, function(err) {

btn.disabled = false;

btn.innerHTML = `<i class="fa-solid fa-right-to-bracket me-2"></i> Masuk Aplikasi`;

alertBox.innerText = "Error Server: " + (err.message || err);

alertBox.classList.remove("d-none");

});

}

function applyRoleRestrictions() {

if (!currentUser) return;

let role = String(currentUser.role || '').toLowerCase();

if (role === "super admin" || role === "superadmin") {

document.querySelectorAll('#sidebar .nav-link').forEach(e => e.classList.remove('d-none'));

} else if (role !== "admin") {

document.querySelectorAll('.role-admin').forEach(e => e.classList.add('d-none'));

if (role === "kasir") {

document.querySelectorAll('.role-kasir').forEach(e => e.classList.remove('d-none'));

} else if (role === "catat") {

document.querySelectorAll('.role-catat').forEach(e => e.classList.remove('d-none'));

}

} else {

document.querySelectorAll('#sidebar .nav-link').forEach(e => e.classList.remove('d-none'));

}

}

function logout() {

currentUser = null;

document.getElementById("loginUsername").value = "";

document.getElementById("loginPassword").value = "";

let alertBox = document.getElementById("loginAlert");

if (alertBox) alertBox.classList.add("d-none");

document.getElementById("mainApp").classList.add("d-none");

document.getElementById("landingPage").classList.remove("d-none");

}

function loadAllData(callbackAfterLoad = null) {

runGasMethod("getAllData", [], function(data) {

globalData = data || {};

globalData.pelanggan = globalData.pelanggan || [];

globalData.tarif = globalData.tarif || [];

globalData.pencatatan = globalData.pencatatan || [];

globalData.tagihan = globalData.tagihan || [];

globalData.users = globalData.users || [];

initDashPeriodeOptions();

renderDashboard();

renderPelanggan();

renderTarif();

renderCatat();

renderTagihan();

renderUsers();

initPeriodeOptions();

renderLaporanBulanan();

if (typeof callbackAfterLoad === 'function') {

callbackAfterLoad();

}

});

}

function showPage(pageName) {

document.querySelectorAll('.page-section').forEach(p => p.classList.add('d-none'));

document.querySelectorAll('#sidebar .nav-link').forEach(n => n.classList.remove('active'));

let page = document.getElementById('page-' + pageName);

let nav = document.getElementById('nav-' + pageName);

if (page) page.classList.remove('d-none');

if (nav) nav.classList.add('active');

if (window.innerWidth <= 768) {

let sidebar = document.getElementById("sidebar");

let overlay = document.getElementById("sidebarOverlay");

if (sidebar) sidebar.classList.remove("show-sidebar");

if (overlay) overlay.classList.remove("active");

}

if (pageName === 'laporan') {

renderLaporanBulanan();

}

}

function filterTagihanStatus(statusFilter) {

showPage('tagihan');

renderTagihan(statusFilter);

}

function initDashPeriodeOptions() {

let select = document.getElementById("dashPeriodeFilter");

if (!select) return;

let selectedCurrent = select.value;

select.innerHTML = `<option value="ALL">Semua Periode</option>`;

let tagihanFiltered = globalData.tagihan.filter(isRowVisibleByDesa);

let periodes = [...new Set(tagihanFiltered.map(t => getVal(t, ['Periode'])))].filter(Boolean).sort().reverse();

periodes.forEach(per => {

let label = formatPeriodeBulanTahun(per);

select.innerHTML += `<option value="${per}">${label}</option>`;

});

if (periodes.includes(selectedCurrent)) {

select.value = selectedCurrent;

}

}

function renderDashboardFinance() {

let filterVal = document.getElementById("dashPeriodeFilter") ? document.getElementById("dashPeriodeFilter").value : "ALL";

let tagihanFiltered = globalData.tagihan.filter(isRowVisibleByDesa);

let filteredTagihan = tagihanFiltered.filter(t => {

if (filterVal === "ALL") return true;

return getVal(t, ['Periode']) === filterVal;

});

let paidList = filteredTagihan.filter(t => getVal(t, ['Status']) === "Lunas");

let unpaidList = filteredTagihan.filter(t => getVal(t, ['Status']) !== "Lunas");

let totalRevenue = paidList.reduce((acc, curr) => acc + Number(getVal(curr, ['Total_Bayar', 'Total Bayar', 'Total'], 0)), 0);

let totalPiutang = unpaidList.reduce((acc, curr) => acc + Number(getVal(curr, ['Total_Bayar', 'Total Bayar', 'Total'], 0)), 0);

document.getElementById("dash-revenue").innerText = "Rp " + totalRevenue.toLocaleString('id-ID');

document.getElementById("dash-piutang").innerText = "Rp " + totalPiutang.toLocaleString('id-ID');

}

function renderDashboard() {

let plgFiltered = globalData.pelanggan.filter(isRowVisibleByDesa);

let tagihanFiltered = globalData.tagihan.filter(isRowVisibleByDesa);

let catatFiltered = globalData.pencatatan.filter(isRowVisibleByDesa);

document.getElementById("dash-pelanggan").innerText = plgFiltered.length;

let unpaidList = tagihanFiltered.filter(t => getVal(t, ['Status']) !== "Lunas");

let paidList = tagihanFiltered.filter(t => getVal(t, ['Status']) === "Lunas");

document.getElementById("dash-unpaid").innerText = unpaidList.length;

document.getElementById("dash-paid").innerText = paidList.length;

document.getElementById("dash-catat").innerText = catatFiltered.length;

renderDashboardFinance();

let recentList = document.getElementById("recentCatatList");

recentList.innerHTML = "";

let recent = catatFiltered.slice(-5).reverse();

if(recent.length === 0) {

recentList.innerHTML = `<li class="list-group-item text-muted text-center py-3">Belum ada data</li>`;

} else {

recent.forEach(c => {

let idPlg = getVal(c, ['ID_Pelanggan', 'ID Pelanggan']);

let nama = getNamaPelanggan(idPlg);

let m3 = getVal(c, ['Pemakaian_M3', 'Pemakaian M3', 'Pemakaian']);

let per = formatPeriodeBulanTahun(getVal(c, ['Periode']));

recentList.innerHTML += `

<li class="list-group-item d-flex justify-content-between align-items-center">

<div>

<strong>${nama}</strong> <small class="text-muted">(${idPlg})</small><br>

<small class="text-secondary">${per}</small>

</div>

<span class="badge bg-primary rounded-pill">${m3} m </span>

</li>`;

});

}

renderChart();

}

function renderChart() {

let ctx = document.getElementById('usageChart');

if (!ctx) return;

let catatFiltered = globalData.pencatatan.filter(isRowVisibleByDesa);

let usagePerPeriod = {};

catatFiltered.forEach(c => {

let p = getVal(c, ['Periode']);

let val = Number(getVal(c, ['Pemakaian_M3', 'Pemakaian M3', 'Pemakaian'], 0));

if (p) usagePerPeriod[p] = (usagePerPeriod[p] || 0) + val;

});

let sortedPeriods = Object.keys(usagePerPeriod).sort();

let labels = sortedPeriods.map(formatPeriodeBulanTahun);

let data = sortedPeriods.map(p => usagePerPeriod[p]);

if (usageChartInstance) usageChartInstance.destroy();

usageChartInstance = new Chart(ctx, {

type: 'line',

data: {

labels: labels,

datasets: [{

label: 'Total Pemakaian Air (m )',

data: data,

borderColor: '#0d6efd',

backgroundColor: 'rgba(13, 110, 253, 0.1)',

fill: true,

tension: 0.3

}]

},

options: {

responsive: true,

maintainAspectRatio: false,

plugins: { legend: { display: false } },

scales: { y: { beginAtZero: true } }

}

});

}

function renderPelanggan() {

let tbody = document.querySelector("#tablePelanggan tbody");

if(!tbody) return;

tbody.innerHTML = "";

let filtered = globalData.pelanggan.filter(isRowVisibleByDesa);

filtered.forEach(p => {

let id = getVal(p, ['ID_Pelanggan', 'ID Pelanggan', 'ID']);

let nama = getVal(p, ['Nama_Pelanggan', 'Nama Pelanggan', 'Nama']);

let alamat = getVal(p, ['Alamat']);

let desa = getVal(p, ['Desa', 'Gampong'], 'Suak Sigadeng');

let gol = getVal(p, ['Golongan']);

let status = getVal(p, ['Status'], 'Aktif');

let kordinat = getVal(p, ['Koordinat', 'Kordinat', 'Maps'], '-');

let rIdx = p._rowIndex;

let statusBadge = status.toLowerCase() === 'aktif' ? '<span class="badge bg-success">Aktif</span>' : '<span class="badge bg-secondary">Nonaktif</span>';

let qrBtn = `<button class="btn btn-sm btn-outline-dark" onclick="printHouseSticker('${id}', '${nama}', '${gol}', '${alamat}', '${desa}')"><i class="fa-solid fa-qrcode me-1"></i> Cetak QR</button>`;

tbody.innerHTML += `

<tr>

<td><b>${id}</b></td>

<td>${nama}</td>

<td>${alamat}</td>

<td><span class="badge bg-light text-dark border">${desa}</span></td>

<td>${gol}</td>

<td><small class="text-muted"><i class="fa-solid fa-location-dot me-1 text-danger"></i>${kordinat}</small></td>

<td>${statusBadge}</td>

<td>${qrBtn}</td>

<td>

<button class="btn btn-sm btn-warning me-1" onclick="editPelanggan(${rIdx})"><i class="fa-solid fa-pen"></i></button>

<button class="btn btn-sm btn-danger" onclick="confirmDelete('Pelanggan', ${rIdx})"><i class="fa-solid fa-trash"></i></button>

</td>

</tr>`;

});

}

function renderTarif() {

let tbody = document.querySelector("#tableTarif tbody");

if(!tbody) return;

tbody.innerHTML = "";

globalData.tarif.forEach(t => {

let gol = getVal(t, ['Golongan']);

let tarifM3 = Number(getVal(t, ['Tarif_Per_M3', 'Tarif Per M3', 'Tarif'])).toLocaleString('id-ID');

let admin = Number(getVal(t, ['Biaya_Admin', 'Biaya Admin', 'Admin'])).toLocaleString('id-ID');

let ket = getVal(t, ['Keterangan'], '-');

let rIdx = t._rowIndex;

tbody.innerHTML += `

<tr>

<td><b>${gol}</b></td>

<td>Rp ${tarifM3}</td>

<td>Rp ${admin}</td>

<td>${ket}</td>

<td>

<button class="btn btn-sm btn-warning me-1" onclick="editTarif(${rIdx})"><i class="fa-solid fa-pen"></i></button>

<button class="btn btn-sm btn-danger" onclick="confirmDelete('Tarif', ${rIdx})"><i class="fa-solid fa-trash"></i></button>

</td>

</tr>`;

});

}

function renderCatat() {

let tbody = document.querySelector("#tableCatat tbody");

if(!tbody) return;

tbody.innerHTML = "";

let q = (document.getElementById("searchCatat") ? document.getElementById("searchCatat").value : "").toLowerCase();

let filtered = globalData.pencatatan.filter(isRowVisibleByDesa).filter(c => {

if(!q) return true;

let idC = getVal(c, ['ID_Catat', 'ID Catat']).toLowerCase();

let idP = getVal(c, ['ID_Pelanggan', 'ID Pelanggan']).toLowerCase();

let nama = getNamaPelanggan(getVal(c, ['ID_Pelanggan', 'ID Pelanggan'])).toLowerCase();

let desa = getVal(c, ['Desa', 'Gampong']).toLowerCase();

let per = getVal(c, ['Periode']).toLowerCase();

let pet = getVal(c, ['Petugas']).toLowerCase();

return idC.includes(q) || idP.includes(q) || nama.includes(q) || desa.includes(q) || per.includes(q) || pet.includes(q);

});

filtered.reverse().forEach(c => {

let idCatat = getVal(c, ['ID_Catat', 'ID Catat']);

let per = formatPeriodeBulanTahun(getVal(c, ['Periode']));

let idPlg = getVal(c, ['ID_Pelanggan', 'ID Pelanggan']);

let nama = getNamaPelanggan(idPlg);

let desa = getVal(c, ['Desa', 'Gampong'], 'Suak Sigadeng');

let mAwal = getVal(c, ['Meter_Awal', 'Meter Awal']);

let mAkhir = getVal(c, ['Meter_Akhir', 'Meter Akhir']);

let m3 = getVal(c, ['Pemakaian_M3', 'Pemakaian M3', 'Pemakaian']);

let petugas = getVal(c, ['Petugas'], '-');

let tgl = formatDateToIndo(getVal(c, ['Tanggal_Catat', 'Tanggal Catat', 'Tanggal']));

let rIdx = c._rowIndex;

tbody.innerHTML += `

<tr>

<td><b>${idCatat}</b></td>

<td>${per}</td>

<td>${idPlg}</td>

<td>${nama}</td>

<td><span class="badge bg-light text-dark border">${desa}</span></td>

<td>${mAwal}</td>

<td>${mAkhir}</td>

<td><span class="badge bg-primary fs-7">${m3} m </span></td>

<td>${petugas}</td>

<td><small class="text-muted">${tgl}</small></td>

<td>

<button class="btn btn-sm btn-warning me-1" onclick="editCatat(${rIdx})"><i class="fa-solid fa-pen"></i></button>

<button class="btn btn-sm btn-danger" onclick="confirmDelete('Pencatatan_Meter', ${rIdx})"><i class="fa-solid fa-trash"></i></button>

</td>

</tr>`;

});

}

function renderTagihan(statusFilterInput = null) {

let tbody = document.querySelector("#tableTagihan tbody");

if(!tbody) return;

tbody.innerHTML = "";

let q = (document.getElementById("searchTagihan") ? document.getElementById("searchTagihan").value : "").toLowerCase();

let badgeBox = document.getElementById("filterBadge");

if (statusFilterInput) {

if(badgeBox) {

badgeBox.classList.remove("d-none");

document.getElementById("filterStatusText").innerText = "Filter Status: " + statusFilterInput;

}

} else {

if(badgeBox) badgeBox.classList.add("d-none");

}

let filtered = globalData.tagihan.filter(isRowVisibleByDesa).filter(t => {

if (statusFilterInput) {

let st = getVal(t, ['Status']);

if (statusFilterInput === 'Lunas' && st !== 'Lunas') return false;

if (statusFilterInput === 'Belum Lunas' && st === 'Lunas') return false;

}

if (!q) return true;

let idT = getVal(t, ['ID_Tagihan', 'ID Tagihan']).toLowerCase();

let idP = getVal(t, ['ID_Pelanggan', 'ID Pelanggan']).toLowerCase();

let nama = getNamaPelanggan(getVal(t, ['ID_Pelanggan', 'ID Pelanggan'])).toLowerCase();

let desa = getVal(t, ['Desa', 'Gampong']).toLowerCase();

let per = getVal(t, ['Periode']).toLowerCase();

let st = getVal(t, ['Status']).toLowerCase();

return idT.includes(q) || idP.includes(q) || nama.includes(q) || desa.includes(q) || per.includes(q) || st.includes(q);

});

filtered.reverse().forEach(t => {

let idTagihan = getVal(t, ['ID_Tagihan', 'ID Tagihan']);

let idPlg = getVal(t, ['ID_Pelanggan', 'ID Pelanggan']);

let nama = getNamaPelanggan(idPlg);

let desa = getVal(t, ['Desa', 'Gampong'], 'Suak Sigadeng');

let per = formatPeriodeBulanTahun(getVal(t, ['Periode']));

let m3 = getVal(t, ['Pemakaian_M3', 'Pemakaian M3', 'Pemakaian']);

let total = Number(getVal(t, ['Total_Bayar', 'Total Bayar', 'Total'])).toLocaleString('id-ID');

let st = getVal(t, ['Status']);

let tglBayar = formatDateToIndo(getVal(t, ['Tanggal_Bayar', 'Tanggal Bayar']));

let statusBtn = st.toLowerCase() === 'lunas'

? `<span class="badge bg-success"><i class="fa-solid fa-check me-1"></i>Lunas</span>`

: `<button class="btn btn-sm btn-outline-danger fw-bold" onclick="doBayar('${idTagihan}')"><i class="fa-solid fa-hand-holding-dollar me-1"></i>Bayar</button>`;

let printBtn = `<button class="btn btn-sm btn-outline-primary" onclick="printThermalReceipt('${idTagihan}')"><i class="fa-solid fa-print me-1"></i>Struk</button>`;

tbody.innerHTML += `

<tr>

<td><b>${idTagihan}</b></td>

<td>${idPlg}</td>

<td>${nama}</td>

<td><span class="badge bg-light text-dark border">${desa}</span></td>

<td>${per}</td>

<td>${m3} m </td>

<td><b>Rp ${total}</b></td>

<td>${statusBtn}</td>

<td><small class="text-muted">${tglBayar}</small></td>

<td>${printBtn}</td>

</tr>`;

});

}

// MANAJEMEN USERS: HANYA TAMPIL SESUAI DESA ATAU AKSES ROLE (SUPER ADMIN BISA LIHAT SEMUA)

function renderUsers() {

let tbody = document.querySelector("#tableUsers tbody");

if(!tbody) return;

tbody.innerHTML = "";

let filtered = globalData.users.filter(isRowVisibleByDesa);

filtered.forEach(u => {

let user = getVal(u, ['Username']);

let nama = getVal(u, ['Nama_Lengkap', 'Nama Lengkap', 'Nama']);

let desa = getVal(u, ['Desa'], 'Semua Desa');

let status = getVal(u, ['Status'], 'Aktif');

let role = getVal(u, ['Role'], 'Admin');

let rIdx = u._rowIndex;

let stBadge = status.toLowerCase() === 'aktif' ? '<span class="badge bg-success">Aktif</span>' : '<span class="badge bg-secondary">Nonaktif</span>';

tbody.innerHTML += `

<tr>

<td><b>${user}</b></td>

<td>${nama}</td>

<td><span class="badge bg-info text-dark">${desa}</span></td>

<td>${stBadge}</td>

<td><span class="badge bg-primary">${role}</span></td>

<td>

<button class="btn btn-sm btn-warning me-1" onclick="editUser(${rIdx})"><i class="fa-solid fa-pen"></i></button>

<button class="btn btn-sm btn-danger" onclick="confirmDelete('Akun', ${rIdx})"><i class="fa-solid fa-trash"></i></button>

</td>

</tr>`;

});

}

function initPeriodeOptions() {

let select = document.getElementById("selectPeriodeLaporan");

if (!select) return;

select.innerHTML = `<option value="ALL">-- Semua Periode --</option>`;

let tagihanFiltered = globalData.tagihan.filter(isRowVisibleByDesa);

let periodes = [...new Set(tagihanFiltered.map(t => getVal(t, ['Periode'])))].filter(Boolean).sort().reverse();

periodes.forEach(per => {

let label = formatPeriodeBulanTahun(per);

select.innerHTML += `<option value="${per}">${label}</option>`;

});

}

function renderLaporanBulanan() {

let select = document.getElementById("selectPeriodeLaporan");

let selectedPeriode = select ? select.value : "ALL";

let tbody = document.querySelector("#tableLaporan tbody");

if(!tbody) return;

tbody.innerHTML = "";

let desaUser = currentUser ? (currentUser.desa || "Semua Desa") : "Semua Desa";

let titleSubHeader = desaUser === "Semua Desa" ? "SPAM SWAKELOLA GAMPONG ACEH BARAT" : "SPAM SWAKELOLA DESA " + desaUser.toUpperCase();

document.getElementById("lapSubHeaderTitle").innerText = titleSubHeader;

document.getElementById("lblPeriodeLaporan").innerText = "Periode: " + (selectedPeriode === "ALL" ? "Semua Periode" : formatPeriodeBulanTahun(selectedPeriode));

document.getElementById("lapTanggalCetak").innerText = formatDateToIndo(new Date());

if(currentUser) {

document.getElementById("lapNamaPengelola").innerText = currentUser.nama || currentUser.username;

document.getElementById("lapKasirRoleLabel").innerText = currentUser.role + " - Desa " + desaUser;

document.getElementById("lapPengelolaSignTitle").innerText = "Pengelola SPAM Desa " + (desaUser === "Semua Desa" ? "Bina Bersama" : desaUser);

}

let tagihanFiltered = globalData.tagihan.filter(isRowVisibleByDesa);

let list = tagihanFiltered.filter(t => {

if (selectedPeriode === "ALL") return true;

return getVal(t, ['Periode']) === selectedPeriode;

});

let totalM3 = 0, totalAir = 0, totalAdmin = 0, totalBayarAll = 0;

let countLunas = 0, countUnpaid = 0, totalRupiahLunas = 0;

list.forEach((t, idx) => {

let idT = getVal(t, ['ID_Tagihan', 'ID Tagihan']);

let idP = getVal(t, ['ID_Pelanggan', 'ID Pelanggan']);

let nama = getNamaPelanggan(idP);

let desa = getVal(t, ['Desa', 'Gampong'], 'Suak Sigadeng');

let per = formatPeriodeBulanTahun(getVal(t, ['Periode']));

let m3 = Number(getVal(t, ['Pemakaian_M3', 'Pemakaian M3', 'Pemakaian'], 0));

let bAir = Number(getVal(t, ['Biaya_Air', 'Biaya Air'], 0));

let bAdmin = Number(getVal(t, ['Biaya_Admin', 'Biaya Admin'], 0));

let tot = Number(getVal(t, ['Total_Bayar', 'Total Bayar', 'Total'], 0));

let st = getVal(t, ['Status'], 'Belum Lunas');

let tglB = formatDateToIndo(getVal(t, ['Tanggal_Bayar', 'Tanggal Bayar']));

let kasir = getVal(t, ['Kasir'], '-');

totalM3 += m3;

totalAir += bAir;

totalAdmin += bAdmin;

totalBayarAll += tot;

if (st.toLowerCase() === 'lunas') {

countLunas++;

totalRupiahLunas += tot;

} else {

countUnpaid++;

}

let stBadge = st.toLowerCase() === 'lunas' ? '<span class="badge bg-success">Lunas</span>' : '<span class="badge bg-danger">Belum Lunas</span>';

tbody.innerHTML += `

<tr>

<td>${idx + 1}</td>

<td><b>${idT}</b></td>

<td>${idP}</td>

<td class="text-start">${nama}</td>

<td>${desa}</td>

<td>${per}</td>

<td>${m3}</td>

<td>Rp ${bAir.toLocaleString('id-ID')}</td>

<td>Rp ${bAdmin.toLocaleString('id-ID')}</td>

<td><b>Rp ${tot.toLocaleString('id-ID')}</b></td>

<td>${stBadge}</td>

<td>${tglB}</td>

<td>${kasir}</td>

</tr>`;

});

document.getElementById("lap-total-tagihan").innerText = list.length;

document.getElementById("lap-total-lunas").innerText = countLunas;

document.getElementById("lap-total-unpaid").innerText = countUnpaid;

document.getElementById("lap-total-rupiah").innerText = "Rp " + totalRupiahLunas.toLocaleString('id-ID');

document.getElementById("footM3").innerText = totalM3 + " m ";

document.getElementById("footBiayaAir").innerText = "Rp " + totalAir.toLocaleString('id-ID');

document.getElementById("footBiayaAdmin").innerText = "Rp " + totalAdmin.toLocaleString('id-ID');

document.getElementById("footTotalBayar").innerText = "Rp " + totalBayarAll.toLocaleString('id-ID');

}

function downloadLaporanPDF() {

window.print();

}

// Dynamic Pop-up Form Engine (Kriteria & Visual Form Tetap Utuh)

function openModalPelanggan(data = null, rowIndex = 0) {

activeSheet = "Pelanggan";

activeRowIndex = rowIndex;

document.getElementById("modalTitle").innerText = rowIndex ? "Edit Data Pelanggan" : "Tambah Pelanggan Baru";

let userDesa = currentUser ? currentUser.desa : "Suak Sigadeng";

let isSuperAdmin = currentUser && (currentUser.role.toLowerCase() === 'super admin' || currentUser.role.toLowerCase() === 'superadmin');

let desaOptions = LIST_DESA_ENUM.filter(d => d !== "Semua Desa").map(d => `<option value="${d}" ${data && getVal(data,['Desa'])===d ? 'selected':(!data && userDesa===d?'selected':'')}>${d}</option>`).join('');

let golOptions = globalData.tarif.map(t => `<option value="${t.Golongan}" ${data && getVal(data,['Golongan'])===t.Golongan?'selected':''}>${t.Golongan}</option>`).join('');

let bodyHtml = `

<form id="dynamicForm">

${rowIndex ? `<div class="mb-2"><label class="form-label small fw-bold">ID Pelanggan</label><input type="text" class="form-control" name="ID_Pelanggan" value="${getVal(data,['ID_Pelanggan', 'ID Pelanggan'])}" readonly></div>` : ''}

<div class="mb-2"><label class="form-label small fw-bold">Nama Pelanggan</label><input type="text" class="form-control" name="Nama_Pelanggan" value="${getVal(data,['Nama_Pelanggan', 'Nama Pelanggan'])}" required></div>

<div class="mb-2"><label class="form-label small fw-bold">Alamat / Nomor Rumah</label><input type="text" class="form-control" name="Alamat" value="${getVal(data,['Alamat'])}" required></div>

<div class="mb-2"><label class="form-label small fw-bold">Desa / Gampong</label>

<select class="form-select" name="Desa" ${!isSuperAdmin && userDesa !== 'Semua Desa' ? 'disabled' : ''}>${desaOptions}</select>

${!isSuperAdmin && userDesa !== 'Semua Desa' ? `<input type="hidden" name="Desa" value="${userDesa}">` : ''}

</div>

<div class="mb-2"><label class="form-label small fw-bold">Golongan Tarif</label><select class="form-select" name="Golongan">${golOptions}</select></div>

<div class="mb-2"><label class="form-label small fw-bold">Status Pelanggan</label>

<select class="form-select" name="Status">

<option value="Aktif" ${data && getVal(data,['Status'])==='Aktif'?'selected':''}>Aktif</option>

<option value="Nonaktif" ${data && getVal(data,['Status'])==='Nonaktif'?'selected':''}>Nonaktif</option>

</select>

</div>

<div class="mb-2">

<label class="form-label small fw-bold">Koordinat Lokasi (Latitude, Longitude)</label>

<div class="input-group">

<input type="text" class="form-control" id="formKoordinat" name="Koordinat" value="${getVal(data,['Koordinat', 'Kordinat'], '-')}">

<button type="button" class="btn btn-outline-secondary" onclick="getCurrentLocation()"><i class="fa-solid fa-crosshairs"></i> GPS</button>

</div>

<div id="leafletMapPicker"></div>

</div>

</form>`;

document.getElementById("modalBody").innerHTML = bodyHtml;

let modal = new bootstrap.Modal(document.getElementById("formModal"));

modal.show();

setTimeout(initLeafletPicker, 400);

}

function initLeafletPicker() {

let defaultLat = -4.1500;

let defaultLng = 96.1300;

let existingCoord = document.getElementById("formKoordinat").value;

if (existingCoord && existingCoord.includes(',')) {

let parts = existingCoord.split(',');

if (!isNaN(parts[0]) && !isNaN(parts[1])) {

defaultLat = parseFloat(parts[0]);

defaultLng = parseFloat(parts[1]);

}

}

if (leafletMap) { leafletMap.remove(); }

leafletMap = L.map('leafletMapPicker').setView([defaultLat, defaultLng], 14);

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(leafletMap);

leafletMarker = L.marker([defaultLat, defaultLng], { draggable: true }).addTo(leafletMap);

leafletMarker.on('dragend', function (e) {

let latlng = e.target.getLatLng();

document.getElementById("formKoordinat").value = latlng.lat.toFixed(6) + ", " + latlng.lng.toFixed(6);

});

leafletMap.on('click', function (e) {

leafletMarker.setLatLng(e.latlng);

document.getElementById("formKoordinat").value = e.latlng.lat.toFixed(6) + ", " + e.latlng.lng.toFixed(6);

});

}

function getCurrentLocation() {

if (navigator.geolocation) {

navigator.geolocation.getCurrentPosition(function (pos) {

let lat = pos.coords.latitude.toFixed(6);

let lng = pos.coords.longitude.toFixed(6);

document.getElementById("formKoordinat").value = lat + ", " + lng;

if (leafletMap && leafletMarker) {

leafletMap.setView([lat, lng], 16);

leafletMarker.setLatLng([lat, lng]);

}

}, function (err) {

alert("Gagal mendapatkan lokasi GPS: " + err.message);

});

} else {

alert("Geolocation tidak didukung oleh browser ini.");

}

}

function editPelanggan(rowIndex) {

let p = globalData.pelanggan.find(item => item._rowIndex === rowIndex);

if (p) openModalPelanggan(p, rowIndex);

}

function openModalTarif(data = null, rowIndex = 0) {

activeSheet = "Tarif";

activeRowIndex = rowIndex;

document.getElementById("modalTitle").innerText = rowIndex ? "Edit Tarif" : "Tambah Tarif Baru";

let bodyHtml = `

<form id="dynamicForm">

<div class="mb-2"><label class="form-label small fw-bold">Golongan Tarif</label><input type="text" class="form-control" name="Golongan" value="${getVal(data,['Golongan'])}" required></div>

<div class="mb-2"><label class="form-label small fw-bold">Tarif Per m (Rp)</label><input type="number" class="form-control" name="Tarif_Per_M3" value="${getVal(data,['Tarif_Per_M3', 'Tarif Per M3'])}" required></div>

<div class="mb-2"><label class="form-label small fw-bold">Biaya Administrasi (Rp)</label><input type="number" class="form-control" name="Biaya_Admin" value="${getVal(data,['Biaya_Admin', 'Biaya Admin'])}" required></div>

<div class="mb-2"><label class="form-label small fw-bold">Keterangan</label><input type="text" class="form-control" name="Keterangan" value="${getVal(data,['Keterangan'])}"></div>

</form>`;

document.getElementById("modalBody").innerHTML = bodyHtml;

let modal = new bootstrap.Modal(document.getElementById("formModal"));

modal.show();

}

function editTarif(rowIndex) {

let t = globalData.tarif.find(item => item._rowIndex === rowIndex);

if (t) openModalTarif(t, rowIndex);

}

function openModalCatat(data = null, rowIndex = 0) {

activeSheet = "Pencatatan_Meter";

activeRowIndex = rowIndex;

document.getElementById("modalTitle").innerText = rowIndex ? "Edit Catat Meter" : "Input Pencatatan Meter Air";

let pelangganFiltered = globalData.pelanggan.filter(isRowVisibleByDesa);

let plgOptions = pelangganFiltered.map(p => {

let idP = getVal(p, ['ID_Pelanggan', 'ID Pelanggan']);

let nm = getVal(p, ['Nama_Pelanggan', 'Nama Pelanggan']);

let ds = getVal(p, ['Desa', 'Gampong']);

return `<option value="${idP}" ${data && getVal(data,['ID_Pelanggan', 'ID Pelanggan'])===idP ? 'selected':''}>${idP} - ${nm} (${ds})</option>`;

}).join('');

let nowMonth = new Date().toISOString().substring(0, 7);

let todayStr = new Date().toISOString().substring(0, 10);

let petugasName = currentUser ? (currentUser.nama || currentUser.username) : "Petugas";

let userDesa = currentUser ? currentUser.desa : "Suak Sigadeng";

let isSuperAdmin = currentUser && (currentUser.role.toLowerCase() === 'super admin' || currentUser.role.toLowerCase() === 'superadmin');

let desaOptions = LIST_DESA_ENUM.filter(d => d !== "Semua Desa").map(d => `<option value="${d}" ${data && getVal(data,['Desa'])===d ? 'selected':(!data && userDesa===d?'selected':'')}>${d}</option>`).join('');

let bodyHtml = `

<form id="dynamicForm">

${rowIndex ? `<div class="mb-2"><label class="form-label small fw-bold">ID Catat</label><input type="text" class="form-control" name="ID_Catat" value="${getVal(data,['ID_Catat', 'ID Catat'])}" readonly></div>` : ''}

<div class="mb-2"><label class="form-label small fw-bold">Periode (Tahun-Bulan)</label><input type="month" class="form-control" name="Periode" value="${data ? getVal(data,['Periode']) : nowMonth}" required></div>

<div class="mb-2"><label class="form-label small fw-bold">Pilih Pelanggan</label><select class="form-select" name="ID_Pelanggan" onchange="autoFillMeterAwal(this.value)">${plgOptions}</select></div>

<div class="mb-2"><label class="form-label small fw-bold">Desa / Gampong</label>

<select class="form-select" name="Desa" ${!isSuperAdmin && userDesa !== 'Semua Desa' ? 'disabled' : ''}>${desaOptions}</select>

${!isSuperAdmin && userDesa !== 'Semua Desa' ? `<input type="hidden" name="Desa" value="${userDesa}">` : ''}

</div>

<div class="row g-2 mb-2">

<div class="col-6"><label class="form-label small fw-bold">Meter Awal</label><input type="number" class="form-control" id="inputMeterAwal" name="Meter_Awal" value="${data ? getVal(data,['Meter_Awal', 'Meter Awal']) : 0}" required></div>

<div class="col-6"><label class="form-label small fw-bold">Meter Akhir</label><input type="number" class="form-control" id="inputMeterAkhir" name="Meter_Akhir" value="${data ? getVal(data,['Meter_Akhir', 'Meter Akhir']) : 0}" required></div>

</div>

<div class="mb-2"><label class="form-label small fw-bold">Petugas Pencatat</label><input type="text" class="form-control" name="Petugas" value="${data ? getVal(data,['Petugas']) : petugasName}" required></div>

<div class="mb-2"><label class="form-label small fw-bold">Tanggal Catat</label><input type="date" class="form-control" name="Tanggal_Catat" value="${todayStr}"></div>

</form>`;

document.getElementById("modalBody").innerHTML = bodyHtml;

let modal = new bootstrap.Modal(document.getElementById("formModal"));

modal.show();

if (!data && pelangganFiltered.length > 0) {

autoFillMeterAwal(getVal(pelangganFiltered[0], ['ID_Pelanggan', 'ID Pelanggan']));

}

}

function autoFillMeterAwal(idPelanggan) {

let myCatat = globalData.pencatatan.filter(c => getVal(c, ['ID_Pelanggan', 'ID Pelanggan']) === idPelanggan);

let meterAwalEl = document.getElementById("inputMeterAwal");

if (myCatat.length > 0) {

let lastRecord = myCatat[myCatat.length - 1];

if (meterAwalEl) meterAwalEl.value = getVal(lastRecord, ['Meter_Akhir', 'Meter Akhir'], 0);

} else {

if (meterAwalEl) meterAwalEl.value = 0;

}

}

function editCatat(rowIndex) {

let c = globalData.pencatatan.find(item => item._rowIndex === rowIndex);

if (c) openModalCatat(c, rowIndex);

}

function openModalUser(data = null, rowIndex = 0) {

activeSheet = "Akun";

activeRowIndex = rowIndex;

document.getElementById("modalTitle").innerText = rowIndex ? "Edit Akun User" : "Tambah Akun User";

let userDesa = currentUser ? currentUser.desa : "Suak Sigadeng";

let isSuperAdmin = currentUser && (currentUser.role.toLowerCase() === 'super admin' || currentUser.role.toLowerCase() === 'superadmin');

let desaOptions = LIST_DESA_ENUM.map(d => `<option value="${d}" ${data && getVal(data,['Desa'])===d ? 'selected':(!data && !isSuperAdmin && userDesa===d?'selected':'')}>${d}</option>`).join('');

let bodyHtml = `

<form id="dynamicForm">

<div class="mb-2"><label class="form-label small fw-bold">Username</label><input type="text" class="form-control" name="Username" value="${getVal(data,['Username'])}" ${rowIndex ? 'readonly': 'required'}></div>

<div class="mb-2"><label class="form-label small fw-bold">Password</label><input type="password" class="form-control" name="Password" value="${getVal(data,['Password'])}" required></div>

<div class="mb-2"><label class="form-label small fw-bold">Nama Lengkap</label><input type="text" class="form-control" name="Nama_Lengkap" value="${getVal(data,['Nama_Lengkap', 'Nama Lengkap', 'Nama'])}" required></div>

<div class="mb-2"><label class="form-label small fw-bold">Hak Akses Desa</label>

<select class="form-select" name="Desa" ${!isSuperAdmin && userDesa !== 'Semua Desa' ? 'disabled' : ''}>${desaOptions}</select>

${!isSuperAdmin && userDesa !== 'Semua Desa' ? `<input type="hidden" name="Desa" value="${userDesa}">` : ''}

</div>

<div class="mb-2"><label class="form-label small fw-bold">Status Akun</label>

<select class="form-select" name="Status">

<option value="Aktif" ${data && getVal(data,['Status'])==='Aktif'?'selected':''}>Aktif</option>

<option value="Nonaktif" ${data && getVal(data,['Status'])==='Nonaktif'?'selected':''}>Nonaktif</option>

</select>

</div>

<div class="mb-2"><label class="form-label small fw-bold">Role / Level Akses</label>

<select class="form-select" name="Role">

<option value="Super Admin" ${data && getVal(data,['Role'])==='Super Admin'?'selected':''}>Super Admin</option>

<option value="Admin" ${data && getVal(data,['Role'])==='Admin'?'selected':''}>Admin</option>

<option value="Kasir" ${data && getVal(data,['Role'])==='Kasir'?'selected':''}>Kasir</option>

<option value="Catat" ${data && getVal(data,['Role'])==='Catat'?'selected':''}>Catat Meter</option>

</select>

</div>

</form>`;

document.getElementById("modalBody").innerHTML = bodyHtml;

let modal = new bootstrap.Modal(document.getElementById("formModal"));

modal.show();

}

function editUser(rowIndex) {

let u = globalData.users.find(item => item._rowIndex === rowIndex);

if (u) openModalUser(u, rowIndex);

}

function submitForm() {

let form = document.getElementById("dynamicForm");

if (!form.checkValidity()) {

form.reportValidity();

return;

}

let formData = {};

let elements = form.querySelectorAll("input, select");

elements.forEach(el => {

if (el.name) formData[el.name] = el.value;

});

let btnSave = document.getElementById("btnSaveData");

btnSave.disabled = true;

btnSave.innerHTML = `<span class="spinner-border spinner-border-sm me-1"></span> Menyimpan...`;

runGasMethod("saveData", [activeSheet, formData, activeRowIndex], function(res) {

btnSave.disabled = false;

btnSave.innerHTML = "Simpan Data";

if (res && res.success) {

let modalEl = document.getElementById("formModal");

let modal = bootstrap.Modal.getInstance(modalEl);

if (modal) modal.hide();

alert(res.message);

loadAllData();

} else {

alert("Gagal menyimpan data: " + (res ? res.message : "Error"));

}

});

}

function confirmDelete(sheetName, rowIndex) {

if (confirm("Apakah Anda yakin ingin menghapus data ini secara permanen?")) {

runGasMethod("deleteData", [sheetName, rowIndex], function(res) {

if (res && res.success) {

alert(res.message);

loadAllData();

} else {

alert("Gagal menghapus data!");

}

});

}

}

function doBayar(idTagihan) {

let kasirName = currentUser ? (currentUser.nama || currentUser.username) : "Kasir Loket";

if (confirm("Konfirmasi pembayaran tagihan " + idTagihan + " oleh kasir " + kasirName + "?")) {

runGasMethod("bayarTagihan", [idTagihan, kasirName], function(res) {

if (res && res.success) {

alert(res.message);

loadAllData(function() {

printThermalReceipt(idTagihan);

});

} else {

alert("Gagal memproses pembayaran!");

}

});

}

}

// Thermal Printer Resi (58 mm)

function printThermalReceipt(idTagihan) {

let tagihan = globalData.tagihan.find(t => getVal(t, ['ID_Tagihan', 'ID Tagihan']) === idTagihan);

if (!tagihan) {

alert("Data tagihan tidak ditemukan!");

return;

}

let idPlg = getVal(tagihan, ['ID_Pelanggan', 'ID Pelanggan']);

let nama = getNamaPelanggan(idPlg);

let desa = getVal(tagihan, ['Desa', 'Gampong'], 'Suak Sigadeng');

let per = formatPeriodeBulanTahun(getVal(tagihan, ['Periode']));

let mAwal = getVal(tagihan, ['Meter_Awal', 'Meter Awal'], 0);

let mAkhir = getVal(tagihan, ['Meter_Akhir', 'Meter Akhir'], 0);

let m3 = getVal(tagihan, ['Pemakaian_M3', 'Pemakaian M3', 'Pemakaian'], 0);

let bAir = Number(getVal(tagihan, ['Biaya_Air', 'Biaya Air'], 0)).toLocaleString('id-ID');

let bAdmin = Number(getVal(tagihan, ['Biaya_Admin', 'Biaya Admin'], 0)).toLocaleString('id-ID');

let total = Number(getVal(tagihan, ['Total_Bayar', 'Total Bayar', 'Total'], 0)).toLocaleString('id-ID');

let st = getVal(tagihan, ['Status'], 'LUNAS');

let kasir = getVal(tagihan, ['Kasir'], '-');

let tgl = formatDateToIndo(getVal(tagihan, ['Tanggal_Bayar', 'Tanggal Bayar']));

document.getElementById("rDesaHeader").innerText = "SPAM DESA " + desa.toUpperCase();

document.getElementById("rTgl").innerText = tgl;

document.getElementById("rId").innerText = idTagihan;

document.getElementById("rNama").innerText = nama;

document.getElementById("rIdPlg").innerText = idPlg;

document.getElementById("rPeriode").innerText = per;

document.getElementById("rMtrAwal").innerText = mAwal;

document.getElementById("rMtrAkhir").innerText = mAkhir;

document.getElementById("rPakai").innerText = m3;

document.getElementById("rBiayaAir").innerText = bAir;

document.getElementById("rBiayaAdmin").innerText = bAdmin;

document.getElementById("rTotal").innerText = total;

document.getElementById("rStatus").innerText = st.toUpperCase();

document.getElementById("rKasir").innerText = kasir;

let pBox = document.getElementById("receiptPrint");

pBox.classList.remove("d-none");

window.print();

pBox.classList.add("d-none");

}

function printHouseSticker(idPlg, nama, gol, alamat, desa) {

document.getElementById("stkDesaHeader").innerText = "SPAM DESA " + desa.toUpperCase();

document.getElementById("stkId").innerText = idPlg;

document.getElementById("stkNama").innerText = nama;

document.getElementById("stkGol").innerText = gol;

document.getElementById("stkAlamat").innerText = alamat;

let qrApi = "https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=" + encodeURIComponent(idPlg);

document.getElementById("stkQrImg").src = qrApi;

let pBox = document.getElementById("houseStickerPrint");

pBox.classList.remove("d-none");

setTimeout(function() {

window.print();

pBox.classList.add("d-none");

}, 400);

}

// QR Code Scanner Module

function startQRScanner() {

let box = document.getElementById("qrScannerContainer");

box.classList.remove("d-none");

if (html5QrCodeScanner) {

html5QrCodeScanner.clear();

}

html5QrCodeScanner = new Html5QrcodeScanner("reader", { fps: 10, qrbox: 250 });

html5QrCodeScanner.render(onScanSuccess, onScanFailure);

}

function stopQRScanner() {

if (html5QrCodeScanner) {

html5QrCodeScanner.clear();

}

document.getElementById("qrScannerContainer").classList.add("d-none");

}

function onScanSuccess(decodedText, decodedResult) {

stopQRScanner();

let cleanID = decodedText.trim().toUpperCase();

let plg = globalData.pelanggan.find(p => getVal(p, ['ID_Pelanggan', 'ID Pelanggan', 'ID']).toUpperCase() === cleanID);

if (plg) {

openModalCatat({ ID_Pelanggan: cleanID }, 0);

} else {

alert("Kode QR Pelanggan '" + cleanID + "' tidak ditemukan dalam database!");

}

}

function onScanFailure(error) {

// Handling kegagalan scan pasif

}

// Inisialisasi Aplikasi Saat Ditemukan

window.addEventListener('DOMContentLoaded', function() {

// Inisialisasi Awal

});

</script>

</body>

</html>