Deploy WebGIS 01-05 dengan Docker

This commit is contained in:
2026-06-12 17:58:06 +07:00
commit 1bf000979c
76 changed files with 12301 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
-- phpMyAdmin SQL Dump
-- version 5.2.3
-- https://www.phpmyadmin.net/
--
-- Host: localhost:3306
-- Generation Time: Jun 11, 2026 at 11:13 AM
-- Server version: 8.4.3
-- PHP Version: 8.3.30
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Database: `04_spbudb`
--
-- --------------------------------------------------------
--
-- Table structure for table `spbu`
--
CREATE TABLE `spbu` (
`id` int NOT NULL,
`nama_spbu` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`status_ramai` enum('ramai','tidak_ramai') COLLATE utf8mb4_unicode_ci NOT NULL,
`buka_24_jam` enum('ya','tidak') COLLATE utf8mb4_unicode_ci NOT NULL,
`status_operasional` enum('buka','tutup') COLLATE utf8mb4_unicode_ci NOT NULL,
`nomor_hp` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`latitude` double NOT NULL,
`longitude` double NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `spbu`
--
INSERT INTO `spbu` (`id`, `nama_spbu`, `status_ramai`, `buka_24_jam`, `status_operasional`, `nomor_hp`, `latitude`, `longitude`, `created_at`, `updated_at`) VALUES
(1, 'SPBU Gertak 1', 'tidak_ramai', 'ya', 'buka', '0815497422156', -0.0551481, 109.3480718, '2026-06-09 06:06:31', '2026-06-09 06:06:31'),
(2, 'SPBU Gertak 1', 'ramai', 'ya', 'buka', '0815497422156', -0.0548271, 109.3477392, '2026-06-09 06:06:52', '2026-06-09 06:07:09'),
(3, 'SPBU OSO', 'ramai', 'tidak', 'buka', '0815497422156', -0.0551277, 109.3474925, '2026-06-09 06:07:35', '2026-06-09 06:07:35'),
(4, 'SPBU OSO', 'tidak_ramai', 'tidak', 'buka', '087894587965', -0.0552458, 109.3478036, '2026-06-09 06:07:54', '2026-06-09 06:07:54'),
(5, 'SPBU Gertak 2', 'ramai', 'ya', 'tutup', '0815497422156', -0.0554175, 109.3474495, '2026-06-09 06:08:15', '2026-06-09 06:08:15');
--
-- Indexes for dumped tables
--
--
-- Indexes for table `spbu`
--
ALTER TABLE `spbu`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `spbu`
--
ALTER TABLE `spbu`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* ambil_data.php
* Mengambil seluruh data SPBU dari database dan mengembalikannya
* dalam format JSON. Dipanggil oleh JavaScript saat halaman dimuat
* dan saat dibutuhkan (misalnya untuk membuka form edit).
*/
// ── Sertakan file koneksi database ───────────────────────────────────────────
require_once 'koneksi.php';
// Set header response sebagai JSON
header('Content-Type: application/json');
// ── Ambil semua data dari tabel spbu, diurutkan dari yang terbaru ─────────────
$result = $koneksi->query("SELECT * FROM spbu ORDER BY id DESC");
// Cek apakah query berhasil
if (!$result) {
echo json_encode(['status' => 'error', 'message' => 'Gagal mengambil data: ' . $koneksi->error]);
exit;
}
// ── Kumpulkan semua baris ke dalam array ──────────────────────────────────────
$data = [];
while ($row = $result->fetch_assoc()) {
// Konversi latitude & longitude ke float agar JSON valid
$row['latitude'] = (float) $row['latitude'];
$row['longitude'] = (float) $row['longitude'];
$data[] = $row;
}
// ── Kembalikan data sebagai JSON ──────────────────────────────────────────────
echo json_encode([
'status' => 'success',
'data' => $data
]);
$koneksi->close();
?>
+115
View File
@@ -0,0 +1,115 @@
<?php
/**
* edit.php
* Menerima data POST dari JavaScript (fetch + FormData),
* memvalidasi, lalu memperbarui data SPBU di database.
* PENTING: Latitude dan longitude TIDAK diproses dan TIDAK diubah.
* Mengembalikan data terbaru agar JS bisa memperbarui marker dan layer group.
*/
// ── Sertakan file koneksi database ───────────────────────────────────────────
require_once 'koneksi.php';
// Set header response sebagai JSON
header('Content-Type: application/json');
// ── Hanya menerima method POST ────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan.']);
exit;
}
// ── Ambil & bersihkan input dari form (tanpa latitude/longitude) ──────────────
$id = intval($_POST['id'] ?? 0);
$nama_spbu = trim($_POST['nama_spbu'] ?? '');
$status_ramai = trim($_POST['status_ramai'] ?? '');
$buka_24_jam = trim($_POST['buka_24_jam'] ?? '');
$status_operasional = trim($_POST['status_operasional'] ?? '');
$nomor_hp = trim($_POST['nomor_hp'] ?? '');
// ── Validasi: ID harus valid ──────────────────────────────────────────────────
if ($id <= 0) {
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid.']);
exit;
}
// ── Validasi: semua field wajib terisi ───────────────────────────────────────
if (
empty($nama_spbu) ||
empty($status_ramai) ||
empty($buka_24_jam) ||
empty($status_operasional) ||
empty($nomor_hp)
) {
echo json_encode(['status' => 'error', 'message' => 'Semua field wajib diisi.']);
exit;
}
// ── Validasi nilai ENUM (server-side) ────────────────────────────────────────
$valid_ramai = ['ramai', 'tidak_ramai'];
$valid_24jam = ['ya', 'tidak'];
$valid_operasional = ['buka', 'tutup'];
if (!in_array($status_ramai, $valid_ramai)) {
echo json_encode(['status' => 'error', 'message' => 'Nilai status ramai tidak valid.']);
exit;
}
if (!in_array($buka_24_jam, $valid_24jam)) {
echo json_encode(['status' => 'error', 'message' => 'Nilai buka 24 jam tidak valid.']);
exit;
}
if (!in_array($status_operasional, $valid_operasional)) {
echo json_encode(['status' => 'error', 'message' => 'Nilai status operasional tidak valid.']);
exit;
}
// ── Update data di database menggunakan prepared statement ───────────────────
// Latitude dan longitude TIDAK disertakan dalam UPDATE — tidak berubah
$stmt = $koneksi->prepare(
"UPDATE spbu
SET nama_spbu = ?, status_ramai = ?, buka_24_jam = ?,
status_operasional = ?, nomor_hp = ?
WHERE id = ?"
);
// Bind parameter: s = string, i = integer
$stmt->bind_param(
'sssssi',
$nama_spbu,
$status_ramai,
$buka_24_jam,
$status_operasional,
$nomor_hp,
$id
);
// Eksekusi query
if ($stmt->execute()) {
if ($stmt->affected_rows > 0 || $stmt->affected_rows === 0) {
// Ambil data terbaru dari DB untuk dikembalikan ke JavaScript
// JS menggunakan data ini untuk memperbarui marker & layer group
$result = $koneksi->query("SELECT * FROM spbu WHERE id = $id");
if ($result && $result->num_rows > 0) {
$updatedData = $result->fetch_assoc();
$updatedData['latitude'] = (float) $updatedData['latitude'];
$updatedData['longitude'] = (float) $updatedData['longitude'];
echo json_encode([
'status' => 'success',
'message' => 'Data SPBU berhasil diperbarui.',
'data' => $updatedData
]);
} else {
echo json_encode(['status' => 'error', 'message' => 'Data tidak ditemukan setelah update.']);
}
} else {
echo json_encode(['status' => 'error', 'message' => 'Data tidak ditemukan.']);
}
} else {
echo json_encode(['status' => 'error', 'message' => 'Gagal memperbarui data: ' . $stmt->error]);
}
$stmt->close();
$koneksi->close();
?>
+53
View File
@@ -0,0 +1,53 @@
<?php
/**
* hapus.php
* Menerima ID dari JavaScript (fetch + FormData),
* lalu menghapus data SPBU yang sesuai dari database.
* JS akan menghapus marker dari layer group setelah response sukses.
*/
// ── Sertakan file koneksi database ───────────────────────────────────────────
require_once 'koneksi.php';
// Set header response sebagai JSON
header('Content-Type: application/json');
// ── Hanya menerima method POST ────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan.']);
exit;
}
// ── Ambil ID dari data POST ───────────────────────────────────────────────────
$id = intval($_POST['id'] ?? 0);
// ── Validasi: ID harus valid (lebih dari 0) ───────────────────────────────────
if ($id <= 0) {
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid.']);
exit;
}
// ── Hapus data dari database menggunakan prepared statement ──────────────────
$stmt = $koneksi->prepare("DELETE FROM spbu WHERE id = ?");
$stmt->bind_param('i', $id);
// Eksekusi query
if ($stmt->execute()) {
if ($stmt->affected_rows > 0) {
// Kembalikan ID yang dihapus agar JS bisa membersihkan marker dari layer group
echo json_encode([
'status' => 'success',
'message' => 'Data SPBU berhasil dihapus.',
'id' => $id
]);
} else {
echo json_encode(['status' => 'error', 'message' => 'Data tidak ditemukan.']);
}
} else {
echo json_encode(['status' => 'error', 'message' => 'Gagal menghapus data: ' . $stmt->error]);
}
$stmt->close();
$koneksi->close();
?>
+793
View File
@@ -0,0 +1,793 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS SPBU Layer Groups</title>
<!-- Leaflet CSS (tanpa integrity dan crossorigin) -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
/* ── Reset ────────────────────────────────────────────────── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html { height: 100%; }
body {
height: 100%;
font-family: sans-serif;
display: flex;
flex-direction: column;
}
/* ── Header ──────────────────────────────────────────────── */
#header {
background: #1a56db;
color: white;
padding: 0 20px;
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
z-index: 1000;
}
#header h1 { font-size: 1rem; font-weight: 700; }
#header span { font-size: 0.75rem; opacity: 0.85; }
/* ── Peta (tinggi wajib sesuai spec) ─────────────────────── */
#map {
width: 100%;
height: calc(100vh - 50px);
flex: 1;
}
/* ── Toast notification ──────────────────────────────────── */
#toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%) translateY(60px);
background: #1f2937;
color: white;
padding: 8px 20px;
border-radius: 20px;
font-size: 0.85rem;
z-index: 9999;
opacity: 0;
transition: all 0.3s;
pointer-events: none;
}
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
#toast.ok { background: #057a55; }
#toast.err { background: #e02424; }
/* ── Loading spinner ─────────────────────────────────────── */
#loading {
display: none;
position: fixed;
inset: 0;
background: rgba(255,255,255,0.5);
z-index: 8000;
align-items: center;
justify-content: center;
}
#loading.show { display: flex; }
.spin {
width: 36px; height: 36px;
border: 4px solid #ddd;
border-top-color: #1a56db;
border-radius: 50%;
animation: sp 0.7s linear infinite;
}
@keyframes sp { to { transform: rotate(360deg); } }
/* ── Popup Leaflet ───────────────────────────────────────── */
.leaflet-popup-content-wrapper {
border-radius: 10px !important;
padding: 0 !important;
overflow: hidden;
min-width: 270px;
}
.leaflet-popup-content { margin: 0 !important; }
/* Header popup */
.ph {
background: #1a56db;
color: white;
padding: 10px 14px;
font-weight: 700;
font-size: 0.9rem;
}
/* Body popup */
.pb { padding: 12px 14px; background: white; }
/* Tabel detail */
table.dt {
width: 100%; font-size: 0.82rem;
border-collapse: collapse; margin-bottom: 10px;
}
table.dt td { padding: 4px 3px; vertical-align: top; }
table.dt td:first-child { color: #6b7280; width: 110px; }
table.dt td:last-child { font-weight: 600; }
/* Form group */
.fg { margin-bottom: 9px; }
.fg label {
display: block; font-size: 0.75rem; font-weight: 700;
color: #6b7280; text-transform: uppercase;
letter-spacing: .04em; margin-bottom: 3px;
}
.fg input, .fg select {
width: 100%; padding: 6px 9px;
border: 1.5px solid #d1d9e0; border-radius: 6px;
font-size: 0.84rem; outline: none;
}
.fg input:focus, .fg select:focus { border-color: #1a56db; }
/* Button row */
.br { display: flex; gap: 7px; margin-top: 10px; }
.btn {
flex: 1; padding: 7px 10px; border: none;
border-radius: 6px; font-size: 0.82rem;
font-weight: 600; cursor: pointer;
}
.bp { background: #1a56db; color: white; }
.bd { background: #e02424; color: white; }
.bs { background: #f1f5f9; color: #333; border: 1px solid #ddd; }
.bg { background: #057a55; color: white; }
/* Kotak koordinat */
.coord-box {
background: #f0f9ff; border: 1px solid #bae6fd;
border-radius: 6px; padding: 6px 10px;
font-size: 0.78rem; color: #0369a1; margin-bottom: 9px;
}
/* ── Legend ──────────────────────────────────────────────── */
.legend {
background: white; border-radius: 8px;
padding: 10px 12px; font-size: 0.77rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
}
.legend b { display: block; margin-bottom: 6px; font-size: 0.8rem; }
.li { display: flex; align-items: center; gap: 7px; margin-bottom: 4px; }
.dot {
width: 13px; height: 13px; border-radius: 50%;
border: 2px solid rgba(0,0,0,0.2); flex-shrink: 0;
}
</style>
</head>
<body>
<div id="header">
<h1> WebGIS SPBU Layer Groups &amp; Layers Control</h1>
<span>💡 Klik lokasi pada peta untuk menambahkan SPBU</span>
</div>
<div id="map"></div>
<div id="toast"></div>
<div id="loading"><div class="spin"></div></div>
<!-- Leaflet JS (tanpa integrity dan crossorigin) -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// ════════════════════════════════════════════════════════════════
// CEK LEAFLET SEBELUM INISIALISASI
// Pastikan library Leaflet berhasil dimuat dari CDN
// ════════════════════════════════════════════════════════════════
if (typeof L === 'undefined') {
document.getElementById('map').innerHTML =
'<p style="padding:20px;color:red;">❌ Leaflet gagal dimuat. Periksa koneksi internet.</p>';
throw new Error('Leaflet not loaded');
}
// ════════════════════════════════════════════════════════════════
// VARIABEL GLOBAL
// ════════════════════════════════════════════════════════════════
// markersMap menyimpan referensi marker dengan key = id SPBU
// Setiap entry: { marker: L.Marker, layer: layerGroup }
// Menyimpan layer asal memudahkan pemindahan saat edit buka_24_jam berubah
const markersMap = {};
// Popup sementara untuk form tambah data
let tempPopup = null;
// ════════════════════════════════════════════════════════════════
// INISIALISASI PETA
// Koordinat awal: sekitar Pontianak, Kalimantan Barat
// ════════════════════════════════════════════════════════════════
const map = L.map('map').setView([-0.05566422376778593, 109.34854622732368], 17);
// Tile layer OpenStreetMap
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
// ════════════════════════════════════════════════════════════════
// PEMBUATAN LAYER GROUPS
// Dua layer group untuk memisahkan SPBU berdasarkan jam operasi
// ════════════════════════════════════════════════════════════════
// Layer group untuk SPBU yang buka 24 jam
const layerSPBU24Jam = L.layerGroup();
// Layer group untuk SPBU yang tidak buka 24 jam
const layerSPBUTidak24Jam = L.layerGroup();
// Tambahkan kedua layer ke peta secara default (langsung tampil saat halaman dibuka)
layerSPBU24Jam.addTo(map);
layerSPBUTidak24Jam.addTo(map);
// ════════════════════════════════════════════════════════════════
// PEMBUATAN LAYERS CONTROL (OVERLAY)
// Checkbox overlay — bukan radio button base layer
// Null pada parameter pertama berarti tidak ada base layer switcher
// ════════════════════════════════════════════════════════════════
const overlayMaps = {
"SPBU Buka 24 Jam": layerSPBU24Jam,
"SPBU Tidak Buka 24 Jam": layerSPBUTidak24Jam
};
// L.control.layers(baseLayers, overlayMaps) — baseLayers = null → hanya checkbox overlay
L.control.layers(null, overlayMaps).addTo(map);
// ════════════════════════════════════════════════════════════════
// LEGEND
// ════════════════════════════════════════════════════════════════
const leg = L.control({ position: 'bottomright' });
leg.onAdd = () => {
const d = L.DomUtil.create('div', 'legend');
d.innerHTML = `<b>🗺️ Keterangan Warna</b>
<div class="li"><div class="dot" style="background:#f97316"></div>Ramai · 24 Jam · Buka</div>
<div class="li"><div class="dot" style="background:#22c55e"></div>Tdk Ramai · 24 Jam · Buka</div>
<div class="li"><div class="dot" style="background:#ef4444"></div>Ramai · Tdk 24 Jam · Buka</div>
<div class="li"><div class="dot" style="background:#3b82f6"></div>Tdk Ramai · Tdk 24 Jam · Buka</div>
<div class="li"><div class="dot" style="background:#1f2937"></div>Tutup (semua jenis)</div>`;
return d;
};
leg.addTo(map);
// ════════════════════════════════════════════════════════════════
// UTILITAS: TOAST, LOADING
// ════════════════════════════════════════════════════════════════
/**
* showToast — tampilkan notifikasi singkat di bawah layar
* @param {string} message - Teks yang ditampilkan
* @param {string} type - 'ok' (hijau) atau 'err' (merah)
*/
function showToast(message, type = 'ok') {
const t = document.getElementById('toast');
t.textContent = message;
t.className = 'show ' + type;
setTimeout(() => { t.className = ''; }, 3000);
}
/** showLoading — tampilkan overlay loading spinner */
function showLoading() {
document.getElementById('loading').classList.add('show');
}
/** hideLoading — sembunyikan overlay loading spinner */
function hideLoading() {
document.getElementById('loading').classList.remove('show');
}
// ════════════════════════════════════════════════════════════════
// PENENTUAN WARNA MARKER
// Prioritas: tutup → hitam (apapun kondisi lain)
// Jika buka, warna ditentukan kombinasi status_ramai + buka_24_jam
// ════════════════════════════════════════════════════════════════
/**
* getMarkerColor — menentukan warna marker berdasarkan data SPBU
* @param {Object} data - Objek data SPBU
* @returns {string} Kode warna HEX
*/
function getMarkerColor(data) {
// Prioritas 1: jika tutup → warna HITAM
if (data.status_operasional === 'tutup') return '#1f2937';
// Prioritas 2: jika buka, tentukan warna dari kombinasi status_ramai & buka_24_jam
if (data.status_ramai === 'ramai' && data.buka_24_jam === 'ya') return '#f97316'; // ORANGE
if (data.status_ramai === 'tidak_ramai' && data.buka_24_jam === 'ya') return '#22c55e'; // HIJAU
if (data.status_ramai === 'ramai' && data.buka_24_jam === 'tidak') return '#ef4444'; // MERAH
if (data.status_ramai === 'tidak_ramai' && data.buka_24_jam === 'tidak') return '#3b82f6'; // BIRU
return '#9ca3af'; // fallback abu-abu
}
/**
* createMarkerIcon — membuat ikon marker SVG inline melalui L.divIcon
* @param {Object} data - Objek data SPBU
* @returns {L.DivIcon}
*/
function createMarkerIcon(data) {
const c = getMarkerColor(data);
// SVG pin berbentuk teardrop dengan lingkaran putih di tengah
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 36" width="26" height="38">
<path d="M12 0C5.4 0 0 5.4 0 12c0 9 12 24 12 24S24 21 24 12C24 5.4 18.6 0 12 0z"
fill="${c}" stroke="#fff" stroke-width="1.5"/>
<circle cx="12" cy="12" r="5" fill="#fff" opacity="0.9"/>
</svg>`;
return L.divIcon({
html: svg,
className: '',
iconSize: [26, 38],
iconAnchor: [13, 38],
popupAnchor: [0, -38]
});
}
/**
* getTargetLayer — menentukan layer group yang tepat berdasarkan buka_24_jam
* @param {Object} data - Objek data SPBU
* @returns {L.LayerGroup} Layer group yang sesuai
*/
function getTargetLayer(data) {
// buka_24_jam = 'ya' → masuk layerSPBU24Jam
// buka_24_jam = 'tidak' → masuk layerSPBUTidak24Jam
return data.buka_24_jam === 'ya' ? layerSPBU24Jam : layerSPBUTidak24Jam;
}
// ════════════════════════════════════════════════════════════════
// BADGE HELPER — menghasilkan label berwarna untuk popup detail
// ════════════════════════════════════════════════════════════════
function badgeR(v) {
return v === 'ramai'
? '<span style="background:#fef3c7;color:#92400e;padding:1px 7px;border-radius:10px;font-size:.75rem">Ramai</span>'
: '<span style="background:#ecfdf5;color:#065f46;padding:1px 7px;border-radius:10px;font-size:.75rem">Tidak Ramai</span>';
}
function badge24(v) {
return v === 'ya'
? '<span style="background:#eff6ff;color:#1e40af;padding:1px 7px;border-radius:10px;font-size:.75rem">Ya</span>'
: '<span style="background:#f9fafb;color:#374151;padding:1px 7px;border-radius:10px;font-size:.75rem">Tidak</span>';
}
function badgeO(v) {
return v === 'buka'
? '<span style="background:#d1fae5;color:#065f46;padding:1px 7px;border-radius:10px;font-size:.75rem">Buka</span>'
: '<span style="background:#fee2e2;color:#991b1b;padding:1px 7px;border-radius:10px;font-size:.75rem">Tutup</span>';
}
// ════════════════════════════════════════════════════════════════
// POPUP DETAIL MARKER
// Menampilkan semua kolom termasuk lat, lng, created_at, updated_at
// ════════════════════════════════════════════════════════════════
function buildPopupDetail(s) {
const namaSafe = s.nama_spbu.replace(/'/g, "\\'");
return `<div class="ph">⛽ ${s.nama_spbu}</div>
<div class="pb">
<table class="dt">
<tr><td>Status Ramai</td><td>${badgeR(s.status_ramai)}</td></tr>
<tr><td>Buka 24 Jam</td><td>${badge24(s.buka_24_jam)}</td></tr>
<tr><td>Status</td><td>${badgeO(s.status_operasional)}</td></tr>
<tr><td>Nomor HP</td><td>📞 ${s.nomor_hp}</td></tr>
<tr><td>Latitude</td><td>${parseFloat(s.latitude).toFixed(6)}</td></tr>
<tr><td>Longitude</td><td>${parseFloat(s.longitude).toFixed(6)}</td></tr>
<tr><td>Dibuat</td><td>${s.created_at}</td></tr>
<tr><td>Diperbarui</td><td>${s.updated_at}</td></tr>
</table>
<div class="br">
<button class="btn bp" onclick="bukaEdit(${s.id})">✏️ Edit</button>
<button class="btn bd" onclick="hapus(${s.id},'${namaSafe}')">🗑️ Hapus</button>
</div>
</div>`;
}
// ════════════════════════════════════════════════════════════════
// PENEMPATAN MARKER KE LAYER GROUP
// ════════════════════════════════════════════════════════════════
/**
* addMarkerToMap — membuat marker dan menempatkannya ke layer group yang sesuai
* @param {Object} data - Objek data SPBU dari database
*/
function addMarkerToMap(data) {
// Tentukan layer group berdasarkan nilai buka_24_jam
const targetLayer = getTargetLayer(data);
// Buat marker dengan ikon SVG sesuai warna
const marker = L.marker([data.latitude, data.longitude], {
icon: createMarkerIcon(data)
});
// Pasang popup detail ke marker
marker.bindPopup(buildPopupDetail(data), { maxWidth: 320 });
// Tambahkan marker ke layer group yang tepat (bukan langsung ke map)
// Layer group sudah ditambahkan ke map, jadi marker otomatis tampil
marker.addTo(targetLayer);
// Simpan referensi marker + layer asalnya ke markersMap
markersMap[data.id] = {
marker: marker,
layer: targetLayer // simpan layer asal untuk kebutuhan edit
};
}
/**
* removeMarkerFromMap — hapus marker dari layer group dan dari markersMap
* @param {number|string} id - ID SPBU yang akan dihapus
*/
function removeMarkerFromMap(id) {
if (!markersMap[id]) return;
// Hapus marker dari layer group asalnya
markersMap[id].layer.removeLayer(markersMap[id].marker);
// Hapus entry dari markersMap
delete markersMap[id];
}
/**
* refreshMarkerAfterEdit — perbarui marker setelah data diedit
* Jika buka_24_jam berubah, marker dipindahkan ke layer group yang baru
* @param {Object} data - Data SPBU terbaru dari server
*/
function refreshMarkerAfterEdit(data) {
if (!markersMap[data.id]) return;
const oldEntry = markersMap[data.id];
const newLayer = getTargetLayer(data);
const layerChanged = oldEntry.layer !== newLayer;
if (layerChanged) {
// ── Hapus marker lama dari layer group lama ──────────────
oldEntry.layer.removeLayer(oldEntry.marker);
// ── Buat marker baru dengan data terbaru ─────────────────
const newMarker = L.marker([data.latitude, data.longitude], {
icon: createMarkerIcon(data)
});
newMarker.bindPopup(buildPopupDetail(data), { maxWidth: 320 });
// ── Masukkan ke layer group baru ──────────────────────────
newMarker.addTo(newLayer);
newMarker.openPopup();
// ── Perbarui entry di markersMap ──────────────────────────
markersMap[data.id] = { marker: newMarker, layer: newLayer };
} else {
// Layer tidak berubah — cukup update icon dan konten popup
oldEntry.marker.setIcon(createMarkerIcon(data));
oldEntry.marker.setPopupContent(buildPopupDetail(data));
oldEntry.marker.openPopup();
}
}
// ════════════════════════════════════════════════════════════════
// LOAD DATA — muat semua marker dari database saat halaman dibuka
// ════════════════════════════════════════════════════════════════
/**
* loadData — fetch semua data SPBU dari ambil_data.php
* lalu tempatkan marker ke layer group masing-masing
*/
async function loadData() {
try {
const res = await fetch('ambil_data.php');
const json = await res.json();
if (json.status !== 'success') return;
// Bersihkan semua marker yang ada (jika ada dari load sebelumnya)
Object.keys(markersMap).forEach(id => removeMarkerFromMap(id));
// Tambahkan semua marker ke layer group sesuai buka_24_jam masing-masing
json.data.forEach(spbu => addMarkerToMap(spbu));
} catch (err) {
console.error('loadData error:', err);
}
}
// ════════════════════════════════════════════════════════════════
// KLIK PETA → POPUP FORM TAMBAH DATA (menggunakan L.popup())
// Latitude dan longitude diambil otomatis dari titik klik,
// TIDAK ditampilkan di form — hanya dikirim saat simpan
// ════════════════════════════════════════════════════════════════
map.on('click', function(e) {
const lat = e.latlng.lat.toFixed(7);
const lng = e.latlng.lng.toFixed(7);
// Tutup popup tambah sebelumnya jika ada
if (tempPopup) { map.closePopup(tempPopup); tempPopup = null; }
// HTML form tambah — lat & lng tidak tampil di form, hanya di kotak info
const html = `<div class="ph">📍 Tambah SPBU Baru</div>
<div class="pb">
<div class="coord-box">📌 Lat: <b>${lat}</b> &nbsp;|&nbsp; Lng: <b>${lng}</b></div>
<div class="fg">
<label>Nama SPBU</label>
<input id="f-nm" placeholder="Contoh: SPBU 44.501.12" maxlength="100">
</div>
<div class="fg">
<label>Status Keramaian</label>
<select id="f-rm">
<option value="">-- Pilih --</option>
<option value="ramai">Ramai</option>
<option value="tidak_ramai">Tidak Ramai</option>
</select>
</div>
<div class="fg">
<label>Buka 24 Jam?</label>
<select id="f-24">
<option value="">-- Pilih --</option>
<option value="ya">Ya</option>
<option value="tidak">Tidak</option>
</select>
</div>
<div class="fg">
<label>Status Operasional</label>
<select id="f-op">
<option value="">-- Pilih --</option>
<option value="buka">Buka</option>
<option value="tutup">Tutup</option>
</select>
</div>
<div class="fg">
<label>Nomor HP</label>
<input id="f-hp" placeholder="0812-3456-7890" maxlength="20">
</div>
<div class="br">
<button class="btn bp" onclick="simpan(${lat}, ${lng})">💾 Simpan</button>
<button class="btn bs" onclick="map.closePopup()">✕ Batal</button>
</div>
</div>`;
// Buat popup tambah menggunakan L.popup() dan buka di titik klik
tempPopup = L.popup({ maxWidth: 320 })
.setLatLng(e.latlng)
.setContent(html)
.openOn(map);
});
// ════════════════════════════════════════════════════════════════
// PROSES TAMBAH DATA via fetch()
// ════════════════════════════════════════════════════════════════
/**
* simpan — ambil nilai form, kirim ke tambah.php via fetch + FormData,
* lalu tambahkan marker baru ke layer group yang sesuai
* @param {number} lat - Latitude dari titik klik
* @param {number} lng - Longitude dari titik klik
*/
async function simpan(lat, lng) {
const nm = document.getElementById('f-nm')?.value.trim();
const rm = document.getElementById('f-rm')?.value;
const j24 = document.getElementById('f-24')?.value;
const op = document.getElementById('f-op')?.value;
const hp = document.getElementById('f-hp')?.value.trim();
// Validasi sisi klien sebelum kirim ke server
if (!nm || !rm || !j24 || !op || !hp) {
showToast('⚠️ Semua field wajib diisi!', 'err');
return;
}
// Buat FormData untuk dikirim via fetch POST
const fd = new FormData();
fd.append('nama_spbu', nm);
fd.append('status_ramai', rm);
fd.append('buka_24_jam', j24);
fd.append('status_operasional', op);
fd.append('nomor_hp', hp);
fd.append('latitude', lat); // lat dari klik peta
fd.append('longitude', lng); // lng dari klik peta
showLoading();
try {
// Kirim data ke tambah.php menggunakan fetch POST
const res = await fetch('tambah.php', { method: 'POST', body: fd });
const json = await res.json();
if (json.status === 'success') {
// Tutup popup form tambah
map.closePopup(); tempPopup = null;
// Tambahkan marker baru ke layer group yang sesuai (berdasarkan buka_24_jam)
addMarkerToMap(json.data);
// Pan ke marker baru dan buka popup detail-nya
map.panTo([json.data.latitude, json.data.longitude]);
markersMap[json.data.id].marker.openPopup();
showToast('✅ Data berhasil ditambahkan!', 'ok');
} else {
showToast('❌ ' + json.message, 'err');
}
} catch (err) {
showToast('❌ Gagal menghubungi server', 'err');
} finally {
hideLoading();
}
}
// ════════════════════════════════════════════════════════════════
// BUKA FORM EDIT (dari tombol di popup detail)
// ════════════════════════════════════════════════════════════════
/**
* bukaEdit — fetch data SPBU lalu tampilkan form edit di popup marker
* @param {number} id - ID SPBU yang akan diedit
*/
async function bukaEdit(id) {
showLoading();
try {
// Ambil data terbaru dari server untuk mengisi form edit
const res = await fetch('ambil_data.php');
const json = await res.json();
hideLoading();
const s = json.data.find(x => x.id == id);
if (!s) { showToast('❌ Data tidak ditemukan', 'err'); return; }
// HTML form edit — lat & lng tidak tampil, hanya field yang boleh diubah
const html = `<div class="ph">✏️ Edit Data SPBU</div>
<div class="pb">
<div class="fg">
<label>Nama SPBU</label>
<input id="e-nm" value="${s.nama_spbu.replace(/"/g, '&quot;')}" maxlength="100">
</div>
<div class="fg">
<label>Status Keramaian</label>
<select id="e-rm">
<option value="ramai" ${s.status_ramai === 'ramai' ? 'selected' : ''}>Ramai</option>
<option value="tidak_ramai" ${s.status_ramai === 'tidak_ramai' ? 'selected' : ''}>Tidak Ramai</option>
</select>
</div>
<div class="fg">
<label>Buka 24 Jam?</label>
<select id="e-24">
<option value="ya" ${s.buka_24_jam === 'ya' ? 'selected' : ''}>Ya</option>
<option value="tidak" ${s.buka_24_jam === 'tidak' ? 'selected' : ''}>Tidak</option>
</select>
</div>
<div class="fg">
<label>Status Operasional</label>
<select id="e-op">
<option value="buka" ${s.status_operasional === 'buka' ? 'selected' : ''}>Buka</option>
<option value="tutup" ${s.status_operasional === 'tutup' ? 'selected' : ''}>Tutup</option>
</select>
</div>
<div class="fg">
<label>Nomor HP</label>
<input id="e-hp" value="${s.nomor_hp}" maxlength="20">
</div>
<div class="br">
<button class="btn bg" onclick="simpanEdit(${id})">💾 Simpan</button>
<button class="btn bs" onclick="kembaliDetail(${id})"> Batal</button>
</div>
</div>`;
// Tampilkan form edit di popup marker yang bersangkutan
if (markersMap[id]) {
markersMap[id].marker.setPopupContent(html).openPopup();
}
} catch (err) {
hideLoading();
showToast('❌ Gagal menghubungi server', 'err');
}
}
// ════════════════════════════════════════════════════════════════
// PROSES EDIT DATA via fetch()
// ════════════════════════════════════════════════════════════════
/**
* simpanEdit — kirim perubahan ke edit.php via fetch POST,
* lalu perbarui marker (termasuk pindah layer group jika buka_24_jam berubah)
* @param {number} id - ID SPBU yang sedang diedit
*/
async function simpanEdit(id) {
const nm = document.getElementById('e-nm')?.value.trim();
const rm = document.getElementById('e-rm')?.value;
const j24 = document.getElementById('e-24')?.value;
const op = document.getElementById('e-op')?.value;
const hp = document.getElementById('e-hp')?.value.trim();
// Validasi sisi klien
if (!nm || !rm || !j24 || !op || !hp) {
showToast('⚠️ Semua field wajib diisi!', 'err');
return;
}
// FormData hanya berisi field yang boleh diubah — lat & lng tidak dikirim
const fd = new FormData();
fd.append('id', id);
fd.append('nama_spbu', nm);
fd.append('status_ramai', rm);
fd.append('buka_24_jam', j24);
fd.append('status_operasional', op);
fd.append('nomor_hp', hp);
showLoading();
try {
// Kirim ke edit.php menggunakan fetch POST
const res = await fetch('edit.php', { method: 'POST', body: fd });
const json = await res.json();
if (json.status === 'success') {
// Perbarui marker: hapus dari layer lama, buat baru di layer yang sesuai
// Jika buka_24_jam berubah, marker otomatis berpindah layer group
refreshMarkerAfterEdit(json.data);
showToast('✅ Data berhasil diperbarui!', 'ok');
} else {
showToast('❌ ' + json.message, 'err');
}
} catch (err) {
showToast('❌ Gagal menghubungi server', 'err');
} finally {
hideLoading();
}
}
// ════════════════════════════════════════════════════════════════
// PROSES HAPUS DATA via fetch()
// ════════════════════════════════════════════════════════════════
/**
* hapus — konfirmasi lalu kirim request hapus ke hapus.php,
* setelah sukses hapus marker dari layer group dan markersMap
* @param {number} id - ID SPBU yang akan dihapus
* @param {string} nama - Nama SPBU untuk konfirmasi dialog
*/
async function hapus(id, nama) {
if (!confirm(`Hapus SPBU "${nama}"? Tindakan ini tidak dapat dibatalkan.`)) return;
const fd = new FormData();
fd.append('id', id);
showLoading();
try {
// Kirim ke hapus.php menggunakan fetch POST
const res = await fetch('hapus.php', { method: 'POST', body: fd });
const json = await res.json();
if (json.status === 'success') {
// Hapus marker dari layer group asalnya dan dari markersMap
removeMarkerFromMap(id);
showToast('🗑️ Data berhasil dihapus', 'ok');
} else {
showToast('❌ ' + json.message, 'err');
}
} catch (err) {
showToast('❌ Gagal menghubungi server', 'err');
} finally {
hideLoading();
}
}
// ════════════════════════════════════════════════════════════════
// KEMBALI KE POPUP DETAIL (dari form edit saat batal)
// ════════════════════════════════════════════════════════════════
/**
* kembaliDetail — ambil data terbaru lalu tampilkan kembali popup detail
* @param {number} id - ID SPBU
*/
async function kembaliDetail(id) {
try {
const res = await fetch('ambil_data.php');
const json = await res.json();
const s = json.data.find(x => x.id == id);
if (s && markersMap[id]) {
markersMap[id].marker.setPopupContent(buildPopupDetail(s)).openPopup();
}
} catch (err) {
console.error('kembaliDetail error:', err);
}
}
// ════════════════════════════════════════════════════════════════
// INISIALISASI — muat semua marker saat halaman pertama dibuka
// ════════════════════════════════════════════════════════════════
loadData();
</script>
</body>
</html>
+20
View File
@@ -0,0 +1,20 @@
<?php
/**
* Koneksi database Project 04.
*/
$host = getenv('DB_HOST') ?: 'db';
$dbname = '04_spbudb';
$username = getenv('DB_USER') ?: 'root';
$password = getenv('DB_PASSWORD') ?: 'webgisidolapass';
$koneksi = new mysqli($host, $username, $password, $dbname);
$koneksi->set_charset('utf8mb4');
if ($koneksi->connect_error) {
http_response_code(500);
die(json_encode([
'status' => 'error',
'message' => 'Koneksi database gagal: ' . $koneksi->connect_error
]));
}
?>
+33
View File
@@ -0,0 +1,33 @@
-- ============================================================
-- schema.sql
-- Jalankan query ini di phpMyAdmin atau MySQL CLI Laragon
-- untuk membuat database dan tabel project 04
-- ============================================================
-- Buat database (jika belum ada)
CREATE DATABASE IF NOT EXISTS `04_spbudb`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
-- Gunakan database yang baru dibuat
USE `04_spbudb`;
-- Hapus tabel jika sudah ada (untuk reset bersih)
DROP TABLE IF EXISTS `spbu`;
-- Buat tabel spbu
CREATE TABLE `spbu` (
`id` INT NOT NULL AUTO_INCREMENT,
`nama_spbu` VARCHAR(100) NOT NULL,
`status_ramai` ENUM('ramai','tidak_ramai') NOT NULL,
`buka_24_jam` ENUM('ya','tidak') NOT NULL,
`status_operasional` ENUM('buka','tutup') NOT NULL,
`nomor_hp` VARCHAR(20) NOT NULL,
`latitude` DOUBLE NOT NULL,
`longitude` DOUBLE NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;
+116
View File
@@ -0,0 +1,116 @@
<?php
/**
* tambah.php
* Menerima data POST dari JavaScript (fetch + FormData),
* memvalidasi, lalu menyimpan data SPBU baru ke database.
* Mengembalikan response JSON termasuk data lengkap yang baru disimpan
* agar JavaScript bisa langsung menambahkan marker ke layer group yang tepat.
*/
// ── Sertakan file koneksi database ───────────────────────────────────────────
require_once 'koneksi.php';
// Set header response sebagai JSON
header('Content-Type: application/json');
// ── Hanya menerima method POST ────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan.']);
exit;
}
// ── Ambil & bersihkan input dari form ────────────────────────────────────────
$nama_spbu = trim($_POST['nama_spbu'] ?? '');
$status_ramai = trim($_POST['status_ramai'] ?? '');
$buka_24_jam = trim($_POST['buka_24_jam'] ?? '');
$status_operasional = trim($_POST['status_operasional'] ?? '');
$nomor_hp = trim($_POST['nomor_hp'] ?? '');
$latitude = trim($_POST['latitude'] ?? '');
$longitude = trim($_POST['longitude'] ?? '');
// ── Validasi: semua field wajib terisi ───────────────────────────────────────
if (
empty($nama_spbu) ||
empty($status_ramai) ||
empty($buka_24_jam) ||
empty($status_operasional) ||
empty($nomor_hp) ||
$latitude === '' ||
$longitude === ''
) {
echo json_encode(['status' => 'error', 'message' => 'Semua field wajib diisi.']);
exit;
}
// ── Validasi nilai ENUM (server-side) ────────────────────────────────────────
$valid_ramai = ['ramai', 'tidak_ramai'];
$valid_24jam = ['ya', 'tidak'];
$valid_operasional = ['buka', 'tutup'];
if (!in_array($status_ramai, $valid_ramai)) {
echo json_encode(['status' => 'error', 'message' => 'Nilai status ramai tidak valid.']);
exit;
}
if (!in_array($buka_24_jam, $valid_24jam)) {
echo json_encode(['status' => 'error', 'message' => 'Nilai buka 24 jam tidak valid.']);
exit;
}
if (!in_array($status_operasional, $valid_operasional)) {
echo json_encode(['status' => 'error', 'message' => 'Nilai status operasional tidak valid.']);
exit;
}
// ── Validasi latitude & longitude harus numerik ──────────────────────────────
if (!is_numeric($latitude) || !is_numeric($longitude)) {
echo json_encode(['status' => 'error', 'message' => 'Koordinat tidak valid.']);
exit;
}
// Konversi ke float
$latitude = (float) $latitude;
$longitude = (float) $longitude;
// ── Simpan ke database menggunakan prepared statement ────────────────────────
// Prepared statement mencegah SQL Injection
$stmt = $koneksi->prepare(
"INSERT INTO spbu
(nama_spbu, status_ramai, buka_24_jam, status_operasional, nomor_hp, latitude, longitude)
VALUES (?, ?, ?, ?, ?, ?, ?)"
);
// Bind parameter: s = string, d = double
$stmt->bind_param(
'sssssdd',
$nama_spbu,
$status_ramai,
$buka_24_jam,
$status_operasional,
$nomor_hp,
$latitude,
$longitude
);
// Eksekusi query
if ($stmt->execute()) {
$newId = $koneksi->insert_id;
// Ambil data lengkap termasuk created_at & updated_at dari DB
$res = $koneksi->query("SELECT * FROM spbu WHERE id = $newId");
$newData = $res->fetch_assoc();
$newData['latitude'] = (float) $newData['latitude'];
$newData['longitude'] = (float) $newData['longitude'];
// Kembalikan data lengkap agar JS bisa menentukan layer group yang tepat
echo json_encode([
'status' => 'success',
'message' => 'Data SPBU berhasil ditambahkan.',
'data' => $newData
]);
} else {
echo json_encode(['status' => 'error', 'message' => 'Gagal menyimpan data: ' . $stmt->error]);
}
$stmt->close();
$koneksi->close();
?>