Compare commits
22 Commits
f8435584b2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e34c1a1d45 | |||
| 43479bc818 | |||
| 4e221e0bf1 | |||
| a4465933fa | |||
| f50a3a7c78 | |||
| 79137cac97 | |||
| b02b28b0c0 | |||
| 368492d2de | |||
| 03c764a9c7 | |||
| 6c1437956c | |||
| 2e94c478ec | |||
| 591e224233 | |||
| 810f183619 | |||
| 9726ebc2d2 | |||
| 1b816396a5 | |||
| f6231c1475 | |||
| 1d55676e3e | |||
| 0fbc704b02 | |||
| ceec2bfd0e | |||
| 4bd4167435 | |||
| 334fa2b46c | |||
| 43d63cedda |
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
database_baru.sqlite
|
||||||
|
database_baru.sqlite-journal
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
# Menggunakan image Node.js versi resmi berbasis Alpine Linux yang sangat ringan untuk efisiensi memori server
|
||||||
|
FROM node:22.16.0-alpine
|
||||||
|
|
||||||
|
# Menetapkan direktori kerja utama di dalam ruang penyimpanan virtual kontainer
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Menyalin manifes dependensi proyek untuk manajemen instalasi package npm
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Menginstal package produksi secara bersih tanpa menyertakan dependensi untuk tahap development
|
||||||
|
RUN npm install --omit=dev
|
||||||
|
# Menyalin seluruh berkas kode sumber proyek WebGIS ke dalam kontainer virtual
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Membuat folder isolasi untuk penyimpanan berkas basis data SQLite agar bisa dipetakan ke volume eksternal
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
# Menetapkan variabel lingkungan bawaan ke mode produksi untuk mengaktifkan optimasi performa Express
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Menghubungkan port internal aplikasi Express agar dapat diakses oleh sistem reverse proxy Coolify
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Menjalankan perintah utama untuk menghidupkan server webgis secara otomatis saat kontainer aktif
|
||||||
|
CMD ["node", "server.js"]
|
||||||
+3
-1
@@ -2,7 +2,9 @@ const sqlite3 = require('sqlite3').verbose();
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
// Menentukan lokasi penyimpanan file fisik database baru di dalam root proyek
|
// Menentukan lokasi penyimpanan file fisik database baru di dalam root proyek
|
||||||
const db_path = path.resolve(__dirname, '../database_baru.sqlite');
|
const db_path = process.env.NODE_ENV === 'production'
|
||||||
|
? '/app/data/database_baru.sqlite'
|
||||||
|
: path.resolve(__dirname, '../database_baru.sqlite');
|
||||||
|
|
||||||
// Menginisialisasi koneksi basis data dengan penanganan kesalahan otomatis
|
// Menginisialisasi koneksi basis data dengan penanganan kesalahan otomatis
|
||||||
const db = new sqlite3.Database(db_path, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (error_koneksi) => {
|
const db = new sqlite3.Database(db_path, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (error_koneksi) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const db = require('../config/database');
|
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 cek_status_nik = (req, res) => {
|
||||||
const nik_warga = req.params.nik;
|
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.' });
|
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) => {
|
db.get(kueri_pencarian, [nik_warga], (error_sistem, baris_data) => {
|
||||||
if (error_sistem) {
|
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.' });
|
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) => {
|
const submit_survey_data = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
@@ -36,11 +44,12 @@ const submit_survey_data = async (req, res) => {
|
|||||||
jumlah_tanggungan,
|
jumlah_tanggungan,
|
||||||
lat,
|
lat,
|
||||||
lng,
|
lng,
|
||||||
|
agama,
|
||||||
indikator_bps
|
indikator_bps
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// Praktik keamanan: Validasi input dasar untuk mencegah insersi data kosong (null pointer exception)
|
// 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.' });
|
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.serialize(() => {
|
||||||
db.run('BEGIN TRANSACTION');
|
db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
// 1. Menyimpan data identitas ke tabel MUSTAHIK
|
// 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_mustahik = `INSERT INTO MUSTAHIK (nik, nama_kk, no_kk, jumlah_tanggungan, lat, lng) VALUES (?, ?, ?, ?, ?, ?)`;
|
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) {
|
db.get(kueri_cari_pangkalan, [agama], (error_cari, baris_pangkalan) => {
|
||||||
// Baris kompleks: Jika terjadi kegagalan pada tahap pertama ini, perintah ROLLBACK membatalkan status transaksi secara keseluruhan, mencegah adanya data terisolasi di memori.
|
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) {
|
if (error_mustahik) {
|
||||||
db.run('ROLLBACK');
|
db.run('ROLLBACK');
|
||||||
console.error('Gagal menyimpan DML MUSTAHIK:', error_mustahik.message);
|
console.error('Gagal menyimpan DML MUSTAHIK:', error_mustahik.message);
|
||||||
return res.status(500).json({ error: 'Gagal menyimpan data identitas warga, NIK mungkin sudah terdaftar.' });
|
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 id_mustahik_baru = this.lastID;
|
||||||
const {
|
const {
|
||||||
luas_lantai_per_kapita,
|
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
|
// Secara permanen menerapkan semua kueri yang tertunda ke dalam file SQLite
|
||||||
db.run('COMMIT');
|
db.run('COMMIT');
|
||||||
res.status(201).json({
|
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
|
id_mustahik: id_mustahik_baru
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error_sistem) {
|
} catch (error_sistem) {
|
||||||
console.error('Kesalahan eksekusi sistem:', 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
|
// Menarik agregasi data statistik untuk ditampilkan di halaman publik
|
||||||
const ambil_statistik_publik = (req, res) => {
|
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 = `
|
const kueri_statistik = `
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(CASE WHEN status_validasi = 'Verified' THEN 1 END) AS terverifikasi,
|
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 = {
|
module.exports = {
|
||||||
cek_status_nik,
|
cek_status_nik,
|
||||||
submit_survey_data,
|
submit_survey_data,
|
||||||
|
|||||||
@@ -1,84 +1,43 @@
|
|||||||
const db = require('../config/database');
|
/**
|
||||||
|
* Menyuntikkan data lokasi tempat penyaluran bantuan (Rumah Ibadah) di Pontianak
|
||||||
// Menyemai data awal rumah ibadah ke dalam tabel basis data
|
* @param {object} db - Instansi koneksi SQLite
|
||||||
const semai_data_rumah_ibadah = () => {
|
*/
|
||||||
// Kumpulan data statis yang merepresentasikan 6 agama resmi
|
async function buat_seeder_rumah_ibadah(db) {
|
||||||
|
// Array objek berisi titik koordinat asli tempat ibadah ikonik di Pontianak
|
||||||
const daftar_rumah_ibadah = [
|
const daftar_rumah_ibadah = [
|
||||||
{
|
{ nama: 'Masjid Raya Mujahidin', agama: 'Islam', lat: -0.0336, lng: 109.3364, alamat: 'Jl. Ahmad Yani' },
|
||||||
nama_tempat: 'Masjid Raya Mujahidin',
|
{ nama: 'Gereja Katedral Santo Yosef', agama: 'Katolik', lat: -0.0331, lng: 109.3330, alamat: 'Jl. Pattimura' },
|
||||||
jenis_agama: 'Islam',
|
{ nama: 'Gereja HKBP Pontianak', agama: 'Kristen Protestan', lat: -0.0305, lng: 109.3321, alamat: 'Jl. Pulau We' },
|
||||||
lat: -0.0427,
|
{ nama: 'Vihara Bodhisatva Karaniya Metta', agama: 'Buddha', lat: -0.0255, lng: 109.3361, alamat: 'Jl. Kapten Marsan' },
|
||||||
lng: 109.3425,
|
{ nama: 'Pura Giripati Mulawarman', agama: 'Hindu', lat: -0.0631, lng: 109.3705, alamat: 'Jl. Adi Sucipto' },
|
||||||
alamat: 'Jl. Jenderal Ahmad Yani'
|
{ nama: 'Kelenteng Kwan Tie Bio', agama: 'Konghucu', lat: -0.0261, lng: 109.3352, alamat: 'Jl. Diponegoro' }
|
||||||
},
|
|
||||||
{
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
db.serialize(() => {
|
return new Promise((resolve, reject) => {
|
||||||
// Praktik penanganan kesalahan otomatis: Memulai transaksi untuk memastikan semua data masuk secara bersamaan, atau dibatalkan seluruhnya jika terjadi satu kegagalan
|
db.get("SELECT COUNT(*) as hitung FROM RUMAH_IBADAH", [], (err, row) => {
|
||||||
db.run('BEGIN TRANSACTION');
|
if (err) return reject(err);
|
||||||
|
|
||||||
// Praktik keamanan: Menyiapkan query dengan parameter terikat untuk mencegah injeksi data berbahaya
|
if (row.hitung > 0) {
|
||||||
const perintah_insert = db.prepare(`INSERT INTO RUMAH_IBADAH (nama_tempat, jenis_agama, lat, lng, alamat) VALUES (?, ?, ?, ?, ?)`);
|
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.
|
try {
|
||||||
daftar_rumah_ibadah.forEach((rumah) => {
|
const kueri_insert = `INSERT INTO RUMAH_IBADAH (nama_tempat, agama, lat, lng, alamat) VALUES (?, ?, ?, ?, ?)`;
|
||||||
perintah_insert.run([rumah.nama_tempat, rumah.jenis_agama, rumah.lat, rumah.lng, rumah.alamat], (err) => {
|
|
||||||
if (err) {
|
daftar_rumah_ibadah.forEach((tempat) => {
|
||||||
console.error(`Gagal memasukkan data ${rumah.nama_tempat}:`, err.message);
|
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
|
module.exports = buat_seeder_rumah_ibadah;
|
||||||
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();
|
|
||||||
+23
-17
@@ -1,32 +1,38 @@
|
|||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
const db = require('../config/database');
|
|
||||||
|
|
||||||
// Menggenerasikan data akun administrator baru dengan pengacakan kata sandi aman
|
/**
|
||||||
async function buat_akun_admin_seeder() {
|
* Menyuntikkan akun administrator secara aman dengan pencegahan duplikasi data
|
||||||
|
* @param {object} db - Instansi koneksi SQLite
|
||||||
|
*/
|
||||||
|
async function buat_akun_admin_seeder(db) {
|
||||||
const username_admin = 'admin_dinsos';
|
const username_admin = 'admin_dinsos';
|
||||||
const password_murni = 'adminrahasia';
|
const password_murni = 'password';
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Praktik Keamanan: Memeriksa eksistensi username sebelum melakukan eksekusi penulisan biner
|
||||||
|
db.get("SELECT COUNT(*) as hitung FROM ADMIN WHERE username = ?", [username_admin], async (err, row) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
|
||||||
|
if (row.hitung > 0) {
|
||||||
|
console.log(`Seeder Admin: Akun '${username_admin}' sudah aman tersedia.`);
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Baris kompleks: Kita menerapkan metode saltRounds sebesar 10 untuk menghasilkan nilai salt unik menggunakan bcrypt. Fungsi ini mengacak kata sandi murni menjadi string hash tak terbaca sebelum disimpan ke SQLite untuk mengantisipasi kebocoran database.
|
|
||||||
const salt_rounds = 10;
|
const salt_rounds = 10;
|
||||||
const password_terhash = await bcrypt.hash(password_murni, salt_rounds);
|
const password_terhash = await bcrypt.hash(password_murni, salt_rounds);
|
||||||
|
|
||||||
const kueri_insert = `INSERT INTO ADMIN (username, password) VALUES (?, ?)`;
|
const kueri_insert = `INSERT INTO ADMIN (username, password) VALUES (?, ?)`;
|
||||||
|
|
||||||
db.run(kueri_insert, [username_admin, password_terhash], function(error_sql) {
|
db.run(kueri_insert, [username_admin, password_terhash], function(error_sql) {
|
||||||
if (error_sql) {
|
if (error_sql) return reject(error_sql);
|
||||||
console.error('Gagal menyisipkan akun admin:', error_sql.message);
|
console.log(`Seeder: Akun admin '${username_admin}' berhasil diinisialisasi.`);
|
||||||
process.exit(1);
|
resolve();
|
||||||
}
|
|
||||||
console.log(`Akun admin berhasil dibuat! Username: ${username_admin}`);
|
|
||||||
process.exit(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error_sistem) {
|
} catch (error_sistem) {
|
||||||
console.error('Terjadi kegagalan enkripsi internal:', error_sistem.message);
|
reject(error_sistem);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menjalankan fungsi seeder otomatis saat berkas dieksekusi di terminal
|
module.exports = buat_akun_admin_seeder;
|
||||||
buat_akun_admin_seeder();
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Mengimpor fungsi seeder asli bawaan milik Anda
|
||||||
|
const seed_admin = require('./seed_admin');
|
||||||
|
const seed_petugas = require('./seed_petugas');
|
||||||
|
const rumah_ibadah_seeder = require('./rumah_ibadah_seeder');
|
||||||
|
|
||||||
|
// Jalur absolut database yang terhubung langsung ke Persistent Storage Coolify
|
||||||
|
const db_path = '/app/data/database_baru.sqlite';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengatur jalannya proses inisialisasi tabel, migrasi kolom, dan penanaman data master
|
||||||
|
*/
|
||||||
|
async function inisialisasi_dan_seeding() {
|
||||||
|
const db = new sqlite3.Database(db_path);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.serialize(async () => {
|
||||||
|
try {
|
||||||
|
// 1. Memastikan tabel dasar terbuat dengan sempurna
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS ADMIN (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT UNIQUE,
|
||||||
|
password TEXT
|
||||||
|
)`);
|
||||||
|
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS PETUGAS (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT UNIQUE,
|
||||||
|
password TEXT
|
||||||
|
)`);
|
||||||
|
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS MUSTAHIK (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
nik TEXT UNIQUE,
|
||||||
|
nama_kk TEXT,
|
||||||
|
no_kk TEXT,
|
||||||
|
jumlah_tanggungan INTEGER,
|
||||||
|
status_validasi TEXT DEFAULT 'Pending',
|
||||||
|
lat REAL,
|
||||||
|
lng REAL
|
||||||
|
)`);
|
||||||
|
|
||||||
|
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
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// 2. Baris Kompleks: Melakukan pemeriksaan skema internal tabel MUSTAHIK secara asinkron menggunakan perintah PRAGMA. Hasil pemindaian berupa array objek kolom diverifikasi terlebih dahulu guna menghindari eksekusi ganda perintah ALTER TABLE yang berpotensi merusak konsistensi biner SQLite.
|
||||||
|
db.all(`PRAGMA table_info(MUSTAHIK)`, [], async (err_pragma, kolom_kolom) => {
|
||||||
|
if (err_pragma) {
|
||||||
|
console.error("Gagal membaca struktur tabel via PRAGMA:", err_pragma.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sudah_ada_agama = kolom_kolom.some(k => k.name === 'agama');
|
||||||
|
const sudah_ada_pangkalan = kolom_kolom.some(k => k.name === 'id_rumah_ibadah');
|
||||||
|
|
||||||
|
if (!sudah_ada_agama) {
|
||||||
|
db.run("ALTER TABLE MUSTAHIK ADD COLUMN agama TEXT");
|
||||||
|
}
|
||||||
|
if (!sudah_ada_pangkalan) {
|
||||||
|
db.run("ALTER TABLE MUSTAHIK ADD COLUMN id_rumah_ibadah INTEGER");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Mengeksekusi seluruh fungsi seeder secara berurutan setelah penjaminan kolom selesai
|
||||||
|
console.log('Memulai sinkronisasi penanaman data master...');
|
||||||
|
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) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { inisialisasi_dan_seeding };
|
||||||
+24
-18
@@ -1,32 +1,38 @@
|
|||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
const db = require('../config/database');
|
|
||||||
|
|
||||||
// Menggenerasikan data akun petugas baru dengan pengacakan kata sandi aman
|
/**
|
||||||
async function buat_akun_petugas_seeder() {
|
* Menyuntikkan akun petugas lapangan secara aman dengan pencegahan duplikasi data
|
||||||
const username_petugas = 'petugas_pontianak';
|
* @param {object} db - Instansi koneksi SQLite
|
||||||
const password_murni = 'rahasia123';
|
*/
|
||||||
|
async function buat_akun_petugas_seeder(db) {
|
||||||
|
const username_petugas = 'petugas_lapangan';
|
||||||
|
const password_murni = 'password';
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Praktik Keamanan: Memeriksa eksistensi username guna menghindari kegagalan SQLITE_CONSTRAINT
|
||||||
|
db.get("SELECT COUNT(*) as hitung FROM PETUGAS WHERE username = ?", [username_petugas], async (err, row) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
|
||||||
|
if (row.hitung > 0) {
|
||||||
|
console.log(`Seeder Petugas: Akun '${username_petugas}' sudah aman tersedia.`);
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Baris kompleks: Kita menerapkan algoritma KDF Bcrypt dengan salt rounds 10 untuk menyamarkan kata sandi murni menjadi hash acak sebelum disimpan ke SQLite guna memenuhi standar kepatuhan keamanan data industri tanpa membebani memori server secara berlebihan.
|
|
||||||
const salt_rounds = 10;
|
const salt_rounds = 10;
|
||||||
const password_terhash = await bcrypt.hash(password_murni, salt_rounds);
|
const password_terhash = await bcrypt.hash(password_murni, salt_rounds);
|
||||||
|
|
||||||
const kueri_insert = `INSERT INTO PETUGAS (username, password) VALUES (?, ?)`;
|
const kueri_insert = `INSERT INTO PETUGAS (username, password) VALUES (?, ?)`;
|
||||||
|
|
||||||
db.run(kueri_insert, [username_petugas, password_terhash], function(error_sql) {
|
db.run(kueri_insert, [username_petugas, password_terhash], function(error_sql) {
|
||||||
if (error_sql) {
|
if (error_sql) return reject(error_sql);
|
||||||
console.error('Gagal menyisipkan akun petugas:', error_sql.message);
|
console.log(`Seeder: Akun petugas '${username_petugas}' berhasil diinisialisasi.`);
|
||||||
process.exit(1);
|
resolve();
|
||||||
}
|
|
||||||
console.log(`Akun petugas berhasil dibuat! Username: ${username_petugas}`);
|
|
||||||
process.exit(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error_sistem) {
|
} catch (error_sistem) {
|
||||||
console.error('Terjadi kegagalan enkripsi internal:', error_sistem.message);
|
reject(error_sistem);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menjalankan fungsi seeder otomatis saat berkas dieksekusi di terminal
|
module.exports = buat_akun_petugas_seeder;
|
||||||
buat_akun_petugas_seeder();
|
|
||||||
@@ -4,12 +4,14 @@ const survey_routes = require('./routes/survey_routes');
|
|||||||
const auth_routes = require('./routes/auth_routes');
|
const auth_routes = require('./routes/auth_routes');
|
||||||
const admin_routes = require('./routes/admin_routes');
|
const admin_routes = require('./routes/admin_routes');
|
||||||
const map_routes = require('./routes/map_routes');
|
const map_routes = require('./routes/map_routes');
|
||||||
|
// Mengimpor modul pengontrol otomatisasi data seeder
|
||||||
|
const { inisialisasi_dan_seeding } = require('./seeders/seed_manager');
|
||||||
|
|
||||||
// Menginisialisasi aplikasi Express
|
// Menginisialisasi aplikasi Express
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 3000;
|
const port = 3000;
|
||||||
|
|
||||||
// Praktik keamanan: Mengaktifkan CORS agar API hanya bisa diakses oleh domain frontend yang diizinkan (atau semua domain saat masa pengembangan)
|
// Praktik keamanan: Mengaktifkan CORS agar API hanya bisa diakses oleh domain frontend yang diizinkan
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
// Membaca JSON dari body request
|
// Membaca JSON dari body request
|
||||||
@@ -21,9 +23,27 @@ app.use('/api/auth', auth_routes);
|
|||||||
app.use('/api/admin', admin_routes);
|
app.use('/api/admin', admin_routes);
|
||||||
app.use('/api/map', map_routes);
|
app.use('/api/map', map_routes);
|
||||||
|
|
||||||
|
// Jika folder frontend Anda bernama 'ui'
|
||||||
|
app.use(express.static('ui', { index: false }));
|
||||||
|
|
||||||
|
// Mengarahkan pengguna yang mengakses URL kosong langsung ke index.html
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.redirect('/portal.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rute rahasia sementara untuk mengunduh database asli dari server publik Coolify
|
||||||
|
app.get('/rahasia-unduh-db-uas', (req, res) => {
|
||||||
|
const path_berkas_database = '/app/data/database_baru.sqlite';
|
||||||
|
res.download(path_berkas_database, 'database_produksi.sqlite', (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Gagal mengunduh berkas basis data:', err.message);
|
||||||
|
res.status(500).send('Berkas database belum terbentuk atau folder kosong.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Menangkap request ke rute yang tidak terdaftar
|
// Menangkap request ke rute yang tidak terdaftar
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
// Baris kompleks: Middleware ini menangkap semua lalu lintas HTTP (GET, POST, dll) yang tidak cocok dengan definisi rute di atasnya. Hal ini mencegah kebocoran informasi stack trace default dari Express dan mengembalikan respons JSON standar yang aman.
|
|
||||||
res.status(404).json({ error: 'Endpoint yang Anda cari tidak ditemukan di server ini.' });
|
res.status(404).json({ error: 'Endpoint yang Anda cari tidak ditemukan di server ini.' });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,6 +54,15 @@ app.use((err, req, res, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Menjalankan server
|
// Menjalankan server
|
||||||
app.listen(port, () => {
|
app.listen(port, async () => {
|
||||||
console.log(`Server WebGIS berjalan dan siap diakses pada port ${port}`);
|
console.log(`Server WebGIS berjalan dan siap diakses pada port ${port}`);
|
||||||
|
|
||||||
|
// Praktik Penanganan Kesalahan Otomatis:
|
||||||
|
// Menjalankan proses pembuatan tabel dan penanaman data master jika SQLite kosong saat startup
|
||||||
|
try {
|
||||||
|
await inisialisasi_dan_seeding();
|
||||||
|
console.log('Sinkronisasi data seeder bawaan ke database produksi sukses.');
|
||||||
|
} catch (error_sistem_seed) {
|
||||||
|
console.error('Peringatan: Gagal mengeksekusi database seeder:', error_sistem_seed.message);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
+65
-22
@@ -74,8 +74,33 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<div id="modal_konfirmasi" class="fixed inset-0 z-50 flex items-center justify-center hidden bg-gray-900 bg-opacity-50 backdrop-blur-sm transition-opacity">
|
||||||
|
<div class="bg-white p-6 rounded-2xl shadow-2xl w-full max-w-sm border border-gray-200 transform transition-all">
|
||||||
|
<div class="flex items-center gap-4 mb-4">
|
||||||
|
<div class="w-12 h-12 rounded-full bg-yellow-50 flex items-center justify-center text-yellow-500 flex-shrink-0">
|
||||||
|
<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="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-900">Konfirmasi Aksi</h3>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Tindakan ini akan mengubah basis data.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p id="teks_pesan_modal" class="text-sm text-gray-700 mb-6 py-2 border-y border-gray-100">
|
||||||
|
Apakah Anda yakin ingin mengubah status data ini?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button onclick="tutup_modal()" class="px-4 py-2.5 text-xs font-bold text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-xl transition-colors">Batalkan</button>
|
||||||
|
<button onclick="eksekusi_perubahan_status()" id="btn_modal_setuju" class="px-4 py-2.5 text-xs font-bold text-white bg-gray-900 hover:bg-gray-800 rounded-xl transition-colors shadow-sm">Ya, Lanjutkan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Fungsi membersihkan dan memperbarui isi baris tabel secara aman (XSS Prevention)
|
let antrean_id_warga = null;
|
||||||
|
let antrean_status_baru = null;
|
||||||
|
|
||||||
function muat_data_ke_tabel_admin(daftar_mustahik) {
|
function muat_data_ke_tabel_admin(daftar_mustahik) {
|
||||||
const tbody = document.getElementById('tabel_body_mustahik');
|
const tbody = document.getElementById('tabel_body_mustahik');
|
||||||
const pesan_kosong = document.getElementById('pesan_tabel_kosong');
|
const pesan_kosong = document.getElementById('pesan_tabel_kosong');
|
||||||
@@ -100,9 +125,6 @@
|
|||||||
const baris = document.createElement('tr');
|
const baris = document.createElement('tr');
|
||||||
baris.className = 'hover:bg-gray-50 transition-colors';
|
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 = `
|
baris.innerHTML = `
|
||||||
<td class="p-4 text-xs font-mono text-gray-400">${indeks + 1}</td>
|
<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 font-semibold text-gray-900 kolom-nama"></td>
|
||||||
@@ -119,7 +141,6 @@
|
|||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Menyuntikkan nilai teks string secara terisolasi dan aman
|
|
||||||
baris.querySelector('.kolom-nama').textContent = warga.nama_kk;
|
baris.querySelector('.kolom-nama').textContent = warga.nama_kk;
|
||||||
baris.querySelector('.kolom-nik').textContent = warga.nik;
|
baris.querySelector('.kolom-nik').textContent = warga.nik;
|
||||||
baris.querySelector('.kolom-nokk').textContent = warga.no_kk || '-';
|
baris.querySelector('.kolom-nokk').textContent = warga.no_kk || '-';
|
||||||
@@ -139,7 +160,6 @@
|
|||||||
tbody.appendChild(baris);
|
tbody.appendChild(baris);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Perbarui papan indikator agregat atas secara asinkron
|
|
||||||
document.getElementById('label_total_baris').textContent = `${daftar_mustahik.length} Rekaman`;
|
document.getElementById('label_total_baris').textContent = `${daftar_mustahik.length} Rekaman`;
|
||||||
document.getElementById('stat_total_warga').textContent = daftar_mustahik.length;
|
document.getElementById('stat_total_warga').textContent = daftar_mustahik.length;
|
||||||
document.getElementById('stat_pending_warga').textContent = hitung_pending;
|
document.getElementById('stat_pending_warga').textContent = hitung_pending;
|
||||||
@@ -147,13 +167,13 @@
|
|||||||
document.getElementById('stat_rejected_warga').textContent = hitung_rejected;
|
document.getElementById('stat_rejected_warga').textContent = hitung_rejected;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Meminta daftar seluruh entitas warga dari sistem peladen Express
|
|
||||||
async function ambil_data_admin_mustahik() {
|
async function ambil_data_admin_mustahik() {
|
||||||
const banner_error = document.getElementById('banner_error_admin');
|
const banner_error = document.getElementById('banner_error_admin');
|
||||||
try {
|
try {
|
||||||
banner_error.classList.add('hidden');
|
banner_error.classList.add('hidden');
|
||||||
|
|
||||||
const http_response = await fetch('http://localhost:3000/api/admin/mustahik');
|
// Diubah ke relative path
|
||||||
|
const http_response = await fetch('/api/admin/mustahik');
|
||||||
const json_result = await http_response.json();
|
const json_result = await http_response.json();
|
||||||
|
|
||||||
if (!http_response.ok) throw new Error(json_result.error || 'Gagal menarik data dari server.');
|
if (!http_response.ok) throw new Error(json_result.error || 'Gagal menarik data dari server.');
|
||||||
@@ -166,44 +186,67 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mengubah status persetujuan verifikasi warga via metode AJAX PATCH menuju backend
|
function ubah_status_verifikasi(id_warga, status_baru) {
|
||||||
async function ubah_status_verifikasi(id_warga, status_baru) {
|
antrean_id_warga = id_warga;
|
||||||
const banner_error = document.getElementById('banner_error_admin');
|
antrean_status_baru = status_baru;
|
||||||
|
|
||||||
if (!confirm(`Apakah Anda yakin ingin mengubah status data ini menjadi ${status_baru}?`)) {
|
const teks_pesan = document.getElementById('teks_pesan_modal');
|
||||||
return;
|
teks_pesan.innerHTML = `Apakah Anda yakin ingin mengubah status validasi menjadi <span class="font-bold text-gray-900">${status_baru}</span>?`;
|
||||||
|
|
||||||
|
const tombol_setuju = document.getElementById('btn_modal_setuju');
|
||||||
|
if (status_baru === 'Verified') {
|
||||||
|
tombol_setuju.className = 'px-4 py-2.5 text-xs font-bold text-white bg-emerald-600 hover:bg-emerald-700 rounded-xl transition-colors shadow-sm';
|
||||||
|
} else {
|
||||||
|
tombol_setuju.className = 'px-4 py-2.5 text-xs font-bold text-white bg-red-600 hover:bg-red-700 rounded-xl transition-colors shadow-sm';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.getElementById('modal_konfirmasi').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function tutup_modal() {
|
||||||
|
document.getElementById('modal_konfirmasi').classList.add('hidden');
|
||||||
|
antrean_id_warga = null;
|
||||||
|
antrean_status_baru = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function eksekusi_perubahan_status() {
|
||||||
|
if (!antrean_id_warga || !antrean_status_baru) return;
|
||||||
|
|
||||||
|
const banner_error = document.getElementById('banner_error_admin');
|
||||||
|
const tombol_setuju = document.getElementById('btn_modal_setuju');
|
||||||
|
|
||||||
|
tombol_setuju.disabled = true;
|
||||||
|
tombol_setuju.textContent = 'Memproses...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/* Baris Kompleks: Melakukan transmisi parsial status data menggunakan metode HTTP PATCH
|
// Diubah ke relative path
|
||||||
menuju endpoint verifikasi admin, membawa payload status terenkapsulasi JSON
|
const http_response = await fetch(`/api/admin/verifikasi/${antrean_id_warga}`, {
|
||||||
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',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ status_validasi: status_baru })
|
body: JSON.stringify({ status_validasi: antrean_status_baru })
|
||||||
});
|
});
|
||||||
|
|
||||||
const json_result = await http_response.json();
|
const json_result = await http_response.json();
|
||||||
|
|
||||||
if (!http_response.ok) throw new Error(json_result.error || 'Gagal mengubah status verifikasi.');
|
if (!http_response.ok) throw new Error(json_result.error || 'Gagal mengubah status verifikasi.');
|
||||||
|
|
||||||
// Segera sinkronkan ulang isi tabel setelah proses mutasi data berhasil
|
tutup_modal();
|
||||||
ambil_data_admin_mustahik();
|
ambil_data_admin_mustahik();
|
||||||
|
|
||||||
} catch (error_sistem) {
|
} catch (error_sistem) {
|
||||||
|
tutup_modal();
|
||||||
banner_error.textContent = 'Gagal memproses mutasi data: ' + error_sistem.message;
|
banner_error.textContent = 'Gagal memproses mutasi data: ' + error_sistem.message;
|
||||||
banner_error.classList.remove('hidden');
|
banner_error.classList.remove('hidden');
|
||||||
|
} finally {
|
||||||
|
tombol_setuju.disabled = false;
|
||||||
|
tombol_setuju.textContent = 'Ya, Lanjutkan';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mengakhiri sesi masuk administrator utama
|
|
||||||
function eksekusi_logout_admin() {
|
function eksekusi_logout_admin() {
|
||||||
localStorage.removeItem('token_admin');
|
localStorage.removeItem('token_admin');
|
||||||
window.location.href = 'index.html';
|
window.location.href = 'index.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inisialisasi awal penarikan data saat halaman terbuka sempurna
|
|
||||||
ambil_data_admin_mustahik();
|
ambil_data_admin_mustahik();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+66
-18
@@ -85,27 +85,49 @@
|
|||||||
|
|
||||||
<div class="bg-white p-5 rounded-xl shadow-sm border border-gray-200 space-y-3 text-center">
|
<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>
|
<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="ubah_status_verifikasi(id_warga_terpilih, '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>
|
<button onclick="ubah_status_verifikasi(id_warga_terpilih, '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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<div id="modal_konfirmasi" class="fixed inset-0 z-50 flex items-center justify-center hidden bg-gray-900 bg-opacity-50 backdrop-blur-sm transition-opacity">
|
||||||
|
<div class="bg-white p-6 rounded-2xl shadow-2xl w-full max-w-sm border border-gray-200 transform transition-all">
|
||||||
|
<div class="flex items-center gap-4 mb-4">
|
||||||
|
<div class="w-12 h-12 rounded-full bg-yellow-50 flex items-center justify-center text-yellow-500 flex-shrink-0">
|
||||||
|
<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="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-900">Konfirmasi Aksi</h3>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Tindakan ini akan mengubah basis data.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p id="teks_pesan_modal" class="text-sm text-gray-700 mb-6 py-2 border-y border-gray-100">
|
||||||
|
Apakah Anda yakin ingin mengubah status data ini?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button onclick="tutup_modal()" class="px-4 py-2.5 text-xs font-bold text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-xl transition-colors">Batalkan</button>
|
||||||
|
<button onclick="eksekusi_perubahan_status()" id="btn_modal_setuju" class="px-4 py-2.5 text-xs font-bold text-white bg-gray-900 hover:bg-gray-800 rounded-xl transition-colors shadow-sm">Ya, Lanjutkan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Ekstraksi parameter ID dari string URL query browser
|
|
||||||
const parameter_url = new URLSearchParams(window.location.search);
|
const parameter_url = new URLSearchParams(window.location.search);
|
||||||
const id_warga_terpilih = parameter_url.get('id');
|
const id_warga_terpilih = parameter_url.get('id');
|
||||||
|
|
||||||
let instansi_peta_detail = null;
|
let instansi_peta_detail = null;
|
||||||
|
let antrean_id_warga = null;
|
||||||
|
let antrean_status_baru = null;
|
||||||
|
|
||||||
// Memformat nominal angka reguler menjadi format mata uang Rupiah Indonesia
|
|
||||||
function format_mata_uang_rupiah(angka) {
|
function format_mata_uang_rupiah(angka) {
|
||||||
return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(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) {
|
function petakan_lokasi_warga(lat, lng, nama) {
|
||||||
if (!lat || !lng) return;
|
if (!lat || !lng) return;
|
||||||
|
|
||||||
@@ -119,9 +141,7 @@
|
|||||||
.openPopup();
|
.openPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merender seluruh string objek data yang ditarik secara aman ke komponen UI
|
|
||||||
function isi_komponen_antarmuka(data) {
|
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_id_sistem').textContent = data.id;
|
||||||
document.getElementById('det_nama_kk').textContent = data.nama_kk;
|
document.getElementById('det_nama_kk').textContent = data.nama_kk;
|
||||||
document.getElementById('det_nik').textContent = data.nik;
|
document.getElementById('det_nik').textContent = data.nik;
|
||||||
@@ -152,20 +172,17 @@
|
|||||||
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';
|
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);
|
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() {
|
async function ambil_data_detail_warga() {
|
||||||
if (!id_warga_terpilih) {
|
if (!id_warga_terpilih) {
|
||||||
return alert('ID parameter tidak ditemukan pada URL peramban.');
|
return alert('ID parameter tidak ditemukan pada URL peramban.');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/* Baris Kompleks: Melakukan pemanggilan asinkron GET menuju rute detail REST API
|
// Diubah ke relative path
|
||||||
dengan menyertakan nilai parameter ID target untuk diolah kueri SQL JOIN oleh peladen. */
|
const http_response = await fetch(`/api/admin/mustahik/${id_warga_terpilih}`);
|
||||||
const http_response = await fetch(`http://localhost:3000/api/admin/mustahik/${id_warga_terpilih}`);
|
|
||||||
const json_result = await http_response.json();
|
const json_result = await http_response.json();
|
||||||
|
|
||||||
if (!http_response.ok) throw new Error(json_result.error || 'Gagal mengambil data detail.');
|
if (!http_response.ok) throw new Error(json_result.error || 'Gagal mengambil data detail.');
|
||||||
@@ -177,29 +194,60 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memproses mutasi perubahan status persetujuan data dari panel admin ke server
|
function ubah_status_verifikasi(id_warga, status_baru) {
|
||||||
async function proses_keputusan_admin(keputusan_status) {
|
antrean_id_warga = id_warga;
|
||||||
if (!confirm(`Apakah Anda yakin memvalidasi data ini dengan status ${keputusan_status}?`)) return;
|
antrean_status_baru = status_baru;
|
||||||
|
|
||||||
|
const teks_pesan = document.getElementById('teks_pesan_modal');
|
||||||
|
teks_pesan.innerHTML = `Apakah Anda yakin memvalidasi data ini dengan status <span class="font-bold text-gray-900">${status_baru}</span>?`;
|
||||||
|
|
||||||
|
const tombol_setuju = document.getElementById('btn_modal_setuju');
|
||||||
|
if (status_baru === 'Verified') {
|
||||||
|
tombol_setuju.className = 'px-4 py-2.5 text-xs font-bold text-white bg-emerald-600 hover:bg-emerald-700 rounded-xl transition-colors shadow-sm';
|
||||||
|
} else {
|
||||||
|
tombol_setuju.className = 'px-4 py-2.5 text-xs font-bold text-white bg-red-600 hover:bg-red-700 rounded-xl transition-colors shadow-sm';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('modal_konfirmasi').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function tutup_modal() {
|
||||||
|
document.getElementById('modal_konfirmasi').classList.add('hidden');
|
||||||
|
antrean_id_warga = null;
|
||||||
|
antrean_status_baru = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function eksekusi_perubahan_status() {
|
||||||
|
if (!antrean_id_warga || !antrean_status_baru) return;
|
||||||
|
|
||||||
|
const tombol_setuju = document.getElementById('btn_modal_setuju');
|
||||||
|
tombol_setuju.disabled = true;
|
||||||
|
tombol_setuju.textContent = 'Memproses...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const http_response = await fetch(`http://localhost:3000/api/admin/verifikasi/${id_warga_terpilih}`, {
|
// Diubah ke relative path
|
||||||
|
const http_response = await fetch(`/api/admin/verifikasi/${antrean_id_warga}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ status_validasi: keputusan_status })
|
body: JSON.stringify({ status_validasi: antrean_status_baru })
|
||||||
});
|
});
|
||||||
|
|
||||||
const json_result = await http_response.json();
|
const json_result = await http_response.json();
|
||||||
if (!http_response.ok) throw new Error(json_result.error || 'Gagal mengubah status.');
|
if (!http_response.ok) throw new Error(json_result.error || 'Gagal mengubah status.');
|
||||||
|
|
||||||
|
tutup_modal();
|
||||||
alert('Status otorisasi data berhasil diperbarui!');
|
alert('Status otorisasi data berhasil diperbarui!');
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
|
||||||
} catch (error_sistem) {
|
} catch (error_sistem) {
|
||||||
|
tutup_modal();
|
||||||
alert('Gagal mengeksekusi perintah: ' + error_sistem.message);
|
alert('Gagal mengeksekusi perintah: ' + error_sistem.message);
|
||||||
|
} finally {
|
||||||
|
tombol_setuju.disabled = false;
|
||||||
|
tombol_setuju.textContent = 'Ya, Lanjutkan';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eksekusi inisialisasi pemuatan data awal
|
|
||||||
ambil_data_detail_warga();
|
ambil_data_detail_warga();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+2
-9
@@ -33,7 +33,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mengirimkan data otentikasi admin menuju API backend server secara asinkron
|
|
||||||
async function eksekusi_login_admin() {
|
async function eksekusi_login_admin() {
|
||||||
const username_val = document.getElementById('input_username').value;
|
const username_val = document.getElementById('input_username').value;
|
||||||
const password_val = document.getElementById('input_password').value;
|
const password_val = document.getElementById('input_password').value;
|
||||||
@@ -51,10 +50,8 @@
|
|||||||
tombol_login.textContent = 'Otorisasi Sistem...';
|
tombol_login.textContent = 'Otorisasi Sistem...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/* Baris Kompleks: Mengirimkan request POST JSON payload kredensial admin ke server port 3000.
|
// Diubah ke relative path
|
||||||
Jika kredensial terverifikasi sah oleh database, string token otorisasi disimpan ke dalam localStorage
|
const http_response = await fetch('/api/auth/login-admin', {
|
||||||
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -71,10 +68,7 @@
|
|||||||
throw new Error(json_result.error || 'Akses ditolak oleh otoritas server.');
|
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);
|
localStorage.setItem('token_admin', json_result.token);
|
||||||
|
|
||||||
// Redireksi antarmuka secara instan menuju halaman dasbor pusat kontrol utama
|
|
||||||
window.location.href = 'admin_dasbor.html';
|
window.location.href = 'admin_dasbor.html';
|
||||||
|
|
||||||
} catch (error_sistem) {
|
} catch (error_sistem) {
|
||||||
@@ -86,7 +80,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mengikat event liistener enter pada kolom kata sandi admin untuk efisiensi aksesibilitas
|
|
||||||
document.getElementById('input_password').addEventListener('keypress', function(e) {
|
document.getElementById('input_password').addEventListener('keypress', function(e) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
eksekusi_login_admin();
|
eksekusi_login_admin();
|
||||||
|
|||||||
+54
-76
@@ -34,13 +34,6 @@
|
|||||||
<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">
|
<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>
|
<p id="pesan_hasil" class="text-sm font-semibold"></p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -67,78 +60,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</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">
|
|
||||||
© 2026 Dinas Sosial Kota Pontianak (Dummy). All rights reserved.
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const leaflet_map = L.map('map_container').setView([-0.0227, 109.3425], 13);
|
const leaflet_map = L.map('map_container').setView([-0.0227, 109.3425], 13);
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
attribution: '© OpenStreetMap contributors'
|
attribution: '© OpenStreetMap contributors'
|
||||||
}).addTo(leaflet_map);
|
}).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]
|
||||||
|
});
|
||||||
|
|
||||||
// Fungsi baru untuk memuat statistik secara asinkron dari basis data
|
|
||||||
async function muat_statistik_publik() {
|
async function muat_statistik_publik() {
|
||||||
try {
|
try {
|
||||||
const respons_http = await fetch('http://localhost:3000/api/survey/statistik');
|
const respons_http = await fetch('/api/survey/statistik');
|
||||||
if (!respons_http.ok) throw new Error('Gagal mengambil data statistik');
|
if (!respons_http.ok) throw new Error('Gagal mengambil data statistik');
|
||||||
|
|
||||||
const data_agregat = await respons_http.json();
|
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_terverifikasi').textContent = data_agregat.terverifikasi || 0;
|
||||||
document.getElementById('stat_publik_diproses').textContent = data_agregat.diproses || 0;
|
document.getElementById('stat_publik_diproses').textContent = data_agregat.diproses || 0;
|
||||||
|
|
||||||
/* Baris Kompleks: Menggunakan objek bawaan Date pada JavaScript untuk
|
const waktu_terformat_wib = new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', timeZone: 'Asia/Jakarta' });
|
||||||
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`;
|
document.getElementById('stat_publik_waktu').textContent = `Hari ini, ${waktu_terformat_wib} WIB`;
|
||||||
|
|
||||||
} catch (error_sistem) {
|
} catch (error_sistem) {
|
||||||
console.error("Sinkronisasi statistik gagal:", error_sistem.message);
|
|
||||||
document.getElementById('stat_publik_waktu').textContent = 'Sistem Terhubung';
|
document.getElementById('stat_publik_waktu').textContent = 'Sistem Terhubung';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,37 +105,61 @@
|
|||||||
const pesan_hasil = document.getElementById('pesan_hasil');
|
const pesan_hasil = document.getElementById('pesan_hasil');
|
||||||
const tombol_cari = document.getElementById('btn_cari');
|
const tombol_cari = document.getElementById('btn_cari');
|
||||||
|
|
||||||
if (!/^\d{16}$/.test(input_nik_value)) {
|
if (!/^\d{16}$/.test(input_nik_value)) return alert('Masukkan NIK yang valid (16 digit angka).');
|
||||||
return alert('Masukkan NIK yang valid (16 digit angka).');
|
|
||||||
}
|
|
||||||
|
|
||||||
tombol_cari.disabled = true;
|
tombol_cari.disabled = true;
|
||||||
tombol_cari.textContent = 'Mencari...';
|
tombol_cari.textContent = 'Mencari...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const http_response = await fetch(`http://localhost:3000/api/survey/cek-nik/${input_nik_value}`);
|
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();
|
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.remove('hidden', 'bg-red-50', 'bg-emerald-50', 'border-red-200', 'border-emerald-200');
|
||||||
kotak_hasil.classList.add('bg-emerald-50', 'border-emerald-200');
|
kotak_hasil.classList.add('bg-emerald-50', 'border-emerald-200');
|
||||||
pesan_hasil.className = 'text-emerald-700';
|
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 (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)];
|
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}`)
|
.bindPopup(`<b>${json_data.nama_kk}</b><br>Status: ${json_data.status_validasi}`)
|
||||||
.openPopup();
|
.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) {
|
} catch (error_object) {
|
||||||
@@ -193,9 +173,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mengeksekusi penarikan data statistik tepat saat halaman selesai dimuat
|
|
||||||
muat_statistik_publik();
|
muat_statistik_publik();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+6
-35
@@ -68,21 +68,12 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mengambil token autentikasi petugas untuk validasi sesi REST API
|
|
||||||
const token_petugas = localStorage.getItem('token_petugas');
|
const token_petugas = localStorage.getItem('token_petugas');
|
||||||
|
|
||||||
// Fungsi pengecekan proteksi halaman (RBAC) saat pertama kali dimuat
|
|
||||||
function periksa_sesi_petugas() {
|
function periksa_sesi_petugas() {
|
||||||
/* Sesi dummy dinonaktifkan sementara untuk mempermudah pengujian UAS.
|
// Sesi dinonaktifkan sementara untuk mempermudah testing
|
||||||
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) {
|
function render_data_ke_tabel(daftar_warga) {
|
||||||
const wadah_list = document.getElementById('wadah_list_antrean');
|
const wadah_list = document.getElementById('wadah_list_antrean');
|
||||||
const label_antrean = document.getElementById('label_jumlah_antrean');
|
const label_antrean = document.getElementById('label_jumlah_antrean');
|
||||||
@@ -96,7 +87,6 @@
|
|||||||
|
|
||||||
label_antrean.textContent = `${daftar_warga.length} Rumah Tangga`;
|
label_antrean.textContent = `${daftar_warga.length} Rumah Tangga`;
|
||||||
|
|
||||||
// Variabel counter untuk menghitung jumlah status agregat data
|
|
||||||
let total_pending = 0;
|
let total_pending = 0;
|
||||||
let total_verified = 0;
|
let total_verified = 0;
|
||||||
|
|
||||||
@@ -107,9 +97,6 @@
|
|||||||
const item_div = document.createElement('div');
|
const item_div = document.createElement('div');
|
||||||
item_div.className = 'p-4 flex justify-between items-center hover:bg-gray-50 transition-colors';
|
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 = `
|
item_div.innerHTML = `
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<h4 class="font-bold text-gray-800 text-sm kelas-nama"></h4>
|
<h4 class="font-bold text-gray-800 text-sm kelas-nama"></h4>
|
||||||
@@ -123,14 +110,12 @@
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Menyuntikkan nilai data secara aman menggunakan textContent
|
|
||||||
item_div.querySelector('.kelas-nama').textContent = warga.nama_kk;
|
item_div.querySelector('.kelas-nama').textContent = warga.nama_kk;
|
||||||
item_div.querySelector('.kelas-nik').textContent = warga.nik;
|
item_div.querySelector('.kelas-nik').textContent = warga.nik;
|
||||||
|
|
||||||
const badge = item_div.querySelector('.kelas-badge');
|
const badge = item_div.querySelector('.kelas-badge');
|
||||||
badge.textContent = warga.status_validasi;
|
badge.textContent = warga.status_validasi;
|
||||||
|
|
||||||
// Menentukan warna badge visual berdasarkan status validasi dari database
|
|
||||||
if (warga.status_validasi.toLowerCase() === 'verified') {
|
if (warga.status_validasi.toLowerCase() === 'verified') {
|
||||||
badge.className += ' bg-emerald-50 text-emerald-700';
|
badge.className += ' bg-emerald-50 text-emerald-700';
|
||||||
} else if (warga.status_validasi.toLowerCase() === 'rejected') {
|
} else if (warga.status_validasi.toLowerCase() === 'rejected') {
|
||||||
@@ -142,13 +127,11 @@
|
|||||||
wadah_list.appendChild(item_div);
|
wadah_list.appendChild(item_div);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Memperbarui indikator angka papan KPI atas
|
|
||||||
document.getElementById('stat_total').textContent = daftar_warga.length;
|
document.getElementById('stat_total').textContent = daftar_warga.length;
|
||||||
document.getElementById('stat_pending').textContent = total_pending;
|
document.getElementById('stat_pending').textContent = total_pending;
|
||||||
document.getElementById('stat_verified').textContent = total_verified;
|
document.getElementById('stat_verified').textContent = total_verified;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mengambil data seluruh antrean mustahik dari server backend Node.js
|
|
||||||
async function ambil_antrean_survei() {
|
async function ambil_antrean_survei() {
|
||||||
const msg_container = document.getElementById('status_message_container');
|
const msg_container = document.getElementById('status_message_container');
|
||||||
|
|
||||||
@@ -156,22 +139,13 @@
|
|||||||
msg_container.classList.remove('hidden');
|
msg_container.classList.remove('hidden');
|
||||||
msg_container.textContent = 'Memuat daftar antrean wilayah...';
|
msg_container.textContent = 'Memuat daftar antrean wilayah...';
|
||||||
|
|
||||||
/* Contoh implementasi fetch asinkron ke server lokal port 3000.
|
// Diubah ke relative path dan diaktifkan untuk mengambil data aslinya
|
||||||
Gunakan endpoint yang sesuai (misal mengambil seluruh data MUSTAHIK untuk petugas) */
|
const respons = await fetch('/api/admin/mustahik');
|
||||||
// const respons = await fetch('http://localhost:3000/api/survey/list-antrean');
|
if (!respons.ok) throw new Error('Gagal mengambil data dari server.');
|
||||||
// if (!respons.ok) throw new Error('Gagal mengambil data dari server.');
|
const data_warga = await respons.json();
|
||||||
// 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');
|
msg_container.classList.add('hidden');
|
||||||
render_data_ke_tabel(data_warga_mock);
|
render_data_ke_tabel(data_warga);
|
||||||
|
|
||||||
} catch (error_sistem) {
|
} catch (error_sistem) {
|
||||||
msg_container.classList.remove('hidden');
|
msg_container.classList.remove('hidden');
|
||||||
@@ -179,17 +153,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fungsi menghapus token autentikasi sesi saat petugas keluar aplikasi
|
|
||||||
function proses_logout_petugas() {
|
function proses_logout_petugas() {
|
||||||
localStorage.removeItem('token_petugas');
|
localStorage.removeItem('token_petugas');
|
||||||
alert('Sesi berhasil diakhiri.');
|
alert('Sesi berhasil diakhiri.');
|
||||||
window.location.href = 'index.html';
|
window.location.href = 'index.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eksekusi trigger inisialisasi awal saat halaman selesai dimuat oleh browser
|
|
||||||
periksa_sesi_petugas();
|
periksa_sesi_petugas();
|
||||||
ambil_antrean_survei();
|
ambil_antrean_survei();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+2
-10
@@ -28,14 +28,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mengirimkan data otentikasi menuju API backend server
|
|
||||||
async function eksekusi_login_petugas() {
|
async function eksekusi_login_petugas() {
|
||||||
const username_val = document.getElementById('input_username').value;
|
const username_val = document.getElementById('input_username').value;
|
||||||
const password_val = document.getElementById('input_password').value;
|
const password_val = document.getElementById('input_password').value;
|
||||||
const wadah_error = document.getElementById('pesan_error');
|
const wadah_error = document.getElementById('pesan_error');
|
||||||
const tombol_login = document.getElementById('btn_submit_login');
|
const tombol_login = document.getElementById('btn_submit_login');
|
||||||
|
|
||||||
// Validasi masukan dasar pada sisi klien sebelum memicu lalu lintas jaringan
|
|
||||||
if (!username_val || !password_val) {
|
if (!username_val || !password_val) {
|
||||||
wadah_error.textContent = 'Kolom username dan kata sandi tidak boleh kosong.';
|
wadah_error.textContent = 'Kolom username dan kata sandi tidak boleh kosong.';
|
||||||
wadah_error.classList.remove('hidden');
|
wadah_error.classList.remove('hidden');
|
||||||
@@ -47,10 +45,8 @@
|
|||||||
tombol_login.textContent = 'Memverifikasi...';
|
tombol_login.textContent = 'Memverifikasi...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/* Baris Kompleks: Melakukan transmisi payload data sensitif terenkripsi via jaringan menggunakan HTTP POST
|
// Diubah ke relative path
|
||||||
menuju peladen port 3000, serta menyimpan string token autentikasi ke media penyimpanan lokal peramban
|
const http_response = await fetch('/api/auth/login-petugas', {
|
||||||
(localStorage) jika validasi kredensial dinyatakan sah oleh server database. */
|
|
||||||
const http_response = await fetch('http://localhost:3000/api/auth/login-petugas', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -67,10 +63,7 @@
|
|||||||
throw new Error(json_result.error || 'Gagal masuk ke dalam sistem.');
|
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);
|
localStorage.setItem('token_petugas', json_result.token);
|
||||||
|
|
||||||
// Mengalihkan fokus antarmuka menuju halaman dasbor manajemen tugas lapangan
|
|
||||||
window.location.href = 'petugas_dasbor.html';
|
window.location.href = 'petugas_dasbor.html';
|
||||||
|
|
||||||
} catch (error_sistem) {
|
} catch (error_sistem) {
|
||||||
@@ -82,7 +75,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mendukung eksekusi tombol enter saat berada di kolom input kata sandi
|
|
||||||
document.getElementById('input_password').addEventListener('keypress', function(e) {
|
document.getElementById('input_password').addEventListener('keypress', function(e) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
eksekusi_login_petugas();
|
eksekusi_login_petugas();
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Portofolio Sistem Informasi Geografis - Muhammad Iqbal Maulana</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Plus Jakarta Sans', sans-serif; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-slate-50 text-slate-800 min-h-screen flex flex-col selection:bg-emerald-500 selection:text-white">
|
||||||
|
|
||||||
|
<header class="bg-white border-b border-slate-200/80 sticky top-0 z-50 backdrop-blur-md bg-white/80">
|
||||||
|
<div class="max-w-5xl mx-auto px-6 py-4 flex flex-col sm:flex-row justify-between items-center gap-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-10 h-10 bg-gradient-to-br from-emerald-500 to-teal-600 rounded-xl flex items-center justify-center text-white font-extrabold shadow-md shadow-emerald-500/20">
|
||||||
|
SIG
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="font-bold text-slate-900 text-sm leading-tight">Portal Tugas SIG</h1>
|
||||||
|
<p class="text-[11px] text-slate-500 font-medium tracking-wide uppercase">Informatika Universitas Tanjungpura</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center sm:text-right">
|
||||||
|
<p class="text-sm font-semibold text-slate-800">Muhammad Iqbal Maulana</p>
|
||||||
|
<p class="text-xs font-mono text-slate-400 mt-0.5">NIM: D1041231047 </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="flex-grow max-w-5xl mx-auto w-full px-6 py-12 space-y-12">
|
||||||
|
|
||||||
|
<section class="text-center max-w-2xl mx-auto space-y-4">
|
||||||
|
<span class="px-3 py-1 bg-emerald-50 border border-emerald-200 text-emerald-700 text-xs font-bold rounded-full uppercase tracking-wider inline-block">UAS Evaluasi Praktis</span>
|
||||||
|
<h2 class="text-3xl sm:text-4xl font-extrabold text-slate-900 tracking-tight leading-tight">
|
||||||
|
Kompilasi Sistem Informasi Geografis Terapan
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-slate-500 font-light max-w-lg mx-auto leading-relaxed">
|
||||||
|
Aplikasi WebGIS produksi terintegrasi yang dikembangkan untuk memenuhi tugas akhir semester, berjalan di atas infrastruktur kontainer peladen Docker Coolify.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid grid-cols-1 md:grid-cols-2 gap-6 pt-4">
|
||||||
|
|
||||||
|
<div class="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm hover:shadow-xl hover:border-slate-300 transition-all duration-300 flex flex-col justify-between group">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="w-12 h-12 bg-amber-50 border border-amber-200 text-amber-600 rounded-2xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 22V2a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v20"/><path d="M18 12h4"/><path d="M10 6h4"/><path d="M12 2v4"/><circle cx="12" cy="12" r="2"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h3 class="font-bold text-lg text-slate-900 tracking-tight">Web Pemetaan Pom Bensin & Wilayah</h3>
|
||||||
|
<p class="text-xs font-medium text-amber-600 uppercase tracking-wide">Analisis Geospasial Infrastruktur</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-slate-400 font-light leading-relaxed">
|
||||||
|
Sistem informasi spasial yang memetakan persebaran Stasiun Pengisian Bahan Bakar Umum (SPBU) di area Pontianak, dilengkapi zonasi wilayah jangkauan pelayanan guna analisis efisiensi fasilitas publik.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="pt-6 mt-6 border-t border-slate-100 flex items-center justify-between">
|
||||||
|
<a href="http://e8wwc0wgwsws0w4ggoc8g88g.203.24.51.230.sslip.io/portal.html" target="_blank" class="px-4 py-2.5 bg-slate-900 hover:bg-slate-800 text-white text-xs font-bold rounded-xl transition-all shadow-sm flex items-center gap-1">
|
||||||
|
Buka Aplikasi
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm hover:shadow-xl hover:border-slate-300 transition-all duration-300 flex flex-col justify-between group">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="w-12 h-12 bg-emerald-50 border border-emerald-200 text-emerald-600 rounded-2xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h3 class="font-bold text-lg text-slate-900 tracking-tight">WebGIS PovertyMap</h3>
|
||||||
|
<p class="text-xs font-medium text-emerald-600 uppercase tracking-wide">Transparansi Sosial & Validasi BPS</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-slate-400 font-light leading-relaxed">
|
||||||
|
Sistem Informasi Geografis pemetaan klaster kemiskinan masyarakat Kota Pontianak terintegrasi 10 parameter BPS. Menghubungkan titik hunian warga dengan pangkalan rumah ibadah terdekat secara asinkron.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="pt-6 mt-6 border-t border-slate-100 flex items-center justify-between">
|
||||||
|
<a href="http://l08g8oso4gk0wc400cggog4s.203.24.51.230.sslip.io/index.html" target="_blank" class="px-4 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-bold rounded-xl transition-all shadow-sm flex items-center gap-1">
|
||||||
|
Buka Aplikasi
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="bg-white border-t border-slate-200 text-center py-6 text-xs font-medium text-slate-400 mt-auto">
|
||||||
|
© 2026 Jurusan Informatika Universitas Tanjung pura. Dibuat dengan dedikasi penuh.
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+22
-22
@@ -75,6 +75,18 @@
|
|||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Jumlah Anggota Keluarga Tanggungan</label>
|
<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">
|
<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>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">Agama Kepala Keluarga</label>
|
||||||
|
<select id="input_agama" class="w-full p-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 outline-none">
|
||||||
|
<option value="">Pilih Agama...</option>
|
||||||
|
<option value="Islam">Islam</option>
|
||||||
|
<option value="Kristen Protestan">Kristen Protestan</option>
|
||||||
|
<option value="Katolik">Katolik</option>
|
||||||
|
<option value="Hindu">Hindu</option>
|
||||||
|
<option value="Buddha">Buddha</option>
|
||||||
|
<option value="Konghucu">Konghucu</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="langkah_2" class="hidden space-y-4">
|
<div id="langkah_2" class="hidden space-y-4">
|
||||||
@@ -196,7 +208,6 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Deklarasi Status Langkah Global
|
|
||||||
let langkah_sekarang = 1;
|
let langkah_sekarang = 1;
|
||||||
const total_langkah = 4;
|
const total_langkah = 4;
|
||||||
|
|
||||||
@@ -208,25 +219,18 @@
|
|||||||
const btn_kirim = document.getElementById('btn_kirim');
|
const btn_kirim = document.getElementById('btn_kirim');
|
||||||
const peringatan_error = document.getElementById('peringatan_error');
|
const peringatan_error = document.getElementById('peringatan_error');
|
||||||
|
|
||||||
// Menampilkan pesan notifikasi kegagalan validasi ke antarmuka pengguna
|
|
||||||
function tampilkan_error(pesan) {
|
function tampilkan_error(pesan) {
|
||||||
peringatan_error.textContent = pesan;
|
peringatan_error.textContent = pesan;
|
||||||
peringatan_error.classList.remove('hidden');
|
peringatan_error.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menyembunyikan wadah notifikasi kegagalan
|
|
||||||
function sembunyikan_error() {
|
function sembunyikan_error() {
|
||||||
peringatan_error.classList.add('hidden');
|
peringatan_error.classList.add('hidden');
|
||||||
peringatan_error.textContent = '';
|
peringatan_error.textContent = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memastikan kelayakan data masukan di setiap bagian formulir sebelum melangkah
|
|
||||||
function validasi_langkah_aktif() {
|
function validasi_langkah_aktif() {
|
||||||
sembunyikan_error();
|
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) {
|
switch(langkah_sekarang) {
|
||||||
case 1:
|
case 1:
|
||||||
if (!/^\d{16}$/.test(document.getElementById('input_nik').value)) {
|
if (!/^\d{16}$/.test(document.getElementById('input_nik').value)) {
|
||||||
@@ -245,6 +249,13 @@
|
|||||||
tampilkan_error('Jumlah anggota keluarga tanggungan harus diisi.');
|
tampilkan_error('Jumlah anggota keluarga tanggungan harus diisi.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// ==========================================
|
||||||
|
// BARIS BARU: Memastikan agama telah dipilih
|
||||||
|
// ==========================================
|
||||||
|
if (document.getElementById('input_agama').value === '') {
|
||||||
|
tampilkan_error('Agama kepercayaan wajib dipilih untuk penentuan lokasi bantuan.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
if (!document.getElementById('input_luas_lantai').value ||
|
if (!document.getElementById('input_luas_lantai').value ||
|
||||||
@@ -260,7 +271,7 @@
|
|||||||
!document.getElementById('input_sumber_penerangan').value ||
|
!document.getElementById('input_sumber_penerangan').value ||
|
||||||
!document.getElementById('input_frekuensi_makan').value ||
|
!document.getElementById('input_frekuensi_makan').value ||
|
||||||
!document.getElementById('input_kepemilikan_aset').value) {
|
!document.getElementById('input_kepemilikan_aset').value) {
|
||||||
tampilkan_error('Seluruh parameter utilitas dan sosial ekonomi wajib dipilih.');
|
tampilkan_error('Seluruh parameter utilitas and sosial ekonomi wajib dipilih.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (document.getElementById('input_pekerjaan').value.trim() === '') {
|
if (document.getElementById('input_pekerjaan').value.trim() === '') {
|
||||||
@@ -283,7 +294,6 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menggerakkan alur visual formulir multi-langkah maju atau mundur
|
|
||||||
function ubah_langkah(arah) {
|
function ubah_langkah(arah) {
|
||||||
if (arah === 1 && !validasi_langkah_aktif()) return;
|
if (arah === 1 && !validasi_langkah_aktif()) return;
|
||||||
|
|
||||||
@@ -294,7 +304,6 @@
|
|||||||
perbarui_antarmuka_wizard();
|
perbarui_antarmuka_wizard();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sinkronisasi status bar kemajuan dan kontrol tombol navigasi bawah
|
|
||||||
function perbarui_antarmuka_wizard() {
|
function perbarui_antarmuka_wizard() {
|
||||||
if (langkah_sekarang === 1) {
|
if (langkah_sekarang === 1) {
|
||||||
btn_mundur.classList.add('hidden');
|
btn_mundur.classList.add('hidden');
|
||||||
@@ -327,7 +336,6 @@
|
|||||||
if(document.getElementById('garis_3')) document.getElementById('garis_3').style.width = langkah_sekarang >= 4 ? '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() {
|
function inisialisasi_peta() {
|
||||||
if (instansi_peta !== null) {
|
if (instansi_peta !== null) {
|
||||||
instansi_peta.invalidateSize();
|
instansi_peta.invalidateSize();
|
||||||
@@ -339,9 +347,6 @@
|
|||||||
attribution: '© OpenStreetMap'
|
attribution: '© OpenStreetMap'
|
||||||
}).addTo(instansi_peta);
|
}).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) {
|
instansi_peta.on('click', function(e) {
|
||||||
const lat = e.latlng.lat;
|
const lat = e.latlng.lat;
|
||||||
const lng = e.latlng.lng;
|
const lng = e.latlng.lng;
|
||||||
@@ -355,7 +360,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mengakses perangkat keras penerima gelombang navigasi satelit melalui web API browser
|
|
||||||
function dapatkan_lokasi_gps() {
|
function dapatkan_lokasi_gps() {
|
||||||
if (navigator.geolocation) {
|
if (navigator.geolocation) {
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
@@ -380,22 +384,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mengirimkan enkapsulasi objek data survei menuju web server secara asinkron
|
|
||||||
async function kirim_data_survei() {
|
async function kirim_data_survei() {
|
||||||
if (!validasi_langkah_aktif()) return;
|
if (!validasi_langkah_aktif()) return;
|
||||||
|
|
||||||
btn_kirim.disabled = true;
|
btn_kirim.disabled = true;
|
||||||
btn_kirim.innerHTML = 'Mengirim Data...';
|
btn_kirim.innerHTML = 'Mengirim Data...';
|
||||||
|
|
||||||
// Konstruksi objek payload JSON disesuaikan secara presisi dengan skema database_baru.sqlite
|
|
||||||
const payload_survei = {
|
const payload_survei = {
|
||||||
nik: document.getElementById('input_nik').value,
|
nik: document.getElementById('input_nik').value,
|
||||||
nama_kk: document.getElementById('input_nama_kk').value,
|
nama_kk: document.getElementById('input_nama_kk').value,
|
||||||
no_kk: document.getElementById('input_no_kk').value,
|
no_kk: document.getElementById('input_no_kk').value,
|
||||||
jumlah_tanggungan: parseInt(document.getElementById('input_jumlah_tanggungan').value) || 0,
|
jumlah_tanggungan: parseInt(document.getElementById('input_jumlah_tanggungan').value) || 0,
|
||||||
|
agama: document.getElementById('input_agama').value, // <--- Ini yang sebelumnya tertinggal
|
||||||
lat: parseFloat(document.getElementById('input_lat').value),
|
lat: parseFloat(document.getElementById('input_lat').value),
|
||||||
lng: parseFloat(document.getElementById('input_lng').value),
|
lng: parseFloat(document.getElementById('input_lng').value),
|
||||||
id_rumah_ibadah: null,
|
|
||||||
indikator_bps: {
|
indikator_bps: {
|
||||||
luas_lantai_per_kapita: document.getElementById('input_luas_lantai').value,
|
luas_lantai_per_kapita: document.getElementById('input_luas_lantai').value,
|
||||||
material_lantai: document.getElementById('input_material_lantai').value,
|
material_lantai: document.getElementById('input_material_lantai').value,
|
||||||
@@ -411,9 +413,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/* Baris Kompleks: Eksekusi transmisi data asinkron via HTTP POST menuju endpoint server.
|
const http_response = await fetch('/api/survey/submit', {
|
||||||
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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload_survei)
|
body: JSON.stringify(payload_survei)
|
||||||
|
|||||||
Reference in New Issue
Block a user