669 lines
30 KiB
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'
|
|
));
|
|
}
|
|
}
|