UAS SIG Proyek WebGis
This commit is contained in:
@@ -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 (100–3000m, 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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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'),
|
||||
]);
|
||||
@@ -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;
|
||||
@@ -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 ?> | 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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
]);
|
||||
@@ -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']]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 100–5000m
|
||||
|
||||
$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"]);
|
||||
@@ -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 |
@@ -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
|
||||
);
|
||||
@@ -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']);
|
||||
}
|
||||
@@ -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
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user