Files
UAS_SIG_WEBGIS_TalithaSyaki…/3_PovertyMapping/index.php
T
2026-06-09 15:29:53 +07:00

2011 lines
82 KiB
PHP

<?php
session_start();
require_once __DIR__ . '/../env.php';
$host = env('DB_HOST', '127.0.0.1');
$user = env('DB_USER', 'root');
$pass = env('DB_PASS', '');
$db = env('DB_NAME', 'sig');
$port = (int) env('DB_PORT', 3306);
$conn = new mysqli($host, $user, $pass, $db, $port);
if ($conn->connect_error) {
die("Koneksi database gagal.");
}
$conn->set_charset("utf8mb4");
$conn = new mysqli($host, $user, $pass, $db);
if ($conn->connect_error) {
die("Koneksi gagal: " . $conn->connect_error);
}
$conn->set_charset("utf8mb4");
function is_logged_in() {
return isset($_SESSION['user']);
}
function current_role() {
return $_SESSION['user']['role'] ?? 'masyarakat';
}
function current_instansi() {
return $_SESSION['user']['instansi'] ?? '';
}
function is_admin() {
return current_role() === 'admin';
}
function is_walikota() {
return current_role() === 'walikota';
}
function is_dinas() {
return current_role() === 'dinas';
}
function can_view_dashboard() {
return is_admin() || is_walikota() || is_dinas();
}
function can_download_data() {
return is_admin() || is_walikota() || is_dinas();
}
function can_add_histori() {
return is_admin() || is_dinas();
}
function json_out($arr) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode($arr);
exit;
}
function post($key, $default = '') {
return trim($_POST[$key] ?? $default);
}
function ipost($key, $default = 0) {
return (int) ($_POST[$key] ?? $default);
}
function fpost($key, $default = 0) {
return (float) ($_POST[$key] ?? $default);
}
function require_admin() {
if (!is_admin()) {
json_out([
'status' => false,
'message' => 'Akses ditolak. Fitur ini hanya untuk admin pengelola.'
]);
}
}
function require_dashboard_access() {
if (!can_view_dashboard()) {
json_out([
'status' => false,
'message' => 'Akses ditolak. Silakan login terlebih dahulu.'
]);
}
}
function require_download_access() {
if (!can_download_data()) {
json_out([
'status' => false,
'message' => 'Akses download hanya untuk admin, wali kota, atau dinas terkait.'
]);
}
}
function require_histori_write() {
if (!can_add_histori()) {
json_out([
'status' => false,
'message' => 'Akses ditolak. Histori bantuan hanya dapat ditambahkan oleh admin atau dinas terkait.'
]);
}
}
function valid_enum($value, $allowed, $default) {
return in_array($value, $allowed, true) ? $value : $default;
}
function allowed_aktor_tujuan() {
return [
'RT/RW',
'Kelurahan',
'Kecamatan',
'Dinas Sosial',
'Dinas Kesehatan',
'Wali Kota'
];
}
function dinas_filter_sql($field = 'aktor_tujuan') {
if (!is_dinas()) return ['', []];
$instansi = current_instansi() ?: 'Dinas Sosial';
return [" WHERE $field = ? ", [$instansi]];
}
function find_matching_penduduk_id($conn, $namaKepalaKeluarga, $nik) {
$namaKepalaKeluarga = trim((string) $namaKepalaKeluarga);
$nik = trim((string) $nik);
if ($nik !== '') {
$stmt = $conn->prepare("SELECT id FROM penduduk_kemiskinan WHERE nik = ? LIMIT 1");
$stmt->bind_param("s", $nik);
$stmt->execute();
$row = $stmt->get_result()->fetch_assoc();
if ($row) {
return (int) $row['id'];
}
}
if ($namaKepalaKeluarga !== '') {
$stmt = $conn->prepare("SELECT id FROM penduduk_kemiskinan WHERE LOWER(TRIM(nama_kepala_keluarga)) = LOWER(TRIM(?)) LIMIT 1");
$stmt->bind_param("s", $namaKepalaKeluarga);
$stmt->execute();
$row = $stmt->get_result()->fetch_assoc();
if ($row) {
return (int) $row['id'];
}
}
return 0;
}
if (isset($_GET['logout'])) {
session_destroy();
header('Location: index.php');
exit;
}
if (isset($_POST['login'])) {
$username = post('username');
$password = $_POST['password'] ?? '';
$stmt = $conn->prepare("SELECT * FROM users WHERE username=? LIMIT 1");
$stmt->bind_param("s", $username);
$stmt->execute();
$userRow = $stmt->get_result()->fetch_assoc();
if ($userRow && password_verify($password, $userRow['password'])) {
$_SESSION['user'] = [
'id' => $userRow['id'],
'nama' => $userRow['nama'],
'username' => $userRow['username'],
'role' => $userRow['role'],
'instansi' => $userRow['instansi'] ?? ''
];
header('Location: index.php');
exit;
}
$login_error = "Username atau password salah.";
}
/* =========================
DOWNLOAD CSV
========================= */
if (isset($_GET['download'])) {
require_download_access();
$type = $_GET['download'];
if ($type === 'laporan') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=laporan_masyarakat.csv');
$out = fopen('php://output', 'w');
fputcsv($out, [
'ID', 'Nama Pelapor', 'No HP', 'Kategori', 'Aktor Tujuan',
'Status', 'Deskripsi Publik',
'Nama Warga Dilaporkan', 'NIK', 'Tanggal Lahir', 'Pendidikan',
'Riwayat Penyakit', 'Jumlah KK', 'Alamat', 'Kecamatan', 'Kelurahan',
'Status Bantuan Awal', 'Latitude', 'Longitude', 'Tanggal'
]);
if (is_dinas()) {
$instansi = current_instansi() ?: 'Dinas Sosial';
$stmt = $conn->prepare("SELECT * FROM laporan_masyarakat WHERE aktor_tujuan=? ORDER BY id DESC");
$stmt->bind_param("s", $instansi);
$stmt->execute();
$result = $stmt->get_result();
} else {
$result = $conn->query("SELECT * FROM laporan_masyarakat ORDER BY id DESC");
}
while ($row = $result->fetch_assoc()) {
fputcsv($out, [
$row['id'], $row['nama_pelapor'], $row['no_hp'], $row['kategori_laporan'],
$row['aktor_tujuan'], $row['status_laporan'], $row['deskripsi'],
$row['data_nama_kepala_keluarga'] ?? '', $row['data_nik'] ?? '', $row['data_tanggal_lahir'] ?? '',
$row['data_pendidikan'] ?? '', $row['data_riwayat_penyakit'] ?? '', $row['data_jumlah_kk'] ?? '',
$row['data_alamat'] ?? '', $row['data_kecamatan'] ?? '', $row['data_kelurahan'] ?? '',
$row['data_status_bantuan'] ?? '',
$row['latitude'], $row['longitude'], $row['created_at']
]);
}
exit;
}
if ($type === 'histori') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=histori_bantuan.csv');
$out = fopen('php://output', 'w');
fputcsv($out, [
'ID', 'Kepala Keluarga', 'Jenis Bantuan', 'Tanggal',
'Nominal', 'Sumber Bantuan', 'Rumah Ibadah', 'Keterangan'
]);
$sql = "SELECT hb.*, pk.nama_kepala_keluarga, m.nama_masjid, m.jenis_rumah_ibadah
FROM histori_bantuan hb
JOIN penduduk_kemiskinan pk ON pk.id = hb.penduduk_id
LEFT JOIN masjid m ON m.id = hb.masjid_id
ORDER BY hb.id DESC";
$result = $conn->query($sql);
while ($row = $result->fetch_assoc()) {
fputcsv($out, [
$row['id'],
$row['nama_kepala_keluarga'],
$row['jenis_bantuan'],
$row['tanggal_bantuan'],
$row['nominal'],
$row['sumber_bantuan'],
$row['nama_masjid'] ?? '',
$row['keterangan']
]);
}
exit;
}
if ($type === 'penduduk') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=masyarakat_miskin.csv');
$out = fopen('php://output', 'w');
fputcsv($out, [
'ID', 'Nama Kepala Keluarga', 'NIK', 'Tanggal Lahir', 'Pendidikan',
'Riwayat Penyakit', 'Jumlah KK', 'Alamat', 'Kecamatan', 'Kelurahan',
'Status Bantuan', 'Latitude', 'Longitude', 'Tanggal Input'
]);
$result = $conn->query("SELECT * FROM penduduk_kemiskinan ORDER BY id DESC");
while ($row = $result->fetch_assoc()) {
fputcsv($out, [
$row['id'], $row['nama_kepala_keluarga'], $row['nik'], $row['tanggal_lahir'],
$row['pendidikan'], $row['riwayat_penyakit'], $row['jumlah_kk'], $row['alamat'],
$row['kecamatan'], $row['kelurahan'], $row['status_bantuan'],
$row['latitude'], $row['longitude'], $row['created_at']
]);
}
exit;
}
if ($type === 'rumah_ibadah') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=rumah_ibadah.csv');
$out = fopen('php://output', 'w');
fputcsv($out, [
'ID', 'Jenis', 'Nama Rumah Ibadah', 'PIC', 'Alamat',
'Latitude', 'Longitude', 'Radius Meter', 'Tanggal Input'
]);
$result = $conn->query("SELECT * FROM masjid ORDER BY id DESC");
while ($row = $result->fetch_assoc()) {
fputcsv($out, [
$row['id'], $row['jenis_rumah_ibadah'], $row['nama_masjid'], $row['pic_masjid'],
$row['alamat_masjid'], $row['latitude'], $row['longitude'], $row['radius_meter'],
$row['created_at']
]);
}
exit;
}
}
/* =========================
API ACTIONS
========================= */
if (isset($_GET['action'])) {
$action = $_GET['action'];
if ($action === 'get_all') {
$data = [];
$tables = [
'penduduk_kemiskinan',
'masjid',
'laporan_masyarakat'
];
foreach ($tables as $table) {
$data[$table] = [];
if ($table === 'laporan_masyarakat' && is_dinas()) {
$instansi = current_instansi() ?: 'Dinas Sosial';
$stmt = $conn->prepare("SELECT * FROM laporan_masyarakat WHERE aktor_tujuan=? ORDER BY id DESC");
$stmt->bind_param("s", $instansi);
$stmt->execute();
$result = $stmt->get_result();
} else {
$result = $conn->query("SELECT * FROM $table ORDER BY id DESC");
}
if ($result) {
while ($row = $result->fetch_assoc()) {
$data[$table][] = $row;
}
}
}
$data['users'] = [];
if (is_admin()) {
$resultUsers = $conn->query("SELECT id, nama, username, role, instansi, created_at FROM users ORDER BY role, nama");
if ($resultUsers) {
while ($row = $resultUsers->fetch_assoc()) {
$data['users'][] = $row;
}
}
}
$data['histori_bantuan'] = [];
$sqlHistori = "SELECT hb.*, pk.nama_kepala_keluarga, m.nama_masjid, m.jenis_rumah_ibadah
FROM histori_bantuan hb
JOIN penduduk_kemiskinan pk ON pk.id = hb.penduduk_id
LEFT JOIN masjid m ON m.id = hb.masjid_id
ORDER BY hb.id DESC";
$result = $conn->query($sqlHistori);
if ($result) {
while ($row = $result->fetch_assoc()) {
$data['histori_bantuan'][] = $row;
}
}
json_out($data);
}
/* =========================
PENDUDUK KEMISKINAN
========================= */
if ($action === 'add_penduduk' || $action === 'update_penduduk') {
require_admin();
$id = ipost('id');
$nama = post('nama_kepala_keluarga');
$nik = post('nik');
$tanggalLahir = post('tanggal_lahir') ?: null;
$pendidikan = valid_enum(post('pendidikan'), ['Tidak sekolah', 'SD', 'SMP', 'SMA/SMK', 'Diploma', 'S1', 'S2', 'S3', 'Tidak diketahui'], 'Tidak diketahui');
$riwayat = post('riwayat_penyakit');
$jumlahKk = ipost('jumlah_kk', 1);
$alamat = post('alamat');
$kecamatan = post('kecamatan');
$kelurahan = post('kelurahan');
$statusBantuan = valid_enum(post('status_bantuan'), ['Belum menerima', 'Sudah menerima', 'Perlu verifikasi'], 'Belum menerima');
$lat = fpost('latitude');
$lng = fpost('longitude');
if ($nama === '' || $jumlahKk <= 0 || $alamat === '' || $lat == 0 || $lng == 0) {
json_out(['status' => false, 'message' => 'Data masyarakat prasejahtera belum lengkap']);
}
if ($action === 'add_penduduk') {
$stmt = $conn->prepare("INSERT INTO penduduk_kemiskinan
(nama_kepala_keluarga, nik, tanggal_lahir, pendidikan, riwayat_penyakit, jumlah_kk, alamat, kecamatan, kelurahan, status_bantuan, latitude, longitude)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param("sssssissssdd", $nama, $nik, $tanggalLahir, $pendidikan, $riwayat, $jumlahKk, $alamat, $kecamatan, $kelurahan, $statusBantuan, $lat, $lng);
} else {
if ($id <= 0) json_out(['status' => false, 'message' => 'ID masyarakat prasejahtera tidak valid']);
$stmt = $conn->prepare("UPDATE penduduk_kemiskinan SET
nama_kepala_keluarga=?, nik=?, tanggal_lahir=?, pendidikan=?, riwayat_penyakit=?, jumlah_kk=?, alamat=?, kecamatan=?, kelurahan=?, status_bantuan=?, latitude=?, longitude=?
WHERE id=?");
$stmt->bind_param("sssssissssddi", $nama, $nik, $tanggalLahir, $pendidikan, $riwayat, $jumlahKk, $alamat, $kecamatan, $kelurahan, $statusBantuan, $lat, $lng, $id);
}
json_out($stmt->execute()
? ['status' => true, 'message' => $action === 'add_penduduk' ? 'Data masyarakat prasejahtera berhasil ditambahkan' : 'Data masyarakat prasejahtera berhasil diupdate']
: ['status' => false, 'message' => 'Gagal menyimpan data masyarakat prasejahtera']
);
}
if ($action === 'update_position_penduduk') {
require_admin();
$id = ipost('id');
$lat = fpost('latitude');
$lng = fpost('longitude');
$stmt = $conn->prepare("UPDATE penduduk_kemiskinan SET latitude=?, longitude=? WHERE id=?");
$stmt->bind_param("ddi", $lat, $lng, $id);
json_out($stmt->execute()
? ['status' => true, 'message' => 'Posisi masyarakat prasejahtera berhasil diupdate']
: ['status' => false, 'message' => 'Gagal update posisi masyarakat prasejahtera']
);
}
/* =========================
RUMAH IBADAH / MASJID
========================= */
if ($action === 'add_masjid' || $action === 'update_masjid') {
require_admin();
$id = ipost('id');
$jenis = valid_enum(post('jenis_rumah_ibadah'), ['Masjid', 'Gereja', 'Vihara', 'Kelenteng', 'Pura'], 'Masjid');
$nama = post('nama_masjid');
$pic = post('pic_masjid');
$alamat = post('alamat_masjid');
$lat = fpost('latitude');
$lng = fpost('longitude');
$radius = max(100, min(5000, ipost('radius_meter', 500)));
if ($nama === '' || $pic === '' || $alamat === '' || $lat == 0 || $lng == 0) {
json_out(['status' => false, 'message' => 'Data rumah ibadah belum lengkap']);
}
if ($action === 'add_masjid') {
$stmt = $conn->prepare("INSERT INTO masjid
(jenis_rumah_ibadah, nama_masjid, pic_masjid, alamat_masjid, latitude, longitude, radius_meter)
VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param("ssssddi", $jenis, $nama, $pic, $alamat, $lat, $lng, $radius);
} else {
if ($id <= 0) json_out(['status' => false, 'message' => 'ID rumah ibadah tidak valid']);
$stmt = $conn->prepare("UPDATE masjid SET
jenis_rumah_ibadah=?, nama_masjid=?, pic_masjid=?, alamat_masjid=?, latitude=?, longitude=?, radius_meter=?
WHERE id=?");
$stmt->bind_param("ssssddii", $jenis, $nama, $pic, $alamat, $lat, $lng, $radius, $id);
}
json_out($stmt->execute()
? ['status' => true, 'message' => $action === 'add_masjid' ? 'Data rumah ibadah berhasil ditambahkan' : 'Data rumah ibadah berhasil diupdate']
: ['status' => false, 'message' => 'Gagal menyimpan data rumah ibadah']
);
}
if ($action === 'update_position_masjid') {
require_admin();
$id = ipost('id');
$lat = fpost('latitude');
$lng = fpost('longitude');
$stmt = $conn->prepare("UPDATE masjid SET latitude=?, longitude=? WHERE id=?");
$stmt->bind_param("ddi", $lat, $lng, $id);
json_out($stmt->execute()
? ['status' => true, 'message' => 'Posisi rumah ibadah berhasil diupdate']
: ['status' => false, 'message' => 'Gagal update posisi rumah ibadah']
);
}
/* =========================
LAPORAN MASYARAKAT
========================= */
if ($action === 'add_laporan' || $action === 'update_laporan') {
if ($action === 'add_laporan' && is_logged_in()) {
json_out([
'status' => false,
'message' => 'Laporan cepat hanya dibuat dari mode masyarakat umum. Admin cukup memperbarui status laporan.'
]);
}
if ($action === 'update_laporan') {
require_admin();
}
$id = ipost('id');
$pendudukIdInput = ipost('penduduk_id');
$pendudukId = $pendudukIdInput > 0 ? $pendudukIdInput : null;
// Data publik laporan
$nama = post('nama_pelapor');
$hp = post('no_hp');
$kategori = valid_enum(post('kategori_laporan'), ['Data kemiskinan', 'Bantuan belum diterima', 'Kondisi kesehatan', 'Data tidak sesuai', 'Lainnya'], 'Data kemiskinan');
$deskripsi = post('deskripsi');
$aktor = valid_enum(post('aktor_tujuan'), allowed_aktor_tujuan(), 'Dinas Sosial');
$status = valid_enum(post('status_laporan'), ['Masuk', 'Ditinjau', 'Diproses', 'Selesai', 'Dijadikan Data Masyarakat', 'Ditolak'], 'Masuk');
// Data lengkap calon masyarakat prasejahtera dari laporan
$dataNamaKK = post('data_nama_kepala_keluarga');
$dataNik = post('data_nik');
$dataTanggalLahir = post('data_tanggal_lahir') ?: null;
$dataPendidikan = valid_enum(post('data_pendidikan'), ['Tidak sekolah', 'SD', 'SMP', 'SMA/SMK', 'Diploma', 'S1', 'S2', 'S3', 'Tidak diketahui'], 'Tidak diketahui');
$dataRiwayat = post('data_riwayat_penyakit');
$dataJumlahKk = max(1, ipost('data_jumlah_kk', 1));
$dataAlamat = post('data_alamat');
$dataKecamatan = post('data_kecamatan');
$dataKelurahan = post('data_kelurahan');
$dataStatusBantuan = valid_enum(post('data_status_bantuan'), ['Belum menerima', 'Sudah menerima', 'Perlu verifikasi'], 'Belum menerima');
// Logika auto-terhubung:
// Jika NIK sama dengan data masyarakat yang sudah ada, sistem langsung menghubungkan laporan ke data tersebut.
// Jika NIK kosong, sistem mencoba mencocokkan dari nama kepala keluarga/warga yang dilaporkan.
// Status laporan tetap Masuk agar kebutuhan bantuan terbaru tetap terlihat di dashboard.
if (!$pendudukId) {
$matchedPendudukId = find_matching_penduduk_id($conn, $dataNamaKK, $dataNik);
$pendudukId = $matchedPendudukId > 0 ? $matchedPendudukId : null;
}
$lat = fpost('latitude');
$lng = fpost('longitude');
if ($nama === '' || $hp === '' || $deskripsi === '' || $dataNamaKK === '' || $dataAlamat === '' || $lat == 0 || $lng == 0) {
json_out(['status' => false, 'message' => 'Data laporan belum lengkap. Isi data pelapor, data warga yang dilaporkan, alamat, deskripsi, dan titik lokasi.']);
}
if ($action === 'add_laporan') {
$stmt = $conn->prepare("INSERT INTO laporan_masyarakat
(penduduk_id, nama_pelapor, no_hp, kategori_laporan, deskripsi, aktor_tujuan, status_laporan,
data_nama_kepala_keluarga, data_nik, data_tanggal_lahir, data_pendidikan, data_riwayat_penyakit,
data_jumlah_kk, data_alamat, data_kecamatan, data_kelurahan, data_status_bantuan,
latitude, longitude)
VALUES (?, ?, ?, ?, ?, ?, 'Masuk', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
if (!$stmt) {
json_out(['status' => false, 'message' => 'Gagal menyiapkan query laporan: ' . $conn->error]);
}
$stmt->bind_param("issssssssssissssdd", $pendudukId, $nama, $hp, $kategori, $deskripsi, $aktor,
$dataNamaKK, $dataNik, $dataTanggalLahir, $dataPendidikan, $dataRiwayat,
$dataJumlahKk, $dataAlamat, $dataKecamatan, $dataKelurahan, $dataStatusBantuan,
$lat, $lng);
} else {
if ($id <= 0) json_out(['status' => false, 'message' => 'ID laporan tidak valid']);
$stmt = $conn->prepare("UPDATE laporan_masyarakat SET
penduduk_id=?, nama_pelapor=?, no_hp=?, kategori_laporan=?, deskripsi=?, aktor_tujuan=?, status_laporan=?,
data_nama_kepala_keluarga=?, data_nik=?, data_tanggal_lahir=?, data_pendidikan=?, data_riwayat_penyakit=?,
data_jumlah_kk=?, data_alamat=?, data_kecamatan=?, data_kelurahan=?, data_status_bantuan=?,
latitude=?, longitude=?
WHERE id=?");
if (!$stmt) {
json_out(['status' => false, 'message' => 'Gagal menyiapkan query update laporan: ' . $conn->error]);
}
$stmt->bind_param("isssssssssssissssddi", $pendudukId, $nama, $hp, $kategori, $deskripsi, $aktor, $status,
$dataNamaKK, $dataNik, $dataTanggalLahir, $dataPendidikan, $dataRiwayat,
$dataJumlahKk, $dataAlamat, $dataKecamatan, $dataKelurahan, $dataStatusBantuan,
$lat, $lng, $id);
}
if ($stmt->execute()) {
$linkedText = $pendudukId
? ' Sistem mendeteksi nama/NIK yang sama, sehingga laporan otomatis terhubung ke data masyarakat prasejahtera yang sudah ada.'
: '';
json_out([
'status' => true,
'message' => ($action === 'add_laporan'
? 'Laporan berhasil dikirim dan menunggu tindak lanjut admin.'
: 'Laporan berhasil diupdate.') . $linkedText
]);
}
json_out([
'status' => false,
'message' => 'Gagal menyimpan laporan: ' . ($stmt ? $stmt->error : $conn->error)
]);
}
if ($action === 'update_position_laporan') {
require_admin();
$id = ipost('id');
$lat = fpost('latitude');
$lng = fpost('longitude');
$stmt = $conn->prepare("UPDATE laporan_masyarakat SET latitude=?, longitude=? WHERE id=?");
$stmt->bind_param("ddi", $lat, $lng, $id);
json_out($stmt->execute()
? ['status' => true, 'message' => 'Posisi laporan berhasil diupdate']
: ['status' => false, 'message' => 'Gagal update posisi laporan']
);
}
if ($action === 'update_status_laporan') {
require_admin();
$id = ipost('id');
$status = valid_enum(post('status_laporan'), ['Masuk', 'Ditinjau', 'Diproses', 'Selesai', 'Dijadikan Data Masyarakat', 'Ditolak'], 'Masuk');
$stmt = $conn->prepare("UPDATE laporan_masyarakat SET status_laporan=? WHERE id=?");
$stmt->bind_param("si", $status, $id);
json_out($stmt->execute()
? ['status' => true, 'message' => 'Status laporan berhasil diperbarui']
: ['status' => false, 'message' => 'Gagal update status laporan']
);
}
/* =========================
KONVERSI LAPORAN MENJADI DATA MASYARAKAT PRASEJAHTERA
========================= */
if ($action === 'convert_laporan_to_penduduk') {
require_admin();
$id = ipost('id');
if ($id <= 0) {
json_out(['status' => false, 'message' => 'ID laporan tidak valid']);
}
$stmt = $conn->prepare("SELECT * FROM laporan_masyarakat WHERE id=? LIMIT 1");
$stmt->bind_param("i", $id);
$stmt->execute();
$laporan = $stmt->get_result()->fetch_assoc();
if (!$laporan) {
json_out(['status' => false, 'message' => 'Laporan tidak ditemukan']);
}
if (($laporan['status_laporan'] ?? '') !== 'Selesai') {
json_out([
'status' => false,
'message' => 'Laporan belum bisa dijadikan data masyarakat. Ubah status laporan menjadi Selesai setelah laporan benar-benar valid.'
]);
}
if (!empty($laporan['penduduk_id'])) {
$update = $conn->prepare("UPDATE laporan_masyarakat SET status_laporan='Dijadikan Data Masyarakat' WHERE id=?");
$update->bind_param("i", $id);
$update->execute();
json_out([
'status' => true,
'message' => 'Laporan ini sudah terhubung dengan data masyarakat prasejahtera.'
]);
}
$namaKK = trim($laporan['data_nama_kepala_keluarga'] ?? '');
$alamat = trim($laporan['data_alamat'] ?? '');
$matchedPendudukId = find_matching_penduduk_id($conn, $namaKK, $laporan['data_nik'] ?? '');
if ($matchedPendudukId > 0) {
$update = $conn->prepare("UPDATE laporan_masyarakat
SET penduduk_id=?, status_laporan='Dijadikan Data Masyarakat'
WHERE id=?");
$update->bind_param("ii", $matchedPendudukId, $id);
$update->execute();
json_out([
'status' => true,
'message' => 'Nama/NIK pada laporan cocok dengan data masyarakat yang sudah ada. Laporan otomatis dihubungkan, sehingga tidak membuat data masyarakat ganda.'
]);
}
if ($namaKK === '' || $alamat === '') {
json_out([
'status' => false,
'message' => 'Data warga pada laporan belum lengkap. Isi nama kepala keluarga/warga dan alamat terlebih dahulu.'
]);
}
$nik = $laporan['data_nik'] ?? null;
$tanggalLahir = $laporan['data_tanggal_lahir'] ?: null;
$pendidikan = $laporan['data_pendidikan'] ?: 'Tidak diketahui';
$riwayat = $laporan['data_riwayat_penyakit'] ?? '';
$jumlahKk = (int)($laporan['data_jumlah_kk'] ?? 1);
if ($jumlahKk <= 0) $jumlahKk = 1;
$kecamatan = $laporan['data_kecamatan'] ?? '';
$kelurahan = $laporan['data_kelurahan'] ?? '';
$statusBantuan = $laporan['data_status_bantuan'] ?: 'Belum menerima';
$lat = (float)$laporan['latitude'];
$lng = (float)$laporan['longitude'];
$insert = $conn->prepare("INSERT INTO penduduk_kemiskinan
(nama_kepala_keluarga, nik, tanggal_lahir, pendidikan, riwayat_penyakit, jumlah_kk, alamat, kecamatan, kelurahan, status_bantuan, latitude, longitude)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$insert->bind_param("sssssissssdd", $namaKK, $nik, $tanggalLahir, $pendidikan, $riwayat, $jumlahKk, $alamat, $kecamatan, $kelurahan, $statusBantuan, $lat, $lng);
if (!$insert->execute()) {
json_out(['status' => false, 'message' => 'Gagal menjadikan laporan sebagai data masyarakat prasejahtera']);
}
$pendudukIdBaru = $conn->insert_id;
$update = $conn->prepare("UPDATE laporan_masyarakat
SET penduduk_id=?, status_laporan='Dijadikan Data Masyarakat'
WHERE id=?");
$update->bind_param("ii", $pendudukIdBaru, $id);
$update->execute();
json_out([
'status' => true,
'message' => 'Laporan berhasil dijadikan data masyarakat prasejahtera. Bubble laporan akan disembunyikan dari peta, tetapi tetap tersimpan di dashboard/statistik.'
]);
}
/* =========================
HUBUNGKAN LAPORAN DENGAN DATA MASYARAKAT YANG SUDAH ADA
========================= */
if ($action === 'link_laporan_to_penduduk') {
require_admin();
$id = ipost('id');
$pendudukId = ipost('penduduk_id');
if ($id <= 0 || $pendudukId <= 0) {
json_out(['status' => false, 'message' => 'Pilih laporan dan data masyarakat yang valid']);
}
$cekLaporan = $conn->prepare("SELECT id, status_laporan, penduduk_id FROM laporan_masyarakat WHERE id=? LIMIT 1");
$cekLaporan->bind_param("i", $id);
$cekLaporan->execute();
$laporan = $cekLaporan->get_result()->fetch_assoc();
if (!$laporan) {
json_out(['status' => false, 'message' => 'Laporan tidak ditemukan']);
}
if (($laporan['status_laporan'] ?? '') !== 'Selesai') {
json_out([
'status' => false,
'message' => 'Laporan belum bisa dihubungkan. Ubah status laporan menjadi Selesai setelah laporan benar-benar valid.'
]);
}
if (!empty($laporan['penduduk_id'])) {
json_out([
'status' => true,
'message' => 'Laporan ini sudah terhubung dengan data masyarakat prasejahtera.'
]);
}
$cekPenduduk = $conn->prepare("SELECT id FROM penduduk_kemiskinan WHERE id=? LIMIT 1");
$cekPenduduk->bind_param("i", $pendudukId);
$cekPenduduk->execute();
if (!$cekPenduduk->get_result()->fetch_assoc()) {
json_out(['status' => false, 'message' => 'Data masyarakat prasejahtera tidak ditemukan']);
}
$stmt = $conn->prepare("UPDATE laporan_masyarakat SET penduduk_id=?, status_laporan='Dijadikan Data Masyarakat' WHERE id=?");
$stmt->bind_param("ii", $pendudukId, $id);
json_out($stmt->execute()
? ['status' => true, 'message' => 'Laporan berhasil dihubungkan ke data masyarakat yang sudah ada. Laporan tetap tersimpan untuk arsip/statistik, tetapi bubble laporan disembunyikan dari peta.']
: ['status' => false, 'message' => 'Gagal menghubungkan laporan ke data masyarakat']
);
}
/* =========================
HISTORI BANTUAN
========================= */
if ($action === 'add_histori') {
require_histori_write();
$pendudukId = ipost('penduduk_id');
$masjidIdInput = ipost('masjid_id');
$masjidId = $masjidIdInput > 0 ? $masjidIdInput : null;
$jenis = post('jenis_bantuan');
$tanggal = post('tanggal_bantuan');
$nominal = fpost('nominal');
$sumber = post('sumber_bantuan');
if ($sumber === '' && $masjidId) {
$stmtMasjid = $conn->prepare("SELECT nama_masjid FROM masjid WHERE id=? LIMIT 1");
$stmtMasjid->bind_param("i", $masjidId);
$stmtMasjid->execute();
$rowMasjid = $stmtMasjid->get_result()->fetch_assoc();
if ($rowMasjid) {
$sumber = $rowMasjid['nama_masjid'];
}
}
if ($sumber === '') {
$sumber = is_dinas() ? (current_instansi() ?: 'Dinas Terkait') : 'Pemkot Pontianak / Rumah Ibadah / Donatur';
}
$keterangan = post('keterangan');
if ($pendudukId <= 0 || $jenis === '' || $tanggal === '') {
json_out(['status' => false, 'message' => 'Data histori bantuan belum lengkap']);
}
$stmt = $conn->prepare("INSERT INTO histori_bantuan
(penduduk_id, masjid_id, jenis_bantuan, tanggal_bantuan, nominal, sumber_bantuan, keterangan)
VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param("iissdss", $pendudukId, $masjidId, $jenis, $tanggal, $nominal, $sumber, $keterangan);
if ($stmt->execute()) {
$updateStatus = $conn->prepare("UPDATE penduduk_kemiskinan SET status_bantuan='Sudah menerima' WHERE id=?");
$updateStatus->bind_param("i", $pendudukId);
$updateStatus->execute();
json_out([
'status' => true,
'message' => 'Histori bantuan berhasil ditambahkan dan status bantuan warga menjadi Sudah menerima'
]);
}
json_out([
'status' => false,
'message' => 'Gagal menyimpan histori bantuan'
]);
}
/* =========================
KELOLA AKUN ROLE
========================= */
if ($action === 'add_user') {
require_admin();
$nama = post('nama');
$username = post('username');
$password = $_POST['password'] ?? '';
$role = valid_enum(post('role'), ['admin', 'walikota', 'dinas'], 'dinas');
$instansi = post('instansi');
if ($nama === '' || $username === '' || $password === '') {
json_out(['status' => false, 'message' => 'Nama, username, dan password wajib diisi']);
}
if ($role === 'walikota') {
$instansi = 'Wali Kota';
} elseif ($role === 'admin') {
$instansi = 'Admin Pengelola';
} elseif ($instansi === '') {
$instansi = 'Dinas Sosial';
}
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $conn->prepare("INSERT INTO users (nama, username, password, role, instansi) VALUES (?, ?, ?, ?, ?)");
$stmt->bind_param("sssss", $nama, $username, $hash, $role, $instansi);
json_out($stmt->execute()
? ['status' => true, 'message' => 'Akun berhasil ditambahkan']
: ['status' => false, 'message' => 'Gagal menambahkan akun. Username mungkin sudah digunakan.']
);
}
if ($action === 'delete_user') {
require_admin();
$id = ipost('id');
if ($id <= 0) json_out(['status' => false, 'message' => 'ID akun tidak valid']);
if ($id === (int)($_SESSION['user']['id'] ?? 0)) {
json_out(['status' => false, 'message' => 'Akun yang sedang login tidak boleh dihapus']);
}
$stmt = $conn->prepare("DELETE FROM users WHERE id=?");
$stmt->bind_param("i", $id);
json_out($stmt->execute()
? ['status' => true, 'message' => 'Akun berhasil dihapus']
: ['status' => false, 'message' => 'Gagal menghapus akun']
);
}
/* =========================
DELETE GENERIC
========================= */
if ($action === 'delete') {
require_admin();
$allowed = [
'penduduk_kemiskinan',
'masjid',
'laporan_masyarakat',
'histori_bantuan'
];
$table = post('table');
$id = ipost('id');
if (!in_array($table, $allowed, true)) {
json_out(['status' => false, 'message' => 'Tabel tidak valid']);
}
$stmt = $conn->prepare("DELETE FROM $table WHERE id=?");
$stmt->bind_param("i", $id);
json_out($stmt->execute()
? ['status' => true, 'message' => 'Data berhasil dihapus']
: ['status' => false, 'message' => 'Gagal menghapus data']
);
}
json_out(['status' => false, 'message' => 'Action tidak dikenal']);
}
$admin = is_admin();
$loggedIn = is_logged_in();
$canViewDashboard = can_view_dashboard();
$roleLabel = ['admin' => 'Admin Pengelola', 'walikota' => 'Wali Kota', 'dinas' => 'Dinas Terkait'][$_SESSION['user']['role'] ?? ''] ?? 'Masyarakat Umum';
$instansiLabel = current_instansi();
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Prasejahtera Pontianak</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css">
<link rel="stylesheet" href="style.css?v=map-left-right-1">
</head>
<body class="public-map-body">
<div class="public-page">
<header class="public-header">
<div class="public-header-inner">
<div>
<p class="eyebrow">Sistem Informasi Geografis</p>
<h1>WebGIS Prasejahtera Pontianak</h1>
<p class="subtitle">
Pemetaan masyarakat prasejahtera, rumah ibadah, laporan cepat, dan histori bantuan berbasis lokasi.
</p>
<p class="user-status">
<?= $loggedIn ? 'Login: ' . htmlspecialchars($_SESSION['user']['nama']) . ' (' . htmlspecialchars($roleLabel) . ($instansiLabel ? ' - ' . $instansiLabel : '') . ')' : 'Mode: Masyarakat Umum' ?>
</p>
</div>
<div class="header-actions">
<?php if ($loggedIn): ?>
<a href="dashboard.php">Dashboard</a>
<a href="?logout=1">Logout</a>
<?php else: ?>
<button type="button" onclick="openLogin()">Login</button>
<?php endif; ?>
</div>
</div>
</header>
<main class="public-main">
<section class="map-card">
<div class="map-card-header">
<div>
<h2>Peta WebGIS Poverty Mapping</h2>
<p>
Pilih mode, klik titik pada peta, lalu isi form pada panel kanan sesuai kebutuhan data.
</p>
</div>
<div class="mini-legend">
<span><i class="legend-dot poor-out"></i>Luar radius</span>
<span><i class="legend-dot poor-in"></i>Dalam radius</span>
</div>
</div>
<div class="map-layout-grid">
<div class="map-area">
<div id="map"></div>
</div>
<aside class="side-control-panel">
<div class="mode-panel">
<b>Mode Tambah Point</b>
<select id="pointMode">
<?php if ($admin): ?>
<option value="penduduk">Masyarakat Prasejahtera</option>
<option value="masjid">Rumah Ibadah</option>
<?php elseif ($loggedIn): ?>
<option value="">Mode pemantauan data</option>
<?php else: ?>
<option value="laporan">Pelaporan Cepat</option>
<?php endif; ?>
</select>
<small>
<?php if ($admin): ?>
Admin mengelola data masyarakat prasejahtera, rumah ibadah, radius bantuan, laporan cepat, dan histori bantuan.
<?php elseif ($loggedIn): ?>
Wali Kota dan Dinas dapat memantau serta mengunduh data sesuai hak akses masing-masing.
<?php else: ?>
Masyarakat umum dapat mengirim laporan cepat berbasis lokasi melalui peta.
<?php endif; ?>
</small>
</div>
<?php if (!$loggedIn): ?>
<div id="loginBox" class="login-box hidden">
<form method="POST">
<h3>Login Sistem</h3>
<?php if (isset($login_error)): ?>
<p class="err"><?= htmlspecialchars($login_error) ?></p>
<?php endif; ?>
<input name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<div class="btn-group">
<button name="login" value="1">Login</button>
<button type="button" class="btn-secondary" onclick="closeLogin()">Batal</button>
</div>
</form>
</div>
<?php endif; ?>
<div id="formPanel" class="form-panel hidden">
<h3 id="formTitle">Form</h3>
<div id="formContent"></div>
<div class="btn-group">
<button id="saveBtn">Simpan</button>
<button class="btn-secondary" onclick="hideForm()">Batal</button>
</div>
</div>
<?php if ($canViewDashboard): ?>
<div class="dashboard-link-panel">
<b>Dashboard Data</b>
<p>
Lihat tabel laporan, histori bantuan, masyarakat prasejahtera, rumah ibadah,
ringkasan data, dan menu download pada halaman dashboard.
</p>
<a href="dashboard.php">Buka Dashboard</a>
</div>
<?php endif; ?>
</aside>
</div>
</section>
</main>
<footer class="public-footer">
<b>WebGIS Kemiskinan Pontianak</b>
<span>© 2026 - Talitha Syakirah Artanti | D1041231013 | Sistem Informasi Geografis</span>
</footer>
</div>
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.geometryutil/0.10.3/leaflet.geometryutil.min.js"></script>
<script>
const IS_ADMIN = <?= $admin ? 'true' : 'false' ?>;
const CAN_VIEW_DASHBOARD = <?= $canViewDashboard ? 'true' : 'false' ?>;
const CAN_ADD_HISTORI = <?= can_add_histori() ? 'true' : 'false' ?>;
const USER_ROLE = '<?= htmlspecialchars($_SESSION['user']['role'] ?? 'masyarakat', ENT_QUOTES) ?>';
const USER_INSTANSI = '<?= htmlspecialchars(current_instansi(), ENT_QUOTES) ?>';
const AKTOR_OPTIONS = ['RT/RW', 'Kelurahan', 'Kecamatan', 'Dinas Sosial', 'Dinas Kesehatan', 'Wali Kota'];
let allData = {};
let tempMarker = null;
let currentType = '';
let currentMode = 'add';
let currentId = null;
let currentLat = 0;
let currentLng = 0;
let currentLayer = null;
const map = L.map('map').setView([-0.0263, 109.3425], 13);
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 20,
attribution: '&copy; OpenStreetMap'
});
const osmHot = L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
maxZoom: 20,
attribution: '&copy; OpenStreetMap, HOT'
});
osm.addTo(map);
// Refresh ukuran peta setelah layout landing page selesai dirender
window.addEventListener('load', () => {
setTimeout(() => map.invalidateSize(), 300);
});
const layers = {
penduduk: L.layerGroup().addTo(map),
masjid: L.layerGroup().addTo(map),
radius: L.layerGroup().addTo(map),
laporan: L.layerGroup().addTo(map)
};
const baseMaps = {
'OpenStreetMap': osm,
'OpenStreetMap HOT': osmHot
};
const overlayMaps = {
'Masyarakat Prasejahtera': layers.penduduk,
'Point Rumah Ibadah': layers.masjid,
'Radius Rumah Ibadah': layers.radius,
'Pelaporan Cepat': layers.laporan
};
L.control.layers(baseMaps, overlayMaps, {
collapsed: false
}).addTo(map);
function openLogin() {
document.getElementById('loginBox')?.classList.remove('hidden');
}
function closeLogin() {
document.getElementById('loginBox')?.classList.add('hidden');
}
function esc(value) {
return String(value ?? '').replace(/[&<>"]/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;'
}[char]));
}
function icon(url) {
return L.icon({
iconUrl: url,
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34]
});
}
const icons = {
green: icon('https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png'),
red: icon('https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png'),
blue: icon('https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-blue.png'),
orange: icon('https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-orange.png'),
violet: icon('https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-violet.png')
};
function svgPinIcon(svgInner, color = '#198754', size = [36, 48]) {
const width = size[0];
const height = size[1];
const html = `
<svg class="svg-marker" width="${width}" height="${height}" viewBox="0 0 36 48" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M18 47C18 47 3 30.5 3 17.5C3 8.4 9.7 2 18 2C26.3 2 33 8.4 33 17.5C33 30.5 18 47 18 47Z"
fill="${color}" stroke="#ffffff" stroke-width="2.4"/>
<circle cx="18" cy="17.5" r="10.3" fill="#ffffff"/>
${svgInner}
</svg>
`;
return L.divIcon({
className: 'svg-map-icon',
html: html,
iconSize: [width, height],
iconAnchor: [width / 2, height],
popupAnchor: [0, -height + 6]
});
}
function personSvgInner(color = '#111111') {
return `
<circle cx="18" cy="13.8" r="4.2" fill="${color}"/>
<path d="M10.7 26C11.6 21.9 14.3 19.4 18 19.4C21.7 19.4 24.4 21.9 25.3 26Z" fill="${color}"/>
`;
}
function createKemiskinanIcon(isInsideRadius = false) {
const color = isInsideRadius ? '#198754' : '#dc3545';
return svgPinIcon(personSvgInner('#111111'), color, [36, 48]);
}
function getRumahIbadahColor(jenis = 'Masjid') {
const colorMap = {
'Masjid': '#159447',
'Gereja': '#1f6ed4',
'Vihara': '#7b35c8',
'Kelenteng': '#dc3545',
'Pura': '#d8a100'
};
return colorMap[jenis] || '#159447';
}
function getRumahIbadahSymbol(jenis = 'Masjid') {
const symbolMap = {
'Masjid': '🕌',
'Gereja': '⛪',
'Vihara': '☸️',
'Kelenteng': '⛩️',
'Pura': '🛕'
};
return symbolMap[jenis] || '📍';
}
function createMasjidIcon(jenis = 'Masjid') {
const color = getRumahIbadahColor(jenis);
const symbol = getRumahIbadahSymbol(jenis);
const fontSize = jenis === 'Kelenteng' ? 12 : 16;
const svgInner = `
<text x="18" y="22.6" text-anchor="middle" font-size="${fontSize}" font-weight="900"
font-family="Arial, Helvetica, sans-serif" fill="${color}">${symbol}</text>
`;
return svgPinIcon(svgInner, color, [36, 48]);
}
function laporanSvgInner(color = '#111111') {
return `
<path d="M10 12.5C10 10.6 11.6 9 13.5 9H22.5C24.4 9 26 10.6 26 12.5V18.5C26 20.4 24.4 22 22.5 22H17L12 26V22H13.5C11.6 22 10 20.4 10 18.5V12.5Z"
fill="${color}"/>
<circle cx="14" cy="15.5" r="1.3" fill="#ffffff"/>
<circle cx="18" cy="15.5" r="1.3" fill="#ffffff"/>
<circle cx="22" cy="15.5" r="1.3" fill="#ffffff"/>
`;
}
function createLaporanIcon(status = 'Masuk') {
let color = '#f59e0b';
if (status === 'Ditinjau') {
color = '#7b35c8';
} else if (status === 'Diproses') {
color = '#0d6efd';
} else if (status === 'Selesai') {
color = '#198754';
} else if (status === 'Dijadikan Data Masyarakat') {
color = '#198754';
} else if (status === 'Ditolak') {
color = '#dc3545';
}
return svgPinIcon(laporanSvgInner('#111111'), color, [36, 48]);
}
function getJalanColor(status) {
if (status === 'Jalan Nasional') return 'red';
if (status === 'Jalan Provinsi') return 'blue';
if (status === 'Jalan Kabupaten') return 'green';
return 'black';
}
function getParsilColor(status) {
if (status === 'SHM') return '#2ecc71';
if (status === 'HGB') return '#f39c12';
if (status === 'HGU') return '#8e44ad';
if (status === 'HP') return '#e74c3c';
return '#666666';
}
function getWorshipColor(jenis) {
const colors = {
'Masjid': 'green',
'Gereja': 'blue',
'Vihara': 'violet',
'Kelenteng': 'red',
'Pura': 'orange'
};
return colors[jenis] || 'green';
}
function isInsideAnyRadius(item) {
const point = L.latLng(parseFloat(item.latitude), parseFloat(item.longitude));
for (const rumah of allData.masjid || []) {
const center = L.latLng(parseFloat(rumah.latitude), parseFloat(rumah.longitude));
const radius = parseInt(rumah.radius_meter) || 500;
if (point.distanceTo(center) <= radius) {
return true;
}
}
return false;
}
function clearTemporaryObjects() {
if (tempMarker) {
map.removeLayer(tempMarker);
tempMarker = null;
}
}
function hideForm() {
document.getElementById('formPanel').classList.add('hidden');
if (currentMode === 'add') {
clearTemporaryObjects();
}
currentType = '';
currentMode = 'add';
currentId = null;
currentLayer = null;
}
function getFields(type, data = {}) {
if (type === 'penduduk') {
return `
<input id="nama_kepala_keluarga" placeholder="Nama Kepala Keluarga" value="${esc(data.nama_kepala_keluarga)}">
<input id="nik" placeholder="NIK (opsional)" value="${esc(data.nik)}">
<input id="tanggal_lahir" type="date" value="${esc(data.tanggal_lahir)}">
<select id="pendidikan">
${optionSet(['Tidak diketahui', 'Tidak sekolah', 'SD', 'SMP', 'SMA/SMK', 'Diploma', 'S1', 'S2', 'S3'], data.pendidikan || 'Tidak diketahui')}
</select>
<textarea id="riwayat_penyakit" placeholder="Riwayat penyakit (opsional)">${esc(data.riwayat_penyakit)}</textarea>
<input id="jumlah_kk" type="number" min="1" value="${esc(data.jumlah_kk || 1)}" placeholder="Jumlah KK">
<input id="alamat" placeholder="Alamat" value="${esc(data.alamat)}">
<input id="kecamatan" placeholder="Kecamatan" value="${esc(data.kecamatan)}">
<input id="kelurahan" placeholder="Kelurahan" value="${esc(data.kelurahan)}">
<select id="status_bantuan">
${optionSet(['Belum menerima', 'Sudah menerima', 'Perlu verifikasi'], data.status_bantuan || 'Belum menerima')}
</select>
<input id="latitude" readonly placeholder="Latitude" value="${esc(data.latitude ?? currentLat)}">
<input id="longitude" readonly placeholder="Longitude" value="${esc(data.longitude ?? currentLng)}">
`;
}
if (type === 'masjid') {
return `
<select id="jenis_rumah_ibadah">
${optionSet(['Masjid', 'Gereja', 'Vihara', 'Kelenteng', 'Pura'], data.jenis_rumah_ibadah || 'Masjid')}
</select>
<input id="nama_masjid" placeholder="Nama Rumah Ibadah" value="${esc(data.nama_masjid)}">
<input id="pic_masjid" placeholder="PIC Rumah Ibadah" value="${esc(data.pic_masjid)}">
<input id="alamat_masjid" placeholder="Alamat Rumah Ibadah" value="${esc(data.alamat_masjid)}">
<input id="radius_meter" type="number" min="100" max="5000" value="${esc(data.radius_meter || 500)}" placeholder="Radius meter">
<input id="latitude" readonly placeholder="Latitude" value="${esc(data.latitude ?? currentLat)}">
<input id="longitude" readonly placeholder="Longitude" value="${esc(data.longitude ?? currentLng)}">
`;
}
if (type === 'laporan') {
return `
<div class="form-note">
Data yang tampil di peta publik hanya data umum laporan. Data lengkap warga digunakan admin untuk membuat data masyarakat prasejahtera. Jika warga yang sama membutuhkan bantuan lagi, tetap buat laporan baru; admin akan menghubungkan laporan ini ke data masyarakat yang sudah ada.
</div>
${IS_ADMIN ? `
<select id="penduduk_id">
<option value="">-- Belum menjadi data masyarakat --</option>
${(allData.penduduk_kemiskinan || []).map(item => `<option value="${item.id}" ${String(data.penduduk_id || '') === String(item.id) ? 'selected' : ''}>${esc(item.nama_kepala_keluarga)}</option>`).join('')}
</select>
` : ''}
<input id="nama_pelapor" placeholder="Nama Pelapor" value="${esc(data.nama_pelapor)}">
<input id="no_hp" placeholder="No HP Pelapor" value="${esc(data.no_hp)}">
<select id="kategori_laporan">
${optionSet(['Data kemiskinan', 'Bantuan belum diterima', 'Kondisi kesehatan', 'Data tidak sesuai', 'Lainnya'], data.kategori_laporan || 'Data kemiskinan')}
</select>
<select id="aktor_tujuan">
${optionSet(AKTOR_OPTIONS, data.aktor_tujuan || 'Dinas Sosial')}
</select>
${IS_ADMIN ? `<select id="status_laporan">${optionSet(['Masuk', 'Ditinjau', 'Diproses', 'Selesai', 'Dijadikan Data Masyarakat', 'Ditolak'], data.status_laporan || 'Masuk')}</select>` : ''}
<textarea id="deskripsi" placeholder="Deskripsi kondisi singkat yang boleh tampil di peta">${esc(data.deskripsi)}</textarea>
<hr class="form-divider">
<b class="form-subtitle">Data Lengkap Warga yang Dilaporkan</b>
<input id="data_nama_kepala_keluarga" placeholder="Nama Kepala Keluarga / Warga yang Dilaporkan" value="${esc(data.data_nama_kepala_keluarga)}">
<input id="data_nik" placeholder="NIK (opsional)" value="${esc(data.data_nik)}">
<input id="data_tanggal_lahir" type="date" value="${esc(data.data_tanggal_lahir)}">
<select id="data_pendidikan">
${optionSet(['Tidak diketahui', 'Tidak sekolah', 'SD', 'SMP', 'SMA/SMK', 'Diploma', 'S1', 'S2', 'S3'], data.data_pendidikan || 'Tidak diketahui')}
</select>
<textarea id="data_riwayat_penyakit" placeholder="Riwayat penyakit / kebutuhan kesehatan (opsional)">${esc(data.data_riwayat_penyakit)}</textarea>
<input id="data_jumlah_kk" type="number" min="1" value="${esc(data.data_jumlah_kk || 1)}" placeholder="Jumlah KK">
<input id="data_alamat" placeholder="Alamat lengkap warga yang dilaporkan" value="${esc(data.data_alamat)}">
<input id="data_kecamatan" placeholder="Kecamatan" value="${esc(data.data_kecamatan)}">
<input id="data_kelurahan" placeholder="Kelurahan" value="${esc(data.data_kelurahan)}">
<select id="data_status_bantuan">
${optionSet(['Belum menerima', 'Sudah menerima', 'Perlu verifikasi'], data.data_status_bantuan || 'Belum menerima')}
</select>
<input id="latitude" readonly placeholder="Latitude" value="${esc(data.latitude ?? currentLat)}">
<input id="longitude" readonly placeholder="Longitude" value="${esc(data.longitude ?? currentLng)}">
`;
}
return '';
}
function optionSet(options, selected) {
return options.map(option => `
<option value="${esc(option)}" ${String(option) === String(selected) ? 'selected' : ''}>
${esc(option)}
</option>
`).join('');
}
function showForm(type, mode = 'add', data = {}, layer = null, lat = null, lng = null) {
currentType = type;
currentMode = mode;
currentId = data.id || null;
currentLayer = layer;
currentLat = lat ?? parseFloat(data.latitude || 0);
currentLng = lng ?? parseFloat(data.longitude || 0);
let titleMap = {
penduduk: 'Data Masyarakat Prasejahtera',
masjid: 'Data Rumah Ibadah',
laporan: 'Laporan Cepat Masyarakat',
};
document.getElementById('formTitle').textContent = (mode === 'add' ? 'Tambah ' : 'Edit ') + titleMap[type];
document.getElementById('formContent').innerHTML = getFields(type, data);
document.getElementById('formPanel').classList.remove('hidden');
}
function collectFormData() {
const formData = new URLSearchParams();
if (currentMode === 'edit') {
formData.append('id', currentId);
}
document.querySelectorAll('#formContent input, #formContent select, #formContent textarea').forEach(element => {
if (!element.id) return;
if (element.type === 'checkbox') {
formData.append(element.id, element.checked ? 1 : 0);
} else {
formData.append(element.id, element.value);
}
});
return formData;
}
document.getElementById('saveBtn').addEventListener('click', function () {
if (!currentType) {
alert('Pilih data yang ingin dikelola terlebih dahulu.');
return;
}
const action = (currentMode === 'edit' ? 'update_' : 'add_') + currentType;
const formData = collectFormData();
fetch('index.php?action=' + action, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(result => {
alert(result.message);
if (result.status) {
hideForm();
loadAll();
}
})
.catch(error => {
console.error(error);
alert('Terjadi kesalahan saat menyimpan data.');
});
});
map.on('click', function (event) {
const type = document.getElementById('pointMode').value;
if (type === '') {
return;
}
if (IS_ADMIN && type === 'laporan') {
alert('Laporan cepat hanya dibuat oleh masyarakat umum. Admin cukup memperbarui status laporan yang masuk.');
return;
}
if (!IS_ADMIN && type !== 'laporan') {
return;
}
if (tempMarker) {
map.removeLayer(tempMarker);
}
const latlng = event.latlng;
tempMarker = L.marker(latlng, {
icon: type === 'laporan' ? createLaporanIcon('Masuk') : icons.blue
}).addTo(map);
showForm(type, 'add', {}, tempMarker, latlng.lat, latlng.lng);
});
function updatePosition(type, id, latlng) {
if (!IS_ADMIN) return;
const formData = new URLSearchParams();
formData.append('id', id);
formData.append('latitude', latlng.lat);
formData.append('longitude', latlng.lng);
fetch('index.php?action=update_position_' + type, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(result => {
console.log(result.message);
loadAll();
});
}
/* =========================
POPUP ACTIONS
========================= */
window.editPenduduk = function (id) {
const data = allData.penduduk_kemiskinan.find(item => String(item.id) === String(id));
if (data) showForm('penduduk', 'edit', data);
};
window.editMasjid = function (id) {
const data = allData.masjid.find(item => String(item.id) === String(id));
if (data) showForm('masjid', 'edit', data);
};
window.editLaporan = function (id) {
const data = allData.laporan_masyarakat.find(item => String(item.id) === String(id));
if (data) showForm('laporan', 'edit', data);
};
window.deleteData = function (table, id) {
if (!confirm('Yakin ingin menghapus data ini?')) return;
const formData = new URLSearchParams();
formData.append('table', table);
formData.append('id', id);
fetch('index.php?action=delete', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(result => {
alert(result.message);
loadAll();
});
};
window.updateStatusLaporan = function (id) {
if (!IS_ADMIN) return;
const status = document.getElementById('status_laporan_' + id).value;
const formData = new URLSearchParams();
formData.append('id', id);
formData.append('status_laporan', status);
fetch('index.php?action=update_status_laporan', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(result => {
alert(result.message);
loadAll();
});
};
window.addHistori = function () {
if (!IS_ADMIN) return;
const fields = [
'penduduk_id',
'masjid_id',
'jenis_bantuan',
'tanggal_bantuan',
'nominal',
'sumber_bantuan',
'keterangan'
];
const formData = new URLSearchParams();
fields.forEach(id => {
formData.append(id, document.getElementById(id).value);
});
fetch('index.php?action=add_histori', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(result => {
alert(result.message);
loadAll();
});
};
function createActionButtons(type, id) {
if (!IS_ADMIN) return '';
const mapTable = {
penduduk: 'penduduk_kemiskinan',
masjid: 'masjid',
laporan: 'laporan_masyarakat'
};
const editFunction = {
penduduk: 'editPenduduk',
masjid: 'editMasjid',
laporan: 'editLaporan'
};
if (type === 'laporan') {
return `
<br><br>
<button class="danger" onclick="deleteData('${mapTable[type]}', ${id})">Hapus</button>
`;
}
return `
<br><br>
<button onclick="${editFunction[type]}(${id})">Edit Data</button>
<button class="danger" onclick="deleteData('${mapTable[type]}', ${id})">Hapus</button>
`;
}
function loadAll() {
fetch('index.php?action=get_all')
.then(async response => {
const text = await response.text();
try {
return JSON.parse(text);
} catch (error) {
console.error('Response bukan JSON. Cek output PHP:', text);
return {
penduduk_kemiskinan: [],
masjid: [],
laporan_masyarakat: [],
histori_bantuan: [],
users: []
};
}
})
.then(data => {
allData = data || {};
Object.values(layers).forEach(layer => layer.clearLayers());
(data.masjid || []).forEach(item => {
const jenis = item.jenis_rumah_ibadah || 'Masjid';
const radiusColor = getRumahIbadahColor(jenis);
const marker = L.marker([item.latitude, item.longitude], {
icon: createMasjidIcon(jenis),
draggable: IS_ADMIN
});
marker.bindPopup(`
<b>${esc(item.nama_masjid)}</b><br>
Jenis: ${esc(jenis)}<br>
PIC: ${esc(item.pic_masjid)}<br>
Alamat: ${esc(item.alamat_masjid)}<br>
Radius: ${esc(item.radius_meter)} meter<br>
Lat: ${esc(item.latitude)}<br>
Lng: ${esc(item.longitude)}
${createActionButtons('masjid', item.id)}
`);
marker.on('dragend', event => updatePosition('masjid', item.id, event.target.getLatLng()));
marker.addTo(layers.masjid);
L.circle([item.latitude, item.longitude], {
radius: parseInt(item.radius_meter),
color: radiusColor,
fillColor: radiusColor,
fillOpacity: 0.12,
weight: 2
}).addTo(layers.radius);
});
(data.penduduk_kemiskinan || []).forEach(item => {
const inside = isInsideAnyRadius(item);
const statusRadius = inside
? '<span style="color:#198754;font-weight:bold;">Masuk radius rumah ibadah</span>'
: '<span style="color:#dc3545;font-weight:bold;">Di luar radius rumah ibadah</span>';
const marker = L.marker([item.latitude, item.longitude], {
icon: createKemiskinanIcon(inside),
draggable: IS_ADMIN
});
marker.bindPopup(`
<b>${esc(item.nama_kepala_keluarga)}</b><br>
Jumlah KK: ${esc(item.jumlah_kk)}<br>
Alamat: ${esc(item.alamat)}<br>
Status: ${statusRadius}<br>
Lat: ${esc(item.latitude)}<br>
Lng: ${esc(item.longitude)}
${createActionButtons('penduduk', item.id)}
`);
marker.on('dragend', event => updatePosition('penduduk', item.id, event.target.getLatLng()));
marker.addTo(layers.penduduk);
});
(data.laporan_masyarakat || []).forEach(item => {
const hiddenStatuses = ['Selesai', 'Dijadikan Data Masyarakat', 'Ditolak'];
// Laporan yang sudah selesai/dijadikan data masyarakat tetap tersimpan di dashboard,
// tetapi tidak ditampilkan sebagai bubble aktif agar tidak menumpuk dengan marker masyarakat prasejahtera.
if (hiddenStatuses.includes(item.status_laporan)) {
return;
}
const marker = L.marker([item.latitude, item.longitude], {
icon: createLaporanIcon(item.status_laporan),
draggable: IS_ADMIN
});
marker.bindPopup(`
<b>${esc(item.kategori_laporan)}</b><br>
Pelapor: ${esc(item.nama_pelapor)}<br>
Tujuan: ${esc(item.aktor_tujuan)}<br>
Status: ${esc(item.status_laporan)}<br>
${esc(item.deskripsi)}
${createActionButtons('laporan', item.id)}
`);
marker.on('dragend', event => updatePosition('laporan', item.id, event.target.getLatLng()));
marker.addTo(layers.laporan);
});
// Dashboard sekarang berada di dashboard.php.
// Tidak perlu memanggil showTab() di halaman peta.
})
.catch(error => {
console.error('Gagal memuat data peta:', error);
allData = {
penduduk_kemiskinan: [],
masjid: [],
laporan_masyarakat: [],
histori_bantuan: [],
users: []
};
});
}
function addUser() {
if (!IS_ADMIN) return;
const formData = new FormData();
formData.append('nama', document.getElementById('user_nama').value);
formData.append('username', document.getElementById('user_username').value);
formData.append('password', document.getElementById('user_password').value);
formData.append('role', document.getElementById('user_role').value);
formData.append('instansi', document.getElementById('user_instansi').value);
fetch('index.php?action=add_user', { method: 'POST', body: formData })
.then(res => res.json())
.then(res => {
alert(res.message);
if (res.status) loadAll();
});
}
function deleteUser(id) {
if (!IS_ADMIN) return;
if (!confirm('Hapus akun ini?')) return;
const formData = new FormData();
formData.append('id', id);
fetch('index.php?action=delete_user', { method: 'POST', body: formData })
.then(res => res.json())
.then(res => {
alert(res.message);
if (res.status) loadAll();
});
}
function showTab(tabName) {
if (!CAN_VIEW_DASHBOARD) return;
const content = document.getElementById('tableContent');
if (!content) return;
if (tabName === 'laporan') {
content.innerHTML = `
<h3>${USER_ROLE === 'dinas' ? 'Laporan Instansi' : 'Laporan Cepat'} <a href="?download=laporan">Download CSV</a></h3>
${USER_ROLE === 'dinas' ? `<p class="info-note">Data yang tampil hanya laporan dengan tujuan: <b>${esc(USER_INSTANSI || 'Dinas Sosial')}</b>.</p>` : ''}
<div class="table-wrap">
<table>
<tr>
<th>Pelapor</th>
<th>Kategori</th>
<th>Aktor</th>
<th>Status</th>
<th>Aksi</th>
</tr>
${(allData.laporan_masyarakat || []).map(item => `
<tr>
<td>${esc(item.nama_pelapor)}<br><small>${esc(item.no_hp)}</small></td>
<td>${esc(item.kategori_laporan)}</td>
<td>${esc(item.aktor_tujuan)}</td>
<td>
${IS_ADMIN ? `
<select id="status_laporan_${item.id}">
${optionSet(['Masuk', 'Ditinjau', 'Diproses', 'Selesai', 'Dijadikan Data Masyarakat', 'Ditolak'], item.status_laporan)}
</select>
` : `<b>${esc(item.status_laporan)}</b>`}
</td>
<td>
${IS_ADMIN ? `
<button onclick="updateStatusLaporan(${item.id})">Update Status</button>
<button class="danger" onclick="deleteData('laporan_masyarakat', ${item.id})">Hapus</button>
` : `<small>Hanya lihat</small>`}
</td>
</tr>
`).join('')}
</table>
</div>
`;
}
if (tabName === 'histori') {
const options = (allData.penduduk_kemiskinan || []).map(item => `
<option value="${item.id}">${esc(item.nama_kepala_keluarga)}</option>
`).join('');
const rumahOptions = (allData.masjid || []).map(item => `
<option value="${item.id}">${esc(item.nama_masjid)} - ${esc(item.jenis_rumah_ibadah)}</option>
`).join('');
content.innerHTML = `
<h3>Histori Bantuan <a href="?download=histori">Download CSV</a></h3>
${IS_ADMIN ? `
<div class="inline-form">
<select id="penduduk_id">
<option value="">-- Pilih masyarakat penerima --</option>
${options}
</select>
<input id="jenis_bantuan" placeholder="Jenis Bantuan">
<input id="tanggal_bantuan" type="date">
<input id="nominal" type="number" placeholder="Nominal">
<input id="sumber_bantuan" value="Pemkot Pontianak">
<select id="masjid_id">
<option value="">-- Rumah ibadah pemberi bantuan, jika ada --</option>
${rumahOptions}
</select>
<input id="keterangan" placeholder="Keterangan">
<button onclick="addHistori()">Tambah Histori</button>
</div>
` : `<p class="info-note">Role ini hanya dapat melihat dan mengunduh histori bantuan.</p>`}
<div class="table-wrap">
<table>
<tr>
<th>Nama</th>
<th>Bantuan</th>
<th>Tanggal</th>
<th>Nominal</th>
<th>Sumber</th>
<th>Rumah Ibadah</th>
<th>Aksi</th>
</tr>
${(allData.histori_bantuan || []).map(item => `
<tr>
<td>${esc(item.nama_kepala_keluarga)}</td>
<td>${esc(item.jenis_bantuan)}</td>
<td>${esc(item.tanggal_bantuan)}</td>
<td>${esc(item.nominal)}</td>
<td>${esc(item.sumber_bantuan)}</td>
<td>${item.nama_masjid ? `${esc(item.nama_masjid)}<br><small>${esc(item.jenis_rumah_ibadah || '-')}</small>` : '-'}</td>
<td>
${IS_ADMIN ? `<button class="danger" onclick="deleteData('histori_bantuan', ${item.id})">Hapus</button>` : `<small>Hanya lihat</small>`}
</td>
</tr>
`).join('')}
</table>
</div>
`;
}
if (tabName === 'penduduk') {
content.innerHTML = `
<h3>Data Masyarakat Prasejahtera <a href="?download=penduduk">Download CSV</a></h3>
<div class="table-wrap">
<table>
<tr>
<th>Nama</th>
<th>Pendidikan</th>
<th>Alamat</th>
<th>Status Bantuan</th>
<th>Aksi</th>
</tr>
${(allData.penduduk_kemiskinan || []).map(item => `
<tr>
<td>${esc(item.nama_kepala_keluarga)}</td>
<td>${esc(item.pendidikan)}</td>
<td>${esc(item.alamat)}</td>
<td>${esc(item.status_bantuan)}</td>
<td>
${IS_ADMIN ? `
<button onclick="editPenduduk(${item.id})">Edit</button>
<button class="danger" onclick="deleteData('penduduk_kemiskinan', ${item.id})">Hapus</button>
` : `<small>Hanya lihat</small>`}
</td>
</tr>
`).join('')}
</table>
</div>
`;
}
if (tabName === 'download') {
content.innerHTML = `
<h3>Download Data</h3>
<p class="info-note">Data yang dapat diunduh hanya data utama sesuai tujuan sistem: Laporan Cepat, Histori Bantuan, Masyarakat Prasejahtera, dan Rumah Ibadah.</p>
<div class="download-grid">
<a href="?download=laporan">Laporan Cepat</a>
<a href="?download=histori">Histori Bantuan</a>
<a href="?download=penduduk">Masyarakat Prasejahtera</a>
<a href="?download=rumah_ibadah">Rumah Ibadah</a>
</div>
`;
}
if (tabName === 'akun') {
if (!IS_ADMIN) return;
content.innerHTML = `
<h3>Kelola Akun Role</h3>
<p class="info-note">
Pembeda role: <b>Wali Kota</b> melihat seluruh rekap kota, sedangkan <b>Dinas</b> melihat laporan sesuai nama instansi yang dipilih.
</p>
<div class="inline-form">
<input id="user_nama" placeholder="Nama akun / nama dinas">
<input id="user_username" placeholder="Username">
<input id="user_password" type="password" placeholder="Password">
<select id="user_role">
${optionSet(['dinas', 'walikota', 'admin'], 'dinas')}
</select>
<select id="user_instansi">
${optionSet(['Dinas Sosial', 'Dinas Kesehatan', 'Wali Kota', 'Admin Pengelola'], 'Dinas Sosial')}
</select>
<button onclick="addUser()">Tambah Akun</button>
</div>
<div class="table-wrap">
<table>
<tr>
<th>Nama</th>
<th>Username</th>
<th>Role</th>
<th>Instansi</th>
<th>Aksi</th>
</tr>
${(allData.users || []).map(item => `
<tr>
<td>${esc(item.nama)}</td>
<td>${esc(item.username)}</td>
<td>${esc(item.role)}</td>
<td>${esc(item.instansi || '-')}</td>
<td><button class="danger" onclick="deleteUser(${item.id})">Hapus</button></td>
</tr>
`).join('')}
</table>
</div>
`;
}
}
loadAll();
</script>
</body>
</html>