648 lines
26 KiB
PHP
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.");
|
|
}
|
|
}
|