get(); $sudahDihitung = HasilSaw::where('periode_id', $periodeId)->exists(); $wargaDinilaiIds = DB::table('penilaian_warga') ->select('warga_id') ->where('periode_id', $periodeId) ->distinct() ->pluck('warga_id'); $wargaDinilai = Warga::with(['penilaian' => function ($q) use ($periodeId) { $q->where('periode_id', $periodeId)->with('kriteria'); }]) ->whereIn('id', $wargaDinilaiIds) ->orderBy('nama') ->get(); return view('admin.saw.index', compact('periode', 'kriteria', 'wargaDinilai', 'sudahDihitung')); } public function hitung($periodeId) { $periode = PeriodeSeleksi::findOrFail($periodeId); $kriteria = Kriteria::orderBy('kode')->get(); // ✅ BUG #5 FIX: Blokir hitung ulang jika periode sudah selesai/arsip if (in_array($periode->status, ['selesai', 'arsip'])) { return back()->with('error', 'Periode ini sudah ditutup dan tidak bisa dihitung ulang. Buat periode baru untuk seleksi berikutnya.'); } if ($kriteria->count() === 0) { return back()->with('error', 'Data kriteria belum ada. Silakan hubungi Superadmin untuk menambahkan kriteria.'); } // Validasi total bobot kriteria harus tepat 1.00 $totalBobot = Kriteria::sum('bobot'); if (abs($totalBobot - 1.00) > 0.001) { return back()->with('error', 'Perhitungan SAW tidak dapat dilanjutkan karena total bobot kriteria saat ini adalah ' . number_format($totalBobot, 2) . '. Total bobot harus bernilai tepat 1.00. Silakan hubungi Superadmin untuk menyesuaikan.'); } $wargaDinilaiIds = DB::table('penilaian_warga') ->select('warga_id') ->where('periode_id', $periodeId) ->distinct() ->pluck('warga_id'); if ($wargaDinilaiIds->isEmpty()) { return back()->with('error', 'Belum ada data penilaian untuk periode ini.'); } // ✅ BUG #4 FIX: Pre-fetch semua max/min per kriteria sebelum loop (cegah N+1) $statPerKriteria = PenilaianWarga::where('periode_id', $periodeId) ->groupBy('kriteria_id') ->selectRaw('kriteria_id, MAX(nilai) as max_val, MIN(nilai) as min_val') ->get() ->keyBy('kriteria_id'); // Hapus hasil sebelumnya HasilSaw::where('periode_id', $periodeId)->delete(); DB::beginTransaction(); try { // Pre-fetch semua penilaian untuk periode ini (cegah N+1 per warga) $semuaPenilaian = PenilaianWarga::where('periode_id', $periodeId) ->get() ->groupBy('warga_id'); foreach ($wargaDinilaiIds as $wargaId) { $penilaian = isset($semuaPenilaian[$wargaId]) ? $semuaPenilaian[$wargaId]->keyBy('kriteria_id') : collect(); // Skip warga dengan data tidak lengkap if ($penilaian->count() < $kriteria->count()) { Log::warning("Warga ID {$wargaId} hanya punya {$penilaian->count()} dari {$kriteria->count()} kriteria. Dilewati."); continue; } $hasilSaw = HasilSaw::create([ 'warga_id' => $wargaId, 'periode_id' => $periodeId, 'skor_akhir' => 0, 'ranking' => 0, 'status' => 'tidak_penerima', ]); $skorTotal = 0; foreach ($kriteria as $k) { if (!isset($penilaian[$k->id])) continue; $nilaiAsli = $penilaian[$k->id]->nilai; $stat = $statPerKriteria[$k->id] ?? null; if ($k->jenis === 'benefit') { $nilaiMax = $stat ? $stat->max_val : 0; $nilaiNormalisasi = $nilaiMax > 0 ? $nilaiAsli / $nilaiMax : 0; } else { $nilaiMin = $stat ? $stat->min_val : 0; $nilaiNormalisasi = $nilaiAsli > 0 ? $nilaiMin / $nilaiAsli : 0; } $nilaiTerbobot = $nilaiNormalisasi * $k->bobot; $skorTotal += $nilaiTerbobot; DetailNormalisasi::create([ 'hasil_saw_id' => $hasilSaw->id, 'kriteria_id' => $k->id, 'nilai_asli' => $nilaiAsli, 'nilai_normalisasi' => $nilaiNormalisasi, 'nilai_terbobot' => $nilaiTerbobot, ]); } $hasilSaw->update(['skor_akhir' => $skorTotal]); } DB::commit(); } catch (\Exception $e) { DB::rollBack(); Log::error('Error hitung SAW: ' . $e->getMessage()); return back()->with('error', 'Terjadi kesalahan saat perhitungan: ' . $e->getMessage()); } return redirect()->route('admin.saw.hasil', $periode) ->with('success', 'Perhitungan SAW berhasil. Silakan lihat hasil dan tetapkan penerima.'); } public function hasil($periodeId) { $periode = PeriodeSeleksi::findOrFail($periodeId); $hasilRaw = $this->getHasilWithTieBreaking($periodeId); // Update ranking $ranking = 1; foreach ($hasilRaw as $h) { HasilSaw::where('id', $h->id)->update(['ranking' => $ranking++]); } $hasil = HasilSaw::where('periode_id', $periodeId) ->with(['warga.wilayah', 'detailNormalisasi.kriteria']) ->orderBy('ranking') ->get(); $kriteria = Kriteria::orderBy('kode')->get(); $totalPenerima = HasilSaw::where('periode_id', $periodeId)->where('status', 'penerima')->count(); $totalCadangan = HasilSaw::where('periode_id', $periodeId)->where('status', 'cadangan')->count(); return view('admin.saw.hasil', compact('periode', 'hasil', 'kriteria', 'totalPenerima', 'totalCadangan')); } public function tetapkan($periodeId) { $periode = PeriodeSeleksi::findOrFail($periodeId); $hasil = $this->getHasilWithTieBreaking($periodeId); $kuota = $periode->kuota_penerima; // ✅ BUG #2 FIX: Baca kuota cadangan dari database, bukan hardcoded $kuotaCadangan = $periode->kuota_cadangan ?? 5; $ranking = 1; foreach ($hasil as $h) { if ($ranking <= $kuota) { $status = 'penerima'; } elseif ($ranking <= $kuota + $kuotaCadangan) { $status = 'cadangan'; } else { $status = 'tidak_penerima'; } HasilSaw::where('id', $h->id)->update([ 'ranking' => $ranking++, 'status' => $status, ]); } $periode->update(['status' => 'selesai']); return redirect()->route('admin.saw.hasil', $periode) ->with('success', 'Penerima berhasil ditetapkan dengan mekanisme tie-breaking.'); } /** * Helper: Ambil hasil SAW dengan tie-breaking * ✅ BUG #1 FIX: Gunakan kode kriteria (C1, C2, C7) bukan hardcoded ID */ private function getHasilWithTieBreaking($periodeId) { // Ambil ID berdasarkan kode — aman jika DB di-reset $idC5 = Kriteria::where('kode', 'C5')->value('id') ?? 0; $idC1 = Kriteria::where('kode', 'C1')->value('id') ?? 0; $idC2 = Kriteria::where('kode', 'C2')->value('id') ?? 0; return DB::table('hasil_saw as hs') ->join('warga as w', 'hs.warga_id', '=', 'w.id') ->leftJoin('penilaian_warga as p5', function ($join) use ($periodeId, $idC5) { $join->on('hs.warga_id', '=', 'p5.warga_id') ->where('p5.periode_id', '=', $periodeId) ->where('p5.kriteria_id', '=', $idC5); }) ->leftJoin('penilaian_warga as p1', function ($join) use ($periodeId, $idC1) { $join->on('hs.warga_id', '=', 'p1.warga_id') ->where('p1.periode_id', '=', $periodeId) ->where('p1.kriteria_id', '=', $idC1); }) ->leftJoin('penilaian_warga as p2', function ($join) use ($periodeId, $idC2) { $join->on('hs.warga_id', '=', 'p2.warga_id') ->where('p2.periode_id', '=', $periodeId) ->where('p2.kriteria_id', '=', $idC2); }) ->where('hs.periode_id', $periodeId) ->select('hs.*', 'w.nama', 'w.dusun', 'w.rw', 'p5.nilai as c5_nilai', 'p1.nilai as c1_nilai', 'p2.nilai as c2_nilai') ->orderBy('hs.skor_akhir', 'desc') ->orderByRaw('COALESCE(p5.nilai, 999) ASC') ->orderByRaw('COALESCE(p1.nilai, 999) ASC') ->orderByRaw('COALESCE(p2.nilai, 0) DESC') ->orderBy('hs.created_at', 'asc') ->get(); } // ✅ BUG #7 FIX: Validasi status periode sebelum export PDF public function exportPdf($periodeId) { $periode = PeriodeSeleksi::findOrFail($periodeId); if ($periode->status !== 'selesai') { return back()->with('error', 'Export PDF hanya tersedia setelah periode ditetapkan (status: selesai).'); } $kriteria = Kriteria::orderBy('kode')->get(); $hasil = HasilSaw::where('periode_id', $periodeId) ->with(['warga.wilayah', 'detailNormalisasi.kriteria']) ->orderBy('ranking') ->get(); $pdf = Pdf::loadView('admin.saw.pdf', compact('periode', 'hasil', 'kriteria')) ->setPaper('a4', 'portrait') ->setOptions([ 'defaultFont' => 'DejaVu Sans', 'isRemoteEnabled' => false, 'isHtml5ParserEnabled' => true, ]); $filename = 'Hasil-SAW-' . str_replace(' ', '-', $periode->nama) . '.pdf'; return $pdf->download($filename); } }