menambah fitur-poverty

This commit is contained in:
tistopandita
2026-06-10 22:46:14 +07:00
parent 08b84fea98
commit 08844a90d7
5 changed files with 1982 additions and 59 deletions
+325
View File
@@ -0,0 +1,325 @@
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;
+41 -58
View File
@@ -3,6 +3,7 @@ const express = require("express");
const cors = require("cors");
const path = require("path");
const db = require("./db");
const povertyRoutes = require("./routes/poverty");
require("dotenv").config();
const app = express();
@@ -11,7 +12,7 @@ const PORT = process.env.PORT || 3000;
// ─── Middleware ───────────────────────────────────────────────────────────────
app.use(cors());
app.use(express.json());
app.use("/api/poverty", povertyRoutes);
// Sajikan folder frontend secara statis
app.use(express.static(path.join(__dirname, "../frontend")));
@@ -284,12 +285,10 @@ app.post("/api/spbu", async (req, res) => {
.json({ success: false, message: "Semua field wajib diisi." });
}
if (!Array.isArray(jenis_bbm) || jenis_bbm.length === 0) {
return res
.status(400)
.json({
success: false,
message: "Minimal satu jenis BBM harus dipilih.",
});
return res.status(400).json({
success: false,
message: "Minimal satu jenis BBM harus dipilih.",
});
}
try {
const [result] = await db.execute(
@@ -307,21 +306,17 @@ app.post("/api/spbu", async (req, res) => {
manual_override || "auto",
],
);
res
.status(201)
.json({
success: true,
message: "SPBU berhasil ditambahkan.",
id: result.insertId,
});
res.status(201).json({
success: true,
message: "SPBU berhasil ditambahkan.",
id: result.insertId,
});
} catch (err) {
if (err.code === "ER_DUP_ENTRY") {
return res
.status(409)
.json({
success: false,
message: `Kode SPBU '${kode}' sudah digunakan.`,
});
return res.status(409).json({
success: false,
message: `Kode SPBU '${kode}' sudah digunakan.`,
});
}
res.status(500).json({ success: false, message: err.message });
}
@@ -460,12 +455,10 @@ app.get("/api/jalan/:id", async (req, res) => {
app.post("/api/jalan", async (req, res) => {
const { nama_jalan, jenis_jalan, koordinat } = req.body;
if (!nama_jalan || !jenis_jalan || !koordinat) {
return res
.status(400)
.json({
success: false,
message: "nama_jalan, jenis_jalan, dan koordinat wajib diisi.",
});
return res.status(400).json({
success: false,
message: "nama_jalan, jenis_jalan, dan koordinat wajib diisi.",
});
}
const jenisValid = ["nasional", "provinsi", "kota_kabupaten"];
if (!jenisValid.includes(jenis_jalan)) {
@@ -480,13 +473,11 @@ app.post("/api/jalan", async (req, res) => {
"INSERT INTO jalan (nama_jalan, jenis_jalan, koordinat) VALUES (?, ?, ?)",
[nama_jalan.trim(), jenis_jalan, koordinatStr],
);
res
.status(201)
.json({
success: true,
message: "Jalan berhasil ditambahkan.",
id: result.insertId,
});
res.status(201).json({
success: true,
message: "Jalan berhasil ditambahkan.",
id: result.insertId,
});
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
@@ -496,12 +487,10 @@ app.post("/api/jalan", async (req, res) => {
app.put("/api/jalan/:id", async (req, res) => {
const { nama_jalan, jenis_jalan, koordinat } = req.body;
if (!nama_jalan || !jenis_jalan || !koordinat) {
return res
.status(400)
.json({
success: false,
message: "nama_jalan, jenis_jalan, dan koordinat wajib diisi.",
});
return res.status(400).json({
success: false,
message: "nama_jalan, jenis_jalan, dan koordinat wajib diisi.",
});
}
try {
const koordinatStr =
@@ -573,12 +562,10 @@ app.get("/api/bangunan/:id", async (req, res) => {
app.post("/api/bangunan", async (req, res) => {
const { nama_bangunan, jenis_bangunan, nama_jalan, koordinat } = req.body;
if (!nama_bangunan || !jenis_bangunan || !koordinat) {
return res
.status(400)
.json({
success: false,
message: "nama_bangunan, jenis_bangunan, dan koordinat wajib diisi.",
});
return res.status(400).json({
success: false,
message: "nama_bangunan, jenis_bangunan, dan koordinat wajib diisi.",
});
}
const jenisValid = ["SHM", "HGB", "HGU", "HP", "HPL"];
if (!jenisValid.includes(jenis_bangunan)) {
@@ -598,13 +585,11 @@ app.post("/api/bangunan", async (req, res) => {
koordinatStr,
],
);
res
.status(201)
.json({
success: true,
message: "Bangunan berhasil ditambahkan.",
id: result.insertId,
});
res.status(201).json({
success: true,
message: "Bangunan berhasil ditambahkan.",
id: result.insertId,
});
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
@@ -614,12 +599,10 @@ app.post("/api/bangunan", async (req, res) => {
app.put("/api/bangunan/:id", async (req, res) => {
const { nama_bangunan, jenis_bangunan, nama_jalan, koordinat } = req.body;
if (!nama_bangunan || !jenis_bangunan || !koordinat) {
return res
.status(400)
.json({
success: false,
message: "nama_bangunan, jenis_bangunan, dan koordinat wajib diisi.",
});
return res.status(400).json({
success: false,
message: "nama_bangunan, jenis_bangunan, dan koordinat wajib diisi.",
});
}
try {
const koordinatStr =