Fix: Convert 03PovertyMapping from submodule to regular folder
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/aid/index.php — Aid History (requireAuth on all)
|
||||
// ============================================================
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../../config/bootstrap.php';
|
||||
|
||||
requireAuth();
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
switch ("$method:$action") {
|
||||
|
||||
case 'GET:list':
|
||||
case 'GET:': {
|
||||
$pdo = Database::get();
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
|
||||
if (!empty($_GET['household_id'])) { $where[] = 'ah.household_id = ?'; $params[] = (int)$_GET['household_id']; }
|
||||
if (!empty($_GET['center_id'])) { $where[] = 'ah.center_id = ?'; $params[] = (int)$_GET['center_id']; }
|
||||
if (!empty($_GET['aid_type'])) { $where[] = 'ah.aid_type = ?'; $params[] = $_GET['aid_type']; }
|
||||
if (!empty($_GET['from'])) { $where[] = 'ah.aid_date >= ?'; $params[] = $_GET['from']; }
|
||||
if (!empty($_GET['to'])) { $where[] = 'ah.aid_date <= ?'; $params[] = $_GET['to']; }
|
||||
|
||||
$whereSQL = implode(' AND ', $where);
|
||||
$limit = min((int)($_GET['limit'] ?? 100), 500);
|
||||
$offset = max(0, (int)($_GET['offset'] ?? 0));
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT ah.*, h.head_name, h.poverty_status, rc.name AS center_name
|
||||
FROM aid_history ah
|
||||
LEFT JOIN households h ON h.id = ah.household_id
|
||||
LEFT JOIN religious_centers rc ON rc.id = ah.center_id
|
||||
WHERE $whereSQL
|
||||
ORDER BY ah.aid_date DESC, ah.created_at DESC
|
||||
LIMIT $limit OFFSET $offset
|
||||
");
|
||||
$stmt->execute($params);
|
||||
|
||||
$cntStmt = $pdo->prepare("SELECT COUNT(*) FROM aid_history ah WHERE $whereSQL");
|
||||
$cntStmt->execute($params);
|
||||
|
||||
Response::success(['aid_history' => $stmt->fetchAll(), 'total' => (int)$cntStmt->fetchColumn()]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'GET:show': {
|
||||
if (!$id) Response::error('ID is required.', 400);
|
||||
$pdo = Database::get();
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT ah.*, h.head_name, rc.name AS center_name
|
||||
FROM aid_history ah
|
||||
LEFT JOIN households h ON h.id = ah.household_id
|
||||
LEFT JOIN religious_centers rc ON rc.id = ah.center_id
|
||||
WHERE ah.id = ?
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) Response::notFound('Aid record not found.');
|
||||
Response::success($row);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'POST:create': {
|
||||
$data = Validator::json();
|
||||
Validator::make($data, [
|
||||
'household_id' => 'required|integer',
|
||||
'aid_type' => 'required|in:sembako,pendanaan,pelatihan,sembako_pendanaan,sembako_pelatihan,pendanaan_pelatihan,lengkap',
|
||||
'aid_date' => 'required|date',
|
||||
'amount' => 'integer|min:0',
|
||||
])->validate_or_fail();
|
||||
|
||||
$pdo = Database::get();
|
||||
|
||||
// Check if household exists
|
||||
$hh = $pdo->prepare('SELECT id FROM households WHERE id = ? AND is_active = 1');
|
||||
$hh->execute([(int)$data['household_id']]);
|
||||
if (!$hh->fetch()) Response::notFound('Household not found.');
|
||||
|
||||
// Insert menggunakan kolom description (bukan notes)
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO aid_history
|
||||
(household_id, center_id, aid_type, aid_date, amount, description, given_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([
|
||||
(int)$data['household_id'],
|
||||
!empty($data['center_id']) ? (int)$data['center_id'] : null,
|
||||
$data['aid_type'],
|
||||
$data['aid_date'],
|
||||
!empty($data['amount']) ? (int)$data['amount'] : null,
|
||||
!empty($data['notes']) ? Validator::sanitizeString($data['notes']) : null,
|
||||
(int)($_SESSION['user_id'] ?? 1) // current user ID
|
||||
]);
|
||||
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
|
||||
// Update household aid_status to 'received'
|
||||
$pdo->prepare("UPDATE households SET aid_status = 'received' WHERE id = ?")
|
||||
->execute([(int)$data['household_id']]);
|
||||
|
||||
AuditLog::record('Catat Bantuan', 'aid_history', $newId, null, $data);
|
||||
Response::created(['id' => $newId], 'Bantuan berhasil dicatat.');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'POST:update': {
|
||||
if (!$id) Response::error('ID is required.', 400);
|
||||
|
||||
$data = Validator::json();
|
||||
Validator::make($data, [
|
||||
'aid_type' => 'required|in:sembako,pendanaan,pelatihan,sembako_pendanaan,sembako_pelatihan,pendanaan_pelatihan,lengkap',
|
||||
'aid_date' => 'required|date',
|
||||
'amount' => 'integer|min:0',
|
||||
])->validate_or_fail();
|
||||
|
||||
$pdo = Database::get();
|
||||
|
||||
// Get old data for audit
|
||||
$old = $pdo->prepare('SELECT * FROM aid_history WHERE id = ?');
|
||||
$old->execute([$id]);
|
||||
$oldRow = $old->fetch();
|
||||
if (!$oldRow) Response::notFound('Aid record not found.');
|
||||
|
||||
// Update menggunakan kolom description
|
||||
$pdo->prepare("
|
||||
UPDATE aid_history SET
|
||||
aid_type = ?, aid_date = ?, amount = ?, description = ?, center_id = ?
|
||||
WHERE id = ?
|
||||
")->execute([
|
||||
$data['aid_type'],
|
||||
$data['aid_date'],
|
||||
!empty($data['amount']) ? (int)$data['amount'] : null,
|
||||
!empty($data['notes']) ? Validator::sanitizeString($data['notes']) : null,
|
||||
!empty($data['center_id']) ? (int)$data['center_id'] : null,
|
||||
$id,
|
||||
]);
|
||||
|
||||
AuditLog::record('Update Bantuan', 'aid_history', $id, $oldRow, $data);
|
||||
Response::success(null, 'Bantuan diperbarui.');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'POST:delete': {
|
||||
requireAdmin(); // only admin can delete aid records
|
||||
|
||||
if (!$id) Response::error('ID is required.', 400);
|
||||
|
||||
$pdo = Database::get();
|
||||
|
||||
// Get old data for audit
|
||||
$old = $pdo->prepare('SELECT * FROM aid_history WHERE id = ?');
|
||||
$old->execute([$id]);
|
||||
$row = $old->fetch();
|
||||
if (!$row) Response::notFound('Aid record not found.');
|
||||
|
||||
// Delete record
|
||||
$pdo->prepare('DELETE FROM aid_history WHERE id = ?')->execute([$id]);
|
||||
|
||||
// Check if household still has any aid records
|
||||
$checkStmt = $pdo->prepare('SELECT COUNT(*) FROM aid_history WHERE household_id = ?');
|
||||
$checkStmt->execute([$row['household_id']]);
|
||||
$remaining = (int)$checkStmt->fetchColumn();
|
||||
|
||||
// If no more aid records, update aid_status back to 'not_yet'
|
||||
if ($remaining === 0) {
|
||||
$pdo->prepare("UPDATE households SET aid_status = 'not_yet' WHERE id = ?")
|
||||
->execute([$row['household_id']]);
|
||||
}
|
||||
|
||||
AuditLog::record('Hapus Bantuan', 'aid_history', $id, $row);
|
||||
Response::success(null, 'Bantuan dihapus.');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'GET:stats': {
|
||||
$pdo = Database::get();
|
||||
|
||||
$byType = $pdo->query("
|
||||
SELECT aid_type, COUNT(*) AS cnt, COALESCE(SUM(amount), 0) AS total_amount
|
||||
FROM aid_history
|
||||
GROUP BY aid_type
|
||||
ORDER BY cnt DESC
|
||||
")->fetchAll();
|
||||
|
||||
$monthly = $pdo->query("
|
||||
SELECT
|
||||
DATE_FORMAT(aid_date, '%Y-%m') AS month,
|
||||
COUNT(*) AS distributions,
|
||||
COALESCE(SUM(amount), 0) AS total_amount
|
||||
FROM aid_history
|
||||
GROUP BY month
|
||||
ORDER BY month DESC
|
||||
LIMIT 12
|
||||
")->fetchAll();
|
||||
|
||||
$total = (int)$pdo->query("SELECT COUNT(DISTINCT household_id) FROM aid_history")->fetchColumn();
|
||||
$totalDist = (int)$pdo->query("SELECT COUNT(*) FROM aid_history")->fetchColumn();
|
||||
|
||||
Response::success([
|
||||
'by_type' => $byType,
|
||||
'monthly' => array_reverse($monthly),
|
||||
'summary' => [
|
||||
'total_distributions' => $totalDist,
|
||||
'total_households_aided' => $total
|
||||
],
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Response::methodNotAllowed();
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/auth/check.php — Session check & login/logout
|
||||
//
|
||||
// GET /api/auth/check.php → check session
|
||||
// POST /api/auth/check.php?action=login → login
|
||||
// POST /api/auth/check.php?action=logout→ logout
|
||||
// ============================================================
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../../config/bootstrap.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'CLI';
|
||||
$action = $_GET['action'] ?? 'check';
|
||||
|
||||
// ================================================================
|
||||
// CHECK — return current session state
|
||||
// ================================================================
|
||||
if ($method === 'GET' || $action === 'check') {
|
||||
if (!empty($_SESSION['user_id']) && !empty($_SESSION['role'])) {
|
||||
Response::success([
|
||||
'logged_in' => true,
|
||||
'user_id' => (int)$_SESSION['user_id'],
|
||||
'name' => $_SESSION['name'] ?? '',
|
||||
'email' => $_SESSION['email'] ?? '',
|
||||
'role' => $_SESSION['role'],
|
||||
]);
|
||||
} else {
|
||||
Response::success(['logged_in' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// LOGIN
|
||||
// ================================================================
|
||||
if ($method === 'POST' && $action === 'login') {
|
||||
$data = Validator::json();
|
||||
$v = Validator::make($data, [
|
||||
'email' => 'required',
|
||||
'password' => 'required',
|
||||
]);
|
||||
$v->validate_or_fail();
|
||||
|
||||
$pdo = Database::get();
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT id, name, email, password_hash, role, is_active FROM users WHERE email = ? LIMIT 1'
|
||||
);
|
||||
$stmt->execute([strtolower(trim($data['email']))]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user || !$user['is_active']) {
|
||||
AuditLog::record('auth.login_failed', 'users', null, null, ['email' => $data['email']]);
|
||||
Response::error('Username atau password salah.', 401);
|
||||
}
|
||||
|
||||
if (!password_verify($data['password'], $user['password_hash'])) {
|
||||
AuditLog::record('auth.login_failed', 'users', null, null, ['email' => $data['email']]);
|
||||
Response::error('Username atau password salah.', 401);
|
||||
}
|
||||
|
||||
// Regenerate session ID to prevent fixation
|
||||
session_regenerate_id(true);
|
||||
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['name'] = $user['name'];
|
||||
$_SESSION['email'] = $user['email'];
|
||||
$_SESSION['role'] = $user['role'];
|
||||
|
||||
// Update last_login_at
|
||||
try {
|
||||
$pdo->prepare('UPDATE users SET last_login_at = NOW() WHERE id = ?')
|
||||
->execute([$user['id']]);
|
||||
} catch (\Throwable) {}
|
||||
|
||||
AuditLog::record('auth.login', 'users', (int)$user['id']);
|
||||
|
||||
Response::success([
|
||||
'user_id' => (int)$user['id'],
|
||||
'name' => $user['name'],
|
||||
'email' => $user['email'],
|
||||
'role' => $user['role'],
|
||||
], 'Login berhasil.');
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// LOGOUT
|
||||
// ================================================================
|
||||
if ($method === 'POST' && $action === 'logout') {
|
||||
$userId = $_SESSION['user_id'] ?? null;
|
||||
AuditLog::record('auth.logout', 'users', $userId ? (int)$userId : null);
|
||||
$_SESSION = [];
|
||||
session_destroy();
|
||||
Response::success(null, 'Logout berhasil.');
|
||||
}
|
||||
|
||||
Response::methodNotAllowed();
|
||||
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
// api/centers/index.php - FINAL VERSION
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../../config/bootstrap.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
// sendJSON function removed in favor of Response::success and Response::error
|
||||
|
||||
try {
|
||||
switch ("$method:$action") {
|
||||
|
||||
case 'GET:list':
|
||||
case 'GET:': {
|
||||
$pdo = Database::get();
|
||||
|
||||
$where = ['rc.is_active = 1'];
|
||||
$params = [];
|
||||
|
||||
if (!empty($_GET['type'])) {
|
||||
$allowed = ['masjid','gereja','klenteng','pura','vihara'];
|
||||
if (in_array($_GET['type'], $allowed, true)) {
|
||||
$where[] = 'rc.worship_type = ?';
|
||||
$params[] = $_GET['type'];
|
||||
}
|
||||
}
|
||||
if (!empty($_GET['q'])) {
|
||||
$where[] = '(rc.name LIKE ? OR rc.address LIKE ?)';
|
||||
$q = '%' . $_GET['q'] . '%';
|
||||
$params[] = $q; $params[] = $q;
|
||||
}
|
||||
|
||||
$whereSQL = implode(' AND ', $where);
|
||||
|
||||
// Query tanpa subquery yang bermasalah
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
rc.id, rc.name, rc.worship_type, rc.address,
|
||||
rc.kelurahan, rc.kecamatan,
|
||||
rc.latitude, rc.longitude, rc.radius,
|
||||
rc.contact_person, rc.contact_phone, rc.notes,
|
||||
rc.is_active, rc.created_at, rc.updated_at
|
||||
FROM religious_centers rc
|
||||
WHERE $whereSQL
|
||||
ORDER BY rc.name
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
// Hitung household_count dan pending_aid_count secara terpisah
|
||||
foreach ($rows as &$r) {
|
||||
$r['latitude'] = (float)$r['latitude'];
|
||||
$r['longitude'] = (float)$r['longitude'];
|
||||
$r['radius'] = (int)$r['radius'];
|
||||
|
||||
// Hitung jumlah rumah yang dikelola center ini
|
||||
$countStmt = $pdo->prepare("
|
||||
SELECT COUNT(*) FROM households
|
||||
WHERE managing_center_id = ? AND is_active = 1
|
||||
");
|
||||
$countStmt->execute([$r['id']]);
|
||||
$r['household_count'] = (int)$countStmt->fetchColumn();
|
||||
|
||||
// Hitung jumlah rumah yang belum terima bantuan
|
||||
$pendingStmt = $pdo->prepare("
|
||||
SELECT COUNT(*) FROM households
|
||||
WHERE managing_center_id = ? AND is_active = 1 AND aid_status = 'not_yet'
|
||||
");
|
||||
$pendingStmt->execute([$r['id']]);
|
||||
$r['pending_aid_count'] = (int)$pendingStmt->fetchColumn();
|
||||
}
|
||||
unset($r);
|
||||
|
||||
Response::success(['centers' => $rows, 'total' => count($rows)]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'GET:show': {
|
||||
if (!$id) Response::error('ID required', 400);
|
||||
|
||||
$pdo = Database::get();
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT * FROM religious_centers
|
||||
WHERE id = ? AND is_active = 1
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row) {
|
||||
Response::notFound('Not found');
|
||||
}
|
||||
|
||||
$row['latitude'] = (float)$row['latitude'];
|
||||
$row['longitude'] = (float)$row['longitude'];
|
||||
$row['radius'] = (int)$row['radius'];
|
||||
|
||||
Response::success($row);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'GET:nearby': {
|
||||
$lat = (float)($_GET['lat'] ?? 0);
|
||||
$lng = (float)($_GET['lng'] ?? 0);
|
||||
$km = min(50, (float)($_GET['km'] ?? 5));
|
||||
|
||||
if ($lat === 0.0 && $lng === 0.0) {
|
||||
Response::error('lat and lng required', 400);
|
||||
}
|
||||
|
||||
$pdo = Database::get();
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
id, name, worship_type, address,
|
||||
latitude, longitude, radius,
|
||||
(6371 * ACOS(
|
||||
COS(RADIANS(?)) * COS(RADIANS(latitude)) *
|
||||
COS(RADIANS(longitude) - RADIANS(?)) +
|
||||
SIN(RADIANS(?)) * SIN(RADIANS(latitude))
|
||||
)) AS distance_km
|
||||
FROM religious_centers
|
||||
WHERE is_active = 1
|
||||
HAVING distance_km <= ?
|
||||
ORDER BY distance_km
|
||||
LIMIT 10
|
||||
");
|
||||
$stmt->execute([$lat, $lng, $lat, $km]);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
foreach ($rows as &$r) {
|
||||
$r['latitude'] = (float)$r['latitude'];
|
||||
$r['longitude'] = (float)$r['longitude'];
|
||||
$r['radius'] = (int)$r['radius'];
|
||||
$r['distance_km'] = round((float)$r['distance_km'], 3);
|
||||
}
|
||||
unset($r);
|
||||
|
||||
Response::success(['centers' => $rows]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'GET:coverage': {
|
||||
if (!$id) Response::error('ID required', 400);
|
||||
|
||||
$pdo = Database::get();
|
||||
$center = $pdo->prepare("SELECT * FROM religious_centers WHERE id = ? AND is_active = 1");
|
||||
$center->execute([$id]);
|
||||
$c = $center->fetch();
|
||||
|
||||
if (!$c) {
|
||||
Response::notFound('Center not found');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
id, head_name, full_address, latitude, longitude,
|
||||
poverty_status, aid_status
|
||||
FROM households
|
||||
WHERE is_active = 1
|
||||
");
|
||||
$stmt->execute();
|
||||
$allHouseholds = $stmt->fetchAll();
|
||||
|
||||
$households = [];
|
||||
foreach ($allHouseholds as $h) {
|
||||
$distance = haversineDistance(
|
||||
(float)$c['latitude'], (float)$c['longitude'],
|
||||
(float)$h['latitude'], (float)$h['longitude']
|
||||
);
|
||||
if ($distance <= (float)$c['radius']) {
|
||||
$h['distance_m'] = round($distance, 1);
|
||||
$h['latitude'] = (float)$h['latitude'];
|
||||
$h['longitude'] = (float)$h['longitude'];
|
||||
$households[] = $h;
|
||||
}
|
||||
}
|
||||
|
||||
usort($households, function($a, $b) {
|
||||
return $a['distance_m'] <=> $b['distance_m'];
|
||||
});
|
||||
|
||||
Response::success([
|
||||
'center' => $c,
|
||||
'households' => $households,
|
||||
'count' => count($households)
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'POST:create': {
|
||||
requireAuth();
|
||||
$data = Validator::json();
|
||||
|
||||
$pdo = Database::get();
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO religious_centers
|
||||
(name, worship_type, address, latitude, longitude, radius,
|
||||
contact_person, contact_phone, notes)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$data['name'] ?? '',
|
||||
$data['worship_type'] ?? 'masjid',
|
||||
$data['address'] ?? '',
|
||||
(float)($data['latitude'] ?? 0),
|
||||
(float)($data['longitude'] ?? 0),
|
||||
(int)($data['radius'] ?? 300),
|
||||
$data['contact_person'] ?? null,
|
||||
$data['contact_phone'] ?? null,
|
||||
$data['notes'] ?? null
|
||||
]);
|
||||
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
AuditLog::record('Tambah Tempat Ibadah', 'religious_centers', $newId, null, $data);
|
||||
Response::created(['id' => $newId], 'Center created');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'POST:update': {
|
||||
requireAuth();
|
||||
if (!$id) Response::error('ID required', 400);
|
||||
|
||||
$data = Validator::json();
|
||||
|
||||
$pdo = Database::get();
|
||||
$old = $pdo->prepare("SELECT * FROM religious_centers WHERE id = ? AND is_active = 1");
|
||||
$old->execute([$id]);
|
||||
$oldRow = $old->fetch();
|
||||
|
||||
if (!$oldRow) {
|
||||
Response::notFound('Not found');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE religious_centers SET
|
||||
name = ?, worship_type = ?, address = ?,
|
||||
latitude = ?, longitude = ?, radius = ?,
|
||||
contact_person = ?, contact_phone = ?, notes = ?
|
||||
WHERE id = ? AND is_active = 1
|
||||
");
|
||||
$stmt->execute([
|
||||
$data['name'] ?? $oldRow['name'],
|
||||
$data['worship_type'] ?? $oldRow['worship_type'],
|
||||
$data['address'] ?? $oldRow['address'],
|
||||
(float)($data['latitude'] ?? $oldRow['latitude']),
|
||||
(float)($data['longitude'] ?? $oldRow['longitude']),
|
||||
(int)($data['radius'] ?? $oldRow['radius']),
|
||||
$data['contact_person'] ?? $oldRow['contact_person'],
|
||||
$data['contact_phone'] ?? $oldRow['contact_phone'],
|
||||
$data['notes'] ?? $oldRow['notes'],
|
||||
$id
|
||||
]);
|
||||
|
||||
AuditLog::record('Update Tempat Ibadah', 'religious_centers', $id, $oldRow, $data);
|
||||
Response::success(null, 'Updated');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'POST:patch': {
|
||||
requireAuth();
|
||||
if (!$id) Response::error('ID required', 400);
|
||||
|
||||
$data = Validator::json();
|
||||
$fields = [];
|
||||
$params = [];
|
||||
|
||||
if (isset($data['radius'])) {
|
||||
$fields[] = 'radius = ?';
|
||||
$params[] = (int)$data['radius'];
|
||||
}
|
||||
if (isset($data['latitude']) && isset($data['longitude'])) {
|
||||
$fields[] = 'latitude = ?';
|
||||
$params[] = (float)$data['latitude'];
|
||||
$fields[] = 'longitude = ?';
|
||||
$params[] = (float)$data['longitude'];
|
||||
if (isset($data['address'])) {
|
||||
$fields[] = 'address = ?';
|
||||
$params[] = $data['address'];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($fields)) {
|
||||
Response::error('No fields to update', 400);
|
||||
}
|
||||
|
||||
$params[] = $id;
|
||||
$sql = "UPDATE religious_centers SET " . implode(', ', $fields) . " WHERE id = ? AND is_active = 1";
|
||||
|
||||
$pdo = Database::get();
|
||||
$pdo->prepare($sql)->execute($params);
|
||||
|
||||
AuditLog::record('Geser/Resize Tempat Ibadah', 'religious_centers', $id, null, $data);
|
||||
Response::success(null, 'Patched');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'POST:delete': {
|
||||
requireAdmin();
|
||||
if (!$id) Response::error('ID required', 400);
|
||||
|
||||
$pdo = Database::get();
|
||||
$old = $pdo->prepare("SELECT * FROM religious_centers WHERE id = ? AND is_active = 1");
|
||||
$old->execute([$id]);
|
||||
$oldRow = $old->fetch();
|
||||
|
||||
if (!$oldRow) {
|
||||
Response::notFound('Not found');
|
||||
}
|
||||
|
||||
$pdo->prepare("UPDATE religious_centers SET is_active = 0 WHERE id = ?")->execute([$id]);
|
||||
AuditLog::record('Hapus Tempat Ibadah', 'religious_centers', $id, $oldRow);
|
||||
Response::success(null, 'Deleted');
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Response::methodNotAllowed();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$message = APP_DEBUG ? $e->getMessage() : 'Internal Server Error';
|
||||
Response::error($message, 500);
|
||||
}
|
||||
|
||||
function haversineDistance($lat1, $lon1, $lat2, $lon2) {
|
||||
$R = 6371000;
|
||||
$dLat = deg2rad($lat2 - $lat1);
|
||||
$dLon = deg2rad($lon2 - $lon1);
|
||||
$a = sin($dLat/2) * sin($dLat/2) +
|
||||
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
|
||||
sin($dLon/2) * sin($dLon/2);
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
|
||||
return $R * $c;
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/houses/index.php — Households CRUD (Adapted for new schema)
|
||||
// ============================================================
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../../config/bootstrap.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
switch ("$method:$action") {
|
||||
|
||||
// ================================================================
|
||||
// LIST — untuk map markers
|
||||
// ================================================================
|
||||
case 'GET:list':
|
||||
case 'GET:': {
|
||||
requireAuth();
|
||||
$pdo = Database::get();
|
||||
$where = ['h.is_active = 1'];
|
||||
$params = [];
|
||||
|
||||
if (!empty($_GET['poverty_status'])) {
|
||||
$allowed = ['terdata','rentan_miskin','miskin','sangat_miskin'];
|
||||
if (in_array($_GET['poverty_status'], $allowed, true)) {
|
||||
$where[] = 'h.poverty_status = ?';
|
||||
$params[] = $_GET['poverty_status'];
|
||||
}
|
||||
}
|
||||
if (!empty($_GET['aid_status'])) {
|
||||
if (in_array($_GET['aid_status'], ['not_yet','received'], true)) {
|
||||
$where[] = 'h.aid_status = ?';
|
||||
$params[] = $_GET['aid_status'];
|
||||
}
|
||||
}
|
||||
if (!empty($_GET['house_condition'])) {
|
||||
if (in_array($_GET['house_condition'], ['layak','tidak_layak'], true)) {
|
||||
$where[] = 'h.house_condition = ?';
|
||||
$params[] = $_GET['house_condition'];
|
||||
}
|
||||
}
|
||||
if (!empty($_GET['center_id'])) {
|
||||
$where[] = 'h.managing_center_id = ?';
|
||||
$params[] = (int)$_GET['center_id'];
|
||||
}
|
||||
if (!empty($_GET['q'])) {
|
||||
$where[] = '(h.head_name LIKE ? OR h.full_address LIKE ? OR h.head_nik LIKE ?)';
|
||||
$q = '%' . $_GET['q'] . '%';
|
||||
$params[] = $q; $params[] = $q; $params[] = $q;
|
||||
}
|
||||
|
||||
$whereSQL = implode(' AND ', $where);
|
||||
$limit = min((int)($_GET['limit'] ?? PAGE_SIZE), PAGE_SIZE);
|
||||
$offset = max(0, (int)($_GET['offset'] ?? 0));
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
h.*,
|
||||
rc.name AS center_name,
|
||||
(SELECT COUNT(*) FROM household_members hm WHERE hm.household_id = h.id) AS member_count,
|
||||
(SELECT COUNT(*) FROM aid_history ah WHERE ah.household_id = h.id) AS aid_count
|
||||
FROM households h
|
||||
LEFT JOIN religious_centers rc ON rc.id = h.managing_center_id
|
||||
WHERE $whereSQL
|
||||
ORDER BY h.created_at DESC
|
||||
LIMIT $limit OFFSET $offset
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$cntStmt = $pdo->prepare("SELECT COUNT(*) FROM households h WHERE $whereSQL");
|
||||
$cntStmt->execute($params);
|
||||
$total = (int)$cntStmt->fetchColumn();
|
||||
|
||||
foreach ($rows as &$r) {
|
||||
castHousehold($r);
|
||||
}
|
||||
unset($r);
|
||||
|
||||
Response::success(['households' => $rows, 'total' => $total]);
|
||||
break;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// SHOW — detail with aid history and family members
|
||||
// ================================================================
|
||||
case 'GET:show': {
|
||||
requireAuth();
|
||||
if (!$id) Response::error('ID is required.', 400);
|
||||
|
||||
$pdo = Database::get();
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
h.*,
|
||||
TIMESTAMPDIFF(YEAR, h.head_date_of_birth, CURDATE()) AS age,
|
||||
rc.name AS center_name,
|
||||
rc.worship_type AS center_type,
|
||||
rc.latitude AS center_lat,
|
||||
rc.longitude AS center_lng,
|
||||
rc.radius AS center_radius
|
||||
FROM households h
|
||||
LEFT JOIN religious_centers rc ON rc.id = h.managing_center_id
|
||||
WHERE h.id = ? AND h.is_active = 1
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) Response::notFound('Household not found.');
|
||||
|
||||
castHousehold($row);
|
||||
|
||||
// ⭐ FIX: Pastikan aid_history selalu diambil dengan lengkap
|
||||
$aidStmt = $pdo->prepare("
|
||||
SELECT
|
||||
ah.*,
|
||||
rc.name AS center_name,
|
||||
DATE_FORMAT(ah.aid_date, '%Y-%m-%d') AS aid_date_formatted
|
||||
FROM aid_history ah
|
||||
LEFT JOIN religious_centers rc ON rc.id = ah.center_id
|
||||
WHERE ah.household_id = ?
|
||||
ORDER BY ah.aid_date DESC
|
||||
LIMIT 20
|
||||
");
|
||||
$aidStmt->execute([$id]);
|
||||
$aidHistory = $aidStmt->fetchAll();
|
||||
|
||||
// ⭐ Pastikan format data bantuan konsisten
|
||||
foreach ($aidHistory as &$aid) {
|
||||
$aid['aid_type_label'] = match($aid['aid_type'] ?? '') {
|
||||
'sembako' => 'Sembako',
|
||||
'pendanaan' => 'Pendanaan',
|
||||
'pelatihan' => 'Pelatihan',
|
||||
'sembako_pendanaan' => 'Sembako + Pendanaan',
|
||||
'sembako_pelatihan' => 'Sembako + Pelatihan',
|
||||
'pendanaan_pelatihan' => 'Pendanaan + Pelatihan',
|
||||
'lengkap' => 'Lengkap (Semua)',
|
||||
default => $aid['aid_type'] ?? 'Bantuan'
|
||||
};
|
||||
}
|
||||
unset($aid);
|
||||
|
||||
$row['aid_history'] = $aidHistory;
|
||||
|
||||
// Family members
|
||||
$depStmt = $pdo->prepare("
|
||||
SELECT * FROM household_members
|
||||
WHERE household_id = ?
|
||||
ORDER BY id
|
||||
");
|
||||
$depStmt->execute([$id]);
|
||||
$row['household_members'] = $depStmt->fetchAll();
|
||||
|
||||
Response::success($row);
|
||||
break;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// CREATE
|
||||
// ================================================================
|
||||
case 'POST:create': {
|
||||
requireAuth();
|
||||
$data = Validator::json();
|
||||
|
||||
$v = Validator::make($data, [
|
||||
'head_name' => 'required|string|maxlen:150',
|
||||
'head_nik' => 'required|string|maxlen:16',
|
||||
'full_address' => 'required|string',
|
||||
'latitude' => 'required|latitude',
|
||||
'longitude' => 'required|longitude',
|
||||
'house_condition' => 'in:layak,tidak_layak',
|
||||
'head_education' => 'in:tidak_sekolah,sd,smp,sma,diploma,sarjana,pascasarjana',
|
||||
'aid_status' => 'in:not_yet,received',
|
||||
'head_gender' => 'in:male,female',
|
||||
'head_date_of_birth' => 'date',
|
||||
'land_ownership' => 'in:milik,sewa,numpang,lainnya',
|
||||
]);
|
||||
$v->validate_or_fail();
|
||||
|
||||
$pdo = Database::get();
|
||||
|
||||
$calc = PovertyCalculator::calculate(
|
||||
(int)($data['head_monthly_income'] ?? $data['income'] ?? 0),
|
||||
(int)($data['dependents'] ?? 1),
|
||||
$data['house_condition'] ?? 'layak',
|
||||
$data['head_education'] ?? 'sd',
|
||||
$data['land_ownership'] ?? 'milik'
|
||||
);
|
||||
|
||||
$managingId = resolveManagingCenter($pdo, (float)$data['latitude'], (float)$data['longitude']);
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO households (
|
||||
rt, rw, kelurahan, kecamatan, full_address,
|
||||
latitude, longitude, house_condition, managing_center_id,
|
||||
head_name, head_nik, head_gender, head_date_of_birth, head_education,
|
||||
head_employment_status, head_job_name, head_institution_name, head_monthly_income,
|
||||
poverty_score, poverty_status, aid_status, notes
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
");
|
||||
$stmt->execute([
|
||||
$data['rt'] ?? null,
|
||||
$data['rw'] ?? null,
|
||||
$data['kelurahan'] ?? null,
|
||||
$data['kecamatan'] ?? null,
|
||||
$data['full_address'],
|
||||
(float)$data['latitude'],
|
||||
(float)$data['longitude'],
|
||||
$data['house_condition'] ?? 'layak',
|
||||
$managingId,
|
||||
$data['head_name'],
|
||||
$data['head_nik'],
|
||||
$data['head_gender'] ?? 'male',
|
||||
$data['head_date_of_birth'],
|
||||
$data['head_education'] ?? 'sd',
|
||||
$data['head_employment_status'] ?? 'unemployed',
|
||||
$data['head_job_name'] ?? null,
|
||||
$data['head_institution_name'] ?? null,
|
||||
(int)($data['head_monthly_income'] ?? $data['income'] ?? 0),
|
||||
$calc['score'] ?? 0,
|
||||
$calc['status'],
|
||||
$data['aid_status'] ?? 'not_yet',
|
||||
$data['notes'] ?? $data['description'] ?? null,
|
||||
]);
|
||||
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
|
||||
// Save family members
|
||||
if (!empty($data['household_members']) && is_array($data['household_members'])) {
|
||||
saveMembers($pdo, $newId, $data['household_members']);
|
||||
}
|
||||
|
||||
AuditLog::record('Tambah Rumah', 'households', $newId, null, $data);
|
||||
Response::created([
|
||||
'id' => $newId,
|
||||
'poverty_status' => $calc['status'],
|
||||
'poverty_label' => $calc['label'],
|
||||
'marker_color' => PovertyCalculator::markerColor($calc['status']),
|
||||
'managing_center_id' => $managingId,
|
||||
], 'Rumah berhasil ditambahkan.');
|
||||
break;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// UPDATE
|
||||
// ================================================================
|
||||
case 'POST:update': {
|
||||
requireAuth();
|
||||
if (!$id) Response::error('ID is required.', 400);
|
||||
|
||||
$data = Validator::json();
|
||||
$v = Validator::make($data, [
|
||||
'head_name' => 'required|string|maxlen:150',
|
||||
'head_nik' => 'required|string|maxlen:16',
|
||||
'full_address' => 'required|string',
|
||||
'latitude' => 'required|latitude',
|
||||
'longitude' => 'required|longitude',
|
||||
'house_condition' => 'in:layak,tidak_layak',
|
||||
'head_education' => 'in:tidak_sekolah,sd,smp,sma,diploma,sarjana,pascasarjana',
|
||||
'aid_status' => 'in:not_yet,received',
|
||||
'head_gender' => 'in:male,female',
|
||||
'head_date_of_birth' => 'date',
|
||||
'land_ownership' => 'in:milik,sewa,numpang,lainnya',
|
||||
]);
|
||||
$v->validate_or_fail();
|
||||
|
||||
$pdo = Database::get();
|
||||
$old = $pdo->prepare("SELECT * FROM households WHERE id = ? AND is_active = 1");
|
||||
$old->execute([$id]);
|
||||
$oldRow = $old->fetch();
|
||||
if (!$oldRow) Response::notFound('Household not found.');
|
||||
|
||||
$calc = PovertyCalculator::calculate(
|
||||
(int)($data['head_monthly_income'] ?? $data['income'] ?? 0),
|
||||
(int)($data['dependents'] ?? 1),
|
||||
$data['house_condition'] ?? 'layak',
|
||||
$data['head_education'] ?? 'sd',
|
||||
$data['land_ownership'] ?? 'milik'
|
||||
);
|
||||
|
||||
$managingId = resolveManagingCenter($pdo, (float)$data['latitude'], (float)$data['longitude']);
|
||||
|
||||
$pdo->prepare("
|
||||
UPDATE households SET
|
||||
rt = ?, rw = ?, kelurahan = ?, kecamatan = ?, full_address = ?,
|
||||
latitude = ?, longitude = ?, house_condition = ?, managing_center_id = ?,
|
||||
head_name = ?, head_nik = ?, head_gender = ?, head_date_of_birth = ?, head_education = ?,
|
||||
head_employment_status = ?, head_job_name = ?, head_institution_name = ?, head_monthly_income = ?,
|
||||
poverty_score = ?, poverty_status = ?, aid_status = ?, notes = ?
|
||||
WHERE id = ? AND is_active = 1
|
||||
")->execute([
|
||||
$data['rt'] ?? $oldRow['rt'],
|
||||
$data['rw'] ?? $oldRow['rw'],
|
||||
$data['kelurahan'] ?? $oldRow['kelurahan'],
|
||||
$data['kecamatan'] ?? $oldRow['kecamatan'],
|
||||
$data['full_address'],
|
||||
(float)$data['latitude'],
|
||||
(float)$data['longitude'],
|
||||
$data['house_condition'] ?? 'layak',
|
||||
$managingId,
|
||||
$data['head_name'],
|
||||
$data['head_nik'],
|
||||
$data['head_gender'] ?? 'male',
|
||||
$data['head_date_of_birth'],
|
||||
$data['head_education'] ?? 'sd',
|
||||
$data['head_employment_status'] ?? 'unemployed',
|
||||
$data['head_job_name'] ?? null,
|
||||
$data['head_institution_name'] ?? null,
|
||||
(int)($data['head_monthly_income'] ?? $data['income'] ?? 0),
|
||||
$calc['score'] ?? 0,
|
||||
$calc['status'],
|
||||
$data['aid_status'] ?? 'not_yet',
|
||||
$data['notes'] ?? $data['description'] ?? null,
|
||||
$id,
|
||||
]);
|
||||
|
||||
if (isset($data['household_members']) && is_array($data['household_members'])) {
|
||||
saveMembers($pdo, $id, $data['household_members']);
|
||||
}
|
||||
|
||||
AuditLog::record('Update Rumah', 'households', $id, $oldRow, $data);
|
||||
Response::success([
|
||||
'id' => $id,
|
||||
'poverty_status' => $calc['status'],
|
||||
'poverty_label' => $calc['label'],
|
||||
'marker_color' => PovertyCalculator::markerColor($calc['status']),
|
||||
'managing_center_id' => $managingId,
|
||||
], 'Data rumah diperbarui.');
|
||||
break;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// PATCH — untuk drag marker
|
||||
// ================================================================
|
||||
case 'POST:patch': {
|
||||
requireAuth();
|
||||
if (!$id) Response::error('ID is required.', 400);
|
||||
|
||||
$data = Validator::json();
|
||||
|
||||
// Validasi: minimal latitude dan longitude harus ada
|
||||
if (!isset($data['latitude']) || !isset($data['longitude'])) {
|
||||
Response::error('Latitude dan longitude diperlukan.', 400);
|
||||
}
|
||||
|
||||
Validator::make($data, [
|
||||
'latitude' => 'required|latitude',
|
||||
'longitude' => 'required|longitude',
|
||||
])->validate_or_fail();
|
||||
|
||||
$pdo = Database::get();
|
||||
|
||||
// Cek apakah household ada
|
||||
$check = $pdo->prepare("SELECT id, managing_center_id FROM households WHERE id = ? AND is_active = 1");
|
||||
$check->execute([$id]);
|
||||
$existing = $check->fetch();
|
||||
if (!$existing) Response::notFound('Household tidak ditemukan.');
|
||||
|
||||
// Hitung ulang managing_center_id berdasarkan posisi baru
|
||||
$managingId = resolveManagingCenter($pdo, (float)$data['latitude'], (float)$data['longitude']);
|
||||
|
||||
// Update query
|
||||
$sql = "UPDATE households SET latitude = ?, longitude = ?, managing_center_id = ?";
|
||||
$params = [(float)$data['latitude'], (float)$data['longitude'], $managingId];
|
||||
|
||||
if (!empty($data['full_address'])) {
|
||||
$sql .= ", full_address = ?";
|
||||
$params[] = Validator::sanitizeString($data['full_address']);
|
||||
}
|
||||
|
||||
$sql .= " WHERE id = ? AND is_active = 1";
|
||||
$params[] = $id;
|
||||
|
||||
$pdo->prepare($sql)->execute($params);
|
||||
|
||||
// Ambil data center untuk response
|
||||
$centerName = null;
|
||||
if ($managingId) {
|
||||
$cStmt = $pdo->prepare("SELECT name FROM religious_centers WHERE id = ?");
|
||||
$cStmt->execute([$managingId]);
|
||||
$center = $cStmt->fetch();
|
||||
$centerName = $center ? $center['name'] : null;
|
||||
}
|
||||
|
||||
AuditLog::record('Pindah Posisi Rumah', 'households', $id, null, $data);
|
||||
Response::success([
|
||||
'managing_center_id' => $managingId,
|
||||
'center_name' => $centerName
|
||||
], 'Posisi rumah berhasil diperbarui.');
|
||||
break;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// DELETE (soft delete)
|
||||
// ================================================================
|
||||
case 'POST:delete': {
|
||||
requireAdmin();
|
||||
if (!$id) Response::error('ID is required.', 400);
|
||||
|
||||
$pdo = Database::get();
|
||||
$old = $pdo->prepare("SELECT * FROM households WHERE id = ? AND is_active = 1");
|
||||
$old->execute([$id]);
|
||||
$oldRow = $old->fetch();
|
||||
if (!$oldRow) Response::notFound('Household not found.');
|
||||
|
||||
$pdo->prepare("UPDATE households SET is_active = 0 WHERE id = ?")->execute([$id]);
|
||||
AuditLog::record('Hapus Rumah', 'households', $id, $oldRow);
|
||||
Response::success(null, 'Data rumah dihapus.');
|
||||
break;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// DELETE PHOTO — hapus foto rumah tangga satu per satu
|
||||
// ================================================================
|
||||
case 'POST:delete_photo': {
|
||||
requireAuth();
|
||||
if (!$id) Response::error('ID is required.', 400);
|
||||
|
||||
$data = Validator::json();
|
||||
$filename = trim($data['filename'] ?? '');
|
||||
|
||||
if ($filename === '' || preg_match('/[\/\\\\]/', $filename) ||
|
||||
!preg_match('/^[a-zA-Z0-9_\-\.]+$/', $filename)) {
|
||||
Response::error('Nama file tidak valid.', 400);
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, ['jpg', 'jpeg', 'png'], true)) {
|
||||
Response::error('Ekstensi file tidak diizinkan.', 400);
|
||||
}
|
||||
|
||||
$pdo = Database::get();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT house_photos FROM households WHERE id = ? AND is_active = 1");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
Response::notFound('Data rumah tangga tidak ditemukan.');
|
||||
}
|
||||
|
||||
$photos = json_decode($row['house_photos'] ?? '[]', true) ?: [];
|
||||
|
||||
if (!in_array($filename, $photos, true)) {
|
||||
Response::error('File tidak ditemukan dalam data ini.', 404);
|
||||
}
|
||||
|
||||
$photos = array_values(array_filter($photos, fn($p) => $p !== $filename));
|
||||
|
||||
$upd = $pdo->prepare("UPDATE households SET house_photos = ? WHERE id = ?");
|
||||
$upd->execute([json_encode($photos, JSON_UNESCAPED_UNICODE), $id]);
|
||||
|
||||
$dir = __DIR__ . '/../../uploads/houses/';
|
||||
$fullPath = realpath($dir . $filename);
|
||||
$realDir = realpath($dir);
|
||||
if ($fullPath && $realDir && str_starts_with($fullPath, $realDir)) {
|
||||
@unlink($fullPath);
|
||||
}
|
||||
|
||||
AuditLog::record('Hapus Foto Rumah', 'households', $id, null, ['filename' => $filename]);
|
||||
Response::success(['remaining' => $photos], 'Foto berhasil dihapus.');
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Response::methodNotAllowed();
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// HELPERS
|
||||
// ================================================================
|
||||
|
||||
function castHousehold(array &$r): void
|
||||
{
|
||||
$r['latitude'] = (float)$r['latitude'];
|
||||
$r['longitude'] = (float)$r['longitude'];
|
||||
$r['age'] = isset($r['age']) ? (int)$r['age'] : null;
|
||||
$r['is_active'] = (bool)$r['is_active'];
|
||||
$r['marker_color'] = PovertyCalculator::markerColor($r['poverty_status'] ?? '');
|
||||
$r['poverty_label'] = PovertyCalculator::label($r['poverty_status'] ?? '');
|
||||
|
||||
// Backward compatibility untuk frontend lama
|
||||
$r['address'] = $r['full_address'] ?? '';
|
||||
$r['nik'] = $r['head_nik'] ?? '';
|
||||
$r['head_name'] = $r['head_name'] ?? '';
|
||||
$r['head_nik'] = $r['head_nik'] ?? '';
|
||||
$r['head_education'] = $r['head_education'] ?? 'sd';
|
||||
$r['head_employment_status'] = $r['head_employment_status'] ?? 'unemployed';
|
||||
$r['head_job_name'] = $r['head_job_name'] ?? '';
|
||||
$r['head_monthly_income'] = (int)($r['head_monthly_income'] ?? 0);
|
||||
$r['house_condition'] = $r['house_condition'] ?? 'layak';
|
||||
$r['aid_status'] = $r['aid_status'] ?? 'not_yet';
|
||||
$r['notes'] = $r['notes'] ?? '';
|
||||
$r['dependents'] = (int)($r['family_members_count'] ?? 0) + 1;
|
||||
$r['income'] = $r['head_monthly_income'] ?? 0;
|
||||
$r['job'] = $r['head_job_name'] ?? '';
|
||||
}
|
||||
|
||||
function resolveManagingCenter(\PDO $pdo, float $lat, float $lng): ?int
|
||||
{
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT sub.id FROM (
|
||||
SELECT
|
||||
rc.id,
|
||||
rc.radius,
|
||||
(6371000 * ACOS(
|
||||
COS(RADIANS(:lat1)) * COS(RADIANS(rc.latitude)) *
|
||||
COS(RADIANS(rc.longitude) - RADIANS(:lng)) +
|
||||
SIN(RADIANS(:lat2)) * SIN(RADIANS(rc.latitude))
|
||||
)) AS distance_m,
|
||||
(SELECT COUNT(*) FROM households hh
|
||||
WHERE hh.managing_center_id = rc.id AND hh.is_active = 1) AS load_count
|
||||
FROM religious_centers rc
|
||||
WHERE rc.is_active = 1
|
||||
) sub
|
||||
WHERE sub.distance_m <= sub.radius
|
||||
ORDER BY sub.load_count ASC, sub.distance_m ASC
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([':lat1' => $lat, ':lng' => $lng, ':lat2' => $lat]);
|
||||
$row = $stmt->fetch();
|
||||
return $row ? (int)$row['id'] : null;
|
||||
}
|
||||
|
||||
function saveMembers(\PDO $pdo, int $householdId, array $data): void
|
||||
{
|
||||
try {
|
||||
// Delete existing members
|
||||
$pdo->prepare("DELETE FROM household_members WHERE household_id = ?")->execute([$householdId]);
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO household_members
|
||||
(household_id, name, nik, gender, date_of_birth, education,
|
||||
relationship, employment_status, job_name, institution_name, monthly_income)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||
");
|
||||
|
||||
foreach ($data as $member) {
|
||||
$name = trim($member['name'] ?? '');
|
||||
if (!$name) continue;
|
||||
|
||||
$stmt->execute([
|
||||
$householdId,
|
||||
$name,
|
||||
$member['nik'] ?? null,
|
||||
$member['gender'] ?? 'male',
|
||||
$member['date_of_birth'] ?? null,
|
||||
$member['education'] ?? 'sd',
|
||||
$member['relationship'] ?? 'lainnya',
|
||||
$member['employment_status'] ?? 'unemployed',
|
||||
$member['job_name'] ?? null,
|
||||
$member['institution_name'] ?? null,
|
||||
(int)($member['monthly_income'] ?? 0),
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
error_log('Save members error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
<?php
|
||||
/**
|
||||
* api/public/report.php
|
||||
* Public poverty reports — create (lapor.html) + admin CRUD.
|
||||
*/
|
||||
|
||||
// Matikan semua error reporting ke output
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
// Bersihkan buffer
|
||||
while (ob_get_level()) ob_end_clean();
|
||||
ob_start();
|
||||
|
||||
// Load bootstrap (sudah include requireAuth() dan requireAdmin())
|
||||
if (!file_exists(__DIR__ . '/../../config/bootstrap.php')) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'message' => 'Konfigurasi server tidak lengkap.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../../config/bootstrap.php';
|
||||
|
||||
// Pastikan tidak ada output sebelum JSON
|
||||
if (ob_get_length()) ob_clean();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
$action = $_GET['action'] ?? '';
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
try {
|
||||
// POST tanpa action = create (dari lapor.html)
|
||||
if ($method === 'POST' && ($action === '' || $action === 'create')) {
|
||||
handleCreate();
|
||||
}
|
||||
// GET list
|
||||
elseif ($method === 'GET' && $action === 'list') {
|
||||
handleList();
|
||||
}
|
||||
// GET show
|
||||
elseif ($method === 'GET' && $action === 'show' && $id) {
|
||||
handleShow($id);
|
||||
}
|
||||
// POST approve (admin) - requireAdmin() dari bootstrap.php
|
||||
elseif ($method === 'POST' && $action === 'approve' && $id) {
|
||||
requireAdmin();
|
||||
handleApprove($id);
|
||||
}
|
||||
// POST reject (admin)
|
||||
elseif ($method === 'POST' && $action === 'reject' && $id) {
|
||||
requireAdmin();
|
||||
handleReject($id);
|
||||
}
|
||||
// POST delete (admin)
|
||||
elseif ($method === 'POST' && $action === 'delete' && $id) {
|
||||
requireAdmin();
|
||||
handleDelete($id);
|
||||
}
|
||||
elseif ($method === 'POST' && $action === 'delete_photo' && $id) {
|
||||
requireAuth(); // at minimum logged in
|
||||
handleDeletePhoto($id, 'public_reports', 'proof_photos',
|
||||
__DIR__ . '/../../uploads/reports/');
|
||||
}
|
||||
else {
|
||||
jsonError('Method or action not allowed', 405);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log('[public/report.php] PDO: ' . $e->getMessage());
|
||||
jsonError('Terjadi kesalahan database. Silakan coba lagi.', 500);
|
||||
} catch (Exception $e) {
|
||||
error_log('[public/report.php] ' . $e->getMessage());
|
||||
jsonError('Terjadi kesalahan server.', 500);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CREATE — public submission, all fields required
|
||||
// ============================================================
|
||||
function handleCreate(): void
|
||||
{
|
||||
$raw = file_get_contents('php://input');
|
||||
$data = json_decode($raw, true);
|
||||
|
||||
if (!is_array($data)) {
|
||||
jsonError('Data tidak valid.', 400);
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
|
||||
// Required fields
|
||||
$required = [
|
||||
'reporter_name' => 'Nama pelapor',
|
||||
'reporter_phone' => 'Nomor telepon',
|
||||
'head_name' => 'Nama kepala keluarga',
|
||||
'address' => 'Alamat',
|
||||
'kelurahan' => 'Kelurahan',
|
||||
'kecamatan' => 'Kecamatan',
|
||||
'description' => 'Deskripsi',
|
||||
'latitude' => 'Latitude',
|
||||
'longitude' => 'Longitude',
|
||||
'severity' => 'Tingkat urgensi'
|
||||
];
|
||||
|
||||
foreach ($required as $field => $label) {
|
||||
$value = trim($data[$field] ?? '');
|
||||
if ($value === '') {
|
||||
$errors[$field] = "$label wajib diisi.";
|
||||
}
|
||||
}
|
||||
|
||||
// RT/RW juga required
|
||||
if (trim($data['rt'] ?? '') === '') {
|
||||
$errors['rt'] = 'RT wajib diisi.';
|
||||
}
|
||||
if (trim($data['rw'] ?? '') === '') {
|
||||
$errors['rw'] = 'RW wajib diisi.';
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
$firstError = reset($errors);
|
||||
jsonError($firstError, 422, ['validation_errors' => $errors]);
|
||||
}
|
||||
|
||||
// Phone validation
|
||||
$phone = preg_replace('/[\s\-\(\)\+]/', '', $data['reporter_phone']);
|
||||
if (!preg_match('/^\d{8,15}$/', $phone)) {
|
||||
jsonError('Nomor telepon tidak valid (8-15 digit).', 422);
|
||||
}
|
||||
|
||||
// Description min 20 chars
|
||||
if (mb_strlen($data['description']) < 20) {
|
||||
jsonError('Deskripsi minimal 20 karakter.', 422);
|
||||
}
|
||||
|
||||
// Coordinates
|
||||
$lat = (float)$data['latitude'];
|
||||
$lng = (float)$data['longitude'];
|
||||
if ($lat < -90 || $lat > 90 || $lat == 0) {
|
||||
jsonError('Latitude tidak valid.', 422);
|
||||
}
|
||||
if ($lng < -180 || $lng > 180 || $lng == 0) {
|
||||
jsonError('Longitude tidak valid.', 422);
|
||||
}
|
||||
|
||||
// Severity validation
|
||||
$allowedSeverity = ['ringan', 'sedang', 'berat', 'kritis'];
|
||||
if (!in_array($data['severity'], $allowedSeverity)) {
|
||||
jsonError('Tingkat urgensi tidak valid.', 422);
|
||||
}
|
||||
// Photo requirement
|
||||
$photoCount = (int)($data['proof_photo_count'] ?? -1);
|
||||
if ($photoCount === 0) {
|
||||
jsonError('Foto bukti wajib diunggah minimal 1 foto.', 422);
|
||||
}
|
||||
|
||||
// Rate limit
|
||||
$pdo = Database::get();
|
||||
$ip = getClientIP();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM public_reports WHERE ip_address = ? AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)");
|
||||
$stmt->execute([$ip]);
|
||||
if ((int)$stmt->fetchColumn() >= 5) {
|
||||
jsonError('Terlalu banyak laporan. Coba lagi besok.', 429);
|
||||
}
|
||||
|
||||
// Sanitize
|
||||
$sanitize = fn($s) => htmlspecialchars(strip_tags(trim($s)), ENT_QUOTES, 'UTF-8');
|
||||
|
||||
// Insert
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO public_reports
|
||||
(reporter_name, reporter_phone, rt, rw, head_name, address, kelurahan, kecamatan,
|
||||
latitude, longitude, description, severity, urgent_need, status, ip_address, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, NOW())
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$sanitize($data['reporter_name']),
|
||||
$sanitize($data['reporter_phone']),
|
||||
$sanitize($data['rt'] ?? ''),
|
||||
$sanitize($data['rw'] ?? ''),
|
||||
$sanitize($data['head_name']),
|
||||
$sanitize($data['address']),
|
||||
$sanitize($data['kelurahan']),
|
||||
$sanitize($data['kecamatan']),
|
||||
round($lat, 7),
|
||||
round($lng, 7),
|
||||
$sanitize($data['description']),
|
||||
$data['severity'],
|
||||
$sanitize($data['urgent_need'] ?? ''),
|
||||
$ip
|
||||
]);
|
||||
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
|
||||
jsonSuccess(['id' => $newId], 'Laporan berhasil dikirim. Petugas akan segera memverifikasi.', 201);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LIST
|
||||
// ============================================================
|
||||
function handleList(): void
|
||||
{
|
||||
$pdo = Database::get();
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
|
||||
if (!empty($_GET['status'])) {
|
||||
$where[] = 'status = ?';
|
||||
$params[] = $_GET['status'];
|
||||
}
|
||||
|
||||
$whereSQL = implode(' AND ', $where);
|
||||
$limit = min((int)($_GET['limit'] ?? 100), 500);
|
||||
$offset = max(0, (int)($_GET['offset'] ?? 0));
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT * FROM public_reports
|
||||
WHERE $whereSQL
|
||||
ORDER BY FIELD(status, 'pending', 'approved', 'rejected'), created_at DESC
|
||||
LIMIT $limit OFFSET $offset
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$cnt = $pdo->prepare("SELECT COUNT(*) FROM public_reports WHERE $whereSQL");
|
||||
$cnt->execute($params);
|
||||
|
||||
jsonSuccess(['reports' => $rows, 'total' => (int)$cnt->fetchColumn()]);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SHOW
|
||||
// ============================================================
|
||||
function handleShow(int $id): void
|
||||
{
|
||||
$pdo = Database::get();
|
||||
$stmt = $pdo->prepare("SELECT * FROM public_reports WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
jsonError('Laporan tidak ditemukan.', 404);
|
||||
}
|
||||
|
||||
jsonSuccess($row);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// APPROVE
|
||||
// ============================================================
|
||||
function handleApprove(int $id): void
|
||||
{
|
||||
$pdo = Database::get();
|
||||
|
||||
// Get report
|
||||
$stmt = $pdo->prepare("SELECT * FROM public_reports WHERE id = ? AND status = 'pending'");
|
||||
$stmt->execute([$id]);
|
||||
$report = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$report) {
|
||||
jsonError('Laporan tidak ditemukan atau sudah diproses.', 404);
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
// Create household
|
||||
$income = (int)($data['income'] ?? 0);
|
||||
$condition = $data['house_condition'] ?? 'tidak_layak';
|
||||
$education = $data['education'] ?? 'sd';
|
||||
|
||||
// Simple poverty score
|
||||
$score = 0;
|
||||
if ($income < 500000) $score += 30;
|
||||
elseif ($income < 1500000) $score += 20;
|
||||
elseif ($income < 3000000) $score += 10;
|
||||
if ($condition === 'tidak_layak') $score += 20;
|
||||
|
||||
$povertyStatus = match(true) {
|
||||
$score >= 60 => 'sangat_miskin',
|
||||
$score >= 40 => 'miskin',
|
||||
$score >= 20 => 'rentan_miskin',
|
||||
default => 'terdata'
|
||||
};
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO households
|
||||
(full_address, kelurahan, kecamatan, latitude, longitude, head_name,
|
||||
head_education, head_monthly_income, house_condition, land_ownership,
|
||||
poverty_score, poverty_status, notes)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'numpang', ?, ?, ?)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$report['address'],
|
||||
$report['kelurahan'] ?? '',
|
||||
$report['kecamatan'] ?? '',
|
||||
$report['latitude'],
|
||||
$report['longitude'],
|
||||
$report['head_name'],
|
||||
$education,
|
||||
$income,
|
||||
$condition,
|
||||
$score,
|
||||
$povertyStatus,
|
||||
'Dari laporan publik #' . $id
|
||||
]);
|
||||
|
||||
$householdId = (int)$pdo->lastInsertId();
|
||||
|
||||
// Update report
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE public_reports
|
||||
SET status = 'approved', converted_household_id = ?, admin_notes = ?, reviewed_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$householdId, $data['admin_notes'] ?? null, $id]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
jsonSuccess(['household_id' => $householdId], 'Laporan disetujui.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// REJECT
|
||||
// ============================================================
|
||||
function handleReject(int $id): void
|
||||
{
|
||||
$pdo = Database::get();
|
||||
$data = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE public_reports SET status = 'rejected', admin_notes = ?, reviewed_at = NOW() WHERE id = ? AND status = 'pending'");
|
||||
$stmt->execute([$data['admin_notes'] ?? null, $id]);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
jsonError('Laporan tidak ditemukan atau sudah diproses.', 404);
|
||||
}
|
||||
|
||||
jsonSuccess(null, 'Laporan ditolak.');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DELETE SINGLE PHOTO (shared by report & house endpoints)
|
||||
// ============================================================
|
||||
function handleDeletePhoto(int $id, string $table, string $col, string $dir): void
|
||||
{
|
||||
// Role guard: admin or field_officer only
|
||||
$user = requireAuth();
|
||||
$role = $user['role'] ?? '';
|
||||
if (!in_array($role, ['admin', 'field_officer'], true)) {
|
||||
jsonError('Akses ditolak. Hanya admin atau petugas lapangan.', 403);
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
$filename = trim($data['filename'] ?? '');
|
||||
|
||||
// Security: filename must be a plain filename with no path separators
|
||||
if ($filename === '' || preg_match('/[\/\\\\]/', $filename) ||
|
||||
!preg_match('/^[a-zA-Z0-9_\-\.]+$/', $filename)) {
|
||||
jsonError('Nama file tidak valid.', 400);
|
||||
}
|
||||
|
||||
// Must end with allowed extension
|
||||
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, ['jpg', 'jpeg', 'png'], true)) {
|
||||
jsonError('Ekstensi file tidak diizinkan.', 400);
|
||||
}
|
||||
|
||||
$pdo = Database::get();
|
||||
$stmt = $pdo->prepare("SELECT $col FROM $table WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
jsonError('Data tidak ditemukan.', 404);
|
||||
}
|
||||
|
||||
$photos = json_decode($row[$col] ?? '[]', true) ?: [];
|
||||
|
||||
// Check file is actually in this record's photo list (prevents arbitrary deletion)
|
||||
if (!in_array($filename, $photos, true)) {
|
||||
jsonError('File tidak ditemukan dalam data ini.', 404);
|
||||
}
|
||||
|
||||
// Remove from array
|
||||
$photos = array_values(array_filter($photos, fn($p) => $p !== $filename));
|
||||
|
||||
// Update DB
|
||||
$upd = $pdo->prepare("UPDATE $table SET $col = ? WHERE id = ?");
|
||||
$upd->execute([json_encode($photos, JSON_UNESCAPED_UNICODE), $id]);
|
||||
|
||||
// Delete physical file — realpath check prevents traversal
|
||||
$fullPath = realpath($dir . $filename);
|
||||
$realDir = realpath($dir);
|
||||
if ($fullPath && $realDir && str_starts_with($fullPath, $realDir)) {
|
||||
@unlink($fullPath);
|
||||
}
|
||||
|
||||
jsonSuccess(['remaining' => $photos], 'Foto berhasil dihapus.');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DELETE
|
||||
// ============================================================
|
||||
function handleDelete(int $id): void
|
||||
{
|
||||
$pdo = Database::get();
|
||||
$stmt = $pdo->prepare("DELETE FROM public_reports WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
jsonError('Laporan tidak ditemukan.', 404);
|
||||
}
|
||||
|
||||
jsonSuccess(null, 'Laporan dihapus.');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HELPERS (Hanya fungsi yang tidak ada di bootstrap.php)
|
||||
// ============================================================
|
||||
|
||||
// getClientIP - tidak ada di bootstrap, aman didefinisikan
|
||||
function getClientIP(): string
|
||||
{
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
return trim(explode(',', $ip)[0]);
|
||||
}
|
||||
|
||||
// jsonSuccess dan jsonError - fungsi custom untuk response JSON
|
||||
function jsonSuccess($data, string $message = 'OK', int $code = 200): void
|
||||
{
|
||||
http_response_code($code);
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => $data
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
function jsonError(string $message, int $code = 400, array $extra = []): void
|
||||
{
|
||||
http_response_code($code);
|
||||
$response = ['success' => false, 'message' => $message];
|
||||
if (!empty($extra)) {
|
||||
$response = array_merge($response, $extra);
|
||||
}
|
||||
echo json_encode($response, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
// ===== DEBUG MODE - FULL ERROR REPORTING =====
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$debug = [
|
||||
'step' => 1,
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
try {
|
||||
// Step 2: Cek method
|
||||
$debug['step'] = 2;
|
||||
$debug['method'] = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
throw new Exception('Method not allowed. Use POST.');
|
||||
}
|
||||
|
||||
// Step 3: Cek parameters
|
||||
$debug['step'] = 3;
|
||||
$debug['get'] = $_GET;
|
||||
$target = $_GET['target'] ?? '';
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
$debug['target'] = $target;
|
||||
$debug['id'] = $id;
|
||||
|
||||
if (!in_array($target, ['report', 'house'], true) || $id <= 0) {
|
||||
throw new Exception('Invalid target or id. target=' . $target . ', id=' . $id);
|
||||
}
|
||||
|
||||
// Step 4: Load bootstrap
|
||||
$debug['step'] = 4;
|
||||
$bootstrap_path = __DIR__ . '/../../config/bootstrap.php';
|
||||
$debug['bootstrap_path'] = $bootstrap_path;
|
||||
$debug['bootstrap_exists'] = file_exists($bootstrap_path);
|
||||
|
||||
if (!file_exists($bootstrap_path)) {
|
||||
throw new Exception('Bootstrap not found at: ' . $bootstrap_path);
|
||||
}
|
||||
|
||||
require_once $bootstrap_path;
|
||||
$debug['step'] = 5;
|
||||
$debug['bootstrap_loaded'] = true;
|
||||
|
||||
// Step 5: Authentication for house target
|
||||
$debug['step'] = 6;
|
||||
if ($target === 'house') {
|
||||
$user = requireAuth();
|
||||
$debug['auth_user'] = $user['name'] ?? 'unknown';
|
||||
}
|
||||
|
||||
// Step 6: Setup directories
|
||||
$debug['step'] = 7;
|
||||
$dir = $target === 'report' ? 'reports' : 'houses';
|
||||
$table = $target === 'report' ? 'public_reports' : 'households';
|
||||
$dest = __DIR__ . '/../../uploads/' . $dir . '/';
|
||||
$debug['dest'] = $dest;
|
||||
$debug['dest_exists'] = is_dir($dest);
|
||||
$debug['dest_writable'] = is_writable($dest);
|
||||
|
||||
if (!is_dir($dest)) {
|
||||
mkdir($dest, 0777, true);
|
||||
$debug['dest_created'] = true;
|
||||
}
|
||||
|
||||
// Step 7: Check record exists
|
||||
$debug['step'] = 8;
|
||||
$pdo = Database::get();
|
||||
$colName = $target === 'report' ? 'proof_photos' : 'house_photos';
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id, $colName FROM $table WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$record = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$debug['record_exists'] = !empty($record);
|
||||
|
||||
if (!$record) {
|
||||
throw new Exception("Record with id $id not found in $table");
|
||||
}
|
||||
|
||||
$existing = json_decode($record[$colName] ?? '[]', true) ?: [];
|
||||
$debug['existing_photos'] = count($existing);
|
||||
|
||||
// Step 8: Check uploaded files
|
||||
$debug['step'] = 9;
|
||||
$files = $_FILES['photos'] ?? null;
|
||||
$debug['has_files'] = !empty($files);
|
||||
$debug['files_count'] = $files ? count($files['name']) : 0;
|
||||
|
||||
if (!$files || empty($files['name'][0])) {
|
||||
throw new Exception('No files uploaded');
|
||||
}
|
||||
|
||||
// Step 9: Process files
|
||||
$debug['step'] = 10;
|
||||
$saved = [];
|
||||
|
||||
foreach ($files['name'] as $i => $originalName) {
|
||||
if ($files['error'][$i] !== UPLOAD_ERR_OK) {
|
||||
throw new Exception('Upload error for file: ' . $originalName);
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
||||
$unique = bin2hex(random_bytes(12));
|
||||
$filename = "{$dir}_{$id}_{$unique}.{$ext}";
|
||||
$fullPath = $dest . $filename;
|
||||
|
||||
if (move_uploaded_file($files['tmp_name'][$i], $fullPath)) {
|
||||
$saved[] = $filename;
|
||||
$debug['saved'][] = $filename;
|
||||
} else {
|
||||
throw new Exception('Failed to move uploaded file: ' . $originalName);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 10: Update database
|
||||
$debug['step'] = 11;
|
||||
$all = array_merge($existing, $saved);
|
||||
$upd = $pdo->prepare("UPDATE $table SET $colName = ? WHERE id = ?");
|
||||
$upd->execute([json_encode($all, JSON_UNESCAPED_UNICODE), $id]);
|
||||
$debug['db_updated'] = true;
|
||||
|
||||
// Success response
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Foto berhasil diunggah',
|
||||
'data' => [
|
||||
'filenames' => $saved,
|
||||
'all_photos' => $all
|
||||
],
|
||||
'debug' => $debug
|
||||
], JSON_PRETTY_PRINT);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$debug['error'] = $e->getMessage();
|
||||
$debug['trace'] = $e->getTraceAsString();
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
'debug' => $debug
|
||||
], JSON_PRETTY_PRINT);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/reports/index.php — Emergency Reports CRUD (no auth)
|
||||
// UI tabs removed; API kept for data integrity & popup access
|
||||
// ============================================================
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../../config/bootstrap.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
|
||||
switch ("$method:$action") {
|
||||
|
||||
case 'GET:list':
|
||||
case 'GET:': {
|
||||
$pdo = Database::get();
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
|
||||
if (!empty($_GET['status'])) { $where[] = 'er.status = ?'; $params[] = $_GET['status']; }
|
||||
if (!empty($_GET['severity'])) { $where[] = 'er.severity = ?'; $params[] = $_GET['severity']; }
|
||||
if (!empty($_GET['household_id'])) { $where[] = 'er.household_id = ?'; $params[] = (int)$_GET['household_id']; }
|
||||
if (!empty($_GET['type'])) { $where[] = 'er.type = ?'; $params[] = $_GET['type']; }
|
||||
|
||||
$whereSQL = implode(' AND ', $where);
|
||||
$limit = min((int)($_GET['limit'] ?? 50), 200);
|
||||
$offset = max(0, (int)($_GET['offset'] ?? 0));
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT er.*, h.head_name, h.address, h.latitude, h.longitude, h.nik
|
||||
FROM emergency_reports er
|
||||
LEFT JOIN households h ON h.id = er.household_id
|
||||
WHERE $whereSQL
|
||||
ORDER BY FIELD(er.severity,'kritis','berat','sedang','ringan'),
|
||||
FIELD(er.status,'open','in_progress','resolved','closed'),
|
||||
er.created_at DESC
|
||||
LIMIT $limit OFFSET $offset
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$cntStmt = $pdo->prepare("SELECT COUNT(*) FROM emergency_reports er WHERE $whereSQL");
|
||||
$cntStmt->execute($params);
|
||||
|
||||
foreach ($rows as &$r) { $r['severity_color'] = severityColor($r['severity']); }
|
||||
unset($r);
|
||||
|
||||
Response::success(['reports' => $rows, 'total' => (int)$cntStmt->fetchColumn()]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'GET:show': {
|
||||
if (!$id) Response::error('ID is required.', 400);
|
||||
$pdo = Database::get();
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT er.*, h.head_name, h.address, h.latitude, h.longitude, h.nik,
|
||||
h.poverty_status, h.dependents, h.income, h.house_condition
|
||||
FROM emergency_reports er
|
||||
LEFT JOIN households h ON h.id = er.household_id
|
||||
WHERE er.id = ?
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) Response::notFound('Report not found.');
|
||||
$row['severity_color'] = severityColor($row['severity']);
|
||||
Response::success($row);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'POST:create': {
|
||||
$data = Validator::json();
|
||||
$v = Validator::make($data, [
|
||||
'household_id' => 'required|integer',
|
||||
'type' => 'required|in:sakit,kecelakaan,bencana,kehilangan_pekerjaan,kematian,lainnya',
|
||||
'severity' => 'required|in:ringan,sedang,berat,kritis',
|
||||
'description' => 'required|string',
|
||||
]);
|
||||
$v->validate_or_fail();
|
||||
|
||||
$pdo = Database::get();
|
||||
$hh = $pdo->prepare('SELECT id FROM households WHERE id=? AND is_active=1');
|
||||
$hh->execute([(int)$data['household_id']]);
|
||||
if (!$hh->fetch()) Response::notFound('Household not found.');
|
||||
|
||||
$existing = $pdo->prepare("
|
||||
SELECT id, type, status FROM emergency_reports
|
||||
WHERE household_id = ? AND status IN ('open','in_progress') LIMIT 1
|
||||
");
|
||||
$existing->execute([(int)$data['household_id']]);
|
||||
$active = $existing->fetch();
|
||||
if ($active) {
|
||||
$lbl = ['sakit'=>'Sakit','kecelakaan'=>'Kecelakaan','bencana'=>'Bencana',
|
||||
'kehilangan_pekerjaan'=>'Kehilangan Pekerjaan','kematian'=>'Kematian','lainnya'=>'Lainnya'];
|
||||
Response::error(
|
||||
'Sudah ada laporan aktif: ' . ($lbl[$active['type']] ?? $active['type']) . '. Selesaikan dulu.',
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
$pdo->prepare("INSERT INTO emergency_reports (household_id, type, severity, description, status) VALUES (?,?,?,?,'open')")
|
||||
->execute([(int)$data['household_id'], $data['type'], $data['severity'], Validator::sanitizeString($data['description'])]);
|
||||
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
AuditLog::record('Tambah Laporan Darurat', 'emergency_reports', $newId, null, $data);
|
||||
Response::created(['id' => $newId], 'Laporan darurat berhasil dibuat.');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'POST:update': {
|
||||
requireAuth();
|
||||
if (!$id) Response::error('ID is required.', 400);
|
||||
$data = Validator::json();
|
||||
Validator::make($data, [
|
||||
'status' => 'required|in:open,in_progress,resolved,closed',
|
||||
'severity' => 'in:ringan,sedang,berat,kritis',
|
||||
])->validate_or_fail();
|
||||
|
||||
$pdo = Database::get();
|
||||
$old = $pdo->prepare('SELECT * FROM emergency_reports WHERE id=?');
|
||||
$old->execute([$id]);
|
||||
$oldRow = $old->fetch();
|
||||
if (!$oldRow) Response::notFound('Report not found.');
|
||||
|
||||
$fields = ['status = ?'];
|
||||
$params = [$data['status']];
|
||||
if (!empty($data['severity'])) { $fields[] = 'severity = ?'; $params[] = $data['severity']; }
|
||||
if (!empty($data['description'])) { $fields[] = 'description = ?'; $params[] = Validator::sanitizeString($data['description']); }
|
||||
if (in_array($data['status'], ['resolved','closed']) && !$oldRow['resolved_at']) { $fields[] = 'resolved_at = NOW()'; }
|
||||
$params[] = $id;
|
||||
|
||||
$pdo->prepare('UPDATE emergency_reports SET ' . implode(', ', $fields) . ' WHERE id=?')->execute($params);
|
||||
AuditLog::record('Update Laporan Darurat', 'emergency_reports', $id, $oldRow, $data);
|
||||
Response::success(['id' => $id], 'Laporan diperbarui.');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'POST:resolve': {
|
||||
requireAuth();
|
||||
if (!$id) Response::error('ID is required.', 400);
|
||||
$pdo = Database::get();
|
||||
$stmt = $pdo->prepare("UPDATE emergency_reports SET status='resolved', resolved_at=NOW() WHERE id=? AND status NOT IN ('resolved','closed')");
|
||||
$stmt->execute([$id]);
|
||||
if ($stmt->rowCount() === 0) Response::error('Report not found or already resolved.', 409);
|
||||
AuditLog::record('Selesaikan Laporan', 'emergency_reports', $id);
|
||||
Response::success(null, 'Laporan diselesaikan.');
|
||||
break;
|
||||
}
|
||||
|
||||
case 'POST:delete': {
|
||||
requireAdmin();
|
||||
if (!$id) Response::error('ID is required.', 400);
|
||||
$pdo = Database::get();
|
||||
$old = $pdo->prepare('SELECT * FROM emergency_reports WHERE id=?');
|
||||
$old->execute([$id]);
|
||||
$row = $old->fetch();
|
||||
if (!$row) Response::notFound('Report not found.');
|
||||
$pdo->prepare('DELETE FROM emergency_reports WHERE id=?')->execute([$id]);
|
||||
AuditLog::record('Hapus Laporan Darurat', 'emergency_reports', $id, $row);
|
||||
Response::success(null, 'Laporan dihapus.');
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Response::methodNotAllowed();
|
||||
}
|
||||
|
||||
function severityColor(string $s): string
|
||||
{
|
||||
return match($s) {
|
||||
'kritis' => '#d63230', 'berat' => '#f76707',
|
||||
'sedang' => '#f59e0b', 'ringan' => '#0b9e73',
|
||||
default => '#9ba4b5',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
// api/stats/index.php — Dashboard Statistics
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/../../config/bootstrap.php';
|
||||
|
||||
requireAuth();
|
||||
$pdo = Database::get();
|
||||
$action = $_GET['action'] ?? 'overview';
|
||||
|
||||
switch ($action) {
|
||||
|
||||
case 'overview': {
|
||||
$centers = (int)$pdo->query("SELECT COUNT(*) FROM religious_centers WHERE is_active = 1")->fetchColumn();
|
||||
$households = (int)$pdo->query("SELECT COUNT(*) FROM households WHERE is_active = 1")->fetchColumn();
|
||||
|
||||
// Total population (head + members)
|
||||
$members = (int)$pdo->query("SELECT COUNT(*) FROM household_members")->fetchColumn();
|
||||
$population = $households + $members;
|
||||
|
||||
// Aid received (based on aid_history)
|
||||
$aidReceived = (int)$pdo->query("SELECT COUNT(DISTINCT household_id) FROM aid_history")->fetchColumn();
|
||||
|
||||
// Open emergency reports
|
||||
$openReports = 0;
|
||||
try {
|
||||
$openReports = (int)$pdo->query("SELECT COUNT(*) FROM emergency_reports WHERE status IN ('open', 'in_progress')")->fetchColumn();
|
||||
} catch (\Throwable) {}
|
||||
|
||||
// Pending public reports
|
||||
$pendingPublic = 0;
|
||||
try {
|
||||
$pendingPublic = (int)$pdo->query("SELECT COUNT(*) FROM public_reports WHERE status = 'pending'")->fetchColumn();
|
||||
} catch (\Throwable) {}
|
||||
|
||||
// Poverty breakdown
|
||||
$povertyBreakdown = $pdo->query("
|
||||
SELECT poverty_status, COUNT(*) AS cnt
|
||||
FROM households WHERE is_active = 1
|
||||
GROUP BY poverty_status
|
||||
")->fetchAll(\PDO::FETCH_KEY_PAIR);
|
||||
|
||||
// Condition breakdown
|
||||
$conditionBreakdown = $pdo->query("
|
||||
SELECT house_condition, COUNT(*) AS cnt
|
||||
FROM households WHERE is_active = 1
|
||||
GROUP BY house_condition
|
||||
")->fetchAll(\PDO::FETCH_KEY_PAIR);
|
||||
|
||||
Response::success([
|
||||
'centers' => $centers,
|
||||
'households' => $households,
|
||||
'population' => $population,
|
||||
'aid_received' => $aidReceived,
|
||||
'aid_not_yet' => max(0, $households - $aidReceived),
|
||||
'open_reports' => $openReports,
|
||||
'pending_public' => $pendingPublic,
|
||||
'poverty_breakdown' => [
|
||||
'sangat_miskin' => (int)($povertyBreakdown['sangat_miskin'] ?? 0),
|
||||
'miskin' => (int)($povertyBreakdown['miskin'] ?? 0),
|
||||
'rentan_miskin' => (int)($povertyBreakdown['rentan_miskin'] ?? 0),
|
||||
'terdata' => (int)($povertyBreakdown['terdata'] ?? 0),
|
||||
],
|
||||
'condition_breakdown' => [
|
||||
'layak' => (int)($conditionBreakdown['layak'] ?? 0),
|
||||
'tidak_layak' => (int)($conditionBreakdown['tidak_layak'] ?? 0),
|
||||
],
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'trend': {
|
||||
$rows = $pdo->query("
|
||||
SELECT
|
||||
DATE_FORMAT(created_at, '%Y-%m') AS month,
|
||||
COUNT(*) AS new_households,
|
||||
SUM(CASE WHEN aid_status = 'received' THEN 1 ELSE 0 END) AS aided,
|
||||
ROUND(AVG(poverty_score), 1) AS avg_score
|
||||
FROM households
|
||||
WHERE is_active = 1
|
||||
AND created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
|
||||
GROUP BY month
|
||||
ORDER BY month
|
||||
")->fetchAll();
|
||||
|
||||
foreach ($rows as &$r) {
|
||||
$r['new_households'] = (int)$r['new_households'];
|
||||
$r['aided'] = (int)$r['aided'];
|
||||
$r['avg_score'] = (float)$r['avg_score'];
|
||||
}
|
||||
unset($r);
|
||||
|
||||
Response::success(['trend' => $rows]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'poverty_chart': {
|
||||
$breakdown = $pdo->query("
|
||||
SELECT poverty_status, COUNT(*) AS count,
|
||||
ROUND(AVG(poverty_score), 1) AS avg_score
|
||||
FROM households WHERE is_active = 1
|
||||
GROUP BY poverty_status
|
||||
ORDER BY FIELD(poverty_status, 'sangat_miskin', 'miskin', 'rentan_miskin', 'terdata')
|
||||
")->fetchAll();
|
||||
|
||||
Response::success(['breakdown' => $breakdown]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'aid_chart': {
|
||||
$byType = $pdo->query("
|
||||
SELECT aid_type, COUNT(*) AS cnt, COALESCE(SUM(amount), 0) AS total_amount
|
||||
FROM aid_history
|
||||
GROUP BY aid_type
|
||||
ORDER BY cnt DESC
|
||||
")->fetchAll();
|
||||
|
||||
$total = (int)$pdo->query("SELECT COUNT(*) FROM aid_history")->fetchColumn();
|
||||
|
||||
Response::success([
|
||||
'by_type' => $byType,
|
||||
'summary' => ['total_distributions' => $total],
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'age_distribution': {
|
||||
$headQuery = "
|
||||
SELECT
|
||||
CASE
|
||||
WHEN TIMESTAMPDIFF(YEAR, head_date_of_birth, CURDATE()) < 12 THEN 'anak'
|
||||
WHEN TIMESTAMPDIFF(YEAR, head_date_of_birth, CURDATE()) BETWEEN 12 AND 17 THEN 'remaja'
|
||||
WHEN TIMESTAMPDIFF(YEAR, head_date_of_birth, CURDATE()) BETWEEN 18 AND 30 THEN 'pemuda'
|
||||
WHEN TIMESTAMPDIFF(YEAR, head_date_of_birth, CURDATE()) BETWEEN 31 AND 59 THEN 'dewasa'
|
||||
WHEN TIMESTAMPDIFF(YEAR, head_date_of_birth, CURDATE()) >= 60 THEN 'lansia'
|
||||
ELSE NULL
|
||||
END AS age_group
|
||||
FROM households
|
||||
WHERE is_active = 1 AND head_date_of_birth IS NOT NULL
|
||||
";
|
||||
|
||||
$memberQuery = "
|
||||
SELECT
|
||||
CASE
|
||||
WHEN TIMESTAMPDIFF(YEAR, hm.date_of_birth, CURDATE()) < 12 THEN 'anak'
|
||||
WHEN TIMESTAMPDIFF(YEAR, hm.date_of_birth, CURDATE()) BETWEEN 12 AND 17 THEN 'remaja'
|
||||
WHEN TIMESTAMPDIFF(YEAR, hm.date_of_birth, CURDATE()) BETWEEN 18 AND 30 THEN 'pemuda'
|
||||
WHEN TIMESTAMPDIFF(YEAR, hm.date_of_birth, CURDATE()) BETWEEN 31 AND 59 THEN 'dewasa'
|
||||
WHEN TIMESTAMPDIFF(YEAR, hm.date_of_birth, CURDATE()) >= 60 THEN 'lansia'
|
||||
ELSE NULL
|
||||
END AS age_group
|
||||
FROM household_members hm
|
||||
INNER JOIN households h ON h.id = hm.household_id
|
||||
WHERE h.is_active = 1 AND hm.date_of_birth IS NOT NULL
|
||||
";
|
||||
|
||||
$combinedQuery = "
|
||||
SELECT age_group, COUNT(*) as total
|
||||
FROM (
|
||||
$headQuery
|
||||
UNION ALL
|
||||
$memberQuery
|
||||
) AS all_persons
|
||||
WHERE age_group IS NOT NULL
|
||||
GROUP BY age_group
|
||||
";
|
||||
|
||||
$result = $pdo->query($combinedQuery);
|
||||
$rows = $result->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
// Inisialisasi array dengan nilai default 0
|
||||
$distribution = [
|
||||
'anak' => 0, // < 12 tahun
|
||||
'remaja' => 0, // 12-17 tahun
|
||||
'pemuda' => 0, // 18-30 tahun
|
||||
'dewasa' => 0, // 31-59 tahun
|
||||
'lansia' => 0, // >= 60 tahun
|
||||
'unknown' => 0, // data tanggal lahir tidak tersedia
|
||||
];
|
||||
|
||||
// Hitung jumlah orang tanpa tanggal lahir
|
||||
$unknownCount = 0;
|
||||
|
||||
// Hitung kepala keluarga tanpa tanggal lahir
|
||||
$stmt = $pdo->query("SELECT COUNT(*) FROM households WHERE is_active = 1 AND head_date_of_birth IS NULL");
|
||||
$unknownCount += (int)$stmt->fetchColumn();
|
||||
|
||||
// Hitung anggota keluarga tanpa tanggal lahir
|
||||
$stmt = $pdo->query("
|
||||
SELECT COUNT(*) FROM household_members hm
|
||||
INNER JOIN households h ON h.id = hm.household_id
|
||||
WHERE h.is_active = 1 AND hm.date_of_birth IS NULL
|
||||
");
|
||||
$unknownCount += (int)$stmt->fetchColumn();
|
||||
$distribution['unknown'] = $unknownCount;
|
||||
|
||||
// Isi hasil query ke array distribution
|
||||
foreach ($rows as $row) {
|
||||
if (isset($distribution[$row['age_group']])) {
|
||||
$distribution[$row['age_group']] = (int)$row['total'];
|
||||
}
|
||||
}
|
||||
|
||||
// Hitung total keseluruhan untuk debugging (opsional)
|
||||
$totalPeople = array_sum($distribution) - $distribution['unknown'];
|
||||
|
||||
Response::success([
|
||||
'age_distribution' => $distribution,
|
||||
'total_people' => $totalPeople,
|
||||
'includes_members' => true // Indikator bahwa sudah termasuk anggota keluarga
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Response::error('Unknown stats action.', 400);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user