chore: prepare docker webgis deployment
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
require_once '../config.php';
|
||||
require_once '../auth/helper.php';
|
||||
require_once '../koneksi.php';
|
||||
if (!is_logged_in() || !has_role('administrator')) { header('Location: ../auth/login.php'); exit; }
|
||||
require_password_changed('../auth/change_password.php');
|
||||
$page_title = 'Analisis & Blank Spot';
|
||||
$page_subtitle = 'Warga yang tidak terjangkau radius rumah ibadah manapun';
|
||||
$active_nav = 'analisis';
|
||||
include '../includes/page-start.php';
|
||||
?>
|
||||
|
||||
<style>
|
||||
.kat-sm{background:#fef2f2;color:#ef4444;}
|
||||
.kat-m{background:#fffbeb;color:#f59e0b;}
|
||||
.kat-hm{background:#eff6ff;color:#3b82f6;}
|
||||
.text-danger{color:#dc2626;}
|
||||
.text-success{color:#16a34a;}
|
||||
</style>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:16px;margin-bottom:24px;">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top"><div class="stat-icon amber"><i data-lucide="alert-triangle"></i></div><div class="stat-label">Jiwa Blank Spot</div></div>
|
||||
<div class="stat-value text-danger" id="an-blank"><span class="spinner"></span></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top"><div class="stat-icon blue"><i data-lucide="percent"></i></div><div class="stat-label">% Blank Spot dari Total</div></div>
|
||||
<div class="stat-value" style="font-size:22px;" id="an-pct"><span class="spinner"></span></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top"><div class="stat-icon green"><i data-lucide="check-circle-2"></i></div><div class="stat-label">Jiwa Terjangkau</div></div>
|
||||
<div class="stat-value text-success" id="an-cover"><span class="spinner"></span></div>
|
||||
</div>
|
||||
<div class="stat-card" style="cursor:pointer;transition:box-shadow .2s;" onmouseover="this.style.boxShadow='0 4px 12px rgba(0,0,0,.12)'" onmouseout="this.style.boxShadow=''" onclick="recalcAll()">
|
||||
<div class="stat-card-top"><div class="stat-icon slate"><i data-lucide="refresh-cw"></i></div><div class="stat-label">Hitung Ulang Proximity</div></div>
|
||||
<div class="stat-value" style="font-size:14px;font-family:var(--font);color:var(--text-secondary);" id="an-recalc">Klik untuk jalankan</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<div class="table-card-header">
|
||||
<div>
|
||||
<div class="table-card-title">Daftar Warga Blank Spot</div>
|
||||
<div class="table-card-count" id="an-count"></div>
|
||||
</div>
|
||||
<a href="map.php" class="btn-see-all"><i data-lucide="map" style="width:14px;height:14px;margin-right:4px;"></i> Lihat di Peta</a>
|
||||
</div>
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:44px;">No</th>
|
||||
<th>Nama KK</th>
|
||||
<th>Kategori</th>
|
||||
<th style="text-align:right;">Jiwa</th>
|
||||
<th>Alamat</th>
|
||||
<th>Jarak ke Ibadah Terdekat</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="anTbody">
|
||||
<tr><td colspan="6" style="text-align:center;padding:28px;color:var(--text-muted);"><span class="spinner"></span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function esc(s){const d=document.createElement('div');d.appendChild(document.createTextNode(s??''));return d.innerHTML;}
|
||||
function fmt(n){return Number(n||0).toLocaleString('id-ID');}
|
||||
|
||||
async function loadAnalisis() {
|
||||
try {
|
||||
const [rs, rp] = await Promise.all([
|
||||
fetch('../api/stats/ambil.php?_='+Date.now()),
|
||||
fetch('../api/penduduk/ambil.php?_='+Date.now())
|
||||
]);
|
||||
const js = await rs.json();
|
||||
const jp = await rp.json();
|
||||
|
||||
if (js.status === 'success') {
|
||||
const d = js.data;
|
||||
document.getElementById('an-blank').textContent = fmt(d.jiwa_blank_spot);
|
||||
document.getElementById('an-cover').textContent = fmt(d.jiwa_terjangkau);
|
||||
const pct = d.total_jiwa > 0
|
||||
? ((d.jiwa_blank_spot / d.total_jiwa) * 100).toFixed(1) + '%'
|
||||
: '0%';
|
||||
document.getElementById('an-pct').textContent = pct;
|
||||
}
|
||||
|
||||
if (jp.status === 'success') {
|
||||
const blanks = jp.data.filter(p => parseInt(p.is_blank_spot) === 1);
|
||||
document.getElementById('an-count').textContent = blanks.length + ' warga blank spot';
|
||||
const katCls = {'Sangat Miskin':'kat-sm','Miskin':'kat-m','Hampir Miskin':'kat-hm'};
|
||||
document.getElementById('anTbody').innerHTML = blanks.length
|
||||
? blanks.map((p, i) => `<tr>
|
||||
<td style="color:var(--text-muted);font-size:12px;">${i + 1}</td>
|
||||
<td style="font-weight:600;">${esc(p.nama_kk||'—')}</td>
|
||||
<td><span style="display:inline-block;padding:2px 9px;border-radius:9999px;font-size:11px;font-weight:500;" class="${esc(katCls[p.kategori]||'')}">${esc(p.kategori||'—')}</span></td>
|
||||
<td style="text-align:right;font-family:var(--font-mono);">${esc(String(p.jumlah_jiwa||0))}</td>
|
||||
<td style="font-size:12px;color:var(--text-muted);">${esc(p.alamat||'—')}</td>
|
||||
<td style="font-size:12px;color:var(--text-muted);">${p.jarak_m ? esc(String(Math.round(p.jarak_m)))+' m (di luar radius)' : '—'}</td>
|
||||
</tr>`).join('')
|
||||
: '<tr><td colspan="6" style="text-align:center;padding:24px;color:var(--text-muted);"><i data-lucide="check" style="color:var(--text-success);margin-right:4px;"></i> Tidak ada blank spot saat ini.</td></tr>';
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
} catch(e) {
|
||||
document.getElementById('anTbody').innerHTML =
|
||||
'<tr><td colspan="6" style="text-align:center;padding:24px;color:#ef4444;">Gagal memuat data.</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function showConfirm({ title, message, confirmLabel = 'Ya, Lanjutkan', cancelLabel = 'Batal', danger = false }) {
|
||||
return new Promise(resolve => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'confirm-overlay';
|
||||
el.innerHTML = `<div class="confirm-box">
|
||||
<div class="confirm-title">${title}</div>
|
||||
<p class="confirm-msg">${message}</p>
|
||||
<div class="confirm-actions">
|
||||
<button class="btn-secondary" id="_sc_cancel">${cancelLabel}</button>
|
||||
<button class="${danger ? 'btn-danger' : 'btn-primary'}" id="_sc_ok">${confirmLabel}</button>
|
||||
</div></div>`;
|
||||
document.body.appendChild(el);
|
||||
const done = val => { el.remove(); resolve(val); };
|
||||
el.querySelector('#_sc_ok').onclick = () => done(true);
|
||||
el.querySelector('#_sc_cancel').onclick = () => done(false);
|
||||
el.addEventListener('click', e => { if (e.target === el) done(false); });
|
||||
});
|
||||
}
|
||||
|
||||
async function recalcAll() {
|
||||
const ok = await showConfirm({
|
||||
title: 'Hitung Ulang Proximity',
|
||||
message: 'Proses ini akan mengubah assignment warga ke rumah ibadah/operator berdasarkan radius terkini. Lanjutkan?',
|
||||
confirmLabel: 'Ya, Hitung Ulang'
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
const lbl = document.getElementById('an-recalc');
|
||||
lbl.innerHTML = '<span class="spinner"></span> Menghitung...';
|
||||
try {
|
||||
const r = await fetch('../api/ibadah/recalculate.php', {method:'POST', body: appendCsrf(new FormData())});
|
||||
const j = await r.json();
|
||||
lbl.innerHTML = j.status === 'success' ? '<i data-lucide="check"></i> Selesai!' : '<i data-lucide="x"></i> Gagal: '+(j.message||'');
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
setTimeout(() => { lbl.textContent = 'Klik untuk jalankan'; loadAnalisis(); }, 2500);
|
||||
} catch(e) {
|
||||
lbl.innerHTML = '<i data-lucide="x"></i> Error koneksi';
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
setTimeout(() => { lbl.textContent = 'Klik untuk jalankan'; }, 2500);
|
||||
}
|
||||
}
|
||||
|
||||
loadAnalisis();
|
||||
</script>
|
||||
|
||||
<?php include '../includes/page-end.php'; ?>
|
||||
@@ -0,0 +1,560 @@
|
||||
<?php
|
||||
require_once '../config.php';
|
||||
require_once '../auth/helper.php';
|
||||
require_once '../koneksi.php';
|
||||
if (!is_logged_in() || !has_role('administrator')) { header('Location: ../auth/login.php'); exit; }
|
||||
require_password_changed('../auth/change_password.php');
|
||||
$page_title = 'Rumah Ibadah';
|
||||
$page_subtitle = 'Kelola data rumah ibadah dan radius pelayanan';
|
||||
$active_nav = 'ibadah';
|
||||
include '../includes/page-start.php';
|
||||
?>
|
||||
|
||||
<!-- Leaflet -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="">
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
||||
|
||||
<style>
|
||||
.ib-filter-bar { display:flex; flex-direction:column; gap:8px; margin-bottom:16px; }
|
||||
.ib-search { width:100%; box-sizing:border-box; padding:8px 12px; border:1px solid var(--card-border); border-radius:8px; font-family:var(--font); font-size:13px; background:#fafaf9; color:var(--text-primary); outline:none; }
|
||||
.ib-search:focus { border-color:#0d7490; }
|
||||
.ib-filter-chips { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
||||
.ib-chip { width:auto; height:auto; padding:7px 11px; border:1px solid var(--card-border); border-radius:8px; font-family:var(--font); font-size:13px; background:#fafaf9; color:var(--text-primary); cursor:pointer; white-space:nowrap; }
|
||||
.ib-chip:focus { outline:none; border-color:#0d7490; }
|
||||
.modal-dark { position:fixed; inset:0; background:rgba(32,21,21,.55); backdrop-filter:blur(4px); display:flex; align-items:center; justify-content:center; padding:20px; z-index:9999; opacity:0; pointer-events:none; transition:opacity .2s; }
|
||||
.modal-dark.show { opacity:1; pointer-events:auto; }
|
||||
.modal-box { width:min(600px,100%); background:#fafaf9; border:1px solid var(--card-border); border-radius:12px; padding:28px; box-shadow:0 4px 16px rgba(32,21,21,.1); max-height:90vh; overflow-y:auto; }
|
||||
.modal-box h3 { font-size:16px; font-weight:600; margin-bottom:20px; color:var(--text-primary); }
|
||||
.mfg { margin-bottom:14px; }
|
||||
.mfg label { display:block; font-size:11px; font-weight:600; color:var(--text-secondary); text-transform:uppercase; letter-spacing:.5px; margin-bottom:5px; }
|
||||
.mfg input, .mfg select { width:100%; padding:9px 12px; border:1px solid var(--card-border); border-radius:8px; font-family:var(--font); font-size:13px; color:var(--text-primary); background:#fafaf9; outline:none; transition:border-color .2s; }
|
||||
.mfg input:focus, .mfg select:focus { border-color:#0d7490; }
|
||||
.mfg small { font-size:11px; color:var(--text-muted); margin-top:4px; display:block; }
|
||||
.modal-actions { display:flex; gap:10px; justify-content:flex-end; margin-top:20px; }
|
||||
.btn-cancel { padding:9px 16px; background:transparent; border:1px solid var(--card-border); border-radius:8px; font-family:var(--font); font-size:13px; font-weight:600; color:var(--text-secondary); cursor:pointer; transition:background .15s; }
|
||||
.btn-cancel:hover { background:#ede8e2; }
|
||||
.btn-submit { padding:9px 18px; background:#0d7490; border:none; border-radius:8px; font-family:var(--font); font-size:14px; font-weight:600; color:#fff; cursor:pointer; transition:background .15s; }
|
||||
.btn-submit:hover { background:#0a5f7a; }
|
||||
.btn-submit:disabled { opacity:.5; cursor:not-allowed; }
|
||||
.btn-danger-submit { padding:9px 18px; background:#ef4444; border:none; border-radius:8px; font-family:var(--font); font-size:13px; font-weight:600; color:#fff; cursor:pointer; transition:background .15s; }
|
||||
.btn-danger-submit:hover { background:#dc2626; }
|
||||
.msg-err { font-size:12px; color:#ef4444; margin-top:8px; min-height:16px; }
|
||||
|
||||
/* ── Map picker ── */
|
||||
.map-picker-wrap { border:1px solid var(--card-border); border-radius:10px; overflow:hidden; position:relative; }
|
||||
.map-picker-wrap #ibMapPicker { height:260px; cursor:crosshair; }
|
||||
.map-picker-hint { display:flex; align-items:center; gap:6px; font-size:11px; color:var(--text-muted); margin-top:6px; }
|
||||
.map-geocoding { font-size:11px; color:var(--text-muted); margin-top:5px; min-height:16px; font-style:italic; }
|
||||
/* Override Leaflet z-index agar tidak keluar modal */
|
||||
#ibMapPicker .leaflet-control-zoom { display:block; }
|
||||
</style>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="ib-filter-bar">
|
||||
<input type="text" class="ib-search" id="ibSearch" placeholder="Cari nama rumah ibadah..." oninput="filterTable()">
|
||||
<div class="ib-filter-chips">
|
||||
<select class="ib-chip" id="ibFilterJenis" onchange="filterTable()">
|
||||
<option value="">Semua Jenis</option>
|
||||
<option>Masjid</option><option>Mushola</option><option>Gereja</option>
|
||||
<option>Pura</option><option>Vihara</option><option>Klenteng</option>
|
||||
</select>
|
||||
<div style="width:1px;height:20px;background:var(--card-border);margin:0 2px;align-self:center;flex-shrink:0;"></div>
|
||||
<select class="ib-chip" id="ibSort" onchange="filterTable()">
|
||||
<option value="">Urutkan</option>
|
||||
<option value="nama_asc">Nama A→Z</option>
|
||||
<option value="nama_desc">Nama Z→A</option>
|
||||
<option value="radius_desc">Radius Terbesar</option>
|
||||
<option value="radius_asc">Radius Terkecil</option>
|
||||
<option value="kk_desc">KK Binaan Terbanyak</option>
|
||||
<option value="kk_asc">KK Binaan Tersedikit</option>
|
||||
<option value="jiwa_desc">Jiwa Terbanyak</option>
|
||||
<option value="jiwa_asc">Jiwa Tersedikit</option>
|
||||
</select>
|
||||
<div style="flex:1;"></div>
|
||||
<a href="map.php" class="btn-see-all" style="text-decoration:none;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;display:inline-flex;align-items:center;gap:4px;white-space:nowrap;"><i data-lucide="map"></i> Lihat di Peta</a>
|
||||
<button class="btn-submit" onclick="openAdd()" style="white-space:nowrap;">+ Tambah Ibadah</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="table-card">
|
||||
<div class="table-card-header">
|
||||
<div>
|
||||
<div class="table-card-title">Daftar Rumah Ibadah</div>
|
||||
<div class="table-card-count" id="ibCount"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:44px;">No</th>
|
||||
<th>Nama</th>
|
||||
<th>Jenis</th>
|
||||
<th style="text-align:right;">Radius (m)</th>
|
||||
<th style="text-align:right;">KK Binaan</th>
|
||||
<th style="text-align:right;">Jiwa</th>
|
||||
<th>Kontak</th>
|
||||
<th style="width:100px;text-align:center;">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ibTbody">
|
||||
<tr><td colspan="8" style="text-align:center;padding:28px;color:var(--text-muted);"><span class="spinner"></span> Memuat...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Add/Edit -->
|
||||
<div class="modal-dark" id="ibModal">
|
||||
<div class="modal-box">
|
||||
<h3 id="ibModalTitle">Tambah Rumah Ibadah</h3>
|
||||
<input type="hidden" id="ibEditId">
|
||||
<div class="mfg">
|
||||
<label>Nama Rumah Ibadah</label>
|
||||
<input type="text" id="ibNama" placeholder="Masjid Al-Ikhlas">
|
||||
</div>
|
||||
<div class="mfg">
|
||||
<label>Jenis</label>
|
||||
<select id="ibJenis">
|
||||
<option>Masjid</option><option>Mushola</option><option>Gereja</option>
|
||||
<option>Pura</option><option>Vihara</option><option>Klenteng</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mfg">
|
||||
<label>Alamat</label>
|
||||
<input type="text" id="ibAlamat" placeholder="Jl. Contoh No. 1">
|
||||
</div>
|
||||
<div class="mfg">
|
||||
<label>Kontak Pengurus</label>
|
||||
<input type="text" id="ibKontak" placeholder="08xx-xxxx-xxxx">
|
||||
</div>
|
||||
<!-- Map Picker -->
|
||||
<div class="mfg">
|
||||
<label>Lokasi di Peta <span style="font-weight:400;color:var(--text-muted);text-transform:none;letter-spacing:0;">— klik untuk set titik, drag marker untuk geser</span></label>
|
||||
<div class="map-picker-wrap">
|
||||
<div id="ibMapPicker"></div>
|
||||
</div>
|
||||
<div class="map-picker-hint" style="display:flex;align-items:center;gap:12px;">
|
||||
<span style="display:inline-flex;align-items:center;gap:4px;"><i data-lucide="mouse-pointer"></i> Klik peta</span>
|
||||
<span style="color:var(--card-border);">·</span>
|
||||
<span style="display:inline-flex;align-items:center;gap:4px;"><i data-lucide="map-pin"></i> Drag marker</span>
|
||||
<span style="color:var(--card-border);">·</span>
|
||||
<span style="display:inline-flex;align-items:center;gap:4px;"><i data-lucide="zoom-in"></i> Scroll untuk zoom</span>
|
||||
</div>
|
||||
<div class="map-geocoding" id="ibGeocodingStatus"></div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||
<div class="mfg">
|
||||
<label>Latitude</label>
|
||||
<input type="number" id="ibLat" step="any" placeholder="-0.0557"
|
||||
oninput="onCoordsManualChange()">
|
||||
</div>
|
||||
<div class="mfg">
|
||||
<label>Longitude</label>
|
||||
<input type="number" id="ibLng" step="any" placeholder="109.3487"
|
||||
oninput="onCoordsManualChange()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mfg">
|
||||
<label>Radius Pelayanan</label>
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<input type="range" id="ibRadius" value="500" min="100" max="5000" step="50"
|
||||
oninput="onRadiusChange(this.value)"
|
||||
style="flex:1;height:4px;accent-color:#111111;cursor:pointer;">
|
||||
<span id="ibRadiusVal"
|
||||
style="min-width:56px;text-align:right;font-family:var(--font);
|
||||
font-size:14px;font-weight:600;color:#374151;">500m</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--text-muted);margin-top:3px;">
|
||||
<span>100m</span><span>500m</span><span>1km</span><span>2.5km</span><span>5km</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="msg-err" id="ibMsg"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-cancel" onclick="closeIbModal()">Batal</button>
|
||||
<button class="btn-submit" id="ibSaveBtn" onclick="saveIbadah()">Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Konfirmasi Hapus (2-step) -->
|
||||
<div class="modal-dark" id="ibHapusModal">
|
||||
<div class="modal-box" style="max-width:440px;">
|
||||
<h3 id="ibHapusTitle">Konfirmasi Hapus</h3>
|
||||
<div id="ibHapusWarning" style="font-size:13px;color:var(--text-secondary);line-height:1.6;margin-bottom:16px;"></div>
|
||||
<input type="hidden" id="ibHapusId">
|
||||
<input type="hidden" id="ibHapusConfirmed" value="0">
|
||||
<div class="modal-actions">
|
||||
<button class="btn-cancel" onclick="closeHapusModal()">Batal</button>
|
||||
<button class="btn-danger-submit" id="ibHapusBtn" onclick="doHapus()">Ya, Hapus</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let _ibAll = [];
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(s ?? ''));
|
||||
return d.innerHTML;
|
||||
}
|
||||
function attr(s) {
|
||||
return String(s ?? '').replace(/&/g, '&').replace(/"/g, '"')
|
||||
.replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
async function loadIbadah() {
|
||||
try {
|
||||
const r = await fetch('../api/ibadah/ambil.php?_=' + Date.now());
|
||||
const j = await r.json();
|
||||
if (j.status !== 'success') return;
|
||||
_ibAll = j.data;
|
||||
document.getElementById('ibCount').textContent = j.total + ' rumah ibadah';
|
||||
filterTable();
|
||||
} catch(e) {
|
||||
document.getElementById('ibTbody').innerHTML =
|
||||
'<tr><td colspan="8" style="text-align:center;padding:24px;color:#ef4444;">Gagal memuat data.</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function filterTable() {
|
||||
const q = document.getElementById('ibSearch').value.toLowerCase();
|
||||
const jenis = document.getElementById('ibFilterJenis').value;
|
||||
const sort = document.getElementById('ibSort').value;
|
||||
const rows = _ibAll.filter(r =>
|
||||
(!q || r.nama.toLowerCase().includes(q)) &&
|
||||
(!jenis || r.jenis === jenis)
|
||||
);
|
||||
if (sort) {
|
||||
rows.sort((a, b) => {
|
||||
switch (sort) {
|
||||
case 'nama_asc': return (a.nama||'').localeCompare(b.nama||'', 'id');
|
||||
case 'nama_desc': return (b.nama||'').localeCompare(a.nama||'', 'id');
|
||||
case 'radius_desc': return (b.radius_m||0) - (a.radius_m||0);
|
||||
case 'radius_asc': return (a.radius_m||0) - (b.radius_m||0);
|
||||
case 'kk_desc': return (b.total_kk||0) - (a.total_kk||0);
|
||||
case 'kk_asc': return (a.total_kk||0) - (b.total_kk||0);
|
||||
case 'jiwa_desc': return (b.total_jiwa||0) - (a.total_jiwa||0);
|
||||
case 'jiwa_asc': return (a.total_jiwa||0) - (b.total_jiwa||0);
|
||||
default: return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
const jenisMap = { Masjid:'masjid', Mushola:'mushola', Gereja:'gereja', Pura:'pura', Vihara:'vihara', Klenteng:'klenteng' };
|
||||
const tbody = document.getElementById('ibTbody');
|
||||
if (!rows.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;padding:24px;color:var(--text-muted);">Tidak ada data.</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = rows.map((r, i) => `<tr>
|
||||
<td style="color:var(--text-muted);font-size:12px;">${i + 1}</td>
|
||||
<td style="font-weight:600;">${esc(r.nama)}</td>
|
||||
<td><span class="jenis-badge ${jenisMap[r.jenis] || ''}">${esc(r.jenis)}</span></td>
|
||||
<td style="text-align:right;font-family:var(--font-mono);">${esc(String(r.radius_m))}</td>
|
||||
<td style="text-align:right;font-family:var(--font-mono);">${esc(String(r.total_kk || 0))}</td>
|
||||
<td style="text-align:right;font-family:var(--font-mono);">${esc(String(r.total_jiwa || 0))}</td>
|
||||
<td style="color:var(--text-muted);font-size:12px;">${esc(r.kontak || '—')}</td>
|
||||
<td style="text-align:center;display:flex;align-items:center;justify-content:center;gap:4px;">
|
||||
<button class="tbl-action-btn" title="Edit" data-id="${attr(parseInt(r.id, 10) || 0)}" onclick="openEditById(this.dataset.id)"><i data-lucide="pencil"></i></button>
|
||||
<button class="tbl-action-btn" title="Hapus" style="color:#ef4444;" data-id="${attr(parseInt(r.id, 10) || 0)}" onclick="openHapus(this.dataset.id)"><i data-lucide="trash-2"></i></button>
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
document.getElementById('ibEditId').value = '';
|
||||
document.getElementById('ibModalTitle').textContent = 'Tambah Rumah Ibadah';
|
||||
['ibNama', 'ibAlamat', 'ibKontak', 'ibLat', 'ibLng'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
el.value = '';
|
||||
delete el.dataset.userEdited;
|
||||
});
|
||||
document.getElementById('ibRadius').value = 500;
|
||||
document.getElementById('ibRadiusVal').textContent = '500m';
|
||||
document.getElementById('ibJenis').value = 'Masjid';
|
||||
document.getElementById('ibMsg').textContent = '';
|
||||
document.getElementById('ibGeocodingStatus').textContent = '';
|
||||
document.getElementById('ibSaveBtn').textContent = 'Simpan';
|
||||
document.getElementById('ibModal').classList.add('show');
|
||||
// Init map tanpa koordinat awal — center ke default wilayah
|
||||
initMapPicker(null, null);
|
||||
}
|
||||
|
||||
function openEdit(r) {
|
||||
document.getElementById('ibEditId').value = r.id;
|
||||
document.getElementById('ibModalTitle').textContent = 'Edit Rumah Ibadah';
|
||||
document.getElementById('ibNama').value = r.nama;
|
||||
document.getElementById('ibJenis').value = r.jenis;
|
||||
document.getElementById('ibKontak').value = r.kontak || '';
|
||||
document.getElementById('ibLat').value = r.lat;
|
||||
document.getElementById('ibLng').value = r.lng;
|
||||
const rv = parseInt(r.radius_m) || 500;
|
||||
document.getElementById('ibRadius').value = rv;
|
||||
document.getElementById('ibRadiusVal').textContent = rv >= 1000
|
||||
? (rv / 1000).toFixed(rv % 1000 === 0 ? 0 : 1) + 'km' : rv + 'm';
|
||||
document.getElementById('ibMsg').textContent = '';
|
||||
document.getElementById('ibGeocodingStatus').textContent = '';
|
||||
document.getElementById('ibSaveBtn').textContent = 'Simpan';
|
||||
|
||||
// Set alamat dan tandai sudah diedit (jangan di-overwrite geocoding)
|
||||
const alamatEl = document.getElementById('ibAlamat');
|
||||
alamatEl.value = r.alamat || '';
|
||||
alamatEl.dataset.userEdited = r.alamat ? '1' : '';
|
||||
|
||||
document.getElementById('ibModal').classList.add('show');
|
||||
// Init map pada koordinat yang sudah ada
|
||||
initMapPicker(parseFloat(r.lat), parseFloat(r.lng));
|
||||
}
|
||||
|
||||
function openEditById(id) {
|
||||
const row = _ibAll.find(r => String(r.id) === String(id));
|
||||
if (row) openEdit(row);
|
||||
}
|
||||
|
||||
function closeIbModal() {
|
||||
document.getElementById('ibModal').classList.remove('show');
|
||||
clearTimeout(_geocodeTimer);
|
||||
if (_ibMap) { _ibMap.remove(); _ibMap = null; _ibMarker = null; _ibCircle = null; }
|
||||
}
|
||||
|
||||
async function saveIbadah() {
|
||||
const id = document.getElementById('ibEditId').value;
|
||||
const btn = document.getElementById('ibSaveBtn');
|
||||
const msg = document.getElementById('ibMsg');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Menyimpan...';
|
||||
msg.textContent = '';
|
||||
|
||||
const namaVal = document.getElementById('ibNama').value.trim();
|
||||
const latVal = document.getElementById('ibLat').value.trim();
|
||||
const lngVal = document.getElementById('ibLng').value.trim();
|
||||
if (!namaVal) {
|
||||
msg.textContent = 'Nama wajib diisi.';
|
||||
btn.disabled = false; btn.textContent = 'Simpan'; return;
|
||||
}
|
||||
if (!latVal || !lngVal) {
|
||||
msg.textContent = 'Koordinat latitude dan longitude wajib diisi.';
|
||||
btn.disabled = false; btn.textContent = 'Simpan'; return;
|
||||
}
|
||||
|
||||
const fd = new FormData();
|
||||
if (id) fd.append('id', id);
|
||||
fd.append('nama', document.getElementById('ibNama').value.trim());
|
||||
fd.append('jenis', document.getElementById('ibJenis').value);
|
||||
fd.append('alamat', document.getElementById('ibAlamat').value.trim());
|
||||
fd.append('kontak', document.getElementById('ibKontak').value.trim());
|
||||
fd.append('lat', document.getElementById('ibLat').value);
|
||||
fd.append('lng', document.getElementById('ibLng').value);
|
||||
fd.append('radius_m', document.getElementById('ibRadius').value);
|
||||
|
||||
const url = id ? '../api/ibadah/update.php' : '../api/ibadah/simpan.php';
|
||||
try {
|
||||
const r = await fetch(url, { method: 'POST', body: appendCsrf(fd) });
|
||||
const j = await r.json();
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Simpan';
|
||||
if (j.status === 'success') { closeIbModal(); loadIbadah(); }
|
||||
else { msg.textContent = j.message || 'Terjadi kesalahan.'; }
|
||||
} catch(e) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Simpan';
|
||||
msg.textContent = 'Gagal terhubung ke server.';
|
||||
}
|
||||
}
|
||||
|
||||
function openHapus(id) {
|
||||
document.getElementById('ibHapusId').value = id;
|
||||
document.getElementById('ibHapusConfirmed').value = '0';
|
||||
document.getElementById('ibHapusTitle').textContent = 'Konfirmasi Hapus';
|
||||
document.getElementById('ibHapusWarning').textContent =
|
||||
'Yakin ingin menghapus rumah ibadah ini? Semua data proximity akan dihitung ulang.';
|
||||
document.getElementById('ibHapusBtn').textContent = 'Ya, Hapus';
|
||||
document.getElementById('ibHapusModal').classList.add('show');
|
||||
}
|
||||
|
||||
function closeHapusModal() {
|
||||
document.getElementById('ibHapusModal').classList.remove('show');
|
||||
}
|
||||
|
||||
async function doHapus() {
|
||||
const id = document.getElementById('ibHapusId').value;
|
||||
const confirmed = document.getElementById('ibHapusConfirmed').value;
|
||||
const btn = document.getElementById('ibHapusBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Menghapus...';
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('id', id);
|
||||
if (confirmed === '1') fd.append('confirmed', '1');
|
||||
|
||||
try {
|
||||
const r = await fetch('../api/ibadah/hapus.php', { method: 'POST', body: appendCsrf(fd) });
|
||||
const j = await r.json();
|
||||
btn.disabled = false;
|
||||
|
||||
if (j.status === 'warning') {
|
||||
const opNames = (j.operators || []).map(o => esc(o.nama_lengkap)).join(', ');
|
||||
document.getElementById('ibHapusTitle').innerHTML = '<i data-lucide="alert-triangle" style="color:#f59e0b;vertical-align:middle;"></i> Peringatan';
|
||||
document.getElementById('ibHapusWarning').innerHTML =
|
||||
`Rumah ibadah ini terhubung dengan Operator: <strong>${opNames}</strong>.<br>
|
||||
Operator tidak bisa login sampai dikaitkan ke ibadah lain.<br><br>Tetap hapus?`;
|
||||
document.getElementById('ibHapusConfirmed').value = '1';
|
||||
document.getElementById('ibHapusBtn').textContent = 'Ya, Tetap Hapus';
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
} else if (j.status === 'success') {
|
||||
closeHapusModal();
|
||||
loadIbadah();
|
||||
} else {
|
||||
document.getElementById('ibHapusWarning').innerHTML =
|
||||
`<span style="color:#ef4444;">${esc(j.message || 'Terjadi kesalahan.')}</span>`;
|
||||
btn.textContent = 'Ya, Hapus';
|
||||
}
|
||||
} catch(e) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Ya, Hapus';
|
||||
document.getElementById('ibHapusWarning').innerHTML =
|
||||
'<span style="color:#ef4444;">Gagal terhubung ke server.</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Map Picker ──────────────────────────────────────────────
|
||||
const DEFAULT_LAT = <?= CENTER_LAT ?>;
|
||||
const DEFAULT_LNG = <?= CENTER_LNG ?>;
|
||||
const DEFAULT_ZOOM = <?= ZOOM_LEVEL ?>;
|
||||
|
||||
let _ibMap = null;
|
||||
let _ibMarker = null;
|
||||
let _ibCircle = null;
|
||||
let _geocodeTimer = null;
|
||||
|
||||
function initMapPicker(lat, lng) {
|
||||
const centerLat = lat || DEFAULT_LAT;
|
||||
const centerLng = lng || DEFAULT_LNG;
|
||||
const zoom = lat ? 17 : DEFAULT_ZOOM;
|
||||
|
||||
if (_ibMap) {
|
||||
_ibMap.remove();
|
||||
_ibMap = null;
|
||||
_ibMarker = null;
|
||||
}
|
||||
|
||||
_ibMap = L.map('ibMapPicker', { zoomControl: true }).setView([centerLat, centerLng], zoom);
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(_ibMap);
|
||||
|
||||
// Marker awal jika ada koordinat
|
||||
if (lat && lng) {
|
||||
placeMarker(lat, lng);
|
||||
}
|
||||
|
||||
// Klik peta → set lokasi
|
||||
_ibMap.on('click', function (e) {
|
||||
placeMarker(e.latlng.lat, e.latlng.lng);
|
||||
fillCoords(e.latlng.lat, e.latlng.lng);
|
||||
scheduleGeocode(e.latlng.lat, e.latlng.lng);
|
||||
});
|
||||
|
||||
// Fix ukuran setelah modal terlihat
|
||||
setTimeout(() => _ibMap && _ibMap.invalidateSize(), 80);
|
||||
}
|
||||
|
||||
function placeMarker(lat, lng) {
|
||||
if (_ibMarker) {
|
||||
_ibMarker.setLatLng([lat, lng]);
|
||||
} else {
|
||||
_ibMarker = L.marker([lat, lng], { draggable: true }).addTo(_ibMap);
|
||||
_ibMarker.on('dragend', function (e) {
|
||||
const ll = e.target.getLatLng();
|
||||
fillCoords(ll.lat, ll.lng);
|
||||
updateCircle(ll.lat, ll.lng);
|
||||
scheduleGeocode(ll.lat, ll.lng);
|
||||
});
|
||||
}
|
||||
updateCircle(lat, lng);
|
||||
_ibMap.panTo([lat, lng]);
|
||||
}
|
||||
|
||||
function updateCircle(lat, lng) {
|
||||
const r = parseInt(document.getElementById('ibRadius').value) || 500;
|
||||
if (_ibCircle) {
|
||||
_ibCircle.setLatLng([lat, lng]).setRadius(r);
|
||||
} else if (_ibMap) {
|
||||
_ibCircle = L.circle([lat, lng], {
|
||||
radius: r, color: '#111111', weight: 1.5,
|
||||
fillColor: '#111111', fillOpacity: 0.05, opacity: 0.6
|
||||
}).addTo(_ibMap);
|
||||
}
|
||||
}
|
||||
|
||||
function onRadiusChange(val) {
|
||||
document.getElementById('ibRadiusVal').textContent = val >= 1000
|
||||
? (val / 1000).toFixed(val % 1000 === 0 ? 0 : 1) + 'km'
|
||||
: val + 'm';
|
||||
if (_ibCircle) _ibCircle.setRadius(parseInt(val));
|
||||
}
|
||||
|
||||
function fillCoords(lat, lng) {
|
||||
document.getElementById('ibLat').value = lat.toFixed(8);
|
||||
document.getElementById('ibLng').value = lng.toFixed(8);
|
||||
}
|
||||
|
||||
// Ketik manual di field lat/lng → pindahkan marker
|
||||
function onCoordsManualChange() {
|
||||
const lat = parseFloat(document.getElementById('ibLat').value);
|
||||
const lng = parseFloat(document.getElementById('ibLng').value);
|
||||
if (!isNaN(lat) && !isNaN(lng) && _ibMap) {
|
||||
placeMarker(lat, lng);
|
||||
scheduleGeocode(lat, lng);
|
||||
}
|
||||
}
|
||||
|
||||
// Debounce reverse geocoding (delay 600ms setelah berhenti drag/klik)
|
||||
function scheduleGeocode(lat, lng) {
|
||||
clearTimeout(_geocodeTimer);
|
||||
const status = document.getElementById('ibGeocodingStatus');
|
||||
status.innerHTML = '<span class="spinner"></span> Mencari alamat...';
|
||||
_geocodeTimer = setTimeout(() => reverseGeocode(lat, lng), 600);
|
||||
}
|
||||
|
||||
async function reverseGeocode(lat, lng) {
|
||||
const status = document.getElementById('ibGeocodingStatus');
|
||||
const alamatEl = document.getElementById('ibAlamat');
|
||||
try {
|
||||
const r = await fetch(
|
||||
`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json`,
|
||||
{ headers: { 'Accept-Language': 'id' } }
|
||||
);
|
||||
const j = await r.json();
|
||||
if (j && j.display_name) {
|
||||
// Hanya isi alamat jika masih kosong atau user belum mengetik manual
|
||||
if (!alamatEl.dataset.userEdited) {
|
||||
alamatEl.value = j.display_name;
|
||||
}
|
||||
status.innerHTML = '<i data-lucide="check" style="color:var(--text-success);margin-right:4px;"></i> ' + (j.address?.road || j.address?.suburb || j.display_name.split(',')[0]);
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
} else {
|
||||
status.textContent = '';
|
||||
}
|
||||
} catch {
|
||||
status.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Tandai alamat sudah diedit manual → jangan di-overwrite geocoding
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.getElementById('ibAlamat').addEventListener('input', function () {
|
||||
this.dataset.userEdited = '1';
|
||||
});
|
||||
});
|
||||
|
||||
loadIbadah();
|
||||
</script>
|
||||
|
||||
<?php include '../includes/page-end.php'; ?>
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
require_once '../config.php';
|
||||
require_once '../auth/helper.php';
|
||||
require_once '../koneksi.php';
|
||||
if (!is_logged_in() || !has_role('administrator')) { header('Location: ../auth/login.php'); exit; }
|
||||
require_password_changed('../auth/change_password.php');
|
||||
$page_title = 'Import CSV';
|
||||
$page_subtitle = 'Upload data penduduk miskin dari file CSV';
|
||||
$active_nav = 'import';
|
||||
include '../includes/page-start.php';
|
||||
?>
|
||||
|
||||
<div style="max-width:700px;">
|
||||
<div class="dash-card">
|
||||
<div class="dash-card-title">Import Data Penduduk via CSV</div>
|
||||
|
||||
<div style="display:flex;gap:10px;margin-bottom:20px;align-items:center;flex-wrap:wrap;">
|
||||
<a href="../api/import/template.php" class="btn-primary" style="text-decoration:none;padding:9px 16px;font-size:13px;display:inline-flex;align-items:center;gap:6px;"><i data-lucide="download"></i> Download Template CSV</a>
|
||||
<span style="font-size:12px;color:var(--text-muted);">Maks. 500 baris per upload</span>
|
||||
<span style="font-size:12px;color:var(--text-muted);">Data valid otomatis masuk sebagai Terverifikasi.</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:16px;">
|
||||
<label style="display:block;font-size:11px;font-weight:600;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;">Pilih File CSV</label>
|
||||
<input type="file" id="importFile" accept=".csv" style="font-size:13px;color:var(--text-primary);" onchange="previewCSV()">
|
||||
</div>
|
||||
|
||||
<div id="importPreviewArea" style="display:none;">
|
||||
<div id="importSummary" style="margin-bottom:12px;font-size:13px;"></div>
|
||||
<div id="importPreviewTable" style="overflow-x:auto;margin-bottom:12px;"></div>
|
||||
<div id="importErrorList" style="display:none;margin-bottom:12px;"></div>
|
||||
<button id="btnDoImport" class="btn-primary" style="font-size:14px;display:inline-flex;align-items:center;gap:6px;" onclick="doImport()"><i data-lucide="check"></i> Import Sekarang</button>
|
||||
</div>
|
||||
<div id="importResultMsg" style="font-size:13px;margin-top:12px;min-height:16px;font-family:var(--font);"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function esc(s){const d=document.createElement('div');d.appendChild(document.createTextNode(String(s??'')));return d.innerHTML;}
|
||||
|
||||
async function previewCSV() {
|
||||
const file = document.getElementById('importFile').files[0];
|
||||
if (!file) return;
|
||||
const fd = new FormData();
|
||||
fd.append('csv_file', file);
|
||||
fd.append('mode', 'preview');
|
||||
document.getElementById('importPreviewArea').style.display = 'none';
|
||||
const msg = document.getElementById('importResultMsg');
|
||||
msg.style.color = 'var(--text-muted)';
|
||||
msg.innerHTML = '<span class="spinner"></span> Menganalisis...';
|
||||
try {
|
||||
const res = await fetch('../api/import/csv.php', {method:'POST', body: appendCsrf(fd)});
|
||||
const j = await res.json();
|
||||
msg.textContent = '';
|
||||
if (j.status !== 'success') {
|
||||
msg.style.color = '#ef4444';
|
||||
msg.innerHTML = '<i data-lucide="x-circle" style="color:#ef4444;vertical-align:middle;margin-right:4px;"></i> ' + esc(j.message || 'Error tidak diketahui.');
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
document.getElementById('importSummary').innerHTML =
|
||||
`<span style="color:#16a34a;font-weight:600;">${esc(String(j.total_ok))} baris valid</span>` +
|
||||
(j.total_error > 0
|
||||
? ` · <span style="color:#ef4444;font-weight:600;">${esc(String(j.total_error))} baris error</span>`
|
||||
: '');
|
||||
if (j.preview && j.preview.length) {
|
||||
const cols = ['nama_kk','nik','jumlah_jiwa','kategori','lat','lng'];
|
||||
document.getElementById('importPreviewTable').innerHTML =
|
||||
`<table style="width:100%;border-collapse:collapse;background:#fafaf9;border:1px solid var(--card-border);border-radius:8px;overflow:hidden;font-size:12px;">
|
||||
<thead><tr>${cols.map(c=>`<th style="padding:6px 10px;font-size:10px;font-weight:600;color:var(--text-secondary);text-transform:uppercase;background:#ede8e2;text-align:left;">${esc(c)}</th>`).join('')}</tr></thead>
|
||||
<tbody>${j.preview.map(row=>'<tr>'+cols.map(c=>`<td style="padding:5px 10px;border-top:1px solid var(--card-border);">${esc(String(row[c]??''))}</td>`).join('')+'</tr>').join('')}</tbody>
|
||||
</table>
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">Menampilkan ${esc(String(j.preview.length))} dari ${esc(String(j.total_ok))} baris valid</div>`;
|
||||
} else {
|
||||
document.getElementById('importPreviewTable').innerHTML =
|
||||
'<div style="font-size:12px;color:var(--text-muted);padding:10px 0;">Tidak ada baris valid untuk ditampilkan.</div>';
|
||||
}
|
||||
if (j.errors && j.errors.length) {
|
||||
document.getElementById('importErrorList').innerHTML =
|
||||
`<div style="background:#fee2e2;border:1px solid #fca5a5;border-radius:8px;padding:10px 14px;">
|
||||
${j.errors.slice(0,10).map(e=>`<div style="font-size:11px;color:#ef4444;padding:2px 0;">Baris ${esc(String(e.row))}: ${esc(e.error)}</div>`).join('')}
|
||||
${j.errors.length>10?`<div style="font-size:11px;color:var(--text-muted);">...dan ${esc(String(j.errors.length-10))} error lainnya</div>`:''}
|
||||
</div>`;
|
||||
document.getElementById('importErrorList').style.display = '';
|
||||
} else {
|
||||
document.getElementById('importErrorList').style.display = 'none';
|
||||
}
|
||||
document.getElementById('importPreviewArea').style.display = '';
|
||||
document.getElementById('btnDoImport').disabled = (j.total_ok === 0);
|
||||
} catch(e) {
|
||||
msg.style.color = '#ef4444';
|
||||
msg.innerHTML = '<i data-lucide="x-circle" style="color:#ef4444;vertical-align:middle;margin-right:4px;"></i> Gagal menghubungi server.';
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
async function doImport() {
|
||||
const file = document.getElementById('importFile').files[0];
|
||||
if (!file) return;
|
||||
const btn = document.getElementById('btnDoImport');
|
||||
const msg = document.getElementById('importResultMsg');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Mengimport...';
|
||||
msg.textContent = '';
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('csv_file', file);
|
||||
fd.append('mode', 'import');
|
||||
const res = await fetch('../api/import/csv.php', {method:'POST', body: appendCsrf(fd)});
|
||||
const j = await res.json();
|
||||
if (j.status === 'success') {
|
||||
msg.style.color = '#16a34a';
|
||||
msg.innerHTML = '<i data-lucide="check-circle" style="color:#16a34a;vertical-align:middle;margin-right:4px;"></i> ' + esc(j.message);
|
||||
document.getElementById('importFile').value = '';
|
||||
document.getElementById('importPreviewArea').style.display = 'none';
|
||||
document.getElementById('importErrorList').style.display = 'none';
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
} else {
|
||||
msg.style.color = '#ef4444';
|
||||
msg.innerHTML = '<i data-lucide="x-circle" style="color:#ef4444;vertical-align:middle;margin-right:4px;"></i> ' + esc(j.message || 'Import gagal.');
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
} catch(e) {
|
||||
msg.style.color = '#ef4444';
|
||||
msg.innerHTML = '<i data-lucide="x-circle" style="color:#ef4444;vertical-align:middle;margin-right:4px;"></i> Gagal menghubungi server.';
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i data-lucide="check"></i> Import Sekarang';
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php include '../includes/page-end.php'; ?>
|
||||
@@ -0,0 +1,443 @@
|
||||
<?php
|
||||
require_once '../config.php';
|
||||
require_once '../auth/helper.php';
|
||||
require_once '../koneksi.php';
|
||||
if (!is_logged_in() || !has_role('operator')) { header('Location: ../auth/login.php'); exit; }
|
||||
require_password_changed('../auth/change_password.php');
|
||||
$is_admin = has_role('administrator');
|
||||
$ibadah_id = get_ibadah_id();
|
||||
$page_title = 'Kebutuhan & Papan Publik';
|
||||
$page_subtitle = 'Kelola kebutuhan warga dan pantau papan publik';
|
||||
$active_nav = 'kebutuhan';
|
||||
include '../includes/page-start.php';
|
||||
?>
|
||||
<script>window._IBADAH_ID=<?= $ibadah_id?(int)$ibadah_id:'null'?>;window._IS_ADMIN=<?= $is_admin?'true':'false'?>;</script>
|
||||
|
||||
<style>
|
||||
.keb-wrap{display:grid;grid-template-columns:1fr 360px;gap:16px;align-items:start;}
|
||||
@media(max-width:900px){.keb-wrap{grid-template-columns:1fr;}}
|
||||
.keb-list-item{padding:10px 16px;border-bottom:1px solid var(--hd-border);cursor:pointer;transition:background .15s;}
|
||||
.keb-list-item:hover{background:#ede8e2;}
|
||||
.keb-list-item.active{background:#ede8e2;border-left:3px solid #0d7490;padding-left:13px;}
|
||||
.keb-open-badge{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:22px;border-radius:9999px;background:#fef2f2;color:#ef4444;font-size:11px;font-weight:500;padding:0 6px;}
|
||||
.keb-status-sel{padding:5px 10px;border-radius:6px;font-size:12px;font-weight:500;cursor:pointer;border:1px solid var(--card-border);background:#fafaf9;font-family:var(--font);color:var(--text-secondary);}
|
||||
.keb-status-sel.belum{background:#f5f0eb;color:#7a7067;border-color:#c5bdb5;}
|
||||
.keb-status-sel.proses{background:#fffbeb;color:#d97706;border-color:#fcd34d;}
|
||||
.keb-status-sel.terpenuhi{background:#f0fdf4;color:#16a34a;border-color:#86efac;}
|
||||
/* ── Tab navigation ─────────────────────────────────────────── */
|
||||
.tab-nav{display:flex;gap:2px;margin-bottom:16px;border-bottom:2px solid var(--card-border);padding-bottom:0;}
|
||||
.tab-btn{background:none;border:none;padding:10px 18px;font-family:var(--font);font-size:13px;font-weight:500;
|
||||
color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-2px;
|
||||
display:inline-flex;align-items:center;gap:6px;transition:color .15s;}
|
||||
.tab-btn:hover{color:var(--text-primary);}
|
||||
.tab-btn.active{color:#0d7490;border-bottom-color:#0d7490;}
|
||||
.tab-badge{background:#ef4444;color:#fff;font-size:10px;font-weight:700;min-width:18px;height:18px;
|
||||
border-radius:9999px;display:inline-flex;align-items:center;justify-content:center;padding:0 5px;}
|
||||
/* ── Donor inbox cards ──────────────────────────────────────── */
|
||||
.donor-card{padding:14px 16px;border-bottom:1px solid var(--hd-border);}
|
||||
.donor-card.unread{border-left:3px solid #0d7490;padding-left:13px;background:#f7fbfd;}
|
||||
.donor-card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;}
|
||||
.donor-nama{font-weight:600;font-size:14px;}
|
||||
.donor-meta{font-size:11px;color:var(--text-muted);}
|
||||
.donor-kontak{font-size:12px;color:var(--text-secondary);margin-bottom:6px;display:flex;align-items:center;gap:4px;}
|
||||
.keb-copyable{cursor:pointer;border-radius:5px;padding:2px 5px;margin:-2px -5px;transition:background .15s,color .15s;}
|
||||
.keb-copyable:hover{background:#ede8e2;color:var(--text-primary);}
|
||||
.donor-pesan{font-size:13px;line-height:1.5;margin-bottom:8px;}
|
||||
.donor-actions{display:flex;gap:8px;flex-wrap:wrap;}
|
||||
.btn-hubungi{background:#16a34a;color:#fff;border:none;border-radius:6px;padding:5px 12px;
|
||||
font-size:12px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:4px;text-decoration:none;}
|
||||
.btn-hubungi:hover{background:#15803d;}
|
||||
.btn-hapus{background:transparent;color:#ef4444;border:1px solid #fca5a5;border-radius:6px;
|
||||
padding:5px 12px;font-size:12px;font-weight:600;cursor:pointer;}
|
||||
.btn-hapus:hover{background:#fef2f2;}
|
||||
/* ── Toast ──────────────────────────────────────────────────── */
|
||||
.keb-toast{position:fixed;bottom:24px;right:24px;z-index:10000;display:flex;align-items:center;gap:10px;
|
||||
padding:12px 18px;border-radius:10px;font-size:13px;font-weight:500;box-shadow:0 4px 16px rgba(32,21,21,.15);
|
||||
transform:translateY(12px);opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;}
|
||||
.keb-toast.show{opacity:1;transform:translateY(0);pointer-events:auto;}
|
||||
.keb-toast.success{background:#f0fdf4;color:#15803d;border:1px solid #86efac;}
|
||||
.keb-toast.error{background:#fef2f2;color:#dc2626;border:1px solid #fca5a5;}
|
||||
</style>
|
||||
|
||||
<div class="tab-nav">
|
||||
<button class="tab-btn active" data-tab="kebutuhan" onclick="switchTab('kebutuhan')">
|
||||
<i data-lucide="heart" style="width:14px;height:14px;"></i> Kebutuhan Warga
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="donatur" onclick="switchTab('donatur')">
|
||||
<i data-lucide="mail" style="width:14px;height:14px;"></i> Pesan Donatur
|
||||
<span class="tab-badge" id="donaturBadge" style="display:none;"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="tabKebutuhan" class="tab-panel">
|
||||
<div class="keb-wrap">
|
||||
<!-- Left: penduduk list -->
|
||||
<div class="table-card">
|
||||
<div class="table-card-header">
|
||||
<div class="table-card-title">Penduduk dengan Kebutuhan</div>
|
||||
<a href="../papan-kebutuhan.php" target="_blank" class="btn-see-all">🌐 Papan Publik</a>
|
||||
</div>
|
||||
<div style="padding:10px 14px;border-bottom:1px solid var(--hd-border);">
|
||||
<input type="text" id="kebSearch" style="width:100%;padding:7px 12px;border:1px solid var(--card-border);border-radius:8px;font-family:var(--font);font-size:13px;outline:none;" placeholder="Cari nama KK..." oninput="filterKebList()">
|
||||
</div>
|
||||
<div id="kebList" style="max-height:65vh;overflow-y:auto;">
|
||||
<div style="padding:20px;text-align:center;color:var(--text-muted);"><span class="spinner"></span> Memuat...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: kebutuhan detail -->
|
||||
<div class="dash-card" id="kebDetail" style="position:sticky;top:16px;">
|
||||
<div class="dash-card-title" id="kebDetailTitle" style="margin-bottom:12px;">Detail Kebutuhan</div>
|
||||
<div id="kebDetailBody" style="font-size:13px;color:var(--text-muted);text-align:center;padding:32px 0;">
|
||||
Pilih warga di sebelah kiri untuk melihat dan mengelola kebutuhannya.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- #tabKebutuhan -->
|
||||
|
||||
<div id="kebToast" class="keb-toast" role="alert" aria-live="polite"></div>
|
||||
|
||||
<div id="tabDonatur" class="tab-panel" style="display:none;">
|
||||
<?php if ($is_admin): ?>
|
||||
<div class="table-card">
|
||||
<div class="table-card-header">
|
||||
<div>
|
||||
<div class="table-card-title">Pesan dari Calon Donatur</div>
|
||||
<div class="table-card-count" id="donaturCount"></div>
|
||||
</div>
|
||||
<button class="btn-primary" id="btnMarkAll" onclick="markAllRead()"
|
||||
style="font-size:12px;padding:6px 14px;white-space:nowrap;">
|
||||
Tandai Semua Sudah Dibaca
|
||||
</button>
|
||||
</div>
|
||||
<div id="donaturList" style="padding:16px;">
|
||||
<span class="spinner"></span> Memuat...
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="dash-card" style="text-align:center;padding:48px 24px;color:var(--text-muted);">
|
||||
<div style="font-size:13px;">Hanya administrator yang dapat melihat pesan donatur.</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div><!-- #tabDonatur -->
|
||||
|
||||
<script>
|
||||
let _kebAll = [], _kebSelectedId = null;
|
||||
function esc(s){const d=document.createElement('div');d.appendChild(document.createTextNode(s??''));return d.innerHTML;}
|
||||
function attr(s){return String(s??'').replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,''').replace(/</g,'<').replace(/>/g,'>');}
|
||||
|
||||
async function loadKeb() {
|
||||
try {
|
||||
const r = await fetch('../api/penduduk/ambil.php?_='+Date.now());
|
||||
const j = await r.json();
|
||||
if (j.status !== 'success') return;
|
||||
let data = j.data.filter(p => p.status_verifikasi === 'Terverifikasi');
|
||||
if (window._IBADAH_ID && !window._IS_ADMIN) {
|
||||
data = data.filter(p => parseInt(p.ibadah_id) === window._IBADAH_ID);
|
||||
}
|
||||
_kebAll = data;
|
||||
filterKebList();
|
||||
} catch(e) {
|
||||
document.getElementById('kebList').innerHTML = '<div style="padding:16px;color:#ef4444;">Gagal memuat data.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function filterKebList() {
|
||||
const q = document.getElementById('kebSearch').value.toLowerCase();
|
||||
const rows = _kebAll.filter(p => (p.nama_kk||'').toLowerCase().includes(q));
|
||||
const el = document.getElementById('kebList');
|
||||
if (!rows.length) {
|
||||
el.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-muted);">Tidak ada data.</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = rows.map(p => {
|
||||
const open = parseInt(p.kebutuhan_open||0) + parseInt(p.kebutuhan_proses||0);
|
||||
const active = _kebSelectedId === p.id ? ' active' : '';
|
||||
return `<div class="keb-list-item${active}" data-id="${attr(parseInt(p.id, 10) || 0)}" data-nama="${attr(p.nama_kk||'?')}" onclick="selectPenduduk(parseInt(this.dataset.id, 10), this.dataset.nama)">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;">
|
||||
<span style="font-weight:600;font-size:13px;">${esc(p.nama_kk||'—')}</span>
|
||||
<span class="keb-open-badge">${open}</span>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:3px;">${esc(p.nama_ibadah||'Blank Spot')} · ${esc(p.kategori||'—')}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const KEB_KATEGORI = ['Sembako','Biaya Sekolah','Biaya Kesehatan','Modal Usaha','Renovasi Rumah','Perlengkapan Rumah','Pakaian','Lainnya'];
|
||||
|
||||
async function selectPenduduk(id, nama) {
|
||||
_kebSelectedId = id;
|
||||
filterKebList();
|
||||
document.getElementById('kebDetailTitle').textContent = nama;
|
||||
document.getElementById('kebDetailBody').innerHTML = '<span class="spinner"></span> Memuat...';
|
||||
try {
|
||||
const r = await fetch('../api/kebutuhan/ambil.php?penduduk_id='+id+'&_='+Date.now());
|
||||
const j = await r.json();
|
||||
const clsMap = {'Belum Terpenuhi':'belum','Dalam Proses':'proses','Terpenuhi':'terpenuhi'};
|
||||
|
||||
const listHtml = (!j.data || !j.data.length)
|
||||
? '<p style="color:var(--text-muted);font-size:13px;margin-bottom:4px;">Belum ada kebutuhan tercatat.</p>'
|
||||
: j.data.map(k => `
|
||||
<div style="padding:10px 0;border-bottom:1px solid var(--hd-border);">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:5px;">
|
||||
<span style="font-weight:600;font-size:13px;">${esc(k.kategori)}</span>
|
||||
<select class="keb-status-sel ${clsMap[k.status]||''}"
|
||||
data-id="${attr(parseInt(k.id,10)||0)}" data-orig="${attr(k.status||'')}" onchange="updateKeb(this)">
|
||||
<option ${k.status==='Belum Terpenuhi'?'selected':''}>Belum Terpenuhi</option>
|
||||
<option ${k.status==='Dalam Proses'?'selected':''}>Dalam Proses</option>
|
||||
<option ${k.status==='Terpenuhi'?'selected':''}>Terpenuhi</option>
|
||||
</select>
|
||||
</div>
|
||||
${k.deskripsi?`<div style="font-size:12px;color:var(--text-muted);">${esc(k.deskripsi)}</div>`:''}
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">Oleh ${esc(k.created_by_nama||'—')} · ${esc((k.created_at||'').slice(0,10))}</div>
|
||||
</div>`).join('');
|
||||
|
||||
const addFormHtml = `
|
||||
<div style="margin-top:14px;padding-top:14px;border-top:2px dashed var(--hd-border);">
|
||||
<div style="font-size:11px;font-weight:700;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px;">Tambah Kebutuhan</div>
|
||||
<select id="kebAddKat_${id}" style="width:100%;padding:7px 10px;border:1px solid var(--card-border);border-radius:7px;font-family:var(--font);font-size:13px;background:#fafaf9;color:var(--text-primary);outline:none;margin-bottom:8px;">
|
||||
${KEB_KATEGORI.map(k=>`<option value="${esc(k)}">${esc(k)}</option>`).join('')}
|
||||
</select>
|
||||
<input type="text" id="kebAddDesc_${id}" placeholder="Deskripsi (opsional, maks 300 karakter)" maxlength="300"
|
||||
style="width:100%;padding:7px 10px;border:1px solid var(--card-border);border-radius:7px;font-family:var(--font);font-size:13px;background:#fafaf9;color:var(--text-primary);outline:none;margin-bottom:8px;box-sizing:border-box;">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<button onclick="addKebAPI(${id})" id="kebAddBtn_${id}" class="btn-primary" style="padding:7px 18px;font-size:13px;">Simpan</button>
|
||||
<span id="kebAddMsg_${id}" style="font-size:12px;color:#ef4444;"></span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.getElementById('kebDetailBody').innerHTML = listHtml + addFormHtml;
|
||||
} catch(e) {
|
||||
document.getElementById('kebDetailBody').innerHTML = '<span style="color:#ef4444;">Gagal memuat kebutuhan.</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function addKebAPI(id) {
|
||||
const kat = document.getElementById(`kebAddKat_${id}`).value;
|
||||
const desc = document.getElementById(`kebAddDesc_${id}`).value.trim();
|
||||
const btn = document.getElementById(`kebAddBtn_${id}`);
|
||||
const msg = document.getElementById(`kebAddMsg_${id}`);
|
||||
btn.disabled = true; btn.textContent = 'Menyimpan...'; msg.textContent = '';
|
||||
const fd = new FormData();
|
||||
fd.append('penduduk_id', id); fd.append('kategori', kat); fd.append('deskripsi', desc);
|
||||
try {
|
||||
const r = await fetch('../api/kebutuhan/simpan.php', { method:'POST', body: appendCsrf(fd) });
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
loadKeb();
|
||||
selectPenduduk(id, document.getElementById('kebDetailTitle').textContent);
|
||||
} else {
|
||||
msg.textContent = j.message || 'Gagal menyimpan.';
|
||||
btn.disabled = false; btn.textContent = 'Simpan';
|
||||
}
|
||||
} catch(e) {
|
||||
msg.textContent = 'Gagal terhubung ke server.';
|
||||
btn.disabled = false; btn.textContent = 'Simpan';
|
||||
}
|
||||
}
|
||||
|
||||
async function updateKeb(sel) {
|
||||
const id = sel.dataset.id;
|
||||
const orig = sel.dataset.orig;
|
||||
const catatan = prompt('Catatan perubahan kebutuhan (opsional):', '');
|
||||
if (catatan === null) {
|
||||
sel.value = orig;
|
||||
return;
|
||||
}
|
||||
const fd = new FormData();
|
||||
fd.append('id', id); fd.append('status', sel.value); fd.append('catatan', catatan);
|
||||
try {
|
||||
const r = await fetch('../api/kebutuhan/update_status.php', {method:'POST', body: appendCsrf(fd)});
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
sel.dataset.orig = sel.value;
|
||||
const clsMap = {'Belum Terpenuhi':'belum','Dalam Proses':'proses','Terpenuhi':'terpenuhi'};
|
||||
sel.className = 'keb-status-sel ' + (clsMap[sel.value]||'');
|
||||
loadKeb();
|
||||
} else { alert(j.message||'Gagal update.'); }
|
||||
} catch(e) { alert('Gagal terhubung ke server.'); }
|
||||
}
|
||||
|
||||
loadKeb();
|
||||
if (window._IS_ADMIN) loadDonorInbox();
|
||||
|
||||
// ── Toast ─────────────────────────────────────────────────────────────
|
||||
let _kebToastTimer = null;
|
||||
function showKebToast(msg, type) {
|
||||
const el = document.getElementById('kebToast');
|
||||
if (!el) return;
|
||||
clearTimeout(_kebToastTimer);
|
||||
el.className = 'keb-toast ' + (type || 'success');
|
||||
el.textContent = msg;
|
||||
el.classList.add('show');
|
||||
_kebToastTimer = setTimeout(function() { el.classList.remove('show'); }, 3500);
|
||||
}
|
||||
|
||||
// ── Confirm dialog ────────────────────────────────────────────────────
|
||||
function showKebConfirm(title, message) {
|
||||
return new Promise(function(resolve) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'confirm-overlay';
|
||||
el.innerHTML = '<div class="confirm-box">'
|
||||
+ '<div class="confirm-title">' + esc(title) + '</div>'
|
||||
+ '<p class="confirm-msg">' + esc(message) + '</p>'
|
||||
+ '<div class="confirm-actions">'
|
||||
+ '<button class="btn-secondary" id="_kc_cancel">Batal</button>'
|
||||
+ '<button class="btn-danger" id="_kc_ok">Ya, Hapus</button>'
|
||||
+ '</div></div>';
|
||||
document.body.appendChild(el);
|
||||
const done = function(val) { el.remove(); resolve(val); };
|
||||
el.querySelector('#_kc_ok').onclick = function() { done(true); };
|
||||
el.querySelector('#_kc_cancel').onclick = function() { done(false); };
|
||||
el.addEventListener('click', function(e) { if (e.target === el) done(false); });
|
||||
});
|
||||
}
|
||||
|
||||
// ── Tab system ────────────────────────────────────────────────────────
|
||||
function switchTab(tab) {
|
||||
document.querySelectorAll('.tab-btn').forEach(function(b) {
|
||||
b.classList.toggle('active', b.dataset.tab === tab);
|
||||
});
|
||||
document.getElementById('tabKebutuhan').style.display = tab === 'kebutuhan' ? '' : 'none';
|
||||
document.getElementById('tabDonatur').style.display = tab === 'donatur' ? '' : 'none';
|
||||
if (tab === 'donatur' && window._IS_ADMIN && !_donorLoaded) loadDonorInbox();
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
// ── Donor inbox ───────────────────────────────────────────────────────
|
||||
function copyKontak(el) {
|
||||
const text = el.dataset.copy || '';
|
||||
if (!text) return;
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
showKebToast('Tersalin: ' + text, 'success');
|
||||
}).catch(function() { _fallbackCopy(text); });
|
||||
} else {
|
||||
_fallbackCopy(text);
|
||||
}
|
||||
}
|
||||
function _fallbackCopy(text) {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.cssText = 'position:fixed;opacity:0;top:0;left:0;';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
try { document.execCommand('copy'); showKebToast('Tersalin: ' + text, 'success'); }
|
||||
catch(e) { showKebToast('Gagal menyalin.', 'error'); }
|
||||
ta.remove();
|
||||
}
|
||||
|
||||
function buildKontakUrl(kontak) {
|
||||
const trimmed = (kontak || '').trim();
|
||||
if (trimmed.includes('@')) return 'mailto:' + trimmed;
|
||||
const digits = trimmed.replace(/\D/g, '');
|
||||
if (digits.length >= 8) {
|
||||
const normalized = digits.startsWith('0') ? '62' + digits.slice(1) : digits;
|
||||
return 'https://wa.me/' + normalized;
|
||||
}
|
||||
return 'mailto:' + trimmed;
|
||||
}
|
||||
|
||||
let _donorData = [], _donorLoaded = false;
|
||||
|
||||
async function loadDonorInbox() {
|
||||
const list = document.getElementById('donaturList');
|
||||
if (!list) return;
|
||||
list.innerHTML = '<span class="spinner"></span> Memuat...';
|
||||
try {
|
||||
const r = await fetch('../api/papan/donatur.php?_=' + Date.now());
|
||||
const j = await r.json();
|
||||
if (j.status !== 'success') throw new Error(j.message || 'Gagal memuat.');
|
||||
_donorData = j.data || [];
|
||||
_donorLoaded = true;
|
||||
renderDonorInbox(_donorData);
|
||||
} catch (e) {
|
||||
list.innerHTML = '<div style="color:#ef4444;padding:8px;">Gagal memuat pesan: ' + esc(e.message) + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderDonorInbox(data) {
|
||||
const list = document.getElementById('donaturList');
|
||||
const badge = document.getElementById('donaturBadge');
|
||||
const counter = document.getElementById('donaturCount');
|
||||
if (!list) return;
|
||||
|
||||
const unread = data.filter(function(d) { return !parseInt(d.is_read); }).length;
|
||||
if (badge) { badge.textContent = unread; badge.style.display = unread > 0 ? '' : 'none'; }
|
||||
if (counter) counter.textContent = data.length + ' pesan' + (unread > 0 ? ', ' + unread + ' belum dibaca' : '');
|
||||
|
||||
if (!data.length) {
|
||||
list.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-muted);font-size:13px;">Belum ada pesan masuk dari calon donatur.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = data.map(function(d) {
|
||||
const isUnread = !parseInt(d.is_read);
|
||||
const kontakUrl = buildKontakUrl(d.kontak || '');
|
||||
const isWa = kontakUrl.startsWith('https://wa.me');
|
||||
const safeId = parseInt(d.id, 10) || 0;
|
||||
return '<div class="donor-card' + (isUnread ? ' unread' : '') + '" id="dc-' + safeId + '">'
|
||||
+ '<div class="donor-card-header">'
|
||||
+ '<div style="display:flex;align-items:center;gap:8px;">'
|
||||
+ '<span class="donor-nama">' + esc(d.nama) + '</span>'
|
||||
+ (isUnread ? '<span style="display:inline-block;width:7px;height:7px;background:#0d7490;border-radius:50%;"></span>' : '')
|
||||
+ (d.kategori_minat ? '<span class="badge-pill" style="background:#eff6ff;color:#2563eb;">' + esc(d.kategori_minat) + '</span>' : '')
|
||||
+ '</div>'
|
||||
+ '<span class="donor-meta">' + esc((d.created_at || '').slice(0, 10)) + '</span>'
|
||||
+ '</div>'
|
||||
+ '<div class="donor-kontak keb-copyable" data-copy="' + attr(d.kontak || '') + '" onclick="copyKontak(this)" title="Klik untuk salin">'
|
||||
+ '<i data-lucide="copy" style="width:11px;height:11px;opacity:.5;flex-shrink:0;"></i> '
|
||||
+ esc(d.kontak)
|
||||
+ '</div>'
|
||||
+ (d.pesan ? '<div class="donor-pesan">' + esc(d.pesan) + '</div>' : '')
|
||||
+ '<div class="donor-actions">'
|
||||
+ '<a href="' + attr(kontakUrl) + '" target="_blank" rel="noopener" class="btn-hubungi">'
|
||||
+ (isWa ? 'WhatsApp' : 'Email')
|
||||
+ '</a>'
|
||||
+ '<button class="btn-hapus" onclick="hapusDonatur(' + safeId + ')">Hapus</button>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
async function markAllRead() {
|
||||
const btn = document.getElementById('btnMarkAll');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Memproses...'; }
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'mark_all');
|
||||
try {
|
||||
const r = await fetch('../api/papan/donatur.php', { method: 'POST', body: appendCsrf(fd) });
|
||||
const j = await r.json();
|
||||
if (j.status !== 'success') throw new Error(j.message || 'Gagal.');
|
||||
_donorData = _donorData.map(function(d) { return Object.assign({}, d, { is_read: '1' }); });
|
||||
renderDonorInbox(_donorData);
|
||||
showKebToast('Semua pesan ditandai sudah dibaca.', 'success');
|
||||
} catch (e) {
|
||||
showKebToast('Gagal menandai: ' + e.message, 'error');
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Tandai Semua Sudah Dibaca'; }
|
||||
}
|
||||
}
|
||||
|
||||
async function hapusDonatur(id) {
|
||||
const ok = await showKebConfirm('Hapus Pesan', 'Hapus pesan donatur ini secara permanen?');
|
||||
if (!ok) return;
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'hapus');
|
||||
fd.append('id', id);
|
||||
try {
|
||||
const r = await fetch('../api/papan/donatur.php', { method: 'POST', body: appendCsrf(fd) });
|
||||
const j = await r.json();
|
||||
if (j.status !== 'success') throw new Error(j.message || 'Gagal hapus.');
|
||||
_donorData = _donorData.filter(function(d) { return parseInt(d.id) !== id; });
|
||||
renderDonorInbox(_donorData);
|
||||
showKebToast('Pesan dihapus.', 'success');
|
||||
} catch (e) {
|
||||
showKebToast('Gagal menghapus: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php include '../includes/page-end.php'; ?>
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
require_once '../config.php';
|
||||
require_once '../auth/helper.php';
|
||||
require_once '../koneksi.php';
|
||||
if (!is_logged_in() || !has_role('administrator')) { header('Location: ../auth/login.php'); exit; }
|
||||
require_password_changed('../auth/change_password.php');
|
||||
$page_title = 'Laporan & Export';
|
||||
$page_subtitle = 'Export data binaan per rumah ibadah';
|
||||
$active_nav = 'laporan';
|
||||
include '../includes/page-start.php';
|
||||
?>
|
||||
|
||||
<div style="max-width:640px;">
|
||||
<div class="dash-card" style="margin-bottom:20px;">
|
||||
<div class="dash-card-title">Pilih Rumah Ibadah</div>
|
||||
<select id="lapIbadah" style="width:100%;padding:10px 12px;border:1px solid var(--card-border);border-radius:8px;font-family:var(--font);font-size:14px;color:var(--text-primary);background:#fafaf9;cursor:pointer;margin-bottom:12px;outline:none;" onchange="loadPreview()">
|
||||
<option value="">-- Pilih Rumah Ibadah --</option>
|
||||
</select>
|
||||
<div id="lapPreview" style="display:none;">
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:16px;" id="lapCards"></div>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
||||
<a id="btnCsv" href="" aria-disabled="true" onclick="return this.getAttribute('aria-disabled') !== 'true';" class="btn-primary disabled" style="text-decoration:none;padding:10px 20px;display:inline-flex;align-items:center;gap:6px;"><i data-lucide="download"></i> Download CSV</a>
|
||||
<a id="btnPdf" href="" aria-disabled="true" onclick="return this.getAttribute('aria-disabled') !== 'true';" target="_blank" class="btn-secondary disabled" style="text-decoration:none;padding:10px 20px;display:inline-flex;align-items:center;gap:6px;"><i data-lucide="printer"></i> Cetak PDF</a>
|
||||
</div>
|
||||
<p style="font-size:11px;color:var(--text-muted);margin-top:10px;">CSV tidak menyertakan NIK untuk meminimalkan risiko penyebaran data sensitif.</p>
|
||||
</div>
|
||||
<div id="lapMsg" style="font-size:13px;margin-top:8px;color:var(--text-muted);"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function esc(s){const d=document.createElement('div');d.appendChild(document.createTextNode(s??''));return d.innerHTML;}
|
||||
function fmt(n){return Number(n||0).toLocaleString('id-ID');}
|
||||
|
||||
async function loadIbadahList() {
|
||||
try {
|
||||
const r = await fetch('../api/ibadah/ambil.php?_='+Date.now());
|
||||
const j = await r.json();
|
||||
if (j.status !== 'success') return;
|
||||
const sel = document.getElementById('lapIbadah');
|
||||
j.data.forEach(ib => {
|
||||
const o = document.createElement('option');
|
||||
o.value = ib.id;
|
||||
o.textContent = '['+ib.jenis+'] '+ib.nama+' ('+fmt(ib.total_kk||0)+' KK)';
|
||||
o.dataset.kk = ib.total_kk || 0;
|
||||
o.dataset.jiwa = ib.total_jiwa || 0;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
} catch(e) {
|
||||
document.getElementById('lapMsg').textContent = 'Gagal memuat daftar ibadah.';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreview() {
|
||||
const sel = document.getElementById('lapIbadah');
|
||||
const id = sel.value;
|
||||
const preview = document.getElementById('lapPreview');
|
||||
if (!id) { preview.style.display = 'none'; return; }
|
||||
|
||||
const opt = sel.selectedOptions[0];
|
||||
const kk = opt.dataset.kk || 0;
|
||||
const jiwa = opt.dataset.jiwa || 0;
|
||||
|
||||
document.getElementById('lapMsg').innerHTML = '<span class="spinner"></span> Memuat statistik...';
|
||||
preview.style.display = 'none';
|
||||
|
||||
try {
|
||||
const r = await fetch('../api/stats/ambil.php?_='+Date.now());
|
||||
const j = await r.json();
|
||||
document.getElementById('lapMsg').textContent = '';
|
||||
let pct = 0;
|
||||
if (j.status === 'success') {
|
||||
const stat = (j.data.rekap_ibadah || []).find(x => String(x.id) === String(id));
|
||||
if (stat && parseInt(stat.jml_kk) > 0) {
|
||||
pct = Math.round(parseInt(stat.jml_ditangani) / parseInt(stat.jml_kk) * 100);
|
||||
}
|
||||
}
|
||||
document.getElementById('lapCards').innerHTML =
|
||||
`<div class="need-stat"><div class="need-stat-val blue">${fmt(kk)}</div><div class="need-stat-lbl">KK Binaan</div></div>
|
||||
<div class="need-stat"><div class="need-stat-val">${fmt(jiwa)}</div><div class="need-stat-lbl">Jiwa Binaan</div></div>
|
||||
<div class="need-stat"><div class="need-stat-val green">${esc(String(pct))}%</div><div class="need-stat-lbl">Sudah Ditangani</div></div>`;
|
||||
document.getElementById('btnCsv').href = '../api/export/csv.php?ibadah_id=' + encodeURIComponent(id);
|
||||
document.getElementById('btnPdf').href = '../api/export/pdf.php?ibadah_id=' + encodeURIComponent(id);
|
||||
document.getElementById('btnCsv').setAttribute('aria-disabled', 'false');
|
||||
document.getElementById('btnPdf').setAttribute('aria-disabled', 'false');
|
||||
document.getElementById('btnCsv').classList.remove('disabled');
|
||||
document.getElementById('btnPdf').classList.remove('disabled');
|
||||
preview.style.display = '';
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
} catch(e) {
|
||||
document.getElementById('lapMsg').textContent = 'Gagal memuat statistik.';
|
||||
}
|
||||
}
|
||||
|
||||
loadIbadahList();
|
||||
</script>
|
||||
|
||||
<?php include '../includes/page-end.php'; ?>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
require_once '../config.php';
|
||||
require_once '../auth/helper.php';
|
||||
require_once '../koneksi.php';
|
||||
if (!is_logged_in() || !has_role('operator')) {
|
||||
header('Location: ../auth/login.php');
|
||||
exit;
|
||||
}
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
require_password_changed('../auth/change_password.php');
|
||||
$page_title = 'Peta Interaktif';
|
||||
$page_subtitle = 'Peta sebaran rumah ibadah dan penduduk miskin';
|
||||
$active_nav = 'map';
|
||||
include '../includes/page-start.php';
|
||||
?>
|
||||
|
||||
<style>
|
||||
.al-content {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.map-frame-wrap {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
background: #f5f0eb;
|
||||
}
|
||||
.map-frame {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: #f5f0eb;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="map-frame-wrap">
|
||||
<iframe
|
||||
class="map-frame"
|
||||
src="../index.php?embedded=1&v=20260608-ui6"
|
||||
title="Peta Interaktif"
|
||||
loading="eager"
|
||||
referrerpolicy="same-origin">
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
<?php include '../includes/page-end.php'; ?>
|
||||
@@ -0,0 +1,836 @@
|
||||
<?php
|
||||
require_once '../config.php';
|
||||
require_once '../auth/helper.php';
|
||||
require_once '../koneksi.php';
|
||||
if (!is_logged_in() || !has_role('operator')) { header('Location: ../auth/login.php'); exit; }
|
||||
require_password_changed('../auth/change_password.php');
|
||||
$is_admin = has_role('administrator');
|
||||
$ibadah_id = get_ibadah_id();
|
||||
$page_title = 'Penduduk Miskin';
|
||||
$page_subtitle = 'Daftar dan manajemen data penduduk miskin';
|
||||
$active_nav = 'penduduk';
|
||||
include '../includes/page-start.php';
|
||||
?>
|
||||
<script>
|
||||
window._IBADAH_ID = <?= $ibadah_id ? (int)$ibadah_id : 'null' ?>;
|
||||
window._IS_ADMIN = <?= $is_admin ? 'true' : 'false' ?>;
|
||||
</script>
|
||||
|
||||
<!-- Leaflet -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="">
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
||||
|
||||
<style>
|
||||
.pd-filter-bar{display:flex;flex-direction:column;gap:8px;margin-bottom:16px;}
|
||||
.pd-input{width:100%;box-sizing:border-box;padding:8px 12px;border:1px solid var(--card-border);border-radius:8px;font-family:var(--font);font-size:13px;background:#fafaf9;color:var(--text-primary);outline:none;}
|
||||
.pd-input:focus{border-color:#0d7490;}
|
||||
.pd-filter-chips{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}
|
||||
.pd-chip{width:auto;height:auto;padding:7px 11px;border:1px solid var(--card-border);border-radius:8px;font-family:var(--font);font-size:13px;background:#fafaf9;color:var(--text-primary);cursor:pointer;white-space:nowrap;}
|
||||
.pd-chip:focus{outline:none;border-color:#0d7490;}
|
||||
.pd-chip-spacer{flex:1;}
|
||||
.kat-badge{display:inline-block;padding:2px 9px;border-radius:9999px;font-size:11px;font-weight:500;}
|
||||
.kat-sm{background:#fef2f2;color:#ef4444;}
|
||||
.kat-m{background:#fffbeb;color:#f59e0b;}
|
||||
.kat-hm{background:#eff6ff;color:#3b82f6;}
|
||||
.bs-badge{display:inline-block;padding:2px 9px;border-radius:9999px;font-size:11px;font-weight:500;}
|
||||
.bs-blank{background:#fef2f2;color:#ef4444;}
|
||||
.bs-cover{background:#f0fdf4;color:#16a34a;}
|
||||
.sv-badge{display:inline-flex;align-items:center;gap:3px;padding:2px 9px;border-radius:9999px;font-size:11px;font-weight:500;}
|
||||
.sv-pending{background:#fef3c7;color:#d97706;}
|
||||
.sv-ok{background:#d1fae5;color:#16a34a;}
|
||||
.sv-tolak{background:#fee2e2;color:#dc2626;}
|
||||
.stat-sel{padding:5px 10px;border-radius:6px;font-size:12px;font-weight:500;cursor:pointer;border:1px solid var(--card-border);background:#fafaf9;font-family:var(--font);color:var(--text-secondary);}
|
||||
.stat-sel.belum{background:#f5f0eb;color:#7a7067;border-color:#c5bdb5;}
|
||||
.stat-sel.proses{background:#fffbeb;color:#d97706;border-color:#fcd34d;}
|
||||
.stat-sel.sudah{background:#f0fdf4;color:#16a34a;border-color:#86efac;}
|
||||
.modal-dark{position:fixed;inset:0;background:rgba(32,21,21,.55);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;padding:20px;z-index:9999;opacity:0;pointer-events:none;transition:opacity .2s;}
|
||||
.modal-dark.show{opacity:1;pointer-events:auto;}
|
||||
.modal-box{width:min(620px,100%);background:#fafaf9;border:1px solid var(--card-border);border-radius:12px;padding:24px;box-shadow:0 4px 16px rgba(32,21,21,.1);max-height:90vh;display:flex;flex-direction:column;}
|
||||
.modal-box h3{font-size:15px;font-weight:600;margin-bottom:14px;color:var(--text-primary);flex-shrink:0;}
|
||||
.modal-scroll{overflow-y:auto;flex:1;padding-right:4px;}
|
||||
.mfg{margin-bottom:14px;}
|
||||
.mfg label{display:block;font-size:11px;font-weight:600;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px;margin-bottom:5px;}
|
||||
.mfg input,.mfg select,.mfg textarea{width:100%;padding:9px 12px;border:1px solid var(--card-border);border-radius:8px;font-family:var(--font);font-size:13px;color:var(--text-primary);background:#fafaf9;outline:none;transition:border-color .2s;}
|
||||
.mfg input:focus,.mfg select:focus,.mfg textarea:focus{border-color:#0d7490;}
|
||||
.mfg textarea{resize:vertical;min-height:70px;}
|
||||
.mfg small{font-size:11px;color:var(--text-muted);margin-top:4px;display:block;}
|
||||
.modal-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:16px;flex-shrink:0;padding-top:14px;border-top:1px solid var(--card-border);}
|
||||
.btn-cancel{padding:9px 16px;background:transparent;border:1px solid var(--card-border);border-radius:8px;font-family:var(--font);font-size:13px;font-weight:600;color:var(--text-secondary);cursor:pointer;transition:background .15s;}
|
||||
.btn-cancel:hover{background:#ede8e2;}
|
||||
.btn-primary{padding:9px 18px;background:#0d7490;border:none;border-radius:8px;font-family:var(--font);font-size:14px;font-weight:600;color:#fff;cursor:pointer;transition:background .15s;}
|
||||
.btn-primary:hover{background:#0a5f7a;}
|
||||
.btn-primary:disabled{opacity:.5;cursor:not-allowed;}
|
||||
.btn-approve{padding:4px 10px;background:#f0fdf4;border:1px solid #86efac;border-radius:9999px;font-size:11px;font-weight:500;color:#16a34a;cursor:pointer;white-space:nowrap;}
|
||||
.btn-approve:hover{background:#dcfce7;}
|
||||
.btn-reject{padding:4px 10px;background:#fef2f2;border:1px solid #fca5a5;border-radius:9999px;font-size:11px;font-weight:500;color:#dc2626;cursor:pointer;white-space:nowrap;}
|
||||
.btn-reject:hover{background:#fee2e2;}
|
||||
.msg-err{font-size:12px;color:#ef4444;margin-top:6px;min-height:16px;}
|
||||
.map-picker-wrap{border:1px solid var(--card-border);border-radius:10px;overflow:hidden;}
|
||||
.map-picker-wrap #pdMapPicker{height:240px;cursor:crosshair;}
|
||||
.map-picker-hint{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--text-muted);margin-top:6px;}
|
||||
.map-geocoding{font-size:11px;color:var(--text-muted);margin-top:4px;min-height:16px;font-style:italic;}
|
||||
</style>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<div class="pd-filter-bar">
|
||||
<input type="text" class="pd-input" id="pdSearch" placeholder="Cari nama KK..." oninput="applyFilter()">
|
||||
<div class="pd-filter-chips">
|
||||
<select class="pd-chip" id="pdKat" onchange="applyFilter()">
|
||||
<option value="">Semua Kategori</option>
|
||||
<option value="Sangat Miskin">Sangat Miskin</option>
|
||||
<option value="Miskin">Miskin</option>
|
||||
<option value="Hampir Miskin">Hampir Miskin</option>
|
||||
</select>
|
||||
<select class="pd-chip" id="pdStatus" onchange="applyFilter()">
|
||||
<option value="">Semua Status</option>
|
||||
<option value="Belum Ditangani">Belum Ditangani</option>
|
||||
<option value="Dalam Proses">Dalam Proses</option>
|
||||
<option value="Sudah Ditangani">Sudah Ditangani</option>
|
||||
</select>
|
||||
<select class="pd-chip" id="pdBlank" onchange="applyFilter()">
|
||||
<option value="">Semua Spot</option>
|
||||
<option value="1">Blank Spot</option>
|
||||
<option value="0">Terjangkau</option>
|
||||
</select>
|
||||
<select class="pd-chip" id="pdVerif" onchange="applyFilter()">
|
||||
<option value="">Semua Verifikasi</option>
|
||||
<option value="Pending">Pending</option>
|
||||
<option value="Terverifikasi">Terverifikasi</option>
|
||||
<option value="Ditolak">Ditolak</option>
|
||||
</select>
|
||||
<div style="width:1px;height:20px;background:var(--card-border);margin:0 2px;align-self:center;flex-shrink:0;"></div>
|
||||
<select class="pd-chip" id="pdSort" onchange="applyFilter()">
|
||||
<option value="">Urutkan</option>
|
||||
<option value="nama_asc">Nama A→Z</option>
|
||||
<option value="nama_desc">Nama Z→A</option>
|
||||
<option value="jiwa_desc">Jiwa Terbanyak</option>
|
||||
<option value="jiwa_asc">Jiwa Tersedikit</option>
|
||||
<option value="jarak_asc">Jarak Terdekat</option>
|
||||
<option value="jarak_desc">Jarak Terjauh</option>
|
||||
</select>
|
||||
<div class="pd-chip-spacer"></div>
|
||||
<a href="map.php" class="btn-see-all" style="text-decoration:none;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;display:inline-flex;align-items:center;gap:4px;white-space:nowrap;"><i data-lucide="map"></i> Peta</a>
|
||||
<button class="btn-primary" onclick="openAddPd()" style="white-space:nowrap;">+ Tambah Penduduk</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<div class="table-card-header">
|
||||
<div>
|
||||
<div class="table-card-title">Daftar Penduduk Miskin</div>
|
||||
<div class="table-card-count" id="pdCount"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px;">No</th>
|
||||
<th>Nama KK</th>
|
||||
<th>Kategori</th>
|
||||
<th style="text-align:right;">Jiwa</th>
|
||||
<th>Rumah Ibadah</th>
|
||||
<th>Status Bantuan</th>
|
||||
<th>Keterjangkauan</th>
|
||||
<th>Verifikasi</th>
|
||||
<th style="width:100px;text-align:center;">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pdTbody">
|
||||
<tr><td colspan="8" style="text-align:center;padding:28px;color:var(--text-muted);"><span class="spinner"></span> Memuat...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<span id="pd-info"></span>
|
||||
<div class="page-btns" id="pd-pages"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Riwayat Modal -->
|
||||
<div class="modal-dark" id="riwayatModal">
|
||||
<div class="modal-box">
|
||||
<h3 id="riwayatTitle">Riwayat Bantuan</h3>
|
||||
<div class="modal-scroll" id="riwayatBody" style="font-size:13px;"></div>
|
||||
<div style="margin-top:14px;flex-shrink:0;">
|
||||
<button style="padding:8px 16px;background:transparent;border:1px solid var(--card-border);border-radius:8px;font-family:var(--font);font-size:13px;font-weight:600;color:var(--text-secondary);cursor:pointer;" onclick="document.getElementById('riwayatModal').classList.remove('show')">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const PD_PER = 15;
|
||||
let _pdAll = [], _pdFiltered = [], _pdPage = 1;
|
||||
|
||||
function esc(s) { const d = document.createElement('div'); d.appendChild(document.createTextNode(s ?? '')); return d.innerHTML; }
|
||||
function attr(s) { return String(s ?? '').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>'); }
|
||||
function fmt(n) { return Number(n || 0).toLocaleString('id-ID'); }
|
||||
|
||||
async function loadPd() {
|
||||
try {
|
||||
const r = await fetch('../api/penduduk/ambil.php?_=' + Date.now());
|
||||
const j = await r.json();
|
||||
if (j.status !== 'success') return;
|
||||
// Operator: filter to own ibadah only
|
||||
_pdAll = (window._IBADAH_ID !== null && !window._IS_ADMIN)
|
||||
? j.data.filter(p => parseInt(p.ibadah_id) === window._IBADAH_ID)
|
||||
: j.data;
|
||||
document.getElementById('pdCount').textContent = _pdAll.length + ' penduduk';
|
||||
applyFilter();
|
||||
} catch(e) {
|
||||
document.getElementById('pdTbody').innerHTML =
|
||||
'<tr><td colspan="8" style="text-align:center;padding:24px;color:#ef4444;">Gagal memuat data.</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
const q = document.getElementById('pdSearch').value.toLowerCase();
|
||||
const kat = document.getElementById('pdKat').value;
|
||||
const stat = document.getElementById('pdStatus').value;
|
||||
const blank = document.getElementById('pdBlank').value;
|
||||
const verif = document.getElementById('pdVerif').value;
|
||||
const sort = document.getElementById('pdSort').value;
|
||||
_pdFiltered = _pdAll.filter(p =>
|
||||
(!q || (p.nama_kk || '').toLowerCase().includes(q)) &&
|
||||
(!kat || p.kategori === kat) &&
|
||||
(!stat || p.status_bantuan === stat) &&
|
||||
(blank === '' || String(p.is_blank_spot) === blank) &&
|
||||
(!verif || (p.status_verifikasi || 'Pending') === verif)
|
||||
);
|
||||
if (sort) {
|
||||
_pdFiltered.sort((a, b) => {
|
||||
switch (sort) {
|
||||
case 'nama_asc': return (a.nama_kk||'').localeCompare(b.nama_kk||'', 'id');
|
||||
case 'nama_desc': return (b.nama_kk||'').localeCompare(a.nama_kk||'', 'id');
|
||||
case 'jiwa_desc': return (b.jumlah_jiwa||0) - (a.jumlah_jiwa||0);
|
||||
case 'jiwa_asc': return (a.jumlah_jiwa||0) - (b.jumlah_jiwa||0);
|
||||
case 'jarak_asc': return (a.jarak_m||0) - (b.jarak_m||0);
|
||||
case 'jarak_desc': return (b.jarak_m||0) - (a.jarak_m||0);
|
||||
default: return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
_pdPage = 1;
|
||||
renderPage();
|
||||
}
|
||||
|
||||
function renderPage() {
|
||||
const start = (_pdPage - 1) * PD_PER;
|
||||
const slice = _pdFiltered.slice(start, start + PD_PER);
|
||||
const pages = Math.ceil(_pdFiltered.length / PD_PER) || 1;
|
||||
const tbody = document.getElementById('pdTbody');
|
||||
|
||||
const katCls = { 'Sangat Miskin': 'kat-sm', 'Miskin': 'kat-m', 'Hampir Miskin': 'kat-hm' };
|
||||
const statCls = { 'Belum Ditangani': 'belum', 'Dalam Proses': 'proses', 'Sudah Ditangani': 'sudah' };
|
||||
|
||||
if (!slice.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;padding:24px;color:var(--text-muted);">Tidak ada data.</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = slice.map((p, i) => {
|
||||
const ibadahText = p.nama_ibadah
|
||||
? `${esc(p.nama_ibadah)}<br><small style="color:var(--text-muted);">${fmt(Math.round(p.jarak_m || 0))} m</small>`
|
||||
: '<span style="color:var(--text-muted);">—</span>';
|
||||
const nonaktifBtn = window._IS_ADMIN
|
||||
? `<button class="tbl-action-btn" title="Nonaktifkan" style="color:#d97706;" data-id="${attr(parseInt(p.id, 10) || 0)}" onclick="doNonaktifkan(this.dataset.id)"><i data-lucide="ban"></i></button>`
|
||||
: '';
|
||||
const sc = statCls[p.status_bantuan] || '';
|
||||
const sv = p.status_verifikasi || 'Pending';
|
||||
const isVerified = sv === 'Terverifikasi';
|
||||
const svCls = { Terverifikasi: 'sv-ok', Pending: 'sv-pending', Ditolak: 'sv-tolak' }[sv] || 'sv-pending';
|
||||
const svIcon = {
|
||||
Terverifikasi: '<i data-lucide="check-circle-2" style="width:12px;height:12px;vertical-align:middle;margin-right:2px;"></i>',
|
||||
Pending: '<i data-lucide="clock" style="width:12px;height:12px;vertical-align:middle;margin-right:2px;"></i>',
|
||||
Ditolak: '<i data-lucide="x-circle" style="width:12px;height:12px;vertical-align:middle;margin-right:2px;"></i>'
|
||||
}[sv] || '';
|
||||
const verifBtns = (window._IS_ADMIN && sv === 'Pending')
|
||||
? `<div style="display:flex;gap:4px;margin-top:4px;">
|
||||
<button class="btn-approve" data-id="${attr(parseInt(p.id,10)||0)}" onclick="doVerifRow(this,'approve')"><i data-lucide="check" style="width:10px;height:10px;"></i></button>
|
||||
<button class="btn-reject" data-id="${attr(parseInt(p.id,10)||0)}" onclick="doVerifRow(this,'reject')"><i data-lucide="x" style="width:10px;height:10px;"></i></button>
|
||||
</div>` : '';
|
||||
const statusControl = isVerified
|
||||
? `<select class="stat-sel ${esc(sc)}"
|
||||
onchange="updateStatus(${p.id}, this)"
|
||||
data-orig="${esc(p.status_bantuan || '')}">
|
||||
<option ${p.status_bantuan === 'Belum Ditangani' ? 'selected' : ''}>Belum Ditangani</option>
|
||||
<option ${p.status_bantuan === 'Dalam Proses' ? 'selected' : ''}>Dalam Proses</option>
|
||||
<option ${p.status_bantuan === 'Sudah Ditangani' ? 'selected' : ''}>Sudah Ditangani</option>
|
||||
</select>`
|
||||
: `<div>
|
||||
<select class="stat-sel ${esc(sc)}" disabled data-orig="${esc(p.status_bantuan || '')}">
|
||||
<option selected>${esc(p.status_bantuan || 'Belum Ditangani')}</option>
|
||||
</select>
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">Menunggu verifikasi admin</div>
|
||||
</div>`;
|
||||
return `<tr>
|
||||
<td style="color:var(--text-muted);font-size:12px;">${start + i + 1}</td>
|
||||
<td style="font-weight:600;">${esc(p.nama_kk || '—')}</td>
|
||||
<td><span class="kat-badge ${esc(katCls[p.kategori] || '')}">${esc(p.kategori || '—')}</span></td>
|
||||
<td style="text-align:right;font-family:var(--font-mono);">${esc(String(p.jumlah_jiwa || 0))}</td>
|
||||
<td style="font-size:12px;">${ibadahText}</td>
|
||||
<td>
|
||||
${statusControl}
|
||||
</td>
|
||||
<td><span class="bs-badge ${p.is_blank_spot ? 'bs-blank' : 'bs-cover'}">${p.is_blank_spot ? 'Blank Spot' : 'Terjangkau'}</span></td>
|
||||
<td>
|
||||
<span class="sv-badge ${svCls}">${svIcon}${sv}</span>
|
||||
${verifBtns}
|
||||
</td>
|
||||
<td style="text-align:center;display:flex;align-items:center;justify-content:center;gap:4px;">
|
||||
<button class="tbl-action-btn" title="Edit Data" style="color:#d97706;" data-id="${attr(parseInt(p.id,10)||0)}" onclick="openEditPd(parseInt(this.dataset.id,10))"><i data-lucide="pencil"></i></button>
|
||||
<button class="tbl-action-btn" title="Riwayat Bantuan" data-id="${attr(parseInt(p.id, 10) || 0)}" data-nama="${attr(p.nama_kk || '?')}" onclick="loadRiwayat(this.dataset.id, this.dataset.nama)"><i data-lucide="clipboard-list"></i></button>
|
||||
${isVerified ? `<button class="tbl-action-btn" title="Kelola Kebutuhan" style="color:#0d7490;" data-id="${attr(parseInt(p.id,10)||0)}" data-nama="${attr(p.nama_kk||'?')}" onclick="openKebPd(parseInt(this.dataset.id,10), this.dataset.nama)"><i data-lucide="package"></i></button>` : ''}
|
||||
${nonaktifBtn}
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const end = Math.min(start + PD_PER, _pdFiltered.length);
|
||||
document.getElementById('pd-info').textContent = _pdFiltered.length > 0
|
||||
? `Menampilkan ${start + 1}–${end} dari ${_pdFiltered.length}`
|
||||
: '';
|
||||
|
||||
let btns = `<button class="page-btn" onclick="goPage(${_pdPage - 1})" ${_pdPage === 1 ? 'disabled' : ''}><i data-lucide="chevron-left"></i></button>`;
|
||||
for (let p = 1; p <= pages; p++) {
|
||||
btns += `<button class="page-btn ${p === _pdPage ? 'active' : ''}" onclick="goPage(${p})">${p}</button>`;
|
||||
}
|
||||
btns += `<button class="page-btn" onclick="goPage(${_pdPage + 1})" ${_pdPage === pages ? 'disabled' : ''}><i data-lucide="chevron-right"></i></button>`;
|
||||
document.getElementById('pd-pages').innerHTML = btns;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
function goPage(p) {
|
||||
const pages = Math.ceil(_pdFiltered.length / PD_PER) || 1;
|
||||
if (p < 1 || p > pages) return;
|
||||
_pdPage = p;
|
||||
renderPage();
|
||||
}
|
||||
|
||||
async function updateStatus(id, sel) {
|
||||
const newStatus = sel.value;
|
||||
const orig = sel.dataset.orig;
|
||||
const item = _pdAll.find(p => String(p.id) === String(id));
|
||||
if (item && (item.status_verifikasi || 'Pending') !== 'Terverifikasi') {
|
||||
alert('Menunggu verifikasi admin');
|
||||
sel.value = orig;
|
||||
return;
|
||||
}
|
||||
const fd = new FormData();
|
||||
fd.append('id', id);
|
||||
fd.append('status', newStatus);
|
||||
try {
|
||||
const r = await fetch('../api/penduduk/update_status.php', { method: 'POST', body: appendCsrf(fd) });
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
sel.dataset.orig = newStatus;
|
||||
const statCls = { 'Belum Ditangani': 'belum', 'Dalam Proses': 'proses', 'Sudah Ditangani': 'sudah' };
|
||||
sel.className = 'stat-sel ' + (statCls[newStatus] || '');
|
||||
// Update local data so filter stays consistent
|
||||
if (item) item.status_bantuan = newStatus;
|
||||
} else {
|
||||
alert(j.message || 'Gagal update status.');
|
||||
sel.value = orig;
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Gagal terhubung ke server.');
|
||||
sel.value = orig;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRiwayat(id, nama) {
|
||||
document.getElementById('riwayatTitle').textContent = 'Riwayat Bantuan — ' + nama;
|
||||
document.getElementById('riwayatBody').innerHTML = '<span class="spinner"></span> Memuat...';
|
||||
document.getElementById('riwayatModal').classList.add('show');
|
||||
try {
|
||||
const r = await fetch('../api/penduduk/riwayat.php?id=' + id + '&_=' + Date.now());
|
||||
const j = await r.json();
|
||||
if (!j.data || !j.data.length) {
|
||||
document.getElementById('riwayatBody').innerHTML = '<p style="color:var(--text-muted);display:inline-flex;align-items:center;gap:6px;"><i data-lucide="info"></i> Belum ada riwayat perubahan status.</p>';
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
document.getElementById('riwayatBody').innerHTML =
|
||||
'<table class="admin-table" style="font-size:12px;">' +
|
||||
'<thead><tr><th>Waktu</th><th>Oleh</th><th>Perubahan</th><th>Catatan</th></tr></thead>' +
|
||||
'<tbody>' + j.data.map(rv => `<tr>
|
||||
<td style="white-space:nowrap;color:var(--text-muted);">${esc(rv.created_at ? rv.created_at.slice(0, 16) : '—')}</td>
|
||||
<td>${esc(rv.user_nama || '—')}</td>
|
||||
<td style="white-space:nowrap;"><span style="color:var(--text-muted);">${esc(rv.status_lama || '—')}</span> → <strong>${esc(rv.status_baru || '—')}</strong></td>
|
||||
<td style="color:var(--text-muted);">${esc(rv.catatan || '—')}</td>
|
||||
</tr>`).join('') + '</tbody></table>';
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
} catch(e) {
|
||||
document.getElementById('riwayatBody').innerHTML = '<span style="color:#ef4444;display:inline-flex;align-items:center;gap:6px;"><i data-lucide="x-circle"></i> Gagal memuat riwayat.</span>';
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
function showConfirm({ title, message, confirmLabel = 'Ya, Lanjutkan', cancelLabel = 'Batal', danger = false }) {
|
||||
return new Promise(resolve => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'confirm-overlay';
|
||||
el.innerHTML = `<div class="confirm-box">
|
||||
<div class="confirm-title">${title}</div>
|
||||
<p class="confirm-msg">${message}</p>
|
||||
<div class="confirm-actions">
|
||||
<button class="btn-secondary" id="_sc_cancel">${cancelLabel}</button>
|
||||
<button class="${danger ? 'btn-danger' : 'btn-primary'}" id="_sc_ok">${confirmLabel}</button>
|
||||
</div></div>`;
|
||||
document.body.appendChild(el);
|
||||
const done = val => { el.remove(); resolve(val); };
|
||||
el.querySelector('#_sc_ok').onclick = () => done(true);
|
||||
el.querySelector('#_sc_cancel').onclick = () => done(false);
|
||||
el.addEventListener('click', e => { if (e.target === el) done(false); });
|
||||
});
|
||||
}
|
||||
|
||||
async function doNonaktifkan(id) {
|
||||
const ok = await showConfirm({
|
||||
title: 'Nonaktifkan Warga',
|
||||
message: 'Tandai warga ini sebagai tidak aktif (meninggal/pindah)? Data akan diarsipkan.',
|
||||
confirmLabel: 'Ya, Nonaktifkan', danger: true
|
||||
});
|
||||
if (!ok) return;
|
||||
const fd = new FormData();
|
||||
fd.append('id', id);
|
||||
try {
|
||||
const r = await fetch('../api/penduduk/nonaktifkan.php', { method: 'POST', body: appendCsrf(fd) });
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
loadPd();
|
||||
} else {
|
||||
alert(j.message || 'Gagal menonaktifkan warga.');
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Gagal terhubung ke server.');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Verifikasi langsung dari tabel ────────────────────────────
|
||||
async function doVerifRow(btn, aksi) {
|
||||
const id = btn.dataset.id;
|
||||
const item = _pdAll.find(p => String(p.id) === String(id));
|
||||
const label = aksi === 'approve' ? 'Terverifikasi' : 'Ditolak';
|
||||
const nama = item?.nama_kk ? `"${item.nama_kk}"` : `#${id}`;
|
||||
const ok = await showConfirm({
|
||||
title: aksi === 'approve' ? 'Konfirmasi Verifikasi' : 'Konfirmasi Tolak',
|
||||
message: `Ubah status verifikasi ${nama} menjadi <strong>${label}</strong>?`,
|
||||
confirmLabel: aksi === 'approve' ? 'Ya, Verifikasi' : 'Ya, Tolak',
|
||||
danger: aksi === 'reject'
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
btn.disabled = true;
|
||||
const fd = new FormData();
|
||||
fd.append('id', id); fd.append('aksi', aksi);
|
||||
try {
|
||||
const r = await fetch('../api/penduduk/verifikasi.php', { method:'POST', body: appendCsrf(fd) });
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
if (item) item.status_verifikasi = j.status_verifikasi;
|
||||
applyFilter();
|
||||
} else { alert(j.message || 'Gagal.'); btn.disabled = false; }
|
||||
} catch { alert('Gagal terhubung.'); btn.disabled = false; }
|
||||
}
|
||||
|
||||
// ── Map Picker untuk form tambah ──────────────────────────────
|
||||
const PD_DEF_LAT = <?= CENTER_LAT ?>;
|
||||
const PD_DEF_LNG = <?= CENTER_LNG ?>;
|
||||
const PD_DEF_ZOOM = <?= ZOOM_LEVEL ?>;
|
||||
let _pdMap = null, _pdMarker = null, _pdGeoTimer = null;
|
||||
|
||||
function initPdMap() {
|
||||
if (_pdMap) { _pdMap.remove(); _pdMap = null; _pdMarker = null; }
|
||||
_pdMap = L.map('pdMapPicker').setView([PD_DEF_LAT, PD_DEF_LNG], PD_DEF_ZOOM);
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom:19, attribution:'© OpenStreetMap'
|
||||
}).addTo(_pdMap);
|
||||
_pdMap.on('click', e => { pdPlaceMarker(e.latlng.lat, e.latlng.lng); pdFillCoords(e.latlng.lat, e.latlng.lng); pdGeocode(e.latlng.lat, e.latlng.lng); });
|
||||
setTimeout(() => _pdMap && _pdMap.invalidateSize(), 80);
|
||||
}
|
||||
|
||||
function pdPlaceMarker(lat, lng) {
|
||||
if (_pdMarker) { _pdMarker.setLatLng([lat, lng]); }
|
||||
else {
|
||||
_pdMarker = L.marker([lat, lng], { draggable: true }).addTo(_pdMap);
|
||||
_pdMarker.on('dragend', e => {
|
||||
const ll = e.target.getLatLng();
|
||||
pdFillCoords(ll.lat, ll.lng);
|
||||
pdGeocode(ll.lat, ll.lng);
|
||||
});
|
||||
}
|
||||
_pdMap.panTo([lat, lng]);
|
||||
}
|
||||
|
||||
function pdFillCoords(lat, lng) {
|
||||
document.getElementById('pdLat').value = lat.toFixed(8);
|
||||
document.getElementById('pdLng').value = lng.toFixed(8);
|
||||
}
|
||||
|
||||
function pdCoordsManual() {
|
||||
const lat = parseFloat(document.getElementById('pdLat').value);
|
||||
const lng = parseFloat(document.getElementById('pdLng').value);
|
||||
if (!isNaN(lat) && !isNaN(lng) && _pdMap) { pdPlaceMarker(lat, lng); pdGeocode(lat, lng); }
|
||||
}
|
||||
|
||||
async function pdGeocode(lat, lng) {
|
||||
clearTimeout(_pdGeoTimer);
|
||||
document.getElementById('pdGeoStatus').innerHTML = '<span class="spinner"></span> Mencari alamat...';
|
||||
_pdGeoTimer = setTimeout(async () => {
|
||||
try {
|
||||
const r = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json`, { headers:{'Accept-Language':'id'} });
|
||||
const j = await r.json();
|
||||
const alamatEl = document.getElementById('pdAlamat');
|
||||
if (j && j.display_name && !alamatEl.dataset.userEdited) {
|
||||
alamatEl.value = j.display_name;
|
||||
}
|
||||
document.getElementById('pdGeoStatus').innerHTML = j.display_name
|
||||
? '<i data-lucide="check" style="color:var(--text-success);margin-right:4px;"></i> ' + esc(j.address?.road || j.address?.suburb || j.display_name.split(',')[0])
|
||||
: '';
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
} catch { document.getElementById('pdGeoStatus').textContent = ''; }
|
||||
}, 600);
|
||||
}
|
||||
|
||||
function openAddPd() {
|
||||
['pdNama','pdNik','pdAlamat','pdCatatan','pdLat','pdLng'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
el.value = '';
|
||||
delete el.dataset.userEdited;
|
||||
});
|
||||
document.getElementById('pdJiwa').value = 1;
|
||||
document.getElementById('pdKategori').value = 'Miskin';
|
||||
document.getElementById('pdMsg').textContent = '';
|
||||
document.getElementById('pdGeoStatus').textContent = '';
|
||||
document.getElementById('pdSaveBtn').disabled = false;
|
||||
document.getElementById('pdSaveBtn').textContent = 'Simpan';
|
||||
document.getElementById('pdModal').classList.add('show');
|
||||
initPdMap();
|
||||
}
|
||||
|
||||
function closeAddPd() {
|
||||
document.getElementById('pdModal').classList.remove('show');
|
||||
clearTimeout(_pdGeoTimer);
|
||||
if (_pdMap) { _pdMap.remove(); _pdMap = null; _pdMarker = null; }
|
||||
}
|
||||
|
||||
async function savePd() {
|
||||
const btn = document.getElementById('pdSaveBtn');
|
||||
const msg = document.getElementById('pdMsg');
|
||||
const lat = document.getElementById('pdLat').value.trim();
|
||||
const lng = document.getElementById('pdLng').value.trim();
|
||||
const nama = document.getElementById('pdNama').value.trim();
|
||||
|
||||
if (!nama) { msg.textContent = 'Nama Kepala Keluarga wajib diisi.'; return; }
|
||||
if (!lat || !lng) { msg.textContent = 'Klik peta untuk menentukan lokasi.'; return; }
|
||||
|
||||
btn.disabled = true; btn.innerHTML = '<span class="spinner"></span> Menyimpan...'; msg.textContent = '';
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('nama_kk', nama);
|
||||
fd.append('nik', document.getElementById('pdNik').value.trim());
|
||||
fd.append('jumlah_jiwa',document.getElementById('pdJiwa').value);
|
||||
fd.append('kategori', document.getElementById('pdKategori').value);
|
||||
fd.append('alamat', document.getElementById('pdAlamat').value.trim());
|
||||
fd.append('catatan', document.getElementById('pdCatatan').value.trim());
|
||||
fd.append('lat', lat); fd.append('lng', lng);
|
||||
|
||||
try {
|
||||
const r = await fetch('../api/penduduk/simpan.php', { method:'POST', body: appendCsrf(fd) });
|
||||
const j = await r.json();
|
||||
btn.disabled = false; btn.innerHTML = 'Simpan';
|
||||
if (j.status === 'success') {
|
||||
closeAddPd();
|
||||
loadPd();
|
||||
} else { msg.textContent = j.message || 'Terjadi kesalahan.'; }
|
||||
} catch { btn.disabled = false; btn.innerHTML = 'Simpan'; msg.textContent = 'Gagal terhubung ke server.'; }
|
||||
}
|
||||
|
||||
// ── Edit data penduduk ────────────────────────────────────────
|
||||
function openEditPd(id) {
|
||||
const p = _pdAll.find(x => String(x.id) === String(id));
|
||||
if (!p) return;
|
||||
document.getElementById('editPdId').value = p.id;
|
||||
document.getElementById('editPdNama').value = p.nama_kk || '';
|
||||
document.getElementById('editPdNik').value = p.nik || '';
|
||||
document.getElementById('editPdJiwa').value = p.jumlah_jiwa || 1;
|
||||
document.getElementById('editPdKategori').value = p.kategori || 'Miskin';
|
||||
document.getElementById('editPdAlamat').value = p.alamat || '';
|
||||
document.getElementById('editPdCatatan').value = p.catatan || '';
|
||||
document.getElementById('editPdMsg').textContent = '';
|
||||
document.getElementById('editPdSaveBtn').disabled = false;
|
||||
document.getElementById('editPdSaveBtn').textContent = 'Simpan Perubahan';
|
||||
document.getElementById('editPdModal').classList.add('show');
|
||||
}
|
||||
|
||||
function closeEditPd() {
|
||||
document.getElementById('editPdModal').classList.remove('show');
|
||||
}
|
||||
|
||||
async function saveEditPd() {
|
||||
const btn = document.getElementById('editPdSaveBtn');
|
||||
const msg = document.getElementById('editPdMsg');
|
||||
const nama = document.getElementById('editPdNama').value.trim();
|
||||
if (!nama) { msg.textContent = 'Nama Kepala Keluarga wajib diisi.'; return; }
|
||||
|
||||
btn.disabled = true; btn.innerHTML = '<span class="spinner"></span> Menyimpan...'; msg.textContent = '';
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('id', document.getElementById('editPdId').value);
|
||||
fd.append('nama_kk', nama);
|
||||
fd.append('nik', document.getElementById('editPdNik').value.trim());
|
||||
fd.append('jumlah_jiwa', document.getElementById('editPdJiwa').value);
|
||||
fd.append('kategori', document.getElementById('editPdKategori').value);
|
||||
fd.append('alamat', document.getElementById('editPdAlamat').value.trim());
|
||||
fd.append('catatan', document.getElementById('editPdCatatan').value.trim());
|
||||
|
||||
try {
|
||||
const r = await fetch('../api/penduduk/update.php', { method: 'POST', body: appendCsrf(fd) });
|
||||
const j = await r.json();
|
||||
btn.disabled = false; btn.textContent = 'Simpan Perubahan';
|
||||
if (j.status === 'success') {
|
||||
closeEditPd();
|
||||
loadPd();
|
||||
} else {
|
||||
msg.textContent = j.message || 'Terjadi kesalahan.';
|
||||
}
|
||||
} catch {
|
||||
btn.disabled = false; btn.textContent = 'Simpan Perubahan';
|
||||
msg.textContent = 'Gagal terhubung ke server.';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Kelola Kebutuhan dari tabel ───────────────────────────────
|
||||
const KEB_KAT_LIST = ['Sembako','Biaya Sekolah','Biaya Kesehatan','Modal Usaha','Renovasi Rumah','Perlengkapan Rumah','Pakaian','Lainnya'];
|
||||
|
||||
async function openKebPd(id, nama) {
|
||||
document.getElementById('kebPdTitle').textContent = 'Kebutuhan — ' + nama;
|
||||
document.getElementById('kebPdBody').innerHTML = '<span class="spinner"></span> Memuat...';
|
||||
document.getElementById('kebPdModal').classList.add('show');
|
||||
await renderKebPd(id);
|
||||
}
|
||||
|
||||
async function renderKebPd(id) {
|
||||
const clsMap = {'Belum Terpenuhi':'belum','Dalam Proses':'proses','Terpenuhi':'terpenuhi'};
|
||||
try {
|
||||
const r = await fetch('../api/kebutuhan/ambil.php?penduduk_id='+id+'&_='+Date.now());
|
||||
const j = await r.json();
|
||||
|
||||
const listHtml = (!j.data || !j.data.length)
|
||||
? '<p style="color:var(--text-muted);margin-bottom:4px;">Belum ada kebutuhan tercatat.</p>'
|
||||
: j.data.map(k => `
|
||||
<div style="padding:10px 0;border-bottom:1px solid var(--card-border);">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;">
|
||||
<span style="font-weight:600;">${esc(k.kategori)}</span>
|
||||
<select class="stat-sel ${clsMap[k.status]||''}"
|
||||
data-id="${attr(parseInt(k.id,10)||0)}" data-orig="${attr(k.status||'')}" onchange="updateKebFromPd(this)">
|
||||
<option ${k.status==='Belum Terpenuhi'?'selected':''}>Belum Terpenuhi</option>
|
||||
<option ${k.status==='Dalam Proses'?'selected':''}>Dalam Proses</option>
|
||||
<option ${k.status==='Terpenuhi'?'selected':''}>Terpenuhi</option>
|
||||
</select>
|
||||
</div>
|
||||
${k.deskripsi?`<div style="font-size:12px;color:var(--text-muted);">${esc(k.deskripsi)}</div>`:''}
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:3px;">Oleh ${esc(k.created_by_nama||'—')} · ${esc((k.created_at||'').slice(0,10))}</div>
|
||||
</div>`).join('');
|
||||
|
||||
const addFormHtml = `
|
||||
<div style="margin-top:14px;padding-top:14px;border-top:2px dashed var(--card-border);">
|
||||
<div style="font-size:11px;font-weight:700;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px;">Tambah Kebutuhan</div>
|
||||
<select id="kebPdKat_${id}" style="width:100%;padding:8px 10px;border:1px solid var(--card-border);border-radius:7px;font-family:var(--font);font-size:13px;background:#fafaf9;color:var(--text-primary);outline:none;margin-bottom:8px;">
|
||||
${KEB_KAT_LIST.map(k=>`<option value="${esc(k)}">${esc(k)}</option>`).join('')}
|
||||
</select>
|
||||
<input type="text" id="kebPdDesc_${id}" placeholder="Deskripsi (opsional, maks 300 karakter)" maxlength="300"
|
||||
style="width:100%;padding:8px 10px;border:1px solid var(--card-border);border-radius:7px;font-family:var(--font);font-size:13px;background:#fafaf9;color:var(--text-primary);outline:none;margin-bottom:8px;box-sizing:border-box;">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<button onclick="addKebFromPd(${id})" id="kebPdAddBtn_${id}" class="btn-primary" style="padding:7px 18px;font-size:13px;">Simpan</button>
|
||||
<span id="kebPdAddMsg_${id}" style="font-size:12px;color:#ef4444;"></span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.getElementById('kebPdBody').innerHTML = listHtml + addFormHtml;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
} catch(e) {
|
||||
document.getElementById('kebPdBody').innerHTML = '<span style="color:#ef4444;">Gagal memuat kebutuhan.</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function updateKebFromPd(sel) {
|
||||
const id = sel.dataset.id;
|
||||
const orig = sel.dataset.orig;
|
||||
const catatan = prompt('Catatan perubahan kebutuhan (opsional):', '');
|
||||
if (catatan === null) { sel.value = orig; return; }
|
||||
const fd = new FormData();
|
||||
fd.append('id', id); fd.append('status', sel.value); fd.append('catatan', catatan);
|
||||
try {
|
||||
const r = await fetch('../api/kebutuhan/update_status.php', {method:'POST', body: appendCsrf(fd)});
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
sel.dataset.orig = sel.value;
|
||||
const clsMap = {'Belum Terpenuhi':'belum','Dalam Proses':'proses','Terpenuhi':'terpenuhi'};
|
||||
sel.className = 'stat-sel ' + (clsMap[sel.value]||'');
|
||||
} else { alert(j.message||'Gagal update.'); sel.value = orig; }
|
||||
} catch(e) { alert('Gagal terhubung ke server.'); sel.value = orig; }
|
||||
}
|
||||
|
||||
async function addKebFromPd(id) {
|
||||
const kat = document.getElementById(`kebPdKat_${id}`).value;
|
||||
const desc = document.getElementById(`kebPdDesc_${id}`).value.trim();
|
||||
const btn = document.getElementById(`kebPdAddBtn_${id}`);
|
||||
const msg = document.getElementById(`kebPdAddMsg_${id}`);
|
||||
btn.disabled = true; btn.textContent = 'Menyimpan...'; msg.textContent = '';
|
||||
const fd = new FormData();
|
||||
fd.append('penduduk_id', id); fd.append('kategori', kat); fd.append('deskripsi', desc);
|
||||
try {
|
||||
const r = await fetch('../api/kebutuhan/simpan.php', {method:'POST', body: appendCsrf(fd)});
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
await renderKebPd(id);
|
||||
} else {
|
||||
msg.textContent = j.message || 'Gagal menyimpan.';
|
||||
btn.disabled = false; btn.textContent = 'Simpan';
|
||||
}
|
||||
} catch(e) {
|
||||
msg.textContent = 'Gagal terhubung ke server.';
|
||||
btn.disabled = false; btn.textContent = 'Simpan';
|
||||
}
|
||||
}
|
||||
|
||||
loadPd();
|
||||
</script>
|
||||
|
||||
<!-- Modal Edit Penduduk -->
|
||||
<div class="modal-dark" id="editPdModal">
|
||||
<div class="modal-box">
|
||||
<h3>Edit Data Penduduk</h3>
|
||||
<div class="modal-scroll">
|
||||
<input type="hidden" id="editPdId">
|
||||
<div class="mfg">
|
||||
<label>Nama Kepala Keluarga <span style="color:#ef4444;">*</span></label>
|
||||
<input type="text" id="editPdNama" placeholder="Nama lengkap KK">
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||
<div class="mfg">
|
||||
<label>NIK <small style="text-transform:none;">(opsional)</small></label>
|
||||
<input type="text" id="editPdNik" placeholder="16 digit" maxlength="16">
|
||||
</div>
|
||||
<div class="mfg">
|
||||
<label>Jumlah Jiwa <small style="text-transform:none;">(orang)</small></label>
|
||||
<input type="number" id="editPdJiwa" value="1" min="1" max="30">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mfg">
|
||||
<label>Kategori Kemiskinan</label>
|
||||
<select id="editPdKategori">
|
||||
<option value="Sangat Miskin">Sangat Miskin</option>
|
||||
<option value="Miskin" selected>Miskin</option>
|
||||
<option value="Hampir Miskin">Hampir Miskin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mfg">
|
||||
<label>Alamat</label>
|
||||
<input type="text" id="editPdAlamat" placeholder="Alamat lengkap">
|
||||
</div>
|
||||
<div class="mfg">
|
||||
<label>Catatan Kondisi <small style="text-transform:none;">(opsional)</small></label>
|
||||
<textarea id="editPdCatatan" placeholder="Kondisi rumah, kebutuhan khusus, dll..."></textarea>
|
||||
</div>
|
||||
<div class="msg-err" id="editPdMsg"></div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-cancel" onclick="closeEditPd()">Batal</button>
|
||||
<button class="btn-primary" id="editPdSaveBtn" onclick="saveEditPd()">Simpan Perubahan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Kelola Kebutuhan -->
|
||||
<div class="modal-dark" id="kebPdModal">
|
||||
<div class="modal-box">
|
||||
<h3 id="kebPdTitle">Kebutuhan</h3>
|
||||
<div class="modal-scroll" id="kebPdBody" style="font-size:13px;"></div>
|
||||
<div style="margin-top:14px;flex-shrink:0;display:flex;gap:8px;">
|
||||
<button style="padding:8px 16px;background:transparent;border:1px solid var(--card-border);border-radius:8px;font-family:var(--font);font-size:13px;font-weight:600;color:var(--text-secondary);cursor:pointer;" onclick="document.getElementById('kebPdModal').classList.remove('show')">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Tambah Penduduk -->
|
||||
<div class="modal-dark" id="pdModal">
|
||||
<div class="modal-box">
|
||||
<h3>Tambah Penduduk Miskin</h3>
|
||||
<div class="modal-scroll">
|
||||
|
||||
<!-- Peta -->
|
||||
<div class="mfg">
|
||||
<label>Lokasi di Peta <span style="font-weight:400;color:var(--text-muted);text-transform:none;letter-spacing:0;">— klik untuk set titik</span></label>
|
||||
<div class="map-picker-wrap"><div id="pdMapPicker"></div></div>
|
||||
<div class="map-picker-hint" style="display:flex;align-items:center;gap:12px;margin-top:6px;">
|
||||
<span style="display:inline-flex;align-items:center;gap:4px;"><i data-lucide="mouse-pointer"></i> Klik peta</span> · <span style="display:inline-flex;align-items:center;gap:4px;"><i data-lucide="map-pin"></i> Drag marker</span> · <span style="display:inline-flex;align-items:center;gap:4px;"><i data-lucide="zoom-in"></i> Scroll zoom</span>
|
||||
</div>
|
||||
<div class="map-geocoding" id="pdGeoStatus"></div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||
<div class="mfg">
|
||||
<label>Latitude</label>
|
||||
<input type="number" id="pdLat" step="any" placeholder="-0.0557" oninput="pdCoordsManual()">
|
||||
</div>
|
||||
<div class="mfg">
|
||||
<label>Longitude</label>
|
||||
<input type="number" id="pdLng" step="any" placeholder="109.3487" oninput="pdCoordsManual()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Identitas -->
|
||||
<div class="mfg">
|
||||
<label>Nama Kepala Keluarga <span style="color:#ef4444;">*</span></label>
|
||||
<input type="text" id="pdNama" placeholder="Nama lengkap KK">
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||
<div class="mfg">
|
||||
<label>NIK <small style="text-transform:none;">(opsional)</small></label>
|
||||
<input type="text" id="pdNik" placeholder="16 digit" maxlength="16">
|
||||
</div>
|
||||
<div class="mfg">
|
||||
<label>Jumlah Jiwa</label>
|
||||
<input type="number" id="pdJiwa" value="1" min="1" max="30">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mfg">
|
||||
<label>Kategori Kemiskinan</label>
|
||||
<select id="pdKategori">
|
||||
<option value="Sangat Miskin">Sangat Miskin</option>
|
||||
<option value="Miskin" selected>Miskin</option>
|
||||
<option value="Hampir Miskin">Hampir Miskin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mfg">
|
||||
<label>Alamat</label>
|
||||
<input type="text" id="pdAlamat" placeholder="Alamat lengkap"
|
||||
oninput="this.dataset.userEdited='1'">
|
||||
</div>
|
||||
<div class="mfg">
|
||||
<label>Catatan Kondisi <small style="text-transform:none;">(opsional)</small></label>
|
||||
<textarea id="pdCatatan" placeholder="Kondisi rumah, kebutuhan khusus, dll..."></textarea>
|
||||
</div>
|
||||
|
||||
<?php if ($is_admin): ?>
|
||||
<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:10px 14px;font-size:12px;color:#16a34a;margin-bottom:4px;display:inline-flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;">
|
||||
<i data-lucide="check-circle" style="color:#16a34a;flex-shrink:0;"></i> <span>Sebagai Admin, data yang disimpan langsung berstatus <strong>Terverifikasi</strong>.</span>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:10px 14px;font-size:12px;color:#d97706;margin-bottom:4px;display:inline-flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;">
|
||||
<i data-lucide="clock" style="color:#d97706;flex-shrink:0;"></i> <span>Data akan berstatus <strong>Pending</strong> hingga diverifikasi oleh Administrator.</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="msg-err" id="pdMsg"></div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-cancel" onclick="closeAddPd()">Batal</button>
|
||||
<button class="btn-primary" id="pdSaveBtn" onclick="savePd()">Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include '../includes/page-end.php'; ?>
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
require_once '../config.php';
|
||||
require_once '../auth/helper.php';
|
||||
require_once '../koneksi.php';
|
||||
if (!is_logged_in() || !has_role('operator')) { header('Location: ../auth/login.php'); exit; }
|
||||
require_password_changed('../auth/change_password.php');
|
||||
$ibadah_id = get_ibadah_id();
|
||||
$page_title = 'Status Bantuan';
|
||||
$page_subtitle = 'Monitor dan update status penyaluran bantuan per warga';
|
||||
$active_nav = 'status';
|
||||
include '../includes/page-start.php';
|
||||
?>
|
||||
<script>
|
||||
window._IBADAH_ID = <?= $ibadah_id ? (int)$ibadah_id : 'null' ?>;
|
||||
window._IS_ADMIN = <?= has_role('administrator') ? 'true' : 'false' ?>;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.sb-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:24px;}
|
||||
@media(max-width:600px){.sb-cards{grid-template-columns:1fr;}}
|
||||
.stat-sel-sb{padding:5px 10px;border-radius:6px;font-size:12px;font-weight:500;cursor:pointer;border:1px solid var(--card-border);background:#fafaf9;font-family:var(--font);color:var(--text-secondary);}
|
||||
.stat-sel-sb.belum{background:#f5f0eb;color:#7a7067;border-color:#c5bdb5;}
|
||||
.stat-sel-sb.proses{background:#fffbeb;color:#d97706;border-color:#fcd34d;}
|
||||
.stat-sel-sb.sudah{background:#f0fdf4;color:#16a34a;border-color:#86efac;}
|
||||
.text-danger{color:#dc2626;}
|
||||
.text-success{color:#16a34a;}
|
||||
.sb-filter-bar{display:flex;flex-direction:column;gap:8px;margin-bottom:16px;}
|
||||
.sb-input{width:100%;box-sizing:border-box;padding:8px 12px;border:1px solid var(--card-border);border-radius:8px;font-family:var(--font);font-size:13px;background:#fafaf9;color:var(--text-primary);outline:none;}
|
||||
.sb-input:focus{border-color:#0d7490;}
|
||||
.sb-filter-chips{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}
|
||||
.sb-chip{width:auto;height:auto;padding:7px 11px;border:1px solid var(--card-border);border-radius:8px;font-family:var(--font);font-size:13px;background:#fafaf9;color:var(--text-primary);cursor:pointer;white-space:nowrap;}
|
||||
.sb-chip:focus{outline:none;border-color:#0d7490;}
|
||||
</style>
|
||||
|
||||
<div class="sb-cards">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top"><div class="stat-icon amber"><i data-lucide="clipboard-list"></i></div><div class="stat-label">Belum Ditangani</div></div>
|
||||
<div class="stat-value text-danger" id="sb-belum">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top"><div class="stat-icon blue"><i data-lucide="clock"></i></div><div class="stat-label">Dalam Proses</div></div>
|
||||
<div class="stat-value" style="color:#d97706;" id="sb-proses">-</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top"><div class="stat-icon green"><i data-lucide="check-circle-2"></i></div><div class="stat-label">Sudah Ditangani</div></div>
|
||||
<div class="stat-value text-success" id="sb-sudah">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sb-filter-bar">
|
||||
<input type="text" class="sb-input" id="sbSearch" placeholder="Cari nama KK..." oninput="filterSb()">
|
||||
<div class="sb-filter-chips">
|
||||
<select class="sb-chip" id="sbFilter" onchange="filterSb()">
|
||||
<option value="">Semua Status</option>
|
||||
<option value="Belum Ditangani">Belum Ditangani</option>
|
||||
<option value="Dalam Proses">Dalam Proses</option>
|
||||
<option value="Sudah Ditangani">Sudah Ditangani</option>
|
||||
</select>
|
||||
<div style="width:1px;height:20px;background:var(--card-border);margin:0 2px;align-self:center;flex-shrink:0;"></div>
|
||||
<select class="sb-chip" id="sbSort" onchange="filterSb()">
|
||||
<option value="">Urutkan</option>
|
||||
<option value="nama_asc">Nama A→Z</option>
|
||||
<option value="nama_desc">Nama Z→A</option>
|
||||
<option value="jiwa_desc">Jiwa Terbanyak</option>
|
||||
<option value="jiwa_asc">Jiwa Tersedikit</option>
|
||||
<option value="updated_desc">Diupdate Terbaru</option>
|
||||
<option value="updated_asc">Diupdate Terlama</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<div class="table-card-header">
|
||||
<div class="table-card-title">Daftar Penyaluran Bantuan</div>
|
||||
<span class="table-card-count" id="sb-count"></span>
|
||||
</div>
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:44px;">No</th>
|
||||
<th>Nama KK</th>
|
||||
<th style="text-align:right;">Jiwa</th>
|
||||
<th>Ibadah</th>
|
||||
<th>Status</th>
|
||||
<th>Terakhir Diupdate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sbTbody">
|
||||
<tr><td colspan="6" style="text-align:center;padding:28px;color:var(--text-muted);"><span class="spinner"></span> Memuat...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let _sbAll = [];
|
||||
function esc(s){const d=document.createElement('div');d.appendChild(document.createTextNode(s??''));return d.innerHTML;}
|
||||
function attr(s){return String(s??'').replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,''').replace(/</g,'<').replace(/>/g,'>');}
|
||||
function fmt(n){return Number(n||0).toLocaleString('id-ID');}
|
||||
|
||||
async function loadSb() {
|
||||
try {
|
||||
const r = await fetch('../api/penduduk/ambil.php?_='+Date.now());
|
||||
const j = await r.json();
|
||||
if (j.status !== 'success') {
|
||||
document.getElementById('sbTbody').innerHTML =
|
||||
'<tr><td colspan="6" style="text-align:center;padding:24px;color:#ef4444;">'+esc(j.message || 'Gagal memuat data.')+'</td></tr>';
|
||||
return;
|
||||
}
|
||||
const verifiedRows = j.data.filter(p => p.status_verifikasi === 'Terverifikasi');
|
||||
_sbAll = (window._IBADAH_ID && !window._IS_ADMIN)
|
||||
? verifiedRows.filter(p => parseInt(p.ibadah_id) === window._IBADAH_ID)
|
||||
: verifiedRows;
|
||||
updateCards();
|
||||
filterSb();
|
||||
} catch(e) {
|
||||
document.getElementById('sbTbody').innerHTML =
|
||||
'<tr><td colspan="6" style="text-align:center;padding:24px;color:#ef4444;">Gagal memuat data.</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function updateCards() {
|
||||
const b = _sbAll.filter(p => p.status_bantuan === 'Belum Ditangani').length;
|
||||
const pr = _sbAll.filter(p => p.status_bantuan === 'Dalam Proses').length;
|
||||
const s = _sbAll.filter(p => p.status_bantuan === 'Sudah Ditangani').length;
|
||||
document.getElementById('sb-belum').textContent = fmt(b);
|
||||
document.getElementById('sb-proses').textContent = fmt(pr);
|
||||
document.getElementById('sb-sudah').textContent = fmt(s);
|
||||
}
|
||||
|
||||
function filterSb() {
|
||||
const q = document.getElementById('sbSearch').value.toLowerCase();
|
||||
const f = document.getElementById('sbFilter').value;
|
||||
const sort = document.getElementById('sbSort').value;
|
||||
const rows = _sbAll.filter(p =>
|
||||
(!q || (p.nama_kk||'').toLowerCase().includes(q)) &&
|
||||
(!f || p.status_bantuan === f)
|
||||
);
|
||||
if (sort) {
|
||||
rows.sort((a, b) => {
|
||||
switch (sort) {
|
||||
case 'nama_asc': return (a.nama_kk||'').localeCompare(b.nama_kk||'', 'id');
|
||||
case 'nama_desc': return (b.nama_kk||'').localeCompare(a.nama_kk||'', 'id');
|
||||
case 'jiwa_desc': return (b.jumlah_jiwa||0) - (a.jumlah_jiwa||0);
|
||||
case 'jiwa_asc': return (a.jumlah_jiwa||0) - (b.jumlah_jiwa||0);
|
||||
case 'updated_desc': return new Date(b.updated_at||b.created_at||0) - new Date(a.updated_at||a.created_at||0);
|
||||
case 'updated_asc': return new Date(a.updated_at||a.created_at||0) - new Date(b.updated_at||b.created_at||0);
|
||||
default: return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
document.getElementById('sb-count').textContent = rows.length + ' warga';
|
||||
const clsMap = {'Belum Ditangani':'belum','Dalam Proses':'proses','Sudah Ditangani':'sudah'};
|
||||
document.getElementById('sbTbody').innerHTML = rows.length
|
||||
? rows.map((p, i) => `<tr>
|
||||
<td style="color:var(--text-muted);font-size:12px;">${i+1}</td>
|
||||
<td style="font-weight:600;">${esc(p.nama_kk||'—')}</td>
|
||||
<td style="text-align:right;font-family:var(--font-mono);">${esc(String(p.jumlah_jiwa||0))}</td>
|
||||
<td style="font-size:12px;color:var(--text-muted);">${esc(p.nama_ibadah||'—')}</td>
|
||||
<td>
|
||||
<select class="stat-sel-sb ${esc(clsMap[p.status_bantuan]||'')}"
|
||||
data-id="${attr(parseInt(p.id, 10) || 0)}" data-orig="${attr(p.status_bantuan||'')}"
|
||||
onchange="updateStatusSb(this)">
|
||||
<option ${p.status_bantuan==='Belum Ditangani'?'selected':''}>Belum Ditangani</option>
|
||||
<option ${p.status_bantuan==='Dalam Proses'?'selected':''}>Dalam Proses</option>
|
||||
<option ${p.status_bantuan==='Sudah Ditangani'?'selected':''}>Sudah Ditangani</option>
|
||||
</select>
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--text-muted);">${esc((p.updated_at||p.created_at||'').slice(0,10))}</td>
|
||||
</tr>`).join('')
|
||||
: '<tr><td colspan="6" style="text-align:center;padding:24px;color:var(--text-muted);">Tidak ada data.</td></tr>';
|
||||
}
|
||||
|
||||
async function updateStatusSb(sel) {
|
||||
const id = sel.dataset.id;
|
||||
const orig = sel.dataset.orig;
|
||||
const catatan = prompt('Catatan perubahan status (opsional):', '');
|
||||
if (catatan === null) {
|
||||
sel.value = orig;
|
||||
return;
|
||||
}
|
||||
const fd = new FormData();
|
||||
fd.append('id', id); fd.append('status', sel.value); fd.append('catatan', catatan);
|
||||
try {
|
||||
const r = await fetch('../api/penduduk/update_status.php', {method:'POST', body: appendCsrf(fd)});
|
||||
const j = await r.json();
|
||||
if (j.status === 'success') {
|
||||
sel.dataset.orig = sel.value;
|
||||
const clsMap = {'Belum Ditangani':'belum','Dalam Proses':'proses','Sudah Ditangani':'sudah'};
|
||||
sel.className = 'stat-sel-sb ' + (clsMap[sel.value]||'');
|
||||
const item = _sbAll.find(p => String(p.id) === String(id));
|
||||
if (item) item.status_bantuan = sel.value;
|
||||
updateCards();
|
||||
} else {
|
||||
alert(j.message||'Gagal update status.');
|
||||
sel.value = orig;
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Gagal terhubung ke server.');
|
||||
sel.value = orig;
|
||||
}
|
||||
}
|
||||
|
||||
loadSb();
|
||||
</script>
|
||||
|
||||
<?php include '../includes/page-end.php'; ?>
|
||||
@@ -0,0 +1,318 @@
|
||||
<?php
|
||||
require_once '../config.php';
|
||||
require_once '../auth/helper.php';
|
||||
if (!is_logged_in() || !has_role('administrator')) {
|
||||
header('Location: ../auth/login.php'); exit;
|
||||
}
|
||||
require_password_changed('../auth/change_password.php');
|
||||
require_once '../koneksi.php';
|
||||
|
||||
$ibadah_list = [];
|
||||
$res = $conn->query("SELECT id, nama, jenis FROM rumah_ibadah WHERE deleted_at IS NULL ORDER BY nama");
|
||||
while ($row = $res->fetch_assoc()) $ibadah_list[] = $row;
|
||||
|
||||
$page_title = 'Pengguna & Akun';
|
||||
$page_subtitle = 'Kelola akun pengguna sistem';
|
||||
$active_nav = 'users';
|
||||
include '../includes/page-start.php';
|
||||
?>
|
||||
|
||||
<style>
|
||||
.users-content {
|
||||
--c-bg:#f5f0eb; --c-surface:#fafaf9; --c-surface2:#ede8e2;
|
||||
--c-border:#ddd8d2; --c-text:#201515; --c-muted:#7a7067;
|
||||
--c-accent:#0d7490; --c-accent-h:#0a5f7a; --c-danger:#ef4444;
|
||||
--c-info:#3b82f6; --c-warn:#f59e0b;
|
||||
--font-body:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
|
||||
color: var(--c-text);
|
||||
}
|
||||
.users-content .section-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:16px; }
|
||||
.users-content .section-title { font-size:15px; font-weight:600; letter-spacing:-0.3px; color:var(--c-text); }
|
||||
.users-content .btn { padding:8px 16px; border-radius:8px; font-family:var(--font-body); font-size:13px; font-weight:600; cursor:pointer; border:none; transition:background .15s; }
|
||||
.users-content .btn-primary { background:#0d7490; color:#fff; }
|
||||
.users-content .btn-primary:hover { background:#0a5f7a; }
|
||||
.users-content .btn-sm { padding:5px 10px; font-size:12px; border-radius:6px; }
|
||||
.users-content .btn-danger { background:#fef2f2; color:#ef4444; border:1px solid rgba(239,68,68,.3); }
|
||||
.users-content .btn-danger:hover { background:#fee2e2; }
|
||||
.users-content .btn-secondary { background:#ede8e2; color:#3d3530; border:1px solid #ddd8d2; }
|
||||
.users-content .btn-secondary:hover { background:#ddd8d2; }
|
||||
.users-content table { width:100%; border-collapse:collapse; background:var(--c-surface); border:1px solid var(--c-border); border-radius:10px; overflow:hidden; }
|
||||
.users-content th, .users-content td { padding:12px 14px; text-align:left; border-bottom:1px solid var(--c-border); font-size:13px; color:var(--c-text); }
|
||||
.users-content th { font-size:11px; font-weight:500; color:var(--c-muted); text-transform:uppercase; letter-spacing:.5px; background:var(--c-surface2); }
|
||||
.users-content tr:last-child td { border-bottom:none; }
|
||||
.users-content tr:hover td { background:#f5f0eb; }
|
||||
.users-content .badge { display:inline-block; padding:3px 10px; border-radius:9999px; font-size:11px; font-weight:500; }
|
||||
.users-content .badge-admin { background:#f0fdf4; color:#16a34a; }
|
||||
.users-content .badge-op { background:#eff6ff; color:#2563eb; }
|
||||
.users-content .badge-view { background:#ede8e2; color:#7a7067; }
|
||||
.users-content .badge-on { background:#f0fdf4; color:#16a34a; }
|
||||
.users-content .badge-off { background:#fef2f2; color:#ef4444; }
|
||||
.users-content .modal-overlay { position:fixed; inset:0; background:rgba(32,21,21,.45); backdrop-filter:blur(4px); display:flex; align-items:center; justify-content:center; padding:20px; z-index:9999; opacity:0; pointer-events:none; transition:opacity .2s; }
|
||||
.users-content .modal-overlay.show { opacity:1; pointer-events:auto; }
|
||||
.users-content .modal { width:min(440px,100%); background:#fafaf9; border:1px solid #ddd8d2; border-radius:12px; padding:24px; box-shadow:0 4px 16px rgba(32,21,21,.1); }
|
||||
.users-content .modal h3 { font-size:15px; font-weight:600; letter-spacing:-0.3px; margin-bottom:18px; color:var(--c-text); }
|
||||
.users-content .form-group { margin-bottom:14px; }
|
||||
.users-content .form-group label { display:block; font-size:13px; font-weight:500; color:#3d3530; margin-bottom:5px; }
|
||||
.users-content .form-group input,
|
||||
.users-content .form-group select { width:100%; padding:9px 12px; background:#fafaf9; border:1px solid #ddd8d2; border-radius:8px; color:#201515; font-family:var(--font-body); font-size:13px; outline:none; transition:border-color .15s; }
|
||||
.users-content .form-group input:focus,
|
||||
.users-content .form-group select:focus { border-color:#0d7490; }
|
||||
.users-content .modal-actions { display:flex; gap:10px; justify-content:flex-end; margin-top:18px; }
|
||||
.users-content .msg-box { margin-top:10px; font-size:12px; min-height:16px; }
|
||||
.users-content .msg-box.error { color:#ef4444; }
|
||||
.users-content .msg-box.success { color:#16a34a; }
|
||||
.users-content .temp-pw-box { background:#ede8e2; border:1px solid #ddd8d2; border-radius:8px; padding:12px 16px; text-align:center; font-size:20px; font-weight:600; color:#201515; letter-spacing:4px; margin:12px 0; }
|
||||
</style>
|
||||
|
||||
<div class="users-content">
|
||||
|
||||
<div class="section-header">
|
||||
<div class="section-title">Daftar Akun Pengguna</div>
|
||||
<button class="btn btn-primary" onclick="openModal('create')">+ Akun Baru</button>
|
||||
</div>
|
||||
|
||||
<table id="userTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th><th>Nama</th><th>Role</th>
|
||||
<th>Rumah Ibadah</th><th>Status</th><th>Dibuat</th><th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userTbody">
|
||||
<tr><td colspan="7" style="text-align:center;color:var(--c-muted);">Memuat...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Modal Create/Edit -->
|
||||
<div class="modal-overlay" id="modalForm">
|
||||
<div class="modal">
|
||||
<h3 id="modalTitle">Akun Baru</h3>
|
||||
<input type="hidden" id="editId">
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" id="fUsername" placeholder="username" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group" id="fgPassword">
|
||||
<label>Password Awal</label>
|
||||
<input type="text" id="fPassword" placeholder="Min 8 karakter, huruf+angka" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nama Lengkap</label>
|
||||
<input type="text" id="fNama" placeholder="Nama lengkap">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Role</label>
|
||||
<select id="fRole" onchange="toggleIbadahField()">
|
||||
<option value="administrator">Administrator</option>
|
||||
<option value="operator">Operator</option>
|
||||
<option value="viewer" disabled>Viewer (legacy)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="fgIbadah" style="display:none;">
|
||||
<label>Rumah Ibadah</label>
|
||||
<select id="fIbadah">
|
||||
<option value="">-- Pilih Rumah Ibadah --</option>
|
||||
<?php foreach ($ibadah_list as $ib): ?>
|
||||
<option value="<?= $ib['id'] ?>"><?= htmlspecialchars("[{$ib['jenis']}] {$ib['nama']}") ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="msg-box" id="modalMsg"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" onclick="closeModal()">Batal</button>
|
||||
<button class="btn btn-primary" id="btnModalSave" onclick="saveUser()">Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Reset Password -->
|
||||
<div class="modal-overlay" id="modalReset">
|
||||
<div class="modal">
|
||||
<h3>Reset Password</h3>
|
||||
<p style="font-size:13px;color:var(--c-muted);line-height:1.6;">
|
||||
Password sementara untuk akun ini. Sampaikan ke pengguna secara langsung —
|
||||
mereka akan diminta mengganti saat login pertama.
|
||||
</p>
|
||||
<div class="temp-pw-box" id="tempPwDisplay">—</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-primary" onclick="document.getElementById('modalReset').classList.remove('show')">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- .users-content -->
|
||||
|
||||
<script>
|
||||
const SELF_ID = <?= get_user_id() ?>;
|
||||
|
||||
async function loadUsers() {
|
||||
const res = await fetch('../api/users/ambil.php');
|
||||
const j = await res.json();
|
||||
if (j.status !== 'success') return;
|
||||
|
||||
const tbody = document.getElementById('userTbody');
|
||||
if (!j.data.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#8b949e;">Belum ada akun.</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = j.data.map(u => {
|
||||
const roleBadge = u.role === 'administrator'
|
||||
? `<span class="badge badge-admin">Admin</span>`
|
||||
: u.role === 'operator'
|
||||
? `<span class="badge badge-op">Operator</span>`
|
||||
: `<span class="badge badge-view">Viewer</span>`;
|
||||
const statusBadge = u.is_active == 1
|
||||
? `<span class="badge badge-on">Aktif</span>`
|
||||
: `<span class="badge badge-off">Nonaktif</span>`;
|
||||
const ibadah = u.nama_ibadah ? escapeHTML(u.nama_ibadah) : '<span style="color:#8b949e">—</span>';
|
||||
const isSelf = u.id == SELF_ID;
|
||||
const toggleBtn = isSelf ? '' :
|
||||
`<button class="btn btn-sm ${u.is_active==1 ? 'btn-danger' : 'btn-secondary'}"
|
||||
onclick="toggleActive(${u.id})">
|
||||
${u.is_active==1 ? 'Nonaktifkan' : 'Aktifkan'}
|
||||
</button>`;
|
||||
return `<tr>
|
||||
<td style="font-weight:500">${escapeHTML(u.username)}</td>
|
||||
<td>${escapeHTML(u.nama_lengkap)}</td>
|
||||
<td>${roleBadge}</td>
|
||||
<td>${ibadah}</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td style="color:#8b949e;font-size:12px">${u.created_at.slice(0,10)}</td>
|
||||
<td style="display:flex;gap:6px;flex-wrap:wrap">
|
||||
<button class="btn btn-sm btn-secondary" onclick='openModal("edit", ${JSON.stringify(u)})'>Edit</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="resetPw(${u.id})">Reset PW</button>
|
||||
${toggleBtn}
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function escapeHTML(s) {
|
||||
const d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(s ?? ''));
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function toggleIbadahField() {
|
||||
const role = document.getElementById('fRole').value;
|
||||
document.getElementById('fgIbadah').style.display = role === 'operator' ? '' : 'none';
|
||||
}
|
||||
|
||||
function openModal(mode, user = null) {
|
||||
const isEdit = mode === 'edit';
|
||||
document.getElementById('modalTitle').textContent = isEdit ? 'Edit Akun' : 'Akun Baru';
|
||||
document.getElementById('editId').value = isEdit ? user.id : '';
|
||||
document.getElementById('fUsername').value = isEdit ? user.username : '';
|
||||
document.getElementById('fUsername').disabled = isEdit;
|
||||
document.getElementById('fNama').value = isEdit ? user.nama_lengkap : '';
|
||||
document.getElementById('fRole').value = isEdit ? user.role : 'operator';
|
||||
document.getElementById('fIbadah').value = isEdit ? (user.ibadah_id ?? '') : '';
|
||||
document.getElementById('fgPassword').style.display = isEdit ? 'none' : '';
|
||||
document.getElementById('modalMsg').textContent = '';
|
||||
toggleIbadahField();
|
||||
document.getElementById('modalForm').classList.add('show');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modalForm').classList.remove('show');
|
||||
}
|
||||
|
||||
async function saveUser() {
|
||||
const id = document.getElementById('editId').value;
|
||||
const isEdit = !!id;
|
||||
const msg = document.getElementById('modalMsg');
|
||||
const btn = document.getElementById('btnModalSave');
|
||||
btn.disabled = true; btn.textContent = '⏳';
|
||||
msg.className = 'msg-box'; msg.textContent = '';
|
||||
|
||||
const fd = new FormData();
|
||||
if (isEdit) {
|
||||
fd.append('id', id);
|
||||
fd.append('nama_lengkap', document.getElementById('fNama').value.trim());
|
||||
fd.append('role', document.getElementById('fRole').value);
|
||||
fd.append('ibadah_id', document.getElementById('fIbadah').value);
|
||||
} else {
|
||||
fd.append('username', document.getElementById('fUsername').value.trim());
|
||||
fd.append('password', document.getElementById('fPassword').value);
|
||||
fd.append('nama_lengkap', document.getElementById('fNama').value.trim());
|
||||
fd.append('role', document.getElementById('fRole').value);
|
||||
fd.append('ibadah_id', document.getElementById('fIbadah').value);
|
||||
}
|
||||
|
||||
const url = isEdit ? '../api/users/update.php' : '../api/users/simpan.php';
|
||||
const res = await fetch(url, { method: 'POST', body: appendCsrf(fd) });
|
||||
const j = await res.json();
|
||||
btn.disabled = false; btn.textContent = 'Simpan';
|
||||
|
||||
if (j.status === 'success') {
|
||||
closeModal();
|
||||
loadUsers();
|
||||
} else {
|
||||
msg.className = 'msg-box error';
|
||||
msg.textContent = j.message;
|
||||
}
|
||||
}
|
||||
|
||||
function showConfirm({ title, message, confirmLabel = 'Ya, Lanjutkan', cancelLabel = 'Batal', danger = false }) {
|
||||
return new Promise(resolve => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'confirm-overlay';
|
||||
el.innerHTML = `<div class="confirm-box">
|
||||
<div class="confirm-title">${title}</div>
|
||||
<p class="confirm-msg">${message}</p>
|
||||
<div class="confirm-actions">
|
||||
<button class="btn-secondary" id="_sc_cancel">${cancelLabel}</button>
|
||||
<button class="${danger ? 'btn-danger' : 'btn-primary'}" id="_sc_ok">${confirmLabel}</button>
|
||||
</div></div>`;
|
||||
document.body.appendChild(el);
|
||||
const done = val => { el.remove(); resolve(val); };
|
||||
el.querySelector('#_sc_ok').onclick = () => done(true);
|
||||
el.querySelector('#_sc_cancel').onclick = () => done(false);
|
||||
el.addEventListener('click', e => { if (e.target === el) done(false); });
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleActive(id) {
|
||||
const user = [...document.querySelectorAll('#userTbody tr')].find(tr =>
|
||||
tr.querySelector(`[onclick*="toggleActive(${id})"]`)
|
||||
);
|
||||
const isActive = user?.querySelector('.badge-on') !== null;
|
||||
const ok = await showConfirm({
|
||||
title: isActive ? 'Nonaktifkan Akun' : 'Aktifkan Akun',
|
||||
message: isActive
|
||||
? 'Akun ini akan dinonaktifkan dan tidak bisa login.'
|
||||
: 'Akun ini akan diaktifkan kembali.',
|
||||
confirmLabel: isActive ? 'Ya, Nonaktifkan' : 'Ya, Aktifkan',
|
||||
danger: isActive
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
const fd = new FormData(); fd.append('id', id);
|
||||
const res = await fetch('../api/users/toggle_active.php', { method:'POST', body: appendCsrf(fd) });
|
||||
const j = await res.json();
|
||||
if (j.status === 'success') loadUsers();
|
||||
else alert(j.message);
|
||||
}
|
||||
|
||||
async function resetPw(id) {
|
||||
const ok = await showConfirm({
|
||||
title: 'Reset Password',
|
||||
message: 'Password akun ini akan direset. Password sementara akan ditampilkan setelah proses selesai.',
|
||||
confirmLabel: 'Ya, Reset'
|
||||
});
|
||||
if (!ok) return;
|
||||
const fd = new FormData(); fd.append('id', id);
|
||||
const res = await fetch('../api/users/reset_password.php', { method:'POST', body: appendCsrf(fd) });
|
||||
const j = await res.json();
|
||||
if (j.status === 'success') {
|
||||
document.getElementById('tempPwDisplay').textContent = j.temp_password;
|
||||
document.getElementById('modalReset').classList.add('show');
|
||||
} else {
|
||||
alert(j.message);
|
||||
}
|
||||
}
|
||||
|
||||
loadUsers();
|
||||
</script>
|
||||
|
||||
<?php include '../includes/page-end.php'; ?>
|
||||
Reference in New Issue
Block a user