555 lines
22 KiB
PHP
555 lines
22 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
use App\Models\Kriteria;
|
|
use App\Models\Makanan;
|
|
use App\Models\BobotKriteria;
|
|
use App\Models\SkorMakanan;
|
|
use App\Models\Rekomendasi;
|
|
use App\Models\RekomendasiAhli;
|
|
use App\Models\Komponen;
|
|
use App\Models\WaktuMakan;
|
|
use App\Models\PreferensiWaktuKriteria;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Carbon\Carbon;
|
|
use App\Models\PerbandinganKriteria;
|
|
use Illuminate\Support\Facades\DB;
|
|
use App\Traits\KriteriaTrait;
|
|
use App\Models\PerbandinganAlternatif;
|
|
use App\Models\ConsistencyRatioCriteria;
|
|
use App\Models\ConsistencyRatioAlternatif;
|
|
|
|
class RekomendasiController extends Controller
|
|
{
|
|
use KriteriaTrait;
|
|
|
|
public function hitungDanSimpanOtomatis()
|
|
{
|
|
try {
|
|
// Ambil data dari session
|
|
$sessionData = session('hasil_rekomendasi');
|
|
$alternatifsByKomponen = session('alternatifs_by_komponen');
|
|
$waktuMakanId = session('waktu_makan_id');
|
|
|
|
if (!$sessionData || !$alternatifsByKomponen || !$waktuMakanId) {
|
|
\Log::error('Data session tidak lengkap:', [
|
|
'hasil_rekomendasi' => $sessionData,
|
|
'alternatifs_by_komponen' => $alternatifsByKomponen,
|
|
'waktu_makan_id' => $waktuMakanId
|
|
]);
|
|
return redirect()->route('alternatif.pilih')
|
|
->with('error', 'Data session tidak lengkap. Silakan pilih alternatif terlebih dahulu.');
|
|
}
|
|
|
|
// Ambil semua kriteria
|
|
$kriterias = Kriteria::all();
|
|
$bobotKriteria = $this->getBobotKriteria($kriterias);
|
|
|
|
// Proses untuk setiap komponen
|
|
foreach ($alternatifsByKomponen as $komponenId => $komponenData) {
|
|
$alternatifs = $komponenData['alternatifs'];
|
|
|
|
// Log untuk debugging
|
|
\Log::info('Memproses komponen:', [
|
|
'komponen_id' => $komponenId,
|
|
'jumlah_alternatif' => count($alternatifs)
|
|
]);
|
|
|
|
// Inisialisasi matriks perbandingan
|
|
$matriksPerbandingan = [];
|
|
foreach ($kriterias as $kriteria) {
|
|
$matriksPerbandingan[$kriteria->id] = [];
|
|
foreach ($alternatifs as $alt1) {
|
|
$matriksPerbandingan[$kriteria->id][$alt1['id']] = [];
|
|
foreach ($alternatifs as $alt2) {
|
|
// Menggunakan nilai yang sudah dinormalisasi dari session
|
|
$nilai1 = $alt1[strtolower($kriteria->nama) . '_normalized'] ?? 0;
|
|
$nilai2 = $alt2[strtolower($kriteria->nama) . '_normalized'] ?? 0;
|
|
|
|
if ($nilai2 == 0) {
|
|
$matriksPerbandingan[$kriteria->id][$alt1['id']][$alt2['id']] = 0;
|
|
} else {
|
|
$matriksPerbandingan[$kriteria->id][$alt1['id']][$alt2['id']] = $nilai1 / $nilai2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hitung prioritas lokal untuk setiap kriteria
|
|
$prioritasLokal = [];
|
|
foreach ($kriterias as $kriteria) {
|
|
$prioritasLokal[$kriteria->id] = [];
|
|
|
|
// Hitung jumlah kolom
|
|
$jumlahKolom = [];
|
|
foreach ($alternatifs as $alt2) {
|
|
$jumlahKolom[$alt2['id']] = 0;
|
|
foreach ($alternatifs as $alt1) {
|
|
$jumlahKolom[$alt2['id']] += $matriksPerbandingan[$kriteria->id][$alt1['id']][$alt2['id']];
|
|
}
|
|
}
|
|
|
|
// Normalisasi matriks
|
|
$matriksNormal = [];
|
|
foreach ($alternatifs as $alt1) {
|
|
$matriksNormal[$alt1['id']] = [];
|
|
foreach ($alternatifs as $alt2) {
|
|
if ($jumlahKolom[$alt2['id']] > 0) {
|
|
$matriksNormal[$alt1['id']][$alt2['id']] =
|
|
$matriksPerbandingan[$kriteria->id][$alt1['id']][$alt2['id']] / $jumlahKolom[$alt2['id']];
|
|
} else {
|
|
$matriksNormal[$alt1['id']][$alt2['id']] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hitung prioritas lokal (rata-rata baris)
|
|
foreach ($alternatifs as $alt) {
|
|
$jumlahBaris = array_sum($matriksNormal[$alt['id']]);
|
|
$prioritasLokal[$kriteria->id][$alt['id']] = $jumlahBaris / count($alternatifs);
|
|
}
|
|
|
|
// Simpan skor ke database
|
|
foreach ($alternatifs as $alt) {
|
|
SkorMakanan::updateOrCreate(
|
|
[
|
|
'makanan_id' => $alt['id'],
|
|
'kriteria_id' => $kriteria->id,
|
|
'waktu_makan_id' => $waktuMakanId,
|
|
'komponen_id' => $komponenId
|
|
],
|
|
['nilai' => $prioritasLokal[$kriteria->id][$alt['id']]]
|
|
);
|
|
}
|
|
}
|
|
|
|
// Hitung skor akhir
|
|
foreach ($alternatifs as $alt) {
|
|
$skorAkhir = 0;
|
|
foreach ($kriterias as $kriteria) {
|
|
$skorAkhir += $prioritasLokal[$kriteria->id][$alt['id']] * $bobotKriteria[$kriteria->id];
|
|
}
|
|
|
|
// Simpan rekomendasi
|
|
Rekomendasi::updateOrCreate(
|
|
[
|
|
'makanan_id' => $alt['id'],
|
|
'komponen_id' => $komponenId,
|
|
'waktu_makan_id' => $waktuMakanId,
|
|
'user_id' => Auth::id(),
|
|
'tanggal_rekomendasi' => $sessionData['tanggal_rekomendasi']
|
|
],
|
|
['nilai_akhir' => $skorAkhir]
|
|
);
|
|
}
|
|
}
|
|
|
|
return redirect()->route('rekomendasi.index')
|
|
->with('success', 'Perhitungan AHP berhasil dilakukan.');
|
|
|
|
} catch (\Exception $e) {
|
|
\Log::error('Error in hitungDanSimpanOtomatis:', [
|
|
'message' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString(),
|
|
'session_data' => session()->all()
|
|
]);
|
|
return redirect()->back()
|
|
->with('error', 'Terjadi kesalahan saat melakukan perhitungan: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function getBobotKriteria($kriterias)
|
|
{
|
|
try {
|
|
$waktuMakanId = session('waktu_makan_id');
|
|
|
|
if (!$waktuMakanId) {
|
|
throw new \Exception('Waktu makan ID tidak ditemukan di session');
|
|
}
|
|
|
|
$bobotKriteria = [];
|
|
foreach ($kriterias as $kriteria) {
|
|
$bobot = BobotKriteria::where([
|
|
'kriteria_id' => $kriteria->id,
|
|
'waktu_makan_id' => $waktuMakanId
|
|
])->first();
|
|
|
|
if (!$bobot) {
|
|
// Jika tidak ada bobot, coba ambil dari perbandingan kriteria
|
|
$perbandinganKriteria = PerbandinganKriteria::where([
|
|
'kriteria_id_1' => $kriteria->id,
|
|
'waktu_makan_id' => $waktuMakanId
|
|
])->first();
|
|
|
|
if ($perbandinganKriteria) {
|
|
$total = PerbandinganKriteria::where([
|
|
'kriteria_id_1' => $kriteria->id,
|
|
'waktu_makan_id' => $waktuMakanId
|
|
])->sum('nilai');
|
|
|
|
$bobotKriteria[$kriteria->id] = $total > 0 ? $perbandinganKriteria->nilai / $total : 0;
|
|
} else {
|
|
\Log::warning("Bobot kriteria tidak ditemukan untuk kriteria {$kriteria->nama} pada waktu makan ID: {$waktuMakanId}");
|
|
$bobotKriteria[$kriteria->id] = 0;
|
|
}
|
|
} else {
|
|
$bobotKriteria[$kriteria->id] = $bobot->bobot;
|
|
}
|
|
}
|
|
|
|
// Normalisasi bobot kriteria
|
|
$totalBobot = array_sum($bobotKriteria);
|
|
if ($totalBobot > 0) {
|
|
foreach ($bobotKriteria as $key => $value) {
|
|
$bobotKriteria[$key] = $value / $totalBobot;
|
|
}
|
|
}
|
|
|
|
// Log untuk debugging
|
|
\Log::info('Bobot kriteria yang diambil:', [
|
|
'waktu_makan_id' => $waktuMakanId,
|
|
'bobot' => $bobotKriteria
|
|
]);
|
|
|
|
return $bobotKriteria;
|
|
} catch (\Exception $e) {
|
|
\Log::error('Error in getBobotKriteria: ' . $e->getMessage());
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
public function tampilHasil()
|
|
{
|
|
try {
|
|
// Ambil data dari session
|
|
$hasilRekomendasi = session('hasil_rekomendasi');
|
|
|
|
if (!$hasilRekomendasi) {
|
|
return redirect()->route('alternatif.pilih')
|
|
->with('error', 'Data rekomendasi tidak ditemukan.');
|
|
}
|
|
|
|
// Ambil data yang diperlukan dengan select spesifik
|
|
$kriterias = Kriteria::select('id', 'nama')->get();
|
|
$komponens = Komponen::select('id', 'nama')->get();
|
|
$waktuMakan = WaktuMakan::select('id', 'nama')->findOrFail($hasilRekomendasi['waktu_makan_id']);
|
|
|
|
// Inisialisasi array untuk menyimpan hasil per komponen
|
|
$hasilPerKomponen = [];
|
|
|
|
// Proses setiap komponen
|
|
foreach ($komponens as $komponen) {
|
|
// Ambil rekomendasi untuk komponen ini
|
|
$rekomendasis = Rekomendasi::with('makanan')
|
|
->where('waktu_makan_id', $hasilRekomendasi['waktu_makan_id'])
|
|
->where('komponen_id', $komponen->id)
|
|
->orderByDesc('nilai_akhir')
|
|
->get();
|
|
|
|
if ($rekomendasis->isNotEmpty()) {
|
|
// Ambil detail skor hanya untuk makanan yang ada di rekomendasi
|
|
$detailSkor = [];
|
|
foreach ($rekomendasis as $rekomendasi) {
|
|
$detailSkor[$rekomendasi->makanan_id] = [];
|
|
foreach ($kriterias as $kriteria) {
|
|
$skor = SkorMakanan::select('nilai')
|
|
->where('makanan_id', $rekomendasi->makanan_id)
|
|
->where('kriteria_id', $kriteria->id)
|
|
->first();
|
|
$detailSkor[$rekomendasi->makanan_id][$kriteria->nama] = $skor ? $skor->nilai : 0;
|
|
}
|
|
}
|
|
|
|
// Simpan hasil untuk komponen ini
|
|
$hasilPerKomponen[$komponen->id] = [
|
|
'komponen' => $komponen,
|
|
'rekomendasis' => $rekomendasis,
|
|
'detailSkor' => $detailSkor
|
|
];
|
|
}
|
|
}
|
|
|
|
// Ambil bobot kriteria
|
|
$bobotKriteria = [];
|
|
foreach ($kriterias as $kriteria) {
|
|
$bobot = BobotKriteria::select('bobot')
|
|
->where('kriteria_id', $kriteria->id)
|
|
->first();
|
|
$bobotKriteria[$kriteria->id] = $bobot ? $bobot->bobot : 0;
|
|
}
|
|
|
|
// Normalisasi bobot
|
|
$totalBobot = array_sum($bobotKriteria);
|
|
if ($totalBobot > 0) {
|
|
foreach ($bobotKriteria as $key => $value) {
|
|
$bobotKriteria[$key] = $value / $totalBobot;
|
|
}
|
|
}
|
|
|
|
// Set CR to null since it's temporarily disabled
|
|
$consistencyRatio = null;
|
|
|
|
return view('admin.rekomendasi', compact(
|
|
'hasilPerKomponen',
|
|
'kriterias',
|
|
'waktuMakan',
|
|
'consistencyRatio',
|
|
'bobotKriteria'
|
|
));
|
|
|
|
} catch (\Exception $e) {
|
|
\Log::error('Error in tampilHasil:', [
|
|
'message' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
|
|
return redirect()->route('alternatif.pilih')
|
|
->with('error', 'Terjadi kesalahan saat menampilkan hasil: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function hitungConsistencyRatio()
|
|
{
|
|
$waktuMakanId = session('waktu_makan_id');
|
|
|
|
if (!$waktuMakanId) {
|
|
return 0; // Return 0 if no waktu_makan_id in session
|
|
}
|
|
|
|
$perbandinganKriterias = PerbandinganKriteria::where('waktu_makan_id', $waktuMakanId)->get();
|
|
$n = Kriteria::count();
|
|
|
|
if ($perbandinganKriterias->isEmpty()) {
|
|
return 0; // Return 0 if no perbandingan data
|
|
}
|
|
|
|
// Buat matriks perbandingan
|
|
$matrix = array_fill(0, $n, array_fill(0, $n, 1));
|
|
foreach ($perbandinganKriterias as $perbandingan) {
|
|
$matrix[$perbandingan->kriteria_id_1 - 1][$perbandingan->kriteria_id_2 - 1] = $perbandingan->nilai;
|
|
$matrix[$perbandingan->kriteria_id_2 - 1][$perbandingan->kriteria_id_1 - 1] = 1 / $perbandingan->nilai;
|
|
}
|
|
|
|
// Hitung eigenvalue maksimum
|
|
$rowSums = array_map(function($row) {
|
|
return array_sum($row);
|
|
}, $matrix);
|
|
|
|
$totalSum = array_sum($rowSums);
|
|
$normalizedMatrix = array_map(function($row) use ($totalSum) {
|
|
return array_map(function($val) use ($totalSum) {
|
|
return $val / $totalSum;
|
|
}, $row);
|
|
}, $matrix);
|
|
|
|
$eigenvalue = 0;
|
|
for ($i = 0; $i < $n; $i++) {
|
|
$sum = 0;
|
|
for ($j = 0; $j < $n; $j++) {
|
|
$sum += $matrix[$i][$j] * array_sum($normalizedMatrix[$j]);
|
|
}
|
|
$eigenvalue += $sum / array_sum($normalizedMatrix[$i]);
|
|
}
|
|
$eigenvalue /= $n;
|
|
|
|
// Random Index values for n = 1 to 10
|
|
$RI = [0, 0, 0.58, 0.90, 1.12, 1.24, 1.32, 1.41, 1.45, 1.49];
|
|
|
|
// Hitung Consistency Index
|
|
$CI = ($eigenvalue - $n) / ($n - 1);
|
|
|
|
// Hitung Consistency Ratio
|
|
return $n <= 2 ? 0 : $CI / $RI[$n - 1];
|
|
}
|
|
|
|
private function getNilaiKriteria($makanan, $kriteria)
|
|
{
|
|
switch (strtolower($kriteria->nama)) {
|
|
case 'energi':
|
|
return is_array($makanan) ? $makanan['energi'] : $makanan->energi;
|
|
case 'protein':
|
|
return is_array($makanan) ? $makanan['protein'] : $makanan->protein;
|
|
case 'lemak':
|
|
return is_array($makanan) ? $makanan['lemak'] : $makanan->lemak;
|
|
case 'karbohidrat':
|
|
return is_array($makanan) ? $makanan['karbohidrat'] : $makanan->karbohidrat;
|
|
case 'natrium':
|
|
return is_array($makanan) ? $makanan['natrium'] : $makanan->natrium;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private function isCostCriteria($kriteriaNama)
|
|
{
|
|
return in_array(strtolower($kriteriaNama), ['lemak', 'natrium']);
|
|
}
|
|
|
|
private function normalisasiNilai($nilai, $kriteria, $makanans)
|
|
{
|
|
// Kumpulkan semua nilai untuk kriteria ini
|
|
$nilaiKriteria = [];
|
|
foreach ($makanans as $makanan) {
|
|
$nilaiKriteria[] = $this->getNilaiKriteria($makanan, $kriteria);
|
|
}
|
|
|
|
// Cek jenis kriteria (benefit atau cost)
|
|
if ($this->isCostCriteria($kriteria->nama)) {
|
|
// Untuk kriteria cost (lemak dan natrium), nilai lebih kecil lebih baik
|
|
$nilaiInverse = $nilai > 0 ? 1 / $nilai : 0;
|
|
$totalInverse = 0;
|
|
foreach ($nilaiKriteria as $n) {
|
|
$totalInverse += ($n > 0 ? 1 / $n : 0);
|
|
}
|
|
return $totalInverse > 0 ? $nilaiInverse / $totalInverse : 0;
|
|
} else {
|
|
// Untuk kriteria benefit (energi dan karbohidrat), nilai lebih besar lebih baik
|
|
$total = array_sum($nilaiKriteria);
|
|
return $total > 0 ? $nilai / $total : 0;
|
|
}
|
|
}
|
|
|
|
public function index()
|
|
{
|
|
$waktuMakans = WaktuMakan::with(['komponens', 'consistencyRatios' => function($query) {
|
|
$query->where('user_id', Auth::id())
|
|
->latest('tanggal_perhitungan');
|
|
}])
|
|
->select('waktu_makans.*')
|
|
->selectRaw('(
|
|
SELECT COUNT(*) > 0
|
|
FROM rekomendasis r
|
|
WHERE r.waktu_makan_id = waktu_makans.id
|
|
AND r.user_id = ?
|
|
) as has_recommendation', [Auth::id()])
|
|
->selectRaw('(
|
|
SELECT MAX(tanggal_rekomendasi)
|
|
FROM rekomendasis r
|
|
WHERE r.waktu_makan_id = waktu_makans.id
|
|
AND r.user_id = ?
|
|
) as latest_calculation', [Auth::id()])
|
|
->get();
|
|
|
|
return view('admin.rekomendasi-list', compact('waktuMakans'));
|
|
}
|
|
|
|
public function detail($waktuMakanId)
|
|
{
|
|
try {
|
|
$waktuMakan = WaktuMakan::findOrFail($waktuMakanId);
|
|
|
|
// Ambil rekomendasi terbaru untuk waktu makan ini
|
|
$latestRekomendasi = Rekomendasi::where('waktu_makan_id', $waktuMakanId)
|
|
->where('user_id', Auth::id())
|
|
->orderBy('tanggal_rekomendasi', 'desc')
|
|
->first();
|
|
|
|
if (!$latestRekomendasi) {
|
|
return redirect()->route('rekomendasi.index')
|
|
->with('error', 'Belum ada rekomendasi untuk waktu makan ini.');
|
|
}
|
|
|
|
// Set data untuk tampilan detail
|
|
$hasilRekomendasi = [
|
|
'tanggal_rekomendasi' => $latestRekomendasi->tanggal_rekomendasi,
|
|
'waktu_makan_id' => $waktuMakanId
|
|
];
|
|
|
|
// Ambil semua kriteria
|
|
$kriterias = Kriteria::all();
|
|
|
|
// Ambil bobot kriteria dari database
|
|
$bobotKriteria = [];
|
|
foreach ($kriterias as $kriteria) {
|
|
$bobot = BobotKriteria::where('kriteria_id', $kriteria->id)->first();
|
|
$bobotKriteria[$kriteria->id] = $bobot ? $bobot->bobot : 0;
|
|
}
|
|
|
|
// Normalisasi bobot
|
|
$totalBobot = array_sum($bobotKriteria);
|
|
if ($totalBobot > 0) {
|
|
foreach ($bobotKriteria as $key => $value) {
|
|
$bobotKriteria[$key] = $value / $totalBobot;
|
|
}
|
|
}
|
|
|
|
// Ambil komponen yang memiliki rekomendasi
|
|
$komponens = Komponen::whereExists(function ($query) use ($waktuMakanId, $latestRekomendasi) {
|
|
$query->select(DB::raw(1))
|
|
->from('rekomendasis')
|
|
->whereColumn('rekomendasis.komponen_id', 'komponens.id')
|
|
->where('rekomendasis.waktu_makan_id', $waktuMakanId)
|
|
->where('rekomendasis.tanggal_rekomendasi', $latestRekomendasi->tanggal_rekomendasi);
|
|
})->get();
|
|
|
|
$hasilPerKomponen = [];
|
|
foreach ($komponens as $komponen) {
|
|
// Ambil rekomendasi untuk komponen ini dengan eager loading
|
|
$rekomendasis = Rekomendasi::with(['makanan' => function($query) {
|
|
$query->select('makanans.id', 'makanans.nama', 'makanans.energi', 'makanans.lemak',
|
|
'makanans.karbohidrat', 'makanans.natrium');
|
|
}])
|
|
->where('waktu_makan_id', $waktuMakanId)
|
|
->where('komponen_id', $komponen->id)
|
|
->where('user_id', Auth::id())
|
|
->where('tanggal_rekomendasi', $latestRekomendasi->tanggal_rekomendasi)
|
|
->orderByDesc('nilai_akhir')
|
|
->get();
|
|
|
|
// Ambil detail skor untuk setiap makanan
|
|
$detailSkor = [];
|
|
foreach ($rekomendasis as $rekomendasi) {
|
|
$detailSkor[$rekomendasi->makanan_id] = [];
|
|
foreach ($kriterias as $kriteria) {
|
|
$skor = SkorMakanan::where('makanan_id', $rekomendasi->makanan_id)
|
|
->where('kriteria_id', $kriteria->id)
|
|
->where('waktu_makan_id', $waktuMakanId)
|
|
->where('komponen_id', $komponen->id)
|
|
->first();
|
|
$detailSkor[$rekomendasi->makanan_id][$kriteria->nama] = $skor ? $skor->nilai : 0;
|
|
}
|
|
|
|
// Ambil data perbandingan untuk makanan ini
|
|
$perbandinganData = PerbandinganAlternatif::where('waktu_makan_id', $waktuMakanId)
|
|
->where('komponen_id', $komponen->id)
|
|
->where(function($query) use ($rekomendasi) {
|
|
$query->where('alternatif_id_1', $rekomendasi->makanan_id)
|
|
->orWhere('alternatif_id_2', $rekomendasi->makanan_id);
|
|
})
|
|
->get();
|
|
|
|
if ($perbandinganData->isNotEmpty()) {
|
|
$detailSkor[$rekomendasi->makanan_id]['perbandingan'] = $perbandinganData;
|
|
}
|
|
}
|
|
|
|
$hasilPerKomponen[$komponen->id] = [
|
|
'komponen' => $komponen,
|
|
'rekomendasis' => $rekomendasis,
|
|
'detailSkor' => $detailSkor
|
|
];
|
|
}
|
|
|
|
// Ambil CR terbaru dari ConsistencyRatioCriteria untuk waktu makan ini
|
|
$latestCR = ConsistencyRatioCriteria::where('waktu_makan_id', $waktuMakanId)
|
|
->latest()
|
|
->first();
|
|
|
|
return view('admin.rekomendasi', compact(
|
|
'waktuMakan',
|
|
'hasilRekomendasi',
|
|
'kriterias',
|
|
'bobotKriteria',
|
|
'hasilPerKomponen',
|
|
'latestCR'
|
|
));
|
|
|
|
} catch (\Exception $e) {
|
|
return redirect()->route('rekomendasi.index')
|
|
->with('error', 'Terjadi kesalahan saat menampilkan detail: ' . $e->getMessage());
|
|
}
|
|
}
|
|
}
|