1276 lines
68 KiB
HTML
1276 lines
68 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="id">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Sistem GIS — Manajemen Bantuan</title>
|
|
|
|
<!-- Google Fonts -->
|
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
|
<!-- Leaflet CSS & Draw CSS -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
sans: ['Outfit', 'sans-serif'],
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
html, body { height: 100%; font-family: 'Outfit', sans-serif; overflow: hidden; }
|
|
#map {
|
|
height: 100vh;
|
|
width: 100%;
|
|
z-index: 1;
|
|
}
|
|
.custom-popup .leaflet-popup-content-wrapper {
|
|
border-radius: 16px;
|
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
|
border: 1px solid rgba(226, 232, 240, 0.8);
|
|
padding: 4px;
|
|
}
|
|
.custom-popup .leaflet-popup-content {
|
|
margin: 12px;
|
|
font-family: 'Outfit', sans-serif;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-slate-50 text-slate-800 font-sans">
|
|
|
|
<!-- App Shell -->
|
|
<div class="relative w-full h-screen flex overflow-hidden">
|
|
|
|
<!-- Sidebar -->
|
|
<div class="w-[400px] h-full bg-white border-r border-slate-200/80 flex flex-col z-10 shadow-lg relative shrink-0">
|
|
<!-- Header -->
|
|
<div class="p-6 border-b border-slate-100 flex flex-col gap-1.5">
|
|
<div class="flex items-center gap-2">
|
|
<a href="../index.html" class="flex items-center justify-center w-8 h-8 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200 transition">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
|
</a>
|
|
<div class="flex items-center gap-1.5 text-xs font-semibold text-indigo-600 bg-indigo-50 px-2.5 py-1 rounded-full uppercase tracking-wider">
|
|
<span>Informatika UNTAN</span>
|
|
</div>
|
|
</div>
|
|
<h1 class="text-xl font-bold text-slate-800 mt-2 font-black">Manajemen Bantuan</h1>
|
|
<p class="text-xs text-slate-500">Penyaluran bantuan penduduk miskin berbasis wilayah tempat ibadah.</p>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="border-b border-slate-100 flex overflow-x-auto text-xs font-bold scrollbar-none">
|
|
<button onclick="switchTab('ibadah')" id="tabIbadah" class="flex-1 py-3 text-center border-b-2 border-indigo-600 text-indigo-600 transition min-w-[90px]">
|
|
Rumah Ibadah
|
|
</button>
|
|
<button onclick="switchTab('miskin')" id="tabMiskin" class="flex-1 py-3 text-center border-b-2 border-transparent text-slate-500 hover:text-slate-700 transition min-w-[90px]">
|
|
Penduduk Miskin
|
|
</button>
|
|
<button onclick="switchTab('bantuan')" id="tabBantuan" class="flex-1 py-3 text-center border-b-2 border-transparent text-slate-500 hover:text-slate-700 transition min-w-[90px]">
|
|
Status Bantuan
|
|
</button>
|
|
<button onclick="switchTab('verif')" id="tabVerif" class="flex-1 py-3 text-center border-b-2 border-transparent text-slate-500 hover:text-slate-700 transition min-w-[90px]">
|
|
Verifikasi
|
|
</button>
|
|
</div>
|
|
|
|
<!-- List & Search -->
|
|
<div class="flex-1 flex flex-col min-h-0">
|
|
<div class="p-4 border-b border-slate-100 flex gap-2">
|
|
<div class="relative flex-1">
|
|
<input id="searchQuery" type="text" placeholder="Cari..." class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-10 pr-4 py-2 text-xs outline-none focus:border-indigo-500 focus:bg-white focus:ring-2 focus:ring-indigo-100 transition shadow-sm">
|
|
<svg class="absolute left-3.5 top-2.5 text-slate-400" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 1: Rumah Ibadah -->
|
|
<div id="listIbadah" class="flex-1 overflow-y-auto p-4 space-y-3">
|
|
<div class="text-center py-8 text-slate-400 text-sm">Memuat data...</div>
|
|
</div>
|
|
|
|
<!-- Tab 2: Penduduk Miskin -->
|
|
<div id="listMiskin" class="flex-1 overflow-y-auto p-4 space-y-3 hidden">
|
|
<div class="text-center py-8 text-slate-400 text-sm">Memuat data...</div>
|
|
</div>
|
|
|
|
<!-- Tab 3: Status Bantuan -->
|
|
<div id="listBantuan" class="flex-1 overflow-y-auto p-4 space-y-3 hidden flex flex-col min-h-0">
|
|
<div class="space-y-2.5">
|
|
<div>
|
|
<label class="block text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-1">Pilih Rumah Ibadah Wilayah</label>
|
|
<select id="bantuanIbadahFilter" onchange="filterBantuanByIbadah()" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs outline-none focus:border-indigo-500 focus:bg-white transition">
|
|
<option value="">Pilih rumah ibadah...</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-1">Status Bantuan</label>
|
|
<select id="bantuanStatusFilter" onchange="filterBantuanByIbadah()" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs outline-none focus:border-indigo-500 focus:bg-white transition">
|
|
<option value="all">Semua Status</option>
|
|
<option value="belum">Belum Dibantu</option>
|
|
<option value="proses">Dalam Proses</option>
|
|
<option value="sudah">Sudah Dibantu</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div id="bantuanItems" class="flex-1 overflow-y-auto space-y-3 pt-3">
|
|
<div class="text-center py-8 text-slate-400 text-xs">Pilih rumah ibadah untuk memfilter penerima bantuan.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 4: Verifikasi Lapangan -->
|
|
<div id="listVerif" class="flex-1 overflow-y-auto p-4 space-y-3 hidden">
|
|
<div class="text-center py-8 text-slate-400 text-sm">Memuat data...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Floating buttons for adding items -->
|
|
<div class="p-4 bg-slate-50 border-t border-slate-100 flex gap-2">
|
|
<button onclick="activateAddMode('ibadah')" id="addIbadahBtn" class="flex-1 flex items-center justify-center gap-1.5 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-xs font-bold transition shadow-sm">
|
|
<span>+ Rumah Ibadah</span>
|
|
</button>
|
|
<button onclick="activateAddMode('miskin')" id="addMiskinBtn" class="flex-1 flex items-center justify-center gap-1.5 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-bold transition shadow-sm">
|
|
<span>+ Keluarga Miskin</span>
|
|
</button>
|
|
<button onclick="activateAddMode('verif')" id="addVerifBtn" class="flex-1 flex items-center justify-center gap-1.5 py-2 bg-slate-700 hover:bg-slate-800 text-white rounded-lg text-xs font-bold transition shadow-sm">
|
|
<span>+ Verifikasi</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Map Container -->
|
|
<div class="flex-1 h-full relative">
|
|
<div id="map"></div>
|
|
|
|
<!-- Toast notification -->
|
|
<div id="toast" class="absolute top-4 right-4 z-[2000] hidden items-center gap-3 px-4 py-3 bg-slate-900/90 text-white rounded-xl shadow-xl backdrop-blur-md transition transform scale-95 opacity-0">
|
|
<span id="toastMessage" class="text-sm font-medium">Notifikasi</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Input Rumah Ibadah -->
|
|
<div id="ibadahModal" class="fixed inset-0 z-[10000] hidden items-center justify-center bg-slate-900/40 backdrop-blur-sm px-4">
|
|
<div class="w-full max-w-md bg-white rounded-2xl shadow-2xl border border-slate-100 overflow-hidden transform scale-95 opacity-0 transition-all duration-300">
|
|
<div class="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
|
<h3 class="font-bold text-slate-800" id="ibadahModalTitle">Tambah Rumah Ibadah</h3>
|
|
<button onclick="closeIbadahModal()" class="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-slate-200 transition text-slate-500">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
</button>
|
|
</div>
|
|
<div class="p-5 space-y-4">
|
|
<input type="hidden" id="editIbadahId">
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nama Rumah Ibadah</label>
|
|
<input id="ibadahName" type="text" placeholder="Contoh: Masjid Al-Mukhlisin / Gereja Katedral" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 text-sm outline-none focus:border-indigo-500 focus:bg-white transition">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Alamat Lengkap</label>
|
|
<div class="flex gap-2">
|
|
<input id="ibadahAddress" type="text" placeholder="Mencari alamat..." class="flex-1 bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 text-sm outline-none focus:border-indigo-500 focus:bg-white transition">
|
|
<button onclick="reverseGeocodeIbadah()" class="bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs px-3 rounded-xl transition font-semibold">Cari</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nama Pengurus (PIC)</label>
|
|
<input id="ibadahPic" type="text" placeholder="Nama Koordinator" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 text-sm outline-none focus:border-indigo-500 focus:bg-white transition">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Radius Pelayanan (meter)</label>
|
|
<input id="ibadahRadius" type="number" value="1000" min="100" max="5000" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 text-sm outline-none focus:border-indigo-500 focus:bg-white transition">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-[10px] text-slate-400 bg-slate-50 p-2.5 rounded-lg font-mono">
|
|
📍 Koordinat: <span id="ibadahCoords" class="font-bold text-slate-600">0, 0</span>
|
|
</div>
|
|
</div>
|
|
<div class="p-5 border-t border-slate-100 bg-slate-50/50 flex gap-2">
|
|
<button onclick="saveIbadah()" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm py-2.5 px-4 rounded-xl shadow-lg transition">Simpan</button>
|
|
<button onclick="closeIbadahModal()" class="bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Batal</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Input Penduduk Miskin -->
|
|
<div id="miskinModal" class="fixed inset-0 z-[10000] hidden items-center justify-center bg-slate-900/40 backdrop-blur-sm px-4">
|
|
<div class="w-full max-w-md bg-white rounded-2xl shadow-2xl border border-slate-100 overflow-hidden transform scale-95 opacity-0 transition-all duration-300">
|
|
<div class="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
|
<h3 class="font-bold text-slate-800" id="miskinModalTitle">Tambah Penduduk Miskin</h3>
|
|
<button onclick="closeMiskinModal()" class="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-slate-200 transition text-slate-500">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
</button>
|
|
</div>
|
|
<div class="p-5 space-y-4">
|
|
<input type="hidden" id="editMiskinId">
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nama Ketua KK</label>
|
|
<input id="miskinName" type="text" placeholder="Masukkan nama kepala keluarga" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white transition shadow-sm">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Jumlah Anggota Keluarga (Jiwa)</label>
|
|
<input id="miskinCount" type="number" min="1" max="20" value="4" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:border-indigo-500 focus:bg-white transition shadow-sm">
|
|
</div>
|
|
|
|
<div class="text-[10px] text-slate-400 bg-slate-50 p-2.5 rounded-lg space-y-1">
|
|
<div>📍 Koordinat: <span id="miskinCoords" class="font-mono font-bold text-slate-600">0, 0</span></div>
|
|
<div class="text-indigo-600 font-medium">Sistem akan mencocokkan secara otomatis rumah ibadah terdekat beserta jaraknya.</div>
|
|
</div>
|
|
</div>
|
|
<div class="p-5 border-t border-slate-100 bg-slate-50/50 flex gap-2">
|
|
<button onclick="saveMiskin()" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm py-2.5 px-4 rounded-xl shadow-lg transition">Simpan</button>
|
|
<button onclick="closeMiskinModal()" class="bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Batal</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Update Bantuan Status -->
|
|
<div id="bantuanModal" class="fixed inset-0 z-[10000] hidden items-center justify-center bg-slate-900/40 backdrop-blur-sm px-4">
|
|
<div class="w-full max-w-md bg-white rounded-2xl shadow-2xl border border-slate-100 overflow-hidden transform scale-95 opacity-0 transition-all duration-300">
|
|
<div class="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
|
<h3 class="font-bold text-slate-800">Perbarui Status Bantuan</h3>
|
|
<button onclick="closeBantuanModal()" class="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-slate-200 transition text-slate-500">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
</button>
|
|
</div>
|
|
<div class="p-5 space-y-4">
|
|
<input type="hidden" id="bantuanPendudukId">
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5 font-bold">Keluarga Ketua KK</label>
|
|
<input id="bantuanKKName" type="text" readonly class="w-full bg-slate-100 border border-slate-200 rounded-xl px-4 py-2 text-sm text-slate-500 outline-none">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Status Bantuan</label>
|
|
<select id="bantuanStatus" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-sm outline-none focus:border-indigo-500 focus:bg-white transition shadow-sm">
|
|
<option value="belum">Belum Dibantu</option>
|
|
<option value="proses">Dalam Proses</option>
|
|
<option value="sudah">Sudah Dibantu</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Catatan Penyaluran</label>
|
|
<textarea id="bantuanCatatan" placeholder="Masukkan catatan, tanggal penyaluran, nama bansos, dsb..." rows="3" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 text-sm outline-none focus:border-indigo-500 focus:bg-white transition shadow-sm"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="p-5 border-t border-slate-100 bg-slate-50/50 flex gap-2">
|
|
<button onclick="updateBantuanStatus()" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm py-2.5 px-4 rounded-xl shadow-lg transition">Simpan Perubahan</button>
|
|
<button onclick="closeBantuanModal()" class="bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Batal</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Input Verifikasi Lapangan -->
|
|
<div id="verifModal" class="fixed inset-0 z-[10000] hidden items-center justify-center bg-slate-900/40 backdrop-blur-sm px-4">
|
|
<div class="w-full max-w-md bg-white rounded-2xl shadow-2xl border border-slate-100 overflow-hidden transform scale-95 opacity-0 transition-all duration-300">
|
|
<div class="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
|
<h3 class="font-bold text-slate-800" id="verifModalTitle">Lapor Verifikasi Lapangan</h3>
|
|
<button onclick="closeVerifModal()" class="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-slate-200 transition text-slate-500">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
</button>
|
|
</div>
|
|
<div class="p-5 space-y-4">
|
|
<input type="hidden" id="editVerifId">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Jenis Target</label>
|
|
<select id="verifTargetType" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-sm outline-none focus:border-indigo-500 focus:bg-white transition">
|
|
<option value="penduduk_miskin">Keluarga Miskin</option>
|
|
<option value="rumah_ibadah">Rumah Ibadah</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nama Target</label>
|
|
<input id="verifTargetName" type="text" placeholder="Nama target" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 text-sm outline-none focus:border-indigo-500 focus:bg-white transition">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nama Petugas</label>
|
|
<input id="verifOfficerName" type="text" placeholder="Petugas Lapangan" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 text-sm outline-none focus:border-indigo-500 focus:bg-white transition">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Status Verifikasi</label>
|
|
<select id="verifStatus" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-sm outline-none focus:border-indigo-500 focus:bg-white transition">
|
|
<option value="menunggu">Menunggu</option>
|
|
<option value="terverifikasi">Terverifikasi</option>
|
|
<option value="ditolak">Ditolak</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Hasil Temuan</label>
|
|
<textarea id="verifFindings" placeholder="Tulis kondisi aktual temuan lapangan..." rows="2" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 text-sm outline-none focus:border-indigo-500 focus:bg-white transition"></textarea>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Tindak Lanjut</label>
|
|
<textarea id="verifFollowUp" placeholder="Rencana tindak lanjut..." rows="2" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 text-sm outline-none focus:border-indigo-500 focus:bg-white transition"></textarea>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 mb-1.5">Foto Temuan (Opsional)</label>
|
|
<input id="verifPhoto" type="file" accept="image/*" class="w-full border border-slate-200 rounded-xl p-1.5 text-xs bg-slate-50">
|
|
</div>
|
|
|
|
<div class="text-[10px] text-slate-400 bg-slate-50 p-2 rounded-lg font-mono">
|
|
📍 Koordinat: <span id="verifCoords">0, 0</span>
|
|
</div>
|
|
</div>
|
|
<div class="p-5 border-t border-slate-100 bg-slate-50/50 flex gap-2">
|
|
<button onclick="saveVerif()" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm py-2.5 px-4 rounded-xl shadow-lg transition">Simpan</button>
|
|
<button onclick="closeVerifModal()" class="bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold text-sm py-2.5 px-4 rounded-xl transition">Batal</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Leaflet & Draw JS -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
|
|
|
<script>
|
|
// Init Map
|
|
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
|
|
L.control.zoom({ position: 'topright' }).addTo(map);
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors'
|
|
}).addTo(map);
|
|
|
|
// Feature Groups for markers
|
|
const ibadahGroup = L.featureGroup().addTo(map);
|
|
const miskinGroup = L.featureGroup().addTo(map);
|
|
const verifGroup = L.featureGroup().addTo(map);
|
|
const bufferGroup = L.featureGroup().addTo(map);
|
|
|
|
// Leaflet Draw Control (Restricted to Markers)
|
|
const drawControl = new L.Control.Draw({
|
|
draw: {
|
|
marker: true,
|
|
polygon: false,
|
|
polyline: false,
|
|
circle: false,
|
|
circlemarker: false,
|
|
rectangle: false
|
|
},
|
|
edit: false
|
|
});
|
|
map.addControl(drawControl);
|
|
|
|
let activeTab = 'ibadah';
|
|
let addModeType = null; // 'ibadah', 'miskin', 'verif'
|
|
let pendingLatLng = null;
|
|
|
|
let ibadahRecords = [];
|
|
let miskinRecords = [];
|
|
let verifRecords = [];
|
|
|
|
let mapIbadahLayers = {};
|
|
let mapMiskinLayers = {};
|
|
let mapVerifLayers = {};
|
|
|
|
// Load all data
|
|
async function loadAllData() {
|
|
try {
|
|
// Ibadah
|
|
const resIbadah = await fetch('api/get_rumah_ibadah.php');
|
|
ibadahRecords = await resIbadah.json();
|
|
renderIbadahList(ibadahRecords);
|
|
displayIbadahOnMap(ibadahRecords);
|
|
populateIbadahDropdown();
|
|
|
|
// Miskin
|
|
const resMiskin = await fetch('api/get_penduduk_miskin.php');
|
|
miskinRecords = await resMiskin.json();
|
|
renderMiskinList(miskinRecords);
|
|
displayMiskinOnMap(miskinRecords);
|
|
|
|
// Verif
|
|
const resVerif = await fetch('api/get_verifikasi_lapangan.php');
|
|
verifRecords = await resVerif.json();
|
|
renderVerifList(verifRecords);
|
|
displayVerifOnMap(verifRecords);
|
|
} catch (err) {
|
|
console.error(err);
|
|
showToast('Gagal memuat data spasial.', 'error');
|
|
}
|
|
}
|
|
|
|
// Dropdown for Bantuan Filter
|
|
function populateIbadahDropdown() {
|
|
const dropdown = document.getElementById('bantuanIbadahFilter');
|
|
dropdown.innerHTML = '<option value="">Pilih rumah ibadah...</option>' +
|
|
ibadahRecords.map(rec => `<option value="${rec.id}">${rec.nama}</option>`).join('');
|
|
}
|
|
|
|
// Custom DivIcon Worship Places
|
|
function createWorshipIcon() {
|
|
return L.divIcon({
|
|
html: `
|
|
<div class="relative w-8 h-8 flex items-center justify-center rounded-xl bg-white shadow-lg border-2 border-indigo-600 transition transform hover:scale-110">
|
|
<svg class="text-indigo-600 w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
<path d="M12 3 4 8v13h16V8z"/>
|
|
<path d="M9 21v-7h6v7"/>
|
|
</svg>
|
|
</div>`,
|
|
className: 'spbu-marker-icon',
|
|
iconSize: [32, 32],
|
|
iconAnchor: [16, 16]
|
|
});
|
|
}
|
|
|
|
// Custom DivIcon Poor Residents
|
|
function createMiskinIcon(status) {
|
|
const color = status === 'sudah' ? '#10b981' : (status === 'proses' ? '#f59e0b' : '#ef4444');
|
|
return L.divIcon({
|
|
html: `
|
|
<div class="relative w-6 h-6 flex items-center justify-center rounded-full bg-white shadow-lg border border-slate-200 transition transform hover:scale-110">
|
|
<div class="w-3.5 h-3.5 rounded-full flex items-center justify-center" style="background-color: ${color}">
|
|
<svg class="text-white w-2 h-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
|
<circle cx="12" cy="7" r="4"/><path d="M4 21v-2a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v2"/>
|
|
</svg>
|
|
</div>
|
|
</div>`,
|
|
className: 'spbu-marker-icon',
|
|
iconSize: [24, 24],
|
|
iconAnchor: [12, 12]
|
|
});
|
|
}
|
|
|
|
// Custom DivIcon Field Verifications
|
|
function createVerifIcon(status) {
|
|
const color = status === 'terverifikasi' ? '#10b981' : (status === 'ditolak' ? '#dc2626' : '#64748b');
|
|
return L.divIcon({
|
|
html: `
|
|
<div class="relative w-6 h-6 flex items-center justify-center rounded-full bg-white shadow-lg border border-slate-200 transition transform hover:scale-110">
|
|
<div class="w-3.5 h-3.5 rounded-full flex items-center justify-center" style="background-color: ${color}">
|
|
<svg class="text-white w-2 h-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
|
<polyline points="20 6 9 17 4 12"/>
|
|
</svg>
|
|
</div>
|
|
</div>`,
|
|
className: 'spbu-marker-icon',
|
|
iconSize: [24, 24],
|
|
iconAnchor: [12, 12]
|
|
});
|
|
}
|
|
|
|
// Display Rumah Ibadah on Map
|
|
function displayIbadahOnMap(items) {
|
|
ibadahGroup.clearLayers();
|
|
mapIbadahLayers = {};
|
|
|
|
items.forEach(item => {
|
|
const lat = parseFloat(item.latitude);
|
|
const lng = parseFloat(item.longitude);
|
|
if (isNaN(lat) || isNaN(lng)) return;
|
|
|
|
const marker = L.marker([lat, lng], { icon: createWorshipIcon() }).addTo(ibadahGroup);
|
|
|
|
marker.on('click', (e) => {
|
|
L.DomEvent.stopPropagation(e);
|
|
|
|
// Show radius buffer circle
|
|
bufferGroup.clearLayers();
|
|
L.circle([lat, lng], {
|
|
radius: parseFloat(item.radius_m) || 0,
|
|
color: '#6366f1',
|
|
fillColor: '#818cf8',
|
|
fillOpacity: 0.1,
|
|
weight: 1.5
|
|
}).addTo(bufferGroup);
|
|
|
|
// Zoom and center
|
|
map.setView([lat, lng], 15);
|
|
|
|
// Info popup
|
|
const popupContent = `
|
|
<div class="custom-popup">
|
|
<h4 class="font-bold text-slate-800 text-sm">${escapeHtml(item.nama)}</h4>
|
|
<p class="text-xs text-slate-500 mt-1">${escapeHtml(item.alamat)}</p>
|
|
<div class="text-[10px] text-slate-400 mt-2 space-y-0.5">
|
|
<div>PIC: <strong>${escapeHtml(item.pic)}</strong></div>
|
|
<div>Radius Pelayanan: <strong>${item.radius_m} meter</strong></div>
|
|
</div>
|
|
<div class="mt-3 pt-2 border-t border-slate-100 flex gap-2 justify-between">
|
|
<button onclick="findNearbyPoor(${item.id}, ${item.radius_m}, ${lat}, ${lng})" class="bg-indigo-50 hover:bg-indigo-100 text-indigo-700 text-xs font-bold px-3 py-1.5 rounded-lg transition">Cari Miskin</button>
|
|
<button onclick="openEditIbadah(${item.id})" class="text-xs text-slate-600 font-semibold hover:underline">Edit</button>
|
|
<button onclick="deleteIbadahDirect(${item.id})" class="text-xs text-red-600 font-semibold hover:underline">Hapus</button>
|
|
</div>
|
|
</div>`;
|
|
|
|
marker.bindPopup(popupContent).openPopup();
|
|
});
|
|
|
|
mapIbadahLayers[item.id] = marker;
|
|
});
|
|
}
|
|
|
|
// Display Miskin on Map
|
|
function displayMiskinOnMap(items) {
|
|
miskinGroup.clearLayers();
|
|
mapMiskinLayers = {};
|
|
|
|
items.forEach(item => {
|
|
const lat = parseFloat(item.latitude);
|
|
const lng = parseFloat(item.longitude);
|
|
if (isNaN(lat) || isNaN(lng)) return;
|
|
|
|
const marker = L.marker([lat, lng], {
|
|
icon: createMiskinIcon(item.bantuan_status)
|
|
}).addTo(miskinGroup);
|
|
|
|
marker.on('click', (e) => {
|
|
L.DomEvent.stopPropagation(e);
|
|
|
|
const popupContent = `
|
|
<div class="custom-popup">
|
|
<h4 class="font-bold text-slate-800 text-sm">KK: ${escapeHtml(item.nama_ketua_kk)}</h4>
|
|
<div class="text-[10px] text-slate-500 mt-1.5 space-y-1">
|
|
<div>Jumlah Anggota: <strong>${item.jumlah_anggota} jiwa</strong></div>
|
|
<div class="text-indigo-700 font-semibold">Tempat Ibadah Terdekat: ${escapeHtml(item.rumah_ibadah_nama)}</div>
|
|
<div>Jarak: <strong>${Math.round(item.jarak_ke_rumah_ibadah_m)} meter</strong></div>
|
|
</div>
|
|
<div class="mt-3 pt-2 border-t border-slate-100 flex justify-between items-center">
|
|
<button onclick="openBantuanStatusModal(${item.id}, '${escapeHtml(item.nama_ketua_kk)}', '${item.bantuan_status}', '${escapeHtml(item.bantuan_catatan)}')" class="bg-indigo-600 hover:bg-indigo-700 text-white text-[10px] font-bold px-2.5 py-1.5 rounded-lg transition shadow-md">Bantuan Status</button>
|
|
<button onclick="openEditMiskin(${item.id})" class="text-xs text-slate-600 font-semibold hover:underline">Edit</button>
|
|
<button onclick="deleteMiskinDirect(${item.id})" class="text-xs text-red-600 font-semibold hover:underline">Hapus</button>
|
|
</div>
|
|
</div>`;
|
|
|
|
marker.bindPopup(popupContent).openPopup();
|
|
});
|
|
|
|
mapMiskinLayers[item.id] = marker;
|
|
});
|
|
}
|
|
|
|
// Display Verif on Map
|
|
function displayVerifOnMap(items) {
|
|
verifGroup.clearLayers();
|
|
mapVerifLayers = {};
|
|
|
|
items.forEach(item => {
|
|
const lat = parseFloat(item.latitude);
|
|
const lng = parseFloat(item.longitude);
|
|
if (isNaN(lat) || isNaN(lng)) return;
|
|
|
|
const marker = L.marker([lat, lng], {
|
|
icon: createVerifIcon(item.status_verifikasi)
|
|
}).addTo(verifGroup);
|
|
|
|
marker.on('click', (e) => {
|
|
L.DomEvent.stopPropagation(e);
|
|
|
|
const photoTag = item.foto ? `<img src="../../${item.foto}" alt="Foto Temuan" class="w-full max-h-36 rounded-lg object-cover mt-2.5 shadow-sm">` : '';
|
|
|
|
const popupContent = `
|
|
<div class="custom-popup">
|
|
<h4 class="font-bold text-slate-800 text-sm">Petugas: ${escapeHtml(item.petugas_nama)}</h4>
|
|
<div class="text-[10px] text-slate-500 mt-1 space-y-0.5">
|
|
<div>Target: <strong>${escapeHtml(item.target_nama)} (${item.jenis_target.replace('_', ' ')})</strong></div>
|
|
<div>Status: <span class="uppercase font-extrabold text-[9px] px-1.5 py-0.5 rounded ${item.status_verifikasi === 'terverifikasi' ? 'bg-emerald-100 text-emerald-700' : (item.status_verifikasi === 'ditolak' ? 'bg-rose-100 text-rose-700' : 'bg-slate-100 text-slate-700')}">${item.status_verifikasi}</span></div>
|
|
</div>
|
|
<p class="text-xs text-slate-700 mt-2"><strong>Temuan:</strong> ${escapeHtml(item.hasil_temuan || '-')}</p>
|
|
<p class="text-xs text-indigo-700"><strong>Tindak Lanjut:</strong> ${escapeHtml(item.tindak_lanjut || '-')}</p>
|
|
${photoTag}
|
|
<div class="mt-3 pt-2 border-t border-slate-100 flex justify-end gap-2">
|
|
<button onclick="openEditVerif(${item.id})" class="text-xs text-slate-600 font-semibold hover:underline">Edit</button>
|
|
<button onclick="deleteVerifDirect(${item.id})" class="text-xs text-red-600 font-semibold hover:underline">Hapus</button>
|
|
</div>
|
|
</div>`;
|
|
|
|
marker.bindPopup(popupContent).openPopup();
|
|
});
|
|
|
|
mapVerifLayers[item.id] = marker;
|
|
});
|
|
}
|
|
|
|
// Render Lists in Sidebar
|
|
function renderIbadahList(items) {
|
|
const list = document.getElementById('listIbadah');
|
|
if (items.length === 0) {
|
|
list.innerHTML = `<div class="text-center py-8 text-slate-400 text-sm">Belum ada rumah ibadah.</div>`;
|
|
return;
|
|
}
|
|
list.innerHTML = items.map(item => `
|
|
<div onclick="focusIbadah(${item.id})" class="p-3.5 bg-white border border-slate-200 rounded-2xl hover:border-indigo-500 cursor-pointer transition flex flex-col gap-1.5">
|
|
<span class="font-bold text-slate-800 text-sm">${escapeHtml(item.nama)}</span>
|
|
<span class="text-xs text-slate-500 line-clamp-1">${escapeHtml(item.alamat)}</span>
|
|
<div class="flex items-center justify-between text-[10px] text-slate-400 border-t border-slate-50 pt-2">
|
|
<span>PIC: ${escapeHtml(item.pic)}</span>
|
|
<span>Radius: <strong>${item.radius_m}m</strong></span>
|
|
</div>
|
|
</div>`).join('');
|
|
}
|
|
|
|
function renderMiskinList(items) {
|
|
const list = document.getElementById('listMiskin');
|
|
if (items.length === 0) {
|
|
list.innerHTML = `<div class="text-center py-8 text-slate-400 text-sm">Belum ada data keluarga miskin.</div>`;
|
|
return;
|
|
}
|
|
list.innerHTML = items.map(item => {
|
|
const color = item.bantuan_status === 'sudah' ? 'bg-emerald-100 text-emerald-700' : (item.bantuan_status === 'proses' ? 'bg-amber-100 text-amber-700' : 'bg-rose-100 text-rose-700');
|
|
return `
|
|
<div onclick="focusMiskin(${item.id})" class="p-3.5 bg-white border border-slate-200 rounded-2xl hover:border-emerald-500 cursor-pointer transition flex flex-col gap-1.5">
|
|
<div class="flex justify-between items-start gap-3">
|
|
<span class="font-bold text-slate-800 text-sm">${escapeHtml(item.nama_ketua_kk)}</span>
|
|
<span class="text-[9px] font-extrabold uppercase tracking-wider px-2 py-0.5 rounded-full ${color}">
|
|
${item.bantuan_status}
|
|
</span>
|
|
</div>
|
|
<div class="text-[10px] text-slate-400">
|
|
<div>Anggota: <strong>${item.jumlah_anggota} jiwa</strong></div>
|
|
<div class="text-slate-500 font-medium mt-1">Terdekat: ${escapeHtml(item.rumah_ibadah_nama)} (${Math.round(item.jarak_ke_rumah_ibadah_m)}m)</div>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
function renderVerifList(items) {
|
|
const list = document.getElementById('listVerif');
|
|
if (items.length === 0) {
|
|
list.innerHTML = `<div class="text-center py-8 text-slate-400 text-sm">Belum ada verifikasi lapangan.</div>`;
|
|
return;
|
|
}
|
|
list.innerHTML = items.map(item => {
|
|
const statusColor = item.status_verifikasi === 'terverifikasi' ? 'bg-emerald-100 text-emerald-700' : (item.status_verifikasi === 'ditolak' ? 'bg-rose-100 text-rose-700' : 'bg-slate-100 text-slate-700');
|
|
return `
|
|
<div onclick="focusVerif(${item.id})" class="p-3.5 bg-white border border-slate-200 rounded-2xl hover:border-slate-500 cursor-pointer transition flex flex-col gap-1.5">
|
|
<div class="flex justify-between items-start gap-3">
|
|
<span class="font-bold text-slate-800 text-sm">Petugas: ${escapeHtml(item.petugas_nama)}</span>
|
|
<span class="text-[9px] font-extrabold uppercase tracking-wider px-2 py-0.5 rounded-full ${statusColor}">
|
|
${item.status_verifikasi}
|
|
</span>
|
|
</div>
|
|
<div class="text-[10px] text-slate-500">
|
|
Target: <strong>${escapeHtml(item.target_nama)}</strong> (${item.jenis_target.replace('_', ' ')})
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
// Focus Handlers
|
|
function focusIbadah(id) {
|
|
const marker = mapIbadahLayers[id];
|
|
if (marker) {
|
|
map.setView(marker.getLatLng(), 15);
|
|
marker.fire('click');
|
|
}
|
|
}
|
|
|
|
function focusMiskin(id) {
|
|
const marker = mapMiskinLayers[id];
|
|
if (marker) {
|
|
map.setView(marker.getLatLng(), 16);
|
|
marker.openPopup();
|
|
}
|
|
}
|
|
|
|
function focusVerif(id) {
|
|
const marker = mapVerifLayers[id];
|
|
if (marker) {
|
|
map.setView(marker.getLatLng(), 16);
|
|
marker.openPopup();
|
|
}
|
|
}
|
|
|
|
// Find Nearby Poor Families (Radius analysis)
|
|
window.findNearbyPoor = function(ibadahId, radius, lat, lng) {
|
|
bufferGroup.clearLayers();
|
|
L.circle([lat, lng], {
|
|
radius: radius,
|
|
color: '#10b981',
|
|
fillColor: '#a7f3d0',
|
|
fillOpacity: 0.15,
|
|
weight: 1.5
|
|
}).addTo(bufferGroup);
|
|
|
|
// Filter poor residents inside radius
|
|
const inside = miskinRecords.filter(rec => rec.rumah_ibadah_id === ibadahId && rec.jarak_ke_rumah_ibadah_m <= radius);
|
|
|
|
if (inside.length === 0) {
|
|
showToast('Tidak ada keluarga miskin dalam radius pelayanan rumah ibadah ini.', 'info');
|
|
return;
|
|
}
|
|
|
|
// Create a bounds group to fit all residents + houses of worship
|
|
const boundsGroup = L.featureGroup();
|
|
boundsGroup.addLayer(mapIbadahLayers[ibadahId]);
|
|
inside.forEach(rec => boundsGroup.addLayer(mapMiskinLayers[rec.id]));
|
|
|
|
map.fitBounds(boundsGroup.getBounds(), { padding: [50, 50] });
|
|
showToast(`Ditemukan ${inside.length} keluarga miskin dalam radius pelayanan.`, 'success');
|
|
};
|
|
|
|
// Filter Bantuan List
|
|
window.filterBantuanByIbadah = function() {
|
|
const ibadahIdStr = document.getElementById('bantuanIbadahFilter').value;
|
|
const statusFilter = document.getElementById('bantuanStatusFilter').value;
|
|
const container = document.getElementById('bantuanItems');
|
|
|
|
if (!ibadahIdStr) {
|
|
container.innerHTML = `<div class="text-center py-8 text-slate-400 text-xs">Pilih rumah ibadah untuk memfilter penerima bantuan.</div>`;
|
|
return;
|
|
}
|
|
|
|
const ibadahId = parseInt(ibadahIdStr);
|
|
let filtered = miskinRecords.filter(rec => rec.rumah_ibadah_id === ibadahId);
|
|
|
|
if (statusFilter !== 'all') {
|
|
filtered = filtered.filter(rec => rec.bantuan_status === statusFilter);
|
|
}
|
|
|
|
if (filtered.length === 0) {
|
|
container.innerHTML = `<div class="text-center py-8 text-slate-400 text-xs">Tidak ada penerima bantuan di wilayah rumah ibadah ini dengan filter terpilih.</div>`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = filtered.map(rec => {
|
|
const color = rec.bantuan_status === 'sudah' ? 'bg-emerald-100 text-emerald-700' : (rec.bantuan_status === 'proses' ? 'bg-amber-100 text-amber-700' : 'bg-rose-100 text-rose-700');
|
|
return `
|
|
<div class="p-4 bg-white border border-slate-200 rounded-2xl flex flex-col gap-2 shadow-sm">
|
|
<div class="flex justify-between items-start gap-3">
|
|
<span class="font-bold text-slate-800 text-sm">${escapeHtml(rec.nama_ketua_kk)}</span>
|
|
<span class="text-[9px] font-extrabold uppercase tracking-wider px-2 py-0.5 rounded-full ${color}">
|
|
${rec.bantuan_status}
|
|
</span>
|
|
</div>
|
|
<div class="text-[10px] text-slate-500">
|
|
<div>Jumlah Anggota: <strong>${rec.jumlah_anggota} jiwa</strong></div>
|
|
<div>Catatan: <span class="italic text-slate-400">${escapeHtml(rec.bantuan_catatan || 'Belum ada catatan')}</span></div>
|
|
</div>
|
|
<div class="mt-2.5 pt-2 border-t border-slate-50 flex justify-between gap-2">
|
|
<button onclick="focusMiskin(${rec.id})" class="text-xs text-indigo-600 font-bold hover:underline">Fokus Peta</button>
|
|
<button onclick="openBantuanStatusModal(${rec.id}, '${escapeHtml(rec.nama_ketua_kk)}', '${rec.bantuan_status}', '${escapeHtml(rec.bantuan_catatan)}')" class="bg-indigo-50 hover:bg-indigo-100 text-indigo-700 text-xs px-2.5 py-1 rounded-lg transition font-bold">Update Status</button>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
};
|
|
|
|
// Draw Marker Event
|
|
map.on(L.Draw.Event.CREATED, function (e) {
|
|
const layer = e.layer;
|
|
pendingLatLng = layer.getLatLng();
|
|
|
|
if (addModeType === 'ibadah') {
|
|
document.getElementById('editIbadahId').value = '';
|
|
document.getElementById('ibadahName').value = '';
|
|
document.getElementById('ibadahAddress').value = '';
|
|
document.getElementById('ibadahPic').value = '';
|
|
document.getElementById('ibadahRadius').value = '1000';
|
|
document.getElementById('ibadahCoords').textContent = `${pendingLatLng.lat.toFixed(6)}, ${pendingLatLng.lng.toFixed(6)}`;
|
|
|
|
openIbadahModal();
|
|
reverseGeocodeIbadah(); // auto geocode
|
|
} else if (addModeType === 'miskin') {
|
|
document.getElementById('editMiskinId').value = '';
|
|
document.getElementById('miskinName').value = '';
|
|
document.getElementById('miskinCount').value = '4';
|
|
document.getElementById('miskinCoords').textContent = `${pendingLatLng.lat.toFixed(6)}, ${pendingLatLng.lng.toFixed(6)}`;
|
|
|
|
openMiskinModal();
|
|
} else if (addModeType === 'verif') {
|
|
document.getElementById('editVerifId').value = '';
|
|
document.getElementById('verifTargetName').value = '';
|
|
document.getElementById('verifOfficerName').value = '';
|
|
document.getElementById('verifFindings').value = '';
|
|
document.getElementById('verifFollowUp').value = '';
|
|
document.getElementById('verifPhoto').value = '';
|
|
document.getElementById('verifCoords').textContent = `${pendingLatLng.lat.toFixed(6)}, ${pendingLatLng.lng.toFixed(6)}`;
|
|
|
|
openVerifModal();
|
|
}
|
|
});
|
|
|
|
// Add Mode triggers
|
|
function activateAddMode(type) {
|
|
addModeType = type;
|
|
showToast(`Klik lokasi pada peta untuk meletakkan titik ${type.replace('_', ' ')}.`, 'info');
|
|
|
|
// Trigger marker drawing tool programmatically
|
|
new L.Draw.Marker(map, drawControl.options.draw.marker).enable();
|
|
}
|
|
|
|
// Reverse Geocoding via Nominatim
|
|
async function reverseGeocodeIbadah() {
|
|
if (!pendingLatLng) return;
|
|
const input = document.getElementById('ibadahAddress');
|
|
input.value = 'Mencari alamat...';
|
|
|
|
try {
|
|
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${pendingLatLng.lat}&lon=${pendingLatLng.lng}&zoom=18&addressdetails=1`;
|
|
const response = await fetch(url, { headers: { 'Accept-Language': 'id' } });
|
|
const data = await response.json();
|
|
if (data && data.display_name) {
|
|
input.value = data.display_name;
|
|
} else {
|
|
input.value = '';
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
input.value = '';
|
|
}
|
|
}
|
|
|
|
// Save Worship Place
|
|
async function saveIbadah() {
|
|
const id = document.getElementById('editIbadahId').value;
|
|
const name = document.getElementById('ibadahName').value.trim();
|
|
const address = document.getElementById('ibadahAddress').value.trim();
|
|
const pic = document.getElementById('ibadahPic').value.trim();
|
|
const radius = document.getElementById('ibadahRadius').value;
|
|
|
|
if (!name || !address || !pic) {
|
|
showToast('Lengkapi semua data input rumah ibadah.', 'error');
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('nama', name);
|
|
formData.append('alamat', address);
|
|
formData.append('pic', pic);
|
|
formData.append('latitude', pendingLatLng.lat);
|
|
formData.append('longitude', pendingLatLng.lng);
|
|
formData.append('radius_m', radius);
|
|
|
|
let endpoint = 'api/save_rumah_ibadah.php';
|
|
if (id) {
|
|
endpoint = 'api/update_rumah_ibadah.php';
|
|
formData.append('id', id);
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(endpoint, { method: 'POST', body: formData });
|
|
const data = await res.json();
|
|
if (data.status === 'success') {
|
|
showToast('Data rumah ibadah disimpan!', 'success');
|
|
closeIbadahModal();
|
|
loadAllData();
|
|
} else {
|
|
throw new Error(data.message);
|
|
}
|
|
} catch (err) {
|
|
showToast(err.message || 'Gagal menyimpan.', 'error');
|
|
}
|
|
}
|
|
|
|
// Save Poor Family
|
|
async function saveMiskin() {
|
|
const id = document.getElementById('editMiskinId').value;
|
|
const name = document.getElementById('miskinName').value.trim();
|
|
const count = document.getElementById('miskinCount').value;
|
|
|
|
if (!name || count <= 0) {
|
|
showToast('Lengkapi data ketua KK dan jumlah anggota.', 'error');
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('nama_ketua_kk', name);
|
|
formData.append('jumlah_anggota', count);
|
|
formData.append('latitude', pendingLatLng.lat);
|
|
formData.append('longitude', pendingLatLng.lng);
|
|
|
|
let endpoint = 'api/save_penduduk_miskin.php';
|
|
if (id) {
|
|
endpoint = 'api/update_penduduk_miskin.php';
|
|
formData.append('id', id);
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(endpoint, { method: 'POST', body: formData });
|
|
const data = await res.json();
|
|
if (data.status === 'success') {
|
|
showToast('Data keluarga miskin disimpan!', 'success');
|
|
closeMiskinModal();
|
|
loadAllData();
|
|
} else {
|
|
throw new Error(data.message);
|
|
}
|
|
} catch (err) {
|
|
showToast(err.message || 'Gagal menyimpan.', 'error');
|
|
}
|
|
}
|
|
|
|
// Save Verification report
|
|
async function saveVerif() {
|
|
const id = document.getElementById('editVerifId').value;
|
|
const targetType = document.getElementById('verifTargetType').value;
|
|
const targetName = document.getElementById('verifTargetName').value.trim();
|
|
const officerName = document.getElementById('verifOfficerName').value.trim();
|
|
const status = document.getElementById('verifStatus').value;
|
|
const findings = document.getElementById('verifFindings').value.trim();
|
|
const followUp = document.getElementById('verifFollowUp').value.trim();
|
|
const fileImg = document.getElementById('verifPhoto').files[0];
|
|
|
|
if (!officerName || !targetName) {
|
|
showToast('Nama petugas dan target wajib diisi.', 'error');
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('jenis_target', targetType);
|
|
formData.append('target_nama', targetName);
|
|
formData.append('petugas_nama', officerName);
|
|
formData.append('status_verifikasi', status);
|
|
formData.append('hasil_temuan', findings);
|
|
formData.append('tindak_lanjut', followUp);
|
|
formData.append('latitude', pendingLatLng.lat);
|
|
formData.append('longitude', pendingLatLng.lng);
|
|
if (fileImg) {
|
|
formData.append('foto', fileImg);
|
|
}
|
|
|
|
let endpoint = 'api/save_verifikasi_lapangan.php';
|
|
if (id) {
|
|
endpoint = 'api/update_verifikasi_lapangan.php';
|
|
formData.append('id', id);
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(endpoint, { method: 'POST', body: formData });
|
|
const data = await res.json();
|
|
if (data.status === 'success') {
|
|
showToast('Laporan verifikasi lapangan disimpan!', 'success');
|
|
closeVerifModal();
|
|
loadAllData();
|
|
} else {
|
|
throw new Error(data.message);
|
|
}
|
|
} catch (err) {
|
|
showToast(err.message || 'Gagal menyimpan.', 'error');
|
|
}
|
|
}
|
|
|
|
// Edit Worship Place Mode Trigger
|
|
window.openEditIbadah = function(id) {
|
|
const rec = ibadahRecords.find(r => r.id === id);
|
|
if (!rec) return;
|
|
|
|
pendingLatLng = L.latLng(rec.latitude, rec.longitude);
|
|
|
|
document.getElementById('editIbadahId').value = rec.id;
|
|
document.getElementById('ibadahName').value = rec.nama;
|
|
document.getElementById('ibadahAddress').value = rec.alamat;
|
|
document.getElementById('ibadahPic').value = rec.pic;
|
|
document.getElementById('ibadahRadius').value = rec.radius_m;
|
|
document.getElementById('ibadahCoords').textContent = `${rec.latitude.toFixed(6)}, ${rec.longitude.toFixed(6)}`;
|
|
|
|
document.getElementById('ibadahModalTitle').textContent = 'Edit Rumah Ibadah';
|
|
openIbadahModal();
|
|
};
|
|
|
|
// Edit Poor Resident Mode Trigger
|
|
window.openEditMiskin = function(id) {
|
|
const rec = miskinRecords.find(r => r.id === id);
|
|
if (!rec) return;
|
|
|
|
pendingLatLng = L.latLng(rec.latitude, rec.longitude);
|
|
|
|
document.getElementById('editMiskinId').value = rec.id;
|
|
document.getElementById('miskinName').value = rec.nama_ketua_kk;
|
|
document.getElementById('miskinCount').value = rec.jumlah_anggota;
|
|
document.getElementById('miskinCoords').textContent = `${rec.latitude.toFixed(6)}, ${rec.longitude.toFixed(6)}`;
|
|
|
|
document.getElementById('miskinModalTitle').textContent = 'Edit Penduduk Miskin';
|
|
openMiskinModal();
|
|
};
|
|
|
|
// Edit Verif trigger
|
|
window.openEditVerif = function(id) {
|
|
const rec = verifRecords.find(r => r.id === id);
|
|
if (!rec) return;
|
|
|
|
pendingLatLng = L.latLng(rec.latitude, rec.longitude);
|
|
|
|
document.getElementById('editVerifId').value = rec.id;
|
|
document.getElementById('verifTargetType').value = rec.jenis_target;
|
|
document.getElementById('verifTargetName').value = rec.target_nama;
|
|
document.getElementById('verifOfficerName').value = rec.petugas_nama;
|
|
document.getElementById('verifStatus').value = rec.status_verifikasi;
|
|
document.getElementById('verifFindings').value = rec.hasil_temuan;
|
|
document.getElementById('verifFollowUp').value = rec.tindak_lanjut;
|
|
document.getElementById('verifCoords').textContent = `${rec.latitude.toFixed(6)}, ${rec.longitude.toFixed(6)}`;
|
|
|
|
document.getElementById('verifModalTitle').textContent = 'Edit Verifikasi Lapangan';
|
|
openVerifModal();
|
|
};
|
|
|
|
// Open status modal
|
|
window.openBantuanStatusModal = function(id, name, status, catatan) {
|
|
document.getElementById('bantuanPendudukId').value = id;
|
|
document.getElementById('bantuanKKName').value = name;
|
|
document.getElementById('bantuanStatus').value = status;
|
|
document.getElementById('bantuanCatatan').value = catatan;
|
|
|
|
const modal = document.getElementById('bantuanModal');
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
setTimeout(() => {
|
|
modal.children[0].classList.remove('scale-95', 'opacity-0');
|
|
modal.children[0].classList.add('scale-100', 'opacity-100');
|
|
}, 10);
|
|
};
|
|
|
|
// Update bantuan status in DB
|
|
async function updateBantuanStatus() {
|
|
const id = document.getElementById('bantuanPendudukId').value;
|
|
const status = document.getElementById('bantuanStatus').value;
|
|
const catatan = document.getElementById('bantuanCatatan').value.trim();
|
|
|
|
const formData = new FormData();
|
|
formData.append('id', id);
|
|
formData.append('bantuan_status', status);
|
|
formData.append('bantuan_catatan', catatan);
|
|
|
|
try {
|
|
const res = await fetch('api/update_bantuan_penduduk.php', { method: 'POST', body: formData });
|
|
const data = await res.json();
|
|
if (data.status === 'success') {
|
|
showToast('Status bantuan berhasil diperbarui.', 'success');
|
|
closeBantuanModal();
|
|
loadAllData();
|
|
setTimeout(filterBantuanByIbadah, 300);
|
|
} else {
|
|
throw new Error(data.message);
|
|
}
|
|
} catch (err) {
|
|
showToast(err.message || 'Gagal memperbarui status.', 'error');
|
|
}
|
|
}
|
|
|
|
// Delete Worship Place
|
|
window.deleteIbadahDirect = async function(id) {
|
|
if (!confirm('Apakah Anda ingin menghapus rumah ibadah ini?')) return;
|
|
const formData = new FormData();
|
|
formData.append('id', id);
|
|
try {
|
|
const res = await fetch('api/delete_rumah_ibadah.php', { method: 'POST', body: formData });
|
|
const data = await res.json();
|
|
if (data.status === 'success') {
|
|
showToast('Rumah ibadah berhasil dihapus.', 'success');
|
|
map.closePopup();
|
|
loadAllData();
|
|
} else {
|
|
throw new Error(data.message);
|
|
}
|
|
} catch (err) {
|
|
showToast(err.message || 'Gagal menghapus.', 'error');
|
|
}
|
|
};
|
|
|
|
// Delete Poor family
|
|
window.deleteMiskinDirect = async function(id) {
|
|
if (!confirm('Apakah Anda yakin ingin menghapus data keluarga miskin ini?')) return;
|
|
const formData = new FormData();
|
|
formData.append('id', id);
|
|
try {
|
|
const res = await fetch('api/delete_penduduk_miskin.php', { method: 'POST', body: formData });
|
|
const data = await res.json();
|
|
if (data.status === 'success') {
|
|
showToast('Data keluarga miskin berhasil dihapus.', 'success');
|
|
map.closePopup();
|
|
loadAllData();
|
|
} else {
|
|
throw new Error(data.message);
|
|
}
|
|
} catch (err) {
|
|
showToast(err.message || 'Gagal menghapus.', 'error');
|
|
}
|
|
};
|
|
|
|
// Delete Verif
|
|
window.deleteVerifDirect = async function(id) {
|
|
if (!confirm('Hapus laporan verifikasi ini?')) return;
|
|
const formData = new FormData();
|
|
formData.append('id', id);
|
|
try {
|
|
const res = await fetch('api/delete_verifikasi_lapangan.php', { method: 'POST', body: formData });
|
|
const data = await res.json();
|
|
if (data.status === 'success') {
|
|
showToast('Verifikasi berhasil dihapus.', 'success');
|
|
map.closePopup();
|
|
loadAllData();
|
|
} else {
|
|
throw new Error(data.message);
|
|
}
|
|
} catch (err) {
|
|
showToast(err.message || 'Gagal menghapus.', 'error');
|
|
}
|
|
};
|
|
|
|
// Tabs management
|
|
function switchTab(tab) {
|
|
activeTab = tab;
|
|
const tabs = ['ibadah', 'miskin', 'bantuan', 'verif'];
|
|
tabs.forEach(t => {
|
|
const button = document.getElementById('tab' + capitalizeFirst(t));
|
|
const list = document.getElementById('list' + capitalizeFirst(t));
|
|
if (t === tab) {
|
|
button.className = "flex-1 py-3 text-center border-b-2 border-indigo-600 text-indigo-600 transition min-w-[90px]";
|
|
list.classList.remove('hidden');
|
|
} else {
|
|
button.className = "flex-1 py-3 text-center border-b-2 border-transparent text-slate-500 hover:text-slate-700 transition min-w-[90px]";
|
|
list.classList.add('hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Search Filter
|
|
document.getElementById('searchQuery').addEventListener('input', function (e) {
|
|
const query = e.target.value.toLowerCase();
|
|
if (activeTab === 'ibadah') {
|
|
const filtered = ibadahRecords.filter(rec => rec.nama.toLowerCase().includes(query) || rec.alamat.toLowerCase().includes(query));
|
|
renderIbadahList(filtered);
|
|
} else if (activeTab === 'miskin') {
|
|
const filtered = miskinRecords.filter(rec => rec.nama_ketua_kk.toLowerCase().includes(query) || rec.rumah_ibadah_nama.toLowerCase().includes(query));
|
|
renderMiskinList(filtered);
|
|
} else if (activeTab === 'verif') {
|
|
const filtered = verifRecords.filter(rec => rec.petugas_nama.toLowerCase().includes(query) || rec.target_nama.toLowerCase().includes(query));
|
|
renderVerifList(filtered);
|
|
}
|
|
});
|
|
|
|
// Modals management helpers
|
|
function openIbadahModal() {
|
|
const modal = document.getElementById('ibadahModal');
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
setTimeout(() => {
|
|
modal.children[0].classList.remove('scale-95', 'opacity-0');
|
|
modal.children[0].classList.add('scale-100', 'opacity-100');
|
|
}, 10);
|
|
}
|
|
|
|
function closeIbadahModal() {
|
|
const modal = document.getElementById('ibadahModal');
|
|
modal.children[0].classList.remove('scale-100', 'opacity-100');
|
|
modal.children[0].classList.add('scale-95', 'opacity-0');
|
|
setTimeout(() => {
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}, 150);
|
|
}
|
|
|
|
function openMiskinModal() {
|
|
const modal = document.getElementById('miskinModal');
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
setTimeout(() => {
|
|
modal.children[0].classList.remove('scale-95', 'opacity-0');
|
|
modal.children[0].classList.add('scale-100', 'opacity-100');
|
|
}, 10);
|
|
}
|
|
|
|
function closeMiskinModal() {
|
|
const modal = document.getElementById('miskinModal');
|
|
modal.children[0].classList.remove('scale-100', 'opacity-100');
|
|
modal.children[0].classList.add('scale-95', 'opacity-0');
|
|
setTimeout(() => {
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}, 150);
|
|
}
|
|
|
|
function openBantuanModal() {
|
|
const modal = document.getElementById('bantuanModal');
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
setTimeout(() => {
|
|
modal.children[0].classList.remove('scale-95', 'opacity-0');
|
|
modal.children[0].classList.add('scale-100', 'opacity-100');
|
|
}, 10);
|
|
}
|
|
|
|
function closeBantuanModal() {
|
|
const modal = document.getElementById('bantuanModal');
|
|
modal.children[0].classList.remove('scale-100', 'opacity-100');
|
|
modal.children[0].classList.add('scale-95', 'opacity-0');
|
|
setTimeout(() => {
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}, 150);
|
|
}
|
|
|
|
function openVerifModal() {
|
|
const modal = document.getElementById('verifModal');
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
setTimeout(() => {
|
|
modal.children[0].classList.remove('scale-95', 'opacity-0');
|
|
modal.children[0].classList.add('scale-100', 'opacity-100');
|
|
}, 10);
|
|
}
|
|
|
|
function closeVerifModal() {
|
|
const modal = document.getElementById('verifModal');
|
|
modal.children[0].classList.remove('scale-100', 'opacity-100');
|
|
modal.children[0].classList.add('scale-95', 'opacity-0');
|
|
setTimeout(() => {
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}, 150);
|
|
}
|
|
|
|
// Toast Helper
|
|
function showToast(message, type = 'info') {
|
|
const toast = document.getElementById('toast');
|
|
const msg = document.getElementById('toastMessage');
|
|
msg.textContent = message;
|
|
|
|
toast.classList.remove('hidden', 'bg-red-500', 'bg-emerald-500', 'bg-indigo-500');
|
|
toast.classList.add('flex');
|
|
|
|
if (type === 'error') {
|
|
toast.classList.add('bg-red-500');
|
|
} else if (type === 'success') {
|
|
toast.classList.add('bg-emerald-500');
|
|
} else {
|
|
toast.classList.add('bg-indigo-500');
|
|
}
|
|
|
|
setTimeout(() => {
|
|
toast.classList.remove('scale-95', 'opacity-0');
|
|
toast.classList.add('scale-100', 'opacity-100');
|
|
}, 10);
|
|
|
|
setTimeout(() => {
|
|
toast.classList.remove('scale-100', 'opacity-100');
|
|
toast.classList.add('scale-95', 'opacity-0');
|
|
setTimeout(() => {
|
|
toast.classList.add('hidden');
|
|
}, 150);
|
|
}, 3000);
|
|
}
|
|
|
|
// General helpers
|
|
function escapeHtml(str) {
|
|
return String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
function capitalizeFirst(string) {
|
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
}
|
|
|
|
// Startup load
|
|
loadAllData();
|
|
</script>
|
|
</body>
|
|
</html>
|