Initial WebGIS portal project
This commit is contained in:
@@ -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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user