Files
2026-06-11 23:54:47 +07:00

1911 lines
66 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin WebGIS Pontianak</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
crossorigin=""
/>
<link
rel="stylesheet"
href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css"
/>
<style>
:root {
--accent: #2563eb;
--accent-h: #1d4ed8;
--open: #16a34a;
--closed: #dc2626;
--warn: #f59e0b;
--bg: #f1f5f9;
--bg-card: #ffffff;
--border: #e2e8f0;
--text: #0f172a;
--muted: #64748b;
--radius: 16px;
--shadow: 0 4px 24px rgba(15, 23, 42, 0.1);
--jalan-nasional: #dc2626;
--jalan-provinsi: #ea580c;
--jalan-kota: #2563eb;
--bangunan-shm: #16a34a;
--bangunan-hgb: #7c3aed;
--bangunan-hgu: #b45309;
--bangunan-hp: #0891b2;
--bangunan-hpl: #be185d;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Inter", sans-serif;
background: var(--bg);
color: var(--text);
}
/* ── Nav ── */
.topnav {
position: sticky;
top: 0;
z-index: 200;
display: flex;
align-items: center;
gap: 12px;
padding: 0 24px;
height: 62px;
background: var(--text);
color: #fff;
box-shadow: var(--shadow);
}
.topnav a {
color: #93c5fd;
text-decoration: none;
font-size: 0.88rem;
}
.topnav a:hover {
text-decoration: underline;
}
.topnav h1 {
margin: 0;
font-size: 1.05rem;
flex: 1;
}
/* ── Tabs ── */
.tab-bar {
display: flex;
gap: 4px;
padding: 16px 20px 0;
max-width: 1200px;
margin: 0 auto;
}
.tab-btn {
padding: 10px 20px;
border: 1px solid var(--border);
border-bottom: 0;
border-radius: 12px 12px 0 0;
background: #e2e8f0;
color: var(--muted);
font: inherit;
font-weight: 600;
font-size: 0.88rem;
cursor: pointer;
transition: 0.15s;
}
.tab-btn.active {
background: var(--bg-card);
color: var(--accent);
border-color: var(--border);
}
.tab-btn:hover:not(.active) {
background: #cbd5e1;
}
/* ── Layout ── */
.page {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px 60px;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* ── Alert ── */
.alert {
display: none;
padding: 13px 16px;
border-radius: 12px;
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 18px;
margin-top: 16px;
}
.alert.show {
display: block;
}
.alert.ok {
background: #dcfce7;
color: #14532d;
}
.alert.err {
background: #fee2e2;
color: #7f1d1d;
}
/* ── Card ── */
.card {
background: var(--bg-card);
border-radius: 0 var(--radius) var(--radius) var(--radius);
border: 1px solid var(--border);
box-shadow: var(--shadow);
margin-bottom: 28px;
}
.card.no-tab-radius {
border-radius: var(--radius);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 18px 22px;
border-bottom: 1px solid var(--border);
}
.card-header h2 {
margin: 0;
font-size: 1rem;
}
.card-body {
padding: 22px;
}
/* ── Map container ── */
.map-input-wrap {
display: grid;
grid-template-columns: 1fr 340px;
gap: 20px;
align-items: start;
}
.draw-map {
height: 420px;
border-radius: 12px;
border: 2px solid var(--border);
overflow: hidden;
position: relative;
}
.map-hint {
padding: 10px 12px;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 10px;
font-size: 0.82rem;
color: #1e40af;
margin-bottom: 12px;
line-height: 1.5;
}
.map-hint strong {
display: block;
margin-bottom: 4px;
}
.coords-display {
padding: 10px 12px;
background: #f8fafc;
border: 1px solid var(--border);
border-radius: 10px;
font-size: 0.78rem;
color: var(--muted);
font-family: monospace;
min-height: 54px;
word-break: break-all;
margin-top: 8px;
}
/* ── Form ── */
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 14px;
}
.form-group {
display: grid;
gap: 6px;
}
.form-group.full {
grid-column: 1 / -1;
}
.form-group label {
font-size: 0.82rem;
font-weight: 700;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 11px;
font: inherit;
font-size: 0.92rem;
color: var(--text);
background: #f8fafc;
outline: none;
transition:
border-color 0.15s,
box-shadow 0.15s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 18px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 42px;
padding: 0 18px;
border: 0;
border-radius: 11px;
font: inherit;
font-weight: 700;
font-size: 0.9rem;
cursor: pointer;
transition: 0.15s;
}
.btn-primary {
background: var(--accent);
color: #fff;
}
.btn-primary:hover {
background: var(--accent-h);
}
.btn-ghost {
background: #f1f5f9;
color: var(--text);
border: 1px solid var(--border);
}
.btn-ghost:hover {
background: #e2e8f0;
}
.btn-danger {
background: #fee2e2;
color: var(--closed);
border: 1px solid #fecaca;
}
.btn-danger:hover {
background: #fecaca;
}
.btn-sm {
min-height: 34px;
padding: 0 12px;
font-size: 0.82rem;
}
/* ── Table ── */
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
}
th,
td {
padding: 11px 13px;
border-bottom: 1px solid var(--border);
text-align: left;
vertical-align: middle;
}
th {
background: #f8fafc;
color: var(--muted);
font-size: 0.74rem;
text-transform: uppercase;
letter-spacing: 0.07em;
white-space: nowrap;
}
tr:last-child td {
border-bottom: 0;
}
tr:hover td {
background: #f8fafc;
}
.td-actions {
display: flex;
gap: 6px;
}
.badge {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 4px 10px;
font-size: 0.72rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.03em;
color: #fff;
white-space: nowrap;
}
.badge.open {
background: var(--open);
}
.badge.closed {
background: var(--closed);
}
/* Jalan badges */
.badge.nasional {
background: var(--jalan-nasional);
}
.badge.provinsi {
background: var(--jalan-provinsi);
}
.badge.kota_kabupaten {
background: var(--jalan-kota);
}
/* Bangunan badges */
.badge.SHM {
background: var(--bangunan-shm);
}
.badge.HGB {
background: var(--bangunan-hgb);
}
.badge.HGU {
background: var(--bangunan-hgu);
}
.badge.HP {
background: var(--bangunan-hp);
}
.badge.HPL {
background: var(--bangunan-hpl);
}
.fuel-pill {
display: inline-flex;
border-radius: 999px;
padding: 3px 8px;
background: #dbeafe;
color: #1e3a8a;
font-size: 0.7rem;
font-weight: 700;
margin: 2px;
}
.empty-row td {
text-align: center;
color: var(--muted);
padding: 32px;
}
/* ── Fuel checks (SPBU tab) ── */
.fuel-checks {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.fuel-check {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 12px;
border: 1px solid var(--border);
border-radius: 999px;
cursor: pointer;
font-size: 0.84rem;
transition: 0.15s;
}
.fuel-check input {
accent-color: var(--accent);
}
.fuel-check:has(input:checked) {
border-color: var(--accent);
background: #eff6ff;
}
/* ── Modal ── */
.modal-bg {
display: none;
position: fixed;
inset: 0;
z-index: 500;
background: rgba(15, 23, 42, 0.45);
backdrop-filter: blur(3px);
align-items: center;
justify-content: center;
}
.modal-bg.open {
display: flex;
}
.modal {
background: var(--bg-card);
border-radius: 20px;
box-shadow: 0 24px 56px rgba(15, 23, 42, 0.22);
width: min(480px, calc(100vw - 32px));
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
position: sticky;
top: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--border);
background: var(--bg-card);
}
.modal-header h3 {
margin: 0;
font-size: 1rem;
}
.modal-body {
padding: 20px;
}
.close-modal {
width: 32px;
height: 32px;
border: 0;
border-radius: 8px;
background: #f1f5f9;
cursor: pointer;
font-size: 1.1rem;
font-weight: 900;
}
/* ── Map SPBU (pin klik) ── */
.spbu-map-wrap {
grid-column: 1 / -1;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
align-items: start;
}
.spbu-map-box {
border: 2px solid var(--border);
border-radius: 12px;
overflow: hidden;
height: 300px;
position: relative;
cursor: crosshair;
}
.spbu-map-box.has-pin {
border-color: var(--accent);
}
.spbu-coords-hint {
padding: 9px 12px;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 10px;
font-size: 0.82rem;
color: #1e40af;
margin-bottom: 10px;
line-height: 1.5;
}
.spbu-coords-hint strong {
display: block;
margin-bottom: 3px;
}
.coords-pill {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 12px;
background: #f8fafc;
border: 1px solid var(--border);
border-radius: 10px;
font-size: 0.82rem;
color: var(--muted);
font-family: monospace;
margin-top: 8px;
}
.coords-pill.filled {
color: var(--text);
border-color: var(--accent);
background: #eff6ff;
}
.coords-pill span {
flex: 1;
}
.coords-clear {
width: 22px;
height: 22px;
border: 0;
border-radius: 6px;
background: #e2e8f0;
cursor: pointer;
font-size: 0.85rem;
line-height: 1;
display: none;
}
.coords-pill.filled .coords-clear {
display: inline-flex;
align-items: center;
justify-content: center;
}
/* ── Responsive ── */
@media (max-width: 860px) {
.map-input-wrap {
grid-template-columns: 1fr;
}
.spbu-map-wrap {
grid-template-columns: 1fr;
}
}
@media (max-width: 600px) {
.form-grid {
grid-template-columns: 1fr;
}
.topnav h1 {
font-size: 0.9rem;
}
.tab-btn {
padding: 8px 12px;
font-size: 0.8rem;
}
}
</style>
</head>
<body>
<nav class="topnav">
<a href="/">← Kembali ke Peta</a>
<h1>Panel Admin WebGIS Pontianak</h1>
</nav>
<!-- ── Tabs ── -->
<div class="tab-bar">
<button class="tab-btn active" data-tab="tab-spbu" type="button">
⛽ SPBU
</button>
<button class="tab-btn" data-tab="tab-jalan" type="button">
🛣️ Jalan
</button>
<button class="tab-btn" data-tab="tab-bangunan" type="button">
🏠 Bangunan
</button>
</div>
<div class="page">
<!-- ══════════════════════════════════════════════════════
TAB SPBU (kode lama, tidak diubah)
══════════════════════════════════════════════════════ -->
<div class="tab-content active" id="tab-spbu">
<div class="alert" id="alertSpbu"></div>
<div class="card">
<div class="card-header">
<h2 id="spbuFormTitle"> Tambah SPBU Baru</h2>
<button
class="btn btn-ghost btn-sm"
id="resetSpbuBtn"
type="button"
style="display: none"
>
Batalkan Edit
</button>
</div>
<div class="card-body">
<input type="hidden" id="editSpbuId" />
<div class="form-grid">
<div class="form-group">
<label for="fKode">Kode SPBU *</label>
<input id="fKode" type="text" placeholder="Contoh: 64.781.10" />
</div>
<div class="form-group">
<label for="fNama">Nama SPBU *</label>
<input
id="fNama"
type="text"
placeholder="Contoh: SPBU 64.781.10 Sudirman"
/>
</div>
<div class="form-group full">
<label for="fAlamat">Alamat *</label>
<input
id="fAlamat"
type="text"
placeholder="Jl. ..., Kelurahan, Kecamatan"
/>
</div>
<!-- Peta pin SPBU -->
<div class="spbu-map-wrap">
<div>
<div class="spbu-coords-hint">
<strong>📍 Tentukan lokasi SPBU di peta</strong>
Klik langsung di peta untuk menempatkan pin lokasi SPBU.
Klik di tempat lain untuk memindahkan pin.
</div>
<div id="mapSpbu" class="spbu-map-box"></div>
<div class="coords-pill" id="coordsPillSpbu">
<span id="coordsTextSpbu"
>Belum ada pin. Klik di peta untuk menentukan
lokasi.</span
>
<button
class="coords-clear"
id="clearPinSpbu"
title="Hapus pin"
type="button"
>
×
</button>
</div>
</div>
<!-- Input lat/lng tetap ada tapi read-only dan tersembunyi untuk keperluan submit -->
<div style="display: flex; flex-direction: column; gap: 10px">
<div class="form-group">
<label>Latitude (otomatis dari peta)</label>
<input
id="fLat"
type="text"
placeholder="Klik peta untuk mengisi"
readonly
style="
background: #f1f5f9;
cursor: not-allowed;
color: var(--muted);
"
/>
</div>
<div class="form-group">
<label>Longitude (otomatis dari peta)</label>
<input
id="fLng"
type="text"
placeholder="Klik peta untuk mengisi"
readonly
style="
background: #f1f5f9;
cursor: not-allowed;
color: var(--muted);
"
/>
</div>
<p
style="
margin: 4px 0 0;
font-size: 0.78rem;
color: var(--muted);
line-height: 1.5;
"
>
Koordinat diisi otomatis saat kamu klik peta. Tidak perlu
mengetik secara manual.
</p>
</div>
</div>
<div class="form-group">
<label for="fJamBuka">Jam Buka *</label>
<input id="fJamBuka" type="time" value="06:00" />
</div>
<div class="form-group">
<label for="fJamTutup">Jam Tutup *</label>
<input id="fJamTutup" type="time" value="22:00" />
</div>
<div class="form-group">
<label for="fOverride">Override Status</label>
<select id="fOverride">
<option value="auto">Auto (ikuti jam)</option>
<option value="open">Force Open</option>
<option value="closed">Force Closed</option>
</select>
</div>
<div class="form-group full">
<label>Jenis BBM Tersedia *</label>
<div class="fuel-checks" id="fuelChecks"></div>
</div>
</div>
<div class="form-actions">
<button class="btn btn-primary" id="submitSpbuBtn" type="button">
💾 Simpan SPBU
</button>
</div>
</div>
</div>
<div class="card no-tab-radius">
<div class="card-header">
<h2>📋 Daftar SPBU</h2>
<span
id="spbuCount"
style="color: var(--muted); font-size: 0.88rem"
></span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>#</th>
<th>Kode</th>
<th>Nama & Alamat</th>
<th>Koordinat</th>
<th>Jam Operasi</th>
<th>Jenis BBM</th>
<th>Status</th>
<th>Aksi</th>
</tr>
</thead>
<tbody id="spbuTableBody">
<tr class="empty-row">
<td colspan="8">Memuat data...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- /tab-spbu -->
<!-- ══════════════════════════════════════════════════════
TAB JALAN
══════════════════════════════════════════════════════ -->
<div class="tab-content" id="tab-jalan">
<div class="alert" id="alertJalan"></div>
<div class="card">
<div class="card-header">
<h2 id="jalanFormTitle"> Tambah Jalan Baru</h2>
<button
class="btn btn-ghost btn-sm"
id="resetJalanBtn"
type="button"
style="display: none"
>
Batalkan Edit
</button>
</div>
<div class="card-body">
<input type="hidden" id="editJalanId" />
<div class="map-input-wrap">
<!-- Peta draw -->
<div>
<div class="map-hint">
<strong>🖊️ Cara menggambar polyline jalan:</strong>
Klik tombol <em>Draw a polyline</em> di peta (ikon garis di
kiri peta), lalu klik titik-titik jalur jalan. Klik dua kali
untuk mengakhiri gambar. Klik <em>Edit layers</em> untuk
mengubah, atau <em>Delete layers</em> untuk menghapus.
</div>
<div id="mapJalan" class="draw-map"></div>
<div class="coords-display" id="coordsJalan">
Belum ada koordinat. Gambar polyline di peta terlebih dahulu.
</div>
</div>
<!-- Form atribut -->
<div>
<div class="form-grid" style="grid-template-columns: 1fr">
<div class="form-group">
<label for="fNamaJalan">Nama Jalan *</label>
<input
id="fNamaJalan"
type="text"
placeholder="Contoh: Jl. Ahmad Yani"
/>
</div>
<div class="form-group">
<label for="fJenisJalan">Jenis Jalan *</label>
<select id="fJenisJalan">
<option value="nasional">Nasional</option>
<option value="provinsi">Provinsi</option>
<option value="kota_kabupaten" selected>
Kota / Kabupaten
</option>
</select>
</div>
</div>
<div class="form-actions" style="margin-top: 14px">
<button
class="btn btn-primary"
id="submitJalanBtn"
type="button"
>
💾 Simpan Jalan
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card no-tab-radius">
<div class="card-header">
<h2>🛣️ Daftar Jalan</h2>
<span
id="jalanCount"
style="color: var(--muted); font-size: 0.88rem"
></span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>#</th>
<th>Nama Jalan</th>
<th>Jenis</th>
<th>Titik Koordinat</th>
<th>Aksi</th>
</tr>
</thead>
<tbody id="jalanTableBody">
<tr class="empty-row">
<td colspan="5">Memuat data...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- /tab-jalan -->
<!-- ══════════════════════════════════════════════════════
TAB BANGUNAN
══════════════════════════════════════════════════════ -->
<div class="tab-content" id="tab-bangunan">
<div class="alert" id="alertBangunan"></div>
<div class="card">
<div class="card-header">
<h2 id="bangunanFormTitle"> Tambah Bangunan Baru</h2>
<button
class="btn btn-ghost btn-sm"
id="resetBangunanBtn"
type="button"
style="display: none"
>
Batalkan Edit
</button>
</div>
<div class="card-body">
<input type="hidden" id="editBangunanId" />
<div class="map-input-wrap">
<!-- Peta draw -->
<div>
<div class="map-hint">
<strong>🖊️ Cara menggambar polygon bangunan:</strong>
Klik tombol <em>Draw a polygon</em> di peta (ikon segi banyak
di kiri peta), lalu klik sudut-sudut bangunan. Klik titik
pertama untuk menutup polygon. Klik <em>Edit layers</em> untuk
mengubah, atau <em>Delete layers</em> untuk menghapus.
</div>
<div id="mapBangunan" class="draw-map"></div>
<div class="coords-display" id="coordsBangunan">
Belum ada koordinat. Gambar polygon di peta terlebih dahulu.
</div>
</div>
<!-- Form atribut -->
<div>
<div class="form-grid" style="grid-template-columns: 1fr">
<div class="form-group">
<label for="fNamaBangunan">Nama Bangunan *</label>
<input
id="fNamaBangunan"
type="text"
placeholder="Contoh: Ruko Ahmad Yani No. 5"
/>
</div>
<div class="form-group">
<label for="fJenisBangunan">Jenis / Hak *</label>
<select id="fJenisBangunan">
<option value="SHM">SHM Sertifikat Hak Milik</option>
<option value="HGB">HGB Hak Guna Bangunan</option>
<option value="HGU">HGU Hak Guna Usaha</option>
<option value="HP">HP Hak Pakai</option>
<option value="HPL">HPL Hak Pengelolaan</option>
</select>
</div>
<div class="form-group">
<label for="fNamaJalanBangunan">Nama Jalan</label>
<input
id="fNamaJalanBangunan"
type="text"
placeholder="Contoh: Jl. Gajah Mada"
/>
</div>
</div>
<div class="form-actions" style="margin-top: 14px">
<button
class="btn btn-primary"
id="submitBangunanBtn"
type="button"
>
💾 Simpan Bangunan
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card no-tab-radius">
<div class="card-header">
<h2>🏠 Daftar Bangunan</h2>
<span
id="bangunanCount"
style="color: var(--muted); font-size: 0.88rem"
></span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>#</th>
<th>Nama Bangunan</th>
<th>Jenis/Hak</th>
<th>Nama Jalan</th>
<th>Titik Koordinat</th>
<th>Aksi</th>
</tr>
</thead>
<tbody id="bangunanTableBody">
<tr class="empty-row">
<td colspan="6">Memuat data...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- /tab-bangunan -->
</div>
<!-- /page -->
<!-- ── Konfirmasi Hapus ── -->
<div class="modal-bg" id="deleteModal">
<div class="modal">
<div class="modal-header">
<h3 id="deleteModalTitle">🗑️ Konfirmasi Hapus</h3>
<button class="close-modal" id="closeDeleteModal" type="button">
×
</button>
</div>
<div class="modal-body">
<p>
Apakah Anda yakin ingin menghapus <strong id="deleteName"></strong>?
</p>
<p style="color: var(--muted); font-size: 0.88rem">
Data tidak akan hilang permanen (soft delete), namun tidak akan
tampil di peta.
</p>
<div class="form-actions">
<button class="btn btn-danger" id="confirmDeleteBtn" type="button">
Ya, Hapus
</button>
<button class="btn btn-ghost" id="cancelDeleteBtn" type="button">
Batal
</button>
</div>
</div>
</div>
</div>
<script
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
crossorigin=""
></script>
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
<script>
// ══════════════════════════════════════════════════════════════════════════════
// ─── Shared helpers ───────────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════════
const PONTIANAK_CENTER = [-0.0263, 109.3425];
let alertTimers = {};
function showAlert(scopeId, msg, type = "ok") {
const el = document.getElementById(scopeId);
el.textContent = msg;
el.className = `alert show ${type}`;
clearTimeout(alertTimers[scopeId]);
alertTimers[scopeId] = setTimeout(
() => el.classList.remove("show"),
5500,
);
el.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
// Delete modal state
let deleteState = { id: null, endpoint: null, onDone: null, name: "" };
function openDeleteModal(id, name, endpoint, onDone) {
deleteState = { id, endpoint, onDone, name };
document.getElementById("deleteName").textContent = name;
document.getElementById("deleteModal").classList.add("open");
}
document
.getElementById("confirmDeleteBtn")
.addEventListener("click", async () => {
if (!deleteState.id) return;
try {
const res = await fetch(
`${deleteState.endpoint}/${deleteState.id}`,
{ method: "DELETE" },
);
const json = await res.json();
if (!json.success) throw new Error(json.message);
document.getElementById("deleteModal").classList.remove("open");
if (deleteState.onDone) deleteState.onDone();
} catch (err) {
alert("Gagal menghapus: " + err.message);
} finally {
deleteState = { id: null, endpoint: null, onDone: null };
}
});
document
.getElementById("cancelDeleteBtn")
.addEventListener("click", () =>
document.getElementById("deleteModal").classList.remove("open"),
);
document
.getElementById("closeDeleteModal")
.addEventListener("click", () =>
document.getElementById("deleteModal").classList.remove("open"),
);
document.getElementById("deleteModal").addEventListener("click", (e) => {
if (e.target === document.getElementById("deleteModal"))
document.getElementById("deleteModal").classList.remove("open");
});
// ─── Tab switching ────────────────────────────────────────────────────────────
let mapsInitialized = { spbu: false, jalan: false, bangunan: false };
document.querySelectorAll(".tab-btn").forEach((btn) => {
btn.addEventListener("click", () => {
document
.querySelectorAll(".tab-btn")
.forEach((b) => b.classList.remove("active"));
document
.querySelectorAll(".tab-content")
.forEach((t) => t.classList.remove("active"));
btn.classList.add("active");
const tabId = btn.dataset.tab;
document.getElementById(tabId).classList.add("active");
// Init peta saat tab dibuka pertama kali (Leaflet butuh container visible)
if (tabId === "tab-spbu" && !mapsInitialized.spbu) {
setTimeout(() => {
initSpbuMap();
mapsInitialized.spbu = true;
}, 80);
}
if (tabId === "tab-jalan" && !mapsInitialized.jalan) {
setTimeout(() => {
initJalanMap();
mapsInitialized.jalan = true;
}, 80);
}
if (tabId === "tab-bangunan" && !mapsInitialized.bangunan) {
setTimeout(() => {
initBangunanMap();
mapsInitialized.bangunan = true;
}, 80);
}
});
});
// ══════════════════════════════════════════════════════════════════════════════
// ─── PETA SPBU (pin klik) ────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════════
let spbuMap, spbuPinMarker;
function initSpbuMap() {
spbuMap = L.map("mapSpbu").setView(PONTIANAK_CENTER, 13);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(spbuMap);
spbuMap.on("click", (e) => {
const { lat, lng } = e.latlng;
setSpbuPin(lat, lng);
});
}
function setSpbuPin(lat, lng) {
const latR = parseFloat(lat.toFixed(6));
const lngR = parseFloat(lng.toFixed(6));
// Hapus marker lama, pasang yang baru
if (spbuPinMarker) spbuMap.removeLayer(spbuPinMarker);
spbuPinMarker = L.marker([latR, lngR], {
icon: L.divIcon({
className: "",
html: `<div style="
width:36px;height:36px;display:grid;place-items:center;
background:#dc2626;border:3px solid white;border-radius:50% 50% 50% 10%;
transform:rotate(-45deg);box-shadow:0 6px 18px rgba(0,0,0,.3);">
<span style="transform:rotate(45deg);font-size:18px">⛽</span>
</div>`,
iconSize: [36, 36],
iconAnchor: [18, 34],
}),
draggable: true,
}).addTo(spbuMap);
// Drag marker juga update koordinat
spbuPinMarker.on("dragend", (ev) => {
const pos = ev.target.getLatLng();
setSpbuPin(pos.lat, pos.lng);
});
// Update input fields & pill
spbuEl.fLat.value = latR;
spbuEl.fLng.value = lngR;
document.getElementById("coordsTextSpbu").textContent =
`Lat: ${latR} | Lng: ${lngR}`;
const pill = document.getElementById("coordsPillSpbu");
pill.classList.add("filled");
document.getElementById("mapSpbu").classList.add("has-pin");
}
function clearSpbuPin() {
if (spbuPinMarker) {
spbuMap.removeLayer(spbuPinMarker);
spbuPinMarker = null;
}
spbuEl.fLat.value = "";
spbuEl.fLng.value = "";
document.getElementById("coordsTextSpbu").textContent =
"Belum ada pin. Klik di peta untuk menentukan lokasi.";
document.getElementById("coordsPillSpbu").classList.remove("filled");
document.getElementById("mapSpbu").classList.remove("has-pin");
}
document
.getElementById("clearPinSpbu")
.addEventListener("click", clearSpbuPin);
// ══════════════════════════════════════════════════════════════════════════════
// ─── TAB SPBU ────────────────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════════
const API_SPBU = "/api/spbu";
const fuelList = [
"Pertalite",
"Pertamax",
"Pertamax Turbo",
"Solar",
"Dexlite",
"Pertamina Dex",
];
let allSpbu = [];
const spbuEl = {
formTitle: document.getElementById("spbuFormTitle"),
resetBtn: document.getElementById("resetSpbuBtn"),
editId: document.getElementById("editSpbuId"),
fKode: document.getElementById("fKode"),
fNama: document.getElementById("fNama"),
fAlamat: document.getElementById("fAlamat"),
fLat: document.getElementById("fLat"),
fLng: document.getElementById("fLng"),
fJamBuka: document.getElementById("fJamBuka"),
fJamTutup: document.getElementById("fJamTutup"),
fOverride: document.getElementById("fOverride"),
fuelChecks: document.getElementById("fuelChecks"),
submitBtn: document.getElementById("submitSpbuBtn"),
tableBody: document.getElementById("spbuTableBody"),
count: document.getElementById("spbuCount"),
};
function renderFuelChecks(selected = []) {
spbuEl.fuelChecks.innerHTML = fuelList
.map(
(f) => `
<label class="fuel-check">
<input type="checkbox" value="${f}" ${selected.includes(f) ? "checked" : ""} />
${f}
</label>`,
)
.join("");
}
function getCheckedFuels() {
return [...spbuEl.fuelChecks.querySelectorAll("input:checked")].map(
(i) => i.value,
);
}
function getWibMin() {
const d = new Date();
const w = new Date(
d.getTime() + d.getTimezoneOffset() * 60000 + 7 * 3600000,
);
return w.getHours() * 60 + w.getMinutes();
}
function parseMin(t) {
const [h, m] = t.split(":").map(Number);
return h * 60 + m;
}
function isOpen(spbu, now = getWibMin()) {
const o = parseMin(spbu.jam_buka),
c = parseMin(spbu.jam_tutup);
return o <= c ? now >= o && now <= c : now >= o || now <= c;
}
function getStatus(spbu) {
if (spbu.manual_override === "open") return "open";
if (spbu.manual_override === "closed") return "closed";
return isOpen(spbu) ? "open" : "closed";
}
async function loadSpbu() {
try {
const res = await fetch(API_SPBU);
const json = await res.json();
if (!json.success) throw new Error(json.message);
allSpbu = json.data;
renderSpbuTable();
} catch (err) {
spbuEl.tableBody.innerHTML = `<tr class="empty-row"><td colspan="8">❌ Gagal memuat: ${err.message}</td></tr>`;
}
}
function renderSpbuTable() {
spbuEl.count.textContent = `${allSpbu.length} SPBU`;
if (!allSpbu.length) {
spbuEl.tableBody.innerHTML = `<tr class="empty-row"><td colspan="8">Belum ada data.</td></tr>`;
return;
}
spbuEl.tableBody.innerHTML = allSpbu
.map((spbu, i) => {
const status = getStatus(spbu);
const fuels = spbu.jenis_bbm
.map((f) => `<span class="fuel-pill">${f}</span>`)
.join("");
return `<tr>
<td>${i + 1}</td>
<td><strong>${spbu.kode}</strong></td>
<td><strong>${spbu.nama}</strong><br><span style="color:var(--muted);font-size:.8rem">${spbu.alamat}</span></td>
<td style="font-size:.8rem;font-variant-numeric:tabular-nums">${spbu.lat}, ${spbu.lng}</td>
<td style="white-space:nowrap">${spbu.jam_buka} ${spbu.jam_tutup}</td>
<td>${fuels}</td>
<td><span class="badge ${status}">${status === "open" ? "BUKA" : "TUTUP"}</span></td>
<td><div class="td-actions">
<button class="btn btn-ghost btn-sm" onclick="startEditSpbu(${spbu.id})">✏️ Edit</button>
<button class="btn btn-danger btn-sm" onclick="openDeleteModal(${spbu.id},'${spbu.nama.replace(/'/g, "\\'")}','${API_SPBU}',() => { showAlert('alertSpbu','✅ SPBU berhasil dihapus.'); loadSpbu(); })">🗑️</button>
</div></td>
</tr>`;
})
.join("");
}
function resetSpbuForm() {
spbuEl.editId.value = "";
spbuEl.fKode.value = spbuEl.fNama.value = spbuEl.fAlamat.value = "";
spbuEl.fJamBuka.value = "06:00";
spbuEl.fJamTutup.value = "22:00";
spbuEl.fOverride.value = "auto";
renderFuelChecks([]);
clearSpbuPin();
spbuEl.formTitle.textContent = " Tambah SPBU Baru";
spbuEl.submitBtn.textContent = "💾 Simpan SPBU";
spbuEl.resetBtn.style.display = "none";
}
function startEditSpbu(id) {
const spbu = allSpbu.find((s) => s.id === id);
if (!spbu) return;
spbuEl.editId.value = spbu.id;
spbuEl.fKode.value = spbu.kode;
spbuEl.fNama.value = spbu.nama;
spbuEl.fAlamat.value = spbu.alamat;
spbuEl.fJamBuka.value = spbu.jam_buka;
spbuEl.fJamTutup.value = spbu.jam_tutup;
spbuEl.fOverride.value = spbu.manual_override || "auto";
renderFuelChecks(spbu.jenis_bbm);
spbuEl.formTitle.textContent = `✏️ Edit SPBU: ${spbu.nama}`;
spbuEl.submitBtn.textContent = "💾 Update SPBU";
spbuEl.resetBtn.style.display = "";
window.scrollTo({ top: 0, behavior: "smooth" });
// Set pin di peta — inisialisasi peta dulu kalau belum
const doPin = () => {
setSpbuPin(spbu.lat, spbu.lng);
spbuMap.setView([spbu.lat, spbu.lng], 15);
};
if (!mapsInitialized.spbu) {
setTimeout(() => {
initSpbuMap();
mapsInitialized.spbu = true;
setTimeout(doPin, 200);
}, 80);
} else {
doPin();
}
}
async function submitSpbu() {
const id = spbuEl.editId.value;
const fuels = getCheckedFuels();
const payload = {
kode: spbuEl.fKode.value.trim(),
nama: spbuEl.fNama.value.trim(),
alamat: spbuEl.fAlamat.value.trim(),
lat: parseFloat(spbuEl.fLat.value),
lng: parseFloat(spbuEl.fLng.value),
jam_buka: spbuEl.fJamBuka.value,
jam_tutup: spbuEl.fJamTutup.value,
jenis_bbm: fuels,
manual_override: spbuEl.fOverride.value,
};
if (
!payload.kode ||
!payload.nama ||
!payload.alamat ||
isNaN(payload.lat) ||
isNaN(payload.lng)
) {
showAlert("alertSpbu", "⚠️ Lengkapi semua field wajib.", "err");
return;
}
if (!fuels.length) {
showAlert("alertSpbu", "⚠️ Pilih minimal satu jenis BBM.", "err");
return;
}
spbuEl.submitBtn.disabled = true;
spbuEl.submitBtn.textContent = "⏳ Menyimpan...";
try {
const res = await fetch(id ? `${API_SPBU}/${id}` : API_SPBU, {
method: id ? "PUT" : "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const json = await res.json();
if (!json.success) throw new Error(json.message);
showAlert(
"alertSpbu",
id
? "✅ SPBU berhasil diperbarui."
: "✅ SPBU baru berhasil ditambahkan.",
);
resetSpbuForm();
await loadSpbu();
} catch (err) {
showAlert("alertSpbu", "❌ Gagal menyimpan: " + err.message, "err");
} finally {
spbuEl.submitBtn.disabled = false;
spbuEl.submitBtn.textContent = spbuEl.editId.value
? "💾 Update SPBU"
: "💾 Simpan SPBU";
}
}
spbuEl.submitBtn.addEventListener("click", submitSpbu);
spbuEl.resetBtn.addEventListener("click", resetSpbuForm);
renderFuelChecks([]);
loadSpbu();
// Tab SPBU aktif saat load — init petanya langsung
setTimeout(() => {
initSpbuMap();
mapsInitialized.spbu = true;
}, 120);
// ══════════════════════════════════════════════════════════════════════════════
// ─── TAB JALAN ───────────────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════════
const API_JALAN = "/api/jalan";
let allJalan = [];
let jalanMap, jalanDrawLayer, jalanDrawControl;
let currentJalanCoords = null; // GeoJSON LineString coordinates array
const JALAN_COLORS = {
nasional: "#dc2626",
provinsi: "#ea580c",
kota_kabupaten: "#2563eb",
};
const JALAN_LABEL = {
nasional: "Nasional",
provinsi: "Provinsi",
kota_kabupaten: "Kota/Kabupaten",
};
function initJalanMap() {
jalanMap = L.map("mapJalan").setView(PONTIANAK_CENTER, 13);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(jalanMap);
jalanDrawLayer = new L.FeatureGroup().addTo(jalanMap);
jalanDrawControl = new L.Control.Draw({
edit: { featureGroup: jalanDrawLayer },
draw: {
polyline: { shapeOptions: { color: "#2563eb", weight: 4 } },
polygon: false,
rectangle: false,
circle: false,
circlemarker: false,
marker: false,
},
});
jalanMap.addControl(jalanDrawControl);
jalanMap.on(L.Draw.Event.CREATED, (e) => {
jalanDrawLayer.clearLayers();
jalanDrawLayer.addLayer(e.layer);
const geojson = e.layer.toGeoJSON();
currentJalanCoords = geojson.geometry.coordinates; // [[lng,lat], ...]
document.getElementById("coordsJalan").textContent =
`${currentJalanCoords.length} titik — ${JSON.stringify(currentJalanCoords).slice(0, 120)}...`;
});
jalanMap.on(L.Draw.Event.EDITED, (e) => {
e.layers.eachLayer((layer) => {
const geojson = layer.toGeoJSON();
currentJalanCoords = geojson.geometry.coordinates;
document.getElementById("coordsJalan").textContent =
`${currentJalanCoords.length} titik — ${JSON.stringify(currentJalanCoords).slice(0, 120)}...`;
});
});
jalanMap.on(L.Draw.Event.DELETED, () => {
currentJalanCoords = null;
document.getElementById("coordsJalan").textContent =
"Koordinat dihapus. Gambar ulang polyline.";
});
}
function resetJalanForm() {
document.getElementById("editJalanId").value = "";
document.getElementById("fNamaJalan").value = "";
document.getElementById("fJenisJalan").value = "kota_kabupaten";
currentJalanCoords = null;
document.getElementById("coordsJalan").textContent =
"Belum ada koordinat. Gambar polyline di peta.";
if (jalanDrawLayer) jalanDrawLayer.clearLayers();
document.getElementById("jalanFormTitle").textContent =
" Tambah Jalan Baru";
document.getElementById("submitJalanBtn").textContent =
"💾 Simpan Jalan";
document.getElementById("resetJalanBtn").style.display = "none";
}
function startEditJalan(id) {
const jalan = allJalan.find((j) => j.id === id);
if (!jalan) return;
// Switch to jalan tab
document
.querySelectorAll(".tab-btn")
.forEach((b) => b.classList.remove("active"));
document
.querySelectorAll(".tab-content")
.forEach((t) => t.classList.remove("active"));
document
.querySelector('[data-tab="tab-jalan"]')
.classList.add("active");
document.getElementById("tab-jalan").classList.add("active");
const doEdit = () => {
document.getElementById("editJalanId").value = jalan.id;
document.getElementById("fNamaJalan").value = jalan.nama_jalan;
document.getElementById("fJenisJalan").value = jalan.jenis_jalan;
// Gambar ulang polyline di peta
const coords = jalan.koordinat; // [[lng,lat],...]
currentJalanCoords = coords;
if (jalanDrawLayer) {
jalanDrawLayer.clearLayers();
const latLngs = coords.map((c) => [c[1], c[0]]);
const pl = L.polyline(latLngs, { color: "#2563eb", weight: 4 });
jalanDrawLayer.addLayer(pl);
jalanMap.fitBounds(pl.getBounds(), { padding: [30, 30] });
}
document.getElementById("coordsJalan").textContent =
`${coords.length} titik — ${JSON.stringify(coords).slice(0, 120)}...`;
document.getElementById("jalanFormTitle").textContent =
`✏️ Edit Jalan: ${jalan.nama_jalan}`;
document.getElementById("submitJalanBtn").textContent =
"💾 Update Jalan";
document.getElementById("resetJalanBtn").style.display = "";
window.scrollTo({ top: 0, behavior: "smooth" });
};
if (!mapsInitialized.jalan) {
setTimeout(() => {
initJalanMap();
mapsInitialized.jalan = true;
setTimeout(doEdit, 200);
}, 80);
} else {
doEdit();
}
}
async function submitJalan() {
const id = document.getElementById("editJalanId").value;
const namaJalan = document.getElementById("fNamaJalan").value.trim();
const jenisJalan = document.getElementById("fJenisJalan").value;
if (!namaJalan) {
showAlert("alertJalan", "⚠️ Nama jalan wajib diisi.", "err");
return;
}
if (!currentJalanCoords || currentJalanCoords.length < 2) {
showAlert(
"alertJalan",
"⚠️ Gambar polyline di peta terlebih dahulu (minimal 2 titik).",
"err",
);
return;
}
const payload = {
nama_jalan: namaJalan,
jenis_jalan: jenisJalan,
koordinat: { type: "LineString", coordinates: currentJalanCoords },
};
const btn = document.getElementById("submitJalanBtn");
btn.disabled = true;
btn.textContent = "⏳ Menyimpan...";
try {
const res = await fetch(id ? `${API_JALAN}/${id}` : API_JALAN, {
method: id ? "PUT" : "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const json = await res.json();
if (!json.success) throw new Error(json.message);
showAlert(
"alertJalan",
id
? "✅ Jalan berhasil diperbarui."
: "✅ Jalan baru berhasil ditambahkan.",
);
resetJalanForm();
await loadJalan();
} catch (err) {
showAlert("alertJalan", "❌ Gagal menyimpan: " + err.message, "err");
} finally {
btn.disabled = false;
btn.textContent = document.getElementById("editJalanId").value
? "💾 Update Jalan"
: "💾 Simpan Jalan";
}
}
async function loadJalan() {
try {
const res = await fetch(API_JALAN);
const json = await res.json();
if (!json.success) throw new Error(json.message);
allJalan = json.data;
renderJalanTable();
} catch (err) {
document.getElementById("jalanTableBody").innerHTML =
`<tr class="empty-row"><td colspan="5">❌ Gagal memuat: ${err.message}</td></tr>`;
}
}
function renderJalanTable() {
document.getElementById("jalanCount").textContent =
`${allJalan.length} jalan`;
if (!allJalan.length) {
document.getElementById("jalanTableBody").innerHTML =
`<tr class="empty-row"><td colspan="5">Belum ada data jalan. Tambahkan menggunakan form di atas.</td></tr>`;
return;
}
document.getElementById("jalanTableBody").innerHTML = allJalan
.map((j, i) => {
const coords = Array.isArray(j.koordinat)
? j.koordinat
: j.koordinat.coordinates || [];
const titik = coords.length;
return `<tr>
<td>${i + 1}</td>
<td><strong>${j.nama_jalan}</strong></td>
<td><span class="badge ${j.jenis_jalan}">${JALAN_LABEL[j.jenis_jalan] || j.jenis_jalan}</span></td>
<td style="font-size:.8rem;color:var(--muted)">${titik} titik</td>
<td><div class="td-actions">
<button class="btn btn-ghost btn-sm" onclick="startEditJalan(${j.id})">✏️ Edit</button>
<button class="btn btn-danger btn-sm" onclick="openDeleteModal(${j.id},'${j.nama_jalan.replace(/'/g, "\\'")}','${API_JALAN}',() => { showAlert('alertJalan','✅ Jalan berhasil dihapus.'); loadJalan(); })">🗑️</button>
</div></td>
</tr>`;
})
.join("");
}
document
.getElementById("submitJalanBtn")
.addEventListener("click", submitJalan);
document
.getElementById("resetJalanBtn")
.addEventListener("click", resetJalanForm);
loadJalan();
// ══════════════════════════════════════════════════════════════════════════════
// ─── TAB BANGUNAN ─────────────────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════════════════
const API_BANGUNAN = "/api/bangunan";
let allBangunan = [];
let bangunanMap, bangunanDrawLayer, bangunanDrawControl;
let currentBangunanCoords = null; // GeoJSON Polygon coordinates array
const BANGUNAN_LABEL = {
SHM: "SHM",
HGB: "HGB",
HGU: "HGU",
HP: "HP",
HPL: "HPL",
};
const BANGUNAN_DESC = {
SHM: "Sertifikat Hak Milik",
HGB: "Hak Guna Bangunan",
HGU: "Hak Guna Usaha",
HP: "Hak Pakai",
HPL: "Hak Pengelolaan",
};
function initBangunanMap() {
bangunanMap = L.map("mapBangunan").setView(PONTIANAK_CENTER, 13);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(bangunanMap);
bangunanDrawLayer = new L.FeatureGroup().addTo(bangunanMap);
bangunanDrawControl = new L.Control.Draw({
edit: { featureGroup: bangunanDrawLayer },
draw: {
polygon: {
shapeOptions: {
color: "#16a34a",
fillColor: "#86efac",
fillOpacity: 0.35,
weight: 2,
},
},
polyline: false,
rectangle: {
shapeOptions: {
color: "#16a34a",
fillColor: "#86efac",
fillOpacity: 0.35,
weight: 2,
},
},
circle: false,
circlemarker: false,
marker: false,
},
});
bangunanMap.addControl(bangunanDrawControl);
bangunanMap.on(L.Draw.Event.CREATED, (e) => {
bangunanDrawLayer.clearLayers();
bangunanDrawLayer.addLayer(e.layer);
const geojson = e.layer.toGeoJSON();
currentBangunanCoords = geojson.geometry.coordinates; // [[[lng,lat],...]]
const total = currentBangunanCoords[0]
? currentBangunanCoords[0].length
: 0;
document.getElementById("coordsBangunan").textContent =
`${total} titik sudut — ${JSON.stringify(currentBangunanCoords).slice(0, 120)}...`;
});
bangunanMap.on(L.Draw.Event.EDITED, (e) => {
e.layers.eachLayer((layer) => {
const geojson = layer.toGeoJSON();
currentBangunanCoords = geojson.geometry.coordinates;
const total = currentBangunanCoords[0]
? currentBangunanCoords[0].length
: 0;
document.getElementById("coordsBangunan").textContent =
`${total} titik sudut — ${JSON.stringify(currentBangunanCoords).slice(0, 120)}...`;
});
});
bangunanMap.on(L.Draw.Event.DELETED, () => {
currentBangunanCoords = null;
document.getElementById("coordsBangunan").textContent =
"Koordinat dihapus. Gambar ulang polygon.";
});
}
function resetBangunanForm() {
document.getElementById("editBangunanId").value = "";
document.getElementById("fNamaBangunan").value = "";
document.getElementById("fJenisBangunan").value = "SHM";
document.getElementById("fNamaJalanBangunan").value = "";
currentBangunanCoords = null;
document.getElementById("coordsBangunan").textContent =
"Belum ada koordinat. Gambar polygon di peta.";
if (bangunanDrawLayer) bangunanDrawLayer.clearLayers();
document.getElementById("bangunanFormTitle").textContent =
" Tambah Bangunan Baru";
document.getElementById("submitBangunanBtn").textContent =
"💾 Simpan Bangunan";
document.getElementById("resetBangunanBtn").style.display = "none";
}
function startEditBangunan(id) {
const b = allBangunan.find((x) => x.id === id);
if (!b) return;
document
.querySelectorAll(".tab-btn")
.forEach((btn) => btn.classList.remove("active"));
document
.querySelectorAll(".tab-content")
.forEach((t) => t.classList.remove("active"));
document
.querySelector('[data-tab="tab-bangunan"]')
.classList.add("active");
document.getElementById("tab-bangunan").classList.add("active");
const doEdit = () => {
document.getElementById("editBangunanId").value = b.id;
document.getElementById("fNamaBangunan").value = b.nama_bangunan;
document.getElementById("fJenisBangunan").value = b.jenis_bangunan;
document.getElementById("fNamaJalanBangunan").value =
b.nama_jalan || "";
const coords = b.koordinat.coordinates || b.koordinat;
currentBangunanCoords = coords;
if (bangunanDrawLayer) {
bangunanDrawLayer.clearLayers();
const latLngs = coords[0].map((c) => [c[1], c[0]]);
const pg = L.polygon(latLngs, {
color: "#16a34a",
fillColor: "#86efac",
fillOpacity: 0.35,
weight: 2,
});
bangunanDrawLayer.addLayer(pg);
bangunanMap.fitBounds(pg.getBounds(), { padding: [30, 30] });
}
const total = coords[0] ? coords[0].length : 0;
document.getElementById("coordsBangunan").textContent =
`${total} titik sudut — ${JSON.stringify(coords).slice(0, 120)}...`;
document.getElementById("bangunanFormTitle").textContent =
`✏️ Edit Bangunan: ${b.nama_bangunan}`;
document.getElementById("submitBangunanBtn").textContent =
"💾 Update Bangunan";
document.getElementById("resetBangunanBtn").style.display = "";
window.scrollTo({ top: 0, behavior: "smooth" });
};
if (!mapsInitialized.bangunan) {
setTimeout(() => {
initBangunanMap();
mapsInitialized.bangunan = true;
setTimeout(doEdit, 200);
}, 80);
} else {
doEdit();
}
}
async function submitBangunan() {
const id = document.getElementById("editBangunanId").value;
const namaBangunan = document
.getElementById("fNamaBangunan")
.value.trim();
const jenisBangunan = document.getElementById("fJenisBangunan").value;
const namaJalan = document
.getElementById("fNamaJalanBangunan")
.value.trim();
if (!namaBangunan) {
showAlert("alertBangunan", "⚠️ Nama bangunan wajib diisi.", "err");
return;
}
if (!currentBangunanCoords) {
showAlert(
"alertBangunan",
"⚠️ Gambar polygon di peta terlebih dahulu.",
"err",
);
return;
}
const payload = {
nama_bangunan: namaBangunan,
jenis_bangunan: jenisBangunan,
nama_jalan: namaJalan || null,
koordinat: { type: "Polygon", coordinates: currentBangunanCoords },
};
const btn = document.getElementById("submitBangunanBtn");
btn.disabled = true;
btn.textContent = "⏳ Menyimpan...";
try {
const res = await fetch(id ? `${API_BANGUNAN}/${id}` : API_BANGUNAN, {
method: id ? "PUT" : "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const json = await res.json();
if (!json.success) throw new Error(json.message);
showAlert(
"alertBangunan",
id
? "✅ Bangunan berhasil diperbarui."
: "✅ Bangunan baru berhasil ditambahkan.",
);
resetBangunanForm();
await loadBangunan();
} catch (err) {
showAlert(
"alertBangunan",
"❌ Gagal menyimpan: " + err.message,
"err",
);
} finally {
btn.disabled = false;
btn.textContent = document.getElementById("editBangunanId").value
? "💾 Update Bangunan"
: "💾 Simpan Bangunan";
}
}
async function loadBangunan() {
try {
const res = await fetch(API_BANGUNAN);
const json = await res.json();
if (!json.success) throw new Error(json.message);
allBangunan = json.data;
renderBangunanTable();
} catch (err) {
document.getElementById("bangunanTableBody").innerHTML =
`<tr class="empty-row"><td colspan="6">❌ Gagal memuat: ${err.message}</td></tr>`;
}
}
function renderBangunanTable() {
document.getElementById("bangunanCount").textContent =
`${allBangunan.length} bangunan`;
if (!allBangunan.length) {
document.getElementById("bangunanTableBody").innerHTML =
`<tr class="empty-row"><td colspan="6">Belum ada data bangunan. Tambahkan menggunakan form di atas.</td></tr>`;
return;
}
document.getElementById("bangunanTableBody").innerHTML = allBangunan
.map((b, i) => {
const coords = b.koordinat.coordinates
? b.koordinat.coordinates
: b.koordinat;
const titik = coords[0] ? coords[0].length : 0;
return `<tr>
<td>${i + 1}</td>
<td><strong>${b.nama_bangunan}</strong></td>
<td><span class="badge ${b.jenis_bangunan}" title="${BANGUNAN_DESC[b.jenis_bangunan]}">${b.jenis_bangunan}</span></td>
<td style="font-size:.85rem">${b.nama_jalan || '<span style="color:var(--muted)">—</span>'}</td>
<td style="font-size:.8rem;color:var(--muted)">${titik} titik sudut</td>
<td><div class="td-actions">
<button class="btn btn-ghost btn-sm" onclick="startEditBangunan(${b.id})">✏️ Edit</button>
<button class="btn btn-danger btn-sm" onclick="openDeleteModal(${b.id},'${b.nama_bangunan.replace(/'/g, "\\'")}','${API_BANGUNAN}',() => { showAlert('alertBangunan','✅ Bangunan berhasil dihapus.'); loadBangunan(); })">🗑️</button>
</div></td>
</tr>`;
})
.join("");
}
document
.getElementById("submitBangunanBtn")
.addEventListener("click", submitBangunan);
document
.getElementById("resetBangunanBtn")
.addEventListener("click", resetBangunanForm);
loadBangunan();
</script>
</body>
</html>