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
+8
View File
@@ -0,0 +1,8 @@
.git
**/.git
__MACOSX
*.zip
*.rar
*.7z
.DS_Store
Thumbs.db
+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();
?>
+143
View File
@@ -0,0 +1,143 @@
-- 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: `02_spbudb`
--
-- --------------------------------------------------------
--
-- Table structure for table `polygon`
--
CREATE TABLE `polygon` (
`id` int NOT NULL,
`nama_polygon` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`keterangan` text COLLATE utf8mb4_unicode_ci,
`warna` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '#22c55e',
`koordinat` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `polygon`
--
INSERT INTO `polygon` (`id`, `nama_polygon`, `keterangan`, `warna`, `koordinat`, `created_at`, `updated_at`) VALUES
(1, 'Rumah A', 'Rumah A di tepi jalan imbon', '#c42170', '[[-0.04619857363388995,109.35349255800249],[-0.04609122197957324,109.35370445251466],[-0.046228095338798736,109.35379564762117],[-0.04634349836696752,109.35357302427295]]', '2026-06-08 09:59:46', '2026-06-08 09:59:46');
-- --------------------------------------------------------
--
-- Table structure for table `polyline`
--
CREATE TABLE `polyline` (
`id` int NOT NULL,
`nama_line` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`keterangan` text COLLATE utf8mb4_unicode_ci,
`warna` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '#3b82f6',
`koordinat` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `polyline`
--
INSERT INTO `polyline` (`id`, `nama_line`, `keterangan`, `warna`, `koordinat`, `created_at`, `updated_at`) VALUES
(1, 'Jl Tanjung Sari', 'Jalan tembusan \'TanSar\", dari mulut di ImBon dan dari mulut Jl. Jend. Ayani', '#3b82f6', '[[-0.052839376622127024,109.34751927852632],[-0.05137402667155636,109.34858143329622],[-0.050509845915652725,109.34916079044343],[-0.04923236129902831,109.35004591941835],[-0.04847016458323742,109.35060918331146],[-0.04772943818927965,109.35115635395051],[-0.04757377829389879,109.35129046440126],[-0.047589881041730155,109.35139775276186],[-0.04771333544149917,109.35161232948305],[-0.04650562934709907,109.3523955345154],[-0.045646816111789314,109.35296952724458],[-0.0457756380977098,109.35312509536745]]', '2026-06-08 10:03:20', '2026-06-08 10:03:20');
-- --------------------------------------------------------
--
-- Table structure for table `spbu`
--
CREATE TABLE `spbu` (
`id` int NOT NULL,
`nama_spbu` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`status_ramai` enum('ramai','tidak_ramai') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'tidak_ramai',
`buka_24_jam` enum('ya','tidak') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'tidak',
`status_operasional` enum('buka','tutup') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'buka',
`nomor_hp` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
`latitude` double NOT NULL,
`longitude` double NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `spbu`
--
INSERT INTO `spbu` (`id`, `nama_spbu`, `status_ramai`, `buka_24_jam`, `status_operasional`, `nomor_hp`, `latitude`, `longitude`, `created_at`, `updated_at`) VALUES
(4, 'SPBU Imbon', 'ramai', 'ya', 'buka', '0812345678913', -0.046981865484009226, 109.35455471277238, '2026-06-08 09:58:55', '2026-06-08 09:58:55');
--
-- Indexes for dumped tables
--
--
-- Indexes for table `polygon`
--
ALTER TABLE `polygon`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `polyline`
--
ALTER TABLE `polyline`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `spbu`
--
ALTER TABLE `spbu`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `polygon`
--
ALTER TABLE `polygon`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT for table `polyline`
--
ALTER TABLE `polyline`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT for table `spbu`
--
ALTER TABLE `spbu`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5;
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 */;
+69
View File
@@ -0,0 +1,69 @@
<?php
/**
* ambil_data.php
* Mengambil semua data SPBU, polyline, dan polygon dari database.
* Response dalam format JSON.
*/
header('Content-Type: application/json');
require_once 'koneksi.php';
$result = [
'status' => 'success',
'spbu' => [],
'polyline' => [],
'polygon' => [],
];
// ─── Ambil data SPBU ─────────────────────────────────────────────────────────
$query_spbu = "SELECT id, nama_spbu, status_ramai, buka_24_jam, status_operasional, nomor_hp, latitude, longitude FROM spbu ORDER BY id ASC";
$res = $koneksi->query($query_spbu);
if ($res) {
while ($row = $res->fetch_assoc()) {
// Cast tipe data numerik
$row['id'] = (int)$row['id'];
$row['latitude'] = (float)$row['latitude'];
$row['longitude'] = (float)$row['longitude'];
$result['spbu'][] = $row;
}
$res->free();
} else {
$result['status'] = 'partial_error';
$result['error_spbu'] = $koneksi->error;
}
// ─── Ambil data Polyline ──────────────────────────────────────────────────────
$query_poly = "SELECT id, nama_line, keterangan, warna, koordinat FROM polyline ORDER BY id ASC";
$res = $koneksi->query($query_poly);
if ($res) {
while ($row = $res->fetch_assoc()) {
$row['id'] = (int)$row['id'];
// Decode koordinat JSON → array, kembalikan sebagai array PHP (akan di-encode ulang ke JSON)
$row['koordinat'] = json_decode($row['koordinat'], true);
$result['polyline'][] = $row;
}
$res->free();
} else {
$result['status'] = 'partial_error';
$result['error_polyline'] = $koneksi->error;
}
// ─── Ambil data Polygon ───────────────────────────────────────────────────────
$query_pgon = "SELECT id, nama_polygon, keterangan, warna, koordinat FROM polygon ORDER BY id ASC";
$res = $koneksi->query($query_pgon);
if ($res) {
while ($row = $res->fetch_assoc()) {
$row['id'] = (int)$row['id'];
$row['koordinat'] = json_decode($row['koordinat'], true);
$result['polygon'][] = $row;
}
$res->free();
} else {
$result['status'] = 'partial_error';
$result['error_polygon'] = $koneksi->error;
}
$koneksi->close();
echo json_encode($result);
+82
View File
@@ -0,0 +1,82 @@
<?php
/**
* edit.php
* Menangani edit data SPBU.
* Latitude dan longitude TIDAK bisa diubah melalui endpoint ini.
*/
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan.']);
exit;
}
require_once 'koneksi.php';
$tipe = isset($_POST['tipe']) ? trim($_POST['tipe']) : '';
// Saat ini hanya menangani edit SPBU
if ($tipe === 'spbu') {
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
$nama_spbu = isset($_POST['nama_spbu']) ? trim($_POST['nama_spbu']) : '';
$status_ramai = isset($_POST['status_ramai']) ? trim($_POST['status_ramai']) : '';
$buka_24_jam = isset($_POST['buka_24_jam']) ? trim($_POST['buka_24_jam']) : '';
$status_operasional = isset($_POST['status_operasional']) ? trim($_POST['status_operasional']) : '';
$nomor_hp = isset($_POST['nomor_hp']) ? trim($_POST['nomor_hp']) : '';
// Validasi
if ($id <= 0 || empty($nama_spbu) || empty($status_ramai) || empty($buka_24_jam) || empty($status_operasional)) {
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap atau ID tidak valid.']);
exit;
}
$valid_ramai = ['ramai', 'tidak_ramai'];
$valid_24 = ['ya', 'tidak'];
$valid_ops = ['buka', 'tutup'];
if (!in_array($status_ramai, $valid_ramai) || !in_array($buka_24_jam, $valid_24) || !in_array($status_operasional, $valid_ops)) {
echo json_encode(['status' => 'error', 'message' => 'Nilai enum tidak valid.']);
exit;
}
// Update — latitude & longitude sengaja tidak diubah
$stmt = $koneksi->prepare(
"UPDATE spbu SET nama_spbu=?, status_ramai=?, buka_24_jam=?, status_operasional=?, nomor_hp=?
WHERE id=?"
);
$stmt->bind_param('sssssi', $nama_spbu, $status_ramai, $buka_24_jam, $status_operasional, $nomor_hp, $id);
if ($stmt->execute()) {
if ($stmt->affected_rows >= 0) {
// Ambil data lengkap (termasuk lat/lng) untuk dikembalikan ke JS
$sel = $koneksi->prepare("SELECT id, nama_spbu, status_ramai, buka_24_jam, status_operasional, nomor_hp, latitude, longitude FROM spbu WHERE id=?");
$sel->bind_param('i', $id);
$sel->execute();
$res = $sel->get_result();
$data = $res->fetch_assoc();
$sel->close();
$data['id'] = (int)$data['id'];
$data['latitude'] = (float)$data['latitude'];
$data['longitude'] = (float)$data['longitude'];
echo json_encode([
'status' => 'success',
'message' => 'Data SPBU berhasil diupdate.',
'data' => $data
]);
} else {
echo json_encode(['status' => 'error', 'message' => 'Data tidak ditemukan.']);
}
} else {
echo json_encode(['status' => 'error', 'message' => 'Gagal update: ' . $stmt->error]);
}
$stmt->close();
} else {
echo json_encode(['status' => 'error', 'message' => 'Tipe tidak dikenali untuk edit.']);
}
$koneksi->close();
+60
View File
@@ -0,0 +1,60 @@
<?php
/**
* hapus.php
* Menangani hapus data SPBU, polyline, dan polygon.
* Gunakan parameter 'tipe' untuk membedakan jenis data.
*/
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan.']);
exit;
}
require_once 'koneksi.php';
$tipe = isset($_POST['tipe']) ? trim($_POST['tipe']) : '';
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
if ($id <= 0) {
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid.']);
exit;
}
// Mapping tipe ke nama tabel
$tabel_map = [
'spbu' => 'spbu',
'polyline' => 'polyline',
'polygon' => 'polygon',
];
if (!array_key_exists($tipe, $tabel_map)) {
echo json_encode(['status' => 'error', 'message' => 'Tipe tidak dikenali.']);
exit;
}
$tabel = $tabel_map[$tipe];
// Prepared statement hapus berdasarkan tipe dan id
$stmt = $koneksi->prepare("DELETE FROM `{$tabel}` WHERE id = ?");
$stmt->bind_param('i', $id);
if ($stmt->execute()) {
if ($stmt->affected_rows > 0) {
echo json_encode([
'status' => 'success',
'message' => ucfirst($tipe) . ' berhasil dihapus.',
'id' => $id,
'tipe' => $tipe
]);
} else {
echo json_encode(['status' => 'error', 'message' => 'Data tidak ditemukan.']);
}
} else {
echo json_encode(['status' => 'error', 'message' => 'Gagal menghapus: ' . $stmt->error]);
}
$stmt->close();
$koneksi->close();
+1136
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
<?php
/**
* Koneksi database Project 02.
*/
$host = getenv('DB_HOST') ?: 'db';
$user = getenv('DB_USER') ?: 'root';
$password = getenv('DB_PASSWORD') ?: 'webgisidolapass';
$database = '02_spbudb';
$koneksi = new mysqli($host, $user, $password, $database);
if ($koneksi->connect_error) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode([
'status' => 'error',
'message' => 'Koneksi database gagal: ' . $koneksi->connect_error
]);
exit;
}
$koneksi->set_charset('utf8mb4');
?>
+70
View File
@@ -0,0 +1,70 @@
-- ═══════════════════════════════════════════════════════════════════
-- setup_database.sql
-- Jalankan query ini di phpMyAdmin (Laragon) atau MySQL CLI
-- untuk membuat database dan tabel yang diperlukan.
-- ═══════════════════════════════════════════════════════════════════
-- 1. Buat dan gunakan database
CREATE DATABASE IF NOT EXISTS `02_spbudb`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE `02_spbudb`;
-- ─────────────────────────────────────────────────────────────────────
-- 2. Tabel SPBU (marker titik lokasi)
-- ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS `spbu` (
`id` INT NOT NULL AUTO_INCREMENT,
`nama_spbu` VARCHAR(100) NOT NULL,
`status_ramai` ENUM('ramai','tidak_ramai') NOT NULL DEFAULT 'tidak_ramai',
`buka_24_jam` ENUM('ya','tidak') NOT NULL DEFAULT 'tidak',
`status_operasional` ENUM('buka','tutup') NOT NULL DEFAULT 'buka',
`nomor_hp` VARCHAR(20) NOT NULL DEFAULT '',
`latitude` DOUBLE NOT NULL,
`longitude` DOUBLE NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ─────────────────────────────────────────────────────────────────────
-- 3. Tabel Polyline (garis)
-- Kolom koordinat menyimpan JSON array of [lat, lng]
-- Contoh: [[-0.055, 109.348], [-0.056, 109.349]]
-- ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS `polyline` (
`id` INT NOT NULL AUTO_INCREMENT,
`nama_line` VARCHAR(100) NOT NULL,
`keterangan` TEXT,
`warna` VARCHAR(20) NOT NULL DEFAULT '#3b82f6',
`koordinat` LONGTEXT NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ─────────────────────────────────────────────────────────────────────
-- 4. Tabel Polygon (area)
-- Kolom koordinat menyimpan JSON array of [lat, lng]
-- Contoh: [[-0.055, 109.348], [-0.056, 109.349], [-0.055, 109.350]]
-- ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS `polygon` (
`id` INT NOT NULL AUTO_INCREMENT,
`nama_polygon` VARCHAR(100) NOT NULL,
`keterangan` TEXT,
`warna` VARCHAR(20) NOT NULL DEFAULT '#22c55e',
`koordinat` LONGTEXT NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ─────────────────────────────────────────────────────────────────────
-- 5. (Opsional) Data contoh untuk testing awal
-- ─────────────────────────────────────────────────────────────────────
INSERT INTO `spbu` (nama_spbu, status_ramai, buka_24_jam, status_operasional, nomor_hp, latitude, longitude)
VALUES
('SPBU Contoh A', 'ramai', 'ya', 'buka', '08123456789', -0.05566, 109.34854),
('SPBU Contoh B', 'tidak_ramai', 'tidak', 'buka', '08129876543', -0.05700, 109.34900),
('SPBU Contoh C', 'ramai', 'tidak', 'tutup', '', -0.05400, 109.34800);
+163
View File
@@ -0,0 +1,163 @@
<?php
/**
* tambah.php
* Menangani tambah data SPBU, polyline, dan polygon.
* Menerima POST dengan parameter 'tipe' untuk membedakan jenis data.
*/
header('Content-Type: application/json');
// Hanya terima method POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan.']);
exit;
}
require_once 'koneksi.php';
$tipe = isset($_POST['tipe']) ? trim($_POST['tipe']) : '';
// ─── TAMBAH SPBU ────────────────────────────────────────────────────────────
if ($tipe === 'spbu') {
$nama_spbu = isset($_POST['nama_spbu']) ? trim($_POST['nama_spbu']) : '';
$status_ramai = isset($_POST['status_ramai']) ? trim($_POST['status_ramai']) : '';
$buka_24_jam = isset($_POST['buka_24_jam']) ? trim($_POST['buka_24_jam']) : '';
$status_operasional = isset($_POST['status_operasional']) ? trim($_POST['status_operasional']) : '';
$nomor_hp = isset($_POST['nomor_hp']) ? trim($_POST['nomor_hp']) : '';
$latitude = isset($_POST['latitude']) ? floatval($_POST['latitude']) : null;
$longitude = isset($_POST['longitude']) ? floatval($_POST['longitude']) : null;
// Validasi server-side
if (empty($nama_spbu) || empty($status_ramai) || empty($buka_24_jam) || empty($status_operasional) || $latitude === null || $longitude === null) {
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap.']);
exit;
}
$valid_ramai = ['ramai', 'tidak_ramai'];
$valid_24 = ['ya', 'tidak'];
$valid_ops = ['buka', 'tutup'];
if (!in_array($status_ramai, $valid_ramai) || !in_array($buka_24_jam, $valid_24) || !in_array($status_operasional, $valid_ops)) {
echo json_encode(['status' => 'error', 'message' => 'Nilai enum tidak valid.']);
exit;
}
$stmt = $koneksi->prepare(
"INSERT INTO spbu (nama_spbu, status_ramai, buka_24_jam, status_operasional, nomor_hp, latitude, longitude)
VALUES (?, ?, ?, ?, ?, ?, ?)"
);
$stmt->bind_param('sssssdd', $nama_spbu, $status_ramai, $buka_24_jam, $status_operasional, $nomor_hp, $latitude, $longitude);
if ($stmt->execute()) {
$id = $koneksi->insert_id;
echo json_encode([
'status' => 'success',
'message' => 'SPBU berhasil ditambahkan.',
'id' => $id,
'data' => [
'id' => $id,
'nama_spbu' => $nama_spbu,
'status_ramai' => $status_ramai,
'buka_24_jam' => $buka_24_jam,
'status_operasional' => $status_operasional,
'nomor_hp' => $nomor_hp,
'latitude' => $latitude,
'longitude' => $longitude,
]
]);
} else {
echo json_encode(['status' => 'error', 'message' => 'Gagal menyimpan SPBU: ' . $stmt->error]);
}
$stmt->close();
// ─── TAMBAH POLYLINE ─────────────────────────────────────────────────────────
} elseif ($tipe === 'polyline') {
$nama_line = isset($_POST['nama_line']) ? trim($_POST['nama_line']) : '';
$keterangan = isset($_POST['keterangan']) ? trim($_POST['keterangan']) : '';
$warna = isset($_POST['warna']) ? trim($_POST['warna']) : '#3b82f6';
$koordinat = isset($_POST['koordinat']) ? trim($_POST['koordinat']) : '';
if (empty($nama_line) || empty($koordinat)) {
echo json_encode(['status' => 'error', 'message' => 'Nama line dan koordinat wajib diisi.']);
exit;
}
// Validasi JSON koordinat
$decoded = json_decode($koordinat, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) {
echo json_encode(['status' => 'error', 'message' => 'Format koordinat tidak valid.']);
exit;
}
$stmt = $koneksi->prepare(
"INSERT INTO polyline (nama_line, keterangan, warna, koordinat) VALUES (?, ?, ?, ?)"
);
$stmt->bind_param('ssss', $nama_line, $keterangan, $warna, $koordinat);
if ($stmt->execute()) {
$id = $koneksi->insert_id;
echo json_encode([
'status' => 'success',
'message' => 'Polyline berhasil ditambahkan.',
'id' => $id,
'data' => [
'id' => $id,
'nama_line' => $nama_line,
'keterangan' => $keterangan,
'warna' => $warna,
'koordinat' => $koordinat,
]
]);
} else {
echo json_encode(['status' => 'error', 'message' => 'Gagal menyimpan polyline: ' . $stmt->error]);
}
$stmt->close();
// ─── TAMBAH POLYGON ──────────────────────────────────────────────────────────
} elseif ($tipe === 'polygon') {
$nama_polygon = isset($_POST['nama_polygon']) ? trim($_POST['nama_polygon']) : '';
$keterangan = isset($_POST['keterangan']) ? trim($_POST['keterangan']) : '';
$warna = isset($_POST['warna']) ? trim($_POST['warna']) : '#22c55e';
$koordinat = isset($_POST['koordinat']) ? trim($_POST['koordinat']) : '';
if (empty($nama_polygon) || empty($koordinat)) {
echo json_encode(['status' => 'error', 'message' => 'Nama polygon dan koordinat wajib diisi.']);
exit;
}
$decoded = json_decode($koordinat, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) {
echo json_encode(['status' => 'error', 'message' => 'Format koordinat tidak valid.']);
exit;
}
$stmt = $koneksi->prepare(
"INSERT INTO polygon (nama_polygon, keterangan, warna, koordinat) VALUES (?, ?, ?, ?)"
);
$stmt->bind_param('ssss', $nama_polygon, $keterangan, $warna, $koordinat);
if ($stmt->execute()) {
$id = $koneksi->insert_id;
echo json_encode([
'status' => 'success',
'message' => 'Polygon berhasil ditambahkan.',
'id' => $id,
'data' => [
'id' => $id,
'nama_polygon' => $nama_polygon,
'keterangan' => $keterangan,
'warna' => $warna,
'koordinat' => $koordinat,
]
]);
} else {
echo json_encode(['status' => 'error', 'message' => 'Gagal menyimpan polygon: ' . $stmt->error]);
}
$stmt->close();
} else {
echo json_encode(['status' => 'error', 'message' => 'Tipe tidak dikenali.']);
}
$koneksi->close();
+106
View File
@@ -0,0 +1,106 @@
-- 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: `03_webgisdb`
--
-- --------------------------------------------------------
--
-- Table structure for table `jalan`
--
CREATE TABLE `jalan` (
`id` int NOT NULL,
`nama_jalan` varchar(150) NOT NULL,
`status_jalan` enum('nasional','provinsi','kabupaten') NOT NULL,
`panjang_meter` double NOT NULL,
`geojson` longtext 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 `jalan`
--
INSERT INTO `jalan` (`id`, `nama_jalan`, `status_jalan`, `panjang_meter`, `geojson`, `created_at`, `updated_at`) VALUES
(1, 'Gg. Pak Cik', 'kabupaten', 169.77, '{\"type\":\"Feature\",\"properties\":{},\"geometry\":{\"type\":\"LineString\",\"coordinates\":[[109.34135,-0.055498],[109.342616,-0.056351]]}}', '2026-06-08 12:30:39', '2026-06-08 12:30:39'),
(2, 'Jl. Trans Kalimantan', 'nasional', 5544.23, '{\"type\":\"Feature\",\"properties\":{},\"geometry\":{\"type\":\"LineString\",\"coordinates\":[[109.374583,-0.069933],[109.375999,-0.068258],[109.379303,-0.064651],[109.380591,-0.063685],[109.381771,-0.062891],[109.383423,-0.061517],[109.385548,-0.059992],[109.386578,-0.059327],[109.387114,-0.059048],[109.387758,-0.058833],[109.38868,-0.058318],[109.389496,-0.058038],[109.390697,-0.05748],[109.391813,-0.056815],[109.393015,-0.056128],[109.394302,-0.055677],[109.39559,-0.055226],[109.396598,-0.054882],[109.397478,-0.054603],[109.398851,-0.054324],[109.401062,-0.053723],[109.401855,-0.053465],[109.402649,-0.052907],[109.403057,-0.052284],[109.403958,-0.049257],[109.405246,-0.044942],[109.406469,-0.041313],[109.406598,-0.04054],[109.406598,-0.039338],[109.406254,-0.038694],[109.406254,-0.038221],[109.406641,-0.038178],[109.406984,-0.038565]]}}', '2026-06-08 12:34:53', '2026-06-08 12:34:53');
-- --------------------------------------------------------
--
-- Table structure for table `parsil_tanah`
--
CREATE TABLE `parsil_tanah` (
`id` int NOT NULL,
`nama_parsil` varchar(150) NOT NULL,
`status_kepemilikan` enum('SHM','HGB','HGU','HP') NOT NULL,
`luas_meter` double NOT NULL,
`geojson` longtext 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 `parsil_tanah`
--
INSERT INTO `parsil_tanah` (`id`, `nama_parsil`, `status_kepemilikan`, `luas_meter`, `geojson`, `created_at`, `updated_at`) VALUES
(1, 'kavling A1', 'HP', 1239.31, '{\"type\":\"Feature\",\"properties\":{},\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[109.339339,-0.060625],[109.339548,-0.060442],[109.339803,-0.060689],[109.339575,-0.060877],[109.339339,-0.060625]]]}}', '2026-06-08 12:30:03', '2026-06-08 12:30:03');
--
-- Indexes for dumped tables
--
--
-- Indexes for table `jalan`
--
ALTER TABLE `jalan`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `parsil_tanah`
--
ALTER TABLE `parsil_tanah`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `jalan`
--
ALTER TABLE `jalan`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
--
-- AUTO_INCREMENT for table `parsil_tanah`
--
ALTER TABLE `parsil_tanah`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
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 */;
+60
View File
@@ -0,0 +1,60 @@
<?php
// ambil_data.php
// Mengambil semua data jalan dan parsil tanah dari database dalam format JSON.
require_once "koneksi.php";
if ($_SERVER["REQUEST_METHOD"] !== "GET") {
json_response([
"success" => false,
"message" => "Method tidak diizinkan."
], 405);
}
$jalan = [];
$parsil = [];
// Ambil data jalan
$queryJalan = "SELECT id, nama_jalan, status_jalan, panjang_meter, geojson, created_at, updated_at
FROM jalan
ORDER BY id ASC";
$resultJalan = $koneksi->query($queryJalan);
if (!$resultJalan) {
json_response([
"success" => false,
"message" => "Gagal mengambil data jalan: " . $koneksi->error
], 500);
}
while ($row = $resultJalan->fetch_assoc()) {
$row["id"] = (int) $row["id"];
$row["panjang_meter"] = (float) $row["panjang_meter"];
$jalan[] = $row;
}
// Ambil data parsil tanah
$queryParsil = "SELECT id, nama_parsil, status_kepemilikan, luas_meter, geojson, created_at, updated_at
FROM parsil_tanah
ORDER BY id ASC";
$resultParsil = $koneksi->query($queryParsil);
if (!$resultParsil) {
json_response([
"success" => false,
"message" => "Gagal mengambil data parsil tanah: " . $koneksi->error
], 500);
}
while ($row = $resultParsil->fetch_assoc()) {
$row["id"] = (int) $row["id"];
$row["luas_meter"] = (float) $row["luas_meter"];
$parsil[] = $row;
}
json_response([
"success" => true,
"jalan" => $jalan,
"parsil" => $parsil
]);
?>
+26
View File
@@ -0,0 +1,26 @@
-- Database dan tabel aplikasi WebGIS
CREATE DATABASE IF NOT EXISTS `03_webgisdb`
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;
USE `03_webgisdb`;
CREATE TABLE IF NOT EXISTS `jalan` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`nama_jalan` VARCHAR(150) NOT NULL,
`status_jalan` ENUM('nasional','provinsi','kabupaten') NOT NULL,
`panjang_meter` DOUBLE NOT NULL,
`geojson` LONGTEXT NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `parsil_tanah` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`nama_parsil` VARCHAR(150) NOT NULL,
`status_kepemilikan` ENUM('SHM','HGB','HGU','HP') NOT NULL,
`luas_meter` DOUBLE NOT NULL,
`geojson` LONGTEXT NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+129
View File
@@ -0,0 +1,129 @@
<?php
// edit.php
// Mengedit atribut data jalan atau parsil tanah.
// Panjang, luas, dan geojson tidak diedit manual.
require_once "koneksi.php";
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
json_response([
"success" => false,
"message" => "Edit data hanya menerima method POST."
], 405);
}
$tipe = $_POST["tipe"] ?? "";
$id = $_POST["id"] ?? "";
if (!is_numeric($id) || (int) $id <= 0) {
json_response([
"success" => false,
"message" => "ID tidak valid."
], 422);
}
$id = (int) $id;
if ($tipe === "jalan") {
$nama_jalan = trim($_POST["nama_jalan"] ?? "");
$status_jalan = $_POST["status_jalan"] ?? "";
$status_valid = ["nasional", "provinsi", "kabupaten"];
if ($nama_jalan === "") {
json_response(["success" => false, "message" => "Nama jalan wajib diisi."], 422);
}
if (!in_array($status_jalan, $status_valid, true)) {
json_response(["success" => false, "message" => "Status jalan tidak valid."], 422);
}
$stmt = $koneksi->prepare(
"UPDATE jalan
SET nama_jalan = ?, status_jalan = ?
WHERE id = ?"
);
if (!$stmt) {
json_response(["success" => false, "message" => "Prepare statement gagal: " . $koneksi->error], 500);
}
$stmt->bind_param("ssi", $nama_jalan, $status_jalan, $id);
if (!$stmt->execute()) {
json_response(["success" => false, "message" => "Gagal mengedit data jalan: " . $stmt->error], 500);
}
if ($stmt->affected_rows === 0) {
// Bisa saja data sama persis, jadi tetap cek apakah ID ada.
$cek = $koneksi->prepare("SELECT id FROM jalan WHERE id = ?");
$cek->bind_param("i", $id);
$cek->execute();
$hasil = $cek->get_result();
if ($hasil->num_rows === 0) {
json_response(["success" => false, "message" => "Data jalan tidak ditemukan."], 404);
}
$cek->close();
}
$stmt->close();
json_response([
"success" => true,
"message" => "Data jalan berhasil diedit."
]);
}
if ($tipe === "parsil") {
$nama_parsil = trim($_POST["nama_parsil"] ?? "");
$status_kepemilikan = $_POST["status_kepemilikan"] ?? "";
$status_valid = ["SHM", "HGB", "HGU", "HP"];
if ($nama_parsil === "") {
json_response(["success" => false, "message" => "Nama parsil wajib diisi."], 422);
}
if (!in_array($status_kepemilikan, $status_valid, true)) {
json_response(["success" => false, "message" => "Status kepemilikan tidak valid."], 422);
}
$stmt = $koneksi->prepare(
"UPDATE parsil_tanah
SET nama_parsil = ?, status_kepemilikan = ?
WHERE id = ?"
);
if (!$stmt) {
json_response(["success" => false, "message" => "Prepare statement gagal: " . $koneksi->error], 500);
}
$stmt->bind_param("ssi", $nama_parsil, $status_kepemilikan, $id);
if (!$stmt->execute()) {
json_response(["success" => false, "message" => "Gagal mengedit data parsil tanah: " . $stmt->error], 500);
}
if ($stmt->affected_rows === 0) {
// Bisa saja data sama persis, jadi tetap cek apakah ID ada.
$cek = $koneksi->prepare("SELECT id FROM parsil_tanah WHERE id = ?");
$cek->bind_param("i", $id);
$cek->execute();
$hasil = $cek->get_result();
if ($hasil->num_rows === 0) {
json_response(["success" => false, "message" => "Data parsil tanah tidak ditemukan."], 404);
}
$cek->close();
}
$stmt->close();
json_response([
"success" => true,
"message" => "Data parsil tanah berhasil diedit."
]);
}
json_response([
"success" => false,
"message" => "Tipe data tidak valid. Gunakan tipe=jalan atau tipe=parsil."
], 422);
?>
+80
View File
@@ -0,0 +1,80 @@
<?php
// hapus.php
// Menghapus data jalan atau parsil tanah berdasarkan ID.
require_once "koneksi.php";
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
json_response([
"success" => false,
"message" => "Hapus data hanya menerima method POST."
], 405);
}
$tipe = $_POST["tipe"] ?? "";
$id = $_POST["id"] ?? "";
if (!is_numeric($id) || (int) $id <= 0) {
json_response([
"success" => false,
"message" => "ID tidak valid."
], 422);
}
$id = (int) $id;
if ($tipe === "jalan") {
$stmt = $koneksi->prepare("DELETE FROM jalan WHERE id = ?");
if (!$stmt) {
json_response(["success" => false, "message" => "Prepare statement gagal: " . $koneksi->error], 500);
}
$stmt->bind_param("i", $id);
if (!$stmt->execute()) {
json_response(["success" => false, "message" => "Gagal menghapus data jalan: " . $stmt->error], 500);
}
if ($stmt->affected_rows === 0) {
json_response(["success" => false, "message" => "Data jalan tidak ditemukan."], 404);
}
$stmt->close();
json_response([
"success" => true,
"message" => "Data jalan berhasil dihapus."
]);
}
if ($tipe === "parsil") {
$stmt = $koneksi->prepare("DELETE FROM parsil_tanah WHERE id = ?");
if (!$stmt) {
json_response(["success" => false, "message" => "Prepare statement gagal: " . $koneksi->error], 500);
}
$stmt->bind_param("i", $id);
if (!$stmt->execute()) {
json_response(["success" => false, "message" => "Gagal menghapus data parsil tanah: " . $stmt->error], 500);
}
if ($stmt->affected_rows === 0) {
json_response(["success" => false, "message" => "Data parsil tanah tidak ditemukan."], 404);
}
$stmt->close();
json_response([
"success" => true,
"message" => "Data parsil tanah berhasil dihapus."
]);
}
json_response([
"success" => false,
"message" => "Tipe data tidak valid. Gunakan tipe=jalan atau tipe=parsil."
], 422);
?>
+1177
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
<?php
/**
* Koneksi database Project 03.
*/
$host = getenv('DB_HOST') ?: 'db';
$user = getenv('DB_USER') ?: 'root';
$password = getenv('DB_PASSWORD') ?: 'webgisidolapass';
$database = '03_webgisdb';
$koneksi = new mysqli($host, $user, $password, $database);
if ($koneksi->connect_error) {
header('Content-Type: application/json; charset=utf-8');
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Koneksi database gagal: ' . $koneksi->connect_error
]);
exit;
}
$koneksi->set_charset('utf8mb4');
function json_response($data, $status_code = 200)
{
header('Content-Type: application/json; charset=utf-8');
http_response_code($status_code);
echo json_encode($data);
exit;
}
?>
+145
View File
@@ -0,0 +1,145 @@
<?php
// tambah.php
// Menambah data jalan atau parsil tanah.
// Parameter pembeda: tipe=jalan atau tipe=parsil.
require_once "koneksi.php";
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
json_response([
"success" => false,
"message" => "Tambah data hanya menerima method POST."
], 405);
}
function valid_geojson($geojson)
{
if ($geojson === "") {
return false;
}
json_decode($geojson, true);
return json_last_error() === JSON_ERROR_NONE;
}
$tipe = $_POST["tipe"] ?? "";
if ($tipe === "jalan") {
$nama_jalan = trim($_POST["nama_jalan"] ?? "");
$status_jalan = $_POST["status_jalan"] ?? "";
$panjang_meter = $_POST["panjang_meter"] ?? "";
$geojson = trim($_POST["geojson"] ?? "");
$status_valid = ["nasional", "provinsi", "kabupaten"];
if ($nama_jalan === "") {
json_response(["success" => false, "message" => "Nama jalan wajib diisi."], 422);
}
if (!in_array($status_jalan, $status_valid, true)) {
json_response(["success" => false, "message" => "Status jalan tidak valid."], 422);
}
if (!is_numeric($panjang_meter) || (float) $panjang_meter <= 0) {
json_response(["success" => false, "message" => "Panjang jalan tidak valid."], 422);
}
if (!valid_geojson($geojson)) {
json_response(["success" => false, "message" => "GeoJSON jalan tidak valid."], 422);
}
$panjang_meter = (float) $panjang_meter;
$stmt = $koneksi->prepare(
"INSERT INTO jalan (nama_jalan, status_jalan, panjang_meter, geojson)
VALUES (?, ?, ?, ?)"
);
if (!$stmt) {
json_response(["success" => false, "message" => "Prepare statement gagal: " . $koneksi->error], 500);
}
$stmt->bind_param("ssds", $nama_jalan, $status_jalan, $panjang_meter, $geojson);
if (!$stmt->execute()) {
json_response(["success" => false, "message" => "Gagal menyimpan data jalan: " . $stmt->error], 500);
}
$id = $stmt->insert_id;
$stmt->close();
json_response([
"success" => true,
"message" => "Data jalan berhasil ditambahkan.",
"data" => [
"id" => (int) $id,
"nama_jalan" => $nama_jalan,
"status_jalan" => $status_jalan,
"panjang_meter" => $panjang_meter,
"geojson" => $geojson
]
]);
}
if ($tipe === "parsil") {
$nama_parsil = trim($_POST["nama_parsil"] ?? "");
$status_kepemilikan = $_POST["status_kepemilikan"] ?? "";
$luas_meter = $_POST["luas_meter"] ?? "";
$geojson = trim($_POST["geojson"] ?? "");
$status_valid = ["SHM", "HGB", "HGU", "HP"];
if ($nama_parsil === "") {
json_response(["success" => false, "message" => "Nama parsil wajib diisi."], 422);
}
if (!in_array($status_kepemilikan, $status_valid, true)) {
json_response(["success" => false, "message" => "Status kepemilikan tidak valid."], 422);
}
if (!is_numeric($luas_meter) || (float) $luas_meter <= 0) {
json_response(["success" => false, "message" => "Luas tanah tidak valid."], 422);
}
if (!valid_geojson($geojson)) {
json_response(["success" => false, "message" => "GeoJSON parsil tanah tidak valid."], 422);
}
$luas_meter = (float) $luas_meter;
$stmt = $koneksi->prepare(
"INSERT INTO parsil_tanah (nama_parsil, status_kepemilikan, luas_meter, geojson)
VALUES (?, ?, ?, ?)"
);
if (!$stmt) {
json_response(["success" => false, "message" => "Prepare statement gagal: " . $koneksi->error], 500);
}
$stmt->bind_param("ssds", $nama_parsil, $status_kepemilikan, $luas_meter, $geojson);
if (!$stmt->execute()) {
json_response(["success" => false, "message" => "Gagal menyimpan data parsil tanah: " . $stmt->error], 500);
}
$id = $stmt->insert_id;
$stmt->close();
json_response([
"success" => true,
"message" => "Data parsil tanah berhasil ditambahkan.",
"data" => [
"id" => (int) $id,
"nama_parsil" => $nama_parsil,
"status_kepemilikan" => $status_kepemilikan,
"luas_meter" => $luas_meter,
"geojson" => $geojson
]
]);
}
json_response([
"success" => false,
"message" => "Tipe data tidak valid. Gunakan tipe=jalan atau tipe=parsil."
], 422);
?>
+77
View File
@@ -0,0 +1,77 @@
-- phpMyAdmin SQL Dump
-- version 5.2.3
-- https://www.phpmyadmin.net/
--
-- Host: localhost:3306
-- Generation Time: Jun 11, 2026 at 11:13 AM
-- Server version: 8.4.3
-- PHP Version: 8.3.30
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Database: `04_spbudb`
--
-- --------------------------------------------------------
--
-- Table structure for table `spbu`
--
CREATE TABLE `spbu` (
`id` int NOT NULL,
`nama_spbu` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`status_ramai` enum('ramai','tidak_ramai') COLLATE utf8mb4_unicode_ci NOT NULL,
`buka_24_jam` enum('ya','tidak') COLLATE utf8mb4_unicode_ci NOT NULL,
`status_operasional` enum('buka','tutup') COLLATE utf8mb4_unicode_ci NOT NULL,
`nomor_hp` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`latitude` double NOT NULL,
`longitude` double NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `spbu`
--
INSERT INTO `spbu` (`id`, `nama_spbu`, `status_ramai`, `buka_24_jam`, `status_operasional`, `nomor_hp`, `latitude`, `longitude`, `created_at`, `updated_at`) VALUES
(1, 'SPBU Gertak 1', 'tidak_ramai', 'ya', 'buka', '0815497422156', -0.0551481, 109.3480718, '2026-06-09 06:06:31', '2026-06-09 06:06:31'),
(2, 'SPBU Gertak 1', 'ramai', 'ya', 'buka', '0815497422156', -0.0548271, 109.3477392, '2026-06-09 06:06:52', '2026-06-09 06:07:09'),
(3, 'SPBU OSO', 'ramai', 'tidak', 'buka', '0815497422156', -0.0551277, 109.3474925, '2026-06-09 06:07:35', '2026-06-09 06:07:35'),
(4, 'SPBU OSO', 'tidak_ramai', 'tidak', 'buka', '087894587965', -0.0552458, 109.3478036, '2026-06-09 06:07:54', '2026-06-09 06:07:54'),
(5, 'SPBU Gertak 2', 'ramai', 'ya', 'tutup', '0815497422156', -0.0554175, 109.3474495, '2026-06-09 06:08:15', '2026-06-09 06:08:15');
--
-- Indexes for dumped tables
--
--
-- Indexes for table `spbu`
--
ALTER TABLE `spbu`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `spbu`
--
ALTER TABLE `spbu`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* ambil_data.php
* Mengambil seluruh data SPBU dari database dan mengembalikannya
* dalam format JSON. Dipanggil oleh JavaScript saat halaman dimuat
* dan saat dibutuhkan (misalnya untuk membuka form edit).
*/
// ── Sertakan file koneksi database ───────────────────────────────────────────
require_once 'koneksi.php';
// Set header response sebagai JSON
header('Content-Type: application/json');
// ── Ambil semua data dari tabel spbu, diurutkan dari yang terbaru ─────────────
$result = $koneksi->query("SELECT * FROM spbu ORDER BY id DESC");
// Cek apakah query berhasil
if (!$result) {
echo json_encode(['status' => 'error', 'message' => 'Gagal mengambil data: ' . $koneksi->error]);
exit;
}
// ── Kumpulkan semua baris ke dalam array ──────────────────────────────────────
$data = [];
while ($row = $result->fetch_assoc()) {
// Konversi latitude & longitude ke float agar JSON valid
$row['latitude'] = (float) $row['latitude'];
$row['longitude'] = (float) $row['longitude'];
$data[] = $row;
}
// ── Kembalikan data sebagai JSON ──────────────────────────────────────────────
echo json_encode([
'status' => 'success',
'data' => $data
]);
$koneksi->close();
?>
+115
View File
@@ -0,0 +1,115 @@
<?php
/**
* edit.php
* Menerima data POST dari JavaScript (fetch + FormData),
* memvalidasi, lalu memperbarui data SPBU di database.
* PENTING: Latitude dan longitude TIDAK diproses dan TIDAK diubah.
* Mengembalikan data terbaru agar JS bisa memperbarui marker dan layer group.
*/
// ── Sertakan file koneksi database ───────────────────────────────────────────
require_once 'koneksi.php';
// Set header response sebagai JSON
header('Content-Type: application/json');
// ── Hanya menerima method POST ────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan.']);
exit;
}
// ── Ambil & bersihkan input dari form (tanpa latitude/longitude) ──────────────
$id = intval($_POST['id'] ?? 0);
$nama_spbu = trim($_POST['nama_spbu'] ?? '');
$status_ramai = trim($_POST['status_ramai'] ?? '');
$buka_24_jam = trim($_POST['buka_24_jam'] ?? '');
$status_operasional = trim($_POST['status_operasional'] ?? '');
$nomor_hp = trim($_POST['nomor_hp'] ?? '');
// ── Validasi: ID harus valid ──────────────────────────────────────────────────
if ($id <= 0) {
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid.']);
exit;
}
// ── Validasi: semua field wajib terisi ───────────────────────────────────────
if (
empty($nama_spbu) ||
empty($status_ramai) ||
empty($buka_24_jam) ||
empty($status_operasional) ||
empty($nomor_hp)
) {
echo json_encode(['status' => 'error', 'message' => 'Semua field wajib diisi.']);
exit;
}
// ── Validasi nilai ENUM (server-side) ────────────────────────────────────────
$valid_ramai = ['ramai', 'tidak_ramai'];
$valid_24jam = ['ya', 'tidak'];
$valid_operasional = ['buka', 'tutup'];
if (!in_array($status_ramai, $valid_ramai)) {
echo json_encode(['status' => 'error', 'message' => 'Nilai status ramai tidak valid.']);
exit;
}
if (!in_array($buka_24_jam, $valid_24jam)) {
echo json_encode(['status' => 'error', 'message' => 'Nilai buka 24 jam tidak valid.']);
exit;
}
if (!in_array($status_operasional, $valid_operasional)) {
echo json_encode(['status' => 'error', 'message' => 'Nilai status operasional tidak valid.']);
exit;
}
// ── Update data di database menggunakan prepared statement ───────────────────
// Latitude dan longitude TIDAK disertakan dalam UPDATE — tidak berubah
$stmt = $koneksi->prepare(
"UPDATE spbu
SET nama_spbu = ?, status_ramai = ?, buka_24_jam = ?,
status_operasional = ?, nomor_hp = ?
WHERE id = ?"
);
// Bind parameter: s = string, i = integer
$stmt->bind_param(
'sssssi',
$nama_spbu,
$status_ramai,
$buka_24_jam,
$status_operasional,
$nomor_hp,
$id
);
// Eksekusi query
if ($stmt->execute()) {
if ($stmt->affected_rows > 0 || $stmt->affected_rows === 0) {
// Ambil data terbaru dari DB untuk dikembalikan ke JavaScript
// JS menggunakan data ini untuk memperbarui marker & layer group
$result = $koneksi->query("SELECT * FROM spbu WHERE id = $id");
if ($result && $result->num_rows > 0) {
$updatedData = $result->fetch_assoc();
$updatedData['latitude'] = (float) $updatedData['latitude'];
$updatedData['longitude'] = (float) $updatedData['longitude'];
echo json_encode([
'status' => 'success',
'message' => 'Data SPBU berhasil diperbarui.',
'data' => $updatedData
]);
} else {
echo json_encode(['status' => 'error', 'message' => 'Data tidak ditemukan setelah update.']);
}
} else {
echo json_encode(['status' => 'error', 'message' => 'Data tidak ditemukan.']);
}
} else {
echo json_encode(['status' => 'error', 'message' => 'Gagal memperbarui data: ' . $stmt->error]);
}
$stmt->close();
$koneksi->close();
?>
+53
View File
@@ -0,0 +1,53 @@
<?php
/**
* hapus.php
* Menerima ID dari JavaScript (fetch + FormData),
* lalu menghapus data SPBU yang sesuai dari database.
* JS akan menghapus marker dari layer group setelah response sukses.
*/
// ── Sertakan file koneksi database ───────────────────────────────────────────
require_once 'koneksi.php';
// Set header response sebagai JSON
header('Content-Type: application/json');
// ── Hanya menerima method POST ────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan.']);
exit;
}
// ── Ambil ID dari data POST ───────────────────────────────────────────────────
$id = intval($_POST['id'] ?? 0);
// ── Validasi: ID harus valid (lebih dari 0) ───────────────────────────────────
if ($id <= 0) {
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid.']);
exit;
}
// ── Hapus data dari database menggunakan prepared statement ──────────────────
$stmt = $koneksi->prepare("DELETE FROM spbu WHERE id = ?");
$stmt->bind_param('i', $id);
// Eksekusi query
if ($stmt->execute()) {
if ($stmt->affected_rows > 0) {
// Kembalikan ID yang dihapus agar JS bisa membersihkan marker dari layer group
echo json_encode([
'status' => 'success',
'message' => 'Data SPBU berhasil dihapus.',
'id' => $id
]);
} else {
echo json_encode(['status' => 'error', 'message' => 'Data tidak ditemukan.']);
}
} else {
echo json_encode(['status' => 'error', 'message' => 'Gagal menghapus data: ' . $stmt->error]);
}
$stmt->close();
$koneksi->close();
?>
+793
View File
@@ -0,0 +1,793 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS SPBU Layer Groups</title>
<!-- Leaflet CSS (tanpa integrity dan crossorigin) -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
/* ── Reset ────────────────────────────────────────────────── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html { height: 100%; }
body {
height: 100%;
font-family: sans-serif;
display: flex;
flex-direction: column;
}
/* ── Header ──────────────────────────────────────────────── */
#header {
background: #1a56db;
color: white;
padding: 0 20px;
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
z-index: 1000;
}
#header h1 { font-size: 1rem; font-weight: 700; }
#header span { font-size: 0.75rem; opacity: 0.85; }
/* ── Peta (tinggi wajib sesuai spec) ─────────────────────── */
#map {
width: 100%;
height: calc(100vh - 50px);
flex: 1;
}
/* ── Toast notification ──────────────────────────────────── */
#toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%) translateY(60px);
background: #1f2937;
color: white;
padding: 8px 20px;
border-radius: 20px;
font-size: 0.85rem;
z-index: 9999;
opacity: 0;
transition: all 0.3s;
pointer-events: none;
}
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
#toast.ok { background: #057a55; }
#toast.err { background: #e02424; }
/* ── Loading spinner ─────────────────────────────────────── */
#loading {
display: none;
position: fixed;
inset: 0;
background: rgba(255,255,255,0.5);
z-index: 8000;
align-items: center;
justify-content: center;
}
#loading.show { display: flex; }
.spin {
width: 36px; height: 36px;
border: 4px solid #ddd;
border-top-color: #1a56db;
border-radius: 50%;
animation: sp 0.7s linear infinite;
}
@keyframes sp { to { transform: rotate(360deg); } }
/* ── Popup Leaflet ───────────────────────────────────────── */
.leaflet-popup-content-wrapper {
border-radius: 10px !important;
padding: 0 !important;
overflow: hidden;
min-width: 270px;
}
.leaflet-popup-content { margin: 0 !important; }
/* Header popup */
.ph {
background: #1a56db;
color: white;
padding: 10px 14px;
font-weight: 700;
font-size: 0.9rem;
}
/* Body popup */
.pb { padding: 12px 14px; background: white; }
/* Tabel detail */
table.dt {
width: 100%; font-size: 0.82rem;
border-collapse: collapse; margin-bottom: 10px;
}
table.dt td { padding: 4px 3px; vertical-align: top; }
table.dt td:first-child { color: #6b7280; width: 110px; }
table.dt td:last-child { font-weight: 600; }
/* Form group */
.fg { margin-bottom: 9px; }
.fg label {
display: block; font-size: 0.75rem; font-weight: 700;
color: #6b7280; text-transform: uppercase;
letter-spacing: .04em; margin-bottom: 3px;
}
.fg input, .fg select {
width: 100%; padding: 6px 9px;
border: 1.5px solid #d1d9e0; border-radius: 6px;
font-size: 0.84rem; outline: none;
}
.fg input:focus, .fg select:focus { border-color: #1a56db; }
/* Button row */
.br { display: flex; gap: 7px; margin-top: 10px; }
.btn {
flex: 1; padding: 7px 10px; border: none;
border-radius: 6px; font-size: 0.82rem;
font-weight: 600; cursor: pointer;
}
.bp { background: #1a56db; color: white; }
.bd { background: #e02424; color: white; }
.bs { background: #f1f5f9; color: #333; border: 1px solid #ddd; }
.bg { background: #057a55; color: white; }
/* Kotak koordinat */
.coord-box {
background: #f0f9ff; border: 1px solid #bae6fd;
border-radius: 6px; padding: 6px 10px;
font-size: 0.78rem; color: #0369a1; margin-bottom: 9px;
}
/* ── Legend ──────────────────────────────────────────────── */
.legend {
background: white; border-radius: 8px;
padding: 10px 12px; font-size: 0.77rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
}
.legend b { display: block; margin-bottom: 6px; font-size: 0.8rem; }
.li { display: flex; align-items: center; gap: 7px; margin-bottom: 4px; }
.dot {
width: 13px; height: 13px; border-radius: 50%;
border: 2px solid rgba(0,0,0,0.2); flex-shrink: 0;
}
</style>
</head>
<body>
<div id="header">
<h1> WebGIS SPBU Layer Groups &amp; Layers Control</h1>
<span>💡 Klik lokasi pada peta untuk menambahkan SPBU</span>
</div>
<div id="map"></div>
<div id="toast"></div>
<div id="loading"><div class="spin"></div></div>
<!-- Leaflet JS (tanpa integrity dan crossorigin) -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// ════════════════════════════════════════════════════════════════
// CEK LEAFLET SEBELUM INISIALISASI
// Pastikan library Leaflet berhasil dimuat dari CDN
// ════════════════════════════════════════════════════════════════
if (typeof L === 'undefined') {
document.getElementById('map').innerHTML =
'<p style="padding:20px;color:red;">❌ Leaflet gagal dimuat. Periksa koneksi internet.</p>';
throw new Error('Leaflet not loaded');
}
// ════════════════════════════════════════════════════════════════
// VARIABEL GLOBAL
// ════════════════════════════════════════════════════════════════
// markersMap menyimpan referensi marker dengan key = id SPBU
// Setiap entry: { marker: L.Marker, layer: layerGroup }
// Menyimpan layer asal memudahkan pemindahan saat edit buka_24_jam berubah
const markersMap = {};
// Popup sementara untuk form tambah data
let tempPopup = null;
// ════════════════════════════════════════════════════════════════
// INISIALISASI PETA
// Koordinat awal: sekitar Pontianak, Kalimantan Barat
// ════════════════════════════════════════════════════════════════
const map = L.map('map').setView([-0.05566422376778593, 109.34854622732368], 17);
// Tile layer OpenStreetMap
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
// ════════════════════════════════════════════════════════════════
// PEMBUATAN LAYER GROUPS
// Dua layer group untuk memisahkan SPBU berdasarkan jam operasi
// ════════════════════════════════════════════════════════════════
// Layer group untuk SPBU yang buka 24 jam
const layerSPBU24Jam = L.layerGroup();
// Layer group untuk SPBU yang tidak buka 24 jam
const layerSPBUTidak24Jam = L.layerGroup();
// Tambahkan kedua layer ke peta secara default (langsung tampil saat halaman dibuka)
layerSPBU24Jam.addTo(map);
layerSPBUTidak24Jam.addTo(map);
// ════════════════════════════════════════════════════════════════
// PEMBUATAN LAYERS CONTROL (OVERLAY)
// Checkbox overlay — bukan radio button base layer
// Null pada parameter pertama berarti tidak ada base layer switcher
// ════════════════════════════════════════════════════════════════
const overlayMaps = {
"SPBU Buka 24 Jam": layerSPBU24Jam,
"SPBU Tidak Buka 24 Jam": layerSPBUTidak24Jam
};
// L.control.layers(baseLayers, overlayMaps) — baseLayers = null → hanya checkbox overlay
L.control.layers(null, overlayMaps).addTo(map);
// ════════════════════════════════════════════════════════════════
// LEGEND
// ════════════════════════════════════════════════════════════════
const leg = L.control({ position: 'bottomright' });
leg.onAdd = () => {
const d = L.DomUtil.create('div', 'legend');
d.innerHTML = `<b>🗺️ Keterangan Warna</b>
<div class="li"><div class="dot" style="background:#f97316"></div>Ramai · 24 Jam · Buka</div>
<div class="li"><div class="dot" style="background:#22c55e"></div>Tdk Ramai · 24 Jam · Buka</div>
<div class="li"><div class="dot" style="background:#ef4444"></div>Ramai · Tdk 24 Jam · Buka</div>
<div class="li"><div class="dot" style="background:#3b82f6"></div>Tdk Ramai · Tdk 24 Jam · Buka</div>
<div class="li"><div class="dot" style="background:#1f2937"></div>Tutup (semua jenis)</div>`;
return d;
};
leg.addTo(map);
// ════════════════════════════════════════════════════════════════
// UTILITAS: TOAST, LOADING
// ════════════════════════════════════════════════════════════════
/**
* showToast — tampilkan notifikasi singkat di bawah layar
* @param {string} message - Teks yang ditampilkan
* @param {string} type - 'ok' (hijau) atau 'err' (merah)
*/
function showToast(message, type = 'ok') {
const t = document.getElementById('toast');
t.textContent = message;
t.className = 'show ' + type;
setTimeout(() => { t.className = ''; }, 3000);
}
/** showLoading — tampilkan overlay loading spinner */
function showLoading() {
document.getElementById('loading').classList.add('show');
}
/** hideLoading — sembunyikan overlay loading spinner */
function hideLoading() {
document.getElementById('loading').classList.remove('show');
}
// ════════════════════════════════════════════════════════════════
// PENENTUAN WARNA MARKER
// Prioritas: tutup → hitam (apapun kondisi lain)
// Jika buka, warna ditentukan kombinasi status_ramai + buka_24_jam
// ════════════════════════════════════════════════════════════════
/**
* getMarkerColor — menentukan warna marker berdasarkan data SPBU
* @param {Object} data - Objek data SPBU
* @returns {string} Kode warna HEX
*/
function getMarkerColor(data) {
// Prioritas 1: jika tutup → warna HITAM
if (data.status_operasional === 'tutup') return '#1f2937';
// Prioritas 2: jika buka, tentukan warna dari kombinasi status_ramai & buka_24_jam
if (data.status_ramai === 'ramai' && data.buka_24_jam === 'ya') return '#f97316'; // ORANGE
if (data.status_ramai === 'tidak_ramai' && data.buka_24_jam === 'ya') return '#22c55e'; // HIJAU
if (data.status_ramai === 'ramai' && data.buka_24_jam === 'tidak') return '#ef4444'; // MERAH
if (data.status_ramai === 'tidak_ramai' && data.buka_24_jam === 'tidak') return '#3b82f6'; // BIRU
return '#9ca3af'; // fallback abu-abu
}
/**
* createMarkerIcon — membuat ikon marker SVG inline melalui L.divIcon
* @param {Object} data - Objek data SPBU
* @returns {L.DivIcon}
*/
function createMarkerIcon(data) {
const c = getMarkerColor(data);
// SVG pin berbentuk teardrop dengan lingkaran putih di tengah
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 36" width="26" height="38">
<path d="M12 0C5.4 0 0 5.4 0 12c0 9 12 24 12 24S24 21 24 12C24 5.4 18.6 0 12 0z"
fill="${c}" stroke="#fff" stroke-width="1.5"/>
<circle cx="12" cy="12" r="5" fill="#fff" opacity="0.9"/>
</svg>`;
return L.divIcon({
html: svg,
className: '',
iconSize: [26, 38],
iconAnchor: [13, 38],
popupAnchor: [0, -38]
});
}
/**
* getTargetLayer — menentukan layer group yang tepat berdasarkan buka_24_jam
* @param {Object} data - Objek data SPBU
* @returns {L.LayerGroup} Layer group yang sesuai
*/
function getTargetLayer(data) {
// buka_24_jam = 'ya' → masuk layerSPBU24Jam
// buka_24_jam = 'tidak' → masuk layerSPBUTidak24Jam
return data.buka_24_jam === 'ya' ? layerSPBU24Jam : layerSPBUTidak24Jam;
}
// ════════════════════════════════════════════════════════════════
// BADGE HELPER — menghasilkan label berwarna untuk popup detail
// ════════════════════════════════════════════════════════════════
function badgeR(v) {
return v === 'ramai'
? '<span style="background:#fef3c7;color:#92400e;padding:1px 7px;border-radius:10px;font-size:.75rem">Ramai</span>'
: '<span style="background:#ecfdf5;color:#065f46;padding:1px 7px;border-radius:10px;font-size:.75rem">Tidak Ramai</span>';
}
function badge24(v) {
return v === 'ya'
? '<span style="background:#eff6ff;color:#1e40af;padding:1px 7px;border-radius:10px;font-size:.75rem">Ya</span>'
: '<span style="background:#f9fafb;color:#374151;padding:1px 7px;border-radius:10px;font-size:.75rem">Tidak</span>';
}
function badgeO(v) {
return v === 'buka'
? '<span style="background:#d1fae5;color:#065f46;padding:1px 7px;border-radius:10px;font-size:.75rem">Buka</span>'
: '<span style="background:#fee2e2;color:#991b1b;padding:1px 7px;border-radius:10px;font-size:.75rem">Tutup</span>';
}
// ════════════════════════════════════════════════════════════════
// POPUP DETAIL MARKER
// Menampilkan semua kolom termasuk lat, lng, created_at, updated_at
// ════════════════════════════════════════════════════════════════
function buildPopupDetail(s) {
const namaSafe = s.nama_spbu.replace(/'/g, "\\'");
return `<div class="ph">⛽ ${s.nama_spbu}</div>
<div class="pb">
<table class="dt">
<tr><td>Status Ramai</td><td>${badgeR(s.status_ramai)}</td></tr>
<tr><td>Buka 24 Jam</td><td>${badge24(s.buka_24_jam)}</td></tr>
<tr><td>Status</td><td>${badgeO(s.status_operasional)}</td></tr>
<tr><td>Nomor HP</td><td>📞 ${s.nomor_hp}</td></tr>
<tr><td>Latitude</td><td>${parseFloat(s.latitude).toFixed(6)}</td></tr>
<tr><td>Longitude</td><td>${parseFloat(s.longitude).toFixed(6)}</td></tr>
<tr><td>Dibuat</td><td>${s.created_at}</td></tr>
<tr><td>Diperbarui</td><td>${s.updated_at}</td></tr>
</table>
<div class="br">
<button class="btn bp" onclick="bukaEdit(${s.id})">✏️ Edit</button>
<button class="btn bd" onclick="hapus(${s.id},'${namaSafe}')">🗑️ Hapus</button>
</div>
</div>`;
}
// ════════════════════════════════════════════════════════════════
// PENEMPATAN MARKER KE LAYER GROUP
// ════════════════════════════════════════════════════════════════
/**
* addMarkerToMap — membuat marker dan menempatkannya ke layer group yang sesuai
* @param {Object} data - Objek data SPBU dari database
*/
function addMarkerToMap(data) {
// Tentukan layer group berdasarkan nilai buka_24_jam
const targetLayer = getTargetLayer(data);
// Buat marker dengan ikon SVG sesuai warna
const marker = L.marker([data.latitude, data.longitude], {
icon: createMarkerIcon(data)
});
// Pasang popup detail ke marker
marker.bindPopup(buildPopupDetail(data), { maxWidth: 320 });
// Tambahkan marker ke layer group yang tepat (bukan langsung ke map)
// Layer group sudah ditambahkan ke map, jadi marker otomatis tampil
marker.addTo(targetLayer);
// Simpan referensi marker + layer asalnya ke markersMap
markersMap[data.id] = {
marker: marker,
layer: targetLayer // simpan layer asal untuk kebutuhan edit
};
}
/**
* removeMarkerFromMap — hapus marker dari layer group dan dari markersMap
* @param {number|string} id - ID SPBU yang akan dihapus
*/
function removeMarkerFromMap(id) {
if (!markersMap[id]) return;
// Hapus marker dari layer group asalnya
markersMap[id].layer.removeLayer(markersMap[id].marker);
// Hapus entry dari markersMap
delete markersMap[id];
}
/**
* refreshMarkerAfterEdit — perbarui marker setelah data diedit
* Jika buka_24_jam berubah, marker dipindahkan ke layer group yang baru
* @param {Object} data - Data SPBU terbaru dari server
*/
function refreshMarkerAfterEdit(data) {
if (!markersMap[data.id]) return;
const oldEntry = markersMap[data.id];
const newLayer = getTargetLayer(data);
const layerChanged = oldEntry.layer !== newLayer;
if (layerChanged) {
// ── Hapus marker lama dari layer group lama ──────────────
oldEntry.layer.removeLayer(oldEntry.marker);
// ── Buat marker baru dengan data terbaru ─────────────────
const newMarker = L.marker([data.latitude, data.longitude], {
icon: createMarkerIcon(data)
});
newMarker.bindPopup(buildPopupDetail(data), { maxWidth: 320 });
// ── Masukkan ke layer group baru ──────────────────────────
newMarker.addTo(newLayer);
newMarker.openPopup();
// ── Perbarui entry di markersMap ──────────────────────────
markersMap[data.id] = { marker: newMarker, layer: newLayer };
} else {
// Layer tidak berubah — cukup update icon dan konten popup
oldEntry.marker.setIcon(createMarkerIcon(data));
oldEntry.marker.setPopupContent(buildPopupDetail(data));
oldEntry.marker.openPopup();
}
}
// ════════════════════════════════════════════════════════════════
// LOAD DATA — muat semua marker dari database saat halaman dibuka
// ════════════════════════════════════════════════════════════════
/**
* loadData — fetch semua data SPBU dari ambil_data.php
* lalu tempatkan marker ke layer group masing-masing
*/
async function loadData() {
try {
const res = await fetch('ambil_data.php');
const json = await res.json();
if (json.status !== 'success') return;
// Bersihkan semua marker yang ada (jika ada dari load sebelumnya)
Object.keys(markersMap).forEach(id => removeMarkerFromMap(id));
// Tambahkan semua marker ke layer group sesuai buka_24_jam masing-masing
json.data.forEach(spbu => addMarkerToMap(spbu));
} catch (err) {
console.error('loadData error:', err);
}
}
// ════════════════════════════════════════════════════════════════
// KLIK PETA → POPUP FORM TAMBAH DATA (menggunakan L.popup())
// Latitude dan longitude diambil otomatis dari titik klik,
// TIDAK ditampilkan di form — hanya dikirim saat simpan
// ════════════════════════════════════════════════════════════════
map.on('click', function(e) {
const lat = e.latlng.lat.toFixed(7);
const lng = e.latlng.lng.toFixed(7);
// Tutup popup tambah sebelumnya jika ada
if (tempPopup) { map.closePopup(tempPopup); tempPopup = null; }
// HTML form tambah — lat & lng tidak tampil di form, hanya di kotak info
const html = `<div class="ph">📍 Tambah SPBU Baru</div>
<div class="pb">
<div class="coord-box">📌 Lat: <b>${lat}</b> &nbsp;|&nbsp; Lng: <b>${lng}</b></div>
<div class="fg">
<label>Nama SPBU</label>
<input id="f-nm" placeholder="Contoh: SPBU 44.501.12" maxlength="100">
</div>
<div class="fg">
<label>Status Keramaian</label>
<select id="f-rm">
<option value="">-- Pilih --</option>
<option value="ramai">Ramai</option>
<option value="tidak_ramai">Tidak Ramai</option>
</select>
</div>
<div class="fg">
<label>Buka 24 Jam?</label>
<select id="f-24">
<option value="">-- Pilih --</option>
<option value="ya">Ya</option>
<option value="tidak">Tidak</option>
</select>
</div>
<div class="fg">
<label>Status Operasional</label>
<select id="f-op">
<option value="">-- Pilih --</option>
<option value="buka">Buka</option>
<option value="tutup">Tutup</option>
</select>
</div>
<div class="fg">
<label>Nomor HP</label>
<input id="f-hp" placeholder="0812-3456-7890" maxlength="20">
</div>
<div class="br">
<button class="btn bp" onclick="simpan(${lat}, ${lng})">💾 Simpan</button>
<button class="btn bs" onclick="map.closePopup()">✕ Batal</button>
</div>
</div>`;
// Buat popup tambah menggunakan L.popup() dan buka di titik klik
tempPopup = L.popup({ maxWidth: 320 })
.setLatLng(e.latlng)
.setContent(html)
.openOn(map);
});
// ════════════════════════════════════════════════════════════════
// PROSES TAMBAH DATA via fetch()
// ════════════════════════════════════════════════════════════════
/**
* simpan — ambil nilai form, kirim ke tambah.php via fetch + FormData,
* lalu tambahkan marker baru ke layer group yang sesuai
* @param {number} lat - Latitude dari titik klik
* @param {number} lng - Longitude dari titik klik
*/
async function simpan(lat, lng) {
const nm = document.getElementById('f-nm')?.value.trim();
const rm = document.getElementById('f-rm')?.value;
const j24 = document.getElementById('f-24')?.value;
const op = document.getElementById('f-op')?.value;
const hp = document.getElementById('f-hp')?.value.trim();
// Validasi sisi klien sebelum kirim ke server
if (!nm || !rm || !j24 || !op || !hp) {
showToast('⚠️ Semua field wajib diisi!', 'err');
return;
}
// Buat FormData untuk dikirim via fetch POST
const fd = new FormData();
fd.append('nama_spbu', nm);
fd.append('status_ramai', rm);
fd.append('buka_24_jam', j24);
fd.append('status_operasional', op);
fd.append('nomor_hp', hp);
fd.append('latitude', lat); // lat dari klik peta
fd.append('longitude', lng); // lng dari klik peta
showLoading();
try {
// Kirim data ke tambah.php menggunakan fetch POST
const res = await fetch('tambah.php', { method: 'POST', body: fd });
const json = await res.json();
if (json.status === 'success') {
// Tutup popup form tambah
map.closePopup(); tempPopup = null;
// Tambahkan marker baru ke layer group yang sesuai (berdasarkan buka_24_jam)
addMarkerToMap(json.data);
// Pan ke marker baru dan buka popup detail-nya
map.panTo([json.data.latitude, json.data.longitude]);
markersMap[json.data.id].marker.openPopup();
showToast('✅ Data berhasil ditambahkan!', 'ok');
} else {
showToast('❌ ' + json.message, 'err');
}
} catch (err) {
showToast('❌ Gagal menghubungi server', 'err');
} finally {
hideLoading();
}
}
// ════════════════════════════════════════════════════════════════
// BUKA FORM EDIT (dari tombol di popup detail)
// ════════════════════════════════════════════════════════════════
/**
* bukaEdit — fetch data SPBU lalu tampilkan form edit di popup marker
* @param {number} id - ID SPBU yang akan diedit
*/
async function bukaEdit(id) {
showLoading();
try {
// Ambil data terbaru dari server untuk mengisi form edit
const res = await fetch('ambil_data.php');
const json = await res.json();
hideLoading();
const s = json.data.find(x => x.id == id);
if (!s) { showToast('❌ Data tidak ditemukan', 'err'); return; }
// HTML form edit — lat & lng tidak tampil, hanya field yang boleh diubah
const html = `<div class="ph">✏️ Edit Data SPBU</div>
<div class="pb">
<div class="fg">
<label>Nama SPBU</label>
<input id="e-nm" value="${s.nama_spbu.replace(/"/g, '&quot;')}" maxlength="100">
</div>
<div class="fg">
<label>Status Keramaian</label>
<select id="e-rm">
<option value="ramai" ${s.status_ramai === 'ramai' ? 'selected' : ''}>Ramai</option>
<option value="tidak_ramai" ${s.status_ramai === 'tidak_ramai' ? 'selected' : ''}>Tidak Ramai</option>
</select>
</div>
<div class="fg">
<label>Buka 24 Jam?</label>
<select id="e-24">
<option value="ya" ${s.buka_24_jam === 'ya' ? 'selected' : ''}>Ya</option>
<option value="tidak" ${s.buka_24_jam === 'tidak' ? 'selected' : ''}>Tidak</option>
</select>
</div>
<div class="fg">
<label>Status Operasional</label>
<select id="e-op">
<option value="buka" ${s.status_operasional === 'buka' ? 'selected' : ''}>Buka</option>
<option value="tutup" ${s.status_operasional === 'tutup' ? 'selected' : ''}>Tutup</option>
</select>
</div>
<div class="fg">
<label>Nomor HP</label>
<input id="e-hp" value="${s.nomor_hp}" maxlength="20">
</div>
<div class="br">
<button class="btn bg" onclick="simpanEdit(${id})">💾 Simpan</button>
<button class="btn bs" onclick="kembaliDetail(${id})"> Batal</button>
</div>
</div>`;
// Tampilkan form edit di popup marker yang bersangkutan
if (markersMap[id]) {
markersMap[id].marker.setPopupContent(html).openPopup();
}
} catch (err) {
hideLoading();
showToast('❌ Gagal menghubungi server', 'err');
}
}
// ════════════════════════════════════════════════════════════════
// PROSES EDIT DATA via fetch()
// ════════════════════════════════════════════════════════════════
/**
* simpanEdit — kirim perubahan ke edit.php via fetch POST,
* lalu perbarui marker (termasuk pindah layer group jika buka_24_jam berubah)
* @param {number} id - ID SPBU yang sedang diedit
*/
async function simpanEdit(id) {
const nm = document.getElementById('e-nm')?.value.trim();
const rm = document.getElementById('e-rm')?.value;
const j24 = document.getElementById('e-24')?.value;
const op = document.getElementById('e-op')?.value;
const hp = document.getElementById('e-hp')?.value.trim();
// Validasi sisi klien
if (!nm || !rm || !j24 || !op || !hp) {
showToast('⚠️ Semua field wajib diisi!', 'err');
return;
}
// FormData hanya berisi field yang boleh diubah — lat & lng tidak dikirim
const fd = new FormData();
fd.append('id', id);
fd.append('nama_spbu', nm);
fd.append('status_ramai', rm);
fd.append('buka_24_jam', j24);
fd.append('status_operasional', op);
fd.append('nomor_hp', hp);
showLoading();
try {
// Kirim ke edit.php menggunakan fetch POST
const res = await fetch('edit.php', { method: 'POST', body: fd });
const json = await res.json();
if (json.status === 'success') {
// Perbarui marker: hapus dari layer lama, buat baru di layer yang sesuai
// Jika buka_24_jam berubah, marker otomatis berpindah layer group
refreshMarkerAfterEdit(json.data);
showToast('✅ Data berhasil diperbarui!', 'ok');
} else {
showToast('❌ ' + json.message, 'err');
}
} catch (err) {
showToast('❌ Gagal menghubungi server', 'err');
} finally {
hideLoading();
}
}
// ════════════════════════════════════════════════════════════════
// PROSES HAPUS DATA via fetch()
// ════════════════════════════════════════════════════════════════
/**
* hapus — konfirmasi lalu kirim request hapus ke hapus.php,
* setelah sukses hapus marker dari layer group dan markersMap
* @param {number} id - ID SPBU yang akan dihapus
* @param {string} nama - Nama SPBU untuk konfirmasi dialog
*/
async function hapus(id, nama) {
if (!confirm(`Hapus SPBU "${nama}"? Tindakan ini tidak dapat dibatalkan.`)) return;
const fd = new FormData();
fd.append('id', id);
showLoading();
try {
// Kirim ke hapus.php menggunakan fetch POST
const res = await fetch('hapus.php', { method: 'POST', body: fd });
const json = await res.json();
if (json.status === 'success') {
// Hapus marker dari layer group asalnya dan dari markersMap
removeMarkerFromMap(id);
showToast('🗑️ Data berhasil dihapus', 'ok');
} else {
showToast('❌ ' + json.message, 'err');
}
} catch (err) {
showToast('❌ Gagal menghubungi server', 'err');
} finally {
hideLoading();
}
}
// ════════════════════════════════════════════════════════════════
// KEMBALI KE POPUP DETAIL (dari form edit saat batal)
// ════════════════════════════════════════════════════════════════
/**
* kembaliDetail — ambil data terbaru lalu tampilkan kembali popup detail
* @param {number} id - ID SPBU
*/
async function kembaliDetail(id) {
try {
const res = await fetch('ambil_data.php');
const json = await res.json();
const s = json.data.find(x => x.id == id);
if (s && markersMap[id]) {
markersMap[id].marker.setPopupContent(buildPopupDetail(s)).openPopup();
}
} catch (err) {
console.error('kembaliDetail error:', err);
}
}
// ════════════════════════════════════════════════════════════════
// INISIALISASI — muat semua marker saat halaman pertama dibuka
// ════════════════════════════════════════════════════════════════
loadData();
</script>
</body>
</html>
+20
View File
@@ -0,0 +1,20 @@
<?php
/**
* Koneksi database Project 04.
*/
$host = getenv('DB_HOST') ?: 'db';
$dbname = '04_spbudb';
$username = getenv('DB_USER') ?: 'root';
$password = getenv('DB_PASSWORD') ?: 'webgisidolapass';
$koneksi = new mysqli($host, $username, $password, $dbname);
$koneksi->set_charset('utf8mb4');
if ($koneksi->connect_error) {
http_response_code(500);
die(json_encode([
'status' => 'error',
'message' => 'Koneksi database gagal: ' . $koneksi->connect_error
]));
}
?>
+33
View File
@@ -0,0 +1,33 @@
-- ============================================================
-- schema.sql
-- Jalankan query ini di phpMyAdmin atau MySQL CLI Laragon
-- untuk membuat database dan tabel project 04
-- ============================================================
-- Buat database (jika belum ada)
CREATE DATABASE IF NOT EXISTS `04_spbudb`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
-- Gunakan database yang baru dibuat
USE `04_spbudb`;
-- Hapus tabel jika sudah ada (untuk reset bersih)
DROP TABLE IF EXISTS `spbu`;
-- Buat tabel spbu
CREATE TABLE `spbu` (
`id` INT NOT NULL AUTO_INCREMENT,
`nama_spbu` VARCHAR(100) NOT NULL,
`status_ramai` ENUM('ramai','tidak_ramai') NOT NULL,
`buka_24_jam` ENUM('ya','tidak') NOT NULL,
`status_operasional` ENUM('buka','tutup') NOT NULL,
`nomor_hp` VARCHAR(20) NOT NULL,
`latitude` DOUBLE NOT NULL,
`longitude` DOUBLE NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;
+116
View File
@@ -0,0 +1,116 @@
<?php
/**
* tambah.php
* Menerima data POST dari JavaScript (fetch + FormData),
* memvalidasi, lalu menyimpan data SPBU baru ke database.
* Mengembalikan response JSON termasuk data lengkap yang baru disimpan
* agar JavaScript bisa langsung menambahkan marker ke layer group yang tepat.
*/
// ── Sertakan file koneksi database ───────────────────────────────────────────
require_once 'koneksi.php';
// Set header response sebagai JSON
header('Content-Type: application/json');
// ── Hanya menerima method POST ────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan.']);
exit;
}
// ── Ambil & bersihkan input dari form ────────────────────────────────────────
$nama_spbu = trim($_POST['nama_spbu'] ?? '');
$status_ramai = trim($_POST['status_ramai'] ?? '');
$buka_24_jam = trim($_POST['buka_24_jam'] ?? '');
$status_operasional = trim($_POST['status_operasional'] ?? '');
$nomor_hp = trim($_POST['nomor_hp'] ?? '');
$latitude = trim($_POST['latitude'] ?? '');
$longitude = trim($_POST['longitude'] ?? '');
// ── Validasi: semua field wajib terisi ───────────────────────────────────────
if (
empty($nama_spbu) ||
empty($status_ramai) ||
empty($buka_24_jam) ||
empty($status_operasional) ||
empty($nomor_hp) ||
$latitude === '' ||
$longitude === ''
) {
echo json_encode(['status' => 'error', 'message' => 'Semua field wajib diisi.']);
exit;
}
// ── Validasi nilai ENUM (server-side) ────────────────────────────────────────
$valid_ramai = ['ramai', 'tidak_ramai'];
$valid_24jam = ['ya', 'tidak'];
$valid_operasional = ['buka', 'tutup'];
if (!in_array($status_ramai, $valid_ramai)) {
echo json_encode(['status' => 'error', 'message' => 'Nilai status ramai tidak valid.']);
exit;
}
if (!in_array($buka_24_jam, $valid_24jam)) {
echo json_encode(['status' => 'error', 'message' => 'Nilai buka 24 jam tidak valid.']);
exit;
}
if (!in_array($status_operasional, $valid_operasional)) {
echo json_encode(['status' => 'error', 'message' => 'Nilai status operasional tidak valid.']);
exit;
}
// ── Validasi latitude & longitude harus numerik ──────────────────────────────
if (!is_numeric($latitude) || !is_numeric($longitude)) {
echo json_encode(['status' => 'error', 'message' => 'Koordinat tidak valid.']);
exit;
}
// Konversi ke float
$latitude = (float) $latitude;
$longitude = (float) $longitude;
// ── Simpan ke database menggunakan prepared statement ────────────────────────
// Prepared statement mencegah SQL Injection
$stmt = $koneksi->prepare(
"INSERT INTO spbu
(nama_spbu, status_ramai, buka_24_jam, status_operasional, nomor_hp, latitude, longitude)
VALUES (?, ?, ?, ?, ?, ?, ?)"
);
// Bind parameter: s = string, d = double
$stmt->bind_param(
'sssssdd',
$nama_spbu,
$status_ramai,
$buka_24_jam,
$status_operasional,
$nomor_hp,
$latitude,
$longitude
);
// Eksekusi query
if ($stmt->execute()) {
$newId = $koneksi->insert_id;
// Ambil data lengkap termasuk created_at & updated_at dari DB
$res = $koneksi->query("SELECT * FROM spbu WHERE id = $newId");
$newData = $res->fetch_assoc();
$newData['latitude'] = (float) $newData['latitude'];
$newData['longitude'] = (float) $newData['longitude'];
// Kembalikan data lengkap agar JS bisa menentukan layer group yang tepat
echo json_encode([
'status' => 'success',
'message' => 'Data SPBU berhasil ditambahkan.',
'data' => $newData
]);
} else {
echo json_encode(['status' => 'error', 'message' => 'Gagal menyimpan data: ' . $stmt->error]);
}
$stmt->close();
$koneksi->close();
?>
+373
View File
@@ -0,0 +1,373 @@
-- phpMyAdmin SQL Dump
-- version 5.2.3
-- https://www.phpmyadmin.net/
--
-- Host: localhost:3306
-- Generation Time: Jun 12, 2026 at 10:30 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: `05_povertydb`
--
-- --------------------------------------------------------
--
-- Table structure for table `penerima_bantuan`
--
CREATE TABLE `penerima_bantuan` (
`id` int NOT NULL,
`user_id` int DEFAULT NULL,
`rumah_ibadah_id` int DEFAULT NULL,
`nama_penerima` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`nik` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`no_hp` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`alamat` text COLLATE utf8mb4_unicode_ci NOT NULL,
`pekerjaan` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`penghasilan` int NOT NULL,
`jumlah_tanggungan` int NOT NULL,
`kondisi_rumah` enum('baik','sedang','rusak') COLLATE utf8mb4_unicode_ci NOT NULL,
`jenis_bantuan` enum('zakat','sembako','uang_tunai','lainnya') COLLATE utf8mb4_unicode_ci NOT NULL,
`latitude` double NOT NULL,
`longitude` double NOT NULL,
`jarak_meter` double DEFAULT NULL,
`status_pengajuan` enum('menunggu','diterima','ditolak','dibatalkan') COLLATE utf8mb4_unicode_ci DEFAULT 'menunggu',
`status_hapus` enum('normal','menunggu','disetujui','ditolak') COLLATE utf8mb4_unicode_ci DEFAULT 'normal',
`alasan_hapus` text COLLATE utf8mb4_unicode_ci,
`alasan_status` text COLLATE utf8mb4_unicode_ci,
`sumber_pengajuan` enum('mandiri','admin_rumah_ibadah') COLLATE utf8mb4_unicode_ci 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_unicode_ci;
--
-- Dumping data for table `penerima_bantuan`
--
INSERT INTO `penerima_bantuan` (`id`, `user_id`, `rumah_ibadah_id`, `nama_penerima`, `nik`, `no_hp`, `alamat`, `pekerjaan`, `penghasilan`, `jumlah_tanggungan`, `kondisi_rumah`, `jenis_bantuan`, `latitude`, `longitude`, `jarak_meter`, `status_pengajuan`, `status_hapus`, `alasan_hapus`, `alasan_status`, `sumber_pengajuan`, `created_at`, `updated_at`) VALUES
(1, 4, 1, 'Jotok', '60103011704940005', '08567891325', 'Jl. Bujang Malaka, RT/RW 024/008, Beringin, Kapuas, Sanggau', 'Tukang Antar Koran', 500000, 2, 'sedang', 'sembako', -0.07088082929060342, 109.36113953590394, 160.17, 'diterima', 'normal', NULL, '', 'mandiri', '2026-06-09 10:10:05', '2026-06-09 11:56:27'),
(2, 6, 5, 'caca', '124854', '410154', 'Gg. Kasturi Raja, Jl. Kenari', 'Mahasiswa', 0, 0, 'baik', 'uang_tunai', -0.020854501391015294, 109.33250427246095, 259.51, 'diterima', 'normal', NULL, '', 'mandiri', '2026-06-09 10:57:26', '2026-06-09 14:53:12'),
(4, NULL, 2, 'angel1', '123456', '123456', '1234', 'wwwwww', 0, 0, 'sedang', 'uang_tunai', -0.025105475576032914, 109.33623790740968, 128.27, 'diterima', 'normal', NULL, '', 'admin_rumah_ibadah', '2026-06-09 11:27:12', '2026-06-09 11:28:31'),
(5, NULL, 2, 'angel2', '123', '123', 'ssss', 'sssss', 0, 0, 'sedang', 'uang_tunai', -0.025245714451433383, 109.33630228042604, 122.41, 'diterima', 'normal', NULL, '', 'admin_rumah_ibadah', '2026-06-09 11:28:22', '2026-06-09 11:28:29'),
(6, 4, 1, 'k', 'g', 'u', 'k', 'i', 5, 7, 'baik', 'zakat', -0.06864918884517689, 109.3591332435608, 218.94, 'dibatalkan', 'disetujui', 'h', 'h', 'admin_rumah_ibadah', '2026-06-09 11:57:59', '2026-06-09 12:19:58'),
(7, 9, 5, 'penerima03', '124854', '123', 'Gang Merak II, Mariana, Pontianak Kota, Pontianak, Kalimantan Barat, Kalimantan, 78112, Indonesia', 'Mahasiswa', 50000, 1, 'sedang', 'sembako', -0.02033240043514327, 109.33441936969759, 136.81, 'diterima', 'normal', NULL, '', 'admin_rumah_ibadah', '2026-06-09 14:52:40', '2026-06-09 14:53:12'),
(8, 10, 1, 'penerima04', '222223432', '13334341', 'Komplek Mitra Indah Utama I, Bangka Belitung Darat, Pontianak Tenggara, Pontianak, Kalimantan Barat, Kalimantan, 78124, Indonesia', 'wiraswasta', 1000000, 3, 'baik', 'sembako', -0.06962094072669782, 109.35720741748811, 299.36, 'diterima', 'normal', NULL, '', 'admin_rumah_ibadah', '2026-06-10 01:22:26', '2026-06-10 01:22:34'),
(9, 11, 1, 'penerima05', '111111111111111', '11111111111111111', 'Nilam Permai, Sungai Raya, Kubu Raya, Kalimantan Barat, Kalimantan, 78124, Indonesia', 'disabilitas', 0, 1, 'rusak', 'uang_tunai', -0.07270815709759908, 109.3613098561764, 299.07, 'diterima', 'normal', NULL, '', 'admin_rumah_ibadah', '2026-06-10 01:25:25', '2026-06-10 01:25:37');
-- --------------------------------------------------------
--
-- Table structure for table `penyaluran_bantuan`
--
CREATE TABLE `penyaluran_bantuan` (
`id` int NOT NULL,
`program_id` int DEFAULT NULL,
`penerima_id` int NOT NULL,
`rumah_ibadah_id` int NOT NULL,
`admin_id` int NOT NULL,
`jenis_bantuan` enum('zakat','sembako','uang_tunai','lainnya') COLLATE utf8mb4_unicode_ci NOT NULL,
`nominal` int DEFAULT NULL,
`jumlah_item` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`bantuan_ke` int DEFAULT NULL,
`keterangan` text COLLATE utf8mb4_unicode_ci,
`tanggal_penyaluran` date NOT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `penyaluran_bantuan`
--
INSERT INTO `penyaluran_bantuan` (`id`, `program_id`, `penerima_id`, `rumah_ibadah_id`, `admin_id`, `jenis_bantuan`, `nominal`, `jumlah_item`, `bantuan_ke`, `keterangan`, `tanggal_penyaluran`, `created_at`) VALUES
(1, 5, 5, 2, 5, 'zakat', 10000, NULL, 1, 'gg', '2026-06-09', '2026-06-09 11:33:36');
-- --------------------------------------------------------
--
-- Table structure for table `program_bantuan`
--
CREATE TABLE `program_bantuan` (
`id` int NOT NULL,
`rumah_ibadah_id` int DEFAULT NULL,
`nama_program` varchar(150) COLLATE utf8mb4_unicode_ci NOT NULL,
`jenis_bantuan` enum('zakat','sembako','uang_tunai','lainnya') COLLATE utf8mb4_unicode_ci NOT NULL,
`deskripsi` text COLLATE utf8mb4_unicode_ci,
`bulan` int NOT NULL,
`tahun` int NOT NULL,
`target_frekuensi` int DEFAULT '1',
`nominal` int DEFAULT NULL,
`jumlah_item` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status_program` enum('aktif','selesai','dibatalkan') COLLATE utf8mb4_unicode_ci DEFAULT 'aktif',
`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_unicode_ci;
--
-- Dumping data for table `program_bantuan`
--
INSERT INTO `program_bantuan` (`id`, `rumah_ibadah_id`, `nama_program`, `jenis_bantuan`, `deskripsi`, `bulan`, `tahun`, `target_frekuensi`, `nominal`, `jumlah_item`, `status_program`, `created_at`, `updated_at`) VALUES
(3, 2, 'a', 'uang_tunai', 'a', 6, 2026, 1, NULL, NULL, 'aktif', '2026-06-09 11:29:28', '2026-06-09 11:29:28'),
(4, 2, 'b', 'uang_tunai', 'b', 6, 2026, 1, NULL, NULL, 'aktif', '2026-06-09 11:29:47', '2026-06-09 11:29:47'),
(5, 2, 'c', 'sembako', 'c', 6, 2026, 1, NULL, NULL, 'aktif', '2026-06-09 11:30:54', '2026-06-09 11:30:54');
-- --------------------------------------------------------
--
-- Table structure for table `riwayat_status`
--
CREATE TABLE `riwayat_status` (
`id` int NOT NULL,
`penerima_id` int NOT NULL,
`user_id` int NOT NULL,
`status_lama` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status_baru` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`catatan` text COLLATE utf8mb4_unicode_ci,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `riwayat_status`
--
INSERT INTO `riwayat_status` (`id`, `penerima_id`, `user_id`, `status_lama`, `status_baru`, `catatan`, `created_at`) VALUES
(1, 1, 4, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 10:10:05'),
(2, 1, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 10:48:16'),
(3, 2, 6, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 10:57:26'),
(4, 2, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 10:59:04'),
(7, 4, 5, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 11:27:12'),
(8, 5, 1, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 11:28:22'),
(9, 5, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 11:28:29'),
(10, 4, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 11:28:31'),
(11, 6, 1, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 11:57:59'),
(12, 6, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 11:58:09'),
(13, 6, 1, 'diterima', 'dibatalkan', 'jotok 2 rmh', '2026-06-09 11:58:22'),
(14, 6, 1, 'dibatalkan', 'hapus_disetujui', 'h', '2026-06-09 12:19:58'),
(15, 7, 1, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 14:52:40'),
(16, 7, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 14:52:49'),
(17, 8, 1, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-10 01:22:26'),
(18, 8, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-10 01:22:34'),
(19, 9, 2, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-10 01:25:25'),
(20, 9, 2, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-10 01:25:37');
-- --------------------------------------------------------
--
-- Table structure for table `rumah_ibadah`
--
CREATE TABLE `rumah_ibadah` (
`id` int NOT NULL,
`user_id` int DEFAULT NULL,
`nama_rumah_ibadah` varchar(150) COLLATE utf8mb4_unicode_ci NOT NULL,
`agama` enum('islam','kristen','katolik','hindu','buddha','konghucu') COLLATE utf8mb4_unicode_ci NOT NULL,
`alamat` text COLLATE utf8mb4_unicode_ci NOT NULL,
`nomor_hp` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`latitude` double NOT NULL,
`longitude` double NOT NULL,
`status_pengajuan` enum('menunggu','diterima','ditolak') COLLATE utf8mb4_unicode_ci DEFAULT 'menunggu',
`status_hapus` enum('normal','menunggu','disetujui','ditolak') COLLATE utf8mb4_unicode_ci DEFAULT 'normal',
`alasan_hapus` text COLLATE utf8mb4_unicode_ci,
`keterangan` text COLLATE utf8mb4_unicode_ci,
`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_unicode_ci;
--
-- Dumping data for table `rumah_ibadah`
--
INSERT INTO `rumah_ibadah` (`id`, `user_id`, `nama_rumah_ibadah`, `agama`, `alamat`, `nomor_hp`, `latitude`, `longitude`, `status_pengajuan`, `status_hapus`, `alasan_hapus`, `keterangan`, `created_at`, `updated_at`) VALUES
(1, 2, 'Gereja Katolik Paroki St. Sesilia', 'katolik', 'Jl. Sungai Raya Dalam', '0812345678913', -0.07052077848657445, 109.3597447872162, 'diterima', 'normal', NULL, '', '2026-06-09 10:11:48', '2026-06-09 10:48:18'),
(2, 5, 'GPIB', 'kristen', 'Jl. Kartini, Pontianak', '0812345678913', -0.02508443513561694, 109.33739125728609, 'diterima', 'normal', NULL, '', '2026-06-09 10:54:13', '2026-06-09 10:59:11'),
(4, 8, 'Gereja Elim', 'kristen', 'Jalan Rajawali, Tengah, Pontianak Kota, Pontianak, Kalimantan Barat, Kalimantan, 78112, Indonesia', '0812345678913', -0.02152069970142388, 109.33472514152527, 'ditolak', 'disetujui', 's', 's', '2026-06-09 14:50:50', '2026-06-09 14:51:16'),
(5, 8, 'Gereja Elim', 'kristen', 'Jalan Rajawali, Tengah, Pontianak Kota, Pontianak, Kalimantan Barat, Kalimantan, 78112, Indonesia', '0812345678913', -0.02151988492191236, 109.33474123477937, 'diterima', 'normal', NULL, 'Ditambahkan langsung oleh super admin.', '2026-06-09 14:53:12', '2026-06-09 14:53:12');
-- --------------------------------------------------------
--
-- Table structure for table `users`
--
CREATE TABLE `users` (
`id` int NOT NULL,
`nama` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`username` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`role` enum('super_admin','admin_rumah_ibadah','penerima') COLLATE utf8mb4_unicode_ci NOT NULL,
`status_akun` enum('aktif','nonaktif') COLLATE utf8mb4_unicode_ci DEFAULT 'aktif',
`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_unicode_ci;
--
-- Dumping data for table `users`
--
INSERT INTO `users` (`id`, `nama`, `username`, `password`, `role`, `status_akun`, `created_at`, `updated_at`) VALUES
(1, 'Super Admin Dinas Sosial', 'superadmin', '$2y$12$79Vc.m66x5uIIIt4rifL0OIY3qrzzXmgktTW3Y1QEwWB9GMaeLFni', 'super_admin', 'aktif', '2026-06-09 10:04:24', '2026-06-09 14:48:23'),
(2, 'admingerejasesilia', 'admingerejasesilia', '$2y$10$Pt4VTj7yDLt1xabM5WwGJuBo.gat807fkV0Q.KiGKv.V0tRbo1SHW', 'admin_rumah_ibadah', 'aktif', '2026-06-09 10:06:38', '2026-06-10 01:23:46'),
(4, 'penerima01', 'penerima01', '$2y$12$79Vc.m66x5uIIIt4rifL0OIY3qrzzXmgktTW3Y1QEwWB9GMaeLFni', 'penerima', 'nonaktif', '2026-06-09 10:08:28', '2026-06-10 01:18:35'),
(5, 'admingpib', 'admingpib', '$2y$12$79Vc.m66x5uIIIt4rifL0OIY3qrzzXmgktTW3Y1QEwWB9GMaeLFni', 'admin_rumah_ibadah', 'nonaktif', '2026-06-09 10:52:35', '2026-06-09 14:47:58'),
(6, 'penerima02', 'penerima02', '$2y$12$79Vc.m66x5uIIIt4rifL0OIY3qrzzXmgktTW3Y1QEwWB9GMaeLFni', 'penerima', 'aktif', '2026-06-09 10:55:15', '2026-06-09 14:47:48'),
(8, 'Admin Gereja Elim', 'admingerejaelim', '$2y$10$hiPcHEFAb2lPq3WKLra1MuZ8220s/UY.3PG24ZIrmbxrbp2imboGO', 'admin_rumah_ibadah', 'aktif', '2026-06-09 14:49:55', '2026-06-09 14:49:55'),
(9, 'penerima03', 'penerima03', '$2y$10$.50kERk9OSPvJhe.9IFxd.3qi2j5spTxtM36Kx1mI7Q3HkeVlyzvm', 'penerima', 'nonaktif', '2026-06-09 14:51:50', '2026-06-10 01:18:20'),
(10, 'penerima04', 'penerima04', '$2y$10$Q0yfgX7/KnCa4sxnz/f5YupK2aVyKWLDPO8CD/mdQlCSOTRbzLBli', 'penerima', 'aktif', '2026-06-10 01:20:04', '2026-06-10 01:20:04'),
(11, 'penerima05', 'penerima05', '$2y$10$0UJ97IYsDsS/K/qJbHJsiO938TFSU2AorU0fqxAawnbpwhJN/qBqi', 'penerima', 'aktif', '2026-06-10 01:21:01', '2026-06-10 01:21:01');
--
-- Indexes for dumped tables
--
--
-- Indexes for table `penerima_bantuan`
--
ALTER TABLE `penerima_bantuan`
ADD PRIMARY KEY (`id`),
ADD KEY `idx_pb_user` (`user_id`),
ADD KEY `idx_pb_ri` (`rumah_ibadah_id`),
ADD KEY `idx_pb_status` (`status_pengajuan`),
ADD KEY `idx_pb_jenis` (`jenis_bantuan`);
--
-- Indexes for table `penyaluran_bantuan`
--
ALTER TABLE `penyaluran_bantuan`
ADD PRIMARY KEY (`id`),
ADD KEY `idx_pen_program` (`program_id`),
ADD KEY `idx_pen_penerima` (`penerima_id`),
ADD KEY `idx_pen_ri` (`rumah_ibadah_id`),
ADD KEY `idx_pen_admin` (`admin_id`),
ADD KEY `idx_pen_tanggal` (`tanggal_penyaluran`);
--
-- Indexes for table `program_bantuan`
--
ALTER TABLE `program_bantuan`
ADD PRIMARY KEY (`id`),
ADD KEY `idx_prog_ri` (`rumah_ibadah_id`),
ADD KEY `idx_prog_periode` (`bulan`,`tahun`),
ADD KEY `idx_prog_status` (`status_program`);
--
-- Indexes for table `riwayat_status`
--
ALTER TABLE `riwayat_status`
ADD PRIMARY KEY (`id`),
ADD KEY `idx_rw_penerima` (`penerima_id`),
ADD KEY `idx_rw_user` (`user_id`);
--
-- Indexes for table `rumah_ibadah`
--
ALTER TABLE `rumah_ibadah`
ADD PRIMARY KEY (`id`),
ADD KEY `idx_ri_user` (`user_id`),
ADD KEY `idx_ri_status` (`status_pengajuan`),
ADD KEY `idx_ri_agama` (`agama`);
--
-- Indexes for table `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `username` (`username`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `penerima_bantuan`
--
ALTER TABLE `penerima_bantuan`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=10;
--
-- AUTO_INCREMENT for table `penyaluran_bantuan`
--
ALTER TABLE `penyaluran_bantuan`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT for table `program_bantuan`
--
ALTER TABLE `program_bantuan`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
--
-- AUTO_INCREMENT for table `riwayat_status`
--
ALTER TABLE `riwayat_status`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=21;
--
-- AUTO_INCREMENT for table `rumah_ibadah`
--
ALTER TABLE `rumah_ibadah`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
--
-- AUTO_INCREMENT for table `users`
--
ALTER TABLE `users`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=12;
--
-- Constraints for dumped tables
--
--
-- Constraints for table `penerima_bantuan`
--
ALTER TABLE `penerima_bantuan`
ADD CONSTRAINT `fk_pb_ri` FOREIGN KEY (`rumah_ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,
ADD CONSTRAINT `fk_pb_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
--
-- Constraints for table `penyaluran_bantuan`
--
ALTER TABLE `penyaluran_bantuan`
ADD CONSTRAINT `fk_pen_admin` FOREIGN KEY (`admin_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `fk_pen_penerima` FOREIGN KEY (`penerima_id`) REFERENCES `penerima_bantuan` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `fk_pen_program` FOREIGN KEY (`program_id`) REFERENCES `program_bantuan` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,
ADD CONSTRAINT `fk_pen_ri` FOREIGN KEY (`rumah_ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
--
-- Constraints for table `program_bantuan`
--
ALTER TABLE `program_bantuan`
ADD CONSTRAINT `fk_prog_ri` FOREIGN KEY (`rumah_ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
--
-- Constraints for table `riwayat_status`
--
ALTER TABLE `riwayat_status`
ADD CONSTRAINT `fk_rw_penerima` FOREIGN KEY (`penerima_id`) REFERENCES `penerima_bantuan` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `fk_rw_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
--
-- Constraints for table `rumah_ibadah`
--
ALTER TABLE `rumah_ibadah`
ADD CONSTRAINT `fk_ri_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
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,723 @@
# PANDUAN INSTALASI LOCALHOST SAMPAI HOSTING
**Project:** 05 - WebGIS Poverty Mapping
**Teknologi:** PHP Native, MySQL/MariaDB, Leaflet.js, HTML, CSS, JavaScript Vanilla
**Folder lokal:** `webgis-poverty/05/`
**URL lokal:** `http://localhost/webgis-poverty/05/`
---
## 1. Ringkasan Aplikasi
Aplikasi ini adalah WebGIS Poverty Mapping untuk memetakan penerima bantuan dan rumah ibadah sebagai pemberi/penyalur bantuan. Sistem memiliki tiga role utama:
1. **Super Admin**
- Mengelola seluruh data.
- Menyetujui/menolak rumah ibadah.
- Menyetujui/menolak penerima bantuan.
- Mengelola akun admin rumah ibadah dan penerima.
- Mengelola program bantuan dan laporan.
- Menyetujui/menolak pengajuan hapus marker/pin.
2. **Admin Rumah Ibadah**
- Mengelola rumah ibadah miliknya.
- Mengelola akun penerima.
- Mengajukan/mengelola penerima dalam radius rumah ibadahnya.
- Membuat program bantuan.
- Mencatat penyaluran bantuan.
- Melihat laporan dan riwayat sesuai rumah ibadah yang dikelola.
3. **Penerima**
- Registrasi akun.
- Mengajukan titik lokasi rumah melalui peta.
- Melihat status pengajuan.
- Melihat riwayat bantuan.
- Mengajukan hapus pin lokasi miliknya.
Aturan utama sistem:
- Radius tanggung jawab rumah ibadah adalah **300 meter**.
- Penerima otomatis dikaitkan ke **rumah ibadah terdekat** dalam radius 300 meter.
- Satu akun penerima hanya boleh memiliki **satu pin aktif**.
- Admin rumah ibadah dapat mengelola lebih dari satu rumah ibadah.
- Data yang sudah punya relasi tidak langsung dihapus permanen, tetapi dinonaktifkan atau diberi status hapus agar riwayat audit tetap aman.
---
## 2. Struktur File Project
Struktur project terbaru:
```text
05/
├── index.php
├── login.php
├── logout.php
├── register.php
├── dashboard.php
├── koneksi.php
├── auth.php
├── ambil_data_peta.php
├── ambil_laporan_penyaluran.php
├── ambil_program_bantuan.php
├── ambil_riwayat.php
├── ambil_user.php
├── ajukan_rumah_ibadah.php
├── hapus_rumah_ibadah.php
├── update_status_rumah_ibadah.php
├── tambah_penerima.php
├── edit_penerima.php
├── hapus_penerima.php
├── update_status_penerima.php
├── tambah_program_bantuan.php
├── edit_program_bantuan.php
├── hapus_program_bantuan.php
├── tambah_penyaluran.php
├── tambah_user.php
├── edit_user.php
├── hapus_user.php
├── backup_sql/
│ ├── schema.sql
│ ├── migration_fix_multi_ri_pin.sql
│ ├── migration_revisi_persetujuan_hapus.sql
│ └── migration_revisi_sinkronisasi.sql
├── RANGKUMAN_PENTING_PROJECT_05.md
└── PANDUAN_INSTALASI_LOCALHOST_SAMPAI_HOSTING.md
```
---
## 3. Persiapan Localhost dengan Laragon
1. Buka Laragon.
2. Klik **Start All** agar Apache dan MySQL/MariaDB aktif.
3. Buka folder Laragon:
```text
C:\laragon\www\
```
4. Buat folder project:
```text
C:\laragon\www\webgis-poverty\05\
```
5. Ekstrak isi ZIP project ke folder `05`.
6. Pastikan file `index.php`, `login.php`, `dashboard.php`, dan `koneksi.php` berada langsung di dalam folder `05`, bukan di dalam folder tambahan.
Struktur yang benar:
```text
C:\laragon\www\webgis-poverty\05\index.php
C:\laragon\www\webgis-poverty\05\login.php
C:\laragon\www\webgis-poverty\05\dashboard.php
C:\laragon\www\webgis-poverty\05\koneksi.php
```
Struktur yang salah:
```text
C:\laragon\www\webgis-poverty\05\05\index.php
```
Jika terjadi struktur salah, pindahkan semua file dari folder `05\05\` ke folder `05\` utama.
---
## 4. Import Database di Localhost
### 4.1 Buka phpMyAdmin
1. Jalankan Laragon.
2. Klik **Database** atau buka browser:
```text
http://localhost/phpmyadmin
```
3. Login phpMyAdmin Laragon:
```text
Username : root
Password : kosongkan
```
### 4.2 Pilihan File SQL
Ada dua pilihan import database:
#### Opsi A - Database Demo Lengkap
Gunakan file:
```text
05_povertydb.sql
```
Opsi ini cocok jika ingin langsung ada data contoh, akun demo, rumah ibadah, penerima, program, dan laporan.
#### Opsi B - Struktur Kosong dari ZIP
Gunakan file:
```text
backup_sql/schema.sql
```
Opsi ini cocok jika ingin membuat database dari awal. Setelah import, data contoh mungkin belum lengkap.
Jika ada file `05_povertydb.sql`, disarankan memakai file tersebut untuk demo karena akun-akun contoh sudah tersedia.
### 4.3 Cara Import
1. Buka phpMyAdmin.
2. Klik tab **Import**.
3. Pilih file SQL.
4. Klik **Go**.
5. Pastikan database `05_povertydb` berhasil dibuat.
6. Pastikan tabel berikut muncul:
```text
users
rumah_ibadah
penerima_bantuan
program_bantuan
penyaluran_bantuan
riwayat_status
```
---
## 5. Konfigurasi Database Localhost
Buka file:
```text
koneksi.php
```
Untuk Laragon, gunakan konfigurasi berikut:
```php
$host = "localhost";
$user = "root";
$pass = "";
$db = "05_povertydb";
```
Simpan file tersebut.
---
## 6. Akun Login Default
Semua password akun demo adalah:
```text
password
```
Akun utama:
```text
Role : Super Admin
Username : superadmin
Password : password
```
Contoh akun admin rumah ibadah jika memakai database demo:
```text
Username : admingerejasesilia
Password : password
Role : admin_rumah_ibadah
```
```text
Username : admingerejaelim
Password : password
Role : admin_rumah_ibadah
```
Contoh akun penerima jika memakai database demo:
```text
Username : penerima02
Password : password
Role : penerima
```
```text
Username : penerima04
Password : password
Role : penerima
```
```text
Username : penerima05
Password : password
Role : penerima
```
Catatan:
- Akun dengan status `nonaktif` tidak bisa login.
- Password disimpan menggunakan `password_hash()` dan diverifikasi menggunakan `password_verify()`.
- Jangan menulis password plain text langsung di database.
---
## 7. Menjalankan Aplikasi di Localhost
1. Pastikan Laragon sudah **Start All**.
2. Buka browser.
3. Akses:
```text
http://localhost/webgis-poverty/05/
```
4. Jika diarahkan ke login, masuk menggunakan:
```text
Username : superadmin
Password : password
```
5. Setelah login, menu utama:
```text
index.php = halaman peta WebGIS
dashboard.php = dashboard, akun, program, penyaluran, laporan, dan riwayat
logout.php = keluar dari sistem
```
---
## 8. Pengujian Fitur Localhost
### 8.1 Login Super Admin
1. Buka halaman login.
2. Masukkan username `superadmin`.
3. Masukkan password `password`.
4. Pastikan masuk ke dashboard.
### 8.2 Kelola Akun
1. Login sebagai super admin.
2. Buka **Dashboard**.
3. Cari bagian **Kelola Akun Pengguna**.
4. Tambah akun admin rumah ibadah atau penerima.
5. Gunakan password minimal 6 karakter. Untuk demo, isi `password`.
6. Simpan.
7. Coba login menggunakan akun baru.
### 8.3 Tambah Rumah Ibadah
1. Login sebagai super admin.
2. Buka **Peta**.
3. Klik lokasi pada peta.
4. Pilih **Tambah Rumah Ibadah**.
5. Isi data rumah ibadah.
6. Masukkan username admin rumah ibadah.
7. Simpan.
8. Jika super admin yang menambah, status rumah ibadah langsung dapat diterima.
### 8.4 Pengajuan Rumah Ibadah oleh Admin
1. Login sebagai admin rumah ibadah.
2. Buka peta.
3. Klik lokasi rumah ibadah.
4. Pilih **Ajukan Rumah Ibadah**.
5. Isi data.
6. Pengajuan akan berstatus `menunggu`.
7. Super admin harus menyetujui agar rumah ibadah aktif.
### 8.5 Pengajuan Penerima
1. Login sebagai penerima.
2. Buka peta.
3. Klik lokasi rumah.
4. Pilih **Simpan Pengajuan**.
5. Isi data penerima.
6. Sistem mencari rumah ibadah terdekat dalam radius 300 meter.
7. Status awal penerima adalah `menunggu`.
8. Super admin atau admin rumah ibadah terkait dapat menyetujui/menolak.
### 8.6 Program Bantuan
1. Login sebagai super admin atau admin rumah ibadah.
2. Buka dashboard.
3. Isi form **Buat Program Bantuan**.
4. Pilih rumah ibadah, jenis bantuan, bulan, tahun, target frekuensi, nominal/item, dan deskripsi.
5. Simpan.
### 8.7 Penyaluran Bantuan
1. Pastikan penerima sudah berstatus `diterima`.
2. Pastikan penerima berada dalam rumah ibadah yang sama dengan program.
3. Buka form **Catat Penyaluran Bantuan**.
4. Pilih program.
5. Pilih penerima.
6. Isi tanggal dan keterangan.
7. Simpan.
8. Sistem otomatis menghitung bantuan ke-berapa pada program tersebut.
### 8.8 Laporan Bulanan
1. Buka dashboard.
2. Cari bagian **Monitoring Penyaluran Per Periode**.
3. Pilih bulan dan tahun.
4. Gunakan filter rumah ibadah, agama, program, jenis bantuan, dan status penyaluran.
5. Klik tampilkan/muat laporan.
6. Laporan menampilkan penerima yang belum diberi, sudah diberi, belum memenuhi target, sesuai target, atau melebihi target.
### 8.9 Pengajuan Hapus Marker/Pin
1. Super admin dapat menghapus marker langsung.
2. Admin rumah ibadah hanya dapat mengajukan hapus rumah ibadah miliknya atau pin penerima yang dikelola.
3. Penerima dapat mengajukan hapus pin miliknya sendiri.
4. Pengajuan hapus akan masuk ke panel persetujuan super admin.
5. Jika disetujui, marker/pin hilang dari peta aktif.
---
## 9. Persiapan Sebelum Hosting
Sebelum upload ke hosting, pastikan:
```text
[ ] Aplikasi berjalan di localhost.
[ ] Database berhasil diimport.
[ ] Login superadmin berhasil.
[ ] Peta muncul.
[ ] Dashboard muncul.
[ ] Tambah akun berhasil.
[ ] Tambah rumah ibadah berhasil.
[ ] Tambah penerima berhasil.
[ ] Program bantuan berhasil dibuat.
[ ] Penyaluran bantuan berhasil dicatat.
[ ] Laporan berhasil tampil.
```
File yang wajib diupload ke hosting:
```text
index.php
login.php
logout.php
register.php
dashboard.php
koneksi.php
auth.php
ambil_data_peta.php
ambil_laporan_penyaluran.php
ambil_program_bantuan.php
ambil_riwayat.php
ambil_user.php
ajukan_rumah_ibadah.php
hapus_rumah_ibadah.php
update_status_rumah_ibadah.php
tambah_penerima.php
edit_penerima.php
hapus_penerima.php
update_status_penerima.php
tambah_program_bantuan.php
edit_program_bantuan.php
hapus_program_bantuan.php
tambah_penyaluran.php
tambah_user.php
edit_user.php
hapus_user.php
```
File yang tidak wajib diupload ke hosting setelah database berhasil diimport:
```text
backup_sql/
05_povertydb.sql
RANGKUMAN_PENTING_PROJECT_05.md
PANDUAN_INSTALASI_LOCALHOST_SAMPAI_HOSTING.md
file ZIP backup
```
Untuk keamanan, file SQL dan ZIP backup sebaiknya tidak dibiarkan terbuka di `public_html`.
---
## 10. Upload ke Hosting cPanel
1. Login ke cPanel hosting.
2. Buka **File Manager**.
3. Masuk ke folder:
```text
public_html
```
4. Buat folder:
```text
webgis-poverty
```
5. Di dalamnya buat folder:
```text
05
```
6. Upload file aplikasi ke:
```text
public_html/webgis-poverty/05/
```
7. Jika upload berupa ZIP, klik kanan ZIP lalu pilih **Extract**.
8. Pastikan `index.php` berada langsung di folder `05`.
URL setelah upload:
```text
https://namadomain.com/webgis-poverty/05/
```
---
## 11. Membuat Database Hosting
1. Buka menu **MySQL Databases** di cPanel.
2. Buat database baru, contoh:
```text
cpaneluser_05_povertydb
```
3. Buat user database baru, contoh:
```text
cpaneluser_userdb
```
4. Buat password database.
5. Tambahkan user ke database.
6. Berikan akses **ALL PRIVILEGES**.
7. Catat data berikut:
```text
Host database : localhost
Nama database : cpaneluser_05_povertydb
Username database : cpaneluser_userdb
Password database : password database hosting
```
---
## 12. Import Database di Hosting
1. Buka **phpMyAdmin** dari cPanel.
2. Pilih database hosting yang sudah dibuat.
3. Klik tab **Import**.
4. Upload file SQL.
5. Klik **Go**.
6. Pastikan semua tabel muncul.
Jika hosting menolak perintah:
```sql
CREATE DATABASE IF NOT EXISTS `05_povertydb`;
USE `05_povertydb`;
```
maka hapus atau komentari dua bagian tersebut dari file SQL, lalu import ulang pada database yang sudah dipilih di phpMyAdmin.
---
## 13. Mengubah koneksi.php untuk Hosting
Buka `koneksi.php`, lalu ubah konfigurasi menjadi data hosting.
Contoh:
```php
$host = "localhost";
$user = "cpaneluser_userdb";
$pass = "PasswordDatabaseHosting";
$db = "cpaneluser_05_povertydb";
```
Simpan file.
---
## 14. Akses Aplikasi Online
Jika foldernya:
```text
public_html/webgis-poverty/05/
```
maka URL biasanya:
```text
https://namadomain.com/webgis-poverty/05/
```
Jika langsung diletakkan di `public_html`, URL menjadi:
```text
https://namadomain.com/
```
Jika memakai subdomain, misalnya `gis.namadomain.com`, arahkan document root subdomain ke folder aplikasi.
---
## 15. Checklist Setelah Hosting
```text
[ ] Halaman login terbuka.
[ ] Login superadmin berhasil.
[ ] Dashboard terbuka.
[ ] Peta Leaflet muncul.
[ ] Marker rumah ibadah muncul.
[ ] Marker penerima muncul.
[ ] Tambah akun berhasil.
[ ] Tambah/ajukan rumah ibadah berhasil.
[ ] Setujui/tolak rumah ibadah berhasil.
[ ] Tambah/ajukan penerima berhasil.
[ ] Setujui/tolak penerima berhasil.
[ ] Buat program bantuan berhasil.
[ ] Catat penyaluran bantuan berhasil.
[ ] Laporan bulanan tampil.
[ ] Riwayat tampil.
[ ] Logout berhasil.
[ ] Aplikasi bisa dibuka dari HP.
```
---
## 16. Troubleshooting
### 16.1 Halaman Blank Putih
Penyebab umum:
- Error PHP.
- Koneksi database gagal.
- File belum lengkap.
- Ada output sebelum `session_start()`.
Solusi:
- Cek file `error_log` di hosting.
- Pastikan `koneksi.php` benar.
- Pastikan semua file PHP terupload.
### 16.2 Database Connection Failed
Solusi:
- Cek nama database.
- Cek username database.
- Cek password database.
- Cek host database, biasanya `localhost`.
- Pastikan user database sudah diberi **ALL PRIVILEGES**.
### 16.3 Login Gagal
Solusi:
- Pastikan username benar.
- Pastikan akun berstatus `aktif`.
- Pastikan password adalah `password` untuk akun demo.
- Pastikan password di database dibuat memakai `password_hash()`.
### 16.4 Peta Tidak Muncul
Solusi:
- Pastikan internet aktif karena Leaflet memakai CDN.
- Pastikan elemen peta memiliki tinggi.
- Buka Console browser untuk melihat error JavaScript.
### 16.5 Marker Tidak Muncul
Solusi:
- Cek latitude dan longitude.
- Cek status pengajuan.
- Data `ditolak` atau `status_hapus='disetujui'` memang tidak tampil sebagai marker aktif.
- Cek response `ambil_data_peta.php` melalui tab Network browser.
### 16.6 Import SQL Gagal
Solusi:
- Pastikan database sudah dipilih sebelum import.
- Jika error `CREATE DATABASE`, hapus baris `CREATE DATABASE` dan `USE`.
- Jika error `IF NOT EXISTS` pada `ALTER TABLE`, gunakan file migration versi kompatibel.
- Jika ada pin aktif ganda, rapikan data penerima terlebih dahulu sebelum membuat unique key satu pin aktif.
### 16.7 Fetch Error
Solusi:
- Pastikan file PHP tujuan ada.
- Pastikan path fetch memakai path relatif, misalnya:
```javascript
fetch('ambil_data_peta.php')
```
Jangan memakai path localhost saat sudah hosting:
```javascript
fetch('http://localhost/webgis-poverty/05/ambil_data_peta.php')
```
---
## 17. Keamanan Dasar Setelah Online
Setelah aplikasi online:
1. Ganti password akun penting jika sudah masuk tahap produksi.
2. Jangan biarkan file `.sql` dan `.zip` di folder publik.
3. Jangan tampilkan detail error kepada pengguna umum.
4. Gunakan HTTPS.
5. Pastikan semua akun admin menggunakan password yang kuat.
6. Nonaktifkan akun yang tidak digunakan.
7. Backup database secara berkala.
---
## 18. Ringkasan Akhir
Urutan instalasi yang benar:
```text
1. Ekstrak project ke C:\laragon\www\webgis-poverty\05\
2. Start All di Laragon.
3. Import database melalui phpMyAdmin.
4. Cek koneksi.php.
5. Buka http://localhost/webgis-poverty/05/
6. Login superadmin / password.
7. Uji peta, dashboard, akun, rumah ibadah, penerima, program, penyaluran, dan laporan.
8. Upload file PHP ke hosting.
9. Buat database hosting.
10. Import SQL di hosting.
11. Ubah koneksi.php sesuai hosting.
12. Hapus file SQL/ZIP dari public_html setelah selesai.
```
+230
View File
@@ -0,0 +1,230 @@
# RANGKUMAN PENTING PROJECT 05 - WebGIS Poverty Mapping
File ini merangkum catatan penting saja dari beberapa file catatan revisi/migrasi.
---
## 1. File SQL yang Penting Disimpan
Simpan file SQL berikut sebagai backup database dan migrasi:
1. `schema.sql`
- Struktur database dasar.
- Dipakai jika ingin instal ulang database dari awal.
2. `migration_fix_multi_ri_pin.sql`
- Memperbaiki masalah admin rumah ibadah yang punya lebih dari satu rumah ibadah.
- Menambahkan aturan satu akun penerima hanya boleh punya satu pin aktif.
3. `migration_revisi_persetujuan_hapus.sql`
- Menambahkan fitur pengajuan/persetujuan hapus marker rumah ibadah dan pin penerima.
4. `migration_revisi_sinkronisasi.sql`
- Memperbaiki sinkronisasi penerima dengan rumah ibadah terdekat.
Rekomendasi:
- Jangan letakkan file SQL di folder publik saat hosting.
- Simpan di folder backup lokal, misalnya `backup_sql/`.
---
## 2. Perbaikan Paling Penting pada Sistem
### A. Admin Rumah Ibadah Multi-RI
Masalah lama:
- Admin rumah ibadah hanya terbaca punya 1 rumah ibadah karena query memakai `LIMIT 1`.
- Akibatnya, pin/marker dari rumah ibadah lain tidak muncul.
Perbaikan:
- Sistem sekarang mendukung admin yang mengelola lebih dari satu rumah ibadah.
- Query memakai daftar ID rumah ibadah, bukan hanya 1 ID.
Dampak:
- Pin penerima dan rumah ibadah milik admin tampil lebih lengkap.
- Laporan, riwayat, program bantuan, dan penyaluran mengikuti semua rumah ibadah milik admin.
---
### B. Satu Akun Penerima Hanya Boleh Satu Pin Aktif
Masalah lama:
- Satu akun penerima bisa punya lebih dari satu pin aktif.
- Ini menyebabkan data tidak sinkron dan penerima bisa muncul di beberapa rumah ibadah.
Perbaikan:
- Sebelum menambah pin, sistem mengecek apakah user penerima sudah punya pin aktif.
- Pin aktif yang dimaksud adalah status `menunggu` atau `diterima`, dan belum disetujui hapus.
- Database juga ditambah unique key agar satu user tidak bisa punya lebih dari satu pin aktif.
---
### C. Sinkronisasi Radius 300 Meter
Masalah lama:
- Penerima bisa terhubung ke rumah ibadah yang salah.
- Jika radius beberapa rumah ibadah saling beririsan, sistem belum selalu memilih yang paling dekat.
Perbaikan:
- Penerima dihitung ulang berdasarkan rumah ibadah aktif terdekat dalam radius 300 meter.
- Jika rumah ibadah dihapus, penerima dilepas lalu dicari ulang ke rumah ibadah aktif terdekat lain.
---
### D. Persetujuan Hapus Marker/Pin
Alur baru:
- Super admin bisa menghapus marker rumah ibadah dan pin penerima secara langsung.
- Admin rumah ibadah hanya bisa mengajukan hapus rumah ibadah/penerima.
- Penerima bisa mengajukan hapus pin miliknya sendiri.
- Pengajuan hapus harus disetujui super admin.
Dampak:
- Data tidak langsung hilang sembarangan.
- Ada kontrol dari super admin.
- Marker yang sudah disetujui hapus tidak tampil lagi di peta dan laporan aktif.
---
### E. CRUD Akun
Super admin dapat mengelola:
- Akun admin rumah ibadah.
- Akun penerima.
Admin rumah ibadah dapat mengelola:
- Akun penerima saja.
Fitur:
- Tambah akun.
- Lihat daftar akun.
- Filter role/status.
- Cari nama/username.
- Edit akun.
- Hapus akun jika belum punya relasi data.
- Nonaktifkan akun jika sudah punya relasi agar audit data tidak rusak.
---
### F. Validasi Program dan Penyaluran Bantuan
Perbaikan penting:
- Program bantuan tidak otomatis mengambil rumah ibadah pertama.
- Admin yang punya banyak rumah ibadah harus memilih rumah ibadah.
- Penyaluran hanya bisa dicatat jika program dan penerima berada di rumah ibadah yang sama.
- Tombol "Catat" tidak muncul lagi jika program sudah selesai.
- Dropdown penerima difilter sesuai rumah ibadah dari program yang dipilih.
---
### G. Perbaikan Logika NULL
Masalah lama:
- Nilai kosong seperti nominal, jarak meter, atau rumah_ibadah_id bisa tersimpan sebagai `0`, bukan `NULL`.
Perbaikan:
- Query INSERT/UPDATE dipisah agar nilai kosong benar-benar masuk sebagai `NULL`.
Dampak:
- Data lebih bersih.
- Tidak muncul rumah_ibadah_id = 0 atau jarak_meter = 0 yang menyesatkan.
---
### H. Validasi Client-side
Ditambahkan validasi sebelum data dikirim:
- Program wajib dipilih.
- Penerima wajib dipilih.
- Nama program wajib diisi.
- Bulan harus 1-12.
- Tahun minimal 2000.
- Nama, username, dan password wajib saat membuat akun baru.
- Password minimal 6 karakter.
---
## 3. Catatan Error SQL
### Error #1044 information_schema
Penyebab:
- Akun MySQL tidak punya akses membaca `INFORMATION_SCHEMA`.
Solusi:
- Gunakan file migrasi versi manual.
- Atau jalankan `jalankan_migrasi_fix.php` jika file itu tersedia.
- Setelah selesai, hapus `jalankan_migrasi_fix.php`.
---
### Error #1064 IF NOT EXISTS
Penyebab:
- Versi MySQL/MariaDB tidak mendukung syntax:
`ALTER TABLE ... ADD COLUMN IF NOT EXISTS`
Solusi:
- Gunakan file `migration_fix_multi_ri_pin.sql` versi kompatibel.
- Jika ada penerima dengan pin aktif ganda, rapikan dulu lewat dashboard super admin.
- Setelah itu jalankan ulang migration.
---
## 4. File Catatan yang Bisa Digabung/Hapus
Setelah rangkuman ini dibuat, file catatan berikut boleh dipindahkan ke backup:
- `CATATAN_FIXED_MULTI_RI.md`
- `CATATAN_PERBAIKAN_LOGIKA.md`
- `CATATAN_REVISI_CRUD_AKUN.md`
- `CATATAN_REVISI_PERSETUJUAN_HAPUS.md`
- `CATATAN_SQL_ERROR_1044.md`
- `CATATAN_SQL_ERROR_1064.md`
Jangan langsung buang permanen jika proyek masih diperiksa dosen/penguji.
---
## 5. Rekomendasi Struktur Folder
Gunakan struktur seperti ini:
```text
05/
├── index.php
├── login.php
├── register.php
├── koneksi.php
├── auth.php
├── tambah_penerima.php
├── tambah_penyaluran.php
├── tambah_program_bantuan.php
├── edit_user.php
├── hapus_user.php
├── ...
└── backup_dokumentasi/
├── RANGKUMAN_PENTING_PROJECT_05.md
└── backup_sql/
├── schema.sql
├── migration_fix_multi_ri_pin.sql
├── migration_revisi_persetujuan_hapus.sql
└── migration_revisi_sinkronisasi.sql
```
---
## 6. Kesimpulan
Yang paling penting disimpan:
1. `schema.sql`
2. Semua file `migration_*.sql`
3. Satu file rangkuman ini
4. File PHP aplikasi utama
Yang tidak wajib ada di folder utama aplikasi:
- File catatan `.md`
- File SQL backup
- File migrasi yang sudah selesai dijalankan
Untuk hosting, file SQL dan catatan revisi sebaiknya tidak ikut berada di folder publik.
@@ -0,0 +1,632 @@
# SKPL / SRS WEBGIS POVERTY MAPPING PROJECT 05
**Nama Dokumen:** Spesifikasi Kebutuhan Perangkat Lunak / Software Requirements Specification
**Nama Sistem:** WebGIS Poverty Mapping
**Versi:** 1.0
**Teknologi:** PHP Native, MySQL/MariaDB, Leaflet.js, HTML, CSS, JavaScript Vanilla
**Lingkungan Pengembangan:** Laragon Localhost dan Hosting cPanel
---
## 1. Pendahuluan
### 1.1 Tujuan Dokumen
Dokumen ini menjelaskan kebutuhan perangkat lunak untuk aplikasi **WebGIS Poverty Mapping Project 05**. Dokumen ini dapat digunakan sebagai acuan pengembangan, pengujian, instalasi, dan evaluasi aplikasi.
### 1.2 Ruang Lingkup Sistem
WebGIS Poverty Mapping adalah aplikasi berbasis web untuk memetakan rumah ibadah dan penerima bantuan. Sistem digunakan untuk membantu proses pendataan, validasi, pengelolaan program bantuan, penyaluran bantuan, dan pelaporan penerima yang sudah atau belum memperoleh bantuan.
Sistem memiliki fitur utama:
1. Login dan manajemen role pengguna.
2. Registrasi akun penerima.
3. Pengelolaan akun pengguna.
4. Pemetaan rumah ibadah dan penerima bantuan menggunakan Leaflet.
5. Pengajuan dan persetujuan rumah ibadah.
6. Pengajuan dan persetujuan penerima bantuan.
7. Sinkronisasi penerima ke rumah ibadah terdekat dalam radius 300 meter.
8. Pengelolaan program bantuan.
9. Pencatatan penyaluran bantuan.
10. Laporan penyaluran bantuan per periode.
11. Riwayat status dan penyaluran.
12. Pengajuan dan persetujuan hapus marker/pin.
### 1.3 Definisi Istilah
| Istilah | Definisi |
|---|---|
| WebGIS | Sistem informasi geografis berbasis web. |
| Rumah Ibadah | Entitas pemberi/penyalur bantuan yang memiliki lokasi pada peta. |
| Penerima | Pengguna atau warga yang mengajukan bantuan dan memiliki titik lokasi. |
| Super Admin | Pengguna dengan akses tertinggi. |
| Admin Rumah Ibadah | Pengguna yang mengelola satu atau lebih rumah ibadah. |
| Radius 300 Meter | Jarak maksimal penerima agar dapat dikaitkan dengan rumah ibadah. |
| Marker | Titik rumah ibadah pada peta. |
| Pin | Titik penerima pada peta. |
| Program Bantuan | Program penyaluran bantuan pada periode tertentu. |
| Penyaluran Bantuan | Catatan pemberian bantuan kepada penerima. |
| SKPL | Spesifikasi Kebutuhan Perangkat Lunak. |
| SRS | Software Requirements Specification. |
### 1.4 Referensi Teknologi
Sistem dibangun menggunakan:
1. PHP Native tanpa framework.
2. MySQL/MariaDB sebagai basis data.
3. Leaflet.js sebagai library peta.
4. HTML, CSS, dan JavaScript Vanilla.
5. AJAX/fetch API untuk pertukaran data dengan file PHP backend.
6. Laragon sebagai lingkungan localhost.
7. cPanel sebagai contoh lingkungan hosting.
---
## 2. Deskripsi Umum Sistem
### 2.1 Perspektif Produk
Aplikasi ini merupakan sistem mandiri berbasis web yang berjalan pada server PHP dan database MySQL/MariaDB. Pengguna mengakses aplikasi melalui browser. Data spasial berupa latitude dan longitude ditampilkan menggunakan Leaflet.js.
### 2.2 Fungsi Umum Produk
Sistem menyediakan fungsi:
1. Autentikasi pengguna.
2. Otorisasi berdasarkan role.
3. Pengelolaan data pengguna.
4. Pengelolaan titik rumah ibadah.
5. Pengelolaan titik penerima bantuan.
6. Sinkronisasi area penerima dengan rumah ibadah.
7. Pengelolaan program bantuan.
8. Pencatatan penyaluran bantuan.
9. Pelaporan penerima sudah/belum diberi bantuan.
10. Riwayat perubahan status.
### 2.3 Kelas Pengguna
#### 2.3.1 Super Admin
Super Admin adalah pengguna dengan hak akses penuh. Super Admin dapat mengelola semua akun, rumah ibadah, penerima, program bantuan, penyaluran bantuan, laporan, dan persetujuan hapus marker/pin.
#### 2.3.2 Admin Rumah Ibadah
Admin Rumah Ibadah adalah pengguna yang mengelola satu atau lebih rumah ibadah. Admin dapat mengelola penerima yang masuk dalam wilayah rumah ibadahnya, membuat program bantuan, mencatat penyaluran, dan melihat laporan sesuai data miliknya.
#### 2.3.3 Penerima
Penerima adalah pengguna yang dapat registrasi, login, mengajukan pin lokasi rumah, melihat status pengajuan, melihat riwayat bantuan, dan mengajukan hapus pin miliknya.
### 2.4 Batasan Sistem
1. Sistem menggunakan PHP Native tanpa framework.
2. Sistem menggunakan MySQL/MariaDB.
3. Sistem menggunakan Leaflet CDN sehingga peta membutuhkan koneksi internet.
4. Radius tanggung jawab rumah ibadah ditetapkan 300 meter.
5. Sistem tidak memakai API geocoding berbayar.
6. Sistem tidak menyediakan upload dokumen identitas.
7. Sistem tidak menyediakan notifikasi email/WhatsApp otomatis.
8. Sistem tidak menyediakan pencetakan laporan PDF otomatis.
### 2.5 Asumsi dan Ketergantungan
1. Browser pengguna mendukung JavaScript.
2. Server mendukung PHP dan MySQL/MariaDB.
3. Database sudah diimport sebelum aplikasi dijalankan.
4. File `koneksi.php` sudah disesuaikan dengan konfigurasi database.
5. Leaflet CDN dapat diakses dari jaringan pengguna.
---
## 3. Aktor Sistem
| Kode | Aktor | Deskripsi |
|---|---|---|
| A-01 | Super Admin | Pengelola utama sistem. |
| A-02 | Admin Rumah Ibadah | Pengelola rumah ibadah dan penyaluran bantuan. |
| A-03 | Penerima | Pengguna yang mengajukan dan menerima bantuan. |
---
## 4. Kebutuhan Fungsional
### 4.1 Autentikasi dan Akun
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-01 | Sistem harus menyediakan login menggunakan username dan password. | Semua aktor | Tinggi |
| FR-02 | Sistem harus menolak login akun yang tidak aktif. | Semua aktor | Tinggi |
| FR-03 | Sistem harus menyediakan logout. | Semua aktor | Tinggi |
| FR-04 | Sistem harus menyimpan password menggunakan hash. | Sistem | Tinggi |
| FR-05 | Sistem harus menyediakan registrasi akun penerima. | Penerima | Tinggi |
| FR-06 | Sistem harus mengizinkan super admin membuat akun admin rumah ibadah dan penerima. | Super Admin | Tinggi |
| FR-07 | Sistem harus mengizinkan admin rumah ibadah membuat akun penerima. | Admin Rumah Ibadah | Sedang |
| FR-08 | Sistem harus menyediakan fitur edit akun. | Super Admin, Admin Rumah Ibadah | Sedang |
| FR-09 | Sistem harus menonaktifkan akun yang sudah punya relasi data jika tidak dapat dihapus permanen. | Super Admin, Admin Rumah Ibadah | Tinggi |
### 4.2 Peta WebGIS
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-10 | Sistem harus menampilkan peta menggunakan Leaflet. | Semua aktor | Tinggi |
| FR-11 | Sistem harus menampilkan marker rumah ibadah sesuai hak akses. | Semua aktor | Tinggi |
| FR-12 | Sistem harus menampilkan pin penerima sesuai hak akses. | Semua aktor | Tinggi |
| FR-13 | Sistem harus menampilkan lingkaran radius 300 meter untuk rumah ibadah aktif. | Semua aktor | Tinggi |
| FR-14 | Sistem harus menyediakan popup detail pada marker/pin. | Semua aktor | Sedang |
| FR-15 | Sistem harus memungkinkan input lokasi berdasarkan klik pada peta. | Semua aktor sesuai role | Tinggi |
### 4.3 Pengelolaan Rumah Ibadah
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-16 | Sistem harus mengizinkan super admin menambah rumah ibadah. | Super Admin | Tinggi |
| FR-17 | Sistem harus mengizinkan admin rumah ibadah mengajukan rumah ibadah. | Admin Rumah Ibadah | Tinggi |
| FR-18 | Sistem harus menyediakan status rumah ibadah: menunggu, diterima, ditolak. | Sistem | Tinggi |
| FR-19 | Sistem harus mengizinkan super admin menyetujui/menolak rumah ibadah. | Super Admin | Tinggi |
| FR-20 | Sistem harus menghubungkan rumah ibadah dengan akun admin rumah ibadah. | Super Admin | Tinggi |
| FR-21 | Sistem harus mendukung satu admin mengelola lebih dari satu rumah ibadah. | Admin Rumah Ibadah | Tinggi |
| FR-22 | Sistem harus melepas atau menyinkronkan ulang penerima jika rumah ibadah ditolak atau dihapus. | Sistem | Tinggi |
### 4.4 Pengelolaan Penerima Bantuan
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-23 | Sistem harus mengizinkan penerima mengajukan lokasi dirinya sendiri. | Penerima | Tinggi |
| FR-24 | Sistem harus mengizinkan super admin/admin rumah ibadah menambahkan penerima berdasarkan username penerima. | Super Admin, Admin Rumah Ibadah | Tinggi |
| FR-25 | Sistem harus menyimpan data nama, NIK, nomor HP, alamat, pekerjaan, penghasilan, tanggungan, kondisi rumah, jenis bantuan, latitude, dan longitude. | Sistem | Tinggi |
| FR-26 | Sistem harus menyediakan status penerima: menunggu, diterima, ditolak, dibatalkan. | Sistem | Tinggi |
| FR-27 | Sistem harus mengizinkan super admin menyetujui, menolak, mereset, atau membatalkan penerima. | Super Admin | Tinggi |
| FR-28 | Sistem harus mengizinkan admin rumah ibadah menyetujui/menolak penerima dalam wilayahnya. | Admin Rumah Ibadah | Tinggi |
| FR-29 | Sistem harus mencegah admin rumah ibadah mengembalikan status ke menunggu. | Admin Rumah Ibadah | Tinggi |
| FR-30 | Sistem harus mencegah admin rumah ibadah membatalkan penerima yang sudah diterima. | Admin Rumah Ibadah | Tinggi |
| FR-31 | Sistem harus membatasi satu akun penerima hanya memiliki satu pin aktif. | Sistem | Tinggi |
### 4.5 Sinkronisasi Radius
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-32 | Sistem harus menghitung jarak penerima ke rumah ibadah menggunakan koordinat. | Sistem | Tinggi |
| FR-33 | Sistem harus mengaitkan penerima ke rumah ibadah terdekat dalam radius 300 meter. | Sistem | Tinggi |
| FR-34 | Sistem harus membiarkan penerima belum terhubung jika tidak ada rumah ibadah dalam radius 300 meter. | Sistem | Sedang |
| FR-35 | Sistem harus menyinkronkan ulang penerima saat rumah ibadah disetujui. | Sistem | Tinggi |
| FR-36 | Sistem harus menyinkronkan ulang penerima saat lokasi penerima berubah. | Sistem | Tinggi |
| FR-37 | Sistem harus memilih rumah ibadah paling dekat jika radius beberapa rumah ibadah saling beririsan. | Sistem | Tinggi |
### 4.6 Pengajuan dan Persetujuan Hapus Marker/Pin
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-38 | Sistem harus mengizinkan super admin menghapus marker rumah ibadah secara langsung dari peta aktif. | Super Admin | Tinggi |
| FR-39 | Sistem harus mengizinkan super admin menghapus pin penerima secara langsung dari peta aktif. | Super Admin | Tinggi |
| FR-40 | Sistem harus mengizinkan admin rumah ibadah mengajukan hapus rumah ibadah miliknya. | Admin Rumah Ibadah | Sedang |
| FR-41 | Sistem harus mengizinkan admin rumah ibadah mengajukan hapus pin penerima yang dikelola. | Admin Rumah Ibadah | Sedang |
| FR-42 | Sistem harus mengizinkan penerima mengajukan hapus pin miliknya sendiri. | Penerima | Sedang |
| FR-43 | Sistem harus menyediakan panel persetujuan hapus marker/pin untuk super admin. | Super Admin | Tinggi |
| FR-44 | Sistem harus menyembunyikan marker/pin yang sudah disetujui penghapusannya. | Sistem | Tinggi |
### 4.7 Program Bantuan
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-45 | Sistem harus mengizinkan super admin/admin rumah ibadah membuat program bantuan. | Super Admin, Admin Rumah Ibadah | Tinggi |
| FR-46 | Program bantuan harus memiliki rumah ibadah, nama program, jenis bantuan, bulan, tahun, target frekuensi, nominal/item, deskripsi, dan status. | Sistem | Tinggi |
| FR-47 | Sistem harus mengizinkan penghapusan/pembatalan program bantuan. | Super Admin, Admin Rumah Ibadah | Sedang |
| FR-48 | Sistem harus mencegah penyaluran baru pada program berstatus selesai atau dibatalkan. | Sistem | Tinggi |
| FR-49 | Sistem harus menampilkan daftar program bantuan sesuai hak akses. | Super Admin, Admin Rumah Ibadah, Penerima | Sedang |
### 4.8 Penyaluran Bantuan
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-50 | Sistem harus mengizinkan super admin/admin rumah ibadah mencatat penyaluran bantuan. | Super Admin, Admin Rumah Ibadah | Tinggi |
| FR-51 | Sistem harus memastikan program dan penerima berada pada rumah ibadah yang sama. | Sistem | Tinggi |
| FR-52 | Sistem harus memastikan penerima berstatus diterima sebelum bantuan disalurkan. | Sistem | Tinggi |
| FR-53 | Sistem harus memastikan penerima masih berada dalam radius 300 meter rumah ibadah program. | Sistem | Tinggi |
| FR-54 | Sistem harus mengambil jenis bantuan, nominal, dan jumlah item dari program. | Sistem | Sedang |
| FR-55 | Sistem harus menghitung bantuan_ke secara otomatis. | Sistem | Tinggi |
| FR-56 | Sistem harus memberi peringatan jika penyaluran melebihi target frekuensi program. | Sistem | Sedang |
### 4.9 Laporan dan Riwayat
| Kode | Kebutuhan | Aktor | Prioritas |
|---|---|---|---|
| FR-57 | Sistem harus menyediakan laporan penyaluran berdasarkan bulan dan tahun. | Semua aktor sesuai hak akses | Tinggi |
| FR-58 | Sistem harus menyediakan filter rumah ibadah, agama, program, jenis bantuan, dan status penyaluran. | Semua aktor sesuai hak akses | Sedang |
| FR-59 | Sistem harus menampilkan status belum_diberi, sudah_diberi, belum_memenuhi_target, sesuai_target, dan melebihi_target. | Sistem | Tinggi |
| FR-60 | Sistem harus menyediakan rekap laporan. | Semua aktor sesuai hak akses | Sedang |
| FR-61 | Sistem harus menyimpan riwayat perubahan status penerima. | Sistem | Tinggi |
| FR-62 | Sistem harus menampilkan riwayat pengajuan, status, dan penyaluran sesuai hak akses. | Semua aktor | Sedang |
---
## 5. Kebutuhan Non-Fungsional
| Kode | Kebutuhan | Keterangan | Prioritas |
|---|---|---|---|
| NFR-01 | Keamanan Password | Password harus disimpan dengan `password_hash()` dan diverifikasi dengan `password_verify()`. | Tinggi |
| NFR-02 | Otorisasi Role | Akses fitur harus dibatasi berdasarkan role. | Tinggi |
| NFR-03 | Validasi Input | Input wajib divalidasi di sisi server. | Tinggi |
| NFR-04 | Prepared Statement | Query yang menerima input pengguna harus memakai prepared statement. | Tinggi |
| NFR-05 | Kompatibilitas Browser | Sistem dapat berjalan pada browser modern seperti Chrome, Edge, dan Firefox. | Sedang |
| NFR-06 | Responsivitas | Tampilan harus dapat digunakan pada laptop dan perangkat mobile. | Sedang |
| NFR-07 | Ketersediaan | Sistem dapat berjalan pada server PHP dan MySQL/MariaDB standar. | Tinggi |
| NFR-08 | Maintainability | Struktur file sederhana dan mudah dipelihara. | Sedang |
| NFR-09 | Usability | Antarmuka harus mudah dipahami oleh admin dan penerima. | Sedang |
| NFR-10 | Auditability | Perubahan status penting harus tercatat dalam riwayat. | Tinggi |
---
## 6. Kebutuhan Data
### 6.1 Tabel `users`
Menyimpan data akun pengguna.
Kolom utama:
- id
- nama
- username
- password
- role
- status_akun
- created_at
- updated_at
Role yang tersedia:
- super_admin
- admin_rumah_ibadah
- penerima
Status akun:
- aktif
- nonaktif
### 6.2 Tabel `rumah_ibadah`
Menyimpan data rumah ibadah.
Kolom utama:
- id
- user_id
- nama_rumah_ibadah
- agama
- alamat
- nomor_hp
- latitude
- longitude
- status_pengajuan
- status_hapus
- alasan_hapus
- keterangan
- created_at
- updated_at
Status pengajuan:
- menunggu
- diterima
- ditolak
Status hapus:
- normal
- menunggu
- disetujui
- ditolak
### 6.3 Tabel `penerima_bantuan`
Menyimpan data penerima bantuan.
Kolom utama:
- id
- user_id
- rumah_ibadah_id
- nama_penerima
- nik
- no_hp
- alamat
- pekerjaan
- penghasilan
- jumlah_tanggungan
- kondisi_rumah
- jenis_bantuan
- latitude
- longitude
- jarak_meter
- status_pengajuan
- status_hapus
- pin_aktif_user_id
- alasan_hapus
- alasan_status
- sumber_pengajuan
- created_at
- updated_at
Status pengajuan:
- menunggu
- diterima
- ditolak
- dibatalkan
Aturan penting:
- `pin_aktif_user_id` digunakan untuk mencegah satu akun penerima memiliki lebih dari satu pin aktif.
### 6.4 Tabel `program_bantuan`
Menyimpan program bantuan.
Kolom utama:
- id
- rumah_ibadah_id
- nama_program
- jenis_bantuan
- deskripsi
- bulan
- tahun
- target_frekuensi
- nominal
- jumlah_item
- status_program
- created_at
- updated_at
Status program:
- aktif
- selesai
- dibatalkan
### 6.5 Tabel `penyaluran_bantuan`
Menyimpan catatan penyaluran bantuan.
Kolom utama:
- id
- program_id
- penerima_id
- rumah_ibadah_id
- admin_id
- jenis_bantuan
- nominal
- jumlah_item
- bantuan_ke
- keterangan
- tanggal_penyaluran
- created_at
### 6.6 Tabel `riwayat_status`
Menyimpan riwayat perubahan status penerima.
Kolom utama:
- id
- penerima_id
- user_id
- status_lama
- status_baru
- catatan
- created_at
---
## 7. Aturan Bisnis
| Kode | Aturan Bisnis |
|---|---|
| BR-01 | Setiap pengguna harus login untuk mengakses sistem. |
| BR-02 | Akun nonaktif tidak boleh login. |
| BR-03 | Registrasi umum hanya untuk role penerima. |
| BR-04 | Admin rumah ibadah hanya boleh membuat akun penerima. |
| BR-05 | Super admin dapat membuat akun admin rumah ibadah dan penerima. |
| BR-06 | Satu akun penerima hanya boleh memiliki satu pin aktif. |
| BR-07 | Penerima dikaitkan ke rumah ibadah terdekat dalam radius 300 meter. |
| BR-08 | Jika tidak ada rumah ibadah dalam radius 300 meter, penerima belum memiliki rumah ibadah peninjau. |
| BR-09 | Admin rumah ibadah hanya boleh mengelola rumah ibadah miliknya. |
| BR-10 | Admin rumah ibadah tidak boleh membatalkan penerima yang sudah diterima. |
| BR-11 | Penghapusan marker/pin oleh admin rumah ibadah dan penerima harus disetujui super admin. |
| BR-12 | Marker/pin yang status hapusnya disetujui tidak tampil sebagai data aktif. |
| BR-13 | Program dan penerima harus berada pada rumah ibadah yang sama agar penyaluran dapat dicatat. |
| BR-14 | Program berstatus selesai atau dibatalkan tidak dapat menerima penyaluran baru. |
| BR-15 | Target frekuensi menjadi acuan laporan dan peringatan, bukan penghapus data riwayat. |
| BR-16 | Data yang sudah punya relasi tidak boleh dihapus sembarangan agar audit tetap aman. |
---
## 8. Use Case Sistem
### 8.1 Daftar Use Case
| Kode | Use Case | Aktor |
|---|---|---|
| UC-01 | Login | Semua aktor |
| UC-02 | Logout | Semua aktor |
| UC-03 | Registrasi Akun Penerima | Penerima |
| UC-04 | Kelola Akun | Super Admin, Admin Rumah Ibadah |
| UC-05 | Lihat Peta | Semua aktor |
| UC-06 | Tambah/Ajukan Rumah Ibadah | Super Admin, Admin Rumah Ibadah |
| UC-07 | Setujui/Tolak Rumah Ibadah | Super Admin |
| UC-08 | Tambah/Ajukan Penerima | Semua aktor sesuai hak akses |
| UC-09 | Setujui/Tolak Penerima | Super Admin, Admin Rumah Ibadah |
| UC-10 | Ajukan Hapus Marker/Pin | Admin Rumah Ibadah, Penerima |
| UC-11 | Setujui/Tolak Hapus Marker/Pin | Super Admin |
| UC-12 | Buat Program Bantuan | Super Admin, Admin Rumah Ibadah |
| UC-13 | Hapus/Batalkan Program Bantuan | Super Admin, Admin Rumah Ibadah |
| UC-14 | Catat Penyaluran Bantuan | Super Admin, Admin Rumah Ibadah |
| UC-15 | Lihat Laporan Penyaluran | Semua aktor sesuai hak akses |
| UC-16 | Lihat Riwayat | Semua aktor sesuai hak akses |
### 8.2 Deskripsi Use Case Utama
#### UC-01 Login
Aktor memasukkan username dan password. Sistem memeriksa akun pada tabel `users`. Jika akun aktif dan password benar, sistem membuat session dan mengarahkan pengguna ke dashboard.
#### UC-04 Kelola Akun
Super Admin dapat membuat, mengedit, menonaktifkan, dan menghapus akun admin rumah ibadah atau penerima. Admin Rumah Ibadah hanya dapat mengelola akun penerima.
#### UC-06 Tambah/Ajukan Rumah Ibadah
Super Admin atau Admin Rumah Ibadah memilih titik pada peta dan mengisi data rumah ibadah. Data disimpan dengan status pengajuan sesuai role. Super Admin dapat menyetujui data agar rumah ibadah aktif.
#### UC-08 Tambah/Ajukan Penerima
Pengguna memilih lokasi rumah penerima pada peta dan mengisi data penerima. Sistem mengecek satu pin aktif dan menghitung rumah ibadah terdekat dalam radius 300 meter.
#### UC-14 Catat Penyaluran Bantuan
Admin memilih program dan penerima yang sudah diterima. Sistem memastikan program dan penerima berada pada rumah ibadah yang sama, lalu menyimpan catatan penyaluran.
---
## 9. Matriks Hak Akses
| Fitur | Super Admin | Admin Rumah Ibadah | Penerima |
|---|---:|---:|---:|
| Login | Ya | Ya | Ya |
| Registrasi akun sendiri | Tidak | Tidak | Ya |
| Kelola akun admin rumah ibadah | Ya | Tidak | Tidak |
| Kelola akun penerima | Ya | Ya | Tidak |
| Lihat peta | Ya | Ya | Ya |
| Tambah rumah ibadah | Ya | Ajukan | Tidak |
| Setujui rumah ibadah | Ya | Tidak | Tidak |
| Tambah penerima | Ya | Ya, sesuai radius | Ajukan diri sendiri |
| Setujui penerima | Ya | Ya, sesuai wilayah | Tidak |
| Reset/batalkan penerima diterima | Ya | Tidak | Tidak |
| Buat program bantuan | Ya | Ya, miliknya | Tidak |
| Catat penyaluran bantuan | Ya | Ya, miliknya | Tidak |
| Lihat laporan | Ya, semua | Ya, miliknya | Ya, miliknya |
| Hapus marker/pin langsung | Ya | Tidak | Tidak |
| Ajukan hapus marker/pin | Tidak perlu | Ya | Ya, pin sendiri |
| Setujui hapus marker/pin | Ya | Tidak | Tidak |
---
## 10. Antarmuka Sistem
### 10.1 Halaman Login
Komponen:
- Input username.
- Input password.
- Tombol Login.
- Link Registrasi Penerima.
- Informasi akun demo.
### 10.2 Halaman Register
Komponen:
- Nama lengkap.
- Username.
- Password.
- Konfirmasi password.
- Tombol Daftar.
### 10.3 Halaman Peta
Komponen:
- Peta Leaflet.
- Marker rumah ibadah.
- Pin penerima.
- Popup detail.
- Form tambah/ajukan rumah ibadah.
- Form tambah/ajukan penerima.
- Tombol setujui/tolak.
- Tombol hapus/ajukan hapus.
### 10.4 Halaman Dashboard
Komponen:
- Statistik data.
- Panel persetujuan hapus marker/pin.
- Form kelola akun.
- Daftar akun.
- Monitoring penyaluran per periode.
- Rekap penyaluran.
- Form program bantuan.
- Daftar program bantuan.
- Form penyaluran bantuan.
- Tabel laporan penerima.
- Riwayat pengajuan dan bantuan.
---
## 11. Kebutuhan Lingkungan
### 11.1 Localhost
| Komponen | Kebutuhan |
|---|---|
| Web server | Apache dari Laragon |
| Database | MySQL/MariaDB |
| PHP | PHP versi yang mendukung MySQLi dan password_hash |
| Browser | Chrome, Edge, Firefox, atau browser modern lain |
| Internet | Dibutuhkan untuk Leaflet CDN |
### 11.2 Hosting
| Komponen | Kebutuhan |
|---|---|
| Hosting | cPanel atau hosting PHP sejenis |
| PHP | Mendukung MySQLi |
| Database | MySQL/MariaDB |
| SSL | Disarankan menggunakan HTTPS |
| File Manager/FTP | Untuk upload file aplikasi |
---
## 12. Kriteria Penerimaan Sistem
Sistem dianggap berhasil jika:
1. Pengguna dapat login sesuai role.
2. Akun nonaktif tidak dapat login.
3. Super admin dapat mengelola akun.
4. Admin rumah ibadah dapat mengelola akun penerima.
5. Peta dapat menampilkan rumah ibadah dan penerima.
6. Rumah ibadah dapat diajukan dan disetujui.
7. Penerima dapat diajukan dan disetujui.
8. Sistem mencegah satu akun penerima memiliki lebih dari satu pin aktif.
9. Sistem menghubungkan penerima ke rumah ibadah terdekat dalam radius 300 meter.
10. Program bantuan dapat dibuat.
11. Penyaluran bantuan dapat dicatat.
12. Laporan bulanan dapat menampilkan status bantuan.
13. Riwayat perubahan status tersimpan.
14. Pengajuan hapus marker/pin dapat diproses oleh super admin.
15. Aplikasi dapat berjalan di localhost dan hosting.
---
## 13. Risiko dan Mitigasi
| Risiko | Dampak | Mitigasi |
|---|---|---|
| File SQL gagal diimport | Sistem tidak dapat berjalan | Sediakan panduan import dan troubleshooting. |
| Password akun salah | Pengguna tidak bisa login | Gunakan akun demo dengan password `password`. |
| Leaflet CDN tidak bisa diakses | Peta tidak muncul | Pastikan koneksi internet aktif atau siapkan Leaflet lokal jika diperlukan. |
| Data penerima ganda | Laporan tidak akurat | Terapkan aturan satu pin aktif per akun penerima. |
| Admin salah mengelola data RI lain | Data tidak valid | Terapkan pembatasan akses berdasarkan user_id rumah ibadah. |
| Hapus data merusak audit | Riwayat hilang | Gunakan status hapus/nonaktif, bukan hapus permanen untuk data berelasi. |
---
## 14. Penutup
Dokumen SKPL/SRS ini menjadi acuan kebutuhan aplikasi WebGIS Poverty Mapping Project 05. Sistem dirancang untuk membantu pendataan penerima bantuan berbasis lokasi, pengelolaan rumah ibadah, pencatatan program bantuan, penyaluran bantuan, dan pelaporan penerima yang sudah atau belum menerima bantuan.
@@ -0,0 +1,510 @@
TUTORIAL CARA PAKAI APLIKASI WEBGIS POVERTY MAPPING PROJECT 05
=================================================================
A. AKUN LOGIN DEMO
------------------
Semua password akun demo adalah:
password
Akun utama:
1. Super Admin
Username : superadmin
Password : password
Contoh akun admin rumah ibadah jika memakai database demo:
2. Admin Gereja Sesilia
Username : admingerejasesilia
Password : password
3. Admin Gereja Elim
Username : admingerejaelim
Password : password
Contoh akun penerima jika memakai database demo:
4. Penerima 02
Username : penerima02
Password : password
5. Penerima 04
Username : penerima04
Password : password
6. Penerima 05
Username : penerima05
Password : password
Catatan:
- Akun nonaktif tidak bisa login.
- Jika membuat akun baru, password minimal 6 karakter.
=================================================================
B. ALUR UMUM APLIKASI
---------------------
Aplikasi ini dipakai untuk:
1. Memetakan rumah ibadah.
2. Memetakan calon penerima bantuan.
3. Menghubungkan penerima dengan rumah ibadah terdekat dalam radius 300 meter.
4. Menyetujui atau menolak pengajuan penerima.
5. Membuat program bantuan.
6. Mencatat penyaluran bantuan.
7. Melihat laporan penerima yang sudah/belum diberi bantuan per bulan.
Role pengguna:
1. Super Admin
Mengelola semua data.
2. Admin Rumah Ibadah
Mengelola rumah ibadah, penerima, program, dan penyaluran sesuai rumah ibadahnya.
3. Penerima
Mengajukan titik lokasi rumah dan melihat riwayat bantuan.
=================================================================
C. CARA LOGIN
-------------
1. Buka aplikasi:
http://localhost/webgis-poverty/05/
2. Masukkan username.
3. Masukkan password.
4. Klik Login.
5. Setelah berhasil, pengguna masuk ke dashboard.
=================================================================
D. CARA LOGOUT
--------------
1. Klik tombol Logout di bagian atas halaman.
2. Sistem akan keluar dari akun.
3. Pengguna diarahkan kembali ke halaman login.
=================================================================
E. CARA PAKAI SEBAGAI SUPER ADMIN
---------------------------------
Super Admin adalah role tertinggi.
Super Admin dapat mengelola seluruh data aplikasi.
1. Login Super Admin
Username : superadmin
Password : password
2. Melihat Dashboard
Setelah login, dashboard menampilkan statistik seperti:
- Rumah ibadah menunggu.
- Rumah ibadah diterima.
- Penerima menunggu.
- Penerima diterima.
- Penerima ditolak.
- Program aktif.
3. Mengelola Akun
Langkah:
- Buka Dashboard.
- Cari bagian Kelola Akun Pengguna.
- Isi nama, username, password, role, dan status akun.
- Pilih role: Admin Rumah Ibadah atau Penerima.
- Klik Buat Akun.
Super Admin dapat:
- membuat akun admin rumah ibadah;
- membuat akun penerima;
- mengedit akun;
- menonaktifkan akun;
- menghapus akun jika belum punya relasi data.
Jika akun sudah punya relasi data, sistem tidak menghapus permanen, tetapi menonaktifkan akun agar data riwayat tetap aman.
4. Menambah Rumah Ibadah
Langkah:
- Klik Buka Peta.
- Klik titik lokasi rumah ibadah di peta.
- Klik Tambah Rumah Ibadah.
- Isi username admin rumah ibadah.
- Isi nama rumah ibadah.
- Pilih agama.
- Isi alamat dan nomor HP.
- Klik Ajukan/Tambah.
Jika Super Admin yang menambah, data dapat langsung aktif/diterima.
5. Menyetujui Rumah Ibadah
Langkah:
- Buka peta.
- Klik marker rumah ibadah yang berstatus menunggu.
- Klik Setujui jika data benar.
- Klik Tolak jika data tidak valid.
Jika rumah ibadah disetujui, sistem akan menghitung ulang penerima yang berada dalam radius 300 meter.
6. Menambah Penerima
Langkah:
- Buka peta.
- Klik lokasi rumah penerima.
- Klik Tambah Penerima.
- Isi username akun penerima.
- Isi nama, NIK, nomor HP, alamat, pekerjaan, penghasilan, jumlah tanggungan, kondisi rumah, dan jenis bantuan.
- Klik Simpan.
Sistem akan mencari rumah ibadah terdekat dalam radius 300 meter.
7. Menyetujui Penerima
Langkah:
- Buka peta.
- Klik marker penerima berstatus menunggu.
- Klik Setujui untuk menerima.
- Klik Tolak untuk menolak.
Penerima yang diterima dapat masuk ke laporan dan penyaluran bantuan.
8. Menghapus Marker/Pin
Super Admin dapat langsung menghapus marker rumah ibadah atau pin penerima dari peta aktif.
Langkah:
- Klik marker/pin.
- Klik Hapus Marker atau Hapus Pin.
- Isi alasan jika diminta.
- Konfirmasi.
Data tidak hilang permanen dari database, tetapi tidak tampil lagi sebagai data aktif.
9. Memproses Pengajuan Hapus Marker/Pin
Langkah:
- Buka Dashboard.
- Cari bagian Persetujuan Hapus Marker/Pin.
- Lihat pengajuan hapus rumah ibadah atau penerima.
- Klik Setujui Hapus atau Tolak Hapus.
10. Membuat Program Bantuan
Langkah:
- Buka Dashboard.
- Cari bagian Buat Program Bantuan.
- Pilih rumah ibadah.
- Isi nama program.
- Pilih jenis bantuan.
- Isi bulan dan tahun.
- Isi target frekuensi.
- Isi nominal atau jumlah item jika ada.
- Isi deskripsi.
- Klik Simpan.
11. Menghapus Program Bantuan
Langkah:
- Buka bagian Daftar Program.
- Pilih program.
- Klik Hapus.
Jika program sudah punya data penyaluran, sistem dapat membatalkan program, bukan menghapus permanen.
12. Mencatat Penyaluran Bantuan
Langkah:
- Buka Dashboard.
- Cari bagian Catat Penyaluran Bantuan.
- Pilih program.
- Pilih penerima diterima.
- Jenis bantuan, nominal, dan item akan mengikuti program.
- Isi tanggal.
- Isi keterangan jika perlu.
- Klik Simpan.
Syarat:
- Penerima harus berstatus diterima.
- Penerima harus berada pada rumah ibadah yang sama dengan program.
- Penerima harus berada dalam radius 300 meter.
- Program tidak boleh berstatus selesai atau dibatalkan.
13. Melihat Laporan Bulanan
Langkah:
- Buka Dashboard.
- Cari bagian Monitoring Penyaluran Per Periode.
- Pilih bulan dan tahun.
- Gunakan filter rumah ibadah, agama, program, jenis bantuan, dan status.
- Klik tampilkan/muat laporan.
Status laporan:
- belum_diberi
- sudah_diberi
- belum_memenuhi_target
- sesuai_target
- melebihi_target
14. Melihat Riwayat
Langkah:
- Buka Dashboard.
- Cari bagian Riwayat Pengajuan dan Bantuan.
- Lihat daftar pengajuan, perubahan status, dan penyaluran.
=================================================================
F. CARA PAKAI SEBAGAI ADMIN RUMAH IBADAH
----------------------------------------
Admin Rumah Ibadah mengelola data sesuai rumah ibadah miliknya.
Satu admin dapat mengelola lebih dari satu rumah ibadah.
1. Login Admin Rumah Ibadah
Contoh:
Username : admingerejasesilia
Password : password
2. Mengajukan Rumah Ibadah
Langkah:
- Buka peta.
- Klik lokasi rumah ibadah.
- Klik Ajukan Rumah Ibadah.
- Isi nama rumah ibadah, agama, alamat, dan nomor HP.
- Klik Ajukan.
Status awal adalah menunggu.
Super Admin harus menyetujui pengajuan tersebut.
3. Membuat Akun Penerima
Langkah:
- Buka Dashboard.
- Cari Kelola Akun Pengguna.
- Isi nama, username, dan password penerima.
- Role otomatis/terbatas sebagai penerima.
- Klik Buat Akun.
4. Menambah Penerima
Langkah:
- Buka peta.
- Klik lokasi rumah penerima.
- Klik Tambah Penerima.
- Isi username penerima.
- Isi data penerima.
- Klik Simpan.
Catatan:
- Penerima harus berada dalam radius 300 meter dari rumah ibadah milik admin.
- Jika lokasi lebih dekat ke rumah ibadah lain, sistem akan menolak agar data tidak salah sinkron.
5. Menyetujui atau Menolak Penerima
Langkah:
- Buka peta.
- Klik marker penerima menunggu.
- Klik Setujui atau Tolak.
Batasan:
- Admin rumah ibadah tidak boleh mengembalikan status penerima ke menunggu.
- Admin rumah ibadah tidak boleh membatalkan penerima yang sudah diterima.
- Untuk reset/batal penerima diterima, hubungi Super Admin.
6. Membuat Program Bantuan
Langkah:
- Buka Dashboard.
- Pilih rumah ibadah milik admin.
- Isi nama program, jenis bantuan, bulan, tahun, target, nominal/item, dan deskripsi.
- Klik Simpan.
7. Mencatat Penyaluran Bantuan
Langkah:
- Buka Dashboard.
- Pilih program.
- Pilih penerima yang sudah diterima.
- Isi tanggal dan keterangan.
- Klik Simpan.
Sistem hanya menampilkan/menerima penerima yang sesuai dengan rumah ibadah program.
8. Melihat Laporan
Langkah:
- Buka Dashboard.
- Pilih bulan dan tahun.
- Gunakan filter yang tersedia.
- Lihat data penerima yang sudah atau belum diberi bantuan.
9. Mengajukan Hapus Marker/Pin
Langkah:
- Buka peta.
- Klik marker rumah ibadah milik admin atau pin penerima yang dikelola.
- Klik Ajukan Hapus.
- Isi alasan.
- Kirim.
Super Admin akan memutuskan setuju atau tolak.
=================================================================
G. CARA PAKAI SEBAGAI PENERIMA
------------------------------
Penerima dapat membuat akun sendiri melalui registrasi.
1. Registrasi Akun Penerima
Langkah:
- Buka halaman login.
- Klik Registrasi.
- Isi nama lengkap.
- Isi username.
- Isi password minimal 6 karakter.
- Isi konfirmasi password.
- Klik Daftar.
2. Login Penerima
Langkah:
- Buka halaman login.
- Masukkan username dan password.
- Klik Login.
3. Mengajukan Titik Lokasi Rumah
Langkah:
- Buka peta.
- Klik lokasi rumah pada peta.
- Isi data penerima.
- Klik Simpan Pengajuan.
Sistem akan memeriksa apakah lokasi masuk radius 300 meter dari rumah ibadah diterima.
4. Melihat Status Pengajuan
Langkah:
- Buka peta atau dashboard.
- Lihat status pengajuan.
Status yang mungkin:
- menunggu
- diterima
- ditolak
- dibatalkan
5. Melihat Riwayat Bantuan
Langkah:
- Buka Dashboard.
- Lihat bagian riwayat atau laporan yang tersedia untuk akun penerima.
6. Mengajukan Hapus Pin
Langkah:
- Buka peta.
- Klik pin milik sendiri.
- Klik Ajukan Hapus Pin.
- Isi alasan.
- Kirim.
Pengajuan hapus harus disetujui Super Admin.
=================================================================
H. ATURAN PENTING SISTEM
------------------------
1. Satu akun penerima hanya boleh punya satu pin aktif.
2. Rumah ibadah aktif adalah rumah ibadah yang statusnya diterima dan tidak dihapus.
3. Penerima aktif adalah penerima yang statusnya menunggu/diterima dan tidak dihapus.
4. Radius tanggung jawab rumah ibadah adalah 300 meter.
5. Jika ada beberapa rumah ibadah dalam radius 300 meter, penerima dikaitkan ke rumah ibadah terdekat.
6. Admin rumah ibadah hanya boleh mengelola data rumah ibadahnya sendiri.
7. Super Admin memiliki akses paling luas.
8. Password akun demo adalah password.
9. File SQL dan ZIP backup jangan dibiarkan di public_html setelah hosting.
=================================================================
I. TIPS SAAT PRESENTASI / DEMO
------------------------------
Urutan demo yang rapi:
1. Login sebagai Super Admin.
2. Tunjukkan dashboard.
3. Buka peta.
4. Tunjukkan marker rumah ibadah dan penerima.
5. Tambahkan akun penerima baru.
6. Tambahkan atau ajukan rumah ibadah.
7. Tambahkan penerima dari peta.
8. Setujui penerima.
9. Buat program bantuan.
10. Catat penyaluran bantuan.
11. Buka laporan bulanan.
12. Tunjukkan status sudah/belum diberi bantuan.
13. Tunjukkan pengajuan hapus marker/pin.
14. Logout.
=================================================================
J. MASALAH UMUM DAN SOLUSI CEPAT
--------------------------------
1. Tidak bisa login
- Cek username.
- Cek password: password.
- Cek status akun harus aktif.
2. Peta tidak muncul
- Pastikan internet aktif.
- Leaflet diambil dari CDN.
3. Marker tidak muncul
- Cek status data.
- Data ditolak atau dihapus memang tidak tampil aktif.
4. Penerima tidak bisa ditambah
- Cek apakah akun penerima sudah punya pin aktif.
- Cek apakah koordinat valid.
- Cek apakah username penerima benar dan aktif.
5. Admin rumah ibadah tidak bisa menambah penerima
- Cek apakah lokasi penerima masuk radius 300 meter rumah ibadah miliknya.
- Cek apakah penerima lebih dekat ke rumah ibadah lain.
6. Tidak bisa mencatat bantuan
- Cek status program.
- Cek status penerima.
- Cek apakah program dan penerima berada pada rumah ibadah yang sama.
=================================================================
SELESAI
=================================================================
+49
View File
@@ -0,0 +1,49 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role(['super_admin','admin_rumah_ibadah'], true);
$allowedAgama = ['islam','kristen','katolik','hindu','buddha','konghucu'];
$nama = clean_text(post('nama_rumah_ibadah'), 150);
$agama = post('agama');
$alamat = clean_text(post('alamat'), 1000);
$nomor_hp = clean_text(post('nomor_hp'), 20);
$username_admin = clean_text(post('username_admin'), 50);
$lat = post('latitude');
$lng = post('longitude');
if ($nama==='' || $alamat==='' || $nomor_hp==='' || !valid_latlng($lat, $lng)) json_response('err', 'Data rumah ibadah belum lengkap atau koordinat tidak valid.');
if (!valid_enum($agama, $allowedAgama)) json_response('err', 'Agama tidak valid.');
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
$user_id = (int) $_SESSION['user_id'];
$status = 'menunggu';
$ket = 'Menunggu persetujuan super admin.';
} else {
// Super admin wajib mengaitkan titik rumah ibadah ke username admin rumah ibadah.
if ($username_admin === '') json_response('err', 'Username admin rumah ibadah wajib diisi agar titik rumah ibadah punya pengelola.', [], 422);
$akunAdmin = get_user_by_username_role($username_admin, 'admin_rumah_ibadah');
if (!$akunAdmin) json_response('err', 'Username admin rumah ibadah tidak ditemukan, bukan role admin_rumah_ibadah, atau akun tidak aktif.', [], 404);
$user_id = (int) $akunAdmin['id'];
$status = 'diterima';
$ket = 'Ditambahkan langsung oleh super admin.';
}
$stmt = $koneksi->prepare("INSERT INTO rumah_ibadah (user_id, nama_rumah_ibadah, agama, alamat, nomor_hp, latitude, longitude, status_pengajuan, keterangan) VALUES (?,?,?,?,?,?,?,?,?)");
$stmt->bind_param('issssddss', $user_id, $nama, $agama, $alamat, $nomor_hp, $lat, $lng, $status, $ket);
if (!$stmt->execute()) json_response('err', 'Gagal mengajukan rumah ibadah: ' . $koneksi->error);
$id = $stmt->insert_id;
$jumlahSinkron = 0;
if ($status === 'diterima') {
// Jika rumah ibadah langsung diterima, penerima lama yang berada dalam radius otomatis ditautkan.
$jumlahSinkron = sync_penerima_area_by_rumah_ibadah($id);
}
$stmt = $koneksi->prepare("SELECT ri.*, u.nama AS nama_admin, u.username AS username_admin FROM rumah_ibadah ri LEFT JOIN users u ON u.id=ri.user_id WHERE ri.id=?");
$stmt->bind_param('i', $id);
$row = fetch_all_stmt($stmt)[0] ?? null;
if ($row) $row['jumlah_penerima_tersinkron'] = $jumlahSinkron;
json_response('ok', 'Rumah ibadah berhasil diajukan.' . ($jumlahSinkron ? " $jumlahSinkron penerima lama otomatis masuk area tanggung jawab." : ''), $row);
?>
+55
View File
@@ -0,0 +1,55 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
$user = current_user();
$role = $user['role'];
$data = ['rumah_ibadah' => [], 'penerima' => []];
// Ambil rumah ibadah sesuai role.
if ($role === 'super_admin') {
$sql = "SELECT ri.*, u.nama AS nama_admin FROM rumah_ibadah ri LEFT JOIN users u ON u.id=ri.user_id WHERE ri.status_pengajuan IN ('diterima','menunggu') AND COALESCE(ri.status_hapus,'normal')<>'disetujui' ORDER BY ri.id DESC";
$res = $koneksi->query($sql);
while ($row = $res->fetch_assoc()) $data['rumah_ibadah'][] = $row;
} elseif ($role === 'admin_rumah_ibadah') {
$uid = (int) $user['id'];
// Admin hanya melihat rumah ibadah miliknya, termasuk yang masih menunggu.
$stmt = $koneksi->prepare("SELECT ri.*, u.nama AS nama_admin FROM rumah_ibadah ri LEFT JOIN users u ON u.id=ri.user_id WHERE ri.user_id=? AND ri.status_pengajuan IN ('diterima','menunggu') AND COALESCE(ri.status_hapus,'normal')<>'disetujui' ORDER BY ri.id DESC");
$stmt->bind_param('i', $uid);
$data['rumah_ibadah'] = fetch_all_stmt($stmt);
} else {
$res = $koneksi->query("SELECT ri.*, NULL AS nama_admin FROM rumah_ibadah ri WHERE ri.status_pengajuan='diterima' AND COALESCE(ri.status_hapus,'normal')<>'disetujui' ORDER BY ri.id DESC");
while ($row = $res->fetch_assoc()) $data['rumah_ibadah'][] = $row;
}
// Ambil penerima sesuai role. Marker ditolak tidak dikirim.
if ($role === 'super_admin') {
$sql = "SELECT pb.*, ri.nama_rumah_ibadah, ri.agama FROM penerima_bantuan pb LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id WHERE pb.status_pengajuan IN ('diterima','menunggu','dibatalkan') AND COALESCE(pb.status_hapus,'normal')<>'disetujui' ORDER BY pb.id DESC";
$res = $koneksi->query($sql);
while ($row = $res->fetch_assoc()) $data['penerima'][] = $row;
} elseif ($role === 'admin_rumah_ibadah') {
$ids = get_admin_rumah_ibadah_ids((int)$user['id'], true);
if (!empty($ids)) {
$types = '';
$params = [];
$in = build_in_condition('pb.rumah_ibadah_id', $ids, $types, $params);
$sql = "SELECT pb.*, ri.nama_rumah_ibadah, ri.agama
FROM penerima_bantuan pb
LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id
WHERE $in
AND pb.status_pengajuan IN ('diterima','menunggu')
AND COALESCE(pb.status_hapus,'normal')<>'disetujui'
ORDER BY pb.id DESC";
$stmt = $koneksi->prepare($sql);
stmt_bind($stmt, $types, $params);
$data['penerima'] = fetch_all_stmt($stmt);
}
} else {
$uid = (int) $user['id'];
$stmt = $koneksi->prepare("SELECT pb.*, ri.nama_rumah_ibadah, ri.agama FROM penerima_bantuan pb LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id WHERE pb.user_id=? AND pb.status_pengajuan IN ('diterima','menunggu','dibatalkan') AND COALESCE(pb.status_hapus,'normal')<>'disetujui' ORDER BY pb.id DESC");
$stmt->bind_param('i', $uid);
$data['penerima'] = fetch_all_stmt($stmt);
}
json_response('ok', 'Data peta berhasil diambil.', $data);
?>
+138
View File
@@ -0,0 +1,138 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
$bulan = (int) getv('bulan', date('n'));
$tahun = (int) getv('tahun', date('Y'));
$rumah_filter = getv('rumah_ibadah_id', '');
$agama_filter = getv('agama', '');
$program_filter = getv('program_id', '');
$status_filter = getv('status_penyaluran', 'semua');
$jenis_filter = getv('jenis_bantuan', '');
if ($bulan < 1 || $bulan > 12 || $tahun < 2000) json_response('err', 'Filter bulan/tahun tidak valid.');
if (!valid_enum($status_filter, ['semua','belum_diberi','sudah_diberi','belum_memenuhi_target','sesuai_target','melebihi_target'])) json_response('err', 'Status penyaluran tidak valid.');
$where = ["pb.status_pengajuan='diterima'", "COALESCE(pb.status_hapus,'normal')<>'disetujui'", "COALESCE(ri.status_hapus,'normal')<>'disetujui'", "prog.bulan=?", "prog.tahun=?", "prog.status_program<>'dibatalkan'"];
$types = 'ii';
$params = [$bulan, $tahun];
if ($program_filter !== '') {
$where[] = 'prog.id=?';
$types .= 'i';
$params[] = (int)$program_filter;
}
if ($jenis_filter !== '') {
$where[] = 'prog.jenis_bantuan=?';
$types .= 's';
$params[] = $jenis_filter;
}
if ($agama_filter !== '') {
$where[] = 'ri.agama=?';
$types .= 's';
$params[] = $agama_filter;
}
if ($rumah_filter !== '') {
$where[] = 'ri.id=?';
$types .= 'i';
$params[] = (int)$rumah_filter;
}
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
$ids = get_admin_rumah_ibadah_ids((int)$_SESSION['user_id'], true);
if (empty($ids)) json_response('ok', 'Admin belum memiliki rumah ibadah diterima.', ['rows'=>[], 'rekap'=>[]]);
$where[] = build_in_condition('ri.id', $ids, $types, $params);
} elseif ($_SESSION['role'] === 'penerima') {
$where[] = 'pb.user_id=?';
$types .= 'i';
$params[] = (int)$_SESSION['user_id'];
}
$whereSql = implode(' AND ', $where);
$sql = "
SELECT
pb.id AS penerima_id,
pb.nama_penerima,
pb.no_hp,
pb.alamat,
pb.jenis_bantuan AS jenis_bantuan_diminta,
ri.id AS rumah_ibadah_id,
ri.nama_rumah_ibadah,
ri.agama,
prog.id AS program_id,
prog.nama_program,
prog.status_program,
prog.jenis_bantuan,
prog.bulan,
prog.tahun,
prog.target_frekuensi,
prog.nominal,
prog.jumlah_item,
COALESCE(cnt.jumlah_penyaluran, 0) AS jumlah_penyaluran,
cnt.tanggal_terakhir,
cnt.riwayat_penyaluran
FROM penerima_bantuan pb
JOIN rumah_ibadah ri ON ri.id = pb.rumah_ibadah_id
JOIN program_bantuan prog ON prog.rumah_ibadah_id = ri.id
LEFT JOIN (
SELECT penerima_id, program_id,
COUNT(id) AS jumlah_penyaluran,
MAX(tanggal_penyaluran) AS tanggal_terakhir,
GROUP_CONCAT(CONCAT(tanggal_penyaluran, ' | ', jenis_bantuan, ' | Rp', COALESCE(nominal,0), ' | Item: ', COALESCE(jumlah_item,''), ' | ke-', COALESCE(bantuan_ke,0), ' | ', COALESCE(keterangan,'')) ORDER BY tanggal_penyaluran SEPARATOR ';;') AS riwayat_penyaluran
FROM penyaluran_bantuan
WHERE MONTH(tanggal_penyaluran)=? AND YEAR(tanggal_penyaluran)=?
GROUP BY penerima_id, program_id
) cnt ON cnt.penerima_id = pb.id AND cnt.program_id = prog.id
WHERE $whereSql
ORDER BY ri.nama_rumah_ibadah ASC, pb.nama_penerima ASC, prog.nama_program ASC
";
// Dua parameter pertama untuk subquery count bulan/tahun, lalu parameter where.
$bindTypes = 'ii' . $types;
$bindParams = array_merge([$bulan, $tahun], $params);
$stmt = $koneksi->prepare($sql);
if (!$stmt) json_response('err', 'Query laporan gagal disiapkan: ' . $koneksi->error);
stmt_bind($stmt, $bindTypes, $bindParams);
$stmt->execute();
$res = $stmt->get_result();
$rows = [];
$rekap = [
'total' => 0,
'belum_diberi' => 0,
'sudah_diberi' => 0,
'belum_memenuhi_target' => 0,
'sesuai_target' => 0,
'melebihi_target' => 0
];
while ($row = $res->fetch_assoc()) {
$jumlah = (int)$row['jumlah_penyaluran'];
$target = max(1, (int)$row['target_frekuensi']);
if ($jumlah === 0) $status = 'belum_diberi';
elseif ($jumlah > 0 && $jumlah < $target) $status = 'belum_memenuhi_target';
elseif ($jumlah === $target) $status = 'sesuai_target';
else $status = 'melebihi_target';
$row['status_penyaluran'] = $status;
$row['riwayat'] = $row['riwayat_penyaluran'] ? explode(';;', $row['riwayat_penyaluran']) : [];
$include = false;
if ($status_filter === 'semua') $include = true;
elseif ($status_filter === 'sudah_diberi') $include = $jumlah > 0;
else $include = ($status_filter === $status);
if ($include) {
$rows[] = $row;
$rekap['total']++;
if ($jumlah > 0) $rekap['sudah_diberi']++;
else $rekap['belum_diberi']++;
if ($status === 'belum_memenuhi_target') $rekap['belum_memenuhi_target']++;
if ($status === 'sesuai_target') $rekap['sesuai_target']++;
if ($status === 'melebihi_target') $rekap['melebihi_target']++;
}
}
json_response('ok', 'Laporan penyaluran berhasil diambil.', ['rows' => $rows, 'rekap' => $rekap]);
?>
+28
View File
@@ -0,0 +1,28 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
$role = $_SESSION['role'];
$rows = [];
if ($role === 'super_admin') {
$sql = "SELECT p.*, ri.nama_rumah_ibadah FROM program_bantuan p LEFT JOIN rumah_ibadah ri ON ri.id=p.rumah_ibadah_id ORDER BY p.tahun DESC, p.bulan DESC, p.id DESC";
$res = $koneksi->query($sql);
while ($row = $res->fetch_assoc()) $rows[] = $row;
} elseif ($role === 'admin_rumah_ibadah') {
$ids = get_admin_rumah_ibadah_ids((int)$_SESSION['user_id'], true);
if (!empty($ids)) {
$types = '';
$params = [];
$in = build_in_condition('p.rumah_ibadah_id', $ids, $types, $params);
$stmt = $koneksi->prepare("SELECT p.*, ri.nama_rumah_ibadah FROM program_bantuan p LEFT JOIN rumah_ibadah ri ON ri.id=p.rumah_ibadah_id WHERE $in ORDER BY p.tahun DESC, p.bulan DESC, p.id DESC");
stmt_bind($stmt, $types, $params);
$rows = fetch_all_stmt($stmt);
}
} else {
$uid = (int) $_SESSION['user_id'];
$stmt = $koneksi->prepare("SELECT DISTINCT p.*, ri.nama_rumah_ibadah FROM program_bantuan p INNER JOIN penyaluran_bantuan pb ON pb.program_id=p.id INNER JOIN penerima_bantuan pr ON pr.id=pb.penerima_id LEFT JOIN rumah_ibadah ri ON ri.id=p.rumah_ibadah_id WHERE pr.user_id=? ORDER BY p.tahun DESC, p.bulan DESC, p.id DESC");
$stmt->bind_param('i', $uid);
$rows = fetch_all_stmt($stmt);
}
json_response('ok', 'Program bantuan berhasil diambil.', $rows);
?>
+44
View File
@@ -0,0 +1,44 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
$role = $_SESSION['role'];
$data = ['pengajuan' => [], 'status' => [], 'penyaluran' => []];
if ($role === 'super_admin') {
$sql = "SELECT pb.*, ri.nama_rumah_ibadah FROM penerima_bantuan pb LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id ORDER BY pb.created_at DESC LIMIT 100";
$res = $koneksi->query($sql); while($row=$res->fetch_assoc()) $data['pengajuan'][]=$row;
$sql = "SELECT rw.*, pb.nama_penerima, u.nama AS nama_user FROM riwayat_status rw JOIN penerima_bantuan pb ON pb.id=rw.penerima_id JOIN users u ON u.id=rw.user_id ORDER BY rw.created_at DESC LIMIT 100";
$res = $koneksi->query($sql); while($row=$res->fetch_assoc()) $data['status'][]=$row;
$sql = "SELECT pn.*, pb.nama_penerima, ri.nama_rumah_ibadah, pr.nama_program FROM penyaluran_bantuan pn JOIN penerima_bantuan pb ON pb.id=pn.penerima_id JOIN rumah_ibadah ri ON ri.id=pn.rumah_ibadah_id LEFT JOIN program_bantuan pr ON pr.id=pn.program_id ORDER BY pn.tanggal_penyaluran DESC, pn.id DESC LIMIT 100";
$res = $koneksi->query($sql); while($row=$res->fetch_assoc()) $data['penyaluran'][]=$row;
} elseif ($role === 'admin_rumah_ibadah') {
$ids = get_admin_rumah_ibadah_ids((int)$_SESSION['user_id'], true);
if (!empty($ids)) {
$types = ''; $params = [];
$inPb = build_in_condition('pb.rumah_ibadah_id', $ids, $types, $params);
$stmt = $koneksi->prepare("SELECT pb.*, ri.nama_rumah_ibadah FROM penerima_bantuan pb LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id WHERE $inPb ORDER BY pb.created_at DESC LIMIT 100");
stmt_bind($stmt, $types, $params); $data['pengajuan'] = fetch_all_stmt($stmt);
$types = ''; $params = [];
$inPb = build_in_condition('pb.rumah_ibadah_id', $ids, $types, $params);
$stmt = $koneksi->prepare("SELECT rw.*, pb.nama_penerima, u.nama AS nama_user FROM riwayat_status rw JOIN penerima_bantuan pb ON pb.id=rw.penerima_id JOIN users u ON u.id=rw.user_id WHERE $inPb ORDER BY rw.created_at DESC LIMIT 100");
stmt_bind($stmt, $types, $params); $data['status'] = fetch_all_stmt($stmt);
$types = ''; $params = [];
$inPn = build_in_condition('pn.rumah_ibadah_id', $ids, $types, $params);
$stmt = $koneksi->prepare("SELECT pn.*, pb.nama_penerima, ri.nama_rumah_ibadah, pr.nama_program FROM penyaluran_bantuan pn JOIN penerima_bantuan pb ON pb.id=pn.penerima_id JOIN rumah_ibadah ri ON ri.id=pn.rumah_ibadah_id LEFT JOIN program_bantuan pr ON pr.id=pn.program_id WHERE $inPn ORDER BY pn.tanggal_penyaluran DESC, pn.id DESC LIMIT 100");
stmt_bind($stmt, $types, $params); $data['penyaluran'] = fetch_all_stmt($stmt);
}
} else {
$uid = (int)$_SESSION['user_id'];
$stmt = $koneksi->prepare("SELECT pb.*, ri.nama_rumah_ibadah FROM penerima_bantuan pb LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id WHERE pb.user_id=? ORDER BY pb.created_at DESC LIMIT 100");
$stmt->bind_param('i', $uid); $data['pengajuan'] = fetch_all_stmt($stmt);
$stmt = $koneksi->prepare("SELECT rw.*, pb.nama_penerima, u.nama AS nama_user FROM riwayat_status rw JOIN penerima_bantuan pb ON pb.id=rw.penerima_id JOIN users u ON u.id=rw.user_id WHERE pb.user_id=? ORDER BY rw.created_at DESC LIMIT 100");
$stmt->bind_param('i', $uid); $data['status'] = fetch_all_stmt($stmt);
$stmt = $koneksi->prepare("SELECT pn.*, pb.nama_penerima, ri.nama_rumah_ibadah, pr.nama_program FROM penyaluran_bantuan pn JOIN penerima_bantuan pb ON pb.id=pn.penerima_id JOIN rumah_ibadah ri ON ri.id=pn.rumah_ibadah_id LEFT JOIN program_bantuan pr ON pr.id=pn.program_id WHERE pb.user_id=? ORDER BY pn.tanggal_penyaluran DESC, pn.id DESC LIMIT 100");
$stmt->bind_param('i', $uid); $data['penyaluran'] = fetch_all_stmt($stmt);
}
json_response('ok', 'Riwayat berhasil diambil.', $data);
?>
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* ambil_user.php
* Mengambil daftar akun untuk tabel kelola akun di dashboard.
* Filter: role, status_akun, keyword.
*/
require_once __DIR__ . '/auth.php';
require_login(true);
require_role(['super_admin', 'admin_rumah_ibadah'], true);
$actor = current_user();
$actorRole = $actor['role'];
$filterRole = getv('role', 'semua');
$status = getv('status_akun', 'semua');
$keyword = clean_text(getv('keyword', ''), 100);
$where = [];
$types = '';
$params = [];
if ($actorRole === 'super_admin') {
// Super admin mengelola akun admin rumah ibadah dan penerima saja.
$where[] = "u.role IN ('admin_rumah_ibadah','penerima')";
if (in_array($filterRole, ['admin_rumah_ibadah', 'penerima'], true)) {
$where[] = "u.role=?";
$types .= 's';
$params[] = $filterRole;
}
} else {
// Admin rumah ibadah hanya mengelola akun penerima.
$where[] = "u.role='penerima'";
// Jika admin punya lebih dari satu rumah ibadah, semua ID rumah ibadah miliknya harus ikut terbaca.
// Akun penerima tanpa titik tetap ditampilkan agar bisa dipakai saat tambah penerima.
$ids = get_admin_rumah_ibadah_ids((int)$actor['id'], true);
if (!empty($ids)) {
$in = build_in_condition('pb.rumah_ibadah_id', $ids, $types, $params);
$where[] = "(pb.id IS NULL OR $in)";
} else {
$where[] = "pb.id IS NULL";
}
}
if (in_array($status, ['aktif', 'nonaktif'], true)) {
$where[] = "u.status_akun=?";
$types .= 's';
$params[] = $status;
}
if ($keyword !== '') {
$where[] = "(u.nama LIKE ? OR u.username LIKE ?)";
$kw = '%' . $keyword . '%';
$types .= 'ss';
$params[] = $kw;
$params[] = $kw;
}
$sql = "SELECT
u.id, u.nama, u.username, u.role, u.status_akun, u.created_at, u.updated_at,
COUNT(DISTINCT pb.id) AS jumlah_titik_penerima,
COUNT(DISTINCT ri.id) AS jumlah_rumah_ibadah,
GROUP_CONCAT(DISTINCT ri.nama_rumah_ibadah SEPARATOR ', ') AS rumah_ibadah_dikelola,
GROUP_CONCAT(DISTINCT rpb.nama_rumah_ibadah SEPARATOR ', ') AS rumah_ibadah_penerima
FROM users u
LEFT JOIN penerima_bantuan pb ON pb.user_id=u.id AND COALESCE(pb.status_hapus,'normal')<>'disetujui'
LEFT JOIN rumah_ibadah rpb ON rpb.id=pb.rumah_ibadah_id
LEFT JOIN rumah_ibadah ri ON ri.user_id=u.id AND COALESCE(ri.status_hapus,'normal')<>'disetujui'
WHERE " . implode(' AND ', $where) . "
GROUP BY u.id
ORDER BY u.updated_at DESC, u.id DESC";
$stmt = $koneksi->prepare($sql);
if (!$stmt) json_response('err', 'Query daftar akun gagal: ' . $koneksi->error, [], 500);
if ($types) stmt_bind($stmt, $types, $params);
$rows = fetch_all_stmt($stmt);
json_response('ok', 'Daftar akun berhasil diambil.', $rows);
?>
+314
View File
@@ -0,0 +1,314 @@
<?php
/**
* auth.php
* Fungsi session, role pengguna, response JSON, validasi umum,
* prepared statement helper, perhitungan Haversine, dan sinkronisasi area.
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/koneksi.php';
function json_response($status = 'ok', $message = '', $data = [], $http_code = 200) {
http_response_code($http_code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'status' => $status,
'message' => $message,
'data' => $data
], JSON_UNESCAPED_UNICODE);
exit;
}
function require_post() {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
json_response('err', 'Method harus POST.', [], 405);
}
}
function is_logged_in() {
return isset($_SESSION['user_id'], $_SESSION['role']);
}
function require_login($json = false) {
if (!is_logged_in()) {
if ($json) json_response('err', 'Silakan login terlebih dahulu.', [], 401);
header('Location: login.php');
exit;
}
}
function current_user() {
global $koneksi;
if (!is_logged_in()) return null;
$id = (int) $_SESSION['user_id'];
$stmt = $koneksi->prepare("SELECT id, nama, username, role, status_akun FROM users WHERE id=? LIMIT 1");
$stmt->bind_param('i', $id);
$stmt->execute();
$res = $stmt->get_result();
return $res->fetch_assoc();
}
function has_role($roles) {
if (!is_array($roles)) $roles = [$roles];
return is_logged_in() && in_array($_SESSION['role'], $roles, true);
}
function require_role($roles, $json = false) {
if (!has_role($roles)) {
if ($json) json_response('err', 'Akses ditolak. Role tidak sesuai.', [], 403);
http_response_code(403);
echo '<h2>403 - Akses ditolak</h2>';
exit;
}
}
function post($key, $default = '') {
return isset($_POST[$key]) ? trim((string) $_POST[$key]) : $default;
}
function getv($key, $default = '') {
return isset($_GET[$key]) ? trim((string) $_GET[$key]) : $default;
}
function clean_text($str, $max = 255) {
$str = trim((string) $str);
if (mb_strlen($str) > $max) $str = mb_substr($str, 0, $max);
return $str;
}
function valid_enum($value, $allowed) {
return in_array($value, $allowed, true);
}
function valid_latlng($lat, $lng) {
return is_numeric($lat) && is_numeric($lng) && $lat >= -90 && $lat <= 90 && $lng >= -180 && $lng <= 180;
}
function haversine_meter($lat1, $lon1, $lat2, $lon2) {
$R = 6371000; // meter
$dLat = deg2rad($lat2 - $lat1);
$dLon = deg2rad($lon2 - $lon1);
$a = sin($dLat / 2) * sin($dLat / 2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon / 2) * sin($dLon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return $R * $c;
}
function stmt_bind($stmt, $types, $params) {
if ($types === '' || empty($params)) return true;
$refs = [];
$refs[] = $types;
foreach ($params as $k => $v) {
$refs[] = &$params[$k];
}
return call_user_func_array([$stmt, 'bind_param'], $refs);
}
function fetch_all_stmt($stmt) {
$stmt->execute();
$result = $stmt->get_result();
$rows = [];
while ($row = $result->fetch_assoc()) $rows[] = $row;
return $rows;
}
function build_in_condition($column, $values, &$types, &$params) {
$ids = [];
foreach ((array)$values as $v) {
$iv = (int)$v;
if ($iv > 0) $ids[] = $iv;
}
$ids = array_values(array_unique($ids));
if (empty($ids)) return '1=0';
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$types .= str_repeat('i', count($ids));
foreach ($ids as $id) $params[] = $id;
return "$column IN ($placeholders)";
}
function get_admin_rumah_ibadah_list($user_id = null, $only_diterima = true) {
global $koneksi;
if ($user_id === null) $user_id = (int) $_SESSION['user_id'];
$sql = "SELECT * FROM rumah_ibadah WHERE user_id=? AND COALESCE(status_hapus,'normal')<>'disetujui'";
if ($only_diterima) $sql .= " AND status_pengajuan='diterima'";
$sql .= " ORDER BY id ASC";
$stmt = $koneksi->prepare($sql);
$stmt->bind_param('i', $user_id);
return fetch_all_stmt($stmt);
}
function get_admin_rumah_ibadah_ids($user_id = null, $only_diterima = true) {
$rows = get_admin_rumah_ibadah_list($user_id, $only_diterima);
return array_map('intval', array_column($rows, 'id'));
}
function get_admin_rumah_ibadah($user_id = null) {
// Dipertahankan untuk kompatibilitas lama. Untuk logic baru, pakai get_admin_rumah_ibadah_list()/ids().
$rows = get_admin_rumah_ibadah_list($user_id, true);
return $rows[0] ?? null;
}
function is_admin_rumah_ibadah_id($rumah_ibadah_id, $user_id = null, $only_diterima = true) {
if ($user_id === null) $user_id = (int) $_SESSION['user_id'];
$ids = get_admin_rumah_ibadah_ids($user_id, $only_diterima);
return in_array((int)$rumah_ibadah_id, $ids, true);
}
function get_nearest_rumah_ibadah($lat, $lng, $max_meter = 300, $admin_user_id = null) {
global $koneksi;
$rows = [];
if ($admin_user_id !== null) {
$stmt = $koneksi->prepare("SELECT * FROM rumah_ibadah WHERE user_id=? AND status_pengajuan='diterima' AND COALESCE(status_hapus,'normal')<>'disetujui'");
$admin_user_id = (int)$admin_user_id;
$stmt->bind_param('i', $admin_user_id);
$rumahRows = fetch_all_stmt($stmt);
} else {
$rumahRows = [];
$res = $koneksi->query("SELECT * FROM rumah_ibadah WHERE status_pengajuan='diterima' AND COALESCE(status_hapus,'normal')<>'disetujui'");
while ($row = $res->fetch_assoc()) $rumahRows[] = $row;
}
foreach ($rumahRows as $row) {
$jarak = haversine_meter((float) $lat, (float) $lng, (float) $row['latitude'], (float) $row['longitude']);
if ($jarak <= $max_meter) {
$row['jarak_meter'] = round($jarak, 2);
$rows[] = $row;
}
}
usort($rows, fn($a, $b) => $a['jarak_meter'] <=> $b['jarak_meter']);
return $rows[0] ?? null;
}
function get_penerima_by_id($id) {
global $koneksi;
$stmt = $koneksi->prepare("SELECT pb.*, ri.nama_rumah_ibadah, ri.user_id AS admin_rumah_user_id, ri.latitude AS ri_latitude, ri.longitude AS ri_longitude FROM penerima_bantuan pb LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id WHERE pb.id=? LIMIT 1");
$stmt->bind_param('i', $id);
$stmt->execute();
return $stmt->get_result()->fetch_assoc();
}
function get_rumah_ibadah_by_id($id) {
global $koneksi;
$stmt = $koneksi->prepare("SELECT ri.*, u.nama AS nama_admin, u.username AS username_admin FROM rumah_ibadah ri LEFT JOIN users u ON u.id=ri.user_id WHERE ri.id=? LIMIT 1");
$stmt->bind_param('i', $id);
$stmt->execute();
return $stmt->get_result()->fetch_assoc();
}
function can_manage_rumah_ibadah($rumah) {
if (!$rumah || !is_logged_in()) return false;
if ($_SESSION['role'] === 'super_admin') return true;
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
return (int)$rumah['user_id'] === (int)$_SESSION['user_id'];
}
return false;
}
function can_manage_penerima($penerima) {
if (!$penerima || !is_logged_in()) return false;
if ($_SESSION['role'] === 'super_admin') return true;
if ($_SESSION['role'] === 'penerima' && (int) $penerima['user_id'] === (int) $_SESSION['user_id']) return true;
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
return !empty($penerima['rumah_ibadah_id']) && is_admin_rumah_ibadah_id((int)$penerima['rumah_ibadah_id'], null, true);
}
return false;
}
function insert_riwayat_status($penerima_id, $status_lama, $status_baru, $catatan) {
global $koneksi;
$uid = (int) $_SESSION['user_id'];
$stmt = $koneksi->prepare("INSERT INTO riwayat_status (penerima_id, user_id, status_lama, status_baru, catatan) VALUES (?,?,?,?,?)");
$stmt->bind_param('iisss', $penerima_id, $uid, $status_lama, $status_baru, $catatan);
$stmt->execute();
}
function get_user_by_username_role($username, $roles) {
global $koneksi;
if (!is_array($roles)) $roles = [$roles];
$username = clean_text($username, 50);
if ($username === '') return null;
$placeholders = implode(',', array_fill(0, count($roles), '?'));
$types = 's' . str_repeat('s', count($roles));
$params = array_merge([$username], $roles);
$stmt = $koneksi->prepare("SELECT id, nama, username, role, status_akun FROM users WHERE username=? AND role IN ($placeholders) AND status_akun='aktif' LIMIT 1");
stmt_bind($stmt, $types, $params);
$stmt->execute();
return $stmt->get_result()->fetch_assoc();
}
function has_active_penerima_pin($user_id, $exclude_penerima_id = 0) {
global $koneksi;
$user_id = (int)$user_id;
$exclude_penerima_id = (int)$exclude_penerima_id;
if ($user_id <= 0) return false;
$sql = "SELECT id FROM penerima_bantuan
WHERE user_id=?
AND status_pengajuan IN ('menunggu','diterima')
AND COALESCE(status_hapus,'normal')<>'disetujui'";
$types = 'i';
$params = [$user_id];
if ($exclude_penerima_id > 0) {
$sql .= " AND id<>?";
$types .= 'i';
$params[] = $exclude_penerima_id;
}
$sql .= " LIMIT 1";
$stmt = $koneksi->prepare($sql);
stmt_bind($stmt, $types, $params);
$stmt->execute();
return (bool)$stmt->get_result()->fetch_assoc();
}
function sync_single_penerima_area($penerima_id) {
global $koneksi;
$penerima_id = (int)$penerima_id;
$stmt = $koneksi->prepare("SELECT id, latitude, longitude FROM penerima_bantuan WHERE id=? AND status_pengajuan IN ('menunggu','diterima') AND COALESCE(status_hapus,'normal')<>'disetujui' LIMIT 1");
$stmt->bind_param('i', $penerima_id);
$stmt->execute();
$pb = $stmt->get_result()->fetch_assoc();
if (!$pb) return false;
$nearest = get_nearest_rumah_ibadah((float)$pb['latitude'], (float)$pb['longitude'], 300);
if ($nearest) {
$riId = (int)$nearest['id'];
$jarak = (float)$nearest['jarak_meter'];
$stmtUp = $koneksi->prepare("UPDATE penerima_bantuan SET rumah_ibadah_id=?, jarak_meter=? WHERE id=?");
$stmtUp->bind_param('idi', $riId, $jarak, $penerima_id);
} else {
$stmtUp = $koneksi->prepare("UPDATE penerima_bantuan SET rumah_ibadah_id=NULL, jarak_meter=NULL WHERE id=?");
$stmtUp->bind_param('i', $penerima_id);
}
return $stmtUp->execute();
}
function sync_all_penerima_area() {
global $koneksi;
$ids = [];
$res = $koneksi->query("SELECT id FROM penerima_bantuan WHERE status_pengajuan IN ('menunggu','diterima') AND COALESCE(status_hapus,'normal')<>'disetujui'");
while ($row = $res->fetch_assoc()) $ids[] = (int)$row['id'];
$updated = 0;
foreach ($ids as $id) {
if (sync_single_penerima_area($id)) $updated++;
}
return $updated;
}
function sync_penerima_area_by_rumah_ibadah($rumah_ibadah_id) {
// Tetap menerima parameter lama, tetapi sinkronisasi dihitung ulang secara global agar radius beririsan memilih RI terdekat.
return sync_all_penerima_area();
}
function detach_penerima_from_rumah_ibadah($rumah_ibadah_id) {
global $koneksi;
$rumah_ibadah_id = (int) $rumah_ibadah_id;
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET rumah_ibadah_id=NULL, jarak_meter=NULL WHERE rumah_ibadah_id=? AND status_pengajuan IN ('menunggu','diterima')");
$stmt->bind_param('i', $rumah_ibadah_id);
$stmt->execute();
$affected = $stmt->affected_rows;
// Setelah dilepas, cari ulang rumah ibadah aktif terdekat lain jika ada.
sync_all_penerima_area();
return $affected;
}
?>
+436
View File
@@ -0,0 +1,436 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(false);
$user = current_user();
$role = $user['role'];
$bulanIni = (int) date('n');
$tahunIni = (int) date('Y');
function scalar_count($sql, $types = '', $params = []) {
global $koneksi;
$stmt = $koneksi->prepare($sql);
if ($types) stmt_bind($stmt, $types, $params);
$stmt->execute();
$row = $stmt->get_result()->fetch_assoc();
return (int) array_values($row)[0];
}
$riAdminList = ($role === 'admin_rumah_ibadah') ? get_admin_rumah_ibadah_list((int)$user['id'], true) : [];
$riAdminIds = array_map('intval', array_column($riAdminList, 'id'));
$stats = [];
if ($role === 'super_admin') {
$stats['Total Rumah Ibadah'] = scalar_count("SELECT COUNT(*) FROM rumah_ibadah WHERE COALESCE(status_hapus,'normal')<>'disetujui'");
$stats['Rumah Ibadah Menunggu'] = scalar_count("SELECT COUNT(*) FROM rumah_ibadah WHERE status_pengajuan='menunggu' AND COALESCE(status_hapus,'normal')<>'disetujui'");
$stats['Total Penerima'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE COALESCE(status_hapus,'normal')<>'disetujui'");
$stats['Penerima Menunggu'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE status_pengajuan='menunggu' AND COALESCE(status_hapus,'normal')<>'disetujui'");
$stats['Penerima Diterima'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE status_pengajuan='diterima' AND COALESCE(status_hapus,'normal')<>'disetujui'");
$stats['Penerima Ditolak'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE status_pengajuan='ditolak' AND COALESCE(status_hapus,'normal')<>'disetujui'");
$stats['Penerima Dibatalkan'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE status_pengajuan='dibatalkan' AND COALESCE(status_hapus,'normal')<>'disetujui'");
$stats['Program Aktif'] = scalar_count("SELECT COUNT(*) FROM program_bantuan WHERE status_program='aktif'");
$stats['Bantuan Tersalurkan'] = scalar_count("SELECT COUNT(*) FROM penyaluran_bantuan");
} elseif ($role === 'admin_rumah_ibadah') {
if (!empty($riAdminIds)) {
$stats['Jumlah Rumah Ibadah'] = count($riAdminIds);
$stats['Rumah Ibadah'] = implode(', ', array_column($riAdminList, 'nama_rumah_ibadah'));
$types = ''; $params = []; $in = build_in_condition('rumah_ibadah_id', $riAdminIds, $types, $params);
$stats['Pengajuan Dalam Radius'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE $in AND status_pengajuan='menunggu' AND COALESCE(status_hapus,'normal')<>'disetujui'", $types, $params);
$types = ''; $params = []; $in = build_in_condition('rumah_ibadah_id', $riAdminIds, $types, $params);
$stats['Penerima Diterima'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE $in AND status_pengajuan='diterima' AND COALESCE(status_hapus,'normal')<>'disetujui'", $types, $params);
$types = ''; $params = []; $in = build_in_condition('rumah_ibadah_id', $riAdminIds, $types, $params);
$stats['Penerima Ditolak'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE $in AND status_pengajuan='ditolak' AND COALESCE(status_hapus,'normal')<>'disetujui'", $types, $params);
$types = ''; $params = []; $in = build_in_condition('rumah_ibadah_id', $riAdminIds, $types, $params);
$stats['Program Aktif'] = scalar_count("SELECT COUNT(*) FROM program_bantuan WHERE $in AND status_program='aktif'", $types, $params);
$types = ''; $params = []; $in = build_in_condition('rumah_ibadah_id', $riAdminIds, $types, $params);
$stats['Bantuan Tersalurkan'] = scalar_count("SELECT COUNT(*) FROM penyaluran_bantuan WHERE $in", $types, $params);
} else {
$stats['Rumah Ibadah'] = 'Belum ada rumah ibadah diterima';
}
} elseif ($role === 'penerima') {
$uid = (int)$user['id'];
$stats['Pengajuan Saya'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE user_id=? AND COALESCE(status_hapus,'normal')<>'disetujui'", 'i', [$uid]);
$stats['Status Diterima'] = scalar_count("SELECT COUNT(*) FROM penerima_bantuan WHERE user_id=? AND status_pengajuan='diterima' AND COALESCE(status_hapus,'normal')<>'disetujui'", 'i', [$uid]);
$stats['Bantuan Bulan Ini'] = scalar_count("SELECT COUNT(*) FROM penyaluran_bantuan pn JOIN penerima_bantuan pb ON pb.id=pn.penerima_id WHERE pb.user_id=? AND MONTH(pn.tanggal_penyaluran)=? AND YEAR(pn.tanggal_penyaluran)=?", 'iii', [$uid, $bulanIni, $tahunIni]);
}
$hapusRumahRows = [];
$hapusPenerimaRows = [];
if ($role === 'super_admin') {
$stmt = $koneksi->prepare("SELECT ri.*, u.nama AS nama_admin, u.username AS username_admin FROM rumah_ibadah ri LEFT JOIN users u ON u.id=ri.user_id WHERE ri.status_hapus='menunggu' ORDER BY ri.updated_at DESC");
$hapusRumahRows = fetch_all_stmt($stmt);
$stmt = $koneksi->prepare("SELECT pb.*, u.username AS username_penerima, ri.nama_rumah_ibadah FROM penerima_bantuan pb LEFT JOIN users u ON u.id=pb.user_id LEFT JOIN rumah_ibadah ri ON ri.id=pb.rumah_ibadah_id WHERE pb.status_hapus='menunggu' ORDER BY pb.updated_at DESC");
$hapusPenerimaRows = fetch_all_stmt($stmt);
}
?>
<!DOCTYPE html><html lang="id"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Dashboard WebGIS Poverty Mapping</title>
<style>
*{box-sizing:border-box}body{margin:0;font-family:Arial,sans-serif;background:#f8fafc;color:#0f172a}.top{background:#0f766e;color:#fff;padding:18px 24px;display:flex;justify-content:space-between;gap:12px;align-items:center}.top h1{font-size:22px;margin:0}.top a{color:#fff;text-decoration:none;background:rgba(255,255,255,.16);padding:8px 12px;border-radius:8px}.wrap{padding:22px;max-width:1280px;margin:auto}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:14px}.card,.section{background:#fff;border:1px solid #e2e8f0;border-radius:14px;box-shadow:0 8px 24px rgba(15,23,42,.06)}.card{padding:16px}.card .k{color:#64748b;font-size:12px}.card .v{font-size:22px;font-weight:800;margin-top:7px}.section{padding:18px;margin-top:18px}.section h2{font-size:18px;margin:0 0 14px}.filters,.row2{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.fg label{display:block;font-size:12px;font-weight:700;color:#475569;margin-bottom:5px}.fg input,.fg select,.fg textarea{width:100%;border:1px solid #cbd5e1;border-radius:9px;padding:9px}.fg textarea{min-height:70px}.btn{border:0;border-radius:9px;padding:9px 12px;font-weight:700;cursor:pointer}.btn[disabled]{cursor:not-allowed;opacity:.55}.btn.primary{background:#0f766e;color:#fff}.btn.gray{background:#e2e8f0;color:#334155}.btn.small{padding:7px 9px;font-size:12px}table{width:100%;border-collapse:collapse;font-size:13px}th,td{padding:10px;border-bottom:1px solid #e2e8f0;text-align:left;vertical-align:top}th{background:#f1f5f9;color:#334155}.muted{color:#64748b;font-size:12px}.badge{display:inline-block;border-radius:999px;padding:4px 8px;font-size:11px;font-weight:700;background:#e2e8f0}.belum_diberi{background:#fee2e2;color:#991b1b}.sudah_diberi,.sesuai_target{background:#dcfce7;color:#166534}.belum_memenuhi_target{background:#fef3c7;color:#92400e}.melebihi_target{background:#e0e7ff;color:#3730a3}.readonly{background:#f8fafc}.notice{background:#ecfeff;border:1px solid #a5f3fc;color:#155e75;padding:10px;border-radius:10px;margin-bottom:12px}.toastbox{position:fixed;right:18px;top:18px;z-index:9999;display:grid;gap:10px}.toast{background:#0f172a;color:#fff;padding:12px 14px;border-radius:10px;box-shadow:0 12px 30px rgba(15,23,42,.25);max-width:360px;font-size:13px}.toast.err{background:#991b1b}.toast.ok{background:#166534}
</style></head><body>
<div class="top"><div><h1>Dashboard WebGIS Poverty Mapping</h1><div class="muted" style="color:#d1fae5">Role: <?= htmlspecialchars($role) ?> | <?= htmlspecialchars($user['nama']) ?></div></div><div><a href="index.php">Buka Peta</a> <a href="logout.php">Logout</a></div></div>
<div id="toastBox" class="toastbox"></div>
<div class="wrap">
<div class="grid"><?php foreach($stats as $k=>$v): ?><div class="card"><div class="k"><?= htmlspecialchars($k) ?></div><div class="v"><?= htmlspecialchars((string)$v) ?></div></div><?php endforeach; ?></div>
<?php if($role === 'super_admin'): ?>
<div class="section">
<h2>Persetujuan Hapus Marker/Pin</h2>
<p class="muted">Admin rumah ibadah dan penerima hanya mengajukan hapus. Marker benar-benar hilang dari peta setelah disetujui super admin.</p>
<h3 style="font-size:15px;margin:12px 0 8px">Pengajuan Hapus Rumah Ibadah</h3>
<div style="overflow:auto"><table><thead><tr><th>Rumah Ibadah</th><th>Admin</th><th>Alasan</th><th>Aksi</th></tr></thead><tbody>
<?php if(empty($hapusRumahRows)): ?><tr><td colspan="4">Tidak ada pengajuan hapus rumah ibadah.</td></tr><?php endif; ?>
<?php foreach($hapusRumahRows as $r): ?><tr>
<td><b><?= htmlspecialchars($r['nama_rumah_ibadah']) ?></b><br><span class="muted"><?= htmlspecialchars($r['agama']) ?> | <?= htmlspecialchars($r['alamat']) ?></span></td>
<td><?= htmlspecialchars($r['nama_admin'] ?? '-') ?><br><span class="muted"><?= htmlspecialchars($r['username_admin'] ?? '-') ?></span></td>
<td><?= htmlspecialchars($r['alasan_hapus'] ?? '-') ?></td>
<td><button class="btn primary small" onclick="updateStatusRumahIbadah(<?= (int)$r['id'] ?>,'hapus_disetujui')">Setujui</button> <button class="btn gray small" onclick="updateStatusRumahIbadah(<?= (int)$r['id'] ?>,'hapus_ditolak')">Tolak</button></td>
</tr><?php endforeach; ?>
</tbody></table></div>
<h3 style="font-size:15px;margin:16px 0 8px">Pengajuan Hapus Penerima</h3>
<div style="overflow:auto"><table><thead><tr><th>Penerima</th><th>Rumah Ibadah</th><th>Alasan</th><th>Aksi</th></tr></thead><tbody>
<?php if(empty($hapusPenerimaRows)): ?><tr><td colspan="4">Tidak ada pengajuan hapus penerima.</td></tr><?php endif; ?>
<?php foreach($hapusPenerimaRows as $p): ?><tr>
<td><b><?= htmlspecialchars($p['nama_penerima']) ?></b><br><span class="muted">User: <?= htmlspecialchars($p['username_penerima'] ?? '-') ?> | <?= htmlspecialchars($p['alamat']) ?></span></td>
<td><?= htmlspecialchars($p['nama_rumah_ibadah'] ?? '-') ?></td>
<td><?= htmlspecialchars($p['alasan_hapus'] ?? '-') ?></td>
<td><button class="btn primary small" onclick="updateStatusPenerima(<?= (int)$p['id'] ?>,'hapus_disetujui')">Setujui</button> <button class="btn gray small" onclick="updateStatusPenerima(<?= (int)$p['id'] ?>,'hapus_ditolak')">Tolak</button></td>
</tr><?php endforeach; ?>
</tbody></table></div>
</div>
<?php endif; ?>
<?php if($role === 'super_admin' || $role === 'admin_rumah_ibadah'): ?>
<div class="section">
<h2>Kelola Akun Pengguna</h2>
<p class="muted">
<?php if($role === 'super_admin'): ?>
Super admin dapat mengelola akun penerima dan admin rumah ibadah. Akun super admin lain tidak ditampilkan di daftar ini.
<?php else: ?>
Admin rumah ibadah hanya dapat mengelola akun penerima. Akun admin rumah ibadah lain tidak bisa dilihat atau diubah.
<?php endif; ?>
</p>
<input type="hidden" id="u_id" value="">
<div class="filters">
<div class="fg"><label>Nama</label><input id="u_nama" placeholder="Contoh: Admin Masjid / Nama Penerima"></div>
<div class="fg"><label>Username</label><input id="u_username" placeholder="Contoh: adminmasjid / penerima01"></div>
<div class="fg"><label>Password</label><input id="u_password" type="password" placeholder="Minimal 6 karakter. Kosongkan saat edit jika tidak diganti."></div>
<div class="fg"><label>Role</label><select id="u_role">
<?php if($role === 'super_admin'): ?><option value="admin_rumah_ibadah">Admin Rumah Ibadah</option><?php endif; ?>
<option value="penerima">Penerima</option>
</select></div>
<div class="fg"><label>Status</label><select id="u_status"><option value="aktif">Aktif</option><option value="nonaktif">Nonaktif</option></select></div>
<div class="fg"><label>&nbsp;</label><button class="btn primary" id="u_submit_btn" onclick="submitUser()">Buat Akun</button></div>
<div class="fg"><label>&nbsp;</label><button class="btn gray" onclick="resetUserForm()">Reset Form</button></div>
</div>
<h3 style="font-size:15px;margin:16px 0 8px">Daftar Akun</h3>
<div class="filters">
<div class="fg"><label>Filter Role</label><select id="fu_role" onchange="loadUsers()">
<?php if($role === 'super_admin'): ?>
<option value="semua">Semua</option>
<option value="admin_rumah_ibadah">Admin Rumah Ibadah</option>
<?php endif; ?>
<option value="penerima">Penerima</option>
</select></div>
<div class="fg"><label>Filter Status</label><select id="fu_status" onchange="loadUsers()"><option value="semua">Semua</option><option value="aktif">Aktif</option><option value="nonaktif">Nonaktif</option></select></div>
<div class="fg"><label>Cari Nama/Username</label><input id="fu_keyword" placeholder="Ketik nama atau username" oninput="debouncedLoadUsers()"></div>
<div class="fg"><label>&nbsp;</label><button class="btn primary" onclick="loadUsers()">Terapkan Filter</button></div>
</div>
<div style="overflow:auto"><table><thead><tr><th>Akun</th><th>Role</th><th>Status</th><th>Relasi Data</th><th>Dibuat/Update</th><th>Aksi</th></tr></thead><tbody id="akunBody"><tr><td colspan="6">Memuat akun...</td></tr></tbody></table></div>
</div>
<?php endif; ?>
<div class="section">
<h2>Monitoring Penyaluran Per Periode</h2>
<div class="filters">
<div class="fg"><label>Bulan</label><select id="f_bulan"><?php for($i=1;$i<=12;$i++): ?><option value="<?= $i ?>" <?= $i===$bulanIni?'selected':'' ?>><?= $i ?></option><?php endfor; ?></select></div>
<div class="fg"><label>Tahun</label><input id="f_tahun" type="number" value="<?= $tahunIni ?>"></div>
<div class="fg"><label>Rumah Ibadah</label><select id="f_rumah_ibadah"><option value="">Semua</option></select></div>
<div class="fg"><label>Agama</label><select id="f_agama"><option value="">Semua</option><option value="islam">Islam</option><option value="kristen">Kristen</option><option value="katolik">Katolik</option><option value="hindu">Hindu</option><option value="buddha">Buddha</option><option value="konghucu">Konghucu</option></select></div>
<div class="fg"><label>Program Bantuan</label><select id="f_program"><option value="">Semua</option></select></div>
<div class="fg"><label>Jenis Bantuan</label><select id="f_jenis"><option value="">Semua</option><option value="zakat">Zakat</option><option value="sembako">Sembako</option><option value="uang_tunai">Uang Tunai</option><option value="lainnya">Lainnya</option></select></div>
<div class="fg"><label>Status</label><select id="f_status"><option value="semua">Semua</option><option value="belum_diberi">Belum diberi</option><option value="sudah_diberi">Sudah diberi</option><option value="belum_memenuhi_target">Belum memenuhi target</option><option value="sesuai_target">Sesuai target</option><option value="melebihi_target">Melebihi target</option></select></div>
<div class="fg"><label>&nbsp;</label><button class="btn primary" onclick="loadLaporanPenyaluran()">Terapkan Filter</button></div>
</div>
</div>
<div class="section">
<h2>Rekap Penyaluran</h2>
<div class="grid" id="rekapBox"></div>
</div>
<?php if($role === 'super_admin' || $role === 'admin_rumah_ibadah'): ?>
<div class="row2">
<div class="section">
<h2>Buat Program Bantuan</h2>
<div class="notice">Jenis bantuan, nominal, dan jumlah item yang diatur di sini akan otomatis dipakai saat penyaluran. Petugas tidak perlu mengetik ulang agar data sinkron.</div>
<?php if($role === 'super_admin' || $role === 'admin_rumah_ibadah'): ?><div class="fg"><label>Rumah Ibadah</label><select id="pr_rumah_ibadah_id"><option value="">Pilih rumah ibadah</option></select></div><?php endif; ?>
<div class="fg"><label>Nama Program</label><input id="pr_nama" placeholder="Contoh: Sembako Januari 2026"></div>
<div class="filters">
<div class="fg"><label>Jenis</label><select id="pr_jenis"><option value="zakat">Zakat</option><option value="sembako">Sembako</option><option value="uang_tunai">Uang Tunai</option><option value="lainnya">Lainnya</option></select></div>
<div class="fg"><label>Bulan</label><input type="number" id="pr_bulan" value="<?= $bulanIni ?>"></div>
<div class="fg"><label>Tahun</label><input type="number" id="pr_tahun" value="<?= $tahunIni ?>"></div>
<div class="fg"><label>Target Frekuensi</label><input type="number" id="pr_target" value="1"></div>
</div>
<div class="filters">
<div class="fg"><label>Nominal Program</label><input type="number" id="pr_nominal" placeholder="Opsional, contoh: 50000"></div>
<div class="fg"><label>Jumlah Item/Paket</label><input id="pr_jumlah_item" placeholder="Contoh: 1 paket sembako / 5 kg beras"></div>
</div>
<div class="fg"><label>Deskripsi</label><textarea id="pr_deskripsi"></textarea></div>
<button class="btn primary" onclick="submitProgramBantuan()">Simpan Program</button>
<h3 style="font-size:15px;margin:16px 0 8px">Daftar Program</h3>
<div style="overflow:auto"><table><thead><tr><th>Program</th><th>Periode</th><th>Jenis</th><th>Nominal/Item</th><th>Status</th><th>Aksi</th></tr></thead><tbody id="programBody"><tr><td colspan="6">Memuat program...</td></tr></tbody></table></div>
</div>
<div class="section">
<h2>Catat Penyaluran Bantuan</h2>
<div class="fg"><label>Program</label><select id="pn_program" onchange="syncProgramDetail()"></select></div>
<div class="fg"><label>Penerima Diterima</label><select id="pn_penerima"></select></div>
<div class="filters">
<div class="fg"><label>Jenis dari Program</label><input id="pn_jenis_view" class="readonly" readonly></div>
<div class="fg"><label>Nominal dari Program</label><input id="pn_nominal_view" class="readonly" readonly></div>
<div class="fg"><label>Jumlah Item dari Program</label><input id="pn_jumlah_item_view" class="readonly" readonly></div>
<div class="fg"><label>Tanggal</label><input type="date" id="pn_tanggal" value="<?= date('Y-m-d') ?>"></div>
</div>
<div class="fg"><label>Keterangan</label><textarea id="pn_keterangan" placeholder="Isi alasan jika bantuan melebihi target program"></textarea></div>
<button class="btn primary" onclick="submitPenyaluran()">Catat Bantuan</button>
</div>
</div>
<?php endif; ?>
<div class="section">
<h2>Data Laporan Penerima Sudah/Belum Diberi Bantuan</h2>
<div style="overflow:auto"><table><thead><tr><th>Penerima</th><th>Rumah Ibadah</th><th>Program</th><th>Target</th><th>Jumlah</th><th>Status</th><th>Terakhir</th><th>Riwayat Bulan Ini</th><th>Aksi</th></tr></thead><tbody id="laporanBody"><tr><td colspan="9">Memuat data...</td></tr></tbody></table></div>
</div>
<div class="section">
<h2>Riwayat Pengajuan dan Bantuan</h2>
<div id="riwayatBox" class="muted">Memuat riwayat...</div>
</div>
</div>
<script>
const USER_ROLE = <?= json_encode($role) ?>;
function esc(v){return String(v??'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m]))}
function toast(msg,type='ok'){ const box=document.getElementById('toastBox'); if(!box){console.log(msg);return;} const el=document.createElement('div'); el.className='toast '+type; el.textContent=msg; box.appendChild(el); setTimeout(()=>{el.style.opacity='0';el.style.transform='translateY(-6px)';},3000); setTimeout(()=>el.remove(),3500); }
function getFilterQuery(){ const q=new URLSearchParams(); q.set('bulan',document.getElementById('f_bulan').value); q.set('tahun',document.getElementById('f_tahun').value); q.set('rumah_ibadah_id',document.getElementById('f_rumah_ibadah')?.value||''); q.set('agama',document.getElementById('f_agama')?.value||''); q.set('program_id',document.getElementById('f_program').value); q.set('jenis_bantuan',document.getElementById('f_jenis').value); q.set('status_penyaluran',document.getElementById('f_status').value); return q.toString(); }
let userSearchTimer=null;
function debouncedLoadUsers(){ clearTimeout(userSearchTimer); userSearchTimer=setTimeout(loadUsers, 350); }
function resetUserForm(){
if(!document.getElementById('u_id')) return;
document.getElementById('u_id').value='';
['u_nama','u_username','u_password'].forEach(id=>document.getElementById(id).value='');
document.getElementById('u_status').value='aktif';
const roleSelect=document.getElementById('u_role'); if(roleSelect) roleSelect.selectedIndex=0;
document.getElementById('u_submit_btn').textContent='Buat Akun';
}
function submitUser(){
const namaEl=document.getElementById('u_nama'); if(!namaEl) return;
const id=document.getElementById('u_id').value;
const usernameEl=document.getElementById('u_username');
const passwordEl=document.getElementById('u_password');
// FIX: validasi input sebelum fetch.
if(!namaEl.value.trim()){ toast('Nama akun wajib diisi.','err'); return; }
if(!usernameEl.value.trim()){ toast('Username wajib diisi.','err'); return; }
if(!id && !passwordEl.value){ toast('Password wajib diisi saat membuat akun baru.','err'); return; }
if(passwordEl.value && passwordEl.value.length < 6){ toast('Password minimal 6 karakter.','err'); return; }
const fd=new FormData();
fd.append('id', id);
fd.append('nama', namaEl.value);
fd.append('username', usernameEl.value);
fd.append('password', passwordEl.value);
fd.append('role', document.getElementById('u_role').value);
fd.append('status_akun', document.getElementById('u_status').value);
const url = id ? 'edit_user.php' : 'tambah_user.php';
fetch(url,{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
toast(res.message, res.status==='ok'?'ok':'err');
if(res.status==='ok'){ resetUserForm(); loadUsers(); }
}).catch(e=>toast('Gagal menyimpan akun: '+e.message,'err'));
}
function loadUsers(){
const body=document.getElementById('akunBody'); if(!body) return;
const q=new URLSearchParams();
q.set('role', document.getElementById('fu_role').value);
q.set('status_akun', document.getElementById('fu_status').value);
q.set('keyword', document.getElementById('fu_keyword').value);
body.innerHTML='<tr><td colspan="6">Memuat akun...</td></tr>';
fetch('ambil_user.php?'+q.toString()).then(r=>r.json()).then(res=>{
if(res.status!=='ok') throw new Error(res.message);
renderUserTable(res.data||[]);
}).catch(e=>{ body.innerHTML=`<tr><td colspan="6">${esc(e.message)}</td></tr>`; });
}
function renderUserTable(rows){
const body=document.getElementById('akunBody'); if(!body) return;
if(!rows.length){body.innerHTML='<tr><td colspan="6">Tidak ada akun sesuai filter.</td></tr>'; return;}
body.innerHTML=rows.map(u=>{
const relasi = u.role==='admin_rumah_ibadah'
? `Rumah ibadah: ${esc(u.rumah_ibadah_dikelola||'-')}`
: `Titik penerima: ${esc(u.jumlah_titik_penerima||0)}<br><span class="muted">Rumah ibadah: ${esc(u.rumah_ibadah_penerima||'-')}</span>`;
return `<tr>
<td><b>${esc(u.nama)}</b><br><span class="muted">${esc(u.username)}</span></td>
<td><span class="badge">${esc(u.role)}</span></td>
<td><span class="badge">${esc(u.status_akun)}</span></td>
<td>${relasi}</td>
<td><span class="muted">${esc(u.created_at||'-')}<br>${esc(u.updated_at||'-')}</span></td>
<td><!--<button class="btn primary small" onclick='editUserForm(${JSON.stringify(u).replace(/'/g,"&#039;")})'>Edit</button>--> <button class="btn gray small" onclick="hapusUser(${u.id})">Hapus/Nonaktifkan</button></td>
</tr>`;
}).join('');
}
function editUserForm(u){
document.getElementById('u_id').value=u.id;
document.getElementById('u_nama').value=u.nama||'';
document.getElementById('u_username').value=u.username||'';
document.getElementById('u_password').value='';
const roleSelect=document.getElementById('u_role');
if([...roleSelect.options].some(o=>o.value===u.role)) roleSelect.value=u.role;
document.getElementById('u_status').value=u.status_akun||'aktif';
document.getElementById('u_submit_btn').textContent='Update Akun';
window.scrollTo({top: document.getElementById('u_nama').getBoundingClientRect().top + window.scrollY - 120, behavior:'smooth'});
}
function hapusUser(id){
if(!confirm('Hapus akun ini? Jika sudah punya relasi data, akun akan dinonaktifkan agar data tidak rusak.')) return;
const fd=new FormData(); fd.append('id', id);
fetch('hapus_user.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{toast(res.message); if(res.status==='ok'){resetUserForm();loadUsers();}}).catch(e=>toast('Gagal hapus akun: '+e.message));
}
function programOption(p){
const label=`${esc(p.nama_program)} (${p.bulan}/${p.tahun}, ${esc(p.jenis_bantuan)}, target ${p.target_frekuensi})`;
// FIX: tambah data-riid agar syncProgramDetail bisa filter penerima per RI.
return `<option value="${p.id}" data-jenis="${esc(p.jenis_bantuan)}" data-nominal="${esc(p.nominal??'')}" data-jumlah="${esc(p.jumlah_item??'')}" data-riid="${esc(p.rumah_ibadah_id??'')}">${label}</option>`;
}
function renderProgramTable(rows){
const body=document.getElementById('programBody'); if(!body) return;
if(!rows.length){body.innerHTML='<tr><td colspan="6">Belum ada program.</td></tr>'; return;}
body.innerHTML=rows.map(p=>`<tr><td><b>${esc(p.nama_program)}</b><br><span class="muted">${esc(p.deskripsi||'')}</span></td><td>${p.bulan}/${p.tahun}<br><span class="muted">Target ${p.target_frekuensi} kali</span></td><td>${esc(p.jenis_bantuan)}</td><td>Nominal: ${esc(p.nominal??'-')}<br>Item: ${esc(p.jumlah_item||'-')}</td><td><span class="badge">${esc(p.status_program)}</span></td><td><button class="btn gray small" onclick="hapusProgramBantuan(${p.id})">Hapus</button></td></tr>`).join('');
}
function hapusProgramBantuan(id){
if(!confirm('Hapus program bantuan ini? Jika sudah ada penyaluran, status program akan dibatalkan.')) return;
const fd=new FormData(); fd.append('id',id);
fetch('hapus_program_bantuan.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{toast(res.message); if(res.status==='ok'){loadProgramBantuan().then(loadLaporanPenyaluran);}}).catch(e=>toast('Gagal hapus program: '+e.message));
}
function syncProgramDetail(){
const sel=document.getElementById('pn_program'); if(!sel) return;
const opt=sel.options[sel.selectedIndex];
document.getElementById('pn_jenis_view').value=opt?.dataset?.jenis||'';
document.getElementById('pn_nominal_view').value=opt?.dataset?.nominal||'-';
document.getElementById('pn_jumlah_item_view').value=opt?.dataset?.jumlah||'-';
// FIX: reload penerima sesuai RI dari program yang dipilih.
const riId=opt?.dataset?.riid||null;
loadPenerimaDiterima(riId);
}
function loadProgramBantuan(){
return fetch('ambil_program_bantuan.php').then(r=>r.json()).then(res=>{
if(res.status!=='ok') throw new Error(res.message);
const f=document.getElementById('f_program'); const pn=document.getElementById('pn_program');
f.innerHTML='<option value="">Semua</option>'; if(pn) pn.innerHTML='';
(res.data||[]).forEach(p=>{ const opt=programOption(p); f.insertAdjacentHTML('beforeend',opt); if(pn && p.status_program!=='dibatalkan') pn.insertAdjacentHTML('beforeend',opt); });
renderProgramTable(res.data||[]);
syncProgramDetail();
});
}
function loadRumahIbadahUntukProgram(){
const pr=document.getElementById('pr_rumah_ibadah_id');
const fr=document.getElementById('f_rumah_ibadah');
fetch('ambil_data_peta.php').then(r=>r.json()).then(res=>{
const rows=(res.data.rumah_ibadah||[]).filter(ri=>ri.status_pengajuan==='diterima');
rows.forEach(ri=>{
const opt=`<option value="${ri.id}">${esc(ri.nama_rumah_ibadah)} - ${esc(ri.agama)}</option>`;
if(pr) pr.insertAdjacentHTML('beforeend',opt);
if(fr) fr.insertAdjacentHTML('beforeend',opt);
});
});
}
function loadPenerimaDiterima(filterRiId=null){
const el=document.getElementById('pn_penerima'); if(!el) return;
fetch('ambil_data_peta.php').then(r=>r.json()).then(res=>{
el.innerHTML='';
let list = (res.data.penerima||[]).filter(p=>p.status_pengajuan==='diterima');
// FIX: filter penerima sesuai RI dari program yang dipilih agar tidak bisa pilih penerima RI lain.
if(filterRiId) list = list.filter(p=>String(p.rumah_ibadah_id)===String(filterRiId));
if(!list.length) el.insertAdjacentHTML('beforeend','<option value="">-- Tidak ada penerima di RI ini --</option>');
list.forEach(p=>el.insertAdjacentHTML('beforeend',`<option value="${p.id}">${esc(p.nama_penerima)} - ${esc(p.nama_rumah_ibadah||'')}</option>`));
});
}
function submitProgramBantuan(){
const namaEl = document.getElementById('pr_nama');
const bulanEl = document.getElementById('pr_bulan');
const tahunEl = document.getElementById('pr_tahun');
// FIX: validasi input wajib sebelum fetch.
if(!namaEl?.value.trim()){ toast('Nama program wajib diisi.','err'); return; }
if(!bulanEl?.value || bulanEl.value < 1 || bulanEl.value > 12){ toast('Bulan tidak valid (1-12).','err'); return; }
if(!tahunEl?.value || tahunEl.value < 2000){ toast('Tahun tidak valid.','err'); return; }
const fd=new FormData();
const ri=document.getElementById('pr_rumah_ibadah_id');
if(ri) fd.append('rumah_ibadah_id',ri.value);
fd.append('nama_program', namaEl.value);
fd.append('jenis_bantuan', document.getElementById('pr_jenis').value);
fd.append('bulan', bulanEl.value);
fd.append('tahun', tahunEl.value);
fd.append('target_frekuensi', document.getElementById('pr_target').value);
fd.append('nominal', document.getElementById('pr_nominal').value);
fd.append('jumlah_item', document.getElementById('pr_jumlah_item').value);
fd.append('deskripsi', document.getElementById('pr_deskripsi').value);
fetch('tambah_program_bantuan.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
toast(res.message, res.status==='ok'?'ok':'err');
if(res.status==='ok'){ loadProgramBantuan().then(loadLaporanPenyaluran); }
}).catch(e=>toast('Gagal membuat program: '+e.message,'err'));
}
function submitPenyaluran(customProgramId=null, customPenerimaId=null){
const programId = customProgramId || document.getElementById('pn_program').value;
const penerimaId = customPenerimaId || document.getElementById('pn_penerima').value;
// FIX: validasi input wajib sebelum fetch agar tidak kirim request dengan data kosong.
if(!programId){ toast('Pilih program bantuan terlebih dahulu.','err'); return; }
if(!penerimaId){ toast('Pilih penerima terlebih dahulu.','err'); return; }
const fd=new FormData();
fd.append('program_id', programId);
fd.append('penerima_id', penerimaId);
fd.append('keterangan', customProgramId ? 'Dicatat cepat dari tabel laporan.' : document.getElementById('pn_keterangan').value);
fd.append('tanggal_penyaluran', document.getElementById('pn_tanggal')?.value || new Date().toISOString().slice(0,10));
fetch('tambah_penyaluran.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
toast(res.message, res.status==='ok'?'ok':'err');
if(res.status==='ok'){ loadLaporanPenyaluran(); loadPenerimaDiterima(); }
}).catch(e=>toast('Gagal mencatat penyaluran: '+e.message,'err'));
}
function quickCatatPenyaluran(programId,penerimaId){
if(!confirm('Catat penyaluran bantuan untuk penerima ini sesuai jenis, nominal, dan jumlah item pada program?')) return;
submitPenyaluran(programId,penerimaId);
}
function loadLaporanPenyaluran(){
fetch('ambil_laporan_penyaluran.php?'+getFilterQuery()).then(r=>r.json()).then(res=>{ if(res.status!=='ok') throw new Error(res.message); renderRekapPenyaluran(res.data.rekap); renderTabelPenyaluran(res.data.rows); }).catch(e=>{document.getElementById('laporanBody').innerHTML=`<tr><td colspan="9">${esc(e.message)}</td></tr>`});
}
function renderRekapPenyaluran(r){
const labels={total:'Total Baris',belum_diberi:'Belum diberi',sudah_diberi:'Sudah diberi',belum_memenuhi_target:'Belum memenuhi target',sesuai_target:'Sesuai target',melebihi_target:'Melebihi target'};
document.getElementById('rekapBox').innerHTML=Object.keys(labels).map(k=>`<div class="card"><div class="k">${labels[k]}</div><div class="v">${r?.[k]??0}</div></div>`).join('');
}
function renderTabelPenyaluran(rows){
const body=document.getElementById('laporanBody');
if(!rows.length){body.innerHTML='<tr><td colspan="9">Tidak ada data sesuai filter.</td></tr>'; return;}
body.innerHTML=rows.map(r=>{
const target=Number(r.target_frekuensi||1), jumlah=Number(r.jumlah_penyaluran||0);
const bolehCatat = USER_ROLE!=='penerima' && jumlah < target && r.status_program !== 'selesai';
let aksiLabel = 'Sudah sesuai target';
if(r.status_program==='selesai') aksiLabel = 'Program selesai';
const aksi = bolehCatat ? `<button class="btn primary small" onclick="quickCatatPenyaluran(${r.program_id},${r.penerima_id})">Catat</button>` : `<button class="btn gray small" disabled>${aksiLabel}</button>`;
return `<tr><td><b>${esc(r.nama_penerima)}</b><br><span class="muted">${esc(r.no_hp||'')}</span></td><td>${esc(r.nama_rumah_ibadah)}<br><span class="muted">${esc(r.agama)}</span></td><td>${esc(r.nama_program)}<br><span class="muted">${r.bulan}/${r.tahun} - ${esc(r.jenis_bantuan)}</span><br><span class="muted">Nominal: ${esc(r.nominal??'-')} | Item: ${esc(r.jumlah_item||'-')}</span></td><td>${r.target_frekuensi}</td><td>${r.jumlah_penyaluran}</td><td><span class="badge ${r.status_penyaluran}">${esc(r.status_penyaluran)}</span></td><td>${esc(r.tanggal_terakhir||'-')}</td><td>${(r.riwayat||[]).map(x=>`<div>${esc(x)}</div>`).join('')||'-'}</td><td>${aksi}</td></tr>`;
}).join('');
}
function filterLaporanPenyaluran(){ loadLaporanPenyaluran(); }
function updateStatusPenerima(id,status){
const alasan=prompt('Masukkan catatan/alasan:')||'';
const fd=new FormData(); fd.append('id',id); fd.append('status_pengajuan',status); fd.append('alasan_status',alasan);
fetch('update_status_penerima.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{toast(res.message); if(res.status==='ok') location.reload();}).catch(e=>toast('Gagal update: '+e.message));
}
function updateStatusRumahIbadah(id,status){
const alasan=prompt('Masukkan catatan/alasan:')||'';
const fd=new FormData(); fd.append('id',id); fd.append('status_pengajuan',status); fd.append('keterangan',alasan);
fetch('update_status_rumah_ibadah.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{toast(res.message); if(res.status==='ok') location.reload();}).catch(e=>toast('Gagal update: '+e.message));
}
function loadRiwayat(){ fetch('ambil_riwayat.php').then(r=>r.json()).then(res=>{ const d=res.data; document.getElementById('riwayatBox').innerHTML=`<b>Pengajuan:</b> ${(d.pengajuan||[]).length} data<br><b>Perubahan status:</b> ${(d.status||[]).length} data<br><b>Penyaluran:</b> ${(d.penyaluran||[]).length} data`; }); }
['f_bulan','f_tahun','f_rumah_ibadah','f_agama','f_program','f_jenis','f_status'].forEach(id=>document.getElementById(id)?.addEventListener('change',filterLaporanPenyaluran));
loadUsers();
loadRumahIbadahUntukProgram();
loadProgramBantuan().then(()=>{loadLaporanPenyaluran();loadPenerimaDiterima();loadRiwayat();});
</script></body></html>
+70
View File
@@ -0,0 +1,70 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
$id = (int) post('id', 0);
$penerima = get_penerima_by_id($id);
if (!$penerima || !can_manage_penerima($penerima)) json_response('err', 'Data tidak ditemukan atau akses ditolak.', [], 403);
$allowedKondisi = ['baik','sedang','rusak'];
$allowedJenis = ['zakat','sembako','uang_tunai','lainnya'];
$nama = clean_text(post('nama_penerima'), 100);
$nik = clean_text(post('nik'), 30);
$no_hp = clean_text(post('no_hp'), 20);
$alamat = clean_text(post('alamat'), 1000);
$pekerjaan = clean_text(post('pekerjaan'), 100);
$penghasilan = (int) post('penghasilan', 0);
$jumlah_tanggungan = (int) post('jumlah_tanggungan', 0);
$kondisi_rumah = post('kondisi_rumah');
$jenis_bantuan = post('jenis_bantuan');
$lat = post('latitude', $penerima['latitude']);
$lng = post('longitude', $penerima['longitude']);
if ($nama==='' || $no_hp==='' || $alamat==='' || $pekerjaan==='' || !valid_latlng($lat, $lng)) json_response('err', 'Data wajib belum lengkap atau koordinat tidak valid.');
if (!valid_enum($kondisi_rumah, $allowedKondisi) || !valid_enum($jenis_bantuan, $allowedJenis)) json_response('err', 'Pilihan tidak valid.');
if ($penghasilan < 0 || $jumlah_tanggungan < 0) json_response('err', 'Penghasilan dan tanggungan tidak valid.');
$rumah_ibadah_id = null;
$jarak_meter = null;
// FIX: cek apakah koordinat berubah untuk keperluan riwayat
$latBefore = (float)$penerima['latitude'];
$lngBefore = (float)$penerima['longitude'];
$lokasiBerubar = (abs((float)$lat - $latBefore) > 0.000001 || abs((float)$lng - $lngBefore) > 0.000001);
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
$globalNearest = get_nearest_rumah_ibadah((float)$lat, (float)$lng, 300);
$ownNearest = get_nearest_rumah_ibadah((float)$lat, (float)$lng, 300, (int)$_SESSION['user_id']);
if (!$ownNearest) json_response('err', 'Lokasi baru di luar radius 300 meter seluruh rumah ibadah Anda.');
if ($globalNearest && (int)$globalNearest['id'] !== (int)$ownNearest['id']) {
json_response('err', 'Lokasi baru lebih dekat ke rumah ibadah lain. Sistem hanya boleh mengaitkan penerima ke rumah ibadah terdekat.', [], 409);
}
$rumah_ibadah_id = (int)$ownNearest['id'];
$jarak_meter = (float)$ownNearest['jarak_meter'];
} else {
$nearest = get_nearest_rumah_ibadah((float)$lat, (float)$lng, 300);
if ($nearest) {
$rumah_ibadah_id = (int)$nearest['id'];
$jarak_meter = (float)$nearest['jarak_meter'];
}
}
// FIX: pisahkan cabang UPDATE agar rumah_ibadah_id/jarak_meter NULL tersimpan benar, bukan 0.
if ($rumah_ibadah_id !== null) {
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET rumah_ibadah_id=?, nama_penerima=?, nik=?, no_hp=?, alamat=?, pekerjaan=?, penghasilan=?, jumlah_tanggungan=?, kondisi_rumah=?, jenis_bantuan=?, latitude=?, longitude=?, jarak_meter=? WHERE id=?");
$stmt->bind_param('isssssiissdddi', $rumah_ibadah_id, $nama, $nik, $no_hp, $alamat, $pekerjaan, $penghasilan, $jumlah_tanggungan, $kondisi_rumah, $jenis_bantuan, $lat, $lng, $jarak_meter, $id);
} else {
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET rumah_ibadah_id=NULL, nama_penerima=?, nik=?, no_hp=?, alamat=?, pekerjaan=?, penghasilan=?, jumlah_tanggungan=?, kondisi_rumah=?, jenis_bantuan=?, latitude=?, longitude=?, jarak_meter=NULL WHERE id=?");
$stmt->bind_param('sssssiissddi', $nama, $nik, $no_hp, $alamat, $pekerjaan, $penghasilan, $jumlah_tanggungan, $kondisi_rumah, $jenis_bantuan, $lat, $lng, $id);
}
if (!$stmt->execute()) json_response('err', 'Gagal mengubah data: ' . $koneksi->error);
// FIX: catat riwayat saat lokasi berubah agar ada jejak audit.
if ($lokasiBerubar) {
$riNama = $rumah_ibadah_id ? ('RI ID ' . $rumah_ibadah_id) : 'belum terhubung RI';
insert_riwayat_status($id, $penerima['status_pengajuan'], $penerima['status_pengajuan'], "Lokasi penerima diperbarui. RI baru: $riNama, jarak: " . ($jarak_meter ?? 'N/A') . " m.");
}
json_response('ok', 'Data penerima berhasil diubah.', get_penerima_by_id($id));
?>
+36
View File
@@ -0,0 +1,36 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role(['super_admin','admin_rumah_ibadah'], true);
$id = (int) post('id', 0);
$stmt = $koneksi->prepare("SELECT * FROM program_bantuan WHERE id=? LIMIT 1");
$stmt->bind_param('i', $id);
$program = fetch_all_stmt($stmt)[0] ?? null;
if (!$program) json_response('err', 'Program tidak ditemukan.');
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
if (!is_admin_rumah_ibadah_id((int)$program['rumah_ibadah_id'], (int)$_SESSION['user_id'], true)) {
json_response('err', 'Akses ditolak. Program ini bukan milik rumah ibadah yang dikelola akun Anda.', [], 403);
}
}
$jenis = post('jenis_bantuan');
$status = post('status_program', 'aktif');
if (!valid_enum($jenis, ['zakat','sembako','uang_tunai','lainnya']) || !valid_enum($status, ['aktif','selesai','dibatalkan'])) json_response('err', 'Pilihan tidak valid.');
$nama = clean_text(post('nama_program'), 150);
$deskripsi = clean_text(post('deskripsi'), 1500);
$bulan = (int) post('bulan');
$tahun = (int) post('tahun');
$target = (int) post('target_frekuensi', 1);
$nominalInput = post('nominal', '');
$nominal = $nominalInput === '' ? null : (int) $nominalInput;
$jumlah_item = clean_text(post('jumlah_item', ''), 100);
if ($nama==='' || $bulan<1 || $bulan>12 || $tahun<2000 || $target<1) json_response('err', 'Data program tidak valid.');
if ($nominal !== null && $nominal < 0) json_response('err', 'Nominal tidak boleh minus.');
$stmt = $koneksi->prepare("UPDATE program_bantuan SET nama_program=?, jenis_bantuan=?, deskripsi=?, bulan=?, tahun=?, target_frekuensi=?, nominal=?, jumlah_item=?, status_program=? WHERE id=?");
$stmt->bind_param('sssiiiissi', $nama, $jenis, $deskripsi, $bulan, $tahun, $target, $nominal, $jumlah_item, $status, $id);
if (!$stmt->execute()) json_response('err', 'Gagal mengubah program: ' . $koneksi->error);
json_response('ok', 'Program bantuan berhasil diubah.', ['id' => $id]);
?>
+86
View File
@@ -0,0 +1,86 @@
<?php
/**
* edit_user.php
* Update akun pengguna.
* Password boleh dikosongkan jika tidak ingin diganti.
*/
require_once __DIR__ . '/auth.php';
require_login(true);
require_role(['super_admin', 'admin_rumah_ibadah'], true);
require_post();
$actor = current_user();
$actorRole = $actor['role'];
$id = (int) post('id');
$nama = clean_text(post('nama'), 100);
$username = clean_text(post('username'), 50);
$password = post('password');
$role = post('role');
$status_akun = post('status_akun', 'aktif');
$statusList = ['aktif', 'nonaktif'];
if ($id <= 0) json_response('err', 'ID akun tidak valid.', [], 422);
if ($nama === '' || $username === '') json_response('err', 'Nama dan username wajib diisi.', [], 422);
if (!preg_match('/^[a-zA-Z0-9_\.\-]{4,50}$/', $username)) {
json_response('err', 'Username minimal 4 karakter dan hanya boleh huruf, angka, titik, strip, atau underscore.', [], 422);
}
if ($password !== '' && strlen($password) < 6) json_response('err', 'Password minimal 6 karakter.', [], 422);
if (!valid_enum($status_akun, $statusList)) json_response('err', 'Status akun tidak valid.', [], 422);
$stmt = $koneksi->prepare("SELECT id, role FROM users WHERE id=? LIMIT 1");
$stmt->bind_param('i', $id);
$stmt->execute();
$target = $stmt->get_result()->fetch_assoc();
if (!$target) json_response('err', 'Akun tidak ditemukan.', [], 404);
if ($actorRole === 'super_admin') {
if (!in_array($role, ['admin_rumah_ibadah', 'penerima'], true)) {
json_response('err', 'Super admin hanya mengelola akun admin rumah ibadah dan penerima dari halaman ini.', [], 403);
}
if (!in_array($target['role'], ['admin_rumah_ibadah', 'penerima'], true)) {
json_response('err', 'Akun ini tidak boleh diubah dari halaman ini.', [], 403);
}
} else {
// Admin rumah ibadah hanya bisa mengubah akun penerima, dan role tetap penerima.
if ($target['role'] !== 'penerima') json_response('err', 'Admin rumah ibadah hanya boleh mengelola akun penerima.', [], 403);
$role = 'penerima';
$ids = get_admin_rumah_ibadah_ids((int)$actor['id'], true);
$types = 'i';
$params = [$id];
if (!empty($ids)) {
$notIn = build_in_condition('rumah_ibadah_id', $ids, $types, $params);
$stmt = $koneksi->prepare("SELECT COUNT(*) AS jml FROM penerima_bantuan WHERE user_id=? AND COALESCE(status_hapus,'normal')<>'disetujui' AND rumah_ibadah_id IS NOT NULL AND NOT ($notIn)");
} else {
$stmt = $koneksi->prepare("SELECT COUNT(*) AS jml FROM penerima_bantuan WHERE user_id=? AND COALESCE(status_hapus,'normal')<>'disetujui' AND rumah_ibadah_id IS NOT NULL");
}
stmt_bind($stmt, $types, $params);
$stmt->execute();
$row = $stmt->get_result()->fetch_assoc();
if ((int)$row['jml'] > 0) json_response('err', 'Akun penerima ini terhubung dengan rumah ibadah lain.', [], 403);
}
$stmt = $koneksi->prepare("SELECT id FROM users WHERE username=? AND id<>? LIMIT 1");
$stmt->bind_param('si', $username, $id);
$stmt->execute();
if ($stmt->get_result()->fetch_assoc()) json_response('err', 'Username sudah digunakan akun lain.', [], 409);
if ($password !== '') {
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $koneksi->prepare("UPDATE users SET nama=?, username=?, password=?, role=?, status_akun=? WHERE id=?");
$stmt->bind_param('sssssi', $nama, $username, $hash, $role, $status_akun, $id);
} else {
$stmt = $koneksi->prepare("UPDATE users SET nama=?, username=?, role=?, status_akun=? WHERE id=?");
$stmt->bind_param('ssssi', $nama, $username, $role, $status_akun, $id);
}
if (!$stmt->execute()) json_response('err', 'Gagal mengubah akun: ' . $stmt->error, [], 500);
json_response('ok', 'Akun berhasil diubah.', [
'id' => $id,
'nama' => $nama,
'username' => $username,
'role' => $role,
'status_akun' => $status_akun
]);
?>
+35
View File
@@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
$id = (int) post('id', 0);
$alasan = clean_text(post('alasan_hapus', post('alasan', '')), 1000);
$penerima = get_penerima_by_id($id);
if (!$penerima) json_response('err', 'Data penerima tidak ditemukan.', [], 404);
// Super admin boleh langsung menghapus marker/pin penerima dari peta.
// Data tidak dihapus permanen agar riwayat penyaluran tetap aman untuk audit.
if ($_SESSION['role'] === 'super_admin') {
$statusBaru = 'dibatalkan';
$hapus = 'disetujui';
$statusLama = $penerima['status_pengajuan'];
$catatan = $alasan ?: 'Marker penerima dihapus oleh super admin.';
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET status_pengajuan=?, status_hapus=?, alasan_hapus=?, alasan_status=? WHERE id=?");
$stmt->bind_param('ssssi', $statusBaru, $hapus, $catatan, $catatan, $id);
if (!$stmt->execute()) json_response('err', 'Gagal menghapus marker penerima: ' . $koneksi->error);
insert_riwayat_status($id, $statusLama, 'hapus_disetujui', $catatan);
json_response('ok', 'Marker penerima berhasil dihapus oleh super admin.', get_penerima_by_id($id));
}
// Admin rumah ibadah dan penerima hanya mengajukan penghapusan, menunggu persetujuan super admin.
if (!can_manage_penerima($penerima)) json_response('err', 'Akses ditolak.', [], 403);
if ($alasan === '') json_response('err', 'Alasan penghapusan wajib diisi.');
$statusHapus = 'menunggu';
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET status_hapus=?, alasan_hapus=? WHERE id=?");
$stmt->bind_param('ssi', $statusHapus, $alasan, $id);
if (!$stmt->execute()) json_response('err', 'Gagal mengajukan penghapusan marker penerima: ' . $koneksi->error);
insert_riwayat_status($id, $penerima['status_pengajuan'], 'hapus_menunggu', $alasan);
json_response('ok', 'Pengajuan hapus marker penerima dikirim. Menunggu persetujuan super admin.', get_penerima_by_id($id));
?>
+32
View File
@@ -0,0 +1,32 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role(['super_admin','admin_rumah_ibadah'], true);
$id = (int) post('id', 0);
$stmt = $koneksi->prepare("SELECT * FROM program_bantuan WHERE id=? LIMIT 1");
$stmt->bind_param('i', $id);
$program = fetch_all_stmt($stmt)[0] ?? null;
if (!$program) json_response('err', 'Program tidak ditemukan.');
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
if (!is_admin_rumah_ibadah_id((int)$program['rumah_ibadah_id'], (int)$_SESSION['user_id'], true)) {
json_response('err', 'Akses ditolak. Program ini bukan milik rumah ibadah yang dikelola akun Anda.', [], 403);
}
}
$stmt = $koneksi->prepare("SELECT COUNT(*) AS jml FROM penyaluran_bantuan WHERE program_id=?");
$stmt->bind_param('i', $id);
$row = fetch_all_stmt($stmt)[0];
if ((int)$row['jml'] > 0) {
$status = 'dibatalkan';
$stmt = $koneksi->prepare("UPDATE program_bantuan SET status_program=? WHERE id=?");
$stmt->bind_param('si', $status, $id);
$stmt->execute();
json_response('ok', 'Program sudah memiliki penyaluran, maka status diubah menjadi dibatalkan.', ['id'=>$id]);
}
$stmt = $koneksi->prepare("DELETE FROM program_bantuan WHERE id=?");
$stmt->bind_param('i', $id);
if (!$stmt->execute()) json_response('err', 'Gagal menghapus program: ' . $koneksi->error);
json_response('ok', 'Program bantuan berhasil dihapus.', ['id'=>$id]);
?>
+36
View File
@@ -0,0 +1,36 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role(['super_admin','admin_rumah_ibadah'], true);
$id = (int) post('id', 0);
$alasan = clean_text(post('alasan_hapus', post('alasan', '')), 1000);
$rumah = get_rumah_ibadah_by_id($id);
if (!$rumah) json_response('err', 'Rumah ibadah tidak ditemukan.', [], 404);
if ($_SESSION['role'] === 'super_admin') {
// Super admin langsung menghapus marker rumah ibadah dari peta.
// Data dibuat tidak aktif, bukan dihapus permanen, supaya riwayat tetap aman.
$status = 'ditolak';
$hapus = 'disetujui';
$catatan = $alasan ?: 'Marker rumah ibadah dihapus oleh super admin.';
$stmt = $koneksi->prepare("UPDATE rumah_ibadah SET status_pengajuan=?, status_hapus=?, alasan_hapus=?, keterangan=? WHERE id=?");
$stmt->bind_param('ssssi', $status, $hapus, $catatan, $catatan, $id);
if (!$stmt->execute()) json_response('err', 'Gagal menghapus marker rumah ibadah: ' . $koneksi->error);
$jumlahLepas = detach_penerima_from_rumah_ibadah($id);
$row = get_rumah_ibadah_by_id($id);
$row['jumlah_penerima_dilepas'] = $jumlahLepas;
json_response('ok', 'Marker rumah ibadah berhasil dihapus oleh super admin.', $row);
}
// Admin rumah ibadah hanya boleh mengajukan hapus untuk rumah ibadah miliknya.
if ((int)$rumah['user_id'] !== (int)$_SESSION['user_id']) json_response('err', 'Akses ditolak. Admin hanya boleh mengajukan hapus rumah ibadah miliknya.', [], 403);
if ($alasan === '') json_response('err', 'Alasan penghapusan wajib diisi.');
$statusHapus = 'menunggu';
$stmt = $koneksi->prepare("UPDATE rumah_ibadah SET status_hapus=?, alasan_hapus=? WHERE id=?");
$stmt->bind_param('ssi', $statusHapus, $alasan, $id);
if (!$stmt->execute()) json_response('err', 'Gagal mengajukan penghapusan rumah ibadah: ' . $koneksi->error);
json_response('ok', 'Pengajuan hapus marker rumah ibadah dikirim. Menunggu persetujuan super admin.', get_rumah_ibadah_by_id($id));
?>
+71
View File
@@ -0,0 +1,71 @@
<?php
/**
* hapus_user.php
* Menghapus akun jika belum punya relasi data. Jika sudah punya relasi, akun dinonaktifkan agar data audit tidak rusak.
*/
require_once __DIR__ . '/auth.php';
require_login(true);
require_role(['super_admin', 'admin_rumah_ibadah'], true);
require_post();
$actor = current_user();
$actorRole = $actor['role'];
$id = (int) post('id');
if ($id <= 0) json_response('err', 'ID akun tidak valid.', [], 422);
if ($id === (int)$actor['id']) json_response('err', 'Akun yang sedang dipakai tidak boleh dihapus dari sini.', [], 403);
$stmt = $koneksi->prepare("SELECT id, role FROM users WHERE id=? LIMIT 1");
$stmt->bind_param('i', $id);
$stmt->execute();
$target = $stmt->get_result()->fetch_assoc();
if (!$target) json_response('err', 'Akun tidak ditemukan.', [], 404);
if ($actorRole === 'super_admin') {
if (!in_array($target['role'], ['admin_rumah_ibadah', 'penerima'], true)) {
json_response('err', 'Super admin hanya menghapus/menonaktifkan akun admin rumah ibadah dan penerima dari halaman ini.', [], 403);
}
} else {
if ($target['role'] !== 'penerima') json_response('err', 'Admin rumah ibadah hanya boleh menghapus/menonaktifkan akun penerima.', [], 403);
$ids = get_admin_rumah_ibadah_ids((int)$actor['id'], true);
$types = 'i';
$params = [$id];
if (!empty($ids)) {
$notIn = build_in_condition('rumah_ibadah_id', $ids, $types, $params);
$stmt = $koneksi->prepare("SELECT COUNT(*) AS jml FROM penerima_bantuan WHERE user_id=? AND COALESCE(status_hapus,'normal')<>'disetujui' AND rumah_ibadah_id IS NOT NULL AND NOT ($notIn)");
} else {
$stmt = $koneksi->prepare("SELECT COUNT(*) AS jml FROM penerima_bantuan WHERE user_id=? AND COALESCE(status_hapus,'normal')<>'disetujui' AND rumah_ibadah_id IS NOT NULL");
}
stmt_bind($stmt, $types, $params);
$stmt->execute();
$row = $stmt->get_result()->fetch_assoc();
if ((int)$row['jml'] > 0) json_response('err', 'Akun penerima ini terhubung dengan rumah ibadah lain.', [], 403);
}
// Cek relasi agar tidak merusak foreign key dan riwayat audit.
$related = 0;
$checks = [
["SELECT COUNT(*) AS jml FROM rumah_ibadah WHERE user_id=?", 'i'],
["SELECT COUNT(*) AS jml FROM penerima_bantuan WHERE user_id=?", 'i'],
["SELECT COUNT(*) AS jml FROM penyaluran_bantuan WHERE admin_id=?", 'i'],
["SELECT COUNT(*) AS jml FROM riwayat_status WHERE user_id=?", 'i']
];
foreach ($checks as $c) {
$stmt = $koneksi->prepare($c[0]);
$stmt->bind_param('i', $id);
$stmt->execute();
$row = $stmt->get_result()->fetch_assoc();
$related += (int)$row['jml'];
}
if ($related > 0) {
$stmt = $koneksi->prepare("UPDATE users SET status_akun='nonaktif' WHERE id=?");
$stmt->bind_param('i', $id);
if (!$stmt->execute()) json_response('err', 'Gagal menonaktifkan akun: ' . $stmt->error, [], 500);
json_response('ok', 'Akun memiliki relasi data, jadi tidak dihapus permanen. Akun sudah dinonaktifkan.', ['mode' => 'nonaktif']);
}
$stmt = $koneksi->prepare("DELETE FROM users WHERE id=?");
$stmt->bind_param('i', $id);
if (!$stmt->execute()) json_response('err', 'Gagal menghapus akun: ' . $stmt->error, [], 500);
json_response('ok', 'Akun berhasil dihapus permanen.', ['mode' => 'delete']);
?>
+286
View File
@@ -0,0 +1,286 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(false);
$user = current_user();
$role = $user['role'];
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Poverty Mapping</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
*{margin:0;padding:0;box-sizing:border-box} html,body{height:100%;font-family:Arial,sans-serif;background:#f8fafc;color:#0f172a}
#header{height:50px;background:#0f766e;color:#fff;display:flex;align-items:center;justify-content:space-between;padding:0 18px;z-index:1000;position:relative;box-shadow:0 2px 12px rgba(15,23,42,.18)}
#header h1{font-size:16px}.nav{display:flex;gap:9px;align-items:center;font-size:13px}.nav a{color:#fff;text-decoration:none;background:rgba(255,255,255,.16);padding:7px 10px;border-radius:8px}.role{font-weight:700;text-transform:uppercase;font-size:11px;letter-spacing:.05em;background:#064e3b;padding:6px 8px;border-radius:999px}
#map{width:100%;height:calc(100vh - 50px)}
#toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%) translateY(60px);background:#1f2937;color:#fff;padding:10px 18px;border-radius:999px;font-size:13px;z-index:9999;opacity:0;transition:.25s;box-shadow:0 10px 24px rgba(0,0,0,.22)}#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}#toast.ok{background:#16a34a}#toast.err{background:#dc2626}
#loading{display:none;position:fixed;inset:0;background:rgba(255,255,255,.55);z-index:9000;align-items:center;justify-content:center}.show-loading{display:flex!important}.spin{width:42px;height:42px;border-radius:50%;border:5px solid #cbd5e1;border-top-color:#0f766e;animation:sp .7s linear infinite}@keyframes sp{to{transform:rotate(360deg)}}
.leaflet-popup-content-wrapper{border-radius:12px!important;overflow:hidden;padding:0!important}.leaflet-popup-content{margin:0!important;min-width:280px}.ph{background:#0f766e;color:white;padding:10px 14px;font-weight:700}.pb{background:#fff;padding:12px 14px}.muted{color:#64748b;font-size:12px}.dt{width:100%;border-collapse:collapse;font-size:12px}.dt td{padding:4px;border-bottom:1px solid #eef2f7;vertical-align:top}.dt td:first-child{color:#64748b;width:115px}.fg{margin:8px 0}.fg label{font-size:11px;text-transform:uppercase;font-weight:700;color:#64748b;display:block;margin-bottom:4px}.fg input,.fg select,.fg textarea{width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:8px;font-size:13px}.fg textarea{min-height:58px;resize:vertical}.br{display:flex;gap:7px;margin-top:9px;flex-wrap:wrap}.btn{border:0;border-radius:8px;padding:8px 10px;font-size:12px;font-weight:700;cursor:pointer}.bp{background:#0f766e;color:#fff}.bd{background:#dc2626;color:#fff}.bo{background:#f97316;color:#fff}.bs{background:#e2e8f0;color:#0f172a}.small{font-size:11px}.coord{background:#ecfeff;color:#155e75;border:1px solid #a5f3fc;border-radius:8px;padding:7px;font-size:12px;margin-bottom:8px}.legend{background:white;border-radius:10px;padding:10px 12px;box-shadow:0 6px 18px rgba(15,23,42,.18);font-size:12px}.legend b{display:block;margin-bottom:6px}.li{display:flex;align-items:center;gap:6px;margin:4px 0}.dot{width:12px;height:12px;border-radius:50%;border:1px solid rgba(0,0,0,.25)}.svg-marker{line-height:0}.actions{margin-top:8px}
</style>
</head>
<body>
<div id="header">
<h1>🗺️ WebGIS Poverty Mapping</h1>
<div class="nav"><span class="role"><?= htmlspecialchars($role) ?></span><span><?= htmlspecialchars($user['nama']) ?></span><a href="dashboard.php">Dashboard</a><a href="logout.php">Logout</a></div>
</div>
<div id="map"></div><div id="toast"></div><div id="loading"><div class="spin"></div></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const USER_ROLE = <?= json_encode($role) ?>;
const USER_ID = <?= json_encode((int)$user['id']) ?>;
const USER_NAME = <?= json_encode($user['nama']) ?>;
const markersMap = {};
let map, tempPopup = null;
// Cek Leaflet sebelum inisialisasi peta.
if (typeof L === 'undefined') {
document.getElementById('map').innerHTML = '<div style="padding:20px;color:red">Leaflet gagal dimuat. Periksa internet/CDN.</div>';
throw new Error('Leaflet tidak tersedia');
}
// Inisialisasi peta.
map = L.map('map').setView([-0.05566422376778593, 109.34854622732368], 17);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {maxZoom: 20, attribution:'&copy; OpenStreetMap'}).addTo(map);
// Pembuatan Layer Groups.
const layerIslam = L.layerGroup();
const layerKristen = L.layerGroup();
const layerKatolik = L.layerGroup();
const layerHindu = L.layerGroup();
const layerBuddha = L.layerGroup();
const layerKonghucu = L.layerGroup();
const layerPenerimaDiterima = L.layerGroup();
const layerPenerimaMenunggu = L.layerGroup();
const layerPenerimaDibatalkan = L.layerGroup();
const layerRadiusBantuan = L.layerGroup();
// Layers Control.
const overlayMaps = {
"Rumah Ibadah Islam": layerIslam,
"Rumah Ibadah Kristen": layerKristen,
"Rumah Ibadah Katolik": layerKatolik,
"Rumah Ibadah Hindu": layerHindu,
"Rumah Ibadah Buddha": layerBuddha,
"Rumah Ibadah Konghucu": layerKonghucu,
"Penerima Diterima": layerPenerimaDiterima,
"Pengajuan Menunggu": layerPenerimaMenunggu,
"Penerima Dibatalkan": layerPenerimaDibatalkan,
"Radius Bantuan 300 Meter": layerRadiusBantuan
};
layerIslam.addTo(map); layerKristen.addTo(map); layerKatolik.addTo(map); layerHindu.addTo(map); layerBuddha.addTo(map); layerKonghucu.addTo(map);
layerPenerimaDiterima.addTo(map); layerRadiusBantuan.addTo(map);
if (USER_ROLE !== 'penerima') layerPenerimaMenunggu.addTo(map);
if (USER_ROLE === 'super_admin') layerPenerimaDibatalkan.addTo(map);
L.control.layers(null, overlayMaps).addTo(map);
// Penentuan warna marker.
function getRumahIbadahColor(data){
const c={islam:'#22c55e',kristen:'#3b82f6',katolik:'#8b5cf6',hindu:'#f97316',buddha:'#eab308',konghucu:'#ef4444'};
return c[data.agama] || '#0f766e';
}
function getPenerimaColor(data){
const c={menunggu:'#6b7280',diterima:'#111827',ditolak:'#dc2626',dibatalkan:'#92400e'};
return c[data.status_pengajuan] || '#111827';
}
// Pembuatan SVG marker via L.divIcon.
function svgIcon(color, label){
return L.divIcon({className:'svg-marker', iconSize:[30,42], iconAnchor:[15,42], popupAnchor:[0,-38], html:`
<svg width="30" height="42" viewBox="0 0 30 42" xmlns="http://www.w3.org/2000/svg">
<path d="M15 0C6.7 0 0 6.7 0 15c0 11.2 15 27 15 27s15-15.8 15-27C30 6.7 23.3 0 15 0z" fill="${color}" stroke="white" stroke-width="2"/>
<circle cx="15" cy="15" r="8" fill="white" opacity=".94"/><text x="15" y="19" text-anchor="middle" font-size="10" font-weight="700" fill="${color}">${label}</text>
</svg>`});
}
function createRumahIbadahIcon(data){ return svgIcon(getRumahIbadahColor(data), 'RI'); }
function createPenerimaIcon(data){ return svgIcon(getPenerimaColor(data), 'P'); }
function getRumahIbadahLayer(data){
return ({islam:layerIslam,kristen:layerKristen,katolik:layerKatolik,hindu:layerHindu,buddha:layerBuddha,konghucu:layerKonghucu})[data.agama] || layerIslam;
}
function getPenerimaLayer(data){
if(data.status_pengajuan==='menunggu') return layerPenerimaMenunggu;
if(data.status_pengajuan==='dibatalkan') return layerPenerimaDibatalkan;
return layerPenerimaDiterima;
}
function showToast(message,type='ok'){
const t=document.getElementById('toast'); t.textContent=message; t.className='show '+type; setTimeout(()=>t.className='',3200);
}
function showLoading(){document.getElementById('loading').classList.add('show-loading')}
function hideLoading(){document.getElementById('loading').classList.remove('show-loading')}
function esc(v){return String(v??'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[m]))}
function rupiah(n){ if(!n) return '-'; return 'Rp '+Number(n).toLocaleString('id-ID') }
// Perhitungan jarak frontend untuk tampilan. Validasi utama tetap di backend.
function hitungJarakMeter(lat1, lon1, lat2, lon2){
const R=6371000,dLat=(lat2-lat1)*Math.PI/180,dLon=(lon2-lon1)*Math.PI/180;
const a=Math.sin(dLat/2)**2+Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*Math.sin(dLon/2)**2;
return R*2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));
}
function alamatDefault(lat,lng){ return `Koordinat: ${Number(lat).toFixed(6)}, ${Number(lng).toFixed(6)}`; }
function isiAlamatOtomatis(lat,lng,fieldId){
const el=document.getElementById(fieldId); if(!el) return;
const awal=alamatDefault(lat,lng); el.value=awal;
// Reverse geocoding dari OpenStreetMap. Jika gagal, alamat koordinat tetap dipakai dan bisa diedit manual.
fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lng)}&accept-language=id`)
.then(r=>r.ok?r.json():null).then(d=>{
const terbaru=document.getElementById(fieldId);
if(terbaru && d && d.display_name && (terbaru.value==='' || terbaru.value===awal)) terbaru.value=d.display_name;
}).catch(()=>{});
}
function addRumahIbadahMarker(data){
if(data.status_pengajuan==='ditolak' || data.status_hapus==='disetujui') return;
const layer=getRumahIbadahLayer(data);
const marker=L.marker([+data.latitude,+data.longitude],{icon:createRumahIbadahIcon(data)});
let actions = '';
const statusHapus = data.status_hapus || 'normal';
if(USER_ROLE==='super_admin' && data.status_pengajuan==='menunggu'){
actions += `<div class="br actions"><button class="btn bp" onclick="updateStatusRumahIbadah(${data.id},'diterima')">Setujui</button><button class="btn bd" onclick="updateStatusRumahIbadah(${data.id},'ditolak')">Tolak</button></div>`;
}
if(USER_ROLE==='super_admin' && statusHapus==='menunggu'){
actions += `<div class="br actions"><button class="btn bp" onclick="updateStatusRumahIbadah(${data.id},'hapus_disetujui')">Setujui Hapus</button><button class="btn bd" onclick="updateStatusRumahIbadah(${data.id},'hapus_ditolak')">Tolak Hapus</button></div>`;
}
if(USER_ROLE==='super_admin' && statusHapus!=='menunggu'){
actions += `<div class="br actions"><button class="btn bd" onclick="hapusRumahIbadah(${data.id})">Hapus Marker</button></div>`;
}
if(USER_ROLE==='admin_rumah_ibadah' && Number(data.user_id)===Number(USER_ID) && statusHapus!=='menunggu'){
actions += `<div class="br actions"><button class="btn bo" onclick="hapusRumahIbadah(${data.id})">Ajukan Hapus Marker</button></div>`;
}
const hapusInfo = statusHapus==='menunggu' ? `<div class="muted" style="margin-top:8px;color:#92400e"><b>Pengajuan hapus sedang menunggu persetujuan super admin.</b><br>${esc(data.alasan_hapus||'')}</div>` : '';
marker.bindPopup(`<div class="ph">${esc(data.nama_rumah_ibadah)}</div><div class="pb"><table class="dt"><tr><td>Agama</td><td>${esc(data.agama)}</td></tr><tr><td>Status</td><td>${esc(data.status_pengajuan)}</td></tr><tr><td>Status hapus</td><td>${esc(statusHapus)}</td></tr><tr><td>Alamat</td><td>${esc(data.alamat)}</td></tr><tr><td>No HP</td><td>${esc(data.nomor_hp)}</td></tr><tr><td>Koordinat</td><td>${(+data.latitude).toFixed(6)}, ${(+data.longitude).toFixed(6)}</td></tr></table><div class="muted">Radius bantuan: 300 meter.</div>${hapusInfo}${actions}</div>`);
marker.addTo(layer);
let circle=null;
// Pembuatan radius 300 meter untuk rumah ibadah yang diterima.
if(data.status_pengajuan==='diterima'){
circle=L.circle([+data.latitude,+data.longitude],{radius:300,color:getRumahIbadahColor(data),weight:1,fillOpacity:.08}).addTo(layerRadiusBantuan);
}
markersMap['rumah_'+data.id]={marker,layer,circle};
}
function addPenerimaMarker(data){
if(data.status_pengajuan==='ditolak') return;
if(data.status_hapus==='disetujui') return;
if(data.status_pengajuan==='menunggu' && USER_ROLE==='penerima') return;
if(data.status_pengajuan==='dibatalkan' && USER_ROLE!=='super_admin') return;
const layer=getPenerimaLayer(data);
const marker=L.marker([+data.latitude,+data.longitude],{icon:createPenerimaIcon(data)});
let actions='';
const statusHapus = data.status_hapus || 'normal';
if((USER_ROLE==='super_admin'||USER_ROLE==='admin_rumah_ibadah') && data.status_pengajuan==='menunggu'){
actions+=`<div class="br actions"><button class="btn bp" onclick="updateStatusPenerima(${data.id},'diterima')">Setujui</button><button class="btn bd" onclick="updateStatusPenerima(${data.id},'ditolak')">Tolak</button></div>`;
}
if(USER_ROLE==='super_admin' && statusHapus==='menunggu'){
actions+=`<div class="br actions"><button class="btn bp" onclick="updateStatusPenerima(${data.id},'hapus_disetujui')">Setujui Hapus</button><button class="btn bd" onclick="updateStatusPenerima(${data.id},'hapus_ditolak')">Tolak Hapus</button></div>`;
}
if(USER_ROLE==='super_admin' && statusHapus!=='menunggu'){
actions+=`<div class="br actions"><button class="btn bd" onclick="hapusPenerima(${data.id})">Hapus Marker</button></div>`;
}
if((USER_ROLE==='admin_rumah_ibadah'||USER_ROLE==='penerima') && statusHapus!=='menunggu'){
actions+=`<div class="br actions"><button class="btn bo" onclick="hapusPenerima(${data.id})">Ajukan Hapus Pin</button></div>`;
}
const hapusInfo = statusHapus==='menunggu' ? `<div class="muted" style="margin-top:8px;color:#92400e"><b>Pengajuan hapus sedang menunggu persetujuan super admin.</b><br>${esc(data.alasan_hapus||'')}</div>` : '';
marker.bindPopup(`<div class="ph">${esc(data.nama_penerima)}</div><div class="pb"><table class="dt"><tr><td>Status</td><td>${esc(data.status_pengajuan)}</td></tr><tr><td>Status hapus</td><td>${esc(statusHapus)}</td></tr><tr><td>Bantuan</td><td>${esc(data.jenis_bantuan)}</td></tr><tr><td>Pekerjaan</td><td>${esc(data.pekerjaan)}</td></tr><tr><td>Penghasilan</td><td>${rupiah(data.penghasilan)}</td></tr><tr><td>Tanggungan</td><td>${esc(data.jumlah_tanggungan)}</td></tr><tr><td>Rumah ibadah</td><td>${esc(data.nama_rumah_ibadah || '-')}</td></tr><tr><td>Jarak</td><td>${data.jarak_meter?Number(data.jarak_meter).toFixed(1)+' meter':'-'}</td></tr></table>${hapusInfo}${actions}</div>`);
marker.addTo(layer);
markersMap['penerima_'+data.id]={marker,layer,circle:null};
}
function removeMarkerFromMap(id,type){
const key=type+'_'+id, obj=markersMap[key]; if(!obj) return;
if(obj.marker && obj.layer) obj.layer.removeLayer(obj.marker);
if(obj.circle) layerRadiusBantuan.removeLayer(obj.circle);
delete markersMap[key];
}
function refreshMarkerAfterUpdate(data,type){ removeMarkerFromMap(data.id,type); if(type==='penerima') addPenerimaMarker(data); else addRumahIbadahMarker(data); }
function clearAll(){Object.keys(markersMap).forEach(k=>{const [type,id]=k.split('_'); removeMarkerFromMap(id,type)}); layerRadiusBantuan.clearLayers();}
function loadDataPeta(){
showLoading();
fetch('ambil_data_peta.php')
.then(r=>r.json()).then(res=>{
if(res.status!=='ok') throw new Error(res.message);
clearAll();
(res.data.rumah_ibadah||[]).forEach(addRumahIbadahMarker);
(res.data.penerima||[]).forEach(addPenerimaMarker);
}).catch(e=>showToast(e.message,'err')).finally(hideLoading);
}
function formPenerima(lat,lng){ return `<div class="ph">Tambah/Pengajuan Penerima</div><div class="pb"><div class="coord">Lat ${lat.toFixed(6)}<br>Lng ${lng.toFixed(6)}</div>
<input type="hidden" id="p_lat" value="${lat}"><input type="hidden" id="p_lng" value="${lng}">
${USER_ROLE!=='penerima'?'<div class="fg"><label>Username Akun Penerima</label><input id="p_username" placeholder="Username penerima yang terkait"></div>':''}
<div class="fg"><label>Nama Penerima</label><input id="p_nama" value="${USER_ROLE==='penerima'?esc(USER_NAME):''}"></div>
<div class="fg"><label>NIK</label><input id="p_nik"></div><div class="fg"><label>No HP</label><input id="p_hp"></div>
<div class="fg"><label>Alamat</label><textarea id="p_alamat">${esc(alamatDefault(lat,lng))}</textarea></div><div class="fg"><label>Pekerjaan</label><input id="p_pekerjaan"></div>
<div class="fg"><label>Penghasilan</label><input type="number" id="p_penghasilan" value="0"></div><div class="fg"><label>Jumlah Tanggungan</label><input type="number" id="p_tanggungan" value="0"></div>
<div class="fg"><label>Kondisi Rumah</label><select id="p_kondisi"><option value="baik">Baik</option><option value="sedang">Sedang</option><option value="rusak">Rusak</option></select></div>
<div class="fg"><label>Jenis Bantuan</label><select id="p_jenis"><option value="zakat">Zakat</option><option value="sembako">Sembako</option><option value="uang_tunai">Uang Tunai</option><option value="lainnya">Lainnya</option></select></div>
<div class="br"><button class="btn bp" onclick="submitPenerima()">Simpan Pengajuan</button></div></div>`; }
function formRumahIbadah(lat,lng){ return `<div class="ph">Ajukan Rumah Ibadah</div><div class="pb"><div class="coord">Lat ${lat.toFixed(6)}<br>Lng ${lng.toFixed(6)}</div>
<input type="hidden" id="ri_lat" value="${lat}"><input type="hidden" id="ri_lng" value="${lng}">
${USER_ROLE==='super_admin'?'<div class="fg"><label>Username Admin Rumah Ibadah</label><input id="ri_username_admin" placeholder="Username admin pengelola"></div>':''}
<div class="fg"><label>Nama Rumah Ibadah</label><input id="ri_nama"></div><div class="fg"><label>Agama</label><select id="ri_agama"><option value="islam">Islam</option><option value="kristen">Kristen</option><option value="katolik">Katolik</option><option value="hindu">Hindu</option><option value="buddha">Buddha</option><option value="konghucu">Konghucu</option></select></div>
<div class="fg"><label>Alamat</label><textarea id="ri_alamat">${esc(alamatDefault(lat,lng))}</textarea></div><div class="fg"><label>Nomor HP</label><input id="ri_hp"></div>
<div class="br"><button class="btn bp" onclick="submitRumahIbadah()">Ajukan</button></div></div>`; }
function showPopupTambahPenerima(latlng){ tempPopup=L.popup().setLatLng(latlng).setContent(formPenerima(latlng.lat,latlng.lng)).openOn(map); setTimeout(()=>isiAlamatOtomatis(latlng.lat,latlng.lng,'p_alamat'),80); }
function showPopupAjukanRumahIbadah(latlng){ tempPopup=L.popup().setLatLng(latlng).setContent(formRumahIbadah(latlng.lat,latlng.lng)).openOn(map); setTimeout(()=>isiAlamatOtomatis(latlng.lat,latlng.lng,'ri_alamat'),80); }
map.on('click', e=>{
if(USER_ROLE==='penerima') return showPopupTambahPenerima(e.latlng);
if(USER_ROLE==='admin_rumah_ibadah') return L.popup().setLatLng(e.latlng).setContent(`<div class="ph">Pilih Aksi</div><div class="pb"><div class="br"><button class="btn bp" onclick="showPopupTambahPenerima({lat:${e.latlng.lat},lng:${e.latlng.lng}})">Tambah Penerima</button><button class="btn bs" onclick="showPopupAjukanRumahIbadah({lat:${e.latlng.lat},lng:${e.latlng.lng}})">Ajukan Rumah Ibadah</button></div></div>`).openOn(map);
if(USER_ROLE==='super_admin') return L.popup().setLatLng(e.latlng).setContent(`<div class="ph">Pilih Aksi</div><div class="pb"><div class="br"><button class="btn bp" onclick="showPopupTambahPenerima({lat:${e.latlng.lat},lng:${e.latlng.lng}})">Tambah Penerima</button><button class="btn bs" onclick="showPopupAjukanRumahIbadah({lat:${e.latlng.lat},lng:${e.latlng.lng}})">Tambah Rumah Ibadah</button></div></div>`).openOn(map);
});
function submitPenerima(){
const fd=new FormData();
const userEl=document.getElementById('p_username'); if(userEl) fd.append('username_penerima', userEl.value);
fd.append('nama_penerima',document.getElementById('p_nama').value); fd.append('nik',document.getElementById('p_nik').value); fd.append('no_hp',document.getElementById('p_hp').value); fd.append('alamat',document.getElementById('p_alamat').value); fd.append('pekerjaan',document.getElementById('p_pekerjaan').value); fd.append('penghasilan',document.getElementById('p_penghasilan').value); fd.append('jumlah_tanggungan',document.getElementById('p_tanggungan').value); fd.append('kondisi_rumah',document.getElementById('p_kondisi').value); fd.append('jenis_bantuan',document.getElementById('p_jenis').value); fd.append('latitude',document.getElementById('p_lat').value); fd.append('longitude',document.getElementById('p_lng').value);
showLoading(); fetch('tambah_penerima.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{showToast(res.message,res.status==='ok'?'ok':'err'); if(res.status==='ok'){map.closePopup(); loadDataPeta();}}).catch(e=>showToast(e.message,'err')).finally(hideLoading);
}
function submitRumahIbadah(){
const fd=new FormData(); const adminEl=document.getElementById('ri_username_admin'); if(adminEl) fd.append('username_admin', adminEl.value); fd.append('nama_rumah_ibadah',document.getElementById('ri_nama').value); fd.append('agama',document.getElementById('ri_agama').value); fd.append('alamat',document.getElementById('ri_alamat').value); fd.append('nomor_hp',document.getElementById('ri_hp').value); fd.append('latitude',document.getElementById('ri_lat').value); fd.append('longitude',document.getElementById('ri_lng').value);
showLoading(); fetch('ajukan_rumah_ibadah.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{showToast(res.message,res.status==='ok'?'ok':'err'); if(res.status==='ok'){map.closePopup(); loadDataPeta();}}).catch(e=>showToast(e.message,'err')).finally(hideLoading);
}
function hapusPenerima(id){
const alasan=prompt(USER_ROLE==='super_admin'?'Alasan hapus marker penerima:':'Alasan mengajukan hapus pin penerima:')||'';
if(alasan.trim()==='') return showToast('Alasan penghapusan wajib diisi.','err');
const fd=new FormData(); fd.append('id',id); fd.append('alasan_hapus',alasan);
showLoading(); fetch('hapus_penerima.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{showToast(res.message,res.status==='ok'?'ok':'err'); if(res.status==='ok'){map.closePopup(); loadDataPeta();}}).catch(e=>showToast(e.message,'err')).finally(hideLoading);
}
function hapusRumahIbadah(id){
const alasan=prompt(USER_ROLE==='super_admin'?'Alasan hapus marker rumah ibadah:':'Alasan mengajukan hapus marker rumah ibadah:')||'';
if(alasan.trim()==='') return showToast('Alasan penghapusan wajib diisi.','err');
const fd=new FormData(); fd.append('id',id); fd.append('alasan_hapus',alasan);
showLoading(); fetch('hapus_rumah_ibadah.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{showToast(res.message,res.status==='ok'?'ok':'err'); if(res.status==='ok'){map.closePopup(); loadDataPeta();}}).catch(e=>showToast(e.message,'err')).finally(hideLoading);
}
function updateStatusPenerima(id,status,alasan=''){
if(!alasan && (status==='ditolak'||status==='dibatalkan'||status==='hapus_disetujui'||status==='hapus_ditolak')) alasan=prompt('Masukkan alasan/catatan:')||'';
const fd=new FormData(); fd.append('id',id); fd.append('status_pengajuan',status); fd.append('alasan_status',alasan);
showLoading(); fetch('update_status_penerima.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{showToast(res.message,res.status==='ok'?'ok':'err'); if(res.status==='ok'){map.closePopup(); refreshMarkerAfterUpdate(res.data,'penerima');}}).catch(e=>showToast(e.message,'err')).finally(hideLoading);
}
function updateStatusRumahIbadah(id,status,alasan=''){
if(!alasan && (status==='ditolak'||status==='hapus_disetujui'||status==='hapus_ditolak')) alasan=prompt('Masukkan alasan/catatan:')||'';
const fd=new FormData(); fd.append('id',id); fd.append('status_pengajuan',status); fd.append('keterangan',alasan);
showLoading(); fetch('update_status_rumah_ibadah.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{showToast(res.message,res.status==='ok'?'ok':'err'); if(res.status==='ok'){map.closePopup(); loadDataPeta();}}).catch(e=>showToast(e.message,'err')).finally(hideLoading);
}
// Fungsi berikut tersedia agar pola index konsisten dengan dashboard dan endpoint.
function loadProgramBantuan(){ return fetch('ambil_program_bantuan.php').then(r=>r.json()); }
function submitProgramBantuan(){ showToast('Kelola program bantuan tersedia di dashboard.','ok'); }
function submitPenyaluran(){ showToast('Catat penyaluran bantuan tersedia di dashboard.','ok'); }
function loadLaporanPenyaluran(){ return fetch('ambil_laporan_penyaluran.php').then(r=>r.json()); }
function renderTabelPenyaluran(data){ return data; }
function renderRekapPenyaluran(data){ return data; }
function filterLaporanPenyaluran(){ return loadLaporanPenyaluran(); }
function showDetailPenyaluran(id){ showToast('Detail penyaluran dapat dilihat di dashboard/riwayat.','ok'); }
const legend=L.control({position:'bottomright'}); legend.onAdd=function(){const d=L.DomUtil.create('div','legend');d.innerHTML='<b>Legenda</b><div class="li"><span class="dot" style="background:#22c55e"></span>Islam</div><div class="li"><span class="dot" style="background:#3b82f6"></span>Kristen</div><div class="li"><span class="dot" style="background:#8b5cf6"></span>Katolik</div><div class="li"><span class="dot" style="background:#111827"></span>Penerima diterima</div><div class="li"><span class="dot" style="background:#6b7280"></span>Menunggu</div><div class="li"><span class="dot" style="background:#92400e"></span>Dibatalkan</div>';return d};legend.addTo(map);
loadDataPeta();
</script>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
<?php
/**
* Koneksi database Project 05.
*/
$host = getenv('DB_HOST') ?: 'db';
$user = getenv('DB_USER') ?: 'root';
$pass = getenv('DB_PASSWORD') ?: 'webgisidolapass';
$db = '05_povertydb';
$koneksi = new mysqli($host, $user, $pass, $db);
if ($koneksi->connect_error) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'status' => 'err',
'message' => 'Koneksi database gagal: ' . $koneksi->connect_error
]);
exit;
}
$koneksi->set_charset('utf8mb4');
?>
+53
View File
@@ -0,0 +1,53 @@
<?php
require_once __DIR__ . '/auth.php';
if (is_logged_in()) {
header('Location: dashboard.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = clean_text(post('username'), 50);
$password = post('password');
$stmt = $koneksi->prepare("SELECT * FROM users WHERE username=? AND status_akun='aktif' LIMIT 1");
$stmt->bind_param('s', $username);
$stmt->execute();
$user = $stmt->get_result()->fetch_assoc();
// password_verify() digunakan agar password tidak disimpan plain text.
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = (int) $user['id'];
$_SESSION['nama'] = $user['nama'];
$_SESSION['role'] = $user['role'];
header('Location: dashboard.php');
exit;
}
$error = 'Username atau password salah.';
}
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login - WebGIS Poverty Mapping</title>
<style>
*{box-sizing:border-box} body{margin:0;min-height:100vh;font-family:Arial,sans-serif;background:linear-gradient(135deg,#0f766e,#0f172a);display:flex;align-items:center;justify-content:center;padding:20px}.card{width:100%;max-width:410px;background:#fff;border-radius:18px;box-shadow:0 18px 45px rgba(0,0,0,.25);padding:28px}.brand{display:flex;gap:12px;align-items:center;margin-bottom:18px}.logo{width:46px;height:46px;border-radius:13px;background:#0f766e;color:white;display:flex;align-items:center;justify-content:center;font-size:24px}h1{font-size:22px;margin:0;color:#0f172a}.muted{color:#64748b;font-size:13px;margin-top:4px}.fg{margin:14px 0}.fg label{font-weight:700;font-size:13px;color:#334155;display:block;margin-bottom:6px}.fg input{width:100%;padding:12px;border:1px solid #cbd5e1;border-radius:10px;font-size:14px}.btn{width:100%;padding:12px;border:0;border-radius:10px;background:#0f766e;color:white;font-weight:700;cursor:pointer}.err{background:#fee2e2;color:#991b1b;padding:10px;border-radius:10px;font-size:13px;margin:10px 0}.links{text-align:center;margin-top:15px;font-size:13px}.links a{color:#0f766e;text-decoration:none;font-weight:700}.demo{background:#f8fafc;border:1px solid #e2e8f0;padding:10px;border-radius:10px;font-size:12px;color:#475569;margin-top:14px}
</style>
</head>
<body>
<div class="card">
<div class="brand"><div class="logo">🗺️</div><div><h1>WebGIS Poverty Mapping</h1><div class="muted">Manajemen bantuan/zakat berbasis lokasi</div></div></div>
<?php if($error): ?><div class="err"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<form method="post">
<div class="fg"><label>Username</label><input name="username" required autofocus></div>
<div class="fg"><label>Password</label><input name="password" type="password" required></div>
<button class="btn">Login</button>
</form>
<div class="links">Belum punya akun penerima? <a href="register.php">Registrasi</a></div>
<div class="demo">Akun awal:<br>username <b>superadmin</b><br>password <b>password</b><br><br><b>Catatan:</b> registrasi hanya untuk penerima. Akun admin rumah ibadah dibuat oleh super admin dari dashboard.</div>
</div>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
<?php
if (session_status() === PHP_SESSION_NONE) session_start();
$_SESSION = [];
session_destroy();
header('Location: login.php');
exit;
?>
+58
View File
@@ -0,0 +1,58 @@
<?php
require_once __DIR__ . '/auth.php';
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nama = clean_text(post('nama'), 100);
$username = clean_text(post('username'), 50);
$password = post('password');
$password2 = post('password2');
if ($nama === '' || $username === '' || strlen($password) < 6) {
$error = 'Nama, username, dan password minimal 6 karakter wajib diisi.';
} elseif ($password !== $password2) {
$error = 'Konfirmasi password tidak sama.';
} else {
$cek = $koneksi->prepare("SELECT id FROM users WHERE username=? LIMIT 1");
$cek->bind_param('s', $username);
$cek->execute();
if ($cek->get_result()->fetch_assoc()) {
$error = 'Username sudah digunakan.';
} else {
// password_hash() untuk keamanan password.
$hash = password_hash($password, PASSWORD_DEFAULT);
$role = 'penerima';
$stmt = $koneksi->prepare("INSERT INTO users (nama, username, password, role) VALUES (?,?,?,?)");
$stmt->bind_param('ssss', $nama, $username, $hash, $role);
if ($stmt->execute()) {
$success = 'Registrasi berhasil. Silakan login.';
} else {
$error = 'Registrasi gagal: ' . $koneksi->error;
}
}
}
}
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Registrasi Penerima</title>
<style>
*{box-sizing:border-box} body{margin:0;min-height:100vh;font-family:Arial,sans-serif;background:#f1f5f9;display:flex;align-items:center;justify-content:center;padding:20px}.card{width:100%;max-width:460px;background:#fff;border-radius:18px;box-shadow:0 12px 32px rgba(15,23,42,.12);padding:28px}h1{margin:0 0 5px;color:#0f172a;font-size:23px}.muted{color:#64748b;font-size:13px;margin-bottom:16px}.fg{margin:13px 0}.fg label{display:block;margin-bottom:6px;font-size:13px;font-weight:700;color:#334155}.fg input{width:100%;border:1px solid #cbd5e1;border-radius:10px;padding:12px}.btn{width:100%;border:0;border-radius:10px;padding:12px;background:#0f766e;color:#fff;font-weight:700;cursor:pointer}.msg{padding:10px;border-radius:10px;font-size:13px;margin:10px 0}.err{background:#fee2e2;color:#991b1b}.ok{background:#dcfce7;color:#166534}.links{text-align:center;margin-top:15px}.links a{color:#0f766e;text-decoration:none;font-weight:700}
</style>
</head>
<body><div class="card">
<h1>Registrasi Penerima</h1><div class="muted">Akun ini digunakan untuk mengajukan penerima bantuan melalui peta.</div>
<?php if($error): ?><div class="msg err"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<?php if($success): ?><div class="msg ok"><?= htmlspecialchars($success) ?></div><?php endif; ?>
<form method="post">
<div class="fg"><label>Nama Lengkap</label><input name="nama" required></div>
<div class="fg"><label>Username</label><input name="username" required></div>
<div class="fg"><label>Password</label><input type="password" name="password" required></div>
<div class="fg"><label>Konfirmasi Password</label><input type="password" name="password2" required></div>
<button class="btn">Daftar</button>
</form>
<div class="links"><a href="login.php">Kembali ke login</a></div>
</div></body></html>
+83
View File
@@ -0,0 +1,83 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
$allowedKondisi = ['baik','sedang','rusak'];
$allowedJenis = ['zakat','sembako','uang_tunai','lainnya'];
$nama = clean_text(post('nama_penerima'), 100);
$nik = clean_text(post('nik'), 30);
$no_hp = clean_text(post('no_hp'), 20);
$alamat = clean_text(post('alamat'), 1000);
$pekerjaan = clean_text(post('pekerjaan'), 100);
$penghasilan = (int) post('penghasilan', 0);
$jumlah_tanggungan = (int) post('jumlah_tanggungan', 0);
$kondisi_rumah = post('kondisi_rumah');
$jenis_bantuan = post('jenis_bantuan');
$username_penerima = clean_text(post('username_penerima'), 50);
$lat = post('latitude');
$lng = post('longitude');
if ($nama==='' || $no_hp==='' || $alamat==='' || $pekerjaan==='' || !valid_latlng($lat, $lng)) json_response('err', 'Data wajib belum lengkap atau koordinat tidak valid.');
if (!valid_enum($kondisi_rumah, $allowedKondisi) || !valid_enum($jenis_bantuan, $allowedJenis)) json_response('err', 'Pilihan kondisi rumah atau jenis bantuan tidak valid.');
if ($penghasilan < 0 || $jumlah_tanggungan < 0) json_response('err', 'Penghasilan dan tanggungan tidak valid.');
$role = $_SESSION['role'];
$user_id = null;
$rumah_ibadah_id = null;
$jarak_meter = null;
$sumber = 'mandiri';
if ($role === 'penerima') {
$user_id = (int) $_SESSION['user_id'];
$sumber = 'mandiri';
} elseif ($role === 'admin_rumah_ibadah' || $role === 'super_admin') {
if ($username_penerima === '') json_response('err', 'Username penerima wajib diisi agar titik rumah terkait ke akun yang benar.', [], 422);
$akunPenerima = get_user_by_username_role($username_penerima, 'penerima');
if (!$akunPenerima) json_response('err', 'Username penerima tidak ditemukan, bukan role penerima, atau akun tidak aktif.', [], 404);
$user_id = (int) $akunPenerima['id'];
$sumber = 'admin_rumah_ibadah';
} else {
json_response('err', 'Role tidak valid.', [], 403);
}
// Satu akun penerima hanya boleh punya satu pin aktif.
if (has_active_penerima_pin($user_id)) {
json_response('err', 'Akun penerima ini sudah memiliki pin aktif. Satu akun penerima hanya boleh punya satu pin aktif.', [], 409);
}
$nearest = get_nearest_rumah_ibadah((float) $lat, (float) $lng, 300);
if ($nearest) {
$rumah_ibadah_id = (int) $nearest['id'];
$jarak_meter = (float) $nearest['jarak_meter'];
}
if ($role === 'admin_rumah_ibadah') {
$ownNearest = get_nearest_rumah_ibadah((float)$lat, (float)$lng, 300, (int)$_SESSION['user_id']);
if (!$ownNearest) json_response('err', 'Lokasi penerima berada di luar radius 300 meter seluruh rumah ibadah Anda.');
if ($nearest && (int)$nearest['id'] !== (int)$ownNearest['id']) {
json_response('err', 'Lokasi penerima lebih dekat ke rumah ibadah lain. Sistem hanya boleh mengaitkan penerima ke rumah ibadah terdekat.', [], 409);
}
$rumah_ibadah_id = (int) $ownNearest['id'];
$jarak_meter = (float) $ownNearest['jarak_meter'];
}
$status = 'menunggu';
// FIX: pisahkan cabang INSERT agar rumah_ibadah_id dan jarak_meter NULL tersimpan benar, bukan 0.
if ($rumah_ibadah_id !== null) {
$stmt = $koneksi->prepare("INSERT INTO penerima_bantuan (user_id, rumah_ibadah_id, nama_penerima, nik, no_hp, alamat, pekerjaan, penghasilan, jumlah_tanggungan, kondisi_rumah, jenis_bantuan, latitude, longitude, jarak_meter, status_pengajuan, sumber_pengajuan) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
$stmt->bind_param('iisssssiissdddss', $user_id, $rumah_ibadah_id, $nama, $nik, $no_hp, $alamat, $pekerjaan, $penghasilan, $jumlah_tanggungan, $kondisi_rumah, $jenis_bantuan, $lat, $lng, $jarak_meter, $status, $sumber);
} else {
$stmt = $koneksi->prepare("INSERT INTO penerima_bantuan (user_id, rumah_ibadah_id, nama_penerima, nik, no_hp, alamat, pekerjaan, penghasilan, jumlah_tanggungan, kondisi_rumah, jenis_bantuan, latitude, longitude, jarak_meter, status_pengajuan, sumber_pengajuan) VALUES (?,NULL,?,?,?,?,?,?,?,?,?,?,?,NULL,?,?)");
$stmt->bind_param('isssssiissddss', $user_id, $nama, $nik, $no_hp, $alamat, $pekerjaan, $penghasilan, $jumlah_tanggungan, $kondisi_rumah, $jenis_bantuan, $lat, $lng, $status, $sumber);
}
if (!$stmt->execute()) json_response('err', 'Gagal menyimpan penerima: ' . $koneksi->error);
$id = $stmt->insert_id;
insert_riwayat_status($id, null, $status, 'Pengajuan penerima dibuat.');
$row = get_penerima_by_id($id);
$msg = $rumah_ibadah_id ? 'Pengajuan penerima berhasil disimpan dan masuk radius rumah ibadah terdekat.' : 'Pengajuan tersimpan, tetapi belum ada pemberi bantuan dalam radius 300 meter. Nanti akan otomatis terhubung jika ada rumah ibadah diterima di radius tersebut.';
json_response('ok', $msg, $row);
?>
+76
View File
@@ -0,0 +1,76 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role(['super_admin','admin_rumah_ibadah'], true);
$program_id = (int) post('program_id', 0);
$penerima_id = (int) post('penerima_id', 0);
$ket = clean_text(post('keterangan'), 1500);
$tanggal = post('tanggal_penyaluran', date('Y-m-d'));
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $tanggal)) json_response('err', 'Tanggal penyaluran tidak valid.');
$stmt = $koneksi->prepare("SELECT p.*, ri.user_id AS admin_rumah_user_id, ri.latitude AS ri_latitude, ri.longitude AS ri_longitude, ri.nama_rumah_ibadah FROM program_bantuan p LEFT JOIN rumah_ibadah ri ON ri.id=p.rumah_ibadah_id WHERE p.id=? AND COALESCE(ri.status_hapus,'normal')<>'disetujui' LIMIT 1");
$stmt->bind_param('i', $program_id);
$program = fetch_all_stmt($stmt)[0] ?? null;
if (!$program || $program['status_program'] === 'dibatalkan') json_response('err', 'Program bantuan tidak ditemukan atau dibatalkan.');
// FIX: cek status program selesai — tidak bisa menyalurkan ke program yang sudah selesai
if ($program['status_program'] === 'selesai') json_response('err', 'Program bantuan sudah berstatus selesai, tidak dapat menyalurkan bantuan baru.', [], 409);
$jenis = $program['jenis_bantuan'];
$nominal = $program['nominal'] === null ? null : (int)$program['nominal'];
$jumlah_item = $program['jumlah_item'] === null ? '' : (string)$program['jumlah_item'];
$penerima = get_penerima_by_id($penerima_id);
if (!$penerima || $penerima['status_pengajuan'] !== 'diterima' || ($penerima['status_hapus'] ?? 'normal') === 'disetujui') json_response('err', 'Penerima tidak ditemukan, belum diterima, atau marker sudah dihapus.');
$rumah_ibadah_id = (int) $program['rumah_ibadah_id'];
if (!$rumah_ibadah_id) json_response('err', 'Program belum terhubung ke rumah ibadah.');
if ((int)$penerima['rumah_ibadah_id'] !== $rumah_ibadah_id) {
json_response('err', 'Program dan penerima tidak berada pada rumah ibadah yang sama. Sinkronkan data penerima terlebih dahulu.', [], 409);
}
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
if (!is_admin_rumah_ibadah_id($rumah_ibadah_id, (int)$_SESSION['user_id'], true)) {
json_response('err', 'Anda hanya bisa menyalurkan bantuan untuk seluruh rumah ibadah milik akun Anda sendiri.', [], 403);
}
}
$jarak = haversine_meter((float)$penerima['latitude'], (float)$penerima['longitude'], (float)$program['ri_latitude'], (float)$program['ri_longitude']);
if ($jarak > 300) json_response('err', 'Penerima berada di luar radius 300 meter rumah ibadah program.');
$bulan = (int) date('m', strtotime($tanggal));
$tahun = (int) date('Y', strtotime($tanggal));
$stmt = $koneksi->prepare("SELECT COUNT(*) AS jml FROM penyaluran_bantuan WHERE penerima_id=? AND program_id=? AND MONTH(tanggal_penyaluran)=? AND YEAR(tanggal_penyaluran)=?");
$stmt->bind_param('iiii', $penerima_id, $program_id, $bulan, $tahun);
$count = fetch_all_stmt($stmt)[0];
$bantuan_ke = ((int)$count['jml']) + 1;
$target = (int)$program['target_frekuensi'];
$warning = '';
if ($bantuan_ke > $target) {
$warning = "Peringatan: penerima ini sudah/akan menerima bantuan $bantuan_ke kali pada program ini. Target program adalah $target kali. Data tetap dicatat untuk audit.";
}
$admin_id = (int) $_SESSION['user_id'];
// FIX: pisahkan cabang INSERT agar nominal NULL tersimpan sebagai NULL, bukan 0.
if ($nominal !== null) {
$stmt = $koneksi->prepare("INSERT INTO penyaluran_bantuan (program_id, penerima_id, rumah_ibadah_id, admin_id, jenis_bantuan, nominal, jumlah_item, bantuan_ke, keterangan, tanggal_penyaluran) VALUES (?,?,?,?,?,?,?,?,?,?)");
$stmt->bind_param('iiiisisiss', $program_id, $penerima_id, $rumah_ibadah_id, $admin_id, $jenis, $nominal, $jumlah_item, $bantuan_ke, $ket, $tanggal);
} else {
$stmt = $koneksi->prepare("INSERT INTO penyaluran_bantuan (program_id, penerima_id, rumah_ibadah_id, admin_id, jenis_bantuan, nominal, jumlah_item, bantuan_ke, keterangan, tanggal_penyaluran) VALUES (?,?,?,?,?,NULL,?,?,?,?)");
$stmt->bind_param('iiiississ', $program_id, $penerima_id, $rumah_ibadah_id, $admin_id, $jenis, $jumlah_item, $bantuan_ke, $ket, $tanggal);
}
if (!$stmt->execute()) json_response('err', 'Gagal mencatat penyaluran bantuan: ' . $koneksi->error);
json_response('ok', 'Penyaluran bantuan berhasil dicatat sesuai master program.' . ($warning ? ' ' . $warning : ''), [
'id' => $stmt->insert_id,
'bantuan_ke' => $bantuan_ke,
'jenis_bantuan' => $jenis,
'nominal' => $nominal,
'jumlah_item' => $jumlah_item,
'warning' => $warning
]);
?>
+48
View File
@@ -0,0 +1,48 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role(['super_admin','admin_rumah_ibadah'], true);
$allowedJenis = ['zakat','sembako','uang_tunai','lainnya'];
$nama = clean_text(post('nama_program'), 150);
$jenis = post('jenis_bantuan');
$deskripsi = clean_text(post('deskripsi'), 1500);
$bulan = (int) post('bulan');
$tahun = (int) post('tahun');
$target = (int) post('target_frekuensi', 1);
$nominalInput = post('nominal', '');
$nominal = ($nominalInput === '') ? null : (int) $nominalInput;
$jumlah_item = clean_text(post('jumlah_item', ''), 100);
$rumah_ibadah_id = post('rumah_ibadah_id', '');
if ($nama==='' || !valid_enum($jenis, $allowedJenis)) json_response('err', 'Nama program atau jenis bantuan tidak valid.');
if ($bulan < 1 || $bulan > 12 || $tahun < 2000 || $target < 1) json_response('err', 'Bulan, tahun, atau target frekuensi tidak valid.');
if ($nominal !== null && $nominal < 0) json_response('err', 'Nominal tidak boleh minus.');
if ($_SESSION['role'] === 'admin_rumah_ibadah') {
$ids = get_admin_rumah_ibadah_ids((int)$_SESSION['user_id'], true);
if (empty($ids)) json_response('err', 'Admin belum memiliki rumah ibadah diterima.');
if ($rumah_ibadah_id === '') {
if (count($ids) > 1) json_response('err', 'Pilih rumah ibadah terlebih dahulu karena akun ini mengelola lebih dari satu rumah ibadah.', [], 422);
$rumah_ibadah_id = $ids[0];
} else {
$rumah_ibadah_id = (int)$rumah_ibadah_id;
if (!in_array($rumah_ibadah_id, $ids, true)) json_response('err', 'Rumah ibadah tidak termasuk milik admin ini.', [], 403);
}
} else {
$rumah_ibadah_id = ($rumah_ibadah_id === '') ? null : (int) $rumah_ibadah_id;
if (!$rumah_ibadah_id) json_response('err', 'Rumah ibadah wajib dipilih.');
}
// FIX: pisahkan cabang INSERT agar nominal NULL tersimpan sebagai NULL, bukan 0.
if ($nominal !== null) {
$stmt = $koneksi->prepare("INSERT INTO program_bantuan (rumah_ibadah_id, nama_program, jenis_bantuan, deskripsi, bulan, tahun, target_frekuensi, nominal, jumlah_item) VALUES (?,?,?,?,?,?,?,?,?)");
$stmt->bind_param('isssiiiis', $rumah_ibadah_id, $nama, $jenis, $deskripsi, $bulan, $tahun, $target, $nominal, $jumlah_item);
} else {
$stmt = $koneksi->prepare("INSERT INTO program_bantuan (rumah_ibadah_id, nama_program, jenis_bantuan, deskripsi, bulan, tahun, target_frekuensi, nominal, jumlah_item) VALUES (?,?,?,?,?,?,?,NULL,?)");
$stmt->bind_param('isssiiis', $rumah_ibadah_id, $nama, $jenis, $deskripsi, $bulan, $tahun, $target, $jumlah_item);
}
if (!$stmt->execute()) json_response('err', 'Gagal membuat program bantuan: ' . $koneksi->error);
json_response('ok', 'Program bantuan berhasil dibuat. Jenis, nominal, dan jumlah item akan menjadi acuan mutlak saat penyaluran.', ['id' => $stmt->insert_id]);
?>
+63
View File
@@ -0,0 +1,63 @@
<?php
/**
* tambah_user.php
* Membuat akun pengguna dari dashboard.
* - Super admin dapat membuat akun admin_rumah_ibadah dan penerima.
* - Admin rumah ibadah hanya dapat membuat akun penerima.
* Password disimpan memakai password_hash().
*/
require_once __DIR__ . '/auth.php';
require_login(true);
require_role(['super_admin', 'admin_rumah_ibadah'], true);
require_post();
$actor = current_user();
$actorRole = $actor['role'];
$nama = clean_text(post('nama'), 100);
$username = clean_text(post('username'), 50);
$password = post('password');
$role = post('role');
$status_akun = post('status_akun', 'aktif');
$statusList = ['aktif', 'nonaktif'];
$allowedRoles = ($actorRole === 'super_admin') ? ['admin_rumah_ibadah', 'penerima'] : ['penerima'];
if ($nama === '' || $username === '' || $password === '') {
json_response('err', 'Nama, username, dan password wajib diisi.', [], 422);
}
if (!preg_match('/^[a-zA-Z0-9_\.\-]{4,50}$/', $username)) {
json_response('err', 'Username minimal 4 karakter dan hanya boleh huruf, angka, titik, strip, atau underscore.', [], 422);
}
if (strlen($password) < 6) {
json_response('err', 'Password minimal 6 karakter.', [], 422);
}
if (!valid_enum($role, $allowedRoles)) {
json_response('err', 'Role tidak boleh dibuat oleh akun ini.', [], 403);
}
if (!valid_enum($status_akun, $statusList)) {
json_response('err', 'Status akun tidak valid.', [], 422);
}
$stmt = $koneksi->prepare("SELECT id FROM users WHERE username=? LIMIT 1");
$stmt->bind_param('s', $username);
$stmt->execute();
if ($stmt->get_result()->fetch_assoc()) {
json_response('err', 'Username sudah digunakan.', [], 409);
}
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $koneksi->prepare("INSERT INTO users (nama, username, password, role, status_akun) VALUES (?,?,?,?,?)");
$stmt->bind_param('sssss', $nama, $username, $hash, $role, $status_akun);
if (!$stmt->execute()) {
json_response('err', 'Gagal membuat akun: ' . $stmt->error, [], 500);
}
json_response('ok', 'Akun berhasil dibuat.', [
'id' => $stmt->insert_id,
'nama' => $nama,
'username' => $username,
'role' => $role,
'status_akun' => $status_akun
]);
?>
+56
View File
@@ -0,0 +1,56 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role(['super_admin','admin_rumah_ibadah'], true);
$id = (int) post('id', 0);
$status = post('status_pengajuan', post('status', ''));
$alasan = clean_text(post('alasan_status', post('alasan', '')), 1000);
$allowed = ['menunggu','diterima','ditolak','dibatalkan','hapus_disetujui','hapus_ditolak'];
if (!valid_enum($status, $allowed)) json_response('err', 'Status tidak valid.');
$penerima = get_penerima_by_id($id);
if (!$penerima) json_response('err', 'Data penerima tidak ditemukan.', [], 404);
// Persetujuan/penolakan hapus marker hanya boleh dilakukan super admin.
if ($status === 'hapus_disetujui' || $status === 'hapus_ditolak') {
require_role('super_admin', true);
$catatan = $alasan ?: ($status === 'hapus_disetujui' ? 'Pengajuan hapus marker penerima disetujui.' : 'Pengajuan hapus marker penerima ditolak.');
if ($status === 'hapus_disetujui') {
$statusPengajuan = 'dibatalkan';
$statusHapus = 'disetujui';
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET status_pengajuan=?, status_hapus=?, alasan_hapus=?, alasan_status=? WHERE id=?");
$stmt->bind_param('ssssi', $statusPengajuan, $statusHapus, $catatan, $catatan, $id);
} else {
$statusHapus = 'ditolak';
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET status_hapus=?, alasan_hapus=? WHERE id=?");
$stmt->bind_param('ssi', $statusHapus, $catatan, $id);
}
if (!$stmt->execute()) json_response('err', 'Gagal memproses persetujuan hapus penerima: ' . $koneksi->error);
insert_riwayat_status($id, $penerima['status_pengajuan'], $status, $catatan);
json_response('ok', 'Persetujuan hapus marker penerima berhasil diproses.', get_penerima_by_id($id));
}
if (!can_manage_penerima($penerima)) json_response('err', 'Data tidak ditemukan atau akses ditolak.', [], 403);
// FIX: admin_rumah_ibadah tidak boleh memundurkan status ke 'menunggu' jika sudah 'diterima' atau 'ditolak'.
// Hanya super_admin yang bisa mereset status.
if ($status === 'menunggu' && $_SESSION['role'] === 'admin_rumah_ibadah') {
json_response('err', 'Admin rumah ibadah tidak dapat mereset status penerima ke menunggu. Hubungi super admin.', [], 403);
}
// FIX: admin_rumah_ibadah tidak bisa membatalkan penerima yang sudah 'diterima' — hanya super_admin.
if ($status === 'dibatalkan' && $penerima['status_pengajuan'] === 'diterima' && $_SESSION['role'] === 'admin_rumah_ibadah') {
json_response('err', 'Admin rumah ibadah tidak dapat membatalkan penerima yang sudah berstatus diterima. Hubungi super admin.', [], 403);
}
$status_lama = $penerima['status_pengajuan'];
$stmt = $koneksi->prepare("UPDATE penerima_bantuan SET status_pengajuan=?, alasan_status=? WHERE id=?");
$stmt->bind_param('ssi', $status, $alasan, $id);
if (!$stmt->execute()) json_response('err', 'Gagal update status: ' . $koneksi->error);
insert_riwayat_status($id, $status_lama, $status, $alasan ?: 'Status penerima diubah.');
$row = get_penerima_by_id($id);
json_response('ok', 'Status penerima berhasil diubah.', $row);
?>
+63
View File
@@ -0,0 +1,63 @@
<?php
require_once __DIR__ . '/auth.php';
require_login(true);
require_post();
require_role('super_admin', true);
$id = (int) post('id', 0);
$status = post('status_pengajuan', post('status', ''));
$ket = clean_text(post('keterangan', post('alasan', '')), 1000);
if (!valid_enum($status, ['menunggu','diterima','ditolak','hapus_disetujui','hapus_ditolak'])) json_response('err', 'Status rumah ibadah tidak valid.');
$rumah = get_rumah_ibadah_by_id($id);
if (!$rumah) json_response('err', 'Rumah ibadah tidak ditemukan.', [], 404);
$jumlahSinkron = 0;
$jumlahLepas = 0;
if ($status === 'hapus_disetujui' || $status === 'hapus_ditolak') {
$catatan = $ket ?: ($status === 'hapus_disetujui' ? 'Pengajuan hapus marker rumah ibadah disetujui.' : 'Pengajuan hapus marker rumah ibadah ditolak.');
if ($status === 'hapus_disetujui') {
$statusPengajuan = 'ditolak';
$statusHapus = 'disetujui';
$stmt = $koneksi->prepare("UPDATE rumah_ibadah SET status_pengajuan=?, status_hapus=?, alasan_hapus=?, keterangan=? WHERE id=?");
$stmt->bind_param('ssssi', $statusPengajuan, $statusHapus, $catatan, $catatan, $id);
if (!$stmt->execute()) json_response('err', 'Gagal menyetujui hapus rumah ibadah: ' . $koneksi->error);
$jumlahLepas = detach_penerima_from_rumah_ibadah($id);
} else {
$statusHapus = 'ditolak';
$stmt = $koneksi->prepare("UPDATE rumah_ibadah SET status_hapus=?, alasan_hapus=? WHERE id=?");
$stmt->bind_param('ssi', $statusHapus, $catatan, $id);
if (!$stmt->execute()) json_response('err', 'Gagal menolak hapus rumah ibadah: ' . $koneksi->error);
}
$row = get_rumah_ibadah_by_id($id);
$row['jumlah_penerima_tersinkron'] = 0;
$row['jumlah_penerima_dilepas'] = $jumlahLepas;
json_response('ok', 'Persetujuan hapus marker rumah ibadah berhasil diproses.', $row);
}
// Update status pengajuan rumah ibadah biasa.
$statusHapus = 'normal';
$stmt = $koneksi->prepare("UPDATE rumah_ibadah SET status_pengajuan=?, status_hapus=?, keterangan=? WHERE id=?");
$stmt->bind_param('sssi', $status, $statusHapus, $ket, $id);
if (!$stmt->execute()) json_response('err', 'Gagal update status rumah ibadah: ' . $koneksi->error);
if ($status === 'diterima') {
// Saat rumah ibadah disetujui, penerima yang dibuat lebih dulu otomatis masuk area jika radius <= 300 meter.
$jumlahSinkron = sync_penerima_area_by_rumah_ibadah($id);
} elseif ($status === 'ditolak') {
// Jika rumah ibadah ditolak, penerima yang sebelumnya terkait dilepas agar tidak mengarah ke pemberi bantuan tidak aktif.
$jumlahLepas = detach_penerima_from_rumah_ibadah($id);
}
$row = get_rumah_ibadah_by_id($id);
if ($row) {
$row['jumlah_penerima_tersinkron'] = $jumlahSinkron;
$row['jumlah_penerima_dilepas'] = $jumlahLepas;
}
$msg = 'Status rumah ibadah berhasil diubah.';
if ($jumlahSinkron) $msg .= " $jumlahSinkron penerima otomatis masuk area tanggung jawab.";
if ($jumlahLepas) $msg .= " $jumlahLepas penerima dilepas dari rumah ibadah ini.";
json_response('ok', $msg, $row);
?>
+9
View File
@@ -0,0 +1,9 @@
FROM php:8.2-apache
RUN docker-php-ext-install mysqli pdo pdo_mysql && a2enmod rewrite && echo "ServerName localhost" >> /etc/apache2/apache2.conf
WORKDIR /var/www/html
COPY . /var/www/html/
RUN chown -R www-data:www-data /var/www/html
EXPOSE 80
+72
View File
@@ -0,0 +1,72 @@
# D1041231091 - WebGIS 01 sampai 05
Repository ini sudah disiapkan untuk deploy **sekali saja** menggunakan Docker Compose.
## Isi Project
- `/` = landing page utama
- `/01/` = Tugas 01 WebGIS SPBU
- `/02/` = Tugas 02 WebGIS SPBU + polyline + polygon
- `/03/` = Tugas 03 WebGIS jalan dan parsil tanah
- `/04/` = Tugas 04 WebGIS SPBU dengan layer control
- `/05/` = Tugas 05 WebGIS Poverty Mapping
## Akun awal Tugas 05
- Username: `superadmin`
- Password: `password`
## Cara push ke Gitea
```bat
cd C:\laragon\www\webgis-spbu\D1041231091-WebGIS-All
git init
git branch -M main
git add .
git commit -m "Deploy WebGIS 01-05 dengan Docker"
git remote add origin https://git.ifuntanhub.dev/requestha/D1041231091-WebGIS-All.git
git push -u origin main
```
Kalau remote sudah ada:
```bat
git remote set-url origin https://git.ifuntanhub.dev/requestha/D1041231091-WebGIS-All.git
git push -u origin main
```
## Cara deploy di Coolify
1. Buat resource baru.
2. Pilih **Public Repository**.
3. Isi repository URL Gitea.
4. Branch: `main`.
5. Build Pack: **Docker Compose**.
6. Docker Compose Location: `/docker-compose.yml`.
7. Setelah resource dibuat, masuk ke service `web`.
8. Isi domain pada service `web`, contoh:
```text
http://d1041231091-requestha-webgisall.203.24.51.230.sslip.io
```
9. Klik **Deploy** satu kali.
10. Tunggu sampai `web` dan `db` running.
## Link setelah deploy
```text
http://domain-kamu/
http://domain-kamu/01/
http://domain-kamu/02/
http://domain-kamu/03/
http://domain-kamu/04/
http://domain-kamu/05/
```
## Catatan penting
- Jangan pilih menu database terpisah di Coolify. Database sudah ada di `docker-compose.yml`.
- Jangan pakai Docker Compose Empty jika kode belum ada di server. Pakai Public Repository agar Coolify build langsung dari Gitea.
- Jangan klik Deploy berkali-kali. Kalau Bad Gateway, cek log `web` dulu.
- Database otomatis diimport dari folder `db/init/` saat container `db` pertama kali dibuat.
+2
View File
@@ -0,0 +1,2 @@
FROM mysql:8.4
COPY init/ /docker-entrypoint-initdb.d/
+2
View File
@@ -0,0 +1,2 @@
Folder ini berisi Dockerfile database dan SQL init.
MySQL akan menjalankan file di `init/` otomatis saat container pertama kali dibuat.
+82
View File
@@ -0,0 +1,82 @@
CREATE DATABASE IF NOT EXISTS `01_spbudb` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `01_spbudb`;
-- 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 */;
+146
View File
@@ -0,0 +1,146 @@
CREATE DATABASE IF NOT EXISTS `02_spbudb` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `02_spbudb`;
-- 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: `02_spbudb`
--
-- --------------------------------------------------------
--
-- Table structure for table `polygon`
--
CREATE TABLE `polygon` (
`id` int NOT NULL,
`nama_polygon` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`keterangan` text COLLATE utf8mb4_unicode_ci,
`warna` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '#22c55e',
`koordinat` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `polygon`
--
INSERT INTO `polygon` (`id`, `nama_polygon`, `keterangan`, `warna`, `koordinat`, `created_at`, `updated_at`) VALUES
(1, 'Rumah A', 'Rumah A di tepi jalan imbon', '#c42170', '[[-0.04619857363388995,109.35349255800249],[-0.04609122197957324,109.35370445251466],[-0.046228095338798736,109.35379564762117],[-0.04634349836696752,109.35357302427295]]', '2026-06-08 09:59:46', '2026-06-08 09:59:46');
-- --------------------------------------------------------
--
-- Table structure for table `polyline`
--
CREATE TABLE `polyline` (
`id` int NOT NULL,
`nama_line` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`keterangan` text COLLATE utf8mb4_unicode_ci,
`warna` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '#3b82f6',
`koordinat` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `polyline`
--
INSERT INTO `polyline` (`id`, `nama_line`, `keterangan`, `warna`, `koordinat`, `created_at`, `updated_at`) VALUES
(1, 'Jl Tanjung Sari', 'Jalan tembusan \'TanSar\", dari mulut di ImBon dan dari mulut Jl. Jend. Ayani', '#3b82f6', '[[-0.052839376622127024,109.34751927852632],[-0.05137402667155636,109.34858143329622],[-0.050509845915652725,109.34916079044343],[-0.04923236129902831,109.35004591941835],[-0.04847016458323742,109.35060918331146],[-0.04772943818927965,109.35115635395051],[-0.04757377829389879,109.35129046440126],[-0.047589881041730155,109.35139775276186],[-0.04771333544149917,109.35161232948305],[-0.04650562934709907,109.3523955345154],[-0.045646816111789314,109.35296952724458],[-0.0457756380977098,109.35312509536745]]', '2026-06-08 10:03:20', '2026-06-08 10:03:20');
-- --------------------------------------------------------
--
-- Table structure for table `spbu`
--
CREATE TABLE `spbu` (
`id` int NOT NULL,
`nama_spbu` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`status_ramai` enum('ramai','tidak_ramai') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'tidak_ramai',
`buka_24_jam` enum('ya','tidak') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'tidak',
`status_operasional` enum('buka','tutup') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'buka',
`nomor_hp` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
`latitude` double NOT NULL,
`longitude` double NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `spbu`
--
INSERT INTO `spbu` (`id`, `nama_spbu`, `status_ramai`, `buka_24_jam`, `status_operasional`, `nomor_hp`, `latitude`, `longitude`, `created_at`, `updated_at`) VALUES
(4, 'SPBU Imbon', 'ramai', 'ya', 'buka', '0812345678913', -0.046981865484009226, 109.35455471277238, '2026-06-08 09:58:55', '2026-06-08 09:58:55');
--
-- Indexes for dumped tables
--
--
-- Indexes for table `polygon`
--
ALTER TABLE `polygon`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `polyline`
--
ALTER TABLE `polyline`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `spbu`
--
ALTER TABLE `spbu`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `polygon`
--
ALTER TABLE `polygon`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT for table `polyline`
--
ALTER TABLE `polyline`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT for table `spbu`
--
ALTER TABLE `spbu`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5;
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 */;
+109
View File
@@ -0,0 +1,109 @@
CREATE DATABASE IF NOT EXISTS `03_webgisdb` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `03_webgisdb`;
-- 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: `03_webgisdb`
--
-- --------------------------------------------------------
--
-- Table structure for table `jalan`
--
CREATE TABLE `jalan` (
`id` int NOT NULL,
`nama_jalan` varchar(150) NOT NULL,
`status_jalan` enum('nasional','provinsi','kabupaten') NOT NULL,
`panjang_meter` double NOT NULL,
`geojson` longtext 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 `jalan`
--
INSERT INTO `jalan` (`id`, `nama_jalan`, `status_jalan`, `panjang_meter`, `geojson`, `created_at`, `updated_at`) VALUES
(1, 'Gg. Pak Cik', 'kabupaten', 169.77, '{\"type\":\"Feature\",\"properties\":{},\"geometry\":{\"type\":\"LineString\",\"coordinates\":[[109.34135,-0.055498],[109.342616,-0.056351]]}}', '2026-06-08 12:30:39', '2026-06-08 12:30:39'),
(2, 'Jl. Trans Kalimantan', 'nasional', 5544.23, '{\"type\":\"Feature\",\"properties\":{},\"geometry\":{\"type\":\"LineString\",\"coordinates\":[[109.374583,-0.069933],[109.375999,-0.068258],[109.379303,-0.064651],[109.380591,-0.063685],[109.381771,-0.062891],[109.383423,-0.061517],[109.385548,-0.059992],[109.386578,-0.059327],[109.387114,-0.059048],[109.387758,-0.058833],[109.38868,-0.058318],[109.389496,-0.058038],[109.390697,-0.05748],[109.391813,-0.056815],[109.393015,-0.056128],[109.394302,-0.055677],[109.39559,-0.055226],[109.396598,-0.054882],[109.397478,-0.054603],[109.398851,-0.054324],[109.401062,-0.053723],[109.401855,-0.053465],[109.402649,-0.052907],[109.403057,-0.052284],[109.403958,-0.049257],[109.405246,-0.044942],[109.406469,-0.041313],[109.406598,-0.04054],[109.406598,-0.039338],[109.406254,-0.038694],[109.406254,-0.038221],[109.406641,-0.038178],[109.406984,-0.038565]]}}', '2026-06-08 12:34:53', '2026-06-08 12:34:53');
-- --------------------------------------------------------
--
-- Table structure for table `parsil_tanah`
--
CREATE TABLE `parsil_tanah` (
`id` int NOT NULL,
`nama_parsil` varchar(150) NOT NULL,
`status_kepemilikan` enum('SHM','HGB','HGU','HP') NOT NULL,
`luas_meter` double NOT NULL,
`geojson` longtext 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 `parsil_tanah`
--
INSERT INTO `parsil_tanah` (`id`, `nama_parsil`, `status_kepemilikan`, `luas_meter`, `geojson`, `created_at`, `updated_at`) VALUES
(1, 'kavling A1', 'HP', 1239.31, '{\"type\":\"Feature\",\"properties\":{},\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[109.339339,-0.060625],[109.339548,-0.060442],[109.339803,-0.060689],[109.339575,-0.060877],[109.339339,-0.060625]]]}}', '2026-06-08 12:30:03', '2026-06-08 12:30:03');
--
-- Indexes for dumped tables
--
--
-- Indexes for table `jalan`
--
ALTER TABLE `jalan`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `parsil_tanah`
--
ALTER TABLE `parsil_tanah`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `jalan`
--
ALTER TABLE `jalan`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
--
-- AUTO_INCREMENT for table `parsil_tanah`
--
ALTER TABLE `parsil_tanah`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
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 */;
+80
View File
@@ -0,0 +1,80 @@
CREATE DATABASE IF NOT EXISTS `04_spbudb` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `04_spbudb`;
-- phpMyAdmin SQL Dump
-- version 5.2.3
-- https://www.phpmyadmin.net/
--
-- Host: localhost:3306
-- Generation Time: Jun 11, 2026 at 11:13 AM
-- Server version: 8.4.3
-- PHP Version: 8.3.30
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Database: `04_spbudb`
--
-- --------------------------------------------------------
--
-- Table structure for table `spbu`
--
CREATE TABLE `spbu` (
`id` int NOT NULL,
`nama_spbu` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`status_ramai` enum('ramai','tidak_ramai') COLLATE utf8mb4_unicode_ci NOT NULL,
`buka_24_jam` enum('ya','tidak') COLLATE utf8mb4_unicode_ci NOT NULL,
`status_operasional` enum('buka','tutup') COLLATE utf8mb4_unicode_ci NOT NULL,
`nomor_hp` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`latitude` double NOT NULL,
`longitude` double NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `spbu`
--
INSERT INTO `spbu` (`id`, `nama_spbu`, `status_ramai`, `buka_24_jam`, `status_operasional`, `nomor_hp`, `latitude`, `longitude`, `created_at`, `updated_at`) VALUES
(1, 'SPBU Gertak 1', 'tidak_ramai', 'ya', 'buka', '0815497422156', -0.0551481, 109.3480718, '2026-06-09 06:06:31', '2026-06-09 06:06:31'),
(2, 'SPBU Gertak 1', 'ramai', 'ya', 'buka', '0815497422156', -0.0548271, 109.3477392, '2026-06-09 06:06:52', '2026-06-09 06:07:09'),
(3, 'SPBU OSO', 'ramai', 'tidak', 'buka', '0815497422156', -0.0551277, 109.3474925, '2026-06-09 06:07:35', '2026-06-09 06:07:35'),
(4, 'SPBU OSO', 'tidak_ramai', 'tidak', 'buka', '087894587965', -0.0552458, 109.3478036, '2026-06-09 06:07:54', '2026-06-09 06:07:54'),
(5, 'SPBU Gertak 2', 'ramai', 'ya', 'tutup', '0815497422156', -0.0554175, 109.3474495, '2026-06-09 06:08:15', '2026-06-09 06:08:15');
--
-- Indexes for dumped tables
--
--
-- Indexes for table `spbu`
--
ALTER TABLE `spbu`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `spbu`
--
ALTER TABLE `spbu`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+378
View File
@@ -0,0 +1,378 @@
CREATE DATABASE IF NOT EXISTS `05_povertydb` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `05_povertydb`;
SET FOREIGN_KEY_CHECKS=0;
-- phpMyAdmin SQL Dump
-- version 5.2.3
-- https://www.phpmyadmin.net/
--
-- Host: localhost:3306
-- Generation Time: Jun 12, 2026 at 10:30 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: `05_povertydb`
--
-- --------------------------------------------------------
--
-- Table structure for table `penerima_bantuan`
--
CREATE TABLE `penerima_bantuan` (
`id` int NOT NULL,
`user_id` int DEFAULT NULL,
`rumah_ibadah_id` int DEFAULT NULL,
`nama_penerima` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`nik` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`no_hp` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`alamat` text COLLATE utf8mb4_unicode_ci NOT NULL,
`pekerjaan` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`penghasilan` int NOT NULL,
`jumlah_tanggungan` int NOT NULL,
`kondisi_rumah` enum('baik','sedang','rusak') COLLATE utf8mb4_unicode_ci NOT NULL,
`jenis_bantuan` enum('zakat','sembako','uang_tunai','lainnya') COLLATE utf8mb4_unicode_ci NOT NULL,
`latitude` double NOT NULL,
`longitude` double NOT NULL,
`jarak_meter` double DEFAULT NULL,
`status_pengajuan` enum('menunggu','diterima','ditolak','dibatalkan') COLLATE utf8mb4_unicode_ci DEFAULT 'menunggu',
`status_hapus` enum('normal','menunggu','disetujui','ditolak') COLLATE utf8mb4_unicode_ci DEFAULT 'normal',
`alasan_hapus` text COLLATE utf8mb4_unicode_ci,
`alasan_status` text COLLATE utf8mb4_unicode_ci,
`sumber_pengajuan` enum('mandiri','admin_rumah_ibadah') COLLATE utf8mb4_unicode_ci 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_unicode_ci;
--
-- Dumping data for table `penerima_bantuan`
--
INSERT INTO `penerima_bantuan` (`id`, `user_id`, `rumah_ibadah_id`, `nama_penerima`, `nik`, `no_hp`, `alamat`, `pekerjaan`, `penghasilan`, `jumlah_tanggungan`, `kondisi_rumah`, `jenis_bantuan`, `latitude`, `longitude`, `jarak_meter`, `status_pengajuan`, `status_hapus`, `alasan_hapus`, `alasan_status`, `sumber_pengajuan`, `created_at`, `updated_at`) VALUES
(1, 4, 1, 'Jotok', '60103011704940005', '08567891325', 'Jl. Bujang Malaka, RT/RW 024/008, Beringin, Kapuas, Sanggau', 'Tukang Antar Koran', 500000, 2, 'sedang', 'sembako', -0.07088082929060342, 109.36113953590394, 160.17, 'diterima', 'normal', NULL, '', 'mandiri', '2026-06-09 10:10:05', '2026-06-09 11:56:27'),
(2, 6, 5, 'caca', '124854', '410154', 'Gg. Kasturi Raja, Jl. Kenari', 'Mahasiswa', 0, 0, 'baik', 'uang_tunai', -0.020854501391015294, 109.33250427246095, 259.51, 'diterima', 'normal', NULL, '', 'mandiri', '2026-06-09 10:57:26', '2026-06-09 14:53:12'),
(4, NULL, 2, 'angel1', '123456', '123456', '1234', 'wwwwww', 0, 0, 'sedang', 'uang_tunai', -0.025105475576032914, 109.33623790740968, 128.27, 'diterima', 'normal', NULL, '', 'admin_rumah_ibadah', '2026-06-09 11:27:12', '2026-06-09 11:28:31'),
(5, NULL, 2, 'angel2', '123', '123', 'ssss', 'sssss', 0, 0, 'sedang', 'uang_tunai', -0.025245714451433383, 109.33630228042604, 122.41, 'diterima', 'normal', NULL, '', 'admin_rumah_ibadah', '2026-06-09 11:28:22', '2026-06-09 11:28:29'),
(6, 4, 1, 'k', 'g', 'u', 'k', 'i', 5, 7, 'baik', 'zakat', -0.06864918884517689, 109.3591332435608, 218.94, 'dibatalkan', 'disetujui', 'h', 'h', 'admin_rumah_ibadah', '2026-06-09 11:57:59', '2026-06-09 12:19:58'),
(7, 9, 5, 'penerima03', '124854', '123', 'Gang Merak II, Mariana, Pontianak Kota, Pontianak, Kalimantan Barat, Kalimantan, 78112, Indonesia', 'Mahasiswa', 50000, 1, 'sedang', 'sembako', -0.02033240043514327, 109.33441936969759, 136.81, 'diterima', 'normal', NULL, '', 'admin_rumah_ibadah', '2026-06-09 14:52:40', '2026-06-09 14:53:12'),
(8, 10, 1, 'penerima04', '222223432', '13334341', 'Komplek Mitra Indah Utama I, Bangka Belitung Darat, Pontianak Tenggara, Pontianak, Kalimantan Barat, Kalimantan, 78124, Indonesia', 'wiraswasta', 1000000, 3, 'baik', 'sembako', -0.06962094072669782, 109.35720741748811, 299.36, 'diterima', 'normal', NULL, '', 'admin_rumah_ibadah', '2026-06-10 01:22:26', '2026-06-10 01:22:34'),
(9, 11, 1, 'penerima05', '111111111111111', '11111111111111111', 'Nilam Permai, Sungai Raya, Kubu Raya, Kalimantan Barat, Kalimantan, 78124, Indonesia', 'disabilitas', 0, 1, 'rusak', 'uang_tunai', -0.07270815709759908, 109.3613098561764, 299.07, 'diterima', 'normal', NULL, '', 'admin_rumah_ibadah', '2026-06-10 01:25:25', '2026-06-10 01:25:37');
-- --------------------------------------------------------
--
-- Table structure for table `penyaluran_bantuan`
--
CREATE TABLE `penyaluran_bantuan` (
`id` int NOT NULL,
`program_id` int DEFAULT NULL,
`penerima_id` int NOT NULL,
`rumah_ibadah_id` int NOT NULL,
`admin_id` int NOT NULL,
`jenis_bantuan` enum('zakat','sembako','uang_tunai','lainnya') COLLATE utf8mb4_unicode_ci NOT NULL,
`nominal` int DEFAULT NULL,
`jumlah_item` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`bantuan_ke` int DEFAULT NULL,
`keterangan` text COLLATE utf8mb4_unicode_ci,
`tanggal_penyaluran` date NOT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `penyaluran_bantuan`
--
INSERT INTO `penyaluran_bantuan` (`id`, `program_id`, `penerima_id`, `rumah_ibadah_id`, `admin_id`, `jenis_bantuan`, `nominal`, `jumlah_item`, `bantuan_ke`, `keterangan`, `tanggal_penyaluran`, `created_at`) VALUES
(1, 5, 5, 2, 5, 'zakat', 10000, NULL, 1, 'gg', '2026-06-09', '2026-06-09 11:33:36');
-- --------------------------------------------------------
--
-- Table structure for table `program_bantuan`
--
CREATE TABLE `program_bantuan` (
`id` int NOT NULL,
`rumah_ibadah_id` int DEFAULT NULL,
`nama_program` varchar(150) COLLATE utf8mb4_unicode_ci NOT NULL,
`jenis_bantuan` enum('zakat','sembako','uang_tunai','lainnya') COLLATE utf8mb4_unicode_ci NOT NULL,
`deskripsi` text COLLATE utf8mb4_unicode_ci,
`bulan` int NOT NULL,
`tahun` int NOT NULL,
`target_frekuensi` int DEFAULT '1',
`nominal` int DEFAULT NULL,
`jumlah_item` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status_program` enum('aktif','selesai','dibatalkan') COLLATE utf8mb4_unicode_ci DEFAULT 'aktif',
`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_unicode_ci;
--
-- Dumping data for table `program_bantuan`
--
INSERT INTO `program_bantuan` (`id`, `rumah_ibadah_id`, `nama_program`, `jenis_bantuan`, `deskripsi`, `bulan`, `tahun`, `target_frekuensi`, `nominal`, `jumlah_item`, `status_program`, `created_at`, `updated_at`) VALUES
(3, 2, 'a', 'uang_tunai', 'a', 6, 2026, 1, NULL, NULL, 'aktif', '2026-06-09 11:29:28', '2026-06-09 11:29:28'),
(4, 2, 'b', 'uang_tunai', 'b', 6, 2026, 1, NULL, NULL, 'aktif', '2026-06-09 11:29:47', '2026-06-09 11:29:47'),
(5, 2, 'c', 'sembako', 'c', 6, 2026, 1, NULL, NULL, 'aktif', '2026-06-09 11:30:54', '2026-06-09 11:30:54');
-- --------------------------------------------------------
--
-- Table structure for table `riwayat_status`
--
CREATE TABLE `riwayat_status` (
`id` int NOT NULL,
`penerima_id` int NOT NULL,
`user_id` int NOT NULL,
`status_lama` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status_baru` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`catatan` text COLLATE utf8mb4_unicode_ci,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Dumping data for table `riwayat_status`
--
INSERT INTO `riwayat_status` (`id`, `penerima_id`, `user_id`, `status_lama`, `status_baru`, `catatan`, `created_at`) VALUES
(1, 1, 4, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 10:10:05'),
(2, 1, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 10:48:16'),
(3, 2, 6, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 10:57:26'),
(4, 2, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 10:59:04'),
(7, 4, 5, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 11:27:12'),
(8, 5, 1, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 11:28:22'),
(9, 5, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 11:28:29'),
(10, 4, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 11:28:31'),
(11, 6, 1, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 11:57:59'),
(12, 6, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 11:58:09'),
(13, 6, 1, 'diterima', 'dibatalkan', 'jotok 2 rmh', '2026-06-09 11:58:22'),
(14, 6, 1, 'dibatalkan', 'hapus_disetujui', 'h', '2026-06-09 12:19:58'),
(15, 7, 1, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-09 14:52:40'),
(16, 7, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-09 14:52:49'),
(17, 8, 1, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-10 01:22:26'),
(18, 8, 1, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-10 01:22:34'),
(19, 9, 2, NULL, 'menunggu', 'Pengajuan penerima dibuat.', '2026-06-10 01:25:25'),
(20, 9, 2, 'menunggu', 'diterima', 'Status penerima diubah.', '2026-06-10 01:25:37');
-- --------------------------------------------------------
--
-- Table structure for table `rumah_ibadah`
--
CREATE TABLE `rumah_ibadah` (
`id` int NOT NULL,
`user_id` int DEFAULT NULL,
`nama_rumah_ibadah` varchar(150) COLLATE utf8mb4_unicode_ci NOT NULL,
`agama` enum('islam','kristen','katolik','hindu','buddha','konghucu') COLLATE utf8mb4_unicode_ci NOT NULL,
`alamat` text COLLATE utf8mb4_unicode_ci NOT NULL,
`nomor_hp` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
`latitude` double NOT NULL,
`longitude` double NOT NULL,
`status_pengajuan` enum('menunggu','diterima','ditolak') COLLATE utf8mb4_unicode_ci DEFAULT 'menunggu',
`status_hapus` enum('normal','menunggu','disetujui','ditolak') COLLATE utf8mb4_unicode_ci DEFAULT 'normal',
`alasan_hapus` text COLLATE utf8mb4_unicode_ci,
`keterangan` text COLLATE utf8mb4_unicode_ci,
`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_unicode_ci;
--
-- Dumping data for table `rumah_ibadah`
--
INSERT INTO `rumah_ibadah` (`id`, `user_id`, `nama_rumah_ibadah`, `agama`, `alamat`, `nomor_hp`, `latitude`, `longitude`, `status_pengajuan`, `status_hapus`, `alasan_hapus`, `keterangan`, `created_at`, `updated_at`) VALUES
(1, 2, 'Gereja Katolik Paroki St. Sesilia', 'katolik', 'Jl. Sungai Raya Dalam', '0812345678913', -0.07052077848657445, 109.3597447872162, 'diterima', 'normal', NULL, '', '2026-06-09 10:11:48', '2026-06-09 10:48:18'),
(2, 5, 'GPIB', 'kristen', 'Jl. Kartini, Pontianak', '0812345678913', -0.02508443513561694, 109.33739125728609, 'diterima', 'normal', NULL, '', '2026-06-09 10:54:13', '2026-06-09 10:59:11'),
(4, 8, 'Gereja Elim', 'kristen', 'Jalan Rajawali, Tengah, Pontianak Kota, Pontianak, Kalimantan Barat, Kalimantan, 78112, Indonesia', '0812345678913', -0.02152069970142388, 109.33472514152527, 'ditolak', 'disetujui', 's', 's', '2026-06-09 14:50:50', '2026-06-09 14:51:16'),
(5, 8, 'Gereja Elim', 'kristen', 'Jalan Rajawali, Tengah, Pontianak Kota, Pontianak, Kalimantan Barat, Kalimantan, 78112, Indonesia', '0812345678913', -0.02151988492191236, 109.33474123477937, 'diterima', 'normal', NULL, 'Ditambahkan langsung oleh super admin.', '2026-06-09 14:53:12', '2026-06-09 14:53:12');
-- --------------------------------------------------------
--
-- Table structure for table `users`
--
CREATE TABLE `users` (
`id` int NOT NULL,
`nama` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`username` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`role` enum('super_admin','admin_rumah_ibadah','penerima') COLLATE utf8mb4_unicode_ci NOT NULL,
`status_akun` enum('aktif','nonaktif') COLLATE utf8mb4_unicode_ci DEFAULT 'aktif',
`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_unicode_ci;
--
-- Dumping data for table `users`
--
INSERT INTO `users` (`id`, `nama`, `username`, `password`, `role`, `status_akun`, `created_at`, `updated_at`) VALUES
(1, 'Super Admin Dinas Sosial', 'superadmin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'super_admin', 'aktif', '2026-06-09 10:04:24', '2026-06-09 14:48:23'),
(2, 'admingerejasesilia', 'admingerejasesilia', '$2y$10$Pt4VTj7yDLt1xabM5WwGJuBo.gat807fkV0Q.KiGKv.V0tRbo1SHW', 'admin_rumah_ibadah', 'aktif', '2026-06-09 10:06:38', '2026-06-10 01:23:46'),
(4, 'penerima01', 'penerima01', '$2y$12$79Vc.m66x5uIIIt4rifL0OIY3qrzzXmgktTW3Y1QEwWB9GMaeLFni', 'penerima', 'nonaktif', '2026-06-09 10:08:28', '2026-06-10 01:18:35'),
(5, 'admingpib', 'admingpib', '$2y$12$79Vc.m66x5uIIIt4rifL0OIY3qrzzXmgktTW3Y1QEwWB9GMaeLFni', 'admin_rumah_ibadah', 'nonaktif', '2026-06-09 10:52:35', '2026-06-09 14:47:58'),
(6, 'penerima02', 'penerima02', '$2y$12$79Vc.m66x5uIIIt4rifL0OIY3qrzzXmgktTW3Y1QEwWB9GMaeLFni', 'penerima', 'aktif', '2026-06-09 10:55:15', '2026-06-09 14:47:48'),
(8, 'Admin Gereja Elim', 'admingerejaelim', '$2y$10$hiPcHEFAb2lPq3WKLra1MuZ8220s/UY.3PG24ZIrmbxrbp2imboGO', 'admin_rumah_ibadah', 'aktif', '2026-06-09 14:49:55', '2026-06-09 14:49:55'),
(9, 'penerima03', 'penerima03', '$2y$10$.50kERk9OSPvJhe.9IFxd.3qi2j5spTxtM36Kx1mI7Q3HkeVlyzvm', 'penerima', 'nonaktif', '2026-06-09 14:51:50', '2026-06-10 01:18:20'),
(10, 'penerima04', 'penerima04', '$2y$10$Q0yfgX7/KnCa4sxnz/f5YupK2aVyKWLDPO8CD/mdQlCSOTRbzLBli', 'penerima', 'aktif', '2026-06-10 01:20:04', '2026-06-10 01:20:04'),
(11, 'penerima05', 'penerima05', '$2y$10$0UJ97IYsDsS/K/qJbHJsiO938TFSU2AorU0fqxAawnbpwhJN/qBqi', 'penerima', 'aktif', '2026-06-10 01:21:01', '2026-06-10 01:21:01');
--
-- Indexes for dumped tables
--
--
-- Indexes for table `penerima_bantuan`
--
ALTER TABLE `penerima_bantuan`
ADD PRIMARY KEY (`id`),
ADD KEY `idx_pb_user` (`user_id`),
ADD KEY `idx_pb_ri` (`rumah_ibadah_id`),
ADD KEY `idx_pb_status` (`status_pengajuan`),
ADD KEY `idx_pb_jenis` (`jenis_bantuan`);
--
-- Indexes for table `penyaluran_bantuan`
--
ALTER TABLE `penyaluran_bantuan`
ADD PRIMARY KEY (`id`),
ADD KEY `idx_pen_program` (`program_id`),
ADD KEY `idx_pen_penerima` (`penerima_id`),
ADD KEY `idx_pen_ri` (`rumah_ibadah_id`),
ADD KEY `idx_pen_admin` (`admin_id`),
ADD KEY `idx_pen_tanggal` (`tanggal_penyaluran`);
--
-- Indexes for table `program_bantuan`
--
ALTER TABLE `program_bantuan`
ADD PRIMARY KEY (`id`),
ADD KEY `idx_prog_ri` (`rumah_ibadah_id`),
ADD KEY `idx_prog_periode` (`bulan`,`tahun`),
ADD KEY `idx_prog_status` (`status_program`);
--
-- Indexes for table `riwayat_status`
--
ALTER TABLE `riwayat_status`
ADD PRIMARY KEY (`id`),
ADD KEY `idx_rw_penerima` (`penerima_id`),
ADD KEY `idx_rw_user` (`user_id`);
--
-- Indexes for table `rumah_ibadah`
--
ALTER TABLE `rumah_ibadah`
ADD PRIMARY KEY (`id`),
ADD KEY `idx_ri_user` (`user_id`),
ADD KEY `idx_ri_status` (`status_pengajuan`),
ADD KEY `idx_ri_agama` (`agama`);
--
-- Indexes for table `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `username` (`username`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `penerima_bantuan`
--
ALTER TABLE `penerima_bantuan`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=10;
--
-- AUTO_INCREMENT for table `penyaluran_bantuan`
--
ALTER TABLE `penyaluran_bantuan`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT for table `program_bantuan`
--
ALTER TABLE `program_bantuan`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
--
-- AUTO_INCREMENT for table `riwayat_status`
--
ALTER TABLE `riwayat_status`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=21;
--
-- AUTO_INCREMENT for table `rumah_ibadah`
--
ALTER TABLE `rumah_ibadah`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
--
-- AUTO_INCREMENT for table `users`
--
ALTER TABLE `users`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=12;
--
-- Constraints for dumped tables
--
--
-- Constraints for table `penerima_bantuan`
--
ALTER TABLE `penerima_bantuan`
ADD CONSTRAINT `fk_pb_ri` FOREIGN KEY (`rumah_ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,
ADD CONSTRAINT `fk_pb_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
--
-- Constraints for table `penyaluran_bantuan`
--
ALTER TABLE `penyaluran_bantuan`
ADD CONSTRAINT `fk_pen_admin` FOREIGN KEY (`admin_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `fk_pen_penerima` FOREIGN KEY (`penerima_id`) REFERENCES `penerima_bantuan` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `fk_pen_program` FOREIGN KEY (`program_id`) REFERENCES `program_bantuan` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,
ADD CONSTRAINT `fk_pen_ri` FOREIGN KEY (`rumah_ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
--
-- Constraints for table `program_bantuan`
--
ALTER TABLE `program_bantuan`
ADD CONSTRAINT `fk_prog_ri` FOREIGN KEY (`rumah_ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
--
-- Constraints for table `riwayat_status`
--
ALTER TABLE `riwayat_status`
ADD CONSTRAINT `fk_rw_penerima` FOREIGN KEY (`penerima_id`) REFERENCES `penerima_bantuan` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `fk_rw_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
--
-- Constraints for table `rumah_ibadah`
--
ALTER TABLE `rumah_ibadah`
ADD CONSTRAINT `fk_ri_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
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 */;
SET FOREIGN_KEY_CHECKS=1;
+29
View File
@@ -0,0 +1,29 @@
services:
web:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
depends_on:
- db
ports:
- "80"
environment:
DB_HOST: db
DB_USER: root
DB_PASSWORD: webgisidolapass
db:
build:
context: ./db
dockerfile: Dockerfile
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: webgisidolapass
MYSQL_ROOT_HOST: "%"
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -pwebgisidolapass --silent"]
interval: 10s
timeout: 5s
retries: 10
+85
View File
@@ -0,0 +1,85 @@
<?php
$tasks = [
['id' => '01', 'title' => 'Tugas 01', 'subtitle' => 'Manajemen SPBU', 'desc' => 'CRUD titik SPBU dengan status ramai, jam operasional, buka/tutup, nomor HP, dan visualisasi Leaflet.', 'url' => '01/', 'badge' => 'SPBU'],
['id' => '02', 'title' => 'Tugas 02', 'subtitle' => 'SPBU + Polyline + Polygon', 'desc' => 'Pemetaan SPBU, garis jalan, dan area polygon dengan penyimpanan data MySQL.', 'url' => '02/', 'badge' => 'Layer'],
['id' => '03', 'title' => 'Tugas 03', 'subtitle' => 'Manajemen Jalan & Parsil Tanah', 'desc' => 'WebGIS untuk data jalan dan parsil tanah menggunakan Leaflet dan database geospasial sederhana.', 'url' => '03/', 'badge' => 'Jalan'],
['id' => '04', 'title' => 'Tugas 04', 'subtitle' => 'SPBU dengan Layer Groups & Layer Control', 'desc' => 'Pengelompokan layer, kontrol tampilan peta, serta manajemen titik SPBU berbasis PHP native.', 'url' => '04/', 'badge' => 'Control'],
['id' => '05', 'title' => 'Tugas 05', 'subtitle' => 'WebGIS Poverty Mapping', 'desc' => 'Aplikasi bantuan/zakat berbasis lokasi dengan role super admin, admin rumah ibadah, dan penerima.', 'url' => '05/', 'badge' => 'Poverty']
];
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D1041231091 - Kumpulan Tugas WebGIS</title>
<style>
:root{
--bg1:#062f2f; --bg2:#071827; --card:#ffffff; --text:#0f172a;
--muted:#64748b; --green:#0f766e; --green2:#14b8a6; --line:#dbe7e6;
}
*{box-sizing:border-box}
body{margin:0;font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:radial-gradient(circle at top left,#0f766e 0,#0d3d43 36%,#08111f 100%);color:#fff;min-height:100vh}
.wrap{max-width:1180px;margin:0 auto;padding:36px 20px 42px}
.hero{display:grid;grid-template-columns:1.25fr .75fr;gap:24px;align-items:stretch;margin-bottom:24px}
.panel{border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.10);box-shadow:0 18px 60px rgba(0,0,0,.22);backdrop-filter:blur(14px);border-radius:28px;padding:30px}
.eyebrow{display:inline-flex;align-items:center;gap:8px;padding:8px 12px;border-radius:999px;background:rgba(20,184,166,.18);border:1px solid rgba(94,234,212,.3);font-weight:700;font-size:13px;color:#ccfbf1;margin-bottom:16px}
h1{font-size:clamp(30px,5vw,58px);line-height:1.02;margin:0 0 16px;letter-spacing:-.04em}
.lead{font-size:18px;line-height:1.7;color:#d1fae5;margin:0;max-width:720px}
.identity{display:grid;gap:12px;height:100%}
.info{border-radius:20px;background:rgba(255,255,255,.92);color:var(--text);padding:20px;border:1px solid rgba(255,255,255,.5)}
.info strong{display:block;font-size:13px;color:var(--muted);margin-bottom:5px;text-transform:uppercase;letter-spacing:.08em}
.info span{font-size:18px;font-weight:800}
.grid{display:grid;grid-template-columns:repeat(5,1fr);gap:16px}
.card{position:relative;display:flex;min-height:260px;flex-direction:column;justify-content:space-between;text-decoration:none;color:var(--text);background:linear-gradient(180deg,#fff,#f8fffe);border:1px solid var(--line);border-radius:24px;padding:22px;overflow:hidden;box-shadow:0 14px 34px rgba(2,12,27,.18);transition:.2s ease}
.card:before{content:"";position:absolute;inset:0;background:radial-gradient(circle at top right,rgba(20,184,166,.18),transparent 36%);pointer-events:none}
.card:hover{transform:translateY(-6px);box-shadow:0 24px 42px rgba(2,12,27,.28)}
.top{position:relative;display:flex;justify-content:space-between;gap:12px;align-items:flex-start}
.num{width:48px;height:48px;border-radius:16px;background:#0f766e;color:#fff;display:grid;place-items:center;font-weight:900;font-size:20px}
.badge{font-size:12px;font-weight:800;color:#0f766e;background:#ccfbf1;border:1px solid #99f6e4;border-radius:999px;padding:6px 10px}
.card h2{position:relative;font-size:22px;line-height:1.1;margin:24px 0 8px;letter-spacing:-.02em}
.subtitle{position:relative;font-weight:800;color:#0f766e;margin:0 0 10px}
.desc{position:relative;color:var(--muted);line-height:1.55;margin:0;font-size:14px}
.open{position:relative;margin-top:22px;font-weight:900;color:#0f172a;display:flex;align-items:center;gap:8px}
.footer{margin-top:24px;text-align:center;color:#d1fae5;font-size:14px}
.login-note{margin-top:18px;padding:14px 16px;border-radius:18px;background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.16);color:#ecfeff;font-size:14px}
@media(max-width:980px){.hero{grid-template-columns:1fr}.grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:620px){.wrap{padding:22px 14px}.panel{padding:22px;border-radius:22px}.grid{grid-template-columns:1fr}.card{min-height:220px}}
</style>
</head>
<body>
<main class="wrap">
<section class="hero">
<div class="panel">
<div class="eyebrow">🗺️ Kumpulan Deploy WebGIS</div>
<h1>D1041231091<br>Requestha Amorine Montass</h1>
<p class="lead">Landing page untuk mengakses seluruh tugas WebGIS 01 sampai 05 dalam satu deployment Docker. Setiap kartu di bawah membuka aplikasi sesuai folder tugas.</p>
<div class="login-note">Akun awal Tugas 05: <b>superadmin</b> / <b>password</b></div>
</div>
<div class="identity">
<div class="info"><strong>Mahasiswa</strong><span>Requestha Amorine Montass</span></div>
<div class="info"><strong>NIM</strong><span>D1041231091</span></div>
<div class="info"><strong>Teknologi</strong><span>PHP Native, MySQL, Leaflet.js, Docker</span></div>
</div>
</section>
<section class="grid">
<?php foreach ($tasks as $task): ?>
<a class="card" href="<?= htmlspecialchars($task['url']) ?>">
<div>
<div class="top">
<div class="num"><?= htmlspecialchars($task['id']) ?></div>
<div class="badge"><?= htmlspecialchars($task['badge']) ?></div>
</div>
<h2><?= htmlspecialchars($task['title']) ?></h2>
<p class="subtitle"><?= htmlspecialchars($task['subtitle']) ?></p>
<p class="desc"><?= htmlspecialchars($task['desc']) ?></p>
</div>
<div class="open">Buka aplikasi <span>→</span></div>
</a>
<?php endforeach; ?>
</section>
<div class="footer">Satu repository, satu Docker Compose, satu domain untuk semua tugas.</div>
</main>
</body>
</html>