Files
2026-06-15 21:53:17 +07:00

2711 lines
131 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
session_start();
$is_readonly = false;
require_once 'config.php';
if (!isset($_GET['action'])) {
if(!isset($_SESSION['user'])) { header('Location: login.php'); exit; }
$is_readonly = ($_SESSION['role'] === 'superadmin') || isset($_GET['readonly']);
}
// ── AJAX handler ────────────────────────────────────────
if (isset($_GET['action'])) {
header('Content-Type: application/json; charset=utf-8');
$action = $_GET['action'];
// Haversine distance in metres
function haversine($lat1,$lng1,$lat2,$lng2){
$R=6371000;
$p1=deg2rad($lat1);$p2=deg2rad($lat2);
$dp=deg2rad($lat2-$lat1);$dl=deg2rad($lng2-$lng1);
$a=sin($dp/2)**2+cos($p1)*cos($p2)*sin($dl/2)**2;
return $R*2*atan2(sqrt($a),sqrt(1-$a));
}
// Recalculate nearest ibadah for one or all penduduk
function recalc($conn,$pid=null){
$ib=$conn->query("SELECT id,lat,lng,radius FROM rumah_ibadah");
$ilist=[];
while($r=$ib->fetch_assoc()) $ilist[]=$r;
$w=$pid?"WHERE id=".intval($pid):"";
$pd=$conn->query("SELECT id,lat,lng FROM penduduk_miskin $w");
while($p=$pd->fetch_assoc()){
$best=null;$bd=PHP_FLOAT_MAX;
foreach($ilist as $i){
$d=haversine($p['lat'],$p['lng'],$i['lat'],$i['lng']);
if($d<=$i['radius']&&$d<$bd){$bd=$d;$best=$i['id'];}
}
if($best){
$s=$conn->prepare("UPDATE penduduk_miskin SET rumah_ibadah_id=?,jarak_ke_ibadah=? WHERE id=?");
$s->bind_param('idi',$best,$bd,$p['id']);
} else {
$s=$conn->prepare("UPDATE penduduk_miskin SET rumah_ibadah_id=NULL,jarak_ke_ibadah=NULL WHERE id=?");
$s->bind_param('i',$p['id']);
}
$s->execute();
}
}
// ── GET ALL ────────────────────────────────
if($action==='get_all'){
$ri=[];
$r=$conn->query("SELECT * FROM rumah_ibadah ORDER BY nama");
while($row=$r->fetch_assoc()) $ri[]=$row;
$pm=[];
$r2=$conn->query("SELECT p.*,i.nama as nama_ibadah,i.jenis as jenis_ibadah
FROM penduduk_miskin p
LEFT JOIN rumah_ibadah i ON p.rumah_ibadah_id=i.id
ORDER BY p.nama_kk");
while($row=$r2->fetch_assoc()) $pm[]=$row;
$stats=[
'total_ibadah'=>(int)$conn->query("SELECT COUNT(*) c FROM rumah_ibadah")->fetch_assoc()['c'],
'total_penduduk'=>(int)$conn->query("SELECT COUNT(*) c FROM penduduk_miskin")->fetch_assoc()['c'],
'total_jiwa'=>(int)($conn->query("SELECT COALESCE(SUM(jumlah_anggota),0) c FROM penduduk_miskin")->fetch_assoc()['c']),
'terdaftar'=>(int)$conn->query("SELECT COUNT(*) c FROM penduduk_miskin WHERE rumah_ibadah_id IS NOT NULL")->fetch_assoc()['c'],
'belum'=>(int)$conn->query("SELECT COUNT(*) c FROM penduduk_miskin WHERE rumah_ibadah_id IS NULL")->fetch_assoc()['c'],
];
echo json_encode(['rumah_ibadah'=>$ri,'penduduk'=>$pm,'stats'=>$stats]);
exit;
}
// ── ADD IBADAH ─────────────────────────────
if($action==='add_ibadah'&&$_SERVER['REQUEST_METHOD']==='POST'){
$d=json_decode(file_get_contents('php://input'),true)??[];
$nama=$conn->real_escape_string($d['nama']??'');
$jenis=$conn->real_escape_string($d['jenis']??'Masjid');
$alamat=$conn->real_escape_string($d['alamat']??'');
$pic=$conn->real_escape_string($d['pic']??'');
$no_wa=$conn->real_escape_string($d['no_wa']??'');
$lat=floatval($d['lat']??0);
$lng=floatval($d['lng']??0);
$radius=max(500,intval($d['radius']??500));
if(!$nama||!$lat||!$lng){echo json_encode(['error'=>'Data tidak lengkap']);exit;}
$s=$conn->prepare("INSERT INTO rumah_ibadah(nama,jenis,alamat,pic,no_wa,lat,lng,radius) VALUES(?,?,?,?,?,?,?,?)");
$s->bind_param('sssssddi',$nama,$jenis,$alamat,$pic,$no_wa,$lat,$lng,$radius);
if($s->execute()){
$id=$conn->insert_id;
recalc($conn);
$row=$conn->query("SELECT * FROM rumah_ibadah WHERE id=$id")->fetch_assoc();
echo json_encode(['success'=>true,'id'=>$id,'data'=>$row]);
} else echo json_encode(['error'=>$conn->error]);
exit;
}
// ── EDIT IBADAH ────────────────────────────
if($action==='edit_ibadah'&&$_SERVER['REQUEST_METHOD']==='POST'){
$d=json_decode(file_get_contents('php://input'),true)??[];
$id=intval($d['id']??0);
$nama=$conn->real_escape_string($d['nama']??'');
$jenis=$conn->real_escape_string($d['jenis']??'Masjid');
$alamat=$conn->real_escape_string($d['alamat']??'');
$pic=$conn->real_escape_string($d['pic']??'');
$no_wa=$conn->real_escape_string($d['no_wa']??'');
$lat=floatval($d['lat']??0);
$lng=floatval($d['lng']??0);
$radius=max(500,intval($d['radius']??500));
$s=$conn->prepare("UPDATE rumah_ibadah SET nama=?,jenis=?,alamat=?,pic=?,no_wa=?,lat=?,lng=?,radius=? WHERE id=?");
$s->bind_param('sssssddii',$nama,$jenis,$alamat,$pic,$no_wa,$lat,$lng,$radius,$id);
if($s->execute()){recalc($conn);echo json_encode(['success'=>true]);}
else echo json_encode(['error'=>$conn->error]);
exit;
}
// ── DELETE IBADAH ──────────────────────────
if($action==='delete_ibadah'){
$d=json_decode(file_get_contents('php://input'),true)??[];
$id=intval($d['id']??$_GET['id']??0);
$s=$conn->prepare("DELETE FROM rumah_ibadah WHERE id=?");
$s->bind_param('i',$id);
if($s->execute()){recalc($conn);echo json_encode(['success'=>true]);}
else echo json_encode(['error'=>$conn->error]);
exit;
}
// ── UPLOAD FOTO RUMAH ──────────────────────
if($action==='upload_foto'&&$_SERVER['REQUEST_METHOD']==='POST'){
$id = intval($_POST['id'] ?? 0);
if(!$id || empty($_FILES['foto']['tmp_name'])){
echo json_encode(['error'=>'Data tidak valid']); exit;
}
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
$allowed = ['jpg','jpeg','png','webp'];
if(!in_array($ext, $allowed)){ echo json_encode(['error'=>'Format tidak didukung']); exit; }
$filename = 'foto_'.$id.'_'.time().'.'.$ext;
$dest = __DIR__.'/uploads/foto_rumah/'.$filename;
if(move_uploaded_file($_FILES['foto']['tmp_name'], $dest)){
// Hapus foto lama jika ada
$old = $conn->query("SELECT foto_rumah FROM penduduk_miskin WHERE id=$id")->fetch_assoc();
if($old['foto_rumah'] && file_exists(__DIR__.'/'.$old['foto_rumah']))
unlink(__DIR__.'/'.$old['foto_rumah']);
$path = 'uploads/foto_rumah/'.$filename;
$conn->query("UPDATE penduduk_miskin SET foto_rumah='$path' WHERE id=$id");
echo json_encode(['success'=>true,'path'=>$path]);
} else echo json_encode(['error'=>'Gagal upload']);
exit;
}
//ADD PENDUDUK
if($action==='add_penduduk'&&$_SERVER['REQUEST_METHOD']==='POST'){
// Ambil dari $_POST (bukan json), karena ada file upload
$nama_kk = $conn->real_escape_string($_POST['nama_kk'] ?? '');
$jml = intval($_POST['jumlah_anggota'] ?? 1);
$alamat = $conn->real_escape_string($_POST['alamat'] ?? '');
$lat = floatval($_POST['lat'] ?? 0);
$lng = floatval($_POST['lng'] ?? 0);
$jenis_bantuan = $conn->real_escape_string($_POST['jenis_bantuan'] ?? '');
$tanggal_bantuan = $conn->real_escape_string($_POST['tanggal_bantuan'] ?? '');
if(!$nama_kk||!$lat||!$lng){echo json_encode(['error'=>'Data tidak lengkap']);exit;}
$s=$conn->prepare("INSERT INTO penduduk_miskin(nama_kk,jumlah_anggota,alamat,lat,lng,jenis_bantuan,tanggal_bantuan) VALUES(?,?,?,?,?,?,?)");
$tanggal_bantuan_val = $tanggal_bantuan ? $tanggal_bantuan : null;
$s->bind_param('sisddss',$nama_kk,$jml,$alamat,$lat,$lng,$jenis_bantuan,$tanggal_bantuan_val);
if($s->execute()){
$id=$conn->insert_id;
// Log to riwayat_bantuan if provided
if($jenis_bantuan && $tanggal_bantuan_val) {
$conn->query("INSERT INTO riwayat_bantuan (penduduk_id, jenis_bantuan, tanggal_pemberian, keterangan) VALUES ($id, '$jenis_bantuan', '$tanggal_bantuan_val', 'Bantuan pertama saat pendaftaran KK')");
}
// Proses upload foto jika ada
if(!empty($_FILES['foto']['tmp_name'])){
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
$allowed = ['jpg','jpeg','png','webp'];
if(in_array($ext,$allowed)){
$filename = 'foto_'.$id.'_'.time().'.'.$ext;
$dest = __DIR__.'/uploads/foto_rumah/'.$filename;
if(move_uploaded_file($_FILES['foto']['tmp_name'],$dest)){
$path = 'uploads/foto_rumah/'.$filename;
$conn->query("UPDATE penduduk_miskin SET foto_rumah='$path' WHERE id=$id");
}
}
}
recalc($conn,$id);
$row=$conn->query("SELECT p.*,i.nama as nama_ibadah,i.jenis as jenis_ibadah
FROM penduduk_miskin p LEFT JOIN rumah_ibadah i ON p.rumah_ibadah_id=i.id
WHERE p.id=$id")->fetch_assoc();
echo json_encode(['success'=>true,'id'=>$id,'data'=>$row]);
} else echo json_encode(['error'=>$conn->error]);
exit;
}
// ── EDIT PENDUDUK ──────────────────────────
if($action==='edit_penduduk'&&$_SERVER['REQUEST_METHOD']==='POST'){
$id=intval($_POST['id']??0);
$nama_kk=$conn->real_escape_string($_POST['nama_kk']??'');
$jml=intval($_POST['jumlah_anggota']??1);
$alamat=$conn->real_escape_string($_POST['alamat']??'');
$lat=floatval($_POST['lat']??0);
$lng=floatval($_POST['lng']??0);
$jenis_bantuan = $conn->real_escape_string($_POST['jenis_bantuan'] ?? '');
$tanggal_bantuan = $conn->real_escape_string($_POST['tanggal_bantuan'] ?? '');
$s=$conn->prepare("UPDATE penduduk_miskin SET nama_kk=?,jumlah_anggota=?,alamat=?,lat=?,lng=?,jenis_bantuan=?,tanggal_bantuan=? WHERE id=?");
$tanggal_bantuan_val = $tanggal_bantuan ? $tanggal_bantuan : null;
$s->bind_param('sisddssi',$nama_kk,$jml,$alamat,$lat,$lng,$jenis_bantuan,$tanggal_bantuan_val,$id);
// Proses upload foto jika ada
if(!empty($_FILES['foto']['tmp_name'])){
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
$allowed = ['jpg','jpeg','png','webp'];
if(in_array($ext,$allowed)){
$filename = 'foto_'.$id.'_'.time().'.'.$ext;
$dest = __DIR__.'/uploads/foto_rumah/'.$filename;
if(move_uploaded_file($_FILES['foto']['tmp_name'],$dest)){
// Hapus foto lama jika ada
$old = $conn->query("SELECT foto_rumah FROM penduduk_miskin WHERE id=$id")->fetch_assoc();
if($old['foto_rumah'] && file_exists(__DIR__.'/'.$old['foto_rumah'])) unlink(__DIR__.'/'.$old['foto_rumah']);
$path = 'uploads/foto_rumah/'.$filename;
$conn->query("UPDATE penduduk_miskin SET foto_rumah='$path' WHERE id=$id");
}
}
}
if($s->execute()){
// Log to riwayat_bantuan if new combination
if($jenis_bantuan && $tanggal_bantuan_val) {
$check = $conn->query("SELECT id FROM riwayat_bantuan WHERE penduduk_id=$id AND jenis_bantuan='$jenis_bantuan' AND tanggal_pemberian='$tanggal_bantuan_val'")->num_rows;
if ($check == 0) {
$conn->query("INSERT INTO riwayat_bantuan (penduduk_id, jenis_bantuan, tanggal_pemberian, keterangan) VALUES ($id, '$jenis_bantuan', '$tanggal_bantuan_val', 'Bantuan baru diinput lewat edit KK')");
}
}
recalc($conn,$id);
echo json_encode(['success'=>true]);
}
else echo json_encode(['error'=>$conn->error]);
exit;
}
// ── DELETE PENDUDUK ────────────────────────
if($action==='delete_penduduk'){
$d=json_decode(file_get_contents('php://input'),true)??[];
$id=intval($d['id']??$_GET['id']??0);
$s=$conn->prepare("DELETE FROM penduduk_miskin WHERE id=?");
$s->bind_param('i',$id);
if($s->execute()) echo json_encode(['success'=>true]);
else echo json_encode(['error'=>$conn->error]);
exit;
}
// ── RECALCULATE ────────────────────────────
if($action==='recalculate'){
recalc($conn);
echo json_encode(['success'=>true]);
exit;
}
// ── ADD BANTUAN ───────────────────────────
if($action==='add_bantuan'&&$_SERVER['REQUEST_METHOD']==='POST'){
$d=json_decode(file_get_contents('php://input'),true)??[];
$pid=intval($d['penduduk_id']??0);
$jenis=$conn->real_escape_string($d['jenis_bantuan']??'');
$tanggal=$conn->real_escape_string($d['tanggal_pemberian']??'');
$ket=$conn->real_escape_string($d['keterangan']??'');
$ri_id=isset($d['rumah_ibadah_id']) && $d['rumah_ibadah_id'] !== '' ? intval($d['rumah_ibadah_id']) : null;
if(!$pid||!$jenis||!$tanggal){echo json_encode(['error'=>'Data tidak lengkap']);exit;}
$s=$conn->prepare("INSERT INTO riwayat_bantuan(penduduk_id,jenis_bantuan,tanggal_pemberian,keterangan,rumah_ibadah_id) VALUES(?,?,?,?,?)");
$s->bind_param('isssi',$pid,$jenis,$tanggal,$ket,$ri_id);
if($s->execute()){
// Update jenis_bantuan & tanggal_bantuan di penduduk_miskin dengan yg terbaru
$conn->query("UPDATE penduduk_miskin SET jenis_bantuan='$jenis', tanggal_bantuan='$tanggal' WHERE id=$pid");
echo json_encode(['success'=>true,'id'=>$conn->insert_id]);
} else echo json_encode(['error'=>$conn->error]);
exit;
}
// ── GET BANTUAN (per penduduk) ────────────
if($action==='get_bantuan'){
$pid=intval($_GET['penduduk_id']??0);
$rows=[];
$r=$conn->query("SELECT rb.*, ri.nama as nama_ibadah FROM riwayat_bantuan rb LEFT JOIN rumah_ibadah ri ON rb.rumah_ibadah_id=ri.id WHERE rb.penduduk_id=$pid ORDER BY rb.tanggal_pemberian DESC, rb.id DESC");
while($row=$r->fetch_assoc()) $rows[]=$row;
echo json_encode($rows);
exit;
}
// ── DELETE BANTUAN ────────────────────────
if($action==='delete_bantuan'){
$d=json_decode(file_get_contents('php://input'),true)??[];
$id=intval($d['id']??$_GET['id']??0);
// Get penduduk_id before delete
$row=$conn->query("SELECT penduduk_id FROM riwayat_bantuan WHERE id=$id")->fetch_assoc();
$s=$conn->prepare("DELETE FROM riwayat_bantuan WHERE id=?");
$s->bind_param('i',$id);
if($s->execute()){
// Update penduduk with latest remaining bantuan
if($row){
$pid=$row['penduduk_id'];
$latest=$conn->query("SELECT jenis_bantuan,tanggal_pemberian FROM riwayat_bantuan WHERE penduduk_id=$pid ORDER BY tanggal_pemberian DESC LIMIT 1")->fetch_assoc();
if($latest) $conn->query("UPDATE penduduk_miskin SET jenis_bantuan='".$conn->real_escape_string($latest['jenis_bantuan'])."', tanggal_bantuan='".$latest['tanggal_pemberian']."' WHERE id=$pid");
else $conn->query("UPDATE penduduk_miskin SET jenis_bantuan=NULL, tanggal_bantuan=NULL WHERE id=$pid");
}
echo json_encode(['success'=>true]);
}
else echo json_encode(['error'=>$conn->error]);
exit;
}
echo json_encode(['error'=>'Action tidak dikenali']);
exit;
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Pemetaan Kemiskinan Terpadu — Pontianak</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
/* ══════════════════════════════════════════
LIGHT THEME — CLEAN GOVERNMENT/CIVIC STYLE
══════════════════════════════════════════ */
:root {
--white: #ffffff;
--bg: #f4f6f9;
--bg2: #eef0f5;
--surface: #ffffff;
--border: #dde2ea;
--border2: #c8d0dc;
--text: #1a2233;
--text2: #4b5a6e;
--text3: #8a97a8;
--blue: #2563eb;
--blue-lt: #dbeafe;
--blue-dk: #1d4ed8;
--green: #16a34a;
--green-lt: #dcfce7;
--red: #dc2626;
--red-lt: #fee2e2;
--orange: #ea580c;
--orange-lt:#ffedd5;
--amber: #d97706;
--violet: #7c3aed;
--violet-lt:#ede9fe;
--cyan: #0891b2;
--cyan-lt: #cffafe;
--pink: #db2777;
--pink-lt: #fce7f3;
--radius: 10px;
--shadow: 0 1px 4px rgba(0,0,0,.08), 0 4px 16px rgba(0,0,0,.06);
--shadow-lg:0 8px 32px rgba(0,0,0,.12);
--sidebar-w:270px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; font-family: 'DM Sans', sans-serif; color: var(--text); background: var(--bg); }
/* ── LAYOUT ── */
.app { display: flex; height: 100vh; overflow: hidden; }
/* ── SIDEBAR ── */
#sidebar {
width: var(--sidebar-w); min-width: var(--sidebar-w);
background: var(--white);
border-right: 1px solid var(--border);
display: flex; flex-direction: column;
z-index: 900; overflow: hidden;
box-shadow: 2px 0 8px rgba(0,0,0,.06);
}
.sb-brand {
padding: 18px 16px 14px;
border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 11px;
}
.sb-logo {
width: 38px; height: 38px; border-radius: 10px;
background: linear-gradient(135deg, var(--blue), #60a5fa);
display: flex; align-items: center; justify-content: center;
font-size: 18px; flex-shrink: 0;
box-shadow: 0 2px 8px rgba(37,99,235,.25);
}
.sb-brand h1 { font-size: 13px; font-weight: 700; line-height: 1.3; color: var(--text); }
.sb-brand p { font-size: 10px; color: var(--text3); margin-top: 1px; }
/* Stats strip */
.sb-stats {
display: grid; grid-template-columns: 1fr 1fr;
gap: 8px; padding: 12px;
border-bottom: 1px solid var(--border);
}
.sb-stat {
background: var(--bg); border: 1px solid var(--border);
border-radius: 8px; padding: 9px 10px; text-align: center;
}
.sb-stat-n { font-size: 20px; font-weight: 800; font-family: 'DM Mono', monospace; line-height: 1; }
.sb-stat-n.blue { color: var(--blue); }
.sb-stat-n.green { color: var(--green); }
.sb-stat-n.red { color: var(--red); }
.sb-stat-n.amber { color: var(--amber); }
.sb-stat-l { font-size: 9.5px; color: var(--text3); margin-top: 3px; font-weight: 600; text-transform: uppercase; letter-spacing: .4px; }
/* Nav */
.sb-nav { flex: 1; overflow-y: auto; padding: 8px; }
.sb-nav::-webkit-scrollbar { width: 3px; }
.sb-nav::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
.sb-section-lbl {
font-size: 9.5px; font-weight: 700; color: var(--text3);
text-transform: uppercase; letter-spacing: .8px;
padding: 10px 8px 4px;
}
.sb-item {
display: flex; align-items: center; gap: 9px;
padding: 8px 10px; border-radius: 8px; cursor: pointer;
font-size: 13px; font-weight: 500; color: var(--text2);
transition: all .15s; border: 1px solid transparent;
margin-bottom: 2px;
}
.sb-item:hover { background: var(--bg); color: var(--text); }
.sb-item.active { background: var(--blue-lt); color: var(--blue); border-color: #bfdbfe; font-weight: 600; }
.sb-item .ico { font-size: 16px; width: 20px; text-align: center; flex-shrink: 0; }
.sb-badge {
margin-left: auto; padding: 2px 7px; border-radius: 20px;
font-size: 10px; font-weight: 700;
}
.sb-badge.blue { background: var(--blue-lt); color: var(--blue); }
.sb-badge.green { background: var(--green-lt); color: var(--green); }
.sb-badge.red { background: var(--red-lt); color: var(--red); }
.sb-badge.amber { background: var(--orange-lt); color: var(--orange); }
.sb-footer {
padding: 10px;
border-top: 1px solid var(--border);
}
.sb-footer-btn {
width: 100%; padding: 8px; background: var(--bg);
border: 1px solid var(--border); border-radius: 8px;
font-size: 12px; color: var(--text2); cursor: pointer;
font-family: inherit; font-weight: 500;
display: flex; align-items: center; justify-content: center; gap: 6px;
transition: all .15s;
}
.sb-footer-btn:hover { border-color: var(--blue); color: var(--blue); }
.btn-back-dashboard {
width: 100%; padding: 8px; background: var(--blue-lt);
border: 1px solid #bfdbfe; border-radius: 8px;
font-size: 12px; color: var(--blue); cursor: pointer;
font-family: inherit; font-weight: 600;
display: flex; align-items: center; justify-content: center; gap: 6px;
text-decoration: none; transition: all .15s; margin-bottom: 6px;
}
.btn-back-dashboard:hover { background: var(--blue); color: #fff; }
.btn-logout {
width: 100%; padding: 8px; background: var(--red-lt);
border: 1px solid #fca5a5; border-radius: 8px;
font-size: 12px; color: var(--red); cursor: pointer;
font-family: inherit; font-weight: 600;
display: flex; align-items: center; justify-content: center; gap: 6px;
text-decoration: none; transition: all .15s;
}
.btn-logout:hover { background: var(--red); color: #fff; }
/* ── SEARCH BAR (sidebar) ── */
.sb-search {
padding: 10px 12px; border-bottom: 1px solid var(--border);
position: relative;
}
.sb-search-wrap {
display: flex; align-items: center; gap: 7px;
background: var(--bg); border: 1px solid var(--border);
border-radius: 8px; padding: 7px 10px; transition: border-color .15s;
}
.sb-search-wrap:focus-within { border-color: var(--blue); background: #fff; }
.sb-search-wrap .s-ico { font-size: 13px; color: var(--text3); flex-shrink: 0; }
.sb-search-wrap input {
border: none; background: transparent; outline: none;
font-family: inherit; font-size: 12.5px; color: var(--text); flex: 1; min-width: 0;
}
.sb-search-wrap input::placeholder { color: var(--text3); }
.sb-search-clear {
background: none; border: none; cursor: pointer; color: var(--text3);
font-size: 13px; padding: 0; line-height: 1; display: none; flex-shrink: 0;
}
.sb-search-clear.show { display: block; }
.sb-search-results {
position: absolute; left: 12px; right: 12px; top: calc(100% - 2px); z-index: 300;
background: #fff; border: 1px solid var(--border); border-top: none;
border-radius: 0 0 10px 10px; box-shadow: var(--shadow-lg);
max-height: 260px; overflow-y: auto; display: none;
}
.sb-search-results.show { display: block; }
.sr-item {
display: flex; align-items: center; gap: 9px;
padding: 9px 12px; cursor: pointer; border-bottom: 1px solid var(--border);
font-size: 12.5px; transition: background .1s;
}
.sr-item:last-child { border-bottom: none; }
.sr-item:hover { background: var(--bg); }
.sr-ico {
width: 28px; height: 28px; border-radius: 7px;
display: flex; align-items: center; justify-content: center;
font-size: 13px; flex-shrink: 0;
}
.sr-name { font-weight: 600; line-height: 1.3; color: var(--text); }
.sr-sub { font-size: 11px; color: var(--text3); margin-top: 1px; }
.sr-type {
margin-left: auto; font-size: 10px; padding: 2px 6px;
border-radius: 10px; font-weight: 600; flex-shrink: 0;
}
.dp-search {
padding: 8px 10px; border-bottom: 1px solid var(--border); flex-shrink: 0;
}
.dp-search input {
width: 100%; padding: 7px 10px; border: 1px solid var(--border);
border-radius: 8px; font-family: inherit; font-size: 12.5px;
color: var(--text); background: var(--bg); outline: none; transition: border-color .15s;
}
.dp-search input:focus { border-color: var(--blue); background: #fff; }
/* ── MAIN ── */
#main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* Topbar */
#topbar {
height: 50px; background: var(--white);
border-bottom: 1px solid var(--border);
display: flex; align-items: center; padding: 0 16px; gap: 10px;
flex-shrink: 0;
}
.tb-title { font-size: 14px; font-weight: 700; flex: 1; color: var(--text); }
.tb-title span { color: var(--text3); font-weight: 400; font-size: 12px; margin-left: 6px; }
/* Buttons */
.btn {
padding: 7px 14px; border-radius: 8px; font-size: 12.5px; font-weight: 600;
cursor: pointer; font-family: inherit; border: 1px solid transparent;
transition: all .15s; display: inline-flex; align-items: center; gap: 6px;
white-space: nowrap;
}
.btn-primary { background: var(--blue); color: #fff; }
.btn-primary:hover { background: var(--blue-dk); box-shadow: 0 2px 8px rgba(37,99,235,.3); }
.btn-success { background: var(--green); color: #fff; }
.btn-success:hover { background: #15803d; }
.btn-outline { background: var(--white); color: var(--text2); border-color: var(--border); }
.btn-outline:hover { border-color: var(--blue); color: var(--blue); }
.btn-danger { background: transparent; color: var(--red); border-color: var(--red); }
.btn-danger:hover { background: var(--red); color: #fff; }
.btn-amber { background: var(--amber); color: #fff; border-color: var(--amber); }
.btn-sm { padding: 5px 10px; font-size: 11.5px; }
.btn-xs { padding: 3px 8px; font-size: 11px; }
.btn:disabled { opacity: .5; cursor: not-allowed; }
/* ── MAP AREA ── */
#map-wrap { flex: 1; position: relative; overflow: hidden; }
#map { width: 100%; height: 100%; }
/* Map controls (top-right overlay) */
#map-controls {
position: absolute; top: 12px; right: 12px; z-index: 500;
display: flex; flex-direction: column; gap: 8px; width: 220px;
}
.ctrl-card {
background: var(--white); border: 1px solid var(--border);
border-radius: var(--radius); padding: 12px;
box-shadow: var(--shadow);
}
.ctrl-card h4 {
font-size: 10px; font-weight: 700; color: var(--text3);
text-transform: uppercase; letter-spacing: .5px; margin-bottom: 8px;
}
.radius-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.radius-val { font-family: 'DM Mono', monospace; font-size: 13px; font-weight: 600; color: var(--blue); }
input[type=range] {
width: 100%; accent-color: var(--blue); cursor: pointer;
height: 4px;
}
.legend-row { display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--text2); margin-bottom: 4px; }
.leg-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.leg-ring { width: 12px; height: 12px; border-radius: 50%; border: 2px solid; background: transparent; flex-shrink: 0; }
/* Radius inline editor (top-right, appears when circle clicked) */
#radius-editor {
position: absolute; top: 12px; right: 12px; z-index: 700;
background: var(--white); border: 2px solid var(--blue);
border-radius: 12px; padding: 14px 16px;
box-shadow: 0 4px 24px rgba(37,99,235,.2);
width: 240px; display: none;
}
#radius-editor.show { display: block; }
#radius-editor h4 {
font-size: 11px; font-weight: 700; color: var(--blue);
text-transform: uppercase; letter-spacing: .5px; margin-bottom: 10px;
display: flex; align-items: center; gap: 6px;
}
.re-name { font-size: 13px; font-weight: 700; color: var(--text); margin-bottom: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.re-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.re-val { font-family: 'DM Mono', monospace; font-size: 18px; font-weight: 700; color: var(--blue); }
.re-hint { font-size: 10px; color: var(--text3); margin-bottom: 8px; }
.re-actions { display: flex; gap: 6px; margin-top: 10px; }
/* Add mode instruction banner */
#add-banner {
position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
z-index: 500; background: var(--blue); color: #fff;
padding: 8px 18px; border-radius: 24px;
font-size: 13px; font-weight: 600;
box-shadow: 0 4px 16px rgba(37,99,235,.35);
display: none; align-items: center; gap: 8px;
}
/* ── DATA PANEL (right drawer) ── */
#data-panel {
position: absolute; top: 0; right: -360px; width: 360px; height: 100%;
background: var(--white); border-left: 1px solid var(--border);
z-index: 600; transition: right .3s cubic-bezier(.4,0,.2,1);
display: flex; flex-direction: column; overflow: hidden;
box-shadow: -4px 0 20px rgba(0,0,0,.08);
}
#data-panel.open { right: 0; }
.dp-head {
padding: 14px 16px; border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 10px; flex-shrink: 0;
}
.dp-head h3 { font-size: 14px; font-weight: 700; flex: 1; }
.dp-close {
width: 28px; height: 28px; border-radius: 7px;
background: var(--bg); border: 1px solid var(--border);
cursor: pointer; font-size: 14px; color: var(--text2);
display: flex; align-items: center; justify-content: center; transition: all .15s;
}
.dp-close:hover { border-color: var(--red); color: var(--red); }
.dp-tabs { display: flex; border-bottom: 1px solid var(--border); flex-shrink: 0; }
.dp-tab {
flex: 1; padding: 9px; text-align: center; cursor: pointer;
font-size: 12px; font-weight: 600; color: var(--text3);
border-bottom: 2px solid transparent; transition: all .15s;
}
.dp-tab.active { color: var(--blue); border-bottom-color: var(--blue); }
.dp-body { flex: 1; overflow-y: auto; padding: 10px; }
.dp-body::-webkit-scrollbar { width: 3px; }
.dp-body::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
/* Data cards in panel */
.dc {
background: var(--white); border: 1px solid var(--border);
border-radius: var(--radius); padding: 11px; margin-bottom: 7px;
transition: border-color .15s, box-shadow .15s; cursor: pointer;
}
.dc:hover { border-color: var(--blue); box-shadow: 0 2px 8px rgba(37,99,235,.1); }
.dc-top { display: flex; align-items: flex-start; gap: 9px; margin-bottom: 7px; }
.dc-ico {
width: 30px; height: 30px; border-radius: 8px; font-size: 13px;
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.dc-name { font-size: 13px; font-weight: 700; line-height: 1.3; }
.dc-sub { font-size: 11px; color: var(--text3); margin-top: 2px; }
.dc-acts { margin-left: auto; display: flex; gap: 4px; }
.dc-tags { display: flex; flex-wrap: wrap; gap: 4px; }
.tag {
font-size: 10px; padding: 2px 7px; border-radius: 20px; font-weight: 600;
}
.tag-blue { background: var(--blue-lt); color: var(--blue); }
.tag-green { background: var(--green-lt); color: var(--green); }
.tag-red { background: var(--red-lt); color: var(--red); }
.tag-amber { background: var(--orange-lt); color: var(--orange); }
.tag-violet { background: var(--violet-lt); color: var(--violet); }
.tag-gray { background: var(--bg2); color: var(--text2); }
/* ── MODAL ── */
.overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.45);
z-index: 2000; display: flex; align-items: center; justify-content: center;
opacity: 0; pointer-events: none; transition: opacity .2s;
backdrop-filter: blur(3px);
}
.overlay.show { opacity: 1; pointer-events: all; }
.modal {
background: var(--white); border: 1px solid var(--border);
border-radius: 14px; width: 500px; max-width: 96vw; max-height: 92vh;
overflow-y: auto; box-shadow: var(--shadow-lg);
transform: scale(.96) translateY(16px); transition: transform .2s;
}
.overlay.show .modal { transform: scale(1) translateY(0); }
.modal-hd {
padding: 18px 20px 14px; border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 11px;
position: sticky; top: 0; background: var(--white); z-index: 1;
}
.modal-ico {
width: 38px; height: 38px; border-radius: 9px;
display: flex; align-items: center; justify-content: center; font-size: 17px;
flex-shrink: 0;
}
.modal-ico.blue { background: var(--blue-lt); }
.modal-ico.green { background: var(--green-lt); }
.modal-ico.red { background: var(--red-lt); }
.modal-hd h3 { font-size: 15px; font-weight: 700; }
.modal-hd p { font-size: 11.5px; color: var(--text3); margin-top: 2px; }
.modal-x {
margin-left: auto; width: 30px; height: 30px; border-radius: 7px;
background: var(--bg); border: 1px solid var(--border);
cursor: pointer; font-size: 15px; color: var(--text2);
display: flex; align-items: center; justify-content: center; transition: all .15s;
}
.modal-x:hover { border-color: var(--red); color: var(--red); }
.modal-body { padding: 18px 20px; }
.modal-ft {
padding: 14px 20px; border-top: 1px solid var(--border);
display: flex; justify-content: flex-end; gap: 8px;
position: sticky; bottom: 0; background: var(--white);
}
/* Form */
.fg { margin-bottom: 14px; }
.fl { display: block; font-size: 12px; font-weight: 600; color: var(--text2); margin-bottom: 5px; }
.fl .req { color: var(--red); margin-left: 3px; }
.fi, .fs, .fta {
width: 100%; padding: 8px 11px;
background: var(--white); border: 1px solid var(--border);
border-radius: 8px; color: var(--text); font-family: inherit; font-size: 13px;
transition: border-color .15s, box-shadow .15s;
}
.fi:focus, .fs:focus, .fta:focus {
outline: none; border-color: var(--blue);
box-shadow: 0 0 0 3px rgba(37,99,235,.1);
}
.fs { cursor: pointer; }
.fta { resize: vertical; min-height: 65px; }
.fg2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.fhint { font-size: 11px; color: var(--text3); margin-top: 4px; }
.fcoords {
background: var(--bg); border: 1px solid var(--border);
border-radius: 8px; padding: 9px 11px;
font-family: 'DM Mono', monospace; font-size: 12px; color: var(--blue);
display: flex; align-items: center; gap: 8px;
}
.fcoords-lbl { font-family: 'DM Sans', sans-serif; font-size: 11px; color: var(--text3); }
.info-box {
background: #eff6ff; border: 1px solid #bfdbfe;
border-radius: 8px; padding: 9px 12px; font-size: 12px; color: #1e40af;
margin-bottom: 14px; display: flex; gap: 7px; align-items: flex-start;
}
.info-box .ii { font-size: 13px; flex-shrink: 0; margin-top: 1px; }
/* Radius slider in form */
.radius-form-row { display: flex; align-items: center; gap: 10px; }
.radius-form-val { font-family: 'DM Mono', monospace; font-weight: 600; color: var(--blue); font-size: 13px; min-width: 55px; }
/* ── POPUP ── */
.leaflet-popup-content-wrapper {
background: var(--white) !important;
border: 1px solid var(--border) !important;
border-radius: 12px !important;
box-shadow: var(--shadow-lg) !important;
padding: 0 !important;
}
.leaflet-popup-content { margin: 0 !important; }
.leaflet-popup-tip { background: var(--white) !important; }
.pc { padding: 14px 16px; min-width: 210px; font-family: 'DM Sans', sans-serif; }
.pc-head { display: flex; align-items: flex-start; gap: 9px; margin-bottom: 10px; }
.pc-ico { font-size: 20px; flex-shrink: 0; }
.pc-name { font-size: 14px; font-weight: 700; color: var(--text); line-height: 1.3; }
.pc-badge { display: inline-block; font-size: 10px; padding: 1px 7px; border-radius: 20px; margin-top: 3px; font-weight: 600; }
.pc-row { font-size: 12px; color: var(--text2); margin-bottom: 5px; display: flex; gap: 5px; }
.pc-row b { color: var(--text); min-width: 80px; display: inline-block; }
.pc-status { display: flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600; padding: 5px 0; margin: 6px 0; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.pc-actions { display: flex; gap: 5px; margin-top: 10px; }
.pca {
flex: 1; padding: 5px; border-radius: 7px; font-size: 11px; font-weight: 600;
cursor: pointer; font-family: inherit; border: 1px solid var(--border);
background: var(--bg); color: var(--text2); transition: all .15s; text-align: center;
}
.pca:hover { border-color: var(--blue); color: var(--blue); background: var(--blue-lt); }
.pca.del:hover { border-color: var(--red); color: var(--red); background: var(--red-lt); }
/* ── TOAST ── */
#toasts {
position: fixed; bottom: 18px; right: 18px; z-index: 9999;
display: flex; flex-direction: column; gap: 7px; pointer-events: none;
}
.toast {
background: var(--white); border: 1px solid var(--border); border-radius: 10px;
padding: 11px 15px; display: flex; align-items: center; gap: 10px;
font-size: 13px; box-shadow: var(--shadow-lg); min-width: 250px;
animation: tIn .25s ease; pointer-events: all;
border-left: 3px solid var(--blue);
}
.toast.success { border-left-color: var(--green); }
.toast.error { border-left-color: var(--red); }
.toast.warn { border-left-color: var(--amber); }
@keyframes tIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes tOut { to { transform: translateX(110%); opacity: 0; } }
.toast.out { animation: tOut .25s ease forwards; }
/* ── LOADING ── */
#loading {
position: fixed; inset: 0; background: rgba(244,246,249,.85);
z-index: 8000; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 12px;
}
.spinner {
width: 38px; height: 38px; border-radius: 50%;
border: 3px solid var(--border2);
border-top-color: var(--blue);
animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
#loading p { font-size: 14px; color: var(--text2); font-weight: 500; }
/* Leaflet zoom */
.leaflet-control-zoom { border: 1px solid var(--border) !important; border-radius: 9px !important; overflow: hidden; box-shadow: var(--shadow) !important; }
.leaflet-control-zoom a { background: var(--white) !important; color: var(--text) !important; border-color: var(--border) !important; width: 30px !important; height: 30px !important; line-height: 30px !important; font-size: 16px !important; transition: all .15s !important; }
.leaflet-control-zoom a:hover { background: var(--bg) !important; color: var(--blue) !important; }
/* Empty state */
.empty { text-align: center; padding: 32px 16px; color: var(--text3); }
.empty .ei { font-size: 32px; margin-bottom: 8px; }
.empty p { font-size: 13px; }
/* Dragging cursor */
.map-dragging { cursor: grabbing !important; }
</style>
<script src="https://cdn.jsdelivr.net/npm/exif-js"></script>
</head>
<body>
<div class="app">
<!-- ═══════ SIDEBAR ═══════ -->
<aside id="sidebar">
<div class="sb-brand">
<div class="sb-logo"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#fff" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg></div>
<div>
<h1>Kemiskinan Terpadu</h1>
<p>Sistem Informasi Geografis</p>
</div>
</div>
<!-- ── SEARCH BAR ── -->
<div class="sb-search">
<div class="sb-search-wrap">
<span class="s-ico"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg></span>
<input type="text" id="sb-q" placeholder="Cari ibadah / penduduk…"
oninput="onSbSearch(this.value)" autocomplete="off">
<button class="sb-search-clear" id="sb-q-clear" onclick="clearSbSearch()" title="Hapus pencarian">✕</button>
</div>
<div class="sb-search-results" id="sb-search-results"></div>
</div>
<div class="sb-stats" id="sb-stats">
<div class="sb-stat"><div class="sb-stat-n blue" id="s-ibadah">0</div><div class="sb-stat-l">Rumah Ibadah</div></div>
<div class="sb-stat"><div class="sb-stat-n amber" id="s-penduduk">0</div><div class="sb-stat-l">KK Miskin</div></div>
<div class="sb-stat"><div class="sb-stat-n green" id="s-terdaftar">0</div><div class="sb-stat-l">Terdaftar</div></div>
<div class="sb-stat"><div class="sb-stat-n red" id="s-belum">0</div><div class="sb-stat-l">Belum Terdaftar</div></div>
</div>
<nav class="sb-nav">
<div class="sb-section-lbl">Peta</div>
<div class="sb-item active" id="nav-peta" onclick="showView('peta')">
<span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg></span><span>Lihat Peta</span>
</div>
<div class="sb-section-lbl">Kelola Data</div>
<div class="sb-item" id="nav-ibadah" onclick="showView('ibadah')">
<span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 21h18M5 21V7l7-4 7 4v14"/><path d="M9 21v-6h6v6"/></svg></span><span>Rumah Ibadah</span>
<span class="sb-badge blue" id="nb-ibadah">0</span>
</div>
<div class="sb-item" id="nav-penduduk" onclick="showView('penduduk')">
<span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/></svg></span><span>Penduduk Miskin</span>
<span class="sb-badge amber" id="nb-penduduk">0</span>
</div>
<div class="sb-section-lbl" <?= $is_readonly?'style="display:none"':'' ?>>Aksi</div>
<div class="sb-item" onclick="startAddMode('ibadah')" <?= $is_readonly?'style="display:none"':'' ?>>
<span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg></span><span>Tambah Rumah Ibadah</span>
</div>
<div class="sb-item" onclick="startAddMode('penduduk')" <?= $is_readonly?'style="display:none"':'' ?>>
<span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg></span><span>Tambah Penduduk Miskin</span>
</div>
<div class="sb-item" onclick="doRecalculate()" <?= $is_readonly?'style="display:none"':'' ?>>
<span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg></span><span>Hitung Ulang Radius</span>
</div>
</nav>
<div class="sb-footer">
<?php include __DIR__.'/includes/user-profile.php'; ?>
<?php if($is_readonly && $_SESSION['role'] === 'superadmin'): ?>
<a href="dashboard.php" class="btn-back-dashboard">Kembali ke Dashboard</a>
<?php endif ?>
<a href="logout.php" class="btn-logout" data-confirm-logout>Keluar</a>
</div>
</aside>
<!-- ═══════ MAIN ═══════ -->
<div id="main">
<!-- Topbar -->
<div id="topbar">
<button id="sb-toggle" onclick="toggleSidebar()" style="background:none;border:none;cursor:pointer;color:var(--text2);padding:4px;display:flex;align-items:center;"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg></button>
<div class="tb-title" id="tb-title">Peta GIS <span>Rumah Ibadah &amp; Penduduk Miskin</span></div>
<button class="btn btn-primary btn-sm" onclick="startAddMode('ibadah')" <?= $is_readonly?'style="display:none"':'' ?>><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px"><path d="M3 21h18M5 21V7l7-4 7 4v14"/><path d="M9 21v-6h6v6"/></svg>+ Ibadah</button>
<button class="btn btn-success btn-sm" onclick="startAddMode('penduduk')" <?= $is_readonly?'style="display:none"':'' ?>><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>+ Penduduk</button>
<button class="btn btn-outline btn-sm" id="dp-toggle" onclick="toggleDataPanel()"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/></svg>Data</button>
</div>
<!-- Map wrap -->
<div id="map-wrap">
<div id="map"></div>
<!-- Add mode banner -->
<div id="add-banner">
<span id="banner-txt"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle;margin-right:5px"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>Klik peta untuk menambah titik</span>
<button class="btn btn-xs" style="color:#fff; background:rgba(255,255,255,0.15); border:1px solid rgba(255,255,255,0.5); transition:all 0.15s;" onmouseover="this.style.background='#fff'; this.style.color='var(--blue)'" onmouseout="this.style.background='rgba(255,255,255,0.15)'; this.style.color='#fff'" onclick="cancelAddMode()">✕ Batal</button>
</div>
<!-- Map controls (top-right) — hidden when radius-editor is open -->
<div id="map-controls">
<!-- Radius global -->
<?php if(!$is_readonly): ?>
<div class="ctrl-card">
<h4><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle;margin-right:4px"><circle cx="12" cy="12" r="10"/></svg>Radius Default</h4>
<div class="radius-row">
<span style="font-size:12px;color:var(--text2)">Jarak cakupan:</span>
<span class="radius-val" id="global-radius-val">500 m</span>
</div>
<input type="range" id="global-radius" min="500" max="3000" step="50" value="500">
<button class="btn btn-outline btn-xs" style="width:100%;margin-top:6px" onclick="applyGlobalRadius()">Terapkan ke Semua</button>
</div>
<?php endif ?>
<!-- Legend -->
<div class="ctrl-card">
<h4><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle;margin-right:4px"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>Legenda</h4>
<div class="legend-row"><div class="leg-dot" style="background:#2563eb"></div>Masjid</div>
<div class="legend-row"><div class="leg-dot" style="background:#7c3aed"></div>Gereja</div>
<div class="legend-row"><div class="leg-dot" style="background:#d97706"></div>Pura</div>
<div class="legend-row"><div class="leg-dot" style="background:#db2777"></div>Vihara / Kelenteng</div>
<div class="legend-row"><div class="leg-dot" style="background:#22c55e"></div>Penduduk — Terdaftar</div>
<div class="legend-row"><div class="leg-dot" style="background:#ef4444"></div>Penduduk — Belum</div>
<div class="legend-row"><div class="leg-ring" style="border-color:rgba(37,99,235,.6)"></div>Radius Ibadah</div>
</div>
<!-- Filter -->
<div class="ctrl-card">
<h4><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle;margin-right:4px"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>Tampilkan</h4>
<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;margin-bottom:5px">
<input type="checkbox" id="show-ibadah" checked onchange="refreshMap()"> Rumah Ibadah
</label>
<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;margin-bottom:5px">
<input type="checkbox" id="show-penduduk" checked onchange="refreshMap()"> Penduduk Miskin
</label>
<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer">
<input type="checkbox" id="show-radius" checked onchange="refreshMap()"> Lingkaran Radius
</label>
</div>
</div>
<!-- Radius inline editor (replaces map-controls when circle is clicked) -->
<?php if (!$is_readonly): ?>
<div id="radius-editor">
<h4><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle;margin-right:4px"><circle cx="12" cy="12" r="10"/></svg>Edit Radius</h4>
<div class="re-name" id="re-name">—</div>
<div class="re-row">
<span style="font-size:12px;color:var(--text2)">Radius:</span>
<span class="re-val" id="re-val">500 m</span>
</div>
<input type="range" id="re-slider" min="500" max="3000" step="50" value="500"
oninput="onReSlider(this.value)">
<div class="re-hint">Min. 500 m · Geser untuk ubah ukuran lingkaran</div>
<div class="re-actions">
<button class="btn btn-outline btn-sm" style="flex:1" onclick="cancelRadiusEdit()">✕ Batal</button>
<button class="btn btn-primary btn-sm" style="flex:1" onclick="saveRadiusEdit()">✔ Simpan</button>
</div>
</div>
<?php endif; ?>
<!-- Data panel (right drawer) -->
<div id="data-panel">
<div class="dp-head">
<h3 id="dp-title">Data</h3>
<button class="dp-close" onclick="toggleDataPanel()">✕</button>
</div>
<div class="dp-tabs">
<div class="dp-tab active" id="dptab-ibadah" onclick="switchDPTab('ibadah')"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px"><path d="M3 21h18M5 21V7l7-4 7 4v14"/><path d="M9 21v-6h6v6"/></svg>Ibadah</div>
<div class="dp-tab" id="dptab-penduduk" onclick="switchDPTab('penduduk')"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>Penduduk</div>
</div>
<!-- ── SEARCH di data panel ── -->
<div class="dp-search">
<input type="text" id="dp-q" placeholder="Cari di daftar ini…" oninput="renderDP()">
</div>
<div class="dp-body" id="dp-body"></div>
</div>
</div><!-- /map-wrap -->
</div><!-- /main -->
</div><!-- /app -->
<!-- ═══════ MODAL RUMAH IBADAH ═══════ -->
<div class="overlay" id="modal-ibadah">
<div class="modal">
<div class="modal-hd">
<div class="modal-ico blue"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 21h18M5 21V7l7-4 7 4v14"/><path d="M9 21v-6h6v6"/></svg></div>
<div>
<h3 id="mi-title">Tambah Rumah Ibadah</h3>
<p>Isi data rumah ibadah dengan lengkap</p>
</div>
<button class="modal-x" onclick="closeModal('ibadah')">✕</button>
</div>
<div class="modal-body">
<div class="info-box" id="mi-info" style="display:none">
<span class="ii"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg></span>
<span>Klik lokasi di peta, form akan otomatis terisi koordinat dan alamat.</span>
</div>
<input type="hidden" id="mi-id">
<input type="hidden" id="mi-lat">
<input type="hidden" id="mi-lng">
<div class="fg">
<label class="fl">Koordinat <span class="req">*</span></label>
<div class="fcoords" id="mi-coords">
<span class="fcoords-lbl">Lat, Lng:</span>
<span id="mi-coords-txt" style="flex:1">— klik peta —</span>
<button class="btn btn-outline btn-xs" onclick="pickCoord('ibadah')"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>Pilih di Peta</button>
</div>
</div>
<div class="fg">
<label class="fl">Nama Rumah Ibadah <span class="req">*</span></label>
<input class="fi" type="text" id="mi-nama" placeholder="Contoh: Masjid Al-Ikhlas">
</div>
<div class="fg2">
<div class="fg">
<label class="fl">Jenis <span class="req">*</span></label>
<select class="fs" id="mi-jenis">
<option>Masjid</option><option>Gereja</option><option>Pura</option>
<option>Vihara</option><option>Kelenteng</option><option>Lainnya</option>
</select>
</div>
<div class="fg">
<label class="fl">Radius Cakupan (m) <span style="font-size:10px;color:var(--text3)">min 500</span></label>
<div class="radius-form-row">
<input type="range" id="mi-radius" min="500" max="3000" step="50" value="500" oninput="document.getElementById('mi-radius-val').textContent=this.value+' m'" style="flex:1">
<span class="radius-form-val" id="mi-radius-val">500 m</span>
</div>
</div>
</div>
<div class="fg">
<label class="fl">Alamat <span style="font-size:10px;color:var(--text3)">(otomatis dari koordinat)</span></label>
<textarea class="fta" id="mi-alamat" rows="2" placeholder="Akan terisi otomatis via reverse geocoding…"></textarea>
</div>
<div class="fg2">
<div class="fg">
<label class="fl">PIC (Penanggung Jawab)</label>
<input class="fi" type="text" id="mi-pic" placeholder="Nama PIC">
</div>
<div class="fg">
<label class="fl">No. WhatsApp</label>
<input class="fi" type="text" id="mi-wa" placeholder="08xxxxxxxxxx">
</div>
</div>
</div>
<div class="modal-ft">
<button class="btn btn-outline" onclick="closeModal('ibadah')">Batal</button>
<button class="btn btn-primary" id="mi-save" onclick="saveIbadah()"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>Simpan</button>
</div>
</div>
</div>
<!-- ═══════ MODAL PENDUDUK MISKIN ═══════ -->
<div class="overlay" id="modal-penduduk">
<div class="modal">
<div class="modal-hd">
<div class="modal-ico green"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/></svg></div>
<div>
<h3 id="mp-title">Tambah Penduduk Miskin</h3>
<p>Isi data kepala keluarga</p>
</div>
<button class="modal-x" onclick="closeModal('penduduk')">✕</button>
</div>
<div class="modal-body">
<div class="info-box" id="mp-info" style="display:none">
<span class="ii"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg></span>
<span>Klik lokasi di peta, koordinat dan alamat akan terisi otomatis.</span>
</div>
<input type="hidden" id="mp-id">
<input type="hidden" id="mp-lat">
<input type="hidden" id="mp-lng">
<input type="hidden" id="mp-foto-lama">
<div class="fg">
<label class="fl">Koordinat <span class="req">*</span></label>
<div class="fcoords" id="mp-coords">
<span class="fcoords-lbl">Lat, Lng:</span>
<span id="mp-coords-txt" style="flex:1">— klik peta —</span>
<button class="btn btn-outline btn-xs" onclick="pickCoord('penduduk')"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>Pilih di Peta</button>
</div>
</div>
<div class="fg">
<label class="fl">Nama Kepala Keluarga <span class="req">*</span></label>
<input class="fi" type="text" id="mp-nama" placeholder="Nama lengkap KK">
</div>
<div class="fg">
<label class="fl">Jumlah Anggota KK</label>
<input class="fi" type="number" id="mp-jml" min="1" value="1" placeholder="Jumlah jiwa">
</div>
<div class="fg">
<label class="fl">Alamat <span style="font-size:10px;color:var(--text3)">(otomatis dari koordinat)</span></label>
<textarea class="fta" id="mp-alamat" rows="2" placeholder="Akan terisi otomatis via reverse geocoding…"></textarea>
</div>
<div class="fg2">
<div class="fg">
<label class="fl">Jenis Bantuan</label>
<select class="fi" id="mp-jenis-bantuan">
<option value="">-- Pilih Bantuan --</option>
<option value="BPH">BPH (Bantuan Pangan Harapan)</option>
<option value="BPNT">BPNT (Bantuan Pangan Non Tunai)</option>
<option value="PKH">PKH (Program Keluarga Harapan)</option>
<option value="BLT">BLT (Bantuan Langsung Tunai)</option>
<option value="BST">BST (Bantuan Sosial Tunai)</option>
<option value="Bantuan Sembako">Bantuan Sembako</option>
<option value="KIP">KIP (Kartu Indonesia Pintar)</option>
<option value="KIS">KIS (Kartu Indonesia Sehat)</option>
<option value="Lainnya">Lainnya</option>
</select>
</div>
<div class="fg">
<label class="fl">Tanggal Bantuan</label>
<input class="fi" type="date" id="mp-tanggal-bantuan">
</div>
</div>
<!-- Field Foto Rumah -->
<div class="fg">
<label class="fl" style="display:flex;align-items:center;gap:4px"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> Foto Rumah</label>
<input class="fi" type="file" id="mp-foto" accept="image/jpeg,image/png,image/webp">
<div id="mp-foto-preview" style="margin-top:6px;display:none">
<img id="mp-foto-img" src="" style="max-width:100%;max-height:150px;border-radius:8px;object-fit:cover;cursor:zoom-in;" title="Klik untuk perbesar" onclick="bukaLightboxFoto(this.src)">
</div>
</div>
<div class="fg" id="mp-ibadah-info" style="display:none">
<label class="fl">Rumah Ibadah Terdekat</label>
<div class="fcoords" style="color:var(--green)">
<span><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 21h18M5 21V7l7-4 7 4v14"/><path d="M9 21v-6h6v6"/></svg></span>
<span id="mp-ibadah-txt">—</span>
</div>
</div>
</div>
<div class="modal-ft">
<button class="btn btn-outline" onclick="closeModal('penduduk')">Batal</button>
<button class="btn btn-success" id="mp-save" onclick="savePenduduk()"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>Simpan</button>
</div>
</div>
</div>
<!-- ═══════ MODAL RIWAYAT BANTUAN ═══════ -->
<div class="overlay" id="modal-bantuan-history">
<div class="modal" style="max-width: 500px;">
<div class="modal-hd">
<div class="modal-ico green">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
</div>
<div>
<h3 id="mbh-title">Riwayat Bantuan</h3>
<p id="mbh-subtitle">Nama KK: -</p>
</div>
<button class="modal-x" onclick="closeModal('bantuan-history')">✕</button>
</div>
<div class="modal-body" style="padding-top:10px">
<!-- Form Input Bantuan Baru -->
<div id="mbh-form" style="background:#f8fafc; padding:16px; border-radius:8px; margin-bottom:20px; border: 1px solid var(--border)">
<h4 style="font-size:12px; font-weight:700; margin-bottom:12px; text-transform:uppercase; color:var(--text2); display:flex; align-items:center; gap:6px">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg> Input Bantuan Baru
</h4>
<input type="hidden" id="mbh-penduduk-id">
<div class="fg2" style="display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:12px">
<div class="fg" style="margin-bottom:0">
<label class="fl" style="font-size:10px; font-weight:600; text-transform:uppercase; color:var(--text3); margin-bottom:4px; display:block">Jenis Bantuan</label>
<select class="fi" id="mbh-jenis" style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:6px; font-size:13px">
<option value="BPH">BPH (Bantuan Pangan Harapan)</option>
<option value="BPNT">BPNT (Bantuan Pangan Non Tunai)</option>
<option value="PKH">PKH (Program Keluarga Harapan)</option>
<option value="BLT">BLT (Bantuan Langsung Tunai)</option>
<option value="BST">BST (Bantuan Sosial Tunai)</option>
<option value="Bantuan Sembako">Bantuan Sembako</option>
<option value="KIP">KIP (Kartu Indonesia Pintar)</option>
<option value="KIS">KIS (Kartu Indonesia Sehat)</option>
<option value="Lainnya">Lainnya</option>
</select>
</div>
<div class="fg" style="margin-bottom:0">
<label class="fl" style="font-size:10px; font-weight:600; text-transform:uppercase; color:var(--text3); margin-bottom:4px; display:block">Tanggal Diberikan</label>
<input class="fi" type="date" id="mbh-tanggal" value="<?= date('Y-m-d') ?>" style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:6px; font-size:13px">
</div>
</div>
<div class="fg" style="margin-bottom:12px">
<label class="fl" style="font-size:10px; font-weight:600; text-transform:uppercase; color:var(--text3); margin-bottom:4px; display:block">Pemberi Bantuan (Rumah Ibadah)</label>
<select class="fi" id="mbh-ibadah-pemberi" style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:6px; font-size:13px">
<option value="">-- Pilih Rumah Ibadah (Jika dari Rumah Ibadah) --</option>
</select>
</div>
<div class="fg" style="margin-bottom:12px">
<label class="fl" style="font-size:10px; font-weight:600; text-transform:uppercase; color:var(--text3); margin-bottom:4px; display:block">Keterangan / Catatan</label>
<input class="fi" type="text" id="mbh-keterangan" placeholder="Contoh: Tahap I, Sembako Bulanan" style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:6px; font-size:13px">
</div>
<button class="btn btn-sm" onclick="submitBantuanBaru()" style="width:100%; background:var(--green); color:white; border:none; padding:10px; border-radius:6px; font-weight:600; cursor:pointer; font-size:13px; display:flex; align-items:center; justify-content:center; gap:6px">💾 Tambah Riwayat Bantuan</button>
</div>
<!-- List Riwayat Bantuan -->
<h4 style="font-size:12px; font-weight:700; margin-bottom:10px; text-transform:uppercase; color:var(--text2)">Daftar Bantuan Diterima</h4>
<div id="mbh-list-wrap" style="max-height: 220px; overflow-y: auto; border:1px solid var(--border); border-radius:8px; background:#fff">
<table style="width:100%; border-collapse:collapse; font-size:12px; text-align:left">
<thead>
<tr style="background:#f1f5f9; border-bottom:1px solid var(--border)">
<th style="padding:8px 12px; font-weight:600; color:var(--text2)">Jenis</th>
<th style="padding:8px 12px; font-weight:600; color:var(--text2)">Tanggal</th>
<th style="padding:8px 12px; font-weight:600; color:var(--text2)">Keterangan</th>
<th style="padding:8px 12px; font-weight:600; color:var(--text2); text-align:center">Aksi</th>
</tr>
</thead>
<tbody id="mbh-tbody">
<tr>
<td colspan="4" style="padding:20px; text-align:center; color:var(--text3)">Memuat data...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Toast container -->
<div id="toasts"></div>
<!-- Loading -->
<div id="loading" style="display:none">
<div class="spinner"></div>
<p id="loading-txt">Memuat data…</p>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const READONLY = <?= $is_readonly ? 'true' : 'false' ?>;
/* ══════════════════════════════════════════
STATE
══════════════════════════════════════════ */
var allIbadah = [], allPenduduk = [];
var ibadahLayers = {}, pendudukLayers = {}, radiusLayers = {};
var addMode = null; // 'ibadah' | 'penduduk'
var dpTab = 'ibadah';
var dpOpen = false;
var sidebarOpen = true;
// Drag state
var dragState = null;
/* dragState = {
type: 'penduduk'|'ibadah',
id: number,
originalLat: number,
originalLng: number,
newLat: number,
newLng: number,
marker: Leaflet marker
}
*/
// Radius editor state
var reState = null;
/* reState = { ibadahId, originalRadius, currentRadius } */
/* ══════════════════════════════════════════
MAP INIT
══════════════════════════════════════════ */
var map = L.map('map', { zoomControl: true, maxZoom: 19 }).setView([-0.0263, 109.3425], 14);
var baseMap = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://www.openstreetmap.org/">OpenStreetMap</a>',
maxZoom: 19
}).addTo(map);
var baseSat = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '© Esri', maxZoom: 19
});
// Satellite toggle in map-controls
(function(){
var ctrl = document.getElementById('map-controls');
if(ctrl){
var card = document.createElement('div');
card.className = 'ctrl-card';
card.innerHTML = '<h4><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle;margin-right:4px"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>Peta Dasar</h4>'+
'<div style="display:flex;gap:5px;margin-top:6px">'+
'<button type="button" class="base-sw active" id="sw-map" style="flex:1;padding:6px;border:1px solid var(--border);border-radius:7px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;background:var(--blue);color:#fff;border-color:var(--blue)">Peta</button>'+
'<button type="button" class="base-sw" id="sw-sat" style="flex:1;padding:6px;border:1px solid var(--border);border-radius:7px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;background:#fff;color:var(--text2)">Satelit</button>'+
'</div>';
ctrl.appendChild(card);
document.getElementById('sw-map').onclick = function(){
if(map.hasLayer(baseSat)) map.removeLayer(baseSat);
if(!map.hasLayer(baseMap)) baseMap.addTo(map);
this.style.background='var(--blue)';this.style.color='#fff';this.style.borderColor='var(--blue)';
document.getElementById('sw-sat').style.background='#fff';document.getElementById('sw-sat').style.color='var(--text2)';document.getElementById('sw-sat').style.borderColor='var(--border)';
};
document.getElementById('sw-sat').onclick = function(){
if(map.hasLayer(baseMap)) map.removeLayer(baseMap);
if(!map.hasLayer(baseSat)) baseSat.addTo(map);
this.style.background='var(--blue)';this.style.color='#fff';this.style.borderColor='var(--blue)';
document.getElementById('sw-map').style.background='#fff';document.getElementById('sw-map').style.color='var(--text2)';document.getElementById('sw-map').style.borderColor='var(--border)';
};
}
})();
// Custom panes
map.createPane('pendudukPane');
map.getPane('pendudukPane').style.zIndex = 450;
map.createPane('ibadahPane');
map.getPane('ibadahPane').style.zIndex = 440;
/* Map click handler */
map.on('click', function(e) {
if (!addMode) return;
var lat = e.latlng.lat, lng = e.latlng.lng;
if (addMode === 'ibadah') {
document.getElementById('mi-lat').value = lat;
document.getElementById('mi-lng').value = lng;
document.getElementById('mi-coords-txt').textContent = lat.toFixed(6) + ', ' + lng.toFixed(6);
reverseGeocode(lat, lng, 'mi-alamat');
exitPickMode();
openModal('ibadah');
} else if (addMode === 'penduduk') {
document.getElementById('mp-lat').value = lat;
document.getElementById('mp-lng').value = lng;
document.getElementById('mp-coords-txt').textContent = lat.toFixed(6) + ', ' + lng.toFixed(6);
reverseGeocode(lat, lng, 'mp-alamat');
exitPickMode();
openModal('penduduk');
}
});
/* ══════════════════════════════════════════
JENIS → COLOR / ICON
══════════════════════════════════════════ */
var JENIS_COLOR = {
'Masjid': '#2563eb', 'Gereja': '#7c3aed', 'Pura': '#d97706',
'Vihara': '#db2777', 'Kelenteng': '#db2777', 'Lainnya': '#64748b'
};
var JENIS_ICON = {
'Masjid': '🕌', 'Gereja': '⛪', 'Pura': '🛕',
'Vihara': '☯', 'Kelenteng': '🏯', 'Lainnya': '🏛'
};
function ibadahColor(jenis) { return JENIS_COLOR[jenis] || '#64748b'; }
function ibadahIcon(jenis) { return JENIS_ICON[jenis] || '🏛'; }
// FIXED: icon size NEVER changes — only color/style, anchor is always fixed
// Changing iconSize/iconAnchor during drag causes the marker to jump
function makeIbadahIcon(jenis) {
var c = ibadahColor(jenis);
return L.divIcon({
html: '<div style="width:32px;height:32px;border-radius:50%;background:'+c+';border:3px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.25);display:flex;align-items:center;justify-content:center;font-size:14px;">'+ibadahIcon(jenis)+'</div>',
className: '', iconAnchor: [16,16], iconSize: [32,32]
});
}
function makePendudukIcon(terdaftar) {
var c = terdaftar ? '#22c55e' : '#ef4444';
var shadow = terdaftar ? 'rgba(34,197,94,.5)' : 'rgba(239,68,68,.5)';
return L.divIcon({
html: '<div style="width:14px;height:14px;border-radius:50%;background:'+c+';border:2px solid #fff;box-shadow:0 1px 6px '+shadow+';"></div>',
className: '', iconAnchor: [7,7], iconSize: [14,14]
});
}
// Invisible drag handle icon for circle center — fixed size, never changes
function makeCircleDragIcon() {
return L.divIcon({
html: '<div style="width:22px;height:22px;border-radius:50%;background:rgba(255,255,255,0.01);border:2px dashed rgba(100,100,100,0.4);cursor:move;box-sizing:border-box;"></div>',
className: '', iconAnchor: [11,11], iconSize: [22,22]
});
}
/* ══════════════════════════════════════════
HAVERSINE (client-side for live preview)
══════════════════════════════════════════ */
function haversineJS(lat1,lng1,lat2,lng2){
var R=6371000;
var p1=lat1*Math.PI/180, p2=lat2*Math.PI/180;
var dp=(lat2-lat1)*Math.PI/180, dl=(lng2-lng1)*Math.PI/180;
var a=Math.sin(dp/2)**2+Math.cos(p1)*Math.cos(p2)*Math.sin(dl/2)**2;
return R*2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));
}
/* Check if penduduk point (lat,lng) is covered by any ibadah */
function isCoveredAt(lat, lng) {
return allIbadah.some(ib => haversineJS(lat, lng, parseFloat(ib.lat), parseFloat(ib.lng)) <= parseInt(ib.radius));
}
/* ══════════════════════════════════════════
LOAD DATA
══════════════════════════════════════════ */
function loadData() {
showLoading('Memuat data…');
fetch('index.php?action=get_all')
.then(r => r.json())
.then(d => {
allIbadah = d.rumah_ibadah || [];
allPenduduk = d.penduduk || [];
updateStats(d.stats || {});
refreshMap();
renderDP();
hideLoading();
})
.catch(() => { hideLoading(); toast('Gagal memuat data!', 'error'); });
}
function updateStats(s) {
document.getElementById('s-ibadah').textContent = s.total_ibadah || 0;
document.getElementById('s-penduduk').textContent = s.total_penduduk || 0;
document.getElementById('s-terdaftar').textContent = s.terdaftar || 0;
document.getElementById('s-belum').textContent = s.belum || 0;
document.getElementById('nb-ibadah').textContent = s.total_ibadah || 0;
document.getElementById('nb-penduduk').textContent = s.total_penduduk || 0;
}
/* ══════════════════════════════════════════
MAP RENDERING
══════════════════════════════════════════ */
function clearMapLayers() {
Object.values(ibadahLayers).forEach(l => map.removeLayer(l));
Object.values(pendudukLayers).forEach(l => map.removeLayer(l));
Object.values(radiusLayers).forEach(l => map.removeLayer(l));
// Also clear circle drag handles
if (window.circleDragHandles) {
Object.values(window.circleDragHandles).forEach(l => map.removeLayer(l));
window.circleDragHandles = {};
}
ibadahLayers = {}; pendudukLayers = {}; radiusLayers = {};
}
function refreshMap() {
clearMapLayers();
var showI = document.getElementById('show-ibadah').checked;
var showP = document.getElementById('show-penduduk').checked;
var showR = document.getElementById('show-radius').checked;
// ── CIRCLES — draggable directly + penduduk live color update ──
if (!window.circleDragHandles) window.circleDragHandles = {};
allIbadah.forEach(function(ib) {
var c = ibadahColor(ib.jenis);
var ibRadius = parseInt(ib.radius) || 500;
// Helper: recompute and update ALL penduduk dot colors based on a
// temporary ibadah center (used during live drag preview)
function updatePendudukColors(tempIbLat, tempIbLng) {
allPenduduk.forEach(function(p) {
var marker = pendudukLayers[p.id];
if (!marker) return;
var el = marker.getElement();
if (!el) return;
var dot = el.querySelector('div');
if (!dot) return;
// Check coverage against ALL ibadah, but use temp position for THIS one
var covered = allIbadah.some(function(other) {
var oLat = (other.id == ib.id) ? tempIbLat : parseFloat(other.lat);
var oLng = (other.id == ib.id) ? tempIbLng : parseFloat(other.lng);
return haversineJS(parseFloat(p.lat), parseFloat(p.lng), oLat, oLng) <= parseInt(other.radius);
});
dot.style.background = covered ? '#22c55e' : '#ef4444';
});
}
if (showR) {
var circle = L.circle([ib.lat, ib.lng], {
radius: ibRadius,
color: c, fillColor: c, fillOpacity: 0.06,
weight: 2, dashArray: '6 4', opacity: 0.6,
interactive: !READONLY
}).addTo(map);
if (!READONLY) {
// Click on circle border/fill: open radius editor (unless addMode or mid-drag)
circle.on('click', function(e) {
if (addMode) return;
if (dragState) return;
L.DomEvent.stopPropagation(e);
openRadiusEditor(ib.id);
});
circle.on('mouseover', function() {
if (!reState && !dragState && !addMode) {
circle.setStyle({ weight: 3, opacity: 0.9, fillOpacity: 0.12 });
map.getContainer().style.cursor = 'grab';
}
});
circle.on('mouseout', function() {
if (!reState || reState.ibadahId != ib.id) circle.setStyle({ weight: 2, opacity: 0.6, fillOpacity: 0.06 });
if (!addMode && !dragState) map.getContainer().style.cursor = '';
});
// ── CIRCLE DRAG via mousedown ─────────────────────────────
circle.on('mousedown', function(e) {
if (addMode) return;
if (reState) cancelRadiusEdit();
map.closePopup();
L.DomEvent.stopPropagation(e);
L.DomEvent.preventDefault(e);
map.dragging.disable();
map.getContainer().style.cursor = 'grabbing';
var isDragging = false;
var startLat = parseFloat(ib.lat);
var startLng = parseFloat(ib.lng);
var curLat = startLat, curLng = startLng;
// Use native DOM events on document — more reliable than Leaflet map events
function onMouseMove(ev) {
isDragging = true;
// map.mouseEventToLatLng needs a native MouseEvent
var pos = map.mouseEventToLatLng(ev);
curLat = pos.lat;
curLng = pos.lng;
circle.setLatLng([curLat, curLng]);
if (ibadahLayers[ib.id]) ibadahLayers[ib.id].setLatLng([curLat, curLng]);
if (window.circleDragHandles && window.circleDragHandles[ib.id]) {
window.circleDragHandles[ib.id].setLatLng([curLat, curLng]);
}
updatePendudukColors(curLat, curLng);
}
function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
map.dragging.enable();
map.getContainer().style.cursor = '';
if (!isDragging) return;
dragState = {
type: 'ibadah',
id: ib.id,
originalLat: startLat,
originalLng: startLng,
newLat: curLat,
newLng: curLng,
circle: circle,
circleDragHandle: window.circleDragHandles ? window.circleDragHandles[ib.id] : null,
marker: ibadahLayers[ib.id],
jenis: ib.jenis,
nama: ib.nama
};
showDragConfirm('ibadah', ib.nama, curLat, curLng);
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
}
radiusLayers[ib.id] = circle;
}
// ── Invisible center handle (for dragging via the center dot) ──
var circleDragHandle = L.marker([ib.lat, ib.lng], {
icon: makeCircleDragIcon(),
pane: 'ibadahPane',
draggable: !READONLY,
zIndexOffset: -100
}).addTo(map);
circleDragHandle.on('mouseover', function() {
if (!dragState && !addMode) map.getContainer().style.cursor = 'move';
});
circleDragHandle.on('mouseout', function() {
if (!addMode && !dragState) map.getContainer().style.cursor = '';
});
circleDragHandle.on('dragstart', function() {
map.closePopup();
if (reState) cancelRadiusEdit();
});
circleDragHandle.on('drag', function(e) {
var pos = e.latlng;
if (radiusLayers[ib.id]) radiusLayers[ib.id].setLatLng(pos);
if (ibadahLayers[ib.id]) ibadahLayers[ib.id].setLatLng(pos);
// Live penduduk color update
allPenduduk.forEach(function(p) {
var m = pendudukLayers[p.id]; if (!m) return;
var el = m.getElement(); if (!el) return;
var dot = el.querySelector('div'); if (!dot) return;
var covered = allIbadah.some(function(other) {
var oLat = (other.id == ib.id) ? pos.lat : parseFloat(other.lat);
var oLng = (other.id == ib.id) ? pos.lng : parseFloat(other.lng);
return haversineJS(parseFloat(p.lat), parseFloat(p.lng), oLat, oLng) <= parseInt(other.radius);
});
dot.style.background = covered ? '#22c55e' : '#ef4444';
});
});
circleDragHandle.on('dragend', function(e) {
var newPos = e.target.getLatLng();
dragState = {
type: 'ibadah',
id: ib.id,
originalLat: parseFloat(ib.lat),
originalLng: parseFloat(ib.lng),
newLat: newPos.lat,
newLng: newPos.lng,
circle: radiusLayers[ib.id],
circleDragHandle: circleDragHandle,
marker: ibadahLayers[ib.id],
jenis: ib.jenis,
nama: ib.nama
};
showDragConfirm('ibadah', ib.nama, newPos.lat, newPos.lng);
});
window.circleDragHandles[ib.id] = circleDragHandle;
});
// ── IBADAH MARKERS — draggable, icon NEVER resized ─────────
if (showI) {
allIbadah.forEach(function(ib) {
var marker = L.marker([ib.lat, ib.lng], {
icon: makeIbadahIcon(ib.jenis),
pane: 'ibadahPane',
draggable: !READONLY,
zIndexOffset: 100
}).addTo(map);
marker.bindPopup(makeIbadahPopup(ib), { maxWidth: 260, minWidth: 220 });
marker.on('dragstart', function() {
map.closePopup();
if (reState) cancelRadiusEdit();
// Style via element opacity — no icon resize so marker stays still
var el = marker.getElement();
if (el) el.style.opacity = '0.75';
});
// Move circle live while dragging the ibadah marker + update penduduk colors
marker.on('drag', function(e) {
var pos = e.latlng;
if (radiusLayers[ib.id]) radiusLayers[ib.id].setLatLng(pos);
if (window.circleDragHandles && window.circleDragHandles[ib.id]) {
window.circleDragHandles[ib.id].setLatLng(pos);
}
// Live penduduk color update
allPenduduk.forEach(function(p) {
var m = pendudukLayers[p.id]; if (!m) return;
var el = m.getElement(); if (!el) return;
var dot = el.querySelector('div'); if (!dot) return;
var covered = allIbadah.some(function(other) {
var oLat = (other.id == ib.id) ? pos.lat : parseFloat(other.lat);
var oLng = (other.id == ib.id) ? pos.lng : parseFloat(other.lng);
return haversineJS(parseFloat(p.lat), parseFloat(p.lng), oLat, oLng) <= parseInt(other.radius);
});
dot.style.background = covered ? '#22c55e' : '#ef4444';
});
});
marker.on('dragend', function(e) {
var el = marker.getElement();
if (el) el.style.opacity = '';
var newPos = e.target.getLatLng();
dragState = {
type: 'ibadah',
id: ib.id,
originalLat: parseFloat(ib.lat),
originalLng: parseFloat(ib.lng),
newLat: newPos.lat,
newLng: newPos.lng,
marker: marker,
circleDragHandle: window.circleDragHandles ? window.circleDragHandles[ib.id] : null,
jenis: ib.jenis,
nama: ib.nama
};
showDragConfirm('ibadah', ib.nama, newPos.lat, newPos.lng);
});
ibadahLayers[ib.id] = marker;
});
}
// ── PENDUDUK MARKERS — draggable, icon NEVER resized ───────
if (showP) {
allPenduduk.forEach(function(p) {
var terdaftar = !!p.rumah_ibadah_id;
var marker = L.marker([p.lat, p.lng], {
icon: makePendudukIcon(terdaftar),
pane: 'pendudukPane',
draggable: !READONLY
}).addTo(map);
marker.bindPopup(makePendudukPopup(p), { maxWidth: 260, minWidth: 220 });
marker.on('dragstart', function() {
map.closePopup();
var el = marker.getElement();
if (el) el.style.opacity = '0.7';
});
// While dragging: update icon color via DOM (no setIcon = no jump)
marker.on('drag', function(e) {
var pos = e.latlng;
var covered = isCoveredAt(pos.lat, pos.lng);
var el = marker.getElement();
if (el) {
var dot = el.querySelector('div');
if (dot) dot.style.background = covered ? '#22c55e' : '#ef4444';
}
});
marker.on('dragend', function(e) {
var el = marker.getElement();
if (el) el.style.opacity = '';
var newPos = e.target.getLatLng();
var coveredAtNew = isCoveredAt(newPos.lat, newPos.lng);
// Update final color via DOM
if (el) {
var dot = el.querySelector('div');
if (dot) dot.style.background = coveredAtNew ? '#22c55e' : '#ef4444';
}
dragState = {
type: 'penduduk',
id: p.id,
originalLat: parseFloat(p.lat),
originalLng: parseFloat(p.lng),
newLat: newPos.lat,
newLng: newPos.lng,
marker: marker,
nama: p.nama_kk,
coveredAtNew: coveredAtNew
};
showDragConfirm('penduduk', p.nama_kk, newPos.lat, newPos.lng, coveredAtNew);
});
pendudukLayers[p.id] = marker;
});
}
}
/* ══════════════════════════════════════════
DRAG CONFIRM
══════════════════════════════════════════ */
function showDragConfirm(type, nama, lat, lng, coveredAtNew) {
var label = type === 'ibadah' ? 'rumah ibadah' : 'penduduk';
showLoading('Mendeteksi alamat baru…');
fetch('https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat='+lat+'&lon='+lng+'&accept-language=id&zoom=18')
.then(r => r.json())
.then(d => {
hideLoading();
var alamat = d.display_name || 'Alamat tidak ditemukan';
var msg = '<div style="text-align:left;line-height:1.6">' +
'<b>' + nama + '</b><br>' +
'<span style="color:var(--text3);font-size:12px">Koordinat: ' + lat.toFixed(6) + ', ' + lng.toFixed(6) + '</span><br>' +
'<span style="font-size:13px;color:var(--text2)"><b>Alamat:</b> ' + alamat + '</span>';
if (type === 'penduduk') {
msg += coveredAtNew
? '<br><span style="color:var(--green);font-size:12px;font-weight:600;margin-top:6px;display:inline-block">✅ Lokasi baru masuk dalam radius ibadah.</span>'
: '<br><span style="color:var(--red);font-size:12px;font-weight:600;margin-top:6px;display:inline-block">❌ Lokasi baru di luar radius semua ibadah.</span>';
}
msg += '</div>';
showConfirm({
title: 'Pindahkan ' + label + '?',
message: msg,
confirmText: 'Ya, Pindahkan',
cancelText: 'Batal',
icon: 'move',
position: 'top'
}).then(function(ok) {
if (ok) confirmDrag();
else cancelDrag();
});
})
.catch(() => {
hideLoading();
var msg = '<div style="text-align:left;line-height:1.6">' +
'<b>' + nama + '</b><br>' +
'<span style="color:var(--text3);font-size:12px">Koordinat: ' + lat.toFixed(6) + ', ' + lng.toFixed(6) + '</span><br>' +
'<span style="font-size:13px;color:var(--text2)"><b>Alamat:</b> Gagal memuat alamat</span>';
if (type === 'penduduk') {
msg += coveredAtNew
? '<br><span style="color:var(--green);font-size:12px;font-weight:600;margin-top:6px;display:inline-block">✅ Lokasi baru masuk dalam radius ibadah.</span>'
: '<br><span style="color:var(--red);font-size:12px;font-weight:600;margin-top:6px;display:inline-block">❌ Lokasi baru di luar radius semua ibadah.</span>';
}
msg += '</div>';
showConfirm({
title: 'Pindahkan ' + label + '?',
message: msg,
confirmText: 'Ya, Pindahkan',
cancelText: 'Batal',
icon: 'move',
position: 'top'
}).then(function(ok) {
if (ok) confirmDrag();
else cancelDrag();
});
});
}
function confirmDrag() {
if (!dragState) return;
var ds = dragState;
dragState = null;
var payload;
if (ds.type === 'penduduk') {
var p = allPenduduk.find(x => x.id == ds.id);
if (!p) return;
payload = { id: ds.id, nama_kk: p.nama_kk, jumlah_anggota: p.jumlah_anggota, alamat: p.alamat || '', lat: ds.newLat, lng: ds.newLng };
} else {
var ib = allIbadah.find(x => x.id == ds.id);
if (!ib) return;
payload = { id: ds.id, nama: ib.nama, jenis: ib.jenis, alamat: ib.alamat || '', pic: ib.pic || '', no_wa: ib.no_wa || '', lat: ds.newLat, lng: ds.newLng, radius: ib.radius };
}
var action = ds.type === 'penduduk' ? 'edit_penduduk' : 'edit_ibadah';
showLoading('Menyimpan posisi baru…');
fetch('index.php?action=' + action, {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
})
.then(r => r.json())
.then(d => {
hideLoading();
if (d.error) { toast(d.error, 'error'); loadData(); return; }
toast('Posisi berhasil diperbarui!', 'success');
loadData();
})
.catch(() => { hideLoading(); toast('Koneksi gagal!', 'error'); loadData(); });
}
function cancelDrag() {
if (!dragState) return;
var ds = dragState;
dragState = null;
if (ds.type === 'ibadah') {
// Restore all ibadah visual positions
if (ds.marker) ds.marker.setLatLng([ds.originalLat, ds.originalLng]);
if (ds.circle) ds.circle.setLatLng([ds.originalLat, ds.originalLng]);
if (ds.circleDragHandle) ds.circleDragHandle.setLatLng([ds.originalLat, ds.originalLng]);
// Also restore by id in case only one ref was stored
if (ibadahLayers[ds.id]) ibadahLayers[ds.id].setLatLng([ds.originalLat, ds.originalLng]);
if (radiusLayers[ds.id]) radiusLayers[ds.id].setLatLng([ds.originalLat, ds.originalLng]);
if (window.circleDragHandles && window.circleDragHandles[ds.id]) {
window.circleDragHandles[ds.id].setLatLng([ds.originalLat, ds.originalLng]);
}
// Restore penduduk colors back to actual DB state
allPenduduk.forEach(function(p) {
var m = pendudukLayers[p.id]; if (!m) return;
var el = m.getElement(); if (!el) return;
var dot = el.querySelector('div'); if (!dot) return;
dot.style.background = p.rumah_ibadah_id ? '#22c55e' : '#ef4444';
});
} else {
// Restore penduduk marker position and color
if (ds.marker) {
ds.marker.setLatLng([ds.originalLat, ds.originalLng]);
var el = ds.marker.getElement();
if (el) el.style.opacity = '';
var p = allPenduduk.find(function(x) { return x.id == ds.id; });
if (el) {
var dot = el.querySelector('div');
if (dot) dot.style.background = (p && p.rumah_ibadah_id) ? '#22c55e' : '#ef4444';
}
}
}
toast('Pemindahan dibatalkan.', 'warn');
}
/* ══════════════════════════════════════════
RADIUS INLINE EDITOR
══════════════════════════════════════════ */
function openRadiusEditor(ibadahId) {
if (READONLY) return;
var ib = allIbadah.find(x => x.id == ibadahId);
if (!ib) return;
reState = { ibadahId: ibadahId, originalRadius: parseInt(ib.radius) || 500, currentRadius: parseInt(ib.radius) || 500 };
document.getElementById('re-name').textContent = ib.nama;
document.getElementById('re-slider').value = reState.currentRadius;
document.getElementById('re-val').textContent = reState.currentRadius + ' m';
// Highlight circle
if (radiusLayers[ibadahId]) {
var c = ibadahColor(ib.jenis);
radiusLayers[ibadahId].setStyle({ weight: 3, opacity: 0.9, fillOpacity: 0.12, color: c });
}
// Show editor, hide main controls
document.getElementById('map-controls').style.display = 'none';
document.getElementById('radius-editor').classList.add('show');
}
function onReSlider(val) {
val = Math.max(500, parseInt(val));
document.getElementById('re-val').textContent = val + ' m';
if (reState && radiusLayers[reState.ibadahId]) {
radiusLayers[reState.ibadahId].setRadius(val);
reState.currentRadius = val;
}
}
function saveRadiusEdit() {
if (!reState) return;
var ib = allIbadah.find(x => x.id == reState.ibadahId);
if (!ib) return;
var newRadius = reState.currentRadius;
var payload = { id: ib.id, nama: ib.nama, jenis: ib.jenis, alamat: ib.alamat || '', pic: ib.pic || '', no_wa: ib.no_wa || '', lat: parseFloat(ib.lat), lng: parseFloat(ib.lng), radius: newRadius };
closeRadiusEditor();
showLoading('Menyimpan radius…');
fetch('index.php?action=edit_ibadah', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
})
.then(r => r.json())
.then(d => {
hideLoading();
if (d.error) { toast(d.error, 'error'); }
else { toast('Radius diperbarui ke ' + newRadius + ' m!', 'success'); }
loadData();
})
.catch(() => { hideLoading(); toast('Koneksi gagal!', 'error'); loadData(); });
}
function cancelRadiusEdit() {
if (!reState) return;
// Restore original radius visually
if (radiusLayers[reState.ibadahId]) {
var ib = allIbadah.find(x => x.id == reState.ibadahId);
radiusLayers[reState.ibadahId].setRadius(reState.originalRadius);
if (ib) radiusLayers[reState.ibadahId].setStyle({ weight: 2, opacity: 0.6, fillOpacity: 0.06 });
}
closeRadiusEditor();
}
function closeRadiusEditor() {
reState = null;
document.getElementById('radius-editor').classList.remove('show');
document.getElementById('map-controls').style.display = '';
}
/* ══════════════════════════════════════════
POPUP HTML
══════════════════════════════════════════ */
function esc(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function makeIbadahPopup(ib) {
var c = ibadahColor(ib.jenis);
var pd = allPenduduk.filter(p => p.rumah_ibadah_id == ib.id);
return '<div class="pc">'+
'<div class="pc-head"><span class="pc-ico">'+ibadahIcon(ib.jenis)+'</span>'+
'<div><div class="pc-name">'+esc(ib.nama)+'</div>'+
'<span class="pc-badge" style="background:'+c+'22;color:'+c+'">'+esc(ib.jenis)+'</span></div></div>'+
'<div class="pc-row"><b>Alamat</b>'+esc(ib.alamat||'—')+'</div>'+
(ib.pic?'<div class="pc-row"><b>PIC</b>'+esc(ib.pic)+'</div>':'')+
(ib.no_wa?'<div class="pc-row"><b>WA</b><a href="https://wa.me/'+ib.no_wa.replace(/\D/g,'')+'" target="_blank" style="color:var(--green)">'+esc(ib.no_wa)+'</a></div>':'')+
'<div class="pc-row"><b>Radius</b>'+(ib.radius||500)+' m'+(READONLY?'':' · <span style="color:var(--text3);font-size:11px">klik lingkaran untuk edit</span>')+'</div>'+
'<div class="pc-row"><b>Cakupan</b><span style="color:var(--green);font-weight:600">'+pd.length+' KK terdaftar</span></div>'+
'<div class="pc-actions">'+
(READONLY?'':('<div class="pca" onclick="editIbadah('+ib.id+');map.closePopup()"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:3px"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>Edit</div>'))+
(READONLY?'':('<div class="pca" onclick="openRadiusEditor('+ib.id+');map.closePopup()">⭕ Radius</div>'))+
(READONLY?'':('<div class="pca del" onclick="deleteIbadah('+ib.id+');map.closePopup()"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:3px"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>Hapus</div>'))+
'</div></div>';
}
function makePendudukPopup(p) {
var terdaftar = !!p.rumah_ibadah_id;
var statusColor = terdaftar ? 'var(--green)' : 'var(--red)';
var statusTxt = terdaftar ? '✅ Terdaftar — '+esc(p.nama_ibadah||'') : '❌ Belum Terdaftar';
return '<div class="pc">'+
'<div class="pc-head"><span class="pc-ico"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/></svg></span>'+
'<div><div class="pc-name">'+esc(p.nama_kk)+'</div>' +
(p.jenis_bantuan ? '<div class="pc-badge" style="background:#dcfce7;color:#16a34a">Bantuan: '+esc(p.jenis_bantuan)+' ('+(p.tanggal_bantuan?p.tanggal_bantuan:'-')+')</div>' : '') +
'<span class="pc-badge" style="background:#fef9c3;color:#a16207">'+p.jumlah_anggota+' jiwa</span></div></div>'+
'<div class="pc-row"><b>Alamat</b>'+esc(p.alamat||'—')+'</div>'+
'<div class="pc-status" style="color:'+statusColor+'">'+statusTxt+
(p.jarak_ke_ibadah?'<span style="margin-left:auto;color:var(--text3);font-weight:400">'+Math.round(p.jarak_ke_ibadah)+' m</span>':'')+
'</div>'+
'<div class="pc-actions">'+
(READONLY?'':('<div class="pca" onclick="editPenduduk('+p.id+');map.closePopup()"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:3px"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>Edit</div>'))+
('<div class="pca" style="background:#eff6ff;color:#1d4ed8" onclick="openBantuanHistory('+p.id+');map.closePopup()">🎁 Bantuan</div>')+
(READONLY?'':('<div class="pca del" onclick="deletePenduduk('+p.id+');map.closePopup()"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:3px"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>Hapus</div>'))+
'</div></div>';
}
/* ══════════════════════════════════════════
ADD MODE (klik peta)
══════════════════════════════════════════ */
function startAddMode(type) {
showView('peta');
addMode = type;
document.getElementById('add-banner').style.display = 'flex';
document.getElementById('banner-txt').textContent =
type === 'ibadah' ? 'Klik di peta untuk menambah Rumah Ibadah' : 'Klik di peta untuk menambah Penduduk Miskin';
map.getContainer().style.cursor = 'crosshair';
}
function pickCoord(type) {
document.getElementById('modal-'+type).classList.remove('show');
startAddMode(type);
}
function exitPickMode() {
addMode = null;
document.getElementById('add-banner').style.display = 'none';
map.getContainer().style.cursor = '';
}
function cancelAddMode() {
exitPickMode();
closeModal('ibadah');
closeModal('penduduk');
}
/* ══════════════════════════════════════════
REVERSE GEOCODING
══════════════════════════════════════════ */
function reverseGeocode(lat, lng, targetId) {
var el = document.getElementById(targetId);
if (!el) return;
el.value = 'Mencari alamat…';
fetch('https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat='+lat+'&lon='+lng+'&accept-language=id&zoom=18')
.then(r => r.json())
.then(d => {
var a = d.address || {};
var parts = [
a.road || a.residential || a.pedestrian || '',
a.neighbourhood || a.suburb || '',
a.city_district || a.quarter || '',
a.city || a.town || a.village || '',
a.county || '',
a.state || '',
].filter(Boolean);
el.value = parts.join(', ') || d.display_name || '';
})
.catch(() => { el.value = 'Gagal mendapatkan alamat'; });
}
/* ══════════════════════════════════════════
MODAL IBADAH
══════════════════════════════════════════ */
function openModal(type) {
document.getElementById('modal-'+type).classList.add('show');
}
function closeModal(type) {
document.getElementById('modal-'+type).classList.remove('show');
exitPickMode();
}
function resetIbadahForm() {
['mi-id','mi-lat','mi-lng','mi-nama','mi-pic','mi-wa','mi-alamat'].forEach(id => {
document.getElementById(id).value = '';
});
document.getElementById('mi-coords-txt').textContent = '— klik peta —';
document.getElementById('mi-jenis').value = 'Masjid';
document.getElementById('mi-radius').value = 500;
document.getElementById('mi-radius-val').textContent = '500 m';
document.getElementById('mi-title').textContent = 'Tambah Rumah Ibadah';
document.getElementById('mi-info').style.display = 'block';
}
function editIbadah(id) {
var ib = allIbadah.find(x => x.id == id);
if (!ib) return;
resetIbadahForm();
document.getElementById('mi-id').value = ib.id;
document.getElementById('mi-lat').value = ib.lat;
document.getElementById('mi-lng').value = ib.lng;
document.getElementById('mi-coords-txt').textContent = parseFloat(ib.lat).toFixed(6)+', '+parseFloat(ib.lng).toFixed(6);
document.getElementById('mi-nama').value = ib.nama;
document.getElementById('mi-jenis').value = ib.jenis;
document.getElementById('mi-alamat').value = ib.alamat || '';
document.getElementById('mi-pic').value = ib.pic || '';
document.getElementById('mi-wa').value = ib.no_wa || '';
var r = Math.max(500, parseInt(ib.radius) || 500);
document.getElementById('mi-radius').value = r;
document.getElementById('mi-radius-val').textContent = r+' m';
document.getElementById('mi-title').textContent = 'Edit Rumah Ibadah';
document.getElementById('mi-info').style.display = 'none';
openModal('ibadah');
}
function saveIbadah() {
var lat = parseFloat(document.getElementById('mi-lat').value);
var lng = parseFloat(document.getElementById('mi-lng').value);
var nama = document.getElementById('mi-nama').value.trim();
if (!nama || !lat || !lng) { toast('Nama dan koordinat wajib diisi!', 'warn'); return; }
var id = document.getElementById('mi-id').value;
var radius = Math.max(500, parseInt(document.getElementById('mi-radius').value) || 500);
var payload = {
nama: nama, jenis: document.getElementById('mi-jenis').value,
alamat: document.getElementById('mi-alamat').value,
pic: document.getElementById('mi-pic').value,
no_wa: document.getElementById('mi-wa').value,
lat: lat, lng: lng, radius: radius
};
if (id) payload.id = parseInt(id);
var action = id ? 'edit_ibadah' : 'add_ibadah';
var btn = document.getElementById('mi-save');
btn.disabled = true; btn.textContent = '⏳ Menyimpan…';
fetch('index.php?action='+action, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(payload) })
.then(r => r.json())
.then(d => {
btn.disabled = false; btn.textContent = 'Simpan';
if (d.error) { toast(d.error, 'error'); return; }
toast(id ? 'Rumah ibadah diperbarui!' : 'Rumah ibadah berhasil ditambahkan!', 'success');
closeModal('ibadah');
loadData();
})
.catch(() => { btn.disabled = false; btn.textContent = 'Simpan'; toast('Koneksi gagal!', 'error'); });
}
async function deleteIbadah(id) {
var ok = await showConfirm({
title: 'Hapus Rumah Ibadah',
message: 'Data penduduk yang terdaftar di lokasi ini akan terputus. Lanjutkan?',
confirmText: 'Ya, Hapus',
danger: true
});
if (!ok) return;
showLoading('Menghapus…');
fetch('index.php?action=delete_ibadah', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({id:id}) })
.then(r => r.json())
.then(d => { hideLoading(); if (d.success) { toast('Data dihapus.', 'success'); loadData(); } else toast(d.error, 'error'); })
.catch(() => { hideLoading(); toast('Koneksi gagal!', 'error'); });
}
/* ══════════════════════════════════════════
MODAL PENDUDUK
══════════════════════════════════════════ */
function resetPendudukForm() {
['mp-id','mp-lat','mp-lng','mp-nama','mp-alamat','mp-jenis-bantuan','mp-tanggal-bantuan'].forEach(id => document.getElementById(id).value = '');
document.getElementById('mp-jml').value = 1;
document.getElementById('mp-coords-txt').textContent = '— klik peta —';
document.getElementById('mp-title').textContent = 'Tambah Penduduk Miskin';
document.getElementById('mp-info').style.display = 'block';
document.getElementById('mp-ibadah-info').style.display = 'none';
document.getElementById('mp-foto').value = '';
document.getElementById('mp-foto-preview').style.display = 'none';
}
document.getElementById('mp-foto').addEventListener('change', function() {
var preview = document.getElementById('mp-foto-preview');
var img = document.getElementById('mp-foto-img');
var file = this.files[0];
if (file) {
img.src = URL.createObjectURL(file);
preview.style.display = 'block';
EXIF.getData(file, function() {
var lat = EXIF.getTag(this, "GPSLatitude");
var lon = EXIF.getTag(this, "GPSLongitude");
var latRef = EXIF.getTag(this, "GPSLatitudeRef") || "N";
var lonRef = EXIF.getTag(this, "GPSLongitudeRef") || "E";
if (lat && lon) {
function parseDms(dms) {
if (!dms || dms.length < 3) return null;
var d = (typeof dms[0] === 'object' && dms[0].denominator) ? dms[0].numerator / dms[0].denominator : Number(dms[0]);
var m = (typeof dms[1] === 'object' && dms[1].denominator) ? dms[1].numerator / dms[1].denominator : Number(dms[1]);
var s = (typeof dms[2] === 'object' && dms[2].denominator) ? dms[2].numerator / dms[2].denominator : Number(dms[2]);
return d + (m / 60) + (s / 3600);
}
var latDec = parseDms(lat);
var lonDec = parseDms(lon);
if (latDec !== null && lonDec !== null && !isNaN(latDec) && !isNaN(lonDec)) {
if (latRef === "S") latDec = -latDec;
if (lonRef === "W") lonDec = -lonDec;
document.getElementById('mp-lat').value = latDec;
document.getElementById('mp-lng').value = lonDec;
document.getElementById('mp-coords-txt').textContent = latDec.toFixed(6) + ', ' + lonDec.toFixed(6);
toast('Geotagging terdeteksi dari foto!', 'success');
fetch('https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=' + latDec + '&lon=' + lonDec + '&zoom=18&accept-language=id')
.then(r => r.json())
.then(geo => {
var alamat = geo.display_name || '';
if (alamat) document.getElementById('mp-alamat').value = alamat;
});
}
}
});
} else {
preview.style.display = 'none';
}
});
function editPenduduk(id) {
var p = allPenduduk.find(x => x.id == id);
if (!p) return;
resetPendudukForm();
document.getElementById('mp-id').value = p.id;
document.getElementById('mp-lat').value = p.lat;
document.getElementById('mp-lng').value = p.lng;
document.getElementById('mp-coords-txt').textContent = parseFloat(p.lat).toFixed(6)+', '+parseFloat(p.lng).toFixed(6);
document.getElementById('mp-nama').value = p.nama_kk;
document.getElementById('mp-jml').value = p.jumlah_anggota;
document.getElementById('mp-alamat').value = p.alamat || '';
document.getElementById('mp-jenis-bantuan').value = p.jenis_bantuan || '';
document.getElementById('mp-tanggal-bantuan').value = p.tanggal_bantuan || '';
// Tambahkan setelah baris 1914
var fotoPreview = document.getElementById('mp-foto-preview');
var fotoImg = document.getElementById('mp-foto-img');
var fotoLama = document.getElementById('mp-foto-lama');
if (p.foto_rumah) {
fotoImg.src = p.foto_rumah;
fotoPreview.style.display = 'block';
fotoLama.value = p.foto_rumah;
} else {
fotoPreview.style.display = 'none';
fotoLama.value = '';
}
document.getElementById('mp-title').textContent = 'Edit Penduduk Miskin';
document.getElementById('mp-info').style.display = 'none';
if (p.nama_ibadah) {
document.getElementById('mp-ibadah-info').style.display = 'block';
document.getElementById('mp-ibadah-txt').textContent = p.nama_ibadah + (p.jarak_ke_ibadah?' ('+Math.round(p.jarak_ke_ibadah)+' m)':'');
}
openModal('penduduk');
}
function savePenduduk() {
var lat = parseFloat(document.getElementById('mp-lat').value);
var lng = parseFloat(document.getElementById('mp-lng').value);
var nama = document.getElementById('mp-nama').value.trim();
if (!nama || !lat || !lng) { toast('Nama dan koordinat wajib diisi!', 'warn'); return; }
var id = document.getElementById('mp-id').value;
var fd = new FormData();
fd.append('nama_kk', nama);
fd.append('jumlah_anggota', document.getElementById('mp-jml').value || 1);
fd.append('alamat', document.getElementById('mp-alamat').value);
fd.append('lat', lat); fd.append('lng', lng);
fd.append('jenis_bantuan', document.getElementById('mp-jenis-bantuan').value);
fd.append('tanggal_bantuan', document.getElementById('mp-tanggal-bantuan').value);
if (id) fd.append('id', id);
var fotoFile = document.getElementById('mp-foto').files[0];
if (fotoFile) fd.append('foto', fotoFile);
var action = id ? 'edit_penduduk' : 'add_penduduk';
var btn = document.getElementById('mp-save');
btn.disabled = true; btn.textContent = '⏳ Menyimpan…';
fetch('index.php?action='+action, { method: 'POST', body: fd })
.then(r => r.json())
.then(d => {
btn.disabled = false; btn.textContent = 'Simpan';
if (d.error) { toast(d.error, 'error'); return; }
toast(id ? 'Data penduduk diperbarui!' : 'Penduduk berhasil ditambahkan!', 'success');
if (d.data && d.data.nama_ibadah) toast('Terdaftar di: '+d.data.nama_ibadah, 'success');
closeModal('penduduk');
loadData();
})
.catch(() => { btn.disabled = false; btn.textContent = 'Simpan'; toast('Koneksi gagal!', 'error'); });
}
async function deletePenduduk(id) {
var ok = await showConfirm({
title: 'Hapus Data Penduduk',
message: 'Data penduduk ini akan dihapus permanen. Lanjutkan?',
confirmText: 'Ya, Hapus',
danger: true
});
if (!ok) return;
showLoading('Menghapus…');
fetch('index.php?action=delete_penduduk', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({id:id}) })
.then(r => r.json())
.then(d => { hideLoading(); if (d.success) { toast('Data dihapus.', 'success'); loadData(); } else toast(d.error, 'error'); })
.catch(() => { hideLoading(); toast('Koneksi gagal!', 'error'); });
}
/* ══════════════════════════════════════════
RIWAYAT BANTUAN SYSTEM
══════════════════════════════════════════ */
function openBantuanHistory(id) {
var p = allPenduduk.find(x => x.id == id);
if (!p) return;
document.getElementById('mbh-penduduk-id').value = id;
document.getElementById('mbh-subtitle').textContent = 'Nama KK: ' + p.nama_kk;
document.getElementById('mbh-jenis').value = 'BPH';
document.getElementById('mbh-keterangan').value = '';
// Populate Rumah Ibadah select dropdown
var ibSelect = document.getElementById('mbh-ibadah-pemberi');
ibSelect.innerHTML = '<option value="">-- Pilih Rumah Ibadah (Jika dari Rumah Ibadah) --</option>';
allIbadah.forEach(ib => {
var opt = document.createElement('option');
opt.value = ib.id;
opt.textContent = ib.nama + ' (' + ib.jenis + ')';
ibSelect.appendChild(opt);
});
if (p.rumah_ibadah_id) {
ibSelect.value = p.rumah_ibadah_id;
}
// Set default date to today
var today = new Date().toISOString().split('T')[0];
document.getElementById('mbh-tanggal').value = today;
// Load current history
loadBantuanHistoryList(id);
openModal('bantuan-history');
}
function loadBantuanHistoryList(id) {
var tbody = document.getElementById('mbh-tbody');
tbody.innerHTML = '<tr><td colspan="4" style="padding:20px; text-align:center; color:var(--text3)">⏳ Memuat data...</td></tr>';
fetch('index.php?action=get_bantuan&penduduk_id=' + id)
.then(r => r.json())
.then(data => {
tbody.innerHTML = '';
if (!data || data.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" style="padding:20px; text-align:center; color:var(--text3)">️ Belum ada riwayat bantuan</td></tr>';
return;
}
data.forEach(item => {
var tr = document.createElement('tr');
tr.style.borderBottom = '1px solid var(--border)';
var dateFormatted = item.tanggal_pemberian.split('-').reverse().join('/');
// Disable delete for read-only users
var actionHtml = READONLY ? '<span style="color:var(--text3)">-</span>' :
`<button onclick="deleteBantuan(${item.id}, ${id})" style="background:none; border:none; color:var(--red); cursor:pointer; font-weight:600; font-size:12px; display:inline-flex; align-items:center; gap:2px">🗑️ Hapus</button>`;
var infoPemberi = item.nama_ibadah ? `<br><span style="font-size:11px; color:var(--blue); font-weight:600">⛪ ${esc(item.nama_ibadah)}</span>` : '';
tr.innerHTML = `
<td style="padding:10px 12px; font-weight:600">${esc(item.jenis_bantuan)}${infoPemberi}</td>
<td style="padding:10px 12px; font-family:monospace">${dateFormatted}</td>
<td style="padding:10px 12px; color:var(--text2)">${esc(item.keterangan || '-')}</td>
<td style="padding:10px 12px; text-align:center">${actionHtml}</td>
`;
tbody.appendChild(tr);
});
})
.catch(() => {
tbody.innerHTML = '<tr><td colspan="4" style="padding:20px; text-align:center; color:var(--red)">⚠️ Gagal memuat data</td></tr>';
});
}
function submitBantuanBaru() {
if (READONLY) { toast('Anda tidak memiliki akses.', 'error'); return; }
var id = parseInt(document.getElementById('mbh-penduduk-id').value);
var jenis = document.getElementById('mbh-jenis').value;
var tanggal = document.getElementById('mbh-tanggal').value;
var ket = document.getElementById('mbh-keterangan').value.trim();
var riId = document.getElementById('mbh-ibadah-pemberi').value;
if (!id || !jenis || !tanggal) {
toast('Data tidak lengkap!', 'warn');
return;
}
var payload = {
penduduk_id: id,
jenis_bantuan: jenis,
tanggal_pemberian: tanggal,
keterangan: ket,
rumah_ibadah_id: riId
};
fetch('index.php?action=add_bantuan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(r => r.json())
.then(d => {
if (d.error) {
toast(d.error, 'error');
} else {
toast('Bantuan berhasil dicatat!', 'success');
document.getElementById('mbh-keterangan').value = '';
document.getElementById('mbh-ibadah-pemberi').value = '';
loadBantuanHistoryList(id);
loadData(); // Reload map data to reflect new assistance status
}
})
.catch(() => {
toast('Koneksi gagal!', 'error');
});
}
async function deleteBantuan(bantuanId, pendudukId) {
if (READONLY) { toast('Anda tidak memiliki akses.', 'error'); return; }
var ok = await showConfirm({
title: 'Hapus Catatan Bantuan',
message: 'Catatan pembagian bantuan ini akan dihapus dari riwayat. Lanjutkan?',
confirmText: 'Ya, Hapus',
danger: true
});
if (!ok) return;
fetch('index.php?action=delete_bantuan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: bantuanId })
})
.then(r => r.json())
.then(d => {
if (d.success) {
toast('Catatan bantuan dihapus!', 'success');
loadBantuanHistoryList(pendudukId);
loadData(); // Reload map data to reflect updated assistance status
} else {
toast(d.error, 'error');
}
})
.catch(() => {
toast('Koneksi gagal!', 'error');
});
}
/* ══════════════════════════════════════════
GLOBAL RADIUS
══════════════════════════════════════════ */
var el = document.getElementById('global-radius');
if(el) {
el.addEventListener('input', function() {
document.getElementById('global-radius-val').textContent = this.value + ' m';
});
}
async function applyGlobalRadius() {
var r = Math.max(500, parseInt(document.getElementById('global-radius').value));
if (!allIbadah.length) { toast('Belum ada data rumah ibadah.', 'warn'); return; }
var ok = await showConfirm({
title: 'Terapkan Radius Global',
message: 'Terapkan radius '+r+' m ke semua ('+allIbadah.length+') rumah ibadah?'
});
if (!ok) return;
showLoading('Memperbarui radius…');
var promises = allIbadah.map(ib => {
var payload = Object.assign({}, ib, { radius: r });
return fetch('index.php?action=edit_ibadah', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify(payload)
}).then(r2 => r2.json());
});
Promise.all(promises).then(() => {
hideLoading(); toast('Radius semua ibadah diperbarui!', 'success'); loadData();
}).catch(() => { hideLoading(); toast('Sebagian gagal!', 'error'); });
}
/* ══════════════════════════════════════════
DATA PANEL
══════════════════════════════════════════ */
function toggleDataPanel() {
dpOpen = !dpOpen;
document.getElementById('data-panel').classList.toggle('open', dpOpen);
renderDP();
}
function switchDPTab(tab) {
dpTab = tab;
document.getElementById('dptab-ibadah').classList.toggle('active', tab==='ibadah');
document.getElementById('dptab-penduduk').classList.toggle('active', tab==='penduduk');
var dpq = document.getElementById('dp-q'); if (dpq) dpq.value = '';
renderDP();
}
function renderDP() {
var body = document.getElementById('dp-body');
if (!dpOpen) return;
var q = ((document.getElementById('dp-q') || {}).value || '').toLowerCase().trim();
if (dpTab === 'ibadah') {
var list = q
? allIbadah.filter(ib => (ib.nama||'').toLowerCase().includes(q)||(ib.alamat||'').toLowerCase().includes(q)||(ib.jenis||'').toLowerCase().includes(q)||(ib.pic||'').toLowerCase().includes(q))
: allIbadah;
document.getElementById('dp-title').textContent = 'Rumah Ibadah ('+(q?list.length+' hasil':allIbadah.length)+')';
if (!list.length) { body.innerHTML = '<div class="empty"><div class="ei">'+(q?'🔍':'🕌')+'</div><p>'+(q?'Tidak ada hasil untuk "'+esc(q)+'"':'Belum ada data')+'</p></div>'; return; }
body.innerHTML = list.map(ib => {
var c = ibadahColor(ib.jenis);
var cnt = allPenduduk.filter(p => p.rumah_ibadah_id == ib.id).length;
return '<div class="dc" onclick="flyToIbadah('+ib.id+')">'+
'<div class="dc-top">'+
'<div class="dc-ico" style="background:'+c+'22;color:'+c+'">'+ibadahIcon(ib.jenis)+'</div>'+
'<div><div class="dc-name">'+esc(ib.nama)+'</div><div class="dc-sub">'+esc(ib.jenis)+' · r='+ib.radius+'m</div></div>'+
'<div class="dc-acts">'+
(READONLY?'':('<button class="btn btn-outline btn-xs" onclick="event.stopPropagation();editIbadah('+ib.id+')"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg></button>'))+
(READONLY?'':('<button class="btn btn-danger btn-xs" onclick="event.stopPropagation();deleteIbadah('+ib.id+')"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg></button>'))+
'</div></div>'+
'<div class="dc-tags">'+
'<span class="tag tag-blue">'+cnt+' KK terdaftar</span>'+
(ib.pic?'<span class="tag tag-gray">PIC: '+esc(ib.pic)+'</span>':'')+
'</div></div>';
}).join('');
} else {
var list2 = q
? allPenduduk.filter(p => (p.nama_kk||'').toLowerCase().includes(q)||(p.alamat||'').toLowerCase().includes(q)||(p.nama_ibadah||'').toLowerCase().includes(q))
: allPenduduk;
document.getElementById('dp-title').textContent = 'Penduduk Miskin ('+(q?list2.length+' hasil':allPenduduk.length)+')';
if (!list2.length) { body.innerHTML = '<div class="empty"><div class="ei">'+(q?'🔍':'👨‍👩‍👧')+'</div><p>'+(q?'Tidak ada hasil untuk "'+esc(q)+'"':'Belum ada data')+'</p></div>'; return; }
body.innerHTML = list2.map(p => {
var terdaftar = !!p.rumah_ibadah_id;
return '<div class="dc" onclick="flyToPenduduk('+p.id+')">'+
'<div class="dc-top">'+
'<div class="dc-ico" style="background:'+(terdaftar?'#dcfce7':'#fee2e2')+'"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/></svg></div>'+
'<div><div class="dc-name">'+esc(p.nama_kk)+'</div><div class="dc-sub">'+p.jumlah_anggota+' jiwa</div></div>'+
'<div class="dc-acts" style="display:flex;gap:4px">'+
(READONLY?'':('<button class="btn btn-outline btn-xs" onclick="event.stopPropagation();editPenduduk('+p.id+')" title="Edit"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg></button>'))+
('<button class="btn btn-outline btn-xs" style="border-color:#1d4ed8;color:#1d4ed8" onclick="event.stopPropagation();openBantuanHistory('+p.id+')" title="Riwayat Bantuan"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="3" x2="9" y2="21"/></svg></button>')+
(READONLY?'':('<button class="btn btn-danger btn-xs" onclick="event.stopPropagation();deletePenduduk('+p.id+')" title="Hapus"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg></button>'))+
'</div></div>'+
'<div class="dc-tags">'+
(terdaftar
? '<span class="tag tag-green">✅ '+esc(p.nama_ibadah)+'</span><span class="tag tag-gray">'+Math.round(p.jarak_ke_ibadah||0)+' m</span>'
: '<span class="tag tag-red">❌ Belum Terdaftar</span>')+
'</div></div>';
}).join('');
}
}
function flyToIbadah(id) {
var ib = allIbadah.find(x => x.id == id); if (!ib) return;
map.flyTo([ib.lat, ib.lng], 16, {duration: .8});
setTimeout(() => { if (ibadahLayers[id]) ibadahLayers[id].openPopup(); }, 900);
}
function flyToPenduduk(id) {
var p = allPenduduk.find(x => x.id == id); if (!p) return;
map.flyTo([p.lat, p.lng], 17, {duration: .8});
setTimeout(() => { if (pendudukLayers[id]) pendudukLayers[id].openPopup(); }, 900);
}
/* ══════════════════════════════════════════
SEARCH — SIDEBAR GLOBAL
══════════════════════════════════════════ */
function onSbSearch(q) {
var clearBtn = document.getElementById('sb-q-clear');
var resBox = document.getElementById('sb-search-results');
clearBtn.classList.toggle('show', q.length > 0);
if (!q.trim()) { resBox.classList.remove('show'); return; }
var ql = q.toLowerCase();
var hits = [];
allIbadah.forEach(function(ib) {
if ((ib.nama||'').toLowerCase().includes(ql) || (ib.alamat||'').toLowerCase().includes(ql) ||
(ib.jenis||'').toLowerCase().includes(ql) || (ib.pic||'').toLowerCase().includes(ql)) {
hits.push({ type: 'ibadah', data: ib });
}
});
allPenduduk.forEach(function(p) {
if ((p.nama_kk||'').toLowerCase().includes(ql) || (p.alamat||'').toLowerCase().includes(ql) ||
(p.nama_ibadah||'').toLowerCase().includes(ql)) {
hits.push({ type: 'penduduk', data: p });
}
});
if (!hits.length) {
resBox.innerHTML = '<div style="padding:14px 12px;text-align:center;color:var(--text3);font-size:12px">Tidak ada hasil untuk <b>'+esc(q)+'</b></div>';
resBox.classList.add('show');
return;
}
resBox.innerHTML = hits.slice(0, 15).map(function(h) {
if (h.type === 'ibadah') {
var ib = h.data, c = ibadahColor(ib.jenis);
var cnt = allPenduduk.filter(function(p){ return p.rumah_ibadah_id == ib.id; }).length;
return '<div class="sr-item" onclick="flyToAndClose(\'ibadah\','+ib.id+')">' +
'<div class="sr-ico" style="background:'+c+'22;color:'+c+'">'+ibadahIcon(ib.jenis)+'</div>' +
'<div><div class="sr-name">'+esc(ib.nama)+'</div>' +
'<div class="sr-sub">'+esc(ib.jenis)+' · '+cnt+' KK · r='+ib.radius+'m</div></div>' +
'<span class="sr-type tag tag-blue">Ibadah</span></div>';
} else {
var p = h.data, terdaftar = !!p.rumah_ibadah_id;
return '<div class="sr-item" onclick="flyToAndClose(\'penduduk\','+p.id+')">' +
'<div class="sr-ico" style="background:'+(terdaftar?'var(--green-lt)':'var(--red-lt)')+'"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/></svg></div>' +
'<div><div class="sr-name">'+esc(p.nama_kk)+'</div>' +
'<div class="sr-sub">'+p.jumlah_anggota+' jiwa · '+(p.nama_ibadah?esc(p.nama_ibadah):'Belum terdaftar')+'</div></div>' +
'<span class="sr-type tag '+(terdaftar?'tag-green':'tag-red')+'">'+(terdaftar?'Terdaftar':'Belum')+'</span></div>';
}
}).join('');
resBox.classList.add('show');
}
function clearSbSearch() {
document.getElementById('sb-q').value = '';
document.getElementById('sb-q-clear').classList.remove('show');
document.getElementById('sb-search-results').classList.remove('show');
}
function flyToAndClose(type, id) {
clearSbSearch();
if (type === 'ibadah') flyToIbadah(id);
else flyToPenduduk(id);
}
/* ══════════════════════════════════════════
RECALCULATE
══════════════════════════════════════════ */
function doRecalculate() {
showLoading('Menghitung ulang radius…');
fetch('index.php?action=recalculate', { method: 'POST', headers: {'Content-Type':'application/json'}, body: '{}' })
.then(r => r.json())
.then(() => { hideLoading(); toast('Perhitungan selesai!', 'success'); loadData(); })
.catch(() => { hideLoading(); toast('Gagal!', 'error'); });
}
/* ══════════════════════════════════════════
VIEW / SIDEBAR / NAV
══════════════════════════════════════════ */
function showView(v) {
['peta','ibadah','penduduk'].forEach(k => {
document.getElementById('nav-'+k) && document.getElementById('nav-'+k).classList.toggle('active', k===v);
});
if (v === 'ibadah') { switchDPTab('ibadah'); if (!dpOpen) toggleDataPanel(); }
if (v === 'penduduk') { switchDPTab('penduduk'); if (!dpOpen) toggleDataPanel(); }
if (v === 'peta' && dpOpen) { toggleDataPanel(); }
}
function toggleSidebar() {
sidebarOpen = !sidebarOpen;
document.getElementById('sidebar').style.display = sidebarOpen ? '' : 'none';
setTimeout(() => map.invalidateSize(), 310);
}
/* ══════════════════════════════════════════
TOAST
══════════════════════════════════════════ */
function toast(msg, type) {
type = type || 'info';
var icons = { info: '️', success: '✅', error: '❌', warn: '⚠️' };
var t = document.createElement('div');
t.className = 'toast ' + type;
t.innerHTML = '<span>'+icons[type]+'</span><span style="flex:1">'+msg+'</span>';
document.getElementById('toasts').appendChild(t);
setTimeout(() => { t.classList.add('out'); setTimeout(() => t.remove(), 280); }, 3000);
}
/* ══════════════════════════════════════════
LOADING
══════════════════════════════════════════ */
function showLoading(txt) {
document.getElementById('loading-txt').textContent = txt || 'Memuat…';
document.getElementById('loading').style.display = 'flex';
}
function hideLoading() { document.getElementById('loading').style.display = 'none'; }
/* Close modals on overlay click */
document.querySelectorAll('.overlay').forEach(ov => {
ov.addEventListener('click', e => { if (e.target === ov) { ov.classList.remove('show'); exitPickMode(); } });
});
/* Close radius editor if user clicks map (not on circle) */
map.on('click', function() {
if (reState) cancelRadiusEdit();
});
/* Close search dropdown when clicking outside */
document.addEventListener('click', function(e) {
if (!e.target.closest('.sb-search')) clearSbSearch();
});
/* Init */
loadData();
</script>
<!-- ══ LIGHTBOX FOTO RUMAH ══ -->
<div id="lightbox-foto" onclick="tutupLightboxFoto()" style="display:none;position:fixed;inset:0;z-index:99999; background:rgba(0,0,0,0.85); display:none;align-items:center;justify-content:center;">
<button onclick="tutupLightboxFoto()" style="position:fixed;top:18px;right:22px;background:rgba(255,255,255,0.15);border:none;color:#fff;font-size:28px;line-height:1;width:42px;height:42px;border-radius:50%;cursor:pointer;z-index:100000;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);">✕</button>
<img id="lightbox-foto-img" src="" onclick="event.stopPropagation()" style="max-width:90vw;max-height:88vh;border-radius:12px;box-shadow:0 8px 40px rgba(0,0,0,0.6);object-fit:contain;">
</div>
<script>
function bukaLightboxFoto(src) {
var lb = document.getElementById('lightbox-foto');
var img = document.getElementById('lightbox-foto-img');
img.src = src;
lb.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function tutupLightboxFoto() {
document.getElementById('lightbox-foto').style.display = 'none';
document.getElementById('lightbox-foto-img').src = '';
document.body.style.overflow = '';
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') tutupLightboxFoto();
});
</script>
<?php include __DIR__.'/includes/confirm-modal.php'; ?>
</body>
</html>