update besar besaran
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
$host = 'localhost';
|
||||||
|
$db = 'webgis_bansos';
|
||||||
|
$user = 'root';
|
||||||
|
$pass = ''; // Default XAMPP password is empty
|
||||||
|
$charset = 'utf8mb4';
|
||||||
|
|
||||||
|
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
|
||||||
|
$options = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO($dsn, $user, $pass, $options);
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
echo json_encode(['error' => 'Database connection failed: ' . $e->getMessage()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Access-Control-Allow-Methods: GET, POST');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type');
|
||||||
|
|
||||||
|
require_once 'db.php';
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||||
|
if ($action === 'alldb') {
|
||||||
|
$stmt = $pdo->query("SELECT * FROM features");
|
||||||
|
$rows = $stmt->fetchAll();
|
||||||
|
$db = [
|
||||||
|
'spbu' => [],
|
||||||
|
'jalan' => [],
|
||||||
|
'parsil' => [],
|
||||||
|
'warga' => [],
|
||||||
|
'rumahIbadah' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$layer = $row['layer'];
|
||||||
|
$data = json_decode($row['data'], true);
|
||||||
|
// Ensure ID is part of the data
|
||||||
|
if (!isset($data['id'])) {
|
||||||
|
$data['id'] = $row['id'];
|
||||||
|
}
|
||||||
|
if (!isset($db[$layer])) {
|
||||||
|
$db[$layer] = [];
|
||||||
|
}
|
||||||
|
$db[$layer][] = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($db);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if ($action === 'add') {
|
||||||
|
$layer = $input['layer'];
|
||||||
|
$data = $input['data'];
|
||||||
|
$id = $data['id']; // Expecting frontend to generate ID or we could generate it here
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO features (id, layer, data) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([$id, $layer, json_encode($data)]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'update') {
|
||||||
|
$layer = $input['layer'];
|
||||||
|
$id = $input['id'];
|
||||||
|
$patch = $input['patch'];
|
||||||
|
|
||||||
|
// Fetch existing data
|
||||||
|
$stmt = $pdo->prepare("SELECT data FROM features WHERE id = ? AND layer = ?");
|
||||||
|
$stmt->execute([$id, $layer]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($row) {
|
||||||
|
$currentData = json_decode($row['data'], true);
|
||||||
|
$newData = array_merge($currentData, $patch);
|
||||||
|
|
||||||
|
$updateStmt = $pdo->prepare("UPDATE features SET data = ? WHERE id = ? AND layer = ?");
|
||||||
|
$updateStmt->execute([json_encode($newData), $id, $layer]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Record not found']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'remove') {
|
||||||
|
$layer = $input['layer'];
|
||||||
|
$id = $input['id'];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM features WHERE id = ? AND layer = ?");
|
||||||
|
$stmt->execute([$id, $layer]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['error' => 'Invalid action']);
|
||||||
|
?>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE DATABASE IF NOT EXISTS webgis_bansos;
|
||||||
|
USE webgis_bansos;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS features (
|
||||||
|
id VARCHAR(100) PRIMARY KEY,
|
||||||
|
layer VARCHAR(50) NOT NULL,
|
||||||
|
data JSON NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for faster queries by layer
|
||||||
|
CREATE INDEX idx_layer ON features(layer);
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# Rencana Penggabungan (Merge) & Pembaruan Fitur
|
|
||||||
|
|
||||||
## Tujuan
|
|
||||||
1. **Penggabungan Folder:** Menghapus folder `webgis` yang lama (karena sudah usang) dan mengubah nama folder `webgis02` menjadi `webgis`. Hal ini dilakukan agar proyek menjadi satu kesatuan (tidak terpisah-pisah) dan tetap mempertahankan kode versi terbaru yang ada di `webgis02`.
|
|
||||||
2. **Penggabungan Fitur Warga & Bansos:** Menggabungkan layer "Warga Miskin" dan "Rumah Bansos" menjadi satu layer tunggal **"Pendataan Warga & Bansos"** (`warga`). Tujuannya adalah menyederhanakan proses pendataan warga sekitar sekaligus menandai warga mana saja yang menerima bansos, tanpa harus membuat titik/layer yang terpisah.
|
|
||||||
3. **Fungsi Rumah Ibadah:** Tetap menggunakan Rumah Ibadah sebagai "Hub" (radius 300m) untuk mendistribusikan bansos kepada warga yang terdata di sekitarnya.
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> **User Review Required**
|
|
||||||
> Proses ini akan **menghapus folder `webgis` lama** dan mengganti nama `webgis02` menjadi `webgis`. Harap pastikan tidak ada kode penting di `webgis` lama yang belum dipindahkan. Silakan setujui rencana ini untuk mulai mengeksekusi!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Proposed Changes
|
|
||||||
|
|
||||||
### 1. Konsolidasi Folder
|
|
||||||
- Menghapus folder `webgis` lama.
|
|
||||||
- Me-rename folder `webgis02` menjadi `webgis`.
|
|
||||||
|
|
||||||
### 2. Penggabungan Layer "Warga" dan "Bansos" di `webgis/app.js`
|
|
||||||
|
|
||||||
#### [MODIFY] `app.js`
|
|
||||||
- **Konfigurasi Layer (`CFG`)**:
|
|
||||||
- Menghapus konfigurasi `CFG.bansos`.
|
|
||||||
- Memperbarui `CFG.warga` dengan ikon, warna, dan status penerimaan gabungan.
|
|
||||||
- **Form UI (`Forms.warga`)**:
|
|
||||||
- Mengadopsi sistem multi-tab yang sebelumnya kita buat untuk bansos (Data KK, Ekonomi, Pendidikan, Kesehatan, Riwayat Bansos) ke dalam form `warga`.
|
|
||||||
- **Popup & Card (`Popup.warga` & `Card.warga`)**:
|
|
||||||
- Memperbarui informasi popup dan card agar menampilkan status jangkauan (radius) dan riwayat bansos warga.
|
|
||||||
- **Render Map (`renderMap`)**:
|
|
||||||
- Menghapus logic render khusus `bansos`.
|
|
||||||
- Mengimplementasikan kalkulasi radius `rumahIbadah` langsung ke marker `warga` (menandai warga mana yang masuk jangkauan 300m).
|
|
||||||
- **Dashboard Analitik (`Dashboard.render`)**:
|
|
||||||
- Memperbarui tab "Ringkasan" dan "Distribusi" agar membaca data gabungan dari database `warga`.
|
|
||||||
|
|
||||||
### 3. Pembersihan di `webgis/index.html`
|
|
||||||
#### [MODIFY] `index.html`
|
|
||||||
- Menghapus tab navigasi (nav-pill) untuk "Rumah Bansos", menyisakan 5 tab (SPBU, Jalan, Parsil, Warga & Bansos, Rumah Ibadah).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification Plan
|
|
||||||
|
|
||||||
### Manual Verification
|
|
||||||
1. Verifikasi struktur folder hanya tersisa 1 direktori utama (`webgis`).
|
|
||||||
2. Buka `webgis/index.html` di browser.
|
|
||||||
3. Klik tab "Warga & Bansos" dan coba tambahkan data. Form multi-tab harus muncul lengkap.
|
|
||||||
4. Aktifkan tab "Rumah Ibadah" dan periksa apakah lingkaran radius 300m tergambar dan Warga di sekitarnya mendapatkan tanda terjangkau.
|
|
||||||
5. Periksa Dashboard Analitik, pastikan data warga dan distribusi bansos terbaca dengan benar.
|
|
||||||
+261
@@ -0,0 +1,261 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Portal WebGIS - Praktikum SIG</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #0f172a;
|
||||||
|
--card-bg: rgba(30, 41, 59, 0.7);
|
||||||
|
--card-border: rgba(255, 255, 255, 0.1);
|
||||||
|
--text-main: #f8fafc;
|
||||||
|
--text-muted: #cbd5e1;
|
||||||
|
--accent-1: #34d47e; /* SPBU green */
|
||||||
|
--accent-2: #f0a030; /* Jalan orange */
|
||||||
|
--accent-3: #9b7bfa; /* Parsil purple */
|
||||||
|
--accent-hover: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'DM Sans', sans-serif;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(at 0% 0%, rgba(52, 212, 126, 0.15) 0px, transparent 50%),
|
||||||
|
radial-gradient(at 100% 0%, rgba(155, 123, 250, 0.15) 0px, transparent 50%),
|
||||||
|
radial-gradient(at 100% 100%, rgba(240, 160, 48, 0.15) 0px, transparent 50%);
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
animation: fadeInDown 0.8s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background: linear-gradient(135deg, #fff, #cbd5e1);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.subtitle {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
animation: fadeInUp 0.8s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2.5rem 2rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-main);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--accent-1);
|
||||||
|
transition: height 0.3s ease;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:nth-child(2)::before { background: var(--accent-2); }
|
||||||
|
.card:nth-child(3)::before { background: linear-gradient(90deg, var(--accent-3), #2ec4b6); }
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title-icon {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-features {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-tag {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover .card-btn {
|
||||||
|
background: white;
|
||||||
|
color: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover .card-btn svg {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-btn svg {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from { opacity: 0; transform: translateY(-20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
margin-top: 4rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>Portal WebGIS</h1>
|
||||||
|
<p class="subtitle">Pilih modul pertemuan di bawah ini untuk mengakses sistem pemetaan geospasial.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="grid-container">
|
||||||
|
|
||||||
|
<!-- Pertemuan 1 -->
|
||||||
|
<a href="pertemuan_1/index.html" class="card">
|
||||||
|
<div class="card-title">
|
||||||
|
<span class="card-title-icon">📍</span> Pertemuan 1
|
||||||
|
</div>
|
||||||
|
<p class="card-desc">Fokus pada digitasi Point (Titik) untuk mendata persebaran dan informasi Stasiun Bahan Bakar Umum (SPBU).</p>
|
||||||
|
<div class="card-features">
|
||||||
|
<span class="feature-tag">Point</span>
|
||||||
|
<span class="feature-tag">Data SPBU</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-btn">
|
||||||
|
Buka Aplikasi
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Pertemuan 2 -->
|
||||||
|
<a href="pertemuan_2/index.html" class="card">
|
||||||
|
<div class="card-title">
|
||||||
|
<span class="card-title-icon">🛣️</span> Pertemuan 2
|
||||||
|
</div>
|
||||||
|
<p class="card-desc">Pengembangan digitasi spasial lanjutan dengan menambahkan fitur Polyline (Jalan) dan Polygon (Parsil Tanah).</p>
|
||||||
|
<div class="card-features">
|
||||||
|
<span class="feature-tag">Point</span>
|
||||||
|
<span class="feature-tag">Polyline</span>
|
||||||
|
<span class="feature-tag">Polygon</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-btn">
|
||||||
|
Buka Aplikasi
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Pertemuan 3 -->
|
||||||
|
<a href="pertemuan_3/index.html" class="card">
|
||||||
|
<div class="card-title">
|
||||||
|
<span class="card-title-icon">🌐</span> Pertemuan 3
|
||||||
|
</div>
|
||||||
|
<p class="card-desc">Sistem komprehensif Distribusi Bansos Berbasis Rumah Ibadah dengan dashboard analitik dan radius spasial otomatis.</p>
|
||||||
|
<div class="card-features">
|
||||||
|
<span class="feature-tag">All Geometries</span>
|
||||||
|
<span class="feature-tag">Data Warga</span>
|
||||||
|
<span class="feature-tag">Dashboard Analitik</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-btn">
|
||||||
|
Buka Aplikasi
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
© Praktikum Sistem Informasi Geografis
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Submodule
+1
Submodule pertemuan_1/UAS_SIG added at 30bf02df8b
@@ -0,0 +1,125 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>WebGIS – Sistem Distribusi Bansos Berbasis Rumah Ibadah</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"/>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
|
||||||
|
<!-- ═══ TOPBAR ═══ -->
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="brand">
|
||||||
|
<div class="brand-mark">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="brand-name">WebGIS Distribusi Bansos<span>Berbasis Rumah Ibadah — Radius 300m</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="topbar-div"></div>
|
||||||
|
<nav class="nav-pills" id="nav-pills">
|
||||||
|
<button class="nav-pill active" data-layer="spbu">
|
||||||
|
<span class="pill-dot" style="background:#34d47e"></span>SPBU
|
||||||
|
<span class="pill-badge" id="badge-spbu">0</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="topbar-stat"><span class="stat-dot"></span><span id="total-count">0 fitur</span></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- ═══ MAIN ═══ -->
|
||||||
|
<div class="main">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-head">
|
||||||
|
<div>
|
||||||
|
<div class="sidebar-title" id="sb-title">Data SPBU</div>
|
||||||
|
<div class="sidebar-sub" id="sb-sub">Stasiun Bahan Bakar Umum</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-add" id="btn-add">
|
||||||
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>Tambah
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-body" id="sb-list"></div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Map -->
|
||||||
|
<div class="map-area">
|
||||||
|
<div id="map"></div>
|
||||||
|
<!-- Draw indicator -->
|
||||||
|
<div class="draw-bar hidden" id="draw-bar">
|
||||||
|
<span class="draw-pulse"></span>
|
||||||
|
<span id="draw-text">Mode gambar aktif</span>
|
||||||
|
<button class="btn-cancel-draw" id="btn-cancel">Batal</button>
|
||||||
|
</div>
|
||||||
|
<!-- Legend -->
|
||||||
|
<div class="map-legend" id="legend">
|
||||||
|
<div class="legend-title">Legenda</div>
|
||||||
|
<div id="legend-items"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ MODAL ═══ -->
|
||||||
|
<div class="modal-ov hidden" id="modal-ov">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-head">
|
||||||
|
<div class="modal-head-left">
|
||||||
|
<div class="modal-icon" id="modal-icon">📍</div>
|
||||||
|
<div>
|
||||||
|
<div class="modal-title" id="modal-title">Form Data</div>
|
||||||
|
<div class="modal-sub" id="modal-sub"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" id="modal-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="modal-body"></div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<button class="btn btn-ghost" id="modal-cancel">Batal</button>
|
||||||
|
<button class="btn btn-primary" id="modal-save">Simpan Data</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ DASHBOARD ═══ -->
|
||||||
|
<div class="dash-panel hidden" id="dash-panel">
|
||||||
|
<div class="dash-inner">
|
||||||
|
<div class="dash-head">
|
||||||
|
<div>
|
||||||
|
<div class="dash-head-title">Dashboard Analitik Kemiskinan</div>
|
||||||
|
<div class="dash-head-sub">Ringkasan & analisis data geospasial untuk pengambilan kebijakan</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" onclick="Dashboard.toggle()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="dash-body" id="dash-body"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ TOASTS ═══ -->
|
||||||
|
<div class="toasts" id="toasts"></div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
||||||
|
<script src="js/utils/helpers.js"></script>
|
||||||
|
<script src="js/utils/store.js"></script>
|
||||||
|
<script src="js/utils/toast.js"></script>
|
||||||
|
<script src="js/config/config.js"></script>
|
||||||
|
<script src="js/components/forms.js"></script>
|
||||||
|
<script src="js/components/popup.js"></script>
|
||||||
|
<script src="js/components/card.js"></script>
|
||||||
|
<script src="js/components/legend.js"></script>
|
||||||
|
<script src="js/modules/draw.js"></script>
|
||||||
|
<script src="js/modules/modal.js"></script>
|
||||||
|
<script src="js/modules/sidebar.js"></script>
|
||||||
|
<script src="js/modules/dashboard.js"></script>
|
||||||
|
<script src="js/modules/map.js"></script>
|
||||||
|
<script src="js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
14. APP ROOT
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const App = {
|
||||||
|
state:{ activeLayer:'spbu' },
|
||||||
|
_groups:{},
|
||||||
|
|
||||||
|
async init(){
|
||||||
|
// Map
|
||||||
|
this._map=L.map('map',{zoomControl:false}).setView([-0.056031,109.348641],13);
|
||||||
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{
|
||||||
|
attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',maxZoom:19
|
||||||
|
}).addTo(this._map);
|
||||||
|
L.control.zoom({position:'topright'}).addTo(this._map);
|
||||||
|
['spbu'].forEach(k=>{
|
||||||
|
this._groups[k]=L.layerGroup().addTo(this._map);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expose map for Draw
|
||||||
|
App.map={
|
||||||
|
instance: this._map,
|
||||||
|
zoomTo:(ln,id)=>{
|
||||||
|
const r=Store.find(ln,id); if(!r) return;
|
||||||
|
if(r.lat!=null) this._map.setView([r.lat,r.lng],17);
|
||||||
|
else if(r.coords?.length) try{this._map.fitBounds(L.latLngBounds(r.coords),{padding:[40,40]})}catch{}
|
||||||
|
highlightCard(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Draw.init(this._map);
|
||||||
|
|
||||||
|
// Nav
|
||||||
|
document.getElementById('nav-pills').addEventListener('click',e=>{
|
||||||
|
const pill=e.target.closest('.nav-pill'); if(!pill) return;
|
||||||
|
Draw.cancel();
|
||||||
|
this.state.activeLayer=pill.dataset.layer;
|
||||||
|
document.querySelectorAll('.nav-pill').forEach(p=>p.classList.remove('active'));
|
||||||
|
pill.classList.add('active');
|
||||||
|
renderSidebar(this.state.activeLayer);
|
||||||
|
renderLegend(this.state.activeLayer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add btn
|
||||||
|
document.getElementById('btn-add').addEventListener('click',()=>Draw.start(this.state.activeLayer));
|
||||||
|
|
||||||
|
// Modal
|
||||||
|
document.getElementById('modal-save').addEventListener('click', Modal.submit);
|
||||||
|
document.getElementById('modal-cancel').addEventListener('click', Modal.close);
|
||||||
|
document.getElementById('modal-close').addEventListener('click', Modal.close);
|
||||||
|
document.getElementById('modal-ov').addEventListener('click',e=>{ if(e.target===e.currentTarget) Modal.close(); });
|
||||||
|
|
||||||
|
await Store.load();
|
||||||
|
this.refresh();
|
||||||
|
},
|
||||||
|
|
||||||
|
crud:{
|
||||||
|
edit(ln,id){ App.state.activeLayer=ln; Modal.openEdit(ln,id); },
|
||||||
|
async delete(ln,id){
|
||||||
|
if(!confirm('Yakin hapus data ini?')) return;
|
||||||
|
await Store.remove(ln,id); App._map.closePopup();
|
||||||
|
App.refresh(); Toast.info('Data dihapus');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh(){
|
||||||
|
renderMap(); renderSidebar(this.state.activeLayer);
|
||||||
|
renderLegend(this.state.activeLayer); updateBadges();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded',()=>App.init());
|
||||||
@@ -42,11 +42,11 @@ const Modal = (() => {
|
|||||||
|
|
||||||
function close(){ ov().classList.add('hidden'); Draw.clearPending(); }
|
function close(){ ov().classList.add('hidden'); Draw.clearPending(); }
|
||||||
|
|
||||||
function submit(){
|
async function submit(){
|
||||||
const data=Forms.readAll(_ln); if(!data) return;
|
const data=Forms.readAll(_ln); if(!data) return;
|
||||||
if(_editId){
|
if(_editId){
|
||||||
data.updatedAt=H.today();
|
data.updatedAt=H.today();
|
||||||
Store.update(_ln,_editId,data);
|
await Store.update(_ln,_editId,data);
|
||||||
Toast.success('Data berhasil diperbarui');
|
Toast.success('Data berhasil diperbarui');
|
||||||
} else {
|
} else {
|
||||||
const geo=_geo;
|
const geo=_geo;
|
||||||
@@ -58,7 +58,7 @@ const Modal = (() => {
|
|||||||
data.coords=geo.getLatLngs()[0].map(l=>[l.lat,l.lng]);
|
data.coords=geo.getLatLngs()[0].map(l=>[l.lat,l.lng]);
|
||||||
}
|
}
|
||||||
data.id=H.uid(); data.createdAt=H.today(); data.updatedAt=H.today();
|
data.id=H.uid(); data.createdAt=H.today(); data.updatedAt=H.today();
|
||||||
Store.add(_ln,data);
|
await Store.add(_ln,data);
|
||||||
Toast.success('Data berhasil disimpan');
|
Toast.success('Data berhasil disimpan');
|
||||||
}
|
}
|
||||||
close(); App.refresh();
|
close(); App.refresh();
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
2. STORAGE (MySQL API Version)
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Store = (() => {
|
||||||
|
const API_URL = '../api/index.php';
|
||||||
|
|
||||||
|
let db = {
|
||||||
|
spbu: [],
|
||||||
|
jalan: [],
|
||||||
|
parsil: [],
|
||||||
|
warga: [],
|
||||||
|
rumahIbadah: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = async (action, payload = {}) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_URL}?action=${action}`, {
|
||||||
|
method: action === 'alldb' ? 'GET' : 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: action === 'alldb' ? null : JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
return await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('API Error:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
load: async () => {
|
||||||
|
try {
|
||||||
|
const data = await request('alldb');
|
||||||
|
if (data) db = { ...db, ...data };
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Gagal load data dari server", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
all: ln => db[ln]||[],
|
||||||
|
find: (ln,id) => (db[ln]||[]).find(r=>r.id===id)||null,
|
||||||
|
add: async (ln,rec) => {
|
||||||
|
db[ln].push(rec);
|
||||||
|
await request('add', { layer: ln, data: rec });
|
||||||
|
},
|
||||||
|
update: async (ln,id,patch) => {
|
||||||
|
const i=db[ln].findIndex(r=>r.id===id); if(i<0)return;
|
||||||
|
db[ln][i]={...db[ln][i],...patch};
|
||||||
|
await request('update', { layer: ln, id: id, patch: patch });
|
||||||
|
},
|
||||||
|
remove: async (ln,id) => {
|
||||||
|
db[ln]=db[ln].filter(r=>r.id!==id);
|
||||||
|
await request('remove', { layer: ln, id: id });
|
||||||
|
},
|
||||||
|
allDb: () => db,
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# WebGIS – Sistem Distribusi Bansos Berbasis Rumah Ibadah
|
||||||
|
|
||||||
|
> Aplikasi pemetaan geospasial (*WebGIS*) interaktif untuk pendataan infrastruktur, profil warga rentan, dan perencanaan distribusi Bantuan Sosial (Bansos) berbasis jangkauan Rumah Ibadah.
|
||||||
|
|
||||||
|
Aplikasi ini menggunakan pendekatan **radius spasial 300 meter** untuk secara otomatis mendeteksi apakah seorang warga masuk dalam jangkauan agen distribusi (Rumah Ibadah) atau tidak — membantu pemerintah desa/kelurahan dalam pengambilan keputusan yang akurat dan berbasis data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 Fitur Utama
|
||||||
|
|
||||||
|
| No | Fitur | Tipe Layer | Deskripsi |
|
||||||
|
|----|-------|-----------|-----------|
|
||||||
|
| 1 | **Warga & Bansos** | Point | Profil lengkap KK (ekonomi, kesehatan, pendidikan) + riwayat bansos |
|
||||||
|
| 2 | **Rumah Ibadah** | Point + Buffer | Titik *hub* distribusi bansos; lingkaran jangkauan 300m otomatis |
|
||||||
|
| 3 | **SPBU** | Point | Ketersediaan BBM & aksesibilitas |
|
||||||
|
| 4 | **Jalan** | Polyline | Status, kondisi, dan dampak sosial ruas jalan |
|
||||||
|
| 5 | **Parsil Tanah** | Polygon | Status kepemilikan sertifikat tanah |
|
||||||
|
| 6 | **Dashboard Analitik** | — | Ringkasan kemiskinan, distribusi bansos, dan rekomendasi kebijakan |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂️ Data yang Dikumpulkan
|
||||||
|
|
||||||
|
### 👨👩👧 Warga & Bansos
|
||||||
|
Data yang dikumpulkan mencakup 5 dimensi:
|
||||||
|
- **Data KK:** Nama, NIK, No. KK, jumlah anggota, alamat, tingkat kemiskinan
|
||||||
|
- **Ekonomi:** Pendapatan, pekerjaan, kondisi rumah, aset, air bersih, listrik
|
||||||
|
- **Pendidikan:** Tingkat pendidikan, jumlah anak sekolah/putus sekolah, penerima KIP
|
||||||
|
- **Kesehatan:** Kepesertaan BPJS, jenis penyakit kronis, anggota difabel
|
||||||
|
- **Riwayat Bansos:** Program (PKH, BPNT, BLT, dll), tahun, dan status penerimaan
|
||||||
|
|
||||||
|
### 🕌 Rumah Ibadah
|
||||||
|
Nama, jenis (Masjid/Gereja/Pura/dll), pengurus, kapasitas jamaah, radius binaan, program sosial aktif, dan alokasi dana sosial per bulan.
|
||||||
|
|
||||||
|
### ⛽ SPBU
|
||||||
|
Nama, jam operasional (24 jam / tidak), jenis BBM, harga, dan aksesibilitas disabilitas.
|
||||||
|
|
||||||
|
### 🛣️ Jalan
|
||||||
|
Nama ruas, status (Nasional/Provinsi/Kabupaten/Desa), kondisi, lebar, tahun perbaikan, dan dampak sosial.
|
||||||
|
|
||||||
|
### 🏗️ Parsil Tanah
|
||||||
|
Nama, NIB, status sertifikat (SHM/HGB/HGU/dll), NJOP, penggunaan lahan, dan status sengketa.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Arsitektur & Struktur Folder
|
||||||
|
|
||||||
|
Aplikasi ini dibangun menggunakan arsitektur **modular Vanilla JavaScript** tanpa *build tool* atau *framework*, sehingga bisa langsung dijalankan di browser.
|
||||||
|
|
||||||
|
```text
|
||||||
|
webgis/
|
||||||
|
├── index.html # Halaman utama & markup HTML
|
||||||
|
├── README.md # Dokumentasi proyek ini
|
||||||
|
├── css/
|
||||||
|
│ └── style.css # Seluruh styling & tema UI
|
||||||
|
└── js/
|
||||||
|
├── app.js # Root: Inisialisasi & event binding utama
|
||||||
|
├── config/
|
||||||
|
│ └── config.js # CFG: Kontrak data (warna, label, atribut tiap layer)
|
||||||
|
├── utils/
|
||||||
|
│ ├── helpers.js # H: Utilitas (uid, format angka/jarak/luas, geometri)
|
||||||
|
│ ├── store.js # Store: Baca/tulis LocalStorage
|
||||||
|
│ └── toast.js # Toast: Notifikasi UI (sukses/error/info)
|
||||||
|
├── components/
|
||||||
|
│ ├── forms.js # Forms: Generator HTML form input per layer
|
||||||
|
│ ├── popup.js # Popup: Generator HTML popup Leaflet per layer
|
||||||
|
│ ├── card.js # Card: Generator HTML item daftar di sidebar
|
||||||
|
│ └── legend.js # renderLegend: Fungsi render legenda peta
|
||||||
|
└── modules/
|
||||||
|
├── map.js # renderMap: Render semua marker/layer ke peta
|
||||||
|
├── draw.js # Draw: Integrasi Leaflet Draw (titik, garis, poligon)
|
||||||
|
├── modal.js # Modal: Manajemen pop-up form tambah/edit
|
||||||
|
├── sidebar.js # renderSidebar, updateBadges, highlightCard
|
||||||
|
└── dashboard.js # Dashboard: Kalkulasi analitik & render statistik
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💾 Penyimpanan Data (LocalStorage)
|
||||||
|
|
||||||
|
Semua data tersimpan di *LocalStorage* browser Anda menggunakan key berikut:
|
||||||
|
|
||||||
|
| Layer | LocalStorage Key |
|
||||||
|
|-------|-----------------|
|
||||||
|
| SPBU | `wg2_spbu` |
|
||||||
|
| Jalan | `wg2_jalan` |
|
||||||
|
| Parsil Tanah | `wg2_parsil` |
|
||||||
|
| Warga & Bansos | `wg2_warga` |
|
||||||
|
| Rumah Ibadah | `wg2_ri` |
|
||||||
|
|
||||||
|
> **Catatan Penting:** Data tersimpan di browser lokal Anda. Untuk memindahkan data ke perangkat lain, gunakan fitur *Export/Import* browser atau salin nilai dari DevTools > Application > LocalStorage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Cara Menjalankan
|
||||||
|
|
||||||
|
Tidak perlu instalasi apapun! Cukup:
|
||||||
|
|
||||||
|
1. Unduh atau *clone* repositori ini:
|
||||||
|
```bash
|
||||||
|
git clone <url-repo>
|
||||||
|
```
|
||||||
|
2. Buka folder `webgis/`.
|
||||||
|
3. Klik dua kali pada **`index.html`** untuk membukanya langsung di browser.
|
||||||
|
4. Aplikasi siap digunakan. ✅
|
||||||
|
|
||||||
|
> Direkomendasikan menggunakan **Google Chrome** atau **Mozilla Firefox** versi terbaru.
|
||||||
|
> Anda juga bisa menggunakan ekstensi **Live Server** di VSCode untuk pengalaman pengembangan yang lebih baik.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗺️ Panduan Penggunaan
|
||||||
|
|
||||||
|
### Menambah Data Baru
|
||||||
|
1. Pilih layer yang ingin diisi melalui **navigasi tab** di bagian atas (SPBU, Jalan, Parsil, Warga, Rumah Ibadah).
|
||||||
|
2. Klik tombol **`+ Tambah`** di sidebar kiri.
|
||||||
|
3. **Klik pada peta** untuk menandai lokasi (untuk Point), atau **gambar garis/poligon** (untuk Jalan/Parsil).
|
||||||
|
4. Isi formulir atribut yang muncul, lalu klik **Simpan Data**.
|
||||||
|
|
||||||
|
### Mengedit atau Menghapus Data
|
||||||
|
- Klik marker/layer di peta → Popup muncul → Klik **Edit** atau **Hapus**.
|
||||||
|
- Atau klik kartu di sidebar → gunakan tombol **Edit** / **Hapus** / **Zoom**.
|
||||||
|
|
||||||
|
### Melihat Dashboard Analitik
|
||||||
|
- Klik tombol **📊 Dashboard Analitik** di topbar kanan.
|
||||||
|
- Tab **Ringkasan Utama**: Statistik jumlah KK, distribusi kemiskinan, status jalan.
|
||||||
|
- Tab **Distribusi Bansos**: Peta panas jangkauan Rumah Ibadah, beban distribusi per *hub*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Teknologi yang Digunakan
|
||||||
|
|
||||||
|
| Teknologi | Versi | Fungsi |
|
||||||
|
|-----------|-------|--------|
|
||||||
|
| HTML5 & CSS3 | — | Struktur & tampilan UI |
|
||||||
|
| Vanilla JavaScript | ES6+ | Logika aplikasi & state management |
|
||||||
|
| [Leaflet.js](https://leafletjs.com/) | 1.9.4 | Render peta interaktif |
|
||||||
|
| [Leaflet Draw](https://github.com/Leaflet/Leaflet.draw) | 1.0.4 | Digitasi titik, garis, dan poligon |
|
||||||
|
| [DM Sans & DM Mono](https://fonts.google.com/) | — | Tipografi UI |
|
||||||
|
| LocalStorage API | — | Basis data *client-side* |
|
||||||
|
| OpenStreetMap | — | Tile peta dasar |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Panduan Modifikasi Kode
|
||||||
|
|
||||||
|
| Ingin mengubah... | Buka file... |
|
||||||
|
|-------------------|--------------|
|
||||||
|
| Warna marker atau label layer | [`js/config/config.js`](js/config/config.js) |
|
||||||
|
| Isian form input | [`js/components/forms.js`](js/components/forms.js) |
|
||||||
|
| Tampilan popup peta | [`js/components/popup.js`](js/components/popup.js) |
|
||||||
|
| Tampilan kartu di sidebar | [`js/components/card.js`](js/components/card.js) |
|
||||||
|
| Logika & tampilan dashboard | [`js/modules/dashboard.js`](js/modules/dashboard.js) |
|
||||||
|
| Cara layer dirender di peta | [`js/modules/map.js`](js/modules/map.js) |
|
||||||
|
| Tema / warna UI global | [`css/style.css`](css/style.css) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Lisensi
|
||||||
|
|
||||||
|
Proyek ini dibuat untuk keperluan akademik mata kuliah **Sistem Informasi Geografis (SIG)**. Bebas digunakan dan dimodifikasi untuk keperluan non-komersial.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dikembangkan dengan ❤️ untuk mendukung pemerataan distribusi Bantuan Sosial berbasis data geospasial.*
|
||||||
Submodule
+1
Submodule pertemuan_2/UAS_SIG added at 30bf02df8b
@@ -0,0 +1,420 @@
|
|||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
DESIGN TOKENS
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
:root{
|
||||||
|
--bg0:#07090f;--bg1:#0d1120;--bg2:#131929;--bg3:#1a2235;--bg4:#212d42;--bg5:#2a3a52;
|
||||||
|
--border0:#1a2438;--border1:#243045;--border2:#2e3e58;--border3:#3d5070;
|
||||||
|
--text0:#f0f4ff;--text1:#c8d4ea;--text2:#8fa3c0;--text3:#5a7292;--text4:#3a5070;
|
||||||
|
--accent:#5b9cf6;--accent2:#3a7de4;--accent-glow:rgba(91,156,246,.18);
|
||||||
|
--green:#34d47e;--green2:#22aa60;--green-s:rgba(52,212,126,.14);
|
||||||
|
--red:#f05c5c;--red2:#c83e3e;--red-s:rgba(240,92,92,.14);
|
||||||
|
--amber:#f0a030;--amber2:#c47c18;--amber-s:rgba(240,160,48,.14);
|
||||||
|
--purple:#9b7bfa;--purple2:#7a5ae0;--purple-s:rgba(155,123,250,.14);
|
||||||
|
--teal:#2ec4b6;--teal2:#1ea090;--teal-s:rgba(46,196,182,.14);
|
||||||
|
--orange:#f07840;--orange2:#c45820;--orange-s:rgba(240,120,64,.14);
|
||||||
|
--pink:#f06090;--pink-s:rgba(240,96,144,.14);
|
||||||
|
--font:'DM Sans',system-ui,sans-serif;
|
||||||
|
--mono:'DM Mono',monospace;
|
||||||
|
--r4:4px;--r6:6px;--r8:8px;--r10:10px;--r12:12px;--r16:16px;--rfull:9999px;
|
||||||
|
--shadow-sm:0 2px 8px rgba(0,0,0,.35);
|
||||||
|
--shadow-md:0 4px 20px rgba(0,0,0,.45);
|
||||||
|
--shadow-lg:0 8px 40px rgba(0,0,0,.6);
|
||||||
|
--transition:.15s ease;
|
||||||
|
}
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
html,body{height:100%;overflow:hidden;font-family:var(--font)}
|
||||||
|
body{background:var(--bg0);color:var(--text0);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}
|
||||||
|
button{font-family:var(--font);cursor:pointer;border:none;outline:none;background:none}
|
||||||
|
input,select,textarea{font-family:var(--font);outline:none}
|
||||||
|
::-webkit-scrollbar{width:4px;height:4px}
|
||||||
|
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:var(--rfull)}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
LAYOUT
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
.app{display:flex;flex-direction:column;height:100vh}
|
||||||
|
.topbar{height:52px;background:var(--bg1);border-bottom:1px solid var(--border1);
|
||||||
|
display:flex;align-items:center;padding:0 16px;gap:12px;flex-shrink:0;z-index:500}
|
||||||
|
.main{display:flex;flex:1;overflow:hidden}
|
||||||
|
.sidebar{width:320px;background:var(--bg1);border-right:1px solid var(--border0);
|
||||||
|
display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}
|
||||||
|
.map-area{flex:1;position:relative}
|
||||||
|
#map{width:100%;height:100%}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
TOPBAR
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
.brand{display:flex;align-items:center;gap:10px}
|
||||||
|
.brand-mark{width:32px;height:32px;border-radius:var(--r8);
|
||||||
|
background:linear-gradient(135deg,var(--accent),var(--purple));
|
||||||
|
display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||||
|
.brand-mark svg{width:17px;height:17px;fill:#fff}
|
||||||
|
.brand-name{font-size:14px;font-weight:700;color:var(--text0);letter-spacing:-.3px;line-height:1.1}
|
||||||
|
.brand-name span{display:block;font-size:10px;font-weight:400;color:var(--text3);letter-spacing:.2px}
|
||||||
|
.topbar-div{width:1px;height:26px;background:var(--border1)}
|
||||||
|
.nav-pills{display:flex;gap:3px}
|
||||||
|
.nav-pill{display:flex;align-items:center;gap:6px;padding:5px 12px;border-radius:var(--r6);
|
||||||
|
font-size:12px;font-weight:500;color:var(--text2);transition:background var(--transition),color var(--transition)}
|
||||||
|
.nav-pill:hover{background:var(--bg3);color:var(--text0)}
|
||||||
|
.nav-pill.active{background:var(--accent);color:#fff}
|
||||||
|
.nav-pill.active .pill-badge{background:rgba(255,255,255,.25);color:#fff}
|
||||||
|
.pill-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
|
||||||
|
.pill-badge{min-width:17px;height:17px;padding:0 4px;border-radius:var(--rfull);
|
||||||
|
background:var(--bg4);color:var(--text3);font-size:10px;font-weight:700;
|
||||||
|
display:inline-flex;align-items:center;justify-content:center}
|
||||||
|
.topbar-spacer{flex:1}
|
||||||
|
/* Dashboard toggle */
|
||||||
|
.dash-btn{display:flex;align-items:center;gap:6px;padding:6px 14px;border-radius:var(--r6);
|
||||||
|
font-size:12px;font-weight:600;background:var(--purple-s);color:var(--purple);
|
||||||
|
border:1px solid rgba(155,123,250,.25);transition:background var(--transition)}
|
||||||
|
.dash-btn:hover{background:rgba(155,123,250,.25)}
|
||||||
|
.topbar-stat{font-size:11px;color:var(--text3);display:flex;align-items:center;gap:5px}
|
||||||
|
.stat-dot{width:6px;height:6px;border-radius:50%;background:var(--green)}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
SIDEBAR
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
.sidebar-head{padding:12px 14px;border-bottom:1px solid var(--border0);
|
||||||
|
display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
|
||||||
|
.sidebar-title{font-size:13px;font-weight:700;color:var(--text0)}
|
||||||
|
.sidebar-sub{font-size:10px;color:var(--text3);margin-top:1px}
|
||||||
|
.btn-add{display:flex;align-items:center;gap:4px;padding:5px 10px;border-radius:var(--r6);
|
||||||
|
font-size:11px;font-weight:700;background:var(--accent-glow);color:var(--accent);
|
||||||
|
border:1px solid rgba(91,156,246,.25);transition:background var(--transition)}
|
||||||
|
.btn-add:hover{background:rgba(91,156,246,.28)}
|
||||||
|
.sidebar-body{flex:1;overflow-y:auto;padding:10px}
|
||||||
|
|
||||||
|
/* Data Cards */
|
||||||
|
.dcard{background:var(--bg2);border:1px solid var(--border0);border-radius:var(--r10);
|
||||||
|
margin-bottom:8px;overflow:hidden;transition:border-color var(--transition)}
|
||||||
|
.dcard:hover{border-color:var(--border2)}
|
||||||
|
.dcard.active{border-color:var(--accent)}
|
||||||
|
.dcard-head{padding:9px 11px;display:flex;align-items:center;gap:8px;cursor:pointer}
|
||||||
|
.dcard-head:hover{background:var(--bg3)}
|
||||||
|
.dcard-sym{width:9px;height:9px;flex-shrink:0}
|
||||||
|
.dcard-sym.circle{border-radius:50%}
|
||||||
|
.dcard-sym.line{height:4px;border-radius:2px;width:20px}
|
||||||
|
.dcard-sym.poly{border-radius:2px;width:13px;border:2px solid currentColor;background:transparent}
|
||||||
|
.dcard-name{font-size:12px;font-weight:600;color:var(--text0);flex:1;
|
||||||
|
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
.dcard-attrs{padding:4px 11px 8px;border-top:1px solid var(--border0)}
|
||||||
|
.attr{display:flex;justify-content:space-between;padding:3px 0;
|
||||||
|
border-bottom:1px solid var(--border0);font-size:11px}
|
||||||
|
.attr:last-child{border-bottom:none}
|
||||||
|
.attr-k{color:var(--text3)}
|
||||||
|
.attr-v{color:var(--text2);font-weight:500;text-align:right;max-width:60%}
|
||||||
|
.dcard-acts{display:flex;gap:5px;padding:7px 11px;border-top:1px solid var(--border0);background:var(--bg1)}
|
||||||
|
|
||||||
|
/* Badges */
|
||||||
|
.badge{display:inline-flex;align-items:center;padding:1px 7px;border-radius:var(--rfull);font-size:10px;font-weight:700}
|
||||||
|
.badge-green {background:var(--green-s);color:var(--green)}
|
||||||
|
.badge-red {background:var(--red-s);color:var(--red)}
|
||||||
|
.badge-amber {background:var(--amber-s);color:var(--amber)}
|
||||||
|
.badge-accent {background:var(--accent-glow);color:var(--accent)}
|
||||||
|
.badge-purple {background:var(--purple-s);color:var(--purple)}
|
||||||
|
.badge-teal {background:var(--teal-s);color:var(--teal)}
|
||||||
|
.badge-orange {background:var(--orange-s);color:var(--orange)}
|
||||||
|
.badge-pink {background:var(--pink-s);color:var(--pink)}
|
||||||
|
|
||||||
|
/* Btn small */
|
||||||
|
.bsm{padding:3px 9px;border-radius:var(--r4);font-size:10px;font-weight:700;border:1px solid transparent;transition:background var(--transition)}
|
||||||
|
.bsm-zoom {background:var(--green-s);color:var(--green);border-color:rgba(52,212,126,.2)}
|
||||||
|
.bsm-zoom:hover{background:rgba(52,212,126,.24)}
|
||||||
|
.bsm-edit {background:var(--accent-glow);color:var(--accent);border-color:rgba(91,156,246,.2)}
|
||||||
|
.bsm-edit:hover{background:rgba(91,156,246,.25)}
|
||||||
|
.bsm-del {background:var(--red-s);color:var(--red);border-color:rgba(240,92,92,.2)}
|
||||||
|
.bsm-del:hover{background:rgba(240,92,92,.24)}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.empty{text-align:center;padding:48px 20px;color:var(--text4)}
|
||||||
|
.empty svg{margin:0 auto 12px;display:block;opacity:.2}
|
||||||
|
.empty p{font-size:12px;line-height:1.7}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
MAP OVERLAYS
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
.map-legend{position:absolute;bottom:20px;left:12px;z-index:800;
|
||||||
|
background:var(--bg1);border:1px solid var(--border1);border-radius:var(--r10);
|
||||||
|
padding:10px 13px;min-width:160px;box-shadow:var(--shadow-md)}
|
||||||
|
.legend-title{font-size:9px;font-weight:700;color:var(--text3);
|
||||||
|
text-transform:uppercase;letter-spacing:.8px;margin-bottom:8px}
|
||||||
|
.legend-row{display:flex;align-items:center;gap:8px;margin-bottom:5px;
|
||||||
|
font-size:11px;color:var(--text2)}
|
||||||
|
.legend-row:last-child{margin-bottom:0}
|
||||||
|
.leg-c{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
||||||
|
.leg-l{width:20px;height:4px;border-radius:2px;flex-shrink:0}
|
||||||
|
.leg-p{width:13px;height:11px;border-radius:2px;flex-shrink:0;border:2px solid}
|
||||||
|
|
||||||
|
.draw-bar{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:800;
|
||||||
|
background:var(--bg1);border:1px solid var(--border2);border-radius:var(--rfull);
|
||||||
|
padding:7px 16px;font-size:11px;font-weight:600;color:var(--text0);
|
||||||
|
display:flex;align-items:center;gap:10px;box-shadow:var(--shadow-md);white-space:nowrap}
|
||||||
|
.draw-bar.hidden{display:none}
|
||||||
|
.draw-pulse{width:7px;height:7px;border-radius:50%;background:var(--green);flex-shrink:0;
|
||||||
|
animation:pulse 1.5s ease-in-out infinite}
|
||||||
|
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.75)}}
|
||||||
|
.btn-cancel-draw{background:var(--red-s);color:var(--red);border:1px solid rgba(240,92,92,.2);
|
||||||
|
border-radius:var(--r4);padding:3px 9px;font-size:10px;font-weight:700;transition:background var(--transition)}
|
||||||
|
.btn-cancel-draw:hover{background:rgba(240,92,92,.24)}
|
||||||
|
|
||||||
|
/* Leaflet popup */
|
||||||
|
.leaflet-popup-content-wrapper{background:var(--bg2)!important;border:1px solid var(--border2)!important;
|
||||||
|
border-radius:var(--r10)!important;box-shadow:var(--shadow-lg)!important;color:var(--text0)!important}
|
||||||
|
.leaflet-popup-tip{background:var(--bg2)!important}
|
||||||
|
.leaflet-popup-content{margin:0!important;padding:0!important}
|
||||||
|
.leaflet-popup-close-button{color:var(--text2)!important;font-size:14px!important;top:8px!important;right:10px!important}
|
||||||
|
.popup-wrap{padding:12px 14px;min-width:220px;max-width:300px}
|
||||||
|
.popup-title{font-size:13px;font-weight:700;color:var(--text0);margin-bottom:8px;
|
||||||
|
display:flex;align-items:center;gap:7px}
|
||||||
|
.popup-sym{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
||||||
|
.popup-section{font-size:9px;font-weight:700;color:var(--text4);text-transform:uppercase;
|
||||||
|
letter-spacing:.8px;margin:8px 0 4px;padding-bottom:4px;border-bottom:1px solid var(--border0)}
|
||||||
|
.popup-row{display:flex;justify-content:space-between;gap:10px;padding:3px 0;
|
||||||
|
border-bottom:1px solid var(--border0);font-size:11px}
|
||||||
|
.popup-row:last-of-type{border-bottom:none}
|
||||||
|
.popup-k{color:var(--text3)}
|
||||||
|
.popup-v{color:var(--text1);font-weight:500;text-align:right;max-width:55%}
|
||||||
|
.popup-acts{display:flex;gap:5px;margin-top:9px;padding-top:8px;border-top:1px solid var(--border0)}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
MODAL
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
.modal-ov{position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:9999;
|
||||||
|
display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);
|
||||||
|
animation:fadeIn .15s ease}
|
||||||
|
.modal-ov.hidden{display:none}
|
||||||
|
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
|
||||||
|
.modal{background:var(--bg2);border:1px solid var(--border2);border-radius:var(--r16);
|
||||||
|
width:520px;max-width:calc(100vw - 24px);max-height:88vh;overflow-y:auto;
|
||||||
|
box-shadow:var(--shadow-lg);animation:mIn .18s ease}
|
||||||
|
@keyframes mIn{from{transform:translateY(-10px) scale(.97);opacity:0}to{transform:none;opacity:1}}
|
||||||
|
.modal-head{padding:16px 18px 12px;border-bottom:1px solid var(--border0);
|
||||||
|
display:flex;align-items:center;justify-content:space-between;
|
||||||
|
position:sticky;top:0;background:var(--bg2);z-index:1}
|
||||||
|
.modal-head-left{display:flex;align-items:center;gap:10px}
|
||||||
|
.modal-icon{width:30px;height:30px;border-radius:var(--r8);display:flex;align-items:center;justify-content:center;font-size:15px}
|
||||||
|
.modal-title{font-size:14px;font-weight:700;color:var(--text0)}
|
||||||
|
.modal-sub{font-size:10px;color:var(--text3);margin-top:1px}
|
||||||
|
.modal-close{width:26px;height:26px;border-radius:var(--r6);background:var(--bg3);
|
||||||
|
border:1px solid var(--border1);color:var(--text2);font-size:12px;
|
||||||
|
display:flex;align-items:center;justify-content:center;transition:background var(--transition)}
|
||||||
|
.modal-close:hover{background:var(--bg4);color:var(--text0)}
|
||||||
|
.modal-body{padding:16px 18px}
|
||||||
|
.modal-foot{padding:12px 18px;border-top:1px solid var(--border0);
|
||||||
|
display:flex;gap:8px;justify-content:flex-end}
|
||||||
|
|
||||||
|
/* Form */
|
||||||
|
.form-sect{font-size:9px;font-weight:700;color:var(--text4);text-transform:uppercase;
|
||||||
|
letter-spacing:.8px;margin:14px 0 8px;padding-bottom:6px;border-bottom:1px solid var(--border0)}
|
||||||
|
.form-sect:first-child{margin-top:0}
|
||||||
|
.form-row{display:grid;gap:10px;margin-bottom:10px}
|
||||||
|
.form-row.cols2{grid-template-columns:1fr 1fr}
|
||||||
|
.form-row.cols3{grid-template-columns:1fr 1fr 1fr}
|
||||||
|
.fg{display:flex;flex-direction:column;gap:5px}
|
||||||
|
.flabel{font-size:11px;font-weight:600;color:var(--text2);letter-spacing:.2px}
|
||||||
|
.flabel .req{color:var(--red);margin-left:2px}
|
||||||
|
.finput{padding:8px 10px;background:var(--bg3);border:1px solid var(--border1);
|
||||||
|
border-radius:var(--r6);color:var(--text0);font-size:12px;font-family:var(--font);
|
||||||
|
transition:border-color var(--transition),box-shadow var(--transition);width:100%}
|
||||||
|
.finput:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(91,156,246,.12)}
|
||||||
|
.finput:disabled{opacity:.45;cursor:not-allowed}
|
||||||
|
.finput[readonly]{background:var(--bg4);color:var(--text2);cursor:default}
|
||||||
|
select.finput{cursor:pointer}
|
||||||
|
select.finput option{background:var(--bg3)}
|
||||||
|
textarea.finput{resize:vertical;min-height:64px}
|
||||||
|
.info-box{background:var(--bg4);border:1px solid var(--border1);border-radius:var(--r6);
|
||||||
|
padding:8px 10px;margin-bottom:10px;font-size:11px;color:var(--text2);line-height:1.6}
|
||||||
|
.info-box strong{color:var(--text0)}
|
||||||
|
.info-box.warn{border-color:rgba(240,160,48,.3);background:var(--amber-s)}
|
||||||
|
|
||||||
|
/* Radio toggle */
|
||||||
|
.rtoggle{display:flex;gap:6px}
|
||||||
|
.ropt{flex:1;display:flex;align-items:center;justify-content:center;gap:5px;
|
||||||
|
padding:8px;border-radius:var(--r6);border:1px solid var(--border1);cursor:pointer;
|
||||||
|
font-size:11px;font-weight:600;transition:all var(--transition);color:var(--text3);
|
||||||
|
background:var(--bg3);user-select:none}
|
||||||
|
.ropt input{display:none}
|
||||||
|
.ropt.yes{border-color:rgba(52,212,126,.35);color:var(--green);background:var(--green-s)}
|
||||||
|
.ropt.no {border-color:rgba(240,92,92,.35);color:var(--red);background:var(--red-s)}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;
|
||||||
|
padding:8px 18px;border-radius:var(--r6);font-size:13px;font-weight:600;
|
||||||
|
font-family:var(--font);transition:background var(--transition)}
|
||||||
|
.btn-primary{background:var(--accent);color:#fff}
|
||||||
|
.btn-primary:hover{background:var(--accent2)}
|
||||||
|
.btn-ghost{background:var(--bg3);color:var(--text2);border:1px solid var(--border1)}
|
||||||
|
.btn-ghost:hover{background:var(--bg4);color:var(--text0)}
|
||||||
|
.hidden{display:none!important}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
DASHBOARD PANEL
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
.dash-panel{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:8000;
|
||||||
|
display:flex;align-items:stretch;justify-content:flex-end;backdrop-filter:blur(4px);
|
||||||
|
animation:fadeIn .15s ease}
|
||||||
|
.dash-panel.hidden{display:none}
|
||||||
|
.dash-inner{width:680px;max-width:95vw;background:var(--bg1);border-left:1px solid var(--border1);
|
||||||
|
display:flex;flex-direction:column;overflow:hidden}
|
||||||
|
.dash-head{padding:18px 20px;border-bottom:1px solid var(--border0);
|
||||||
|
display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
|
||||||
|
.dash-head-title{font-size:16px;font-weight:700;color:var(--text0)}
|
||||||
|
.dash-head-sub{font-size:11px;color:var(--text3);margin-top:2px}
|
||||||
|
.dash-body{flex:1;overflow-y:auto;padding:18px 20px}
|
||||||
|
|
||||||
|
/* Stat cards */
|
||||||
|
.stat-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:18px}
|
||||||
|
.stat-card{background:var(--bg2);border:1px solid var(--border0);border-radius:var(--r10);
|
||||||
|
padding:12px 14px;position:relative;overflow:hidden}
|
||||||
|
.stat-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px}
|
||||||
|
.stat-card.c-accent::before{background:var(--accent)}
|
||||||
|
.stat-card.c-green::before{background:var(--green)}
|
||||||
|
.stat-card.c-amber::before{background:var(--amber)}
|
||||||
|
.stat-card.c-purple::before{background:var(--purple)}
|
||||||
|
.stat-card.c-red::before{background:var(--red)}
|
||||||
|
.stat-card.c-teal::before{background:var(--teal)}
|
||||||
|
.sc-val{font-size:26px;font-weight:700;color:var(--text0);font-family:var(--mono);line-height:1}
|
||||||
|
.sc-label{font-size:10px;color:var(--text3);margin-top:4px}
|
||||||
|
.sc-sub{font-size:11px;color:var(--text2);margin-top:6px}
|
||||||
|
|
||||||
|
/* Dash sections */
|
||||||
|
.dash-sect{margin-bottom:20px}
|
||||||
|
.dash-sect-title{font-size:11px;font-weight:700;color:var(--text2);
|
||||||
|
text-transform:uppercase;letter-spacing:.6px;margin-bottom:10px;
|
||||||
|
padding-bottom:6px;border-bottom:1px solid var(--border0)}
|
||||||
|
.risk-table{width:100%;border-collapse:collapse;font-size:11px}
|
||||||
|
.risk-table th{text-align:left;padding:6px 10px;color:var(--text3);font-weight:600;
|
||||||
|
border-bottom:1px solid var(--border0);font-size:10px}
|
||||||
|
.risk-table td{padding:7px 10px;border-bottom:1px solid var(--border0);color:var(--text1)}
|
||||||
|
.risk-table tr:last-child td{border-bottom:none}
|
||||||
|
.risk-table tr:hover td{background:var(--bg2)}
|
||||||
|
.risk-bar-wrap{display:flex;align-items:center;gap:7px}
|
||||||
|
.risk-bar{height:5px;border-radius:3px;background:var(--bg4);flex:1}
|
||||||
|
.risk-bar-fill{height:100%;border-radius:3px;transition:width .3s ease}
|
||||||
|
.prog-row{display:flex;align-items:center;justify-content:space-between;
|
||||||
|
padding:6px 0;border-bottom:1px solid var(--border0);font-size:11px}
|
||||||
|
.prog-row:last-child{border-bottom:none}
|
||||||
|
.prog-name{color:var(--text1)}
|
||||||
|
.prog-count{font-family:var(--mono);font-weight:600;color:var(--text0)}
|
||||||
|
.intervensi-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
||||||
|
.intervensi-card{background:var(--bg2);border:1px solid var(--border0);border-radius:var(--r8);padding:10px 12px}
|
||||||
|
.ic-label{font-size:10px;color:var(--text3);margin-bottom:4px}
|
||||||
|
.ic-val{font-size:13px;font-weight:700;color:var(--text0)}
|
||||||
|
.ic-sub{font-size:10px;color:var(--text2);margin-top:3px}
|
||||||
|
|
||||||
|
/* Toast */
|
||||||
|
.toasts{position:fixed;bottom:22px;right:22px;z-index:10000;display:flex;flex-direction:column;gap:7px;pointer-events:none}
|
||||||
|
.toast{background:var(--bg2);border:1px solid var(--border2);border-radius:var(--r10);
|
||||||
|
padding:10px 14px;font-size:12px;color:var(--text0);box-shadow:var(--shadow-lg);
|
||||||
|
display:flex;align-items:center;gap:9px;min-width:240px;pointer-events:all;
|
||||||
|
animation:tIn .2s ease}
|
||||||
|
.toast.success{border-left:3px solid var(--green)}
|
||||||
|
.toast.error{border-left:3px solid var(--red)}
|
||||||
|
.toast.info{border-left:3px solid var(--accent)}
|
||||||
|
@keyframes tIn{from{transform:translateX(110%);opacity:0}to{transform:none;opacity:1}}
|
||||||
|
@keyframes tOut{from{transform:none;opacity:1}to{transform:translateX(110%);opacity:0}}
|
||||||
|
.toast.out{animation:tOut .2s ease forwards}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
BANSOS DISTRIBUTION SYSTEM
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* History table (multi-entry) inside modal */
|
||||||
|
.history-table{width:100%;border-collapse:collapse;margin-top:6px;font-size:11px}
|
||||||
|
.history-table th{text-align:left;padding:5px 8px;color:var(--text3);font-weight:600;
|
||||||
|
font-size:10px;border-bottom:1px solid var(--border1);background:var(--bg3)}
|
||||||
|
.history-table td{padding:6px 8px;border-bottom:1px solid var(--border0);color:var(--text1)}
|
||||||
|
.history-table tr:last-child td{border-bottom:none}
|
||||||
|
.history-table tr:hover td{background:var(--bg3)}
|
||||||
|
.history-table .status-aktif{color:var(--green);font-weight:700}
|
||||||
|
.history-table .status-berhenti{color:var(--red);font-weight:700}
|
||||||
|
.history-table .status-proses{color:var(--amber);font-weight:700}
|
||||||
|
|
||||||
|
/* Dynamic add-row button */
|
||||||
|
.btn-add-row{display:inline-flex;align-items:center;gap:4px;margin-top:6px;
|
||||||
|
padding:4px 10px;border-radius:var(--r4);font-size:10px;font-weight:700;
|
||||||
|
background:var(--green-s);color:var(--green);border:1px solid rgba(52,212,126,.25);
|
||||||
|
cursor:pointer;transition:background var(--transition)}
|
||||||
|
.btn-add-row:hover{background:rgba(52,212,126,.24)}
|
||||||
|
.btn-del-row{padding:2px 7px;border-radius:var(--r4);font-size:10px;font-weight:700;
|
||||||
|
background:var(--red-s);color:var(--red);border:1px solid rgba(240,92,92,.2);
|
||||||
|
cursor:pointer;transition:background var(--transition)}
|
||||||
|
.btn-del-row:hover{background:rgba(240,92,92,.24)}
|
||||||
|
|
||||||
|
/* Radius coverage indicator in popup */
|
||||||
|
.coverage-badge{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;
|
||||||
|
border-radius:var(--rfull);font-size:10px;font-weight:700;margin-top:4px}
|
||||||
|
.coverage-badge.covered{background:var(--green-s);color:var(--green);border:1px solid rgba(52,212,126,.25)}
|
||||||
|
.coverage-badge.uncovered{background:var(--red-s);color:var(--red);border:1px solid rgba(240,92,92,.25)}
|
||||||
|
.coverage-badge svg{width:10px;height:10px;fill:currentColor;flex-shrink:0}
|
||||||
|
|
||||||
|
/* Radius circle legend */
|
||||||
|
.legend-radius{display:flex;align-items:center;gap:7px;margin-top:6px;
|
||||||
|
padding-top:6px;border-top:1px solid var(--border0);font-size:10px;color:var(--text3)}
|
||||||
|
.leg-radius-sym{width:14px;height:14px;border-radius:50%;border:2px dashed var(--teal);
|
||||||
|
background:rgba(46,196,182,.08);flex-shrink:0}
|
||||||
|
|
||||||
|
/* Dashboard distribution tab */
|
||||||
|
.dash-tab-bar{display:flex;gap:3px;margin-bottom:14px;background:var(--bg2);
|
||||||
|
border:1px solid var(--border0);border-radius:var(--r8);padding:3px}
|
||||||
|
.dash-tab{flex:1;padding:6px 12px;border-radius:var(--r6);font-size:11px;font-weight:600;
|
||||||
|
color:var(--text3);cursor:pointer;transition:all var(--transition);text-align:center}
|
||||||
|
.dash-tab.active{background:var(--accent);color:#fff}
|
||||||
|
.dash-tab:not(.active):hover{background:var(--bg3);color:var(--text1)}
|
||||||
|
|
||||||
|
/* Coverage summary cards */
|
||||||
|
.coverage-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px}
|
||||||
|
.coverage-card{background:var(--bg2);border:1px solid var(--border0);border-radius:var(--r10);
|
||||||
|
padding:12px 14px;display:flex;align-items:center;gap:12px}
|
||||||
|
.cc-icon{width:34px;height:34px;border-radius:var(--r8);display:flex;align-items:center;
|
||||||
|
justify-content:center;font-size:16px;flex-shrink:0}
|
||||||
|
.cc-icon.green{background:var(--green-s)}
|
||||||
|
.cc-icon.red{background:var(--red-s)}
|
||||||
|
.cc-icon.amber{background:var(--amber-s)}
|
||||||
|
.cc-icon.teal{background:var(--teal-s)}
|
||||||
|
.cc-info{min-width:0}
|
||||||
|
.cc-val{font-size:20px;font-weight:700;color:var(--text0);font-family:var(--mono)}
|
||||||
|
.cc-label{font-size:10px;color:var(--text3);margin-top:1px}
|
||||||
|
|
||||||
|
/* Hub assignment table */
|
||||||
|
.hub-table{width:100%;border-collapse:collapse;font-size:11px}
|
||||||
|
.hub-table th{text-align:left;padding:7px 10px;color:var(--text3);font-weight:600;
|
||||||
|
font-size:10px;border-bottom:1px solid var(--border1);background:var(--bg2);
|
||||||
|
position:sticky;top:0}
|
||||||
|
.hub-table td{padding:7px 10px;border-bottom:1px solid var(--border0);color:var(--text1)}
|
||||||
|
.hub-table tr:last-child td{border-bottom:none}
|
||||||
|
.hub-table tr:hover td{background:var(--bg2)}
|
||||||
|
.hub-name-cell{display:flex;align-items:center;gap:7px}
|
||||||
|
.hub-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
||||||
|
.hub-count-bar{display:flex;align-items:center;gap:7px}
|
||||||
|
.hub-bar{height:5px;border-radius:3px;background:var(--bg4);flex:1;min-width:60px}
|
||||||
|
.hub-bar-fill{height:100%;border-radius:3px;transition:width .4s ease}
|
||||||
|
|
||||||
|
/* Multi-section form tab inside modal */
|
||||||
|
.form-tabs{display:flex;gap:2px;background:var(--bg3);border-radius:var(--r6);
|
||||||
|
padding:3px;margin-bottom:14px}
|
||||||
|
.form-tab{flex:1;padding:5px 8px;border-radius:var(--r4);font-size:10px;font-weight:700;
|
||||||
|
color:var(--text3);cursor:pointer;transition:all var(--transition);text-align:center}
|
||||||
|
.form-tab.active{background:var(--accent);color:#fff}
|
||||||
|
.form-tab:not(.active):hover{background:var(--bg4);color:var(--text1)}
|
||||||
|
.form-section-panel{display:none}
|
||||||
|
.form-section-panel.active{display:block}
|
||||||
|
|
||||||
|
/* Pendidikan tag badges */
|
||||||
|
.pendidikan-grid{display:flex;flex-wrap:wrap;gap:5px;margin-top:4px}
|
||||||
|
.pddk-tag{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;
|
||||||
|
border-radius:var(--rfull);font-size:10px;font-weight:600;cursor:pointer;
|
||||||
|
border:1px solid var(--border1);background:var(--bg3);color:var(--text3);
|
||||||
|
transition:all var(--transition);user-select:none}
|
||||||
|
.pddk-tag.selected{background:var(--accent-glow);border-color:rgba(91,156,246,.4);color:var(--accent)}
|
||||||
|
|
||||||
|
/* Penyakit checklist */
|
||||||
|
.penyakit-grid{display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-top:4px}
|
||||||
|
.penyakit-check{display:flex;align-items:center;gap:6px;padding:5px 8px;
|
||||||
|
border-radius:var(--r6);border:1px solid var(--border0);background:var(--bg3);
|
||||||
|
cursor:pointer;font-size:11px;color:var(--text2);transition:all var(--transition);user-select:none}
|
||||||
|
.penyakit-check input{accent-color:var(--red);width:12px;height:12px}
|
||||||
|
.penyakit-check.checked{border-color:rgba(240,92,92,.35);background:var(--red-s);color:var(--red)}
|
||||||
|
|
||||||
|
/* Badge pink (already in root vars) */
|
||||||
|
.badge-pink{background:var(--pink-s);color:var(--pink)}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>WebGIS – Sistem Distribusi Bansos Berbasis Rumah Ibadah</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"/>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
|
||||||
|
<!-- ═══ TOPBAR ═══ -->
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="brand">
|
||||||
|
<div class="brand-mark">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="brand-name">WebGIS Distribusi Bansos<span>Berbasis Rumah Ibadah — Radius 300m</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="topbar-div"></div>
|
||||||
|
<nav class="nav-pills" id="nav-pills">
|
||||||
|
<button class="nav-pill active" data-layer="spbu">
|
||||||
|
<span class="pill-dot" style="background:#34d47e"></span>SPBU
|
||||||
|
<span class="pill-badge" id="badge-spbu">0</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-pill" data-layer="jalan">
|
||||||
|
<span class="pill-dot" style="background:#f0a030"></span>Jalan
|
||||||
|
<span class="pill-badge" id="badge-jalan">0</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-pill" data-layer="parsil">
|
||||||
|
<span class="pill-dot" style="background:#9b7bfa"></span>Parsil
|
||||||
|
<span class="pill-badge" id="badge-parsil">0</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="topbar-stat"><span class="stat-dot"></span><span id="total-count">0 fitur</span></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- ═══ MAIN ═══ -->
|
||||||
|
<div class="main">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-head">
|
||||||
|
<div>
|
||||||
|
<div class="sidebar-title" id="sb-title">Data SPBU</div>
|
||||||
|
<div class="sidebar-sub" id="sb-sub">Stasiun Bahan Bakar Umum</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-add" id="btn-add">
|
||||||
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>Tambah
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-body" id="sb-list"></div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Map -->
|
||||||
|
<div class="map-area">
|
||||||
|
<div id="map"></div>
|
||||||
|
<!-- Draw indicator -->
|
||||||
|
<div class="draw-bar hidden" id="draw-bar">
|
||||||
|
<span class="draw-pulse"></span>
|
||||||
|
<span id="draw-text">Mode gambar aktif</span>
|
||||||
|
<button class="btn-cancel-draw" id="btn-cancel">Batal</button>
|
||||||
|
</div>
|
||||||
|
<!-- Legend -->
|
||||||
|
<div class="map-legend" id="legend">
|
||||||
|
<div class="legend-title">Legenda</div>
|
||||||
|
<div id="legend-items"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ MODAL ═══ -->
|
||||||
|
<div class="modal-ov hidden" id="modal-ov">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-head">
|
||||||
|
<div class="modal-head-left">
|
||||||
|
<div class="modal-icon" id="modal-icon">📍</div>
|
||||||
|
<div>
|
||||||
|
<div class="modal-title" id="modal-title">Form Data</div>
|
||||||
|
<div class="modal-sub" id="modal-sub"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" id="modal-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="modal-body"></div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<button class="btn btn-ghost" id="modal-cancel">Batal</button>
|
||||||
|
<button class="btn btn-primary" id="modal-save">Simpan Data</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ DASHBOARD ═══ -->
|
||||||
|
<div class="dash-panel hidden" id="dash-panel">
|
||||||
|
<div class="dash-inner">
|
||||||
|
<div class="dash-head">
|
||||||
|
<div>
|
||||||
|
<div class="dash-head-title">Dashboard Analitik Kemiskinan</div>
|
||||||
|
<div class="dash-head-sub">Ringkasan & analisis data geospasial untuk pengambilan kebijakan</div>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" onclick="Dashboard.toggle()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="dash-body" id="dash-body"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ TOASTS ═══ -->
|
||||||
|
<div class="toasts" id="toasts"></div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
||||||
|
<script src="js/utils/helpers.js"></script>
|
||||||
|
<script src="js/utils/store.js"></script>
|
||||||
|
<script src="js/utils/toast.js"></script>
|
||||||
|
<script src="js/config/config.js"></script>
|
||||||
|
<script src="js/components/forms.js"></script>
|
||||||
|
<script src="js/components/popup.js"></script>
|
||||||
|
<script src="js/components/card.js"></script>
|
||||||
|
<script src="js/components/legend.js"></script>
|
||||||
|
<script src="js/modules/draw.js"></script>
|
||||||
|
<script src="js/modules/modal.js"></script>
|
||||||
|
<script src="js/modules/sidebar.js"></script>
|
||||||
|
<script src="js/modules/dashboard.js"></script>
|
||||||
|
<script src="js/modules/map.js"></script>
|
||||||
|
<script src="js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
14. APP ROOT
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const App = {
|
||||||
|
state:{ activeLayer:'spbu' },
|
||||||
|
_groups:{},
|
||||||
|
|
||||||
|
async init(){
|
||||||
|
// Map
|
||||||
|
this._map=L.map('map',{zoomControl:false}).setView([-0.056031,109.348641],13);
|
||||||
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{
|
||||||
|
attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',maxZoom:19
|
||||||
|
}).addTo(this._map);
|
||||||
|
L.control.zoom({position:'topright'}).addTo(this._map);
|
||||||
|
['spbu','jalan','parsil'].forEach(k=>{
|
||||||
|
this._groups[k]=L.layerGroup().addTo(this._map);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expose map for Draw
|
||||||
|
App.map={
|
||||||
|
instance: this._map,
|
||||||
|
zoomTo:(ln,id)=>{
|
||||||
|
const r=Store.find(ln,id); if(!r) return;
|
||||||
|
if(r.lat!=null) this._map.setView([r.lat,r.lng],17);
|
||||||
|
else if(r.coords?.length) try{this._map.fitBounds(L.latLngBounds(r.coords),{padding:[40,40]})}catch{}
|
||||||
|
highlightCard(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Draw.init(this._map);
|
||||||
|
|
||||||
|
// Nav
|
||||||
|
document.getElementById('nav-pills').addEventListener('click',e=>{
|
||||||
|
const pill=e.target.closest('.nav-pill'); if(!pill) return;
|
||||||
|
Draw.cancel();
|
||||||
|
this.state.activeLayer=pill.dataset.layer;
|
||||||
|
document.querySelectorAll('.nav-pill').forEach(p=>p.classList.remove('active'));
|
||||||
|
pill.classList.add('active');
|
||||||
|
renderSidebar(this.state.activeLayer);
|
||||||
|
renderLegend(this.state.activeLayer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add btn
|
||||||
|
document.getElementById('btn-add').addEventListener('click',()=>Draw.start(this.state.activeLayer));
|
||||||
|
|
||||||
|
// Modal
|
||||||
|
document.getElementById('modal-save').addEventListener('click', Modal.submit);
|
||||||
|
document.getElementById('modal-cancel').addEventListener('click', Modal.close);
|
||||||
|
document.getElementById('modal-close').addEventListener('click', Modal.close);
|
||||||
|
document.getElementById('modal-ov').addEventListener('click',e=>{ if(e.target===e.currentTarget) Modal.close(); });
|
||||||
|
|
||||||
|
await Store.load();
|
||||||
|
this.refresh();
|
||||||
|
},
|
||||||
|
|
||||||
|
crud:{
|
||||||
|
edit(ln,id){ App.state.activeLayer=ln; Modal.openEdit(ln,id); },
|
||||||
|
async delete(ln,id){
|
||||||
|
if(!confirm('Yakin hapus data ini?')) return;
|
||||||
|
await Store.remove(ln,id); App._map.closePopup();
|
||||||
|
App.refresh(); Toast.info('Data dihapus');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh(){
|
||||||
|
renderMap(); renderSidebar(this.state.activeLayer);
|
||||||
|
renderLegend(this.state.activeLayer); updateBadges();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded',()=>App.init());
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
7. CARD BUILDERS (sidebar)
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Card = {
|
||||||
|
_acts:(ln,id)=>`<div class="dcard-acts">
|
||||||
|
<button class="bsm bsm-zoom" onclick="App.map.zoomTo('${ln}','${id}')">Zoom</button>
|
||||||
|
<button class="bsm bsm-edit" onclick="App.crud.edit('${ln}','${id}')">Edit</button>
|
||||||
|
<button class="bsm bsm-del" onclick="App.crud.delete('${ln}','${id}')">Hapus</button></div>`,
|
||||||
|
|
||||||
|
spbu(r){ const clr=CFG.spbu.color(r),bdg=CFG.spbu.badge(r);
|
||||||
|
return `<div class="dcard" id="card-${r.id}">
|
||||||
|
<div class="dcard-head" onclick="App.map.zoomTo('spbu','${r.id}')">
|
||||||
|
<span class="dcard-sym circle" style="background:${clr}"></span>
|
||||||
|
<span class="dcard-name">${H.esc(r.nama)}</span><span class="badge ${bdg.c}">${bdg.t}</span></div>
|
||||||
|
<div class="dcard-attrs">
|
||||||
|
<div class="attr"><span class="attr-k">WA</span><span class="attr-v">${H.esc(r.wa||'-')}</span></div>
|
||||||
|
<div class="attr"><span class="attr-k">BBM</span><span class="attr-v">${H.esc(r.jenis_bbm||'-')}</span></div>
|
||||||
|
</div>${this._acts('spbu',r.id)}</div>`; },
|
||||||
|
|
||||||
|
jalan(r){ const clr=CFG.jalan.color(r),bdg=CFG.jalan.badge(r);
|
||||||
|
return `<div class="dcard" id="card-${r.id}">
|
||||||
|
<div class="dcard-head" onclick="App.map.zoomTo('jalan','${r.id}')">
|
||||||
|
<span class="dcard-sym line" style="background:${clr}"></span>
|
||||||
|
<span class="dcard-name">${H.esc(r.nama)}</span><span class="badge ${bdg.c}">${bdg.t}</span></div>
|
||||||
|
<div class="dcard-attrs">
|
||||||
|
<div class="attr"><span class="attr-k">Panjang</span><span class="attr-v">${H.fmDist(r.panjang)}</span></div>
|
||||||
|
<div class="attr"><span class="attr-k">Kondisi</span><span class="attr-v">${H.esc(r.kondisi||'-')}</span></div>
|
||||||
|
</div>${this._acts('jalan',r.id)}</div>`; },
|
||||||
|
|
||||||
|
parsil(r){ const clr=CFG.parsil.color(r),bdg=CFG.parsil.badge(r);
|
||||||
|
return `<div class="dcard" id="card-${r.id}">
|
||||||
|
<div class="dcard-head" onclick="App.map.zoomTo('parsil','${r.id}')">
|
||||||
|
<span class="dcard-sym poly" style="color:${clr};height:9px"></span>
|
||||||
|
<span class="dcard-name">${H.esc(r.nama)}</span><span class="badge ${bdg.c}">${bdg.t}</span></div>
|
||||||
|
<div class="dcard-attrs">
|
||||||
|
<div class="attr"><span class="attr-k">Luas</span><span class="attr-v">${H.fmArea(r.luas)}</span></div>
|
||||||
|
<div class="attr"><span class="attr-k">Sengketa</span><span class="attr-v">${H.esc(r.sengketa||'-')}</span></div>
|
||||||
|
</div>${this._acts('parsil',r.id)}</div>`; },
|
||||||
|
|
||||||
|
warga(r){ const clr=CFG.warga.color(r),bdg=CFG.warga.badge(r);
|
||||||
|
const pendapatan=r.pendapatan?'Rp '+parseInt(r.pendapatan).toLocaleString('id-ID'):'-';
|
||||||
|
const histCount=r.history_bansos?.length||0;
|
||||||
|
return `<div class="dcard" id="card-${r.id}">
|
||||||
|
<div class="dcard-head" onclick="App.map.zoomTo('warga','${r.id}')">
|
||||||
|
<span class="dcard-sym circle" style="background:${clr}"></span>
|
||||||
|
<span class="dcard-name">${H.esc(r.nama)}</span><span class="badge ${bdg.c}">${bdg.t}</span></div>
|
||||||
|
<div class="dcard-attrs">
|
||||||
|
<div class="attr"><span class="attr-k">Anggota</span><span class="attr-v">${r.jml_anggota||'-'} jiwa</span></div>
|
||||||
|
<div class="attr"><span class="attr-k">Pendapatan</span><span class="attr-v">${pendapatan}</span></div>
|
||||||
|
<div class="attr"><span class="attr-k">Bansos</span><span class="attr-v">${H.esc(r.bansos||'-')}</span></div>
|
||||||
|
<div class="attr"><span class="attr-k">Riwayat</span><span class="attr-v">${histCount} program</span></div>
|
||||||
|
</div>${this._acts('warga',r.id)}</div>`; },
|
||||||
|
|
||||||
|
rumahIbadah(r){ const clr=CFG.rumahIbadah.color(r),bdg=CFG.rumahIbadah.badge(r);
|
||||||
|
return `<div class="dcard" id="card-${r.id}">
|
||||||
|
<div class="dcard-head" onclick="App.map.zoomTo('rumahIbadah','${r.id}')">
|
||||||
|
<span class="dcard-sym circle" style="background:${clr}"></span>
|
||||||
|
<span class="dcard-name">${H.esc(r.nama)}</span><span class="badge ${bdg.c}">${bdg.t}</span></div>
|
||||||
|
<div class="dcard-attrs">
|
||||||
|
<div class="attr"><span class="attr-k">Ketua</span><span class="attr-v">${H.esc(r.ketua||'-')}</span></div>
|
||||||
|
<div class="attr"><span class="attr-k">Binaan</span><span class="attr-v">${r.jml_binaan||'-'} KK</span></div>
|
||||||
|
</div>${this._acts('rumahIbadah',r.id)}</div>`; },
|
||||||
|
};
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
5. FORM BUILDERS
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Forms = {
|
||||||
|
spbu(rec, latlng){
|
||||||
|
const lat=latlng?latlng.lat.toFixed(6):(rec?.lat?.toFixed(6)||'');
|
||||||
|
const lng=latlng?latlng.lng.toFixed(6):(rec?.lng?.toFixed(6)||'');
|
||||||
|
const v=f=>rec?H.esc(rec[f]||''):'';
|
||||||
|
return `
|
||||||
|
<div class="info-box">📍 Koordinat: <strong>${lat}, ${lng}</strong></div>
|
||||||
|
<div class="form-sect">Identitas & Operasional</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Nama SPBU<span class="req">*</span></label>
|
||||||
|
<input class="finput" id="f-nama" placeholder="cth: SPBU 64.754.01" value="${v('nama')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">No. WhatsApp</label>
|
||||||
|
<input class="finput" id="f-wa" placeholder="081234..." value="${v('wa')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Jenis BBM Tersedia</label>
|
||||||
|
<select class="finput" id="f-jenis_bbm">
|
||||||
|
${['Pertalite & Pertamax','Pertalite Saja','Pertamax Series','Solar & Pertamina Dex','Semua Jenis'].map(j=>`<option ${v('jenis_bbm')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Buka 24 Jam?</label>
|
||||||
|
<div class="rtoggle" id="rtog">
|
||||||
|
<label class="ropt ${rec?.buka24!==false?'yes':''}" data-v="true">
|
||||||
|
<input type="radio" name="buka24" value="true" ${rec?.buka24!==false?'checked':''}/>✓ Ya
|
||||||
|
</label>
|
||||||
|
<label class="ropt ${rec?.buka24===false?'no':''}" data-v="false">
|
||||||
|
<input type="radio" name="buka24" value="false" ${rec?.buka24===false?'checked':''}/>✗ Tidak
|
||||||
|
</label>
|
||||||
|
</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Aksesibilitas & Sosial</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Pelayanan Disabilitas</label>
|
||||||
|
<select class="finput" id="f-disabilitas">
|
||||||
|
${['Ada','Tidak Ada','Dalam Proses'].map(j=>`<option ${v('disabilitas')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Harga BBM Subsidi</label>
|
||||||
|
<select class="finput" id="f-harga_bbm">
|
||||||
|
${['Sesuai HET','Di Atas HET','Tidak Menjual Subsidi'].map(j=>`<option ${v('harga_bbm')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Catatan Lapangan</div>
|
||||||
|
<div class="fg"><label class="flabel">Catatan / Keterangan Tambahan</label>
|
||||||
|
<textarea class="finput" id="f-catatan" placeholder="Kondisi antrean, ketersediaan stok, dll...">${v('catatan')}</textarea></div>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
jalan(rec, len){
|
||||||
|
const pj=len??rec?.panjang??0; const v=f=>rec?H.esc(rec[f]||''):'';
|
||||||
|
return `
|
||||||
|
<div class="info-box">📏 Panjang terukur otomatis: <strong>${H.fmDist(pj)}</strong></div>
|
||||||
|
<input type="hidden" id="f-panjang" value="${pj}"/>
|
||||||
|
<div class="form-sect">Identitas Ruas Jalan</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Nama Ruas Jalan<span class="req">*</span></label>
|
||||||
|
<input class="finput" id="f-nama" placeholder="cth: Jl. H. Rais A Rahman" value="${v('nama')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Status Jalan<span class="req">*</span></label>
|
||||||
|
<select class="finput" id="f-status">
|
||||||
|
${CFG.jalan.statuses.map(s=>`<option ${v('status')===s?'selected':''}>${s}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Kondisi & Infrastruktur</div>
|
||||||
|
<div class="form-row cols3">
|
||||||
|
<div class="fg"><label class="flabel">Kondisi Jalan</label>
|
||||||
|
<select class="finput" id="f-kondisi">
|
||||||
|
${CFG.jalan.kondisi.map(k=>`<option ${v('kondisi')===k?'selected':''}>${k}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Tahun Perbaikan Terakhir</label>
|
||||||
|
<input class="finput" id="f-tahun_perbaikan" type="number" min="2000" max="2030" placeholder="2023" value="${v('tahun_perbaikan')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Lebar Jalan (meter)</label>
|
||||||
|
<input class="finput" id="f-lebar" type="number" min="1" max="50" placeholder="4" value="${v('lebar')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Dampak Sosial-Ekonomi</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Dilalui Angkutan Umum</label>
|
||||||
|
<select class="finput" id="f-angkutan">
|
||||||
|
${['Ya','Tidak'].map(k=>`<option ${v('angkutan')===k?'selected':''}>${k}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Akses ke Fasilitas Publik</label>
|
||||||
|
<select class="finput" id="f-akses_publik">
|
||||||
|
${['Pasar','RS/Puskesmas','Sekolah','Kantor Pemerintah','Tidak Ada'].map(k=>`<option ${v('akses_publik')===k?'selected':''}>${k}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="fg"><label class="flabel">Catatan Kondisi</label>
|
||||||
|
<textarea class="finput" id="f-catatan" placeholder="Kondisi berlubang, banjir saat hujan, dll...">${v('catatan')}</textarea></div>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
parsil(rec, area){
|
||||||
|
const ls=area??rec?.luas??0; const v=f=>rec?H.esc(rec[f]||''):'';
|
||||||
|
return `
|
||||||
|
<div class="info-box">📐 Luas terukur otomatis: <strong>${H.fmArea(ls)}</strong></div>
|
||||||
|
<input type="hidden" id="f-luas" value="${ls}"/>
|
||||||
|
<div class="form-sect">Identitas Parsil</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Nama / ID Parsil<span class="req">*</span></label>
|
||||||
|
<input class="finput" id="f-nama" placeholder="cth: Kavling A-01" value="${v('nama')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Nomor Induk Bidang (NIB)</label>
|
||||||
|
<input class="finput" id="f-nib" placeholder="14.02.01.001" value="${v('nib')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Status Kepemilikan</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Status Sertifikat<span class="req">*</span></label>
|
||||||
|
<select class="finput" id="f-status">
|
||||||
|
${CFG.parsil.statuses.map(s=>`<option ${v('status')===s?'selected':''}>${s}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Sengketa / Masalah Hukum</label>
|
||||||
|
<select class="finput" id="f-sengketa">
|
||||||
|
${['Tidak Ada','Ada Sengketa','Dalam Proses PTSL','Klaim Pihak Lain'].map(k=>`<option ${v('sengketa')===k?'selected':''}>${k}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Penilaian & Pemanfaatan</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">NJOP (Rp/m²)</label>
|
||||||
|
<input class="finput" id="f-njop" type="number" placeholder="500000" value="${v('njop')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Penggunaan Lahan</label>
|
||||||
|
<select class="finput" id="f-penggunaan">
|
||||||
|
${['Rumah Tinggal','Lahan Kosong','Pertanian','Usaha/Warung','Fasilitas Umum','Lainnya'].map(k=>`<option ${v('penggunaan')===k?'selected':''}>${k}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="fg"><label class="flabel">Catatan Tambahan</label>
|
||||||
|
<textarea class="finput" id="f-catatan" placeholder="Kondisi, penghuni, riwayat...">${v('catatan')}</textarea></div>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
warga(rec, latlng){
|
||||||
|
const lat=latlng?latlng.lat.toFixed(6):(rec?.lat?.toFixed(6)||'');
|
||||||
|
const lng=latlng?latlng.lng.toFixed(6):(rec?.lng?.toFixed(6)||'');
|
||||||
|
const v=f=>rec?H.esc(rec[f]||''):'';
|
||||||
|
const c=v=>rec?.penyakit?.includes(v)?'checked':'';
|
||||||
|
const hist=rec?.history_bansos||[];
|
||||||
|
const ri = Store.all('rumahIbadah');
|
||||||
|
const hubOpts = ['Belum ditugaskan', ...ri.map(r=>r.nama)].map(opt=>`<option ${v('hub_ri')===opt?'selected':''}>${opt}</option>`).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="info-box warn">⚠️ Data warga bersifat sensitif. Pastikan survei akurat.</div>
|
||||||
|
<div class="info-box">📍 Koordinat: <strong>${lat}, ${lng}</strong></div>
|
||||||
|
<div class="form-tabs">
|
||||||
|
<div class="form-tab active" data-panel="kk">Data KK</div>
|
||||||
|
<div class="form-tab" data-panel="eko">Ekonomi</div>
|
||||||
|
<div class="form-tab" data-panel="pddk">Pddk</div>
|
||||||
|
<div class="form-tab" data-panel="kes">Kes</div>
|
||||||
|
<div class="form-tab" data-panel="hist">Bansos</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: Data KK -->
|
||||||
|
<div class="form-section-panel active" id="panel-kk">
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Nama KK<span class="req">*</span></label>
|
||||||
|
<input class="finput" id="f-nama" placeholder="Nama KTP" value="${v('nama')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">NIK</label>
|
||||||
|
<input class="finput" id="f-nik" placeholder="3471..." maxlength="16" value="${v('nik')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">No. KK</label>
|
||||||
|
<input class="finput" id="f-no_kk" value="${v('no_kk')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">WA/HP</label>
|
||||||
|
<input class="finput" id="f-wa" value="${v('wa')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row cols3">
|
||||||
|
<div class="fg"><label class="flabel">Tingkat Kemiskinan</label>
|
||||||
|
<select class="finput" id="f-tingkat_kemiskinan">
|
||||||
|
${CFG.warga.tingkat.map(t=>`<option ${v('tingkat_kemiskinan')===t?'selected':''}>${t}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Jml Anggota</label>
|
||||||
|
<input class="finput" id="f-jml_anggota" type="number" placeholder="4" value="${v('jml_anggota')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Jml Lansia</label>
|
||||||
|
<input class="finput" id="f-jml_lansia" type="number" placeholder="0" value="${v('jml_lansia')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="fg"><label class="flabel">Alamat</label>
|
||||||
|
<textarea class="finput" id="f-alamat" style="min-height:40px">${v('alamat')}</textarea></div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Penerima Bansos</label>
|
||||||
|
<select class="finput" id="f-bansos">
|
||||||
|
${CFG.warga.bantuan.map(b=>`<option ${v('bansos')===b?'selected':''}>${b}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Hub Rumah Ibadah</label>
|
||||||
|
<select class="finput" id="f-hub_ri">${hubOpts}</select></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: Ekonomi -->
|
||||||
|
<div class="form-section-panel" id="panel-eko">
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Pendapatan (Rp/Bln)</label>
|
||||||
|
<input class="finput" id="f-pendapatan" type="number" value="${v('pendapatan')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Pekerjaan KK</label>
|
||||||
|
<select class="finput" id="f-pekerjaan">
|
||||||
|
${CFG.warga.pekerjaan.map(p=>`<option ${v('pekerjaan')===p?'selected':''}>${p}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Kondisi Rumah</label>
|
||||||
|
<select class="finput" id="f-kondisi_rumah">
|
||||||
|
${CFG.warga.kondisiRumah.map(k=>`<option ${v('kondisi_rumah')===k?'selected':''}>${k}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Aset (Tanah/Kendaraan)</label>
|
||||||
|
<select class="finput" id="f-kepemilikan_aset">
|
||||||
|
${['Tidak Ada','Motor Tua','Motor Baru','Tanah','Mobil'].map(j=>`<option ${v('kepemilikan_aset')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Air Bersih</label>
|
||||||
|
<select class="finput" id="f-air_bersih">
|
||||||
|
${['PDAM','Sumur','Sungai/Tadah Hujan'].map(j=>`<option ${v('air_bersih')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Listrik</label>
|
||||||
|
<select class="finput" id="f-listrik">
|
||||||
|
${['Sendiri (>900W)','Subsidi (450/900W)','Numpang/Tidak Ada'].map(j=>`<option ${v('listrik')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: Pendidikan -->
|
||||||
|
<div class="form-section-panel" id="panel-pddk">
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Pendidikan Terakhir KK</label>
|
||||||
|
<select class="finput" id="f-pendidikan_kk">
|
||||||
|
${CFG.warga.tingkatPendidikan.map(j=>`<option ${v('pendidikan_kk')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Anak Bersekolah</label>
|
||||||
|
<input class="finput" id="f-jml_anak" type="number" placeholder="1" value="${v('jml_anak')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Anak Putus Sekolah</label>
|
||||||
|
<input class="finput" id="f-anak_putus_sekolah" type="number" placeholder="0" value="${v('anak_putus_sekolah')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Penerima KIP/Beasiswa?</label>
|
||||||
|
<select class="finput" id="f-penerima_beasiswa">
|
||||||
|
${['Ya','Tidak'].map(j=>`<option ${v('penerima_beasiswa')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: Kesehatan -->
|
||||||
|
<div class="form-section-panel" id="panel-kes">
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Kepesertaan BPJS</label>
|
||||||
|
<select class="finput" id="f-bpjs">
|
||||||
|
${['PBI (Gratis)','Mandiri','Perusahaan','Tidak Punya BPJS'].map(j=>`<option ${v('bpjs')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Anggota Difabel/Kronis</label>
|
||||||
|
<input class="finput" id="f-jml_sakit_kronis" type="number" value="${v('jml_sakit_kronis')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="fg"><label class="flabel">Jenis Penyakit Khusus</label>
|
||||||
|
<div class="penyakit-grid">
|
||||||
|
${CFG.warga.jenisPenyakit.map(p=>`<label class="penyakit-check ${c(p)}"><input type="checkbox" name="penyakit" value="${p}" ${c(p)}/>${p}</label>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: Riwayat Bansos -->
|
||||||
|
<div class="form-section-panel" id="panel-hist">
|
||||||
|
<table class="history-table">
|
||||||
|
<thead><tr><th>Program</th><th style="width:60px">Thn</th><th>Status</th><th style="width:28px"></th></tr></thead>
|
||||||
|
<tbody id="hist-tbody">
|
||||||
|
${hist.map(h=>`
|
||||||
|
<tr>
|
||||||
|
<td><input class="finput hist-program" style="padding:3px" value="${H.esc(h.program)}"/></td>
|
||||||
|
<td><input class="finput hist-tahun" style="padding:3px" type="number" value="${h.tahun}"/></td>
|
||||||
|
<td><select class="finput hist-status" style="padding:3px"><option ${h.status==='Aktif'?'selected':''}>Aktif</option><option ${h.status==='Berhenti'?'selected':''}>Berhenti</option></select></td>
|
||||||
|
<td><button class="btn-del-row" type="button" onclick="this.closest('tr').remove()">✕</button></td>
|
||||||
|
</tr>`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button class="btn-add-row" type="button" id="btn-add-hist">+ Tambah</button>
|
||||||
|
<div class="fg" style="margin-top:10px"><label class="flabel">Catatan / Rekomendasi</label>
|
||||||
|
<textarea class="finput" id="f-catatan" style="min-height:50px">${v('catatan')}</textarea></div>
|
||||||
|
</div>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
rumahIbadah(rec, latlng){
|
||||||
|
const lat=latlng?latlng.lat.toFixed(6):(rec?.lat?.toFixed(6)||'');
|
||||||
|
const lng=latlng?latlng.lng.toFixed(6):(rec?.lng?.toFixed(6)||'');
|
||||||
|
const v=f=>rec?H.esc(rec[f]||''):'';
|
||||||
|
return `
|
||||||
|
<div class="info-box">📍 Koordinat: <strong>${lat}, ${lng}</strong></div>
|
||||||
|
<div class="form-sect">Identitas Rumah Ibadah</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Nama Rumah Ibadah<span class="req">*</span></label>
|
||||||
|
<input class="finput" id="f-nama" placeholder="cth: Masjid Al-Ikhlas" value="${v('nama')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Jenis<span class="req">*</span></label>
|
||||||
|
<select class="finput" id="f-jenis">
|
||||||
|
${CFG.rumahIbadah.jenis.map(j=>`<option ${v('jenis')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Nama Ketua / Imam / Pastor</label>
|
||||||
|
<input class="finput" id="f-ketua" placeholder="Nama lengkap" value="${v('ketua')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">No. WhatsApp Pengurus</label>
|
||||||
|
<input class="finput" id="f-wa" placeholder="081..." value="${v('wa')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Kapasitas & Jangkauan</div>
|
||||||
|
<div class="form-row cols3">
|
||||||
|
<div class="fg"><label class="flabel">Kapasitas Jamaah</label>
|
||||||
|
<input class="finput" id="f-kapasitas" type="number" placeholder="200" value="${v('kapasitas')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Radius Wilayah Binaan (m)</label>
|
||||||
|
<input class="finput" id="f-radius" type="number" placeholder="300" value="${v('radius')||300}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Jml Warga Binaan</label>
|
||||||
|
<input class="finput" id="f-jml_binaan" type="number" placeholder="150" value="${v('jml_binaan')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Program Sosial</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Program Aktif</label>
|
||||||
|
<select class="finput" id="f-program">
|
||||||
|
${['Baznas/Zakat','Bantuan Sembako','Santunan Anak Yatim','Pemberdayaan UMKM','Beasiswa','Belum Ada'].map(k=>`<option ${v('program')===k?'selected':''}>${k}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Dana Sosial/Bulan (Rp)</label>
|
||||||
|
<input class="finput" id="f-dana_sosial" type="number" placeholder="2000000" value="${v('dana_sosial')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="fg"><label class="flabel">Catatan / Kebutuhan Dukungan</label>
|
||||||
|
<textarea class="finput" id="f-catatan" placeholder="Kebutuhan data, pelatihan, dll...">${v('catatan')}</textarea></div>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
readAll(ln){
|
||||||
|
const g = id => document.getElementById(id)?.value?.trim();
|
||||||
|
if(ln==='spbu'){
|
||||||
|
const nm=g('f-nama'); if(!nm){Toast.error('Nama SPBU wajib diisi!');return null;}
|
||||||
|
const buka=document.querySelector('input[name="buka24"]:checked')?.value!=='false';
|
||||||
|
return {nama:nm,wa:g('f-wa'),buka24:buka,jenis_bbm:g('f-jenis_bbm'),disabilitas:g('f-disabilitas'),harga_bbm:g('f-harga_bbm'),catatan:g('f-catatan')};
|
||||||
|
}
|
||||||
|
if(ln==='jalan'){
|
||||||
|
const nm=g('f-nama'); if(!nm){Toast.error('Nama jalan wajib diisi!');return null;}
|
||||||
|
return {nama:nm,status:g('f-status'),panjang:parseFloat(g('f-panjang'))||0,kondisi:g('f-kondisi'),tahun_perbaikan:g('f-tahun_perbaikan'),lebar:g('f-lebar'),angkutan:g('f-angkutan'),akses_publik:g('f-akses_publik'),catatan:g('f-catatan')};
|
||||||
|
}
|
||||||
|
if(ln==='parsil'){
|
||||||
|
const nm=g('f-nama'); if(!nm){Toast.error('Nama parsil wajib diisi!');return null;}
|
||||||
|
return {nama:nm,status:g('f-status'),luas:parseFloat(g('f-luas'))||0,nib:g('f-nib'),sengketa:g('f-sengketa'),njop:g('f-njop'),penggunaan:g('f-penggunaan'),catatan:g('f-catatan')};
|
||||||
|
}
|
||||||
|
if(ln==='warga'){
|
||||||
|
const nm=g('f-nama'); if(!nm){Toast.error('Nama KK wajib diisi!');return null;}
|
||||||
|
const penyakit=[...document.querySelectorAll('input[name="penyakit"]:checked')].map(el=>el.value);
|
||||||
|
const history_bansos=[];
|
||||||
|
document.querySelectorAll('#hist-tbody tr').forEach(tr=>{
|
||||||
|
const prog=tr.querySelector('.hist-program')?.value?.trim();
|
||||||
|
const thn=tr.querySelector('.hist-tahun')?.value?.trim();
|
||||||
|
const sts=tr.querySelector('.hist-status')?.value;
|
||||||
|
if(prog) history_bansos.push({program:prog,tahun:parseInt(thn)||new Date().getFullYear(),status:sts});
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
nama: nm, nik: g('f-nik'), no_kk: g('f-no_kk'), wa: g('f-wa'), tingkat_kemiskinan: g('f-tingkat_kemiskinan'),
|
||||||
|
jml_anggota: parseInt(g('f-jml_anggota'))||0, jml_lansia: parseInt(g('f-jml_lansia'))||0,
|
||||||
|
alamat: g('f-alamat'), bansos: g('f-bansos'), hub_ri: g('f-hub_ri'),
|
||||||
|
pendapatan: g('f-pendapatan'), pekerjaan: g('f-pekerjaan'),
|
||||||
|
kondisi_rumah: g('f-kondisi_rumah'), kepemilikan_aset: g('f-kepemilikan_aset'),
|
||||||
|
air_bersih: g('f-air_bersih'), listrik: g('f-listrik'),
|
||||||
|
pendidikan_kk: g('f-pendidikan_kk'), jml_anak: parseInt(g('f-jml_anak'))||0,
|
||||||
|
anak_putus_sekolah: parseInt(g('f-anak_putus_sekolah'))||0, penerima_beasiswa: g('f-penerima_beasiswa'),
|
||||||
|
bpjs: g('f-bpjs'), jml_sakit_kronis: parseInt(g('f-jml_sakit_kronis'))||0,
|
||||||
|
penyakit, history_bansos, catatan: g('f-catatan'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if(ln==='rumahIbadah'){
|
||||||
|
const nm=g('f-nama'); if(!nm){Toast.error('Nama Rumah Ibadah wajib diisi!');return null;}
|
||||||
|
return {
|
||||||
|
nama: nm, jenis: g('f-jenis'), ketua: g('f-ketua'), wa: g('f-wa'),
|
||||||
|
kapasitas: parseInt(g('f-kapasitas'))||0, radius: parseFloat(g('f-radius'))||300,
|
||||||
|
jml_binaan: parseInt(g('f-jml_binaan'))||0, program: g('f-program'),
|
||||||
|
dana_sosial: g('f-dana_sosial'), catatan: g('f-catatan')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
10. LEGEND
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
function renderLegend(ln){
|
||||||
|
const el=document.getElementById('legend-items');
|
||||||
|
const rows=CFG[ln].legend||[];
|
||||||
|
el.innerHTML=rows.map(r=>{
|
||||||
|
let sym='';
|
||||||
|
if(r.sym==='c') sym=`<span class="leg-c" style="background:${r.clr}"></span>`;
|
||||||
|
else if(r.sym==='l') sym=`<span class="leg-l" style="background:${r.clr}"></span>`;
|
||||||
|
else sym=`<span class="leg-p" style="border-color:${r.clr};background:${r.clr}33"></span>`;
|
||||||
|
return `<div class="legend-row">${sym}<span>${r.l}</span></div>`;
|
||||||
|
}).join('');
|
||||||
|
// Add radius note for rumahIbadah and bansos
|
||||||
|
if(ln==='rumahIbadah'||ln==='warga'){
|
||||||
|
el.innerHTML+=`<div class="legend-radius"><span class="leg-radius-sym"></span>Radius Hub 300m</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
6. POPUP BUILDERS
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Popup = {
|
||||||
|
_row:(k,v)=>`<div class="popup-row"><span class="popup-k">${k}</span><span class="popup-v">${H.esc(v||'-')}</span></div>`,
|
||||||
|
_sect:t=>`<div class="popup-section">${t}</div>`,
|
||||||
|
_acts:(ln,id)=>`<div class="popup-acts"><button class="bsm bsm-edit" onclick="App.crud.edit('${ln}','${id}')">Edit</button><button class="bsm bsm-del" onclick="App.crud.delete('${ln}','${id}')">Hapus</button></div>`,
|
||||||
|
|
||||||
|
spbu(r){ const cfg=CFG.spbu,clr=cfg.color(r),bdg=cfg.badge(r);
|
||||||
|
return `<div class="popup-wrap"><div class="popup-title"><span class="popup-sym" style="background:${clr}"></span>${H.esc(r.nama)}</div>
|
||||||
|
<span class="badge ${bdg.c}">${bdg.t}</span>
|
||||||
|
${this._sect('Operasional')}
|
||||||
|
${this._row('No. WA',r.wa)}${this._row('Jenis BBM',r.jenis_bbm)}${this._row('Harga BBM',r.harga_bbm)}${this._row('Akses Disabilitas',r.disabilitas)}
|
||||||
|
${r.catatan?this._row('Catatan',r.catatan):''}
|
||||||
|
${this._acts('spbu',r.id)}</div>`; },
|
||||||
|
|
||||||
|
jalan(r){ const cfg=CFG.jalan,bdg=cfg.badge(r);
|
||||||
|
return `<div class="popup-wrap"><div class="popup-title">🛣️ ${H.esc(r.nama)}</div>
|
||||||
|
<span class="badge ${bdg.c}">${bdg.t}</span>
|
||||||
|
${this._sect('Spesifikasi')}
|
||||||
|
${this._row('Panjang',H.fmDist(r.panjang))}${this._row('Lebar',r.lebar?(r.lebar+' m'):'-')}${this._row('Kondisi',r.kondisi)}${this._row('Perbaikan Terakhir',r.tahun_perbaikan||'-')}
|
||||||
|
${this._sect('Dampak Sosial')}
|
||||||
|
${this._row('Angkutan Umum',r.angkutan)}${this._row('Akses ke',r.akses_publik)}
|
||||||
|
${r.catatan?this._row('Catatan',r.catatan):''}
|
||||||
|
${this._acts('jalan',r.id)}</div>`; },
|
||||||
|
|
||||||
|
parsil(r){ const cfg=CFG.parsil,bdg=cfg.badge(r);
|
||||||
|
return `<div class="popup-wrap"><div class="popup-title">🏗️ ${H.esc(r.nama)}</div>
|
||||||
|
<span class="badge ${bdg.c}">${bdg.t}</span>
|
||||||
|
${this._sect('Data Hukum')}
|
||||||
|
${this._row('NIB',r.nib||'-')}${this._row('Status Sengketa',r.sengketa)}${this._row('Penggunaan',r.penggunaan)}
|
||||||
|
${this._sect('Penilaian')}
|
||||||
|
${this._row('Luas',H.fmArea(r.luas))}${this._row('NJOP/m²',r.njop?'Rp '+parseInt(r.njop).toLocaleString('id-ID'):'-')}
|
||||||
|
${r.catatan?this._row('Catatan',r.catatan):''}
|
||||||
|
${this._acts('parsil',r.id)}</div>`; },
|
||||||
|
|
||||||
|
warga(r){ const cfg=CFG.warga,clr=cfg.color(r),bdg=cfg.badge(r);
|
||||||
|
const ri=Store.all('rumahIbadah');
|
||||||
|
let nearestRI=null, nearestDist=Infinity;
|
||||||
|
ri.forEach(hub=>{
|
||||||
|
if(hub.lat==null||r.lat==null) return;
|
||||||
|
const d=L.latLng(r.lat,r.lng).distanceTo(L.latLng(hub.lat,hub.lng));
|
||||||
|
if(d<nearestDist){nearestDist=d;nearestRI=hub;}
|
||||||
|
});
|
||||||
|
const radius=nearestRI?(parseFloat(nearestRI.radius)||300):300;
|
||||||
|
const covered=nearestRI&&nearestDist<=radius;
|
||||||
|
const coverageHtml=nearestRI
|
||||||
|
?`<div class="coverage-badge ${covered?'covered':'uncovered'}">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="${covered?'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z':'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'}"/></svg>
|
||||||
|
${covered?'Terjangkau Hub: '+H.esc(nearestRI.nama)+' ('+Math.round(nearestDist)+' m)':'Tidak Terjangkau ('+Math.round(nearestDist)+' m dari RI)'}
|
||||||
|
</div>`
|
||||||
|
:'<div class="coverage-badge uncovered">Belum ada Rumah Ibadah Hub</div>';
|
||||||
|
const histHtml=r.history_bansos?.length
|
||||||
|
?r.history_bansos.map(h=>`<div style="font-size:10px;padding:3px 0;border-bottom:1px solid var(--border0);display:flex;justify-content:space-between"><span>${H.esc(h.program)} (${h.tahun||'-'})</span><span class="status-${(h.status||'').toLowerCase().replace(' ','')}" style="font-weight:700">${H.esc(h.status)}</span></div>`).join('')
|
||||||
|
:'<div style="font-size:10px;color:var(--text4)">Belum ada riwayat bansos</div>';
|
||||||
|
return `<div class="popup-wrap"><div class="popup-title"><span class="popup-sym" style="background:${clr}"></span>${H.esc(r.nama)}</div>
|
||||||
|
<span class="badge ${bdg.c}">${bdg.t}</span>
|
||||||
|
${coverageHtml}
|
||||||
|
${this._sect('Data Keluarga')}
|
||||||
|
${this._row('NIK',r.nik||'-')}${this._row('No. KK',r.no_kk||'-')}${this._row('Jml Anggota',r.jml_anggota+' jiwa')}
|
||||||
|
${this._sect('Ekonomi')}
|
||||||
|
${this._row('Pekerjaan KK',r.pekerjaan)}${this._row('Pendapatan/Bln',r.pendapatan?'Rp '+parseInt(r.pendapatan).toLocaleString('id-ID'):'-')}${this._row('Kondisi Rumah',r.kondisi_rumah)}
|
||||||
|
${this._sect('Fasilitas')}
|
||||||
|
${this._row('Air Bersih',r.air_bersih)}${this._row('Listrik',r.listrik)}
|
||||||
|
${this._sect('Bansos')}
|
||||||
|
${this._row('Bansos Saat Ini',r.bansos)}${this._row('Hub RI',r.hub_ri||'-')}
|
||||||
|
<div style="margin-top:2px">${histHtml}</div>
|
||||||
|
${r.catatan?this._row('Rekomendasi',r.catatan):''}
|
||||||
|
${this._acts('warga',r.id)}</div>`; },
|
||||||
|
|
||||||
|
rumahIbadah(r){ const cfg=CFG.rumahIbadah,clr=cfg.color(r),bdg=cfg.badge(r);
|
||||||
|
return `<div class="popup-wrap"><div class="popup-title"><span class="popup-sym" style="background:${clr}"></span>${H.esc(r.nama)}</div>
|
||||||
|
<span class="badge ${bdg.c}">${bdg.t}</span>
|
||||||
|
${this._sect('Kontak')}
|
||||||
|
${this._row('Ketua/Imam',r.ketua||'-')}${this._row('No. WA',r.wa||'-')}
|
||||||
|
${this._sect('Kapasitas & Binaan')}
|
||||||
|
${this._row('Kapasitas',r.kapasitas?r.kapasitas+' orang':'-')}${this._row('Radius Binaan',(r.radius||300)+' m')}${this._row('Warga Binaan',r.jml_binaan?r.jml_binaan+' KK':'-')}
|
||||||
|
${this._sect('Program Sosial')}
|
||||||
|
${this._row('Program Aktif',r.program)}${this._row('Dana Sosial/Bln',r.dana_sosial?'Rp '+parseInt(r.dana_sosial).toLocaleString('id-ID'):'-')}
|
||||||
|
${r.catatan?this._row('Catatan',r.catatan):''}
|
||||||
|
${this._acts('rumahIbadah',r.id)}</div>`; },
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
4. LAYER CONFIG (atribut + warna per layer)
|
||||||
|
── Ini adalah "kontrak data" antara programmer & client ──
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const CFG = {
|
||||||
|
spbu: {
|
||||||
|
label:'Data SPBU', sub:'Stasiun Bahan Bakar Umum', icon:'⛽', drawType:'point',
|
||||||
|
color: r => r.buka24?'#34d47e':'#f05c5c',
|
||||||
|
badge: r => r.buka24?{t:'24 Jam',c:'badge-green'}:{t:'Tutup',c:'badge-red'},
|
||||||
|
legend:[{l:'Buka 24 Jam',clr:'#34d47e',sym:'c'},{l:'Tidak 24 Jam',clr:'#f05c5c',sym:'c'}],
|
||||||
|
},
|
||||||
|
jalan: {
|
||||||
|
label:'Data Jalan', sub:'Ruas jalan berdasarkan status', icon:'🛣️', drawType:'polyline',
|
||||||
|
statuses:['Nasional','Provinsi','Kabupaten','Desa'],
|
||||||
|
colors:{'Nasional':'#f05c5c','Provinsi':'#f0a030','Kabupaten':'#5b9cf6','Desa':'#34d47e'},
|
||||||
|
badgeClass:{'Nasional':'badge-red','Provinsi':'badge-amber','Kabupaten':'badge-accent','Desa':'badge-green'},
|
||||||
|
kondisi:['Baik','Sedang','Rusak Ringan','Rusak Berat'],
|
||||||
|
color: r => ({'Nasional':'#f05c5c','Provinsi':'#f0a030','Kabupaten':'#5b9cf6','Desa':'#34d47e'}[r.status]||'#888'),
|
||||||
|
badge: r => ({t:r.status, c:{'Nasional':'badge-red','Provinsi':'badge-amber','Kabupaten':'badge-accent','Desa':'badge-green'}[r.status]||'badge-accent'}),
|
||||||
|
legend:[{l:'Nasional',clr:'#f05c5c',sym:'l'},{l:'Provinsi',clr:'#f0a030',sym:'l'},{l:'Kabupaten',clr:'#5b9cf6',sym:'l'},{l:'Desa',clr:'#34d47e',sym:'l'}],
|
||||||
|
},
|
||||||
|
parsil: {
|
||||||
|
label:'Data Parsil Tanah', sub:'Persil kavling berdasarkan kepemilikan', icon:'🏗️', drawType:'polygon',
|
||||||
|
statuses:['SHM','HGB','HGU','HP','Belum Bersertifikat'],
|
||||||
|
colors:{'SHM':'#34d47e','HGB':'#5b9cf6','HGU':'#f0a030','HP':'#9b7bfa','Belum Bersertifikat':'#f05c5c'},
|
||||||
|
badgeClass:{'SHM':'badge-green','HGB':'badge-accent','HGU':'badge-amber','HP':'badge-purple','Belum Bersertifikat':'badge-red'},
|
||||||
|
color: r => ({'SHM':'#34d47e','HGB':'#5b9cf6','HGU':'#f0a030','HP':'#9b7bfa','Belum Bersertifikat':'#f05c5c'}[r.status]||'#888'),
|
||||||
|
badge: r => ({t:r.status,c:{'SHM':'badge-green','HGB':'badge-accent','HGU':'badge-amber','HP':'badge-purple','Belum Bersertifikat':'badge-red'}[r.status]||'badge-accent'}),
|
||||||
|
legend:[{l:'SHM',clr:'#34d47e',sym:'p'},{l:'HGB',clr:'#5b9cf6',sym:'p'},{l:'HGU',clr:'#f0a030',sym:'p'},{l:'HP',clr:'#9b7bfa',sym:'p'},{l:'Belum Sertifikat',clr:'#f05c5c',sym:'p'}],
|
||||||
|
},
|
||||||
|
warga: {
|
||||||
|
label:'Data Warga & Bansos', sub:'Keluarga rentan & sasaran distribusi bansos', icon:'👨👩👧', drawType:'point',
|
||||||
|
tingkat:['Sangat Miskin','Miskin','Hampir Miskin','Rentan'],
|
||||||
|
colors:{'Sangat Miskin':'#f05c5c','Miskin':'#f0a030','Hampir Miskin':'#f0c040','Rentan':'#5b9cf6'},
|
||||||
|
badgeClass:{'Sangat Miskin':'badge-red','Miskin':'badge-orange','Hampir Miskin':'badge-amber','Rentan':'badge-accent'},
|
||||||
|
color: r => ({'Sangat Miskin':'#f05c5c','Miskin':'#f0a030','Hampir Miskin':'#f0c040','Rentan':'#5b9cf6'}[r.tingkat_kemiskinan]||'#888'),
|
||||||
|
badge: r => ({t:r.tingkat_kemiskinan,c:{'Sangat Miskin':'badge-red','Miskin':'badge-orange','Hampir Miskin':'badge-amber','Rentan':'badge-accent'}[r.tingkat_kemiskinan]||'badge-accent'}),
|
||||||
|
legend:[{l:'Sangat Miskin',clr:'#f05c5c',sym:'c'},{l:'Miskin',clr:'#f0a030',sym:'c'},{l:'Hampir Miskin',clr:'#f0c040',sym:'c'},{l:'Rentan',clr:'#5b9cf6',sym:'c'}],
|
||||||
|
bantuan:['PKH','BPNT/Sembako','BLT Dana Desa','BLT BBM','PIP/KIP','KIS/BPJS','Zakat/Infaq RI','Bansos Pemda','Belum Ada'],
|
||||||
|
kondisiRumah:['Permanen','Semi Permanen','Tidak Layak Huni','Menumpang'],
|
||||||
|
pekerjaan:['Buruh Harian','Petani/Nelayan','Pedagang Kecil','PNS/TNI/Polri','Pensiunan','Tidak Bekerja','Lainnya'],
|
||||||
|
statusPenerimaan:['Aktif Menerima','Belum Menerima','Proses Verifikasi','Pernah Menerima'],
|
||||||
|
sumberPendapatan:['Buruh Harian','Pedagang Kecil','Petani/Nelayan','PNS/TNI/Polri','Pensiunan','Tidak Bekerja','Lainnya'],
|
||||||
|
jenisPenyakit:['Hipertensi','Diabetes','TBC','Jantung','Stroke','Kanker','Gangguan Jiwa','Disabilitas','Tidak Ada'],
|
||||||
|
tingkatPendidikan:['Tidak Sekolah','SD/Sederajat','SMP/Sederajat','SMA/Sederajat','D3/S1','S2/S3'],
|
||||||
|
},
|
||||||
|
rumahIbadah: {
|
||||||
|
label:'Rumah Ibadah', sub:'Masjid, Gereja, Pura, Vihara, Klenteng', icon:'🕌', drawType:'point',
|
||||||
|
jenis:['Masjid','Musholla','Gereja Kristen','Gereja Katolik','Pura','Vihara','Klenteng'],
|
||||||
|
colors:{'Masjid':'#2ec4b6','Musholla':'#2ec4b6','Gereja Kristen':'#5b9cf6','Gereja Katolik':'#5b9cf6','Pura':'#f0a030','Vihara':'#9b7bfa','Klenteng':'#f07840'},
|
||||||
|
badgeClass:{'Masjid':'badge-teal','Musholla':'badge-teal','Gereja Kristen':'badge-accent','Gereja Katolik':'badge-accent','Pura':'badge-amber','Vihara':'badge-purple','Klenteng':'badge-orange'},
|
||||||
|
color: r => ({'Masjid':'#2ec4b6','Musholla':'#2ec4b6','Gereja Kristen':'#5b9cf6','Gereja Katolik':'#5b9cf6','Pura':'#f0a030','Vihara':'#9b7bfa','Klenteng':'#f07840'}[r.jenis]||'#888'),
|
||||||
|
badge: r => ({t:r.jenis,c:({'Masjid':'badge-teal','Musholla':'badge-teal','Gereja Kristen':'badge-accent','Gereja Katolik':'badge-accent','Pura':'badge-amber','Vihara':'badge-purple','Klenteng':'badge-orange'}[r.jenis]||'badge-teal')}),
|
||||||
|
legend:[{l:'Masjid/Musholla',clr:'#2ec4b6',sym:'c'},{l:'Gereja',clr:'#5b9cf6',sym:'c'},{l:'Pura',clr:'#f0a030',sym:'c'},{l:'Vihara/Klenteng',clr:'#9b7bfa',sym:'c'}],
|
||||||
|
coverageRadius: 300,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
12. DASHBOARD ANALITIK
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Dashboard = {
|
||||||
|
toggle(){
|
||||||
|
document.getElementById('dash-panel').classList.toggle('hidden');
|
||||||
|
if(!document.getElementById('dash-panel').classList.contains('hidden')) this.render();
|
||||||
|
},
|
||||||
|
render(){
|
||||||
|
const db=Store.allDb();
|
||||||
|
const warga=db.warga||[], ri=db.rumahIbadah||[], jalan=db.jalan||[], parsil=db.parsil||[];
|
||||||
|
const totalWarga=warga.reduce((s,r)=>s+(parseInt(r.jml_anggota)||1),0);
|
||||||
|
const miskinBerat=warga.filter(r=>r.tingkat_kemiskinan==='Sangat Miskin'||r.tingkat_kemiskinan==='Miskin').length;
|
||||||
|
const blmBansos=warga.filter(r=>r.bansos==='Belum Ada').length;
|
||||||
|
const blmSertif=parsil.filter(r=>r.status==='Belum Bersertifikat').length;
|
||||||
|
const jalanRusak=jalan.filter(r=>r.kondisi==='Rusak Berat'||r.kondisi==='Rusak Ringan').length;
|
||||||
|
const totalRI=ri.length;
|
||||||
|
|
||||||
|
// Radius coverage for Warga layer
|
||||||
|
let coveredKK=0, uncoveredKK=0;
|
||||||
|
const coverageHubs = ri.map(hub=>({ ...hub, count:0 }));
|
||||||
|
warga.forEach(b => {
|
||||||
|
if(!b.lat) return;
|
||||||
|
let nearestDist=Infinity, nearestHub=null;
|
||||||
|
ri.forEach(hub=>{
|
||||||
|
if(!hub.lat) return;
|
||||||
|
const d=L.latLng(b.lat,b.lng).distanceTo(L.latLng(hub.lat,hub.lng));
|
||||||
|
if(d<nearestDist){nearestDist=d;nearestHub=hub;}
|
||||||
|
});
|
||||||
|
if(nearestHub && nearestDist<=(parseFloat(nearestHub.radius)||300)){
|
||||||
|
coveredKK++;
|
||||||
|
const h = coverageHubs.find(h=>h.id===nearestHub.id);
|
||||||
|
if(h) h.count++;
|
||||||
|
} else {
|
||||||
|
uncoveredKK++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// tingkat kemiskinan breakdown
|
||||||
|
const tkBreak={};
|
||||||
|
CFG.warga.tingkat.forEach(t=>{ tkBreak[t]=warga.filter(r=>r.tingkat_kemiskinan===t).length; });
|
||||||
|
const maxTK=Math.max(...Object.values(tkBreak),1);
|
||||||
|
|
||||||
|
// bansos breakdown
|
||||||
|
const bansosBreak={};
|
||||||
|
CFG.warga.bantuan.forEach(b=>{ const c=warga.filter(r=>r.bansos===b).length; if(c>0) bansosBreak[b]=c; });
|
||||||
|
|
||||||
|
// jalan status breakdown
|
||||||
|
const jalanBreak={};
|
||||||
|
CFG.jalan.statuses.forEach(s=>{ jalanBreak[s]=jalan.filter(r=>r.status===s).length; });
|
||||||
|
|
||||||
|
document.getElementById('dash-body').innerHTML=`
|
||||||
|
<div class="dash-tab-bar">
|
||||||
|
<div class="dash-tab active" onclick="Dashboard.switchTab('ringkasan')">Ringkasan Utama</div>
|
||||||
|
<div class="dash-tab" onclick="Dashboard.switchTab('distribusi')">Distribusi Bansos</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB RINGKASAN -->
|
||||||
|
<div id="dash-panel-ringkasan">
|
||||||
|
<div class="stat-grid">
|
||||||
|
<div class="stat-card c-red"><div class="sc-val">${H.fmNum(warga.length)}</div><div class="sc-label">KK Warga Miskin Terdata</div><div class="sc-sub">${H.fmNum(totalWarga)} jiwa total</div></div>
|
||||||
|
<div class="stat-card c-amber"><div class="sc-val">${H.fmNum(miskinBerat)}</div><div class="sc-label">Miskin & Sangat Miskin</div><div class="sc-sub">${warga.length?Math.round(miskinBerat/warga.length*100):0}% dari total KK</div></div>
|
||||||
|
<div class="stat-card c-accent"><div class="sc-val">${H.fmNum(blmBansos)}</div><div class="sc-label">KK Belum Terima Bansos</div><div class="sc-sub">Perlu intervensi segera</div></div>
|
||||||
|
<div class="stat-card c-teal"><div class="sc-val">${H.fmNum(totalRI)}</div><div class="sc-label">Rumah Ibadah Aktif</div><div class="sc-sub">Agen lapangan terdaftar</div></div>
|
||||||
|
<div class="stat-card c-purple"><div class="sc-val">${H.fmNum(blmSertif)}</div><div class="sc-label">Parsil Belum Bersertifikat</div><div class="sc-sub">Kandidat program PTSL</div></div>
|
||||||
|
<div class="stat-card c-green"><div class="sc-val">${H.fmNum(jalanRusak)}</div><div class="sc-label">Ruas Jalan Rusak</div><div class="sc-sub">Dari ${H.fmNum(jalan.length)} ruas terdata</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dash-sect">
|
||||||
|
<div class="dash-sect-title">Sebaran Tingkat Kemiskinan</div>
|
||||||
|
<table class="risk-table"><thead><tr><th>Kategori</th><th>Jumlah KK</th><th>Proporsi</th></tr></thead><tbody>
|
||||||
|
${CFG.warga.tingkat.map(t=>{
|
||||||
|
const n=tkBreak[t]||0, pct=warga.length?Math.round(n/warga.length*100):0;
|
||||||
|
const clr={'Sangat Miskin':'#f05c5c','Miskin':'#f0a030','Hampir Miskin':'#f0c040','Rentan':'#5b9cf6'}[t];
|
||||||
|
return \`<tr><td>\${t}</td><td>\${H.fmNum(n)} KK</td><td><div class="risk-bar-wrap"><div class="risk-bar"><div class="risk-bar-fill" style="width:\${Math.round(n/maxTK*100)}%;background:\${clr}"></div></div>\${pct}%</div></td></tr>\`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody></table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dash-sect">
|
||||||
|
<div class="dash-sect-title">Penerima Program Bantuan Sosial</div>
|
||||||
|
${Object.entries(bansosBreak).length?Object.entries(bansosBreak).sort((a,b)=>b[1]-a[1]).map(([b,n])=>`
|
||||||
|
<div class="prog-row"><span class="prog-name">${b}</span><span class="prog-count">${n} KK</span></div>`).join('')
|
||||||
|
:'<div style="color:var(--text4);font-size:12px">Belum ada data warga tercatat.</div>'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dash-sect">
|
||||||
|
<div class="dash-sect-title">Status Ruas Jalan</div>
|
||||||
|
<table class="risk-table"><thead><tr><th>Status</th><th>Jumlah Ruas</th><th>Total Panjang</th></tr></thead><tbody>
|
||||||
|
${CFG.jalan.statuses.map(s=>{
|
||||||
|
const ruas=jalan.filter(r=>r.status===s);
|
||||||
|
const pj=ruas.reduce((t,r)=>t+(r.panjang||0),0);
|
||||||
|
return \`<tr><td><span class="badge \${CFG.jalan.badgeClass[s]}">\${s}</span></td><td>\${ruas.length} ruas</td><td>\${H.fmDist(pj)}</td></tr>\`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody></table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dash-sect">
|
||||||
|
<div class="dash-sect-title">Rekomendasi Intervensi Kebijakan</div>
|
||||||
|
<div class="intervensi-grid">
|
||||||
|
<div class="intervensi-card"><div class="ic-label">🏠 Prioritas Bansos</div>
|
||||||
|
<div class="ic-val">${H.fmNum(blmBansos)} KK</div>
|
||||||
|
<div class="ic-sub">Warga belum menerima bantuan apapun</div></div>
|
||||||
|
<div class="intervensi-card"><div class="ic-label">📜 Prioritas PTSL</div>
|
||||||
|
<div class="ic-val">${H.fmNum(blmSertif)} Parsil</div>
|
||||||
|
<div class="ic-sub">Lahan tanpa sertifikat, akses kredit terbatas</div></div>
|
||||||
|
<div class="intervensi-card"><div class="ic-label">🛣️ Prioritas Perbaikan Jalan</div>
|
||||||
|
<div class="ic-val">${H.fmNum(jalanRusak)} Ruas</div>
|
||||||
|
<div class="ic-sub">Jalan rusak ringan-berat perlu perbaikan</div></div>
|
||||||
|
<div class="intervensi-card"><div class="ic-label">🕌 Coverage Rumah Ibadah</div>
|
||||||
|
<div class="ic-val">${H.fmNum(totalRI)} RI Aktif</div>
|
||||||
|
<div class="ic-sub">Total dana sosial: Rp ${H.fmNum(ri.reduce((t,r)=>t+(parseInt(r.dana_sosial)||0),0))}</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB DISTRIBUSI BANSOS -->
|
||||||
|
<div id="dash-panel-distribusi" style="display:none">
|
||||||
|
<div class="coverage-grid">
|
||||||
|
<div class="coverage-card">
|
||||||
|
<div class="cc-icon green">✓</div>
|
||||||
|
<div class="cc-info">
|
||||||
|
<div class="cc-val">${coveredKK} KK</div>
|
||||||
|
<div class="cc-label">Terjangkau Hub (≤300m)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="coverage-card">
|
||||||
|
<div class="cc-icon red">✕</div>
|
||||||
|
<div class="cc-info">
|
||||||
|
<div class="cc-val">${uncoveredKK} KK</div>
|
||||||
|
<div class="cc-label">Di Luar Jangkauan Hub</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="coverage-card" style="grid-column:1/3;padding:16px">
|
||||||
|
<div class="cc-icon teal" style="width:40px;height:40px">🕌</div>
|
||||||
|
<div class="cc-info">
|
||||||
|
<div class="cc-val">${totalRI} Rumah Ibadah</div>
|
||||||
|
<div class="cc-label">Agen Distribusi Lapangan Terdaftar</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dash-sect">
|
||||||
|
<div class="dash-sect-title">Beban Distribusi per Rumah Ibadah</div>
|
||||||
|
<table class="hub-table">
|
||||||
|
<thead><tr><th>Nama Hub</th><th>Kapasitas</th><th>Beban KK</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
${coverageHubs.sort((a,b)=>b.count-a.count).map(h=>{
|
||||||
|
const clr=CFG.rumahIbadah.color(h);
|
||||||
|
const max = Math.max(...coverageHubs.map(x=>x.count), 1);
|
||||||
|
const pct = Math.round(h.count/max*100);
|
||||||
|
return \`<tr>
|
||||||
|
<td><div class="hub-name-cell"><span class="hub-dot" style="background:\${clr}"></span>\${H.esc(h.nama)}</div></td>
|
||||||
|
<td>\${h.jml_binaan||'-'} KK</td>
|
||||||
|
<td>
|
||||||
|
<div class="hub-count-bar">
|
||||||
|
<div class="hub-bar"><div class="hub-bar-fill" style="width:\${pct}%;background:\${pct>80?'var(--amber)':clr}"></div></div>
|
||||||
|
<span>\${h.count} KK</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>\`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
${coverageHubs.length===0?'<div style="text-align:center;color:var(--text4);font-size:11px;padding:20px">Belum ada data Rumah Ibadah.</div>':''}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
switchTab(tabId){
|
||||||
|
document.querySelectorAll('.dash-tab').forEach(t=>t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('[id^="dash-panel-"]').forEach(p=>p.style.display='none');
|
||||||
|
|
||||||
|
if(tabId === 'ringkasan'){
|
||||||
|
document.querySelectorAll('.dash-tab')[0].classList.add('active');
|
||||||
|
document.getElementById('dash-panel-ringkasan').style.display='block';
|
||||||
|
} else {
|
||||||
|
document.querySelectorAll('.dash-tab')[1].classList.add('active');
|
||||||
|
document.getElementById('dash-panel-distribusi').style.display='block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
8. DRAW MANAGER
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Draw = (() => {
|
||||||
|
let _map, _drawn, _pending, _handler, _active=false;
|
||||||
|
const BAR=()=>document.getElementById('draw-bar');
|
||||||
|
const TXT=()=>document.getElementById('draw-indicator-text');
|
||||||
|
|
||||||
|
function init(m){
|
||||||
|
_map=m; _drawn=new L.FeatureGroup().addTo(_map);
|
||||||
|
_map.on(L.Draw.Event.CREATED, e=>{
|
||||||
|
_drawn.clearLayers(); _drawn.addLayer(e.layer);
|
||||||
|
_pending=e.layer; stop(); Modal.openAdd(App.state.activeLayer, e.layer);
|
||||||
|
});
|
||||||
|
_map.on('click', e=>{
|
||||||
|
if(!_active||App.state.activeLayer!=='spbu'&&App.state.activeLayer!=='warga'&&App.state.activeLayer!=='rumahIbadah') return;
|
||||||
|
const mk=L.marker(e.latlng);
|
||||||
|
_drawn.clearLayers(); _drawn.addLayer(mk);
|
||||||
|
_pending=mk; stop(); Modal.openAdd(App.state.activeLayer, mk);
|
||||||
|
});
|
||||||
|
document.getElementById('btn-cancel').addEventListener('click', cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function start(ln){
|
||||||
|
_active=true;
|
||||||
|
const msgs={spbu:'Klik peta untuk menandai lokasi SPBU',jalan:'Klik titik-titik ruas jalan — klik ganda untuk selesai',parsil:'Klik titik-titik batas kavling — klik ganda untuk selesai',warga:'Klik peta untuk menandai lokasi rumah warga miskin',rumahIbadah:'Klik peta untuk menandai lokasi rumah ibadah'};
|
||||||
|
document.getElementById('draw-text').textContent=msgs[ln]||'Mode gambar aktif';
|
||||||
|
BAR().classList.remove('hidden');
|
||||||
|
if(ln==='jalan'){ _handler=new L.Draw.Polyline(_map,{shapeOptions:{color:'#f0a030',weight:4}}); _handler.enable(); }
|
||||||
|
else if(ln==='parsil'){ _handler=new L.Draw.Polygon(_map,{shapeOptions:{color:'#9b7bfa',weight:2,fillOpacity:.2}}); _handler.enable(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop(){ _active=false; BAR().classList.add('hidden'); if(_handler){_handler.disable();_handler=null;} }
|
||||||
|
function cancel(){ stop(); _drawn.clearLayers(); _pending=null; }
|
||||||
|
function clearPending(){ _drawn.clearLayers(); _pending=null; }
|
||||||
|
|
||||||
|
return {init, start, stop, cancel, clearPending};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
13. MAP RENDER
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
function renderMap(){
|
||||||
|
const db=Store.allDb();
|
||||||
|
// SPBU
|
||||||
|
App._groups.spbu.clearLayers();
|
||||||
|
db.spbu.forEach(r=>{
|
||||||
|
const m=L.circleMarker([r.lat,r.lng],{radius:9,fillColor:CFG.spbu.color(r),color:'#fff',weight:2,fillOpacity:1});
|
||||||
|
m.bindPopup(Popup.spbu(r),{maxWidth:320});
|
||||||
|
m.on('click',()=>highlightCard(r.id));
|
||||||
|
App._groups.spbu.addLayer(m);
|
||||||
|
});
|
||||||
|
// Warga
|
||||||
|
App._groups.warga.clearLayers();
|
||||||
|
db.warga.forEach(r=>{
|
||||||
|
const clr=CFG.warga.color(r);
|
||||||
|
// Check if in any RI radius
|
||||||
|
const inRadius=db.rumahIbadah.some(hub=>{
|
||||||
|
if(!hub.lat) return false;
|
||||||
|
const d=L.latLng(r.lat,r.lng).distanceTo(L.latLng(hub.lat,hub.lng));
|
||||||
|
return d<=(parseFloat(hub.radius)||300);
|
||||||
|
});
|
||||||
|
const iconHtml=`<div style="width:22px;height:22px;background:${clr};border:2px solid #fff;border-radius:50%;
|
||||||
|
display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,.4);
|
||||||
|
font-size:11px;line-height:1">👨👩👧</div>${inRadius?'':'<div style="position:absolute;top:-4px;right:-4px;width:10px;height:10px;background:#f05c5c;border-radius:50%;border:1px solid #fff"></div>'}`;
|
||||||
|
const icon=L.divIcon({html:`<div style="position:relative">${iconHtml}</div>`,className:'',iconSize:[22,22],iconAnchor:[11,11]});
|
||||||
|
const m=L.marker([r.lat,r.lng],{icon});
|
||||||
|
m.bindPopup(Popup.warga(r),{maxWidth:360});
|
||||||
|
m.on('click',()=>highlightCard(r.id));
|
||||||
|
App._groups.warga.addLayer(m);
|
||||||
|
});
|
||||||
|
// Rumah Ibadah
|
||||||
|
App._groups.rumahIbadah.clearLayers();
|
||||||
|
db.rumahIbadah.forEach(r=>{
|
||||||
|
const clr=CFG.rumahIbadah.color(r);
|
||||||
|
const m=L.circleMarker([r.lat,r.lng],{radius:10,fillColor:clr,color:'#fff',weight:2.5,fillOpacity:1});
|
||||||
|
m.bindPopup(Popup.rumahIbadah(r),{maxWidth:320});
|
||||||
|
m.on('click',()=>highlightCard(r.id));
|
||||||
|
App._groups.rumahIbadah.addLayer(m);
|
||||||
|
// Always draw radius 300m coverage circle
|
||||||
|
const radiusM=parseFloat(r.radius)||300;
|
||||||
|
const ring=L.circle([r.lat,r.lng],{radius:radiusM,color:clr,weight:1.5,fillColor:clr,fillOpacity:.06,dashArray:'6 4'});
|
||||||
|
App._groups.rumahIbadah.addLayer(ring);
|
||||||
|
});
|
||||||
|
// Jalan
|
||||||
|
App._groups.jalan.clearLayers();
|
||||||
|
db.jalan.forEach(r=>{
|
||||||
|
const l=L.polyline(r.coords,{color:CFG.jalan.color(r),weight:4,opacity:.9});
|
||||||
|
l.bindPopup(Popup.jalan(r),{maxWidth:300});
|
||||||
|
l.on('click',()=>highlightCard(r.id));
|
||||||
|
App._groups.jalan.addLayer(l);
|
||||||
|
});
|
||||||
|
// Parsil
|
||||||
|
App._groups.parsil.clearLayers();
|
||||||
|
db.parsil.forEach(r=>{
|
||||||
|
const clr=CFG.parsil.color(r);
|
||||||
|
const p=L.polygon(r.coords,{color:clr,weight:2,fillColor:clr,fillOpacity:.2});
|
||||||
|
p.bindPopup(Popup.parsil(r),{maxWidth:300});
|
||||||
|
p.on('click',()=>highlightCard(r.id));
|
||||||
|
App._groups.parsil.addLayer(p);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
9. MODAL MANAGER
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Modal = (() => {
|
||||||
|
let _ln=null, _editId=null, _geo=null;
|
||||||
|
const ov=()=>document.getElementById('modal-ov');
|
||||||
|
|
||||||
|
function openAdd(ln, leafletLayer){
|
||||||
|
_ln=ln; _editId=null; _geo=leafletLayer;
|
||||||
|
const cfg=CFG[ln];
|
||||||
|
document.getElementById('modal-icon').textContent=cfg.icon;
|
||||||
|
document.getElementById('modal-title').textContent='Tambah '+cfg.label;
|
||||||
|
document.getElementById('modal-sub').textContent='Isi atribut data baru';
|
||||||
|
let html='';
|
||||||
|
if(ln==='spbu') html=Forms.spbu(null, leafletLayer.getLatLng());
|
||||||
|
else if(ln==='jalan') html=Forms.jalan(null, H.geoLen(leafletLayer.getLatLngs()));
|
||||||
|
else if(ln==='parsil') html=Forms.parsil(null, H.geoArea(leafletLayer.getLatLngs()[0]));
|
||||||
|
else if(ln==='warga') html=Forms.warga(null, leafletLayer.getLatLng());
|
||||||
|
else if(ln==='rumahIbadah') html=Forms.rumahIbadah(null, leafletLayer.getLatLng());
|
||||||
|
document.getElementById('modal-body').innerHTML=html;
|
||||||
|
_bindRadio(); _bindFormTabs(); _bindHistBtn(); ov().classList.remove('hidden');
|
||||||
|
setTimeout(()=>document.getElementById('f-nama')?.focus(),60);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(ln, id){
|
||||||
|
_ln=ln; _editId=id; _geo=null;
|
||||||
|
const rec=Store.find(ln,id); if(!rec) return;
|
||||||
|
const cfg=CFG[ln];
|
||||||
|
document.getElementById('modal-icon').textContent=cfg.icon;
|
||||||
|
document.getElementById('modal-title').textContent='Edit '+cfg.label;
|
||||||
|
document.getElementById('modal-sub').textContent=H.esc(rec.nama);
|
||||||
|
let html='';
|
||||||
|
if(ln==='spbu') html=Forms.spbu(rec,null);
|
||||||
|
else if(ln==='jalan') html=Forms.jalan(rec,null);
|
||||||
|
else if(ln==='parsil') html=Forms.parsil(rec,null);
|
||||||
|
else if(ln==='warga') html=Forms.warga(rec,null);
|
||||||
|
else if(ln==='rumahIbadah') html=Forms.rumahIbadah(rec,null);
|
||||||
|
document.getElementById('modal-body').innerHTML=html;
|
||||||
|
_bindRadio(); _bindFormTabs(); _bindHistBtn(); ov().classList.remove('hidden');
|
||||||
|
setTimeout(()=>document.getElementById('f-nama')?.focus(),60);
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(){ ov().classList.add('hidden'); Draw.clearPending(); }
|
||||||
|
|
||||||
|
async function submit(){
|
||||||
|
const data=Forms.readAll(_ln); if(!data) return;
|
||||||
|
if(_editId){
|
||||||
|
data.updatedAt=H.today();
|
||||||
|
await Store.update(_ln,_editId,data);
|
||||||
|
Toast.success('Data berhasil diperbarui');
|
||||||
|
} else {
|
||||||
|
const geo=_geo;
|
||||||
|
if(_ln==='spbu'||_ln==='warga'||_ln==='rumahIbadah'){
|
||||||
|
const ll=geo.getLatLng(); data.lat=ll.lat; data.lng=ll.lng;
|
||||||
|
} else if(_ln==='jalan'){
|
||||||
|
data.coords=geo.getLatLngs().map(l=>[l.lat,l.lng]);
|
||||||
|
} else if(_ln==='parsil'){
|
||||||
|
data.coords=geo.getLatLngs()[0].map(l=>[l.lat,l.lng]);
|
||||||
|
}
|
||||||
|
data.id=H.uid(); data.createdAt=H.today(); data.updatedAt=H.today();
|
||||||
|
await Store.add(_ln,data);
|
||||||
|
Toast.success('Data berhasil disimpan');
|
||||||
|
}
|
||||||
|
close(); App.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bindRadio(){
|
||||||
|
document.querySelectorAll('.ropt').forEach(el=>{
|
||||||
|
el.addEventListener('click',()=>{
|
||||||
|
const parent=el.closest('.rtoggle'); if(!parent) return;
|
||||||
|
parent.querySelectorAll('.ropt').forEach(r=>r.classList.remove('yes','no'));
|
||||||
|
const v=el.dataset.v; el.classList.add(v==='true'?'yes':'no');
|
||||||
|
const inp=el.querySelector('input'); if(inp) inp.checked=true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Bind penyakit checkboxes
|
||||||
|
document.querySelectorAll('.penyakit-check input').forEach(inp=>{
|
||||||
|
inp.addEventListener('change', e=>{
|
||||||
|
if(e.target.checked) e.target.parentElement.classList.add('checked');
|
||||||
|
else e.target.parentElement.classList.remove('checked');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bindFormTabs(){
|
||||||
|
const tabs=document.querySelectorAll('.form-tab');
|
||||||
|
if(!tabs.length) return;
|
||||||
|
tabs.forEach(tab=>{
|
||||||
|
tab.addEventListener('click',()=>{
|
||||||
|
tabs.forEach(t=>t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.form-section-panel').forEach(p=>p.classList.remove('active'));
|
||||||
|
tab.classList.add('active');
|
||||||
|
const panelId='panel-'+tab.dataset.panel;
|
||||||
|
document.getElementById(panelId)?.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bindHistBtn(){
|
||||||
|
const btn=document.getElementById('btn-add-hist');
|
||||||
|
if(!btn) return;
|
||||||
|
btn.addEventListener('click',()=>{
|
||||||
|
const tbody=document.getElementById('hist-tbody');
|
||||||
|
if(!tbody) return;
|
||||||
|
const tr=document.createElement('tr');
|
||||||
|
tr.innerHTML=`
|
||||||
|
<td><input class="finput hist-program" style="padding:3px 6px;font-size:10px" placeholder="cth: PKH"/></td>
|
||||||
|
<td><input class="finput hist-tahun" style="padding:3px 6px;font-size:10px;width:70px" type="number" min="2000" max="2030" placeholder="${new Date().getFullYear()}"/></td>
|
||||||
|
<td><select class="finput hist-status" style="padding:3px 6px;font-size:10px">
|
||||||
|
<option>Aktif</option><option>Berhenti</option><option>Dalam Proses</option>
|
||||||
|
</select></td>
|
||||||
|
<td><input class="finput hist-nominal" style="padding:3px 6px;font-size:10px" type="number" placeholder="Rp"/></td>
|
||||||
|
<td><button class="btn-del-row" type="button" onclick="this.closest('tr').remove()">✕</button></td>`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {openAdd, openEdit, close, submit};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
11. SIDEBAR
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
function renderSidebar(ln){
|
||||||
|
const cfg=CFG[ln];
|
||||||
|
document.getElementById('sb-title').textContent=cfg.label;
|
||||||
|
document.getElementById('sb-sub').textContent=cfg.sub;
|
||||||
|
const data=Store.all(ln);
|
||||||
|
const list=document.getElementById('sb-list');
|
||||||
|
if(!data.length){
|
||||||
|
list.innerHTML=`<div class="empty"><svg width="44" height="44" viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg><p>Belum ada data.<br/>Klik <strong>Tambah</strong> untuk mulai digitasi.</p></div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.innerHTML=data.map(r=>Card[ln](r)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBadges(){
|
||||||
|
const db=Store.allDb();
|
||||||
|
['spbu','jalan','parsil','warga','rumahIbadah'].forEach(k=>{
|
||||||
|
const el=document.getElementById('badge-'+k);
|
||||||
|
if(el) el.textContent=db[k]?.length||0;
|
||||||
|
});
|
||||||
|
const total=Object.values(db).reduce((s,a)=>s+a.length,0);
|
||||||
|
document.getElementById('total-count').textContent=total+' fitur';
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightCard(id){
|
||||||
|
document.querySelectorAll('.dcard').forEach(c=>c.classList.remove('active'));
|
||||||
|
const c=document.getElementById('card-'+id);
|
||||||
|
if(c){ c.classList.add('active'); c.scrollIntoView({behavior:'smooth',block:'nearest'}); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
'use strict';
|
||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
1. HELPERS
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const H = {
|
||||||
|
uid: () => Math.random().toString(36).slice(2,9) + Date.now().toString(36),
|
||||||
|
esc: s => String(s||'').replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])),
|
||||||
|
fmDist: m => m==null?'-': m>=1000? (m/1000).toFixed(2)+' km': Math.round(m).toLocaleString('id-ID')+' m',
|
||||||
|
fmArea: m => m==null?'-': m>=10000? (m/10000).toFixed(4)+' ha': Math.round(m).toLocaleString('id-ID')+' m²',
|
||||||
|
fmNum: n => (n||0).toLocaleString('id-ID'),
|
||||||
|
today: () => new Date().toISOString().slice(0,10),
|
||||||
|
geoLen: lls => { let t=0; for(let i=0;i<lls.length-1;i++) t+=lls[i].distanceTo(lls[i+1]); return Math.round(t); },
|
||||||
|
geoArea: lls => {
|
||||||
|
const R=6371008.8; let a=0,n=lls.length;
|
||||||
|
for(let i=0;i<n;i++){const j=(i+1)%n,φ1=lls[i].lat*Math.PI/180,φ2=lls[j].lat*Math.PI/180,dλ=(lls[j].lng-lls[i].lng)*Math.PI/180;a+=dλ*(2+Math.sin(φ1)+Math.sin(φ2));}
|
||||||
|
return Math.round(Math.abs(a*R*R/2));
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
2. STORAGE (MySQL API Version)
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Store = (() => {
|
||||||
|
const API_URL = '../api/index.php';
|
||||||
|
|
||||||
|
let db = {
|
||||||
|
spbu: [],
|
||||||
|
jalan: [],
|
||||||
|
parsil: [],
|
||||||
|
warga: [],
|
||||||
|
rumahIbadah: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = async (action, payload = {}) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_URL}?action=${action}`, {
|
||||||
|
method: action === 'alldb' ? 'GET' : 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: action === 'alldb' ? null : JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
return await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('API Error:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
load: async () => {
|
||||||
|
try {
|
||||||
|
const data = await request('alldb');
|
||||||
|
if (data) db = { ...db, ...data };
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Gagal load data dari server", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
all: ln => db[ln]||[],
|
||||||
|
find: (ln,id) => (db[ln]||[]).find(r=>r.id===id)||null,
|
||||||
|
add: async (ln,rec) => {
|
||||||
|
db[ln].push(rec);
|
||||||
|
await request('add', { layer: ln, data: rec });
|
||||||
|
},
|
||||||
|
update: async (ln,id,patch) => {
|
||||||
|
const i=db[ln].findIndex(r=>r.id===id); if(i<0)return;
|
||||||
|
db[ln][i]={...db[ln][i],...patch};
|
||||||
|
await request('update', { layer: ln, id: id, patch: patch });
|
||||||
|
},
|
||||||
|
remove: async (ln,id) => {
|
||||||
|
db[ln]=db[ln].filter(r=>r.id!==id);
|
||||||
|
await request('remove', { layer: ln, id: id });
|
||||||
|
},
|
||||||
|
allDb: () => db,
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
3. TOAST
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Toast = {
|
||||||
|
_show(msg,type){ const c=document.getElementById('toasts'),el=document.createElement('div');
|
||||||
|
el.className=`toast ${type}`;el.innerHTML=`<span>${{success:'✓',error:'✕',info:'ℹ'}[type]||'ℹ'}</span>${H.esc(msg)}`;
|
||||||
|
c.appendChild(el);setTimeout(()=>{el.classList.add('out');el.addEventListener('animationend',()=>el.remove(),{once:true})},2800); },
|
||||||
|
success: m=>Toast._show(m,'success'), error: m=>Toast._show(m,'error'), info: m=>Toast._show(m,'info'),
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<script src="app.js"></script>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# WebGIS – Sistem Distribusi Bansos Berbasis Rumah Ibadah
|
||||||
|
|
||||||
|
> Aplikasi pemetaan geospasial (*WebGIS*) interaktif untuk pendataan infrastruktur, profil warga rentan, dan perencanaan distribusi Bantuan Sosial (Bansos) berbasis jangkauan Rumah Ibadah.
|
||||||
|
|
||||||
|
Aplikasi ini menggunakan pendekatan **radius spasial 300 meter** untuk secara otomatis mendeteksi apakah seorang warga masuk dalam jangkauan agen distribusi (Rumah Ibadah) atau tidak — membantu pemerintah desa/kelurahan dalam pengambilan keputusan yang akurat dan berbasis data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 Fitur Utama
|
||||||
|
|
||||||
|
| No | Fitur | Tipe Layer | Deskripsi |
|
||||||
|
|----|-------|-----------|-----------|
|
||||||
|
| 1 | **Warga & Bansos** | Point | Profil lengkap KK (ekonomi, kesehatan, pendidikan) + riwayat bansos |
|
||||||
|
| 2 | **Rumah Ibadah** | Point + Buffer | Titik *hub* distribusi bansos; lingkaran jangkauan 300m otomatis |
|
||||||
|
| 3 | **SPBU** | Point | Ketersediaan BBM & aksesibilitas |
|
||||||
|
| 4 | **Jalan** | Polyline | Status, kondisi, dan dampak sosial ruas jalan |
|
||||||
|
| 5 | **Parsil Tanah** | Polygon | Status kepemilikan sertifikat tanah |
|
||||||
|
| 6 | **Dashboard Analitik** | — | Ringkasan kemiskinan, distribusi bansos, dan rekomendasi kebijakan |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂️ Data yang Dikumpulkan
|
||||||
|
|
||||||
|
### 👨👩👧 Warga & Bansos
|
||||||
|
Data yang dikumpulkan mencakup 5 dimensi:
|
||||||
|
- **Data KK:** Nama, NIK, No. KK, jumlah anggota, alamat, tingkat kemiskinan
|
||||||
|
- **Ekonomi:** Pendapatan, pekerjaan, kondisi rumah, aset, air bersih, listrik
|
||||||
|
- **Pendidikan:** Tingkat pendidikan, jumlah anak sekolah/putus sekolah, penerima KIP
|
||||||
|
- **Kesehatan:** Kepesertaan BPJS, jenis penyakit kronis, anggota difabel
|
||||||
|
- **Riwayat Bansos:** Program (PKH, BPNT, BLT, dll), tahun, dan status penerimaan
|
||||||
|
|
||||||
|
### 🕌 Rumah Ibadah
|
||||||
|
Nama, jenis (Masjid/Gereja/Pura/dll), pengurus, kapasitas jamaah, radius binaan, program sosial aktif, dan alokasi dana sosial per bulan.
|
||||||
|
|
||||||
|
### ⛽ SPBU
|
||||||
|
Nama, jam operasional (24 jam / tidak), jenis BBM, harga, dan aksesibilitas disabilitas.
|
||||||
|
|
||||||
|
### 🛣️ Jalan
|
||||||
|
Nama ruas, status (Nasional/Provinsi/Kabupaten/Desa), kondisi, lebar, tahun perbaikan, dan dampak sosial.
|
||||||
|
|
||||||
|
### 🏗️ Parsil Tanah
|
||||||
|
Nama, NIB, status sertifikat (SHM/HGB/HGU/dll), NJOP, penggunaan lahan, dan status sengketa.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Arsitektur & Struktur Folder
|
||||||
|
|
||||||
|
Aplikasi ini dibangun menggunakan arsitektur **modular Vanilla JavaScript** tanpa *build tool* atau *framework*, sehingga bisa langsung dijalankan di browser.
|
||||||
|
|
||||||
|
```text
|
||||||
|
webgis/
|
||||||
|
├── index.html # Halaman utama & markup HTML
|
||||||
|
├── README.md # Dokumentasi proyek ini
|
||||||
|
├── css/
|
||||||
|
│ └── style.css # Seluruh styling & tema UI
|
||||||
|
└── js/
|
||||||
|
├── app.js # Root: Inisialisasi & event binding utama
|
||||||
|
├── config/
|
||||||
|
│ └── config.js # CFG: Kontrak data (warna, label, atribut tiap layer)
|
||||||
|
├── utils/
|
||||||
|
│ ├── helpers.js # H: Utilitas (uid, format angka/jarak/luas, geometri)
|
||||||
|
│ ├── store.js # Store: Baca/tulis LocalStorage
|
||||||
|
│ └── toast.js # Toast: Notifikasi UI (sukses/error/info)
|
||||||
|
├── components/
|
||||||
|
│ ├── forms.js # Forms: Generator HTML form input per layer
|
||||||
|
│ ├── popup.js # Popup: Generator HTML popup Leaflet per layer
|
||||||
|
│ ├── card.js # Card: Generator HTML item daftar di sidebar
|
||||||
|
│ └── legend.js # renderLegend: Fungsi render legenda peta
|
||||||
|
└── modules/
|
||||||
|
├── map.js # renderMap: Render semua marker/layer ke peta
|
||||||
|
├── draw.js # Draw: Integrasi Leaflet Draw (titik, garis, poligon)
|
||||||
|
├── modal.js # Modal: Manajemen pop-up form tambah/edit
|
||||||
|
├── sidebar.js # renderSidebar, updateBadges, highlightCard
|
||||||
|
└── dashboard.js # Dashboard: Kalkulasi analitik & render statistik
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💾 Penyimpanan Data (LocalStorage)
|
||||||
|
|
||||||
|
Semua data tersimpan di *LocalStorage* browser Anda menggunakan key berikut:
|
||||||
|
|
||||||
|
| Layer | LocalStorage Key |
|
||||||
|
|-------|-----------------|
|
||||||
|
| SPBU | `wg2_spbu` |
|
||||||
|
| Jalan | `wg2_jalan` |
|
||||||
|
| Parsil Tanah | `wg2_parsil` |
|
||||||
|
| Warga & Bansos | `wg2_warga` |
|
||||||
|
| Rumah Ibadah | `wg2_ri` |
|
||||||
|
|
||||||
|
> **Catatan Penting:** Data tersimpan di browser lokal Anda. Untuk memindahkan data ke perangkat lain, gunakan fitur *Export/Import* browser atau salin nilai dari DevTools > Application > LocalStorage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Cara Menjalankan
|
||||||
|
|
||||||
|
Tidak perlu instalasi apapun! Cukup:
|
||||||
|
|
||||||
|
1. Unduh atau *clone* repositori ini:
|
||||||
|
```bash
|
||||||
|
git clone <url-repo>
|
||||||
|
```
|
||||||
|
2. Buka folder `webgis/`.
|
||||||
|
3. Klik dua kali pada **`index.html`** untuk membukanya langsung di browser.
|
||||||
|
4. Aplikasi siap digunakan. ✅
|
||||||
|
|
||||||
|
> Direkomendasikan menggunakan **Google Chrome** atau **Mozilla Firefox** versi terbaru.
|
||||||
|
> Anda juga bisa menggunakan ekstensi **Live Server** di VSCode untuk pengalaman pengembangan yang lebih baik.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗺️ Panduan Penggunaan
|
||||||
|
|
||||||
|
### Menambah Data Baru
|
||||||
|
1. Pilih layer yang ingin diisi melalui **navigasi tab** di bagian atas (SPBU, Jalan, Parsil, Warga, Rumah Ibadah).
|
||||||
|
2. Klik tombol **`+ Tambah`** di sidebar kiri.
|
||||||
|
3. **Klik pada peta** untuk menandai lokasi (untuk Point), atau **gambar garis/poligon** (untuk Jalan/Parsil).
|
||||||
|
4. Isi formulir atribut yang muncul, lalu klik **Simpan Data**.
|
||||||
|
|
||||||
|
### Mengedit atau Menghapus Data
|
||||||
|
- Klik marker/layer di peta → Popup muncul → Klik **Edit** atau **Hapus**.
|
||||||
|
- Atau klik kartu di sidebar → gunakan tombol **Edit** / **Hapus** / **Zoom**.
|
||||||
|
|
||||||
|
### Melihat Dashboard Analitik
|
||||||
|
- Klik tombol **📊 Dashboard Analitik** di topbar kanan.
|
||||||
|
- Tab **Ringkasan Utama**: Statistik jumlah KK, distribusi kemiskinan, status jalan.
|
||||||
|
- Tab **Distribusi Bansos**: Peta panas jangkauan Rumah Ibadah, beban distribusi per *hub*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Teknologi yang Digunakan
|
||||||
|
|
||||||
|
| Teknologi | Versi | Fungsi |
|
||||||
|
|-----------|-------|--------|
|
||||||
|
| HTML5 & CSS3 | — | Struktur & tampilan UI |
|
||||||
|
| Vanilla JavaScript | ES6+ | Logika aplikasi & state management |
|
||||||
|
| [Leaflet.js](https://leafletjs.com/) | 1.9.4 | Render peta interaktif |
|
||||||
|
| [Leaflet Draw](https://github.com/Leaflet/Leaflet.draw) | 1.0.4 | Digitasi titik, garis, dan poligon |
|
||||||
|
| [DM Sans & DM Mono](https://fonts.google.com/) | — | Tipografi UI |
|
||||||
|
| LocalStorage API | — | Basis data *client-side* |
|
||||||
|
| OpenStreetMap | — | Tile peta dasar |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Panduan Modifikasi Kode
|
||||||
|
|
||||||
|
| Ingin mengubah... | Buka file... |
|
||||||
|
|-------------------|--------------|
|
||||||
|
| Warna marker atau label layer | [`js/config/config.js`](js/config/config.js) |
|
||||||
|
| Isian form input | [`js/components/forms.js`](js/components/forms.js) |
|
||||||
|
| Tampilan popup peta | [`js/components/popup.js`](js/components/popup.js) |
|
||||||
|
| Tampilan kartu di sidebar | [`js/components/card.js`](js/components/card.js) |
|
||||||
|
| Logika & tampilan dashboard | [`js/modules/dashboard.js`](js/modules/dashboard.js) |
|
||||||
|
| Cara layer dirender di peta | [`js/modules/map.js`](js/modules/map.js) |
|
||||||
|
| Tema / warna UI global | [`css/style.css`](css/style.css) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Lisensi
|
||||||
|
|
||||||
|
Proyek ini dibuat untuk keperluan akademik mata kuliah **Sistem Informasi Geografis (SIG)**. Bebas digunakan dan dimodifikasi untuk keperluan non-komersial.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dikembangkan dengan ❤️ untuk mendukung pemerataan distribusi Bantuan Sosial berbasis data geospasial.*
|
||||||
Submodule
+1
Submodule pertemuan_3/UAS_SIG added at 30bf02df8b
@@ -0,0 +1,420 @@
|
|||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
DESIGN TOKENS
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
:root{
|
||||||
|
--bg0:#07090f;--bg1:#0d1120;--bg2:#131929;--bg3:#1a2235;--bg4:#212d42;--bg5:#2a3a52;
|
||||||
|
--border0:#1a2438;--border1:#243045;--border2:#2e3e58;--border3:#3d5070;
|
||||||
|
--text0:#f0f4ff;--text1:#c8d4ea;--text2:#8fa3c0;--text3:#5a7292;--text4:#3a5070;
|
||||||
|
--accent:#5b9cf6;--accent2:#3a7de4;--accent-glow:rgba(91,156,246,.18);
|
||||||
|
--green:#34d47e;--green2:#22aa60;--green-s:rgba(52,212,126,.14);
|
||||||
|
--red:#f05c5c;--red2:#c83e3e;--red-s:rgba(240,92,92,.14);
|
||||||
|
--amber:#f0a030;--amber2:#c47c18;--amber-s:rgba(240,160,48,.14);
|
||||||
|
--purple:#9b7bfa;--purple2:#7a5ae0;--purple-s:rgba(155,123,250,.14);
|
||||||
|
--teal:#2ec4b6;--teal2:#1ea090;--teal-s:rgba(46,196,182,.14);
|
||||||
|
--orange:#f07840;--orange2:#c45820;--orange-s:rgba(240,120,64,.14);
|
||||||
|
--pink:#f06090;--pink-s:rgba(240,96,144,.14);
|
||||||
|
--font:'DM Sans',system-ui,sans-serif;
|
||||||
|
--mono:'DM Mono',monospace;
|
||||||
|
--r4:4px;--r6:6px;--r8:8px;--r10:10px;--r12:12px;--r16:16px;--rfull:9999px;
|
||||||
|
--shadow-sm:0 2px 8px rgba(0,0,0,.35);
|
||||||
|
--shadow-md:0 4px 20px rgba(0,0,0,.45);
|
||||||
|
--shadow-lg:0 8px 40px rgba(0,0,0,.6);
|
||||||
|
--transition:.15s ease;
|
||||||
|
}
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
html,body{height:100%;overflow:hidden;font-family:var(--font)}
|
||||||
|
body{background:var(--bg0);color:var(--text0);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}
|
||||||
|
button{font-family:var(--font);cursor:pointer;border:none;outline:none;background:none}
|
||||||
|
input,select,textarea{font-family:var(--font);outline:none}
|
||||||
|
::-webkit-scrollbar{width:4px;height:4px}
|
||||||
|
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:var(--rfull)}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
LAYOUT
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
.app{display:flex;flex-direction:column;height:100vh}
|
||||||
|
.topbar{height:52px;background:var(--bg1);border-bottom:1px solid var(--border1);
|
||||||
|
display:flex;align-items:center;padding:0 16px;gap:12px;flex-shrink:0;z-index:500}
|
||||||
|
.main{display:flex;flex:1;overflow:hidden}
|
||||||
|
.sidebar{width:320px;background:var(--bg1);border-right:1px solid var(--border0);
|
||||||
|
display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}
|
||||||
|
.map-area{flex:1;position:relative}
|
||||||
|
#map{width:100%;height:100%}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
TOPBAR
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
.brand{display:flex;align-items:center;gap:10px}
|
||||||
|
.brand-mark{width:32px;height:32px;border-radius:var(--r8);
|
||||||
|
background:linear-gradient(135deg,var(--accent),var(--purple));
|
||||||
|
display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||||
|
.brand-mark svg{width:17px;height:17px;fill:#fff}
|
||||||
|
.brand-name{font-size:14px;font-weight:700;color:var(--text0);letter-spacing:-.3px;line-height:1.1}
|
||||||
|
.brand-name span{display:block;font-size:10px;font-weight:400;color:var(--text3);letter-spacing:.2px}
|
||||||
|
.topbar-div{width:1px;height:26px;background:var(--border1)}
|
||||||
|
.nav-pills{display:flex;gap:3px}
|
||||||
|
.nav-pill{display:flex;align-items:center;gap:6px;padding:5px 12px;border-radius:var(--r6);
|
||||||
|
font-size:12px;font-weight:500;color:var(--text2);transition:background var(--transition),color var(--transition)}
|
||||||
|
.nav-pill:hover{background:var(--bg3);color:var(--text0)}
|
||||||
|
.nav-pill.active{background:var(--accent);color:#fff}
|
||||||
|
.nav-pill.active .pill-badge{background:rgba(255,255,255,.25);color:#fff}
|
||||||
|
.pill-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
|
||||||
|
.pill-badge{min-width:17px;height:17px;padding:0 4px;border-radius:var(--rfull);
|
||||||
|
background:var(--bg4);color:var(--text3);font-size:10px;font-weight:700;
|
||||||
|
display:inline-flex;align-items:center;justify-content:center}
|
||||||
|
.topbar-spacer{flex:1}
|
||||||
|
/* Dashboard toggle */
|
||||||
|
.dash-btn{display:flex;align-items:center;gap:6px;padding:6px 14px;border-radius:var(--r6);
|
||||||
|
font-size:12px;font-weight:600;background:var(--purple-s);color:var(--purple);
|
||||||
|
border:1px solid rgba(155,123,250,.25);transition:background var(--transition)}
|
||||||
|
.dash-btn:hover{background:rgba(155,123,250,.25)}
|
||||||
|
.topbar-stat{font-size:11px;color:var(--text3);display:flex;align-items:center;gap:5px}
|
||||||
|
.stat-dot{width:6px;height:6px;border-radius:50%;background:var(--green)}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
SIDEBAR
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
.sidebar-head{padding:12px 14px;border-bottom:1px solid var(--border0);
|
||||||
|
display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
|
||||||
|
.sidebar-title{font-size:13px;font-weight:700;color:var(--text0)}
|
||||||
|
.sidebar-sub{font-size:10px;color:var(--text3);margin-top:1px}
|
||||||
|
.btn-add{display:flex;align-items:center;gap:4px;padding:5px 10px;border-radius:var(--r6);
|
||||||
|
font-size:11px;font-weight:700;background:var(--accent-glow);color:var(--accent);
|
||||||
|
border:1px solid rgba(91,156,246,.25);transition:background var(--transition)}
|
||||||
|
.btn-add:hover{background:rgba(91,156,246,.28)}
|
||||||
|
.sidebar-body{flex:1;overflow-y:auto;padding:10px}
|
||||||
|
|
||||||
|
/* Data Cards */
|
||||||
|
.dcard{background:var(--bg2);border:1px solid var(--border0);border-radius:var(--r10);
|
||||||
|
margin-bottom:8px;overflow:hidden;transition:border-color var(--transition)}
|
||||||
|
.dcard:hover{border-color:var(--border2)}
|
||||||
|
.dcard.active{border-color:var(--accent)}
|
||||||
|
.dcard-head{padding:9px 11px;display:flex;align-items:center;gap:8px;cursor:pointer}
|
||||||
|
.dcard-head:hover{background:var(--bg3)}
|
||||||
|
.dcard-sym{width:9px;height:9px;flex-shrink:0}
|
||||||
|
.dcard-sym.circle{border-radius:50%}
|
||||||
|
.dcard-sym.line{height:4px;border-radius:2px;width:20px}
|
||||||
|
.dcard-sym.poly{border-radius:2px;width:13px;border:2px solid currentColor;background:transparent}
|
||||||
|
.dcard-name{font-size:12px;font-weight:600;color:var(--text0);flex:1;
|
||||||
|
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
.dcard-attrs{padding:4px 11px 8px;border-top:1px solid var(--border0)}
|
||||||
|
.attr{display:flex;justify-content:space-between;padding:3px 0;
|
||||||
|
border-bottom:1px solid var(--border0);font-size:11px}
|
||||||
|
.attr:last-child{border-bottom:none}
|
||||||
|
.attr-k{color:var(--text3)}
|
||||||
|
.attr-v{color:var(--text2);font-weight:500;text-align:right;max-width:60%}
|
||||||
|
.dcard-acts{display:flex;gap:5px;padding:7px 11px;border-top:1px solid var(--border0);background:var(--bg1)}
|
||||||
|
|
||||||
|
/* Badges */
|
||||||
|
.badge{display:inline-flex;align-items:center;padding:1px 7px;border-radius:var(--rfull);font-size:10px;font-weight:700}
|
||||||
|
.badge-green {background:var(--green-s);color:var(--green)}
|
||||||
|
.badge-red {background:var(--red-s);color:var(--red)}
|
||||||
|
.badge-amber {background:var(--amber-s);color:var(--amber)}
|
||||||
|
.badge-accent {background:var(--accent-glow);color:var(--accent)}
|
||||||
|
.badge-purple {background:var(--purple-s);color:var(--purple)}
|
||||||
|
.badge-teal {background:var(--teal-s);color:var(--teal)}
|
||||||
|
.badge-orange {background:var(--orange-s);color:var(--orange)}
|
||||||
|
.badge-pink {background:var(--pink-s);color:var(--pink)}
|
||||||
|
|
||||||
|
/* Btn small */
|
||||||
|
.bsm{padding:3px 9px;border-radius:var(--r4);font-size:10px;font-weight:700;border:1px solid transparent;transition:background var(--transition)}
|
||||||
|
.bsm-zoom {background:var(--green-s);color:var(--green);border-color:rgba(52,212,126,.2)}
|
||||||
|
.bsm-zoom:hover{background:rgba(52,212,126,.24)}
|
||||||
|
.bsm-edit {background:var(--accent-glow);color:var(--accent);border-color:rgba(91,156,246,.2)}
|
||||||
|
.bsm-edit:hover{background:rgba(91,156,246,.25)}
|
||||||
|
.bsm-del {background:var(--red-s);color:var(--red);border-color:rgba(240,92,92,.2)}
|
||||||
|
.bsm-del:hover{background:rgba(240,92,92,.24)}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.empty{text-align:center;padding:48px 20px;color:var(--text4)}
|
||||||
|
.empty svg{margin:0 auto 12px;display:block;opacity:.2}
|
||||||
|
.empty p{font-size:12px;line-height:1.7}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
MAP OVERLAYS
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
.map-legend{position:absolute;bottom:20px;left:12px;z-index:800;
|
||||||
|
background:var(--bg1);border:1px solid var(--border1);border-radius:var(--r10);
|
||||||
|
padding:10px 13px;min-width:160px;box-shadow:var(--shadow-md)}
|
||||||
|
.legend-title{font-size:9px;font-weight:700;color:var(--text3);
|
||||||
|
text-transform:uppercase;letter-spacing:.8px;margin-bottom:8px}
|
||||||
|
.legend-row{display:flex;align-items:center;gap:8px;margin-bottom:5px;
|
||||||
|
font-size:11px;color:var(--text2)}
|
||||||
|
.legend-row:last-child{margin-bottom:0}
|
||||||
|
.leg-c{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
||||||
|
.leg-l{width:20px;height:4px;border-radius:2px;flex-shrink:0}
|
||||||
|
.leg-p{width:13px;height:11px;border-radius:2px;flex-shrink:0;border:2px solid}
|
||||||
|
|
||||||
|
.draw-bar{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:800;
|
||||||
|
background:var(--bg1);border:1px solid var(--border2);border-radius:var(--rfull);
|
||||||
|
padding:7px 16px;font-size:11px;font-weight:600;color:var(--text0);
|
||||||
|
display:flex;align-items:center;gap:10px;box-shadow:var(--shadow-md);white-space:nowrap}
|
||||||
|
.draw-bar.hidden{display:none}
|
||||||
|
.draw-pulse{width:7px;height:7px;border-radius:50%;background:var(--green);flex-shrink:0;
|
||||||
|
animation:pulse 1.5s ease-in-out infinite}
|
||||||
|
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.75)}}
|
||||||
|
.btn-cancel-draw{background:var(--red-s);color:var(--red);border:1px solid rgba(240,92,92,.2);
|
||||||
|
border-radius:var(--r4);padding:3px 9px;font-size:10px;font-weight:700;transition:background var(--transition)}
|
||||||
|
.btn-cancel-draw:hover{background:rgba(240,92,92,.24)}
|
||||||
|
|
||||||
|
/* Leaflet popup */
|
||||||
|
.leaflet-popup-content-wrapper{background:var(--bg2)!important;border:1px solid var(--border2)!important;
|
||||||
|
border-radius:var(--r10)!important;box-shadow:var(--shadow-lg)!important;color:var(--text0)!important}
|
||||||
|
.leaflet-popup-tip{background:var(--bg2)!important}
|
||||||
|
.leaflet-popup-content{margin:0!important;padding:0!important}
|
||||||
|
.leaflet-popup-close-button{color:var(--text2)!important;font-size:14px!important;top:8px!important;right:10px!important}
|
||||||
|
.popup-wrap{padding:12px 14px;min-width:220px;max-width:300px}
|
||||||
|
.popup-title{font-size:13px;font-weight:700;color:var(--text0);margin-bottom:8px;
|
||||||
|
display:flex;align-items:center;gap:7px}
|
||||||
|
.popup-sym{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
||||||
|
.popup-section{font-size:9px;font-weight:700;color:var(--text4);text-transform:uppercase;
|
||||||
|
letter-spacing:.8px;margin:8px 0 4px;padding-bottom:4px;border-bottom:1px solid var(--border0)}
|
||||||
|
.popup-row{display:flex;justify-content:space-between;gap:10px;padding:3px 0;
|
||||||
|
border-bottom:1px solid var(--border0);font-size:11px}
|
||||||
|
.popup-row:last-of-type{border-bottom:none}
|
||||||
|
.popup-k{color:var(--text3)}
|
||||||
|
.popup-v{color:var(--text1);font-weight:500;text-align:right;max-width:55%}
|
||||||
|
.popup-acts{display:flex;gap:5px;margin-top:9px;padding-top:8px;border-top:1px solid var(--border0)}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
MODAL
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
.modal-ov{position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:9999;
|
||||||
|
display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);
|
||||||
|
animation:fadeIn .15s ease}
|
||||||
|
.modal-ov.hidden{display:none}
|
||||||
|
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
|
||||||
|
.modal{background:var(--bg2);border:1px solid var(--border2);border-radius:var(--r16);
|
||||||
|
width:520px;max-width:calc(100vw - 24px);max-height:88vh;overflow-y:auto;
|
||||||
|
box-shadow:var(--shadow-lg);animation:mIn .18s ease}
|
||||||
|
@keyframes mIn{from{transform:translateY(-10px) scale(.97);opacity:0}to{transform:none;opacity:1}}
|
||||||
|
.modal-head{padding:16px 18px 12px;border-bottom:1px solid var(--border0);
|
||||||
|
display:flex;align-items:center;justify-content:space-between;
|
||||||
|
position:sticky;top:0;background:var(--bg2);z-index:1}
|
||||||
|
.modal-head-left{display:flex;align-items:center;gap:10px}
|
||||||
|
.modal-icon{width:30px;height:30px;border-radius:var(--r8);display:flex;align-items:center;justify-content:center;font-size:15px}
|
||||||
|
.modal-title{font-size:14px;font-weight:700;color:var(--text0)}
|
||||||
|
.modal-sub{font-size:10px;color:var(--text3);margin-top:1px}
|
||||||
|
.modal-close{width:26px;height:26px;border-radius:var(--r6);background:var(--bg3);
|
||||||
|
border:1px solid var(--border1);color:var(--text2);font-size:12px;
|
||||||
|
display:flex;align-items:center;justify-content:center;transition:background var(--transition)}
|
||||||
|
.modal-close:hover{background:var(--bg4);color:var(--text0)}
|
||||||
|
.modal-body{padding:16px 18px}
|
||||||
|
.modal-foot{padding:12px 18px;border-top:1px solid var(--border0);
|
||||||
|
display:flex;gap:8px;justify-content:flex-end}
|
||||||
|
|
||||||
|
/* Form */
|
||||||
|
.form-sect{font-size:9px;font-weight:700;color:var(--text4);text-transform:uppercase;
|
||||||
|
letter-spacing:.8px;margin:14px 0 8px;padding-bottom:6px;border-bottom:1px solid var(--border0)}
|
||||||
|
.form-sect:first-child{margin-top:0}
|
||||||
|
.form-row{display:grid;gap:10px;margin-bottom:10px}
|
||||||
|
.form-row.cols2{grid-template-columns:1fr 1fr}
|
||||||
|
.form-row.cols3{grid-template-columns:1fr 1fr 1fr}
|
||||||
|
.fg{display:flex;flex-direction:column;gap:5px}
|
||||||
|
.flabel{font-size:11px;font-weight:600;color:var(--text2);letter-spacing:.2px}
|
||||||
|
.flabel .req{color:var(--red);margin-left:2px}
|
||||||
|
.finput{padding:8px 10px;background:var(--bg3);border:1px solid var(--border1);
|
||||||
|
border-radius:var(--r6);color:var(--text0);font-size:12px;font-family:var(--font);
|
||||||
|
transition:border-color var(--transition),box-shadow var(--transition);width:100%}
|
||||||
|
.finput:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(91,156,246,.12)}
|
||||||
|
.finput:disabled{opacity:.45;cursor:not-allowed}
|
||||||
|
.finput[readonly]{background:var(--bg4);color:var(--text2);cursor:default}
|
||||||
|
select.finput{cursor:pointer}
|
||||||
|
select.finput option{background:var(--bg3)}
|
||||||
|
textarea.finput{resize:vertical;min-height:64px}
|
||||||
|
.info-box{background:var(--bg4);border:1px solid var(--border1);border-radius:var(--r6);
|
||||||
|
padding:8px 10px;margin-bottom:10px;font-size:11px;color:var(--text2);line-height:1.6}
|
||||||
|
.info-box strong{color:var(--text0)}
|
||||||
|
.info-box.warn{border-color:rgba(240,160,48,.3);background:var(--amber-s)}
|
||||||
|
|
||||||
|
/* Radio toggle */
|
||||||
|
.rtoggle{display:flex;gap:6px}
|
||||||
|
.ropt{flex:1;display:flex;align-items:center;justify-content:center;gap:5px;
|
||||||
|
padding:8px;border-radius:var(--r6);border:1px solid var(--border1);cursor:pointer;
|
||||||
|
font-size:11px;font-weight:600;transition:all var(--transition);color:var(--text3);
|
||||||
|
background:var(--bg3);user-select:none}
|
||||||
|
.ropt input{display:none}
|
||||||
|
.ropt.yes{border-color:rgba(52,212,126,.35);color:var(--green);background:var(--green-s)}
|
||||||
|
.ropt.no {border-color:rgba(240,92,92,.35);color:var(--red);background:var(--red-s)}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;
|
||||||
|
padding:8px 18px;border-radius:var(--r6);font-size:13px;font-weight:600;
|
||||||
|
font-family:var(--font);transition:background var(--transition)}
|
||||||
|
.btn-primary{background:var(--accent);color:#fff}
|
||||||
|
.btn-primary:hover{background:var(--accent2)}
|
||||||
|
.btn-ghost{background:var(--bg3);color:var(--text2);border:1px solid var(--border1)}
|
||||||
|
.btn-ghost:hover{background:var(--bg4);color:var(--text0)}
|
||||||
|
.hidden{display:none!important}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
DASHBOARD PANEL
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
.dash-panel{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:8000;
|
||||||
|
display:flex;align-items:stretch;justify-content:flex-end;backdrop-filter:blur(4px);
|
||||||
|
animation:fadeIn .15s ease}
|
||||||
|
.dash-panel.hidden{display:none}
|
||||||
|
.dash-inner{width:680px;max-width:95vw;background:var(--bg1);border-left:1px solid var(--border1);
|
||||||
|
display:flex;flex-direction:column;overflow:hidden}
|
||||||
|
.dash-head{padding:18px 20px;border-bottom:1px solid var(--border0);
|
||||||
|
display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
|
||||||
|
.dash-head-title{font-size:16px;font-weight:700;color:var(--text0)}
|
||||||
|
.dash-head-sub{font-size:11px;color:var(--text3);margin-top:2px}
|
||||||
|
.dash-body{flex:1;overflow-y:auto;padding:18px 20px}
|
||||||
|
|
||||||
|
/* Stat cards */
|
||||||
|
.stat-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:18px}
|
||||||
|
.stat-card{background:var(--bg2);border:1px solid var(--border0);border-radius:var(--r10);
|
||||||
|
padding:12px 14px;position:relative;overflow:hidden}
|
||||||
|
.stat-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px}
|
||||||
|
.stat-card.c-accent::before{background:var(--accent)}
|
||||||
|
.stat-card.c-green::before{background:var(--green)}
|
||||||
|
.stat-card.c-amber::before{background:var(--amber)}
|
||||||
|
.stat-card.c-purple::before{background:var(--purple)}
|
||||||
|
.stat-card.c-red::before{background:var(--red)}
|
||||||
|
.stat-card.c-teal::before{background:var(--teal)}
|
||||||
|
.sc-val{font-size:26px;font-weight:700;color:var(--text0);font-family:var(--mono);line-height:1}
|
||||||
|
.sc-label{font-size:10px;color:var(--text3);margin-top:4px}
|
||||||
|
.sc-sub{font-size:11px;color:var(--text2);margin-top:6px}
|
||||||
|
|
||||||
|
/* Dash sections */
|
||||||
|
.dash-sect{margin-bottom:20px}
|
||||||
|
.dash-sect-title{font-size:11px;font-weight:700;color:var(--text2);
|
||||||
|
text-transform:uppercase;letter-spacing:.6px;margin-bottom:10px;
|
||||||
|
padding-bottom:6px;border-bottom:1px solid var(--border0)}
|
||||||
|
.risk-table{width:100%;border-collapse:collapse;font-size:11px}
|
||||||
|
.risk-table th{text-align:left;padding:6px 10px;color:var(--text3);font-weight:600;
|
||||||
|
border-bottom:1px solid var(--border0);font-size:10px}
|
||||||
|
.risk-table td{padding:7px 10px;border-bottom:1px solid var(--border0);color:var(--text1)}
|
||||||
|
.risk-table tr:last-child td{border-bottom:none}
|
||||||
|
.risk-table tr:hover td{background:var(--bg2)}
|
||||||
|
.risk-bar-wrap{display:flex;align-items:center;gap:7px}
|
||||||
|
.risk-bar{height:5px;border-radius:3px;background:var(--bg4);flex:1}
|
||||||
|
.risk-bar-fill{height:100%;border-radius:3px;transition:width .3s ease}
|
||||||
|
.prog-row{display:flex;align-items:center;justify-content:space-between;
|
||||||
|
padding:6px 0;border-bottom:1px solid var(--border0);font-size:11px}
|
||||||
|
.prog-row:last-child{border-bottom:none}
|
||||||
|
.prog-name{color:var(--text1)}
|
||||||
|
.prog-count{font-family:var(--mono);font-weight:600;color:var(--text0)}
|
||||||
|
.intervensi-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
||||||
|
.intervensi-card{background:var(--bg2);border:1px solid var(--border0);border-radius:var(--r8);padding:10px 12px}
|
||||||
|
.ic-label{font-size:10px;color:var(--text3);margin-bottom:4px}
|
||||||
|
.ic-val{font-size:13px;font-weight:700;color:var(--text0)}
|
||||||
|
.ic-sub{font-size:10px;color:var(--text2);margin-top:3px}
|
||||||
|
|
||||||
|
/* Toast */
|
||||||
|
.toasts{position:fixed;bottom:22px;right:22px;z-index:10000;display:flex;flex-direction:column;gap:7px;pointer-events:none}
|
||||||
|
.toast{background:var(--bg2);border:1px solid var(--border2);border-radius:var(--r10);
|
||||||
|
padding:10px 14px;font-size:12px;color:var(--text0);box-shadow:var(--shadow-lg);
|
||||||
|
display:flex;align-items:center;gap:9px;min-width:240px;pointer-events:all;
|
||||||
|
animation:tIn .2s ease}
|
||||||
|
.toast.success{border-left:3px solid var(--green)}
|
||||||
|
.toast.error{border-left:3px solid var(--red)}
|
||||||
|
.toast.info{border-left:3px solid var(--accent)}
|
||||||
|
@keyframes tIn{from{transform:translateX(110%);opacity:0}to{transform:none;opacity:1}}
|
||||||
|
@keyframes tOut{from{transform:none;opacity:1}to{transform:translateX(110%);opacity:0}}
|
||||||
|
.toast.out{animation:tOut .2s ease forwards}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
BANSOS DISTRIBUTION SYSTEM
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* History table (multi-entry) inside modal */
|
||||||
|
.history-table{width:100%;border-collapse:collapse;margin-top:6px;font-size:11px}
|
||||||
|
.history-table th{text-align:left;padding:5px 8px;color:var(--text3);font-weight:600;
|
||||||
|
font-size:10px;border-bottom:1px solid var(--border1);background:var(--bg3)}
|
||||||
|
.history-table td{padding:6px 8px;border-bottom:1px solid var(--border0);color:var(--text1)}
|
||||||
|
.history-table tr:last-child td{border-bottom:none}
|
||||||
|
.history-table tr:hover td{background:var(--bg3)}
|
||||||
|
.history-table .status-aktif{color:var(--green);font-weight:700}
|
||||||
|
.history-table .status-berhenti{color:var(--red);font-weight:700}
|
||||||
|
.history-table .status-proses{color:var(--amber);font-weight:700}
|
||||||
|
|
||||||
|
/* Dynamic add-row button */
|
||||||
|
.btn-add-row{display:inline-flex;align-items:center;gap:4px;margin-top:6px;
|
||||||
|
padding:4px 10px;border-radius:var(--r4);font-size:10px;font-weight:700;
|
||||||
|
background:var(--green-s);color:var(--green);border:1px solid rgba(52,212,126,.25);
|
||||||
|
cursor:pointer;transition:background var(--transition)}
|
||||||
|
.btn-add-row:hover{background:rgba(52,212,126,.24)}
|
||||||
|
.btn-del-row{padding:2px 7px;border-radius:var(--r4);font-size:10px;font-weight:700;
|
||||||
|
background:var(--red-s);color:var(--red);border:1px solid rgba(240,92,92,.2);
|
||||||
|
cursor:pointer;transition:background var(--transition)}
|
||||||
|
.btn-del-row:hover{background:rgba(240,92,92,.24)}
|
||||||
|
|
||||||
|
/* Radius coverage indicator in popup */
|
||||||
|
.coverage-badge{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;
|
||||||
|
border-radius:var(--rfull);font-size:10px;font-weight:700;margin-top:4px}
|
||||||
|
.coverage-badge.covered{background:var(--green-s);color:var(--green);border:1px solid rgba(52,212,126,.25)}
|
||||||
|
.coverage-badge.uncovered{background:var(--red-s);color:var(--red);border:1px solid rgba(240,92,92,.25)}
|
||||||
|
.coverage-badge svg{width:10px;height:10px;fill:currentColor;flex-shrink:0}
|
||||||
|
|
||||||
|
/* Radius circle legend */
|
||||||
|
.legend-radius{display:flex;align-items:center;gap:7px;margin-top:6px;
|
||||||
|
padding-top:6px;border-top:1px solid var(--border0);font-size:10px;color:var(--text3)}
|
||||||
|
.leg-radius-sym{width:14px;height:14px;border-radius:50%;border:2px dashed var(--teal);
|
||||||
|
background:rgba(46,196,182,.08);flex-shrink:0}
|
||||||
|
|
||||||
|
/* Dashboard distribution tab */
|
||||||
|
.dash-tab-bar{display:flex;gap:3px;margin-bottom:14px;background:var(--bg2);
|
||||||
|
border:1px solid var(--border0);border-radius:var(--r8);padding:3px}
|
||||||
|
.dash-tab{flex:1;padding:6px 12px;border-radius:var(--r6);font-size:11px;font-weight:600;
|
||||||
|
color:var(--text3);cursor:pointer;transition:all var(--transition);text-align:center}
|
||||||
|
.dash-tab.active{background:var(--accent);color:#fff}
|
||||||
|
.dash-tab:not(.active):hover{background:var(--bg3);color:var(--text1)}
|
||||||
|
|
||||||
|
/* Coverage summary cards */
|
||||||
|
.coverage-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px}
|
||||||
|
.coverage-card{background:var(--bg2);border:1px solid var(--border0);border-radius:var(--r10);
|
||||||
|
padding:12px 14px;display:flex;align-items:center;gap:12px}
|
||||||
|
.cc-icon{width:34px;height:34px;border-radius:var(--r8);display:flex;align-items:center;
|
||||||
|
justify-content:center;font-size:16px;flex-shrink:0}
|
||||||
|
.cc-icon.green{background:var(--green-s)}
|
||||||
|
.cc-icon.red{background:var(--red-s)}
|
||||||
|
.cc-icon.amber{background:var(--amber-s)}
|
||||||
|
.cc-icon.teal{background:var(--teal-s)}
|
||||||
|
.cc-info{min-width:0}
|
||||||
|
.cc-val{font-size:20px;font-weight:700;color:var(--text0);font-family:var(--mono)}
|
||||||
|
.cc-label{font-size:10px;color:var(--text3);margin-top:1px}
|
||||||
|
|
||||||
|
/* Hub assignment table */
|
||||||
|
.hub-table{width:100%;border-collapse:collapse;font-size:11px}
|
||||||
|
.hub-table th{text-align:left;padding:7px 10px;color:var(--text3);font-weight:600;
|
||||||
|
font-size:10px;border-bottom:1px solid var(--border1);background:var(--bg2);
|
||||||
|
position:sticky;top:0}
|
||||||
|
.hub-table td{padding:7px 10px;border-bottom:1px solid var(--border0);color:var(--text1)}
|
||||||
|
.hub-table tr:last-child td{border-bottom:none}
|
||||||
|
.hub-table tr:hover td{background:var(--bg2)}
|
||||||
|
.hub-name-cell{display:flex;align-items:center;gap:7px}
|
||||||
|
.hub-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
||||||
|
.hub-count-bar{display:flex;align-items:center;gap:7px}
|
||||||
|
.hub-bar{height:5px;border-radius:3px;background:var(--bg4);flex:1;min-width:60px}
|
||||||
|
.hub-bar-fill{height:100%;border-radius:3px;transition:width .4s ease}
|
||||||
|
|
||||||
|
/* Multi-section form tab inside modal */
|
||||||
|
.form-tabs{display:flex;gap:2px;background:var(--bg3);border-radius:var(--r6);
|
||||||
|
padding:3px;margin-bottom:14px}
|
||||||
|
.form-tab{flex:1;padding:5px 8px;border-radius:var(--r4);font-size:10px;font-weight:700;
|
||||||
|
color:var(--text3);cursor:pointer;transition:all var(--transition);text-align:center}
|
||||||
|
.form-tab.active{background:var(--accent);color:#fff}
|
||||||
|
.form-tab:not(.active):hover{background:var(--bg4);color:var(--text1)}
|
||||||
|
.form-section-panel{display:none}
|
||||||
|
.form-section-panel.active{display:block}
|
||||||
|
|
||||||
|
/* Pendidikan tag badges */
|
||||||
|
.pendidikan-grid{display:flex;flex-wrap:wrap;gap:5px;margin-top:4px}
|
||||||
|
.pddk-tag{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;
|
||||||
|
border-radius:var(--rfull);font-size:10px;font-weight:600;cursor:pointer;
|
||||||
|
border:1px solid var(--border1);background:var(--bg3);color:var(--text3);
|
||||||
|
transition:all var(--transition);user-select:none}
|
||||||
|
.pddk-tag.selected{background:var(--accent-glow);border-color:rgba(91,156,246,.4);color:var(--accent)}
|
||||||
|
|
||||||
|
/* Penyakit checklist */
|
||||||
|
.penyakit-grid{display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-top:4px}
|
||||||
|
.penyakit-check{display:flex;align-items:center;gap:6px;padding:5px 8px;
|
||||||
|
border-radius:var(--r6);border:1px solid var(--border0);background:var(--bg3);
|
||||||
|
cursor:pointer;font-size:11px;color:var(--text2);transition:all var(--transition);user-select:none}
|
||||||
|
.penyakit-check input{accent-color:var(--red);width:12px;height:12px}
|
||||||
|
.penyakit-check.checked{border-color:rgba(240,92,92,.35);background:var(--red-s);color:var(--red)}
|
||||||
|
|
||||||
|
/* Badge pink (already in root vars) */
|
||||||
|
.badge-pink{background:var(--pink-s);color:var(--pink)}
|
||||||
@@ -5,7 +5,7 @@ const App = {
|
|||||||
state:{ activeLayer:'spbu' },
|
state:{ activeLayer:'spbu' },
|
||||||
_groups:{},
|
_groups:{},
|
||||||
|
|
||||||
init(){
|
async init(){
|
||||||
// Map
|
// Map
|
||||||
this._map=L.map('map',{zoomControl:false}).setView([-0.056031,109.348641],13);
|
this._map=L.map('map',{zoomControl:false}).setView([-0.056031,109.348641],13);
|
||||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{
|
||||||
@@ -49,14 +49,15 @@ const App = {
|
|||||||
document.getElementById('modal-close').addEventListener('click', Modal.close);
|
document.getElementById('modal-close').addEventListener('click', Modal.close);
|
||||||
document.getElementById('modal-ov').addEventListener('click',e=>{ if(e.target===e.currentTarget) Modal.close(); });
|
document.getElementById('modal-ov').addEventListener('click',e=>{ if(e.target===e.currentTarget) Modal.close(); });
|
||||||
|
|
||||||
|
await Store.load();
|
||||||
this.refresh();
|
this.refresh();
|
||||||
},
|
},
|
||||||
|
|
||||||
crud:{
|
crud:{
|
||||||
edit(ln,id){ App.state.activeLayer=ln; Modal.openEdit(ln,id); },
|
edit(ln,id){ App.state.activeLayer=ln; Modal.openEdit(ln,id); },
|
||||||
delete(ln,id){
|
async delete(ln,id){
|
||||||
if(!confirm('Yakin hapus data ini?')) return;
|
if(!confirm('Yakin hapus data ini?')) return;
|
||||||
Store.remove(ln,id); App._map.closePopup();
|
await Store.remove(ln,id); App._map.closePopup();
|
||||||
App.refresh(); Toast.info('Data dihapus');
|
App.refresh(); Toast.info('Data dihapus');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
7. CARD BUILDERS (sidebar)
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Card = {
|
||||||
|
_acts:(ln,id)=>`<div class="dcard-acts">
|
||||||
|
<button class="bsm bsm-zoom" onclick="App.map.zoomTo('${ln}','${id}')">Zoom</button>
|
||||||
|
<button class="bsm bsm-edit" onclick="App.crud.edit('${ln}','${id}')">Edit</button>
|
||||||
|
<button class="bsm bsm-del" onclick="App.crud.delete('${ln}','${id}')">Hapus</button></div>`,
|
||||||
|
|
||||||
|
spbu(r){ const clr=CFG.spbu.color(r),bdg=CFG.spbu.badge(r);
|
||||||
|
return `<div class="dcard" id="card-${r.id}">
|
||||||
|
<div class="dcard-head" onclick="App.map.zoomTo('spbu','${r.id}')">
|
||||||
|
<span class="dcard-sym circle" style="background:${clr}"></span>
|
||||||
|
<span class="dcard-name">${H.esc(r.nama)}</span><span class="badge ${bdg.c}">${bdg.t}</span></div>
|
||||||
|
<div class="dcard-attrs">
|
||||||
|
<div class="attr"><span class="attr-k">WA</span><span class="attr-v">${H.esc(r.wa||'-')}</span></div>
|
||||||
|
<div class="attr"><span class="attr-k">BBM</span><span class="attr-v">${H.esc(r.jenis_bbm||'-')}</span></div>
|
||||||
|
</div>${this._acts('spbu',r.id)}</div>`; },
|
||||||
|
|
||||||
|
jalan(r){ const clr=CFG.jalan.color(r),bdg=CFG.jalan.badge(r);
|
||||||
|
return `<div class="dcard" id="card-${r.id}">
|
||||||
|
<div class="dcard-head" onclick="App.map.zoomTo('jalan','${r.id}')">
|
||||||
|
<span class="dcard-sym line" style="background:${clr}"></span>
|
||||||
|
<span class="dcard-name">${H.esc(r.nama)}</span><span class="badge ${bdg.c}">${bdg.t}</span></div>
|
||||||
|
<div class="dcard-attrs">
|
||||||
|
<div class="attr"><span class="attr-k">Panjang</span><span class="attr-v">${H.fmDist(r.panjang)}</span></div>
|
||||||
|
<div class="attr"><span class="attr-k">Kondisi</span><span class="attr-v">${H.esc(r.kondisi||'-')}</span></div>
|
||||||
|
</div>${this._acts('jalan',r.id)}</div>`; },
|
||||||
|
|
||||||
|
parsil(r){ const clr=CFG.parsil.color(r),bdg=CFG.parsil.badge(r);
|
||||||
|
return `<div class="dcard" id="card-${r.id}">
|
||||||
|
<div class="dcard-head" onclick="App.map.zoomTo('parsil','${r.id}')">
|
||||||
|
<span class="dcard-sym poly" style="color:${clr};height:9px"></span>
|
||||||
|
<span class="dcard-name">${H.esc(r.nama)}</span><span class="badge ${bdg.c}">${bdg.t}</span></div>
|
||||||
|
<div class="dcard-attrs">
|
||||||
|
<div class="attr"><span class="attr-k">Luas</span><span class="attr-v">${H.fmArea(r.luas)}</span></div>
|
||||||
|
<div class="attr"><span class="attr-k">Sengketa</span><span class="attr-v">${H.esc(r.sengketa||'-')}</span></div>
|
||||||
|
</div>${this._acts('parsil',r.id)}</div>`; },
|
||||||
|
|
||||||
|
warga(r){ const clr=CFG.warga.color(r),bdg=CFG.warga.badge(r);
|
||||||
|
const pendapatan=r.pendapatan?'Rp '+parseInt(r.pendapatan).toLocaleString('id-ID'):'-';
|
||||||
|
const histCount=r.history_bansos?.length||0;
|
||||||
|
return `<div class="dcard" id="card-${r.id}">
|
||||||
|
<div class="dcard-head" onclick="App.map.zoomTo('warga','${r.id}')">
|
||||||
|
<span class="dcard-sym circle" style="background:${clr}"></span>
|
||||||
|
<span class="dcard-name">${H.esc(r.nama)}</span><span class="badge ${bdg.c}">${bdg.t}</span></div>
|
||||||
|
<div class="dcard-attrs">
|
||||||
|
<div class="attr"><span class="attr-k">Anggota</span><span class="attr-v">${r.jml_anggota||'-'} jiwa</span></div>
|
||||||
|
<div class="attr"><span class="attr-k">Pendapatan</span><span class="attr-v">${pendapatan}</span></div>
|
||||||
|
<div class="attr"><span class="attr-k">Bansos</span><span class="attr-v">${H.esc(r.bansos||'-')}</span></div>
|
||||||
|
<div class="attr"><span class="attr-k">Riwayat</span><span class="attr-v">${histCount} program</span></div>
|
||||||
|
</div>${this._acts('warga',r.id)}</div>`; },
|
||||||
|
|
||||||
|
rumahIbadah(r){ const clr=CFG.rumahIbadah.color(r),bdg=CFG.rumahIbadah.badge(r);
|
||||||
|
return `<div class="dcard" id="card-${r.id}">
|
||||||
|
<div class="dcard-head" onclick="App.map.zoomTo('rumahIbadah','${r.id}')">
|
||||||
|
<span class="dcard-sym circle" style="background:${clr}"></span>
|
||||||
|
<span class="dcard-name">${H.esc(r.nama)}</span><span class="badge ${bdg.c}">${bdg.t}</span></div>
|
||||||
|
<div class="dcard-attrs">
|
||||||
|
<div class="attr"><span class="attr-k">Ketua</span><span class="attr-v">${H.esc(r.ketua||'-')}</span></div>
|
||||||
|
<div class="attr"><span class="attr-k">Binaan</span><span class="attr-v">${r.jml_binaan||'-'} KK</span></div>
|
||||||
|
</div>${this._acts('rumahIbadah',r.id)}</div>`; },
|
||||||
|
};
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
5. FORM BUILDERS
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Forms = {
|
||||||
|
spbu(rec, latlng){
|
||||||
|
const lat=latlng?latlng.lat.toFixed(6):(rec?.lat?.toFixed(6)||'');
|
||||||
|
const lng=latlng?latlng.lng.toFixed(6):(rec?.lng?.toFixed(6)||'');
|
||||||
|
const v=f=>rec?H.esc(rec[f]||''):'';
|
||||||
|
return `
|
||||||
|
<div class="info-box">📍 Koordinat: <strong>${lat}, ${lng}</strong></div>
|
||||||
|
<div class="form-sect">Identitas & Operasional</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Nama SPBU<span class="req">*</span></label>
|
||||||
|
<input class="finput" id="f-nama" placeholder="cth: SPBU 64.754.01" value="${v('nama')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">No. WhatsApp</label>
|
||||||
|
<input class="finput" id="f-wa" placeholder="081234..." value="${v('wa')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Jenis BBM Tersedia</label>
|
||||||
|
<select class="finput" id="f-jenis_bbm">
|
||||||
|
${['Pertalite & Pertamax','Pertalite Saja','Pertamax Series','Solar & Pertamina Dex','Semua Jenis'].map(j=>`<option ${v('jenis_bbm')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Buka 24 Jam?</label>
|
||||||
|
<div class="rtoggle" id="rtog">
|
||||||
|
<label class="ropt ${rec?.buka24!==false?'yes':''}" data-v="true">
|
||||||
|
<input type="radio" name="buka24" value="true" ${rec?.buka24!==false?'checked':''}/>✓ Ya
|
||||||
|
</label>
|
||||||
|
<label class="ropt ${rec?.buka24===false?'no':''}" data-v="false">
|
||||||
|
<input type="radio" name="buka24" value="false" ${rec?.buka24===false?'checked':''}/>✗ Tidak
|
||||||
|
</label>
|
||||||
|
</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Aksesibilitas & Sosial</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Pelayanan Disabilitas</label>
|
||||||
|
<select class="finput" id="f-disabilitas">
|
||||||
|
${['Ada','Tidak Ada','Dalam Proses'].map(j=>`<option ${v('disabilitas')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Harga BBM Subsidi</label>
|
||||||
|
<select class="finput" id="f-harga_bbm">
|
||||||
|
${['Sesuai HET','Di Atas HET','Tidak Menjual Subsidi'].map(j=>`<option ${v('harga_bbm')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Catatan Lapangan</div>
|
||||||
|
<div class="fg"><label class="flabel">Catatan / Keterangan Tambahan</label>
|
||||||
|
<textarea class="finput" id="f-catatan" placeholder="Kondisi antrean, ketersediaan stok, dll...">${v('catatan')}</textarea></div>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
jalan(rec, len){
|
||||||
|
const pj=len??rec?.panjang??0; const v=f=>rec?H.esc(rec[f]||''):'';
|
||||||
|
return `
|
||||||
|
<div class="info-box">📏 Panjang terukur otomatis: <strong>${H.fmDist(pj)}</strong></div>
|
||||||
|
<input type="hidden" id="f-panjang" value="${pj}"/>
|
||||||
|
<div class="form-sect">Identitas Ruas Jalan</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Nama Ruas Jalan<span class="req">*</span></label>
|
||||||
|
<input class="finput" id="f-nama" placeholder="cth: Jl. H. Rais A Rahman" value="${v('nama')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Status Jalan<span class="req">*</span></label>
|
||||||
|
<select class="finput" id="f-status">
|
||||||
|
${CFG.jalan.statuses.map(s=>`<option ${v('status')===s?'selected':''}>${s}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Kondisi & Infrastruktur</div>
|
||||||
|
<div class="form-row cols3">
|
||||||
|
<div class="fg"><label class="flabel">Kondisi Jalan</label>
|
||||||
|
<select class="finput" id="f-kondisi">
|
||||||
|
${CFG.jalan.kondisi.map(k=>`<option ${v('kondisi')===k?'selected':''}>${k}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Tahun Perbaikan Terakhir</label>
|
||||||
|
<input class="finput" id="f-tahun_perbaikan" type="number" min="2000" max="2030" placeholder="2023" value="${v('tahun_perbaikan')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Lebar Jalan (meter)</label>
|
||||||
|
<input class="finput" id="f-lebar" type="number" min="1" max="50" placeholder="4" value="${v('lebar')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Dampak Sosial-Ekonomi</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Dilalui Angkutan Umum</label>
|
||||||
|
<select class="finput" id="f-angkutan">
|
||||||
|
${['Ya','Tidak'].map(k=>`<option ${v('angkutan')===k?'selected':''}>${k}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Akses ke Fasilitas Publik</label>
|
||||||
|
<select class="finput" id="f-akses_publik">
|
||||||
|
${['Pasar','RS/Puskesmas','Sekolah','Kantor Pemerintah','Tidak Ada'].map(k=>`<option ${v('akses_publik')===k?'selected':''}>${k}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="fg"><label class="flabel">Catatan Kondisi</label>
|
||||||
|
<textarea class="finput" id="f-catatan" placeholder="Kondisi berlubang, banjir saat hujan, dll...">${v('catatan')}</textarea></div>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
parsil(rec, area){
|
||||||
|
const ls=area??rec?.luas??0; const v=f=>rec?H.esc(rec[f]||''):'';
|
||||||
|
return `
|
||||||
|
<div class="info-box">📐 Luas terukur otomatis: <strong>${H.fmArea(ls)}</strong></div>
|
||||||
|
<input type="hidden" id="f-luas" value="${ls}"/>
|
||||||
|
<div class="form-sect">Identitas Parsil</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Nama / ID Parsil<span class="req">*</span></label>
|
||||||
|
<input class="finput" id="f-nama" placeholder="cth: Kavling A-01" value="${v('nama')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Nomor Induk Bidang (NIB)</label>
|
||||||
|
<input class="finput" id="f-nib" placeholder="14.02.01.001" value="${v('nib')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Status Kepemilikan</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Status Sertifikat<span class="req">*</span></label>
|
||||||
|
<select class="finput" id="f-status">
|
||||||
|
${CFG.parsil.statuses.map(s=>`<option ${v('status')===s?'selected':''}>${s}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Sengketa / Masalah Hukum</label>
|
||||||
|
<select class="finput" id="f-sengketa">
|
||||||
|
${['Tidak Ada','Ada Sengketa','Dalam Proses PTSL','Klaim Pihak Lain'].map(k=>`<option ${v('sengketa')===k?'selected':''}>${k}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Penilaian & Pemanfaatan</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">NJOP (Rp/m²)</label>
|
||||||
|
<input class="finput" id="f-njop" type="number" placeholder="500000" value="${v('njop')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Penggunaan Lahan</label>
|
||||||
|
<select class="finput" id="f-penggunaan">
|
||||||
|
${['Rumah Tinggal','Lahan Kosong','Pertanian','Usaha/Warung','Fasilitas Umum','Lainnya'].map(k=>`<option ${v('penggunaan')===k?'selected':''}>${k}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="fg"><label class="flabel">Catatan Tambahan</label>
|
||||||
|
<textarea class="finput" id="f-catatan" placeholder="Kondisi, penghuni, riwayat...">${v('catatan')}</textarea></div>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
warga(rec, latlng){
|
||||||
|
const lat=latlng?latlng.lat.toFixed(6):(rec?.lat?.toFixed(6)||'');
|
||||||
|
const lng=latlng?latlng.lng.toFixed(6):(rec?.lng?.toFixed(6)||'');
|
||||||
|
const v=f=>rec?H.esc(rec[f]||''):'';
|
||||||
|
const c=v=>rec?.penyakit?.includes(v)?'checked':'';
|
||||||
|
const hist=rec?.history_bansos||[];
|
||||||
|
const ri = Store.all('rumahIbadah');
|
||||||
|
const hubOpts = ['Belum ditugaskan', ...ri.map(r=>r.nama)].map(opt=>`<option ${v('hub_ri')===opt?'selected':''}>${opt}</option>`).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="info-box warn">⚠️ Data warga bersifat sensitif. Pastikan survei akurat.</div>
|
||||||
|
<div class="info-box">📍 Koordinat: <strong>${lat}, ${lng}</strong></div>
|
||||||
|
<div class="form-tabs">
|
||||||
|
<div class="form-tab active" data-panel="kk">Data KK</div>
|
||||||
|
<div class="form-tab" data-panel="eko">Ekonomi</div>
|
||||||
|
<div class="form-tab" data-panel="pddk">Pddk</div>
|
||||||
|
<div class="form-tab" data-panel="kes">Kes</div>
|
||||||
|
<div class="form-tab" data-panel="hist">Bansos</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: Data KK -->
|
||||||
|
<div class="form-section-panel active" id="panel-kk">
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Nama KK<span class="req">*</span></label>
|
||||||
|
<input class="finput" id="f-nama" placeholder="Nama KTP" value="${v('nama')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">NIK</label>
|
||||||
|
<input class="finput" id="f-nik" placeholder="3471..." maxlength="16" value="${v('nik')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">No. KK</label>
|
||||||
|
<input class="finput" id="f-no_kk" value="${v('no_kk')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">WA/HP</label>
|
||||||
|
<input class="finput" id="f-wa" value="${v('wa')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row cols3">
|
||||||
|
<div class="fg"><label class="flabel">Tingkat Kemiskinan</label>
|
||||||
|
<select class="finput" id="f-tingkat_kemiskinan">
|
||||||
|
${CFG.warga.tingkat.map(t=>`<option ${v('tingkat_kemiskinan')===t?'selected':''}>${t}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Jml Anggota</label>
|
||||||
|
<input class="finput" id="f-jml_anggota" type="number" placeholder="4" value="${v('jml_anggota')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Jml Lansia</label>
|
||||||
|
<input class="finput" id="f-jml_lansia" type="number" placeholder="0" value="${v('jml_lansia')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="fg"><label class="flabel">Alamat</label>
|
||||||
|
<textarea class="finput" id="f-alamat" style="min-height:40px">${v('alamat')}</textarea></div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Penerima Bansos</label>
|
||||||
|
<select class="finput" id="f-bansos">
|
||||||
|
${CFG.warga.bantuan.map(b=>`<option ${v('bansos')===b?'selected':''}>${b}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Hub Rumah Ibadah</label>
|
||||||
|
<select class="finput" id="f-hub_ri">${hubOpts}</select></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: Ekonomi -->
|
||||||
|
<div class="form-section-panel" id="panel-eko">
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Pendapatan (Rp/Bln)</label>
|
||||||
|
<input class="finput" id="f-pendapatan" type="number" value="${v('pendapatan')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Pekerjaan KK</label>
|
||||||
|
<select class="finput" id="f-pekerjaan">
|
||||||
|
${CFG.warga.pekerjaan.map(p=>`<option ${v('pekerjaan')===p?'selected':''}>${p}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Kondisi Rumah</label>
|
||||||
|
<select class="finput" id="f-kondisi_rumah">
|
||||||
|
${CFG.warga.kondisiRumah.map(k=>`<option ${v('kondisi_rumah')===k?'selected':''}>${k}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Aset (Tanah/Kendaraan)</label>
|
||||||
|
<select class="finput" id="f-kepemilikan_aset">
|
||||||
|
${['Tidak Ada','Motor Tua','Motor Baru','Tanah','Mobil'].map(j=>`<option ${v('kepemilikan_aset')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Air Bersih</label>
|
||||||
|
<select class="finput" id="f-air_bersih">
|
||||||
|
${['PDAM','Sumur','Sungai/Tadah Hujan'].map(j=>`<option ${v('air_bersih')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Listrik</label>
|
||||||
|
<select class="finput" id="f-listrik">
|
||||||
|
${['Sendiri (>900W)','Subsidi (450/900W)','Numpang/Tidak Ada'].map(j=>`<option ${v('listrik')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: Pendidikan -->
|
||||||
|
<div class="form-section-panel" id="panel-pddk">
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Pendidikan Terakhir KK</label>
|
||||||
|
<select class="finput" id="f-pendidikan_kk">
|
||||||
|
${CFG.warga.tingkatPendidikan.map(j=>`<option ${v('pendidikan_kk')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Anak Bersekolah</label>
|
||||||
|
<input class="finput" id="f-jml_anak" type="number" placeholder="1" value="${v('jml_anak')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Anak Putus Sekolah</label>
|
||||||
|
<input class="finput" id="f-anak_putus_sekolah" type="number" placeholder="0" value="${v('anak_putus_sekolah')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Penerima KIP/Beasiswa?</label>
|
||||||
|
<select class="finput" id="f-penerima_beasiswa">
|
||||||
|
${['Ya','Tidak'].map(j=>`<option ${v('penerima_beasiswa')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: Kesehatan -->
|
||||||
|
<div class="form-section-panel" id="panel-kes">
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Kepesertaan BPJS</label>
|
||||||
|
<select class="finput" id="f-bpjs">
|
||||||
|
${['PBI (Gratis)','Mandiri','Perusahaan','Tidak Punya BPJS'].map(j=>`<option ${v('bpjs')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Anggota Difabel/Kronis</label>
|
||||||
|
<input class="finput" id="f-jml_sakit_kronis" type="number" value="${v('jml_sakit_kronis')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="fg"><label class="flabel">Jenis Penyakit Khusus</label>
|
||||||
|
<div class="penyakit-grid">
|
||||||
|
${CFG.warga.jenisPenyakit.map(p=>`<label class="penyakit-check ${c(p)}"><input type="checkbox" name="penyakit" value="${p}" ${c(p)}/>${p}</label>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: Riwayat Bansos -->
|
||||||
|
<div class="form-section-panel" id="panel-hist">
|
||||||
|
<table class="history-table">
|
||||||
|
<thead><tr><th>Program</th><th style="width:60px">Thn</th><th>Status</th><th style="width:28px"></th></tr></thead>
|
||||||
|
<tbody id="hist-tbody">
|
||||||
|
${hist.map(h=>`
|
||||||
|
<tr>
|
||||||
|
<td><input class="finput hist-program" style="padding:3px" value="${H.esc(h.program)}"/></td>
|
||||||
|
<td><input class="finput hist-tahun" style="padding:3px" type="number" value="${h.tahun}"/></td>
|
||||||
|
<td><select class="finput hist-status" style="padding:3px"><option ${h.status==='Aktif'?'selected':''}>Aktif</option><option ${h.status==='Berhenti'?'selected':''}>Berhenti</option></select></td>
|
||||||
|
<td><button class="btn-del-row" type="button" onclick="this.closest('tr').remove()">✕</button></td>
|
||||||
|
</tr>`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button class="btn-add-row" type="button" id="btn-add-hist">+ Tambah</button>
|
||||||
|
<div class="fg" style="margin-top:10px"><label class="flabel">Catatan / Rekomendasi</label>
|
||||||
|
<textarea class="finput" id="f-catatan" style="min-height:50px">${v('catatan')}</textarea></div>
|
||||||
|
</div>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
rumahIbadah(rec, latlng){
|
||||||
|
const lat=latlng?latlng.lat.toFixed(6):(rec?.lat?.toFixed(6)||'');
|
||||||
|
const lng=latlng?latlng.lng.toFixed(6):(rec?.lng?.toFixed(6)||'');
|
||||||
|
const v=f=>rec?H.esc(rec[f]||''):'';
|
||||||
|
return `
|
||||||
|
<div class="info-box">📍 Koordinat: <strong>${lat}, ${lng}</strong></div>
|
||||||
|
<div class="form-sect">Identitas Rumah Ibadah</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Nama Rumah Ibadah<span class="req">*</span></label>
|
||||||
|
<input class="finput" id="f-nama" placeholder="cth: Masjid Al-Ikhlas" value="${v('nama')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Jenis<span class="req">*</span></label>
|
||||||
|
<select class="finput" id="f-jenis">
|
||||||
|
${CFG.rumahIbadah.jenis.map(j=>`<option ${v('jenis')===j?'selected':''}>${j}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Nama Ketua / Imam / Pastor</label>
|
||||||
|
<input class="finput" id="f-ketua" placeholder="Nama lengkap" value="${v('ketua')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">No. WhatsApp Pengurus</label>
|
||||||
|
<input class="finput" id="f-wa" placeholder="081..." value="${v('wa')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Kapasitas & Jangkauan</div>
|
||||||
|
<div class="form-row cols3">
|
||||||
|
<div class="fg"><label class="flabel">Kapasitas Jamaah</label>
|
||||||
|
<input class="finput" id="f-kapasitas" type="number" placeholder="200" value="${v('kapasitas')}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Radius Wilayah Binaan (m)</label>
|
||||||
|
<input class="finput" id="f-radius" type="number" placeholder="300" value="${v('radius')||300}"/></div>
|
||||||
|
<div class="fg"><label class="flabel">Jml Warga Binaan</label>
|
||||||
|
<input class="finput" id="f-jml_binaan" type="number" placeholder="150" value="${v('jml_binaan')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-sect">Program Sosial</div>
|
||||||
|
<div class="form-row cols2">
|
||||||
|
<div class="fg"><label class="flabel">Program Aktif</label>
|
||||||
|
<select class="finput" id="f-program">
|
||||||
|
${['Baznas/Zakat','Bantuan Sembako','Santunan Anak Yatim','Pemberdayaan UMKM','Beasiswa','Belum Ada'].map(k=>`<option ${v('program')===k?'selected':''}>${k}</option>`).join('')}
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label class="flabel">Dana Sosial/Bulan (Rp)</label>
|
||||||
|
<input class="finput" id="f-dana_sosial" type="number" placeholder="2000000" value="${v('dana_sosial')}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="fg"><label class="flabel">Catatan / Kebutuhan Dukungan</label>
|
||||||
|
<textarea class="finput" id="f-catatan" placeholder="Kebutuhan data, pelatihan, dll...">${v('catatan')}</textarea></div>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
readAll(ln){
|
||||||
|
const g = id => document.getElementById(id)?.value?.trim();
|
||||||
|
if(ln==='spbu'){
|
||||||
|
const nm=g('f-nama'); if(!nm){Toast.error('Nama SPBU wajib diisi!');return null;}
|
||||||
|
const buka=document.querySelector('input[name="buka24"]:checked')?.value!=='false';
|
||||||
|
return {nama:nm,wa:g('f-wa'),buka24:buka,jenis_bbm:g('f-jenis_bbm'),disabilitas:g('f-disabilitas'),harga_bbm:g('f-harga_bbm'),catatan:g('f-catatan')};
|
||||||
|
}
|
||||||
|
if(ln==='jalan'){
|
||||||
|
const nm=g('f-nama'); if(!nm){Toast.error('Nama jalan wajib diisi!');return null;}
|
||||||
|
return {nama:nm,status:g('f-status'),panjang:parseFloat(g('f-panjang'))||0,kondisi:g('f-kondisi'),tahun_perbaikan:g('f-tahun_perbaikan'),lebar:g('f-lebar'),angkutan:g('f-angkutan'),akses_publik:g('f-akses_publik'),catatan:g('f-catatan')};
|
||||||
|
}
|
||||||
|
if(ln==='parsil'){
|
||||||
|
const nm=g('f-nama'); if(!nm){Toast.error('Nama parsil wajib diisi!');return null;}
|
||||||
|
return {nama:nm,status:g('f-status'),luas:parseFloat(g('f-luas'))||0,nib:g('f-nib'),sengketa:g('f-sengketa'),njop:g('f-njop'),penggunaan:g('f-penggunaan'),catatan:g('f-catatan')};
|
||||||
|
}
|
||||||
|
if(ln==='warga'){
|
||||||
|
const nm=g('f-nama'); if(!nm){Toast.error('Nama KK wajib diisi!');return null;}
|
||||||
|
const penyakit=[...document.querySelectorAll('input[name="penyakit"]:checked')].map(el=>el.value);
|
||||||
|
const history_bansos=[];
|
||||||
|
document.querySelectorAll('#hist-tbody tr').forEach(tr=>{
|
||||||
|
const prog=tr.querySelector('.hist-program')?.value?.trim();
|
||||||
|
const thn=tr.querySelector('.hist-tahun')?.value?.trim();
|
||||||
|
const sts=tr.querySelector('.hist-status')?.value;
|
||||||
|
if(prog) history_bansos.push({program:prog,tahun:parseInt(thn)||new Date().getFullYear(),status:sts});
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
nama: nm, nik: g('f-nik'), no_kk: g('f-no_kk'), wa: g('f-wa'), tingkat_kemiskinan: g('f-tingkat_kemiskinan'),
|
||||||
|
jml_anggota: parseInt(g('f-jml_anggota'))||0, jml_lansia: parseInt(g('f-jml_lansia'))||0,
|
||||||
|
alamat: g('f-alamat'), bansos: g('f-bansos'), hub_ri: g('f-hub_ri'),
|
||||||
|
pendapatan: g('f-pendapatan'), pekerjaan: g('f-pekerjaan'),
|
||||||
|
kondisi_rumah: g('f-kondisi_rumah'), kepemilikan_aset: g('f-kepemilikan_aset'),
|
||||||
|
air_bersih: g('f-air_bersih'), listrik: g('f-listrik'),
|
||||||
|
pendidikan_kk: g('f-pendidikan_kk'), jml_anak: parseInt(g('f-jml_anak'))||0,
|
||||||
|
anak_putus_sekolah: parseInt(g('f-anak_putus_sekolah'))||0, penerima_beasiswa: g('f-penerima_beasiswa'),
|
||||||
|
bpjs: g('f-bpjs'), jml_sakit_kronis: parseInt(g('f-jml_sakit_kronis'))||0,
|
||||||
|
penyakit, history_bansos, catatan: g('f-catatan'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if(ln==='rumahIbadah'){
|
||||||
|
const nm=g('f-nama'); if(!nm){Toast.error('Nama Rumah Ibadah wajib diisi!');return null;}
|
||||||
|
return {
|
||||||
|
nama: nm, jenis: g('f-jenis'), ketua: g('f-ketua'), wa: g('f-wa'),
|
||||||
|
kapasitas: parseInt(g('f-kapasitas'))||0, radius: parseFloat(g('f-radius'))||300,
|
||||||
|
jml_binaan: parseInt(g('f-jml_binaan'))||0, program: g('f-program'),
|
||||||
|
dana_sosial: g('f-dana_sosial'), catatan: g('f-catatan')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
10. LEGEND
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
function renderLegend(ln){
|
||||||
|
const el=document.getElementById('legend-items');
|
||||||
|
const rows=CFG[ln].legend||[];
|
||||||
|
el.innerHTML=rows.map(r=>{
|
||||||
|
let sym='';
|
||||||
|
if(r.sym==='c') sym=`<span class="leg-c" style="background:${r.clr}"></span>`;
|
||||||
|
else if(r.sym==='l') sym=`<span class="leg-l" style="background:${r.clr}"></span>`;
|
||||||
|
else sym=`<span class="leg-p" style="border-color:${r.clr};background:${r.clr}33"></span>`;
|
||||||
|
return `<div class="legend-row">${sym}<span>${r.l}</span></div>`;
|
||||||
|
}).join('');
|
||||||
|
// Add radius note for rumahIbadah and bansos
|
||||||
|
if(ln==='rumahIbadah'||ln==='warga'){
|
||||||
|
el.innerHTML+=`<div class="legend-radius"><span class="leg-radius-sym"></span>Radius Hub 300m</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
6. POPUP BUILDERS
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Popup = {
|
||||||
|
_row:(k,v)=>`<div class="popup-row"><span class="popup-k">${k}</span><span class="popup-v">${H.esc(v||'-')}</span></div>`,
|
||||||
|
_sect:t=>`<div class="popup-section">${t}</div>`,
|
||||||
|
_acts:(ln,id)=>`<div class="popup-acts"><button class="bsm bsm-edit" onclick="App.crud.edit('${ln}','${id}')">Edit</button><button class="bsm bsm-del" onclick="App.crud.delete('${ln}','${id}')">Hapus</button></div>`,
|
||||||
|
|
||||||
|
spbu(r){ const cfg=CFG.spbu,clr=cfg.color(r),bdg=cfg.badge(r);
|
||||||
|
return `<div class="popup-wrap"><div class="popup-title"><span class="popup-sym" style="background:${clr}"></span>${H.esc(r.nama)}</div>
|
||||||
|
<span class="badge ${bdg.c}">${bdg.t}</span>
|
||||||
|
${this._sect('Operasional')}
|
||||||
|
${this._row('No. WA',r.wa)}${this._row('Jenis BBM',r.jenis_bbm)}${this._row('Harga BBM',r.harga_bbm)}${this._row('Akses Disabilitas',r.disabilitas)}
|
||||||
|
${r.catatan?this._row('Catatan',r.catatan):''}
|
||||||
|
${this._acts('spbu',r.id)}</div>`; },
|
||||||
|
|
||||||
|
jalan(r){ const cfg=CFG.jalan,bdg=cfg.badge(r);
|
||||||
|
return `<div class="popup-wrap"><div class="popup-title">🛣️ ${H.esc(r.nama)}</div>
|
||||||
|
<span class="badge ${bdg.c}">${bdg.t}</span>
|
||||||
|
${this._sect('Spesifikasi')}
|
||||||
|
${this._row('Panjang',H.fmDist(r.panjang))}${this._row('Lebar',r.lebar?(r.lebar+' m'):'-')}${this._row('Kondisi',r.kondisi)}${this._row('Perbaikan Terakhir',r.tahun_perbaikan||'-')}
|
||||||
|
${this._sect('Dampak Sosial')}
|
||||||
|
${this._row('Angkutan Umum',r.angkutan)}${this._row('Akses ke',r.akses_publik)}
|
||||||
|
${r.catatan?this._row('Catatan',r.catatan):''}
|
||||||
|
${this._acts('jalan',r.id)}</div>`; },
|
||||||
|
|
||||||
|
parsil(r){ const cfg=CFG.parsil,bdg=cfg.badge(r);
|
||||||
|
return `<div class="popup-wrap"><div class="popup-title">🏗️ ${H.esc(r.nama)}</div>
|
||||||
|
<span class="badge ${bdg.c}">${bdg.t}</span>
|
||||||
|
${this._sect('Data Hukum')}
|
||||||
|
${this._row('NIB',r.nib||'-')}${this._row('Status Sengketa',r.sengketa)}${this._row('Penggunaan',r.penggunaan)}
|
||||||
|
${this._sect('Penilaian')}
|
||||||
|
${this._row('Luas',H.fmArea(r.luas))}${this._row('NJOP/m²',r.njop?'Rp '+parseInt(r.njop).toLocaleString('id-ID'):'-')}
|
||||||
|
${r.catatan?this._row('Catatan',r.catatan):''}
|
||||||
|
${this._acts('parsil',r.id)}</div>`; },
|
||||||
|
|
||||||
|
warga(r){ const cfg=CFG.warga,clr=cfg.color(r),bdg=cfg.badge(r);
|
||||||
|
const ri=Store.all('rumahIbadah');
|
||||||
|
let nearestRI=null, nearestDist=Infinity;
|
||||||
|
ri.forEach(hub=>{
|
||||||
|
if(hub.lat==null||r.lat==null) return;
|
||||||
|
const d=L.latLng(r.lat,r.lng).distanceTo(L.latLng(hub.lat,hub.lng));
|
||||||
|
if(d<nearestDist){nearestDist=d;nearestRI=hub;}
|
||||||
|
});
|
||||||
|
const radius=nearestRI?(parseFloat(nearestRI.radius)||300):300;
|
||||||
|
const covered=nearestRI&&nearestDist<=radius;
|
||||||
|
const coverageHtml=nearestRI
|
||||||
|
?`<div class="coverage-badge ${covered?'covered':'uncovered'}">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="${covered?'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z':'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'}"/></svg>
|
||||||
|
${covered?'Terjangkau Hub: '+H.esc(nearestRI.nama)+' ('+Math.round(nearestDist)+' m)':'Tidak Terjangkau ('+Math.round(nearestDist)+' m dari RI)'}
|
||||||
|
</div>`
|
||||||
|
:'<div class="coverage-badge uncovered">Belum ada Rumah Ibadah Hub</div>';
|
||||||
|
const histHtml=r.history_bansos?.length
|
||||||
|
?r.history_bansos.map(h=>`<div style="font-size:10px;padding:3px 0;border-bottom:1px solid var(--border0);display:flex;justify-content:space-between"><span>${H.esc(h.program)} (${h.tahun||'-'})</span><span class="status-${(h.status||'').toLowerCase().replace(' ','')}" style="font-weight:700">${H.esc(h.status)}</span></div>`).join('')
|
||||||
|
:'<div style="font-size:10px;color:var(--text4)">Belum ada riwayat bansos</div>';
|
||||||
|
return `<div class="popup-wrap"><div class="popup-title"><span class="popup-sym" style="background:${clr}"></span>${H.esc(r.nama)}</div>
|
||||||
|
<span class="badge ${bdg.c}">${bdg.t}</span>
|
||||||
|
${coverageHtml}
|
||||||
|
${this._sect('Data Keluarga')}
|
||||||
|
${this._row('NIK',r.nik||'-')}${this._row('No. KK',r.no_kk||'-')}${this._row('Jml Anggota',r.jml_anggota+' jiwa')}
|
||||||
|
${this._sect('Ekonomi')}
|
||||||
|
${this._row('Pekerjaan KK',r.pekerjaan)}${this._row('Pendapatan/Bln',r.pendapatan?'Rp '+parseInt(r.pendapatan).toLocaleString('id-ID'):'-')}${this._row('Kondisi Rumah',r.kondisi_rumah)}
|
||||||
|
${this._sect('Fasilitas')}
|
||||||
|
${this._row('Air Bersih',r.air_bersih)}${this._row('Listrik',r.listrik)}
|
||||||
|
${this._sect('Bansos')}
|
||||||
|
${this._row('Bansos Saat Ini',r.bansos)}${this._row('Hub RI',r.hub_ri||'-')}
|
||||||
|
<div style="margin-top:2px">${histHtml}</div>
|
||||||
|
${r.catatan?this._row('Rekomendasi',r.catatan):''}
|
||||||
|
${this._acts('warga',r.id)}</div>`; },
|
||||||
|
|
||||||
|
rumahIbadah(r){ const cfg=CFG.rumahIbadah,clr=cfg.color(r),bdg=cfg.badge(r);
|
||||||
|
return `<div class="popup-wrap"><div class="popup-title"><span class="popup-sym" style="background:${clr}"></span>${H.esc(r.nama)}</div>
|
||||||
|
<span class="badge ${bdg.c}">${bdg.t}</span>
|
||||||
|
${this._sect('Kontak')}
|
||||||
|
${this._row('Ketua/Imam',r.ketua||'-')}${this._row('No. WA',r.wa||'-')}
|
||||||
|
${this._sect('Kapasitas & Binaan')}
|
||||||
|
${this._row('Kapasitas',r.kapasitas?r.kapasitas+' orang':'-')}${this._row('Radius Binaan',(r.radius||300)+' m')}${this._row('Warga Binaan',r.jml_binaan?r.jml_binaan+' KK':'-')}
|
||||||
|
${this._sect('Program Sosial')}
|
||||||
|
${this._row('Program Aktif',r.program)}${this._row('Dana Sosial/Bln',r.dana_sosial?'Rp '+parseInt(r.dana_sosial).toLocaleString('id-ID'):'-')}
|
||||||
|
${r.catatan?this._row('Catatan',r.catatan):''}
|
||||||
|
${this._acts('rumahIbadah',r.id)}</div>`; },
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
4. LAYER CONFIG (atribut + warna per layer)
|
||||||
|
── Ini adalah "kontrak data" antara programmer & client ──
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const CFG = {
|
||||||
|
spbu: {
|
||||||
|
label:'Data SPBU', sub:'Stasiun Bahan Bakar Umum', icon:'⛽', drawType:'point',
|
||||||
|
color: r => r.buka24?'#34d47e':'#f05c5c',
|
||||||
|
badge: r => r.buka24?{t:'24 Jam',c:'badge-green'}:{t:'Tutup',c:'badge-red'},
|
||||||
|
legend:[{l:'Buka 24 Jam',clr:'#34d47e',sym:'c'},{l:'Tidak 24 Jam',clr:'#f05c5c',sym:'c'}],
|
||||||
|
},
|
||||||
|
jalan: {
|
||||||
|
label:'Data Jalan', sub:'Ruas jalan berdasarkan status', icon:'🛣️', drawType:'polyline',
|
||||||
|
statuses:['Nasional','Provinsi','Kabupaten','Desa'],
|
||||||
|
colors:{'Nasional':'#f05c5c','Provinsi':'#f0a030','Kabupaten':'#5b9cf6','Desa':'#34d47e'},
|
||||||
|
badgeClass:{'Nasional':'badge-red','Provinsi':'badge-amber','Kabupaten':'badge-accent','Desa':'badge-green'},
|
||||||
|
kondisi:['Baik','Sedang','Rusak Ringan','Rusak Berat'],
|
||||||
|
color: r => ({'Nasional':'#f05c5c','Provinsi':'#f0a030','Kabupaten':'#5b9cf6','Desa':'#34d47e'}[r.status]||'#888'),
|
||||||
|
badge: r => ({t:r.status, c:{'Nasional':'badge-red','Provinsi':'badge-amber','Kabupaten':'badge-accent','Desa':'badge-green'}[r.status]||'badge-accent'}),
|
||||||
|
legend:[{l:'Nasional',clr:'#f05c5c',sym:'l'},{l:'Provinsi',clr:'#f0a030',sym:'l'},{l:'Kabupaten',clr:'#5b9cf6',sym:'l'},{l:'Desa',clr:'#34d47e',sym:'l'}],
|
||||||
|
},
|
||||||
|
parsil: {
|
||||||
|
label:'Data Parsil Tanah', sub:'Persil kavling berdasarkan kepemilikan', icon:'🏗️', drawType:'polygon',
|
||||||
|
statuses:['SHM','HGB','HGU','HP','Belum Bersertifikat'],
|
||||||
|
colors:{'SHM':'#34d47e','HGB':'#5b9cf6','HGU':'#f0a030','HP':'#9b7bfa','Belum Bersertifikat':'#f05c5c'},
|
||||||
|
badgeClass:{'SHM':'badge-green','HGB':'badge-accent','HGU':'badge-amber','HP':'badge-purple','Belum Bersertifikat':'badge-red'},
|
||||||
|
color: r => ({'SHM':'#34d47e','HGB':'#5b9cf6','HGU':'#f0a030','HP':'#9b7bfa','Belum Bersertifikat':'#f05c5c'}[r.status]||'#888'),
|
||||||
|
badge: r => ({t:r.status,c:{'SHM':'badge-green','HGB':'badge-accent','HGU':'badge-amber','HP':'badge-purple','Belum Bersertifikat':'badge-red'}[r.status]||'badge-accent'}),
|
||||||
|
legend:[{l:'SHM',clr:'#34d47e',sym:'p'},{l:'HGB',clr:'#5b9cf6',sym:'p'},{l:'HGU',clr:'#f0a030',sym:'p'},{l:'HP',clr:'#9b7bfa',sym:'p'},{l:'Belum Sertifikat',clr:'#f05c5c',sym:'p'}],
|
||||||
|
},
|
||||||
|
warga: {
|
||||||
|
label:'Data Warga & Bansos', sub:'Keluarga rentan & sasaran distribusi bansos', icon:'👨👩👧', drawType:'point',
|
||||||
|
tingkat:['Sangat Miskin','Miskin','Hampir Miskin','Rentan'],
|
||||||
|
colors:{'Sangat Miskin':'#f05c5c','Miskin':'#f0a030','Hampir Miskin':'#f0c040','Rentan':'#5b9cf6'},
|
||||||
|
badgeClass:{'Sangat Miskin':'badge-red','Miskin':'badge-orange','Hampir Miskin':'badge-amber','Rentan':'badge-accent'},
|
||||||
|
color: r => ({'Sangat Miskin':'#f05c5c','Miskin':'#f0a030','Hampir Miskin':'#f0c040','Rentan':'#5b9cf6'}[r.tingkat_kemiskinan]||'#888'),
|
||||||
|
badge: r => ({t:r.tingkat_kemiskinan,c:{'Sangat Miskin':'badge-red','Miskin':'badge-orange','Hampir Miskin':'badge-amber','Rentan':'badge-accent'}[r.tingkat_kemiskinan]||'badge-accent'}),
|
||||||
|
legend:[{l:'Sangat Miskin',clr:'#f05c5c',sym:'c'},{l:'Miskin',clr:'#f0a030',sym:'c'},{l:'Hampir Miskin',clr:'#f0c040',sym:'c'},{l:'Rentan',clr:'#5b9cf6',sym:'c'}],
|
||||||
|
bantuan:['PKH','BPNT/Sembako','BLT Dana Desa','BLT BBM','PIP/KIP','KIS/BPJS','Zakat/Infaq RI','Bansos Pemda','Belum Ada'],
|
||||||
|
kondisiRumah:['Permanen','Semi Permanen','Tidak Layak Huni','Menumpang'],
|
||||||
|
pekerjaan:['Buruh Harian','Petani/Nelayan','Pedagang Kecil','PNS/TNI/Polri','Pensiunan','Tidak Bekerja','Lainnya'],
|
||||||
|
statusPenerimaan:['Aktif Menerima','Belum Menerima','Proses Verifikasi','Pernah Menerima'],
|
||||||
|
sumberPendapatan:['Buruh Harian','Pedagang Kecil','Petani/Nelayan','PNS/TNI/Polri','Pensiunan','Tidak Bekerja','Lainnya'],
|
||||||
|
jenisPenyakit:['Hipertensi','Diabetes','TBC','Jantung','Stroke','Kanker','Gangguan Jiwa','Disabilitas','Tidak Ada'],
|
||||||
|
tingkatPendidikan:['Tidak Sekolah','SD/Sederajat','SMP/Sederajat','SMA/Sederajat','D3/S1','S2/S3'],
|
||||||
|
},
|
||||||
|
rumahIbadah: {
|
||||||
|
label:'Rumah Ibadah', sub:'Masjid, Gereja, Pura, Vihara, Klenteng', icon:'🕌', drawType:'point',
|
||||||
|
jenis:['Masjid','Musholla','Gereja Kristen','Gereja Katolik','Pura','Vihara','Klenteng'],
|
||||||
|
colors:{'Masjid':'#2ec4b6','Musholla':'#2ec4b6','Gereja Kristen':'#5b9cf6','Gereja Katolik':'#5b9cf6','Pura':'#f0a030','Vihara':'#9b7bfa','Klenteng':'#f07840'},
|
||||||
|
badgeClass:{'Masjid':'badge-teal','Musholla':'badge-teal','Gereja Kristen':'badge-accent','Gereja Katolik':'badge-accent','Pura':'badge-amber','Vihara':'badge-purple','Klenteng':'badge-orange'},
|
||||||
|
color: r => ({'Masjid':'#2ec4b6','Musholla':'#2ec4b6','Gereja Kristen':'#5b9cf6','Gereja Katolik':'#5b9cf6','Pura':'#f0a030','Vihara':'#9b7bfa','Klenteng':'#f07840'}[r.jenis]||'#888'),
|
||||||
|
badge: r => ({t:r.jenis,c:({'Masjid':'badge-teal','Musholla':'badge-teal','Gereja Kristen':'badge-accent','Gereja Katolik':'badge-accent','Pura':'badge-amber','Vihara':'badge-purple','Klenteng':'badge-orange'}[r.jenis]||'badge-teal')}),
|
||||||
|
legend:[{l:'Masjid/Musholla',clr:'#2ec4b6',sym:'c'},{l:'Gereja',clr:'#5b9cf6',sym:'c'},{l:'Pura',clr:'#f0a030',sym:'c'},{l:'Vihara/Klenteng',clr:'#9b7bfa',sym:'c'}],
|
||||||
|
coverageRadius: 300,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
12. DASHBOARD ANALITIK
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Dashboard = {
|
||||||
|
toggle(){
|
||||||
|
document.getElementById('dash-panel').classList.toggle('hidden');
|
||||||
|
if(!document.getElementById('dash-panel').classList.contains('hidden')) this.render();
|
||||||
|
},
|
||||||
|
render(){
|
||||||
|
const db=Store.allDb();
|
||||||
|
const warga=db.warga||[], ri=db.rumahIbadah||[], jalan=db.jalan||[], parsil=db.parsil||[];
|
||||||
|
const totalWarga=warga.reduce((s,r)=>s+(parseInt(r.jml_anggota)||1),0);
|
||||||
|
const miskinBerat=warga.filter(r=>r.tingkat_kemiskinan==='Sangat Miskin'||r.tingkat_kemiskinan==='Miskin').length;
|
||||||
|
const blmBansos=warga.filter(r=>r.bansos==='Belum Ada').length;
|
||||||
|
const blmSertif=parsil.filter(r=>r.status==='Belum Bersertifikat').length;
|
||||||
|
const jalanRusak=jalan.filter(r=>r.kondisi==='Rusak Berat'||r.kondisi==='Rusak Ringan').length;
|
||||||
|
const totalRI=ri.length;
|
||||||
|
|
||||||
|
// Radius coverage for Warga layer
|
||||||
|
let coveredKK=0, uncoveredKK=0;
|
||||||
|
const coverageHubs = ri.map(hub=>({ ...hub, count:0 }));
|
||||||
|
warga.forEach(b => {
|
||||||
|
if(!b.lat) return;
|
||||||
|
let nearestDist=Infinity, nearestHub=null;
|
||||||
|
ri.forEach(hub=>{
|
||||||
|
if(!hub.lat) return;
|
||||||
|
const d=L.latLng(b.lat,b.lng).distanceTo(L.latLng(hub.lat,hub.lng));
|
||||||
|
if(d<nearestDist){nearestDist=d;nearestHub=hub;}
|
||||||
|
});
|
||||||
|
if(nearestHub && nearestDist<=(parseFloat(nearestHub.radius)||300)){
|
||||||
|
coveredKK++;
|
||||||
|
const h = coverageHubs.find(h=>h.id===nearestHub.id);
|
||||||
|
if(h) h.count++;
|
||||||
|
} else {
|
||||||
|
uncoveredKK++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// tingkat kemiskinan breakdown
|
||||||
|
const tkBreak={};
|
||||||
|
CFG.warga.tingkat.forEach(t=>{ tkBreak[t]=warga.filter(r=>r.tingkat_kemiskinan===t).length; });
|
||||||
|
const maxTK=Math.max(...Object.values(tkBreak),1);
|
||||||
|
|
||||||
|
// bansos breakdown
|
||||||
|
const bansosBreak={};
|
||||||
|
CFG.warga.bantuan.forEach(b=>{ const c=warga.filter(r=>r.bansos===b).length; if(c>0) bansosBreak[b]=c; });
|
||||||
|
|
||||||
|
// jalan status breakdown
|
||||||
|
const jalanBreak={};
|
||||||
|
CFG.jalan.statuses.forEach(s=>{ jalanBreak[s]=jalan.filter(r=>r.status===s).length; });
|
||||||
|
|
||||||
|
document.getElementById('dash-body').innerHTML=`
|
||||||
|
<div class="dash-tab-bar">
|
||||||
|
<div class="dash-tab active" onclick="Dashboard.switchTab('ringkasan')">Ringkasan Utama</div>
|
||||||
|
<div class="dash-tab" onclick="Dashboard.switchTab('distribusi')">Distribusi Bansos</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB RINGKASAN -->
|
||||||
|
<div id="dash-panel-ringkasan">
|
||||||
|
<div class="stat-grid">
|
||||||
|
<div class="stat-card c-red"><div class="sc-val">${H.fmNum(warga.length)}</div><div class="sc-label">KK Warga Miskin Terdata</div><div class="sc-sub">${H.fmNum(totalWarga)} jiwa total</div></div>
|
||||||
|
<div class="stat-card c-amber"><div class="sc-val">${H.fmNum(miskinBerat)}</div><div class="sc-label">Miskin & Sangat Miskin</div><div class="sc-sub">${warga.length?Math.round(miskinBerat/warga.length*100):0}% dari total KK</div></div>
|
||||||
|
<div class="stat-card c-accent"><div class="sc-val">${H.fmNum(blmBansos)}</div><div class="sc-label">KK Belum Terima Bansos</div><div class="sc-sub">Perlu intervensi segera</div></div>
|
||||||
|
<div class="stat-card c-teal"><div class="sc-val">${H.fmNum(totalRI)}</div><div class="sc-label">Rumah Ibadah Aktif</div><div class="sc-sub">Agen lapangan terdaftar</div></div>
|
||||||
|
<div class="stat-card c-purple"><div class="sc-val">${H.fmNum(blmSertif)}</div><div class="sc-label">Parsil Belum Bersertifikat</div><div class="sc-sub">Kandidat program PTSL</div></div>
|
||||||
|
<div class="stat-card c-green"><div class="sc-val">${H.fmNum(jalanRusak)}</div><div class="sc-label">Ruas Jalan Rusak</div><div class="sc-sub">Dari ${H.fmNum(jalan.length)} ruas terdata</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dash-sect">
|
||||||
|
<div class="dash-sect-title">Sebaran Tingkat Kemiskinan</div>
|
||||||
|
<table class="risk-table"><thead><tr><th>Kategori</th><th>Jumlah KK</th><th>Proporsi</th></tr></thead><tbody>
|
||||||
|
${CFG.warga.tingkat.map(t=>{
|
||||||
|
const n=tkBreak[t]||0, pct=warga.length?Math.round(n/warga.length*100):0;
|
||||||
|
const clr={'Sangat Miskin':'#f05c5c','Miskin':'#f0a030','Hampir Miskin':'#f0c040','Rentan':'#5b9cf6'}[t];
|
||||||
|
return \`<tr><td>\${t}</td><td>\${H.fmNum(n)} KK</td><td><div class="risk-bar-wrap"><div class="risk-bar"><div class="risk-bar-fill" style="width:\${Math.round(n/maxTK*100)}%;background:\${clr}"></div></div>\${pct}%</div></td></tr>\`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody></table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dash-sect">
|
||||||
|
<div class="dash-sect-title">Penerima Program Bantuan Sosial</div>
|
||||||
|
${Object.entries(bansosBreak).length?Object.entries(bansosBreak).sort((a,b)=>b[1]-a[1]).map(([b,n])=>`
|
||||||
|
<div class="prog-row"><span class="prog-name">${b}</span><span class="prog-count">${n} KK</span></div>`).join('')
|
||||||
|
:'<div style="color:var(--text4);font-size:12px">Belum ada data warga tercatat.</div>'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dash-sect">
|
||||||
|
<div class="dash-sect-title">Status Ruas Jalan</div>
|
||||||
|
<table class="risk-table"><thead><tr><th>Status</th><th>Jumlah Ruas</th><th>Total Panjang</th></tr></thead><tbody>
|
||||||
|
${CFG.jalan.statuses.map(s=>{
|
||||||
|
const ruas=jalan.filter(r=>r.status===s);
|
||||||
|
const pj=ruas.reduce((t,r)=>t+(r.panjang||0),0);
|
||||||
|
return \`<tr><td><span class="badge \${CFG.jalan.badgeClass[s]}">\${s}</span></td><td>\${ruas.length} ruas</td><td>\${H.fmDist(pj)}</td></tr>\`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody></table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dash-sect">
|
||||||
|
<div class="dash-sect-title">Rekomendasi Intervensi Kebijakan</div>
|
||||||
|
<div class="intervensi-grid">
|
||||||
|
<div class="intervensi-card"><div class="ic-label">🏠 Prioritas Bansos</div>
|
||||||
|
<div class="ic-val">${H.fmNum(blmBansos)} KK</div>
|
||||||
|
<div class="ic-sub">Warga belum menerima bantuan apapun</div></div>
|
||||||
|
<div class="intervensi-card"><div class="ic-label">📜 Prioritas PTSL</div>
|
||||||
|
<div class="ic-val">${H.fmNum(blmSertif)} Parsil</div>
|
||||||
|
<div class="ic-sub">Lahan tanpa sertifikat, akses kredit terbatas</div></div>
|
||||||
|
<div class="intervensi-card"><div class="ic-label">🛣️ Prioritas Perbaikan Jalan</div>
|
||||||
|
<div class="ic-val">${H.fmNum(jalanRusak)} Ruas</div>
|
||||||
|
<div class="ic-sub">Jalan rusak ringan-berat perlu perbaikan</div></div>
|
||||||
|
<div class="intervensi-card"><div class="ic-label">🕌 Coverage Rumah Ibadah</div>
|
||||||
|
<div class="ic-val">${H.fmNum(totalRI)} RI Aktif</div>
|
||||||
|
<div class="ic-sub">Total dana sosial: Rp ${H.fmNum(ri.reduce((t,r)=>t+(parseInt(r.dana_sosial)||0),0))}</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB DISTRIBUSI BANSOS -->
|
||||||
|
<div id="dash-panel-distribusi" style="display:none">
|
||||||
|
<div class="coverage-grid">
|
||||||
|
<div class="coverage-card">
|
||||||
|
<div class="cc-icon green">✓</div>
|
||||||
|
<div class="cc-info">
|
||||||
|
<div class="cc-val">${coveredKK} KK</div>
|
||||||
|
<div class="cc-label">Terjangkau Hub (≤300m)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="coverage-card">
|
||||||
|
<div class="cc-icon red">✕</div>
|
||||||
|
<div class="cc-info">
|
||||||
|
<div class="cc-val">${uncoveredKK} KK</div>
|
||||||
|
<div class="cc-label">Di Luar Jangkauan Hub</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="coverage-card" style="grid-column:1/3;padding:16px">
|
||||||
|
<div class="cc-icon teal" style="width:40px;height:40px">🕌</div>
|
||||||
|
<div class="cc-info">
|
||||||
|
<div class="cc-val">${totalRI} Rumah Ibadah</div>
|
||||||
|
<div class="cc-label">Agen Distribusi Lapangan Terdaftar</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dash-sect">
|
||||||
|
<div class="dash-sect-title">Beban Distribusi per Rumah Ibadah</div>
|
||||||
|
<table class="hub-table">
|
||||||
|
<thead><tr><th>Nama Hub</th><th>Kapasitas</th><th>Beban KK</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
${coverageHubs.sort((a,b)=>b.count-a.count).map(h=>{
|
||||||
|
const clr=CFG.rumahIbadah.color(h);
|
||||||
|
const max = Math.max(...coverageHubs.map(x=>x.count), 1);
|
||||||
|
const pct = Math.round(h.count/max*100);
|
||||||
|
return \`<tr>
|
||||||
|
<td><div class="hub-name-cell"><span class="hub-dot" style="background:\${clr}"></span>\${H.esc(h.nama)}</div></td>
|
||||||
|
<td>\${h.jml_binaan||'-'} KK</td>
|
||||||
|
<td>
|
||||||
|
<div class="hub-count-bar">
|
||||||
|
<div class="hub-bar"><div class="hub-bar-fill" style="width:\${pct}%;background:\${pct>80?'var(--amber)':clr}"></div></div>
|
||||||
|
<span>\${h.count} KK</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>\`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
${coverageHubs.length===0?'<div style="text-align:center;color:var(--text4);font-size:11px;padding:20px">Belum ada data Rumah Ibadah.</div>':''}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
switchTab(tabId){
|
||||||
|
document.querySelectorAll('.dash-tab').forEach(t=>t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('[id^="dash-panel-"]').forEach(p=>p.style.display='none');
|
||||||
|
|
||||||
|
if(tabId === 'ringkasan'){
|
||||||
|
document.querySelectorAll('.dash-tab')[0].classList.add('active');
|
||||||
|
document.getElementById('dash-panel-ringkasan').style.display='block';
|
||||||
|
} else {
|
||||||
|
document.querySelectorAll('.dash-tab')[1].classList.add('active');
|
||||||
|
document.getElementById('dash-panel-distribusi').style.display='block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
8. DRAW MANAGER
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Draw = (() => {
|
||||||
|
let _map, _drawn, _pending, _handler, _active=false;
|
||||||
|
const BAR=()=>document.getElementById('draw-bar');
|
||||||
|
const TXT=()=>document.getElementById('draw-indicator-text');
|
||||||
|
|
||||||
|
function init(m){
|
||||||
|
_map=m; _drawn=new L.FeatureGroup().addTo(_map);
|
||||||
|
_map.on(L.Draw.Event.CREATED, e=>{
|
||||||
|
_drawn.clearLayers(); _drawn.addLayer(e.layer);
|
||||||
|
_pending=e.layer; stop(); Modal.openAdd(App.state.activeLayer, e.layer);
|
||||||
|
});
|
||||||
|
_map.on('click', e=>{
|
||||||
|
if(!_active||App.state.activeLayer!=='spbu'&&App.state.activeLayer!=='warga'&&App.state.activeLayer!=='rumahIbadah') return;
|
||||||
|
const mk=L.marker(e.latlng);
|
||||||
|
_drawn.clearLayers(); _drawn.addLayer(mk);
|
||||||
|
_pending=mk; stop(); Modal.openAdd(App.state.activeLayer, mk);
|
||||||
|
});
|
||||||
|
document.getElementById('btn-cancel').addEventListener('click', cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function start(ln){
|
||||||
|
_active=true;
|
||||||
|
const msgs={spbu:'Klik peta untuk menandai lokasi SPBU',jalan:'Klik titik-titik ruas jalan — klik ganda untuk selesai',parsil:'Klik titik-titik batas kavling — klik ganda untuk selesai',warga:'Klik peta untuk menandai lokasi rumah warga miskin',rumahIbadah:'Klik peta untuk menandai lokasi rumah ibadah'};
|
||||||
|
document.getElementById('draw-text').textContent=msgs[ln]||'Mode gambar aktif';
|
||||||
|
BAR().classList.remove('hidden');
|
||||||
|
if(ln==='jalan'){ _handler=new L.Draw.Polyline(_map,{shapeOptions:{color:'#f0a030',weight:4}}); _handler.enable(); }
|
||||||
|
else if(ln==='parsil'){ _handler=new L.Draw.Polygon(_map,{shapeOptions:{color:'#9b7bfa',weight:2,fillOpacity:.2}}); _handler.enable(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop(){ _active=false; BAR().classList.add('hidden'); if(_handler){_handler.disable();_handler=null;} }
|
||||||
|
function cancel(){ stop(); _drawn.clearLayers(); _pending=null; }
|
||||||
|
function clearPending(){ _drawn.clearLayers(); _pending=null; }
|
||||||
|
|
||||||
|
return {init, start, stop, cancel, clearPending};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
13. MAP RENDER
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
function renderMap(){
|
||||||
|
const db=Store.allDb();
|
||||||
|
// SPBU
|
||||||
|
App._groups.spbu.clearLayers();
|
||||||
|
db.spbu.forEach(r=>{
|
||||||
|
const m=L.circleMarker([r.lat,r.lng],{radius:9,fillColor:CFG.spbu.color(r),color:'#fff',weight:2,fillOpacity:1});
|
||||||
|
m.bindPopup(Popup.spbu(r),{maxWidth:320});
|
||||||
|
m.on('click',()=>highlightCard(r.id));
|
||||||
|
App._groups.spbu.addLayer(m);
|
||||||
|
});
|
||||||
|
// Warga
|
||||||
|
App._groups.warga.clearLayers();
|
||||||
|
db.warga.forEach(r=>{
|
||||||
|
const clr=CFG.warga.color(r);
|
||||||
|
// Check if in any RI radius
|
||||||
|
const inRadius=db.rumahIbadah.some(hub=>{
|
||||||
|
if(!hub.lat) return false;
|
||||||
|
const d=L.latLng(r.lat,r.lng).distanceTo(L.latLng(hub.lat,hub.lng));
|
||||||
|
return d<=(parseFloat(hub.radius)||300);
|
||||||
|
});
|
||||||
|
const iconHtml=`<div style="width:22px;height:22px;background:${clr};border:2px solid #fff;border-radius:50%;
|
||||||
|
display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,.4);
|
||||||
|
font-size:11px;line-height:1">👨👩👧</div>${inRadius?'':'<div style="position:absolute;top:-4px;right:-4px;width:10px;height:10px;background:#f05c5c;border-radius:50%;border:1px solid #fff"></div>'}`;
|
||||||
|
const icon=L.divIcon({html:`<div style="position:relative">${iconHtml}</div>`,className:'',iconSize:[22,22],iconAnchor:[11,11]});
|
||||||
|
const m=L.marker([r.lat,r.lng],{icon});
|
||||||
|
m.bindPopup(Popup.warga(r),{maxWidth:360});
|
||||||
|
m.on('click',()=>highlightCard(r.id));
|
||||||
|
App._groups.warga.addLayer(m);
|
||||||
|
});
|
||||||
|
// Rumah Ibadah
|
||||||
|
App._groups.rumahIbadah.clearLayers();
|
||||||
|
db.rumahIbadah.forEach(r=>{
|
||||||
|
const clr=CFG.rumahIbadah.color(r);
|
||||||
|
const m=L.circleMarker([r.lat,r.lng],{radius:10,fillColor:clr,color:'#fff',weight:2.5,fillOpacity:1});
|
||||||
|
m.bindPopup(Popup.rumahIbadah(r),{maxWidth:320});
|
||||||
|
m.on('click',()=>highlightCard(r.id));
|
||||||
|
App._groups.rumahIbadah.addLayer(m);
|
||||||
|
// Always draw radius 300m coverage circle
|
||||||
|
const radiusM=parseFloat(r.radius)||300;
|
||||||
|
const ring=L.circle([r.lat,r.lng],{radius:radiusM,color:clr,weight:1.5,fillColor:clr,fillOpacity:.06,dashArray:'6 4'});
|
||||||
|
App._groups.rumahIbadah.addLayer(ring);
|
||||||
|
});
|
||||||
|
// Jalan
|
||||||
|
App._groups.jalan.clearLayers();
|
||||||
|
db.jalan.forEach(r=>{
|
||||||
|
const l=L.polyline(r.coords,{color:CFG.jalan.color(r),weight:4,opacity:.9});
|
||||||
|
l.bindPopup(Popup.jalan(r),{maxWidth:300});
|
||||||
|
l.on('click',()=>highlightCard(r.id));
|
||||||
|
App._groups.jalan.addLayer(l);
|
||||||
|
});
|
||||||
|
// Parsil
|
||||||
|
App._groups.parsil.clearLayers();
|
||||||
|
db.parsil.forEach(r=>{
|
||||||
|
const clr=CFG.parsil.color(r);
|
||||||
|
const p=L.polygon(r.coords,{color:clr,weight:2,fillColor:clr,fillOpacity:.2});
|
||||||
|
p.bindPopup(Popup.parsil(r),{maxWidth:300});
|
||||||
|
p.on('click',()=>highlightCard(r.id));
|
||||||
|
App._groups.parsil.addLayer(p);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
9. MODAL MANAGER
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Modal = (() => {
|
||||||
|
let _ln=null, _editId=null, _geo=null;
|
||||||
|
const ov=()=>document.getElementById('modal-ov');
|
||||||
|
|
||||||
|
function openAdd(ln, leafletLayer){
|
||||||
|
_ln=ln; _editId=null; _geo=leafletLayer;
|
||||||
|
const cfg=CFG[ln];
|
||||||
|
document.getElementById('modal-icon').textContent=cfg.icon;
|
||||||
|
document.getElementById('modal-title').textContent='Tambah '+cfg.label;
|
||||||
|
document.getElementById('modal-sub').textContent='Isi atribut data baru';
|
||||||
|
let html='';
|
||||||
|
if(ln==='spbu') html=Forms.spbu(null, leafletLayer.getLatLng());
|
||||||
|
else if(ln==='jalan') html=Forms.jalan(null, H.geoLen(leafletLayer.getLatLngs()));
|
||||||
|
else if(ln==='parsil') html=Forms.parsil(null, H.geoArea(leafletLayer.getLatLngs()[0]));
|
||||||
|
else if(ln==='warga') html=Forms.warga(null, leafletLayer.getLatLng());
|
||||||
|
else if(ln==='rumahIbadah') html=Forms.rumahIbadah(null, leafletLayer.getLatLng());
|
||||||
|
document.getElementById('modal-body').innerHTML=html;
|
||||||
|
_bindRadio(); _bindFormTabs(); _bindHistBtn(); ov().classList.remove('hidden');
|
||||||
|
setTimeout(()=>document.getElementById('f-nama')?.focus(),60);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(ln, id){
|
||||||
|
_ln=ln; _editId=id; _geo=null;
|
||||||
|
const rec=Store.find(ln,id); if(!rec) return;
|
||||||
|
const cfg=CFG[ln];
|
||||||
|
document.getElementById('modal-icon').textContent=cfg.icon;
|
||||||
|
document.getElementById('modal-title').textContent='Edit '+cfg.label;
|
||||||
|
document.getElementById('modal-sub').textContent=H.esc(rec.nama);
|
||||||
|
let html='';
|
||||||
|
if(ln==='spbu') html=Forms.spbu(rec,null);
|
||||||
|
else if(ln==='jalan') html=Forms.jalan(rec,null);
|
||||||
|
else if(ln==='parsil') html=Forms.parsil(rec,null);
|
||||||
|
else if(ln==='warga') html=Forms.warga(rec,null);
|
||||||
|
else if(ln==='rumahIbadah') html=Forms.rumahIbadah(rec,null);
|
||||||
|
document.getElementById('modal-body').innerHTML=html;
|
||||||
|
_bindRadio(); _bindFormTabs(); _bindHistBtn(); ov().classList.remove('hidden');
|
||||||
|
setTimeout(()=>document.getElementById('f-nama')?.focus(),60);
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(){ ov().classList.add('hidden'); Draw.clearPending(); }
|
||||||
|
|
||||||
|
async function submit(){
|
||||||
|
const data=Forms.readAll(_ln); if(!data) return;
|
||||||
|
if(_editId){
|
||||||
|
data.updatedAt=H.today();
|
||||||
|
await Store.update(_ln,_editId,data);
|
||||||
|
Toast.success('Data berhasil diperbarui');
|
||||||
|
} else {
|
||||||
|
const geo=_geo;
|
||||||
|
if(_ln==='spbu'||_ln==='warga'||_ln==='rumahIbadah'){
|
||||||
|
const ll=geo.getLatLng(); data.lat=ll.lat; data.lng=ll.lng;
|
||||||
|
} else if(_ln==='jalan'){
|
||||||
|
data.coords=geo.getLatLngs().map(l=>[l.lat,l.lng]);
|
||||||
|
} else if(_ln==='parsil'){
|
||||||
|
data.coords=geo.getLatLngs()[0].map(l=>[l.lat,l.lng]);
|
||||||
|
}
|
||||||
|
data.id=H.uid(); data.createdAt=H.today(); data.updatedAt=H.today();
|
||||||
|
await Store.add(_ln,data);
|
||||||
|
Toast.success('Data berhasil disimpan');
|
||||||
|
}
|
||||||
|
close(); App.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bindRadio(){
|
||||||
|
document.querySelectorAll('.ropt').forEach(el=>{
|
||||||
|
el.addEventListener('click',()=>{
|
||||||
|
const parent=el.closest('.rtoggle'); if(!parent) return;
|
||||||
|
parent.querySelectorAll('.ropt').forEach(r=>r.classList.remove('yes','no'));
|
||||||
|
const v=el.dataset.v; el.classList.add(v==='true'?'yes':'no');
|
||||||
|
const inp=el.querySelector('input'); if(inp) inp.checked=true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Bind penyakit checkboxes
|
||||||
|
document.querySelectorAll('.penyakit-check input').forEach(inp=>{
|
||||||
|
inp.addEventListener('change', e=>{
|
||||||
|
if(e.target.checked) e.target.parentElement.classList.add('checked');
|
||||||
|
else e.target.parentElement.classList.remove('checked');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bindFormTabs(){
|
||||||
|
const tabs=document.querySelectorAll('.form-tab');
|
||||||
|
if(!tabs.length) return;
|
||||||
|
tabs.forEach(tab=>{
|
||||||
|
tab.addEventListener('click',()=>{
|
||||||
|
tabs.forEach(t=>t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.form-section-panel').forEach(p=>p.classList.remove('active'));
|
||||||
|
tab.classList.add('active');
|
||||||
|
const panelId='panel-'+tab.dataset.panel;
|
||||||
|
document.getElementById(panelId)?.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bindHistBtn(){
|
||||||
|
const btn=document.getElementById('btn-add-hist');
|
||||||
|
if(!btn) return;
|
||||||
|
btn.addEventListener('click',()=>{
|
||||||
|
const tbody=document.getElementById('hist-tbody');
|
||||||
|
if(!tbody) return;
|
||||||
|
const tr=document.createElement('tr');
|
||||||
|
tr.innerHTML=`
|
||||||
|
<td><input class="finput hist-program" style="padding:3px 6px;font-size:10px" placeholder="cth: PKH"/></td>
|
||||||
|
<td><input class="finput hist-tahun" style="padding:3px 6px;font-size:10px;width:70px" type="number" min="2000" max="2030" placeholder="${new Date().getFullYear()}"/></td>
|
||||||
|
<td><select class="finput hist-status" style="padding:3px 6px;font-size:10px">
|
||||||
|
<option>Aktif</option><option>Berhenti</option><option>Dalam Proses</option>
|
||||||
|
</select></td>
|
||||||
|
<td><input class="finput hist-nominal" style="padding:3px 6px;font-size:10px" type="number" placeholder="Rp"/></td>
|
||||||
|
<td><button class="btn-del-row" type="button" onclick="this.closest('tr').remove()">✕</button></td>`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {openAdd, openEdit, close, submit};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
11. SIDEBAR
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
function renderSidebar(ln){
|
||||||
|
const cfg=CFG[ln];
|
||||||
|
document.getElementById('sb-title').textContent=cfg.label;
|
||||||
|
document.getElementById('sb-sub').textContent=cfg.sub;
|
||||||
|
const data=Store.all(ln);
|
||||||
|
const list=document.getElementById('sb-list');
|
||||||
|
if(!data.length){
|
||||||
|
list.innerHTML=`<div class="empty"><svg width="44" height="44" viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg><p>Belum ada data.<br/>Klik <strong>Tambah</strong> untuk mulai digitasi.</p></div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.innerHTML=data.map(r=>Card[ln](r)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBadges(){
|
||||||
|
const db=Store.allDb();
|
||||||
|
['spbu','jalan','parsil','warga','rumahIbadah'].forEach(k=>{
|
||||||
|
const el=document.getElementById('badge-'+k);
|
||||||
|
if(el) el.textContent=db[k]?.length||0;
|
||||||
|
});
|
||||||
|
const total=Object.values(db).reduce((s,a)=>s+a.length,0);
|
||||||
|
document.getElementById('total-count').textContent=total+' fitur';
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightCard(id){
|
||||||
|
document.querySelectorAll('.dcard').forEach(c=>c.classList.remove('active'));
|
||||||
|
const c=document.getElementById('card-'+id);
|
||||||
|
if(c){ c.classList.add('active'); c.scrollIntoView({behavior:'smooth',block:'nearest'}); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
'use strict';
|
||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
1. HELPERS
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const H = {
|
||||||
|
uid: () => Math.random().toString(36).slice(2,9) + Date.now().toString(36),
|
||||||
|
esc: s => String(s||'').replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])),
|
||||||
|
fmDist: m => m==null?'-': m>=1000? (m/1000).toFixed(2)+' km': Math.round(m).toLocaleString('id-ID')+' m',
|
||||||
|
fmArea: m => m==null?'-': m>=10000? (m/10000).toFixed(4)+' ha': Math.round(m).toLocaleString('id-ID')+' m²',
|
||||||
|
fmNum: n => (n||0).toLocaleString('id-ID'),
|
||||||
|
today: () => new Date().toISOString().slice(0,10),
|
||||||
|
geoLen: lls => { let t=0; for(let i=0;i<lls.length-1;i++) t+=lls[i].distanceTo(lls[i+1]); return Math.round(t); },
|
||||||
|
geoArea: lls => {
|
||||||
|
const R=6371008.8; let a=0,n=lls.length;
|
||||||
|
for(let i=0;i<n;i++){const j=(i+1)%n,φ1=lls[i].lat*Math.PI/180,φ2=lls[j].lat*Math.PI/180,dλ=(lls[j].lng-lls[i].lng)*Math.PI/180;a+=dλ*(2+Math.sin(φ1)+Math.sin(φ2));}
|
||||||
|
return Math.round(Math.abs(a*R*R/2));
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
2. STORAGE (MySQL API Version)
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Store = (() => {
|
||||||
|
const API_URL = '../api/index.php'; // Or absolute URL if hosted elsewhere
|
||||||
|
|
||||||
|
let db = {
|
||||||
|
spbu: [],
|
||||||
|
jalan: [],
|
||||||
|
parsil: [],
|
||||||
|
warga: [],
|
||||||
|
rumahIbadah: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = async (action, payload = {}) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_URL}?action=${action}`, {
|
||||||
|
method: action === 'alldb' ? 'GET' : 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: action === 'alldb' ? null : JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
return await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('API Error:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
load: async () => {
|
||||||
|
try {
|
||||||
|
const data = await request('alldb');
|
||||||
|
if (data) {
|
||||||
|
db = { ...db, ...data };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Gagal load data dari server", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
all: ln => db[ln]||[],
|
||||||
|
find: (ln,id) => (db[ln]||[]).find(r=>r.id===id)||null,
|
||||||
|
add: async (ln,rec) => {
|
||||||
|
db[ln].push(rec);
|
||||||
|
await request('add', { layer: ln, data: rec });
|
||||||
|
},
|
||||||
|
update: async (ln,id,patch) => {
|
||||||
|
const i=db[ln].findIndex(r=>r.id===id); if(i<0)return;
|
||||||
|
db[ln][i]={...db[ln][i],...patch};
|
||||||
|
await request('update', { layer: ln, id: id, patch: patch });
|
||||||
|
},
|
||||||
|
remove: async (ln,id) => {
|
||||||
|
db[ln]=db[ln].filter(r=>r.id!==id);
|
||||||
|
await request('remove', { layer: ln, id: id });
|
||||||
|
},
|
||||||
|
allDb: () => db,
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════
|
||||||
|
3. TOAST
|
||||||
|
═══════════════════════════════════════════════════════════ */
|
||||||
|
const Toast = {
|
||||||
|
_show(msg,type){ const c=document.getElementById('toasts'),el=document.createElement('div');
|
||||||
|
el.className=`toast ${type}`;el.innerHTML=`<span>${{success:'✓',error:'✕',info:'ℹ'}[type]||'ℹ'}</span>${H.esc(msg)}`;
|
||||||
|
c.appendChild(el);setTimeout(()=>{el.classList.add('out');el.addEventListener('animationend',()=>el.remove(),{once:true})},2800); },
|
||||||
|
success: m=>Toast._show(m,'success'), error: m=>Toast._show(m,'error'), info: m=>Toast._show(m,'info'),
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<script src="app.js"></script>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
/* ═══════════════════════════════════════════════════════════
|
|
||||||
2. STORAGE
|
|
||||||
═══════════════════════════════════════════════════════════ */
|
|
||||||
const Store = (() => {
|
|
||||||
const KEYS = { spbu:'wg2_spbu', jalan:'wg2_jalan', parsil:'wg2_parsil', warga:'wg2_warga', rumahIbadah:'wg2_ri' };
|
|
||||||
const _r = k => { try{ return JSON.parse(localStorage.getItem(k)||'[]') }catch{return[]} };
|
|
||||||
const _w = (k,d) => { try{ localStorage.setItem(k,JSON.stringify(d)) }catch{} };
|
|
||||||
let db = Object.fromEntries(Object.entries(KEYS).map(([k,v])=>[k,_r(v)]));
|
|
||||||
return {
|
|
||||||
all: ln => db[ln]||[],
|
|
||||||
find:(ln,id) => (db[ln]||[]).find(r=>r.id===id)||null,
|
|
||||||
add:(ln,rec) => { db[ln].push(rec); _w(KEYS[ln],db[ln]); },
|
|
||||||
update:(ln,id,patch) => {
|
|
||||||
const i=db[ln].findIndex(r=>r.id===id); if(i<0)return;
|
|
||||||
db[ln][i]={...db[ln][i],...patch}; _w(KEYS[ln],db[ln]);
|
|
||||||
},
|
|
||||||
remove:(ln,id) => { db[ln]=db[ln].filter(r=>r.id!==id); _w(KEYS[ln],db[ln]); },
|
|
||||||
allDb: () => db,
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
Reference in New Issue
Block a user