Deploy WebGIS 01-05 dengan Docker

This commit is contained in:
2026-06-12 17:58:06 +07:00
commit 1bf000979c
76 changed files with 12301 additions and 0 deletions
+79
View File
@@ -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 */;
+40
View File
@@ -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();
?>
+354
View File
@@ -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; }
+590
View File
@@ -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: '&copy; <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, '&quot;')}" 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
View File
@@ -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();
?>
+48
View File
@@ -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
View File
@@ -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: '&copy; <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> &nbsp; Lng: <b>${lng}</b></div>
<div class="fg"><label>Nama SPBU</label><input id="f-nm" placeholder="Contoh: SPBU 44.501.12" maxlength="100"></div>
<div class="fg"><label>Status Keramaian</label>
<select id="f-rm"><option value="">-- Pilih --</option>
<option value="ramai">Ramai</option><option value="tidak_ramai">Tidak Ramai</option></select></div>
<div class="fg"><label>Buka 24 Jam?</label>
<select id="f-24"><option value="">-- Pilih --</option>
<option value="ya">Ya</option><option value="tidak">Tidak</option></select></div>
<div class="fg"><label>Status Operasional</label>
<select id="f-op"><option value="">-- Pilih --</option>
<option value="buka">Buka</option><option value="tutup">Tutup</option></select></div>
<div class="fg"><label>Nomor HP</label><input id="f-hp" placeholder="0812-3456-7890" maxlength="20"></div>
<div class="br">
<button class="btn bp" onclick="simpan(${lat},${lng})">💾 Simpan</button>
<button class="btn bs" onclick="map.closePopup()">✕ Batal</button>
</div>
</div>`;
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,'&quot;')}" maxlength="100"></div>
<div class="fg"><label>Status Keramaian</label>
<select id="e-rm">
<option value="ramai" ${s.status_ramai==='ramai'?'selected':''}>Ramai</option>
<option value="tidak_ramai" ${s.status_ramai==='tidak_ramai'?'selected':''}>Tidak Ramai</option></select></div>
<div class="fg"><label>Buka 24 Jam?</label>
<select id="e-24">
<option value="ya" ${s.buka_24_jam==='ya'?'selected':''}>Ya</option>
<option value="tidak" ${s.buka_24_jam==='tidak'?'selected':''}>Tidak</option></select></div>
<div class="fg"><label>Status Operasional</label>
<select id="e-op">
<option value="buka" ${s.status_operasional==='buka'?'selected':''}>Buka</option>
<option value="tutup" ${s.status_operasional==='tutup'?'selected':''}>Tutup</option></select></div>
<div class="fg"><label>Nomor HP</label><input id="e-hp" value="${s.nomor_hp}" maxlength="20"></div>
<div class="br">
<button class="btn bg" onclick="simpanEdit(${id})">💾 Simpan</button>
<button class="btn bs" onclick="kembaliDetail(${id})">✕ Batal</button>
</div>
</div>`;
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>
+21
View File
@@ -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
View File
@@ -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();
?>