520 lines
21 KiB
PHP
520 lines
21 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
use App\Models\Kriteria;
|
|
use App\Models\Makanan;
|
|
use App\Models\Kategori;
|
|
use App\Models\PerbandinganAlternatif;
|
|
use App\Models\SkorMakanan;
|
|
use App\Models\RekomendasiAhli;
|
|
use App\Models\Komponen;
|
|
use App\Models\WaktuMakan;
|
|
use App\Models\MakananKomponenWaktu;
|
|
use Illuminate\Support\Facades\DB;
|
|
use App\Models\BobotKriteria;
|
|
use App\Models\Rekomendasi;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Carbon\Carbon;
|
|
use App\Models\PerbandinganKriteria;
|
|
use App\Models\ConsistencyRatioAlternatif;
|
|
use App\Traits\KriteriaTrait;
|
|
|
|
class AlternatifController extends Controller
|
|
{
|
|
use KriteriaTrait;
|
|
|
|
// 1. Tampilkan form pemilihan alternatif berdasarkan rekomendasi ahli
|
|
public function formPilihAlternatif(Request $request)
|
|
{
|
|
// Cek apakah tahap kriteria sudah selesai
|
|
if (!session('tahap_kriteria_selesai')) {
|
|
return redirect()->route('perbandingan')
|
|
->with('error', 'Selesaikan perhitungan bobot kriteria terlebih dahulu.');
|
|
}
|
|
|
|
$komponens = Komponen::all();
|
|
$waktuMakans = WaktuMakan::all();
|
|
|
|
// Get filter parameters
|
|
$waktuMakanId = $request->waktu_makan_id;
|
|
$search = $request->q;
|
|
|
|
// Get expert recommendations and available foods
|
|
$rekomendasiAhli = collect();
|
|
$makanans = collect();
|
|
|
|
if ($waktuMakanId) {
|
|
// Get expert recommendations for all components with eager loading
|
|
$rekomendasiAhli = RekomendasiAhli::with(['makanan', 'komponen', 'waktuMakan'])
|
|
->where('waktu_makan_id', $waktuMakanId)
|
|
->get();
|
|
|
|
// Get all foods from pivot table makanan_komponen_waktu
|
|
$query = MakananKomponenWaktu::with(['makanan', 'komponen'])
|
|
->where('waktu_makan_id', $waktuMakanId);
|
|
|
|
// Try with status filter first
|
|
$makananKomponenWaktu = $query->where('status', true)->get();
|
|
|
|
// If no data with status true, try without status filter
|
|
if ($makananKomponenWaktu->isEmpty()) {
|
|
$makananKomponenWaktu = $query->get();
|
|
\Log::info('No data with status=true, using all data without status filter');
|
|
}
|
|
|
|
if ($search) {
|
|
$makananKomponenWaktu = $makananKomponenWaktu->filter(function($item) use ($search) {
|
|
return stripos($item->makanan->nama, $search) !== false;
|
|
});
|
|
}
|
|
|
|
// Debug: Check all data without status filter
|
|
$allData = MakananKomponenWaktu::with(['makanan', 'komponen'])
|
|
->where('waktu_makan_id', $waktuMakanId)
|
|
->get();
|
|
|
|
\Log::info('Debug - All data without status filter:', [
|
|
'waktu_makan_id' => $waktuMakanId,
|
|
'total_all_data' => $allData->count(),
|
|
'status_counts' => $allData->groupBy('status')->map->count(),
|
|
'all_data' => $allData->toArray()
|
|
]);
|
|
|
|
// Log untuk debugging
|
|
\Log::info('Data makanan dari pivot table:', [
|
|
'waktu_makan_id' => $waktuMakanId,
|
|
'total_makanan' => $makananKomponenWaktu->count(),
|
|
'makanan_data' => $makananKomponenWaktu->toArray()
|
|
]);
|
|
|
|
// Transform data to match expected format
|
|
$makanans = $makananKomponenWaktu->map(function($item) {
|
|
$makanan = $item->makanan;
|
|
$makanan->komponen_id = $item->komponen_id;
|
|
return $makanan;
|
|
});
|
|
}
|
|
|
|
return view('admin.alternatif.pilih', compact(
|
|
'rekomendasiAhli',
|
|
'makanans',
|
|
'komponens',
|
|
'waktuMakans',
|
|
'waktuMakanId',
|
|
'search'
|
|
));
|
|
}
|
|
|
|
// 2. Simpan pilihan alternatif dan hitung bobot
|
|
public function pilihAlternatif(Request $request)
|
|
{
|
|
try {
|
|
$alternatifIds = $request->input('alternatifs');
|
|
$waktuMakanId = $request->input('waktu_makan_id');
|
|
$komponenIds = $request->input('komponen_ids');
|
|
$tanggalRekomendasi = now()->toDateString();
|
|
|
|
if (!$alternatifIds || !$waktuMakanId || !$komponenIds) {
|
|
return redirect()->back()
|
|
->with('error', 'Pilih minimal 4 alternatif untuk setiap komponen.');
|
|
}
|
|
|
|
// Validasi minimal 4 alternatif per komponen
|
|
$makananKomponenWaktu = MakananKomponenWaktu::with(['makanan', 'komponen'])
|
|
->whereIn('makanan_id', $alternatifIds)
|
|
->where('waktu_makan_id', $waktuMakanId)
|
|
->get();
|
|
|
|
// Group by komponen dan cek jumlah
|
|
$alternatifPerKomponen = $makananKomponenWaktu->groupBy('komponen_id');
|
|
$errorMessages = [];
|
|
|
|
foreach ($alternatifPerKomponen as $komponenId => $items) {
|
|
if ($items->count() < 4) {
|
|
$komponen = $items->first()->komponen;
|
|
$errorMessages[] = "Komponen {$komponen->nama}: hanya {$items->count()} alternatif (minimal 4)";
|
|
}
|
|
}
|
|
|
|
if (!empty($errorMessages)) {
|
|
return redirect()->back()
|
|
->with('error', 'Validasi gagal:<br>' . implode('<br>', $errorMessages));
|
|
}
|
|
|
|
// Log request data untuk debugging
|
|
\Log::info('Request data:', [
|
|
'alternatifIds' => $alternatifIds,
|
|
'waktuMakanId' => $waktuMakanId,
|
|
'komponenIds' => $komponenIds,
|
|
'all_request' => $request->all()
|
|
]);
|
|
|
|
// Ambil data makanan yang dipilih dari pivot table
|
|
$makananKomponenWaktu = MakananKomponenWaktu::with(['makanan'])
|
|
->whereIn('makanan_id', $alternatifIds)
|
|
->where('waktu_makan_id', $waktuMakanId)
|
|
->where('status', true)
|
|
->get();
|
|
|
|
// If no data with status true, try without status filter
|
|
if ($makananKomponenWaktu->isEmpty()) {
|
|
$makananKomponenWaktu = MakananKomponenWaktu::with(['makanan'])
|
|
->whereIn('makanan_id', $alternatifIds)
|
|
->where('waktu_makan_id', $waktuMakanId)
|
|
->get();
|
|
\Log::info('No data with status=true in pilihAlternatif, using all data without status filter');
|
|
}
|
|
|
|
// Group makanan berdasarkan komponen
|
|
$alternatifsByKomponen = [];
|
|
foreach ($makananKomponenWaktu as $item) {
|
|
$makanan = $item->makanan;
|
|
$komponenId = $item->komponen_id;
|
|
|
|
if (!isset($alternatifsByKomponen[$komponenId])) {
|
|
$komponen = Komponen::find($komponenId);
|
|
$alternatifsByKomponen[$komponenId] = [
|
|
'nama_komponen' => $komponen->nama,
|
|
'alternatifs' => []
|
|
];
|
|
}
|
|
|
|
// Simpan data makanan dalam format array
|
|
$alternatifsByKomponen[$komponenId]['alternatifs'][] = [
|
|
'id' => $makanan->id,
|
|
'nama' => $makanan->nama,
|
|
'energi' => $makanan->energi,
|
|
'lemak' => $makanan->lemak,
|
|
'karbohidrat' => $makanan->karbohidrat,
|
|
'natrium' => $makanan->natrium,
|
|
'lemak_invers' => $makanan->lemak > 0 ? 1 / $makanan->lemak : 0,
|
|
'natrium_invers' => $makanan->natrium > 0 ? 1 / $makanan->natrium : 0
|
|
];
|
|
}
|
|
|
|
// Log data untuk debugging
|
|
\Log::info('Data alternatif yang akan disimpan:', [
|
|
'alternatifs_by_komponen' => $alternatifsByKomponen,
|
|
'waktu_makan_id' => $waktuMakanId,
|
|
'tanggal_rekomendasi' => $tanggalRekomendasi
|
|
]);
|
|
|
|
// Simpan ke session
|
|
session([
|
|
'alternatifs_by_komponen' => $alternatifsByKomponen,
|
|
'waktu_makan_id' => $waktuMakanId,
|
|
'hasil_rekomendasi' => [
|
|
'waktu_makan_id' => $waktuMakanId,
|
|
'tanggal_rekomendasi' => $tanggalRekomendasi,
|
|
'waktu_makan_nama' => WaktuMakan::find($waktuMakanId)->nama,
|
|
'alternatifs_dipilih' => $alternatifIds
|
|
]
|
|
]);
|
|
|
|
return redirect()->route('alternatif.view')
|
|
->with('success', 'Alternatif berhasil dipilih.');
|
|
|
|
} catch (\Exception $e) {
|
|
\Log::error('Error in simpanPilihan:', [
|
|
'message' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
|
|
return redirect()->back()
|
|
->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function viewAlternatif()
|
|
{
|
|
try {
|
|
// Ambil data dari session
|
|
$alternatifsByKomponen = session('alternatifs_by_komponen');
|
|
$hasilRekomendasi = session('hasil_rekomendasi');
|
|
|
|
if (!$hasilRekomendasi || !$alternatifsByKomponen) {
|
|
return redirect()->route('alternatif.pilih')
|
|
->with('error', 'Silakan pilih alternatif terlebih dahulu');
|
|
}
|
|
|
|
// Log data untuk debugging
|
|
\Log::info('Data untuk view:', [
|
|
'alternatifs_by_komponen' => $alternatifsByKomponen,
|
|
'hasil_rekomendasi' => $hasilRekomendasi
|
|
]);
|
|
|
|
return view('admin.alternatif.view', compact('alternatifsByKomponen', 'hasilRekomendasi'));
|
|
} catch (\Exception $e) {
|
|
\Log::error('Error in viewAlternatif:', [
|
|
'message' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
|
|
return redirect()->route('alternatif.pilih')
|
|
->with('error', 'Terjadi kesalahan saat memproses data: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
// 3. Hitung bobot alternatif
|
|
private function hitungBobotAlternatif($alternativesByComponent, $kriterias, $waktuMakanId)
|
|
{
|
|
try {
|
|
// Random Index values
|
|
$RI = [
|
|
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, 10 => 1.49,
|
|
11 => 1.51, 12 => 1.48, 13 => 1.56, 14 => 1.57, 15 => 1.59
|
|
];
|
|
|
|
// Ambil bobot kriteria berdasarkan waktu makan
|
|
$bobotKriteria = [];
|
|
foreach ($kriterias as $kriteria) {
|
|
$bobot = BobotKriteria::where([
|
|
'kriteria_id' => $kriteria->id,
|
|
'waktu_makan_id' => $waktuMakanId
|
|
])->first();
|
|
|
|
if (!$bobot) {
|
|
throw new \Exception("Bobot kriteria untuk {$kriteria->nama} pada waktu makan ini belum dihitung.");
|
|
}
|
|
$bobotKriteria[$kriteria->id] = $bobot->bobot;
|
|
}
|
|
|
|
foreach ($alternativesByComponent as $currentKomponenId => $komponenAlternatifs) {
|
|
// Untuk setiap kriteria
|
|
foreach ($kriterias as $kriteria) {
|
|
// 1. Buat matriks perbandingan berpasangan untuk komponen ini
|
|
$matriksPerbandingan = [];
|
|
|
|
// Isi matriks perbandingan
|
|
foreach ($komponenAlternatifs as $alt1) {
|
|
foreach ($komponenAlternatifs as $alt2) {
|
|
$nilai1 = $alt1->{strtolower($kriteria->nama)};
|
|
$nilai2 = $alt2->{strtolower($kriteria->nama)};
|
|
|
|
// Untuk kriteria cost (lemak dan natrium), gunakan nilai inverse
|
|
if ($this->isCostCriteria($kriteria->nama)) {
|
|
$nilai1 = $nilai1 > 0 ? 1 / $nilai1 : 0;
|
|
$nilai2 = $nilai2 > 0 ? 1 / $nilai2 : 0;
|
|
}
|
|
|
|
if ($nilai2 == 0) {
|
|
$matriksPerbandingan[$alt1->id][$alt2->id] = 0;
|
|
} else {
|
|
$matriksPerbandingan[$alt1->id][$alt2->id] = $nilai1 / $nilai2;
|
|
}
|
|
|
|
// Simpan perbandingan ke database
|
|
PerbandinganAlternatif::updateOrCreate(
|
|
[
|
|
'alternatif_id_1' => $alt1->id,
|
|
'alternatif_id_2' => $alt2->id,
|
|
'kriteria_id' => $kriteria->id,
|
|
'waktu_makan_id' => $waktuMakanId,
|
|
'komponen_id' => $currentKomponenId
|
|
],
|
|
['nilai' => $matriksPerbandingan[$alt1->id][$alt2->id]]
|
|
);
|
|
}
|
|
}
|
|
|
|
// 2. Hitung jumlah kolom
|
|
$jumlahKolom = [];
|
|
foreach ($komponenAlternatifs as $alt2) {
|
|
$jumlahKolom[$alt2->id] = 0;
|
|
foreach ($komponenAlternatifs as $alt1) {
|
|
$jumlahKolom[$alt2->id] += $matriksPerbandingan[$alt1->id][$alt2->id];
|
|
}
|
|
}
|
|
|
|
// 3. Normalisasi matriks
|
|
$matriksNormal = [];
|
|
foreach ($komponenAlternatifs as $alt1) {
|
|
foreach ($komponenAlternatifs as $alt2) {
|
|
if ($jumlahKolom[$alt2->id] > 0) {
|
|
$matriksNormal[$alt1->id][$alt2->id] =
|
|
$matriksPerbandingan[$alt1->id][$alt2->id] / $jumlahKolom[$alt2->id];
|
|
} else {
|
|
$matriksNormal[$alt1->id][$alt2->id] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Hitung prioritas lokal
|
|
$priorityVector = [];
|
|
foreach ($komponenAlternatifs as $alt) {
|
|
$priorityVector[$alt->id] = array_sum($matriksNormal[$alt->id]) / count($komponenAlternatifs);
|
|
}
|
|
|
|
// 5. Hitung CI dan CR
|
|
$lambdaMax = 0;
|
|
foreach ($komponenAlternatifs as $alt) {
|
|
$sum = 0;
|
|
foreach ($komponenAlternatifs as $alt2) {
|
|
$sum += $matriksPerbandingan[$alt->id][$alt2->id] * $priorityVector[$alt2->id];
|
|
}
|
|
if ($priorityVector[$alt->id] > 0) {
|
|
$lambdaMax += $sum / $priorityVector[$alt->id];
|
|
}
|
|
}
|
|
$lambdaMax = $lambdaMax / count($komponenAlternatifs);
|
|
|
|
$CI = ($lambdaMax - count($komponenAlternatifs)) / (count($komponenAlternatifs) - 1);
|
|
$CR = $CI / ($RI[count($komponenAlternatifs)] ?? 1.59);
|
|
|
|
// Simpan CR ke database
|
|
ConsistencyRatioAlternatif::updateOrCreate(
|
|
[
|
|
'kriteria_id' => $kriteria->id,
|
|
'waktu_makan_id' => $waktuMakanId,
|
|
'komponen_id' => $currentKomponenId
|
|
],
|
|
[
|
|
'ci' => $CI,
|
|
'cr' => $CR,
|
|
'is_consistent' => $CR <= 0.1
|
|
]
|
|
);
|
|
|
|
// Simpan skor akhir ke database
|
|
foreach ($komponenAlternatifs as $alt) {
|
|
SkorMakanan::updateOrCreate(
|
|
[
|
|
'makanan_id' => $alt->id,
|
|
'kriteria_id' => $kriteria->id,
|
|
'waktu_makan_id' => $waktuMakanId,
|
|
'komponen_id' => $currentKomponenId
|
|
],
|
|
['nilai' => $priorityVector[$alt->id] * $bobotKriteria[$kriteria->id]]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} catch (\Exception $e) {
|
|
\Log::error('Error in hitungBobotAlternatif: ' . $e->getMessage());
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
// 4. Tampilkan form perbandingan alternatif
|
|
public function tampilPerbandingan()
|
|
{
|
|
try {
|
|
// Ambil data dari session
|
|
$alternatifsByKomponen = session('alternatifs_by_komponen');
|
|
|
|
if (!$alternatifsByKomponen) {
|
|
return redirect()->route('alternatif.view')
|
|
->with('error', 'Silakan lihat data alternatif terlebih dahulu');
|
|
}
|
|
|
|
// Ambil semua kriteria
|
|
$kriterias = \App\Models\Kriteria::all();
|
|
|
|
// Data sudah siap di session, tampilkan view dengan data kriteria
|
|
return view('admin.alternatif.perbandingan', [
|
|
'alternatifsByKomponen' => $alternatifsByKomponen,
|
|
'kriterias' => $kriterias
|
|
]);
|
|
|
|
} catch (\Exception $e) {
|
|
\Log::error('Error in tampilPerbandingan: ' . $e->getMessage());
|
|
return redirect()->route('alternatif.view')
|
|
->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function simpanNormalisasi(Request $request)
|
|
{
|
|
try {
|
|
// Ambil data normalisasi dari request dalam format JSON
|
|
$normalisasiDataJson = $request->input('normalisasi_data_json');
|
|
|
|
if (!$normalisasiDataJson) {
|
|
return redirect()->back()->with('error', 'Data normalisasi tidak ditemukan');
|
|
}
|
|
|
|
// Decode JSON data
|
|
$normalisasiData = json_decode($normalisasiDataJson, true);
|
|
|
|
if (!$normalisasiData || !isset($normalisasiData['data'])) {
|
|
return redirect()->back()->with('error', 'Format data normalisasi tidak valid');
|
|
}
|
|
|
|
// Simpan ke session
|
|
session(['alternatifs_by_komponen' => $normalisasiData['data']]);
|
|
|
|
// Log untuk debugging
|
|
\Log::info('Data normalisasi disimpan:', [
|
|
'normalisasi_data' => $normalisasiData['data']
|
|
]);
|
|
|
|
return redirect()->route('rekomendasi.hitung.otomatis')
|
|
->with('success', 'Data normalisasi berhasil disimpan');
|
|
|
|
} catch (\Exception $e) {
|
|
\Log::error('Error in simpanNormalisasi:', [
|
|
'message' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
|
|
return redirect()->back()
|
|
->with('error', 'Terjadi kesalahan: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function hitungNilaiPerbandingan($alt1, $alt2, $kriteria)
|
|
{
|
|
switch ($kriteria->kode) {
|
|
case 'C1': // Lemak (cost)
|
|
return $alt2['lemak'] > 0 ? $alt1['lemak_invers'] / $alt2['lemak_invers'] : 1;
|
|
|
|
case 'C2': // Natrium (cost)
|
|
return $alt2['natrium'] > 0 ? $alt1['natrium_invers'] / $alt2['natrium_invers'] : 1;
|
|
|
|
case 'C3': // Energi (benefit)
|
|
return $alt2['energi'] > 0 ? $alt1['energi'] / $alt2['energi'] : 1;
|
|
|
|
case 'C4': // Karbohidrat (benefit)
|
|
return $alt2['karbohidrat'] > 0 ? $alt1['karbohidrat'] / $alt2['karbohidrat'] : 1;
|
|
|
|
default:
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
private function getNilaiKriteria($makanan, $kriteria)
|
|
{
|
|
switch (strtolower($kriteria->nama)) {
|
|
case 'energi':
|
|
return $makanan->energi;
|
|
case 'lemak':
|
|
return $makanan->lemak;
|
|
case 'karbohidrat':
|
|
return $makanan->karbohidrat;
|
|
case 'natrium':
|
|
return $makanan->natrium;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private function normalisasiNilai($nilai, $kriteria, $makanans)
|
|
{
|
|
// Hitung jumlah total untuk kriteria ini
|
|
$total = 0;
|
|
foreach ($makanans as $makanan) {
|
|
$nilaiKriteria = $this->getNilaiKriteria($makanan, $kriteria);
|
|
$total += $nilaiKriteria;
|
|
}
|
|
|
|
// Normalisasi sesuai perhitungan manual (nilai/jumlah)
|
|
// Hindari pembagian dengan 0
|
|
return $total > 0 ? $nilai / $total : 0;
|
|
}
|
|
}
|