From 5386300498fd68c5352afcce044170326fed7ec0 Mon Sep 17 00:00:00 2001 From: tistopandita Date: Thu, 11 Jun 2026 13:56:18 +0700 Subject: [PATCH] add poverty --- backend/routes/poverty.js | 174 +++++++- frontend/admin-poverty.html | 169 ++++++- frontend/pengajuan.html | 457 +++++++++++++++++++ frontend/poverty-map.html | 848 +++++++++++++++++++++++++++--------- 4 files changed, 1421 insertions(+), 227 deletions(-) create mode 100644 frontend/pengajuan.html diff --git a/backend/routes/poverty.js b/backend/routes/poverty.js index a3d6a65..2b5b731 100644 --- a/backend/routes/poverty.js +++ b/backend/routes/poverty.js @@ -5,15 +5,17 @@ const TEMPAT_IBADAH_TYPES = ["Gereja Katolik", "GPIB", "GKI", "HKBP", "Pentakost const createTempatIbadah = ` CREATE TABLE IF NOT EXISTS tempat_ibadah ( - id INT AUTO_INCREMENT PRIMARY KEY, - nama VARCHAR(150) NOT NULL, - jenis ENUM('Gereja Katolik','GPIB','GKI','HKBP','Pentakosta') NOT NULL, - alamat TEXT NOT NULL, - kelurahan VARCHAR(100) NOT NULL, - lat DOUBLE NOT NULL, - lng DOUBLE NOT NULL, - aktif TINYINT(1) NOT NULL DEFAULT 1, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + id INT AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(150) NOT NULL, + jenis ENUM('Gereja Katolik','GPIB','GKI','HKBP','Pentakosta') NOT NULL, + alamat TEXT NOT NULL, + kelurahan VARCHAR(100) NOT NULL, + lat DOUBLE NOT NULL, + lng DOUBLE NOT NULL, + kapasitas_bantuan INT NOT NULL DEFAULT 0, + keterangan TEXT, + aktif TINYINT(1) NOT NULL DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `; @@ -26,6 +28,12 @@ async function initTempatIbadahTable() { if (!columns.includes("created_at")) { await db.execute("ALTER TABLE tempat_ibadah ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"); } + if (!columns.includes("kapasitas_bantuan")) { + await db.execute("ALTER TABLE tempat_ibadah ADD COLUMN kapasitas_bantuan INT NOT NULL DEFAULT 0"); + } + if (!columns.includes("keterangan")) { + await db.execute("ALTER TABLE tempat_ibadah ADD COLUMN keterangan TEXT"); + } await db.execute(` UPDATE tempat_ibadah SET jenis = 'Gereja Katolik' @@ -291,6 +299,24 @@ async function ensureTables() { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; `); + await db.execute(` + CREATE TABLE IF NOT EXISTS pengajuan_bantuan ( + id_pengajuan INT AUTO_INCREMENT PRIMARY KEY, + nama_pemohon VARCHAR(160) NOT NULL, + nik VARCHAR(20) NOT NULL, + alamat TEXT NOT NULL, + wilayah VARCHAR(120) NOT NULL DEFAULT 'Belum Dipetakan', + alasan TEXT NOT NULL, + status ENUM('menunggu','diverifikasi','disetujui','ditolak','selesai') NOT NULL DEFAULT 'menunggu', + catatan_admin TEXT, + id_keluarga INT, + tanggal_pengajuan DATE NOT NULL, + tanggal_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_pengajuan_keluarga FOREIGN KEY (id_keluarga) REFERENCES keluarga(id_keluarga) ON DELETE SET NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `); + await seedDefaults(); await calculateSaw(); tablesReady = true; @@ -560,9 +586,33 @@ function validateIbadah(body) { if (!Number.isFinite(Number(body.lat)) || !Number.isFinite(Number(body.lng))) { return "Latitude dan longitude wajib valid."; } + if (body.kapasitas_bantuan !== undefined && !Number.isFinite(Number(body.kapasitas_bantuan))) { + return "Kapasitas bantuan harus berupa angka."; + } return null; } +function validatePengajuan(body) { + const required = ["nama_pemohon", "nik", "alamat", "alasan", "tanggal_pengajuan"]; + for (const key of required) { + if (!body[key] || String(body[key]).trim() === "") { + return `${key} wajib diisi.`; + } + } + return null; +} + +// Haversine distance in meters +function haversineMeters(lat1, lng1, lat2, lng2) { + const R = 6371000; + const dLat = ((lat2 - lat1) * Math.PI) / 180; + const dLng = ((lng2 - lng1) * Math.PI) / 180; + const a = + Math.sin(dLat / 2) ** 2 + + Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLng / 2) ** 2; + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +} + router.use(async (req, res, next) => { try { await ensureTables(); @@ -1000,8 +1050,8 @@ router.post("/ibadah", async (req, res) => { const body = req.body; try { const [result] = await db.execute( - "INSERT INTO tempat_ibadah (nama, jenis, alamat, kelurahan, lat, lng) VALUES (?, ?, ?, ?, ?, ?)", - [body.nama, body.jenis, body.alamat, body.kelurahan, Number(body.lat), Number(body.lng)], + "INSERT INTO tempat_ibadah (nama, jenis, alamat, kelurahan, lat, lng, kapasitas_bantuan, keterangan) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + [body.nama, body.jenis, body.alamat, body.kelurahan, Number(body.lat), Number(body.lng), toNumber(body.kapasitas_bantuan), body.keterangan || null], ); res.status(201).json({ success: true, id: result.insertId, message: "Tempat ibadah berhasil ditambahkan." }); } catch (err) { @@ -1015,8 +1065,8 @@ router.put("/ibadah/:id", async (req, res) => { const body = req.body; try { const [result] = await db.execute( - "UPDATE tempat_ibadah SET nama=?, jenis=?, alamat=?, kelurahan=?, lat=?, lng=? WHERE id=? AND aktif = 1", - [body.nama, body.jenis, body.alamat, body.kelurahan, Number(body.lat), Number(body.lng), req.params.id], + "UPDATE tempat_ibadah SET nama=?, jenis=?, alamat=?, kelurahan=?, lat=?, lng=?, kapasitas_bantuan=?, keterangan=? WHERE id=? AND aktif = 1", + [body.nama, body.jenis, body.alamat, body.kelurahan, Number(body.lat), Number(body.lng), toNumber(body.kapasitas_bantuan), body.keterangan || null, 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." }); @@ -1025,6 +1075,104 @@ router.put("/ibadah/:id", async (req, res) => { } }); +// ─── Nearest Church ───────────────────────────────────────────────────────── +router.get("/nearest-church", async (req, res) => { + try { + const lat = Number(req.query.lat); + const lng = Number(req.query.lng); + if (!Number.isFinite(lat) || !Number.isFinite(lng)) { + return res.status(400).json({ success: false, message: "lat dan lng wajib valid." }); + } + const [rows] = await db.execute("SELECT * FROM tempat_ibadah WHERE aktif = 1 ORDER BY nama ASC"); + if (!rows.length) return res.json({ success: true, data: null }); + let nearest = null; + let minDist = Infinity; + for (const item of rows) { + const dist = haversineMeters(lat, lng, Number(item.lat), Number(item.lng)); + if (dist < minDist) { + minDist = dist; + nearest = { ...item, jarak_meter: Math.round(dist) }; + } + } + // Also return all churches sorted by distance + const sorted = rows + .map((item) => ({ ...item, jarak_meter: Math.round(haversineMeters(lat, lng, Number(item.lat), Number(item.lng))) })) + .sort((a, b) => a.jarak_meter - b.jarak_meter); + res.json({ success: true, data: { nearest, all: sorted } }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}); + +// ─── Pengajuan Bantuan ─────────────────────────────────────────────────────── +router.get("/pengajuan", async (req, res) => { + try { + const status = req.query.status || ""; + const sql = status + ? "SELECT * FROM pengajuan_bantuan WHERE status = ? ORDER BY created_at DESC" + : "SELECT * FROM pengajuan_bantuan ORDER BY created_at DESC"; + const params = status ? [status] : []; + const [rows] = await db.execute(sql, params); + res.json({ success: true, data: rows }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.get("/pengajuan/:id", async (req, res) => { + try { + const [rows] = await db.execute("SELECT * FROM pengajuan_bantuan WHERE id_pengajuan = ?", [req.params.id]); + if (!rows.length) return res.status(404).json({ success: false, message: "Pengajuan tidak ditemukan." }); + res.json({ success: true, data: rows[0] }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.post("/pengajuan", async (req, res) => { + const error = validatePengajuan(req.body); + if (error) return res.status(400).json({ success: false, message: error }); + const body = req.body; + try { + const [result] = await db.execute( + `INSERT INTO pengajuan_bantuan (nama_pemohon, nik, alamat, wilayah, alasan, tanggal_pengajuan) + VALUES (?, ?, ?, ?, ?, ?)`, + [body.nama_pemohon.trim(), body.nik.trim(), body.alamat.trim(), body.wilayah || "Belum Dipetakan", body.alasan.trim(), body.tanggal_pengajuan], + ); + res.status(201).json({ success: true, id: result.insertId, message: "Pengajuan berhasil dikirim. Kami akan segera menghubungi Anda." }); + } catch (err) { + res.status(500).json({ success: false, message: err.message }); + } +}); + +router.put("/pengajuan/:id/status", async (req, res) => { + const { status, catatan_admin, id_keluarga } = req.body; + const validStatuses = ["menunggu", "diverifikasi", "disetujui", "ditolak", "selesai"]; + if (!validStatuses.includes(status)) { + return res.status(400).json({ success: false, message: "Status tidak valid." }); + } + try { + const [result] = await db.execute( + "UPDATE pengajuan_bantuan SET status=?, catatan_admin=?, id_keluarga=? WHERE id_pengajuan=?", + [status, catatan_admin || null, id_keluarga || null, req.params.id], + ); + if (!result.affectedRows) return res.status(404).json({ success: false, message: "Pengajuan tidak ditemukan." }); + res.json({ success: true, message: `Status pengajuan diperbarui menjadi '${status}'.` }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.delete("/pengajuan/:id", async (req, res) => { + try { + const [result] = await db.execute("DELETE FROM pengajuan_bantuan WHERE id_pengajuan = ?", [req.params.id]); + if (!result.affectedRows) return res.status(404).json({ success: false, message: "Pengajuan tidak ditemukan." }); + res.json({ success: true, message: "Pengajuan berhasil dihapus." }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}); + router.delete("/ibadah/:id", async (req, res) => { try { const [result] = await db.execute("UPDATE tempat_ibadah SET aktif = 0 WHERE id = ?", [req.params.id]); diff --git a/frontend/admin-poverty.html b/frontend/admin-poverty.html index aa6e85d..d6cf8b8 100644 --- a/frontend/admin-poverty.html +++ b/frontend/admin-poverty.html @@ -137,6 +137,12 @@ .butuh { background: #ffedd5; color: #7c2d12; } .rentan { background: #fef9c3; color: #713f12; } .mampu { background: #dcfce7; color: #14532d; } + /* Status pengajuan */ + .status-menunggu { background: #fef3c7; color: #92400e; } + .status-diverifikasi { background: #dbeafe; color: #1e40af; } + .status-disetujui { background: #dcfce7; color: #14532d; } + .status-ditolak { background: #fee2e2; color: #7f1d1d; } + .status-selesai { background: #e0e7ff; color: #3730a3; } .bar-row { display: grid; grid-template-columns: 150px 1fr 48px; gap: 10px; align-items: center; margin: 10px 0; font-size: .86rem; } .bar-track { height: 12px; border-radius: 99px; background: #e2e8f0; overflow: hidden; } .bar-fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--accent), var(--accent-2)); } @@ -203,8 +209,10 @@ + Buka Peta Poverty + Form Pengajuan Publik Admin SPBU @@ -343,6 +351,9 @@ + +
+
@@ -365,13 +376,73 @@

Daftar Tempat Ibadah

0 data
- +
#NamaJenisAlamatKelurahanKoordinatAksi
#NamaJenisAlamatKelurahanKoordinatKapasitasAksi
+ +
+
+
Total Pengajuan0
+
Menunggu0
+
Disetujui0
+
+
+
+

Daftar Pengajuan Bantuan

+ +
+
+ + + +
#PemohonNIKWilayahAlasanTgl PengajuanStatusAksi
+
+
+ + + +
+

Preview Peta Keluarga Prioritas

Klik peta untuk mengisi koordinat form keluarga.
@@ -384,7 +455,7 @@ + + diff --git a/frontend/poverty-map.html b/frontend/poverty-map.html index 664fd03..92a2772 100644 --- a/frontend/poverty-map.html +++ b/frontend/poverty-map.html @@ -4,24 +4,26 @@ WebGIS Poverty Mapping Gereja + + - +
-
+
-
Ranking tertinggi-
-
Rata-rata skor0.000
+
Ranking #1-
+
Rata-rata Skor SAW0.000
+