Initial commit
This commit is contained in:
@@ -0,0 +1,531 @@
|
||||
<?php
|
||||
// ─── Matikan exception mysqli — wajib sebelum koneksi apapun ─────────────
|
||||
mysqli_report(MYSQLI_REPORT_OFF);
|
||||
|
||||
// ─── Safe stats: selalu return nilai, tidak pernah crash ──────────────────
|
||||
function safeCount($db, $table) {
|
||||
try {
|
||||
$conn = @new mysqli("localhost", "root", "", $db);
|
||||
if (!$conn || $conn->connect_error) return null;
|
||||
$conn->set_charset("utf8mb4");
|
||||
$r = $conn->query("SELECT COUNT(*) AS c FROM `$table`");
|
||||
$val = ($r) ? (int)$r->fetch_assoc()['c'] : null;
|
||||
$conn->close();
|
||||
return $val;
|
||||
} catch (Throwable $e) { return null; }
|
||||
}
|
||||
|
||||
function safeSum($db, $table, $col) {
|
||||
try {
|
||||
$conn = @new mysqli("localhost", "root", "", $db);
|
||||
if (!$conn || $conn->connect_error) return null;
|
||||
$conn->set_charset("utf8mb4");
|
||||
$r = $conn->query("SELECT COALESCE(SUM(`$col`),0) AS s FROM `$table`");
|
||||
$val = ($r) ? (float)$r->fetch_assoc()['s'] : null;
|
||||
$conn->close();
|
||||
return $val;
|
||||
} catch (Throwable $e) { return null; }
|
||||
}
|
||||
|
||||
function safeQuery($db, $sql) {
|
||||
try {
|
||||
$conn = @new mysqli("localhost", "root", "", $db);
|
||||
if (!$conn || $conn->connect_error) return null;
|
||||
$conn->set_charset("utf8mb4");
|
||||
$r = $conn->query($sql);
|
||||
$val = ($r) ? $r->fetch_assoc() : null;
|
||||
$conn->close();
|
||||
return $val;
|
||||
} catch (Throwable $e) { return null; }
|
||||
}
|
||||
|
||||
// Coba kedua nama database untuk kemiskinan
|
||||
$dbPoverty = null;
|
||||
foreach (['webgis_poverty', 'webgis'] as $dbName) {
|
||||
try {
|
||||
$test = @new mysqli("localhost", "root", "", $dbName);
|
||||
if ($test && !$test->connect_error) { $dbPoverty = $dbName; $test->close(); break; }
|
||||
} catch (Throwable $e) { continue; }
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'jalan_ruas' => safeCount('jalan', 'jalan'),
|
||||
'jalan_km' => round((safeSum('jalan', 'jalan', 'panjang_meter') ?? 0) / 1000, 2),
|
||||
'parsil' => safeCount('jalan', 'parsil_tanah'),
|
||||
'spbu_total' => safeCount('spbu', 'spbu'),
|
||||
'spbu_24' => null,
|
||||
'kk_miskin' => $dbPoverty ? safeCount($dbPoverty, 'penduduk_miskin') : null,
|
||||
'log_bantuan' => $dbPoverty ? safeCount($dbPoverty, 'log_bantuan') : null,
|
||||
'ibadah' => $dbPoverty ? safeCount($dbPoverty, 'rumah_ibadah') : null,
|
||||
];
|
||||
|
||||
$spbuRow = safeQuery('spbu', "SELECT SUM(buka_24_jam=1) AS b FROM spbu");
|
||||
if ($spbuRow) $stats['spbu_24'] = (int)$spbuRow['b'];
|
||||
|
||||
// Helper: tampilkan nilai atau dash
|
||||
function sv($v, $suffix = '') {
|
||||
if ($v === null) return '<span class="stat-na">—</span>';
|
||||
return htmlspecialchars($v . $suffix);
|
||||
}
|
||||
|
||||
$year = date('Y');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS Pontianak — Portal</title>
|
||||
<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:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
/* ── Reset ── */
|
||||
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
/* ── Tokens ── */
|
||||
:root {
|
||||
--bg: #0b0e17;
|
||||
--surf: #131722;
|
||||
--surf2: #1a2133;
|
||||
--bdr: rgba(255,255,255,.07);
|
||||
--bdr2: rgba(255,255,255,.13);
|
||||
--txt: #e8edf5;
|
||||
--muted: #7a8499;
|
||||
--j: #3b82f6;
|
||||
--p: #10b981;
|
||||
--s: #f97316;
|
||||
--k: #a855f7;
|
||||
--gj: rgba(59,130,246,.15);
|
||||
--gp: rgba(16,185,129,.15);
|
||||
--gs: rgba(249,115,22,.15);
|
||||
--gk: rgba(168,85,247,.15);
|
||||
--r: 16px;
|
||||
--r-sm: 10px;
|
||||
}
|
||||
|
||||
/* ── Base ── */
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--txt);
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ── Ambient background ── */
|
||||
.bg-ambient {
|
||||
position: fixed; inset: 0; z-index: 0; pointer-events: none;
|
||||
background:
|
||||
radial-gradient(ellipse 60% 50% at 15% 10%, rgba(59,130,246,.08) 0%, transparent 70%),
|
||||
radial-gradient(ellipse 50% 40% at 85% 80%, rgba(168,85,247,.08) 0%, transparent 70%),
|
||||
radial-gradient(ellipse 40% 35% at 78% 12%, rgba(16,185,129,.05) 0%, transparent 60%);
|
||||
}
|
||||
.bg-dots {
|
||||
position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: .3;
|
||||
background-image: radial-gradient(circle, rgba(255,255,255,.15) 1px, transparent 1px);
|
||||
background-size: 28px 28px;
|
||||
}
|
||||
|
||||
/* ── Layout ── */
|
||||
.wrap {
|
||||
position: relative; z-index: 1;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px 80px;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
header { padding: 52px 0 8px; }
|
||||
|
||||
.eyebrow {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
font-size: 11px; font-weight: 700;
|
||||
letter-spacing: .12em; text-transform: uppercase;
|
||||
color: var(--muted); margin-bottom: 14px;
|
||||
}
|
||||
.eyebrow-dot {
|
||||
width: 7px; height: 7px; border-radius: 50%;
|
||||
background: var(--p);
|
||||
box-shadow: 0 0 0 3px rgba(16,185,129,.2);
|
||||
animation: livepulse 2.2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes livepulse {
|
||||
0%,100% { box-shadow: 0 0 0 3px rgba(16,185,129,.2); }
|
||||
50% { box-shadow: 0 0 0 7px rgba(16,185,129,.0); }
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: clamp(32px, 5vw, 52px);
|
||||
font-weight: 800;
|
||||
line-height: 1.06;
|
||||
letter-spacing: -.025em;
|
||||
color: #fff;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.title-grad {
|
||||
background: linear-gradient(95deg, var(--j) 0%, var(--k) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 15px; color: var(--muted);
|
||||
max-width: 540px; line-height: 1.65; font-weight: 400;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/* ── Section label ── */
|
||||
.sec-label {
|
||||
font-size: 10px; font-weight: 700;
|
||||
letter-spacing: .12em; text-transform: uppercase;
|
||||
color: var(--muted); margin-bottom: 16px;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
}
|
||||
.sec-label::after { content:''; flex:1; height:1px; background:var(--bdr); }
|
||||
|
||||
/* ── Summary bar ── */
|
||||
.summary-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
@media (max-width:700px) { .summary-bar { grid-template-columns: repeat(2,1fr); } }
|
||||
@media (max-width:400px) { .summary-bar { grid-template-columns: 1fr; } }
|
||||
|
||||
.sum-card {
|
||||
background: var(--surf);
|
||||
border: 1px solid var(--bdr);
|
||||
border-radius: var(--r-sm);
|
||||
padding: 18px 20px;
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.sum-card::before {
|
||||
content:''; position:absolute;
|
||||
left:0; top:0; bottom:0; width:3px;
|
||||
background: var(--c);
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
.sum-num {
|
||||
font-size: 26px; font-weight: 800;
|
||||
color: var(--c); line-height: 1; margin-bottom: 6px;
|
||||
}
|
||||
.sum-lbl {
|
||||
font-size: 10px; font-weight: 600;
|
||||
text-transform: uppercase; letter-spacing: .07em;
|
||||
color: var(--muted);
|
||||
}
|
||||
.stat-na { opacity: .35; font-weight: 400; }
|
||||
|
||||
/* ── Divider ── */
|
||||
.divider { height:1px; background:var(--bdr); margin: 4px 0 28px; }
|
||||
|
||||
/* ── Apps grid ── */
|
||||
.apps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2,1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
@media (max-width:640px) { .apps-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ── App card ── */
|
||||
.app-card {
|
||||
background: var(--surf);
|
||||
border: 1px solid var(--bdr);
|
||||
border-radius: var(--r);
|
||||
padding: 26px 26px 66px;
|
||||
text-decoration: none;
|
||||
color: var(--txt);
|
||||
display: block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: border-color .2s, transform .18s, background .2s;
|
||||
}
|
||||
.app-card::after {
|
||||
content:''; position:absolute;
|
||||
top:-70px; right:-70px;
|
||||
width:180px; height:180px;
|
||||
border-radius:50%;
|
||||
background: var(--cg);
|
||||
transition: opacity .3s, transform .3s;
|
||||
opacity: .6;
|
||||
}
|
||||
.app-card:hover {
|
||||
border-color: var(--ca);
|
||||
background: var(--surf2);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
.app-card:hover::after { opacity:1; transform:scale(1.15); }
|
||||
|
||||
/* Card accent variables per type */
|
||||
.c-jalan { --ca:var(--j); --cg:var(--gj); }
|
||||
.c-penduduk { --ca:var(--p); --cg:var(--gp); }
|
||||
.c-spbu { --ca:var(--s); --cg:var(--gs); }
|
||||
.c-kemiskin { --ca:var(--k); --cg:var(--gk); }
|
||||
|
||||
/* Card top row */
|
||||
.card-head {
|
||||
display: flex; align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
position: relative; z-index:1;
|
||||
}
|
||||
.card-ico {
|
||||
width: 46px; height: 46px; border-radius: 13px;
|
||||
background: var(--cg);
|
||||
border: 1px solid rgba(255,255,255,.09);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 20px; color: var(--ca);
|
||||
}
|
||||
.card-tag {
|
||||
font-size: 9px; font-weight: 700;
|
||||
letter-spacing: .07em; text-transform: uppercase;
|
||||
padding: 4px 10px; border-radius: 99px;
|
||||
border: 1px solid var(--ca);
|
||||
color: var(--ca); opacity: .75;
|
||||
}
|
||||
|
||||
/* Card body */
|
||||
.card-body { position: relative; z-index:1; margin-bottom: 18px; }
|
||||
.card-name {
|
||||
font-size: 18px; font-weight: 700;
|
||||
color: #fff; line-height: 1.2; margin-bottom: 8px;
|
||||
}
|
||||
.card-desc {
|
||||
font-size: 12.5px; color: var(--muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Card mini-stats */
|
||||
.card-stats {
|
||||
display: flex; gap: 18px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--bdr);
|
||||
position: relative; z-index:1;
|
||||
}
|
||||
.cs-val {
|
||||
font-size: 17px; font-weight: 700;
|
||||
color: var(--ca); line-height: 1; margin-bottom: 4px;
|
||||
}
|
||||
.cs-lbl {
|
||||
font-size: 9px; font-weight: 600;
|
||||
text-transform: uppercase; letter-spacing: .05em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Arrow button */
|
||||
.card-arrow {
|
||||
position: absolute; bottom: 22px; right: 22px; z-index:2;
|
||||
width: 34px; height: 34px; border-radius: 50%;
|
||||
background: rgba(255,255,255,.04);
|
||||
border: 1px solid var(--bdr2);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 12px; color: var(--muted);
|
||||
transition: all .2s;
|
||||
}
|
||||
.app-card:hover .card-arrow {
|
||||
background: var(--ca);
|
||||
border-color: var(--ca);
|
||||
color: #fff;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
/* ── Footer ── */
|
||||
footer {
|
||||
margin-top: 60px; padding-top: 22px;
|
||||
border-top: 1px solid var(--bdr);
|
||||
display: flex; justify-content: space-between;
|
||||
align-items: center; flex-wrap: wrap; gap: 12px;
|
||||
}
|
||||
.ft-l { font-size: 12px; color: var(--muted); }
|
||||
.ft-r { display:flex; gap:7px; align-items:center; }
|
||||
.ft-dot { width:5px; height:5px; border-radius:50%; background:var(--p); }
|
||||
.ft-txt { font-size: 11px; color: var(--muted); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="bg-ambient"></div>
|
||||
<div class="bg-dots"></div>
|
||||
|
||||
<div class="wrap">
|
||||
|
||||
<!-- Header -->
|
||||
<header>
|
||||
<div class="eyebrow">
|
||||
<span class="eyebrow-dot"></span>
|
||||
WebGIS Kota Pontianak
|
||||
</div>
|
||||
<h1 class="title">
|
||||
Portal <span class="title-grad">Sistem Informasi</span><br>Geospasial
|
||||
</h1>
|
||||
<p class="subtitle">
|
||||
Platform terpadu pemetaan infrastruktur, kependudukan, energi, dan kemiskinan
|
||||
Kota Pontianak — Kalimantan Barat.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Ringkasan Data -->
|
||||
<div class="sec-label">Ringkasan Data</div>
|
||||
<div class="summary-bar">
|
||||
<div class="sum-card" style="--c:var(--j)">
|
||||
<div class="sum-num"><?= sv($stats['jalan_ruas']) ?></div>
|
||||
<div class="sum-lbl">Ruas Jalan</div>
|
||||
</div>
|
||||
<div class="sum-card" style="--c:var(--p)">
|
||||
<div class="sum-num">6</div>
|
||||
<div class="sum-lbl">Kecamatan</div>
|
||||
</div>
|
||||
<div class="sum-card" style="--c:var(--s)">
|
||||
<div class="sum-num"><?= sv($stats['spbu_total']) ?></div>
|
||||
<div class="sum-lbl">SPBU Terdaftar</div>
|
||||
</div>
|
||||
<div class="sum-card" style="--c:var(--k)">
|
||||
<div class="sum-num"><?= sv($stats['kk_miskin']) ?></div>
|
||||
<div class="sum-lbl">KK Miskin</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- App Cards -->
|
||||
<div class="sec-label">Aplikasi Tersedia</div>
|
||||
<div class="apps-grid">
|
||||
|
||||
<!-- Jalan -->
|
||||
<a href="jalan/index.php" class="app-card c-jalan">
|
||||
<div class="card-head">
|
||||
<div class="card-ico"><i class="fas fa-road"></i></div>
|
||||
<span class="card-tag">GIS Layer</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-name">Pemetaan Jalan<br>& Persil Tanah</div>
|
||||
<div class="card-desc">
|
||||
Peta interaktif ruas jalan (polyline) dan persil tanah (polygon)
|
||||
dengan fitur CRUD dan penggambaran langsung di peta.
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-stats">
|
||||
<div>
|
||||
<div class="cs-val"><?= sv($stats['jalan_ruas']) ?></div>
|
||||
<div class="cs-lbl">Ruas Jalan</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="cs-val"><?= sv($stats['jalan_km'], ' km') ?></div>
|
||||
<div class="cs-lbl">Panjang Total</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="cs-val"><?= sv($stats['parsil']) ?></div>
|
||||
<div class="cs-lbl">Persil Tanah</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-arrow"><i class="fas fa-arrow-right"></i></div>
|
||||
</a>
|
||||
|
||||
<!-- Penduduk -->
|
||||
<a href="penduduk/index.php" class="app-card c-penduduk">
|
||||
<div class="card-head">
|
||||
<div class="card-ico"><i class="fas fa-users"></i></div>
|
||||
<span class="card-tag">Choropleth</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-name">Kepadatan<br>Penduduk</div>
|
||||
<div class="card-desc">
|
||||
Visualisasi choropleth kepadatan penduduk per kecamatan di
|
||||
Kota Pontianak dengan legenda gradasi warna interaktif.
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-stats">
|
||||
<div>
|
||||
<div class="cs-val">6</div>
|
||||
<div class="cs-lbl">Kecamatan</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="cs-val">9.33</div>
|
||||
<div class="cs-lbl">Kepadatan Maks</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="cs-val">UTM</div>
|
||||
<div class="cs-lbl">Proyeksi</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-arrow"><i class="fas fa-arrow-right"></i></div>
|
||||
</a>
|
||||
|
||||
<!-- SPBU -->
|
||||
<a href="spbu/index.php" class="app-card c-spbu">
|
||||
<div class="card-head">
|
||||
<div class="card-ico"><i class="fas fa-gas-pump"></i></div>
|
||||
<span class="card-tag">Point Layer</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-name">Peta SPBU<br>Pontianak</div>
|
||||
<div class="card-desc">
|
||||
Sebaran lokasi SPBU Pertamina dengan informasi operasional,
|
||||
nomor WhatsApp, status 24 jam, dan fitur geser marker.
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-stats">
|
||||
<div>
|
||||
<div class="cs-val"><?= sv($stats['spbu_total']) ?></div>
|
||||
<div class="cs-lbl">Total SPBU</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="cs-val"><?= sv($stats['spbu_24']) ?></div>
|
||||
<div class="cs-lbl">Buka 24 Jam</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="cs-val">WA</div>
|
||||
<div class="cs-lbl">Kontak</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-arrow"><i class="fas fa-arrow-right"></i></div>
|
||||
</a>
|
||||
|
||||
<!-- Kemiskinan -->
|
||||
<a href="webgis-poverty-pontianak/index.php" class="app-card c-kemiskin">
|
||||
<div class="card-head">
|
||||
<div class="card-ico"><i class="fas fa-map-marked-alt"></i></div>
|
||||
<span class="card-tag">Multi Layer</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-name">Pemetaan<br>Kemiskinan</div>
|
||||
<div class="card-desc">
|
||||
Sistem manajemen penduduk miskin, rumah ibadah sebagai pusat
|
||||
bantuan, log distribusi bantuan, dan dashboard analitik.
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-stats">
|
||||
<div>
|
||||
<div class="cs-val"><?= sv($stats['kk_miskin']) ?></div>
|
||||
<div class="cs-lbl">KK Miskin</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="cs-val"><?= sv($stats['ibadah']) ?></div>
|
||||
<div class="cs-lbl">Pusat Bantuan</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="cs-val"><?= sv($stats['log_bantuan']) ?></div>
|
||||
<div class="cs-lbl">Log Bantuan</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-arrow"><i class="fas fa-arrow-right"></i></div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<div class="ft-l">WebGIS Pontianak © <?= $year ?> — Sistem Informasi Geospasial Kota</div>
|
||||
<div class="ft-r">
|
||||
<div class="ft-dot"></div>
|
||||
<div class="ft-txt">Kalimantan Barat, Indonesia</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div><!-- /wrap -->
|
||||
</body>
|
||||
</html>
|
||||
+1468
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS jalan
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE jalan;
|
||||
|
||||
-- Tabel Jalan (Polyline)
|
||||
CREATE TABLE IF NOT EXISTS jalan (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
nama_jalan VARCHAR(255) NOT NULL,
|
||||
status_jalan ENUM('Nasional','Provinsi','Kabupaten') NOT NULL DEFAULT 'Kabupaten',
|
||||
panjang_meter FLOAT NOT NULL DEFAULT 0,
|
||||
geojson LONGTEXT NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Tabel Parsil Tanah (Polygon)
|
||||
CREATE TABLE IF NOT EXISTS parsil_tanah (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
nama_parsil VARCHAR(255) NOT NULL,
|
||||
status_kepemilikan ENUM('SHM','HGB','HGU','HP') NOT NULL DEFAULT 'SHM',
|
||||
luas_m2 FLOAT NOT NULL DEFAULT 0,
|
||||
geojson LONGTEXT NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -0,0 +1,163 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base target="_top">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Kepadatan Penduduk Pontianak</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.9.0/proj4.js"></script>
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
.info {
|
||||
padding: 6px 8px;
|
||||
font: 14px/16px Arial, Helvetica, sans-serif;
|
||||
background: rgba(255,255,255,0.85);
|
||||
box-shadow: 0 0 15px rgba(0,0,0,0.2);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.info h4 { margin: 0 0 5px; color: #777; }
|
||||
.legend { text-align: left; line-height: 22px; color: #555; }
|
||||
.legend i { width: 18px; height: 18px; float: left; margin-right: 8px; opacity: 0.7; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
<script>
|
||||
// ── 1. Register UTM Zone 49N (covers Pontianak at ~109°E) ──────────────────
|
||||
proj4.defs('EPSG:32649', '+proj=utm +zone=49 +datum=WGS84 +units=m +no_defs');
|
||||
|
||||
// Convert a ring of [easting, northing] pairs → [lng, lat]
|
||||
function convertRings(rings) {
|
||||
return rings.map(ring =>
|
||||
ring.map(([x, y]) => proj4('EPSG:32649', 'WGS84', [x, y]))
|
||||
);
|
||||
}
|
||||
|
||||
// ── 2. Raw data (UTM coords) ───────────────────────────────────────────────
|
||||
var rawData = {
|
||||
type: "FeatureCollection",
|
||||
features: [
|
||||
{ type:"Feature", id:"1", properties:{ name:"Pontianak Selatan", density:5.673 }, geometry:{ type:"Polygon", coordinates:[[[315791.7496880172,-2872.6773100267],[315493.9173600795,-3043.9080895001625],[315326.6521934429,-3151.333180362999],[315140.4188381387,-3281.935194921576],[315066.9918184001,-3327.8058926928916],[314996.8467820659,-3371.6262979517323],[314888.67104393616,-3478.080758171731],[314812.70706351567,-3552.7994604061805],[314746.1257071411,-3618.2893480278017],[314655.6433510417,-3707.2884260803185],[314606.10591966007,-3756.013789919325],[314454.0137261031,-3905.612734831022],[314344.4320643814,-3996.3010035972147],[314259.6778418068,-4066.4424268491857],[314085.032268201,-4210.976689869447],[313840.1791924136,-4413.613711391907],[313536.192724674,-4712.359024276697],[313300.8322324334,-4943.661570138169],[312916.42216176353,-5321.443869765168],[312612.50988089014,-5620.11627486908],[312370.45105957426,-5854.688712254208],[312057.7659053877,-6157.703154576355],[311585.92455374077,-6614.9514189124275],[311333.4393242495,-6891.340090787351],[313474.832878937,-8889.931704557468],[313757.1209584046,-8461.458752544078],[314215.6677178042,-7765.450320278544],[314471.81653076597,-7473.440609211619],[314877.1271904595,-7011.386355436478],[315170.0992892701,-6667.707583206353],[315565.04540335014,-6204.405459439875],[315762.9765556948,-5956.206301724727],[315946.9456108697,-5783.346926725002],[316115.1479424136,-5571.442019498811],[316233.71287194826,-5370.136871324419],[316413.0633267751,-5142.274731615995],[316588.3382888399,-4973.07120414943],[315803.17076956015,-2870.648806607189],[315791.7496880172,-2872.6773100267]]] } },
|
||||
{ type:"Feature", id:"2", properties:{ name:"Pontianak Utara", density:3.642 }, geometry:{ type:"Polygon", coordinates:[[[309688.2266975902,951.2434479881485],[310155.9051329456,933.8867466094162],[310357.96077316906,885.1327602118836],[311332.6877226224,552.8154289115046],[312269.31459587533,306.7524367839651],[313016.67581282463,28.516046978937084],[313225.22203908674,-84.19626616407731],[313546.372705549,-322.5867385597503],[313720.9980547987,-448.5286571102988],[314170.79062105156,-811.0085487379129],[314504.16628780216,-895.1462170118866],[314775.1001630025,-1443.89314784007],[315291.5124306837,-1928.5005853951407],[315734.48124843184,-2324.9574099686342],[316159.40293160826,-2368.349163418803],[316654.17475448735,-2251.9322639187267],[317038.8796905633,-2161.4445829436972],[317913.59393999074,-2046.6151866172168],[318283.48217976745,-2054.023534766595],[318775.15660756174,-2147.859083549327],[319287.81429954246,-2395.9329130315673],[319416.881835456,-2033.8234388107558],[319526.1020538965,-1800.9896398106011],[319533.72206913494,-1723.0961506912936],[319510.86202341504,-1622.342615849966],[319574.3621504167,-1479.2556630090148],[319598.91553285625,-1507.1957188887664],[319482.0752991745,-1853.4830781301353],[319424.3083182946,-874.973119525288],[319320.91374074016,-644.5676602992062],[318847.9647948425,226.72111005606712],[318658.4169157464,989.0401346929066],[316567.76460495684,2999.9040346896763],[314562.85392847005,3541.7717850915287],[312083.8089703787,4381.666798215181],[309688.2266975902,951.2434479881485]]] } },
|
||||
{ type:"Feature", id:"3", properties:{ name:"Pontianak Timur", density:9.196 }, geometry:{ type:"Polygon", coordinates:[[[319887.57044806925,-5373.344392278639],[319702.88422977563,-5132.534790727044],[319580.270434435,-4979.559484159208],[319416.78537397756,-4779.407060143594],[319288.3328264768,-4635.773757029033],[319063.0274988217,-4397.196101115128],[318712.6388119413,-4027.525809986318],[319240.45505877363,-3500.8371123406214],[319482.70326267194,-3416.1079811805625],[319930.7033710063,-3290.5899437818016],[320060.2158619128,-2885.3158597254237],[319668.47127330344,-2832.0761712812755],[319302.34583165945,-2724.1663001296456],[319016.246975861,-2582.2846226671395],[318611.86326987745,-2402.6633110638345],[318312.0934654712,-2349.9022867323624],[317831.7500457056,-2325.396742533832],[317450.6828969274,-2376.0793589098607],[316961.72475783527,-2448.1651844011494],[316290.7174679831,-2617.4144481542617],[315891.63306421525,-2646.1325691096436],[316108.6562515756,-3080.1789438243754],[316374.4062489303,-3648.9273296033716],[316666.3438568887,-4078.0756132950055],[316960.2915036416,-4459.3151686055135],[317511.1983193059,-4982.14799761562],[318003.96436076245,-5498.157720267835],[318525.46364961116,-6295.854170744091],[318997.8492128665,-6873.389132317587],[319200.2395574156,-6725.148423245205],[319464.4036415865,-6506.445073789114],[319929.8147521868,-6173.089029280636],[320230.3441586008,-5948.45374608942],[319892.69977100904,-5382.870277737669],[319887.57044806925,-5373.344392278639]]] } },
|
||||
{ type:"Feature", id:"4", properties:{ name:"Pontianak Tenggara", density:3.103 }, geometry:{ type:"Polygon", coordinates:[[[316509.95719540527,-5101.833566810722],[316277.42321839795,-5368.326560621595],[315586.95250728936,-6227.493567962928],[314650.4343267438,-7312.870467518172],[313943.0478521116,-8266.304411587676],[313570.5827409144,-8808.522535934284],[314482.8144881896,-9756.429398367298],[315386.15532470803,-10595.173154648382],[315969.86388160125,-10324.183934093831],[316202.0711808827,-9921.28120289028],[316389.8178299225,-9632.15192111897],[316955.59155603434,-8811.299556391281],[317370.6954695009,-8105.002669764531],[318021.28465803474,-6941.185008999439],[318253.2629668124,-6576.773915689321],[318236.68267524854,-6280.690804057879],[317914.4077884978,-5744.010290518615],[317403.09039107355,-5217.314926254428],[316771.0250557211,-4765.816074840871],[316509.95719540527,-5101.833566810722]]] } },
|
||||
{ type:"Feature", id:"5", properties:{ name:"Pontianak Kota", density:8.102 }, geometry:{ type:"Polygon", coordinates:[[[310276.47296142485,-3455.9797973638633],[309858.4304809589,-3797.2929687509313],[309527.70056152344,-4114.79357910133],[309256.67931494024,-4394.240588930203],[309517.11724853516,-4686.294799805153],[309900.76403808687,-5093.753967286088],[310421.994140625,-5638.796691895346],[310977.62023925874,-6427.256591796759],[311295.12091064546,-6863.819946288946],[311784.60089111235,-6451.0691528309835],[312297.8936767578,-5916.60974120989],[312922.31152343843,-5310.71270752023],[313407.04170880467,-4843.868031024118],[313851.00109863374,-4424.356750487466],[314247.8766479511,-4080.397766113165],[314713.5443115253,-3659.709350586054],[315263.87866211124,-3191.395935058943],[315803.6297607431,-2876.541137695778],[315533.75421142764,-2532.582153319032],[315086.40909403935,-2251.8851285779383],[314597.12731933594,-1876.4141235346906],[314187.02246093843,-2037.810302733793],[313546.7294311533,-2148.9354858405422],[312935.5408325214,-2270.64410400379],[312366.6854858408,-2413.519348145579],[311906.3095092783,-2564.3322143565165],[311385.0793457031,-2783.936767577892],[310773.8905639658,-3093.4998779299203],[310276.47296142485,-3455.9797973638633]]] } },
|
||||
{ type:"Feature", id:"6", properties:{ name:"Pontianak Barat", density:9.331 }, geometry:{ type:"Polygon", coordinates:[[[314496.5855712909,-1831.4348754876992],[314333.6083123619,-1673.1813620609464],[313976.42009798624,-1469.4517879342893],[313709.19039686024,-1196.9304095584666],[313412.8564708587,-1024.9508989329915],[313156.21012423374,-776.2420681805816],[312690.5425262293,-424.3455310544232],[312129.62473772746,-204.74092517967802],[311526.37353122514,-96.26154155412223],[310896.6639384739,-149.17831405519973],[310303.9960864708,-154.46999130456243],[309732.49494346976,70.42629182199016],[309351.49418146536,70.42629182199016],[309319.74411796685,-400.5329834310105],[309364.7233745931,-715.3877798067406],[309351.49418146536,-974.679965057876],[309232.4314433411,-1506.493528684834],[309176.8688322166,-1879.5567748110043],[309205.97305709217,-2040.952930937754],[308552.45091671497,-2472.2246268143645],[308147.6376070874,-3011.9757063165307],[308430.74233996496,-3734.289650943014],[308827.6181337144,-4363.999243696919],[309142.47293009236,-4490.9994976965245],[309479.5459254207,-4168.759302856284],[310007.24186643306,-3657.238962248084],[310429.9315185547,-3344.854614257114],[310773.8905639658,-3093.4998779299203],[311336.9246401787,-2797.156559649622],[311906.3095092783,-2564.3322143565165],[312525.43572998233,-2360.602661131881],[313125.56844345294,-2227.800214529503],[313970.063720705,-2077.4978637696477],[314456.89801025484,-1908.1641845697304],[314496.5855712909,-1831.4348754876992]]] } }
|
||||
]
|
||||
};
|
||||
|
||||
// ── 3. Reproject every feature to WGS84 ───────────────────────────────────
|
||||
var statesData = {
|
||||
type: "FeatureCollection",
|
||||
features: rawData.features.map(function(f) {
|
||||
return Object.assign({}, f, {
|
||||
geometry: Object.assign({}, f.geometry, {
|
||||
coordinates: convertRings(f.geometry.coordinates)
|
||||
})
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
// ── 4. Map & tiles ─────────────────────────────────────────────────────────
|
||||
const map = L.map('map').setView([-0.03, 109.33], 12);
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
|
||||
// ── 5. Hover info box ──────────────────────────────────────────────────────
|
||||
const info = L.control();
|
||||
info.onAdd = function() {
|
||||
this._div = L.DomUtil.create('div', 'info');
|
||||
this.update();
|
||||
return this._div;
|
||||
};
|
||||
info.update = function(props) {
|
||||
const contents = props
|
||||
? `<b>${props.name}</b><br />${props.density} ribu jiwa / km<sup>2</sup>`
|
||||
: 'Arahkan kursor ke kecamatan';
|
||||
this._div.innerHTML = `<h4>Kepadatan Penduduk Pontianak</h4>${contents}`;
|
||||
};
|
||||
info.addTo(map);
|
||||
|
||||
// ── 6. Three-level colour scale ────────────────────────────────────────────
|
||||
// Rendah (low) : ≤ 5 → light yellow
|
||||
// Sedang (medium) : 5–7 → orange
|
||||
// Tinggi (high) : > 7 → red
|
||||
function getColor(d) {
|
||||
return d > 7 ? '#E31A1C'
|
||||
: d > 5 ? '#FD8D3C'
|
||||
: '#d3ffa0';
|
||||
}
|
||||
|
||||
function style(feature) {
|
||||
return {
|
||||
weight: 2, opacity: 1, color: 'white', dashArray: '3',
|
||||
fillOpacity: 0.7,
|
||||
fillColor: getColor(feature.properties.density)
|
||||
};
|
||||
}
|
||||
|
||||
// ── 7. Interaction ─────────────────────────────────────────────────────────
|
||||
function highlightFeature(e) {
|
||||
const layer = e.target;
|
||||
layer.setStyle({ weight: 5, color: '#666', dashArray: '', fillOpacity: 0.7 });
|
||||
layer.bringToFront();
|
||||
info.update(layer.feature.properties);
|
||||
}
|
||||
|
||||
function resetHighlight(e) {
|
||||
geojson.resetStyle(e.target);
|
||||
info.update();
|
||||
}
|
||||
|
||||
function zoomToFeature(e) {
|
||||
map.fitBounds(e.target.getBounds());
|
||||
}
|
||||
|
||||
function onEachFeature(feature, layer) {
|
||||
layer.on({ mouseover: highlightFeature, mouseout: resetHighlight, click: zoomToFeature });
|
||||
}
|
||||
|
||||
const geojson = L.geoJson(statesData, { style, onEachFeature }).addTo(map);
|
||||
|
||||
// Auto-fit the map to the actual data extent
|
||||
map.fitBounds(geojson.getBounds());
|
||||
|
||||
map.attributionControl.addAttribution(
|
||||
'Data penduduk © <a href="https://pontianakkota.bps.go.id/">BPS Kota Pontianak</a>'
|
||||
);
|
||||
|
||||
// ── 8. Legend ──────────────────────────────────────────────────────────────
|
||||
const legend = L.control({ position: 'bottomright' });
|
||||
legend.onAdd = function() {
|
||||
const div = L.DomUtil.create('div', 'info legend');
|
||||
const levels = [
|
||||
{ threshold: 0, label: '≤ 5 (Rendah)' },
|
||||
{ threshold: 5, label: '5 – 7 (Sedang)' },
|
||||
{ threshold: 7, label: '> 7 (Tinggi)' }
|
||||
];
|
||||
levels.forEach(({ threshold, label }) => {
|
||||
div.innerHTML +=
|
||||
`<i style="background:${getColor(threshold + 0.1)}"></i> ${label}<br>`;
|
||||
});
|
||||
div.innerHTML += '<small>ribu jiwa / km²</small>';
|
||||
return div;
|
||||
};
|
||||
legend.addTo(map);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,59 @@
|
||||
var statesData = {
|
||||
type: "FeatureCollection",
|
||||
features: [
|
||||
{
|
||||
type: "Feature",
|
||||
id: "1",
|
||||
properties: { name: "Pontianak Selatan", density: 5.673 },
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [[[315791.7496880172,-2872.6773100267],[315493.9173600795,-3043.9080895001625],[315326.6521934429,-3151.333180362999],[315140.4188381387,-3281.935194921576],[315066.9918184001,-3327.8058926928916],[314996.8467820659,-3371.6262979517323],[314888.67104393616,-3478.080758171731],[314812.70706351567,-3552.7994604061805],[314746.1257071411,-3618.2893480278017],[314655.6433510417,-3707.2884260803185],[314606.10591966007,-3756.013789919325],[314454.0137261031,-3905.612734831022],[314344.4320643814,-3996.3010035972147],[314259.6778418068,-4066.4424268491857],[314085.032268201,-4210.976689869447],[313840.1791924136,-4413.613711391907],[313536.192724674,-4712.359024276697],[313300.8322324334,-4943.661570138169],[312916.42216176353,-5321.443869765168],[312612.50988089014,-5620.11627486908],[312370.45105957426,-5854.688712254208],[312057.7659053877,-6157.703154576355],[311585.92455374077,-6614.9514189124275],[311333.4393242495,-6891.340090787351],[313474.832878937,-8889.931704557468],[313757.1209584046,-8461.458752544078],[314215.6677178042,-7765.450320278544],[314471.81653076597,-7473.440609211619],[314877.1271904595,-7011.386355436478],[315170.0992892701,-6667.707583206353],[315565.04540335014,-6204.405459439875],[315762.9765556948,-5956.206301724727],[315946.9456108697,-5783.346926725002],[316115.1479424136,-5571.442019498811],[316233.71287194826,-5370.136871324419],[316413.0633267751,-5142.274731615995],[316588.3382888399,-4973.07120414943],[315803.17076956015,-2870.648806607189],[315791.7496880172,-2872.6773100267]]]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "Feature",
|
||||
id: "2",
|
||||
properties: { name: "Pontianak Utara", density: 3.642 },
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [[[309688.2266975902,951.2434479881485],[310155.9051329456,933.8867466094162],[310357.96077316906,885.1327602118836],[311332.6877226224,552.8154289115046],[312269.31459587533,306.7524367839651],[313016.67581282463,28.516046978937084],[313225.22203908674,-84.19626616407731],[313546.372705549,-322.5867385597503],[313720.9980547987,-448.5286571102988],[314170.79062105156,-811.0085487379129],[314504.16628780216,-895.1462170118866],[314775.1001630025,-1443.89314784007],[315291.5124306837,-1928.5005853951407],[315734.48124843184,-2324.9574099686342],[316159.40293160826,-2368.349163418803],[316654.17475448735,-2251.9322639187267],[317038.8796905633,-2161.4445829436972],[317913.59393999074,-2046.6151866172168],[318283.48217976745,-2054.023534766595],[318775.15660756174,-2147.859083549327],[319287.81429954246,-2395.9329130315673],[319416.881835456,-2033.8234388107558],[319526.1020538965,-1800.9896398106011],[319533.72206913494,-1723.0961506912936],[319510.86202341504,-1622.342615849966],[319574.3621504167,-1479.2556630090148],[319598.91553285625,-1507.1957188887664],[319482.0752991745,-1853.4830781301353],[319424.3083182946,-874.973119525288],[319320.91374074016,-644.5676602992062],[318847.9647948425,226.72111005606712],[318658.4169157464,989.0401346929066],[316567.76460495684,2999.9040346896763],[314562.85392847005,3541.7717850915287],[312083.8089703787,4381.666798215181],[309688.2266975902,951.2434479881485]]]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "Feature",
|
||||
id: "3",
|
||||
properties: { name: "Pontianak Timur", density: 9.196 },
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [[[319887.57044806925,-5373.344392278639],[319702.88422977563,-5132.534790727044],[319580.270434435,-4979.559484159208],[319416.78537397756,-4779.407060143594],[319288.3328264768,-4635.773757029033],[319063.0274988217,-4397.196101115128],[318712.6388119413,-4027.525809986318],[319240.45505877363,-3500.8371123406214],[319482.70326267194,-3416.1079811805625],[319930.7033710063,-3290.5899437818016],[320060.2158619128,-2885.3158597254237],[319668.47127330344,-2832.0761712812755],[319302.34583165945,-2724.1663001296456],[319016.246975861,-2582.2846226671395],[318611.86326987745,-2402.6633110638345],[318312.0934654712,-2349.9022867323624],[317831.7500457056,-2325.396742533832],[317450.6828969274,-2376.0793589098607],[316961.72475783527,-2448.1651844011494],[316290.7174679831,-2617.4144481542617],[315891.63306421525,-2646.1325691096436],[316108.6562515756,-3080.1789438243754],[316374.4062489303,-3648.9273296033716],[316666.3438568887,-4078.0756132950055],[316960.2915036416,-4459.3151686055135],[317511.1983193059,-4982.14799761562],[318003.96436076245,-5498.157720267835],[318525.46364961116,-6295.854170744091],[318997.8492128665,-6873.389132317587],[319200.2395574156,-6725.148423245205],[319464.4036415865,-6506.445073789114],[319929.8147521868,-6173.089029280636],[320230.3441586008,-5948.45374608942],[319892.69977100904,-5382.870277737669],[319887.57044806925,-5373.344392278639]]]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "Feature",
|
||||
id: "4",
|
||||
properties: { name: "Pontianak Tenggara", density: 3.103 },
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [[[316509.95719540527,-5101.833566810722],[316277.42321839795,-5368.326560621595],[315586.95250728936,-6227.493567962928],[314650.4343267438,-7312.870467518172],[313943.0478521116,-8266.304411587676],[313570.5827409144,-8808.522535934284],[314482.8144881896,-9756.429398367298],[315386.15532470803,-10595.173154648382],[315969.86388160125,-10324.183934093831],[316202.0711808827,-9921.28120289028],[316389.8178299225,-9632.15192111897],[316955.59155603434,-8811.299556391281],[317370.6954695009,-8105.002669764531],[318021.28465803474,-6941.185008999439],[318253.2629668124,-6576.773915689321],[318236.68267524854,-6280.690804057879],[317914.4077884978,-5744.010290518615],[317403.09039107355,-5217.314926254428],[316771.0250557211,-4765.816074840871],[316509.95719540527,-5101.833566810722]]]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "Feature",
|
||||
id: "5",
|
||||
properties: { name: "Pontianak Kota", density: 8.102 },
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [[[310276.47296142485,-3455.9797973638633],[309858.4304809589,-3797.2929687509313],[309527.70056152344,-4114.79357910133],[309256.67931494024,-4394.240588930203],[309517.11724853516,-4686.294799805153],[309900.76403808687,-5093.753967286088],[310421.994140625,-5638.796691895346],[310977.62023925874,-6427.256591796759],[311295.12091064546,-6863.819946288946],[311784.60089111235,-6451.0691528309835],[312297.8936767578,-5916.60974120989],[312922.31152343843,-5310.71270752023],[313407.04170880467,-4843.868031024118],[313851.00109863374,-4424.356750487466],[314247.8766479511,-4080.397766113165],[314713.5443115253,-3659.709350586054],[315263.87866211124,-3191.395935058943],[315803.6297607431,-2876.541137695778],[315533.75421142764,-2532.582153319032],[315086.40909403935,-2251.8851285779383],[314597.12731933594,-1876.4141235346906],[314187.02246093843,-2037.810302733793],[313546.7294311533,-2148.9354858405422],[312935.5408325214,-2270.64410400379],[312366.6854858408,-2413.519348145579],[311906.3095092783,-2564.3322143565165],[311385.0793457031,-2783.936767577892],[310773.8905639658,-3093.4998779299203],[310276.47296142485,-3455.9797973638633]]]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "Feature",
|
||||
id: "6",
|
||||
properties: { name: "Pontianak Barat", density: 9.331 },
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [[[314496.5855712909,-1831.4348754876992],[314333.6083123619,-1673.1813620609464],[313976.42009798624,-1469.4517879342893],[313709.19039686024,-1196.9304095584666],[313412.8564708587,-1024.9508989329915],[313156.21012423374,-776.2420681805816],[312690.5425262293,-424.3455310544232],[312129.62473772746,-204.74092517967802],[311526.37353122514,-96.26154155412223],[310896.6639384739,-149.17831405519973],[310303.9960864708,-154.46999130456243],[309732.49494346976,70.42629182199016],[309351.49418146536,70.42629182199016],[309319.74411796685,-400.5329834310105],[309364.7233745931,-715.3877798067406],[309351.49418146536,-974.679965057876],[309232.4314433411,-1506.493528684834],[309176.8688322166,-1879.5567748110043],[309205.97305709217,-2040.952930937754],[308552.45091671497,-2472.2246268143645],[308147.6376070874,-3011.9757063165307],[308430.74233996496,-3734.289650943014],[308827.6181337144,-4363.999243696919],[309142.47293009236,-4490.9994976965245],[309479.5459254207,-4168.759302856284],[310007.24186643306,-3657.238962248084],[310429.9315185547,-3344.854614257114],[310773.8905639658,-3093.4998779299203],[311336.9246401787,-2797.156559649622],[311906.3095092783,-2564.3322143565165],[312525.43572998233,-2360.602661131881],[313125.56844345294,-2227.800214529503],[313970.063720705,-2077.4978637696477],[314456.89801025484,-1908.1641845697304],[314496.5855712909,-1831.4348754876992]]]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
+750
@@ -0,0 +1,750 @@
|
||||
<?php
|
||||
$conn = new mysqli("localhost", "root", "", "spbu");
|
||||
|
||||
// HANDLE INSERT
|
||||
if(isset($_POST['nama'])){
|
||||
$nama = $_POST['nama'];
|
||||
$wa = $_POST['wa'];
|
||||
$buka = $_POST['buka'];
|
||||
$lat = $_POST['lat'];
|
||||
$lng = $_POST['lng'];
|
||||
|
||||
$conn->query("INSERT INTO spbu (nama_spbu, nomor_wa, buka_24_jam, latitude, longitude)
|
||||
VALUES ('$nama','$wa','$buka','$lat','$lng')");
|
||||
exit("success");
|
||||
}
|
||||
|
||||
// HANDLE DELETE
|
||||
if(isset($_POST['delete_id'])){
|
||||
$id = intval($_POST['delete_id']);
|
||||
$conn->query("DELETE FROM spbu WHERE id = $id");
|
||||
exit("deleted");
|
||||
}
|
||||
|
||||
// HANDLE MOVE (update lat/lng)
|
||||
if(isset($_POST['move_id'])){
|
||||
$id = intval($_POST['move_id']);
|
||||
$lat = $_POST['move_lat'];
|
||||
$lng = $_POST['move_lng'];
|
||||
$conn->query("UPDATE spbu SET latitude='$lat', longitude='$lng' WHERE id=$id");
|
||||
exit("moved");
|
||||
}
|
||||
|
||||
// HANDLE GET DATA
|
||||
if(isset($_GET['get'])){
|
||||
$result = $conn->query("SELECT * FROM spbu");
|
||||
$data = [];
|
||||
while($row = $result->fetch_assoc()){
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SPBU Map — Pontianak</title>
|
||||
|
||||
<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>
|
||||
<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=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--fuel-orange: #FF5722;
|
||||
--fuel-amber: #FFA726;
|
||||
--night: #0D0F14;
|
||||
--panel: #13161E;
|
||||
--panel-border: rgba(255,255,255,0.07);
|
||||
--text: #F0F0F0;
|
||||
--text-muted: #8A8FA0;
|
||||
--green: #00C896;
|
||||
--red: #FF4757;
|
||||
--radius: 14px;
|
||||
--shadow: 0 20px 60px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
background: var(--night);
|
||||
color: var(--text);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#map {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
filter: brightness(0.9) saturate(0.85);
|
||||
}
|
||||
|
||||
/* ── HEADER ── */
|
||||
#header {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 50px;
|
||||
padding: 10px 22px 10px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
box-shadow: var(--shadow), 0 0 0 1px rgba(255,87,34,0.15);
|
||||
backdrop-filter: blur(20px);
|
||||
animation: slideDown 0.5s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: linear-gradient(135deg, var(--fuel-orange), var(--fuel-amber));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#header h1 {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#header p {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
/* ── LEGEND ── */
|
||||
#legend {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: 20px;
|
||||
z-index: 1000;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px 18px;
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(20px);
|
||||
animation: slideUp 0.5s 0.1s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
}
|
||||
|
||||
#legend h3 {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
margin-bottom: 7px;
|
||||
font-size: 12.5px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.legend-item:last-child { margin-bottom: 0; }
|
||||
|
||||
.legend-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 8px currentColor;
|
||||
}
|
||||
|
||||
.dot-green { background: var(--green); color: var(--green); }
|
||||
.dot-red { background: var(--red); color: var(--red); }
|
||||
|
||||
/* ── HINT CHIP ── */
|
||||
#hint {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 50px;
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(20px);
|
||||
animation: slideUp 0.5s 0.2s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
}
|
||||
|
||||
.hint-pulse {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--fuel-orange);
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── TOAST ── */
|
||||
#toast {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-10px);
|
||||
z-index: 2000;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 50px;
|
||||
padding: 10px 20px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(20px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s, transform 0.25s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#toast.show {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.toast-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── LEAFLET POPUP OVERRIDE ── */
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: var(--panel) !important;
|
||||
color: var(--text) !important;
|
||||
border: 1px solid var(--panel-border) !important;
|
||||
border-radius: var(--radius) !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 0 !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
background: var(--panel) !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-close-button {
|
||||
color: var(--text-muted) !important;
|
||||
font-size: 18px !important;
|
||||
top: 10px !important;
|
||||
right: 12px !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-close-button:hover {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
/* ── POPUP CONTENT ── */
|
||||
.popup-header {
|
||||
background: linear-gradient(135deg, rgba(255,87,34,0.15), rgba(255,167,38,0.08));
|
||||
padding: 16px 18px 14px;
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
}
|
||||
|
||||
.popup-header .popup-label {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--fuel-orange);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.popup-header .popup-title {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
padding: 14px 18px 16px;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 9px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.popup-row:last-child { margin-bottom: 0; }
|
||||
|
||||
.popup-row svg { flex-shrink: 0; opacity: 0.7; }
|
||||
|
||||
.popup-row a { color: var(--fuel-amber); text-decoration: none; }
|
||||
.popup-row a:hover { text-decoration: underline; }
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 50px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-green {
|
||||
background: rgba(0,200,150,0.12);
|
||||
color: var(--green);
|
||||
border: 1px solid rgba(0,200,150,0.2);
|
||||
}
|
||||
|
||||
.badge-red {
|
||||
background: rgba(255,71,87,0.12);
|
||||
color: var(--red);
|
||||
border: 1px solid rgba(255,71,87,0.2);
|
||||
}
|
||||
|
||||
/* ── POPUP FOOTER ── */
|
||||
.popup-footer {
|
||||
border-top: 1px solid var(--panel-border);
|
||||
padding: 10px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.drag-hint {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(255,71,87,0.1);
|
||||
color: var(--red);
|
||||
border: 1px solid rgba(255,71,87,0.2);
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.15s;
|
||||
}
|
||||
|
||||
.btn-delete:hover { background: rgba(255,71,87,0.22); transform: translateY(-1px); }
|
||||
.btn-delete:active { transform: translateY(0); }
|
||||
|
||||
/* ── FORM POPUP ── */
|
||||
.form-popup-header {
|
||||
background: linear-gradient(135deg, rgba(255,87,34,0.15), rgba(255,167,38,0.08));
|
||||
padding: 14px 18px 12px;
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
}
|
||||
|
||||
.form-popup-header .fp-label {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--fuel-orange);
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.form-popup-header .fp-title {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.form-popup-body {
|
||||
padding: 14px 18px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.fp-field label {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.fp-field input,
|
||||
.fp-field select {
|
||||
width: 100%;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 8px;
|
||||
padding: 9px 12px;
|
||||
font-size: 13px;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.fp-field input::placeholder { color: rgba(255,255,255,0.2); }
|
||||
|
||||
.fp-field input:focus,
|
||||
.fp-field select:focus {
|
||||
border-color: rgba(255,87,34,0.5);
|
||||
background: rgba(255,87,34,0.05);
|
||||
}
|
||||
|
||||
.fp-field select option { background: #1a1d26; color: var(--text); }
|
||||
|
||||
.fp-btn {
|
||||
width: 100%;
|
||||
background: linear-gradient(135deg, var(--fuel-orange), var(--fuel-amber));
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 11px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-family: 'Syne', sans-serif;
|
||||
letter-spacing: 0.04em;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.15s;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.fp-btn:hover { opacity: 0.88; transform: translateY(-1px); }
|
||||
.fp-btn:active { transform: translateY(0); opacity: 1; }
|
||||
|
||||
/* ── ANIMATIONS ── */
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(-20px); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.4; transform: scale(0.8); }
|
||||
}
|
||||
|
||||
.leaflet-control-attribution {
|
||||
background: rgba(13,15,20,0.7) !important;
|
||||
color: #666 !important;
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: 6px 0 0 0 !important;
|
||||
}
|
||||
|
||||
.leaflet-control-attribution a { color: #888 !important; }
|
||||
|
||||
.leaflet-control-zoom a {
|
||||
background: var(--panel) !important;
|
||||
color: var(--text) !important;
|
||||
border-color: var(--panel-border) !important;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a:hover {
|
||||
background: rgba(255,87,34,0.15) !important;
|
||||
color: var(--fuel-orange) !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<div id="header">
|
||||
<div class="header-icon">⛽</div>
|
||||
<div>
|
||||
<h1>SPBU Map</h1>
|
||||
<p>Pontianak, Kalimantan Barat</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="legend">
|
||||
<h3>Keterangan</h3>
|
||||
<div class="legend-item"><div class="legend-dot dot-green"></div>Buka 24 Jam</div>
|
||||
<div class="legend-item"><div class="legend-dot dot-red"></div>Tidak 24 Jam</div>
|
||||
</div>
|
||||
|
||||
<div id="hint">
|
||||
<div class="hint-pulse"></div>
|
||||
Klik peta untuk tambah SPBU
|
||||
</div>
|
||||
|
||||
<div id="toast">
|
||||
<div class="toast-dot" id="toast-dot"></div>
|
||||
<span id="toast-msg"></span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
// ── TOAST ──
|
||||
function showToast(msg, color) {
|
||||
const toast = document.getElementById('toast');
|
||||
document.getElementById('toast-dot').style.background = color || '#00C896';
|
||||
document.getElementById('toast-msg').textContent = msg;
|
||||
toast.classList.add('show');
|
||||
clearTimeout(toast._t);
|
||||
toast._t = setTimeout(() => toast.classList.remove('show'), 2500);
|
||||
}
|
||||
|
||||
// ── ICONS ──
|
||||
const greenIcon = new L.Icon({
|
||||
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png',
|
||||
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
|
||||
iconSize: [25, 41], iconAnchor: [12, 41]
|
||||
});
|
||||
|
||||
const redIcon = new L.Icon({
|
||||
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png',
|
||||
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
|
||||
iconSize: [25, 41], iconAnchor: [12, 41]
|
||||
});
|
||||
|
||||
// ── MAP ──
|
||||
const map = L.map('map').setView([-0.05538, 109.34947], 16);
|
||||
|
||||
// ── TILE ──
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
// ── LAYER GROUPS (MUST BE HERE) ──
|
||||
const layer24 = L.layerGroup();
|
||||
const layerNon24 = L.layerGroup();
|
||||
|
||||
// ── CONTROL ──
|
||||
const overlayMaps = {
|
||||
"SPBU 24 Jam": layer24,
|
||||
"SPBU Tidak 24 Jam": layerNon24
|
||||
};
|
||||
|
||||
L.control.layers(null, overlayMaps, { collapsed: false }).addTo(map);
|
||||
|
||||
// ── DEFAULT SHOW ──
|
||||
layer24.addTo(map);
|
||||
layerNon24.addTo(map);
|
||||
|
||||
// ── BUILD POPUP HTML ──
|
||||
function buildPopup(d) {
|
||||
const is24 = d.buka_24_jam == 1;
|
||||
return `
|
||||
<div class="popup-header">
|
||||
<div class="popup-label">Stasiun BBM</div>
|
||||
<div class="popup-title">${d.nama_spbu}</div>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="popup-row">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.63A2 2 0 012 0h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 14h-1.08"/></svg>
|
||||
<a href="https://wa.me/${d.nomor_wa}" target="_blank">${d.nomor_wa}</a>
|
||||
</div>
|
||||
<div class="popup-row">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
<span class="badge ${is24 ? 'badge-green' : 'badge-red'}">${is24 ? '✓ Buka 24 Jam' : '✗ Tidak 24 Jam'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="popup-footer">
|
||||
<span class="drag-hint">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="5 9 2 12 5 15"/><polyline points="9 5 12 2 15 5"/><polyline points="15 19 12 22 9 19"/><polyline points="19 9 22 12 19 15"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="12" y1="2" x2="12" y2="22"/></svg>
|
||||
Tarik untuk pindah
|
||||
</span>
|
||||
<button class="btn-delete" onclick="deleteEntry(${d.id}, this)">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
||||
Hapus
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ── LOAD & CREATE MARKERS ──
|
||||
// Keep a map of id → marker for deletion
|
||||
const markerMap = {};
|
||||
|
||||
function loadData() {
|
||||
// CLEAR OLD MARKERS FIRST
|
||||
layer24.clearLayers();
|
||||
layerNon24.clearLayers();
|
||||
|
||||
fetch('index.php?get=1')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
data.forEach(d => addMarker(d));
|
||||
});
|
||||
}
|
||||
|
||||
function addMarker(d) {
|
||||
const is24 = d.buka_24_jam == 1;
|
||||
const marker = L.marker([d.latitude, d.longitude], {
|
||||
icon: is24 ? greenIcon : redIcon,
|
||||
draggable: true
|
||||
});
|
||||
|
||||
if (is24) {
|
||||
marker.addTo(layer24);
|
||||
} else {
|
||||
marker.addTo(layerNon24);
|
||||
}
|
||||
|
||||
marker.bindPopup(buildPopup(d));
|
||||
markerMap[d.id] = marker;
|
||||
|
||||
// DRAG → AUTO SAVE
|
||||
marker.on('dragend', function(e) {
|
||||
map.closePopup();
|
||||
const { lat, lng } = e.target.getLatLng();
|
||||
const fd = new FormData();
|
||||
fd.append('move_id', d.id);
|
||||
fd.append('move_lat', lat);
|
||||
fd.append('move_lng', lng);
|
||||
fetch('index.php', { method: 'POST', body: fd })
|
||||
.then(() => {
|
||||
d.latitude = lat;
|
||||
d.longitude = lng;
|
||||
showToast('Posisi disimpan ✓', '#FFA726');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── LAYER GROUPS ──
|
||||
loadData();
|
||||
|
||||
// ── DELETE ──
|
||||
function deleteEntry(id) {
|
||||
if (!confirm('Hapus SPBU ini?')) return;
|
||||
const fd = new FormData();
|
||||
fd.append('delete_id', id);
|
||||
fetch('index.php', { method: 'POST', body: fd })
|
||||
.then(() => {
|
||||
map.closePopup();
|
||||
if (markerMap[id]) {
|
||||
layer24.removeLayer(markerMap[id]);
|
||||
layerNon24.removeLayer(markerMap[id]);
|
||||
delete markerMap[id];
|
||||
}
|
||||
showToast('SPBU dihapus', '#FF4757');
|
||||
});
|
||||
}
|
||||
|
||||
// ── ADD FORM ──
|
||||
function createForm(lat, lng) {
|
||||
return `
|
||||
<div class="form-popup-header">
|
||||
<div class="fp-label">Tambah Titik</div>
|
||||
<div class="fp-title">SPBU Baru</div>
|
||||
</div>
|
||||
<div class="form-popup-body">
|
||||
<div class="fp-field">
|
||||
<label>Nama SPBU</label>
|
||||
<input id="nama" placeholder="Contoh: Pertamina Sungai Raya">
|
||||
</div>
|
||||
<div class="fp-field">
|
||||
<label>Nomor WhatsApp</label>
|
||||
<input id="wa" placeholder="628xx...">
|
||||
</div>
|
||||
<div class="fp-field">
|
||||
<label>Buka 24 Jam?</label>
|
||||
<select id="buka">
|
||||
<option value="1">✓ Iya, 24 Jam</option>
|
||||
<option value="0">✗ Tidak</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="fp-btn" onclick="submitData(${lat}, ${lng})">Simpan SPBU</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ── SUBMIT ──
|
||||
function submitData(lat, lng) {
|
||||
const nama = document.getElementById('nama').value;
|
||||
const wa = document.getElementById('wa').value;
|
||||
const buka = document.getElementById('buka').value;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('nama', nama);
|
||||
fd.append('wa', wa);
|
||||
fd.append('buka', buka);
|
||||
fd.append('lat', lat);
|
||||
fd.append('lng', lng);
|
||||
|
||||
fetch('index.php', { method: 'POST', body: fd })
|
||||
.then(res => res.text())
|
||||
.then(() => {
|
||||
// Fetch the newly inserted row (last inserted id) by re-fetching all
|
||||
// and finding the one not yet in markerMap
|
||||
fetch('index.php?get=1')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
data.forEach(d => {
|
||||
if (!markerMap[d.id]) addMarker(d);
|
||||
});
|
||||
});
|
||||
map.closePopup();
|
||||
showToast('SPBU berhasil disimpan ✓', '#00C896');
|
||||
});
|
||||
}
|
||||
|
||||
// ── CLICK MAP → ADD FORM ──
|
||||
map.on('click', function(e) {
|
||||
L.popup()
|
||||
.setLatLng(e.latlng)
|
||||
.setContent(createForm(e.latlng.lat, e.latlng.lng))
|
||||
.openOn(map);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS spbu
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE spbu;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spbu (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
nama_spbu VARCHAR(255) NOT NULL,
|
||||
nomor_wa VARCHAR(20) NOT NULL,
|
||||
buka_24_jam TINYINT(1) NOT NULL DEFAULT 0,
|
||||
latitude DECIMAL(10,7) NOT NULL,
|
||||
longitude DECIMAL(10,7) NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Data contoh
|
||||
INSERT INTO spbu (nama_spbu, nomor_wa, buka_24_jam, latitude, longitude) VALUES
|
||||
('SPBU Pertamina Sungai Raya', '6281234567890', 1, -0.0553800, 109.3494700),
|
||||
('SPBU Pertamina Ahmad Yani', '6289876543210', 0, -0.0621000, 109.3420000);
|
||||
Binary file not shown.
@@ -0,0 +1,84 @@
|
||||
# WebGIS Pemetaan Kemiskinan Kota Pontianak
|
||||
|
||||
Aplikasi WebGIS sederhana berbasis Leaflet.js untuk memetakan data kemiskinan, sebaran penduduk miskin, log bantuan, dan rumah ibadah di Kota Pontianak.
|
||||
|
||||
> [!WARNING]
|
||||
> **DISCLAIMER — DATA DUMMY / BUKAN DATA NYATA**
|
||||
>
|
||||
> Seluruh data yang ditampilkan dalam aplikasi ini, termasuk namun tidak terbatas pada:
|
||||
> - Data nama, lokasi, dan kategori **penduduk miskin**
|
||||
> - **Foto rumah** dan **Foto Kartu Keluarga (KK)** yang diunggah
|
||||
> - Data **rumah ibadah** beserta radius jangkauannya
|
||||
> - **Log bantuan** yang tercatat di sistem
|
||||
>
|
||||
> adalah **data fiktif / dummy** yang dibuat semata-mata untuk keperluan **demonstrasi, pengembangan, dan pengujian sistem** ini.
|
||||
> Data tersebut **tidak mewakili kondisi, identitas, atau situasi nyata** dari individu maupun lokasi mana pun di Kota Pontianak.
|
||||
|
||||
## Fitur Utama
|
||||
|
||||
- **Pemetaan Interaktif**: Visualisasi peta dengan Leaflet.js yang berpusat di Kota Pontianak.
|
||||
- **Manajemen Data Spasial**:
|
||||
- **Rumah Ibadah**: Lokasi titik rumah ibadah (Masjid, Gereja, Vihara, dll) beserta jangkauan radius jemaah/bantuan.
|
||||
- **Penduduk Miskin**: Pemetaan warga kurang mampu dan integrasi log/riwayat bantuan sosial yang disalurkan melalui rumah ibadah terdekat.
|
||||
- **Pencarian Cepat**: Cari data rumah ibadah langsung dari bilah pencarian.
|
||||
- **Impor GeoJSON Eksternal**: Pengguna dapat menambahkan file GeoJSON dari perangkat lokal ke dalam peta.
|
||||
|
||||
## Persyaratan Sistem
|
||||
|
||||
- **Server Web**: Apache (misal menggunakan XAMPP atau Laragon).
|
||||
- **Bahasa Pemrograman**: PHP 7.4 ke atas.
|
||||
- **Database**: MySQL versi 5.7+ atau MariaDB versi 10.2+ (Wajib mendukung fitur fungsi spasial seperti `ST_GeomFromGeoJSON` dan `ST_AsGeoJSON`).
|
||||
|
||||
## Cara Instalasi & Penggunaan
|
||||
|
||||
1. **Unduh/Clone Repositori**:
|
||||
Tempatkan folder `webgis-poverty-pontianak` di dalam direktori root server web Anda (misalnya `C:/xampp/htdocs/webgis/webgis-poverty-pontianak`).
|
||||
|
||||
2. **Setup Database**:
|
||||
- Buka phpMyAdmin atau MySQL client pilihan Anda.
|
||||
- Buat database baru bernama `webgis`:
|
||||
```sql
|
||||
CREATE DATABASE webgis;
|
||||
```
|
||||
- Import file `database.sql` ke dalam database `webgis` tersebut.
|
||||
|
||||
3. **Konfigurasi Koneksi**:
|
||||
- Buka file [api/db_connect.php](api/db_connect.php).
|
||||
- Sesuaikan konfigurasi host, user, password, dan database sesuai dengan setup database MySQL lokal Anda:
|
||||
```php
|
||||
$host = "localhost";
|
||||
$user = "root";
|
||||
$pass = "";
|
||||
$db = "webgis";
|
||||
```
|
||||
|
||||
4. **Jalankan Aplikasi**:
|
||||
- Aktifkan modul **Apache** dan **MySQL** di Control Panel XAMPP Anda.
|
||||
- Buka browser dan navigasikan ke alamat:
|
||||
[http://localhost/webgis/webgis-poverty-pontianak/index.php](http://localhost/webgis/webgis-poverty-pontianak/index.php)
|
||||
|
||||
## Akun Akses Default (Login)
|
||||
|
||||
Anda dapat masuk ke dalam sistem menggunakan akun simulasi berikut:
|
||||
|
||||
- **Akun Administrator (Admin)**:
|
||||
- **Username**: `admin`
|
||||
- **Password**: `password`
|
||||
- **Hak Akses**: Mengelola data spasial rumah ibadah, penduduk miskin, mengelola akun pengelola rumah ibadah, dan manajemen pengguna secara penuh.
|
||||
|
||||
- **Akun Pengelola Rumah Ibadah**:
|
||||
- **Username**: `pengelola_mujahidin`
|
||||
- **Password**: `password`
|
||||
- **Hak Akses**: Mengelola (tambah, edit, hapus, impor, geser lokasi) data penduduk miskin, serta menginput log bantuan sosial yang disalurkan melalui masjid kelolaannya (Masjid Raya Mujahidin).
|
||||
|
||||
## Struktur Folder
|
||||
|
||||
- `api/` — Kumpulan REST API endpoint untuk operasi CRUD masing-masing fitur.
|
||||
- `rumah_ibadah/`, `penduduk_miskin/`, `log_bantuan/`
|
||||
- `assets/` — File aset frontend.
|
||||
- `css/` — Gaya UI (custom style.css).
|
||||
- `js/` — Logika frontend peta dan pengelolaan interaksi panel data.
|
||||
- `features/` — Modul fitur Leaflet (manajemen layer, geolokasi, dll).
|
||||
- `index.php` — Halaman utama interface WebGIS.
|
||||
- `database.sql` — Skema database beserta **data dummy** Pontianak untuk pengetesan awal. Seluruh data di dalamnya bersifat fiktif dan tidak merepresentasikan data nyata.
|
||||
- `uploads/` — Folder penyimpanan foto yang diunggah. File yang sudah ada merupakan contoh dummy semata.
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
function isLoggedIn() {
|
||||
return isset($_SESSION['user_id']);
|
||||
}
|
||||
|
||||
function getCurrentUser() {
|
||||
if (!isLoggedIn()) {
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
'id' => $_SESSION['user_id'],
|
||||
'username' => $_SESSION['username'],
|
||||
'role' => $_SESSION['role'],
|
||||
'ibadah_id' => $_SESSION['ibadah_id'] ? intval($_SESSION['ibadah_id']) : null
|
||||
];
|
||||
}
|
||||
|
||||
function requireLogin() {
|
||||
if (!isLoggedIn()) {
|
||||
header('HTTP/1.1 401 Unauthorized');
|
||||
echo json_encode(['status' => 'error', 'message' => 'Unauthorized: Silakan login terlebih dahulu.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function requireAdmin() {
|
||||
requireLogin();
|
||||
if ($_SESSION['role'] !== 'admin') {
|
||||
header('HTTP/1.1 403 Forbidden');
|
||||
echo json_encode(['status' => 'error', 'message' => 'Forbidden: Akses ditolak. Hanya Admin yang diizinkan.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function requireAdminOrPengelola() {
|
||||
requireLogin();
|
||||
if ($_SESSION['role'] !== 'admin' && $_SESSION['role'] !== 'pengelola') {
|
||||
header('HTTP/1.1 403 Forbidden');
|
||||
echo json_encode(['status' => 'error', 'message' => 'Forbidden: Akses ditolak.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function canManageIbadah($ibadah_id) {
|
||||
if (!isLoggedIn()) return false;
|
||||
if ($_SESSION['role'] === 'admin') return true;
|
||||
if ($_SESSION['role'] === 'pengelola') {
|
||||
return intval($_SESSION['ibadah_id']) === intval($ibadah_id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'isLoggedIn' => true,
|
||||
'data' => [
|
||||
'id' => intval($_SESSION['user_id']),
|
||||
'username' => $_SESSION['username'],
|
||||
'role' => $_SESSION['role'],
|
||||
'ibadah_id' => $_SESSION['ibadah_id'] ? intval($_SESSION['ibadah_id']) : null,
|
||||
'nama_ibadah' => $_SESSION['nama_ibadah'] ?? null
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'isLoggedIn' => false,
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
require_once '../auth_helper.php';
|
||||
requireLogin();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$user = getCurrentUser();
|
||||
$isPengelola = $user['role'] === 'pengelola';
|
||||
$ibadahId = $isPengelola ? intval($user['ibadah_id']) : null;
|
||||
|
||||
// ── Helper: Haversine distance in meters ──────────────────────────────────
|
||||
function haversine($lat1, $lng1, $lat2, $lng2) {
|
||||
$R = 6371000;
|
||||
$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);
|
||||
return $R * 2 * atan2(sqrt($a), sqrt(1-$a));
|
||||
}
|
||||
|
||||
// ── 1. Rumah Ibadah ───────────────────────────────────────────────────────
|
||||
if ($isPengelola && $ibadahId) {
|
||||
$riRes = $conn->query("SELECT * FROM rumah_ibadah WHERE id=$ibadahId");
|
||||
} else {
|
||||
$riRes = $conn->query("SELECT * FROM rumah_ibadah");
|
||||
}
|
||||
$ibadahList = [];
|
||||
while ($row = $riRes->fetch_assoc()) $ibadahList[] = $row;
|
||||
$totalIbadah = count($ibadahList);
|
||||
|
||||
// Distribusi per jenis
|
||||
$jenisDist = [];
|
||||
foreach ($ibadahList as $ib) {
|
||||
$j = $ib['jenis'] ?? 'Lainnya';
|
||||
$jenisDist[$j] = ($jenisDist[$j] ?? 0) + 1;
|
||||
}
|
||||
|
||||
// ── 2. Penduduk Miskin ────────────────────────────────────────────────────
|
||||
$pmRes = $conn->query("SELECT * FROM penduduk_miskin");
|
||||
$allMiskin = [];
|
||||
while ($row = $pmRes->fetch_assoc()) $allMiskin[] = $row;
|
||||
|
||||
// Filter: pengelola hanya lihat yang dalam radius ibadahnya
|
||||
if ($isPengelola && $ibadahId && !empty($ibadahList)) {
|
||||
$ib = $ibadahList[0];
|
||||
$filteredMiskin = array_filter($allMiskin, function($m) use ($ib) {
|
||||
return haversine((float)$ib['lat'], (float)$ib['lng'], (float)$m['lat'], (float)$m['lng']) <= (float)$ib['radius'];
|
||||
});
|
||||
$filteredMiskin = array_values($filteredMiskin);
|
||||
} else {
|
||||
$filteredMiskin = $allMiskin;
|
||||
}
|
||||
|
||||
$totalMiskin = count($filteredMiskin);
|
||||
$totalJiwa = array_sum(array_column($filteredMiskin, 'jumlah_jiwa'));
|
||||
|
||||
// Kategori bantuan
|
||||
$kategoriDist = [];
|
||||
foreach ($filteredMiskin as $m) {
|
||||
$k = $m['kategori_bantuan'] ?? 'Lainnya';
|
||||
$kategoriDist[$k] = ($kategoriDist[$k] ?? 0) + 1;
|
||||
}
|
||||
|
||||
// Terurus vs Tidak (dalam radius salah satu ibadah)
|
||||
$terurusCount = 0;
|
||||
$tidakTerurusCount = 0;
|
||||
foreach ($filteredMiskin as $m) {
|
||||
$inAnyRadius = false;
|
||||
foreach ($ibadahList as $ib) {
|
||||
if (haversine((float)$ib['lat'], (float)$ib['lng'], (float)$m['lat'], (float)$m['lng']) <= (float)$ib['radius']) {
|
||||
$inAnyRadius = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($inAnyRadius) $terurusCount++;
|
||||
else $tidakTerurusCount++;
|
||||
}
|
||||
|
||||
// Jika admin: hitung seluruh miskin dalam radius seluruh ibadah
|
||||
if (!$isPengelola) {
|
||||
$allIbadahRes = $conn->query("SELECT * FROM rumah_ibadah");
|
||||
$allIbadahList = [];
|
||||
while ($row = $allIbadahRes->fetch_assoc()) $allIbadahList[] = $row;
|
||||
|
||||
$terurusCount = 0;
|
||||
$tidakTerurusCount = 0;
|
||||
foreach ($allMiskin as $m) {
|
||||
$inAnyRadius = false;
|
||||
foreach ($allIbadahList as $ib) {
|
||||
if (haversine((float)$ib['lat'], (float)$ib['lng'], (float)$m['lat'], (float)$m['lng']) <= (float)$ib['radius']) {
|
||||
$inAnyRadius = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($inAnyRadius) $terurusCount++;
|
||||
else $tidakTerurusCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. Log Bantuan ────────────────────────────────────────────────────────
|
||||
if ($isPengelola && $ibadahId) {
|
||||
$logRes = $conn->query("
|
||||
SELECT lb.*, ri.nama as nama_ibadah, ri.jenis as jenis_ibadah, pm.nama as nama_kk
|
||||
FROM log_bantuan lb
|
||||
JOIN rumah_ibadah ri ON ri.id = lb.ibadah_id
|
||||
JOIN penduduk_miskin pm ON pm.id = lb.miskin_id
|
||||
WHERE lb.ibadah_id = $ibadahId
|
||||
ORDER BY lb.tanggal DESC, lb.id DESC
|
||||
LIMIT 10
|
||||
");
|
||||
$totalLogRes = $conn->query("SELECT COUNT(*) as cnt FROM log_bantuan WHERE ibadah_id=$ibadahId");
|
||||
} else {
|
||||
$logRes = $conn->query("
|
||||
SELECT lb.*, ri.nama as nama_ibadah, ri.jenis as jenis_ibadah, pm.nama as nama_kk
|
||||
FROM log_bantuan lb
|
||||
JOIN rumah_ibadah ri ON ri.id = lb.ibadah_id
|
||||
JOIN penduduk_miskin pm ON pm.id = lb.miskin_id
|
||||
ORDER BY lb.tanggal DESC, lb.id DESC
|
||||
LIMIT 10
|
||||
");
|
||||
$totalLogRes = $conn->query("SELECT COUNT(*) as cnt FROM log_bantuan");
|
||||
}
|
||||
|
||||
$recentLogs = [];
|
||||
while ($row = $logRes->fetch_assoc()) $recentLogs[] = $row;
|
||||
|
||||
$totalLog = 0;
|
||||
if ($totalLogRes && $trow = $totalLogRes->fetch_assoc()) $totalLog = (int)$trow['cnt'];
|
||||
|
||||
// Distribusi tipe bantuan dari log
|
||||
$tipeDist = [];
|
||||
if ($isPengelola && $ibadahId) {
|
||||
$tipeRes = $conn->query("SELECT tipe_bantuan, COUNT(*) as cnt FROM log_bantuan WHERE ibadah_id=$ibadahId GROUP BY tipe_bantuan");
|
||||
} else {
|
||||
$tipeRes = $conn->query("SELECT tipe_bantuan, COUNT(*) as cnt FROM log_bantuan GROUP BY tipe_bantuan");
|
||||
}
|
||||
while ($row = $tipeRes->fetch_assoc()) $tipeDist[$row['tipe_bantuan']] = (int)$row['cnt'];
|
||||
|
||||
// Bantuan per bulan (6 bulan terakhir)
|
||||
if ($isPengelola && $ibadahId) {
|
||||
$bulanRes = $conn->query("
|
||||
SELECT DATE_FORMAT(tanggal, '%Y-%m') as bulan, COUNT(*) as cnt
|
||||
FROM log_bantuan
|
||||
WHERE ibadah_id=$ibadahId AND tanggal >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
|
||||
GROUP BY bulan ORDER BY bulan ASC
|
||||
");
|
||||
} else {
|
||||
$bulanRes = $conn->query("
|
||||
SELECT DATE_FORMAT(tanggal, '%Y-%m') as bulan, COUNT(*) as cnt
|
||||
FROM log_bantuan
|
||||
WHERE tanggal >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
|
||||
GROUP BY bulan ORDER BY bulan ASC
|
||||
");
|
||||
}
|
||||
$bantuanPerBulan = [];
|
||||
while ($row = $bulanRes->fetch_assoc()) $bantuanPerBulan[] = $row;
|
||||
|
||||
// ── 4. Ibadah Tipe (admin only: performa per ibadah) ─────────────────────
|
||||
$ibadahPerforma = [];
|
||||
if (!$isPengelola) {
|
||||
$perfRes = $conn->query("
|
||||
SELECT ri.id, ri.nama, ri.jenis, ri.radius,
|
||||
COUNT(DISTINCT lb.id) as total_log,
|
||||
COUNT(DISTINCT lb.miskin_id) as total_penerima
|
||||
FROM rumah_ibadah ri
|
||||
LEFT JOIN log_bantuan lb ON lb.ibadah_id = ri.id
|
||||
GROUP BY ri.id
|
||||
ORDER BY total_log DESC
|
||||
");
|
||||
while ($row = $perfRes->fetch_assoc()) $ibadahPerforma[] = $row;
|
||||
}
|
||||
|
||||
// ── Output ────────────────────────────────────────────────────────────────
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'role' => $user['role'],
|
||||
'ibadah_id' => $ibadahId,
|
||||
'stats' => [
|
||||
'total_ibadah' => $totalIbadah,
|
||||
'total_miskin' => $totalMiskin,
|
||||
'total_jiwa' => $totalJiwa,
|
||||
'total_log' => $totalLog,
|
||||
'terurus' => $terurusCount,
|
||||
'tidak_terurus' => $tidakTerurusCount,
|
||||
'pct_terurus' => $totalMiskin > 0 ? round($terurusCount / $totalMiskin * 100, 1) : 0,
|
||||
],
|
||||
'distribusi' => [
|
||||
'jenis_ibadah' => $jenisDist,
|
||||
'kategori' => $kategoriDist,
|
||||
'tipe_bantuan' => $tipeDist,
|
||||
'per_bulan' => $bantuanPerBulan,
|
||||
],
|
||||
'ibadah_list' => $ibadahList,
|
||||
'ibadah_performa' => $ibadahPerforma,
|
||||
'recent_logs' => $recentLogs,
|
||||
]);
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
/**
|
||||
* stats_extended.php
|
||||
* Dashboard terintegrasi: webgis_poverty + jalan + spbu + penduduk choropleth
|
||||
*/
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
require_once '../auth_helper.php';
|
||||
requireLogin();
|
||||
|
||||
// ── Helper: buat koneksi terpisah ─────────────────────────────────────────
|
||||
function makeConn($dbName) {
|
||||
$c = new mysqli("localhost", "root", "", $dbName);
|
||||
if ($c->connect_error) return null;
|
||||
$c->set_charset("utf8mb4");
|
||||
return $c;
|
||||
}
|
||||
|
||||
// ── Koneksi ke masing-masing database ────────────────────────────────────
|
||||
$connPoverty = makeConn("webgis_poverty"); // atau "webgis" sesuai db.sql
|
||||
$connJalan = makeConn("jalan");
|
||||
$connSpbu = makeConn("spbu");
|
||||
|
||||
// ── 1. Data dari webgis_poverty (kemiskinan) ─────────────────────────────
|
||||
$poverty = [
|
||||
'total_miskin' => 0,
|
||||
'total_jiwa' => 0,
|
||||
'total_ibadah' => 0,
|
||||
'total_log' => 0,
|
||||
'terurus' => 0,
|
||||
'tidak_terurus' => 0,
|
||||
'pct_terurus' => 0,
|
||||
'distribusi' => ['kategori' => [], 'tipe_bantuan' => [], 'per_bulan' => []],
|
||||
];
|
||||
|
||||
if ($connPoverty) {
|
||||
// Total KK miskin & jiwa
|
||||
$r = $connPoverty->query("SELECT COUNT(*) as c, COALESCE(SUM(jumlah_jiwa),0) as j FROM penduduk_miskin");
|
||||
if ($r && $row = $r->fetch_assoc()) {
|
||||
$poverty['total_miskin'] = (int)$row['c'];
|
||||
$poverty['total_jiwa'] = (int)$row['j'];
|
||||
}
|
||||
|
||||
// Total rumah ibadah
|
||||
$r = $connPoverty->query("SELECT COUNT(*) as c FROM rumah_ibadah");
|
||||
if ($r && $row = $r->fetch_assoc()) $poverty['total_ibadah'] = (int)$row['c'];
|
||||
|
||||
// Total log bantuan
|
||||
$r = $connPoverty->query("SELECT COUNT(*) as c FROM log_bantuan");
|
||||
if ($r && $row = $r->fetch_assoc()) $poverty['total_log'] = (int)$row['c'];
|
||||
|
||||
// Kategori bantuan
|
||||
$r = $connPoverty->query("SELECT kategori_bantuan, COUNT(*) as c FROM penduduk_miskin GROUP BY kategori_bantuan");
|
||||
if ($r) while ($row = $r->fetch_assoc()) $poverty['distribusi']['kategori'][$row['kategori_bantuan']] = (int)$row['c'];
|
||||
|
||||
// Tipe bantuan dari log
|
||||
$r = $connPoverty->query("SELECT tipe_bantuan, COUNT(*) as c FROM log_bantuan GROUP BY tipe_bantuan");
|
||||
if ($r) while ($row = $r->fetch_assoc()) $poverty['distribusi']['tipe_bantuan'][$row['tipe_bantuan']] = (int)$row['c'];
|
||||
|
||||
// Bantuan per bulan (6 bulan terakhir)
|
||||
$r = $connPoverty->query("
|
||||
SELECT DATE_FORMAT(tanggal,'%Y-%m') as bulan, COUNT(*) as cnt
|
||||
FROM log_bantuan
|
||||
WHERE tanggal >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
|
||||
GROUP BY bulan ORDER BY bulan ASC
|
||||
");
|
||||
if ($r) while ($row = $r->fetch_assoc()) $poverty['distribusi']['per_bulan'][] = $row;
|
||||
|
||||
// Terurus vs tidak (radius sederhana: ada di radius salah satu ibadah)
|
||||
$ibadahRes = $connPoverty->query("SELECT lat, lng, radius FROM rumah_ibadah");
|
||||
$ibadahArr = [];
|
||||
if ($ibadahRes) while ($row = $ibadahRes->fetch_assoc()) $ibadahArr[] = $row;
|
||||
|
||||
$miskinRes = $connPoverty->query("SELECT lat, lng FROM penduduk_miskin");
|
||||
if ($miskinRes) {
|
||||
while ($m = $miskinRes->fetch_assoc()) {
|
||||
$inRadius = false;
|
||||
foreach ($ibadahArr as $ib) {
|
||||
$dlat = deg2rad($ib['lat'] - $m['lat']);
|
||||
$dlng = deg2rad($ib['lng'] - $m['lng']);
|
||||
$a = sin($dlat/2)**2 + cos(deg2rad($m['lat']))*cos(deg2rad($ib['lat']))*sin($dlng/2)**2;
|
||||
$dist = 6371000 * 2 * atan2(sqrt($a), sqrt(1-$a));
|
||||
if ($dist <= (float)$ib['radius']) { $inRadius = true; break; }
|
||||
}
|
||||
if ($inRadius) $poverty['terurus']++; else $poverty['tidak_terurus']++;
|
||||
}
|
||||
$tot = $poverty['total_miskin'];
|
||||
$poverty['pct_terurus'] = $tot > 0 ? round($poverty['terurus'] / $tot * 100, 1) : 0;
|
||||
}
|
||||
$connPoverty->close();
|
||||
}
|
||||
|
||||
// ── 2. Data Jalan ─────────────────────────────────────────────────────────
|
||||
$jalan = [
|
||||
'total_jalan' => 0,
|
||||
'total_panjang_m' => 0,
|
||||
'per_status' => [],
|
||||
'total_parsil' => 0,
|
||||
'total_luas_m2' => 0,
|
||||
'per_kepemilikan' => [],
|
||||
];
|
||||
|
||||
if ($connJalan) {
|
||||
$r = $connJalan->query("SELECT COUNT(*) as c, COALESCE(SUM(panjang_meter),0) as p FROM jalan");
|
||||
if ($r && $row = $r->fetch_assoc()) {
|
||||
$jalan['total_jalan'] = (int)$row['c'];
|
||||
$jalan['total_panjang_m'] = (float)$row['p'];
|
||||
}
|
||||
|
||||
$r = $connJalan->query("SELECT status_jalan, COUNT(*) as c FROM jalan GROUP BY status_jalan");
|
||||
if ($r) while ($row = $r->fetch_assoc()) $jalan['per_status'][$row['status_jalan']] = (int)$row['c'];
|
||||
|
||||
$r = $connJalan->query("SELECT COUNT(*) as c, COALESCE(SUM(luas_m2),0) as l FROM parsil_tanah");
|
||||
if ($r && $row = $r->fetch_assoc()) {
|
||||
$jalan['total_parsil'] = (int)$row['c'];
|
||||
$jalan['total_luas_m2'] = (float)$row['l'];
|
||||
}
|
||||
|
||||
$r = $connJalan->query("SELECT status_kepemilikan, COUNT(*) as c FROM parsil_tanah GROUP BY status_kepemilikan");
|
||||
if ($r) while ($row = $r->fetch_assoc()) $jalan['per_kepemilikan'][$row['status_kepemilikan']] = (int)$row['c'];
|
||||
|
||||
$connJalan->close();
|
||||
}
|
||||
|
||||
// ── 3. Data SPBU ──────────────────────────────────────────────────────────
|
||||
$spbu = [
|
||||
'total_spbu' => 0,
|
||||
'buka_24_jam' => 0,
|
||||
'tidak_24_jam' => 0,
|
||||
'list' => [],
|
||||
];
|
||||
|
||||
if ($connSpbu) {
|
||||
$r = $connSpbu->query("SELECT COUNT(*) as c FROM spbu");
|
||||
if ($r && $row = $r->fetch_assoc()) $spbu['total_spbu'] = (int)$row['c'];
|
||||
|
||||
$r = $connSpbu->query("SELECT SUM(buka_24_jam=1) as buka, SUM(buka_24_jam=0) as tidak FROM spbu");
|
||||
if ($r && $row = $r->fetch_assoc()) {
|
||||
$spbu['buka_24_jam'] = (int)$row['buka'];
|
||||
$spbu['tidak_24_jam'] = (int)$row['tidak'];
|
||||
}
|
||||
|
||||
$r = $connSpbu->query("SELECT nama_spbu, nomor_wa, buka_24_jam, latitude, longitude FROM spbu ORDER BY nama_spbu");
|
||||
if ($r) while ($row = $r->fetch_assoc()) $spbu['list'][] = $row;
|
||||
|
||||
$connSpbu->close();
|
||||
}
|
||||
|
||||
// ── 4. Data Penduduk (choropleth — static dari pontianak.json) ────────────
|
||||
// Data kepadatan per kecamatan sudah ada di file JS, ringkasan di sini
|
||||
$penduduk = [
|
||||
'kecamatan' => [
|
||||
['nama' => 'Pontianak Barat', 'density' => 9.331],
|
||||
['nama' => 'Pontianak Kota', 'density' => 8.102],
|
||||
['nama' => 'Pontianak Timur', 'density' => 9.196],
|
||||
['nama' => 'Pontianak Selatan', 'density' => 5.673],
|
||||
['nama' => 'Pontianak Utara', 'density' => 3.642],
|
||||
['nama' => 'Pontianak Tenggara', 'density' => 3.103],
|
||||
]
|
||||
];
|
||||
$penduduk['total_kecamatan'] = count($penduduk['kecamatan']);
|
||||
$penduduk['rata_density'] = round(array_sum(array_column($penduduk['kecamatan'],'density')) / $penduduk['total_kecamatan'], 3);
|
||||
$penduduk['terpadat'] = $penduduk['kecamatan'][array_search(max(array_column($penduduk['kecamatan'],'density')), array_column($penduduk['kecamatan'],'density'))]['nama'];
|
||||
|
||||
// ── Output ────────────────────────────────────────────────────────────────
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'poverty' => $poverty,
|
||||
'jalan' => $jalan,
|
||||
'spbu' => $spbu,
|
||||
'penduduk' => $penduduk,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
$host = "localhost";
|
||||
$user = "root";
|
||||
$pass = "";
|
||||
$db = "webgis";
|
||||
|
||||
// Coba koneksi ke server dan pilih database
|
||||
$conn = new mysqli($host, $user, $pass, $db);
|
||||
|
||||
if ($conn->connect_error) {
|
||||
die("Koneksi gagal: " . $conn->connect_error);
|
||||
}
|
||||
// Set charset
|
||||
$conn->set_charset("utf8mb4");
|
||||
|
||||
// Jika di-include oleh file API, biarkan $conn tersedia
|
||||
?>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once '../auth_helper.php';
|
||||
requireAdminOrPengelola();
|
||||
require_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$miskin_id = intval($data['miskin_id'] ?? 0);
|
||||
$ibadah_id = intval($data['ibadah_id'] ?? 0);
|
||||
$tipe_bantuan = trim($data['tipe_bantuan'] ?? '');
|
||||
$tanggal = trim($data['tanggal'] ?? '');
|
||||
$keterangan = trim($data['keterangan'] ?? '');
|
||||
|
||||
if ($_SESSION['role'] === 'pengelola') {
|
||||
if ($ibadah_id !== intval($_SESSION['ibadah_id'])) {
|
||||
header('HTTP/1.1 403 Forbidden');
|
||||
echo json_encode(['status' => 'error', 'message' => 'Forbidden: Anda tidak diizinkan membuat log untuk rumah ibadah lain.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$miskin_id || !$ibadah_id || !$tipe_bantuan || !$tanggal) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO log_bantuan (miskin_id, ibadah_id, tipe_bantuan, tanggal, keterangan) VALUES (?, ?, ?, ?, ?)";
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param('iisss', $miskin_id, $ibadah_id, $tipe_bantuan, $tanggal, $keterangan);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'id' => $stmt->insert_id]);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => $conn->error]);
|
||||
}
|
||||
$conn->close();
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once '../db_connect.php';
|
||||
|
||||
$miskin_id = isset($_GET['miskin_id']) ? intval($_GET['miskin_id']) : 0;
|
||||
|
||||
if (!$miskin_id) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'miskin_id diperlukan']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
lb.id,
|
||||
lb.tipe_bantuan,
|
||||
lb.tanggal,
|
||||
lb.keterangan,
|
||||
ri.nama AS nama_ibadah,
|
||||
ri.jenis AS jenis_ibadah
|
||||
FROM log_bantuan lb
|
||||
JOIN rumah_ibadah ri ON ri.id = lb.ibadah_id
|
||||
WHERE lb.miskin_id = ?
|
||||
ORDER BY lb.tanggal DESC, lb.id DESC
|
||||
";
|
||||
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param('i', $miskin_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
$data = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
echo json_encode(['status' => 'success', 'data' => $data]);
|
||||
$conn->close();
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
require_once 'db_connect.php';
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$username = trim($data['username'] ?? '');
|
||||
$password = trim($data['password'] ?? '');
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Username dan password wajib diisi']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cari user di database
|
||||
$sql = "SELECT u.*, ri.nama as nama_ibadah
|
||||
FROM users u
|
||||
LEFT JOIN rumah_ibadah ri ON u.ibadah_id = ri.id
|
||||
WHERE u.username = ?";
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param('s', $username);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
if ($result->num_rows === 1) {
|
||||
$user = $result->fetch_assoc();
|
||||
// Verifikasi password
|
||||
if (password_verify($password, $user['password'])) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$_SESSION['role'] = $user['role'];
|
||||
$_SESSION['ibadah_id'] = $user['ibadah_id'];
|
||||
$_SESSION['nama_ibadah'] = $user['nama_ibadah'];
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Login berhasil',
|
||||
'data' => [
|
||||
'id' => intval($user['id']),
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role'],
|
||||
'ibadah_id' => $user['ibadah_id'] ? intval($user['ibadah_id']) : null,
|
||||
'nama_ibadah' => $user['nama_ibadah']
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['status' => 'error', 'message' => 'Username atau password salah']);
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$_SESSION = array();
|
||||
|
||||
if (ini_get("session.use_cookies")) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000,
|
||||
$params["path"], $params["domain"],
|
||||
$params["secure"], $params["httponly"]
|
||||
);
|
||||
}
|
||||
|
||||
session_destroy();
|
||||
|
||||
echo json_encode(['status' => 'success', 'message' => 'Logout berhasil']);
|
||||
?>
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdminOrPengelola();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
// Ambil info ibadah pengelola jika role pengelola
|
||||
$currentUser = getCurrentUser();
|
||||
$ibadahFilter = null; // null = admin (tidak dibatasi)
|
||||
if ($currentUser && $currentUser['role'] === 'pengelola') {
|
||||
$ibadah_id = $currentUser['ibadah_id'];
|
||||
if (!$ibadah_id) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Akun pengelola tidak terhubung ke rumah ibadah manapun.']);
|
||||
exit;
|
||||
}
|
||||
$res = $conn->query("SELECT lat, lng, radius FROM rumah_ibadah WHERE id=$ibadah_id");
|
||||
if (!$res || $res->num_rows === 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Rumah ibadah yang dikelola tidak ditemukan.']);
|
||||
exit;
|
||||
}
|
||||
$ibadahFilter = $res->fetch_assoc();
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
|
||||
if (is_array($data)) {
|
||||
$conn->begin_transaction();
|
||||
$inserted = 0;
|
||||
$skipped = 0;
|
||||
$errors = [];
|
||||
foreach ($data as $item) {
|
||||
if (isset($item->lat) && isset($item->lng) && $item->lat !== '' && $item->lng !== '') {
|
||||
$nama = $conn->real_escape_string($item->nama ?? '');
|
||||
$kategori = $conn->real_escape_string($item->kategori_bantuan ?? 'Makan');
|
||||
$jumlah_jiwa = (int)($item->jumlah_jiwa ?? 1);
|
||||
$lat = (float)$item->lat;
|
||||
$lng = (float)$item->lng;
|
||||
|
||||
// Jika pengelola, cek apakah item berada dalam radius ibadahnya
|
||||
if ($ibadahFilter !== null) {
|
||||
$R = 6371000;
|
||||
$lat1 = deg2rad((float)$ibadahFilter['lat']);
|
||||
$lat2 = deg2rad($lat);
|
||||
$dLat = $lat2 - $lat1;
|
||||
$dLng = deg2rad($lng - (float)$ibadahFilter['lng']);
|
||||
$a = sin($dLat/2)*sin($dLat/2) + cos($lat1)*cos($lat2)*sin($dLng/2)*sin($dLng/2);
|
||||
$dist = $R * 2 * atan2(sqrt($a), sqrt(1-$a));
|
||||
if ($dist > (float)$ibadahFilter['radius']) {
|
||||
$skipped++;
|
||||
continue; // Lewati item di luar radius
|
||||
}
|
||||
}
|
||||
|
||||
$query = "INSERT INTO penduduk_miskin (nama, kategori_bantuan, jumlah_jiwa, lat, lng)
|
||||
VALUES ('$nama', '$kategori', $jumlah_jiwa, $lat, $lng)";
|
||||
|
||||
if (!$conn->query($query)) {
|
||||
$errors[] = $conn->error;
|
||||
} else {
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($errors) && $inserted > 0) {
|
||||
$conn->commit();
|
||||
$msg = "Berhasil mengimpor $inserted data penduduk miskin.";
|
||||
if ($skipped > 0) $msg .= " ($skipped data di luar radius diabaikan.)";
|
||||
echo json_encode(["status" => "success", "message" => $msg]);
|
||||
} else if ($inserted === 0) {
|
||||
$conn->rollback();
|
||||
$msg = $skipped > 0
|
||||
? "Semua $skipped data berada di luar radius rumah ibadah Anda, tidak ada yang diimpor."
|
||||
: "Tidak ada data valid yang diimpor.";
|
||||
echo json_encode(["status" => "error", "message" => $msg]);
|
||||
} else {
|
||||
$conn->rollback();
|
||||
echo json_encode(["status" => "error", "message" => "Gagal mengimpor beberapa data. Transaksi dibatalkan.", "errors" => $errors]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Format data salah."]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdminOrPengelola();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$nama = '';
|
||||
$kategori = 'Makan';
|
||||
$jumlah_jiwa = 1;
|
||||
$lat = 0.0;
|
||||
$lng = 0.0;
|
||||
|
||||
if (!empty($_POST)) {
|
||||
$nama = $conn->real_escape_string($_POST['nama'] ?? '');
|
||||
$kategori = $conn->real_escape_string($_POST['kategori_bantuan'] ?? 'Makan');
|
||||
$jumlah_jiwa = (int)($_POST['jumlah_jiwa'] ?? 1);
|
||||
$lat = (float)($_POST['lat'] ?? 0);
|
||||
$lng = (float)($_POST['lng'] ?? 0);
|
||||
} else {
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
if ($data) {
|
||||
$nama = $conn->real_escape_string($data->nama ?? '');
|
||||
$kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan');
|
||||
$jumlah_jiwa = (int)($data->jumlah_jiwa ?? 1);
|
||||
$lat = (float)($data->lat ?? 0);
|
||||
$lng = (float)($data->lng ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Validasi pengelola: hanya boleh tambah jika lokasi berada dalam radius ibadah yang dikelolanya
|
||||
$currentUser = getCurrentUser();
|
||||
if ($currentUser && $currentUser['role'] === 'pengelola') {
|
||||
$ibadah_id = $currentUser['ibadah_id'];
|
||||
if (!$ibadah_id) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Akun pengelola tidak terhubung ke rumah ibadah manapun.']);
|
||||
exit;
|
||||
}
|
||||
// Ambil data ibadah (lat, lng, radius)
|
||||
$res = $conn->query("SELECT lat, lng, radius FROM rumah_ibadah WHERE id=$ibadah_id");
|
||||
if (!$res || $res->num_rows === 0) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Rumah ibadah yang dikelola tidak ditemukan.']);
|
||||
exit;
|
||||
}
|
||||
$ib = $res->fetch_assoc();
|
||||
// Hitung jarak menggunakan Haversine formula
|
||||
$R = 6371000; // meter
|
||||
$lat1 = deg2rad((float)$ib['lat']);
|
||||
$lat2 = deg2rad($lat);
|
||||
$dLat = $lat2 - $lat1;
|
||||
$dLng = deg2rad($lng - (float)$ib['lng']);
|
||||
$a = sin($dLat/2)*sin($dLat/2) + cos($lat1)*cos($lat2)*sin($dLng/2)*sin($dLng/2);
|
||||
$dist = $R * 2 * atan2(sqrt($a), sqrt(1-$a));
|
||||
if ($dist > (float)$ib['radius']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Lokasi penduduk di luar radius rumah ibadah Anda. Hanya boleh menambah penduduk dalam radius yang dikelola.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if ($lat != 0 && $lng != 0) {
|
||||
$upload_dir = "../../uploads/";
|
||||
if (!file_exists($upload_dir)) {
|
||||
mkdir($upload_dir, 0777, true);
|
||||
}
|
||||
|
||||
$foto_rumah = null;
|
||||
$foto_kk = null;
|
||||
|
||||
function uploadFile($fileKey, $upload_dir) {
|
||||
if (isset($_FILES[$fileKey]) && $_FILES[$fileKey]['error'] === UPLOAD_ERR_OK) {
|
||||
$fileTmpPath = $_FILES[$fileKey]['tmp_name'];
|
||||
$fileName = $_FILES[$fileKey]['name'];
|
||||
|
||||
$fileNameCmps = explode(".", $fileName);
|
||||
$fileExtension = strtolower(end($fileNameCmps));
|
||||
|
||||
$allowedfileExtensions = array('jpg', 'jpeg', 'png', 'gif');
|
||||
if (in_array($fileExtension, $allowedfileExtensions)) {
|
||||
$newFileName = uniqid() . '_' . preg_replace('/[^a-zA-Z0-9\._-]/', '_', $fileName);
|
||||
$dest_path = $upload_dir . $newFileName;
|
||||
|
||||
if (move_uploaded_file($fileTmpPath, $dest_path)) {
|
||||
return $newFileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$foto_rumah = uploadFile('foto_rumah', $upload_dir);
|
||||
$foto_kk = uploadFile('foto_kk', $upload_dir);
|
||||
|
||||
$val_foto_rumah = $foto_rumah ? "'$foto_rumah'" : "NULL";
|
||||
$val_foto_kk = $foto_kk ? "'$foto_kk'" : "NULL";
|
||||
|
||||
$query = "INSERT INTO penduduk_miskin (nama, kategori_bantuan, jumlah_jiwa, lat, lng, foto_rumah, foto_kk)
|
||||
VALUES ('$nama', '$kategori', $jumlah_jiwa, $lat, $lng, $val_foto_rumah, $val_foto_kk)";
|
||||
|
||||
if ($conn->query($query)) {
|
||||
echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Berhasil ditambahkan."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Gagal: " . $conn->error]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Data tidak lengkap."]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdminOrPengelola();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
|
||||
if(!empty($data->id)) {
|
||||
$id = (int)$data->id;
|
||||
|
||||
// Validasi pengelola: hanya boleh hapus penduduk dalam radius ibadah yang dikelolanya
|
||||
$currentUser = getCurrentUser();
|
||||
if ($currentUser && $currentUser['role'] === 'pengelola') {
|
||||
$ibadah_id = $currentUser['ibadah_id'];
|
||||
if (!$ibadah_id) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Akun pengelola tidak terhubung ke rumah ibadah manapun.']);
|
||||
exit;
|
||||
}
|
||||
// Ambil koordinat penduduk
|
||||
$mRes = $conn->query("SELECT lat, lng FROM penduduk_miskin WHERE id=$id");
|
||||
if (!$mRes || $mRes->num_rows === 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data penduduk tidak ditemukan.']);
|
||||
exit;
|
||||
}
|
||||
$mRow = $mRes->fetch_assoc();
|
||||
$pLat = (float)$mRow['lat'];
|
||||
$pLng = (float)$mRow['lng'];
|
||||
|
||||
// Ambil data ibadah
|
||||
$iRes = $conn->query("SELECT lat, lng, radius FROM rumah_ibadah WHERE id=$ibadah_id");
|
||||
if (!$iRes || $iRes->num_rows === 0) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Rumah ibadah yang dikelola tidak ditemukan.']);
|
||||
exit;
|
||||
}
|
||||
$ib = $iRes->fetch_assoc();
|
||||
|
||||
// Hitung jarak Haversine
|
||||
$R = 6371000;
|
||||
$lat1 = deg2rad((float)$ib['lat']);
|
||||
$lat2 = deg2rad($pLat);
|
||||
$dLat = $lat2 - $lat1;
|
||||
$dLng = deg2rad($pLng - (float)$ib['lng']);
|
||||
$a = sin($dLat/2)*sin($dLat/2) + cos($lat1)*cos($lat2)*sin($dLng/2)*sin($dLng/2);
|
||||
$dist = $R * 2 * atan2(sqrt($a), sqrt(1-$a));
|
||||
if ($dist > (float)$ib['radius']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Penduduk ini berada di luar radius rumah ibadah Anda. Tidak diizinkan menghapus.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$query = "DELETE FROM penduduk_miskin WHERE id=$id";
|
||||
|
||||
if($conn->query($query)) {
|
||||
echo json_encode(["status" => "success", "message" => "Berhasil dihapus."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Gagal hapus: " . $conn->error]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$query = "SELECT * FROM penduduk_miskin";
|
||||
$result = $conn->query($query);
|
||||
|
||||
$arr = array();
|
||||
|
||||
if($result->num_rows > 0) {
|
||||
while($row = $result->fetch_assoc()) {
|
||||
$item = array(
|
||||
"id" => $row['id'],
|
||||
"nama" => $row['nama'],
|
||||
"kategori_bantuan" => $row['kategori_bantuan'],
|
||||
"jumlah_jiwa" => isset($row['jumlah_jiwa']) ? (int)$row['jumlah_jiwa'] : 1,
|
||||
"lat" => (float)$row['lat'],
|
||||
"lng" => (float)$row['lng'],
|
||||
"foto_rumah" => $row['foto_rumah'],
|
||||
"foto_kk" => $row['foto_kk']
|
||||
);
|
||||
array_push($arr, $item);
|
||||
}
|
||||
echo json_encode(["status" => "success", "data" => $arr]);
|
||||
} else {
|
||||
echo json_encode(["status" => "success", "data" => []]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdminOrPengelola();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$id = 0;
|
||||
$nama = '';
|
||||
$kategori = 'Makan';
|
||||
$jumlah_jiwa = 1;
|
||||
$lat = 0.0;
|
||||
$lng = 0.0;
|
||||
|
||||
if (!empty($_POST)) {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$nama = $conn->real_escape_string($_POST['nama'] ?? '');
|
||||
$kategori = $conn->real_escape_string($_POST['kategori_bantuan'] ?? 'Makan');
|
||||
$jumlah_jiwa = (int)($_POST['jumlah_jiwa'] ?? 1);
|
||||
$lat = (float)($_POST['lat'] ?? 0);
|
||||
$lng = (float)($_POST['lng'] ?? 0);
|
||||
} else {
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
if ($data) {
|
||||
$id = (int)($data->id ?? 0);
|
||||
$nama = $conn->real_escape_string($data->nama ?? '');
|
||||
$kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan');
|
||||
$jumlah_jiwa = (int)($data->jumlah_jiwa ?? 1);
|
||||
$lat = (float)($data->lat ?? 0);
|
||||
$lng = (float)($data->lng ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Validasi pengelola: hanya boleh edit penduduk yang lokasinya dalam radius ibadah yang dikelolanya
|
||||
$currentUser = getCurrentUser();
|
||||
if ($currentUser && $currentUser['role'] === 'pengelola') {
|
||||
$ibadah_id = $currentUser['ibadah_id'];
|
||||
if (!$ibadah_id) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Akun pengelola tidak terhubung ke rumah ibadah manapun.']);
|
||||
exit;
|
||||
}
|
||||
$res = $conn->query("SELECT lat, lng, radius FROM rumah_ibadah WHERE id=$ibadah_id");
|
||||
if (!$res || $res->num_rows === 0) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Rumah ibadah yang dikelola tidak ditemukan.']);
|
||||
exit;
|
||||
}
|
||||
$ib = $res->fetch_assoc();
|
||||
// Gunakan koordinat penduduk yang sedang diedit (dari DB jika lat/lng 0, atau dari input)
|
||||
$checkLat = $lat;
|
||||
$checkLng = $lng;
|
||||
if ($checkLat == 0 && $checkLng == 0 && $id > 0) {
|
||||
$mRes = $conn->query("SELECT lat, lng FROM penduduk_miskin WHERE id=$id");
|
||||
if ($mRes && $mRow = $mRes->fetch_assoc()) {
|
||||
$checkLat = (float)$mRow['lat'];
|
||||
$checkLng = (float)$mRow['lng'];
|
||||
}
|
||||
}
|
||||
$R = 6371000;
|
||||
$lat1 = deg2rad((float)$ib['lat']);
|
||||
$lat2 = deg2rad($checkLat);
|
||||
$dLat = $lat2 - $lat1;
|
||||
$dLng = deg2rad($checkLng - (float)$ib['lng']);
|
||||
$a = sin($dLat/2)*sin($dLat/2) + cos($lat1)*cos($lat2)*sin($dLng/2)*sin($dLng/2);
|
||||
$dist = $R * 2 * atan2(sqrt($a), sqrt(1-$a));
|
||||
if ($dist > (float)$ib['radius']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Penduduk ini berada di luar radius rumah ibadah Anda. Tidak diizinkan mengedit.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($id > 0) {
|
||||
// Get current files to delete them if updated
|
||||
$old_foto_rumah = null;
|
||||
$old_foto_kk = null;
|
||||
$res = $conn->query("SELECT foto_rumah, foto_kk FROM penduduk_miskin WHERE id=$id");
|
||||
if ($res && $row = $res->fetch_assoc()) {
|
||||
$old_foto_rumah = $row['foto_rumah'];
|
||||
$old_foto_kk = $row['foto_kk'];
|
||||
}
|
||||
|
||||
$upload_dir = "../../uploads/";
|
||||
if (!file_exists($upload_dir)) {
|
||||
mkdir($upload_dir, 0777, true);
|
||||
}
|
||||
|
||||
$foto_rumah = null;
|
||||
$foto_kk = null;
|
||||
|
||||
function uploadFile($fileKey, $upload_dir) {
|
||||
if (isset($_FILES[$fileKey]) && $_FILES[$fileKey]['error'] === UPLOAD_ERR_OK) {
|
||||
$fileTmpPath = $_FILES[$fileKey]['tmp_name'];
|
||||
$fileName = $_FILES[$fileKey]['name'];
|
||||
|
||||
$fileNameCmps = explode(".", $fileName);
|
||||
$fileExtension = strtolower(end($fileNameCmps));
|
||||
|
||||
$allowedfileExtensions = array('jpg', 'jpeg', 'png', 'gif');
|
||||
if (in_array($fileExtension, $allowedfileExtensions)) {
|
||||
$newFileName = uniqid() . '_' . preg_replace('/[^a-zA-Z0-9\._-]/', '_', $fileName);
|
||||
$dest_path = $upload_dir . $newFileName;
|
||||
|
||||
if (move_uploaded_file($fileTmpPath, $dest_path)) {
|
||||
return $newFileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$foto_rumah = uploadFile('foto_rumah', $upload_dir);
|
||||
$foto_kk = uploadFile('foto_kk', $upload_dir);
|
||||
|
||||
// Build update fields
|
||||
$update_fields = [
|
||||
"nama='$nama'",
|
||||
"kategori_bantuan='$kategori'",
|
||||
"jumlah_jiwa=$jumlah_jiwa",
|
||||
"lat=$lat",
|
||||
"lng=$lng"
|
||||
];
|
||||
|
||||
if ($foto_rumah !== null) {
|
||||
$update_fields[] = "foto_rumah='$foto_rumah'";
|
||||
if ($old_foto_rumah && file_exists($upload_dir . $old_foto_rumah)) {
|
||||
unlink($upload_dir . $old_foto_rumah);
|
||||
}
|
||||
}
|
||||
if ($foto_kk !== null) {
|
||||
$update_fields[] = "foto_kk='$foto_kk'";
|
||||
if ($old_foto_kk && file_exists($upload_dir . $old_foto_kk)) {
|
||||
unlink($upload_dir . $old_foto_kk);
|
||||
}
|
||||
}
|
||||
|
||||
$query = "UPDATE penduduk_miskin SET " . implode(", ", $update_fields) . " WHERE id=$id";
|
||||
|
||||
if ($conn->query($query)) {
|
||||
echo json_encode(["status" => "success", "message" => "Update berhasil."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Gagal update: " . $conn->error]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
|
||||
if (!empty($data->lat) && !empty($data->lng)) {
|
||||
$nama = $conn->real_escape_string($data->nama ?? '');
|
||||
$jenis = $conn->real_escape_string($data->jenis ?? 'Masjid');
|
||||
$alamat = $conn->real_escape_string($data->alamat ?? '');
|
||||
$radius = (float)($data->radius ?? 500);
|
||||
$lat = (float)$data->lat;
|
||||
$lng = (float)$data->lng;
|
||||
|
||||
$query = "INSERT INTO rumah_ibadah (nama, jenis, alamat, radius, lat, lng)
|
||||
VALUES ('$nama', '$jenis', '$alamat', $radius, $lat, $lng)";
|
||||
|
||||
if ($conn->query($query)) {
|
||||
echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Rumah Ibadah berhasil ditambahkan."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Gagal menambahkan: " . $conn->error]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Data tidak lengkap."]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
|
||||
if(!empty($data->id)) {
|
||||
$id = (int)$data->id;
|
||||
|
||||
$query = "DELETE FROM rumah_ibadah WHERE id=$id";
|
||||
|
||||
if($conn->query($query)) {
|
||||
echo json_encode(["status" => "success", "message" => "Berhasil dihapus."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Gagal hapus: " . $conn->error]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$query = "SELECT * FROM rumah_ibadah";
|
||||
$result = $conn->query($query);
|
||||
|
||||
$arr = array();
|
||||
|
||||
if($result->num_rows > 0) {
|
||||
while($row = $result->fetch_assoc()) {
|
||||
$item = array(
|
||||
"id" => $row['id'],
|
||||
"nama" => $row['nama'],
|
||||
"jenis" => $row['jenis'],
|
||||
"alamat" => $row['alamat'],
|
||||
"radius" => (float)$row['radius'],
|
||||
"lat" => (float)$row['lat'],
|
||||
"lng" => (float)$row['lng']
|
||||
);
|
||||
array_push($arr, $item);
|
||||
}
|
||||
echo json_encode(["status" => "success", "data" => $arr]);
|
||||
} else {
|
||||
echo json_encode(["status" => "success", "data" => []]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
|
||||
if (!empty($data->id)) {
|
||||
$id = (int)$data->id;
|
||||
$nama = $conn->real_escape_string($data->nama ?? '');
|
||||
$jenis = $conn->real_escape_string($data->jenis ?? 'Masjid');
|
||||
$alamat = $conn->real_escape_string($data->alamat ?? '');
|
||||
$radius = (float)($data->radius ?? 500);
|
||||
$lat = (float)$data->lat;
|
||||
$lng = (float)$data->lng;
|
||||
|
||||
$query = "UPDATE rumah_ibadah
|
||||
SET nama='$nama', jenis='$jenis', alamat='$alamat', radius=$radius, lat=$lat, lng=$lng
|
||||
WHERE id=$id";
|
||||
|
||||
if ($conn->query($query)) {
|
||||
echo json_encode(["status" => "success", "message" => "Update berhasil."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Gagal update: " . $conn->error]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
require_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$username = trim($data['username'] ?? '');
|
||||
$password = trim($data['password'] ?? '');
|
||||
$role = trim($data['role'] ?? 'pengelola');
|
||||
$ibadah_id = !empty($data['ibadah_id']) ? intval($data['ibadah_id']) : null;
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Username dan password wajib diisi']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($role !== 'admin' && $role !== 'pengelola') {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Role tidak valid']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cek apakah username sudah ada
|
||||
$stmt = $conn->prepare("SELECT id FROM users WHERE username = ?");
|
||||
$stmt->bind_param('s', $username);
|
||||
$stmt->execute();
|
||||
if ($stmt->get_result()->num_rows > 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Username sudah digunakan']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Hash password
|
||||
$hashed_password = password_hash($password, PASSWORD_BCRYPT);
|
||||
|
||||
// Insert user baru
|
||||
$stmt = $conn->prepare("INSERT INTO users (username, password, role, ibadah_id) VALUES (?, ?, ?, ?)");
|
||||
$stmt->bind_param('sssi', $username, $hashed_password, $role, $ibadah_id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'User berhasil ditambahkan', 'id' => $stmt->insert_id]);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Gagal menambahkan user: ' . $conn->error]);
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
require_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$id = intval($data['id'] ?? 0);
|
||||
|
||||
if (!$id) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID tidak valid']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($id === intval($_SESSION['user_id'])) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Anda tidak bisa menghapus akun Anda sendiri']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("DELETE FROM users WHERE id = ?");
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'User berhasil dihapus']);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Gagal menghapus user: ' . $conn->error]);
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
require_once '../db_connect.php';
|
||||
|
||||
$sql = "SELECT u.id, u.username, u.role, u.ibadah_id, ri.nama as nama_ibadah
|
||||
FROM users u
|
||||
LEFT JOIN rumah_ibadah ri ON u.ibadah_id = ri.id
|
||||
ORDER BY u.id DESC";
|
||||
|
||||
$result = $conn->query($sql);
|
||||
$arr = array();
|
||||
|
||||
if ($result && $result->num_rows > 0) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$item = array(
|
||||
"id" => intval($row['id']),
|
||||
"username" => $row['username'],
|
||||
"role" => $row['role'],
|
||||
"ibadah_id" => $row['ibadah_id'] ? intval($row['ibadah_id']) : null,
|
||||
"nama_ibadah" => $row['nama_ibadah'] ?? '-'
|
||||
);
|
||||
array_push($arr, $item);
|
||||
}
|
||||
echo json_encode(["status" => "success", "data" => $arr]);
|
||||
} else {
|
||||
echo json_encode(["status" => "success", "data" => []]);
|
||||
}
|
||||
$conn->close();
|
||||
?>
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
require_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$id = intval($data['id'] ?? 0);
|
||||
$username = trim($data['username'] ?? '');
|
||||
$password = trim($data['password'] ?? '');
|
||||
$role = trim($data['role'] ?? 'pengelola');
|
||||
$ibadah_id = !empty($data['ibadah_id']) ? intval($data['ibadah_id']) : null;
|
||||
|
||||
if (!$id || empty($username)) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Data tidak lengkap']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($role !== 'admin' && $role !== 'pengelola') {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Role tidak valid']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cek apakah username sudah ada di user lain
|
||||
$stmt = $conn->prepare("SELECT id FROM users WHERE username = ? AND id != ?");
|
||||
$stmt->bind_param('si', $username, $id);
|
||||
$stmt->execute();
|
||||
if ($stmt->get_result()->num_rows > 0) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Username sudah digunakan oleh user lain']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!empty($password)) {
|
||||
// Hash new password
|
||||
$hashed_password = password_hash($password, PASSWORD_BCRYPT);
|
||||
$stmt = $conn->prepare("UPDATE users SET username = ?, password = ?, role = ?, ibadah_id = ? WHERE id = ?");
|
||||
$stmt->bind_param('ssssi', $username, $hashed_password, $role, $ibadah_id, $id);
|
||||
} else {
|
||||
// Keep existing password
|
||||
$stmt = $conn->prepare("UPDATE users SET username = ?, role = ?, ibadah_id = ? WHERE id = ?");
|
||||
$stmt->bind_param('sssi', $username, $role, $ibadah_id, $id);
|
||||
}
|
||||
|
||||
if ($stmt->execute()) {
|
||||
echo json_encode(['status' => 'success', 'message' => 'User berhasil diperbarui']);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Gagal memperbarui user: ' . $conn->error]);
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
?>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,383 @@
|
||||
// --- Fitur Autentikasi dan Manajemen User (RBAC) ---
|
||||
|
||||
(function() {
|
||||
window.currentUser = null;
|
||||
|
||||
// Elements
|
||||
const authWidget = document.getElementById('authWidget');
|
||||
const loginModal = document.getElementById('loginModal');
|
||||
const loginUsernameInput = document.getElementById('loginUsername');
|
||||
const loginPasswordInput = document.getElementById('loginPassword');
|
||||
const loginSubmitBtn = document.getElementById('loginSubmitBtn');
|
||||
const loginErrorMsg = document.getElementById('loginErrorMsg');
|
||||
const closeLoginModal = document.getElementById('closeLoginModal');
|
||||
|
||||
// User Management Modal Elements
|
||||
const userManagementModal = document.getElementById('userManagementModal');
|
||||
const closeUserManagementModal = document.getElementById('closeUserManagementModal');
|
||||
const menuUsersBtn = document.getElementById('menuUsers');
|
||||
const userTableBody = document.getElementById('userTableBody');
|
||||
const btnSaveUser = document.getElementById('btnSaveUser');
|
||||
const btnCancelUserEdit = document.getElementById('btnCancelUserEdit');
|
||||
const userFormTitle = document.getElementById('userFormTitle');
|
||||
|
||||
const manageUserId = document.getElementById('manageUserId');
|
||||
const manageUsername = document.getElementById('manageUsername');
|
||||
const managePassword = document.getElementById('managePassword');
|
||||
const manageRole = document.getElementById('manageRole');
|
||||
const manageIbadahId = document.getElementById('manageIbadahId');
|
||||
const manageIbadahGroup = document.getElementById('manageIbadahGroup');
|
||||
|
||||
// Startup Session Check
|
||||
function checkSession() {
|
||||
fetch('api/check_session.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.isLoggedIn) {
|
||||
window.currentUser = data.data;
|
||||
} else {
|
||||
window.currentUser = null;
|
||||
}
|
||||
updateAuthUI();
|
||||
refreshAllLayers();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Session check failed:", err);
|
||||
window.currentUser = null;
|
||||
updateAuthUI();
|
||||
});
|
||||
}
|
||||
|
||||
// Update UI based on logged-in state
|
||||
function updateAuthUI() {
|
||||
if (!authWidget) return;
|
||||
|
||||
const dashboardBtn = document.getElementById('menuDashboard');
|
||||
|
||||
if (window.currentUser) {
|
||||
// Logged In state
|
||||
const roleLabel = window.currentUser.role === 'admin' ? 'Admin' : 'Pengelola';
|
||||
const extraLabel = window.currentUser.nama_ibadah ? ` - ${window.currentUser.nama_ibadah}` : '';
|
||||
|
||||
authWidget.innerHTML = `
|
||||
<div class="user-profile-pill">
|
||||
<div class="user-profile-info">
|
||||
<div class="user-profile-name">👤 ${escHtml(window.currentUser.username)}</div>
|
||||
<div class="user-profile-role">${roleLabel}${escHtml(extraLabel)}</div>
|
||||
</div>
|
||||
<button class="btn-profile-logout" id="authLogoutBtn" title="Logout">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('authLogoutBtn').addEventListener('click', logout);
|
||||
|
||||
// Show Dashboard button for all logged-in users
|
||||
if (dashboardBtn) dashboardBtn.style.display = 'flex';
|
||||
|
||||
// Show user management button in sidebar for Admin
|
||||
const adminDivider = document.getElementById('sidebarAdminDivider');
|
||||
if (window.currentUser.role === 'admin') {
|
||||
if (menuUsersBtn) menuUsersBtn.style.display = 'flex';
|
||||
if (adminDivider) adminDivider.style.display = 'block';
|
||||
} else {
|
||||
if (menuUsersBtn) menuUsersBtn.style.display = 'none';
|
||||
if (adminDivider) adminDivider.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
// Logged Out state
|
||||
authWidget.innerHTML = `
|
||||
<button class="btn-login" id="authLoginBtn"><i class="fas fa-sign-in-alt"></i> Login</button>
|
||||
`;
|
||||
document.getElementById('authLoginBtn').addEventListener('click', showLoginModal);
|
||||
if (menuUsersBtn) menuUsersBtn.style.display = 'none';
|
||||
if (dashboardBtn) dashboardBtn.style.display = 'none';
|
||||
const adminDivider = document.getElementById('sidebarAdminDivider');
|
||||
if (adminDivider) adminDivider.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function refreshAllLayers() {
|
||||
if (typeof loadSpbu === 'function') loadSpbu();
|
||||
if (typeof loadJalan === 'function') loadJalan();
|
||||
if (typeof loadParsil === 'function') loadParsil();
|
||||
if (typeof loadRumahIbadah === 'function') loadRumahIbadah();
|
||||
if (typeof loadPendudukMiskin === 'function') loadPendudukMiskin();
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
}
|
||||
|
||||
// Login Modals logic
|
||||
function showLoginModal() {
|
||||
loginUsernameInput.value = '';
|
||||
loginPasswordInput.value = '';
|
||||
loginErrorMsg.style.display = 'none';
|
||||
loginModal.classList.add('show');
|
||||
}
|
||||
|
||||
function hideLoginModal() {
|
||||
loginModal.classList.remove('show');
|
||||
}
|
||||
|
||||
if (closeLoginModal) {
|
||||
closeLoginModal.addEventListener('click', hideLoginModal);
|
||||
}
|
||||
|
||||
loginSubmitBtn.addEventListener('click', function() {
|
||||
const username = loginUsernameInput.value.trim();
|
||||
const password = loginPasswordInput.value;
|
||||
|
||||
if (!username || !password) {
|
||||
loginErrorMsg.textContent = 'Username dan password wajib diisi';
|
||||
loginErrorMsg.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('api/login.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
window.currentUser = data.data;
|
||||
hideLoginModal();
|
||||
updateAuthUI();
|
||||
refreshAllLayers();
|
||||
showToast('Login berhasil', 'success');
|
||||
} else {
|
||||
loginErrorMsg.textContent = data.message || 'Login gagal';
|
||||
loginErrorMsg.style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
loginErrorMsg.textContent = 'Koneksi ke server terputus';
|
||||
loginErrorMsg.style.display = 'block';
|
||||
});
|
||||
});
|
||||
|
||||
function logout() {
|
||||
fetch('api/logout.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
window.currentUser = null;
|
||||
updateAuthUI();
|
||||
refreshAllLayers();
|
||||
showToast('Logout berhasil', 'success');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Gagal logout', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// User Management Modal Logic
|
||||
if (menuUsersBtn) {
|
||||
menuUsersBtn.addEventListener('click', function() {
|
||||
userManagementModal.classList.add('show');
|
||||
loadUsers();
|
||||
populateIbadahDropdown();
|
||||
resetUserForm();
|
||||
});
|
||||
}
|
||||
|
||||
// Dashboard Button
|
||||
const menuDashboardBtn = document.getElementById('menuDashboard');
|
||||
if (menuDashboardBtn) {
|
||||
menuDashboardBtn.addEventListener('click', function() {
|
||||
if (typeof window.openDashboard === 'function') window.openDashboard();
|
||||
});
|
||||
}
|
||||
|
||||
if (closeUserManagementModal) {
|
||||
closeUserManagementModal.addEventListener('click', function() {
|
||||
userManagementModal.classList.remove('show');
|
||||
});
|
||||
}
|
||||
|
||||
// Show/hide ibadah dropdown based on selected role in form
|
||||
if (manageRole) {
|
||||
manageRole.addEventListener('change', function() {
|
||||
if (this.value === 'admin') {
|
||||
manageIbadahGroup.style.display = 'none';
|
||||
manageIbadahId.value = '';
|
||||
} else {
|
||||
manageIbadahGroup.style.display = 'flex';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function populateIbadahDropdown() {
|
||||
if (!manageIbadahId) return;
|
||||
manageIbadahId.innerHTML = '<option value="">-- Pilih Rumah Ibadah --</option>';
|
||||
if (typeof ibadahDataList !== 'undefined') {
|
||||
ibadahDataList.forEach(ib => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = ib.id;
|
||||
opt.textContent = `${ib.jenis || '🕌'} ${ib.nama}`;
|
||||
manageIbadahId.appendChild(opt);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function loadUsers() {
|
||||
if (!userTableBody) return;
|
||||
userTableBody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:15px; color:#aaa;">Memuat user...</td></tr>';
|
||||
|
||||
fetch('api/users/read.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
userTableBody.innerHTML = '';
|
||||
if (data.data.length === 0) {
|
||||
userTableBody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:15px; color:#aaa;">Belum ada user</td></tr>';
|
||||
return;
|
||||
}
|
||||
data.data.forEach(user => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.style.borderBottom = '1px solid #e2e8f0';
|
||||
|
||||
const roleBadge = user.role === 'admin'
|
||||
? '<span class="data-card-badge badge-red">Admin</span>'
|
||||
: '<span class="data-card-badge badge-blue">Pengelola</span>';
|
||||
|
||||
tr.innerHTML = `
|
||||
<td style="padding:10px 12px; font-weight:600;">${escHtml(user.username)}</td>
|
||||
<td style="padding:10px 12px;">${roleBadge}</td>
|
||||
<td style="padding:10px 12px; color:#64748b;">${escHtml(user.nama_ibadah)}</td>
|
||||
<td style="padding:10px 12px; text-align:center; display:flex; gap:6px; justify-content:center;">
|
||||
<button class="btn-card-action btn-card-edit" title="Edit User" onclick="editUser(${JSON.stringify(user).replace(/"/g, '"')})">
|
||||
<i class="fas fa-pen"></i>
|
||||
</button>
|
||||
<button class="btn-card-action btn-card-delete" title="Hapus User" onclick="deleteUser(${user.id})">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
userTableBody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
userTableBody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:15px; color:#ef4444;">Gagal memuat user</td></tr>';
|
||||
});
|
||||
}
|
||||
|
||||
window.editUser = function(user) {
|
||||
userFormTitle.textContent = 'Edit Pengguna';
|
||||
manageUserId.value = user.id;
|
||||
manageUsername.value = user.username;
|
||||
managePassword.value = ''; // Password cleared in edit
|
||||
manageRole.value = user.role;
|
||||
|
||||
if (user.role === 'admin') {
|
||||
manageIbadahGroup.style.display = 'none';
|
||||
manageIbadahId.value = '';
|
||||
} else {
|
||||
manageIbadahGroup.style.display = 'flex';
|
||||
manageIbadahId.value = user.ibadah_id || '';
|
||||
}
|
||||
|
||||
btnCancelUserEdit.style.display = 'inline-block';
|
||||
};
|
||||
|
||||
window.deleteUser = function(id) {
|
||||
if (confirm('Apakah Anda yakin ingin menghapus user ini?')) {
|
||||
fetch('api/users/delete.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
showToast(data.message, 'success');
|
||||
loadUsers();
|
||||
resetUserForm();
|
||||
} else {
|
||||
showToast(data.message || 'Gagal menghapus user', 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
showToast('Koneksi terputus', 'error');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
btnSaveUser.addEventListener('click', function() {
|
||||
const id = manageUserId.value;
|
||||
const username = manageUsername.value.trim();
|
||||
const password = managePassword.value;
|
||||
const role = manageRole.value;
|
||||
const ibadah_id = manageIbadahId.value;
|
||||
|
||||
if (!username) {
|
||||
alert('Username wajib diisi');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!id && !password) {
|
||||
alert('Password wajib diisi untuk user baru');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = { username, role, ibadah_id };
|
||||
if (id) payload.id = id;
|
||||
if (password) payload.password = password;
|
||||
|
||||
const endpoint = id ? 'api/users/update.php' : 'api/users/create.php';
|
||||
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
showToast(data.message, 'success');
|
||||
loadUsers();
|
||||
resetUserForm();
|
||||
} else {
|
||||
alert(data.message || 'Gagal menyimpan user');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
alert('Koneksi terputus');
|
||||
});
|
||||
});
|
||||
|
||||
btnCancelUserEdit.addEventListener('click', resetUserForm);
|
||||
|
||||
function resetUserForm() {
|
||||
userFormTitle.textContent = 'Tambah User Baru';
|
||||
manageUserId.value = '';
|
||||
manageUsername.value = '';
|
||||
managePassword.value = '';
|
||||
manageRole.value = 'pengelola';
|
||||
manageIbadahGroup.style.display = 'flex';
|
||||
manageIbadahId.value = '';
|
||||
btnCancelUserEdit.style.display = 'none';
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function escHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// Toast Notification helper
|
||||
function showToast(message, type = 'success') {
|
||||
if (typeof window.showToast === 'function') {
|
||||
window.showToast(message, type);
|
||||
return;
|
||||
}
|
||||
// Fallback alert
|
||||
alert(message);
|
||||
}
|
||||
|
||||
// Run session check on initialization
|
||||
checkSession();
|
||||
})();
|
||||
@@ -0,0 +1,468 @@
|
||||
// ===== Dashboard Feature (Terintegrasi) =====
|
||||
// Menghubungkan: webgis-poverty-pontianak, jalan, penduduk, spbu
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ── State ──────────────────────────────────────────────────────────────
|
||||
let dashboardData = null;
|
||||
let activeTab = 'kemiskinan';
|
||||
|
||||
// ── Modal Element ──────────────────────────────────────────────────────
|
||||
const modal = document.getElementById('dashboardModal');
|
||||
if (!modal) return;
|
||||
|
||||
// ── Open / Close ───────────────────────────────────────────────────────
|
||||
window.openDashboard = function () {
|
||||
modal.classList.add('show');
|
||||
if (!dashboardData) loadDashboard();
|
||||
else renderTab(activeTab);
|
||||
};
|
||||
|
||||
window.closeDashboard = function () {
|
||||
modal.classList.remove('show');
|
||||
};
|
||||
|
||||
document.getElementById('closeDashboardModal')?.addEventListener('click', window.closeDashboard);
|
||||
modal.addEventListener('click', function (e) {
|
||||
if (e.target === modal) window.closeDashboard();
|
||||
});
|
||||
|
||||
// ── Load Data ──────────────────────────────────────────────────────────
|
||||
function loadDashboard() {
|
||||
const body = document.getElementById('dashboardBody');
|
||||
body.innerHTML = `
|
||||
<div class="db-loading">
|
||||
<div class="db-spinner"></div>
|
||||
<span>Memuat data dashboard terintegrasi…</span>
|
||||
</div>`;
|
||||
|
||||
fetch('api/dashboard/stats_extended.php')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status !== 'success') throw new Error(data.message || 'Gagal memuat');
|
||||
dashboardData = data;
|
||||
renderShell();
|
||||
renderTab(activeTab);
|
||||
})
|
||||
.catch(err => {
|
||||
body.innerHTML = `<div class="db-error">❌ Gagal memuat data: ${err.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Shell (tab bar + container) ────────────────────────────────────────
|
||||
function renderShell() {
|
||||
const body = document.getElementById('dashboardBody');
|
||||
const tabs = [
|
||||
{ id: 'kemiskinan', label: '🏠 Kemiskinan' },
|
||||
{ id: 'jalan', label: '🛣️ Jalan' },
|
||||
{ id: 'spbu', label: '⛽ SPBU' },
|
||||
{ id: 'penduduk', label: '👥 Penduduk' },
|
||||
];
|
||||
|
||||
body.innerHTML = `
|
||||
<div class="db-tab-bar" id="dbTabBar">
|
||||
${tabs.map(t => `
|
||||
<button class="db-tab-btn${t.id === activeTab ? ' active' : ''}"
|
||||
data-tab="${t.id}">${t.label}</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div id="dbTabContent" class="db-tab-content"></div>`;
|
||||
|
||||
document.getElementById('dbTabBar').addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('[data-tab]');
|
||||
if (!btn) return;
|
||||
activeTab = btn.dataset.tab;
|
||||
document.querySelectorAll('.db-tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === activeTab));
|
||||
renderTab(activeTab);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Route to tab ───────────────────────────────────────────────────────
|
||||
function renderTab(tab) {
|
||||
const content = document.getElementById('dbTabContent');
|
||||
if (!content || !dashboardData) return;
|
||||
if (tab === 'kemiskinan') renderKemiskinan(content, dashboardData.poverty);
|
||||
else if (tab === 'jalan') renderJalan(content, dashboardData.jalan);
|
||||
else if (tab === 'spbu') renderSpbu(content, dashboardData.spbu);
|
||||
else if (tab === 'penduduk') renderPenduduk(content, dashboardData.penduduk);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// Tab 1: Kemiskinan
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
function renderKemiskinan(el, d) {
|
||||
const pctColor = d.pct_terurus >= 70 ? '#22c55e' : d.pct_terurus >= 40 ? '#f59e0b' : '#ef4444';
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="db-kpi-grid">
|
||||
<div class="db-kpi-card kpi-ibadah">
|
||||
<div class="db-kpi-icon">🕌</div>
|
||||
<div class="db-kpi-val">${d.total_ibadah}</div>
|
||||
<div class="db-kpi-label">Rumah Ibadah</div>
|
||||
</div>
|
||||
<div class="db-kpi-card kpi-miskin">
|
||||
<div class="db-kpi-icon">🏠</div>
|
||||
<div class="db-kpi-val">${d.total_miskin}</div>
|
||||
<div class="db-kpi-label">KK Miskin</div>
|
||||
</div>
|
||||
<div class="db-kpi-card kpi-jiwa">
|
||||
<div class="db-kpi-icon">👥</div>
|
||||
<div class="db-kpi-val">${d.total_jiwa}</div>
|
||||
<div class="db-kpi-label">Total Jiwa</div>
|
||||
</div>
|
||||
<div class="db-kpi-card kpi-log">
|
||||
<div class="db-kpi-icon">📋</div>
|
||||
<div class="db-kpi-val">${d.total_log}</div>
|
||||
<div class="db-kpi-label">Log Bantuan</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${d.total_miskin > 0 ? `
|
||||
<div class="db-section">
|
||||
<div class="db-section-title">📊 Cakupan Penanganan</div>
|
||||
<div class="db-coverage-row">
|
||||
<div class="db-coverage-labels">
|
||||
<span style="color:#22c55e">✅ Terurus: <strong>${d.terurus}</strong></span>
|
||||
<span style="color:#ef4444">⚠️ Tidak Terurus: <strong>${d.tidak_terurus}</strong></span>
|
||||
<span style="color:${pctColor}; font-weight:700">${d.pct_terurus}%</span>
|
||||
</div>
|
||||
<div class="db-progress-bar">
|
||||
<div class="db-progress-fill" style="width:${d.pct_terurus}%; background:${pctColor};"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>` : ''}
|
||||
|
||||
<div class="db-charts-grid">
|
||||
<div class="db-chart-card">
|
||||
<div class="db-chart-title">📦 Kategori Bantuan</div>
|
||||
<canvas id="chartKategori" height="180"></canvas>
|
||||
</div>
|
||||
<div class="db-chart-card">
|
||||
<div class="db-chart-title">🎁 Tipe Bantuan</div>
|
||||
<canvas id="chartTipe" height="180"></canvas>
|
||||
</div>
|
||||
<div class="db-chart-card db-chart-wide">
|
||||
<div class="db-chart-title">📅 Aktivitas Bantuan (6 Bulan)</div>
|
||||
<canvas id="chartBulan" height="140"></canvas>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
setTimeout(() => {
|
||||
drawPieChart('chartKategori', d.distribusi.kategori, ['#6366f1','#f59e0b','#22c55e','#ef4444','#06b6d4']);
|
||||
drawPieChart('chartTipe', d.distribusi.tipe_bantuan, ['#4f46e5','#16a34a','#dc2626','#d97706','#0891b2','#7c3aed']);
|
||||
drawBarChart('chartBulan', d.distribusi.per_bulan);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// Tab 2: Jalan
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
function renderJalan(el, d) {
|
||||
const panjangKm = (d.total_panjang_m / 1000).toFixed(2);
|
||||
const luasHa = (d.total_luas_m2 / 10000).toFixed(2);
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="db-kpi-grid">
|
||||
<div class="db-kpi-card" style="--kpi-color:#3b82f6">
|
||||
<div class="db-kpi-icon">🛣️</div>
|
||||
<div class="db-kpi-val">${d.total_jalan}</div>
|
||||
<div class="db-kpi-label">Ruas Jalan</div>
|
||||
</div>
|
||||
<div class="db-kpi-card" style="--kpi-color:#0ea5e9">
|
||||
<div class="db-kpi-icon">📏</div>
|
||||
<div class="db-kpi-val">${panjangKm} km</div>
|
||||
<div class="db-kpi-label">Total Panjang</div>
|
||||
</div>
|
||||
<div class="db-kpi-card" style="--kpi-color:#8b5cf6">
|
||||
<div class="db-kpi-icon">🗂️</div>
|
||||
<div class="db-kpi-val">${d.total_parsil}</div>
|
||||
<div class="db-kpi-label">Persil Tanah</div>
|
||||
</div>
|
||||
<div class="db-kpi-card" style="--kpi-color:#6d28d9">
|
||||
<div class="db-kpi-icon">📐</div>
|
||||
<div class="db-kpi-val">${luasHa} ha</div>
|
||||
<div class="db-kpi-label">Total Luas Persil</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="db-charts-grid">
|
||||
<div class="db-chart-card">
|
||||
<div class="db-chart-title">🛣️ Jalan per Status</div>
|
||||
<canvas id="chartStatusJalan" height="180"></canvas>
|
||||
</div>
|
||||
<div class="db-chart-card">
|
||||
<div class="db-chart-title">📋 Persil per Kepemilikan</div>
|
||||
<canvas id="chartKepemilikan" height="180"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${Object.keys(d.per_status).length > 0 ? `
|
||||
<div class="db-section">
|
||||
<div class="db-section-title">🛣️ Rincian Status Jalan</div>
|
||||
<div class="db-table-wrap">
|
||||
<table class="db-table">
|
||||
<thead><tr><th>Status</th><th>Jumlah Ruas</th></tr></thead>
|
||||
<tbody>
|
||||
${Object.entries(d.per_status).map(([k,v]) => `
|
||||
<tr><td>${esc(k)}</td><td><span class="db-badge">${v}</span></td></tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>` : '<div class="db-empty">Belum ada data jalan tersimpan di database.</div>'}`;
|
||||
|
||||
setTimeout(() => {
|
||||
drawPieChart('chartStatusJalan', d.per_status, ['#3b82f6','#0ea5e9','#38bdf8']);
|
||||
drawPieChart('chartKepemilikan', d.per_kepemilikan, ['#8b5cf6','#a78bfa','#c4b5fd','#7c3aed']);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// Tab 3: SPBU
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
function renderSpbu(el, d) {
|
||||
el.innerHTML = `
|
||||
<div class="db-kpi-grid">
|
||||
<div class="db-kpi-card" style="--kpi-color:#f97316">
|
||||
<div class="db-kpi-icon">⛽</div>
|
||||
<div class="db-kpi-val">${d.total_spbu}</div>
|
||||
<div class="db-kpi-label">Total SPBU</div>
|
||||
</div>
|
||||
<div class="db-kpi-card" style="--kpi-color:#22c55e">
|
||||
<div class="db-kpi-icon">🕐</div>
|
||||
<div class="db-kpi-val">${d.buka_24_jam}</div>
|
||||
<div class="db-kpi-label">Buka 24 Jam</div>
|
||||
</div>
|
||||
<div class="db-kpi-card" style="--kpi-color:#ef4444">
|
||||
<div class="db-kpi-icon">🕙</div>
|
||||
<div class="db-kpi-val">${d.tidak_24_jam}</div>
|
||||
<div class="db-kpi-label">Tidak 24 Jam</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="db-charts-grid">
|
||||
<div class="db-chart-card">
|
||||
<div class="db-chart-title">⏰ Status Operasional</div>
|
||||
<canvas id="chartSpbuOps" height="180"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${d.list.length > 0 ? `
|
||||
<div class="db-section">
|
||||
<div class="db-section-title">📍 Daftar SPBU</div>
|
||||
<div class="db-table-wrap">
|
||||
<table class="db-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Nama SPBU</th>
|
||||
<th>Nomor WA</th>
|
||||
<th>Operasional</th>
|
||||
<th>Koordinat</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${d.list.map((s, i) => `
|
||||
<tr>
|
||||
<td>${i+1}</td>
|
||||
<td><strong>${esc(s.nama_spbu)}</strong></td>
|
||||
<td>${esc(s.nomor_wa)}</td>
|
||||
<td>${s.buka_24_jam == 1
|
||||
? '<span class="db-badge" style="background:#22c55e">24 Jam</span>'
|
||||
: '<span class="db-badge" style="background:#f59e0b">Terbatas</span>'}</td>
|
||||
<td style="font-size:11px;color:#888">${parseFloat(s.latitude).toFixed(5)}, ${parseFloat(s.longitude).toFixed(5)}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>` : '<div class="db-empty">Belum ada data SPBU tersimpan di database.</div>'}`;
|
||||
|
||||
setTimeout(() => {
|
||||
drawPieChart('chartSpbuOps', { 'Buka 24 Jam': d.buka_24_jam, 'Tidak 24 Jam': d.tidak_24_jam }, ['#22c55e','#ef4444']);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// Tab 4: Penduduk (Choropleth Kecamatan)
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
function renderPenduduk(el, d) {
|
||||
const maxDensity = Math.max(...d.kecamatan.map(k => k.density));
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="db-kpi-grid">
|
||||
<div class="db-kpi-card" style="--kpi-color:#0891b2">
|
||||
<div class="db-kpi-icon">🗺️</div>
|
||||
<div class="db-kpi-val">${d.total_kecamatan}</div>
|
||||
<div class="db-kpi-label">Kecamatan</div>
|
||||
</div>
|
||||
<div class="db-kpi-card" style="--kpi-color:#06b6d4">
|
||||
<div class="db-kpi-icon">📊</div>
|
||||
<div class="db-kpi-val">${d.rata_density.toLocaleString('id-ID')}</div>
|
||||
<div class="db-kpi-label">Rata-rata Kepadatan (jiwa/km²)</div>
|
||||
</div>
|
||||
<div class="db-kpi-card" style="--kpi-color:#0e7490">
|
||||
<div class="db-kpi-icon">🏆</div>
|
||||
<div class="db-kpi-val" style="font-size:14px">${esc(d.terpadat)}</div>
|
||||
<div class="db-kpi-label">Kecamatan Terpadat</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="db-section">
|
||||
<div class="db-section-title">👥 Kepadatan Penduduk per Kecamatan (jiwa/km²)</div>
|
||||
${d.kecamatan.map(k => {
|
||||
const pct = ((k.density / maxDensity) * 100).toFixed(1);
|
||||
const color = k.density >= 8 ? '#ef4444' : k.density >= 5 ? '#f59e0b' : '#22c55e';
|
||||
return `
|
||||
<div style="margin-bottom:10px">
|
||||
<div style="display:flex; justify-content:space-between; font-size:13px; margin-bottom:3px">
|
||||
<span>${esc(k.nama)}</span>
|
||||
<strong style="color:${color}">${k.density.toLocaleString('id-ID')} jiwa/km²</strong>
|
||||
</div>
|
||||
<div class="db-progress-bar">
|
||||
<div class="db-progress-fill" style="width:${pct}%; background:${color};"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('')}
|
||||
<div style="margin-top:8px; font-size:11px; color:#888; display:flex; gap:16px">
|
||||
<span>🟢 <5 Rendah</span>
|
||||
<span>🟡 5–8 Sedang</span>
|
||||
<span>🔴 ≥8 Tinggi</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="db-charts-grid">
|
||||
<div class="db-chart-card db-chart-wide">
|
||||
<div class="db-chart-title">📊 Kepadatan per Kecamatan</div>
|
||||
<canvas id="chartDensity" height="160"></canvas>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
setTimeout(() => {
|
||||
drawDensityBar('chartDensity', d.kecamatan);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// Chart Helpers
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
function drawPieChart(canvasId, dataObj, colors) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
const entries = Object.entries(dataObj || {}).filter(([,v]) => v > 0);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (entries.length === 0) {
|
||||
ctx.fillStyle = '#aaa'; ctx.font = '13px Inter'; ctx.textAlign = 'center';
|
||||
ctx.fillText('Belum ada data', canvas.width/2, canvas.height/2);
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = entries.map(([k]) => k);
|
||||
const values = entries.map(([,v]) => v);
|
||||
const total = values.reduce((a,b) => a+b, 0);
|
||||
const cx = canvas.width/2, cy = canvas.height * 0.42;
|
||||
const r = Math.min(cx, cy) * 0.72;
|
||||
|
||||
let startAngle = -Math.PI/2;
|
||||
values.forEach((v,i) => {
|
||||
const slice = (v/total)*2*Math.PI;
|
||||
ctx.beginPath(); ctx.moveTo(cx, cy);
|
||||
ctx.arc(cx, cy, r, startAngle, startAngle+slice);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = colors[i % colors.length];
|
||||
ctx.fill(); ctx.strokeStyle='#fff'; ctx.lineWidth=2; ctx.stroke();
|
||||
startAngle += slice;
|
||||
});
|
||||
|
||||
ctx.fillStyle='#333'; ctx.font='bold 15px Inter'; ctx.textAlign='center';
|
||||
ctx.fillText(total, cx, cy+5);
|
||||
|
||||
const legY = cy+r+14, legItemW = 92, perRow = Math.floor(canvas.width/legItemW);
|
||||
labels.forEach((lbl,i) => {
|
||||
const row=Math.floor(i/perRow), col=i%perRow;
|
||||
const lx=(canvas.width - Math.min(labels.length,perRow)*legItemW)/2 + col*legItemW;
|
||||
const ly=legY+row*17;
|
||||
ctx.fillStyle=colors[i%colors.length]; ctx.fillRect(lx,ly,10,10);
|
||||
ctx.fillStyle='#555'; ctx.font='10px Inter'; ctx.textAlign='left';
|
||||
const shortLbl = lbl.length>11 ? lbl.slice(0,10)+'…' : lbl;
|
||||
ctx.fillText(`${shortLbl} (${values[i]})`, lx+13, ly+9);
|
||||
});
|
||||
}
|
||||
|
||||
function drawBarChart(canvasId, perBulan) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
if (!perBulan || perBulan.length === 0) {
|
||||
ctx.fillStyle='#aaa'; ctx.font='13px Inter'; ctx.textAlign='center';
|
||||
ctx.fillText('Belum ada data bantuan', canvas.width/2, canvas.height/2);
|
||||
return;
|
||||
}
|
||||
const padL=36, padR=12, padT=12, padB=28;
|
||||
const W=canvas.width-padL-padR, H=canvas.height-padT-padB;
|
||||
const values=perBulan.map(d=>parseInt(d.cnt)), maxVal=Math.max(...values,1);
|
||||
const barW=W/perBulan.length*0.6, gap=W/perBulan.length;
|
||||
ctx.strokeStyle='#e5e7eb'; ctx.lineWidth=1;
|
||||
for (let i=0;i<=4;i++){
|
||||
const y=padT+H-(i/4)*H;
|
||||
ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(padL+W,y); ctx.stroke();
|
||||
ctx.fillStyle='#9ca3af'; ctx.font='9px Inter'; ctx.textAlign='right';
|
||||
ctx.fillText(Math.round((i/4)*maxVal), padL-4, y+3);
|
||||
}
|
||||
perBulan.forEach((d,i)=>{
|
||||
const bh=(parseInt(d.cnt)/maxVal)*H, bx=padL+i*gap+(gap-barW)/2, by=padT+H-bh;
|
||||
const grad=ctx.createLinearGradient(0,by,0,padT+H);
|
||||
grad.addColorStop(0,'#6366f1'); grad.addColorStop(1,'#a5b4fc');
|
||||
ctx.fillStyle=grad;
|
||||
ctx.beginPath();
|
||||
if(ctx.roundRect) ctx.roundRect(bx,by,barW,bh,[4,4,0,0]); else ctx.rect(bx,by,barW,bh);
|
||||
ctx.fill();
|
||||
ctx.fillStyle='#4f46e5'; ctx.font='bold 10px Inter'; ctx.textAlign='center';
|
||||
ctx.fillText(d.cnt, bx+barW/2, by-4);
|
||||
const ml=d.bulan ? d.bulan.slice(5)+'/'+d.bulan.slice(2,4) : '';
|
||||
ctx.fillStyle='#6b7280'; ctx.font='9px Inter';
|
||||
ctx.fillText(ml, bx+barW/2, padT+H+14);
|
||||
});
|
||||
}
|
||||
|
||||
function drawDensityBar(canvasId, kecamatan) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
const padL=130, padR=50, padT=12, padB=12;
|
||||
const W=canvas.width-padL-padR, H=canvas.height-padT-padB;
|
||||
const maxVal=Math.max(...kecamatan.map(k=>k.density),1);
|
||||
const barH=Math.floor(H/kecamatan.length)-6;
|
||||
|
||||
kecamatan.forEach((k,i) => {
|
||||
const bw=(k.density/maxVal)*W;
|
||||
const by=padT+i*(barH+6);
|
||||
const color=k.density>=8?'#ef4444':k.density>=5?'#f59e0b':'#22c55e';
|
||||
// Label kecamatan
|
||||
ctx.fillStyle='#374151'; ctx.font='11px Inter'; ctx.textAlign='right';
|
||||
ctx.fillText(k.nama, padL-6, by+barH/2+4);
|
||||
// Bar
|
||||
ctx.fillStyle=color+'33';
|
||||
ctx.fillRect(padL, by, W, barH);
|
||||
const grad=ctx.createLinearGradient(padL,0,padL+bw,0);
|
||||
grad.addColorStop(0,color); grad.addColorStop(1,color+'99');
|
||||
ctx.fillStyle=grad;
|
||||
if(ctx.roundRect) ctx.roundRect(padL,by,bw,barH,[0,4,4,0]); else ctx.fillRect(padL,by,bw,barH);
|
||||
ctx.fill();
|
||||
// Value
|
||||
ctx.fillStyle=color; ctx.font='bold 11px Inter'; ctx.textAlign='left';
|
||||
ctx.fillText(k.density.toFixed(3), padL+bw+5, by+barH/2+4);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Utility ───────────────────────────────────────────────────────────
|
||||
function esc(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,181 @@
|
||||
// --- Fitur Import GeoJSON ---
|
||||
|
||||
const fileGeoJson = document.getElementById('fileGeoJson');
|
||||
let geoJsonLayers = {};
|
||||
let geoJsonCounter = 0;
|
||||
|
||||
window.toggleGeoJsonMenu = function() {
|
||||
const list = document.getElementById('geoJsonFileList');
|
||||
const icon = document.getElementById('geoJsonToggleIcon');
|
||||
if (list.style.display === 'none') {
|
||||
list.style.display = 'block';
|
||||
icon.className = 'fas fa-minus';
|
||||
} else {
|
||||
list.style.display = 'none';
|
||||
icon.className = 'fas fa-plus';
|
||||
}
|
||||
};
|
||||
|
||||
// Cek apakah fitur GeoJSON ini terlihat seperti data penduduk miskin
|
||||
function isMiskinFeature(feature) {
|
||||
if (!feature || !feature.geometry || feature.geometry.type !== 'Point') return false;
|
||||
const props = feature.properties || {};
|
||||
const keys = Object.keys(props).map(k => k.toLowerCase());
|
||||
// Dianggap data miskin jika punya properti nama (dan bukan ibadah atau spbu)
|
||||
const hasNama = keys.some(k => ['nama', 'name', 'penduduk'].includes(k));
|
||||
const isIbadah = keys.some(k => ['jenis', 'radius', 'alamat'].includes(k));
|
||||
const isSpbu = keys.some(k => ['alamat_spbu', 'is_24_jam'].includes(k));
|
||||
return hasNama && !isIbadah && !isSpbu;
|
||||
}
|
||||
|
||||
fileGeoJson.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
try {
|
||||
const geoJsonData = JSON.parse(event.target.result);
|
||||
const fileName = file.name;
|
||||
|
||||
// Pisahkan fitur menjadi dua kelompok: miskin dan non-miskin
|
||||
const features = geoJsonData.type === 'FeatureCollection'
|
||||
? geoJsonData.features
|
||||
: [geoJsonData];
|
||||
|
||||
const miskinFeatures = features.filter(f => isMiskinFeature(f));
|
||||
const otherFeatures = features.filter(f => !isMiskinFeature(f));
|
||||
|
||||
// ---- Proses fitur miskin: Simpan ke DB, lalu tampilkan seperti penduduk miskin ----
|
||||
if (miskinFeatures.length > 0) {
|
||||
const bulkData = miskinFeatures.map(f => {
|
||||
const props = f.properties || {};
|
||||
const coords = f.geometry.coordinates;
|
||||
return {
|
||||
nama: props.nama || props.name || props.penduduk || 'Data Impor',
|
||||
kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan',
|
||||
jumlah_jiwa: parseInt(props.jumlah_jiwa || props.jumlah || props.jiwa || 1, 10) || 1,
|
||||
lat: parseFloat(coords[1]),
|
||||
lng: parseFloat(coords[0])
|
||||
};
|
||||
});
|
||||
|
||||
fetch('api/penduduk_miskin/bulk_create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(bulkData)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// Reload layer penduduk miskin agar marker tampil dengan edit/hapus/log bantuan
|
||||
if (typeof loadPendudukMiskin === 'function') {
|
||||
loadPendudukMiskin();
|
||||
}
|
||||
if (typeof window.refreshActivePanel === 'function') {
|
||||
window.refreshActivePanel();
|
||||
}
|
||||
showToast(data.message || 'Data berhasil diimpor ke layer Penduduk Miskin.', 'success');
|
||||
} else {
|
||||
showToast(data.message || 'Gagal menyimpan data miskin.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Gagal terhubung ke server saat menyimpan data miskin.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Proses fitur non-miskin: Render sebagai layer GeoJSON biasa ----
|
||||
if (otherFeatures.length > 0) {
|
||||
const layerId = 'gj_' + (++geoJsonCounter);
|
||||
const individualLayer = L.featureGroup();
|
||||
|
||||
const otherGeoJson = {
|
||||
type: 'FeatureCollection',
|
||||
features: otherFeatures
|
||||
};
|
||||
|
||||
L.geoJSON(otherGeoJson, {
|
||||
pointToLayer: function(feature, latlng) {
|
||||
const props = feature.properties || {};
|
||||
let emoji = props.emoji;
|
||||
|
||||
if (!emoji) {
|
||||
const text = JSON.stringify(props).toLowerCase();
|
||||
if (text.includes('spbu')) emoji = '⛽';
|
||||
else if (text.includes('masjid')) emoji = '🕌';
|
||||
else if (text.includes('gereja')) emoji = '⛪';
|
||||
else if (text.includes('vihara')) emoji = '🪷';
|
||||
else if (text.includes('pura')) emoji = '🛕';
|
||||
else if (text.includes('kelenteng')) emoji = '🏮';
|
||||
else emoji = '📍';
|
||||
}
|
||||
|
||||
let cls = 'miskin-out';
|
||||
if (emoji === '⛽') cls = 'spbu-24';
|
||||
else if (['🕌','⛪','🛕','🪷','🏮'].includes(emoji)) cls = 'ibadah';
|
||||
|
||||
const icon = L.divIcon({
|
||||
className: '',
|
||||
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>${emoji}</span></div></div>`,
|
||||
iconSize: [38, 38],
|
||||
iconAnchor: [19, 38],
|
||||
popupAnchor: [0, -40]
|
||||
});
|
||||
|
||||
return L.marker(latlng, { icon: icon });
|
||||
},
|
||||
onEachFeature: function(feature, layer) {
|
||||
if (feature.properties) {
|
||||
let popupContent = '<h4>Informasi Feature</h4><ul style="list-style:none; padding:0; font-size:12px; margin: 0;">';
|
||||
for (let key in feature.properties) {
|
||||
popupContent += `<li><strong>${key}:</strong> ${feature.properties[key]}</li>`;
|
||||
}
|
||||
popupContent += '</ul>';
|
||||
layer.bindPopup(popupContent);
|
||||
}
|
||||
},
|
||||
style: function(feature) {
|
||||
return {
|
||||
color: (feature.properties && feature.properties.color) || '#3388ff',
|
||||
weight: 2,
|
||||
fillOpacity: 0.4
|
||||
};
|
||||
}
|
||||
}).addTo(individualLayer);
|
||||
|
||||
individualLayer.addTo(map);
|
||||
geoJsonLayers[layerId] = individualLayer;
|
||||
|
||||
// Tambahkan checkbox ke UI
|
||||
const container = document.getElementById('geoJsonLayersContainer');
|
||||
const label = document.createElement('label');
|
||||
label.className = 'layer-option';
|
||||
label.innerHTML = `<input type="checkbox" id="chk_${layerId}" checked> ${fileName}`;
|
||||
|
||||
label.querySelector('input').addEventListener('change', function(e) {
|
||||
if (e.target.checked) {
|
||||
map.addLayer(geoJsonLayers[layerId]);
|
||||
} else {
|
||||
map.removeLayer(geoJsonLayers[layerId]);
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(label);
|
||||
|
||||
if (miskinFeatures.length === 0) {
|
||||
showToast('File GeoJSON berhasil dimuat!', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// Reset input file agar bisa import file yang sama
|
||||
fileGeoJson.value = '';
|
||||
|
||||
} catch (error) {
|
||||
showToast('Gagal memproses file GeoJSON. Pastikan format valid.', 'error');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
// --- Fitur Geolocation ---
|
||||
|
||||
// Tambahkan tombol di dalam wadah zoom control
|
||||
const geoBtn = document.createElement('button');
|
||||
geoBtn.className = 'custom-layer-btn';
|
||||
geoBtn.style.position = 'relative';
|
||||
geoBtn.style.top = '0';
|
||||
geoBtn.style.left = '0';
|
||||
geoBtn.style.right = 'auto';
|
||||
geoBtn.innerHTML = '<i class="fas fa-crosshairs fa-lg"></i>';
|
||||
geoBtn.title = 'Lokasi Saya';
|
||||
document.querySelector('.custom-zoom-control').appendChild(geoBtn);
|
||||
|
||||
let userMarker = null;
|
||||
|
||||
geoBtn.addEventListener('click', function() {
|
||||
map.locate({setView: true, maxZoom: 16});
|
||||
});
|
||||
|
||||
map.on('locationfound', function(e) {
|
||||
if (userMarker) {
|
||||
map.removeLayer(userMarker);
|
||||
}
|
||||
userMarker = L.marker(e.latlng).addTo(map)
|
||||
.bindPopup("Anda berada di sini!").openPopup();
|
||||
});
|
||||
|
||||
map.on('locationerror', function(e) {
|
||||
alert("Gagal mendapatkan lokasi Anda.");
|
||||
});
|
||||
@@ -0,0 +1,916 @@
|
||||
// --- Fitur Pemetaan Kemiskinan ---
|
||||
|
||||
// Emoji per jenis ibadah
|
||||
const IBADAH_EMOJI_MAP = {
|
||||
'Masjid': '🕌',
|
||||
'Gereja': '⛪',
|
||||
'Pura': '🛕',
|
||||
'Vihara': '🪷',
|
||||
'Kelenteng': '🏮',
|
||||
};
|
||||
|
||||
// Emoji Bubble Icon builder
|
||||
function makeIbadahIcon(jenis) {
|
||||
const emot = IBADAH_EMOJI_MAP[jenis] || '🕌';
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div class="emoji-marker"><div class="bubble ibadah"><span>${emot}</span></div></div>`,
|
||||
iconSize: [38, 38],
|
||||
iconAnchor: [19, 38],
|
||||
popupAnchor: [0, -40]
|
||||
});
|
||||
}
|
||||
|
||||
function makeMiskinIcon(inRadius) {
|
||||
const cls = inRadius ? 'miskin-in' : 'miskin-out';
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>🏠</span></div></div>`,
|
||||
iconSize: [38, 38],
|
||||
iconAnchor: [19, 38],
|
||||
popupAnchor: [0, -40]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let ibadahDataList = [];
|
||||
let miskinMarkerList = []; // Simpan referensi marker untuk update warna
|
||||
|
||||
// Helper: cek apakah koordinat (lat, lng) berada dalam radius ibadah pengelola yang login
|
||||
function isInMyIbadahRadius(lat, lng) {
|
||||
if (!window.currentUser || window.currentUser.role !== 'pengelola') return true; // admin selalu bisa
|
||||
const myIbadahId = window.currentUser.ibadah_id;
|
||||
if (!myIbadahId) return false;
|
||||
const ib = ibadahDataList.find(i => i.id == myIbadahId);
|
||||
if (!ib) return false;
|
||||
const dist = L.latLng(lat, lng).distanceTo(L.latLng(ib.lat, ib.lng));
|
||||
return dist <= ib.radius;
|
||||
}
|
||||
window.isInMyIbadahRadius = isInMyIbadahRadius;
|
||||
|
||||
let isResizing = false;
|
||||
let resizingCircle = null;
|
||||
let resizingIbadah = null;
|
||||
|
||||
map.on('mousemove', function(e) {
|
||||
if (isResizing && resizingCircle && resizingIbadah) {
|
||||
const center = resizingCircle.getLatLng();
|
||||
const newRadius = center.distanceTo(e.latlng);
|
||||
resizingCircle.setRadius(newRadius);
|
||||
resizingIbadah.radius = newRadius;
|
||||
|
||||
// Update data radius di marker juga
|
||||
rumahIbadahLayer.eachLayer(function(layer) {
|
||||
if (layer.ibadahData && layer.ibadahData.id === resizingIbadah.id && layer instanceof L.Marker) {
|
||||
layer.ibadahData.radius = newRadius;
|
||||
}
|
||||
});
|
||||
|
||||
updateSemuaWarnaMiskin();
|
||||
}
|
||||
});
|
||||
|
||||
map.on('mouseup', function(e) {
|
||||
if (isResizing) {
|
||||
isResizing = false;
|
||||
map.dragging.enable();
|
||||
if (resizingIbadah) {
|
||||
updateIbadah(resizingIbadah.id, resizingIbadah.nama, resizingIbadah.jenis, resizingIbadah.alamat, resizingIbadah.radius, resizingIbadah.lat, resizingIbadah.lng);
|
||||
}
|
||||
resizingCircle = null;
|
||||
resizingIbadah = null;
|
||||
}
|
||||
});
|
||||
|
||||
// --- Rumah Ibadah ---
|
||||
function loadRumahIbadah() {
|
||||
rumahIbadahLayer.clearLayers();
|
||||
ibadahDataList = [];
|
||||
fetch('api/rumah_ibadah/read.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.data) {
|
||||
ibadahDataList = data.data;
|
||||
data.data.forEach(item => {
|
||||
addIbadahMarker(item);
|
||||
});
|
||||
updateSemuaWarnaMiskin();
|
||||
}
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
});
|
||||
}
|
||||
|
||||
function addIbadahMarker(item) {
|
||||
const lat = parseFloat(item.lat);
|
||||
const lng = parseFloat(item.lng);
|
||||
const radius = parseFloat(item.radius) || 100; // fallback if invalid
|
||||
|
||||
if (isNaN(lat) || isNaN(lng)) return;
|
||||
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
|
||||
const marker = L.marker([lat, lng], { icon: makeIbadahIcon(item.jenis), draggable: isAdmin });
|
||||
|
||||
// Lingkaran radius
|
||||
const circle = L.circle([lat, lng], {
|
||||
radius: radius,
|
||||
color: '#ff7800',
|
||||
weight: 2,
|
||||
fillColor: '#ff7800',
|
||||
fillOpacity: 0.2
|
||||
});
|
||||
|
||||
circle.on('mousemove', function(e) {
|
||||
if (!isAdmin || isResizing) return;
|
||||
const center = circle.getLatLng();
|
||||
const radius = circle.getRadius();
|
||||
const dist = center.distanceTo(e.latlng);
|
||||
|
||||
const metersPerPixel = map.distance(map.containerPointToLatLng([0,0]), map.containerPointToLatLng([0,1]));
|
||||
const tolerance = Math.max(radius * 0.1, metersPerPixel * 15); // toleransi 15px atau 10%
|
||||
|
||||
if (Math.abs(dist - radius) <= tolerance) {
|
||||
if (circle._path) circle._path.style.cursor = 'ew-resize';
|
||||
circle.nearEdge = true;
|
||||
} else {
|
||||
if (circle._path) circle._path.style.cursor = 'pointer';
|
||||
circle.nearEdge = false;
|
||||
}
|
||||
});
|
||||
|
||||
circle.on('mousedown', function(e) {
|
||||
if (isAdmin && circle.nearEdge) {
|
||||
map.dragging.disable();
|
||||
isResizing = true;
|
||||
resizingCircle = circle;
|
||||
resizingIbadah = item;
|
||||
L.DomEvent.stopPropagation(e);
|
||||
}
|
||||
});
|
||||
|
||||
marker.ibadahData = item;
|
||||
marker.circleLayer = circle;
|
||||
|
||||
const d = item;
|
||||
const emot = IBADAH_EMOJI_MAP[d.jenis] || '🕌';
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div style="display:flex; gap:5px;">
|
||||
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditIbadahModal(${d.id})">Edit</button>
|
||||
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteIbadah(${d.id})">Hapus</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const popupContent = `
|
||||
<div style="font-family: Arial, sans-serif; min-width: 160px;">
|
||||
<h4 style="margin:0 0 5px 0;">${emot} ${d.nama}</h4>
|
||||
<p style="margin: 0 0 3px 0;"><b>Jenis:</b> ${d.jenis || 'Masjid'}</p>
|
||||
<p style="margin: 0 0 3px 0;"><b>Alamat:</b> ${d.alamat}</p>
|
||||
<p style="margin: 0 0 10px 0;"><b>Radius:</b> ${d.radius} m</p>
|
||||
${actionButtons}
|
||||
</div>
|
||||
`;
|
||||
marker.bindPopup(popupContent);
|
||||
|
||||
marker.on('dragend', function(e) {
|
||||
const newPos = marker.getLatLng();
|
||||
circle.setLatLng(newPos);
|
||||
item.lat = newPos.lat;
|
||||
item.lng = newPos.lng;
|
||||
// Update ke DB
|
||||
updateIbadah(item.id, item.nama, item.jenis, item.alamat, item.radius, newPos.lat, newPos.lng);
|
||||
});
|
||||
|
||||
rumahIbadahLayer.addLayer(circle);
|
||||
rumahIbadahLayer.addLayer(marker);
|
||||
}
|
||||
|
||||
// Konteks Menu untuk Tambah Penduduk Miskin (klik kanan peta)
|
||||
map.on('contextmenu', function(e) {
|
||||
|
||||
const isAdminOrPengelola = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
|
||||
if (!isAdminOrPengelola) return;
|
||||
|
||||
const canAddHere = isInMyIbadahRadius(e.latlng.lat, e.latlng.lng);
|
||||
|
||||
if (!canAddHere) {
|
||||
// Pengelola: lokasi di luar radius ibadahnya
|
||||
const warnContent = `
|
||||
<div class="popup-form">
|
||||
<h4>⚠️ Di Luar Radius</h4>
|
||||
<p style="font-size:12px; color:#e11d48; margin:4px 0 0;">Anda hanya dapat menambah penduduk miskin di dalam radius rumah ibadah yang Anda kelola.</p>
|
||||
</div>
|
||||
`;
|
||||
L.popup().setLatLng(e.latlng).setContent(warnContent).openOn(map);
|
||||
return;
|
||||
}
|
||||
|
||||
const popupContent = `
|
||||
<div class="popup-form">
|
||||
<h4>Tambah Titik</h4>
|
||||
<button class="btn-danger" style="margin-top:5px;" onclick="formAddMiskin(${e.latlng.lat}, ${e.latlng.lng})">Tambah Penduduk Miskin</button>
|
||||
</div>
|
||||
`;
|
||||
L.popup().setLatLng(e.latlng).setContent(popupContent).openOn(map);
|
||||
});
|
||||
|
||||
// Map Click -> Form Add Rumah Ibadah
|
||||
map.on('click', function(e) {
|
||||
if (window.currentAddMode === 'rumah_ibadah') {
|
||||
const lat = e.latlng.lat;
|
||||
const lng = e.latlng.lng;
|
||||
|
||||
// Reverse Geocoding
|
||||
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`)
|
||||
.then(res => res.json())
|
||||
.then(geoData => {
|
||||
const alamat = geoData.display_name || 'Alamat tidak ditemukan';
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nama Rumah Ibadah</label>
|
||||
<input type="text" id="modalIbadahNama" placeholder="Nama Rumah Ibadah">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Jenis</label>
|
||||
<select id="modalIbadahJenis">
|
||||
<option value="Masjid">🕌 Masjid</option>
|
||||
<option value="Gereja">⛪ Gereja</option>
|
||||
<option value="Pura">🛕 Pura</option>
|
||||
<option value="Vihara">🪷 Vihara</option>
|
||||
<option value="Kelenteng">🏮 Kelenteng</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Alamat (Auto)</label>
|
||||
<textarea id="modalIbadahAlamat" rows="3">${alamat}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Radius (m)</label>
|
||||
<input type="number" id="modalIbadahRadius" value="500">
|
||||
</div>
|
||||
`;
|
||||
|
||||
openModal("Tambah Rumah Ibadah", bodyHTML, function() {
|
||||
window.saveNewIbadah(lat, lng, 'modalIbadahNama', 'modalIbadahJenis', 'modalIbadahAlamat', 'modalIbadahRadius');
|
||||
});
|
||||
|
||||
window.deactivateAddMode();
|
||||
})
|
||||
.catch(err => {
|
||||
// Tetap buka modal meskipun gagal mendapatkan alamat
|
||||
const alamat = '';
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nama Rumah Ibadah</label>
|
||||
<input type="text" id="modalIbadahNama" placeholder="Nama Rumah Ibadah">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Jenis</label>
|
||||
<select id="modalIbadahJenis">
|
||||
<option value="Masjid">🕌 Masjid</option>
|
||||
<option value="Gereja">⛪ Gereja</option>
|
||||
<option value="Pura">🛕 Pura</option>
|
||||
<option value="Vihara">🪷 Vihara</option>
|
||||
<option value="Kelenteng">🏮 Kelenteng</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Alamat (Manual)</label>
|
||||
<textarea id="modalIbadahAlamat" rows="3" placeholder="Masukkan alamat manual">${alamat}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Radius (m)</label>
|
||||
<input type="number" id="modalIbadahRadius" value="500">
|
||||
</div>
|
||||
`;
|
||||
|
||||
openModal("Tambah Rumah Ibadah", bodyHTML, function() {
|
||||
window.saveNewIbadah(lat, lng, 'modalIbadahNama', 'modalIbadahJenis', 'modalIbadahAlamat', 'modalIbadahRadius');
|
||||
});
|
||||
|
||||
window.deactivateAddMode();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.saveNewIbadah = function(lat, lng, namaId, jenisId, alamatId, radiusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const jenis = document.getElementById(jenisId).value;
|
||||
const alamat = document.getElementById(alamatId).value;
|
||||
const radius = document.getElementById(radiusId).value;
|
||||
|
||||
if (!nama) { alert("Nama Rumah Ibadah harus diisi!"); return; }
|
||||
|
||||
fetch('api/rumah_ibadah/create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nama, jenis, alamat, radius, lat, lng })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') { closeModal(); loadRumahIbadah(); }
|
||||
else { alert(data.message); }
|
||||
});
|
||||
};
|
||||
|
||||
window.openEditIbadahModal = function(id) {
|
||||
let d = null;
|
||||
rumahIbadahLayer.eachLayer(function(layer) {
|
||||
if (layer.ibadahData && layer.ibadahData.id == id) d = layer.ibadahData;
|
||||
});
|
||||
if (!d) return;
|
||||
|
||||
const jenisOpts = ['Masjid','Gereja','Pura','Vihara','Kelenteng'].map(j =>
|
||||
`<option value="${j}" ${d.jenis === j ? 'selected' : ''}>${IBADAH_EMOJI_MAP[j] || ''} ${j}</option>`
|
||||
).join('');
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Rumah Ibadah</label>
|
||||
<input type="text" id="editIbadahNama" value="${d.nama}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Jenis</label>
|
||||
<select id="editIbadahJenis">${jenisOpts}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Alamat</label>
|
||||
<textarea id="editIbadahAlamat" rows="2">${d.alamat}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Radius (m)</label>
|
||||
<input type="number" id="editIbadahRadius" value="${d.radius}" oninput="previewRadius(${d.id}, this.value)">
|
||||
</div>
|
||||
`;
|
||||
map.closePopup();
|
||||
openModal("Edit Rumah Ibadah", bodyHTML, function() {
|
||||
window.saveEditIbadah(d.id, d.lat, d.lng, 'editIbadahNama', 'editIbadahJenis', 'editIbadahAlamat', 'editIbadahRadius');
|
||||
});
|
||||
};
|
||||
|
||||
window.previewRadius = function(id, newRadius) {
|
||||
rumahIbadahLayer.eachLayer(function(layer) {
|
||||
// layer ini bisa marker atau circle, kita cek yang punya circleLayer (marker)
|
||||
if (layer.ibadahData && layer.ibadahData.id === id && layer.circleLayer) {
|
||||
layer.circleLayer.setRadius(parseFloat(newRadius));
|
||||
layer.ibadahData.radius = parseFloat(newRadius);
|
||||
updateSemuaWarnaMiskin(); // Update warna miskin real-time
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.saveEditIbadah = function(id, lat, lng, namaId, jenisId, alamatId, radiusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const jenis = document.getElementById(jenisId).value;
|
||||
const alamat = document.getElementById(alamatId).value;
|
||||
const radius = document.getElementById(radiusId).value;
|
||||
updateIbadah(id, nama, jenis, alamat, radius, lat, lng);
|
||||
};
|
||||
|
||||
function updateIbadah(id, nama, jenis, alamat, radius, lat, lng) {
|
||||
fetch('api/rumah_ibadah/update.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, nama, jenis, alamat, radius, lat, lng })
|
||||
}).then(res => res.json()).then(data => {
|
||||
if(data.status === 'success') {
|
||||
if (typeof closeModal === 'function') closeModal();
|
||||
map.closePopup();
|
||||
loadRumahIbadah();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.deleteIbadah = function(id) {
|
||||
openConfirmModal("Yakin hapus rumah ibadah ini?", function() {
|
||||
fetch('api/rumah_ibadah/delete.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
}).then(res => res.json()).then(data => {
|
||||
if(data.status === 'success') { map.closePopup(); loadRumahIbadah(); }
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// --- Penduduk Miskin ---
|
||||
function loadPendudukMiskin() {
|
||||
pendudukMiskinLayer.clearLayers();
|
||||
miskinMarkerList = [];
|
||||
fetch('api/penduduk_miskin/read.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.data) {
|
||||
data.data.forEach(item => {
|
||||
addMiskinMarker(item);
|
||||
});
|
||||
updateSemuaWarnaMiskin();
|
||||
}
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
});
|
||||
}
|
||||
|
||||
function addMiskinMarker(item) {
|
||||
const lat = parseFloat(item.lat);
|
||||
const lng = parseFloat(item.lng);
|
||||
if (isNaN(lat) || isNaN(lng)) return;
|
||||
|
||||
// Pengelola hanya bisa manage marker yang ada di dalam radius ibadahnya
|
||||
const isAdminOrPengelola = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
|
||||
const canManage = isAdminOrPengelola && isInMyIbadahRadius(lat, lng);
|
||||
|
||||
const marker = L.marker([lat, lng], { icon: makeMiskinIcon(false), draggable: canManage });
|
||||
marker.miskinData = item;
|
||||
|
||||
const d = item;
|
||||
let actionButtons = '';
|
||||
if (canManage) {
|
||||
actionButtons = `
|
||||
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditMiskinModal(${d.id})">Edit</button>
|
||||
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteMiskin(${d.id})">Hapus</button>
|
||||
`;
|
||||
}
|
||||
|
||||
let photoLinksHtml = '';
|
||||
if (d.foto_rumah) {
|
||||
photoLinksHtml += `
|
||||
<div style="margin: 5px 0;">
|
||||
<b style="font-size:11px;">Foto Rumah:</b><br>
|
||||
<a href="/poverty/uploads/${d.foto_rumah}" target="_blank">
|
||||
<img src="/poverty/uploads/${d.foto_rumah}" alt="Foto Rumah"
|
||||
style="max-width:150px; max-height:100px; border-radius:4px; margin-top:3px; border:1px solid #ddd; object-fit:cover; cursor:pointer;"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
|
||||
<span style="display:none; font-size:11px; color:#ef4444;">❌ Foto tidak dapat dimuat</span>
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
if (d.foto_kk) {
|
||||
photoLinksHtml += `
|
||||
<div style="margin: 5px 0;">
|
||||
<b style="font-size:11px;">Foto KK:</b><br>
|
||||
<a href="/poverty/uploads/${d.foto_kk}" target="_blank">
|
||||
<img src="/poverty/uploads/${d.foto_kk}" alt="Foto KK"
|
||||
style="max-width:150px; max-height:100px; border-radius:4px; margin-top:3px; border:1px solid #ddd; object-fit:cover; cursor:pointer;"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
|
||||
<span style="display:none; font-size:11px; color:#ef4444;">❌ Foto tidak dapat dimuat</span>
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const popupContent = `
|
||||
<div style="font-family: Arial, sans-serif; min-width: 165px;">
|
||||
<h4 style="margin:0 0 5px 0;">🏠 ${d.nama}</h4>
|
||||
<p style="margin: 0 0 3px 0;"><b>Bantuan:</b> ${d.kategori_bantuan}</p>
|
||||
<p style="margin: 0 0 5px 0;"><b>Jiwa:</b> ${d.jumlah_jiwa || '-'}</p>
|
||||
${photoLinksHtml}
|
||||
<div style="display:flex; gap:5px; flex-wrap:wrap; margin-top:8px;">
|
||||
<button style="padding:4px 8px; background:#6366f1; color:white; border:none; border-radius:3px; cursor:pointer; font-size:11px;" onclick="openLogBantuan(${d.id}, '${d.nama.replace(/'/g, "\\'")}');">📋 Log Bantuan</button>
|
||||
${actionButtons}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
marker.bindPopup(popupContent);
|
||||
|
||||
marker.on('dragend', function(e) {
|
||||
const newPos = marker.getLatLng();
|
||||
item.lat = newPos.lat;
|
||||
item.lng = newPos.lng;
|
||||
updateSemuaWarnaMiskin(); // Update warna segera
|
||||
// Simpan ke DB
|
||||
fetch('api/penduduk_miskin/update.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(item)
|
||||
});
|
||||
});
|
||||
|
||||
miskinMarkerList.push(marker);
|
||||
pendudukMiskinLayer.addLayer(marker);
|
||||
}
|
||||
|
||||
window.formAddMiskin = function(lat, lng) {
|
||||
map.closePopup();
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nama Kepala Keluarga</label>
|
||||
<input type="text" id="miskinNama" placeholder="Nama Kepala Keluarga">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Kategori Bantuan</label>
|
||||
<select id="miskinKategori">
|
||||
<option value="Makan">Bantuan Makan</option>
|
||||
<option value="Pemberdayaan">Pemberdayaan</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Jumlah Jiwa</label>
|
||||
<input type="number" id="miskinJiwa" value="1" min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Foto Rumah</label>
|
||||
<input type="file" id="miskinFotoRumah" accept="image/*">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Foto Kartu Keluarga (KK)</label>
|
||||
<input type="file" id="miskinFotoKK" accept="image/*">
|
||||
</div>
|
||||
`;
|
||||
openModal("Tambah Penduduk Miskin", bodyHTML, function() {
|
||||
window.saveNewMiskin(lat, lng);
|
||||
});
|
||||
};
|
||||
|
||||
window.saveNewMiskin = function(lat, lng) {
|
||||
const nama = document.getElementById('miskinNama').value;
|
||||
const kategori_bantuan = document.getElementById('miskinKategori').value;
|
||||
const jumlah_jiwa = document.getElementById('miskinJiwa').value;
|
||||
const fotoRumahInput = document.getElementById('miskinFotoRumah');
|
||||
const fotoKKInput = document.getElementById('miskinFotoKK');
|
||||
|
||||
if (!nama) { alert('Nama kepala keluarga harus diisi!'); return; }
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('nama', nama);
|
||||
formData.append('kategori_bantuan', kategori_bantuan);
|
||||
formData.append('jumlah_jiwa', jumlah_jiwa);
|
||||
formData.append('lat', lat);
|
||||
formData.append('lng', lng);
|
||||
if (fotoRumahInput.files[0]) formData.append('foto_rumah', fotoRumahInput.files[0]);
|
||||
if (fotoKKInput.files[0]) formData.append('foto_kk', fotoKKInput.files[0]);
|
||||
|
||||
fetch('api/penduduk_miskin/create.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
closeModal();
|
||||
loadPendudukMiskin();
|
||||
} else { alert(data.message); }
|
||||
});
|
||||
};
|
||||
|
||||
window.openEditMiskinModal = function(id) {
|
||||
let d = null;
|
||||
pendudukMiskinLayer.eachLayer(function(layer) {
|
||||
if (layer.miskinData && layer.miskinData.id == id) d = layer.miskinData;
|
||||
});
|
||||
if (!d) return;
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Kepala Keluarga</label>
|
||||
<input type="text" id="editMiskinNama" value="${d.nama}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Kategori Bantuan</label>
|
||||
<select id="editMiskinKategori">
|
||||
<option value="Makan" ${d.kategori_bantuan === 'Makan' ? 'selected' : ''}>Bantuan Makan</option>
|
||||
<option value="Pemberdayaan" ${d.kategori_bantuan === 'Pemberdayaan' ? 'selected' : ''}>Pemberdayaan</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Jumlah Jiwa</label>
|
||||
<input type="number" id="editMiskinJiwa" value="${d.jumlah_jiwa || 1}" min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Foto Rumah</label>
|
||||
<input type="file" id="editMiskinFotoRumah" accept="image/*">
|
||||
${d.foto_rumah ? `
|
||||
<div style="margin-top:5px;">
|
||||
<a href="/poverty/uploads/${d.foto_rumah}" target="_blank">
|
||||
<img src="/poverty/uploads/${d.foto_rumah}" alt="Foto Rumah" style="max-width:120px; max-height:80px; border-radius:4px; border:1px solid #ddd; object-fit:cover;"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
|
||||
<span style="display:none; font-size:11px; color:#ef4444;">❌ File gambar rusak atau tidak ditemukan</span>
|
||||
</a>
|
||||
<div style="font-size:11px;color:#888;margin-top:2px;">Upload baru untuk mengganti</div>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Foto Kartu Keluarga (KK)</label>
|
||||
<input type="file" id="editMiskinFotoKK" accept="image/*">
|
||||
${d.foto_kk ? `
|
||||
<div style="margin-top:5px;">
|
||||
<a href="/poverty/uploads/${d.foto_kk}" target="_blank">
|
||||
<img src="/poverty/uploads/${d.foto_kk}" alt="Foto KK" style="max-width:120px; max-height:80px; border-radius:4px; border:1px solid #ddd; object-fit:cover;"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
|
||||
<span style="display:none; font-size:11px; color:#ef4444;">❌ File gambar rusak atau tidak ditemukan</span>
|
||||
</a>
|
||||
<div style="font-size:11px;color:#888;margin-top:2px;">Upload baru untuk mengganti</div>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
map.closePopup();
|
||||
openModal("Edit Penduduk Miskin", bodyHTML, function() {
|
||||
window.saveEditMiskin(d.id, d.lat, d.lng, 'editMiskinNama', 'editMiskinKategori', 'editMiskinJiwa');
|
||||
});
|
||||
};
|
||||
|
||||
window.saveEditMiskin = function(id, lat, lng, namaId, kategoriId, jiwaId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const kategori_bantuan = document.getElementById(kategoriId).value;
|
||||
const jumlah_jiwa = jiwaId ? document.getElementById(jiwaId).value : 1;
|
||||
const fotoRumahInput = document.getElementById('editMiskinFotoRumah');
|
||||
const fotoKKInput = document.getElementById('editMiskinFotoKK');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('id', id);
|
||||
formData.append('nama', nama);
|
||||
formData.append('kategori_bantuan', kategori_bantuan);
|
||||
formData.append('jumlah_jiwa', jumlah_jiwa);
|
||||
formData.append('lat', lat);
|
||||
formData.append('lng', lng);
|
||||
if (fotoRumahInput.files[0]) formData.append('foto_rumah', fotoRumahInput.files[0]);
|
||||
if (fotoKKInput.files[0]) formData.append('foto_kk', fotoKKInput.files[0]);
|
||||
|
||||
fetch('api/penduduk_miskin/update.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(res => res.json()).then(data => {
|
||||
if(data.status === 'success') { closeModal(); loadPendudukMiskin(); }
|
||||
});
|
||||
};
|
||||
|
||||
window.deleteMiskin = function(id) {
|
||||
openConfirmModal("Yakin hapus penduduk miskin ini?", function() {
|
||||
fetch('api/penduduk_miskin/delete.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
}).then(res => res.json()).then(data => {
|
||||
if(data.status === 'success') { map.closePopup(); loadPendudukMiskin(); }
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Logika dinamis warna: Merah jika di dalam salah satu radius Rumah Ibadah, Hijau jika di luar
|
||||
function updateSemuaWarnaMiskin() {
|
||||
miskinMarkerList.forEach(marker => {
|
||||
let inRadius = false;
|
||||
const pLatlng = L.latLng(marker.miskinData.lat, marker.miskinData.lng);
|
||||
|
||||
for (let i = 0; i < ibadahDataList.length; i++) {
|
||||
const ibadah = ibadahDataList[i];
|
||||
const iLatlng = L.latLng(ibadah.lat, ibadah.lng);
|
||||
const dist = pLatlng.distanceTo(iLatlng); // meter
|
||||
|
||||
if (dist <= ibadah.radius) {
|
||||
inRadius = true;
|
||||
break; // Jika sudah masuk satu radius, langsung merah
|
||||
}
|
||||
}
|
||||
|
||||
if (inRadius) {
|
||||
marker.setIcon(makeMiskinIcon(true));
|
||||
} else {
|
||||
marker.setIcon(makeMiskinIcon(false));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// ===== Import Feature =====
|
||||
// ==========================
|
||||
let parsedImportData = [];
|
||||
|
||||
window.openImportMiskinModal = function() {
|
||||
parsedImportData = []; // Reset data
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Pilih File (CSV, JSON, atau GeoJSON)</label>
|
||||
<input type="file" id="importMiskinFile" accept=".csv,.json,.geojson" style="display:block; margin-bottom:10px; width:100%;">
|
||||
<div style="font-size:12px; color:#666; margin-bottom:15px; line-height:1.4;">
|
||||
<strong>Format CSV yang didukung:</strong><br>
|
||||
Header minimal berisi: <code>nama</code>, <code>kategori_bantuan</code> (atau <code>kategori</code>), <code>jumlah_jiwa</code>, <code>lat</code>, <code>lng</code>.<br>
|
||||
Pemisah: Koma (<code>,</code>), Titik Koma (<code>;</code>), atau Tab.
|
||||
</div>
|
||||
</div>
|
||||
<div id="importPreviewContainer" style="display:none; margin-top: 15px;">
|
||||
<h4 style="margin:0 0 8px 0; font-size: 13px; color: #333; display: flex; justify-content: space-between;">
|
||||
<span>Pratinjau Data (<span id="importPreviewCount">0</span> item)</span>
|
||||
<span id="importPreviewStatus" style="font-weight: normal; font-size: 11px;"></span>
|
||||
</h4>
|
||||
<div style="max-height:180px; overflow-y:auto; border:1px solid #e2e8f0; border-radius:6px; background:#f8fafc;">
|
||||
<table style="width:100%; border-collapse:collapse; font-size:11px; text-align:left;">
|
||||
<thead>
|
||||
<tr style="background:#f1f5f9; border-bottom:1px solid #cbd5e1; color:#475569; position: sticky; top: 0;">
|
||||
<th style="padding:6px 8px;">Kepala Keluarga</th>
|
||||
<th style="padding:6px 8px;">Kategori</th>
|
||||
<th style="padding:6px 8px;">Jiwa</th>
|
||||
<th style="padding:6px 8px;">Koordinat</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="importPreviewTableBody" style="color:#334155;"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="importErrorMsg" style="display:none; margin-top:10px; color:#ef4444; font-size:12px; font-weight: 500;"></div>
|
||||
`;
|
||||
|
||||
map.closePopup();
|
||||
|
||||
openModal("Impor Penduduk Miskin", bodyHTML, function() {
|
||||
if (parsedImportData.length === 0) {
|
||||
showToast('Silakan pilih file terlebih dahulu atau pastikan data valid.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Kirim data ke API bulk_create.php
|
||||
fetch('api/penduduk_miskin/bulk_create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(parsedImportData)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
closeModal();
|
||||
loadPendudukMiskin();
|
||||
showToast(data.message, 'success');
|
||||
} else {
|
||||
showToast(data.message || 'Gagal mengimpor data.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Terjadi kesalahan saat menghubungi server.', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Register file input listener
|
||||
document.getElementById('importMiskinFile').addEventListener('change', handleImportFileChange);
|
||||
};
|
||||
|
||||
function parseCSV(text) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
if (lines.length < 2) return [];
|
||||
|
||||
const firstLine = lines[0];
|
||||
let delimiter = ',';
|
||||
if (firstLine.includes(';')) delimiter = ';';
|
||||
else if (firstLine.includes('\t')) delimiter = '\t';
|
||||
|
||||
const headers = firstLine.split(delimiter).map(h => h.trim().replace(/^["']|["']$/g, '').toLowerCase());
|
||||
|
||||
const results = [];
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
|
||||
let fields = [];
|
||||
let currentField = '';
|
||||
let insideQuote = false;
|
||||
|
||||
for (let j = 0; j < line.length; j++) {
|
||||
const char = line[j];
|
||||
if (char === '"' || char === "'") {
|
||||
insideQuote = !insideQuote;
|
||||
} else if (char === delimiter && !insideQuote) {
|
||||
fields.push(currentField.trim());
|
||||
currentField = '';
|
||||
} else {
|
||||
currentField += char;
|
||||
}
|
||||
}
|
||||
fields.push(currentField.trim());
|
||||
|
||||
fields = fields.map(f => f.replace(/^["']|["']$/g, ''));
|
||||
|
||||
const row = {};
|
||||
headers.forEach((header, index) => {
|
||||
row[header] = fields[index] || '';
|
||||
});
|
||||
results.push(row);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function handleImportFileChange(e) {
|
||||
const file = e.target.files[0];
|
||||
const previewContainer = document.getElementById('importPreviewContainer');
|
||||
const previewCount = document.getElementById('importPreviewCount');
|
||||
const previewTableBody = document.getElementById('importPreviewTableBody');
|
||||
const errorMsg = document.getElementById('importErrorMsg');
|
||||
|
||||
if (!file) {
|
||||
previewContainer.style.display = 'none';
|
||||
parsedImportData = [];
|
||||
return;
|
||||
}
|
||||
|
||||
errorMsg.style.display = 'none';
|
||||
previewContainer.style.display = 'none';
|
||||
previewTableBody.innerHTML = '';
|
||||
parsedImportData = [];
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
try {
|
||||
const text = event.target.result;
|
||||
let rawData = [];
|
||||
|
||||
if (file.name.endsWith('.csv')) {
|
||||
rawData = parseCSV(text);
|
||||
} else if (file.name.endsWith('.json') || file.name.endsWith('.geojson')) {
|
||||
const json = JSON.parse(text);
|
||||
if (json.type === 'FeatureCollection' && Array.isArray(json.features)) {
|
||||
rawData = json.features.map(f => {
|
||||
const props = f.properties || {};
|
||||
const coords = f.geometry && f.geometry.coordinates;
|
||||
return {
|
||||
nama: props.nama || props.name || props.penduduk || '',
|
||||
kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan',
|
||||
jumlah_jiwa: props.jumlah_jiwa || props.jumlah || props.jiwa || 1,
|
||||
lat: coords ? coords[1] : (props.lat || props.latitude),
|
||||
lng: coords ? coords[0] : (props.lng || props.longitude)
|
||||
};
|
||||
});
|
||||
} else if (Array.isArray(json)) {
|
||||
rawData = json;
|
||||
} else {
|
||||
throw new Error("Format JSON tidak didukung.");
|
||||
}
|
||||
}
|
||||
|
||||
if (rawData.length === 0) {
|
||||
errorMsg.textContent = "File kosong atau format tidak didukung.";
|
||||
errorMsg.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
const validRows = [];
|
||||
let invalidCount = 0;
|
||||
|
||||
rawData.forEach(row => {
|
||||
const nama = row.nama || row.name || row.penduduk || row['nama lengkap'] || row.fullname || '';
|
||||
const kategori_bantuan = row.kategori_bantuan || row.kategori || row.bantuan || row['kategori bantuan'] || 'Makan';
|
||||
const jumlah_jiwa = parseInt(row.jumlah_jiwa || row.jumlah || row.jiwa || row['jumlah jiwa'] || 1, 10) || 1;
|
||||
const lat = parseFloat(row.lat || row.latitude || row.y);
|
||||
const lng = parseFloat(row.lng || row.longitude || row.long || row.x);
|
||||
|
||||
const isValid = !isNaN(lat) && !isNaN(lng) && lat !== 0 && lng !== 0 && nama.trim() !== '';
|
||||
|
||||
if (isValid) {
|
||||
validRows.push({ nama, kategori_bantuan, jumlah_jiwa, lat, lng });
|
||||
} else {
|
||||
invalidCount++;
|
||||
}
|
||||
});
|
||||
|
||||
parsedImportData = validRows;
|
||||
|
||||
if (validRows.length === 0) {
|
||||
errorMsg.textContent = "Tidak ditemukan data valid (pastikan kolom nama_kk/nama, lat, dan lng terisi).";
|
||||
errorMsg.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
validRows.forEach(row => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.style.borderBottom = '1px solid #e2e8f0';
|
||||
tr.innerHTML = `
|
||||
<td style="padding:6px 8px; font-weight: 500;">${row.nama}</td>
|
||||
<td style="padding:6px 8px;">${row.kategori_bantuan}</td>
|
||||
<td style="padding:6px 8px;">${row.jumlah_jiwa}</td>
|
||||
<td style="padding:6px 8px; color: #64748b; font-family: monospace;">${row.lat.toFixed(5)}, ${row.lng.toFixed(5)}</td>
|
||||
`;
|
||||
previewTableBody.appendChild(tr);
|
||||
});
|
||||
|
||||
previewCount.textContent = validRows.length;
|
||||
previewContainer.style.display = 'block';
|
||||
|
||||
const statusEl = document.getElementById('importPreviewStatus');
|
||||
if (statusEl) {
|
||||
if (invalidCount > 0) {
|
||||
statusEl.innerHTML = `<span style="color:#e11d48;"><i class="fas fa-exclamation-triangle"></i> ${invalidCount} baris tidak valid diabaikan</span>`;
|
||||
} else {
|
||||
statusEl.innerHTML = `<span style="color:#16a34a;"><i class="fas fa-check-circle"></i> Semua data valid</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
errorMsg.textContent = "Gagal memproses file. Pastikan format file sesuai.";
|
||||
errorMsg.style.display = 'block';
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
// Initial Load
|
||||
loadRumahIbadah();
|
||||
loadPendudukMiskin();
|
||||
@@ -0,0 +1,242 @@
|
||||
// Inisialisasi Peta
|
||||
// Koordinat awal: [-0.0263, 109.3425] (Pontianak)
|
||||
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
|
||||
|
||||
// ===== Toast Notification =====
|
||||
window.showToast = function(message, type = 'success', duration = 3500) {
|
||||
const icons = { success: '✅', error: '❌', info: 'ℹ️', warning: '⚠️' };
|
||||
const container = document.getElementById('toastContainer');
|
||||
if (!container) return;
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.innerHTML = `<span class="toast-icon">${icons[type] || 'ℹ️'}</span><span>${message}</span>`;
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('hiding');
|
||||
toast.addEventListener('animationend', () => toast.remove());
|
||||
}, duration);
|
||||
};
|
||||
|
||||
// Custom Zoom Control Logic
|
||||
document.getElementById('zoomInBtn').addEventListener('click', function() { map.zoomIn(); });
|
||||
document.getElementById('zoomOutBtn').addEventListener('click', function() { map.zoomOut(); });
|
||||
|
||||
// Base Map dari OpenStreetMap
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Inisialisasi FeatureGroups untuk masing-masing layer
|
||||
const rumahIbadahLayer = L.featureGroup().addTo(map);
|
||||
const pendudukMiskinLayer = L.featureGroup().addTo(map);
|
||||
const geoJsonLayer = L.featureGroup().addTo(map);
|
||||
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchClear = document.getElementById('searchClear');
|
||||
|
||||
searchInput.addEventListener('focus', function() {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
const val = this.value.toLowerCase();
|
||||
if (val.length > 0) {
|
||||
searchClear.style.visibility = 'visible';
|
||||
window.showSearchResults(val);
|
||||
} else {
|
||||
searchClear.style.visibility = 'hidden';
|
||||
document.getElementById('searchResults').style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
searchClear.addEventListener('click', function() {
|
||||
searchInput.value = '';
|
||||
searchClear.style.visibility = 'hidden';
|
||||
document.getElementById('searchResults').style.display = 'none';
|
||||
searchInput.focus();
|
||||
});
|
||||
|
||||
window.showSearchResults = function(query) {
|
||||
const resultsContainer = document.getElementById('searchResults');
|
||||
resultsContainer.innerHTML = '';
|
||||
let results = [];
|
||||
|
||||
|
||||
|
||||
if (typeof rumahIbadahLayer !== 'undefined') {
|
||||
rumahIbadahLayer.eachLayer(layer => {
|
||||
if (layer.ibadahData && layer.ibadahData.nama && layer.ibadahData.nama.toLowerCase().includes(query)) {
|
||||
if(layer instanceof L.Marker) {
|
||||
results.push({ type: 'Rumah Ibadah', nama: layer.ibadahData.nama, layer: layer });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
resultsContainer.innerHTML = '<div style="padding: 10px 15px; color: #666; font-size: 14px;">Tidak ada hasil</div>';
|
||||
} else {
|
||||
results.forEach(res => {
|
||||
const item = document.createElement('div');
|
||||
item.style.padding = '10px 15px';
|
||||
item.style.cursor = 'pointer';
|
||||
item.style.borderBottom = '1px solid #eee';
|
||||
item.style.fontSize = '14px';
|
||||
item.innerHTML = `<strong>${res.type}:</strong> ${res.nama}`;
|
||||
item.addEventListener('mouseenter', () => item.style.backgroundColor = '#f8f9fa');
|
||||
item.addEventListener('mouseleave', () => item.style.backgroundColor = 'white');
|
||||
item.addEventListener('click', () => {
|
||||
if (res.layer instanceof L.Marker) {
|
||||
map.setView(res.layer.getLatLng(), 17);
|
||||
} else if (res.layer.getBounds) {
|
||||
map.fitBounds(res.layer.getBounds());
|
||||
}
|
||||
res.layer.openPopup();
|
||||
resultsContainer.style.display = 'none';
|
||||
});
|
||||
resultsContainer.appendChild(item);
|
||||
});
|
||||
}
|
||||
resultsContainer.style.display = 'block';
|
||||
};
|
||||
|
||||
// UI Logic: Custom Layer Control Toggle
|
||||
const layerBtn = document.getElementById('layerBtn');
|
||||
const layerPanel = document.getElementById('layerPanel');
|
||||
|
||||
layerBtn.addEventListener('click', function() {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
layerPanel.style.display = layerPanel.style.display === 'block' ? 'none' : 'block';
|
||||
});
|
||||
|
||||
// Sembunyikan layer panel saat klik di peta
|
||||
map.on('click', function() {
|
||||
layerPanel.style.display = 'none';
|
||||
});
|
||||
|
||||
|
||||
// Logic untuk Toggle Layer Visibility
|
||||
document.getElementById('layerRumahIbadah').addEventListener('change', function(e) {
|
||||
e.target.checked ? map.addLayer(rumahIbadahLayer) : map.removeLayer(rumahIbadahLayer);
|
||||
});
|
||||
document.getElementById('layerMiskin').addEventListener('change', function(e) {
|
||||
e.target.checked ? map.addLayer(pendudukMiskinLayer) : map.removeLayer(pendudukMiskinLayer);
|
||||
});
|
||||
|
||||
// --- Sub-layer Toggle (expand/collapse) ---
|
||||
window.toggleSubLayer = function(subId, iconEl) {
|
||||
const sub = document.getElementById(subId);
|
||||
if (!sub) return;
|
||||
const isHidden = sub.style.display === 'none';
|
||||
sub.style.display = isHidden ? '' : 'none';
|
||||
iconEl.classList.toggle('collapsed', !isHidden);
|
||||
};
|
||||
|
||||
// --- Sub-layer Filter ---
|
||||
window.applySubFilter = function(type) {
|
||||
|
||||
if (type === 'miskin') {
|
||||
const checked = [...document.querySelectorAll('.sub-miskin:checked')].map(el => el.value);
|
||||
pendudukMiskinLayer.eachLayer(layer => {
|
||||
if (!layer.miskinData) return;
|
||||
if (checked.includes(layer.miskinData.kategori_bantuan)) {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = '');
|
||||
} else {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// --- Modal Logic ---
|
||||
const unifiedModal = document.getElementById('unifiedModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const modalBody = document.getElementById('modalBody');
|
||||
|
||||
window.openModal = function(title, bodyHTML, saveCallback) {
|
||||
modalTitle.textContent = title;
|
||||
modalBody.innerHTML = bodyHTML;
|
||||
unifiedModal.classList.add('show');
|
||||
|
||||
// Ambil elemen tombol save terbaru dari DOM
|
||||
const currentSaveBtn = document.getElementById('modalSaveBtn');
|
||||
|
||||
// Clone untuk menghapus semua event listener lama
|
||||
const newSaveBtn = currentSaveBtn.cloneNode(true);
|
||||
currentSaveBtn.parentNode.replaceChild(newSaveBtn, currentSaveBtn);
|
||||
|
||||
newSaveBtn.addEventListener('click', saveCallback);
|
||||
};
|
||||
|
||||
window.closeModal = function() {
|
||||
unifiedModal.classList.remove('show');
|
||||
};
|
||||
|
||||
const confirmModal = document.getElementById('confirmModal');
|
||||
const confirmMessage = document.getElementById('confirmMessage');
|
||||
|
||||
window.openConfirmModal = function(msg, confirmCallback) {
|
||||
confirmMessage.textContent = msg;
|
||||
confirmModal.classList.add('show');
|
||||
|
||||
const currentYesBtn = document.getElementById('confirmYesBtn');
|
||||
const newYesBtn = currentYesBtn.cloneNode(true);
|
||||
currentYesBtn.parentNode.replaceChild(newYesBtn, currentYesBtn);
|
||||
|
||||
newYesBtn.addEventListener('click', function() {
|
||||
confirmModal.classList.remove('show');
|
||||
confirmCallback();
|
||||
});
|
||||
};
|
||||
|
||||
window.closeConfirmModal = function() {
|
||||
confirmModal.classList.remove('show');
|
||||
};
|
||||
|
||||
// --- Action Menu Logic (replaced by left sidebar) ---
|
||||
|
||||
window.currentAddMode = null; // 'rumah_ibadah' atau 'miskin_click'
|
||||
|
||||
window.activateAddMode = function(mode) {
|
||||
// Toggle: jika mode yang sama diklik lagi, batalkan
|
||||
if (window.currentAddMode === mode) {
|
||||
window.deactivateAddMode();
|
||||
return;
|
||||
}
|
||||
|
||||
window.currentAddMode = mode;
|
||||
|
||||
// Tooltip
|
||||
const tooltips = {
|
||||
'rumah_ibadah': 'Klik untuk Tambah Rumah Ibadah'
|
||||
};
|
||||
if (window.cursorTooltip && tooltips[mode]) window.cursorTooltip.textContent = tooltips[mode];
|
||||
|
||||
// Ubah kursor map menjadi crosshair
|
||||
document.getElementById('map').style.cursor = 'crosshair';
|
||||
};
|
||||
|
||||
window.deactivateAddMode = function() {
|
||||
window.currentAddMode = null;
|
||||
document.getElementById('map').style.cursor = '';
|
||||
if (window.cursorTooltip) window.cursorTooltip.style.display = 'none';
|
||||
};
|
||||
|
||||
// --- Custom Tooltip Cursor ---
|
||||
window.cursorTooltip = document.createElement('div');
|
||||
window.cursorTooltip.className = 'custom-cursor-tooltip';
|
||||
document.body.appendChild(window.cursorTooltip);
|
||||
|
||||
// Gunakan document-level mousemove agar selalu terpicu
|
||||
// bahkan saat Leaflet Draw overlay aktif menangkap event map
|
||||
document.addEventListener('mousemove', function(e) {
|
||||
if (window.currentAddMode) {
|
||||
window.cursorTooltip.style.display = 'block';
|
||||
window.cursorTooltip.style.left = e.pageX + 15 + 'px';
|
||||
window.cursorTooltip.style.top = e.pageY + 15 + 'px';
|
||||
} else {
|
||||
window.cursorTooltip.style.display = 'none';
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,465 @@
|
||||
// ===== Panel Management =====
|
||||
// Mengelola Left Sidebar Toggle + Right Panel + Layer Control
|
||||
|
||||
(function () {
|
||||
// ---- Konstanta Jenis Ibadah ----
|
||||
const IBADAH_EMOJI = {
|
||||
'Masjid': '🕌',
|
||||
'Gereja': '⛪',
|
||||
'Pura': '🛕',
|
||||
'Vihara': '🪷',
|
||||
'Kelenteng': '🏮',
|
||||
};
|
||||
window.IBADAH_EMOJI = IBADAH_EMOJI;
|
||||
|
||||
// ---- State ----
|
||||
let activePanel = null;
|
||||
|
||||
// ---- Elements ----
|
||||
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
|
||||
const sidebarToggleIcon = document.getElementById('sidebarToggleIcon');
|
||||
const leftSidebar = document.getElementById('leftSidebar');
|
||||
const rightPanel = document.getElementById('rightPanel');
|
||||
const rightPanelTitle = document.getElementById('rightPanelTitle');
|
||||
const rightPanelActions = document.getElementById('rightPanelActions');
|
||||
const rightPanelBody = document.getElementById('rightPanelBody');
|
||||
const rightPanelClose = document.getElementById('rightPanelClose');
|
||||
|
||||
// ---- Sidebar Toggle ----
|
||||
sidebarToggleBtn.addEventListener('click', function () {
|
||||
const isOpen = leftSidebar.style.display !== 'none';
|
||||
if (isOpen) {
|
||||
leftSidebar.style.display = 'none';
|
||||
sidebarToggleBtn.classList.remove('open');
|
||||
sidebarToggleIcon.className = 'fas fa-chevron-right';
|
||||
} else {
|
||||
leftSidebar.style.display = 'flex';
|
||||
sidebarToggleBtn.classList.add('open');
|
||||
sidebarToggleIcon.className = 'fas fa-chevron-left';
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Sidebar Buttons ----
|
||||
document.querySelectorAll('.sidebar-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
const panel = this.dataset.panel;
|
||||
if (activePanel === panel) {
|
||||
closePanel();
|
||||
} else {
|
||||
openPanel(panel);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
rightPanelClose.addEventListener('click', closePanel);
|
||||
|
||||
// ---- Open / Close Panel ----
|
||||
function openPanel(panel) {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
activePanel = panel;
|
||||
rightPanel.classList.add('open');
|
||||
document.body.classList.add('right-panel-open');
|
||||
document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.remove('active'));
|
||||
const activeBtn = document.querySelector(`.sidebar-btn[data-panel="${panel}"]`);
|
||||
if (activeBtn) activeBtn.classList.add('active');
|
||||
renderPanel(panel);
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
activePanel = null;
|
||||
rightPanel.classList.remove('open');
|
||||
document.body.classList.remove('right-panel-open');
|
||||
document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.remove('active'));
|
||||
}
|
||||
|
||||
window.refreshActivePanel = function () {
|
||||
if (activePanel && activePanel !== 'layer') renderPanel(activePanel);
|
||||
};
|
||||
|
||||
// ---- Render Panel by Type ----
|
||||
function renderPanel(type) {
|
||||
rightPanelBody.innerHTML = '<div class="panel-empty">Memuat data...</div>';
|
||||
rightPanelActions.innerHTML = '';
|
||||
|
||||
switch (type) {
|
||||
case 'ibadah': renderIbadahPanel(); break;
|
||||
case 'miskin': renderMiskinPanel(); break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==========================
|
||||
// ===== Ibadah Panel =======
|
||||
// ==========================
|
||||
function renderIbadahPanel() {
|
||||
rightPanelTitle.textContent = '🕌 Rumah Ibadah';
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
if (isAdmin) {
|
||||
rightPanelActions.innerHTML = `
|
||||
<button class="btn-panel-add" id="btnPanelAddIbadah">
|
||||
<i class="fas fa-plus"></i> Tambah Ibadah
|
||||
</button>`;
|
||||
document.getElementById('btnPanelAddIbadah').addEventListener('click', () => {
|
||||
window.activateAddMode('rumah_ibadah');
|
||||
});
|
||||
} else {
|
||||
rightPanelActions.innerHTML = '';
|
||||
}
|
||||
|
||||
const items = [];
|
||||
rumahIbadahLayer.eachLayer(l => {
|
||||
if (l.ibadahData && l instanceof L.Marker) items.push(l.ibadahData);
|
||||
});
|
||||
|
||||
if (!items.length) {
|
||||
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data rumah ibadah</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
rightPanelBody.innerHTML = '';
|
||||
items.forEach(d => {
|
||||
const emoji = IBADAH_EMOJI[d.jenis] || '🕌';
|
||||
const card = document.createElement('div');
|
||||
card.className = 'data-card';
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div class="data-card-actions">
|
||||
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
|
||||
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
card.innerHTML = `
|
||||
<div class="data-card-main">
|
||||
<div class="data-card-icon card-icon-ibadah">${emoji}</div>
|
||||
<div class="data-card-info">
|
||||
<div class="data-card-name">${escHtml(d.nama)}</div>
|
||||
<div class="data-card-sub">${escHtml(d.jenis || 'Masjid')} • 📡 ${d.radius} m</div>
|
||||
</div>
|
||||
${actionButtons}
|
||||
</div>`;
|
||||
if (isAdmin) {
|
||||
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditIbadahModal(d.id); });
|
||||
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteIbadah(d.id); });
|
||||
}
|
||||
card.addEventListener('click', () => flyToIbadah(d.id));
|
||||
rightPanelBody.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function flyToIbadah(id) {
|
||||
rumahIbadahLayer.eachLayer(l => {
|
||||
if (l.ibadahData && l.ibadahData.id == id && l instanceof L.Marker) {
|
||||
map.setView(l.getLatLng(), 17); l.openPopup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// ===== Miskin Panel =======
|
||||
// ==========================
|
||||
function renderMiskinPanel() {
|
||||
rightPanelTitle.textContent = '🏠 Penduduk Miskin';
|
||||
const canManage = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
|
||||
if (canManage) {
|
||||
const isPengelola = window.currentUser.role === 'pengelola';
|
||||
const addLabel = isPengelola
|
||||
? '<i class="fas fa-plus"></i> Tambah <span style="font-size:10px;opacity:0.8;">(dalam radius)</span>'
|
||||
: '<i class="fas fa-plus"></i> Tambah';
|
||||
rightPanelActions.innerHTML = `
|
||||
<button class="btn-panel-add" id="btnPanelTambahMiskin">
|
||||
${addLabel}
|
||||
</button>
|
||||
<button class="btn-panel-import" id="btnPanelImportMiskin">
|
||||
<i class="fas fa-file-import"></i> Impor
|
||||
</button>`;
|
||||
|
||||
document.getElementById('btnPanelTambahMiskin').addEventListener('click', () => {
|
||||
window.currentAddMode = 'miskin_click';
|
||||
document.getElementById('map').style.cursor = 'crosshair';
|
||||
if (window.cursorTooltip) {
|
||||
const tipText = isPengelola
|
||||
? 'Klik di dalam radius ibadah Anda untuk tambah penduduk'
|
||||
: 'Klik di peta untuk tentukan lokasi penduduk miskin';
|
||||
window.cursorTooltip.textContent = tipText;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btnPanelImportMiskin').addEventListener('click', () => {
|
||||
if (typeof window.openImportMiskinModal === 'function') {
|
||||
window.openImportMiskinModal();
|
||||
} else {
|
||||
alert('Fitur impor belum siap.');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
rightPanelActions.innerHTML = '';
|
||||
}
|
||||
|
||||
const all = [];
|
||||
pendudukMiskinLayer.eachLayer(l => { if (l.miskinData) all.push(l.miskinData); });
|
||||
|
||||
if (!all.length) {
|
||||
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data penduduk miskin</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const inRadius = (d) => {
|
||||
if (typeof ibadahDataList === 'undefined') return false;
|
||||
for (const ib of ibadahDataList) {
|
||||
const dist = L.latLng(d.lat, d.lng).distanceTo(L.latLng(ib.lat, ib.lng));
|
||||
if (dist <= ib.radius) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
rightPanelBody.innerHTML = '';
|
||||
|
||||
const terurusItems = all.filter(d => inRadius(d));
|
||||
const tidakItems = all.filter(d => !inRadius(d));
|
||||
|
||||
if (terurusItems.length) {
|
||||
const h1 = document.createElement('div');
|
||||
h1.className = 'panel-section-title';
|
||||
h1.innerHTML = `✅ Terurus <span style="font-weight:400;text-transform:none;font-size:10px;">(${terurusItems.length})</span>`;
|
||||
rightPanelBody.appendChild(h1);
|
||||
terurusItems.forEach(d => rightPanelBody.appendChild(buildMiskinCard(d, true)));
|
||||
}
|
||||
|
||||
if (tidakItems.length) {
|
||||
const h2 = document.createElement('div');
|
||||
h2.className = 'panel-section-title';
|
||||
h2.innerHTML = `⚠️ Tidak Terurus <span style="font-weight:400;text-transform:none;font-size:10px;">(${tidakItems.length})</span>`;
|
||||
rightPanelBody.appendChild(h2);
|
||||
tidakItems.forEach(d => rightPanelBody.appendChild(buildMiskinCard(d, false)));
|
||||
}
|
||||
}
|
||||
|
||||
function buildMiskinCard(d, terurus) {
|
||||
// Pengelola hanya boleh edit/hapus penduduk yang dalam radius ibadahnya
|
||||
const isPengelolaOrAdmin = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
|
||||
const canManage = isPengelolaOrAdmin && (typeof window.isInMyIbadahRadius === 'function'
|
||||
? window.isInMyIbadahRadius(d.lat, d.lng)
|
||||
: true);
|
||||
const iconCls = terurus ? 'card-icon-miskin-in' : 'card-icon-miskin-out';
|
||||
const badge = d.kategori_bantuan === 'Makan'
|
||||
? '<span class="data-card-badge badge-orange">Bantuan Makan</span>'
|
||||
: '<span class="data-card-badge badge-blue">Pemberdayaan</span>';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'data-card';
|
||||
card.dataset.miskinId = d.id;
|
||||
|
||||
let actionButtons = '';
|
||||
if (canManage) {
|
||||
actionButtons = `
|
||||
<div class="data-card-actions">
|
||||
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
|
||||
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
let fotoHtml = '';
|
||||
if (d.foto_rumah || d.foto_kk) {
|
||||
fotoHtml += `<div class="expand-row-photos" style="display:flex; gap:10px; margin-top:8px; justify-content: space-around;">`;
|
||||
if (d.foto_rumah) {
|
||||
fotoHtml += `
|
||||
<div style="text-align:center;">
|
||||
<span style="font-size:10px; display:block; font-weight:500; color:#555;">Foto Rumah</span>
|
||||
<a href="uploads/${d.foto_rumah}" target="_blank">
|
||||
<img src="uploads/${d.foto_rumah}" style="width:80px; height:60px; object-fit:cover; border-radius:4px; border:1px solid #ddd; margin-top:3px;" />
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
if (d.foto_kk) {
|
||||
fotoHtml += `
|
||||
<div style="text-align:center;">
|
||||
<span style="font-size:10px; display:block; font-weight:500; color:#555;">Foto KK</span>
|
||||
<a href="uploads/${d.foto_kk}" target="_blank">
|
||||
<img src="uploads/${d.foto_kk}" style="width:80px; height:60px; object-fit:cover; border-radius:4px; border:1px solid #ddd; margin-top:3px;" />
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
fotoHtml += `</div>`;
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="data-card-main">
|
||||
<div class="data-card-icon ${iconCls}">🏠</div>
|
||||
<div class="data-card-info">
|
||||
<div class="data-card-name">${escHtml(d.nama)}</div>
|
||||
<div class="data-card-sub">👨👩👧 ${d.jumlah_jiwa || '-'} jiwa</div>
|
||||
${badge}
|
||||
</div>
|
||||
${actionButtons}
|
||||
</div>
|
||||
<div class="data-card-expand">
|
||||
<div class="expand-row"><span>Kepala Keluarga</span><span>${escHtml(d.nama)}</span></div>
|
||||
<div class="expand-row"><span>Kategori Bantuan</span><span>${escHtml(d.kategori_bantuan)}</span></div>
|
||||
<div class="expand-row"><span>Jumlah Jiwa</span><span>${d.jumlah_jiwa || '-'}</span></div>
|
||||
<div class="expand-row"><span>Status</span><span>${terurus ? 'Terurus ✅' : 'Tidak Terurus ⚠️'}</span></div>
|
||||
${fotoHtml}
|
||||
<button class="btn-log-bantuan" data-id="${d.id}" data-nama="${escHtml(d.nama)}">
|
||||
<i class="fas fa-history"></i> Lihat Log Bantuan
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
if (canManage) {
|
||||
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditMiskinModal(d.id); });
|
||||
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteMiskin(d.id); });
|
||||
}
|
||||
card.querySelector('.btn-log-bantuan').addEventListener('click', (e) => { e.stopPropagation(); openLogBantuan(d.id, d.nama); });
|
||||
|
||||
card.querySelector('.data-card-main').addEventListener('click', () => {
|
||||
card.classList.toggle('expanded');
|
||||
flyToMiskin(d.id);
|
||||
});
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function flyToMiskin(id) {
|
||||
pendudukMiskinLayer.eachLayer(l => {
|
||||
if (l.miskinData && l.miskinData.id == id) {
|
||||
map.setView(l.getLatLng(), 17); l.openPopup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Hook: klik peta saat mode miskin_click ----
|
||||
// Ditangkap oleh kemiskinan.js via contextmenu — mode ini pakai klik kiri
|
||||
map.on('click', function(e) {
|
||||
if (window.currentAddMode === 'miskin_click') {
|
||||
window.currentAddMode = null;
|
||||
document.getElementById('map').style.cursor = '';
|
||||
if (window.cursorTooltip) window.cursorTooltip.style.display = 'none';
|
||||
// Buka form tambah miskin
|
||||
if (typeof window.formAddMiskin === 'function') {
|
||||
window.formAddMiskin(e.latlng.lat, e.latlng.lng);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================
|
||||
// ===== Log Bantuan Modal ==
|
||||
// ==========================
|
||||
const logModal = document.getElementById('logBantuanModal');
|
||||
const logTitle = document.getElementById('logBantuanTitle');
|
||||
const logList = document.getElementById('logBantuanList');
|
||||
const logSaveBtn = document.getElementById('logSaveBtn');
|
||||
const logIbadahSel = document.getElementById('logIbadahId');
|
||||
let _logMiskinId = null;
|
||||
|
||||
window.openLogBantuan = function (miskinId, nama) {
|
||||
_logMiskinId = miskinId;
|
||||
logTitle.textContent = `Log Bantuan — ${nama}`;
|
||||
logModal.classList.add('show');
|
||||
|
||||
const isAuth = !!window.currentUser;
|
||||
const formContainer = document.querySelector('#logBantuanModal .modal-body > div:last-child');
|
||||
const modalFooter = document.querySelector('#logBantuanModal .modal-footer');
|
||||
|
||||
if (isAuth) {
|
||||
if (formContainer) formContainer.style.display = 'block';
|
||||
if (modalFooter) modalFooter.style.display = 'flex';
|
||||
} else {
|
||||
if (formContainer) formContainer.style.display = 'none';
|
||||
if (modalFooter) modalFooter.style.display = 'none';
|
||||
}
|
||||
|
||||
// Isi dropdown ibadah
|
||||
logIbadahSel.innerHTML = '<option value="">-- Pilih Rumah Ibadah --</option>';
|
||||
if (typeof ibadahDataList !== 'undefined') {
|
||||
ibadahDataList.forEach(ib => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = ib.id;
|
||||
opt.textContent = `${IBADAH_EMOJI[ib.jenis] || '🕌'} ${ib.nama}`;
|
||||
logIbadahSel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
if (window.currentUser && window.currentUser.role === 'pengelola') {
|
||||
logIbadahSel.value = window.currentUser.ibadah_id;
|
||||
logIbadahSel.disabled = true;
|
||||
} else {
|
||||
logIbadahSel.disabled = false;
|
||||
}
|
||||
|
||||
// Set tanggal default hari ini
|
||||
document.getElementById('logTanggal').valueAsDate = new Date();
|
||||
fetchLogBantuan(miskinId);
|
||||
};
|
||||
|
||||
window.closeLogBantuanModal = function () {
|
||||
logModal.classList.remove('show');
|
||||
_logMiskinId = null;
|
||||
};
|
||||
|
||||
function fetchLogBantuan(miskinId) {
|
||||
logList.innerHTML = '<div class="panel-empty" style="padding:15px;">Memuat...</div>';
|
||||
fetch(`api/log_bantuan/read.php?miskin_id=${miskinId}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.data || !data.data.length) {
|
||||
logList.innerHTML = '<div class="panel-empty" style="padding:15px;">Belum ada riwayat bantuan</div>';
|
||||
return;
|
||||
}
|
||||
logList.innerHTML = '';
|
||||
data.data.forEach(log => {
|
||||
const tgl = new Date(log.tanggal).toLocaleDateString('id-ID', { day:'2-digit', month:'short', year:'numeric' });
|
||||
const item = document.createElement('div');
|
||||
item.className = 'log-item';
|
||||
item.innerHTML = `
|
||||
<div class="log-dot"></div>
|
||||
<div class="log-info">
|
||||
<div class="log-type">${escHtml(log.tipe_bantuan)}</div>
|
||||
<div class="log-meta">${IBADAH_EMOJI[log.jenis_ibadah] || '🕌'} ${escHtml(log.nama_ibadah)} • ${tgl}</div>
|
||||
${log.keterangan ? `<div class="log-keterangan">${escHtml(log.keterangan)}</div>` : ''}
|
||||
</div>`;
|
||||
logList.appendChild(item);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
logList.innerHTML = '<div class="panel-empty" style="padding:15px; color:#ef4444;">Gagal memuat data</div>';
|
||||
});
|
||||
}
|
||||
|
||||
logSaveBtn.addEventListener('click', function () {
|
||||
const ibadah_id = logIbadahSel.value;
|
||||
const tipe_bantuan = document.getElementById('logTipeBantuan').value;
|
||||
const tanggal = document.getElementById('logTanggal').value;
|
||||
const keterangan = document.getElementById('logKeterangan').value;
|
||||
|
||||
if (!ibadah_id) { alert('Pilih rumah ibadah terlebih dahulu!'); return; }
|
||||
if (!tanggal) { alert('Tanggal harus diisi!'); return; }
|
||||
|
||||
fetch('api/log_bantuan/create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ miskin_id: _logMiskinId, ibadah_id, tipe_bantuan, tanggal, keterangan })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
document.getElementById('logKeterangan').value = '';
|
||||
fetchLogBantuan(_logMiskinId);
|
||||
} else {
|
||||
alert(data.message || 'Gagal menyimpan');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Utility ----
|
||||
function escHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ---- Expose ----
|
||||
window.openRightPanel = openPanel;
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,90 @@
|
||||
-- Database schema for WebGIS Pemetaan Kemiskinan Pontianak
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis;
|
||||
USE webgis;
|
||||
|
||||
-- 1. Tabel rumah_ibadah
|
||||
DROP TABLE IF EXISTS `log_bantuan`;
|
||||
DROP TABLE IF EXISTS `rumah_ibadah`;
|
||||
CREATE TABLE `rumah_ibadah` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`nama` varchar(255) NOT NULL,
|
||||
`jenis` varchar(100) NOT NULL,
|
||||
`alamat` text,
|
||||
`radius` double NOT NULL DEFAULT 0,
|
||||
`lat` double NOT NULL,
|
||||
`lng` double NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 2. Tabel penduduk_miskin
|
||||
DROP TABLE IF EXISTS `penduduk_miskin`;
|
||||
CREATE TABLE `penduduk_miskin` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`nama` varchar(255) NOT NULL,
|
||||
`kategori_bantuan` varchar(255) NOT NULL,
|
||||
`jumlah_jiwa` int(11) NOT NULL DEFAULT 1,
|
||||
`lat` double NOT NULL,
|
||||
`lng` double NOT NULL,
|
||||
`foto_rumah` varchar(255) DEFAULT NULL,
|
||||
`foto_kk` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 3. Tabel log_bantuan
|
||||
CREATE TABLE `log_bantuan` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`miskin_id` int(11) NOT NULL,
|
||||
`ibadah_id` int(11) NOT NULL,
|
||||
`tipe_bantuan` varchar(255) NOT NULL,
|
||||
`tanggal` date NOT NULL,
|
||||
`keterangan` text,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `fk_log_miskin` (`miskin_id`),
|
||||
KEY `fk_log_ibadah` (`ibadah_id`),
|
||||
CONSTRAINT `fk_log_ibadah` FOREIGN KEY (`ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_log_miskin` FOREIGN KEY (`miskin_id`) REFERENCES `penduduk_miskin` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
|
||||
-- Data Contoh (Sample Data) untuk Pontianak
|
||||
|
||||
-- Data Contoh Rumah Ibadah
|
||||
INSERT INTO `rumah_ibadah` (`nama`, `jenis`, `alamat`, `radius`, `lat`, `lng`) VALUES
|
||||
('Masjid Raya Mujahidin', 'Masjid', 'Jl. Jenderal Ahmad Yani, Akcaya, Kec. Pontianak Sel.', 500, -0.0435, 109.3440),
|
||||
('Gereja Katedral Santo Yosef', 'Gereja', 'Jl. Patria Tama, Darat Sekip, Kec. Pontianak Kota', 500, -0.0278, 109.3370),
|
||||
('Vihara Bodhisatva Karaniya Metta', 'Vihara', 'Jl. Kapten Marsan, Darat Sekip, Kec. Pontianak Kota', 500, -0.0232, 109.3432);
|
||||
|
||||
-- Data Contoh Penduduk Miskin
|
||||
INSERT INTO `penduduk_miskin` (`nama`, `kategori_bantuan`, `jumlah_jiwa`, `lat`, `lng`) VALUES
|
||||
('Budi Santoso', 'Makan', 4, -0.0420, 109.3415),
|
||||
('Siti Aminah', 'Pemberdayaan', 3, -0.0285, 109.3395),
|
||||
('Ahmad Subarjo', 'Makan', 5, -0.0245, 109.3448);
|
||||
|
||||
-- Data Contoh Log Bantuan
|
||||
INSERT INTO `log_bantuan` (`miskin_id`, `ibadah_id`, `tipe_bantuan`, `tanggal`, `keterangan`) VALUES
|
||||
(1, 1, 'Sembako', '2026-05-15', 'Pembagian paket sembako bulanan'),
|
||||
(2, 2, 'Pendidikan', '2026-05-20', 'Bantuan biaya sekolah anak'),
|
||||
(3, 1, 'Uang Tunai', '2026-06-01', 'Santunan tunai darurat');
|
||||
|
||||
|
||||
|
||||
-- 7. Tabel users
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
CREATE TABLE `users` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(100) NOT NULL UNIQUE,
|
||||
`password` varchar(255) NOT NULL,
|
||||
`role` enum('admin', 'pengelola') NOT NULL,
|
||||
`ibadah_id` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
CONSTRAINT `fk_users_ibadah` FOREIGN KEY (`ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Data Contoh Pengguna (Default Users)
|
||||
-- Password admin: password
|
||||
-- Password pengelola: password (terhubung ke Masjid Raya Mujahidin, ibadah_id = 1)
|
||||
INSERT INTO `users` (`username`, `password`, `role`, `ibadah_id`) VALUES
|
||||
('admin', '$2y$10$qqKxivn4646czFu/ffwa/uZxJrkj3T9xmsrRbyxJbHx6jllpOOfDO', 'admin', NULL),
|
||||
('pengelola_mujahidin', '$2y$10$qqKxivn4646czFu/ffwa/uZxJrkj3T9xmsrRbyxJbHx6jllpOOfDO', 'pengelola', 1);
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS Pemetaan Kemiskinan</title>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Google+Sans+Flex:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- FontAwesome (Icons) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
|
||||
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- UI Container over map -->
|
||||
<div class="ui-container">
|
||||
<!-- Search Bar -->
|
||||
<div class="search-bar">
|
||||
<div class="search-icon">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
<input type="text" class="search-input" id="searchInput" placeholder="Cari rumah ibadah...">
|
||||
<button class="search-clear" id="searchClear">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Search Results -->
|
||||
<div id="searchResults" style="display: none; background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-height: 200px; overflow-y: auto;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Zoom Control -->
|
||||
<div class="custom-zoom-control" style="position: absolute; top: 20px; left: 20px; z-index: 1000; display: flex; flex-direction: column; gap: 5px;">
|
||||
<button class="custom-layer-btn" id="zoomInBtn" style="position: relative; top: 0; left: 0; right: auto;" title="Zoom In"><i class="fas fa-plus"></i></button>
|
||||
<button class="custom-layer-btn" id="zoomOutBtn" style="position: relative; top: 0; left: 0; right: auto;" title="Zoom Out"><i class="fas fa-minus"></i></button>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Toggle Button -->
|
||||
<button class="sidebar-toggle-btn" id="sidebarToggleBtn" title="Menu">
|
||||
<i class="fas fa-chevron-right" id="sidebarToggleIcon"></i>
|
||||
</button>
|
||||
|
||||
<!-- Left Sidebar Menu -->
|
||||
<nav class="left-sidebar" id="leftSidebar" style="display:none;">
|
||||
|
||||
<button class="sidebar-btn" id="menuIbadah" data-panel="ibadah" title="Rumah Ibadah">
|
||||
<span class="sidebar-emoji">🕌</span>
|
||||
<span class="sidebar-label">Ibadah</span>
|
||||
</button>
|
||||
<button class="sidebar-btn" id="menuMiskin" data-panel="miskin" title="Penduduk Miskin">
|
||||
<span class="sidebar-emoji">🏠</span>
|
||||
<span class="sidebar-label">Miskin</span>
|
||||
</button>
|
||||
<button class="sidebar-btn" id="menuDashboard" style="display:none;" title="Dashboard">
|
||||
<span class="sidebar-emoji">📊</span>
|
||||
<span class="sidebar-label">Dashboard</span>
|
||||
</button>
|
||||
|
||||
<div class="sidebar-divider" id="sidebarAdminDivider" style="display:none;"></div>
|
||||
<button class="sidebar-btn" id="menuUsers" style="display:none;" title="Manajemen Pengguna">
|
||||
<span class="sidebar-emoji">👥</span>
|
||||
<span class="sidebar-label">User</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
<!-- Custom Layer Control Button -->
|
||||
<button class="custom-layer-btn" id="layerBtn" title="Atur Layer">
|
||||
<i class="fas fa-layer-group fa-lg"></i>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<!-- Custom Layer Control Panel -->
|
||||
<div class="custom-layer-panel" id="layerPanel">
|
||||
<h3>Daftar Layer</h3>
|
||||
|
||||
|
||||
|
||||
<div class="layer-section-title">Pemetaan Kemiskinan</div>
|
||||
|
||||
<!-- Rumah Ibadah -->
|
||||
<label class="layer-option">
|
||||
<input type="checkbox" id="layerRumahIbadah" checked> Rumah Ibadah
|
||||
</label>
|
||||
|
||||
<!-- Penduduk Miskin -->
|
||||
<div class="layer-group-item">
|
||||
<div class="layer-group-header">
|
||||
<label class="layer-option" style="margin:0; flex:1;">
|
||||
<input type="checkbox" id="layerMiskin" checked> Penduduk Miskin
|
||||
</label>
|
||||
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subMiskin', this)"><i class="fas fa-chevron-down"></i></span>
|
||||
</div>
|
||||
<div id="subMiskin" class="sub-layer-list" style="display:none;">
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-miskin" value="Makan" checked onchange="applySubFilter('miskin')"> Bantuan Makan
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-miskin" value="Pemberdayaan" checked onchange="applySubFilter('miskin')"> Pemberdayaan
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GeoJSON Eksternal -->
|
||||
<div class="layer-group" style="margin-top: 10px; border-top: 1px solid #eee; padding-top: 10px;">
|
||||
<div class="layer-group-header" onclick="window.toggleGeoJsonMenu()" style="cursor:pointer; display:flex; justify-content:space-between; align-items:center; font-size:14px; margin-bottom:8px; font-weight: 500;">
|
||||
<span><i class="fas fa-folder"></i> GeoJSON Eksternal</span>
|
||||
<i id="geoJsonToggleIcon" class="fas fa-plus"></i>
|
||||
</div>
|
||||
<div id="geoJsonFileList" style="display:none; padding-left:15px; margin-bottom:10px;">
|
||||
<div id="geoJsonLayersContainer"></div>
|
||||
<div style="margin-top: 10px; padding-top: 10px; border-top: 1px dashed #ddd;">
|
||||
<label style="font-size: 12px; display:block; margin-bottom: 5px;">Import GeoJSON Baru:</label>
|
||||
<input type="file" id="fileGeoJson" accept=".geojson,.json" style="font-size: 12px; max-width: 100%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /custom-layer-panel -->
|
||||
|
||||
<!-- Right Data Panel -->
|
||||
<div class="right-panel" id="rightPanel">
|
||||
<div class="right-panel-header">
|
||||
<h3 id="rightPanelTitle">Data</h3>
|
||||
<button class="right-panel-close" id="rightPanelClose"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="right-panel-actions" id="rightPanelActions"></div>
|
||||
<div class="right-panel-body" id="rightPanelBody">
|
||||
<div class="panel-empty">Pilih menu di kiri untuk menampilkan data</div>
|
||||
</div>
|
||||
</div><!-- /right-panel -->
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Map Container -->
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Unified Modal -->
|
||||
<div id="unifiedModal" class="unified-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalTitle">Judul Form</h3>
|
||||
<span class="modal-close" onclick="closeModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body" id="modalBody">
|
||||
<!-- Konten dinamis -->
|
||||
</div>
|
||||
<div class="modal-footer" id="modalFooter">
|
||||
<button id="modalSaveBtn" class="btn-save">Simpan</button>
|
||||
<button class="btn-cancel" onclick="closeModal()">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Modal -->
|
||||
<div id="confirmModal" class="unified-modal">
|
||||
<div class="modal-content" style="width: 300px;">
|
||||
<div class="modal-header">
|
||||
<h3>Konfirmasi</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="confirmMessage"></p>
|
||||
</div>
|
||||
<div class="modal-footer" style="display: flex; gap: 10px; justify-content: flex-end;">
|
||||
<button id="confirmYesBtn" class="btn-save" style="background-color: #dc3545;">Ya, Hapus</button>
|
||||
<button class="btn-cancel" onclick="closeConfirmModal()">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Bantuan Modal -->
|
||||
<div id="logBantuanModal" class="unified-modal">
|
||||
<div class="modal-content" style="width:500px; max-height:85vh; overflow:hidden; display:flex; flex-direction:column;">
|
||||
<div class="modal-header">
|
||||
<h3 id="logBantuanTitle">Log Bantuan</h3>
|
||||
<span class="modal-close" onclick="closeLogBantuanModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body" style="flex:1; overflow-y:auto; gap:0; padding:0;">
|
||||
<!-- Riwayat -->
|
||||
<div style="padding:15px; border-bottom:1px solid #eee;">
|
||||
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Riwayat Bantuan</h4>
|
||||
<div id="logBantuanList" style="max-height:220px; overflow-y:auto; display:flex; flex-direction:column; gap:8px;">
|
||||
<div class="panel-empty" style="padding:20px;">Belum ada riwayat</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Tambah -->
|
||||
<div style="padding:15px;">
|
||||
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Tambah Log Bantuan</h4>
|
||||
<div class="form-group">
|
||||
<label>Rumah Ibadah</label>
|
||||
<select id="logIbadahId"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tipe Bantuan</label>
|
||||
<select id="logTipeBantuan">
|
||||
<option value="Sembako">Sembako</option>
|
||||
<option value="Uang Tunai">Uang Tunai</option>
|
||||
<option value="Pakaian">Pakaian</option>
|
||||
<option value="Pendidikan">Pendidikan</option>
|
||||
<option value="Kesehatan">Kesehatan</option>
|
||||
<option value="Lainnya">Lainnya</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tanggal</label>
|
||||
<input type="date" id="logTanggal">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Keterangan (opsional)</label>
|
||||
<textarea id="logKeterangan" rows="2" placeholder="Catatan tambahan..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-save" id="logSaveBtn">Simpan Log</button>
|
||||
<button class="btn-cancel" onclick="closeLogBantuanModal()">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Profile / Auth Control -->
|
||||
<div id="authWidget" class="auth-widget">
|
||||
<button class="btn-login" id="authLoginBtn"><i class="fas fa-sign-in-alt"></i> Login</button>
|
||||
</div>
|
||||
|
||||
<!-- Login Modal -->
|
||||
<div id="loginModal" class="unified-modal">
|
||||
<div class="modal-content" style="width: 360px;">
|
||||
<div class="modal-header">
|
||||
<h3>Login Ke Sistem</h3>
|
||||
<span class="modal-close" id="closeLoginModal">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="loginUsername">Username</label>
|
||||
<input type="text" id="loginUsername" placeholder="Masukkan username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword">Password</label>
|
||||
<input type="password" id="loginPassword" placeholder="Masukkan password">
|
||||
</div>
|
||||
<div id="loginErrorMsg" style="display:none; color:#ef4444; font-size:12px; font-weight:500;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="loginSubmitBtn" class="btn-save" style="width: 100%;">Masuk</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Modal -->
|
||||
<div id="dashboardModal" class="unified-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>📊 Dashboard Informasi</h3>
|
||||
<span class="modal-close" id="closeDashboardModal">×</span>
|
||||
</div>
|
||||
<div id="dashboardBody"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Management Modal -->
|
||||
<div id="userManagementModal" class="unified-modal">
|
||||
<div class="modal-content" style="width: 700px; max-width: 95%;">
|
||||
<div class="modal-header">
|
||||
<h3>Manajemen Pengguna (Admin)</h3>
|
||||
<span class="modal-close" id="closeUserManagementModal">×</span>
|
||||
</div>
|
||||
<div class="modal-body" style="gap: 15px;">
|
||||
<!-- Form Tambah/Edit User -->
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; display: flex; flex-direction: column; gap: 10px;">
|
||||
<h4 id="userFormTitle" style="font-size: 13px; font-weight:700; color:#334155; margin-bottom: 2px;">Tambah User Baru</h4>
|
||||
<input type="hidden" id="manageUserId" value="">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" id="manageUsername" placeholder="Username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password (Kosongkan jika tidak diubah)</label>
|
||||
<input type="password" id="managePassword" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<div class="form-group">
|
||||
<label>Role</label>
|
||||
<select id="manageRole">
|
||||
<option value="pengelola" selected>Pengelola Rumah Ibadah</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="manageIbadahGroup">
|
||||
<label>Rumah Ibadah yang Dikelola</label>
|
||||
<select id="manageIbadahId">
|
||||
<!-- Dikonstruksi dinamis dari data rumah ibadah -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 5px;">
|
||||
<button id="btnCancelUserEdit" class="btn-cancel" style="padding: 6px 12px; display:none;">Batal</button>
|
||||
<button id="btnSaveUser" class="btn-save" style="padding: 6px 16px;">Simpan User</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabel Daftar User -->
|
||||
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #cbd5e1; border-radius: 8px;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 13px; text-align: left;">
|
||||
<thead>
|
||||
<tr style="background: #f1f5f9; border-bottom: 1px solid #cbd5e1; color:#475569; position: sticky; top: 0; z-index: 10;">
|
||||
<th style="padding: 10px 12px;">Username</th>
|
||||
<th style="padding: 10px 12px;">Role</th>
|
||||
<th style="padding: 10px 12px;">Rumah Ibadah</th>
|
||||
<th style="padding: 10px 12px; text-align: center;">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userTableBody">
|
||||
<!-- Diisi dinamis -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notification Container -->
|
||||
<div id="toastContainer"></div>
|
||||
|
||||
<!-- Disclaimer Banner -->
|
||||
<div id="disclaimerBanner" style="
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2000;
|
||||
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||
color: #f1f5f9;
|
||||
border: 1px solid #f59e0b;
|
||||
border-left: 4px solid #f59e0b;
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
max-width: 520px;
|
||||
width: calc(100% - 40px);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
animation: slideUpBanner 0.4s ease;
|
||||
">
|
||||
<span style="font-size: 20px; flex-shrink:0; margin-top:1px;">⚠️</span>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size: 13px; font-weight: 700; color: #fbbf24; margin-bottom: 4px; letter-spacing: 0.3px;">DISCLAIMER — DATA DUMMY</div>
|
||||
<div style="font-size: 12px; line-height: 1.6; color: #cbd5e1;">
|
||||
Seluruh data pada aplikasi ini termasuk <strong style="color:#fde68a;">nama kepala keluarga, foto rumah, dan foto Kartu Keluarga (KK)</strong> merupakan <strong style="color:#fde68a;">data fiktif / dummy</strong> yang hanya digunakan untuk keperluan demonstrasi dan pengujian sistem. Bukan data nyata.
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="document.getElementById('disclaimerBanner').style.display='none'" style="
|
||||
background: transparent;
|
||||
border: 1px solid #475569;
|
||||
color: #94a3b8;
|
||||
border-radius: 6px;
|
||||
padding: 3px 8px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
transition: all 0.2s;
|
||||
" onmouseover="this.style.borderColor='#f59e0b';this.style.color='#f59e0b'" onmouseout="this.style.borderColor='#475569';this.style.color='#94a3b8'">Tutup</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes slideUpBanner {
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(20px); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
|
||||
|
||||
<!-- Main Map Initialization -->
|
||||
<script src="assets/js/map.js?v=<?= time() ?>"></script>
|
||||
<!-- Fitur & Komponen -->
|
||||
<script src="assets/js/features/geolocation.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/kemiskinan.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/geojson.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
|
||||
|
||||
<!-- Core Auth Feature -->
|
||||
<script src="assets/js/features/auth.js?v=<?= time() ?>"></script>
|
||||
|
||||
<!-- Dashboard Feature -->
|
||||
<script src="assets/js/features/dashboard.js?v=<?= time() ?>"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,4 @@
|
||||
nama,kategori_bantuan,jumlah_jiwa,lat,lng
|
||||
Budi Santoso,Makan,4,-0.025,109.340
|
||||
Siti Aminah,Pemberdayaan,3,-0.028,109.345
|
||||
Andi Wijaya,Makan,5,-0.024,109.341
|
||||
|
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"nama": "Joko Widodo",
|
||||
"kategori_bantuan": "Pemberdayaan",
|
||||
"jumlah_jiwa": 2
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [109.343, -0.027]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"nama": "Mega Wati",
|
||||
"kategori_bantuan": "Makan",
|
||||
"jumlah_jiwa": 6
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [109.344, -0.029]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user