338 lines
14 KiB
PHP
338 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Exports\HasilSeleksiExport;
|
|
use App\Models\HasilSeleksi;
|
|
use App\Models\Kriteria;
|
|
use App\Models\Periode;
|
|
use App\Models\SubKriteria;
|
|
use App\Models\Warga;
|
|
use App\Models\WargaDetail;
|
|
use Barryvdh\DomPDF\Facade\Pdf;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Maatwebsite\Excel\Facades\Excel;
|
|
|
|
class SeleksiController extends Controller
|
|
{
|
|
public function index(Request $request)
|
|
{
|
|
$periode = Periode::where('is_aktif', true)->first();
|
|
|
|
// Ambil daftar warga yang sudah disetujui untuk ditampilkan di pilihan checkbox (Targeted)
|
|
$wargaSiap = Warga::where('status_verifikasi', 'setuju')->orderBy('nama', 'asc')->get();
|
|
$jumlahWargaSiap = $wargaSiap->count();
|
|
|
|
// Inisialisasi default
|
|
$results = collect();
|
|
$mean = 0;
|
|
$countUtama = 0;
|
|
$countCadangan = 0;
|
|
$countTidakLayak = 0;
|
|
$countDialihkan = 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('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');
|
|
|
|
// 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,
|
|
'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)
|
|
{
|
|
// 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');
|
|
}
|
|
}
|