feat : first commit

This commit is contained in:
unknown
2026-06-03 20:05:50 +07:00
commit 9cd232bf2d
6701 changed files with 440316 additions and 0 deletions
+210
View File
@@ -0,0 +1,210 @@
<!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>
+206
View File
@@ -0,0 +1,206 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Detail Warga & Peta Lokal - Admin</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></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-950 min-h-screen flex flex-col">
<nav class="bg-white border-b border-gray-200 p-4 flex items-center gap-4 sticky top-0 z-30 px-6">
<a href="admin_dasbor.html" class="hover:bg-gray-100 p-2 rounded-lg transition-colors text-gray-600">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
</a>
<h1 class="text-md font-bold text-gray-800">Detail Rekaman Data Validasi</h1>
</nav>
<main class="flex-grow p-4 md:p-6 max-w-5xl mx-auto w-full grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
<div class="md:col-span-2 space-y-6">
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-200 space-y-4">
<div class="flex justify-between items-start border-b pb-3">
<div>
<h2 id="det_nama_kk" class="text-xl font-bold text-gray-900">-</h2>
<p class="text-xs text-gray-400 font-mono mt-1">Sistem ID: <span id="det_id_sistem">-</span></p>
</div>
<span id="det_badge_status" class="inline-block px-2.5 py-1 text-[10px] font-bold rounded-md">Pending</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div>
<span class="text-xs text-gray-400 block">Nomor Induk Kependudukan (NIK)</span>
<p id="det_nik" class="font-mono font-medium mt-0.5">-</p>
</div>
<div>
<span class="text-xs text-gray-400 block">Nomor Kartu Keluarga (No KK)</span>
<p id="det_no_kk" class="font-mono font-medium mt-0.5">-</p>
</div>
<div>
<span class="text-xs text-gray-400 block">Jumlah Tanggungan Keluarga</span>
<p id="det_tanggungan" class="font-medium mt-0.5">-</p>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="bg-gray-50 p-4 border-b border-gray-200">
<h3 class="text-xs font-bold uppercase tracking-wider text-gray-500">Evaluasi Parameter Instrumen Kemiskinan BPS</h3>
</div>
<div class="divide-y divide-gray-100 text-sm">
<div class="p-4 flex justify-between"><span class="text-gray-500">Luas Lantai Per Kapita</span><span id="det_luas_lantai" class="font-semibold text-gray-800">-</span></div>
<div class="p-4 flex justify-between"><span class="text-gray-500">Material Bahan Lantai</span><span id="det_mat_lantai" class="font-semibold text-gray-800">-</span></div>
<div class="p-4 flex justify-between"><span class="text-gray-500">Material Bahan Dinding</span><span id="det_mat_dinding" class="font-semibold text-gray-800">-</span></div>
<div class="p-4 flex justify-between"><span class="text-gray-500">Fasilitas Sanitasi (MCK)</span><span id="det_sanitasi" class="font-semibold text-gray-800">-</span></div>
<div class="p-4 flex justify-between"><span class="text-gray-500">Sumber Air Minum Utama</span><span id="det_air_minum" class="font-semibold text-gray-800">-</span></div>
<div class="p-4 flex justify-between"><span class="text-gray-500">Sumber Penerangan Rumah</span><span id="det_penerangan" class="font-semibold text-gray-800">-</span></div>
<div class="p-4 flex justify-between"><span class="text-gray-500">Pekerjaan Utama Kepala Keluarga</span><span id="det_pekerjaan" class="font-semibold text-gray-800">-</span></div>
<div class="p-4 flex justify-between"><span class="text-gray-500">Estimasi Pendapatan Bulanan</span><span id="det_pendapatan" class="font-semibold text-emerald-700">-</span></div>
<div class="p-4 flex justify-between"><span class="text-gray-500">Frekuensi Makan Harian</span><span id="det_frekuensi_makan" class="font-semibold text-gray-800">-</span></div>
<div class="p-4 flex justify-between"><span class="text-gray-500">Kepemilikan Aset Kendaraan</span><span id="det_kepemilikan_aset" class="font-semibold text-gray-800">-</span></div>
</div>
</div>
</div>
<div class="space-y-6">
<div class="bg-white p-4 rounded-xl shadow-sm border border-gray-200 space-y-3">
<h3 class="text-xs font-bold uppercase tracking-wider text-gray-400">Verifikasi Lokasi Spasial</h3>
<div class="h-56 w-full rounded-lg overflow-hidden border border-gray-200 z-0 relative">
<div id="map_detail_container" class="absolute inset-0"></div>
</div>
<div class="text-[11px] font-mono text-gray-500 space-y-0.5">
<p>Lat: <span id="det_lat">-</span></p>
<p>Lng: <span id="det_lng">-</span></p>
</div>
</div>
<div class="bg-white p-5 rounded-xl shadow-sm border border-gray-200 space-y-3 text-center">
<h3 class="text-xs font-bold uppercase tracking-wider text-gray-500 mb-2">Aksi Otoritas Dokumen</h3>
<button onclick="proses_keputusan_admin('Verified')" class="w-full py-3 bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-bold rounded-lg transition-colors shadow-sm">Setujui Sebagai Mustahik</button>
<button onclick="proses_keputusan_admin('Rejected')" class="w-full py-3 bg-red-600 hover:bg-red-700 text-white text-xs font-bold rounded-lg transition-colors shadow-sm">Tolak Pengajuan Bantuan</button>
</div>
</div>
</main>
<script>
// Ekstraksi parameter ID dari string URL query browser
const parameter_url = new URLSearchParams(window.location.search);
const id_warga_terpilih = parameter_url.get('id');
let instansi_peta_detail = null;
// Memformat nominal angka reguler menjadi format mata uang Rupiah Indonesia
function format_mata_uang_rupiah(angka) {
return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(angka);
}
// Memetakan posisi koordinat spasial warga ke dalam objek Leaflet.js
function petakan_lokasi_warga(lat, lng, nama) {
if (!lat || !lng) return;
instansi_peta_detail = L.map('map_detail_container').setView([lat, lng], 15);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors'
}).addTo(instansi_peta_detail);
L.marker([lat, lng]).addTo(instansi_peta_detail)
.bindPopup(`<b>${nama}</b><br>Titik Lokasi Rumah`)
.openPopup();
}
// Merender seluruh string objek data yang ditarik secara aman ke komponen UI
function isi_komponen_antarmuka(data) {
// Keamanan XSS: Memanfaatkan textContent murni untuk mencegah injeksi skrip HTML
document.getElementById('det_id_sistem').textContent = data.id;
document.getElementById('det_nama_kk').textContent = data.nama_kk;
document.getElementById('det_nik').textContent = data.nik;
document.getElementById('det_no_kk').textContent = data.no_kk || '-';
document.getElementById('det_tanggungan').textContent = `${data.jumlah_tanggungan} Jiwa`;
document.getElementById('det_luas_lantai').textContent = data.luas_lantai_per_kapita || 'Belum diisi';
document.getElementById('det_mat_lantai').textContent = data.material_lantai || 'Belum diisi';
document.getElementById('det_mat_dinding').textContent = data.material_dinding || 'Belum diisi';
document.getElementById('det_sanitasi').textContent = data.fasilitas_sanitasi || 'Belum diisi';
document.getElementById('det_air_minum').textContent = data.sumber_air_minum || 'Belum diisi';
document.getElementById('det_penerangan').textContent = data.sumber_penerangan || 'Belum diisi';
document.getElementById('det_pekerjaan').textContent = data.pekerjaan_utama || 'Belum diisi';
document.getElementById('det_pendapatan').textContent = data.pendapatan_bulanan ? format_mata_uang_rupiah(data.pendapatan_bulanan) : 'Belum diisi';
document.getElementById('det_frekuensi_makan').textContent = data.frekuensi_makan || 'Belum diisi';
document.getElementById('det_kepemilikan_aset').textContent = data.kepemilikan_aset || 'Belum diisi';
document.getElementById('det_lat').textContent = data.lat;
document.getElementById('det_lng').textContent = data.lng;
const badge = document.getElementById('det_badge_status');
badge.textContent = data.status_validasi;
if (data.status_validasi.toLowerCase() === 'verified') {
badge.className = 'inline-block px-2.5 py-1 text-[10px] font-bold rounded-md bg-emerald-50 text-emerald-700 border border-emerald-200';
} else if (data.status_validasi.toLowerCase() === 'rejected') {
badge.className = 'inline-block px-2.5 py-1 text-[10px] font-bold rounded-md bg-red-50 text-red-700 border border-red-200';
} else {
badge.className = 'inline-block px-2.5 py-1 text-[10px] font-bold rounded-md bg-yellow-50 text-yellow-700 border border-yellow-200';
}
// Eksekusi pemetaan visual spasial
petakan_lokasi_warga(parseFloat(data.lat), parseFloat(data.lng), data.nama_kk);
}
// Meminta muatan data spesifik satu warga ke peladen Express Node.js
async function ambil_data_detail_warga() {
if (!id_warga_terpilih) {
return alert('ID parameter tidak ditemukan pada URL peramban.');
}
try {
/* Baris Kompleks: Melakukan pemanggilan asinkron GET menuju rute detail REST API
dengan menyertakan nilai parameter ID target untuk diolah kueri SQL JOIN oleh peladen. */
const http_response = await fetch(`http://localhost:3000/api/admin/mustahik/${id_warga_terpilih}`);
const json_result = await http_response.json();
if (!http_response.ok) throw new Error(json_result.error || 'Gagal mengambil data detail.');
isi_komponen_antarmuka(json_result);
} catch (error_sistem) {
alert('Kegagalan Koneksi Sistem: ' + error_sistem.message);
}
}
// Memproses mutasi perubahan status persetujuan data dari panel admin ke server
async function proses_keputusan_admin(keputusan_status) {
if (!confirm(`Apakah Anda yakin memvalidasi data ini dengan status ${keputusan_status}?`)) return;
try {
const http_response = await fetch(`http://localhost:3000/api/admin/verifikasi/${id_warga_terpilih}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status_validasi: keputusan_status })
});
const json_result = await http_response.json();
if (!http_response.ok) throw new Error(json_result.error || 'Gagal mengubah status.');
alert('Status otorisasi data berhasil diperbarui!');
window.location.reload();
} catch (error_sistem) {
alert('Gagal mengeksekusi perintah: ' + error_sistem.message);
}
}
// Eksekusi inisialisasi pemuatan data awal
ambil_data_detail_warga();
</script>
</body>
</html>
+97
View File
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Administrator - 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 flex items-center justify-center h-screen p-4">
<div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-sm border border-gray-200">
<div class="mb-6">
<div class="w-10 h-10 bg-gray-900 rounded-xl mb-4 flex items-center justify-center text-white font-bold text-lg">A</div>
<h2 class="text-2xl font-bold text-gray-900 tracking-tight">Dinsos Admin Login</h2>
<p class="text-gray-500 text-xs mt-1">Otoritas manajemen data verifikasi kemiskinan Kota Pontianak.</p>
</div>
<div class="space-y-4">
<div>
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Username Admin</label>
<input type="text" id="input_username" placeholder="Masukkan ID Administrator" class="w-full p-3.5 rounded-lg border border-gray-300 bg-gray-50 outline-none focus:ring-2 focus:ring-gray-800 focus:bg-white transition-all text-gray-900 text-sm font-medium">
</div>
<div>
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Kata Sandi Akun</label>
<input type="password" id="input_password" placeholder="••••••••" class="w-full p-3.5 rounded-lg border border-gray-300 bg-gray-50 outline-none focus:ring-2 focus:ring-gray-800 focus:bg-white transition-all text-gray-900 text-sm font-medium">
</div>
<p id="pesan_error_admin" class="text-red-600 text-xs font-semibold px-1 hidden"></p>
<button onclick="eksekusi_login_admin()" id="btn_submit_admin" class="w-full bg-gray-900 text-white font-bold py-3.5 rounded-lg hover:bg-gray-800 transition shadow-md text-sm tracking-wide">Masuk ke Dasbor</button>
</div>
</div>
<script>
// Mengirimkan data otentikasi admin menuju API backend server secara asinkron
async function eksekusi_login_admin() {
const username_val = document.getElementById('input_username').value;
const password_val = document.getElementById('input_password').value;
const wadah_error = document.getElementById('pesan_error_admin');
const tombol_login = document.getElementById('btn_submit_admin');
if (!username_val || !password_val) {
wadah_error.textContent = 'Seluruh kolom input wajib dilengkapi.';
wadah_error.classList.remove('hidden');
return;
}
wadah_error.classList.add('hidden');
tombol_login.disabled = true;
tombol_login.textContent = 'Otorisasi Sistem...';
try {
/* Baris Kompleks: Mengirimkan request POST JSON payload kredensial admin ke server port 3000.
Jika kredensial terverifikasi sah oleh database, string token otorisasi disimpan ke dalam localStorage
peramban sebelum mengalihkan hak akses visual secara penuh ke halaman dasbor admin. */
const http_response = await fetch('http://localhost:3000/api/auth/login-admin', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username_val,
password: password_val
})
});
const json_result = await http_response.json();
if (!http_response.ok) {
throw new Error(json_result.error || 'Akses ditolak oleh otoritas server.');
}
// Menyimpan identitas sesi token administrator ke memori lokal peramban web
localStorage.setItem('token_admin', json_result.token);
// Redireksi antarmuka secara instan menuju halaman dasbor pusat kontrol utama
window.location.href = 'admin_dasbor.html';
} catch (error_sistem) {
wadah_error.textContent = error_sistem.message;
wadah_error.classList.remove('hidden');
} finally {
tombol_login.disabled = false;
tombol_login.textContent = 'Masuk ke Dasbor';
}
}
// Mengikat event listener enter pada kolom kata sandi admin untuk efisiensi aksesibilitas
document.getElementById('input_password').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
eksekusi_login_admin();
}
});
</script>
</body>
</html>
+201
View File
@@ -0,0 +1,201 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Peta Bantuan Sosial - Dinsos Pontianak</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Poppins', sans-serif; }
</style>
</head>
<body class="bg-gray-50 flex flex-col min-h-screen">
<nav class="bg-white p-4 shadow-sm z-30 flex justify-between items-center px-8 sticky top-0">
<h1 class="text-xl font-bold text-gray-800">SIG Dinsos Pontianak</h1>
<a href="petugas_login.html" class="text-sm font-semibold text-emerald-600 hover:text-emerald-800 transition">Login Petugas</a>
</nav>
<main class="flex-grow flex flex-col gap-10 pb-12">
<section class="bg-emerald-600 text-white px-6 py-12 text-center rounded-b-[3rem] shadow-md">
<div class="max-w-3xl mx-auto">
<h2 class="text-3xl font-bold mb-4">Cek Status Bantuan Sosial Anda</h2>
<p class="text-emerald-100 mb-8 font-light">Sistem Informasi Geografis terintegrasi untuk transparansi validasi data kemiskinan masyarakat Kota Pontianak.</p>
<div class="bg-white p-2 rounded-2xl shadow-xl flex flex-col sm:flex-row gap-2 max-w-xl mx-auto border border-emerald-100">
<input type="text" id="nik_input" placeholder="Masukkan 16 digit NIK..." maxlength="16" class="w-full p-4 rounded-xl bg-transparent focus:outline-none text-gray-800">
<button onclick="cek_status_bantuan()" id="btn_cari" class="bg-emerald-600 text-white px-8 py-4 rounded-xl font-bold hover:bg-emerald-700 transition flex-shrink-0">Cari Data</button>
</div>
<div id="hasil_pencarian" class="mt-6 hidden p-4 rounded-xl border max-w-xl mx-auto text-left bg-white text-gray-800 shadow-lg transition-all">
<p id="pesan_hasil" class="text-sm font-semibold"></p>
</div>
<div class="mt-8 flex justify-center">
<a href="survei_lapangan.html" class="bg-emerald-50 text-emerald-700 hover:bg-emerald-100 border border-emerald-200 px-6 py-3 rounded-xl font-semibold transition-colors flex items-center gap-2 shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>
Mulai Survei Lapangan
</a>
</div>
</div>
</section>
<section class="px-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto">
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100 text-center">
<h3 class="text-gray-500 text-sm font-semibold uppercase tracking-wider mb-2">Total Terverifikasi</h3>
<p id="stat_publik_terverifikasi" class="text-3xl font-bold text-emerald-600">0</p>
</div>
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100 text-center">
<h3 class="text-gray-500 text-sm font-semibold uppercase tracking-wider mb-2">Sedang Diproses</h3>
<p id="stat_publik_diproses" class="text-3xl font-bold text-yellow-500">0</p>
</div>
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100 text-center">
<h3 class="text-gray-500 text-sm font-semibold uppercase tracking-wider mb-2">Pembaruan Terakhir</h3>
<p id="stat_publik_waktu" class="text-lg font-bold text-gray-700 mt-2">Menghitung...</p>
</div>
</div>
</section>
<section class="px-6">
<div class="h-[400px] w-full max-w-5xl mx-auto rounded-2xl overflow-hidden shadow-sm border border-gray-200 z-0 relative">
<div id="map_container" class="absolute inset-0"></div>
</div>
</section>
<section class="px-6 max-w-3xl mx-auto w-full">
<h3 class="text-xl font-bold text-gray-800 mb-6 text-center">Pertanyaan yang Sering Diajukan</h3>
<div class="space-y-4">
<div class="bg-white p-5 rounded-xl border border-gray-100 shadow-sm">
<h4 class="font-bold text-gray-800">Bagaimana jika NIK saya tidak ditemukan?</h4>
<p class="text-gray-600 text-sm mt-2">Pastikan Anda memasukkan 16 digit angka dengan benar. Jika masih tidak ditemukan, silakan laporkan ke RT/RW setempat untuk pendataan ulang.</p>
</div>
<div class="bg-white p-5 rounded-xl border border-gray-100 shadow-sm">
<h4 class="font-bold text-gray-800">Berapa lama proses verifikasi lapangan?</h4>
<p class="text-gray-600 text-sm mt-2">Proses verifikasi oleh petugas lapangan membutuhkan waktu maksimal 14 hari kerja sejak data masuk ke dalam sistem.</p>
</div>
</div>
</section>
</main>
<footer class="bg-gray-900 text-gray-300 pt-10 pb-6 px-6 mt-auto">
<div class="max-w-5xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8 border-b border-gray-700 pb-8 mb-6">
<div>
<h4 class="text-white text-lg font-bold mb-4">Pusat Bantuan & Pengaduan</h4>
<p class="text-sm mb-2">Email: pengaduan@dinsos.pontianak.go.id (Dummy)</p>
<p class="text-sm mb-2">Telepon: (0561) 123-4567 (Dummy)</p>
<p class="text-sm">Jam Operasional: Senin - Jumat (08.00 - 15.00 WIB)</p>
</div>
<div class="md:text-right">
<h4 class="text-white text-lg font-bold mb-4">Tautan Penting</h4>
<ul class="text-sm space-y-2">
<li><a href="#" class="hover:text-emerald-400 transition">Kebijakan Privasi</a></li>
<li><a href="#" class="hover:text-emerald-400 transition">Syarat & Ketentuan</a></li>
</ul>
</div>
</div>
<div class="text-center text-sm text-gray-500">
&copy; 2026 Dinas Sosial Kota Pontianak (Dummy). All rights reserved.
</div>
</footer>
<script>
const leaflet_map = L.map('map_container').setView([-0.0227, 109.3425], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors'
}).addTo(leaflet_map);
let map_marker = null;
// Fungsi baru untuk memuat statistik secara asinkron dari basis data
async function muat_statistik_publik() {
try {
const respons_http = await fetch('http://localhost:3000/api/survey/statistik');
if (!respons_http.ok) throw new Error('Gagal mengambil data statistik');
const data_agregat = await respons_http.json();
// Memasukkan hasil hitungan ke dalam DOM menggunakan textContent
document.getElementById('stat_publik_terverifikasi').textContent = data_agregat.terverifikasi || 0;
document.getElementById('stat_publik_diproses').textContent = data_agregat.diproses || 0;
/* Baris Kompleks: Menggunakan objek bawaan Date pada JavaScript untuk
mendapatkan waktu spesifik saat skrip ini berjalan di perangkat pengguna,
lalu memformatnya secara spesifik ke dalam zona waktu WIB (Asia/Jakarta)
agar seragam untuk seluruh pengunjung situs terlepas dari lokasi fisik perangkat mereka. */
const objek_waktu_sekarang = new Date();
const waktu_terformat_wib = objek_waktu_sekarang.toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit',
timeZone: 'Asia/Jakarta'
});
document.getElementById('stat_publik_waktu').textContent = `Hari ini, ${waktu_terformat_wib} WIB`;
} catch (error_sistem) {
console.error("Sinkronisasi statistik gagal:", error_sistem.message);
document.getElementById('stat_publik_waktu').textContent = 'Sistem Terhubung';
}
}
const cek_status_bantuan = async () => {
const input_nik_value = document.getElementById('nik_input').value;
const kotak_hasil = document.getElementById('hasil_pencarian');
const pesan_hasil = document.getElementById('pesan_hasil');
const tombol_cari = document.getElementById('btn_cari');
if (!/^\d{16}$/.test(input_nik_value)) {
return alert('Masukkan NIK yang valid (16 digit angka).');
}
tombol_cari.disabled = true;
tombol_cari.textContent = 'Mencari...';
try {
const http_response = await fetch(`http://localhost:3000/api/survey/cek-nik/${input_nik_value}`);
if (!http_response.ok) {
throw new Error('Data tidak ditemukan');
}
const json_data = await http_response.json();
kotak_hasil.classList.remove('hidden', 'bg-red-50', 'bg-emerald-50', 'border-red-200', 'border-emerald-200');
kotak_hasil.classList.add('bg-emerald-50', 'border-emerald-200');
pesan_hasil.className = 'text-emerald-700';
pesan_hasil.textContent = `Data ditemukan: ${json_data.nama_kk} | Status: ${json_data.status_validasi}`;
if (json_data.lat && json_data.lng) {
if (map_marker) {
leaflet_map.removeLayer(map_marker);
}
const koordinat_warga = [parseFloat(json_data.lat), parseFloat(json_data.lng)];
leaflet_map.setView(koordinat_warga, 16);
map_marker = L.marker(koordinat_warga).addTo(leaflet_map)
.bindPopup(`<b>${json_data.nama_kk}</b><br>Status: ${json_data.status_validasi}`)
.openPopup();
}
} catch (error_object) {
kotak_hasil.classList.remove('hidden', 'bg-red-50', 'bg-emerald-50', 'border-red-200', 'border-emerald-200');
kotak_hasil.classList.add('bg-red-50', 'border-red-200');
pesan_hasil.className = 'text-red-700';
pesan_hasil.textContent = 'Mohon maaf, NIK tidak terdaftar atau terjadi kesalahan sistem.';
} finally {
tombol_cari.disabled = false;
tombol_cari.textContent = 'Cari Data';
}
};
// Mengeksekusi penarikan data statistik tepat saat halaman selesai dimuat
muat_statistik_publik();
</script>
</body>
</html>
+195
View File
@@ -0,0 +1,195 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dasbor Petugas Lapangan - Dinsos Pontianak</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Poppins', sans-serif; }
</style>
</head>
<body class="bg-gray-50 flex flex-col min-h-screen text-gray-800">
<nav class="bg-white p-4 shadow-sm z-30 flex justify-between items-center px-6 sticky top-0 border-b border-gray-100">
<div class="flex items-center gap-2">
<div class="bg-emerald-600 text-white p-2 rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
</div>
<div>
<h1 class="text-sm font-bold text-gray-800 leading-none">Panel Petugas</h1>
<span class="text-[10px] text-gray-500 font-medium">Dinsos Kota Pontianak</span>
</div>
</div>
<button onclick="proses_logout_petugas()" class="text-xs font-semibold text-red-600 hover:bg-red-50 px-3 py-2 rounded-xl transition">Keluar</button>
</nav>
<main class="flex-grow p-4 md:p-6 max-w-4xl mx-auto w-full space-y-6">
<section class="bg-gradient-to-br from-emerald-600 to-emerald-700 text-white p-6 rounded-2xl shadow-sm flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h2 class="text-xl font-bold">Selamat Datang, Petugas Lapangan</h2>
<p class="text-xs text-emerald-100 mt-1 font-light">Tetap teliti dan objektif dalam melakukan validasi indikator kemiskinan warga.</p>
</div>
<a href="survei_lapangan.html" class="bg-white text-emerald-700 hover:bg-emerald-50 px-5 py-3 rounded-xl text-sm font-bold shadow-md transition-all flex items-center gap-2 w-full sm:w-auto justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
Mulai Survei Baru
</a>
</section>
<section class="grid grid-cols-3 gap-3 md:gap-6">
<div class="bg-white p-4 rounded-xl shadow-sm border border-gray-100 text-center">
<span class="text-[10px] text-gray-400 font-bold uppercase tracking-wider block mb-1">Total Tugas</span>
<p class="text-2xl font-bold text-gray-800" id="stat_total">0</p>
</div>
<div class="bg-white p-4 rounded-xl shadow-sm border border-gray-100 text-center">
<span class="text-[10px] text-yellow-600 font-bold uppercase tracking-wider block mb-1">Pending</span>
<p class="text-2xl font-bold text-yellow-600" id="stat_pending">0</p>
</div>
<div class="bg-white p-4 rounded-xl shadow-sm border border-gray-100 text-center">
<span class="text-[10px] text-emerald-600 font-bold uppercase tracking-wider block mb-1">Disetujui</span>
<p class="text-2xl font-bold text-emerald-600" id="stat_verified">0</p>
</div>
</section>
<section class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div class="p-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
<h3 class="font-bold text-gray-800 text-sm">Daftar Antrean Survei Wilayah</h3>
<span class="text-[11px] bg-emerald-100 text-emerald-800 font-semibold px-2 py-1 rounded-full" id="label_jumlah_antrean">0 Antrean</span>
</div>
<div id="status_message_container" class="p-8 text-center text-sm text-gray-500 hidden"></div>
<div class="divide-y divide-gray-100" id="wadah_list_antrean">
</div>
</section>
</main>
<script>
// Mengambil token autentikasi petugas untuk validasi sesi REST API
const token_petugas = localStorage.getItem('token_petugas');
// Fungsi pengecekan proteksi halaman (RBAC) saat pertama kali dimuat
function periksa_sesi_petugas() {
/* Sesi dummy dinonaktifkan sementara untuk mempermudah pengujian UAS.
Jika nanti sistem login admin/petugas Anda sudah aktif menggunakan JWT,
cukup hapus baris komentar pada blok proteksi di bawah ini. */
// if (!token_petugas) {
// alert('Akses ditolak. Silakan login terlebih dahulu.');
// window.location.href = 'petugas_login.html';
// }
}
// Fungsi merender list data antrean warga ke elemen DOM secara aman (XSS Prevention)
function render_data_ke_tabel(daftar_warga) {
const wadah_list = document.getElementById('wadah_list_antrean');
const label_antrean = document.getElementById('label_jumlah_antrean');
wadah_list.innerHTML = '';
if (daftar_warga.length === 0) {
wadah_list.innerHTML = '<div class="p-8 text-center text-sm text-gray-400">Tidak ada agenda survei aktif saat ini.</div>';
return;
}
label_antrean.textContent = `${daftar_warga.length} Rumah Tangga`;
// Variabel counter untuk menghitung jumlah status agregat data
let total_pending = 0;
let total_verified = 0;
daftar_warga.forEach(warga => {
if (warga.status_validasi.toLowerCase() === 'pending') total_pending++;
if (warga.status_validasi.toLowerCase() === 'verified') total_verified++;
const item_div = document.createElement('div');
item_div.className = 'p-4 flex justify-between items-center hover:bg-gray-50 transition-colors';
/* Baris Kompleks: Pembuatan elemen daftar dilakukan dengan kombinasi literal template Tailwind CSS.
Setiap teks string mentah yang berasal dari properti database dilewatkan melalui properti textContent
pada sub-elemen internal terpisah guna menangkal celah keamanan injeksi skrip berbahaya (Cross-Site Scripting). */
item_div.innerHTML = `
<div class="space-y-1">
<h4 class="font-bold text-gray-800 text-sm kelas-nama"></h4>
<p class="text-xs text-gray-500">NIK: <span class="kelas-nik"></span></p>
</div>
<div class="flex items-center gap-3">
<span class="inline-block px-2 py-1 text-[10px] font-bold rounded-full kelas-badge"></span>
<a href="survei_lapangan.html?nik=${warga.nik}" class="text-xs bg-gray-100 hover:bg-emerald-50 hover:text-emerald-700 text-gray-600 px-3 py-2 rounded-lg font-medium transition-colors border border-gray-200">
Buka Form
</a>
</div>
`;
// Menyuntikkan nilai data secara aman menggunakan textContent
item_div.querySelector('.kelas-nama').textContent = warga.nama_kk;
item_div.querySelector('.kelas-nik').textContent = warga.nik;
const badge = item_div.querySelector('.kelas-badge');
badge.textContent = warga.status_validasi;
// Menentukan warna badge visual berdasarkan status validasi dari database
if (warga.status_validasi.toLowerCase() === 'verified') {
badge.className += ' bg-emerald-50 text-emerald-700';
} else if (warga.status_validasi.toLowerCase() === 'rejected') {
badge.className += ' bg-red-50 text-red-700';
} else {
badge.className += ' bg-yellow-50 text-yellow-700';
}
wadah_list.appendChild(item_div);
});
// Memperbarui indikator angka papan KPI atas
document.getElementById('stat_total').textContent = daftar_warga.length;
document.getElementById('stat_pending').textContent = total_pending;
document.getElementById('stat_verified').textContent = total_verified;
}
// Mengambil data seluruh antrean mustahik dari server backend Node.js
async function ambil_antrean_survei() {
const msg_container = document.getElementById('status_message_container');
try {
msg_container.classList.remove('hidden');
msg_container.textContent = 'Memuat daftar antrean wilayah...';
/* Contoh implementasi fetch asinkron ke server lokal port 3000.
Gunakan endpoint yang sesuai (misal mengambil seluruh data MUSTAHIK untuk petugas) */
// const respons = await fetch('http://localhost:3000/api/survey/list-antrean');
// if (!respons.ok) throw new Error('Gagal mengambil data dari server.');
// const data_warga = await respons.json();
// Simulasi Data Mock untuk testing (Hapus blok ini jika endpoint backend Anda sudah siap)
await new Promise(resolve => setTimeout(resolve, 600));
const data_warga_mock = [
{ nik: "6171012345670001", nama_kk: "Syahroni", status_validasi: "Pending" },
{ nik: "6171029876540002", nama_kk: "Mariani", status_validasi: "Pending" },
{ nik: "6171031122330003", nama_kk: "Zulkifli", status_validasi: "Verified" }
];
msg_container.classList.add('hidden');
render_data_ke_tabel(data_warga_mock);
} catch (error_sistem) {
msg_container.classList.remove('hidden');
msg_container.textContent = 'Gagal terhubung ke server backend: ' + error_sistem.message;
}
}
// Fungsi menghapus token autentikasi sesi saat petugas keluar aplikasi
function proses_logout_petugas() {
localStorage.removeItem('token_petugas');
alert('Sesi berhasil diakhiri.');
window.location.href = 'index.html';
}
// Eksekusi trigger inisialisasi awal saat halaman selesai dimuat oleh browser
periksa_sesi_petugas();
ambil_antrean_survei();
</script>
</body>
</html>
+93
View File
@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Petugas Lapangan - Dinsos Pontianak</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<style>body { font-family: 'Poppins', sans-serif; }</style>
</head>
<body class="bg-emerald-50 flex items-center justify-center h-screen p-4">
<div class="bg-white p-8 rounded-3xl shadow-xl w-full max-w-sm border border-emerald-100">
<h2 class="text-2xl font-bold text-gray-800 mb-2">Halo, Petugas!</h2>
<p class="text-gray-500 text-sm mb-8">Silakan masuk untuk mulai melakukan survei lapangan hari ini.</p>
<div class="space-y-4">
<div>
<input type="text" id="input_username" placeholder="Username Petugas" class="w-full p-4 rounded-xl border bg-gray-50 outline-none focus:ring-2 focus:ring-emerald-500 transition-all text-gray-800 text-sm">
</div>
<div>
<input type="password" id="input_password" placeholder="Kata Sandi" class="w-full p-4 rounded-xl border bg-gray-50 outline-none focus:ring-2 focus:ring-emerald-500 transition-all text-gray-800 text-sm">
</div>
<p id="pesan_error" class="text-red-500 text-xs font-semibold px-1 hidden"></p>
<button onclick="eksekusi_login_petugas()" id="btn_submit_login" class="w-full bg-emerald-600 text-white font-bold py-4 rounded-xl hover:bg-emerald-700 transition shadow-md">Masuk ke Survei</button>
</div>
</div>
<script>
// Mengirimkan data otentikasi menuju API backend server
async function eksekusi_login_petugas() {
const username_val = document.getElementById('input_username').value;
const password_val = document.getElementById('input_password').value;
const wadah_error = document.getElementById('pesan_error');
const tombol_login = document.getElementById('btn_submit_login');
// Validasi masukan dasar pada sisi klien sebelum memicu lalu lintas jaringan
if (!username_val || !password_val) {
wadah_error.textContent = 'Kolom username dan kata sandi tidak boleh kosong.';
wadah_error.classList.remove('hidden');
return;
}
wadah_error.classList.add('hidden');
tombol_login.disabled = true;
tombol_login.textContent = 'Memverifikasi...';
try {
/* Baris Kompleks: Melakukan transmisi payload data sensitif terenkripsi via jaringan menggunakan HTTP POST
menuju peladen port 3000, serta menyimpan string token autentikasi ke media penyimpanan lokal peramban
(localStorage) jika validasi kredensial dinyatakan sah oleh server database. */
const http_response = await fetch('http://localhost:3000/api/auth/login-petugas', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username_val,
password: password_val
})
});
const json_result = await http_response.json();
if (!http_response.ok) {
throw new Error(json_result.error || 'Gagal masuk ke dalam sistem.');
}
// Menyimpan identitas sesi token ke memori peramban web klien
localStorage.setItem('token_petugas', json_result.token);
// Mengalihkan fokus antarmuka menuju halaman dasbor manajemen tugas lapangan
window.location.href = 'petugas_dasbor.html';
} catch (error_sistem) {
wadah_error.textContent = error_sistem.message;
wadah_error.classList.remove('hidden');
} finally {
tombol_login.disabled = false;
tombol_login.textContent = 'Masuk ke Survei';
}
}
// Mendukung eksekusi tombol enter saat berada di kolom input kata sandi
document.getElementById('input_password').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
eksekusi_login_petugas();
}
});
</script>
</body>
</html>
+437
View File
@@ -0,0 +1,437 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Survei Lapangan - Dinsos Pontianak</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Poppins', sans-serif; }
</style>
</head>
<body class="bg-gray-100 flex flex-col min-h-screen">
<nav class="bg-emerald-600 p-4 shadow-md z-30 flex items-center gap-4 text-white sticky top-0">
<a href="index.html" class="hover:bg-emerald-700 p-2 rounded-lg transition">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
</a>
<h1 class="text-xl font-bold">Formulir Survei Kemiskinan Kriteria BPS</h1>
</nav>
<main class="flex-grow p-4 md:p-8 flex justify-center items-start">
<div class="bg-white w-full max-w-2xl rounded-2xl shadow-lg border border-gray-200 overflow-hidden">
<div class="bg-gray-50 border-b border-gray-200 p-6 flex justify-between items-center">
<div class="flex flex-col items-center gap-2">
<div id="indikator_1" class="w-8 h-8 rounded-full bg-emerald-600 text-white flex justify-center items-center font-bold text-sm transition-colors">1</div>
<span class="text-[10px] font-semibold text-gray-600">Identitas</span>
</div>
<div class="h-1 flex-1 bg-gray-200 mx-1 rounded">
<div id="garis_1" class="h-full bg-emerald-600 w-0 transition-all duration-300"></div>
</div>
<div class="flex flex-col items-center gap-2">
<div id="indikator_2" class="w-8 h-8 rounded-full bg-gray-300 text-gray-600 flex justify-center items-center font-bold text-sm transition-colors">2</div>
<span class="text-[10px] font-semibold text-gray-600">Rumah</span>
</div>
<div class="h-1 flex-1 bg-gray-200 mx-1 rounded">
<div id="garis_2" class="h-full bg-emerald-600 w-0 transition-all duration-300"></div>
</div>
<div class="flex flex-col items-center gap-2">
<div id="indikator_3" class="w-8 h-8 rounded-full bg-gray-300 text-gray-600 flex justify-center items-center font-bold text-sm transition-colors">3</div>
<span class="text-[10px] font-semibold text-gray-600">Ekonomi</span>
</div>
<div class="h-1 flex-1 bg-gray-200 mx-1 rounded">
<div id="garis_3" class="h-full bg-emerald-600 w-0 transition-all duration-300"></div>
</div>
<div class="flex flex-col items-center gap-2">
<div id="indikator_4" class="w-8 h-8 rounded-full bg-gray-300 text-gray-600 flex justify-center items-center font-bold text-sm transition-colors">4</div>
<span class="text-[10px] font-semibold text-gray-600">Lokasi</span>
</div>
</div>
<div class="p-6">
<div id="peringatan_error" class="hidden mb-4 p-3 bg-red-50 border border-red-200 text-red-700 rounded-lg text-sm font-medium"></div>
<form id="form_survei" onsubmit="return false;">
<div id="langkah_1" class="space-y-4">
<h2 class="text-lg font-bold text-gray-800 border-b pb-2">Informasi Legalitas Identitas</h2>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Nomor Induk Kependudukan (NIK)</label>
<input type="text" id="input_nik" maxlength="16" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 outline-none" placeholder="16 Digit NIK KTP Kepala Keluarga">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Nama Lengkap Kepala Keluarga</label>
<input type="text" id="input_nama_kk" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 outline-none" placeholder="Masukkan nama lengkap">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Nomor Kartu Keluarga (No KK)</label>
<input type="text" id="input_no_kk" maxlength="16" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 outline-none" placeholder="16 Digit Nomor KK">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Jumlah Anggota Keluarga Tanggungan</label>
<input type="number" id="input_jumlah_tanggungan" min="0" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 outline-none" placeholder="Contoh: 4">
</div>
</div>
<div id="langkah_2" class="hidden space-y-4">
<h2 class="text-lg font-bold text-gray-800 border-b pb-2">Kondisi Fisik Rumah Tinggal</h2>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Luas Lantai Per Kapita (Per Orang)</label>
<select id="input_luas_lantai" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 outline-none">
<option value="">Pilih ukuran luas...</option>
<option value="Kurang dari 8m2">Kurang dari 8 meter persegi per orang</option>
<option value="8m2 atau Lebih">8 meter persegi atau lebih per orang</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Material Bahan Lantai Terluas</label>
<select id="input_material_lantai" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 outline-none">
<option value="">Pilih jenis material...</option>
<option value="Tanah / Bambu">Tanah / Bambu / Kayu Murah</option>
<option value="Semen / Keramik">Semen / Ubin / Keramik</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Material Bahan Dinding Terluas</label>
<select id="input_material_dinding" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 outline-none">
<option value="">Pilih jenis dinding...</option>
<option value="Bambu / Rumbia / Kayu Murah">Bambu / Rumbia / Kayu Kualitas Rendah</option>
<option value="Tembok Permanen">Tembok / Bata / Plesteran</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Fasilitas Sanitasi (MCK)</label>
<select id="input_fasilitas_sanitasi" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 outline-none">
<option value="">Pilih fasilitas sanitasi...</option>
<option value="Tidak Ada / Bersama">Tidak Ada / Menggunakan MCK Umum atau Bersama</option>
<option value="Milik Sendiri">Ada Pribadi di Dalam Rumah</option>
</select>
</div>
</div>
<div id="langkah_3" class="hidden space-y-4">
<h2 class="text-lg font-bold text-gray-800 border-b pb-2">Kondisi Ekonomi & Fasilitas Utilitas</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Sumber Air Minum Utama</label>
<select id="input_sumber_air" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 outline-none">
<option value="">Pilih sumber air...</option>
<option value="Sumur Tak Terlindung / Sungai / Air Hujan">Sumur Tak Terlindung / Air Hujan / Sungai</option>
<option value="PDAM / Sumur Terlindung">PDAM / Sumur Bor Terlindung</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Sumber Penerangan Utama</label>
<select id="input_sumber_penerangan" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 outline-none">
<option value="">Pilih jenis penerangan...</option>
<option value="Non-PLN / Menumpang">Bukan Listrik PLN / Menumpang Tetangga</option>
<option value="PLN Mandiri">Listrik Meteran PLN Sendiri</option>
</select>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Pekerjaan Utama Kepala Keluarga</label>
<input type="text" id="input_pekerjaan" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 outline-none" placeholder="Contoh: Buruh Harian Lepas, Pemulung">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Estimasi Pendapatan Total Per Bulan (Rp)</label>
<input type="number" id="input_pendapatan" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 outline-none" placeholder="Contoh: 1200000">
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Frekuensi Makan Per Hari</label>
<select id="input_frekuensi_makan" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 outline-none">
<option value="">Pilih frekuensi...</option>
<option value="1 Kali atau Kurang">1 Kali Sehari atau Kurang</option>
<option value="2 Kali">2 Kali Sehari</option>
<option value="3 Kali atau Lebih">3 Kali Sehari atau Lebih</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Kepemilikan Aset Kendaraan</label>
<select id="input_kepemilikan_aset" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 outline-none">
<option value="">Pilih status kepemilikan...</option>
<option value="Tidak Memiliki Motor">Tidak Memiliki Sepeda Motor</option>
<option value="Memiliki Motor / Aset Lain">Memiliki Motor atau Aset Berharga Lain</option>
</select>
</div>
</div>
</div>
<div id="langkah_4" class="hidden space-y-4">
<h2 class="text-lg font-bold text-gray-800 border-b pb-2">Verifikasi Titik Lokasi Geografis</h2>
<button type="button" onclick="dapatkan_lokasi_gps()" class="bg-blue-50 text-blue-700 hover:bg-blue-100 border border-blue-200 px-4 py-3 rounded-xl text-sm font-bold transition w-full">
Ambil Koordinat Lewat GPS Perangkat
</button>
<div class="h-64 w-full rounded-xl overflow-hidden border border-gray-300 relative z-0">
<div id="wadah_peta" class="h-full w-full"></div>
</div>
<div class="flex gap-4">
<div class="flex-1">
<label class="block text-xs font-medium text-gray-500">Garis Lintang (Latitude)</label>
<input type="text" id="input_lat" class="w-full p-2 bg-gray-50 border rounded text-sm text-gray-700" readonly>
</div>
<div class="flex-1">
<label class="block text-xs font-medium text-gray-500">Garis Bujur (Longitude)</label>
<input type="text" id="input_lng" class="w-full p-2 bg-gray-50 border rounded text-sm text-gray-700" readonly>
</div>
</div>
</div>
</form>
</div>
<div class="bg-gray-50 border-t border-gray-200 p-4 flex justify-between">
<button type="button" id="btn_mundur" onclick="ubah_langkah(-1)" class="hidden px-6 py-2 rounded-lg font-bold text-gray-600 hover:bg-gray-200 transition">Sebelumnya</button>
<div class="flex-1"></div>
<button type="button" id="btn_maju" onclick="ubah_langkah(1)" class="bg-emerald-600 text-white px-8 py-2 rounded-lg font-bold hover:bg-emerald-700 transition shadow-sm">Selanjutnya</button>
<button type="button" id="btn_kirim" onclick="kirim_data_survei()" class="hidden bg-emerald-600 text-white px-8 py-2 rounded-lg font-bold hover:bg-emerald-700 transition shadow-sm">Kirim Hasil Survei</button>
</div>
</div>
</main>
<script>
// Deklarasi Status Langkah Global
let langkah_sekarang = 1;
const total_langkah = 4;
let instansi_peta = null;
let penanda_peta = null;
const btn_mundur = document.getElementById('btn_mundur');
const btn_maju = document.getElementById('btn_maju');
const btn_kirim = document.getElementById('btn_kirim');
const peringatan_error = document.getElementById('peringatan_error');
// Menampilkan pesan notifikasi kegagalan validasi ke antarmuka pengguna
function tampilkan_error(pesan) {
peringatan_error.textContent = pesan;
peringatan_error.classList.remove('hidden');
}
// Menyembunyikan wadah notifikasi kegagalan
function sembunyikan_error() {
peringatan_error.classList.add('hidden');
peringatan_error.textContent = '';
}
// Memastikan kelayakan data masukan di setiap bagian formulir sebelum melangkah
function validasi_langkah_aktif() {
sembunyikan_error();
/* Baris Kompleks: Logika kondisional bersarang (switch case) memisahkan aturan validasi
berdasarkan indeks tahapan aktif, memastikan konversi dan ekspresi reguler teks tepat
sebelum dievaluasi agar terhindar dari pengiriman muatan bernilai kosong ke server. */
switch(langkah_sekarang) {
case 1:
if (!/^\d{16}$/.test(document.getElementById('input_nik').value)) {
tampilkan_error('NIK wajib diisi tepat 16 digit angka.');
return false;
}
if (document.getElementById('input_nama_kk').value.trim().length < 3) {
tampilkan_error('Nama lengkap kepala keluarga wajib diisi (minimal 3 karakter).');
return false;
}
if (!/^\d{16}$/.test(document.getElementById('input_no_kk').value)) {
tampilkan_error('Nomor Kartu Keluarga wajib diisi tepat 16 digit angka.');
return false;
}
if (document.getElementById('input_jumlah_tanggungan').value === '') {
tampilkan_error('Jumlah anggota keluarga tanggungan harus diisi.');
return false;
}
break;
case 2:
if (!document.getElementById('input_luas_lantai').value ||
!document.getElementById('input_material_lantai').value ||
!document.getElementById('input_material_dinding').value ||
!document.getElementById('input_fasilitas_sanitasi').value) {
tampilkan_error('Seluruh parameter kelayakan rumah tinggal wajib dipilih.');
return false;
}
break;
case 3:
if (!document.getElementById('input_sumber_air').value ||
!document.getElementById('input_sumber_penerangan').value ||
!document.getElementById('input_frekuensi_makan').value ||
!document.getElementById('input_kepemilikan_aset').value) {
tampilkan_error('Seluruh parameter utilitas dan sosial ekonomi wajib dipilih.');
return false;
}
if (document.getElementById('input_pekerjaan').value.trim() === '') {
tampilkan_error('Pekerjaan utama harus diisi.');
return false;
}
const pendapatan = document.getElementById('input_pendapatan').value;
if (pendapatan === '' || Number(pendapatan) < 0) {
tampilkan_error('Estimasi nominal pendapatan harus diisi dengan angka valid.');
return false;
}
break;
case 4:
if (!document.getElementById('input_lat').value) {
tampilkan_error('Titik koordinat penanda spasial belum terekam di peta.');
return false;
}
break;
}
return true;
}
// Menggerakkan alur visual formulir multi-langkah maju atau mundur
function ubah_langkah(arah) {
if (arah === 1 && !validasi_langkah_aktif()) return;
document.getElementById(`langkah_${langkah_sekarang}`).classList.add('hidden');
langkah_sekarang += arah;
document.getElementById(`langkah_${langkah_sekarang}`).classList.remove('hidden');
perbarui_antarmuka_wizard();
}
// Sinkronisasi status bar kemajuan dan kontrol tombol navigasi bawah
function perbarui_antarmuka_wizard() {
if (langkah_sekarang === 1) {
btn_mundur.classList.add('hidden');
} else {
btn_mundur.classList.remove('hidden');
}
if (langkah_sekarang === total_langkah) {
btn_maju.classList.add('hidden');
btn_kirim.classList.remove('hidden');
setTimeout(inisialisasi_peta, 100);
} else {
btn_maju.classList.remove('hidden');
btn_kirim.classList.add('hidden');
}
for (let i = 1; i <= total_langkah; i++) {
const indikator = document.getElementById(`indikator_${i}`);
if (i <= langkah_sekarang) {
indikator.classList.replace('bg-gray-300', 'bg-emerald-600');
indikator.classList.replace('text-gray-600', 'text-white');
} else {
indikator.classList.replace('bg-emerald-600', 'bg-gray-300');
indikator.classList.replace('text-white', 'text-gray-600');
}
}
if(document.getElementById('garis_1')) document.getElementById('garis_1').style.width = langkah_sekarang >= 2 ? '100%' : '0%';
if(document.getElementById('garis_2')) document.getElementById('garis_2').style.width = langkah_sekarang >= 3 ? '100%' : '0%';
if(document.getElementById('garis_3')) document.getElementById('garis_3').style.width = langkah_sekarang >= 4 ? '100%' : '0%';
}
// Inisialisasi komponen GIS Leaflet untuk penentuan lokasi rumah warga
function inisialisasi_peta() {
if (instansi_peta !== null) {
instansi_peta.invalidateSize();
return;
}
instansi_peta = L.map('wadah_peta').setView([-0.0227, 109.3425], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap'
}).addTo(instansi_peta);
/* Baris Kompleks: Penangkapan event klik pada objek peta Leaflet secara dinamis
mengekstrak properti koordinat lintang dan bujur (latlng) untuk memindahkan objek penanda
serta mengisi elemen masukan teks masukan secara otomatis. */
instansi_peta.on('click', function(e) {
const lat = e.latlng.lat;
const lng = e.latlng.lng;
if (penanda_peta) instansi_peta.removeLayer(penanda_peta);
penanda_peta = L.marker([lat, lng]).addTo(instansi_peta);
document.getElementById('input_lat').value = lat.toFixed(6);
document.getElementById('input_lng').value = lng.toFixed(6);
sembunyikan_error();
});
}
// Mengakses perangkat keras penerima gelombang navigasi satelit melalui web API browser
function dapatkan_lokasi_gps() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
function(posisi) {
const lat = posisi.coords.latitude;
const lng = posisi.coords.longitude;
document.getElementById('input_lat').value = lat.toFixed(6);
document.getElementById('input_lng').value = lng.toFixed(6);
instansi_peta.setView([lat, lng], 16);
if (penanda_peta) instansi_peta.removeLayer(penanda_peta);
penanda_peta = L.marker([lat, lng]).addTo(instansi_peta);
sembunyikan_error();
},
function(error) {
tampilkan_error("Gagal menangkap koordinat satelit GPS. Pastikan izin lokasi aktif.");
}
);
} else {
tampilkan_error("Perangkat peramban tidak mendukung navigasi Geolocation.");
}
}
// Mengirimkan enkapsulasi objek data survei menuju web server secara asinkron
async function kirim_data_survei() {
if (!validasi_langkah_aktif()) return;
btn_kirim.disabled = true;
btn_kirim.innerHTML = 'Mengirim Data...';
// Konstruksi objek payload JSON disesuaikan secara presisi dengan skema database_baru.sqlite
const payload_survei = {
nik: document.getElementById('input_nik').value,
nama_kk: document.getElementById('input_nama_kk').value,
no_kk: document.getElementById('input_no_kk').value,
jumlah_tanggungan: parseInt(document.getElementById('input_jumlah_tanggungan').value) || 0,
lat: parseFloat(document.getElementById('input_lat').value),
lng: parseFloat(document.getElementById('input_lng').value),
id_rumah_ibadah: null,
indikator_bps: {
luas_lantai_per_kapita: document.getElementById('input_luas_lantai').value,
material_lantai: document.getElementById('input_material_lantai').value,
material_dinding: document.getElementById('input_material_dinding').value,
fasilitas_sanitasi: document.getElementById('input_fasilitas_sanitasi').value,
sumber_air_minum: document.getElementById('input_sumber_air').value,
sumber_penerangan: document.getElementById('input_sumber_penerangan').value,
pekerjaan_utama: document.getElementById('input_pekerjaan').value,
pendapatan_bulanan: parseFloat(document.getElementById('input_pendapatan').value) || 0,
frekuensi_makan: document.getElementById('input_frekuensi_makan').value,
kepemilikan_aset: document.getElementById('input_kepemilikan_aset').value
}
};
try {
/* Baris Kompleks: Eksekusi transmisi data asinkron via HTTP POST menuju endpoint server.
Header 'Content-Type' diset ke JSON agar dapat dibaca oleh middleware express.json() di backend. */
const http_response = await fetch('http://localhost:3000/api/survey/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload_survei)
});
const json_result = await http_response.json();
if (!http_response.ok) throw new Error(json_result.error || 'Terjadi kesalahan internal server.');
alert('Data rekam survei kriteria baru berhasil disimpan ke basis data!');
window.location.href = 'index.html';
} catch (error_sistem) {
tampilkan_error(error_sistem.message);
btn_kirim.disabled = false;
btn_kirim.innerHTML = 'Kirim Hasil Survei';
}
}
</script>
</body>
</html>