Implement reverse geocoding for Rumah Miskin

This commit is contained in:
GuavaPopper
2026-04-28 10:08:31 +07:00
commit 7759b83638
15 changed files with 1363 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
DB_HOST=127.0.0.1
DB_NAME=webgis_miskin
DB_USER=root
DB_PASS=
+46
View File
@@ -0,0 +1,46 @@
<?php
// ── SHARED DATABASE CONFIGURATION ───────────────────────────────────────────
// Digunakan oleh semua endpoint API. File .env berada di root project.
$envFile = __DIR__ . '/../.env';
if (!file_exists($envFile)) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => 'File .env tidak ditemukan']);
exit();
}
$env = [];
foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
if (strpos(trim($line), '#') === 0 || !strpos($line, '=')) continue;
list($key, $value) = explode('=', $line, 2);
$env[trim($key)] = trim($value);
}
$host = $env['DB_HOST'] ?? 'localhost';
$dbname = $env['DB_NAME'] ?? 'webgis_miskin';
$username = $env['DB_USER'] ?? 'root';
$password = $env['DB_PASS'] ?? '';
// ── KONEKSI PDO ───────────────────────────────────────────────────────────────
try {
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
// ── SCHEMA MIGRATION ───────────────────────────────────────────────────────
// Pastikan kolom 'alamat' ada di tabel 'rumah_miskin'
try {
$pdo->exec("ALTER TABLE rumah_miskin ADD COLUMN alamat TEXT AFTER id_rumah");
} catch (PDOException $e) {
// Kolom mungkin sudah ada, abaikan error 1060 (Duplicate column name)
}
} catch (PDOException $e) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => 'Koneksi database gagal: ' . $e->getMessage()]);
exit();
}
+24
View File
@@ -0,0 +1,24 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['id'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'ID tidak ditemukan']);
exit();
}
try {
$stmt = $pdo->prepare("DELETE FROM rumah_ibadah WHERE id = ?");
$stmt->execute([$input['id']]);
echo json_encode([
'success' => true,
'message' => 'Data Rumah Ibadah berhasil dihapus'
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+24
View File
@@ -0,0 +1,24 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['id_rumah'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'ID Rumah tidak ditemukan']);
exit();
}
try {
$stmt = $pdo->prepare("DELETE FROM rumah_miskin WHERE id_rumah = ?");
$stmt->execute([$input['id_rumah']]);
echo json_encode([
'success' => true,
'message' => 'Data Rumah Miskin berhasil dihapus'
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+12
View File
@@ -0,0 +1,12 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
try {
$stmt = $pdo->query("SELECT * FROM rumah_ibadah ORDER BY created_at DESC");
$data = $stmt->fetchAll();
echo json_encode($data);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+12
View File
@@ -0,0 +1,12 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
try {
$stmt = $pdo->query("SELECT * FROM rumah_miskin ORDER BY created_at DESC");
$data = $stmt->fetchAll();
echo json_encode($data);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+31
View File
@@ -0,0 +1,31 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['nama']) || empty($input['latitude']) || empty($input['longitude'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Data tidak lengkap']);
exit();
}
try {
$stmt = $pdo->prepare("INSERT INTO rumah_ibadah (nama, alamat, latitude, longitude, radius) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([
$input['nama'],
$input['alamat'] ?? '',
$input['latitude'],
$input['longitude'],
$input['radius'] ?? 500
]);
echo json_encode([
'success' => true,
'id' => $pdo->lastInsertId(),
'message' => 'Data Rumah Ibadah berhasil disimpan'
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+40
View File
@@ -0,0 +1,40 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['id_rumah']) || empty($input['latitude']) || empty($input['longitude'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Data tidak lengkap']);
exit();
}
try {
// Check if ID exists
$check = $pdo->prepare("SELECT id_rumah FROM rumah_miskin WHERE id_rumah = ?");
$check->execute([$input['id_rumah']]);
if ($check->rowCount() > 0) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'ID Rumah sudah terdaftar']);
exit();
}
$stmt = $pdo->prepare("INSERT INTO rumah_miskin (id_rumah, alamat, jumlah_kk, jumlah_orang, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([
$input['id_rumah'],
$input['alamat'] ?? '',
$input['jumlah_kk'] ?? 1,
$input['jumlah_orang'] ?? 1,
$input['latitude'],
$input['longitude']
]);
echo json_encode([
'success' => true,
'message' => 'Data Rumah Miskin berhasil disimpan'
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+31
View File
@@ -0,0 +1,31 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['id']) || empty($input['nama'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Data tidak lengkap']);
exit();
}
try {
$stmt = $pdo->prepare("UPDATE rumah_ibadah SET nama = ?, alamat = ?, latitude = ?, longitude = ?, radius = ? WHERE id = ?");
$stmt->execute([
$input['nama'],
$input['alamat'] ?? '',
(float)$input['latitude'],
(float)$input['longitude'],
$input['radius'] ?? 500,
$input['id']
]);
echo json_encode([
'success' => true,
'message' => 'Data Rumah Ibadah berhasil diperbarui'
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+31
View File
@@ -0,0 +1,31 @@
<?php
header('Content-Type: application/json');
require_once 'config.php';
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['id_rumah'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Data tidak lengkap']);
exit();
}
try {
$stmt = $pdo->prepare("UPDATE rumah_miskin SET alamat = ?, jumlah_kk = ?, jumlah_orang = ?, latitude = ?, longitude = ? WHERE id_rumah = ?");
$stmt->execute([
$input['alamat'] ?? '',
$input['jumlah_kk'] ?? 1,
$input['jumlah_orang'] ?? 1,
(float)$input['latitude'],
(float)$input['longitude'],
$input['id_rumah']
]);
echo json_encode([
'success' => true,
'message' => 'Data Rumah Miskin berhasil diperbarui'
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+69
View File
@@ -0,0 +1,69 @@
<?php
// Function to parse minimal .env file
function parseEnv($filePath) {
if (!file_exists($filePath)) return [];
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$env = [];
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
list($name, $value) = explode('=', $line, 2) + [NULL, NULL];
if ($name !== NULL) {
$env[trim($name)] = trim($value);
putenv(trim($name) . "=" . trim($value));
}
}
return $env;
}
$env = parseEnv(__DIR__ . '/.env');
$host = getenv('DB_HOST') ?: '127.0.0.1';
$dbname = getenv('DB_NAME') ?: 'webgis_miskin';
$user = getenv('DB_USER') ?: 'root';
$pass = getenv('DB_PASS') ?: '';
try {
// First connect without specifying a database to create it if it doesn't exist
$pdo_init = new PDO("mysql:host=$host", $user, $pass);
$pdo_init->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo_init->exec("CREATE DATABASE IF NOT EXISTS `$dbname`");
// Now connect to the specific database
$pdo = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Create tables if not exists
$pdo->exec("
CREATE TABLE IF NOT EXISTS rumah_ibadah (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(255) NOT NULL,
alamat TEXT NOT NULL,
latitude DECIMAL(10, 8) NOT NULL,
longitude DECIMAL(11, 8) NOT NULL,
radius INT DEFAULT 500,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
");
$pdo->exec("
CREATE TABLE IF NOT EXISTS rumah_miskin (
id_rumah VARCHAR(50) PRIMARY KEY,
alamat TEXT,
jumlah_kk INT NOT NULL DEFAULT 1,
jumlah_orang INT NOT NULL DEFAULT 1,
latitude DECIMAL(10, 8) NOT NULL,
longitude DECIMAL(11, 8) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
");
// Ensure alamat column exists if table was already created
try {
$pdo->exec("ALTER TABLE rumah_miskin ADD COLUMN alamat TEXT AFTER id_rumah");
} catch (PDOException $e) {
// Column might already exist, ignore error
}
} catch(PDOException $e) {
die("Database Connection failed: " . $e->getMessage());
}
?>
+164
View File
@@ -0,0 +1,164 @@
<!DOCTYPE html>
<html lang="id" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebGIS Miskin & Ibadah</title>
<!-- Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- DaisyUI + Tailwind -->
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<!-- Plus Jakarta Sans Font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
<style>
* {
font-family: 'Plus Jakarta Sans', sans-serif;
}
body {
font-weight: 500;
}
h1, h2, h3, h4, h5, h6, .btn, .card-title, .stat-value {
font-weight: 700 !important;
letter-spacing: -0.02em;
}
#bgMap {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
opacity: 0.5;
filter: saturate(1.2) brightness(0.7);
}
.hero {
position: relative;
z-index: 10;
background: transparent !important;
}
/* Glass effect for content */
.hero-content {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
padding: 3rem;
border-radius: 2rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.3);
}
[data-theme="dark"] .hero-content {
background: rgba(15, 23, 42, 0.7);
border: 1px solid rgba(255, 255, 255, 0.1);
}
</style>
</head>
<body class="min-h-screen bg-base-200 flex flex-col items-center justify-center overflow-hidden">
<!-- Background Map -->
<div id="bgMap"></div>
<div class="hero min-h-screen">
<div class="hero-content text-center flex-col gap-2">
<!-- Logo -->
<div class="text-7xl mb-2">🏘️</div>
<h1 class="text-4xl font-bold tracking-tight text-primary">WebGIS Miskin & Ibadah</h1>
<p class="max-w-md text-base-content/60 mt-2 mb-6 leading-relaxed">
Sistem Informasi Geografis untuk pemetaan rumah ibadah dan
analisis sebaran keluarga kurang mampu.
</p>
<!-- Buttons -->
<div class="flex flex-col sm:flex-row gap-3">
<a href="pages/map.html" class="btn btn-primary btn-lg gap-2">
<i class="fa-solid fa-map-location-dot"></i>
Buka Peta & Input
</a>
<a href="pages/data.html" class="btn btn-outline btn-lg gap-2">
<i class="fa-solid fa-table-list"></i>
Lihat Data Tabular
</a>
</div>
<!-- Redirect progress -->
<div class="mt-8 w-72">
<p class="text-xs text-base-content/40 mb-2">Mengarahkan ke halaman Peta dalam 5 detik...</p>
<progress class="progress progress-primary w-full" id="redirectProgress" value="0" max="100"></progress>
</div>
</div>
</div>
<script>
// Progress bar logic
let val = 0;
const bar = document.getElementById('redirectProgress');
const tick = setInterval(() => {
val += 100 / 50;
bar.value = val;
if (val >= 100) clearInterval(tick);
}, 100);
setTimeout(() => { window.location.href = 'pages/map.html'; }, 5000);
// Background Map logic
const map = L.map('bgMap', {
zoomControl: false,
attributionControl: false,
dragging: false,
scrollWheelZoom: false,
doubleClickZoom: false
}).setView([-0.0553, 109.3463], 16);
// Satellite Hybrid (Imagery + Labels)
L.layerGroup([
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 19
}),
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 19
}),
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Transportation/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 19
})
]).addTo(map);
// Fetch markers to show on background
Promise.all([
fetch('api/get_ibadah.php').then(r => r.json()),
fetch('api/get_miskin.php').then(r => r.json())
]).then(([ibadah, miskin]) => {
const markers = [];
ibadah.forEach(item => {
L.circleMarker([+item.latitude, +item.longitude], {
radius: 7, fillColor: "#22c55e", color: "#fff", weight: 2, opacity: 1, fillOpacity: 0.8
}).addTo(map);
markers.push([+item.latitude, +item.longitude]);
});
miskin.forEach(item => {
L.circleMarker([+item.latitude, +item.longitude], {
radius: 5, fillColor: "#ef4444", color: "#fff", weight: 1, opacity: 1, fillOpacity: 0.7
}).addTo(map);
markers.push([+item.latitude, +item.longitude]);
});
if (markers.length) {
map.fitBounds(markers, { padding: [50, 50] });
}
// Subtle animation: slow pan
let lat = map.getCenter().lat;
setInterval(() => {
lat += 0.000005;
map.panTo([lat, map.getCenter().lng], { animate: true, duration: 1 });
}, 1000);
}).catch(() => {});
</script>
</body>
</html>
BIN
View File
Binary file not shown.
+213
View File
@@ -0,0 +1,213 @@
<!DOCTYPE html>
<html lang="id" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Data Tabular - WebGIS Miskin & Ibadah</title>
<!-- DaisyUI + Tailwind -->
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<!-- Plus Jakarta Sans Font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
<style>
* { font-family: 'Plus Jakarta Sans', sans-serif; }
body { font-weight: 500; background-color: #f8fafc; }
h1, h2, h3, h4, h5, h6, .btn, .card-title, .stat-value { font-weight: 700 !important; letter-spacing: -0.01em; }
</style>
</head>
<body class="min-h-screen">
<!-- ═══ NAVBAR ═══════════════════════════════════════════════════════════ -->
<div class="navbar bg-base-100 border-b border-base-200 shadow-sm sticky top-0 z-[1000]">
<div class="flex-1">
<a href="../index.html" class="btn btn-ghost text-lg font-bold gap-2 text-primary">
<i class="fa-solid fa-house-chimney-crack"></i>
WebGIS Miskin & Ibadah
</a>
</div>
<div class="flex-none">
<ul class="menu menu-horizontal px-1 gap-1">
<li><a href="map.html" class="font-medium"><i class="fa-solid fa-map"></i> Peta</a></li>
<li><a href="data.html" class="font-semibold bg-primary/10 text-primary rounded-lg"><i class="fa-solid fa-table-list"></i> Data Tabular</a></li>
</ul>
</div>
</div>
<!-- ═══ MAIN CONTENT ════════════════════════════════════════════════════ -->
<main class="max-w-screen-xl mx-auto px-4 py-8">
<!-- Header -->
<div class="flex flex-wrap items-center justify-between gap-4 mb-8">
<div>
<h1 class="text-3xl font-bold flex items-center gap-3">
<i class="fa-solid fa-database text-primary"></i>
Manajemen Data
</h1>
<p class="text-base-content/50 mt-1">Daftar rumah ibadah dan keluarga miskin dalam matriks tabular.</p>
</div>
<div class="flex gap-2">
<a href="map.html" class="btn btn-primary shadow-lg"><i class="fa-solid fa-plus"></i> Tambah Data di Peta</a>
</div>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="stat bg-white shadow rounded-2xl border border-base-200">
<div class="stat-figure text-success text-3xl"><i class="fa-solid fa-mosque"></i></div>
<div class="stat-title text-xs uppercase font-bold opacity-50">Total Rumah Ibadah</div>
<div class="stat-value text-success" id="countIbadah">-</div>
</div>
<div class="stat bg-white shadow rounded-2xl border border-base-200">
<div class="stat-figure text-error text-3xl"><i class="fa-solid fa-house-user"></i></div>
<div class="stat-title text-xs uppercase font-bold opacity-50">Total Rumah Miskin</div>
<div class="stat-value text-error" id="countMiskin">-</div>
</div>
<div class="stat bg-white shadow rounded-2xl border border-base-200">
<div class="stat-figure text-warning text-3xl"><i class="fa-solid fa-users"></i></div>
<div class="stat-title text-xs uppercase font-bold opacity-50">Total Jiwa Terdata</div>
<div class="stat-value text-warning" id="countJiwa">-</div>
</div>
</div>
<!-- Tabs -->
<div role="tablist" class="tabs tabs-lifted tabs-lg mb-6">
<input type="radio" name="data_tabs" role="tab" class="tab font-bold" aria-label="🕌 Rumah Ibadah" checked />
<div role="tabpanel" class="tab-content bg-base-100 border-base-300 rounded-box p-6 shadow-sm">
<div class="overflow-x-auto">
<table class="table table-zebra w-full" id="tableIbadah">
<thead>
<tr class="text-xs font-bold uppercase opacity-50">
<th>#</th>
<th>Nama Rumah Ibadah</th>
<th>Alamat</th>
<th>Radius Analisis</th>
<th>Koordinat</th>
<th class="text-center">Aksi</th>
</tr>
</thead>
<tbody id="bodyIbadah"></tbody>
</table>
</div>
</div>
<input type="radio" name="data_tabs" role="tab" class="tab font-bold" aria-label="🏠 Rumah Miskin" />
<div role="tabpanel" class="tab-content bg-base-100 border-base-300 rounded-box p-6 shadow-sm">
<div class="overflow-x-auto">
<table class="table table-zebra w-full" id="tableMiskin">
<thead>
<tr class="text-xs font-bold uppercase opacity-50">
<th>ID Rumah</th>
<th>Alamat</th>
<th>Jumlah KK</th>
<th>Jumlah Jiwa</th>
<th>Koordinat</th>
<th class="text-center">Aksi</th>
</tr>
</thead>
<tbody id="bodyMiskin"></tbody>
</table>
</div>
</div>
</div>
</main>
<!-- Modal: Confirm Delete -->
<dialog id="deleteModal" class="modal">
<div class="modal-box max-w-sm text-center">
<div class="text-5xl mb-4 text-error"><i class="fa-solid fa-triangle-exclamation"></i></div>
<h3 class="font-bold text-xl mb-2">Hapus Item?</h3>
<p class="text-sm text-base-content/60 mb-6">Penghapusan tidak dapat dibatalkan.</p>
<div class="flex gap-2 justify-center">
<button class="btn btn-ghost px-8" onclick="deleteModal.close()">Batal</button>
<button class="btn btn-error px-8 text-white" id="confirmDeleteBtn">Konfirmasi Hapus</button>
</div>
</div>
</dialog>
<!-- Toast Container -->
<div class="toast toast-end toast-bottom z-[9999]" id="toastBox"></div>
<script>
document.addEventListener('DOMContentLoaded', loadAllData);
async function loadAllData() {
try {
const [ri, rm] = await Promise.all([
fetch('../api/get_ibadah.php').then(r => r.json()),
fetch('../api/get_miskin.php').then(r => r.json())
]);
// Stats
document.getElementById('countIbadah').innerText = ri.length;
document.getElementById('countMiskin').innerText = rm.length;
document.getElementById('countJiwa').innerText = rm.reduce((acc, h) => acc + parseInt(h.jumlah_orang), 0);
// Ibadah Table
const bodyI = document.getElementById('bodyIbadah');
bodyI.innerHTML = ri.length ? ri.map((item, i) => `
<tr>
<td class="text-xs font-bold opacity-30">${i + 1}</td>
<td class="font-bold text-primary">${item.nama}</td>
<td class="text-sm opacity-70 max-w-xs truncate">${item.alamat}</td>
<td><span class="badge badge-outline badge-sm">${item.radius}m</span></td>
<td class="font-mono text-[10px]">${parseFloat(item.latitude).toFixed(6)}, ${parseFloat(item.longitude).toFixed(6)}</td>
<td class="text-center">
<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('ibadah', ${item.id})"><i class="fa-solid fa-trash"></i></button>
</td>
</tr>
`).join('') : '<tr><td colspan="6" class="text-center py-10 opacity-30 italic">Belum ada data</td></tr>';
// Miskin Table
const bodyM = document.getElementById('bodyMiskin');
bodyM.innerHTML = rm.length ? rm.map(item => `
<tr>
<td class="font-bold text-error">${item.id_rumah}</td>
<td class="text-xs opacity-70 max-w-xs truncate">${item.alamat || '-'}</td>
<td><span class="badge badge-warning badge-sm">${item.jumlah_kk} KK</span></td>
<td><span class="badge badge-info badge-sm">${item.jumlah_orang} Jiwa</span></td>
<td class="font-mono text-[10px]">${parseFloat(item.latitude).toFixed(6)}, ${parseFloat(item.longitude).toFixed(6)}</td>
<td class="text-center">
<button class="btn btn-xs btn-error btn-outline" onclick="prepDelete('miskin', '${item.id_rumah}')"><i class="fa-solid fa-trash"></i></button>
</td>
</tr>
`).join('') : '<tr><td colspan="6" class="text-center py-10 opacity-30 italic">Belum ada data</td></tr>';
} catch (e) { showToast('Gagal memuat data', 'error'); }
}
let deleteTarget = null;
function prepDelete(type, id) {
deleteTarget = { type, id };
deleteModal.showModal();
}
document.getElementById('confirmDeleteBtn').onclick = async () => {
const { type, id } = deleteTarget;
const res = await fetch(`../api/delete_${type}.php`, {
method: 'POST', body: JSON.stringify(type === 'ibadah' ? { id } : { id_rumah: id })
});
const resJson = await res.json();
if (resJson.success) {
showToast('Data dihapus', 'success');
deleteModal.close();
loadAllData();
} else showToast('Gagal menghapus', 'error');
};
function showToast(msg, type = 'info') {
const t = document.createElement('div');
const alertCls = type === 'success' ? 'alert-success' : type === 'error' ? 'alert-error' : 'alert-info';
t.className = `alert ${alertCls} shadow-lg py-3 text-xs font-bold`;
t.innerHTML = `<span>${msg}</span>`;
document.getElementById('toastBox').appendChild(t);
setTimeout(() => t.remove(), 3000);
}
</script>
</body>
</html>
+662
View File
@@ -0,0 +1,662 @@
<!DOCTYPE html>
<html lang="id" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Peta Interaktif - WebGIS Miskin & Ibadah</title>
<!-- Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Turf.js -->
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
<!-- DaisyUI + Tailwind -->
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<!-- Plus Jakarta Sans Font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
<style>
* { font-family: 'Plus Jakarta Sans', sans-serif; }
body { font-weight: 500; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
h1, h2, h3, h4, h5, h6, .btn, .card-title, .stat-value { font-weight: 700 !important; letter-spacing: -0.01em; }
.map-wrapper { flex: 1; position: relative; min-height: 0; }
#map { width: 100%; height: 100%; }
/* Floating Controls */
.floating-panel {
position: absolute;
top: 20px;
left: 20px;
z-index: 1000;
width: 300px;
max-height: calc(100vh - 100px);
overflow-y: auto;
}
.glass-card {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
}
/* Marker dragging hint */
#dragHint, #mapHint {
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
}
/* Custom Layer Control */
.leaflet-control-layers {
border-radius: 12px !important;
border: none !important;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15) !important;
backdrop-filter: blur(8px);
background: rgba(255, 255, 255, 0.85) !important;
padding: 8px 12px !important;
}
</style>
</head>
<body>
<!-- ═══ NAVBAR ═══════════════════════════════════════════════════════════ -->
<div class="navbar bg-base-100 border-b border-base-200 shadow-sm flex-shrink-0 z-[1001]">
<div class="flex-1">
<a href="../index.html" class="btn btn-ghost text-lg font-bold gap-2 text-primary">
<i class="fa-solid fa-house-chimney-crack"></i>
WebGIS Miskin & Ibadah
</a>
</div>
<div class="flex-none">
<ul class="menu menu-horizontal px-1 gap-1">
<li>
<a href="map.html" class="font-semibold bg-primary/10 text-primary rounded-lg">
<i class="fa-solid fa-map"></i> Peta Interaktif
</a>
</li>
<li>
<a href="data.html" class="font-medium">
<i class="fa-solid fa-table-list"></i> Data Tabular
</a>
</li>
</ul>
</div>
</div>
<!-- ═══ MAP CONTAINER ════════════════════════════════════════════════════ -->
<div class="map-wrapper">
<div id="map"></div>
<!-- Floating Control Panel -->
<div class="floating-panel flex flex-col gap-3">
<!-- Mode Selector -->
<div class="card glass-card p-4">
<h3 class="text-sm font-bold mb-3 flex items-center gap-2">
<i class="fa-solid fa-layer-group text-primary"></i>
Mode Operasi
</h3>
<div class="grid grid-cols-1 gap-2">
<button class="btn btn-sm btn-outline btn-primary justify-start gap-2 active" id="btnModeView" onclick="setMode('view')">
<i class="fa-solid fa-eye"></i> View & Analisis
</button>
<button class="btn btn-sm btn-outline btn-success justify-start gap-2" id="btnModeIbadah" onclick="setMode('ibadah')">
<i class="fa-solid fa-plus"></i> Tambah Rumah Ibadah
</button>
<button class="btn btn-sm btn-outline btn-error justify-start gap-2" id="btnModeMiskin" onclick="setMode('miskin')">
<i class="fa-solid fa-plus"></i> Tambah Rumah Miskin
</button>
</div>
</div>
<!-- Analysis Panel (Conditional) -->
<div id="analysisPanel" class="card glass-card p-4 hidden">
<h3 class="text-sm font-bold mb-2 flex items-center gap-2">
<i class="fa-solid fa-chart-pie text-secondary"></i>
Statistik Radius
</h3>
<div id="noSelectionMsg" class="text-xs text-base-content/50 italic py-2 text-center">
Klik Rumah Ibadah pada peta <br> untuk memulai analisis.
</div>
<div id="statsContent" class="hidden">
<div class="bg-base-200/50 rounded-lg p-3 mb-3 border border-base-300">
<div class="text-xs font-bold text-primary truncate" id="activeName">Nama Ibadah</div>
<div class="text-[10px] text-base-content/40" id="activeAddr">Alamat</div>
</div>
<div class="form-control mb-4">
<label class="label py-1">
<span class="label-text-alt font-bold">Radius: <span id="radiusVal">500</span>m</span>
</label>
<input type="range" min="50" max="2000" value="500" step="50" class="range range-xs range-primary" id="radiusSlider">
</div>
<div class="grid grid-cols-3 gap-2">
<div class="text-center p-2 bg-white rounded-lg border border-base-200 shadow-sm">
<div class="text-lg font-bold text-error" id="statRumah">0</div>
<div class="text-[8px] uppercase tracking-wider font-bold opacity-50">Rumah</div>
</div>
<div class="text-center p-2 bg-white rounded-lg border border-base-200 shadow-sm">
<div class="text-lg font-bold text-warning" id="statKk">0</div>
<div class="text-[8px] uppercase tracking-wider font-bold opacity-50">KK</div>
</div>
<div class="text-center p-2 bg-white rounded-lg border border-base-200 shadow-sm">
<div class="text-lg font-bold text-info" id="statJiwa">0</div>
<div class="text-[8px] uppercase tracking-wider font-bold opacity-50">Jiwa</div>
</div>
</div>
</div>
</div>
</div>
<!-- Hints -->
<div id="mapHint" class="badge badge-primary badge-lg py-4 px-6 gap-2 shadow-xl hidden">
<i class="fa-solid fa-mouse-pointer"></i>
<span>Klik peta untuk menentukan lokasi baru</span>
</div>
<div id="dragHint" class="badge badge-warning badge-lg py-4 px-6 gap-2 shadow-xl hidden">
<i class="fa-solid fa-arrows-up-down-left-right"></i>
<span>Lepaskan untuk menyimpan lokasi baru</span>
</div>
</div>
<!-- ═══ MODALS ═══════════════════════════════════════════════════════════ -->
<!-- Modal: Input Ibadah -->
<dialog id="modalIbadah" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg flex items-center gap-2 mb-4">
<i class="fa-solid fa-mosque text-success"></i> Data Rumah Ibadah
</h3>
<div class="form-control gap-3">
<div class="bg-base-200 p-2 rounded text-xs font-mono" id="ib_coords">Lat: -, Lng: -</div>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Nama Rumah Ibadah</span></div>
<input type="text" id="ib_nama" placeholder="Contoh: Masjid Nurul Iman" class="input input-bordered w-full" />
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Alamat</span></div>
<textarea id="ib_alamat" class="textarea textarea-bordered h-20" placeholder="Alamat lengkap..."></textarea>
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Radius Default Analysis (Meter)</span></div>
<input type="number" id="ib_radius" value="500" class="input input-bordered w-full" />
</label>
</div>
<div class="modal-action">
<button class="btn btn-ghost" onclick="modalIbadah.close()">Batal</button>
<button class="btn btn-success" id="btnSaveIbadah" onclick="saveIbadah()">Simpan</button>
</div>
</div>
</dialog>
<!-- Modal: Input Miskin -->
<dialog id="modalMiskin" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg flex items-center gap-2 mb-4">
<i class="fa-solid fa-home text-error"></i> Data Rumah Miskin
</h3>
<div class="form-control gap-3">
<div class="bg-base-200 p-2 rounded text-xs font-mono" id="rm_coords">Lat: -, Lng: -</div>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Alamat</span></div>
<textarea id="rm_alamat" class="textarea textarea-bordered h-20" placeholder="Alamat lengkap..."></textarea>
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">ID Rumah / Nama Pemilik</span></div>
<input type="text" id="rm_id" placeholder="Contoh: RM-001" class="input input-bordered w-full" />
</label>
<div class="grid grid-cols-2 gap-3">
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Jumlah KK</span></div>
<input type="number" id="rm_kk" value="1" class="input input-bordered w-full" />
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Jumlah Jiwa</span></div>
<input type="number" id="rm_jiwa" value="4" class="input input-bordered w-full" />
</label>
</div>
</div>
<div class="modal-action">
<button class="btn btn-ghost" onclick="modalMiskin.close()">Batal</button>
<button class="btn btn-error" id="btnSaveMiskin" onclick="saveMiskin()">Simpan</button>
</div>
</div>
</dialog>
<!-- Modal: Confirm Delete -->
<dialog id="deleteModal" class="modal">
<div class="modal-box max-w-sm text-center">
<div class="text-5xl mb-4 text-error"><i class="fa-solid fa-trash-can"></i></div>
<h3 class="font-bold text-xl mb-2">Hapus Data?</h3>
<p class="text-sm text-base-content/60 mb-6">Data ini akan dihapus secara permanen dari sistem.</p>
<div class="flex gap-2 justify-center">
<button class="btn btn-ghost px-8" onclick="deleteModal.close()">Batal</button>
<button class="btn btn-error px-8 text-white" id="confirmDeleteBtn">Ya, Hapus</button>
</div>
</div>
</dialog>
<!-- Toast Container -->
<div class="toast toast-end toast-bottom z-[9999]" id="toastBox"></div>
<!-- ═══ JAVASCRIPT ════════════════════════════════════════════════════════ -->
<script>
// ── CONFIG & STATE ──────────────────────────────────────────────────
let map, currentMode = 'view';
let layerIbadah = L.layerGroup(), layerMiskin = L.layerGroup();
let activeCircle = null, activeIbadah = null;
let ibadahData = [], miskinData = [];
let tempMarker = null;
let ibadahMarkers = new Map(); // id -> marker
let miskinMarkers = new Map(); // id_rumah -> marker
const icons = {
ibadah: (color = '#22c55e') => L.divIcon({
html: `<div style="background:${color};width:32px;height:32px;border-radius:32px 32px 0 32px;border:3px solid white;transform:rotate(-45deg);display:flex;align-items:center;justify-center;box-shadow:0 4px 10px rgba(0,0,0,0.3);position:relative">
<i class="fa-solid fa-mosque" style="transform:rotate(45deg);color:white;font-size:14px;margin-left:6px;margin-top:2px"></i>
</div>`,
className: '', iconSize: [32, 32], iconAnchor: [0, 32], popupAnchor: [16, -35]
}),
miskin: (color = '#ef4444', glow = false) => L.divIcon({
html: `<div style="background:${color};width:24px;height:24px;border-radius:24px;border:2px solid white;display:flex;align-items:center;justify-content:center;box-shadow:0 3px 8px rgba(0,0,0,0.3);${glow ? 'box-shadow: 0 0 15px ' + color : ''}">
<i class="fa-solid fa-home" style="color:white;font-size:10px"></i>
</div>`,
className: '', iconSize: [24, 24], iconAnchor: [12, 12], popupAnchor: [0, -15]
}),
temp: () => L.divIcon({
html: `<div class="animate-pulse" style="background:#3b82f6;width:20px;height:20px;border-radius:20px;border:3px solid white;box-shadow:0 0 15px rgba(59,130,246,0.6)"></div>`,
className: '', iconSize: [20, 20], iconAnchor: [10, 10]
})
};
// ── INIT ────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
initMap();
loadData();
});
function initMap() {
map = L.map('map', { zoomControl: false }).setView([-0.0553, 109.3463], 17);
const osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 });
const satellite = L.layerGroup([
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { maxZoom: 19 }),
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}', { maxZoom: 19 }),
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Transportation/MapServer/tile/{z}/{y}/{x}', { maxZoom: 19 })
]);
satellite.addTo(map);
L.control.zoom({ position: 'bottomright' }).addTo(map);
L.control.layers({ "Satelit": satellite, "Street": osm },
{ "Rumah Ibadah": layerIbadah, "Rumah Miskin": layerMiskin },
{ position: 'topright' }).addTo(map);
layerIbadah.addTo(map);
layerMiskin.addTo(map);
map.on('click', e => {
if (currentMode === 'view') return;
handleMapClick(e.latlng);
});
}
async function loadData() {
try {
const [ri, rm] = await Promise.all([
fetch('../api/get_ibadah.php').then(r => r.json()),
fetch('../api/get_miskin.php').then(r => r.json())
]);
// Validate data is array
if (!Array.isArray(ri) || !Array.isArray(rm)) {
console.error("Invalid API response", { ri, rm });
return;
}
ibadahData = ri;
miskinData = rm;
// Sync activeIbadah to point to the new object in the fresh data
if (activeIbadah) {
const matched = ibadahData.find(i => i.id == activeIbadah.id);
if (matched) activeIbadah = matched;
}
renderMarkers();
} catch (e) {
console.error(e);
showToast('Gagal memuat data', 'error');
}
}
function renderMarkers() {
layerIbadah.clearLayers();
layerMiskin.clearLayers();
ibadahMarkers.clear();
miskinMarkers.clear();
ibadahData.forEach(item => {
const m = L.marker([item.latitude, item.longitude], { icon: icons.ibadah(), draggable: true })
.bindPopup(buildPopupIbadah(item))
.on('click', () => handleIbadahClick(item));
m.on('dragend', e => updateLocation('ibadah', item.id, e.target.getLatLng(), item));
layerIbadah.addLayer(m);
ibadahMarkers.set(item.id, m);
});
miskinData.forEach(item => {
const marker = L.marker([item.latitude, item.longitude], {
icon: icons.miskin('#ef4444'),
draggable: true
}).bindPopup(buildPopupMiskin(item));
marker.on('dragend', e => updateLocation('miskin', item.id_rumah, e.target.getLatLng(), item));
layerMiskin.addLayer(marker);
miskinMarkers.set(item.id_rumah, marker);
});
// If an analysis is active, update highlights immediately
if (activeCircle) {
updateAnalysisUI(activeCircle.getLatLng(), activeCircle.getRadius());
}
}
// ── MODE & CLICK HANDLING ───────────────────────────────────────────
function setMode(mode) {
currentMode = mode;
document.querySelectorAll('.btn-outline').forEach(b => b.classList.remove('active'));
document.getElementById('btnMode' + mode.charAt(0).toUpperCase() + mode.slice(1)).classList.add('active');
document.getElementById('mapHint').classList.toggle('hidden', mode === 'view');
document.getElementById('analysisPanel').classList.toggle('hidden', mode !== 'view');
if (mode !== 'view') {
if (activeCircle) { map.removeLayer(activeCircle); activeCircle = null; }
document.getElementById('statsContent').classList.add('hidden');
document.getElementById('noSelectionMsg').classList.remove('hidden');
}
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
}
function handleMapClick(latlng) {
if (tempMarker) map.removeLayer(tempMarker);
tempMarker = L.marker(latlng, { icon: icons.temp() }).addTo(map);
if (currentMode === 'ibadah') {
document.getElementById('ib_coords').innerText = `Lat: ${latlng.lat.toFixed(6)}, Lng: ${latlng.lng.toFixed(6)}`;
document.getElementById('ib_nama').value = '';
document.getElementById('ib_alamat').value = 'Mencari alamat...';
modalIbadah.showModal();
// Auto fetch address
reverseGeocode(latlng.lat, latlng.lng, 'ib_alamat');
} else {
document.getElementById('rm_coords').innerText = `Lat: ${latlng.lat.toFixed(6)}, Lng: ${latlng.lng.toFixed(6)}`;
document.getElementById('rm_id').value = '';
document.getElementById('rm_alamat').value = 'Mencari alamat...';
modalMiskin.showModal();
// Auto fetch address
reverseGeocode(latlng.lat, latlng.lng, 'rm_alamat');
}
}
async function reverseGeocode(lat, lng, targetId) {
try {
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lng}`);
const data = await res.json();
if (data && data.display_name) {
document.getElementById(targetId).value = data.display_name;
} else {
document.getElementById(targetId).value = "Alamat tidak ditemukan";
}
} catch (e) {
document.getElementById(targetId).value = "Gagal mengambil alamat otomatis";
}
}
function handleIbadahClick(item) {
if (currentMode !== 'view') return;
activeIbadah = item;
document.getElementById('noSelectionMsg').classList.add('hidden');
document.getElementById('statsContent').classList.remove('hidden');
document.getElementById('activeName').innerText = item.nama;
document.getElementById('activeAddr').innerText = item.alamat;
const radius = document.getElementById('radiusSlider').value = item.radius || 500;
updateRadius(item.latitude, item.longitude, radius);
}
// ── RADIUS ANALYSIS ────────────────────────────────────────────────
function updateRadius(lat, lng, radius) {
if (activeCircle) map.removeLayer(activeCircle);
activeCircle = L.circle([lat, lng], {
radius: parseInt(radius),
color: '#2563eb',
fillColor: '#3b82f6',
fillOpacity: 0.2,
weight: 2,
dashArray: '5, 10'
}).addTo(map);
document.getElementById('radiusVal').innerText = radius;
updateAnalysisUI([lat, lng], radius);
}
function updateAnalysisUI(center, radius) {
let houses = 0, kk = 0, jiwa = 0;
miskinData.forEach(h => {
const houseMarker = miskinMarkers.get(h.id_rumah);
const isH = isInside(h, center, radius);
if (isH) {
houses++;
kk += parseInt(h.jumlah_kk);
jiwa += parseInt(h.jumlah_orang);
}
// Update marker icon in real-time
if (houseMarker) {
const currentColor = isH ? '#fbbf24' : '#ef4444';
// Only update icon if state changed (optional optimization, but setIcon is fine)
houseMarker.setIcon(icons.miskin(currentColor, isH));
houseMarker.setZIndexOffset(isH ? 1000 : 0);
}
});
document.getElementById('statRumah').innerText = houses;
document.getElementById('statKk').innerText = kk;
document.getElementById('statJiwa').innerText = jiwa;
}
function isInside(point, center, radius) {
// center can be [lat, lng] or {lat, lng}
const cLat = center.lat !== undefined ? center.lat : center[0];
const cLng = center.lng !== undefined ? center.lng : center[1];
const p1 = turf.point([Number(point.longitude), Number(point.latitude)]);
const p2 = turf.point([Number(cLng), Number(cLat)]);
return turf.distance(p1, p2, { units: 'meters' }) <= radius;
}
document.getElementById('radiusSlider').addEventListener('input', e => {
if (activeIbadah) {
updateRadius(activeIbadah.latitude, activeIbadah.longitude, e.target.value);
}
});
// Save radius to DB when user finishes sliding
document.getElementById('radiusSlider').addEventListener('change', async e => {
if (!activeIbadah) return;
const newRadius = e.target.value;
try {
const res = await fetch('../api/update_ibadah.php', {
method: 'POST',
body: JSON.stringify({
id: activeIbadah.id,
nama: activeIbadah.nama,
alamat: activeIbadah.alamat,
latitude: activeIbadah.latitude,
longitude: activeIbadah.longitude,
radius: newRadius
})
});
const resJson = await res.json();
if (resJson.success) {
activeIbadah.radius = newRadius;
showToast('Radius disimpan ke database', 'success');
// Update the local data copy so it persists in memory
const localItem = ibadahData.find(i => i.id == activeIbadah.id);
if (localItem) localItem.radius = newRadius;
}
} catch (e) {
showToast('Gagal menyimpan radius ke database', 'error');
}
});
// ── POPUPS ──────────────────────────────────────────────────────────
function buildPopupIbadah(item) {
return `<div class="p-1">
<div class="font-bold text-sm">🕌 ${item.nama}</div>
<div class="text-[10px] text-gray-500 mb-2">${item.alamat}</div>
<div class="flex gap-1 border-t pt-2 mt-1">
<button class="btn btn-xs btn-outline btn-error" onclick="prepDelete('ibadah', ${item.id})">Hapus</button>
</div>
</div>`;
}
function buildPopupMiskin(item) {
return `<div class="p-1">
<div class="font-bold text-sm">🏠 ID: ${item.id_rumah}</div>
<div class="text-[10px] text-gray-500 mb-1">${item.alamat || 'Alamat tidak tersedia'}</div>
<div class="text-[10px] text-gray-400">KK: ${item.jumlah_kk} | Jiwa: ${item.jumlah_orang}</div>
<div class="flex gap-1 border-t pt-2 mt-1">
<button class="btn btn-xs btn-outline btn-error" onclick="prepDelete('miskin', '${item.id_rumah}')">Hapus</button>
</div>
</div>`;
}
// ── CRUD OPERATIONS ────────────────────────────────────────────────
async function saveIbadah() {
const pos = tempMarker.getLatLng();
const data = {
nama: document.getElementById('ib_nama').value,
alamat: document.getElementById('ib_alamat').value,
latitude: pos.lat, longitude: pos.lng,
radius: document.getElementById('ib_radius').value
};
const btn = document.getElementById('btnSaveIbadah');
btn.disabled = true;
const res = await fetch('../api/save_ibadah.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const resJson = await res.json();
if (resJson.success) {
showToast('Rumah Ibadah disimpan', 'success');
modalIbadah.close();
loadData();
} else showToast(resJson.message, 'error');
btn.disabled = false;
}
async function saveMiskin() {
const pos = tempMarker.getLatLng();
const data = {
id_rumah: document.getElementById('rm_id').value,
alamat: document.getElementById('rm_alamat').value,
jumlah_kk: document.getElementById('rm_kk').value,
jumlah_orang: document.getElementById('rm_jiwa').value,
latitude: pos.lat, longitude: pos.lng
};
const btn = document.getElementById('btnSaveMiskin');
btn.disabled = true;
const res = await fetch('../api/save_miskin.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const resJson = await res.json();
if (resJson.success) {
showToast('Rumah Miskin disimpan', 'success');
modalMiskin.close();
loadData();
} else showToast(resJson.message, 'error');
btn.disabled = false;
}
async function updateLocation(type, id, latlng, oldData) {
const endpoint = `../api/update_${type}.php`;
const payload = type === 'ibadah'
? { id, nama: oldData.nama, alamat: oldData.alamat, latitude: latlng.lat, longitude: latlng.lng, radius: oldData.radius }
: { id_rumah: id, alamat: oldData.alamat, jumlah_kk: oldData.jumlah_kk, jumlah_orang: oldData.jumlah_orang, latitude: latlng.lat, longitude: latlng.lng };
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const resJson = await res.json();
if (resJson.success) {
showToast('Lokasi diperbarui', 'success');
setTimeout(loadData, 100); // Small delay to let Leaflet finish
} else {
showToast(resJson.message || 'Gagal memperbarui lokasi', 'error');
setTimeout(loadData, 100);
}
} catch (e) {
showToast('Gagal terhubung ke server', 'error');
setTimeout(loadData, 100);
}
}
let deleteTarget = null;
function prepDelete(type, id) {
deleteTarget = { type, id };
map.closePopup();
deleteModal.showModal();
}
document.getElementById('confirmDeleteBtn').onclick = async () => {
const { type, id } = deleteTarget;
const res = await fetch(`../api/delete_${type}.php`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(type === 'ibadah' ? { id } : { id_rumah: id })
});
const resJson = await res.json();
if (resJson.success) {
showToast('Data dihapus', 'success');
deleteModal.close();
loadData();
} else showToast('Gagal menghapus', 'error');
};
// ── TOAST ──────────────────────────────────────────────────────────
function showToast(msg, type = 'info') {
const t = document.createElement('div');
const alertCls = type === 'success' ? 'alert-success' : type === 'error' ? 'alert-error' : 'alert-info';
t.className = `alert ${alertCls} shadow-lg py-3 text-xs font-bold`;
t.innerHTML = `<span><i class="fa-solid fa-circle-info"></i> ${msg}</span>`;
document.getElementById('toastBox').appendChild(t);
setTimeout(() => t.remove(), 3000);
}
</script>
</body>
</html>