"ALTER TABLE penduduk_miskin ADD COLUMN kategori_kebutuhan VARCHAR(100) NULL AFTER status_bantuan", 'deskripsi_kebutuhan' => "ALTER TABLE penduduk_miskin ADD COLUMN deskripsi_kebutuhan TEXT NULL AFTER kategori_kebutuhan", 'tindak_lanjut' => "ALTER TABLE penduduk_miskin ADD COLUMN tindak_lanjut VARCHAR(60) NOT NULL DEFAULT 'Belum disurvei' AFTER deskripsi_kebutuhan", 'kondisi_rumah' => "ALTER TABLE penduduk_miskin ADD COLUMN kondisi_rumah VARCHAR(80) NULL AFTER tindak_lanjut", 'penghasilan' => "ALTER TABLE penduduk_miskin ADD COLUMN penghasilan INT NULL AFTER kondisi_rumah", 'catatan_survei' => "ALTER TABLE penduduk_miskin ADD COLUMN catatan_survei TEXT NULL AFTER penghasilan", 'bukti_file' => "ALTER TABLE penduduk_miskin ADD COLUMN bukti_file VARCHAR(255) NULL AFTER catatan_survei", 'skor_prioritas' => "ALTER TABLE penduduk_miskin ADD COLUMN skor_prioritas INT NOT NULL DEFAULT 0 AFTER bukti_file", ]; foreach ($columns as $column => $sql) { $stmt = $pdo->query("SHOW COLUMNS FROM penduduk_miskin LIKE " . $pdo->quote($column)); if ($stmt->fetch() === false) { $pdo->exec($sql); } } $pdo->exec(" CREATE TABLE IF NOT EXISTS riwayat_bantuan ( id INT AUTO_INCREMENT PRIMARY KEY, penduduk_id INT NOT NULL, jenis_bantuan VARCHAR(120) NOT NULL, tanggal_bantuan DATE NOT NULL, nilai_bantuan VARCHAR(80) NULL, pemberi_bantuan VARCHAR(120) NULL, catatan TEXT NULL, bukti_file VARCHAR(255) NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (penduduk_id) REFERENCES penduduk_miskin(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci "); } function normalizeDateOrNull(?string $value): ?string { $value = trim((string)$value); return $value === '' || $value === '0000-00-00' ? null : $value; } function normalizeNonNegativeIntOrNull(mixed $value, string $label): ?int { $value = trim((string)$value); if ($value === '') { return null; } if (!is_numeric($value) || (float)$value < 0) { throw new InvalidArgumentException($label . ' tidak boleh bernilai negatif.'); } return (int)$value; } function calculatePriorityScore(array $penduduk, array $anggota = []): int { $score = 0; $anggotaCount = count($anggota); $penghasilan = isset($penduduk['penghasilan']) ? (int)$penduduk['penghasilan'] : 0; if (($penduduk['status_bantuan'] ?? '') === 'Belum dibantu') $score += 30; if (($penduduk['status_bantuan'] ?? '') === 'Menunggu verifikasi') $score += 15; if ((int)($penduduk['is_blank_spot'] ?? 0) === 1) $score += 15; $score += min(20, $anggotaCount * 4); foreach ($anggota as $a) { $umur = isset($a['umur']) ? (int)$a['umur'] : null; if ($umur !== null && ($umur < 6 || $umur >= 60)) { $score += 5; } } if ($penghasilan > 0 && $penghasilan <= 1000000) $score += 15; elseif ($penghasilan > 1000000 && $penghasilan <= 2000000) $score += 8; $kondisi = strtolower((string)($penduduk['kondisi_rumah'] ?? '')); if (str_contains($kondisi, 'tidak layak')) $score += 15; elseif (str_contains($kondisi, 'rentan')) $score += 8; $kebutuhan = strtolower((string)($penduduk['kategori_kebutuhan'] ?? '')); if (in_array($kebutuhan, ['kesehatan', 'rumah tidak layak', 'bantuan darurat'], true)) $score += 10; return min(100, max(0, $score)); } function getPriorityLabel(int $score): string { if ($score >= 70) return 'Prioritas Tinggi'; if ($score >= 40) return 'Prioritas Sedang'; return 'Prioritas Rendah'; } function saveUploadedEvidence(string $fieldName): ?string { if (empty($_FILES[$fieldName]) || !is_array($_FILES[$fieldName])) { return null; } $file = $_FILES[$fieldName]; if (($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) { return null; } if (($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) { throw new RuntimeException('Upload bukti gagal.'); } if (($file['size'] ?? 0) > 3 * 1024 * 1024) { throw new RuntimeException('Ukuran bukti maksimal 3 MB.'); } $allowed = [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'application/pdf' => 'pdf', ]; $mime = mime_content_type($file['tmp_name']); if (!isset($allowed[$mime])) { throw new RuntimeException('Format bukti harus JPG, PNG, WEBP, atau PDF.'); } $dir = __DIR__ . '/../uploads/bukti'; if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) { throw new RuntimeException('Folder upload tidak bisa dibuat.'); } $filename = date('YmdHis') . '-' . bin2hex(random_bytes(6)) . '.' . $allowed[$mime]; $target = $dir . '/' . $filename; if (!move_uploaded_file($file['tmp_name'], $target)) { throw new RuntimeException('Bukti tidak bisa disimpan.'); } return 'uploads/bukti/' . $filename; } function readPendudukInput(): array { if (str_starts_with((string)($_SERVER['CONTENT_TYPE'] ?? ''), 'multipart/form-data')) { $input = $_POST; $input['anggota'] = json_decode((string)($_POST['anggota'] ?? '[]'), true) ?: []; return $input; } return json_decode(file_get_contents('php://input'), true) ?: []; } function updatePendudukPriority(PDO $pdo, int $pendudukId): void { $stmt = $pdo->prepare("SELECT * FROM penduduk_miskin WHERE id = ?"); $stmt->execute([$pendudukId]); $penduduk = $stmt->fetch(PDO::FETCH_ASSOC); if (!$penduduk) return; $stmtAnggota = $pdo->prepare("SELECT * FROM anggota_keluarga WHERE penduduk_id = ?"); $stmtAnggota->execute([$pendudukId]); $anggota = $stmtAnggota->fetchAll(PDO::FETCH_ASSOC); $score = calculatePriorityScore($penduduk, $anggota); $update = $pdo->prepare("UPDATE penduduk_miskin SET skor_prioritas = ? WHERE id = ?"); $update->execute([$score, $pendudukId]); } function refreshAllPriorityScores(PDO $pdo): void { ensurePendudukFeatureSchema($pdo); $ids = $pdo->query("SELECT id FROM penduduk_miskin")->fetchAll(PDO::FETCH_COLUMN); foreach ($ids as $id) { updatePendudukPriority($pdo, (int)$id); } }