add poverty

This commit is contained in:
tistopandita
2026-06-11 13:56:18 +07:00
parent 7ef7620d2a
commit 5386300498
4 changed files with 1421 additions and 227 deletions
+161 -13
View File
@@ -5,15 +5,17 @@ const TEMPAT_IBADAH_TYPES = ["Gereja Katolik", "GPIB", "GKI", "HKBP", "Pentakost
const createTempatIbadah = ` const createTempatIbadah = `
CREATE TABLE IF NOT EXISTS tempat_ibadah ( CREATE TABLE IF NOT EXISTS tempat_ibadah (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(150) NOT NULL, nama VARCHAR(150) NOT NULL,
jenis ENUM('Gereja Katolik','GPIB','GKI','HKBP','Pentakosta') NOT NULL, jenis ENUM('Gereja Katolik','GPIB','GKI','HKBP','Pentakosta') NOT NULL,
alamat TEXT NOT NULL, alamat TEXT NOT NULL,
kelurahan VARCHAR(100) NOT NULL, kelurahan VARCHAR(100) NOT NULL,
lat DOUBLE NOT NULL, lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL, lng DOUBLE NOT NULL,
aktif TINYINT(1) NOT NULL DEFAULT 1, kapasitas_bantuan INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP keterangan TEXT,
aktif TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`; `;
@@ -26,6 +28,12 @@ async function initTempatIbadahTable() {
if (!columns.includes("created_at")) { if (!columns.includes("created_at")) {
await db.execute("ALTER TABLE tempat_ibadah ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"); 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(` await db.execute(`
UPDATE tempat_ibadah UPDATE tempat_ibadah
SET jenis = 'Gereja Katolik' SET jenis = 'Gereja Katolik'
@@ -291,6 +299,24 @@ async function ensureTables() {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) 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 seedDefaults();
await calculateSaw(); await calculateSaw();
tablesReady = true; tablesReady = true;
@@ -560,9 +586,33 @@ function validateIbadah(body) {
if (!Number.isFinite(Number(body.lat)) || !Number.isFinite(Number(body.lng))) { if (!Number.isFinite(Number(body.lat)) || !Number.isFinite(Number(body.lng))) {
return "Latitude dan longitude wajib valid."; 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; 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) => { router.use(async (req, res, next) => {
try { try {
await ensureTables(); await ensureTables();
@@ -1000,8 +1050,8 @@ router.post("/ibadah", async (req, res) => {
const body = req.body; const body = req.body;
try { try {
const [result] = await db.execute( const [result] = await db.execute(
"INSERT INTO tempat_ibadah (nama, jenis, alamat, kelurahan, lat, lng) VALUES (?, ?, ?, ?, ?, ?)", "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)], [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." }); res.status(201).json({ success: true, id: result.insertId, message: "Tempat ibadah berhasil ditambahkan." });
} catch (err) { } catch (err) {
@@ -1015,8 +1065,8 @@ router.put("/ibadah/:id", async (req, res) => {
const body = req.body; const body = req.body;
try { try {
const [result] = await db.execute( const [result] = await db.execute(
"UPDATE tempat_ibadah SET nama=?, jenis=?, alamat=?, kelurahan=?, lat=?, lng=? WHERE id=? AND aktif = 1", "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), req.params.id], [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." }); if (!result.affectedRows) return res.status(404).json({ success: false, message: "Tempat ibadah tidak ditemukan." });
res.json({ success: true, message: "Tempat ibadah berhasil diperbarui." }); 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) => { router.delete("/ibadah/:id", async (req, res) => {
try { try {
const [result] = await db.execute("UPDATE tempat_ibadah SET aktif = 0 WHERE id = ?", [req.params.id]); const [result] = await db.execute("UPDATE tempat_ibadah SET aktif = 0 WHERE id = ?", [req.params.id]);
+162 -7
View File
@@ -137,6 +137,12 @@
.butuh { background: #ffedd5; color: #7c2d12; } .butuh { background: #ffedd5; color: #7c2d12; }
.rentan { background: #fef9c3; color: #713f12; } .rentan { background: #fef9c3; color: #713f12; }
.mampu { background: #dcfce7; color: #14532d; } .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-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-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)); } .bar-fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--accent), var(--accent-2)); }
@@ -203,8 +209,10 @@
<button data-section="bantuan">Data Bantuan</button> <button data-section="bantuan">Data Bantuan</button>
<button data-section="kriteria">Kriteria SAW</button> <button data-section="kriteria">Kriteria SAW</button>
<button data-section="ibadah">⛪ Tempat Ibadah</button> <button data-section="ibadah">⛪ Tempat Ibadah</button>
<button data-section="pengajuan">📋 Pengajuan Bantuan</button>
<button data-section="peta">Preview Peta</button> <button data-section="peta">Preview Peta</button>
<a href="/poverty-map.html">Buka Peta Poverty</a> <a href="/poverty-map.html">Buka Peta Poverty</a>
<a href="/pengajuan.html">Form Pengajuan Publik</a>
<a href="/admin.html">Admin SPBU</a> <a href="/admin.html">Admin SPBU</a>
</nav> </nav>
</aside> </aside>
@@ -343,6 +351,9 @@
<label>Jenis<select id="ibadah_jenis" required></select></label> <label>Jenis<select id="ibadah_jenis" required></select></label>
<label>Alamat<input id="ibadah_alamat" required /></label> <label>Alamat<input id="ibadah_alamat" required /></label>
<label>Kelurahan<input id="ibadah_kelurahan" required /></label> <label>Kelurahan<input id="ibadah_kelurahan" required /></label>
<label>Kapasitas Bantuan (KK)<input id="ibadah_kapasitas" type="number" min="0" value="0" /></label>
<div></div>
<label class="full">Keterangan / Catatan<textarea id="ibadah_keterangan" placeholder="Jadwal bantuan, kontak, dll."></textarea></label>
<div class="full"> <div class="full">
<label>Koordinat dari Peta</label> <label>Koordinat dari Peta</label>
<div id="ibadahMap" class="ibadah-map"></div> <div id="ibadahMap" class="ibadah-map"></div>
@@ -365,13 +376,73 @@
<div class="card-head"><h3>Daftar Tempat Ibadah</h3><span id="ibadahCount">0 data</span></div> <div class="card-head"><h3>Daftar Tempat Ibadah</h3><span id="ibadahCount">0 data</span></div>
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<thead><tr><th>#</th><th>Nama</th><th>Jenis</th><th>Alamat</th><th>Kelurahan</th><th>Koordinat</th><th>Aksi</th></tr></thead> <thead><tr><th>#</th><th>Nama</th><th>Jenis</th><th>Alamat</th><th>Kelurahan</th><th>Koordinat</th><th>Kapasitas</th><th>Aksi</th></tr></thead>
<tbody id="ibadahBody"></tbody> <tbody id="ibadahBody"></tbody>
</table> </table>
</div> </div>
</article> </article>
</section> </section>
<!-- ═══════════════════════ PENGAJUAN BANTUAN ═══════════════════════ -->
<section class="section" id="pengajuan">
<div class="stats" style="grid-template-columns:repeat(3,minmax(0,1fr))">
<article class="stat"><span>Total Pengajuan</span><strong id="statPengajuanTotal">0</strong></article>
<article class="stat"><span>Menunggu</span><strong id="statPengajuanMenunggu">0</strong></article>
<article class="stat"><span>Disetujui</span><strong id="statPengajuanDisetujui">0</strong></article>
</div>
<article class="card" style="margin-top:18px">
<div class="card-head">
<h3>Daftar Pengajuan Bantuan</h3>
<select id="filterPengajuanStatus" style="min-width:160px;border-radius:10px;padding:7px 10px;border:1px solid #dbe2ea;font-size:.82rem">
<option value="">Semua Status</option>
<option value="menunggu">Menunggu</option>
<option value="diverifikasi">Diverifikasi</option>
<option value="disetujui">Disetujui</option>
<option value="ditolak">Ditolak</option>
<option value="selesai">Selesai</option>
</select>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>#</th><th>Pemohon</th><th>NIK</th><th>Wilayah</th><th>Alasan</th><th>Tgl Pengajuan</th><th>Status</th><th>Aksi</th></tr></thead>
<tbody id="pengajuanBody"></tbody>
</table>
</div>
</article>
<!-- Detail / Update Status Modal (inline) -->
<article class="card" id="pengajuanDetailCard" style="display:none;margin-top:18px">
<div class="card-head">
<h3>Update Status Pengajuan</h3>
<button class="btn secondary small" onclick="el('pengajuanDetailCard').style.display='none'">Tutup</button>
</div>
<div class="card-body">
<p id="pengajuanDetailInfo" style="color:#475569;margin:0 0 14px"></p>
<div class="form-grid">
<label>Status Baru
<select id="pengajuanNewStatus">
<option value="menunggu">Menunggu</option>
<option value="diverifikasi">Diverifikasi</option>
<option value="disetujui">Disetujui</option>
<option value="ditolak">Ditolak</option>
<option value="selesai">Selesai</option>
</select>
</label>
<label>Hubungkan ke Keluarga (opsional)
<select id="pengajuanKeluarga"><option value="">— Tidak dihubungkan —</option></select>
</label>
<label class="full">Catatan Admin
<textarea id="pengajuanCatatan" placeholder="Alasan penolakan, catatan verifikasi, dll."></textarea>
</label>
</div>
<input type="hidden" id="pengajuanEditId" />
<div class="actions">
<button class="btn" id="savePengajuanStatus">Simpan Status</button>
</div>
</div>
</article>
</section>
<section class="section" id="peta"> <section class="section" id="peta">
<article class="card"> <article class="card">
<div class="card-head"><h3>Preview Peta Keluarga Prioritas</h3><span>Klik peta untuk mengisi koordinat form keluarga.</span></div> <div class="card-head"><h3>Preview Peta Keluarga Prioritas</h3><span>Klik peta untuk mengisi koordinat form keluarga.</span></div>
@@ -384,7 +455,7 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script> <script>
const API = "/api/poverty"; const API = "/api/poverty";
const state = { keluarga: [], bantuan: [], riwayat: [], kriteria: [], ibadah: [], dashboard: null, analytics: null }; const state = { keluarga: [], bantuan: [], riwayat: [], kriteria: [], ibadah: [], pengajuan: [], dashboard: null, analytics: null };
const today = new Date().toISOString().slice(0, 10); const today = new Date().toISOString().slice(0, 10);
const DEFAULT_CENTER = [-0.0263, 109.3425]; const DEFAULT_CENTER = [-0.0263, 109.3425];
const IBADAH_TYPES = ["Gereja Katolik", "GPIB", "GKI", "HKBP", "Pentakosta"]; const IBADAH_TYPES = ["Gereja Katolik", "GPIB", "GKI", "HKBP", "Pentakosta"];
@@ -469,6 +540,12 @@
function refreshSelects() { function refreshSelects() {
el("riwayat_keluarga").innerHTML = state.keluarga.map((item) => `<option value="${item.id_keluarga}">${esc(item.nama_kepala_keluarga)} - ${esc(item.kategori || "")}</option>`).join(""); el("riwayat_keluarga").innerHTML = state.keluarga.map((item) => `<option value="${item.id_keluarga}">${esc(item.nama_kepala_keluarga)} - ${esc(item.kategori || "")}</option>`).join("");
el("riwayat_bantuan").innerHTML = state.bantuan.map((item) => `<option value="${item.id_bantuan}">${esc(item.jenis_bantuan)} (${rp(item.nilai_bantuan)})</option>`).join(""); el("riwayat_bantuan").innerHTML = state.bantuan.map((item) => `<option value="${item.id_bantuan}">${esc(item.jenis_bantuan)} (${rp(item.nilai_bantuan)})</option>`).join("");
// Populate pengajuan keluarga select (with leading empty option)
const pengajuanSel = el("pengajuanKeluarga");
if (pengajuanSel) {
pengajuanSel.innerHTML = `<option value="">— Tidak dihubungkan —</option>` +
state.keluarga.map((item) => `<option value="${item.id_keluarga}">#${item.ranking||'-'} ${esc(item.nama_kepala_keluarga)} (${esc(item.wilayah)})</option>`).join("");
}
} }
function renderBantuan() { function renderBantuan() {
@@ -497,9 +574,37 @@
<td>${esc(item.alamat)}</td> <td>${esc(item.alamat)}</td>
<td>${esc(item.kelurahan)}</td> <td>${esc(item.kelurahan)}</td>
<td>${Number(item.lat).toFixed(6)}, ${Number(item.lng).toFixed(6)}</td> <td>${Number(item.lat).toFixed(6)}, ${Number(item.lng).toFixed(6)}</td>
<td>${item.kapasitas_bantuan || 0} KK</td>
<td><button class="btn secondary small" onclick="editIbadah(${item.id})">Edit</button> <button class="btn danger small" onclick="hapus('ibadah', ${item.id})">Hapus</button></td> <td><button class="btn secondary small" onclick="editIbadah(${item.id})">Edit</button> <button class="btn danger small" onclick="hapus('ibadah', ${item.id})">Hapus</button></td>
</tr> </tr>
`).join("") || `<tr><td colspan="7">Belum ada data tempat ibadah.</td></tr>`; `).join("") || `<tr><td colspan="8">Belum ada data tempat ibadah.</td></tr>`;
}
function pengajuanBadgeClass(status) {
return `status-${status || 'menunggu'}`;
}
function renderPengajuan() {
const rows = state.pengajuan;
el("statPengajuanTotal").textContent = rows.length;
el("statPengajuanMenunggu").textContent = rows.filter(r => r.status === "menunggu").length;
el("statPengajuanDisetujui").textContent = rows.filter(r => r.status === "disetujui").length;
el("pengajuanBody").innerHTML = rows.map((item, idx) => `
<tr>
<td>${idx + 1}</td>
<td><strong>${esc(item.nama_pemohon)}</strong></td>
<td>${esc(item.nik)}</td>
<td>${esc(item.wilayah)}</td>
<td style="max-width:200px;white-space:normal;font-size:.82rem">${esc(item.alasan)}</td>
<td>${fmt(item.tanggal_pengajuan)}</td>
<td><span class="badge ${pengajuanBadgeClass(item.status)}">${esc(item.status)}</span></td>
<td>
<button class="btn secondary small" onclick="openPengajuanEdit(${item.id_pengajuan})">Review</button>
<button class="btn danger small" onclick="hapusPengajuan(${item.id_pengajuan})">Hapus</button>
</td>
</tr>
`).join("") || `<tr><td colspan="8">Belum ada pengajuan.</td></tr>`;
} }
let ibadahMap, ibadahMarker, ibadahManual = false; let ibadahMap, ibadahMarker, ibadahManual = false;
@@ -565,6 +670,8 @@
kelurahan: el("ibadah_kelurahan").value.trim(), kelurahan: el("ibadah_kelurahan").value.trim(),
lat: Number(el("ibadah_lat").value), lat: Number(el("ibadah_lat").value),
lng: Number(el("ibadah_lng").value), lng: Number(el("ibadah_lng").value),
kapasitas_bantuan: Number(el("ibadah_kapasitas").value) || 0,
keterangan: el("ibadah_keterangan").value.trim() || null,
}; };
} }
@@ -594,11 +701,12 @@
} }
async function loadAll() { async function loadAll() {
const [dashboard, analytics, keluarga, bantuan, riwayat, kriteria, ibadah] = await Promise.all([ const [dashboard, analytics, keluarga, bantuan, riwayat, kriteria, ibadah, pengajuan] = await Promise.all([
api("/dashboard"), api("/analytics"), api("/keluarga"), api("/bantuan"), api("/riwayat-bantuan"), api("/kriteria"), api("/ibadah") api("/dashboard"), api("/analytics"), api("/keluarga"), api("/bantuan"),
api("/riwayat-bantuan"), api("/kriteria"), api("/ibadah"), api("/pengajuan")
]); ]);
Object.assign(state, { dashboard, analytics, keluarga, bantuan, riwayat, kriteria, ibadah }); Object.assign(state, { dashboard, analytics, keluarga, bantuan, riwayat, kriteria, ibadah, pengajuan });
renderDashboard(); renderKeluarga(); renderBantuan(); renderKriteria(); renderIbadah(); renderDashboard(); renderKeluarga(); renderBantuan(); renderKriteria(); renderIbadah(); renderPengajuan();
} }
function familyPayload() { function familyPayload() {
@@ -634,6 +742,8 @@
function resetIbadah() { function resetIbadah() {
el("ibadahForm").reset(); el("ibadahForm").reset();
el("id_ibadah").value = ""; el("id_ibadah").value = "";
el("ibadah_kapasitas").value = 0;
el("ibadah_keterangan").value = "";
el("ibadah_lat").readOnly = true; el("ibadah_lat").readOnly = true;
el("ibadah_lng").readOnly = true; el("ibadah_lng").readOnly = true;
ibadahManual = false; ibadahManual = false;
@@ -675,6 +785,8 @@
el("ibadah_jenis").value = item.jenis; el("ibadah_jenis").value = item.jenis;
el("ibadah_alamat").value = item.alamat; el("ibadah_alamat").value = item.alamat;
el("ibadah_kelurahan").value = item.kelurahan; el("ibadah_kelurahan").value = item.kelurahan;
el("ibadah_kapasitas").value = item.kapasitas_bantuan || 0;
el("ibadah_keterangan").value = item.keterangan || "";
el("ibadahSubmit").textContent = "Update Tempat Ibadah"; el("ibadahSubmit").textContent = "Update Tempat Ibadah";
el("cancelIbadahEdit").style.display = "inline-flex"; el("cancelIbadahEdit").style.display = "inline-flex";
setTimeout(() => { setTimeout(() => {
@@ -682,6 +794,27 @@
el("ibadah").scrollIntoView({ behavior: "smooth", block: "start" }); el("ibadah").scrollIntoView({ behavior: "smooth", block: "start" });
}, 100); }, 100);
}; };
window.openPengajuanEdit = (id) => {
const item = state.pengajuan.find(r => r.id_pengajuan === id);
if (!item) return;
document.querySelector('[data-section="pengajuan"]').click();
el("pengajuanEditId").value = id;
el("pengajuanNewStatus").value = item.status;
el("pengajuanCatatan").value = item.catatan_admin || "";
el("pengajuanKeluarga").value = item.id_keluarga || "";
el("pengajuanDetailInfo").textContent =
`Pemohon: ${item.nama_pemohon} | NIK: ${item.nik} | Wilayah: ${item.wilayah} | Tgl: ${String(item.tanggal_pengajuan||'').slice(0,10)}`;
el("pengajuanDetailCard").style.display = "block";
el("pengajuanDetailCard").scrollIntoView({ behavior: "smooth" });
};
window.hapusPengajuan = async (id) => {
if (!confirm("Hapus pengajuan ini?")) return;
await api(`/pengajuan/${id}`, { method: "DELETE" });
toast("Pengajuan berhasil dihapus.");
await loadAll();
};
window.hapus = async (resource, id) => { window.hapus = async (resource, id) => {
if (!confirm("Hapus/nonaktifkan data ini?")) return; if (!confirm("Hapus/nonaktifkan data ini?")) return;
await api(`/${resource}/${id}`, { method: "DELETE" }); await api(`/${resource}/${id}`, { method: "DELETE" });
@@ -757,6 +890,28 @@
el("familySearch").addEventListener("input", renderKeluarga); el("familySearch").addEventListener("input", renderKeluarga);
el("recalculateBtn").addEventListener("click", async () => { await api("/recalculate", { method: "POST" }); toast("SAW berhasil dihitung ulang."); await loadAll(); }); el("recalculateBtn").addEventListener("click", async () => { await api("/recalculate", { method: "POST" }); toast("SAW berhasil dihitung ulang."); await loadAll(); });
el("filterPengajuanStatus").addEventListener("change", async () => {
const status = el("filterPengajuanStatus").value;
state.pengajuan = await api(`/pengajuan${status ? '?status='+status : ''}`);
renderPengajuan();
});
el("savePengajuanStatus").addEventListener("click", async () => {
const id = el("pengajuanEditId").value;
if (!id) return;
await api(`/pengajuan/${id}/status`, {
method: "PUT",
body: JSON.stringify({
status: el("pengajuanNewStatus").value,
catatan_admin: el("pengajuanCatatan").value.trim(),
id_keluarga: el("pengajuanKeluarga").value || null,
}),
});
toast("Status pengajuan berhasil diperbarui.");
el("pengajuanDetailCard").style.display = "none";
await loadAll();
});
fillOptions("pekerjaan", labels.pekerjaan); fillOptions("pekerjaan", labels.pekerjaan);
fillOptions("pendidikan", labels.pendidikan); fillOptions("pendidikan", labels.pendidikan);
fillOptions("kondisi_rumah", labels.kondisi_rumah); fillOptions("kondisi_rumah", labels.kondisi_rumah);
+457
View File
@@ -0,0 +1,457 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ajukan Bantuan Sosial WebGIS Gereja Peduli</title>
<meta name="description" content="Form pengajuan bantuan sosial dari gereja. Isi data diri Anda dan tim kami akan segera memproses permohonan Anda." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet" />
<style>
:root {
--accent: #7c3aed;
--accent2: #06b6d4;
--dark: #0f172a;
--panel: #111827;
--border: rgba(148,163,184,.22);
--shadow: 0 24px 70px rgba(15,23,42,.24);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: "Inter", system-ui, sans-serif;
background:
radial-gradient(circle at top left, rgba(124,58,237,.32), transparent 36%),
linear-gradient(135deg, #130f2e 0%, #111827 50%, #312e81 100%);
display: flex;
flex-direction: column;
align-items: center;
padding: 32px 16px 64px;
}
a { color: inherit; text-decoration: none; }
button, input, select, textarea { font: inherit; }
/* ── Hero ── */
.hero {
text-align: center;
color: white;
max-width: 600px;
margin-bottom: 36px;
}
.hero small {
display: block;
font-size: .78rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: .1em;
color: #a5b4fc;
margin-bottom: 12px;
}
.hero h1 {
margin: 0 0 14px;
font-size: clamp(1.8rem, 5vw, 2.8rem);
line-height: 1.1;
background: linear-gradient(135deg, #e0e7ff, #c4b5fd, #93c5fd);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero p {
margin: 0;
color: #cbd5e1;
line-height: 1.6;
font-size: .95rem;
}
/* ── Status badges ── */
.status-flow {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
margin: 22px 0 0;
}
.flow-step {
display: flex;
align-items: center;
gap: 6px;
padding: 7px 14px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,.15);
background: rgba(255,255,255,.07);
color: #e5e7eb;
font-size: .78rem;
font-weight: 700;
}
/* ── Card ── */
.card {
width: 100%;
max-width: 680px;
background: rgba(255,255,255,.96);
border-radius: 28px;
box-shadow: var(--shadow);
overflow: hidden;
}
.card-head {
padding: 24px 28px 20px;
border-bottom: 1px solid #e5e7eb;
background: linear-gradient(135deg, #f8fafc, #f0f4ff);
}
.card-head h2 {
margin: 0 0 4px;
font-size: 1.22rem;
color: #1e1b4b;
}
.card-head p {
margin: 0;
color: #64748b;
font-size: .86rem;
line-height: 1.5;
}
.card-body { padding: 26px 28px; }
/* ── Form ── */
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.full { grid-column: 1 / -1; }
label {
display: grid;
gap: 6px;
color: #334155;
font-size: .82rem;
font-weight: 800;
}
input[type="text"],
input[type="date"],
input[type="tel"],
select,
textarea {
width: 100%;
border: 1px solid #dbe2ea;
border-radius: 14px;
padding: 11px 13px;
outline: none;
color: #1e293b;
background: #f8fafc;
transition: border-color .15s, box-shadow .15s;
font-size: .9rem;
}
input:focus, select:focus, textarea:focus {
border-color: #a78bfa;
box-shadow: 0 0 0 4px rgba(124,58,237,.1);
background: #fff;
}
textarea { min-height: 100px; resize: vertical; }
/* ── Required mark ── */
.req { color: #dc2626; }
/* ── Divider ── */
.divider {
grid-column: 1/-1;
border: 0;
border-top: 1px solid #e2e8f0;
margin: 6px 0;
}
/* ── Info box ── */
.info-box {
grid-column: 1/-1;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 14px;
padding: 14px 16px;
color: #1e3a8a;
font-size: .84rem;
line-height: 1.5;
}
.info-box strong { display: block; margin-bottom: 4px; }
/* ── Submit ── */
.btn-submit {
width: 100%;
margin-top: 18px;
border: 0;
border-radius: 16px;
padding: 15px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
color: white;
font-weight: 800;
font-size: 1rem;
cursor: pointer;
box-shadow: 0 14px 32px rgba(124,58,237,.3);
transition: transform .15s, box-shadow .15s;
}
.btn-submit:hover { transform: translateY(-2px); box-shadow: 0 18px 38px rgba(124,58,237,.4); }
.btn-submit:disabled { opacity: .6; cursor: not-allowed; transform: none; }
/* ── Success ── */
.success-screen {
display: none;
text-align: center;
padding: 40px 28px;
animation: fadeUp .4s ease forwards;
}
.success-icon {
font-size: 4rem;
margin-bottom: 18px;
display: block;
}
.success-screen h2 { margin: 0 0 10px; color: #14532d; font-size: 1.5rem; }
.success-screen p { margin: 0 0 8px; color: #475569; line-height: 1.5; }
.ref-num { font-weight: 800; color: var(--accent); font-size: 1.1rem; }
.btn-back {
display: inline-block;
margin-top: 20px;
padding: 11px 22px;
border-radius: 14px;
background: var(--accent);
color: white;
font-weight: 700;
font-size: .88rem;
cursor: pointer;
border: 0;
transition: .15s;
}
.btn-back:hover { background: #6d28d9; }
/* ── Toast ── */
.toast {
position: fixed;
bottom: 22px;
right: 22px;
background: #0f172a;
color: white;
padding: 12px 16px;
border-radius: 13px;
box-shadow: var(--shadow);
opacity: 0;
transform: translateY(10px);
pointer-events: none;
transition: .22s ease;
z-index: 1000;
max-width: 320px;
font-size: .88rem;
}
.toast.show { opacity: 1; transform: translateY(0); }
/* ── Footer links ── */
.footer-links {
display: flex;
gap: 14px;
justify-content: center;
flex-wrap: wrap;
margin-top: 28px;
}
.footer-links a {
color: rgba(255,255,255,.6);
font-size: .82rem;
font-weight: 600;
transition: color .15s;
}
.footer-links a:hover { color: #fff; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 580px) {
.form-grid { grid-template-columns: 1fr; }
.card-head, .card-body { padding: 20px; }
}
</style>
</head>
<body>
<div class="toast" id="toast"></div>
<!-- Hero -->
<div class="hero">
<small>⛪ Gereja Peduli — Program Bantuan Sosial</small>
<h1>Ajukan Bantuan Sosial</h1>
<p>Isi formulir berikut untuk mengajukan permohonan bantuan. Tim kami akan memverifikasi dan menghubungi Anda secepatnya.</p>
<div class="status-flow">
<div class="flow-step">📋 Ajukan</div>
<div class="flow-step">🔍 Verifikasi</div>
<div class="flow-step">✅ Disetujui</div>
<div class="flow-step">🤝 Tersalurkan</div>
</div>
</div>
<!-- Form Card -->
<div class="card" id="formCard">
<div class="card-head">
<h2>Formulir Pengajuan Bantuan</h2>
<p>Semua data akan dijaga kerahasiaannya dan digunakan hanya untuk keperluan verifikasi bantuan.</p>
</div>
<div class="card-body">
<form id="pengajuanForm">
<div class="form-grid">
<div class="info-box">
<strong>📌 Petunjuk Pengisian</strong>
Isi semua kolom bertanda <span class="req">*</span> dengan data yang benar dan sesuai KTP.
Pengajuan akan diproses dalam 37 hari kerja.
</div>
<hr class="divider" />
<!-- Data Diri -->
<label class="full">Nama Lengkap <span class="req">*</span>
<input type="text" id="nama_pemohon" placeholder="Sesuai Kartu Tanda Penduduk" required />
</label>
<label>NIK (16 digit) <span class="req">*</span>
<input type="tel" id="nik" placeholder="1234567890123456" maxlength="16" pattern="[0-9]{16}" required />
</label>
<label>Tanggal Pengajuan <span class="req">*</span>
<input type="date" id="tanggal_pengajuan" required />
</label>
<label class="full">Alamat Lengkap <span class="req">*</span>
<textarea id="alamat" placeholder="Nama jalan, nomor rumah, RT/RW…" required></textarea>
</label>
<label>Wilayah / Paroki <span class="req">*</span>
<input type="text" id="wilayah" placeholder="Contoh: Paroki Katedral" required />
</label>
<label>No. Telepon / WhatsApp
<input type="tel" id="telepon" placeholder="08xxxxxxxxxx" />
</label>
<hr class="divider" />
<!-- Kondisi -->
<label class="full">Alasan & Kondisi Ekonomi Saat Ini <span class="req">*</span>
<textarea id="alasan" rows="5"
placeholder="Jelaskan kondisi ekonomi Anda saat ini, jenis bantuan yang dibutuhkan (pangan, pendidikan, kesehatan, dll.), dan alasan mengajukan permohonan ini…"
required></textarea>
</label>
<label>Pekerjaan Saat Ini
<select id="pekerjaan">
<option value="">— Pilih Pekerjaan —</option>
<option value="pengangguran">Tidak Bekerja / Pengangguran</option>
<option value="buruh_harian">Buruh Harian</option>
<option value="petani">Petani</option>
<option value="nelayan">Nelayan</option>
<option value="pedagang_kecil">Pedagang Kecil</option>
<option value="karyawan">Karyawan</option>
<option value="wiraswasta">Wiraswasta</option>
<option value="lainnya">Lainnya</option>
</select>
</label>
<label>Jumlah Anggota Keluarga
<input type="tel" id="jumlah_anggota" placeholder="Contoh: 5" pattern="[0-9]+" />
</label>
<div class="info-box full" style="background:#f0fdf4;border-color:#bbf7d0;color:#14532d">
<strong>🔒 Privasi &amp; Keamanan Data</strong>
Data Anda hanya digunakan untuk keperluan verifikasi bantuan sosial.
Kami tidak akan membagikan informasi Anda kepada pihak lain tanpa persetujuan.
</div>
<label class="full" style="flex-direction:row;align-items:flex-start;gap:10px;cursor:pointer">
<input type="checkbox" id="setuju" required style="width:auto;margin-top:2px" />
<span style="font-weight:600;color:#334155">
Saya menyatakan bahwa data yang saya isi adalah benar dan saya bertanggung jawab atas kebenaran informasi tersebut. <span class="req">*</span>
</span>
</label>
</div>
<button type="submit" class="btn-submit" id="submitBtn">
📋 Kirim Permohonan Bantuan
</button>
</form>
<!-- Success Screen -->
<div class="success-screen" id="successScreen">
<span class="success-icon">🎉</span>
<h2>Permohonan Terkirim!</h2>
<p>Nomor referensi pengajuan Anda:</p>
<p class="ref-num" id="refNum">#000</p>
<p style="margin-top:12px">Tim kami akan segera memverifikasi permohonan Anda dan menghubungi Anda melalui nomor telepon yang tercantum.</p>
<p style="margin-top:8px;color:#94a3b8;font-size:.82rem">Proses verifikasi biasanya memakan waktu 37 hari kerja.</p>
<button class="btn-back" onclick="resetForm()">📋 Ajukan Lagi</button>
</div>
</div>
</div>
<!-- Footer links -->
<div class="footer-links">
<a href="/poverty-map.html">🗺️ Lihat Peta Distribusi</a>
<a href="/admin-poverty.html">⚙️ Admin</a>
<a href="/">⛽ WebGIS SPBU</a>
</div>
<script>
const API = "/api/poverty";
const el = (id) => document.getElementById(id);
// Set today's date as default
el("tanggal_pengajuan").value = new Date().toISOString().slice(0, 10);
let toastTimer;
function toast(msg, dur = 3000) {
el("toast").textContent = msg;
el("toast").classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el("toast").classList.remove("show"), dur);
}
function resetForm() {
el("pengajuanForm").reset();
el("tanggal_pengajuan").value = new Date().toISOString().slice(0, 10);
el("pengajuanForm").style.display = "block";
el("successScreen").style.display = "none";
el("submitBtn").style.display = "block";
}
el("pengajuanForm").addEventListener("submit", async (e) => {
e.preventDefault();
const btn = el("submitBtn");
btn.disabled = true;
btn.textContent = "⏳ Mengirim…";
const payload = {
nama_pemohon: el("nama_pemohon").value.trim(),
nik: el("nik").value.trim(),
alamat: el("alamat").value.trim(),
wilayah: el("wilayah").value.trim() || "Belum Dipetakan",
alasan: el("alasan").value.trim(),
tanggal_pengajuan: el("tanggal_pengajuan").value,
};
// Basic NIK validation
if (!/^\d{16}$/.test(payload.nik)) {
toast("⚠️ NIK harus 16 digit angka.");
btn.disabled = false;
btn.textContent = "📋 Kirim Permohonan Bantuan";
return;
}
try {
const res = await fetch(`${API}/pengajuan`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
if (!res.ok || data.success === false) throw new Error(data.message || "Pengajuan gagal dikirim.");
// Show success
el("refNum").textContent = `#${String(data.id).padStart(4, "0")}`;
el("pengajuanForm").style.display = "none";
el("submitBtn").style.display = "none";
el("successScreen").style.display = "block";
} catch (err) {
toast(`${err.message}`);
btn.disabled = false;
btn.textContent = "📋 Kirim Permohonan Bantuan";
}
});
</script>
</body>
</html>
+641 -207
View File
File diff suppressed because it is too large Load Diff