Files
UAS_WEBGIS_DeranggaArayAbim…/sig-01/admin.php
T
Araya's Project 6462f2e396 First Commit
2026-06-10 12:12:51 +07:00

905 lines
38 KiB
PHP

<?php
session_start();
// ── Konfigurasi password admin ─────────────────────────────────────────────
define('ADMIN_PASSWORD', 'admin123');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['logout'])) {
session_destroy();
header('Location: admin.php');
exit;
}
if (isset($_POST['password']) && $_POST['password'] === ADMIN_PASSWORD) {
$_SESSION['admin'] = true;
header('Location: admin.php');
exit;
}
}
$isAdmin = !empty($_SESSION['admin']);
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin — WebGIS SPBU</title>
<!-- TailwindCSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: { brand: { DEFAULT: '#1d4ed8', dark: '#1e3a8a' } }
}
}
}
</script>
<!-- Leaflet.js -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
html, body { height: 100%; margin: 0; }
#map { height: 100%; width: 100%; }
.leaflet-popup-content-wrapper {
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,.15);
padding: 0; overflow: hidden;
}
.leaflet-popup-content { margin: 0; width: 260px !important; }
.leaflet-popup-tip-container { display: none; }
.spbu-marker {
display: flex; align-items: center; justify-content: center;
width: 36px; height: 36px;
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
border: 3px solid #fff;
box-shadow: 0 3px 10px rgba(0,0,0,.35);
cursor: grab;
transition: transform .15s, box-shadow .15s;
}
.spbu-marker:hover { box-shadow: 0 6px 18px rgba(0,0,0,.45); }
.spbu-marker span {
transform: rotate(45deg);
font-size: 11px; font-weight: 800;
color: #fff; line-height: 1;
}
.marker-24 { background: #16a34a; }
.marker-biasa { background: #ea580c; }
.marker-temp { background: #6d28d9; opacity: .85; }
/* Cursor saat klik peta untuk tambah */
.map-adding { cursor: crosshair !important; }
/* Sidebar */
#spbu-list { overflow-y: auto; max-height: calc(100vh - 280px); }
/* Modal overlay */
.modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.5);
display: flex; align-items: center; justify-content: center;
z-index: 9999;
opacity: 0; pointer-events: none; transition: opacity .2s;
}
.modal-overlay.show { opacity: 1; pointer-events: auto; }
.modal-box {
background: #fff; border-radius: 16px;
width: 420px; max-width: 95vw;
box-shadow: 0 20px 60px rgba(0,0,0,.25);
transform: translateY(12px); transition: transform .2s;
}
.modal-overlay.show .modal-box { transform: translateY(0); }
/* Toast */
#toast {
position: fixed; bottom: 24px; right: 24px;
background: #1e293b; color: #fff;
padding: 12px 20px; border-radius: 10px;
font-size: 13px; z-index: 99999;
display: none; align-items: center; gap-8px;
box-shadow: 0 8px 24px rgba(0,0,0,.25);
min-width: 220px;
}
/* Spinner */
.spinner {
width: 24px; height: 24px;
border: 3px solid #e2e8f0; border-top-color: #1d4ed8;
border-radius: 50%;
animation: spin .7s linear infinite;
margin: 20px auto;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Drag hint badge */
#drag-hint {
position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
background: rgba(109,40,217,.9); color: #fff;
border-radius: 99px; padding: 6px 16px; font-size: 13px;
z-index: 1000; pointer-events: none;
display: none;
}
</style>
</head>
<body class="bg-gray-100 font-sans">
<?php if (!$isAdmin): ?>
<!-- ══════════════════════════════════════════════════════════════════════
LOGIN PAGE
════════════════════════════════════════════════════════════════════════ -->
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br
from-blue-900 via-blue-800 to-blue-600 p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden">
<div class="bg-gradient-to-r from-blue-700 to-blue-900 px-8 py-6 text-white text-center">
<svg class="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor"
stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2
2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
<h1 class="text-xl font-bold">Panel Admin</h1>
<p class="text-blue-200 text-sm mt-0.5">WebGIS SPBU</p>
</div>
<form method="POST" class="px-8 py-6 space-y-4">
<?php if (isset($_POST['password']) && $_POST['password'] !== ADMIN_PASSWORD): ?>
<p class="bg-red-50 text-red-600 text-sm rounded-lg px-3 py-2 text-center">
Password salah, coba lagi.
</p>
<?php endif; ?>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Password</label>
<input type="password" name="password" autofocus required
class="w-full border border-gray-200 rounded-xl px-4 py-2.5 text-sm
focus:outline-none focus:ring-2 focus:ring-blue-400"
placeholder="Masukkan password admin">
</div>
<button type="submit"
class="w-full bg-blue-700 hover:bg-blue-800 text-white font-semibold
py-2.5 rounded-xl transition-colors text-sm">
Masuk
</button>
<a href="index.php"
class="block text-center text-sm text-gray-400 hover:text-blue-600 mt-1">
← Kembali ke Peta Publik
</a>
</form>
</div>
</div>
<?php else: ?>
<!-- ══════════════════════════════════════════════════════════════════════
ADMIN DASHBOARD
════════════════════════════════════════════════════════════════════════ -->
<!-- Toast Notification -->
<div id="toast" class="flex items-center gap-2">
<span id="toast-icon"></span>
<span id="toast-msg"></span>
</div>
<!-- ── MODAL TAMBAH / EDIT ─────────────────────────────────────────────── -->
<div id="modal" class="modal-overlay" onclick="closeModal(event)">
<div class="modal-box">
<!-- Header -->
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-100">
<h2 id="modal-title" class="text-base font-bold text-gray-800">Tambah SPBU</h2>
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-700 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<!-- Body -->
<form id="spbu-form" class="px-6 py-5 space-y-4">
<input type="hidden" id="f-id">
<input type="hidden" id="f-lat">
<input type="hidden" id="f-lng">
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1 uppercase tracking-wide">
Kode SPBU <span class="text-red-500">*</span>
</label>
<input id="f-kode" type="text" required placeholder="Contoh: 31.401.06"
class="w-full border border-gray-200 rounded-xl px-4 py-2.5 text-sm
focus:outline-none focus:ring-2 focus:ring-blue-400">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1 uppercase tracking-wide">
Nama SPBU <span class="text-red-500">*</span>
</label>
<input id="f-nama" type="text" required placeholder="Contoh: SPBU Pertamina Sudirman"
class="w-full border border-gray-200 rounded-xl px-4 py-2.5 text-sm
focus:outline-none focus:ring-2 focus:ring-blue-400">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-2 uppercase tracking-wide">
Jam Operasional
</label>
<div class="flex gap-3">
<label class="flex-1 flex items-center gap-2 border-2 border-gray-100 rounded-xl px-4 py-3
cursor-pointer has-[:checked]:border-green-500 has-[:checked]:bg-green-50
transition-all">
<input type="radio" name="is24" value="1" id="r-24" class="accent-green-600">
<span class="text-sm font-medium text-gray-700">Buka 24 Jam</span>
</label>
<label class="flex-1 flex items-center gap-2 border-2 border-gray-100 rounded-xl px-4 py-3
cursor-pointer has-[:checked]:border-orange-500 has-[:checked]:bg-orange-50
transition-all">
<input type="radio" name="is24" value="0" id="r-biasa" class="accent-orange-500">
<span class="text-sm font-medium text-gray-700">Non-24 Jam</span>
</label>
</div>
</div>
<!-- Koordinat (info only) -->
<div class="bg-gray-50 rounded-xl px-4 py-3">
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1">Koordinat</p>
<p id="f-coord-display" class="text-sm text-gray-700 font-mono">—</p>
<p id="coord-hint" class="text-xs text-blue-500 mt-1 hidden">
Klik peta untuk mengubah lokasi
</p>
</div>
<!-- Error msg -->
<p id="form-error" class="text-sm text-red-500 hidden"></p>
</form>
<!-- Footer -->
<div class="px-6 py-4 border-t border-gray-100 flex justify-end gap-3">
<button onclick="closeModal()"
class="px-5 py-2 text-sm font-medium text-gray-600 bg-gray-100
hover:bg-gray-200 rounded-xl transition-colors">
Batal
</button>
<button id="btn-submit" onclick="submitForm()"
class="px-5 py-2 text-sm font-semibold text-white bg-blue-700
hover:bg-blue-800 rounded-xl transition-colors flex items-center gap-2">
<span id="btn-submit-text">Simpan</span>
<svg id="btn-spin" class="hidden w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
</button>
</div>
</div>
</div>
<!-- ── CONFIRM DELETE MODAL ────────────────────────────────────────────── -->
<div id="modal-del" class="modal-overlay" onclick="closeDeleteModal(event)">
<div class="modal-box max-w-xs">
<div class="px-6 py-5 text-center">
<div class="w-14 h-14 rounded-full bg-red-100 flex items-center justify-center mx-auto mb-3">
<svg class="w-7 h-7 text-red-600" fill="none" stroke="currentColor"
stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5
7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</div>
<h3 class="font-bold text-gray-800 mb-1">Hapus SPBU?</h3>
<p class="text-sm text-gray-500" id="del-name"></p>
</div>
<div class="px-6 pb-5 flex gap-3">
<button onclick="closeDeleteModal()"
class="flex-1 py-2 text-sm font-medium text-gray-600 bg-gray-100
hover:bg-gray-200 rounded-xl transition-colors">
Batal
</button>
<button onclick="confirmDelete()"
class="flex-1 py-2 text-sm font-semibold text-white bg-red-600
hover:bg-red-700 rounded-xl transition-colors">
Hapus
</button>
</div>
</div>
</div>
<!-- ── LAYOUT ──────────────────────────────────────────────────────────── -->
<div class="flex h-screen">
<!-- SIDEBAR -->
<aside class="w-80 bg-white shadow-xl flex flex-col z-10 flex-shrink-0">
<!-- Header -->
<div class="bg-gradient-to-br from-blue-700 to-blue-900 text-white px-5 py-4">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<svg class="w-6 h-6 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="2"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3 10h2l1 2h13l1-2h2M5 12v6a1 1 0 001 1h12a1 1 0 001-1v-6
M9 22v-4h6v4M7 4h10l1 4H6L7 4z"/>
</svg>
<div>
<h1 class="text-base font-bold leading-tight">Admin Panel</h1>
<p class="text-blue-200 text-xs">WebGIS SPBU</p>
</div>
</div>
<form method="POST">
<button name="logout" type="submit"
class="bg-blue-800 hover:bg-blue-900 text-xs text-blue-100
px-3 py-1.5 rounded-lg transition-colors">
Keluar
</button>
</form>
</div>
<!-- Add button -->
<button id="btn-add-mode" onclick="toggleAddMode()"
class="w-full mt-1 bg-white text-blue-700 font-semibold text-sm py-2
rounded-xl hover:bg-blue-50 transition-colors flex items-center
justify-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/>
</svg>
<span id="btn-add-label">Tambah SPBU (Klik Peta)</span>
</button>
</div>
<!-- Filter -->
<div class="px-4 py-3 border-b border-gray-100">
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Filter</p>
<div class="flex gap-2">
<button onclick="setFilter('semua')" id="btn-semua"
class="flex-1 py-1.5 text-xs rounded-lg font-medium transition-all filter-btn
bg-blue-600 text-white">
Semua
</button>
<button onclick="setFilter('24jam')" id="btn-24jam"
class="flex-1 py-1.5 text-xs rounded-lg font-medium transition-all filter-btn
bg-gray-100 text-gray-600 hover:bg-green-50 hover:text-green-700">
24 Jam
</button>
<button onclick="setFilter('tidak')" id="btn-tidak"
class="flex-1 py-1.5 text-xs rounded-lg font-medium transition-all filter-btn
bg-gray-100 text-gray-600 hover:bg-orange-50 hover:text-orange-700">
Non-24 Jam
</button>
</div>
</div>
<!-- Stats -->
<div class="px-4 py-2.5 border-b border-gray-100 flex gap-2">
<div class="flex-1 text-center bg-blue-50 rounded-lg py-2">
<p class="text-lg font-bold text-blue-700" id="stat-total">—</p>
<p class="text-xs text-blue-400">Total</p>
</div>
<div class="flex-1 text-center bg-green-50 rounded-lg py-2">
<p class="text-lg font-bold text-green-700" id="stat-24">—</p>
<p class="text-xs text-green-400">24 Jam</p>
</div>
<div class="flex-1 text-center bg-orange-50 rounded-lg py-2">
<p class="text-lg font-bold text-orange-700" id="stat-biasa">—</p>
<p class="text-xs text-orange-400">Non-24 Jam</p>
</div>
</div>
<!-- SPBU List -->
<div id="spbu-list" class="flex-1 px-3 py-2 space-y-1">
<div class="spinner"></div>
</div>
<!-- Footer -->
<div class="px-4 py-3 border-t border-gray-100">
<a href="index.php"
class="flex items-center justify-center gap-1.5 text-xs text-gray-500
hover:text-blue-600 font-medium">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
Lihat Tampilan Publik
</a>
</div>
</aside>
<!-- MAP -->
<main class="flex-1 relative">
<div id="map"></div>
<div id="drag-hint">Seret marker untuk pindah lokasi</div>
<!-- Legend -->
<div class="absolute bottom-6 right-4 bg-white rounded-xl shadow-lg px-4 py-3 z-[1000]">
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Keterangan</p>
<div class="flex items-center gap-2 mb-1">
<span class="inline-block w-3 h-3 rounded-full bg-green-600"></span>
<span class="text-xs text-gray-700">Buka 24 Jam</span>
</div>
<div class="flex items-center gap-2 mb-1">
<span class="inline-block w-3 h-3 rounded-full bg-orange-600"></span>
<span class="text-xs text-gray-700">Non-24 Jam</span>
</div>
<div class="flex items-center gap-2">
<span class="inline-block w-3 h-3 rounded-full bg-purple-700"></span>
<span class="text-xs text-gray-700">Posisi baru</span>
</div>
</div>
</main>
</div>
<!-- ── SCRIPTS ─────────────────────────────────────────────────────────── -->
<script>
// ── State ──────────────────────────────────────────────────────────────────
let allSpbu = [];
let markers = {}; // id → Leaflet marker
let activeFilter = 'semua';
let addMode = false; // klik peta = tambah SPBU
let editingId = null; // ID yg sedang diedit
let deleteId = null;
let pendingLat = null;
let pendingLng = null;
let tempMarker = null; // marker sementara saat picking lokasi baru di edit
// ── Map Init ───────────────────────────────────────────────────────────────
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 19
}).addTo(map);
L.control.zoom({ position: 'bottomleft' }).addTo(map);
// ── Toast ──────────────────────────────────────────────────────────────────
function toast(msg, type = 'success') {
const el = document.getElementById('toast');
const ico = document.getElementById('toast-icon');
const txt = document.getElementById('toast-msg');
ico.innerHTML = type === 'success'
? '<svg class="w-4 h-4 text-green-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>'
: '<svg class="w-4 h-4 text-red-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>';
txt.textContent = msg;
el.style.display = 'flex';
clearTimeout(el._t);
el._t = setTimeout(() => { el.style.display = 'none'; }, 3000);
}
// ── Icons ──────────────────────────────────────────────────────────────────
function makeIcon(is24, isTemp = false) {
const cls = isTemp ? 'marker-temp' : (is24 ? 'marker-24' : 'marker-biasa');
const lbl = isTemp ? '?' : (is24 ? '24' : '');
return L.divIcon({
className: '',
html: `<div class="spbu-marker ${cls}"><span>${lbl}</span></div>`,
iconSize: [36, 36],
iconAnchor: [18, 36],
popupAnchor:[0, -38]
});
}
// ── Popup ──────────────────────────────────────────────────────────────────
function makePopup(s) {
const badge = s.is_24jam
? `<span class="inline-flex items-center gap-1 bg-green-100 text-green-700
text-xs font-semibold px-2 py-0.5 rounded-full">
Buka 24 Jam
</span>`
: `<span class="inline-flex items-center gap-1 bg-orange-100 text-orange-700
text-xs font-semibold px-2 py-0.5 rounded-full">
Non-24 Jam
</span>`;
return `
<div>
<div class="px-4 py-3 border-b border-gray-100">
<p class="font-bold text-gray-800 text-sm">${s.nama}</p>
<p class="text-xs text-gray-400">Kode: ${s.kode}</p>
</div>
<div class="px-4 py-3 space-y-2">
${badge}
<p class="text-xs text-gray-500 font-mono">
${s.latitude.toFixed(6)}, ${s.longitude.toFixed(6)}
</p>
<p class="text-xs text-purple-600 italic">
Seret marker untuk pindah lokasi
</p>
</div>
<div class="px-4 pb-3 flex gap-2">
<button onclick="openEditModal(${s.id})"
class="flex-1 py-1.5 text-xs font-semibold text-white bg-blue-600
hover:bg-blue-700 rounded-lg transition-colors">
Edit
</button>
<button onclick="openDeleteModal(${s.id})"
class="flex-1 py-1.5 text-xs font-semibold text-white bg-red-500
hover:bg-red-600 rounded-lg transition-colors">
Hapus
</button>
</div>
</div>`;
}
// ── Add Marker to Map ──────────────────────────────────────────────────────
function addMarker(s) {
const m = L.marker([s.latitude, s.longitude], {
icon: makeIcon(s.is_24jam),
draggable: true
});
m.on('dragstart', () => {
document.getElementById('drag-hint').style.display = 'block';
m.closePopup();
});
m.on('dragend', (e) => {
document.getElementById('drag-hint').style.display = 'none';
const { lat, lng } = e.target.getLatLng();
updateLocation(s.id, lat, lng);
});
m.bindPopup(makePopup(s), { maxWidth: 280 });
m.addTo(map);
markers[s.id] = m;
}
// ── Render Sidebar List ────────────────────────────────────────────────────
function renderList(data) {
const el = document.getElementById('spbu-list');
if (!data.length) {
el.innerHTML = `<p class="text-sm text-gray-400 text-center py-8">Tidak ada data.</p>`;
return;
}
el.innerHTML = data.map(s => `
<div onclick="focusSpbu(${s.id})"
class="flex items-start gap-3 p-3 rounded-xl cursor-pointer
hover:bg-blue-50 transition-colors group">
<span class="mt-0.5 flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center
${s.is_24jam ? 'bg-green-100' : 'bg-orange-100'}">
<span class="text-xs font-bold ${s.is_24jam ? 'text-green-700' : 'text-orange-700'}">
${s.is_24jam ? '24' : '—'}
</span>
</span>
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold text-gray-800 truncate">${s.nama}</p>
<p class="text-xs text-gray-400">Kode: ${s.kode}</p>
</div>
<div class="flex gap-1 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<button onclick="event.stopPropagation(); openEditModal(${s.id})"
class="p-1 rounded-lg bg-blue-100 hover:bg-blue-200 text-blue-600 transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5
m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828
l8.586-8.586z"/>
</svg>
</button>
<button onclick="event.stopPropagation(); openDeleteModal(${s.id})"
class="p-1 rounded-lg bg-red-100 hover:bg-red-200 text-red-500 transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858
L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
</div>
</div>`).join('');
}
// ── Filter ─────────────────────────────────────────────────────────────────
function setFilter(f) {
activeFilter = f;
document.querySelectorAll('.filter-btn').forEach(b => {
b.classList.remove('bg-blue-600','bg-green-600','bg-orange-500','text-white');
b.classList.add('bg-gray-100','text-gray-600');
});
const colorMap = { semua:['bg-blue-600','text-white'],
'24jam':['bg-green-600','text-white'],
tidak:['bg-orange-500','text-white'] };
const btn = document.getElementById('btn-' + f);
btn.classList.remove('bg-gray-100','text-gray-600');
btn.classList.add(...colorMap[f]);
applyFilter();
}
function applyFilter() {
let data = allSpbu;
if (activeFilter === '24jam') data = data.filter(s => s.is_24jam);
if (activeFilter === 'tidak') data = data.filter(s => !s.is_24jam);
// Redraw markers
Object.values(markers).forEach(m => map.removeLayer(m));
markers = {};
data.forEach(addMarker);
renderList(data);
}
function updateStats() {
document.getElementById('stat-total').textContent = allSpbu.length;
document.getElementById('stat-24').textContent = allSpbu.filter(s => s.is_24jam).length;
document.getElementById('stat-biasa').textContent = allSpbu.filter(s => !s.is_24jam).length;
}
function focusSpbu(id) {
const m = markers[id];
if (!m) return;
map.flyTo(m.getLatLng(), 16, { duration: .8 });
setTimeout(() => m.openPopup(), 850);
}
// ── Load Data ──────────────────────────────────────────────────────────────
function loadSpbu() {
fetch('api/spbu.php?filter=semua')
.then(r => r.json())
.then(res => {
if (!res.success) throw new Error(res.message);
allSpbu = res.data;
updateStats();
applyFilter();
})
.catch(err => toast(err.message, 'error'));
}
// ── Add Mode ───────────────────────────────────────────────────────────────
function toggleAddMode() {
addMode = !addMode;
const btn = document.getElementById('btn-add-mode');
const label = document.getElementById('btn-add-label');
if (addMode) {
btn.classList.replace('bg-white','bg-yellow-300');
btn.classList.replace('text-blue-700','text-yellow-900');
label.textContent = 'Mode Tambah AKTIF — Klik Peta';
map.getContainer().classList.add('map-adding');
} else {
btn.classList.replace('bg-yellow-300','bg-white');
btn.classList.replace('text-yellow-900','text-blue-700');
label.textContent = 'Tambah SPBU (Klik Peta)';
map.getContainer().classList.remove('map-adding');
removeTempMarker();
}
}
map.on('click', function(e) {
if (!addMode) return;
const { lat, lng } = e.latlng;
pendingLat = lat;
pendingLng = lng;
removeTempMarker();
tempMarker = L.marker([lat, lng], { icon: makeIcon(false, true) }).addTo(map);
openAddModal(lat, lng);
});
function removeTempMarker() {
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
}
// ── Modal TAMBAH ───────────────────────────────────────────────────────────
function openAddModal(lat, lng) {
editingId = null;
document.getElementById('modal-title').textContent = 'Tambah SPBU Baru';
document.getElementById('btn-submit-text').textContent = 'Tambah';
document.getElementById('f-id').value = '';
document.getElementById('f-kode').value = '';
document.getElementById('f-nama').value = '';
document.querySelector('input[name="is24"][value="1"]').checked = true;
document.getElementById('f-lat').value = lat;
document.getElementById('f-lng').value = lng;
document.getElementById('f-coord-display').textContent =
`${lat.toFixed(6)}, ${lng.toFixed(6)}`;
document.getElementById('coord-hint').classList.add('hidden');
document.getElementById('form-error').classList.add('hidden');
showModal();
}
// ── Modal EDIT ─────────────────────────────────────────────────────────────
function openEditModal(id) {
const s = allSpbu.find(x => x.id === id);
if (!s) return;
editingId = id;
document.getElementById('modal-title').textContent = 'Edit SPBU';
document.getElementById('btn-submit-text').textContent = 'Simpan Perubahan';
document.getElementById('f-id').value = s.id;
document.getElementById('f-kode').value = s.kode;
document.getElementById('f-nama').value = s.nama;
document.querySelector(`input[name="is24"][value="${s.is_24jam ? 1 : 0}"]`).checked = true;
document.getElementById('f-lat').value = s.latitude;
document.getElementById('f-lng').value = s.longitude;
document.getElementById('f-coord-display').textContent =
`${s.latitude.toFixed(6)}, ${s.longitude.toFixed(6)}`;
document.getElementById('coord-hint').classList.remove('hidden');
document.getElementById('form-error').classList.add('hidden');
// Tutup popup jika ada
if (markers[id]) markers[id].closePopup();
// Saat modal edit terbuka, klik peta bisa update koordinat
map._editCoordListener = function(e) {
if (!document.getElementById('modal').classList.contains('show')) return;
const { lat, lng } = e.latlng;
document.getElementById('f-lat').value = lat;
document.getElementById('f-lng').value = lng;
document.getElementById('f-coord-display').textContent =
`${lat.toFixed(6)}, ${lng.toFixed(6)}`;
removeTempMarker();
tempMarker = L.marker([lat, lng], { icon: makeIcon(false, true) }).addTo(map);
};
map.on('click', map._editCoordListener);
showModal();
}
function showModal() {
document.getElementById('modal').classList.add('show');
}
function closeModal(e) {
if (e && e.target !== document.getElementById('modal')) return;
document.getElementById('modal').classList.remove('show');
removeTempMarker();
if (map._editCoordListener) {
map.off('click', map._editCoordListener);
map._editCoordListener = null;
}
if (addMode && !editingId) {
// tutup add mode jika batal tambah
toggleAddMode();
}
}
// ── Submit Form ────────────────────────────────────────────────────────────
function submitForm() {
const kode = document.getElementById('f-kode').value.trim();
const nama = document.getElementById('f-nama').value.trim();
const is24 = document.querySelector('input[name="is24"]:checked')?.value;
const lat = parseFloat(document.getElementById('f-lat').value);
const lng = parseFloat(document.getElementById('f-lng').value);
const errEl = document.getElementById('form-error');
if (!kode || !nama || is24 === undefined || isNaN(lat) || isNaN(lng)) {
errEl.textContent = 'Semua field wajib diisi.';
errEl.classList.remove('hidden');
return;
}
errEl.classList.add('hidden');
document.getElementById('btn-spin').classList.remove('hidden');
document.getElementById('btn-submit').disabled = true;
const body = { kode, nama, is_24jam: is24 === '1', latitude: lat, longitude: lng };
const isEdit = !!editingId;
if (isEdit) body.id = editingId;
fetch('api/spbu.php', {
method: isEdit ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
.then(r => r.json())
.then(res => {
document.getElementById('btn-spin').classList.add('hidden');
document.getElementById('btn-submit').disabled = false;
if (!res.success) {
errEl.textContent = res.message;
errEl.classList.remove('hidden');
return;
}
const s = res.data;
if (isEdit) {
// Update local array
const idx = allSpbu.findIndex(x => x.id === s.id);
if (idx >= 0) allSpbu[idx] = s;
// Rebuild marker
if (markers[s.id]) map.removeLayer(markers[s.id]);
delete markers[s.id];
addMarker(s);
} else {
allSpbu.push(s);
addMarker(s);
}
updateStats();
applyFilter();
removeTempMarker();
document.getElementById('modal').classList.remove('show');
if (map._editCoordListener) {
map.off('click', map._editCoordListener);
map._editCoordListener = null;
}
if (addMode) toggleAddMode();
toast(res.message);
})
.catch(() => {
document.getElementById('btn-spin').classList.add('hidden');
document.getElementById('btn-submit').disabled = false;
errEl.textContent = 'Terjadi kesalahan, coba lagi.';
errEl.classList.remove('hidden');
});
}
// ── Update Location (Drag) ─────────────────────────────────────────────────
function updateLocation(id, lat, lng) {
fetch('api/spbu.php', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, latitude: lat, longitude: lng })
})
.then(r => r.json())
.then(res => {
if (!res.success) { toast(res.message, 'error'); return; }
const idx = allSpbu.findIndex(x => x.id === id);
if (idx >= 0) {
allSpbu[idx].latitude = lat;
allSpbu[idx].longitude = lng;
}
// Refresh popup content
if (markers[id]) {
markers[id].setPopupContent(makePopup(allSpbu[idx]));
}
toast('Lokasi SPBU berhasil diperbarui');
})
.catch(() => toast('Gagal memperbarui lokasi', 'error'));
}
// ── Delete ─────────────────────────────────────────────────────────────────
function openDeleteModal(id) {
deleteId = id;
const s = allSpbu.find(x => x.id === id);
document.getElementById('del-name').textContent = s ? s.nama : '';
document.getElementById('modal-del').classList.add('show');
if (markers[id]) markers[id].closePopup();
}
function closeDeleteModal(e) {
if (e && e.target !== document.getElementById('modal-del')) return;
document.getElementById('modal-del').classList.remove('show');
deleteId = null;
}
function confirmDelete() {
if (!deleteId) return;
fetch(`api/spbu.php?id=${deleteId}`, { method: 'DELETE' })
.then(r => r.json())
.then(res => {
if (!res.success) { toast(res.message, 'error'); return; }
if (markers[deleteId]) map.removeLayer(markers[deleteId]);
delete markers[deleteId];
allSpbu = allSpbu.filter(x => x.id !== deleteId);
updateStats();
applyFilter();
document.getElementById('modal-del').classList.remove('show');
deleteId = null;
toast(res.message);
})
.catch(() => toast('Gagal menghapus SPBU', 'error'));
}
// ── Keyboard shortcut ──────────────────────────────────────────────────────
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
if (document.getElementById('modal').classList.contains('show')) {
closeModal();
} else if (document.getElementById('modal-del').classList.contains('show')) {
closeDeleteModal();
} else if (addMode) {
toggleAddMode();
}
}
if (e.key === 'Enter' && document.getElementById('modal').classList.contains('show')) {
submitForm();
}
});
// ── Boot ───────────────────────────────────────────────────────────────────
loadSpbu();
</script>
<?php endif; ?>
</body>
</html>