First Commit

This commit is contained in:
Araya's Project
2026-06-10 12:12:51 +07:00
commit 6462f2e396
34 changed files with 5884 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
-- ============================================================
-- WebGIS BantSOSial — Database Schema
-- Database: sig_bansos
-- ============================================================
CREATE DATABASE IF NOT EXISTS sig_bansos
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE sig_bansos;
-- ============================================================
-- Table: religious_centers
-- ============================================================
CREATE TABLE IF NOT EXISTS religious_centers (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
address TEXT,
kas DECIMAL(15,2) DEFAULT 0,
latitude DOUBLE NOT NULL,
longitude DOUBLE NOT NULL,
radius INT DEFAULT 300,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- Table: houses
-- ============================================================
CREATE TABLE IF NOT EXISTS houses (
id INT AUTO_INCREMENT PRIMARY KEY,
latitude DOUBLE NOT NULL,
longitude DOUBLE NOT NULL,
address TEXT,
rt VARCHAR(10) DEFAULT '',
rw VARCHAR(10) DEFAULT '',
kelurahan VARCHAR(100) DEFAULT '',
status_miskin ENUM('sangat_miskin','miskin','tidak_miskin','') DEFAULT '',
jumlah_anggota INT DEFAULT 0,
anggota JSON,
aid_status ENUM('helped','not_helped','outside') DEFAULT 'outside',
has_data TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- Table: laporan
-- ============================================================
CREATE TABLE IF NOT EXISTS laporan (
id INT AUTO_INCREMENT PRIMARY KEY,
pelapor VARCHAR(150) DEFAULT 'Anonim',
deskripsi TEXT NOT NULL,
lokasi VARCHAR(255) DEFAULT '',
foto_base64 MEDIUMTEXT,
status ENUM('baru','ditangani','selesai') DEFAULT 'baru',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- Table: aid_logs
-- ============================================================
CREATE TABLE IF NOT EXISTS aid_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
house_id INT NOT NULL,
religious_center_id INT DEFAULT 0,
status ENUM('helped','reverted') NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (house_id) REFERENCES houses(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+65
View File
@@ -0,0 +1,65 @@
<?php
require_once 'koneksi.php';
$response = ['success' => true, 'centers' => [], 'houses' => [], 'reports' => []];
$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();
}
$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();
}
$result = $conn->query("SELECT * FROM laporan ORDER BY created_at DESC LIMIT 100");
if ($result) {
while ($row = $result->fetch_assoc()) {
$response['reports'][] = [
'id' => (int)$row['id'],
'name' => $row['pelapor'] ?? 'Anonim',
'text' => $row['deskripsi'],
'lokasi' => $row['lokasi'] ?? '',
'imgBase64' => $row['foto_base64'] ?? null,
'status' => $row['status'] ?? 'baru',
'time' => date('d/m/Y H:i', strtotime($row['created_at']))
];
}
$result->free();
}
echo json_encode($response);
$conn->close();
+12
View File
@@ -0,0 +1,12 @@
<?php
require_once 'koneksi.php';
$id = intval($_POST['id'] ?? 0);
if ($id < 1) { echo json_encode(['success' => false, 'message' => 'ID tidak valid']); exit; }
if ($conn->query("DELETE FROM laporan WHERE id=$id")) {
echo json_encode(['success' => true, 'id' => $id]);
} else {
echo json_encode(['success' => false, 'message' => $conn->error]);
}
$conn->close();
+14
View File
@@ -0,0 +1,14 @@
<?php
require_once 'koneksi.php';
$id = intval($_POST['id'] ?? 0);
if ($id < 1) { echo json_encode(['success' => false, 'message' => 'ID tidak valid']); exit; }
$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();
+12
View File
@@ -0,0 +1,12 @@
<?php
require_once 'koneksi.php';
$id = intval($_POST['id'] ?? 0);
if ($id < 1) { echo json_encode(['success' => false, 'message' => 'ID tidak valid']); exit; }
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();
+381
View File
@@ -0,0 +1,381 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BantSOSial GIS — Distribusi Bantuan Sosial</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:wght@300;400;500;600;700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css"/>
</head>
<body>
<!-- =====================================================================
NAVBAR
===================================================================== -->
<nav id="topNav">
<div class="nav-brand">
<span class="nav-brand-icon">🕌</span>
<div>
<div class="nav-brand-title">BantSOSial GIS</div>
<div class="nav-brand-sub">Distribusi Bantuan Sosial</div>
</div>
</div>
<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>
<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:440px">
<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, dan kelola semua data</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 & lihat 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
===================================================================== -->
<div id="pagePeta" class="page active-page">
<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>
<div id="viewerNotice" style="display:none;margin:10px 14px 0;padding:10px 12px;background:#f3e8ff;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>
<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>
<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>
<div class="stat-card yellow">
<div class="stat-num" id="statHelped">0</div>
<div class="stat-label">Sudah Dibantu</div>
</div>
<div class="stat-card green">
<div class="stat-num" id="statOutside">0</div>
<div class="stat-label">Luar Radius</div>
</div>
</div>
</div>
<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..." 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>
<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 KK..." oninput="filterHouses()"/>
</div>
<div class="filter-row">
<select id="filterStatus" class="filter-select" onchange="filterHouses()">
<option value="">Semua Status</option>
<option value="helped">✅ Sudah dibantu</option>
<option value="not_helped">⏳ Belum dibantu</option>
<option value="outside">🟢 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>
<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 (belum dibantu)</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>
<button id="btnReset" class="btn-danger" onclick="confirmReset()">🗑 Reset Semua Data</button>
</div>
<div id="map"></div>
<div id="radiusTooltip" class="radius-tooltip hidden"></div>
</div>
<!-- =====================================================================
PAGE: PELAPORAN
===================================================================== -->
<div id="pagePelaporan" class="page">
<div class="report-page">
<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..."></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, maks 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" onclick="submitReport()" style="margin-top:4px;padding:12px;font-size:13px;">
📤 Kirim Laporan
</button>
</div>
</div>
<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 dari masyarakat</div>
</div>
<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>
<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 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.
</div>
</div>
</div>
</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 — EDIT RUMAH IBADAH
===================================================================== -->
<div id="centerEditModal" class="modal-overlay hidden">
<div class="modal-box" style="max-width:460px">
<div class="modal-header">
<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">
<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()">💾 Simpan Perubahan</button>
</div>
</div>
</div>
<!-- =====================================================================
MODAL — DETAIL LAPORAN
===================================================================== -->
<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>
<!-- =====================================================================
CUSTOM DATE PICKER
===================================================================== -->
<div id="datepickerOverlay" class="datepicker-overlay hidden" onclick="closeDatepickerIfOutside(event)">
<div class="datepicker-box" id="datepickerBox"></div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="script.js"></script>
</body>
</html>
+19
View File
@@ -0,0 +1,19 @@
<?php
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'sig_bansos');
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($conn->connect_error) {
http_response_code(500);
die(json_encode(['success' => false, 'message' => 'Koneksi database gagal: ' . $conn->connect_error]));
}
$conn->set_charset('utf8mb4');
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
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Hanya menerima POST']); exit;
}
$conn->query("SET FOREIGN_KEY_CHECKS=0");
$conn->query("TRUNCATE TABLE aid_logs");
$conn->query("TRUNCATE TABLE houses");
$conn->query("TRUNCATE TABLE religious_centers");
$conn->query("TRUNCATE TABLE laporan");
$conn->query("SET FOREIGN_KEY_CHECKS=1");
echo json_encode(['success' => true, 'message' => 'Semua data berhasil direset']);
$conn->close();
+1462
View File
File diff suppressed because it is too large Load Diff
+115
View File
@@ -0,0 +1,115 @@
USE sig_bansos;
-- ============================================================
-- Seed: religious_centers (5 titik di Pontianak)
-- ============================================================
INSERT INTO religious_centers (name, address, kas, latitude, longitude, radius) VALUES
('Masjid Jami Pontianak', 'Jl. Tanjungpura No.1, Pontianak Selatan', 12500000, -0.0294, 109.3225, 400),
('Masjid Mujahidin', 'Jl. Ahmad Yani, Pontianak Barat', 8750000, -0.0510, 109.3100, 350),
('Gereja Katedral Santo Yosef','Jl. Rahadi Usman No.2, Pontianak Kota', 6200000, -0.0245, 109.3344, 300),
('Vihara Bodhisattva Karaniya','Jl. Diponegoro No.47, Pontianak Kota', 4300000, -0.0348, 109.3278, 280),
('Masjid Al-Falah', 'Jl. Pahlawan, Pontianak Timur', 5100000, -0.0600, 109.3600, 320);
-- ============================================================
-- Seed: houses (15 rumah miskin)
-- ============================================================
INSERT INTO houses (latitude, longitude, address, rt, rw, kelurahan, status_miskin, jumlah_anggota, anggota, aid_status, has_data) VALUES
-- Cluster sekitar Masjid Jami (radius 400m)
(-0.0301, 109.3240, 'Jl. Tanjungpura Gang Melati No.3, Pontianak Selatan', '002', '001', 'Dalam Bugis',
'sangat_miskin', 4,
'[{"nama":"Suryanto","statusAnggota":"Kepala Keluarga","tglLahir":"1978-04-12","pekerjaan":"Buruh/Karyawan","gaji":900000},{"nama":"Dewi Suryanto","statusAnggota":"Istri/Suami","tglLahir":"1982-07-20","pekerjaan":"Tidak Bekerja","gaji":0},{"nama":"Rian Suryanto","statusAnggota":"Anak","tglLahir":"2008-01-15","pekerjaan":"Pelajar/Mahasiswa","gaji":0},{"nama":"Sari Suryanto","statusAnggota":"Anak","tglLahir":"2011-09-03","pekerjaan":"Pelajar/Mahasiswa","gaji":0}]',
'helped', 1),
(-0.0310, 109.3255, 'Jl. Tanjungpura Gang Anggrek No.7, Pontianak Selatan', '003', '001', 'Dalam Bugis',
'miskin', 3,
'[{"nama":"Hendra Wijaya","statusAnggota":"Kepala Keluarga","tglLahir":"1985-06-28","pekerjaan":"Petani/Nelayan","gaji":1200000},{"nama":"Yuli Wijaya","statusAnggota":"Istri/Suami","tglLahir":"1988-11-14","pekerjaan":"Tidak Bekerja","gaji":0},{"nama":"Budi Wijaya","statusAnggota":"Anak","tglLahir":"2014-03-22","pekerjaan":"Pelajar/Mahasiswa","gaji":0}]',
'not_helped', 1),
(-0.0285, 109.3210, 'Jl. Gajah Mada Gg. Sejahtera No.12, Pontianak Selatan', '001', '002', 'Benua Melayu Darat',
'sangat_miskin', 5,
'[{"nama":"Ahmad Fauzi","statusAnggota":"Kepala Keluarga","tglLahir":"1970-02-10","pekerjaan":"Tidak Bekerja","gaji":0},{"nama":"Siti Aminah","statusAnggota":"Istri/Suami","tglLahir":"1974-08-30","pekerjaan":"Tidak Bekerja","gaji":0},{"nama":"Rizky Fauzi","statusAnggota":"Anak","tglLahir":"1998-05-17","pekerjaan":"Buruh/Karyawan","gaji":800000},{"nama":"Nurul Fauzi","statusAnggota":"Anak","tglLahir":"2002-12-01","pekerjaan":"Pelajar/Mahasiswa","gaji":0},{"nama":"Kakek Hamid","statusAnggota":"Orang Tua","tglLahir":"1945-01-01","pekerjaan":"Tidak Bekerja","gaji":0}]',
'helped', 1),
-- Cluster sekitar Masjid Mujahidin (radius 350m)
(-0.0490, 109.3085, 'Jl. Ahmad Yani Gang Damai No.4, Pontianak Barat', '005', '003', 'Akcaya',
'miskin', 4,
'[{"nama":"Bambang Santoso","statusAnggota":"Kepala Keluarga","tglLahir":"1975-09-05","pekerjaan":"Wiraswasta","gaji":1500000},{"nama":"Rahayu Santoso","statusAnggota":"Istri/Suami","tglLahir":"1979-04-18","pekerjaan":"Tidak Bekerja","gaji":0},{"nama":"Dika Santoso","statusAnggota":"Anak","tglLahir":"2005-07-11","pekerjaan":"Pelajar/Mahasiswa","gaji":0},{"nama":"Tika Santoso","statusAnggota":"Anak","tglLahir":"2009-02-25","pekerjaan":"Pelajar/Mahasiswa","gaji":0}]',
'not_helped', 1),
(-0.0530, 109.3115, 'Jl. Kom. Yos Sudarso Gg. Mawar No.9, Pontianak Barat', '007', '004', 'Akcaya',
'sangat_miskin', 2,
'[{"nama":"Pak Idrus","statusAnggota":"Kepala Keluarga","tglLahir":"1952-03-14","pekerjaan":"Tidak Bekerja","gaji":0},{"nama":"Bu Rohani","statusAnggota":"Istri/Suami","tglLahir":"1956-11-08","pekerjaan":"Tidak Bekerja","gaji":0}]',
'helped', 1),
(-0.0480, 109.3070, 'Jl. Purnama Gang Murni No.2, Pontianak Barat', '004', '002', 'Sungai Beliung',
'miskin', 3,
'[{"nama":"Eko Prasetyo","statusAnggota":"Kepala Keluarga","tglLahir":"1983-12-20","pekerjaan":"Buruh/Karyawan","gaji":1100000},{"nama":"Fitri Prasetyo","statusAnggota":"Istri/Suami","tglLahir":"1986-05-30","pekerjaan":"Tidak Bekerja","gaji":0},{"nama":"Kevin Prasetyo","statusAnggota":"Anak","tglLahir":"2012-08-14","pekerjaan":"Pelajar/Mahasiswa","gaji":0}]',
'not_helped', 1),
-- Cluster sekitar Gereja Katedral (radius 300m)
(-0.0260, 109.3360, 'Jl. Rahadi Usman Gg. Teratai No.6, Pontianak Kota', '001', '001', 'Darat Sekip',
'tidak_miskin', 4,
'[{"nama":"Antonius Liong","statusAnggota":"Kepala Keluarga","tglLahir":"1972-07-22","pekerjaan":"Wiraswasta","gaji":3500000},{"nama":"Maria Liong","statusAnggota":"Istri/Suami","tglLahir":"1975-10-09","pekerjaan":"Buruh/Karyawan","gaji":1800000},{"nama":"Felix Liong","statusAnggota":"Anak","tglLahir":"2003-04-05","pekerjaan":"Pelajar/Mahasiswa","gaji":0},{"nama":"Clara Liong","statusAnggota":"Anak","tglLahir":"2007-01-17","pekerjaan":"Pelajar/Mahasiswa","gaji":0}]',
'outside', 1),
(-0.0235, 109.3330, 'Jl. Tanjung Raya Gg. Bakti No.11, Pontianak Kota', '002', '001', 'Darat Sekip',
'miskin', 3,
'[{"nama":"Yohanes Budi","statusAnggota":"Kepala Keluarga","tglLahir":"1980-08-16","pekerjaan":"Petani/Nelayan","gaji":900000},{"nama":"Elisabeth Budi","statusAnggota":"Istri/Suami","tglLahir":"1984-03-27","pekerjaan":"Tidak Bekerja","gaji":0},{"nama":"Theresia Budi","statusAnggota":"Anak","tglLahir":"2010-06-13","pekerjaan":"Pelajar/Mahasiswa","gaji":0}]',
'not_helped', 1),
-- Cluster sekitar Vihara (radius 280m)
(-0.0365, 109.3290, 'Jl. Diponegoro Gg. Lestari No.5, Pontianak Kota', '003', '002', 'Mariana',
'miskin', 4,
'[{"nama":"Lim Ah Kow","statusAnggota":"Kepala Keluarga","tglLahir":"1968-05-10","pekerjaan":"Wiraswasta","gaji":1200000},{"nama":"Tan Siu Lan","statusAnggota":"Istri/Suami","tglLahir":"1972-09-24","pekerjaan":"Tidak Bekerja","gaji":0},{"nama":"Lim Wei","statusAnggota":"Anak","tglLahir":"2000-11-29","pekerjaan":"Buruh/Karyawan","gaji":700000},{"nama":"Lim Hui","statusAnggota":"Anak","tglLahir":"2006-02-08","pekerjaan":"Pelajar/Mahasiswa","gaji":0}]',
'helped', 1),
(-0.0340, 109.3265, 'Jl. Veteran Gg. Rukun No.8, Pontianak Kota', '006', '003', 'Mariana',
'sangat_miskin', 2,
'[{"nama":"Wang Cheng","statusAnggota":"Kepala Keluarga","tglLahir":"1960-04-04","pekerjaan":"Tidak Bekerja","gaji":0},{"nama":"Liu Fang","statusAnggota":"Istri/Suami","tglLahir":"1963-12-18","pekerjaan":"Tidak Bekerja","gaji":0}]',
'not_helped', 1),
-- Cluster sekitar Masjid Al-Falah (radius 320m)
(-0.0580, 109.3580, 'Jl. Pahlawan Gg. Bersatu No.3, Pontianak Timur', '001', '001', 'Saigon',
'sangat_miskin', 5,
'[{"nama":"Syaiful Anwar","statusAnggota":"Kepala Keluarga","tglLahir":"1973-01-25","pekerjaan":"Buruh/Karyawan","gaji":750000},{"nama":"Nurhayati","statusAnggota":"Istri/Suami","tglLahir":"1977-06-14","pekerjaan":"Tidak Bekerja","gaji":0},{"nama":"Fajar Anwar","statusAnggota":"Anak","tglLahir":"1999-10-07","pekerjaan":"Buruh/Karyawan","gaji":600000},{"nama":"Dewi Anwar","statusAnggota":"Anak","tglLahir":"2004-03-19","pekerjaan":"Pelajar/Mahasiswa","gaji":0},{"nama":"Si Kecil","statusAnggota":"Anak","tglLahir":"2016-07-30","pekerjaan":"Pelajar/Mahasiswa","gaji":0}]',
'helped', 1),
(-0.0620, 109.3615, 'Jl. Sultan Hamid Gg. Damai No.10, Pontianak Timur', '002', '002', 'Saigon',
'miskin', 3,
'[{"nama":"Mukhtar Halim","statusAnggota":"Kepala Keluarga","tglLahir":"1981-11-03","pekerjaan":"Petani/Nelayan","gaji":1000000},{"nama":"Salmah Halim","statusAnggota":"Istri/Suami","tglLahir":"1984-08-22","pekerjaan":"Tidak Bekerja","gaji":0},{"nama":"Hafiz Halim","statusAnggota":"Anak","tglLahir":"2013-05-16","pekerjaan":"Pelajar/Mahasiswa","gaji":0}]',
'not_helped', 1),
(-0.0565, 109.3560, 'Jl. DR. Wahidin Gg. Sempurna No.15, Pontianak Timur', '004', '003', 'Banjar Serasan',
'miskin', 4,
'[{"nama":"Roslan Idris","statusAnggota":"Kepala Keluarga","tglLahir":"1976-02-17","pekerjaan":"Wiraswasta","gaji":1300000},{"nama":"Yanti Idris","statusAnggota":"Istri/Suami","tglLahir":"1979-07-05","pekerjaan":"Tidak Bekerja","gaji":0},{"nama":"Reza Idris","statusAnggota":"Anak","tglLahir":"2007-09-28","pekerjaan":"Pelajar/Mahasiswa","gaji":0},{"nama":"Nenek Maimunah","statusAnggota":"Orang Tua","tglLahir":"1950-06-12","pekerjaan":"Tidak Bekerja","gaji":0}]',
'not_helped', 1),
-- Pin tanpa data (has_data=0)
(-0.0330, 109.3310, 'Jl. Nusa Indah, Pontianak Kota', '005', '002', 'Mariana',
'', 0, '[]', 'outside', 0),
(-0.0520, 109.3090, 'Jl. Ahmad Yani, Pontianak Barat', '008', '005', 'Akcaya',
'', 0, '[]', 'outside', 0);
-- ============================================================
-- Seed: laporan
-- ============================================================
INSERT INTO laporan (pelapor, deskripsi, lokasi, status) VALUES
('Budi Santoso', 'Ada keluarga lansia di Gang Flamboyan RT 007 yang tinggal sendiri, rumahnya hampir roboh. Perlu perhatian segera.', 'Gang Flamboyan RT 007, Pontianak Barat', 'baru'),
('Siti Rahayu', 'Keluarga dengan 6 anak di Jl. Pahlawan belum pernah menerima bantuan apapun. Kondisi rumah sangat tidak layak.', 'Jl. Pahlawan, Pontianak Timur', 'ditangani'),
('Anonim', 'Ada balita yang kekurangan gizi di RT 003 RW 001, orang tuanya pengangguran. Butuh bantuan pangan segera.', 'RT 003/001, Dalam Bugis, Pontianak Selatan', 'baru'),
('Pak RT Hamzah', 'Terdapat 3 kepala keluarga di gang belakang pasar yang belum terdata. Mereka tinggal di bantaran sungai.', 'Bantaran Sungai Kapuas, Pontianak Kota', 'selesai'),
('Marlina Dewi', 'Ibu tunggal dengan 4 anak di Jl. Veteran, suami meninggal tahun lalu. Sangat membutuhkan bantuan sosial.', 'Jl. Veteran, Pontianak Kota', 'baru');
-- ============================================================
-- Seed: aid_logs (riwayat bantuan)
-- ============================================================
INSERT INTO aid_logs (house_id, religious_center_id, status, timestamp) VALUES
(1, 1, 'helped', '2026-03-10 09:00:00'),
(3, 1, 'helped', '2026-03-12 10:30:00'),
(5, 2, 'helped', '2026-03-15 08:45:00'),
(9, 4, 'helped', '2026-04-01 11:00:00'),
(11, 5, 'helped', '2026-04-05 09:30:00'),
(2, 1, 'reverted', '2026-02-20 14:00:00'),
(4, 2, 'reverted', '2026-02-25 13:00:00');
+28
View File
@@ -0,0 +1,28 @@
<?php
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Hanya POST']); exit;
}
$name = strip_tags(trim($_POST['name'] ?? 'Anonim'));
$text = strip_tags(trim($_POST['text'] ?? ''));
$lokasi = strip_tags(trim($_POST['lokasi'] ?? ''));
$img = $_POST['img'] ?? '';
if (empty($text)) {
echo json_encode(['success' => false, 'message' => 'Deskripsi laporan tidak boleh kosong']); exit;
}
if (strlen($img) > 7_000_000) {
echo json_encode(['success' => false, 'message' => 'Ukuran foto terlalu besar (max 5MB)']); exit;
}
$stmt = $conn->prepare("INSERT INTO laporan (pelapor, deskripsi, lokasi, foto_base64) VALUES (?, ?, ?, ?)");
$stmt->bind_param('ssss', $name, $text, $lokasi, $img);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'id' => $conn->insert_id]);
} else {
echo json_encode(['success' => false, 'message' => $stmt->error]);
}
$stmt->close();
$conn->close();
+41
View File
@@ -0,0 +1,41 @@
<?php
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(trim($_POST['name'] ?? '')));
$address = $conn->real_escape_string(strip_tags(trim($_POST['address'] ?? '')));
$kas = floatval($_POST['kas'] ?? 0);
$lat = floatval($_POST['lat'] ?? 0);
$lng = floatval($_POST['lng'] ?? 0);
$radius = intval($_POST['radius'] ?? 300);
$isUpd = 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 || $isUpd) {
$stmt = $conn->prepare("UPDATE religious_centers SET name=?, address=?, kas=?, latitude=?, longitude=?, radius=?, updated_at=NOW() WHERE id=?");
$stmt->bind_param('ssdddii', $name, $address, $kas, $lat, $lng, $radius, $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'id' => $id, 'action' => 'updated']);
} else {
echo json_encode(['success' => false, 'message' => $stmt->error]);
}
$stmt->close();
} else {
$stmt = $conn->prepare("INSERT INTO religious_centers (name, address, kas, latitude, longitude, radius) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->bind_param('ssdddi', $name, $address, $kas, $lat, $lng, $radius);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'id' => $conn->insert_id, 'action' => 'inserted']);
} else {
echo json_encode(['success' => false, 'message' => $stmt->error]);
}
$stmt->close();
}
$conn->close();
+49
View File
@@ -0,0 +1,49 @@
<?php
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(trim($_POST['address'] ?? '')));
$rt = $conn->real_escape_string(strip_tags(trim($_POST['rt'] ?? '')));
$rw = $conn->real_escape_string(strip_tags(trim($_POST['rw'] ?? '')));
$kelurahan = $conn->real_escape_string(strip_tags(trim($_POST['kelurahan'] ?? '')));
$statusMiskin = $_POST['status_miskin'] ?? '';
$jumlahAnggota = intval($_POST['jumlah_anggota'] ?? 0);
$anggotaRaw = $_POST['anggota'] ?? '[]';
$aidStatus = $_POST['aid_status'] ?? 'outside';
$hasData = intval($_POST['has_data'] ?? 0);
$validStatuses = ['sangat_miskin', 'miskin', 'tidak_miskin', ''];
if (!in_array($statusMiskin, $validStatuses)) $statusMiskin = '';
$validAid = ['helped', 'not_helped', 'outside'];
if (!in_array($aidStatus, $validAid)) $aidStatus = 'outside';
$anggotaDecoded = json_decode($anggotaRaw, true);
if (!is_array($anggotaDecoded)) $anggotaDecoded = [];
$anggotaJson = json_encode($anggotaDecoded, JSON_UNESCAPED_UNICODE);
if ($id > 0) {
$stmt = $conn->prepare("UPDATE houses SET latitude=?, longitude=?, address=?, rt=?, rw=?, kelurahan=?, status_miskin=?, jumlah_anggota=?, anggota=?, aid_status=?, has_data=?, updated_at=NOW() WHERE id=?");
$stmt->bind_param('ddsssssiisii', $lat, $lng, $address, $rt, $rw, $kelurahan, $statusMiskin, $jumlahAnggota, $anggotaJson, $aidStatus, $hasData, $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'id' => $id, 'action' => 'updated']);
} else {
echo json_encode(['success' => false, 'message' => $stmt->error]);
}
$stmt->close();
} else {
$stmt = $conn->prepare("INSERT INTO houses (latitude, longitude, address, rt, rw, kelurahan, status_miskin, jumlah_anggota, anggota, aid_status, has_data) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param('ddsssssissi', $lat, $lng, $address, $rt, $rw, $kelurahan, $statusMiskin, $jumlahAnggota, $anggotaJson, $aidStatus, $hasData);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'id' => $conn->insert_id, 'action' => 'inserted']);
} else {
echo json_encode(['success' => false, 'message' => $stmt->error]);
}
$stmt->close();
}
$conn->close();
+649
View File
@@ -0,0 +1,649 @@
/* ===================================================================
BantSOSial GIS — style.css
=================================================================== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--primary: #0d9488;
--primary-light: #ccfbf1;
--primary-dark: #0f766e;
--orange: #ea580c;
--orange-light: #fff7ed;
--danger: #dc2626;
--danger-light: #fef2f2;
--yellow: #d97706;
--yellow-light: #fffbeb;
--green: #16a34a;
--green-light: #f0fdf4;
--purple: #7c3aed;
--purple-light: #f5f3ff;
--bg: #f8fafc;
--surface: #ffffff;
--border: #e2e8f0;
--border-hover: #cbd5e1;
--text: #0f172a;
--text-muted: #64748b;
--text-dim: #94a3b8;
--nav-h: 56px;
--sidebar-w: 300px;
--font: 'DM Sans', -apple-system, system-ui, sans-serif;
--mono: 'DM Mono', monospace;
--radius: 10px;
--shadow: 0 2px 12px rgba(15,23,42,.07);
--shadow-md: 0 8px 30px rgba(15,23,42,.10);
}
html, body { height: 100%; overflow: hidden; font-family: var(--font); color: var(--text); background: var(--bg); }
/* ===================================================================
NAVBAR
=================================================================== */
#topNav {
position: fixed; top: 0; left: 0; right: 0; z-index: 1000;
height: var(--nav-h);
display: flex; align-items: center; justify-content: space-between;
padding: 0 16px;
background: var(--surface);
border-bottom: 1px solid var(--border);
box-shadow: var(--shadow);
}
.nav-brand { display: flex; align-items: center; gap: 10px; }
.nav-brand-icon { font-size: 22px; line-height: 1; }
.nav-brand-title { font-size: 14px; font-weight: 700; letter-spacing: -.3px; }
.nav-brand-sub { font-size: 10px; color: var(--text-muted); }
.nav-menu { display: flex; gap: 4px; }
.nav-item {
display: flex; align-items: center; gap: 6px;
padding: 6px 14px; border: none; border-radius: 8px;
background: transparent; cursor: pointer;
font-family: var(--font); font-size: 13px; font-weight: 500;
color: var(--text-muted); transition: all .2s; position: relative;
}
.nav-item:hover { background: var(--bg); color: var(--text); }
.nav-item.active { background: var(--primary-light); color: var(--primary-dark); font-weight: 600; }
.nav-item-icon { font-size: 14px; }
.nav-badge {
position: absolute; top: 4px; right: 6px;
min-width: 16px; height: 16px; padding: 0 4px;
background: var(--danger); color: #fff;
border-radius: 10px; font-size: 9px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
}
.nav-badge.hidden { display: none; }
.nav-right { display: flex; align-items: center; gap: 8px; }
.role-pill {
display: flex; align-items: center; gap: 6px;
padding: 5px 12px; border-radius: 20px;
background: var(--primary-light); color: var(--primary-dark);
font-size: 12px; font-weight: 700;
}
#roleSwitchBtn {
padding: 6px 12px; border: 1px solid var(--border); border-radius: 8px;
background: var(--surface); cursor: pointer;
font-family: var(--font); font-size: 12px; font-weight: 500;
color: var(--text-muted); transition: all .2s;
}
#roleSwitchBtn:hover { border-color: var(--border-hover); color: var(--text); }
/* ===================================================================
PAGE LAYOUT
=================================================================== */
.page { display: none; position: fixed; inset: 0; top: var(--nav-h); }
.page.active-page { display: flex; }
/* ===================================================================
SIDEBAR
=================================================================== */
#sidebar {
width: var(--sidebar-w); min-width: var(--sidebar-w);
height: 100%; overflow-y: auto;
background: var(--surface); border-right: 1px solid var(--border);
display: flex; flex-direction: column; gap: 0;
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
#sidebar::-webkit-scrollbar { width: 4px; }
#sidebar::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
/* ===================================================================
MAP
=================================================================== */
#map { flex: 1; height: 100%; z-index: 0; }
#map.adding-mode { cursor: crosshair !important; }
/* ===================================================================
MODE BADGE
=================================================================== */
.mode-badge {
margin: 10px 14px 0;
display: flex; align-items: center; gap: 8px;
padding: 8px 12px;
background: var(--primary-light); border: 1px solid var(--primary);
border-radius: 8px; font-size: 11px; font-weight: 600; color: var(--primary-dark);
}
.mode-badge.house-mode { background: #fff7ed; border-color: var(--orange); color: var(--orange); }
.mode-badge.hidden { display: none !important; }
.pulse-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--primary); flex-shrink: 0;
animation: pulse 1.5s ease-in-out infinite;
}
.mode-badge.house-mode .pulse-dot { background: var(--orange); }
@keyframes pulse { 0%,100% { opacity:1; transform:scale(1); } 50% { opacity:.5; transform:scale(1.4); } }
/* ===================================================================
BUTTONS
=================================================================== */
.btn-group {
display: flex; flex-direction: column; gap: 6px;
padding: 12px 14px;
border-bottom: 1px solid var(--border);
}
.btn-group button {
padding: 9px 14px; border: 1px solid var(--border); border-radius: 9px;
background: var(--surface); cursor: pointer;
font-family: var(--font); font-size: 12px; font-weight: 600;
color: var(--text); text-align: left; transition: all .2s;
}
.btn-group button:hover { border-color: var(--primary); color: var(--primary); background: var(--primary-light); }
.btn-group button.active { background: var(--primary); color: #fff; border-color: var(--primary-dark); }
.btn-danger {
margin: 8px 14px 14px; padding: 9px 14px;
border: 1px solid #fca5a5; border-radius: 9px;
background: var(--danger-light); cursor: pointer;
font-family: var(--font); font-size: 12px; font-weight: 600;
color: var(--danger); transition: all .2s;
}
.btn-danger:hover { background: #fee2e2; }
/* ===================================================================
STATS
=================================================================== */
.stat-panel { padding: 12px 14px; border-bottom: 1px solid var(--border); }
.stat-title { font-size: 11px; font-weight: 700; 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 {
padding: 8px 10px; border-radius: 8px;
background: var(--bg); border: 1px solid var(--border); text-align: center;
}
.stat-card.red { background: #fef2f2; border-color: #fca5a5; }
.stat-card.yellow { background: #fffbeb; border-color: #fcd34d; }
.stat-card.green { background: #f0fdf4; border-color: #86efac; }
.stat-num { font-size: 22px; font-weight: 800; font-family: var(--mono); color: var(--text); }
.stat-card.red .stat-num { color: var(--danger); }
.stat-card.yellow .stat-num { color: var(--yellow); }
.stat-card.green .stat-num { color: var(--green); }
.stat-label { font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; color: var(--text-muted); margin-top: 2px; }
/* ===================================================================
SECTIONS
=================================================================== */
.section { padding: 12px 14px; border-bottom: 1px solid var(--border); }
.section-title {
display: flex; align-items: center; justify-content: space-between;
font-size: 12px; font-weight: 700; color: var(--text); margin-bottom: 8px;
}
.badge {
display: inline-flex; align-items: center; justify-content: center;
min-width: 20px; height: 18px; padding: 0 6px;
border-radius: 10px; font-size: 10px; font-weight: 700;
}
.badge-teal { background: var(--primary-light); color: var(--primary-dark); }
.badge-orange { background: #fff7ed; color: var(--orange); }
.search-wrap { margin-bottom: 6px; }
.search-input {
width: 100%; padding: 7px 10px; border: 1px solid var(--border); border-radius: 8px;
font-family: var(--font); font-size: 12px; color: var(--text);
background: var(--bg); outline: none; transition: border-color .2s;
}
.search-input:focus { border-color: var(--primary); }
.filter-row { display: flex; gap: 6px; margin-bottom: 8px; }
.filter-select {
width: 100%; padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px;
font-family: var(--font); font-size: 11px; color: var(--text);
background: var(--bg); outline: none; cursor: pointer;
}
/* ===================================================================
CENTER LIST ITEMS
=================================================================== */
.center-item {
display: flex; align-items: flex-start; gap: 8px;
padding: 8px 10px; border-radius: 8px; border: 1px solid var(--border);
margin-bottom: 6px; cursor: pointer; transition: all .2s;
background: var(--surface);
}
.center-item:hover { border-color: var(--primary); background: var(--primary-light); }
.center-item-icon { font-size: 18px; line-height: 1.2; flex-shrink: 0; }
.center-item-body { flex: 1; min-width: 0; }
.center-item-name { font-size: 12px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.center-item-addr { font-size: 10px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.center-item-meta { display: flex; gap: 6px; margin-top: 3px; flex-wrap: wrap; }
.meta-tag {
padding: 1px 6px; border-radius: 4px; font-size: 9px; font-weight: 600;
background: var(--bg); border: 1px solid var(--border); color: var(--text-muted);
font-family: var(--mono);
}
.center-item-del {
padding: 3px 7px; border: 1px solid #fca5a5; border-radius: 6px;
background: var(--danger-light); color: var(--danger);
font-size: 10px; cursor: pointer; flex-shrink: 0; transition: all .2s;
}
.center-item-del:hover { background: #fee2e2; }
/* ===================================================================
HOUSE LIST ITEMS
=================================================================== */
.house-item {
display: flex; align-items: flex-start; gap: 8px;
padding: 8px 10px; border-radius: 8px; border: 1px solid var(--border);
margin-bottom: 6px; cursor: pointer; transition: all .2s;
background: var(--surface);
}
.house-item:hover { border-color: var(--orange); background: var(--orange-light); }
.house-item.status-helped { border-left: 3px solid var(--yellow); }
.house-item.status-not_helped { border-left: 3px solid var(--danger); }
.house-item.status-outside { border-left: 3px solid var(--green); }
.house-item-icon { font-size: 16px; line-height: 1.3; flex-shrink: 0; }
.house-item-body { flex: 1; min-width: 0; }
.house-item-name { font-size: 12px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.house-item-addr { font-size: 10px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.house-item-tags { display: flex; gap: 4px; margin-top: 3px; flex-wrap: wrap; }
.tag {
padding: 1px 6px; border-radius: 4px; font-size: 9px; font-weight: 700;
text-transform: uppercase; letter-spacing: .3px;
}
.tag-helped { background: #fef9c3; color: #854d0e; }
.tag-not_helped { background: #fef2f2; color: var(--danger); }
.tag-outside { background: var(--green-light); color: var(--green); }
.tag-nodata { background: var(--bg); color: var(--text-muted); border: 1px solid var(--border); }
.tag-sangat_miskin { background: #fef2f2; color: #991b1b; }
.tag-miskin { background: #fff7ed; color: #9a3412; }
.tag-tidak_miskin { background: var(--green-light); color: #166534; }
.house-item-del {
padding: 3px 7px; border: 1px solid #fca5a5; border-radius: 6px;
background: var(--danger-light); color: var(--danger);
font-size: 10px; cursor: pointer; flex-shrink: 0; transition: all .2s;
}
.house-item-del:hover { background: #fee2e2; }
/* ===================================================================
LEGEND
=================================================================== */
.legend-list { display: flex; flex-direction: column; gap: 6px; }
.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%; flex-shrink: 0;
border: 2px dashed #3b82f6; background: rgba(59,130,246,.08);
}
/* ===================================================================
EMPTY STATE
=================================================================== */
.empty-state { text-align: center; font-size: 11px; color: var(--text-dim); padding: 12px 0; }
/* ===================================================================
RADIUS TOOLTIP
=================================================================== */
.radius-tooltip {
position: absolute; bottom: 24px; left: 50%; transform: translateX(-50%);
z-index: 800; padding: 6px 14px;
background: rgba(15,23,42,.85); color: #fff; border-radius: 20px;
font-size: 12px; font-weight: 700; font-family: var(--mono);
pointer-events: none;
}
.radius-tooltip.hidden { display: none; }
/* ===================================================================
MAP POPUPS
=================================================================== */
.leaflet-popup-content-wrapper { padding: 0 !important; border-radius: 12px !important; overflow: hidden; box-shadow: var(--shadow-md) !important; }
.leaflet-popup-content { margin: 0 !important; width: 240px !important; }
.leaflet-popup-tip { background: var(--surface) !important; }
.map-popup { padding: 12px 14px 8px; }
.map-popup-badge { font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 10px; display: inline-block; margin-bottom: 6px; }
.map-popup-badge.helped { background: #fef9c3; color: #854d0e; }
.map-popup-badge.not_helped { background: #fef2f2; color: var(--danger); }
.map-popup-badge.outside { background: var(--green-light); color: var(--green); }
.map-popup-badge.nodata { background: var(--bg); color: var(--text-muted); }
.map-popup-title { font-size: 13px; font-weight: 700; color: var(--text); margin-bottom: 3px; }
.map-popup-addr { font-size: 11px; color: var(--text-muted); line-height: 1.5; }
.map-popup-actions { display: flex; gap: 5px; padding: 8px 14px 12px; border-top: 1px solid var(--border); }
.popup-btn {
flex: 1; padding: 6px 8px; border-radius: 7px; border: 1px solid var(--border);
background: var(--bg); cursor: pointer; font-family: var(--font);
font-size: 10px; font-weight: 600; color: var(--text); transition: all .2s;
}
.popup-btn:hover { border-color: var(--border-hover); }
.popup-btn.primary { background: var(--primary-light); border-color: var(--primary); color: var(--primary-dark); }
.popup-btn.orange { background: #fff7ed; border-color: var(--orange); color: var(--orange); }
.center-popup { padding: 12px 13px 8px; }
.center-popup-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.center-popup-icon {
width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
font-size: 16px; background: var(--primary-light); flex-shrink: 0;
}
.center-popup-title { font-size: 13px; font-weight: 700; color: var(--text); line-height: 1.3; }
.center-popup-addr { font-size: 11px; color: var(--text-muted); margin-bottom: 8px; }
.kas-badge {
display: flex; align-items: center; gap: 6px; margin-bottom: 6px;
padding: 5px 8px; background: var(--green-light); border: 1px solid #86efac; border-radius: 7px;
font-size: 11px;
}
.kas-label { color: var(--green); font-weight: 600; flex: 1; }
.kas-value { font-family: var(--mono); font-size: 12px; font-weight: 700; color: var(--green); }
.radius-info-box {
display: flex; justify-content: space-between; align-items: center;
padding: 5px 8px; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 7px;
font-size: 11px; color: #1e40af; margin-bottom: 5px;
}
.radius-val { font-family: var(--mono); font-weight: 700; }
.radius-drag-hint { font-size: 10px; color: var(--text-dim); text-align: center; padding: 3px 0 5px; }
.form-popup { padding: 12px; }
.form-popup h3 { font-size: 13px; font-weight: 700; margin-bottom: 10px; color: var(--text); }
/* ===================================================================
MODALS
=================================================================== */
.modal-overlay {
position: fixed; inset: 0; z-index: 2000;
background: rgba(15,23,42,.45);
display: flex; align-items: center; justify-content: center;
padding: 20px;
backdrop-filter: blur(4px);
}
.modal-overlay.hidden { display: none !important; }
.modal-box {
background: var(--surface); border-radius: 16px;
width: 100%; max-width: 580px; max-height: 88vh;
display: flex; flex-direction: column;
box-shadow: 0 24px 60px rgba(15,23,42,.18);
overflow: hidden;
}
.modal-wide { max-width: 700px; }
.modal-header {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 20px; border-bottom: 1px solid var(--border);
background: linear-gradient(135deg, #f0fdfa, #e0f2fe); flex-shrink: 0;
}
.modal-title { font-size: 15px; font-weight: 700; color: var(--text); }
.modal-close {
width: 28px; height: 28px; border: none; border-radius: 8px;
background: rgba(15,23,42,.06); cursor: pointer;
font-size: 13px; color: var(--text-muted); transition: all .2s;
display: flex; align-items: center; justify-content: center;
}
.modal-close:hover { background: rgba(15,23,42,.12); color: var(--text); }
.modal-body { overflow-y: auto; flex: 1; padding: 20px; }
.modal-actions {
display: flex; justify-content: flex-end; gap: 8px;
padding: 14px 20px; border-top: 1px solid var(--border); flex-shrink: 0;
}
.btn-modal-cancel {
padding: 8px 18px; border: 1px solid var(--border); border-radius: 8px;
background: var(--surface); cursor: pointer; font-family: var(--font);
font-size: 13px; font-weight: 500; color: var(--text-muted); transition: all .2s;
}
.btn-modal-cancel:hover { border-color: var(--border-hover); color: var(--text); }
.btn-modal-save {
padding: 8px 18px; border: none; border-radius: 8px;
background: var(--primary); cursor: pointer; font-family: var(--font);
font-size: 13px; font-weight: 700; color: #fff; transition: all .2s;
}
.btn-modal-save:hover { background: var(--primary-dark); }
/* ===================================================================
FORMS (modal)
=================================================================== */
.modal-section { margin-bottom: 20px; }
.modal-section:last-child { margin-bottom: 0; }
.modal-section-title {
font-size: 11px; font-weight: 700; text-transform: uppercase;
letter-spacing: .8px; color: var(--text-muted); margin-bottom: 10px;
padding-bottom: 6px; border-bottom: 1px solid var(--border);
}
.form-group { margin-bottom: 12px; }
.form-group:last-child { margin-bottom: 0; }
.form-row { display: flex; gap: 10px; }
.form-row .form-group { flex: 1; margin-bottom: 0; }
.form-label { display: block; font-size: 11px; font-weight: 600; color: var(--text-muted); margin-bottom: 5px; }
.form-input {
width: 100%; padding: 8px 10px; border: 1.5px solid var(--border); border-radius: 8px;
font-family: var(--font); font-size: 13px; color: var(--text);
background: var(--bg); outline: none; transition: border-color .2s;
}
.form-input:focus { border-color: var(--primary); background: var(--surface); }
.form-textarea { resize: vertical; min-height: 80px; }
.form-submit {
width: 100%; padding: 10px; border: none; border-radius: 9px;
background: var(--primary); color: #fff; cursor: pointer;
font-family: var(--font); font-size: 13px; font-weight: 700; transition: all .2s;
}
.form-submit:hover { background: var(--primary-dark); }
.info-box { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; margin-bottom: 10px; }
.info-box-row { display: flex; justify-content: space-between; padding: 7px 12px; border-bottom: 1px solid var(--border); font-size: 12px; }
.info-box-row:last-child { border-bottom: none; }
.info-box-label { color: var(--text-muted); font-weight: 500; }
.info-box-val { font-weight: 600; color: var(--text); text-align: right; max-width: 60%; }
/* ===================================================================
STATUS PILLS
=================================================================== */
.status-pills { display: flex; gap: 8px; flex-wrap: wrap; }
.status-pill {
flex: 1; min-width: 100px; padding: 8px 10px; border-radius: 9px;
border: 2px solid var(--border); cursor: pointer;
font-size: 12px; font-weight: 600; text-align: center;
background: var(--surface); transition: all .2s; color: var(--text-muted);
}
.status-pill:hover { border-color: var(--border-hover); }
.status-pill.selected.sangat_miskin { background: #fef2f2; border-color: var(--danger); color: #991b1b; }
.status-pill.selected.miskin { background: #fff7ed; border-color: var(--orange); color: #9a3412; }
.status-pill.selected.tidak_miskin { background: var(--green-light); border-color: var(--green); color: #166534; }
/* ===================================================================
MEMBER CARD
=================================================================== */
.member-card {
border: 1px solid var(--border); border-radius: 10px;
margin-bottom: 10px; overflow: hidden;
}
.member-card-header {
display: flex; align-items: center; gap: 8px;
padding: 8px 12px;
background: var(--bg); border-bottom: 1px solid var(--border);
font-size: 12px; font-weight: 700; color: var(--text-muted);
}
.member-num {
width: 20px; height: 20px; border-radius: 50%;
background: var(--primary); color: #fff;
display: flex; align-items: center; justify-content: center;
font-size: 10px; font-weight: 700; flex-shrink: 0;
}
.member-card-body { padding: 10px 12px; }
/* ===================================================================
DETAIL MODAL
=================================================================== */
.detail-section { margin-bottom: 16px; }
.detail-section-title {
font-size: 11px; font-weight: 700; text-transform: uppercase;
letter-spacing: .8px; color: var(--text-muted); margin-bottom: 8px;
padding-bottom: 5px; border-bottom: 1px solid var(--border);
}
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.detail-kv { background: var(--bg); border-radius: 7px; padding: 8px 10px; }
.detail-key { font-size: 10px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; letter-spacing: .4px; }
.detail-val { font-size: 13px; font-weight: 600; color: var(--text); margin-top: 2px; }
.member-table { width: 100%; border-collapse: collapse; font-size: 11px; }
.member-table th { padding: 6px 10px; background: var(--bg); font-weight: 700; text-align: left; border-bottom: 1px solid var(--border); color: var(--text-muted); font-size: 10px; text-transform: uppercase; }
.member-table td { padding: 7px 10px; border-bottom: 1px solid var(--border); color: var(--text); }
.member-table tr:last-child td { border-bottom: none; }
.member-table tr:hover td { background: var(--bg); }
.aid-btn-row { display: flex; gap: 6px; margin-top: 10px; }
.aid-btn {
flex: 1; padding: 8px; border-radius: 8px; border: 1px solid var(--border);
background: var(--bg); cursor: pointer; font-family: var(--font);
font-size: 11px; font-weight: 700; transition: all .2s; color: var(--text-muted);
}
.aid-btn:hover { border-color: var(--border-hover); }
.aid-btn.active-helped { background: #fef9c3; border-color: #fcd34d; color: #854d0e; }
.aid-btn.active-not_helped { background: #fef2f2; border-color: #fca5a5; color: var(--danger); }
.aid-btn.active-outside { background: var(--green-light); border-color: #86efac; color: var(--green); }
/* ===================================================================
ROLE CARDS
=================================================================== */
.role-cards { display: flex; flex-direction: column; gap: 8px; }
.role-card {
padding: 12px 14px; border: 2px solid var(--border); border-radius: 10px;
cursor: pointer; transition: all .2s; background: var(--surface);
}
.role-card:hover { border-color: var(--border-hover); }
.role-card.active-role { border-color: var(--primary); background: var(--primary-light); }
.role-card-icon { font-size: 20px; margin-bottom: 4px; }
.role-card-name { font-size: 13px; font-weight: 700; color: var(--text); margin-bottom: 3px; }
.role-card-desc { font-size: 11px; color: var(--text-muted); margin-bottom: 8px; }
.role-card-perms { display: flex; flex-wrap: wrap; gap: 5px; }
.perm { padding: 2px 8px; border-radius: 5px; font-size: 10px; font-weight: 600; }
.perm-green { background: var(--green-light); color: var(--green); }
.perm-red { background: #fef2f2; color: var(--danger); }
/* ===================================================================
REPORT PAGE
=================================================================== */
#pagePelaporan.active-page { overflow: hidden; }
.report-page {
display: flex; width: 100%; height: 100%;
overflow: hidden;
}
.report-col {
display: flex; flex-direction: column;
height: 100%; overflow: hidden;
}
.report-col-form { width: 380px; min-width: 320px; border-right: 1px solid var(--border); background: var(--surface); }
.report-col-list { flex: 1; background: var(--bg); }
.report-col-viewer { flex: 1; background: var(--bg); }
.report-col-header {
padding: 16px 20px 14px; border-bottom: 1px solid var(--border);
background: var(--surface); flex-shrink: 0;
}
.report-col-title { font-size: 15px; font-weight: 700; color: var(--text); display: flex; align-items: center; }
.report-col-sub { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
.report-form-body { padding: 16px 20px; overflow-y: auto; flex: 1; }
.report-list-toolbar {
display: flex; gap: 8px; align-items: center;
padding: 12px 16px; border-bottom: 1px solid var(--border);
background: var(--surface); flex-shrink: 0;
}
.report-list-container { flex: 1; overflow-y: auto; padding: 12px 16px; }
.upload-area {
border: 2px dashed var(--border); border-radius: 10px;
padding: 20px; text-align: center; cursor: pointer;
transition: all .2s; background: var(--bg);
}
.upload-area:hover { border-color: var(--primary); background: var(--primary-light); }
.upload-icon { font-size: 28px; margin-bottom: 6px; }
.upload-text { font-size: 13px; font-weight: 600; color: var(--text); }
.upload-sub { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
.remove-img-btn {
margin-top: 6px; padding: 4px 10px; border: 1px solid #fca5a5; border-radius: 6px;
background: var(--danger-light); color: var(--danger); cursor: pointer;
font-size: 11px; font-weight: 600;
}
/* Report card */
.report-card {
background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
margin-bottom: 10px; overflow: hidden; transition: all .2s;
}
.report-card:hover { border-color: var(--border-hover); box-shadow: var(--shadow); }
.report-card-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px 8px; }
.report-card-meta { font-size: 10px; color: var(--text-muted); }
.report-card-name { font-weight: 700; color: var(--text); }
.report-status {
padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 700;
}
.report-status.baru { background: #dbeafe; color: #1d4ed8; }
.report-status.ditangani { background: #fef9c3; color: #854d0e; }
.report-status.selesai { background: var(--green-light); color: var(--green); }
.report-card-text { padding: 0 14px 8px; font-size: 12px; color: var(--text); line-height: 1.6; }
.report-card-footer {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 14px; border-top: 1px solid var(--border); background: var(--bg);
}
.report-card-time { font-size: 10px; color: var(--text-dim); }
.report-card-actions { display: flex; gap: 5px; }
.report-action-btn {
padding: 3px 8px; border: 1px solid var(--border); border-radius: 6px;
background: var(--surface); cursor: pointer; font-size: 10px; font-weight: 600;
color: var(--text-muted); font-family: var(--font); transition: all .2s;
}
.report-action-btn:hover { border-color: var(--primary); color: var(--primary); }
.report-action-btn.danger { border-color: #fca5a5; color: var(--danger); }
.report-action-btn.danger:hover { background: var(--danger-light); }
/* ===================================================================
DATE PICKER
=================================================================== */
.datepicker-overlay {
position: fixed; inset: 0; z-index: 3000;
background: rgba(15,23,42,.3);
display: flex; align-items: center; justify-content: center;
}
.datepicker-overlay.hidden { display: none !important; }
.datepicker-box {
background: var(--surface); border-radius: 14px;
box-shadow: var(--shadow-md); padding: 16px;
width: 280px; user-select: none;
}
.dp-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.dp-nav { background: none; border: 1px solid var(--border); border-radius: 7px; width: 28px; height: 28px; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; transition: all .2s; }
.dp-nav:hover { background: var(--bg); }
.dp-title { font-size: 13px; font-weight: 700; color: var(--text); }
.dp-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin-bottom: 8px; }
.dp-day-label { font-size: 9px; font-weight: 700; text-align: center; color: var(--text-muted); padding: 4px 0; text-transform: uppercase; }
.dp-day {
aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
border-radius: 6px; font-size: 11px; cursor: pointer; transition: all .15s; color: var(--text);
}
.dp-day:hover { background: var(--primary-light); color: var(--primary-dark); }
.dp-day.selected { background: var(--primary); color: #fff; font-weight: 700; }
.dp-day.today { font-weight: 700; color: var(--primary); }
.dp-day.other-month { color: var(--text-dim); }
.dp-day.disabled { opacity: .35; pointer-events: none; }
.dp-footer { display: flex; justify-content: flex-end; gap: 6px; }
.dp-btn {
padding: 6px 14px; border-radius: 7px; border: 1px solid var(--border);
cursor: pointer; font-size: 12px; font-weight: 600; font-family: var(--font);
background: var(--bg); color: var(--text-muted); transition: all .2s;
}
.dp-btn.primary { background: var(--primary); border-color: var(--primary); color: #fff; }
.dp-btn.primary:hover { background: var(--primary-dark); }
.date-input-wrap { position: relative; }
.date-display-btn {
width: 100%; padding: 8px 10px; border: 1.5px solid var(--border); border-radius: 8px;
text-align: left; background: var(--bg); cursor: pointer;
font-family: var(--font); font-size: 13px; color: var(--text);
transition: border-color .2s;
}
.date-display-btn:hover, .date-display-btn:focus { border-color: var(--primary); }
/* ===================================================================
SALARY TOGGLE
=================================================================== */
.salary-row { display: none; }
.salary-row.visible { display: flex; }
+24
View File
@@ -0,0 +1,24 @@
<?php
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Hanya menerima POST']); exit;
}
$id = intval($_POST['id'] ?? 0);
$status = $_POST['status'] ?? 'baru';
$valid = ['baru', 'ditangani', 'selesai'];
if (!in_array($status, $valid) || $id < 1) {
echo json_encode(['success' => false, 'message' => 'Data tidak valid']); exit;
}
$stmt = $conn->prepare("UPDATE laporan SET status=? WHERE id=?");
$stmt->bind_param('si', $status, $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'id' => $id, 'status' => $status]);
} else {
echo json_encode(['success' => false, 'message' => $stmt->error]);
}
$stmt->close();
$conn->close();
+22
View File
@@ -0,0 +1,22 @@
<?php
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; }
if ($radius < 50) $radius = 50;
$stmt = $conn->prepare("UPDATE religious_centers SET radius=?, updated_at=NOW() WHERE id=?");
$stmt->bind_param('ii', $radius, $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'id' => $id, 'radius' => $radius]);
} else {
echo json_encode(['success' => false, 'message' => $stmt->error]);
}
$stmt->close();
$conn->close();
+41
View File
@@ -0,0 +1,41 @@
<?php
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);
$aidStatus = $_POST['aid_status'] ?? 'outside';
$centerId = intval($_POST['center_id'] ?? 0);
$validAid = ['helped', 'not_helped', 'outside'];
if (!in_array($aidStatus, $validAid)) {
echo json_encode(['success' => false, 'message' => 'Status tidak valid']); exit;
}
if ($houseId < 1) {
echo json_encode(['success' => false, 'message' => 'house_id tidak valid']); exit;
}
$stmt = $conn->prepare("UPDATE houses SET aid_status=?, updated_at=NOW() WHERE id=?");
$stmt->bind_param('si', $aidStatus, $houseId);
if ($stmt->execute()) {
$stmt->close();
if ($aidStatus === 'helped' && $centerId > 0) {
$logStatus = 'helped';
$ls = $conn->prepare("INSERT INTO aid_logs (house_id, religious_center_id, status) VALUES (?, ?, ?)");
$ls->bind_param('iis', $houseId, $centerId, $logStatus);
$ls->execute(); $ls->close();
} elseif ($aidStatus !== 'helped') {
$logStatus = 'reverted';
$ls = $conn->prepare("INSERT INTO aid_logs (house_id, religious_center_id, status) VALUES (?, ?, ?)");
$ls->bind_param('iis', $houseId, $centerId, $logStatus);
$ls->execute(); $ls->close();
}
echo json_encode(['success' => true, 'house_id' => $houseId, 'aid_status' => $aidStatus]);
} else {
echo json_encode(['success' => false, 'message' => $stmt->error]);
$stmt->close();
}
$conn->close();