feat: complete WebGIS implementation with nominal bantuan and cache busting

This commit is contained in:
fananazril
2026-06-11 19:18:25 +07:00
parent 0d59e629bd
commit 0ae2768849
36 changed files with 7687 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
<?php
// ================================================================
// Auth Helper
// ================================================================
require_once __DIR__ . '/../config/config.php';
if (session_status() === PHP_SESSION_NONE) {
session_name(SESSION_NAME);
session_start();
}
function isLoggedIn(): bool {
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
}
function requireLogin(): void {
if (!isLoggedIn()) {
header('Location: ' . BASE_URL . '/admin/login.php');
exit;
}
}
function requireAdmin(): void {
requireLogin();
if (getCurrentUser()['role'] !== 'admin') {
http_response_code(403);
die('<h2>Akses Ditolak</h2><p>Anda tidak memiliki izin untuk mengakses halaman ini.</p>');
}
}
function getCurrentUser(): ?array {
if (!isLoggedIn()) return null;
return [
'id' => $_SESSION['user_id'],
'nama' => $_SESSION['user_nama'] ?? '',
'email' => $_SESSION['user_email'] ?? '',
'role' => $_SESSION['user_role'] ?? 'operator',
];
}
function loginUser(array $user): void {
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_nama'] = $user['nama'];
$_SESSION['user_email'] = $user['email'];
$_SESSION['user_role'] = $user['role'];
// Also store as array for API compatibility
$_SESSION['user'] = [
'id' => $user['id'],
'nama' => $user['nama'],
'email' => $user['email'],
'role' => $user['role'],
];
}
function logoutUser(): void {
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$p = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']);
}
session_destroy();
}
+5
View File
@@ -0,0 +1,5 @@
</main>
</div><!-- .admin-layout -->
<?= $extraFooter ?? '' ?>
</body>
</html>
+128
View File
@@ -0,0 +1,128 @@
<?php
// ================================================================
// Helper Functions
// ================================================================
require_once __DIR__ . '/../config/database.php';
/**
* JSON response helper
*/
function jsonResponse(array $data, int $code = 200): void {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
/**
* Haversine formula: jarak antara dua koordinat (meter)
*/
function haversineDistance(float $lat1, float $lng1, float $lat2, float $lng2): float {
$R = 6371000; // radius bumi meter
$dLat = deg2rad($lat2 - $lat1);
$dLng = deg2rad($lng2 - $lng1);
$a = sin($dLat / 2) * sin($dLat / 2)
+ cos(deg2rad($lat1)) * cos(deg2rad($lat2))
* sin($dLng / 2) * sin($dLng / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return $R * $c;
}
/**
* Cari rumah ibadah terdekat dari koordinat
*/
function findNearestIbadah(float $lat, float $lng): ?array {
$db = getDB();
$rows = $db->query("SELECT id, nama, jenis, lat, lng, radius FROM rumah_ibadah")->fetchAll();
$best = null;
$minDist = PHP_FLOAT_MAX;
foreach ($rows as $row) {
$dist = haversineDistance($lat, $lng, (float)$row['lat'], (float)$row['lng']);
if ($dist <= (float)$row['radius'] && $dist < $minDist) {
$minDist = $dist;
$best = array_merge($row, ['jarak' => round($dist, 2)]);
}
}
// Jika tidak ada dalam radius, cari yang paling dekat saja
if ($best === null && !empty($rows)) {
foreach ($rows as $row) {
$dist = haversineDistance($lat, $lng, (float)$row['lat'], (float)$row['lng']);
if ($dist < $minDist) {
$minDist = $dist;
$best = array_merge($row, ['jarak' => round($dist, 2), 'di_luar_radius' => true]);
}
}
}
return $best;
}
/**
* Sanitize string input
*/
function sanitize(string $val): string {
return trim(htmlspecialchars($val, ENT_QUOTES, 'UTF-8'));
}
/**
* Format radius display
*/
function formatRadius(int $meter): string {
return $meter >= 1000 ? number_format($meter / 1000, 1) . ' km' : $meter . ' m';
}
/**
* Format tanggal Indonesia
*/
function formatTanggal(?string $date): string {
if (!$date) return '-';
$ts = strtotime($date);
$bulan = ['','Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agt','Sep','Okt','Nov','Des'];
return date('d', $ts) . ' ' . $bulan[(int)date('n', $ts)] . ' ' . date('Y', $ts);
}
/**
* Handle file upload
*/
function handleUpload(array $file, string $prefix = 'bukti'): ?string {
if ($file['error'] !== UPLOAD_ERR_OK) return null;
if ($file['size'] > UPLOAD_MAX_SIZE) return null;
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, UPLOAD_ALLOWED)) return null;
if (!is_dir(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0755, true);
$filename = $prefix . '_' . date('Ymd_His') . '_' . uniqid() . '.' . $ext;
$dest = UPLOAD_DIR . $filename;
if (move_uploaded_file($file['tmp_name'], $dest)) return $filename;
return null;
}
/**
* Emoji dan label untuk jenis ibadah
*/
function ibadahEmoji(string $jenis): string {
$map = [
'Masjid' => '🕌',
'Musholla' => '🕌',
'Gereja' => '⛪',
'Katolik' => '⛪',
'Pura' => '🛕',
'Vihara' => '🛕',
'Klenteng' => '⛩️',
'Lainnya' => '🙏',
];
return $map[$jenis] ?? '🏛️';
}
/**
* Color untuk marker berdasarkan status
*/
function statusColor(string $status): string {
return match($status) {
'sudah' => '#22c55e',
'menunggu' => '#f59e0b',
default => '#ef4444',
};
}
+94
View File
@@ -0,0 +1,94 @@
<?php
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/auth.php';
$user = getCurrentUser();
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= isset($pageTitle) ? htmlspecialchars($pageTitle) . ' — ' : '' ?><?= APP_NAME ?></title>
<meta name="description" content="WebGIS Sebaran Penduduk Miskin — Sistem pemetaan interaktif rumah ibadah dan penduduk miskin berbasis web.">
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<!-- SweetAlert2 CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
<!-- Admin CSS -->
<link rel="stylesheet" href="<?= BASE_URL ?>/assets/css/admin.css?v=<?= time() ?>">
<?= $extraHead ?? '' ?>
</head>
<body>
<!-- Bootstrap 5 JS (di head agar tersedia untuk modal) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- SweetAlert2 JS -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- ══ NAVBAR ══════════════════════════════════════════════ -->
<nav class="admin-navbar">
<a href="<?= BASE_URL ?>/admin/" class="navbar-brand">
<span class="brand-icon">🗺️</span>
<span><?= APP_NAME ?></span>
</a>
<div class="navbar-right">
<a href="<?= BASE_URL ?>/" target="_blank" class="btn-nav-link" title="Lihat Peta">
<i class="bi bi-map"></i> Buka Peta
</a>
<?php if ($user): ?>
<div class="user-badge">
<span class="user-role-dot <?= $user['role'] ?>"></span>
<span><?= htmlspecialchars($user['nama']) ?></span>
<span class="role-pill"><?= ucfirst($user['role']) ?></span>
</div>
<a href="<?= BASE_URL ?>/admin/logout.php" class="btn-nav-link danger" title="Logout">
<i class="bi bi-box-arrow-right"></i>
</a>
<?php endif; ?>
</div>
</nav>
<div class="admin-layout">
<!-- ══ SIDEBAR ══════════════════════════════════════════════ -->
<aside class="admin-sidebar" id="admin-sidebar">
<nav class="sidebar-nav">
<div class="nav-section-label">Menu</div>
<a href="<?= BASE_URL ?>/admin/" class="nav-item <?= (basename($_SERVER['PHP_SELF']) === 'index.php' && strpos($_SERVER['REQUEST_URI'], '/admin/') !== false) ? 'active' : '' ?>">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
<a href="<?= BASE_URL ?>/admin/rumah_ibadah.php" class="nav-item <?= (basename($_SERVER['PHP_SELF']) === 'rumah_ibadah.php') ? 'active' : '' ?>">
<i class="bi bi-building"></i> Rumah Ibadah
</a>
<a href="<?= BASE_URL ?>/admin/penduduk_miskin.php" class="nav-item <?= (basename($_SERVER['PHP_SELF']) === 'penduduk_miskin.php') ? 'active' : '' ?>">
<i class="bi bi-people"></i> Penduduk Miskin
</a>
<?php if ($user && $user['role'] === 'admin'): ?>
<div class="nav-section-label" style="margin-top:20px;">Admin</div>
<a href="<?= BASE_URL ?>/admin/import.php" class="nav-item <?= (basename($_SERVER['PHP_SELF']) === 'import.php') ? 'active' : '' ?>">
<i class="bi bi-upload"></i> Import Data
</a>
<a href="<?= BASE_URL ?>/admin/export.php" class="nav-item <?= (basename($_SERVER['PHP_SELF']) === 'export.php') ? 'active' : '' ?>">
<i class="bi bi-download"></i> Export Data
</a>
<a href="<?= BASE_URL ?>/admin/users.php" class="nav-item <?= (basename($_SERVER['PHP_SELF']) === 'users.php') ? 'active' : '' ?>">
<i class="bi bi-person-gear"></i> Pengguna
</a>
<?php endif; ?>
<div class="nav-section-label" style="margin-top:20px;">Lainnya</div>
<a href="<?= BASE_URL ?>/" target="_blank" class="nav-item">
<i class="bi bi-map-fill"></i> Lihat Peta
</a>
<a href="<?= BASE_URL ?>/admin/logout.php" class="nav-item danger">
<i class="bi bi-box-arrow-right"></i> Logout
</a>
</nav>
</aside>
<!-- ══ MAIN CONTENT ══════════════════════════════════════════ -->
<main class="admin-content">
+86
View File
@@ -0,0 +1,86 @@
<?php // Modal: Tambah / Edit Rumah Ibadah ?>
<div class="modal-overlay" id="modal-ibadah">
<div class="modal-card">
<!-- Header -->
<div class="modal-header" style="background:linear-gradient(135deg,rgba(91,127,255,0.12),rgba(124,91,255,0.08));border-bottom:1px solid rgba(91,127,255,0.2);">
<div class="modal-title" id="modal-ibadah-title">🏛️ Tambah Rumah Ibadah</div>
<button class="modal-close" onclick="closeModalIbadah()" type="button" title="Tutup"></button>
</div>
<!-- Body -->
<div class="modal-body">
<input type="hidden" id="ibadah-edit-id">
<!-- Lokasi -->
<div class="section-label">📍 Lokasi</div>
<div class="form-group">
<div class="addr-preview" id="ibadah-coord-display" style="font-family:monospace;font-size:11px;">Lat: Lng: </div>
<div class="addr-preview" id="ibadah-addr-display" style="margin-top:6px;">
<span class="addr-loading">Pilih titik di peta terlebih dahulu...</span>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Kelurahan</label>
<input class="form-control" id="ibadah-kelurahan" placeholder="Kelurahan / Desa">
</div>
<div class="form-group">
<label class="form-label">Kecamatan</label>
<input class="form-control" id="ibadah-kecamatan" placeholder="Kecamatan">
</div>
</div>
<!-- Data Rumah Ibadah -->
<div class="section-label">🕌 Data Rumah Ibadah</div>
<div class="form-group">
<label class="form-label">Nama Rumah Ibadah <span style="color:var(--red)">*</span></label>
<input class="form-control" id="ibadah-nama" placeholder="Mis. Masjid Al-Ikhlas">
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Jenis <span style="color:var(--red)">*</span></label>
<select class="form-control" id="ibadah-jenis">
<option value="Masjid">🕌 Masjid</option>
<option value="Musholla">🕌 Musholla</option>
<option value="Gereja"> Gereja</option>
<option value="Katolik"> Gereja Katolik</option>
<option value="Pura">🛕 Pura</option>
<option value="Vihara">🛕 Vihara</option>
<option value="Klenteng">⛩️ Klenteng</option>
<option value="Lainnya">🙏 Lainnya</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Kontak / Telepon</label>
<input class="form-control" id="ibadah-kontak" placeholder="0561-xxx / 08xx-xxxx">
</div>
</div>
<!-- Radius -->
<div class="section-label">📏 Radius Layanan</div>
<div class="form-group">
<div class="range-wrapper">
<input type="range" id="ibadah-radius" min="100" max="5000" step="50" value="300"
style="flex:1;accent-color:var(--accent);">
<span class="range-val" id="ibadah-radius-val">300 m</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:9px;color:var(--text3);margin-top:3px;">
<span>Min: 100 m</span><span>Default: 300 m</span><span>Max: 5 km</span>
</div>
</div>
</div><!-- /.modal-body -->
<!-- Footer -->
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModalIbadah()" type="button">
<i class="bi bi-x-lg"></i> Batal
</button>
<button class="btn btn-primary" id="btn-save-ibadah" type="button">
<i class="bi bi-floppy2-fill"></i> Simpan
</button>
</div>
</div><!-- /.modal-card -->
</div><!-- /.modal-overlay #modal-ibadah -->
+132
View File
@@ -0,0 +1,132 @@
<?php // Modal: Tambah / Edit Penduduk Miskin ?>
<div class="modal-overlay" id="modal-miskin">
<div class="modal-card" style="max-width:600px;">
<!-- Header -->
<div class="modal-header" style="background:linear-gradient(135deg,rgba(34,197,94,0.1),rgba(34,197,94,0.04));border-bottom:1px solid rgba(34,197,94,0.2);">
<div class="modal-title" id="modal-miskin-title">👤 Tambah Penduduk Miskin</div>
<button class="modal-close" onclick="closeModalMiskin()" type="button" title="Tutup">&#x2715;</button>
</div>
<!-- Body -->
<div class="modal-body">
<input type="hidden" id="miskin-edit-id">
<!-- Lokasi -->
<div class="section-label">📍 Lokasi</div>
<div class="form-group">
<div class="addr-preview" id="miskin-coord-display" style="font-family:monospace;font-size:11px;">Lat: Lng: </div>
<div class="addr-preview" id="miskin-addr-display" style="margin-top:6px;">
<span class="addr-loading">Pilih titik di peta terlebih dahulu...</span>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Kelurahan</label>
<input class="form-control" id="miskin-kelurahan" placeholder="Kelurahan / Desa">
</div>
<div class="form-group">
<label class="form-label">Kecamatan</label>
<input class="form-control" id="miskin-kecamatan" placeholder="Kecamatan">
</div>
</div>
<!-- Data Keluarga -->
<div class="section-label">👨‍👩‍👧 Data Kepala Keluarga</div>
<div class="form-group">
<label class="form-label">Nama Kepala Keluarga <span style="color:var(--red)">*</span></label>
<input class="form-control" id="miskin-kk-nama" placeholder="Nama lengkap kepala keluarga">
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">NIK KK</label>
<input class="form-control" id="miskin-nik" placeholder="16 digit NIK (opsional)" maxlength="16">
</div>
<div class="form-group">
<label class="form-label">Jumlah Anggota</label>
<input class="form-control" id="miskin-jumlah" type="number" min="1" max="30" value="1" placeholder="1">
</div>
</div>
<!-- Anggota Keluarga -->
<div class="section-label">👥 Detail Anggota Keluarga</div>
<div id="anggota-container"></div>
<button class="btn btn-outline-accent" id="btn-add-anggota" type="button"
style="width:100%;margin-bottom:4px;">
<i class="bi bi-plus-circle"></i> Tambah Anggota Keluarga
</button>
<!-- Status Bantuan -->
<div class="section-label">🤝 Status Bantuan</div>
<div class="form-group">
<label class="form-label">Status Bantuan <span style="color:var(--red)">*</span></label>
<select class="form-control" id="miskin-status">
<option value="belum"> Belum Dibantu</option>
<option value="menunggu"> Menunggu Verifikasi</option>
<option value="sudah"> Sudah Dibantu</option>
</select>
</div>
<!-- Bantuan fields (tampil untuk: sudah DAN menunggu) -->
<div id="bantuan-fields" style="display:none;">
<div class="form-row">
<div class="form-group">
<label class="form-label">Jenis Bantuan</label>
<input class="form-control" id="miskin-jenis-bantuan" placeholder="Mis. Sembako + Uang Tunai">
</div>
<div class="form-group" id="wrap-tanggal">
<label class="form-label">Tanggal Bantuan</label>
<input class="form-control" id="miskin-tanggal" type="date">
</div>
</div>
<!-- BARU: Nominal Bantuan (Rupiah) -->
<div class="form-group">
<label class="form-label">💰 Nominal Bantuan (Rupiah)</label>
<div style="position:relative;">
<span style="position:absolute;left:12px;top:50%;transform:translateY(-50%);font-size:12px;color:var(--text2);font-weight:700;pointer-events:none;">Rp</span>
<input class="form-control" id="miskin-nominal"
type="text" inputmode="numeric"
placeholder="0"
style="padding-left:36px;font-family:'JetBrains Mono',monospace;"
oninput="formatNominalInput(this)">
</div>
<div style="font-size:10px;color:var(--text3);margin-top:4px;">Kosongkan jika belum ditentukan nominal</div>
</div>
<!-- Keterangan (tampil khusus status menunggu) -->
<div class="form-group" id="keterangan-menunggu-wrap" style="display:none;">
<label class="form-label">📋 Keterangan Verifikasi</label>
<input class="form-control" id="miskin-keterangan" placeholder="Alasan atau catatan menunggu verifikasi...">
</div>
</div>
<!-- Upload Bukti -->
<div class="section-label">📎 Bukti Pendukung (Opsional)</div>
<div class="form-group">
<label class="upload-area" for="miskin-bukti" id="upload-area-miskin">
<input type="file" id="miskin-bukti" accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx" style="position:absolute;inset:0;opacity:0;cursor:pointer;">
<div class="upload-text">
<div style="font-size:22px;margin-bottom:4px;">📁</div>
<div>Klik atau seret file ke sini</div>
<div style="font-size:10px;color:var(--text3);margin-top:3px;">JPG · PNG · PDF · DOC (maks 10 MB)</div>
</div>
</label>
<div id="miskin-bukti-preview"></div>
</div>
</div><!-- /.modal-body -->
<!-- Footer -->
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModalMiskin()" type="button">
<i class="bi bi-x-lg"></i> Batal
</button>
<button class="btn btn-success" id="btn-save-miskin" type="button">
<i class="bi bi-floppy2-fill"></i> Simpan
</button>
</div>
</div><!-- /.modal-card -->
</div><!-- /.modal-overlay #modal-miskin -->