feat : penambahan rumah ibadah

This commit is contained in:
unknown
2026-06-09 13:49:51 +07:00
parent 6c1437956c
commit 03c764a9c7
4 changed files with 181 additions and 201 deletions
+35 -17
View File
@@ -1,6 +1,6 @@
const db = require('../config/database');
// Mengambil data status bantuan warga berdasarkan pencarian NIK dari halaman publik
// Mengambil data status bantuan warga beserta lokasi pangkalan ibadah
const cek_status_nik = (req, res) => {
const nik_warga = req.params.nik;
@@ -9,12 +9,20 @@ const cek_status_nik = (req, res) => {
return res.status(400).json({ error: 'Format NIK tidak valid.' });
}
const kueri_pencarian = `SELECT nik, nama_kk, status_validasi, lat, lng FROM MUSTAHIK WHERE nik = ?`;
// Baris kompleks: Operasi LEFT JOIN menghubungkan tabel MUSTAHIK dengan RUMAH_IBADAH berdasarkan Foreign Key id_rumah_ibadah untuk menarik koordinat pangkalan distribusi secara simultan dalam satu siklus I/O.
const kueri_pencarian = `
SELECT m.nik, m.nama_kk, m.status_validasi, m.lat, m.lng, m.agama,
r.nama_tempat AS rumah_ibadah_nama,
r.lat AS rumah_ibadah_lat,
r.lng AS rumah_ibadah_lng
FROM MUSTAHIK m
LEFT JOIN RUMAH_IBADAH r ON m.id_rumah_ibadah = r.id
WHERE m.nik = ?
`;
// Baris kompleks: Penggunaan parameter '?' (parameterized query) memastikan bahwa mesin SQLite menganggap variabel 'nik_warga' murni sebagai nilai data, bukan sebagai perintah yang bisa dieksekusi, sehingga secara absolut mencegah serangan SQL Injection.
db.get(kueri_pencarian, [nik_warga], (error_sistem, baris_data) => {
if (error_sistem) {
console.error('Sistem gagal membaca tabel:', error_sistem.message);
console.error('Sistem gagal membaca tabel gabungan:', error_sistem.message);
return res.status(500).json({ error: 'Terjadi kegagalan server internal.' });
}
@@ -26,7 +34,7 @@ const cek_status_nik = (req, res) => {
});
};
// Mengontrol alur penerimaan data survei lapangan secara transaksional
// Mengontrol alur penerimaan data survei lapangan beserta penentuan pangkalan bantuan
const submit_survey_data = async (req, res) => {
try {
const {
@@ -36,11 +44,12 @@ const submit_survey_data = async (req, res) => {
jumlah_tanggungan,
lat,
lng,
agama,
indikator_bps
} = req.body;
// Praktik keamanan: Validasi input dasar untuk mencegah insersi data kosong (null pointer exception)
if (!nik || !nama_kk || !lat || !lng || !indikator_bps) {
if (!nik || !nama_kk || !lat || !lng || !agama || !indikator_bps) {
return res.status(400).json({ error: 'Payload data survei tidak lengkap dari klien.' });
}
@@ -48,18 +57,31 @@ const submit_survey_data = async (req, res) => {
db.serialize(() => {
db.run('BEGIN TRANSACTION');
// 1. Menyimpan data identitas ke tabel MUSTAHIK
const kueri_mustahik = `INSERT INTO MUSTAHIK (nik, nama_kk, no_kk, jumlah_tanggungan, lat, lng) VALUES (?, ?, ?, ?, ?, ?)`;
// Baris kompleks: Memanfaatkan db.serialize untuk memastikan kueri SELECT pencarian pangkalan ibadah selesai dieksekusi sebelum beralih ke perintah INSERT transaksional berikutnya guna menghindari balapan data (race condition).
const kueri_cari_pangkalan = `SELECT id FROM RUMAH_IBADAH WHERE agama = ? LIMIT 1`;
db.run(kueri_mustahik, [nik, nama_kk, no_kk, jumlah_tanggungan, lat, lng], function(error_mustahik) {
// Baris kompleks: Jika terjadi kegagalan pada tahap pertama ini, perintah ROLLBACK membatalkan status transaksi secara keseluruhan, mencegah adanya data terisolasi di memori.
db.get(kueri_cari_pangkalan, [agama], (error_cari, baris_pangkalan) => {
if (error_cari) {
db.run('ROLLBACK');
console.error('Gagal mencocokkan rumah ibadah:', error_cari.message);
return res.status(500).json({ error: 'Gagal memproses penentuan lokasi bantuan.' });
}
const id_pangkalan_terpilih = baris_pangkalan ? baris_pangkalan.id : null;
// 1. Menyimpan data identitas dan relasi pangkalan ke tabel MUSTAHIK
const kueri_mustahik = `
INSERT INTO MUSTAHIK (nik, nama_kk, no_kk, jumlah_tanggungan, lat, lng, agama, id_rumah_ibadah)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`;
db.run(kueri_mustahik, [nik, nama_kk, no_kk, jumlah_tanggungan, lat, lng, agama, id_pangkalan_terpilih], function(error_mustahik) {
if (error_mustahik) {
db.run('ROLLBACK');
console.error('Gagal menyimpan DML MUSTAHIK:', error_mustahik.message);
return res.status(500).json({ error: 'Gagal menyimpan data identitas warga, NIK mungkin sudah terdaftar.' });
}
// Mengekstrak ID yang otomatis digenerate (Auto Increment) untuk digunakan sebagai Foreign Key
const id_mustahik_baru = this.lastID;
const {
luas_lantai_per_kapita,
@@ -105,12 +127,13 @@ const submit_survey_data = async (req, res) => {
// Secara permanen menerapkan semua kueri yang tertunda ke dalam file SQLite
db.run('COMMIT');
res.status(201).json({
message: 'Data survei kriteria BPS berhasil disubmit',
message: 'Data survei kriteria BPS dan pangkalan bantuan berhasil disimpan.',
id_mustahik: id_mustahik_baru
});
});
});
});
});
} catch (error_sistem) {
console.error('Kesalahan eksekusi sistem:', error_sistem);
@@ -120,10 +143,6 @@ const submit_survey_data = async (req, res) => {
// Menarik agregasi data statistik untuk ditampilkan di halaman publik
const ambil_statistik_publik = (req, res) => {
/* Baris kompleks: Kueri ini menggunakan fungsi agregasi COUNT dikombinasikan dengan ekspresi
kondisional CASE WHEN. Pendekatan ini sangat optimal karena mesin SQLite hanya perlu
memindai tabel MUSTAHIK satu kali untuk mendapatkan dua nilai hitungan yang berbeda,
sehingga menghemat alokasi memori dan waktu eksekusi server backend. */
const kueri_statistik = `
SELECT
COUNT(CASE WHEN status_validasi = 'Verified' THEN 1 END) AS terverifikasi,
@@ -140,7 +159,6 @@ const ambil_statistik_publik = (req, res) => {
});
};
// Mengekspor semua fungsi agar dapat diakses oleh file router
module.exports = {
cek_status_nik,
submit_survey_data,
+34 -75
View File
@@ -1,84 +1,43 @@
const db = require('../config/database');
// Menyemai data awal rumah ibadah ke dalam tabel basis data
const semai_data_rumah_ibadah = () => {
// Kumpulan data statis yang merepresentasikan 6 agama resmi
/**
* Menyuntikkan data lokasi tempat penyaluran bantuan (Rumah Ibadah) di Pontianak
* @param {object} db - Instansi koneksi SQLite
*/
async function buat_seeder_rumah_ibadah(db) {
// Array objek berisi titik koordinat asli tempat ibadah ikonik di Pontianak
const daftar_rumah_ibadah = [
{
nama_tempat: 'Masjid Raya Mujahidin',
jenis_agama: 'Islam',
lat: -0.0427,
lng: 109.3425,
alamat: 'Jl. Jenderal Ahmad Yani'
},
{
nama_tempat: 'Gereja Katedral Santo Yosef',
jenis_agama: 'Katolik',
lat: -0.0355,
lng: 109.3468,
alamat: 'Jl. Pattimura'
},
{
nama_tempat: 'Gereja HKBP',
jenis_agama: 'Kristen',
lat: -0.0381,
lng: 109.3361,
alamat: 'Jl. Imam Bonjol'
},
{
nama_tempat: 'Pura Giripati Mulawarman',
jenis_agama: 'Hindu',
lat: -0.0556,
lng: 109.3499,
alamat: 'Jl. Adi Sucipto'
},
{
nama_tempat: 'Vihara Bodhisatva Karaniya Metta',
jenis_agama: 'Buddha',
lat: -0.0225,
lng: 109.3323,
alamat: 'Jl. Kapten Marsan'
},
{
nama_tempat: 'Makin Kelenteng',
jenis_agama: 'Konghucu',
lat: -0.0289,
lng: 109.3375,
alamat: 'Jl. Gajah Mada'
}
{ nama: 'Masjid Raya Mujahidin', agama: 'Islam', lat: -0.0336, lng: 109.3364, alamat: 'Jl. Ahmad Yani' },
{ nama: 'Gereja Katedral Santo Yosef', agama: 'Katolik', lat: -0.0331, lng: 109.3330, alamat: 'Jl. Pattimura' },
{ nama: 'Gereja HKBP Pontianak', agama: 'Kristen Protestan', lat: -0.0305, lng: 109.3321, alamat: 'Jl. Pulau We' },
{ nama: 'Vihara Bodhisatva Karaniya Metta', agama: 'Buddha', lat: -0.0255, lng: 109.3361, alamat: 'Jl. Kapten Marsan' },
{ nama: 'Pura Giripati Mulawarman', agama: 'Hindu', lat: -0.0631, lng: 109.3705, alamat: 'Jl. Adi Sucipto' },
{ nama: 'Kelenteng Kwan Tie Bio', agama: 'Konghucu', lat: -0.0261, lng: 109.3352, alamat: 'Jl. Diponegoro' }
];
db.serialize(() => {
// Praktik penanganan kesalahan otomatis: Memulai transaksi untuk memastikan semua data masuk secara bersamaan, atau dibatalkan seluruhnya jika terjadi satu kegagalan
db.run('BEGIN TRANSACTION');
return new Promise((resolve, reject) => {
db.get("SELECT COUNT(*) as hitung FROM RUMAH_IBADAH", [], (err, row) => {
if (err) return reject(err);
// Praktik keamanan: Menyiapkan query dengan parameter terikat untuk mencegah injeksi data berbahaya
const perintah_insert = db.prepare(`INSERT INTO RUMAH_IBADAH (nama_tempat, jenis_agama, lat, lng, alamat) VALUES (?, ?, ?, ?, ?)`);
if (row.hitung > 0) {
console.log('Seeder Rumah Ibadah: Data titik penyaluran sudah tersedia.');
return resolve();
}
// Baris kompleks: Melakukan perulangan pada array data, lalu mengeksekusi perintah SQL (statement.run) secara asinkron untuk setiap objek rumah ibadah. Parameter yang dikirimkan dipetakan secara berurutan sesuai dengan tanda tanya pada query di atas.
daftar_rumah_ibadah.forEach((rumah) => {
perintah_insert.run([rumah.nama_tempat, rumah.jenis_agama, rumah.lat, rumah.lng, rumah.alamat], (err) => {
if (err) {
console.error(`Gagal memasukkan data ${rumah.nama_tempat}:`, err.message);
try {
const kueri_insert = `INSERT INTO RUMAH_IBADAH (nama_tempat, agama, lat, lng, alamat) VALUES (?, ?, ?, ?, ?)`;
daftar_rumah_ibadah.forEach((tempat) => {
db.run(kueri_insert, [tempat.nama, tempat.agama, tempat.lat, tempat.lng, tempat.alamat], (error_sql) => {
if (error_sql) console.error(`Gagal menyisipkan ${tempat.nama}:`, error_sql.message);
});
});
console.log('Seeder: 6 Titik pangkalan rumah ibadah Pontianak berhasil diinisialisasi.');
resolve();
} catch (error_sistem) {
reject(error_sistem);
}
});
});
}
// Menyelesaikan penyiapan statement agar memori peladen dibebaskan
perintah_insert.finalize();
// Mengunci seluruh perubahan ke dalam basis data SQLite
db.run('COMMIT', (err) => {
if (err) {
console.error('Kegagalan transaksi basis data:', err.message);
process.exit(1);
} else {
console.log('Proses penyemaian data Rumah Ibadah berhasil diselesaikan.');
// Menghentikan eksekusi skrip karena proses seeder berjalan terpisah dari peladen utama
process.exit(0);
}
});
});
};
semai_data_rumah_ibadah();
module.exports = buat_seeder_rumah_ibadah;
+12
View File
@@ -51,6 +51,17 @@ async function inisialisasi_dan_seeding() {
frekuensi_makan TEXT,
kepemilikan_aset TEXT
)`);
db.run(`CREATE TABLE IF NOT EXISTS RUMAH_IBADAH (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nama_tempat TEXT,
agama TEXT,
lat REAL,
lng REAL,
alamat TEXT
)`);
try { db.run("ALTER TABLE MUSTAHIK ADD COLUMN agama TEXT"); } catch (e) {}
try { db.run("ALTER TABLE MUSTAHIK ADD COLUMN id_rumah_ibadah INTEGER"); } catch (e) {}
// 2. Mengeksekusi modul fungsi seeder bawaan milik Anda secara berurutan
console.log('Memulai penanaman data kredensial...');
@@ -58,6 +69,7 @@ async function inisialisasi_dan_seeding() {
// Catatan: Pastikan file seeder Anda mengekspor fungsi (contoh: module.exports = function...)
if (typeof seed_admin === 'function') await seed_admin(db);
if (typeof seed_petugas === 'function') await seed_petugas(db);
if (typeof rumah_ibadah_seeder === 'function') await rumah_ibadah_seeder(db);
resolve();
} catch (err) {
+52 -61
View File
@@ -60,73 +60,41 @@
</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;
let map_marker_warga = null;
let map_marker_pangkalan = null;
let map_garis_hubung = null;
// Custom ikon untuk pangkalan distribusi (Rumah Ibadah)
const ikon_pangkalan = L.icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
async function muat_statistik_publik() {
try {
// Diubah ke relative path
const respons_http = await fetch('/api/survey/statistik');
if (!respons_http.ok) throw new Error('Gagal mengambil data statistik');
const data_agregat = await respons_http.json();
document.getElementById('stat_publik_terverifikasi').textContent = data_agregat.terverifikasi || 0;
document.getElementById('stat_publik_diproses').textContent = data_agregat.diproses || 0;
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'
});
const waktu_terformat_wib = new Date().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';
}
}
@@ -137,38 +105,61 @@
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).');
}
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 {
// Diubah ke relative path
const http_response = await fetch(`/api/survey/cek-nik/${input_nik_value}`);
if (!http_response.ok) {
throw new Error('Data tidak ditemukan');
}
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}`;
let teks_respons = `Data ditemukan: ${json_data.nama_kk} | Status: ${json_data.status_validasi}`;
if (json_data.rumah_ibadah_nama) {
teks_respons += `<br><span class="text-xs text-emerald-600 font-normal mt-1 block">📍 Tempat Penyaluran: ${json_data.rumah_ibadah_nama}</span>`;
}
pesan_hasil.innerHTML = teks_respons;
// Menghapus layer peta sebelumnya secara bersih
if (map_marker_warga) leaflet_map.removeLayer(map_marker_warga);
if (map_marker_pangkalan) leaflet_map.removeLayer(map_marker_pangkalan);
if (map_garis_hubung) leaflet_map.removeLayer(map_garis_hubung);
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)
// Merender titik lokasi rumah mustahik (ikon biru bawaan)
map_marker_warga = L.marker(koordinat_warga).addTo(leaflet_map)
.bindPopup(`<b>${json_data.nama_kk}</b><br>Status: ${json_data.status_validasi}`)
.openPopup();
// Baris Kompleks: Jika data pangkalan silang (JOIN) tersedia, kita merender ikon hijau khusus
// pada koordinat rumah ibadah, lalu menggambar objek vektor polyline merah putus-putus
// untuk memvisualisasikan jarak dan rute dari rumah warga menuju pangkalan penyaluran.
if (json_data.rumah_ibadah_lat && json_data.rumah_ibadah_lng) {
const koordinat_pangkalan = [parseFloat(json_data.rumah_ibadah_lat), parseFloat(json_data.rumah_ibadah_lng)];
map_marker_pangkalan = L.marker(koordinat_pangkalan, { icon: ikon_pangkalan }).addTo(leaflet_map)
.bindPopup(`<b>Pusat Penyaluran</b><br>${json_data.rumah_ibadah_nama}`);
map_garis_hubung = L.polyline([koordinat_warga, koordinat_pangkalan], {
color: 'red',
weight: 3,
dashArray: '10, 10',
opacity: 0.7
}).addTo(leaflet_map);
// Menyesuaikan bidang pandang kamera agar kedua titik terlihat di layar
leaflet_map.fitBounds(map_garis_hubung.getBounds(), { padding: [50, 50] });
} else {
leaflet_map.setView(koordinat_warga, 16);
}
}
} catch (error_object) {