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;