UAS SIG Proyek WebGis

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