add files and folders

This commit is contained in:
2026-06-10 17:28:32 +07:00
parent 52f4cf86ab
commit 61f4fb2e71
80 changed files with 17551 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
<?php
// ============================================================
// API: Autentikasi
// POST ?action=login
// POST ?action=logout
// GET ?action=me
// ============================================================
require_once __DIR__ . '/../middleware/auth.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit; }
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
if ($method === 'POST' && $action === 'login') {
$body = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$username = sanitizeStr($body['username'] ?? '');
$password = $body['password'] ?? '';
if (!$username || !$password) {
jsonResponse(false, 'Username dan password wajib diisi.', [], 400);
}
$result = doLogin($pdo, $username, $password);
if (!$result['success']) {
jsonResponse(false, $result['message'], [], 401);
}
jsonResponse(true, 'Login berhasil.', [
'role' => $result['role'],
'user_id' => $_SESSION['user_id'],
'nama' => $_SESSION['user_nama'],
]);
}
if ($method === 'POST' && $action === 'logout') {
doLogout($pdo);
jsonResponse(true, 'Logout berhasil.');
}
if ($method === 'GET' && $action === 'me') {
$user = getApiUser($pdo);
if (!$user) jsonResponse(false, 'Belum login.', [], 401);
$detail = $pdo->prepare("SELECT u.id, u.nama, u.username, u.email, r.nama AS role, u.rumah_ibadah_id, ri.nama AS nama_ri FROM users u JOIN roles r ON r.id=u.role_id LEFT JOIN rumah_ibadah ri ON ri.id=u.rumah_ibadah_id WHERE u.id=:id");
$detail->execute([':id' => $user['id']]);
jsonResponse(true, 'OK', ['data' => $detail->fetch()]);
}
jsonResponse(false, 'Action tidak dikenal.', [], 400);
+239
View File
@@ -0,0 +1,239 @@
<?php
// ============================================================
// API: Bantuan & Histori Bantuan
// GET ?action=jenis|histori&nik=...
// POST action=salurkan
// ============================================================
require_once __DIR__ . '/../middleware/auth.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'];
$action = $_GET['action'] ?? '';
$currentUser = getApiUser($pdo);
// ============================================================
// GET
// ============================================================
if ($method === 'GET') {
if ($action === 'jenis') {
$rows = $pdo->query("SELECT * FROM jenis_bantuan WHERE aktif=1 ORDER BY kategori, nama")->fetchAll();
jsonResponse(true, 'OK', ['data' => $rows]);
}
if ($action === 'histori') {
$nik = sanitizeStr($_GET['nik'] ?? '');
if ($nik && validateNIK($nik)) {
// Histori per penduduk
$stmt = $pdo->prepare("
SELECT hb.*, jb.nama AS nama_bantuan, jb.kategori, jb.satuan,
ri.nama AS nama_rumah_ibadah, u.nama AS disalurkan_oleh_nama
FROM histori_bantuan hb
JOIN jenis_bantuan jb ON jb.id = hb.id_jenis_bantuan
LEFT JOIN rumah_ibadah ri ON ri.id = hb.rumah_ibadah_id
LEFT JOIN users u ON u.id = hb.disalurkan_oleh
WHERE hb.nik = :nik
ORDER BY hb.tanggal DESC
");
$stmt->execute([':nik' => $nik]);
$data = $stmt->fetchAll();
// Ringkasan
$summary = [
'total_bantuan' => count($data),
'kategori' => [],
];
foreach ($data as $row) {
$summary['kategori'][$row['kategori']] = ($summary['kategori'][$row['kategori']] ?? 0) + 1;
}
jsonResponse(true, 'OK', ['data' => $data, 'summary' => $summary]);
}
// List semua histori (admin/pengurus)
if (!canViewAll($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$where = [];
$params = [];
if (!empty($_GET['bulan'])) { $where[] = 'MONTH(hb.tanggal) = :bln'; $params[':bln'] = (int)$_GET['bulan']; }
if (!empty($_GET['tahun'])) { $where[] = 'YEAR(hb.tanggal) = :thn'; $params[':thn'] = (int)$_GET['tahun']; }
if (!empty($_GET['kategori'])) { $where[] = 'jb.kategori = :kat'; $params[':kat'] = $_GET['kategori']; }
$whereStr = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$stmt = $pdo->prepare("
SELECT hb.id, hb.nik, p.nama_lengkap, hb.tanggal,
jb.nama AS nama_bantuan, jb.kategori,
hb.jumlah, hb.nilai_rupiah, hb.status_penyaluran,
ri.nama AS nama_ri
FROM histori_bantuan hb
JOIN penduduk p ON p.nik = hb.nik
JOIN jenis_bantuan jb ON jb.id = hb.id_jenis_bantuan
LEFT JOIN rumah_ibadah ri ON ri.id = hb.rumah_ibadah_id
$whereStr
ORDER BY hb.tanggal DESC
LIMIT 1000
");
$stmt->execute($params);
jsonResponse(true, 'OK', ['data' => $stmt->fetchAll()]);
}
if ($action === 'belum_dibantu') {
// Warga miskin yang BELUM pernah menerima bantuan
$rows = $pdo->query("
SELECT p.nik, p.nama_lengkap, p.status_ekonomi, p.kondisi_kesehatan,
p.pekerjaan, p.alamat, p.latitude, p.longitude,
k.nama_kk, k.no_kk, ri.nama AS nama_ri
FROM penduduk p
LEFT JOIN keluarga k ON k.id = p.id_keluarga
LEFT JOIN rumah_ibadah ri ON ri.id = k.rumah_ibadah_id
WHERE p.status_hidup = 'hidup'
AND p.status_ekonomi IN ('miskin', 'rentan')
AND NOT EXISTS (
SELECT 1 FROM histori_bantuan hb
WHERE hb.nik = p.nik AND hb.status_penyaluran = 'disalurkan'
)
ORDER BY
CASE p.status_ekonomi WHEN 'miskin' THEN 1 ELSE 2 END,
CASE p.kondisi_kesehatan WHEN 'sakit_parah' THEN 1 WHEN 'disabilitas' THEN 2 ELSE 3 END
")->fetchAll();
jsonResponse(true, 'OK', ['data' => $rows, 'total' => count($rows)]);
}
if ($action === 'dashboard_stats') {
// Statistik untuk dashboard
$stats = $pdo->query("
SELECT
(SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='miskin' AND status_hidup='hidup') AS total_miskin,
(SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='rentan' AND status_hidup='hidup') AS total_rentan,
(SELECT COUNT(*) FROM penduduk WHERE status_ekonomi='mampu' AND status_hidup='hidup') AS total_mampu,
(SELECT COUNT(*) FROM keluarga) AS total_keluarga,
(SELECT COUNT(*) FROM rumah_ibadah) AS total_rumah_ibadah,
(SELECT COUNT(*) FROM laporan WHERE status='pending') AS laporan_pending,
(SELECT COUNT(*) FROM laporan WHERE status='diproses') AS laporan_diproses,
(SELECT COUNT(*) FROM histori_bantuan WHERE MONTH(tanggal)=MONTH(CURDATE()) AND status_penyaluran='disalurkan') AS bantuan_bulan_ini,
(SELECT COUNT(*) FROM penduduk WHERE status_hidup='hidup' AND status_ekonomi IN ('miskin','rentan')
AND NOT EXISTS (SELECT 1 FROM histori_bantuan hb WHERE hb.nik=penduduk.nik AND hb.status_penyaluran='disalurkan')
) AS belum_pernah_dibantu
")->fetch();
// Bantuan per kategori bulan ini
$per_kategori = $pdo->query("
SELECT jb.kategori, COUNT(*) AS jumlah
FROM histori_bantuan hb
JOIN jenis_bantuan jb ON jb.id = hb.id_jenis_bantuan
WHERE MONTH(hb.tanggal) = MONTH(CURDATE())
AND hb.status_penyaluran = 'disalurkan'
GROUP BY jb.kategori
")->fetchAll();
jsonResponse(true, 'OK', ['data' => $stats, 'per_kategori' => $per_kategori]);
}
jsonResponse(false, 'Action tidak dikenal.', [], 400);
}
// ============================================================
// POST: salurkan bantuan
// ============================================================
if ($method === 'POST' && $action === 'salurkan') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$body = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$nik = sanitizeStr($body['nik'] ?? '');
$idJenis = (int)($body['id_jenis_bantuan'] ?? 0);
$tanggal = sanitizeStr($body['tanggal'] ?? date('Y-m-d'));
$jumlah = sanitizeStr($body['jumlah'] ?? '');
$status = sanitizeStr($body['status_penyaluran'] ?? 'dijadwalkan');
if (!validateNIK($nik)) jsonResponse(false, 'NIK tidak valid.', [], 400);
if ($idJenis <= 0) jsonResponse(false, 'Jenis bantuan harus dipilih.', [], 400);
if (!strtotime($tanggal)) jsonResponse(false, 'Format tanggal tidak valid.', [], 400);
// Cek penduduk ada
$cek = $pdo->prepare("SELECT nik, id_keluarga FROM penduduk WHERE nik = :nik AND status_hidup='hidup'");
$cek->execute([':nik' => $nik]);
$pddk = $cek->fetch();
if (!$pddk) jsonResponse(false, 'Penduduk tidak ditemukan atau sudah meninggal.', [], 404);
// Cek jenis bantuan ada
$cekJb = $pdo->prepare("SELECT id FROM jenis_bantuan WHERE id = :id AND aktif=1");
$cekJb->execute([':id' => $idJenis]);
if (!$cekJb->fetch()) jsonResponse(false, 'Jenis bantuan tidak ditemukan.', [], 404);
$buktiNama = null;
if (!empty($_FILES['bukti']['name'])) {
try { $buktiNama = uploadFile($_FILES['bukti'], UPLOAD_BUKTI); }
catch (RuntimeException $e) { jsonResponse(false, $e->getMessage(), [], 400); }
}
$stmt = $pdo->prepare("
INSERT INTO histori_bantuan
(nik, id_jenis_bantuan, id_keluarga, rumah_ibadah_id, tanggal,
jumlah, nilai_rupiah, status_penyaluran, catatan, bukti_file, disalurkan_oleh)
VALUES
(:nik,:jb,:kk,:ri,:tgl,:jml,:rp,:status,:cat,:bukti,:user)
");
$stmt->execute([
':nik' => $nik,
':jb' => $idJenis,
':kk' => $pddk['id_keluarga'],
':ri' => $currentUser['rumah_ibadah_id'],
':tgl' => $tanggal,
':jml' => $jumlah ?: null,
':rp' => !empty($body['nilai_rupiah']) ? (float)$body['nilai_rupiah'] : null,
':status' => in_array($status, ['dijadwalkan','disalurkan','dibatalkan']) ? $status : 'dijadwalkan',
':cat' => sanitizeStr($body['catatan'] ?? '') ?: null,
':bukti' => $buktiNama,
':user' => $currentUser['id'],
]);
$hbId = (int)$pdo->lastInsertId();
logAktivitas($pdo, $currentUser['id'], 'CREATE', 'histori_bantuan', (string)$hbId, null, $body);
// Notifikasi ke pimpinan
$pimpinan = $pdo->query("SELECT id FROM users WHERE role_id=3")->fetchAll();
foreach ($pimpinan as $pm) {
$pdo->prepare("INSERT INTO notifikasi (user_id, judul, isi, tipe, ref_id) VALUES (:u,:j,:i,'bantuan',:r)")
->execute([':u' => $pm['id'], ':j' => 'Bantuan Disalurkan', ':i' => "Bantuan untuk NIK $nik telah dicatat.", ':r' => $hbId]);
}
jsonResponse(true, 'Bantuan berhasil dicatat.', ['id' => $hbId], 201);
}
// ============================================================
// PUT: update status penyaluran
// ============================================================
if ($method === 'PUT') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$id = (int)($_GET['id'] ?? 0);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$status = sanitizeStr($body['status_penyaluran'] ?? '');
if (!in_array($status, ['dijadwalkan','disalurkan','dibatalkan'])) {
jsonResponse(false, 'Status tidak valid.', [], 400);
}
$old = $pdo->prepare("SELECT * FROM histori_bantuan WHERE id = :id");
$old->execute([':id' => $id]);
if (!$old->fetch()) jsonResponse(false, 'Data tidak ditemukan.', [], 404);
$pdo->prepare("UPDATE histori_bantuan SET status_penyaluran=:s WHERE id=:id")
->execute([':s' => $status, ':id' => $id]);
logAktivitas($pdo, $currentUser['id'], 'UPDATE', 'histori_bantuan', (string)$id);
jsonResponse(true, 'Status penyaluran diperbarui.', ['id' => $id]);
}
jsonResponse(false, 'Method tidak didukung.', [], 405);
+183
View File
@@ -0,0 +1,183 @@
<?php
// ============================================================
// API: Keluarga
// ============================================================
require_once __DIR__ . '/../middleware/auth.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'];
$action = $_GET['action'] ?? '';
$currentUser = getApiUser($pdo);
if ($method === 'GET') {
if ($action === 'list') {
$where = [];
$params = [];
if (!empty($_GET['status_ekonomi'])) {
$where[] = 'k.status_ekonomi = :se';
$params[':se'] = $_GET['status_ekonomi'];
}
$whereStr = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$rows = $pdo->prepare("
SELECT k.*,
ri.nama AS nama_ri, ri.jenis AS jenis_ri,
COUNT(p.nik) AS jumlah_anggota_db,
SUM(CASE WHEN p.kondisi_kesehatan IN ('sakit_parah','disabilitas') THEN 1 ELSE 0 END) AS anggota_sakit
FROM keluarga k
LEFT JOIN rumah_ibadah ri ON ri.id = k.rumah_ibadah_id
LEFT JOIN penduduk p ON p.id_keluarga = k.id AND p.status_hidup='hidup'
$whereStr
GROUP BY k.id
ORDER BY CASE k.status_ekonomi WHEN 'miskin' THEN 1 WHEN 'rentan' THEN 2 ELSE 3 END ASC
");
$rows->execute($params);
jsonResponse(true, 'OK', ['data' => $rows->fetchAll()]);
}
if ($action === 'detail') {
$id = (int)($_GET['id'] ?? 0);
$stmt = $pdo->prepare("SELECT k.*, ri.nama AS nama_ri FROM keluarga k LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id WHERE k.id=:id");
$stmt->execute([':id' => $id]);
$kk = $stmt->fetch();
if (!$kk) jsonResponse(false, 'Keluarga tidak ditemukan.', [], 404);
$anggota = $pdo->prepare("SELECT * FROM penduduk WHERE id_keluarga=:id ORDER BY CASE status_keluarga WHEN 'kepala_keluarga' THEN 1 ELSE 2 END, tanggal_lahir");
$anggota->execute([':id' => $id]);
$histori = $pdo->prepare("
SELECT hb.tanggal, jb.nama AS bantuan, hb.jumlah, hb.status_penyaluran, p.nama_lengkap
FROM histori_bantuan hb
JOIN penduduk p ON p.nik=hb.nik
JOIN jenis_bantuan jb ON jb.id=hb.id_jenis_bantuan
WHERE hb.id_keluarga=:id
ORDER BY hb.tanggal DESC
LIMIT 50
");
$histori->execute([':id' => $id]);
jsonResponse(true, 'OK', ['data' => $kk, 'anggota' => $anggota->fetchAll(), 'histori' => $histori->fetchAll()]);
}
if ($action === 'geojson') {
$stmt = $pdo->query("
SELECT k.id, k.no_kk, k.nama_kk, k.status_ekonomi, k.latitude, k.longitude,
COUNT(p.nik) AS jml_anggota, ri.nama AS nama_ri
FROM keluarga k
LEFT JOIN penduduk p ON p.id_keluarga=k.id AND p.status_hidup='hidup'
LEFT JOIN rumah_ibadah ri ON ri.id=k.rumah_ibadah_id
WHERE k.latitude IS NOT NULL AND k.longitude IS NOT NULL
GROUP BY k.id
");
$rows = $stmt->fetchAll();
$features = [];
foreach ($rows as $r) {
$features[] = [
'type' => 'Feature',
'geometry' => ['type'=>'Point','coordinates'=>[(float)$r['longitude'],(float)$r['latitude']]],
'properties' => array_merge($r, [
'color' => match($r['status_ekonomi']){'miskin'=>'#ef4444','rentan'=>'#f59e0b',default=>'#22c55e'},
'layer' => 'keluarga'
])
];
}
header('Content-Type: application/json');
echo json_encode(['type'=>'FeatureCollection','features'=>$features], JSON_UNESCAPED_UNICODE);
exit;
}
jsonResponse(false, 'Action tidak dikenal.', [], 400);
}
if ($method === 'POST') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$body = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$noKk = sanitizeStr($body['no_kk'] ?? '');
$namaKk = sanitizeStr($body['nama_kk'] ?? '');
if (strlen($noKk) !== 16) jsonResponse(false, 'No KK harus 16 digit.', [], 400);
if (strlen($namaKk) < 3) jsonResponse(false, 'Nama KK terlalu pendek.', [], 400);
$cek = $pdo->prepare("SELECT id FROM keluarga WHERE no_kk=:nkk");
$cek->execute([':nkk' => $noKk]);
if ($cek->fetch()) jsonResponse(false, 'No KK sudah terdaftar.', [], 409);
$stmt = $pdo->prepare("
INSERT INTO keluarga (no_kk, nama_kk, alamat, kelurahan, kecamatan, kota, latitude, longitude, status_ekonomi, rumah_ibadah_id)
VALUES (:nkk,:nkk_nama,:almt,:kel,:kec,:kota,:lat,:lng,:se,:ri)
");
$stmt->execute([
':nkk' => $noKk,
':nkk_nama' => $namaKk,
':almt' => sanitizeStr($body['alamat'] ?? '') ?: null,
':kel' => sanitizeStr($body['kelurahan'] ?? '') ?: null,
':kec' => sanitizeStr($body['kecamatan'] ?? '') ?: null,
':kota' => sanitizeStr($body['kota'] ?? '') ?: null,
':lat' => !empty($body['latitude']) ? (float)$body['latitude'] : null,
':lng' => !empty($body['longitude']) ? (float)$body['longitude'] : null,
':se' => sanitizeStr($body['status_ekonomi'] ?? 'rentan'),
':ri' => !empty($body['rumah_ibadah_id']) ? (int)$body['rumah_ibadah_id'] : null,
]);
$newId = (int)$pdo->lastInsertId();
logAktivitas($pdo, $currentUser['id']??null, 'CREATE', 'keluarga', (string)$newId, null, $body);
jsonResponse(true, 'Keluarga berhasil ditambahkan.', ['id' => $newId], 201);
}
if ($method === 'PUT') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$id = (int)($_GET['id'] ?? 0);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
if ($id <= 0) jsonResponse(false, 'ID tidak valid.', [], 400);
if ($action === 'update_koordinat') {
$lat = (float)($body['latitude'] ?? 0);
$lng = (float)($body['longitude'] ?? 0);
if (!$lat || !$lng) jsonResponse(false, 'Koordinat tidak valid.', [], 400);
$pdo->prepare("UPDATE keluarga SET latitude=:lat, longitude=:lng WHERE id=:id")
->execute([':lat'=>$lat, ':lng'=>$lng, ':id'=>$id]);
logAktivitas($pdo, $currentUser['id']??null, 'UPDATE', 'keluarga', (string)$id, null, ['lat'=>$lat,'lng'=>$lng]);
jsonResponse(true, 'Koordinat diperbarui.', ['id' => $id]);
}
$pdo->prepare("
UPDATE keluarga SET
nama_kk=:nkk, alamat=:almt, kelurahan=:kel, kecamatan=:kec,
kota=:kota, latitude=:lat, longitude=:lng,
status_ekonomi=:se, rumah_ibadah_id=:ri
WHERE id=:id
")->execute([
':nkk' => sanitizeStr($body['nama_kk'] ?? ''),
':almt' => sanitizeStr($body['alamat'] ?? '') ?: null,
':kel' => sanitizeStr($body['kelurahan'] ?? '') ?: null,
':kec' => sanitizeStr($body['kecamatan'] ?? '') ?: null,
':kota' => sanitizeStr($body['kota'] ?? '') ?: null,
':lat' => !empty($body['latitude']) ? (float)$body['latitude'] : null,
':lng' => !empty($body['longitude']) ? (float)$body['longitude'] : null,
':se' => sanitizeStr($body['status_ekonomi'] ?? 'rentan'),
':ri' => !empty($body['rumah_ibadah_id']) ? (int)$body['rumah_ibadah_id'] : null,
':id' => $id,
]);
logAktivitas($pdo, $currentUser['id']??null, 'UPDATE', 'keluarga', (string)$id);
jsonResponse(true, 'Keluarga diperbarui.', ['id' => $id]);
}
if ($method === 'DELETE') {
if (!isAdmin($currentUser)) jsonResponse(false, 'Hanya admin yang bisa menghapus keluarga.', [], 403);
$id = (int)($_GET['id'] ?? 0);
// Cek apakah masih ada anggota
$jml = (int)$pdo->prepare("SELECT COUNT(*) FROM penduduk WHERE id_keluarga=:id")->execute([':id'=>$id]);
if ($jml > 0) jsonResponse(false, 'Tidak bisa dihapus: masih ada anggota keluarga.', [], 409);
$pdo->prepare("DELETE FROM keluarga WHERE id=:id")->execute([':id' => $id]);
logAktivitas($pdo, $currentUser['id']??null, 'DELETE', 'keluarga', (string)$id);
jsonResponse(true, 'Keluarga dihapus.');
}
jsonResponse(false, 'Method tidak didukung.', [], 405);
+255
View File
@@ -0,0 +1,255 @@
<?php
// ============================================================
// API: Laporan Masyarakat
// GET ?action=list|detail&id=...
// POST action=create (multipart untuk foto)
// PUT action=update_status&id=...
// ============================================================
require_once __DIR__ . '/../middleware/auth.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'];
$action = $_GET['action'] ?? '';
$currentUser = getApiUser($pdo);
// ============================================================
// GET: list laporan
// ============================================================
if ($method === 'GET') {
if ($action === 'list') {
$where = [];
$params = [];
if (!empty($_GET['status'])) {
$where[] = 'l.status = :status';
$params[':status'] = $_GET['status'];
}
if (!empty($_GET['urgensi'])) {
$where[] = 'l.tingkat_urgensi = :urgensi';
$params[':urgensi'] = $_GET['urgensi'];
}
$whereStr = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$rows = $pdo->prepare("
SELECT l.*,
p.nama_lengkap AS nama_terdampak,
u.nama AS nama_verifikator
FROM laporan l
LEFT JOIN penduduk p ON p.nik = l.nik_terdampak
LEFT JOIN users u ON u.id = l.diverifikasi_oleh
$whereStr
ORDER BY
CASE l.tingkat_urgensi WHEN 'darurat' THEN 1 WHEN 'tinggi' THEN 2 WHEN 'sedang' THEN 3 ELSE 4 END ASC,
CASE l.status WHEN 'pending' THEN 1 WHEN 'diverifikasi' THEN 2 ELSE 3 END ASC,
l.created_at DESC
LIMIT 500
");
$rows->execute($params);
jsonResponse(true, 'OK', ['data' => $rows->fetchAll(), 'total' => $rows->rowCount()]);
}
if ($action === 'detail') {
$id = (int)($_GET['id'] ?? 0);
$stmt = $pdo->prepare("
SELECT l.*, p.nama_lengkap AS nama_terdampak, p.status_ekonomi,
p.kondisi_kesehatan, p.alamat AS alamat_penduduk
FROM laporan l
LEFT JOIN penduduk p ON p.nik = l.nik_terdampak
WHERE l.id = :id
");
$stmt->execute([':id' => $id]);
$data = $stmt->fetch();
if (!$data) jsonResponse(false, 'Laporan tidak ditemukan.', [], 404);
jsonResponse(true, 'OK', ['data' => $data]);
}
if ($action === 'geojson') {
// Untuk layer marker laporan di peta
$stmt = $pdo->query("
SELECT id, nama_pelapor, pelapor_id, kategori, tingkat_urgensi, status,
latitude, longitude, alamat, created_at
FROM laporan
WHERE latitude IS NOT NULL AND longitude IS NOT NULL
AND status NOT IN ('selesai','ditolak')
");
$rows = $stmt->fetchAll();
$features = [];
foreach ($rows as $r) {
$color = match($r['tingkat_urgensi']) {
'darurat' => '#dc2626',
'tinggi' => '#ea580c',
'sedang' => '#ca8a04',
default => '#2563eb',
};
$features[] = [
'type' => 'Feature',
'geometry' => ['type' => 'Point', 'coordinates' => [(float)$r['longitude'], (float)$r['latitude']]],
'properties' => array_merge($r, ['color' => $color, 'layer' => 'laporan'])
];
}
header('Content-Type: application/json');
echo json_encode(['type' => 'FeatureCollection', 'features' => $features], JSON_UNESCAPED_UNICODE);
exit;
}
if ($action === 'stats') {
$stats = $pdo->query("
SELECT
SUM(status='pending') AS pending,
SUM(status='diverifikasi') AS diverifikasi,
SUM(status='diproses') AS diproses,
SUM(status='selesai') AS selesai,
SUM(tingkat_urgensi='darurat') AS darurat,
COUNT(*) AS total
FROM laporan
")->fetch();
jsonResponse(true, 'OK', ['data' => $stats]);
}
jsonResponse(false, 'Action tidak dikenal.', [], 400);
}
// ============================================================
// POST: buat laporan baru (bisa dilakukan masyarakat umum)
// ============================================================
if ($method === 'POST') {
$deskripsi = sanitizeStr($_POST['deskripsi'] ?? '', 2000);
$kategori = sanitizeStr($_POST['kategori'] ?? 'butuh_bantuan_lain');
$urgensi = sanitizeStr($_POST['tingkat_urgensi'] ?? 'sedang');
$lat = !empty($_POST['latitude']) ? (float)$_POST['latitude'] : null;
$lng = !empty($_POST['longitude']) ? (float)$_POST['longitude'] : null;
if (strlen($deskripsi) < 10) jsonResponse(false, 'Deskripsi laporan terlalu singkat.', [], 400);
$fotoNama = null;
if (!empty($_FILES['foto']['name'])) {
try {
$fotoNama = uploadFile($_FILES['foto'], UPLOAD_LAPORAN, ALLOWED_IMG_EXT);
} catch (RuntimeException $e) {
jsonResponse(false, $e->getMessage(), [], 400);
}
}
$stmt = $pdo->prepare("
INSERT INTO laporan
(nama_pelapor, pelapor_id, nik_terdampak, deskripsi, kategori, tingkat_urgensi,
latitude, longitude, alamat, foto, status)
VALUES
(:np, :pid, :nik, :dsk, :kat, :urg, :lat, :lng, :almt, :foto, 'pending')
");
$stmt->execute([
':np' => sanitizeStr($_POST['nama_pelapor'] ?? '') ?: null,
':pid' => $currentUser['id'] ?? null,
':nik' => sanitizeStr($_POST['nik_terdampak'] ?? '') ?: null,
':dsk' => $deskripsi,
':kat' => $kategori,
':urg' => $urgensi,
':lat' => $lat,
':lng' => $lng,
':almt' => sanitizeStr($_POST['alamat'] ?? '') ?: null,
':foto' => $fotoNama,
]);
$laporanId = (int)$pdo->lastInsertId();
// Kirim notifikasi ke pengurus jika darurat
if (in_array($urgensi, ['darurat', 'tinggi'])) {
$pengurus = $pdo->query("SELECT id FROM users WHERE role_id = 2")->fetchAll();
foreach ($pengurus as $p) {
$pdo->prepare("
INSERT INTO notifikasi (user_id, judul, isi, tipe, ref_id)
VALUES (:uid, :judul, :isi, 'laporan', :ref)
")->execute([
':uid' => $p['id'],
':judul' => '🚨 Laporan ' . strtoupper($urgensi) . ' masuk!',
':isi' => "Ada laporan baru kategori '$kategori'. Segera ditangani.",
':ref' => $laporanId,
]);
}
}
logAktivitas($pdo, $currentUser['id'] ?? null, 'LAPORAN', 'laporan', (string)$laporanId);
jsonResponse(true, 'Laporan berhasil dikirim. Terima kasih!', ['id' => $laporanId], 201);
}
// ============================================================
// PUT: Update status laporan (hanya pengurus/admin)
// ============================================================
if ($method === 'PUT') {
if (!canVerify($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) jsonResponse(false, 'ID tidak valid.', [], 400);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$status = sanitizeStr($body['status'] ?? '');
$valid = ['diverifikasi', 'diproses', 'selesai', 'ditolak'];
if (!in_array($status, $valid)) jsonResponse(false, 'Status tidak valid.', [], 400);
$old = $pdo->prepare("SELECT * FROM laporan WHERE id = :id");
$old->execute([':id' => $id]);
$dataLama = $old->fetch();
if (!$dataLama) jsonResponse(false, 'Laporan tidak ditemukan.', [], 404);
$sets = ['status = :status', 'updated_at = NOW()'];
$params = [':id' => $id, ':status' => $status];
if ($status === 'diverifikasi' && $currentUser) {
$sets[] = 'diverifikasi_oleh = :verif';
$params[':verif'] = $currentUser['id'];
}
if ($status === 'diproses' && $currentUser) {
$sets[] = 'ditangani_oleh = :tangani';
$params[':tangani'] = $currentUser['id'];
}
if ($status === 'selesai') {
$sets[] = 'tanggal_selesai = NOW()';
}
if (!empty($body['catatan_verifikasi'])) {
$sets[] = 'catatan_verifikasi = :catatan';
$params[':catatan'] = sanitizeStr($body['catatan_verifikasi'], 1000);
}
$pdo->prepare("UPDATE laporan SET " . implode(', ', $sets) . " WHERE id = :id")->execute($params);
logAktivitas($pdo, $currentUser['id'], 'UPDATE', 'laporan', (string)$id, $dataLama, $body);
jsonResponse(true, "Status laporan diperbarui menjadi '$status'.", ['id' => $id]);
}
// ============================================================
// PUT: Update koordinat (via drag map)
// ============================================================
if ($method === 'PUT' && $action === 'update_koordinat') {
if (!canVerify($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$id = (int)($_GET['id'] ?? 0);
$body = json_decode(file_get_contents('php://input'), true);
$lat = isset($body['latitude']) ? (float)$body['latitude'] : null;
$lng = isset($body['longitude']) ? (float)$body['longitude'] : null;
if (!$lat || !$lng) jsonResponse(false, 'Latitude dan Longitude wajib diisi.', [], 400);
$pdo->prepare("UPDATE laporan SET latitude = :lat, longitude = :lng, updated_at = NOW() WHERE id = :id")->execute([
':lat' => $lat,
':lng' => $lng,
':id' => $id
]);
logAktivitas($pdo, $currentUser['id'], 'UPDATE_KOORDINAT', 'laporan', (string)$id);
jsonResponse(true, 'Koordinat laporan berhasil diperbarui.', ['id' => $id]);
}
jsonResponse(false, 'Method tidak didukung.', [], 405);
+328
View File
@@ -0,0 +1,328 @@
<?php
// ============================================================
// API: Penduduk
// GET ?action=list|detail&nik=...
// POST action=create
// PUT action=update&nik=...
// DELETE action=delete&nik=...
// ============================================================
require_once __DIR__ . '/../middleware/auth.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'];
$action = $_GET['action'] ?? '';
$currentUser = getApiUser($pdo);
// ============================================================
// GET
// ============================================================
if ($method === 'GET') {
if ($action === 'list') {
$where = ['p.status_hidup = "hidup"'];
$params = [];
if (!empty($_GET['status_ekonomi'])) {
$where[] = 'p.status_ekonomi = :se';
$params[':se'] = $_GET['status_ekonomi'];
}
if (!empty($_GET['id_keluarga'])) {
$where[] = 'p.id_keluarga = :kk';
$params[':kk'] = (int)$_GET['id_keluarga'];
}
if (!empty($_GET['belum_dibantu'])) {
$where[] = 'NOT EXISTS (SELECT 1 FROM histori_bantuan hb WHERE hb.nik = p.nik AND hb.status_penyaluran="disalurkan")';
}
if (!empty($_GET['kondisi_kesehatan'])) {
$where[] = 'p.kondisi_kesehatan = :kk2';
$params[':kk2'] = $_GET['kondisi_kesehatan'];
}
if (!empty($_GET['search'])) {
$where[] = '(p.nama_lengkap LIKE :q OR p.nik LIKE :q2)';
$params[':q'] = '%' . $_GET['search'] . '%';
$params[':q2'] = '%' . $_GET['search'] . '%';
}
$whereStr = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
$sql = "
SELECT
p.nik, p.nama_lengkap, p.jenis_kelamin, p.tanggal_lahir,
p.umur, p.status_keluarga, p.status_perkawinan,
p.status_ekonomi, p.kondisi_kesehatan,
p.pekerjaan, p.penghasilan,
p.pendidikan_terakhir, p.status_pendidikan,
p.alamat, p.latitude, p.longitude, p.foto,
p.id_keluarga,
k.no_kk, k.nama_kk, k.status_ekonomi AS status_ekonomi_kk,
ri.nama AS nama_rumah_ibadah, ri.jenis AS jenis_rumah_ibadah,
(SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.nik = p.nik AND hb.status_penyaluran='disalurkan') AS jumlah_bantuan_diterima,
(SELECT MAX(hb2.tanggal) FROM histori_bantuan hb2 WHERE hb2.nik = p.nik AND hb2.status_penyaluran='disalurkan') AS terakhir_dibantu
FROM penduduk p
LEFT JOIN keluarga k ON k.id = p.id_keluarga
LEFT JOIN rumah_ibadah ri ON ri.id = k.rumah_ibadah_id
$whereStr
ORDER BY
CASE p.status_ekonomi WHEN 'miskin' THEN 1 WHEN 'rentan' THEN 2 ELSE 3 END ASC,
CASE p.kondisi_kesehatan WHEN 'sakit_parah' THEN 1 WHEN 'disabilitas' THEN 2 WHEN 'sakit_ringan' THEN 3 ELSE 4 END ASC
LIMIT 1000
";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
// Hitung prioritas untuk setiap penduduk
foreach ($rows as &$r) {
$belumDibantu = (int)$r['jumlah_bantuan_diterima'] === 0;
$r['prioritas_bantuan'] = hitungPrioritasBantuan($r['status_ekonomi'], $r['kondisi_kesehatan'], $belumDibantu);
}
unset($r);
jsonResponse(true, 'OK', ['data' => $rows, 'total' => count($rows)]);
}
if ($action === 'detail') {
$nik = sanitizeStr($_GET['nik'] ?? '');
if (!validateNIK($nik)) jsonResponse(false, 'NIK tidak valid.', [], 400);
$stmt = $pdo->prepare("
SELECT p.*, k.no_kk, k.nama_kk, ri.nama AS nama_ri
FROM penduduk p
LEFT JOIN keluarga k ON k.id = p.id_keluarga
LEFT JOIN rumah_ibadah ri ON ri.id = k.rumah_ibadah_id
WHERE p.nik = :nik
");
$stmt->execute([':nik' => $nik]);
$penduduk = $stmt->fetch();
if (!$penduduk) jsonResponse(false, 'Data tidak ditemukan.', [], 404);
// Histori bantuan
$stmtHB = $pdo->prepare("
SELECT hb.*, jb.nama AS nama_bantuan, jb.kategori, ri.nama AS nama_ri
FROM histori_bantuan hb
JOIN jenis_bantuan jb ON jb.id = hb.id_jenis_bantuan
LEFT JOIN rumah_ibadah ri ON ri.id = hb.rumah_ibadah_id
WHERE hb.nik = :nik
ORDER BY hb.tanggal DESC
");
$stmtHB->execute([':nik' => $nik]);
$histori = $stmtHB->fetchAll();
// Pelatihan
$stmtPl = $pdo->prepare("SELECT * FROM riwayat_pelatihan WHERE nik = :nik ORDER BY tanggal_mulai DESC");
$stmtPl->execute([':nik' => $nik]);
$pelatihan = $stmtPl->fetchAll();
// Validasi pendidikan
$validasi = validasiPendidikanUmur(
(int)$penduduk['umur'],
$penduduk['pendidikan_terakhir'],
$penduduk['status_pendidikan']
);
$belumDibantu = empty($histori);
$penduduk['prioritas_bantuan'] = hitungPrioritasBantuan($penduduk['status_ekonomi'], $penduduk['kondisi_kesehatan'], $belumDibantu);
jsonResponse(true, 'OK', [
'data' => $penduduk,
'histori' => $histori,
'pelatihan' => $pelatihan,
'validasi' => $validasi,
]);
}
if ($action === 'geojson') {
// Untuk layer Leaflet
$stmt = $pdo->query("
SELECT p.nik, p.nama_lengkap, p.status_ekonomi, p.kondisi_kesehatan,
p.latitude, p.longitude,
k.nama_kk,
(SELECT COUNT(*) FROM histori_bantuan hb WHERE hb.nik=p.nik AND hb.status_penyaluran='disalurkan') AS jml_bantuan
FROM penduduk p
LEFT JOIN keluarga k ON k.id = p.id_keluarga
WHERE p.latitude IS NOT NULL AND p.longitude IS NOT NULL
AND p.status_hidup = 'hidup'
");
$rows = $stmt->fetchAll();
$features = [];
foreach ($rows as $r) {
$color = match($r['status_ekonomi']) {
'miskin' => '#ef4444',
'rentan' => '#f59e0b',
default => '#22c55e'
};
$features[] = [
'type' => 'Feature',
'geometry' => [
'type' => 'Point',
'coordinates' => [(float)$r['longitude'], (float)$r['latitude']]
],
'properties' => array_merge($r, ['color' => $color])
];
}
header('Content-Type: application/json');
echo json_encode(['type' => 'FeatureCollection', 'features' => $features], JSON_UNESCAPED_UNICODE);
exit;
}
jsonResponse(false, 'Action tidak dikenal.', [], 400);
}
// ============================================================
// POST - Create
// ============================================================
if ($method === 'POST') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$body = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$nik = sanitizeStr($body['nik'] ?? '');
$nama = sanitizeStr($body['nama_lengkap'] ?? '');
$jk = sanitizeStr($body['jenis_kelamin'] ?? '');
$tgl = sanitizeStr($body['tanggal_lahir'] ?? '');
$idKk = !empty($body['id_keluarga']) ? (int)$body['id_keluarga'] : null;
if (!validateNIK($nik)) jsonResponse(false, 'Format NIK harus 16 digit angka.', [], 400);
if (strlen($nama) < 3) jsonResponse(false, 'Nama terlalu pendek.', [], 400);
if (!in_array($jk, ['L','P'])) jsonResponse(false, 'Jenis kelamin tidak valid.', [], 400);
if (!strtotime($tgl)) jsonResponse(false, 'Format tanggal lahir tidak valid.', [], 400);
// Cek duplikat NIK
$cek = $pdo->prepare("SELECT nik FROM penduduk WHERE nik = :nik");
$cek->execute([':nik' => $nik]);
if ($cek->fetch()) jsonResponse(false, 'NIK sudah terdaftar.', [], 409);
// Hitung umur untuk validasi pendidikan
$umur = (int)(new DateTime())->diff(new DateTime($tgl))->y;
$statusPendidikan = sanitizeStr($body['status_pendidikan'] ?? 'tidak_sekolah');
$pendidikan = sanitizeStr($body['pendidikan_terakhir'] ?? 'tidak_sekolah');
$validasi = validasiPendidikanUmur($umur, $pendidikan, $statusPendidikan);
$lat = !empty($body['latitude']) ? (float)$body['latitude'] : null;
$lng = !empty($body['longitude']) ? (float)$body['longitude'] : null;
if ($lat !== null && !validateCoord((string)$lat, 'lat')) jsonResponse(false, 'Latitude tidak valid.', [], 400);
if ($lng !== null && !validateCoord((string)$lng, 'lng')) jsonResponse(false, 'Longitude tidak valid.', [], 400);
$stmt = $pdo->prepare("
INSERT INTO penduduk
(nik, id_keluarga, nama_lengkap, jenis_kelamin, tanggal_lahir,
status_keluarga, status_perkawinan, status_hidup,
pendidikan_terakhir, status_pendidikan,
pekerjaan, penghasilan, status_ekonomi,
kondisi_kesehatan, catatan_kesehatan,
no_telp, alamat, latitude, longitude)
VALUES
(:nik,:kk,:nama,:jk,:tgl,
:sk,:sp,:sh,
:ptk,:spd,
:pkr,:pgn,:se,
:kkh,:ckkh,
:tlp,:almt,:lat,:lng)
");
$stmt->execute([
':nik' => $nik,
':kk' => $idKk,
':nama' => $nama,
':jk' => $jk,
':tgl' => $tgl,
':sk' => sanitizeStr($body['status_keluarga'] ?? 'anggota_lain'),
':sp' => sanitizeStr($body['status_perkawinan'] ?? 'belum_kawin'),
':sh' => sanitizeStr($body['status_hidup'] ?? 'hidup'),
':ptk' => $pendidikan,
':spd' => $statusPendidikan,
':pkr' => sanitizeStr($body['pekerjaan'] ?? '') ?: null,
':pgn' => !empty($body['penghasilan']) ? (float)$body['penghasilan'] : 0,
':se' => sanitizeStr($body['status_ekonomi'] ?? 'rentan'),
':kkh' => sanitizeStr($body['kondisi_kesehatan'] ?? 'sehat'),
':ckkh' => sanitizeStr($body['catatan_kesehatan'] ?? '') ?: null,
':tlp' => sanitizeStr($body['no_telp'] ?? '') ?: null,
':almt' => sanitizeStr($body['alamat'] ?? '') ?: null,
':lat' => $lat,
':lng' => $lng,
]);
logAktivitas($pdo, $currentUser['id'] ?? null, 'CREATE', 'penduduk', $nik, null, $body);
jsonResponse(true, 'Penduduk berhasil ditambahkan.', [
'nik' => $nik,
'validasi' => $validasi
], 201);
}
// ============================================================
// PUT - Update
// ============================================================
if ($method === 'PUT') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$nik = sanitizeStr($_GET['nik'] ?? '');
if (!validateNIK($nik)) jsonResponse(false, 'NIK tidak valid.', [], 400);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
// Ambil data lama
$old = $pdo->prepare("SELECT * FROM penduduk WHERE nik = :nik");
$old->execute([':nik' => $nik]);
$dataLama = $old->fetch();
if (!$dataLama) jsonResponse(false, 'Data tidak ditemukan.', [], 404);
$sets = [];
$params = [':nik' => $nik];
$fieldMap = [
'id_keluarga' => ':kk', 'nama_lengkap' => ':nama', 'jenis_kelamin' => ':jk',
'tanggal_lahir' => ':tgl', 'status_keluarga' => ':sk', 'status_perkawinan' => ':sp',
'status_hidup' => ':sh', 'pendidikan_terakhir' => ':ptk', 'status_pendidikan' => ':spd',
'pekerjaan' => ':pkr', 'penghasilan' => ':pgn', 'status_ekonomi' => ':se',
'kondisi_kesehatan' => ':kkh', 'catatan_kesehatan' => ':ckkh',
'no_telp' => ':tlp', 'alamat' => ':almt', 'latitude' => ':lat', 'longitude' => ':lng',
];
foreach ($fieldMap as $field => $param) {
if (array_key_exists($field, $body)) {
$sets[] = "$field = $param";
$params[$param] = $body[$field] === '' ? null : $body[$field];
}
}
if (empty($sets)) jsonResponse(false, 'Tidak ada data yang diubah.', [], 400);
$sql = "UPDATE penduduk SET " . implode(', ', $sets) . " WHERE nik = :nik";
$pdo->prepare($sql)->execute($params);
logAktivitas($pdo, $currentUser['id'] ?? null, 'UPDATE', 'penduduk', $nik, $dataLama, $body);
jsonResponse(true, 'Data penduduk berhasil diperbarui.', ['nik' => $nik]);
}
// ============================================================
// DELETE
// ============================================================
if ($method === 'DELETE') {
if (!isAdmin($currentUser)) jsonResponse(false, 'Hanya admin yang bisa menghapus data.', [], 403);
$nik = sanitizeStr($_GET['nik'] ?? '');
if (!validateNIK($nik)) jsonResponse(false, 'NIK tidak valid.', [], 400);
$old = $pdo->prepare("SELECT * FROM penduduk WHERE nik = :nik");
$old->execute([':nik' => $nik]);
$dataLama = $old->fetch();
if (!$dataLama) jsonResponse(false, 'Data tidak ditemukan.', [], 404);
$pdo->prepare("DELETE FROM penduduk WHERE nik = :nik")->execute([':nik' => $nik]);
logAktivitas($pdo, $currentUser['id'] ?? null, 'DELETE', 'penduduk', $nik, $dataLama, null);
jsonResponse(true, 'Data penduduk dihapus.');
}
jsonResponse(false, 'Method tidak didukung.', [], 405);
+127
View File
@@ -0,0 +1,127 @@
<?php
// ============================================================
// API: Pesan / Chat antar Pengurus Rumah Ibadah
// ============================================================
require_once __DIR__ . '/../middleware/auth.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit; }
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
$currentUser = getApiUser($pdo);
// Harus login untuk fitur chat
if (!$currentUser) jsonResponse(false, 'Harus login.', [], 401);
if ($method === 'GET') {
if ($action === 'inbox') {
$riId = !empty($_GET['rumah_ibadah_id']) ? (int)$_GET['rumah_ibadah_id'] : null;
if ($riId) {
// Chat diskusi per rumah ibadah
$stmt = $pdo->prepare("
SELECT p.*, u.nama AS pengirim, u.role_id,
ri.nama AS nama_ri
FROM pesan p
JOIN users u ON u.id = p.dari_user_id
LEFT JOIN rumah_ibadah ri ON ri.id = p.ke_rumah_ibadah_id
WHERE p.ke_rumah_ibadah_id = :ri
ORDER BY p.created_at ASC
LIMIT 200
");
$stmt->execute([':ri' => $riId]);
} else {
// Pesan langsung ke user
$stmt = $pdo->prepare("
SELECT p.*, u.nama AS pengirim
FROM pesan p
JOIN users u ON u.id = p.dari_user_id
WHERE p.ke_user_id = :uid OR p.dari_user_id = :uid2
ORDER BY p.created_at ASC
LIMIT 200
");
$stmt->execute([':uid' => $currentUser['id'], ':uid2' => $currentUser['id']]);
}
$pesan = $stmt->fetchAll();
// Tandai sudah dibaca
if ($riId) {
$pdo->prepare("UPDATE pesan SET dibaca=1 WHERE ke_rumah_ibadah_id=:ri AND dari_user_id != :uid")
->execute([':ri' => $riId, ':uid' => $currentUser['id']]);
} else {
$pdo->prepare("UPDATE pesan SET dibaca=1 WHERE ke_user_id=:uid")
->execute([':uid' => $currentUser['id']]);
}
jsonResponse(true, 'OK', ['data' => $pesan]);
}
if ($action === 'notifikasi') {
$stmt = $pdo->prepare("
SELECT * FROM notifikasi
WHERE user_id = :uid
ORDER BY created_at DESC
LIMIT 30
");
$stmt->execute([':uid' => $currentUser['id']]);
$notif = $stmt->fetchAll();
$unread = (int)$pdo->prepare("SELECT COUNT(*) FROM notifikasi WHERE user_id=:uid AND dibaca=0")
->execute([':uid' => $currentUser['id']]);
jsonResponse(true, 'OK', ['data' => $notif, 'unread' => array_sum(array_column($notif, 'dibaca') === array_map(fn($r) => $r['dibaca'] == 0, $notif))]);
}
if ($action === 'pengurus_list') {
// Daftar pengurus untuk pilihan chat
$rows = $pdo->query("
SELECT u.id, u.nama, u.email, ri.nama AS nama_ri, ri.jenis AS jenis_ri
FROM users u
LEFT JOIN rumah_ibadah ri ON ri.id = u.rumah_ibadah_id
WHERE u.role_id IN (1,2) AND u.aktif=1
ORDER BY u.nama
")->fetchAll();
jsonResponse(true, 'OK', ['data' => $rows]);
}
jsonResponse(false, 'Action tidak dikenal.', [], 400);
}
if ($method === 'POST') {
// Kirim pesan
$body = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$isi = sanitizeStr($body['isi'] ?? '', 2000);
if (strlen(trim($isi)) < 1) jsonResponse(false, 'Pesan tidak boleh kosong.', [], 400);
$keUserId = !empty($body['ke_user_id']) ? (int)$body['ke_user_id'] : null;
$keRiId = !empty($body['ke_rumah_ibadah_id']) ? (int)$body['ke_rumah_ibadah_id'] : null;
if (!$keUserId && !$keRiId) jsonResponse(false, 'Tujuan pesan harus diisi.', [], 400);
$stmt = $pdo->prepare("
INSERT INTO pesan (dari_user_id, ke_user_id, ke_rumah_ibadah_id, isi)
VALUES (:dari, :ke_user, :ke_ri, :isi)
");
$stmt->execute([
':dari' => $currentUser['id'],
':ke_user' => $keUserId,
':ke_ri' => $keRiId,
':isi' => $isi,
]);
$newId = (int)$pdo->lastInsertId();
// Notifikasi ke penerima
if ($keUserId) {
$pdo->prepare("INSERT INTO notifikasi (user_id, judul, isi, tipe, ref_id) VALUES (:u,:j,:i,'pesan',:r)")
->execute([':u'=>$keUserId, ':j'=>"Pesan dari {$currentUser['role']}", ':i'=>substr($isi,0,100), ':r'=>$newId]);
}
jsonResponse(true, 'Pesan terkirim.', ['id' => $newId], 201);
}
jsonResponse(false, 'Method tidak didukung.', [], 405);
+111
View File
@@ -0,0 +1,111 @@
<?php
// ============================================================
// API: Rumah Ibadah
// ============================================================
require_once __DIR__ . '/../middleware/auth.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'];
$action = $_GET['action'] ?? '';
$currentUser = getApiUser($pdo);
if ($method === 'GET') {
if ($action === 'list') {
$rows = $pdo->query("
SELECT ri.*,
COUNT(k.id) AS jumlah_kk,
SUM(CASE WHEN k.status_ekonomi='miskin' THEN 1 ELSE 0 END) AS kk_miskin
FROM rumah_ibadah ri
LEFT JOIN keluarga k ON k.rumah_ibadah_id = ri.id
GROUP BY ri.id
ORDER BY ri.nama
")->fetchAll();
jsonResponse(true, 'OK', ['data' => $rows]);
}
if ($action === 'geojson') {
$rows = $pdo->query("SELECT * FROM rumah_ibadah")->fetchAll();
$features = [];
foreach ($rows as $r) {
$features[] = [
'type' => 'Feature',
'geometry' => ['type'=>'Point','coordinates'=>[(float)$r['longitude'],(float)$r['latitude']]],
'properties' => $r
];
}
header('Content-Type: application/json');
echo json_encode(['type'=>'FeatureCollection','features'=>$features], JSON_UNESCAPED_UNICODE);
exit;
}
jsonResponse(false, 'Action tidak dikenal.', [], 400);
}
if ($method === 'POST') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$body = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$nama = sanitizeStr($body['nama'] ?? '');
$lat = (float)($body['latitude'] ?? 0);
$lng = (float)($body['longitude'] ?? 0);
if (strlen($nama) < 3) jsonResponse(false, 'Nama terlalu pendek.', [], 400);
if (!$lat || !$lng) jsonResponse(false, 'Koordinat wajib diisi.', [], 400);
$stmt = $pdo->prepare("
INSERT INTO rumah_ibadah (nama, jenis, kontak, radius_meter, latitude, longitude, alamat, kelurahan, kecamatan)
VALUES (:nama,:jenis,:kontak,:radius,:lat,:lng,:alamat,:kel,:kec)
");
$stmt->execute([
':nama' => $nama,
':jenis' => sanitizeStr($body['jenis'] ?? 'Lainnya'),
':kontak' => sanitizeStr($body['kontak'] ?? '') ?: null,
':radius' => (float)($body['radius_meter'] ?? 300),
':lat' => $lat, ':lng' => $lng,
':alamat' => sanitizeStr($body['alamat'] ?? '') ?: null,
':kel' => sanitizeStr($body['kelurahan'] ?? '') ?: null,
':kec' => sanitizeStr($body['kecamatan'] ?? '') ?: null,
]);
$newId = (int)$pdo->lastInsertId();
logAktivitas($pdo, $currentUser['id']??null, 'CREATE', 'rumah_ibadah', (string)$newId, null, $body);
jsonResponse(true, 'Rumah ibadah ditambahkan.', ['id' => $newId], 201);
}
if ($method === 'PUT') {
if (!canEdit($currentUser)) jsonResponse(false, 'Akses ditolak.', [], 403);
$id = (int)($_GET['id'] ?? 0);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
if ($action === 'update_koordinat') {
$lat = (float)($body['latitude'] ?? 0);
$lng = (float)($body['longitude'] ?? 0);
if (!$lat || !$lng) jsonResponse(false, 'Koordinat tidak valid.', [], 400);
$pdo->prepare("UPDATE rumah_ibadah SET latitude=:lat, longitude=:lng WHERE id=:id")
->execute([':lat'=>$lat, ':lng'=>$lng, ':id'=>$id]);
logAktivitas($pdo, $currentUser['id']??null, 'UPDATE', 'rumah_ibadah', (string)$id, null, ['lat'=>$lat,'lng'=>$lng]);
jsonResponse(true, 'Koordinat diperbarui.', ['id' => $id]);
}
$pdo->prepare("
UPDATE rumah_ibadah SET nama=:nama,jenis=:jenis,kontak=:kontak,radius_meter=:radius,
latitude=:lat,longitude=:lng,alamat=:alamat,kelurahan=:kel,kecamatan=:kec WHERE id=:id
")->execute([
':nama'=>sanitizeStr($body['nama']??''), ':jenis'=>sanitizeStr($body['jenis']??'Lainnya'),
':kontak'=>sanitizeStr($body['kontak']??'')?: null, ':radius'=>(float)($body['radius_meter']??300),
':lat'=>(float)($body['latitude']??0), ':lng'=>(float)($body['longitude']??0),
':alamat'=>sanitizeStr($body['alamat']??'')?: null, ':kel'=>sanitizeStr($body['kelurahan']??'')?: null,
':kec'=>sanitizeStr($body['kecamatan']??'')?: null, ':id'=>$id,
]);
logAktivitas($pdo, $currentUser['id']??null, 'UPDATE', 'rumah_ibadah', (string)$id);
jsonResponse(true, 'Rumah ibadah diperbarui.', ['id' => $id]);
}
if ($method === 'DELETE') {
if (!isAdmin($currentUser)) jsonResponse(false, 'Hanya admin.', [], 403);
$id = (int)($_GET['id'] ?? 0);
$pdo->prepare("DELETE FROM rumah_ibadah WHERE id=:id")->execute([':id'=>$id]);
logAktivitas($pdo, $currentUser['id']??null, 'DELETE', 'rumah_ibadah', (string)$id);
jsonResponse(true, 'Dihapus.');
}
jsonResponse(false, 'Method tidak didukung.', [], 405);
+177
View File
@@ -0,0 +1,177 @@
<?php
// ============================================================
// Konfigurasi & Koneksi Database
// WebGIS Pengentasan Kemiskinan v2.0
// ============================================================
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
define('DB_PORT', getenv('DB_PORT') ?: '3306');
define('DB_NAME', getenv('DB_NAME') ?: 'webgis_kemiskinan');
define('DB_USER', getenv('DB_USER') ?: 'root');
define('DB_PASS', getenv('DB_PASSWORD') ?: '');
define('DB_CHARSET', 'utf8mb4');
define('APP_NAME', 'WebGIS Pengentasan Kemiskinan');
define('APP_VERSION', '2.0.0');
define('APP_URL', 'http://localhost/webgis-kemiskinan');
define('APP_ENV', 'development'); // Ganti ke 'production' saat deploy
define('UPLOAD_PATH', __DIR__ . '/../uploads/');
define('UPLOAD_BUKTI', UPLOAD_PATH . 'bukti/');
define('UPLOAD_LAPORAN', UPLOAD_PATH . 'foto_laporan/');
define('UPLOAD_FOTO', UPLOAD_PATH . 'foto_profil/');
define('MAX_FILE_SIZE', 5 * 1024 * 1024); // 5 MB
define('ALLOWED_IMG_EXT', ['jpg','jpeg','png','webp']);
define('ALLOWED_FILE_EXT', ['jpg','jpeg','png','webp','pdf']);
// Buat folder upload jika belum ada
foreach ([UPLOAD_BUKTI, UPLOAD_LAPORAN, UPLOAD_FOTO] as $dir) {
if (!is_dir($dir)) @mkdir($dir, 0755, true);
}
// ============================================================
// Koneksi PDO
// ============================================================
$dsn = "mysql:host=" . DB_HOST . ";port=" . DB_PORT . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
$pdoOptions = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, DB_USER, DB_PASS, $pdoOptions);
} catch (PDOException $e) {
http_response_code(500);
header('Content-Type: application/json');
// Jangan expose detail error di produksi
$errMsg = (defined('APP_ENV') && APP_ENV === 'development') ? $e->getMessage() : 'Hubungi administrator sistem.';
echo json_encode([
'success' => false,
'message' => 'Koneksi database gagal: ' . DB_NAME . '@' . DB_HOST . ' tidak ditemukan. Pastikan database sudah dibuat dan nama database benar.',
'error' => $errMsg
]);
exit;
}
// ============================================================
// Helper: Response JSON
// ============================================================
function jsonResponse(bool $success, string $message, array $data = [], int $httpCode = 200): void {
http_response_code($httpCode);
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
echo json_encode(array_merge(['success' => $success, 'message' => $message], $data), JSON_UNESCAPED_UNICODE);
exit;
}
// ============================================================
// Helper: Sanitasi & Validasi
// ============================================================
function h(string $s): string {
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
function sanitizeStr(?string $v, int $max = 255): string {
return mb_substr(trim($v ?? ''), 0, $max);
}
function validateNIK(string $nik): bool {
return preg_match('/^\d{16}$/', $nik) === 1;
}
function validateCoord(string $val, string $type = 'lat'): bool {
$f = (float)$val;
return $type === 'lat' ? ($f >= -90 && $f <= 90) : ($f >= -180 && $f <= 180);
}
// ============================================================
// Helper: Upload File
// ============================================================
function uploadFile(array $file, string $destDir, array $allowedExt = null): ?string {
if ($allowedExt === null) $allowedExt = ALLOWED_FILE_EXT;
if ($file['error'] !== UPLOAD_ERR_OK) return null;
if ($file['size'] > MAX_FILE_SIZE) throw new RuntimeException('Ukuran file melebihi 5MB.');
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExt, true)) throw new RuntimeException("Format file .$ext tidak diizinkan.");
$filename = uniqid('img_', true) . '.' . $ext;
$dest = rtrim($destDir, '/') . '/' . $filename;
if (!move_uploaded_file($file['tmp_name'], $dest)) throw new RuntimeException('Gagal menyimpan file.');
return $filename;
}
// ============================================================
// Helper: Log Aktivitas
// ============================================================
function logAktivitas(PDO $pdo, ?int $userId, string $aksi, string $tabel = '', string $recordId = '', $dataLama = null, $dataBaru = null): void {
try {
$stmt = $pdo->prepare("
INSERT INTO log_aktivitas (user_id, aksi, tabel, record_id, data_lama, data_baru, ip_address, user_agent)
VALUES (:u, :a, :t, :r, :dl, :db, :ip, :ua)
");
$stmt->execute([
':u' => $userId,
':a' => $aksi,
':t' => $tabel,
':r' => $recordId,
':dl' => $dataLama ? json_encode($dataLama, JSON_UNESCAPED_UNICODE) : null,
':db' => $dataBaru ? json_encode($dataBaru, JSON_UNESCAPED_UNICODE) : null,
':ip' => $_SERVER['REMOTE_ADDR'] ?? null,
':ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255),
]);
} catch (Exception) { /* log gagal tidak menghentikan proses */ }
}
// ============================================================
// Validasi Logika Pendidikan vs Umur
// ============================================================
function validasiPendidikanUmur(int $umur, string $pendidikan, string $statusPendidikan): array {
$warnings = [];
$errors = [];
if ($umur >= 7 && $umur <= 12) {
if ($statusPendidikan !== 'sekolah') {
$warnings[] = "Anak usia $umur tahun semestinya masih sekolah SD.";
}
} elseif ($umur >= 13 && $umur <= 15) {
if ($statusPendidikan !== 'sekolah') {
$warnings[] = "Anak usia $umur tahun semestinya masih sekolah SMP.";
}
} elseif ($umur >= 16 && $umur <= 18) {
if ($statusPendidikan !== 'sekolah') {
$warnings[] = "Anak usia $umur tahun semestinya masih sekolah SMA/SMK.";
}
}
// Umur > 18: bebas, boleh tidak sekolah, boleh pelatihan
return ['warnings' => $warnings, 'errors' => $errors];
}
// ============================================================
// Prioritas Bantuan
// ============================================================
function hitungPrioritasBantuan(string $statusEkonomi, string $kondisiKesehatan, bool $belumPernahDibantu): string {
if ($statusEkonomi === 'miskin' && in_array($kondisiKesehatan, ['sakit_parah', 'disabilitas'])) {
return 'KRITIS';
}
if ($statusEkonomi === 'miskin' && $belumPernahDibantu) {
return 'TINGGI';
}
if ($statusEkonomi === 'miskin') {
return 'SEDANG';
}
if ($statusEkonomi === 'rentan' && in_array($kondisiKesehatan, ['sakit_parah', 'disabilitas'])) {
return 'SEDANG';
}
return 'RENDAH';
}
+125
View File
@@ -0,0 +1,125 @@
<?php
// ============================================================
// Middleware Autentikasi & Otorisasi
// ============================================================
require_once __DIR__ . '/../koneksi.php';
if (session_status() === PHP_SESSION_NONE) {
session_start([
'cookie_httponly' => true,
'cookie_samesite' => 'Strict',
'use_strict_mode' => true,
]);
}
// ============================================================
// Fungsi login / logout
// ============================================================
function doLogin(PDO $pdo, string $username, string $password): array {
$stmt = $pdo->prepare("
SELECT u.*, r.nama AS role_nama
FROM users u
JOIN roles r ON r.id = u.role_id
WHERE u.username = :u AND u.aktif = 1
LIMIT 1
");
$stmt->execute([':u' => $username]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password_hash'])) {
return ['success' => false, 'message' => 'Username atau password salah.'];
}
// Update last_login
$pdo->prepare("UPDATE users SET last_login = NOW() WHERE id = :id")
->execute([':id' => $user['id']]);
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_nama'] = $user['nama'];
$_SESSION['role'] = $user['role_nama'];
$_SESSION['role_id'] = $user['role_id'];
$_SESSION['rumah_ibadah_id'] = $user['rumah_ibadah_id'];
session_regenerate_id(true);
logAktivitas($pdo, $user['id'], 'LOGIN', 'users', (string)$user['id']);
return ['success' => true, 'role' => $user['role_nama']];
}
function doLogout(PDO $pdo): void {
if (isset($_SESSION['user_id'])) {
logAktivitas($pdo, $_SESSION['user_id'], 'LOGOUT', 'users', (string)$_SESSION['user_id']);
}
session_destroy();
}
// ============================================================
// Guard: wajib login
// ============================================================
function requireLogin(array $allowedRoles = []): array {
if (empty($_SESSION['user_id'])) {
// Jika request API
if (!empty($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) {
jsonResponse(false, 'Sesi habis. Silakan login kembali.', [], 401);
}
header('Location: ' . APP_URL . '/admin/login.php');
exit;
}
if (!empty($allowedRoles) && !in_array($_SESSION['role'], $allowedRoles, true)) {
if (!empty($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) {
jsonResponse(false, 'Tidak punya akses.', [], 403);
}
header('Location: ' . APP_URL . '/admin/forbidden.php');
exit;
}
return [
'id' => (int)$_SESSION['user_id'],
'nama' => $_SESSION['user_nama'],
'role' => $_SESSION['role'],
'role_id' => (int)$_SESSION['role_id'],
'rumah_ibadah_id' => $_SESSION['rumah_ibadah_id'] ? (int)$_SESSION['rumah_ibadah_id'] : null,
];
}
// ============================================================
// Guard API: token-based (opsional, untuk mobile app nanti)
// ============================================================
function getApiUser(PDO $pdo): ?array {
// Cek session dulu
if (!empty($_SESSION['user_id'])) {
return [
'id' => (int)$_SESSION['user_id'],
'role' => $_SESSION['role'],
'role_id' => (int)$_SESSION['role_id'],
'rumah_ibadah_id' => $_SESSION['rumah_ibadah_id'] ? (int)$_SESSION['rumah_ibadah_id'] : null,
];
}
return null; // belum login = null
}
// ============================================================
// Cek izin per fitur
// ============================================================
function canEdit(?array $user): bool {
if (!$user) return false;
return in_array($user['role'], ['admin', 'pengurus'], true);
}
function canVerify(?array $user): bool {
if (!$user) return false;
return in_array($user['role'], ['admin', 'pengurus'], true);
}
function canViewAll(?array $user): bool {
if (!$user) return false;
return in_array($user['role'], ['admin', 'pengurus', 'pimpinan'], true);
}
function isAdmin(?array $user): bool {
return $user && $user['role'] === 'admin';
}