2711 lines
131 KiB
PHP
2711 lines
131 KiB
PHP
<?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 & 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,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||
|
||
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>
|