From 017fe392bcadcd04fc8a8bc89bf323fb11f39175 Mon Sep 17 00:00:00 2001 From: TigerStable Date: Fri, 12 Jun 2026 16:36:31 +0700 Subject: [PATCH] update besar besaran --- api/db.php | 21 + api/index.php | 92 ++++ database.sql | 12 + implementation_plan.md | 49 -- index.html | 261 +++++++++++ {webgis => pertemuan_1}/README.md | 0 pertemuan_1/UAS_SIG | 1 + {webgis => pertemuan_1}/css/style.css | 0 pertemuan_1/index.html | 125 ++++++ pertemuan_1/js/app.js | 71 +++ {webgis => pertemuan_1}/js/components/card.js | 0 .../js/components/forms.js | 0 .../js/components/legend.js | 0 .../js/components/popup.js | 0 {webgis => pertemuan_1}/js/config/config.js | 0 .../js/modules/dashboard.js | 0 {webgis => pertemuan_1}/js/modules/draw.js | 0 {webgis => pertemuan_1}/js/modules/map.js | 0 {webgis => pertemuan_1}/js/modules/modal.js | 6 +- {webgis => pertemuan_1}/js/modules/sidebar.js | 0 {webgis => pertemuan_1}/js/utils/helpers.js | 0 pertemuan_1/js/utils/store.js | 55 +++ {webgis => pertemuan_1}/js/utils/toast.js | 0 {webgis => pertemuan_1}/p.text | 0 pertemuan_2/README.md | 165 +++++++ pertemuan_2/UAS_SIG | 1 + pertemuan_2/css/style.css | 420 ++++++++++++++++++ pertemuan_2/index.html | 133 ++++++ pertemuan_2/js/app.js | 71 +++ pertemuan_2/js/components/card.js | 63 +++ pertemuan_2/js/components/forms.js | 361 +++++++++++++++ pertemuan_2/js/components/legend.js | 18 + pertemuan_2/js/components/popup.js | 82 ++++ pertemuan_2/js/config/config.js | 57 +++ pertemuan_2/js/modules/dashboard.js | 182 ++++++++ pertemuan_2/js/modules/draw.js | 38 ++ pertemuan_2/js/modules/map.js | 63 +++ pertemuan_2/js/modules/modal.js | 119 +++++ pertemuan_2/js/modules/sidebar.js | 31 ++ pertemuan_2/js/utils/helpers.js | 18 + pertemuan_2/js/utils/store.js | 55 +++ pertemuan_2/js/utils/toast.js | 9 + pertemuan_2/p.text | 2 + pertemuan_3/README.md | 165 +++++++ pertemuan_3/UAS_SIG | 1 + pertemuan_3/css/style.css | 420 ++++++++++++++++++ {webgis => pertemuan_3}/index.html | 0 {webgis => pertemuan_3}/js/app.js | 7 +- pertemuan_3/js/components/card.js | 63 +++ pertemuan_3/js/components/forms.js | 361 +++++++++++++++ pertemuan_3/js/components/legend.js | 18 + pertemuan_3/js/components/popup.js | 82 ++++ pertemuan_3/js/config/config.js | 57 +++ pertemuan_3/js/modules/dashboard.js | 182 ++++++++ pertemuan_3/js/modules/draw.js | 38 ++ pertemuan_3/js/modules/map.js | 63 +++ pertemuan_3/js/modules/modal.js | 119 +++++ pertemuan_3/js/modules/sidebar.js | 31 ++ pertemuan_3/js/utils/helpers.js | 18 + pertemuan_3/js/utils/store.js | 57 +++ pertemuan_3/js/utils/toast.js | 9 + pertemuan_3/p.text | 2 + webgis/js/utils/store.js | 20 - 63 files changed, 4219 insertions(+), 75 deletions(-) create mode 100644 api/db.php create mode 100644 api/index.php create mode 100644 database.sql delete mode 100644 implementation_plan.md create mode 100644 index.html rename {webgis => pertemuan_1}/README.md (100%) create mode 160000 pertemuan_1/UAS_SIG rename {webgis => pertemuan_1}/css/style.css (100%) create mode 100644 pertemuan_1/index.html create mode 100644 pertemuan_1/js/app.js rename {webgis => pertemuan_1}/js/components/card.js (100%) rename {webgis => pertemuan_1}/js/components/forms.js (100%) rename {webgis => pertemuan_1}/js/components/legend.js (100%) rename {webgis => pertemuan_1}/js/components/popup.js (100%) rename {webgis => pertemuan_1}/js/config/config.js (100%) rename {webgis => pertemuan_1}/js/modules/dashboard.js (100%) rename {webgis => pertemuan_1}/js/modules/draw.js (100%) rename {webgis => pertemuan_1}/js/modules/map.js (100%) rename {webgis => pertemuan_1}/js/modules/modal.js (98%) rename {webgis => pertemuan_1}/js/modules/sidebar.js (100%) rename {webgis => pertemuan_1}/js/utils/helpers.js (100%) create mode 100644 pertemuan_1/js/utils/store.js rename {webgis => pertemuan_1}/js/utils/toast.js (100%) rename {webgis => pertemuan_1}/p.text (100%) create mode 100644 pertemuan_2/README.md create mode 160000 pertemuan_2/UAS_SIG create mode 100644 pertemuan_2/css/style.css create mode 100644 pertemuan_2/index.html create mode 100644 pertemuan_2/js/app.js create mode 100644 pertemuan_2/js/components/card.js create mode 100644 pertemuan_2/js/components/forms.js create mode 100644 pertemuan_2/js/components/legend.js create mode 100644 pertemuan_2/js/components/popup.js create mode 100644 pertemuan_2/js/config/config.js create mode 100644 pertemuan_2/js/modules/dashboard.js create mode 100644 pertemuan_2/js/modules/draw.js create mode 100644 pertemuan_2/js/modules/map.js create mode 100644 pertemuan_2/js/modules/modal.js create mode 100644 pertemuan_2/js/modules/sidebar.js create mode 100644 pertemuan_2/js/utils/helpers.js create mode 100644 pertemuan_2/js/utils/store.js create mode 100644 pertemuan_2/js/utils/toast.js create mode 100644 pertemuan_2/p.text create mode 100644 pertemuan_3/README.md create mode 160000 pertemuan_3/UAS_SIG create mode 100644 pertemuan_3/css/style.css rename {webgis => pertemuan_3}/index.html (100%) rename {webgis => pertemuan_3}/js/app.js (95%) create mode 100644 pertemuan_3/js/components/card.js create mode 100644 pertemuan_3/js/components/forms.js create mode 100644 pertemuan_3/js/components/legend.js create mode 100644 pertemuan_3/js/components/popup.js create mode 100644 pertemuan_3/js/config/config.js create mode 100644 pertemuan_3/js/modules/dashboard.js create mode 100644 pertemuan_3/js/modules/draw.js create mode 100644 pertemuan_3/js/modules/map.js create mode 100644 pertemuan_3/js/modules/modal.js create mode 100644 pertemuan_3/js/modules/sidebar.js create mode 100644 pertemuan_3/js/utils/helpers.js create mode 100644 pertemuan_3/js/utils/store.js create mode 100644 pertemuan_3/js/utils/toast.js create mode 100644 pertemuan_3/p.text delete mode 100644 webgis/js/utils/store.js diff --git a/api/db.php b/api/db.php new file mode 100644 index 0000000..273cbf5 --- /dev/null +++ b/api/db.php @@ -0,0 +1,21 @@ + 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; +} +?> diff --git a/api/index.php b/api/index.php new file mode 100644 index 0000000..c98c060 --- /dev/null +++ b/api/index.php @@ -0,0 +1,92 @@ +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']); +?> diff --git a/database.sql b/database.sql new file mode 100644 index 0000000..b2b7c59 --- /dev/null +++ b/database.sql @@ -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); diff --git a/implementation_plan.md b/implementation_plan.md deleted file mode 100644 index e91bb06..0000000 --- a/implementation_plan.md +++ /dev/null @@ -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. diff --git a/index.html b/index.html new file mode 100644 index 0000000..4fdd1e7 --- /dev/null +++ b/index.html @@ -0,0 +1,261 @@ + + + + + + Portal WebGIS - Praktikum SIG + + + + + + +
+

Portal WebGIS

+

Pilih modul pertemuan di bawah ini untuk mengakses sistem pemetaan geospasial.

+
+ +
+ + + +
+ πŸ“ Pertemuan 1 +
+

Fokus pada digitasi Point (Titik) untuk mendata persebaran dan informasi Stasiun Bahan Bakar Umum (SPBU).

+
+ Point + Data SPBU +
+
+ Buka Aplikasi + +
+
+ + + +
+ πŸ›£οΈ Pertemuan 2 +
+

Pengembangan digitasi spasial lanjutan dengan menambahkan fitur Polyline (Jalan) dan Polygon (Parsil Tanah).

+
+ Point + Polyline + Polygon +
+
+ Buka Aplikasi + +
+
+ + + +
+ 🌐 Pertemuan 3 +
+

Sistem komprehensif Distribusi Bansos Berbasis Rumah Ibadah dengan dashboard analitik dan radius spasial otomatis.

+
+ All Geometries + Data Warga + Dashboard Analitik +
+
+ Buka Aplikasi + +
+
+ +
+ + + + + diff --git a/webgis/README.md b/pertemuan_1/README.md similarity index 100% rename from webgis/README.md rename to pertemuan_1/README.md diff --git a/pertemuan_1/UAS_SIG b/pertemuan_1/UAS_SIG new file mode 160000 index 0000000..30bf02d --- /dev/null +++ b/pertemuan_1/UAS_SIG @@ -0,0 +1 @@ +Subproject commit 30bf02df8b737d68262f42931ee51ce9754963d3 diff --git a/webgis/css/style.css b/pertemuan_1/css/style.css similarity index 100% rename from webgis/css/style.css rename to pertemuan_1/css/style.css diff --git a/pertemuan_1/index.html b/pertemuan_1/index.html new file mode 100644 index 0000000..e112bd4 --- /dev/null +++ b/pertemuan_1/index.html @@ -0,0 +1,125 @@ + + + + + +WebGIS – Sistem Distribusi Bansos Berbasis Rumah Ibadah + + + + + + + +
+ + +
+
+
+ +
+
WebGIS Distribusi BansosBerbasis Rumah Ibadah β€” Radius 300m
+
+
+ + +
0 fitur
+
+ + +
+ + + + +
+
+ + + +
+
Legenda
+
+
+
+
+
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pertemuan_1/js/app.js b/pertemuan_1/js/app.js new file mode 100644 index 0000000..404ec1a --- /dev/null +++ b/pertemuan_1/js/app.js @@ -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:'© OpenStreetMap',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()); diff --git a/webgis/js/components/card.js b/pertemuan_1/js/components/card.js similarity index 100% rename from webgis/js/components/card.js rename to pertemuan_1/js/components/card.js diff --git a/webgis/js/components/forms.js b/pertemuan_1/js/components/forms.js similarity index 100% rename from webgis/js/components/forms.js rename to pertemuan_1/js/components/forms.js diff --git a/webgis/js/components/legend.js b/pertemuan_1/js/components/legend.js similarity index 100% rename from webgis/js/components/legend.js rename to pertemuan_1/js/components/legend.js diff --git a/webgis/js/components/popup.js b/pertemuan_1/js/components/popup.js similarity index 100% rename from webgis/js/components/popup.js rename to pertemuan_1/js/components/popup.js diff --git a/webgis/js/config/config.js b/pertemuan_1/js/config/config.js similarity index 100% rename from webgis/js/config/config.js rename to pertemuan_1/js/config/config.js diff --git a/webgis/js/modules/dashboard.js b/pertemuan_1/js/modules/dashboard.js similarity index 100% rename from webgis/js/modules/dashboard.js rename to pertemuan_1/js/modules/dashboard.js diff --git a/webgis/js/modules/draw.js b/pertemuan_1/js/modules/draw.js similarity index 100% rename from webgis/js/modules/draw.js rename to pertemuan_1/js/modules/draw.js diff --git a/webgis/js/modules/map.js b/pertemuan_1/js/modules/map.js similarity index 100% rename from webgis/js/modules/map.js rename to pertemuan_1/js/modules/map.js diff --git a/webgis/js/modules/modal.js b/pertemuan_1/js/modules/modal.js similarity index 98% rename from webgis/js/modules/modal.js rename to pertemuan_1/js/modules/modal.js index 39dd572..6bcfe6f 100644 --- a/webgis/js/modules/modal.js +++ b/pertemuan_1/js/modules/modal.js @@ -42,11 +42,11 @@ const Modal = (() => { function close(){ ov().classList.add('hidden'); Draw.clearPending(); } - function submit(){ + async function submit(){ const data=Forms.readAll(_ln); if(!data) return; if(_editId){ data.updatedAt=H.today(); - Store.update(_ln,_editId,data); + await Store.update(_ln,_editId,data); Toast.success('Data berhasil diperbarui'); } else { const geo=_geo; @@ -58,7 +58,7 @@ const Modal = (() => { data.coords=geo.getLatLngs()[0].map(l=>[l.lat,l.lng]); } 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'); } close(); App.refresh(); diff --git a/webgis/js/modules/sidebar.js b/pertemuan_1/js/modules/sidebar.js similarity index 100% rename from webgis/js/modules/sidebar.js rename to pertemuan_1/js/modules/sidebar.js diff --git a/webgis/js/utils/helpers.js b/pertemuan_1/js/utils/helpers.js similarity index 100% rename from webgis/js/utils/helpers.js rename to pertemuan_1/js/utils/helpers.js diff --git a/pertemuan_1/js/utils/store.js b/pertemuan_1/js/utils/store.js new file mode 100644 index 0000000..e9c7362 --- /dev/null +++ b/pertemuan_1/js/utils/store.js @@ -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, + }; +})(); diff --git a/webgis/js/utils/toast.js b/pertemuan_1/js/utils/toast.js similarity index 100% rename from webgis/js/utils/toast.js rename to pertemuan_1/js/utils/toast.js diff --git a/webgis/p.text b/pertemuan_1/p.text similarity index 100% rename from webgis/p.text rename to pertemuan_1/p.text diff --git a/pertemuan_2/README.md b/pertemuan_2/README.md new file mode 100644 index 0000000..22b2bd5 --- /dev/null +++ b/pertemuan_2/README.md @@ -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 + ``` +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.* diff --git a/pertemuan_2/UAS_SIG b/pertemuan_2/UAS_SIG new file mode 160000 index 0000000..30bf02d --- /dev/null +++ b/pertemuan_2/UAS_SIG @@ -0,0 +1 @@ +Subproject commit 30bf02df8b737d68262f42931ee51ce9754963d3 diff --git a/pertemuan_2/css/style.css b/pertemuan_2/css/style.css new file mode 100644 index 0000000..d55bb55 --- /dev/null +++ b/pertemuan_2/css/style.css @@ -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)} \ No newline at end of file diff --git a/pertemuan_2/index.html b/pertemuan_2/index.html new file mode 100644 index 0000000..3781d9a --- /dev/null +++ b/pertemuan_2/index.html @@ -0,0 +1,133 @@ + + + + + +WebGIS – Sistem Distribusi Bansos Berbasis Rumah Ibadah + + + + + + + +
+ + +
+
+
+ +
+
WebGIS Distribusi BansosBerbasis Rumah Ibadah β€” Radius 300m
+
+
+ + +
0 fitur
+
+ + +
+ + + + +
+
+ + + +
+
Legenda
+
+
+
+
+
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pertemuan_2/js/app.js b/pertemuan_2/js/app.js new file mode 100644 index 0000000..9df15c2 --- /dev/null +++ b/pertemuan_2/js/app.js @@ -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:'© OpenStreetMap',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()); diff --git a/pertemuan_2/js/components/card.js b/pertemuan_2/js/components/card.js new file mode 100644 index 0000000..881419b --- /dev/null +++ b/pertemuan_2/js/components/card.js @@ -0,0 +1,63 @@ +/* ═══════════════════════════════════════════════════════════ + 7. CARD BUILDERS (sidebar) +═══════════════════════════════════════════════════════════ */ +const Card = { + _acts:(ln,id)=>`
+ + +
`, + + spbu(r){ const clr=CFG.spbu.color(r),bdg=CFG.spbu.badge(r); + return `
+
+ + ${H.esc(r.nama)}${bdg.t}
+
+
WA${H.esc(r.wa||'-')}
+
BBM${H.esc(r.jenis_bbm||'-')}
+
${this._acts('spbu',r.id)}
`; }, + + jalan(r){ const clr=CFG.jalan.color(r),bdg=CFG.jalan.badge(r); + return `
+
+ + ${H.esc(r.nama)}${bdg.t}
+
+
Panjang${H.fmDist(r.panjang)}
+
Kondisi${H.esc(r.kondisi||'-')}
+
${this._acts('jalan',r.id)}
`; }, + + parsil(r){ const clr=CFG.parsil.color(r),bdg=CFG.parsil.badge(r); + return `
+
+ + ${H.esc(r.nama)}${bdg.t}
+
+
Luas${H.fmArea(r.luas)}
+
Sengketa${H.esc(r.sengketa||'-')}
+
${this._acts('parsil',r.id)}
`; }, + + 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 `
+
+ + ${H.esc(r.nama)}${bdg.t}
+
+
Anggota${r.jml_anggota||'-'} jiwa
+
Pendapatan${pendapatan}
+
Bansos${H.esc(r.bansos||'-')}
+
Riwayat${histCount} program
+
${this._acts('warga',r.id)}
`; }, + + rumahIbadah(r){ const clr=CFG.rumahIbadah.color(r),bdg=CFG.rumahIbadah.badge(r); + return `
+
+ + ${H.esc(r.nama)}${bdg.t}
+
+
Ketua${H.esc(r.ketua||'-')}
+
Binaan${r.jml_binaan||'-'} KK
+
${this._acts('rumahIbadah',r.id)}
`; }, +}; diff --git a/pertemuan_2/js/components/forms.js b/pertemuan_2/js/components/forms.js new file mode 100644 index 0000000..4ebe467 --- /dev/null +++ b/pertemuan_2/js/components/forms.js @@ -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 ` +
πŸ“ Koordinat: ${lat}, ${lng}
+
Identitas & Operasional
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
Aksesibilitas & Sosial
+
+
+
+
+
+
+
Catatan Lapangan
+
+
`; + }, + + jalan(rec, len){ + const pj=len??rec?.panjang??0; const v=f=>rec?H.esc(rec[f]||''):''; + return ` +
πŸ“ Panjang terukur otomatis: ${H.fmDist(pj)}
+ +
Identitas Ruas Jalan
+
+
+
+
+
+
+
Kondisi & Infrastruktur
+
+
+
+
+
+
+
+
+
Dampak Sosial-Ekonomi
+
+
+
+
+
+
+
+
`; + }, + + parsil(rec, area){ + const ls=area??rec?.luas??0; const v=f=>rec?H.esc(rec[f]||''):''; + return ` +
πŸ“ Luas terukur otomatis: ${H.fmArea(ls)}
+ +
Identitas Parsil
+
+
+
+
+
+
+
Status Kepemilikan
+
+
+
+
+
+
+
Penilaian & Pemanfaatan
+
+
+
+
+
+
+
+
`; + }, + + 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=>``).join(''); + + return ` +
⚠️ Data warga bersifat sensitif. Pastikan survei akurat.
+
πŸ“ Koordinat: ${lat}, ${lng}
+
+
Data KK
+
Ekonomi
+
Pddk
+
Kes
+
Bansos
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ ${CFG.warga.jenisPenyakit.map(p=>``).join('')} +
+
+
+ + +
+ + + + ${hist.map(h=>` + + + + + + `).join('')} + +
ProgramThnStatus
+ +
+
+
`; + }, + + 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 ` +
πŸ“ Koordinat: ${lat}, ${lng}
+
Identitas Rumah Ibadah
+
+
+
+
+
+
+
+
+
+
+
+
+
Kapasitas & Jangkauan
+
+
+
+
+
+
+
+
+
Program Sosial
+
+
+
+
+
+
+
+
`; + }, + + 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') + }; + } + } +}; diff --git a/pertemuan_2/js/components/legend.js b/pertemuan_2/js/components/legend.js new file mode 100644 index 0000000..755fa00 --- /dev/null +++ b/pertemuan_2/js/components/legend.js @@ -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=``; + else if(r.sym==='l') sym=``; + else sym=``; + return `
${sym}${r.l}
`; + }).join(''); + // Add radius note for rumahIbadah and bansos + if(ln==='rumahIbadah'||ln==='warga'){ + el.innerHTML+=`
Radius Hub 300m
`; + } +} diff --git a/pertemuan_2/js/components/popup.js b/pertemuan_2/js/components/popup.js new file mode 100644 index 0000000..ae05da7 --- /dev/null +++ b/pertemuan_2/js/components/popup.js @@ -0,0 +1,82 @@ +/* ═══════════════════════════════════════════════════════════ + 6. POPUP BUILDERS +═══════════════════════════════════════════════════════════ */ +const Popup = { + _row:(k,v)=>``, + _sect:t=>``, + _acts:(ln,id)=>``, + + spbu(r){ const cfg=CFG.spbu,clr=cfg.color(r),bdg=cfg.badge(r); + return ``; }, + + jalan(r){ const cfg=CFG.jalan,bdg=cfg.badge(r); + return ``; }, + + parsil(r){ const cfg=CFG.parsil,bdg=cfg.badge(r); + return ``; }, + + 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 + + ${covered?'Terjangkau Hub: '+H.esc(nearestRI.nama)+' ('+Math.round(nearestDist)+' m)':'Tidak Terjangkau ('+Math.round(nearestDist)+' m dari RI)'} + ` + :'
Belum ada Rumah Ibadah Hub
'; + const histHtml=r.history_bansos?.length + ?r.history_bansos.map(h=>`
${H.esc(h.program)} (${h.tahun||'-'})${H.esc(h.status)}
`).join('') + :'
Belum ada riwayat bansos
'; + return ``; }, + + rumahIbadah(r){ const cfg=CFG.rumahIbadah,clr=cfg.color(r),bdg=cfg.badge(r); + return ``; }, +}; diff --git a/pertemuan_2/js/config/config.js b/pertemuan_2/js/config/config.js new file mode 100644 index 0000000..6952a35 --- /dev/null +++ b/pertemuan_2/js/config/config.js @@ -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, + }, +}; diff --git a/pertemuan_2/js/modules/dashboard.js b/pertemuan_2/js/modules/dashboard.js new file mode 100644 index 0000000..6ae5095 --- /dev/null +++ b/pertemuan_2/js/modules/dashboard.js @@ -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(dh.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=` +
+
Ringkasan Utama
+
Distribusi Bansos
+
+ + +
+
+
${H.fmNum(warga.length)}
KK Warga Miskin Terdata
${H.fmNum(totalWarga)} jiwa total
+
${H.fmNum(miskinBerat)}
Miskin & Sangat Miskin
${warga.length?Math.round(miskinBerat/warga.length*100):0}% dari total KK
+
${H.fmNum(blmBansos)}
KK Belum Terima Bansos
Perlu intervensi segera
+
${H.fmNum(totalRI)}
Rumah Ibadah Aktif
Agen lapangan terdaftar
+
${H.fmNum(blmSertif)}
Parsil Belum Bersertifikat
Kandidat program PTSL
+
${H.fmNum(jalanRusak)}
Ruas Jalan Rusak
Dari ${H.fmNum(jalan.length)} ruas terdata
+
+ +
+
Sebaran Tingkat Kemiskinan
+ + ${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 \`\`; + }).join('')} +
KategoriJumlah KKProporsi
\${t}\${H.fmNum(n)} KK
\${pct}%
+
+ +
+
Penerima Program Bantuan Sosial
+ ${Object.entries(bansosBreak).length?Object.entries(bansosBreak).sort((a,b)=>b[1]-a[1]).map(([b,n])=>` +
${b}${n} KK
`).join('') + :'
Belum ada data warga tercatat.
'} +
+ +
+
Status Ruas Jalan
+ + ${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 \`\`; + }).join('')} +
StatusJumlah RuasTotal Panjang
\${s}\${ruas.length} ruas\${H.fmDist(pj)}
+
+ +
+
Rekomendasi Intervensi Kebijakan
+
+
🏠 Prioritas Bansos
+
${H.fmNum(blmBansos)} KK
+
Warga belum menerima bantuan apapun
+
πŸ“œ Prioritas PTSL
+
${H.fmNum(blmSertif)} Parsil
+
Lahan tanpa sertifikat, akses kredit terbatas
+
πŸ›£οΈ Prioritas Perbaikan Jalan
+
${H.fmNum(jalanRusak)} Ruas
+
Jalan rusak ringan-berat perlu perbaikan
+
πŸ•Œ Coverage Rumah Ibadah
+
${H.fmNum(totalRI)} RI Aktif
+
Total dana sosial: Rp ${H.fmNum(ri.reduce((t,r)=>t+(parseInt(r.dana_sosial)||0),0))}
+
+
+
+ + + `; + }, + + 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'; + } + } +}; diff --git a/pertemuan_2/js/modules/draw.js b/pertemuan_2/js/modules/draw.js new file mode 100644 index 0000000..4717ddf --- /dev/null +++ b/pertemuan_2/js/modules/draw.js @@ -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}; +})(); diff --git a/pertemuan_2/js/modules/map.js b/pertemuan_2/js/modules/map.js new file mode 100644 index 0000000..ee7bc7e --- /dev/null +++ b/pertemuan_2/js/modules/map.js @@ -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=`
πŸ‘¨β€πŸ‘©β€πŸ‘§
${inRadius?'':'
'}`; + const icon=L.divIcon({html:`
${iconHtml}
`,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); + }); +} diff --git a/pertemuan_2/js/modules/modal.js b/pertemuan_2/js/modules/modal.js new file mode 100644 index 0000000..6bcfe6f --- /dev/null +++ b/pertemuan_2/js/modules/modal.js @@ -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=` + + + + + `; + tbody.appendChild(tr); + }); + } + + return {openAdd, openEdit, close, submit}; +})(); diff --git a/pertemuan_2/js/modules/sidebar.js b/pertemuan_2/js/modules/sidebar.js new file mode 100644 index 0000000..6c8a2be --- /dev/null +++ b/pertemuan_2/js/modules/sidebar.js @@ -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=`

Belum ada data.
Klik Tambah untuk mulai digitasi.

`; + 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'}); } +} diff --git a/pertemuan_2/js/utils/helpers.js b/pertemuan_2/js/utils/helpers.js new file mode 100644 index 0000000..2d8aacc --- /dev/null +++ b/pertemuan_2/js/utils/helpers.js @@ -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 { + const R=6371008.8; let a=0,n=lls.length; + for(let i=0;i { + 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, + }; +})(); diff --git a/pertemuan_2/js/utils/toast.js b/pertemuan_2/js/utils/toast.js new file mode 100644 index 0000000..6622d78 --- /dev/null +++ b/pertemuan_2/js/utils/toast.js @@ -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=`${{success:'βœ“',error:'βœ•',info:'β„Ή'}[type]||'β„Ή'}${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'), +}; diff --git a/pertemuan_2/p.text b/pertemuan_2/p.text new file mode 100644 index 0000000..39eae7b --- /dev/null +++ b/pertemuan_2/p.text @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/pertemuan_3/README.md b/pertemuan_3/README.md new file mode 100644 index 0000000..22b2bd5 --- /dev/null +++ b/pertemuan_3/README.md @@ -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 + ``` +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.* diff --git a/pertemuan_3/UAS_SIG b/pertemuan_3/UAS_SIG new file mode 160000 index 0000000..30bf02d --- /dev/null +++ b/pertemuan_3/UAS_SIG @@ -0,0 +1 @@ +Subproject commit 30bf02df8b737d68262f42931ee51ce9754963d3 diff --git a/pertemuan_3/css/style.css b/pertemuan_3/css/style.css new file mode 100644 index 0000000..d55bb55 --- /dev/null +++ b/pertemuan_3/css/style.css @@ -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)} \ No newline at end of file diff --git a/webgis/index.html b/pertemuan_3/index.html similarity index 100% rename from webgis/index.html rename to pertemuan_3/index.html diff --git a/webgis/js/app.js b/pertemuan_3/js/app.js similarity index 95% rename from webgis/js/app.js rename to pertemuan_3/js/app.js index d0f3e72..17d0b92 100644 --- a/webgis/js/app.js +++ b/pertemuan_3/js/app.js @@ -5,7 +5,7 @@ const App = { state:{ activeLayer:'spbu' }, _groups:{}, - init(){ + 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',{ @@ -49,14 +49,15 @@ const App = { 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); }, - delete(ln,id){ + async delete(ln,id){ 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'); }, }, diff --git a/pertemuan_3/js/components/card.js b/pertemuan_3/js/components/card.js new file mode 100644 index 0000000..881419b --- /dev/null +++ b/pertemuan_3/js/components/card.js @@ -0,0 +1,63 @@ +/* ═══════════════════════════════════════════════════════════ + 7. CARD BUILDERS (sidebar) +═══════════════════════════════════════════════════════════ */ +const Card = { + _acts:(ln,id)=>`
+ + +
`, + + spbu(r){ const clr=CFG.spbu.color(r),bdg=CFG.spbu.badge(r); + return `
+
+ + ${H.esc(r.nama)}${bdg.t}
+
+
WA${H.esc(r.wa||'-')}
+
BBM${H.esc(r.jenis_bbm||'-')}
+
${this._acts('spbu',r.id)}
`; }, + + jalan(r){ const clr=CFG.jalan.color(r),bdg=CFG.jalan.badge(r); + return `
+
+ + ${H.esc(r.nama)}${bdg.t}
+
+
Panjang${H.fmDist(r.panjang)}
+
Kondisi${H.esc(r.kondisi||'-')}
+
${this._acts('jalan',r.id)}
`; }, + + parsil(r){ const clr=CFG.parsil.color(r),bdg=CFG.parsil.badge(r); + return `
+
+ + ${H.esc(r.nama)}${bdg.t}
+
+
Luas${H.fmArea(r.luas)}
+
Sengketa${H.esc(r.sengketa||'-')}
+
${this._acts('parsil',r.id)}
`; }, + + 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 `
+
+ + ${H.esc(r.nama)}${bdg.t}
+
+
Anggota${r.jml_anggota||'-'} jiwa
+
Pendapatan${pendapatan}
+
Bansos${H.esc(r.bansos||'-')}
+
Riwayat${histCount} program
+
${this._acts('warga',r.id)}
`; }, + + rumahIbadah(r){ const clr=CFG.rumahIbadah.color(r),bdg=CFG.rumahIbadah.badge(r); + return `
+
+ + ${H.esc(r.nama)}${bdg.t}
+
+
Ketua${H.esc(r.ketua||'-')}
+
Binaan${r.jml_binaan||'-'} KK
+
${this._acts('rumahIbadah',r.id)}
`; }, +}; diff --git a/pertemuan_3/js/components/forms.js b/pertemuan_3/js/components/forms.js new file mode 100644 index 0000000..4ebe467 --- /dev/null +++ b/pertemuan_3/js/components/forms.js @@ -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 ` +
πŸ“ Koordinat: ${lat}, ${lng}
+
Identitas & Operasional
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
Aksesibilitas & Sosial
+
+
+
+
+
+
+
Catatan Lapangan
+
+
`; + }, + + jalan(rec, len){ + const pj=len??rec?.panjang??0; const v=f=>rec?H.esc(rec[f]||''):''; + return ` +
πŸ“ Panjang terukur otomatis: ${H.fmDist(pj)}
+ +
Identitas Ruas Jalan
+
+
+
+
+
+
+
Kondisi & Infrastruktur
+
+
+
+
+
+
+
+
+
Dampak Sosial-Ekonomi
+
+
+
+
+
+
+
+
`; + }, + + parsil(rec, area){ + const ls=area??rec?.luas??0; const v=f=>rec?H.esc(rec[f]||''):''; + return ` +
πŸ“ Luas terukur otomatis: ${H.fmArea(ls)}
+ +
Identitas Parsil
+
+
+
+
+
+
+
Status Kepemilikan
+
+
+
+
+
+
+
Penilaian & Pemanfaatan
+
+
+
+
+
+
+
+
`; + }, + + 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=>``).join(''); + + return ` +
⚠️ Data warga bersifat sensitif. Pastikan survei akurat.
+
πŸ“ Koordinat: ${lat}, ${lng}
+
+
Data KK
+
Ekonomi
+
Pddk
+
Kes
+
Bansos
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ ${CFG.warga.jenisPenyakit.map(p=>``).join('')} +
+
+
+ + +
+ + + + ${hist.map(h=>` + + + + + + `).join('')} + +
ProgramThnStatus
+ +
+
+
`; + }, + + 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 ` +
πŸ“ Koordinat: ${lat}, ${lng}
+
Identitas Rumah Ibadah
+
+
+
+
+
+
+
+
+
+
+
+
+
Kapasitas & Jangkauan
+
+
+
+
+
+
+
+
+
Program Sosial
+
+
+
+
+
+
+
+
`; + }, + + 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') + }; + } + } +}; diff --git a/pertemuan_3/js/components/legend.js b/pertemuan_3/js/components/legend.js new file mode 100644 index 0000000..755fa00 --- /dev/null +++ b/pertemuan_3/js/components/legend.js @@ -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=``; + else if(r.sym==='l') sym=``; + else sym=``; + return `
${sym}${r.l}
`; + }).join(''); + // Add radius note for rumahIbadah and bansos + if(ln==='rumahIbadah'||ln==='warga'){ + el.innerHTML+=`
Radius Hub 300m
`; + } +} diff --git a/pertemuan_3/js/components/popup.js b/pertemuan_3/js/components/popup.js new file mode 100644 index 0000000..ae05da7 --- /dev/null +++ b/pertemuan_3/js/components/popup.js @@ -0,0 +1,82 @@ +/* ═══════════════════════════════════════════════════════════ + 6. POPUP BUILDERS +═══════════════════════════════════════════════════════════ */ +const Popup = { + _row:(k,v)=>``, + _sect:t=>``, + _acts:(ln,id)=>``, + + spbu(r){ const cfg=CFG.spbu,clr=cfg.color(r),bdg=cfg.badge(r); + return ``; }, + + jalan(r){ const cfg=CFG.jalan,bdg=cfg.badge(r); + return ``; }, + + parsil(r){ const cfg=CFG.parsil,bdg=cfg.badge(r); + return ``; }, + + 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 + + ${covered?'Terjangkau Hub: '+H.esc(nearestRI.nama)+' ('+Math.round(nearestDist)+' m)':'Tidak Terjangkau ('+Math.round(nearestDist)+' m dari RI)'} + ` + :'
Belum ada Rumah Ibadah Hub
'; + const histHtml=r.history_bansos?.length + ?r.history_bansos.map(h=>`
${H.esc(h.program)} (${h.tahun||'-'})${H.esc(h.status)}
`).join('') + :'
Belum ada riwayat bansos
'; + return ``; }, + + rumahIbadah(r){ const cfg=CFG.rumahIbadah,clr=cfg.color(r),bdg=cfg.badge(r); + return ``; }, +}; diff --git a/pertemuan_3/js/config/config.js b/pertemuan_3/js/config/config.js new file mode 100644 index 0000000..6952a35 --- /dev/null +++ b/pertemuan_3/js/config/config.js @@ -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, + }, +}; diff --git a/pertemuan_3/js/modules/dashboard.js b/pertemuan_3/js/modules/dashboard.js new file mode 100644 index 0000000..6ae5095 --- /dev/null +++ b/pertemuan_3/js/modules/dashboard.js @@ -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(dh.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=` +
+
Ringkasan Utama
+
Distribusi Bansos
+
+ + +
+
+
${H.fmNum(warga.length)}
KK Warga Miskin Terdata
${H.fmNum(totalWarga)} jiwa total
+
${H.fmNum(miskinBerat)}
Miskin & Sangat Miskin
${warga.length?Math.round(miskinBerat/warga.length*100):0}% dari total KK
+
${H.fmNum(blmBansos)}
KK Belum Terima Bansos
Perlu intervensi segera
+
${H.fmNum(totalRI)}
Rumah Ibadah Aktif
Agen lapangan terdaftar
+
${H.fmNum(blmSertif)}
Parsil Belum Bersertifikat
Kandidat program PTSL
+
${H.fmNum(jalanRusak)}
Ruas Jalan Rusak
Dari ${H.fmNum(jalan.length)} ruas terdata
+
+ +
+
Sebaran Tingkat Kemiskinan
+ + ${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 \`\`; + }).join('')} +
KategoriJumlah KKProporsi
\${t}\${H.fmNum(n)} KK
\${pct}%
+
+ +
+
Penerima Program Bantuan Sosial
+ ${Object.entries(bansosBreak).length?Object.entries(bansosBreak).sort((a,b)=>b[1]-a[1]).map(([b,n])=>` +
${b}${n} KK
`).join('') + :'
Belum ada data warga tercatat.
'} +
+ +
+
Status Ruas Jalan
+ + ${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 \`\`; + }).join('')} +
StatusJumlah RuasTotal Panjang
\${s}\${ruas.length} ruas\${H.fmDist(pj)}
+
+ +
+
Rekomendasi Intervensi Kebijakan
+
+
🏠 Prioritas Bansos
+
${H.fmNum(blmBansos)} KK
+
Warga belum menerima bantuan apapun
+
πŸ“œ Prioritas PTSL
+
${H.fmNum(blmSertif)} Parsil
+
Lahan tanpa sertifikat, akses kredit terbatas
+
πŸ›£οΈ Prioritas Perbaikan Jalan
+
${H.fmNum(jalanRusak)} Ruas
+
Jalan rusak ringan-berat perlu perbaikan
+
πŸ•Œ Coverage Rumah Ibadah
+
${H.fmNum(totalRI)} RI Aktif
+
Total dana sosial: Rp ${H.fmNum(ri.reduce((t,r)=>t+(parseInt(r.dana_sosial)||0),0))}
+
+
+
+ + + `; + }, + + 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'; + } + } +}; diff --git a/pertemuan_3/js/modules/draw.js b/pertemuan_3/js/modules/draw.js new file mode 100644 index 0000000..4717ddf --- /dev/null +++ b/pertemuan_3/js/modules/draw.js @@ -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}; +})(); diff --git a/pertemuan_3/js/modules/map.js b/pertemuan_3/js/modules/map.js new file mode 100644 index 0000000..ee7bc7e --- /dev/null +++ b/pertemuan_3/js/modules/map.js @@ -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=`
πŸ‘¨β€πŸ‘©β€πŸ‘§
${inRadius?'':'
'}`; + const icon=L.divIcon({html:`
${iconHtml}
`,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); + }); +} diff --git a/pertemuan_3/js/modules/modal.js b/pertemuan_3/js/modules/modal.js new file mode 100644 index 0000000..6bcfe6f --- /dev/null +++ b/pertemuan_3/js/modules/modal.js @@ -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=` + + + + + `; + tbody.appendChild(tr); + }); + } + + return {openAdd, openEdit, close, submit}; +})(); diff --git a/pertemuan_3/js/modules/sidebar.js b/pertemuan_3/js/modules/sidebar.js new file mode 100644 index 0000000..6c8a2be --- /dev/null +++ b/pertemuan_3/js/modules/sidebar.js @@ -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=`

Belum ada data.
Klik Tambah untuk mulai digitasi.

`; + 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'}); } +} diff --git a/pertemuan_3/js/utils/helpers.js b/pertemuan_3/js/utils/helpers.js new file mode 100644 index 0000000..2d8aacc --- /dev/null +++ b/pertemuan_3/js/utils/helpers.js @@ -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 { + const R=6371008.8; let a=0,n=lls.length; + for(let i=0;i { + 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, + }; +})(); diff --git a/pertemuan_3/js/utils/toast.js b/pertemuan_3/js/utils/toast.js new file mode 100644 index 0000000..6622d78 --- /dev/null +++ b/pertemuan_3/js/utils/toast.js @@ -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=`${{success:'βœ“',error:'βœ•',info:'β„Ή'}[type]||'β„Ή'}${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'), +}; diff --git a/pertemuan_3/p.text b/pertemuan_3/p.text new file mode 100644 index 0000000..39eae7b --- /dev/null +++ b/pertemuan_3/p.text @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/webgis/js/utils/store.js b/webgis/js/utils/store.js deleted file mode 100644 index 23480d1..0000000 --- a/webgis/js/utils/store.js +++ /dev/null @@ -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, - }; -})();