326 lines
12 KiB
JavaScript
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;
|