1187 lines
48 KiB
JavaScript
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;
|