Initial WebGIS portal project

This commit is contained in:
2026-06-10 19:53:39 +07:00
commit c0aa8e09d5
62 changed files with 9013 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
function start_admin_session(): void
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
function is_admin_logged_in(): bool
{
start_admin_session();
return !empty($_SESSION['admin_id']);
}
function require_admin_login(bool $jsonResponse = false): void
{
if (is_admin_logged_in()) {
return;
}
if ($jsonResponse) {
http_response_code(401);
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Sesi login tidak valid. Silakan login ulang.',
]);
} else {
header('Location: login.php');
}
exit;
}
function current_admin_name(): string
{
start_admin_session();
return (string)($_SESSION['admin_name'] ?? $_SESSION['admin_username'] ?? 'Admin');
}
function ensure_admin_table(PDO $pdo): void
{
$pdo->exec("
CREATE TABLE IF NOT EXISTS admins (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
nama VARCHAR(100) NOT NULL DEFAULT 'Administrator',
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
$count = (int)$pdo->query("SELECT COUNT(*) FROM admins")->fetchColumn();
if ($count === 0) {
$stmt = $pdo->prepare("INSERT INTO admins (username, password_hash, nama) VALUES (?, ?, ?)");
$stmt->execute([
'admin',
password_hash('admin123', PASSWORD_DEFAULT),
'Administrator',
]);
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
require_once 'db_pdo.php';
require_once 'auth.php';
require_once 'penduduk_feature_helper.php';
require_admin_login(true);
header('Content-Type: application/json');
$input = $_POST ?: (json_decode(file_get_contents('php://input'), true) ?: []);
if (empty($input['penduduk_id']) || empty($input['jenis_bantuan']) || empty($input['tanggal_bantuan'])) {
echo json_encode(['success' => false, 'message' => 'Penduduk, jenis bantuan, dan tanggal wajib diisi.']);
exit;
}
try {
ensurePendudukFeatureSchema($pdo);
$buktiFile = saveUploadedEvidence('bukti_file');
$stmt = $pdo->prepare("
INSERT INTO riwayat_bantuan (penduduk_id, jenis_bantuan, tanggal_bantuan, nilai_bantuan, pemberi_bantuan, catatan, bukti_file)
VALUES (?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$input['penduduk_id'],
$input['jenis_bantuan'],
normalizeDateOrNull($input['tanggal_bantuan']) ?? date('Y-m-d'),
$input['nilai_bantuan'] ?? null,
$input['pemberi_bantuan'] ?? 'Admin',
$input['catatan'] ?? null,
$buktiFile,
]);
$update = $pdo->prepare("
UPDATE penduduk_miskin
SET status_bantuan = 'Sudah dibantu', jenis_bantuan = ?, tanggal_bantuan = ?
WHERE id = ?
");
$update->execute([
$input['jenis_bantuan'],
normalizeDateOrNull($input['tanggal_bantuan']) ?? date('Y-m-d'),
$input['penduduk_id'],
]);
updatePendudukPriority($pdo, (int)$input['penduduk_id']);
echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+59
View File
@@ -0,0 +1,59 @@
<?php
require_once 'db_pdo.php';
require_once 'auth.php';
require_once 'penduduk_feature_helper.php';
require_admin_login(true);
header('Content-Type: application/json');
try {
ensurePendudukFeatureSchema($pdo);
refreshAllPriorityScores($pdo);
$stats = [];
$ri = $pdo->query("SELECT COUNT(*) as total FROM rumah_ibadah")->fetch();
$stats['total_rumah_ibadah'] = $ri['total'];
$pm = $pdo->query("SELECT COUNT(*) as total FROM penduduk_miskin")->fetch();
$stats['total_penduduk'] = $pm['total'];
$pm_sudah = $pdo->query("SELECT COUNT(*) as total FROM penduduk_miskin WHERE status_bantuan = 'Sudah dibantu'")->fetch();
$stats['sudah_dibantu'] = $pm_sudah['total'];
$pm_belum = $pdo->query("SELECT COUNT(*) as total FROM penduduk_miskin WHERE status_bantuan = 'Belum dibantu'")->fetch();
$stats['belum_dibantu'] = $pm_belum['total'];
$pm_tunggu = $pdo->query("SELECT COUNT(*) as total FROM penduduk_miskin WHERE status_bantuan = 'Menunggu verifikasi'")->fetch();
$stats['menunggu_verifikasi'] = $pm_tunggu['total'];
$blank = $pdo->query("SELECT COUNT(*) as total FROM penduduk_miskin WHERE is_blank_spot = 1")->fetch();
$stats['luar_radius'] = $blank['total'];
$prioritas = $pdo->query("SELECT COUNT(*) as total FROM penduduk_miskin WHERE skor_prioritas >= 70")->fetch();
$stats['prioritas_tinggi'] = $prioritas['total'];
$survei = $pdo->query("SELECT COUNT(*) as total FROM penduduk_miskin WHERE tindak_lanjut IN ('Sudah disurvei', 'Sudah diverifikasi')")->fetch();
$stats['sudah_survei'] = $survei['total'];
$stats['top_prioritas'] = $pdo->query("
SELECT id, kk_nama, kategori_kebutuhan, tindak_lanjut, skor_prioritas
FROM penduduk_miskin
ORDER BY skor_prioritas DESC, id DESC
LIMIT 5
")->fetchAll();
$stats['kebutuhan_terbanyak'] = $pdo->query("
SELECT COALESCE(NULLIF(kategori_kebutuhan, ''), 'Belum diisi') AS kategori, COUNT(*) AS total
FROM penduduk_miskin
GROUP BY COALESCE(NULLIF(kategori_kebutuhan, ''), 'Belum diisi')
ORDER BY total DESC
LIMIT 5
")->fetchAll();
echo json_encode([
'success' => true,
'data' => $stats
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
$host = getenv('DB_HOST') ?: '127.0.0.1';
$database = getenv('DB_DATABASE') ?: 'webgis_miskin';
$username = getenv('DB_USERNAME') ?: 'root';
$password = getenv('DB_PASSWORD') ?: '';
$charset = getenv('DB_CHARSET') ?: 'utf8mb4';
set_exception_handler(static function (Throwable $exception): void {
if (!headers_sent()) {
http_response_code(500);
header('Content-Type: application/json');
}
echo json_encode([
'success' => false,
'message' => 'Terjadi kesalahan server.',
'detail' => $exception->getMessage(),
]);
exit;
});
$dsn = "mysql:host={$host};dbname={$database};charset={$charset}";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $username, $password, $options);
} catch (PDOException $exception) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Koneksi database gagal: ' . $exception->getMessage(),
]);
exit;
}
+145
View File
@@ -0,0 +1,145 @@
<?php
/**
* Menghitung jarak antara dua koordinat menggunakan formula Haversine.
* @param float $lat1 Latitude titik pertama
* @param float $lon1 Longitude titik pertama
* @param float $lat2 Latitude titik kedua
* @param float $lon2 Longitude titik kedua
* @return float Jarak dalam satuan meter
*/
function calculateHaversineDistance($lat1, $lon1, $lat2, $lon2) {
$earthRadius = 6371000; // Radius bumi dalam meter
$latFrom = deg2rad($lat1);
$lonFrom = deg2rad($lon1);
$latTo = deg2rad($lat2);
$lonTo = deg2rad($lon2);
$latDelta = $latTo - $latFrom;
$lonDelta = $lonTo - $lonFrom;
$angle = 2 * asin(sqrt(pow(sin($latDelta / 2), 2) +
cos($latFrom) * cos($latTo) * pow(sin($lonDelta / 2), 2)));
return $angle * $earthRadius;
}
function calculateOsrmRouteDistance($lat1, $lon1, $lat2, $lon2): ?float
{
$coordinates = sprintf(
'%.8F,%.8F;%.8F,%.8F',
$lon1,
$lat1,
$lon2,
$lat2
);
$url = 'https://router.project-osrm.org/route/v1/driving/' . $coordinates . '?overview=false&alternatives=false&steps=false';
$ch = curl_init($url);
if ($ch === false) {
return null;
}
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 4,
CURLOPT_TIMEOUT => 8,
CURLOPT_USERAGENT => 'WebGIS-Peduli/1.0',
]);
$response = curl_exec($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($response === false || $httpCode !== 200) {
return null;
}
$json = json_decode($response, true);
if (!is_array($json) || ($json['code'] ?? '') !== 'Ok') {
return null;
}
$distance = $json['routes'][0]['distance'] ?? null;
return is_numeric($distance) ? (float)$distance : null;
}
function calculateRoadDistance($lat1, $lon1, $lat2, $lon2): ?float
{
return calculateOsrmRouteDistance((float)$lat1, (float)$lon1, (float)$lat2, (float)$lon2);
}
function ensurePendudukDistanceColumns(PDO $pdo): void
{
$stmt = $pdo->query("SHOW COLUMNS FROM penduduk_miskin LIKE 'jarak_rute_m'");
if ($stmt->fetch() === false) {
$pdo->exec("ALTER TABLE penduduk_miskin ADD COLUMN jarak_rute_m DECIMAL(10, 2) NULL AFTER jarak_m");
}
}
function calculatePendudukCoverage(PDO $pdo, float $lat, float $lng): array
{
$stmtRi = $pdo->query("SELECT id, lat, lng, radius FROM rumah_ibadah");
$rumahIbadah = $stmtRi->fetchAll(PDO::FETCH_ASSOC);
$nearest = null;
$nearestCovered = null;
foreach ($rumahIbadah as $ri) {
$distance = calculateHaversineDistance($lat, $lng, (float)$ri['lat'], (float)$ri['lng']);
$result = [
'ibadah_id' => (int)$ri['id'],
'ri_lat' => (float)$ri['lat'],
'ri_lng' => (float)$ri['lng'],
'jarak_m' => round($distance, 2),
'distance' => $distance,
];
if ($nearest === null || $distance < $nearest['distance']) {
$nearest = $result;
}
if ($distance <= (float)$ri['radius'] && ($nearestCovered === null || $distance < $nearestCovered['distance'])) {
$nearestCovered = $result;
}
}
$selected = $nearestCovered ?? $nearest;
$routeDistance = null;
if ($selected !== null) {
$routeDistance = calculateRoadDistance($lat, $lng, $selected['ri_lat'], $selected['ri_lng']);
}
return [
'ibadah_id' => $selected['ibadah_id'] ?? null,
'jarak_m' => $selected['jarak_m'] ?? null,
'jarak_rute_m' => $routeDistance !== null ? round($routeDistance, 2) : null,
'is_blank_spot' => $nearestCovered === null ? 1 : 0,
];
}
function recalculateAllPendudukCoverage(PDO $pdo): void
{
ensurePendudukDistanceColumns($pdo);
$stmt = $pdo->query("SELECT id, lat, lng FROM penduduk_miskin");
$penduduk = $stmt->fetchAll(PDO::FETCH_ASSOC);
$update = $pdo->prepare("
UPDATE penduduk_miskin
SET ibadah_id = ?, jarak_m = ?, jarak_rute_m = ?, is_blank_spot = ?
WHERE id = ?
");
foreach ($penduduk as $row) {
$coverage = calculatePendudukCoverage($pdo, (float)$row['lat'], (float)$row['lng']);
$update->execute([
$coverage['ibadah_id'],
$coverage['jarak_m'],
$coverage['jarak_rute_m'],
$coverage['is_blank_spot'],
$row['id'],
]);
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
require_once 'db_pdo.php';
require_once 'haversine_helper.php';
require_once 'penduduk_feature_helper.php';
require_once 'auth.php';
require_admin_login(true);
header('Content-Type: application/json');
$input = readPendudukInput();
if (!$input) {
echo json_encode(['success' => false, 'message' => 'Input tidak valid']);
exit;
}
try {
ensurePendudukDistanceColumns($pdo);
ensurePendudukFeatureSchema($pdo);
$input_lat = floatval($input['lat']);
$input_lng = floatval($input['lng']);
$coverage = calculatePendudukCoverage($pdo, $input_lat, $input_lng);
$buktiFile = saveUploadedEvidence('bukti_file');
$stmt = $pdo->prepare("
INSERT INTO penduduk_miskin
(kk_nama, kk_nik, alamat, lat, lng, ibadah_id, jarak_m, jarak_rute_m, is_blank_spot, status_bantuan, kategori_kebutuhan, deskripsi_kebutuhan, tindak_lanjut, kondisi_rumah, penghasilan, catatan_survei, bukti_file, jenis_bantuan, tanggal_bantuan)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$input['kk_nama'],
$input['kk_nik'] ?? null,
$input['alamat'],
$input['lat'],
$input['lng'],
$coverage['ibadah_id'],
$coverage['jarak_m'],
$coverage['jarak_rute_m'],
$coverage['is_blank_spot'],
$input['status_bantuan'] ?? 'Belum dibantu',
$input['kategori_kebutuhan'] ?? null,
$input['deskripsi_kebutuhan'] ?? null,
$input['tindak_lanjut'] ?? 'Belum disurvei',
$input['kondisi_rumah'] ?? null,
normalizeNonNegativeIntOrNull($input['penghasilan'] ?? '', 'Penghasilan'),
$input['catatan_survei'] ?? null,
$buktiFile,
$input['jenis_bantuan'] ?? null,
normalizeDateOrNull($input['tanggal_bantuan'] ?? null)
]);
$penduduk_id = $pdo->lastInsertId();
// 4. Insert anggota keluarga if exist
if (!empty($input['anggota']) && is_array($input['anggota'])) {
$stmtAnggota = $pdo->prepare("INSERT INTO anggota_keluarga (penduduk_id, nama, hubungan, umur, pekerjaan) VALUES (?, ?, ?, ?, ?)");
foreach ($input['anggota'] as $anggota) {
$stmtAnggota->execute([
$penduduk_id,
$anggota['nama'],
$anggota['hubungan'],
$anggota['umur'] ?? null,
$anggota['pekerjaan'] ?? null
]);
}
}
if (($input['status_bantuan'] ?? '') === 'Sudah dibantu' && !empty($input['jenis_bantuan'])) {
$stmtRiwayat = $pdo->prepare("
INSERT INTO riwayat_bantuan (penduduk_id, jenis_bantuan, tanggal_bantuan, pemberi_bantuan, catatan, bukti_file)
VALUES (?, ?, ?, ?, ?, ?)
");
$stmtRiwayat->execute([
$penduduk_id,
$input['jenis_bantuan'],
normalizeDateOrNull($input['tanggal_bantuan'] ?? null) ?? date('Y-m-d'),
'Admin',
'Dicatat dari form penduduk.',
$buktiFile,
]);
}
updatePendudukPriority($pdo, (int)$penduduk_id);
echo json_encode([
'success' => true,
'id' => $penduduk_id,
'ibadah_id' => $coverage['ibadah_id'],
'jarak_m' => $coverage['jarak_m'],
'jarak_rute_m' => $coverage['jarak_rute_m'],
'is_blank_spot' => $coverage['is_blank_spot']
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+22
View File
@@ -0,0 +1,22 @@
<?php
require_once 'db_pdo.php';
require_once 'auth.php';
require_admin_login(true);
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['id'])) {
echo json_encode(['success' => false, 'message' => 'Input tidak valid']);
exit;
}
try {
$stmt = $pdo->prepare("DELETE FROM penduduk_miskin WHERE id=?");
$stmt->execute([$input['id']]);
// Note: anggota_keluarga will be deleted automatically due to ON DELETE CASCADE
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+179
View File
@@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
function ensurePendudukFeatureSchema(PDO $pdo): void
{
$columns = [
'kategori_kebutuhan' => "ALTER TABLE penduduk_miskin ADD COLUMN kategori_kebutuhan VARCHAR(100) NULL AFTER status_bantuan",
'deskripsi_kebutuhan' => "ALTER TABLE penduduk_miskin ADD COLUMN deskripsi_kebutuhan TEXT NULL AFTER kategori_kebutuhan",
'tindak_lanjut' => "ALTER TABLE penduduk_miskin ADD COLUMN tindak_lanjut VARCHAR(60) NOT NULL DEFAULT 'Belum disurvei' AFTER deskripsi_kebutuhan",
'kondisi_rumah' => "ALTER TABLE penduduk_miskin ADD COLUMN kondisi_rumah VARCHAR(80) NULL AFTER tindak_lanjut",
'penghasilan' => "ALTER TABLE penduduk_miskin ADD COLUMN penghasilan INT NULL AFTER kondisi_rumah",
'catatan_survei' => "ALTER TABLE penduduk_miskin ADD COLUMN catatan_survei TEXT NULL AFTER penghasilan",
'bukti_file' => "ALTER TABLE penduduk_miskin ADD COLUMN bukti_file VARCHAR(255) NULL AFTER catatan_survei",
'skor_prioritas' => "ALTER TABLE penduduk_miskin ADD COLUMN skor_prioritas INT NOT NULL DEFAULT 0 AFTER bukti_file",
];
foreach ($columns as $column => $sql) {
$stmt = $pdo->query("SHOW COLUMNS FROM penduduk_miskin LIKE " . $pdo->quote($column));
if ($stmt->fetch() === false) {
$pdo->exec($sql);
}
}
$pdo->exec("
CREATE TABLE IF NOT EXISTS riwayat_bantuan (
id INT AUTO_INCREMENT PRIMARY KEY,
penduduk_id INT NOT NULL,
jenis_bantuan VARCHAR(120) NOT NULL,
tanggal_bantuan DATE NOT NULL,
nilai_bantuan VARCHAR(80) NULL,
pemberi_bantuan VARCHAR(120) NULL,
catatan TEXT NULL,
bukti_file VARCHAR(255) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
}
function normalizeDateOrNull(?string $value): ?string
{
$value = trim((string)$value);
return $value === '' || $value === '0000-00-00' ? null : $value;
}
function normalizeNonNegativeIntOrNull(mixed $value, string $label): ?int
{
$value = trim((string)$value);
if ($value === '') {
return null;
}
if (!is_numeric($value) || (float)$value < 0) {
throw new InvalidArgumentException($label . ' tidak boleh bernilai negatif.');
}
return (int)$value;
}
function calculatePriorityScore(array $penduduk, array $anggota = []): int
{
$score = 0;
$anggotaCount = count($anggota);
$penghasilan = isset($penduduk['penghasilan']) ? (int)$penduduk['penghasilan'] : 0;
if (($penduduk['status_bantuan'] ?? '') === 'Belum dibantu') $score += 30;
if (($penduduk['status_bantuan'] ?? '') === 'Menunggu verifikasi') $score += 15;
if ((int)($penduduk['is_blank_spot'] ?? 0) === 1) $score += 15;
$score += min(20, $anggotaCount * 4);
foreach ($anggota as $a) {
$umur = isset($a['umur']) ? (int)$a['umur'] : null;
if ($umur !== null && ($umur < 6 || $umur >= 60)) {
$score += 5;
}
}
if ($penghasilan > 0 && $penghasilan <= 1000000) $score += 15;
elseif ($penghasilan > 1000000 && $penghasilan <= 2000000) $score += 8;
$kondisi = strtolower((string)($penduduk['kondisi_rumah'] ?? ''));
if (str_contains($kondisi, 'tidak layak')) $score += 15;
elseif (str_contains($kondisi, 'rentan')) $score += 8;
$kebutuhan = strtolower((string)($penduduk['kategori_kebutuhan'] ?? ''));
if (in_array($kebutuhan, ['kesehatan', 'rumah tidak layak', 'bantuan darurat'], true)) $score += 10;
return min(100, max(0, $score));
}
function getPriorityLabel(int $score): string
{
if ($score >= 70) return 'Prioritas Tinggi';
if ($score >= 40) return 'Prioritas Sedang';
return 'Prioritas Rendah';
}
function saveUploadedEvidence(string $fieldName): ?string
{
if (empty($_FILES[$fieldName]) || !is_array($_FILES[$fieldName])) {
return null;
}
$file = $_FILES[$fieldName];
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
return null;
}
if (($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
throw new RuntimeException('Upload bukti gagal.');
}
if (($file['size'] ?? 0) > 3 * 1024 * 1024) {
throw new RuntimeException('Ukuran bukti maksimal 3 MB.');
}
$allowed = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'application/pdf' => 'pdf',
];
$mime = mime_content_type($file['tmp_name']);
if (!isset($allowed[$mime])) {
throw new RuntimeException('Format bukti harus JPG, PNG, WEBP, atau PDF.');
}
$dir = __DIR__ . '/../uploads/bukti';
if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
throw new RuntimeException('Folder upload tidak bisa dibuat.');
}
$filename = date('YmdHis') . '-' . bin2hex(random_bytes(6)) . '.' . $allowed[$mime];
$target = $dir . '/' . $filename;
if (!move_uploaded_file($file['tmp_name'], $target)) {
throw new RuntimeException('Bukti tidak bisa disimpan.');
}
return 'uploads/bukti/' . $filename;
}
function readPendudukInput(): array
{
if (str_starts_with((string)($_SERVER['CONTENT_TYPE'] ?? ''), 'multipart/form-data')) {
$input = $_POST;
$input['anggota'] = json_decode((string)($_POST['anggota'] ?? '[]'), true) ?: [];
return $input;
}
return json_decode(file_get_contents('php://input'), true) ?: [];
}
function updatePendudukPriority(PDO $pdo, int $pendudukId): void
{
$stmt = $pdo->prepare("SELECT * FROM penduduk_miskin WHERE id = ?");
$stmt->execute([$pendudukId]);
$penduduk = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$penduduk) return;
$stmtAnggota = $pdo->prepare("SELECT * FROM anggota_keluarga WHERE penduduk_id = ?");
$stmtAnggota->execute([$pendudukId]);
$anggota = $stmtAnggota->fetchAll(PDO::FETCH_ASSOC);
$score = calculatePriorityScore($penduduk, $anggota);
$update = $pdo->prepare("UPDATE penduduk_miskin SET skor_prioritas = ? WHERE id = ?");
$update->execute([$score, $pendudukId]);
}
function refreshAllPriorityScores(PDO $pdo): void
{
ensurePendudukFeatureSchema($pdo);
$ids = $pdo->query("SELECT id FROM penduduk_miskin")->fetchAll(PDO::FETCH_COLUMN);
foreach ($ids as $id) {
updatePendudukPriority($pdo, (int)$id);
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
require_once 'db_pdo.php';
require_once 'auth.php';
require_once 'penduduk_feature_helper.php';
require_admin_login(true);
header('Content-Type: application/json');
try {
ensurePendudukFeatureSchema($pdo);
refreshAllPriorityScores($pdo);
// Join with rumah_ibadah to get name
$stmt = $pdo->query("
SELECT p.*, r.nama as rumah_ibadah_nama
FROM penduduk_miskin p
LEFT JOIN rumah_ibadah r ON p.ibadah_id = r.id
ORDER BY p.skor_prioritas DESC, p.id DESC
");
$data = $stmt->fetchAll();
// Get all anggota
$stmtAnggota = $pdo->query("SELECT * FROM anggota_keluarga");
$anggota = $stmtAnggota->fetchAll();
// Map anggota to penduduk
$anggotaByPenduduk = [];
foreach ($anggota as $a) {
$anggotaByPenduduk[$a['penduduk_id']][] = $a;
}
$stmtRiwayat = $pdo->query("SELECT * FROM riwayat_bantuan ORDER BY tanggal_bantuan DESC, id DESC");
$riwayat = $stmtRiwayat->fetchAll();
$riwayatByPenduduk = [];
foreach ($riwayat as $r) {
$riwayatByPenduduk[$r['penduduk_id']][] = $r;
}
foreach ($data as &$d) {
$d['anggota'] = $anggotaByPenduduk[$d['id']] ?? [];
$d['riwayat_bantuan'] = $riwayatByPenduduk[$d['id']] ?? [];
$d['label_prioritas'] = getPriorityLabel((int)($d['skor_prioritas'] ?? 0));
}
echo json_encode([
'success' => true,
'data' => $data
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+84
View File
@@ -0,0 +1,84 @@
<?php
require_once 'db_pdo.php';
require_once 'haversine_helper.php';
require_once 'penduduk_feature_helper.php';
require_once 'auth.php';
require_admin_login(true);
header('Content-Type: application/json');
$input = readPendudukInput();
if (!$input || empty($input['id'])) {
echo json_encode(['success' => false, 'message' => 'Input tidak valid']);
exit;
}
try {
ensurePendudukDistanceColumns($pdo);
ensurePendudukFeatureSchema($pdo);
$input_lat = floatval($input['lat']);
$input_lng = floatval($input['lng']);
$coverage = calculatePendudukCoverage($pdo, $input_lat, $input_lng);
$buktiFile = saveUploadedEvidence('bukti_file');
if ($buktiFile === null && !empty($input['existing_bukti_file'])) {
$buktiFile = $input['existing_bukti_file'];
}
$stmt = $pdo->prepare("
UPDATE penduduk_miskin
SET kk_nama=?, kk_nik=?, alamat=?, lat=?, lng=?, status_bantuan=?, kategori_kebutuhan=?, deskripsi_kebutuhan=?, tindak_lanjut=?, kondisi_rumah=?, penghasilan=?, catatan_survei=?, bukti_file=?, jenis_bantuan=?, tanggal_bantuan=?, ibadah_id=?, jarak_m=?, jarak_rute_m=?, is_blank_spot=?
WHERE id=?
");
$stmt->execute([
$input['kk_nama'],
$input['kk_nik'] ?? null,
$input['alamat'],
$input['lat'],
$input['lng'],
$input['status_bantuan'] ?? 'Belum dibantu',
$input['kategori_kebutuhan'] ?? null,
$input['deskripsi_kebutuhan'] ?? null,
$input['tindak_lanjut'] ?? 'Belum disurvei',
$input['kondisi_rumah'] ?? null,
normalizeNonNegativeIntOrNull($input['penghasilan'] ?? '', 'Penghasilan'),
$input['catatan_survei'] ?? null,
$buktiFile,
$input['jenis_bantuan'] ?? null,
normalizeDateOrNull($input['tanggal_bantuan'] ?? null),
$coverage['ibadah_id'],
$coverage['jarak_m'],
$coverage['jarak_rute_m'],
$coverage['is_blank_spot'],
$input['id']
]);
// 4. Delete old anggota and insert new ones
$pdo->prepare("DELETE FROM anggota_keluarga WHERE penduduk_id=?")->execute([$input['id']]);
if (!empty($input['anggota']) && is_array($input['anggota'])) {
$stmtAnggota = $pdo->prepare("INSERT INTO anggota_keluarga (penduduk_id, nama, hubungan, umur, pekerjaan) VALUES (?, ?, ?, ?, ?)");
foreach ($input['anggota'] as $anggota) {
$stmtAnggota->execute([
$input['id'],
$anggota['nama'],
$anggota['hubungan'],
$anggota['umur'] ?? null,
$anggota['pekerjaan'] ?? null
]);
}
}
updatePendudukPriority($pdo, (int)$input['id']);
echo json_encode([
'success' => true,
'ibadah_id' => $coverage['ibadah_id'],
'jarak_m' => $coverage['jarak_m'],
'jarak_rute_m' => $coverage['jarak_rute_m'],
'is_blank_spot' => $coverage['is_blank_spot']
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+38
View File
@@ -0,0 +1,38 @@
<?php
require_once 'db_pdo.php';
require_once 'auth.php';
require_once 'haversine_helper.php';
require_once 'penduduk_feature_helper.php';
require_once 'rumah_ibadah_helper.php';
require_admin_login(true);
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
echo json_encode(['success' => false, 'message' => 'Input tidak valid']);
exit;
}
try {
assertRumahIbadahNotDuplicate($pdo, $input);
$stmt = $pdo->prepare("INSERT INTO rumah_ibadah (nama, jenis, kontak, alamat, lat, lng, radius) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$input['nama'],
$input['jenis'],
$input['kontak'],
$input['alamat'],
$input['lat'],
$input['lng'],
$input['radius'] ?? 500
]);
$id = $pdo->lastInsertId();
recalculateAllPendudukCoverage($pdo);
refreshAllPriorityScores($pdo);
echo json_encode(['success' => true, 'id' => $id]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+26
View File
@@ -0,0 +1,26 @@
<?php
require_once 'db_pdo.php';
require_once 'auth.php';
require_once 'haversine_helper.php';
require_once 'penduduk_feature_helper.php';
require_admin_login(true);
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['id'])) {
echo json_encode(['success' => false, 'message' => 'Input tidak valid']);
exit;
}
try {
$stmt = $pdo->prepare("DELETE FROM rumah_ibadah WHERE id=?");
$stmt->execute([$input['id']]);
recalculateAllPendudukCoverage($pdo);
refreshAllPriorityScores($pdo);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
function assertRumahIbadahNotDuplicate(PDO $pdo, array $input, ?int $ignoreId = null): void
{
$nama = trim((string)($input['nama'] ?? ''));
$jenis = trim((string)($input['jenis'] ?? ''));
$lat = round((float)($input['lat'] ?? 0), 6);
$lng = round((float)($input['lng'] ?? 0), 6);
$allowedJenis = ['Masjid', 'Gereja', 'Pura', 'Vihara', 'Klenteng', 'Lainnya'];
if ($nama === '' || $jenis === '' || !isset($input['lat'], $input['lng'])) {
throw new InvalidArgumentException('Nama, jenis, dan lokasi rumah ibadah wajib diisi.');
}
if (!in_array($jenis, $allowedJenis, true)) {
throw new InvalidArgumentException('Jenis rumah ibadah tidak valid.');
}
$sql = "
SELECT id
FROM rumah_ibadah
WHERE LOWER(TRIM(nama)) = LOWER(TRIM(?))
AND jenis = ?
AND ROUND(lat, 6) = ?
AND ROUND(lng, 6) = ?
";
$params = [$nama, $jenis, $lat, $lng];
if ($ignoreId !== null) {
$sql .= " AND id <> ?";
$params[] = $ignoreId;
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
if ($stmt->fetch()) {
throw new InvalidArgumentException('Rumah ibadah dengan nama, jenis, dan lokasi yang sama sudah ada.');
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
require_once 'db_pdo.php';
require_once 'auth.php';
require_admin_login(true);
header('Content-Type: application/json');
try {
$stmt = $pdo->query("SELECT * FROM rumah_ibadah ORDER BY nama ASC");
$data = $stmt->fetchAll();
echo json_encode([
'success' => true,
'data' => $data
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+38
View File
@@ -0,0 +1,38 @@
<?php
require_once 'db_pdo.php';
require_once 'auth.php';
require_once 'haversine_helper.php';
require_once 'penduduk_feature_helper.php';
require_once 'rumah_ibadah_helper.php';
require_admin_login(true);
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['id'])) {
echo json_encode(['success' => false, 'message' => 'Input tidak valid']);
exit;
}
try {
assertRumahIbadahNotDuplicate($pdo, $input, (int)$input['id']);
$stmt = $pdo->prepare("UPDATE rumah_ibadah SET nama=?, jenis=?, kontak=?, alamat=?, lat=?, lng=?, radius=? WHERE id=?");
$stmt->execute([
$input['nama'],
$input['jenis'],
$input['kontak'],
$input['alamat'],
$input['lat'],
$input['lng'],
$input['radius'],
$input['id']
]);
recalculateAllPendudukCoverage($pdo);
refreshAllPriorityScores($pdo);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
+46
View File
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/auth.php';
require_admin_login(true);
header('Content-Type: application/json');
$host = getenv('DB_HOST') ?: '127.0.0.1';
$username = getenv('DB_SETUP_USERNAME') ?: getenv('DB_USERNAME') ?: 'root';
$password = getenv('DB_SETUP_PASSWORD') ?: getenv('DB_PASSWORD') ?: '';
$charset = getenv('DB_CHARSET') ?: 'utf8mb4';
$schemaPath = __DIR__ . '/../sql/miskin_schema.sql';
$sql = file_get_contents($schemaPath);
if ($sql === false) {
echo json_encode([
'success' => false,
'message' => 'File skema database tidak ditemukan.',
]);
exit;
}
try {
$dsn = "mysql:host={$host};charset={$charset}";
$pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$pdo->exec($sql);
echo json_encode([
'success' => true,
'message' => 'Database webgis_miskin berhasil di-reset.',
]);
} catch (PDOException $exception) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Gagal reset database.',
'error' => $exception->getMessage(),
]);
}