add poverty
This commit is contained in:
+161
-13
@@ -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]);
|
||||
|
||||
+162
-7
@@ -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 @@
|
||||
<button data-section="bantuan">Data Bantuan</button>
|
||||
<button data-section="kriteria">Kriteria SAW</button>
|
||||
<button data-section="ibadah">⛪ Tempat Ibadah</button>
|
||||
<button data-section="pengajuan">📋 Pengajuan Bantuan</button>
|
||||
<button data-section="peta">Preview Peta</button>
|
||||
<a href="/poverty-map.html">Buka Peta Poverty</a>
|
||||
<a href="/pengajuan.html">Form Pengajuan Publik</a>
|
||||
<a href="/admin.html">Admin SPBU</a>
|
||||
</nav>
|
||||
</aside>
|
||||
@@ -343,6 +351,9 @@
|
||||
<label>Jenis<select id="ibadah_jenis" required></select></label>
|
||||
<label>Alamat<input id="ibadah_alamat" 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">
|
||||
<label>Koordinat dari Peta</label>
|
||||
<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="table-wrap">
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
</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">
|
||||
<article class="card">
|
||||
<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>
|
||||
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 DEFAULT_CENTER = [-0.0263, 109.3425];
|
||||
const IBADAH_TYPES = ["Gereja Katolik", "GPIB", "GKI", "HKBP", "Pentakosta"];
|
||||
@@ -469,6 +540,12 @@
|
||||
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_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() {
|
||||
@@ -497,9 +574,37 @@
|
||||
<td>${esc(item.alamat)}</td>
|
||||
<td>${esc(item.kelurahan)}</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>
|
||||
</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;
|
||||
@@ -565,6 +670,8 @@
|
||||
kelurahan: el("ibadah_kelurahan").value.trim(),
|
||||
lat: Number(el("ibadah_lat").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() {
|
||||
const [dashboard, analytics, keluarga, bantuan, riwayat, kriteria, ibadah] = await Promise.all([
|
||||
api("/dashboard"), api("/analytics"), api("/keluarga"), api("/bantuan"), api("/riwayat-bantuan"), api("/kriteria"), api("/ibadah")
|
||||
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("/pengajuan")
|
||||
]);
|
||||
Object.assign(state, { dashboard, analytics, keluarga, bantuan, riwayat, kriteria, ibadah });
|
||||
renderDashboard(); renderKeluarga(); renderBantuan(); renderKriteria(); renderIbadah();
|
||||
Object.assign(state, { dashboard, analytics, keluarga, bantuan, riwayat, kriteria, ibadah, pengajuan });
|
||||
renderDashboard(); renderKeluarga(); renderBantuan(); renderKriteria(); renderIbadah(); renderPengajuan();
|
||||
}
|
||||
|
||||
function familyPayload() {
|
||||
@@ -634,6 +742,8 @@
|
||||
function resetIbadah() {
|
||||
el("ibadahForm").reset();
|
||||
el("id_ibadah").value = "";
|
||||
el("ibadah_kapasitas").value = 0;
|
||||
el("ibadah_keterangan").value = "";
|
||||
el("ibadah_lat").readOnly = true;
|
||||
el("ibadah_lng").readOnly = true;
|
||||
ibadahManual = false;
|
||||
@@ -675,6 +785,8 @@
|
||||
el("ibadah_jenis").value = item.jenis;
|
||||
el("ibadah_alamat").value = item.alamat;
|
||||
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("cancelIbadahEdit").style.display = "inline-flex";
|
||||
setTimeout(() => {
|
||||
@@ -682,6 +794,27 @@
|
||||
el("ibadah").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}, 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) => {
|
||||
if (!confirm("Hapus/nonaktifkan data ini?")) return;
|
||||
await api(`/${resource}/${id}`, { method: "DELETE" });
|
||||
@@ -757,6 +890,28 @@
|
||||
el("familySearch").addEventListener("input", renderKeluarga);
|
||||
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("pendidikan", labels.pendidikan);
|
||||
fillOptions("kondisi_rumah", labels.kondisi_rumah);
|
||||
|
||||
@@ -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 3–7 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 & 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 3–7 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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user