Files
Tugas_Pertemuan1/index.php
T
2026-06-03 05:14:58 +00:00

1027 lines
43 KiB
PHP

<?php include 'koneksi.php'; ?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> WEBGIS— Peta Interaktif</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<!-- Leaflet Draw CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet"/>
<style>
/* ─── Reset & Base ─────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--c-bg: #0d1117;
--c-surface: #161b22;
--c-border: #30363d;
--c-accent: #58a6ff;
--c-green: #3fb950;
--c-red: #f85149;
--c-orange: #d29922;
--c-purple: #bc8cff;
--c-text: #e6edf3;
--c-muted: #8b949e;
--radius: 10px;
--shadow: 0 8px 32px rgba(0,0,0,.5);
}
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background: var(--c-bg);
color: var(--c-text);
height: 100vh;
overflow: hidden;
}
/* ─── Layout ────────────────────────────────────── */
#app { display: flex; height: 100vh; }
/* ─── Sidebar ───────────────────────────────────── */
#sidebar {
width: 300px;
min-width: 300px;
background: var(--c-surface);
border-right: 1px solid var(--c-border);
display: flex;
flex-direction: column;
z-index: 1000;
overflow: hidden;
}
.sidebar-header {
padding: 20px 18px 14px;
border-bottom: 1px solid var(--c-border);
background: linear-gradient(135deg, #1a2332 0%, #161b22 100%);
}
.sidebar-header h1 {
font-family: 'Space Mono', monospace;
font-size: 17px;
font-weight: 700;
color: var(--c-accent);
letter-spacing: -0.5px;
}
.sidebar-header p {
font-size: 11px;
color: var(--c-muted);
margin-top: 3px;
}
/* ─── Stats Bar ─────────────────────────────────── */
.stats-bar {
display: flex;
padding: 10px 14px;
gap: 6px;
border-bottom: 1px solid var(--c-border);
background: #0d1117;
}
.stat-chip {
flex: 1;
text-align: center;
background: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: 6px;
padding: 6px 4px;
}
.stat-chip .val {
font-family: 'Space Mono', monospace;
font-size: 18px;
font-weight: 700;
line-height: 1;
}
.stat-chip .lbl {
font-size: 9px;
color: var(--c-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 2px;
}
.stat-chip.m .val { color: var(--c-green); }
.stat-chip.l .val { color: var(--c-accent); }
.stat-chip.p .val { color: var(--c-purple); }
/* ─── Data List ─────────────────────────────────── */
.list-header {
padding: 10px 14px 6px;
font-size: 10px;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
color: var(--c-muted);
}
#data-list {
flex: 1;
overflow-y: auto;
padding: 0 8px 8px;
}
#data-list::-webkit-scrollbar { width: 4px; }
#data-list::-webkit-scrollbar-track { background: transparent; }
#data-list::-webkit-scrollbar-thumb { background: var(--c-border); border-radius: 4px; }
.data-item {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 10px;
border-radius: 7px;
border: 1px solid transparent;
cursor: pointer;
transition: all .15s ease;
margin-bottom: 3px;
}
.data-item:hover {
background: rgba(88,166,255,.08);
border-color: rgba(88,166,255,.2);
}
.data-item.active {
background: rgba(88,166,255,.12);
border-color: rgba(88,166,255,.35);
}
.item-icon {
width: 30px; height: 30px;
border-radius: 7px;
display: flex; align-items: center; justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.icon-marker { background: rgba(63,185,80,.15); color: var(--c-green); }
.icon-polyline{ background: rgba(88,166,255,.15); color: var(--c-accent); }
.icon-polygon { background: rgba(188,140,255,.15); color: var(--c-purple); }
.item-info { flex: 1; min-width: 0; }
.item-name {
font-size: 13px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-meta {
font-size: 10px;
color: var(--c-muted);
margin-top: 1px;
}
.badge-24 {
font-size: 9px;
padding: 2px 5px;
border-radius: 4px;
font-weight: 700;
flex-shrink: 0;
}
.badge-ya { background: rgba(63,185,80,.2); color: var(--c-green); }
.badge-tidak{ background: rgba(248,81,73,.15); color: var(--c-red); }
/* ─── Map ───────────────────────────────────────── */
#map {
flex: 1;
height: 100%;
}
/* ─── Leaflet Popup Custom ──────────────────────── */
.leaflet-popup-content-wrapper {
background: var(--c-surface) !important;
border: 1px solid var(--c-border) !important;
border-radius: var(--radius) !important;
box-shadow: var(--shadow) !important;
padding: 0 !important;
color: var(--c-text) !important;
}
.leaflet-popup-tip {
background: var(--c-surface) !important;
}
.leaflet-popup-content {
margin: 0 !important;
width: auto !important;
}
.leaflet-popup-close-button {
color: var(--c-muted) !important;
font-size: 18px !important;
top: 8px !important;
right: 8px !important;
}
.leaflet-popup-close-button:hover { color: var(--c-text) !important; }
/* ─── Popup Panels ──────────────────────────────── */
.pop-panel { width: 240px; }
.pop-header {
padding: 14px 16px 10px;
border-bottom: 1px solid var(--c-border);
display: flex;
align-items: center;
gap: 10px;
}
.pop-header .ph-icon {
width: 32px; height: 32px;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 16px;
}
.pop-header .ph-title {
font-size: 13px;
font-weight: 700;
color: var(--c-text);
}
.pop-header .ph-sub {
font-size: 10px;
color: var(--c-muted);
margin-top: 1px;
}
.pop-body { padding: 12px 16px; }
/* Menu pilihan aksi */
.menu-choices { display: flex; flex-direction: column; gap: 6px; }
.menu-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
border: 1px solid var(--c-border);
border-radius: 7px;
background: transparent;
color: var(--c-text);
cursor: pointer;
font-family: 'Plus Jakarta Sans', sans-serif;
font-size: 12px;
font-weight: 600;
transition: all .15s ease;
width: 100%;
text-align: left;
}
.menu-btn:hover { border-color: var(--c-accent); background: rgba(88,166,255,.08); }
.menu-btn.m:hover { border-color: var(--c-green); background: rgba(63,185,80,.08); }
.menu-btn.l:hover { border-color: var(--c-accent); background: rgba(88,166,255,.08); }
.menu-btn.p:hover { border-color: var(--c-purple); background: rgba(188,140,255,.08); }
.menu-btn .mb-icon { font-size: 16px; }
.menu-btn .mb-desc { font-size: 10px; color: var(--c-muted); margin-top: 1px; }
/* Form Fields */
.form-group { margin-bottom: 10px; }
.form-group label {
display: block;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.5px;
text-transform: uppercase;
color: var(--c-muted);
margin-bottom: 4px;
}
.form-group input,
.form-group select {
width: 100%;
padding: 7px 10px;
background: var(--c-bg);
border: 1px solid var(--c-border);
border-radius: 6px;
color: var(--c-text);
font-family: 'Plus Jakarta Sans', sans-serif;
font-size: 12px;
transition: border-color .15s;
outline: none;
}
.form-group input:focus,
.form-group select:focus { border-color: var(--c-accent); }
.form-group input::placeholder { color: var(--c-muted); }
.form-group select option { background: var(--c-surface); }
/* Buttons */
.btn-row { display: flex; gap: 6px; margin-top: 12px; }
.btn {
flex: 1;
padding: 8px;
border: none;
border-radius: 6px;
font-family: 'Plus Jakarta Sans', sans-serif;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: opacity .15s, transform .1s;
}
.btn:hover { opacity: .88; transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn-primary { background: var(--c-accent); color: #0d1117; }
.btn-success { background: var(--c-green); color: #0d1117; }
.btn-danger { background: var(--c-red); color: #fff; }
.btn-muted { background: var(--c-border); color: var(--c-text); }
/* Info row di popup detail */
.info-row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
border-bottom: 1px solid rgba(48,54,61,.7);
font-size: 12px;
}
.info-row:last-child { border-bottom: none; }
.info-row .ir-label { color: var(--c-muted); font-size: 10px; width: 52px; flex-shrink: 0; }
.info-row .ir-val { font-weight: 500; }
/* Toast Notification */
#toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: 10px;
padding: 10px 18px;
font-size: 13px;
font-weight: 600;
box-shadow: var(--shadow);
z-index: 9999;
opacity: 0;
transition: all .25s ease;
pointer-events: none;
white-space: nowrap;
}
#toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
#toast.ok { border-color: var(--c-green); color: var(--c-green); }
#toast.err { border-color: var(--c-red); color: var(--c-red); }
#toast.info { border-color: var(--c-accent); color: var(--c-accent); }
/* Drawing hint bar */
#draw-hint {
position: fixed;
top: 12px;
left: 50%;
transform: translateX(-50%);
background: var(--c-surface);
border: 1px solid var(--c-orange);
border-radius: 8px;
padding: 8px 18px;
font-size: 12px;
color: var(--c-orange);
font-weight: 600;
z-index: 9000;
display: none;
box-shadow: var(--shadow);
}
/* Leaflet Draw toolbar dark override */
.leaflet-draw-toolbar a { background-color: var(--c-surface) !important; color: var(--c-text) !important; }
/* Empty state */
.empty-state {
text-align: center;
padding: 30px 20px;
color: var(--c-muted);
font-size: 12px;
}
.empty-state .es-icon { font-size: 28px; margin-bottom: 6px; }
/* Tooltip leaflet */
.leaflet-tooltip {
background: var(--c-surface) !important;
border: 1px solid var(--c-border) !important;
color: var(--c-text) !important;
font-family: 'Plus Jakarta Sans', sans-serif !important;
font-size: 11px !important;
font-weight: 600 !important;
padding: 3px 8px !important;
border-radius: 5px !important;
box-shadow: none !important;
}
</style>
</head>
<body>
<div id="app">
<!-- ═══════════════════ SIDEBAR ═══════════════════ -->
<aside id="sidebar">
<div class="sidebar-header">
<center><h1>WEBGIS</h1></center>
<center><p>Klik pada peta untuk menambah objek</p></center>
</div>
<div class="stats-bar">
<div class="stat-chip m">
<div class="val" id="cnt-marker">0</div>
<div class="lbl">📍 Titik</div>
</div>
<div class="stat-chip l">
<div class="val" id="cnt-polyline">0</div>
<div class="lbl">📏 Garis</div>
</div>
<div class="stat-chip p">
<div class="val" id="cnt-polygon">0</div>
<div class="lbl">⬡ Area</div>
</div>
</div>
<div class="list-header">Daftar Objek</div>
<div id="data-list">
<div class="empty-state" id="empty-state">
<div class="es-icon">🗺️</div>
<div>Belum ada data</div>
</div>
</div>
</aside>
<!-- ═══════════════════ MAP ═══════════════════════ -->
<div id="map"></div>
</div>
<!-- Toast Notifikasi -->
<div id="toast"></div>
<!-- Hint saat drawing -->
<div id="draw-hint" id="draw-hint">
✏️ <span id="hint-text">Klik pada peta untuk mulai menggambar. Dobel-klik untuk selesai.</span>
&nbsp;&nbsp;<button onclick="cancelDraw()" style="background:var(--c-red);color:#fff;border:none;border-radius:5px;padding:3px 8px;font-size:11px;cursor:pointer;">Batal</button>
</div>
<!-- ═══════════════ SCRIPTS ════════════════════════════ -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script>
// ─────────────────────────────────────────────────────
// SETUP MAP
// ─────────────────────────────────────────────────────
var map = L.map('map', { zoomControl: true }).setView([-0.0263, 109.3425], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap',
maxZoom: 19
}).addTo(map);
// Layer untuk semua objek yang sudah tersimpan
var drawnLayers = {}; // { id: layer }
var pendingLayer = null; // layer sementara saat drawing baru
var activeDrawTool = null; // referensi tool draw yang aktif
var pendingCoords = null; // koordinat hasil draw sebelum save
var pendingType = null; // jenis geometri yang sedang di-draw
// ─────────────────────────────────────────────────────
// ICON HELPER
// ─────────────────────────────────────────────────────
function makeIcon(color) {
var colors = { green: 'green', red: 'red', blue: 'blue', orange: 'orange', violet: 'violet' };
var c = colors[color] || 'blue';
return L.icon({
iconUrl: `https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-${c}.png`,
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
}
// ─────────────────────────────────────────────────────
// TOAST NOTIFICATION
// ─────────────────────────────────────────────────────
function toast(msg, type = 'ok', duration = 2500) {
var el = document.getElementById('toast');
el.textContent = msg;
el.className = 'show ' + type;
clearTimeout(el._t);
el._t = setTimeout(() => { el.className = ''; }, duration);
}
// ─────────────────────────────────────────────────────
// DRAW HINT
// ─────────────────────────────────────────────────────
function showHint(msg) {
document.getElementById('hint-text').textContent = msg;
document.getElementById('draw-hint').style.display = 'block';
}
function hideHint() {
document.getElementById('draw-hint').style.display = 'none';
}
// ─────────────────────────────────────────────────────
// KLIK PETA → POPUP PILIHAN AKSI
// ─────────────────────────────────────────────────────
map.on('click', function(e) {
// Jangan tampilkan menu kalau sedang drawing
if (activeDrawTool) return;
var html = `
<div class="pop-panel">
<div class="pop-header">
<div class="ph-icon icon-marker">🗺️</div>
<div>
<div class="ph-title">Tambah Objek Baru</div>
<div class="ph-sub">${e.latlng.lat.toFixed(5)}, ${e.latlng.lng.toFixed(5)}</div>
</div>
</div>
<div class="pop-body">
<div class="menu-choices">
<button class="menu-btn m" onclick="startDraw('marker', ${e.latlng.lat}, ${e.latlng.lng})">
<span class="mb-icon">📍</span>
<div>
<div>Buat Titik</div>
<div class="mb-desc">Tandai lokasi tertentu di peta</div>
</div>
</button>
<button class="menu-btn l" onclick="startDraw('polyline')">
<span class="mb-icon">📏</span>
<div>
<div>Buat Garis</div>
<div class="mb-desc">Gambar jalur atau batas garis</div>
</div>
</button>
<button class="menu-btn p" onclick="startDraw('polygon')">
<span class="mb-icon">⬡</span>
<div>
<div>Buat Area (Polygon)</div>
<div class="mb-desc">Gambar area dengan bentuk bebas</div>
</div>
</button>
</div>
</div>
</div>`;
L.popup().setLatLng(e.latlng).setContent(html).openOn(map);
});
// ─────────────────────────────────────────────────────
// START DRAWING
// ─────────────────────────────────────────────────────
function startDraw(type, lat, lng) {
map.closePopup();
if (type === 'marker') {
// Untuk marker, langsung pakai koordinat dari klik
pendingType = 'marker';
pendingCoords = { lat: lat, lng: lng };
// Tampilkan marker sementara (preview)
if (pendingLayer) map.removeLayer(pendingLayer);
pendingLayer = L.marker([lat, lng], {
draggable: true,
icon: makeIcon('blue'),
opacity: 0.7
}).addTo(map);
pendingLayer.on('dragend', function(ev) {
pendingCoords = { lat: ev.target.getLatLng().lat, lng: ev.target.getLatLng().lng };
});
openFormPopup('marker', [lat, lng]);
} else if (type === 'polyline') {
pendingType = 'polyline';
activeDrawTool = new L.Draw.Polyline(map, {
shapeOptions: { color: '#58a6ff', weight: 4, opacity: 0.9 },
metric: true
});
activeDrawTool.enable();
showHint('Klik untuk menambah titik garis. Dobel-klik untuk selesai.');
} else if (type === 'polygon') {
pendingType = 'polygon';
activeDrawTool = new L.Draw.Polygon(map, {
shapeOptions: { color: '#bc8cff', fillOpacity: 0.25, weight: 3 },
showArea: true
});
activeDrawTool.enable();
showHint('Klik untuk menambah sudut area. Klik titik awal atau dobel-klik untuk menutup area.');
}
}
function cancelDraw() {
if (activeDrawTool) { activeDrawTool.disable(); activeDrawTool = null; }
if (pendingLayer) { map.removeLayer(pendingLayer); pendingLayer = null; }
pendingCoords = null; pendingType = null;
hideHint();
toast('Drawing dibatalkan', 'info');
}
// ─────────────────────────────────────────────────────
// EVENT: SELESAI MENGGAMBAR (polyline / polygon)
// ─────────────────────────────────────────────────────
map.on(L.Draw.Event.CREATED, function(e) {
activeDrawTool = null;
hideHint();
var layer = e.layer;
var type = e.layerType === 'polyline' ? 'polyline' : 'polygon';
// Simpan koordinat
var rawCoords = layer.getLatLngs();
pendingCoords = rawCoords;
pendingType = type;
// Tampilkan layer preview
if (pendingLayer) map.removeLayer(pendingLayer);
pendingLayer = layer;
map.addLayer(pendingLayer);
// Hitung titik tengah untuk popup form
var center = layer.getBounds ? layer.getBounds().getCenter() : rawCoords[0];
openFormPopup(type, center);
});
// ─────────────────────────────────────────────────────
// POPUP FORM INPUT (CREATE)
// ─────────────────────────────────────────────────────
function openFormPopup(type, latlng) {
var icons = { marker: '📍', polyline: '📏', polygon: '⬡' };
var labels = { marker: 'Titik', polyline: 'Garis', polygon: 'Area' };
var iconClass = { marker: 'icon-marker', polyline: 'icon-polyline', polygon: 'icon-polygon' };
var html = `
<div class="pop-panel">
<div class="pop-header">
<div class="ph-icon ${iconClass[type]}">${icons[type]}</div>
<div>
<div class="ph-title">Tambah ${labels[type]}</div>
<div class="ph-sub">Isi detail objek baru</div>
</div>
</div>
<div class="pop-body">
<div class="form-group">
<label>Nama Objek *</label>
<input type="text" id="f-nama" placeholder="Contoh: SPBU Pontianak" autocomplete="off"/>
</div>
<div class="form-group">
<label>Buka 24 Jam?</label>
<select id="f-jam">
<option value="Ya">Ya</option>
<option value="Tidak" selected>Tidak</option>
</select>
</div>
<div class="btn-row">
<button class="btn btn-muted" onclick="cancelDraw()">Batal</button>
<button class="btn btn-success" onclick="simpanData()">Simpan</button>
</div>
</div>
</div>`;
L.popup({ closeButton: true, closeOnClick: false })
.setLatLng(latlng)
.setContent(html)
.openOn(map);
// Fokus ke input nama
setTimeout(() => { var el = document.getElementById('f-nama'); if(el) el.focus(); }, 100);
}
// ─────────────────────────────────────────────────────
// SIMPAN DATA (CREATE via AJAX)
// ─────────────────────────────────────────────────────
function simpanData() {
var nama = (document.getElementById('f-nama')?.value || '').trim();
var wa = (document.getElementById('f-wa')?.value || '').trim();
var jam = document.getElementById('f-jam')?.value || 'Tidak';
if (!nama) { toast('⚠️ Nama wajib diisi!', 'err'); return; }
if (!pendingCoords || !pendingType) { toast('⚠️ Objek belum digambar!', 'err'); return; }
var fd = new FormData();
fd.append('aksi', 'simpan');
fd.append('nama', nama);
fd.append('wa', wa);
fd.append('jam', jam);
fd.append('jenis', pendingType);
fd.append('koordinat', JSON.stringify(pendingCoords));
fetch('proses.php', { method: 'POST', body: fd })
.then(r => r.json())
.then(res => {
if (res.status === 'ok') {
map.closePopup();
if (pendingLayer) map.removeLayer(pendingLayer);
pendingLayer = null; pendingCoords = null; pendingType = null;
toast('✅ ' + res.message, 'ok');
loadData(); // Reload data tanpa refresh halaman
} else {
toast('❌ ' + res.message, 'err');
}
})
.catch(() => toast('❌ Gagal terhubung ke server', 'err'));
}
// ─────────────────────────────────────────────────────
// LOAD DATA (READ)
// ─────────────────────────────────────────────────────
function loadData() {
// Hapus semua layer lama
Object.values(drawnLayers).forEach(l => map.removeLayer(l));
drawnLayers = {};
fetch('proses.php?aksi=baca')
.then(r => r.json())
.then(res => {
if (res.status !== 'ok') return;
var data = res.data;
// Update stats
document.getElementById('cnt-marker').textContent = data.filter(d => d.jenis_geometri === 'marker').length;
document.getElementById('cnt-polyline').textContent = data.filter(d => d.jenis_geometri === 'polyline').length;
document.getElementById('cnt-polygon').textContent = data.filter(d => d.jenis_geometri === 'polygon').length;
// Update sidebar list
var listEl = document.getElementById('data-list');
var empEl = document.getElementById('empty-state');
listEl.innerHTML = '';
if (data.length === 0) {
listEl.appendChild(empEl);
return;
}
data.forEach(row => {
addToMap(row);
addToSidebar(row, listEl);
});
})
.catch(() => toast('❌ Gagal memuat data', 'err'));
}
// ─────────────────────────────────────────────────────
// TAMBAH LAYER KE PETA
// ─────────────────────────────────────────────────────
function addToMap(row) {
var id = row.id;
var type = row.jenis_geometri;
var coords = JSON.parse(row.koordinat);
var color = (row.buka_24jam === 'Ya') ? 'green' : 'red';
var layer;
if (type === 'marker') {
layer = L.marker([coords.lat, coords.lng], {
draggable: true,
icon: makeIcon(color)
});
// Update koordinat saat di-drag
layer.on('dragend', function(e) {
var newCoord = e.target.getLatLng();
var fd = new FormData();
fd.append('aksi', 'update_koord');
fd.append('id', id);
fd.append('koordinat', JSON.stringify({ lat: newCoord.lat, lng: newCoord.lng }));
fetch('proses.php', { method: 'POST', body: fd })
.then(r => r.json())
.then(res => toast(res.status === 'ok' ? '📍 Posisi diperbarui' : '❌ ' + res.message, res.status === 'ok' ? 'ok' : 'err'));
});
} else if (type === 'polyline') {
layer = L.polyline(coords, { color: '#58a6ff', weight: 4, opacity: 0.9 });
} else if (type === 'polygon') {
layer = L.polygon(coords, { color: '#bc8cff', fillOpacity: 0.25, weight: 3 });
}
if (!layer) return;
layer.bindTooltip(row.nama_objek, { permanent: false, direction: 'top' });
layer.bindPopup(buildDetailPopup(row));
layer.addTo(map);
drawnLayers[id] = layer;
}
// ─────────────────────────────────────────────────────
// BUILD POPUP DETAIL
// ─────────────────────────────────────────────────────
function buildDetailPopup(row) {
var icons = { marker: '📍', polyline: '📏', polygon: '⬡' };
var cls = { marker: 'icon-marker', polyline: 'icon-polyline', polygon: 'icon-polygon' };
var labels = { marker: 'Titik', polyline: 'Garis', polygon: 'Area' };
var badge = row.buka_24jam === 'Ya'
? '<span class="badge-24 badge-ya">24 Jam</span>'
: '<span class="badge-24 badge-tidak">Tutup</span>';
return `
<div class="pop-panel">
<div class="pop-header">
<div class="ph-icon ${cls[row.jenis_geometri]}">${icons[row.jenis_geometri]}</div>
<div>
<div class="ph-title">${escHtml(row.nama_objek)}</div>
<div class="ph-sub">${labels[row.jenis_geometri]} · ${badge}</div>
</div>
</div>
<div class="pop-body">
<div class="info-row">
<span class="ir-label">WA</span>
<span class="ir-val">${row.no_wa ? escHtml(row.no_wa) : '<em style="color:var(--c-muted)">—</em>'}</span>
</div>
<div class="info-row">
<span class="ir-label">24 Jam</span>
<span class="ir-val">${row.buka_24jam}</span>
</div>
<div class="btn-row" style="margin-top:10px;">
<button class="btn btn-primary" onclick="openEditForm(${row.id})">✏️ Edit</button>
<button class="btn btn-danger" onclick="hapusData(${row.id})">🗑️ Hapus</button>
</div>
</div>
</div>`;
}
// ─────────────────────────────────────────────────────
// TAMBAH ITEM KE SIDEBAR
// ─────────────────────────────────────────────────────
function addToSidebar(row, listEl) {
var icons = { marker: '📍', polyline: '📏', polygon: '⬡' };
var cls = { marker: 'icon-marker', polyline: 'icon-polyline', polygon: 'icon-polygon' };
var labels = { marker: 'Titik', polyline: 'Garis', polygon: 'Area' };
var item = document.createElement('div');
item.className = 'data-item';
item.dataset.id = row.id;
item.innerHTML = `
<div class="item-icon ${cls[row.jenis_geometri]}">${icons[row.jenis_geometri]}</div>
<div class="item-info">
<div class="item-name">${escHtml(row.nama_objek)}</div>
<div class="item-meta">${labels[row.jenis_geometri]}${row.no_wa ? ' · ' + escHtml(row.no_wa) : ''}</div>
</div>
<span class="badge-24 ${row.buka_24jam === 'Ya' ? 'badge-ya' : 'badge-tidak'}">${row.buka_24jam === 'Ya' ? '24J' : '—'}</span>`;
item.addEventListener('click', function() {
// Highlight active
document.querySelectorAll('.data-item').forEach(el => el.classList.remove('active'));
item.classList.add('active');
var layer = drawnLayers[row.id];
if (!layer) return;
// Fly ke lokasi
if (layer.getBounds) {
map.flyToBounds(layer.getBounds(), { padding: [60, 60], duration: 0.6 });
} else if (layer.getLatLng) {
map.flyTo(layer.getLatLng(), 16, { duration: 0.6 });
}
// Buka popup
setTimeout(() => layer.openPopup(), 700);
});
listEl.appendChild(item);
}
// ─────────────────────────────────────────────────────
// EDIT FORM (UPDATE)
// ─────────────────────────────────────────────────────
function openEditForm(id) {
fetch(`proses.php?aksi=detail&id=${id}`)
.then(r => r.json())
.then(res => {
if (res.status !== 'ok') { toast('❌ Data tidak ditemukan', 'err'); return; }
var row = res.data;
var layer = drawnLayers[id];
if (!layer) return;
var coords = JSON.parse(row.koordinat);
var center = layer.getBounds ? layer.getBounds().getCenter()
: (layer.getLatLng ? layer.getLatLng() : [coords.lat, coords.lng]);
var html = `
<div class="pop-panel">
<div class="pop-header">
<div class="ph-icon icon-marker">✏️</div>
<div>
<div class="ph-title">Edit Objek</div>
<div class="ph-sub">ID: ${row.id}</div>
</div>
</div>
<div class="pop-body">
<div class="form-group">
<label>Nama Objek *</label>
<input type="text" id="e-nama" value="${escHtml(row.nama_objek)}"/>
</div>
<div class="form-group">
<label>No. WhatsApp</label>
<input type="text" id="e-wa" value="${escHtml(row.no_wa || '')}"/>
</div>
<div class="form-group">
<label>Buka 24 Jam?</label>
<select id="e-jam">
<option value="Ya" ${row.buka_24jam === 'Ya' ? 'selected' : ''}>Ya</option>
<option value="Tidak" ${row.buka_24jam === 'Tidak' ? 'selected' : ''}>Tidak</option>
</select>
</div>
<div class="btn-row">
<button class="btn btn-muted" onclick="map.closePopup()">Batal</button>
<button class="btn btn-primary" onclick="updateData(${id})">Simpan</button>
</div>
</div>
</div>`;
L.popup({ closeButton: true, closeOnClick: false })
.setLatLng(center)
.setContent(html)
.openOn(map);
setTimeout(() => { var el = document.getElementById('e-nama'); if(el) el.focus(); }, 100);
})
.catch(() => toast('❌ Gagal memuat data', 'err'));
}
// ─────────────────────────────────────────────────────
// UPDATE DATA
// ─────────────────────────────────────────────────────
function updateData(id) {
var nama = (document.getElementById('e-nama')?.value || '').trim();
var wa = (document.getElementById('e-wa')?.value || '').trim();
var jam = document.getElementById('e-jam')?.value || 'Tidak';
if (!nama) { toast('⚠️ Nama wajib diisi!', 'err'); return; }
var fd = new FormData();
fd.append('aksi', 'update');
fd.append('id', id);
fd.append('nama', nama);
fd.append('wa', wa);
fd.append('jam', jam);
fetch('proses.php', { method: 'POST', body: fd })
.then(r => r.json())
.then(res => {
if (res.status === 'ok') {
map.closePopup();
toast('✅ ' + res.message, 'ok');
loadData();
} else {
toast('❌ ' + res.message, 'err');
}
})
.catch(() => toast('❌ Gagal terhubung ke server', 'err'));
}
// ─────────────────────────────────────────────────────
// HAPUS DATA (DELETE)
// ─────────────────────────────────────────────────────
function hapusData(id) {
// Konfirmasi custom tanpa menggunakan native confirm()
var layer = drawnLayers[id];
if (!layer) return;
var center = layer.getBounds ? layer.getBounds().getCenter()
: (layer.getLatLng ? layer.getLatLng() : map.getCenter());
var html = `
<div class="pop-panel">
<div class="pop-header">
<div class="ph-icon" style="background:rgba(248,81,73,.15);color:var(--c-red);">🗑️</div>
<div>
<div class="ph-title" style="color:var(--c-red)">Konfirmasi Hapus</div>
<div class="ph-sub">Tindakan ini tidak bisa dibatalkan</div>
</div>
</div>
<div class="pop-body">
<p style="font-size:12px;color:var(--c-muted);margin-bottom:12px;">Apakah kamu yakin ingin menghapus objek ini?</p>
<div class="btn-row">
<button class="btn btn-muted" onclick="map.closePopup()">Batal</button>
<button class="btn btn-danger" onclick="konfirmasiHapus(${id})">Ya, Hapus</button>
</div>
</div>
</div>`;
L.popup({ closeButton: true, closeOnClick: false })
.setLatLng(center)
.setContent(html)
.openOn(map);
}
function konfirmasiHapus(id) {
var fd = new FormData();
fd.append('aksi', 'hapus');
fd.append('id', id);
fetch('proses.php', { method: 'POST', body: fd })
.then(r => r.json())
.then(res => {
if (res.status === 'ok') {
map.closePopup();
toast('🗑️ ' + res.message, 'ok');
loadData();
} else {
toast('❌ ' + res.message, 'err');
}
})
.catch(() => toast('❌ Gagal terhubung ke server', 'err'));
}
// ─────────────────────────────────────────────────────
// UTIL: Escape HTML
// ─────────────────────────────────────────────────────
function escHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ─────────────────────────────────────────────────────
// INIT
// ─────────────────────────────────────────────────────
loadData();
</script>
</body>
</html>