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(),
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: #172033;
|
||||
background: #eef2f7;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.guide-page {
|
||||
width: min(1040px, calc(100% - 32px));
|
||||
margin: 28px auto;
|
||||
background: #ffffff;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.13);
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.guide-hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
border-bottom: 2px solid #1f2937;
|
||||
padding-bottom: 22px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 8px;
|
||||
color: #4f46e5;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 14px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.lead {
|
||||
max-width: 720px;
|
||||
color: #475467;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.print-btn {
|
||||
align-self: flex-start;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: #4f46e5;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-weight: 800;
|
||||
padding: 11px 15px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.access-panel {
|
||||
border: 1px solid #d8dee9;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-grid div,
|
||||
.legend-grid div {
|
||||
border: 1px solid #d8dee9;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.info-grid span {
|
||||
display: block;
|
||||
color: #667085;
|
||||
font-size: 0.82rem;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.info-grid strong {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: #667085;
|
||||
font-size: 0.9rem;
|
||||
margin: 12px 0 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #d8dee9;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #eef2ff;
|
||||
border-radius: 6px;
|
||||
color: #3730a3;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.steps ol {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.steps li,
|
||||
ul li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.legend-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.legend-grid strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.legend-grid p {
|
||||
color: #667085;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: inline-block;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dot.worship {
|
||||
background: #4f46e5;
|
||||
}
|
||||
|
||||
.dot.reached {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.dot.outside {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.guide-hero,
|
||||
.info-grid,
|
||||
.legend-grid {
|
||||
grid-template-columns: 1fr;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.guide-page {
|
||||
padding: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.guide-page {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.print-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
:root {
|
||||
--primary: #4f46e5;
|
||||
--primary-hover: #4338ca;
|
||||
--dark: #1e293b;
|
||||
--gray: #64748b;
|
||||
--gray-light: #e2e8f0;
|
||||
--light: #f8fafc;
|
||||
--white: #ffffff;
|
||||
--danger: #ef4444;
|
||||
--shadow-lg: 0 18px 40px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--dark);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(79, 70, 229, 0.16), transparent 34%),
|
||||
linear-gradient(135deg, #f8fafc 0%, #eef2ff 52%, #f8fafc 100%);
|
||||
}
|
||||
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
width: min(100%, 430px);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(226, 232, 240, 0.9);
|
||||
border-radius: 18px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 34px;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--white);
|
||||
background: var(--primary);
|
||||
font-size: 1.45rem;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
color: var(--primary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--gray);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
color: #991b1b;
|
||||
background-color: #fee2e2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 18px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-group span {
|
||||
color: var(--dark);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.input-shell {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border: 1px solid var(--gray-light);
|
||||
border-radius: 12px;
|
||||
background: var(--light);
|
||||
padding: 0 14px;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.input-shell:focus-within {
|
||||
background: var(--white);
|
||||
border-color: rgba(79, 70, 229, 0.45);
|
||||
box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.1);
|
||||
}
|
||||
|
||||
.input-shell i {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.input-shell input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: var(--dark);
|
||||
font-size: 0.96rem;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
height: 48px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: var(--primary);
|
||||
color: var(--white);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 800;
|
||||
margin-top: 4px;
|
||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.login-hint {
|
||||
color: var(--gray);
|
||||
font-size: 0.86rem;
|
||||
margin-top: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.login-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
padding: 26px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: #172033;
|
||||
background: #eef2f7;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.report-page {
|
||||
width: min(1180px, calc(100% - 32px));
|
||||
margin: 24px auto;
|
||||
background: #fff;
|
||||
padding: 28px;
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.report-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
border-bottom: 2px solid #1f2937;
|
||||
padding-bottom: 18px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 6px;
|
||||
color: #4f46e5;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.print-btn {
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: #4f46e5;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.summary-grid div {
|
||||
border: 1px solid #d8dee9;
|
||||
border-left: 4px solid #4f46e5;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.summary-grid span {
|
||||
display: block;
|
||||
color: #667085;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.summary-grid strong {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.report-note {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #d8dee9;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #d8dee9;
|
||||
padding: 8px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f1f5f9;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
small {
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #667085;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.report-page {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.print-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
table {
|
||||
page-break-inside: auto;
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: auto;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,793 @@
|
||||
// Globals
|
||||
let map;
|
||||
let layerRI, layerPM, layerRadius;
|
||||
let riData = [];
|
||||
let pmData = [];
|
||||
let pickingMode = null; // 'ri' or 'pm'
|
||||
let tempMarker = null;
|
||||
let currentView = 'dashboard';
|
||||
let searchTerm = '';
|
||||
|
||||
const rumahIbadahIconConfig = {
|
||||
Masjid: { icon: 'fa-mosque', className: 'mosque' },
|
||||
Gereja: { icon: 'fa-church', className: 'church' },
|
||||
Pura: { icon: 'fa-gopuram', className: 'temple' },
|
||||
Vihara: { icon: 'fa-vihara', className: 'vihara' },
|
||||
Klenteng: { icon: 'fa-torii-gate', className: 'klenteng' },
|
||||
Lainnya: { icon: 'fa-place-of-worship', className: 'other' }
|
||||
};
|
||||
|
||||
function getRumahIbadahIcon(jenis) {
|
||||
const config = rumahIbadahIconConfig[jenis] || rumahIbadahIconConfig.Lainnya;
|
||||
|
||||
return L.divIcon({
|
||||
className: 'ri-div-icon',
|
||||
html: `<div class="ri-marker ri-${config.className}" title="${jenis || 'Rumah Ibadah'}"><i class="fas ${config.icon}"></i></div>`,
|
||||
iconSize: [36, 46],
|
||||
iconAnchor: [18, 43],
|
||||
popupAnchor: [0, -43]
|
||||
});
|
||||
}
|
||||
|
||||
// PM Icons are now circleMarkers generated inline in renderPM
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await initMap();
|
||||
initUI();
|
||||
loadData();
|
||||
});
|
||||
|
||||
async function initMap() {
|
||||
map = L.map('map', {
|
||||
zoomControl: false,
|
||||
preferCanvas: true
|
||||
}).setView([-0.060547, 109.344902], 13);
|
||||
window.map = map;
|
||||
|
||||
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
||||
|
||||
const tileOptions = {
|
||||
keepBuffer: 6,
|
||||
updateWhenIdle: false,
|
||||
updateWhenZooming: false,
|
||||
updateInterval: 100
|
||||
};
|
||||
|
||||
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
...tileOptions,
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
});
|
||||
|
||||
const satelit = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
...tileOptions,
|
||||
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
|
||||
});
|
||||
|
||||
const darkMap = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
...tileOptions,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
subdomains: 'abcd',
|
||||
maxZoom: 20
|
||||
});
|
||||
|
||||
osm.addTo(map);
|
||||
|
||||
const baseMaps = {
|
||||
"OSM Standard": osm,
|
||||
"Citra Satelit": satelit,
|
||||
"Dark Mode": darkMap
|
||||
};
|
||||
|
||||
layerRadius = L.featureGroup().addTo(map);
|
||||
layerRI = L.featureGroup().addTo(map);
|
||||
layerPM = L.featureGroup().addTo(map);
|
||||
|
||||
const overlays = {
|
||||
"Radius Layanan": layerRadius,
|
||||
"Rumah Ibadah": layerRI,
|
||||
"Penduduk Miskin": layerPM
|
||||
};
|
||||
|
||||
L.control.layers(baseMaps, overlays, { position: 'topright' }).addTo(map);
|
||||
|
||||
// Map Click for Picking Location
|
||||
map.on('click', async (e) => {
|
||||
if (!pickingMode) return;
|
||||
|
||||
const lat = e.latlng.lat;
|
||||
const lng = e.latlng.lng;
|
||||
|
||||
if (tempMarker) map.removeLayer(tempMarker);
|
||||
tempMarker = L.marker([lat, lng]).addTo(map);
|
||||
|
||||
if (pickingMode === 'ri') {
|
||||
document.getElementById('ri-lat').value = lat.toFixed(6);
|
||||
document.getElementById('ri-lng').value = lng.toFixed(6);
|
||||
document.getElementById('ri-alamat').value = 'Mencari alamat...';
|
||||
document.getElementById('ri-alamat').value = await reverseGeocode(lat, lng);
|
||||
document.getElementById('modal-ri').classList.add('show');
|
||||
} else if (pickingMode === 'pm') {
|
||||
document.getElementById('pm-lat').value = lat.toFixed(6);
|
||||
document.getElementById('pm-lng').value = lng.toFixed(6);
|
||||
document.getElementById('pm-alamat').value = 'Mencari alamat...';
|
||||
document.getElementById('pm-alamat').value = await reverseGeocode(lat, lng);
|
||||
document.getElementById('modal-pm').classList.add('show');
|
||||
}
|
||||
|
||||
pickingMode = null;
|
||||
if(tempMarker) map.removeLayer(tempMarker);
|
||||
});
|
||||
}
|
||||
|
||||
function initUI() {
|
||||
// Sidebar Toggle
|
||||
document.getElementById('toggle-sidebar').addEventListener('click', () => {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (window.innerWidth <= 768) {
|
||||
sidebar.classList.toggle('open');
|
||||
} else {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.menu-item[data-view]').forEach(item => {
|
||||
item.addEventListener('click', () => switchView(item.dataset.view));
|
||||
});
|
||||
|
||||
// Menus
|
||||
const openAddRI = () => {
|
||||
switchView('map');
|
||||
document.getElementById('form-ri').reset();
|
||||
document.getElementById('ri-id').value = '';
|
||||
document.getElementById('modal-ri-title').innerText = 'Tambah Rumah Ibadah';
|
||||
document.getElementById('modal-ri').classList.add('show');
|
||||
};
|
||||
document.getElementById('menu-add-ri').addEventListener('click', openAddRI);
|
||||
document.getElementById('fab-add-ri')?.addEventListener('click', openAddRI);
|
||||
|
||||
const openAddPM = () => {
|
||||
switchView('map');
|
||||
document.getElementById('form-pm').reset();
|
||||
document.getElementById('pm-id').value = '';
|
||||
document.getElementById('anggota-container').innerHTML = '';
|
||||
document.getElementById('bantuan-fields').style.display = 'none';
|
||||
document.getElementById('modal-pm-title').innerText = 'Tambah Penduduk Miskin';
|
||||
document.getElementById('modal-pm').classList.add('show');
|
||||
};
|
||||
document.getElementById('menu-add-pm').addEventListener('click', openAddPM);
|
||||
document.getElementById('fab-add-pm')?.addEventListener('click', openAddPM);
|
||||
document.getElementById('dashboard-add-ri')?.addEventListener('click', openAddRI);
|
||||
document.getElementById('dashboard-add-pm')?.addEventListener('click', openAddPM);
|
||||
document.getElementById('dashboard-open-map')?.addEventListener('click', () => switchView('map'));
|
||||
|
||||
// Filters
|
||||
document.getElementById('filter-status').addEventListener('change', renderMapLayers);
|
||||
document.getElementById('filter-jenis').addEventListener('change', renderMapLayers);
|
||||
document.getElementById('filter-kebutuhan').addEventListener('change', renderPM);
|
||||
|
||||
// Search
|
||||
document.getElementById('search-input').addEventListener('input', () => {
|
||||
searchTerm = getSearchTerm();
|
||||
renderMapLayers();
|
||||
});
|
||||
document.getElementById('search-input').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
applyMapSearch(true);
|
||||
}
|
||||
});
|
||||
document.getElementById('search-button').addEventListener('click', () => applyMapSearch(true));
|
||||
|
||||
// Location
|
||||
document.getElementById('btn-my-location').addEventListener('click', () => {
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const lat = position.coords.latitude;
|
||||
const lng = position.coords.longitude;
|
||||
map.flyTo([lat, lng], 16);
|
||||
L.marker([lat, lng]).addTo(map).bindPopup('Lokasi Anda').openPopup();
|
||||
},
|
||||
(err) => alert('Gagal mendapatkan lokasi: ' + err.message)
|
||||
);
|
||||
} else {
|
||||
alert('Geolocation tidak didukung browser ini.');
|
||||
}
|
||||
});
|
||||
|
||||
switchView(currentView);
|
||||
}
|
||||
|
||||
function switchView(view) {
|
||||
currentView = view;
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
|
||||
document.querySelectorAll('.view-panel').forEach(panel => {
|
||||
panel.classList.toggle('active', panel.id === `${view}-view`);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.menu-item[data-view]').forEach(item => {
|
||||
item.classList.toggle('active', item.dataset.view === view);
|
||||
});
|
||||
|
||||
mainContent?.classList.toggle('map-mode', view === 'map');
|
||||
mainContent?.classList.toggle('dashboard-mode', view !== 'map');
|
||||
document.querySelector('.search-bar')?.classList.toggle('hidden', view !== 'map');
|
||||
document.querySelector('.map-header-controls')?.classList.toggle('hidden', view !== 'map');
|
||||
|
||||
if (window.innerWidth <= 768) {
|
||||
document.getElementById('sidebar').classList.remove('open');
|
||||
}
|
||||
|
||||
if (view === 'map' && map) {
|
||||
refreshMapSize();
|
||||
}
|
||||
}
|
||||
|
||||
function refreshMapSize() {
|
||||
[0, 100, 300].forEach(delay => {
|
||||
setTimeout(() => map.invalidateSize({ pan: false }), delay);
|
||||
});
|
||||
}
|
||||
|
||||
function getSearchTerm() {
|
||||
return document.getElementById('search-input').value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function textIncludes(value, term) {
|
||||
return String(value || '').toLowerCase().includes(term);
|
||||
}
|
||||
|
||||
function matchesRI(ri) {
|
||||
if (!searchTerm) return true;
|
||||
|
||||
return [
|
||||
ri.nama,
|
||||
ri.jenis,
|
||||
ri.kontak,
|
||||
ri.alamat
|
||||
].some(value => textIncludes(value, searchTerm));
|
||||
}
|
||||
|
||||
function matchesPM(pm) {
|
||||
if (!searchTerm) return true;
|
||||
|
||||
const anggotaMatch = Array.isArray(pm.anggota) && pm.anggota.some(anggota => [
|
||||
anggota.nama,
|
||||
anggota.hubungan,
|
||||
anggota.pekerjaan
|
||||
].some(value => textIncludes(value, searchTerm)));
|
||||
|
||||
return anggotaMatch || [
|
||||
pm.kk_nama,
|
||||
pm.kk_nik,
|
||||
pm.alamat,
|
||||
pm.status_bantuan,
|
||||
pm.jenis_bantuan,
|
||||
pm.kategori_kebutuhan,
|
||||
pm.deskripsi_kebutuhan,
|
||||
pm.tindak_lanjut,
|
||||
pm.kondisi_rumah,
|
||||
pm.rumah_ibadah_nama
|
||||
].some(value => textIncludes(value, searchTerm));
|
||||
}
|
||||
|
||||
function getFilteredRI() {
|
||||
const filterJenis = document.getElementById('filter-jenis').value;
|
||||
|
||||
return riData.filter(ri => {
|
||||
if (filterJenis !== 'all' && ri.jenis !== filterJenis) return false;
|
||||
return matchesRI(ri);
|
||||
});
|
||||
}
|
||||
|
||||
function getFilteredPM() {
|
||||
const filterStatus = document.getElementById('filter-status').value;
|
||||
const filterKebutuhan = document.getElementById('filter-kebutuhan').value;
|
||||
|
||||
return pmData.filter(pm => {
|
||||
if (filterStatus !== 'all' && pm.status_bantuan !== filterStatus) return false;
|
||||
if (filterKebutuhan !== 'all' && pm.kategori_kebutuhan !== filterKebutuhan) return false;
|
||||
return matchesPM(pm);
|
||||
});
|
||||
}
|
||||
|
||||
function renderMapLayers() {
|
||||
renderRI();
|
||||
renderPM();
|
||||
}
|
||||
|
||||
function applyMapSearch(focusResult = false) {
|
||||
searchTerm = getSearchTerm();
|
||||
renderMapLayers();
|
||||
|
||||
if (focusResult) {
|
||||
switchView('map');
|
||||
focusSearchResults();
|
||||
}
|
||||
}
|
||||
|
||||
function focusSearchResults() {
|
||||
const results = [
|
||||
...getFilteredRI().map(item => [parseFloat(item.lat), parseFloat(item.lng)]),
|
||||
...getFilteredPM().map(item => [parseFloat(item.lat), parseFloat(item.lng)])
|
||||
].filter(([lat, lng]) => Number.isFinite(lat) && Number.isFinite(lng));
|
||||
|
||||
if (!searchTerm || results.length === 0) {
|
||||
alert(searchTerm ? 'Data tidak ditemukan di peta.' : 'Masukkan kata kunci pencarian.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.length === 1) {
|
||||
map.flyTo(results[0], 16);
|
||||
return;
|
||||
}
|
||||
|
||||
map.fitBounds(L.latLngBounds(results), { padding: [40, 40], maxZoom: 16 });
|
||||
}
|
||||
|
||||
function pickLocation(mode) {
|
||||
switchView('map');
|
||||
pickingMode = mode;
|
||||
closeModal('modal-' + mode);
|
||||
}
|
||||
|
||||
function closeModal(id) {
|
||||
document.getElementById(id).classList.remove('show');
|
||||
}
|
||||
|
||||
async function reverseGeocode(lat, lng) {
|
||||
try {
|
||||
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`);
|
||||
const data = await res.json();
|
||||
return data.display_name || 'Alamat tidak ditemukan';
|
||||
} catch (e) {
|
||||
return 'Gagal memuat alamat';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleBantuanFields() {
|
||||
const status = document.getElementById('pm-status').value;
|
||||
document.getElementById('bantuan-fields').style.display = status === 'Sudah dibantu' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function addAnggotaField(data = null) {
|
||||
const container = document.getElementById('anggota-container');
|
||||
const id = Date.now() + Math.floor(Math.random() * 100);
|
||||
const html = `
|
||||
<div class="dynamic-item" id="anggota-${id}">
|
||||
<button type="button" class="btn-remove" onclick="document.getElementById('anggota-${id}').remove()">X</button>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<div class="form-group">
|
||||
<label>Nama</label>
|
||||
<input type="text" class="form-control a-nama" value="${data ? data.nama : ''}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Hubungan</label>
|
||||
<input type="text" class="form-control a-hubungan" value="${data ? data.hubungan : ''}" placeholder="Anak/Istri/dll" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Umur</label>
|
||||
<input type="number" class="form-control a-umur" value="${data ? data.umur : ''}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Pekerjaan</label>
|
||||
<input type="text" class="form-control a-pekerjaan" value="${data && data.pekerjaan ? data.pekerjaan : ''}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
// Data Fetching and Rendering
|
||||
async function loadData() {
|
||||
await loadStats();
|
||||
await fetchRI();
|
||||
await fetchPM();
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res = await fetch('api/dashboard_stats.php');
|
||||
const json = await res.json();
|
||||
if(json.success) {
|
||||
document.getElementById('stat-ri').innerText = json.data.total_rumah_ibadah;
|
||||
document.getElementById('stat-pm').innerText = json.data.total_penduduk;
|
||||
document.getElementById('stat-sudah').innerText = json.data.sudah_dibantu;
|
||||
document.getElementById('stat-belum').innerText = json.data.belum_dibantu;
|
||||
document.getElementById('stat-menunggu').innerText = json.data.menunggu_verifikasi || 0;
|
||||
document.getElementById('stat-prioritas').innerText = json.data.prioritas_tinggi || 0;
|
||||
document.getElementById('stat-luar-radius').innerText = json.data.luar_radius || 0;
|
||||
document.getElementById('summary-sudah').innerText = json.data.sudah_dibantu;
|
||||
document.getElementById('summary-belum').innerText = json.data.belum_dibantu;
|
||||
document.getElementById('summary-menunggu').innerText = json.data.menunggu_verifikasi || 0;
|
||||
renderDashboardLists(json.data);
|
||||
}
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
function renderDashboardLists(stats) {
|
||||
const priorityList = document.getElementById('priority-list');
|
||||
const needList = document.getElementById('need-list');
|
||||
|
||||
if (priorityList) {
|
||||
const items = stats.top_prioritas || [];
|
||||
priorityList.innerHTML = items.length ? items.map(item => `
|
||||
<div class="priority-item">
|
||||
<div class="priority-score">${item.skor_prioritas || 0}</div>
|
||||
<div>
|
||||
<strong>${item.kk_nama}</strong>
|
||||
<p>${item.kategori_kebutuhan || 'Kebutuhan belum diisi'} - ${item.tindak_lanjut || 'Belum disurvei'}</p>
|
||||
</div>
|
||||
</div>
|
||||
`).join('') : '<div class="need-item"><p>Belum ada data prioritas.</p></div>';
|
||||
}
|
||||
|
||||
if (needList) {
|
||||
const items = stats.kebutuhan_terbanyak || [];
|
||||
needList.innerHTML = items.length ? items.map(item => `
|
||||
<div class="need-item">
|
||||
<strong>${item.kategori}</strong>
|
||||
<p>${item.total} keluarga membutuhkan kategori ini</p>
|
||||
</div>
|
||||
`).join('') : '<div class="need-item"><p>Belum ada data kebutuhan.</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRI() {
|
||||
try {
|
||||
const res = await fetch('api/rumah_ibadah_read.php');
|
||||
const json = await res.json();
|
||||
if(json.success) {
|
||||
riData = json.data;
|
||||
renderRI();
|
||||
}
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function fetchPM() {
|
||||
try {
|
||||
const res = await fetch('api/penduduk_read.php');
|
||||
const json = await res.json();
|
||||
if(json.success) {
|
||||
pmData = json.data;
|
||||
renderPM();
|
||||
}
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
function renderRI() {
|
||||
layerRI.clearLayers();
|
||||
layerRadius.clearLayers();
|
||||
|
||||
getFilteredRI().forEach(ri => {
|
||||
const lat = parseFloat(ri.lat);
|
||||
const lng = parseFloat(ri.lng);
|
||||
const rad = parseInt(ri.radius) || 500;
|
||||
|
||||
// Circle
|
||||
L.circle([lat, lng], {
|
||||
color: '#4f46e5',
|
||||
fillColor: '#4f46e5',
|
||||
fillOpacity: 0.1,
|
||||
radius: rad,
|
||||
interactive: false
|
||||
}).addTo(layerRadius);
|
||||
|
||||
// Marker
|
||||
const marker = L.marker([lat, lng], { icon: getRumahIbadahIcon(ri.jenis) }).addTo(layerRI);
|
||||
marker.feature = ri;
|
||||
|
||||
let popup = `
|
||||
<div class="popup-header">
|
||||
<h3>${ri.nama}</h3>
|
||||
<p>${ri.jenis}</p>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="popup-row"><div class="label">Kontak</div><div class="value">${ri.kontak || '-'}</div></div>
|
||||
<div class="popup-row"><div class="label">Radius</div><div class="value">${rad} m</div></div>
|
||||
<div class="popup-row" style="flex-direction: column;"><div class="label" style="width:100%">Alamat</div><div class="value" style="font-size:0.8rem; font-weight:400">${ri.alamat}</div></div>
|
||||
<div class="popup-actions">
|
||||
<button class="btn-edit" onclick='editRI(${JSON.stringify(ri)})'>Edit</button>
|
||||
<button class="btn-delete" onclick='deleteRI(${ri.id})'>Hapus</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
marker.bindPopup(popup);
|
||||
});
|
||||
|
||||
layerRadius.eachLayer(circle => circle.bringToBack());
|
||||
}
|
||||
|
||||
function renderPM() {
|
||||
layerPM.clearLayers();
|
||||
|
||||
getFilteredPM().forEach(pm => {
|
||||
const lat = parseFloat(pm.lat);
|
||||
const lng = parseFloat(pm.lng);
|
||||
const anggotaCount = pm.anggota ? pm.anggota.length : 0;
|
||||
|
||||
let badgeClass = 'badge-danger';
|
||||
if(pm.status_bantuan === 'Sudah dibantu') badgeClass = 'badge-success';
|
||||
else if(pm.status_bantuan === 'Menunggu verifikasi') badgeClass = 'badge-warning';
|
||||
|
||||
const isBlankSpot = pm.is_blank_spot == 1;
|
||||
const pmColor = isBlankSpot ? '#ef4444' : '#10b981'; // Merah (Blank Spot), Hijau (Terjangkau)
|
||||
const riwayatHtml = Array.isArray(pm.riwayat_bantuan) && pm.riwayat_bantuan.length
|
||||
? pm.riwayat_bantuan.slice(0, 3).map(r => `${r.tanggal_bantuan}: ${r.jenis_bantuan}`).join('<br>')
|
||||
: '-';
|
||||
const buktiHtml = pm.bukti_file
|
||||
? `<a href="${pm.bukti_file}" target="_blank">Lihat bukti</a>`
|
||||
: '-';
|
||||
|
||||
const marker = L.circleMarker([lat, lng], {
|
||||
radius: 7,
|
||||
fillColor: pmColor,
|
||||
color: '#ffffff',
|
||||
weight: 1.5,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.9
|
||||
}).addTo(layerPM);
|
||||
marker.feature = pm;
|
||||
|
||||
let popup = `
|
||||
<div class="popup-header ${pm.status_bantuan === 'Sudah dibantu' ? 'success' : pm.status_bantuan === 'Menunggu verifikasi' ? 'warning' : 'danger'}">
|
||||
<h3>Keluarga ${pm.kk_nama}</h3>
|
||||
<span class="badge ${badgeClass}">${pm.status_bantuan}</span>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="popup-row"><div class="label">Jumlah Anggota</div><div class="value">${anggotaCount} orang</div></div>
|
||||
<div class="popup-row"><div class="label">Rumah Ibadah Terdekat</div><div class="value">${pm.rumah_ibadah_nama || '-'}</div></div>
|
||||
<div class="popup-row"><div class="label">Prioritas</div><div class="value">${pm.skor_prioritas || 0} (${pm.label_prioritas || '-'})</div></div>
|
||||
<div class="popup-row"><div class="label">Kebutuhan</div><div class="value">${pm.kategori_kebutuhan || '-'}</div></div>
|
||||
<div class="popup-row"><div class="label">Tindak Lanjut</div><div class="value">${pm.tindak_lanjut || 'Belum disurvei'}</div></div>
|
||||
<div class="popup-row"><div class="label">Jarak Lurus</div><div class="value">${pm.jarak_m ? pm.jarak_m + ' meter' : '-'} ${isBlankSpot ? '<i style="color:red">(Di luar radius)</i>' : '<i style="color:green">(Terjangkau)</i>'}</div></div>
|
||||
<div class="popup-row"><div class="label">Jarak Rute</div><div class="value">${pm.jarak_rute_m ? pm.jarak_rute_m + ' meter' : '-'}</div></div>
|
||||
${pm.status_bantuan === 'Sudah dibantu' ? `<div class="popup-row"><div class="label">Bantuan</div><div class="value">${pm.jenis_bantuan} (${pm.tanggal_bantuan})</div></div>` : ''}
|
||||
<div class="popup-row"><div class="label">Riwayat</div><div class="value">${riwayatHtml}</div></div>
|
||||
<div class="popup-row"><div class="label">Bukti</div><div class="value">${buktiHtml}</div></div>
|
||||
<div class="popup-row" style="flex-direction: column;"><div class="label" style="width:100%">Alamat</div><div class="value" style="font-size:0.8rem; font-weight:400">${pm.alamat}</div></div>
|
||||
<div class="popup-actions">
|
||||
<button class="btn-edit" onclick='editPM(${JSON.stringify(pm).replace(/'/g, "\\'")})'>Edit</button>
|
||||
<button class="btn-edit" onclick='openBantuanModal(${pm.id})'>Bantuan</button>
|
||||
<button class="btn-delete" onclick='deletePM(${pm.id})'>Hapus</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
marker.bindPopup(popup);
|
||||
});
|
||||
|
||||
layerPM.bringToFront();
|
||||
}
|
||||
|
||||
// CRUD Operations
|
||||
async function saveRI() {
|
||||
const id = document.getElementById('ri-id').value;
|
||||
const data = {
|
||||
nama: document.getElementById('ri-nama').value,
|
||||
jenis: document.getElementById('ri-jenis').value,
|
||||
kontak: document.getElementById('ri-kontak').value,
|
||||
lat: document.getElementById('ri-lat').value,
|
||||
lng: document.getElementById('ri-lng').value,
|
||||
alamat: document.getElementById('ri-alamat').value,
|
||||
radius: document.getElementById('ri-radius').value
|
||||
};
|
||||
|
||||
if(!data.nama || !data.lat) { alert("Isi form dengan lengkap"); return; }
|
||||
|
||||
const url = id ? 'api/rumah_ibadah_update.php' : 'api/rumah_ibadah_create.php';
|
||||
if(id) data.id = id;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const json = await res.json();
|
||||
if(json.success) {
|
||||
closeModal('modal-ri');
|
||||
await fetchRI();
|
||||
await fetchPM();
|
||||
await loadStats();
|
||||
} else {
|
||||
alert(json.message);
|
||||
}
|
||||
} catch(e) { alert('Error saving data'); }
|
||||
}
|
||||
|
||||
async function savePM() {
|
||||
const id = document.getElementById('pm-id').value;
|
||||
const data = new FormData();
|
||||
const anggota = [];
|
||||
|
||||
data.append('kk_nama', document.getElementById('pm-nama').value);
|
||||
data.append('kk_nik', document.getElementById('pm-nik').value);
|
||||
data.append('lat', document.getElementById('pm-lat').value);
|
||||
data.append('lng', document.getElementById('pm-lng').value);
|
||||
data.append('alamat', document.getElementById('pm-alamat').value);
|
||||
data.append('kategori_kebutuhan', document.getElementById('pm-kategori-kebutuhan').value);
|
||||
data.append('deskripsi_kebutuhan', document.getElementById('pm-deskripsi-kebutuhan').value);
|
||||
data.append('kondisi_rumah', document.getElementById('pm-kondisi-rumah').value);
|
||||
data.append('penghasilan', document.getElementById('pm-penghasilan').value);
|
||||
data.append('status_bantuan', document.getElementById('pm-status').value);
|
||||
data.append('jenis_bantuan', document.getElementById('pm-jenis-bantuan').value);
|
||||
data.append('tanggal_bantuan', document.getElementById('pm-tanggal-bantuan').value);
|
||||
data.append('tindak_lanjut', document.getElementById('pm-tindak-lanjut').value);
|
||||
data.append('catatan_survei', document.getElementById('pm-catatan-survei').value);
|
||||
data.append('existing_bukti_file', document.getElementById('pm-existing-bukti-file').value);
|
||||
|
||||
const buktiFile = document.getElementById('pm-bukti-file').files[0];
|
||||
if (buktiFile) data.append('bukti_file', buktiFile);
|
||||
|
||||
if(!data.get('kk_nama') || !data.get('lat')) { alert("Isi form dengan lengkap"); return; }
|
||||
if (data.get('penghasilan') !== '' && Number(data.get('penghasilan')) < 0) {
|
||||
alert('Penghasilan tidak boleh bernilai negatif.');
|
||||
document.getElementById('pm-penghasilan').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const items = document.querySelectorAll('.dynamic-item');
|
||||
items.forEach(el => {
|
||||
anggota.push({
|
||||
nama: el.querySelector('.a-nama').value,
|
||||
hubungan: el.querySelector('.a-hubungan').value,
|
||||
umur: el.querySelector('.a-umur').value,
|
||||
pekerjaan: el.querySelector('.a-pekerjaan').value
|
||||
});
|
||||
});
|
||||
data.append('anggota', JSON.stringify(anggota));
|
||||
|
||||
const url = id ? 'api/penduduk_update.php' : 'api/penduduk_create.php';
|
||||
if(id) data.append('id', id);
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: data
|
||||
});
|
||||
const json = await res.json();
|
||||
if(json.success) {
|
||||
closeModal('modal-pm');
|
||||
await fetchPM();
|
||||
await loadStats();
|
||||
if(json.ibadah_id) {
|
||||
const ri = riData.find(r => r.id == json.ibadah_id);
|
||||
if(ri) alert(`Warga otomatis dimasukkan ke binaan: ${ri.nama}`);
|
||||
}
|
||||
} else {
|
||||
alert(json.message);
|
||||
}
|
||||
} catch(e) { alert('Error saving data'); }
|
||||
}
|
||||
|
||||
function editRI(data) {
|
||||
document.getElementById('ri-id').value = data.id;
|
||||
document.getElementById('ri-nama').value = data.nama;
|
||||
document.getElementById('ri-jenis').value = data.jenis;
|
||||
document.getElementById('ri-kontak').value = data.kontak;
|
||||
document.getElementById('ri-lat').value = data.lat;
|
||||
document.getElementById('ri-lng').value = data.lng;
|
||||
document.getElementById('ri-alamat').value = data.alamat;
|
||||
document.getElementById('ri-radius').value = data.radius;
|
||||
document.getElementById('modal-ri-title').innerText = 'Edit Rumah Ibadah';
|
||||
document.getElementById('modal-ri').classList.add('show');
|
||||
}
|
||||
|
||||
function editPM(data) {
|
||||
document.getElementById('pm-id').value = data.id;
|
||||
document.getElementById('pm-nama').value = data.kk_nama;
|
||||
document.getElementById('pm-nik').value = data.kk_nik;
|
||||
document.getElementById('pm-lat').value = data.lat;
|
||||
document.getElementById('pm-lng').value = data.lng;
|
||||
document.getElementById('pm-alamat').value = data.alamat;
|
||||
document.getElementById('pm-kategori-kebutuhan').value = data.kategori_kebutuhan || '';
|
||||
document.getElementById('pm-deskripsi-kebutuhan').value = data.deskripsi_kebutuhan || '';
|
||||
document.getElementById('pm-kondisi-rumah').value = data.kondisi_rumah || '';
|
||||
document.getElementById('pm-penghasilan').value = data.penghasilan || '';
|
||||
document.getElementById('pm-status').value = data.status_bantuan;
|
||||
document.getElementById('pm-jenis-bantuan').value = data.jenis_bantuan;
|
||||
document.getElementById('pm-tanggal-bantuan').value = data.tanggal_bantuan;
|
||||
document.getElementById('pm-tindak-lanjut').value = data.tindak_lanjut || 'Belum disurvei';
|
||||
document.getElementById('pm-catatan-survei').value = data.catatan_survei || '';
|
||||
document.getElementById('pm-existing-bukti-file').value = data.bukti_file || '';
|
||||
document.getElementById('pm-bukti-file').value = '';
|
||||
|
||||
toggleBantuanFields();
|
||||
|
||||
document.getElementById('anggota-container').innerHTML = '';
|
||||
if(data.anggota && data.anggota.length > 0) {
|
||||
data.anggota.forEach(a => addAnggotaField(a));
|
||||
}
|
||||
|
||||
document.getElementById('modal-pm-title').innerText = 'Edit Penduduk Miskin';
|
||||
document.getElementById('modal-pm').classList.add('show');
|
||||
}
|
||||
|
||||
function setBantuanAlert(message = '', type = 'error') {
|
||||
const alertBox = document.getElementById('bantuan-alert');
|
||||
if (!alertBox) return;
|
||||
|
||||
alertBox.textContent = message;
|
||||
alertBox.hidden = !message;
|
||||
alertBox.classList.toggle('success', type === 'success');
|
||||
}
|
||||
|
||||
function openBantuanModal(id) {
|
||||
const penerima = pmData.find(pm => String(pm.id) === String(id));
|
||||
const form = document.getElementById('form-bantuan');
|
||||
|
||||
form.reset();
|
||||
setBantuanAlert();
|
||||
document.getElementById('bantuan-penduduk-id').value = id;
|
||||
document.getElementById('bantuan-penerima').value = penerima ? `Keluarga ${penerima.kk_nama}` : `ID ${id}`;
|
||||
document.getElementById('bantuan-jenis').value = penerima?.kategori_kebutuhan || '';
|
||||
document.getElementById('bantuan-tanggal').value = new Date().toISOString().slice(0, 10);
|
||||
document.getElementById('bantuan-pemberi').value = 'Admin';
|
||||
document.getElementById('bantuan-bukti-file').value = '';
|
||||
document.getElementById('btn-save-bantuan').disabled = false;
|
||||
document.getElementById('modal-bantuan').classList.add('show');
|
||||
}
|
||||
|
||||
async function saveBantuan() {
|
||||
const id = document.getElementById('bantuan-penduduk-id').value;
|
||||
const jenis = document.getElementById('bantuan-jenis').value.trim();
|
||||
const tanggal = document.getElementById('bantuan-tanggal').value;
|
||||
const nilai = document.getElementById('bantuan-nilai').value;
|
||||
const pemberi = document.getElementById('bantuan-pemberi').value.trim() || 'Admin';
|
||||
const catatan = document.getElementById('bantuan-catatan').value.trim();
|
||||
const buktiFile = document.getElementById('bantuan-bukti-file').files[0];
|
||||
const saveButton = document.getElementById('btn-save-bantuan');
|
||||
|
||||
if (!id || !jenis || !tanggal) {
|
||||
setBantuanAlert('Keluarga penerima, jenis bantuan, dan tanggal wajib diisi.');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('penduduk_id', id);
|
||||
formData.append('jenis_bantuan', jenis);
|
||||
formData.append('tanggal_bantuan', tanggal);
|
||||
formData.append('nilai_bantuan', nilai || '');
|
||||
formData.append('pemberi_bantuan', pemberi);
|
||||
formData.append('catatan', catatan || '');
|
||||
if (buktiFile) formData.append('bukti_file', buktiFile);
|
||||
|
||||
try {
|
||||
saveButton.disabled = true;
|
||||
saveButton.innerText = 'Menyimpan...';
|
||||
const res = await fetch('api/bantuan_create.php', { method: 'POST', body: formData });
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
closeModal('modal-bantuan');
|
||||
await fetchPM();
|
||||
await loadStats();
|
||||
renderMapLayers();
|
||||
} else {
|
||||
setBantuanAlert(json.message || 'Gagal menambah bantuan.');
|
||||
}
|
||||
} catch(e) {
|
||||
setBantuanAlert('Terjadi kesalahan saat menyimpan bantuan.');
|
||||
} finally {
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerText = 'Simpan Bantuan';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRI(id) {
|
||||
if(!confirm('Hapus rumah ibadah ini?')) return;
|
||||
try {
|
||||
const res = await fetch('api/rumah_ibadah_delete.php', { method:'POST', body:JSON.stringify({id}) });
|
||||
const json = await res.json();
|
||||
if(json.success) { fetchRI(); fetchPM(); loadStats(); }
|
||||
} catch(e) { alert('Error'); }
|
||||
}
|
||||
|
||||
async function deletePM(id) {
|
||||
if(!confirm('Hapus penduduk miskin ini?')) return;
|
||||
try {
|
||||
const res = await fetch('api/penduduk_delete.php', { method:'POST', body:JSON.stringify({id}) });
|
||||
const json = await res.json();
|
||||
if(json.success) { fetchPM(); loadStats(); }
|
||||
} catch(e) { alert('Error'); }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="0;url=index.php">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS Peduli</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>window.location.replace('index.php');</script>
|
||||
<p>Mengalihkan ke halaman sistem...</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,498 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/api/auth.php';
|
||||
require_admin_login();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS Sebaran Penduduk Miskin</title>
|
||||
<!-- Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="assets/vendor/leaflet/leaflet.css" />
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=<?= filemtime(__DIR__ . '/assets/css/style.css') ?>">
|
||||
</head>
|
||||
<body>
|
||||
<div class="dashboard-container">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<i class="fas fa-map-marked-alt fa-2x" style="color: var(--primary);"></i>
|
||||
<h2>WebGIS Peduli</h2>
|
||||
</div>
|
||||
<div class="sidebar-menu">
|
||||
<div class="menu-item active" id="menu-dashboard" data-view="dashboard">
|
||||
<i class="fas fa-home"></i> Dashboard
|
||||
</div>
|
||||
<div class="menu-item" id="menu-map" data-view="map">
|
||||
<i class="fas fa-map"></i> Peta
|
||||
</div>
|
||||
<div class="menu-item" id="menu-add-ri">
|
||||
<i class="fas fa-plus-circle"></i> Tambah Rumah Ibadah
|
||||
</div>
|
||||
<div class="menu-item" id="menu-add-pm">
|
||||
<i class="fas fa-user-plus"></i> Tambah Penduduk
|
||||
</div>
|
||||
<a class="menu-item" href="laporan.php">
|
||||
<i class="fas fa-file-lines"></i> Laporan
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content dashboard-mode">
|
||||
<!-- Topbar -->
|
||||
<header class="topbar">
|
||||
<button id="toggle-sidebar" class="btn-icon"><i class="fas fa-bars"></i></button>
|
||||
<div class="search-bar hidden">
|
||||
<input type="text" id="search-input" placeholder="Cari nama warga / rumah ibadah...">
|
||||
<button id="search-button" type="button"><i class="fas fa-search"></i></button>
|
||||
</div>
|
||||
<div class="map-header-controls hidden">
|
||||
<div class="header-filter">
|
||||
<i class="fas fa-circle-check"></i>
|
||||
<label>Status</label>
|
||||
<select id="filter-status">
|
||||
<option value="all">Semua Status</option>
|
||||
<option value="Sudah dibantu">Sudah Dibantu</option>
|
||||
<option value="Belum dibantu">Belum Dibantu</option>
|
||||
<option value="Menunggu verifikasi">Menunggu Verifikasi</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="header-filter">
|
||||
<i class="fas fa-place-of-worship"></i>
|
||||
<label>Jenis</label>
|
||||
<select id="filter-jenis">
|
||||
<option value="all">Semua Jenis</option>
|
||||
<option value="Masjid">Masjid</option>
|
||||
<option value="Gereja">Gereja</option>
|
||||
<option value="Pura">Pura</option>
|
||||
<option value="Vihara">Vihara</option>
|
||||
<option value="Klenteng">Klenteng</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="header-filter">
|
||||
<i class="fas fa-hand-holding-heart"></i>
|
||||
<label>Kebutuhan</label>
|
||||
<select id="filter-kebutuhan">
|
||||
<option value="all">Semua Kebutuhan</option>
|
||||
<option value="Sembako">Sembako</option>
|
||||
<option value="Pendidikan">Pendidikan</option>
|
||||
<option value="Kesehatan">Kesehatan</option>
|
||||
<option value="Modal Usaha">Modal Usaha</option>
|
||||
<option value="Rumah Tidak Layak">Rumah Tidak Layak</option>
|
||||
<option value="Bantuan Darurat">Bantuan Darurat</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-profile">
|
||||
<span><?= htmlspecialchars(current_admin_name(), ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<div class="avatar"><i class="fas fa-user-shield"></i></div>
|
||||
<a class="logout-link" href="logout.php" title="Logout"><i class="fas fa-right-from-bracket"></i></a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Dashboard -->
|
||||
<section id="dashboard-view" class="view-panel dashboard-view active">
|
||||
<div class="dashboard-page-header">
|
||||
<div>
|
||||
<p class="eyebrow">Ringkasan Data</p>
|
||||
<h1>Dashboard WebGIS Peduli</h1>
|
||||
<p>Pantau sebaran warga, status bantuan, dan cakupan layanan rumah ibadah dari satu halaman.</p>
|
||||
</div>
|
||||
<div class="dashboard-header-actions">
|
||||
<button class="btn btn-primary" id="dashboard-open-map"><i class="fas fa-map-location-dot"></i> Buka Peta</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-container" id="stats-container">
|
||||
<div class="stat-card">
|
||||
<h3>Rumah Ibadah</h3>
|
||||
<div class="value" id="stat-ri">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Total Warga</h3>
|
||||
<div class="value" id="stat-pm">0</div>
|
||||
</div>
|
||||
<div class="stat-card success">
|
||||
<h3>Sudah Dibantu</h3>
|
||||
<div class="value" id="stat-sudah">0</div>
|
||||
</div>
|
||||
<div class="stat-card warning">
|
||||
<h3>Menunggu</h3>
|
||||
<div class="value" id="stat-menunggu">0</div>
|
||||
</div>
|
||||
<div class="stat-card danger">
|
||||
<h3>Belum Dibantu</h3>
|
||||
<div class="value" id="stat-belum">0</div>
|
||||
</div>
|
||||
<div class="stat-card warning">
|
||||
<h3>Prioritas Tinggi</h3>
|
||||
<div class="value" id="stat-prioritas">0</div>
|
||||
</div>
|
||||
<div class="stat-card danger">
|
||||
<h3>Luar Radius</h3>
|
||||
<div class="value" id="stat-luar-radius">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<div class="dashboard-card status-card">
|
||||
<div class="card-title">
|
||||
<h2>Status Bantuan</h2>
|
||||
<span>Data keluarga miskin</span>
|
||||
</div>
|
||||
<div class="status-list">
|
||||
<div class="status-item">
|
||||
<span class="status-dot success"></span>
|
||||
<div>
|
||||
<strong id="summary-sudah">0</strong>
|
||||
<p>Sudah menerima bantuan</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-dot warning"></span>
|
||||
<div>
|
||||
<strong id="summary-menunggu">0</strong>
|
||||
<p>Menunggu verifikasi</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-dot danger"></span>
|
||||
<div>
|
||||
<strong id="summary-belum">0</strong>
|
||||
<p>Belum menerima bantuan</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<div class="card-title">
|
||||
<h2>Aksi Cepat</h2>
|
||||
<span>Input dan pemetaan data</span>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<button class="quick-action" id="dashboard-add-ri">
|
||||
<i class="fas fa-place-of-worship"></i>
|
||||
<span>Tambah Rumah Ibadah</span>
|
||||
</button>
|
||||
<button class="quick-action" id="dashboard-add-pm">
|
||||
<i class="fas fa-user-plus"></i>
|
||||
<span>Tambah Penduduk</span>
|
||||
</button>
|
||||
<a class="quick-action" href="laporan.php?prioritas=tinggi">
|
||||
<i class="fas fa-file-export"></i>
|
||||
<span>Cetak Laporan Prioritas</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card map-summary-card">
|
||||
<div class="card-title">
|
||||
<h2>Legenda Peta</h2>
|
||||
<span>Indikator cakupan layanan</span>
|
||||
</div>
|
||||
<div class="legend-list">
|
||||
<div><span class="legend-marker ri"></span> Rumah ibadah dan radius layanan</div>
|
||||
<div><span class="legend-marker reached"></span> Warga dalam radius layanan</div>
|
||||
<div><span class="legend-marker blank"></span> Warga di luar radius layanan</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<div class="card-title">
|
||||
<h2>Prioritas Bantuan</h2>
|
||||
<span>Urutan paling mendesak</span>
|
||||
</div>
|
||||
<div class="priority-list" id="priority-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<div class="card-title">
|
||||
<h2>Kebutuhan Terbanyak</h2>
|
||||
<span>Basis penyaluran bantuan</span>
|
||||
</div>
|
||||
<div class="need-list" id="need-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Peta -->
|
||||
<section id="map-view" class="view-panel map-view">
|
||||
<div id="map" class="map-fullscreen"></div>
|
||||
|
||||
<button id="btn-my-location" class="fab-location" title="Lokasi Saya"><i class="fas fa-crosshairs"></i></button>
|
||||
<div class="fab-container">
|
||||
<button class="fab-main" title="Tambah Data"><i class="fas fa-plus"></i></button>
|
||||
<div class="fab-menu">
|
||||
<button class="fab-item" id="fab-add-ri" title="Tambah Rumah Ibadah"><i class="fas fa-place-of-worship"></i></button>
|
||||
<button class="fab-item" id="fab-add-pm" title="Tambah Penduduk Miskin"><i class="fas fa-user-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Modal Rumah Ibadah -->
|
||||
<div class="modal-backdrop" id="modal-ri">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="modal-ri-title">Tambah Rumah Ibadah</h2>
|
||||
<button class="btn-close" onclick="closeModal('modal-ri')"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="form-ri">
|
||||
<input type="hidden" id="ri-id">
|
||||
<div class="form-group">
|
||||
<label>Nama Rumah Ibadah</label>
|
||||
<input type="text" id="ri-nama" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Jenis</label>
|
||||
<select id="ri-jenis" class="form-control" required>
|
||||
<option value="Masjid">Masjid</option>
|
||||
<option value="Gereja">Gereja</option>
|
||||
<option value="Pura">Pura</option>
|
||||
<option value="Vihara">Vihara</option>
|
||||
<option value="Klenteng">Klenteng</option>
|
||||
<option value="Lainnya">Lainnya</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Kontak</label>
|
||||
<input type="text" id="ri-kontak" class="form-control">
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; gap: 10px;">
|
||||
<div style="flex: 1;">
|
||||
<label>Latitude</label>
|
||||
<input type="number" step="any" id="ri-lat" class="form-control" required readonly>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label>Longitude</label>
|
||||
<input type="number" step="any" id="ri-lng" class="form-control" required readonly>
|
||||
</div>
|
||||
<div style="display: flex; align-items: flex-end;">
|
||||
<button type="button" class="btn btn-secondary" onclick="pickLocation('ri')"><i class="fas fa-map-marker-alt"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Alamat (Otomatis)</label>
|
||||
<textarea id="ri-alamat" class="form-control" rows="2" readonly></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Radius Pelayanan (meter)</label>
|
||||
<input type="number" id="ri-radius" class="form-control" value="500" required>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal('modal-ri')">Batal</button>
|
||||
<button class="btn btn-primary" onclick="saveRI()">Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Penduduk Miskin -->
|
||||
<div class="modal-backdrop" id="modal-pm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="modal-pm-title">Tambah Penduduk Miskin</h2>
|
||||
<button class="btn-close" onclick="closeModal('modal-pm')"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="form-pm">
|
||||
<input type="hidden" id="pm-id">
|
||||
|
||||
<h3 style="margin-bottom: 12px; border-bottom: 1px solid #eee; padding-bottom: 8px;">Data Kepala Keluarga</h3>
|
||||
<div class="form-group">
|
||||
<label>Nama Kepala Keluarga</label>
|
||||
<input type="text" id="pm-nama" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>NIK</label>
|
||||
<input type="text" id="pm-nik" class="form-control">
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; gap: 10px;">
|
||||
<div style="flex: 1;">
|
||||
<label>Latitude</label>
|
||||
<input type="number" step="any" id="pm-lat" class="form-control" required readonly>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label>Longitude</label>
|
||||
<input type="number" step="any" id="pm-lng" class="form-control" required readonly>
|
||||
</div>
|
||||
<div style="display: flex; align-items: flex-end;">
|
||||
<button type="button" class="btn btn-secondary" onclick="pickLocation('pm')"><i class="fas fa-map-marker-alt"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Alamat</label>
|
||||
<textarea id="pm-alamat" class="form-control" rows="2" readonly></textarea>
|
||||
</div>
|
||||
|
||||
<h3 style="margin: 20px 0 12px; border-bottom: 1px solid #eee; padding-bottom: 8px;">Kebutuhan & Kondisi</h3>
|
||||
<div class="form-group">
|
||||
<label>Kategori Kebutuhan</label>
|
||||
<select id="pm-kategori-kebutuhan" class="form-control">
|
||||
<option value="">Pilih Kebutuhan</option>
|
||||
<option value="Sembako">Sembako</option>
|
||||
<option value="Pendidikan">Pendidikan</option>
|
||||
<option value="Kesehatan">Kesehatan</option>
|
||||
<option value="Modal Usaha">Modal Usaha</option>
|
||||
<option value="Rumah Tidak Layak">Rumah Tidak Layak</option>
|
||||
<option value="Bantuan Darurat">Bantuan Darurat</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Deskripsi Kebutuhan</label>
|
||||
<textarea id="pm-deskripsi-kebutuhan" class="form-control" rows="2" placeholder="Contoh: butuh sembako bulanan, biaya obat, seragam sekolah"></textarea>
|
||||
</div>
|
||||
<div class="form-group" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<div>
|
||||
<label>Kondisi Rumah</label>
|
||||
<select id="pm-kondisi-rumah" class="form-control">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
<option value="Layak">Layak</option>
|
||||
<option value="Rentan">Rentan</option>
|
||||
<option value="Tidak Layak">Tidak Layak</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Penghasilan / Bulan</label>
|
||||
<input type="number" id="pm-penghasilan" class="form-control" min="0" inputmode="numeric" placeholder="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin: 20px 0 12px; border-bottom: 1px solid #eee; padding-bottom: 8px;">Status Bantuan</h3>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select id="pm-status" class="form-control" onchange="toggleBantuanFields()" required>
|
||||
<option value="Belum dibantu">Belum Dibantu</option>
|
||||
<option value="Menunggu verifikasi">Menunggu Verifikasi</option>
|
||||
<option value="Sudah dibantu">Sudah Dibantu</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="bantuan-fields" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label>Jenis Bantuan</label>
|
||||
<input type="text" id="pm-jenis-bantuan" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tanggal Bantuan</label>
|
||||
<input type="date" id="pm-tanggal-bantuan" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin: 20px 0 12px; border-bottom: 1px solid #eee; padding-bottom: 8px;">Survey Lapangan</h3>
|
||||
<div class="form-group">
|
||||
<label>Status Tindak Lanjut</label>
|
||||
<select id="pm-tindak-lanjut" class="form-control">
|
||||
<option value="Belum disurvei">Belum Disurvei</option>
|
||||
<option value="Perlu survei">Perlu Survei</option>
|
||||
<option value="Sudah disurvei">Sudah Disurvei</option>
|
||||
<option value="Sudah diverifikasi">Sudah Diverifikasi</option>
|
||||
<option value="Sedang diproses">Sedang Diproses</option>
|
||||
<option value="Sudah dibantu">Sudah Dibantu</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Catatan Survey</label>
|
||||
<textarea id="pm-catatan-survei" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Bukti / Foto / Dokumen</label>
|
||||
<input type="file" id="pm-bukti-file" class="form-control" accept="image/*,.pdf">
|
||||
<input type="hidden" id="pm-existing-bukti-file">
|
||||
</div>
|
||||
|
||||
<h3 style="margin: 20px 0 12px; border-bottom: 1px solid #eee; padding-bottom: 8px; display: flex; justify-content: space-between;">
|
||||
Anggota Keluarga
|
||||
<button type="button" class="btn btn-secondary" style="padding: 4px 8px; font-size: 0.8rem;" onclick="addAnggotaField()"><i class="fas fa-plus"></i> Tambah</button>
|
||||
</h3>
|
||||
<div id="anggota-container" class="dynamic-list">
|
||||
<!-- Dynamic fields here -->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal('modal-pm')">Batal</button>
|
||||
<button class="btn btn-primary" onclick="savePM()">Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Riwayat Bantuan -->
|
||||
<div class="modal-backdrop" id="modal-bantuan">
|
||||
<div class="modal-content modal-content-sm">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<p class="modal-kicker">Penyaluran Bantuan</p>
|
||||
<h2 id="modal-bantuan-title">Tambah Riwayat Bantuan</h2>
|
||||
</div>
|
||||
<button class="btn-close" onclick="closeModal('modal-bantuan')"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="form-bantuan">
|
||||
<input type="hidden" id="bantuan-penduduk-id">
|
||||
<div class="form-group">
|
||||
<label>Keluarga Penerima</label>
|
||||
<input type="text" id="bantuan-penerima" class="form-control" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Jenis Bantuan</label>
|
||||
<input type="text" id="bantuan-jenis" class="form-control" list="bantuan-jenis-list" placeholder="Contoh: Sembako bulanan" required>
|
||||
<datalist id="bantuan-jenis-list">
|
||||
<option value="Sembako">
|
||||
<option value="Pendidikan">
|
||||
<option value="Kesehatan">
|
||||
<option value="Modal Usaha">
|
||||
<option value="Renovasi Rumah">
|
||||
<option value="Bantuan Darurat">
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="form-grid two-cols">
|
||||
<div class="form-group">
|
||||
<label>Tanggal Bantuan</label>
|
||||
<input type="date" id="bantuan-tanggal" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nilai Bantuan</label>
|
||||
<input type="number" id="bantuan-nilai" class="form-control" min="0" placeholder="Opsional">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Pemberi Bantuan</label>
|
||||
<input type="text" id="bantuan-pemberi" class="form-control" value="Admin">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Catatan</label>
|
||||
<textarea id="bantuan-catatan" class="form-control" rows="3" placeholder="Contoh: bantuan diserahkan langsung ke rumah penerima"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Bukti Bantuan</label>
|
||||
<input type="file" id="bantuan-bukti-file" class="form-control" accept="image/*,.pdf">
|
||||
<p class="form-hint">Opsional, bisa berupa foto penyerahan atau dokumen PDF.</p>
|
||||
</div>
|
||||
<div class="form-alert" id="bantuan-alert" hidden></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal('modal-bantuan')">Batal</button>
|
||||
<button class="btn btn-primary" id="btn-save-bantuan" onclick="saveBantuan()">Simpan Bantuan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<script src="assets/js/app.js?v=<?= filemtime(__DIR__ . '/assets/js/app.js') ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/api/db_pdo.php';
|
||||
require_once __DIR__ . '/api/auth.php';
|
||||
require_once __DIR__ . '/api/penduduk_feature_helper.php';
|
||||
require_admin_login();
|
||||
|
||||
ensurePendudukFeatureSchema($pdo);
|
||||
refreshAllPriorityScores($pdo);
|
||||
|
||||
$status = trim((string)($_GET['status'] ?? ''));
|
||||
$priority = trim((string)($_GET['prioritas'] ?? ''));
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($status !== '') {
|
||||
$where[] = 'p.status_bantuan = ?';
|
||||
$params[] = $status;
|
||||
}
|
||||
|
||||
if ($priority === 'tinggi') {
|
||||
$where[] = 'p.skor_prioritas >= 70';
|
||||
} elseif ($priority === 'sedang') {
|
||||
$where[] = 'p.skor_prioritas BETWEEN 40 AND 69';
|
||||
} elseif ($priority === 'rendah') {
|
||||
$where[] = 'p.skor_prioritas < 40';
|
||||
}
|
||||
|
||||
$sql = "
|
||||
SELECT p.*, r.nama AS rumah_ibadah_nama
|
||||
FROM penduduk_miskin p
|
||||
LEFT JOIN rumah_ibadah r ON p.ibadah_id = r.id
|
||||
";
|
||||
|
||||
if ($where) {
|
||||
$sql .= ' WHERE ' . implode(' AND ', $where);
|
||||
}
|
||||
|
||||
$sql .= ' ORDER BY p.skor_prioritas DESC, p.id DESC';
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$summary = [
|
||||
'total' => count($rows),
|
||||
'tinggi' => count(array_filter($rows, fn($r) => (int)$r['skor_prioritas'] >= 70)),
|
||||
'belum' => count(array_filter($rows, fn($r) => $r['status_bantuan'] === 'Belum dibantu')),
|
||||
'luar' => count(array_filter($rows, fn($r) => (int)$r['is_blank_spot'] === 1)),
|
||||
];
|
||||
|
||||
function e(?string $value): string
|
||||
{
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Laporan Prioritas Bantuan</title>
|
||||
<link rel="stylesheet" href="assets/css/report.css?v=<?= filemtime(__DIR__ . '/assets/css/report.css') ?>">
|
||||
</head>
|
||||
<body>
|
||||
<main class="report-page">
|
||||
<header class="report-header">
|
||||
<div>
|
||||
<p class="eyebrow">WebGIS Peduli</p>
|
||||
<h1>Laporan Prioritas Bantuan</h1>
|
||||
<p>Dicetak pada <?= date('d/m/Y H:i') ?> oleh <?= e(current_admin_name()) ?></p>
|
||||
</div>
|
||||
<button onclick="window.print()" class="print-btn">Cetak / Simpan PDF</button>
|
||||
</header>
|
||||
|
||||
<section class="summary-grid">
|
||||
<div><span>Total Data</span><strong><?= $summary['total'] ?></strong></div>
|
||||
<div><span>Prioritas Tinggi</span><strong><?= $summary['tinggi'] ?></strong></div>
|
||||
<div><span>Belum Dibantu</span><strong><?= $summary['belum'] ?></strong></div>
|
||||
<div><span>Luar Radius</span><strong><?= $summary['luar'] ?></strong></div>
|
||||
</section>
|
||||
|
||||
<section class="report-note">
|
||||
<strong>Keterangan:</strong> Skor prioritas dihitung dari status bantuan, jumlah tanggungan, lansia/balita,
|
||||
penghasilan, kondisi rumah, kebutuhan mendesak, dan posisi terhadap radius layanan.
|
||||
</section>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>Kepala Keluarga</th>
|
||||
<th>Prioritas</th>
|
||||
<th>Kebutuhan</th>
|
||||
<th>Status</th>
|
||||
<th>Tindak Lanjut</th>
|
||||
<th>Rumah Ibadah</th>
|
||||
<th>Jarak</th>
|
||||
<th>Catatan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $i => $row): ?>
|
||||
<tr>
|
||||
<td><?= $i + 1 ?></td>
|
||||
<td>
|
||||
<strong><?= e($row['kk_nama']) ?></strong><br>
|
||||
<small><?= e($row['alamat']) ?></small>
|
||||
</td>
|
||||
<td>
|
||||
<strong><?= (int)$row['skor_prioritas'] ?></strong><br>
|
||||
<small><?= e(getPriorityLabel((int)$row['skor_prioritas'])) ?></small>
|
||||
</td>
|
||||
<td><?= e($row['kategori_kebutuhan'] ?: 'Belum diisi') ?></td>
|
||||
<td><?= e($row['status_bantuan']) ?></td>
|
||||
<td><?= e($row['tindak_lanjut'] ?: 'Belum disurvei') ?></td>
|
||||
<td><?= e($row['rumah_ibadah_nama'] ?: '-') ?></td>
|
||||
<td>
|
||||
Lurus: <?= e((string)$row['jarak_m']) ?> m<br>
|
||||
<small>Rute: <?= e((string)($row['jarak_rute_m'] ?? '-')) ?> m</small>
|
||||
</td>
|
||||
<td><?= e($row['catatan_survei'] ?: $row['deskripsi_kebutuhan'] ?: '-') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (!$rows): ?>
|
||||
<tr><td colspan="9" class="empty">Tidak ada data sesuai filter.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/api/db_pdo.php';
|
||||
require_once __DIR__ . '/api/auth.php';
|
||||
|
||||
start_admin_session();
|
||||
ensure_admin_table($pdo);
|
||||
|
||||
if (is_admin_logged_in()) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$token = (string)($_POST['csrf_token'] ?? '');
|
||||
$username = trim((string)($_POST['username'] ?? ''));
|
||||
$password = (string)($_POST['password'] ?? '');
|
||||
|
||||
if (!hash_equals((string)$_SESSION['csrf_token'], $token)) {
|
||||
$error = 'Sesi form tidak valid. Muat ulang halaman lalu coba lagi.';
|
||||
} elseif ($username === '' || $password === '') {
|
||||
$error = 'Username dan password wajib diisi.';
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT * FROM admins WHERE username = ? AND is_active = 1 LIMIT 1");
|
||||
$stmt->execute([$username]);
|
||||
$admin = $stmt->fetch();
|
||||
|
||||
if ($admin && password_verify($password, $admin['password_hash'])) {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['admin_id'] = (int)$admin['id'];
|
||||
$_SESSION['admin_username'] = $admin['username'];
|
||||
$_SESSION['admin_name'] = $admin['nama'];
|
||||
unset($_SESSION['csrf_token']);
|
||||
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = 'Username atau password salah.';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login Admin - WebGIS Peduli</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="assets/css/login.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="login-page">
|
||||
<section class="login-panel">
|
||||
<div class="brand-mark">
|
||||
<i class="fas fa-map-location-dot"></i>
|
||||
</div>
|
||||
<p class="eyebrow">Admin Area</p>
|
||||
<h1>Masuk ke WebGIS Peduli</h1>
|
||||
<p class="subtitle">Gunakan akun admin untuk mengelola data peta, warga, dan rumah ibadah.</p>
|
||||
|
||||
<?php if ($error !== ''): ?>
|
||||
<div class="alert-error">
|
||||
<i class="fas fa-circle-exclamation"></i>
|
||||
<span><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" class="login-form" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars((string)$_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8') ?>">
|
||||
|
||||
<label class="input-group">
|
||||
<span>Username</span>
|
||||
<div class="input-shell">
|
||||
<i class="fas fa-user"></i>
|
||||
<input type="text" name="username" placeholder="admin" required autofocus>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="input-group">
|
||||
<span>Password</span>
|
||||
<div class="input-shell">
|
||||
<i class="fas fa-lock"></i>
|
||||
<input type="password" name="password" placeholder="Masukkan password" required>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn-login">
|
||||
<span>Masuk</span>
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="login-hint">Akun awal: <strong>admin</strong> / <strong>admin123</strong></p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/api/auth.php';
|
||||
|
||||
start_admin_session();
|
||||
$_SESSION = [];
|
||||
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(
|
||||
session_name(),
|
||||
'',
|
||||
time() - 42000,
|
||||
$params['path'],
|
||||
$params['domain'],
|
||||
(bool)$params['secure'],
|
||||
(bool)$params['httponly']
|
||||
);
|
||||
}
|
||||
|
||||
session_destroy();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/api/auth.php';
|
||||
require_admin_login();
|
||||
|
||||
function e(string $value): string
|
||||
{
|
||||
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
$baseUrl = 'http://localhost/Leaflet2/';
|
||||
$projectUrl = '[ISI URL PROJECT KELAS DI SINI]';
|
||||
$giteaUrl = '[https://git.ifuntanhub.dev/Arif1031/WebGIS-PovertyMap.git]';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Panduan Penggunaan WebGIS Poverty Map</title>
|
||||
<link rel="stylesheet" href="assets/css/guide.css?v=<?= filemtime(__DIR__ . '/assets/css/guide.css') ?>">
|
||||
</head>
|
||||
<body>
|
||||
<main class="guide-page">
|
||||
<header class="guide-hero">
|
||||
<div>
|
||||
<p class="eyebrow">Panduan Penggunaan</p>
|
||||
<h1>WebGIS Poverty Map</h1>
|
||||
<p class="lead">Panduan singkat untuk mengakses, mengelola data peta, mencatat bantuan, dan mencetak laporan prioritas masyarakat miskin/kurang mampu.</p>
|
||||
</div>
|
||||
<button class="print-btn" onclick="window.print()">Cetak / Simpan PDF</button>
|
||||
</header>
|
||||
|
||||
<section class="access-panel">
|
||||
<h2>Informasi Akses Project</h2>
|
||||
<div class="info-grid">
|
||||
<div>
|
||||
<span>URL Aplikasi Lokal</span>
|
||||
<strong><?= e($baseUrl) ?></strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>URL Project Kelas</span>
|
||||
<strong><?= e($projectUrl) ?></strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Link Repo Gitea</span>
|
||||
<strong><?= e($giteaUrl) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Akun dan Role</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Role</th>
|
||||
<th>Username</th>
|
||||
<th>Password</th>
|
||||
<th>Hak Akses</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Admin</td>
|
||||
<td><code>admin</code></td>
|
||||
<td><code>admin123</code></td>
|
||||
<td>Mengelola dashboard, peta, rumah ibadah, penduduk miskin, bantuan, bukti, dan laporan.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="note">Versi sistem saat ini hanya memiliki satu role aktif, yaitu Admin. Jika role baru ditambahkan, akun dapat ditulis pada tabel ini.</p>
|
||||
</section>
|
||||
|
||||
<section class="steps">
|
||||
<h2>Alur Penggunaan Utama</h2>
|
||||
<ol>
|
||||
<li>Buka URL aplikasi, lalu login menggunakan akun admin.</li>
|
||||
<li>Periksa ringkasan data pada Dashboard untuk melihat jumlah rumah ibadah, warga, status bantuan, prioritas, dan warga luar radius.</li>
|
||||
<li>Buka menu Peta untuk melihat marker rumah ibadah, radius layanan, dan marker warga.</li>
|
||||
<li>Gunakan pencarian dan filter untuk menemukan warga, rumah ibadah, status bantuan, jenis rumah ibadah, atau kategori kebutuhan.</li>
|
||||
<li>Tambah atau edit rumah ibadah melalui menu atau tombol tambah, lalu pilih titik koordinat di peta.</li>
|
||||
<li>Tambah atau edit penduduk miskin, lengkapi kebutuhan, kondisi rumah, penghasilan, status survei, anggota keluarga, dan bukti jika ada.</li>
|
||||
<li>Gunakan tombol Bantuan pada popup warga untuk mencatat riwayat bantuan tanpa popup browser.</li>
|
||||
<li>Buka Laporan untuk mencetak atau menyimpan daftar prioritas bantuan sebagai PDF.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Keterangan Warna Peta</h2>
|
||||
<div class="legend-grid">
|
||||
<div><span class="dot worship"></span><strong>Rumah Ibadah</strong><p>Pusat layanan atau rujukan bantuan sosial.</p></div>
|
||||
<div><span class="dot reached"></span><strong>Warga Dalam Radius</strong><p>Warga berada dalam radius layanan rumah ibadah.</p></div>
|
||||
<div><span class="dot outside"></span><strong>Warga Luar Radius</strong><p>Warga berada di luar semua radius layanan.</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Catatan Pengisian Data</h2>
|
||||
<ul>
|
||||
<li>Radius default rumah ibadah adalah 500 meter dan dapat diubah oleh admin.</li>
|
||||
<li>Status dalam/luar radius dihitung menggunakan jarak lurus pada peta.</li>
|
||||
<li>Jarak rute jalan ditampilkan sebagai informasi tambahan jika layanan OSRM berhasil diakses.</li>
|
||||
<li>Penghasilan tidak boleh bernilai negatif.</li>
|
||||
<li>Bukti bantuan atau survei dapat berupa JPG, PNG, WEBP, atau PDF dengan ukuran maksimal 3 MB.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,76 @@
|
||||
DROP DATABASE IF EXISTS webgis_miskin;
|
||||
CREATE DATABASE webgis_miskin CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE webgis_miskin;
|
||||
|
||||
CREATE TABLE 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;
|
||||
|
||||
INSERT INTO admins (username, password_hash, nama) VALUES
|
||||
('admin', '$2y$10$7O061pVz/aZ5YNK/fQyYIePIPnzDb.uZn38pC/Te3s7gRyko20dIK', 'Administrator');
|
||||
|
||||
CREATE TABLE rumah_ibadah (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(150) NOT NULL,
|
||||
jenis ENUM('Masjid', 'Gereja', 'Pura', 'Vihara', 'Klenteng', 'Lainnya') NOT NULL,
|
||||
kontak VARCHAR(50),
|
||||
alamat TEXT,
|
||||
lat DECIMAL(10, 8) NOT NULL,
|
||||
lng DECIMAL(11, 8) NOT NULL,
|
||||
radius INT NOT NULL DEFAULT 500,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE penduduk_miskin (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
kk_nama VARCHAR(150) NOT NULL,
|
||||
kk_nik VARCHAR(20),
|
||||
alamat TEXT,
|
||||
lat DECIMAL(10, 8) NOT NULL,
|
||||
lng DECIMAL(11, 8) NOT NULL,
|
||||
ibadah_id INT NULL,
|
||||
jarak_m DECIMAL(10, 2) NULL,
|
||||
jarak_rute_m DECIMAL(10, 2) NULL,
|
||||
is_blank_spot TINYINT(1) NOT NULL DEFAULT 1,
|
||||
status_bantuan ENUM('Sudah dibantu', 'Belum dibantu', 'Menunggu verifikasi') NOT NULL DEFAULT 'Belum dibantu',
|
||||
kategori_kebutuhan VARCHAR(100),
|
||||
deskripsi_kebutuhan TEXT,
|
||||
tindak_lanjut VARCHAR(60) NOT NULL DEFAULT 'Belum disurvei',
|
||||
kondisi_rumah VARCHAR(80),
|
||||
penghasilan INT,
|
||||
catatan_survei TEXT,
|
||||
bukti_file VARCHAR(255),
|
||||
skor_prioritas INT NOT NULL DEFAULT 0,
|
||||
jenis_bantuan VARCHAR(100),
|
||||
tanggal_bantuan DATE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE anggota_keluarga (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
penduduk_id INT NOT NULL,
|
||||
nama VARCHAR(150) NOT NULL,
|
||||
hubungan VARCHAR(50) NOT NULL,
|
||||
umur INT,
|
||||
pekerjaan VARCHAR(100),
|
||||
FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE 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),
|
||||
pemberi_bantuan VARCHAR(120),
|
||||
catatan TEXT,
|
||||
bukti_file VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
Reference in New Issue
Block a user