get(); // Variabel $allData mengambil semua riwayat perbandingan yang sudah pernah diisi oleh pakar sebelumnya. $allData = Perbandingan::all(); // Variabel $pairs adalah array kosong yang akan kita isi dengan pasangan kriteria. // TIPS SIDANG: Sistem menggunakan algoritma Kombinatorial untuk memasangkan kriteria secara otomatis. // Jika ada Kriteria A, B, dan C, sistem akan membuat pasangan: A-B, A-C, dan B-C. $pairs = []; $n = count($kriterias); // Menghitung total kriteria yang ada // Looping pertama ($i) menentukan kriteria di sisi KIRI for ($i = 0; $i < $n; $i++) { // Looping kedua ($j) menentukan kriteria di sisi KANAN. // Kenapa $j = $i + 1? Agar Kriteria A tidak dipasangkan dengan dirinya sendiri (A-A), // dan mencegah duplikasi terbalik (jika sudah ada A-B, tidak perlu buat B-A di kuesioner). for ($j = $i + 1; $j < $n; $j++) { $pairs[] = [ 'kiri' => $kriterias[$i], 'kanan' => $kriterias[$j], ]; } } // Variabel $existingPakar mengecek ke database, pakar nomor berapa saja yang sudah mengisi kuesioner. $existingPakar = Perbandingan::distinct()->pluck('pakar_ke')->toArray(); // Jika database masih kosong (belum ada yang mengisi sama sekali), default-kan ke Pakar 1. if (empty($existingPakar)) { $existingPakar = [1]; } else { // Jika sudah ada pakar (misal pakar 2 dan 3), tapi pakar 1 tidak ada, tambahkan angka 1 di urutan paling depan. if (! in_array(1, $existingPakar)) { array_unshift($existingPakar, 1); } } sort($existingPakar); // Mengurutkan nomor pakar dari terkecil ke terbesar // Mengambil kode PIN keamanan dari database agar tidak sembarang orang bisa input kuesioner. // Jika di database tidak ada PIN, default-nya adalah '123456'. $pengaturan = Pengaturan::where('nama_pengaturan', 'pin_pakar')->first(); $pinSistem = $pengaturan ? $pengaturan->nilai_pengaturan : '123456'; // Mengirim semua variabel yang sudah diolah di atas ke tampilan layar (View). return view('perhitungan.index', compact('kriterias', 'pairs', 'existingPakar', 'allData', 'pinSistem')); } /** * ============================================================================== * FUNGSI 2: HITUNG() * Fungsi ini bertugas menerima data yang disubmit dari form kuesioner, * menyimpannya ke database, lalu memicu perhitungan ulang bobot Fuzzy AHP. * ============================================================================== */ public function hitung(Request $request) { // 1. Validasi PIN Keamanan $pengaturan = Pengaturan::where('nama_pengaturan', 'pin_pakar')->first(); $pinSistem = $pengaturan ? $pengaturan->nilai_pengaturan : '123456'; // Jika PIN yang dimasukkan user tidak sama dengan PIN sistem, tolak dan kembalikan ke halaman sebelumnya. if ($request->input('pin_rahasia') !== $pinSistem) { return redirect()->back()->with('error', 'Akses Ditolak! PIN Keamanan salah.'); } // 2. Menangkap inputan dari form kuesioner $pakarKe = $request->input('pakar_ke'); // Nomor seri pakar (misal: Pakar 1) $kiriIds = $request->input('kiri_id'); // Kumpulan ID kriteria sisi kiri $kananIds = $request->input('kanan_id'); // Kumpulan ID kriteria sisi kanan $nilaiSkalas = $request->input('nilai_skala'); // Kumpulan angka skala 1-9 yang dipilih pakar // ==================================================================== // TAMBAHAN: CEK KONSISTENSI (CR) SEBELUM SIMPAN // ==================================================================== $kriteriaIds = Kriteria::orderBy('kode', 'asc')->pluck('id')->toArray(); $n = count($kriteriaIds); $matrixTemp = array_fill(0, $n, array_fill(0, $n, 1.0)); $idToIndex = array_flip($kriteriaIds); 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 gaes hadu pusing, kalau bisa memilih ulang sepertinya akan membangun si saja daripada rumus // 3. Menyimpan ke database menggunakan DB::transaction // TIPS SIDANG: Database Transaction menjamin integritas data. Jika sistem mati di tengah proses simpan, // seluruh data yang sedang diproses akan di-Rollback (dibatalkan), sehingga tidak ada data yang masuk setengah-setengah. DB::transaction(function () use ($pakarKe, $kiriIds, $kananIds, $nilaiSkalas) { foreach ($kiriIds as $index => $kiriId) { // updateOrCreate berarti: Kalau data sudah ada (berdasarkan pakar, kriteria kiri & kanan), update nilainya. // Kalau datanya belum ada, buat baris data baru di tabel perbandingan. Perbandingan::updateOrCreate( [ 'pakar_ke' => $pakarKe, 'kriteria_kiri_id' => $kiriId, 'kriteria_kanan_id' => $kananIds[$index], ], ['nilai_skala' => $nilaiSkalas[$index]] ); } }); // Setelah data berhasil disimpan, otomatis panggil fungsi perhitungan matriks Fuzzy AHP $this->updateWeights(); // Kembalikan pengguna ke halaman kuesioner dengan pesan sukses return redirect()->back()->with('success', "Data Pakar $pakarKe berhasil disimpan & Bobot diperbarui!"); } /** * ============================================================================== * FUNGSI 3: UPDATE WEIGHTS() - JANTUNG ALGORITMA FUZZY AHP * Fungsi ini melakukan perhitungan matematis penuh untuk mencari Bobot Global kriteria. * ============================================================================== */ private function updateWeights() { $kriterias = Kriteria::orderBy('kode', 'asc')->get(); $ids = $kriterias->pluck('id')->toArray(); // Mengambil kumpulan ID kriteria saja $allData = Perbandingan::all(); // Jika database perbandingan masih kosong, set semua bobot jadi 0 dan hentikan proses. if ($allData->isEmpty()) { Kriteria::query()->update(['bobot_global' => 0]); return; } // ------------------------------------------------------------------------- // TAHAP 1: MEMBENTUK MATRIKS KONSENSUS MULTI-PAKAR (GEOMETRIC MEAN) // ------------------------------------------------------------------------- $matrixConsensus = []; foreach ($ids as $i) { // $i mewakili Baris foreach ($ids as $j) { // $j mewakili Kolom // Jika Baris dan Kolom adalah kriteria yang sama (misal C1 vs C1), nilainya mutlak 1 (1.0, 1.0, 1.0) if ($i == $j) { $matrixConsensus[$i][$j] = [1.0, 1.0, 1.0]; continue; // Lanjut ke iterasi berikutnya } // Ambil semua nilai dari seluruh pakar yang membandingkan Kriteria i (kiri) vs Kriteria j (kanan) $scores = $allData->where('kriteria_kiri_id', $i) ->where('kriteria_kanan_id', $j) ->pluck('nilai_skala')->toArray(); // Jika datanya ketemu (diinput searah oleh pakar): if (count($scores) > 0) { // RUMUS GEOMETRIC MEAN: Akar pangkat n dari (x1 * x2 * ... * xn) // pow() adalah fungsi pangkat. Di sini kita memangkatkan total kali (array_product) dengan 1/jumlah_pakar (count). $geoMeanValue = pow(array_product($scores), 1 / count($scores)); // Langsung ubah nilai Geometric Mean menjadi nilai rentang Fuzzy (TFN) $matrixConsensus[$i][$j] = $this->mapToTfn($geoMeanValue); // Jika datanya tidak ketemu, berarti kita sedang memproses area matriks kebalikan (Diagonal Bawah) } else { // Cari data perbandingan dengan urutan terbalik (j vs i) $revScores = $allData->where('kriteria_kiri_id', $j) ->where('kriteria_kanan_id', $i) ->pluck('nilai_skala')->toArray(); if (count($revScores) > 0) { // Hitung Geometric Mean dari nilai kebalikan $geoMeanRev = pow(array_product($revScores), 1 / count($revScores)); $tfnRev = $this->mapToTfn($geoMeanRev); // RUMUS INVERS TFN: (1/Upper, 1/Medium, 1/Lower) // Posisi Lower dan Upper ditukar letaknya. $matrixConsensus[$i][$j] = [ 1 / $tfnRev[2], 1 / $tfnRev[1], 1 / $tfnRev[0] ]; } else { // Jaring pengaman jika data benar-benar tidak ada $matrixConsensus[$i][$j] = [1.0, 1.0, 1.0]; } } } } // ------------------------------------------------------------------------- // TAHAP 2: HITUNG NILAI SINTESIS EXTENT (Si) // ------------------------------------------------------------------------- $rowSums = []; // Untuk menyimpan total penjumlahan horizontal (per baris) $totalSum = [0.0, 0.0, 0.0]; // Untuk menyimpan total keseluruhan dari semua elemen matriks foreach ($ids as $i) { $rSum = [0.0, 0.0, 0.0]; foreach ($ids as $j) { // Menjumlahkan nilai Lower (indeks 0), Medium (indeks 1), Upper (indeks 2) per baris $rSum[0] += $matrixConsensus[$i][$j][0]; $rSum[1] += $matrixConsensus[$i][$j][1]; $rSum[2] += $matrixConsensus[$i][$j][2]; } $rowSums[$i] = $rSum; // Tambahkan total baris tadi ke grand total keseluruhan matriks $totalSum[0] += $rSum[0]; $totalSum[1] += $rSum[1]; $totalSum[2] += $rSum[2]; } $si = []; foreach ($ids as $i) { // RUMUS SINTESIS (Si): Total Baris dikali Invers dari Grand Total Matriks. // Karena ini Invers, indeks Grand Total-nya dibalik (2, 1, 0). // TIPS SIDANG: Jelaskan bahwa di sini kita mempertahankan tipe data float tanpa dibulatkan // agar tidak terjadi Compounding Error (Penumpukan Galat/Salah Hitung). $si[$i] = [ $rowSums[$i][0] * (1 / $totalSum[2]), // Lower baris / Upper total $rowSums[$i][1] * (1 / $totalSum[1]), // Medium baris / Medium total $rowSums[$i][2] * (1 / $totalSum[0]), // Upper baris / Lower total ]; } // ------------------------------------------------------------------------- // TAHAP 3: HITUNG DERAJAT KEMUNGKINAN (V Matrix) & NILAI MINIMUM (d') // ------------------------------------------------------------------------- $vMatrix = []; foreach ($ids as $i) { foreach ($ids as $j) { // Membandingkan probabilitas Si milik kriteria i dengan Si milik kriteria j $vMatrix[$i][$j] = ($i == $j) ? 1.0 : $this->calculateV($si[$i], $si[$j]); } } $dPrime = []; // dPrime adalah nilai Bobot yang belum dinormalisasi foreach ($ids as $i) { $vals = []; foreach ($ids as $j) { if ($i !== $j) { $vals[] = $vMatrix[$i][$j]; } } // RUMUS CHANG: Kita cari nilai Minimum (Terkecil) dari hasil perbandingan irisan tadi. $dPrime[$i] = count($vals) > 0 ? min($vals) : 0.0; } $sumDPrime = array_sum($dPrime); // Menjumlahkan seluruh nilai dPrime // ------------------------------------------------------------------------- // TAHAP 4: NORMALISASI BOBOT & SIMPAN KE DATABASE // ------------------------------------------------------------------------- foreach ($ids as $id) { // RUMUS NORMALISASI: Nilai dPrime kriteria tersebut dibagi Total dPrime keseluruhan. // Hasil normalisasi ini pasti jika dijumlahkan semuanya akan bernilai 1. $finalWeight = ($sumDPrime > 0) ? ($dPrime[$id] / $sumDPrime) : 0.0; // Menyimpan hasil akhir (Bobot Global) ke database kriteria. // Dibulatkan 6 angka di belakang koma untuk akurasi tinggi di database. Kriteria::where('id', $id)->update(['bobot_global' => round($finalWeight, 4)]); } } /** * ============================================================================== * FUNGSI RUMUS V: DERAJAT KEMUNGKINAN (Degree of Possibility) * ============================================================================== * Berfungsi menghitung seberapa besar kemungkinan rentang Fuzzy (Si baris) * lebih besar dari rentang Fuzzy (Si kolom). */ private function calculateV(array $si_baris, array $si_kolom): float { [$l1, $m1, $u1] = $si_kolom; // Nilai M1 (Lower, Medium, Upper) [$l2, $m2, $u2] = $si_baris; // Nilai M2 (Lower, Medium, Upper) // Jika titik Puncak (Medium) baris lebih besar/sama dengan kolom, probabilitasnya mutlak 100% (1.0) if ($m2 >= $m1) return 1.0; // Jika titik paling bawah kolom lebih besar dari titik paling atas baris, probabilitasnya 0% (0.0) if ($l1 >= $u2) return 0.0; // Jika saling bersinggungan/beririsan, hitung luas irisan segitiganya. $num = $l1 - $u2; // Pembilang $den = ($m2 - $u2) - ($m1 - $l1); // Penyebut // Cegah error pembagian dengan nol (Division by Zero) return ($den == 0) ? 0.0 : ($num / $den); } /** * ============================================================================== * FUNGSI MAPPING TFN (Triangular Fuzzy Number) * ============================================================================== * Berfungsi mengonversi angka mutlak skala 1-9 menjadi rentang keraguan (TFN). */ private function mapToTfn($val): array { // Kamus Skala Dasar TFN $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], ]; // Jika nilainya mendekati 1 (skala sama penting), kembalikan kemutlakan 1.0 if ($val >= 0.99 && $val <= 1.01) return $baseTFN[1]; if ($val > 1) { // Jika nilai rasio positif (kiri lebih penting dari kanan), bulatkan nilainya untuk mencocokkan ke kamus dasar. $key = min(9, (int) round($val)); return $baseTFN[$key]; } else { // Jika nilai rasio desimal/negatif (kanan lebih penting dari kiri), dicari invers-nya dulu (1 / nilai) $originalKey = max(1, min(9, (int) round(1 / $val))); $tfn = $baseTFN[$originalKey]; // Kembalikan dalam bentuk Invers Fuzzy (dibalik posisinya) return [1 / $tfn[2], 1 / $tfn[1], 1 / $tfn[0]]; } } /** * ============================================================================== * FUNGSI KONVERSI SKALA 1000 (LARGEST REMAINDER METHOD) * ============================================================================== * Mengubah bobot desimal (contoh: 0.33) menjadi angka bulat skala ribuan (contoh: 330) * yang total keseluruhannya dipastikan akurat 1000. */ private function toBobot1000(array $weights): array { $keys = array_keys($weights); $float = array_map(fn($w) => $w * 1000, $weights); // Mengkalikan bobot asli dengan 1000 $result = []; // 1. Bulatkan nilai hasil kali ke bawah menggunakan floor() // Fungsi max(1, ...) memastikan bahwa kriteria sekecil apapun tidak akan menjadi 0. // Ini melindungi parameter sistem yang berprioritas rendah agar tetap valid. foreach ($float as $key => $val) { $result[$key] = max(1, floor($val)); } // 2. Cek apakah totalnya sudah 1000? Cari tahu selisih kekurangannya. $remainder = 1000 - (int) array_sum($result); if ($remainder > 0) { $sortedKeys = $keys; // 3. Urutkan kriteria berdasarkan siapa yang punya angka pecahan desimal terbesar di belakang koma. usort($sortedKeys, function ($a, $b) use ($float) { $decA = $float[$a] - floor($float[$a]); $decB = $float[$b] - floor($float[$b]); return $decB <=> $decA; // Urutkan menurun (Descending) }); // 4. Bagikan poin sisa (kekurangan) satu per satu kepada kriteria yang menduduki peringkat desimal terbesar. for ($k = 0; $k < $remainder; $k++) { $result[$sortedKeys[$k]] += 1; } } // Pastikan format akhirnya adalah Integer (angka bulat tanpa koma) return array_map('intval', $result); } // tahun cek ahp 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; $colSum = array_fill(0, $n, 0); foreach ($matrix as $row) { foreach ($row as $j => $val) { $colSum[$j] += $val; } } $priorityVector = []; foreach ($matrix as $i => $row) { $rowSum = 0; foreach ($row as $j => $val) { // JARING PENGAMAN: Mencegah error "Division by Zero" $normalizedVal = ($colSum[$j] == 0) ? 0 : ($val / $colSum[$j]); $rowSum += $normalizedVal; } $priorityVector[$i] = $rowSum / $n; } $lambdaMax = 0; foreach ($colSum as $j => $sum) { $lambdaMax += ($sum * $priorityVector[$j]); } $ci = ($lambdaMax - $n) / ($n - 1); return $ci / $RI[$n]; } /** * ============================================================================== * FUNGSI HAPUS DATA PAKAR * ============================================================================== */ public function hapusPakar(Request $request) { $pakarDihapus = $request->input('pakar_dihapus'); // Mencegah Pakar 1 (pakar utama) untuk dihapus agar sistem tidak error karena kosong if ($pakarDihapus == 1) { return redirect()->back()->with('error', 'Pakar 1 tidak dapat dihapus.'); } // Hapus data dari tabel perbandingan Perbandingan::where('pakar_ke', $pakarDihapus)->delete(); // Hitung ulang bobot setelah datanya berkurang $this->updateWeights(); return redirect()->back()->with('success', "Data Pakar $pakarDihapus dihapus."); } /** * ============================================================================== * FUNGSI DETAIL() - UNTUK TAMPILAN LAPORAN * ============================================================================== * TIPS SIDANG: Fungsi ini isinya adalah replika persis (copy-paste logika) dari * fungsi updateWeights(). Bedanya, updateWeights() memproses secara rahasia * di belakang layar (background) untuk disimpan ke database. * Sedangkan fungsi detail() bertujuan menangkap setiap langkah prosesnya * sehingga penguji bisa melihat step-by-step perhitungannya. */ /** * ============================================================================== * FUNGSI DETAIL() - UNTUK TAMPILAN LAPORAN (WEB & PDF) * ============================================================================== * Fungsi ini menangkap setiap langkah matematis Fuzzy AHP untuk transparansi data. */ public function detail(Request $request) // Tambahkan parameter Request $request { // 1. DATA DASAR // Mengambil semua data kriteria dan kuesioner dari database $kriterias = Kriteria::orderBy('kode', 'asc')->get(); $kodes = $kriterias->pluck('kode')->toArray(); // Daftar kode kriteria ['C1', 'C2', ...] $ids = $kriterias->pluck('id')->toArray(); // Daftar ID kriteria [1, 2, ...] $allData = Perbandingan::all(); // Cek jika kuesioner belum diisi sama sekali if ($allData->isEmpty()) { return redirect()->route('perbandingan.index')->with('error', 'Data masih kosong!'); } // 2. MAPPING TFN (FUZZIFIKASI) // Mengonversi nilai skala 1-9 menjadi array Fuzzy (Triangular Fuzzy Number) $dataKuesioner = $allData->map(function ($item) { $item->tfn = $this->mapToTfn($item->nilai_skala); return $item; }); // 3. GENERATE PAIRS (Untuk Tabel Ringkasan PDF) // Membuat pasangan perbandingan secara dinamis untuk ditampilkan di laporan $dynamicPairs = []; $n = count($kriterias); for ($i = 0; $i < $n; $i++) { for ($j = $i + 1; $j < $n; $j++) { $dynamicPairs[] = [ 'kiri_kode' => $kriterias[$i]->kode, 'kiri_nama' => $kriterias[$i]->nama, 'kanan_kode' => $kriterias[$j]->kode, 'kanan_nama' => $kriterias[$j]->nama, ]; } } $groupedKuesioner = $allData->groupBy('pakar_ke'); // ======================================================================== // TAHAP 1: MATRIKS CRISP (GEOMETRIC MEAN - KONSENSUS MULTI-PAKAR) // ======================================================================== $crispMatrix = []; foreach ($ids as $i) { foreach ($ids as $j) { $kodeI = $kriterias->where('id', $i)->first()->kode; $kodeJ = $kriterias->where('id', $j)->first()->kode; if ($i == $j) { $crispMatrix[$kodeI][$kodeJ] = 1.0; continue; } $scores = $allData->where('kriteria_kiri_id', $i)->where('kriteria_kanan_id', $j)->pluck('nilai_skala')->toArray(); if (count($scores) > 0) { // Rata-rata ukur jika data searah $crispMatrix[$kodeI][$kodeJ] = pow(array_product($scores), 1 / count($scores)); } else { // Kebalikan (1/GM) jika data berlawanan arah $revScores = $allData->where('kriteria_kiri_id', $j)->where('kriteria_kanan_id', $i)->pluck('nilai_skala')->toArray(); $crispMatrix[$kodeI][$kodeJ] = 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 dari Matriks Crisp foreach ($kodes as $j) { $sum = 0; foreach ($kodes as $i) { $sum += $crispMatrix[$i][$j]; } $colSums[$j] = $sum; } // 2. Normalisasi & Cari Nilai Eigen (Priority Vector) 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; } // tutup // ======================================================================== // TAHAP 2: MATRIKS FUZZY (TFN) // ======================================================================== $matrix = []; foreach ($kodes as $i) { foreach ($kodes as $j) { if (array_search($i, $kodes) <= array_search($j, $kodes)) { $matrix[$i][$j] = $this->mapToTfn($crispMatrix[$i][$j]); } else { $tfnAtas = $matrix[$j][$i]; // Invers Fuzzy: (1/u, 1/m, 1/l) $matrix[$i][$j] = [1 / $tfnAtas[2], 1 / $tfnAtas[1], 1 / $tfnAtas[0]]; } } } // ======================================================================== // TAHAP 3: NILAI SINTESIS FUZZY (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) { $si[$i] = [ $rowSums[$i][0] * (1 / $totalSum[2]), $rowSums[$i][1] * (1 / $totalSum[1]), $rowSums[$i][2] * (1 / $totalSum[0]), ]; } // ======================================================================== // TAHAP 4: DERAJAT KEMUNGKINAN (V MATRIX) & d' (MINIMUM DEGREE) // ======================================================================== $vMatrix = []; foreach ($kodes as $i) { foreach ($kodes as $j) { $vMatrix[$i][$j] = ($i == $j) ? 1.0 : $this->calculateV($si[$i], $si[$j]); } } $dPrime = []; foreach ($kodes as $i) { $vals = []; foreach ($kodes as $j) { if ($i !== $j) { $vals[] = $vMatrix[$i][$j]; } } $dPrime[$i] = count($vals) > 0 ? min($vals) : 0.0; } // ======================================================================== // TAHAP 5: NORMALISASI FINAL & SKALA 1000 // ======================================================================== $sumDPrime = array_sum($dPrime); $finalWeights = []; foreach ($kodes as $i) { $finalWeights[$i] = ($sumDPrime > 0) ? ($dPrime[$i] / $sumDPrime) : 0.0; } // Konversi ke bobot bulat 1000 menggunakan Largest Remainder Method $bobot1000 = $this->toBobot1000($finalWeights); $pakarList = $allData->pluck('pakar_ke')->unique()->sort(); // ======================================================================== // LOGIKA EXPORT PDF // ======================================================================== if ($request->has('export') && $request->export == 'pdf') { // Memanggil View khusus PDF (Anda bisa membuat file perbandingan.pdf) $pdf = Pdf::loadView('perhitungan.pdf', compact( 'crispMatrix', 'matrix', 'si', 'vMatrix', 'dPrime', 'kodes', 'dataKuesioner', 'kriterias', 'pakarList', 'finalWeights', 'bobot1000', 'dynamicPairs', 'groupedKuesioner', // Variabel Tambahan AHP: 'colSums', 'priorityVector', 'lambdaMax', 'ci', 'cr', 'ri', 'nKriteria' ))->setPaper('A4', 'landscape'); return $pdf->stream('Laporan_FAHP_Kriteria_Global.pdf'); } // Mengirimkan semua data langkah demi langkah tersebut ke file detail.blade.php return view('perhitungan.detail', compact( 'crispMatrix', 'matrix', 'si', 'vMatrix', 'dPrime', 'kodes', 'dataKuesioner', 'kriterias', 'pakarList', 'finalWeights', 'bobot1000', 'dynamicPairs', 'groupedKuesioner', // Variabel Tambahan AHP: 'colSums', 'priorityVector', 'lambdaMax', 'ci', 'cr', 'ri', 'nKriteria' )); } }