feat: complete WebGIS implementation with nominal bantuan and cache busting
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
// ================================================================
|
||||
// Auth Action — Proses Login
|
||||
// ================================================================
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/../config/database.php';
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ' . BASE_URL . '/admin/login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if (empty($email) || empty($password)) {
|
||||
header('Location: ' . BASE_URL . '/admin/login.php?error=empty');
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("SELECT * FROM users WHERE email = ? LIMIT 1");
|
||||
$stmt->execute([$email]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user || !password_verify($password, $user['password'])) {
|
||||
// Tambah sedikit delay untuk mencegah brute force
|
||||
sleep(1);
|
||||
header('Location: ' . BASE_URL . '/admin/login.php?error=invalid&email=' . urlencode($email));
|
||||
exit;
|
||||
}
|
||||
|
||||
loginUser($user);
|
||||
header('Location: ' . BASE_URL . '/admin/');
|
||||
exit;
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
// ================================================================
|
||||
// Admin: Export Data (CSV / PDF)
|
||||
// ================================================================
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/../config/database.php';
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/functions.php';
|
||||
requireLogin();
|
||||
|
||||
$pageTitle = 'Export Data';
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
|
||||
$db = getDB();
|
||||
$kecList = $db->query("SELECT DISTINCT kecamatan FROM penduduk_miskin WHERE kecamatan IS NOT NULL AND kecamatan != '' ORDER BY kecamatan")->fetchAll(PDO::FETCH_COLUMN);
|
||||
$ibadahList = $db->query("SELECT id, nama, jenis FROM rumah_ibadah ORDER BY nama")->fetchAll();
|
||||
?>
|
||||
|
||||
<!-- ── Page Header ─────────────────────────────────────────── -->
|
||||
<div class="page-header">
|
||||
<div class="page-title-block">
|
||||
<h1><i class="bi bi-download"></i> Export Data</h1>
|
||||
<div class="page-subtitle">Export data ke format CSV atau cetak PDF</div>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<a href="<?= BASE_URL ?>/admin/" class="btn btn-ghost btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Export Cards ───────────────────────────────────────── -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:24px;">
|
||||
|
||||
<!-- Penduduk Miskin Export -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">👤 Export Penduduk Miskin</div>
|
||||
</div>
|
||||
<div class="card-body" style="padding:20px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status Bantuan</label>
|
||||
<select class="form-control" id="exp-status">
|
||||
<option value="">Semua Status</option>
|
||||
<option value="belum">❌ Belum Dibantu</option>
|
||||
<option value="menunggu">⏳ Menunggu Verifikasi</option>
|
||||
<option value="sudah">✅ Sudah Dibantu</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kecamatan</label>
|
||||
<select class="form-control" id="exp-kecamatan">
|
||||
<option value="">Semua Kecamatan</option>
|
||||
<?php foreach ($kecList as $k): ?>
|
||||
<option value="<?= htmlspecialchars($k) ?>"><?= htmlspecialchars($k) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Rumah Ibadah</label>
|
||||
<select class="form-control" id="exp-ibadah">
|
||||
<option value="">Semua Rumah Ibadah</option>
|
||||
<?php foreach ($ibadahList as $ib): ?>
|
||||
<option value="<?= $ib['id'] ?>"><?= htmlspecialchars($ib['nama']) ?> (<?= $ib['jenis'] ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top:8px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Dari Tanggal</label>
|
||||
<input class="form-control" type="date" id="exp-dari">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Sampai Tanggal</label>
|
||||
<input class="form-control" type="date" id="exp-sampai">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;margin-top:16px;flex-wrap:wrap;">
|
||||
<button class="btn btn-success btn-sm" onclick="doExport('penduduk','csv')">
|
||||
<i class="bi bi-file-earmark-spreadsheet"></i> Export CSV
|
||||
</button>
|
||||
<button class="btn btn-outline-accent btn-sm" onclick="doExport('penduduk','print')">
|
||||
<i class="bi bi-printer"></i> Cetak / PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rumah Ibadah Export -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">🏛️ Export Rumah Ibadah</div>
|
||||
</div>
|
||||
<div class="card-body" style="padding:20px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Jenis Ibadah</label>
|
||||
<select class="form-control" id="exp-jenis-ib">
|
||||
<option value="">Semua Jenis</option>
|
||||
<?php foreach (array_keys(JENIS_IBADAH) as $j): ?>
|
||||
<option value="<?= $j ?>"><?= JENIS_EMOJI[$j]??'' ?> <?= $j ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kecamatan</label>
|
||||
<select class="form-control" id="exp-kec-ib">
|
||||
<option value="">Semua Kecamatan</option>
|
||||
<?php
|
||||
$kecIb = $db->query("SELECT DISTINCT kecamatan FROM rumah_ibadah WHERE kecamatan IS NOT NULL AND kecamatan != '' ORDER BY kecamatan")->fetchAll(PDO::FETCH_COLUMN);
|
||||
foreach ($kecIb as $k): ?>
|
||||
<option value="<?= htmlspecialchars($k) ?>"><?= htmlspecialchars($k) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div style="margin-top:14px;padding:14px;background:rgba(91,127,255,0.06);border-radius:8px;border:1px solid rgba(91,127,255,0.15);">
|
||||
<div style="font-size:12px;color:var(--text2);">
|
||||
<i class="bi bi-info-circle"></i> Export akan mencakup: Nama, Jenis, Kontak, Alamat, Koordinat, Radius, Jumlah Binaan
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;margin-top:16px;flex-wrap:wrap;">
|
||||
<button class="btn btn-success btn-sm" onclick="doExport('ibadah','csv')">
|
||||
<i class="bi bi-file-earmark-spreadsheet"></i> Export CSV
|
||||
</button>
|
||||
<button class="btn btn-outline-accent btn-sm" onclick="doExport('ibadah','print')">
|
||||
<i class="bi bi-printer"></i> Cetak / PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Recent Exports / Quick Stats ───────────────────────── -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">📊 Ringkasan Data</div>
|
||||
</div>
|
||||
<div class="card-body" style="padding:16px;">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;">
|
||||
<?php
|
||||
$counts = [
|
||||
'Total KK' => $db->query("SELECT COUNT(*) FROM penduduk_miskin")->fetchColumn(),
|
||||
'Sudah Dibantu' => $db->query("SELECT COUNT(*) FROM penduduk_miskin WHERE status_bantuan='sudah'")->fetchColumn(),
|
||||
'Belum Dibantu' => $db->query("SELECT COUNT(*) FROM penduduk_miskin WHERE status_bantuan='belum'")->fetchColumn(),
|
||||
'Menunggu' => $db->query("SELECT COUNT(*) FROM penduduk_miskin WHERE status_bantuan='menunggu'")->fetchColumn(),
|
||||
'Total Jiwa' => $db->query("SELECT COALESCE(SUM(jumlah_anggota),0) FROM penduduk_miskin")->fetchColumn(),
|
||||
'Rumah Ibadah' => $db->query("SELECT COUNT(*) FROM rumah_ibadah")->fetchColumn(),
|
||||
];
|
||||
$colors = ['#5b7fff','#22c55e','#ef4444','#f59e0b','#a78bfa','#34d399'];
|
||||
$i = 0;
|
||||
foreach ($counts as $label => $val):
|
||||
$c = $colors[$i++];
|
||||
?>
|
||||
<div style="background:<?= $c ?>18;border:1px solid <?= $c ?>33;border-radius:10px;padding:14px;text-align:center;">
|
||||
<div style="font-size:22px;font-weight:800;color:<?= $c ?>"><?= number_format($val) ?></div>
|
||||
<div style="font-size:11px;color:var(--text2);margin-top:2px;"><?= $label ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const BASE = '<?= BASE_URL ?>';
|
||||
|
||||
function doExport(type, format) {
|
||||
let params = new URLSearchParams({ type, format });
|
||||
|
||||
if (type === 'penduduk') {
|
||||
const status = document.getElementById('exp-status').value;
|
||||
const kec = document.getElementById('exp-kecamatan').value;
|
||||
const ibadah = document.getElementById('exp-ibadah').value;
|
||||
const dari = document.getElementById('exp-dari').value;
|
||||
const sampai = document.getElementById('exp-sampai').value;
|
||||
if (status) params.set('status', status);
|
||||
if (kec) params.set('kecamatan', kec);
|
||||
if (ibadah) params.set('ibadah_id', ibadah);
|
||||
if (dari) params.set('dari', dari);
|
||||
if (sampai) params.set('sampai', sampai);
|
||||
} else {
|
||||
const jenis = document.getElementById('exp-jenis-ib').value;
|
||||
const kec = document.getElementById('exp-kec-ib').value;
|
||||
if (jenis) params.set('jenis', jenis);
|
||||
if (kec) params.set('kecamatan', kec);
|
||||
}
|
||||
|
||||
if (format === 'csv') {
|
||||
window.open(`${BASE}/api/export.php?${params}`, '_blank');
|
||||
} else {
|
||||
window.open(`${BASE}/api/export.php?${params}`, '_blank');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/../includes/footer.php'; ?>
|
||||
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
// ================================================================
|
||||
// Admin: Import Data Massal (CSV)
|
||||
// ================================================================
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/../config/database.php';
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/functions.php';
|
||||
requireLogin();
|
||||
|
||||
$pageTitle = 'Import Data';
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
|
||||
$db = getDB();
|
||||
$ibadahList = $db->query("SELECT id, nama, jenis FROM rumah_ibadah ORDER BY nama")->fetchAll();
|
||||
?>
|
||||
|
||||
<!-- ── Page Header ─────────────────────────────────────────── -->
|
||||
<div class="page-header">
|
||||
<div class="page-title-block">
|
||||
<h1><i class="bi bi-upload"></i> Import Data</h1>
|
||||
<div class="page-subtitle">Import massal data penduduk miskin atau rumah ibadah via CSV</div>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<a href="<?= BASE_URL ?>/admin/" class="btn btn-ghost btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Info Cards ─────────────────────────────────────────── -->
|
||||
<div class="stats-row" style="margin-bottom:24px;">
|
||||
<div class="stat-card blue" style="cursor:pointer;" onclick="setImportType('penduduk')">
|
||||
<div class="stat-icon">👤</div>
|
||||
<div class="stat-value" style="font-size:16px;">Penduduk Miskin</div>
|
||||
<div class="stat-label">Import data KK</div>
|
||||
</div>
|
||||
<div class="stat-card green" style="cursor:pointer;" onclick="setImportType('ibadah')">
|
||||
<div class="stat-icon">🏛️</div>
|
||||
<div class="stat-value" style="font-size:16px;">Rumah Ibadah</div>
|
||||
<div class="stat-label">Import data ibadah</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Import Card ─────────────────────────────────────────── -->
|
||||
<div class="card" style="margin-bottom:20px;">
|
||||
<div class="card-header">
|
||||
<div class="card-title" id="import-type-title">📤 Pilih Jenis Import</div>
|
||||
</div>
|
||||
<div class="card-body" style="padding:24px;">
|
||||
|
||||
<!-- Type Selector -->
|
||||
<div style="display:flex;gap:12px;margin-bottom:24px;flex-wrap:wrap;">
|
||||
<button class="btn btn-primary btn-sm import-type-btn active" id="btn-type-penduduk" onclick="setImportType('penduduk')">
|
||||
<i class="bi bi-people-fill"></i> Penduduk Miskin
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm import-type-btn" id="btn-type-ibadah" onclick="setImportType('ibadah')">
|
||||
<i class="bi bi-building"></i> Rumah Ibadah
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Format Kolom Guide -->
|
||||
<div id="format-penduduk">
|
||||
<div class="card" style="background:rgba(91,127,255,0.06);border:1px solid rgba(91,127,255,0.2);margin-bottom:20px;">
|
||||
<div class="card-body" style="padding:16px;">
|
||||
<div style="font-weight:700;color:var(--accent);margin-bottom:10px;font-size:13px;">
|
||||
<i class="bi bi-info-circle-fill"></i> Format CSV Penduduk Miskin
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text2);line-height:1.8;">
|
||||
Kolom (urutan wajib): <code>kk_nama, nik, jumlah_anggota, alamat, kelurahan, kecamatan, lat, lng, status_bantuan, jenis_bantuan, tanggal_bantuan, nominal_bantuan</code>
|
||||
</div>
|
||||
<div style="margin-top:10px;">
|
||||
<strong style="font-size:12px;">Contoh baris:</strong><br>
|
||||
<code style="font-size:11px;display:block;margin-top:4px;padding:8px;background:rgba(0,0,0,0.2);border-radius:6px;">
|
||||
Ahmad Sulaiman,6171012345678901,4,Jl. Veteran No. 12,Akcaya,Pontianak Selatan,-0.0430,109.3340,belum,
|
||||
</code>
|
||||
</div>
|
||||
<div style="margin-top:10px;font-size:11px;color:var(--text3);">
|
||||
⚠️ status_bantuan: belum / sudah / menunggu | Baris pertama bisa berupa header (akan dideteksi otomatis)
|
||||
</div>
|
||||
<a href="<?= BASE_URL ?>/admin/template_penduduk.csv" class="btn btn-ghost btn-sm" style="margin-top:12px;" onclick="downloadTemplate('penduduk');return false;">
|
||||
<i class="bi bi-download"></i> Download Template CSV
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="format-ibadah" style="display:none;">
|
||||
<div class="card" style="background:rgba(34,197,94,0.06);border:1px solid rgba(34,197,94,0.2);margin-bottom:20px;">
|
||||
<div class="card-body" style="padding:16px;">
|
||||
<div style="font-weight:700;color:var(--green);margin-bottom:10px;font-size:13px;">
|
||||
<i class="bi bi-info-circle-fill"></i> Format CSV Rumah Ibadah
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text2);line-height:1.8;">
|
||||
Kolom (urutan wajib): <code>nama, jenis, kontak, alamat, kelurahan, kecamatan, lat, lng, radius</code>
|
||||
</div>
|
||||
<div style="margin-top:10px;">
|
||||
<strong style="font-size:12px;">Contoh baris:</strong><br>
|
||||
<code style="font-size:11px;display:block;margin-top:4px;padding:8px;background:rgba(0,0,0,0.2);border-radius:6px;">
|
||||
Masjid Al-Ikhlas,Masjid,0561-123456,Jl. Ahmad Yani,Akcaya,Pontianak Selatan,-0.0412,109.3325,300
|
||||
</code>
|
||||
</div>
|
||||
<div style="margin-top:10px;font-size:11px;color:var(--text3);">
|
||||
⚠️ jenis: Masjid / Musholla / Gereja / Katolik / Pura / Vihara / Klenteng / Lainnya
|
||||
</div>
|
||||
<a href="#" class="btn btn-ghost btn-sm" style="margin-top:12px;" onclick="downloadTemplate('ibadah');return false;">
|
||||
<i class="bi bi-download"></i> Download Template CSV
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Form -->
|
||||
<div id="upload-section">
|
||||
<div class="form-group" style="margin-bottom:16px;">
|
||||
<label class="form-label" style="font-weight:600;">Pilih File CSV <span style="color:var(--red)">*</span></label>
|
||||
<div id="drop-zone" style="border:2px dashed var(--border);border-radius:12px;padding:32px;text-align:center;transition:all .3s;cursor:pointer;">
|
||||
<div style="font-size:36px;margin-bottom:8px;">📂</div>
|
||||
<div style="font-weight:600;margin-bottom:4px;">Drag & Drop file CSV di sini</div>
|
||||
<div style="font-size:12px;color:var(--text3);margin-bottom:12px;">atau klik untuk browse file</div>
|
||||
<input type="file" id="csv-file" accept=".csv,.txt" style="display:none;">
|
||||
<button class="btn btn-outline-accent btn-sm" onclick="document.getElementById('csv-file').click()">
|
||||
<i class="bi bi-folder2-open"></i> Browse File
|
||||
</button>
|
||||
</div>
|
||||
<div id="file-info" style="display:none;margin-top:10px;padding:10px 14px;background:rgba(34,197,94,0.1);border:1px solid rgba(34,197,94,0.3);border-radius:8px;font-size:12px;">
|
||||
<i class="bi bi-file-earmark-text" style="color:var(--green)"></i>
|
||||
<span id="file-name"></span> — <span id="file-size"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:12px;flex-wrap:wrap;">
|
||||
<button class="btn btn-outline-accent btn-sm" onclick="previewCSV()" id="btn-preview">
|
||||
<i class="bi bi-eye"></i> Preview Data
|
||||
</button>
|
||||
<button class="btn btn-success btn-sm" onclick="doImport()" id="btn-import" disabled>
|
||||
<i class="bi bi-cloud-upload"></i> Import Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Preview Table ─────────────────────────────────────────── -->
|
||||
<div id="preview-section" style="display:none;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">👁️ Preview Data (<span id="preview-count">0</span> baris)</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<span id="preview-valid-badge" style="font-size:12px;color:var(--green);font-weight:700;"></span>
|
||||
<span id="preview-err-badge" style="font-size:12px;color:var(--red);font-weight:700;"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive" id="preview-table-wrap"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Result Section ─────────────────────────────────────────── -->
|
||||
<div id="result-section" style="display:none;margin-top:20px;">
|
||||
<div class="card" id="result-card">
|
||||
<div class="card-body" style="padding:24px;text-align:center;">
|
||||
<div style="font-size:48px;margin-bottom:12px;" id="result-icon">✅</div>
|
||||
<div style="font-size:18px;font-weight:700;margin-bottom:8px;" id="result-title">Import Selesai</div>
|
||||
<div id="result-detail" style="font-size:14px;color:var(--text2);line-height:1.8;"></div>
|
||||
<div style="display:flex;gap:12px;justify-content:center;margin-top:20px;flex-wrap:wrap;">
|
||||
<button class="btn btn-ghost btn-sm" onclick="resetImport()"><i class="bi bi-arrow-repeat"></i> Import Lagi</button>
|
||||
<a href="<?= BASE_URL ?>/admin/penduduk_miskin.php" class="btn btn-primary btn-sm" id="result-link">
|
||||
<i class="bi bi-list-ul"></i> Lihat Data
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const BASE = '<?= BASE_URL ?>';
|
||||
let currentType = 'penduduk';
|
||||
let parsedRows = [];
|
||||
let hasHeader = false;
|
||||
|
||||
// ── Type Selector ──────────────────────────────────────────────
|
||||
function setImportType(type) {
|
||||
currentType = type;
|
||||
document.getElementById('format-penduduk').style.display = type === 'penduduk' ? 'block' : 'none';
|
||||
document.getElementById('format-ibadah').style.display = type === 'ibadah' ? 'block' : 'none';
|
||||
document.getElementById('btn-type-penduduk').className = 'btn btn-sm import-type-btn ' + (type === 'penduduk' ? 'btn-primary' : 'btn-ghost');
|
||||
document.getElementById('btn-type-ibadah').className = 'btn btn-sm import-type-btn ' + (type === 'ibadah' ? 'btn-primary' : 'btn-ghost');
|
||||
document.getElementById('import-type-title').textContent = type === 'penduduk' ? '👤 Import Penduduk Miskin' : '🏛️ Import Rumah Ibadah';
|
||||
resetPreview();
|
||||
}
|
||||
|
||||
// ── File Input ─────────────────────────────────────────────────
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const csvFile = document.getElementById('csv-file');
|
||||
|
||||
dropZone.addEventListener('click', () => csvFile.click());
|
||||
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.style.borderColor = 'var(--accent)'; });
|
||||
dropZone.addEventListener('dragleave', () => { dropZone.style.borderColor = 'var(--border)'; });
|
||||
dropZone.addEventListener('drop', e => {
|
||||
e.preventDefault(); dropZone.style.borderColor = 'var(--border)';
|
||||
const f = e.dataTransfer.files[0]; if (!f) return;
|
||||
csvFile.files = e.dataTransfer.files; showFileInfo(f);
|
||||
});
|
||||
csvFile.addEventListener('change', () => { if (csvFile.files[0]) showFileInfo(csvFile.files[0]); });
|
||||
|
||||
function showFileInfo(f) {
|
||||
document.getElementById('file-info').style.display = 'block';
|
||||
document.getElementById('file-name').textContent = f.name;
|
||||
document.getElementById('file-size').textContent = (f.size / 1024).toFixed(1) + ' KB';
|
||||
resetPreview();
|
||||
}
|
||||
|
||||
// ── CSV Parser ─────────────────────────────────────────────────
|
||||
function parseCSV(text) {
|
||||
const lines = text.split(/\r?\n/).filter(l => l.trim());
|
||||
return lines.map(line => {
|
||||
const cols = [];
|
||||
let cur = '', inQ = false;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const ch = line[i];
|
||||
if (ch === '"') { inQ = !inQ; }
|
||||
else if (ch === ',' && !inQ) { cols.push(cur.trim()); cur = ''; }
|
||||
else { cur += ch; }
|
||||
}
|
||||
cols.push(cur.trim());
|
||||
return cols;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Preview ────────────────────────────────────────────────────
|
||||
async function previewCSV() {
|
||||
const f = csvFile.files[0];
|
||||
if (!f) { Swal.fire('Error', 'Pilih file CSV terlebih dahulu!', 'error'); return; }
|
||||
const text = await f.text();
|
||||
const rows = parseCSV(text);
|
||||
if (!rows.length) { Swal.fire('Error', 'File kosong atau format tidak valid', 'error'); return; }
|
||||
|
||||
const COLS_P = ['kk_nama','nik','jumlah_anggota','alamat','kelurahan','kecamatan','lat','lng','status_bantuan','jenis_bantuan','tanggal_bantuan','nominal_bantuan'];
|
||||
const COLS_I = ['nama','jenis','kontak','alamat','kelurahan','kecamatan','lat','lng','radius'];
|
||||
const expectedCols = currentType === 'penduduk' ? COLS_P : COLS_I;
|
||||
|
||||
// Deteksi header
|
||||
const first = rows[0].join(',').toLowerCase();
|
||||
hasHeader = first.includes('nama') || first.includes('kk_nama') || first.includes('lat');
|
||||
parsedRows = hasHeader ? rows.slice(1) : rows;
|
||||
|
||||
// Build preview table
|
||||
let valid = 0, errors = 0;
|
||||
let html = '<table class="admin-table"><thead><tr>';
|
||||
expectedCols.forEach(c => html += `<th>${c}</th>`);
|
||||
html += '<th>Status</th></tr></thead><tbody>';
|
||||
|
||||
parsedRows.slice(0, 50).forEach((row, idx) => {
|
||||
const ok = row.length >= (currentType === 'penduduk' ? 8 : 8) && row[currentType === 'penduduk' ? 6 : 6] && row[currentType === 'penduduk' ? 7 : 7];
|
||||
if (ok) valid++; else errors++;
|
||||
html += `<tr style="${ok ? '' : 'background:rgba(239,68,68,0.08);'}">`;
|
||||
for (let i = 0; i < expectedCols.length; i++) {
|
||||
const val = row[i] || '';
|
||||
html += `<td style="font-size:11px;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${val.replace(/"/g, '')}">${val || '<span style="color:var(--text3)">—</span>'}</td>`;
|
||||
}
|
||||
html += `<td>${ok ? '<span style="color:var(--green);font-size:11px;">✅ OK</span>' : '<span style="color:var(--red);font-size:11px;">❌ Error</span>'}</td></tr>`;
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
|
||||
document.getElementById('preview-table-wrap').innerHTML = html;
|
||||
document.getElementById('preview-count').textContent = parsedRows.length;
|
||||
document.getElementById('preview-valid-badge').textContent = `✅ Valid: ${valid}`;
|
||||
document.getElementById('preview-err-badge').textContent = errors ? `❌ Error: ${errors}` : '';
|
||||
document.getElementById('preview-section').style.display = 'block';
|
||||
document.getElementById('btn-import').disabled = valid === 0;
|
||||
document.getElementById('result-section').style.display = 'none';
|
||||
}
|
||||
|
||||
// ── Do Import ─────────────────────────────────────────────────
|
||||
async function doImport() {
|
||||
const f = csvFile.files[0];
|
||||
if (!f || !parsedRows.length) return;
|
||||
|
||||
const res = await Swal.fire({
|
||||
title: 'Mulai Import?',
|
||||
html: `Import <b>${parsedRows.length}</b> baris data <b>${currentType === 'penduduk' ? 'Penduduk Miskin' : 'Rumah Ibadah'}</b>?`,
|
||||
icon: 'question', showCancelButton: true,
|
||||
confirmButtonText: 'Ya, Import!', cancelButtonText: 'Batal',
|
||||
background: '#161b27', color: '#e8ecf4'
|
||||
});
|
||||
if (!res.isConfirmed) return;
|
||||
|
||||
document.getElementById('btn-import').disabled = true;
|
||||
document.getElementById('btn-import').innerHTML = '<span class="spinner-border spinner-border-sm"></span> Mengimport...';
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('file', f);
|
||||
fd.append('type', currentType);
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${BASE}/api/import.php`, { method: 'POST', body: fd });
|
||||
const json = await resp.json();
|
||||
|
||||
document.getElementById('result-section').style.display = 'block';
|
||||
document.getElementById('preview-section').style.display = 'none';
|
||||
|
||||
if (json.success) {
|
||||
document.getElementById('result-icon').textContent = '✅';
|
||||
document.getElementById('result-title').textContent = 'Import Berhasil!';
|
||||
document.getElementById('result-detail').innerHTML = `
|
||||
<div style="display:flex;justify-content:center;gap:24px;flex-wrap:wrap;margin-top:10px;">
|
||||
<div><span style="color:var(--green);font-size:24px;font-weight:800;">${json.imported}</span><br>Berhasil diimpor</div>
|
||||
<div><span style="color:var(--red);font-size:24px;font-weight:800;">${json.failed}</span><br>Gagal</div>
|
||||
<div><span style="color:var(--text2);font-size:24px;font-weight:800;">${json.skipped||0}</span><br>Dilewati</div>
|
||||
</div>
|
||||
${json.errors?.length ? '<div style="margin-top:14px;font-size:11px;color:var(--red);text-align:left;">'+json.errors.map(e=>`• ${e}`).join('<br>')+'</div>' : ''}`;
|
||||
document.getElementById('result-link').href = currentType === 'penduduk'
|
||||
? `${BASE}/admin/penduduk_miskin.php`
|
||||
: `${BASE}/admin/rumah_ibadah.php`;
|
||||
} else {
|
||||
document.getElementById('result-icon').textContent = '❌';
|
||||
document.getElementById('result-title').textContent = 'Import Gagal';
|
||||
document.getElementById('result-detail').textContent = json.message || 'Terjadi kesalahan saat import.';
|
||||
}
|
||||
} catch (e) {
|
||||
Swal.fire('Error', 'Gagal mengirim data ke server', 'error');
|
||||
} finally {
|
||||
document.getElementById('btn-import').disabled = false;
|
||||
document.getElementById('btn-import').innerHTML = '<i class="bi bi-cloud-upload"></i> Import Data';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reset ─────────────────────────────────────────────────────
|
||||
function resetPreview() {
|
||||
parsedRows = [];
|
||||
document.getElementById('preview-section').style.display = 'none';
|
||||
document.getElementById('result-section').style.display = 'none';
|
||||
document.getElementById('btn-import').disabled = true;
|
||||
}
|
||||
|
||||
function resetImport() {
|
||||
csvFile.value = '';
|
||||
document.getElementById('file-info').style.display = 'none';
|
||||
resetPreview();
|
||||
}
|
||||
|
||||
// ── Download Template ──────────────────────────────────────────
|
||||
function downloadTemplate(type) {
|
||||
let content, filename;
|
||||
if (type === 'penduduk') {
|
||||
content = 'kk_nama,nik,jumlah_anggota,alamat,kelurahan,kecamatan,lat,lng,status_bantuan,jenis_bantuan,tanggal_bantuan,nominal_bantuan\r\n';
|
||||
content += 'Ahmad Sulaiman,6171012345678901,4,Jl. Veteran No. 12,Akcaya,Pontianak Selatan,-0.0430,109.3340,belum,,,\r\n';
|
||||
content += 'Budi Santoso,,3,Jl. Imam Bonjol No. 5,Dalam Bugis,Pontianak Kota,-0.0270,109.3440,sudah,Sembako,2026-06-10,500000\r\n';
|
||||
filename = 'template_penduduk_miskin.csv';
|
||||
} else {
|
||||
content = 'nama,jenis,kontak,alamat,kelurahan,kecamatan,lat,lng,radius\r\n';
|
||||
content += 'Masjid Al-Ikhlas,Masjid,0561-123456,Jl. Ahmad Yani,Akcaya,Pontianak Selatan,-0.0412,109.3325,300\r\n';
|
||||
content += 'Gereja Bethel,Gereja,0561-654321,Jl. Diponegoro,Dalam Bugis,Pontianak Kota,-0.0280,109.3450,350\r\n';
|
||||
filename = 'template_rumah_ibadah.csv';
|
||||
}
|
||||
const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = filename;
|
||||
a.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/../includes/footer.php'; ?>
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
// ================================================================
|
||||
// Admin Dashboard
|
||||
// ================================================================
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/../config/database.php';
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/functions.php';
|
||||
requireLogin();
|
||||
|
||||
$pageTitle = 'Dashboard';
|
||||
$extraHead = '<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>';
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
|
||||
$db = getDB();
|
||||
|
||||
// Statistik utama
|
||||
$stats = [
|
||||
'ibadah' => (int)$db->query("SELECT COUNT(*) FROM rumah_ibadah")->fetchColumn(),
|
||||
'miskin' => (int)$db->query("SELECT COUNT(*) FROM penduduk_miskin")->fetchColumn(),
|
||||
'sudah' => (int)$db->query("SELECT COUNT(*) FROM penduduk_miskin WHERE status_bantuan='sudah'")->fetchColumn(),
|
||||
'belum' => (int)$db->query("SELECT COUNT(*) FROM penduduk_miskin WHERE status_bantuan='belum'")->fetchColumn(),
|
||||
'menunggu' => (int)$db->query("SELECT COUNT(*) FROM penduduk_miskin WHERE status_bantuan='menunggu'")->fetchColumn(),
|
||||
'anggota' => (int)$db->query("SELECT SUM(jumlah_anggota) FROM penduduk_miskin")->fetchColumn(),
|
||||
'nominal' => (float)$db->query("SELECT COALESCE(SUM(nominal_bantuan),0) FROM penduduk_miskin WHERE status_bantuan='sudah' AND nominal_bantuan IS NOT NULL")->fetchColumn(),
|
||||
];
|
||||
|
||||
// Per jenis ibadah
|
||||
$jenisStats = $db->query("SELECT jenis, COUNT(*) AS total FROM rumah_ibadah GROUP BY jenis ORDER BY total DESC")->fetchAll();
|
||||
|
||||
// Per kecamatan (top 7)
|
||||
$kecamatanStats = $db->query("
|
||||
SELECT kecamatan, COUNT(*) AS total,
|
||||
SUM(CASE WHEN status_bantuan='sudah' THEN 1 ELSE 0 END) AS sudah,
|
||||
SUM(CASE WHEN status_bantuan='belum' THEN 1 ELSE 0 END) AS belum
|
||||
FROM penduduk_miskin WHERE kecamatan IS NOT NULL AND kecamatan != ''
|
||||
GROUP BY kecamatan ORDER BY total DESC LIMIT 7
|
||||
")->fetchAll();
|
||||
|
||||
// Data terbaru 6 baris
|
||||
$recentMiskin = $db->query("
|
||||
SELECT pm.id, pm.kk_nama, pm.jumlah_anggota, pm.status_bantuan,
|
||||
pm.kecamatan, pm.created_at, ri.nama AS ibadah_nama
|
||||
FROM penduduk_miskin pm
|
||||
LEFT JOIN rumah_ibadah ri ON pm.rumah_ibadah_id = ri.id
|
||||
ORDER BY pm.created_at DESC LIMIT 6
|
||||
")->fetchAll();
|
||||
|
||||
// Tren bulanan (6 bulan terakhir)
|
||||
$trenBulan = $db->query("
|
||||
SELECT DATE_FORMAT(created_at,'%Y-%m') AS bulan,
|
||||
COUNT(*) AS total,
|
||||
SUM(CASE WHEN status_bantuan='sudah' THEN 1 ELSE 0 END) AS sudah
|
||||
FROM penduduk_miskin
|
||||
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 6 MONTH)
|
||||
GROUP BY bulan ORDER BY bulan ASC
|
||||
")->fetchAll();
|
||||
?>
|
||||
|
||||
<!-- ── Page Header ─────────────────────────────────────────── -->
|
||||
<div class="page-header">
|
||||
<div class="page-title-block">
|
||||
<h1>📊 Dashboard</h1>
|
||||
<div class="page-subtitle">Ringkasan data WebGIS Sebaran Penduduk Miskin · <?= date('d M Y, H:i') ?> WIB</div>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<a href="<?= BASE_URL ?>/admin/import.php" class="btn btn-ghost btn-sm">
|
||||
<i class="bi bi-upload"></i> Import
|
||||
</a>
|
||||
<a href="<?= BASE_URL ?>/admin/export.php" class="btn btn-outline-accent btn-sm">
|
||||
<i class="bi bi-download"></i> Export
|
||||
</a>
|
||||
<a href="<?= BASE_URL ?>/" target="_blank" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-map-fill"></i> Buka Peta
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Stat Cards ──────────────────────────────────────────── -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card blue">
|
||||
<div class="stat-icon">🏛️</div>
|
||||
<div class="stat-value"><?= $stats['ibadah'] ?></div>
|
||||
<div class="stat-label">Rumah Ibadah</div>
|
||||
</div>
|
||||
<div class="stat-card yellow">
|
||||
<div class="stat-icon">👥</div>
|
||||
<div class="stat-value"><?= $stats['miskin'] ?></div>
|
||||
<div class="stat-label">KK Terdaftar</div>
|
||||
</div>
|
||||
<div class="stat-card green">
|
||||
<div class="stat-icon">✅</div>
|
||||
<div class="stat-value"><?= $stats['sudah'] ?></div>
|
||||
<div class="stat-label">Sudah Dibantu</div>
|
||||
</div>
|
||||
<div class="stat-card red">
|
||||
<div class="stat-icon">❌</div>
|
||||
<div class="stat-value"><?= $stats['belum'] ?></div>
|
||||
<div class="stat-label">Belum Dibantu</div>
|
||||
</div>
|
||||
<div class="stat-card yellow" style="--yellow:#f59e0b">
|
||||
<div class="stat-icon">⏳</div>
|
||||
<div class="stat-value"><?= $stats['menunggu'] ?></div>
|
||||
<div class="stat-label">Menunggu Verif.</div>
|
||||
</div>
|
||||
<div class="stat-card blue">
|
||||
<div class="stat-icon">👤</div>
|
||||
<div class="stat-value"><?= number_format($stats['anggota'], 0, ',', '.') ?></div>
|
||||
<div class="stat-label">Total Jiwa</div>
|
||||
</div>
|
||||
<div class="stat-card green" style="grid-column: span 2; display:flex; flex-direction:row; justify-content:flex-start; align-items:center; gap:16px;">
|
||||
<div class="stat-icon" style="margin-bottom:0; font-size:32px;">💰</div>
|
||||
<div>
|
||||
<div class="stat-value" style="font-size:20px; font-family:'JetBrains Mono',monospace;">Rp <?= number_format($stats['nominal'], 0, ',', '.') ?></div>
|
||||
<div class="stat-label" style="text-align:left;">Total Bantuan Disalurkan</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Charts Row ──────────────────────────────────────────── -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1.2fr;gap:16px;margin-bottom:20px;">
|
||||
|
||||
<!-- Chart: Status Bantuan (Doughnut) -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">📊 Status Bantuan</div>
|
||||
</div>
|
||||
<div class="card-body" style="padding:14px;">
|
||||
<div class="chart-container" style="height:200px;">
|
||||
<canvas id="chart-status"></canvas>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-top:10px;font-size:11px;">
|
||||
<span style="color:#22c55e">● Sudah: <?= $stats['sudah'] ?></span>
|
||||
<span style="color:#ef4444">● Belum: <?= $stats['belum'] ?></span>
|
||||
<span style="color:#f59e0b">● Menunggu: <?= $stats['menunggu'] ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart: Per Jenis Ibadah (Pie) -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">🕌 Jenis Ibadah</div>
|
||||
</div>
|
||||
<div class="card-body" style="padding:14px;">
|
||||
<div class="chart-container" style="height:200px;">
|
||||
<canvas id="chart-jenis"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart: Tren Bulanan (Line) -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">📈 Tren 6 Bulan</div>
|
||||
</div>
|
||||
<div class="card-body" style="padding:14px;">
|
||||
<div class="chart-container" style="height:200px;">
|
||||
<canvas id="chart-tren"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ── Kecamatan + Recent ──────────────────────────────────── -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1.5fr;gap:16px;margin-bottom:20px;">
|
||||
|
||||
<!-- Per Kecamatan Bar -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">📍 Top Kecamatan</div>
|
||||
</div>
|
||||
<div class="card-body" style="padding:14px;">
|
||||
<div class="chart-container" style="height:240px;">
|
||||
<canvas id="chart-kecamatan"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Terbaru -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">🕐 Data Terbaru</div>
|
||||
<a href="<?= BASE_URL ?>/admin/penduduk_miskin.php" class="btn btn-ghost btn-sm">Lihat Semua</a>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nama KK</th>
|
||||
<th>Kecamatan</th>
|
||||
<th>Status</th>
|
||||
<th>Ibadah</th>
|
||||
<th>Tanggal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($recentMiskin as $r): ?>
|
||||
<tr>
|
||||
<td><a href="<?= BASE_URL ?>/admin/penduduk_miskin.php?edit=<?= $r['id'] ?>" style="color:var(--text);font-weight:600;text-decoration:none;"><?= htmlspecialchars($r['kk_nama']) ?></a></td>
|
||||
<td style="color:var(--text2);font-size:12px;"><?= htmlspecialchars($r['kecamatan'] ?: '—') ?></td>
|
||||
<td><span class="badge-status badge-<?= $r['status_bantuan'] ?>"><?= ucfirst($r['status_bantuan']) ?></span></td>
|
||||
<td style="font-size:11px;color:var(--text2);"><?= htmlspecialchars($r['ibadah_nama'] ?: '—') ?></td>
|
||||
<td style="font-size:11px;color:var(--text3);"><?= formatTanggal($r['created_at']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($recentMiskin)): ?>
|
||||
<tr><td colspan="5" style="text-align:center;color:var(--text3);padding:24px;">Belum ada data</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Chart.js Init ───────────────────────────────────────── -->
|
||||
<script>
|
||||
const cOpts = { responsive: true, maintainAspectRatio: false,
|
||||
plugins: { legend: { labels: { color:'#8890b0', font:{ size:11 } } } } };
|
||||
|
||||
// Status Bantuan — Doughnut
|
||||
new Chart(document.getElementById('chart-status').getContext('2d'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Sudah Dibantu','Belum Dibantu','Menunggu'],
|
||||
datasets: [{ data: [<?= $stats['sudah'] ?>,<?= $stats['belum'] ?>,<?= $stats['menunggu'] ?>],
|
||||
backgroundColor: ['rgba(34,197,94,0.8)','rgba(239,68,68,0.8)','rgba(245,158,11,0.8)'],
|
||||
borderColor: ['#161b27'], borderWidth: 2 }]
|
||||
},
|
||||
options: { ...cOpts, cutout:'65%' }
|
||||
});
|
||||
|
||||
// Jenis Ibadah — Pie
|
||||
const jenisLabels = <?= json_encode(array_column($jenisStats, 'jenis')) ?>;
|
||||
const jenisData = <?= json_encode(array_column($jenisStats, 'total')) ?>;
|
||||
const jenisColors = ['rgba(91,127,255,.8)','rgba(124,91,255,.8)','rgba(34,197,94,.8)','rgba(245,158,11,.8)','rgba(239,68,68,.8)','rgba(96,165,250,.8)','rgba(251,191,36,.8)','rgba(139,92,246,.8)'];
|
||||
new Chart(document.getElementById('chart-jenis').getContext('2d'), {
|
||||
type: 'pie',
|
||||
data: { labels: jenisLabels, datasets: [{ data: jenisData,
|
||||
backgroundColor: jenisColors.slice(0, jenisLabels.length),
|
||||
borderColor: '#161b27', borderWidth: 2 }] },
|
||||
options: { ...cOpts }
|
||||
});
|
||||
|
||||
// Tren Bulanan — Line
|
||||
const trenLabels = <?= json_encode(array_column($trenBulan,'bulan')) ?>;
|
||||
const trenTotal = <?= json_encode(array_column($trenBulan,'total')) ?>;
|
||||
const trenSudah = <?= json_encode(array_column($trenBulan,'sudah')) ?>;
|
||||
new Chart(document.getElementById('chart-tren').getContext('2d'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: trenLabels,
|
||||
datasets: [
|
||||
{ label:'Total KK', data: trenTotal, borderColor:'#5b7fff', backgroundColor:'rgba(91,127,255,0.1)', tension:0.4, fill:true, pointRadius:4 },
|
||||
{ label:'Dibantu', data: trenSudah, borderColor:'#22c55e', backgroundColor:'rgba(34,197,94,0.07)', tension:0.4, fill:true, pointRadius:4 }
|
||||
]
|
||||
},
|
||||
options: { ...cOpts, scales: {
|
||||
x: { ticks:{ color:'#565e80', font:{size:10} }, grid:{color:'rgba(255,255,255,0.04)'} },
|
||||
y: { ticks:{ color:'#565e80', font:{size:10} }, grid:{color:'rgba(255,255,255,0.04)'} }
|
||||
}}
|
||||
});
|
||||
|
||||
// Kecamatan — Bar
|
||||
const kecLabels = <?= json_encode(array_column($kecamatanStats,'kecamatan')) ?>;
|
||||
const kecTotal = <?= json_encode(array_column($kecamatanStats,'total')) ?>;
|
||||
const kecSudah = <?= json_encode(array_column($kecamatanStats,'sudah')) ?>;
|
||||
const kecBelum = <?= json_encode(array_column($kecamatanStats,'belum')) ?>;
|
||||
new Chart(document.getElementById('chart-kecamatan').getContext('2d'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: kecLabels,
|
||||
datasets: [
|
||||
{ label:'Sudah', data: kecSudah, backgroundColor:'rgba(34,197,94,0.75)', borderRadius:4 },
|
||||
{ label:'Belum', data: kecBelum, backgroundColor:'rgba(239,68,68,0.7)', borderRadius:4 }
|
||||
]
|
||||
},
|
||||
options: { ...cOpts, plugins:{...cOpts.plugins, legend:{...cOpts.plugins.legend}},
|
||||
scales:{
|
||||
x:{ ticks:{color:'#565e80',font:{size:10}}, grid:{color:'rgba(255,255,255,0.04)'}, stacked:true },
|
||||
y:{ ticks:{color:'#565e80',font:{size:10}}, grid:{color:'rgba(255,255,255,0.04)'}, stacked:true }
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/../includes/footer.php'; ?>
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
// ================================================================
|
||||
// Admin Login — WebGIS Sebaran Penduduk Miskin
|
||||
// ================================================================
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
|
||||
// Jika sudah login, redirect ke dashboard
|
||||
if (isLoggedIn()) {
|
||||
header('Location: ' . BASE_URL . '/admin/');
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = $_GET['error'] ?? '';
|
||||
$errorMsg = match($error) {
|
||||
'empty' => 'Email dan password tidak boleh kosong.',
|
||||
'invalid' => 'Email atau password salah.',
|
||||
default => ''
|
||||
};
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login — <?= APP_NAME ?></title>
|
||||
<meta name="description" content="Login Admin WebGIS Sebaran Penduduk Miskin">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="<?= BASE_URL ?>/assets/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="login-page">
|
||||
<div class="login-card">
|
||||
|
||||
<!-- Logo -->
|
||||
<div class="login-logo">
|
||||
<div class="login-logo-icon">🗺️</div>
|
||||
<h1><?= APP_NAME ?></h1>
|
||||
<p>Portal Admin · v<?= APP_VERSION ?></p>
|
||||
</div>
|
||||
|
||||
<!-- Error Alert -->
|
||||
<?php if ($errorMsg): ?>
|
||||
<div class="alert alert-danger" style="margin-bottom:18px;" id="login-error">
|
||||
<i class="bi bi-exclamation-circle-fill"></i> <?= htmlspecialchars($errorMsg) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Form -->
|
||||
<form action="<?= BASE_URL ?>/admin/auth_action.php" method="POST" id="login-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Alamat Email</label>
|
||||
<div class="login-input">
|
||||
<i class="bi bi-envelope-fill"></i>
|
||||
<input type="email" name="email" id="login-email"
|
||||
placeholder="admin@povertymap.id"
|
||||
value="<?= htmlspecialchars($_GET['email'] ?? '') ?>"
|
||||
required autocomplete="email">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:20px;">
|
||||
<label class="form-label">Password</label>
|
||||
<div class="login-input">
|
||||
<i class="bi bi-lock-fill"></i>
|
||||
<input type="password" name="password" id="login-password"
|
||||
placeholder="••••••••" required autocomplete="current-password">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="login-btn" id="btn-login">
|
||||
<i class="bi bi-box-arrow-in-right"></i> Masuk ke Dashboard
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="login-footer">
|
||||
<i class="bi bi-shield-lock"></i> Sistem diproteksi — Akses terbatas untuk staf berwenang
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('login-form').addEventListener('submit', function() {
|
||||
const btn = document.getElementById('btn-login');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Memverifikasi...';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// ================================================================
|
||||
// Logout
|
||||
// ================================================================
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
logoutUser();
|
||||
header('Location: ' . BASE_URL . '/admin/login.php');
|
||||
exit;
|
||||
@@ -0,0 +1,427 @@
|
||||
<?php
|
||||
// ================================================================
|
||||
// Admin: CRUD Penduduk Miskin
|
||||
// ================================================================
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/../config/database.php';
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/functions.php';
|
||||
requireLogin();
|
||||
|
||||
$pageTitle = 'Penduduk Miskin';
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
|
||||
$db = getDB();
|
||||
|
||||
$perPage = 15;
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$search = trim($_GET['search'] ?? '');
|
||||
$filterSt = $_GET['status'] ?? '';
|
||||
$filterKec = $_GET['kecamatan'] ?? '';
|
||||
$filterIb = (int)($_GET['ibadah_id'] ?? 0);
|
||||
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
if ($search) { $where[] = '(pm.kk_nama LIKE ? OR pm.nik LIKE ? OR pm.alamat LIKE ?)'; $q="%$search%"; $params=array_merge($params,[$q,$q,$q]); }
|
||||
if ($filterSt) { $where[] = 'pm.status_bantuan = ?'; $params[] = $filterSt; }
|
||||
if ($filterKec){ $where[] = 'pm.kecamatan = ?'; $params[] = $filterKec; }
|
||||
if ($filterIb) { $where[] = 'pm.rumah_ibadah_id = ?'; $params[] = $filterIb; }
|
||||
|
||||
$ws = implode(' AND ', $where);
|
||||
|
||||
$stmtCnt = $db->prepare("SELECT COUNT(*) FROM penduduk_miskin pm WHERE $ws");
|
||||
$stmtCnt->execute($params);
|
||||
$total = (int)$stmtCnt->fetchColumn();
|
||||
$totalPages = max(1, ceil($total / $perPage));
|
||||
|
||||
$stmt = $db->prepare("
|
||||
SELECT pm.*, ri.nama AS ibadah_nama, ri.jenis AS ibadah_jenis
|
||||
FROM penduduk_miskin pm
|
||||
LEFT JOIN rumah_ibadah ri ON pm.rumah_ibadah_id = ri.id
|
||||
WHERE $ws ORDER BY pm.created_at DESC LIMIT $perPage OFFSET $offset
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$kecList = $db->query("SELECT DISTINCT kecamatan FROM penduduk_miskin WHERE kecamatan IS NOT NULL AND kecamatan != '' ORDER BY kecamatan")->fetchAll(PDO::FETCH_COLUMN);
|
||||
$ibadahList = $db->query("SELECT id, nama, jenis FROM rumah_ibadah ORDER BY nama")->fetchAll();
|
||||
?>
|
||||
|
||||
<!-- ── Page Header ─────────────────────────────────────────── -->
|
||||
<div class="page-header">
|
||||
<div class="page-title-block">
|
||||
<h1>👤 Penduduk Miskin</h1>
|
||||
<div class="page-subtitle">Total <?= $total ?> KK terdaftar</div>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<a href="<?= BASE_URL ?>/?mode=miskin" target="_blank" class="btn btn-ghost btn-sm">
|
||||
<i class="bi bi-map"></i> Tambah di Peta
|
||||
</a>
|
||||
<button class="btn btn-success btn-sm" onclick="openMiskinModal()">
|
||||
<i class="bi bi-plus-lg"></i> Tambah Manual
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Filter ─────────────────────────────────────────────── -->
|
||||
<div class="card" style="margin-bottom:16px;">
|
||||
<div class="card-body" style="padding:14px;">
|
||||
<form method="GET" style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;">
|
||||
<div class="search-bar" style="flex:1;min-width:200px;">
|
||||
<i class="bi bi-search search-ico"></i>
|
||||
<input type="text" class="form-control" name="search" placeholder="Cari nama, NIK, alamat..." value="<?= htmlspecialchars($search) ?>">
|
||||
</div>
|
||||
<div>
|
||||
<select class="form-control" name="status">
|
||||
<option value="">Semua Status</option>
|
||||
<option value="belum" <?= $filterSt==='belum'?'selected':'' ?>>❌ Belum Dibantu</option>
|
||||
<option value="menunggu" <?= $filterSt==='menunggu'?'selected':'' ?>>⏳ Menunggu</option>
|
||||
<option value="sudah" <?= $filterSt==='sudah'?'selected':'' ?>>✅ Sudah Dibantu</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<select class="form-control" name="kecamatan">
|
||||
<option value="">Semua Kecamatan</option>
|
||||
<?php foreach ($kecList as $k): ?>
|
||||
<option value="<?= htmlspecialchars($k) ?>" <?= $filterKec===$k?'selected':'' ?>><?= htmlspecialchars($k) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-search"></i> Filter</button>
|
||||
<a href="<?= BASE_URL ?>/admin/penduduk_miskin.php" class="btn btn-ghost btn-sm"><i class="bi bi-x"></i> Reset</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Table ───────────────────────────────────────────────── -->
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Nama KK</th>
|
||||
<th>NIK</th>
|
||||
<th>Anggota</th>
|
||||
<th>Kecamatan</th>
|
||||
<th>Status</th>
|
||||
<th>Rumah Ibadah</th>
|
||||
<th>Jarak</th>
|
||||
<th>Bantuan</th>
|
||||
<th>Nominal</th>
|
||||
<th>Terdaftar</th>
|
||||
<th class="td-actions">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $i => $row): ?>
|
||||
<tr>
|
||||
<td style="color:var(--text3);font-size:11px;"><?= $offset+$i+1 ?></td>
|
||||
<td style="font-weight:700;"><?= htmlspecialchars($row['kk_nama']) ?></td>
|
||||
<td style="font-size:11px;color:var(--text3);font-family:monospace;"><?= htmlspecialchars($row['nik']?: '—') ?></td>
|
||||
<td style="text-align:center;font-weight:700;"><?= $row['jumlah_anggota'] ?></td>
|
||||
<td style="font-size:12px;color:var(--text2);"><?= htmlspecialchars($row['kecamatan']?: '—') ?></td>
|
||||
<td><span class="badge-status badge-<?= $row['status_bantuan'] ?>"><?= ucfirst($row['status_bantuan']) ?></span></td>
|
||||
<td style="font-size:11px;color:var(--text2);"><?= htmlspecialchars($row['ibadah_nama'] ?: '—') ?></td>
|
||||
<td style="font-size:11px;color:var(--accent);"><?= $row['jarak_ke_ibadah'] ? round($row['jarak_ke_ibadah']).' m' : '—' ?></td>
|
||||
<td style="font-size:11px;color:var(--text2);"><?= htmlspecialchars($row['jenis_bantuan'] ?: '—') ?></td>
|
||||
<td style="font-size:11px;color:var(--green);font-family:monospace;"><?= $row['nominal_bantuan'] ? 'Rp '.number_format($row['nominal_bantuan'],0,',','.') : '—' ?></td>
|
||||
<td style="font-size:11px;color:var(--text3);"><?= formatTanggal($row['created_at']) ?></td>
|
||||
<td class="td-actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick='editMiskinRow(<?= htmlspecialchars(json_encode($row)) ?>)' title="Edit">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
<?php if ($row['bukti_file']): ?>
|
||||
<a href="<?= BASE_URL ?>/assets/uploads/<?= htmlspecialchars($row['bukti_file']) ?>" target="_blank" class="btn btn-ghost btn-sm" title="Lihat Bukti">
|
||||
<i class="bi bi-paperclip"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteMiskin(<?= $row['id'] ?>, '<?= addslashes($row['kk_nama']) ?>')" title="Hapus">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($rows)): ?>
|
||||
<tr><td colspan="11" style="text-align:center;padding:32px;color:var(--text3);">
|
||||
<div style="font-size:28px;margin-bottom:6px;">👤</div>Tidak ada data ditemukan
|
||||
</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div style="padding:14px 18px;border-top:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;">
|
||||
<div style="font-size:12px;color:var(--text2);">Menampilkan <?= $offset+1 ?>–<?= min($offset+$perPage,$total) ?> dari <?= $total ?> data</div>
|
||||
<div class="pagination">
|
||||
<?php
|
||||
$base = BASE_URL . '/admin/penduduk_miskin.php?' . http_build_query(array_filter(['search'=>$search,'status'=>$filterSt,'kecamatan'=>$filterKec]));
|
||||
for ($p = 1; $p <= $totalPages; $p++):
|
||||
?>
|
||||
<a href="<?= $base ?>&page=<?= $p ?>" class="page-btn <?= $p===$page?'active':'' ?>"><?= $p ?></a>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- ── Modal Tambah/Edit Penduduk ─────────────────────────── -->
|
||||
<div class="modal fade" id="modal-miskin" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="miskin-modal-title">👤 Tambah Penduduk Miskin</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="m-edit-id">
|
||||
|
||||
<div class="section-label">📍 Lokasi</div>
|
||||
<div class="form-row" style="margin-bottom:12px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Latitude <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-control" id="m-lat" type="number" step="any" placeholder="-0.0263">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Longitude <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-control" id="m-lng" type="number" step="any" placeholder="109.3425">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:12px;">
|
||||
<div class="form-group"><label class="form-label">Kelurahan</label><input class="form-control" id="m-kelurahan" placeholder="Kelurahan"></div>
|
||||
<div class="form-group"><label class="form-label">Kecamatan</label><input class="form-control" id="m-kecamatan" placeholder="Kecamatan"></div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:14px;">
|
||||
<label class="form-label">Alamat Lengkap</label>
|
||||
<input class="form-control" id="m-alamat" placeholder="Jl. xxx No. xxx">
|
||||
</div>
|
||||
|
||||
<div class="section-label">👨👩👧 Data Keluarga</div>
|
||||
<div class="form-row" style="margin-bottom:12px;">
|
||||
<div class="form-group"><label class="form-label">Nama KK <span style="color:var(--red)">*</span></label><input class="form-control" id="m-kk-nama" placeholder="Nama kepala keluarga"></div>
|
||||
<div class="form-group"><label class="form-label">NIK KK</label><input class="form-control" id="m-nik" placeholder="16 digit" maxlength="16"></div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:14px;">
|
||||
<label class="form-label">Jumlah Anggota Keluarga</label>
|
||||
<input class="form-control" id="m-jumlah" type="number" min="1" value="1" style="max-width:160px;">
|
||||
</div>
|
||||
|
||||
<div class="section-label">👥 Detail Anggota</div>
|
||||
<div id="m-anggota-container"></div>
|
||||
<button type="button" class="btn btn-outline-accent btn-sm" onclick="addMAnggota()" style="width:100%;margin-bottom:14px;">
|
||||
<i class="bi bi-plus-circle"></i> Tambah Anggota
|
||||
</button>
|
||||
|
||||
<div class="section-label">🤝 Status Bantuan</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status <span style="color:var(--red)">*</span></label>
|
||||
<select class="form-control" id="m-status" onchange="toggleMBantuan()">
|
||||
<option value="belum">❌ Belum Dibantu</option>
|
||||
<option value="menunggu">⏳ Menunggu Verifikasi</option>
|
||||
<option value="sudah">✅ Sudah Dibantu</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="m-bantuan-fields" style="display:none;">
|
||||
<div class="form-row" style="margin-bottom:12px;">
|
||||
<div class="form-group"><label class="form-label">Jenis Bantuan</label><input class="form-control" id="m-jenis-bantuan" placeholder="Mis. Sembako"></div>
|
||||
<div class="form-group"><label class="form-label">Tanggal Bantuan</label><input class="form-control" id="m-tanggal" type="date"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">💰 Nominal Bantuan (Rupiah)</label>
|
||||
<div style="position:relative;">
|
||||
<span style="position:absolute;left:12px;top:50%;transform:translateY(-50%);font-size:12px;color:var(--text2);font-weight:700;pointer-events:none;">Rp</span>
|
||||
<input class="form-control" id="m-nominal"
|
||||
type="text" inputmode="numeric"
|
||||
placeholder="0"
|
||||
style="padding-left:36px;font-family:'JetBrains Mono',monospace;"
|
||||
oninput="formatNominalInput(this)">
|
||||
</div>
|
||||
<div style="font-size:10px;color:var(--text3);margin-top:4px;">Kosongkan jika belum ditentukan nominal</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-label">📎 Bukti (Opsional)</div>
|
||||
<div class="form-group">
|
||||
<input type="file" class="form-control" id="m-bukti" accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx">
|
||||
<div class="form-hint">JPG, PNG, PDF, DOC maks 10 MB. Biarkan kosong jika tidak diubah.</div>
|
||||
<div id="m-bukti-info" style="margin-top:6px;font-size:11px;color:var(--text2);"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-label">🏛️ Rumah Ibadah (opsional override)</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Rumah Ibadah Terdekat (akan dipilih otomatis)</label>
|
||||
<select class="form-control" id="m-ibadah-id">
|
||||
<option value="">— Otomatis dari koordinat —</option>
|
||||
<?php foreach ($ibadahList as $ib): ?>
|
||||
<option value="<?= $ib['id'] ?>"><?= htmlspecialchars($ib['nama']) ?> (<?= $ib['jenis'] ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" data-bs-dismiss="modal"><i class="bi bi-x-lg"></i> Batal</button>
|
||||
<button type="button" class="btn btn-success" id="btn-save-m"><i class="bi bi-floppy2-fill"></i> Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const BASE = '<?= BASE_URL ?>';
|
||||
let mAIdx = 0;
|
||||
|
||||
function formatNominalInput(el) {
|
||||
let raw = el.value.replace(/[^0-9]/g, '');
|
||||
el.value = raw.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
||||
}
|
||||
|
||||
function openMiskinModal() { fillMModal(null); }
|
||||
|
||||
function editMiskinRow(data) { fillMModal(data); }
|
||||
|
||||
function fillMModal(data) {
|
||||
const isEdit = data !== null;
|
||||
document.getElementById('miskin-modal-title').textContent = isEdit ? '✏️ Edit Penduduk Miskin' : '👤 Tambah Penduduk Miskin';
|
||||
document.getElementById('m-edit-id').value = isEdit ? data.id : '';
|
||||
document.getElementById('m-kk-nama').value = isEdit ? data.kk_nama : '';
|
||||
document.getElementById('m-nik').value = isEdit ? (data.nik||'') : '';
|
||||
document.getElementById('m-jumlah').value = isEdit ? (data.jumlah_anggota||1) : 1;
|
||||
document.getElementById('m-lat').value = isEdit ? data.lat : '';
|
||||
document.getElementById('m-lng').value = isEdit ? data.lng : '';
|
||||
document.getElementById('m-kelurahan').value = isEdit ? (data.kelurahan||'') : '';
|
||||
document.getElementById('m-kecamatan').value = isEdit ? (data.kecamatan||'') : '';
|
||||
document.getElementById('m-alamat').value = isEdit ? (data.alamat||'') : '';
|
||||
document.getElementById('m-status').value = isEdit ? data.status_bantuan : 'belum';
|
||||
document.getElementById('m-jenis-bantuan').value = isEdit ? (data.jenis_bantuan||'') : '';
|
||||
document.getElementById('m-tanggal').value = isEdit ? (data.tanggal_bantuan||'') : '';
|
||||
|
||||
// Set nominal bantuan
|
||||
const nomEl = document.getElementById('m-nominal');
|
||||
if (nomEl) {
|
||||
const rawNom = isEdit && data.nominal_bantuan ? parseFloat(data.nominal_bantuan) : '';
|
||||
nomEl.value = rawNom !== '' ? rawNom.toLocaleString('id-ID', {maximumFractionDigits:0}) : '';
|
||||
}
|
||||
|
||||
document.getElementById('m-ibadah-id').value = isEdit ? (data.rumah_ibadah_id||'') : '';
|
||||
document.getElementById('m-bukti').value = '';
|
||||
document.getElementById('m-bukti-info').innerHTML = isEdit && data.bukti_file
|
||||
? `📎 File saat ini: <a href="${BASE}/assets/uploads/${data.bukti_file}" target="_blank" style="color:var(--accent)">${data.bukti_file}</a>`
|
||||
: '';
|
||||
document.getElementById('m-anggota-container').innerHTML = '';
|
||||
mAIdx = 0;
|
||||
if (isEdit && data.id) loadMAnggota(data.id);
|
||||
toggleMBantuan();
|
||||
new bootstrap.Modal(document.getElementById('modal-miskin')).show();
|
||||
}
|
||||
|
||||
function toggleMBantuan() {
|
||||
const statusVal = document.getElementById('m-status').value;
|
||||
document.getElementById('m-bantuan-fields').style.display =
|
||||
(statusVal === 'sudah' || statusVal === 'menunggu') ? 'block' : 'none';
|
||||
}
|
||||
|
||||
async function loadMAnggota(pid) {
|
||||
try {
|
||||
const r = await fetch(`${BASE}/api/anggota_keluarga.php?penduduk_id=${pid}`);
|
||||
const js = await r.json();
|
||||
if (js.success) js.data.forEach(a => addMAnggota(a));
|
||||
} catch{}
|
||||
}
|
||||
|
||||
function addMAnggota(data = null) {
|
||||
mAIdx++;
|
||||
const c = document.getElementById('m-anggota-container');
|
||||
const d = document.createElement('div');
|
||||
d.className = 'anggota-row'; d.id = `ma-row-${mAIdx}`;
|
||||
d.innerHTML = `
|
||||
<div class="anggota-row-header">
|
||||
<span class="anggota-num">Anggota ${mAIdx}</span>
|
||||
<button class="btn-remove-anggota" type="button" onclick="document.getElementById('ma-row-${mAIdx}').remove()">✕</button>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="margin-bottom:0"><label class="form-label">Nama *</label><input class="form-control ma-nama" id="ma-nama-${mAIdx}" value="${data?.nama||''}"></div>
|
||||
<div class="form-group" style="margin-bottom:0"><label class="form-label">Hubungan</label><input class="form-control" id="ma-hub-${mAIdx}" value="${data?.hubungan||''}"></div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top:8px">
|
||||
<div class="form-group" style="margin-bottom:0"><label class="form-label">Umur</label><input class="form-control" id="ma-umur-${mAIdx}" type="number" min="0" value="${data?.umur||''}"></div>
|
||||
<div class="form-group" style="margin-bottom:0"><label class="form-label">Pekerjaan</label><input class="form-control" id="ma-pkj-${mAIdx}" value="${data?.pekerjaan||''}"></div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:8px;margin-bottom:0">
|
||||
<label class="form-label">Keterangan</label>
|
||||
<input class="form-control" id="ma-ket-${mAIdx}" placeholder="Catatan tambahan (opsional)" value="${data?.keterangan||''}">
|
||||
</div>`;
|
||||
c.appendChild(d);
|
||||
}
|
||||
|
||||
function collectMAnggota() {
|
||||
const rows = document.querySelectorAll('.ma-nama');
|
||||
return Array.from(rows).map(el => {
|
||||
const idx = el.id.replace('ma-nama-','');
|
||||
return {
|
||||
nama: el.value.trim(),
|
||||
hubungan: document.getElementById(`ma-hub-${idx}`)?.value || '',
|
||||
umur: document.getElementById(`ma-umur-${idx}`)?.value || '',
|
||||
pekerjaan: document.getElementById(`ma-pkj-${idx}`)?.value || '',
|
||||
keterangan: document.getElementById(`ma-ket-${idx}`)?.value || ''
|
||||
};
|
||||
}).filter(a => a.nama);
|
||||
}
|
||||
|
||||
document.getElementById('btn-save-m').addEventListener('click', async function() {
|
||||
const nama = document.getElementById('m-kk-nama').value.trim();
|
||||
const lat = document.getElementById('m-lat').value;
|
||||
const lng = document.getElementById('m-lng').value;
|
||||
if (!nama || !lat || !lng) { Swal.fire('Error','Nama KK, Lat, dan Lng wajib diisi!','error'); return; }
|
||||
const editId = document.getElementById('m-edit-id').value;
|
||||
const fd = new FormData();
|
||||
fd.append('kk_nama', nama);
|
||||
fd.append('nik', document.getElementById('m-nik').value);
|
||||
fd.append('jumlah_anggota', document.getElementById('m-jumlah').value||1);
|
||||
fd.append('lat', lat); fd.append('lng', lng);
|
||||
fd.append('kelurahan', document.getElementById('m-kelurahan').value);
|
||||
fd.append('kecamatan', document.getElementById('m-kecamatan').value);
|
||||
fd.append('alamat', document.getElementById('m-alamat').value);
|
||||
fd.append('status_bantuan', document.getElementById('m-status').value);
|
||||
fd.append('jenis_bantuan', document.getElementById('m-jenis-bantuan').value);
|
||||
fd.append('tanggal_bantuan', document.getElementById('m-tanggal').value);
|
||||
|
||||
// Nominal bantuan: hapus titik pemisah ribuan sebelum kirim
|
||||
const nomRaw = (document.getElementById('m-nominal')?.value || '').replace(/\./g,'');
|
||||
fd.append('nominal_bantuan', nomRaw);
|
||||
|
||||
fd.append('anggota', JSON.stringify(collectMAnggota()));
|
||||
const buktiInput = document.getElementById('m-bukti');
|
||||
if (buktiInput.files[0]) fd.append('bukti_file', buktiInput.files[0]);
|
||||
|
||||
this.disabled = true; this.textContent = '⏳ Menyimpan...';
|
||||
try {
|
||||
const url = editId ? `${BASE}/api/penduduk_miskin.php?id=${editId}` : `${BASE}/api/penduduk_miskin.php`;
|
||||
const resp = await fetch(url, { method: editId?'PUT':'POST', body: fd });
|
||||
const json = await resp.json();
|
||||
if (json.success) { location.reload(); } else { Swal.fire('Error', json.message, 'error'); }
|
||||
} catch { Swal.fire('Error','Gagal menyimpan','error'); }
|
||||
finally { this.disabled = false; this.innerHTML = '<i class="bi bi-floppy2-fill"></i> Simpan'; }
|
||||
});
|
||||
|
||||
async function deleteMiskin(id, nama) {
|
||||
const res = await Swal.fire({
|
||||
title: 'Hapus Penduduk?', html: `Data <b>${nama}</b> akan dihapus permanen.`,
|
||||
icon: 'warning', showCancelButton: true, confirmButtonText: 'Ya, Hapus!',
|
||||
cancelButtonText: 'Batal', confirmButtonColor: '#ef4444',
|
||||
background: '#161b27', color: '#e8ecf4'
|
||||
});
|
||||
if (!res.isConfirmed) return;
|
||||
try {
|
||||
const r = await fetch(`${BASE}/api/penduduk_miskin.php?id=${id}`, { method:'DELETE' });
|
||||
const js = await r.json();
|
||||
if (js.success) { Swal.fire({title:'Terhapus!',icon:'success',timer:1500,background:'#161b27',color:'#e8ecf4'}).then(()=>location.reload()); }
|
||||
else Swal.fire('Error', js.message, 'error');
|
||||
} catch { Swal.fire('Error','Gagal menghapus','error'); }
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/../includes/footer.php'; ?>
|
||||
@@ -0,0 +1,328 @@
|
||||
<?php
|
||||
// ================================================================
|
||||
// Admin: CRUD Rumah Ibadah
|
||||
// ================================================================
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/../config/database.php';
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/functions.php';
|
||||
requireLogin();
|
||||
|
||||
$pageTitle = 'Rumah Ibadah';
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
|
||||
$db = getDB();
|
||||
|
||||
// Pagination
|
||||
$perPage = 15;
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
// Filter & search
|
||||
$search = trim($_GET['search'] ?? '');
|
||||
$filterJenis = $_GET['jenis'] ?? '';
|
||||
$filterKec = $_GET['kecamatan'] ?? '';
|
||||
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
if ($search) { $where[] = '(nama LIKE ? OR kontak LIKE ? OR alamat LIKE ?)'; $q = "%$search%"; $params = array_merge($params, [$q,$q,$q]); }
|
||||
if ($filterJenis) { $where[] = 'jenis = ?'; $params[] = $filterJenis; }
|
||||
if ($filterKec) { $where[] = 'kecamatan = ?'; $params[] = $filterKec; }
|
||||
|
||||
$whereStr = implode(' AND ', $where);
|
||||
$total = (int)$db->prepare("SELECT COUNT(*) FROM rumah_ibadah WHERE $whereStr")->execute($params) ? $db->prepare("SELECT COUNT(*) FROM rumah_ibadah WHERE $whereStr")->execute($params) : 0;
|
||||
|
||||
// Re-execute for count
|
||||
$stmtCount = $db->prepare("SELECT COUNT(*) FROM rumah_ibadah WHERE $whereStr");
|
||||
$stmtCount->execute($params);
|
||||
$total = (int)$stmtCount->fetchColumn();
|
||||
$totalPages = max(1, ceil($total / $perPage));
|
||||
|
||||
$stmt = $db->prepare("
|
||||
SELECT ri.*,
|
||||
(SELECT COUNT(*) FROM penduduk_miskin pm WHERE pm.rumah_ibadah_id = ri.id) AS jumlah_binaan
|
||||
FROM rumah_ibadah ri
|
||||
WHERE $whereStr ORDER BY ri.created_at DESC LIMIT $perPage OFFSET $offset
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
// Kecamatan list for filter
|
||||
$kecamatanList = $db->query("SELECT DISTINCT kecamatan FROM rumah_ibadah WHERE kecamatan IS NOT NULL AND kecamatan != '' ORDER BY kecamatan")->fetchAll(PDO::FETCH_COLUMN);
|
||||
?>
|
||||
|
||||
<!-- ── Page Header ─────────────────────────────────────────── -->
|
||||
<div class="page-header">
|
||||
<div class="page-title-block">
|
||||
<h1>🏛️ Rumah Ibadah</h1>
|
||||
<div class="page-subtitle">Total <?= $total ?> data terdaftar</div>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<a href="<?= BASE_URL ?>/?mode=ibadah" target="_blank" class="btn btn-ghost btn-sm">
|
||||
<i class="bi bi-map"></i> Tambah di Peta
|
||||
</a>
|
||||
<button class="btn btn-primary btn-sm" onclick="openModal()">
|
||||
<i class="bi bi-plus-lg"></i> Tambah Manual
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Filter Bar ──────────────────────────────────────────── -->
|
||||
<div class="card" style="margin-bottom:16px;">
|
||||
<div class="card-body" style="padding:14px;">
|
||||
<form method="GET" style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;">
|
||||
<div class="search-bar" style="flex:1;min-width:200px;">
|
||||
<i class="bi bi-search search-ico"></i>
|
||||
<input type="text" class="form-control" name="search" placeholder="Cari nama, kontak, alamat..." value="<?= htmlspecialchars($search) ?>">
|
||||
</div>
|
||||
<div>
|
||||
<select class="form-control" name="jenis">
|
||||
<option value="">Semua Jenis</option>
|
||||
<?php foreach (array_keys(JENIS_IBADAH) as $j): ?>
|
||||
<option value="<?= $j ?>" <?= $filterJenis===$j?'selected':'' ?>><?= JENIS_EMOJI[$j]??'' ?> <?= $j ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<select class="form-control" name="kecamatan">
|
||||
<option value="">Semua Kecamatan</option>
|
||||
<?php foreach ($kecamatanList as $k): ?>
|
||||
<option value="<?= htmlspecialchars($k) ?>" <?= $filterKec===$k?'selected':'' ?>><?= htmlspecialchars($k) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-search"></i> Filter</button>
|
||||
<a href="<?= BASE_URL ?>/admin/rumah_ibadah.php" class="btn btn-ghost btn-sm"><i class="bi bi-x"></i> Reset</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Table ───────────────────────────────────────────────── -->
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Nama</th>
|
||||
<th>Jenis</th>
|
||||
<th>Kontak</th>
|
||||
<th>Kecamatan</th>
|
||||
<th>Radius</th>
|
||||
<th>Binaan</th>
|
||||
<th>Koordinat</th>
|
||||
<th>Ditambahkan</th>
|
||||
<th class="td-actions">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $i => $row): ?>
|
||||
<tr>
|
||||
<td style="color:var(--text3);font-size:11px;"><?= $offset + $i + 1 ?></td>
|
||||
<td style="font-weight:700;"><?= htmlspecialchars($row['nama']) ?></td>
|
||||
<td>
|
||||
<span class="badge-status" style="background:rgba(91,127,255,0.12);color:var(--accent)">
|
||||
<?= JENIS_EMOJI[$row['jenis']]??'' ?> <?= $row['jenis'] ?>
|
||||
</span>
|
||||
</td>
|
||||
<td style="color:var(--text2);font-size:12px;"><?= htmlspecialchars($row['kontak'] ?: '—') ?></td>
|
||||
<td style="color:var(--text2);font-size:12px;"><?= htmlspecialchars($row['kecamatan'] ?: '—') ?></td>
|
||||
<td style="color:var(--accent);font-weight:700;"><?= formatRadius($row['radius']) ?></td>
|
||||
<td>
|
||||
<?php if ($row['jumlah_binaan'] > 0): ?>
|
||||
<a href="<?= BASE_URL ?>/admin/penduduk_miskin.php?ibadah_id=<?= $row['id'] ?>" style="color:var(--green);font-weight:700;text-decoration:none;"><?= $row['jumlah_binaan'] ?> warga</a>
|
||||
<?php else: ?>
|
||||
<span style="color:var(--text3)">0</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="font-size:10px;color:var(--text3);font-family:monospace;"><?= number_format($row['lat'],5) ?>, <?= number_format($row['lng'],5) ?></td>
|
||||
<td style="font-size:11px;color:var(--text3);"><?= formatTanggal($row['created_at']) ?></td>
|
||||
<td class="td-actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick="editRow(<?= htmlspecialchars(json_encode($row)) ?>)" title="Edit">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
<a href="<?= BASE_URL ?>/?focus=ibadah&id=<?= $row['id'] ?>" target="_blank" class="btn btn-ghost btn-sm" title="Lihat di Peta">
|
||||
<i class="bi bi-map-fill"></i>
|
||||
</a>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteRow(<?= $row['id'] ?>, '<?= addslashes($row['nama']) ?>')" title="Hapus">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($rows)): ?>
|
||||
<tr><td colspan="10" style="text-align:center;padding:32px;color:var(--text3);">
|
||||
<div style="font-size:28px;margin-bottom:6px;">🏛️</div>
|
||||
Tidak ada data ditemukan
|
||||
</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<div style="padding:14px 18px;border-top:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;">
|
||||
<div style="font-size:12px;color:var(--text2);">
|
||||
Menampilkan <?= $offset+1 ?>–<?= min($offset+$perPage,$total) ?> dari <?= $total ?> data
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<?php
|
||||
$base = BASE_URL . '/admin/rumah_ibadah.php?' . http_build_query(array_filter(['search'=>$search,'jenis'=>$filterJenis,'kecamatan'=>$filterKec]));
|
||||
for ($p = 1; $p <= $totalPages; $p++):
|
||||
?>
|
||||
<a href="<?= $base ?>&page=<?= $p ?>" class="page-btn <?= $p===$page?'active':'' ?>"><?= $p ?></a>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- ── Modal Tambah/Edit ───────────────────────────────────── -->
|
||||
<div class="modal fade" id="modal-ibadah" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modal-title">🏛️ Tambah Rumah Ibadah</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="edit-id">
|
||||
<div class="section-label">📍 Lokasi</div>
|
||||
<div class="form-row" style="margin-bottom:14px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Latitude <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-control" id="f-lat" type="number" step="any" placeholder="-0.0263">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Longitude <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-control" id="f-lng" type="number" step="any" placeholder="109.3425">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:14px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kelurahan</label>
|
||||
<input class="form-control" id="f-kelurahan" placeholder="Kelurahan">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kecamatan</label>
|
||||
<input class="form-control" id="f-kecamatan" placeholder="Kecamatan">
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-label">🕌 Data Ibadah</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nama Rumah Ibadah <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-control" id="f-nama" placeholder="Nama lengkap rumah ibadah">
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:14px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Jenis</label>
|
||||
<select class="form-control" id="f-jenis">
|
||||
<option value="Masjid">🕌 Masjid</option>
|
||||
<option value="Musholla">🕌 Musholla</option>
|
||||
<option value="Gereja">⛪ Gereja</option>
|
||||
<option value="Katolik">⛪ Gereja Katolik</option>
|
||||
<option value="Pura">🛕 Pura</option>
|
||||
<option value="Vihara">🛕 Vihara</option>
|
||||
<option value="Klenteng">⛩️ Klenteng</option>
|
||||
<option value="Lainnya">🙏 Lainnya</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Kontak</label>
|
||||
<input class="form-control" id="f-kontak" placeholder="Telepon / WhatsApp">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Alamat Lengkap</label>
|
||||
<input class="form-control" id="f-alamat" placeholder="Jalan, nomor, dll.">
|
||||
</div>
|
||||
<div class="section-label">📏 Radius Layanan</div>
|
||||
<div class="form-group">
|
||||
<div class="range-wrapper">
|
||||
<input type="range" id="f-radius" min="100" max="5000" step="50" value="300" style="flex:1;accent-color:var(--accent);">
|
||||
<span class="range-val" id="f-radius-val">300 m</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" data-bs-dismiss="modal"><i class="bi bi-x-lg"></i> Batal</button>
|
||||
<button type="button" class="btn btn-primary" id="btn-save"><i class="bi bi-floppy2-fill"></i> Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const BASE = '<?= BASE_URL ?>';
|
||||
|
||||
// Radius slider
|
||||
document.getElementById('f-radius').addEventListener('input', function() {
|
||||
const v = parseInt(this.value);
|
||||
document.getElementById('f-radius-val').textContent = v >= 1000 ? (v/1000).toFixed(1)+' km' : v+' m';
|
||||
});
|
||||
|
||||
function openModal(data = null) {
|
||||
const isEdit = data !== null;
|
||||
document.getElementById('modal-title').textContent = isEdit ? '✏️ Edit Rumah Ibadah' : '🏛️ Tambah Rumah Ibadah';
|
||||
document.getElementById('edit-id').value = isEdit ? data.id : '';
|
||||
document.getElementById('f-nama').value = isEdit ? data.nama : '';
|
||||
document.getElementById('f-jenis').value = isEdit ? data.jenis : 'Masjid';
|
||||
document.getElementById('f-kontak').value = isEdit ? (data.kontak || '') : '';
|
||||
document.getElementById('f-alamat').value = isEdit ? (data.alamat || '') : '';
|
||||
document.getElementById('f-kelurahan').value = isEdit ? (data.kelurahan||'') : '';
|
||||
document.getElementById('f-kecamatan').value = isEdit ? (data.kecamatan||'') : '';
|
||||
document.getElementById('f-lat').value = isEdit ? data.lat : '';
|
||||
document.getElementById('f-lng').value = isEdit ? data.lng : '';
|
||||
const rad = isEdit ? parseInt(data.radius) : 300;
|
||||
document.getElementById('f-radius').value = rad;
|
||||
document.getElementById('f-radius-val').textContent = rad >= 1000 ? (rad/1000).toFixed(1)+' km' : rad+' m';
|
||||
new bootstrap.Modal(document.getElementById('modal-ibadah')).show();
|
||||
}
|
||||
|
||||
function editRow(data) { openModal(data); }
|
||||
|
||||
document.getElementById('btn-save').addEventListener('click', async function() {
|
||||
const nama = document.getElementById('f-nama').value.trim();
|
||||
const lat = document.getElementById('f-lat').value;
|
||||
const lng = document.getElementById('f-lng').value;
|
||||
if (!nama || !lat || !lng) { Swal.fire('Error','Nama, Lat, dan Lng wajib diisi!','error'); return; }
|
||||
const editId = document.getElementById('edit-id').value;
|
||||
const payload = {
|
||||
nama, jenis: document.getElementById('f-jenis').value,
|
||||
kontak: document.getElementById('f-kontak').value,
|
||||
alamat: document.getElementById('f-alamat').value,
|
||||
kelurahan: document.getElementById('f-kelurahan').value,
|
||||
kecamatan: document.getElementById('f-kecamatan').value,
|
||||
lat: parseFloat(lat), lng: parseFloat(lng),
|
||||
radius: parseInt(document.getElementById('f-radius').value)
|
||||
};
|
||||
this.disabled = true; this.textContent = '⏳ Menyimpan...';
|
||||
try {
|
||||
const url = editId ? `${BASE}/api/rumah_ibadah.php?id=${editId}` : `${BASE}/api/rumah_ibadah.php`;
|
||||
const resp = await fetch(url, { method: editId?'PUT':'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
|
||||
const json = await resp.json();
|
||||
if (json.success) { location.reload(); } else { Swal.fire('Error', json.message, 'error'); }
|
||||
} catch(e) { Swal.fire('Error','Gagal menyimpan data','error'); }
|
||||
finally { this.disabled = false; this.innerHTML = '<i class="bi bi-floppy2-fill"></i> Simpan'; }
|
||||
});
|
||||
|
||||
async function deleteRow(id, nama) {
|
||||
const res = await Swal.fire({
|
||||
title: 'Hapus Rumah Ibadah?',
|
||||
html: `<b>${nama}</b> akan dihapus permanen. Warga binaan akan di-reset.`,
|
||||
icon: 'warning', showCancelButton: true,
|
||||
confirmButtonText: 'Ya, Hapus!', cancelButtonText: 'Batal',
|
||||
confirmButtonColor: '#ef4444', background: '#161b27', color: '#e8ecf4'
|
||||
});
|
||||
if (!res.isConfirmed) return;
|
||||
try {
|
||||
const r = await fetch(`${BASE}/api/rumah_ibadah.php?id=${id}`, { method:'DELETE' });
|
||||
const js = await r.json();
|
||||
if (js.success) { Swal.fire({title:'Terhapus!',icon:'success',timer:1500,background:'#161b27',color:'#e8ecf4'}).then(() => location.reload()); }
|
||||
else Swal.fire('Error', js.message, 'error');
|
||||
} catch { Swal.fire('Error','Gagal menghapus','error'); }
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/../includes/footer.php'; ?>
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
// ================================================================
|
||||
// Admin: Manajemen User
|
||||
// ================================================================
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/../config/database.php';
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
require_once __DIR__ . '/../includes/functions.php';
|
||||
requireLogin();
|
||||
requireAdmin(); // Hanya admin
|
||||
|
||||
$pageTitle = 'Manajemen Pengguna';
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
|
||||
$db = getDB();
|
||||
$current = getCurrentUser();
|
||||
$users = $db->query("SELECT id, nama, email, role, created_at FROM users ORDER BY role DESC, nama ASC")->fetchAll();
|
||||
?>
|
||||
|
||||
<!-- ── Page Header ─────────────────────────────────────────── -->
|
||||
<div class="page-header">
|
||||
<div class="page-title-block">
|
||||
<h1><i class="bi bi-person-gear"></i> Manajemen Pengguna</h1>
|
||||
<div class="page-subtitle">Total <?= count($users) ?> pengguna terdaftar</div>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<button class="btn btn-primary btn-sm" onclick="openUserModal()">
|
||||
<i class="bi bi-person-plus-fill"></i> Tambah Pengguna
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Users Table ─────────────────────────────────────────── -->
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Nama</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Terdaftar</th>
|
||||
<th class="td-actions">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $i => $u): ?>
|
||||
<tr>
|
||||
<td style="color:var(--text3);font-size:11px;"><?= $i + 1 ?></td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<div style="width:34px;height:34px;border-radius:50%;background:<?= $u['role'] === 'admin' ? '#5b7fff' : '#22c55e' ?>;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:14px;flex-shrink:0;">
|
||||
<?= strtoupper(substr($u['nama'], 0, 1)) ?>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-weight:700;"><?= htmlspecialchars($u['nama']) ?></div>
|
||||
<?php if ($u['id'] == $current['id']): ?>
|
||||
<div style="font-size:10px;color:var(--accent);">Anda</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--text2);"><?= htmlspecialchars($u['email']) ?></td>
|
||||
<td>
|
||||
<span class="badge-status" style="background:<?= $u['role'] === 'admin' ? 'rgba(91,127,255,0.15)' : 'rgba(34,197,94,0.15)' ?>;color:<?= $u['role'] === 'admin' ? 'var(--accent)' : 'var(--green)' ?>;">
|
||||
<i class="bi bi-<?= $u['role'] === 'admin' ? 'shield-fill' : 'person-fill' ?>"></i>
|
||||
<?= ucfirst($u['role']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td style="font-size:11px;color:var(--text3);"><?= formatTanggal($u['created_at']) ?></td>
|
||||
<td class="td-actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick='openEditUser(<?= json_encode($u) ?>)' title="Edit">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="openResetPwd(<?= $u['id'] ?>, '<?= addslashes($u['nama']) ?>')" title="Reset Password">
|
||||
<i class="bi bi-key-fill"></i>
|
||||
</button>
|
||||
<?php if ($u['id'] != $current['id']): ?>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteUser(<?= $u['id'] ?>, '<?= addslashes($u['nama']) ?>')" title="Hapus">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<button class="btn btn-ghost btn-sm" disabled title="Tidak bisa hapus akun sendiri">
|
||||
<i class="bi bi-trash" style="opacity:.3"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Info Card ───────────────────────────────────────────── -->
|
||||
<div class="card" style="margin-top:20px;">
|
||||
<div class="card-body" style="padding:16px;">
|
||||
<div style="display:flex;gap:16px;flex-wrap:wrap;font-size:13px;">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="width:12px;height:12px;background:#5b7fff;border-radius:50%;display:inline-block;"></span>
|
||||
<strong>Admin</strong>: Akses penuh ke semua fitur termasuk manajemen user, import/export
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="width:12px;height:12px;background:#22c55e;border-radius:50%;display:inline-block;"></span>
|
||||
<strong>Operator</strong>: Akses CRUD rumah ibadah & penduduk miskin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Modal Tambah/Edit User ─────────────────────────────── -->
|
||||
<div class="modal fade" id="modal-user" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="user-modal-title">👤 Tambah Pengguna</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="u-id">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nama Lengkap <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-control" id="u-nama" placeholder="Nama lengkap pengguna">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-control" id="u-email" type="email" placeholder="email@domain.com">
|
||||
</div>
|
||||
<div class="form-group" id="u-pwd-group">
|
||||
<label class="form-label">Password <span style="color:var(--red)" id="u-pwd-req">*</span></label>
|
||||
<div style="position:relative;">
|
||||
<input class="form-control" id="u-password" type="password" placeholder="Min. 6 karakter" style="padding-right:40px;">
|
||||
<button type="button" onclick="togglePwd()" style="position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--text2);cursor:pointer;">
|
||||
<i class="bi bi-eye" id="pwd-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-hint" id="u-pwd-hint">Biarkan kosong jika tidak ingin mengubah password</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Role <span style="color:var(--red)">*</span></label>
|
||||
<select class="form-control" id="u-role">
|
||||
<option value="operator">Operator</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" data-bs-dismiss="modal"><i class="bi bi-x-lg"></i> Batal</button>
|
||||
<button type="button" class="btn btn-primary" id="btn-save-user"><i class="bi bi-floppy2-fill"></i> Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Modal Reset Password ───────────────────────────────── -->
|
||||
<div class="modal fade" id="modal-reset-pwd" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">🔑 Reset Password</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="rp-id">
|
||||
<div style="margin-bottom:16px;padding:12px;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.3);border-radius:8px;font-size:13px;">
|
||||
<i class="bi bi-exclamation-triangle-fill" style="color:#f59e0b"></i>
|
||||
Reset password untuk: <strong id="rp-nama"></strong>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password Baru <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-control" id="rp-pwd" type="password" placeholder="Min. 6 karakter">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Konfirmasi Password <span style="color:var(--red)">*</span></label>
|
||||
<input class="form-control" id="rp-confirm" type="password" placeholder="Ulangi password baru">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" data-bs-dismiss="modal"><i class="bi bi-x-lg"></i> Batal</button>
|
||||
<button type="button" class="btn btn-warning" id="btn-reset-pwd"><i class="bi bi-key-fill"></i> Reset Password</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const BASE = '<?= BASE_URL ?>';
|
||||
let userModalBs, resetPwdModalBs;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
userModalBs = new bootstrap.Modal(document.getElementById('modal-user'));
|
||||
resetPwdModalBs = new bootstrap.Modal(document.getElementById('modal-reset-pwd'));
|
||||
});
|
||||
|
||||
function openUserModal() {
|
||||
document.getElementById('user-modal-title').textContent = '👤 Tambah Pengguna';
|
||||
document.getElementById('u-id').value = '';
|
||||
document.getElementById('u-nama').value = '';
|
||||
document.getElementById('u-email').value = '';
|
||||
document.getElementById('u-password').value = '';
|
||||
document.getElementById('u-role').value = 'operator';
|
||||
document.getElementById('u-pwd-req').style.display = 'inline';
|
||||
document.getElementById('u-pwd-hint').style.display = 'none';
|
||||
userModalBs.show();
|
||||
}
|
||||
|
||||
function openEditUser(data) {
|
||||
document.getElementById('user-modal-title').textContent = '✏️ Edit Pengguna';
|
||||
document.getElementById('u-id').value = data.id;
|
||||
document.getElementById('u-nama').value = data.nama;
|
||||
document.getElementById('u-email').value = data.email;
|
||||
document.getElementById('u-password').value = '';
|
||||
document.getElementById('u-role').value = data.role;
|
||||
document.getElementById('u-pwd-req').style.display = 'none';
|
||||
document.getElementById('u-pwd-hint').style.display = 'block';
|
||||
userModalBs.show();
|
||||
}
|
||||
|
||||
function openResetPwd(id, nama) {
|
||||
document.getElementById('rp-id').value = id;
|
||||
document.getElementById('rp-nama').textContent = nama;
|
||||
document.getElementById('rp-pwd').value = '';
|
||||
document.getElementById('rp-confirm').value = '';
|
||||
resetPwdModalBs.show();
|
||||
}
|
||||
|
||||
function togglePwd() {
|
||||
const inp = document.getElementById('u-password');
|
||||
const eye = document.getElementById('pwd-eye');
|
||||
if (inp.type === 'password') {
|
||||
inp.type = 'text'; eye.className = 'bi bi-eye-slash';
|
||||
} else {
|
||||
inp.type = 'password'; eye.className = 'bi bi-eye';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Save User ─────────────────────────────────────────────────
|
||||
document.getElementById('btn-save-user').addEventListener('click', async function() {
|
||||
const nama = document.getElementById('u-nama').value.trim();
|
||||
const email = document.getElementById('u-email').value.trim();
|
||||
const pwd = document.getElementById('u-password').value;
|
||||
const role = document.getElementById('u-role').value;
|
||||
const editId = document.getElementById('u-id').value;
|
||||
|
||||
if (!nama || !email) { Swal.fire('Error', 'Nama dan email wajib diisi!', 'error'); return; }
|
||||
if (!editId && !pwd) { Swal.fire('Error', 'Password wajib diisi untuk pengguna baru!', 'error'); return; }
|
||||
if (pwd && pwd.length < 6) { Swal.fire('Error', 'Password minimal 6 karakter!', 'error'); return; }
|
||||
|
||||
const payload = { nama, email, role };
|
||||
if (pwd) payload.password = pwd;
|
||||
|
||||
this.disabled = true; this.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
||||
try {
|
||||
const url = editId ? `${BASE}/api/users.php?id=${editId}` : `${BASE}/api/users.php`;
|
||||
const resp = await fetch(url, { method: editId ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
||||
const json = await resp.json();
|
||||
if (json.success) {
|
||||
Swal.fire({ title: 'Berhasil!', icon: 'success', timer: 1500, background: '#161b27', color: '#e8ecf4' })
|
||||
.then(() => location.reload());
|
||||
} else { Swal.fire('Error', json.message, 'error'); }
|
||||
} catch { Swal.fire('Error', 'Gagal menyimpan data', 'error'); }
|
||||
finally { this.disabled = false; this.innerHTML = '<i class="bi bi-floppy2-fill"></i> Simpan'; }
|
||||
});
|
||||
|
||||
// ── Reset Password ─────────────────────────────────────────────
|
||||
document.getElementById('btn-reset-pwd').addEventListener('click', async function() {
|
||||
const id = document.getElementById('rp-id').value;
|
||||
const pwd = document.getElementById('rp-pwd').value;
|
||||
const confirm = document.getElementById('rp-confirm').value;
|
||||
if (!pwd || pwd.length < 6) { Swal.fire('Error', 'Password minimal 6 karakter!', 'error'); return; }
|
||||
if (pwd !== confirm) { Swal.fire('Error', 'Konfirmasi password tidak cocok!', 'error'); return; }
|
||||
|
||||
this.disabled = true;
|
||||
try {
|
||||
const resp = await fetch(`${BASE}/api/users.php?id=${id}`, {
|
||||
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: pwd })
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (json.success) {
|
||||
Swal.fire({ title: 'Password Direset!', icon: 'success', timer: 1500, background: '#161b27', color: '#e8ecf4' })
|
||||
.then(() => location.reload());
|
||||
} else { Swal.fire('Error', json.message, 'error'); }
|
||||
} catch { Swal.fire('Error', 'Gagal reset password', 'error'); }
|
||||
finally { this.disabled = false; }
|
||||
});
|
||||
|
||||
// ── Delete User ───────────────────────────────────────────────
|
||||
async function deleteUser(id, nama) {
|
||||
const res = await Swal.fire({
|
||||
title: 'Hapus Pengguna?',
|
||||
html: `Pengguna <b>${nama}</b> akan dihapus permanen.`,
|
||||
icon: 'warning', showCancelButton: true,
|
||||
confirmButtonText: 'Ya, Hapus!', cancelButtonText: 'Batal',
|
||||
confirmButtonColor: '#ef4444', background: '#161b27', color: '#e8ecf4'
|
||||
});
|
||||
if (!res.isConfirmed) return;
|
||||
try {
|
||||
const r = await fetch(`${BASE}/api/users.php?id=${id}`, { method: 'DELETE' });
|
||||
const js = await r.json();
|
||||
if (js.success) {
|
||||
Swal.fire({ title: 'Terhapus!', icon: 'success', timer: 1200, background: '#161b27', color: '#e8ecf4' })
|
||||
.then(() => location.reload());
|
||||
} else { Swal.fire('Error', js.message, 'error'); }
|
||||
} catch { Swal.fire('Error', 'Gagal menghapus', 'error'); }
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/../includes/footer.php'; ?>
|
||||
Reference in New Issue
Block a user