Deploy WebGIS 01-05 dengan Docker
This commit is contained in:
@@ -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 */;
|
||||
@@ -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
@@ -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();
|
||||
?>
|
||||
@@ -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
@@ -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 & 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: '© <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> | 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, '"')}" 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>
|
||||
@@ -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
|
||||
]));
|
||||
}
|
||||
?>
|
||||
@@ -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
@@ -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();
|
||||
?>
|
||||
Reference in New Issue
Block a user