feat: complete WebGIS implementation with nominal bantuan and cache busting

This commit is contained in:
fananazril
2026-06-11 19:18:25 +07:00
parent 0d59e629bd
commit 0ae2768849
36 changed files with 7687 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
<?php
// ================================================================
// API: Anggota Keluarga
// ================================================================
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../includes/functions.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
$db = getDB();
$method = $_SERVER['REQUEST_METHOD'];
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
$pid = isset($_GET['penduduk_id']) ? (int)$_GET['penduduk_id'] : null;
if ($method === 'GET') {
if (!$pid) jsonResponse(['success' => false, 'message' => 'penduduk_id wajib'], 400);
$stmt = $db->prepare("SELECT * FROM anggota_keluarga WHERE penduduk_id = ? ORDER BY id");
$stmt->execute([$pid]);
jsonResponse(['success' => true, 'data' => $stmt->fetchAll()]);
}
if ($method === 'POST') {
$body = json_decode(file_get_contents('php://input'), true) ?? [];
if (empty($body['penduduk_id']) || empty($body['nama'])) {
jsonResponse(['success' => false, 'message' => 'penduduk_id dan nama wajib'], 422);
}
$stmt = $db->prepare("INSERT INTO anggota_keluarga (penduduk_id,nama,hubungan,umur,pekerjaan,keterangan) VALUES (?,?,?,?,?,?)");
$stmt->execute([
(int)$body['penduduk_id'],
sanitize($body['nama'] ?? ''),
sanitize($body['hubungan'] ?? ''),
!empty($body['umur']) ? (int)$body['umur'] : null,
sanitize($body['pekerjaan'] ?? ''),
sanitize($body['keterangan'] ?? ''),
]);
jsonResponse(['success' => true, 'id' => $db->lastInsertId()], 201);
}
if ($method === 'DELETE') {
if (!$id) jsonResponse(['success' => false, 'message' => 'id wajib'], 400);
$stmt = $db->prepare("DELETE FROM anggota_keluarga WHERE id = ?");
$stmt->execute([$id]);
jsonResponse(['success' => true]);
}
jsonResponse(['success' => false, 'message' => 'Method tidak didukung'], 405);
+211
View File
@@ -0,0 +1,211 @@
<?php
// ================================================================
// API: Export Data ke CSV atau HTML Print
// ================================================================
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../includes/functions.php';
// Wajib login untuk export
session_name(SESSION_NAME);
session_start();
if (empty($_SESSION['user_id'])) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Unauthorized']);
exit;
}
$db = getDB();
$type = $_GET['type'] ?? 'penduduk'; // penduduk | ibadah
$format = $_GET['format'] ?? 'csv'; // csv | print
// ── Query Penduduk Miskin ──────────────────────────────────────
if ($type === 'penduduk') {
$where = ['1=1'];
$params = [];
if (!empty($_GET['status'])) { $where[] = 'pm.status_bantuan = ?'; $params[] = $_GET['status']; }
if (!empty($_GET['kecamatan'])) { $where[] = 'pm.kecamatan = ?'; $params[] = $_GET['kecamatan']; }
if (!empty($_GET['ibadah_id'])) { $where[] = 'pm.rumah_ibadah_id = ?'; $params[] = (int)$_GET['ibadah_id']; }
if (!empty($_GET['dari'])) { $where[] = 'DATE(pm.created_at) >= ?'; $params[] = $_GET['dari']; }
if (!empty($_GET['sampai'])) { $where[] = 'DATE(pm.created_at) <= ?'; $params[] = $_GET['sampai']; }
$ws = implode(' AND ', $where);
$stmt = $db->prepare("
SELECT pm.id, pm.kk_nama, pm.nik, pm.jumlah_anggota, pm.alamat,
pm.kelurahan, pm.kecamatan, pm.lat, pm.lng,
pm.status_bantuan, pm.jenis_bantuan, pm.tanggal_bantuan, pm.nominal_bantuan, pm.jarak_ke_ibadah,
pm.created_at, ri.nama AS ibadah_nama, ri.jenis AS ibadah_jenis
FROM penduduk_miskin pm
LEFT JOIN rumah_ibadah ri ON pm.rumah_ibadah_id = ri.id
WHERE $ws ORDER BY pm.kecamatan, pm.kk_nama
");
$stmt->execute($params);
$rows = $stmt->fetchAll();
if ($format === 'csv') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="penduduk_miskin_' . date('Ymd_His') . '.csv"');
$out = fopen('php://output', 'w');
// BOM for Excel UTF-8
fputs($out, "\xEF\xBB\xBF");
fputcsv($out, ['No','Nama KK','NIK','Jumlah Anggota','Alamat','Kelurahan','Kecamatan',
'Latitude','Longitude','Status Bantuan','Jenis Bantuan','Tanggal Bantuan','Nominal Bantuan',
'Jarak ke Ibadah (m)','Rumah Ibadah','Jenis Ibadah','Terdaftar']);
foreach ($rows as $i => $row) {
fputcsv($out, [
$i + 1,
$row['kk_nama'],
$row['nik'] ?: '',
$row['jumlah_anggota'],
$row['alamat'] ?: '',
$row['kelurahan'] ?: '',
$row['kecamatan'] ?: '',
$row['lat'],
$row['lng'],
$row['status_bantuan'],
$row['jenis_bantuan'] ?: '',
$row['tanggal_bantuan'] ?: '',
$row['nominal_bantuan'] ? $row['nominal_bantuan'] : '',
$row['jarak_ke_ibadah'] ? round($row['jarak_ke_ibadah']) : '',
$row['ibadah_nama'] ?: '',
$row['ibadah_jenis'] ?: '',
$row['created_at'],
]);
}
fclose($out);
exit;
}
// Print / PDF view
$title = 'Data Penduduk Miskin';
$subtitle = 'WebGIS Sebaran Penduduk Miskin — ' . APP_NAME;
$cols = ['No', 'Nama KK', 'NIK', 'Anggota', 'Kecamatan', 'Status', 'Nominal Bantuan', 'Rumah Ibadah', 'Jarak'];
$tableRows = array_map(function($r, $i) {
$sc = ['sudah' => '#22c55e', 'menunggu' => '#f59e0b', 'belum' => '#ef4444'][$r['status_bantuan']] ?? '#ef4444';
return [$i + 1, $r['kk_nama'], $r['nik'] ?: '—', $r['jumlah_anggota'],
$r['kecamatan'] ?: '—',
"<span style='color:{$sc};font-weight:700'>".ucfirst($r['status_bantuan']).'</span>',
$r['nominal_bantuan'] ? 'Rp '.number_format($r['nominal_bantuan'],0,',','.') : '—',
$r['ibadah_nama'] ?: '—',
$r['jarak_ke_ibadah'] ? round($r['jarak_ke_ibadah']).' m' : '—'];
}, $rows, array_keys($rows));
}
// ── Query Rumah Ibadah ────────────────────────────────────────
if ($type === 'ibadah') {
$where = ['1=1'];
$params = [];
if (!empty($_GET['jenis'])) { $where[] = 'jenis = ?'; $params[] = $_GET['jenis']; }
if (!empty($_GET['kecamatan'])) { $where[] = 'kecamatan = ?'; $params[] = $_GET['kecamatan']; }
$ws = implode(' AND ', $where);
$stmt = $db->prepare("
SELECT ri.*,
(SELECT COUNT(*) FROM penduduk_miskin pm WHERE pm.rumah_ibadah_id = ri.id) AS jumlah_binaan
FROM rumah_ibadah ri WHERE $ws ORDER BY jenis, nama
");
$stmt->execute($params);
$rows = $stmt->fetchAll();
if ($format === 'csv') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="rumah_ibadah_' . date('Ymd_His') . '.csv"');
$out = fopen('php://output', 'w');
fputs($out, "\xEF\xBB\xBF");
fputcsv($out, ['No','Nama','Jenis','Kontak','Alamat','Kelurahan','Kecamatan',
'Latitude','Longitude','Radius (m)','Jumlah Binaan','Terdaftar']);
foreach ($rows as $i => $row) {
fputcsv($out, [
$i + 1, $row['nama'], $row['jenis'], $row['kontak'] ?: '',
$row['alamat'] ?: '', $row['kelurahan'] ?: '', $row['kecamatan'] ?: '',
$row['lat'], $row['lng'], $row['radius'], $row['jumlah_binaan'], $row['created_at']
]);
}
fclose($out);
exit;
}
$title = 'Data Rumah Ibadah';
$subtitle = 'WebGIS Sebaran Penduduk Miskin — ' . APP_NAME;
$cols = ['No', 'Nama', 'Jenis', 'Kontak', 'Kecamatan', 'Radius', 'Binaan'];
$tableRows = array_map(function($r, $i) {
return [$i + 1, $r['nama'], $r['jenis'], $r['kontak'] ?: '—',
$r['kecamatan'] ?: '—', formatRadius($r['radius']), $r['jumlah_binaan']];
}, $rows, array_keys($rows));
}
// ── HTML Print View ───────────────────────────────────────────
$total = count($rows);
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $title ?></title>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Plus Jakarta Sans', sans-serif; background: #fff; color: #1a1a2e; padding: 20px; font-size: 12px; }
.print-header { text-align: center; margin-bottom: 24px; border-bottom: 2px solid #5b7fff; padding-bottom: 16px; }
.print-header h1 { font-size: 20px; font-weight: 800; color: #1a1a2e; }
.print-header p { font-size: 12px; color: #666; margin-top: 4px; }
.print-meta { display: flex; gap: 16px; justify-content: center; margin-top: 8px; flex-wrap: wrap; font-size: 11px; color: #555; }
table { width: 100%; border-collapse: collapse; margin-top: 16px; }
th { background: #5b7fff; color: #fff; padding: 8px 10px; text-align: left; font-size: 11px; font-weight: 600; }
td { padding: 6px 10px; border-bottom: 1px solid #eee; font-size: 11px; vertical-align: top; }
tr:nth-child(even) td { background: #f8f9ff; }
.print-footer { margin-top: 20px; text-align: right; font-size: 10px; color: #999; }
.no-print { position: fixed; top: 20px; right: 20px; display: flex; gap: 8px; z-index: 999; }
.btn-print { background: #5b7fff; color: #fff; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-family: inherit; font-weight: 600; font-size: 13px; }
.btn-close-p { background: #ef4444; color: #fff; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-family: inherit; font-weight: 600; font-size: 13px; }
@media print { .no-print { display: none; } body { padding: 0; } }
</style>
</head>
<body>
<div class="no-print">
<button class="btn-print" onclick="window.print()">🖨️ Cetak / Save PDF</button>
<button class="btn-close-p" onclick="window.close()">✕ Tutup</button>
</div>
<div class="print-header">
<h1><?= htmlspecialchars($title) ?></h1>
<p><?= htmlspecialchars($subtitle) ?></p>
<div class="print-meta">
<span>📅 Dicetak: <?= date('d F Y, H:i') ?> WIB</span>
<span>📊 Total: <?= $total ?> data</span>
<?php if (!empty($_GET['status'])): ?>
<span>🔍 Status: <?= htmlspecialchars(ucfirst($_GET['status'])) ?></span>
<?php endif; ?>
<?php if (!empty($_GET['kecamatan'])): ?>
<span>📍 Kecamatan: <?= htmlspecialchars($_GET['kecamatan']) ?></span>
<?php endif; ?>
</div>
</div>
<table>
<thead>
<tr><?php foreach ($cols as $c) echo "<th>$c</th>"; ?></tr>
</thead>
<tbody>
<?php foreach ($tableRows as $tr): ?>
<tr><?php foreach ($tr as $td) echo "<td>$td</td>"; ?></tr>
<?php endforeach; ?>
<?php if (empty($tableRows)): ?>
<tr><td colspan="<?= count($cols) ?>" style="text-align:center;padding:20px;color:#999;">Tidak ada data</td></tr>
<?php endif; ?>
</tbody>
</table>
<div class="print-footer">
<?= APP_NAME ?> v<?= APP_VERSION ?> — <?= BASE_URL ?>
</div>
<script>
// Auto-trigger print dialog for print format
<?php if ($format === 'print'): ?>
window.addEventListener('load', () => setTimeout(() => {}, 500));
<?php endif; ?>
</script>
</body>
</html>
+44
View File
@@ -0,0 +1,44 @@
<?php
// ================================================================
// API: Reverse Geocoding Proxy (Nominatim)
// ================================================================
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
$lat = isset($_GET['lat']) ? (float)$_GET['lat'] : null;
$lng = isset($_GET['lng']) ? (float)$_GET['lng'] : null;
if (!$lat || !$lng) {
http_response_code(400);
echo json_encode(['error' => 'lat dan lng wajib']);
exit;
}
$url = "https://nominatim.openstreetmap.org/reverse?format=json&lat={$lat}&lon={$lng}&addressdetails=1&accept-language=id";
$opts = ['http' => ['header' => "User-Agent: WebGIS-PovertyMap/2.0 (https://github.com)\r\n", 'timeout' => 10]];
$context = stream_context_create($opts);
$result = @file_get_contents($url, false, $context);
if ($result === false) {
echo json_encode(['display_name' => "{$lat}, {$lng}", 'fallback' => true]);
exit;
}
$data = json_decode($result, true);
$addr = $data['display_name'] ?? "{$lat}, {$lng}";
// Ekstrak komponen alamat
$components = $data['address'] ?? [];
$kelurahan = $components['suburb'] ?? $components['village'] ?? $components['neighbourhood'] ?? '';
$kecamatan = $components['city_district'] ?? $components['district'] ?? '';
$kota = $components['city'] ?? $components['town'] ?? $components['county'] ?? '';
echo json_encode([
'display_name' => $addr,
'kelurahan' => $kelurahan,
'kecamatan' => $kecamatan,
'kota' => $kota,
'lat' => $lat,
'lng' => $lng,
]);
+184
View File
@@ -0,0 +1,184 @@
<?php
// ================================================================
// API: Import Data Massal dari CSV
// ================================================================
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/functions.php';
header('Content-Type: application/json; charset=utf-8');
// Hanya POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method tidak didukung']);
exit;
}
$db = getDB();
$type = $_POST['type'] ?? ''; // 'penduduk' | 'ibadah'
if (!in_array($type, ['penduduk', 'ibadah'])) {
echo json_encode(['success' => false, 'message' => 'Tipe import tidak valid']); exit;
}
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
echo json_encode(['success' => false, 'message' => 'File tidak valid atau tidak ada']); exit;
}
// Baca isi file
$content = file_get_contents($_FILES['file']['tmp_name']);
if ($content === false) {
echo json_encode(['success' => false, 'message' => 'Gagal membaca file']); exit;
}
// Normalize line endings & parse CSV
$content = str_replace("\r\n", "\n", $content);
$content = str_replace("\r", "\n", $content);
$lines = array_filter(explode("\n", $content), 'strlen');
$lines = array_values($lines);
if (empty($lines)) {
echo json_encode(['success' => false, 'message' => 'File kosong']); exit;
}
// Parse CSV baris per baris
function parseCSVLine(string $line): array {
$cols = [];
$cur = '';
$inQ = false;
for ($i = 0; $i < strlen($line); $i++) {
$ch = $line[$i];
if ($ch === '"') { $inQ = !$inQ; }
elseif ($ch === ',' && !$inQ) { $cols[] = trim($cur, " \t\""); $cur = ''; }
else { $cur .= $ch; }
}
$cols[] = trim($cur, " \t\"");
return $cols;
}
// Deteksi header row
$firstLine = strtolower($lines[0]);
$hasHeader = str_contains($firstLine, 'nama') || str_contains($firstLine, 'lat') || str_contains($firstLine, 'kk_nama');
$dataLines = $hasHeader ? array_slice($lines, 1) : $lines;
$imported = 0;
$failed = 0;
$skipped = 0;
$errors = [];
// ── Import Penduduk Miskin ─────────────────────────────────────
if ($type === 'penduduk') {
$stmt = $db->prepare("
INSERT INTO penduduk_miskin
(kk_nama, nik, jumlah_anggota, alamat, kelurahan, kecamatan, lat, lng,
rumah_ibadah_id, jarak_ke_ibadah, status_bantuan, jenis_bantuan, tanggal_bantuan, nominal_bantuan)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
");
foreach ($dataLines as $lineNum => $line) {
if (!trim($line)) continue;
$row = parseCSVLine($line);
// kk_nama (0), nik (1), jumlah_anggota (2), alamat (3), kelurahan (4),
// kecamatan (5), lat (6), lng (7), status_bantuan (8), jenis_bantuan (9),
// tanggal_bantuan (10), nominal_bantuan (11)
if (count($row) < 8 || empty($row[0]) || empty($row[6]) || empty($row[7])) {
$failed++;
$errors[] = "Baris " . ($lineNum + ($hasHeader ? 2 : 1)) . ": kolom tidak lengkap atau koordinat kosong";
if (count($errors) > 10) { $errors[] = '... dan lebih banyak error'; break; }
continue;
}
$lat = (float)$row[6];
$lng = (float)$row[7];
if ($lat === 0.0 && $lng === 0.0) { $skipped++; continue; }
$status = in_array($row[8] ?? '', ['sudah','belum','menunggu']) ? $row[8] : 'belum';
$tanggal_bantuan = !empty($row[10]) ? sanitize($row[10]) : null;
$nominal_bantuan = isset($row[11]) && $row[11] !== '' ? (float)str_replace(['.', ','], ['', '.'], $row[11]) : null;
$nearest = findNearestIbadah($lat, $lng);
$ibadahId = $nearest ? (int)$nearest['id'] : null;
$jarakIbadah = $nearest ? (float)$nearest['jarak'] : null;
try {
$stmt->execute([
sanitize($row[0]),
sanitize($row[1] ?? ''),
max(1, (int)($row[2] ?? 1)),
sanitize($row[3] ?? ''),
sanitize($row[4] ?? ''),
sanitize($row[5] ?? ''),
$lat, $lng,
$ibadahId,
$jarakIbadah,
$status,
sanitize($row[9] ?? ''),
$tanggal_bantuan,
$nominal_bantuan,
]);
$imported++;
} catch (PDOException $e) {
$failed++;
$errors[] = "Baris " . ($lineNum + ($hasHeader ? 2 : 1)) . ": " . $e->getMessage();
}
}
}
// ── Import Rumah Ibadah ───────────────────────────────────────
if ($type === 'ibadah') {
$validJenis = ['Masjid','Musholla','Gereja','Katolik','Pura','Vihara','Klenteng','Lainnya'];
$stmt = $db->prepare("
INSERT INTO rumah_ibadah (nama, jenis, kontak, alamat, kelurahan, kecamatan, lat, lng, radius)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
");
foreach ($dataLines as $lineNum => $line) {
if (!trim($line)) continue;
$row = parseCSVLine($line);
// nama (0), jenis (1), kontak (2), alamat (3), kelurahan (4),
// kecamatan (5), lat (6), lng (7), radius (8)
if (count($row) < 8 || empty($row[0]) || empty($row[6]) || empty($row[7])) {
$failed++;
$errors[] = "Baris " . ($lineNum + ($hasHeader ? 2 : 1)) . ": kolom tidak lengkap";
if (count($errors) > 10) { $errors[] = '... dan lebih banyak error'; break; }
continue;
}
$lat = (float)$row[6];
$lng = (float)$row[7];
if ($lat === 0.0 && $lng === 0.0) { $skipped++; continue; }
$jenis = in_array($row[1] ?? '', $validJenis) ? $row[1] : 'Lainnya';
$radius = max(100, min(5000, (int)($row[8] ?? RADIUS_DEFAULT)));
try {
$stmt->execute([
sanitize($row[0]),
$jenis,
sanitize($row[2] ?? ''),
sanitize($row[3] ?? ''),
sanitize($row[4] ?? ''),
sanitize($row[5] ?? ''),
$lat, $lng, $radius,
]);
$imported++;
} catch (PDOException $e) {
$failed++;
$errors[] = "Baris " . ($lineNum + ($hasHeader ? 2 : 1)) . ": " . $e->getMessage();
}
}
}
echo json_encode([
'success' => true,
'imported' => $imported,
'failed' => $failed,
'skipped' => $skipped,
'errors' => array_slice($errors, 0, 15),
'message' => "Import selesai: $imported berhasil, $failed gagal, $skipped dilewati",
], JSON_UNESCAPED_UNICODE);
+36
View File
@@ -0,0 +1,36 @@
<?php
// ================================================================
// API: Notify — Cek apakah ada data baru sejak timestamp tertentu
// ================================================================
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../includes/functions.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
$db = getDB();
$since = $_GET['since'] ?? date('Y-m-d H:i:s', strtotime('-1 minute'));
// Sanitasi sederhana
$since = preg_replace('/[^0-9\-: ]/', '', $since);
$newMiskin = (int)$db->prepare("SELECT COUNT(*) FROM penduduk_miskin WHERE created_at > ?")->execute([$since]) ? 0 : 0;
$stmtM = $db->prepare("SELECT COUNT(*) FROM penduduk_miskin WHERE created_at > ?");
$stmtM->execute([$since]);
$newMiskin = (int)$stmtM->fetchColumn();
$stmtI = $db->prepare("SELECT COUNT(*) FROM rumah_ibadah WHERE created_at > ?");
$stmtI->execute([$since]);
$newIbadah = (int)$stmtI->fetchColumn();
$stmtTotal = $db->query("SELECT COUNT(*) FROM penduduk_miskin WHERE status_bantuan='belum'");
$belumDibantu = (int)$stmtTotal->fetchColumn();
jsonResponse([
'success' => true,
'new_miskin' => $newMiskin,
'new_ibadah' => $newIbadah,
'belum_dibantu' => $belumDibantu,
'has_new' => ($newMiskin + $newIbadah) > 0,
'server_time' => date('Y-m-d H:i:s'),
]);
+275
View File
@@ -0,0 +1,275 @@
<?php
// ================================================================
// API: Penduduk Miskin
// Methods: GET, POST, PUT, DELETE
// ================================================================
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../includes/functions.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') {
http_response_code(204); exit;
}
$db = getDB();
$method = $_SERVER['REQUEST_METHOD'];
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
// ── GET ────────────────────────────────────────────────────────
if ($method === 'GET') {
if ($id) {
// Detail lengkap termasuk anggota keluarga dan info ibadah
$stmt = $db->prepare("
SELECT pm.*,
ri.nama AS ibadah_nama, ri.jenis AS ibadah_jenis,
ri.kontak AS ibadah_kontak, ri.lat AS ibadah_lat, ri.lng AS ibadah_lng
FROM penduduk_miskin pm
LEFT JOIN rumah_ibadah ri ON pm.rumah_ibadah_id = ri.id
WHERE pm.id = ?
");
$stmt->execute([$id]);
$row = $stmt->fetch();
if (!$row) jsonResponse(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
// Anggota keluarga
$stmtAk = $db->prepare("SELECT * FROM anggota_keluarga WHERE penduduk_id = ? ORDER BY id");
$stmtAk->execute([$id]);
$row['anggota'] = $stmtAk->fetchAll();
jsonResponse(['success' => true, 'data' => $row]);
}
// List dengan filter + search
$where = ['1=1'];
$params = [];
if (!empty($_GET['status'])) {
$where[] = 'pm.status_bantuan = ?';
$params[] = $_GET['status'];
}
if (!empty($_GET['kecamatan'])) {
$where[] = 'pm.kecamatan = ?';
$params[] = $_GET['kecamatan'];
}
if (!empty($_GET['kelurahan'])) {
$where[] = 'pm.kelurahan = ?';
$params[] = $_GET['kelurahan'];
}
if (!empty($_GET['ibadah_id'])) {
$where[] = 'pm.rumah_ibadah_id = ?';
$params[] = (int)$_GET['ibadah_id'];
}
if (!empty($_GET['search'])) {
$where[] = '(pm.kk_nama LIKE ? OR pm.alamat LIKE ? OR pm.nik LIKE ?)';
$q = '%' . $_GET['search'] . '%';
$params = array_merge($params, [$q, $q, $q]);
}
$sql = "SELECT pm.*,
ri.nama AS ibadah_nama,
ri.jenis AS ibadah_jenis
FROM penduduk_miskin pm
LEFT JOIN rumah_ibadah ri ON pm.rumah_ibadah_id = ri.id
WHERE " . implode(' AND ', $where) . "
ORDER BY pm.created_at DESC";
$stmt = $db->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
jsonResponse(['success' => true, 'data' => $rows, 'total' => count($rows)]);
}
// ── POST ───────────────────────────────────────────────────────
if ($method === 'POST') {
$isMultipart = str_contains($_SERVER['CONTENT_TYPE'] ?? '', 'multipart/form-data');
$body = $isMultipart ? $_POST : (json_decode(file_get_contents('php://input'), true) ?? []);
$required = ['kk_nama', 'lat', 'lng'];
foreach ($required as $f) {
if (empty($body[$f])) jsonResponse(['success' => false, 'message' => "Field '$f' wajib diisi"], 422);
}
$lat = (float)$body['lat'];
$lng = (float)$body['lng'];
// Auto-cari rumah ibadah terdekat
$nearest = findNearestIbadah($lat, $lng);
$ibadahId = $nearest ? (int)$nearest['id'] : null;
$jarakIbadah = $nearest ? (float)$nearest['jarak'] : null;
// Handle upload bukti
$buktiFile = null;
if (!empty($_FILES['bukti_file']) && $_FILES['bukti_file']['error'] === UPLOAD_ERR_OK) {
$buktiFile = handleUpload($_FILES['bukti_file'], 'bukti');
}
$stmt = $db->prepare("
INSERT INTO penduduk_miskin
(kk_nama, nik, jumlah_anggota, alamat, kelurahan, kecamatan, lat, lng,
rumah_ibadah_id, jarak_ke_ibadah, status_bantuan, jenis_bantuan,
tanggal_bantuan, nominal_bantuan, bukti_file)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
");
$stmt->execute([
sanitize($body['kk_nama']),
sanitize($body['nik'] ?? ''),
(int)($body['jumlah_anggota'] ?? 1),
sanitize($body['alamat'] ?? ''),
sanitize($body['kelurahan'] ?? ''),
sanitize($body['kecamatan'] ?? ''),
$lat, $lng,
$ibadahId,
$jarakIbadah,
$body['status_bantuan'] ?? 'belum',
sanitize($body['jenis_bantuan'] ?? ''),
!empty($body['tanggal_bantuan']) ? $body['tanggal_bantuan'] : null,
!empty($body['nominal_bantuan']) ? (float)str_replace(['.', ','], ['', '.'], $body['nominal_bantuan']) : null,
$buktiFile,
]);
$newId = $db->lastInsertId();
// Simpan anggota keluarga jika ada (anggota bisa berupa JSON string dari FormData)
$anggotaData = $body['anggota'] ?? null;
if (is_string($anggotaData)) $anggotaData = json_decode($anggotaData, true);
if (!empty($anggotaData) && is_array($anggotaData)) {
saveAnggota($db, $newId, $anggotaData);
}
// Fetch full data
$new = $db->query("
SELECT pm.*, ri.nama AS ibadah_nama, ri.jenis AS ibadah_jenis
FROM penduduk_miskin pm
LEFT JOIN rumah_ibadah ri ON pm.rumah_ibadah_id = ri.id
WHERE pm.id = $newId
")->fetch();
jsonResponse(['success' => true, 'message' => 'Data penduduk berhasil ditambahkan', 'data' => $new], 201);
}
// ── PUT ────────────────────────────────────────────────────────
if ($method === 'PUT') {
if (!$id) jsonResponse(['success' => false, 'message' => 'ID wajib disertakan'], 400);
// Cek apakah multipart (ada file) atau JSON
$isMultipart = str_contains($_SERVER['CONTENT_TYPE'] ?? '', 'multipart/form-data');
$body = $isMultipart ? $_POST : (json_decode(file_get_contents('php://input'), true) ?? []);
$current = $db->prepare("SELECT * FROM penduduk_miskin WHERE id = ?");
$current->execute([$id]);
$existing = $current->fetch();
if (!$existing) jsonResponse(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
$lat = (float)($body['lat'] ?? $existing['lat']);
$lng = (float)($body['lng'] ?? $existing['lng']);
// Re-assign ibadah terdekat jika koordinat berubah
$coordChanged = ($lat != (float)$existing['lat'] || $lng != (float)$existing['lng']);
if ($coordChanged || isset($body['force_reassign'])) {
$nearest = findNearestIbadah($lat, $lng);
$ibadahId = $nearest ? (int)$nearest['id'] : null;
$jarakIbadah = $nearest ? (float)$nearest['jarak'] : null;
} else {
$ibadahId = (int)($body['rumah_ibadah_id'] ?? $existing['rumah_ibadah_id']);
$jarakIbadah = (float)($existing['jarak_ke_ibadah'] ?? 0);
}
// Handle upload bukti baru
$buktiFile = $existing['bukti_file'];
if (!empty($_FILES['bukti_file']) && $_FILES['bukti_file']['error'] === UPLOAD_ERR_OK) {
$newFile = handleUpload($_FILES['bukti_file'], 'bukti');
if ($newFile) $buktiFile = $newFile;
}
$status_bantuan = $body['status_bantuan'] ?? $existing['status_bantuan'];
if ($status_bantuan === 'belum') {
$jenis_bantuan = null;
$tanggal_bantuan = null;
$nominal_bantuan = null;
} else {
$jenis_bantuan = isset($body['jenis_bantuan']) ? sanitize($body['jenis_bantuan']) : $existing['jenis_bantuan'];
$tanggal_bantuan = isset($body['tanggal_bantuan']) ? (!empty($body['tanggal_bantuan']) ? $body['tanggal_bantuan'] : null) : $existing['tanggal_bantuan'];
if (isset($body['nominal_bantuan'])) {
$nominal_bantuan = $body['nominal_bantuan'] !== '' ? (float)str_replace(['.', ','], ['', '.'], $body['nominal_bantuan']) : null;
} else {
$nominal_bantuan = $existing['nominal_bantuan'];
}
}
$stmt = $db->prepare("
UPDATE penduduk_miskin
SET kk_nama=?, nik=?, jumlah_anggota=?, alamat=?, kelurahan=?, kecamatan=?,
lat=?, lng=?, rumah_ibadah_id=?, jarak_ke_ibadah=?,
status_bantuan=?, jenis_bantuan=?, tanggal_bantuan=?, nominal_bantuan=?, bukti_file=?
WHERE id=?
");
$stmt->execute([
sanitize($body['kk_nama'] ?? $existing['kk_nama']),
sanitize($body['nik'] ?? $existing['nik'] ?? ''),
(int)($body['jumlah_anggota'] ?? $existing['jumlah_anggota']),
sanitize($body['alamat'] ?? $existing['alamat'] ?? ''),
sanitize($body['kelurahan'] ?? $existing['kelurahan'] ?? ''),
sanitize($body['kecamatan'] ?? $existing['kecamatan'] ?? ''),
$lat, $lng,
$ibadahId, $jarakIbadah,
$status_bantuan,
$jenis_bantuan,
$tanggal_bantuan,
$nominal_bantuan,
$buktiFile,
$id,
]);
// Update anggota keluarga (anggota bisa berupa JSON string dari FormData)
if (isset($body['anggota'])) {
$anggotaDataPut = $body['anggota'];
if (is_string($anggotaDataPut)) $anggotaDataPut = json_decode($anggotaDataPut, true);
if (is_array($anggotaDataPut)) {
$db->prepare("DELETE FROM anggota_keluarga WHERE penduduk_id = ?")->execute([$id]);
saveAnggota($db, $id, $anggotaDataPut);
}
}
$updated = $db->query("
SELECT pm.*, ri.nama AS ibadah_nama, ri.jenis AS ibadah_jenis
FROM penduduk_miskin pm
LEFT JOIN rumah_ibadah ri ON pm.rumah_ibadah_id = ri.id
WHERE pm.id = $id
")->fetch();
jsonResponse(['success' => true, 'message' => 'Data berhasil diperbarui', 'data' => $updated]);
}
// ── DELETE ─────────────────────────────────────────────────────
if ($method === 'DELETE') {
if (!$id) jsonResponse(['success' => false, 'message' => 'ID wajib disertakan'], 400);
$stmt = $db->prepare("DELETE FROM penduduk_miskin WHERE id = ?");
$stmt->execute([$id]);
if ($stmt->rowCount() === 0) jsonResponse(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
jsonResponse(['success' => true, 'message' => 'Data berhasil dihapus']);
}
// ── Helper ─────────────────────────────────────────────────────
function saveAnggota(PDO $db, int $pendudukId, array $list): void {
$stmt = $db->prepare("
INSERT INTO anggota_keluarga (penduduk_id, nama, hubungan, umur, pekerjaan, keterangan)
VALUES (?,?,?,?,?,?)
");
foreach ($list as $a) {
if (empty($a['nama'])) continue;
$stmt->execute([
$pendudukId,
sanitize($a['nama'] ?? ''),
sanitize($a['hubungan'] ?? ''),
!empty($a['umur']) ? (int)$a['umur'] : null,
sanitize($a['pekerjaan'] ?? ''),
sanitize($a['keterangan'] ?? ''),
]);
}
}
jsonResponse(['success' => false, 'message' => 'Method tidak didukung'], 405);
+148
View File
@@ -0,0 +1,148 @@
<?php
// ================================================================
// API: Rumah Ibadah
// Methods: GET, POST, PUT, DELETE
// ================================================================
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../includes/functions.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') {
http_response_code(204);
exit;
}
$db = getDB();
$method = $_SERVER['REQUEST_METHOD'];
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
// ── GET ────────────────────────────────────────────────────────
if ($method === 'GET') {
if ($id) {
// Single record
$stmt = $db->prepare("
SELECT ri.*,
(SELECT COUNT(*) FROM penduduk_miskin pm WHERE pm.rumah_ibadah_id = ri.id) AS jumlah_binaan
FROM rumah_ibadah ri WHERE ri.id = ?
");
$stmt->execute([$id]);
$row = $stmt->fetch();
if (!$row) jsonResponse(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
jsonResponse(['success' => true, 'data' => $row]);
}
// List dengan filter
$where = ['1=1'];
$params = [];
if (!empty($_GET['jenis'])) {
$where[] = 'jenis = ?';
$params[] = $_GET['jenis'];
}
if (!empty($_GET['kecamatan'])) {
$where[] = 'kecamatan = ?';
$params[] = $_GET['kecamatan'];
}
if (!empty($_GET['search'])) {
$where[] = 'nama LIKE ?';
$params[] = '%' . $_GET['search'] . '%';
}
$sql = "SELECT ri.*,
(SELECT COUNT(*) FROM penduduk_miskin pm WHERE pm.rumah_ibadah_id = ri.id) AS jumlah_binaan
FROM rumah_ibadah ri
WHERE " . implode(' AND ', $where) . "
ORDER BY ri.nama ASC";
$stmt = $db->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
jsonResponse(['success' => true, 'data' => $rows, 'total' => count($rows)]);
}
// ── POST ───────────────────────────────────────────────────────
if ($method === 'POST') {
$body = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$required = ['nama', 'jenis', 'lat', 'lng'];
foreach ($required as $f) {
if (empty($body[$f])) jsonResponse(['success' => false, 'message' => "Field '$f' wajib diisi"], 422);
}
$stmt = $db->prepare("
INSERT INTO rumah_ibadah (nama, jenis, kontak, alamat, kelurahan, kecamatan, lat, lng, radius)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
sanitize($body['nama']),
$body['jenis'],
sanitize($body['kontak'] ?? ''),
sanitize($body['alamat'] ?? ''),
sanitize($body['kelurahan'] ?? ''),
sanitize($body['kecamatan'] ?? ''),
(float)$body['lat'],
(float)$body['lng'],
(int)($body['radius'] ?? RADIUS_DEFAULT),
]);
$newId = $db->lastInsertId();
$new = $db->query("SELECT * FROM rumah_ibadah WHERE id = $newId")->fetch();
jsonResponse(['success' => true, 'message' => 'Rumah ibadah berhasil ditambahkan', 'data' => $new], 201);
}
// ── PUT ────────────────────────────────────────────────────────
if ($method === 'PUT') {
if (!$id) jsonResponse(['success' => false, 'message' => 'ID wajib disertakan'], 400);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$stmt = $db->prepare("
UPDATE rumah_ibadah
SET nama=?, jenis=?, kontak=?, alamat=?, kelurahan=?, kecamatan=?, lat=?, lng=?, radius=?
WHERE id=?
");
$stmt->execute([
sanitize($body['nama'] ?? ''),
$body['jenis'] ?? 'Masjid',
sanitize($body['kontak'] ?? ''),
sanitize($body['alamat'] ?? ''),
sanitize($body['kelurahan'] ?? ''),
sanitize($body['kecamatan'] ?? ''),
(float)($body['lat'] ?? 0),
(float)($body['lng'] ?? 0),
(int)($body['radius'] ?? RADIUS_DEFAULT),
$id,
]);
// Re-assign penduduk yang terhubung jika radius berubah
reassignPendudukAfterIbadahUpdate();
$updated = $db->query("SELECT * FROM rumah_ibadah WHERE id = $id")->fetch();
jsonResponse(['success' => true, 'message' => 'Data berhasil diperbarui', 'data' => $updated]);
}
// ── DELETE ─────────────────────────────────────────────────────
if ($method === 'DELETE') {
if (!$id) jsonResponse(['success' => false, 'message' => 'ID wajib disertakan'], 400);
$stmt = $db->prepare("DELETE FROM rumah_ibadah WHERE id = ?");
$stmt->execute([$id]);
if ($stmt->rowCount() === 0) jsonResponse(['success' => false, 'message' => 'Data tidak ditemukan'], 404);
jsonResponse(['success' => true, 'message' => 'Data berhasil dihapus']);
}
function reassignPendudukAfterIbadahUpdate(): void {
$db = getDB();
$penduduk = $db->query("SELECT id, lat, lng FROM penduduk_miskin")->fetchAll();
foreach ($penduduk as $p) {
$nearest = findNearestIbadah((float)$p['lat'], (float)$p['lng']);
if ($nearest) {
$stmt = $db->prepare("UPDATE penduduk_miskin SET rumah_ibadah_id=?, jarak_ke_ibadah=? WHERE id=?");
$stmt->execute([$nearest['id'], $nearest['jarak'], $p['id']]);
}
}
}
jsonResponse(['success' => false, 'message' => 'Method tidak didukung'], 405);
+63
View File
@@ -0,0 +1,63 @@
<?php
// ================================================================
// API: Statistics
// ================================================================
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../includes/functions.php';
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
$db = getDB();
// Total counts
$totalIbadah = $db->query("SELECT COUNT(*) FROM rumah_ibadah")->fetchColumn();
$totalMiskin = $db->query("SELECT COUNT(*) FROM penduduk_miskin")->fetchColumn();
$sudahDibantu = $db->query("SELECT COUNT(*) FROM penduduk_miskin WHERE status_bantuan='sudah'")->fetchColumn();
$belumDibantu = $db->query("SELECT COUNT(*) FROM penduduk_miskin WHERE status_bantuan='belum'")->fetchColumn();
$menunggu = $db->query("SELECT COUNT(*) FROM penduduk_miskin WHERE status_bantuan='menunggu'")->fetchColumn();
$totalNominal = $db->query("SELECT COALESCE(SUM(nominal_bantuan),0) FROM penduduk_miskin WHERE status_bantuan='sudah' AND nominal_bantuan IS NOT NULL")->fetchColumn();
$totalNominalAll = $db->query("SELECT COALESCE(SUM(nominal_bantuan),0) FROM penduduk_miskin WHERE nominal_bantuan IS NOT NULL")->fetchColumn();
// Per jenis ibadah
$jenisStats = $db->query("
SELECT jenis, COUNT(*) as total,
(SELECT COUNT(*) FROM penduduk_miskin pm WHERE pm.rumah_ibadah_id = ri.id) AS binaan
FROM rumah_ibadah ri
GROUP BY jenis ORDER BY total DESC
")->fetchAll();
// Per kecamatan (top 5)
$kecamatanStats = $db->query("
SELECT kecamatan, COUNT(*) as total,
SUM(CASE WHEN status_bantuan='sudah' THEN 1 ELSE 0 END) AS sudah,
SUM(CASE WHEN status_bantuan='belum' THEN 1 ELSE 0 END) AS belum
FROM penduduk_miskin
WHERE kecamatan != '' AND kecamatan IS NOT NULL
GROUP BY kecamatan ORDER BY total DESC LIMIT 5
")->fetchAll();
// Data terbaru (5 data)
$recentMiskin = $db->query("
SELECT pm.id, pm.kk_nama, pm.status_bantuan, pm.created_at,
ri.nama AS ibadah_nama
FROM penduduk_miskin pm
LEFT JOIN rumah_ibadah ri ON pm.rumah_ibadah_id = ri.id
ORDER BY pm.created_at DESC LIMIT 5
")->fetchAll();
jsonResponse([
'success' => true,
'data' => [
'total_ibadah' => (int)$totalIbadah,
'total_miskin' => (int)$totalMiskin,
'sudah_dibantu' => (int)$sudahDibantu,
'belum_dibantu' => (int)$belumDibantu,
'menunggu' => (int)$menunggu,
'total_nominal' => (float)$totalNominal,
'total_nominal_all' => (float)$totalNominalAll,
'jenis_stats' => $jenisStats,
'kecamatan_stats' => $kecamatanStats,
'recent_miskin' => $recentMiskin,
]
]);
+31
View File
@@ -0,0 +1,31 @@
<?php
// ================================================================
// API: File Upload
// ================================================================
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../includes/functions.php';
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['success' => false, 'message' => 'Method harus POST'], 405);
}
if (empty($_FILES['file'])) {
jsonResponse(['success' => false, 'message' => 'Tidak ada file yang diupload'], 400);
}
$file = $_FILES['file'];
$prefix = sanitize($_POST['prefix'] ?? 'upload');
$filename = handleUpload($file, $prefix);
if (!$filename) {
jsonResponse(['success' => false, 'message' => 'Upload gagal. Pastikan tipe file dan ukuran sesuai (maks 10MB, tipe: jpg/jpeg/png/gif/pdf/doc/docx)'], 422);
}
jsonResponse([
'success' => true,
'filename' => $filename,
'url' => UPLOAD_URL . $filename,
'message' => 'File berhasil diupload',
]);
+132
View File
@@ -0,0 +1,132 @@
<?php
// ================================================================
// API: User Management (CRUD)
// Hanya bisa diakses oleh admin
// ================================================================
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/functions.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') { http_response_code(204); exit; }
// Auth check — hanya admin
session_name(SESSION_NAME);
session_start();
$sessionRole = $_SESSION['user']['role'] ?? $_SESSION['user_role'] ?? null;
$sessionUserId = $_SESSION['user']['id'] ?? $_SESSION['user_id'] ?? null;
if (!$sessionUserId || $sessionRole !== 'admin') {
jsonResponse(['success' => false, 'message' => 'Akses ditolak: hanya admin'], 403);
}
$db = getDB();
$method = $_SERVER['REQUEST_METHOD'];
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
// ── GET ────────────────────────────────────────────────────────
if ($method === 'GET') {
if ($id) {
$stmt = $db->prepare("SELECT id, nama, email, role, created_at FROM users WHERE id = ?");
$stmt->execute([$id]);
$row = $stmt->fetch();
if (!$row) jsonResponse(['success' => false, 'message' => 'User tidak ditemukan'], 404);
jsonResponse(['success' => true, 'data' => $row]);
}
$rows = $db->query("SELECT id, nama, email, role, created_at FROM users ORDER BY role DESC, nama")->fetchAll();
jsonResponse(['success' => true, 'data' => $rows, 'total' => count($rows)]);
}
// ── POST (Tambah user baru) ────────────────────────────────────
if ($method === 'POST') {
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$nama = trim($body['nama'] ?? '');
$email = trim($body['email'] ?? '');
$pwd = $body['password'] ?? '';
$role = in_array($body['role'] ?? '', ['admin','operator']) ? $body['role'] : 'operator';
if (!$nama || !$email || !$pwd) {
jsonResponse(['success' => false, 'message' => 'Nama, email, dan password wajib diisi'], 422);
}
if (strlen($pwd) < 6) {
jsonResponse(['success' => false, 'message' => 'Password minimal 6 karakter'], 422);
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
jsonResponse(['success' => false, 'message' => 'Format email tidak valid'], 422);
}
// Cek duplikat email
$check = $db->prepare("SELECT id FROM users WHERE email = ?");
$check->execute([$email]);
if ($check->fetch()) {
jsonResponse(['success' => false, 'message' => 'Email sudah terdaftar'], 409);
}
$hash = password_hash($pwd, PASSWORD_BCRYPT);
$stmt = $db->prepare("INSERT INTO users (nama, email, password, role) VALUES (?, ?, ?, ?)");
$stmt->execute([$nama, $email, $hash, $role]);
$newId = $db->lastInsertId();
$new = $db->query("SELECT id, nama, email, role, created_at FROM users WHERE id = $newId")->fetch();
jsonResponse(['success' => true, 'message' => 'Pengguna berhasil ditambahkan', 'data' => $new], 201);
}
// ── PUT (Edit user) ────────────────────────────────────────────
if ($method === 'PUT') {
if (!$id) jsonResponse(['success' => false, 'message' => 'ID wajib disertakan'], 400);
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$existing = $db->prepare("SELECT * FROM users WHERE id = ?");
$existing->execute([$id]);
$user = $existing->fetch();
if (!$user) jsonResponse(['success' => false, 'message' => 'User tidak ditemukan'], 404);
$nama = trim($body['nama'] ?? $user['nama']);
$email = trim($body['email'] ?? $user['email']);
$role = in_array($body['role'] ?? '', ['admin','operator']) ? $body['role'] : $user['role'];
$pwd = $body['password'] ?? '';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
jsonResponse(['success' => false, 'message' => 'Format email tidak valid'], 422);
}
// Cek duplikat email (kecuali user ini sendiri)
$checkEmail = $db->prepare("SELECT id FROM users WHERE email = ? AND id != ?");
$checkEmail->execute([$email, $id]);
if ($checkEmail->fetch()) {
jsonResponse(['success' => false, 'message' => 'Email sudah digunakan oleh user lain'], 409);
}
if ($pwd) {
if (strlen($pwd) < 6) jsonResponse(['success' => false, 'message' => 'Password minimal 6 karakter'], 422);
$hash = password_hash($pwd, PASSWORD_BCRYPT);
$stmt = $db->prepare("UPDATE users SET nama=?, email=?, role=?, password=? WHERE id=?");
$stmt->execute([$nama, $email, $role, $hash, $id]);
} else {
$stmt = $db->prepare("UPDATE users SET nama=?, email=?, role=? WHERE id=?");
$stmt->execute([$nama, $email, $role, $id]);
}
$updated = $db->query("SELECT id, nama, email, role, created_at FROM users WHERE id = $id")->fetch();
jsonResponse(['success' => true, 'message' => 'Data pengguna berhasil diperbarui', 'data' => $updated]);
}
// ── DELETE ─────────────────────────────────────────────────────
if ($method === 'DELETE') {
if (!$id) jsonResponse(['success' => false, 'message' => 'ID wajib disertakan'], 400);
// Tidak boleh hapus diri sendiri
if ($id == $sessionUserId) {
jsonResponse(['success' => false, 'message' => 'Tidak bisa menghapus akun sendiri'], 403);
}
$stmt = $db->prepare("DELETE FROM users WHERE id = ?");
$stmt->execute([$id]);
if ($stmt->rowCount() === 0) jsonResponse(['success' => false, 'message' => 'User tidak ditemukan'], 404);
jsonResponse(['success' => true, 'message' => 'Pengguna berhasil dihapus']);
}
jsonResponse(['success' => false, 'message' => 'Method tidak didukung'], 405);