MIF_E31231754/app/Http/Controllers/Admin/SawController.php

271 lines
10 KiB
PHP

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\PeriodeSeleksi;
use App\Models\Warga;
use App\Models\Kriteria;
use App\Models\PenilaianWarga;
use App\Models\HasilSaw;
use App\Models\DetailNormalisasi;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Barryvdh\DomPDF\Facade\Pdf;
class SawController extends Controller
{
public function index($periodeId)
{
$periode = PeriodeSeleksi::findOrFail($periodeId);
$kriteria = Kriteria::orderBy('kode')->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);
}
}