First commit / commit pertama

This commit is contained in:
Mr.Haruna
2026-06-13 11:24:58 +07:00
commit 522c4f7200
166 changed files with 13326 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
<?php
/**
* api/bantuan.php — Status bantuan sosial per rumah tangga (KF-17).
*
* GET ?master=1 → daftar master jenis_bantuan
* GET ?warga_id=ID → daftar bantuan untuk satu warga
* POST {warga_id, jenis_bantuan_id, tgl_mulai, tgl_berakhir, is_aktif, keterangan}
* DELETE ?id=ID
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/../config/activity.php';
$pdo = Database::getConnection();
$method = $_SERVER['REQUEST_METHOD'];
requireOperatorForMutation();
switch ($method) {
case 'GET':
if (isset($_GET['master'])) {
sendSuccess($pdo->query("SELECT id, kode, nama, deskripsi FROM jenis_bantuan ORDER BY id")->fetchAll(), 'Jenis Bantuan');
}
$wargaId = $_GET['warga_id'] ?? null;
if (!$wargaId) sendError('warga_id wajib');
$stmt = $pdo->prepare("
SELECT bs.id, bs.jenis_bantuan_id, jb.kode, jb.nama,
bs.tgl_mulai, bs.tgl_berakhir, bs.is_aktif, bs.keterangan
FROM bantuan_sosial bs
JOIN jenis_bantuan jb ON jb.id = bs.jenis_bantuan_id
WHERE bs.warga_id = ?
ORDER BY bs.id DESC
");
$stmt->execute([$wargaId]);
sendSuccess($stmt->fetchAll(), 'Bantuan warga');
break;
case 'POST':
$d = getInput();
if (empty($d['warga_id']) || empty($d['jenis_bantuan_id'])) sendError('warga_id & jenis_bantuan_id wajib');
$pdo->prepare("
INSERT INTO bantuan_sosial (warga_id, jenis_bantuan_id, tgl_mulai, tgl_berakhir, is_aktif, keterangan)
VALUES (?,?,?,?,?,?)
")->execute([
$d['warga_id'], $d['jenis_bantuan_id'],
($d['tgl_mulai'] ?? '') ?: null, ($d['tgl_berakhir'] ?? '') ?: null,
isset($d['is_aktif']) ? (int)$d['is_aktif'] : 1, ($d['keterangan'] ?? '') ?: null,
]);
$id = (int)$pdo->lastInsertId();
logActivity($pdo, 'create', 'bantuan_sosial', $id, "Tambah bantuan warga #{$d['warga_id']}");
sendSuccess(['id' => $id], 'Bantuan ditambahkan', 201);
break;
case 'DELETE':
$id = $_GET['id'] ?? null;
if (!$id) sendError('ID wajib');
$pdo->prepare("DELETE FROM bantuan_sosial WHERE id=?")->execute([$id]);
logActivity($pdo, 'delete', 'bantuan_sosial', (int)$id, 'Hapus bantuan');
sendSuccess(null, 'Bantuan dihapus');
break;
default:
sendError('Method not allowed', 405);
}
+53
View File
@@ -0,0 +1,53 @@
<?php
/**
* api/cakupan_bantuan.php — Analisis cakupan bantuan (KF-22).
* GET → ringkasan cakupan keseluruhan + per jenis bantuan + per kelurahan.
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/helpers.php';
$pdo = Database::getConnection();
requireApiAnyRole(['operator', 'pimpinan']);
if ($_SERVER['REQUEST_METHOD'] !== 'GET') sendError('Method not allowed', 405);
$total = (int)$pdo->query("SELECT COUNT(*) FROM warga_miskin")->fetchColumn();
$dibantu = (int)$pdo->query("SELECT COUNT(DISTINCT warga_id) FROM bantuan_sosial WHERE is_aktif=1")->fetchColumn();
// Per jenis bantuan
$perJenis = $pdo->query("
SELECT jb.kode, jb.nama,
COUNT(DISTINCT CASE WHEN bs.is_aktif=1 THEN bs.warga_id END) AS penerima
FROM jenis_bantuan jb
LEFT JOIN bantuan_sosial bs ON bs.jenis_bantuan_id = jb.id
GROUP BY jb.id ORDER BY jb.id
")->fetchAll();
foreach ($perJenis as &$j) {
$j['penerima'] = (int)$j['penerima'];
$j['persen'] = $total > 0 ? round($j['penerima'] / $total * 100, 1) : 0;
}
unset($j);
// Per kelurahan
$perWilayah = $pdo->query("
SELECT w.nama,
COUNT(DISTINCT wm.id) AS miskin,
COUNT(DISTINCT CASE WHEN bs.id IS NOT NULL THEN wm.id END) AS dibantu
FROM wilayah w
LEFT JOIN warga_miskin wm ON ST_Contains(w.geom, wm.geom)
LEFT JOIN bantuan_sosial bs ON bs.warga_id = wm.id AND bs.is_aktif=1
GROUP BY w.id ORDER BY w.nama
")->fetchAll();
foreach ($perWilayah as &$w) {
$w['miskin'] = (int)$w['miskin']; $w['dibantu'] = (int)$w['dibantu'];
$w['cakupan'] = $w['miskin'] > 0 ? round($w['dibantu'] / $w['miskin'] * 100, 1) : 0;
}
unset($w);
sendSuccess([
'total_warga' => $total,
'dibantu' => $dibantu,
'belum_dibantu' => $total - $dibantu,
'cakupan_persen' => $total > 0 ? round($dibantu / $total * 100, 1) : 0,
'per_jenis' => $perJenis,
'per_wilayah' => $perWilayah,
], 'Cakupan Bantuan');
+21
View File
@@ -0,0 +1,21 @@
<?php
/**
* api/fasilitas.php — GeoJSON fasilitas publik (untuk peta & proximity).
* GET → FeatureCollection titik fasilitas (sekolah/puskesmas/pasar/dll).
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/helpers.php';
$pdo = Database::getConnection();
if ($_SERVER['REQUEST_METHOD'] !== 'GET') sendError('Method not allowed', 405);
$stmt = $pdo->query("SELECT id, nama, jenis, ST_AsGeoJSON(geom) AS geojson FROM fasilitas_publik ORDER BY jenis, nama");
$features = [];
while ($r = $stmt->fetch()) {
$features[] = [
'type' => 'Feature',
'geometry' => json_decode($r['geojson']),
'properties' => ['id' => (int)$r['id'], 'nama' => $r['nama'], 'jenis' => $r['jenis']],
];
}
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Fasilitas Publik');
+65
View File
@@ -0,0 +1,65 @@
<?php
/**
* api/filter.php — Filter multi-kriteria warga miskin (KF-19).
* GET params (semua opsional):
* wilayah_id, sumber_air, material_dinding, status_kepemilikan, status_listrik,
* verif (status_verifikasi), pekerjaan (LIKE),
* bantuan_jenis (id) + bantuan_mode (menerima|tidak)
* Mengembalikan GeoJSON warga yang cocok + jumlah.
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/helpers.php';
$pdo = Database::getConnection();
requireApiAnyRole(['operator', 'pimpinan']);
if ($_SERVER['REQUEST_METHOD'] !== 'GET') sendError('Method not allowed', 405);
$where = [];
$params = [];
$eq = function ($col, $key) use (&$where, &$params) {
if (isset($_GET[$key]) && $_GET[$key] !== '') { $where[] = "w.$col = ?"; $params[] = $_GET[$key]; }
};
$eq('wilayah_id', 'wilayah_id');
$eq('sumber_air', 'sumber_air');
$eq('material_dinding', 'material_dinding');
$eq('status_kepemilikan', 'status_kepemilikan');
$eq('status_listrik', 'status_listrik');
$eq('status_verifikasi', 'verif');
if (!empty($_GET['pekerjaan'])) { $where[] = "w.pekerjaan_kk LIKE ?"; $params[] = '%' . $_GET['pekerjaan'] . '%'; }
if (!empty($_GET['bantuan_jenis'])) {
$mode = ($_GET['bantuan_mode'] ?? 'menerima') === 'tidak' ? 'NOT EXISTS' : 'EXISTS';
$where[] = "$mode (SELECT 1 FROM bantuan_sosial bs WHERE bs.warga_id=w.id AND bs.jenis_bantuan_id=? AND bs.is_aktif=1)";
$params[] = $_GET['bantuan_jenis'];
}
$sql = "SELECT w.id, w.nama_kk, w.nik_kk, w.penghasilan, w.jumlah_jiwa, w.sumber_air,
w.material_dinding, w.status_verifikasi, w.pekerjaan_kk, wl.nama AS kelurahan,
ST_AsGeoJSON(w.geom) AS geojson,
(SELECT GROUP_CONCAT(jb.kode SEPARATOR ',') FROM bantuan_sosial bs
JOIN jenis_bantuan jb ON jb.id=bs.jenis_bantuan_id
WHERE bs.warga_id=w.id AND bs.is_aktif=1) AS bantuan_aktif
FROM warga_miskin w LEFT JOIN wilayah wl ON wl.id=w.wilayah_id";
if ($where) $sql .= " WHERE " . implode(' AND ', $where);
$sql .= " ORDER BY w.id DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$features = [];
while ($r = $stmt->fetch()) {
$features[] = [
'type' => 'Feature',
'geometry' => json_decode($r['geojson']),
'properties' => [
'id' => (int)$r['id'], 'nama_kk' => $r['nama_kk'], 'nik_kk' => $r['nik_kk'],
'penghasilan' => $r['penghasilan'], 'jumlah_jiwa' => $r['jumlah_jiwa'],
'sumber_air' => $r['sumber_air'], 'material_dinding' => $r['material_dinding'],
'status_verifikasi' => $r['status_verifikasi'], 'pekerjaan_kk' => $r['pekerjaan_kk'],
'kelurahan' => $r['kelurahan'], 'bantuan_aktif' => $r['bantuan_aktif'],
],
];
}
sendSuccess(['type' => 'FeatureCollection', 'features' => $features, 'jumlah' => count($features)], 'Hasil Filter');
+110
View File
@@ -0,0 +1,110 @@
<?php
/**
* api/foto.php — Upload & kelola foto kondisi rumah (KF-13).
*
* GET ?warga_id=ID → daftar foto {id, url}
* POST multipart: warga_id + foto[] (15) → kompres & simpan
* DELETE ?id=ID → hapus foto
*
* Foto dikompres (maks lebar 1024px, JPEG q75) memakai GD.
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/../config/app.php';
require_once __DIR__ . '/../config/activity.php';
$pdo = Database::getConnection();
$method = $_SERVER['REQUEST_METHOD'];
// GET: cukup login. Mutasi: operator atau enumerator.
if ($method !== 'GET') requireApiAnyRole(['operator', 'enumerator']);
const MAX_FOTO = 5;
const UPLOAD_DIR = __DIR__ . '/../uploads/rumah/';
function compressImage(string $src, string $dst, int $maxW = 1024, int $quality = 75): bool {
$info = @getimagesize($src);
if (!$info) return false;
[$w, $h] = $info;
switch ($info['mime']) {
case 'image/jpeg': $img = imagecreatefromjpeg($src); break;
case 'image/png': $img = imagecreatefrompng($src); break;
case 'image/webp': $img = imagecreatefromwebp($src); break;
default: return false;
}
if (!$img) return false;
$nw = $w > $maxW ? $maxW : $w;
$nh = (int)round($h * ($nw / $w));
$canvas = imagecreatetruecolor($nw, $nh);
// latar putih untuk PNG transparan
imagefill($canvas, 0, 0, imagecolorallocate($canvas, 255, 255, 255));
imagecopyresampled($canvas, $img, 0, 0, 0, 0, $nw, $nh, $w, $h);
$ok = imagejpeg($canvas, $dst, $quality);
imagedestroy($img);
imagedestroy($canvas);
return $ok;
}
switch ($method) {
case 'GET':
$wargaId = $_GET['warga_id'] ?? null;
if (!$wargaId) sendError('warga_id wajib');
$stmt = $pdo->prepare("SELECT id, file_path FROM foto_rumah WHERE warga_id=? ORDER BY id");
$stmt->execute([$wargaId]);
$out = [];
while ($r = $stmt->fetch()) {
$out[] = ['id' => (int)$r['id'], 'url' => app_url($r['file_path'])];
}
sendSuccess($out, 'Foto rumah');
break;
case 'POST':
$wargaId = $_POST['warga_id'] ?? null;
if (!$wargaId) sendError('warga_id wajib');
if (empty($_FILES['foto'])) sendError('Tidak ada file diunggah');
$existing = (int)$pdo->query("SELECT COUNT(*) FROM foto_rumah WHERE warga_id=" . (int)$wargaId)->fetchColumn();
// Normalisasi $_FILES['foto'] ke array
$files = $_FILES['foto'];
$names = is_array($files['name']) ? $files['name'] : [$files['name']];
$tmps = is_array($files['tmp_name']) ? $files['tmp_name'] : [$files['tmp_name']];
if (!is_dir(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0775, true);
$saved = 0;
foreach ($tmps as $i => $tmp) {
if ($existing + $saved >= MAX_FOTO) break;
if (!is_uploaded_file($tmp)) continue;
$fname = 'w' . (int)$wargaId . '_' . uniqid() . '.jpg';
$dst = UPLOAD_DIR . $fname;
if (compressImage($tmp, $dst)) {
$rel = 'uploads/rumah/' . $fname;
$pdo->prepare("INSERT INTO foto_rumah (warga_id, file_path) VALUES (?,?)")
->execute([$wargaId, $rel]);
$saved++;
}
}
if ($saved === 0) sendError('Gagal memproses foto (format harus JPG/PNG/WEBP)');
logActivity($pdo, 'create', 'foto_rumah', (int)$wargaId, "Upload {$saved} foto");
sendSuccess(['uploaded' => $saved], "{$saved} foto diunggah", 201);
break;
case 'DELETE':
$id = $_GET['id'] ?? null;
if (!$id) sendError('ID wajib');
$row = $pdo->prepare("SELECT file_path FROM foto_rumah WHERE id=?");
$row->execute([$id]);
$fp = $row->fetchColumn();
if ($fp) {
$abs = __DIR__ . '/../' . $fp;
if (is_file($abs)) @unlink($abs);
$pdo->prepare("DELETE FROM foto_rumah WHERE id=?")->execute([$id]);
logActivity($pdo, 'delete', 'foto_rumah', (int)$id, 'Hapus foto');
}
sendSuccess(null, 'Foto dihapus');
break;
default:
sendError('Method not allowed', 405);
}
+31
View File
@@ -0,0 +1,31 @@
<?php
/**
* api/heatmap.php — Titik kepadatan kemiskinan agregat (KF-08).
*
* GET → daftar [lat, lng, weight] tanpa data pribadi (aman untuk publik).
* weight = jumlah_tanggungan + 1 (intensitas kepadatan).
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/helpers.php';
$pdo = Database::getConnection();
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
sendError('Method not allowed', 405);
}
$stmt = $pdo->query(
"SELECT ST_Y(geom) AS lat, ST_X(geom) AS lng, jumlah_tanggungan
FROM warga_miskin"
);
$points = [];
while ($r = $stmt->fetch()) {
$points[] = [
(float)$r['lat'],
(float)$r['lng'],
(int)$r['jumlah_tanggungan'] + 1,
];
}
sendSuccess($points, 'Heatmap Kepadatan');
+67
View File
@@ -0,0 +1,67 @@
<?php
require_once __DIR__ . '/../config/session.php';
/**
* Shared response helper for all API endpoints
*/
header('Content-Type: application/json');
$allowedOrigin = getenv('CORS_ALLOWED_ORIGIN');
if ($allowedOrigin !== false && $allowedOrigin !== '') {
header('Access-Control-Allow-Origin: ' . $allowedOrigin);
header('Vary: Origin');
header('Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
function sendSuccess($data, string $message = 'OK', int $code = 200): void {
http_response_code($code);
echo json_encode(['status' => 'success', 'message' => $message, 'data' => $data]);
exit;
}
function sendError(string $message = 'Bad Request', int $code = 400): void {
http_response_code($code);
echo json_encode(['status' => 'error', 'message' => $message, 'data' => null]);
exit;
}
function getInput(): array {
return json_decode(file_get_contents('php://input'), true) ?? [];
}
function requireApiRole(string $role): void {
requireApiAnyRole([$role]);
}
function requireApiAnyRole(array $roles): void {
if (session_status() === PHP_SESSION_NONE) {
startAppSession();
}
if (!isset($_SESSION['user_id'], $_SESSION['role'])) {
sendError('Unauthorized', 401);
}
if (!in_array($_SESSION['role'], $roles, true)) {
sendError('Forbidden', 403);
}
}
/**
* Mutasi data (POST/PUT/DELETE) hanya untuk Operator.
* GET tetap terbuka sesuai kebutuhan endpoint.
*/
function requireOperatorForMutation(): void {
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
requireApiAnyRole(['operator']);
}
}
/**
* Alias kompatibilitas lama.
*/
function requireAdminForMutation(): void {
requireOperatorForMutation();
}
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* api/komparatif.php — Indikator komparatif antar kelurahan (KF-31).
* GET → labels + dataset: persentase_miskin, cakupan_bantuan, jumlah_miskin, persen_verifikasi.
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/helpers.php';
$pdo = Database::getConnection();
requireApiAnyRole(['operator', 'pimpinan']);
if ($_SERVER['REQUEST_METHOD'] !== 'GET') sendError('Method not allowed', 405);
$rows = $pdo->query("
SELECT w.nama, w.jumlah_penduduk,
COUNT(DISTINCT wm.id) AS miskin,
COUNT(DISTINCT CASE WHEN bs.id IS NOT NULL THEN wm.id END) AS dibantu,
COUNT(DISTINCT CASE WHEN wm.status_verifikasi='terverifikasi' THEN wm.id END) AS terverif
FROM wilayah w
LEFT JOIN warga_miskin wm ON ST_Contains(w.geom, wm.geom)
LEFT JOIN bantuan_sosial bs ON bs.warga_id = wm.id AND bs.is_aktif=1
GROUP BY w.id ORDER BY w.nama
")->fetchAll();
$labels = []; $persenMiskin = []; $cakupan = []; $jumlahMiskin = []; $persenVerif = [];
foreach ($rows as $r) {
$miskin = (int)$r['miskin']; $penduduk = (int)$r['jumlah_penduduk'];
$labels[] = $r['nama'];
$persenMiskin[] = $penduduk > 0 ? round($miskin / $penduduk * 100, 2) : 0;
$cakupan[] = $miskin > 0 ? round($r['dibantu'] / $miskin * 100, 1) : 0;
$jumlahMiskin[] = $miskin;
$persenVerif[] = $miskin > 0 ? round($r['terverif'] / $miskin * 100, 1) : 0;
}
sendSuccess([
'labels' => $labels,
'persentase_miskin' => $persenMiskin,
'cakupan_bantuan' => $cakupan,
'jumlah_miskin' => $jumlahMiskin,
'persen_verifikasi' => $persenVerif,
], 'Komparatif Wilayah');
+104
View File
@@ -0,0 +1,104 @@
<?php
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/session.php';
startAppSession();
header('Content-Type: application/json');
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
exit;
}
$pdo = Database::getConnection();
$method = $_SERVER['REQUEST_METHOD'];
try {
if ($method === 'GET') {
// Jika parameter my_reports=1, hanya laporan milik user yang login
// Jika tidak dan role=admin, ambil semua.
$my_reports = $_GET['my_reports'] ?? 0;
$sql = "SELECT l.*, ST_X(l.geometry) as lng, ST_Y(l.geometry) as lat, u.nama_lengkap, u.username
FROM laporan_warga l
JOIN users u ON l.user_id = u.id ";
if ($my_reports || $_SESSION['role'] !== 'admin') {
// User biasa hanya boleh melihat laporannya sendiri di tabel dashboard,
// tapi mungkin butuh lihat semua laporan di peta publik?
// Untuk peta public (semua laporan disetujui atau diproses), tambahkan parameter public=1
if (isset($_GET['public']) && $_GET['public'] == 1) {
$sql .= " WHERE l.status IN ('diproses', 'selesai') ";
} else {
$sql .= " WHERE l.user_id = " . (int)$_SESSION['user_id'];
}
}
$sql .= " ORDER BY l.id DESC";
$rows = $pdo->query($sql)->fetchAll();
$features = [];
foreach ($rows as $row) {
$features[] = [
'type' => 'Feature',
'properties' => [
'id' => $row['id'],
'user_id' => $row['user_id'],
'pelapor' => $row['nama_lengkap'] ?: $row['username'],
'kategori' => $row['kategori'],
'deskripsi' => $row['deskripsi'],
'status' => $row['status'],
'created_at' => $row['created_at']
],
'geometry' => [
'type' => 'Point',
'coordinates' => [(float)$row['lng'], (float)$row['lat']]
]
];
}
echo json_encode([
'type' => 'FeatureCollection',
'features' => $features
]);
}
elseif ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
if (!isset($input['kategori'], $input['deskripsi'], $input['geometry'])) {
throw new Exception("Data tidak lengkap.");
}
$coords = $input['geometry']['coordinates'];
$stmt = $pdo->prepare("INSERT INTO laporan_warga (user_id, kategori, deskripsi, geometry) VALUES (?, ?, ?, ST_GeomFromText(?))");
$stmt->execute([
$_SESSION['user_id'],
$input['kategori'],
$input['deskripsi'],
"POINT({$coords[0]} {$coords[1]})"
]);
echo json_encode(['status' => 'success', 'message' => 'Laporan berhasil dibuat.']);
}
elseif ($method === 'PUT') {
if ($_SESSION['role'] !== 'admin') throw new Exception("Unauthorized");
$id = $_GET['id'] ?? 0;
$input = json_decode(file_get_contents('php://input'), true);
$status = $input['status'] ?? 'menunggu';
$stmt = $pdo->prepare("UPDATE laporan_warga SET status = ? WHERE id = ?");
$stmt->execute([$status, $id]);
echo json_encode(['status' => 'success', 'message' => 'Status laporan diperbarui.']);
}
elseif ($method === 'DELETE') {
$id = $_GET['id'] ?? 0;
// User bisa hapus kalau status menunggu. Admin bisa hapus semua.
if ($_SESSION['role'] === 'admin') {
$stmt = $pdo->prepare("DELETE FROM laporan_warga WHERE id = ?");
$stmt->execute([$id]);
} else {
$stmt = $pdo->prepare("DELETE FROM laporan_warga WHERE id = ? AND user_id = ? AND status = 'menunggu'");
$stmt->execute([$id, $_SESSION['user_id']]);
}
echo json_encode(['status' => 'success', 'message' => 'Laporan dihapus.']);
}
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
+67
View File
@@ -0,0 +1,67 @@
<?php
/**
* api/laporan_bantuan.php — Rekapitulasi penerima bantuan per jenis (KF-30).
* ?format=print (default) | json
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/auth_check.php';
requireAnyRole(['operator', 'pimpinan']);
$pdo = Database::getConnection();
$format = $_GET['format'] ?? 'print';
$rows = $pdo->query("
SELECT jb.kode, jb.nama AS jenis_nama, w.nama_kk, w.nik_kk,
bs.tgl_mulai, bs.tgl_berakhir, w.status_verifikasi,
(SELECT wl.nama FROM wilayah wl WHERE ST_Contains(wl.geom, w.geom) LIMIT 1) AS kelurahan
FROM bantuan_sosial bs
JOIN warga_miskin w ON w.id = bs.warga_id
JOIN jenis_bantuan jb ON jb.id = bs.jenis_bantuan_id
WHERE bs.is_aktif = 1
ORDER BY jb.id, w.nama_kk
")->fetchAll();
// kelompokkan per jenis
$grup = [];
foreach ($rows as $r) $grup[$r['kode'] . ' — ' . $r['jenis_nama']][] = $r;
if ($format === 'json') {
header('Content-Type: application/json');
echo json_encode(['status' => 'success', 'data' => ['total' => count($rows), 'jenis' => array_map('count', $grup)]]);
exit;
}
?>
<!DOCTYPE html><html lang="id"><head><meta charset="UTF-8">
<title>Rekap Penerima Bantuan</title>
<style>
body{font-family:Arial,sans-serif;margin:28px;color:#111;}
h2{margin:0 0 2px;} .sub{color:#666;font-size:0.85rem;margin-bottom:16px;}
h3{margin:18px 0 6px;background:#2563EB;color:#fff;padding:6px 10px;border-radius:6px;font-size:0.95rem;}
table{width:100%;border-collapse:collapse;font-size:0.78rem;margin-bottom:8px;}
th,td{border:1px solid #ccc;padding:5px 7px;text-align:left;} th{background:#f1f5f9;}
button{padding:8px 14px;background:#2563EB;color:#fff;border:none;border-radius:6px;cursor:pointer;margin-bottom:14px;}
@media print{button{display:none;}}
</style></head><body>
<button onclick="window.print()">🖨️ Cetak / Simpan PDF</button>
<h2>Rekapitulasi Penerima Bantuan Sosial</h2>
<div class="sub">WebGIS Pemetaan Kemiskinan · dicetak <?= date('d M Y H:i') ?> · total <?= count($rows) ?> penerima aktif</div>
<?php if (!$grup): ?><p>Belum ada penerima bantuan aktif.</p><?php endif; ?>
<?php foreach ($grup as $judul => $list): ?>
<h3><?= htmlspecialchars($judul) ?> (<?= count($list) ?>)</h3>
<table>
<thead><tr><th>#</th><th>Nama KK</th><th>NIK</th><th>Kelurahan</th><th>Mulai</th><th>Berakhir</th><th>Verifikasi</th></tr></thead>
<tbody>
<?php foreach ($list as $i => $r): ?>
<tr>
<td><?= $i+1 ?></td>
<td><?= htmlspecialchars($r['nama_kk']) ?></td>
<td><?= htmlspecialchars($r['nik_kk'] ?: '-') ?></td>
<td><?= htmlspecialchars($r['kelurahan'] ?: '-') ?></td>
<td><?= htmlspecialchars($r['tgl_mulai'] ?: '-') ?></td>
<td><?= htmlspecialchars($r['tgl_berakhir'] ?: '-') ?></td>
<td><?= htmlspecialchars($r['status_verifikasi']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach; ?>
</body></html>
+92
View File
@@ -0,0 +1,92 @@
<?php
/**
* api/laporan_wilayah.php — Laporan per kelurahan (KF-29).
* ?wilayah_id=ID [&format=print|json] (default print)
* Daftar warga dihitung secara spasial (ST_Contains).
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/auth_check.php';
requireAnyRole(['operator', 'pimpinan']);
$pdo = Database::getConnection();
$wid = $_GET['wilayah_id'] ?? null;
$format = $_GET['format'] ?? 'print';
if (!$wid) { http_response_code(400); echo 'wilayah_id wajib'; exit; }
$wil = $pdo->prepare("SELECT id, nama, jumlah_penduduk FROM wilayah WHERE id=?");
$wil->execute([$wid]);
$w = $wil->fetch();
if (!$w) { http_response_code(404); echo 'Wilayah tidak ditemukan'; exit; }
$stmt = $pdo->prepare("
SELECT wm.id, wm.nik_kk, wm.nama_kk, wm.penghasilan, wm.jumlah_jiwa,
wm.sumber_air, wm.material_dinding, wm.status_verifikasi,
(SELECT GROUP_CONCAT(jb.kode SEPARATOR ', ') FROM bantuan_sosial bs
JOIN jenis_bantuan jb ON jb.id=bs.jenis_bantuan_id
WHERE bs.warga_id=wm.id AND bs.is_aktif=1) AS bantuan
FROM warga_miskin wm
JOIN wilayah w ON w.id = ?
WHERE ST_Contains(w.geom, wm.geom)
ORDER BY wm.nama_kk
");
$stmt->execute([$wid]);
$rows = $stmt->fetchAll();
$miskin = count($rows);
$dibantu = count(array_filter($rows, fn($r) => !empty($r['bantuan'])));
$terverif = count(array_filter($rows, fn($r) => $r['status_verifikasi'] === 'terverifikasi'));
$persen = $w['jumlah_penduduk'] > 0 ? round($miskin / $w['jumlah_penduduk'] * 100, 2) : 0;
$cakupan = $miskin > 0 ? round($dibantu / $miskin * 100, 1) : 0;
if ($format === 'json') {
header('Content-Type: application/json');
echo json_encode(['status' => 'success', 'data' => [
'nama' => $w['nama'], 'penduduk' => (int)$w['jumlah_penduduk'],
'miskin' => $miskin, 'dibantu' => $dibantu, 'terverif' => $terverif,
'persen' => $persen, 'cakupan' => $cakupan,
]]);
exit;
}
?>
<!DOCTYPE html><html lang="id"><head><meta charset="UTF-8">
<title>Laporan <?= htmlspecialchars($w['nama']) ?></title>
<style>
body{font-family:Arial,sans-serif;margin:28px;color:#111;}
h2{margin:0 0 2px;} .sub{color:#666;font-size:0.85rem;margin-bottom:16px;}
.kpi{display:flex;gap:12px;margin-bottom:18px;flex-wrap:wrap;}
.kpi .b{border:1px solid #ddd;border-radius:8px;padding:10px 16px;min-width:120px;}
.kpi .n{font-size:1.4rem;font-weight:bold;} .kpi .l{font-size:0.75rem;color:#666;}
table{width:100%;border-collapse:collapse;font-size:0.76rem;}
th,td{border:1px solid #ccc;padding:5px 7px;text-align:left;} th{background:#f1f5f9;}
button{padding:8px 14px;background:#2563EB;color:#fff;border:none;border-radius:6px;cursor:pointer;margin-bottom:14px;}
@media print{button{display:none;}}
</style></head><body>
<button onclick="window.print()">🖨️ Cetak / Simpan PDF</button>
<h2>Laporan Kemiskinan — <?= htmlspecialchars($w['nama']) ?></h2>
<div class="sub">WebGIS Pemetaan Kemiskinan · dicetak <?= date('d M Y H:i') ?></div>
<div class="kpi">
<div class="b"><div class="n"><?= $miskin ?></div><div class="l">Rumah Tangga Miskin</div></div>
<div class="b"><div class="n"><?= $persen ?>%</div><div class="l">dari <?= (int)$w['jumlah_penduduk'] ?> KK</div></div>
<div class="b"><div class="n"><?= $cakupan ?>%</div><div class="l">Cakupan Bantuan</div></div>
<div class="b"><div class="n"><?= $terverif ?></div><div class="l">Terverifikasi</div></div>
</div>
<table>
<thead><tr><th>#</th><th>NIK</th><th>Nama KK</th><th>Jiwa</th><th>Penghasilan</th><th>Sumber Air</th><th>Dinding</th><th>Bantuan</th><th>Verifikasi</th></tr></thead>
<tbody>
<?php foreach ($rows as $i => $r): ?>
<tr>
<td><?= $i+1 ?></td>
<td><?= htmlspecialchars($r['nik_kk'] ?: '-') ?></td>
<td><?= htmlspecialchars($r['nama_kk']) ?></td>
<td><?= $r['jumlah_jiwa'] ?: '-' ?></td>
<td>Rp <?= number_format($r['penghasilan'],0,',','.') ?></td>
<td><?= htmlspecialchars($r['sumber_air'] ?: '-') ?></td>
<td><?= htmlspecialchars($r['material_dinding'] ?: '-') ?></td>
<td><?= htmlspecialchars($r['bantuan'] ?: '-') ?></td>
<td><?= htmlspecialchars($r['status_verifikasi']) ?></td>
</tr>
<?php endforeach; ?>
<?php if (!$rows): ?><tr><td colspan="9" style="text-align:center;padding:20px;">Tidak ada data di wilayah ini.</td></tr><?php endif; ?>
</tbody>
</table>
</body></html>
+51
View File
@@ -0,0 +1,51 @@
<?php
/**
* api/proximity.php — Jarak warga ke fasilitas publik terdekat (KF-21).
* GET ?radius=METER (default 1000) [&jenis=sekolah|puskesmas|pasar|...]
* Mengembalikan GeoJSON warga + fasilitas terdekat + jarak + flag terisolasi.
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/helpers.php';
$pdo = Database::getConnection();
requireApiAnyRole(['operator', 'pimpinan']);
if ($_SERVER['REQUEST_METHOD'] !== 'GET') sendError('Method not allowed', 405);
$radius = (float)($_GET['radius'] ?? 1000);
$jenis = $_GET['jenis'] ?? '';
$jenisFilter = $jenis !== '' ? "WHERE fp.jenis = " . $pdo->quote($jenis) : "";
$sql = "
SELECT w.id, w.nama_kk, ST_AsGeoJSON(w.geom) AS geojson,
(SELECT ST_Distance_Sphere(w.geom, fp.geom) FROM fasilitas_publik fp $jenisFilter
ORDER BY ST_Distance_Sphere(w.geom, fp.geom) LIMIT 1) AS jarak_m,
(SELECT fp.nama FROM fasilitas_publik fp $jenisFilter
ORDER BY ST_Distance_Sphere(w.geom, fp.geom) LIMIT 1) AS fasilitas,
(SELECT fp.jenis FROM fasilitas_publik fp $jenisFilter
ORDER BY ST_Distance_Sphere(w.geom, fp.geom) LIMIT 1) AS fasilitas_jenis
FROM warga_miskin w
ORDER BY jarak_m DESC
";
$stmt = $pdo->query($sql);
$features = [];
$terisolasi = 0;
while ($r = $stmt->fetch()) {
$jarak = $r['jarak_m'] !== null ? round((float)$r['jarak_m']) : null;
$iso = ($jarak !== null && $jarak > $radius);
if ($iso) $terisolasi++;
$features[] = [
'type' => 'Feature',
'geometry' => json_decode($r['geojson']),
'properties' => [
'id' => (int)$r['id'], 'nama_kk' => $r['nama_kk'],
'jarak_m' => $jarak, 'jarak_km' => $jarak !== null ? round($jarak / 1000, 2) : null,
'fasilitas' => $r['fasilitas'], 'fasilitas_jenis' => $r['fasilitas_jenis'],
'terisolasi' => $iso,
],
];
}
sendSuccess([
'type' => 'FeatureCollection', 'features' => $features,
'radius_m' => $radius, 'jumlah_terisolasi' => $terisolasi, 'total' => count($features),
], 'Proximity Analysis');
+45
View File
@@ -0,0 +1,45 @@
<?php
/**
* api/tren.php — Tren waktu jumlah warga & cakupan bantuan (KF-23).
* GET → seri bulanan (12 bulan terakhir): warga baru, kumulatif warga, bantuan baru.
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/helpers.php';
$pdo = Database::getConnection();
requireApiAnyRole(['operator', 'pimpinan']);
if ($_SERVER['REQUEST_METHOD'] !== 'GET') sendError('Method not allowed', 405);
// Bangun 12 bulan terakhir
$months = [];
for ($i = 11; $i >= 0; $i--) {
$months[date('Y-m', strtotime("-$i month"))] = ['warga' => 0, 'bantuan' => 0];
}
foreach ($pdo->query("SELECT DATE_FORMAT(created_at,'%Y-%m') ym, COUNT(*) c FROM warga_miskin GROUP BY ym") as $r) {
if (isset($months[$r['ym']])) $months[$r['ym']]['warga'] = (int)$r['c'];
}
foreach ($pdo->query("SELECT DATE_FORMAT(created_at,'%Y-%m') ym, COUNT(*) c FROM bantuan_sosial GROUP BY ym") as $r) {
if (isset($months[$r['ym']])) $months[$r['ym']]['bantuan'] = (int)$r['c'];
}
// warga sebelum jendela 12 bulan (untuk basis kumulatif)
$firstMonth = array_key_first($months);
$base = (int)$pdo->query("SELECT COUNT(*) FROM warga_miskin WHERE DATE_FORMAT(created_at,'%Y-%m') < '$firstMonth'")->fetchColumn();
$labels = []; $wargaBaru = []; $kumulatif = []; $bantuanBaru = [];
$run = $base;
foreach ($months as $ym => $v) {
$labels[] = $ym;
$wargaBaru[] = $v['warga'];
$run += $v['warga'];
$kumulatif[] = $run;
$bantuanBaru[] = $v['bantuan'];
}
sendSuccess([
'labels' => $labels,
'warga_baru' => $wargaBaru,
'kumulatif' => $kumulatif,
'bantuan_baru' => $bantuanBaru,
], 'Tren Waktu');
+94
View File
@@ -0,0 +1,94 @@
<?php
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/../config/activity.php';
$pdo = Database::getConnection();
$method = $_SERVER['REQUEST_METHOD'];
// Manajemen user hanya untuk Operator (KF-02/03)
requireApiAnyRole(['operator']);
const VALID_ROLES = ['publik', 'enumerator', 'operator', 'pimpinan'];
switch ($method) {
case 'GET':
$stmt = $pdo->query(
"SELECT id, username, email, role, is_active, wilayah_tugas, nama_lengkap, last_login, created_at
FROM users ORDER BY id ASC"
);
sendSuccess($stmt->fetchAll(), 'Data Users');
break;
case 'POST':
$d = getInput();
if (empty($d['username']) || empty($d['password']) || empty($d['role'])) {
sendError('Data tidak lengkap');
}
if (!in_array($d['role'], VALID_ROLES, true)) sendError('Role tidak valid');
$hash = password_hash($d['password'], PASSWORD_BCRYPT, ['cost' => 12]);
try {
$pdo->prepare(
"INSERT INTO users (username, email, password, role, is_active, wilayah_tugas, nama_lengkap)
VALUES (?,?,?,?,?,?,?)"
)->execute([
$d['username'],
$d['email'] ?? null,
$hash,
$d['role'],
isset($d['is_active']) ? (int)$d['is_active'] : 1,
$d['wilayah_tugas'] ?? null,
$d['nama_lengkap'] ?? '',
]);
$id = (int)$pdo->lastInsertId();
logActivity($pdo, 'create', 'users', $id, "Tambah user {$d['username']} ({$d['role']})");
sendSuccess(['id' => $id], 'User disimpan', 201);
} catch (Exception $e) {
sendError('Username sudah digunakan');
}
break;
case 'PUT':
$d = getInput();
$id = $_GET['id'] ?? null;
if (!$id) sendError('ID wajib');
if (!empty($d['role']) && !in_array($d['role'], VALID_ROLES, true)) sendError('Role tidak valid');
if (!empty($d['password'])) {
$hash = password_hash($d['password'], PASSWORD_BCRYPT, ['cost' => 12]);
$pdo->prepare(
"UPDATE users SET username=?, email=?, password=?, role=?, is_active=?, wilayah_tugas=?, nama_lengkap=?
WHERE id=?"
)->execute([
$d['username'], $d['email'] ?? null, $hash, $d['role'],
isset($d['is_active']) ? (int)$d['is_active'] : 1,
$d['wilayah_tugas'] ?? null, $d['nama_lengkap'] ?? '', $id,
]);
} else {
$pdo->prepare(
"UPDATE users SET username=?, email=?, role=?, is_active=?, wilayah_tugas=?, nama_lengkap=?
WHERE id=?"
)->execute([
$d['username'], $d['email'] ?? null, $d['role'],
isset($d['is_active']) ? (int)$d['is_active'] : 1,
$d['wilayah_tugas'] ?? null, $d['nama_lengkap'] ?? '', $id,
]);
}
logActivity($pdo, 'update', 'users', (int)$id, "Edit user {$d['username']}");
sendSuccess(null, 'User diperbarui');
break;
case 'DELETE':
$id = $_GET['id'] ?? null;
if (!$id) sendError('ID wajib');
if ($id == 1) sendError('Akun utama tidak dapat dihapus');
if ($id == ($_SESSION['user_id'] ?? null)) sendError('Tidak dapat menghapus akun sendiri');
$pdo->prepare("DELETE FROM users WHERE id=?")->execute([$id]);
logActivity($pdo, 'delete', 'users', (int)$id, 'Hapus user');
sendSuccess(null, 'User dihapus');
break;
default:
sendError('Method not allowed', 405);
}
+93
View File
@@ -0,0 +1,93 @@
<?php
/**
* api/warga_export.php — Export data warga (KF-16).
* ?format=csv → unduh CSV semua data
* ?format=template → unduh template CSV kosong (Lampiran A)
* ?format=print → halaman cetak (PDF via browser print)
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/auth_check.php';
requireAnyRole(['operator', 'pimpinan']);
$pdo = Database::getConnection();
$format = $_GET['format'] ?? 'csv';
if ($format === 'template') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="template_warga.csv"');
$out = fopen('php://output', 'w');
fputcsv($out, ['nik_kk','nama_kk','kode_kelurahan','latitude','longitude','jumlah_jiwa','sumber_air','status_listrik','material_dinding','pekerjaan_kk']);
fputcsv($out, ['3171010000000001','Contoh Nama','6171010001','-0.0510','109.3610','4','SUNGAI','NON_PLN','TIDAK_PERMANEN','Buruh Harian']);
fclose($out);
exit;
}
$rows = $pdo->query("
SELECT w.id, w.nik_kk, w.nama_kk, wl.nama AS kelurahan, w.alamat, w.rt, w.rw,
w.jumlah_jiwa, w.jumlah_tanggungan, w.pekerjaan_kk, w.penghasilan,
w.sumber_air, w.status_listrik, w.material_dinding, w.status_kepemilikan, w.status_verifikasi,
ST_Y(w.geom) AS lat, ST_X(w.geom) AS lng,
(SELECT GROUP_CONCAT(jb.kode SEPARATOR '; ')
FROM bantuan_sosial bs JOIN jenis_bantuan jb ON jb.id=bs.jenis_bantuan_id
WHERE bs.warga_id=w.id AND bs.is_aktif=1) AS bantuan
FROM warga_miskin w LEFT JOIN wilayah wl ON wl.id=w.wilayah_id
ORDER BY w.id
")->fetchAll();
if ($format === 'csv') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="data_warga_' . date('Ymd') . '.csv"');
$out = fopen('php://output', 'w');
fputcsv($out, ['ID','NIK KK','Nama KK','Kelurahan','Alamat','RT','RW','Jiwa','Tanggungan','Pekerjaan','Penghasilan','Sumber Air','Listrik','Dinding','Kepemilikan','Verifikasi','Bantuan Aktif','Lat','Lng']);
foreach ($rows as $r) {
fputcsv($out, [
$r['id'], $r['nik_kk'], $r['nama_kk'], $r['kelurahan'], $r['alamat'], $r['rt'], $r['rw'],
$r['jumlah_jiwa'], $r['jumlah_tanggungan'], $r['pekerjaan_kk'], $r['penghasilan'],
$r['sumber_air'], $r['status_listrik'], $r['material_dinding'], $r['status_kepemilikan'],
$r['status_verifikasi'], $r['bantuan'], $r['lat'], $r['lng'],
]);
}
fclose($out);
exit;
}
// format=print → HTML untuk dicetak/disimpan PDF
?>
<!DOCTYPE html>
<html lang="id"><head>
<meta charset="UTF-8">
<title>Laporan Data Warga Miskin</title>
<style>
body{font-family:Arial,sans-serif;margin:24px;color:#111;}
h2{margin:0 0 2px;} .sub{color:#666;font-size:0.85rem;margin-bottom:14px;}
table{width:100%;border-collapse:collapse;font-size:0.72rem;}
th,td{border:1px solid #ccc;padding:4px 6px;text-align:left;}
th{background:#f1f5f9;}
.toolbar{margin-bottom:12px;}
button{padding:8px 14px;background:#2563EB;color:#fff;border:none;border-radius:6px;cursor:pointer;}
@media print{.toolbar{display:none;}}
</style></head>
<body>
<div class="toolbar"><button onclick="window.print()">🖨️ Cetak / Simpan PDF</button></div>
<h2>Laporan Data Rumah Tangga Miskin</h2>
<div class="sub">WebGIS Pemetaan Kemiskinan — dicetak <?= date('d M Y H:i') ?> — total <?= count($rows) ?> rumah tangga</div>
<table>
<thead><tr><th>#</th><th>NIK</th><th>Nama KK</th><th>Kelurahan</th><th>Jiwa</th><th>Penghasilan</th><th>Sumber Air</th><th>Dinding</th><th>Bantuan</th><th>Verifikasi</th></tr></thead>
<tbody>
<?php foreach ($rows as $i => $r): ?>
<tr>
<td><?= $i+1 ?></td>
<td><?= htmlspecialchars($r['nik_kk'] ?: '-') ?></td>
<td><?= htmlspecialchars($r['nama_kk']) ?></td>
<td><?= htmlspecialchars($r['kelurahan'] ?: '-') ?></td>
<td><?= $r['jumlah_jiwa'] ?: '-' ?></td>
<td>Rp <?= number_format($r['penghasilan'],0,',','.') ?></td>
<td><?= htmlspecialchars($r['sumber_air'] ?: '-') ?></td>
<td><?= htmlspecialchars($r['material_dinding'] ?: '-') ?></td>
<td><?= htmlspecialchars($r['bantuan'] ?: '-') ?></td>
<td><?= htmlspecialchars($r['status_verifikasi']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</body></html>
+83
View File
@@ -0,0 +1,83 @@
<?php
/**
* api/warga_import.php — Import data warga dari CSV (KF-15).
* Template kolom (Lampiran A):
* nik_kk, nama_kk, kode_kelurahan, latitude, longitude,
* jumlah_jiwa, sumber_air, status_listrik, material_dinding, pekerjaan_kk
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/../config/activity.php';
$pdo = Database::getConnection();
requireApiAnyRole(['operator']);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') sendError('Method not allowed', 405);
if (empty($_FILES['file']['tmp_name'])) sendError('File CSV wajib diunggah');
$fh = fopen($_FILES['file']['tmp_name'], 'r');
if (!$fh) sendError('Gagal membaca file');
// Peta kode kelurahan -> id
$wmap = [];
foreach ($pdo->query("SELECT id, kode_wilayah FROM wilayah") as $w) {
if ($w['kode_wilayah']) $wmap[trim($w['kode_wilayah'])] = $w['id'];
}
$header = fgetcsv($fh);
if (!$header) sendError('CSV kosong');
$header = array_map(fn($h) => strtolower(trim($h)), $header);
$idx = array_flip($header);
$need = ['nama_kk', 'latitude', 'longitude'];
foreach ($need as $col) {
if (!isset($idx[$col])) sendError("Kolom wajib '$col' tidak ditemukan di CSV");
}
$enumAir = ['SUMUR_BOR','PDAM','SUNGAI','HUJAN','TIDAK_ADA'];
$enumListrik = ['PLN','NON_PLN','TIDAK_ADA'];
$enumDinding = ['PERMANEN','SEMI_PERMANEN','TIDAK_PERMANEN'];
$val = fn($row, $key) => isset($idx[$key]) && isset($row[$idx[$key]]) ? trim($row[$idx[$key]]) : null;
$enumOrNull = fn($v, $allowed) => ($v && in_array(strtoupper($v), $allowed, true)) ? strtoupper($v) : null;
$ins = $pdo->prepare("
INSERT INTO warga_miskin
(nama_kk, nik_kk, wilayah_id, jumlah_jiwa, pekerjaan_kk, penghasilan, jumlah_tanggungan,
sumber_air, status_listrik, material_dinding, status_verifikasi, created_by, geom)
VALUES (?,?,?,?,?,?,?,?,?,?, 'belum', ?, ST_GeomFromText(?))
");
$ok = 0; $fail = 0; $errors = []; $line = 1;
while (($row = fgetcsv($fh)) !== false) {
$line++;
if (count(array_filter($row, fn($x) => $x !== '' && $x !== null)) === 0) continue; // baris kosong
$nama = $val($row, 'nama_kk');
$lat = $val($row, 'latitude');
$lng = $val($row, 'longitude');
if (!$nama) { $fail++; $errors[] = "Baris $line: nama_kk kosong"; continue; }
if (!is_numeric($lat) || !is_numeric($lng)) { $fail++; $errors[] = "Baris $line: koordinat tidak valid"; continue; }
$kode = $val($row, 'kode_kelurahan');
$wid = ($kode && isset($wmap[$kode])) ? $wmap[$kode] : null;
try {
$ins->execute([
$nama, $val($row, 'nik_kk'), $wid,
($val($row, 'jumlah_jiwa') !== null && $val($row, 'jumlah_jiwa') !== '') ? (int)$val($row, 'jumlah_jiwa') : null,
$val($row, 'pekerjaan_kk'),
(float)($val($row, 'penghasilan') ?? 0),
(int)($val($row, 'jumlah_tanggungan') ?? 0),
$enumOrNull($val($row, 'sumber_air'), $enumAir),
$enumOrNull($val($row, 'status_listrik'), $enumListrik),
$enumOrNull($val($row, 'material_dinding'), $enumDinding),
$_SESSION['user_id'] ?? null,
sprintf('POINT(%F %F)', (float)$lng, (float)$lat),
]);
$ok++;
} catch (Exception $e) {
$fail++; $errors[] = "Baris $line: " . $e->getMessage();
}
}
fclose($fh);
logActivity($pdo, 'create', 'warga_miskin', null, "Import CSV: $ok berhasil, $fail gagal");
sendSuccess(['berhasil' => $ok, 'gagal' => $fail, 'errors' => array_slice($errors, 0, 15)], 'Import selesai');
+160
View File
@@ -0,0 +1,160 @@
<?php
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/../config/activity.php';
$pdo = Database::getConnection();
$method = $_SERVER['REQUEST_METHOD'];
// Pastikan sesi aktif sebelum membaca role/uid
if (session_status() === PHP_SESSION_NONE) startAppSession();
$role = $_SESSION['role'] ?? '';
$uid = $_SESSION['user_id'] ?? null;
// Mutasi: operator (penuh) atau enumerator (buat & edit data sendiri)
if ($method !== 'GET') requireApiAnyRole(['operator', 'enumerator']);
/** Ubah string kosong jadi NULL (untuk kolom ENUM/opsional). */
function nn($v) { return ($v === '' || $v === null) ? null : $v; }
/** Wilayah_id penugasan enumerator (cocokkan nama wilayah_tugas). */
function enumeratorWilayahId(PDO $pdo): ?int {
$tugas = $_SESSION['wilayah_tugas'] ?? '';
if (!$tugas) return null;
$st = $pdo->prepare("SELECT id FROM wilayah WHERE nama = ? LIMIT 1");
$st->execute([$tugas]);
$id = $st->fetchColumn();
return $id ? (int)$id : null;
}
/** Pastikan record milik enumerator yang sedang login. */
function assertOwn(PDO $pdo, $id, string $role, $uid): void {
if ($role !== 'enumerator') return; // operator bebas
$st = $pdo->prepare("SELECT created_by FROM warga_miskin WHERE id=?");
$st->execute([$id]);
$owner = $st->fetchColumn();
if ((int)$owner !== (int)$uid) sendError('Anda hanya dapat mengubah data yang Anda input', 403);
}
switch ($method) {
case 'GET':
// Enumerator hanya melihat data yang ia input (KF-26)
$where = ($role === 'enumerator') ? "WHERE w.created_by = " . (int)$uid : "";
$stmt = $pdo->query("
SELECT w.id, w.nama_kk, w.nik_kk, w.alamat, w.wilayah_id, w.rt, w.rw,
w.jumlah_jiwa, w.pekerjaan_kk, w.penghasilan, w.jumlah_tanggungan,
w.sumber_air, w.status_listrik, w.material_dinding, w.status_kepemilikan,
w.status_verifikasi, w.created_at,
ST_AsGeoJSON(w.geom) AS geojson,
(SELECT GROUP_CONCAT(jb.kode SEPARATOR ',')
FROM bantuan_sosial bs JOIN jenis_bantuan jb ON jb.id = bs.jenis_bantuan_id
WHERE bs.warga_id = w.id AND bs.is_aktif = 1) AS bantuan_aktif,
(SELECT COUNT(*) FROM foto_rumah f WHERE f.warga_id = w.id) AS jumlah_foto
FROM warga_miskin w
$where
ORDER BY w.id DESC
");
$features = [];
while ($r = $stmt->fetch()) {
$features[] = [
'type' => 'Feature',
'geometry' => json_decode($r['geojson']),
'properties' => [
'id' => (int)$r['id'], 'nama_kk' => $r['nama_kk'], 'nik_kk' => $r['nik_kk'],
'alamat' => $r['alamat'], 'wilayah_id' => $r['wilayah_id'],
'rt' => $r['rt'], 'rw' => $r['rw'], 'jumlah_jiwa' => $r['jumlah_jiwa'],
'pekerjaan_kk' => $r['pekerjaan_kk'], 'penghasilan' => $r['penghasilan'],
'jumlah_tanggungan' => $r['jumlah_tanggungan'], 'sumber_air' => $r['sumber_air'],
'status_listrik' => $r['status_listrik'], 'material_dinding' => $r['material_dinding'],
'status_kepemilikan' => $r['status_kepemilikan'], 'status_verifikasi' => $r['status_verifikasi'],
'bantuan_aktif' => $r['bantuan_aktif'], 'jumlah_foto' => (int)$r['jumlah_foto'],
'created_at' => $r['created_at'],
],
];
}
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Warga Miskin');
break;
case 'POST':
$d = getInput();
if (empty($d['nama_kk']) || empty($d['geometry'])) sendError('Nama KK & lokasi wajib diisi');
// Enumerator: wilayah dipaksa ke wilayah penugasannya
$wilayahId = ($role === 'enumerator') ? enumeratorWilayahId($pdo) : nn($d['wilayah_id'] ?? null);
// Enumerator: data baru selalu berstatus "belum" untuk diverifikasi operator
$verif = ($role === 'enumerator') ? 'belum' : ($d['status_verifikasi'] ?? 'belum');
$pdo->prepare("
INSERT INTO warga_miskin
(nama_kk, nik_kk, alamat, wilayah_id, rt, rw, jumlah_jiwa, pekerjaan_kk,
penghasilan, jumlah_tanggungan, sumber_air, status_listrik, material_dinding,
status_kepemilikan, status_verifikasi, created_by, geom)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, ST_GeomFromGeoJSON(?))
")->execute([
$d['nama_kk'], nn($d['nik_kk'] ?? null), nn($d['alamat'] ?? null), $wilayahId,
nn($d['rt'] ?? null), nn($d['rw'] ?? null), nn($d['jumlah_jiwa'] ?? null), nn($d['pekerjaan_kk'] ?? null),
$d['penghasilan'] ?? 0, $d['jumlah_tanggungan'] ?? 0,
nn($d['sumber_air'] ?? null), nn($d['status_listrik'] ?? null), nn($d['material_dinding'] ?? null),
nn($d['status_kepemilikan'] ?? null), $verif, $uid, json_encode($d['geometry']),
]);
$newId = (int)$pdo->lastInsertId();
logActivity($pdo, 'create', 'warga_miskin', $newId, "Tambah warga {$d['nama_kk']}");
sendSuccess(['id' => $newId], 'Warga disimpan', 201);
break;
case 'PUT':
$d = getInput();
$id = $_GET['id'] ?? null;
if (!$id) sendError('ID wajib');
// Mode verifikasi → hanya operator
if (isset($d['status_verifikasi']) && count($d) === 1) {
if ($role !== 'operator') sendError('Hanya operator yang dapat memverifikasi', 403);
$pdo->prepare("UPDATE warga_miskin SET status_verifikasi=? WHERE id=?")->execute([$d['status_verifikasi'], $id]);
logActivity($pdo, 'update', 'warga_miskin', (int)$id, "Verifikasi: {$d['status_verifikasi']}");
sendSuccess(null, 'Status verifikasi diperbarui');
}
assertOwn($pdo, $id, $role, $uid);
// Mode lokasi saja
if (!empty($d['geometry']) && empty($d['nama_kk'])) {
$pdo->prepare("UPDATE warga_miskin SET geom=ST_GeomFromGeoJSON(?) WHERE id=?")->execute([json_encode($d['geometry']), $id]);
logActivity($pdo, 'update', 'warga_miskin', (int)$id, 'Ubah lokasi warga');
sendSuccess(null, 'Lokasi diperbarui');
}
// Mode data lengkap. Enumerator tidak boleh mengubah status verifikasi.
$verifSet = ($role === 'operator') ? ($d['status_verifikasi'] ?? 'belum') : null;
if ($verifSet !== null) {
$sql = "UPDATE warga_miskin SET nama_kk=?, nik_kk=?, alamat=?, rt=?, rw=?, jumlah_jiwa=?, pekerjaan_kk=?,
penghasilan=?, jumlah_tanggungan=?, sumber_air=?, status_listrik=?, material_dinding=?,
status_kepemilikan=?, status_verifikasi=? WHERE id=?";
$params = [$d['nama_kk'], nn($d['nik_kk'] ?? null), nn($d['alamat'] ?? null), nn($d['rt'] ?? null), nn($d['rw'] ?? null),
nn($d['jumlah_jiwa'] ?? null), nn($d['pekerjaan_kk'] ?? null), $d['penghasilan'] ?? 0, $d['jumlah_tanggungan'] ?? 0,
nn($d['sumber_air'] ?? null), nn($d['status_listrik'] ?? null), nn($d['material_dinding'] ?? null),
nn($d['status_kepemilikan'] ?? null), $verifSet, $id];
} else {
$sql = "UPDATE warga_miskin SET nama_kk=?, nik_kk=?, alamat=?, rt=?, rw=?, jumlah_jiwa=?, pekerjaan_kk=?,
penghasilan=?, jumlah_tanggungan=?, sumber_air=?, status_listrik=?, material_dinding=?,
status_kepemilikan=? WHERE id=?";
$params = [$d['nama_kk'], nn($d['nik_kk'] ?? null), nn($d['alamat'] ?? null), nn($d['rt'] ?? null), nn($d['rw'] ?? null),
nn($d['jumlah_jiwa'] ?? null), nn($d['pekerjaan_kk'] ?? null), $d['penghasilan'] ?? 0, $d['jumlah_tanggungan'] ?? 0,
nn($d['sumber_air'] ?? null), nn($d['status_listrik'] ?? null), nn($d['material_dinding'] ?? null),
nn($d['status_kepemilikan'] ?? null), $id];
}
$pdo->prepare($sql)->execute($params);
logActivity($pdo, 'update', 'warga_miskin', (int)$id, 'Edit data warga');
sendSuccess(null, 'Warga diperbarui');
break;
case 'DELETE':
if ($role !== 'operator') sendError('Hanya operator yang dapat menghapus data', 403);
$id = $_GET['id'] ?? null;
if (!$id) sendError('ID wajib');
$pdo->prepare("DELETE FROM warga_miskin WHERE id=?")->execute([$id]);
logActivity($pdo, 'delete', 'warga_miskin', (int)$id, 'Hapus data warga');
sendSuccess(null, 'Warga dihapus');
break;
default:
sendError('Method not allowed', 405);
}
+89
View File
@@ -0,0 +1,89 @@
<?php
/**
* api/wilayah.php — Wilayah (kelurahan) + statistik kemiskinan (KF-06) & CRUD.
*
* GET → FeatureCollection wilayah + persentase_miskin (publik).
* POST → tambah wilayah (operator). {nama, kode_wilayah, jenis, jumlah_penduduk, geometry}
* PUT ?id= → ubah atribut &/atau geometry (operator).
* DELETE ?id= → hapus (operator).
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/../config/activity.php';
$pdo = Database::getConnection();
$method = $_SERVER['REQUEST_METHOD'];
if ($method !== 'GET') requireApiAnyRole(['operator']);
function nnw($v) { return ($v === '' || $v === null) ? null : $v; }
switch ($method) {
case 'GET':
$sql = "
SELECT w.id, w.nama, w.jenis, w.kode_wilayah, w.jumlah_penduduk,
COUNT(wm.id) AS jumlah_miskin, ST_AsGeoJSON(w.geom) AS geojson
FROM wilayah w
LEFT JOIN warga_miskin wm ON ST_Contains(w.geom, wm.geom)
GROUP BY w.id ORDER BY w.nama
";
$stmt = $pdo->query($sql);
$features = [];
while ($r = $stmt->fetch()) {
$penduduk = (int)$r['jumlah_penduduk'];
$miskin = (int)$r['jumlah_miskin'];
$persen = $penduduk > 0 ? round($miskin / $penduduk * 100, 2) : 0;
$features[] = [
'type' => 'Feature',
'geometry' => json_decode($r['geojson']),
'properties' => [
'id' => (int)$r['id'], 'nama' => $r['nama'], 'jenis' => $r['jenis'],
'kode_wilayah' => $r['kode_wilayah'], 'jumlah_penduduk' => $penduduk,
'jumlah_miskin' => $miskin, 'persentase_miskin' => $persen,
],
];
}
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Wilayah');
break;
case 'POST':
$d = getInput();
if (empty($d['nama']) || empty($d['geometry'])) sendError('Nama & geometri wajib diisi');
$pdo->prepare("INSERT INTO wilayah (nama, kode_wilayah, jenis, jumlah_penduduk, geom)
VALUES (?,?,?,?, ST_GeomFromGeoJSON(?))")
->execute([
$d['nama'], nnw($d['kode_wilayah'] ?? null), $d['jenis'] ?? 'kelurahan',
(int)($d['jumlah_penduduk'] ?? 0), json_encode($d['geometry']),
]);
$id = (int)$pdo->lastInsertId();
logActivity($pdo, 'create', 'wilayah', $id, "Tambah wilayah {$d['nama']}");
sendSuccess(['id' => $id], 'Wilayah disimpan', 201);
break;
case 'PUT':
$d = getInput();
$id = $_GET['id'] ?? null;
if (!$id) sendError('ID wajib');
if (!empty($d['geometry'])) {
$pdo->prepare("UPDATE wilayah SET nama=?, kode_wilayah=?, jenis=?, jumlah_penduduk=?, geom=ST_GeomFromGeoJSON(?) WHERE id=?")
->execute([$d['nama'], nnw($d['kode_wilayah'] ?? null), $d['jenis'] ?? 'kelurahan',
(int)($d['jumlah_penduduk'] ?? 0), json_encode($d['geometry']), $id]);
} else {
$pdo->prepare("UPDATE wilayah SET nama=?, kode_wilayah=?, jenis=?, jumlah_penduduk=? WHERE id=?")
->execute([$d['nama'], nnw($d['kode_wilayah'] ?? null), $d['jenis'] ?? 'kelurahan',
(int)($d['jumlah_penduduk'] ?? 0), $id]);
}
logActivity($pdo, 'update', 'wilayah', (int)$id, 'Ubah wilayah');
sendSuccess(null, 'Wilayah diperbarui');
break;
case 'DELETE':
$id = $_GET['id'] ?? null;
if (!$id) sendError('ID wajib');
$pdo->prepare("DELETE FROM wilayah WHERE id=?")->execute([$id]);
logActivity($pdo, 'delete', 'wilayah', (int)$id, 'Hapus wilayah');
sendSuccess(null, 'Wilayah dihapus');
break;
default:
sendError('Method not allowed', 405);
}
+54
View File
@@ -0,0 +1,54 @@
<?php
/**
* api/zona_prioritas.php — Skor prioritas intervensi per kelurahan (KF-20).
* Skor = persen_miskin*2 + gap_bantuan*0.2 + jumlah_miskin*1
* gap_bantuan = 100 - cakupan% (cakupan = warga dibantu / warga miskin)
* Level: skor>35 Tinggi, >20 Sedang, selain itu Rendah.
* GET → GeoJSON wilayah + properti skor/level/komponen.
*/
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/helpers.php';
$pdo = Database::getConnection();
requireApiAnyRole(['operator', 'pimpinan']);
if ($_SERVER['REQUEST_METHOD'] !== 'GET') sendError('Method not allowed', 405);
$sql = "
SELECT w.id, w.nama, w.jumlah_penduduk, ST_AsGeoJSON(w.geom) AS geojson,
COUNT(DISTINCT wm.id) AS miskin,
COUNT(DISTINCT CASE WHEN bs.id IS NOT NULL THEN wm.id END) AS dibantu
FROM wilayah w
LEFT JOIN warga_miskin wm ON ST_Contains(w.geom, wm.geom)
LEFT JOIN bantuan_sosial bs ON bs.warga_id = wm.id AND bs.is_aktif = 1
GROUP BY w.id
ORDER BY w.nama
";
$stmt = $pdo->query($sql);
$features = [];
while ($r = $stmt->fetch()) {
$penduduk = (int)$r['jumlah_penduduk'];
$miskin = (int)$r['miskin'];
$dibantu = (int)$r['dibantu'];
$persen = $penduduk > 0 ? round($miskin / $penduduk * 100, 2) : 0;
$cakupan = $miskin > 0 ? round($dibantu / $miskin * 100, 1) : 0;
$gap = $miskin > 0 ? (100 - $cakupan) : 0;
$skor = round($persen * 2 + $gap * 0.2 + $miskin * 1, 1);
$level = $skor > 35 ? 'Tinggi' : ($skor > 20 ? 'Sedang' : 'Rendah');
$features[] = [
'type' => 'Feature',
'geometry' => json_decode($r['geojson']),
'properties' => [
'id' => (int)$r['id'], 'nama' => $r['nama'],
'jumlah_penduduk' => $penduduk, 'jumlah_miskin' => $miskin,
'dibantu' => $dibantu, 'persentase_miskin' => $persen,
'cakupan_bantuan' => $cakupan, 'skor' => $skor, 'level' => $level,
],
];
}
// urутkan ranking by skor desc
usort($features, fn($a, $b) => $b['properties']['skor'] <=> $a['properties']['skor']);
foreach ($features as $i => &$f) $f['properties']['rank'] = $i + 1;
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Zona Prioritas');