first commit
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
# Penjelasan WebGIS — Bahan SRS & Manual Book
|
||||
|
||||
**WebGIS Pontianak — Pengentasan Kemiskinan Berbasis Rumah Ibadah**
|
||||
*(Poverty Mapping — WebGIS Kota Pontianak)*
|
||||
|
||||
---
|
||||
|
||||
## 1. Gambaran Umum Sistem
|
||||
|
||||
### Nama & Tujuan
|
||||
Sistem informasi geografis (WebGIS) untuk **pemetaan keluarga miskin** di **Kota Pontianak** dengan pendekatan **berbasis rumah ibadah** sebagai unit pembina sosial. Rumah ibadah (masjid, gereja, pura, vihara, klenteng, dll.) memiliki **radius cakupan wilayah**; keluarga miskin di dalam radius tersebut **otomatis ditugaskan** sebagai KK binaan rumah ibadah terdekat.
|
||||
|
||||
### Masalah yang Diselesaikan
|
||||
- Data kemiskinan tersebar dan sulit divisualisasikan secara spasial
|
||||
- Koordinasi bantuan sosial (PKH, BPNT, bansos, dll.) perlu dilacak per keluarga
|
||||
- Masyarakat perlu saluran laporan tanpa harus login
|
||||
- Pemkot (walikota) butuh dashboard agregat untuk pengambilan keputusan
|
||||
- Pengurus rumah ibadah butuh akses terbatas hanya untuk KK binaannya
|
||||
|
||||
### Cakupan Geografis
|
||||
6 kecamatan di Kota Pontianak:
|
||||
- Pontianak Utara, Selatan, Timur, Barat, Tenggara, Kota
|
||||
|
||||
---
|
||||
|
||||
## 2. Arsitektur Teknis
|
||||
|
||||
### Stack Teknologi
|
||||
|
||||
| Komponen | Teknologi |
|
||||
|----------|-----------|
|
||||
| Frontend | HTML5, CSS3, JavaScript (vanilla), Leaflet 1.9.4 |
|
||||
| Backend | PHP dengan PDO |
|
||||
| Database | MySQL (`webgis_rumah_ibadah`) |
|
||||
| Peta | CARTO Voyager tiles + OpenStreetMap Nominatim (reverse geocoding) |
|
||||
| Ekspor | SheetJS (Excel), jsPDF + autoTable (PDF) |
|
||||
| Autentikasi | PHP Session |
|
||||
| Server dev | Laragon (Apache + MySQL) |
|
||||
|
||||
### Struktur File Utama
|
||||
- `index.html` — seluruh antarmuka & logika frontend
|
||||
- `index.php` — redirect ke `index.html`
|
||||
- `config.php` — koneksi DB, utilitas (Haversine, auto-assign, rate limit, upload)
|
||||
- `api_auth.php` — autentikasi
|
||||
- `api_rumah_ibadah.php` — CRUD rumah ibadah
|
||||
- `api_keluarga_miskin.php` — CRUD keluarga miskin
|
||||
- `api_laporan.php` — laporan masyarakat
|
||||
- `api_histori_bantuan.php` — histori bantuan
|
||||
- `api_dashboard.php` — dashboard walikota
|
||||
- `database.sql` — skema & data contoh
|
||||
- `uploads/` — foto laporan masyarakat
|
||||
|
||||
### Alur Arsitektur
|
||||
```
|
||||
Browser (index.html)
|
||||
↓ fetch JSON
|
||||
PHP API (api_*.php)
|
||||
↓ PDO
|
||||
MySQL (webgis_rumah_ibadah)
|
||||
```
|
||||
|
||||
Peta juga memanggil **Nominatim OpenStreetMap** untuk reverse geocoding (alamat otomatis dari koordinat).
|
||||
|
||||
---
|
||||
|
||||
## 3. Aktor / Pengguna Sistem
|
||||
|
||||
| Role | Deskripsi | Login |
|
||||
|------|-----------|-------|
|
||||
| **Masyarakat (publik)** | Melihat peta & data (read-only), mengirim laporan warga miskin | Tidak perlu login |
|
||||
| **Admin** | Kelola penuh: rumah ibadah, KK, laporan, bantuan, reset password pengurus | `admin` / `admin123` |
|
||||
| **Pengurus Rumah Ibadah** | Kelola KK binaan & histori bantuan di wilayahnya | Kode `RI-XXX-9999` / password pengurus |
|
||||
| **Walikota** | Dashboard agregat, lihat data, ekspor; **tidak bisa** menambah/mengubah/hapus | `walikota` / `admin123` |
|
||||
|
||||
**Format login pengurus:** `RI-[JENIS 3 HURUF]-[4 DIGIT]`
|
||||
Contoh: `RI-MSJ-4821` (Masjid), `RI-GRJ-7153` (Gereja)
|
||||
|
||||
---
|
||||
|
||||
## 4. Modul Fungsional (untuk SRS)
|
||||
|
||||
### 4.1 Modul Peta Interaktif (WebGIS)
|
||||
- Peta interaktif berpusat di Pontianak (lat: -0.0263, lng: 109.3425, zoom 14)
|
||||
- Layer marker:
|
||||
- **Rumah ibadah** — warna per jenis (Masjid hijau, Gereja biru, dll.)
|
||||
- **Keluarga miskin** — warna per tingkat (Sangat Miskin merah, Miskin oranye, Hampir Miskin kuning)
|
||||
- **Radius cakupan** rumah ibadah (lingkaran saat zoom ke RI)
|
||||
- **Pick lokasi di peta** — klik peta untuk set koordinat + auto-fill alamat via Nominatim
|
||||
- Popup detail saat klik marker
|
||||
- Legenda dinamis per tab aktif
|
||||
|
||||
### 4.2 Modul Rumah Ibadah
|
||||
**Data:** nama, jenis, alamat, koordinat, radius (0.5–5 km), kecamatan, kode pengurus, password pengurus
|
||||
|
||||
| Aksi | Admin | Pengurus | Walikota | Publik |
|
||||
|------|-------|----------|----------|--------|
|
||||
| Lihat daftar & peta | ✓ | ✓ (hanya RI sendiri) | ✓ | ✓ |
|
||||
| Tambah | ✓ | ✗ | ✗ | ✗ |
|
||||
| Edit | ✓ | ✓ (RI sendiri) | ✗ | ✗ |
|
||||
| Hapus | ✓ | ✗ | ✗ | ✗ |
|
||||
| Reset password pengurus | ✓ | ✗ | ✗ | ✗ |
|
||||
|
||||
**Fitur khusus:**
|
||||
- Saat tambah RI, sistem generate **kode pengurus** & **password acak** (8 karakter)
|
||||
- Perubahan/hapus RI memicu **re-assign otomatis** semua KK ke RI terdekat
|
||||
|
||||
### 4.3 Modul Keluarga Miskin
|
||||
**Data:** NIK, nama KK, tanggal lahir, pendidikan, jumlah anggota, alamat, kecamatan, koordinat, tingkat kemiskinan, status DTKS, riwayat penyakit (JSON), rumah ibadah binaan, jarak ke RI
|
||||
|
||||
**Tingkat kemiskinan:** Sangat Miskin, Miskin, Hampir Miskin
|
||||
**Status DTKS:** Terdaftar DTKS, Belum DTKS, Sedang Diusulkan
|
||||
**Riwayat penyakit:** Stunting, TBC, Hipertensi, Diabetes, Jantung, ODGJ, DBD, Gizi Buruk, Lainnya
|
||||
|
||||
**Auto-assign:** Algoritma **Haversine** — KK ditugaskan ke RI terdekat yang radiusnya mencakup lokasi KK.
|
||||
|
||||
**Filter:** kecamatan, tingkat kemiskinan, status DTKS
|
||||
|
||||
**Flag bisnis:** KK dengan status `Belum DTKS` ditandai **Eligible Bansos Tunai Pemkot**
|
||||
|
||||
| Aksi | Admin | Pengurus | Walikota |
|
||||
|------|-------|----------|----------|
|
||||
| Lihat | ✓ | ✓ (KK binaan) | ✓ |
|
||||
| Tambah/Edit | ✓ | ✓ (KK binaan) | ✗ |
|
||||
| Hapus | ✓ | ✗ | ✗ |
|
||||
| Ekspor Excel/PDF | ✓ | ✗ | ✓ |
|
||||
|
||||
### 4.4 Modul Laporan Masyarakat
|
||||
- **Tanpa login** — tombol floating *"Laporkan Warga Miskin"*
|
||||
- Input: identitas warga (opsional), NIK (opsional, 16 digit), lokasi, alamat, deskripsi, foto (maks 5MB: JPG/PNG/GIF/WebP), identitas pelapor (boleh anonim)
|
||||
- **Rate limit:** maks **5 laporan per jam per IP**
|
||||
- Status: **Baru → Diverifikasi / Ditolak**
|
||||
- Admin dapat: kelola status, isi alasan tolak, konversi ke data KK, buat KK baru dari laporan
|
||||
|
||||
| Aksi | Admin | Walikota | Publik |
|
||||
|------|-------|----------|--------|
|
||||
| Kirim laporan | ✓ | ✓ | ✓ |
|
||||
| Lihat daftar | ✓ | ✓ | ✗ |
|
||||
| Kelola/verifikasi | ✓ | ✗ | ✗ |
|
||||
|
||||
### 4.5 Modul Histori Bantuan
|
||||
**Jenis bantuan:** PKH, BPNT/Sembako, Beras Bansos, KIS-PBI, Bansos Tunai Pemkot, Rutilahu, PMT Stunting, CSR Swasta, Lainnya
|
||||
**Sumber dana:** APBN, APBD, CSR, Lainnya
|
||||
|
||||
- Pencatatan per KK: jenis, nominal (Rp), keterangan, tanggal penyaluran, sumber dana, penanggung jawab
|
||||
- **Peringatan duplikasi** jika KK sudah punya PKH/BPNT/Beras Bansos
|
||||
- Total bantuan per KK dihitung otomatis
|
||||
|
||||
### 4.6 Modul Dashboard Walikota
|
||||
Tampil di sidebar saat walikota login (tab Rumah Ibadah & Keluarga Miskin). Menampilkan:
|
||||
- Total KK, KK sudah/belum dapat bantuan (tahun berjalan)
|
||||
- Distribusi per kecamatan, penyakit, DTKS, tingkat kemiskinan
|
||||
- Bantuan per jenis & sumber dana + total nominal
|
||||
- Jumlah laporan belum ditangani
|
||||
- **Drill-down** klikabel → detail tabel + ekspor Excel
|
||||
|
||||
Admin juga dapat mengakses API dashboard yang sama.
|
||||
|
||||
### 4.7 Modul Autentikasi
|
||||
- Login via username/password (JSON POST)
|
||||
- Session PHP (`$_SESSION['user']`)
|
||||
- Endpoint: `check`, `login`, `logout`
|
||||
- Pengurus login via kode RI, bukan tabel `users`
|
||||
|
||||
---
|
||||
|
||||
## 5. Model Data
|
||||
|
||||
### Tabel Database (6 tabel)
|
||||
|
||||
1. **rumah_ibadah** — titik lokasi + radius + kredensial pengurus
|
||||
2. **users** — admin & walikota
|
||||
3. **keluarga_miskin** — data KK + auto-assign
|
||||
4. **laporan_masyarakat** — laporan warga
|
||||
5. **histori_bantuan** — riwayat penyaluran
|
||||
6. **rate_limit** — pembatasan IP laporan
|
||||
|
||||
### Relasi Utama
|
||||
- `rumah_ibadah` → `keluarga_miskin` (satu RI membina banyak KK)
|
||||
- `keluarga_miskin` → `histori_bantuan` (satu KK punya banyak bantuan)
|
||||
- `users` → `histori_bantuan` (penanggung jawab)
|
||||
- `laporan_masyarakat` → `keluarga_miskin` (konversi laporan ke KK)
|
||||
|
||||
---
|
||||
|
||||
## 6. Alur Bisnis Utama (untuk Manual Book)
|
||||
|
||||
### Alur A — Pendaftaran Rumah Ibadah (Admin)
|
||||
1. Login sebagai admin
|
||||
2. Tab **Rumah Ibadah** → **Tambah**
|
||||
3. Isi form / klik peta untuk koordinat
|
||||
4. Atur radius cakupan (slider 0.5–5 km)
|
||||
5. Simpan → sistem tampilkan **kode pengurus & password** (diserahkan ke pengurus RI)
|
||||
6. Semua KK di wilayah otomatis di-assign ulang
|
||||
|
||||
### Alur B — Pengurus Mencatat KK Binaan
|
||||
1. Login dengan kode `RI-XXX-9999`
|
||||
2. Tab **Keluarga Miskin** → **Tambah**
|
||||
3. Isi NIK, data keluarga, pilih penyakit, tentukan lokasi di peta
|
||||
4. Simpan → KK terhubung ke RI pengurus
|
||||
5. Klik **Bantuan** pada kartu KK → catat penyaluran bantuan
|
||||
|
||||
### Alur C — Laporan Masyarakat
|
||||
1. Tanpa login, klik **Laporkan Warga Miskin**
|
||||
2. Isi lokasi & deskripsi (identitas opsional)
|
||||
3. Kirim → status **Baru**
|
||||
4. Admin buka tab **Laporan** → **Kelola**
|
||||
5. Verifikasi (bisa buat KK dari laporan) atau tolak dengan alasan
|
||||
|
||||
### Alur D — Monitoring Walikota
|
||||
1. Login `walikota`
|
||||
2. Lihat dashboard di sidebar (statistik & grafik bar)
|
||||
3. Klik kartu untuk drill-down detail
|
||||
4. Tab **Keluarga Miskin** → filter & **Ekspor Excel/PDF**
|
||||
5. Tab **Laporan** → pantau laporan masuk (read-only untuk kelola)
|
||||
|
||||
---
|
||||
|
||||
## 7. Antarmuka Pengguna
|
||||
|
||||
| Area | Fungsi |
|
||||
|------|--------|
|
||||
| **Topbar** | Brand, navigasi tab (RI / KK / Laporan), info user, login/logout |
|
||||
| **Peta (kiri)** | Visualisasi spasial, pick lokasi, zoom ke entitas |
|
||||
| **Sidebar (kanan)** | Daftar data, filter, legenda, statistik, tombol tambah/ekspor |
|
||||
| **Modal** | Form CRUD, login, laporan, bantuan, konfirmasi hapus, dashboard drill-down |
|
||||
| **FAB** | Tombol mengambang laporan masyarakat |
|
||||
| **Toast** | Notifikasi sukses/error |
|
||||
|
||||
**Responsif:** Di layar ≤768px, sidebar menjadi panel bawah setengah layar.
|
||||
|
||||
---
|
||||
|
||||
## 8. Kebutuhan Non-Fungsional (untuk SRS)
|
||||
|
||||
| Aspek | Spesifikasi |
|
||||
|-------|-------------|
|
||||
| **Keamanan** | Password bcrypt, session-based auth, role-based access control, sanitasi input, prepared statements PDO |
|
||||
| **Ketersediaan data** | UTF-8 (`utf8mb4`), format JSON standar `{success, data, message}` |
|
||||
| **Performa** | Single-page, data dimuat via AJAX paralel |
|
||||
| **Batasan upload** | Foto laporan maks 5MB |
|
||||
| **Rate limiting** | 5 laporan/jam/IP |
|
||||
| **Kompatibilitas** | Browser modern dengan JavaScript; butuh koneksi internet (CDN peta & library) |
|
||||
| **Deployment** | PHP 7.4+, MySQL 5.7+, web server Apache/Nginx |
|
||||
|
||||
---
|
||||
|
||||
## 9. Kerangka Dokumen SRS
|
||||
|
||||
1. **Pendahuluan** — latar belakang, tujuan, ruang lingkup, definisi (DTKS, KK, RI, WebGIS)
|
||||
2. **Deskripsi Umum** — perspektif produk, fungsi produk, karakteristik pengguna
|
||||
3. **Kebutuhan Fungsional** — per modul (FR-01 s/d FR-XX)
|
||||
4. **Kebutuhan Data** — entitas & atribut dari database
|
||||
5. **Kebutuhan Antarmuka** — layar peta, sidebar, modal, dashboard
|
||||
6. **Kebutuhan Non-Fungsional** — keamanan, performa, reliabilitas
|
||||
7. **Aturan Bisnis** — auto-assign Haversine, eligible bansos tunai, duplikasi bantuan, rate limit
|
||||
8. **Matriks Hak Akses** — tabel role
|
||||
9. **Integrasi Eksternal** — OpenStreetMap, Nominatim, CDN library
|
||||
10. **Lampiran** — ERD, diagram alur, mockup UI
|
||||
|
||||
---
|
||||
|
||||
## 10. Kerangka Manual Book
|
||||
|
||||
**Bab 1 — Pengenalan**
|
||||
Tentang sistem, siapa penggunanya, persyaratan (browser, Laragon/URL)
|
||||
|
||||
**Bab 2 — Akses & Login**
|
||||
Login admin, walikota, pengurus (contoh kredensial demo)
|
||||
|
||||
**Bab 3 — Panduan Masyarakat**
|
||||
Cara melihat peta, mengirim laporan, batas 5 laporan/jam
|
||||
|
||||
**Bab 4 — Panduan Admin**
|
||||
Kelola rumah ibadah, KK, laporan, verifikasi & konversi ke KK
|
||||
|
||||
**Bab 5 — Panduan Pengurus Rumah Ibadah**
|
||||
Login kode RI, input KK binaan, pencatatan bantuan
|
||||
|
||||
**Bab 6 — Panduan Walikota**
|
||||
Dashboard, drill-down, ekspor data, tab laporan
|
||||
|
||||
**Bab 7 — Fitur Peta**
|
||||
Navigasi, legenda, zoom, pick lokasi
|
||||
|
||||
**Bab 8 — Ekspor Laporan**
|
||||
Excel & PDF (kolom yang diekspor)
|
||||
|
||||
**Bab 9 — Troubleshooting**
|
||||
Gagal login, rate limit, KK di luar radius, koneksi database
|
||||
|
||||
---
|
||||
|
||||
## 11. Data Demo (untuk Testing)
|
||||
|
||||
| Akun | Username | Password |
|
||||
|------|----------|----------|
|
||||
| Admin | `admin` | `admin123` |
|
||||
| Walikota | `walikota` | `admin123` |
|
||||
| Pengurus Masjid Raya | `RI-MSJ-4821` | `pengurus123` |
|
||||
| Pengurus Gereja | `RI-GRJ-7153` | `pengurus123` |
|
||||
|
||||
Database seed berisi: 2 rumah ibadah, 5 keluarga miskin, 1 laporan, 2 histori bantuan.
|
||||
|
||||
---
|
||||
|
||||
## 12. Ringkasan Nilai Sistem
|
||||
|
||||
Sistem ini menggabungkan **GIS**, **manajemen data sosial**, **partisipasi masyarakat**, dan **dashboard kebijakan** dalam satu aplikasi web. Konsep intinya: **rumah ibadah sebagai node pembina** dengan cakupan geografis terukur, sehingga program pengentasan kemiskinan dapat dipantau secara spasial dan berbasis data bantuan per keluarga.
|
||||
|
||||
---
|
||||
|
||||
*Dokumen ini dibuat sebagai bahan penyusunan SRS (Software Requirements Specification) dan Manual Book untuk WebGIS Pontianak.*
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// API: Autentikasi (Login / Logout / Cek Session)
|
||||
// Mendukung 3 role: admin, pengurus, walikota
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
// ── GET: Cek session aktif ───────────────────────────────────
|
||||
if ($method === 'GET' && $action === 'check') {
|
||||
if (!empty($_SESSION['user'])) {
|
||||
jsonResponse($_SESSION['user'], 'Session aktif.');
|
||||
} else {
|
||||
jsonResponse(null, 'Belum login.', false);
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST: Login ──────────────────────────────────────────────
|
||||
if ($method === 'POST' && $action === 'login') {
|
||||
// Ambil data dari body JSON
|
||||
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
$username = trim($body['username'] ?? '');
|
||||
$password = trim($body['password'] ?? '');
|
||||
|
||||
if (empty($username)) {
|
||||
jsonResponse(null, 'Username wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
// Cek apakah login sebagai pengurus (format kode_pengurus: RI-XXX-9999)
|
||||
$isPengurusLogin = preg_match('/^RI-[A-Z]{3}-\d{4}$/', $username);
|
||||
|
||||
if ($isPengurusLogin) {
|
||||
// ── Login pengurus via kode_pengurus di tabel rumah_ibadah ──
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, kode_pengurus, password_pengurus, nama, jenis
|
||||
FROM rumah_ibadah
|
||||
WHERE kode_pengurus = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$username]);
|
||||
$ri = $stmt->fetch();
|
||||
|
||||
if (!$ri) {
|
||||
jsonResponse(null, 'Kode pengurus tidak ditemukan.', false, 401);
|
||||
}
|
||||
|
||||
if (empty($password)) {
|
||||
jsonResponse(null, 'Password pengurus wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
if (!password_verify($password, $ri['password_pengurus'])) {
|
||||
jsonResponse(null, 'Password pengurus salah.', false, 401);
|
||||
}
|
||||
|
||||
// Set session sebagai pengurus
|
||||
$_SESSION['user'] = [
|
||||
'id' => 0, // Pengurus tidak punya ID di tabel users
|
||||
'nama' => 'Pengurus ' . $ri['nama'],
|
||||
'username' => $ri['kode_pengurus'],
|
||||
'role' => 'pengurus',
|
||||
'rumah_ibadah_id' => (int)$ri['id'],
|
||||
];
|
||||
|
||||
jsonResponse($_SESSION['user'], 'Login berhasil sebagai pengurus.');
|
||||
|
||||
} else {
|
||||
// ── Login admin / walikota via tabel users ──
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, nama, username, password, role, rumah_ibadah_id
|
||||
FROM users
|
||||
WHERE username = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$username]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user) {
|
||||
jsonResponse(null, 'Username tidak ditemukan.', false, 401);
|
||||
}
|
||||
|
||||
if (empty($password)) {
|
||||
jsonResponse(null, 'Password wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
if (!password_verify($password, $user['password'])) {
|
||||
jsonResponse(null, 'Password salah.', false, 401);
|
||||
}
|
||||
|
||||
// Set session
|
||||
$_SESSION['user'] = [
|
||||
'id' => (int)$user['id'],
|
||||
'nama' => $user['nama'],
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role'],
|
||||
'rumah_ibadah_id' => $user['rumah_ibadah_id'] ? (int)$user['rumah_ibadah_id'] : null,
|
||||
];
|
||||
|
||||
jsonResponse($_SESSION['user'], 'Login berhasil sebagai ' . $user['role'] . '.');
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST: Logout ─────────────────────────────────────────────
|
||||
if ($method === 'POST' && $action === 'logout') {
|
||||
session_destroy();
|
||||
jsonResponse(null, 'Berhasil logout.');
|
||||
}
|
||||
|
||||
// ── Endpoint tidak valid ─────────────────────────────────────
|
||||
jsonResponse(null, 'Endpoint tidak valid.', false, 404);
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// API: Dashboard Walikota — Data agregasi read-only
|
||||
// GET: walikota + admin only
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
if ($method !== 'GET') {
|
||||
jsonResponse(null, 'Hanya metode GET yang didukung.', false, 405);
|
||||
}
|
||||
|
||||
$user = requireRole(['admin', 'walikota']);
|
||||
|
||||
$tahun = isset($_GET['tahun']) ? (int)$_GET['tahun'] : (int)date('Y');
|
||||
|
||||
// ── 1. Total KK per kecamatan ─────────────────────────────────
|
||||
$stmt = $pdo->query("
|
||||
SELECT kecamatan, COUNT(*) AS jumlah
|
||||
FROM keluarga_miskin
|
||||
GROUP BY kecamatan
|
||||
ORDER BY FIELD(kecamatan,
|
||||
'Pontianak Utara','Pontianak Selatan','Pontianak Timur',
|
||||
'Pontianak Barat','Pontianak Tenggara','Pontianak Kota')
|
||||
");
|
||||
$kkPerKecamatan = $stmt->fetchAll();
|
||||
|
||||
// ── 2. KK yang SUDAH dapat bantuan tahun ini (dari histori_bantuan) ──
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT COUNT(DISTINCT kk_id) AS jumlah
|
||||
FROM histori_bantuan
|
||||
WHERE YEAR(tanggal_penyaluran) = ?
|
||||
");
|
||||
$stmt->execute([$tahun]);
|
||||
$kkSudahBantuan = (int)$stmt->fetch()['jumlah'];
|
||||
|
||||
// ── 3. Total KK ───────────────────────────────────────────────
|
||||
$stmt = $pdo->query("SELECT COUNT(*) AS jumlah FROM keluarga_miskin");
|
||||
$totalKK = (int)$stmt->fetch()['jumlah'];
|
||||
|
||||
$kkBelumBantuan = $totalKK - $kkSudahBantuan;
|
||||
|
||||
// ── 4. Distribusi status DTKS ────────────────────────────────
|
||||
$stmt = $pdo->query("
|
||||
SELECT status_dtks, COUNT(*) AS jumlah
|
||||
FROM keluarga_miskin
|
||||
GROUP BY status_dtks
|
||||
");
|
||||
$distribusiDtks = $stmt->fetchAll();
|
||||
|
||||
// ── 5. Distribusi riwayat penyakit ───────────────────────────
|
||||
$stmt = $pdo->query("SELECT riwayat_penyakit FROM keluarga_miskin WHERE riwayat_penyakit IS NOT NULL AND riwayat_penyakit != '[]'");
|
||||
$penyakitRows = $stmt->fetchAll();
|
||||
$penyakitCount = [];
|
||||
foreach ($penyakitRows as $row) {
|
||||
$arr = json_decode($row['riwayat_penyakit'], true) ?? [];
|
||||
foreach ($arr as $p) {
|
||||
$penyakitCount[$p] = ($penyakitCount[$p] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
arsort($penyakitCount);
|
||||
// Format jadi array [{ penyakit, jumlah }]
|
||||
$distribusiPenyakit = array_map(
|
||||
fn($k, $v) => ['penyakit' => $k, 'jumlah' => $v],
|
||||
array_keys($penyakitCount),
|
||||
array_values($penyakitCount)
|
||||
);
|
||||
|
||||
// ── 6. Laporan belum ditangani ────────────────────────────────
|
||||
$stmt = $pdo->query("SELECT COUNT(*) AS jumlah FROM laporan_masyarakat WHERE status = 'Baru'");
|
||||
$laporanBaru = (int)$stmt->fetch()['jumlah'];
|
||||
|
||||
// ── 7. Bantuan tersalurkan tahun ini per sumber dana ─────────
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT sumber_dana,
|
||||
COUNT(*) AS jumlah_entri,
|
||||
COUNT(DISTINCT kk_id) AS jumlah_kk,
|
||||
COALESCE(SUM(nominal), 0) AS total_nominal
|
||||
FROM histori_bantuan
|
||||
WHERE YEAR(tanggal_penyaluran) = ?
|
||||
GROUP BY sumber_dana
|
||||
ORDER BY total_nominal DESC
|
||||
");
|
||||
$stmt->execute([$tahun]);
|
||||
$bantuanPerSumber = $stmt->fetchAll();
|
||||
|
||||
// Format nominal di setiap baris
|
||||
foreach ($bantuanPerSumber as &$b) {
|
||||
$b['total_nominal_formatted'] = 'Rp ' . number_format((int)$b['total_nominal'], 0, ',', '.');
|
||||
}
|
||||
unset($b);
|
||||
|
||||
// ── 8. Bantuan tersalurkan tahun ini per jenis ───────────────
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT jenis_bantuan,
|
||||
COUNT(*) AS jumlah_entri,
|
||||
COUNT(DISTINCT kk_id) AS jumlah_kk,
|
||||
COALESCE(SUM(nominal), 0) AS total_nominal
|
||||
FROM histori_bantuan
|
||||
WHERE YEAR(tanggal_penyaluran) = ?
|
||||
GROUP BY jenis_bantuan
|
||||
ORDER BY total_nominal DESC
|
||||
");
|
||||
$stmt->execute([$tahun]);
|
||||
$bantuanPerJenis = $stmt->fetchAll();
|
||||
|
||||
foreach ($bantuanPerJenis as &$b) {
|
||||
$b['total_nominal_formatted'] = 'Rp ' . number_format((int)$b['total_nominal'], 0, ',', '.');
|
||||
}
|
||||
unset($b);
|
||||
|
||||
// ── 9. Total entri bantuan + total nominal tahun ini ─────────
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT COUNT(*) AS jumlah, COALESCE(SUM(nominal), 0) AS total_nominal
|
||||
FROM histori_bantuan WHERE YEAR(tanggal_penyaluran) = ?
|
||||
");
|
||||
$stmt->execute([$tahun]);
|
||||
$totalRow = $stmt->fetch();
|
||||
$totalEntri = (int)$totalRow['jumlah'];
|
||||
$totalNominal = (int)$totalRow['total_nominal'];
|
||||
$totalNominalFormatted = 'Rp ' . number_format($totalNominal, 0, ',', '.');
|
||||
|
||||
// ── 10. Distribusi tingkat kemiskinan ────────────────────────
|
||||
$stmt = $pdo->query("
|
||||
SELECT tingkat_kemiskinan, COUNT(*) AS jumlah
|
||||
FROM keluarga_miskin
|
||||
GROUP BY tingkat_kemiskinan
|
||||
");
|
||||
$distribusiKemiskinan = $stmt->fetchAll();
|
||||
|
||||
// ── Kirim semua data ──────────────────────────────────────────
|
||||
jsonResponse([
|
||||
'tahun' => $tahun,
|
||||
'total_kk' => $totalKK,
|
||||
'kk_per_kecamatan' => $kkPerKecamatan,
|
||||
'kk_sudah_bantuan' => $kkSudahBantuan,
|
||||
'kk_belum_bantuan' => $kkBelumBantuan,
|
||||
'distribusi_dtks' => $distribusiDtks,
|
||||
'distribusi_penyakit' => $distribusiPenyakit,
|
||||
'distribusi_kemiskinan' => $distribusiKemiskinan,
|
||||
'laporan_baru' => $laporanBaru,
|
||||
'bantuan_per_sumber' => $bantuanPerSumber,
|
||||
'bantuan_per_jenis' => $bantuanPerJenis,
|
||||
'total_entri_bantuan' => $totalEntri,
|
||||
'total_nominal_bantuan' => $totalNominal,
|
||||
'total_nominal_formatted'=> $totalNominalFormatted,
|
||||
], "Data dashboard tahun $tahun.");
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// API: Histori Bantuan
|
||||
// GET: publik | POST: admin+pengurus | DELETE: admin
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// Blokir aksi tulis walikota
|
||||
blockWalikotaWrite();
|
||||
|
||||
// Helper: format angka ke Rupiah Indonesia
|
||||
function formatRupiah(int $nominal): string {
|
||||
return 'Rp ' . number_format($nominal, 0, ',', '.');
|
||||
}
|
||||
|
||||
// ── GET: Ambil histori bantuan ──────────────────────────────
|
||||
if ($method === 'GET') {
|
||||
$kk_id = isset($_GET['kk_id']) ? (int)$_GET['kk_id'] : null;
|
||||
$jenis = clean($_GET['jenis_bantuan'] ?? '');
|
||||
$sumber = clean($_GET['sumber_dana'] ?? '');
|
||||
$tahun = isset($_GET['tahun']) ? (int)$_GET['tahun'] : null;
|
||||
|
||||
// ── Mode BULK: filter per jenis atau sumber (untuk drilldown walikota) ──
|
||||
if (!$kk_id && ($jenis || $sumber)) {
|
||||
requireRole(['admin', 'walikota']);
|
||||
|
||||
$sql = "
|
||||
SELECT hb.*, km.nama_kepala_keluarga, km.kecamatan, km.nik, km.alamat AS alamat_kk,
|
||||
u.nama AS nama_penanggung_jawab
|
||||
FROM histori_bantuan hb
|
||||
JOIN keluarga_miskin km ON hb.kk_id = km.id
|
||||
LEFT JOIN users u ON hb.penanggung_jawab = u.id
|
||||
WHERE 1=1
|
||||
";
|
||||
$params = [];
|
||||
|
||||
if ($jenis) { $sql .= " AND hb.jenis_bantuan = ?"; $params[] = $jenis; }
|
||||
if ($sumber) { $sql .= " AND hb.sumber_dana = ?"; $params[] = $sumber; }
|
||||
if ($tahun) { $sql .= " AND YEAR(hb.tanggal_penyaluran) = ?"; $params[] = $tahun; }
|
||||
$sql .= " ORDER BY hb.tanggal_penyaluran DESC";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$totalNominal = 0;
|
||||
foreach ($rows as &$r) {
|
||||
$r['nominal_formatted'] = formatRupiah((int)$r['nominal']);
|
||||
$totalNominal += (int)$r['nominal'];
|
||||
}
|
||||
unset($r);
|
||||
|
||||
jsonResponse($rows, count($rows) . ' data bantuan ditemukan.', true, 200, [
|
||||
'total_nominal' => $totalNominal,
|
||||
'total_nominal_formatted' => formatRupiah($totalNominal),
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Mode PER-KK (existing) ──────────────────────────────
|
||||
if (!$kk_id) {
|
||||
jsonResponse(null, 'Parameter kk_id wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT hb.*,
|
||||
u.nama AS nama_penanggung_jawab
|
||||
FROM histori_bantuan hb
|
||||
LEFT JOIN users u ON hb.penanggung_jawab = u.id
|
||||
WHERE hb.kk_id = ?
|
||||
ORDER BY hb.tanggal_penyaluran DESC
|
||||
");
|
||||
$stmt->execute([$kk_id]);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
foreach ($rows as &$r) {
|
||||
$r['nominal_formatted'] = formatRupiah((int)$r['nominal']);
|
||||
}
|
||||
unset($r);
|
||||
|
||||
$stmtTotal = $pdo->prepare("SELECT COALESCE(SUM(nominal), 0) AS total FROM histori_bantuan WHERE kk_id = ?");
|
||||
$stmtTotal->execute([$kk_id]);
|
||||
$totalNominal = (int)$stmtTotal->fetch()['total'];
|
||||
|
||||
jsonResponse($rows, 'Histori bantuan untuk KK ID ' . $kk_id . '.', true, 200, [
|
||||
'total_bantuan' => $totalNominal,
|
||||
'total_bantuan_formatted' => formatRupiah($totalNominal),
|
||||
]);
|
||||
}
|
||||
|
||||
// ── POST: Tambah histori bantuan (admin + pengurus) ──────────
|
||||
if ($method === 'POST') {
|
||||
$user = requireRole(['admin', 'pengurus']);
|
||||
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
|
||||
$kk_id = (int)($body['kk_id'] ?? 0);
|
||||
$jenis_bantuan = clean($body['jenis_bantuan'] ?? '');
|
||||
$nominal = (int)($body['nominal'] ?? 0);
|
||||
$keterangan = clean($body['keterangan'] ?? '');
|
||||
$tanggal_penyaluran = clean($body['tanggal_penyaluran'] ?? '');
|
||||
$sumber_dana = clean($body['sumber_dana'] ?? 'APBD');
|
||||
|
||||
if (!$kk_id || !$jenis_bantuan || !$tanggal_penyaluran) {
|
||||
jsonResponse(null, 'KK ID, jenis bantuan, dan tanggal penyaluran wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
if ($nominal < 0) {
|
||||
jsonResponse(null, 'Nominal tidak boleh negatif.', false, 400);
|
||||
}
|
||||
|
||||
// Validasi jenis_bantuan
|
||||
$validJenis = ['PKH', 'BPNT/Sembako', 'Beras Bansos', 'KIS-PBI', 'Bansos Tunai Pemkot', 'Rutilahu', 'PMT Stunting', 'CSR Swasta', 'Lainnya'];
|
||||
if (!in_array($jenis_bantuan, $validJenis)) {
|
||||
jsonResponse(null, 'Jenis bantuan tidak valid.', false, 400);
|
||||
}
|
||||
|
||||
// Validasi sumber_dana
|
||||
$validSumber = ['APBN', 'APBD', 'CSR', 'Lainnya'];
|
||||
if (!in_array($sumber_dana, $validSumber)) {
|
||||
jsonResponse(null, 'Sumber dana tidak valid.', false, 400);
|
||||
}
|
||||
|
||||
// Cek apakah KK ada
|
||||
$stmt = $pdo->prepare("SELECT id, rumah_ibadah_id FROM keluarga_miskin WHERE id = ? LIMIT 1");
|
||||
$stmt->execute([$kk_id]);
|
||||
$kk = $stmt->fetch();
|
||||
|
||||
if (!$kk) {
|
||||
jsonResponse(null, 'Data keluarga miskin tidak ditemukan.', false, 404);
|
||||
}
|
||||
|
||||
// Pengurus hanya bisa tambah bantuan untuk KK binaan miliknya
|
||||
if ($user['role'] === 'pengurus') {
|
||||
if ((int)($kk['rumah_ibadah_id'] ?? 0) !== (int)$user['rumah_ibadah_id']) {
|
||||
jsonResponse(null, 'Anda hanya dapat menambah bantuan untuk KK binaan rumah ibadah Anda.', false, 403);
|
||||
}
|
||||
}
|
||||
|
||||
// Cek duplikasi bantuan: PKH, BPNT/Sembako, Beras Bansos
|
||||
$duplikasiWarning = false;
|
||||
$bantuanPrioritas = ['PKH', 'BPNT/Sembako', 'Beras Bansos'];
|
||||
|
||||
if (in_array($jenis_bantuan, $bantuanPrioritas)) {
|
||||
$placeholders = implode(',', array_fill(0, count($bantuanPrioritas), '?'));
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT COUNT(*) AS cnt
|
||||
FROM histori_bantuan
|
||||
WHERE kk_id = ? AND jenis_bantuan IN ({$placeholders})
|
||||
");
|
||||
$stmt->execute(array_merge([$kk_id], $bantuanPrioritas));
|
||||
$cnt = (int)$stmt->fetch()['cnt'];
|
||||
if ($cnt > 0) {
|
||||
$duplikasiWarning = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Tentukan penanggung_jawab
|
||||
$penanggungJawab = $user['id'];
|
||||
if ($penanggungJawab === 0) {
|
||||
$stAdmin = $pdo->query("SELECT id FROM users WHERE role = 'admin' LIMIT 1");
|
||||
$admin = $stAdmin->fetch();
|
||||
$penanggungJawab = $admin ? (int)$admin['id'] : 1;
|
||||
}
|
||||
|
||||
// Simpan bantuan
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO histori_bantuan
|
||||
(kk_id, jenis_bantuan, nominal, keterangan, tanggal_penyaluran, sumber_dana, penanggung_jawab)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$kk_id, $jenis_bantuan, $nominal,
|
||||
$keterangan ?: null,
|
||||
$tanggal_penyaluran, $sumber_dana, $penanggungJawab,
|
||||
]);
|
||||
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
|
||||
$message = 'Histori bantuan berhasil ditambahkan.';
|
||||
if ($duplikasiWarning) {
|
||||
$message .= ' PERHATIAN: KK ini sudah memiliki bantuan serupa (PKH/BPNT/Beras Bansos).';
|
||||
}
|
||||
|
||||
jsonResponse(
|
||||
['id' => $newId],
|
||||
$message,
|
||||
true,
|
||||
200,
|
||||
['duplikasi_warning' => $duplikasiWarning]
|
||||
);
|
||||
}
|
||||
|
||||
// ── DELETE: Hapus histori bantuan (admin only) ───────────────
|
||||
if ($method === 'DELETE') {
|
||||
$user = requireRole(['admin']);
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
|
||||
if (!$id) {
|
||||
jsonResponse(null, 'ID histori bantuan wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM histori_bantuan WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
jsonResponse(null, 'Data histori bantuan tidak ditemukan.', false, 404);
|
||||
}
|
||||
|
||||
jsonResponse(null, 'Histori bantuan berhasil dihapus.');
|
||||
}
|
||||
|
||||
// ── Method tidak didukung ────────────────────────────────────
|
||||
jsonResponse(null, 'Method tidak didukung.', false, 405);
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// API: Keluarga Miskin (CRUD + Filter + Auto-assign)
|
||||
// GET: publik | POST: admin+pengurus | PUT: admin+pengurus | DELETE: admin
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// Blokir aksi tulis walikota
|
||||
blockWalikotaWrite();
|
||||
|
||||
// ── GET: Ambil semua (dengan filter) atau satu KK ────────────
|
||||
if ($method === 'GET') {
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
if ($id) {
|
||||
// Ambil satu KK beserta info rumah ibadah terkait
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT km.*, ri.nama AS nama_ri, ri.jenis AS jenis_ri, ri.kode_pengurus,
|
||||
COALESCE((SELECT SUM(hb.nominal) FROM histori_bantuan hb WHERE hb.kk_id = km.id), 0) AS total_bantuan
|
||||
FROM keluarga_miskin km
|
||||
LEFT JOIN rumah_ibadah ri ON km.rumah_ibadah_id = ri.id
|
||||
WHERE km.id = ?
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row) {
|
||||
jsonResponse(null, 'Data keluarga miskin tidak ditemukan.', false, 404);
|
||||
}
|
||||
|
||||
// Cek akses pengurus — hanya boleh lihat KK binaan miliknya
|
||||
$sess = $_SESSION['user'] ?? null;
|
||||
if ($sess && $sess['role'] === 'pengurus') {
|
||||
if ((int)($row['rumah_ibadah_id'] ?? 0) !== (int)$sess['rumah_ibadah_id']) {
|
||||
jsonResponse(null, 'Akses ditolak. KK ini bukan binaan Anda.', false, 403);
|
||||
}
|
||||
}
|
||||
|
||||
// Decode riwayat_penyakit JSON
|
||||
$row['riwayat_penyakit'] = json_decode($row['riwayat_penyakit'] ?? '[]', true);
|
||||
|
||||
// Flag eligible_bansos_tunai: hanya untuk KK dengan status_dtks = 'Belum DTKS'
|
||||
$row['eligible_bansos_tunai'] = ($row['status_dtks'] === 'Belum DTKS');
|
||||
|
||||
// Format total_bantuan
|
||||
$row['total_bantuan_formatted'] = 'Rp ' . number_format((int)$row['total_bantuan'], 0, ',', '.');
|
||||
|
||||
jsonResponse($row, 'Data keluarga miskin ditemukan.');
|
||||
}
|
||||
|
||||
// ── Ambil semua dengan filter ──
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
// Filter berdasarkan role pengurus
|
||||
$sess = $_SESSION['user'] ?? null;
|
||||
if ($sess && $sess['role'] === 'pengurus' && !empty($sess['rumah_ibadah_id'])) {
|
||||
$where[] = 'km.rumah_ibadah_id = ?';
|
||||
$params[] = (int)$sess['rumah_ibadah_id'];
|
||||
}
|
||||
|
||||
// Filter kecamatan
|
||||
if (!empty($_GET['kecamatan'])) {
|
||||
$where[] = 'km.kecamatan = ?';
|
||||
$params[] = clean($_GET['kecamatan']);
|
||||
}
|
||||
|
||||
// Filter tingkat_kemiskinan
|
||||
if (!empty($_GET['tingkat_kemiskinan'])) {
|
||||
$where[] = 'km.tingkat_kemiskinan = ?';
|
||||
$params[] = clean($_GET['tingkat_kemiskinan']);
|
||||
}
|
||||
|
||||
// Filter status_dtks
|
||||
if (!empty($_GET['status_dtks'])) {
|
||||
$where[] = 'km.status_dtks = ?';
|
||||
$params[] = clean($_GET['status_dtks']);
|
||||
}
|
||||
|
||||
// Filter rumah_ibadah_id
|
||||
if (isset($_GET['rumah_ibadah_id']) && $_GET['rumah_ibadah_id'] !== '') {
|
||||
$where[] = 'km.rumah_ibadah_id = ?';
|
||||
$params[] = (int)$_GET['rumah_ibadah_id'];
|
||||
}
|
||||
|
||||
$sql = "
|
||||
SELECT km.*, ri.nama AS nama_ri, ri.jenis AS jenis_ri,
|
||||
COALESCE((SELECT SUM(hb.nominal) FROM histori_bantuan hb WHERE hb.kk_id = km.id), 0) AS total_bantuan
|
||||
FROM keluarga_miskin km
|
||||
LEFT JOIN rumah_ibadah ri ON km.rumah_ibadah_id = ri.id
|
||||
";
|
||||
if (!empty($where)) {
|
||||
$sql .= ' WHERE ' . implode(' AND ', $where);
|
||||
}
|
||||
$sql .= ' ORDER BY km.nama_kepala_keluarga ASC';
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
// Proses setiap baris: decode JSON + flag eligible + format total
|
||||
foreach ($rows as &$r) {
|
||||
$r['riwayat_penyakit'] = json_decode($r['riwayat_penyakit'] ?? '[]', true);
|
||||
$r['eligible_bansos_tunai'] = ($r['status_dtks'] === 'Belum DTKS');
|
||||
$r['total_bantuan_formatted'] = 'Rp ' . number_format((int)$r['total_bantuan'], 0, ',', '.');
|
||||
}
|
||||
|
||||
jsonResponse($rows, 'Daftar keluarga miskin.', true, 200);
|
||||
}
|
||||
|
||||
// ── POST: Tambah KK (admin + pengurus) ───────────────────────
|
||||
if ($method === 'POST') {
|
||||
$user = requireRole(['admin', 'pengurus']);
|
||||
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
|
||||
// Validasi input
|
||||
$nik = clean($body['nik'] ?? '');
|
||||
$nama_kepala_keluarga = clean($body['nama_kepala_keluarga'] ?? '');
|
||||
$tanggal_lahir = clean($body['tanggal_lahir'] ?? '');
|
||||
$pendidikan = clean($body['pendidikan'] ?? 'Tidak Sekolah');
|
||||
$jumlah_anggota = (int)($body['jumlah_anggota'] ?? 1);
|
||||
$alamat = clean($body['alamat'] ?? '');
|
||||
$kecamatan = clean($body['kecamatan'] ?? '');
|
||||
$lat = (float)($body['lat'] ?? 0);
|
||||
$lng = (float)($body['lng'] ?? 0);
|
||||
$tingkat_kemiskinan = clean($body['tingkat_kemiskinan'] ?? 'Miskin');
|
||||
$status_dtks = clean($body['status_dtks'] ?? 'Belum DTKS');
|
||||
$riwayat_penyakit = $body['riwayat_penyakit'] ?? [];
|
||||
|
||||
if (!$nik || !$nama_kepala_keluarga || !$alamat || !$kecamatan || !$lat || !$lng) {
|
||||
jsonResponse(null, 'NIK, nama KK, alamat, kecamatan, dan koordinat wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
// Cek NIK unik
|
||||
$stmt = $pdo->prepare("SELECT id FROM keluarga_miskin WHERE nik = ? LIMIT 1");
|
||||
$stmt->execute([$nik]);
|
||||
if ($stmt->fetch()) {
|
||||
jsonResponse(null, 'NIK sudah terdaftar dalam sistem.', false, 400);
|
||||
}
|
||||
|
||||
// Auto-assign ke rumah ibadah terdekat dalam radius
|
||||
$assign = autoAssign($pdo, $lat, $lng);
|
||||
$riId = $assign['rumah_ibadah_id'];
|
||||
$jarakKm = $assign['jarak_km'];
|
||||
$riwayatJson = json_encode(is_array($riwayat_penyakit) ? $riwayat_penyakit : []);
|
||||
|
||||
// Pengurus: paksa assign ke RI miliknya (jika dalam radius)
|
||||
if ($user['role'] === 'pengurus') {
|
||||
$riId = (int)$user['rumah_ibadah_id'];
|
||||
// Hitung jarak ke RI miliknya
|
||||
$stRi = $pdo->prepare("SELECT lat, lng FROM rumah_ibadah WHERE id = ?");
|
||||
$stRi->execute([$riId]);
|
||||
$riData = $stRi->fetch();
|
||||
$jarakKm = $riData ? haversine($lat, $lng, (float)$riData['lat'], (float)$riData['lng']) : null;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO keluarga_miskin
|
||||
(nik, nama_kepala_keluarga, tanggal_lahir, pendidikan, jumlah_anggota,
|
||||
alamat, kecamatan, lat, lng, tingkat_kemiskinan, status_dtks,
|
||||
riwayat_penyakit, rumah_ibadah_id, jarak_ke_ibadah)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$nik, $nama_kepala_keluarga,
|
||||
$tanggal_lahir ?: null, $pendidikan, $jumlah_anggota,
|
||||
$alamat, $kecamatan, $lat, $lng,
|
||||
$tingkat_kemiskinan, $status_dtks,
|
||||
$riwayatJson, $riId, $jarakKm
|
||||
]);
|
||||
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
|
||||
$assignMsg = $riId
|
||||
? "Auto-assign ke rumah ibadah (jarak: {$jarakKm} km)."
|
||||
: "Tidak ada rumah ibadah dalam radius cakupan.";
|
||||
|
||||
jsonResponse([
|
||||
'id' => $newId,
|
||||
'rumah_ibadah_id' => $riId,
|
||||
'jarak_km' => $jarakKm,
|
||||
], 'Data keluarga miskin berhasil ditambahkan. ' . $assignMsg);
|
||||
}
|
||||
|
||||
// ── PUT: Update KK ───────────────────────────────────────────
|
||||
if ($method === 'PUT') {
|
||||
$user = requireRole(['admin', 'pengurus']);
|
||||
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
$id = (int)($body['id'] ?? 0);
|
||||
|
||||
if (!$id) {
|
||||
jsonResponse(null, 'ID keluarga miskin wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
// Cek data ada
|
||||
$stmt = $pdo->prepare("SELECT rumah_ibadah_id FROM keluarga_miskin WHERE id = ? LIMIT 1");
|
||||
$stmt->execute([$id]);
|
||||
$existing = $stmt->fetch();
|
||||
|
||||
if (!$existing) {
|
||||
jsonResponse(null, 'Data keluarga miskin tidak ditemukan.', false, 404);
|
||||
}
|
||||
|
||||
// Pengurus hanya bisa edit KK dalam RI-nya
|
||||
if ($user['role'] === 'pengurus') {
|
||||
if ((int)($existing['rumah_ibadah_id'] ?? 0) !== (int)$user['rumah_ibadah_id']) {
|
||||
jsonResponse(null, 'Anda hanya dapat mengedit KK binaan rumah ibadah Anda.', false, 403);
|
||||
}
|
||||
}
|
||||
|
||||
$nik = clean($body['nik'] ?? '');
|
||||
$nama_kepala_keluarga = clean($body['nama_kepala_keluarga'] ?? '');
|
||||
$tanggal_lahir = clean($body['tanggal_lahir'] ?? '');
|
||||
$pendidikan = clean($body['pendidikan'] ?? 'Tidak Sekolah');
|
||||
$jumlah_anggota = (int)($body['jumlah_anggota'] ?? 1);
|
||||
$alamat = clean($body['alamat'] ?? '');
|
||||
$kecamatan = clean($body['kecamatan'] ?? '');
|
||||
$lat = (float)($body['lat'] ?? 0);
|
||||
$lng = (float)($body['lng'] ?? 0);
|
||||
$tingkat_kemiskinan = clean($body['tingkat_kemiskinan'] ?? 'Miskin');
|
||||
$status_dtks = clean($body['status_dtks'] ?? 'Belum DTKS');
|
||||
$riwayat_penyakit = $body['riwayat_penyakit'] ?? [];
|
||||
|
||||
if (!$nik || !$nama_kepala_keluarga || !$alamat || !$kecamatan || !$lat || !$lng) {
|
||||
jsonResponse(null, 'Semua field wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
// Cek NIK unik (kecuali milik sendiri)
|
||||
$stmt = $pdo->prepare("SELECT id FROM keluarga_miskin WHERE nik = ? AND id != ? LIMIT 1");
|
||||
$stmt->execute([$nik, $id]);
|
||||
if ($stmt->fetch()) {
|
||||
jsonResponse(null, 'NIK sudah digunakan oleh KK lain.', false, 400);
|
||||
}
|
||||
|
||||
// Re-calculate auto-assign
|
||||
$assign = autoAssign($pdo, $lat, $lng);
|
||||
$riId = $assign['rumah_ibadah_id'];
|
||||
$jarakKm = $assign['jarak_km'];
|
||||
$riwayatJson = json_encode(is_array($riwayat_penyakit) ? $riwayat_penyakit : []);
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE keluarga_miskin SET
|
||||
nik = ?, nama_kepala_keluarga = ?, tanggal_lahir = ?, pendidikan = ?,
|
||||
jumlah_anggota = ?, alamat = ?, kecamatan = ?, lat = ?, lng = ?,
|
||||
tingkat_kemiskinan = ?, status_dtks = ?, riwayat_penyakit = ?,
|
||||
rumah_ibadah_id = ?, jarak_ke_ibadah = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([
|
||||
$nik, $nama_kepala_keluarga,
|
||||
$tanggal_lahir ?: null, $pendidikan, $jumlah_anggota,
|
||||
$alamat, $kecamatan, $lat, $lng,
|
||||
$tingkat_kemiskinan, $status_dtks, $riwayatJson,
|
||||
$riId, $jarakKm, $id
|
||||
]);
|
||||
|
||||
jsonResponse([
|
||||
'rumah_ibadah_id' => $riId,
|
||||
'jarak_km' => $jarakKm,
|
||||
], 'Data keluarga miskin berhasil diperbarui.');
|
||||
}
|
||||
|
||||
// ── DELETE: Hapus KK (admin only) ────────────────────────────
|
||||
if ($method === 'DELETE') {
|
||||
$user = requireRole(['admin']);
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
|
||||
if (!$id) {
|
||||
jsonResponse(null, 'ID keluarga miskin wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM keluarga_miskin WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
jsonResponse(null, 'Data tidak ditemukan.', false, 404);
|
||||
}
|
||||
|
||||
jsonResponse(null, 'Data keluarga miskin berhasil dihapus.');
|
||||
}
|
||||
|
||||
// ── Method tidak didukung ────────────────────────────────────
|
||||
jsonResponse(null, 'Method tidak didukung.', false, 405);
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// API: Laporan Masyarakat
|
||||
// POST: publik (tanpa login, rate-limited) | GET: admin+walikota | PUT: admin
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// ── POST: Submit laporan baru (tanpa auth, dengan rate limit) ─
|
||||
if ($method === 'POST') {
|
||||
// Rate limit: maks 5 submit per jam per IP
|
||||
$ip = getClientIP();
|
||||
if (!checkRateLimit($pdo, $ip)) {
|
||||
jsonResponse(null, 'Anda telah mencapai batas maksimal 5 laporan per jam. Silakan coba lagi nanti.', false, 429);
|
||||
}
|
||||
|
||||
// Ambil data dari form multipart (karena ada file upload)
|
||||
$nama_terlapor = clean($_POST['nama_terlapor'] ?? '');
|
||||
$nik_terlapor = clean($_POST['nik_terlapor'] ?? '');
|
||||
$nama_pelapor = clean($_POST['nama_pelapor'] ?? '');
|
||||
$no_hp = clean($_POST['no_hp'] ?? '');
|
||||
$alamat_terlapor = clean($_POST['alamat_terlapor'] ?? '');
|
||||
$lat = (float)($_POST['lat'] ?? 0);
|
||||
$lng = (float)($_POST['lng'] ?? 0);
|
||||
$deskripsi = clean($_POST['deskripsi'] ?? '');
|
||||
|
||||
if (!$alamat_terlapor || !$lat || !$lng || !$deskripsi) {
|
||||
jsonResponse(null, 'Alamat, koordinat, dan deskripsi wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
// Validasi NIK jika diisi (harus 16 digit angka)
|
||||
if ($nik_terlapor && !preg_match('/^\d{16}$/', $nik_terlapor)) {
|
||||
jsonResponse(null, 'NIK harus 16 digit angka.', false, 400);
|
||||
}
|
||||
|
||||
// Handle upload foto (opsional)
|
||||
$fotoPath = handleUpload('foto');
|
||||
|
||||
// Simpan laporan
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO laporan_masyarakat
|
||||
(nama_terlapor, nik_terlapor, nama_pelapor, no_hp, alamat_terlapor, lat, lng, deskripsi, foto, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'Baru')
|
||||
");
|
||||
$stmt->execute([
|
||||
$nama_terlapor ?: null,
|
||||
$nik_terlapor ?: null,
|
||||
$nama_pelapor ?: null,
|
||||
$no_hp ?: null,
|
||||
$alamat_terlapor,
|
||||
$lat, $lng,
|
||||
$deskripsi,
|
||||
$fotoPath
|
||||
]);
|
||||
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
|
||||
jsonResponse(['id' => $newId], 'Laporan berhasil dikirim. Terima kasih atas partisipasi Anda.');
|
||||
}
|
||||
|
||||
// ── GET: Daftar laporan (admin & walikota) ───────────────────
|
||||
if ($method === 'GET') {
|
||||
$user = requireRole(['admin', 'walikota']);
|
||||
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT lm.*,
|
||||
u.nama AS nama_penangan,
|
||||
kk.nama_kepala_keluarga AS nama_kk_converted
|
||||
FROM laporan_masyarakat lm
|
||||
LEFT JOIN users u ON lm.ditangani_oleh = u.id
|
||||
LEFT JOIN keluarga_miskin kk ON lm.converted_to_kk_id = kk.id
|
||||
WHERE lm.id = ?
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row) {
|
||||
jsonResponse(null, 'Laporan tidak ditemukan.', false, 404);
|
||||
}
|
||||
|
||||
jsonResponse($row, 'Detail laporan.');
|
||||
}
|
||||
|
||||
// Filter status
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if (!empty($_GET['status'])) {
|
||||
$where[] = 'lm.status = ?';
|
||||
$params[] = clean($_GET['status']);
|
||||
}
|
||||
|
||||
$sql = "
|
||||
SELECT lm.*,
|
||||
u.nama AS nama_penangan,
|
||||
kk.nama_kepala_keluarga AS nama_kk_converted
|
||||
FROM laporan_masyarakat lm
|
||||
LEFT JOIN users u ON lm.ditangani_oleh = u.id
|
||||
LEFT JOIN keluarga_miskin kk ON lm.converted_to_kk_id = kk.id
|
||||
";
|
||||
if (!empty($where)) {
|
||||
$sql .= ' WHERE ' . implode(' AND ', $where);
|
||||
}
|
||||
$sql .= ' ORDER BY lm.created_at DESC';
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
jsonResponse($rows, 'Daftar laporan masyarakat.');
|
||||
}
|
||||
|
||||
// ── PUT: Update status laporan (admin only) ──────────────────
|
||||
if ($method === 'PUT') {
|
||||
$user = requireRole(['admin']);
|
||||
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
|
||||
$id = (int)($body['id'] ?? 0);
|
||||
$status = clean($body['status'] ?? '');
|
||||
$alasan_tolak = clean($body['alasan_tolak'] ?? '');
|
||||
$converted_to_kk = isset($body['converted_to_kk_id']) ? (int)$body['converted_to_kk_id'] : null;
|
||||
|
||||
if (!$id || !$status) {
|
||||
jsonResponse(null, 'ID dan status wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
$validStatus = ['Baru', 'Diverifikasi', 'Ditolak'];
|
||||
if (!in_array($status, $validStatus)) {
|
||||
jsonResponse(null, 'Status tidak valid. Pilih: Baru, Diverifikasi, atau Ditolak.', false, 400);
|
||||
}
|
||||
|
||||
// Jika ditolak, wajib ada alasan
|
||||
if ($status === 'Ditolak' && empty($alasan_tolak)) {
|
||||
jsonResponse(null, 'Alasan penolakan wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
// Update laporan
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE laporan_masyarakat SET
|
||||
status = ?,
|
||||
alasan_tolak = ?,
|
||||
ditangani_oleh = ?,
|
||||
converted_to_kk_id = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([
|
||||
$status,
|
||||
$status === 'Ditolak' ? $alasan_tolak : null,
|
||||
$user['id'],
|
||||
$status === 'Diverifikasi' ? $converted_to_kk : null,
|
||||
$id
|
||||
]);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
jsonResponse(null, 'Laporan tidak ditemukan.', false, 404);
|
||||
}
|
||||
|
||||
jsonResponse(null, 'Status laporan berhasil diperbarui ke: ' . $status);
|
||||
}
|
||||
|
||||
// ── Method tidak didukung ────────────────────────────────────
|
||||
jsonResponse(null, 'Method tidak didukung.', false, 405);
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// API: Rumah Ibadah (CRUD)
|
||||
// GET: publik | POST: admin | PUT: admin+pengurus | DELETE: admin
|
||||
// ============================================================
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// Blokir aksi tulis walikota
|
||||
blockWalikotaWrite();
|
||||
|
||||
// ── GET: Ambil semua atau satu rumah ibadah ──────────────────
|
||||
if ($method === 'GET') {
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
if ($id) {
|
||||
// Ambil satu rumah ibadah beserta jumlah KK binaan
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT ri.*,
|
||||
(SELECT COUNT(*) FROM keluarga_miskin km WHERE km.rumah_ibadah_id = ri.id) AS jumlah_kk_binaan
|
||||
FROM rumah_ibadah ri
|
||||
WHERE ri.id = ?
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row) {
|
||||
jsonResponse(null, 'Rumah ibadah tidak ditemukan.', false, 404);
|
||||
}
|
||||
|
||||
// Sembunyikan password_pengurus hash dari response
|
||||
unset($row['password_pengurus']);
|
||||
|
||||
jsonResponse($row, 'Data rumah ibadah ditemukan.');
|
||||
}
|
||||
|
||||
// Ambil semua — pengurus hanya melihat miliknya
|
||||
$sess = $_SESSION['user'] ?? null;
|
||||
if ($sess && $sess['role'] === 'pengurus' && !empty($sess['rumah_ibadah_id'])) {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT ri.*,
|
||||
(SELECT COUNT(*) FROM keluarga_miskin km WHERE km.rumah_ibadah_id = ri.id) AS jumlah_kk_binaan
|
||||
FROM rumah_ibadah ri
|
||||
WHERE ri.id = ?
|
||||
ORDER BY ri.nama ASC
|
||||
");
|
||||
$stmt->execute([$sess['rumah_ibadah_id']]);
|
||||
} else {
|
||||
$stmt = $pdo->query("
|
||||
SELECT ri.*,
|
||||
(SELECT COUNT(*) FROM keluarga_miskin km WHERE km.rumah_ibadah_id = ri.id) AS jumlah_kk_binaan
|
||||
FROM rumah_ibadah ri
|
||||
ORDER BY ri.nama ASC
|
||||
");
|
||||
}
|
||||
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
// Sembunyikan password hash, tapi tampilkan kode_pengurus
|
||||
foreach ($rows as &$r) {
|
||||
unset($r['password_pengurus']);
|
||||
}
|
||||
|
||||
jsonResponse($rows, 'Daftar rumah ibadah.', true, 200);
|
||||
}
|
||||
|
||||
// ── POST: Tambah rumah ibadah (admin only) ───────────────────
|
||||
if ($method === 'POST') {
|
||||
$user = requireRole(['admin']);
|
||||
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
|
||||
// Validasi input wajib
|
||||
$nama = clean($body['nama'] ?? '');
|
||||
$jenis = clean($body['jenis'] ?? '');
|
||||
$alamat = clean($body['alamat'] ?? '');
|
||||
$lat = (float)($body['lat'] ?? 0);
|
||||
$lng = (float)($body['lng'] ?? 0);
|
||||
$radius_km = (float)($body['radius_km'] ?? 2.0);
|
||||
$kecamatan = clean($body['kecamatan'] ?? '');
|
||||
|
||||
if (!$nama || !$jenis || !$alamat || !$lat || !$lng || !$kecamatan) {
|
||||
jsonResponse(null, 'Nama, jenis, alamat, koordinat, dan kecamatan wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
// Validasi jenis
|
||||
$validJenis = ['Masjid', 'Gereja', 'Pura', 'Vihara', 'Klenteng', 'Lainnya'];
|
||||
if (!in_array($jenis, $validJenis)) {
|
||||
jsonResponse(null, 'Jenis rumah ibadah tidak valid.', false, 400);
|
||||
}
|
||||
|
||||
// Validasi kecamatan
|
||||
$validKecamatan = ['Pontianak Utara', 'Pontianak Selatan', 'Pontianak Timur', 'Pontianak Barat', 'Pontianak Tenggara', 'Pontianak Kota'];
|
||||
if (!in_array($kecamatan, $validKecamatan)) {
|
||||
jsonResponse(null, 'Kecamatan tidak valid.', false, 400);
|
||||
}
|
||||
|
||||
// Generate kode pengurus dan password
|
||||
$kodePengurus = generateKodePengurus($pdo, $jenis);
|
||||
$passPlain = substr(bin2hex(random_bytes(4)), 0, 8); // 8 karakter acak
|
||||
$passHash = password_hash($passPlain, PASSWORD_DEFAULT);
|
||||
|
||||
// Simpan ke database
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO rumah_ibadah (nama, jenis, alamat, lat, lng, radius_km, kode_pengurus, password_pengurus, kecamatan)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([$nama, $jenis, $alamat, $lat, $lng, $radius_km, $kodePengurus, $passHash, $kecamatan]);
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
|
||||
// Re-assign semua KK setelah RI baru ditambahkan
|
||||
reassignAllKK($pdo);
|
||||
|
||||
jsonResponse([
|
||||
'id' => $newId,
|
||||
'kode_pengurus' => $kodePengurus,
|
||||
'password_pengurus' => $passPlain,
|
||||
], 'Rumah ibadah berhasil ditambahkan. Kode pengurus: ' . $kodePengurus);
|
||||
}
|
||||
|
||||
// ── PUT: Update rumah ibadah ─────────────────────────────────
|
||||
if ($method === 'PUT') {
|
||||
$user = requireRole(['admin', 'pengurus']);
|
||||
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
$id = (int)($body['id'] ?? 0);
|
||||
|
||||
if (!$id) {
|
||||
jsonResponse(null, 'ID rumah ibadah wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
// Pengurus hanya bisa edit RI miliknya sendiri
|
||||
if ($user['role'] === 'pengurus' && (int)$user['rumah_ibadah_id'] !== $id) {
|
||||
jsonResponse(null, 'Anda hanya dapat mengedit rumah ibadah Anda sendiri.', false, 403);
|
||||
}
|
||||
|
||||
$nama = clean($body['nama'] ?? '');
|
||||
$jenis = clean($body['jenis'] ?? '');
|
||||
$alamat = clean($body['alamat'] ?? '');
|
||||
$lat = (float)($body['lat'] ?? 0);
|
||||
$lng = (float)($body['lng'] ?? 0);
|
||||
$radius_km = (float)($body['radius_km'] ?? 2.0);
|
||||
$kecamatan = clean($body['kecamatan'] ?? '');
|
||||
|
||||
if (!$nama || !$jenis || !$alamat || !$lat || !$lng || !$kecamatan) {
|
||||
jsonResponse(null, 'Semua field wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE rumah_ibadah
|
||||
SET nama = ?, jenis = ?, alamat = ?, lat = ?, lng = ?, radius_km = ?, kecamatan = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$nama, $jenis, $alamat, $lat, $lng, $radius_km, $kecamatan, $id]);
|
||||
|
||||
// Re-assign semua KK setelah radius/posisi berubah
|
||||
reassignAllKK($pdo);
|
||||
|
||||
jsonResponse(null, 'Rumah ibadah berhasil diperbarui.');
|
||||
}
|
||||
|
||||
// ── DELETE: Hapus rumah ibadah (admin only) ──────────────────
|
||||
if ($method === 'DELETE') {
|
||||
$user = requireRole(['admin']);
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
|
||||
if (!$id) {
|
||||
jsonResponse(null, 'ID rumah ibadah wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM rumah_ibadah WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
jsonResponse(null, 'Rumah ibadah tidak ditemukan.', false, 404);
|
||||
}
|
||||
|
||||
// Re-assign semua KK setelah RI dihapus
|
||||
reassignAllKK($pdo);
|
||||
|
||||
jsonResponse(null, 'Rumah ibadah berhasil dihapus.');
|
||||
}
|
||||
|
||||
// ── Helper: Re-assign semua KK ke RI terdekat ────────────────
|
||||
function reassignAllKK(PDO $pdo): void {
|
||||
$rows = $pdo->query("SELECT id, lat, lng FROM keluarga_miskin")->fetchAll();
|
||||
|
||||
foreach ($rows as $kk) {
|
||||
$assign = autoAssign($pdo, (float)$kk['lat'], (float)$kk['lng']);
|
||||
|
||||
if ($assign['rumah_ibadah_id'] !== null) {
|
||||
$stmt = $pdo->prepare("UPDATE keluarga_miskin SET rumah_ibadah_id = ?, jarak_ke_ibadah = ? WHERE id = ?");
|
||||
$stmt->execute([$assign['rumah_ibadah_id'], $assign['jarak_km'], $kk['id']]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("UPDATE keluarga_miskin SET rumah_ibadah_id = NULL, jarak_ke_ibadah = NULL WHERE id = ?");
|
||||
$stmt->execute([$kk['id']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── PATCH: Reset password pengurus (admin only) ──────────────
|
||||
if ($method === 'PATCH') {
|
||||
$user = requireRole(['admin']);
|
||||
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
$id = (int)($body['id'] ?? 0);
|
||||
|
||||
if (!$id) {
|
||||
jsonResponse(null, 'ID rumah ibadah wajib diisi.', false, 400);
|
||||
}
|
||||
|
||||
// Pastikan RI ada
|
||||
$stmt = $pdo->prepare("SELECT id, nama, kode_pengurus FROM rumah_ibadah WHERE id = ? LIMIT 1");
|
||||
$stmt->execute([$id]);
|
||||
$ri = $stmt->fetch();
|
||||
|
||||
if (!$ri) {
|
||||
jsonResponse(null, 'Rumah ibadah tidak ditemukan.', false, 404);
|
||||
}
|
||||
|
||||
// Generate password baru
|
||||
$passPlain = substr(bin2hex(random_bytes(4)), 0, 8); // 8 karakter acak
|
||||
$passHash = password_hash($passPlain, PASSWORD_DEFAULT);
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE rumah_ibadah SET password_pengurus = ? WHERE id = ?");
|
||||
$stmt->execute([$passHash, $id]);
|
||||
|
||||
jsonResponse([
|
||||
'id' => $id,
|
||||
'kode_pengurus' => $ri['kode_pengurus'],
|
||||
'password_pengurus' => $passPlain,
|
||||
], 'Password pengurus berhasil direset untuk: ' . $ri['nama']);
|
||||
}
|
||||
|
||||
// ── Method tidak didukung ────────────────────────────────────
|
||||
jsonResponse(null, 'Method tidak didukung.', false, 405);
|
||||
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// Konfigurasi Koneksi Database & Fungsi Utilitas
|
||||
// WebGIS Pontianak — Pengentasan Kemiskinan Berbasis Rumah Ibadah
|
||||
// ============================================================
|
||||
|
||||
// Konfigurasi database
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', 'webgis_rumah_ibadah');
|
||||
define('DB_USER', 'root');
|
||||
define('DB_PASS', '');
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
|
||||
// Direktori upload foto laporan
|
||||
define('UPLOAD_DIR', __DIR__ . '/uploads/');
|
||||
|
||||
// Mulai session
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// ── Koneksi PDO ──────────────────────────────────────────────
|
||||
try {
|
||||
$pdo = new PDO(
|
||||
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET,
|
||||
DB_USER,
|
||||
DB_PASS,
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]
|
||||
);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'data' => null,
|
||||
'message' => 'Koneksi database gagal: ' . $e->getMessage()
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── CORS Headers ─────────────────────────────────────────────
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
// Handle preflight request
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FUNGSI UTILITAS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Kirim respon JSON standar lalu hentikan eksekusi.
|
||||
* Format: {"success": true/false, "data": ..., "message": "..."}
|
||||
*/
|
||||
function jsonResponse($data = null, string $message = '', bool $success = true, int $httpCode = 200, array $extra = []): void {
|
||||
http_response_code($httpCode);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$response = [
|
||||
'success' => $success,
|
||||
'data' => $data,
|
||||
'message' => $message
|
||||
];
|
||||
// Gabungkan field tambahan (misal: duplikasi_warning)
|
||||
if (!empty($extra)) {
|
||||
$response = array_merge($response, $extra);
|
||||
}
|
||||
echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bersihkan input string dari tag HTML.
|
||||
*/
|
||||
function clean(string $value): string {
|
||||
return htmlspecialchars(strip_tags(trim($value)), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hitung jarak Haversine antara dua koordinat dalam kilometer.
|
||||
*/
|
||||
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
|
||||
$R = 6371; // Radius bumi dalam km
|
||||
$dLat = deg2rad($lat2 - $lat1);
|
||||
$dLng = deg2rad($lng2 - $lng1);
|
||||
$a = sin($dLat / 2) * sin($dLat / 2) +
|
||||
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
|
||||
sin($dLng / 2) * sin($dLng / 2);
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
return round($R * $c, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-assign: cari rumah ibadah terdekat yang radius-nya mencakup lokasi KK.
|
||||
* Mengembalikan ['rumah_ibadah_id' => int|null, 'jarak_km' => float|null]
|
||||
*/
|
||||
function autoAssign(PDO $pdo, float $lat, float $lng): array {
|
||||
$result = ['rumah_ibadah_id' => null, 'jarak_km' => null];
|
||||
|
||||
$stmt = $pdo->query("SELECT id, lat, lng, radius_km FROM rumah_ibadah");
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$bestDist = PHP_FLOAT_MAX;
|
||||
$bestId = null;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$dist = haversine($lat, $lng, (float)$row['lat'], (float)$row['lng']);
|
||||
// Hanya assign jika KK berada dalam radius RI dan merupakan yang terdekat
|
||||
if ($dist <= (float)$row['radius_km'] && $dist < $bestDist) {
|
||||
$bestDist = $dist;
|
||||
$bestId = (int)$row['id'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($bestId !== null) {
|
||||
$result['rumah_ibadah_id'] = $bestId;
|
||||
$result['jarak_km'] = $bestDist;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah user sudah login. Jika belum, kirim 401.
|
||||
* Mengembalikan data session user.
|
||||
*/
|
||||
function requireLogin(): array {
|
||||
if (empty($_SESSION['user'])) {
|
||||
jsonResponse(null, 'Tidak diizinkan. Silakan login terlebih dahulu.', false, 401);
|
||||
}
|
||||
return $_SESSION['user'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah role user termasuk dalam daftar yang diizinkan.
|
||||
* Jika tidak, kirim 403.
|
||||
*/
|
||||
function requireRole(array $allowedRoles): array {
|
||||
$user = requireLogin();
|
||||
if (!in_array($user['role'], $allowedRoles)) {
|
||||
jsonResponse(null, 'Akses ditolak. Role Anda tidak memiliki izin.', false, 403);
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blokir aksi tulis (POST/PUT/DELETE) untuk role walikota.
|
||||
* Walikota hanya boleh GET.
|
||||
*/
|
||||
function blockWalikotaWrite(): void {
|
||||
if (!empty($_SESSION['user']) && $_SESSION['user']['role'] === 'walikota') {
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
if (in_array($method, ['POST', 'PUT', 'DELETE'])) {
|
||||
jsonResponse(null, 'Walikota hanya memiliki akses baca (read-only).', false, 403);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate kode pengurus dengan format: RI-[JENIS 3 HURUF]-[RANDOM 4 DIGIT]
|
||||
* Contoh: RI-MSJ-4821, RI-GRJ-7153
|
||||
*/
|
||||
function generateKodePengurus(PDO $pdo, string $jenis): string {
|
||||
// Mapping jenis ke 3 huruf
|
||||
$map = [
|
||||
'Masjid' => 'MSJ',
|
||||
'Gereja' => 'GRJ',
|
||||
'Pura' => 'PRA',
|
||||
'Vihara' => 'VHR',
|
||||
'Klenteng' => 'KLT',
|
||||
'Lainnya' => 'LNY',
|
||||
];
|
||||
$prefix = $map[$jenis] ?? 'LNY';
|
||||
|
||||
// Coba generate kode unik (maks 100 percobaan)
|
||||
for ($i = 0; $i < 100; $i++) {
|
||||
$angka = str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT);
|
||||
$kode = "RI-{$prefix}-{$angka}";
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id FROM rumah_ibadah WHERE kode_pengurus = ? LIMIT 1");
|
||||
$stmt->execute([$kode]);
|
||||
if ($stmt->rowCount() === 0) {
|
||||
return $kode;
|
||||
}
|
||||
}
|
||||
// Fallback: tambahkan timestamp
|
||||
return "RI-{$prefix}-" . substr(time(), -4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek rate limit untuk IP tertentu. Maks 5 submit per jam.
|
||||
* Mengembalikan true jika masih diizinkan, false jika melebihi batas.
|
||||
*/
|
||||
function checkRateLimit(PDO $pdo, string $ip): bool {
|
||||
// Hapus record yang lebih dari 1 jam
|
||||
$pdo->prepare("DELETE FROM rate_limit WHERE created_at < NOW() - INTERVAL 1 HOUR")->execute();
|
||||
|
||||
// Hitung jumlah submit dalam 1 jam terakhir
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) as cnt FROM rate_limit WHERE ip_address = ? AND created_at >= NOW() - INTERVAL 1 HOUR");
|
||||
$stmt->execute([$ip]);
|
||||
$count = (int)$stmt->fetch()['cnt'];
|
||||
|
||||
if ($count >= 5) {
|
||||
return false; // Sudah melebihi batas
|
||||
}
|
||||
|
||||
// Catat submit baru
|
||||
$stmt = $pdo->prepare("INSERT INTO rate_limit (ip_address) VALUES (?)");
|
||||
$stmt->execute([$ip]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle upload file foto. Mengembalikan path relatif file atau null.
|
||||
*/
|
||||
function handleUpload(string $fieldName): ?string {
|
||||
if (!isset($_FILES[$fieldName]) || $_FILES[$fieldName]['error'] !== UPLOAD_ERR_OK) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$file = $_FILES[$fieldName];
|
||||
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
$maxSize = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
if (!in_array($file['type'], $allowedTypes)) {
|
||||
jsonResponse(null, 'Tipe file tidak diizinkan. Gunakan JPG, PNG, GIF, atau WebP.', false, 400);
|
||||
}
|
||||
|
||||
if ($file['size'] > $maxSize) {
|
||||
jsonResponse(null, 'Ukuran file melebihi batas 5MB.', false, 400);
|
||||
}
|
||||
|
||||
// Buat direktori upload jika belum ada
|
||||
if (!is_dir(UPLOAD_DIR)) {
|
||||
mkdir(UPLOAD_DIR, 0755, true);
|
||||
}
|
||||
|
||||
// Generate nama file unik
|
||||
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||
$filename = 'laporan_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
|
||||
$dest = UPLOAD_DIR . $filename;
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $dest)) {
|
||||
jsonResponse(null, 'Gagal menyimpan file foto.', false, 500);
|
||||
}
|
||||
|
||||
return 'uploads/' . $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dapatkan IP address client (mendukung proxy).
|
||||
*/
|
||||
function getClientIP(): string {
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
return explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
|
||||
}
|
||||
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
-- ============================================================
|
||||
-- DATABASE: webgis_rumah_ibadah
|
||||
-- WebGIS Pontianak — Pengentasan Kemiskinan Berbasis Rumah Ibadah
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis_rumah_ibadah
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE webgis_rumah_ibadah;
|
||||
|
||||
-- Hapus tabel lama (urutan sesuai dependensi FK)
|
||||
DROP TABLE IF EXISTS histori_bantuan;
|
||||
DROP TABLE IF EXISTS laporan_masyarakat;
|
||||
DROP TABLE IF EXISTS keluarga_miskin;
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP TABLE IF EXISTS rumah_ibadah;
|
||||
DROP TABLE IF EXISTS rate_limit;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL: rumah_ibadah
|
||||
-- Menyimpan data titik lokasi rumah ibadah beserta radius cakupan
|
||||
-- ============================================================
|
||||
CREATE TABLE rumah_ibadah (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(200) NOT NULL COMMENT 'Nama rumah ibadah',
|
||||
jenis ENUM('Masjid','Gereja','Pura','Vihara','Klenteng','Lainnya')
|
||||
NOT NULL COMMENT 'Jenis rumah ibadah',
|
||||
alamat TEXT NOT NULL COMMENT 'Alamat lengkap',
|
||||
lat DECIMAL(10,7) NOT NULL COMMENT 'Latitude',
|
||||
lng DECIMAL(10,7) NOT NULL COMMENT 'Longitude',
|
||||
radius_km FLOAT NOT NULL DEFAULT 2.0 COMMENT 'Radius cakupan (km)',
|
||||
kode_pengurus VARCHAR(20) NULL UNIQUE COMMENT 'Kode unik login pengurus, format RI-XXX-9999',
|
||||
password_pengurus VARCHAR(255) NULL COMMENT 'Password pengurus (hashed)',
|
||||
kecamatan ENUM('Pontianak Utara','Pontianak Selatan','Pontianak Timur',
|
||||
'Pontianak Barat','Pontianak Tenggara','Pontianak Kota')
|
||||
NOT NULL COMMENT 'Kecamatan lokasi',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL: users
|
||||
-- Manajemen pengguna dengan 3 role: admin, pengurus, walikota
|
||||
-- ============================================================
|
||||
CREATE TABLE users (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(150) NOT NULL COMMENT 'Nama lengkap pengguna',
|
||||
username VARCHAR(100) NOT NULL UNIQUE COMMENT 'Username login',
|
||||
password VARCHAR(255) NOT NULL COMMENT 'Password ter-hash (bcrypt)',
|
||||
role ENUM('admin','pengurus','walikota')
|
||||
NOT NULL DEFAULT 'pengurus' COMMENT 'Role pengguna',
|
||||
rumah_ibadah_id INT DEFAULT NULL COMMENT 'FK ke rumah_ibadah (hanya untuk pengurus)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL: keluarga_miskin
|
||||
-- Data keluarga miskin dengan auto-assign ke rumah ibadah terdekat
|
||||
-- ============================================================
|
||||
CREATE TABLE keluarga_miskin (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
nik VARCHAR(20) NOT NULL UNIQUE COMMENT 'NIK Kepala Keluarga',
|
||||
nama_kepala_keluarga VARCHAR(200) NOT NULL COMMENT 'Nama Kepala Keluarga',
|
||||
tanggal_lahir DATE NULL COMMENT 'Tanggal lahir KK',
|
||||
pendidikan ENUM('Tidak Sekolah','SD','SMP','SMA','D3','S1','S2/S3')
|
||||
NOT NULL DEFAULT 'Tidak Sekolah',
|
||||
jumlah_anggota INT NOT NULL DEFAULT 1 COMMENT 'Jumlah anggota keluarga',
|
||||
alamat TEXT NOT NULL COMMENT 'Alamat lengkap',
|
||||
kecamatan ENUM('Pontianak Utara','Pontianak Selatan','Pontianak Timur',
|
||||
'Pontianak Barat','Pontianak Tenggara','Pontianak Kota')
|
||||
NOT NULL,
|
||||
lat DECIMAL(10,7) NOT NULL,
|
||||
lng DECIMAL(10,7) NOT NULL,
|
||||
tingkat_kemiskinan ENUM('Sangat Miskin','Miskin','Hampir Miskin')
|
||||
NOT NULL DEFAULT 'Miskin',
|
||||
status_dtks ENUM('Terdaftar DTKS','Belum DTKS','Sedang Diusulkan')
|
||||
NOT NULL DEFAULT 'Belum DTKS',
|
||||
riwayat_penyakit JSON NULL COMMENT 'Array penyakit: Stunting, TBC, Hipertensi, dll',
|
||||
rumah_ibadah_id INT DEFAULT NULL COMMENT 'Auto-assign ke RI terdekat',
|
||||
jarak_ke_ibadah FLOAT DEFAULT NULL COMMENT 'Jarak ke RI terdekat (km)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL: laporan_masyarakat
|
||||
-- Laporan warga tentang kemiskinan (bisa anonim, tanpa login)
|
||||
-- ============================================================
|
||||
CREATE TABLE laporan_masyarakat (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_terlapor VARCHAR(150) NULL COMMENT 'Nama warga yang dilaporkan (boleh null)',
|
||||
nik_terlapor VARCHAR(16) NULL COMMENT 'NIK warga yang dilaporkan (boleh null)',
|
||||
nama_pelapor VARCHAR(150) NULL COMMENT 'Boleh anonim (null)',
|
||||
no_hp VARCHAR(20) NULL,
|
||||
alamat_terlapor TEXT NOT NULL COMMENT 'Alamat yang dilaporkan',
|
||||
lat DECIMAL(10,7) NOT NULL,
|
||||
lng DECIMAL(10,7) NOT NULL,
|
||||
deskripsi TEXT NOT NULL COMMENT 'Deskripsi laporan',
|
||||
foto VARCHAR(255) NULL COMMENT 'Path file foto',
|
||||
status ENUM('Baru','Diverifikasi','Ditolak')
|
||||
NOT NULL DEFAULT 'Baru',
|
||||
alasan_tolak TEXT NULL COMMENT 'Alasan jika ditolak',
|
||||
ditangani_oleh INT NULL COMMENT 'FK ke users.id',
|
||||
converted_to_kk_id INT NULL COMMENT 'FK ke keluarga_miskin.id jika dikonversi',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (ditangani_oleh) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (converted_to_kk_id) REFERENCES keluarga_miskin(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL: histori_bantuan
|
||||
-- Riwayat bantuan yang diterima keluarga miskin
|
||||
-- ============================================================
|
||||
CREATE TABLE histori_bantuan (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
kk_id INT NOT NULL COMMENT 'FK ke keluarga_miskin',
|
||||
jenis_bantuan ENUM('PKH','BPNT/Sembako','Beras Bansos','KIS-PBI',
|
||||
'Bansos Tunai Pemkot','Rutilahu','PMT Stunting',
|
||||
'CSR Swasta','Lainnya')
|
||||
NOT NULL,
|
||||
nominal BIGINT NOT NULL DEFAULT 0 COMMENT 'Nominal dalam rupiah (0 jika non-uang)',
|
||||
keterangan TEXT NULL COMMENT 'Catatan bebas: 10 kg beras, 3 karung, dll',
|
||||
tanggal_penyaluran DATE NOT NULL,
|
||||
sumber_dana ENUM('APBN','APBD','CSR','Lainnya')
|
||||
NOT NULL DEFAULT 'APBD',
|
||||
penanggung_jawab INT NOT NULL COMMENT 'FK ke users.id',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (kk_id) REFERENCES keluarga_miskin(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (penanggung_jawab) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- TABEL: rate_limit
|
||||
-- Pembatasan submit laporan masyarakat per IP (maks 5/jam)
|
||||
-- ============================================================
|
||||
CREATE TABLE rate_limit (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
ip_address VARCHAR(45) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_ip_time (ip_address, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- DATA SEED: Rumah Ibadah (2 data)
|
||||
-- ============================================================
|
||||
INSERT INTO rumah_ibadah (nama, jenis, alamat, lat, lng, radius_km, kode_pengurus, password_pengurus, kecamatan) VALUES
|
||||
(
|
||||
'Masjid Raya Mujahidin',
|
||||
'Masjid',
|
||||
'Jl. Ahmad Yani No. 1, Pontianak Tenggara',
|
||||
-0.0263330, 109.3425200,
|
||||
2.5,
|
||||
'RI-MSJ-4821',
|
||||
-- password: pengurus123
|
||||
'$2y$10$EQKGMwtGqPsn3kxOGkUf3eh3cnXOCEYqVNfCKDVk.Q7WYTCKvi5iG',
|
||||
'Pontianak Tenggara'
|
||||
),
|
||||
(
|
||||
'Gereja Katedral Santo Yosef',
|
||||
'Gereja',
|
||||
'Jl. Diponegoro No. 120, Pontianak Kota',
|
||||
-0.0380000, 109.3422000,
|
||||
2.0,
|
||||
'RI-GRJ-7153',
|
||||
-- password: pengurus123
|
||||
'$2y$10$EQKGMwtGqPsn3kxOGkUf3eh3cnXOCEYqVNfCKDVk.Q7WYTCKvi5iG',
|
||||
'Pontianak Kota'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- DATA SEED: Users (1 admin, 1 walikota)
|
||||
-- Password semua: admin123
|
||||
-- ============================================================
|
||||
INSERT INTO users (nama, username, password, role, rumah_ibadah_id) VALUES
|
||||
(
|
||||
'Administrator Sistem',
|
||||
'admin',
|
||||
'$2y$10$AIO3pPEflVaouN/G.rvgWeZmSjr0Nnjsd2PTna4lSHEoDTWr9KQpi',
|
||||
'admin',
|
||||
NULL
|
||||
),
|
||||
(
|
||||
'Walikota Pontianak',
|
||||
'walikota',
|
||||
'$2y$10$AIO3pPEflVaouN/G.rvgWeZmSjr0Nnjsd2PTna4lSHEoDTWr9KQpi',
|
||||
'walikota',
|
||||
NULL
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- DATA SEED: Keluarga Miskin (5 data)
|
||||
-- Auto-assign rumah_ibadah_id dilakukan oleh aplikasi saat disimpan
|
||||
-- ============================================================
|
||||
INSERT INTO keluarga_miskin (nik, nama_kepala_keluarga, tanggal_lahir, pendidikan, jumlah_anggota, alamat, kecamatan, lat, lng, tingkat_kemiskinan, status_dtks, riwayat_penyakit, rumah_ibadah_id, jarak_ke_ibadah) VALUES
|
||||
(
|
||||
'6171010101010001',
|
||||
'Budi Santoso',
|
||||
'1978-03-15',
|
||||
'SD',
|
||||
4,
|
||||
'Gg. Mawar No. 3, Benua Melayu Darat',
|
||||
'Pontianak Tenggara',
|
||||
-0.0283000, 109.3440000,
|
||||
'Sangat Miskin',
|
||||
'Terdaftar DTKS',
|
||||
'["Stunting","Gizi Buruk"]',
|
||||
1,
|
||||
0.2500
|
||||
),
|
||||
(
|
||||
'6171010101010002',
|
||||
'Siti Rahayu',
|
||||
'1985-07-22',
|
||||
'SMP',
|
||||
6,
|
||||
'Jl. Merdeka Gg. 5 No. 7, Mariana',
|
||||
'Pontianak Kota',
|
||||
-0.0370000, 109.3430000,
|
||||
'Miskin',
|
||||
'Terdaftar DTKS',
|
||||
'["TBC","Hipertensi"]',
|
||||
2,
|
||||
0.1200
|
||||
),
|
||||
(
|
||||
'6171010101010003',
|
||||
'Wayan Dharma',
|
||||
'1972-11-08',
|
||||
'Tidak Sekolah',
|
||||
3,
|
||||
'Gg. Bali No. 2, Bansir Laut',
|
||||
'Pontianak Tenggara',
|
||||
-0.0290000, 109.3410000,
|
||||
'Sangat Miskin',
|
||||
'Belum DTKS',
|
||||
'["Diabetes","Hipertensi","ODGJ"]',
|
||||
1,
|
||||
0.3400
|
||||
),
|
||||
(
|
||||
'6171010101010004',
|
||||
'Ahmad Fauzan',
|
||||
'1990-01-30',
|
||||
'SMA',
|
||||
5,
|
||||
'Jl. Tanjungpura Gg. 12, Parit Tokaya',
|
||||
'Pontianak Selatan',
|
||||
-0.0310000, 109.3350000,
|
||||
'Miskin',
|
||||
'Sedang Diusulkan',
|
||||
'["Stunting"]',
|
||||
NULL,
|
||||
NULL
|
||||
),
|
||||
(
|
||||
'6171010101010005',
|
||||
'Ruslan Effendi',
|
||||
'1968-09-05',
|
||||
'SD',
|
||||
7,
|
||||
'Jl. Sutan Syahrir Gg. Melati, Batu Layang',
|
||||
'Pontianak Utara',
|
||||
-0.0200000, 109.3460000,
|
||||
'Hampir Miskin',
|
||||
'Terdaftar DTKS',
|
||||
'["Jantung","DBD"]',
|
||||
1,
|
||||
0.7800
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- DATA SEED: Laporan Masyarakat (1 contoh)
|
||||
-- ============================================================
|
||||
INSERT INTO laporan_masyarakat (nama_pelapor, no_hp, alamat_terlapor, lat, lng, deskripsi, status) VALUES
|
||||
(
|
||||
'Pak RT 03',
|
||||
'081234567890',
|
||||
'Gg. Flamboyan No. 5, Pontianak Tenggara',
|
||||
-0.0275000, 109.3435000,
|
||||
'Terdapat keluarga dengan 5 anak yang sangat membutuhkan bantuan. Rumah tidak layak huni dan anak-anak putus sekolah.',
|
||||
'Baru'
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- DATA SEED: Histori Bantuan (2 contoh)
|
||||
-- ============================================================
|
||||
INSERT INTO histori_bantuan (kk_id, jenis_bantuan, nominal, keterangan, tanggal_penyaluran, sumber_dana, penanggung_jawab) VALUES
|
||||
(1, 'PKH', 3000000, 'Bantuan tahap I tahun 2026', '2026-01-15', 'APBN', 1),
|
||||
(2, 'BPNT/Sembako', 0, '10 kg beras + minyak goreng', '2026-02-20', 'APBN', 1);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
header('Location: index.html', true, 302);
|
||||
exit;
|
||||
Reference in New Issue
Block a user