Initial commit

This commit is contained in:
2026-06-12 12:00:12 +07:00
commit 46d9a9d1a0
17 changed files with 1607 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
FROM php:8.2-apache
RUN docker-php-ext-install mysqli pdo pdo_mysql
RUN a2enmod rewrite
COPY . /var/www/html/
RUN chown -R www-data:www-data /var/www/html
EXPOSE 80
+21
View File
@@ -0,0 +1,21 @@
<?php
header('Content-Type: application/json');
require_once 'auth.php';
require_once 'config.php';
$result = mysqli_query($koneksi, "SELECT * FROM tempat_ibadah");
$data = [];
while ($row = mysqli_fetch_assoc($result)) {
$data[] = [
"id" => (int)$row['id'],
"jenis" => $row['jenis'],
"nama" => $row['nama'] ?? '',
"lat" => (float)$row['lat'],
"lng" => (float)$row['lng'],
"radius" => (int)$row['radius'],
"alamat" => $row['alamat'] ?? ''
];
}
echo json_encode($data);
mysqli_close($koneksi);
+38
View File
@@ -0,0 +1,38 @@
<?php
header('Content-Type: application/json');
require_once 'auth.php';
require_once 'config.php';
$result = mysqli_query($koneksi, "SELECT * FROM rumah");
$data = [];
while ($row = mysqli_fetch_assoc($result)) {
if (is_admin()) {
// Admin: semua field
$data[] = [
"id" => (int)$row['id'],
"lat" => (float)$row['lat'],
"lng" => (float)$row['lng'],
"status" => $row['status'],
"alamat" => $row['alamat'] ?? '',
"nik" => $row['nik'] ?? '',
"nama" => $row['nama'] ?? '',
"ttl" => $row['ttl'] ?? '',
"pendidikan" => $row['pendidikan'] ?? '',
];
} else {
// Guest / user biasa: nama, NIK, alamat, status
$data[] = [
"id" => (int)$row['id'],
"lat" => (float)$row['lat'],
"lng" => (float)$row['lng'],
"status" => $row['status'],
"alamat" => $row['alamat'] ?? '',
"nik" => $row['nik'] ?? '',
"nama" => $row['nama'] ?? '',
];
}
}
echo json_encode($data);
mysqli_close($koneksi);
+24
View File
@@ -0,0 +1,24 @@
<?php
// auth.php — include di setiap file backend PHP
session_start();
function is_admin(): bool {
return ($_SESSION['role'] ?? '') === 'admin';
}
function is_logged_in(): bool {
return isset($_SESSION['user']);
}
// Untuk endpoint READ: semua boleh akses (termasuk guest tanpa session)
// Tidak perlu fungsi require — langsung akses saja
// Untuk API endpoint: tolak jika bukan admin
function require_admin_api(): void {
if (($_SESSION['role'] ?? '') !== 'admin') {
header('Content-Type: application/json');
echo json_encode(["status" => "error", "message" => "Forbidden: admin only"]);
exit;
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
$koneksi = mysqli_connect(
"mysql-database-ni7pcyiv0zp7hwao4033461g",
"mysql",
"4OfMLOPJfFl1jcfhvxF5NFodtAiEFZ0zkzK2eSFhDnTmgIWd1bCZNbOnZnhvaXL3",
"default",
3306
);
if (!$koneksi) {
die("Koneksi gagal: " . mysqli_connect_error());
}
?>
+33
View File
@@ -0,0 +1,33 @@
<?php
header('Content-Type: application/json');
require_once 'auth.php';
require_admin_api();
require_once 'config.php';
$id = (int)($_POST['id'] ?? 0);
$radius = (int)($_POST['radius'] ?? 0);
if ($id == 0) {
echo json_encode(["status" => "error", "message" => "ID tidak valid"]);
exit;
}
if (isset($_POST['_only_radius'])) {
$stmt = mysqli_prepare($koneksi, "UPDATE tempat_ibadah SET radius=? WHERE id=?");
mysqli_stmt_bind_param($stmt, "ii", $radius, $id);
} else {
$jenis = $_POST['jenis'] ?? '';
$nama = $_POST['nama'] ?? '';
$stmt = mysqli_prepare($koneksi,
"UPDATE tempat_ibadah SET jenis=?, nama=?, radius=? WHERE id=?"
);
mysqli_stmt_bind_param($stmt, "ssii", $jenis, $nama, $radius, $id);
}
if (mysqli_stmt_execute($stmt)) {
echo json_encode(["status" => "success"]);
} else {
echo json_encode(["status" => "error", "message" => mysqli_error($koneksi)]);
}
mysqli_close($koneksi);
+35
View File
@@ -0,0 +1,35 @@
<?php
header('Content-Type: application/json');
require_once 'auth.php';
require_admin_api();
require_once 'config.php';
$id = (int)($_POST['id'] ?? 0);
$status = $_POST['status'] ?? '';
$nik = $_POST['nik'] ?? '';
$nama = $_POST['nama'] ?? '';
$ttl = $_POST['ttl'] ?? '';
$pendidikan = $_POST['pendidikan'] ?? '';
if ($id == 0) {
echo json_encode(["status" => "error", "message" => "ID tidak valid"]);
exit;
}
if (!in_array($status, ['miskin', 'tidak_miskin'])) {
echo json_encode(["status" => "error", "message" => "Status tidak valid"]);
exit;
}
$stmt = mysqli_prepare($koneksi,
"UPDATE rumah SET status=?, nik=?, nama=?, ttl=?, pendidikan=? WHERE id=?"
);
mysqli_stmt_bind_param($stmt, "sssssi", $status, $nik, $nama, $ttl, $pendidikan, $id);
if (mysqli_stmt_execute($stmt)) {
echo json_encode(["status" => "success"]);
} else {
echo json_encode(["status" => "error", "message" => mysqli_error($koneksi)]);
}
mysqli_close($koneksi);
+163
View File
@@ -0,0 +1,163 @@
-- phpMyAdmin SQL Dump
-- version 5.2.1
-- https://www.phpmyadmin.net/
--
-- Host: 127.0.0.1:3307
-- Waktu pembuatan: 11 Jun 2026 pada 12.35
-- Versi server: 10.4.32-MariaDB
-- Versi PHP: 8.2.12
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: `gis_kemiskinan`
--
-- --------------------------------------------------------
--
-- Struktur dari tabel `rumah`
--
CREATE TABLE `rumah` (
`id` int(11) NOT NULL,
`lat` double NOT NULL,
`lng` double NOT NULL,
`status` enum('miskin','tidak_miskin') DEFAULT 'miskin',
`alamat` text DEFAULT NULL,
`nik` varchar(16) DEFAULT '',
`nama` varchar(100) DEFAULT '',
`ttl` varchar(100) DEFAULT '',
`pendidikan` varchar(50) DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
--
-- Dumping data untuk tabel `rumah`
--
INSERT INTO `rumah` (`id`, `lat`, `lng`, `status`, `alamat`, `nik`, `nama`, `ttl`, `pendidikan`) VALUES
(2, -0.024156202282737894, 109.32903081178668, 'miskin', 'Gang Bayan, Tengah, Pontianak Kota, Pontianak, West Kalimantan, Kalimantan, 78244, Indonesia', '', '', '', ''),
(3, -0.024164252873410178, 109.32896912097931, 'miskin', 'Gang Bayan, Tengah, Pontianak Kota, Pontianak, West Kalimantan, Kalimantan, 78244, Indonesia', '', '', '', ''),
(4, -0.02418035405476747, 109.32888865470888, 'tidak_miskin', 'Gang Bayan, Tengah, Pontianak Kota, Pontianak, West Kalimantan, Kalimantan, 78244, Indonesia', '', '', '', ''),
(5, -0.024306479975143014, 109.32889670133592, 'miskin', 'Gang Belibis, Tengah, Pontianak Kota, Pontianak, West Kalimantan, Kalimantan, 78244, Indonesia', '', '', '', ''),
(6, -0.024523845922785795, 109.32900398969652, 'tidak_miskin', 'Gang Belibis, Tengah, Pontianak Kota, Pontianak, West Kalimantan, Kalimantan, 78244, Indonesia', '', '', '', ''),
(8, -0.03882407834072974, 109.33314800262453, 'miskin', 'Jalan Ahmad Marzuki, Akcaya, Pontianak Selatan, Pontianak, West Kalimantan, Kalimantan, 78117, Indonesia', '', '', '', ''),
(9, -0.0241514282241702, 109.32919442653657, 'miskin', 'Gang Bayan, Tengah, Pontianak Kota, Pontianak, West Kalimantan, Kalimantan, 78244, Indonesia', '1111111111111111', 'Ahmad', 'Pontianak, 02 Januari 2026', 'SMA/Sederajat'),
(10, -0.04244148341739716, 109.33556199073793, 'tidak_miskin', 'Jalan Syuhada, Akcaya, Pontianak Selatan, Pontianak, Kalimantan Barat, Kalimantan, 78121, Indonesia', '1111111111111112', 'A', 'Pontianak, 1 Januari 2000', 'S1'),
(11, -0.042548771748473165, 109.33563709259035, 'miskin', 'Jalan Syuhada, Akcaya, Pontianak Selatan, Pontianak, Kalimantan Barat, Kalimantan, 78121, Indonesia', '1111111111111113', 'B', 'Pontianak, 1 Januari 2001', 'Tidak Sekolah'),
(12, -0.0388312309510227, 109.33321237564088, 'miskin', 'Jalan Ahmad Marzuki, Akcaya, Pontianak Selatan, Pontianak, Kalimantan Barat, Kalimantan, 78117, Indonesia', '1111111111111114', 'C', 'Pontianak, 1 Januari 2002', 'SMP/Sederajat');
-- --------------------------------------------------------
--
-- Struktur dari tabel `tempat_ibadah`
--
CREATE TABLE `tempat_ibadah` (
`id` int(11) NOT NULL,
`jenis` varchar(50) DEFAULT NULL,
`lat` double DEFAULT NULL,
`lng` double DEFAULT NULL,
`radius` int(11) DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`alamat` text DEFAULT NULL,
`nama` varchar(255) DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
--
-- Dumping data untuk tabel `tempat_ibadah`
--
INSERT INTO `tempat_ibadah` (`id`, `jenis`, `lat`, `lng`, `radius`, `created_at`, `alamat`, `nama`) VALUES
(11, 'Masjid', -0.024344299151599625, 109.32909250259401, 200, '2026-05-06 01:54:48', 'Gang Belibis, Tengah, Pontianak Kota, Pontianak, West Kalimantan, Kalimantan, 78244, Indonesia', ''),
(12, 'Masjid', -0.04147410027844799, 109.33633446693422, 500, '2026-05-06 03:36:43', 'Mujahiddin, Jalan Mujahidin, Akcaya, Pontianak Selatan, Pontianak, Kalimantan Barat, Kalimantan, 78121, Indonesia', ''),
(13, 'Masjid', -0.05705236443070954, 109.34619426727296, 300, '2026-05-06 03:41:43', 'Universitas Tanjungpura, Jalan Reformasi Untan, Bansir Laut, Pontianak Tenggara, Pontianak, Kalimantan Barat, Kalimantan, 79124, Indonesia', ''),
(14, 'Masjid', -0.05461334353835791, 109.34729933738708, 300, '2026-05-06 03:42:14', 'Masjid Polnep, Jalan Ahmad Yani, Bansir Laut, Pontianak Tenggara, Pontianak, Kalimantan Barat, Kalimantan, 71127, Indonesia', ''),
(15, 'Masjid', -0.05328654474069965, 109.3503624200821, 300, '2026-05-06 03:42:51', 'Universitas Tanjungpura, Vigor Sport Center, Parit Tokaya, Pontianak Selatan, Pontianak, Kalimantan Barat, Kalimantan, 71127, Indonesia', ''),
(16, 'Masjid', -0.058347870748418774, 109.35142725706102, 300, '2026-05-06 03:43:37', 'Al-Azhar, Jalan Sepakat 2, Bansir Darat, Pontianak Tenggara, Pontianak, Kalimantan Barat, Kalimantan, 79124, Indonesia', ''),
(17, 'Masjid', -0.05907653714139404, 109.35188591480257, 300, '2026-05-06 03:43:55', 'Toss, Jalan Ahmad Yani, Bansir Darat, Pontianak Tenggara, Pontianak, Kalimantan Barat, Kalimantan, 79124, Indonesia', ''),
(18, 'Masjid', -0.060203064317339586, 109.34991180896759, 300, '2026-05-06 03:46:06', 'Bansir Laut, Pontianak Tenggara, Pontianak, Kalimantan Barat, Kalimantan, 79124, Indonesia', ''),
(19, 'Gereja', -0.05240141614224753, 109.34470295906068, 300, '2026-05-06 03:48:02', 'Realme Service Center, Vigor Sport Center, Parit Tokaya, Pontianak Selatan, Pontianak, Kalimantan Barat, Kalimantan, 71127, Indonesia', ''),
(20, 'Gereja', -0.051046007065574835, 109.34300243854524, 300, '2026-05-06 03:48:55', 'Sushi Phe, Vigor Sport Center, Parit Tokaya, Pontianak Selatan, Pontianak, Kalimantan Barat, Kalimantan, 71127, Indonesia', ''),
(21, 'Gereja', -0.050502412925482874, 109.34434890747072, 300, '2026-05-06 03:49:38', 'Jalan Ahmad Yani, Parit Tokaya, Pontianak Selatan, Pontianak, Kalimantan Barat, Kalimantan, 71127, Indonesia', ''),
(22, 'Pura', -0.06357191687433154, 109.36737298965456, 300, '2026-05-06 03:51:17', 'Sungai Raya, Kubu Raya, Kalimantan Barat, Kalimantan, 78122, Indonesia', ''),
(23, 'Masjid', -0.02381954295837788, 109.33475732803345, 300, '2026-05-22 05:54:11', 'Jalan Nurali, Tengah, Pontianak Kota, Pontianak, West Kalimantan, Kalimantan, 78112, Indonesia', 'Masjid A');
-- --------------------------------------------------------
--
-- Struktur dari tabel `users`
--
CREATE TABLE `users` (
`id` int(11) NOT NULL,
`username` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL,
`role` enum('admin','user') NOT NULL DEFAULT 'user',
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
--
-- Dumping data untuk tabel `users`
--
INSERT INTO `users` (`id`, `username`, `password`, `role`, `created_at`) VALUES
(1, 'admin', '$2y$10$Lz6hPjGETnmDrr0VoXwMPugAILtnXybHnakZziDmiodbcIl3kDoUW', 'admin', '2026-05-22 05:43:29');
--
-- Indexes for dumped tables
--
--
-- Indeks untuk tabel `rumah`
--
ALTER TABLE `rumah`
ADD PRIMARY KEY (`id`);
--
-- Indeks untuk tabel `tempat_ibadah`
--
ALTER TABLE `tempat_ibadah`
ADD PRIMARY KEY (`id`);
--
-- Indeks untuk tabel `users`
--
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `username` (`username`);
--
-- AUTO_INCREMENT untuk tabel yang dibuang
--
--
-- AUTO_INCREMENT untuk tabel `rumah`
--
ALTER TABLE `rumah`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=13;
--
-- AUTO_INCREMENT untuk tabel `tempat_ibadah`
--
ALTER TABLE `tempat_ibadah`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=24;
--
-- AUTO_INCREMENT untuk tabel `users`
--
ALTER TABLE `users`
MODIFY `id` int(11) 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 */;
+22
View File
@@ -0,0 +1,22 @@
<?php
header('Content-Type: application/json');
require_once 'auth.php';
require_admin_api();
require_once 'config.php';
$id = (int)($_POST['id'] ?? 0);
if ($id == 0) {
echo json_encode(["status" => "error"]);
exit;
}
$stmt = mysqli_prepare($koneksi, "DELETE FROM tempat_ibadah WHERE id=?");
mysqli_stmt_bind_param($stmt, "i", $id);
if (mysqli_stmt_execute($stmt)) {
echo json_encode(["status" => "success"]);
} else {
echo json_encode(["status" => "error"]);
}
mysqli_close($koneksi);
+22
View File
@@ -0,0 +1,22 @@
<?php
header('Content-Type: application/json');
require_once 'auth.php';
require_admin_api();
require_once 'config.php';
$id = (int)($_POST['id'] ?? 0);
if ($id == 0) {
echo json_encode(["status" => "error"]);
exit;
}
$stmt = mysqli_prepare($koneksi, "DELETE FROM rumah WHERE id=?");
mysqli_stmt_bind_param($stmt, "i", $id);
if (mysqli_stmt_execute($stmt)) {
echo json_encode(["status" => "success"]);
} else {
echo json_encode(["status" => "error"]);
}
mysqli_close($koneksi);
+3
View File
@@ -0,0 +1,3 @@
<?php
header("Location: maps.php");
exit();
+157
View File
@@ -0,0 +1,157 @@
<?php
session_start();
// Jika sudah login, redirect ke maps
if (isset($_SESSION['user'])) {
header('Location: maps.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$koneksi = mysqli_connect("127.0.0.1", "root", "", "gis_kemiskinan", 3307);
if (!$koneksi) {
$error = 'Koneksi database gagal.';
} else {
$stmt = mysqli_prepare($koneksi, "SELECT * FROM users WHERE username=? LIMIT 1");
mysqli_stmt_bind_param($stmt, "s", $username);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$user = mysqli_fetch_assoc($result);
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user'] = $user['username'];
$_SESSION['role'] = $user['role'];
mysqli_close($koneksi);
header('Location: maps.php');
exit;
} else {
$error = 'Username atau password salah.';
}
mysqli_close($koneksi);
}
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login GIS Kemiskinan</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', sans-serif;
background: #0f1117;
color: #e2e8f0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-box {
background: #1a1d2e;
border: 1px solid #2d3748;
border-radius: 14px;
padding: 36px 32px;
width: 100%;
max-width: 360px;
box-shadow: 0 20px 60px rgba(0,0,0,.5);
}
.login-box h1 {
font-size: 18px;
font-weight: 700;
color: #63b3ed;
text-align: center;
margin-bottom: 4px;
}
.login-box .sub {
font-size: 12px;
color: #718096;
text-align: center;
margin-bottom: 28px;
}
label {
display: block;
font-size: 12px;
color: #a0aec0;
margin-bottom: 5px;
font-weight: 600;
}
input[type=text], input[type=password] {
width: 100%;
padding: 10px 12px;
background: #0f1117;
border: 1px solid #2d3748;
border-radius: 7px;
color: #e2e8f0;
font-size: 14px;
margin-bottom: 16px;
outline: none;
transition: border-color .2s;
}
input[type=text]:focus, input[type=password]:focus {
border-color: #4299e1;
}
button[type=submit] {
width: 100%;
padding: 10px;
background: #2b6cb0;
border: none;
border-radius: 7px;
color: #fff;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: background .2s;
}
button[type=submit]:hover { background: #2c5282; }
.error-msg {
background: #742a2a;
border: 1px solid #c53030;
color: #fc8181;
padding: 8px 12px;
border-radius: 7px;
font-size: 12px;
margin-bottom: 16px;
text-align: center;
}
.guest-link {
display: block;
text-align: center;
margin-top: 14px;
font-size: 12px;
color: #718096;
text-decoration: none;
transition: color .2s;
}
.guest-link:hover { color: #a0aec0; }
</style>
</head>
<body>
<div class="login-box">
<h1>🗺 GIS Kemiskinan</h1>
<div class="sub">Silakan login untuk melanjutkan</div>
<?php if ($error): ?>
<div class="error-msg">⚠️ <?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="POST">
<label>Username</label>
<input type="text" name="username" placeholder="Masukkan username" autofocus required>
<label>Password</label>
<input type="password" name="password" placeholder="Masukkan password" required>
<button type="submit">🔐 Login</button>
</form>
<a href="maps.php" class="guest-link">👁 Masuk sebagai Pengunjung (hanya lihat)</a>
</div>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
<?php
session_start();
session_destroy();
header('Location: login.php');
exit;
+985
View File
@@ -0,0 +1,985 @@
<?php
session_start();
// ── CEK AKSES ──────────────────────────────────────────────
// Default: semua yang belum login dianggap guest (hanya lihat)
$isGuest = true;
$isAdmin = false;
$isLoggedIn = false;
$currentUser = '';
if (isset($_SESSION['user'])) {
$isLoggedIn = true;
$currentUser = $_SESSION['user'];
$isAdmin = ($_SESSION['role'] ?? '') === 'admin';
$isGuest = false;
}
// Role yang dikirim ke JS
$roleJS = $isAdmin ? 'admin' : 'guest';
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GIS Kemiskinan Tempat Ibadah</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', sans-serif;
background: #0f1117;
color: #e2e8f0;
height: 100vh;
display: flex;
flex-direction: column;
}
/* ── HEADER ── */
#header {
background: linear-gradient(135deg, #1a1d2e 0%, #16213e 100%);
border-bottom: 1px solid #2d3748;
padding: 10px 16px;
display: flex;
align-items: center;
gap: 12px;
z-index: 1000;
flex-shrink: 0;
}
#header h1 { font-size: 15px; font-weight: 700; color: #63b3ed; letter-spacing: .5px; }
#header .subtitle { font-size: 11px; color: #718096; }
/* ── TOOLBAR ── */
#toolbar { display: flex; gap: 6px; margin-left: auto; align-items: center; }
.mode-btn {
padding: 6px 12px; border-radius: 6px; border: 1px solid #2d3748;
background: #1a202c; color: #a0aec0; font-size: 12px; cursor: pointer; transition: all .2s;
}
.mode-btn:hover { border-color: #63b3ed; color: #63b3ed; }
.mode-btn.active { background: #2b6cb0; border-color: #4299e1; color: #fff; }
/* user badge & logout */
.user-badge {
display: flex; align-items: center; gap: 6px;
background: #0f1117; border: 1px solid #2d3748; border-radius: 20px;
padding: 4px 10px; font-size: 12px; color: #a0aec0;
}
.user-badge .role-tag {
background: <?= $isAdmin ? '#2b6cb0' : '#276749' ?>;
color: #fff; font-size: 10px; font-weight: 700;
padding: 1px 6px; border-radius: 8px;
}
.btn-logout {
padding: 5px 10px; border-radius: 6px; border: 1px solid #c53030;
background: transparent; color: #fc8181; font-size: 11px; cursor: pointer; transition: all .2s;
}
.btn-logout:hover { background: #c53030; color: #fff; }
.btn-login-small {
padding: 5px 12px; border-radius: 6px; border: 1px solid #2b6cb0;
background: transparent; color: #63b3ed; font-size: 11px; cursor: pointer; transition: all .2s;
}
.btn-login-small:hover { background: #2b6cb0; color: #fff; }
/* ── MAIN ── */
#main { display: flex; flex: 1; overflow: hidden; }
#map { flex: 1; z-index: 1; }
/* ── SIDE PANEL ── */
#side-panel {
width: 300px; background: #1a1d2e; border-left: 1px solid #2d3748;
display: flex; flex-direction: column; overflow-y: auto; flex-shrink: 0;
}
.panel-section { border-bottom: 1px solid #2d3748; padding: 14px; }
.panel-section h3 {
font-size: 11px; font-weight: 700; text-transform: uppercase;
letter-spacing: 1px; color: #63b3ed; margin-bottom: 10px;
}
/* stats */
.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.stat-card { background: #0f1117; border-radius: 8px; padding: 10px; text-align: center; }
.stat-num { font-size: 22px; font-weight: 700; }
.stat-label { font-size: 10px; color: #718096; margin-top: 2px; }
.stat-red { color: #fc8181; }
.stat-green { color: #68d391; }
.stat-blue { color: #63b3ed; }
.stat-yellow { color: #f6e05e; }
/* selected ibadah */
#selected-info { display: none; }
#selected-info.visible { display: block; }
#radius-display { font-size: 24px; font-weight: 700; color: #63b3ed; text-align: center; margin: 8px 0 4px; }
#radius-label { text-align: center; font-size: 11px; color: #718096; margin-bottom: 10px; }
/* slider */
.slider-wrap { position: relative; padding: 4px 0; }
input[type=range] {
width: 100%; -webkit-appearance: none; height: 6px; border-radius: 3px;
background: #2d3748; outline: none;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; width: 20px; height: 20px; border-radius: 50%;
background: #4299e1; cursor: pointer; box-shadow: 0 0 0 3px rgba(66,153,225,.3);
}
.slider-labels { display: flex; justify-content: space-between; font-size: 10px; color: #718096; margin-top: 4px; }
#btn-save-radius {
width: 100%; margin-top: 10px; padding: 7px;
background: #2b6cb0; border: none; border-radius: 6px;
color: #fff; font-size: 13px; cursor: pointer; transition: background .2s;
}
#btn-save-radius:hover { background: #2c5282; }
/* rumah list in radius */
#rumah-in-radius { max-height: 180px; overflow-y: auto; }
.rumah-item { display: flex; align-items: center; gap: 6px; padding: 5px 0; border-bottom: 1px solid #2d3748; font-size: 12px; color: #a0aec0; }
.rumah-item .dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.dot-miskin { background: #fc8181; }
.dot-aman { background: #68d391; }
/* layer toggle */
.layer-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 13px; }
.layer-icon { font-size: 18px; width: 24px; text-align: center; }
.layer-label { flex: 1; color: #cbd5e0; }
.layer-count { font-size: 10px; background: #2d3748; color: #a0aec0; padding: 2px 6px; border-radius: 10px; }
.toggle { position: relative; width: 34px; height: 18px; flex-shrink: 0; }
.toggle input { opacity: 0; width: 0; height: 0; }
.toggle-slider { position: absolute; inset: 0; background: #2d3748; border-radius: 18px; cursor: pointer; transition: .2s; }
.toggle-slider:before { content: ''; position: absolute; height: 12px; width: 12px; left: 3px; top: 3px; background: #fff; border-radius: 50%; transition: .2s; }
.toggle input:checked + .toggle-slider { background: #2b6cb0; }
.toggle input:checked + .toggle-slider:before { transform: translateX(16px); }
/* legend */
.legend-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: 12px; color: #a0aec0; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
/* leaflet icons */
.icon-ibadah { font-size: 24px; text-align: center; line-height: 32px; text-shadow: 0 2px 6px rgba(0,0,0,.5); filter: drop-shadow(0 1px 3px rgba(0,0,0,.8)); }
/* popup */
.leaflet-popup-content-wrapper {
background: #1a202c !important; color: #e2e8f0 !important;
border: 1px solid #2d3748 !important; border-radius: 10px !important;
box-shadow: 0 10px 30px rgba(0,0,0,.5) !important;
}
.leaflet-popup-tip { background: #1a202c !important; }
.leaflet-popup-content { margin: 12px 16px !important; }
.popup-title { font-weight: 700; font-size: 14px; color: #63b3ed; margin-bottom: 4px; }
.popup-row { font-size: 12px; color: #a0aec0; margin: 2px 0; }
.popup-actions { margin-top: 10px; display: flex; gap: 6px; }
.popup-btn { flex: 1; padding: 5px 8px; border: none; border-radius: 5px; font-size: 11px; cursor: pointer; font-weight: 600; transition: opacity .2s; }
.popup-btn:hover { opacity: .85; }
.btn-edit { background: #2b6cb0; color: #fff; }
.btn-del { background: #c53030; color: #fff; }
.btn-focus { background: #276749; color: #fff; }
/* mode indicator */
#mode-indicator {
position: absolute; top: 10px; left: 50%; transform: translateX(-50%);
background: rgba(26,32,44,.9); border: 1px solid #4299e1; color: #63b3ed;
padding: 6px 14px; border-radius: 20px; font-size: 12px; font-weight: 600;
z-index: 1000; pointer-events: none; display: none;
}
#mode-indicator.visible { display: block; }
/* scrollbar */
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: #1a1d2e; }
::-webkit-scrollbar-thumb { background: #2d3748; border-radius: 3px; }
/* ── DIALOG OVERLAY ── */
.dialog-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.75); z-index: 9999;
display: flex; align-items: center; justify-content: center;
}
.dialog-box {
background: #1a202c; border: 1px solid #2d3748; border-radius: 12px;
padding: 22px 24px; min-width: 300px; max-width: 420px; width: 90%;
max-height: 90vh; overflow-y: auto;
}
.dialog-title { font-weight: 700; font-size: 14px; color: #63b3ed; margin-bottom: 16px; }
.form-row { margin-bottom: 13px; }
.form-row label { display: block; font-size: 11px; color: #a0aec0; font-weight: 600; margin-bottom: 4px; }
.form-row input[type=text],
.form-row input[type=date],
.form-row select,
.form-row textarea {
width: 100%; padding: 8px 10px; background: #0f1117; border: 1px solid #2d3748;
border-radius: 6px; color: #e2e8f0; font-size: 13px; outline: none; transition: border-color .2s;
}
.form-row input:focus, .form-row select:focus, .form-row textarea:focus { border-color: #4299e1; }
.form-row textarea { resize: vertical; min-height: 56px; font-family: inherit; }
.form-row .hint { font-size: 10px; color: #4a5568; margin-top: 3px; }
.form-row select option { background: #1a202c; }
.dialog-actions { display: flex; gap: 8px; margin-top: 16px; }
.dialog-actions button {
flex: 1; padding: 9px; border: none; border-radius: 7px;
font-size: 13px; font-weight: 700; cursor: pointer; transition: opacity .2s;
}
.dialog-actions button:hover { opacity: .85; }
.btn-primary { background: #2b6cb0; color: #fff; }
.btn-cancel { background: #2d3748; color: #a0aec0; }
.btn-danger { background: #c53030; color: #fff; }
.status-miskin { background: #742a2a; border-color: #c53030 !important; color: #fc8181; }
.status-tidak { background: #1c4532; border-color: #276749 !important; color: #68d391; }
</style>
</head>
<body>
<!-- HEADER -->
<div id="header">
<div>
<h1>🗺 GIS Kemiskinan — Tempat Ibadah</h1>
<div class="subtitle" id="header-sub">
<?= $isAdmin ? 'Klik peta untuk tambah data · Klik marker untuk detail' : 'Tampilan pengunjung — hanya lihat data' ?>
</div>
</div>
<div id="toolbar">
<?php if ($isAdmin): ?>
<button class="mode-btn active" id="btn-mode-ibadah" onclick="setMode('ibadah')">🕌 Tambah Ibadah</button>
<button class="mode-btn" id="btn-mode-rumah" onclick="setMode('rumah')">🏠 Tambah Rumah</button>
<button class="mode-btn" id="btn-mode-view" onclick="setMode('view')">👁 Lihat Saja</button>
<?php endif; ?>
<div class="user-badge">
<?php if ($isAdmin): ?>
👤 <?= htmlspecialchars($currentUser) ?>
<span class="role-tag">ADMIN</span>
<?php elseif ($isLoggedIn): ?>
👤 <?= htmlspecialchars($currentUser) ?>
<span class="role-tag" style="background:#276749">USER</span>
<?php else: ?>
👁 Pengunjung <span class="role-tag" style="background:#4a5568">GUEST</span>
<?php endif; ?>
</div>
<?php if ($isLoggedIn): ?>
<a href="logout.php"><button class="btn-logout">⬅ Logout</button></a>
<?php else: ?>
<a href="login.php"><button class="btn-login-small">🔐 Login</button></a>
<?php endif; ?>
</div>
</div>
<!-- MAIN -->
<div id="main">
<div style="position:relative;flex:1;display:flex;">
<div id="map"></div>
<div id="mode-indicator">Mode: Tambah Tempat Ibadah Klik di peta</div>
</div>
<!-- SIDE PANEL -->
<div id="side-panel">
<!-- STATS -->
<div class="panel-section">
<h3>📊 Statistik</h3>
<div class="stat-grid">
<div class="stat-card">
<div class="stat-num stat-blue" id="stat-ibadah">0</div>
<div class="stat-label">Tempat Ibadah</div>
</div>
<div class="stat-card">
<div class="stat-num stat-yellow" id="stat-rumah">0</div>
<div class="stat-label">Total Rumah</div>
</div>
<div class="stat-card">
<div class="stat-num stat-red" id="stat-miskin">0</div>
<div class="stat-label">Rumah Miskin</div>
</div>
<div class="stat-card">
<div class="stat-num stat-green" id="stat-terjangkau">0</div>
<div class="stat-label">Terjangkau Ibadah</div>
</div>
</div>
</div>
<!-- SELECTED IBADAH -->
<div class="panel-section" id="selected-info">
<h3>🎯 Tempat Ibadah Dipilih</h3>
<div id="selected-name" style="font-weight:600;font-size:13px;margin-bottom:6px;color:#e2e8f0;"></div>
<div id="selected-alamat" style="font-size:11px;color:#718096;margin-bottom:10px;"></div>
<div id="radius-display">0 m</div>
<div id="radius-label">Radius Jangkauan</div>
<div class="slider-wrap">
<input type="range" id="radius-slider" min="50" max="2000" step="50" value="200"
oninput="onSliderMove(this.value)">
<div class="slider-labels"><span>50 m</span><span>2.000 m</span></div>
</div>
<button id="btn-save-radius" onclick="saveRadius()">💾 Simpan Radius</button>
<div style="margin-top:12px;">
<h3 style="margin-bottom:6px;">🏠 Rumah dalam Radius (<span id="rumah-count">0</span>)</h3>
<div id="rumah-in-radius"></div>
</div>
</div>
<!-- LAYER IBADAH -->
<div class="panel-section">
<h3>🗂 Layer Tempat Ibadah</h3>
<div id="layer-ibadah-list"></div>
</div>
<!-- LAYER RUMAH -->
<div class="panel-section">
<h3>🗂 Layer Rumah</h3>
<div class="layer-row">
<span class="layer-icon">🔴</span>
<span class="layer-label">Rumah Miskin</span>
<span class="layer-count" id="count-miskin">0</span>
<label class="toggle"><input type="checkbox" checked onchange="toggleLayer('miskin', this.checked)"><span class="toggle-slider"></span></label>
</div>
<div class="layer-row">
<span class="layer-icon">🟡</span>
<span class="layer-label">Rumah Tidak Miskin</span>
<span class="layer-count" id="count-tidak">0</span>
<label class="toggle"><input type="checkbox" checked onchange="toggleLayer('tidak_miskin', this.checked)"><span class="toggle-slider"></span></label>
</div>
</div>
<!-- LEGEND -->
<div class="panel-section">
<h3>📍 Legenda Rumah</h3>
<div class="legend-row"><div class="legend-dot" style="background:#fc8181"></div> Rumah Miskin (tidak terjangkau)</div>
<div class="legend-row"><div class="legend-dot" style="background:#68d391"></div> Rumah Miskin (dalam radius ibadah)</div>
<div class="legend-row"><div class="legend-dot" style="background:#f6e05e"></div> Rumah Tidak Miskin</div>
</div>
</div><!-- /side-panel -->
</div>
<script>
// ════════════════════════════════════════════════
// ROLE (dikirim dari PHP)
// ════════════════════════════════════════════════
const ROLE = '<?= $roleJS ?>'; // 'admin' | 'guest'
const IS_ADMIN = ROLE === 'admin';
// ════════════════════════════════════════════════
// CONFIG
// ════════════════════════════════════════════════
const JENIS_CONFIG = {
'Masjid' : { emoji: '🕌', color: '#4299e1' },
'Gereja' : { emoji: '⛪', color: '#9f7aea' },
'Pura' : { emoji: '🛕', color: '#ed8936' },
'Vihara' : { emoji: '☸️', color: '#ecc94b' },
'Klenteng' : { emoji: '🏮', color: '#fc8181' },
'Lainnya' : { emoji: '🙏', color: '#68d391' },
};
const JENIS_LIST = Object.keys(JENIS_CONFIG);
// ════════════════════════════════════════════════
// STATE
// ════════════════════════════════════════════════
let mode = IS_ADMIN ? 'ibadah' : 'view';
let ibadahData = [];
let rumahData = [];
let ibadahMarkers = {};
let rumahMarkers = {};
let layerGroups = {};
JENIS_LIST.forEach(j => { layerGroups[j] = L.layerGroup(); });
let layerRumahMiskin = L.layerGroup();
let layerRumahTidakMiskin = L.layerGroup();
let selectedIbadahId = null;
let activeCircle = null;
let highlightedRumah = [];
// ════════════════════════════════════════════════
// MAP SETUP
// ════════════════════════════════════════════════
const map = L.map('map').setView([-0.055326, 109.349500], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19, attribution: '©OpenStreetMap contributors'
}).addTo(map);
JENIS_LIST.forEach(j => layerGroups[j].addTo(map));
layerRumahMiskin.addTo(map);
layerRumahTidakMiskin.addTo(map);
// ════════════════════════════════════════════════
// LAYER PANEL
// ════════════════════════════════════════════════
function buildLayerPanel() {
const el = document.getElementById('layer-ibadah-list');
el.innerHTML = '';
JENIS_LIST.forEach(j => {
const cfg = JENIS_CONFIG[j];
const count = ibadahData.filter(d => d.jenis === j).length;
el.innerHTML += `
<div class="layer-row">
<span class="layer-icon">${cfg.emoji}</span>
<span class="layer-label">${j}</span>
<span class="layer-count" id="lcount-${j}">${count}</span>
<label class="toggle">
<input type="checkbox" checked onchange="toggleIbadahLayer('${j}', this.checked)">
<span class="toggle-slider"></span>
</label>
</div>`;
});
}
function toggleIbadahLayer(jenis, visible) {
if (visible) map.addLayer(layerGroups[jenis]);
else map.removeLayer(layerGroups[jenis]);
}
function toggleLayer(status, visible) {
if (status === 'miskin') {
if (visible) map.addLayer(layerRumahMiskin);
else map.removeLayer(layerRumahMiskin);
} else {
if (visible) map.addLayer(layerRumahTidakMiskin);
else map.removeLayer(layerRumahTidakMiskin);
}
}
// ════════════════════════════════════════════════
// MODE
// ════════════════════════════════════════════════
function setMode(m) {
if (!IS_ADMIN) return; // guest tidak bisa ubah mode
mode = m;
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
const activeBtn = document.getElementById('btn-mode-' + m);
if (activeBtn) activeBtn.classList.add('active');
const ind = document.getElementById('mode-indicator');
if (m === 'view') {
ind.classList.remove('visible');
map.getContainer().style.cursor = '';
} else {
const labels = { ibadah: 'Tambah Tempat Ibadah', rumah: 'Tambah Rumah' };
ind.textContent = `Mode: ${labels[m]} Klik di peta`;
ind.classList.add('visible');
map.getContainer().style.cursor = 'crosshair';
}
}
// ════════════════════════════════════════════════
// ICON FACTORY
// ════════════════════════════════════════════════
function makeIbadahIcon(jenis) {
const cfg = JENIS_CONFIG[jenis] || JENIS_CONFIG['Lainnya'];
return L.divIcon({
html: `<div class="icon-ibadah" style="filter:drop-shadow(0 2px 4px rgba(0,0,0,.9))">${cfg.emoji}</div>`,
className: '', iconSize: [32,32], iconAnchor: [16,28], popupAnchor: [0,-30]
});
}
function makeRumahIcon(status, highlighted) {
let color = status === 'miskin' ? (highlighted ? '#68d391' : '#fc8181') : '#f6e05e';
return { radius: 7, fillColor: color, color: '#fff', weight: 1.5, opacity: 1, fillOpacity: 0.9 };
}
// ════════════════════════════════════════════════
// LOAD & RENDER
// ════════════════════════════════════════════════
function loadAll() {
Promise.all([
fetch('ambil.php').then(r => r.json()),
fetch('ambil_rumah.php').then(r => r.json()).catch(() => [])
]).then(([ibadah, rumah]) => {
ibadahData = ibadah;
rumahData = rumah;
renderAll();
});
}
function renderAll() {
JENIS_LIST.forEach(j => layerGroups[j].clearLayers());
layerRumahMiskin.clearLayers();
layerRumahTidakMiskin.clearLayers();
ibadahMarkers = {}; rumahMarkers = {}; highlightedRumah = [];
ibadahData.forEach(addIbadahMarker);
rumahData.forEach(r => addRumahMarker(r, false));
updateStats();
buildLayerPanel();
}
function addIbadahMarker(item) {
const marker = L.marker([item.lat, item.lng], { icon: makeIbadahIcon(item.jenis) });
marker.bindPopup(buildIbadahPopup(item), { maxWidth: 260 });
marker.on('click', () => selectIbadah(item.id));
layerGroups[item.jenis] = layerGroups[item.jenis] || L.layerGroup().addTo(map);
layerGroups[item.jenis].addLayer(marker);
ibadahMarkers[item.id] = { marker };
}
function buildIbadahPopup(item) {
const cfg = JENIS_CONFIG[item.jenis] || JENIS_CONFIG['Lainnya'];
let actions = `<button class="popup-btn btn-focus" onclick="selectIbadah(${item.id})">🎯 Fokus</button>`;
if (IS_ADMIN) {
actions += `<button class="popup-btn btn-edit" onclick="editIbadah(${item.id})">✏️ Edit</button>`;
actions += `<button class="popup-btn btn-del" onclick="hapusIbadah(${item.id})">🗑️</button>`;
}
return `
<div class="popup-title">${cfg.emoji} ${item.jenis}${item.nama ? ' ' + item.nama : ''}</div>
<div class="popup-row">📍 ${item.alamat || '-'}</div>
<div class="popup-row">📏 Radius: <b>${item.radius} m</b></div>
<div class="popup-actions">${actions}</div>`;
}
function addRumahMarker(r, highlighted) {
const opts = makeRumahIcon(r.status, highlighted);
const cm = L.circleMarker([r.lat, r.lng], opts);
cm.bindPopup(buildRumahPopup(r), { maxWidth: 280 });
if (r.status === 'miskin') layerRumahMiskin.addLayer(cm);
else layerRumahTidakMiskin.addLayer(cm);
rumahMarkers[r.id] = cm;
}
function buildRumahPopup(r) {
const statusColor = r.status === 'miskin' ? '#fc8181' : '#f6e05e';
const statusLabel = r.status === 'miskin' ? 'Miskin' : 'Tidak Miskin';
let rows = '';
if (IS_ADMIN) {
// Admin: tampilkan data lengkap
rows = `
<div class="popup-row">🪪 NIK: <b>${r.nik || '-'}</b></div>
<div class="popup-row">👤 Nama: <b>${r.nama || '-'}</b></div>
<div class="popup-row">📍 ${r.alamat || '-'}</div>
<div class="popup-row">🎂 TTL: ${r.ttl || '-'}</div>
<div class="popup-row">🎓 Pendidikan: ${r.pendidikan || '-'}</div>
<div class="popup-row">Status: <b style="color:${statusColor}">${statusLabel}</b></div>`;
} else {
// Guest: NIK, nama, alamat, status
rows = `
<div class="popup-row">🪪 NIK: <b>${r.nik || '-'}</b></div>
<div class="popup-row">👤 ${r.nama || '-'}</div>
<div class="popup-row">📍 ${r.alamat || '-'}</div>
<div class="popup-row">Status: <b style="color:${statusColor}">${statusLabel}</b></div>`;
}
let actions = '';
if (IS_ADMIN) {
actions = `
<div class="popup-actions">
<button class="popup-btn btn-edit" onclick="editRumah(${r.id})">✏️ Edit</button>
<button class="popup-btn btn-del" onclick="hapusRumah(${r.id})">🗑️</button>
</div>`;
}
return `<div class="popup-title">🏠 Rumah</div>${rows}${actions}`;
}
// ════════════════════════════════════════════════
// SELECT IBADAH
// ════════════════════════════════════════════════
function selectIbadah(id) {
if (activeCircle) { map.removeLayer(activeCircle); activeCircle = null; }
resetRumahColors();
const item = ibadahData.find(d => d.id == id);
if (!item) return;
selectedIbadahId = id;
const cfg = JENIS_CONFIG[item.jenis] || JENIS_CONFIG['Lainnya'];
activeCircle = L.circle([item.lat, item.lng], {
radius: item.radius, color: cfg.color, fillColor: cfg.color,
fillOpacity: 0.12, weight: 2, dashArray: '6,4'
}).addTo(map);
map.flyTo([item.lat, item.lng], Math.max(map.getZoom(), 14), { duration: 0.8 });
const panel = document.getElementById('selected-info');
panel.classList.add('visible');
document.getElementById('selected-name').textContent = cfg.emoji + ' ' + item.jenis + (item.nama ? ' ' + item.nama : '');
document.getElementById('selected-alamat').textContent = item.alamat || '';
document.getElementById('radius-display').textContent = item.radius + ' m';
document.getElementById('radius-slider').value = item.radius;
highlightRumahInRadius(item);
}
function highlightRumahInRadius(item) {
highlightedRumah = [];
const inRadius = [];
rumahData.forEach(r => {
const dist = getDistanceMeters(item.lat, item.lng, r.lat, r.lng);
if (dist <= item.radius) {
highlightedRumah.push(r.id);
if (r.status === 'miskin') inRadius.push({ ...r, dist: Math.round(dist) });
if (rumahMarkers[r.id]) rumahMarkers[r.id].setStyle(makeRumahIcon(r.status, true));
}
});
document.getElementById('rumah-count').textContent = highlightedRumah.length;
const listEl = document.getElementById('rumah-in-radius');
if (inRadius.length === 0) {
listEl.innerHTML = '<div style="font-size:12px;color:#718096;padding:6px 0;">Tidak ada rumah miskin dalam radius.</div>';
} else {
listEl.innerHTML = inRadius.map(r =>
`<div class="rumah-item">
<div class="dot dot-aman"></div>
<div>${(r.nama || r.alamat || 'Rumah #' + r.id).substring(0,40)} <span style="color:#4a5568">(${r.dist} m)</span></div>
</div>`
).join('');
}
updateStats();
}
function resetRumahColors() {
highlightedRumah.forEach(id => {
const r = rumahData.find(x => x.id == id);
if (r && rumahMarkers[id]) rumahMarkers[id].setStyle(makeRumahIcon(r.status, false));
});
highlightedRumah = [];
}
// ════════════════════════════════════════════════
// SLIDER
// ════════════════════════════════════════════════
function onSliderMove(val) {
document.getElementById('radius-display').textContent = val + ' m';
if (activeCircle) activeCircle.setRadius(parseInt(val));
if (selectedIbadahId !== null) {
resetRumahColors();
const item = ibadahData.find(d => d.id == selectedIbadahId);
if (item) highlightRumahInRadius({ ...item, radius: parseInt(val) });
}
}
function saveRadius() {
if (selectedIbadahId === null) return;
const newRadius = parseInt(document.getElementById('radius-slider').value);
// Guest hanya update tampilan lokal, tidak simpan ke DB
if (!IS_ADMIN) {
const item = ibadahData.find(d => d.id == selectedIbadahId);
if (item) {
item.radius = newRadius;
if (ibadahMarkers[selectedIbadahId])
ibadahMarkers[selectedIbadahId].marker.setPopupContent(buildIbadahPopup(item));
}
showToast('👁 Radius diubah (tidak disimpan — hanya tampilan)');
return;
}
fetch('edit.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `id=${selectedIbadahId}&radius=${newRadius}&_only_radius=1`
})
.then(r => r.json())
.then(res => {
if (res.status === 'success') {
const item = ibadahData.find(d => d.id == selectedIbadahId);
if (item) {
item.radius = newRadius;
if (ibadahMarkers[selectedIbadahId])
ibadahMarkers[selectedIbadahId].marker.setPopupContent(buildIbadahPopup(item));
}
showToast('✅ Radius berhasil disimpan');
} else {
showToast('❌ Gagal menyimpan radius');
}
});
}
// ════════════════════════════════════════════════
// MAP CLICK → ADD MARKER (admin only)
// ════════════════════════════════════════════════
map.on('click', async function(e) {
if (!IS_ADMIN || mode === 'view') return;
const lat = e.latlng.lat;
const lng = e.latlng.lng;
const alamat = await getAddress(lat, lng);
if (mode === 'ibadah') {
const jenis = await showJenisDialog();
if (!jenis) return;
const nama = prompt(`Nama ${jenis} (opsional):`) || '';
let radius = prompt('Masukkan radius awal (meter):', 300);
radius = parseInt(radius) || 300;
fetch('simpan.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `jenis=${encodeURIComponent(jenis)}&nama=${encodeURIComponent(nama)}&lat=${lat}&lng=${lng}&radius=${radius}&alamat=${encodeURIComponent(alamat)}`
})
.then(r => r.json())
.then(res => {
if (res.status === 'success') { showToast('✅ Tempat ibadah disimpan!'); loadAll(); }
else showToast('❌ Gagal simpan: ' + (res.message || ''));
});
} else if (mode === 'rumah') {
showFormRumah(lat, lng, alamat);
}
});
// ════════════════════════════════════════════════
// FORM RUMAH (tambah data lengkap)
// ════════════════════════════════════════════════
function showFormRumah(lat, lng, alamat, existingData) {
const isEdit = !!existingData;
const d = existingData || {};
const overlay = document.createElement('div');
overlay.className = 'dialog-overlay';
overlay.innerHTML = `
<div class="dialog-box">
<div class="dialog-title">${isEdit ? '✏️ Edit Data Rumah' : '🏠 Tambah Data Rumah'}</div>
<div class="form-row">
<label>NIK *</label>
<input type="text" id="frm-nik" placeholder="16 digit NIK" maxlength="16" value="${d.nik || ''}">
</div>
<div class="form-row">
<label>Nama Kepala Keluarga *</label>
<input type="text" id="frm-nama" placeholder="Nama lengkap" value="${d.nama || ''}">
</div>
<div class="form-row">
<label>Alamat</label>
<textarea id="frm-alamat" rows="2">${d.alamat || alamat || ''}</textarea>
<div class="hint">💡 Terisi otomatis dari titik yang diklik. Bisa diedit manual.</div>
</div>
<div class="form-row">
<label>Tempat, Tanggal Lahir</label>
<input type="text" id="frm-ttl" placeholder="Contoh: Pontianak, 01 Januari 1990" value="${d.ttl || ''}">
</div>
<div class="form-row">
<label>Pendidikan Terakhir</label>
<select id="frm-pendidikan">
${['Tidak Sekolah','SD/Sederajat','SMP/Sederajat','SMA/Sederajat','D3','S1','S2/S3'].map(p =>
`<option value="${p}" ${(d.pendidikan||'')==p?'selected':''}>${p}</option>`
).join('')}
</select>
</div>
<div class="form-row">
<label>Status Kemiskinan</label>
<div style="display:flex;gap:8px;margin-top:4px;">
<button id="frm-btn-miskin" onclick="setStatusBtn('miskin')"
style="flex:1;padding:9px;border-radius:7px;border:1px solid #c53030;cursor:pointer;font-size:12px;font-weight:700;transition:.2s;
background:${(!d.status||d.status==='miskin')?'#742a2a':'#0f1117'};color:${(!d.status||d.status==='miskin')?'#fc8181':'#718096'}">
🔴 Miskin
</button>
<button id="frm-btn-tidak" onclick="setStatusBtn('tidak_miskin')"
style="flex:1;padding:9px;border-radius:7px;border:1px solid #276749;cursor:pointer;font-size:12px;font-weight:700;transition:.2s;
background:${d.status==='tidak_miskin'?'#1c4532':'#0f1117'};color:${d.status==='tidak_miskin'?'#68d391':'#718096'}">
🟡 Tidak Miskin
</button>
</div>
</div>
<input type="hidden" id="frm-status" value="${d.status || 'miskin'}">
<div class="dialog-actions">
<button class="btn-cancel" onclick="this.closest('.dialog-overlay').remove()">Batal</button>
<button class="btn-primary" onclick="submitFormRumah(${lat}, ${lng}, ${isEdit ? d.id : 'null'})">
${isEdit ? '💾 Simpan Perubahan' : '📍 Simpan Rumah'}
</button>
</div>
</div>`;
document.body.appendChild(overlay);
}
function setStatusBtn(val) {
document.getElementById('frm-status').value = val;
const bm = document.getElementById('frm-btn-miskin');
const bt = document.getElementById('frm-btn-tidak');
if (val === 'miskin') {
bm.style.background = '#742a2a'; bm.style.color = '#fc8181';
bt.style.background = '#0f1117'; bt.style.color = '#718096';
} else {
bt.style.background = '#1c4532'; bt.style.color = '#68d391';
bm.style.background = '#0f1117'; bm.style.color = '#718096';
}
}
function submitFormRumah(lat, lng, editId) {
const nik = document.getElementById('frm-nik').value.trim();
const nama = document.getElementById('frm-nama').value.trim();
const alamat = document.getElementById('frm-alamat').value.trim();
const ttl = document.getElementById('frm-ttl').value.trim();
const pendidikan = document.getElementById('frm-pendidikan').value;
const status = document.getElementById('frm-status').value;
if (!nik || !nama) { showToast('⚠️ NIK dan Nama wajib diisi!'); return; }
const isEdit = editId !== null;
const url = isEdit ? 'edit_rumah.php' : 'simpan_rumah.php';
let body = `nik=${encodeURIComponent(nik)}&nama=${encodeURIComponent(nama)}&alamat=${encodeURIComponent(alamat)}&ttl=${encodeURIComponent(ttl)}&pendidikan=${encodeURIComponent(pendidikan)}&status=${status}`;
if (isEdit) body += `&id=${editId}`;
else body += `&lat=${lat}&lng=${lng}`;
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
})
.then(r => r.json())
.then(res => {
if (res.status === 'success') {
showToast(isEdit ? '✅ Data rumah diperbarui!' : '✅ Rumah disimpan!');
document.querySelector('.dialog-overlay')?.remove();
loadAll();
} else {
showToast('❌ Gagal: ' + (res.message || ''));
}
});
}
// ════════════════════════════════════════════════
// EDIT / HAPUS IBADAH
// ════════════════════════════════════════════════
function editIbadah(id) {
const item = ibadahData.find(d => d.id == id);
if (!item) return;
const jenisBaru = prompt('Edit jenis:', item.jenis);
if (!jenisBaru) return;
const namaBaru = prompt('Edit nama:', item.nama || '');
let radiusBaru = prompt('Edit radius:', item.radius);
radiusBaru = parseInt(radiusBaru) || item.radius;
fetch('edit.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `id=${id}&jenis=${encodeURIComponent(jenisBaru)}&nama=${encodeURIComponent(namaBaru)}&radius=${radiusBaru}`
})
.then(r => r.json())
.then(res => {
if (res.status === 'success') { showToast('✅ Data diupdate!'); loadAll(); }
});
}
function hapusIbadah(id) {
if (!confirm('Yakin hapus tempat ibadah ini?')) return;
fetch('hapus.php', {
method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `id=${id}`
})
.then(r => r.json())
.then(res => {
if (res.status === 'success') {
showToast('🗑️ Data dihapus!');
if (activeCircle) { map.removeLayer(activeCircle); activeCircle = null; }
document.getElementById('selected-info').classList.remove('visible');
selectedIbadahId = null;
loadAll();
}
});
}
// ════════════════════════════════════════════════
// EDIT / HAPUS RUMAH
// ════════════════════════════════════════════════
function editRumah(id) {
const r = rumahData.find(x => x.id == id);
if (!r) return;
document.querySelector('.leaflet-popup-close-button')?.click(); // tutup popup
showFormRumah(r.lat, r.lng, r.alamat, r);
}
function hapusRumah(id) {
if (!confirm('Hapus data rumah ini?')) return;
fetch('hapus_rumah.php', {
method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `id=${id}`
})
.then(r => r.json())
.then(res => {
if (res.status === 'success') { showToast('🗑️ Rumah dihapus!'); loadAll(); }
});
}
// ════════════════════════════════════════════════
// DIALOG JENIS IBADAH
// ════════════════════════════════════════════════
function showJenisDialog() {
return new Promise(resolve => {
const overlay = document.createElement('div');
overlay.className = 'dialog-overlay';
overlay.innerHTML = `
<div class="dialog-box" style="min-width:280px;max-width:320px;">
<div class="dialog-title">Pilih Jenis Tempat Ibadah</div>
${JENIS_LIST.map(j => `
<button data-jenis="${j}"
style="display:block;width:100%;margin-bottom:8px;padding:9px 14px;background:#2d3748;border:1px solid #4a5568;
border-radius:7px;color:#e2e8f0;font-size:13px;cursor:pointer;text-align:left;">
${JENIS_CONFIG[j].emoji} ${j}
</button>`).join('')}
<button data-jenis=""
style="width:100%;padding:7px;background:transparent;border:1px solid #4a5568;border-radius:7px;color:#718096;font-size:12px;cursor:pointer;margin-top:2px;">
Batal
</button>
</div>`;
overlay.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', () => {
resolve(btn.dataset.jenis || null);
document.body.removeChild(overlay);
});
});
document.body.appendChild(overlay);
});
}
// ════════════════════════════════════════════════
// HELPERS
// ════════════════════════════════════════════════
function getAddress(lat, lng) {
return fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`)
.then(r => r.json())
.then(d => d.display_name || 'Alamat tidak ditemukan')
.catch(() => 'Gagal ambil alamat');
}
function getDistanceMeters(lat1, lng1, lat2, lng2) {
const R = 6371000;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*Math.sin(dLng/2)**2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
function updateStats() {
document.getElementById('stat-ibadah').textContent = ibadahData.length;
document.getElementById('stat-rumah').textContent = rumahData.length;
const miskin = rumahData.filter(r => r.status === 'miskin');
document.getElementById('stat-miskin').textContent = miskin.length;
document.getElementById('count-miskin').textContent = miskin.length;
document.getElementById('count-tidak').textContent = rumahData.filter(r => r.status !== 'miskin').length;
const terjangkau = new Set();
ibadahData.forEach(item => {
miskin.forEach(r => {
if (getDistanceMeters(item.lat, item.lng, r.lat, r.lng) <= item.radius) terjangkau.add(r.id);
});
});
document.getElementById('stat-terjangkau').textContent = terjangkau.size;
}
let toastTimer;
function showToast(msg) {
let t = document.getElementById('toast');
if (!t) {
t = document.createElement('div');
t.id = 'toast';
t.style.cssText = `position:fixed;bottom:20px;left:50%;transform:translateX(-50%);
background:#1a202c;border:1px solid #2d3748;color:#e2e8f0;
padding:9px 18px;border-radius:20px;font-size:13px;
z-index:9999;box-shadow:0 4px 20px rgba(0,0,0,.4);transition:opacity .3s;`;
document.body.appendChild(t);
}
t.textContent = msg;
t.style.opacity = '1';
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { t.style.opacity = '0'; }, 2500);
}
// ════════════════════════════════════════════════
// INIT
// ════════════════════════════════════════════════
setMode(IS_ADMIN ? 'ibadah' : 'view');
loadAll();
</script>
</body>
</html>
+30
View File
@@ -0,0 +1,30 @@
<?php
header('Content-Type: application/json');
require_once 'auth.php';
require_admin_api();
require_once 'config.php';
$jenis = $_POST['jenis'] ?? '';
$nama = $_POST['nama'] ?? '';
$lat = $_POST['lat'] ?? '';
$lng = $_POST['lng'] ?? '';
$radius = $_POST['radius'] ?? '';
$alamat = $_POST['alamat'] ?? '';
if ($jenis == '' || $lat == '' || $lng == '' || $radius == '') {
echo json_encode(["status" => "error", "message" => "Data tidak lengkap"]);
exit;
}
$stmt = mysqli_prepare($koneksi,
"INSERT INTO tempat_ibadah (jenis, nama, lat, lng, radius, alamat) VALUES (?, ?, ?, ?, ?, ?)"
);
mysqli_stmt_bind_param($stmt, "ssddis", $jenis, $nama, $lat, $lng, $radius, $alamat);
if (mysqli_stmt_execute($stmt)) {
echo json_encode(["status" => "success", "message" => "Data berhasil disimpan"]);
} else {
echo json_encode(["status" => "error", "message" => mysqli_error($koneksi)]);
}
mysqli_close($koneksi);
+36
View File
@@ -0,0 +1,36 @@
<?php
header('Content-Type: application/json');
require_once 'auth.php';
require_admin_api();
require_once 'config.php';
$lat = $_POST['lat'] ?? '';
$lng = $_POST['lng'] ?? '';
$status = $_POST['status'] ?? 'miskin';
$alamat = $_POST['alamat'] ?? '';
$nik = $_POST['nik'] ?? '';
$nama = $_POST['nama'] ?? '';
$ttl = $_POST['ttl'] ?? '';
$pendidikan = $_POST['pendidikan'] ?? '';
if ($lat == '' || $lng == '' || $nik == '' || $nama == '') {
echo json_encode(["status" => "error", "message" => "Data tidak lengkap (NIK dan Nama wajib diisi)"]);
exit;
}
if (!in_array($status, ['miskin', 'tidak_miskin'])) {
$status = 'miskin';
}
$stmt = mysqli_prepare($koneksi,
"INSERT INTO rumah (lat, lng, status, alamat, nik, nama, ttl, pendidikan) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
);
mysqli_stmt_bind_param($stmt, "ddssssss", $lat, $lng, $status, $alamat, $nik, $nama, $ttl, $pendidikan);
if (mysqli_stmt_execute($stmt)) {
echo json_encode(["status" => "success", "id" => mysqli_insert_id($koneksi)]);
} else {
echo json_encode(["status" => "error", "message" => mysqli_error($koneksi)]);
}
mysqli_close($koneksi);
+9
View File
@@ -0,0 +1,9 @@
<?php
$conn = mysqli_connect("127.0.0.1", "root", "", "gis_kemiskinan", 3307);
if ($conn) {
echo "Koneksi berhasil!";
} else {
echo "Gagal koneksi!";
}
?>