From 08844a90d7eddb880971fab51702c3350c158595 Mon Sep 17 00:00:00 2001 From: tistopandita Date: Wed, 10 Jun 2026 22:46:14 +0700 Subject: [PATCH] menambah fitur-poverty --- backend/routes/poverty.js | 325 +++++++++++++ backend/server.js | 99 ++-- frontend/admin-poverty.html | 885 ++++++++++++++++++++++++++++++++++++ frontend/poverty-map.html | 683 ++++++++++++++++++++++++++++ schema.sql | 49 +- 5 files changed, 1982 insertions(+), 59 deletions(-) create mode 100644 backend/routes/poverty.js create mode 100644 frontend/admin-poverty.html create mode 100644 frontend/poverty-map.html diff --git a/backend/routes/poverty.js b/backend/routes/poverty.js new file mode 100644 index 0000000..c30b69e --- /dev/null +++ b/backend/routes/poverty.js @@ -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; diff --git a/backend/server.js b/backend/server.js index 1563b9a..45901b5 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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 = diff --git a/frontend/admin-poverty.html b/frontend/admin-poverty.html new file mode 100644 index 0000000..082e49d --- /dev/null +++ b/frontend/admin-poverty.html @@ -0,0 +1,885 @@ + + + + + + Admin Poverty Mapping + + + + + + + + + +
+ + + +
+ +
+
+
+
+
+

Form Tempat Ibadah

+
+
+
+ + + + + +
+ + +
+
+

Klik peta untuk mengisi koordinat tempat ibadah otomatis.

+
+ + +
+
+
+
+ +
+
+

Daftar Tempat Ibadah

+
+
+ + + + + + + + + + + +
NamaJenisKelurahanKoordinatAksi
+
+
+
+
+ +
+
+
+
+

Form Keluarga Miskin

+
+
+
+ + +
+ + +
+ +
+ + +
+ + +
+ + +
+
+

Klik peta untuk mengisi koordinat keluarga otomatis.

+
+ + +
+
+
+
+ +
+
+

Daftar Keluarga

+
+
+ + + +
+
+ + + + + + + + + + + + +
Nama KKAnggotaKategoriKelurahanKoordinatAksi
+
+
+
+
+ +
+
+
+

Peta Preview & Export

+
+
+ + + + +
+
+
+

Preview menampilkan seluruh tempat ibadah dan keluarga. Export memakai tempat ibadah dan radius yang dipilih.

+
+
+
+
+ + + + + diff --git a/frontend/poverty-map.html b/frontend/poverty-map.html new file mode 100644 index 0000000..8930917 --- /dev/null +++ b/frontend/poverty-map.html @@ -0,0 +1,683 @@ + + + + + + Poverty Mapping Pontianak + + + + + + + +
+ +
+
+
🤝
+
+

Poverty Mapping Pontianak

+

Pemetaan keluarga miskin di sekitar tempat ibadah untuk dukungan komunitas.

+
+
+ +
+
+ Radius analisis + 500 meter +
+ +
+
+ +
+

Dashboard Ringkas

+
+
+ Terdampak + 0 +
+
+ Cakupan + 0% +
+
+ Sangat Miskin + 0 +
+
+ Miskin + 0 +
+
+ Rentan Miskin + 0 +
+
+ Tempat Dipilih + - +
+
+
+ + + + + + + diff --git a/schema.sql b/schema.sql index 882904c..d6dec01 100644 --- a/schema.sql +++ b/schema.sql @@ -50,4 +50,51 @@ CREATE TABLE IF NOT EXISTS bangunan ( aktif TINYINT(1) NOT NULL DEFAULT 1 COMMENT '1=aktif, 0=dihapus (soft delete)', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ── Tabel Poverty Mapping ─────────────────────────────────────────────── +-- Catatan: project ini memakai MySQL/MariaDB. Kolom geom dibuat kompatibel +-- dengan MySQL spatial dan query radius memakai ST_Distance_Sphere. + +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; + +CREATE TABLE IF NOT EXISTS keluarga_miskin ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama_kk VARCHAR(255) NOT NULL COMMENT 'Nama Kepala Keluarga', + 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; + +INSERT INTO tempat_ibadah (nama, jenis, alamat, kelurahan, lat, lng) VALUES +('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); + +INSERT INTO keluarga_miskin (nama_kk, jumlah_anggota, kategori, kelurahan, lat, lng) VALUES +('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.3600);