add poverty

This commit is contained in:
tistopandita
2026-06-11 13:56:18 +07:00
parent 7ef7620d2a
commit 5386300498
4 changed files with 1421 additions and 227 deletions
+161 -13
View File
@@ -5,15 +5,17 @@ const TEMPAT_IBADAH_TYPES = ["Gereja Katolik", "GPIB", "GKI", "HKBP", "Pentakost
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
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;
`;
@@ -26,6 +28,12 @@ async function initTempatIbadahTable() {
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'
@@ -291,6 +299,24 @@ async function ensureTables() {
) 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;
@@ -560,9 +586,33 @@ function validateIbadah(body) {
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();
@@ -1000,8 +1050,8 @@ router.post("/ibadah", async (req, res) => {
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)],
"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) {
@@ -1015,8 +1065,8 @@ router.put("/ibadah/:id", async (req, res) => {
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],
"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." });
@@ -1025,6 +1075,104 @@ router.put("/ibadah/:id", async (req, res) => {
}
});
// ─── 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]);