first(); // Ambil daftar warga yang sudah disetujui untuk ditampilkan di pilihan checkbox (Targeted) $wargaSiap = Warga::where('status_verifikasi', 'setuju') ->where('status_bansos_riil', 'aktif') // <-- TAMBAHAN FILTER ->orderBy('nama', 'asc') ->get(); $jumlahWargaSiap = $wargaSiap->count(); // Inisialisasi default $results = collect(); $mean = 0; $countUtama = 0; $countCadangan = 0; $countTidakLayak = 0; $countDialihkan = 0; $passingGrade = 0; // --- VARIABEL UNTUK CEK DATA USANG (STALE DATA) --- $butuhHitungUlang = false; $perPage = $request->input('per_page', 10); $search = $request->input('search'); if ($periode) { // --- HITUNG STATISTIK BERDASARKAN HASIL SELEKSI --- $statsQuery = HasilSeleksi::where('periode_id', $periode->id); $mean = (clone $statsQuery)->avg('skor_akhir') ?? 0; $countUtama = (clone $statsQuery)->where('status_keputusan', 'Layak Prioritas Utama')->count(); $countCadangan = (clone $statsQuery)->where('status_keputusan', 'Layak Cadangan')->count(); $countTidakLayak = (clone $statsQuery)->where('status_keputusan', 'Tidak Layak')->count(); $countDialihkan = (clone $statsQuery)->where('status_keputusan', 'Dialihkan (Pemerataan)')->count(); $passingGrade = HasilSeleksi::avg('skor_akhir') ?? 0; // --- QUERY UNTUK MENAMPILKAN TABEL HASIL --- $query = HasilSeleksi::with('warga')->where('periode_id', $periode->id); if ($search) { $query->whereHas('warga', function ($q) use ($search) { $q->where('nama', 'like', "%{$search}%") ->orWhere('nik', 'like', "%{$search}%"); }); } $results = $query->orderBy('ranking', 'asc')->paginate($perPage); $results->appends(['search' => $search, 'per_page' => $perPage]); // --- LOGIKA PENGECEKAN DATA BERUBAH (STALE DATA) --- // Cari waktu terakhir kali sistem melakukan perhitungan (created_at terakhir di HasilSeleksi) $terakhirDihitung = (clone $statsQuery)->max('created_at'); if ($terakhirDihitung) { // 1. Cek apakah ada profil utama warga yang diupdate setelah perhitungan terakhir $profilBerubah = Warga::where('status_verifikasi', 'setuju') ->where('status_bansos_riil', 'aktif') // <-- TAMBAHAN FILTER ->where('updated_at', '>', $terakhirDihitung) ->exists(); // 2. Cek apakah ada data kriteria/pendapatan (WargaDetail) yang diupdate setelah perhitungan terakhir // Kita pakai whereHas untuk memastikan hanya mengecek detail milik warga yang statusnya 'setuju' $detailBerubah = WargaDetail::whereHas('warga', function ($q) { $q->where('status_verifikasi', 'setuju'); }) ->where('updated_at', '>', $terakhirDihitung) ->exists(); // Jika salah satu saja ada yang berubah, maka munculkan peringatan hitung ulang if ($profilBerubah || $detailBerubah) { $butuhHitungUlang = true; } } } return view('seleksi.index', compact( 'periode', 'results', 'mean', 'jumlahWargaSiap', 'wargaSiap', 'search', 'perPage', 'countUtama', 'countCadangan', 'countTidakLayak', 'countDialihkan', 'butuhHitungUlang', 'passingGrade' )); } public function prosesHitung(Request $request) { // 1. Inisialisasi & Validasi Parameter Awal // Mencari periode yang sedang aktif untuk memastikan hasil perhitungan tersimpan di konteks waktu yang benar. $periode = Periode::where('is_aktif', true)->first(); if (! $periode) { return redirect()->back()->with('error', 'Gagal! Tidak ada periode yang sedang aktif.'); } // Mengambil input mode perhitungan (murni/pemerataan) dan cakupan target warga (semua/pilihan manual). $modeHitung = $request->input('mode_hitung', 'murni'); $targetWarga = $request->input('target_warga', 'semua'); // 2. Validasi Kesiapan Bobot F-AHP (Constraint Check) // Memastikan pakar telah menyelesaikan perbandingan matriks. // SPK tidak bisa berjalan jika bobot (weight) bernilai 0 karena perhitungan akan menjadi tidak valid/bias. $cekKriteria = Kriteria::where('bobot_global', 0)->orWhereNull('bobot_global')->count(); $cekSub = SubKriteria::where('bobot_lokal', 0)->orWhereNull('bobot_lokal')->count(); if ($cekKriteria > 0 || $cekSub > 0) { return redirect()->back()->with('error', 'Gagal! Bobot F-AHP belum lengkap.'); } // 3. Data Acquisition (Pengambilan Data Warga) // Hanya mengambil warga dengan status 'setuju' (yang sudah lolos verifikasi lapangan). $queryWarga = Warga::with(['details.kriteria', 'details.subKriteria']) ->where('status_verifikasi', 'setuju') ->where('status_bansos_riil', 'aktif'); // <-- TAMBAHAN FILTER // Jika user memilih mode 'Targeted', filter data berdasarkan ID yang dipilih saja. if ($targetWarga == 'pilih') { $request->validate([ 'selected_warga' => 'required|array|min:1', ], ['selected_warga.required' => 'Pilih minimal satu warga!']); $queryWarga->whereIn('id', $request->selected_warga); } $wargas = $queryWarga->get(); if ($wargas->isEmpty()) { return redirect()->back()->with('error', 'Tidak ada data warga yang valid.'); } try { // Menggunakan Database Transaction untuk menjamin integritas data (atomicity). // Jika proses gagal di tengah jalan, data lama tidak akan rusak/hilang. DB::beginTransaction(); // Reset data lama pada periode berjalan sebelum menulis hasil perhitungan baru. HasilSeleksi::where('periode_id', $periode->id)->delete(); // 4. Tahap Analisis Historis (Untuk Mode Pemerataan) // Mengidentifikasi warga yang sudah menerima bantuan di periode sebelumnya // untuk mengimplementasikan prinsip keadilan distributif (agar bantuan tidak menumpuk di satu pihak). $penerimaLalu = []; if ($modeHitung == 'pemerataan') { $periodeLalu = Periode::where('id', '!=', $periode->id)->latest('id')->first(); if ($periodeLalu) { $penerimaLalu = HasilSeleksi::where('periode_id', $periodeLalu->id) ->where('status_keputusan', 'Layak Prioritas Utama') ->pluck('warga_id') ->toArray(); } } // 5. Tahap Matematis: Perhitungan Skor Akhir (Aggregation) // Menggunakan rumus V = Σ (W * w) sesuai kaidah Fuzzy AHP. // W = Bobot Global Kriteria, w = Bobot Lokal Sub-Kriteria. $tempHasil = []; foreach ($wargas as $w) { $totalSkor = 0; foreach ($w->details as $detail) { $bobotGlobal = $detail->kriteria->bobot_global ?? 0; $bobotLokal = $detail->subKriteria->bobot_lokal ?? 0; $totalSkor += ($bobotGlobal * $bobotLokal); } $tempHasil[] = [ 'warga_id' => $w->id, // 'skor' => $totalSkor, 'skor' => round($totalSkor, 4), 'nik' => $w->nik, ]; } // 6. Tahap Sorting & Penentuan Benchmark (Ranking & Statistics) // Mengurutkan dari skor tertinggi (paling membutuhkan) ke terendah. // Jika skor sama, NIK digunakan sebagai tie-breaker agar hasil ranking tetap deterministik. $sortedResults = collect($tempHasil)->sort(function ($a, $b) { if ($a['skor'] === $b['skor']) { return $a['nik'] <=> $b['nik']; } return $b['skor'] <=> $a['skor']; })->values(); // Menghitung rata-rata lokal untuk klasifikasi kelompok 'Layak Cadangan'. $rataRataLokal = $sortedResults->avg('skor'); // AMBIL PASSING GRADE GLOBAL (Threshold Adaptif) // Ini adalah rata-rata historis seluruh data yang pernah diseleksi. // Menjadi instrumen validasi agar warga yang 'mampu' tidak lolos meski kuota tersedia. $passingGrade = HasilSeleksi::avg('skor_akhir') ?? 0; // 7. Tahap Klasifikasi (Hybrid Decision Logic) // Menggabungkan metode ranking, batasan kuota, dan ambang batas (threshold) statistik. $kuotaTerpakai = 0; foreach ($sortedResults as $index => $res) { $ranking = $index + 1; $wargaId = $res['warga_id']; $skor = $res['skor']; // A. VALIDASI AMBANG BATAS (Objective Filter) // Jika skor di bawah standar kelayakan desa (Passing Grade), langsung divonis 'Tidak Layak'. if ($skor < $passingGrade && $passingGrade > 0) { $status = 'Tidak Layak'; } // B. VALIDASI PEMERATAAN (Social Fairness) // Jika warga sudah pernah dapat, status dialihkan untuk memberikan kesempatan pada yang lain. elseif ($modeHitung == 'pemerataan' && in_array($wargaId, $penerimaLalu)) { $status = 'Dialihkan (Pemerataan)'; } // C. VALIDASI KUOTA & RATA-RATA LOKAL (Resource Allocation) else { if ($kuotaTerpakai < $periode->kuota) { $status = 'Layak Prioritas Utama'; $kuotaTerpakai++; } elseif ($skor >= $rataRataLokal) { $status = 'Layak Cadangan'; } else { $status = 'Tidak Layak'; } } // Simpan hasil keputusan ke tabel hasil_seleksi. HasilSeleksi::create([ 'periode_id' => $periode->id, 'warga_id' => $wargaId, 'skor_akhir' => $skor, 'ranking' => $ranking, 'status_keputusan' => $status, ]); } DB::commit(); // Finalisasi penyimpanan data ke database. $msg = ($targetWarga == 'pilih') ? count($wargas).' warga berhasil dihitung.' : 'Seluruh warga berhasil diproses.'; return redirect()->route('seleksi.index')->with('success', $msg); } catch (\Exception $e) { DB::rollback(); // Batalkan semua perubahan jika terjadi error untuk menjaga konsistensi data. return redirect()->back()->with('error', 'Terjadi kesalahan sistem: '.$e->getMessage()); } } public function show($id) { $result = HasilSeleksi::with([ 'warga.details.kriteria', 'warga.details.subKriteria', ])->findOrFail($id); return view('seleksi.show', compact('result')); } public function exportExcel(Request $request) { // 1. Pastikan ada periode yang aktif $periode = Periode::where('is_aktif', true)->first(); if (!$periode) { return back()->with('error', 'Gagal Export! Periode aktif tidak ditemukan.'); } $status = $request->query('status'); $limit = $request->query('limit'); // 2. VALIDASI DATA: Cek apakah ada data yang akan di-export $checkQuery = HasilSeleksi::where('periode_id', $periode->id); if ($status) { $checkQuery->where('status_keputusan', $status); } if ($limit == 'kuota') { $checkQuery->take($periode->kuota); } // Jika hasil query nol, kembalikan dengan pesan error if ($checkQuery->count() === 0) { $pesan = $status ? "untuk kategori [$status]" : ""; return back()->with('error', "Gagal Export! Tidak ada data $pesan pada periode " . $periode->nama_periode); } // 3. Jalankan Export jika data tersedia return Excel::download( new HasilSeleksiExport($periode->id, $status, $limit), 'Hasil-Seleksi-'.$periode->nama_periode.'.xlsx' ); } public function exportPdf(Request $request) { // --- SUNTIKAN TENAGA UNTUK SERVER MENGATASI ERROR 500 --- ini_set('max_execution_time', 600); // Waktu maksimal diperpanjang jadi 10 menit set_time_limit(600); // Backup pengaturan waktu ini_set('memory_limit', '1024M'); // Kapasitas memori (RAM) diperbesar jadi 1 GB // -------------------------------------------------------- // 1. Pastikan ada periode yang aktif $periode = Periode::where('is_aktif', true)->first(); if (!$periode) { return back()->with('error', 'Gagal Export! Periode aktif tidak ditemukan.'); } $status = $request->query('status'); $limit = $request->query('limit'); // 2. Siapkan Query $query = HasilSeleksi::with('warga') ->where('periode_id', $periode->id) ->orderBy('ranking', 'asc'); if ($status) { $query->where('status_keputusan', $status); } if ($limit == 'kuota') { $query->take($periode->kuota); } // 3. VALIDASI DATA: Cek ketersediaan record $results = $query->get(); if ($results->isEmpty()) { $pesan = $status ? "untuk kategori [$status]" : ""; return back()->with('error', "Gagal Export PDF! Tidak ada data $pesan pada periode " . $periode->nama_periode); } // 4. Generate PDF jika data tersedia $pdf = Pdf::loadView('seleksi.pdf', compact('results', 'periode')); $pdf->setPaper('a4', 'potrait'); return $pdf->download('Laporan-Seleksi-'.$periode->nama_periode.'.pdf'); } }