<!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 +=
"¶m" + 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>