271 lines
10 KiB
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);
|
|
}
|
|
}
|