MIF_E31230680/app/Http/Controllers/PerbandinganSubKriteriaCont...

648 lines
26 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Kriteria;
use App\Models\Pengaturan;
use App\Models\PerbandinganSub;
use App\Models\SubKriteria;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PerbandinganSubKriteriaController extends Controller
{
/**
* ==============================================================================
* FUNGSI 1: INDEX()
* Bertugas menampilkan daftar kriteria induk, dan jika sebuah kriteria dipilih,
* sistem akan membuatkan form kuesioner pasangan (pairwise) untuk anak-anaknya (Sub-Kriteria).
* ==============================================================================
*/
public function index(Request $request)
{
// 1. Ambil daftar kriteria induk untuk dropdown pilihan
$kriterias = Kriteria::orderBy('kode', 'asc')->get();
// Tangkap ID kriteria induk yang sedang dipilih oleh user di layar
$selectedKriteriaId = $request->input('kriteria_id');
$subKriterias = [];
$pairs = [];
$allData = [];
// Jika user sudah memilih kriteria induk di dropdown:
if ($selectedKriteriaId) {
// Ambil semua anak-anaknya (Sub-Kriteria) yang berinduk pada kriteria tersebut
$subKriterias = SubKriteria::where('kriteria_id', $selectedKriteriaId)
->orderBy('kode', 'asc')
->get();
// 2. Tahap pembuatan pasangan Pairwise (Algoritma Kombinasi)
// Sistem akan memasangkan setiap Sub-Kriteria secara otomatis.
// Contoh: Sub S1 vs S2, S1 vs S3, S2 vs S3.
$n = count($subKriterias);
for ($i = 0; $i < $n; $i++) {
for ($j = $i + 1; $j < $n; $j++) {
$pairs[] = [
'kiri' => $subKriterias[$i],
'kanan' => $subKriterias[$j],
];
}
}
// Ambil semua data jawaban kuesioner yang sudah tersimpan untuk sub-kriteria ini
$allData = PerbandinganSub::with(['subKiri', 'subKanan'])
->where('kriteria_id', $selectedKriteriaId)
->get();
}
// 3. Deteksi urutan pakar
$existingPakar = PerbandinganSub::where('kriteria_id', $selectedKriteriaId)
->distinct()
->pluck('pakar_ke')
->toArray();
if (empty($existingPakar)) {
$existingPakar = [1]; // Jika belum ada sama sekali, set default Pakar 1
}
sort($existingPakar);
// 4. Ambil PIN keamanan dari database
$pengaturan = Pengaturan::where('nama_pengaturan', 'pin_pakar')->first();
$pinSistem = $pengaturan ? $pengaturan->nilai_pengaturan : '123456';
return view('perbandingan_sub.index', compact(
'kriterias', 'selectedKriteriaId', 'subKriterias', 'pairs', 'existingPakar', 'allData', 'pinSistem'
));
}
/**
* ==============================================================================
* FUNGSI 2: SIMPAN()
* Menerima kiriman form kuesioner, menyimpannya, lalu memicu perhitungan Fuzzy AHP.
* ==============================================================================
*/
public function simpan(Request $request)
{
$pengaturan = Pengaturan::where('nama_pengaturan', 'pin_pakar')->first();
$pinSistem = $pengaturan ? $pengaturan->nilai_pengaturan : '123456';
// Validasi PIN dulu
if ($request->input('pin_rahasia') !== $pinSistem) {
return redirect()->back()->with('error', 'Akses Ditolak! PIN Keamanan salah.');
}
$pakarKe = $request->input('pakar_ke');
$kriteriaId = $request->input('kriteria_id');
$kiriIds = $request->input('sub_kiri_id');
$kananIds = $request->input('sub_kanan_id');
$nilaiSkalas= $request->input('nilai_skala');
if (! $kiriIds) {
return redirect()->back()->with('error', 'Tidak ada data disimpan.');
}
// CEK KONSISTENSI (CR) SEBELUM SIMPAN
$subKriteriaIds = SubKriteria::where('kriteria_id', $kriteriaId)->orderBy('kode', 'asc')->pluck('id')->toArray();
$n = count($subKriteriaIds);
// Buat matriks bayangan untuk dicek
$matrixTemp = array_fill(0, $n, array_fill(0, $n, 1.0));
$idToIndex = array_flip($subKriteriaIds);
foreach ($kiriIds as $index => $kiriId) {
$i = $idToIndex[$kiriId];
$j = $idToIndex[$kananIds[$index]];
$val = (float) $nilaiSkalas[$index];
$matrixTemp[$i][$j] = $val;
$matrixTemp[$j][$i] = 1.0 / $val;
}
$nilaiCR = $this->checkConsistency($matrixTemp, $n);
// Jika tidak konsisten (> 10%), tolak data!
if ($nilaiCR > 0.10) {
$crPersen = round($nilaiCR * 100, 2);
return redirect()->back()->with('error', "Gagal! Data Pakar $pakarKe Tidak Konsisten (CR = $crPersen%). Silakan perbaiki perbandingan.");
}
// selesai cek lanjut ke database
// Gunakan Transaction agar kalau error di tengah jalan, database batal menyimpan (Rollback)
DB::transaction(function () use ($pakarKe, $kriteriaId, $kiriIds, $kananIds, $nilaiSkalas) {
foreach ($kiriIds as $index => $kiriId) {
PerbandinganSub::updateOrCreate(
[
'pakar_ke' => $pakarKe,
'kriteria_id' => $kriteriaId,
'sub_kiri_id' => $kiriId,
'sub_kanan_id' => $kananIds[$index],
],
['nilai_skala' => $nilaiSkalas[$index]]
);
}
});
// Pemicu perhitungan matematis Fuzzy AHP setelah data berhasil masuk
$this->updateLocalWeights($kriteriaId);
return redirect()->back()->with('success', "Data Pakar $pakarKe berhasil disimpan.");
}
/**
* ==============================================================================
* FUNGSI 3: UPDATE LOCAL WEIGHTS (CORE FUZZY AHP)
* TIPS SIDANG: Sistem menjaga akurasi dengan tidak melakukan pembulatan round()
* selama proses menengah, untuk menghindari Compounding Error (Penumpukan Galat).
* ==============================================================================
*/
private function updateLocalWeights($kriteriaId)
{
$subs = SubKriteria::where('kriteria_id', $kriteriaId)->get();
$ids = $subs->pluck('id')->toArray();
$n = count($ids);
$allData = PerbandinganSub::where('kriteria_id', $kriteriaId)->get();
// if ($allData->isEmpty() || count($ids) < 2) {
// return;
// }
if ($n < 2 || $allData->isEmpty()) {
SubKriteria::where('kriteria_id', $kriteriaId)
->update(['bobot_lokal' => ($n == 1 ? 1.0 : 0)]);
return;
}
// TAHAP 1: KONSENSUS MULTI-PAKAR (GEOMETRIC MEAN)
$matrixConsensus = [];
foreach ($ids as $i) {
foreach ($ids as $j) {
if ($i == $j) {
$matrixConsensus[$i][$j] = [1.0, 1.0, 1.0];
continue;
}
$scores = $allData->where('sub_kiri_id', $i)->where('sub_kanan_id', $j)->pluck('nilai_skala')->toArray();
if (count($scores) > 0) {
// Rumus Rata-rata Geometrik (Akar pangkat n)
$gm = pow(array_product($scores), 1 / count($scores));
$matrixConsensus[$i][$j] = $this->mapToTfn($gm);
} else {
// Jika data ada di sisi sebaliknya (kanan vs kiri)
$revScores = $allData->where('sub_kiri_id', $j)->where('sub_kanan_id', $i)->pluck('nilai_skala')->toArray();
if (count($revScores) > 0) {
$gmRev = pow(array_product($revScores), 1 / count($revScores));
$tfnRev = $this->mapToTfn($gmRev);
// INVERS FUZZY: Dibalik urutannya (1/Upper, 1/Medium, 1/Lower).
// Dibiarkan float murni tanpa di round()!
$matrixConsensus[$i][$j] = [
1 / $tfnRev[2],
1 / $tfnRev[1],
1 / $tfnRev[0]
];
} else {
$matrixConsensus[$i][$j] = [1.0, 1.0, 1.0];
}
}
}
}
// TAHAP 2: NILAI SINTESIS FUZZY (Si)
$rowSums = [];
$totalSum = [0.0, 0.0, 0.0];
// 2A: Hitung total Baris (Row) dan Grand Total Matriks
foreach ($ids as $i) {
$rSum = [0.0, 0.0, 0.0];
foreach ($ids as $j) {
$rSum[0] += $matrixConsensus[$i][$j][0];
$rSum[1] += $matrixConsensus[$i][$j][1];
$rSum[2] += $matrixConsensus[$i][$j][2];
}
$rowSums[$i] = $rSum;
$totalSum[0] += $rSum[0];
$totalSum[1] += $rSum[1];
$totalSum[2] += $rSum[2];
}
// 2B: Hitung Si (Baris dikali Invers Grand Total)
$si = [];
foreach ($ids as $i) {
// Dibiarkan presisi tinggi (float) tanpa round()
$si[$i] = [
$rowSums[$i][0] / $totalSum[2],
$rowSums[$i][1] / $totalSum[1],
$rowSums[$i][2] / $totalSum[0],
];
}
// TAHAP 3: DERAJAT KEMUNGKINAN (V Matrix)
$vMatrix = [];
foreach ($ids as $i) {
foreach ($ids as $j) {
// Menghitung probabilitas perpotongan area segitiga antar Fuzzy
$vMatrix[$i][$j] = ($i == $j) ? 1.0 : $this->calculateV($si[$i], $si[$j]);
}
}
// TAHAP 4: MINIMUM DEGREE & NORMALISASI
$dPrime = [];
foreach ($ids as $i) {
$vals = [];
foreach ($ids as $j) {
if ($j !== $i) {
$vals[] = $vMatrix[$i][$j];
}
}
// Mencari irisan terkecil
$dPrime[$i] = count($vals) > 0 ? min($vals) : 0.0;
}
$sumDPrime = array_sum($dPrime);
// Tahap Normalisasi agar total seluruh bobot bernilai 1.0
foreach ($ids as $id) {
$finalWeight = ($sumDPrime > 0) ? ($dPrime[$id] / $sumDPrime) : 0.0;
// Simpan hasil akhir ke database dengan presisi tinggi (6 angka belakang koma) aslinya 6 diganti 4
SubKriteria::where('id', $id)->update(['bobot_lokal' => round($finalWeight, 4)]);
}
}
/**
* ==============================================================================
* FUNGSI RUMUS V (PERPOTONGAN ORDINAT METODE CHANG)
* ==============================================================================
*/
private function calculateV(array $si_baris, array $si_kolom): float
{
[$l1, $m1, $u1] = $si_kolom; // Segitiga Kolom
[$l2, $m2, $u2] = $si_baris; // Segitiga Baris
// Jika nilai Puncak baris >= kolom, kemungkinan = 100% (1.0)
if ($m2 >= $m1) return 1.0;
// Jika dasar kolom lebih tinggi dari ujung atas baris = 0%
if ($l1 >= $u2) return 0.0;
$num = $l1 - $u2; // Pembilang
$den = ($m2 - $u2) - ($m1 - $l1); // Penyebut
// Dibiarkan tanpa round() agar presisi matematis terjaga
return ($den == 0) ? 0.0 : ($num / $den);
}
/**
* ==============================================================================
* FUNGSI MAPPING TFN (SKALA SAATY KE FUZZY)
* ==============================================================================
*/
private function mapToTfn($val): array
{
$baseTFN = [
1 => [1.0, 1.0, 1.0], 2 => [0.5, 1.0, 1.5], 3 => [1.0, 1.5, 2.0],
4 => [1.5, 2.0, 2.5], 5 => [2.0, 2.5, 3.0], 6 => [2.5, 3.0, 3.5],
7 => [3.0, 3.5, 4.0], 8 => [3.5, 4.0, 4.5], 9 => [4.0, 4.5, 4.5],
];
if ($val >= 0.99 && $val <= 1.01) return $baseTFN[1];
if ($val > 1) {
$key = min(9, (int) round($val));
return $baseTFN[$key];
} else {
$originalKey = max(1, min(9, (int) round(1 / $val)));
$tfn = $baseTFN[$originalKey];
// Dibiarkan presisi tinggi (float) tanpa round()
return [1 / $tfn[2], 1 / $tfn[1], 1 / $tfn[0]];
}
}
/**
* ==============================================================================
* FUNGSI ALGORITMA SKALA 1000 (LARGEST REMAINDER METHOD)
* ==============================================================================
* TIPS SIDANG: Ini digunakan untuk menahan agar parameter dengan kepentingan
* terendah sekalipun tetap memiliki bobot > 0, dan total akhir pasti pas 1000.
*/
private function toBobot1000(array $weights): array
{
$keys = array_keys($weights);
$float = array_map(fn($w) => $w * 1000, $weights);
$result = [];
// Pembulatan ke bawah dengan penahan Minimum 1
foreach ($float as $key => $val) {
$result[$key] = max(1, floor($val));
}
// Cari selisih kurangnya
$remainder = 1000 - (int) array_sum($result);
if ($remainder > 0) {
$sortedKeys = $keys;
usort($sortedKeys, function ($a, $b) use ($float) {
$decA = $float[$a] - floor($float[$a]);
$decB = $float[$b] - floor($float[$b]);
return $decB <=> $decA; // Urutkan sisa desimal terbesar ke terkecil
});
// Berikan sisa +1 ke kriteria yang pecahannya paling besar
for ($k = 0; $k < $remainder; $k++) {
$result[$sortedKeys[$k]] += 1;
}
}
return array_map('intval', $result);
}
/**
* ==============================================================================
* FUNGSI DETAIL (UNTUK HALAMAN LAPORAN / CETAK HTML & PDF)
* ==============================================================================
* TIPS SIDANG: Fungsi ini isinya adalah replika persis (copy-paste logika) dari
* fungsi updateLocalWeights(). Bedanya, updateLocalWeights() memproses rahasia di background.
* Sedangkan fungsi detail() menangkap setiap langkah prosesnya untuk dicetak ke layar (transparansi).
*/
public function detail(Request $request)
{
// 1. Menangkap ID Kriteria Induk dari URL (jika ada)
$selectedKriteriaId = $request->input('kriteria_id');
$kriterias = Kriteria::orderBy('kode', 'asc')->get();
// Inisialisasi collection kosong agar View tidak error (Undefined Variable) saat kriteria belum dipilih
$allData = collect([]);
// Jika user belum memilih Kriteria Induk, kembalikan ke layar pemilihan
if (! $selectedKriteriaId) {
return view('perbandingan_sub.detail', compact('kriterias', 'selectedKriteriaId', 'allData'));
}
// 2. Ambil data Induk dan Anak-anaknya (Sub-Kriteria)
$kriteriaInduk = Kriteria::findOrFail($selectedKriteriaId);
$subs = SubKriteria::where('kriteria_id', $selectedKriteriaId)->orderBy('kode', 'asc')->get();
// Pluck berfungsi mengekstrak 1 kolom spesifik jadi array tunggal. Contoh: ['S1', 'S2', 'S3']
$kodes = $subs->pluck('kode')->toArray();
// 3. Ambil data mentah kuesioner yang sudah diisi oleh semua pakar
$allData = PerbandinganSub::with(['subKiri', 'subKanan'])
->where('kriteria_id', $selectedKriteriaId)->get();
if ($allData->isEmpty()) {
return redirect()->route('perbandingansub.index', ['kriteria_id' => $selectedKriteriaId])
->with('error', 'Data perbandingan untuk kriteria ini masih kosong!');
}
// 4. MAP TO TFN (Fuzzifikasi Awal)
// Menyuntikkan array TFN (l, m, u) ke dalam setiap jawaban pakar agar bisa ditampillkan di tabel ringkasan
$allData->map(function ($item) {
$item->tfn = $this->mapToTfn($item->nilai_skala);
return $item;
});
// 5. GENERATE DYNAMIC PAIRS & GROUPING (Untuk Tabel Ringkasan Multi-Pakar)
// Menggabungkan sub-kriteria (S1 vs S2, S1 vs S3) khusus untuk ditampilkan di tabel View.
$dynamicPairs = [];
$n = count($subs);
for ($i = 0; $i < $n; $i++) {
for ($j = $i + 1; $j < $n; $j++) {
$dynamicPairs[] = [
'kiri_kode' => $subs[$i]->kode,
'kiri_nama' => $subs[$i]->nama_sub ?? $subs[$i]->nama,
'kanan_kode' => $subs[$j]->kode,
'kanan_nama' => $subs[$j]->nama_sub ?? $subs[$j]->nama,
];
}
}
// Mengelompokkan data kuesioner berdasarkan siapa yang mengisinya (Pakar 1, Pakar 2, dst)
$groupedKuesioner = $allData->groupBy('pakar_ke');
$pakarList = $groupedKuesioner->keys()->sort();
// ========================================================================
// TAHAP 1: CRISP MATRIX (GEOMETRIC MEAN - KONSENSUS PAKAR)
// ========================================================================
$crispMatrix = [];
foreach ($kodes as $iKode) {
foreach ($kodes as $jKode) {
// Konversi kode menjadi ID asli database
$iId = $subs->where('kode', $iKode)->first()->id;
$jId = $subs->where('kode', $jKode)->first()->id;
if ($iId == $jId) {
$crispMatrix[$iKode][$jKode] = 1.0;
continue;
}
// Cari penilaian searah
$scores = $allData->where('sub_kiri_id', $iId)->where('sub_kanan_id', $jId)->pluck('nilai_skala')->toArray();
if (count($scores) > 0) {
$crispMatrix[$iKode][$jKode] = pow(array_product($scores), 1 / count($scores));
} else {
// Cari penilaian berlawanan (Invers)
$revScores = $allData->where('sub_kiri_id', $jId)->where('sub_kanan_id', $iId)->pluck('nilai_skala')->toArray();
$crispMatrix[$iKode][$jKode] = count($revScores) > 0 ? (1 / pow(array_product($revScores), 1 / count($revScores))) : 1.0;
}
}
}
// ========================================================================
// ---> TAMBAHAN BARU: UJI KONSISTENSI AHP (EIGEN, CI, CR) <---
// ========================================================================
$nKriteria = count($kodes);
$colSums = [];
$priorityVector = [];
$lambdaMax = 0;
$ci = 0;
$cr = 0;
$riBaku = [1=>0.00, 2=>0.00, 3=>0.58, 4=>0.90, 5=>1.12, 6=>1.24, 7=>1.32, 8=>1.41, 9=>1.45];
$ri = $riBaku[$nKriteria] ?? 1.45;
// 1. Hitung total per kolom
foreach ($kodes as $j) {
$sum = 0;
foreach ($kodes as $i) { $sum += $crispMatrix[$i][$j]; }
$colSums[$j] = $sum;
}
// 2. Normalisasi & Cari Nilai Eigen
foreach ($kodes as $i) {
$rowSum = 0;
foreach ($kodes as $j) {
$normalizedVal = ($colSums[$j] == 0) ? 0 : ($crispMatrix[$i][$j] / $colSums[$j]);
$rowSum += $normalizedVal;
}
$priorityVector[$i] = $rowSum / $nKriteria;
}
// 3. Cari Lambda Max
foreach ($kodes as $j) { $lambdaMax += ($colSums[$j] * $priorityVector[$j]); }
// 4. Hitung CI dan CR
if ($nKriteria > 1) {
$ci = ($lambdaMax - $nKriteria) / ($nKriteria - 1);
$cr = ($ri > 0) ? ($ci / $ri) : 0;
}
// selesai
// ========================================================================
// TAHAP 2: FUZZY MATRIX (TFN)
// ========================================================================
$matrix = [];
foreach ($kodes as $i) {
foreach ($kodes as $j) {
// array_search digunakan untuk membedakan area diagonal atas dan bawah matriks
if (array_search($i, $kodes) <= array_search($j, $kodes)) {
$matrix[$i][$j] = $this->mapToTfn($crispMatrix[$i][$j]);
} else {
$tfnAtas = $matrix[$j][$i];
// Invers TFN tanpa fungsi round() agar presisi
$matrix[$i][$j] = [1 / $tfnAtas[2], 1 / $tfnAtas[1], 1 / $tfnAtas[0]];
}
}
}
// ========================================================================
// TAHAP 3: ROW SUMS & NILAI SINTESIS (Si)
// ========================================================================
$rowSums = [];
$totalSum = [0.0, 0.0, 0.0];
foreach ($kodes as $i) {
$rSum = [0.0, 0.0, 0.0];
foreach ($kodes as $j) {
$rSum[0] += $matrix[$i][$j][0];
$rSum[1] += $matrix[$i][$j][1];
$rSum[2] += $matrix[$i][$j][2];
}
$rowSums[$i] = $rSum;
$totalSum[0] += $rSum[0];
$totalSum[1] += $rSum[1];
$totalSum[2] += $rSum[2];
}
$si = [];
foreach ($kodes as $i) {
// Formula Si: Baris L / Total U , Baris M / Total M , Baris U / Total L
$si[$i] = [
$rowSums[$i][0] / $totalSum[2],
$rowSums[$i][1] / $totalSum[1],
$rowSums[$i][2] / $totalSum[0],
];
}
// ========================================================================
// TAHAP 4: V MATRIX (Derajat Kemungkinan)
// ========================================================================
$vMatrix = [];
foreach ($kodes as $i) {
foreach ($kodes as $j) {
$vMatrix[$i][$j] = ($i == $j) ? 1.0 : $this->calculateV($si[$i], $si[$j]);
}
}
// ========================================================================
// TAHAP 5: d'(Ai) & NORMALISASI (W)
// ========================================================================
$dPrime = [];
foreach ($kodes as $i) {
$vals = [];
foreach ($kodes as $j) {
if ($j !== $i) {
$vals[] = $vMatrix[$i][$j];
}
}
// Cari nilai Minimum
$dPrime[$i] = count($vals) > 0 ? min($vals) : 0.0;
}
$sumDPrime = array_sum($dPrime);
$finalWeights = [];
foreach ($kodes as $i) {
$finalWeights[$i] = ($sumDPrime > 0) ? ($dPrime[$i] / $sumDPrime) : 0.0;
}
// ========================================================================
// TAHAP AKHIR: KONVERSI KE SKALA 1000 UNTUK VIEW (Anti-Nol)
// ========================================================================
$bobot1000 = $this->toBobot1000($finalWeights);
// ========================================================================
// OUTPUT: EXPORT PDF ATAU TAMPILAN WEB BIASA
// ========================================================================
if ($request->has('export') && $request->export == 'pdf') {
// Pastikan use Barryvdh\DomPDF\Facade\Pdf; sudah ada di header file
$pdf = Pdf::loadView('perbandingan_sub.pdf', compact(
'kriterias', 'selectedKriteriaId', 'kriteriaInduk',
'crispMatrix', 'matrix', 'si', 'vMatrix',
'dPrime', 'kodes', 'subs', 'allData',
'finalWeights', 'sumDPrime', 'bobot1000', 'pakarList',
'dynamicPairs', 'groupedKuesioner',
'colSums', 'priorityVector', 'lambdaMax', 'ci', 'cr', 'ri', 'nKriteria'
))->setPaper('A4', 'landscape'); // Pakai landscape agar matriksnya muat
return $pdf->stream('Laporan_FAHP_SubKriteria_'.$kriteriaInduk->kode.'.pdf');
}
return view('perbandingan_sub.detail', compact(
'kriterias', 'selectedKriteriaId', 'kriteriaInduk',
'crispMatrix', 'matrix', 'si', 'vMatrix',
'dPrime', 'kodes', 'subs', 'allData',
'finalWeights', 'sumDPrime', 'bobot1000', 'pakarList',
'dynamicPairs', 'groupedKuesioner',
'colSums', 'priorityVector', 'lambdaMax', 'ci', 'cr', 'ri', 'nKriteria'
));
}
/**
* ==============================================================================
* FUNGSI HELPER: UJI KONSISTENSI AHP (SATUAN)
* ==============================================================================
*/
private function checkConsistency($matrix, $n)
{
$RI = [1 => 0, 2 => 0, 3 => 0.58, 4 => 0.90, 5 => 1.12, 6 => 1.24, 7 => 1.32, 8 => 1.41, 9 => 1.45];
if ($n < 3) return 0.0; // Jika kriteria cuma 1 atau 2, otomatis konsisten
// Hitung Total Kolom
$colSum = array_fill(0, $n, 0);
foreach ($matrix as $row) { foreach ($row as $j => $val) { $colSum[$j] += $val; } }
// Cari Eigen Vector
$priorityVector = [];
foreach ($matrix as $i => $row) {
$rowSum = 0;
foreach ($row as $j => $val) {
// Jaring pengaman cegah Division by Zero
$normalizedVal = ($colSum[$j] == 0) ? 0 : ($val / $colSum[$j]);
$rowSum += $normalizedVal;
}
$priorityVector[$i] = $rowSum / $n;
}
// Cari Lambda Max
$lambdaMax = 0;
foreach ($colSum as $j => $sum) { $lambdaMax += ($sum * $priorityVector[$j]); }
$ci = ($lambdaMax - $n) / ($n - 1);
return $ci / $RI[$n];
}
public function hapusPakar(Request $request)
{
if ($request->pakar_dihapus == 1) {
return redirect()->back()->with('error', 'Pakar 1 adalah pakar utama dan tidak dapat dihapus. Silakan lakukan Update data jika ada perubahan.');
}
PerbandinganSub::where('kriteria_id', $request->kriteria_id)
->where('pakar_ke', $request->pakar_dihapus)->delete();
$this->updateLocalWeights($request->kriteria_id);
return redirect()->back()->with('success', "Data Pakar $request->pakar_dihapus dihapus.");
}
}