MIF_E31230680/app/Http/Controllers/PerbandinganController.php

669 lines
30 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Kriteria;
use App\Models\Pengaturan;
use App\Models\Perbandingan;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PerbandinganController extends Controller
{
/**
* ==============================================================================
* FUNGSI 1: INDEX()
* Fungsi ini bertugas menyiapkan data untuk ditampilkan di halaman form kuesioner.
* ==============================================================================
*/
public function index()
{
// Variabel $kriterias mengambil seluruh data kriteria dari database, diurutkan berdasarkan kodenya (C1, C2, dst).
$kriterias = Kriteria::orderBy('kode', 'asc')->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'
));
}
}