UAS SIG Proyek WebGis
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
-- =====================================================
|
||||
-- WebGIS SPBU Pontianak — Database Schema
|
||||
-- Jalankan di phpMyAdmin atau MySQL CLI
|
||||
-- =====================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis_spbu
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE webgis_spbu;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spbu (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(150) NOT NULL COMMENT 'Nama SPBU',
|
||||
no VARCHAR(50) NOT NULL COMMENT 'Nomor SPBU (mis: 64.751.01)',
|
||||
status TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1=Buka 24 Jam, 0=Terbatas',
|
||||
latitude DECIMAL(10,8) NOT NULL COMMENT 'Koordinat Latitude',
|
||||
longitude DECIMAL(11,8) NOT NULL COMMENT 'Koordinat Longitude',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ── Contoh Data SPBU Pontianak ──────────────────────
|
||||
INSERT INTO spbu (nama, no, status, latitude, longitude) VALUES
|
||||
('SPBU Jl. Ahmad Yani', '64.751.01', 1, -0.020500, 109.341200),
|
||||
('SPBU Jl. Gajahmada', '64.751.02', 0, -0.031800, 109.325600),
|
||||
('SPBU Jl. Tanjungpura', '64.751.03', 1, -0.025300, 109.332400),
|
||||
('SPBU Jl. Sutan Syahrir', '64.751.04', 0, -0.018200, 109.348700),
|
||||
('SPBU Jl. Adisucipto', '64.751.05', 1, -0.040100, 109.358900);
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// db_connect.php - Koneksi Database
|
||||
$host = "localhost";
|
||||
$dbname = "webgis_spbu";
|
||||
$username = "root";
|
||||
$password = "";
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $username, $password);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch (PDOException $e) {
|
||||
die(json_encode(["error" => "Koneksi gagal: " . $e->getMessage()]));
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="0; url=index.php">
|
||||
<title>WebGIS SPBU Pontianak</title>
|
||||
<script>
|
||||
window.location.href = "index.php";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Mengalihkan ke halaman utama... Jika tidak otomatis, klik <a href="index.php">di sini</a>.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,397 @@
|
||||
<?php
|
||||
require_once 'db_connect.php';
|
||||
|
||||
// AJAX: update koordinat saat marker di-drag
|
||||
if (isset($_GET['update_coords']) && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
header('Content-Type: application/json');
|
||||
$id = intval($_POST['id']);
|
||||
$lat = floatval($_POST['lat']);
|
||||
$lng = floatval($_POST['lng']);
|
||||
if($id > 0){
|
||||
$pdo->prepare("UPDATE spbu SET latitude=:lat,longitude=:lng WHERE id=:id")
|
||||
->execute([':lat'=>$lat,':lng'=>$lng,':id'=>$id]);
|
||||
echo json_encode(['ok'=>true,'lat'=>$lat,'lng'=>$lng]);
|
||||
} else {
|
||||
echo json_encode(['ok'=>false]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// AJAX: hapus dari index.php
|
||||
if (isset($_GET['delete']) && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
header('Content-Type: application/json');
|
||||
$id = intval($_POST['id']);
|
||||
$pdo->prepare("DELETE FROM spbu WHERE id=:id")->execute([':id'=>$id]);
|
||||
echo json_encode(['ok'=>true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$spbu_list = $pdo->query("SELECT * FROM spbu ORDER BY nama")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$total = count($spbu_list);
|
||||
$total_24 = count(array_filter($spbu_list, fn($s) => $s['status']==1));
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Peta SPBU — WebGIS Pontianak</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#f5f4f0;--card:#fff;--border:#e8e5df;--border2:#d4d0c8;
|
||||
--text:#1a1915;--muted:#7c7a72;--hint:#b0ada5;
|
||||
--accent:#e85a1a;--accent-bg:#fdf0eb;--accent-dk:#c44a12;
|
||||
--green:#1a7a45;--green-bg:#ebf7f0;
|
||||
--red:#c0392b;--red-bg:#fdf0ef;
|
||||
--r:10px;--r-sm:7px;
|
||||
}
|
||||
html,body{height:100%;font-family:'Plus Jakarta Sans',sans-serif;color:var(--text);font-size:14px}
|
||||
body{display:flex;flex-direction:column;background:var(--bg)}
|
||||
|
||||
/* NAV */
|
||||
.topnav{background:var(--card);border-bottom:1px solid var(--border);height:54px;padding:0 24px;display:flex;align-items:center;gap:6px;flex-shrink:0;z-index:9999;position:relative}
|
||||
.brand{display:flex;align-items:center;gap:9px;font-weight:700;font-size:14px;color:var(--text);text-decoration:none;margin-right:auto}
|
||||
.brand-mark{width:30px;height:30px;background:var(--accent);border-radius:8px;display:grid;place-items:center;color:#fff;font-size:14px;font-weight:800}
|
||||
.nav-a{padding:5px 13px;border-radius:var(--r-sm);font-size:13px;font-weight:500;color:var(--muted);text-decoration:none;transition:.12s}
|
||||
.nav-a:hover{background:var(--bg);color:var(--text)}
|
||||
.nav-a.on{background:var(--accent-bg);color:var(--accent);font-weight:600}
|
||||
|
||||
/* STATS BAR */
|
||||
.statsbar{background:var(--card);border-bottom:1px solid var(--border);padding:0 24px;height:40px;display:flex;align-items:center;gap:18px;flex-shrink:0}
|
||||
.stat{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
|
||||
.stat strong{color:var(--text);font-weight:600}
|
||||
.stat-dot{width:8px;height:8px;border-radius:50%}
|
||||
|
||||
/* MAIN */
|
||||
.main{display:flex;flex:1;overflow:hidden}
|
||||
|
||||
/* SIDEBAR */
|
||||
.sidebar{width:300px;flex-shrink:0;background:var(--card);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
|
||||
.sb-head{padding:14px 16px 10px;border-bottom:1px solid var(--border);flex-shrink:0}
|
||||
.sb-head h2{font-size:13px;font-weight:600;margin-bottom:9px;color:var(--text)}
|
||||
.search{width:100%;padding:7px 11px;border:1px solid var(--border2);border-radius:var(--r-sm);font-family:inherit;font-size:13px;color:var(--text);background:#fff;transition:.12s}
|
||||
.search:focus{outline:none;border-color:var(--accent)}
|
||||
.sb-list{flex:1;overflow-y:auto;padding:8px}
|
||||
.sb-list::-webkit-scrollbar{width:3px}
|
||||
.sb-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
||||
.sb-item{border:1px solid var(--border);border-radius:var(--r-sm);padding:10px 12px;margin-bottom:6px;cursor:pointer;transition:.12s}
|
||||
.sb-item:hover{border-color:var(--border2);background:#fafaf8}
|
||||
.sb-item.active{border-color:var(--accent);background:var(--accent-bg)}
|
||||
.sb-name{font-weight:600;font-size:13px;margin-bottom:2px}
|
||||
.sb-no{font-size:12px;color:var(--muted)}
|
||||
.sb-badge{display:inline-flex;align-items:center;gap:3px;margin-top:5px;padding:2px 8px;border-radius:20px;font-size:11px;font-weight:600}
|
||||
.sb-open{background:var(--green-bg);color:var(--green)}
|
||||
.sb-closed{background:var(--bg);color:var(--hint);border:1px solid var(--border)}
|
||||
.empty-sb{text-align:center;padding:32px 16px;color:var(--hint);font-size:13px}
|
||||
|
||||
/* MAP */
|
||||
.mapwrap{flex:1;position:relative;overflow:hidden}
|
||||
#map{width:100%;height:100%}
|
||||
|
||||
/* Rapikan tampilan Leaflet Layers Control */
|
||||
.leaflet-control-layers{
|
||||
border:1px solid var(--border) !important;
|
||||
border-radius:var(--r-sm) !important;
|
||||
box-shadow:0 2px 12px rgba(0,0,0,.09) !important;
|
||||
font-family:'Plus Jakarta Sans',sans-serif !important;
|
||||
font-size:13px !important;
|
||||
}
|
||||
.leaflet-control-layers-expanded{padding:10px 14px !important}
|
||||
.leaflet-control-layers-separator{margin:6px 0 !important;border-color:var(--border) !important}
|
||||
|
||||
/* TOAST */
|
||||
.toast{position:absolute;top:12px;left:50%;transform:translateX(-50%) translateY(-60px);z-index:1001;background:#1a1915;color:#fff;font-size:12px;font-weight:500;padding:8px 16px;border-radius:20px;transition:.25s;pointer-events:none;white-space:nowrap}
|
||||
.toast.show{transform:translateX(-50%) translateY(0)}
|
||||
|
||||
/* ATTR PANEL */
|
||||
#attr-panel{
|
||||
position:absolute;bottom:0;left:0;right:0;
|
||||
background:var(--card);border-top:2px solid var(--accent);
|
||||
transform:translateY(100%);transition:transform .28s cubic-bezier(.4,0,.2,1);
|
||||
z-index:1000;
|
||||
}
|
||||
#attr-panel.open{transform:translateY(0)}
|
||||
.ap-head{padding:12px 18px 10px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.ap-title{font-size:13px;font-weight:700;color:var(--accent)}
|
||||
.ap-actions{display:flex;gap:7px;align-items:center}
|
||||
.ap-close{background:none;border:1px solid var(--border2);color:var(--muted);border-radius:5px;padding:3px 9px;font-size:12px;cursor:pointer;font-family:inherit;transition:.12s}
|
||||
.ap-close:hover{border-color:var(--muted)}
|
||||
.ap-del{background:none;border:1px solid #f0c4bf;color:var(--red);border-radius:5px;padding:3px 9px;font-size:12px;cursor:pointer;font-family:inherit;font-weight:600;transition:.12s}
|
||||
.ap-del:hover{background:var(--red-bg)}
|
||||
.ap-edit{background:none;border:1px solid #f0c5ae;color:var(--accent);border-radius:5px;padding:3px 9px;font-size:12px;cursor:pointer;font-family:inherit;font-weight:600;transition:.12s;text-decoration:none}
|
||||
.ap-edit:hover{background:var(--accent-bg)}
|
||||
.ap-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:0;padding:12px 18px 16px}
|
||||
@media(max-width:600px){.ap-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
.ap-field{padding:6px 10px}
|
||||
.ap-label{font-size:10px;font-weight:600;color:var(--hint);text-transform:uppercase;letter-spacing:.5px;margin-bottom:3px}
|
||||
.ap-val{font-size:14px;font-weight:600;color:var(--text)}
|
||||
.ap-val.green{color:var(--green)}
|
||||
.ap-val.red{color:var(--red)}
|
||||
.ap-val.mono{font-size:12px;font-family:'Courier New',monospace;font-weight:500;color:var(--muted)}
|
||||
.drag-hint{font-size:11px;color:var(--hint);padding:0 18px 10px;display:flex;align-items:center;gap:4px}
|
||||
|
||||
@media(max-width:680px){
|
||||
.sidebar{width:100%;max-height:200px;border-right:none;border-bottom:1px solid var(--border)}
|
||||
.main{flex-direction:column}
|
||||
.statsbar .stat:nth-child(n+3){display:none}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="topnav">
|
||||
<a href="index.php" class="brand"><div class="brand-mark">S</div>WebGIS SPBU Pontianak</a>
|
||||
<a href="insert.php" class="nav-a">Input Data</a>
|
||||
<a href="index.php" class="nav-a on">Lihat Peta</a>
|
||||
</nav>
|
||||
|
||||
<div class="statsbar">
|
||||
<div class="stat"><div class="stat-dot" style="background:var(--accent)"></div>Total <strong><?= $total ?></strong> SPBU</div>
|
||||
<div class="stat"><div class="stat-dot" style="background:var(--green)"></div>Buka 24 Jam: <strong><?= $total_24 ?></strong></div>
|
||||
<div class="stat"><div class="stat-dot" style="background:var(--hint)"></div>Terbatas: <strong><?= $total - $total_24 ?></strong></div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- SIDEBAR -->
|
||||
<div class="sidebar">
|
||||
<div class="sb-head">
|
||||
<h2>Daftar SPBU</h2>
|
||||
<input class="search" id="search" type="text" placeholder="Cari nama atau nomor...">
|
||||
</div>
|
||||
<div class="sb-list" id="sb-list">
|
||||
<?php if(!$total): ?>
|
||||
<div class="empty-sb">Belum ada data.<br><a href="insert.php" style="color:var(--accent)">Tambahkan →</a></div>
|
||||
<?php else: foreach($spbu_list as $s): ?>
|
||||
<div class="sb-item" data-id="<?= $s['id'] ?>">
|
||||
<div class="sb-name"><?= htmlspecialchars($s['nama']) ?></div>
|
||||
<div class="sb-no">No. <?= htmlspecialchars($s['no']) ?></div>
|
||||
<span class="sb-badge <?= $s['status']?'sb-open':'sb-closed' ?>"><?= $s['status']?'● 24 Jam':'○ Terbatas' ?></span>
|
||||
</div>
|
||||
<?php endforeach; endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAP -->
|
||||
<div class="mapwrap">
|
||||
<div id="map"></div>
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<div id="attr-panel">
|
||||
<div class="ap-head">
|
||||
<span class="ap-title">⛽ Informasi SPBU</span>
|
||||
<div class="ap-actions">
|
||||
<a href="#" class="ap-edit" id="ap-edit-btn">Edit</a>
|
||||
<button class="ap-del" id="ap-del-btn">Hapus</button>
|
||||
<button class="ap-close" onclick="closePanel()">✕ Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ap-grid">
|
||||
<div class="ap-field"><div class="ap-label">Nama</div><div class="ap-val" id="ap-nama">—</div></div>
|
||||
<div class="ap-field"><div class="ap-label">Nomor SPBU</div><div class="ap-val" id="ap-no">—</div></div>
|
||||
<div class="ap-field"><div class="ap-label">Status</div><div class="ap-val" id="ap-status">—</div></div>
|
||||
<div class="ap-field"><div class="ap-label">Koordinat</div><div class="ap-val mono" id="ap-coords">—</div></div>
|
||||
</div>
|
||||
<div class="drag-hint">⟳ Drag marker di peta untuk memperbarui lokasi secara langsung</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
const DATA = <?= json_encode($spbu_list, JSON_UNESCAPED_UNICODE) ?>;
|
||||
|
||||
// ── Base Layers ──────────────────────────────────────────────────────────────
|
||||
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap', maxZoom: 19
|
||||
});
|
||||
const sat = L.tileLayer(
|
||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||
{ attribution: 'Esri', maxZoom: 19 }
|
||||
);
|
||||
|
||||
// ── Layer Groups: pisah SPBU berdasarkan status ──────────────────────────────
|
||||
const layer24Jam = L.layerGroup(); // SPBU Buka 24 Jam
|
||||
const layerTerbatas = L.layerGroup(); // SPBU Jam Terbatas
|
||||
|
||||
// ── Inisialisasi Peta ────────────────────────────────────────────────────────
|
||||
const map = L.map('map', {
|
||||
center: [-0.0263, 109.3425],
|
||||
zoom: 13,
|
||||
layers: [osm, layer24Jam, layerTerbatas]
|
||||
});
|
||||
|
||||
// ── Layers Control ───────────────────────────────────────────────────────────
|
||||
const baseLayers = {
|
||||
'🗺️ Peta (OpenStreetMap)' : osm,
|
||||
'🛰️ Satelit (Esri)' : sat
|
||||
};
|
||||
const overlays = {
|
||||
'🟢 SPBU Buka 24 Jam' : layer24Jam,
|
||||
'⚫ SPBU Jam Terbatas' : layerTerbatas
|
||||
};
|
||||
|
||||
L.control.layers(baseLayers, overlays, { collapsed: false }).addTo(map);
|
||||
|
||||
// ── Icon Helper ──────────────────────────────────────────────────────────────
|
||||
function mkIcon(color, big) {
|
||||
const s = big ? 34 : 26;
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="width:${s}px;height:${s}px;border-radius:50%;background:${color};
|
||||
border:2.5px solid #fff;box-shadow:0 2px 10px rgba(0,0,0,.22);
|
||||
display:grid;place-items:center;font-size:${big ? 16 : 12}px;transition:.15s;">⛽</div>`,
|
||||
iconSize : [s, s],
|
||||
iconAnchor : [s/2, s/2],
|
||||
popupAnchor: [0, -s/2 - 2]
|
||||
});
|
||||
}
|
||||
|
||||
const markers = {};
|
||||
let activeId = null;
|
||||
|
||||
// ── Buat Marker & Masukkan ke Layer Group yang Sesuai ────────────────────────
|
||||
DATA.forEach(spbu => {
|
||||
const is24 = spbu.status == 1;
|
||||
const color = is24 ? '#1a7a45' : '#b0ada5';
|
||||
|
||||
const m = L.marker([spbu.latitude, spbu.longitude], {
|
||||
icon : mkIcon(color, false),
|
||||
draggable: true
|
||||
});
|
||||
|
||||
m.on('click', () => showAttr(spbu.id));
|
||||
|
||||
// ── Drag & Drop: simpan koordinat baru ke DB via AJAX ────────────────────
|
||||
m.on('dragend', function(e) {
|
||||
const pos = e.target.getLatLng();
|
||||
const lat = pos.lat.toFixed(6);
|
||||
const lng = pos.lng.toFixed(6);
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('id', spbu.id);
|
||||
fd.append('lat', lat);
|
||||
fd.append('lng', lng);
|
||||
|
||||
fetch('index.php?update_coords=1', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
const d = DATA.find(x => x.id == spbu.id);
|
||||
if (d) { d.latitude = lat; d.longitude = lng; }
|
||||
if (activeId == spbu.id) {
|
||||
document.getElementById('ap-coords').textContent = `${lat}, ${lng}`;
|
||||
}
|
||||
showToast('📍 Lokasi berhasil diperbarui!');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('❌ Gagal memperbarui lokasi'));
|
||||
});
|
||||
|
||||
m.addTo(is24 ? layer24Jam : layerTerbatas);
|
||||
|
||||
markers[spbu.id] = { marker: m, data: spbu };
|
||||
});
|
||||
|
||||
// ── Fit Bounds ───────────────────────────────────────────────────────────────
|
||||
if (DATA.length > 0) {
|
||||
const pts = DATA.map(s => [s.latitude, s.longitude]);
|
||||
DATA.length === 1
|
||||
? map.setView(pts[0], 15)
|
||||
: map.fitBounds(pts, { padding: [40, 40] });
|
||||
}
|
||||
|
||||
// ── Panel Atribut ─────────────────────────────────────────────────────────────
|
||||
function showAttr(id) {
|
||||
const obj = markers[id];
|
||||
if (!obj) return;
|
||||
const spbu = DATA.find(x => x.id == id) || obj.data;
|
||||
|
||||
if (activeId && activeId !== id && markers[activeId]) {
|
||||
const prev = DATA.find(x => x.id == activeId) || markers[activeId].data;
|
||||
markers[activeId].marker.setIcon(mkIcon(prev.status == 1 ? '#1a7a45' : '#b0ada5', false));
|
||||
}
|
||||
|
||||
obj.marker.setIcon(mkIcon('#e85a1a', true));
|
||||
activeId = id;
|
||||
|
||||
document.getElementById('ap-nama').textContent = spbu.nama;
|
||||
document.getElementById('ap-no').textContent = spbu.no;
|
||||
const st = document.getElementById('ap-status');
|
||||
if (spbu.status == 1) {
|
||||
st.textContent = '● 24 Jam'; st.className = 'ap-val green';
|
||||
} else {
|
||||
st.textContent = '○ Terbatas'; st.className = 'ap-val red';
|
||||
}
|
||||
document.getElementById('ap-coords').textContent =
|
||||
`${parseFloat(spbu.latitude).toFixed(6)}, ${parseFloat(spbu.longitude).toFixed(6)}`;
|
||||
|
||||
document.getElementById('ap-edit-btn').href = `insert.php?edit=${id}`;
|
||||
document.getElementById('ap-del-btn').onclick = () => deleteSpbu(id, spbu.nama);
|
||||
|
||||
document.getElementById('attr-panel').classList.add('open');
|
||||
map.setView([spbu.latitude, spbu.longitude], Math.max(map.getZoom(), 15), { animate: true });
|
||||
|
||||
document.querySelectorAll('.sb-item').forEach(el => el.classList.remove('active'));
|
||||
const si = document.querySelector(`.sb-item[data-id="${id}"]`);
|
||||
if (si) { si.classList.add('active'); si.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
document.getElementById('attr-panel').classList.remove('open');
|
||||
if (activeId && markers[activeId]) {
|
||||
const prev = DATA.find(x => x.id == activeId) || markers[activeId].data;
|
||||
markers[activeId].marker.setIcon(mkIcon(prev.status == 1 ? '#1a7a45' : '#b0ada5', false));
|
||||
}
|
||||
activeId = null;
|
||||
document.querySelectorAll('.sb-item').forEach(el => el.classList.remove('active'));
|
||||
}
|
||||
|
||||
// ── Hapus SPBU ────────────────────────────────────────────────────────────────
|
||||
function deleteSpbu(id, nama) {
|
||||
if (!confirm(`Hapus SPBU "${nama}"?`)) return;
|
||||
const fd = new FormData();
|
||||
fd.append('id', id);
|
||||
fetch('index.php?delete=1', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
if (markers[id]) {
|
||||
const spbu = DATA.find(x => x.id == id) || markers[id].data;
|
||||
(spbu.status == 1 ? layer24Jam : layerTerbatas).removeLayer(markers[id].marker);
|
||||
delete markers[id];
|
||||
}
|
||||
const si = document.querySelector(`.sb-item[data-id="${id}"]`);
|
||||
if (si) si.remove();
|
||||
closePanel();
|
||||
showToast('✕ SPBU berhasil dihapus');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.sb-item').forEach(el => {
|
||||
el.addEventListener('click', () => showAttr(parseInt(el.dataset.id)));
|
||||
});
|
||||
|
||||
document.getElementById('search').addEventListener('input', function() {
|
||||
const q = this.value.toLowerCase();
|
||||
document.querySelectorAll('.sb-item').forEach(el => {
|
||||
const n = el.querySelector('.sb-name').textContent.toLowerCase();
|
||||
const no = el.querySelector('.sb-no').textContent.toLowerCase();
|
||||
el.style.display = (n.includes(q) || no.includes(q)) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
function showToast(msg) {
|
||||
const t = document.getElementById('toast');
|
||||
t.textContent = msg;
|
||||
t.classList.add('show');
|
||||
setTimeout(() => t.classList.remove('show'), 2500);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
require_once 'db_connect.php';
|
||||
|
||||
$message = "";
|
||||
$edit_data = null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
if ($_POST['action'] === 'insert') {
|
||||
$stmt = $pdo->prepare("INSERT INTO spbu (nama, no, status, latitude, longitude) VALUES (:nama,:no,:status,:lat,:lng)");
|
||||
$stmt->execute([':nama'=>trim($_POST['nama']),':no'=>trim($_POST['no']),':status'=>isset($_POST['status'])?1:0,':lat'=>floatval($_POST['latitude']),':lng'=>floatval($_POST['longitude'])]);
|
||||
$message = ['type'=>'success','text'=>'Data SPBU berhasil ditambahkan!'];
|
||||
}
|
||||
if ($_POST['action'] === 'update') {
|
||||
$stmt = $pdo->prepare("UPDATE spbu SET nama=:nama,no=:no,status=:status,latitude=:lat,longitude=:lng WHERE id=:id");
|
||||
$stmt->execute([':nama'=>trim($_POST['nama']),':no'=>trim($_POST['no']),':status'=>isset($_POST['status'])?1:0,':lat'=>floatval($_POST['latitude']),':lng'=>floatval($_POST['longitude']),':id'=>intval($_POST['id'])]);
|
||||
$message = ['type'=>'success','text'=>'Data berhasil diperbarui!'];
|
||||
}
|
||||
if ($_POST['action'] === 'delete') {
|
||||
$pdo->prepare("DELETE FROM spbu WHERE id=:id")->execute([':id'=>intval($_POST['id'])]);
|
||||
$message = ['type'=>'danger','text'=>'Data berhasil dihapus.'];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET['edit'])) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM spbu WHERE id=:id");
|
||||
$stmt->execute([':id'=>intval($_GET['edit'])]);
|
||||
$edit_data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$all = $pdo->query("SELECT * FROM spbu ORDER BY id DESC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Input SPBU — WebGIS Pontianak</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#f5f4f0;--card:#fff;--border:#e8e5df;--border2:#d4d0c8;
|
||||
--text:#1a1915;--muted:#7c7a72;--hint:#b0ada5;
|
||||
--accent:#e85a1a;--accent-bg:#fdf0eb;--accent-dk:#c44a12;
|
||||
--green:#1a7a45;--green-bg:#ebf7f0;
|
||||
--red:#c0392b;--red-bg:#fdf0ef;
|
||||
--r:10px;--r-sm:7px;
|
||||
--shadow:0 1px 2px rgba(0,0,0,.05),0 3px 12px rgba(0,0,0,.04);
|
||||
}
|
||||
body{font-family:'Plus Jakarta Sans',sans-serif;background:var(--bg);color:var(--text);font-size:14px;line-height:1.5}
|
||||
|
||||
/* TOP NAV */
|
||||
.topnav{background:var(--card);border-bottom:1px solid var(--border);height:54px;padding:0 24px;display:flex;align-items:center;gap:6px;position:sticky;top:0;z-index:9999}
|
||||
.brand{display:flex;align-items:center;gap:9px;font-weight:700;font-size:14px;color:var(--text);text-decoration:none;margin-right:auto}
|
||||
.brand-mark{width:30px;height:30px;background:var(--accent);border-radius:8px;display:grid;place-items:center;color:#fff;font-size:14px;font-weight:800;letter-spacing:-.5px}
|
||||
.nav-a{padding:5px 13px;border-radius:var(--r-sm);font-size:13px;font-weight:500;color:var(--muted);text-decoration:none;transition:.12s}
|
||||
.nav-a:hover{background:var(--bg);color:var(--text)}
|
||||
.nav-a.on{background:var(--accent-bg);color:var(--accent);font-weight:600}
|
||||
|
||||
/* PAGE */
|
||||
.page{max-width:1140px;margin:0 auto;padding:24px 20px}
|
||||
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:18px}
|
||||
@media(max-width:840px){.grid2{grid-template-columns:1fr}}
|
||||
|
||||
/* CARD */
|
||||
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--r);box-shadow:var(--shadow)}
|
||||
.ch{padding:16px 18px 13px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
|
||||
.ch-icon{width:30px;height:30px;border-radius:var(--r-sm);background:var(--bg);display:grid;place-items:center;font-size:14px;flex-shrink:0}
|
||||
.ch h2{font-size:13px;font-weight:600;color:var(--text)}
|
||||
.cb{padding:18px}
|
||||
|
||||
/* ALERT */
|
||||
.alert{display:flex;align-items:center;gap:8px;padding:10px 14px;border-radius:var(--r-sm);margin-bottom:18px;font-size:13px;font-weight:500}
|
||||
.alert.success{background:var(--green-bg);color:var(--green)}
|
||||
.alert.danger{background:var(--red-bg);color:var(--red)}
|
||||
|
||||
/* MAP */
|
||||
#map{height:310px;border-radius:var(--r-sm);border:1px solid var(--border);overflow:hidden}
|
||||
.map-tip{margin-top:8px;font-size:12px;color:var(--hint);display:flex;align-items:center;gap:4px}
|
||||
.coord-tag{display:none;margin-top:10px;background:var(--accent-bg);border:1px solid #f0c5ae;border-radius:20px;padding:4px 12px;font-size:12px;font-weight:500;color:var(--accent);width:fit-content}
|
||||
|
||||
/* FORM */
|
||||
.fg{margin-bottom:13px}
|
||||
.fg label{display:block;font-size:11px;font-weight:600;color:var(--hint);text-transform:uppercase;letter-spacing:.5px;margin-bottom:5px}
|
||||
.fg input[type=text],.fg input[type=number]{width:100%;padding:8px 11px;border:1px solid var(--border2);border-radius:var(--r-sm);font-family:inherit;font-size:13px;color:var(--text);background:#fff;transition:.12s}
|
||||
.fg input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(232,90,26,.09)}
|
||||
.cols2{display:grid;grid-template-columns:1fr 1fr;gap:9px}
|
||||
|
||||
/* TOGGLE */
|
||||
.trow{display:flex;align-items:center;gap:9px;padding:8px 0}
|
||||
.sw{position:relative;width:36px;height:19px;flex-shrink:0}
|
||||
.sw input{opacity:0;position:absolute;width:0;height:0}
|
||||
.sw-t{position:absolute;inset:0;background:var(--border2);border-radius:20px;cursor:pointer;transition:.18s}
|
||||
.sw-t::after{content:'';position:absolute;width:13px;height:13px;background:#fff;border-radius:50%;top:3px;left:3px;transition:.18s}
|
||||
.sw input:checked~.sw-t{background:var(--green)}
|
||||
.sw input:checked~.sw-t::after{transform:translateX(17px)}
|
||||
.trow span{font-size:13px;font-weight:500}
|
||||
|
||||
/* BUTTONS */
|
||||
.btn{display:inline-flex;align-items:center;gap:5px;padding:8px 16px;border-radius:var(--r-sm);font-family:inherit;font-size:13px;font-weight:600;border:none;cursor:pointer;transition:.12s;text-decoration:none}
|
||||
.btn-p{background:var(--accent);color:#fff}
|
||||
.btn-p:hover{background:var(--accent-dk)}
|
||||
.btn-g{background:transparent;color:var(--muted);border:1px solid var(--border2)}
|
||||
.btn-g:hover{background:var(--bg);color:var(--text)}
|
||||
.btn-xs{padding:4px 10px;font-size:11px;border-radius:6px}
|
||||
.btn-edit{background:transparent;color:var(--accent);border:1px solid #f0c5ae}
|
||||
.btn-edit:hover{background:var(--accent-bg)}
|
||||
.btn-del{background:transparent;color:var(--red);border:1px solid #f0c4bf}
|
||||
.btn-del:hover{background:var(--red-bg)}
|
||||
.btn-row{display:flex;gap:8px;margin-top:16px}
|
||||
|
||||
/* TABLE */
|
||||
.tw{overflow-x:auto}
|
||||
table{width:100%;border-collapse:collapse;font-size:13px}
|
||||
th{padding:9px 14px;text-align:left;font-size:11px;font-weight:600;color:var(--hint);text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid var(--border);white-space:nowrap}
|
||||
td{padding:10px 14px;border-bottom:1px solid var(--bg);vertical-align:middle}
|
||||
tr:last-child td{border-bottom:none}
|
||||
tr:hover td{background:#fafaf8}
|
||||
.badge{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border-radius:20px;font-size:11px;font-weight:600}
|
||||
.b-open{background:var(--green-bg);color:var(--green)}
|
||||
.b-closed{background:var(--bg);color:var(--hint);border:1px solid var(--border)}
|
||||
.acts{display:flex;gap:5px}
|
||||
.mono{font-family:'Courier New',monospace;font-size:12px;color:var(--muted)}
|
||||
.empty{text-align:center;padding:40px;color:var(--hint)}
|
||||
.pill{background:var(--bg);border:1px solid var(--border);border-radius:20px;padding:1px 8px;font-size:11px;color:var(--hint);margin-left:6px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="topnav">
|
||||
<a href="index.php" class="brand">
|
||||
<div class="brand-mark">S</div>
|
||||
WebGIS SPBU Pontianak
|
||||
</a>
|
||||
<a href="insert.php" class="nav-a on">Input Data</a>
|
||||
<a href="index.php" class="nav-a">Lihat Peta</a>
|
||||
</nav>
|
||||
|
||||
<div class="page">
|
||||
|
||||
<?php if($message): ?>
|
||||
<div class="alert <?= $message['type'] ?>">
|
||||
<?= $message['type']==='success' ? '✓' : '✕' ?> <?= $message['text'] ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="grid2">
|
||||
<div class="card">
|
||||
<div class="ch"><div class="ch-icon">📍</div><h2>Klik Peta — Tentukan Lokasi</h2></div>
|
||||
<div class="cb">
|
||||
<div id="map"></div>
|
||||
<div class="map-tip">Klik titik mana saja untuk mengisi koordinat otomatis</div>
|
||||
<div class="coord-tag" id="coord-tag"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="ch">
|
||||
<div class="ch-icon"><?= $edit_data ? '✏️' : '➕' ?></div>
|
||||
<h2><?= $edit_data ? 'Edit Data SPBU' : 'Tambah SPBU Baru' ?></h2>
|
||||
</div>
|
||||
<div class="cb">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="<?= $edit_data ? 'update' : 'insert' ?>">
|
||||
<?php if($edit_data): ?>
|
||||
<input type="hidden" name="id" value="<?= $edit_data['id'] ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="fg">
|
||||
<label>Nama SPBU</label>
|
||||
<input type="text" name="nama" placeholder="cth: SPBU Jl. Ahmad Yani"
|
||||
value="<?= htmlspecialchars($edit_data['nama'] ?? '') ?>" required>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>Nomor SPBU</label>
|
||||
<input type="text" name="no" placeholder="cth: 64.751.01"
|
||||
value="<?= htmlspecialchars($edit_data['no'] ?? '') ?>" required>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>Status Operasional</label>
|
||||
<div class="trow">
|
||||
<label class="sw">
|
||||
<input type="checkbox" name="status" id="chk" <?= ($edit_data['status'] ?? 0) ? 'checked' : '' ?>>
|
||||
<span class="sw-t"></span>
|
||||
</label>
|
||||
<span>Buka 24 Jam</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fg">
|
||||
<label>Koordinat</label>
|
||||
<div class="cols2">
|
||||
<input type="number" name="latitude" id="inp-lat" step="any" placeholder="Latitude"
|
||||
value="<?= $edit_data['latitude'] ?? '' ?>" required>
|
||||
<input type="number" name="longitude" id="inp-lng" step="any" placeholder="Longitude"
|
||||
value="<?= $edit_data['longitude'] ?? '' ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
<button type="submit" class="btn btn-p"><?= $edit_data ? '💾 Simpan' : '📍 Simpan SPBU' ?></button>
|
||||
<?php if($edit_data): ?><a href="insert.php" class="btn btn-g">Batal</a><?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TABEL -->
|
||||
<div class="card" style="margin-top:18px">
|
||||
<div class="ch">
|
||||
<div class="ch-icon">📋</div>
|
||||
<h2>Daftar SPBU <span class="pill"><?= count($all) ?> data</span></h2>
|
||||
</div>
|
||||
<div class="tw">
|
||||
<?php if(!count($all)): ?>
|
||||
<div class="empty">Belum ada data. Tambahkan SPBU lewat form di atas.</div>
|
||||
<?php else: ?>
|
||||
<table>
|
||||
<thead><tr><th>#</th><th>Nama</th><th>No</th><th>Status</th><th>Latitude</th><th>Longitude</th><th>Aksi</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach($all as $i=>$r): ?>
|
||||
<tr>
|
||||
<td style="color:var(--hint)"><?= $i+1 ?></td>
|
||||
<td style="font-weight:600"><?= htmlspecialchars($r['nama']) ?></td>
|
||||
<td style="color:var(--muted)"><?= htmlspecialchars($r['no']) ?></td>
|
||||
<td><span class="badge <?= $r['status']?'b-open':'b-closed' ?>"><?= $r['status']?'● 24 Jam':'○ Terbatas' ?></span></td>
|
||||
<td class="mono"><?= number_format($r['latitude'],6) ?></td>
|
||||
<td class="mono"><?= number_format($r['longitude'],6) ?></td>
|
||||
<td>
|
||||
<div class="acts">
|
||||
<a href="?edit=<?= $r['id'] ?>" class="btn btn-xs btn-edit">Edit</a>
|
||||
<form method="POST" style="display:inline" onsubmit="return confirm('Hapus SPBU ini?')">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="<?= $r['id'] ?>">
|
||||
<button type="submit" class="btn btn-xs btn-del">Hapus</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
const map=L.map('map').setView([-0.0263,109.3425],13);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OSM',maxZoom:19}).addTo(map);
|
||||
|
||||
function mkIcon(color,big){
|
||||
const s=big?32:26;
|
||||
return L.divIcon({className:'',
|
||||
html:`<div style="width:${s}px;height:${s}px;border-radius:50%;background:${color};border:2px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.2);display:grid;place-items:center;font-size:${big?15:12}px;">⛽</div>`,
|
||||
iconSize:[s,s],iconAnchor:[s/2,s/2],popupAnchor:[0,-s/2]});
|
||||
}
|
||||
|
||||
let marker=null;
|
||||
|
||||
<?php if($edit_data): ?>
|
||||
marker=L.marker([<?= $edit_data['latitude'] ?>,<?= $edit_data['longitude'] ?>],{icon:mkIcon('#e85a1a',true),draggable:true}).addTo(map);
|
||||
map.setView([<?= $edit_data['latitude'] ?>,<?= $edit_data['longitude'] ?>],16);
|
||||
marker.on('dragend',e=>{const p=e.target.getLatLng();setCoord(p.lat.toFixed(6),p.lng.toFixed(6));});
|
||||
setCoord(<?= $edit_data['latitude'] ?>,<?= $edit_data['longitude'] ?>);
|
||||
<?php endif; ?>
|
||||
|
||||
<?= json_encode($all) ?>.forEach(s=>{
|
||||
L.marker([s.latitude,s.longitude],{icon:mkIcon(s.status==1?'#1a7a45':'#b0ada5',false)})
|
||||
.addTo(map).bindPopup(`<b>${s.nama}</b><br>${s.no}<br>${s.status==1?'24 Jam':'Terbatas'}`);
|
||||
});
|
||||
|
||||
map.on('click',e=>{
|
||||
const lat=e.latlng.lat.toFixed(6),lng=e.latlng.lng.toFixed(6);
|
||||
if(marker) map.removeLayer(marker);
|
||||
marker=L.marker([lat,lng],{icon:mkIcon('#e85a1a',true),draggable:true}).addTo(map);
|
||||
marker.on('dragend',ev=>{const p=ev.target.getLatLng();setCoord(p.lat.toFixed(6),p.lng.toFixed(6));});
|
||||
setCoord(lat,lng);
|
||||
});
|
||||
|
||||
function setCoord(lat,lng){
|
||||
document.getElementById('inp-lat').value=lat;
|
||||
document.getElementById('inp-lng').value=lng;
|
||||
const t=document.getElementById('coord-tag');
|
||||
t.textContent=`📍 ${lat}, ${lng}`;
|
||||
t.style.display='block';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,414 @@
|
||||
<?php
|
||||
require_once 'db_connect.php';
|
||||
|
||||
// AJAX: update koordinat saat marker di-drag
|
||||
if (isset($_GET['update_coords']) && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
header('Content-Type: application/json');
|
||||
$id = intval($_POST['id']);
|
||||
$lat = floatval($_POST['lat']);
|
||||
$lng = floatval($_POST['lng']);
|
||||
if($id > 0){
|
||||
$pdo->prepare("UPDATE spbu SET latitude=:lat,longitude=:lng WHERE id=:id")
|
||||
->execute([':lat'=>$lat,':lng'=>$lng,':id'=>$id]);
|
||||
echo json_encode(['ok'=>true,'lat'=>$lat,'lng'=>$lng]);
|
||||
} else {
|
||||
echo json_encode(['ok'=>false]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// AJAX: hapus dari show.php
|
||||
if (isset($_GET['delete']) && $_SERVER['REQUEST_METHOD']==='POST') {
|
||||
header('Content-Type: application/json');
|
||||
$id = intval($_POST['id']);
|
||||
$pdo->prepare("DELETE FROM spbu WHERE id=:id")->execute([':id'=>$id]);
|
||||
echo json_encode(['ok'=>true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$spbu_list = $pdo->query("SELECT * FROM spbu ORDER BY nama")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$total = count($spbu_list);
|
||||
$total_24 = count(array_filter($spbu_list, fn($s) => $s['status']==1));
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Peta SPBU — WebGIS Pontianak</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#f5f4f0;--card:#fff;--border:#e8e5df;--border2:#d4d0c8;
|
||||
--text:#1a1915;--muted:#7c7a72;--hint:#b0ada5;
|
||||
--accent:#e85a1a;--accent-bg:#fdf0eb;--accent-dk:#c44a12;
|
||||
--green:#1a7a45;--green-bg:#ebf7f0;
|
||||
--red:#c0392b;--red-bg:#fdf0ef;
|
||||
--r:10px;--r-sm:7px;
|
||||
}
|
||||
html,body{height:100%;font-family:'Plus Jakarta Sans',sans-serif;color:var(--text);font-size:14px}
|
||||
body{display:flex;flex-direction:column;background:var(--bg)}
|
||||
|
||||
/* NAV */
|
||||
.topnav{background:var(--card);border-bottom:1px solid var(--border);height:54px;padding:0 24px;display:flex;align-items:center;gap:6px;flex-shrink:0;z-index:9999;position:relative}
|
||||
.brand{display:flex;align-items:center;gap:9px;font-weight:700;font-size:14px;color:var(--text);text-decoration:none;margin-right:auto}
|
||||
.brand-mark{width:30px;height:30px;background:var(--accent);border-radius:8px;display:grid;place-items:center;color:#fff;font-size:14px;font-weight:800}
|
||||
.nav-a{padding:5px 13px;border-radius:var(--r-sm);font-size:13px;font-weight:500;color:var(--muted);text-decoration:none;transition:.12s}
|
||||
.nav-a:hover{background:var(--bg);color:var(--text)}
|
||||
.nav-a.on{background:var(--accent-bg);color:var(--accent);font-weight:600}
|
||||
|
||||
/* STATS BAR */
|
||||
.statsbar{background:var(--card);border-bottom:1px solid var(--border);padding:0 24px;height:40px;display:flex;align-items:center;gap:18px;flex-shrink:0}
|
||||
.stat{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
|
||||
.stat strong{color:var(--text);font-weight:600}
|
||||
.stat-dot{width:8px;height:8px;border-radius:50%}
|
||||
|
||||
/* MAIN */
|
||||
.main{display:flex;flex:1;overflow:hidden}
|
||||
|
||||
/* SIDEBAR */
|
||||
.sidebar{width:300px;flex-shrink:0;background:var(--card);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
|
||||
.sb-head{padding:14px 16px 10px;border-bottom:1px solid var(--border);flex-shrink:0}
|
||||
.sb-head h2{font-size:13px;font-weight:600;margin-bottom:9px;color:var(--text)}
|
||||
.search{width:100%;padding:7px 11px;border:1px solid var(--border2);border-radius:var(--r-sm);font-family:inherit;font-size:13px;color:var(--text);background:#fff;transition:.12s}
|
||||
.search:focus{outline:none;border-color:var(--accent)}
|
||||
.sb-list{flex:1;overflow-y:auto;padding:8px}
|
||||
.sb-list::-webkit-scrollbar{width:3px}
|
||||
.sb-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
||||
.sb-item{border:1px solid var(--border);border-radius:var(--r-sm);padding:10px 12px;margin-bottom:6px;cursor:pointer;transition:.12s}
|
||||
.sb-item:hover{border-color:var(--border2);background:#fafaf8}
|
||||
.sb-item.active{border-color:var(--accent);background:var(--accent-bg)}
|
||||
.sb-name{font-weight:600;font-size:13px;margin-bottom:2px}
|
||||
.sb-no{font-size:12px;color:var(--muted)}
|
||||
.sb-badge{display:inline-flex;align-items:center;gap:3px;margin-top:5px;padding:2px 8px;border-radius:20px;font-size:11px;font-weight:600}
|
||||
.sb-open{background:var(--green-bg);color:var(--green)}
|
||||
.sb-closed{background:var(--bg);color:var(--hint);border:1px solid var(--border)}
|
||||
.empty-sb{text-align:center;padding:32px 16px;color:var(--hint);font-size:13px}
|
||||
|
||||
/* MAP */
|
||||
.mapwrap{flex:1;position:relative;overflow:hidden}
|
||||
#map{width:100%;height:100%}
|
||||
|
||||
/* Rapikan tampilan Leaflet Layers Control */
|
||||
.leaflet-control-layers{
|
||||
border:1px solid var(--border) !important;
|
||||
border-radius:var(--r-sm) !important;
|
||||
box-shadow:0 2px 12px rgba(0,0,0,.09) !important;
|
||||
font-family:'Plus Jakarta Sans',sans-serif !important;
|
||||
font-size:13px !important;
|
||||
}
|
||||
.leaflet-control-layers-expanded{padding:10px 14px !important}
|
||||
.leaflet-control-layers-separator{margin:6px 0 !important;border-color:var(--border) !important}
|
||||
|
||||
/* TOAST */
|
||||
.toast{position:absolute;top:12px;left:50%;transform:translateX(-50%) translateY(-60px);z-index:1001;background:#1a1915;color:#fff;font-size:12px;font-weight:500;padding:8px 16px;border-radius:20px;transition:.25s;pointer-events:none;white-space:nowrap}
|
||||
.toast.show{transform:translateX(-50%) translateY(0)}
|
||||
|
||||
/* ATTR PANEL */
|
||||
#attr-panel{
|
||||
position:absolute;bottom:0;left:0;right:0;
|
||||
background:var(--card);border-top:2px solid var(--accent);
|
||||
transform:translateY(100%);transition:transform .28s cubic-bezier(.4,0,.2,1);
|
||||
z-index:1000;
|
||||
}
|
||||
#attr-panel.open{transform:translateY(0)}
|
||||
.ap-head{padding:12px 18px 10px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.ap-title{font-size:13px;font-weight:700;color:var(--accent)}
|
||||
.ap-actions{display:flex;gap:7px;align-items:center}
|
||||
.ap-close{background:none;border:1px solid var(--border2);color:var(--muted);border-radius:5px;padding:3px 9px;font-size:12px;cursor:pointer;font-family:inherit;transition:.12s}
|
||||
.ap-close:hover{border-color:var(--muted)}
|
||||
.ap-del{background:none;border:1px solid #f0c4bf;color:var(--red);border-radius:5px;padding:3px 9px;font-size:12px;cursor:pointer;font-family:inherit;font-weight:600;transition:.12s}
|
||||
.ap-del:hover{background:var(--red-bg)}
|
||||
.ap-edit{background:none;border:1px solid #f0c5ae;color:var(--accent);border-radius:5px;padding:3px 9px;font-size:12px;cursor:pointer;font-family:inherit;font-weight:600;transition:.12s;text-decoration:none}
|
||||
.ap-edit:hover{background:var(--accent-bg)}
|
||||
.ap-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:0;padding:12px 18px 16px}
|
||||
@media(max-width:600px){.ap-grid{grid-template-columns:repeat(2,1fr)}}
|
||||
.ap-field{padding:6px 10px}
|
||||
.ap-label{font-size:10px;font-weight:600;color:var(--hint);text-transform:uppercase;letter-spacing:.5px;margin-bottom:3px}
|
||||
.ap-val{font-size:14px;font-weight:600;color:var(--text)}
|
||||
.ap-val.green{color:var(--green)}
|
||||
.ap-val.red{color:var(--red)}
|
||||
.ap-val.mono{font-size:12px;font-family:'Courier New',monospace;font-weight:500;color:var(--muted)}
|
||||
.drag-hint{font-size:11px;color:var(--hint);padding:0 18px 10px;display:flex;align-items:center;gap:4px}
|
||||
|
||||
@media(max-width:680px){
|
||||
.sidebar{width:100%;max-height:200px;border-right:none;border-bottom:1px solid var(--border)}
|
||||
.main{flex-direction:column}
|
||||
.statsbar .stat:nth-child(n+3){display:none}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="topnav">
|
||||
<a href="index.php" class="brand"><div class="brand-mark">S</div>WebGIS SPBU Pontianak</a>
|
||||
<a href="insert.php" class="nav-a">Input Data</a>
|
||||
<a href="index.php" class="nav-a on">Lihat Peta</a>
|
||||
</nav>
|
||||
|
||||
<div class="statsbar">
|
||||
<div class="stat"><div class="stat-dot" style="background:var(--accent)"></div>Total <strong><?= $total ?></strong> SPBU</div>
|
||||
<div class="stat"><div class="stat-dot" style="background:var(--green)"></div>Buka 24 Jam: <strong><?= $total_24 ?></strong></div>
|
||||
<div class="stat"><div class="stat-dot" style="background:var(--hint)"></div>Terbatas: <strong><?= $total - $total_24 ?></strong></div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- SIDEBAR -->
|
||||
<div class="sidebar">
|
||||
<div class="sb-head">
|
||||
<h2>Daftar SPBU</h2>
|
||||
<input class="search" id="search" type="text" placeholder="Cari nama atau nomor...">
|
||||
</div>
|
||||
<div class="sb-list" id="sb-list">
|
||||
<?php if(!$total): ?>
|
||||
<div class="empty-sb">Belum ada data.<br><a href="insert.php" style="color:var(--accent)">Tambahkan →</a></div>
|
||||
<?php else: foreach($spbu_list as $s): ?>
|
||||
<div class="sb-item" data-id="<?= $s['id'] ?>">
|
||||
<div class="sb-name"><?= htmlspecialchars($s['nama']) ?></div>
|
||||
<div class="sb-no">No. <?= htmlspecialchars($s['no']) ?></div>
|
||||
<span class="sb-badge <?= $s['status']?'sb-open':'sb-closed' ?>"><?= $s['status']?'● 24 Jam':'○ Terbatas' ?></span>
|
||||
</div>
|
||||
<?php endforeach; endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAP -->
|
||||
<div class="mapwrap">
|
||||
<div id="map"></div>
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<div id="attr-panel">
|
||||
<div class="ap-head">
|
||||
<span class="ap-title">⛽ Informasi SPBU</span>
|
||||
<div class="ap-actions">
|
||||
<a href="#" class="ap-edit" id="ap-edit-btn">Edit</a>
|
||||
<button class="ap-del" id="ap-del-btn">Hapus</button>
|
||||
<button class="ap-close" onclick="closePanel()">✕ Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ap-grid">
|
||||
<div class="ap-field"><div class="ap-label">Nama</div><div class="ap-val" id="ap-nama">—</div></div>
|
||||
<div class="ap-field"><div class="ap-label">Nomor SPBU</div><div class="ap-val" id="ap-no">—</div></div>
|
||||
<div class="ap-field"><div class="ap-label">Status</div><div class="ap-val" id="ap-status">—</div></div>
|
||||
<div class="ap-field"><div class="ap-label">Koordinat</div><div class="ap-val mono" id="ap-coords">—</div></div>
|
||||
</div>
|
||||
<div class="drag-hint">⟳ Drag marker di peta untuk memperbarui lokasi secara langsung</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
const DATA = <?= json_encode($spbu_list, JSON_UNESCAPED_UNICODE) ?>;
|
||||
|
||||
// ── Base Layers ──────────────────────────────────────────────────────────────
|
||||
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap', maxZoom: 19
|
||||
});
|
||||
const sat = L.tileLayer(
|
||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||
{ attribution: 'Esri', maxZoom: 19 }
|
||||
);
|
||||
|
||||
// ── Layer Groups: pisah SPBU berdasarkan status ──────────────────────────────
|
||||
// Setiap group berisi marker-marker sesuai jenisnya.
|
||||
// Pengguna bisa toggle tampilan lewat checkbox di Layers Control.
|
||||
const layer24Jam = L.layerGroup(); // SPBU Buka 24 Jam
|
||||
const layerTerbatas = L.layerGroup(); // SPBU Jam Terbatas
|
||||
|
||||
// ── Inisialisasi Peta ────────────────────────────────────────────────────────
|
||||
// Sertakan kedua overlay agar keduanya aktif (ter-centang) saat halaman dimuat
|
||||
const map = L.map('map', {
|
||||
center: [-0.0263, 109.3425],
|
||||
zoom: 13,
|
||||
layers: [osm, layer24Jam, layerTerbatas]
|
||||
});
|
||||
|
||||
// ── Layers Control ───────────────────────────────────────────────────────────
|
||||
// baseLayers → radio button, hanya bisa pilih satu base map
|
||||
// overlays → checkbox, bisa pilih / sembunyikan per jenis SPBU
|
||||
const baseLayers = {
|
||||
'🗺️ Peta (OpenStreetMap)' : osm,
|
||||
'🛰️ Satelit (Esri)' : sat
|
||||
};
|
||||
const overlays = {
|
||||
'🟢 SPBU Buka 24 Jam' : layer24Jam,
|
||||
'⚫ SPBU Jam Terbatas' : layerTerbatas
|
||||
};
|
||||
|
||||
// collapsed:false → panel langsung terbuka tanpa harus hover
|
||||
L.control.layers(baseLayers, overlays, { collapsed: false }).addTo(map);
|
||||
|
||||
// ── Icon Helper ──────────────────────────────────────────────────────────────
|
||||
function mkIcon(color, big) {
|
||||
const s = big ? 34 : 26;
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="width:${s}px;height:${s}px;border-radius:50%;background:${color};
|
||||
border:2.5px solid #fff;box-shadow:0 2px 10px rgba(0,0,0,.22);
|
||||
display:grid;place-items:center;font-size:${big ? 16 : 12}px;transition:.15s;">⛽</div>`,
|
||||
iconSize : [s, s],
|
||||
iconAnchor : [s/2, s/2],
|
||||
popupAnchor: [0, -s/2 - 2]
|
||||
});
|
||||
}
|
||||
|
||||
const markers = {};
|
||||
let activeId = null;
|
||||
|
||||
// ── Buat Marker & Masukkan ke Layer Group yang Sesuai ────────────────────────
|
||||
DATA.forEach(spbu => {
|
||||
const is24 = spbu.status == 1;
|
||||
const color = is24 ? '#1a7a45' : '#b0ada5';
|
||||
|
||||
// draggable:true → aktifkan fitur drag & drop lokasi
|
||||
const m = L.marker([spbu.latitude, spbu.longitude], {
|
||||
icon : mkIcon(color, false),
|
||||
draggable: true
|
||||
});
|
||||
|
||||
// Klik marker → tampilkan panel atribut
|
||||
m.on('click', () => showAttr(spbu.id));
|
||||
|
||||
// ── Drag & Drop: simpan koordinat baru ke DB via AJAX ────────────────────
|
||||
m.on('dragend', function(e) {
|
||||
const pos = e.target.getLatLng();
|
||||
const lat = pos.lat.toFixed(6);
|
||||
const lng = pos.lng.toFixed(6);
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('id', spbu.id);
|
||||
fd.append('lat', lat);
|
||||
fd.append('lng', lng);
|
||||
|
||||
fetch('show.php?update_coords=1', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
// Sinkronkan data lokal agar panel atribut juga update
|
||||
const d = DATA.find(x => x.id == spbu.id);
|
||||
if (d) { d.latitude = lat; d.longitude = lng; }
|
||||
if (activeId == spbu.id) {
|
||||
document.getElementById('ap-coords').textContent = `${lat}, ${lng}`;
|
||||
}
|
||||
showToast('📍 Lokasi berhasil diperbarui!');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('❌ Gagal memperbarui lokasi'));
|
||||
});
|
||||
|
||||
// Masukkan ke layer group yang tepat
|
||||
m.addTo(is24 ? layer24Jam : layerTerbatas);
|
||||
|
||||
markers[spbu.id] = { marker: m, data: spbu };
|
||||
});
|
||||
|
||||
// ── Fit Bounds ───────────────────────────────────────────────────────────────
|
||||
if (DATA.length > 0) {
|
||||
const pts = DATA.map(s => [s.latitude, s.longitude]);
|
||||
DATA.length === 1
|
||||
? map.setView(pts[0], 15)
|
||||
: map.fitBounds(pts, { padding: [40, 40] });
|
||||
}
|
||||
|
||||
// ── Panel Atribut ─────────────────────────────────────────────────────────────
|
||||
function showAttr(id) {
|
||||
const obj = markers[id];
|
||||
if (!obj) return;
|
||||
const spbu = DATA.find(x => x.id == id) || obj.data;
|
||||
|
||||
// Reset icon marker yang sebelumnya aktif
|
||||
if (activeId && activeId !== id && markers[activeId]) {
|
||||
const prev = DATA.find(x => x.id == activeId) || markers[activeId].data;
|
||||
markers[activeId].marker.setIcon(mkIcon(prev.status == 1 ? '#1a7a45' : '#b0ada5', false));
|
||||
}
|
||||
|
||||
// Besarkan & warnai marker aktif
|
||||
obj.marker.setIcon(mkIcon('#e85a1a', true));
|
||||
activeId = id;
|
||||
|
||||
// Isi panel
|
||||
document.getElementById('ap-nama').textContent = spbu.nama;
|
||||
document.getElementById('ap-no').textContent = spbu.no;
|
||||
const st = document.getElementById('ap-status');
|
||||
if (spbu.status == 1) {
|
||||
st.textContent = '● 24 Jam'; st.className = 'ap-val green';
|
||||
} else {
|
||||
st.textContent = '○ Terbatas'; st.className = 'ap-val red';
|
||||
}
|
||||
document.getElementById('ap-coords').textContent =
|
||||
`${parseFloat(spbu.latitude).toFixed(6)}, ${parseFloat(spbu.longitude).toFixed(6)}`;
|
||||
|
||||
document.getElementById('ap-edit-btn').href = `insert.php?edit=${id}`;
|
||||
document.getElementById('ap-del-btn').onclick = () => deleteSpbu(id, spbu.nama);
|
||||
|
||||
document.getElementById('attr-panel').classList.add('open');
|
||||
map.setView([spbu.latitude, spbu.longitude], Math.max(map.getZoom(), 15), { animate: true });
|
||||
|
||||
// Highlight sidebar
|
||||
document.querySelectorAll('.sb-item').forEach(el => el.classList.remove('active'));
|
||||
const si = document.querySelector(`.sb-item[data-id="${id}"]`);
|
||||
if (si) { si.classList.add('active'); si.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
document.getElementById('attr-panel').classList.remove('open');
|
||||
if (activeId && markers[activeId]) {
|
||||
const prev = DATA.find(x => x.id == activeId) || markers[activeId].data;
|
||||
markers[activeId].marker.setIcon(mkIcon(prev.status == 1 ? '#1a7a45' : '#b0ada5', false));
|
||||
}
|
||||
activeId = null;
|
||||
document.querySelectorAll('.sb-item').forEach(el => el.classList.remove('active'));
|
||||
}
|
||||
|
||||
// ── Hapus SPBU ────────────────────────────────────────────────────────────────
|
||||
function deleteSpbu(id, nama) {
|
||||
if (!confirm(`Hapus SPBU "${nama}"?`)) return;
|
||||
const fd = new FormData();
|
||||
fd.append('id', id);
|
||||
fetch('show.php?delete=1', { method: 'POST', body: fd })
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
if (markers[id]) {
|
||||
// Hapus dari layer group yang tepat, bukan langsung dari map
|
||||
const spbu = DATA.find(x => x.id == id) || markers[id].data;
|
||||
(spbu.status == 1 ? layer24Jam : layerTerbatas).removeLayer(markers[id].marker);
|
||||
delete markers[id];
|
||||
}
|
||||
const si = document.querySelector(`.sb-item[data-id="${id}"]`);
|
||||
if (si) si.remove();
|
||||
closePanel();
|
||||
showToast('✕ SPBU berhasil dihapus');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Sidebar: klik & search ────────────────────────────────────────────────────
|
||||
document.querySelectorAll('.sb-item').forEach(el => {
|
||||
el.addEventListener('click', () => showAttr(parseInt(el.dataset.id)));
|
||||
});
|
||||
|
||||
document.getElementById('search').addEventListener('input', function() {
|
||||
const q = this.value.toLowerCase();
|
||||
document.querySelectorAll('.sb-item').forEach(el => {
|
||||
const n = el.querySelector('.sb-name').textContent.toLowerCase();
|
||||
const no = el.querySelector('.sb-no').textContent.toLowerCase();
|
||||
el.style.display = (n.includes(q) || no.includes(q)) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// ── Toast notifikasi ──────────────────────────────────────────────────────────
|
||||
function showToast(msg) {
|
||||
const t = document.getElementById('toast');
|
||||
t.textContent = msg;
|
||||
t.classList.add('show');
|
||||
setTimeout(() => t.classList.remove('show'), 2500);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user