Initial commit

This commit is contained in:
2026-06-10 00:49:56 +07:00
commit 813cfcaf9f
27 changed files with 24734 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
<?php
/**
* get_data.php v3 — Ambil Semua Data
*/
require_once 'koneksi.php';
$response = ['success'=>true, 'centers'=>[], 'houses'=>[], 'reports'=>[]];
// ======================== Centers
$result = $conn->query("SELECT * FROM religious_centers ORDER BY id ASC");
if ($result) {
while ($row = $result->fetch_assoc()) {
$response['centers'][] = [
'id' => (int)$row['id'],
'name' => $row['name'],
'address' => $row['address'],
'kas' => (float)($row['kas'] ?? 0),
'latitude' => (float)$row['latitude'],
'longitude' => (float)$row['longitude'],
'radius' => (int)$row['radius']
];
}
$result->free();
}
// ======================== Houses (all fields incl. anggota JSON)
$result = $conn->query("SELECT * FROM houses ORDER BY id ASC");
if ($result) {
while ($row = $result->fetch_assoc()) {
$anggota = [];
if (!empty($row['anggota'])) {
$decoded = json_decode($row['anggota'], true);
if (is_array($decoded)) $anggota = $decoded;
}
$response['houses'][] = [
'id' => (int)$row['id'],
'latitude' => (float)$row['latitude'],
'longitude' => (float)$row['longitude'],
'address' => $row['address'] ?? '',
'rt' => $row['rt'] ?? '',
'rw' => $row['rw'] ?? '',
'kelurahan' => $row['kelurahan'] ?? '',
'status_miskin' => $row['status_miskin'] ?? '',
'jumlah_anggota'=> (int)($row['jumlah_anggota'] ?? 0),
'anggota' => $anggota,
'aid_status' => $row['aid_status'] ?? 'outside',
'has_data' => (int)($row['has_data'] ?? 0)
];
}
$result->free();
}
// ======================== Reports
$result = $conn->query("SELECT * FROM laporan ORDER BY created_at DESC LIMIT 50");
if ($result) {
while ($row = $result->fetch_assoc()) {
$response['reports'][] = [
'id' => (int)$row['id'],
'name' => $row['pelapor'] ?? 'Anonim',
'text' => $row['deskripsi'],
'imgBase64'=> $row['foto_base64'] ?? null,
'time' => date('d/m/Y H:i', strtotime($row['created_at']))
];
}
$result->free();
}
echo json_encode($response);
$conn->close();
+20
View File
@@ -0,0 +1,20 @@
<?php
/**
* hapus_pusat.php — Hapus Rumah Ibadah
* POST: id
*/
require_once 'koneksi.php';
$id = intval($_POST['id'] ?? 0);
if ($id < 1) { echo json_encode(['success'=>false,'message'=>'ID tidak valid']); exit; }
// Hapus log yang terkait
$conn->query("DELETE FROM aid_logs WHERE religious_center_id=$id");
if ($conn->query("DELETE FROM religious_centers WHERE id=$id")) {
echo json_encode(['success'=>true,'id'=>$id]);
} else {
echo json_encode(['success'=>false,'message'=>$conn->error]);
}
$conn->close();
+20
View File
@@ -0,0 +1,20 @@
<?php
/**
* hapus_rumah.php — Hapus Data Rumah Miskin
* POST: id
*/
require_once 'koneksi.php';
$id = intval($_POST['id'] ?? 0);
if ($id < 1) { echo json_encode(['success'=>false,'message'=>'ID tidak valid']); exit; }
// Hapus log yang terkait
$conn->query("DELETE FROM aid_logs WHERE house_id=$id");
if ($conn->query("DELETE FROM houses WHERE id=$id")) {
echo json_encode(['success'=>true,'id'=>$id]);
} else {
echo json_encode(['success'=>false,'message'=>$conn->error]);
}
$conn->close();
+410
View File
@@ -0,0 +1,410 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS — Distribusi Bantuan Sosial Pontianak</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css"/>
</head>
<body>
<!-- =====================================================================
TOP NAVBAR — selalu terlihat di semua halaman
===================================================================== -->
<nav id="topNav">
<!-- Kiri: Logo -->
<div class="nav-brand">
<span class="nav-brand-icon">🕌</span>
<div>
<div class="nav-brand-title">BantSOSial GIS</div>
<div class="nav-brand-sub">Pontianak · Kalimantan Barat</div>
</div>
</div>
<!-- Tengah: Menu navigasi -->
<div class="nav-menu">
<button class="nav-item active" id="navHome" onclick="navigateTo('map')">
<span class="nav-item-icon">🗺</span>
<span class="nav-item-label">Peta</span>
</button>
<button class="nav-item" id="navReport" onclick="navigateTo('report')">
<span class="nav-item-icon">📢</span>
<span class="nav-item-label">Pelaporan</span>
<span class="nav-badge hidden" id="navReportBadge">0</span>
</button>
</div>
<!-- Kanan: Role switcher -->
<div class="nav-right">
<div id="roleIndicator" class="role-pill">
<span id="roleIcon">👑</span>
<span id="roleLabel">Admin</span>
</div>
<button id="roleSwitchBtn" onclick="openRoleModal()">Ganti Role ▾</button>
</div>
</nav>
<!-- =====================================================================
ROLE MODAL
===================================================================== -->
<div id="roleModal" class="modal-overlay hidden">
<div class="modal-box" style="max-width:420px">
<div class="modal-header">
<div class="modal-title">🔐 Pilih Role Pengguna</div>
<button class="modal-close" onclick="closeRoleModal()"></button>
</div>
<div class="modal-body" style="padding:16px">
<p style="font-size:12px;color:var(--text-muted);margin-bottom:14px">Pilih role sesuai hak akses Anda. Perubahan langsung berlaku.</p>
<div class="role-cards">
<div class="role-card" id="roleCard_admin" onclick="setRole('admin')">
<div class="role-card-icon">👑</div>
<div class="role-card-name">Admin</div>
<div class="role-card-desc">Akses penuh — tambah, edit, hapus, lihat semua data dan laporan</div>
<div class="role-card-perms">
<span class="perm perm-green">✓ Tambah & Edit</span>
<span class="perm perm-green">✓ Hapus & Reset</span>
<span class="perm perm-green">✓ Lihat laporan</span>
<span class="perm perm-green">✓ Kirim laporan</span>
</div>
</div>
<div class="role-card" id="roleCard_surveyer" onclick="setRole('surveyer')">
<div class="role-card-icon">📋</div>
<div class="role-card-name">Surveyer</div>
<div class="role-card-desc">Input data & kelola laporan — tidak bisa hapus atau reset</div>
<div class="role-card-perms">
<span class="perm perm-green">✓ Tambah & Edit</span>
<span class="perm perm-red">✕ Hapus & Reset</span>
<span class="perm perm-green">✓ Lihat laporan</span>
<span class="perm perm-green">✓ Kirim laporan</span>
</div>
</div>
<div class="role-card" id="roleCard_viewer" onclick="setRole('viewer')">
<div class="role-card-icon">👁</div>
<div class="role-card-name">Viewer</div>
<div class="role-card-desc">Hanya lihat peta & kirim laporan masyarakat</div>
<div class="role-card-perms">
<span class="perm perm-green">✓ Lihat peta</span>
<span class="perm perm-red">✕ Edit data</span>
<span class="perm perm-red">✕ Lihat laporan</span>
<span class="perm perm-green">✓ Kirim laporan</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- =====================================================================
PAGE: PETA (default)
===================================================================== -->
<div id="pagePeta" class="page active-page">
<!-- SIDEBAR -->
<div id="sidebar">
<div id="modeIndicator" class="mode-badge hidden">
<span class="pulse-dot"></span>
<span id="modeIndicatorText">Klik peta untuk menempatkan titik</span>
</div>
<!-- Viewer notice -->
<div id="viewerNotice" style="display:none;margin:10px 14px 0;padding:10px 12px;background:var(--purple-light);border:1px solid #c4b5fd;border-radius:8px;font-size:11px;color:#5b21b6;font-weight:500;text-align:center;">
👁 Mode Viewer — Anda hanya dapat melihat data
</div>
<!-- Action buttons -->
<div class="btn-group" id="actionBtnGroup">
<button id="btnAddCenter" onclick="startAddingCenter()">🕌 Tambah Rumah Ibadah</button>
<button id="btnAddHouse" onclick="startAddingHouse()">🏠 Tambah Rumah Miskin</button>
</div>
<!-- Stats -->
<div class="stat-panel">
<div class="stat-title">📊 Statistik Bantuan</div>
<div class="stat-grid">
<div class="stat-card">
<div class="stat-num" id="statTotal">0</div>
<div class="stat-label">Total Rumah</div>
</div>
<div class="stat-card red">
<div class="stat-num" id="statInside">0</div>
<div class="stat-label">Dalam Radius</div>
<div class="stat-icon">🔴</div>
</div>
<div class="stat-card yellow">
<div class="stat-num" id="statHelped">0</div>
<div class="stat-label">Sudah Dibantu</div>
<div class="stat-icon">🟡</div>
</div>
<div class="stat-card green">
<div class="stat-num" id="statOutside">0</div>
<div class="stat-label">Luar Radius</div>
<div class="stat-icon">🟢</div>
</div>
</div>
</div>
<!-- Rumah Ibadah -->
<div class="section">
<div class="section-title">
🕌 Rumah Ibadah
<span id="centerCount" class="badge badge-teal">0</span>
</div>
<div class="search-wrap">
<input type="text" id="searchCenter" class="search-input" placeholder="🔍 Cari nama rumah ibadah..." oninput="filterCenters()"/>
</div>
<select id="sortCenter" class="filter-select" onchange="filterCenters()">
<option value="">— Urutkan —</option>
<option value="kas_desc">💰 Kas terbanyak</option>
<option value="kas_asc">💰 Kas terkecil</option>
<option value="tanggungan_desc">👨‍👩‍👧 Tanggungan terbanyak</option>
<option value="tanggungan_asc">👨‍👩‍👧 Tanggungan terkecil</option>
</select>
<div id="centerList"><div class="empty-state">Belum ada rumah ibadah.</div></div>
</div>
<!-- Rumah Miskin -->
<div class="section">
<div class="section-title">
🏠 Data Rumah Miskin
<span id="houseCount" class="badge badge-orange">0</span>
</div>
<div class="search-wrap">
<input type="text" id="searchHouse" class="search-input" placeholder="🔍 Cari nama kepala keluarga..." oninput="filterHouses()"/>
</div>
<div class="filter-row">
<select id="filterStatus" class="filter-select" onchange="filterHouses()">
<option value="">Semua Status Bantuan</option>
<option value="yellow">✅ Sudah dibantu</option>
<option value="red">⏳ Belum dibantu</option>
<option value="green">🟢 Luar radius</option>
</select>
<select id="filterMiskin" class="filter-select" onchange="filterHouses()">
<option value="">Semua Kemiskinan</option>
<option value="sangat_miskin">😢 Sangat Miskin</option>
<option value="miskin">😟 Miskin</option>
<option value="tidak_miskin">😊 Tidak Miskin</option>
</select>
</div>
<div id="houseList"><div class="empty-state">Belum ada data rumah miskin.</div></div>
</div>
<!-- Legenda -->
<div class="section">
<div class="section-title">🗺 Legenda</div>
<div class="legend-list">
<div class="legend-item"><span style="font-size:13px">🕌</span> Masjid/Musholla</div>
<div class="legend-item"><span style="font-size:13px"></span> Gereja Katolik</div>
<div class="legend-item"><span style="font-size:13px">✝️</span> Gereja Protestan</div>
<div class="legend-item"><span style="font-size:13px">🛕</span> Vihara/Klenteng/Pura</div>
<div class="legend-item"><span class="legend-dot green"></span> Luar radius</div>
<div class="legend-item"><span class="legend-dot red"></span> Dalam radius</div>
<div class="legend-item"><span class="legend-dot yellow"></span> Sudah dibantu</div>
<div class="legend-item"><span class="legend-circle"></span> Area jangkauan</div>
</div>
</div>
<!-- Reset -->
<button id="btnReset" class="btn-danger" onclick="confirmReset()">🗑 Reset Semua Data</button>
</div>
<!-- MAP -->
<div id="map"></div>
<div id="radiusTooltip" class="radius-tooltip hidden"></div>
</div><!-- end #pagePeta -->
<!-- =====================================================================
PAGE: PELAPORAN
===================================================================== -->
<div id="pagePelaporan" class="page">
<div class="report-page">
<!-- KIRI: Form kirim laporan (semua role bisa) -->
<div class="report-col report-col-form">
<div class="report-col-header">
<div class="report-col-title">📢 Kirim Laporan</div>
<div class="report-col-sub">Laporkan kondisi kemiskinan yang belum terdata</div>
</div>
<div class="report-form-body">
<div class="form-group">
<label class="form-label">Deskripsi Laporan *</label>
<textarea id="reportText" class="form-input form-textarea" rows="5"
placeholder="Contoh: Ada keluarga miskin di Jl. Merdeka No.5 yang belum terdata, kondisi rumah sangat memprihatinkan dan membutuhkan bantuan segera..."></textarea>
</div>
<div class="form-group">
<label class="form-label">Nama Pelapor (opsional)</label>
<input type="text" id="reportName" class="form-input" placeholder="Nama Anda..."/>
</div>
<div class="form-group">
<label class="form-label">Lokasi / Alamat (opsional)</label>
<input type="text" id="reportLocation" class="form-input" placeholder="Jl. Contoh No.1, Kelurahan..."/>
</div>
<div class="form-group">
<label class="form-label">Foto / Bukti (opsional)</label>
<div class="upload-area" id="uploadArea" onclick="document.getElementById('reportImg').click()">
<div class="upload-icon">📷</div>
<div class="upload-text">Klik untuk upload foto</div>
<div class="upload-sub">JPG, PNG, maksimal 5MB</div>
<input type="file" id="reportImg" accept="image/*" style="display:none" onchange="previewImage(event)"/>
</div>
<div id="imgPreview" style="display:none;margin-top:8px;">
<img id="previewImg" style="width:100%;border-radius:8px;max-height:200px;object-fit:cover;"/>
<button class="remove-img-btn" onclick="removeImage()">✕ Hapus foto</button>
</div>
</div>
<button class="form-submit orange-btn" onclick="submitReport()" style="margin-top:4px;padding:12px;font-size:13px;">
📤 Kirim Laporan
</button>
</div>
</div>
<!-- KANAN: Daftar laporan (hanya admin & surveyer) -->
<div class="report-col report-col-list" id="reportColList">
<div class="report-col-header">
<div class="report-col-title">
📋 Laporan Masuk
<span id="reportCount" class="badge badge-orange" style="margin-left:8px">0</span>
</div>
<div class="report-col-sub">Daftar laporan yang diterima dari masyarakat</div>
</div>
<!-- Filter & search laporan -->
<div class="report-list-toolbar">
<input type="text" id="searchReport" class="search-input" placeholder="🔍 Cari laporan..." oninput="filterReports()" style="flex:1"/>
<select id="filterReportStatus" class="filter-select" onchange="filterReports()" style="width:auto;margin:0">
<option value="">Semua</option>
<option value="baru">🆕 Baru</option>
<option value="ditangani">🔄 Ditangani</option>
<option value="selesai">✅ Selesai</option>
</select>
</div>
<div id="reportList" class="report-list-container">
<div class="empty-state" style="margin-top:20px">Belum ada laporan masuk.</div>
</div>
</div>
<!-- Viewer: panel info (tidak bisa lihat list) -->
<div class="report-col report-col-viewer" id="reportColViewer" style="display:none">
<div class="report-col-header">
<div class="report-col-title">👁 Mode Viewer</div>
<div class="report-col-sub">Daftar laporan hanya tersedia untuk Admin dan Surveyer</div>
</div>
<div style="padding:40px 20px;text-align:center">
<div style="font-size:48px;margin-bottom:16px">🔒</div>
<div style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:8px">Akses Terbatas</div>
<div style="font-size:12px;color:var(--text-muted);line-height:1.6">
Sebagai Viewer, Anda dapat mengirim laporan tapi tidak dapat melihat daftar laporan yang masuk.<br><br>
Hubungi Admin untuk mengubah role akses Anda.
</div>
</div>
</div>
</div>
</div><!-- end #pagePelaporan -->
<!-- =====================================================================
CUSTOM DATE PICKER
===================================================================== -->
<div id="datepickerOverlay" class="datepicker-overlay hidden" onclick="closeDatepickerIfOutside(event)">
<div class="datepicker-box" id="datepickerBox"></div>
</div>
<!-- =====================================================================
MODAL — FORM DATA RUMAH
===================================================================== -->
<div id="houseModal" class="modal-overlay hidden">
<div class="modal-box">
<div class="modal-header">
<div class="modal-title">🏠 Data Kemiskinan</div>
<button class="modal-close" onclick="closeHouseModal()"></button>
</div>
<div class="modal-body" id="houseModalBody"></div>
</div>
</div>
<!-- =====================================================================
MODAL — DETAIL RUMAH
===================================================================== -->
<div id="detailModal" class="modal-overlay hidden">
<div class="modal-box modal-wide">
<div class="modal-header">
<div class="modal-title">📋 Detail Data Rumah</div>
<button class="modal-close" onclick="closeDetailModal()"></button>
</div>
<div class="modal-body" id="detailModalBody"></div>
</div>
</div>
<!-- =====================================================================
MODAL — DETAIL LAPORAN (admin/surveyer)
===================================================================== -->
<div id="reportDetailModal" class="modal-overlay hidden">
<div class="modal-box" style="max-width:500px">
<div class="modal-header">
<div class="modal-title">📋 Detail Laporan</div>
<button class="modal-close" onclick="closeReportDetail()"></button>
</div>
<div class="modal-body" id="reportDetailBody"></div>
</div>
</div>
<!-- =====================================================================
MODAL — EDIT RUMAH IBADAH
===================================================================== -->
<div id="centerEditModal" class="modal-overlay hidden">
<div class="modal-box" style="max-width:460px">
<div class="modal-header" style="background:linear-gradient(135deg,#f0fdf4,#e0f2fe)">
<div class="modal-title">🕌 Edit Rumah Ibadah</div>
<button class="modal-close" onclick="closeCenterEditModal()"></button>
</div>
<div class="modal-body" id="centerEditModalBody">
<input type="hidden" id="ce_id"/>
<div class="form-group">
<label class="form-label">Jenis Rumah Ibadah</label>
<select class="form-input" id="ce_type" onchange="updateCenterEditHeader()">
<option value="masjid">🕌 Masjid / Musholla</option>
<option value="gereja katedral">⛪ Gereja Katolik / Katedral</option>
<option value="gereja protestan">✝️ Gereja Protestan / Kapel</option>
<option value="vihara">🛕 Vihara / Klenteng / Pura</option>
<option value="sinagog">✡️ Sinagog</option>
<option value="default">🏛️ Lainnya</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Nama Rumah Ibadah *</label>
<input class="form-input" id="ce_name" type="text" placeholder="Nama rumah ibadah..."/>
</div>
<div class="form-group">
<label class="form-label">Alamat</label>
<input class="form-input" id="ce_address" type="text" placeholder="Alamat..."/>
</div>
<div class="form-row">
<div class="form-group" style="flex:1">
<label class="form-label">Kas / Dana (Rp)</label>
<input class="form-input" id="ce_kas" type="number" min="0" step="1000" placeholder="0"/>
</div>
<div class="form-group" style="flex:1">
<label class="form-label">Radius (meter)</label>
<input class="form-input" id="ce_radius" type="number" min="50" step="10" placeholder="300"/>
</div>
</div>
</div>
<div class="modal-actions">
<button class="btn-modal-cancel" onclick="closeCenterEditModal()">Batal</button>
<button class="btn-modal-save" onclick="saveCenterEdit()" style="background:var(--primary)">💾 Simpan Perubahan</button>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="script.js"></script>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
<?php
/**
* koneksi.php — Koneksi Database MySQL
* WebGIS BantSOSial Pontianak
*
* Ubah kredensial sesuai konfigurasi server lokal Anda.
*/
define('DB_HOST', 'localhost');
define('DB_USER', 'root'); // Ganti sesuai MySQL user Anda
define('DB_PASS', ''); // Ganti sesuai password MySQL Anda
define('DB_NAME', 'webgis_bansos');
// Buat koneksi
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
// Cek error koneksi
if ($conn->connect_error) {
http_response_code(500);
die(json_encode([
'success' => false,
'message' => 'Koneksi database gagal: ' . $conn->connect_error
]));
}
// Set charset UTF-8
$conn->set_charset('utf8mb4');
// Header JSON untuk semua response
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
+16
View File
@@ -0,0 +1,16 @@
<?php
/**
* reset.php — Reset Semua Data
* Menghapus semua rumah ibadah, rumah miskin, dan log
*/
require_once 'koneksi.php';
$conn->query("DELETE FROM aid_logs");
$conn->query("DELETE FROM houses");
$conn->query("DELETE FROM religious_centers");
$conn->query("ALTER TABLE aid_logs AUTO_INCREMENT = 1");
$conn->query("ALTER TABLE houses AUTO_INCREMENT = 1");
$conn->query("ALTER TABLE religious_centers AUTO_INCREMENT = 1");
echo json_encode(['success' => true, 'message' => 'Semua data berhasil direset']);
$conn->close();
+1636
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
<?php
/**
* simpan_laporan.php — Simpan Laporan Masyarakat
* POST: name, text, img (base64, opsional)
*/
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success'=>false,'message'=>'Hanya POST']); exit;
}
$name = $conn->real_escape_string(strip_tags($_POST['name'] ?? 'Anonim'));
$text = $conn->real_escape_string(strip_tags($_POST['text'] ?? ''));
$img = $_POST['img'] ?? '';
if (empty($text)) {
echo json_encode(['success'=>false,'message'=>'Deskripsi kosong']); exit;
}
// Validasi ukuran base64 img (max ~5MB raw → ~6.7MB base64)
if (strlen($img) > 7_000_000) {
echo json_encode(['success'=>false,'message'=>'Ukuran foto terlalu besar (max 5MB)']); exit;
}
$imgEsc = $conn->real_escape_string($img);
$sql = "INSERT INTO laporan (pelapor, deskripsi, foto_base64) VALUES ('$name', '$text', '$imgEsc')";
if ($conn->query($sql)) {
echo json_encode(['success'=>true,'id'=>$conn->insert_id]);
} else {
echo json_encode(['success'=>false,'message'=>$conn->error]);
}
$conn->close();
+61
View File
@@ -0,0 +1,61 @@
<?php
/**
* simpan_pusat.php — Simpan / Update Rumah Ibadah
*
* POST params:
* id - ID (jika update, opsional)
* name - Nama rumah ibadah
* address - Alamat
* kas - Jumlah kas/dana (Rp)
* lat - Latitude
* lng - Longitude
* radius - Radius dalam meter
* update - "1" jika update existing record
*/
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Hanya menerima POST']);
exit;
}
$id = intval($_POST['id'] ?? 0);
$name = $conn->real_escape_string(strip_tags($_POST['name'] ?? ''));
$address = $conn->real_escape_string(strip_tags($_POST['address'] ?? ''));
$kas = floatval($_POST['kas'] ?? 0);
$lat = floatval($_POST['lat'] ?? 0);
$lng = floatval($_POST['lng'] ?? 0);
$radius = intval($_POST['radius'] ?? 300);
$isUpdate = isset($_POST['update']) && $_POST['update'] === '1';
if (empty($name)) {
echo json_encode(['success' => false, 'message' => 'Nama wajib diisi']);
exit;
}
if ($radius < 50) $radius = 50;
if ($id > 0 || $isUpdate) {
$sql = "UPDATE religious_centers
SET name='$name', address='$address', kas=$kas, latitude=$lat, longitude=$lng, radius=$radius, updated_at=NOW()
WHERE id=$id";
if ($conn->query($sql)) {
echo json_encode(['success' => true, 'id' => $id, 'action' => 'updated']);
} else {
echo json_encode(['success' => false, 'message' => $conn->error]);
}
} else {
$sql = "INSERT INTO religious_centers (name, address, kas, latitude, longitude, radius)
VALUES ('$name', '$address', $kas, $lat, $lng, $radius)";
if ($conn->query($sql)) {
$newId = $conn->insert_id;
echo json_encode(['success' => true, 'id' => $newId, 'action' => 'inserted']);
} else {
echo json_encode(['success' => false, 'message' => $conn->error]);
}
}
$conn->close();
+79
View File
@@ -0,0 +1,79 @@
<?php
/**
* simpan_rumah.php v3 — Simpan / Update Data Rumah Miskin
*
* POST params:
* id - ID rumah (opsional, jika update)
* lat, lng - Koordinat
* address - Alamat lengkap
* rt, rw - RT dan RW
* kelurahan - Nama kelurahan
* status_miskin - sangat_miskin / miskin / tidak_miskin
* jumlah_anggota - Jumlah anggota keluarga
* anggota - JSON array data tiap anggota
* aid_status - helped / not_helped / outside
* has_data - 0 atau 1
*/
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Hanya POST']);
exit;
}
$id = intval($_POST['id'] ?? 0);
$lat = floatval($_POST['lat'] ?? 0);
$lng = floatval($_POST['lng'] ?? 0);
$address = $conn->real_escape_string(strip_tags($_POST['address'] ?? ''));
$rt = $conn->real_escape_string(strip_tags($_POST['rt'] ?? ''));
$rw = $conn->real_escape_string(strip_tags($_POST['rw'] ?? ''));
$kelurahan = $conn->real_escape_string(strip_tags($_POST['kelurahan'] ?? ''));
$statusMiskin = $conn->real_escape_string($_POST['status_miskin'] ?? '');
$jumlahAnggota = intval($_POST['jumlah_anggota'] ?? 0);
$anggotaRaw = $_POST['anggota'] ?? '[]';
$aidStatus = $conn->real_escape_string($_POST['aid_status'] ?? 'outside');
$hasData = intval($_POST['has_data'] ?? 0);
// Validasi anggota JSON
$anggotaDecoded = json_decode($anggotaRaw, true);
if (!is_array($anggotaDecoded)) $anggotaDecoded = [];
$anggotaJson = $conn->real_escape_string(json_encode($anggotaDecoded, JSON_UNESCAPED_UNICODE));
$validStatuses = ['sangat_miskin','miskin','tidak_miskin',''];
if (!in_array($statusMiskin, $validStatuses)) $statusMiskin = '';
$validAid = ['helped','not_helped','outside'];
if (!in_array($aidStatus, $validAid)) $aidStatus = 'outside';
if ($id > 0) {
// UPDATE
$sql = "UPDATE houses SET
latitude=$lat, longitude=$lng,
address='$address', rt='$rt', rw='$rw', kelurahan='$kelurahan',
status_miskin='$statusMiskin', jumlah_anggota=$jumlahAnggota,
anggota='$anggotaJson', aid_status='$aidStatus', has_data=$hasData,
updated_at=NOW()
WHERE id=$id";
if ($conn->query($sql)) {
echo json_encode(['success'=>true,'id'=>$id,'action'=>'updated']);
} else {
echo json_encode(['success'=>false,'message'=>$conn->error]);
}
} else {
// INSERT
$sql = "INSERT INTO houses
(latitude, longitude, address, rt, rw, kelurahan,
status_miskin, jumlah_anggota, anggota, aid_status, has_data)
VALUES
($lat, $lng, '$address', '$rt', '$rw', '$kelurahan',
'$statusMiskin', $jumlahAnggota, '$anggotaJson', '$aidStatus', $hasData)";
if ($conn->query($sql)) {
$newId = $conn->insert_id;
echo json_encode(['success'=>true,'id'=>$newId,'action'=>'inserted']);
} else {
echo json_encode(['success'=>false,'message'=>$conn->error]);
}
}
$conn->close();
+557
View File
@@ -0,0 +1,557 @@
/* ============================================
WebGIS BantSOSial — style.css v5
Navbar + halaman Peta & Pelaporan terpisah
============================================ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--nav-h: 56px;
--sidebar-w: 320px;
--bg: #f0f4f8;
--sidebar-bg: #ffffff;
--border: #e2e8f0;
--text: #1e293b;
--text-muted: #64748b;
--primary: #0f766e;
--primary-light: #ccfbf1;
--primary-hover: #0d6563;
--orange: #ea580c;
--orange-light: #ffedd5;
--orange-hover: #c2410c;
--danger: #dc2626;
--danger-light: #fee2e2;
--warning: #d97706;
--warning-light: #fef3c7;
--success: #16a34a;
--success-light: #dcfce7;
--purple: #7c3aed;
--purple-light: #ede9fe;
--radius-sm: 8px;
--radius-md: 12px;
--shadow: 0 4px 24px rgba(0,0,0,0.08);
--shadow-lg: 0 8px 40px rgba(0,0,0,0.14);
--font: 'DM Sans', sans-serif;
--mono: 'DM Mono', monospace;
}
html, body { height: 100%; font-family: var(--font); background: var(--bg); overflow: hidden; color: var(--text); }
/* ============================================
TOP NAVBAR
============================================ */
#topNav {
position: fixed; top: 0; left: 0; right: 0;
height: var(--nav-h); z-index: 2000;
background: white; border-bottom: 1px solid var(--border);
box-shadow: 0 2px 12px rgba(0,0,0,0.07);
display: flex; align-items: center;
padding: 0 20px; gap: 16px;
}
/* Brand */
.nav-brand {
display: flex; align-items: center; gap: 10px; flex-shrink: 0;
width: var(--sidebar-w);
}
.nav-brand-icon { font-size: 22px; }
.nav-brand-title { font-size: 13px; font-weight: 700; letter-spacing: -.3px; line-height: 1.2; }
.nav-brand-sub { font-size: 9px; color: var(--text-muted); font-family: var(--mono); }
/* Center menu */
.nav-menu {
display: flex; align-items: center; gap: 4px;
flex: 1; justify-content: center;
}
.nav-item {
display: flex; align-items: center; gap: 6px;
padding: 7px 18px; border: none; background: none;
border-radius: 20px; font-family: var(--font);
font-size: 13px; font-weight: 600; color: var(--text-muted);
cursor: pointer; transition: all .2s; position: relative;
}
.nav-item:hover { background: var(--bg); color: var(--text); }
.nav-item.active {
background: var(--primary-light); color: var(--primary);
}
.nav-item-icon { font-size: 15px; }
.nav-badge {
position: absolute; top: 2px; right: 8px;
background: var(--orange); color: white;
font-size: 9px; font-weight: 700; font-family: var(--mono);
min-width: 16px; height: 16px; padding: 0 4px;
border-radius: 8px; display: flex; align-items: center; justify-content: center;
}
.nav-badge.hidden { display: none; }
/* Right: role */
.nav-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.role-pill {
display: flex; align-items: center; gap: 6px;
background: var(--bg); border: 1.5px solid var(--border);
border-radius: 20px; padding: 5px 12px;
font-size: 12px; font-weight: 600;
}
#roleSwitchBtn {
padding: 6px 14px; background: var(--primary); color: white;
border: none; border-radius: 20px; font-family: var(--font);
font-size: 12px; font-weight: 600; cursor: pointer; transition: all .2s;
box-shadow: 0 2px 8px rgba(15,118,110,.3);
}
#roleSwitchBtn:hover { background: var(--primary-hover); }
/* ============================================
PAGE SYSTEM
============================================ */
.page {
position: fixed;
top: var(--nav-h); left: 0; right: 0;
bottom: 0;
display: none;
opacity: 0;
transition: opacity .25s ease;
}
.page.active-page {
display: flex;
opacity: 1;
}
/* ============================================
PAGE: PETA — sidebar + map layout
============================================ */
#pagePeta { display: none; }
#pagePeta.active-page { display: block; }
#sidebar {
position: fixed;
left: 0; top: var(--nav-h);
width: var(--sidebar-w);
height: calc(100% - var(--nav-h));
background: white; z-index: 1000;
overflow-y: auto; overflow-x: hidden;
box-shadow: var(--shadow-lg);
display: flex; flex-direction: column;
border-right: 1px solid var(--border);
}
#sidebar::-webkit-scrollbar { width: 4px; }
#sidebar::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
#map {
position: fixed;
left: var(--sidebar-w); top: var(--nav-h);
width: calc(100% - var(--sidebar-w));
height: calc(100% - var(--nav-h));
z-index: 1;
}
#map.adding-mode { cursor: crosshair !important; }
#map.adding-mode .leaflet-interactive { cursor: crosshair !important; }
.radius-tooltip {
position: fixed; bottom: 24px; right: 24px;
background: rgba(15,23,42,.88); color: white;
padding: 10px 16px; border-radius: var(--radius-md);
font-family: var(--mono); font-size: 14px; font-weight: 500;
z-index: 2000; backdrop-filter: blur(8px);
box-shadow: var(--shadow-lg); pointer-events: none; transition: opacity .2s;
}
.radius-tooltip.hidden { opacity: 0; pointer-events: none; }
.radius-handle { cursor: ew-resize; }
/* ============================================
PAGE: PELAPORAN — dua kolom
============================================ */
#pagePelaporan { display: none; background: var(--bg); }
#pagePelaporan.active-page { display: block; overflow-y: auto; }
.report-page {
display: grid;
grid-template-columns: 420px 1fr;
gap: 0;
min-height: calc(100vh - var(--nav-h));
}
.report-col {
display: flex; flex-direction: column;
height: calc(100vh - var(--nav-h));
overflow: hidden;
}
.report-col-form {
background: white;
border-right: 1px solid var(--border);
overflow-y: auto;
}
.report-col-list {
background: var(--bg);
overflow: hidden;
display: flex; flex-direction: column;
}
.report-col-viewer {
background: var(--bg);
}
.report-col-header {
padding: 20px 24px 16px;
border-bottom: 1px solid var(--border);
background: white; flex-shrink: 0;
}
.report-col-title {
font-size: 16px; font-weight: 700;
display: flex; align-items: center; gap: 6px;
}
.report-col-sub {
font-size: 11px; color: var(--text-muted); margin-top: 3px;
}
.report-form-body { padding: 20px 24px; }
/* List toolbar */
.report-list-toolbar {
padding: 12px 20px;
background: white; border-bottom: 1px solid var(--border);
display: flex; gap: 8px; align-items: center; flex-shrink: 0;
}
.report-list-container {
flex: 1; overflow-y: auto; padding: 16px 20px;
}
.report-list-container::-webkit-scrollbar { width: 4px; }
.report-list-container::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
/* Report card */
.report-card {
background: white; border: 1px solid var(--border);
border-radius: var(--radius-md); padding: 14px 16px;
margin-bottom: 10px; cursor: pointer;
transition: all .2s; animation: fadeIn .3s;
position: relative;
}
.report-card:hover { box-shadow: var(--shadow); transform: translateY(-1px); border-color: var(--primary); }
.report-card-top {
display: flex; align-items: flex-start; justify-content: space-between;
margin-bottom: 6px; gap: 8px;
}
.report-card-name { font-size: 13px; font-weight: 700; }
.report-card-time { font-size: 10px; color: var(--text-muted); font-family: var(--mono); flex-shrink: 0; }
.report-card-text {
font-size: 12px; color: var(--text-muted); line-height: 1.5;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.report-card-location { font-size: 10px; color: var(--text-muted); margin-top: 5px; }
.report-card-footer {
display: flex; align-items: center; justify-content: space-between;
margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border);
}
.report-status-badge {
font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 12px;
}
.report-status-badge.baru { background: #eff6ff; color: #1d4ed8; }
.report-status-badge.ditangani { background: var(--warning-light); color: var(--warning); }
.report-status-badge.selesai { background: var(--success-light); color: var(--success); }
.report-card-img { width: 48px; height: 48px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
.report-card-has-img { border-left: 3px solid var(--orange); }
/* Report detail modal */
.report-detail-img { width: 100%; border-radius: var(--radius-sm); margin: 10px 0; max-height: 260px; object-fit: cover; }
.report-detail-row { display: flex; justify-content: space-between; font-size: 12px; padding: 6px 0; border-bottom: 1px solid var(--border); }
.report-detail-row:last-of-type { border: none; }
.report-detail-label { color: var(--text-muted); }
.report-detail-val { font-weight: 600; text-align: right; max-width: 60%; }
/* Status change buttons */
.status-change-btns { display: flex; gap: 6px; margin-top: 12px; }
.btn-status {
flex: 1; padding: 8px; border: none; border-radius: var(--radius-sm);
font-family: var(--font); font-size: 11px; font-weight: 700; cursor: pointer; transition: all .2s;
}
.btn-status.baru { background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; }
.btn-status.ditangani { background: var(--warning-light); color: var(--warning); border: 1px solid #fcd34d; }
.btn-status.selesai { background: var(--success-light); color: var(--success); border: 1px solid #86efac; }
.btn-status.active { opacity: 1; transform: scale(1.02); box-shadow: var(--shadow); }
/* ============================================
SIDEBAR INNER STYLES
============================================ */
.mode-badge {
margin: 10px 14px 0; padding: 8px 12px;
background: #fef9c3; border: 1px solid #fde047;
border-radius: var(--radius-sm); font-size: 11px;
font-weight: 500; color: #854d0e;
display: flex; align-items: center; gap: 8px;
}
.mode-badge.hidden { display: none; }
.mode-badge.house-mode { background: #fff7ed; border-color: #fdba74; color: #9a3412; }
.pulse-dot { width: 8px; height: 8px; background: #facc15; border-radius: 50%; flex-shrink: 0; animation: pulse 1.2s infinite; }
.mode-badge.house-mode .pulse-dot { background: #fb923c; }
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.5;transform:scale(1.3)} }
.btn-group { padding: 12px 14px 0; display: flex; flex-direction: column; gap: 7px; }
#btnAddCenter {
padding: 10px 14px; background: var(--primary); color: white;
border: none; border-radius: var(--radius-md); font-family: var(--font);
font-size: 13px; font-weight: 600; cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 7px;
transition: all .2s; box-shadow: 0 2px 8px rgba(15,118,110,.25);
}
#btnAddCenter:hover { background: var(--primary-hover); transform: translateY(-1px); }
#btnAddCenter.active { background: #0369a1; }
#btnAddHouse {
padding: 10px 14px; background: var(--orange); color: white;
border: none; border-radius: var(--radius-md); font-family: var(--font);
font-size: 13px; font-weight: 600; cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 7px;
transition: all .2s; box-shadow: 0 2px 8px rgba(234,88,12,.25);
}
#btnAddHouse:hover { background: var(--orange-hover); transform: translateY(-1px); }
#btnAddHouse.active { background: #9a3412; }
.btn-danger {
margin: 6px 14px 20px; padding: 9px 14px; background: white;
color: var(--danger); border: 1.5px solid var(--danger);
border-radius: var(--radius-md); font-family: var(--font);
font-size: 12px; font-weight: 600; cursor: pointer;
width: calc(100% - 28px); transition: all .2s;
}
.btn-danger:hover { background: var(--danger-light); }
.stat-panel { margin: 12px 14px 0; background: var(--bg); border-radius: var(--radius-md); padding: 12px; }
.stat-title { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .8px; color: var(--text-muted); margin-bottom: 8px; }
.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.stat-card { background: white; border-radius: var(--radius-sm); padding: 8px 10px; border: 1px solid var(--border); position: relative; overflow: hidden; transition: all .3s; }
.stat-card:hover { transform: translateY(-1px); box-shadow: var(--shadow); }
.stat-card.red { border-color: #fca5a5; background: #fff5f5; }
.stat-card.yellow { border-color: #fcd34d; background: #fffbeb; }
.stat-card.green { border-color: #86efac; background: #f0fdf4; }
.stat-num { font-size: 20px; font-weight: 700; font-family: var(--mono); line-height: 1; transition: all .4s; }
.stat-card.red .stat-num { color: var(--danger); }
.stat-card.yellow .stat-num { color: var(--warning); }
.stat-card.green .stat-num { color: var(--success); }
.stat-label { font-size: 9px; color: var(--text-muted); margin-top: 2px; font-weight: 500; }
.stat-icon { position: absolute; right: 6px; top: 6px; font-size: 13px; opacity: .3; }
.section { padding: 12px 14px 0; }
.section-title { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .8px; color: var(--text-muted); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
.badge { font-size: 10px; padding: 1px 6px; border-radius: 20px; font-family: var(--mono); margin-left: auto; color: white; }
.badge-teal { background: var(--primary); }
.badge-orange { background: var(--orange); }
.search-wrap { margin-bottom: 6px; }
.search-input { width: 100%; padding: 7px 10px; border: 1.5px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font); font-size: 12px; outline: none; transition: border-color .2s; background: white; }
.search-input:focus { border-color: var(--primary); }
.filter-row { display: flex; gap: 6px; margin-bottom: 8px; }
.filter-select { flex: 1; padding: 6px 8px; border: 1.5px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font); font-size: 11px; outline: none; background: white; color: var(--text); cursor: pointer; margin-bottom: 6px; width: 100%; }
.filter-select:focus { border-color: var(--primary); }
#centerList, #houseList { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.empty-state { text-align: center; padding: 16px 10px; font-size: 11px; color: var(--text-muted); line-height: 1.6; background: var(--bg); border-radius: var(--radius-sm); border: 1.5px dashed var(--border); }
.center-item { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 9px 30px 9px 10px; cursor: pointer; transition: all .2s; position: relative; animation: fadeIn .3s ease; }
.center-item:hover { background: var(--primary-light); border-color: var(--primary); transform: translateX(3px); }
.center-item-name { font-size: 12px; font-weight: 600; }
.center-item-addr { font-size: 10px; color: var(--text-muted); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.center-item-kas { font-size: 10px; color: var(--primary); font-family: var(--mono); font-weight: 600; margin-top: 2px; }
.center-item-radius { font-size: 10px; color: #3b82f6; font-family: var(--mono); margin-top: 1px; }
.house-item { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 9px 30px 9px 10px; cursor: pointer; transition: all .2s; position: relative; animation: fadeIn .3s ease; }
.house-item:hover { background: var(--orange-light); border-color: var(--orange); transform: translateX(3px); }
.house-item.status-red { border-left: 3px solid var(--danger); }
.house-item.status-yellow { border-left: 3px solid var(--warning); }
.house-item.status-green { border-left: 3px solid var(--success); }
.house-item-name { font-size: 12px; font-weight: 600; }
.house-item-sub { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
.house-item-status { font-size: 10px; font-weight: 600; margin-top: 2px; }
.house-item-status.green { color: var(--success); }
.house-item-status.red { color: var(--danger); }
.house-item-status.yellow { color: var(--warning); }
.item-delete { position: absolute; right: 7px; top: 7px; background: none; border: none; cursor: pointer; font-size: 12px; opacity: .35; transition: opacity .2s; padding: 2px 4px; border-radius: 4px; }
.item-delete:hover { opacity: 1; background: var(--danger-light); }
.legend-list { display: flex; flex-direction: column; gap: 5px; margin-bottom: 12px; }
.legend-item { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--text-muted); }
.legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.legend-dot.green { background: #16a34a; }
.legend-dot.red { background: #dc2626; }
.legend-dot.yellow { background: #d97706; }
.legend-circle { width: 14px; height: 14px; border-radius: 50%; border: 2px solid #3b82f6; background: rgba(59,130,246,.1); flex-shrink: 0; }
/* upload & report form */
.form-textarea { width: 100%; padding: 8px 10px; resize: vertical; border: 1.5px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font); font-size: 12px; outline: none; transition: border-color .2s; background: white; color: var(--text); }
.form-textarea:focus { border-color: var(--orange); }
.upload-area { border: 2px dashed var(--border); border-radius: var(--radius-sm); padding: 20px; text-align: center; cursor: pointer; transition: all .2s; background: var(--bg); }
.upload-area:hover { border-color: var(--orange); background: var(--orange-light); }
.upload-icon { font-size: 28px; margin-bottom: 6px; }
.upload-text { font-size: 12px; font-weight: 600; color: var(--text-muted); }
.upload-sub { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
.remove-img-btn { display: block; width: 100%; margin-top: 6px; padding: 5px; background: var(--danger-light); color: var(--danger); border: 1px solid #fca5a5; border-radius: 6px; font-size: 11px; cursor: pointer; font-family: var(--font); font-weight: 600; }
/* ============================================
LEAFLET POPUP
============================================ */
.leaflet-popup-content-wrapper { border-radius: var(--radius-md) !important; box-shadow: var(--shadow-lg) !important; border: 1px solid var(--border); font-family: var(--font) !important; overflow: hidden; padding: 0 !important; }
.leaflet-popup-content { margin: 0 !important; width: 300px !important; }
.leaflet-popup-tip { background: white !important; }
.map-popup { padding: 13px 15px; }
.map-popup-title { font-size: 14px; font-weight: 700; margin-bottom: 4px; }
.map-popup-addr { font-size: 11px; color: var(--text-muted); margin-bottom: 10px; line-height: 1.4; }
.map-popup-badge { display: inline-flex; align-items: center; gap: 5px; font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; padding: 2px 8px; border-radius: 20px; margin-bottom: 8px; }
.map-popup-badge.green { background: var(--success-light); color: var(--success); }
.map-popup-badge.red { background: var(--danger-light); color: var(--danger); }
.map-popup-badge.yellow { background: var(--warning-light); color: var(--warning); }
.map-popup-badge.nodata { background: #f1f5f9; color: var(--text-muted); }
.map-popup-actions { padding: 9px 15px; background: var(--bg); display: flex; gap: 7px; }
.popup-btn { flex: 1; padding: 7px 8px; border: none; border-radius: var(--radius-sm); font-family: var(--font); font-size: 11px; font-weight: 600; cursor: pointer; transition: all .2s; }
.popup-btn.primary { background: var(--primary); color: white; }
.popup-btn.primary:hover { background: var(--primary-hover); }
.popup-btn.orange { background: var(--orange); color: white; }
.popup-btn.orange:hover { background: var(--orange-hover); }
.center-popup { padding: 13px 15px; }
.center-popup-header { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
.center-popup-icon { width: 30px; height: 30px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; }
.center-popup-title { font-size: 13px; font-weight: 700; line-height: 1.2; }
.center-popup-addr { font-size: 10px; color: var(--text-muted); margin-bottom: 8px; line-height: 1.4; }
.kas-badge { display: flex; align-items: center; gap: 6px; background: #f0fdf4; border: 1px solid #86efac; border-radius: var(--radius-sm); padding: 6px 10px; margin-bottom: 7px; }
.kas-label { font-size: 10px; color: var(--success); font-weight: 600; }
.kas-value { font-size: 13px; font-weight: 700; color: var(--success); font-family: var(--mono); margin-left: auto; }
.radius-info-box { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: var(--radius-sm); padding: 7px 10px; font-size: 11px; color: #1d4ed8; display: flex; align-items: center; gap: 6px; margin-bottom: 5px; }
.radius-val { font-family: var(--mono); font-weight: 700; font-size: 13px; margin-left: auto; }
.radius-drag-hint { font-size: 10px; color: var(--text-muted); text-align: center; padding: 2px 0 4px; }
/* ============================================
FORMS (shared)
============================================ */
.form-group { margin-bottom: 12px; }
.form-label { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .6px; color: var(--text-muted); margin-bottom: 4px; display: block; }
.form-input { width: 100%; padding: 8px 10px; border: 1.5px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font); font-size: 12px; color: var(--text); outline: none; transition: border-color .2s; background: white; }
.form-input:focus { border-color: var(--primary); }
.form-row { display: flex; gap: 7px; }
.form-row .form-group { flex: 1; }
.form-divider { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .6px; color: var(--text-muted); border-bottom: 1px solid var(--border); padding-bottom: 4px; margin: 10px 0 8px; }
.form-submit { width: 100%; padding: 9px; background: var(--primary); color: white; border: none; border-radius: var(--radius-sm); font-family: var(--font); font-size: 12px; font-weight: 700; cursor: pointer; margin-top: 10px; transition: all .2s; }
.form-submit:hover { background: var(--primary-hover); }
.form-submit.orange-btn { background: var(--orange); }
.form-submit.orange-btn:hover { background: var(--orange-hover); }
.form-cancel { width: 100%; padding: 7px; background: none; color: var(--text-muted); border: 1.5px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font); font-size: 11px; cursor: pointer; margin-top: 5px; transition: all .2s; }
.form-cancel:hover { background: var(--bg); }
.loading-addr { font-size: 10px; color: var(--primary); padding: 4px 0; animation: blink 1s infinite; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:.4} }
.form-popup { padding: 14px 15px; }
.form-popup h3 { font-size: 13px; font-weight: 700; margin-bottom: 12px; display: flex; align-items: center; gap: 6px; }
/* ============================================
CUSTOM DATE PICKER
============================================ */
.datepicker-overlay { position: fixed; inset: 0; z-index: 5000; display: flex; align-items: center; justify-content: center; background: rgba(15,23,42,.4); backdrop-filter: blur(3px); }
.datepicker-overlay.hidden { display: none; }
.datepicker-box { background: white; border-radius: 16px; box-shadow: 0 24px 80px rgba(0,0,0,.2); width: 320px; overflow: hidden; animation: slideUp .2s ease; }
.dp-header { background: linear-gradient(135deg, var(--primary), #0d9488); color: white; padding: 16px 20px; display: flex; align-items: center; justify-content: space-between; }
.dp-header-label { font-size: 13px; font-weight: 600; opacity: .8; }
.dp-header-value { font-size: 22px; font-weight: 700; font-family: var(--mono); }
.dp-tabs { display: flex; border-bottom: 1px solid var(--border); }
.dp-tab { flex: 1; padding: 10px; text-align: center; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; cursor: pointer; color: var(--text-muted); border-bottom: 2px solid transparent; transition: all .2s; }
.dp-tab.active { color: var(--primary); border-bottom-color: var(--primary); }
.dp-body { padding: 14px; min-height: 180px; }
.dp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.dp-grid-7 { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.dp-cell { padding: 8px 4px; text-align: center; font-size: 12px; font-weight: 500; border-radius: var(--radius-sm); cursor: pointer; border: 1.5px solid transparent; transition: all .15s; color: var(--text); }
.dp-cell:hover { background: var(--primary-light); border-color: var(--primary); color: var(--primary); }
.dp-cell.selected { background: var(--primary); color: white; border-color: var(--primary); }
.dp-cell.today { border-color: var(--primary); color: var(--primary); font-weight: 700; }
.dp-year-nav { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.dp-nav-btn { padding: 5px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; font-size: 12px; cursor: pointer; font-weight: 700; transition: all .2s; }
.dp-nav-btn:hover { background: var(--primary-light); border-color: var(--primary); }
.dp-year-range { font-size: 11px; color: var(--text-muted); font-family: var(--mono); }
.dp-weekday { text-align: center; font-size: 9px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; padding: 4px 0; }
.dp-actions { padding: 10px 14px; border-top: 1px solid var(--border); display: flex; gap: 8px; background: var(--bg); }
.dp-btn-ok { flex: 2; padding: 8px; background: var(--primary); color: white; border: none; border-radius: var(--radius-sm); font-family: var(--font); font-size: 12px; font-weight: 700; cursor: pointer; transition: background .2s; }
.dp-btn-ok:hover { background: var(--primary-hover); }
.dp-btn-cancel { flex: 1; padding: 8px; background: white; color: var(--text-muted); border: 1.5px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font); font-size: 12px; cursor: pointer; }
.date-trigger { width: 100%; padding: 7px 9px; border: 1.5px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font); font-size: 12px; color: var(--text); outline: none; cursor: pointer; background: white; text-align: left; transition: border-color .2s; }
.date-trigger:hover, .date-trigger:focus { border-color: var(--primary); }
.date-trigger-placeholder { color: var(--text-muted); }
/* ============================================
MODAL (shared)
============================================ */
.modal-overlay { position: fixed; inset: 0; background: rgba(15,23,42,.55); z-index: 3000; display: flex; align-items: center; justify-content: center; padding: 16px; backdrop-filter: blur(3px); }
.modal-overlay.hidden { display: none; }
.modal-box { background: white; border-radius: 16px; width: 100%; max-width: 540px; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 24px 80px rgba(0,0,0,.2); animation: slideUp .25s ease; }
.modal-wide { max-width: 640px; }
@keyframes slideUp { from{opacity:0;transform:translateY(20px)} to{opacity:1;transform:translateY(0)} }
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-bottom: 1px solid var(--border); background: linear-gradient(135deg,#f0fdf4,#e0f2fe); flex-shrink: 0; }
.modal-title { font-size: 15px; font-weight: 700; }
.modal-close { background: none; border: none; font-size: 16px; cursor: pointer; color: var(--text-muted); padding: 4px 8px; border-radius: 6px; transition: all .2s; }
.modal-close:hover { background: var(--bg); }
.modal-body { overflow-y: auto; padding: 20px; flex: 1; }
.modal-body::-webkit-scrollbar { width: 4px; }
.modal-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.modal-section { margin-bottom: 16px; }
.modal-section-title { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .8px; color: var(--text-muted); padding-bottom: 6px; border-bottom: 1px solid var(--border); margin-bottom: 10px; }
.info-box { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 12px; margin-bottom: 10px; }
.info-box-row { display: flex; justify-content: space-between; font-size: 11px; padding: 3px 0; }
.info-box-label { color: var(--text-muted); }
.info-box-val { font-weight: 600; color: var(--text); font-family: var(--mono); font-size: 10px; text-align: right; max-width: 60%; }
.member-card { border: 1.5px solid var(--border); border-radius: var(--radius-md); padding: 12px; margin-bottom: 10px; background: #fafafa; }
.member-card-header { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 10px; }
.member-num { width: 22px; height: 22px; background: var(--primary); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; flex-shrink: 0; }
.salary-row { display: none; }
.salary-row.visible { display: flex; }
.status-pills { display: flex; gap: 7px; margin-bottom: 2px; }
.status-pill { flex: 1; padding: 8px 6px; border: 2px solid var(--border); border-radius: var(--radius-sm); text-align: center; cursor: pointer; font-size: 11px; font-weight: 600; transition: all .2s; background: white; }
.status-pill:hover { border-color: var(--orange); }
.status-pill.selected.sangat_miskin { border-color: var(--danger); background: var(--danger-light); color: var(--danger); }
.status-pill.selected.miskin { border-color: var(--warning); background: var(--warning-light); color: var(--warning); }
.status-pill.selected.tidak_miskin { border-color: var(--success); background: var(--success-light); color: var(--success); }
.modal-actions { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; gap: 8px; background: var(--bg); flex-shrink: 0; }
.btn-modal-save { flex: 2; padding: 10px; background: var(--orange); color: white; border: none; border-radius: var(--radius-md); font-family: var(--font); font-size: 13px; font-weight: 700; cursor: pointer; transition: all .2s; }
.btn-modal-save:hover { background: var(--orange-hover); }
.btn-modal-cancel { flex: 1; padding: 10px; background: white; color: var(--text-muted); border: 1.5px solid var(--border); border-radius: var(--radius-md); font-family: var(--font); font-size: 12px; cursor: pointer; }
.btn-modal-cancel:hover { background: var(--bg); }
/* detail modal */
.detail-header-band { background: linear-gradient(135deg,var(--orange-light),#fff7ed); border-radius: var(--radius-sm); padding: 12px 14px; margin-bottom: 14px; }
.detail-addr { font-size: 11px; color: var(--text-muted); margin-top: 3px; }
.detail-center-row { background: var(--primary-light); border: 1px solid #99f6e4; border-radius: var(--radius-sm); padding: 8px 12px; margin-bottom: 12px; font-size: 11px; display: flex; align-items: center; gap: 8px; }
.detail-miskin-badge { display: inline-flex; align-items: center; gap: 5px; padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 700; margin-bottom: 12px; }
.detail-miskin-badge.sangat_miskin { background: var(--danger-light); color: var(--danger); }
.detail-miskin-badge.miskin { background: var(--warning-light); color: var(--warning); }
.detail-miskin-badge.tidak_miskin { background: var(--success-light); color: var(--success); }
.members-grid { display: flex; flex-direction: column; gap: 8px; }
.member-detail-card { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 12px; background: var(--bg); }
.member-detail-name { font-size: 13px; font-weight: 700; margin-bottom: 4px; }
.member-detail-row { display: flex; justify-content: space-between; font-size: 11px; padding: 2px 0; color: var(--text-muted); }
.member-detail-row strong { color: var(--text); font-family: var(--mono); font-size: 10px; }
.bantuan-section { margin-top: 14px; padding: 12px 14px; background: var(--bg); border-radius: var(--radius-sm); border: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
.bantuan-label { font-size: 12px; font-weight: 600; flex: 1; }
.btn-bantuan { padding: 7px 14px; border: none; border-radius: var(--radius-sm); font-family: var(--font); font-size: 11px; font-weight: 700; cursor: pointer; transition: all .2s; }
.btn-bantuan.help { background: var(--success); color: white; }
.btn-bantuan.unhelp { background: var(--danger-light); color: var(--danger); border: 1px solid #fca5a5; }
/* Role modal */
.role-cards { display: flex; flex-direction: column; gap: 10px; }
.role-card { border: 2px solid var(--border); border-radius: var(--radius-md); padding: 14px 16px; cursor: pointer; transition: all .2s; background: var(--bg); }
.role-card:hover { border-color: var(--primary); background: var(--primary-light); transform: translateX(3px); }
.role-card.active-role { border-color: var(--primary); background: var(--primary-light); }
.role-card.active-role[id="roleCard_surveyer"] { border-color: var(--orange); background: var(--orange-light); }
.role-card.active-role[id="roleCard_viewer"] { border-color: var(--purple); background: var(--purple-light); }
.role-card-icon { font-size: 24px; margin-bottom: 4px; }
.role-card-name { font-size: 14px; font-weight: 700; margin-bottom: 3px; }
.role-card-desc { font-size: 11px; color: var(--text-muted); margin-bottom: 8px; line-height: 1.4; }
.role-card-perms { display: flex; flex-wrap: wrap; gap: 4px; }
.perm { font-size: 10px; font-weight: 600; padding: 2px 7px; border-radius: 12px; }
.perm-green { background: var(--success-light); color: var(--success); }
.perm-red { background: var(--danger-light); color: var(--danger); }
/* ============================================
ANIMATIONS
============================================ */
@keyframes fadeIn { from{opacity:0;transform:translateX(-8px)} to{opacity:1;transform:translateX(0)} }
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* update_radius.php — Update Radius Pusat Keagamaan
*
* POST params:
* id - ID pusat keagamaan
* radius - Radius baru dalam meter
*/
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Hanya menerima POST']);
exit;
}
$id = intval($_POST['id'] ?? 0);
$radius = intval($_POST['radius'] ?? 300);
if ($id < 1) {
echo json_encode(['success' => false, 'message' => 'ID tidak valid']);
exit;
}
// Batasi radius minimum 50 meter
if ($radius < 50) $radius = 50;
$sql = "UPDATE religious_centers SET radius=$radius WHERE id=$id";
if ($conn->query($sql)) {
echo json_encode([
'success' => true,
'id' => $id,
'radius' => $radius
]);
} else {
echo json_encode(['success' => false, 'message' => $conn->error]);
}
$conn->close();
+75
View File
@@ -0,0 +1,75 @@
<?php
/**
* update_status.php — Update Status Rumah
*
* POST params:
* house_id - ID rumah
* status - Status baru (green/red/yellow)
* center_id - ID pusat keagamaan terdekat
*/
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Hanya menerima POST']);
exit;
}
$houseId = intval($_POST['house_id'] ?? 0);
$status = $conn->real_escape_string($_POST['status'] ?? 'green');
$centerId = intval($_POST['center_id'] ?? 0);
// Validasi status
$validStatuses = ['green', 'red', 'yellow'];
if (!in_array($status, $validStatuses)) {
echo json_encode(['success' => false, 'message' => 'Status tidak valid']);
exit;
}
// Validasi house_id
if ($houseId < 1) {
echo json_encode(['success' => false, 'message' => 'house_id tidak valid']);
exit;
}
// ========================
// Upsert status rumah
// ========================
// Cek apakah rumah sudah ada
$check = $conn->query("SELECT id FROM houses WHERE id = $houseId");
if ($check && $check->num_rows > 0) {
// Update
$sql = "UPDATE houses SET status='$status' WHERE id=$houseId";
} else {
// Insert (ambil posisi dari fixed positions yang sama seperti di JS)
// Posisi disimpan juga agar konsisten
$lat = floatval($_POST['lat'] ?? 0);
$lng = floatval($_POST['lng'] ?? 0);
$sql = "INSERT INTO houses (id, latitude, longitude, status) VALUES ($houseId, $lat, $lng, '$status')";
}
if ($conn->query($sql)) {
// ========================
// Catat ke aid_logs
// ========================
if ($status === 'yellow' && $centerId > 0) {
$logSql = "INSERT INTO aid_logs (house_id, religious_center_id, status, timestamp)
VALUES ($houseId, $centerId, 'helped', NOW())";
$conn->query($logSql);
} elseif ($status !== 'yellow') {
// Batalkan bantuan — catat sebagai reverted
$logSql = "INSERT INTO aid_logs (house_id, religious_center_id, status, timestamp)
VALUES ($houseId, $centerId, 'reverted', NOW())";
$conn->query($logSql);
}
echo json_encode([
'success' => true,
'house_id' => $houseId,
'status' => $status
]);
} else {
echo json_encode(['success' => false, 'message' => $conn->error]);
}
$conn->close();