Deploy WebGIS 01-05 dengan Docker
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
-- 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: `01_spbudb`
|
||||
--
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `spbu`
|
||||
--
|
||||
|
||||
CREATE TABLE `spbu` (
|
||||
`id` int NOT NULL,
|
||||
`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 NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_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 IMBON', 'ramai', 'ya', 'buka', '0815497422156', -0.0469843, 109.354544, '2026-06-08 02:48:42', '2026-06-08 02:48:42'),
|
||||
(3, 'SPBU Gertak 1', 'ramai', 'tidak', 'buka', '0815497422156', -0.0471444, 109.3556786, '2026-06-08 02:53:40', '2026-06-08 02:53:40'),
|
||||
(4, 'SPBU IMBON', 'ramai', 'ya', 'tutup', '0815497422156', -0.0472983, 109.3563867, '2026-06-08 02:54:03', '2026-06-08 02:54:03'),
|
||||
(5, 'SPBU IMBON', 'tidak_ramai', 'tidak', 'buka', '0815497422156', -0.0476848, 109.3552065, '2026-06-08 02:54:20', '2026-06-08 02:54:50'),
|
||||
(6, 'SPBU Gertak 2', 'tidak_ramai', 'ya', 'buka', '087894587965', -0.0471113, 109.3553186, '2026-06-08 07:32:27', '2026-06-08 07:32:27'),
|
||||
(7, 'SPBU OSO', 'tidak_ramai', 'tidak', 'buka', '026598745234', -0.0449249, 109.3367475, '2026-06-08 15:38:19', '2026-06-08 15:38:19'),
|
||||
(8, 'SPBU GOLDEN TULIP', 'ramai', 'tidak', 'buka', '0564123', -0.0343809, 109.3319061, '2026-06-08 15:39:22', '2026-06-08 15:39:22');
|
||||
|
||||
--
|
||||
-- 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=9;
|
||||
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
|
||||
* File ini mengambil seluruh data SPBU dari database
|
||||
* dan mengembalikannya dalam format JSON.
|
||||
* Dipanggil oleh JavaScript saat halaman dimuat dan setelah operasi CRUD.
|
||||
*/
|
||||
|
||||
// Sertakan file koneksi database
|
||||
require_once 'koneksi.php';
|
||||
|
||||
// Set header agar browser tahu ini adalah JSON
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// ── Ambil semua data dari tabel spbu, urut dari 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 data ke dalam array ─────────────────────────────────
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
// Konversi latitude & longitude ke tipe 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();
|
||||
?>
|
||||
@@ -0,0 +1,354 @@
|
||||
/* ============================================================
|
||||
style.css — WebGIS SPBU
|
||||
Tampilan sederhana, bersih, dan fungsional
|
||||
============================================================ */
|
||||
|
||||
/* ── Import Google Fonts ──────────────────────────────────── */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap');
|
||||
|
||||
/* ── CSS Variables (tema warna) ───────────────────────────── */
|
||||
:root {
|
||||
--primary: #1a56db;
|
||||
--primary-dk: #1341b0;
|
||||
--danger: #e02424;
|
||||
--danger-dk: #b91c1c;
|
||||
--success: #057a55;
|
||||
--bg: #f0f4f8;
|
||||
--panel: #ffffff;
|
||||
--border: #d1d9e0;
|
||||
--text: #1a202c;
|
||||
--text-muted: #6b7280;
|
||||
--shadow: 0 4px 24px rgba(0,0,0,0.10);
|
||||
--radius: 10px;
|
||||
--font: 'Plus Jakarta Sans', sans-serif;
|
||||
}
|
||||
|
||||
/* ── Reset & Base ─────────────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--font);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
height: 100%; /* ← pastikan ada height: 100% */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Header ───────────────────────────────────────────────── */
|
||||
header {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
padding: 0 24px;
|
||||
height: 58px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2px 10px rgba(26,86,219,0.35);
|
||||
z-index: 1000;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
header h1 .icon { font-size: 1.3rem; }
|
||||
|
||||
.header-hint {
|
||||
font-size: 0.78rem;
|
||||
background: rgba(255,255,255,0.18);
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
border-radius: 20px;
|
||||
padding: 4px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* ── Map Container ────────────────────────────────────────── */
|
||||
#map {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 0; /* ← TAMBAHKAN BARIS INI */
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
/* ── Legend ───────────────────────────────────────────────── */
|
||||
.legend {
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px 14px;
|
||||
box-shadow: var(--shadow);
|
||||
font-size: 0.78rem;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.legend h4 {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 5px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
.dot-orange { background: #f97316; }
|
||||
.dot-green { background: #22c55e; }
|
||||
.dot-red { background: #ef4444; }
|
||||
.dot-blue { background: #3b82f6; }
|
||||
.dot-black { background: #1f2937; }
|
||||
|
||||
/* ── Leaflet Popup Overrides ─────────────────────────────── */
|
||||
.leaflet-popup-content-wrapper {
|
||||
border-radius: var(--radius) !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow) !important;
|
||||
border: 1px solid var(--border);
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 0 !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip-container { margin-top: -1px; }
|
||||
|
||||
/* ── Popup: Header ────────────────────────────────────────── */
|
||||
.popup-header {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
padding: 12px 16px;
|
||||
font-weight: 700;
|
||||
font-size: 0.92rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ── Popup: Body ──────────────────────────────────────────── */
|
||||
.popup-body {
|
||||
padding: 14px 16px;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
/* ── Popup: Info Table (detail view) ─────────────────────── */
|
||||
.info-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.83rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.info-table tr td {
|
||||
padding: 5px 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.info-table tr td:first-child {
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
width: 110px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-table tr td:last-child {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ── Badge status ─────────────────────────────────────────── */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge-ramai { background: #fef3c7; color: #92400e; }
|
||||
.badge-tidakramai{ background: #ecfdf5; color: #065f46; }
|
||||
.badge-ya { background: #eff6ff; color: #1e40af; }
|
||||
.badge-tidak { background: #f9fafb; color: #374151; }
|
||||
.badge-buka { background: #d1fae5; color: #065f46; }
|
||||
.badge-tutup { background: #fee2e2; color: #991b1b; }
|
||||
|
||||
/* ── Form Controls ────────────────────────────────────────── */
|
||||
.form-group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 0.79rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: 7px;
|
||||
font-family: var(--font);
|
||||
font-size: 0.85rem;
|
||||
color: var(--text);
|
||||
background: #f8fafc;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(26,86,219,0.12);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ── Buttons ──────────────────────────────────────────────── */
|
||||
.btn-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 7px;
|
||||
font-family: var(--font);
|
||||
font-size: 0.83rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, transform 0.1s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.btn:active { transform: scale(0.97); }
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover { background: var(--primary-dk); }
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-danger:hover { background: var(--danger-dk); }
|
||||
|
||||
.btn-secondary {
|
||||
background: #f1f5f9;
|
||||
color: var(--text);
|
||||
border: 1.5px solid var(--border);
|
||||
}
|
||||
.btn-secondary:hover { background: #e2e8f0; }
|
||||
|
||||
.btn-success {
|
||||
background: var(--success);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-success:hover { background: #046c4e; }
|
||||
|
||||
/* ── Divider ──────────────────────────────────────────────── */
|
||||
.divider {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* ── Toast Notification ───────────────────────────────────── */
|
||||
#toast {
|
||||
position: fixed;
|
||||
bottom: 28px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(80px);
|
||||
background: #1f2937;
|
||||
color: #fff;
|
||||
padding: 10px 22px;
|
||||
border-radius: 30px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
#toast.show {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
#toast.toast-success { background: #057a55; }
|
||||
#toast.toast-error { background: #e02424; }
|
||||
|
||||
/* ── Loading Overlay ──────────────────────────────────────── */
|
||||
#loading {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(255,255,255,0.55);
|
||||
z-index: 8000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
#loading.show { display: flex; }
|
||||
|
||||
.spinner {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border: 4px solid var(--border);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ── Koordinat display ────────────────────────────────────── */
|
||||
.coord-display {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 7px;
|
||||
padding: 7px 10px;
|
||||
font-size: 0.78rem;
|
||||
color: #0369a1;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.coord-display span { font-weight: 600; }
|
||||
@@ -0,0 +1,590 @@
|
||||
/**
|
||||
* script.js — WebGIS SPBU
|
||||
* File ini berisi seluruh logika JavaScript:
|
||||
* - Inisialisasi peta Leaflet
|
||||
* - Klik peta → popup form tambah data
|
||||
* - Ambil data dari database & tampilkan marker
|
||||
* - Tambah, edit, hapus data via AJAX/fetch
|
||||
* - Warna marker sesuai kondisi SPBU
|
||||
*/
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
1. VARIABEL GLOBAL
|
||||
══════════════════════════════════════════════════════════ */
|
||||
|
||||
// Objek yang menyimpan semua marker di peta, dengan key = id SPBU
|
||||
const markersMap = {};
|
||||
|
||||
// Referensi popup sementara saat user klik peta (sebelum data disimpan)
|
||||
let tempPopup = null;
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
2. INISIALISASI PETA LEAFLET
|
||||
══════════════════════════════════════════════════════════ */
|
||||
|
||||
// Buat instance peta, pusatkan ke Informatika
|
||||
const informatikaUntan = [-0.05566422376778593, 109.34854622732368];
|
||||
const map = L.map('map').setView(informatikaUntan, 17);
|
||||
|
||||
// Tambahkan 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);
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
3. LEGEND PETA
|
||||
══════════════════════════════════════════════════════════ */
|
||||
|
||||
const legend = L.control({ position: 'bottomright' });
|
||||
legend.onAdd = function () {
|
||||
const div = L.DomUtil.create('div', 'legend');
|
||||
div.innerHTML = `
|
||||
<h4>🗺️ Keterangan Marker</h4>
|
||||
<div class="legend-item"><div class="legend-dot dot-orange"></div><span>Ramai · 24 Jam · Buka</span></div>
|
||||
<div class="legend-item"><div class="legend-dot dot-green"></div><span>Tidak Ramai · 24 Jam · Buka</span></div>
|
||||
<div class="legend-item"><div class="legend-dot dot-red"></div><span>Ramai · Tidak 24 Jam · Buka</span></div>
|
||||
<div class="legend-item"><div class="legend-dot dot-blue"></div><span>Tidak Ramai · Tidak 24 Jam · Buka</span></div>
|
||||
<div class="legend-item"><div class="legend-dot dot-black"></div><span>Tutup (apapun kondisinya)</span></div>
|
||||
`;
|
||||
return div;
|
||||
};
|
||||
legend.addTo(map);
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
4. FUNGSI UTILITAS
|
||||
══════════════════════════════════════════════════════════ */
|
||||
|
||||
/**
|
||||
* Tampilkan toast notification.
|
||||
* @param {string} pesan - Teks yang ditampilkan
|
||||
* @param {string} tipe - 'success' | 'error'
|
||||
*/
|
||||
function tampilkanToast(pesan, tipe = 'success') {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = pesan;
|
||||
toast.className = `show toast-${tipe}`;
|
||||
setTimeout(() => { toast.className = ''; }, 3000);
|
||||
}
|
||||
|
||||
/** Tampilkan loading spinner */
|
||||
function showLoading() {
|
||||
document.getElementById('loading').classList.add('show');
|
||||
}
|
||||
|
||||
/** Sembunyikan loading spinner */
|
||||
function hideLoading() {
|
||||
document.getElementById('loading').classList.remove('show');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan warna marker berdasarkan kondisi SPBU.
|
||||
* Aturan:
|
||||
* - tutup → hitam
|
||||
* - ramai + 24jam + buka → orange
|
||||
* - tidak_ramai + 24jam + buka → hijau
|
||||
* - ramai + tidak 24jam + buka → merah
|
||||
* - tidak_ramai + tidak 24jam + buka → biru
|
||||
*/
|
||||
function getWarnaMarker(spbu) {
|
||||
if (spbu.status_operasional === 'tutup') return 'black';
|
||||
if (spbu.status_ramai === 'ramai' && spbu.buka_24_jam === 'ya') return 'orange';
|
||||
if (spbu.status_ramai === 'tidak_ramai' && spbu.buka_24_jam === 'ya') return 'green';
|
||||
if (spbu.status_ramai === 'ramai' && spbu.buka_24_jam === 'tidak') return 'red';
|
||||
if (spbu.status_ramai === 'tidak_ramai' && spbu.buka_24_jam === 'tidak') return 'blue';
|
||||
return 'gray'; // fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Buat ikon marker berwarna menggunakan SVG inline.
|
||||
* @param {string} warna - Nama warna CSS
|
||||
*/
|
||||
function buatIkon(warna) {
|
||||
// Palet warna hex
|
||||
const palet = {
|
||||
orange: '#f97316', green: '#22c55e', red: '#ef4444',
|
||||
blue: '#3b82f6', black: '#1f2937', gray: '#9ca3af'
|
||||
};
|
||||
const hex = palet[warna] || '#9ca3af';
|
||||
|
||||
// SVG pin marker
|
||||
const svg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 36" width="28" height="42">
|
||||
<path d="M12 0C5.373 0 0 5.373 0 12c0 9 12 24 12 24s12-15 12-24C24 5.373 18.627 0 12 0z"
|
||||
fill="${hex}" 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: '', // Hapus class default Leaflet
|
||||
iconSize: [28, 42],
|
||||
iconAnchor: [14, 42], // Titik anchor di ujung bawah pin
|
||||
popupAnchor: [0, -42], // Popup muncul di atas marker
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format label yang lebih ramah untuk ditampilkan.
|
||||
*/
|
||||
function labelRamai(val) { return val === 'ramai' ? 'Ramai' : 'Tidak Ramai'; }
|
||||
function labelBuka24(val) { return val === 'ya' ? 'Ya' : 'Tidak'; }
|
||||
function labelOperasional(val) { return val === 'buka' ? 'Buka' : 'Tutup'; }
|
||||
|
||||
function badgeRamai(val) {
|
||||
return val === 'ramai'
|
||||
? `<span class="badge badge-ramai">Ramai</span>`
|
||||
: `<span class="badge badge-tidakramai">Tidak Ramai</span>`;
|
||||
}
|
||||
function badgeBuka24(val) {
|
||||
return val === 'ya'
|
||||
? `<span class="badge badge-ya">Ya (24 Jam)</span>`
|
||||
: `<span class="badge badge-tidak">Tidak</span>`;
|
||||
}
|
||||
function badgeOperasional(val) {
|
||||
return val === 'buka'
|
||||
? `<span class="badge badge-buka">Buka</span>`
|
||||
: `<span class="badge badge-tutup">Tutup</span>`;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
5. BUAT KONTEN POPUP DETAIL DATA SPBU
|
||||
══════════════════════════════════════════════════════════ */
|
||||
|
||||
/**
|
||||
* Menghasilkan HTML untuk popup detail SPBU.
|
||||
* Ditampilkan ketika marker di-klik.
|
||||
*/
|
||||
function buatPopupDetail(spbu) {
|
||||
return `
|
||||
<div class="popup-header">
|
||||
⛽ ${spbu.nama_spbu}
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<td>Status Ramai</td>
|
||||
<td>${badgeRamai(spbu.status_ramai)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Buka 24 Jam</td>
|
||||
<td>${badgeBuka24(spbu.buka_24_jam)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>${badgeOperasional(spbu.status_operasional)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Nomor HP</td>
|
||||
<td>📞 ${spbu.nomor_hp}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Latitude</td>
|
||||
<td>${parseFloat(spbu.latitude).toFixed(6)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Longitude</td>
|
||||
<td>${parseFloat(spbu.longitude).toFixed(6)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr class="divider">
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-primary" onclick="bukaFormEdit(${spbu.id})">✏️ Edit</button>
|
||||
<button class="btn btn-danger" onclick="hapusData(${spbu.id}, '${spbu.nama_spbu.replace(/'/g, "\\'")}')">🗑️ Hapus</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
6. AMBIL DATA DARI DATABASE & TAMPILKAN MARKER
|
||||
══════════════════════════════════════════════════════════ */
|
||||
|
||||
/**
|
||||
* Ambil semua data SPBU dari server lalu tampilkan sebagai marker di peta.
|
||||
*/
|
||||
async function muatSemuaMarker() {
|
||||
try {
|
||||
const res = await fetch('ambil_data.php');
|
||||
const json = await res.json();
|
||||
|
||||
if (json.status !== 'success') {
|
||||
console.error('Gagal ambil data:', json.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Hapus marker lama yang sudah ada di peta
|
||||
Object.values(markersMap).forEach(m => map.removeLayer(m));
|
||||
// Kosongkan objek referensi marker
|
||||
for (const key in markersMap) delete markersMap[key];
|
||||
|
||||
// Buat marker baru untuk setiap SPBU
|
||||
json.data.forEach(spbu => {
|
||||
tambahMarker(spbu);
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error fetch ambil_data:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tambahkan satu marker ke peta dan daftarkan ke markersMap.
|
||||
* @param {object} spbu - Data SPBU dari database
|
||||
*/
|
||||
function tambahMarker(spbu) {
|
||||
const warna = getWarnaMarker(spbu);
|
||||
const ikon = buatIkon(warna);
|
||||
|
||||
const marker = L.marker([spbu.latitude, spbu.longitude], { icon: ikon })
|
||||
.addTo(map)
|
||||
.bindPopup(buatPopupDetail(spbu), { maxWidth: 320 });
|
||||
|
||||
// Simpan referensi marker
|
||||
markersMap[spbu.id] = marker;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
7. KLIK PETA → FORM TAMBAH DATA SPBU
|
||||
══════════════════════════════════════════════════════════ */
|
||||
|
||||
// Event: saat user klik di peta
|
||||
map.on('click', function (e) {
|
||||
const lat = e.latlng.lat.toFixed(7);
|
||||
const lng = e.latlng.lng.toFixed(7);
|
||||
|
||||
// Tutup popup sementara yang mungkin masih terbuka
|
||||
if (tempPopup) {
|
||||
map.closePopup(tempPopup);
|
||||
tempPopup = null;
|
||||
}
|
||||
|
||||
// Konten form tambah data (lat & lng disembunyikan dari user)
|
||||
const formHtml = `
|
||||
<div class="popup-header">📍 Tambah SPBU Baru</div>
|
||||
<div class="popup-body">
|
||||
<div class="coord-display">
|
||||
📌 Koordinat yang akan disimpan:
|
||||
<span>Lat: ${lat}</span>
|
||||
<span>Lng: ${lng}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Nama SPBU</label>
|
||||
<input type="text" id="f-nama" placeholder="Contoh: SPBU 44.501.12" maxlength="100">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Status Keramaian</label>
|
||||
<select id="f-ramai">
|
||||
<option value="">-- Pilih Status --</option>
|
||||
<option value="ramai">Ramai</option>
|
||||
<option value="tidak_ramai">Tidak Ramai</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Buka 24 Jam?</label>
|
||||
<select id="f-24jam">
|
||||
<option value="">-- Pilih --</option>
|
||||
<option value="ya">Ya</option>
|
||||
<option value="tidak">Tidak</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Status Operasional</label>
|
||||
<select id="f-operasional">
|
||||
<option value="">-- Pilih Status --</option>
|
||||
<option value="buka">Buka</option>
|
||||
<option value="tutup">Tutup</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Nomor HP</label>
|
||||
<input type="text" id="f-hp" placeholder="Contoh: 0812-3456-7890" maxlength="20">
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-primary" onclick="simpanData(${lat}, ${lng})">💾 Simpan</button>
|
||||
<button class="btn btn-secondary" onclick="map.closePopup()">✕ Batal</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Buka popup di lokasi yang diklik
|
||||
tempPopup = L.popup({ maxWidth: 340 })
|
||||
.setLatLng(e.latlng)
|
||||
.setContent(formHtml)
|
||||
.openOn(map);
|
||||
});
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
8. SIMPAN DATA SPBU BARU (TAMBAH)
|
||||
══════════════════════════════════════════════════════════ */
|
||||
|
||||
/**
|
||||
* Kirim data form tambah ke server via fetch (AJAX).
|
||||
* Dipanggil oleh tombol "Simpan" di popup form tambah.
|
||||
* @param {number} lat - Latitude dari klik peta
|
||||
* @param {number} lng - Longitude dari klik peta
|
||||
*/
|
||||
async function simpanData(lat, lng) {
|
||||
// Ambil nilai dari form
|
||||
const nama = document.getElementById('f-nama')?.value.trim();
|
||||
const ramai = document.getElementById('f-ramai')?.value;
|
||||
const jam24 = document.getElementById('f-24jam')?.value;
|
||||
const operasional = document.getElementById('f-operasional')?.value;
|
||||
const hp = document.getElementById('f-hp')?.value.trim();
|
||||
|
||||
// Validasi input tidak boleh kosong
|
||||
if (!nama || !ramai || !jam24 || !operasional || !hp) {
|
||||
tampilkanToast('⚠️ Semua field wajib diisi!', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Siapkan data untuk dikirim
|
||||
const formData = new FormData();
|
||||
formData.append('nama_spbu', nama);
|
||||
formData.append('status_ramai', ramai);
|
||||
formData.append('buka_24_jam', jam24);
|
||||
formData.append('status_operasional', operasional);
|
||||
formData.append('nomor_hp', hp);
|
||||
formData.append('latitude', lat);
|
||||
formData.append('longitude', lng);
|
||||
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const res = await fetch('tambah.php', { method: 'POST', body: formData });
|
||||
const json = await res.json();
|
||||
|
||||
if (json.status === 'success') {
|
||||
// Tutup popup form
|
||||
map.closePopup();
|
||||
tempPopup = null;
|
||||
|
||||
// Langsung tambah marker baru tanpa perlu refresh halaman
|
||||
tambahMarker(json.data);
|
||||
|
||||
// Geser peta ke lokasi SPBU baru dan buka popupnya
|
||||
map.panTo([json.data.latitude, json.data.longitude]);
|
||||
markersMap[json.data.id].openPopup();
|
||||
|
||||
tampilkanToast('✅ Data SPBU berhasil ditambahkan!', 'success');
|
||||
} else {
|
||||
tampilkanToast('❌ ' + json.message, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
tampilkanToast('❌ Gagal terhubung ke server.', 'error');
|
||||
console.error(err);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
9. BUKA FORM EDIT DATA SPBU
|
||||
══════════════════════════════════════════════════════════ */
|
||||
|
||||
/**
|
||||
* Ganti isi popup marker dengan form edit.
|
||||
* Latitude dan longitude TIDAK ditampilkan dan TIDAK bisa diedit.
|
||||
* @param {number} id - ID SPBU yang akan diedit
|
||||
*/
|
||||
async function bukaFormEdit(id) {
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
// Ambil data terbaru dari server
|
||||
const res = await fetch('ambil_data.php');
|
||||
const json = await res.json();
|
||||
hideLoading();
|
||||
|
||||
if (json.status !== 'success') {
|
||||
tampilkanToast('❌ Gagal mengambil data.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Cari data SPBU yang sesuai ID
|
||||
const spbu = json.data.find(s => s.id == id);
|
||||
if (!spbu) {
|
||||
tampilkanToast('❌ Data tidak ditemukan.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Buat konten form edit (lat & lng disembunyikan)
|
||||
const formEditHtml = `
|
||||
<div class="popup-header">✏️ Edit Data SPBU</div>
|
||||
<div class="popup-body">
|
||||
<div class="form-group">
|
||||
<label>Nama SPBU</label>
|
||||
<input type="text" id="e-nama" value="${spbu.nama_spbu.replace(/"/g, '"')}" maxlength="100">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Status Keramaian</label>
|
||||
<select id="e-ramai">
|
||||
<option value="ramai" ${spbu.status_ramai === 'ramai' ? 'selected' : ''}>Ramai</option>
|
||||
<option value="tidak_ramai" ${spbu.status_ramai === 'tidak_ramai' ? 'selected' : ''}>Tidak Ramai</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Buka 24 Jam?</label>
|
||||
<select id="e-24jam">
|
||||
<option value="ya" ${spbu.buka_24_jam === 'ya' ? 'selected' : ''}>Ya</option>
|
||||
<option value="tidak" ${spbu.buka_24_jam === 'tidak' ? 'selected' : ''}>Tidak</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Status Operasional</label>
|
||||
<select id="e-operasional">
|
||||
<option value="buka" ${spbu.status_operasional === 'buka' ? 'selected' : ''}>Buka</option>
|
||||
<option value="tutup" ${spbu.status_operasional === 'tutup' ? 'selected' : ''}>Tutup</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Nomor HP</label>
|
||||
<input type="text" id="e-hp" value="${spbu.nomor_hp}" maxlength="20">
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-success" onclick="simpanEdit(${id})">💾 Simpan</button>
|
||||
<button class="btn btn-secondary" onclick="tampilkanDetailDariId(${id})">✕ Batal</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Update isi popup yang sedang terbuka
|
||||
markersMap[id].setPopupContent(formEditHtml).openPopup();
|
||||
|
||||
} catch (err) {
|
||||
hideLoading();
|
||||
tampilkanToast('❌ Gagal terhubung ke server.', 'error');
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
10. SIMPAN PERUBAHAN EDIT DATA SPBU
|
||||
══════════════════════════════════════════════════════════ */
|
||||
|
||||
/**
|
||||
* Kirim data form edit ke server.
|
||||
* Latitude & longitude tidak dikirim → tidak berubah di database.
|
||||
* @param {number} id - ID SPBU yang diedit
|
||||
*/
|
||||
async function simpanEdit(id) {
|
||||
const nama = document.getElementById('e-nama')?.value.trim();
|
||||
const ramai = document.getElementById('e-ramai')?.value;
|
||||
const jam24 = document.getElementById('e-24jam')?.value;
|
||||
const operasional = document.getElementById('e-operasional')?.value;
|
||||
const hp = document.getElementById('e-hp')?.value.trim();
|
||||
|
||||
if (!nama || !ramai || !jam24 || !operasional || !hp) {
|
||||
tampilkanToast('⚠️ Semua field wajib diisi!', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
formData.append('nama_spbu', nama);
|
||||
formData.append('status_ramai', ramai);
|
||||
formData.append('buka_24_jam', jam24);
|
||||
formData.append('status_operasional', operasional);
|
||||
formData.append('nomor_hp', hp);
|
||||
// ⚠️ latitude & longitude sengaja TIDAK dikirim
|
||||
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const res = await fetch('edit.php', { method: 'POST', body: formData });
|
||||
const json = await res.json();
|
||||
|
||||
if (json.status === 'success') {
|
||||
const spbuBaru = json.data;
|
||||
|
||||
// Perbarui ikon marker sesuai kondisi baru
|
||||
const warnaBar = getWarnaMarker(spbuBaru);
|
||||
markersMap[id].setIcon(buatIkon(warnaBar));
|
||||
|
||||
// Perbarui isi popup dengan data terbaru
|
||||
markersMap[id].setPopupContent(buatPopupDetail(spbuBaru));
|
||||
markersMap[id].openPopup();
|
||||
|
||||
tampilkanToast('✅ Data berhasil diperbarui!', 'success');
|
||||
} else {
|
||||
tampilkanToast('❌ ' + json.message, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
tampilkanToast('❌ Gagal terhubung ke server.', 'error');
|
||||
console.error(err);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
11. HAPUS DATA SPBU
|
||||
══════════════════════════════════════════════════════════ */
|
||||
|
||||
/**
|
||||
* Hapus data SPBU dari database dan marker dari peta.
|
||||
* @param {number} id - ID SPBU yang dihapus
|
||||
* @param {string} namaSpbu - Nama SPBU untuk konfirmasi
|
||||
*/
|
||||
async function hapusData(id, namaSpbu) {
|
||||
// Konfirmasi sebelum menghapus
|
||||
if (!confirm(`Yakin ingin menghapus SPBU "${namaSpbu}"?\nTindakan ini tidak dapat dibatalkan.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const res = await fetch('hapus.php', { method: 'POST', body: formData });
|
||||
const json = await res.json();
|
||||
|
||||
if (json.status === 'success') {
|
||||
// Hapus marker dari peta
|
||||
if (markersMap[id]) {
|
||||
map.removeLayer(markersMap[id]);
|
||||
delete markersMap[id];
|
||||
}
|
||||
tampilkanToast('🗑️ Data SPBU berhasil dihapus.', 'success');
|
||||
} else {
|
||||
tampilkanToast('❌ ' + json.message, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
tampilkanToast('❌ Gagal terhubung ke server.', 'error');
|
||||
console.error(err);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
12. HELPER: Tampilkan detail dari ID (tombol Batal di form edit)
|
||||
══════════════════════════════════════════════════════════ */
|
||||
|
||||
async function tampilkanDetailDariId(id) {
|
||||
try {
|
||||
const res = await fetch('ambil_data.php');
|
||||
const json = await res.json();
|
||||
const spbu = json.data.find(s => s.id == id);
|
||||
if (spbu && markersMap[id]) {
|
||||
markersMap[id].setPopupContent(buatPopupDetail(spbu)).openPopup();
|
||||
}
|
||||
} catch (err) { console.error(err); }
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
13. INISIALISASI: Muat semua marker saat halaman dibuka
|
||||
══════════════════════════════════════════════════════════ */
|
||||
|
||||
// Panggil fungsi ini sekali saat halaman selesai dimuat
|
||||
muatSemuaMarker();
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/**
|
||||
* edit.php
|
||||
* File ini menerima data POST dari JavaScript (AJAX/fetch),
|
||||
* lalu memperbarui data SPBU yang sudah ada di database.
|
||||
* PENTING: Latitude dan longitude TIDAK diubah saat edit.
|
||||
*/
|
||||
|
||||
// Sertakan file koneksi database
|
||||
require_once 'koneksi.php';
|
||||
|
||||
// Hanya menerima request dengan method POST
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Ambil & bersihkan data dari form ──────────────────────────────────────────
|
||||
$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 ada dan valid ─────────────────────────────────────────
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Validasi: pastikan tidak ada field yang kosong ────────────────────────────
|
||||
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 ───────────────────────────────────────────────────────
|
||||
$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 (latitude & longitude TIDAK diubah) ───────────────
|
||||
$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) {
|
||||
// Ambil data terbaru setelah di-update untuk dikembalikan ke JavaScript
|
||||
$result = $koneksi->query("SELECT * FROM spbu WHERE id = $id");
|
||||
$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 atau tidak ada perubahan.']);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Gagal memperbarui data: ' . $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$koneksi->close();
|
||||
?>
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* hapus.php
|
||||
* File ini menerima ID dari JavaScript (AJAX/fetch),
|
||||
* lalu menghapus data SPBU yang sesuai dari database.
|
||||
*/
|
||||
|
||||
// Sertakan file koneksi database
|
||||
require_once 'koneksi.php';
|
||||
|
||||
// Hanya menerima request dengan 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 ──────────────────────────────────────────────────
|
||||
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) {
|
||||
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();
|
||||
?>
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS SPBU</title>
|
||||
|
||||
<!-- Leaflet CSS - tanpa integrity -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
|
||||
<style>
|
||||
/* Reset total */
|
||||
* { 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 - ini kuncinya */
|
||||
#map {
|
||||
width: 100%;
|
||||
height: calc(100vh - 50px);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Toast */
|
||||
#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 */
|
||||
#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; }
|
||||
|
||||
.ph {
|
||||
background: #1a56db;
|
||||
color: white;
|
||||
padding: 10px 14px;
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.pb { padding: 12px 14px; background: white; }
|
||||
|
||||
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: 105px; }
|
||||
table.dt td:last-child { font-weight: 600; }
|
||||
|
||||
.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; }
|
||||
|
||||
.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; }
|
||||
|
||||
.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 — Manajemen Stasiun Bahan Bakar</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 -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<script>
|
||||
// ── Cek Leaflet termuat ──────────────────────────────────
|
||||
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 ──────────────────────────────────────
|
||||
const markersMap = {};
|
||||
let tempPopup = null;
|
||||
|
||||
// ── Inisialisasi peta ────────────────────────────────────
|
||||
const map = L.map('map').setView([-0.05566422376778593, 109.34854622732368], 17);
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
|
||||
// ── Legend ───────────────────────────────────────────────
|
||||
const leg = L.control({ position: 'bottomright' });
|
||||
leg.onAdd = () => {
|
||||
const d = L.DomUtil.create('div', 'legend');
|
||||
d.innerHTML = `<b>🗺️ Keterangan</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</div>`;
|
||||
return d;
|
||||
};
|
||||
leg.addTo(map);
|
||||
|
||||
// ── Utilitas ─────────────────────────────────────────────
|
||||
function toast(msg, type='ok') {
|
||||
const t = document.getElementById('toast');
|
||||
t.textContent = msg;
|
||||
t.className = 'show ' + type;
|
||||
setTimeout(() => t.className = '', 3000);
|
||||
}
|
||||
function showLoad() { document.getElementById('loading').classList.add('show'); }
|
||||
function hideLoad() { document.getElementById('loading').classList.remove('show'); }
|
||||
|
||||
function getColor(s) {
|
||||
if (s.status_operasional === 'tutup') return '#1f2937';
|
||||
if (s.status_ramai === 'ramai' && s.buka_24_jam === 'ya') return '#f97316';
|
||||
if (s.status_ramai === 'tidak_ramai' && s.buka_24_jam === 'ya') return '#22c55e';
|
||||
if (s.status_ramai === 'ramai' && s.buka_24_jam === 'tidak') return '#ef4444';
|
||||
if (s.status_ramai === 'tidak_ramai' && s.buka_24_jam === 'tidak') return '#3b82f6';
|
||||
return '#9ca3af';
|
||||
}
|
||||
|
||||
function makeIcon(spbu) {
|
||||
const c = getColor(spbu);
|
||||
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] });
|
||||
}
|
||||
|
||||
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>'; }
|
||||
|
||||
function popupDetail(s) {
|
||||
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>
|
||||
</table>
|
||||
<div class="br">
|
||||
<button class="btn bp" onclick="bukaEdit(${s.id})">✏️ Edit</button>
|
||||
<button class="btn bd" onclick="hapus(${s.id},'${s.nama_spbu.replace(/'/g,"\\'")}')">🗑️ Hapus</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Muat semua marker dari DB ────────────────────────────
|
||||
async function muatMarker() {
|
||||
try {
|
||||
const r = await fetch('ambil_data.php');
|
||||
const j = await r.json();
|
||||
if (j.status !== 'success') return;
|
||||
Object.values(markersMap).forEach(m => map.removeLayer(m));
|
||||
for (const k in markersMap) delete markersMap[k];
|
||||
j.data.forEach(addMarker);
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
function addMarker(s) {
|
||||
const m = L.marker([s.latitude, s.longitude], { icon: makeIcon(s) })
|
||||
.addTo(map)
|
||||
.bindPopup(popupDetail(s), { maxWidth: 310 });
|
||||
markersMap[s.id] = m;
|
||||
}
|
||||
|
||||
// ── Klik peta → form tambah ──────────────────────────────
|
||||
map.on('click', function(e) {
|
||||
const lat = e.latlng.lat.toFixed(7);
|
||||
const lng = e.latlng.lng.toFixed(7);
|
||||
if (tempPopup) { map.closePopup(tempPopup); tempPopup = null; }
|
||||
|
||||
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>`;
|
||||
|
||||
tempPopup = L.popup({ maxWidth: 320 }).setLatLng(e.latlng).setContent(html).openOn(map);
|
||||
});
|
||||
|
||||
// ── Simpan data baru ─────────────────────────────────────
|
||||
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();
|
||||
if (!nm||!rm||!j24||!op||!hp) { toast('⚠️ Semua field wajib diisi!','err'); return; }
|
||||
|
||||
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); fd.append('longitude',lng);
|
||||
|
||||
showLoad();
|
||||
try {
|
||||
const r = await fetch('tambah.php',{method:'POST',body:fd});
|
||||
const j = await r.json();
|
||||
if (j.status==='success') {
|
||||
map.closePopup(); tempPopup=null;
|
||||
addMarker(j.data);
|
||||
map.panTo([j.data.latitude,j.data.longitude]);
|
||||
markersMap[j.data.id].openPopup();
|
||||
toast('✅ Data berhasil ditambahkan!','ok');
|
||||
} else { toast('❌ '+j.message,'err'); }
|
||||
} catch(e) { toast('❌ Gagal ke server','err'); }
|
||||
finally { hideLoad(); }
|
||||
}
|
||||
|
||||
// ── Buka form edit ───────────────────────────────────────
|
||||
async function bukaEdit(id) {
|
||||
showLoad();
|
||||
try {
|
||||
const r = await fetch('ambil_data.php');
|
||||
const j = await r.json();
|
||||
hideLoad();
|
||||
const s = j.data.find(x => x.id == id);
|
||||
if (!s) { toast('❌ Data tidak ditemukan','err'); return; }
|
||||
|
||||
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>`;
|
||||
|
||||
markersMap[id].setPopupContent(html).openPopup();
|
||||
} catch(e) { hideLoad(); toast('❌ Gagal ke server','err'); }
|
||||
}
|
||||
|
||||
// ── Simpan edit ──────────────────────────────────────────
|
||||
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();
|
||||
if (!nm||!rm||!j24||!op||!hp) { toast('⚠️ Semua field wajib diisi!','err'); return; }
|
||||
|
||||
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);
|
||||
|
||||
showLoad();
|
||||
try {
|
||||
const r = await fetch('edit.php',{method:'POST',body:fd});
|
||||
const j = await r.json();
|
||||
if (j.status==='success') {
|
||||
markersMap[id].setIcon(makeIcon(j.data));
|
||||
markersMap[id].setPopupContent(popupDetail(j.data)).openPopup();
|
||||
toast('✅ Data berhasil diperbarui!','ok');
|
||||
} else { toast('❌ '+j.message,'err'); }
|
||||
} catch(e) { toast('❌ Gagal ke server','err'); }
|
||||
finally { hideLoad(); }
|
||||
}
|
||||
|
||||
// ── Hapus data ───────────────────────────────────────────
|
||||
async function hapus(id, nama) {
|
||||
if (!confirm(`Hapus SPBU "${nama}"?`)) return;
|
||||
const fd = new FormData(); fd.append('id',id);
|
||||
showLoad();
|
||||
try {
|
||||
const r = await fetch('hapus.php',{method:'POST',body:fd});
|
||||
const j = await r.json();
|
||||
if (j.status==='success') {
|
||||
map.removeLayer(markersMap[id]); delete markersMap[id];
|
||||
toast('🗑️ Data berhasil dihapus','ok');
|
||||
} else { toast('❌ '+j.message,'err'); }
|
||||
} catch(e) { toast('❌ Gagal ke server','err'); }
|
||||
finally { hideLoad(); }
|
||||
}
|
||||
|
||||
// ── Kembali ke detail dari form edit ────────────────────
|
||||
async function kembaliDetail(id) {
|
||||
try {
|
||||
const r = await fetch('ambil_data.php');
|
||||
const j = await r.json();
|
||||
const s = j.data.find(x => x.id == id);
|
||||
if (s && markersMap[id]) markersMap[id].setPopupContent(popupDetail(s)).openPopup();
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// ── Muat marker saat halaman dibuka ─────────────────────
|
||||
muatMarker();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* Koneksi database Project 01.
|
||||
* Docker/Coolify memakai host db, user root, password webgisidolapass.
|
||||
*/
|
||||
$host = getenv('DB_HOST') ?: 'db';
|
||||
$dbname = '01_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
|
||||
]));
|
||||
}
|
||||
?>
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
/**
|
||||
* tambah.php
|
||||
* File ini menerima data POST dari JavaScript (AJAX/fetch),
|
||||
* lalu menyimpan data SPBU baru ke dalam database.
|
||||
* Mengembalikan response dalam format JSON.
|
||||
*/
|
||||
|
||||
// Sertakan file koneksi database
|
||||
require_once 'koneksi.php';
|
||||
|
||||
// Hanya menerima request dengan method POST
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Ambil & bersihkan data 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: pastikan tidak ada field yang kosong ────────────────────────────
|
||||
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 ───────────────────────────────────────────────────────
|
||||
$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;
|
||||
}
|
||||
|
||||
// ── Simpan ke database menggunakan 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/float
|
||||
$stmt->bind_param(
|
||||
'sssssdd',
|
||||
$nama_spbu,
|
||||
$status_ramai,
|
||||
$buka_24_jam,
|
||||
$status_operasional,
|
||||
$nomor_hp,
|
||||
$latitude,
|
||||
$longitude
|
||||
);
|
||||
|
||||
// Eksekusi query
|
||||
if ($stmt->execute()) {
|
||||
// Berhasil: kembalikan ID baru dan semua data yang disimpan
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Data SPBU berhasil ditambahkan.',
|
||||
'data' => [
|
||||
'id' => $koneksi->insert_id,
|
||||
'nama_spbu' => $nama_spbu,
|
||||
'status_ramai' => $status_ramai,
|
||||
'buka_24_jam' => $buka_24_jam,
|
||||
'status_operasional' => $status_operasional,
|
||||
'nomor_hp' => $nomor_hp,
|
||||
'latitude' => (float) $latitude,
|
||||
'longitude' => (float) $longitude,
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
// Gagal: kembalikan pesan error
|
||||
echo json_encode(['status' => 'error', 'message' => 'Gagal menyimpan data: ' . $stmt->error]);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$koneksi->close();
|
||||
?>
|
||||
Reference in New Issue
Block a user