diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e12869f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,60 @@
+# WebGIS Pemetaan Kemiskinan Kota Pontianak
+
+Aplikasi WebGIS sederhana berbasis Leaflet.js untuk memetakan data kemiskinan, sebaran penduduk miskin, log bantuan, rumah ibadah, SPBU, serta data jalan dan parsil tanah di Kota Pontianak.
+
+## Fitur Utama
+
+- **Pemetaan Interaktif**: Visualisasi peta dengan Leaflet.js yang berpusat di Kota Pontianak.
+- **Manajemen Data Spasial**:
+ - **SPBU**: Lokasi titik SPBU beserta informasi layanan 24 jam.
+ - **Jalan**: Fitur line/polyline dengan kategori status jalan (Nasional, Provinsi, Kabupaten).
+ - **Parsil**: Fitur area/polygon tanah dengan kategori sertifikasi (SHM, HGB, HGU, HP).
+ - **Rumah Ibadah**: Lokasi titik rumah ibadah (Masjid, Gereja, Vihara, dll) beserta jangkauan radius jemaah/bantuan.
+ - **Penduduk Miskin**: Pemetaan warga kurang mampu dan integrasi log/riwayat bantuan sosial yang disalurkan melalui rumah ibadah terdekat.
+- **Pencarian Cepat**: Cari data jalan, parsil, SPBU, atau rumah ibadah langsung dari bilah pencarian.
+- **Impor GeoJSON Eksternal**: Pengguna dapat menambahkan file GeoJSON dari perangkat lokal ke dalam peta.
+
+## Persyaratan Sistem
+
+- **Server Web**: Apache (misal menggunakan XAMPP atau Laragon).
+- **Bahasa Pemrograman**: PHP 7.4 ke atas.
+- **Database**: MySQL versi 5.7+ atau MariaDB versi 10.2+ (Wajib mendukung fitur fungsi spasial seperti `ST_GeomFromGeoJSON` dan `ST_AsGeoJSON`).
+
+## Cara Instalasi & Penggunaan
+
+1. **Unduh/Clone Repositori**:
+ Tempatkan folder `webgis-poverty-pontianak` di dalam direktori root server web Anda (misalnya `C:/xampp/htdocs/webgis/webgis-poverty-pontianak`).
+
+2. **Setup Database**:
+ - Buka phpMyAdmin atau MySQL client pilihan Anda.
+ - Buat database baru bernama `webgis`:
+ ```sql
+ CREATE DATABASE webgis;
+ ```
+ - Import file `database.sql` ke dalam database `webgis` tersebut.
+
+3. **Konfigurasi Koneksi**:
+ - Buka file [api/db_connect.php](api/db_connect.php).
+ - Sesuaikan konfigurasi host, user, password, dan database sesuai dengan setup database MySQL lokal Anda:
+ ```php
+ $host = "localhost";
+ $user = "root";
+ $pass = "";
+ $db = "webgis";
+ ```
+
+4. **Jalankan Aplikasi**:
+ - Aktifkan modul **Apache** dan **MySQL** di Control Panel XAMPP Anda.
+ - Buka browser dan navigasikan ke alamat:
+ [http://localhost/webgis/webgis-poverty-pontianak/index.php](http://localhost/webgis/webgis-poverty-pontianak/index.php)
+
+## Struktur Folder
+
+- `api/` — Kumpulan REST API endpoint untuk operasi CRUD masing-masing fitur.
+ - `jalan/`, `parsil/`, `spbu/`, `rumah_ibadah/`, `penduduk_miskin/`, `log_bantuan/`
+- `assets/` — File aset frontend.
+ - `css/` — Gaya UI (custom style.css).
+ - `js/` — Logika frontend peta dan pengelolaan interaksi panel data.
+ - `features/` — Modul fitur Leaflet (menggambar peta, manajemen layer, geolokasi).
+- `index.php` — Halaman utama interface WebGIS.
+- `database.sql` — Skema database beserta data dummy Pontianak untuk pengetesan awal.
diff --git a/api/penduduk_miskin/bulk_create.php b/api/penduduk_miskin/bulk_create.php
new file mode 100644
index 0000000..e8f60d3
--- /dev/null
+++ b/api/penduduk_miskin/bulk_create.php
@@ -0,0 +1,43 @@
+begin_transaction();
+ $inserted = 0;
+ $errors = [];
+ foreach ($data as $item) {
+ if (isset($item->lat) && isset($item->lng) && $item->lat !== '' && $item->lng !== '') {
+ $nama = $conn->real_escape_string($item->nama ?? '');
+ $kategori = $conn->real_escape_string($item->kategori_bantuan ?? 'Makan');
+ $jumlah_jiwa = (int)($item->jumlah_jiwa ?? 1);
+ $lat = (float)$item->lat;
+ $lng = (float)$item->lng;
+
+ $query = "INSERT INTO penduduk_miskin (nama, kategori_bantuan, jumlah_jiwa, lat, lng)
+ VALUES ('$nama', '$kategori', $jumlah_jiwa, $lat, $lng)";
+
+ if (!$conn->query($query)) {
+ $errors[] = $conn->error;
+ } else {
+ $inserted++;
+ }
+ }
+ }
+
+ if (empty($errors) && $inserted > 0) {
+ $conn->commit();
+ echo json_encode(["status" => "success", "message" => "Berhasil mengimpor $inserted data penduduk miskin."]);
+ } else if ($inserted === 0) {
+ $conn->rollback();
+ echo json_encode(["status" => "error", "message" => "Tidak ada data valid yang diimpor."]);
+ } else {
+ $conn->rollback();
+ echo json_encode(["status" => "error", "message" => "Gagal mengimpor beberapa data. Transaksi dibatalkan.", "errors" => $errors]);
+ }
+} else {
+ echo json_encode(["status" => "error", "message" => "Format data salah."]);
+}
+?>
diff --git a/assets/css/style.css b/assets/css/style.css
index f80837f..0ef9b57 100644
--- a/assets/css/style.css
+++ b/assets/css/style.css
@@ -841,3 +841,55 @@ body.right-panel-open .custom-layer-panel {
.emoji-marker .bubble.ibadah { background: linear-gradient(135deg, #f97316, #ea580c); }
.emoji-marker .bubble.miskin-in { background: linear-gradient(135deg, #ef4444, #dc2626); }
.emoji-marker .bubble.miskin-out { background: linear-gradient(135deg, #22c55e, #16a34a); }
+
+/* ===== Toast Notification ===== */
+#toastContainer {
+ position: fixed;
+ bottom: 28px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 9999;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 10px;
+ pointer-events: none;
+}
+
+.toast {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 12px 20px;
+ border-radius: 12px;
+ font-size: 13px;
+ font-weight: 500;
+ color: white;
+ box-shadow: 0 6px 24px rgba(0,0,0,0.18);
+ backdrop-filter: blur(6px);
+ pointer-events: auto;
+ animation: toastSlideIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) both;
+ max-width: 340px;
+ text-align: center;
+}
+
+.toast.hiding {
+ animation: toastSlideOut 0.3s ease forwards;
+}
+
+.toast-success { background: linear-gradient(135deg, #22c55e, #16a34a); }
+.toast-error { background: linear-gradient(135deg, #ef4444, #dc2626); }
+.toast-info { background: linear-gradient(135deg, #6366f1, #4f46e5); }
+.toast-warning { background: linear-gradient(135deg, #f59e0b, #d97706); }
+
+.toast-icon { font-size: 16px; flex-shrink: 0; }
+
+@keyframes toastSlideIn {
+ 0% { opacity: 0; transform: translateY(20px) scale(0.9); }
+ 100% { opacity: 1; transform: translateY(0) scale(1); }
+}
+
+@keyframes toastSlideOut {
+ 0% { opacity: 1; transform: translateY(0) scale(1); }
+ 100% { opacity: 0; transform: translateY(10px) scale(0.95); }
+}
diff --git a/assets/js/features/geojson.js b/assets/js/features/geojson.js
index f44330b..7c4fab9 100644
--- a/assets/js/features/geojson.js
+++ b/assets/js/features/geojson.js
@@ -16,6 +16,18 @@ window.toggleGeoJsonMenu = function() {
}
};
+// Cek apakah fitur GeoJSON ini terlihat seperti data penduduk miskin
+function isMiskinFeature(feature) {
+ if (!feature || !feature.geometry || feature.geometry.type !== 'Point') return false;
+ const props = feature.properties || {};
+ const keys = Object.keys(props).map(k => k.toLowerCase());
+ // Dianggap data miskin jika punya properti nama (dan bukan ibadah atau spbu)
+ const hasNama = keys.some(k => ['nama', 'name', 'penduduk'].includes(k));
+ const isIbadah = keys.some(k => ['jenis', 'radius', 'alamat'].includes(k));
+ const isSpbu = keys.some(k => ['alamat_spbu', 'is_24_jam'].includes(k));
+ return hasNama && !isIbadah && !isSpbu;
+}
+
fileGeoJson.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
@@ -25,58 +37,143 @@ fileGeoJson.addEventListener('change', function(e) {
try {
const geoJsonData = JSON.parse(event.target.result);
const fileName = file.name;
- const layerId = 'gj_' + (++geoJsonCounter);
-
- // Buat FeatureGroup khusus untuk file ini
- const individualLayer = L.featureGroup();
-
- // Tambahkan ke map
- L.geoJSON(geoJsonData, {
- onEachFeature: function (feature, layer) {
- if (feature.properties) {
- let popupContent = '
Informasi Feature ';
- for (let key in feature.properties) {
- popupContent += `${key}: ${feature.properties[key]} `;
- }
- popupContent += ' ';
- layer.bindPopup(popupContent);
- }
- },
- style: function(feature) {
+
+ // Pisahkan fitur menjadi dua kelompok: miskin dan non-miskin
+ const features = geoJsonData.type === 'FeatureCollection'
+ ? geoJsonData.features
+ : [geoJsonData];
+
+ const miskinFeatures = features.filter(f => isMiskinFeature(f));
+ const otherFeatures = features.filter(f => !isMiskinFeature(f));
+
+ // ---- Proses fitur miskin: Simpan ke DB, lalu tampilkan seperti penduduk miskin ----
+ if (miskinFeatures.length > 0) {
+ const bulkData = miskinFeatures.map(f => {
+ const props = f.properties || {};
+ const coords = f.geometry.coordinates;
return {
- color: feature.properties.color || '#3388ff',
- weight: 2,
- fillOpacity: 0.4
+ nama: props.nama || props.name || props.penduduk || 'Data Impor',
+ kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan',
+ jumlah_jiwa: parseInt(props.jumlah_jiwa || props.jumlah || props.jiwa || 1, 10) || 1,
+ lat: parseFloat(coords[1]),
+ lng: parseFloat(coords[0])
};
+ });
+
+ fetch('api/penduduk_miskin/bulk_create.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(bulkData)
+ })
+ .then(res => res.json())
+ .then(data => {
+ if (data.status === 'success') {
+ // Reload layer penduduk miskin agar marker tampil dengan edit/hapus/log bantuan
+ if (typeof loadPendudukMiskin === 'function') {
+ loadPendudukMiskin();
+ }
+ if (typeof window.refreshActivePanel === 'function') {
+ window.refreshActivePanel();
+ }
+ showToast(data.message || 'Data berhasil diimpor ke layer Penduduk Miskin.', 'success');
+ } else {
+ showToast(data.message || 'Gagal menyimpan data miskin.', 'error');
+ }
+ })
+ .catch(err => {
+ console.error(err);
+ showToast('Gagal terhubung ke server saat menyimpan data miskin.', 'error');
+ });
+ }
+
+ // ---- Proses fitur non-miskin: Render sebagai layer GeoJSON biasa ----
+ if (otherFeatures.length > 0) {
+ const layerId = 'gj_' + (++geoJsonCounter);
+ const individualLayer = L.featureGroup();
+
+ const otherGeoJson = {
+ type: 'FeatureCollection',
+ features: otherFeatures
+ };
+
+ L.geoJSON(otherGeoJson, {
+ pointToLayer: function(feature, latlng) {
+ const props = feature.properties || {};
+ let emoji = props.emoji;
+
+ if (!emoji) {
+ const text = JSON.stringify(props).toLowerCase();
+ if (text.includes('spbu')) emoji = '⛽';
+ else if (text.includes('masjid')) emoji = '🕌';
+ else if (text.includes('gereja')) emoji = '⛪';
+ else if (text.includes('vihara')) emoji = '🪷';
+ else if (text.includes('pura')) emoji = '🛕';
+ else if (text.includes('kelenteng')) emoji = '🏮';
+ else emoji = '📍';
+ }
+
+ let cls = 'miskin-out';
+ if (emoji === '⛽') cls = 'spbu-24';
+ else if (['🕌','⛪','🛕','🪷','🏮'].includes(emoji)) cls = 'ibadah';
+
+ const icon = L.divIcon({
+ className: '',
+ html: ``,
+ iconSize: [38, 38],
+ iconAnchor: [19, 38],
+ popupAnchor: [0, -40]
+ });
+
+ return L.marker(latlng, { icon: icon });
+ },
+ onEachFeature: function(feature, layer) {
+ if (feature.properties) {
+ let popupContent = 'Informasi Feature ';
+ for (let key in feature.properties) {
+ popupContent += `${key}: ${feature.properties[key]} `;
+ }
+ popupContent += ' ';
+ layer.bindPopup(popupContent);
+ }
+ },
+ style: function(feature) {
+ return {
+ color: (feature.properties && feature.properties.color) || '#3388ff',
+ weight: 2,
+ fillOpacity: 0.4
+ };
+ }
+ }).addTo(individualLayer);
+
+ individualLayer.addTo(map);
+ geoJsonLayers[layerId] = individualLayer;
+
+ // Tambahkan checkbox ke UI
+ const container = document.getElementById('geoJsonLayersContainer');
+ const label = document.createElement('label');
+ label.className = 'layer-option';
+ label.innerHTML = ` ${fileName}`;
+
+ label.querySelector('input').addEventListener('change', function(e) {
+ if (e.target.checked) {
+ map.addLayer(geoJsonLayers[layerId]);
+ } else {
+ map.removeLayer(geoJsonLayers[layerId]);
+ }
+ });
+
+ container.appendChild(label);
+
+ if (miskinFeatures.length === 0) {
+ showToast('File GeoJSON berhasil dimuat!', 'success');
}
- }).addTo(individualLayer);
-
- individualLayer.addTo(map);
- geoJsonLayers[layerId] = individualLayer;
-
- // Tambahkan checkbox ke UI
- const container = document.getElementById('geoJsonLayersContainer');
- const label = document.createElement('label');
- label.className = 'layer-option';
- label.innerHTML = ` ${fileName}`;
-
- label.querySelector('input').addEventListener('change', function(e) {
- if (e.target.checked) {
- map.addLayer(geoJsonLayers[layerId]);
- } else {
- map.removeLayer(geoJsonLayers[layerId]);
- }
- });
-
- container.appendChild(label);
-
- // Reset input file agar bisa import file yang sama jika dihapus
+ }
+
+ // Reset input file agar bisa import file yang sama
fileGeoJson.value = '';
-
- alert('File GeoJSON berhasil dimuat!');
} catch (error) {
- alert('Gagal memproses file GeoJSON. Pastikan format valid.');
+ showToast('Gagal memproses file GeoJSON. Pastikan format valid.', 'error');
console.error(error);
}
};
diff --git a/assets/js/features/kemiskinan.js b/assets/js/features/kemiskinan.js
index f606edf..d592a02 100644
--- a/assets/js/features/kemiskinan.js
+++ b/assets/js/features/kemiskinan.js
@@ -552,6 +552,237 @@ function updateSemuaWarnaMiskin() {
});
}
+// ==========================
+// ===== Import Feature =====
+// ==========================
+let parsedImportData = [];
+
+window.openImportMiskinModal = function() {
+ parsedImportData = []; // Reset data
+
+ const bodyHTML = `
+
+
+
+ Pratinjau Data (0 item)
+
+
+
+
+
+
+ Nama
+ Kategori
+ Jiwa
+ Koordinat
+
+
+
+
+
+
+
+ `;
+
+ map.closePopup();
+
+ openModal("Impor Penduduk Miskin", bodyHTML, function() {
+ if (parsedImportData.length === 0) {
+ showToast('Silakan pilih file terlebih dahulu atau pastikan data valid.', 'warning');
+ return;
+ }
+
+ // Kirim data ke API bulk_create.php
+ fetch('api/penduduk_miskin/bulk_create.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(parsedImportData)
+ })
+ .then(res => res.json())
+ .then(data => {
+ if (data.status === 'success') {
+ closeModal();
+ loadPendudukMiskin();
+ showToast(data.message, 'success');
+ } else {
+ showToast(data.message || 'Gagal mengimpor data.', 'error');
+ }
+ })
+ .catch(err => {
+ console.error(err);
+ showToast('Terjadi kesalahan saat menghubungi server.', 'error');
+ });
+ });
+
+ // Register file input listener
+ document.getElementById('importMiskinFile').addEventListener('change', handleImportFileChange);
+};
+
+function parseCSV(text) {
+ const lines = text.split(/\r?\n/);
+ if (lines.length < 2) return [];
+
+ const firstLine = lines[0];
+ let delimiter = ',';
+ if (firstLine.includes(';')) delimiter = ';';
+ else if (firstLine.includes('\t')) delimiter = '\t';
+
+ const headers = firstLine.split(delimiter).map(h => h.trim().replace(/^["']|["']$/g, '').toLowerCase());
+
+ const results = [];
+ for (let i = 1; i < lines.length; i++) {
+ const line = lines[i].trim();
+ if (!line) continue;
+
+ let fields = [];
+ let currentField = '';
+ let insideQuote = false;
+
+ for (let j = 0; j < line.length; j++) {
+ const char = line[j];
+ if (char === '"' || char === "'") {
+ insideQuote = !insideQuote;
+ } else if (char === delimiter && !insideQuote) {
+ fields.push(currentField.trim());
+ currentField = '';
+ } else {
+ currentField += char;
+ }
+ }
+ fields.push(currentField.trim());
+
+ fields = fields.map(f => f.replace(/^["']|["']$/g, ''));
+
+ const row = {};
+ headers.forEach((header, index) => {
+ row[header] = fields[index] || '';
+ });
+ results.push(row);
+ }
+ return results;
+}
+
+function handleImportFileChange(e) {
+ const file = e.target.files[0];
+ const previewContainer = document.getElementById('importPreviewContainer');
+ const previewCount = document.getElementById('importPreviewCount');
+ const previewTableBody = document.getElementById('importPreviewTableBody');
+ const errorMsg = document.getElementById('importErrorMsg');
+
+ if (!file) {
+ previewContainer.style.display = 'none';
+ parsedImportData = [];
+ return;
+ }
+
+ errorMsg.style.display = 'none';
+ previewContainer.style.display = 'none';
+ previewTableBody.innerHTML = '';
+ parsedImportData = [];
+
+ const reader = new FileReader();
+ reader.onload = function(event) {
+ try {
+ const text = event.target.result;
+ let rawData = [];
+
+ if (file.name.endsWith('.csv')) {
+ rawData = parseCSV(text);
+ } else if (file.name.endsWith('.json') || file.name.endsWith('.geojson')) {
+ const json = JSON.parse(text);
+ if (json.type === 'FeatureCollection' && Array.isArray(json.features)) {
+ rawData = json.features.map(f => {
+ const props = f.properties || {};
+ const coords = f.geometry && f.geometry.coordinates;
+ return {
+ nama: props.nama || props.name || props.penduduk || '',
+ kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan',
+ jumlah_jiwa: props.jumlah_jiwa || props.jumlah || props.jiwa || 1,
+ lat: coords ? coords[1] : (props.lat || props.latitude),
+ lng: coords ? coords[0] : (props.lng || props.longitude)
+ };
+ });
+ } else if (Array.isArray(json)) {
+ rawData = json;
+ } else {
+ throw new Error("Format JSON tidak didukung.");
+ }
+ }
+
+ if (rawData.length === 0) {
+ errorMsg.textContent = "File kosong atau format tidak didukung.";
+ errorMsg.style.display = 'block';
+ return;
+ }
+
+ const validRows = [];
+ let invalidCount = 0;
+
+ rawData.forEach(row => {
+ const nama = row.nama || row.name || row.penduduk || row['nama lengkap'] || row.fullname || '';
+ const kategori_bantuan = row.kategori_bantuan || row.kategori || row.bantuan || row['kategori bantuan'] || 'Makan';
+ const jumlah_jiwa = parseInt(row.jumlah_jiwa || row.jumlah || row.jiwa || row['jumlah jiwa'] || 1, 10) || 1;
+ const lat = parseFloat(row.lat || row.latitude || row.y);
+ const lng = parseFloat(row.lng || row.longitude || row.long || row.x);
+
+ const isValid = !isNaN(lat) && !isNaN(lng) && lat !== 0 && lng !== 0 && nama.trim() !== '';
+
+ if (isValid) {
+ validRows.push({ nama, kategori_bantuan, jumlah_jiwa, lat, lng });
+ } else {
+ invalidCount++;
+ }
+ });
+
+ parsedImportData = validRows;
+
+ if (validRows.length === 0) {
+ errorMsg.textContent = "Tidak ditemukan data valid (pastikan kolom nama, lat, dan lng terisi).";
+ errorMsg.style.display = 'block';
+ return;
+ }
+
+ validRows.forEach(row => {
+ const tr = document.createElement('tr');
+ tr.style.borderBottom = '1px solid #e2e8f0';
+ tr.innerHTML = `
+ ${row.nama}
+ ${row.kategori_bantuan}
+ ${row.jumlah_jiwa}
+ ${row.lat.toFixed(5)}, ${row.lng.toFixed(5)}
+ `;
+ previewTableBody.appendChild(tr);
+ });
+
+ previewCount.textContent = validRows.length;
+ previewContainer.style.display = 'block';
+
+ const statusEl = document.getElementById('importPreviewStatus');
+ if (statusEl) {
+ if (invalidCount > 0) {
+ statusEl.innerHTML = ` ${invalidCount} baris tidak valid diabaikan `;
+ } else {
+ statusEl.innerHTML = ` Semua data valid `;
+ }
+ }
+
+ } catch (err) {
+ console.error(err);
+ errorMsg.textContent = "Gagal memproses file. Pastikan format file sesuai.";
+ errorMsg.style.display = 'block';
+ }
+ };
+ reader.readAsText(file);
+}
+
// Initial Load
loadRumahIbadah();
loadPendudukMiskin();
diff --git a/assets/js/map.js b/assets/js/map.js
index 714f5f1..44e0c23 100644
--- a/assets/js/map.js
+++ b/assets/js/map.js
@@ -2,6 +2,23 @@
// Koordinat awal: [-0.0263, 109.3425] (Pontianak)
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
+// ===== Toast Notification =====
+window.showToast = function(message, type = 'success', duration = 3500) {
+ const icons = { success: '✅', error: '❌', info: 'ℹ️', warning: '⚠️' };
+ const container = document.getElementById('toastContainer');
+ if (!container) return;
+
+ const toast = document.createElement('div');
+ toast.className = `toast toast-${type}`;
+ toast.innerHTML = `${icons[type] || 'ℹ️'} ${message} `;
+ container.appendChild(toast);
+
+ setTimeout(() => {
+ toast.classList.add('hiding');
+ toast.addEventListener('animationend', () => toast.remove());
+ }, duration);
+};
+
// Custom Zoom Control Logic
document.getElementById('zoomInBtn').addEventListener('click', function() { map.zoomIn(); });
document.getElementById('zoomOutBtn').addEventListener('click', function() { map.zoomOut(); });
diff --git a/assets/js/panel.js b/assets/js/panel.js
index 67939c9..c380d8c 100644
--- a/assets/js/panel.js
+++ b/assets/js/panel.js
@@ -347,7 +347,11 @@
});
document.getElementById('btnPanelImportMiskin').addEventListener('click', () => {
- alert('Fitur impor CSV akan segera tersedia.');
+ if (typeof window.openImportMiskinModal === 'function') {
+ window.openImportMiskinModal();
+ } else {
+ alert('Fitur impor belum siap.');
+ }
});
const all = [];
diff --git a/database.sql b/database.sql
new file mode 100644
index 0000000..fd63156
--- /dev/null
+++ b/database.sql
@@ -0,0 +1,117 @@
+-- Database schema for WebGIS Pemetaan Kemiskinan Pontianak
+
+CREATE DATABASE IF NOT EXISTS webgis;
+USE webgis;
+
+-- 1. Tabel rumah_ibadah
+DROP TABLE IF EXISTS `log_bantuan`;
+DROP TABLE IF EXISTS `rumah_ibadah`;
+CREATE TABLE `rumah_ibadah` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `nama` varchar(255) NOT NULL,
+ `jenis` varchar(100) NOT NULL,
+ `alamat` text,
+ `radius` double NOT NULL DEFAULT 0,
+ `lat` double NOT NULL,
+ `lng` double NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 2. Tabel penduduk_miskin
+DROP TABLE IF EXISTS `penduduk_miskin`;
+CREATE TABLE `penduduk_miskin` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `nama` varchar(255) NOT NULL,
+ `kategori_bantuan` varchar(255) NOT NULL,
+ `jumlah_jiwa` int(11) NOT NULL DEFAULT 1,
+ `lat` double NOT NULL,
+ `lng` double NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 3. Tabel log_bantuan
+CREATE TABLE `log_bantuan` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `miskin_id` int(11) NOT NULL,
+ `ibadah_id` int(11) NOT NULL,
+ `tipe_bantuan` varchar(255) NOT NULL,
+ `tanggal` date NOT NULL,
+ `keterangan` text,
+ PRIMARY KEY (`id`),
+ KEY `fk_log_miskin` (`miskin_id`),
+ KEY `fk_log_ibadah` (`ibadah_id`),
+ CONSTRAINT `fk_log_ibadah` FOREIGN KEY (`ibadah_id`) REFERENCES `rumah_ibadah` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `fk_log_miskin` FOREIGN KEY (`miskin_id`) REFERENCES `penduduk_miskin` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 4. Tabel jalan
+DROP TABLE IF EXISTS `jalan`;
+CREATE TABLE `jalan` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `nama` varchar(255) NOT NULL,
+ `status` varchar(100) NOT NULL DEFAULT 'Kabupaten',
+ `panjang` double NOT NULL DEFAULT 0,
+ `geom` geometry NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 5. Tabel parsil
+DROP TABLE IF EXISTS `parsil`;
+CREATE TABLE `parsil` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `nama` varchar(255) NOT NULL,
+ `status` varchar(100) NOT NULL DEFAULT 'SHM',
+ `luas` double NOT NULL DEFAULT 0,
+ `geom` geometry NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 6. Tabel spbu
+DROP TABLE IF EXISTS `spbu`;
+CREATE TABLE `spbu` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `nama` varchar(255) NOT NULL,
+ `no_wa` varchar(20) DEFAULT NULL,
+ `is_24_jam` tinyint(1) NOT NULL DEFAULT 0,
+ `lat` double NOT NULL,
+ `lng` double NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- Data Contoh (Sample Data) untuk Pontianak
+
+-- Data Contoh Rumah Ibadah
+INSERT INTO `rumah_ibadah` (`nama`, `jenis`, `alamat`, `radius`, `lat`, `lng`) VALUES
+('Masjid Raya Mujahidin', 'Masjid', 'Jl. Jenderal Ahmad Yani, Akcaya, Kec. Pontianak Sel.', 500, -0.0435, 109.3440),
+('Gereja Katedral Santo Yosef', 'Gereja', 'Jl. Patria Tama, Darat Sekip, Kec. Pontianak Kota', 500, -0.0278, 109.3370),
+('Vihara Bodhisatva Karaniya Metta', 'Vihara', 'Jl. Kapten Marsan, Darat Sekip, Kec. Pontianak Kota', 500, -0.0232, 109.3432);
+
+-- Data Contoh Penduduk Miskin
+INSERT INTO `penduduk_miskin` (`nama`, `kategori_bantuan`, `jumlah_jiwa`, `lat`, `lng`) VALUES
+('Budi Santoso', 'Makan', 4, -0.0420, 109.3415),
+('Siti Aminah', 'Pemberdayaan', 3, -0.0285, 109.3395),
+('Ahmad Subarjo', 'Makan', 5, -0.0245, 109.3448);
+
+-- Data Contoh Log Bantuan
+INSERT INTO `log_bantuan` (`miskin_id`, `ibadah_id`, `tipe_bantuan`, `tanggal`, `keterangan`) VALUES
+(1, 1, 'Sembako', '2026-05-15', 'Pembagian paket sembako bulanan'),
+(2, 2, 'Pendidikan', '2026-05-20', 'Bantuan biaya sekolah anak'),
+(3, 1, 'Uang Tunai', '2026-06-01', 'Santunan tunai darurat');
+
+-- Data Contoh SPBU
+INSERT INTO `spbu` (`nama`, `no_wa`, `is_24_jam`, `lat`, `lng`) VALUES
+('SPBU Ahmad Yani', '081234567890', 1, -0.0485, 109.3490),
+('SPBU Oesman Sapta Odang', '081234567891', 0, -0.0345, 109.3512),
+('SPBU Kota Baru', '081234567892', 1, -0.0512, 109.3198);
+
+-- Data Contoh Jalan (Polyline)
+-- Menggunakan ST_GeomFromText untuk membuat LineString
+INSERT INTO `jalan` (`nama`, `status`, `panjang`, `geom`) VALUES
+('Jl. Jenderal Ahmad Yani', 'Nasional', 2500, ST_GeomFromText('LINESTRING(109.3400 -0.0380, 109.3450 -0.0450, 109.3500 -0.0520)')),
+('Jl. Gajah Mada', 'Provinsi', 1800, ST_GeomFromText('LINESTRING(109.3350 -0.0290, 109.3410 -0.0280, 109.3480 -0.0270)'));
+
+-- Data Contoh Parsil (Polygon)
+-- Menggunakan ST_GeomFromText untuk membuat Polygon (pastikan koordinat berputar kembali ke titik awal untuk menutup polygon)
+INSERT INTO `parsil` (`nama`, `status`, `luas`, `geom`) VALUES
+('Parsil Tanah A', 'SHM', 150, ST_GeomFromText('POLYGON((109.3360 -0.0250, 109.3375 -0.0250, 109.3375 -0.0260, 109.3360 -0.0260, 109.3360 -0.0250))')),
+('Parsil Tanah B', 'HGB', 320, ST_GeomFromText('POLYGON((109.3405 -0.0310, 109.3420 -0.0310, 109.3420 -0.0330, 109.3405 -0.0330, 109.3405 -0.0310))'));
diff --git a/index.php b/index.php
index 345e427..ff36493 100644
--- a/index.php
+++ b/index.php
@@ -290,6 +290,9 @@
+
+
+
diff --git a/test_miskin.csv b/test_miskin.csv
new file mode 100644
index 0000000..d9bc426
--- /dev/null
+++ b/test_miskin.csv
@@ -0,0 +1,4 @@
+nama,kategori_bantuan,jumlah_jiwa,lat,lng
+Budi Santoso,Makan,4,-0.025,109.340
+Siti Aminah,Pemberdayaan,3,-0.028,109.345
+Andi Wijaya,Makan,5,-0.024,109.341
diff --git a/test_miskin.geojson b/test_miskin.geojson
new file mode 100644
index 0000000..ccc4d62
--- /dev/null
+++ b/test_miskin.geojson
@@ -0,0 +1,29 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {
+ "nama": "Joko Widodo",
+ "kategori_bantuan": "Pemberdayaan",
+ "jumlah_jiwa": 2
+ },
+ "geometry": {
+ "type": "Point",
+ "coordinates": [109.343, -0.027]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {
+ "nama": "Mega Wati",
+ "kategori_bantuan": "Makan",
+ "jumlah_jiwa": 6
+ },
+ "geometry": {
+ "type": "Point",
+ "coordinates": [109.344, -0.029]
+ }
+ }
+ ]
+}