Files
WEBGIS_Project/backend/routes/poverty.js
T
2026-06-11 13:04:28 +07:00

1039 lines
41 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,
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");
}
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 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.";
}
return null;
}
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) VALUES (?, ?, ?, ?, ?, ?)",
[body.nama, body.jenis, body.alamat, body.kelurahan, Number(body.lat), Number(body.lng)],
);
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=? WHERE id=? AND aktif = 1",
[body.nama, body.jenis, body.alamat, body.kelurahan, Number(body.lat), Number(body.lng), 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 });
}
});
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;