add files

This commit is contained in:
luthfihadinugroho79
2026-06-10 19:42:59 +07:00
parent 7bf3c98e59
commit e950468760
3541 changed files with 0 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
<?php
include 'koneksi.php';
header('Content-Type: application/json; charset=utf-8');
$query = mysqli_query($koneksi, "SELECT id, nama_spbu, no_wa, status_24_jam, lat, lng FROM `$nama_tabel`");
$data = [];
while ($row = mysqli_fetch_assoc($query)) {
$row['lat'] = floatval($row['lat']);
$row['lng'] = floatval($row['lng']);
$data[] = $row;
}
echo json_encode($data);
?>
+38
View File
@@ -0,0 +1,38 @@
<?php
include 'koneksi.php';
header('Content-Type: text/html; charset=utf-8');
echo "<style>body{font-family:monospace;padding:20px;background:#111;color:#0f0;} .ok{color:#0f0} .err{color:#f44} .warn{color:#fa0}</style>";
echo "<h2>🔍 Cek Koneksi & Database SPBU</h2>";
if ($koneksi) {
echo "<p class='ok'>✅ Koneksi ke MySQL berhasil</p>";
} else {
echo "<p class='err'>❌ Koneksi GAGAL: " . mysqli_connect_error() . "</p>"; exit;
}
$db_cek = mysqli_query($koneksi, "SELECT DATABASE() AS db");
$db_row = mysqli_fetch_assoc($db_cek);
echo "<p class='ok'>✅ Database aktif: <b>{$db_row['db']}</b></p>";
echo "<p class='ok'>✅ Nama tabel: <b>$nama_tabel</b></p>";
$tabel_cek = mysqli_query($koneksi, "SHOW TABLES LIKE '$nama_tabel'");
if (mysqli_num_rows($tabel_cek) > 0) {
echo "<p class='ok'>✅ Tabel <b>$nama_tabel</b> ditemukan</p>";
} else {
echo "<p class='err'>❌ Tabel <b>$nama_tabel</b> TIDAK ADA!</p>";
echo "<p class='warn'>⚠️ Jalankan schema_mysql.sql di phpMyAdmin</p>"; exit;
}
$count_cek = mysqli_query($koneksi, "SELECT COUNT(*) AS total FROM `$nama_tabel`");
$count_row = mysqli_fetch_assoc($count_cek);
echo "<p class='ok'>✅ Jumlah data: <b>{$count_row['total']}</b> baris</p>";
$test = mysqli_query($koneksi, "INSERT INTO `$nama_tabel` (nama_spbu, no_wa, status_24_jam, lat, lng) VALUES ('TEST_DELETE_ME', '000', 'no', 0, 0)");
if ($test) {
$ins_id = mysqli_insert_id($koneksi);
mysqli_query($koneksi, "DELETE FROM `$nama_tabel` WHERE id = $ins_id");
echo "<p class='ok'>✅ Test INSERT & DELETE berhasil — database siap digunakan!</p>";
} else {
echo "<p class='err'>❌ Test INSERT gagal: " . mysqli_error($koneksi) . "</p>";
}
echo "<hr><p>File ini bisa dihapus setelah tidak diperlukan.</p>";
+23
View File
@@ -0,0 +1,23 @@
<?php
include 'koneksi.php';
header('Content-Type: text/plain; charset=utf-8');
$id = mysqli_real_escape_string($koneksi, $_POST['id'] ?? '');
$nama = mysqli_real_escape_string($koneksi, $_POST['nama_spbu'] ?? '');
$wa = mysqli_real_escape_string($koneksi, $_POST['no_wa'] ?? '');
$status = mysqli_real_escape_string($koneksi, $_POST['status_24_jam'] ?? 'no');
if (!$id || !$nama || !$wa) {
echo "Gagal: Data tidak lengkap!";
exit;
}
$query = "UPDATE `$nama_tabel` SET nama_spbu='$nama', no_wa='$wa', status_24_jam='$status' WHERE id='$id'";
if (mysqli_query($koneksi, $query)) {
echo "Data SPBU berhasil diperbarui!";
} else {
echo "Gagal memperbarui data: " . mysqli_error($koneksi);
}
?>
+20
View File
@@ -0,0 +1,20 @@
<?php
include 'koneksi.php';
header('Content-Type: text/plain; charset=utf-8');
$id = mysqli_real_escape_string($koneksi, $_POST['id'] ?? '');
if (!$id) {
echo "Gagal: ID tidak ditemukan!";
exit;
}
$query = "DELETE FROM `$nama_tabel` WHERE id='$id'";
if (mysqli_query($koneksi, $query)) {
echo "Data berhasil dihapus!";
} else {
echo "Gagal menghapus data: " . mysqli_error($koneksi);
}
?>
+790
View File
@@ -0,0 +1,790 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS SPBU Pontianak</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
<!-- ✅ FIX 1: Leaflet CSS & JS dipindah ke <head> agar sudah siap sebelum script dijalankan -->
<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>
:root {
--bg: #0d0f14;
--surface: #161923;
--surface2: #1e2330;
--border: rgba(255,255,255,0.07);
--accent-green: #00e5a0;
--accent-amber: #ffb830;
--accent-blue: #3d8bff;
--text: #e8ecf4;
--text-muted: #6b7385;
--danger: #ff4d6a;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'DM Sans', sans-serif;
background: var(--bg);
color: var(--text);
display: flex;
height: 100vh;
overflow: hidden;
}
/* ─── SIDEBAR ─── */
#sidebar {
width: 340px;
min-width: 340px;
background: var(--surface);
display: flex;
flex-direction: column;
border-right: 1px solid var(--border);
z-index: 1000;
overflow: hidden;
}
.sidebar-header {
padding: 28px 24px 20px;
border-bottom: 1px solid var(--border);
background: linear-gradient(135deg, rgba(61,139,255,0.12), rgba(0,229,160,0.06));
position: relative;
overflow: hidden;
}
.sidebar-header::before {
content: '';
position: absolute;
top: -40px; right: -40px;
width: 130px; height: 130px;
border-radius: 50%;
background: radial-gradient(circle, rgba(0,229,160,0.15), transparent 70%);
}
.logo-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 4px;
}
.logo-icon {
width: 40px; height: 40px;
background: linear-gradient(135deg, var(--accent-green), var(--accent-blue));
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 18px;
}
.sidebar-header h1 {
font-family: 'Syne', sans-serif;
font-size: 20px;
font-weight: 800;
letter-spacing: -0.5px;
}
.sidebar-header p {
font-size: 12px;
color: var(--text-muted);
margin-left: 52px;
}
/* Stats */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
padding: 18px 20px;
border-bottom: 1px solid var(--border);
}
.stat-box {
background: var(--surface2);
border-radius: 10px;
padding: 12px 10px;
text-align: center;
border: 1px solid var(--border);
transition: transform 0.2s;
}
.stat-box:hover { transform: translateY(-2px); }
.stat-num {
font-family: 'Syne', sans-serif;
font-size: 26px;
font-weight: 800;
line-height: 1;
}
.stat-label { font-size: 10px; color: var(--text-muted); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
.stat-box.total .stat-num { color: var(--accent-blue); }
.stat-box.jam24 .stat-num { color: var(--accent-green); }
.stat-box.reguler .stat-num { color: var(--accent-amber); }
/* Layer Control Panel */
.layer-panel {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
}
.panel-title {
font-family: 'Syne', sans-serif;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-muted);
margin-bottom: 12px;
}
.layer-toggle {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
margin-bottom: 6px;
border: 1px solid var(--border);
}
.layer-toggle:hover { background: var(--surface2); }
.toggle-dot {
width: 12px; height: 12px;
border-radius: 3px;
flex-shrink: 0;
}
.toggle-dot.green { background: var(--accent-green); box-shadow: 0 0 8px rgba(0,229,160,0.5); }
.toggle-dot.amber { background: var(--accent-amber); box-shadow: 0 0 8px rgba(255,184,48,0.5); }
.layer-toggle span { font-size: 13px; flex-grow: 1; }
.toggle-count {
font-size: 11px;
background: var(--surface2);
padding: 2px 7px;
border-radius: 20px;
color: var(--text-muted);
}
/* Custom Checkbox */
.layer-toggle input[type="checkbox"] {
appearance: none;
width: 18px; height: 18px;
border: 2px solid var(--border);
border-radius: 5px;
cursor: pointer;
position: relative;
transition: all 0.2s;
flex-shrink: 0;
}
.layer-toggle input[type="checkbox"]:checked {
background: var(--accent-blue);
border-color: var(--accent-blue);
}
.layer-toggle input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
color: white;
font-size: 11px;
top: 0px; left: 2px;
font-weight: bold;
}
/* SPBU List */
.list-header {
padding: 14px 20px 8px;
font-family: 'Syne', sans-serif;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-muted);
}
#spbu-list {
flex-grow: 1;
overflow-y: auto;
padding: 0 12px 12px;
scrollbar-width: thin;
scrollbar-color: var(--surface2) transparent;
}
.spbu-item {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 12px;
border-radius: 8px;
margin-bottom: 5px;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s;
}
.spbu-item:hover {
background: var(--surface2);
border-color: var(--border);
}
.spbu-dot {
width: 8px; height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.spbu-dot.green { background: var(--accent-green); }
.spbu-dot.amber { background: var(--accent-amber); }
.spbu-item-name { font-size: 13px; font-weight: 500; flex-grow: 1; }
.badge-pill {
font-size: 9px;
padding: 3px 7px;
border-radius: 20px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.badge-pill.green { background: rgba(0,229,160,0.15); color: var(--accent-green); }
.badge-pill.amber { background: rgba(255,184,48,0.15); color: var(--accent-amber); }
/* ─── MAP ─── */
#map-container {
flex-grow: 1;
position: relative;
/* ✅ FIX 2: Pastikan tinggi eksplisit agar peta punya dimensi */
height: 100vh;
min-height: 0;
}
#map {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
}
/* Leaflet overrides for dark theme */
.leaflet-popup-content-wrapper {
background: var(--surface) !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
border-radius: 12px !important;
box-shadow: 0 20px 60px rgba(0,0,0,0.6) !important;
padding: 0 !important;
overflow: hidden;
}
.leaflet-popup-tip { background: var(--surface) !important; }
.leaflet-popup-content { margin: 0 !important; }
.leaflet-control-zoom {
border: none !important;
border-radius: 10px !important;
overflow: hidden;
}
.leaflet-control-zoom a {
background: var(--surface) !important;
color: var(--text) !important;
border-bottom: 1px solid var(--border) !important;
}
.leaflet-control-zoom a:hover { background: var(--surface2) !important; }
/* Popup Form */
.popup-wrap { padding: 18px; min-width: 220px; }
.popup-type {
font-size: 9px;
font-family: 'Syne', sans-serif;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 4px;
}
.popup-name {
font-family: 'Syne', sans-serif;
font-size: 16px;
font-weight: 700;
margin-bottom: 12px;
}
.popup-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-muted);
margin-bottom: 6px;
}
.popup-row strong { color: var(--text); }
.popup-divider { height: 1px; background: var(--border); margin: 14px 0; }
.popup-input {
width: 100%;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 7px;
padding: 9px 12px;
color: var(--text);
font-family: 'DM Sans', sans-serif;
font-size: 13px;
margin-bottom: 8px;
outline: none;
transition: border-color 0.2s;
}
.popup-input:focus { border-color: var(--accent-blue); }
.popup-actions { display: flex; gap: 7px; margin-top: 12px; }
.pop-btn {
flex: 1;
padding: 9px;
border: none;
border-radius: 7px;
font-family: 'Syne', sans-serif;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
cursor: pointer;
transition: all 0.2s;
}
.pop-btn:hover { filter: brightness(1.1); transform: translateY(-1px); }
.pop-btn.blue { background: var(--accent-blue); color: white; }
.pop-btn.amber { background: var(--accent-amber); color: #1a1000; }
.pop-btn.red { background: var(--danger); color: white; }
.pop-btn.gray { background: var(--surface2); color: var(--text-muted); border: 1px solid var(--border); }
/* Map hint badge */
.map-hint {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(13,15,20,0.85);
backdrop-filter: blur(10px);
border: 1px solid var(--border);
border-radius: 30px;
padding: 8px 18px;
font-size: 12px;
color: var(--text-muted);
z-index: 999;
pointer-events: none;
}
.map-hint span { color: var(--accent-blue); font-weight: 600; }
/* ✅ FIX 3: Loading overlay HTML ditambahkan */
.loading-overlay {
position: absolute;
inset: 0;
background: var(--bg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
transition: opacity 0.5s;
}
.loading-overlay.hidden { opacity: 0; pointer-events: none; }
.loader-ring {
width: 50px; height: 50px;
border: 3px solid var(--surface2);
border-top-color: var(--accent-green);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 16px;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Animations */
.spbu-item { animation: fadeUp 0.3s ease backwards; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body>
<div id="sidebar">
<div class="sidebar-header">
<div class="logo-row">
<div class="logo-icon"></div>
<h1>SPBU Pontianak</h1>
</div>
<p>WebGIS — Sistem Informasi Geografis</p>
</div>
<div class="stats-grid">
<div class="stat-box total">
<div class="stat-num" id="stat-total"></div>
<div class="stat-label">Total</div>
</div>
<div class="stat-box jam24">
<div class="stat-num" id="stat-24"></div>
<div class="stat-label">24 Jam</div>
</div>
<div class="stat-box reguler">
<div class="stat-num" id="stat-reg"></div>
<div class="stat-label">Reguler</div>
</div>
</div>
<div class="layer-panel">
<div class="panel-title">Layer Control</div>
<label class="layer-toggle">
<div class="toggle-dot green"></div>
<span>SPBU Buka 24 Jam</span>
<span class="toggle-count" id="cnt-24">0</span>
<input type="checkbox" id="chk-24" checked>
</label>
<label class="layer-toggle">
<div class="toggle-dot amber"></div>
<span>SPBU Buka Reguler</span>
<span class="toggle-count" id="cnt-reg">0</span>
<input type="checkbox" id="chk-reg" checked>
</label>
</div>
<div class="list-header">Daftar SPBU</div>
<div id="spbu-list">
<p style="text-align:center;color:var(--text-muted);font-size:13px;padding:20px;">Memuat data...</p>
</div>
</div>
<div id="map-container">
<!-- ✅ FIX 3: Loading overlay ditambahkan ke HTML -->
<div class="loading-overlay" id="loading-overlay">
<div class="loader-ring"></div>
<p style="color:var(--text-muted);font-size:13px;">Memuat peta...</p>
</div>
<div id="map"></div>
<div class="map-hint">Klik <span>di mana saja</span> pada peta untuk menambah SPBU baru</div>
</div>
<!-- ✅ FIX 1: Script Leaflet DIHAPUS dari sini karena sudah dipindah ke <head> -->
<script>
// ── MAP INIT ──
// ✅ FIX 4: Inisialisasi map dibungkus dalam DOMContentLoaded agar aman
document.addEventListener('DOMContentLoaded', function() {
const map = L.map('map', { zoomControl: false }).setView([-0.026330, 109.342504], 13);
L.control.zoom({ position: 'topright' }).addTo(map);
// ✅ FIX 5: Tile layer dengan 2 pilihan — CartoDB dark (utama) + OSM (fallback)
const tileCartoDB = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_matter/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/">CartoDB</a>',
maxZoom: 19,
subdomains: 'abcd'
});
const tileOSM = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 19
});
// Coba CartoDB dulu, jika tile gagal load pakai OSM
tileCartoDB.addTo(map);
tileCartoDB.on('tileerror', function() {
map.removeLayer(tileCartoDB);
tileOSM.addTo(map);
});
// ── LAYER GROUPS ──
// Dibuat dulu, belum ditambah ke map — ditambah sesuai state checkbox
const layer24 = L.layerGroup();
const layerReg = L.layerGroup();
// ── EVENT LAYER CONTROL (FIX UTAMA) ──
document.getElementById('chk-24').addEventListener('change', function() {
if (this.checked) {
layer24.addTo(map);
} else {
map.removeLayer(layer24);
}
});
document.getElementById('chk-reg').addEventListener('change', function() {
if (this.checked) {
layerReg.addTo(map);
} else {
map.removeLayer(layerReg);
}
});
// ── ICONS ──
function makeIcon(color) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="42" viewBox="0 0 32 42">
<defs>
<filter id="s${color.replace('#','')}"><feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="${color}" flood-opacity="0.4"/></filter>
</defs>
<path fill="${color}" filter="url(#s${color.replace('#','')})" d="M16 0C7.163 0 0 7.163 0 16c0 12 16 26 16 26s16-14 16-26C32 7.163 24.837 0 16 0z"/>
<circle cx="16" cy="16" r="8" fill="white" opacity="0.9"/>
<circle cx="16" cy="16" r="5" fill="${color}"/>
</svg>`;
return L.divIcon({
html: svg,
className: '',
iconSize: [32, 42],
iconAnchor: [16, 42],
popupAnchor: [0, -42]
});
}
const iconGreen = makeIcon('#00e5a0');
const iconAmber = makeIcon('#ffb830');
const iconTemp = makeIcon('#3d8bff');
// ── DATA ──
let allData = [];
let tempMarker = null;
function muatData() {
fetch('ambil_data.php')
.then(r => r.json())
.then(data => {
allData = data;
renderAll(data);
})
.catch(() => {
// Demo mode jika tidak ada server
renderAll(demoData());
});
}
function demoData() {
return [
{id:'1', nama_spbu:'SPBU Jl. Ahmad Yani', no_wa:'08123456789', status_24_jam:'yes', lat:-0.0202, lng:109.3423},
{id:'2', nama_spbu:'SPBU Jl. Gajah Mada', no_wa:'08234567890', status_24_jam:'no', lat:-0.0280, lng:109.3310},
{id:'3', nama_spbu:'SPBU Sui Raya', no_wa:'08345678901', status_24_jam:'yes', lat:-0.0450, lng:109.3580},
{id:'4', nama_spbu:'SPBU Jl. Tanjungpura', no_wa:'08456789012', status_24_jam:'no', lat:-0.0120, lng:109.3390},
{id:'5', nama_spbu:'SPBU Jl. Imam Bonjol', no_wa:'08567890123', status_24_jam:'yes', lat:-0.0330, lng:109.3500},
{id:'6', nama_spbu:'SPBU Siantan', no_wa:'08678901234', status_24_jam:'no', lat:0.0050, lng:109.3350},
];
}
function renderAll(data) {
layer24.clearLayers();
layerReg.clearLayers();
let c24 = 0, cReg = 0;
let listHTML = '';
data.forEach((item, i) => {
const is24 = item.status_24_jam === 'yes';
const icon = is24 ? iconGreen : iconAmber;
const layer = is24 ? layer24 : layerReg;
if (is24) c24++; else cReg++;
// Tambah marker ke layerGroup (bukan langsung ke map)
const marker = L.marker([parseFloat(item.lat), parseFloat(item.lng)], { icon, draggable: true });
layer.addLayer(marker);
marker.bindPopup(buildInfoPopup(item), { maxWidth: 280 });
marker.on('dragend', function() {
const pos = marker.getLatLng();
if (confirm('Simpan koordinat baru?')) updateLokasi(item.id, pos.lat, pos.lng);
else muatData();
});
const delay = i * 40;
listHTML += `
<div class="spbu-item" style="animation-delay:${delay}ms" onclick="flyTo(${item.lat},${item.lng})">
<div class="spbu-dot ${is24 ? 'green' : 'amber'}"></div>
<span class="spbu-item-name">${item.nama_spbu}</span>
<span class="badge-pill ${is24 ? 'green' : 'amber'}">${is24 ? '24 Jam' : 'Reguler'}</span>
</div>
`;
});
document.getElementById('stat-total').textContent = data.length;
document.getElementById('stat-24').textContent = c24;
document.getElementById('stat-reg').textContent = cReg;
document.getElementById('cnt-24').textContent = c24;
document.getElementById('cnt-reg').textContent = cReg;
document.getElementById('spbu-list').innerHTML = listHTML || '<p style="text-align:center;color:var(--text-muted);font-size:13px;padding:20px;">Belum ada data.</p>';
// Terapkan state checkbox saat ini ke masing-masing layer
const show24 = document.getElementById('chk-24').checked;
const showReg = document.getElementById('chk-reg').checked;
show24 ? layer24.addTo(map) : map.removeLayer(layer24);
showReg ? layerReg.addTo(map) : map.removeLayer(layerReg);
// Sembunyikan loading overlay setelah data selesai dirender
const overlay = document.getElementById('loading-overlay');
if (overlay) overlay.classList.add('hidden');
}
window.flyTo = function flyTo(lat, lng) {
map.flyTo([lat, lng], 16, { duration: 1 });
};
// ── POPUP BUILDERS ──
function buildInfoPopup(item) {
const is24 = item.status_24_jam === 'yes';
return `
<div class="popup-wrap">
<div class="popup-type">Stasiun Bahan Bakar</div>
<div class="popup-name">${item.nama_spbu}</div>
<div class="popup-row">📞 <strong>${item.no_wa}</strong></div>
<div class="popup-row">🕐 Status: <span class="badge-pill ${is24?'green':'amber'}" style="margin-left:4px">${is24?'Buka 24 Jam':'Buka Reguler'}</span></div>
<div class="popup-divider"></div>
<div class="popup-actions">
<button class="pop-btn amber" onclick="bukaEdit('${item.id}','${item.nama_spbu}','${item.no_wa}','${item.status_24_jam}',${item.lat},${item.lng})">✏ Edit</button>
<button class="pop-btn red" onclick="hapusData('${item.id}')">🗑 Hapus</button>
</div>
</div>
`;
}
function buildAddPopup(lat, lng) {
return `
<div class="popup-wrap">
<div class="popup-type">Tambah Lokasi Baru</div>
<div class="popup-name" style="font-size:14px;margin-bottom:14px;">Data SPBU</div>
<input class="popup-input" id="add_nama" placeholder="Nama SPBU" />
<input class="popup-input" id="add_wa" placeholder="Nomor WhatsApp" />
<select class="popup-input" id="add_status">
<option value="no">Buka Reguler</option>
<option value="yes">Buka 24 Jam</option>
</select>
<div class="popup-actions">
<button class="pop-btn blue" onclick="prosesSimpan(${lat},${lng},'tambah')">💾 Simpan</button>
<button class="pop-btn gray" onclick="batalTambah()">✕</button>
</div>
</div>
`;
}
function buildEditPopup(id, nama, wa, status, lat, lng) {
const sY = status==='yes'?'selected':'';
const sN = status==='no'?'selected':'';
return `
<div class="popup-wrap">
<div class="popup-type">Edit Data</div>
<input type="hidden" id="edit_id" value="${id}">
<input class="popup-input" id="edit_nama" value="${nama}">
<input class="popup-input" id="edit_wa" value="${wa}">
<select class="popup-input" id="edit_status">
<option value="no" ${sN}>Buka Reguler</option>
<option value="yes" ${sY}>Buka 24 Jam</option>
</select>
<div class="popup-actions">
<button class="pop-btn blue" onclick="prosesSimpan(${lat},${lng},'edit')">✓ Update</button>
<button class="pop-btn gray" onclick="map.closePopup()">✕ Batal</button>
</div>
</div>
`;
}
// ── MAP CLICK ADD ──
map.on('click', function(e) {
if (tempMarker) map.removeLayer(tempMarker);
tempMarker = L.marker([e.latlng.lat, e.latlng.lng], { icon: iconTemp }).addTo(map);
tempMarker.bindPopup(buildAddPopup(e.latlng.lat, e.latlng.lng), { maxWidth: 280 }).openPopup();
});
window.batalTambah = function() {
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
map.closePopup();
};
// ── EDIT ──
window.bukaEdit = function(id, nama, wa, status, lat, lng) {
L.popup({ maxWidth: 280 })
.setLatLng([lat, lng])
.setContent(buildEditPopup(id, nama, wa, status, lat, lng))
.openOn(map);
};
// ── SIMPAN (POST) ──
window.prosesSimpan = function prosesSimpan(lat, lng, mode) {
const pfx = mode === 'edit' ? 'edit_' : 'add_';
const id = mode === 'edit' ? document.getElementById('edit_id').value : '';
const nama = document.getElementById(pfx + 'nama').value.trim();
const wa = document.getElementById(pfx + 'wa').value.trim();
const stat = document.getElementById(pfx + 'status').value;
if (!nama || !wa) return alert('Nama dan nomor WA wajib diisi!');
const fd = new FormData();
if (mode === 'edit') fd.append('id', id);
fd.append('nama_spbu', nama);
fd.append('no_wa', wa);
fd.append('status_24_jam', stat);
fd.append('lat', lat);
fd.append('lng', lng);
const url = mode === 'edit' ? 'edit_data.php' : 'simpan_data.php';
fetch(url, { method: 'POST', body: fd })
.then(r => r.text())
.then(msg => {
showToast(msg, 'success');
map.closePopup();
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
muatData();
})
.catch(() => {
// Demo mode: update state lokal agar data langsung tampil tanpa server
if (mode === 'tambah') {
const newItem = {
id: Date.now().toString(),
nama_spbu: nama,
no_wa: wa,
status_24_jam: stat,
lat: lat,
lng: lng
};
allData.push(newItem);
} else {
const idx = allData.findIndex(d => d.id === id);
if (idx !== -1) {
allData[idx].nama_spbu = nama;
allData[idx].no_wa = wa;
allData[idx].status_24_jam = stat;
}
}
renderAll(allData);
showToast(mode === 'tambah' ? 'Data berhasil disimpan! (demo mode)' : 'Data berhasil diperbarui! (demo mode)', 'success');
map.closePopup();
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
});
}
// ── HAPUS ──
window.hapusData = function hapusData(id) {
if (!confirm('Hapus data SPBU ini?')) return;
const fd = new FormData();
fd.append('id', id);
fetch('hapus_data.php', { method: 'POST', body: fd })
.then(r => r.text())
.then(msg => { showToast(msg, 'danger'); muatData(); })
.catch(() => {
allData = allData.filter(d => d.id !== id);
renderAll(allData);
showToast('Data dihapus! (demo mode)', 'danger');
});
}
// ── UPDATE LOKASI ──
window.updateLokasi = function updateLokasi(id, lat, lng) {
const fd = new FormData();
fd.append('id', id); fd.append('lat', lat); fd.append('lng', lng);
fetch('update_lokasi.php', { method: 'POST', body: fd })
.then(() => { showToast('Lokasi diperbarui!', 'success'); muatData(); })
.catch(() => { showToast('Lokasi diperbarui! (demo mode)', 'success'); muatData(); });
}
// ── TOAST ──
function showToast(msg, type) {
const t = document.createElement('div');
t.style.cssText = `
position:fixed; bottom:30px; right:30px; z-index:9999;
background:${type==='success'?'#00e5a0':'#ff4d6a'};
color:${type==='success'?'#001a0d':'#fff'};
padding:12px 20px; border-radius:10px; font-size:13px; font-weight:600;
font-family:'Syne',sans-serif; box-shadow:0 8px 30px rgba(0,0,0,0.4);
animation: slideIn 0.3s ease;
`;
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 3000);
}
// ── START ──
muatData();
// ✅ FIX 6: invalidateSize dipanggil beberapa kali untuk memastikan peta terrender sempurna
setTimeout(() => map.invalidateSize(), 100);
setTimeout(() => map.invalidateSize(), 500);
}); // tutup DOMContentLoaded
</script>
</body>
</html>
+17
View File
@@ -0,0 +1,17 @@
<?php
$host = "localhost";
$user = "root";
$pass = "";
// Berdasarkan gambar phpMyAdmin Anda:
$nama_database = "spbulayer";
$nama_tabel = "tbl_spbu";
$koneksi = mysqli_connect($host, $user, $pass, $nama_database);
// Cek koneksi agar tidak muncul error Fatal lagi
if (!$koneksi) {
die("Koneksi ke database gagal: " . mysqli_connect_error());
}
?>
+6
View File
@@ -0,0 +1,6 @@
{
"name": "spbu' - Copy",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+46
View File
@@ -0,0 +1,46 @@
-- ============================================================
-- Schema MySQL untuk WebGIS SPBU Pontianak
-- ============================================================
-- CARA PAKAI:
-- 1. Buka phpMyAdmin
-- 2. Pilih database "data_spbu" (atau buat baru jika belum ada)
-- 3. Klik tab "SQL"
-- 4. Copy-paste seluruh isi file ini, lalu klik "Go"
-- ============================================================
-- CATATAN: nama tabel sengaja dibuat "tbl_spbu" (bukan "data_spbu")
-- karena MySQL tidak boleh memiliki nama tabel sama dengan nama database
-- (menyebabkan error errno: 121)
CREATE DATABASE IF NOT EXISTS `data_spbu`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE `data_spbu`;
-- Hapus tabel lama jika ada
DROP TABLE IF EXISTS `tbl_spbu`;
DROP TABLE IF EXISTS `data_spbu`;
-- Buat tabel dengan nama tbl_spbu
CREATE TABLE `tbl_spbu` (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_spbu VARCHAR(255) NOT NULL,
no_wa VARCHAR(20) NOT NULL,
status_24_jam ENUM('yes','no') DEFAULT 'no',
lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Data contoh
INSERT INTO `tbl_spbu` (nama_spbu, no_wa, status_24_jam, lat, lng) VALUES
('SPBU Jl. Ahmad Yani', '08123456789', 'yes', -0.0202, 109.3423),
('SPBU Jl. Gajah Mada', '08234567890', 'no', -0.0280, 109.3310),
('SPBU Sui Raya', '08345678901', 'yes', -0.0450, 109.3580),
('SPBU Jl. Tanjungpura', '08456789012', 'no', -0.0120, 109.3390),
('SPBU Jl. Imam Bonjol', '08567890123', 'yes', -0.0330, 109.3500),
('SPBU Siantan', '08678901234', 'no', 0.0050, 109.3350);
-- Cek hasil
SELECT 'Tabel tbl_spbu berhasil dibuat!' AS status, COUNT(*) AS jumlah_data FROM `tbl_spbu`;
+25
View File
@@ -0,0 +1,25 @@
<?php
include 'koneksi.php';
header('Content-Type: text/plain; charset=utf-8');
$nama = mysqli_real_escape_string($koneksi, $_POST['nama_spbu'] ?? '');
$wa = mysqli_real_escape_string($koneksi, $_POST['no_wa'] ?? '');
$status = mysqli_real_escape_string($koneksi, $_POST['status_24_jam'] ?? 'no');
$lat = floatval($_POST['lat'] ?? 0);
$lng = floatval($_POST['lng'] ?? 0);
if (!$nama || !$wa) {
echo "Gagal: Nama dan nomor WA wajib diisi!";
exit;
}
$query = "INSERT INTO `$nama_tabel` (nama_spbu, no_wa, status_24_jam, lat, lng)
VALUES ('$nama', '$wa', '$status', '$lat', '$lng')";
if (mysqli_query($koneksi, $query)) {
echo "Data SPBU berhasil ditambahkan!";
} else {
echo "Gagal menambahkan data: " . mysqli_error($koneksi);
}
?>
@@ -0,0 +1,15 @@
<?php
include 'koneksi.php';
$id = mysqli_real_escape_string($koneksi, $_POST['id']);
$lat = mysqli_real_escape_string($koneksi, $_POST['lat']);
$lng = mysqli_real_escape_string($koneksi, $_POST['lng']);
$query = "UPDATE $nama_tabel SET lat='$lat', lng='$lng' WHERE id='$id'";
if(mysqli_query($koneksi, $query)) {
echo "Lokasi berhasil dipindahkan!";
} else {
echo "Gagal memindahkan lokasi: " . mysqli_error($koneksi);
}
?>
+504
View File
@@ -0,0 +1,504 @@
# 🗺️ WebGIS Persebaran Penduduk Miskin
**Sistem Pemetaan Geospasial berbasis React + Node.js + PostgreSQL/PostGIS**
---
## 📋 DAFTAR ISI
1. [Prasyarat](#prasyarat)
2. [Instalasi PostgreSQL + PostGIS di XAMPP](#instalasi-postgresql--postgis)
3. [Setup Database](#setup-database)
4. [Setup Backend (Node.js)](#setup-backend)
5. [Setup Frontend (React)](#setup-frontend)
6. [Menjalankan Aplikasi](#menjalankan-aplikasi)
7. [Struktur Folder](#struktur-folder)
8. [API Endpoint Reference](#api-endpoint-reference)
9. [Troubleshooting](#troubleshooting)
---
## ✅ PRASYARAT
Sebelum mulai, pastikan sudah terinstal:
| Software | Versi Minimum | Link Download |
|----------|--------------|---------------|
| **XAMPP** | 8.0+ | https://www.apachefriends.org |
| **Node.js** | 18.x LTS | https://nodejs.org |
| **PostgreSQL** | 14+ | https://www.postgresql.org/download |
| **PostGIS** | 3.x | Diinstal via Stack Builder |
| **Git** (opsional) | any | https://git-scm.com |
> ⚠️ **PENTING**: PostgreSQL dan PostGIS TIDAK termasuk dalam XAMPP standar.
> XAMPP hanya menyediakan MySQL/MariaDB. Kita install PostgreSQL secara terpisah.
---
## 🐘 INSTALASI POSTGRESQL + POSTGIS
### Langkah 1: Install PostgreSQL
1. Download PostgreSQL dari https://www.postgresql.org/download/windows/
2. Jalankan installer, ikuti wizard:
- Port: **5432** (default)
- Password superuser: catat dengan baik (contoh: `postgres123`)
- Locale: sesuaikan (Indonesian)
3. ✅ Centang "Stack Builder" di akhir instalasi
### Langkah 2: Install PostGIS via Stack Builder
1. Stack Builder otomatis terbuka setelah instalasi PostgreSQL
2. Pilih server PostgreSQL Anda
3. Expand kategori **Spatial Extensions**
4. Centang **PostGIS** (versi terbaru)
5. Klik Next dan ikuti proses download + instalasi
6. Saat diminta "Create spatial database", **LEWATI** (kita buat manual)
### Langkah 3: Verifikasi Instalasi
Buka **SQL Shell (psql)** dari Start Menu:
```sql
-- Login dengan user postgres
Password: [password yang Anda buat]
-- Cek PostGIS
SELECT PostGIS_version();
-- Output: "3.x USE_GEOS=1 ..."
```
---
## 🗄️ SETUP DATABASE
### Langkah 1: Buat Database
Buka **pgAdmin 4** (tersedia di Start Menu setelah install PostgreSQL):
1. Klik kanan **Databases****Create****Database**
2. Isi nama: `webgis_kemiskinan`
3. Klik **Save**
**ATAU** via SQL Shell (psql):
```sql
CREATE DATABASE webgis_kemiskinan;
\c webgis_kemiskinan
```
### Langkah 2: Jalankan Script SQL
**Via pgAdmin 4:**
1. Klik dua kali database `webgis_kemiskinan`
2. Klik kanan → **Query Tool**
3. Buka file: `database/schema.sql` (dari folder project)
4. Klik tombol **▶ Execute** (F5)
**Via psql:**
```bash
# Pastikan Anda ada di folder project
psql -U postgres -d webgis_kemiskinan -f database/schema.sql
```
**Via Command Prompt:**
```cmd
cd C:\xampp\htdocs\webgis-kemiskinan
"C:\Program Files\PostgreSQL\14\bin\psql.exe" -U postgres -d webgis_kemiskinan -f database\schema.sql
```
### Langkah 3: Verifikasi Database
```sql
-- Di pgAdmin Query Tool atau psql:
\c webgis_kemiskinan
-- Cek tabel
\dt
-- Output yang diharapkan:
-- tabel_rumah_ibadah
-- tabel_penduduk
-- tabel_anggota_keluarga
-- Cek data dummy
SELECT nama, jenis FROM tabel_rumah_ibadah;
SELECT nama_kepala_keluarga FROM tabel_penduduk;
-- Cek PostGIS aktif
SELECT PostGIS_version();
```
---
## ⚙️ SETUP BACKEND
### Langkah 1: Masuk ke Folder Backend
```bash
cd webgis-kemiskinan\backend
```
### Langkah 2: Install Dependensi
```bash
npm install
```
### Langkah 3: Konfigurasi Environment
Salin file `.env.example` menjadi `.env`:
```bash
# Windows
copy .env.example .env
# Linux/Mac
cp .env.example .env
```
Edit file `.env` sesuai konfigurasi Anda:
```env
# Server
PORT=5000
NODE_ENV=development
# Database PostgreSQL
DB_HOST=localhost
DB_PORT=5432
DB_NAME=webgis_kemiskinan
DB_USER=postgres
DB_PASSWORD=postgres123 ← GANTI dengan password Anda
# CORS
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
# Upload
UPLOAD_DIR=uploads
MAX_FILE_SIZE=5242880
# Nominatim
NOMINATIM_URL=https://nominatim.openstreetmap.org
NOMINATIM_USER_AGENT=WebGIS-Kemiskinan/1.0
```
### Langkah 4: Buat Folder Upload
```bash
mkdir uploads
```
### Langkah 5: Test Koneksi
```bash
npm run dev
```
Output yang diharapkan:
```
🚀 Server berjalan di http://localhost:5000
✅ Terhubung ke PostgreSQL + PostGIS
📡 Mode: development
```
Test via browser atau Postman:
```
GET http://localhost:5000/api/health
```
Response: `{"status":"ok","timestamp":"..."}`
---
## 🎨 SETUP FRONTEND
### Langkah 1: Masuk ke Folder Frontend
Buka terminal/CMD **baru**:
```bash
cd webgis-kemiskinan\frontend
```
### Langkah 2: Install Dependensi
```bash
npm install
```
### Langkah 3: Jalankan Development Server
```bash
npm run dev
```
Output:
```
VITE v5.x.x ready in 800ms
➜ Local: http://localhost:3000/
➜ Network: http://192.168.x.x:3000/
```
### Langkah 4: Buka di Browser
Buka: **http://localhost:3000**
---
## 🚀 MENJALANKAN APLIKASI (RINGKASAN)
Setiap kali ingin menjalankan aplikasi, buka **2 terminal terpisah**:
### Terminal 1 Backend
```bash
cd webgis-kemiskinan\backend
npm run dev
```
### Terminal 2 Frontend
```bash
cd webgis-kemiskinan\frontend
npm run dev
```
Lalu buka browser: **http://localhost:3000**
---
## 📁 STRUKTUR FOLDER
```
webgis-kemiskinan/
├── 📂 database/
│ └── schema.sql # Script SQL lengkap (jalankan pertama kali)
├── 📂 backend/
│ ├── package.json
│ ├── .env.example # Salin ke .env dan isi konfigurasi
│ ├── .env # Konfigurasi lokal Anda (jangan di-commit)
│ └── src/
│ ├── server.js # Entry point Express
│ ├── config/
│ │ └── database.js # Koneksi PostgreSQL Pool
│ ├── controllers/
│ │ ├── rumahIbadahController.js # CRUD + geospasial ibadah
│ │ ├── pendudukController.js # CRUD + auto-assign ibadah
│ │ ├── geocodeController.js # Reverse geocoding Nominatim
│ │ └── dashboardController.js # Statistik & agregasi
│ ├── routes/
│ │ ├── rumahIbadah.js
│ │ ├── penduduk.js
│ │ ├── geocode.js
│ │ ├── upload.js
│ │ └── dashboard.js
│ ├── middleware/
│ │ └── upload.js # Multer file upload config
│ └── uploads/ # Folder penyimpanan file upload
└── 📂 frontend/
├── package.json
├── vite.config.js # Proxy API ke backend
├── index.html
└── src/
├── main.jsx # React entry point
├── App.jsx # Router + Layout utama
├── index.css # Global styles (dark theme)
├── services/
│ └── api.js # Axios API client
└── pages/
├── DashboardPage.jsx # Statistik & grafik
├── MapPage.jsx # Peta interaktif Leaflet
├── RumahIbadahPage.jsx # CRUD rumah ibadah
└── PendudukPage.jsx # CRUD penduduk miskin
```
---
## 📡 API ENDPOINT REFERENCE
### Rumah Ibadah
| Method | Endpoint | Deskripsi |
|--------|----------|-----------|
| GET | `/api/rumah-ibadah` | Semua rumah ibadah. Query: `?jenis=Masjid` |
| GET | `/api/rumah-ibadah/:id` | Detail + statistik penduduk |
| GET | `/api/rumah-ibadah/terdekat` | Query: `?lat=X&lng=Y&limit=5` |
| POST | `/api/rumah-ibadah` | Tambah baru |
| PUT | `/api/rumah-ibadah/:id` | Update |
| DELETE | `/api/rumah-ibadah/:id` | Hapus |
### Penduduk Miskin
| Method | Endpoint | Deskripsi |
|--------|----------|-----------|
| GET | `/api/penduduk` | Semua. Query: `?status_bantuan=true&limit=50` |
| GET | `/api/penduduk/:id` | Detail + anggota keluarga |
| POST | `/api/penduduk` | Tambah (multipart/form-data, auto-assign ibadah) |
| PUT | `/api/penduduk/:id` | Update (multipart/form-data) |
| PATCH | `/api/penduduk/:id/status` | Update status bantuan saja |
| DELETE | `/api/penduduk/:id` | Hapus |
### Lainnya
| Method | Endpoint | Deskripsi |
|--------|----------|-----------|
| GET | `/api/geocode/reverse` | Query: `?lat=X&lng=Y` |
| POST | `/api/upload` | Upload file (form field: `file`) |
| GET | `/api/dashboard/statistik` | Data statistik dashboard |
| GET | `/api/health` | Health check server |
---
## 🔧 TROUBLESHOOTING
### ❌ Error: "ECONNREFUSED Cannot connect to database"
**Penyebab:** PostgreSQL tidak berjalan atau password salah.
**Solusi:**
1. Buka **Services** Windows (services.msc)
2. Cari **postgresql-x64-14** → Klik **Start**
3. Atau di Command Prompt: `net start postgresql-x64-14`
4. Periksa password di file `.env`
---
### ❌ Error: "extension 'postgis' does not exist"
**Penyebab:** PostGIS belum terinstal atau belum diaktifkan di database.
**Solusi:**
```sql
-- Di pgAdmin, koneksi ke database webgis_kemiskinan
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
```
---
### ❌ Error: "npm: command not found"
**Penyebab:** Node.js belum terinstal atau belum ada di PATH.
**Solusi:**
1. Download Node.js LTS dari https://nodejs.org
2. Install dengan pilih "Add to PATH"
3. Restart Command Prompt / terminal
4. Test: `node --version` dan `npm --version`
---
### ❌ Error: "Port 5000 already in use"
**Solusi:**
```cmd
# Temukan proses yang menggunakan port 5000
netstat -ano | findstr :5000
# Kill proses (ganti XXXX dengan PID)
taskkill /PID XXXX /F
```
ATAU ganti port di `.env`: `PORT=5001`
---
### ❌ Peta tidak muncul (blank/grey)
**Penyebab:** Leaflet CSS tidak termuat dengan benar.
**Solusi:**
Pastikan di file yang menggunakan Leaflet ada:
```js
import 'leaflet/dist/leaflet.css';
```
---
### ❌ Reverse Geocoding gagal
**Penyebab:** Nominatim rate limit (max 1 request/detik) atau tidak ada internet.
**Solusi:**
- Tunggu beberapa detik lalu coba kembali
- Nominatim memerlukan koneksi internet aktif
- Isi alamat secara manual jika offline
---
### ❌ Upload file gagal
**Penyebab:** Folder `uploads` tidak ada.
**Solusi:**
```bash
cd backend
mkdir uploads
```
---
## 🌐 INTEGRASI XAMPP (Apache sebagai Reverse Proxy Opsional)
Jika ingin mengakses via `http://localhost/webgis` (tanpa port):
1. Aktifkan **mod_proxy** di XAMPP:
Edit `C:\xampp\apache\conf\httpd.conf`, hilangkan tanda `#` dari:
```
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
```
2. Tambahkan konfigurasi di `httpd-vhosts.conf`:
```apache
<VirtualHost *:80>
ProxyPass /api http://localhost:5000/api
ProxyPassReverse /api http://localhost:5000/api
DocumentRoot "C:/xampp/htdocs/webgis-kemiskinan/frontend/dist"
</VirtualHost>
```
3. Build frontend: `cd frontend && npm run build`
4. Restart Apache di XAMPP Control Panel
---
## 📦 BUILD UNTUK PRODUCTION
### Frontend (Build Statis)
```bash
cd frontend
npm run build
# Output: folder dist/ siap di-deploy
```
### Backend (Jalankan Tanpa nodemon)
```bash
cd backend
npm start
# Atau dengan PM2: pm2 start src/server.js --name webgis-backend
```
---
## 💡 TIPS PENGGUNAAN
1. **Menambah Rumah Ibadah**: Masuk ke menu "Rumah Ibadah" → Klik "Tambah" → Klik peta untuk memilih lokasi → Alamat otomatis terisi → Simpan
2. **Menambah Penduduk**: Masuk ke menu "Penduduk Miskin" → Klik "Tambah" → Klik peta untuk memilih lokasi rumah → Sistem otomatis menentukan rumah ibadah terdekat
3. **Melihat di Peta**: Masuk ke menu "Peta Interaktif" → Gunakan filter untuk menyaring tampilan → Klik marker untuk melihat detail
4. **Update Status Bantuan**: Di tabel Penduduk, klik tombol ✓ untuk toggle status sudah/belum dibantu
5. **Lokasi Saya**: Di halaman peta, klik tombol kompas 📍 untuk menampilkan posisi Anda
---
## 📄 LISENSI
MIT License Bebas digunakan untuk keperluan pendidikan dan sosial.
---
*Dibuat dengan ❤️ untuk membantu pemetaan dan penanganan kemiskinan di Indonesia*
+16
View File
@@ -0,0 +1,16 @@
<?php
include 'koneksi.php';
header('Content-Type: application/json; charset=utf-8');
$query = mysqli_query($koneksi, "SELECT id, nama_spbu, no_wa, status_24_jam, lat, lng FROM `$nama_tabel`");
$data = [];
while ($row = mysqli_fetch_assoc($query)) {
$row['lat'] = floatval($row['lat']);
$row['lng'] = floatval($row['lng']);
$data[] = $row;
}
echo json_encode($data);
?>
+38
View File
@@ -0,0 +1,38 @@
<?php
include 'koneksi.php';
header('Content-Type: text/html; charset=utf-8');
echo "<style>body{font-family:monospace;padding:20px;background:#111;color:#0f0;} .ok{color:#0f0} .err{color:#f44} .warn{color:#fa0}</style>";
echo "<h2>🔍 Cek Koneksi & Database SPBU</h2>";
if ($koneksi) {
echo "<p class='ok'>✅ Koneksi ke MySQL berhasil</p>";
} else {
echo "<p class='err'>❌ Koneksi GAGAL: " . mysqli_connect_error() . "</p>"; exit;
}
$db_cek = mysqli_query($koneksi, "SELECT DATABASE() AS db");
$db_row = mysqli_fetch_assoc($db_cek);
echo "<p class='ok'>✅ Database aktif: <b>{$db_row['db']}</b></p>";
echo "<p class='ok'>✅ Nama tabel: <b>$nama_tabel</b></p>";
$tabel_cek = mysqli_query($koneksi, "SHOW TABLES LIKE '$nama_tabel'");
if (mysqli_num_rows($tabel_cek) > 0) {
echo "<p class='ok'>✅ Tabel <b>$nama_tabel</b> ditemukan</p>";
} else {
echo "<p class='err'>❌ Tabel <b>$nama_tabel</b> TIDAK ADA!</p>";
echo "<p class='warn'>⚠️ Jalankan schema_mysql.sql di phpMyAdmin</p>"; exit;
}
$count_cek = mysqli_query($koneksi, "SELECT COUNT(*) AS total FROM `$nama_tabel`");
$count_row = mysqli_fetch_assoc($count_cek);
echo "<p class='ok'>✅ Jumlah data: <b>{$count_row['total']}</b> baris</p>";
$test = mysqli_query($koneksi, "INSERT INTO `$nama_tabel` (nama_spbu, no_wa, status_24_jam, lat, lng) VALUES ('TEST_DELETE_ME', '000', 'no', 0, 0)");
if ($test) {
$ins_id = mysqli_insert_id($koneksi);
mysqli_query($koneksi, "DELETE FROM `$nama_tabel` WHERE id = $ins_id");
echo "<p class='ok'>✅ Test INSERT & DELETE berhasil — database siap digunakan!</p>";
} else {
echo "<p class='err'>❌ Test INSERT gagal: " . mysqli_error($koneksi) . "</p>";
}
echo "<hr><p>File ini bisa dihapus setelah tidak diperlukan.</p>";
+23
View File
@@ -0,0 +1,23 @@
<?php
include 'koneksi.php';
header('Content-Type: text/plain; charset=utf-8');
$id = mysqli_real_escape_string($koneksi, $_POST['id'] ?? '');
$nama = mysqli_real_escape_string($koneksi, $_POST['nama_spbu'] ?? '');
$wa = mysqli_real_escape_string($koneksi, $_POST['no_wa'] ?? '');
$status = mysqli_real_escape_string($koneksi, $_POST['status_24_jam'] ?? 'no');
if (!$id || !$nama || !$wa) {
echo "Gagal: Data tidak lengkap!";
exit;
}
$query = "UPDATE `$nama_tabel` SET nama_spbu='$nama', no_wa='$wa', status_24_jam='$status' WHERE id='$id'";
if (mysqli_query($koneksi, $query)) {
echo "Data SPBU berhasil diperbarui!";
} else {
echo "Gagal memperbarui data: " . mysqli_error($koneksi);
}
?>
+20
View File
@@ -0,0 +1,20 @@
<?php
include 'koneksi.php';
header('Content-Type: text/plain; charset=utf-8');
$id = mysqli_real_escape_string($koneksi, $_POST['id'] ?? '');
if (!$id) {
echo "Gagal: ID tidak ditemukan!";
exit;
}
$query = "DELETE FROM `$nama_tabel` WHERE id='$id'";
if (mysqli_query($koneksi, $query)) {
echo "Data berhasil dihapus!";
} else {
echo "Gagal menghapus data: " . mysqli_error($koneksi);
}
?>
+790
View File
@@ -0,0 +1,790 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS SPBU Pontianak</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
<!-- ✅ FIX 1: Leaflet CSS & JS dipindah ke <head> agar sudah siap sebelum script dijalankan -->
<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>
:root {
--bg: #0d0f14;
--surface: #161923;
--surface2: #1e2330;
--border: rgba(255,255,255,0.07);
--accent-green: #00e5a0;
--accent-amber: #ffb830;
--accent-blue: #3d8bff;
--text: #e8ecf4;
--text-muted: #6b7385;
--danger: #ff4d6a;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'DM Sans', sans-serif;
background: var(--bg);
color: var(--text);
display: flex;
height: 100vh;
overflow: hidden;
}
/* ─── SIDEBAR ─── */
#sidebar {
width: 340px;
min-width: 340px;
background: var(--surface);
display: flex;
flex-direction: column;
border-right: 1px solid var(--border);
z-index: 1000;
overflow: hidden;
}
.sidebar-header {
padding: 28px 24px 20px;
border-bottom: 1px solid var(--border);
background: linear-gradient(135deg, rgba(61,139,255,0.12), rgba(0,229,160,0.06));
position: relative;
overflow: hidden;
}
.sidebar-header::before {
content: '';
position: absolute;
top: -40px; right: -40px;
width: 130px; height: 130px;
border-radius: 50%;
background: radial-gradient(circle, rgba(0,229,160,0.15), transparent 70%);
}
.logo-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 4px;
}
.logo-icon {
width: 40px; height: 40px;
background: linear-gradient(135deg, var(--accent-green), var(--accent-blue));
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 18px;
}
.sidebar-header h1 {
font-family: 'Syne', sans-serif;
font-size: 20px;
font-weight: 800;
letter-spacing: -0.5px;
}
.sidebar-header p {
font-size: 12px;
color: var(--text-muted);
margin-left: 52px;
}
/* Stats */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
padding: 18px 20px;
border-bottom: 1px solid var(--border);
}
.stat-box {
background: var(--surface2);
border-radius: 10px;
padding: 12px 10px;
text-align: center;
border: 1px solid var(--border);
transition: transform 0.2s;
}
.stat-box:hover { transform: translateY(-2px); }
.stat-num {
font-family: 'Syne', sans-serif;
font-size: 26px;
font-weight: 800;
line-height: 1;
}
.stat-label { font-size: 10px; color: var(--text-muted); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
.stat-box.total .stat-num { color: var(--accent-blue); }
.stat-box.jam24 .stat-num { color: var(--accent-green); }
.stat-box.reguler .stat-num { color: var(--accent-amber); }
/* Layer Control Panel */
.layer-panel {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
}
.panel-title {
font-family: 'Syne', sans-serif;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-muted);
margin-bottom: 12px;
}
.layer-toggle {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
margin-bottom: 6px;
border: 1px solid var(--border);
}
.layer-toggle:hover { background: var(--surface2); }
.toggle-dot {
width: 12px; height: 12px;
border-radius: 3px;
flex-shrink: 0;
}
.toggle-dot.green { background: var(--accent-green); box-shadow: 0 0 8px rgba(0,229,160,0.5); }
.toggle-dot.amber { background: var(--accent-amber); box-shadow: 0 0 8px rgba(255,184,48,0.5); }
.layer-toggle span { font-size: 13px; flex-grow: 1; }
.toggle-count {
font-size: 11px;
background: var(--surface2);
padding: 2px 7px;
border-radius: 20px;
color: var(--text-muted);
}
/* Custom Checkbox */
.layer-toggle input[type="checkbox"] {
appearance: none;
width: 18px; height: 18px;
border: 2px solid var(--border);
border-radius: 5px;
cursor: pointer;
position: relative;
transition: all 0.2s;
flex-shrink: 0;
}
.layer-toggle input[type="checkbox"]:checked {
background: var(--accent-blue);
border-color: var(--accent-blue);
}
.layer-toggle input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
color: white;
font-size: 11px;
top: 0px; left: 2px;
font-weight: bold;
}
/* SPBU List */
.list-header {
padding: 14px 20px 8px;
font-family: 'Syne', sans-serif;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-muted);
}
#spbu-list {
flex-grow: 1;
overflow-y: auto;
padding: 0 12px 12px;
scrollbar-width: thin;
scrollbar-color: var(--surface2) transparent;
}
.spbu-item {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 12px;
border-radius: 8px;
margin-bottom: 5px;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s;
}
.spbu-item:hover {
background: var(--surface2);
border-color: var(--border);
}
.spbu-dot {
width: 8px; height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.spbu-dot.green { background: var(--accent-green); }
.spbu-dot.amber { background: var(--accent-amber); }
.spbu-item-name { font-size: 13px; font-weight: 500; flex-grow: 1; }
.badge-pill {
font-size: 9px;
padding: 3px 7px;
border-radius: 20px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.badge-pill.green { background: rgba(0,229,160,0.15); color: var(--accent-green); }
.badge-pill.amber { background: rgba(255,184,48,0.15); color: var(--accent-amber); }
/* ─── MAP ─── */
#map-container {
flex-grow: 1;
position: relative;
/* ✅ FIX 2: Pastikan tinggi eksplisit agar peta punya dimensi */
height: 100vh;
min-height: 0;
}
#map {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
}
/* Leaflet overrides for dark theme */
.leaflet-popup-content-wrapper {
background: var(--surface) !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
border-radius: 12px !important;
box-shadow: 0 20px 60px rgba(0,0,0,0.6) !important;
padding: 0 !important;
overflow: hidden;
}
.leaflet-popup-tip { background: var(--surface) !important; }
.leaflet-popup-content { margin: 0 !important; }
.leaflet-control-zoom {
border: none !important;
border-radius: 10px !important;
overflow: hidden;
}
.leaflet-control-zoom a {
background: var(--surface) !important;
color: var(--text) !important;
border-bottom: 1px solid var(--border) !important;
}
.leaflet-control-zoom a:hover { background: var(--surface2) !important; }
/* Popup Form */
.popup-wrap { padding: 18px; min-width: 220px; }
.popup-type {
font-size: 9px;
font-family: 'Syne', sans-serif;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 4px;
}
.popup-name {
font-family: 'Syne', sans-serif;
font-size: 16px;
font-weight: 700;
margin-bottom: 12px;
}
.popup-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-muted);
margin-bottom: 6px;
}
.popup-row strong { color: var(--text); }
.popup-divider { height: 1px; background: var(--border); margin: 14px 0; }
.popup-input {
width: 100%;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 7px;
padding: 9px 12px;
color: var(--text);
font-family: 'DM Sans', sans-serif;
font-size: 13px;
margin-bottom: 8px;
outline: none;
transition: border-color 0.2s;
}
.popup-input:focus { border-color: var(--accent-blue); }
.popup-actions { display: flex; gap: 7px; margin-top: 12px; }
.pop-btn {
flex: 1;
padding: 9px;
border: none;
border-radius: 7px;
font-family: 'Syne', sans-serif;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
cursor: pointer;
transition: all 0.2s;
}
.pop-btn:hover { filter: brightness(1.1); transform: translateY(-1px); }
.pop-btn.blue { background: var(--accent-blue); color: white; }
.pop-btn.amber { background: var(--accent-amber); color: #1a1000; }
.pop-btn.red { background: var(--danger); color: white; }
.pop-btn.gray { background: var(--surface2); color: var(--text-muted); border: 1px solid var(--border); }
/* Map hint badge */
.map-hint {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(13,15,20,0.85);
backdrop-filter: blur(10px);
border: 1px solid var(--border);
border-radius: 30px;
padding: 8px 18px;
font-size: 12px;
color: var(--text-muted);
z-index: 999;
pointer-events: none;
}
.map-hint span { color: var(--accent-blue); font-weight: 600; }
/* ✅ FIX 3: Loading overlay HTML ditambahkan */
.loading-overlay {
position: absolute;
inset: 0;
background: var(--bg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
transition: opacity 0.5s;
}
.loading-overlay.hidden { opacity: 0; pointer-events: none; }
.loader-ring {
width: 50px; height: 50px;
border: 3px solid var(--surface2);
border-top-color: var(--accent-green);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 16px;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Animations */
.spbu-item { animation: fadeUp 0.3s ease backwards; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body>
<div id="sidebar">
<div class="sidebar-header">
<div class="logo-row">
<div class="logo-icon"></div>
<h1>SPBU Pontianak</h1>
</div>
<p>WebGIS — Sistem Informasi Geografis</p>
</div>
<div class="stats-grid">
<div class="stat-box total">
<div class="stat-num" id="stat-total"></div>
<div class="stat-label">Total</div>
</div>
<div class="stat-box jam24">
<div class="stat-num" id="stat-24"></div>
<div class="stat-label">24 Jam</div>
</div>
<div class="stat-box reguler">
<div class="stat-num" id="stat-reg"></div>
<div class="stat-label">Reguler</div>
</div>
</div>
<div class="layer-panel">
<div class="panel-title">Layer Control</div>
<label class="layer-toggle">
<div class="toggle-dot green"></div>
<span>SPBU Buka 24 Jam</span>
<span class="toggle-count" id="cnt-24">0</span>
<input type="checkbox" id="chk-24" checked>
</label>
<label class="layer-toggle">
<div class="toggle-dot amber"></div>
<span>SPBU Buka Reguler</span>
<span class="toggle-count" id="cnt-reg">0</span>
<input type="checkbox" id="chk-reg" checked>
</label>
</div>
<div class="list-header">Daftar SPBU</div>
<div id="spbu-list">
<p style="text-align:center;color:var(--text-muted);font-size:13px;padding:20px;">Memuat data...</p>
</div>
</div>
<div id="map-container">
<!-- ✅ FIX 3: Loading overlay ditambahkan ke HTML -->
<div class="loading-overlay" id="loading-overlay">
<div class="loader-ring"></div>
<p style="color:var(--text-muted);font-size:13px;">Memuat peta...</p>
</div>
<div id="map"></div>
<div class="map-hint">Klik <span>di mana saja</span> pada peta untuk menambah SPBU baru</div>
</div>
<!-- ✅ FIX 1: Script Leaflet DIHAPUS dari sini karena sudah dipindah ke <head> -->
<script>
// ── MAP INIT ──
// ✅ FIX 4: Inisialisasi map dibungkus dalam DOMContentLoaded agar aman
document.addEventListener('DOMContentLoaded', function() {
const map = L.map('map', { zoomControl: false }).setView([-0.026330, 109.342504], 13);
L.control.zoom({ position: 'topright' }).addTo(map);
// ✅ FIX 5: Tile layer dengan 2 pilihan — CartoDB dark (utama) + OSM (fallback)
const tileCartoDB = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_matter/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/">CartoDB</a>',
maxZoom: 19,
subdomains: 'abcd'
});
const tileOSM = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 19
});
// Coba CartoDB dulu, jika tile gagal load pakai OSM
tileCartoDB.addTo(map);
tileCartoDB.on('tileerror', function() {
map.removeLayer(tileCartoDB);
tileOSM.addTo(map);
});
// ── LAYER GROUPS ──
// Dibuat dulu, belum ditambah ke map — ditambah sesuai state checkbox
const layer24 = L.layerGroup();
const layerReg = L.layerGroup();
// ── EVENT LAYER CONTROL (FIX UTAMA) ──
document.getElementById('chk-24').addEventListener('change', function() {
if (this.checked) {
layer24.addTo(map);
} else {
map.removeLayer(layer24);
}
});
document.getElementById('chk-reg').addEventListener('change', function() {
if (this.checked) {
layerReg.addTo(map);
} else {
map.removeLayer(layerReg);
}
});
// ── ICONS ──
function makeIcon(color) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="42" viewBox="0 0 32 42">
<defs>
<filter id="s${color.replace('#','')}"><feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="${color}" flood-opacity="0.4"/></filter>
</defs>
<path fill="${color}" filter="url(#s${color.replace('#','')})" d="M16 0C7.163 0 0 7.163 0 16c0 12 16 26 16 26s16-14 16-26C32 7.163 24.837 0 16 0z"/>
<circle cx="16" cy="16" r="8" fill="white" opacity="0.9"/>
<circle cx="16" cy="16" r="5" fill="${color}"/>
</svg>`;
return L.divIcon({
html: svg,
className: '',
iconSize: [32, 42],
iconAnchor: [16, 42],
popupAnchor: [0, -42]
});
}
const iconGreen = makeIcon('#00e5a0');
const iconAmber = makeIcon('#ffb830');
const iconTemp = makeIcon('#3d8bff');
// ── DATA ──
let allData = [];
let tempMarker = null;
function muatData() {
fetch('ambil_data.php')
.then(r => r.json())
.then(data => {
allData = data;
renderAll(data);
})
.catch(() => {
// Demo mode jika tidak ada server
renderAll(demoData());
});
}
function demoData() {
return [
{id:'1', nama_spbu:'SPBU Jl. Ahmad Yani', no_wa:'08123456789', status_24_jam:'yes', lat:-0.0202, lng:109.3423},
{id:'2', nama_spbu:'SPBU Jl. Gajah Mada', no_wa:'08234567890', status_24_jam:'no', lat:-0.0280, lng:109.3310},
{id:'3', nama_spbu:'SPBU Sui Raya', no_wa:'08345678901', status_24_jam:'yes', lat:-0.0450, lng:109.3580},
{id:'4', nama_spbu:'SPBU Jl. Tanjungpura', no_wa:'08456789012', status_24_jam:'no', lat:-0.0120, lng:109.3390},
{id:'5', nama_spbu:'SPBU Jl. Imam Bonjol', no_wa:'08567890123', status_24_jam:'yes', lat:-0.0330, lng:109.3500},
{id:'6', nama_spbu:'SPBU Siantan', no_wa:'08678901234', status_24_jam:'no', lat:0.0050, lng:109.3350},
];
}
function renderAll(data) {
layer24.clearLayers();
layerReg.clearLayers();
let c24 = 0, cReg = 0;
let listHTML = '';
data.forEach((item, i) => {
const is24 = item.status_24_jam === 'yes';
const icon = is24 ? iconGreen : iconAmber;
const layer = is24 ? layer24 : layerReg;
if (is24) c24++; else cReg++;
// Tambah marker ke layerGroup (bukan langsung ke map)
const marker = L.marker([parseFloat(item.lat), parseFloat(item.lng)], { icon, draggable: true });
layer.addLayer(marker);
marker.bindPopup(buildInfoPopup(item), { maxWidth: 280 });
marker.on('dragend', function() {
const pos = marker.getLatLng();
if (confirm('Simpan koordinat baru?')) updateLokasi(item.id, pos.lat, pos.lng);
else muatData();
});
const delay = i * 40;
listHTML += `
<div class="spbu-item" style="animation-delay:${delay}ms" onclick="flyTo(${item.lat},${item.lng})">
<div class="spbu-dot ${is24 ? 'green' : 'amber'}"></div>
<span class="spbu-item-name">${item.nama_spbu}</span>
<span class="badge-pill ${is24 ? 'green' : 'amber'}">${is24 ? '24 Jam' : 'Reguler'}</span>
</div>
`;
});
document.getElementById('stat-total').textContent = data.length;
document.getElementById('stat-24').textContent = c24;
document.getElementById('stat-reg').textContent = cReg;
document.getElementById('cnt-24').textContent = c24;
document.getElementById('cnt-reg').textContent = cReg;
document.getElementById('spbu-list').innerHTML = listHTML || '<p style="text-align:center;color:var(--text-muted);font-size:13px;padding:20px;">Belum ada data.</p>';
// Terapkan state checkbox saat ini ke masing-masing layer
const show24 = document.getElementById('chk-24').checked;
const showReg = document.getElementById('chk-reg').checked;
show24 ? layer24.addTo(map) : map.removeLayer(layer24);
showReg ? layerReg.addTo(map) : map.removeLayer(layerReg);
// Sembunyikan loading overlay setelah data selesai dirender
const overlay = document.getElementById('loading-overlay');
if (overlay) overlay.classList.add('hidden');
}
window.flyTo = function flyTo(lat, lng) {
map.flyTo([lat, lng], 16, { duration: 1 });
};
// ── POPUP BUILDERS ──
function buildInfoPopup(item) {
const is24 = item.status_24_jam === 'yes';
return `
<div class="popup-wrap">
<div class="popup-type">Stasiun Bahan Bakar</div>
<div class="popup-name">${item.nama_spbu}</div>
<div class="popup-row">📞 <strong>${item.no_wa}</strong></div>
<div class="popup-row">🕐 Status: <span class="badge-pill ${is24?'green':'amber'}" style="margin-left:4px">${is24?'Buka 24 Jam':'Buka Reguler'}</span></div>
<div class="popup-divider"></div>
<div class="popup-actions">
<button class="pop-btn amber" onclick="bukaEdit('${item.id}','${item.nama_spbu}','${item.no_wa}','${item.status_24_jam}',${item.lat},${item.lng})">✏ Edit</button>
<button class="pop-btn red" onclick="hapusData('${item.id}')">🗑 Hapus</button>
</div>
</div>
`;
}
function buildAddPopup(lat, lng) {
return `
<div class="popup-wrap">
<div class="popup-type">Tambah Lokasi Baru</div>
<div class="popup-name" style="font-size:14px;margin-bottom:14px;">Data SPBU</div>
<input class="popup-input" id="add_nama" placeholder="Nama SPBU" />
<input class="popup-input" id="add_wa" placeholder="Nomor WhatsApp" />
<select class="popup-input" id="add_status">
<option value="no">Buka Reguler</option>
<option value="yes">Buka 24 Jam</option>
</select>
<div class="popup-actions">
<button class="pop-btn blue" onclick="prosesSimpan(${lat},${lng},'tambah')">💾 Simpan</button>
<button class="pop-btn gray" onclick="batalTambah()">✕</button>
</div>
</div>
`;
}
function buildEditPopup(id, nama, wa, status, lat, lng) {
const sY = status==='yes'?'selected':'';
const sN = status==='no'?'selected':'';
return `
<div class="popup-wrap">
<div class="popup-type">Edit Data</div>
<input type="hidden" id="edit_id" value="${id}">
<input class="popup-input" id="edit_nama" value="${nama}">
<input class="popup-input" id="edit_wa" value="${wa}">
<select class="popup-input" id="edit_status">
<option value="no" ${sN}>Buka Reguler</option>
<option value="yes" ${sY}>Buka 24 Jam</option>
</select>
<div class="popup-actions">
<button class="pop-btn blue" onclick="prosesSimpan(${lat},${lng},'edit')">✓ Update</button>
<button class="pop-btn gray" onclick="map.closePopup()">✕ Batal</button>
</div>
</div>
`;
}
// ── MAP CLICK ADD ──
map.on('click', function(e) {
if (tempMarker) map.removeLayer(tempMarker);
tempMarker = L.marker([e.latlng.lat, e.latlng.lng], { icon: iconTemp }).addTo(map);
tempMarker.bindPopup(buildAddPopup(e.latlng.lat, e.latlng.lng), { maxWidth: 280 }).openPopup();
});
window.batalTambah = function() {
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
map.closePopup();
};
// ── EDIT ──
window.bukaEdit = function(id, nama, wa, status, lat, lng) {
L.popup({ maxWidth: 280 })
.setLatLng([lat, lng])
.setContent(buildEditPopup(id, nama, wa, status, lat, lng))
.openOn(map);
};
// ── SIMPAN (POST) ──
window.prosesSimpan = function prosesSimpan(lat, lng, mode) {
const pfx = mode === 'edit' ? 'edit_' : 'add_';
const id = mode === 'edit' ? document.getElementById('edit_id').value : '';
const nama = document.getElementById(pfx + 'nama').value.trim();
const wa = document.getElementById(pfx + 'wa').value.trim();
const stat = document.getElementById(pfx + 'status').value;
if (!nama || !wa) return alert('Nama dan nomor WA wajib diisi!');
const fd = new FormData();
if (mode === 'edit') fd.append('id', id);
fd.append('nama_spbu', nama);
fd.append('no_wa', wa);
fd.append('status_24_jam', stat);
fd.append('lat', lat);
fd.append('lng', lng);
const url = mode === 'edit' ? 'edit_data.php' : 'simpan_data.php';
fetch(url, { method: 'POST', body: fd })
.then(r => r.text())
.then(msg => {
showToast(msg, 'success');
map.closePopup();
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
muatData();
})
.catch(() => {
// Demo mode: update state lokal agar data langsung tampil tanpa server
if (mode === 'tambah') {
const newItem = {
id: Date.now().toString(),
nama_spbu: nama,
no_wa: wa,
status_24_jam: stat,
lat: lat,
lng: lng
};
allData.push(newItem);
} else {
const idx = allData.findIndex(d => d.id === id);
if (idx !== -1) {
allData[idx].nama_spbu = nama;
allData[idx].no_wa = wa;
allData[idx].status_24_jam = stat;
}
}
renderAll(allData);
showToast(mode === 'tambah' ? 'Data berhasil disimpan! (demo mode)' : 'Data berhasil diperbarui! (demo mode)', 'success');
map.closePopup();
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
});
}
// ── HAPUS ──
window.hapusData = function hapusData(id) {
if (!confirm('Hapus data SPBU ini?')) return;
const fd = new FormData();
fd.append('id', id);
fetch('hapus_data.php', { method: 'POST', body: fd })
.then(r => r.text())
.then(msg => { showToast(msg, 'danger'); muatData(); })
.catch(() => {
allData = allData.filter(d => d.id !== id);
renderAll(allData);
showToast('Data dihapus! (demo mode)', 'danger');
});
}
// ── UPDATE LOKASI ──
window.updateLokasi = function updateLokasi(id, lat, lng) {
const fd = new FormData();
fd.append('id', id); fd.append('lat', lat); fd.append('lng', lng);
fetch('update_lokasi.php', { method: 'POST', body: fd })
.then(() => { showToast('Lokasi diperbarui!', 'success'); muatData(); })
.catch(() => { showToast('Lokasi diperbarui! (demo mode)', 'success'); muatData(); });
}
// ── TOAST ──
function showToast(msg, type) {
const t = document.createElement('div');
t.style.cssText = `
position:fixed; bottom:30px; right:30px; z-index:9999;
background:${type==='success'?'#00e5a0':'#ff4d6a'};
color:${type==='success'?'#001a0d':'#fff'};
padding:12px 20px; border-radius:10px; font-size:13px; font-weight:600;
font-family:'Syne',sans-serif; box-shadow:0 8px 30px rgba(0,0,0,0.4);
animation: slideIn 0.3s ease;
`;
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 3000);
}
// ── START ──
muatData();
// ✅ FIX 6: invalidateSize dipanggil beberapa kali untuk memastikan peta terrender sempurna
setTimeout(() => map.invalidateSize(), 100);
setTimeout(() => map.invalidateSize(), 500);
}); // tutup DOMContentLoaded
</script>
</body>
</html>
+16
View File
@@ -0,0 +1,16 @@
<?php
$host = getenv('DB_HOST') ?: 'localhost';
$user = getenv('DB_USER') ?: 'root';
$pass = getenv('DB_PASSWORD') ?: '';
$nama_database = getenv('DB_NAME') ?: 'spbulayer';
$nama_tabel = "tbl_spbu";
$koneksi = mysqli_connect($host, $user, $pass, $nama_database);
// Cek koneksi agar tidak muncul error Fatal lagi
if (!$koneksi) {
die("Koneksi ke database gagal: " . mysqli_connect_error());
}
?>
+6
View File
@@ -0,0 +1,6 @@
{
"name": "spbu' - Copy",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+46
View File
@@ -0,0 +1,46 @@
-- ============================================================
-- Schema MySQL untuk WebGIS SPBU Pontianak
-- ============================================================
-- CARA PAKAI:
-- 1. Buka phpMyAdmin
-- 2. Pilih database "data_spbu" (atau buat baru jika belum ada)
-- 3. Klik tab "SQL"
-- 4. Copy-paste seluruh isi file ini, lalu klik "Go"
-- ============================================================
-- CATATAN: nama tabel sengaja dibuat "tbl_spbu" (bukan "data_spbu")
-- karena MySQL tidak boleh memiliki nama tabel sama dengan nama database
-- (menyebabkan error errno: 121)
CREATE DATABASE IF NOT EXISTS `data_spbu`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE `data_spbu`;
-- Hapus tabel lama jika ada
DROP TABLE IF EXISTS `tbl_spbu`;
DROP TABLE IF EXISTS `data_spbu`;
-- Buat tabel dengan nama tbl_spbu
CREATE TABLE `tbl_spbu` (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_spbu VARCHAR(255) NOT NULL,
no_wa VARCHAR(20) NOT NULL,
status_24_jam ENUM('yes','no') DEFAULT 'no',
lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Data contoh
INSERT INTO `tbl_spbu` (nama_spbu, no_wa, status_24_jam, lat, lng) VALUES
('SPBU Jl. Ahmad Yani', '08123456789', 'yes', -0.0202, 109.3423),
('SPBU Jl. Gajah Mada', '08234567890', 'no', -0.0280, 109.3310),
('SPBU Sui Raya', '08345678901', 'yes', -0.0450, 109.3580),
('SPBU Jl. Tanjungpura', '08456789012', 'no', -0.0120, 109.3390),
('SPBU Jl. Imam Bonjol', '08567890123', 'yes', -0.0330, 109.3500),
('SPBU Siantan', '08678901234', 'no', 0.0050, 109.3350);
-- Cek hasil
SELECT 'Tabel tbl_spbu berhasil dibuat!' AS status, COUNT(*) AS jumlah_data FROM `tbl_spbu`;
+25
View File
@@ -0,0 +1,25 @@
<?php
include 'koneksi.php';
header('Content-Type: text/plain; charset=utf-8');
$nama = mysqli_real_escape_string($koneksi, $_POST['nama_spbu'] ?? '');
$wa = mysqli_real_escape_string($koneksi, $_POST['no_wa'] ?? '');
$status = mysqli_real_escape_string($koneksi, $_POST['status_24_jam'] ?? 'no');
$lat = floatval($_POST['lat'] ?? 0);
$lng = floatval($_POST['lng'] ?? 0);
if (!$nama || !$wa) {
echo "Gagal: Nama dan nomor WA wajib diisi!";
exit;
}
$query = "INSERT INTO `$nama_tabel` (nama_spbu, no_wa, status_24_jam, lat, lng)
VALUES ('$nama', '$wa', '$status', '$lat', '$lng')";
if (mysqli_query($koneksi, $query)) {
echo "Data SPBU berhasil ditambahkan!";
} else {
echo "Gagal menambahkan data: " . mysqli_error($koneksi);
}
?>
+15
View File
@@ -0,0 +1,15 @@
<?php
include 'koneksi.php';
$id = mysqli_real_escape_string($koneksi, $_POST['id']);
$lat = mysqli_real_escape_string($koneksi, $_POST['lat']);
$lng = mysqli_real_escape_string($koneksi, $_POST['lng']);
$query = "UPDATE $nama_tabel SET lat='$lat', lng='$lng' WHERE id='$id'";
if(mysqli_query($koneksi, $query)) {
echo "Lokasi berhasil dipindahkan!";
} else {
echo "Gagal memindahkan lokasi: " . mysqli_error($koneksi);
}
?>