Files
WEBGIS_Project/backend/routes/poverty.js
T
2026-06-10 22:46:14 +07:00

326 lines
12 KiB
JavaScript

const express = require("express");
const db = require("../db");
const router = express.Router();
const JENIS_IBADAH = ["masjid", "gereja", "pura", "vihara", "kelenteng", "lainnya"];
const KATEGORI_KEMISKINAN = ["sangat_miskin", "miskin", "rentan_miskin"];
let tablesReady = false;
function toFeatureCollection(rows) {
return {
type: "FeatureCollection",
features: rows.map((row) => {
const { lat, lng, ...properties } = row;
return {
type: "Feature",
geometry: {
type: "Point",
coordinates: [Number(lng), Number(lat)],
},
properties: {
...properties,
lat: Number(lat),
lng: Number(lng),
},
};
}),
};
}
function validateCoordinate(lat, lng) {
return Number.isFinite(Number(lat)) && Number.isFinite(Number(lng));
}
function escapeCsv(value) {
return `"${String(value ?? "").replace(/"/g, '""')}"`;
}
async function createTableWithFallback(primarySql, fallbackSql) {
try {
await db.execute(primarySql);
} catch (error) {
if (!fallbackSql) throw error;
await db.execute(fallbackSql);
}
}
async function ensureTables() {
if (tablesReady) return;
const tempatIbadahWithGeom = `
CREATE TABLE IF NOT EXISTS tempat_ibadah (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(255) NOT NULL,
jenis ENUM('masjid','gereja','pura','vihara','kelenteng','lainnya') NOT NULL,
alamat TEXT,
kelurahan VARCHAR(100),
lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL,
geom POINT GENERATED ALWAYS AS (ST_SRID(POINT(lng, lat), 4326)) STORED,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
SPATIAL INDEX idx_tempat_ibadah_geom (geom)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`;
const tempatIbadahFallback = `
CREATE TABLE IF NOT EXISTS tempat_ibadah (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(255) NOT NULL,
jenis ENUM('masjid','gereja','pura','vihara','kelenteng','lainnya') NOT NULL,
alamat TEXT,
kelurahan VARCHAR(100),
lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_tempat_ibadah_lat_lng (lat, lng)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`;
const keluargaWithGeom = `
CREATE TABLE IF NOT EXISTS keluarga_miskin (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_kk VARCHAR(255) NOT NULL,
jumlah_anggota INT NOT NULL DEFAULT 1,
kategori ENUM('sangat_miskin','miskin','rentan_miskin') NOT NULL,
alamat TEXT,
rt VARCHAR(5),
rw VARCHAR(5),
kelurahan VARCHAR(100),
keterangan TEXT,
lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL,
geom POINT GENERATED ALWAYS AS (ST_SRID(POINT(lng, lat), 4326)) STORED,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
SPATIAL INDEX idx_keluarga_geom (geom)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`;
const keluargaFallback = `
CREATE TABLE IF NOT EXISTS keluarga_miskin (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_kk VARCHAR(255) NOT NULL,
jumlah_anggota INT NOT NULL DEFAULT 1,
kategori ENUM('sangat_miskin','miskin','rentan_miskin') NOT NULL,
alamat TEXT,
rt VARCHAR(5),
rw VARCHAR(5),
kelurahan VARCHAR(100),
keterangan TEXT,
lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_keluarga_lat_lng (lat, lng)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`;
await createTableWithFallback(tempatIbadahWithGeom, tempatIbadahFallback);
await createTableWithFallback(keluargaWithGeom, keluargaFallback);
await seedPovertyDataIfEmpty();
tablesReady = true;
}
async function seedPovertyDataIfEmpty() {
const [[{ count: tempatCount }]] = await db.execute("SELECT COUNT(*) AS count FROM tempat_ibadah");
if (Number(tempatCount) === 0) {
const rows = [
["Masjid Jami Sultan Syarif Abdurrahman", "masjid", "Jl. Tanjungpura", "Dalam Bugis", -0.0226, 109.3316],
["Gereja Bethel Indonesia", "gereja", "Jl. Gajahmada", "Sungai Bangkong", -0.0334, 109.3253],
["Vihara Bodhisattva", "vihara", "Jl. Diponegoro", "Mariana", -0.0289, 109.3401],
];
for (const row of rows) {
await db.execute(
"INSERT INTO tempat_ibadah (nama, jenis, alamat, kelurahan, lat, lng) VALUES (?, ?, ?, ?, ?, ?)",
row,
);
}
}
const [[{ count: keluargaCount }]] = await db.execute("SELECT COUNT(*) AS count FROM keluarga_miskin");
if (Number(keluargaCount) === 0) {
const rows = [
["Ahmad Fauzi", 5, "sangat_miskin", "Dalam Bugis", -0.0231, 109.3321],
["Siti Rahmah", 3, "miskin", "Dalam Bugis", -0.0219, 109.3308],
["Budi Santoso", 6, "rentan_miskin", "Sungai Bangkong", -0.0341, 109.3261],
["Maria Goretti", 4, "miskin", "Mariana", -0.0295, 109.3412],
["Lim Ah Kow", 2, "sangat_miskin", "Mariana", -0.0501, 109.36],
];
for (const row of rows) {
await db.execute(
"INSERT INTO keluarga_miskin (nama_kk, jumlah_anggota, kategori, kelurahan, lat, lng) VALUES (?, ?, ?, ?, ?, ?)",
row,
);
}
}
}
router.use(async (req, res, next) => {
try {
await ensureTables();
next();
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.get("/tempat-ibadah", async (req, res) => {
try {
const [rows] = await db.execute(
"SELECT id, nama, jenis, alamat, kelurahan, lat, lng, created_at FROM tempat_ibadah ORDER BY nama ASC",
);
res.json(toFeatureCollection(rows));
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.post("/tempat-ibadah", async (req, res) => {
const { nama, jenis, alamat, kelurahan, lat, lng } = req.body;
if (!nama || !JENIS_IBADAH.includes(jenis) || !validateCoordinate(lat, lng)) {
return res.status(400).json({ success: false, message: "Nama, jenis, lat, dan lng wajib valid." });
}
try {
const [result] = await db.execute(
"INSERT INTO tempat_ibadah (nama, jenis, alamat, kelurahan, lat, lng) VALUES (?, ?, ?, ?, ?, ?)",
[nama.trim(), jenis, alamat || null, kelurahan || null, Number(lat), Number(lng)],
);
res.status(201).json({ success: true, id: result.insertId, message: "Tempat ibadah berhasil ditambahkan." });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.put("/tempat-ibadah/:id", async (req, res) => {
const { nama, jenis, alamat, kelurahan, lat, lng } = req.body;
if (!nama || !JENIS_IBADAH.includes(jenis) || !validateCoordinate(lat, lng)) {
return res.status(400).json({ success: false, message: "Nama, jenis, lat, dan lng wajib valid." });
}
try {
const [result] = await db.execute(
"UPDATE tempat_ibadah SET nama=?, jenis=?, alamat=?, kelurahan=?, lat=?, lng=? WHERE id=?",
[nama.trim(), jenis, alamat || null, kelurahan || null, Number(lat), Number(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 (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.delete("/tempat-ibadah/:id", async (req, res) => {
try {
const [result] = await db.execute("DELETE FROM tempat_ibadah 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 });
}
});
router.get("/keluarga", async (req, res) => {
try {
const [rows] = await db.execute(
`SELECT id, nama_kk, jumlah_anggota, kategori, alamat, rt, rw, kelurahan, keterangan, lat, lng, created_at, updated_at
FROM keluarga_miskin
ORDER BY nama_kk ASC`,
);
res.json(toFeatureCollection(rows));
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.post("/keluarga", async (req, res) => {
const { nama_kk, jumlah_anggota, kategori, alamat, rt, rw, kelurahan, keterangan, lat, lng } = req.body;
if (!nama_kk || !KATEGORI_KEMISKINAN.includes(kategori) || !validateCoordinate(lat, lng)) {
return res.status(400).json({ success: false, message: "Nama KK, kategori, lat, dan lng wajib valid." });
}
try {
const [result] = await db.execute(
`INSERT INTO keluarga_miskin
(nama_kk, jumlah_anggota, kategori, alamat, rt, rw, kelurahan, keterangan, lat, lng)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[nama_kk.trim(), Number(jumlah_anggota) || 1, kategori, alamat || null, rt || null, rw || null, kelurahan || null, keterangan || null, Number(lat), Number(lng)],
);
res.status(201).json({ success: true, id: result.insertId, message: "Keluarga berhasil ditambahkan." });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.put("/keluarga/:id", async (req, res) => {
const { nama_kk, jumlah_anggota, kategori, alamat, rt, rw, kelurahan, keterangan, lat, lng } = req.body;
if (!nama_kk || !KATEGORI_KEMISKINAN.includes(kategori) || !validateCoordinate(lat, lng)) {
return res.status(400).json({ success: false, message: "Nama KK, kategori, lat, dan lng wajib valid." });
}
try {
const [result] = await db.execute(
`UPDATE keluarga_miskin
SET nama_kk=?, jumlah_anggota=?, kategori=?, alamat=?, rt=?, rw=?, kelurahan=?, keterangan=?, lat=?, lng=?
WHERE id=?`,
[nama_kk.trim(), Number(jumlah_anggota) || 1, kategori, alamat || null, rt || null, rw || null, kelurahan || null, keterangan || null, Number(lat), Number(lng), req.params.id],
);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Keluarga tidak ditemukan." });
res.json({ success: true, message: "Keluarga berhasil diperbarui." });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.delete("/keluarga/:id", async (req, res) => {
try {
const [result] = await db.execute("DELETE FROM keluarga_miskin WHERE id = ?", [req.params.id]);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Keluarga tidak ditemukan." });
res.json({ success: true, message: "Keluarga berhasil dihapus." });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.get("/export", async (req, res) => {
const ibadahId = Number(req.query.ibadah_id);
const radius = Number(req.query.radius || 500);
if (!Number.isInteger(ibadahId) || !Number.isFinite(radius) || radius <= 0) {
return res.status(400).json({ success: false, message: "ibadah_id dan radius wajib valid." });
}
try {
const [[ibadah]] = await db.execute("SELECT id, nama, lat, lng FROM tempat_ibadah WHERE id = ?", [ibadahId]);
if (!ibadah) return res.status(404).json({ success: false, message: "Tempat ibadah tidak ditemukan." });
const [rows] = await db.execute(
`SELECT
k.id, k.nama_kk, k.jumlah_anggota, k.kategori, k.alamat, k.rt, k.rw, k.kelurahan, k.keterangan, k.lat, k.lng,
ST_Distance_Sphere(POINT(k.lng, k.lat), POINT(?, ?)) AS jarak_meter
FROM keluarga_miskin k
WHERE ST_Distance_Sphere(POINT(k.lng, k.lat), POINT(?, ?)) <= ?
ORDER BY jarak_meter ASC`,
[ibadah.lng, ibadah.lat, ibadah.lng, ibadah.lat, radius],
);
const header = ["nama_kk", "jumlah_anggota", "kategori", "kelurahan", "alamat", "rt", "rw", "jarak_meter"];
const lines = [
`Tempat Ibadah,${escapeCsv(ibadah.nama)}`,
`Radius Meter,${escapeCsv(radius)}`,
header.join(","),
...rows.map((row) => header.map((key) => escapeCsv(key === "jarak_meter" ? Math.round(row.jarak_meter) : row[key])).join(",")),
];
res.setHeader("Content-Type", "text/csv; charset=utf-8");
res.setHeader("Content-Disposition", 'attachment; filename="laporan-keluarga-radius.csv"');
res.send(lines.join("\n"));
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
module.exports = router;