1911 lines
66 KiB
HTML
1911 lines
66 KiB
HTML
<!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>
|