UAS SIG Proyek WebGis

This commit is contained in:
L4zyL4mb
2026-06-10 21:46:56 +07:00
commit df18a7a99a
35 changed files with 8977 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
# SIG Penduduk Miskin Berbasis Rumah Ibadah
Aplikasi Sistem Informasi Geografis (SIG) untuk pendataan penduduk miskin berdasarkan wilayah radius rumah ibadah terdekat.
---
## 📁 Struktur Folder
```
sig-kemiskinan/
├── index.html ← Halaman utama (Leaflet + JS)
├── database.sql ← Schema + data contoh
├── includes/
│ └── config.php ← Koneksi DB + fungsi utilitas
├── api/
│ ├── rumah_ibadah.php ← CRUD Rumah Ibadah
│ ├── penduduk.php ← CRUD Penduduk Miskin
│ ├── geocode.php ← Reverse Geocoding (Nominatim)
│ └── update_radius.php ← Update radius + reassign
└── README.md
```
---
## ⚙️ Setup
### 1. Persyaratan
- PHP 7.4+ (atau 8.x)
- MySQL 5.7+ / MariaDB
- Web server (Apache/Nginx) atau PHP built-in server
### 2. Database
```sql
-- Import file database.sql ke MySQL:
mysql -u root -p < database.sql
```
Atau lewat phpMyAdmin: Import → pilih `database.sql`
### 3. Konfigurasi DB
Edit `includes/config.php`:
```php
define('DB_HOST', 'localhost');
define('DB_USER', 'root'); // ganti sesuai user MySQL Anda
define('DB_PASS', ''); // ganti dengan password MySQL
define('DB_NAME', 'sig_kemiskinan');
```
### 4. Jalankan
```bash
# Opsi 1: PHP built-in server (development)
cd sig-kemiskinan
php -S localhost:8080
# Opsi 2: Taruh di folder htdocs (XAMPP/WAMP)
# Copy folder ke C:/xampp/htdocs/sig-kemiskinan
# Akses: http://localhost/sig-kemiskinan
```
---
## 🗺️ Cara Penggunaan
### Tambah Rumah Ibadah
1. Klik tombol **"+ Rumah Ibadah"** (pojok kanan atas)
2. Kursor berubah → **klik lokasi di peta**
3. Isi form: Nama, Jenis, PIC, No. WA, Radius
4. Alamat **otomatis terisi** dari reverse geocoding (Nominatim/OSM)
5. Klik **Simpan**
### Tambah Penduduk Miskin
1. Klik tombol **"+ Penduduk"**
2. **Klik lokasi rumah** di peta
3. Isi: Nama KK, Jumlah anggota
4. Sistem otomatis menentukan rumah ibadah terdekat dalam radius
5. **Titik hijau** = dalam radius | **Titik merah** = luar radius
### Ubah Radius
1. Klik marker rumah ibadah → popup → tombol **📐 Radius**
2. Atau klik ikon 📐 di list sidebar
3. **Drag slider** untuk mengubah radius (preview langsung)
4. Klik **Simpan** → semua penduduk di-reassign otomatis
### Logika Overlap
Jika penduduk masuk dalam radius **>1 rumah ibadah**:
→ Diassign ke **rumah ibadah yang paling dekat** (Haversine distance)
---
## 🔌 API Endpoints
| Method | URL | Fungsi |
|--------|-----|--------|
| GET | `api/rumah_ibadah.php` | Semua rumah ibadah + stats |
| POST | `api/rumah_ibadah.php` | Tambah rumah ibadah |
| PUT | `api/rumah_ibadah.php` | Update rumah ibadah |
| DELETE | `api/rumah_ibadah.php?id=X` | Hapus |
| GET | `api/penduduk.php` | Semua penduduk |
| POST | `api/penduduk.php` | Tambah penduduk |
| PUT | `api/penduduk.php` | Update penduduk |
| DELETE | `api/penduduk.php?id=X` | Hapus |
| GET | `api/geocode.php?lat=X&lng=Y` | Reverse geocoding |
| POST | `api/update_radius.php` | Update radius + reassign |
---
## 🧮 Algoritma Radius
```
Jarak = Haversine(lat1, lng1, lat2, lng2) // dalam meter
Untuk setiap penduduk P:
candidates = [ibadah | jarak(P, ibadah) <= ibadah.radius]
if candidates:
assign P → min(candidates, key=jarak)
else:
P.rumah_ibadah_id = NULL // luar radius semua
```
---
## 🖥️ Fitur UI
- Peta Leaflet dengan tile OpenStreetMap
- Sidebar panel: list Rumah Ibadah & Penduduk
- Stats bar: total ibadah, KK, jiwa, tercakup/tidak
- Radius fleksibel via slider (1003000m, default 500m)
- Lingkaran radius real-time di peta
- Reverse geocoding otomatis saat klik peta
- Warna marker penduduk: hijau (dalam radius) / merah (luar)
- Popup informatif + tombol CRUD langsung dari peta
- Responsive mobile-friendly
- Dark theme modern
+75
View File
@@ -0,0 +1,75 @@
<?php
// api/auth.php v2.0
// ============================================================
// Login / Logout / Session Check API
// ============================================================
require_once __DIR__ . '/../includes/config.php';
session_start();
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$method = $_SERVER['REQUEST_METHOD'];
$db = getDB();
switch ($method) {
// ── POST /api/auth.php → Login ─────────────────────────
case 'POST':
$input = getInput();
$username = trim($input['username'] ?? '');
$password = $input['password'] ?? '';
if (!$username || !$password) {
jsonResponse(['success' => false, 'error' => 'Username dan password wajib diisi'], 400);
}
$stmt = $db->prepare("SELECT * FROM users WHERE username = ? AND aktif = 1 LIMIT 1");
$stmt->execute([$username]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password'])) {
jsonResponse(['success' => false, 'error' => 'Username atau password salah'], 401);
}
// Simpan ke session
$_SESSION['user_id'] = $user['id'];
$_SESSION['user'] = [
'id' => $user['id'],
'nama' => $user['nama'],
'username' => $user['username'],
'role' => $user['role'],
];
// Update last_login
$db->prepare("UPDATE users SET last_login = NOW() WHERE id = ?")
->execute([$user['id']]);
jsonResponse([
'success' => true,
'message' => 'Login berhasil',
'user' => $_SESSION['user'],
]);
break;
// ── DELETE /api/auth.php → Logout ─────────────────────
case 'DELETE':
session_destroy();
jsonResponse(['success' => true, 'message' => 'Logout berhasil']);
break;
// ── GET /api/auth.php → Cek session ───────────────────
case 'GET':
if (!isset($_SESSION['user_id'])) {
jsonResponse(['success' => false, 'loggedIn' => false], 200);
}
jsonResponse(['success' => true, 'loggedIn' => true, 'user' => $_SESSION['user']]);
break;
default:
jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405);
}
+167
View File
@@ -0,0 +1,167 @@
<?php
// api/bantuan.php v2.0
// ============================================================
// API Bantuan Sosial CRUD jenis bantuan & relasi penduduk
// ============================================================
require_once __DIR__ . '/../includes/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$method = $_SERVER['REQUEST_METHOD'];
$db = getDB();
$action = $_GET['action'] ?? 'pivot'; // 'jenis' | 'pivot'
switch ($method) {
// ════════════════════════════════════════════════════════
// GET
// ════════════════════════════════════════════════════════
case 'GET':
// GET jenis_bantuan (master)
if ($action === 'jenis') {
$rows = $db->query("SELECT * FROM jenis_bantuan ORDER BY kode")->fetchAll();
jsonResponse(['success' => true, 'data' => $rows]);
}
// GET bantuan per penduduk
$pendudukId = $_GET['penduduk_id'] ?? null;
if (!$pendudukId) jsonResponse(['success' => false, 'error' => 'penduduk_id diperlukan'], 400);
$stmt = $db->prepare("
SELECT bp.*, jb.kode, jb.nama AS nama_bantuan, jb.warna_badge, jb.deskripsi
FROM bantuan_penduduk bp
JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id
WHERE bp.penduduk_id = ?
ORDER BY jb.kode
");
$stmt->execute([$pendudukId]);
jsonResponse(['success' => true, 'data' => $stmt->fetchAll()]);
break;
// ════════════════════════════════════════════════════════
// POST Tambah/set bantuan untuk penduduk
// ════════════════════════════════════════════════════════
case 'POST':
$in = getInput();
// Batch upsert terima array bantuans[]
if (isset($in['bantuans']) && is_array($in['bantuans'])) {
$pendudukId = (int)($in['penduduk_id'] ?? 0);
if (!$pendudukId) jsonResponse(['success' => false, 'error' => 'penduduk_id diperlukan'], 400);
// Nonaktifkan semua bantuan lama dulu
$db->prepare("UPDATE bantuan_penduduk SET status = 'tidak_aktif' WHERE penduduk_id = ?")
->execute([$pendudukId]);
foreach ($in['bantuans'] as $b) {
$bantuanId = (int)($b['bantuan_id'] ?? 0);
if (!$bantuanId) continue;
// Cek sudah ada?
$cek = $db->prepare("SELECT id FROM bantuan_penduduk WHERE penduduk_id = ? AND bantuan_id = ?");
$cek->execute([$pendudukId, $bantuanId]);
$existing = $cek->fetch();
if ($existing) {
$db->prepare("
UPDATE bantuan_penduduk
SET status = 'aktif', tanggal_mulai = ?, nominal_bulanan = ?,
nomor_kpj = ?, keterangan = ?, diinput_oleh = ?
WHERE penduduk_id = ? AND bantuan_id = ?
")->execute([
$b['tanggal_mulai'] ?? null,
$b['nominal_bulanan'] ?? null,
$b['nomor_kpj'] ?? null,
$b['keterangan'] ?? null,
$b['diinput_oleh'] ?? 1,
$pendudukId, $bantuanId,
]);
} else {
$db->prepare("
INSERT INTO bantuan_penduduk
(penduduk_id, bantuan_id, status, tanggal_mulai, nominal_bulanan, nomor_kpj, keterangan, diinput_oleh)
VALUES (?,?,'aktif',?,?,?,?,?)
")->execute([
$pendudukId, $bantuanId,
$b['tanggal_mulai'] ?? null,
$b['nominal_bulanan'] ?? null,
$b['nomor_kpj'] ?? null,
$b['keterangan'] ?? null,
$b['diinput_oleh'] ?? 1,
]);
}
}
jsonResponse(['success' => true, 'message' => 'Data bantuan berhasil disimpan']);
}
// Single insert
$required = ['penduduk_id','bantuan_id'];
foreach ($required as $f) {
if (empty($in[$f])) jsonResponse(['success' => false, 'error' => "Field '$f' wajib diisi"], 400);
}
// Upsert
$cek = $db->prepare("SELECT id FROM bantuan_penduduk WHERE penduduk_id = ? AND bantuan_id = ?");
$cek->execute([(int)$in['penduduk_id'], (int)$in['bantuan_id']]);
if ($cek->fetch()) {
jsonResponse(['success' => false, 'error' => 'Bantuan ini sudah tercatat untuk penduduk tersebut. Gunakan PUT untuk update.'], 409);
}
$db->prepare("
INSERT INTO bantuan_penduduk (penduduk_id, bantuan_id, status, tanggal_mulai, nominal_bulanan, nomor_kpj, keterangan, diinput_oleh)
VALUES (?,?,?,?,?,?,?,?)
")->execute([
(int)$in['penduduk_id'],
(int)$in['bantuan_id'],
$in['status'] ?? 'aktif',
$in['tanggal_mulai'] ?? null,
$in['nominal_bulanan'] ?? null,
$in['nomor_kpj'] ?? null,
$in['keterangan'] ?? null,
$in['diinput_oleh'] ?? 1,
]);
jsonResponse(['success' => true, 'id' => $db->lastInsertId(), 'message' => 'Bantuan berhasil ditambahkan']);
break;
// ════════════════════════════════════════════════════════
// PUT Update status bantuan
// ════════════════════════════════════════════════════════
case 'PUT':
$in = getInput();
$id = (int)($in['id'] ?? $_GET['id'] ?? 0);
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
$db->prepare("
UPDATE bantuan_penduduk
SET status = ?, tanggal_mulai = ?, tanggal_selesai = ?,
nominal_bulanan = ?, nomor_kpj = ?, keterangan = ?
WHERE id = ?
")->execute([
$in['status'] ?? 'aktif',
$in['tanggal_mulai'] ?? null,
$in['tanggal_selesai'] ?? null,
$in['nominal_bulanan'] ?? null,
$in['nomor_kpj'] ?? null,
$in['keterangan'] ?? null,
$id,
]);
jsonResponse(['success' => true, 'message' => 'Status bantuan diperbarui']);
break;
// ════════════════════════════════════════════════════════
// DELETE Hapus relasi bantuan
// ════════════════════════════════════════════════════════
case 'DELETE':
$id = (int)($_GET['id'] ?? getInput()['id'] ?? 0);
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
$db->prepare("DELETE FROM bantuan_penduduk WHERE id = ?")->execute([$id]);
jsonResponse(['success' => true, 'message' => 'Bantuan dihapus']);
break;
default:
jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405);
}
+189
View File
@@ -0,0 +1,189 @@
<?php
// api/dashboard_stats.php v2.0
// ============================================================
// API Statistik untuk Dashboard & Grafik
// ============================================================
require_once __DIR__ . '/../includes/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
if ($_SERVER['REQUEST_METHOD'] !== 'GET') jsonResponse(['success'=>false,'error'=>'Method tidak diizinkan'],405);
$db = getDB();
// ── Filter opsional ────────────────────────────────────────
$kelurahan = $_GET['kelurahan'] ?? null;
$kecamatan = $_GET['kecamatan'] ?? null;
$whereBase = "WHERE 1=1";
$params = [];
if ($kelurahan) { $whereBase .= " AND w.kelurahan = ?"; $params[] = $kelurahan; }
if ($kecamatan) { $whereBase .= " AND w.kecamatan = ?"; $params[] = $kecamatan; }
$joinW = "LEFT JOIN wilayah w ON w.id = pm.wilayah_id";
// ═══════════════════════════════════════════════════════════
// 1. RINGKASAN UTAMA (KPI Cards)
// ═══════════════════════════════════════════════════════════
$kpi = $db->prepare("
SELECT
COUNT(pm.id) AS total_kk,
SUM(pm.jumlah_anggota) AS total_jiwa,
SUM(pm.jumlah_anak_sekolah) AS total_anak_sekolah,
SUM(CASE WHEN pm.status_verifikasi='terverifikasi' THEN 1 ELSE 0 END) AS terverifikasi,
SUM(CASE WHEN pm.status_verifikasi='belum_diverifikasi' THEN 1 ELSE 0 END) AS belum_verifikasi,
SUM(CASE WHEN pm.punya_bpjs=1 THEN 1 ELSE 0 END) AS punya_bpjs,
SUM(CASE WHEN pm.kategori_kemiskinan='sangat_miskin' THEN 1 ELSE 0 END) AS sangat_miskin,
SUM(CASE WHEN pm.kategori_kemiskinan='miskin' THEN 1 ELSE 0 END) AS miskin,
SUM(CASE WHEN pm.kategori_kemiskinan='hampir_miskin' THEN 1 ELSE 0 END) AS hampir_miskin,
SUM(CASE WHEN pm.kategori_kemiskinan='mampu' THEN 1 ELSE 0 END) AS mampu,
ROUND(AVG(pm.penghasilan_bulanan),0) AS avg_penghasilan,
SUM(CASE WHEN bp_count.cnt > 0 THEN 1 ELSE 0 END) AS penerima_bantuan,
SUM(CASE WHEN bp_count.cnt = 0 THEN 1 ELSE 0 END) AS belum_bantuan
FROM penduduk_miskin pm
$joinW
LEFT JOIN (
SELECT penduduk_id, COUNT(*) AS cnt
FROM bantuan_penduduk
WHERE status='aktif'
GROUP BY penduduk_id
) bp_count ON bp_count.penduduk_id = pm.id
$whereBase
");
$kpi->execute($params);
$kpiData = $kpi->fetch();
// ═══════════════════════════════════════════════════════════
// 2. DISTRIBUSI KATEGORI KEMISKINAN (Donut Chart)
// ═══════════════════════════════════════════════════════════
$katStmt = $db->prepare("
SELECT pm.kategori_kemiskinan AS label, COUNT(*) AS value
FROM penduduk_miskin pm $joinW $whereBase
GROUP BY pm.kategori_kemiskinan
ORDER BY FIELD(pm.kategori_kemiskinan,'sangat_miskin','miskin','hampir_miskin','mampu')
");
$katStmt->execute($params);
$kategoriChart = $katStmt->fetchAll();
// ═══════════════════════════════════════════════════════════
// 3. KK PER KELURAHAN (Bar Chart)
// ═══════════════════════════════════════════════════════════
$kelStmt = $db->prepare("
SELECT
COALESCE(w.kelurahan,'Tidak Diketahui') AS label,
COUNT(pm.id) AS total_kk,
SUM(pm.jumlah_anggota) AS total_jiwa,
SUM(CASE WHEN bp_c.cnt>0 THEN 1 ELSE 0 END) AS penerima
FROM penduduk_miskin pm $joinW
LEFT JOIN (
SELECT penduduk_id, COUNT(*) AS cnt FROM bantuan_penduduk WHERE status='aktif' GROUP BY penduduk_id
) bp_c ON bp_c.penduduk_id = pm.id
$whereBase
GROUP BY w.kelurahan
ORDER BY total_kk DESC
LIMIT 15
");
$kelStmt->execute($params);
$kelurahanChart = $kelStmt->fetchAll();
// ═══════════════════════════════════════════════════════════
// 4. DISTRIBUSI JENIS BANTUAN (Pie Chart)
// ═══════════════════════════════════════════════════════════
$bantuanStmt = $db->prepare("
SELECT jb.kode, jb.nama AS label, jb.warna_badge AS color, COUNT(bp.id) AS value
FROM bantuan_penduduk bp
JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id
JOIN penduduk_miskin pm ON pm.id = bp.penduduk_id
$joinW
" . str_replace('WHERE 1=1', 'WHERE bp.status=\'aktif\'', $whereBase) . "
GROUP BY jb.id
ORDER BY value DESC
");
$bantuanStmt->execute($params);
$bantuanChart = $bantuanStmt->fetchAll();
// ═══════════════════════════════════════════════════════════
// 5. TREN PENDATAAN PER BULAN (Line Chart) 12 bulan terakhir
// ═══════════════════════════════════════════════════════════
$trenStmt = $db->prepare("
SELECT
DATE_FORMAT(pm.created_at,'%Y-%m') AS bulan,
COUNT(pm.id) AS total
FROM penduduk_miskin pm $joinW $whereBase
AND pm.created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
GROUP BY DATE_FORMAT(pm.created_at,'%Y-%m')
ORDER BY bulan ASC
");
$trenStmt->execute($params);
$trenChart = $trenStmt->fetchAll();
// ═══════════════════════════════════════════════════════════
// 6. DISTRIBUSI PEKERJAAN (Bar horizontal)
// ═══════════════════════════════════════════════════════════
$pekerjaanStmt = $db->prepare("
SELECT pm.status_pekerjaan AS label, COUNT(*) AS value
FROM penduduk_miskin pm $joinW $whereBase
GROUP BY pm.status_pekerjaan
ORDER BY value DESC
");
$pekerjaanStmt->execute($params);
$pekerjaanChart = $pekerjaanStmt->fetchAll();
// ═══════════════════════════════════════════════════════════
// 7. DATA HEATMAP (lat/lng + bobot untuk Leaflet.heat)
// ═══════════════════════════════════════════════════════════
$heatStmt = $db->prepare("
SELECT pm.lat, pm.lng,
CASE pm.kategori_kemiskinan
WHEN 'sangat_miskin' THEN 1.0
WHEN 'miskin' THEN 0.6
WHEN 'hampir_miskin' THEN 0.3
WHEN 'mampu' THEN 0.1
ELSE 0.1
END AS intensity
FROM penduduk_miskin pm $joinW $whereBase
");
$heatStmt->execute($params);
$heatmap = $heatStmt->fetchAll();
// ═══════════════════════════════════════════════════════════
// 8. TOP 10 KK BELUM MENERIMA BANTUAN APAPUN
// ═══════════════════════════════════════════════════════════
$belumBantuanStmt = $db->prepare("
SELECT pm.id, pm.nama_kk, pm.nik_kk, pm.alamat,
pm.kategori_kemiskinan, pm.jumlah_anggota,
pm.status_verifikasi,
COALESCE(w.kelurahan,'') AS kelurahan,
DATEDIFF(NOW(), pm.created_at) AS hari_terdata
FROM penduduk_miskin pm $joinW
$whereBase
AND NOT EXISTS (
SELECT 1 FROM bantuan_penduduk bp
WHERE bp.penduduk_id = pm.id AND bp.status='aktif'
)
ORDER BY pm.kategori_kemiskinan ASC, hari_terdata DESC
LIMIT 10
");
$belumBantuanStmt->execute($params);
$belumBantuan = $belumBantuanStmt->fetchAll();
// ── Statistik Rumah Ibadah (tetap dari v1.1) ──────────────
$ibadahStats = $db->query("
SELECT COUNT(*) AS total_ibadah FROM rumah_ibadah
")->fetch();
jsonResponse([
'success' => true,
'kpi' => $kpiData,
'chart_kategori' => $kategoriChart,
'chart_kelurahan' => $kelurahanChart,
'chart_bantuan' => $bantuanChart,
'chart_tren' => $trenChart,
'chart_pekerjaan' => $pekerjaanChart,
'heatmap' => $heatmap,
'belum_bantuan' => $belumBantuan,
'total_ibadah' => (int)$ibadahStats['total_ibadah'],
'generated_at' => date('Y-m-d H:i:s'),
]);
+118
View File
@@ -0,0 +1,118 @@
<?php
// api/export_excel.php v2.0
// ============================================================
// Export Data Penduduk Miskin ke Excel (format CSV UTF-8 BOM)
// Tidak memerlukan library eksternal
// ============================================================
require_once __DIR__ . '/../includes/config.php';
$db = getDB();
// ── Filter dari query string ───────────────────────────────
$kelurahan = $_GET['kelurahan'] ?? null;
$kecamatan = $_GET['kecamatan'] ?? null;
$kategori = $_GET['kategori'] ?? null;
$verifikasi = $_GET['verifikasi'] ?? null;
$bantuan = $_GET['bantuan'] ?? null;
$where = "WHERE 1=1";
$params = [];
if ($kelurahan) { $where .= " AND w.kelurahan = ?"; $params[] = $kelurahan; }
if ($kecamatan) { $where .= " AND w.kecamatan = ?"; $params[] = $kecamatan; }
if ($kategori) { $where .= " AND pm.kategori_kemiskinan = ?"; $params[] = $kategori; }
if ($verifikasi) { $where .= " AND pm.status_verifikasi = ?"; $params[] = $verifikasi; }
if ($bantuan === 'tidak') {
$where .= " AND (SELECT COUNT(*) FROM bantuan_penduduk bp WHERE bp.penduduk_id = pm.id AND bp.status='aktif') = 0";
} elseif ($bantuan === 'ada') {
$where .= " AND (SELECT COUNT(*) FROM bantuan_penduduk bp WHERE bp.penduduk_id = pm.id AND bp.status='aktif') > 0";
}
$stmt = $db->prepare("
SELECT
pm.nik_kk, pm.nama_kk, pm.no_hp,
pm.alamat, pm.rt, pm.rw,
COALESCE(w.kelurahan,'') AS kelurahan,
COALESCE(w.kecamatan,'') AS kecamatan,
pm.jumlah_anggota, pm.jumlah_anak_sekolah,
pm.penghasilan_bulanan, pm.sumber_penghasilan, pm.status_pekerjaan,
pm.kategori_kemiskinan, pm.kondisi_rumah, pm.kepemilikan_rumah,
CASE pm.punya_bpjs WHEN 1 THEN 'Ya' ELSE 'Tidak' END AS punya_bpjs,
pm.jenis_bpjs,
pm.lat, pm.lng,
pm.status_verifikasi,
COALESCE(u.nama,'') AS nama_surveyor,
DATE_FORMAT(pm.created_at,'%d/%m/%Y %H:%i') AS tgl_input,
(SELECT GROUP_CONCAT(jb.kode ORDER BY jb.kode SEPARATOR ', ')
FROM bantuan_penduduk bp JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id
WHERE bp.penduduk_id = pm.id AND bp.status='aktif') AS bantuan_diterima
FROM penduduk_miskin pm
LEFT JOIN wilayah w ON w.id = pm.wilayah_id
LEFT JOIN users u ON u.id = pm.surveyor_id
$where
ORDER BY w.kelurahan, pm.nama_kk
");
$stmt->execute($params);
$rows = $stmt->fetchAll();
// ── Nama file ─────────────────────────────────────────────
$filter = $kelurahan ? "_" . preg_replace('/\s+/', '-', $kelurahan) : '';
$filename = "data_kemiskinan{$filter}_" . date('Ymd_His') . ".csv";
// ── Output header HTTP ─────────────────────────────────────
header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
// BOM UTF-8 agar Excel baca karakter Indonesia dengan benar
echo "\xEF\xBB\xBF";
$out = fopen('php://output', 'w');
// ── Header kolom ──────────────────────────────────────────
fputcsv($out, [
'No','NIK KK','Nama KK','No HP',
'Alamat','RT','RW','Kelurahan','Kecamatan',
'Jml Anggota','Anak Sekolah',
'Penghasilan/Bln','Sumber Penghasilan','Status Pekerjaan',
'Kategori Kemiskinan','Kondisi Rumah','Kepemilikan Rumah',
'Punya BPJS','Jenis BPJS',
'Latitude','Longitude',
'Status Verifikasi','Bantuan Diterima',
'Petugas Input','Tgl Input',
], ';');
// ── Data rows ─────────────────────────────────────────────
$no = 1;
foreach ($rows as $r) {
fputcsv($out, [
$no++,
$r['nik_kk'],
$r['nama_kk'],
$r['no_hp'] ?? '',
$r['alamat'] ?? '',
$r['rt'] ?? '',
$r['rw'] ?? '',
$r['kelurahan'],
$r['kecamatan'],
$r['jumlah_anggota'],
$r['jumlah_anak_sekolah'],
$r['penghasilan_bulanan'] ? 'Rp ' . number_format($r['penghasilan_bulanan'],0,',','.') : '',
$r['sumber_penghasilan'] ?? '',
ucwords(str_replace('_',' ', $r['status_pekerjaan'])),
$r['kategori_kemiskinan'] === 'hampir_miskin' ? 'Nyaris Miskin' : ucwords(str_replace('_',' ', $r['kategori_kemiskinan'])),
ucwords(str_replace('_',' ', $r['kondisi_rumah'])),
ucwords(str_replace('_',' ', $r['kepemilikan_rumah'])),
$r['punya_bpjs'],
strtoupper(str_replace('_',' ', $r['jenis_bpjs'])),
$r['lat'],
$r['lng'],
ucwords(str_replace('_',' ', $r['status_verifikasi'])),
$r['bantuan_diterima'] ?? 'Belum Ada',
$r['nama_surveyor'],
$r['tgl_input'],
], ';');
}
fclose($out);
exit;
+246
View File
@@ -0,0 +1,246 @@
<?php
// api/export_pdf.php v2.0
// ============================================================
// Export Laporan PDF menggunakan HTML → browser print
// (Pure PHP, tanpa library eksternal)
// ============================================================
require_once __DIR__ . '/../includes/config.php';
$db = getDB();
// ── Filter ────────────────────────────────────────────────
$kelurahan = $_GET['kelurahan'] ?? null;
$kecamatan = $_GET['kecamatan'] ?? null;
$kategori = $_GET['kategori'] ?? null;
$verifikasi = $_GET['verifikasi'] ?? null;
$where = "WHERE 1=1";
$params = [];
if ($kelurahan) { $where .= " AND w.kelurahan = ?"; $params[] = $kelurahan; }
if ($kecamatan) { $where .= " AND w.kecamatan = ?"; $params[] = $kecamatan; }
if ($kategori) { $where .= " AND pm.kategori_kemiskinan = ?"; $params[] = $kategori; }
if ($verifikasi) { $where .= " AND pm.status_verifikasi = ?"; $params[] = $verifikasi; }
// Data penduduk
$stmt = $db->prepare("
SELECT pm.nik_kk, pm.nama_kk, pm.no_hp, pm.alamat, pm.rt, pm.rw,
COALESCE(w.kelurahan,'') AS kelurahan,
pm.jumlah_anggota, pm.jumlah_anak_sekolah,
pm.penghasilan_bulanan, pm.status_pekerjaan,
pm.kategori_kemiskinan, pm.kondisi_rumah,
CASE pm.punya_bpjs WHEN 1 THEN 'Ya' ELSE 'Tidak' END AS punya_bpjs,
pm.status_verifikasi,
(SELECT GROUP_CONCAT(jb.kode ORDER BY jb.kode SEPARATOR ', ')
FROM bantuan_penduduk bp JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id
WHERE bp.penduduk_id = pm.id AND bp.status='aktif') AS bantuan
FROM penduduk_miskin pm
LEFT JOIN wilayah w ON w.id = pm.wilayah_id
$where
ORDER BY w.kelurahan, pm.kategori_kemiskinan, pm.nama_kk
");
$stmt->execute($params);
$rows = $stmt->fetchAll();
// Statistik ringkas
$statsStmt = $db->prepare("
SELECT COUNT(*) AS total, SUM(pm.jumlah_anggota) AS jiwa,
SUM(CASE WHEN pm.kategori_kemiskinan='sangat_miskin' THEN 1 ELSE 0 END) AS sangat_miskin,
SUM(CASE WHEN pm.kategori_kemiskinan='miskin' THEN 1 ELSE 0 END) AS miskin,
SUM(CASE WHEN pm.kategori_kemiskinan='hampir_miskin' THEN 1 ELSE 0 END) AS hampir_miskin,
SUM(CASE WHEN pm.kategori_kemiskinan='mampu' THEN 1 ELSE 0 END) AS mampu,
SUM(CASE WHEN EXISTS(SELECT 1 FROM bantuan_penduduk bp WHERE bp.penduduk_id=pm.id AND bp.status='aktif') THEN 1 ELSE 0 END) AS penerima
FROM penduduk_miskin pm LEFT JOIN wilayah w ON w.id = pm.wilayah_id $where
");
$statsStmt->execute($params);
$stats = $statsStmt->fetch();
$judul = "LAPORAN DATA PENDUDUK MISKIN";
$filter = $kelurahan ? " Kelurahan $kelurahan" : ($kecamatan ? " Kec. $kecamatan" : " Seluruh Wilayah");
$tgl = date('d F Y');
// Warna kategori
function warnaBg(string $k): string {
return match($k) {
'sangat_miskin' => '#fee2e2',
'miskin' => '#fef3c7',
'hampir_miskin' => '#dcfce7',
'mampu' => '#dbeafe',
default => '#f3f4f6',
};
}
function labelKat(string $k): string {
return match($k) {
'sangat_miskin' => 'Sangat Miskin',
'miskin' => 'Miskin',
'hampir_miskin' => 'Nyaris Miskin',
'mampu' => 'Mampu',
default => $k,
};
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title><?= htmlspecialchars($judul . $filter) ?></title>
<style>
@page { size: A4 landscape; margin: 15mm 10mm; }
@media print {
.no-print { display: none; }
body { font-size: 8pt; }
table { font-size: 7.5pt; }
thead { display: table-header-group; }
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, sans-serif; font-size: 9pt; color: #111; background: #fff; }
/* Header laporan */
.kop { text-align: center; border-bottom: 3px double #1e3a5f; padding-bottom: 10px; margin-bottom: 14px; }
.kop h1 { font-size: 13pt; font-weight: 900; text-transform: uppercase; color: #1e3a5f; }
.kop h2 { font-size: 11pt; font-weight: 700; margin: 2px 0; }
.kop p { font-size: 8pt; color: #555; }
/* Statistik */
.stat-grid { display: flex; gap: 8px; margin-bottom: 14px; }
.stat-box {
flex: 1; border: 1px solid #ddd; border-radius: 6px;
padding: 7px 10px; text-align: center; background: #f8faff;
}
.stat-box .val { font-size: 16pt; font-weight: 900; color: #1e3a5f; }
.stat-box .lbl { font-size: 7pt; color: #666; text-transform: uppercase; }
/* Tabel */
table { width: 100%; border-collapse: collapse; margin-top: 4px; }
th { background: #1e3a5f; color: #fff; padding: 5px 4px; text-align: center; font-size: 7.5pt; border: 1px solid #1e3a5f; }
td { padding: 4px; border: 1px solid #ddd; vertical-align: top; font-size: 7.5pt; }
tr:nth-child(even) td { background: #f9fafb; }
.badge-sm {
display: inline-block; padding: 1px 5px; border-radius: 3px;
font-size: 6.5pt; font-weight: bold;
}
.c { text-align: center; }
.r { text-align: right; }
/* Footer */
.footer { margin-top: 20px; display: flex; justify-content: space-between; font-size: 8pt; }
.ttd-box { text-align: center; width: 200px; }
.ttd-box .line { border-top: 1px solid #111; margin-top: 50px; padding-top: 3px; font-weight: bold; }
/* Print button */
.no-print { text-align: center; margin: 12px 0; }
.btn-print {
background: #1e3a5f; color: #fff; border: none; padding: 8px 24px;
border-radius: 5px; font-size: 10pt; cursor: pointer; margin-right: 8px;
}
.btn-print:hover { background: #274e81; }
</style>
</head>
<body>
<!-- Print Button -->
<div class="no-print">
<button class="btn-print" onclick="window.print()">🖨 Cetak / Simpan PDF</button>
<button class="btn-print" style="background:#555;" onclick="window.close()">✕ Tutup</button>
</div>
<!-- Kop Surat -->
<div class="kop">
<h1><?= htmlspecialchars($judul) ?></h1>
<h2>SISTEM INFORMASI GEOGRAFIS KEMISKINAN<?= htmlspecialchars($filter) ?></h2>
<p>Dicetak pada: <?= $tgl ?> &nbsp;|&nbsp; Total: <?= count($rows) ?> KK</p>
</div>
<!-- Statistik Ringkas -->
<div class="stat-grid">
<div class="stat-box"><div class="val"><?= number_format($stats['total']) ?></div><div class="lbl">Total KK</div></div>
<div class="stat-box"><div class="val"><?= number_format($stats['jiwa']) ?></div><div class="lbl">Total Jiwa</div></div>
<div class="stat-box" style="background:#fee2e2"><div class="val"><?= number_format($stats['sangat_miskin']) ?></div><div class="lbl">Sangat Miskin</div></div>
<div class="stat-box" style="background:#fef3c7"><div class="val"><?= number_format($stats['miskin']) ?></div><div class="lbl">Miskin</div></div>
<div class="stat-box" style="background:#dcfce7"><div class="val"><?= number_format($stats['hampir_miskin']) ?></div><div class="lbl">Nyaris Miskin</div></div>
<div class="stat-box" style="background:#dbeafe"><div class="val"><?= number_format($stats['mampu']) ?></div><div class="lbl">Mampu</div></div>
<div class="stat-box"><div class="val"><?= number_format($stats['penerima']) ?></div><div class="lbl">Penerima Bantuan</div></div>
<div class="stat-box" style="background:#fff7ed"><div class="val"><?= number_format($stats['total'] - $stats['penerima']) ?></div><div class="lbl">Belum Bantuan</div></div>
</div>
<!-- Tabel Data -->
<table>
<thead>
<tr>
<th style="width:2%">No</th>
<th style="width:11%">NIK / Nama KK</th>
<th style="width:6%">Kelurahan</th>
<th style="width:4%">RT/RW</th>
<th style="width:13%">Alamat</th>
<th style="width:3%">Angg.</th>
<th style="width:3%">Anak</th>
<th style="width:7%">Penghasilan</th>
<th style="width:7%">Pekerjaan</th>
<th style="width:8%">Kategori</th>
<th style="width:7%">Kondisi Rumah</th>
<th style="width:4%">BPJS</th>
<th style="width:11%">Bantuan Diterima</th>
<th style="width:10%">Status Verifikasi</th>
</tr>
</thead>
<tbody>
<?php $no = 1; foreach ($rows as $r): ?>
<tr>
<td class="c"><?= $no++ ?></td>
<td>
<small style="color:#666"><?= htmlspecialchars($r['nik_kk']) ?></small><br>
<strong><?= htmlspecialchars($r['nama_kk']) ?></strong><br>
<small><?= htmlspecialchars($r['no_hp'] ?? '') ?></small>
</td>
<td class="c"><?= htmlspecialchars($r['kelurahan']) ?></td>
<td class="c"><?= ($r['rt'] ? $r['rt'].'/'.$r['rw'] : '') ?></td>
<td><?= htmlspecialchars($r['alamat'] ?? '') ?></td>
<td class="c"><?= $r['jumlah_anggota'] ?></td>
<td class="c"><?= $r['jumlah_anak_sekolah'] ?></td>
<td class="r"><?= $r['penghasilan_bulanan'] ? 'Rp '.number_format($r['penghasilan_bulanan'],0,',','.') : '' ?></td>
<td><?= htmlspecialchars(ucwords(str_replace('_',' ',$r['status_pekerjaan']))) ?></td>
<td class="c">
<span class="badge-sm" style="background:<?= warnaBg($r['kategori_kemiskinan']) ?>">
<?= labelKat($r['kategori_kemiskinan']) ?>
</span>
</td>
<td class="c"><?= htmlspecialchars(ucwords(str_replace('_',' ',$r['kondisi_rumah']))) ?></td>
<td class="c"><?= $r['punya_bpjs'] ?></td>
<td>
<?php if ($r['bantuan']): ?>
<?php foreach (explode(', ', $r['bantuan']) as $b): ?>
<span class="badge-sm" style="background:#dbeafe;color:#1e40af"><?= htmlspecialchars(trim($b)) ?></span>
<?php endforeach; ?>
<?php else: ?>
<span style="color:#dc2626">Belum Ada</span>
<?php endif; ?>
</td>
<td class="c">
<?php
$sv = $r['status_verifikasi'];
$svColor = match($sv) { 'terverifikasi'=>'#dcfce7', 'belum_diverifikasi'=>'#fef3c7', default=>'#fee2e2' };
?>
<span class="badge-sm" style="background:<?= $svColor ?>">
<?= htmlspecialchars(ucwords(str_replace('_',' ',$sv))) ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- Footer / TTD -->
<div class="footer">
<div>
<p>Dicetak: <?= date('d/m/Y H:i') ?> WIB</p>
<p>Filter: <?= htmlspecialchars(implode(', ', array_filter([$kelurahan, $kecamatan, $kategori ? "Kategori: $kategori" : null])) ?: 'Semua Data') ?></p>
<p>Sumber: SIG Kemiskinan v2.0</p>
</div>
<div class="ttd-box">
<p>Pontianak, <?= $tgl ?></p>
<p>Mengetahui,</p>
<div class="line">Kepala Dinas / Pejabat Berwenang</div>
</div>
</div>
</body>
</html>
+171
View File
@@ -0,0 +1,171 @@
<?php
// api/foto.php v2.0
// ============================================================
// API Upload & Kelola Foto Rumah + GPS Geo-tagging
// ============================================================
require_once __DIR__ . '/../includes/config.php';
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$method = $_SERVER['REQUEST_METHOD'];
$db = getDB();
// Pastikan folder upload ada
if (!is_dir(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0755, true);
switch ($method) {
// ════════════════════════════════════════════════════════
// GET Ambil daftar foto per penduduk
// ════════════════════════════════════════════════════════
case 'GET':
$pendudukId = $_GET['penduduk_id'] ?? null;
if (!$pendudukId) {
header('Content-Type: application/json');
jsonResponse(['success' => false, 'error' => 'penduduk_id diperlukan'], 400);
}
$stmt = $db->prepare("
SELECT f.*, u.nama AS nama_petugas
FROM foto_rumah f
LEFT JOIN users u ON u.id = f.diambil_oleh
WHERE f.penduduk_id = ?
ORDER BY f.is_primary DESC, f.created_at ASC
");
$stmt->execute([$pendudukId]);
header('Content-Type: application/json');
jsonResponse(['success' => true, 'data' => $stmt->fetchAll()]);
break;
// ════════════════════════════════════════════════════════
// POST Upload foto baru
// ════════════════════════════════════════════════════════
case 'POST':
header('Content-Type: application/json');
$pendudukId = (int)($_POST['penduduk_id'] ?? 0);
if (!$pendudukId) jsonResponse(['success' => false, 'error' => 'penduduk_id diperlukan'], 400);
// Cek penduduk ada
$cek = $db->prepare("SELECT id FROM penduduk_miskin WHERE id = ?");
$cek->execute([$pendudukId]);
if (!$cek->fetch()) jsonResponse(['success' => false, 'error' => 'Data penduduk tidak ditemukan'], 404);
if (!isset($_FILES['foto']) || $_FILES['foto']['error'] !== UPLOAD_ERR_OK) {
$errCode = $_FILES['foto']['error'] ?? 'no_file';
jsonResponse(['success' => false, 'error' => "Upload gagal (kode: $errCode)"], 400);
}
$file = $_FILES['foto'];
$tmpPath = $file['tmp_name'];
$origName = $file['name'];
$fileSize = $file['size'];
// Validasi ukuran
if ($fileSize > MAX_FOTO_SIZE) {
jsonResponse(['success' => false, 'error' => 'Ukuran file maksimal 5 MB'], 413);
}
// Validasi ekstensi & MIME type
$ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION));
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $tmpPath);
finfo_close($finfo);
$allowedMime = ['image/jpeg','image/png','image/webp'];
if (!in_array($ext, ALLOWED_EXT) || !in_array($mime, $allowedMime)) {
jsonResponse(['success' => false, 'error' => 'Format file tidak didukung (jpg/png/webp)'], 415);
}
// Generate nama file unik
$newName = 'foto_' . $pendudukId . '_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$destPath = UPLOAD_DIR . $newName;
$relPath = UPLOAD_URL . $newName;
if (!move_uploaded_file($tmpPath, $destPath)) {
jsonResponse(['success' => false, 'error' => 'Gagal menyimpan file'], 500);
}
// Cek apakah ini foto pertama → jadikan primary
$countStmt = $db->prepare("SELECT COUNT(*) FROM foto_rumah WHERE penduduk_id = ?");
$countStmt->execute([$pendudukId]);
$isPrimary = ($countStmt->fetchColumn() == 0) ? 1 : 0;
// Koordinat GPS dari POST (dari browser Geolocation atau EXIF)
$latFoto = isset($_POST['lat_foto']) && $_POST['lat_foto'] !== '' ? (float)$_POST['lat_foto'] : null;
$lngFoto = isset($_POST['lng_foto']) && $_POST['lng_foto'] !== '' ? (float)$_POST['lng_foto'] : null;
$accuracy = isset($_POST['accuracy_meter']) && $_POST['accuracy_meter'] !== '' ? (float)$_POST['accuracy_meter']
: (isset($_POST['accuracy']) && $_POST['accuracy'] !== '' ? (float)$_POST['accuracy'] : null);
// Jika user set is_primary = 1, nonaktifkan primary lain dulu
$isPrimaryReq = isset($_POST['is_primary']) && (int)$_POST['is_primary'] === 1;
if ($isPrimaryReq) {
$db->prepare("UPDATE foto_rumah SET is_primary = 0 WHERE penduduk_id = ?")
->execute([$pendudukId]);
$isPrimary = 1;
}
// diambil_oleh: support field name lama (user_id) dan baru (diambil_oleh)
$diambilOleh = $_POST['diambil_oleh'] ?? $_POST['user_id'] ?? null;
$db->prepare("
INSERT INTO foto_rumah (penduduk_id, file_path, lat_foto, lng_foto, accuracy_meter, is_primary, ukuran_kb, keterangan, diambil_oleh)
VALUES (?,?,?,?,?,?,?,?,?)
")->execute([
$pendudukId,
$relPath,
$latFoto,
$lngFoto,
$accuracy,
$isPrimary,
(int)round($fileSize / 1024),
$_POST['keterangan'] ?? null,
$diambilOleh ? (int)$diambilOleh : null,
]);
$newId = (int)$db->lastInsertId();
jsonResponse([
'success' => true,
'id' => $newId,
'file_path' => $relPath,
'is_primary'=> $isPrimary,
'message' => 'Foto berhasil diupload',
]);
break;
// ════════════════════════════════════════════════════════
// DELETE Hapus foto
// ════════════════════════════════════════════════════════
case 'DELETE':
header('Content-Type: application/json');
$id = (int)($_GET['id'] ?? 0);
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
$stmt = $db->prepare("SELECT * FROM foto_rumah WHERE id = ?");
$stmt->execute([$id]);
$foto = $stmt->fetch();
if (!$foto) jsonResponse(['success' => false, 'error' => 'Foto tidak ditemukan'], 404);
// Hapus file fisik
$filePath = __DIR__ . '/../' . $foto['file_path'];
if (file_exists($filePath)) @unlink($filePath);
$db->prepare("DELETE FROM foto_rumah WHERE id = ?")->execute([$id]);
// Jika foto yang dihapus adalah primary, set foto lain jadi primary
if ($foto['is_primary']) {
$next = $db->prepare("SELECT id FROM foto_rumah WHERE penduduk_id = ? ORDER BY created_at ASC LIMIT 1");
$next->execute([$foto['penduduk_id']]);
$nextFoto = $next->fetch();
if ($nextFoto) {
$db->prepare("UPDATE foto_rumah SET is_primary = 1 WHERE id = ?")->execute([$nextFoto['id']]);
}
}
jsonResponse(['success' => true, 'message' => 'Foto berhasil dihapus']);
break;
default:
header('Content-Type: application/json');
jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405);
}
+71
View File
@@ -0,0 +1,71 @@
<?php
// api/geocode.php
// Reverse Geocoding via Nominatim (OpenStreetMap)
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
$lat = $_GET['lat'] ?? null;
$lng = $_GET['lng'] ?? null;
if (!$lat || !$lng) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'lat dan lng diperlukan']);
exit;
}
$lat = (float)$lat;
$lng = (float)$lng;
$url = "https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={$lat}&lon={$lng}&zoom=18&addressdetails=1";
$context = stream_context_create([
'http' => [
'method' => 'GET',
'header' => "User-Agent: SIG-Kemiskinan/1.0 (sig@example.com)\r\n",
'timeout' => 8,
]
]);
$response = @file_get_contents($url, false, $context);
if ($response === false) {
// Fallback: koordinat saja
echo json_encode([
'success' => true,
'alamat' => "Koordinat: {$lat}, {$lng}",
'detail' => null
]);
exit;
}
$data = json_decode($response, true);
if (!$data || isset($data['error'])) {
echo json_encode([
'success' => true,
'alamat' => "Koordinat: {$lat}, {$lng}",
'detail' => null
]);
exit;
}
// Susun alamat yang rapi
$addr = $data['address'] ?? [];
$parts = array_filter([
$addr['road'] ?? $addr['pedestrian'] ?? $addr['path'] ?? null,
$addr['house_number'] ?? null,
$addr['neighbourhood'] ?? $addr['suburb'] ?? null,
$addr['city'] ?? $addr['town'] ?? $addr['village'] ?? $addr['county'] ?? null,
]);
$alamat = !empty($parts)
? implode(', ', $parts)
: ($data['display_name'] ?? "Koordinat: {$lat}, {$lng}");
echo json_encode([
'success' => true,
'alamat' => $alamat,
'display_name' => $data['display_name'] ?? '',
'detail' => $addr
]);
+161
View File
@@ -0,0 +1,161 @@
<?php
// api/notifikasi.php v2.0
// ============================================================
// API Sistem Notifikasi + Generate Otomatis
// ============================================================
require_once __DIR__ . '/../includes/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$method = $_SERVER['REQUEST_METHOD'];
$db = getDB();
switch ($method) {
// ════════════════════════════════════════════════════════
// GET Ambil notifikasi (dengan auto-generate)
// ════════════════════════════════════════════════════════
case 'GET':
$userId = $_GET['user_id'] ?? null;
$generate = isset($_GET['generate']); // ?generate → trigger auto-create
if ($generate) generateNotifikasi($db);
$sql = "SELECT n.*, pm.nama_kk FROM notifikasi n LEFT JOIN penduduk_miskin pm ON pm.id = n.penduduk_id WHERE 1=1";
$params = [];
if ($userId) {
$sql .= " AND (n.user_id = ? OR n.user_id IS NULL)";
$params[] = $userId;
}
// Filter belum dibaca
if (isset($_GET['unread'])) {
$sql .= " AND n.sudah_dibaca = 0";
}
$sql .= " ORDER BY n.created_at DESC LIMIT 50";
$stmt = $db->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
// Hitung unread
$unreadStmt = $db->prepare("SELECT COUNT(*) FROM notifikasi WHERE sudah_dibaca = 0 AND (user_id = ? OR user_id IS NULL)");
$unreadStmt->execute([$userId ?? 0]);
$unreadCount = (int)$unreadStmt->fetchColumn();
jsonResponse(['success' => true, 'data' => $rows, 'unread_count' => $unreadCount]);
break;
// ════════════════════════════════════════════════════════
// PUT Tandai sudah dibaca
// ════════════════════════════════════════════════════════
case 'PUT':
$in = getInput();
$id = $in['id'] ?? 'all';
if ($id === 'all') {
$userId = (int)($in['user_id'] ?? 0);
$db->prepare("UPDATE notifikasi SET sudah_dibaca = 1 WHERE user_id = ? OR user_id IS NULL")
->execute([$userId]);
jsonResponse(['success' => true, 'message' => 'Semua notifikasi ditandai dibaca']);
} else {
$db->prepare("UPDATE notifikasi SET sudah_dibaca = 1 WHERE id = ?")
->execute([(int)$id]);
jsonResponse(['success' => true, 'message' => 'Notifikasi ditandai dibaca']);
}
break;
// ════════════════════════════════════════════════════════
// POST Buat notifikasi manual
// ════════════════════════════════════════════════════════
case 'POST':
$in = getInput();
if (empty($in['judul']) || empty($in['pesan'])) {
jsonResponse(['success' => false, 'error' => 'Judul dan pesan wajib diisi'], 400);
}
$db->prepare("
INSERT INTO notifikasi (user_id, jenis, judul, pesan, penduduk_id)
VALUES (?,?,?,?,?)
")->execute([
$in['user_id'] ?? null,
$in['jenis'] ?? 'info',
$in['judul'],
$in['pesan'],
$in['penduduk_id'] ?? null,
]);
jsonResponse(['success' => true, 'id' => $db->lastInsertId(), 'message' => 'Notifikasi dibuat']);
break;
default:
jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405);
}
// ============================================================
// FUNGSI: Generate notifikasi otomatis
// ============================================================
function generateNotifikasi(PDO $db): void {
// 1. KK belum terverifikasi
$belumVerif = $db->query("
SELECT id, nama_kk FROM penduduk_miskin
WHERE status_verifikasi = 'belum_diverifikasi'
AND id NOT IN (
SELECT DISTINCT penduduk_id FROM notifikasi
WHERE jenis = 'belum_verifikasi' AND sudah_dibaca = 0
)
LIMIT 20
")->fetchAll();
foreach ($belumVerif as $p) {
$db->prepare("
INSERT INTO notifikasi (jenis, judul, pesan, penduduk_id)
VALUES ('belum_verifikasi','Data Menunggu Verifikasi',?,?)
")->execute(["KK {$p['nama_kk']} belum diverifikasi.", $p['id']]);
}
// 2. KK belum menerima bantuan apapun (sangat miskin)
$belumBantuan = $db->query("
SELECT id, nama_kk FROM penduduk_miskin
WHERE kategori_kemiskinan = 'sangat_miskin'
AND id NOT IN (
SELECT penduduk_id FROM bantuan_penduduk WHERE status = 'aktif'
)
AND id NOT IN (
SELECT DISTINCT penduduk_id FROM notifikasi
WHERE jenis = 'belum_bantuan' AND sudah_dibaca = 0
)
LIMIT 20
")->fetchAll();
foreach ($belumBantuan as $p) {
$db->prepare("
INSERT INTO notifikasi (jenis, judul, pesan, penduduk_id)
VALUES ('belum_bantuan','KK Sangat Miskin Tanpa Bantuan',?,?)
")->execute(["KK {$p['nama_kk']} berkategori sangat miskin namun belum menerima bantuan apapun.", $p['id']]);
}
// 3. Data usang (tidak diupdate > DATA_USANG_HARI hari)
$usang = $db->prepare("
SELECT id, nama_kk, DATEDIFF(NOW(), updated_at) AS hari_usang
FROM penduduk_miskin
WHERE updated_at < DATE_SUB(NOW(), INTERVAL ? DAY)
AND id NOT IN (
SELECT DISTINCT penduduk_id FROM notifikasi
WHERE jenis = 'data_usang' AND sudah_dibaca = 0
)
LIMIT 10
");
$usang->execute([DATA_USANG_HARI]);
foreach ($usang->fetchAll() as $p) {
$db->prepare("
INSERT INTO notifikasi (jenis, judul, pesan, penduduk_id)
VALUES ('data_usang','Data Tidak Diperbarui',?,?)
")->execute(["Data KK {$p['nama_kk']} belum diperbarui selama {$p['hari_usang']} hari.", $p['id']]);
}
}
+308
View File
@@ -0,0 +1,308 @@
<?php
// api/penduduk.php v2.0
// ============================================================
// CRUD API Penduduk Miskin Field diperluas
// ============================================================
require_once __DIR__ . '/../includes/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$method = $_SERVER['REQUEST_METHOD'];
$db = getDB();
// ── Query SELECT lengkap (dipakai di GET & setelah write) ──
$SELECT_SQL = "
SELECT
pm.*,
w.kelurahan, w.kecamatan, w.kota,
ri.nama AS nama_ibadah,
ri.jenis AS jenis_ibadah,
u.nama AS nama_surveyor,
v.nama AS nama_verifikator,
fp.file_path AS foto_utama,
(
SELECT GROUP_CONCAT(jb.kode ORDER BY jb.kode SEPARATOR ',')
FROM bantuan_penduduk bp
JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id
WHERE bp.penduduk_id = pm.id AND bp.status = 'aktif'
) AS kode_bantuan,
(
SELECT COUNT(*) FROM bantuan_penduduk bp
WHERE bp.penduduk_id = pm.id AND bp.status = 'aktif'
) AS jumlah_bantuan
FROM penduduk_miskin pm
LEFT JOIN wilayah w ON w.id = pm.wilayah_id
LEFT JOIN rumah_ibadah ri ON ri.id = pm.rumah_ibadah_id
LEFT JOIN users u ON u.id = pm.surveyor_id
LEFT JOIN users v ON v.id = pm.verified_by
LEFT JOIN foto_rumah fp ON fp.penduduk_id = pm.id AND fp.is_primary = 1
";
try {
switch ($method) {
// ════════════════════════════════════════════════════════
// GET Ambil data penduduk
// ════════════════════════════════════════════════════════
case 'GET':
$id = $_GET['id'] ?? null;
$kelurahan = $_GET['kelurahan'] ?? null;
$kecamatan = $_GET['kecamatan'] ?? null;
$kategori = $_GET['kategori'] ?? null;
$bantuan = $_GET['bantuan'] ?? null; // 'ada'|'tidak'
$verifikasi = $_GET['verifikasi'] ?? null;
$surveyor = $_GET['surveyor_id']?? null;
$search = $_GET['search'] ?? null;
// GET single
if ($id) {
$stmt = $db->prepare($SELECT_SQL . " WHERE pm.id = ?");
$stmt->execute([$id]);
$row = $stmt->fetch();
if (!$row) jsonResponse(['success' => false, 'error' => 'Data tidak ditemukan'], 404);
// Ambil semua foto
$fotoStmt = $db->prepare("SELECT * FROM foto_rumah WHERE penduduk_id = ? ORDER BY is_primary DESC, created_at ASC");
$fotoStmt->execute([$id]);
$row['fotos'] = $fotoStmt->fetchAll();
// Ambil semua bantuan
$bantuanStmt = $db->prepare("
SELECT bp.*, jb.kode, jb.nama AS nama_bantuan, jb.warna_badge
FROM bantuan_penduduk bp
JOIN jenis_bantuan jb ON jb.id = bp.bantuan_id
WHERE bp.penduduk_id = ?
ORDER BY jb.kode
");
$bantuanStmt->execute([$id]);
$row['bantuans'] = $bantuanStmt->fetchAll();
jsonResponse(['success' => true, 'data' => $row]);
}
// GET list dengan filter
$sql = $SELECT_SQL . " WHERE 1=1";
$params = [];
if ($kelurahan) { $sql .= " AND w.kelurahan = ?"; $params[] = $kelurahan; }
if ($kecamatan) { $sql .= " AND w.kecamatan = ?"; $params[] = $kecamatan; }
if ($kategori) { $sql .= " AND pm.kategori_kemiskinan = ?"; $params[] = $kategori; }
if ($verifikasi) { $sql .= " AND pm.status_verifikasi = ?"; $params[] = $verifikasi; }
if ($surveyor) { $sql .= " AND pm.surveyor_id = ?"; $params[] = $surveyor; }
if ($search) { $sql .= " AND (pm.nama_kk LIKE ? OR pm.nik_kk LIKE ? OR pm.alamat LIKE ?)";
$params[] = "%$search%"; $params[] = "%$search%"; $params[] = "%$search%"; }
if ($bantuan === 'tidak') {
$sql .= " AND (SELECT COUNT(*) FROM bantuan_penduduk bp WHERE bp.penduduk_id = pm.id AND bp.status='aktif') = 0";
} elseif ($bantuan === 'ada') {
$sql .= " AND (SELECT COUNT(*) FROM bantuan_penduduk bp WHERE bp.penduduk_id = pm.id AND bp.status='aktif') > 0";
}
$sql .= " ORDER BY pm.nama_kk ASC";
$stmt = $db->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
// Statistik ringkas
$stats = [
'total' => count($rows),
'total_jiwa' => array_sum(array_column($rows, 'jumlah_anggota')),
'penerima_bantuan' => count(array_filter($rows, fn($r) => $r['jumlah_bantuan'] > 0)),
'belum_verifikasi' => count(array_filter($rows, fn($r) => $r['status_verifikasi'] === 'belum_diverifikasi')),
];
jsonResponse(['success' => true, 'data' => $rows, 'stats' => $stats]);
break;
// ════════════════════════════════════════════════════════
// POST Tambah penduduk baru
// ════════════════════════════════════════════════════════
case 'POST':
$in = getInput();
// Validasi wajib
$required = ['nik_kk', 'nama_kk', 'lat', 'lng'];
foreach ($required as $f) {
if (empty($in[$f])) jsonResponse(['success' => false, 'error' => "Field '$f' wajib diisi"], 400);
}
// Cek NIK duplikat
$cek = $db->prepare("SELECT id FROM penduduk_miskin WHERE nik_kk = ?");
$cek->execute([trim($in['nik_kk'])]);
if ($cek->fetch()) jsonResponse(['success' => false, 'error' => 'NIK sudah terdaftar di sistem'], 409);
// Hitung kategori otomatis
$penghasilan = isset($in['penghasilan_bulanan']) && $in['penghasilan_bulanan'] !== ''
? (float)$in['penghasilan_bulanan'] : null;
$kategori = hitungKategori($penghasilan);
$wilayahId = isset($in['wilayah_id']) && $in['wilayah_id'] !== '' ? (int)$in['wilayah_id'] : null;
$surveyorId = isset($in['surveyor_id']) && $in['surveyor_id'] !== '' ? (int)$in['surveyor_id'] : null;
$statusPekerjaan = $in['status_pekerjaan'] ?? 'tidak_bekerja';
$allowedPekerjaan = ['tidak_bekerja','buruh_harian','petani','nelayan','pedagang','pegawai_swasta','wiraswasta','lainnya'];
if (!in_array($statusPekerjaan, $allowedPekerjaan)) {
$statusPekerjaan = 'lainnya';
}
$db->prepare("
INSERT INTO penduduk_miskin (
nik_kk, nama_kk, no_hp, alamat, wilayah_id, rt, rw, lat, lng,
jumlah_anggota, jumlah_anak_sekolah,
penghasilan_bulanan, sumber_penghasilan, status_pekerjaan,
kategori_kemiskinan, kondisi_rumah, kepemilikan_rumah,
punya_bpjs, jenis_bpjs, status_verifikasi, surveyor_id
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
")->execute([
trim($in['nik_kk']),
trim($in['nama_kk']),
$in['no_hp'] ?? null,
$in['alamat'] ?? null,
$wilayahId,
$in['rt'] ?? null,
$in['rw'] ?? null,
(float)$in['lat'],
(float)$in['lng'],
max(1, (int)($in['jumlah_anggota'] ?? 1)),
max(0, (int)($in['jumlah_anak_sekolah'] ?? 0)),
$penghasilan,
$in['sumber_penghasilan'] ?? null,
$statusPekerjaan,
$kategori,
$in['kondisi_rumah'] ?? 'tidak_layak',
$in['kepemilikan_rumah'] ?? 'milik_sendiri',
(int)($in['punya_bpjs'] ?? 0),
$in['jenis_bpjs'] ?? 'tidak_punya',
'belum_diverifikasi',
$surveyorId,
]);
$newId = (int)$db->lastInsertId();
assignRumahIbadah($newId);
// Catat riwayat & notifikasi tidak block jika gagal (misal user_id tidak ada)
try {
catatRiwayat($newId, (int)($surveyorId ?? 1), 'submit', 'Data baru dari survei');
} catch(\Exception $e) { /* abaikan error audit trail */ }
try {
$db->prepare("
INSERT INTO notifikasi (jenis, judul, pesan, penduduk_id)
VALUES ('belum_verifikasi', 'Data Baru Belum Diverifikasi', ?, ?)
")->execute(["Data KK {$in['nama_kk']} (NIK: {$in['nik_kk']}) menunggu verifikasi admin.", $newId]);
} catch(\Exception $e) { /* abaikan error notifikasi */ }
$stmt = $db->prepare($SELECT_SQL . " WHERE pm.id = ?");
$stmt->execute([$newId]);
jsonResponse(['success' => true, 'data' => $stmt->fetch(), 'message' => 'Data penduduk berhasil ditambahkan']);
break;
// ════════════════════════════════════════════════════════
// PUT Update data penduduk
// ════════════════════════════════════════════════════════
case 'PUT':
$in = getInput();
$id = (int)($in['id'] ?? $_GET['id'] ?? 0);
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
// Ambil data lama untuk audit
$old = $db->prepare("SELECT * FROM penduduk_miskin WHERE id = ?");
$old->execute([$id]);
$dataLama = $old->fetch();
if (!$dataLama) jsonResponse(['success' => false, 'error' => 'Data tidak ditemukan'], 404);
// Hitung kategori ulang jika penghasilan berubah
$penghasilan = array_key_exists('penghasilan_bulanan', $in)
? (($in['penghasilan_bulanan'] !== '') ? (float)$in['penghasilan_bulanan'] : null)
: (float)$dataLama['penghasilan_bulanan'];
$kategori = hitungKategori($penghasilan);
$allowed = [
'nama_kk','no_hp','alamat','wilayah_id','rt','rw','lat','lng',
'jumlah_anggota','jumlah_anak_sekolah','penghasilan_bulanan',
'sumber_penghasilan','status_pekerjaan','kondisi_rumah',
'kepemilikan_rumah','punya_bpjs','jenis_bpjs',
'status_verifikasi','catatan_verifikasi'
];
$fields = []; $values = []; $posChanged = false;
foreach ($allowed as $f) {
if (!array_key_exists($f, $in)) continue;
$fields[] = "$f = ?";
if ($f === 'lat' || $f === 'lng') { $values[] = (float)$in[$f]; $posChanged = true; }
elseif ($f === 'wilayah_id') { $values[] = (isset($in[$f]) && $in[$f] !== '') ? (int)$in[$f] : null; }
elseif ($f === 'status_pekerjaan') {
$statusPekerjaan = $in[$f] ?? 'tidak_bekerja';
$allowedPekerjaan = ['tidak_bekerja','buruh_harian','petani','nelayan','pedagang','pegawai_swasta','wiraswasta','lainnya'];
$values[] = in_array($statusPekerjaan, $allowedPekerjaan) ? $statusPekerjaan : 'lainnya';
}
elseif (in_array($f, ['jumlah_anggota','jumlah_anak_sekolah','punya_bpjs'])) $values[] = (int)$in[$f];
elseif ($f === 'penghasilan_bulanan') $values[] = $penghasilan;
else $values[] = trim((string)$in[$f]);
}
// Update kategori otomatis
$fields[] = "kategori_kemiskinan = ?";
$values[] = $kategori;
// Jika diverifikasi, catat verifikator
if (isset($in['status_verifikasi']) && $in['status_verifikasi'] === 'terverifikasi') {
$fields[] = "verified_by = ?"; $values[] = $in['verified_by'] ?? 1;
$fields[] = "verified_at = NOW()";
}
if (empty($fields)) jsonResponse(['success' => false, 'error' => 'Tidak ada data untuk diupdate'], 400);
$values[] = $id;
$db->prepare("UPDATE penduduk_miskin SET " . implode(', ', $fields) . " WHERE id = ?")
->execute($values);
if ($posChanged) assignRumahIbadah($id);
$aksi = 'update';
if (isset($in['status_verifikasi'])) {
if ($in['status_verifikasi'] === 'terverifikasi') {
$aksi = 'verifikasi';
} elseif ($in['status_verifikasi'] === 'ditolak') {
$aksi = 'tolak';
}
}
try {
catatRiwayat($id, (int)($in['updated_by'] ?? 1), $aksi, $in['catatan_verifikasi'] ?? null, $dataLama);
} catch(\Exception $e) { /* abaikan error audit trail */ }
$stmt = $db->prepare($SELECT_SQL . " WHERE pm.id = ?");
$stmt->execute([$id]);
jsonResponse(['success' => true, 'data' => $stmt->fetch(), 'message' => 'Data berhasil diperbarui']);
break;
// ════════════════════════════════════════════════════════
// DELETE Hapus penduduk
// ════════════════════════════════════════════════════════
case 'DELETE':
$id = (int)($_GET['id'] ?? getInput()['id'] ?? 0);
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
// Hapus foto fisik terlebih dahulu
$fotos = $db->prepare("SELECT file_path FROM foto_rumah WHERE penduduk_id = ?");
$fotos->execute([$id]);
foreach ($fotos->fetchAll() as $foto) {
$path = __DIR__ . '/../' . $foto['file_path'];
if (file_exists($path)) @unlink($path);
}
catatRiwayat($id, 1, 'hapus', 'Data dihapus oleh admin');
$db->prepare("DELETE FROM penduduk_miskin WHERE id = ?")->execute([$id]);
jsonResponse(['success' => true, 'message' => 'Data penduduk berhasil dihapus']);
break;
default:
jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405);
}
} catch (\Throwable $e) {
jsonResponse(['success' => false, 'error' => 'Database error: ' . $e->getMessage()], 500);
}
+131
View File
@@ -0,0 +1,131 @@
<?php
// api/rumah_ibadah.php
// CRUD API untuk Rumah Ibadah
require_once __DIR__ . '/../includes/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$method = $_SERVER['REQUEST_METHOD'];
$db = getDB();
switch ($method) {
// ── GET semua rumah ibadah + summary penduduk
case 'GET':
$id = $_GET['id'] ?? null;
if ($id) {
$stmt = $db->prepare("
SELECT ri.*,
COUNT(pm.id) as jumlah_kk,
COALESCE(SUM(pm.jumlah_anggota), 0) as total_jiwa
FROM rumah_ibadah ri
LEFT JOIN penduduk_miskin pm ON pm.rumah_ibadah_id = ri.id
WHERE ri.id = ?
GROUP BY ri.id
");
$stmt->execute([$id]);
$row = $stmt->fetch();
if ($row) jsonResponse(['success' => true, 'data' => $row]);
else jsonResponse(['success' => false, 'error' => 'Not found'], 404);
}
$rows = $db->query("
SELECT ri.*,
COUNT(pm.id) as jumlah_kk,
COALESCE(SUM(pm.jumlah_anggota), 0) as total_jiwa
FROM rumah_ibadah ri
LEFT JOIN penduduk_miskin pm ON pm.rumah_ibadah_id = ri.id
GROUP BY ri.id
ORDER BY ri.nama ASC
")->fetchAll();
jsonResponse(['success' => true, 'data' => $rows]);
break;
// ── POST buat rumah ibadah baru
case 'POST':
$input = getInput();
$required = ['nama', 'lat', 'lng'];
foreach ($required as $field) {
if (empty($input[$field])) {
jsonResponse(['success' => false, 'error' => "Field '$field' wajib diisi"], 400);
}
}
$stmt = $db->prepare("
INSERT INTO rumah_ibadah (nama, jenis, alamat, pic, no_wa, lat, lng, radius)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
trim($input['nama']),
$input['jenis'] ?? 'masjid',
$input['alamat'] ?? '',
$input['pic'] ?? '',
$input['no_wa'] ?? '',
(float)$input['lat'],
(float)$input['lng'],
(int)($input['radius'] ?? 500),
]);
$newId = (int)$db->lastInsertId();
reassignAll();
$stmt2 = $db->prepare("SELECT * FROM rumah_ibadah WHERE id = ?");
$stmt2->execute([$newId]);
jsonResponse(['success' => true, 'data' => $stmt2->fetch(), 'message' => 'Rumah ibadah berhasil ditambahkan']);
break;
// ── PUT update rumah ibadah
case 'PUT':
$input = getInput();
$id = $input['id'] ?? $_GET['id'] ?? null;
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
$fields = [];
$values = [];
$allowed = ['nama', 'jenis', 'alamat', 'pic', 'no_wa', 'lat', 'lng', 'radius'];
foreach ($allowed as $f) {
if (isset($input[$f])) {
$fields[] = "$f = ?";
$values[] = ($f === 'lat' || $f === 'lng') ? (float)$input[$f] :
($f === 'radius' ? (int)$input[$f] : trim($input[$f]));
}
}
if (empty($fields)) jsonResponse(['success' => false, 'error' => 'Tidak ada data yang diupdate'], 400);
$values[] = $id;
$db->prepare("UPDATE rumah_ibadah SET " . implode(', ', $fields) . " WHERE id = ?")->execute($values);
// Radius berubah → reassign semua
if (isset($input['radius']) || isset($input['lat']) || isset($input['lng'])) {
reassignAll();
}
$stmt = $db->prepare("SELECT * FROM rumah_ibadah WHERE id = ?");
$stmt->execute([$id]);
jsonResponse(['success' => true, 'data' => $stmt->fetch(), 'message' => 'Berhasil diupdate']);
break;
// ── DELETE hapus rumah ibadah
case 'DELETE':
$id = $_GET['id'] ?? getInput()['id'] ?? null;
if (!$id) jsonResponse(['success' => false, 'error' => 'ID diperlukan'], 400);
// Lepas referensi penduduk
$db->prepare("UPDATE penduduk_miskin SET rumah_ibadah_id = NULL, jarak_ke_ibadah = NULL WHERE rumah_ibadah_id = ?")
->execute([$id]);
$db->prepare("DELETE FROM rumah_ibadah WHERE id = ?")->execute([$id]);
reassignAll();
jsonResponse(['success' => true, 'message' => 'Berhasil dihapus']);
break;
default:
jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405);
}
+29
View File
@@ -0,0 +1,29 @@
<?php
// api/update_radius.php
// Update radius satu rumah ibadah + reassign penduduk
require_once __DIR__ . '/../includes/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$input = getInput();
$id = $input['id'] ?? null;
$radius = $input['radius'] ?? null;
if (!$id || !$radius) {
jsonResponse(['success' => false, 'error' => 'id dan radius diperlukan'], 400);
}
$radius = max(100, min(5000, (int)$radius)); // clamp 1005000m
$db = getDB();
$db->prepare("UPDATE rumah_ibadah SET radius = ? WHERE id = ?")->execute([$radius, $id]);
reassignAll();
jsonResponse(['success' => true, 'radius' => $radius, 'message' => "Radius diubah ke {$radius}m, penduduk di-reassign"]);
+68
View File
@@ -0,0 +1,68 @@
<?php
// api/wilayah.php v2.0
// ============================================================
// API Data Wilayah (RT/RW/Kelurahan/Kecamatan)
// ============================================================
require_once __DIR__ . '/../includes/config.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$method = $_SERVER['REQUEST_METHOD'];
$db = getDB();
switch ($method) {
// ── GET semua wilayah atau per filter ─────────────────
case 'GET':
$kelurahan = $_GET['kelurahan'] ?? null;
$kecamatan = $_GET['kecamatan'] ?? null;
// GET /api/wilayah.php?list=kelurahan → distinct kelurahan
if (isset($_GET['list'])) {
$type = $_GET['list'];
if ($type === 'kelurahan') {
$rows = $db->query("SELECT DISTINCT kelurahan, kecamatan FROM wilayah ORDER BY kelurahan")->fetchAll();
jsonResponse(['success' => true, 'data' => $rows]);
}
if ($type === 'kecamatan') {
$rows = $db->query("SELECT DISTINCT kecamatan FROM wilayah ORDER BY kecamatan")->fetchAll();
jsonResponse(['success' => true, 'data' => array_column($rows, 'kecamatan')]);
}
}
$sql = "SELECT * FROM wilayah WHERE 1=1";
$params = [];
if ($kelurahan) { $sql .= " AND kelurahan = ?"; $params[] = $kelurahan; }
if ($kecamatan) { $sql .= " AND kecamatan = ?"; $params[] = $kecamatan; }
$sql .= " ORDER BY kelurahan, rt, rw";
$stmt = $db->prepare($sql);
$stmt->execute($params);
jsonResponse(['success' => true, 'data' => $stmt->fetchAll()]);
break;
// ── POST tambah wilayah ───────────────────────────────
case 'POST':
$in = getInput();
if (empty($in['kelurahan']) || empty($in['kecamatan'])) {
jsonResponse(['success' => false, 'error' => 'Kelurahan dan kecamatan wajib diisi'], 400);
}
$db->prepare("INSERT INTO wilayah (kelurahan, kecamatan, kota, rt, rw) VALUES (?,?,?,?,?)")
->execute([
trim($in['kelurahan']),
trim($in['kecamatan']),
trim($in['kota'] ?? 'Pontianak'),
$in['rt'] ?? null,
$in['rw'] ?? null,
]);
jsonResponse(['success' => true, 'id' => $db->lastInsertId(), 'message' => 'Wilayah ditambahkan']);
break;
default:
jsonResponse(['success' => false, 'error' => 'Method tidak diizinkan'], 405);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+363
View File
@@ -0,0 +1,363 @@
-- ============================================================
-- SIG Penduduk Miskin v2.0 Database Schema
-- WebGIS Pendataan & Monitoring Bantuan Sosial
-- Update: 2026 | Versi: 2.0
-- ============================================================
CREATE DATABASE IF NOT EXISTS sig_kemiskinan CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE sig_kemiskinan;
SET FOREIGN_KEY_CHECKS = 0;
-- ============================================================
-- TABEL 1: users
-- ============================================================
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL COMMENT 'bcrypt hash',
role ENUM('admin','petugas','pimpinan') NOT NULL DEFAULT 'petugas',
wilayah_tugas VARCHAR(100) NULL COMMENT 'Kelurahan/RT wilayah tugas petugas',
aktif TINYINT(1) DEFAULT 1,
last_login TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_role (role)
);
-- ============================================================
-- TABEL 2: wilayah
-- ============================================================
CREATE TABLE IF NOT EXISTS wilayah (
id INT AUTO_INCREMENT PRIMARY KEY,
kelurahan VARCHAR(100) NOT NULL,
kecamatan VARCHAR(100) NOT NULL,
kota VARCHAR(100) NOT NULL DEFAULT 'Pontianak',
rt VARCHAR(10) NULL,
rw VARCHAR(10) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_kelurahan (kelurahan),
INDEX idx_kecamatan (kecamatan)
);
-- ============================================================
-- TABEL 3: rumah_ibadah (Tidak berubah)
-- ============================================================
CREATE TABLE IF NOT EXISTS rumah_ibadah (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(255) NOT NULL,
jenis ENUM('masjid','musholla','gereja','pura','vihara','klenteng','lainnya') DEFAULT 'masjid',
alamat TEXT,
pic VARCHAR(255) COMMENT 'Person In Charge / Penanggung Jawab',
no_wa VARCHAR(20),
lat DECIMAL(10, 8) NOT NULL,
lng DECIMAL(11, 8) NOT NULL,
radius INT DEFAULT 500 COMMENT 'Radius dalam meter',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- ============================================================
-- TABEL 4: penduduk_miskin (DIPERLUAS dari v1.1)
-- ============================================================
CREATE TABLE IF NOT EXISTS penduduk_miskin (
id INT AUTO_INCREMENT PRIMARY KEY,
-- Identitas Kepala Keluarga
nik_kk VARCHAR(16) NOT NULL UNIQUE COMMENT 'NIK KK 16 digit',
nama_kk VARCHAR(255) NOT NULL COMMENT 'Nama Kepala Keluarga',
no_hp VARCHAR(20) NULL COMMENT 'No HP/WhatsApp',
-- Lokasi
alamat TEXT NULL,
wilayah_id INT NULL,
rt VARCHAR(10) NULL,
rw VARCHAR(10) NULL,
lat DECIMAL(10,8) NOT NULL,
lng DECIMAL(11,8) NOT NULL,
-- Data Keluarga
jumlah_anggota INT NOT NULL DEFAULT 1,
jumlah_anak_sekolah INT NOT NULL DEFAULT 0,
-- Data Ekonomi
penghasilan_bulanan DECIMAL(12,2) NULL COMMENT 'Penghasilan per bulan (Rp)',
sumber_penghasilan VARCHAR(255) NULL,
status_pekerjaan ENUM(
'tidak_bekerja','buruh_harian','petani','nelayan',
'pedagang','pegawai_swasta','wiraswasta','lainnya'
) DEFAULT 'tidak_bekerja',
-- Kategori Kemiskinan (dihitung otomatis)
kategori_kemiskinan ENUM('sangat_miskin','miskin','hampir_miskin','mampu') DEFAULT 'sangat_miskin',
-- Kondisi Rumah
kondisi_rumah ENUM('sangat_tidak_layak','tidak_layak','layak') DEFAULT 'tidak_layak',
kepemilikan_rumah ENUM('milik_sendiri','kontrak','menumpang','lainnya') DEFAULT 'milik_sendiri',
-- Status BPJS
punya_bpjs TINYINT(1) DEFAULT 0,
jenis_bpjs ENUM('PBI','mandiri','tidak_punya') DEFAULT 'tidak_punya',
-- Relasi ke Rumah Ibadah (kompatibel v1.1)
rumah_ibadah_id INT NULL,
jarak_ke_ibadah DECIMAL(10,2) NULL COMMENT 'Jarak dalam meter',
-- Status Verifikasi
status_verifikasi ENUM('belum_diverifikasi','terverifikasi','ditolak') DEFAULT 'belum_diverifikasi',
catatan_verifikasi TEXT NULL,
-- Audit Trail
surveyor_id INT NULL COMMENT 'FK users - petugas input',
verified_by INT NULL COMMENT 'FK users - admin verifikator',
verified_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Foreign Keys
CONSTRAINT fk_pm_wilayah FOREIGN KEY (wilayah_id) REFERENCES wilayah(id) ON DELETE SET NULL,
CONSTRAINT fk_pm_ibadah FOREIGN KEY (rumah_ibadah_id) REFERENCES rumah_ibadah(id) ON DELETE SET NULL,
CONSTRAINT fk_pm_surveyor FOREIGN KEY (surveyor_id) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_pm_verif FOREIGN KEY (verified_by) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_nik (nik_kk),
INDEX idx_lat_lng (lat, lng),
INDEX idx_kategori (kategori_kemiskinan),
INDEX idx_wilayah (wilayah_id),
INDEX idx_status_v (status_verifikasi),
INDEX idx_surveyor (surveyor_id)
);
-- ============================================================
-- TABEL 5: foto_rumah
-- ============================================================
CREATE TABLE IF NOT EXISTS foto_rumah (
id INT AUTO_INCREMENT PRIMARY KEY,
penduduk_id INT NOT NULL,
file_path VARCHAR(500) NOT NULL COMMENT 'Path relatif dari root web',
lat_foto DECIMAL(10,8) NULL COMMENT 'Koordinat GPS saat foto diambil',
lng_foto DECIMAL(11,8) NULL,
accuracy_meter DECIMAL(8,2) NULL COMMENT 'Akurasi GPS dalam meter',
is_primary TINYINT(1) DEFAULT 0 COMMENT '1 = foto utama (thumbnail peta)',
ukuran_kb INT NULL,
keterangan VARCHAR(255) NULL,
diambil_oleh INT NULL COMMENT 'FK users',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_fr_penduduk FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE CASCADE,
CONSTRAINT fk_fr_user FOREIGN KEY (diambil_oleh) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_penduduk (penduduk_id),
INDEX idx_primary (penduduk_id, is_primary)
);
-- ============================================================
-- TABEL 6: jenis_bantuan
-- ============================================================
CREATE TABLE IF NOT EXISTS jenis_bantuan (
id INT AUTO_INCREMENT PRIMARY KEY,
kode VARCHAR(20) NOT NULL UNIQUE,
nama VARCHAR(100) NOT NULL,
deskripsi TEXT NULL,
warna_badge VARCHAR(7) DEFAULT '#4f8ef7' COMMENT 'Hex color badge',
aktif TINYINT(1) DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ============================================================
-- TABEL 7: bantuan_penduduk (Many-to-Many pivot)
-- ============================================================
CREATE TABLE IF NOT EXISTS bantuan_penduduk (
id INT AUTO_INCREMENT PRIMARY KEY,
penduduk_id INT NOT NULL,
bantuan_id INT NOT NULL,
status ENUM('aktif','tidak_aktif','dicabut') DEFAULT 'aktif',
tanggal_mulai DATE NULL,
tanggal_selesai DATE NULL,
nominal_bulanan DECIMAL(12,2) NULL,
nomor_kpj VARCHAR(50) NULL COMMENT 'Nomor kartu/KPJ bantuan',
keterangan TEXT NULL,
diinput_oleh INT NULL COMMENT 'FK users',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_bp_penduduk FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE CASCADE,
CONSTRAINT fk_bp_bantuan FOREIGN KEY (bantuan_id) REFERENCES jenis_bantuan(id) ON DELETE RESTRICT,
CONSTRAINT fk_bp_user FOREIGN KEY (diinput_oleh) REFERENCES users(id) ON DELETE SET NULL,
UNIQUE KEY uq_penduduk_bantuan (penduduk_id, bantuan_id),
INDEX idx_penduduk (penduduk_id),
INDEX idx_bantuan (bantuan_id),
INDEX idx_status (status)
);
-- ============================================================
-- TABEL 8: riwayat_verifikasi (Audit Trail)
-- ============================================================
CREATE TABLE IF NOT EXISTS riwayat_verifikasi (
id INT AUTO_INCREMENT PRIMARY KEY,
penduduk_id INT NOT NULL,
user_id INT NOT NULL,
aksi ENUM('submit','verifikasi','tolak','update','hapus') NOT NULL,
catatan TEXT NULL,
data_lama JSON NULL COMMENT 'Snapshot data sebelum perubahan',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_rv_penduduk FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE CASCADE,
CONSTRAINT fk_rv_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT,
INDEX idx_penduduk (penduduk_id),
INDEX idx_user (user_id),
INDEX idx_tanggal (created_at)
);
-- ============================================================
-- TABEL 9: notifikasi
-- ============================================================
CREATE TABLE IF NOT EXISTS notifikasi (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NULL COMMENT 'NULL = broadcast semua admin',
jenis ENUM('belum_bantuan','belum_verifikasi','data_usang','info') DEFAULT 'info',
judul VARCHAR(255) NOT NULL,
pesan TEXT NOT NULL,
penduduk_id INT NULL COMMENT 'Referensi penduduk terkait',
sudah_dibaca TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_notif_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_notif_penduduk FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE SET NULL,
INDEX idx_user_baca (user_id, sudah_dibaca),
INDEX idx_jenis (jenis)
);
-- ============================================================
-- INDEX SPASIAL
-- ============================================================
CREATE INDEX idx_ri_lat_lng ON rumah_ibadah(lat, lng);
SET FOREIGN_KEY_CHECKS = 1;
-- ============================================================
-- DATA AWAL: users
-- ============================================================
INSERT INTO users (nama, username, password, role) VALUES
('Administrator', 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin'),
('Petugas Survei', 'petugas', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'petugas'),
('Kepala Dinas', 'pimpinan', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'pimpinan');
-- Password semua akun: "password" (bcrypt)
-- ============================================================
-- DATA AWAL: wilayah
-- ============================================================
INSERT INTO wilayah (kelurahan, kecamatan, kota, rt, rw) VALUES
('Siantan Tengah', 'Pontianak Utara', 'Pontianak', '01', '01'),
('Siantan Tengah', 'Pontianak Utara', 'Pontianak', '02', '01'),
('Siantan Hilir', 'Pontianak Utara', 'Pontianak', '01', '02'),
('Batu Layang', 'Pontianak Utara', 'Pontianak', '01', '01'),
('Batu Layang', 'Pontianak Utara', 'Pontianak', '02', '02'),
('Saigon', 'Pontianak Timur', 'Pontianak', '01', '01'),
('Saigon', 'Pontianak Timur', 'Pontianak', '02', '01'),
('Tanjung Hulu', 'Pontianak Timur', 'Pontianak', '01', '01'),
('Parit Mayor', 'Pontianak Timur', 'Pontianak', '01', '01'),
('Pontianak Kota', 'Pontianak Kota', 'Pontianak', '01', '01');
-- ============================================================
-- DATA AWAL: rumah_ibadah
-- ============================================================
INSERT INTO rumah_ibadah (nama, jenis, alamat, pic, no_wa, lat, lng, radius) VALUES
('Masjid Al-Ikhlas', 'masjid', 'Jl. Merdeka No. 1, Pontianak', 'Ahmad Fauzi', '081234567890', -0.0263303, 109.3425094, 500),
('Masjid Ar-Rahmat', 'masjid', 'Jl. Gajah Mada No. 45, Pontianak', 'Budi Santoso', '082345678901', -0.0320000, 109.3380000, 500),
('Gereja Bethel', 'gereja', 'Jl. Diponegoro No. 12, Pontianak', 'Pendeta Yusuf', '083456789012', -0.0290000, 109.3450000, 500);
-- ============================================================
-- DATA AWAL: jenis_bantuan
-- ============================================================
INSERT INTO jenis_bantuan (kode, nama, deskripsi, warna_badge) VALUES
('PKH', 'Program Keluarga Harapan', 'Bantuan tunai bersyarat untuk KK miskin', '#22c55e'),
('BPNT', 'Bantuan Pangan Non Tunai', 'Sembako via e-warong setiap 2 bulan', '#3b82f6'),
('KIS', 'Kartu Indonesia Sehat', 'Akses layanan kesehatan gratis (BPJS PBI)', '#ec4899'),
('BLT', 'Bantuan Langsung Tunai', 'Bantuan tunai darurat/sementara dari desa/kel', '#f59e0b'),
('BST', 'Bantuan Sosial Tunai', 'Bantuan sosial tunai dari Kemensos', '#8b5cf6'),
('PIP', 'Program Indonesia Pintar', 'Bantuan pendidikan untuk anak sekolah', '#06b6d4');
-- ============================================================
-- DATA AWAL: penduduk_miskin (contoh)
-- ============================================================
INSERT INTO penduduk_miskin (
nik_kk, nama_kk, no_hp, alamat, wilayah_id, rt, rw,
lat, lng, jumlah_anggota, jumlah_anak_sekolah,
penghasilan_bulanan, sumber_penghasilan, status_pekerjaan,
kategori_kemiskinan, kondisi_rumah, kepemilikan_rumah,
punya_bpjs, jenis_bpjs, status_verifikasi, surveyor_id
) VALUES
('6171010101010001', 'Suyatno', '081200000001', 'Jl. Merdeka Gg. III No. 5', 1, '01', '01', -0.0270000, 109.3430000, 4, 2, 850000, 'Buruh bangunan', 'buruh_harian', 'miskin', 'tidak_layak', 'menumpang', 1, 'PBI', 'terverifikasi', 1),
('6171010101010002', 'Rohani', '081200000002', 'Jl. Merdeka Gg. IV No. 2', 1, '02', '01', -0.0255000, 109.3420000, 3, 1, 450000, 'Jualan kecil', 'pedagang', 'sangat_miskin', 'sangat_tidak_layak','menumpang', 1, 'PBI', 'terverifikasi', 1),
('6171010101010003', 'Hamid Husin', '081200000003', 'Jl. Gajah Mada Gg. II No. 8', 2, '01', '02', -0.0315000, 109.3375000, 5, 3, 1200000, 'Ojek online', 'buruh_harian', 'mampu', 'tidak_layak', 'kontrak', 0, 'tidak_punya','belum_diverifikasi',2),
('6171010101010004', 'Yanti Susanti', '081200000004', 'Jl. Veteran No. 99', 3, '01', '01', -0.0500000, 109.3600000, 2, 0, 300000, 'Tidak ada', 'tidak_bekerja', 'sangat_miskin', 'sangat_tidak_layak','menumpang', 1, 'PBI', 'terverifikasi', 1),
('6171010101010005', 'Supriyadi', '081200000005', 'Jl. Budi Utomo No. 12', 4, '02', '01', -0.0340000, 109.3510000, 6, 4, 600000, 'Petani kecil', 'petani', 'miskin', 'tidak_layak', 'milik_sendiri', 1, 'PBI', 'terverifikasi', 1),
('6171010101010006', 'Marlina', '081200000006', 'Gg. Damai No. 3', 5, '01', '02', -0.0410000, 109.3480000, 3, 1, 200000, 'Buruh cuci', 'buruh_harian', 'sangat_miskin', 'sangat_tidak_layak','menumpang', 0, 'tidak_punya','belum_diverifikasi',2),
('6171010101010007', 'Junaidi', '081200000007', 'Jl. Khatulistiwa No. 45', 6, '01', '01', -0.0380000, 109.3550000, 4, 2, 950000, 'Nelayan', 'nelayan', 'miskin', 'tidak_layak', 'milik_sendiri', 1, 'mandiri', 'terverifikasi', 1),
('6171010101010008', 'Siti Aminah', '081200000008', 'Gg. Melati No. 7', 7, '02', '02', -0.0290000, 109.3395000, 5, 3, 400000, 'Pembantu RT', 'buruh_harian', 'sangat_miskin', 'sangat_tidak_layak','menumpang', 1, 'PBI', 'belum_diverifikasi',2),
('6171010101010009', 'Rahman', '081200000009', 'Jl. Tanjung Pura No. 88', 8, '01', '01', -0.0220000, 109.3460000, 3, 1, 750000, 'Pedagang kaki lima','pedagang', 'miskin', 'tidak_layak', 'kontrak', 0, 'tidak_punya','terverifikasi', 1),
('6171010101010010', 'Dewi Kartika', '081200000010', 'Gg. Bahagia No. 2', 9, '01', '01', -0.0450000, 109.3530000, 4, 2, 500000, 'Buruh pabrik', 'buruh_harian', 'sangat_miskin', 'tidak_layak', 'kontrak', 1, 'PBI', 'terverifikasi', 1);
-- ============================================================
-- DATA AWAL: bantuan_penduduk
-- ============================================================
INSERT INTO bantuan_penduduk (penduduk_id, bantuan_id, status, tanggal_mulai, nominal_bulanan, diinput_oleh) VALUES
(1, 1, 'aktif', '2024-01-01', 500000, 1), -- Suyatno: PKH
(1, 2, 'aktif', '2024-01-01', 200000, 1), -- Suyatno: BPNT
(2, 1, 'aktif', '2023-07-01', 500000, 1), -- Rohani: PKH
(2, 3, 'aktif', '2023-07-01', 0, 1), -- Rohani: KIS
(4, 1, 'aktif', '2024-03-01', 500000, 1), -- Yanti: PKH
(4, 3, 'aktif', '2024-03-01', 0, 1), -- Yanti: KIS
(5, 2, 'aktif', '2024-02-01', 200000, 1), -- Supriyadi: BPNT
(7, 3, 'aktif', '2023-09-01', 0, 1), -- Junaidi: KIS
(9, 2, 'aktif', '2024-01-01', 200000, 1), -- Rahman: BPNT
(10, 1, 'aktif','2024-04-01', 500000, 1), -- Dewi: PKH
(10, 3, 'aktif','2024-04-01', 0, 1); -- Dewi: KIS
-- ============================================================
-- Update rumah_ibadah assignment (kompatibel v1.1)
-- ============================================================
UPDATE penduduk_miskin pm
SET
rumah_ibadah_id = (
SELECT ri.id FROM rumah_ibadah ri
WHERE (
6371000 * ACOS(GREATEST(-1, LEAST(1,
COS(RADIANS(pm.lat)) * COS(RADIANS(ri.lat)) *
COS(RADIANS(ri.lng) - RADIANS(pm.lng)) +
SIN(RADIANS(pm.lat)) * SIN(RADIANS(ri.lat))
)))
) <= ri.radius
ORDER BY (
6371000 * ACOS(GREATEST(-1, LEAST(1,
COS(RADIANS(pm.lat)) * COS(RADIANS(ri.lat)) *
COS(RADIANS(ri.lng) - RADIANS(pm.lng)) +
SIN(RADIANS(pm.lat)) * SIN(RADIANS(ri.lat))
)))
) ASC
LIMIT 1
),
jarak_ke_ibadah = (
SELECT ROUND(
6371000 * ACOS(GREATEST(-1, LEAST(1,
COS(RADIANS(pm.lat)) * COS(RADIANS(ri.lat)) *
COS(RADIANS(ri.lng) - RADIANS(pm.lng)) +
SIN(RADIANS(pm.lat)) * SIN(RADIANS(ri.lat))
))), 2
) FROM rumah_ibadah ri
WHERE (
6371000 * ACOS(GREATEST(-1, LEAST(1,
COS(RADIANS(pm.lat)) * COS(RADIANS(ri.lat)) *
COS(RADIANS(ri.lng) - RADIANS(pm.lng)) +
SIN(RADIANS(pm.lat)) * SIN(RADIANS(ri.lat))
)))
) <= ri.radius
LIMIT 1
);
+30
View File
@@ -0,0 +1,30 @@
<?php
// includes/auth.php v2.0
// ============================================================
// Middleware Autentikasi berbasis PHP Session
// ============================================================
require_once __DIR__ . '/config.php';
session_start();
// ── Cek apakah user sudah login ───────────────────────────
function requireAuth(array $allowedRoles = []): array {
if (!isset($_SESSION['user_id'])) {
jsonResponse(['success' => false, 'error' => 'Unauthorized silakan login', 'redirect' => '/index.html'], 401);
}
$user = $_SESSION['user'];
if (!empty($allowedRoles) && !in_array($user['role'], $allowedRoles)) {
jsonResponse(['success' => false, 'error' => 'Akses ditolak untuk role: ' . $user['role']], 403);
}
return $user;
}
// ── Ambil user saat ini (null jika belum login) ───────────
function currentUser(): ?array {
return $_SESSION['user'] ?? null;
}
// ── Cek apakah sudah login (tanpa redirect) ───────────────
function isLoggedIn(): bool {
return isset($_SESSION['user_id']);
}
+144
View File
@@ -0,0 +1,144 @@
<?php
// includes/config.php v2.0
// ============================================================
// Konfigurasi Database & Konstanta Aplikasi
// ============================================================
// ── Database ──────────────────────────────────────────────
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'sig_kemiskinan');
define('DB_CHARSET', 'utf8mb4');
// ── Aplikasi ──────────────────────────────────────────────
define('APP_NAME', 'SIG Kemiskinan v2.0');
define('NOMINATIM_URL', 'https://nominatim.openstreetmap.org');
// ── Upload Foto ────────────────────────────────────────────
define('UPLOAD_DIR', __DIR__ . '/../assets/img/uploads/');
define('UPLOAD_URL', 'assets/img/uploads/');
define('MAX_FOTO_SIZE', 5 * 1024 * 1024); // 5 MB
define('ALLOWED_EXT', ['jpg','jpeg','png','webp']);
// ── Threshold Kategori Kemiskinan (Rp/bulan) ──────────────
// Disesuaikan dengan standar BPS & UMR Kalimantan Barat 2024
define('BATAS_SANGAT_MISKIN', 1500000); // < 1.5jt → sangat_miskin
define('BATAS_MISKIN', 3000000); // < 3jt → miskin
define('BATAS_HAMPIR_MISKIN', 5000000); // < 5jt → hampir_miskin
// >= 5jt → mampu
// ── Notifikasi ────────────────────────────────────────────
define('DATA_USANG_HARI', 30); // data > 30 hari belum update = notif usang
// ============================================================
// FUNGSI: Koneksi Database (Singleton PDO)
// ============================================================
function getDB(): PDO {
static $pdo = null;
if ($pdo === null) {
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
} catch (PDOException $e) {
http_response_code(500);
die(json_encode(['success' => false, 'error' => 'Database connection failed: ' . $e->getMessage()]));
}
}
return $pdo;
}
// ============================================================
// FUNGSI: Response JSON
// ============================================================
function jsonResponse(mixed $data, int $code = 200): void {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
// ============================================================
// FUNGSI: Baca Input (JSON body atau $_POST)
// ============================================================
function getInput(): array {
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$data = $_POST;
}
return $data ?? [];
}
// ============================================================
// FUNGSI: Hitung Jarak Haversine (meter)
// ============================================================
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
$R = 6371000;
$dLat = deg2rad($lat2 - $lat1);
$dLng = deg2rad($lng2 - $lng1);
$a = sin($dLat/2)**2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng/2)**2;
return $R * 2 * atan2(sqrt($a), sqrt(1 - $a));
}
// ============================================================
// FUNGSI: Hitung Kategori Kemiskinan Otomatis
// ============================================================
function hitungKategori(?float $penghasilan): string {
if ($penghasilan === null || $penghasilan < BATAS_SANGAT_MISKIN) return 'sangat_miskin';
if ($penghasilan < BATAS_MISKIN) return 'miskin';
if ($penghasilan < BATAS_HAMPIR_MISKIN) return 'hampir_miskin';
return 'mampu';
}
// ============================================================
// FUNGSI: Assign Rumah Ibadah (kompatibel v1.1)
// ============================================================
function assignRumahIbadah(int $pendudukId): void {
try {
$db = getDB();
$p = $db->prepare("SELECT lat, lng FROM penduduk_miskin WHERE id = ?");
$p->execute([$pendudukId]);
$row = $p->fetch();
if (!$row) return;
$ibadahList = $db->query("SELECT id, lat, lng, radius FROM rumah_ibadah")->fetchAll();
$closest = null;
$closestDist = PHP_FLOAT_MAX;
foreach ($ibadahList as $ri) {
$dist = haversine((float)$row['lat'], (float)$row['lng'], (float)$ri['lat'], (float)$ri['lng']);
if ($dist <= $ri['radius'] && $dist < $closestDist) {
$closest = $ri['id'];
$closestDist = $dist;
}
}
$db->prepare("UPDATE penduduk_miskin SET rumah_ibadah_id = ?, jarak_ke_ibadah = ? WHERE id = ?")
->execute([$closest, $closest ? round($closestDist, 2) : null, $pendudukId]);
} catch (\Exception $e) { /* abaikan error assign ibadah */ }
}
function reassignAll(): void {
$db = getDB();
$all = $db->query("SELECT id FROM penduduk_miskin")->fetchAll();
foreach ($all as $row) assignRumahIbadah((int)$row['id']);
}
// ============================================================
// FUNGSI: Catat Riwayat Verifikasi
// ============================================================
function catatRiwayat(int $pendudukId, int $userId, string $aksi, ?string $catatan = null, ?array $dataLama = null): void {
$db = getDB();
$db->prepare("
INSERT INTO riwayat_verifikasi (penduduk_id, user_id, aksi, catatan, data_lama)
VALUES (?, ?, ?, ?, ?)
")->execute([$pendudukId, $userId, $aksi, $catatan, $dataLama ? json_encode($dataLama, JSON_UNESCAPED_UNICODE) : null]);
}
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
<?php
require_once __DIR__ . '/includes/config.php';
header('Content-Type: text/plain');
try {
$db = getDB();
// 1. Update ENUM to include 'mampu'
echo "Updating ENUM for kategori_kemiskinan...\n";
$db->exec("ALTER TABLE penduduk_miskin MODIFY kategori_kemiskinan ENUM('sangat_miskin','miskin','hampir_miskin','mampu') DEFAULT 'sangat_miskin'");
echo "ENUM updated successfully!\n\n";
// 2. Re-calculate all existing records
echo "Re-calculating categories for existing records...\n";
$stmt = $db->query("SELECT id, penghasilan_bulanan FROM penduduk_miskin");
$rows = $stmt->fetchAll();
$update = $db->prepare("UPDATE penduduk_miskin SET kategori_kemiskinan = ? WHERE id = ?");
$counts = ['sangat_miskin' => 0, 'miskin' => 0, 'hampir_miskin' => 0, 'mampu' => 0];
foreach ($rows as $row) {
$p = $row['penghasilan_bulanan'] !== null ? (float)$row['penghasilan_bulanan'] : null;
if ($p === null || $p < BATAS_SANGAT_MISKIN) $kat = 'sangat_miskin';
elseif ($p < BATAS_MISKIN) $kat = 'miskin';
elseif ($p < BATAS_HAMPIR_MISKIN) $kat = 'hampir_miskin';
else $kat = 'mampu';
$update->execute([$kat, $row['id']]);
$counts[$kat]++;
}
echo "Done! Updated " . count($rows) . " records:\n";
foreach ($counts as $kat => $c) {
echo " $kat: $c\n";
}
} catch (\Exception $e) {
echo "ERROR: " . $e->getMessage() . "\n";
}