Files
WEBGIS_Project/backend/routes/poverty.js
T
tistopandita 5386300498 add poverty
2026-06-11 13:56:18 +07:00

1187 lines
48 KiB
JavaScript

const express = require("express");
const db = require("../db");
const TEMPAT_IBADAH_TYPES = ["Gereja Katolik", "GPIB", "GKI", "HKBP", "Pentakosta"];
const createTempatIbadah = `
CREATE TABLE IF NOT EXISTS tempat_ibadah (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(150) NOT NULL,
jenis ENUM('Gereja Katolik','GPIB','GKI','HKBP','Pentakosta') NOT NULL,
alamat TEXT NOT NULL,
kelurahan VARCHAR(100) NOT NULL,
lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL,
kapasitas_bantuan INT NOT NULL DEFAULT 0,
keterangan TEXT,
aktif TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`;
async function initTempatIbadahTable() {
await db.execute(createTempatIbadah);
const columns = await tableColumns("tempat_ibadah");
if (!columns.includes("aktif")) {
await db.execute("ALTER TABLE tempat_ibadah ADD COLUMN aktif TINYINT(1) NOT NULL DEFAULT 1");
}
if (!columns.includes("created_at")) {
await db.execute("ALTER TABLE tempat_ibadah ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP");
}
if (!columns.includes("kapasitas_bantuan")) {
await db.execute("ALTER TABLE tempat_ibadah ADD COLUMN kapasitas_bantuan INT NOT NULL DEFAULT 0");
}
if (!columns.includes("keterangan")) {
await db.execute("ALTER TABLE tempat_ibadah ADD COLUMN keterangan TEXT");
}
await db.execute(`
UPDATE tempat_ibadah
SET jenis = 'Gereja Katolik'
WHERE jenis IS NULL
OR jenis = ''
OR jenis NOT IN ('Gereja Katolik','GPIB','GKI','HKBP','Pentakosta')
`);
await db.execute(`
ALTER TABLE tempat_ibadah
MODIFY jenis ENUM('Gereja Katolik','GPIB','GKI','HKBP','Pentakosta') NOT NULL
`);
await db.execute(`
UPDATE tempat_ibadah
SET jenis = 'Gereja Katolik'
WHERE jenis IS NULL
OR jenis = ''
OR jenis NOT IN ('Gereja Katolik','GPIB','GKI','HKBP','Pentakosta')
`);
}
(async () => {
try {
await initTempatIbadahTable();
} catch (e) {
console.error(e);
}
})();
const router = express.Router();
let tablesReady = false;
const DEFAULT_KRITERIA = [
{ kode: "pendapatan", nama_kriteria: "Pendapatan", bobot: 0.35, atribut: "cost" },
{ kode: "jumlah_tanggungan", nama_kriteria: "Jumlah Tanggungan", bobot: 0.2, atribut: "benefit" },
{ kode: "kondisi_rumah", nama_kriteria: "Kondisi Rumah", bobot: 0.15, atribut: "benefit" },
{ kode: "pekerjaan", nama_kriteria: "Pekerjaan", bobot: 0.15, atribut: "benefit" },
{ kode: "pendidikan", nama_kriteria: "Pendidikan Kepala Keluarga", bobot: 0.1, atribut: "benefit" },
{ kode: "kondisi_kesehatan", nama_kriteria: "Kondisi Kesehatan", bobot: 0.05, atribut: "benefit" },
];
const HOUSE_SCORE = {
sangat_buruk: 5,
buruk: 4,
semi_permanen: 3,
sederhana: 2,
layak: 1,
};
const JOB_SCORE = {
pengangguran: 5,
buruh_harian: 4,
petani: 3,
nelayan: 3,
pedagang_kecil: 3,
karyawan: 2,
wiraswasta: 2,
pns: 1,
pensiunan: 2,
lainnya: 3,
};
const EDUCATION_SCORE = {
tidak_sekolah: 5,
sd: 4,
smp: 3,
sma: 2,
diploma: 1.5,
sarjana: 1,
};
const HEALTH_SCORE = {
disabilitas: 5,
kronis: 5,
lansia: 4,
sakit_sering: 4,
sakit_ringan: 3,
sehat: 1,
};
function toNumber(value, fallback = 0) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
}
function sawCategory(score) {
if (score >= 0.8) return "Sangat Membutuhkan";
if (score >= 0.6) return "Membutuhkan";
if (score >= 0.4) return "Rentan";
return "Relatif Mampu";
}
function markerColor(kategori) {
return {
"Sangat Membutuhkan": "#dc2626",
Membutuhkan: "#f97316",
Rentan: "#facc15",
"Relatif Mampu": "#22c55e",
}[kategori] || "#64748b";
}
function rawCriterionValue(keluarga, kode) {
if (kode === "pendapatan") return Math.max(toNumber(keluarga.pendapatan_bulanan, 1), 1);
if (kode === "jumlah_tanggungan") return Math.max(toNumber(keluarga.jumlah_tanggungan), 0);
if (kode === "kondisi_rumah") return HOUSE_SCORE[keluarga.kondisi_rumah] || 1;
if (kode === "pekerjaan") return JOB_SCORE[keluarga.pekerjaan] || JOB_SCORE.lainnya;
if (kode === "pendidikan") return EDUCATION_SCORE[keluarga.pendidikan] || 2;
if (kode === "kondisi_kesehatan") return HEALTH_SCORE[keluarga.kondisi_kesehatan] || 1;
return 0;
}
function maskedName(item) {
return `Keluarga ${String(item.id_keluarga).padStart(3, "0")}`;
}
function privacyOffset(id) {
const seed = Number(id) || 1;
return {
lat: (((seed * 37) % 13) - 6) * 0.000035,
lng: (((seed * 53) % 17) - 8) * 0.000035,
};
}
function applyPrivacy(item, role = "admin", privacyMode = false) {
const hidden = role === "pengurus" || privacyMode;
const offset = hidden ? privacyOffset(item.id_keluarga) : { lat: 0, lng: 0 };
return {
...item,
nama_kepala_keluarga: hidden ? maskedName(item) : item.nama_kepala_keluarga,
latitude: toNumber(item.latitude) + offset.lat,
longitude: toNumber(item.longitude) + offset.lng,
pendapatan_bulanan: hidden ? null : item.pendapatan_bulanan,
pendapatan_label: hidden ? "Disembunyikan" : item.pendapatan_bulanan,
nomor_kartu_keluarga: hidden ? null : item.nomor_kartu_keluarga,
is_private: hidden,
};
}
function buildFeature(item) {
return {
type: "Feature",
geometry: {
type: "Point",
coordinates: [toNumber(item.longitude), toNumber(item.latitude)],
},
properties: {
...item,
color: markerColor(item.kategori),
},
};
}
async function ensureTables() {
if (tablesReady) return;
await initTempatIbadahTable();
await db.execute(`
CREATE TABLE IF NOT EXISTS users (
id_user INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(120) NOT NULL,
username VARCHAR(80) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role ENUM('admin','pengurus') NOT NULL DEFAULT 'pengurus',
aktif TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await migrateLegacyKeluarga();
await db.execute(`
CREATE TABLE IF NOT EXISTS keluarga (
id_keluarga INT AUTO_INCREMENT PRIMARY KEY,
nomor_kartu_keluarga VARCHAR(40) NOT NULL UNIQUE,
nama_kepala_keluarga VARCHAR(160) NOT NULL,
alamat TEXT NOT NULL,
wilayah VARCHAR(120) NOT NULL DEFAULT 'Belum Dipetakan',
latitude DOUBLE NOT NULL,
longitude DOUBLE NOT NULL,
jumlah_anggota INT NOT NULL DEFAULT 1,
jumlah_tanggungan INT NOT NULL DEFAULT 0,
pendapatan_bulanan DECIMAL(14,2) NOT NULL DEFAULT 0,
pekerjaan VARCHAR(60) NOT NULL DEFAULT 'lainnya',
pendidikan VARCHAR(60) NOT NULL DEFAULT 'sma',
kondisi_rumah VARCHAR(60) NOT NULL DEFAULT 'sederhana',
kondisi_kesehatan VARCHAR(60) NOT NULL DEFAULT 'sehat',
status_verifikasi ENUM('pending','terverifikasi','ditolak') NOT NULL DEFAULT 'pending',
tanggal_update DATE NOT NULL,
aktif TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_keluarga_lokasi (latitude, longitude),
INDEX idx_keluarga_wilayah (wilayah),
INDEX idx_keluarga_verifikasi (status_verifikasi)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await db.execute(`
CREATE TABLE IF NOT EXISTS kriteria (
id_kriteria INT AUTO_INCREMENT PRIMARY KEY,
kode VARCHAR(60) NOT NULL UNIQUE,
nama_kriteria VARCHAR(120) NOT NULL,
bobot DECIMAL(6,4) NOT NULL,
atribut ENUM('benefit','cost') NOT NULL,
aktif TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await db.execute(`
CREATE TABLE IF NOT EXISTS penilaian (
id_penilaian INT AUTO_INCREMENT PRIMARY KEY,
id_keluarga INT NOT NULL,
id_kriteria INT NOT NULL,
nilai DECIMAL(14,4) NOT NULL DEFAULT 0,
nilai_normalisasi DECIMAL(14,6) NOT NULL DEFAULT 0,
nilai_terbobot DECIMAL(14,6) NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uniq_penilaian (id_keluarga, id_kriteria),
CONSTRAINT fk_penilaian_keluarga FOREIGN KEY (id_keluarga) REFERENCES keluarga(id_keluarga) ON DELETE CASCADE,
CONSTRAINT fk_penilaian_kriteria FOREIGN KEY (id_kriteria) REFERENCES kriteria(id_kriteria) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await db.execute(`
CREATE TABLE IF NOT EXISTS hasil_saw (
id_hasil INT AUTO_INCREMENT PRIMARY KEY,
id_keluarga INT NOT NULL UNIQUE,
skor_saw DECIMAL(12,6) NOT NULL DEFAULT 0,
kategori VARCHAR(40) NOT NULL,
ranking INT NOT NULL DEFAULT 0,
calculated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_hasil_keluarga FOREIGN KEY (id_keluarga) REFERENCES keluarga(id_keluarga) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await db.execute(`
CREATE TABLE IF NOT EXISTS bantuan (
id_bantuan INT AUTO_INCREMENT PRIMARY KEY,
jenis_bantuan VARCHAR(120) NOT NULL,
tanggal_bantuan DATE NOT NULL,
nilai_bantuan DECIMAL(14,2) NOT NULL DEFAULT 0,
keterangan TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await db.execute(`
CREATE TABLE IF NOT EXISTS riwayat_bantuan (
id_riwayat INT AUTO_INCREMENT PRIMARY KEY,
id_keluarga INT NOT NULL,
id_bantuan INT NOT NULL,
tanggal_diterima DATE NOT NULL,
jumlah_bantuan DECIMAL(14,2) NOT NULL DEFAULT 0,
keterangan TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_riwayat_keluarga FOREIGN KEY (id_keluarga) REFERENCES keluarga(id_keluarga) ON DELETE CASCADE,
CONSTRAINT fk_riwayat_bantuan FOREIGN KEY (id_bantuan) REFERENCES bantuan(id_bantuan) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await db.execute(`
CREATE TABLE IF NOT EXISTS pengajuan_bantuan (
id_pengajuan INT AUTO_INCREMENT PRIMARY KEY,
nama_pemohon VARCHAR(160) NOT NULL,
nik VARCHAR(20) NOT NULL,
alamat TEXT NOT NULL,
wilayah VARCHAR(120) NOT NULL DEFAULT 'Belum Dipetakan',
alasan TEXT NOT NULL,
status ENUM('menunggu','diverifikasi','disetujui','ditolak','selesai') NOT NULL DEFAULT 'menunggu',
catatan_admin TEXT,
id_keluarga INT,
tanggal_pengajuan DATE NOT NULL,
tanggal_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_pengajuan_keluarga FOREIGN KEY (id_keluarga) REFERENCES keluarga(id_keluarga) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await seedDefaults();
await calculateSaw();
tablesReady = true;
}
async function tableColumns(tableName) {
const [rows] = await db.execute(`SHOW COLUMNS FROM ${tableName}`);
return rows.map((row) => row.Field);
}
async function hasTable(tableName) {
const [rows] = await db.execute(
"SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?",
[tableName],
);
return rows.length > 0;
}
async function addColumnIfMissing(tableName, columns, columnName, definition) {
if (columns.includes(columnName)) return columns;
await db.execute(`ALTER TABLE ${tableName} ADD COLUMN ${definition}`);
return [...columns, columnName];
}
async function migrateLegacyKeluarga() {
if (!(await hasTable("keluarga"))) return;
let columns = await tableColumns("keluarga");
if (!columns.includes("id_keluarga") && columns.includes("id")) {
await db.execute("ALTER TABLE keluarga CHANGE id id_keluarga INT NOT NULL AUTO_INCREMENT");
columns = columns.map((column) => (column === "id" ? "id_keluarga" : column));
}
if (!columns.includes("id_keluarga")) return;
columns = await addColumnIfMissing("keluarga", columns, "nomor_kartu_keluarga", "nomor_kartu_keluarga VARCHAR(40) NULL");
columns = await addColumnIfMissing("keluarga", columns, "nama_kepala_keluarga", "nama_kepala_keluarga VARCHAR(160) NULL");
columns = await addColumnIfMissing("keluarga", columns, "wilayah", "wilayah VARCHAR(120) NOT NULL DEFAULT 'Belum Dipetakan'");
columns = await addColumnIfMissing("keluarga", columns, "latitude", "latitude DOUBLE NULL");
columns = await addColumnIfMissing("keluarga", columns, "longitude", "longitude DOUBLE NULL");
columns = await addColumnIfMissing("keluarga", columns, "jumlah_tanggungan", "jumlah_tanggungan INT NOT NULL DEFAULT 0");
columns = await addColumnIfMissing("keluarga", columns, "pendapatan_bulanan", "pendapatan_bulanan DECIMAL(14,2) NOT NULL DEFAULT 0");
columns = await addColumnIfMissing("keluarga", columns, "pekerjaan", "pekerjaan VARCHAR(60) NOT NULL DEFAULT 'lainnya'");
columns = await addColumnIfMissing("keluarga", columns, "pendidikan", "pendidikan VARCHAR(60) NOT NULL DEFAULT 'sma'");
columns = await addColumnIfMissing("keluarga", columns, "kondisi_kesehatan", "kondisi_kesehatan VARCHAR(60) NOT NULL DEFAULT 'sehat'");
columns = await addColumnIfMissing("keluarga", columns, "status_verifikasi", "status_verifikasi ENUM('pending','terverifikasi','ditolak') NOT NULL DEFAULT 'terverifikasi'");
columns = await addColumnIfMissing("keluarga", columns, "tanggal_update", "tanggal_update DATE NULL");
columns = await addColumnIfMissing("keluarga", columns, "aktif", "aktif TINYINT(1) NOT NULL DEFAULT 1");
if (columns.includes("kondisi_rumah")) {
await db.execute("ALTER TABLE keluarga MODIFY kondisi_rumah VARCHAR(60) NOT NULL DEFAULT 'sederhana'");
} else {
await db.execute("ALTER TABLE keluarga ADD COLUMN kondisi_rumah VARCHAR(60) NOT NULL DEFAULT 'sederhana'");
columns.push("kondisi_rumah");
}
const refreshed = await tableColumns("keluarga");
if (refreshed.includes("nama_kk")) {
await db.execute("UPDATE keluarga SET nama_kepala_keluarga = COALESCE(nama_kepala_keluarga, nama_kk)");
}
if (refreshed.includes("lat")) {
await db.execute("UPDATE keluarga SET latitude = COALESCE(latitude, lat)");
}
if (refreshed.includes("lng")) {
await db.execute("UPDATE keluarga SET longitude = COALESCE(longitude, lng)");
}
if (refreshed.includes("penghasilan")) {
await db.execute("UPDATE keluarga SET pendapatan_bulanan = IF(pendapatan_bulanan = 0, penghasilan, pendapatan_bulanan)");
}
await db.execute("UPDATE keluarga SET nomor_kartu_keluarga = COALESCE(nomor_kartu_keluarga, CONCAT('LEGACY-', LPAD(id_keluarga, 6, '0')))");
await db.execute("UPDATE keluarga SET tanggal_update = COALESCE(tanggal_update, CURDATE())");
await db.execute("UPDATE keluarga SET jumlah_tanggungan = IF(jumlah_tanggungan = 0, GREATEST(jumlah_anggota - 1, 0), jumlah_tanggungan)");
await db.execute(`
UPDATE keluarga
SET kondisi_rumah = CASE kondisi_rumah
WHEN 'Tidak Permanen' THEN 'sangat_buruk'
WHEN 'Semi Permanen' THEN 'semi_permanen'
WHEN 'Permanen' THEN 'layak'
ELSE kondisi_rumah
END
`);
try {
await db.execute("ALTER TABLE keluarga ADD UNIQUE KEY uniq_keluarga_nomor_kk (nomor_kartu_keluarga)");
} catch (error) {
if (!["ER_DUP_KEYNAME", "ER_DUP_ENTRY"].includes(error.code)) throw error;
}
}
async function seedDefaults() {
await db.execute(
"INSERT IGNORE INTO users (nama, username, password, role) VALUES (?, ?, ?, ?), (?, ?, ?, ?)",
["Administrator", "admin", "admin123", "admin", "Pengurus Gereja", "pengurus", "pengurus123", "pengurus"],
);
for (const item of DEFAULT_KRITERIA) {
await db.execute(
`INSERT INTO kriteria (kode, nama_kriteria, bobot, atribut)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE nama_kriteria=VALUES(nama_kriteria), bobot=VALUES(bobot), atribut=VALUES(atribut), aktif=1`,
[item.kode, item.nama_kriteria, item.bobot, item.atribut],
);
}
const [[{ count }]] = await db.execute("SELECT COUNT(*) AS count FROM keluarga");
if (Number(count) > 0) return;
const today = new Date().toISOString().slice(0, 10);
const families = [
["6171010101010001", "Maria Goretti", "Jl. Gajah Mada Gang Damai", "Paroki Katedral", -0.0339, 109.3367, 5, 4, 1250000, "buruh_harian", "smp", "buruk", "sakit_sering", "terverifikasi", today],
["6171010101010002", "Yohanes Rudi", "Jl. Tanjung Raya II", "Paroki Santo Yosef", -0.0278, 109.3695, 6, 5, 900000, "pengangguran", "sd", "sangat_buruk", "kronis", "terverifikasi", today],
["6171010101010003", "Agnes Lestari", "Jl. Husein Hamzah", "Paroki Santo Hieronymus", -0.0252, 109.3062, 4, 3, 1750000, "pedagang_kecil", "sma", "semi_permanen", "sehat", "terverifikasi", today],
["6171010101010004", "Petrus Antoni", "Jl. Imam Bonjol", "Paroki Keluarga Kudus", -0.0266, 109.3444, 3, 2, 2800000, "karyawan", "sma", "sederhana", "sehat", "terverifikasi", today],
["6171010101010005", "Theresia Ani", "Jl. Khatulistiwa", "Paroki Santo Agustinus", 0.0023, 109.3237, 7, 6, 1150000, "buruh_harian", "sd", "buruk", "disabilitas", "terverifikasi", today],
["6171010101010006", "Fransiskus Deni", "Jl. Dr. Wahidin", "Paroki Santo Paulus", -0.0364, 109.3151, 2, 1, 4200000, "wiraswasta", "diploma", "layak", "sehat", "terverifikasi", today],
];
for (const row of families) {
await db.execute(
`INSERT INTO keluarga
(nomor_kartu_keluarga, nama_kepala_keluarga, alamat, wilayah, latitude, longitude,
jumlah_anggota, jumlah_tanggungan, pendapatan_bulanan, pekerjaan, pendidikan,
kondisi_rumah, kondisi_kesehatan, status_verifikasi, tanggal_update)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
row,
);
}
const [bantuanResult] = await db.execute(
"INSERT INTO bantuan (jenis_bantuan, tanggal_bantuan, nilai_bantuan, keterangan) VALUES (?, ?, ?, ?), (?, ?, ?, ?)",
["Paket Sembako", today, 350000, "Bantuan pangan bulanan", "Bantuan Pendidikan", today, 500000, "Dukungan biaya sekolah"],
);
await db.execute(
"INSERT INTO riwayat_bantuan (id_keluarga, id_bantuan, tanggal_diterima, jumlah_bantuan, keterangan) VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)",
[1, bantuanResult.insertId, today, 350000, "Tahap awal", 2, bantuanResult.insertId + 1, today, 500000, "Untuk anak sekolah"],
);
}
async function calculateSaw() {
const [families] = await db.execute("SELECT * FROM keluarga WHERE aktif = 1");
const [criteria] = await db.execute("SELECT * FROM kriteria WHERE aktif = 1 ORDER BY id_kriteria ASC");
if (!families.length || !criteria.length) return [];
const matrix = families.map((family) => {
const values = {};
criteria.forEach((criterion) => {
values[criterion.kode] = rawCriterionValue(family, criterion.kode);
});
return { family, values };
});
const minMax = {};
criteria.forEach((criterion) => {
const values = matrix.map((row) => toNumber(row.values[criterion.kode], 0)).filter((value) => value > 0);
minMax[criterion.kode] = {
min: values.length ? Math.min(...values) : 1,
max: values.length ? Math.max(...values) : 1,
};
});
const results = [];
for (const row of matrix) {
let score = 0;
for (const criterion of criteria) {
const rawValue = Math.max(toNumber(row.values[criterion.kode], 0), 0);
const normalizer = minMax[criterion.kode];
const normalized =
criterion.atribut === "cost"
? normalizer.min / Math.max(rawValue, 1)
: rawValue / Math.max(normalizer.max, 1);
const weighted = normalized * toNumber(criterion.bobot, 0);
score += weighted;
await db.execute(
`INSERT INTO penilaian (id_keluarga, id_kriteria, nilai, nilai_normalisasi, nilai_terbobot)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE nilai=VALUES(nilai), nilai_normalisasi=VALUES(nilai_normalisasi), nilai_terbobot=VALUES(nilai_terbobot)`,
[row.family.id_keluarga, criterion.id_kriteria, rawValue, normalized, weighted],
);
}
results.push({
id_keluarga: row.family.id_keluarga,
skor_saw: Number(score.toFixed(6)),
kategori: sawCategory(score),
});
}
results.sort((a, b) => b.skor_saw - a.skor_saw);
for (const [index, result] of results.entries()) {
await db.execute(
`INSERT INTO hasil_saw (id_keluarga, skor_saw, kategori, ranking)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE skor_saw=VALUES(skor_saw), kategori=VALUES(kategori), ranking=VALUES(ranking)`,
[result.id_keluarga, result.skor_saw, result.kategori, index + 1],
);
result.ranking = index + 1;
}
return results;
}
async function rankedFamilies() {
await calculateSaw();
const [rows] = await db.execute(`
SELECT k.*, h.skor_saw, h.kategori, h.ranking,
COALESCE(SUM(rb.jumlah_bantuan), 0) AS total_bantuan,
COUNT(rb.id_riwayat) AS jumlah_riwayat
FROM keluarga k
LEFT JOIN hasil_saw h ON h.id_keluarga = k.id_keluarga
LEFT JOIN riwayat_bantuan rb ON rb.id_keluarga = k.id_keluarga
WHERE k.aktif = 1
GROUP BY k.id_keluarga, h.skor_saw, h.kategori, h.ranking
ORDER BY h.ranking ASC, k.nama_kepala_keluarga ASC
`);
return rows;
}
async function familyAidHistory(id) {
const [rows] = await db.execute(
`SELECT rb.*, b.jenis_bantuan, b.nilai_bantuan
FROM riwayat_bantuan rb
JOIN bantuan b ON b.id_bantuan = rb.id_bantuan
WHERE rb.id_keluarga = ?
ORDER BY rb.tanggal_diterima DESC, rb.id_riwayat DESC`,
[id],
);
return rows;
}
function validateFamily(body) {
const required = [
"nomor_kartu_keluarga",
"nama_kepala_keluarga",
"alamat",
"latitude",
"longitude",
"jumlah_anggota",
"jumlah_tanggungan",
"pendapatan_bulanan",
"pekerjaan",
"pendidikan",
"kondisi_rumah",
"kondisi_kesehatan",
"status_verifikasi",
"tanggal_update",
];
for (const key of required) {
if (body[key] === undefined || body[key] === null || body[key] === "") {
return `${key} wajib diisi.`;
}
}
if (!Number.isFinite(Number(body.latitude)) || !Number.isFinite(Number(body.longitude))) {
return "Latitude dan longitude wajib valid.";
}
return null;
}
function validateIbadah(body) {
const required = ["nama", "jenis", "alamat", "kelurahan", "lat", "lng"];
for (const key of required) {
if (body[key] === undefined || body[key] === null || body[key] === "") {
return `${key} wajib diisi.`;
}
}
if (!TEMPAT_IBADAH_TYPES.includes(body.jenis)) {
return "Jenis tempat ibadah wajib valid.";
}
if (!Number.isFinite(Number(body.lat)) || !Number.isFinite(Number(body.lng))) {
return "Latitude dan longitude wajib valid.";
}
if (body.kapasitas_bantuan !== undefined && !Number.isFinite(Number(body.kapasitas_bantuan))) {
return "Kapasitas bantuan harus berupa angka.";
}
return null;
}
function validatePengajuan(body) {
const required = ["nama_pemohon", "nik", "alamat", "alasan", "tanggal_pengajuan"];
for (const key of required) {
if (!body[key] || String(body[key]).trim() === "") {
return `${key} wajib diisi.`;
}
}
return null;
}
// Haversine distance in meters
function haversineMeters(lat1, lng1, lat2, lng2) {
const R = 6371000;
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLng = ((lng2 - lng1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLng / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
router.use(async (req, res, next) => {
try {
await ensureTables();
next();
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.get("/dashboard", async (req, res) => {
try {
const rows = await rankedFamilies();
const [aidRows] = await db.execute("SELECT COALESCE(SUM(jumlah_bantuan), 0) AS total FROM riwayat_bantuan");
const [criteriaRows] = await db.execute("SELECT nama_kriteria, bobot, atribut FROM kriteria WHERE aktif = 1 ORDER BY id_kriteria");
const categoryStats = rows.reduce((acc, item) => {
acc[item.kategori] = (acc[item.kategori] || 0) + 1;
return acc;
}, {});
const wilayahStats = rows.reduce((acc, item) => {
acc[item.wilayah] = (acc[item.wilayah] || 0) + 1;
return acc;
}, {});
res.json({
success: true,
data: {
total_keluarga: rows.length,
prioritas_tinggi: rows.filter((item) => item.kategori === "Sangat Membutuhkan").length,
total_bantuan_tersalurkan: Number(aidRows[0]?.total || 0),
kategori_ekonomi: categoryStats,
wilayah: wilayahStats,
kriteria: criteriaRows,
ranking_top10: rows.slice(0, 10),
},
});
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.get("/analytics", async (req, res) => {
try {
const rows = await rankedFamilies();
const [aidByMonth] = await db.execute(`
SELECT DATE_FORMAT(tanggal_diterima, '%Y-%m') AS bulan, SUM(jumlah_bantuan) AS total
FROM riwayat_bantuan
GROUP BY DATE_FORMAT(tanggal_diterima, '%Y-%m')
ORDER BY bulan ASC
`);
const [trendRows] = await db.execute(`
SELECT DATE_FORMAT(tanggal_update, '%Y-%m') AS bulan, COUNT(*) AS total, AVG(h.skor_saw) AS rata_skor
FROM keluarga k
LEFT JOIN hasil_saw h ON h.id_keluarga = k.id_keluarga
WHERE k.aktif = 1
GROUP BY DATE_FORMAT(tanggal_update, '%Y-%m')
ORDER BY bulan ASC
`);
res.json({
success: true,
data: {
kategori: rows.reduce((acc, item) => {
acc[item.kategori] = (acc[item.kategori] || 0) + 1;
return acc;
}, {}),
bantuan_bulanan: aidByMonth,
wilayah: rows.reduce((acc, item) => {
acc[item.wilayah] = (acc[item.wilayah] || 0) + 1;
return acc;
}, {}),
tren_ekonomi: trendRows,
top10: rows.slice(0, 10),
},
});
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.get("/map", async (req, res) => {
try {
const role = req.query.role === "pengurus" ? "pengurus" : "admin";
const privacy = req.query.privacy === "1" || req.query.privacy === "true";
const kategori = req.query.kategori || "";
const search = String(req.query.search || "").toLowerCase();
const rows = await rankedFamilies();
const filtered = rows
.filter((item) => !kategori || item.kategori === kategori)
.filter((item) => !search || item.nama_kepala_keluarga.toLowerCase().includes(search) || item.alamat.toLowerCase().includes(search))
.map((item) => applyPrivacy(item, role, privacy));
const withHistory = [];
for (const item of filtered) {
withHistory.push({ ...item, riwayat_bantuan: await familyAidHistory(item.id_keluarga) });
}
res.json({
success: true,
data: withHistory,
geojson: {
type: "FeatureCollection",
features: withHistory.map(buildFeature),
},
});
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.get("/ranking", async (req, res) => {
try {
res.json({ success: true, data: await rankedFamilies() });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.post("/recalculate", async (req, res) => {
try {
res.json({ success: true, message: "Perhitungan SAW berhasil diperbarui.", data: await calculateSaw() });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.get("/users", async (req, res) => {
const [rows] = await db.execute("SELECT id_user, nama, username, role, aktif, created_at FROM users ORDER BY nama ASC");
res.json({ success: true, data: rows });
});
router.post("/users", async (req, res) => {
const { nama, username, password, role } = req.body;
if (!nama || !username || !password || !["admin", "pengurus"].includes(role)) {
return res.status(400).json({ success: false, message: "Nama, username, password, dan role wajib valid." });
}
try {
const [result] = await db.execute("INSERT INTO users (nama, username, password, role) VALUES (?, ?, ?, ?)", [nama, username, password, role]);
res.status(201).json({ success: true, id: result.insertId, message: "User berhasil ditambahkan." });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.put("/users/:id", async (req, res) => {
const { nama, username, password, role, aktif } = req.body;
if (!nama || !username || !["admin", "pengurus"].includes(role)) {
return res.status(400).json({ success: false, message: "Nama, username, dan role wajib valid." });
}
const params = password ? [nama, username, password, role, aktif ? 1 : 0, req.params.id] : [nama, username, role, aktif ? 1 : 0, req.params.id];
const sql = password
? "UPDATE users SET nama=?, username=?, password=?, role=?, aktif=? WHERE id_user=?"
: "UPDATE users SET nama=?, username=?, role=?, aktif=? WHERE id_user=?";
const [result] = await db.execute(sql, params);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "User tidak ditemukan." });
res.json({ success: true, message: "User berhasil diperbarui." });
});
router.delete("/users/:id", async (req, res) => {
const [result] = await db.execute("UPDATE users SET aktif = 0 WHERE id_user = ?", [req.params.id]);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "User tidak ditemukan." });
res.json({ success: true, message: "User berhasil dinonaktifkan." });
});
router.get("/keluarga", async (req, res) => {
try {
res.json({ success: true, data: await rankedFamilies() });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.get("/keluarga/:id", async (req, res) => {
const rows = await rankedFamilies();
const item = rows.find((row) => String(row.id_keluarga) === String(req.params.id));
if (!item) return res.status(404).json({ success: false, message: "Keluarga tidak ditemukan." });
res.json({ success: true, data: { ...item, riwayat_bantuan: await familyAidHistory(item.id_keluarga) } });
});
router.post("/keluarga", async (req, res) => {
const error = validateFamily(req.body);
if (error) return res.status(400).json({ success: false, message: error });
const body = req.body;
try {
const [result] = await db.execute(
`INSERT INTO keluarga
(nomor_kartu_keluarga, nama_kepala_keluarga, alamat, wilayah, latitude, longitude,
jumlah_anggota, jumlah_tanggungan, pendapatan_bulanan, pekerjaan, pendidikan,
kondisi_rumah, kondisi_kesehatan, status_verifikasi, tanggal_update)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
body.nomor_kartu_keluarga,
body.nama_kepala_keluarga,
body.alamat,
body.wilayah || "Belum Dipetakan",
body.latitude,
body.longitude,
body.jumlah_anggota,
body.jumlah_tanggungan,
body.pendapatan_bulanan,
body.pekerjaan,
body.pendidikan,
body.kondisi_rumah,
body.kondisi_kesehatan,
body.status_verifikasi,
body.tanggal_update,
],
);
await calculateSaw();
res.status(201).json({ success: true, id: result.insertId, message: "Data keluarga berhasil ditambahkan." });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
router.put("/keluarga/:id", async (req, res) => {
const error = validateFamily(req.body);
if (error) return res.status(400).json({ success: false, message: error });
const body = req.body;
const [result] = await db.execute(
`UPDATE keluarga SET
nomor_kartu_keluarga=?, nama_kepala_keluarga=?, alamat=?, wilayah=?, latitude=?, longitude=?,
jumlah_anggota=?, jumlah_tanggungan=?, pendapatan_bulanan=?, pekerjaan=?, pendidikan=?,
kondisi_rumah=?, kondisi_kesehatan=?, status_verifikasi=?, tanggal_update=?
WHERE id_keluarga=?`,
[
body.nomor_kartu_keluarga,
body.nama_kepala_keluarga,
body.alamat,
body.wilayah || "Belum Dipetakan",
body.latitude,
body.longitude,
body.jumlah_anggota,
body.jumlah_tanggungan,
body.pendapatan_bulanan,
body.pekerjaan,
body.pendidikan,
body.kondisi_rumah,
body.kondisi_kesehatan,
body.status_verifikasi,
body.tanggal_update,
req.params.id,
],
);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Keluarga tidak ditemukan." });
await calculateSaw();
res.json({ success: true, message: "Data keluarga berhasil diperbarui." });
});
router.delete("/keluarga/:id", async (req, res) => {
const [result] = await db.execute("UPDATE keluarga SET aktif = 0 WHERE id_keluarga = ?", [req.params.id]);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Keluarga tidak ditemukan." });
await calculateSaw();
res.json({ success: true, message: "Data keluarga berhasil dihapus." });
});
router.get("/kriteria", async (req, res) => {
const [rows] = await db.execute("SELECT * FROM kriteria ORDER BY id_kriteria ASC");
res.json({ success: true, data: rows });
});
router.post("/kriteria", async (req, res) => {
const { kode, nama_kriteria, bobot, atribut } = req.body;
if (!kode || !nama_kriteria || !["benefit", "cost"].includes(atribut)) {
return res.status(400).json({ success: false, message: "Kode, nama kriteria, dan atribut wajib valid." });
}
const [result] = await db.execute(
"INSERT INTO kriteria (kode, nama_kriteria, bobot, atribut) VALUES (?, ?, ?, ?)",
[kode, nama_kriteria, toNumber(bobot), atribut],
);
await calculateSaw();
res.status(201).json({ success: true, id: result.insertId, message: "Kriteria berhasil ditambahkan." });
});
router.put("/kriteria/:id", async (req, res) => {
const { kode, nama_kriteria, bobot, atribut, aktif } = req.body;
if (!kode || !nama_kriteria || !["benefit", "cost"].includes(atribut)) {
return res.status(400).json({ success: false, message: "Kode, nama kriteria, dan atribut wajib valid." });
}
const [result] = await db.execute(
"UPDATE kriteria SET kode=?, nama_kriteria=?, bobot=?, atribut=?, aktif=? WHERE id_kriteria=?",
[kode, nama_kriteria, toNumber(bobot), atribut, aktif === false ? 0 : 1, req.params.id],
);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Kriteria tidak ditemukan." });
await calculateSaw();
res.json({ success: true, message: "Kriteria berhasil diperbarui." });
});
router.delete("/kriteria/:id", async (req, res) => {
const [result] = await db.execute("UPDATE kriteria SET aktif = 0 WHERE id_kriteria = ?", [req.params.id]);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Kriteria tidak ditemukan." });
await calculateSaw();
res.json({ success: true, message: "Kriteria berhasil dinonaktifkan." });
});
router.get("/penilaian", async (req, res) => {
const [rows] = await db.execute(`
SELECT p.*, k.nama_kriteria, k.kode, kl.nama_kepala_keluarga
FROM penilaian p
JOIN kriteria k ON k.id_kriteria = p.id_kriteria
JOIN keluarga kl ON kl.id_keluarga = p.id_keluarga
ORDER BY kl.nama_kepala_keluarga ASC, k.id_kriteria ASC
`);
res.json({ success: true, data: rows });
});
router.post("/penilaian", async (req, res) => {
const { id_keluarga, id_kriteria, nilai } = req.body;
if (!id_keluarga || !id_kriteria) return res.status(400).json({ success: false, message: "Keluarga dan kriteria wajib dipilih." });
const [result] = await db.execute(
`INSERT INTO penilaian (id_keluarga, id_kriteria, nilai)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE nilai=VALUES(nilai)`,
[id_keluarga, id_kriteria, toNumber(nilai)],
);
res.status(201).json({ success: true, id: result.insertId, message: "Penilaian berhasil disimpan." });
});
router.put("/penilaian/:id", async (req, res) => {
const { id_keluarga, id_kriteria, nilai } = req.body;
const [result] = await db.execute(
"UPDATE penilaian SET id_keluarga=?, id_kriteria=?, nilai=? WHERE id_penilaian=?",
[id_keluarga, id_kriteria, toNumber(nilai), req.params.id],
);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Penilaian tidak ditemukan." });
res.json({ success: true, message: "Penilaian berhasil diperbarui." });
});
router.delete("/penilaian/:id", async (req, res) => {
const [result] = await db.execute("DELETE FROM penilaian WHERE id_penilaian = ?", [req.params.id]);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Penilaian tidak ditemukan." });
res.json({ success: true, message: "Penilaian berhasil dihapus." });
});
router.get("/hasil-saw", async (req, res) => {
const rows = await rankedFamilies();
res.json({ success: true, data: rows.map((item) => ({
id_keluarga: item.id_keluarga,
nama_kepala_keluarga: item.nama_kepala_keluarga,
skor_saw: item.skor_saw,
kategori: item.kategori,
ranking: item.ranking,
})) });
});
router.get("/bantuan", async (req, res) => {
const [rows] = await db.execute("SELECT * FROM bantuan ORDER BY tanggal_bantuan DESC, id_bantuan DESC");
res.json({ success: true, data: rows });
});
router.post("/bantuan", async (req, res) => {
const { jenis_bantuan, tanggal_bantuan, nilai_bantuan, keterangan } = req.body;
if (!jenis_bantuan || !tanggal_bantuan) {
return res.status(400).json({ success: false, message: "Jenis dan tanggal bantuan wajib diisi." });
}
const [result] = await db.execute(
"INSERT INTO bantuan (jenis_bantuan, tanggal_bantuan, nilai_bantuan, keterangan) VALUES (?, ?, ?, ?)",
[jenis_bantuan, tanggal_bantuan, toNumber(nilai_bantuan), keterangan || null],
);
res.status(201).json({ success: true, id: result.insertId, message: "Bantuan berhasil ditambahkan." });
});
router.put("/bantuan/:id", async (req, res) => {
const { jenis_bantuan, tanggal_bantuan, nilai_bantuan, keterangan } = req.body;
const [result] = await db.execute(
"UPDATE bantuan SET jenis_bantuan=?, tanggal_bantuan=?, nilai_bantuan=?, keterangan=? WHERE id_bantuan=?",
[jenis_bantuan, tanggal_bantuan, toNumber(nilai_bantuan), keterangan || null, req.params.id],
);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Bantuan tidak ditemukan." });
res.json({ success: true, message: "Bantuan berhasil diperbarui." });
});
router.delete("/bantuan/:id", async (req, res) => {
const [result] = await db.execute("DELETE FROM bantuan WHERE id_bantuan = ?", [req.params.id]);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Bantuan tidak ditemukan." });
res.json({ success: true, message: "Bantuan berhasil dihapus." });
});
router.get("/riwayat-bantuan", async (req, res) => {
const [rows] = await db.execute(`
SELECT rb.*, k.nama_kepala_keluarga, b.jenis_bantuan, b.nilai_bantuan
FROM riwayat_bantuan rb
JOIN keluarga k ON k.id_keluarga = rb.id_keluarga
JOIN bantuan b ON b.id_bantuan = rb.id_bantuan
ORDER BY rb.tanggal_diterima DESC, rb.id_riwayat DESC
`);
res.json({ success: true, data: rows });
});
router.post("/riwayat-bantuan", async (req, res) => {
const { id_keluarga, id_bantuan, tanggal_diterima, jumlah_bantuan, keterangan } = req.body;
if (!id_keluarga || !id_bantuan || !tanggal_diterima) {
return res.status(400).json({ success: false, message: "Penerima, bantuan, dan tanggal diterima wajib diisi." });
}
const [result] = await db.execute(
"INSERT INTO riwayat_bantuan (id_keluarga, id_bantuan, tanggal_diterima, jumlah_bantuan, keterangan) VALUES (?, ?, ?, ?, ?)",
[id_keluarga, id_bantuan, tanggal_diterima, toNumber(jumlah_bantuan), keterangan || null],
);
res.status(201).json({ success: true, id: result.insertId, message: "Riwayat bantuan berhasil ditambahkan." });
});
router.put("/riwayat-bantuan/:id", async (req, res) => {
const { id_keluarga, id_bantuan, tanggal_diterima, jumlah_bantuan, keterangan } = req.body;
const [result] = await db.execute(
"UPDATE riwayat_bantuan SET id_keluarga=?, id_bantuan=?, tanggal_diterima=?, jumlah_bantuan=?, keterangan=? WHERE id_riwayat=?",
[id_keluarga, id_bantuan, tanggal_diterima, toNumber(jumlah_bantuan), keterangan || null, req.params.id],
);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Riwayat bantuan tidak ditemukan." });
res.json({ success: true, message: "Riwayat bantuan berhasil diperbarui." });
});
router.delete("/riwayat-bantuan/:id", async (req, res) => {
const [result] = await db.execute("DELETE FROM riwayat_bantuan WHERE id_riwayat = ?", [req.params.id]);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Riwayat bantuan tidak ditemukan." });
res.json({ success: true, message: "Riwayat bantuan berhasil dihapus." });
});
router.get("/ibadah", async (req, res) => {
try {
const [rows] = await db.execute("SELECT * FROM tempat_ibadah WHERE aktif = 1 ORDER BY nama ASC");
res.json({ success: true, data: rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.get("/ibadah/:id", async (req, res) => {
try {
const [rows] = await db.execute("SELECT * FROM tempat_ibadah WHERE id = ? AND aktif = 1", [req.params.id]);
if (!rows.length) return res.status(404).json({ success: false, message: "Tempat ibadah tidak ditemukan." });
res.json({ success: true, data: rows[0] });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.post("/ibadah", async (req, res) => {
const error = validateIbadah(req.body);
if (error) return res.status(400).json({ success: false, message: error });
const body = req.body;
try {
const [result] = await db.execute(
"INSERT INTO tempat_ibadah (nama, jenis, alamat, kelurahan, lat, lng, kapasitas_bantuan, keterangan) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
[body.nama, body.jenis, body.alamat, body.kelurahan, Number(body.lat), Number(body.lng), toNumber(body.kapasitas_bantuan), body.keterangan || null],
);
res.status(201).json({ success: true, id: result.insertId, message: "Tempat ibadah berhasil ditambahkan." });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
router.put("/ibadah/:id", async (req, res) => {
const error = validateIbadah(req.body);
if (error) return res.status(400).json({ success: false, message: error });
const body = req.body;
try {
const [result] = await db.execute(
"UPDATE tempat_ibadah SET nama=?, jenis=?, alamat=?, kelurahan=?, lat=?, lng=?, kapasitas_bantuan=?, keterangan=? WHERE id=? AND aktif = 1",
[body.nama, body.jenis, body.alamat, body.kelurahan, Number(body.lat), Number(body.lng), toNumber(body.kapasitas_bantuan), body.keterangan || null, req.params.id],
);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Tempat ibadah tidak ditemukan." });
res.json({ success: true, message: "Tempat ibadah berhasil diperbarui." });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// ─── Nearest Church ─────────────────────────────────────────────────────────
router.get("/nearest-church", async (req, res) => {
try {
const lat = Number(req.query.lat);
const lng = Number(req.query.lng);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
return res.status(400).json({ success: false, message: "lat dan lng wajib valid." });
}
const [rows] = await db.execute("SELECT * FROM tempat_ibadah WHERE aktif = 1 ORDER BY nama ASC");
if (!rows.length) return res.json({ success: true, data: null });
let nearest = null;
let minDist = Infinity;
for (const item of rows) {
const dist = haversineMeters(lat, lng, Number(item.lat), Number(item.lng));
if (dist < minDist) {
minDist = dist;
nearest = { ...item, jarak_meter: Math.round(dist) };
}
}
// Also return all churches sorted by distance
const sorted = rows
.map((item) => ({ ...item, jarak_meter: Math.round(haversineMeters(lat, lng, Number(item.lat), Number(item.lng))) }))
.sort((a, b) => a.jarak_meter - b.jarak_meter);
res.json({ success: true, data: { nearest, all: sorted } });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// ─── Pengajuan Bantuan ───────────────────────────────────────────────────────
router.get("/pengajuan", async (req, res) => {
try {
const status = req.query.status || "";
const sql = status
? "SELECT * FROM pengajuan_bantuan WHERE status = ? ORDER BY created_at DESC"
: "SELECT * FROM pengajuan_bantuan ORDER BY created_at DESC";
const params = status ? [status] : [];
const [rows] = await db.execute(sql, params);
res.json({ success: true, data: rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.get("/pengajuan/:id", async (req, res) => {
try {
const [rows] = await db.execute("SELECT * FROM pengajuan_bantuan WHERE id_pengajuan = ?", [req.params.id]);
if (!rows.length) return res.status(404).json({ success: false, message: "Pengajuan tidak ditemukan." });
res.json({ success: true, data: rows[0] });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.post("/pengajuan", async (req, res) => {
const error = validatePengajuan(req.body);
if (error) return res.status(400).json({ success: false, message: error });
const body = req.body;
try {
const [result] = await db.execute(
`INSERT INTO pengajuan_bantuan (nama_pemohon, nik, alamat, wilayah, alasan, tanggal_pengajuan)
VALUES (?, ?, ?, ?, ?, ?)`,
[body.nama_pemohon.trim(), body.nik.trim(), body.alamat.trim(), body.wilayah || "Belum Dipetakan", body.alasan.trim(), body.tanggal_pengajuan],
);
res.status(201).json({ success: true, id: result.insertId, message: "Pengajuan berhasil dikirim. Kami akan segera menghubungi Anda." });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
router.put("/pengajuan/:id/status", async (req, res) => {
const { status, catatan_admin, id_keluarga } = req.body;
const validStatuses = ["menunggu", "diverifikasi", "disetujui", "ditolak", "selesai"];
if (!validStatuses.includes(status)) {
return res.status(400).json({ success: false, message: "Status tidak valid." });
}
try {
const [result] = await db.execute(
"UPDATE pengajuan_bantuan SET status=?, catatan_admin=?, id_keluarga=? WHERE id_pengajuan=?",
[status, catatan_admin || null, id_keluarga || null, req.params.id],
);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Pengajuan tidak ditemukan." });
res.json({ success: true, message: `Status pengajuan diperbarui menjadi '${status}'.` });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.delete("/pengajuan/:id", async (req, res) => {
try {
const [result] = await db.execute("DELETE FROM pengajuan_bantuan WHERE id_pengajuan = ?", [req.params.id]);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Pengajuan tidak ditemukan." });
res.json({ success: true, message: "Pengajuan berhasil dihapus." });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.delete("/ibadah/:id", async (req, res) => {
try {
const [result] = await db.execute("UPDATE tempat_ibadah SET aktif = 0 WHERE id = ?", [req.params.id]);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Tempat ibadah tidak ditemukan." });
res.json({ success: true, message: "Tempat ibadah berhasil dihapus." });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
module.exports = router;