diff --git a/SRS_CONTEXT.md b/SRS_CONTEXT.md new file mode 100644 index 0000000..f96d23d --- /dev/null +++ b/SRS_CONTEXT.md @@ -0,0 +1,780 @@ + + + + + + +**SPESIFIKASI KEBUTUHAN PERANGKAT LUNAK** + +**(SKPL / Software Requirements Specification)** + + + + + + + +**WebGIS Pemetaan Kemiskinan Berbasis** + +**Partisipasi Rumah Ibadah** + +*(Studi Kasus: Masjid di Kota Pontianak)* + + + + + +Versi 1.0 | Pontianak, 2025 + + + + + + + +*Disusun sebagai pemenuhan tugas mata kuliah* + +***Sistem Informasi Geografis (SIG)*** + + + +# **BAB 1 – PENDAHULUAN** + +## **1.1 Tujuan Dokumen** + +Dokumen Spesifikasi Kebutuhan Perangkat Lunak (SKPL) ini mendeskripsikan secara lengkap kebutuhan fungsional dan non-fungsional dari sistem WebGIS Pemetaan Kemiskinan Berbasis Partisipasi Rumah Ibadah. Dokumen ini menjadi acuan bagi pengembang, penguji, dan seluruh pemangku kepentingan dalam proses pengembangan sistem. + + + +## **1.2 Latar Belakang dan Deskripsi Sistem** + +Kemiskinan merupakan masalah sosial yang memerlukan penanganan terkoordinasi dan berbasis data. Di tingkat komunitas, rumah ibadah — khususnya masjid — memegang peran sentral sebagai pusat kegiatan sosial masyarakat. Masjid secara aktif mengelola program zakat, infak, sedekah (ZIS), dan berbagai bantuan sosial lainnya. Namun, distribusi bantuan tersebut seringkali tidak optimal karena ketiadaan data spasial mengenai sebaran keluarga miskin di sekitar masjid. + + + +Sistem yang dibangun adalah sebuah WebGIS (Web-based Geographic Information System) yang memungkinkan pemetaan interaktif titik-titik kemiskinan dan lokasi masjid di wilayah Kota Pontianak. Dengan fitur radius layanan masjid yang dapat dikonfigurasi, sistem ini secara visual menunjukkan keluarga miskin mana yang berada dalam jangkauan layanan sosial suatu masjid dan mana yang belum terjangkau, sehingga membantu perencanaan distribusi bantuan yang lebih tepat sasaran. + + + +Sistem ini dibangun menggunakan teknologi Leaflet.js untuk peta interaktif, PHP sebagai backend, dan MySQL sebagai basis data, yang dapat diakses melalui browser web tanpa instalasi khusus. + + + +## **1.3 Lingkup Masalah** + +Sistem ini mencakup: + +• Pemetaan dan pengelolaan data titik kemiskinan (keluarga miskin) secara spasial. + +• Pemetaan dan pengelolaan data masjid sebagai representasi Rumah Ibadah. + +• Visualisasi radius jangkauan layanan sosial masjid terhadap sebaran titik kemiskinan. + +• Analisis visual untuk mendukung perencanaan distribusi bantuan berbasis lokasi. + + + +Sistem ini TIDAK mencakup: + +• Pengelolaan transaksi keuangan, zakat, atau distribusi bantuan secara langsung. + +• Integrasi dengan sistem informasi pemerintah (DTKS, BPJS, dsb.) pada versi ini. + +• Rumah ibadah selain masjid (gereja, pura, vihara) — merupakan rencana pengembangan lanjutan. + + + +## **1.4 Definisi, Akronim, dan Istilah** + + + +| Istilah / Akronim | Definisi | +| :---- | :---- | +| WebGIS | Web-based Geographic Information System – sistem informasi geografis berbasis web browser | +| SKPL / SRS | Spesifikasi Kebutuhan Perangkat Lunak / Software Requirements Specification | +| SIG | Sistem Informasi Geografis | +| Kemiskinan (entitas) | Data spasial yang merepresentasikan lokasi dan atribut keluarga miskin dalam sistem | +| KK | Kepala Keluarga | +| Masjid (entitas) | Data spasial lokasi masjid beserta atribut dan radius layanannya dalam sistem | +| PIC Masjid | Person In Charge – pengurus/penanggung jawab masjid yang terdaftar dalam sistem | +| Radius Layanan | Jangkauan area pelayanan sosial masjid, divisualisasikan sebagai lingkaran pada peta (100–5000 m) | +| Leaflet.js | Library JavaScript open-source untuk peta interaktif berbasis web | +| OSM | OpenStreetMap – penyedia basemap peta dasar bersifat open-source | +| CRUD | Create, Read, Update, Delete – operasi dasar pengelolaan data | +| Marker | Ikon titik pada peta yang merepresentasikan lokasi suatu entitas | +| Mode View | Mode tampil saja (read-only); semua fungsi edit dinonaktifkan | +| Mode Edit | Mode aktif yang memungkinkan operator menambah, menghapus, dan memperbarui data | +| ZIS | Zakat, Infak, Sedekah – program sosial berbasis rumah ibadah | + + + +## **1.5 Referensi** + +• Leaflet.js Documentation – https://leafletjs.com/ + +• OpenStreetMap – https://www.openstreetmap.org/ + +• IEEE Std 830-1998: IEEE Recommended Practice for Software Requirements Specifications + +• Peraturan Presiden No. 15 Tahun 2010 tentang Percepatan Penanggulangan Kemiskinan + +• Undang-Undang No. 13 Tahun 2011 tentang Penanganan Fakir Miskin + +• BPS – Data Kemiskinan Kota Pontianak + + + + + +# **BAB 2 – DESKRIPSI UMUM SISTEM** + +## **2.1 Perspektif Produk** + +WebGIS ini adalah aplikasi perangkat lunak berbasis browser yang berdiri mandiri (standalone). Sistem ini menggantikan proses manual pencatatan dan pemetaan data kemiskinan yang selama ini tidak memiliki komponen spasial. Sistem beroperasi pada arsitektur client-server: + +• Client: Browser web modern (Chrome, Firefox, Edge) — tidak memerlukan instalasi tambahan + +• Server: PHP (built-in server untuk pengembangan, Apache/Nginx untuk produksi) + +• Database: MySQL untuk penyimpanan data atribut dan koordinat + +• Basemap: Tile peta OpenStreetMap (memerlukan koneksi internet) + + + +Sistem ini dirancang sebagai alat bantu pengambilan keputusan (Decision Support Tool) bagi: + +• Pengurus masjid: untuk mengetahui sebaran keluarga miskin dalam radius layanannya + +• Aparat kelurahan/kecamatan: untuk pemetaan kemiskinan di wilayah administratif + +• Relawan sosial: untuk perencanaan rute dan target distribusi bantuan + + + +## **2.2 Fungsi Produk** + + + +| No | Fungsi Utama | Deskripsi | +| :---- | :---- | :---- | +| 1 | Pemetaan Kemiskinan | Input, tampil, dan hapus titik lokasi keluarga miskin beserta atribut nama KK, jumlah anggota keluarga, dan alamat | +| 2 | Pemetaan Masjid | Input, tampil, dan hapus data masjid beserta nama PIC dan alamat | +| 3 | Radius Layanan Masjid | Visualisasi lingkaran jangkauan layanan masjid yang dapat dikonfigurasi (100–5000 m) untuk analisis cakupan terhadap titik kemiskinan | +| 4 | Filter Layer | Toggle tampil/sembunyikan layer kemiskinan dan masjid secara independen | +| 5 | Mode View / Edit | Pemisahan mode tampil (publik) dan mode edit (operator) untuk kontrol akses sederhana | +| 6 | Visualisasi Tematik | Simbologi warna berbeda untuk marker kemiskinan (oranye) dan masjid (biru) agar mudah dibedakan | +| 7 | Fokus Lokasi | Klik card di sidebar untuk menavigasi peta ke lokasi entitas terpilih | + + + +## **2.3 Karakteristik Pengguna** + + + +| Tipe Pengguna | Karakteristik | Hak Akses dalam Sistem | +| :---- | :---- | :---- | +| Operator | Pengurus masjid, staf kelurahan, atau relawan yang bertanggung jawab atas kebenaran data. Memiliki kemampuan dasar penggunaan browser dan peta digital. | Mode View \+ Mode Edit (CRUD penuh: tambah, lihat, hapus, update radius) | +| Masyarakat / Publik | Warga atau pihak yang ingin melihat peta sebaran kemiskinan dan masjid. Tidak perlu login. | Mode View saja (hanya lihat dan navigasi peta) | + + + +Catatan: Versi 1.0 belum mengimplementasikan sistem autentikasi berbasis akun. Pemisahan akses dilakukan melalui mode UI (View/Edit). Implementasi login merupakan kebutuhan pengembangan lanjutan. + + + +## **2.4 Batasan Sistem** + +• Belum terdapat sistem autentikasi/login pada versi 1.0. + +• Koordinat disimpan sebagai nilai desimal (DECIMAL), bukan tipe spasial MySQL POINT. + +• Analisis spasial terbatas pada visualisasi radius buffer — belum ada query "titik dalam radius" secara otomatis. + +• Tidak mendukung export data ke format GIS (Shapefile, GeoJSON, KML). + +• Hanya mendukung rumah ibadah jenis masjid; rumah ibadah lain merupakan rencana pengembangan. + +• Memerlukan koneksi internet untuk memuat tile peta OpenStreetMap. + +• Tidak mendukung pengeditan bersamaan oleh banyak pengguna (no concurrent editing). + + + +## **2.5 Asumsi dan Ketergantungan** + +• Server menjalankan PHP versi 7.4 atau lebih baru dengan ekstensi mysqli aktif. + +• MySQL versi 5.7 atau lebih baru tersedia dan dapat diakses oleh PHP. + +• Browser pengguna mendukung ES6, Fetch API, dan Leaflet.js. + +• Koordinat menggunakan sistem referensi WGS84 (EPSG:4326). + +• Data titik kemiskinan diperoleh dari survei lapangan oleh relawan yang terlatih. + + + + + +# **BAB 3 – KEBUTUHAN FUNGSIONAL** + +Kebutuhan fungsional didefinisikan per modul. Setiap kebutuhan memiliki ID unik, deskripsi, spesifikasi input-proses-output, dan tingkat prioritas. + + + +## **3.1 Modul Peta Interaktif** + +### **KF-01 – Inisialisasi Peta** + +| Atribut | Deskripsi | +| :---- | :---- | +| ID Kebutuhan | KF-01 | +| Nama | Inisialisasi Peta Interaktif | +| Deskripsi | Sistem menampilkan peta interaktif berbasis Leaflet.js dengan tile basemap OpenStreetMap, terpusat pada koordinat Kota Pontianak. | +| Input | Koordinat pusat default (Pontianak), zoom level awal | +| Proses | Inisialisasi map container Leaflet, muat tile OSM, set viewport awal | +| Output | Peta interaktif tampil di browser; pengguna dapat zoom, pan, dan klik peta | +| Prioritas | Tinggi (Critical) | + + + +### **KF-02 – Toggle Layer Tematik** + +| Atribut | Deskripsi | +| :---- | :---- | +| ID Kebutuhan | KF-02 | +| Nama | Manajemen Visibilitas Layer | +| Deskripsi | Pengguna dapat menampilkan atau menyembunyikan layer Kemiskinan dan layer Masjid secara independen melalui kontrol overlay Leaflet dan checkbox di sidebar. | +| Input | Klik checkbox sidebar atau overlay control di pojok peta | +| Proses | Tambah atau hapus layer group dari map object; sinkronisasi state checkbox dengan layer control | +| Output | Layer tampil atau hilang dari peta secara real-time tanpa reload halaman | +| Prioritas | Tinggi | + + + +## **3.2 Modul Data Kemiskinan** + +### **KF-03 – Tambah Data Keluarga Miskin** + +| Atribut | Deskripsi | +| :---- | :---- | +| ID Kebutuhan | KF-03 | +| Nama | Input Titik Lokasi Keluarga Miskin | +| Deskripsi | Operator menambahkan data keluarga miskin dengan mengklik titik lokasi di peta dan mengisi formulir atribut. Sistem menyimpan data dan menampilkan marker di peta. | +| Input | Nama kepala keluarga (teks), jumlah KK (angka positif), alamat (teks), koordinat latitude/longitude (dari klik peta) | +| Proses | Validasi input tidak kosong → POST ke create\_kemiskinan.php → INSERT ke tabel kemiskinan → refresh layer kemiskinan | +| Output | Marker oranye muncul di lokasi yang diklik; card data muncul di sidebar; data tersimpan di database | +| Prioritas | Tinggi | + + + +### **KF-04 – Lihat Daftar Data Kemiskinan** + +| Atribut | Deskripsi | +| :---- | :---- | +| ID Kebutuhan | KF-04 | +| Nama | Tampil Semua Data Keluarga Miskin | +| Deskripsi | Sistem memuat seluruh data kemiskinan dari database dan menampilkannya sebagai marker di peta beserta daftar card di sidebar secara otomatis saat halaman dibuka. | +| Input | Request load halaman atau aksi refresh data | +| Proses | GET read\_kemiskinan.php → parse JSON response → render marker per record → render sidebar card per record | +| Output | Semua titik kemiskinan tampil sebagai marker oranye di peta; daftar card tampil di sidebar dengan atribut nama KK, jumlah KK, dan alamat | +| Prioritas | Tinggi | + + + +### **KF-05 – Fokus ke Lokasi Keluarga Miskin** + +| Atribut | Deskripsi | +| :---- | :---- | +| ID Kebutuhan | KF-05 | +| Nama | Navigasi Peta ke Lokasi Keluarga Miskin | +| Deskripsi | Pengguna mengklik tombol 'Fokus' pada card di sidebar, dan peta secara otomatis berpindah serta memperbesar tampilan ke lokasi keluarga miskin tersebut. | +| Input | Klik tombol Fokus pada card keluarga miskin di sidebar | +| Proses | Ambil nilai latitude/longitude dari data card → panggil map.setView(koordinat, zoom) | +| Output | Peta bergerak dan memperbesar ke koordinat keluarga miskin yang dipilih | +| Prioritas | Sedang | + + + +### **KF-06 – Hapus Data Keluarga Miskin** + +| Atribut | Deskripsi | +| :---- | :---- | +| ID Kebutuhan | KF-06 | +| Nama | Hapus Titik Lokasi Keluarga Miskin | +| Deskripsi | Operator (dalam Mode Edit) menghapus data keluarga miskin dari sistem. Tombol hapus hanya aktif dalam Mode Edit. | +| Input | Klik tombol 'Hapus' pada card di sidebar (hanya aktif di Mode Edit) | +| Proses | Konfirmasi penghapusan → POST ke delete\_kemiskinan.php dengan id → DELETE dari database → hapus marker dari layer peta → hapus card dari sidebar | +| Output | Marker hilang dari peta, card hilang dari sidebar, data terhapus permanen dari database | +| Prioritas | Tinggi | + + + +## **3.3 Modul Data Masjid** + +### **KF-07 – Tambah Data Masjid** + +| Atribut | Deskripsi | +| :---- | :---- | +| ID Kebutuhan | KF-07 | +| Nama | Input Lokasi dan Data Masjid | +| Deskripsi | Operator menambahkan data masjid dengan mengklik lokasi di peta dan mengisi formulir. Sistem menampilkan marker masjid beserta lingkaran radius layanannya. | +| Input | Nama masjid (teks), nama PIC/pengurus (teks), alamat (teks), radius layanan awal (integer, default 500 m, range 100–5000), koordinat lat/lng dari klik peta | +| Proses | Validasi input → POST ke create\_masjid.php → INSERT ke tabel masjid → render marker biru \+ L.circle radius di peta → render card di sidebar | +| Output | Marker biru dan lingkaran radius muncul di peta; card masjid muncul di sidebar dengan tombol update radius | +| Prioritas | Tinggi | + + + +### **KF-08 – Lihat Daftar Data Masjid** + +| Atribut | Deskripsi | +| :---- | :---- | +| ID Kebutuhan | KF-08 | +| Nama | Tampil Semua Data Masjid dan Radius Layanan | +| Deskripsi | Sistem memuat seluruh data masjid dari database dan menampilkannya sebagai marker biru beserta lingkaran radius layanan di peta, serta daftar card di sidebar. | +| Input | Request load halaman atau aksi refresh data | +| Proses | GET read\_masjid.php → parse JSON → render marker biru per masjid → render L.circle sesuai nilai radius → render card sidebar | +| Output | Semua masjid tampil sebagai marker biru; lingkaran radius masing-masing masjid tampil di peta; sidebar menampilkan nama, PIC, alamat, dan nilai radius saat ini | +| Prioritas | Tinggi | + + + +### **KF-09 – Fokus ke Lokasi Masjid** + +| Atribut | Deskripsi | +| :---- | :---- | +| ID Kebutuhan | KF-09 | +| Nama | Navigasi Peta ke Lokasi Masjid | +| Deskripsi | Pengguna mengklik tombol 'Fokus' pada card masjid di sidebar untuk menavigasi peta ke lokasi masjid tersebut. | +| Input | Klik tombol Fokus pada card masjid di sidebar | +| Proses | Ambil koordinat masjid → panggil map.setView(koordinat, zoom) | +| Output | Peta bergerak dan memperbesar ke lokasi masjid yang dipilih | +| Prioritas | Sedang | + + + +### **KF-10 – Perbarui Radius Layanan Masjid** + +| Atribut | Deskripsi | +| :---- | :---- | +| ID Kebutuhan | KF-10 | +| Nama | Update Radius Jangkauan Layanan Masjid | +| Deskripsi | Operator mengubah nilai radius layanan masjid untuk memperbarui visualisasi lingkaran cakupan di peta. Fitur ini krusial untuk analisis apakah keluarga miskin tertentu berada dalam jangkauan layanan masjid. | +| Input | ID masjid, nilai radius baru (integer, range 100–5000 meter) | +| Proses | Validasi range nilai radius → POST ke update\_masjid\_radius.php → UPDATE kolom radius di database → re-render L.circle dengan radius baru di peta | +| Output | Lingkaran radius masjid di peta berubah ukuran secara real-time sesuai nilai radius yang diperbarui | +| Prioritas | Tinggi | + + + +### **KF-11 – Hapus Data Masjid** + +| Atribut | Deskripsi | +| :---- | :---- | +| ID Kebutuhan | KF-11 | +| Nama | Hapus Data Masjid | +| Deskripsi | Operator (dalam Mode Edit) menghapus data masjid beserta lingkaran radiusnya dari sistem. | +| Input | Klik tombol 'Hapus' pada card masjid di sidebar (hanya aktif di Mode Edit) | +| Proses | Konfirmasi penghapusan → POST ke delete\_masjid.php dengan id → DELETE dari database → hapus marker dan lingkaran radius dari peta → hapus card dari sidebar | +| Output | Marker dan lingkaran radius masjid hilang dari peta; card hilang dari sidebar; data terhapus dari database | +| Prioritas | Tinggi | + + + +## **3.4 Modul Mode Sistem** + +### **KF-12 – Mode View (Tampil)** + +| Atribut | Deskripsi | +| :---- | :---- | +| ID Kebutuhan | KF-12 | +| Nama | Mode View / Tampil | +| Deskripsi | Mode default saat sistem dibuka. Semua kontrol edit (tambah data, hapus, update radius) dinonaktifkan. Pengguna hanya dapat melihat peta, membaca data di sidebar, menggunakan filter layer, dan navigasi fokus lokasi. | +| Input | Klik tombol/toggle 'View Mode' atau kondisi awal sistem | +| Proses | Set state mode \= 'view' → disable tombol tambah, hapus, update radius → sembunyikan alat gambar | +| Output | UI dalam keadaan read-only; tombol edit tampak abu-abu/nonaktif | +| Prioritas | Tinggi | + + + +### **KF-13 – Mode Edit** + +| Atribut | Deskripsi | +| :---- | :---- | +| ID Kebutuhan | KF-13 | +| Nama | Mode Edit | +| Deskripsi | Mode aktif yang memungkinkan operator melakukan operasi CRUD. Semua kontrol input dan hapus diaktifkan. | +| Input | Klik tombol/toggle 'Edit Mode' | +| Proses | Set state mode \= 'edit' → enable tombol tambah data, hapus, update radius | +| Output | UI dalam keadaan edit aktif; klik pada peta memicu input form data baru; tombol hapus dan update radius aktif | +| Prioritas | Tinggi | + + + + + +# **BAB 4 – KEBUTUHAN NON-FUNGSIONAL** + + + +## **4.1 Performa** + +| ID | Kebutuhan | Metrik Target | +| :---- | :---- | :---- | +| KNF-01 | Waktu muat peta awal | \< 5 detik pada koneksi internet 10 Mbps | +| KNF-02 | Waktu respons operasi CRUD | \< 2 detik per operasi pada jaringan lokal | +| KNF-03 | Kapasitas render data | Mampu menampilkan hingga 500 marker tanpa penurunan performa signifikan | + + + +## **4.2 Keamanan** + +| ID | Kebutuhan | Keterangan | +| :---- | :---- | :---- | +| KNF-04 | Validasi Input Server-side | Semua input dari client divalidasi dan di-sanitasi di sisi PHP sebelum diteruskan ke database untuk mencegah SQL Injection dan XSS. | +| KNF-05 | Prepared Statement | Seluruh query ke database menggunakan mysqli prepared statements atau PDO. | +| KNF-06 | Autentikasi (Pengembangan Lanjutan) | Versi mendatang wajib mengimplementasikan sistem login berbasis session/JWT untuk memisahkan akses Operator dan Publik. | +| KNF-07 | HTTPS di Produksi | Sistem di-deploy menggunakan protokol HTTPS di lingkungan produksi. | + + + +## **4.3 Kemudahan Penggunaan (Usability)** + +| ID | Kebutuhan | Keterangan | +| :---- | :---- | :---- | +| KNF-08 | Antarmuka Intuitif | Navigasi peta mengikuti konvensi WebGIS standar (zoom scroll, drag pan, klik untuk detail). | +| KNF-09 | Resolusi Minimum | Antarmuka dapat digunakan dengan baik pada resolusi layar minimal 1280x720 piksel. | +| KNF-10 | Feedback Aksi | Setiap operasi CRUD memberikan notifikasi sukses atau pesan error yang informatif. | +| KNF-11 | Konsistensi Simbologi | Warna marker konsisten di seluruh sesi: oranye \= kemiskinan, biru \= masjid. | + + + +## **4.4 Reliabilitas** + +| ID | Kebutuhan | Keterangan | +| :---- | :---- | :---- | +| KNF-12 | Integritas Data | Setiap perubahan data langsung disimpan ke database; tidak ada state yang hanya tersimpan di memori browser. | +| KNF-13 | Penanganan Error | Jika koneksi database gagal atau tile peta tidak termuat, sistem menampilkan pesan error yang informatif — tidak memunculkan halaman kosong atau crash. | + + + +## **4.5 Portabilitas** + +| ID | Kebutuhan | Keterangan | +| :---- | :---- | :---- | +| KNF-14 | Lintas Browser | Berjalan normal di Chrome 90+, Firefox 88+, dan Edge 90+. | +| KNF-15 | Lintas OS Server | Dapat di-deploy di Linux (Ubuntu/Debian) maupun Windows Server dengan PHP dan MySQL. | +| KNF-16 | Kemudahan Instalasi | Untuk pengembangan, sistem dapat dijalankan dengan satu perintah: php \-S localhost:8000. | + + + + + +# **BAB 5 – ANTARMUKA EKSTERNAL** + +## **5.1 Antarmuka Pengguna (UI)** + +Sistem menggunakan antarmuka single-page dengan tiga area utama: + + + +### **5.1.1 Toolbar Atas** + +• Tombol toggle Mode (View / Edit) + +• Tombol switch tipe data aktif (Kemiskinan / Masjid) + +• Judul dan identitas aplikasi + + + +### **5.1.2 Sidebar Kiri** + +• Checkbox filter layer: Kemiskinan dan Masjid (sinkron dengan overlay control Leaflet) + +• Daftar card untuk setiap entitas yang termuat dari database + +• Card Kemiskinan berisi: nama KK, jumlah KK, alamat, tombol Fokus, tombol Hapus (aktif di Edit Mode) + +• Card Masjid berisi: nama masjid, nama PIC, alamat, radius saat ini, tombol Fokus, input \+ tombol Update Radius (aktif di Edit Mode), tombol Hapus (aktif di Edit Mode) + + + +### **5.1.3 Area Peta Utama** + +• Peta interaktif Leaflet dengan basemap OpenStreetMap + +• Marker oranye untuk titik kemiskinan; marker biru untuk masjid + +• Lingkaran radius layanan di setiap masjid (dapat dikonfigurasi) + +• Overlay control di pojok kanan atas untuk toggle layer + +• Pop-up atribut saat marker diklik + + + +## **5.2 Antarmuka Perangkat Lunak** + + + +| Komponen Eksternal | Versi | Peran | +| :---- | :---- | :---- | +| Leaflet.js | 1.x | Library peta interaktif utama — render marker, lingkaran radius, kontrol peta | +| OpenStreetMap Tiles | N/A | Penyedia basemap — tile server eksternal, memerlukan koneksi internet | +| PHP (Backend) | 7.4+ | Menangani logika bisnis, validasi input, dan komunikasi dengan database | +| MySQL | 5.7+ | Penyimpanan data atribut kemiskinan dan masjid | +| Fetch API (Browser) | ES6+ | Mekanisme komunikasi asinkron antara frontend JavaScript dan endpoint PHP | + + + +## **5.3 Antarmuka Komunikasi (API Endpoint)** + + + +| Endpoint | Method | Fungsi | Format Respons | +| :---- | :---- | :---- | :---- | +| php/read\_kemiskinan.php | GET | Ambil semua data keluarga miskin | JSON Array | +| php/create\_kemiskinan.php | POST | Tambah data keluarga miskin baru | Text (success/error) | +| php/delete\_kemiskinan.php | POST | Hapus data keluarga miskin berdasarkan ID | Text (success/error) | +| php/read\_masjid.php | GET | Ambil semua data masjid | JSON Array | +| php/create\_masjid.php | POST | Tambah data masjid baru | Text (success/error) | +| php/update\_masjid\_radius.php | POST | Perbarui radius layanan masjid | Text (success/error) | +| php/delete\_masjid.php | POST | Hapus data masjid berdasarkan ID | Text (success/error) | + + + +## **5.4 Skema Basis Data** + + + +### **Tabel: kemiskinan** + +| Kolom | Tipe Data | Keterangan | +| :---- | :---- | :---- | +| id | INT AUTO\_INCREMENT (PK) | Primary key, unik per record | +| nama\_kepala\_keluarga | VARCHAR(255) NOT NULL | Nama kepala keluarga miskin | +| jumlah\_kk | INT NOT NULL | Jumlah anggota keluarga (angka positif) | +| alamat | TEXT NOT NULL | Alamat lengkap tempat tinggal | +| latitude | DECIMAL(10,8) NOT NULL | Koordinat lintang (WGS84, range \-90 hingga \+90) | +| longitude | DECIMAL(11,8) NOT NULL | Koordinat bujur (WGS84, range \-180 hingga \+180) | +| created\_at | TIMESTAMP DEFAULT NOW() | Waktu pencatatan data | + + + +### **Tabel: masjid** + +| Kolom | Tipe Data | Keterangan | +| :---- | :---- | :---- | +| id | INT AUTO\_INCREMENT (PK) | Primary key, unik per record | +| nama\_masjid | VARCHAR(255) NOT NULL | Nama lengkap masjid | +| pic\_masjid | VARCHAR(255) NOT NULL | Nama pengurus/penanggung jawab masjid | +| alamat | TEXT NOT NULL | Alamat lengkap masjid | +| latitude | DECIMAL(10,8) NOT NULL | Koordinat lintang (WGS84) | +| longitude | DECIMAL(11,8) NOT NULL | Koordinat bujur (WGS84) | +| radius | INT DEFAULT 500 | Radius layanan dalam meter (range valid: 100–5000) | +| created\_at | TIMESTAMP DEFAULT NOW() | Waktu pencatatan data | + + + + + +# **BAB 6 – ANALISIS BISNIS PROSES DAN KONTRIBUSI PENGENTASAN KEMISKINAN** + +## **6.1 Konteks dan Permasalahan** + +Pengentasan kemiskinan di tingkat komunitas memerlukan dua elemen kunci: data yang akurat tentang siapa dan di mana keluarga miskin berada, serta jaringan distribusi bantuan yang efisien. Masjid sebagai lembaga sosial komunitas memiliki kapasitas distribusi tersebut melalui program ZIS, namun seringkali tidak memiliki peta sebaran penerima manfaat. + + + +Permasalahan utama yang diselesaikan sistem ini: + +• Tidak adanya data spasial mengenai sebaran keluarga miskin di sekitar masjid. + +• Distribusi bantuan bersifat reaktif (menunggu yang datang) bukan proaktif (mendatangi yang membutuhkan). + +• Potensi tumpang tindih cakupan antar masjid yang berdekatan, atau sebaliknya, ada wilayah yang tidak terjangkau masjid manapun (coverage gap). + + + +## **6.2 Alur Bisnis Proses** + +### **6.2.1 Proses Pendataan Keluarga Miskin (Partisipatif)** + +1\. Relawan atau pengurus RT/RW melakukan survei lapangan untuk mengidentifikasi keluarga miskin di lingkungannya. + +2\. Data survei (nama KK, jumlah anggota, alamat, dan koordinat GPS dari smartphone) dibawa ke sistem. + +3\. Operator membuka WebGIS dalam Mode Edit, memilih tipe 'Kemiskinan', dan mengklik lokasi di peta sesuai koordinat survei. + +4\. Operator mengisi formulir atribut dan menyimpan data. + +5\. Sistem menampilkan marker oranye di peta dan menambahkan card di sidebar. + +6\. Operator dapat memverifikasi posisi marker sesuai alamat yang tercatat. + + + +### **6.2.2 Proses Registrasi Masjid dan Penetapan Radius Layanan** + +7\. Pengurus masjid mendaftarkan masjidnya ke sistem: nama, nama PIC, alamat, dan koordinat lokasi masjid. + +8\. Sistem menampilkan marker biru dengan lingkaran radius default 500 m. + +9\. Pengurus mengamati titik-titik kemiskinan (marker oranye) yang berada di dalam dan di luar radius. + +10\. Pengurus menyesuaikan nilai radius sesuai kapasitas nyata masjid (misalnya dipersempit ke 300 m jika sumber daya terbatas, atau diperluas ke 1000 m jika kapasitas besar). + +11\. Radius yang sudah disepakati disimpan ke database untuk digunakan sebagai acuan distribusi. + + + +### **6.2.3 Proses Analisis Cakupan dan Perencanaan Distribusi** + +12\. Operator membuka peta dalam Mode View dengan kedua layer (Kemiskinan \+ Masjid) aktif. + +13\. Secara visual, operator mengidentifikasi: marker oranye mana yang berada di dalam lingkaran biru (terjangkau) dan mana yang di luar (belum terjangkau). + +14\. Untuk titik kemiskinan yang tidak terjangkau masjid manapun, operator dapat berkoordinasi dengan masjid terdekat untuk memperluas layanan atau dengan aparat kelurahan untuk program bantuan alternatif. + +15\. Daftar card di sidebar digunakan sebagai dasar daftar penerima manfaat untuk masing-masing masjid. + + + +## **6.3 Kontribusi Setiap Fitur terhadap Pengentasan Kemiskinan** + + + +| Fitur Sistem | Kontribusi Langsung terhadap Pengentasan Kemiskinan | +| :---- | :---- | +| Titik Peta Kemiskinan | Mengubah data survei lapangan menjadi basis data spasial yang dapat dianalisis secara visual — menggantikan daftar nama yang tidak dapat dianalisis secara geografis. | +| Marker Tematik Oranye | Memberikan representasi visual yang jelas dan cepat terbaca bagi pengguna non-teknis untuk memahami sebaran kemiskinan di wilayah mereka. | +| Lokasi Masjid di Peta | Menunjukkan posisi relatif masjid terhadap kantong-kantong kemiskinan untuk perencanaan program sosial yang lebih terarah. | +| Radius Layanan Masjid | Fitur kunci: memvisualisasikan jangkauan program sosial masjid dan mengidentifikasi gap cakupan (keluarga miskin yang tidak terjangkau masjid manapun). | +| Konfigurasi Radius (Update) | Memungkinkan penyesuaian dinamis sesuai kapasitas nyata masjid — radius bukan nilai tetap, melainkan mencerminkan kemampuan riil program sosial. | +| Filter Layer Independen | Memudahkan fokus analisis: hanya tampilkan kemiskinan untuk melihat sebaran, hanya tampilkan masjid untuk melihat distribusi layanan, atau keduanya untuk analisis cakupan. | +| Sidebar dengan Data Atribut | Menyediakan daftar terstruktur yang dapat digunakan langsung sebagai daftar penerima bantuan oleh pengurus masjid. | +| Mode View/Edit | Memisahkan penggunaan sistem sebagai alat analisis publik (View) dan alat pemeliharaan data oleh operator (Edit) — mendukung partisipasi masyarakat luas. | + + + +## **6.4 Rencana Pengembangan Lanjutan** + +Untuk meningkatkan dampak sistem terhadap pengentasan kemiskinan di versi selanjutnya: + +• Autentikasi multi-role: Admin, Operator Masjid, Aparat Kelurahan, dan Publik. + +• Analisis spasial otomatis: query database untuk menampilkan daftar keluarga miskin dalam radius suatu masjid secara otomatis (bukan hanya visual). + +• Laporan statistik: jumlah KK per kelurahan, persentase cakupan layanan masjid, peta panas (heatmap) kemiskinan. + +• Modul rumah ibadah lain: gereja, pura, vihara, dan kelenteng — untuk cakupan lintas agama. + +• Export data: ke format Excel atau PDF untuk pelaporan ke instansi pemerintah. + +• Integrasi DTKS (Data Terpadu Kesejahteraan Sosial) Kementerian Sosial. + +• Fitur upload foto rumah sebagai bukti verifikasi lapangan. + + + + + +# **BAB 7 – DIAGRAM SISTEM** + +## **7.1 Arsitektur Sistem** + +Sistem menggunakan arsitektur three-tier (tiga lapis): + + + +| ARSITEKTUR THREE-TIER WebGIS Kemiskinan & Masjid ┌─────────────────────────────────────────────┐ │ PRESENTATION LAYER (Client) │ │ Browser: HTML \+ CSS \+ Vanilla JavaScript │ │ Leaflet.js | Fetch API | Sidebar UI │ └─────────────────┬───────────────────────────┘ *HTTP/HTTPS | JSON Response* ┌─────────────────┴───────────────────────────┐ │ BUSINESS LOGIC LAYER (Server) │ │ PHP Procedural | 7 Endpoint CRUD │ │ Validasi Input | Query Builder │ └─────────────────┬───────────────────────────┘ *mysqli | SQL Query* ┌─────────────────┴───────────────────────────┐ │ DATA LAYER (Database) │ │ MySQL Database │ │ Tabel: kemiskinan | Tabel: masjid │ └─────────────────────────────────────────────┘ | +| :---- | + + + +## **7.2 Alur Data – Tambah Titik Kemiskinan** + +| OPERATOR → Klik lokasi di peta (Leaflet click event) BROWSER → Tampil form: Nama KK | Jumlah KK | Alamat OPERATOR → Isi dan submit form JS → fetch POST php/create\_kemiskinan.php PHP → Validasi & sanitasi input PHP → INSERT INTO kemiskinan (...) VALUES (...) MySQL → Simpan data, return success PHP → Return 'success' (text) JS → Refresh: GET read\_kemiskinan.php MAP → Render marker oranye di koordinat baru SIDEBAR → Tambah card baru di daftar kemiskinan | +| :---- | + + + +## **7.3 Alur Data – Update Radius Masjid** + +| OPERATOR → Input nilai radius baru di card masjid (sidebar) OPERATOR → Klik tombol 'Update Radius' JS → Validasi range (100-5000) JS → fetch POST php/update\_masjid\_radius.php PHP → UPDATE masjid SET radius=? WHERE id=? MySQL → Update berhasil, return success JS → Re-render L.circle dengan radius baru di peta MAP → Lingkaran radius masjid berubah ukuran real-time | +| :---- | + + + +## **7.4 Entity Relationship Diagram (ERD)** + + + +| Entitas | Atribut | +| :---- | :---- | +| kemiskinan | id (PK) | nama\_kepala\_keluarga | jumlah\_kk | alamat | latitude | longitude | created\_at | +| masjid | id (PK) | nama\_masjid | pic\_masjid | alamat | latitude | longitude | radius | created\_at | + + + +Catatan: Kedua tabel tidak memiliki foreign key — relasi logis antara masjid dan kemiskinan ditentukan secara spasial (kalkulasi jarak dan radius) di layer peta, bukan melalui relasi database relasional. Ini merupakan desain yang disederhanakan untuk versi 1.0; versi lanjutan dapat mengimplementasikan tabel relasi atau tipe spasial MySQL. + + + + + +# **LAMPIRAN** + +## **A. Ringkasan Kebutuhan Fungsional** + + + +| ID | Nama Kebutuhan | Modul | Prioritas | +| :---- | :---- | :---- | :---- | +| KF-01 | Inisialisasi Peta Interaktif | Peta | Tinggi | +| KF-02 | Toggle Layer Tematik | Peta | Tinggi | +| KF-03 | Tambah Data Keluarga Miskin | Kemiskinan | Tinggi | +| KF-04 | Lihat Daftar Data Kemiskinan | Kemiskinan | Tinggi | +| KF-05 | Fokus ke Lokasi Keluarga Miskin | Kemiskinan | Sedang | +| KF-06 | Hapus Data Keluarga Miskin | Kemiskinan | Tinggi | +| KF-07 | Tambah Data Masjid | Masjid | Tinggi | +| KF-08 | Lihat Daftar Data Masjid | Masjid | Tinggi | +| KF-09 | Fokus ke Lokasi Masjid | Masjid | Sedang | +| KF-10 | Perbarui Radius Layanan Masjid | Masjid | Tinggi | +| KF-11 | Hapus Data Masjid | Masjid | Tinggi | +| KF-12 | Mode View | Mode Sistem | Tinggi | +| KF-13 | Mode Edit | Mode Sistem | Tinggi | + + + +## **B. Simbologi Peta** + + + +| Elemen Peta | Simbol | Warna | Makna | +| :---- | :---- | :---- | :---- | +| Titik Kemiskinan | Marker bulat | Oranye (\#FFA500) | Lokasi keluarga miskin | +| Masjid | Marker bulat | Biru (\#0000FF) | Lokasi masjid (rumah ibadah) | +| Radius Layanan Masjid | Lingkaran transparan | Biru muda (opacity 0.2) | Area jangkauan program sosial masjid | + + + +## **C. Kamus Data (Data Dictionary)** + + + +| Field | Tabel | Tipe Data | Validasi | +| :---- | :---- | :---- | :---- | +| nama\_kepala\_keluarga | kemiskinan | VARCHAR(255) | Wajib diisi, tidak boleh kosong | +| jumlah\_kk | kemiskinan | INT | Wajib diisi, angka bulat positif \> 0 | +| alamat | kemiskinan | TEXT | Wajib diisi | +| latitude | kemiskinan, masjid | DECIMAL(10,8) | Range: \-90 hingga \+90 | +| longitude | kemiskinan, masjid | DECIMAL(11,8) | Range: \-180 hingga \+180 | +| nama\_masjid | masjid | VARCHAR(255) | Wajib diisi, tidak boleh kosong | +| pic\_masjid | masjid | VARCHAR(255) | Wajib diisi | +| radius | masjid | INT | Range valid: 100–5000 meter; default 500 | + + diff --git a/UI_UX_CONTEXT.md b/UI_UX_CONTEXT.md new file mode 100644 index 0000000..a57d1a0 --- /dev/null +++ b/UI_UX_CONTEXT.md @@ -0,0 +1,88 @@ +# GeoPoverty Light: UI/UX Design Context & Guidelines (v1.0) + +Dokumen ini mendefinisikan bahasa visual dan standar antarmuka untuk platform **GeoPoverty GIS**. Fokus utama desain adalah kejernihan data, profesionalisme, dan efisiensi navigasi spasial dalam balutan estetika yang bersih dan modern. + +--- + +## 1. Filosofi Desain: "Lumina GIS" +Estetika ini mengutamakan **kejernihan (clarity)** dan **kedalaman (depth)**. Dengan menggunakan latar belakang yang terang dan kontras tinggi, data kemiskinan yang kompleks diubah menjadi informasi yang mudah dicerna tanpa kehilangan kesan premium. + +--- + +## 2. Identitas Visual & Palet Warna +Menggunakan skema warna yang didominasi oleh warna dingin dan netral untuk menciptakan kesan platform yang objektif dan tepercaya. + +### Warna Utama (Core Colors) +- **Primary (Electric Indigo)**: `#6366F1` - Digunakan untuk aksi utama (CTA), indikator aktif, dan elemen branding. +- **Surface (Ghost White)**: `#F7F9FB` - Warna latar belakang utama untuk menjaga tampilan tetap segar dan bersih. +- **On-Surface (Deep Slate)**: `#1A2138` - Warna teks utama untuk keterbacaan maksimal. +- **Secondary (Teal)**: `#14B8A6` - Digunakan untuk indikator status positif atau kategori infrastruktur tertentu. +- **Error (Crimson)**: `#EF4444` - Digunakan untuk status "Prioritas 1" atau kondisi kritis. +- **Outline (Soft Steel)**: `#D8DADC` - Untuk garis pemisah dan border yang halus. + +--- + +## 3. Tipografi +Menggunakan font **Hanken Grotesk** untuk kesan modern dan teknis yang tetap ramah pengguna. + +- **Display/Headlines**: Bold, kerning rapat (-0.02em). Memberikan penekanan kuat pada judul panel. +- **Body Text**: Regular, line-height 1.6. Memastikan alamat dan deskripsi panjang mudah dibaca. +- **Labels/Data**: Semi-bold, ukuran 11-12px. Digunakan untuk statistik wilayah dan label marker. + +--- + +## 4. Prinsip Layout & Komposisi +Platform ini menggunakan pendekatan **Layered Architecture**: + +### A. Base Layer (The Map) +Peta selalu menjadi kanvas dasar. Gunakan basemap dengan saturasi rendah agar marker data (Indigo/Teal/Red) terlihat menonjol. + +### B. Glassmorphic Panels +Semua panel UI (Sidebar, Header, Cards) menggunakan efek transparansi: +- `background: rgba(255, 255, 255, 0.8);` +- `backdrop-filter: blur(16px);` +- `border: 1px solid rgba(255, 255, 255, 0.3);` + +### C. Container Strategy +- **Corner Radius**: Konsisten di **16px** untuk elemen besar dan **8px** untuk elemen kecil (tombol/badge). +- **Elevation**: Gunakan bayangan yang sangat lembut (`shadow-sm`) untuk memisahkan panel dari peta, menghindari garis hitam yang kaku. + +--- + +## 5. Komponen Antarmuka (UI Components) + +### Data Cards (Keluarga & Infrastruktur) +- **State**: Kartu yang dipilih (selected) harus mendapatkan aksen border Primary 2px. +- **Hierarchy**: Nama Kepala Keluarga sebagai judul utama, diikuti oleh alamat dan status bantuan dalam bentuk badge berwarna. + +### Interactive Markers +- **Active State**: Saat diklik, marker harus memberikan feedback visual (pembesaran ukuran atau pendaran cahaya/glow). +- **Radius Visualization**: Lingkaran radius rumah ibadah menggunakan fill opacity 5% dengan garis tepi tipis (1px dashed). + +--- + +## 6. Pengalaman Pengguna (UX) & Interaksi +1. **Contextual Disclosure**: Tampilkan detail data hanya saat dibutuhkan (on-click/on-hover) untuk menjaga kebersihan peta. +2. **Smooth Transitions**: Gunakan durasi transisi 300ms untuk pembukaan panel sidebar atau modal. +3. **Action Priority**: Tombol "Lapor Kondisi" atau "Tambah Data" harus memiliki kontras warna tertinggi (Primary) agar mudah ditemukan seketika. + +--- + +## 7. Implementasi CSS (Tokens) +```css +:root { + --lumina-primary: #6366f1; + --lumina-surface: rgba(247, 249, 251, 0.8); + --lumina-text: #1a2138; + --lumina-radius: 16px; + --lumina-glass: blur(16px); +} + +.panel-premium { + background: var(--lumina-surface); + backdrop-filter: var(--lumina-glass); + border-radius: var(--lumina-radius); + border: 1px solid rgba(255, 255, 255, 0.5); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.04); +} +``` \ No newline at end of file diff --git a/css/lumina.css b/css/lumina.css new file mode 100644 index 0000000..4910c13 --- /dev/null +++ b/css/lumina.css @@ -0,0 +1,1096 @@ +/* ════════════════════════════════════════ + LUMINA GIS DESIGN SYSTEM — v3.0 + Pixel-perfect match to reference screenshot +════════════════════════════════════════ */ +:root { + --lumina-primary: #625ff4; + --lumina-primary-weak: #eef0ff; + --lumina-surface: #ffffff; + --lumina-bg: #f4f6fb; + --lumina-text: #151c32; + --lumina-text-muted: #727b90; + --lumina-text-faint: #9aa3b5; + --lumina-secondary: #8fdad2; + --lumina-error: #ef4444; + --lumina-outline: #e7eaf1; + --lumina-blue: #2563eb; + --lumina-blue-weak: #eaf2ff; + --lumina-amber: #d97706; + --lumina-amber-weak: #fff7df; + --lumina-green: #16a34a; + --lumina-green-weak: #eafaf0; + --lumina-red-weak: #fff0f0; + --blue: #2563eb; + --blue-mid: #93c5fd; + --blue-light: #eaf2ff; + --border: #e7eaf1; + + --navbar-h: 56px; + --sidebar-left-w: 240px; + --sidebar-right-w: 380px; + + --lumina-radius: 8px; + --lumina-shadow: 0 4px 20px rgba(28,36,62,0.08); + --lumina-shadow-sm: 0 2px 10px rgba(28,36,62,0.06); + --lumina-shadow-xs: 0 1px 4px rgba(28,36,62,0.05); +} + +/* ════ RESET ════ */ +*, *::before, *::after { box-sizing: border-box; } +html, body { + height: 100%; margin: 0; + font-family: 'Hanken Grotesk', system-ui, sans-serif; + overflow: hidden; + color: var(--lumina-text); + background: #e2e8f0; +} + +/* ════ MAP ════ */ +#map { + position: absolute !important; + top: 0; left: 0; + width: 100vw; height: 100vh; + z-index: 1; +} +#map.edit-mode, +#map.edit-mode .leaflet-interactive { + cursor: crosshair !important; +} +.leaflet-container { + font-family: 'Hanken Grotesk', system-ui, sans-serif !important; +} +.leaflet-pane.leaflet-tile-pane { + filter: saturate(0.88) brightness(1.04); +} +.leaflet-top.leaflet-left { + top: calc(var(--navbar-h) + 24px); + left: calc(var(--sidebar-left-w) + 24px); +} +.leaflet-bottom.leaflet-right { + right: calc(var(--sidebar-right-w) + 10px); + bottom: 10px; +} +.leaflet-control-zoom { + border: none !important; + box-shadow: var(--lumina-shadow-sm) !important; + border-radius: 8px !important; + overflow: hidden; +} + +/* ════════════════════════════════ + NAVBAR +════════════════════════════════ */ +.lumina-navbar { + position: fixed; + top: 8px; left: 12px; right: 12px; /* Floating look */ + height: var(--navbar-h); + z-index: 2000; + display: flex; + align-items: center; + padding: 0 16px; + gap: 10px; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(8px); /* Glassmorphism */ + border: 1px solid var(--lumina-outline); + border-radius: 12px; /* Rounded corners */ + box-shadow: 0 2px 12px rgba(0,0,0,0.06); +} + +.lumina-brand { + font-weight: 700; + font-size: 15px; + color: var(--lumina-primary); + display: flex; + align-items: center; + gap: 8px; + text-decoration: none; + flex-shrink: 0; +} +.lumina-brand-icon { + width: 28px; height: 28px; + border-radius: 7px; + border: 2px solid var(--lumina-primary); + display: inline-flex; align-items: center; justify-content: center; + font-size: 13px; + flex-shrink: 0; +} + +.lumina-nav-right { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; + flex-shrink: 0; +} + +/* Nav pills */ +.nav-btn { + height: 32px; + border-radius: 16px; + font-family: inherit; + font-size: 13px; + font-weight: 600; + padding: 0 14px; + border: 1px solid var(--border); + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; + background: #fff; + color: var(--lumina-text); + transition: all 0.15s; + white-space: nowrap; +} +.nav-btn:hover { background: #f4f6fb; } +.nav-btn.active, +.nav-btn.nav-btn-data { + background: var(--lumina-primary-weak); + border-color: var(--lumina-primary); + color: var(--lumina-primary); +} +.nav-btn.msg-btn { + background: #f0fdf4; + border-color: #bbf7d0; + color: #16a34a; +} +.nav-btn.mgr-btn { + background: #fff7ed; + border-color: #fed7aa; + color: #d97706; +} + +/* Primary button (navbar right) */ +.lumina-btn-primary { + display: inline-flex; align-items: center; gap: 7px; + background: var(--lumina-primary); + color: white; + border: none; + border-radius: 7px; + padding: 0 14px; + height: 34px; + font-family: inherit; + font-weight: 600; + font-size: 13px; + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; + flex-shrink: 0; +} +.lumina-btn-primary:hover { opacity: 0.88; } +.lumina-btn-primary.danger { background: var(--lumina-error); } + +/* Navbar "Masuk" / role chip area */ +.auth-name { + font-weight: 600; + font-size: 13px; + color: var(--lumina-text); + white-space: nowrap; +} +.role-chip { + display: inline-flex; align-items: center; gap: 5px; + font-size: 12px; font-weight: 700; + padding: 4px 10px; + border-radius: 20px; +} +.role-chip.pimpinan { background: #fef9c3; color: #a16207; } +.role-chip.pengurus { background: var(--lumina-primary-weak); color: var(--lumina-primary); } + +.lumina-icon-btn { + width: 32px; height: 32px; + border-radius: 6px; + display: flex; align-items: center; justify-content: center; + cursor: pointer; + color: var(--lumina-text-muted); + border: none; background: transparent; + transition: all 0.15s; +} +.lumina-icon-btn:hover { background: #f1f5f9; color: var(--lumina-text); } + +.lumina-badge { + display: inline-flex; align-items: center; justify-content: center; + min-width: 18px; height: 18px; + padding: 0 4px; + border-radius: 9px; + font-size: 10px; font-weight: 700; + color: white; + margin-left: 3px; +} + +/* Logout nav btn */ +.logout-btn { + background: #fff0f0 !important; + border-color: #fecaca !important; + color: var(--lumina-error) !important; +} + +/* ════════════════════════════════ + LEFT SIDEBAR +════════════════════════════════ */ +.lumina-sidebar { + position: fixed; + top: calc(var(--navbar-h) + 16px); + bottom: 12px; + left: 12px; + width: var(--sidebar-left-w); + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(8px); + z-index: 1000; + display: flex; + flex-direction: column; + border-radius: 16px; + border: 1px solid var(--lumina-outline); + box-shadow: 0 4px 16px rgba(0,0,0,0.06); +} + +.lumina-sidebar-top { + padding: 16px 16px; + border-bottom: 1px solid var(--lumina-outline); + display: flex; + gap: 10px; +} +.lumina-sidebar-top .lumina-btn-primary { + flex: 1; + justify-content: center; + height: 40px; + border-radius: 10px; + font-size: 14px; + padding: 0 12px; +} +.lumina-sidebar-top .lumina-btn-primary.secondary { + background: #14b8a6; + color: white; +} +.lumina-sidebar-top .lumina-btn-primary.secondary:hover { + background: #0d9488; +} + +.lumina-menu { + flex: 1; + overflow-y: auto; + padding: 6px 0 16px; + scrollbar-width: thin; + scrollbar-color: #e7eaf1 transparent; +} +.lumina-menu::-webkit-scrollbar { width: 3px; } +.lumina-menu::-webkit-scrollbar-thumb { background: #e7eaf1; border-radius: 2px; } + +.lumina-menu-section-label { + font-size: 11px; + font-weight: 800; + color: var(--lumina-text-muted); + margin: 22px 18px 8px; + text-transform: uppercase; + letter-spacing: 0.8px; +} + +.lumina-menu-item { + display: flex; align-items: center; gap: 12px; + padding: 10px 14px; + margin: 4px 14px; + border-radius: 10px; + color: var(--lumina-text-muted); + font-weight: 600; + font-size: 13px; + cursor: pointer; + transition: all 0.15s; + border: none; + background: transparent; + width: auto; + text-align: left; + line-height: 1.4; +} +.lumina-menu-item i { + width: 16px; text-align: center; font-size: 14px; flex-shrink: 0; +} +.lumina-menu-item:hover { background: #f1f5f9; color: var(--lumina-text); } +.lumina-menu-item.active { + background: var(--lumina-primary-weak); + color: var(--lumina-primary); + font-weight: 700; +} + +/* Legend */ +.lumina-legend { padding: 0 18px 8px; display: flex; flex-direction: column; gap: 8px; } +.lumina-legend-item { + display: flex; align-items: center; gap: 8px; + font-size: 12px; color: var(--lumina-text-muted); +} +.lumina-legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } + +/* ════════════════════════════════ + STATS PANEL — floating on map +════════════════════════════════ */ +.lumina-stats { + position: fixed; + bottom: 20px; + left: calc(var(--sidebar-left-w) + 24px); + z-index: 800; + background: #ffffff; + border-radius: 14px; + border: 1px solid var(--lumina-outline); + box-shadow: 0 8px 30px rgba(0,0,0,0.08); + padding: 18px 24px; + width: 360px; +} + +.stat-title { + font-size: 10px; + font-weight: 800; + color: var(--lumina-text-muted); + text-transform: uppercase; + letter-spacing: 0.6px; + display: flex; align-items: center; justify-content: space-between; + margin-bottom: 16px; +} +.stat-title > div { display: flex; align-items: center; gap: 8px; } +.stat-title button { + background: none; border: none; cursor: pointer; + color: var(--lumina-text-muted); padding: 0; font-size: 12px; +} + +.stat-sub-label { + font-size: 11px; + color: var(--lumina-text-muted); + margin-bottom: 6px; + display: flex; justify-content: space-between; align-items: center; +} +.stat-sub-label span { font-weight: 700; color: var(--lumina-primary); font-size: 11px; } + +.stat-progress-bar { + height: 4px; background: #eef1f6; border-radius: 2px; + margin-bottom: 20px; position: relative; overflow: hidden; +} +.stat-progress-fill { + position: absolute; top: 0; left: 0; height: 100%; + background: var(--lumina-primary); border-radius: 2px; + transition: width 0.5s ease-in-out; +} + +.stat-grid { + display: grid; grid-template-columns: 1fr 1fr; gap: 16px 8px; + text-align: center; margin-bottom: 16px; +} +.stat-item-val { + font-size: 24px; font-weight: 700; color: var(--lumina-primary); + margin-bottom: 4px; line-height: 1; +} +.stat-item-val.error { color: var(--lumina-error); } +.stat-item-val.success { color: var(--lumina-green); } +.stat-item-lbl { + font-size: 9px; font-weight: 800; color: var(--lumina-text-muted); + text-transform: uppercase; letter-spacing: 0.6px; +} + +.stat-divider { + border: none; border-top: 1px dashed var(--lumina-outline); + margin: 16px 0; +} + +.stat-ibadah-label { + font-size: 9px; font-weight: 800; color: var(--lumina-text-muted); + text-transform: uppercase; letter-spacing: 0.6px; + margin-bottom: 12px; +} +.stat-ibadah-row { + display: flex; justify-content: space-between; text-align: center; + padding: 0 10px; +} +.stat-ibadah-row > div { + display: flex; flex-direction: column; align-items: center; gap: 4px; + font-size: 11px; color: var(--lumina-text-muted); +} +.stat-ibadah-row b { + font-size: 16px; color: var(--lumina-text); line-height: 1; margin-top: 2px; +} + +/* ════════════════════════════════ + RIGHT SIDEBAR (#sidebar) +════════════════════════════════ */ +#sidebar, .lumina-panel-right { + position: fixed; + top: calc(var(--navbar-h) + 16px); + bottom: 8px; + right: 12px; + width: 380px; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(8px); + z-index: 1000; + display: flex; + flex-direction: column; + border-radius: 12px; + border: 1px solid var(--lumina-outline); + box-shadow: 0 4px 16px rgba(0,0,0,0.06); + /* Hidden default unless open */ + transform: translateX(120%); + transition: transform 0.25s ease; +} +#sidebar[style*="display: block"], +#sidebar[style*="display: flex"], +.lumina-panel-right[style*="display: block"], +.lumina-panel-right[style*="display: flex"] { + transform: translateX(0) !important; + display: flex !important; +} + +.sidebar-head { + padding: 16px 20px 12px; + border-bottom: none; + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; +} +.sidebar-head-title { + font-size: 16px; + font-weight: 800; + color: var(--lumina-primary); + display: flex; align-items: center; gap: 8px; +} + +/* Filter row */ +.sidebar-filter { + display: flex; + gap: 8px; + padding: 0 20px 16px; + border-bottom: 1px solid var(--lumina-outline); + flex-shrink: 0; +} +.lumina-btn-filter { + flex: 1; + background: #ffffff; + border: 1px solid var(--lumina-outline); + color: var(--lumina-text-muted); + height: 38px; + padding: 0 10px; + border-radius: 8px; + font-family: inherit; + font-size: 11px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; +} +.lumina-btn-filter:hover { background: #f8fafc; } +.lumina-btn-filter.active { + background: var(--lumina-primary); + color: white; + border-color: var(--lumina-primary); +} + +/* Sub text */ +.sidebar-head-sub { + padding: 5px 14px 6px; + font-size: 11px; + color: var(--lumina-text-muted); + flex-shrink: 0; +} + +/* Scroll area */ +.sidebar-scroll { + flex: 1; + overflow-y: auto; + padding: 0 10px 16px; + scrollbar-width: thin; + scrollbar-color: #e7eaf1 transparent; +} +.sidebar-scroll::-webkit-scrollbar { width: 3px; } +.sidebar-scroll::-webkit-scrollbar-thumb { background: #e7eaf1; border-radius: 2px; } + +/* ════════════════════════════════ + SECTION HEADER inside sidebar +════════════════════════════════ */ +.sec-hd { + display: flex; align-items: center; justify-content: space-between; + padding: 10px 0 6px; + font-size: 10px; font-weight: 800; + color: var(--lumina-text-muted); + text-transform: uppercase; letter-spacing: 0.5px; + border-bottom: 1px solid var(--lumina-outline); + margin-bottom: 8px; +} +.sec-hd-label { flex: 1; } +.sec-count { + background: #f1f5f9; + color: var(--lumina-text-muted); + font-size: 10px; font-weight: 700; + padding: 2px 7px; + border-radius: 10px; +} +.sec-divider { display: none; } + +/* ════════════════════════════════ + CARDS +════════════════════════════════ */ +.card { + border-radius: 10px; + border: 1px solid var(--lumina-outline); + padding: 14px 16px 12px; + margin-bottom: 12px; + background: #ffffff; + cursor: pointer; + transition: box-shadow 0.15s, border-color 0.15s; +} +.card:hover { + border-color: #d0d5e4; + box-shadow: var(--lumina-shadow-xs); +} +.card.selected { border: 2px solid var(--lumina-primary); } + +.card-name { + font-size: 15px; font-weight: 800; color: var(--lumina-text); + margin-bottom: 4px; +} +.card-sub { + font-size: 12px; color: var(--lumina-blue); font-weight: 700; + margin-bottom: 4px; +} +.card-addr { + font-size: 11px; color: var(--lumina-text-muted); + line-height: 1.5; margin-bottom: 12px; +} +.card-ts { + font-size: 10px; color: var(--lumina-text-faint); + display: flex; align-items: center; gap: 4px; + margin-bottom: 6px; +} +.card-foot { + display: flex; align-items: center; justify-content: space-between; + gap: 8px; margin-top: 12px; +} +.card-btns { + display: flex; gap: 6px; align-items: center; +} + +/* ════ STATUS CHIPS ════ */ +.chip { + display: inline-flex; align-items: center; gap: 4px; + padding: 3px 8px; border-radius: 5px; + font-size: 10px; font-weight: 700; +} +.chip-green { background: var(--lumina-green-weak); color: var(--lumina-green); } +.chip-red { background: var(--lumina-red-weak); color: var(--lumina-error); } +.chip-blue { background: var(--lumina-blue-weak); color: var(--lumina-blue); } +.chip-amber { background: var(--lumina-amber-weak); color: var(--lumina-amber); } +.chip-gray { background: #f1f5f9; color: var(--lumina-text-muted); } + +/* ════ BUTTONS in cards ════ */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 4px; + border: 1px solid #e7eaf1; + border-radius: 6px; + height: 28px; + padding: 0 8px; + font: inherit; + font-size: 11px; + font-weight: 700; + cursor: pointer; + background: #fff; + color: #727b90; + transition: all 0.13s; + white-space: nowrap; +} +.btn:hover { background: #f4f6fb; } +.btn-primary { background: #625ff4; border-color: #625ff4; color: #fff; } +.btn-primary:hover { opacity: 0.88; } +.btn-send { background: #16a34a; border-color: #16a34a; color: #fff; } +.btn-add { width: 100%; border-style: dashed; background: #eef0ff; color: #625ff4; } + +/* Icon-only buttons */ +.btn-focus, .btn-lapor, .btn-edit, .btn-delete { + border-radius: 8px; + border: none; + padding: 0 12px; + height: 32px; +} +.card-btns .btn-focus, .card-btns .btn-edit, .card-btns .btn-delete { + width: 32px; padding: 0; flex-shrink: 0; +} +.btn-focus { background: var(--lumina-green-weak); color: var(--lumina-green); } +.btn-focus:hover { background: #dcfce7; } +.btn-edit { background: var(--lumina-primary-weak); color: var(--lumina-primary); } +.btn-edit:hover { background: #e0e7ff; } +.btn-delete { background: var(--lumina-red-weak); color: var(--lumina-error); } +.btn-delete:hover { background: #fee2e2; } +.btn-lapor { background: var(--lumina-amber-weak); color: var(--lumina-amber); } +.btn-lapor:hover { background: #fef3c7; } +.btn-detail { + background: var(--lumina-blue-weak); + border-color: #bcd2f7; + color: var(--lumina-blue); + padding: 0 8px; +} +.btn-detail:hover { background: #d6e8ff; } + +/* Radius row (ibadah cards) */ +.radius-row { + display: flex; align-items: center; gap: 8px; + background: #f8fafc; + border: 1px solid var(--lumina-outline); + border-radius: 8px; + padding: 6px 10px; + margin: 12px 0; + font-size: 12px; + font-weight: 600; + color: var(--lumina-text-muted); +} +.radius-row input[type=number] { + width: 64px; height: 28px; + border: 1px solid var(--border); + border-radius: 4px; + padding: 0 6px; + font: inherit; font-size: 12px; + background: #fff; +} +.radius-unit { font-size: 12px; color: var(--lumina-text-muted); } +.btn-update { + height: 28px; padding: 0 12px; + border-radius: 6px; + font-size: 11px; font-weight: 700; + background: var(--lumina-blue-weak); + color: var(--lumina-blue); + border: none; + cursor: pointer; + margin-left: auto; +} +.btn-update:hover { background: #dbeafe; } + +/* ════════════════════════════════ + MODALS +════════════════════════════════ */ +.modal-bg { + position: fixed; inset: 0; z-index: 5000; + background: rgba(17, 24, 39, 0.45); + backdrop-filter: blur(4px); + display: flex; align-items: center; justify-content: center; +} +.modal-card { + width: 480px; max-width: 94vw; max-height: 90vh; + overflow: auto; background: #fff; + border-radius: 14px; + box-shadow: 0 20px 60px rgba(0,0,0,0.16); +} +.modal-top { + display: flex; align-items: flex-start; gap: 14px; + padding: 20px 22px 14px; + border-bottom: 1px solid #eef1f6; + position: sticky; top: 0; background: #fff; z-index: 1; +} +.modal-icon-wrap { + width: 40px; height: 40px; border-radius: 9px; + display: grid; place-items: center; flex: 0 0 auto; +} +.modal-icon-blue { background: #eaf2ff; color: #2563eb; } +.modal-icon-green { background: #eafaf0; color: #16a34a; } +.modal-icon-red { background: #fff0f0; color: #ef4444; } +.modal-icon-amber { background: #fff7df; color: #d97706; } +.modal-icon-purple { background: #eef0ff; color: #625ff4; } +.modal-title { font-size: 15px; font-weight: 800; color: #151c32; } +.modal-subtitle { font-size: 11px; color: #727b90; margin-top: 2px; } +.modal-close { + margin-left: auto; border: 0; + background: #f4f6fb; color: #727b90; + width: 28px; height: 28px; border-radius: 7px; cursor: pointer; +} +.modal-body { padding: 16px 22px 20px; display: flex; flex-direction: column; gap: 12px; } +.modal-footer { display: flex; justify-content: flex-end; gap: 8px; } + +.field, .field-row .field { display: flex; flex-direction: column; gap: 5px; flex: 1; } +.field-row { display: flex; gap: 10px; } +.field-label { font-size: 12px; font-weight: 700; color: #151c32; } +.req { color: #ef4444; } +.field input, .field select, .field textarea, +#pesan-jenis, #pesan-judul, #pesan-isi { + border: 1px solid #e7eaf1; + border-radius: 7px; + padding: 8px 10px; + font: inherit; + background: #f8fafc; + color: #151c32; + font-size: 13px; +} +.field-hint { font-size: 11px; color: #9aa3b5; } + +/* ════════════════════════════════ + DETAIL PANEL (right slide-in) +════════════════════════════════ */ +#detail-panel { + position: fixed; + top: var(--navbar-h); + right: 0; bottom: 0; + width: 340px; max-width: 94vw; + transform: translateX(calc(100% + 20px)); + transition: 0.25s ease; + z-index: 2100; + background: #fff; + border-left: 1px solid #e7eaf1; + box-shadow: -4px 0 16px rgba(0,0,0,0.06); + display: flex; flex-direction: column; overflow: hidden; +} +#detail-panel.open { transform: translateX(0); } +.dp-head, .dp-foot { + padding: 14px 16px; + border-bottom: 1px solid #e7eaf1; +} +.dp-foot { + border-top: 1px solid #e7eaf1; + border-bottom: 0; + display: flex; gap: 7px; flex-wrap: wrap; +} +.dp-head-row { display: flex; justify-content: space-between; gap: 12px; } +.dp-title { font-weight: 800; font-size: 16px; } +.dp-addr { color: #727b90; font-size: 12px; margin-top: 4px; } +.dp-close, .pdf-close, .ldf-close { + border: 0; width: 28px; height: 28px; border-radius: 7px; + background: #f4f6fb; cursor: pointer; +} +.dp-status { + margin-top: 10px; padding: 7px 10px; border-radius: 7px; + background: #eafaf0; color: #16a34a; + font-weight: 800; font-size: 12px; +} +.dp-tabs { + display: grid; grid-template-columns: repeat(3, 1fr); + padding: 8px; gap: 6px; + border-bottom: 1px solid #e7eaf1; +} +.dp-tab { + border: 1px solid #e7eaf1; background: #fff; + border-radius: 7px; height: 32px; + font-weight: 800; font-size: 12px; + color: #727b90; cursor: pointer; font-family: inherit; +} +.dp-tab.active { background: #625ff4; border-color: #625ff4; color: #fff; } +.dp-content { padding: 12px 16px; overflow: auto; flex: 1; } +.hidden { display: none !important; } + +/* l-card / b-card / p-card */ +.l-card, .b-card, .p-card { + border: 1px solid var(--lumina-outline); + border-radius: 10px; + padding: 14px 16px; + margin-bottom: 12px; + background: #fff; +} +.b-head, .l-head { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} +.b-jenis, .l-cat { + font-size: 15px; + font-weight: 800; + color: var(--lumina-text); +} +.b-desc, .l-desc { + font-size: 14px; + color: var(--lumina-text); + margin-bottom: 8px; +} +.b-meta, .l-meta { + font-size: 13px; + color: var(--lumina-text-muted); + margin-bottom: 4px; + display: flex; + align-items: center; + gap: 6px; +} +.b-acts, .l-acts { + margin-top: 14px; + display: flex; + gap: 8px; + flex-wrap: wrap; +} +.b-acts .btn, .l-acts .btn { + height: 32px; + font-size: 12px; + border-radius: 8px; +} +.b-acts .btn-delete, .l-acts .btn-delete { + width: 32px; padding: 0; justify-content: center; flex-shrink: 0; +} +.btn-proses { background: var(--lumina-blue-weak); color: var(--lumina-blue); } +.btn-proses:hover { background: #dbeafe; } +.btn-selesai { background: var(--lumina-green-weak); color: var(--lumina-green); } +.btn-selesai:hover { background: #dcfce7; } +.btn-tolak { background: var(--lumina-gray-weak); color: var(--lumina-text); } +.btn-tolak:hover { background: #e2e8f0; } +.l-kk { + font-size: 13px; font-weight: 700; color: var(--lumina-primary); + margin-bottom: 6px; + display: flex; align-items: center; gap: 6px; +} + +/* ════ ANGGOTA CARDS ════ */ +.anggota-card { + border: 1px solid var(--lumina-outline); + border-radius: 10px; + padding: 14px 16px; + margin-bottom: 12px; + background: #fff; +} +.ang-head { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} +.ang-name { + font-size: 15px; + font-weight: 800; + color: var(--lumina-text); +} +.ang-row { + font-size: 12px; + color: var(--lumina-text); + margin-bottom: 6px; + display: flex; +} +.ang-lbl { + display: inline-block; + width: 90px; + font-weight: 700; + color: var(--lumina-text-muted); + flex-shrink: 0; +} +.ang-warn { + font-size: 11px; + color: var(--lumina-amber); + background: var(--lumina-amber-weak); + padding: 8px 12px; + border-radius: 8px; + margin: 8px 0; +} +.umur-em { + color: var(--lumina-text-faint); + font-style: italic; + margin-left: 4px; +} +.ang-acts { + display: flex; + gap: 8px; + margin-top: 14px; + padding-top: 12px; + border-top: 1px dashed var(--lumina-outline); +} + +/* ════════════════════════════════ + TOAST +════════════════════════════════ */ +#toast-wrap { + position: fixed; right: 18px; bottom: 18px; + display: flex; flex-direction: column; gap: 7px; + z-index: 999999; +} +.toast { + padding: 10px 14px; border-radius: 9px; + color: #fff; font-weight: 700; font-size: 13px; + box-shadow: 0 8px 24px rgba(0,0,0,0.14); + display: flex; align-items: center; gap: 8px; +} +.toast.success { background: #16a34a; } +.toast.error { background: #ef4444; } +.toast.info { background: #2563eb; } +.toast.removing { opacity: 0; transform: translateX(20px); transition: 0.25s; } + +/* ════════════════════════════════ + FLOATING PANELS (pesan / laporan detail) +════════════════════════════════ */ +#pesan-detail-float, #laporan-detail-float { + display: none; position: fixed; + right: calc(var(--sidebar-right-w) + 12px); + top: calc(var(--navbar-h) + 12px); + z-index: 9999; width: 340px; max-width: 90vw; + background: #fff; border-radius: 16px; + box-shadow: 0 24px 60px rgba(0,0,0,0.2); + overflow: hidden; + flex-direction: column; +} +.pdf-accent-bar { height: 4px; background: var(--lumina-primary); } +#ldf-status-bar { height: 4px; background: var(--lumina-primary); } +#ldf-status-bar.selesai { background: var(--lumina-green); } +#ldf-status-bar.diproses { background: var(--lumina-blue); } +#ldf-status-bar.menunggu { background: var(--lumina-amber); } +#ldf-status-bar.ditolak { background: var(--lumina-gray); } + +.pdf-head, .ldf-head { + padding: 16px 20px; + display: flex; gap: 12px; + border-bottom: 1px solid var(--lumina-outline); + align-items: flex-start; +} +#pdf-icon, #ldf-icon { + font-size: 24px; + background: #f8fafc; + width: 44px; height: 44px; flex-shrink: 0; + display: flex; align-items: center; justify-content: center; + border-radius: 12px; +} +.pdf-close, .ldf-close { + border: none; background: transparent; cursor: pointer; + font-size: 16px; color: var(--lumina-text-muted); + padding: 0; margin-left: auto; +} +.pdf-body, .ldf-body { + padding: 20px; + font-size: 14px; + color: var(--lumina-text); + line-height: 1.6; +} +#pdf-isi, #ldf-deskripsi { + font-size: 15px; + color: var(--lumina-text); + margin-bottom: 16px; +} +#pdf-pengirim, #ldf-pelapor { + font-weight: 700; +} +#pdf-role, #pdf-jenis-label, #pdf-waktu, #ldf-wa, #ldf-waktu, #ldf-ditangani { + font-size: 13px; + color: var(--lumina-text-muted); + margin-bottom: 4px; +} +.pdf-broadcast-badge, .pdf-badge-baru, #ldf-kk-badge { + display: inline-block; + padding: 6px 10px; + border-radius: 8px; + font-size: 11px; + font-weight: 800; + margin-top: 8px; +} +.pdf-broadcast-badge { background: var(--lumina-purple-weak); color: var(--lumina-purple); } +.pdf-badge-baru { background: var(--lumina-amber-weak); color: var(--lumina-amber); } +#ldf-kk-badge { background: var(--lumina-blue-weak); color: var(--lumina-blue); margin-bottom: 12px; margin-top: 0; } +.ldf-footer { padding: 14px 20px; border-top: 1px solid var(--lumina-outline); background: #f8fafc; } + +/* Notif panel */ +#notif-panel { + background: #fff; + border-radius: 10px; + border: 1px solid var(--lumina-outline); + box-shadow: var(--lumina-shadow); +} + +/* Routing instruction */ +#routing-instruction-panel { + background: #fff; + border-radius: 10px; + border: 1px solid var(--lumina-outline); + border-left: 4px solid var(--lumina-primary); + box-shadow: var(--lumina-shadow); +} + +/* Panel compose */ +.panel-compose { + border-top: 1px solid var(--lumina-outline); + padding: 16px 20px; + background: #f8fafc; + flex-shrink: 0; +} +.panel-compose-title { + font-size: 11px; font-weight: 800; + color: var(--lumina-text-muted); + text-transform: uppercase; letter-spacing: 0.5px; + margin-bottom: 12px; +} +.panel-compose select, .panel-compose input, .panel-compose textarea { + width: 100%; margin-bottom: 10px; + border: 1px solid var(--lumina-outline); + border-radius: 8px; padding: 10px 12px; + font: inherit; font-size: 13px; + background: #fff; +} +.panel-compose .btn-primary { + height: 40px; + border-radius: 8px; + font-size: 13px; +} + +/* Pesan Cards */ +.p-head { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 8px; +} +.p-judul { + font-size: 15px; + font-weight: 800; + color: var(--lumina-text); + margin-bottom: 4px; +} +.p-isi { + font-size: 14px; + color: var(--lumina-text); + margin-bottom: 8px; + line-height: 1.5; +} +.p-meta { + font-size: 13px; + color: var(--lumina-text-muted); + display: flex; + align-items: center; + gap: 6px; +} +.lumina-panel-right { overflow: hidden; } + +/* ════════════════════════════════ + HIDE OLD ELEMENTS +════════════════════════════════ */ +.mode-group { display: none !important; } +#toolbar-input-section { display: none !important; } +.status-bar { display: none !important; } + +/* ════════════════════════════════ + SWAL2 OVERRIDES +════════════════════════════════ */ +.swal2-popup { + font-family: 'Hanken Grotesk', system-ui, sans-serif !important; + border-radius: 12px !important; + box-shadow: 0 20px 60px rgba(0,0,0,0.15) !important; +} +.swal2-confirm { + background: var(--lumina-primary) !important; + border-radius: 7px !important; box-shadow: none !important; + font-family: inherit !important; +} +.swal2-cancel { + background: #f1f5f9 !important; color: var(--lumina-text) !important; + border-radius: 7px !important; font-family: inherit !important; +} +.swal2-success-circular-line-left, +.swal2-success-circular-line-right, +.swal2-success-fix { background-color: transparent !important; } + +/* ════════════════════════════════ + EMPTY / LOADING +════════════════════════════════ */ +.empty { + color: var(--lumina-text-faint); + font-size: 12px; text-align: center; + padding: 20px 12px; line-height: 1.5; +} +.loading { + color: var(--lumina-text-faint); + font-size: 12px; text-align: center; + padding: 16px 12px; +} + +/* ════════════════════════════════ + RESPONSIVE +════════════════════════════════ */ +@media (max-width: 900px) { + :root { --sidebar-left-w: 100%; --sidebar-right-w: 100%; } + .lumina-sidebar { + top: auto; bottom: 0; left: 0; right: 0; + width: 100%; max-height: 38vh; + border-right: none; border-top: 1px solid var(--lumina-outline); + } + #sidebar, .lumina-panel-right { + top: auto; bottom: 38vh; left: 0; right: 0; + width: 100% !important; max-height: 45vh; + border-left: none; border-top: 1px solid var(--lumina-outline); + } + .lumina-stats { display: none; } + .leaflet-top.leaflet-left { top: calc(var(--navbar-h) + 12px); left: 12px; } + .leaflet-bottom.leaflet-right { right: 14px; bottom: 46vh; } +} diff --git a/db/webgis_kemiskinan.sql b/db/webgis_kemiskinan.sql new file mode 100644 index 0000000..ad37d78 --- /dev/null +++ b/db/webgis_kemiskinan.sql @@ -0,0 +1,46 @@ +-- ============================================================ +-- WebGIS Pemetaan Kemiskinan Berbasis Partisipasi Rumah Ibadah +-- Studi Kasus: Masjid di Kota Pontianak +-- Schema Versi 1.0 – sesuai SRS_CONTEXT.md Bab 5.4 +-- ============================================================ + +CREATE DATABASE IF NOT EXISTS webgis_kemiskinan + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +USE webgis_kemiskinan; + +-- ── Tabel: kemiskinan ───────────────────────────────────────── +-- Menyimpan data spasial lokasi dan atribut keluarga miskin. +-- Koordinat menggunakan WGS84 (EPSG:4326). +CREATE TABLE IF NOT EXISTS kemiskinan ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama_kepala_keluarga VARCHAR(255) NOT NULL COMMENT 'Nama kepala keluarga miskin', + jumlah_kk INT NOT NULL COMMENT 'Jumlah anggota keluarga, angka bulat positif', + alamat TEXT NOT NULL COMMENT 'Alamat lengkap tempat tinggal', + latitude DECIMAL(10,8) NOT NULL COMMENT 'Koordinat lintang WGS84, range -90 s.d. +90', + longitude DECIMAL(11,8) NOT NULL COMMENT 'Koordinat bujur WGS84, range -180 s.d. +180', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Waktu pencatatan data' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ── Tabel: masjid ───────────────────────────────────────────── +-- Menyimpan data spasial lokasi masjid beserta radius layanan sosial. +-- Radius (meter) digunakan sebagai parameter visualisasi L.circle di Leaflet.js. +CREATE TABLE IF NOT EXISTS masjid ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama_masjid VARCHAR(255) NOT NULL COMMENT 'Nama lengkap masjid', + pic_masjid VARCHAR(255) NOT NULL COMMENT 'Nama pengurus/penanggung jawab (PIC)', + alamat TEXT NOT NULL COMMENT 'Alamat lengkap masjid', + latitude DECIMAL(10,8) NOT NULL COMMENT 'Koordinat lintang WGS84', + longitude DECIMAL(11,8) NOT NULL COMMENT 'Koordinat bujur WGS84', + radius INT NOT NULL DEFAULT 500 COMMENT 'Radius layanan sosial dalam meter, range 100-5000', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Waktu pencatatan data' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ── Catatan ERD (SRS Bab 7.4) ───────────────────────────────── +-- Kedua tabel TIDAK memiliki foreign key. +-- Relasi logis antara masjid dan kemiskinan ditentukan secara spasial +-- (kalkulasi jarak vs radius) di layer peta Leaflet, bukan melalui +-- relasi database relasional. Ini merupakan desain yang disederhanakan +-- untuk versi 1.0. Versi lanjutan dapat mengimplementasikan tabel relasi +-- atau tipe spasial MySQL POINT/GEOMETRY. diff --git a/docs/alur_tambah_kemiskinan.drawio b/docs/alur_tambah_kemiskinan.drawio new file mode 100644 index 0000000..dfee2a4 --- /dev/null +++ b/docs/alur_tambah_kemiskinan.drawio @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/alur_update_radius_masjid.drawio b/docs/alur_update_radius_masjid.drawio new file mode 100644 index 0000000..3397d06 --- /dev/null +++ b/docs/alur_update_radius_masjid.drawio @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/arsitektur_sistem.drawio b/docs/arsitektur_sistem.drawio new file mode 100644 index 0000000..8bfb427 --- /dev/null +++ b/docs/arsitektur_sistem.drawio @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/erd.drawio b/docs/erd.drawio new file mode 100644 index 0000000..f060cc8 --- /dev/null +++ b/docs/erd.drawio @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/penjelasan_diagram.md b/docs/penjelasan_diagram.md new file mode 100644 index 0000000..432f423 --- /dev/null +++ b/docs/penjelasan_diagram.md @@ -0,0 +1,13 @@ +# Penjelasan Diagram WebGIS Kemiskinan & Masjid + +### 1. Arsitektur Sistem (Three-Tier) +Sistem ini dibangun menggunakan arsitektur *Three-Tier* yang memisahkan aplikasi menjadi tiga lapisan utama. Lapisan pertama adalah *Presentation Layer* (Client) yang menggunakan HTML, CSS, JavaScript murni, dan Leaflet.js untuk menampilkan antarmuka peta interaktif kepada pengguna. Lapisan kedua adalah *Business Logic Layer* (Server) berbasis PHP murni yang menangani pemrosesan data, validasi, dan operasi CRUD melalui API *endpoints*. Lapisan ketiga adalah *Data Layer* berupa database MySQL (`webgis_kemiskinan`) yang bertugas menyimpan data spasial dan atribut dari entitas kemiskinan serta masjid secara persisten. + +### 2. Alur Data Tambah Titik Kemiskinan +Proses penambahan data kemiskinan dimulai ketika operator mengklik lokasi tertentu pada peta yang memicu munculnya modal *form* input data. Setelah operator mengisi atribut seperti nama kepala keluarga, jumlah anggota, dan alamat, sistem pada sisi *client* (JavaScript) akan memvalidasi input tersebut sebelum mengirimkannya ke server melalui *Fetch API* (metode POST). Skrip PHP di server kemudian akan memvalidasi ulang data dan melakukan *query INSERT* ke database MySQL; jika berhasil, server mengembalikan respons sukses sehingga antarmuka peta dan daftar pada *sidebar* akan diperbarui secara *real-time* untuk menampilkan titik koordinat kemiskinan yang baru. + +### 3. Alur Data Update Radius Masjid +Pembaruan radius layanan masjid dilakukan langsung melalui *card* masjid yang ada di *sidebar* tanpa perlu membuka halaman baru. Saat operator mengubah nilai radius dan menekan tombol *Update*, sistem memastikan nilai yang dimasukkan valid (berada dalam rentang 100 hingga 5.000 meter). Data ID masjid dan radius baru dikirimkan secara asinkron ke server PHP untuk memperbarui data di database (`UPDATE`), lalu secara instan *client* akan menyesuaikan ukuran lingkaran (*circle*) radius masjid pada peta serta menghitung dan memperbarui ulang warna marker kemiskinan (merah atau hijau) berdasarkan jangkauan radius yang baru. + +### 4. Entity Relationship Diagram (ERD) +ERD sistem ini terdiri dari dua entitas utama yang saling independen secara struktural yaitu `kemiskinan` dan `masjid`, di mana masing-masing memiliki atribut *Primary Key* (`id`) serta menyimpan data koordinat spasial (`latitude` dan `longitude`). Keduanya tidak dihubungkan oleh *Foreign Key* relasional secara fisik di tingkat database, melainkan terhubung melalui relasi spasial logis yang dihitung pada sisi *client* (menggunakan fungsi jarak Leaflet). Status jangkauan sebuah keluarga miskin ditentukan secara dinamis dengan membandingkan jarak koordinat kemiskinan ke koordinat masjid terdekat; jika jarak tersebut lebih kecil atau sama dengan atribut `radius` milik masjid, maka keluarga tersebut masuk dalam area terjangkau. diff --git a/index.html b/index.html new file mode 100644 index 0000000..1aaf3fb --- /dev/null +++ b/index.html @@ -0,0 +1,1555 @@ + + + + SosialMap Pontianak — Sistem Pemetaan Sosial + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + +
+
+
Statistik Wilayah
+ +
+
+
+ Keluarga Terjangkau 0% +
+
+
+
+
+
+
0
+
Total Keluarga
+
+
+
0
+
Total Jiwa
+
+
+
+
+
0
+
Perlu Bantuan
+
+
+
0
+
Sudah Mentas
+
+
+
+
Distribusi Rumah Ibadah
+
+
🕌0Masjid
+
0Gereja
+
🛕0Vihara
+
+
+
+ + + + + + + +
+
+
+
+
-
+
-
+
+ +
+
Aktif
+
+
+ +
+
+ + +
+
+ + + + +
+
+
+
💬
+
+
Detail Pesan
+
+
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
🚨
+
+
Detail Laporan
+
+
+ +
+
+
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/anggota.js b/js/anggota.js new file mode 100644 index 0000000..9bf6f0e --- /dev/null +++ b/js/anggota.js @@ -0,0 +1,244 @@ +/* ============================================================ + js/anggota.js — Profil individu anggota keluarga v2.1 + ============================================================ */ + +window.labelHubungan = { + kepala_keluarga:'Kepala Keluarga', istri:'Istri/Suami', anak:'Anak', + orang_tua:'Orang Tua', saudara:'Saudara', lainnya:'Lainnya' +}; +window.labelStatusHidup = { + hidup:'Hidup', meninggal:'Meninggal', pindah:'Pindah', menikah_keluar:'Menikah/Keluar' +}; +window.labelPendidikan = { + belum_sekolah:'Belum Sekolah', paud:'PAUD/TK', sd:'SD', smp:'SMP', sma:'SMA/SMK', + kuliah:'Kuliah/S1', pesantren:'Pesantren', pelatihan:'Pelatihan Vokasi', + bekerja:'Bekerja', tidak_sekolah:'Tidak Sekolah' +}; + +window.getSaranPendidikan = function (umur) { + if (umur === null || umur < 0) return null; + if (umur < 5) return { saran: 'PAUD/TK atau belum sekolah' }; + if (umur <= 12) return { saran: 'SD (usia ' + umur + ' tahun)' }; + if (umur <= 15) return { saran: 'SMP (usia ' + umur + ' tahun)' }; + if (umur <= 18) return { saran: 'SMA/SMK (usia ' + umur + ' tahun)' }; + if (umur <= 24) return { saran: 'Kuliah / Pelatihan Vokasi (usia ' + umur + ' tahun)' }; + return { saran: 'Pelatihan / Bekerja (usia ' + umur + ' tahun)' }; +}; + +window.validasiPendidikan = function (umur, pendidikan) { + if (umur === null || !pendidikan) return null; + var map = { + belum_sekolah:[0,5], paud:[3,7], sd:[6,13], smp:[12,16], + sma:[15,19], kuliah:[18,30], pesantren:[6,25], + pelatihan:[15,99], bekerja:[17,99], tidak_sekolah:[0,99] + }; + var range = map[pendidikan]; + if (!range) return null; + if (umur < range[0] || umur > range[1]) { + var saran = window.getSaranPendidikan(umur); + return { cocok: false, pesan: 'Umur ' + umur + ' th tidak sesuai dengan "' + + (window.labelPendidikan[pendidikan]||pendidikan) + '".' + + (saran ? ' Disarankan: ' + saran.saran : '') }; + } + return { cocok: true, pesan: null }; +}; + +window._hitungUmur = function (tglLahir) { + if (!tglLahir) return null; + var today = new Date(), lahir = new Date(tglLahir); + if (isNaN(lahir.getTime())) return null; + var u = today.getFullYear() - lahir.getFullYear(); + var m = today.getMonth() - lahir.getMonth(); + if (m < 0 || (m === 0 && today.getDate() < lahir.getDate())) u--; + return u; +}; + +/* ── Buka modal tambah / edit ── */ +window.openTambahAnggotaModal = function (kemiskinanId, namaKK) { + document.getElementById('anggota-kem-id').value = kemiskinanId; + document.getElementById('anggota-edit-id').value = ''; + document.getElementById('anggota-nama').value = ''; + document.getElementById('anggota-tgl-lahir').value = ''; + document.getElementById('anggota-jk').value = 'laki_laki'; + document.getElementById('anggota-hubungan').value = 'anak'; + document.getElementById('anggota-status').value = 'hidup'; + document.getElementById('anggota-pendidikan').value = 'belum_sekolah'; + document.getElementById('anggota-catatan').value = ''; + var s = document.getElementById('anggota-saran'); + if (s) { s.textContent = ''; s.className = 'saran-box'; s.style.display = 'none'; } + document.getElementById('modal-anggota-title').textContent = 'Tambah Anggota — ' + window.escapeHtml(namaKK); + + var modal = document.getElementById('modal-anggota'); + modal.style.display = 'flex'; + document.getElementById('anggota-nama').focus(); + modal.onclick = function (e) { if (e.target === this) this.style.display = 'none'; }; + + window._bindAnggotaEvents(); + document.getElementById('btn-submit-anggota').onclick = window._submitTambahAnggota; +}; + +window.openEditAnggotaModal = function (a, namaKK) { + document.getElementById('anggota-kem-id').value = a.kemiskinan_id; + document.getElementById('anggota-edit-id').value = a.id; + document.getElementById('anggota-nama').value = a.nama; + document.getElementById('anggota-tgl-lahir').value = a.tanggal_lahir || ''; + document.getElementById('anggota-jk').value = a.jenis_kelamin; + document.getElementById('anggota-hubungan').value = a.hubungan_keluarga; + document.getElementById('anggota-status').value = a.status_hidup; + document.getElementById('anggota-pendidikan').value = a.pendidikan; + document.getElementById('anggota-catatan').value = a.catatan_khusus || ''; + document.getElementById('modal-anggota-title').textContent = 'Edit Anggota — ' + window.escapeHtml(namaKK); + + var modal = document.getElementById('modal-anggota'); + modal.style.display = 'flex'; + document.getElementById('anggota-nama').focus(); + modal.onclick = function (e) { if (e.target === this) this.style.display = 'none'; }; + + window._bindAnggotaEvents(); + window._updateAnggotaSaran(); + document.getElementById('btn-submit-anggota').onclick = window._submitEditAnggota; +}; + +window._bindAnggotaEvents = function () { + var tgl = document.getElementById('anggota-tgl-lahir'); + var pend = document.getElementById('anggota-pendidikan'); + if (!tgl._bound) { + tgl.addEventListener('change', window._updateAnggotaSaran); + pend.addEventListener('change', window._updateAnggotaSaran); + tgl._bound = true; + } +}; + +window._updateAnggotaSaran = function () { + var tgl = document.getElementById('anggota-tgl-lahir').value; + var pend = document.getElementById('anggota-pendidikan').value; + var s = document.getElementById('anggota-saran'); + if (!s) return; + var umur = window._hitungUmur(tgl); + if (umur === null) { s.style.display = 'none'; s.textContent = ''; return; } + s.style.display = 'block'; + var v = window.validasiPendidikan(umur, pend); + if (!v) { s.className = 'saran-box info'; s.textContent = 'Umur: ' + umur + ' tahun'; return; } + if (v.cocok) { + var sar = window.getSaranPendidikan(umur); + s.className = 'saran-box ok'; + s.textContent = '✓ Umur ' + umur + ' tahun — pendidikan sesuai.'; + } else { + s.className = 'saran-box warn'; + s.innerHTML = ' ' + v.pesan; + } +}; + +window._submitTambahAnggota = async function () { + var btn = document.getElementById('btn-submit-anggota'); + btn.disabled = true; btn.textContent = 'Menyimpan...'; + try { + await window.postForm('php/create_anggota.php', window._collectAnggotaForm()); + document.getElementById('modal-anggota').style.display = 'none'; + window.showToast('Anggota berhasil ditambahkan.', 'success'); + if (window._currentDetailItem) window.openDetailPanel(window._currentDetailItem); + } catch (err) { window.showToast('Gagal: ' + err.message, 'error'); } + finally { btn.disabled = false; btn.textContent = 'Simpan'; } +}; + +window._submitEditAnggota = async function () { + var btn = document.getElementById('btn-submit-anggota'); + btn.disabled = true; btn.textContent = 'Menyimpan...'; + var payload = window._collectAnggotaForm(); + payload.id = document.getElementById('anggota-edit-id').value; + try { + await window.postForm('php/update_anggota.php', payload); + document.getElementById('modal-anggota').style.display = 'none'; + window.showToast('Data anggota diperbarui.', 'success'); + if (window._currentDetailItem) window.openDetailPanel(window._currentDetailItem); + } catch (err) { window.showToast('Gagal: ' + err.message, 'error'); } + finally { btn.disabled = false; btn.textContent = 'Simpan'; } +}; + +window._collectAnggotaForm = function () { + return { + kemiskinan_id: document.getElementById('anggota-kem-id').value, + nama: document.getElementById('anggota-nama').value.trim(), + tanggal_lahir: document.getElementById('anggota-tgl-lahir').value, + jenis_kelamin: document.getElementById('anggota-jk').value, + hubungan_keluarga: document.getElementById('anggota-hubungan').value, + status_hidup: document.getElementById('anggota-status').value, + pendidikan: document.getElementById('anggota-pendidikan').value, + catatan_khusus: document.getElementById('anggota-catatan').value.trim() + }; +}; + +window.deleteAnggota = async function (id, nama) { + if (!confirm('Hapus anggota "' + nama + '"?')) return; + try { + await window.postForm('php/delete_anggota.php', { id }); + window.showToast('Anggota dihapus.', 'success'); + if (window._currentDetailItem) window.openDetailPanel(window._currentDetailItem); + } catch (err) { window.showToast('Gagal: ' + err.message, 'error'); } +}; + +/* ── Render tab anggota ── */ +window.renderTabAnggota = async function (item) { + var container = document.getElementById('detail-tab-anggota'); + if (!container) return; + container.innerHTML = '
Memuat data anggota...
'; + var data = await window.fetchJson('php/read_anggota.php?kemiskinan_id=' + item.id, []); + container.innerHTML = ''; + + if (window.appAuth.role === 'pengurus_masjid') { + var btnAdd = document.createElement('button'); + btnAdd.className = 'btn btn-add'; + btnAdd.innerHTML = '+ Tambah Anggota'; + btnAdd.onclick = function () { window.openTambahAnggotaModal(item.id, item.nama_kepala_keluarga); }; + container.appendChild(btnAdd); + } + + if (!data.length) { + var empty = document.createElement('div'); + empty.className = 'empty'; + empty.textContent = 'Belum ada data anggota.'; + container.appendChild(empty); + return; + } + + data.forEach(function (a) { + var umur = a.umur !== null ? parseInt(a.umur) : null; + var umurTxt = umur !== null ? umur + ' tahun' : '—'; + var tglTxt = a.tgl_lahir_fmt || '—'; + var val = umur !== null ? window.validasiPendidikan(umur, a.pendidikan) : null; + + var card = document.createElement('div'); + card.className = 'anggota-card ' + a.status_hidup; + + /* Status chip */ + var chipColors = { hidup:'chip-green', meninggal:'chip-gray', pindah:'chip-amber', menikah_keluar:'chip-purple' }; + var chipColor = chipColors[a.status_hidup] || 'chip-gray'; + + card.innerHTML = + '
' + + '' + window.escapeHtml(a.nama) + '' + + '' + (window.labelStatusHidup[a.status_hidup] || a.status_hidup) + '' + + '
' + + '
Hubungan' + (window.labelHubungan[a.hubungan_keluarga] || a.hubungan_keluarga) + '
' + + '
Lahir' + tglTxt + ' (' + umurTxt + ')
' + + '
JK' + (a.jenis_kelamin === 'laki_laki' ? 'Laki-laki' : 'Perempuan') + '
' + + '
Pendidikan' + (window.labelPendidikan[a.pendidikan] || a.pendidikan) + '
' + + (val && !val.cocok ? '
' + window.escapeHtml(val.pesan) + '
' : '') + + (a.catatan_khusus ? '
Catatan' + window.escapeHtml(a.catatan_khusus) + '
' : '') + + (a.updated_at_fmt ? '
Update' + a.updated_at_fmt + '
' : ''); + + if (window.appAuth.role === 'pengurus_masjid') { + var acts = document.createElement('div'); + acts.className = 'ang-acts'; + var btnE = document.createElement('button'); + btnE.className = 'btn btn-edit'; btnE.innerHTML = ' Edit'; + btnE.onclick = (function(aa){ return function(){ window.openEditAnggotaModal(aa, item.nama_kepala_keluarga); }; })(a); + var btnH = document.createElement('button'); + btnH.className = 'btn btn-delete'; btnH.textContent = 'Hapus'; + btnH.onclick = (function(aa){ return function(){ window.deleteAnggota(aa.id, aa.nama); }; })(a); + acts.appendChild(btnE); acts.appendChild(btnH); + card.appendChild(acts); + } + container.appendChild(card); + }); +}; diff --git a/js/auth.js b/js/auth.js new file mode 100644 index 0000000..8aae4ce --- /dev/null +++ b/js/auth.js @@ -0,0 +1,192 @@ +/* ============================================================ + js/auth.js — Login / Logout / Role management v2.1 + ============================================================ */ + +window.appAuth = { loggedIn: false, nama: '', role: '' }; + +window.checkAuthState = async function () { + try { + var res = await fetch('php/check_auth.php', { cache: 'no-store' }); + var data = await res.json(); + window.appAuth.loggedIn = !!data.loggedIn; + window.appAuth.nama = data.nama || ''; + window.appAuth.role = data.role || ''; + } catch (e) { + window.appAuth = { loggedIn: false, nama: '', role: '' }; + } + window.renderAuthBar(); + if (!window.appAuth.loggedIn) { + window.openLoginModal(); + } +}; + +/* ── Render auth area di navbar ── */ +window.renderAuthBar = function () { + var area = document.getElementById('auth-area'); + if (!area) return; + area.innerHTML = ''; + + /* Clean up old nav buttons */ + var navMenus = document.getElementById('nav-menus'); + if (navMenus) { + ['btn-pesan-nav','btn-laporan-mgr-nav'].forEach(function(id) { + var el = document.getElementById(id); + if (el) el.remove(); + }); + } + + if (!window.appAuth.loggedIn) { + /* Show Masuk button */ + var btnMasuk = document.createElement('button'); + btnMasuk.className = 'nav-btn'; + btnMasuk.id = 'btn-masuk-nav'; + btnMasuk.innerHTML = ' Masuk'; + btnMasuk.onclick = window.openLoginModal; + area.appendChild(btnMasuk); + } else { + /* Role chip */ + var chip = document.createElement('span'); + var isPimpinan = window.appAuth.role === 'pimpinan_daerah'; + chip.className = 'role-chip ' + (isPimpinan ? 'pimpinan' : 'pengurus'); + chip.innerHTML = isPimpinan + ? ' Pimpinan' + : ' Pengurus'; + area.appendChild(chip); + + var name = document.createElement('span'); + name.className = 'auth-name'; + name.textContent = window.appAuth.nama; + area.appendChild(name); + + /* Pengurus: add Pesan & Laporan buttons to nav-menus */ + if (window.appAuth.role === 'pengurus_masjid' && navMenus) { + var btnMsg = document.createElement('button'); + btnMsg.className = 'nav-btn msg-btn'; + btnMsg.id = 'btn-pesan-nav'; + btnMsg.innerHTML = ' Pesan '; + btnMsg.onclick = window.openPesanPanel; + navMenus.appendChild(btnMsg); + + var btnLap = document.createElement('button'); + btnLap.className = 'nav-btn mgr-btn'; + btnLap.id = 'btn-laporan-mgr-nav'; + btnLap.innerHTML = ' Laporan '; + btnLap.onclick = window.openLaporanPanel; + navMenus.appendChild(btnLap); + } + + var btnLogout = document.createElement('button'); + btnLogout.className = 'nav-btn logout-btn'; + btnLogout.id = 'btn-logout-nav'; + btnLogout.innerHTML = ' Keluar'; + btnLogout.onclick = window.doLogout; + area.appendChild(btnLogout); + } + + window.updateToolbarForRole(); + + if (window.pollLaporanNotification) window.pollLaporanNotification(); + if (window.pollPesanNotification) window.pollPesanNotification(); +}; + + +/* ── Role-based toolbar control ── */ +window.updateToolbarForRole = function () { + var editBtn = document.getElementById('luminaModeEdit'); + var menuKeluarga = document.getElementById('menu-tambah-keluarga'); + var menuIbadah = document.getElementById('menu-tambah-ibadah'); + + var isPengurus = window.appAuth && window.appAuth.loggedIn && window.appAuth.role === 'pengurus_masjid'; + + if (isPengurus) { + if (editBtn) editBtn.style.display = 'block'; + if (menuKeluarga) menuKeluarga.style.display = 'flex'; + if (menuIbadah) menuIbadah.style.display = 'flex'; + } else { + if (editBtn) editBtn.style.display = 'none'; + if (menuKeluarga) menuKeluarga.style.display = 'none'; + if (menuIbadah) menuIbadah.style.display = 'none'; + if (window.appState && window.appState.mode === 'edit') window.setMode('view'); + } +}; + +/* ── Open login modal ── */ +window.openLoginModal = function () { + var overlay = document.getElementById('modal-login'); + if (!overlay) return; + document.getElementById('login-username').value = ''; + document.getElementById('login-password').value = ''; + document.getElementById('login-error').textContent = ''; + overlay.style.display = 'flex'; + document.getElementById('login-username').focus(); + // Do not allow closing +}; + +/* ── Login submit ── */ +window.doLogin = async function (e) { + e.preventDefault(); + var btn = document.getElementById('btn-submit-login'); + var errEl = document.getElementById('login-error'); + var uname = document.getElementById('login-username').value.trim(); + var pass = document.getElementById('login-password').value; + + if (!uname || !pass) { + errEl.textContent = 'Username dan password wajib diisi.'; + errEl.style.display = 'block'; + return; + } + btn.disabled = true; btn.textContent = 'Masuk...'; + errEl.textContent = ''; + errEl.style.display = 'none'; + + try { + var res = await fetch('php/login.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ username: uname, password: pass }) + }); + var data = await res.json(); + if (data.success) { + window.appAuth = { loggedIn: true, nama: data.nama, role: data.role }; + document.getElementById('modal-login').style.display = 'none'; + window.renderAuthBar(); + window.loadAllData(); + window.showToast('Selamat datang, ' + data.nama + '!', 'success'); + } else { + errEl.textContent = data.error || 'Login gagal.'; + errEl.style.display = 'block'; + } + } catch (err) { + errEl.textContent = 'Koneksi gagal: ' + err.message; + errEl.style.display = 'block'; + } finally { + btn.disabled = false; btn.innerHTML = 'Masuk Sekarang '; + } +}; + +/* ── Logout ── */ +window.doLogout = async function () { + await fetch('php/logout.php', { cache: 'no-store' }); + window.appAuth = { loggedIn: false, nama: '', role: '' }; + window.renderAuthBar(); + if (window.appState && window.appState.mode === 'edit') window.setMode('view'); + window.loadAllData(); + window.openLoginModal(); + window.showToast('Anda telah keluar.', 'info'); +}; + +/* ── Toggle Password Visibility ── */ +window.togglePassword = function () { + var passInput = document.getElementById('login-password'); + var eyeIcon = document.getElementById('login-password-eye'); + if (passInput.type === 'password') { + passInput.type = 'text'; + eyeIcon.classList.remove('fa-eye'); + eyeIcon.classList.add('fa-eye-slash'); + } else { + passInput.type = 'password'; + eyeIcon.classList.remove('fa-eye-slash'); + eyeIcon.classList.add('fa-eye'); + } +}; + diff --git a/js/bantuan.js b/js/bantuan.js new file mode 100644 index 0000000..36e8a26 --- /dev/null +++ b/js/bantuan.js @@ -0,0 +1,154 @@ +/* ============================================================ + js/bantuan.js — Histori bantuan v2.1 + ============================================================ */ + +window.labelJenisBantuan = { + sembako:'Sembako', uang:'Uang/Dana', pendidikan:'Pendidikan', + kesehatan:'Kesehatan', pakaian:'Pakaian', pelatihan:'Pelatihan', lainnya:'Lainnya' +}; +window.labelStatusBantuan = { + tersalur:'Tersalur', belum:'Belum Tersalur', batal:'Dibatalkan' +}; + +/* Render tab bantuan */ +window.renderTabBantuan = async function (item) { + var container = document.getElementById('detail-tab-bantuan'); + if (!container) return; + container.innerHTML = '
Memuat histori bantuan...
'; + + var data = await window.fetchJson('php/read_bantuan.php?kemiskinan_id=' + item.id, []); + container.innerHTML = ''; + + /* Alert jatuh tempo */ + var jt = data.filter(function(d){ return parseInt(d.jatuh_tempo) === 1; }); + if (jt.length) { + var alert = document.createElement('div'); + alert.className = 'bantuan-alert'; + alert.innerHTML = ' ' + jt.length + ' bantuan sudah jatuh tempo dan belum disalurkan!'; + container.appendChild(alert); + } + + if (window.appAuth.role === 'pengurus_masjid') { + var btnAdd = document.createElement('button'); + btnAdd.className = 'btn btn-add'; + btnAdd.innerHTML = '+ Catat Bantuan Baru'; + btnAdd.onclick = function () { window.openTambahBantuanModal(item.id, item.nama_kepala_keluarga); }; + container.appendChild(btnAdd); + } + + if (!data.length) { + var empty = document.createElement('div'); + empty.className = 'empty'; empty.textContent = 'Belum ada histori bantuan.'; + container.appendChild(empty); return; + } + + data.forEach(function (b) { + var jmlTxt = b.jumlah_bantuan ? b.jumlah_bantuan + ' ' + (b.satuan || '') : '—'; + var isJt = parseInt(b.jatuh_tempo) === 1; + + var card = document.createElement('div'); + card.className = 'b-card ' + b.status_penyaluran + (isJt ? ' jatuh' : ''); + + var chipColors = { tersalur:'chip-green', belum:'chip-amber', batal:'chip-gray' }; + + card.innerHTML = + '
' + + '' + (window.labelJenisBantuan[b.jenis_bantuan] || b.jenis_bantuan) + '' + + '' + (window.labelStatusBantuan[b.status_penyaluran]||b.status_penyaluran) + '' + + '
' + + '
' + window.escapeHtml(b.deskripsi_bantuan) + '
' + + '
📅 ' + (b.tanggal_fmt||'—') + ' · ' + window.escapeHtml(jmlTxt) + (b.nama_masjid ? ' · ' + window.escapeHtml(b.nama_masjid) : '') + '
' + + (b.periode_fmt ? '
Berikutnya: ' + b.periode_fmt + (isJt ? ' — Jatuh Tempo!' : '') + '
' : '') + + (b.dicatat_oleh ? '
Dicatat: ' + window.escapeHtml(b.dicatat_oleh) + '
' : ''); + + if (window.appAuth.role === 'pengurus_masjid') { + var acts = document.createElement('div'); + acts.className = 'b-acts'; + + if (b.status_penyaluran === 'belum') { + var btnS = document.createElement('button'); + btnS.className = 'btn btn-salur'; btnS.textContent = '✓ Tandai Tersalur'; + btnS.onclick = (function(bid){ return async function(){ + try { + await window.postForm('php/update_bantuan_status.php', { id: bid, status_penyaluran: 'tersalur' }); + window.showToast('Bantuan ditandai tersalur.', 'success'); + window.renderTabBantuan(item); + } catch(e){ window.showToast('Gagal: ' + e.message, 'error'); } + }; })(b.id); + acts.appendChild(btnS); + + var btnBt = document.createElement('button'); + btnBt.className = 'btn btn-delete'; btnBt.textContent = 'Batalkan'; + btnBt.onclick = (function(bid){ return async function(){ + if (!confirm('Batalkan bantuan ini?')) return; + try { + await window.postForm('php/update_bantuan_status.php', { id: bid, status_penyaluran: 'batal' }); + window.showToast('Bantuan dibatalkan.', 'info'); + window.renderTabBantuan(item); + } catch(e){ window.showToast('Gagal: ' + e.message, 'error'); } + }; })(b.id); + acts.appendChild(btnBt); + } + + var btnH = document.createElement('button'); + btnH.className = 'btn btn-delete'; btnH.innerHTML = ''; + btnH.onclick = (function(bid){ return async function(){ + if (!confirm('Hapus catatan bantuan ini?')) return; + try { + await window.postForm('php/delete_bantuan.php', { id: bid }); + window.showToast('Catatan dihapus.', 'success'); + window.renderTabBantuan(item); + } catch(e){ window.showToast('Gagal: ' + e.message, 'error'); } + }; })(b.id); + acts.appendChild(btnH); + card.appendChild(acts); + } + container.appendChild(card); + }); +}; + +/* Modal tambah bantuan */ +window.openTambahBantuanModal = function (kemiskinanId, namaKK) { + document.getElementById('bantuan-kem-id').value = kemiskinanId; + document.getElementById('bantuan-jenis').value = 'sembako'; + document.getElementById('bantuan-deskripsi').value = ''; + document.getElementById('bantuan-jumlah').value = ''; + document.getElementById('bantuan-satuan').value = ''; + document.getElementById('bantuan-tanggal').value = new Date().toISOString().split('T')[0]; + document.getElementById('bantuan-periode').value = ''; + document.getElementById('bantuan-status').value = 'belum'; + document.getElementById('bantuan-dicatat').value = window.appAuth.nama || ''; + document.getElementById('modal-bantuan-title').textContent = 'Catat Bantuan — ' + namaKK; + + var modal = document.getElementById('modal-bantuan'); + modal.style.display = 'flex'; + document.getElementById('bantuan-deskripsi').focus(); + modal.onclick = function (e) { if (e.target === this) this.style.display = 'none'; }; + + document.getElementById('btn-submit-bantuan').onclick = async function () { + var btn = this; + btn.disabled = true; btn.textContent = 'Menyimpan...'; + var desc = document.getElementById('bantuan-deskripsi').value.trim(); + if (!desc) { window.showToast('Deskripsi wajib diisi.', 'error'); btn.disabled=false; btn.textContent='Simpan Bantuan'; return; } + var payload = { + kemiskinan_id: document.getElementById('bantuan-kem-id').value, + jenis_bantuan: document.getElementById('bantuan-jenis').value, + deskripsi_bantuan: desc, + jumlah_bantuan: document.getElementById('bantuan-jumlah').value, + satuan: document.getElementById('bantuan-satuan').value.trim(), + tanggal_bantuan: document.getElementById('bantuan-tanggal').value, + periode_berikutnya:document.getElementById('bantuan-periode').value, + status_penyaluran: document.getElementById('bantuan-status').value, + dicatat_oleh: document.getElementById('bantuan-dicatat').value.trim() + }; + try { + await window.postForm('php/create_bantuan.php', payload); + modal.style.display = 'none'; + window.showToast('Bantuan berhasil dicatat.', 'success'); + if (window._currentDetailItem && window._currentDetailItem.id === parseInt(payload.kemiskinan_id)) { + window.renderTabBantuan(window._currentDetailItem); + } + } catch (err) { window.showToast('Gagal: ' + err.message, 'error'); } + finally { btn.disabled=false; btn.textContent='Simpan Bantuan'; } + }; +}; diff --git a/js/laporan.js b/js/laporan.js new file mode 100644 index 0000000..7ed6411 --- /dev/null +++ b/js/laporan.js @@ -0,0 +1,330 @@ +/* ============================================================ + js/laporan.js — Sistem pelaporan warga v2.2 + ============================================================ */ + +window.labelKategoriLaporan = { + sakit:'Sakit / Butuh Bantuan Medis', kematian:'Kematian', + kelahiran:'Kelahiran Bayi Baru', pernikahan:'Pernikahan', + pindah:'Pindah Tempat', darurat_ekonomi:'Darurat Ekonomi', lainnya:'Lainnya' +}; +window.labelStatusLaporan = { + menunggu:'Menunggu', diproses:'Diproses', selesai:'Selesai', ditolak:'Ditolak' +}; + +var _kategoriEmoji = { + sakit:'🤒', kematian:'💔', kelahiran:'👶', pernikahan:'💍', + pindah:'📦', darurat_ekonomi:'🆘', lainnya:'📣' +}; + +/* ───────────────────────────────────────────────────── + Floating detail box +───────────────────────────────────────────────────── */ +window.openLaporanDetail = function (laporan) { + var fl = document.getElementById('laporan-detail-float'); + if (!fl) return; + + var status = laporan.status_laporan || 'menunggu'; + var emoji = _kategoriEmoji[laporan.kategori] || '📣'; + var label = window.labelKategoriLaporan[laporan.kategori] || laporan.kategori; + var statusLabel = window.labelStatusLaporan[status] || status; + + /* Status bar colour */ + var bar = document.getElementById('ldf-status-bar'); + bar.className = 'ldf-status-bar ' + status; + + /* Icon */ + var icon = document.getElementById('ldf-icon'); + icon.textContent = emoji; + icon.className = 'ldf-icon ' + status; + + /* Title / subtitle */ + document.getElementById('ldf-title').textContent = label; + document.getElementById('ldf-subtitle').textContent = '● ' + statusLabel; + document.getElementById('ldf-subtitle').style.color = { + menunggu: 'var(--amber)', diproses: 'var(--blue)', + selesai: 'var(--green)', ditolak: 'var(--text-muted)' + }[status] || 'var(--text-muted)'; + + /* Deskripsi */ + document.getElementById('ldf-deskripsi').textContent = laporan.deskripsi || '(tidak ada deskripsi)'; + + /* KK terkait */ + var sectionKK = document.getElementById('ldf-section-kk'); + var badgeKK = document.getElementById('ldf-kk-badge'); + if (laporan.nama_kepala_keluarga) { + badgeKK.innerHTML = ' ' + laporan.nama_kepala_keluarga; + sectionKK.style.display = ''; + } else { + sectionKK.style.display = 'none'; + } + + /* Info rows */ + document.getElementById('ldf-pelapor').textContent = laporan.nama_pelapor || '-'; + + var rowWA = document.getElementById('ldf-row-wa'); + if (laporan.nomor_wa) { + document.getElementById('ldf-wa').textContent = laporan.nomor_wa; + rowWA.style.display = ''; + } else { + rowWA.style.display = 'none'; + } + + document.getElementById('ldf-waktu').textContent = laporan.created_fmt || '-'; + + var rowDitangani = document.getElementById('ldf-row-ditangani'); + if (laporan.ditangani_oleh) { + document.getElementById('ldf-ditangani').textContent = laporan.ditangani_oleh; + rowDitangani.style.display = ''; + } else { + rowDitangani.style.display = 'none'; + } + + /* Action buttons (pengurus only) */ + var footer = document.getElementById('ldf-footer'); + footer.innerHTML = ''; + if (window.appAuth && window.appAuth.role === 'pengurus_masjid' && status !== 'selesai' && status !== 'ditolak') { + footer.style.display = ''; + var statuses = [ + { val: 'diproses', label: ' Proses', cls: 'btn-proses' }, + { val: 'selesai', label: ' Selesai', cls: 'btn-selesai' }, + { val: 'ditolak', label: ' Tolak', cls: 'btn-tolak' } + ]; + statuses.forEach(function (s) { + if (s.val === status) return; + var btn = document.createElement('button'); + btn.className = 'btn ' + s.cls; + btn.innerHTML = s.label; + btn.onclick = (function (lid, sv) { + return async function () { + btn.disabled = true; + try { + await window.postForm('php/update_laporan_status.php', { + id: lid, status_laporan: sv, ditangani_oleh: window.appAuth.nama + }); + window.showToast('Status diperbarui ke: ' + window.labelStatusLaporan[sv], 'success'); + window.closeLaporanDetail(); + window.loadLaporan(document.getElementById('laporan-filter-status').value); + } catch (e) { + window.showToast('Gagal: ' + e.message, 'error'); + btn.disabled = false; + } + }; + })(laporan.id, s.val); + footer.appendChild(btn); + }); + } else { + footer.style.display = 'none'; + } + + /* Close button */ + document.getElementById('ldf-close').onclick = window.closeLaporanDetail; + + /* Show with animation */ + fl.style.display = 'flex'; +}; + +window.closeLaporanDetail = function () { + var fl = document.getElementById('laporan-detail-float'); + if (fl) fl.style.display = 'none'; +}; + +/* ── Form laporan publik ── */ +window.openLaporanPublikModal = function (kemiskinanId, namaKK) { + var modal = document.getElementById('modal-laporan-publik'); + document.getElementById('laporan-kem-id').value = kemiskinanId || ''; + document.getElementById('laporan-nama-pelapor').value = ''; + document.getElementById('laporan-nomor-wa').value = ''; + document.getElementById('laporan-kategori').value = 'lainnya'; + document.getElementById('laporan-deskripsi').value = ''; + document.getElementById('laporan-judul-kkname').textContent = namaKK + ? 'Laporkan Kondisi: ' + namaKK + : 'Laporkan Kondisi Warga'; + modal.style.display = 'flex'; + document.getElementById('laporan-nama-pelapor').focus(); + modal.onclick = function (e) { if (e.target === this) this.style.display = 'none'; }; +}; + +/* ── Tombol submit laporan dipanggil dari inline HTML ── */ +window._submitLaporanBtn = async function () { + var btn = document.getElementById('btn-submit-laporan'); + var nama = document.getElementById('laporan-nama-pelapor').value.trim(); + var desc = document.getElementById('laporan-deskripsi').value.trim(); + if (!nama || !desc) { window.showToast('Nama dan deskripsi wajib diisi.', 'error'); return; } + + btn.disabled = true; btn.textContent = 'Mengirim...'; + var payload = { + kemiskinan_id: document.getElementById('laporan-kem-id').value, + nama_pelapor: nama, + nomor_wa: document.getElementById('laporan-nomor-wa').value.trim(), + kategori: document.getElementById('laporan-kategori').value, + deskripsi: desc + }; + try { + var res = await fetch('php/create_laporan.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams(payload) + }); + var text = (await res.text()).trim(); + if (text === 'success') { + document.getElementById('modal-laporan-publik').style.display = 'none'; + if (window.Swal) { + Swal.fire({ + icon: 'success', + title: 'Laporan Terkirim', + text: 'Laporan berhasil dikirim. Terima kasih atas informasi Anda!', + confirmButtonText: 'Tutup' + }); + } else { + window.showToast('Laporan berhasil dikirim. Terima kasih!', 'success'); + } + } else { + window.showToast('Gagal: ' + text, 'error'); + } + } catch (err) { window.showToast('Gagal: ' + err.message, 'error'); } + finally { btn.disabled = false; btn.textContent = 'Kirim Laporan'; } +}; + +/* ── Panel manajemen (pengurus) ── */ +window.openLaporanPanel = async function () { + var panel = document.getElementById('panel-laporan'); + if (panel) { + var sidebar = document.getElementById('sidebar'); + if (sidebar) sidebar.style.display = 'none'; + var pes = document.getElementById('panel-pesan'); + if (pes) pes.style.display = 'none'; + + panel.style.display = 'flex'; + document.getElementById('laporan-filter-status').value = 'menunggu'; + window.loadLaporan('menunggu'); + } +}; +window.closeLaporanPanel = function () { + var p = document.getElementById('panel-laporan'); + if (p) p.style.display = 'none'; + window.closeLaporanDetail(); + + // Auto-open sidebar + var sidebar = document.getElementById('sidebar'); + if (sidebar) sidebar.style.display = 'flex'; +}; + +window.loadLaporan = async function (statusFilter) { + var container = document.getElementById('laporan-list-container'); + if (!container) return; + container.innerHTML = '
Memuat laporan...
'; + + var url = 'php/read_laporan.php' + (statusFilter ? '?status=' + statusFilter : ''); + var data = await window.fetchJson(url, []); + container.innerHTML = ''; + + if (!data.length) { + container.innerHTML = '
Tidak ada laporan.
'; return; + } + + data.forEach(function (l) { + var chipColors = { menunggu:'chip-amber', diproses:'chip-blue', selesai:'chip-green', ditolak:'chip-gray' }; + var emoji = _kategoriEmoji[l.kategori] || '📣'; + + var card = document.createElement('div'); + card.className = 'l-card ' + l.status_laporan; + card.title = 'Klik untuk detail lengkap'; + + var badgeCls = { menunggu:'chip-amber', diproses:'chip-blue', selesai:'chip-green', ditolak:'chip-gray' }[l.status_laporan] || 'chip-gray'; + var kkHtml = l.nama_kepala_keluarga + ? '
KK: ' + window.escapeHtml(l.nama_kepala_keluarga) + '
' : ''; + + card.innerHTML = + '
' + + '' + emoji + ' ' + (window.labelKategoriLaporan[l.kategori] || l.kategori) + '' + + '' + window.escapeHtml(window.labelStatusLaporan[l.status_laporan] || l.status_laporan) + '' + + '
' + + kkHtml + + '
' + window.escapeHtml(l.deskripsi) + '
' + + '
' + window.escapeHtml(l.nama_pelapor) + + (l.nomor_wa ? ' · ' + window.escapeHtml(l.nomor_wa) : '') + + ' · ' + l.created_fmt + '
' + + (l.ditangani_oleh ? '
Ditangani: ' + window.escapeHtml(l.ditangani_oleh) + '
' : ''); + + /* Klik kartu → buka floating detail */ + card.addEventListener('click', function (e) { + /* Jangan trigger jika yang diklik tombol aksi */ + if (e.target.closest('button')) return; + window.openLaporanDetail(l); + }); + + if (window.appAuth.role === 'pengurus_masjid' && l.status_laporan !== 'selesai' && l.status_laporan !== 'ditolak') { + var acts = document.createElement('div'); + acts.className = 'l-acts'; + var statuses = [ + { val: 'diproses', label: 'Proses', cls: 'btn-proses' }, + { val: 'selesai', label: 'Selesai', cls: 'btn-selesai' }, + { val: 'ditolak', label: 'Tolak', cls: 'btn-tolak' } + ]; + statuses.forEach(function (s) { + if (s.val === l.status_laporan) return; + var btn = document.createElement('button'); + btn.className = 'btn ' + s.cls; btn.textContent = s.label; + btn.onclick = (function(lid, sv){ return async function(){ + try { + await window.postForm('php/update_laporan_status.php', { + id: lid, status_laporan: sv, ditangani_oleh: window.appAuth.nama + }); + window.showToast('Status diperbarui.', 'success'); + window.closeLaporanDetail(); + window.loadLaporan(document.getElementById('laporan-filter-status').value); + } catch(e){ window.showToast('Gagal: ' + e.message, 'error'); } + }; })(l.id, s.val); + acts.appendChild(btn); + }); + card.appendChild(acts); + } + container.appendChild(card); + }); +}; + +/* Polling for Laporan Notification */ +window.pollLaporanNotification = async function() { + if (!window.appAuth || window.appAuth.role !== 'pengurus_masjid') { + var np = document.getElementById('notif-panel'); + if (np) np.style.display = 'none'; + return; + } + try { + var res = await fetch('php/read_laporan.php'); + var text = await res.text(); + var data = JSON.parse(text); + if(Array.isArray(data)) { + var pendingCount = data.filter(function(l) { return l.status_laporan === 'menunggu'; }).length; + + var badge = document.getElementById('nav-badge-laporan'); + if(badge) { + if(pendingCount > 0) { + badge.style.display = 'inline-flex'; + badge.textContent = pendingCount; + } else { + badge.style.display = 'none'; + } + } + + var np = document.getElementById('notif-panel'); + var nb = document.getElementById('notif-body'); + if (np && nb) { + if(pendingCount > 0) { + var isPanelOpen = document.getElementById('panel-laporan') && document.getElementById('panel-laporan').style.display === 'flex'; + if(!isPanelOpen && (!window._notifDismissedForCount || window._notifDismissedForCount < pendingCount)) { + np.style.display = 'block'; + nb.innerHTML = 'Terdapat ' + pendingCount + ' laporan baru dari warga yang belum diproses.
Harap segera ditindaklanjuti!'; + window._notifDismissedForCount = pendingCount; + } + } else { + np.style.display = 'none'; + window._notifDismissedForCount = 0; + } + } + } + } catch(e) {} +}; + +// Start polling every 10 seconds +setInterval(window.pollLaporanNotification, 10000); diff --git a/js/loadData.js b/js/loadData.js new file mode 100644 index 0000000..cd47d3a --- /dev/null +++ b/js/loadData.js @@ -0,0 +1,1105 @@ +/* ============================================================ + loadData.js – WebGIS Pemetaan Kemiskinan & Masjid v2.1 + ============================================================ */ + +/* ── Toast ── */ +window.showToast = function (message, type) { + type = type || "info"; + var container = document.getElementById("toast-wrap"); + if (!container) { return; } + var toast = document.createElement("div"); + toast.className = "toast " + type; + var icon = type === "success" ? "" : type === "error" ? "" : ""; + toast.innerHTML = icon + " " + message; + container.appendChild(toast); + setTimeout(function () { + toast.classList.add("removing"); + setTimeout(function () { toast.remove(); }, 300); + }, 3500); +}; + +/* ── Label helpers ── */ +window.labelStatusKeluarga = { + aktif: ' Aktif', meninggal: ' Meninggal', pindah: ' Pindah', sudah_mampu: ' Sudah Mampu' +}; + +/* ── Detail Panel state ── */ +window._currentDetailItem = null; +window._currentDetailKemiskinanId = null; + +/* ── Open Detail Panel ── */ +window.openDetailPanel = function (item) { + window._currentDetailItem = item; + window._currentDetailKemiskinanId = item.id; + + document.getElementById('detail-panel-nama').textContent = item.nama_kepala_keluarga; + document.getElementById('detail-panel-alamat').textContent = item.alamat; + + var banner = document.getElementById('detail-status-banner'); + var status = item.status_keluarga || 'aktif'; + var statusLabels = { + aktif: ' Aktif – Membutuhkan Bantuan', + meninggal: ' Meninggal', + pindah: ' Pindah', + sudah_mampu: ' Sudah Mampu' + }; + banner.className = 'dp-status ' + status; + banner.innerHTML = statusLabels[status] || status; + + /* Actions bar */ + var actBar = document.getElementById('detail-actions-bar'); + actBar.innerHTML = ''; + + var btnLaporan = document.createElement('button'); + btnLaporan.className = 'btn btn-lapor'; + btnLaporan.innerHTML = ' Laporkan Kondisi'; + btnLaporan.onclick = function () { window.openLaporanPublikModal(item.id, item.nama_kepala_keluarga); }; + actBar.appendChild(btnLaporan); + + if (window.appAuth.role === 'pengurus_masjid') { + var btnEdit = document.createElement('button'); + btnEdit.className = 'btn btn-edit'; + btnEdit.innerHTML = ' Edit Data KK'; + btnEdit.onclick = function () { window.openEditKemiskinanModal(item); }; + actBar.appendChild(btnEdit); + } + + document.getElementById('detail-panel').classList.add('open'); + window.switchDetailTab('anggota'); +}; + +window.closeDetailPanel = function () { + document.getElementById('detail-panel').classList.remove('open'); + window._currentDetailItem = null; + window._currentDetailKemiskinanId = null; +}; + +window.switchDetailTab = function (tabName) { + ['anggota', 'bantuan', 'laporan-detail'].forEach(function (t) { + var btnId = 'tab-btn-' + t; + var contentId = 'detail-tab-' + t; + var btn = document.getElementById(btnId); + var content = document.getElementById(contentId); + if (!btn || !content) return; + var isActive = (t === tabName); + btn.classList.toggle('active', isActive); + content.classList.toggle('hidden', !isActive); + }); + + if (!window._currentDetailItem) return; + if (tabName === 'anggota') window.renderTabAnggota(window._currentDetailItem); + if (tabName === 'bantuan') window.renderTabBantuan(window._currentDetailItem); + if (tabName === 'laporan-detail') window.renderTabLaporanDetail(window._currentDetailItem); +}; + +/* Laporan per KK */ +window.renderTabLaporanDetail = async function (item) { + var container = document.getElementById('detail-tab-laporan-detail'); + if (!container) return; + container.innerHTML = '
Memuat laporan...
'; + + var data = await window.fetchJson('php/read_laporan.php?kemiskinan_id=' + item.id, []); + container.innerHTML = ''; + + var btnLapor = document.createElement('button'); + btnLapor.className = 'btn btn-add'; + btnLapor.innerHTML = '+ Laporkan Kondisi Baru'; + btnLapor.onclick = function () { window.openLaporanPublikModal(item.id, item.nama_kepala_keluarga); }; + container.appendChild(btnLapor); + + if (!data.length) { + var emptyMsg = document.createElement('div'); + emptyMsg.className = 'empty'; + emptyMsg.textContent = 'Belum ada laporan untuk keluarga ini.'; + container.appendChild(emptyMsg); + return; + } + + data.forEach(function (l) { + var card = document.createElement('div'); + card.className = 'l-card ' + l.status_laporan; + card.innerHTML = + '
' + + '' + (window.labelKategoriLaporan[l.kategori] || l.kategori) + '' + + '' + + (window.labelStatusLaporan[l.status_laporan] || l.status_laporan) + '' + + '
' + + '
' + window.escapeHtml(l.deskripsi) + '
' + + '
👤 ' + window.escapeHtml(l.nama_pelapor) + ' · 🕐 ' + l.created_fmt + '
'; + container.appendChild(card); + }); +}; + + +/* ── Modal: Kemiskinan ── */ +window.openKemiskinanModal = function (lat, lng) { + var latVal = Number(lat).toFixed(6); + var lngVal = Number(lng).toFixed(6); + document.getElementById("kem-lat").value = latVal; + document.getElementById("kem-lng").value = lngVal; + document.getElementById("kem-nama").value = ""; + document.getElementById("kem-jumlah").value = ""; + document.getElementById("kem-status").value = "aktif"; + + var alamatInput = document.getElementById("kem-alamat"); + alamatInput.value = ""; + alamatInput.placeholder = "Mencari alamat..."; + alamatInput.dataset.geoLat = latVal; + alamatInput.dataset.geoLng = lngVal; + + fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${latVal}&lon=${lngVal}&accept-language=id`) + .then(res => res.json()) + .then(data => { + if (alamatInput.dataset.geoLat === latVal && alamatInput.dataset.geoLng === lngVal) { + if (data && data.display_name) { + alamatInput.value = data.display_name; + } else { + alamatInput.placeholder = "Gagal memuat alamat. Silakan isi manual."; + } + } + }) + .catch(err => { + console.error("Reverse geocoding failed", err); + if (alamatInput.dataset.geoLat === latVal && alamatInput.dataset.geoLng === lngVal) { + alamatInput.placeholder = "Gagal memuat alamat. Silakan isi manual."; + } + }); + + var overlay = document.getElementById("modal-kemiskinan"); + overlay.style.display = "flex"; + document.getElementById("kem-nama").focus(); + overlay.onclick = function (e) { if (e.target === overlay) window.closeModal("kemiskinan"); }; +}; + +window._submitKemiskinan = async function (event) { + if (event && event.preventDefault) event.preventDefault(); + var btn = document.getElementById("btn-submit-kemiskinan"); + var nama = document.getElementById("kem-nama").value.trim(); + var jumlah = parseInt(document.getElementById("kem-jumlah").value, 10); + var alamat = document.getElementById("kem-alamat").value.trim(); + var status = document.getElementById("kem-status").value; + var lat = document.getElementById("kem-lat").value.trim(); + var lng = document.getElementById("kem-lng").value.trim(); + + if (!nama) { window.showToast("Nama kepala keluarga wajib diisi.", "error"); return; } + if (!Number.isInteger(jumlah) || jumlah < 1) { window.showToast("Jumlah KK harus minimal 1.", "error"); return; } + if (!lat || isNaN(Number(lat)) || Number(lat) < -90 || Number(lat) > 90) { + window.showToast("Latitude harus berupa angka antara -90 dan 90.", "error"); return; + } + if (!lng || isNaN(Number(lng)) || Number(lng) < -180 || Number(lng) > 180) { + window.showToast("Longitude harus berupa angka antara -180 dan 180.", "error"); return; + } + if (!alamat) { window.showToast("Alamat wajib diisi.", "error"); return; } + + btn.disabled = true; btn.textContent = "Menyimpan..."; + try { + await window.postForm("php/create_kemiskinan.php", { + nama_kepala_keluarga: nama, jumlah_kk: jumlah, alamat: alamat, + status_keluarga: status, + lat: Number(lat).toFixed(6), lng: Number(lng).toFixed(6) + }); + window.closeModal("kemiskinan"); + await window.loadAllData(); + window.showToast("Data berhasil disimpan.", "success"); + } catch (err) { + window.showToast("Gagal: " + err.message, "error"); + } finally { + btn.disabled = false; btn.textContent = "Simpan"; + } +}; + +/* ── Modal: Masjid ── */ +window.openMasjidModal = function (lat, lng) { + var latVal = Number(lat).toFixed(6); + var lngVal = Number(lng).toFixed(6); + document.getElementById("msj-lat").value = latVal; + document.getElementById("msj-lng").value = lngVal; + document.getElementById("msj-jenis").value = "masjid"; + document.getElementById("msj-nama").value = ""; + document.getElementById("msj-pic").value = ""; + document.getElementById("msj-radius").value = "500"; + + var alamatInput = document.getElementById("msj-alamat"); + alamatInput.value = ""; + alamatInput.placeholder = "Mencari alamat..."; + alamatInput.dataset.geoLat = latVal; + alamatInput.dataset.geoLng = lngVal; + + fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${latVal}&lon=${lngVal}&accept-language=id`) + .then(res => res.json()) + .then(data => { + if (alamatInput.dataset.geoLat === latVal && alamatInput.dataset.geoLng === lngVal) { + if (data && data.display_name) { + alamatInput.value = data.display_name; + } else { + alamatInput.placeholder = "Gagal memuat alamat. Silakan isi manual."; + } + } + }) + .catch(err => { + console.error("Reverse geocoding failed", err); + if (alamatInput.dataset.geoLat === latVal && alamatInput.dataset.geoLng === lngVal) { + alamatInput.placeholder = "Gagal memuat alamat. Silakan isi manual."; + } + }); + + var overlay = document.getElementById("modal-masjid"); + overlay.style.display = "flex"; + document.getElementById("msj-nama").focus(); + overlay.onclick = function (e) { if (e.target === overlay) window.closeModal("masjid"); }; +}; + +window._submitMasjid = async function (event) { + if (event && event.preventDefault) event.preventDefault(); + var btn = document.getElementById("btn-submit-masjid"); + var jenis = document.getElementById("msj-jenis").value; + var nama = document.getElementById("msj-nama").value.trim(); + var pic = document.getElementById("msj-pic").value.trim(); + var alamat = document.getElementById("msj-alamat").value.trim(); + var radius = parseInt(document.getElementById("msj-radius").value, 10); + var lat = document.getElementById("msj-lat").value.trim(); + var lng = document.getElementById("msj-lng").value.trim(); + + if (!nama) { window.showToast("Nama rumah ibadah wajib diisi.", "error"); return; } + if (!pic) { window.showToast("PIC wajib diisi.", "error"); return; } + if (!lat || isNaN(Number(lat)) || Number(lat) < -90 || Number(lat) > 90) { + window.showToast("Latitude harus berupa angka antara -90 dan 90.", "error"); return; + } + if (!lng || isNaN(Number(lng)) || Number(lng) < -180 || Number(lng) > 180) { + window.showToast("Longitude harus berupa angka antara -180 dan 180.", "error"); return; + } + if (!alamat) { window.showToast("Alamat wajib diisi.", "error"); return; } + if (!Number.isInteger(radius) || radius < 100 || radius > 5000) { + window.showToast("Radius harus antara 100–5000 m.", "error"); return; + } + btn.disabled = true; btn.textContent = "Menyimpan..."; + try { + await window.postForm("php/create_masjid.php", { + jenis_ibadah: jenis, nama_masjid: nama, pic_masjid: pic, alamat: alamat, + radius: radius, lat: Number(lat).toFixed(6), lng: Number(lng).toFixed(6) + }); + window.closeModal("masjid"); + await window.loadAllData(); + window.showToast("Rumah ibadah berhasil disimpan.", "success"); + } catch (err) { + window.showToast("Gagal: " + err.message, "error"); + } finally { + btn.disabled = false; btn.textContent = "Simpan"; + } +}; + +window.closeModal = function (type) { + var overlay = document.getElementById("modal-" + type); + if (overlay) overlay.style.display = "none"; +}; + +/* ── Modal: Edit Kemiskinan ── */ +window.openEditKemiskinanModal = function (item) { + document.getElementById("edit-kem-id").value = item.id; + document.getElementById("edit-kem-nama").value = item.nama_kepala_keluarga; + document.getElementById("edit-kem-jumlah").value = item.jumlah_kk; + document.getElementById("edit-kem-alamat").value = item.alamat; + document.getElementById("edit-kem-status").value = item.status_keluarga || 'aktif'; + + var overlay = document.getElementById("modal-edit-kemiskinan"); + overlay.style.display = "flex"; + document.getElementById("edit-kem-nama").focus(); + overlay.onclick = function (e) { if (e.target === overlay) window.closeModal("edit-kemiskinan"); }; +}; + +window._submitEditKemiskinan = async function (event) { + if (event && event.preventDefault) event.preventDefault(); + var btn = document.getElementById("btn-submit-edit-kemiskinan"); + var id = document.getElementById("edit-kem-id").value; + var nama = document.getElementById("edit-kem-nama").value.trim(); + var jumlah = parseInt(document.getElementById("edit-kem-jumlah").value, 10); + var alamat = document.getElementById("edit-kem-alamat").value.trim(); + var status = document.getElementById("edit-kem-status").value; + + if (!nama) { window.showToast("Nama wajib diisi.", "error"); return; } + if (!Number.isInteger(jumlah) || jumlah < 1) { window.showToast("Jumlah KK minimal 1.", "error"); return; } + if (!alamat) { window.showToast("Alamat wajib diisi.", "error"); return; } + + btn.disabled = true; btn.textContent = "Menyimpan..."; + try { + await window.postForm("php/update_kemiskinan.php", { + id, nama_kepala_keluarga: nama, jumlah_kk: jumlah, alamat, status_keluarga: status + }); + window.closeModal("edit-kemiskinan"); + await window.loadAllData(); + window.showToast("Data berhasil diperbarui.", "success"); + if (window._currentDetailKemiskinanId === parseInt(id)) { + var updatedItem = Object.assign({}, window._currentDetailItem, { nama_kepala_keluarga: nama, jumlah_kk: jumlah, alamat, status_keluarga: status }); + window.openDetailPanel(updatedItem); + } + } catch (err) { + window.showToast("Gagal: " + err.message, "error"); + } finally { + btn.disabled = false; btn.textContent = "Simpan Perubahan"; + } +}; + +/* ── Modal: Edit Rumah Ibadah ── */ +window.openEditMasjidModal = function (item) { + document.getElementById("edit-msj-id").value = item.id; + document.getElementById("edit-msj-jenis").value = item.jenis_ibadah || 'masjid'; + document.getElementById("edit-msj-nama").value = item.nama_masjid; + document.getElementById("edit-msj-pic").value = item.pic_masjid; + document.getElementById("edit-msj-alamat").value = item.alamat; + + var overlay = document.getElementById("modal-edit-masjid"); + overlay.style.display = "flex"; + document.getElementById("edit-msj-nama").focus(); + overlay.onclick = function (e) { if (e.target === overlay) window.closeModal("edit-masjid"); }; +}; + +window._submitEditMasjid = async function (event) { + if (event && event.preventDefault) event.preventDefault(); + var btn = document.getElementById("btn-submit-edit-masjid"); + var id = document.getElementById("edit-msj-id").value; + var jenis = document.getElementById("edit-msj-jenis").value; + var nama = document.getElementById("edit-msj-nama").value.trim(); + var pic = document.getElementById("edit-msj-pic").value.trim(); + var alamat = document.getElementById("edit-msj-alamat").value.trim(); + + if (!nama) { window.showToast("Nama rumah ibadah wajib diisi.", "error"); return; } + if (!pic) { window.showToast("PIC wajib diisi.", "error"); return; } + if (!alamat) { window.showToast("Alamat wajib diisi.", "error"); return; } + + btn.disabled = true; btn.textContent = "Menyimpan..."; + try { + await window.postForm("php/update_masjid.php", { id, jenis_ibadah: jenis, nama_masjid: nama, pic_masjid: pic, alamat }); + window.closeModal("edit-masjid"); + await window.loadAllData(); + window.showToast("Data rumah ibadah diperbarui.", "success"); + } catch (err) { + window.showToast("Gagal: " + err.message, "error"); + } finally { + btn.disabled = false; btn.textContent = "Simpan Perubahan"; + } +}; + +/* ── Fetch helper ── */ +window.fetchJson = async function (url, fallback) { + var response = await fetch(url, { cache: "no-store" }); + var text = await response.text(); + if (!response.ok) { console.error("HTTP " + response.status, url); return fallback !== undefined ? fallback : []; } + if (!text.trim()) { return fallback !== undefined ? fallback : []; } + try { return JSON.parse(text); } catch (e) { console.error("JSON parse:", e); return fallback !== undefined ? fallback : []; } +}; + +/* ── Delete ── */ +window.deleteRecord = async function (url, id, label) { + if (window.appState.mode !== "edit") { window.showToast("Aktifkan Mode Edit untuk menghapus.", "error"); return; } + if (!confirm("Hapus data " + label + "? Tidak dapat dibatalkan.")) return; + try { + await window.postForm(url, { id }); + await window.loadAllData(); + window.showToast("Data dihapus.", "success"); + } catch (err) { window.showToast("Gagal: " + err.message, "error"); } +}; + +/* ── Update radius ── */ +window.updateMasjidRadiusFromCard = async function (masjidId, inputEl, circle, btnEl) { + if (window.appState.mode !== "edit") { window.showToast("Aktifkan Mode Edit.", "error"); return; } + var radius = parseInt(inputEl.value, 10); + if (!Number.isInteger(radius) || radius < 100 || radius > 5000) { + window.showToast("Radius harus 100–5000 m.", "error"); return; + } + btnEl.disabled = true; btnEl.textContent = "..."; + try { + await window.postForm("php/update_masjid_radius.php", { id: masjidId, radius }); + if (circle) circle.setRadius(radius); + window.showToast("Radius diperbarui: " + radius + " m.", "success"); + await window.loadAllData(); + } catch (err) { + window.showToast("Gagal: " + err.message, "error"); + btnEl.disabled = false; btnEl.textContent = "Simpan"; + } +}; + +/* ── Layer filter section ── */ +window.createLayerFilterSection = function () { + var section = document.createElement("div"); + section.className = "sec"; + + var hd = document.createElement("div"); + hd.className = "sec-hd"; + hd.innerHTML = 'Filter Layer'; + section.appendChild(hd); + + var filters = [ + { key: "kemiskinan", label: "Keluarga Prasejahtera", color: "#ef4444" }, + { key: "masjid", label: "Masjid / Rumah Ibadah", color: "#3b82f6" } + ]; + filters.forEach(function (f) { + var row = document.createElement("label"); + row.className = "filter-row"; + var cb = document.createElement("input"); + cb.type = "checkbox"; cb.dataset.layerKey = f.key; + cb.checked = !!window.appState.layerVisibility[f.key]; + cb.onchange = function () { window.setLayerVisibility(f.key, cb.checked); }; + var dot = document.createElement("span"); + dot.className = "filter-dot"; dot.style.background = f.color; + var txt = document.createElement("span"); txt.textContent = f.label; + row.appendChild(cb); row.appendChild(dot); row.appendChild(txt); + section.appendChild(row); + }); + return section; +}; + +window.refreshLayerFilterUI = function () { + document.querySelectorAll("input[data-layer-key]").forEach(function (cb) { + cb.checked = !!window.appState.layerVisibility[cb.dataset.layerKey]; + }); +}; + +/* ── loadAllData ── */ +window.loadAllData = async function () { + var sidebarBody = document.getElementById("sidebar-body"); + var sidebarSub = document.getElementById("sidebar-sub"); + if (!sidebarBody || !window.map) return; + + sidebarBody.innerHTML = '
Memuat data...
'; + if (sidebarSub) sidebarSub.textContent = 'Memuat...'; + + if (window.appState.layers.kemiskinan) window.appState.layers.kemiskinan.clearLayers(); + if (window.appState.layers.masjid) window.appState.layers.masjid.clearLayers(); + + try { + var results = await Promise.all([ + window.fetchJson("php/read_kemiskinan.php", []), + window.fetchJson("php/read_masjid.php", []) + ]); + var dataKem = results[0]; + var dataMsj = results[1]; + + var totalKeluarga = 0; + var perluBantuan = 0; + var countMasjid = 0, countGereja = 0, countVihara = 0; + var totalJiwa = 0; + var keluargaMentas = 0; + var heatData = []; + + if (dataMsj && Array.isArray(dataMsj)) { + dataMsj.forEach(function(m) { + var j = (m.jenis_ibadah || 'masjid').toLowerCase(); + if(j === 'masjid') countMasjid++; + else if(j === 'gereja') countGereja++; + else if(j === 'vihara') countVihara++; + }); + } + + if (dataKem && Array.isArray(dataKem)) { + dataKem.forEach(function(item) { + if (!window.isValidCoordinate(item.latitude, item.longitude)) return; + totalKeluarga++; + + // Add jiwa count + totalJiwa += parseInt(item.jumlah_kk) || 0; + + var kemLL = L.latLng(item.latitude, item.longitude); + var status = item.status_keluarga || 'aktif'; + + if (status === 'sudah_mampu') { + keluargaMentas++; + } + + var isIn = false; + if (dataMsj && dataMsj.length) { + for (var i = 0; i < dataMsj.length; i++) { + var m = dataMsj[i], r = Number(m.radius); + if (window.isValidCoordinate(m.latitude, m.longitude) && window.isValidRadius(r)) { + if (window.map.distance(kemLL, L.latLng(m.latitude, m.longitude)) <= r) { + isIn = true; break; + } + } + } + } + if (!isIn && status === 'aktif') { + perluBantuan++; + var lat = parseFloat(item.latitude); + var lng = parseFloat(item.longitude); + if (!isNaN(lat) && !isNaN(lng)) { + heatData.push([lat, lng, 100]); // Use higher intensity + } + } + }); + } + + // Render Heatmap + if (window.appState.layers.heatmap) { + window.appState.layers.heatmap.clearLayers(); + if (heatData.length > 0 && typeof L.heatLayer === 'function') { + var heat = L.heatLayer(heatData, { + radius: 35, + blur: 25, + maxZoom: 15, + max: 100, + gradient: {0.4: 'blue', 0.6: 'lime', 0.8: 'yellow', 1.0: 'red'} + }); + heat.addTo(window.appState.layers.heatmap); + } + } + + var statTotalEl = document.getElementById('stat-total-keluarga'); + var statPerluEl = document.getElementById('stat-perlu-bantuan'); + var statJiwaEl = document.getElementById('stat-total-jiwa'); + var statMentasEl = document.getElementById('stat-keluarga-mentas'); + var statProgText = document.getElementById('stat-progress-text'); + var statProgFill = document.getElementById('stat-progress-fill'); + + if (statTotalEl) statTotalEl.textContent = totalKeluarga; + if (statPerluEl) statPerluEl.textContent = perluBantuan; + if (statJiwaEl) statJiwaEl.textContent = totalJiwa; + if (statMentasEl) statMentasEl.textContent = keluargaMentas; + + if (statProgText && statProgFill) { + var pct = 0; + if (totalKeluarga > 0) { + var terjangkau = totalKeluarga - perluBantuan; + pct = Math.round((terjangkau / totalKeluarga) * 100); + } + statProgText.textContent = pct + '%'; + statProgFill.style.width = pct + '%'; + } + + var sm = document.getElementById('stat-masjid'); + var sg = document.getElementById('stat-gereja'); + var sv = document.getElementById('stat-vihara'); + if(sm) sm.textContent = countMasjid; + if(sg) sg.textContent = countGereja; + if(sv) sv.textContent = countVihara; + + if (sidebarSub) { + var hint = window.appState.mode === "edit" + ? "Mode Edit — klik peta untuk tambah data" + : "Mode Lihat — klik Detail untuk info lengkap"; + sidebarSub.textContent = hint; + } + + sidebarBody.innerHTML = ''; + + var divider = document.createElement('div'); + divider.className = 'sec-divider'; + sidebarBody.appendChild(divider); + + var secKem = window.renderKemiskinan(dataKem, dataMsj); + secKem.id = 'sec-kemiskinan'; + var secMsj = window.renderMasjid(dataMsj); + secMsj.id = 'sec-masjid'; + + sidebarBody.appendChild(secKem); + sidebarBody.appendChild(secMsj); + + if (typeof window.filterSidebar === 'function') { + window.filterSidebar('semua'); + } + + } catch (err) { + sidebarBody.innerHTML = '
Terjadi kendala saat membaca data.
' + window.escapeHtml(err.message) + '
'; + console.error(err); + } +}; + +/* ── Render: Kemiskinan ── */ +window.renderKemiskinan = function (dataKem, dataMsj) { + var sec = document.createElement('div'); + sec.className = 'sec'; + + var hd = document.createElement('div'); + hd.className = 'sec-hd'; + hd.innerHTML = 'Keluarga Prasejahtera' + + '' + dataKem.length + ''; + sec.appendChild(hd); + + if (!dataKem.length) { + var empty = document.createElement('div'); + empty.className = 'empty'; + empty.textContent = 'Belum ada data. Aktifkan Mode Edit dan klik peta.'; + sec.appendChild(empty); + return sec; + } + + dataKem.forEach(function (item) { + if (!window.isValidCoordinate(item.latitude, item.longitude)) return; + + var kemLL = L.latLng(item.latitude, item.longitude); + var status = item.status_keluarga || 'aktif'; + var isIn = false; + + if (dataMsj && dataMsj.length) { + for (var i = 0; i < dataMsj.length; i++) { + var m = dataMsj[i], r = Number(m.radius); + if (window.isValidCoordinate(m.latitude, m.longitude) && window.isValidRadius(r)) { + if (window.map.distance(kemLL, L.latLng(m.latitude, m.longitude)) <= r) { + isIn = true; break; + } + } + } + } + + var icon = isIn ? window.greenIcon : window.redIcon; + if (status !== 'aktif') icon = window.orangeIcon; + + var marker = L.marker([item.latitude, item.longitude], { icon, searchName: (item.nama_kepala_keluarga || '').toLowerCase() }) + .addTo(window.appState.layers.kemiskinan) + .bindPopup( + '' + window.escapeHtml(item.nama_kepala_keluarga) + '
' + + 'Status: ' + (window.labelStatusKeluarga[status] || status) + '
' + + 'Anggota: ' + item.jumlah_kk + ' jiwa
' + + window.escapeHtml(item.alamat) + ); + + marker.on('click', function(e) { + if (window.appState && window.appState.isRouting) { + window.handleRoutingClick(item.latitude, item.longitude, item.nama_kepala_keluarga); + marker.closePopup(); + } + }); + + var card = document.createElement('div'); + card.dataset.searchName = (item.nama_kepala_keluarga || '').toLowerCase(); + card.className = 'card' + + (isIn ? ' in-radius' : ' out-radius') + + (status === 'meninggal' ? ' st-meninggal' : '') + + (status === 'pindah' ? ' st-pindah' : '') + + (status === 'sudah_mampu' ? ' st-mampu' : ''); + + var chipClass = isIn ? 'chip-green' : 'chip-red'; + if (status === 'meninggal') chipClass = 'chip-gray'; + if (status === 'pindah') chipClass = 'chip-amber'; + if (status === 'sudah_mampu') chipClass = 'chip-green'; + + var chipText = isIn ? 'Terjangkau' : 'Luar Radius'; + if (status !== 'aktif') chipText = window.labelStatusKeluarga[status] || status; + + var updTs = item.updated_at_fmt ? '
' + item.updated_at_fmt + '
' : ''; + + card.innerHTML = + '
' + window.escapeHtml(item.nama_kepala_keluarga) + '
' + + '
' + item.jumlah_kk + ' anggota keluarga
' + + '
' + window.escapeHtml(item.alamat) + '
' + + updTs; + + var foot = document.createElement('div'); + foot.className = 'card-foot'; + + var chip = document.createElement('span'); + chip.className = 'chip ' + chipClass; + chip.innerHTML = chipText; + foot.appendChild(chip); + + var btns = document.createElement('div'); + btns.className = 'card-btns'; + + var btnF = document.createElement('button'); + btnF.className = 'btn btn-focus'; btnF.innerHTML = ''; + btnF.title = 'Fokus ke peta'; + btnF.onclick = (function(it, mk){ return function(e){ + e.stopPropagation(); + window.map.setView([it.latitude, it.longitude], 17); + mk.openPopup(); + }; })(item, marker); + btns.appendChild(btnF); + + var btnD = document.createElement('button'); + btnD.className = 'btn btn-detail'; btnD.innerHTML = ' Detail'; + btnD.onclick = (function(it){ return function(e){ + e.stopPropagation(); window.openDetailPanel(it); + }; })(item); + btns.appendChild(btnD); + + var btnL = document.createElement('button'); + btnL.className = 'btn btn-lapor'; btnL.innerHTML = ''; + btnL.title = 'Laporkan kondisi'; + btnL.onclick = (function(it){ return function(e){ + e.stopPropagation(); window.openLaporanPublikModal(it.id, it.nama_kepala_keluarga); + }; })(item); + btns.appendChild(btnL); + + if (window.appAuth && window.appAuth.role === 'pengurus_masjid') { + var btnE = document.createElement('button'); + btnE.className = 'btn btn-edit'; btnE.innerHTML = ''; + btnE.title = 'Edit data'; + btnE.disabled = window.appState.mode !== 'edit'; + btnE.onclick = (function(it){ return function(e){ + e.stopPropagation(); window.openEditKemiskinanModal(it); + }; })(item); + btns.appendChild(btnE); + + var btnH = document.createElement('button'); + btnH.className = 'btn btn-delete'; btnH.innerHTML = ''; + btnH.title = 'Hapus data'; + btnH.disabled = window.appState.mode !== 'edit'; + btnH.onclick = (function(it){ return function(e){ + e.stopPropagation(); window.deleteRecord('php/delete_kemiskinan.php', it.id, 'Kemiskinan'); + }; })(item); + btns.appendChild(btnH); + } + + foot.appendChild(btns); + card.appendChild(foot); + sec.appendChild(card); + }); + + return sec; +}; + +/* ── Jenis ibadah helpers ── */ +window._jenisIbadahEmoji = { + masjid: '', gereja: '', vihara: '', pura: '', + klenteng: '', kapel: '', lainnya: '' +}; +window._jenisIbadahLabel = { + masjid: 'Masjid', gereja: 'Gereja', vihara: 'Vihara', pura: 'Pura', + klenteng: 'Klenteng', kapel: 'Kapel', lainnya: 'Rumah Ibadah' +}; +window._jenisIbadahColor = { + masjid: '#1d4ed8', gereja: '#7c3aed', vihara: '#b45309', + pura: '#b91c1c', klenteng: '#b45309', kapel: '#4338ca', lainnya: '#6b7280' +}; +window._jenisIbadahFill = { + masjid: '#93c5fd', gereja: '#c4b5fd', vihara: '#fcd34d', + pura: '#fca5a5', klenteng: '#fcd34d', kapel: '#a5b4fc', lainnya: '#d1d5db' +}; + +/* ── Render: Rumah Ibadah ── */ +window.renderMasjid = function (dataMsj) { + var sec = document.createElement('div'); + sec.className = 'sec'; + + var hd = document.createElement('div'); + hd.className = 'sec-hd'; + hd.innerHTML = 'Rumah Ibadah' + + '' + dataMsj.length + ''; + sec.appendChild(hd); + + if (!dataMsj.length) { + var empty = document.createElement('div'); + empty.className = 'empty'; + empty.textContent = 'Belum ada data. Aktifkan Mode Edit dan klik peta.'; + sec.appendChild(empty); + return sec; + } + + dataMsj.forEach(function (item) { + var radius = Number(item.radius); + if (!window.isValidCoordinate(item.latitude, item.longitude) || !window.isValidRadius(radius)) return; + + var jenis = item.jenis_ibadah || 'masjid'; + var emoji = window._jenisIbadahEmoji[jenis] || ''; + var label = window._jenisIbadahLabel[jenis] || 'Rumah Ibadah'; + var color = window._jenisIbadahColor[jenis] || '#6b7280'; + var fill = window._jenisIbadahFill[jenis] || '#d1d5db'; + + /* Ikon pin warna sesuai jenis */ + var pinIcon = window.createPinIcon(color, + '&#' + + ({ masjid: 128332, gereja: 9962, vihara: 127983, pura: 128469, klenteng: 127982, kapel: 9769, lainnya: 127963 }[jenis] || 127963) + + ';' + ); + + var popup = '' + emoji + ' ' + window.escapeHtml(item.nama_masjid) + '
' + + 'Jenis: ' + label + '
' + + 'PIC: ' + window.escapeHtml(item.pic_masjid) + '
' + + 'Radius: ' + window.formatMeters(radius); + + var circle = L.circle([item.latitude, item.longitude], { + radius, color, fillColor: fill, + fillOpacity: 0.12, weight: 1.5, + interactive: false /* ← jangan tangkap klik peta, biarkan map.on('click') jalan */ + }).addTo(window.appState.layers.masjid); + + var marker = L.marker([item.latitude, item.longitude], { + icon: pinIcon, + searchName: (item.nama_masjid || '').toLowerCase(), + type: 'masjid' + }) + .addTo(window.appState.layers.masjid).bindPopup(popup); + + marker.on('click', function(e) { + if (window.appState && window.appState.isRouting) { + window.handleRoutingClick(item.latitude, item.longitude, item.nama_masjid); + marker.closePopup(); + } + }); + + var card = document.createElement('div'); + card.dataset.searchName = (item.nama_masjid || '').toLowerCase(); + card.dataset.type = 'masjid'; + card.className = 'card masjid-card'; + + card.innerHTML = + '
' + emoji + ' ' + window.escapeHtml(item.nama_masjid) + '
' + + '
PIC: ' + window.escapeHtml(item.pic_masjid) + '
' + + '
' + window.escapeHtml(item.alamat) + '
'; + + /* Radius row */ + var canEdit = window.appAuth && window.appAuth.role === 'pengurus_masjid'; + var rr = document.createElement('div'); + rr.className = 'radius-row'; + rr.innerHTML = ''; + var ri = document.createElement('input'); + ri.type = 'number'; ri.min = '100'; ri.max = '5000'; + ri.value = Math.round(radius); + ri.disabled = !canEdit || window.appState.mode !== 'edit'; + var ru = document.createElement('span'); + ru.className = 'radius-unit'; ru.textContent = 'm'; + var rb = document.createElement('button'); + rb.className = 'btn btn-update'; rb.textContent = 'Simpan'; + rb.disabled = !canEdit || window.appState.mode !== 'edit'; + rb.onclick = (function(mid, rinput, ci, rbt){ return function(e){ + e.stopPropagation(); window.updateMasjidRadiusFromCard(mid, rinput, ci, rbt); + }; })(item.id, ri, circle, rb); + rr.appendChild(ri); rr.appendChild(ru); rr.appendChild(rb); + card.appendChild(rr); + + var foot = document.createElement('div'); + foot.className = 'card-foot'; + + var chip = document.createElement('span'); + chip.className = 'chip chip-blue'; + chip.innerHTML = emoji + ' ' + label; + chip.style.background = fill; + chip.style.color = color; + chip.style.border = '1px solid ' + color + '40'; + foot.appendChild(chip); + + var btns = document.createElement('div'); + btns.className = 'card-btns'; + + var btnF = document.createElement('button'); + btnF.className = 'btn btn-focus'; btnF.innerHTML = ''; + btnF.title = 'Fokus ke peta'; + btnF.onclick = (function(ci, mk){ return function(e){ + e.stopPropagation(); window.map.fitBounds(ci.getBounds(), { padding:[40,40] }); mk.openPopup(); + }; })(circle, marker); + btns.appendChild(btnF); + + if (canEdit) { + var btnE = document.createElement('button'); + btnE.className = 'btn btn-edit'; btnE.innerHTML = ''; + btnE.title = 'Edit'; + btnE.disabled = window.appState.mode !== 'edit'; + btnE.onclick = (function(it){ return function(e){ e.stopPropagation(); window.openEditMasjidModal(it); }; })(item); + btns.appendChild(btnE); + + var btnH = document.createElement('button'); + btnH.className = 'btn btn-delete'; btnH.innerHTML = ''; + btnH.title = 'Hapus'; + btnH.disabled = window.appState.mode !== 'edit'; + btnH.onclick = (function(it){ return function(e){ e.stopPropagation(); window.deleteRecord('php/delete_masjid.php', it.id, label); }; })(item); + btns.appendChild(btnH); + } + + foot.appendChild(btns); + card.appendChild(foot); + sec.appendChild(card); + }); + + return sec; +}; + +/* ── Search Map Data ── */ +window.searchMapData = function(query) { + var q = (query || '').toLowerCase().trim(); + + // Filter Kemiskinan + if (window.appState && window.appState.layers && window.appState.layers.kemiskinan) { + window.appState.layers.kemiskinan.eachLayer(function(layer) { + if(layer instanceof L.Marker && layer.options.searchName !== undefined) { + if(q === '' || layer.options.searchName.includes(q)) { + layer.setOpacity(1); + } else { + layer.setOpacity(0.15); + } + } + }); + } + + // Filter Masjid + if (window.appState && window.appState.layers && window.appState.layers.masjid) { + window.appState.layers.masjid.eachLayer(function(layer) { + if(layer instanceof L.Marker && layer.options.searchName !== undefined) { + if(q === '' || layer.options.searchName.includes(q)) { + layer.setOpacity(1); + } else { + layer.setOpacity(0.15); + } + } + }); + } + + // Filter Cards in Sidebar + var sidebarBody = document.getElementById('sidebar-body'); + if (sidebarBody) { + var cards = sidebarBody.getElementsByClassName('card'); + for (var i = 0; i < cards.length; i++) { + var cName = cards[i].dataset.searchName || ''; + if (q === '' || cName.includes(q)) { + cards[i].style.display = 'block'; + } else { + cards[i].style.display = 'none'; + } + } + } +}; + +/* ── Extra Filters ── */ +window.toggleFilterPerluBantuan = function(isActive) { + if (window.appState && window.appState.layers && window.appState.layers.kemiskinan) { + window.appState.layers.kemiskinan.eachLayer(function(layer) { + if (layer instanceof L.Marker) { + // If filter is active, only show markers that are NOT 'in-radius' (which use the greenIcon or have a specific property). + // Wait, how do we know if it's in-radius? In loadData we assigned icon: window.greenIcon or window.redIcon. + // An easy way is to check the icon URL or just rely on the DOM cards. But markers are drawn. + // Let's check the layer.options.icon.options.iconUrl. + var isRed = layer.options.icon && layer.options.icon.options && layer.options.icon.options.iconUrl && layer.options.icon.options.iconUrl.includes('red'); + if (isActive) { + if (isRed) layer.setOpacity(1); + else layer.setOpacity(0); // Hide completely + } else { + layer.setOpacity(1); // Restore + } + } + }); + } + + // Filter Cards in Sidebar + var sidebarBody = document.getElementById('sidebar-body'); + if (sidebarBody) { + var ObjectCards = sidebarBody.getElementsByClassName('card'); + for (var i = 0; i < ObjectCards.length; i++) { + var c = ObjectCards[i]; + // The card has class 'out-radius' if it's red + if (isActive) { + if (c.classList.contains('out-radius')) { + c.style.display = 'block'; + } else { + // Don't hide masjid cards! Check if it's kemiskinan + if (!c.classList.contains('masjid-card')) { + c.style.display = 'none'; + } + } + } else { + c.style.display = 'block'; + } + } + } +}; + +/* ── Export Data ── */ +window.exportToCSV = async function() { + Swal.fire({ + title: 'Menyiapkan Laporan...', + text: 'Mohon tunggu, sistem sedang merangkum data dari database.', + allowOutsideClick: false, + didOpen: () => { Swal.showLoading(); } + }); + + try { + var response = await fetch('php/export_csv.php'); + if (!response.ok) throw new Error('Gagal menghubungi server.'); + + var blob = await response.blob(); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = "Data_Keluarga_Prasejahtera_GeoPoverty.csv"; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + + Swal.fire({ + icon: 'success', + title: 'Unduhan Berhasil!', + text: 'File laporan CSV telah tersimpan di komputermu.', + timer: 2500, + showConfirmButton: false + }); + } catch (e) { + Swal.fire({ + icon: 'error', + title: 'Ekspor Gagal', + text: 'Terjadi kesalahan: ' + e.message + }); + } +}; + +/* ── Export Map PDF ── */ +window.downloadMapPDF = async function() { + Swal.fire({ + title: 'Mencetak Peta...', + text: 'Sistem sedang mengambil tangkapan layar peta. Mohon tunggu...', + allowOutsideClick: false, + didOpen: () => { Swal.showLoading(); } + }); + + try { + var mapElement = document.getElementById('map'); + + // Hide map controls (zoom, layers, dll) to make it clean + var controls = document.querySelectorAll('.leaflet-control-container'); + controls.forEach(c => c.style.display = 'none'); + + // Capture map as JPEG + var rect = mapElement.getBoundingClientRect(); + var imgData = await domtoimage.toJpeg(mapElement, { + quality: 0.95, + bgcolor: '#e2e8f0', + width: rect.width, + height: rect.height, + style: { + // Ensure no unexpected offset during render + transform: 'none', + bottom: '0', + right: '0' + } + }); + + // Restore controls + controls.forEach(c => c.style.display = ''); + + // Setup jsPDF (Landscape A4) + var pdf = new jspdf.jsPDF('l', 'mm', 'a4'); + var pdfWidth = pdf.internal.pageSize.getWidth(); + var pdfHeight = pdf.internal.pageSize.getHeight(); + + var imgProps = pdf.getImageProperties(imgData); + var ratio = imgProps.width / imgProps.height; + + var renderWidth = pdfWidth; + var renderHeight = pdfWidth / ratio; + + if (renderHeight > pdfHeight) { + renderHeight = pdfHeight; + renderWidth = pdfHeight * ratio; + } + + // Center image + var x = (pdfWidth - renderWidth) / 2; + var y = (pdfHeight - renderHeight) / 2; + + pdf.addImage(imgData, 'JPEG', x, y, renderWidth, renderHeight); + pdf.save('Peta_GeoPoverty.pdf'); + + Swal.fire({ + icon: 'success', + title: 'Berhasil!', + text: 'Peta murni telah berhasil diunduh sebagai PDF.', + timer: 2500, + showConfirmButton: false + }); + } catch(e) { + var controls = document.querySelectorAll('.leaflet-control-container'); + controls.forEach(c => c.style.display = ''); + + Swal.fire({ + icon: 'error', + title: 'Gagal Mencetak', + text: 'Terjadi kesalahan saat mengekspor peta: ' + e.message + }); + } +}; diff --git a/js/map.js b/js/map.js new file mode 100644 index 0000000..9285ea1 --- /dev/null +++ b/js/map.js @@ -0,0 +1,377 @@ +/* ============================================================ + map.js – WebGIS Pemetaan Kemiskinan & Masjid (SRS v1.0) + ============================================================ */ + +window.appState = { + mode: "view", + activeType: "kemiskinan", + layers: {}, + layerVisibility: { + kemiskinan: true, + masjid: true + }, + isHeatmap: false, + isRouting: false, + routingWaypoints: [] +}; + +/* ── Utility: escape HTML ── */ +window.escapeHtml = function (value) { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +}; + +/* ── Utility: SVG pin marker ── */ +window.createPinIcon = function (bgColor, svgSymbol) { + var svg = [ + '', + '', + '', + '', + '', + '', + /* Pin body */ + '', + /* White inner circle */ + '', + /* Symbol */ + svgSymbol, + '' + ].join(''); + return L.divIcon({ + className: 'custom-pin-icon', + html: svg, + iconSize: [32, 42], + iconAnchor: [16, 42], + popupAnchor: [0, -44] + }); +}; + +/* Kemiskinan – merah (tidak terjangkau) */ +window.redIcon = window.createPinIcon('#dc2626', + '👪' +); +/* Kemiskinan – hijau (terjangkau) */ +window.greenIcon = window.createPinIcon('#16a34a', + '👪' +); +/* Masjid – biru */ +window.blueIcon = window.createPinIcon('#1d4ed8', + '🕌' +); +/* Fallback orange (tidak digunakan, tapi tetap didefinisikan) */ +window.orangeIcon = window.createPinIcon('#f97316', + '👪' +); + +/* ── Utility: POST form-encoded ── */ +window.postForm = async function (url, payload) { + var response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams(payload) + }); + var text = (await response.text()).trim(); + if (!response.ok || text !== "success") { + throw new Error(text || "Permintaan gagal."); + } +}; + +/* ── Navbar hint ── */ +window.updateMapHint = function () { + var hint = document.getElementById("mapHint"); + if (!hint) { return; } + + if (appState.mode === "view") { + hint.textContent = "Mode Lihat aktif — data keluarga prasejahtera & rumah ibadah ditampilkan. Klik kartu untuk navigasi peta."; + return; + } + if (appState.activeType === "kemiskinan") { + hint.textContent = "Mode Edit aktif — klik titik di peta untuk menambahkan data keluarga prasejahtera."; + return; + } + hint.textContent = "Mode Edit aktif — klik titik di peta untuk menambahkan data rumah ibadah baru."; +}; + +/* ── Toolbar state ── */ +window.updateToolbarState = function () { + var states = { + modeViewBtn: appState.mode === "view", + modeEditBtn: appState.mode === "edit", + typeKemiskinanBtn: appState.activeType === "kemiskinan", + typeMasjidBtn: appState.activeType === "masjid" + }; + Object.keys(states).forEach(function (id) { + var btn = document.getElementById(id); + if (!btn) { return; } + btn.classList.toggle("active", states[id]); + /* Mode buttons get a different active colour */ + if (id === "modeEditBtn" && states[id]) { + btn.classList.add("mode-active"); + } else { + btn.classList.remove("mode-active"); + } + }); + + var viewBtn = document.getElementById("luminaModeView"); + var editBtn = document.getElementById("luminaModeEdit"); + if (viewBtn && editBtn) { + viewBtn.style.opacity = appState.mode === "view" ? "1" : "0.5"; + viewBtn.style.boxShadow = appState.mode === "view" ? "0 4px 12px rgba(99,102,241,0.25)" : "none"; + + editBtn.style.opacity = appState.mode === "edit" ? "1" : "0.5"; + editBtn.style.boxShadow = appState.mode === "edit" ? "0 4px 12px rgba(20,184,166,0.25)" : "none"; + } +}; + +window.setActiveMenu = function(element) { + var items = document.querySelectorAll('.lumina-menu-item'); + items.forEach(function(item) { + item.classList.remove('active'); + }); + element.classList.add('active'); +}; + +/* ── Layer visibility ── */ +window.setLayerVisibility = function (key, isVisible) { + appState.layerVisibility[key] = !!isVisible; + var layer = appState.layers[key]; + if (!layer || !window.map) { return; } + if (isVisible) { + if (!map.hasLayer(layer)) { map.addLayer(layer); } + } else { + if (map.hasLayer(layer)) { map.removeLayer(layer); } + } +}; + +/* ── Mode & type switches ── */ +window.setMode = function (mode) { + appState.mode = mode; + window.updateToolbarState(); + window.updateMapHint(); + + var mapEl = document.getElementById("map"); + if (mapEl) { + if (mode === "edit") { + mapEl.classList.add("edit-mode"); + } else { + mapEl.classList.remove("edit-mode"); + } + } + + if (typeof window.loadAllData === "function") { + window.loadAllData(); + } +}; + +window.setDataType = function (type) { + appState.activeType = type; + window.updateToolbarState(); + window.updateMapHint(); + if (typeof window.filterSidebar === 'function') { + window.filterSidebar(type); + } +}; + +window.filterSidebar = function(type) { + var secKem = document.getElementById('sec-kemiskinan'); + var secMsj = document.getElementById('sec-masjid'); + + if (secKem) secKem.style.display = (type === 'semua' || type === 'kemiskinan') ? 'block' : 'none'; + if (secMsj) secMsj.style.display = (type === 'semua' || type === 'masjid') ? 'block' : 'none'; + + var btnSemua = document.getElementById('filter-btn-semua'); + var btnKem = document.getElementById('filter-btn-kemiskinan'); + var btnMsj = document.getElementById('filter-btn-masjid'); + + if (btnSemua) btnSemua.classList.toggle('active', type === 'semua'); + if (btnKem) btnKem.classList.toggle('active', type === 'kemiskinan'); + if (btnMsj) btnMsj.classList.toggle('active', type === 'masjid'); + + var sidebarSub = document.getElementById('sidebar-sub'); + if (sidebarSub) { + if (type === 'semua') sidebarSub.textContent = 'Menampilkan semua data'; + else if (type === 'kemiskinan') sidebarSub.textContent = 'Menampilkan list Data Keluarga'; + else sidebarSub.textContent = 'Menampilkan list Rumah Ibadah'; + } + + var sidebar = document.getElementById('sidebar'); + if (sidebar && sidebar.style.display === 'none') { + sidebar.style.display = 'flex'; + var pes = document.getElementById('panel-pesan'); + if (pes) pes.style.display = 'none'; + var lap = document.getElementById('panel-laporan'); + if (lap) lap.style.display = 'none'; + } +}; + +/* ── Format helpers ── */ +window.formatMeters = function (value) { + return Number(value).toLocaleString("id-ID", { maximumFractionDigits: 0 }) + " m"; +}; + +window.isValidCoordinate = function (lat, lng) { + return Number.isFinite(Number(lat)) && Number.isFinite(Number(lng)); +}; + +window.isValidRadius = function (radius) { + var r = Number(radius); + return Number.isFinite(r) && r >= 100 && r <= 5000; +}; + +/* ───────────────────────────────────────────── + DOMContentLoaded – inisialisasi peta (KF-01) + ───────────────────────────────────────────── */ +document.addEventListener("DOMContentLoaded", function () { + + /* Inisialisasi peta terpusat di Pontianak */ + window.map = L.map("map").setView([-0.0263, 109.3425], 13); + + // --- Base maps --- + window.osmBase = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }); + window.darkBase = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap contributors © CARTO' + }); + + // Add default basemap + window.osmBase.addTo(map); + + /* ── Layers ── */ + appState.layers.kemiskinan = L.layerGroup().addTo(map); + appState.layers.masjid = L.layerGroup().addTo(map); + appState.layers.heatmap = L.layerGroup(); // Do not add yet + + /* Klik peta – hanya aktif di Mode Edit (KF-03, KF-07) */ + map.on("click", function (event) { + if (appState.mode !== "edit") { return; } + + var lat = event.latlng.lat; + var lng = event.latlng.lng; + + if (appState.activeType === "kemiskinan") { + if (typeof window.openKemiskinanModal === "function") { + window.openKemiskinanModal(lat, lng); + } + return; + } + + if (appState.activeType === "masjid") { + if (typeof window.openMasjidModal === "function") { + window.openMasjidModal(lat, lng); + } + } + }); + + /* Init state awal */ + window.setMode("view"); + window.setDataType("kemiskinan"); + + setTimeout(function () { map.invalidateSize(); }, 300); + + /* Cek session login, lalu load data */ + if (typeof window.checkAuthState === "function") { + window.checkAuthState().then(function () { + if (typeof window.loadAllData === "function") { window.loadAllData(); } + }); + } else if (typeof window.loadAllData === "function") { + window.loadAllData(); + } +}); + +/* ── Heatmap & Routing Features ── */ +window.toggleHeatmap = function(btnEl) { + if(!window.appState) return; + window.appState.isHeatmap = !window.appState.isHeatmap; + btnEl.classList.toggle('active', window.appState.isHeatmap); + + if(window.appState.isHeatmap) { + document.getElementById('desc-heatmap').style.display = 'block'; + // Hide kemiskinan markers + window.appState.layers.kemiskinan.eachLayer(function(l) { l.setOpacity(0); }); + // Show heatmap + if(!window.map.hasLayer(window.appState.layers.heatmap)) { + window.appState.layers.heatmap.addTo(window.map); + } + // Switch to Dark mode for better visual + window.map.removeLayer(window.osmBase); + window.darkBase.addTo(window.map); + } else { + document.getElementById('desc-heatmap').style.display = 'none'; + // Show kemiskinan markers + window.appState.layers.kemiskinan.eachLayer(function(l) { l.setOpacity(1); }); + // Hide heatmap + if(window.map.hasLayer(window.appState.layers.heatmap)) { + window.map.removeLayer(window.appState.layers.heatmap); + } + // Restore map base + window.map.removeLayer(window.darkBase); + window.osmBase.addTo(window.map); + } +}; + +window.toggleRoutingMode = function(btnEl) { + if(!window.appState) return; + window.appState.isRouting = true; + window.appState.routingWaypoints = []; + btnEl.classList.add('active'); + document.getElementById('desc-routing').style.display = 'block'; + + // Show instruction panel + document.getElementById('routing-instruction-panel').style.display = 'block'; + document.getElementById('routing-instruction-text').innerHTML = 'Silakan klik Masjid atau Rumah Warga di peta sebagai titik awal rute.'; +}; + +window.cancelRouting = function() { + if(!window.appState) return; + window.appState.isRouting = false; + window.appState.routingWaypoints = []; + document.getElementById('btn-toggle-routing').classList.remove('active'); + document.getElementById('desc-routing').style.display = 'none'; + document.getElementById('routing-instruction-panel').style.display = 'none'; + + // Remove routing control if exists + if(window.routingControl) { + window.map.removeControl(window.routingControl); + window.routingControl = null; + } +}; + +window.handleRoutingClick = function(lat, lng, name) { + window.appState.routingWaypoints.push(L.latLng(lat, lng)); + var pts = window.appState.routingWaypoints; + var inst = document.getElementById('routing-instruction-text'); + + if(pts.length === 1) { + inst.innerHTML = '
Titik Awal: ' + window.escapeHtml(name) + '
' + + '
Langkah 2: Pilih Tujuan
' + + 'Klik rumah/masjid lain di peta sebagai tujuan akhir.'; + } else if(pts.length === 2) { + inst.innerHTML = '
Rute Berhasil Dibuat
' + + 'Detail arah jalan (navigasi) muncul di layar sebelah kanan peta.'; + + // Remove existing control + if(window.routingControl) { + window.map.removeControl(window.routingControl); + } + + window.routingControl = L.Routing.control({ + waypoints: pts, + routeWhileDragging: false, + addWaypoints: false, + show: true, + lineOptions: { + styles: [{color: '#2563eb', opacity: 0.8, weight: 6}] + } + }).addTo(window.map); + + window.appState.isRouting = false; // Turn off click interception after 2 points + document.getElementById('btn-toggle-routing').classList.remove('active'); + } +}; diff --git a/js/pesan.js b/js/pesan.js new file mode 100644 index 0000000..16b6c73 --- /dev/null +++ b/js/pesan.js @@ -0,0 +1,212 @@ +/* ============================================================ + js/pesan.js — Media komunikasi pengurus v2.2 + ============================================================ */ + +window.labelJenisPesan = { + permintaan_bantuan: 'Permintaan Bantuan', + koordinasi: 'Koordinasi', + pengumuman: 'Pengumuman' +}; + +var _jenisEmoji = { + permintaan_bantuan: '🆘', + koordinasi: '📋', + pengumuman: '📢' +}; + +/* ───────────────────────────────────────────────────── + Floating detail box +───────────────────────────────────────────────────── */ +window.openPesanDetail = async function (p) { + var fl = document.getElementById('pesan-detail-float'); + if (!fl) return; + + var jenis = p.jenis || 'koordinasi'; + var emoji = _jenisEmoji[jenis] || '💬'; + var jenisLabel = window.labelJenisPesan[jenis] || jenis; + var isBroadcast = !p.penerima_id; + var roleLabel = p.role_pengirim === 'pimpinan_daerah' ? ' Pimpinan Daerah' : ' Pengurus Masjid'; + + /* Accent bar */ + document.getElementById('pdf-accent-bar').className = 'pdf-accent-bar ' + jenis; + + /* Icon */ + var icon = document.getElementById('pdf-icon'); + icon.textContent = emoji; + icon.className = 'pdf-icon ' + jenis; + + /* Title (judul pesan) + subtitle (jenis) */ + document.getElementById('pdf-title').textContent = p.judul || 'Pesan'; + document.getElementById('pdf-subtitle').textContent = jenisLabel; + + /* Isi */ + document.getElementById('pdf-isi').textContent = p.isi || '(tidak ada isi)'; + + /* Info rows */ + document.getElementById('pdf-pengirim').textContent = p.nama_pengirim || '-'; + document.getElementById('pdf-role').innerHTML = roleLabel; + document.getElementById('pdf-jenis-label').textContent = emoji + ' ' + jenisLabel; + document.getElementById('pdf-waktu').textContent = p.created_fmt || '-'; + + /* Broadcast badge */ + document.getElementById('pdf-row-broadcast').style.display = isBroadcast ? '' : 'none'; + + /* Baru badge */ + document.getElementById('pdf-row-baru').style.display = (p.status === 'baru') ? '' : 'none'; + + /* Close */ + document.getElementById('pdf-close').onclick = window.closePesanDetail; + + /* Show */ + fl.style.display = 'flex'; + + /* Mark as read if it is new and we are not the sender */ + if (p.status === 'baru' && p.pengirim_id != window.appAuth.id) { + try { + var formData = new FormData(); + formData.append('id', p.id); + formData.append('status', 'dibaca'); + await fetch('php/update_pesan_status.php', { method: 'POST', body: formData }); + + p.status = 'dibaca'; + + // Remove 'baru' indicator from card visually + var cardEl = document.querySelector('.p-card[data-id="' + p.id + '"]'); + if (cardEl) { + cardEl.classList.remove('baru'); + var dot = cardEl.querySelector('.p-new'); + if (dot) dot.remove(); + } + + // Immediately update the notification badge + if (window.pollPesanNotification) window.pollPesanNotification(); + } catch (e) { + console.error('Failed to update message status', e); + } + } +}; + +window.closePesanDetail = function () { + var fl = document.getElementById('pesan-detail-float'); + if (fl) fl.style.display = 'none'; +}; + +/* ── Panel open/close ── */ +window.openPesanPanel = async function () { + var panel = document.getElementById('panel-pesan'); + if (panel) { + var sidebar = document.getElementById('sidebar'); + if (sidebar) sidebar.style.display = 'none'; + var lap = document.getElementById('panel-laporan'); + if (lap) lap.style.display = 'none'; + panel.style.display = 'flex'; + await window.loadPesan(); + } +}; +window.closePesanPanel = function () { + var p = document.getElementById('panel-pesan'); + if (p) p.style.display = 'none'; + window.closePesanDetail(); + + // Auto-open sidebar + var sidebar = document.getElementById('sidebar'); + if (sidebar) sidebar.style.display = 'flex'; +}; + +/* ── Load & render pesan ── */ +window.loadPesan = async function () { + var container = document.getElementById('pesan-list-container'); + if (!container) return; + container.innerHTML = '
Memuat pesan...
'; + var data = await window.fetchJson('php/read_pesan.php', []); + container.innerHTML = ''; + + if (!data.length) { + container.innerHTML = '
Belum ada pesan. Mulai komunikasi di bawah.
'; + return; + } + + data.forEach(function (p) { + var jenisClass = { permintaan_bantuan:'bantuan', koordinasi:'', pengumuman:'pengumuman' }[p.jenis] || ''; + var chipColors = { permintaan_bantuan:'chip-red', koordinasi:'chip-blue', pengumuman:'chip-amber' }; + var isBroadcast = !p.penerima_id; + var emoji = _jenisEmoji[p.jenis] || '💬'; + + var card = document.createElement('div'); + card.className = 'p-card' + (jenisClass ? ' ' + jenisClass : '') + (p.status === 'baru' ? ' baru' : ''); + card.title = 'Klik untuk detail lengkap'; + card.dataset.id = p.id; + + card.innerHTML = + '
' + + '' + emoji + ' ' + (window.labelJenisPesan[p.jenis]||p.jenis) + '' + + (isBroadcast ? '📡 Broadcast' : '') + + (p.status === 'baru' ? '' : '') + + '
' + + '
' + window.escapeHtml(p.judul) + '
' + + '
' + window.escapeHtml(p.isi) + '
' + + '
👤 ' + window.escapeHtml(p.nama_pengirim) + + ' (' + (p.role_pengirim === 'pimpinan_daerah' ? 'Pimpinan' : 'Pengurus') + ')' + + ' · 🕐 ' + p.created_fmt + '
'; + + /* Klik kartu → buka floating detail */ + card.addEventListener('click', function () { + window.openPesanDetail(p); + }); + + container.appendChild(card); + }); +}; + +/* ── Submit kirim pesan ── */ +window.submitPesan = async function (e) { + e.preventDefault(); + if (!window.appAuth.loggedIn) { window.showToast('Login diperlukan.', 'error'); return; } + + var btn = document.getElementById('btn-submit-pesan'); + var judul = document.getElementById('pesan-judul').value.trim(); + var isi = document.getElementById('pesan-isi').value.trim(); + var jenis = document.getElementById('pesan-jenis').value; + + if (!judul || !isi) { window.showToast('Judul dan isi wajib diisi.', 'error'); return; } + btn.disabled = true; btn.textContent = 'Mengirim...'; + + try { + await window.postForm('php/send_pesan.php', { judul, isi, jenis }); + document.getElementById('pesan-judul').value = ''; + document.getElementById('pesan-isi').value = ''; + window.showToast('Pesan terkirim.', 'success'); + await window.loadPesan(); + } catch (err) { window.showToast('Gagal: ' + err.message, 'error'); } + finally { btn.disabled = false; btn.textContent = 'Kirim Pesan'; } +}; + +/* Polling for Pesan Notification */ +window.pollPesanNotification = async function() { + if (!window.appAuth) return; + try { + var res = await fetch('php/read_pesan.php'); + var text = await res.text(); + var data = JSON.parse(text); + if(Array.isArray(data)) { + // Count messages from OTHER users that are considered "baru" + // Since we don't have a read-status per user in this simple app, we can just count messages from others in the last 24 hours, or just count 'baru' + var pendingCount = data.filter(function(m) { + return m.status === 'baru' && m.pengirim_id != window.appAuth.id; + }).length; + + var badge = document.getElementById('nav-badge-pesan'); + if(badge) { + if(pendingCount > 0) { + badge.style.display = 'inline-flex'; + badge.textContent = pendingCount; + } else { + badge.style.display = 'none'; + } + } + } + } catch(e) {} +}; + +// Start polling every 15 seconds +setInterval(window.pollPesanNotification, 15000); diff --git a/php/check_auth.php b/php/check_auth.php new file mode 100644 index 0000000..55464db --- /dev/null +++ b/php/check_auth.php @@ -0,0 +1,17 @@ + false]); +} else { + echo json_encode([ + 'loggedIn' => true, + 'nama' => $_SESSION['user_nama'] ?? '', + 'role' => $_SESSION['user_role'] ?? '' + ]); +} +?> diff --git a/php/create_anggota.php b/php/create_anggota.php new file mode 100644 index 0000000..363ce04 --- /dev/null +++ b/php/create_anggota.php @@ -0,0 +1,46 @@ +prepare( + "INSERT INTO anggota_keluarga (kemiskinan_id, nama, tanggal_lahir, jenis_kelamin, hubungan_keluarga, status_hidup, pendidikan, catatan_khusus) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)" +); +if (!$stmt) { http_response_code(500); echo 'Gagal prepare.'; exit; } + +$stmt->bind_param('isssssss', $kemiskinanId, $nama, $tgl, $jenisKelamin, $hubungan, $statusHidup, $pendidikan, $catatan); +if ($stmt->execute()) { + echo 'success'; +} else { + http_response_code(500); echo 'Gagal menyimpan: ' . $stmt->error; +} +$stmt->close(); +?> diff --git a/php/create_bantuan.php b/php/create_bantuan.php new file mode 100644 index 0000000..dec6022 --- /dev/null +++ b/php/create_bantuan.php @@ -0,0 +1,47 @@ + mysqli_real_escape_string($conn, (string)$v); + +$mjId = $masjidId ? intval($masjidId) : 'NULL'; +$jml = $jumlahBantuan !== null ? floatval($jumlahBantuan) : 'NULL'; +$prd = $periodeBerikutnya ? "'" . $esc($periodeBerikutnya) . "'" : 'NULL'; + +$sql = "INSERT INTO histori_bantuan + (kemiskinan_id, masjid_id, jenis_bantuan, deskripsi_bantuan, jumlah_bantuan, satuan, tanggal_bantuan, periode_berikutnya, status_penyaluran, dicatat_oleh) + VALUES ( + {$kemiskinanId}, {$mjId}, + '{$esc($jenisBantuan)}', '{$esc($deskripsi)}', + {$jml}, '{$esc($satuan)}', '{$esc($tanggal)}', + {$prd}, '{$esc($statusPenyaluran)}', '{$esc($dicatatOleh)}' + )"; + +if (mysqli_query($conn, $sql)) { + echo 'success'; +} else { + http_response_code(500); echo 'Gagal: ' . mysqli_error($conn); +} +?> diff --git a/php/create_kemiskinan.php b/php/create_kemiskinan.php new file mode 100644 index 0000000..0bf734b --- /dev/null +++ b/php/create_kemiskinan.php @@ -0,0 +1,47 @@ +getMessage(); +} + +if ($statement) { + mysqli_stmt_close($statement); +} +?> diff --git a/php/create_laporan.php b/php/create_laporan.php new file mode 100644 index 0000000..9e931bb --- /dev/null +++ b/php/create_laporan.php @@ -0,0 +1,27 @@ + mysqli_real_escape_string($conn, (string)$v); +$kemId = $kemiskinanId ? intval($kemiskinanId) : 'NULL'; + +$sql = "INSERT INTO laporan_kondisi (kemiskinan_id, nama_pelapor, nomor_wa, kategori, deskripsi) + VALUES ({$kemId}, '{$esc($namaPelapor)}', '{$esc($nomorWa)}', '{$esc($kategori)}', '{$esc($deskripsi)}')"; + +if (mysqli_query($conn, $sql)) { echo 'success'; } +else { http_response_code(500); echo 'Gagal: ' . mysqli_error($conn); } +?> diff --git a/php/create_masjid.php b/php/create_masjid.php new file mode 100644 index 0000000..63f1879 --- /dev/null +++ b/php/create_masjid.php @@ -0,0 +1,39 @@ + 5000) { + http_response_code(400); + exit('error: data tidak lengkap'); +} + +$stmt = $conn->prepare( + "INSERT INTO masjid (nama_masjid, pic_masjid, alamat, latitude, longitude, radius, jenis_ibadah) + VALUES (?, ?, ?, ?, ?, ?, ?)" +); +if (!$stmt) { http_response_code(500); echo 'Gagal prepare: ' . $conn->error; exit; } + +$stmt->bind_param('sssddis', $namaIbadah, $picIbadah, $alamat, $latitude, $longitude, $radius, $jenisIbadah); + +if ($stmt->execute()) { + echo 'success'; +} else { + http_response_code(500); + echo 'error: ' . $stmt->error; +} +$stmt->close(); +?> diff --git a/php/delete_anggota.php b/php/delete_anggota.php new file mode 100644 index 0000000..fcc7184 --- /dev/null +++ b/php/delete_anggota.php @@ -0,0 +1,14 @@ +prepare("DELETE FROM anggota_keluarga WHERE id=?"); +$stmt->bind_param('i', $id); +if ($stmt->execute()) { echo 'success'; } +else { http_response_code(500); echo 'Gagal: ' . $stmt->error; } +$stmt->close(); +?> diff --git a/php/delete_bantuan.php b/php/delete_bantuan.php new file mode 100644 index 0000000..22b43dc --- /dev/null +++ b/php/delete_bantuan.php @@ -0,0 +1,9 @@ + diff --git a/php/delete_kemiskinan.php b/php/delete_kemiskinan.php new file mode 100644 index 0000000..bb6b8e6 --- /dev/null +++ b/php/delete_kemiskinan.php @@ -0,0 +1,28 @@ +getMessage(); +} + +mysqli_stmt_close($statement); +?> diff --git a/php/delete_masjid.php b/php/delete_masjid.php new file mode 100644 index 0000000..1a87242 --- /dev/null +++ b/php/delete_masjid.php @@ -0,0 +1,28 @@ +getMessage(); +} + +mysqli_stmt_close($statement); +?> diff --git a/php/export_csv.php b/php/export_csv.php new file mode 100644 index 0000000..093cdbc --- /dev/null +++ b/php/export_csv.php @@ -0,0 +1,67 @@ + diff --git a/php/koneksi.php b/php/koneksi.php new file mode 100644 index 0000000..f0cf573 --- /dev/null +++ b/php/koneksi.php @@ -0,0 +1,206 @@ + 'Tidak terautentikasi. Silakan login.']); + exit; + } + if ($requiredRole && $_SESSION['user_role'] !== $requiredRole) { + http_response_code(403); + header('Content-Type: application/json'); + echo json_encode(['error' => 'Akses ditolak. Role tidak sesuai.']); + exit; + } +} + +function isLoggedIn() +{ + if (session_status() === PHP_SESSION_NONE) { + session_start(); + } + return !empty($_SESSION['user_id']); +} + +function currentRole() +{ + if (session_status() === PHP_SESSION_NONE) { + session_start(); + } + return $_SESSION['user_role'] ?? null; +} +?> \ No newline at end of file diff --git a/php/login.php b/php/login.php new file mode 100644 index 0000000..286e4fe --- /dev/null +++ b/php/login.php @@ -0,0 +1,45 @@ + 'Method not allowed']); + exit; +} + +$username = trim($_POST['username'] ?? ''); +$password = trim($_POST['password'] ?? ''); + +if (!$username || !$password) { + http_response_code(400); + echo json_encode(['error' => 'Username dan password wajib diisi.']); + exit; +} + +$stmt = $conn->prepare("SELECT id, password_hash, nama_lengkap, role FROM pengguna WHERE username = ? AND aktif = 1 LIMIT 1"); +$stmt->bind_param('s', $username); +$stmt->execute(); +$result = $stmt->get_result(); +$user = $result->fetch_assoc(); +$stmt->close(); + +if (!$user || !password_verify($password, $user['password_hash'])) { + http_response_code(401); + echo json_encode(['error' => 'Username atau password salah.']); + exit; +} + +$_SESSION['user_id'] = $user['id']; +$_SESSION['user_nama'] = $user['nama_lengkap']; +$_SESSION['user_role'] = $user['role']; + +echo json_encode([ + 'success' => true, + 'nama' => $user['nama_lengkap'], + 'role' => $user['role'] +]); +?> diff --git a/php/logout.php b/php/logout.php new file mode 100644 index 0000000..7fb3e28 --- /dev/null +++ b/php/logout.php @@ -0,0 +1,7 @@ + true]); +?> diff --git a/php/read_anggota.php b/php/read_anggota.php new file mode 100644 index 0000000..02441f4 --- /dev/null +++ b/php/read_anggota.php @@ -0,0 +1,29 @@ +prepare( + "SELECT *, + TIMESTAMPDIFF(YEAR, tanggal_lahir, CURDATE()) AS umur, + DATE_FORMAT(updated_at, '%Y-%m-%d %H:%i') AS updated_at_fmt, + DATE_FORMAT(tanggal_lahir, '%d/%m/%Y') AS tgl_lahir_fmt + FROM anggota_keluarga + WHERE kemiskinan_id = ? + ORDER BY hubungan_keluarga, nama" +); +$stmt->bind_param('i', $kemiskinanId); +$stmt->execute(); +$result = $stmt->get_result(); +$data = []; +while ($row = $result->fetch_assoc()) { + $data[] = $row; +} +$stmt->close(); +echo json_encode($data); +?> diff --git a/php/read_bantuan.php b/php/read_bantuan.php new file mode 100644 index 0000000..c25cad3 --- /dev/null +++ b/php/read_bantuan.php @@ -0,0 +1,28 @@ + mysqli_real_escape_string($conn, (string)$v); + +$sql = "SELECT hb.*, m.nama_masjid, + DATE_FORMAT(hb.tanggal_bantuan, '%d/%m/%Y') AS tanggal_fmt, + DATE_FORMAT(hb.periode_berikutnya, '%d/%m/%Y') AS periode_fmt, + CASE WHEN hb.periode_berikutnya IS NOT NULL AND hb.periode_berikutnya <= CURDATE() AND hb.status_penyaluran = 'belum' THEN 1 ELSE 0 END AS jatuh_tempo + FROM histori_bantuan hb + LEFT JOIN masjid m ON hb.masjid_id = m.id + WHERE hb.kemiskinan_id = {$kemiskinanId} + ORDER BY hb.tanggal_bantuan DESC"; + +$result = mysqli_query($conn, $sql); +$data = []; +if ($result) { + while ($row = mysqli_fetch_assoc($result)) { + $data[] = $row; + } +} +echo json_encode($data); +?> diff --git a/php/read_kemiskinan.php b/php/read_kemiskinan.php new file mode 100644 index 0000000..0b15de3 --- /dev/null +++ b/php/read_kemiskinan.php @@ -0,0 +1,18 @@ + diff --git a/php/read_laporan.php b/php/read_laporan.php new file mode 100644 index 0000000..546ba24 --- /dev/null +++ b/php/read_laporan.php @@ -0,0 +1,36 @@ + 0) { + $conditions[] = "lk.kemiskinan_id = {$kemiskinanIdRaw}"; +} +if (in_array($statusFilter, $validStatus)) { + $esc = mysqli_real_escape_string($conn, $statusFilter); + $conditions[] = "lk.status_laporan = '{$esc}'"; +} + +$where = count($conditions) ? 'WHERE ' . implode(' AND ', $conditions) : ''; + +$sql = "SELECT lk.*, k.nama_kepala_keluarga, k.alamat AS alamat_kk, + DATE_FORMAT(lk.created_at, '%d/%m/%Y %H:%i') AS created_fmt + FROM laporan_kondisi lk + LEFT JOIN kemiskinan k ON lk.kemiskinan_id = k.id + {$where} + ORDER BY lk.created_at DESC + LIMIT 200"; + +$result = mysqli_query($conn, $sql); +$data = []; +if ($result) { + while ($row = mysqli_fetch_assoc($result)) { $data[] = $row; } +} +echo json_encode($data); +?> diff --git a/php/read_masjid.php b/php/read_masjid.php new file mode 100644 index 0000000..4f823ea --- /dev/null +++ b/php/read_masjid.php @@ -0,0 +1,17 @@ + diff --git a/php/read_pesan.php b/php/read_pesan.php new file mode 100644 index 0000000..fe69d51 --- /dev/null +++ b/php/read_pesan.php @@ -0,0 +1,34 @@ + diff --git a/php/send_pesan.php b/php/send_pesan.php new file mode 100644 index 0000000..efb43ae --- /dev/null +++ b/php/send_pesan.php @@ -0,0 +1,27 @@ + mysqli_real_escape_string($conn, (string)$v); +$penerima = $penerimaId ? intval($penerimaId) : 'NULL'; + +$sql = "INSERT INTO pesan_komunikasi (pengirim_id, penerima_id, judul, isi, jenis) + VALUES ({$pengirimId}, {$penerima}, '{$esc($judul)}', '{$esc($isi)}', '{$esc($jenis)}')"; + +if (mysqli_query($conn, $sql)) { echo 'success'; } +else { http_response_code(500); echo mysqli_error($conn); } +?> diff --git a/php/update_anggota.php b/php/update_anggota.php new file mode 100644 index 0000000..1cd8495 --- /dev/null +++ b/php/update_anggota.php @@ -0,0 +1,49 @@ +prepare( + "UPDATE anggota_keluarga + SET nama=?, tanggal_lahir=?, jenis_kelamin=?, hubungan_keluarga=?, + status_hidup=?, pendidikan=?, catatan_khusus=? + WHERE id=?" +); +if (!$stmt) { http_response_code(500); echo 'Gagal prepare: ' . $conn->error; exit; } + +$stmt->bind_param('sssssssi', $nama, $tgl, $jenisKelamin, $hubungan, $statusHidup, $pendidikan, $catatan, $id); + +if ($stmt->execute()) { + echo 'success'; +} else { + http_response_code(500); echo 'Gagal: ' . $stmt->error; +} +$stmt->close(); +?> diff --git a/php/update_bantuan_status.php b/php/update_bantuan_status.php new file mode 100644 index 0000000..55dc9ca --- /dev/null +++ b/php/update_bantuan_status.php @@ -0,0 +1,17 @@ + diff --git a/php/update_kemiskinan.php b/php/update_kemiskinan.php new file mode 100644 index 0000000..04330eb --- /dev/null +++ b/php/update_kemiskinan.php @@ -0,0 +1,38 @@ +prepare("UPDATE kemiskinan SET nama_kepala_keluarga=?, jumlah_kk=?, alamat=?, status_keluarga=? WHERE id=?"); +if (!$stmt) { + http_response_code(500); + echo 'Gagal menyiapkan query.'; + exit; +} + +$stmt->bind_param('sissi', $nama, $jml, $alamat, $status, $id); +if ($stmt->execute()) { + echo 'success'; +} else { + http_response_code(500); + echo 'Gagal memperbarui data: ' . $stmt->error; +} +$stmt->close(); +$conn->close(); +?> diff --git a/php/update_laporan_status.php b/php/update_laporan_status.php new file mode 100644 index 0000000..62a0592 --- /dev/null +++ b/php/update_laporan_status.php @@ -0,0 +1,20 @@ + mysqli_real_escape_string($conn, (string)$v); +$sql = "UPDATE laporan_kondisi SET status_laporan='{$esc($status)}', ditangani_oleh='{$esc($ditanganiOleh)}' WHERE id={$id}"; + +if (mysqli_query($conn, $sql)) { echo 'success'; } +else { http_response_code(500); echo mysqli_error($conn); } +?> diff --git a/php/update_masjid.php b/php/update_masjid.php new file mode 100644 index 0000000..0d22fc6 --- /dev/null +++ b/php/update_masjid.php @@ -0,0 +1,36 @@ +prepare("UPDATE masjid SET nama_masjid=?, pic_masjid=?, alamat=?, jenis_ibadah=? WHERE id=?"); +if (!$stmt) { + http_response_code(500); + echo 'Gagal menyiapkan query.'; + exit; +} + +$stmt->bind_param('ssssi', $nama, $pic, $alamat, $jenisIbadah, $id); +if ($stmt->execute()) { + echo 'success'; +} else { + http_response_code(500); + echo 'Gagal memperbarui data: ' . $stmt->error; +} +$stmt->close(); +?> diff --git a/php/update_masjid_radius.php b/php/update_masjid_radius.php new file mode 100644 index 0000000..2c99126 --- /dev/null +++ b/php/update_masjid_radius.php @@ -0,0 +1,30 @@ + 5000) { + http_response_code(400); + exit('error'); +} + +$statement = mysqli_prepare($conn, "UPDATE masjid SET radius = ? WHERE id = ?"); +mysqli_stmt_bind_param($statement, "di", $radius, $id); + +try { + if (mysqli_stmt_execute($statement)) { + echo 'success'; + } else { + http_response_code(500); + echo 'error'; + } +} catch (mysqli_sql_exception $exception) { + http_response_code(500); + echo 'error: ' . $exception->getMessage(); +} + +mysqli_stmt_close($statement); +?> diff --git a/php/update_pesan_status.php b/php/update_pesan_status.php new file mode 100644 index 0000000..a725290 --- /dev/null +++ b/php/update_pesan_status.php @@ -0,0 +1,24 @@ +prepare("UPDATE pesan_komunikasi SET status = ? WHERE id = ?"); + $stmt->bind_param("si", $status, $id); + + if ($stmt->execute()) { + echo "Success"; + } else { + http_response_code(500); + echo "Error: " . $conn->error; + } +} +?>