210 lines
12 KiB
HTML
210 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="id">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Dasbor Admin - Dinsos Pontianak</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
body { font-family: 'Inter', sans-serif; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-100 text-gray-900 min-h-screen flex flex-col">
|
|
|
|
<nav class="bg-white border-b border-gray-200 p-4 sticky top-0 z-30 flex justify-between items-center px-8">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-3 h-8 bg-gray-800 rounded-full"></div>
|
|
<h1 class="text-lg font-bold tracking-tight text-gray-800">Dinsos Admin Dashboard</h1>
|
|
</div>
|
|
<div class="flex items-center gap-6">
|
|
<span class="text-sm font-medium text-gray-600">Administrator Utama</span>
|
|
<button onclick="eksekusi_logout_admin()" class="text-xs font-bold text-red-600 hover:bg-red-50 px-3 py-2 rounded-lg transition-colors">Keluar Sistem</button>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="flex-grow p-6 max-w-6xl mx-auto w-full space-y-8">
|
|
|
|
<div id="banner_error_admin" class="hidden p-4 bg-red-50 border border-red-200 text-red-700 rounded-xl text-sm font-medium"></div>
|
|
|
|
<section class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div class="bg-white p-5 rounded-xl border border-gray-200 shadow-sm">
|
|
<span class="text-xs text-gray-400 font-bold uppercase tracking-wider block mb-1">Total Baseline</span>
|
|
<p class="text-3xl font-bold text-gray-800" id="stat_total_warga">0</p>
|
|
</div>
|
|
<div class="bg-white p-5 rounded-xl border border-gray-200 shadow-sm border-l-4 border-l-yellow-500">
|
|
<span class="text-xs text-yellow-600 font-bold uppercase tracking-wider block mb-1">Menunggu Validasi</span>
|
|
<p class="text-3xl font-bold text-gray-800" id="stat_pending_warga">0</p>
|
|
</div>
|
|
<div class="bg-white p-5 rounded-xl border border-gray-200 shadow-sm border-l-4 border-l-emerald-600">
|
|
<span class="text-xs text-emerald-600 font-bold uppercase tracking-wider block mb-1">Disetujui (Verified)</span>
|
|
<p class="text-3xl font-bold text-gray-800" id="stat_verified_warga">0</p>
|
|
</div>
|
|
<div class="bg-white p-5 rounded-xl border border-gray-200 shadow-sm border-l-4 border-l-red-600">
|
|
<span class="text-xs text-red-600 font-bold uppercase tracking-wider block mb-1">Ditolak (Rejected)</span>
|
|
<p class="text-3xl font-bold text-gray-800" id="stat_rejected_warga">0</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
|
<div class="p-5 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
|
|
<h3 class="font-bold text-gray-800 text-sm">Verifikasi Data Kemiskinan Masyarakat</h3>
|
|
<span id="label_total_baris" class="text-xs bg-gray-200 text-gray-700 px-2.5 py-1 rounded-md font-semibold">0 Rekaman</span>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-left border-collapse">
|
|
<thead>
|
|
<tr class="bg-gray-100 border-b border-gray-200 text-[11px] font-bold uppercase text-gray-500 tracking-wider">
|
|
<th class="p-4">No</th>
|
|
<th class="p-4">Nama KK</th>
|
|
<th class="p-4">NIK</th>
|
|
<th class="p-4">No KK</th>
|
|
<th class="p-4">Tanggungan</th>
|
|
<th class="p-4 text-center">Status</th>
|
|
<th class="p-4 text-center">Aksi Manajemen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="tabel_body_mustahik" class="divide-y divide-gray-200 text-sm text-gray-700">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div id="pesan_tabel_kosong" class="p-10 text-center text-gray-400 text-sm hidden">Tidak ada rekaman data warga di dalam sistem.</div>
|
|
</section>
|
|
|
|
</main>
|
|
|
|
<script>
|
|
// Fungsi membersihkan dan memperbarui isi baris tabel secara aman (XSS Prevention)
|
|
function muat_data_ke_tabel_admin(daftar_mustahik) {
|
|
const tbody = document.getElementById('tabel_body_mustahik');
|
|
const pesan_kosong = document.getElementById('pesan_tabel_kosong');
|
|
|
|
tbody.innerHTML = '';
|
|
|
|
if (daftar_mustahik.length === 0) {
|
|
pesan_kosong.classList.remove('hidden');
|
|
return;
|
|
}
|
|
pesan_kosong.classList.add('hidden');
|
|
|
|
let hitung_pending = 0;
|
|
let hitung_verified = 0;
|
|
let hitung_rejected = 0;
|
|
|
|
daftar_mustahik.forEach((warga, indeks) => {
|
|
if (warga.status_validasi.toLowerCase() === 'pending') hitung_pending++;
|
|
if (warga.status_validasi.toLowerCase() === 'verified') hitung_verified++;
|
|
if (warga.status_validasi.toLowerCase() === 'rejected') hitung_rejected++;
|
|
|
|
const baris = document.createElement('tr');
|
|
baris.className = 'hover:bg-gray-50 transition-colors';
|
|
|
|
/* Baris Kompleks: Kita menyusun kerangka visual baris menggunakan template literal,
|
|
namun pengisian teks string sensitif tetap diserahkan ke fungsi properti textContent
|
|
pada sub-elemen guna meminimalkan celah eksploitasi kode HTML injeksi berbahaya. */
|
|
baris.innerHTML = `
|
|
<td class="p-4 text-xs font-mono text-gray-400">${indeks + 1}</td>
|
|
<td class="p-4 font-semibold text-gray-900 kolom-nama"></td>
|
|
<td class="p-4 text-xs font-mono kolom-nik"></td>
|
|
<td class="p-4 text-xs font-mono kolom-nokk"></td>
|
|
<td class="p-4 text-center kolom-tanggungan"></td>
|
|
<td class="p-4 text-center"><span class="inline-block px-2.5 py-1 text-[10px] font-bold rounded-md kolom-badge"></span></td>
|
|
<td class="p-4">
|
|
<div class="flex justify-center gap-2">
|
|
<a href="admin_detail.html?id=${warga.id}" class="px-2.5 py-1.5 bg-blue-50 text-blue-700 hover:bg-blue-100 border border-blue-200 text-xs font-medium rounded-lg transition-colors">Detail</a>
|
|
<button onclick="ubah_status_verifikasi(${warga.id}, 'Verified')" class="px-2.5 py-1.5 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 border border-emerald-200 text-xs font-medium rounded-lg transition-colors">Setuju</button>
|
|
<button onclick="ubah_status_verifikasi(${warga.id}, 'Rejected')" class="px-2.5 py-1.5 bg-red-50 text-red-700 hover:bg-red-100 border border-red-200 text-xs font-medium rounded-lg transition-colors">Tolak</button>
|
|
</div>
|
|
</td>
|
|
`;
|
|
|
|
// Menyuntikkan nilai teks string secara terisolasi dan aman
|
|
baris.querySelector('.kolom-nama').textContent = warga.nama_kk;
|
|
baris.querySelector('.kolom-nik').textContent = warga.nik;
|
|
baris.querySelector('.kolom-nokk').textContent = warga.no_kk || '-';
|
|
baris.querySelector('.kolom-tanggungan').textContent = warga.jumlah_tanggungan;
|
|
|
|
const badge = baris.querySelector('.kolom-badge');
|
|
badge.textContent = warga.status_validasi;
|
|
|
|
if (warga.status_validasi.toLowerCase() === 'verified') {
|
|
badge.className += ' bg-emerald-50 text-emerald-700 border border-emerald-200';
|
|
} else if (warga.status_validasi.toLowerCase() === 'rejected') {
|
|
badge.className += ' bg-red-50 text-red-700 border border-red-200';
|
|
} else {
|
|
badge.className += ' bg-yellow-50 text-yellow-700 border border-yellow-200';
|
|
}
|
|
|
|
tbody.appendChild(baris);
|
|
});
|
|
|
|
// Perbarui papan indikator agregat atas secara asinkron
|
|
document.getElementById('label_total_baris').textContent = `${daftar_mustahik.length} Rekaman`;
|
|
document.getElementById('stat_total_warga').textContent = daftar_mustahik.length;
|
|
document.getElementById('stat_pending_warga').textContent = hitung_pending;
|
|
document.getElementById('stat_verified_warga').textContent = hitung_verified;
|
|
document.getElementById('stat_rejected_warga').textContent = hitung_rejected;
|
|
}
|
|
|
|
// Meminta daftar seluruh entitas warga dari sistem peladen Express
|
|
async function ambil_data_admin_mustahik() {
|
|
const banner_error = document.getElementById('banner_error_admin');
|
|
try {
|
|
banner_error.classList.add('hidden');
|
|
|
|
const http_response = await fetch('http://localhost:3000/api/admin/mustahik');
|
|
const json_result = await http_response.json();
|
|
|
|
if (!http_response.ok) throw new Error(json_result.error || 'Gagal menarik data dari server.');
|
|
|
|
muat_data_ke_tabel_admin(json_result);
|
|
|
|
} catch (error_sistem) {
|
|
banner_error.textContent = 'Kesalahan Komunikasi Server: ' + error_sistem.message;
|
|
banner_error.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
// Mengubah status persetujuan verifikasi warga via metode AJAX PATCH menuju backend
|
|
async function ubah_status_verifikasi(id_warga, status_baru) {
|
|
const banner_error = document.getElementById('banner_error_admin');
|
|
|
|
if (!confirm(`Apakah Anda yakin ingin mengubah status data ini menjadi ${status_baru}?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
/* Baris Kompleks: Melakukan transmisi parsial status data menggunakan metode HTTP PATCH
|
|
menuju endpoint verifikasi admin, membawa payload status terenkapsulasi JSON
|
|
sehingga perubahan status langsung tercermin di file SQLite tanpa memuat ulang layar. */
|
|
const http_response = await fetch(`http://localhost:3000/api/admin/verifikasi/${id_warga}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ status_validasi: status_baru })
|
|
});
|
|
|
|
const json_result = await http_response.json();
|
|
|
|
if (!http_response.ok) throw new Error(json_result.error || 'Gagal mengubah status verifikasi.');
|
|
|
|
// Segera sinkronkan ulang isi tabel setelah proses mutasi data berhasil
|
|
ambil_data_admin_mustahik();
|
|
|
|
} catch (error_sistem) {
|
|
banner_error.textContent = 'Gagal memproses mutasi data: ' + error_sistem.message;
|
|
banner_error.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
// Mengakhiri sesi masuk administrator utama
|
|
function eksekusi_logout_admin() {
|
|
localStorage.removeItem('token_admin');
|
|
window.location.href = 'index.html';
|
|
}
|
|
|
|
// Inisialisasi awal penarikan data saat halaman terbuka sempurna
|
|
ambil_data_admin_mustahik();
|
|
</script>
|
|
</body>
|
|
</html> |